Spec Structure Reference¶
A spec in this toolkit is three artifacts: the YAML frontmatter of specs/<N>-<slug>/spec.md, the
per-spec specs/<N>-<slug>/state.yaml, and one entry under specs_status in the registry
.design-state.yaml. Each has a JSON Schema (draft 2020-12). This page documents every field those
schemas allow, and the parts a schema cannot express: the folder layout and the spec.md body
sections.
The schemas are strict: a field that is not documented here is a violation
(additionalProperties: false). Existing specs that deviate are recorded in a shrink-only baseline;
new specs must match (SPEC-212).
Create a spec with /vt-d-spec-from-requirements or /vt-d-specs-from-prd rather than by hand:
they allocate the id and write all three artifacts. This page describes what they write.
| Artifact | Schema |
|---|---|
spec.md frontmatter |
spec-frontmatter.schema.json |
state.yaml |
spec-state.schema.json |
.design-state.yaml |
design-state.schema.json |
Folder layout¶
specs/<N>-<slug>/
├── spec.md the specification: YAML frontmatter + body sections (required)
├── state.yaml lifecycle status and gate records (required)
├── decisions.md shaping decisions from /vt-d-shape
├── research.md research notes from /vt-d-2-plan
├── data-model.md entities and fields, when the spec has any
├── plan.md implementation plan from /vt-d-2-plan
├── tasks.md task breakdown from /vt-d-2-plan
├── review-triage.md disposition of review findings
├── contracts/ interface contracts (API, CLI, file formats)
└── checklists/ spec-specific checklists
<N> is the spec number without leading zeros, <slug> a kebab-case title: SPEC-052 lives in
specs/52-<slug>/, SPEC-212 in specs/212-json-schema-for-the-spec-structure/. Spec numbers come
from the allocator
(plugins/vt-product-dev/skills/spec-from-requirements/scripts/allocate_spec_id.py), never from
counting directories.
The smallest valid spec¶
specs/999-sample/spec.md starts with this frontmatter (the --- must be the file's first line):
specs/999-sample/state.yaml:
.design-state.yaml (a new spec adds only its entry under the existing specs_status:):
The three blocks, copied as printed into those files, pass
plugins/vt-base/bin/spec-schema-check --all with exit 0.
spec.md frontmatter¶
Required: id, title, priority, created, depends_on.
| Field | Required | Type / values | Example | Meaning |
|---|---|---|---|---|
id |
yes | SPEC- + at least 3 digits |
SPEC-212 |
The spec's identifier; equals its registry key. |
title |
yes | non-empty string | JSON Schema for the spec structure |
Human-readable title. |
priority |
yes | P0 · P1 · P2 · P3 |
P1 |
Delivery priority, P0 highest. |
created |
yes | date YYYY-MM-DD |
2026-09-24 |
Creation date (quoted or unquoted). |
depends_on |
yes | list of spec ids, unique, may be empty | [SPEC-181] |
Blocking dependencies. |
status |
no, deprecated | spec lifecycle value | specified |
Historical copy of the status. The authoritative status is in state.yaml and the registry; SPEC-181 removes it from the templates. |
ui_scope |
no | frontend · backend · mixed (default backend) |
backend |
Whether the spec touches UI; frontend/mixed need a ## Visual Reference section. |
source |
no | a path, or a non-empty list of paths | intake/pending/from-rca/x.md |
Input document(s) the spec came from. |
deliverable_types |
no | non-empty list of deliverable types (default [code]) |
[code, test, document] |
What the spec delivers; decides which reviewers run. |
parent_prd |
no | string | docs/prd/prd-v1.md |
PRD a spec was generated from (/vt-d-specs-from-prd). |
tags |
no | list of strings | [schema, registry] |
Free keywords; nothing interprets them. |
soft_depends_on |
no | list of spec ids | [SPEC-200] |
Non-blocking dependencies. |
Not allowed (baselined where they exist): the legacy keys spec_id and dependencies, the
source_* variants, and every other key.
state.yaml¶
Required: status. Every object below is strict too.
| Field | Required | Type / values | Example | Meaning |
|---|---|---|---|---|
status |
yes | spec lifecycle value | in_progress |
Authoritative lifecycle status; must equal the registry's (guard 54, scripts/tests/setup/54-spec-status-is-declared-and-agrees.sh). |
activated_at |
no | date or date-time, or null |
"2026-09-24T17:39:35" |
When /vt-d-activate started the spec. |
completed_at |
no | date or date-time, or null |
null |
When the spec was completed. |
docs_page |
no | string | docs/user-manual/docs/features/x.md |
Documentation page this spec added or changed. |
rejection_reason |
no | string | superseded by SPEC-200 |
Why the spec was rejected. |
assignee |
no | string | rolf |
Who works on the spec. |
steps |
no | phase number → pending · in_progress · completed |
{1: completed, 2: in_progress} |
Build progress per tasks.md phase. |
spec_check_gate |
no | gate object | see below | Birth-gate record (/vt-d-spec-check). |
shape_gate |
no | gate object | see below | Shaping record (/vt-d-shape). |
plan_gate |
no | gate object | see below | Plan validation record. |
build_gate |
no | gate object | see below | Build record. |
test_gate |
no | gate object | see below | Test gate record. |
review_gate |
no | gate object | see below | Review record. |
finalize_gate |
no | gate object | see below | Finalization record. |
gate_bypass |
no | object | see below | Deliberately skipped gates (/vt-d-complete). |
gate_history |
no | list of history items | see below | Earlier passes of any gate, oldest first; the gate object holds the latest. |
Gate objects¶
Every gate object may carry status, notes (free-text reasoning — status stays a vocabulary
value), validator (agent · inline, SPEC-189) and validators_run (list of agent names).
status is required except in shape_gate, where a skipped shaping writes only skipped: true.
| Field | Required | Type / values | Example | Meaning |
|---|---|---|---|---|
notes |
no | string | re-validated after rev. 3 |
Explanation of the verdict. |
validator |
no | agent · inline |
agent |
Who produced the verdict; absent reads as unknown. |
validators_run |
no | list of strings | [plan-checker, strategic-mentor] |
Validator agents that actually ran. |
spec_check_gate.status |
yes | RUN |
RUN |
The birth gate ran (it never blocks). |
spec_check_gate.date |
no | date | 2026-09-24 |
When it ran. |
spec_check_gate.spec_sha256 |
no | 64 hex characters | 9422a2de… (shortened here) |
SHA-256 of spec.md at that time. |
shape_gate.date |
no | date | 2026-09-24 |
When shaping completed. |
shape_gate.decisions |
no | integer ≥ 0 | 7 |
Decisions locked in decisions.md. |
shape_gate.skipped |
no | boolean | true |
Shaping was skipped. |
shape_gate.reason |
no | string | small spec |
Why it was skipped. |
plan_gate.checked_at |
no | date | 2026-09-24 |
When the plan was validated. |
plan_gate.principles_checked |
no | list of strings | [I, II, III, IV] |
Constitution principles checked. |
plan_gate.violations |
no | list | [] |
Constitution violations found. |
plan_gate.artifacts |
no | list of strings | [plan.md, tasks.md] |
Planning artifacts produced. |
build_gate.date |
no | date | 2026-09-24 |
When the build gate was set. |
build_gate.commits |
no | integer ≥ 0 | 12 |
Commits on the branch at that time. |
build_gate.verification |
no | object | see next row | Test verification result. |
build_gate.verification.command · .iterations · .status · .skipped · .reason |
no | string · integer 1–5 · pass · boolean · string |
npm test, 2, pass |
The convergence loop's outcome. |
review_gate.date · .branch · .head |
no | date · string · commit id (7–40 lowercase hex, write it quoted) | 2026-09-24, feature/x, "6a0fedb2c" |
What was reviewed, when. An unquoted all-digit SHA loads as a number and fails. |
review_gate.critical · .high · .medium · .low |
no | integer ≥ 0 | 0 |
Findings left open, per severity (D15). |
review_gate.completeness |
no | COMPLETE, INCOMPLETE, COMPLETE_FOR_REDUCED_ROSTER |
COMPLETE |
SPEC-192: whether the pass finished. Any uncovered reviewer forces INCOMPLETE, and an INCOMPLETE pass is never PASS — it is FAIL, or explicitly ungraded. COMPLETE_FOR_REDUCED_ROSTER is a legacy value recorded once by SPEC-193, listed so that record stays valid; new passes do not write it. |
review_gate.pass |
no | integer ≥ 1 | 11 |
This branch's review passes so far, +1 each time (SPEC-192). |
review_gate.reviewers_unmeasured · .reviewers_deferred · .reviewers_failed (all array only) · .reviewers_outstanding (array, or a legacy integer) |
no | array of names; reviewers_outstanding additionally accepts an integer |
[], [security-sentinel] |
SPEC-192 FR-7: the per-reviewer record must survive the gitignored gate file. These are the FOUR uncovered states, each of which forces completeness: INCOMPLETE — unmeasured returned without executing anything, outstanding has not returned, deferred was dispatched and then stood down, failed errored or hit Step 2's time bound. ⚠ deferred and failed were absent here until pass 14, so the honest record naming them was REJECTED (additional-property) while the record that drops the names was accepted: FR-7's axis held for two states and was lost for the other two, and /vt-d-complete read two keys that could not exist in the file it reads. An integer is the pre-SPEC-192 count form, accepted only so dated records (SPEC-170, SPEC-183) stay valid without being rewritten, and only for outstanding. |
review_gate.degraded_reason |
no | string | security-sentinel could not run the suite |
Why the pass is INCOMPLETE, verbatim (SPEC-192 pass 6 H-7). Omitted when nothing is uncovered. |
finalize_gate.date · .version |
no | date · string | 2026-09-24, 6.4.0 |
When and which version. |
finalize_gate.checks_passed · .checks_failed |
no | integer ≥ 0 | 14, 0 |
Finalize check counts. |
gate_bypass records gates skipped on purpose:
| Field | Required | Type / values | Example | Meaning |
|---|---|---|---|---|
gate_bypass.date |
yes | date | 2026-05-06 |
When the gates were skipped. |
gate_bypass.reason |
yes | string | cross-repo duplicate |
Why. |
gate_bypass.skipped_gates |
no | list of strings | [plan_gate, build_gate] |
Which gates. |
gate_history¶
A gate object always holds its latest pass — that is the block /vt-d-complete reads. When
/vt-d-4-review runs again, the review_gate pass it replaces moves here, and the new pass is
written into review_gate (D12). Only the fields listed below move as fields — and the list
below is the whole of it, including the six SPEC-192 audit fields (completeness,
reviewers_unmeasured, reviewers_outstanding, reviewers_deferred, reviewers_failed,
degraded_reason). Until pass 13 this table omitted the first four while this sentence claimed to
be exhaustive, so a writer following the page demoted completeness into prose and destroyed the
FR-7 axis on every move. ⚠ And this sentence then said "four" for a day after pass 14 added the
last two rows to the table below (pass 14, data-integrity M-2) — the inverse of the same defect,
two lines from where it is recorded. A10 cannot catch it: it only checks that the page NAMES every
schema field, which the new rows do, so a parenthetical that under-counts the table it introduces
is invisible to it. Count the rows below before trusting this list. Anything NOT
listed below (legacy pass_number, note, a list-valued notes, …) becomes
field: value lines in the entry's notes (D16, rule in /vt-d-4-review Step 6c). A pass is never recorded as a new top-level
key (review_gate_pass_2, remediation_pass_6, … are violations).
Scope today: the history item carries the review gate's audit fields (below). The plan, build and
finalize writers still overwrite their block on a re-run, and their gate-specific fields
(plan_gate.artifacts, build_gate.verification, finalize_gate.version, …) have no place in a
history item yet; a skipped shape_gate has no status to record. Items:
| Field | Required | Type / values | Example | Meaning |
|---|---|---|---|---|
gate |
yes | spec_check_gate, shape_gate, plan_gate, build_gate, test_gate, review_gate, finalize_gate |
review_gate |
Which gate. |
pass |
yes | integer ≥ 1 | 2 |
Pass number. |
status |
yes | gate vocabulary value | PASS |
Verdict of this pass. |
date |
no | date | 2026-09-25 |
When. |
branch · head |
no | string · commit id (quoted) | feature/x, "b1e590ad5" |
What this pass judged. |
critical · high · medium · low |
no | integer ≥ 0 | 0, 1 |
Findings left open after this pass, per severity. |
validator · validators_run |
no | agent or inline · list of strings |
agent, [security-sentinel] |
Who produced the verdict (SPEC-189). |
completeness |
no | COMPLETE, INCOMPLETE, COMPLETE_FOR_REDUCED_ROSTER |
COMPLETE |
SPEC-192 FR-7: whether THAT pass finished. Moves as a FIELD, never into notes — demoting it to prose destroys the only machine-readable record that the pass was degraded. |
reviewers_unmeasured |
no | array of names | [], [security-sentinel] |
SPEC-192 FR-7: returned without executing anything. Array only — no corpus record carries the integer form. |
reviewers_outstanding |
no | array of names, or a legacy integer count | [], 2 |
SPEC-192 FR-7: dispatched and never returned. The integer is the pre-SPEC-192 count form. ⚠ This is the gate_history row, and NO gate_history item in the corpus carries the integer — re-measured 2026-10-06 over specs/*/state.yaml: 2 hits, both in review_gate (SPEC-170, SPEC-183), 0 in history. The integer is admitted here for the day one of those review_gate blocks is MOVED into history by Step 6c, which is the one path that would otherwise reject a record for being faithfully archived. Until pass 14 this row said it was "kept so SPEC-183's dated record stays valid", which named a record that is not in history (document-quality Low); the sibling reviewers_unmeasured row correctly says no corpus record carries the integer. |
reviewers_deferred |
no | array of names | [], [angular-reviewer] |
SPEC-192 FR-7: dispatched and then stood down for this pass — operationally, repeatedly unschedulable on capacity. workflow-4-review Step 2.9 is the single definition; this row and the schema description defer to it. ⚠ Never "not dispatched": the five lists partition reviewers, which IS the dispatched set (pass 14, M7). Array only — there is no legacy integer form, because the key could not be written at all until pass 14. |
reviewers_failed |
no | array of names | [], [security-sentinel] |
SPEC-192 FR-7: errored, or stopped at Step 2's 30-minute bound. Array only — see reviewers_deferred. |
degraded_reason |
no | string | security-sentinel could not run the suite |
Why that pass was INCOMPLETE, verbatim. |
notes |
no | string | closed H1, H2 |
What the pass found or fixed. |
status takes the gate vocabulary only, so a spec_check_gate re-run (whose status is RUN)
cannot be recorded here today.
gate_history:
- gate: review_gate
pass: 1
status: FAIL
date: "2026-09-25"
validator: agent
branch: feature/spec-212-spec-json-schema
head: "b1e590ad5"
critical: 0
high: 2
completeness: COMPLETE
reviewers_unmeasured: []
notes: "pass 1; review_gate now holds pass 2"
.design-state.yaml¶
The file may contain current_phase, specs_directory, specs_status, bugchen_status and the
deprecated specchen_status. Bug records under bugchen_status are out of scope and checked only
as an object — but YAML-level errors (duplicate-key, load-error) are reported anywhere in the
file, and a load-error stops the whole registry check.
| Field | Required | Type / values | Example | Meaning |
|---|---|---|---|---|
current_phase |
no | string | development |
Workflow phase of the repository. |
specs_directory |
no | string | specs/ |
Directory holding the spec folders. |
specs_status |
no | spec id → entry | see below | The spec registry. |
bugchen_status |
no | object | — | Bug registry (opaque here). |
specchen_status |
no, deprecated | object | — | Old name of specs_status. |
Registry entry (specs_status.SPEC-NNN)¶
Required: file, status. activated_at/completed_at never go here — they live in state.yaml.
| Field | Required | Type / values | Example | Meaning |
|---|---|---|---|---|
file |
yes | string | 212-json-schema-for-the-spec-structure/spec.md |
spec.md path relative to specs_directory. |
status |
yes | spec lifecycle value | in_progress |
Mirrors state.yaml (guard 54). |
priority |
no | P0 … P3 |
P1 |
Delivery priority. |
dependencies |
no | list of spec ids | [] |
Blocking dependencies (the registry keeps this key). |
specs_dir |
no | specs/<dir>/ — <dir> starts with a letter or digit, then letters, digits, ., _ or -; so never . or .. |
specs/212-json-schema-for-the-spec-structure/ |
Spec directory. |
dir |
no, deprecated | specs/<dir>/, same rule as specs_dir |
— | Old name of specs_dir. |
title |
no | string | JSON Schema for the spec structure |
Title, read by the domain dashboard. |
deliverable_types |
no | non-empty list of deliverable types | [code] |
Read by guard 58 (scripts/tests/setup/58-deliverable-types-are-declared.sh). |
completed_date |
no | date | 2026-09-30 |
When the spec was completed. |
specs_status:
SPEC-212:
file: 212-json-schema-for-the-spec-structure/spec.md
status: in_progress
priority: P1
dependencies: []
specs_dir: specs/212-json-schema-for-the-spec-structure/
Vocabularies¶
The status enums are mirrors of
plugins/vt-base/lib/status-vocabulary.tsv
(SPEC-181); test-spec-schema-vocab-drift.sh fails on any difference in either direction.
| Vocabulary | Values | Used by |
|---|---|---|
spec lifecycle (spec) |
proposed, specified, in_progress, completed, archiviert, parked, rejected, withdrawn |
state.yaml status, registry status, deprecated frontmatter status |
gate (gate) |
COMPLETE, PASS, GO, GO WITH CAUTION, FAIL, NO-GO, PENDING, SKIPPED, PASS_WITH_UNVALIDATED_REVISION, PASS_WAVE1_SCOPE; alias GO-WITH-CAUTION → GO WITH CAUTION |
every gate status except spec_check_gate, plus gate_history[].status |
spec check (spec-check) |
RUN |
spec_check_gate.status only |
| deliverable types | code, test, research, document (aliases documents, documentation, docs, doc), presentation (presentations), project-plan (project-plans) |
deliverable_types — the tokens of persona-select.sh |
Write the canonical form (GO WITH CAUTION, document); the aliases are accepted because the
vocabulary accepts them.
spec.md body sections¶
In this order (canonical source: /vt-d-spec-from-requirements Step 3). Include only sections with
real content — never placeholder text. The body is documented, not schema-checked; /vt-d-spec-check
checks its quality.
# SPEC-NNN: <Title>## Problem— the observed issue or need## Goal— the desired outcome## User Stories— with Given/When/Then acceptance criteria## Functional Requirements— tableID | Requirement## Visual Reference— screenshots, mockups or a text description (may be omitted forui_scope: backend)## Non-Functional Requirements## Edge Cases— tableID | Edge Case | Expected Behavior## Scope— In Scope / Out of Scope## Dependencies## Success Criteria— numbered, measurable## Definition of Ready— the canonical checklist## Definition of Done— the canonical checklist## Sibling-Candidates— appended by/vt-d-spec-from-requirementsonly when similar specs survive its sibling check (SPEC-132)
Validating a spec¶
plugins/vt-base/bin/spec-schema-check specs/212-json-schema-for-the-spec-structure
plugins/vt-base/bin/spec-schema-check --registry
plugins/vt-base/bin/spec-schema-check --all
Run it from the repository root, or pass --root. Each violation is one line of five
tab-separated fields — directory<TAB>file<TAB>JSON-pointer<TAB>code<TAB>message — sorted
(LC_ALL=C). directory is . for the registry; an empty pointer means the whole document. A
backslash, or a control, format (bidi, zero-width) or line-separator character, inside a field is
written as \\, \xNN, \uNNNN or \UNNNNNNNN, so a field never
spans a tab or a line.
Exit 0 means clean, 1 violations, 2 a usage error, a schema file the validator cannot enforce,
or an internal fault — a crash never reads as "violations". The validator is standard-library Python
(no PyYAML); it reads the YAML subset spec files use and reports anything outside it as load-error
instead of guessing.
| Option | Meaning |
|---|---|
--all · --registry · <spec-dir>… |
What to check: everything, only .design-state.yaml, or the named spec directories. Exactly one. |
--root DIR |
Repository root (default: the current directory). |
--schema-dir DIR |
Schema files (default: plugins/vt-base/lib/schema). |
--baseline-format |
Print only the four key fields (the baseline row format). |
--summary |
Also print # specs=N documents=M load_errors=K on stderr. |
-h, --help |
Usage. |
| Code | Meaning |
|---|---|
additional-property |
A field the schema does not define. |
required |
A required field is missing (the pointer names it). |
type, enum, pattern, format, min-length, min-items, unique-items, minimum, maximum |
A value breaks the rule of the same name. |
missing-frontmatter |
spec.md has no --- frontmatter block. |
missing-file |
spec.md, state.yaml or the registry is absent, or not a regular file. The name must match exactly: SPEC.md does not count as spec.md, on any filesystem. |
symlink |
A spec directory, spec.md, state.yaml or the registry is a symlink, or a spec directory lies outside the root (a link anywhere on its path, a .. step, or an absolute path outside it); it is not followed and nothing is read. A symlinked specs/ itself is one row, . / specs. |
load-error |
The file is not valid UTF-8, or the YAML uses a construct outside the supported subset, or one PyYAML refuses: anchors, aliases, tags, any TAB (quoted or not, D17), a --- inside the document or a delimiter written ---, a plain = or << value, an unquoted impossible date or time (2026-02-31 — quoted, it is a format violation instead), a number that cannot be built, lone CR/NEL/U+2028/U+2029, control characters, nesting deeper than 64 levels, …. The message names the line and the construct. |
duplicate-key |
A key appears twice in one mapping; the last value is validated. |
registry-orphan |
A registry entry points to no spec directory. |
unregistered-spec |
A spec directory whose id has no registry entry. |
Baseline and ratchet¶
Guard 126 (scripts/tests/setup/126-spec-schema-ratchet.sh) runs spec-schema-check --all and
compares the result with scripts/tests/setup/spec-schema-baseline.txt, one tab-separated row
directory<TAB>file<TAB>JSON-pointer<TAB>code per deviation that existed when the schemas landed.
- A violation that is not in the baseline fails the suite — a new spec, or a new field on an old one, must match the schema.
- A baseline row that no longer matches a violation fails too — when you fix an old spec, delete its row in the same commit. The failure message names the row.
- The baseline only shrinks: rows added against the merge base fail, and on
maina merge commit is compared with the main tip before it. Merge a branch intomainas a merge or squash commit — a fast-forward or GitHub's "Rebase and merge" would let added rows through. - Never hand-merge the baseline. On a merge conflict — or after any merge that touches
specs/— regenerate it from the merged tree, under the file's comment header:
The result must be a subset of the previous rows. A new row means the merged branch deviates:
fix that spec (ad-hoc review passes move into gate_history), do not keep the row.
To change the structure itself, change the schema, specs/212-json-schema-for-the-spec-structure/data-model.md
and this page together; the schema tests check the enums against the vocabulary and the
/vt-d-spec-from-requirements and /vt-d-specs-from-prd templates against the schemas.