Skip to content

Schema check at spec birth (SPEC-227 FR-2)

This file is the single source for the birth check. spec-from-requirements and specs-from-prd both follow it, right after the spec's files and its registry entry are written.

Do not paste this procedure into a skill body — reason: a procedure pasted into several skills drifts, and a structural test can only see that each copy names a branch, not that the branches still agree (SPEC-195 review pass 11, resolver-call.md). One copy, cited, cannot drift.

Why it runs

The templates validate (SPEC-212 SC-2), but what an author or a writer puts into them does not have to. On 2026-10-28 spec-status-write.py --create wrote a created: line into a registry entry (BUG-362), and the spec was born with a schema violation that only guard 126 saw, after the fact. The check runs at birth so the author sees the row while the content is still in front of them.

The call

Run the checker on the new spec directory, named relative to the repository root (<spec folder>[N]-kebab-title, the folder the skill just wrote):

python3 "$CLAUDE_PLUGIN_ROOT/../vt-base/bin/spec-schema-check" --root "$(git rev-parse --show-toplevel)" \
  <spec folder>[N]-kebab-title

If $CLAUDE_PLUGIN_ROOT is not set, locate the checker relative to the calling skill's file at ../../../vt-base/bin/spec-schema-check, resolving the skill file's real path first (deployed skills are symlinks into the toolkit checkout). Never call it by a repo-relative plugins/vt-base/… path — reason: the skill runs in the project, so that path exists only when the project is the toolkit (BUG-342).

If neither path resolves (EC-1), report both paths you tried and say that the schema check did not run: the summary value is Schema: not checked (checker not found). Never report the spec as schema-clean in that case — reason: an absent checker is not an absent violation.

Vet the directory before rendering it: it must match ^[A-Za-z0-9._/-]+$ end to end, without a .. segment — reason: it goes onto a command line, and a spec folder is repository text. If it fails the vet (a space or an umlaut in the folder name, for example), do not run the checker: show the name as data and report Schema: not checked (spec folder name not safe for a command line).

The outcomes

The checker validates the spec's spec.md frontmatter, its state.yaml, and its own registry entry in .design-state.yaml (single-directory mode). Read the exit code and stderr:

  • rc 0, no stderr notice → schema clean. Report Schema: PASS.
  • rc 0 with a stderr line ending in not checked (the registry is absent) → the files passed, but the entry check did not run. Report Schema: PASS (registry entry not checked) and name the notice — reason: a check that did not run did not pass. A registry that exists but cannot be read, or is not a mapping, is not this case: it prints the same notice and a .design-state.yaml row, rc 1 (below).
  • rc 1 → one stdout row per violation: dir<TAB>file<TAB>json-pointer<TAB>code<TAB>message. Show every row and report Schema: FAIL (N rows). Then apply the correction rules below and re-run the check once. A row on .design-state.yaml with an empty pointer (load-error, symlink, missing-file, type) means the registry itself is broken, so no entry was checked: report it as that, not as a defect of the new spec.
  • rc 2 → a tool fault (usage error, unenforceable schema, internal error). Report the message and Schema: not checked (tool fault). Never read rc 2 as rc 1 or as a pass.

The spec stays written in every case — reason: the check reports, it does not undo the author's work. Never delete a file to make the check pass.

Correction rules

  • The row is in content the author supplied (EC-2) — an undocumented frontmatter key from a requirements document, for example /x additional-property on spec.md. Name the key and offer to drop it or move its content into the spec body. Never widen the schema for it — reason: the schema is the contract every spec in the repository is held to.
  • The row is in what the toolkit wrote (EC-3). Tell the author it is a toolkit defect, not their mistake, and name the row and its bug id when one is listed below — reason: an author who is not told whose defect it is will take the correction for their own error and stop trusting the check.
  • /specs_status/<id>/created additional-property on .design-state.yaml is BUG-362: spec-status-write.py --create copied the frontmatter's created into the entry. Report it as "toolkit defect BUG-362" with the row, then drop the created: line from that entry only, as 5e5cda643 did for SPEC-226, then re-run.
  • Any other row in the registry entry or in state.yaml that the author did not write is an unknown writer defect: report the row for a bug report (/vt-d-bug-report). Never correct it silently — reason: a silent correction hides the writer defect from every later spec.
  • An unregistered-spec row (with the stderr notice no registry entry for …) means the entry the skill just wrote is not there: the registry has no entry with this spec's id or specs_dir. Check that the skill's registry write ran; if it did, report the row for a bug report like any other writer defect. Never add the entry by hand to clear the row — reason: the writer is the one place that keeps the entry's shape, and a hand-made entry hides why it failed.
  • A registry-mismatch row means the frontmatter id belongs to another spec: its entry's specs_dir names another directory. The id was copied, or allocated twice. Report it with the row, and give this spec its own id from the allocator. Never point the other spec's specs_dir at this directory — reason: that would make this spec validate, and be completed as, the other spec.

After the re-run, report the final result. While rows remain, the completion summary says Schema: FAIL with the rows; it never says the spec is ready.