Files
ai-resume/docs/content-schema.md
Sam Rolfe 0eb151117e Phase 1a: content schema validator (freeze the schema)
Implements AGENTS.md section 7 front-matter schema as a single-file,
standard-library-only Python validator; no third-party YAML dependency
and no framework. Adds the `make validate` gate, a 20-case fixture set
and docs/content-schema.md.

DoD satisfied:
- make validate runs and exits 0 on an empty/clean content/ tree
  (missing dir and empty dir both exit 0; fixture ok-empty covers it).
- Every violation class has a fixture under test/fixtures/fail-*; each
  exits non-zero with one line per violation (file:line: field: rule).
  `make test` runs all 20 cases: 20/20 pass.
- Dangling artefact reference is fatal (fail-dangling-artefact;
  artefacts[].url must resolve to an existing in-repo L3 stub or asset).
- Unknown front-matter field is fatal (fail-unknown-field), including
  unknown keys nested inside artefacts[]/metrics[].
- docs/content-schema.md committed; states the frozen schema, links to
  AGENTS.md section 7, and records that any later schema change is a
  breaking change requiring Sam's approval.
- Scope respected: no content authored, no rendering, no site code.

Files: Makefile, tools/validate_content.py, test/fixtures/**,
docs/content-schema.md, .gitignore (__pycache__).
2026-10-26 17:26:00 +11:00

9.1 KiB
Raw Permalink Blame History

Content schema (frozen)

This document states the front-matter schema that every file in content/ must carry, and the rules the validator enforces. The schema is the one in AGENTS.md §7. Read that section first; this page is the exact, testable restatement of it.

The schema is frozen. Any change to the fields, their types, their enum values or the artefact rules is a breaking change: it invalidates every existing file in content/ and both renderers. A change requires Sam's written approval before it is made. Do not widen or narrow the schema to make a document validate — fix the document.


1. How to validate

make validate     # the gate: exits 0 only on a clean tree
make test         # validator self-test, 20 fixture cases

make validate is the command CI and deploy.sh call in later phases. Run it before every content commit.

Both targets invoke:

python3 tools/validate_content.py [--root DIR] [--content DIR] [--quiet]

Exit codes: 0 clean (also when content/ does not exist yet), 1 at least one violation, 2 usage or internal error.


2. Document shape

  • Every .md file under content/ is a document and is validated. Other file types under content/ (images, SVG, PDF) are assets and are not validated — they exist to be referenced.
  • A document starts with a front-matter block on line 1:
---
<fields>
---
  • The closing delimiter is --- (or ...). Everything after it is the Markdown body and is not validated.
  • One document per file. One front-matter block per file.
  • content/ is organised by area:
content/
  ai/  platform/  edge/  products/  history/  about/   # documents, plus l3/ below each
  assets/                                              # diagrams, screenshots, logos

3. Fields

All thirteen fields are required on every document, including L3 stubs (which use empty lists). Order does not matter.

Field Type Rule
id string Lowercase kebab-case: a-z, 0-9, single hyphens between parts. Must equal the file name without .md. Unique across all of content/.
area enum One of ai, platform, edge, products, history, about.
level integer 1, 2 or 3 (depth level: L1 landing, L2 capability, L3 artefact).
title string Non-empty. Corporate capability framing (see AGENTS.md §7); product names are evidence, not headings.
claim string Non-empty. One sentence a hiring manager can verify.
confidence enum One of delivered, running, in-progress, abandoned, historic. Displayed on the page. Never promote a value to make a page read better.
period string Non-empty. Free text, e.g. "2023–", "2019–2024", "2000–2014".
tech list of strings Non-empty strings. May be []. Product and technology names belong here, not in title.
metrics list of maps May be []. Each item is exactly {label, value}. Both non-empty. value may be a number (8) or a string ("50+"). Every value must trace to the evidence notes — the validator checks shape, not truth.
artefacts list of maps May be []. Each item is exactly {type, label, url}. See §4.
public boolean true or false. false keeps the document out of the published output.
last_verified date YYYY-MM-DD and a real calendar date.
tags list of strings Non-empty strings. May be [].

Unknown fields are rejected — at the top level and inside metrics[] and artefacts[]. There is no extension point. A field that an author thinks is useful is a schema change (§6).

The one exception to "all required": nothing. tech: [], metrics: [], artefacts: [] and tags: [] are the correct way to say "this document has none". An omitted field is a violation; an empty list is not.


Every artefacts[].url must resolve to a file that exists in this repository. This is the rule that keeps every claim one step away from its proof.

url form Resolves against Example
content/... or any path without a leading ./ or ../ the repository root content/edge/l3/audio-architecture.md
./... or ../... the directory of the document ./l3/audio-architecture.md

Rules:

  • The target must be an existing file — an L3 stub document (.md) or an asset (.svg, .png, .pdf, …). A directory is a violation.
  • External URLs (https://…) are not allowed. If the artefact lives outside the repository (a Gitea repo, a video), create an in-repo L3 stub document that describes and links it, and point url at that stub. This keeps every drill-down verifiable offline and keeps Mode A free of third-party dependencies.
  • Root-absolute paths (/content/...) are not allowed — they do not survive a release directory move.
  • A path that escapes the repository is a violation.

type is one of repo, image, video, doc and describes the artefact behind the link, not the file extension of the stub.


5. Cross-document rules

  • id is unique across content/. Two files with the same id are a violation on the second file.
  • id matches the file name. content/edge/snapcast.md must have id: snapcast; an L3 stub content/edge/l3/snapcast-code.md must have id: snapcast-code.

6. Changing the schema

Breaking (requires Sam's written approval, then a migration of every file):

  • adding, removing or renaming a field
  • changing a field's type
  • adding or removing an enum value (area, level, confidence, artefact type)
  • relaxing or tightening a rule (e.g. allowing external artefact URLs)

Not a schema change: adding documents, filling empty lists, adding tags or metrics within the existing shape.

A change that supersedes this document must update, in one commit: AGENTS.md §7, this file, tools/validate_content.py, and the fixtures under test/fixtures/.


7. Error output

One line per violation, on stdout:

<path>:<line>: <field>: <rule broken>

Examples:

content/edge/audio.md:14: artefacts[0].url: dangling reference: 'content/edge/l3/not-written-yet.md' does not exist (create the L3 stub or asset)
content/edge/audio.md:16: author: unknown field (the schema is frozen; see docs/content-schema.md)
content/edge/audio.md:3: area: must be one of ai, platform, edge, products, history, about
content/edge/audio.md:1: claim: missing required field

A missing field is reported on line 1, because there is no line that carries it. A malformed front-matter block is reported as <front-matter>: invalid YAML: … and stops validation of that document only.

The final line is either FAIL: N violation(s) in M document(s) or OK: M document(s) validated, 0 violations.


8. Front-matter dialect the parser accepts

The validator contains its own small YAML-subset parser so the repository needs no third-party dependency (AGENTS.md §4: the stack is frozen, and the .27 build host has no Go toolchain yet). It accepts:

  • block mappings, block sequences, and flow collections: tech: [a, b], metrics: [{label: "endpoints", value: 8}]
  • single- and double-quoted scalars, plain scalars
  • literal (|) and folded (>) block scalars, including |- / >-
  • # comments outside quoted strings
  • blank lines

It does not accept: anchors/aliases (&, *), explicit tags (!!str), multi-document files, complex keys, tab indentation. Use two or four spaces, consistently within a block.

Two consequences worth knowing:

  • A quoted string must be closed. claim: "text # not a comment is an error, not a comment.
  • # after a space ends a plain scalar, as in YAML. Quote values that contain #.

9. What the validator does not check

Enforced elsewhere in Phase 1, and reviewed by Sam:

  • that a metric value appears in the evidence notes (no invented numbers)
  • that confidence matches reality — the validator cannot know; it only forces an honest label to be present
  • the redaction pass (no family names, address, phone, credentials)
  • that every L1 claim has a metric and at least one L3 artefact
  • corporate framing of headings

10. Implementation and tests

Path Purpose
tools/validate_content.py The validator. One file, Python 3 standard library only, no framework.
Makefile validate and test targets.
test/fixtures/<case>/content/ Fixture trees. ok-* cases must pass, fail-* cases must fail.
test/fixtures/<case>/expect.txt Substrings that fail-* output must contain, one per line.

Fixture cases cover: missing field, area enum, level enum, confidence enum, last_verified format and calendar validity, empty metrics[].label, artefact type enum, artefact missing url, dangling artefact, external artefact URL, duplicate id, id/filename mismatch, unknown top-level field, unknown nested artefact field, missing front-matter, malformed YAML. ok-empty proves an empty content/ exits 0; ok-block-scalars proves the block-scalar and block-sequence dialects.

Fixtures live under test/, never under content/.