|
| 1 | +# The requirements documentation surface — generate the view, never annotate the docs |
| 2 | + |
| 3 | +**Status:** design, ready to plan. Additive; no vocabulary change, no breaking change. |
| 4 | +**Depends on:** nothing. **Unblocks:** the structural test link, once FR-038 ships (§7). |
| 5 | + |
| 6 | +`meta docs` gains a third surface, `requirements`, that renders the declared `requirement.*` |
| 7 | +ledger as documentation. It emits two files: one for people, one for machines. It reads |
| 8 | +metadata and nothing else. |
| 9 | + |
| 10 | +--- |
| 11 | + |
| 12 | +## 1. The problem, measured |
| 13 | + |
| 14 | +A capability ledger and a documentation set answer overlapping questions, and only one of them |
| 15 | +is disciplined. |
| 16 | + |
| 17 | +Measured against an adopting estate (an external project; the numbers are reported here, not |
| 18 | +reproducible from this repository): |
| 19 | + |
| 20 | +| | | |
| 21 | +|---|---:| |
| 22 | +| requirement nodes in the ledger | ~321 | |
| 23 | +| `@implementedBy` claims across them | ~790 | |
| 24 | +| `ERR_REQUIREMENT_*` diagnostics on a `verify` run | **0** | |
| 25 | +| that project's specification documents carrying **no** status line | 193 of 403 | |
| 26 | +| distinct unnormalised spellings of status found in prose | 7+ | |
| 27 | + |
| 28 | +The ledger is loaded, validated and gated in five language ports. The prose is not checked by |
| 29 | +anything. That estate had already hand-written a ~165-line script to render a registry view out |
| 30 | +of its own ledger — the demand signal for this feature, and evidence that the missing piece is |
| 31 | +an emitter rather than a format. |
| 32 | + |
| 33 | +## 2. The direction inverts — the ruling that shapes everything below |
| 34 | + |
| 35 | +The original request was the opposite of this design: **annotate the documents with requirement |
| 36 | +references, then strip the annotations at deployment.** That was declined by a two-arm |
| 37 | +challenge, both arms independently, on two grounds: |
| 38 | + |
| 39 | +1. An annotated document is **a second authoring surface that must track the model** — the |
| 40 | + duplication this project exists to remove. |
| 41 | +2. "This passage depends on `@promptStyle` living on `template.prompt`" is a |
| 42 | + **violation-predicate wearing documentation clothing**, which |
| 43 | + [`spec/capability-ledger.md`](../../../spec/capability-ledger.md) already rules out. |
| 44 | + |
| 45 | +**So the doc view is generated FROM the ledger.** Nothing is authored twice, and the view cannot |
| 46 | +drift from the model because it has no independent existence. |
| 47 | + |
| 48 | +The "stripper" the original ask needed **already exists as a convention** and requires no code: |
| 49 | +`description` is chartered user-facing and `notes` is internal-only, never emitted to generated |
| 50 | +user-facing documentation ([`spec/metamodel/documentation.json:15,39`](../../../spec/metamodel/documentation.json)). |
| 51 | +This surface honours that split; it does not reinvent it. |
| 52 | + |
| 53 | +## 3. There is no test link, and that is deliberate |
| 54 | + |
| 55 | +**Decided by challenge on 2026-08-21** (record: `requirements-doc-derived-testlink`, pinned at |
| 56 | +`415549d63`; two arms, same verdict, reached from disjoint premises). |
| 57 | + |
| 58 | +The question put was whether the generated documentation should **scan the repository's test |
| 59 | +sources** and render a derived "verified by" link, with the `@verifiedBy` attribute staying |
| 60 | +retired from source metadata. The intent behind it is sound and is preserved everywhere else in |
| 61 | +this design — *observed evidence belongs in the output, not in the prescriptive source.* The |
| 62 | +scan fails on a narrower and more concrete point. |
| 63 | + |
| 64 | +**There is no join key left to scan on.** |
| 65 | + |
| 66 | +- **`name` / the dotted path** are author-chosen. The retirement's argument — |
| 67 | + *"the author picks the string, so the cheapest way to satisfy the check is to find a name |
| 68 | + that already exists"* |
| 69 | + ([`verified-by-retirement.md:111-115`](../../features/migrations/verified-by-retirement.md)) — |
| 70 | + applies verbatim, and worse: the requirement `name` also keys the future generated stub path |
| 71 | + (§7), so gaming a published column would corrupt codegen identity. |
| 72 | +- **`@implementedBy` is disqualified by measurement, for this exact use.** FR-038 §7 records |
| 73 | + that round 5 of the requirements investigation found it *"worthless **for retrieval** (11/24 |
| 74 | + with structured links against 12/24 with entities named in prose)"*, and salvages it only as |
| 75 | + *"a **mechanical** codegen input … not a retrieval hint"* |
| 76 | + ([design §7](2026-08-15-fr-038-requirement-derived-test-stubs-design.md)). Structured links |
| 77 | + scored **worse than prose**. A documentation scan is retrieval. |
| 78 | + It is also legal on **L4/L5 only** ([`spec/metamodel/requirement.json:69-76`](../../../spec/metamodel/requirement.json)), |
| 79 | + so the upper tiers of any ledger have no key at all; and it is unbounded where present — one |
| 80 | + architectural requirement claimed by 123 entities would render as "verified by" essentially |
| 81 | + every test in the estate. |
| 82 | +- **The corpus classifier is gone too.** The deleted scan's own module header called its |
| 83 | + test-file patterns *"a GUESS about someone else's repository"*, and the seam that let a |
| 84 | + project correct it (`verify.testFiles`) was deleted with it. |
| 85 | + |
| 86 | +**Labelling it "unverified mention" does not rescue it.** Comment-only matches were already |
| 87 | +downgraded to a warning, which reaches one of the four audited failures; the other three are |
| 88 | +semantic, and no lexical rule reaches them |
| 89 | +([`verified-by-retirement.md:113-115`](../../features/migrations/verified-by-retirement.md)). |
| 90 | + |
| 91 | +**And it would contradict five shipped statements**, which is the specific failure that forced |
| 92 | +the retirement in the first place — two load-bearing statements disagreeing about this same |
| 93 | +vocabulary: |
| 94 | + |
| 95 | +| where | what it says | |
| 96 | +|---|---| |
| 97 | +| [`docs/features/migrations/verified-by-retirement.md:118-126`](../../features/migrations/verified-by-retirement.md) | "Nothing yet, deliberately … the replacement inverts the direction" | |
| 98 | +| [`docs/features/requirements.md:167-177`](../../features/requirements.md) | adopter-facing: tying a requirement to a test "is instead the job of a generator that emits the test **from** the requirement" | |
| 99 | +| [`spec/capability-ledger.md:234-244`](../../../spec/capability-ledger.md) | `verifiedBy` — RETIRED | |
| 100 | +| `agent-context/skills/metaobjects-verify/references/requirements.md:59-64` | agent-facing, **byte-gated** in `agent-context-conformance` | |
| 101 | +| [`docs/superpowers/plans/2026-08-21-coordinated-pre-1.0-breaking-batch.md:50`](../plans/2026-08-21-coordinated-pre-1.0-breaking-batch.md) | "a requirement with no test link is a legitimate declared state" | |
| 102 | + |
| 103 | +**So the surface prints no test link at all, and says so.** Silence here is consistent with |
| 104 | +five shipped statements rather than a gap in the output. The structural replacement arrives in |
| 105 | +§7. |
| 106 | + |
| 107 | +## 4. Shape: an index, plus backlinks — not a page per node |
| 108 | + |
| 109 | +Three shapes were measured. (Token counts from the same external estate; reported, not |
| 110 | +reproducible here.) |
| 111 | + |
| 112 | +| shape | output | verdict | |
| 113 | +|---|---|---| |
| 114 | +| **A** — one index page | 1 file, ~11.9K tokens | **adopt** | |
| 115 | +| **B** — page per L1 node | 19 files, ~30.5K tokens, one page at **86KB** | **dominated** — strictly more output, worse to read, worse to diff | |
| 116 | +| **C** — backlinks on existing entity pages | ~466 bytes average per page | **adopt** | |
| 117 | + |
| 118 | +**A + C.** The index is the readable whole; the backlinks put the claim where the reader |
| 119 | +already is. B is rejected on measurement, not taste — an 86KB page is not a documentation page. |
| 120 | + |
| 121 | +**Shape C is the reason this beats a standalone script.** A hand-rolled registry generator can |
| 122 | +produce A. Only something inside the codegen pipeline can add "this entity is claimed by these |
| 123 | +requirements" to the entity's own page, because only it holds both halves of the model. |
| 124 | + |
| 125 | +## 5. Two artifacts, both emitted unconditionally |
| 126 | + |
| 127 | +### `requirements.md` — for people |
| 128 | + |
| 129 | +The index. Requirement nodes in ledger order, nesting preserved (hierarchy **is** nesting), each |
| 130 | +carrying `@statement`, `@violation`, `@level`, `@status`, `@disposition`, `@trackedBy` and its |
| 131 | +resolved `@implementedBy` targets. |
| 132 | + |
| 133 | +`description` is emitted. **`notes` is not**, per |
| 134 | +[`documentation.json:15,39`](../../../spec/metamodel/documentation.json) — it is the |
| 135 | +internal-only slot, and the whole point of the charter is that a generated user-facing document |
| 136 | +honours it without anyone remembering to strip anything. |
| 137 | + |
| 138 | +### `requirements.toon` — for machines |
| 139 | + |
| 140 | +The same data, TOON-encoded. **The reason is the declared-count header, not the token saving.** |
| 141 | + |
| 142 | +Measured with the real `@toon-format/toon` encoder on 321 rows: TOON is **42.1% smaller than |
| 143 | +JSON but only 7.3% smaller than a markdown table** — a table is already header-once, so the |
| 144 | +compression argument is nearly absent. What TOON adds is: |
| 145 | + |
| 146 | +``` |
| 147 | +requirements[321]{path,level,status,claims,statement}: |
| 148 | +``` |
| 149 | + |
| 150 | +A reader can verify it received all 321 rows. That is worth more than 7.3%. |
| 151 | + |
| 152 | +This reuses infrastructure that already exists: `@toon-format/toon` is already a dependency of |
| 153 | +`@metaobjectsdev/cli` (`package.json:61`) and `toonEncode` already lives in |
| 154 | +[`lib/format.ts`](../../../server/typescript/packages/cli/src/lib/format.ts), which established |
| 155 | +the convention at `:17-18` — *"humans at a terminal get text; pipes/agents get TOON."* |
| 156 | + |
| 157 | +**Caveat, stated because it constrains the split:** TOON quotes every comma-bearing string, so |
| 158 | +prose dilutes it badly. That is precisely why prose belongs in the markdown and structure |
| 159 | +belongs in the TOON, rather than either file trying to be both. |
| 160 | + |
| 161 | +### Why both, unconditionally, rather than a `--format` flag |
| 162 | + |
| 163 | +`meta docs` **writes files**; `--format` on `gen`/`migrate` selects a **stdout** encoding. Reusing |
| 164 | +the flag name would make it mean something materially different on this command. And a drift |
| 165 | +gate wants the machine-readable artifact committed regardless of who ran the command or from |
| 166 | +what kind of terminal. |
| 167 | + |
| 168 | +## 6. The surface contract |
| 169 | + |
| 170 | +`DocsSurface` gains a third member |
| 171 | +([`metaobjects-config.ts:195`](../../../server/typescript/packages/codegen-ts/src/metaobjects-config.ts)): |
| 172 | + |
| 173 | +```ts |
| 174 | +export type DocsSurface = "model" | "api" | "requirements"; |
| 175 | +``` |
| 176 | + |
| 177 | +plus a `--requirements` CLI flag, narrowing markdown surfaces exactly as `--model` / `--api` do. |
| 178 | +`--site` and `--metamodel` are **not** surfaces — they are separate branches in `docs.ts` — so |
| 179 | +nothing there changes. |
| 180 | + |
| 181 | +**Three rules govern it:** |
| 182 | + |
| 183 | +1. **Metadata alone.** `meta docs` guarantees it emits *"from metadata **ALONE** — no gen |
| 184 | + config, no codegen pipeline … The neutrality of the output is therefore guaranteed"* |
| 185 | + ([`docs.ts:1-12`](../../../server/typescript/packages/cli/src/commands/docs.ts)). This |
| 186 | + surface introduces **no** new input. That single sentence is what rules out §3's scan, and |
| 187 | + it must keep being true. |
| 188 | +2. **An empty ledger emits nothing** — not an empty page. A project declaring no `requirement.*` |
| 189 | + nodes sees no change at all, matching the feature's existing posture (no requirement nodes ⇒ |
| 190 | + no diagnostics). This is what makes adding `requirements` to the default surface list safe. |
| 191 | +3. **Default on.** `surfaces` defaults to `["model", "api"]` today; it becomes |
| 192 | + `["model", "api", "requirements"]`. Rule 2 is what makes that a no-op for every project |
| 193 | + without a ledger. |
| 194 | + |
| 195 | +## 7. The test link returns structurally, later |
| 196 | + |
| 197 | +When `requirementTests()` ships (FR-038 slice 1, |
| 198 | +[plan](../plans/2026-08-16-fr-038-requirement-test-codegen.md), targeting 1.1), it emits |
| 199 | +`tests/requirements/<requirement dotted path>.<concern>.test.ts`. The link then becomes a |
| 200 | +**derived path, not an inference** — no scanning, nothing matched, nothing guessed. |
| 201 | + |
| 202 | +**One tension must be resolved before that lands, and it is not resolved here.** The stub's |
| 203 | +location is an app seam (design §6, `path`) and §13 leaves "where do stubs live" an open |
| 204 | +default. So the stub path lives in **gen config** — which rule 1 above says this command does |
| 205 | +not read. |
| 206 | + |
| 207 | +The precedent for that already exists in the same file: the `api` surface materialises only |
| 208 | +with a loadable gen config, *because* api docs describe a generated surface that only exists |
| 209 | +when there is one ([`docs.ts:412`](../../../server/typescript/packages/cli/src/commands/docs.ts)): |
| 210 | + |
| 211 | +```ts |
| 212 | +const apiSelected = docsCfg.surfaces.includes("api") && loadedConfig !== undefined; |
| 213 | +``` |
| 214 | + |
| 215 | +A structural test link takes the same shape: **config-gated enrichment on a metadata-alone |
| 216 | +surface.** Absent a loadable config, the requirements surface still emits — just without the |
| 217 | +link. That keeps rule 1 intact rather than quietly eroding it. |
| 218 | + |
| 219 | +**A stronger form worth preferring if it is cheap:** have the generated stub carry a |
| 220 | +machine-readable back-reference to its requirement's FQN. Documentation reading a *generated |
| 221 | +artifact's own declaration* is derivation; documentation matching strings in prose is not. |
| 222 | + |
| 223 | +## 8. The spec-citation slot is a separate defect, and A does not dissolve it |
| 224 | + |
| 225 | +A generated index reproduces a citation like `title: "FR-448 — …"` as **inert text**. It renders |
| 226 | +it; it does not resolve it. So the citation problem survives this feature. |
| 227 | + |
| 228 | +**But it is better posed as field overloading than as "free text needs a check."** A citation is |
| 229 | +living in a node's display label *because there is nowhere else for it to live* — `title` is a |
| 230 | +noun phrase, and a citation is not a name. Framed that way it is a candidate for the |
| 231 | +[ADR-0037](../../../spec/decisions/ADR-0037-metamodel-vocabulary-expansion-decision-framework.md) |
| 232 | +decision procedure, which the "add a check to free text" framing could never pass. |
| 233 | + |
| 234 | +Note the shape of the nearest precedent before proposing vocabulary: `@trackedBy` (0.23.0) is |
| 235 | +free-form and **deliberately never resolved**, because `verify` has no network and two systems |
| 236 | +holding the same answer will drift. Any citation slot must say why it differs — or reuse |
| 237 | +`@trackedBy` and stop there. **Out of scope for this design**; recorded so it is not mistaken |
| 238 | +for something this feature delivered. |
| 239 | + |
| 240 | +## 9. Verification |
| 241 | + |
| 242 | +The gate that matters is not "a file was emitted." It is that the emitted view **cannot silently |
| 243 | +lose the ledger**, since a documentation generator that quietly drops rows produces a document |
| 244 | +that looks complete and is not. |
| 245 | + |
| 246 | +- a **`codegen-conformance` fixture** whose model carries a nested `requirement.*` tree, gating |
| 247 | + both artifacts byte-for-byte — this is the mechanism that keeps A and C in agreement |
| 248 | +- the TOON header's declared count **equals** the number of rows emitted, asserted on a fixture |
| 249 | + with nesting (a flat fixture cannot tell a depth-first walk from a top-level-only one) |
| 250 | +- a model with **zero** requirement nodes emits **no** requirements file — proving rule 2, which |
| 251 | + is what makes the default-on decision safe |
| 252 | +- **`notes` appears in no emitted artifact**, asserted on a fixture where a requirement carries |
| 253 | + both `description` and `notes` — a fixture with only one of them cannot tell suppression from |
| 254 | + absence |
| 255 | +- shape C: an entity claimed by a requirement gains its backlink; an unclaimed entity's page is |
| 256 | + **byte-identical** to today's |
| 257 | +- **no test link is emitted under any input**, including a model whose requirement names exactly |
| 258 | + match real test files in the repo — the case that would regress §3 silently |
| 259 | +- the surface is proven able to fail: sabotage the fixture and watch the lane go red, per this |
| 260 | + repository's standing rule that a gate never demonstrated failing is decorative |
| 261 | + |
| 262 | +## 10. What this does not solve |
| 263 | + |
| 264 | +- **It does not check the prose.** 193 of 403 documents carrying no status line stay that way; |
| 265 | + this generates a parallel view, it does not conscript the existing corpus. |
| 266 | +- **It does not prove a requirement is met.** No documentation surface can. The unfakeable |
| 267 | + formulation remains `@violation`-driven mutation, which is out of scope for the same reason it |
| 268 | + is out of scope for `verify`: *"it never runs them"* is byte-gated across five ports. |
| 269 | +- **It is TypeScript-only at first.** `meta docs` is a Node-CLI-owned surface. Nothing here |
| 270 | + blocks a per-port equivalent, and nothing here requires one. |
| 271 | + |
| 272 | +## 11. Open questions |
| 273 | + |
| 274 | +- **Where does the requirements index sit relative to `outputLayout`?** The model surface follows |
| 275 | + the project's layout; an index is a single root-level page and may want to ignore it. |
| 276 | +- **Does shape C's backlink belong on projections and values, or only on entities?** Object |
| 277 | + coverage is entity-grain today ([`spec/capability-ledger.md:285-296`](../../../spec/capability-ledger.md)), |
| 278 | + which argues for entities only. |
| 279 | +- **Does the TOON artifact belong under the docs `outDir` at all**, or beside the metadata? It is |
| 280 | + a machine artifact in a directory otherwise meant for humans. |
0 commit comments