Skip to Content
ArchitectureInvariants

Invariants

Six rules hold across all three stages. Each one names the file that enforces it, because a rule with no named enforcer is a rule someone will break without anyone noticing.

Where a rule is enforced by an agent instruction rather than by a script, this page says so. That distinction is the useful one: an instruction is a rule the model follows, and a script is a rule the pipeline cannot proceed past.

One artifact per file

Every requirement, component, interface and ADR is one Markdown file with one YAML frontmatter block, named <ID>-<kebab-title>.md and placed in the subdirectory its type owns.

Atomicity is what makes the rest of the pipeline work. A traceability edge addresses a file; a critic can reject one requirement without re-opening a document; a diff shows which artifact changed rather than which paragraph.

Enforced by: plugin/agents/requirements-formatter.md, plugin/agents/design-formatter.md and plugin/agents/qa-formatter.md — the three agents that decide the path and the filename, per type. The structural validators enforce the contents of that arrangement: plugin/lib/artifact_core.py’s parse_frontmatter reads exactly one frontmatter block per file, discover_files treats every .md that is not a named project-level companion as an atomic artifact, and cross_file_checks in each validator rejects a duplicate ID across the set. Note the seam honestly: neither validator compares the filename to the id inside it, so a correctly-formed artifact under a wrong filename passes the gate. The naming convention is a formatter contract, not a gated one.

IDs are categorical, zero-padded, and allocated only by an orchestrator

The prefix encodes the type — FR, NFR, CON, BR, UC in requirements; CMP, IF, ADR in design; TS in QA — followed by a three-digit zero-padded sequence. No specialist invents an ID.

Single allocation is what keeps the two ends of every traceability edge agreeing. If two specialists could both mint IDs they would eventually mint the same one, and neither would be wrong.

Enforced by: the schemas, structurally — plugin/skills/requirements/schema/requirement.schema.json pins ^(FR|NFR|CON|BR|UC)(-[A-Z0-9]+)*-[0-9]{3,}$ and plugin/skills/qa/schema/qa.schema.json pins ^TS(-[A-Z0-9]+)*-[0-9]{3,}$, and each validator’s cross_file_checks rejects an ID whose prefix disagrees with its own type field. Sole allocation is an orchestrator instruction: plugin/agents/requirements-orchestrator.md Stage 3, plugin/agents/design-orchestrator.md Stage 4, and plugin/agents/qa-orchestrator.md Stage 3, each of which states it as “the only ID authority” (or “the single authority for ID allocation”) and makes an IF- ID appearing in a component draft — or a specialist minting its own TS- ID in QA — a re-dispatch rather than a repair.

IDs are never reused

A deleted artifact’s ID stays retired. The artifact is marked status: obsolete rather than removed, and the next allocation continues past it.

An ID that means one thing in a commit and another thing later makes every reference to it ambiguous — including references in code, tests and conversation, which no validator can see.

Enforced by: plugin/agents/requirements-orchestrator.md:103, plugin/agents/design-orchestrator.md:177 and plugin/agents/qa-orchestrator.md:232, as an instruction. It is not gated, because a validator that could enforce it would need the history rather than the set. The design orchestrator’s retired Stages 11 and 12 are the same principle applied to stage numbers, and are what the rule looks like when it is honoured.

The critic gates judgment; the formatter gates structure

The critic runs before anything is written and judges quality. The structural validator runs inside the formatter, against the files it has just written.

This is not a preference about where to put a check. The critic cannot run the structural validator: at gate time there are no files, and in the requirements stage assumptions.md and glossary.md — which that validator hard-gates — are not even assembled until the stage after the gate passes. Folding the validator into the formatter rather than placing it after also means one agent both writes the set and answers for its structure.

Enforced by: plugin/agents/requirements-formatter.md’s “Verify, then report”, plugin/agents/design-formatter.md’s “Validator re-run” and plugin/agents/qa-formatter.md’s “Verify, then report” sections, which run validate_requirements.py, validate_design.py and validate_qa.py respectively (plus validate_traceability.py in design and QA) as part of the write contract.

All three stages’ SKILL.md files state the invariant explicitly, and they say it in that form because stating it wrongly has cost twice: STO-215 and STO-207 were both agent files describing a structural check at a point where the files it checks did not yet exist.

Low-confidence artifacts are triaged, never silently accepted

An artifact the pipeline is unsure of is written with confidence: low and listed in index.yaml’s review_queue with a one-line reason, so a human can triage the uncertain items without opening every file.

Uncertainty is a normal output of all three stages, and each downstream stage inherits the one before it’s queue rather than clearing it — the design stage inherits the requirements stage’s, and the QA stage inherits the design stage’s inherited_review_queue in turn. Several of those items are decisions that correctly landed in an earlier stage’s out-tray and are still unresolved.

Enforced by: plugin/agents/design-formatter.md and plugin/agents/qa-formatter.md, which require review_queue to agree exactly with the confidence: low set and report a review_queue_count each must match; and each stage’s SKILL.md, which surfaces the same list in the sign-off summary.

A contradiction worth knowing about. plugin/agents/requirements-formatter.md:130 says the formatter MAY emit index.yaml, and that the index MAY carry a review_queue. plugin/skills/requirements/SKILL.md:182 describes the formatter writing both as a matter of course, every generated example set has them, and plugin/skills/design/SKILL.md reads that review_queue as an input to the design stage. Neither structural validator gates the file — both skip index.yaml as a project-level companion — so nothing catches the disagreement. Filed as STO-269 #1; it is recorded here rather than resolved, because which way it resolves is a decision, not a typo.

Generated reference data is committed, and CI fails on drift

Everything under site/content/_generated/ is derived from the repository’s own sources, committed alongside them, and re-derived by CI on every pull request. A change to a source that is not accompanied by a regenerated file reddens CI.

This is the tax that makes the generated half of this site trustworthy. The site build itself runs no Python — it reads the committed JSON — so the drift gate is the only thing standing between a source edit and a published page that contradicts it.

Enforced by: .github/workflows/ci.yml, in the Generated reference is not stale step, which runs the exporter’s --check mode and exits 1 naming each stale file.

You will trip it by editing any of these:

  • a rule in lint_design_content.py, lint_requirements_content.py or validate_traceability.py — their RULES registries are rules.json;
  • a field in any of the three JSON Schemas — they are fields.json;
  • an agent’s frontmatter description: — the roster is agents.json;
  • any SKILL.md’s coverage-area list — those are stages.json;
  • a stage heading or its hand-off YAML in any orchestrator — those are pipeline.json, and the pages in this section.

The fix is one command:

python3 site/scripts/export_reference.py

The worked example sets have their own gate, export_examples.py --check, and their own command, python3 site/scripts/export_examples.py.

The exporters fail loudly rather than degrading. A parser that returned an empty result when its source moved would produce committed JSON that still matched what the parser currently emits, and the gate would stay green over a page that had quietly stopped saying anything.

Last updated on