Skip to content

Commit 3d3695c

Browse files
dmealingclaude
andcommitted
docs(requirements): design the requirements doc surface — generate the view, never annotate the docs
`meta docs` gains a third surface, `requirements`, rendering the declared ledger as documentation: one human markdown index plus one TOON artifact, from metadata alone. THE DIRECTION INVERTS. The original ask — annotate documents with requirement refs, then strip them at deploy — was declined by a two-arm challenge, both arms independently: an annotated doc is a second authoring surface that must track the model (the duplication this project exists to remove), and "this passage depends on X" is a violation-predicate in doc clothing, which the capability ledger already rules out. So the view is GENERATED from the ledger. The "stripper" it needed already exists as a convention and needs no code: notes is chartered internal-only and never emitted (documentation.json:15,39). Why an emitter rather than a format: measured on an adopting estate, the ledger is the disciplined system (~321 nodes, ~790 implementedBy claims, zero ERR_REQUIREMENT_*) while its prose is not (193 of 403 specs carry no status line, 7+ unnormalised spellings). That estate had already hand-rolled a ~165-line script to do this. NO TEST LINK, AND THAT IS THE DESIGN. A challenge (record requirements-doc-derived-testlink, pinned 415549d) asked whether the generated docs should scan the repo's test sources and render a derived "verified by" while @Verifiedby stays retired from source metadata. Two arms, same verdict from disjoint premises. The intent — observed evidence belongs in the output, not the prescriptive source — is preserved everywhere else here; the SCAN fails on a narrower point: there is no join key left. `name`/dotted-path are author-chosen (and `name` also keys the future stub path, so gaming a published column would corrupt codegen identity), and @implementedBy is disqualified BY MEASUREMENT for exactly this use — FR-038 §7 found it "worthless for retrieval (11/24 with structured links against 12/24 with entities named in prose)", salvaged only as a mechanical codegen input, "not a retrieval hint". It is also L4/L5-only, so upper tiers have no key at all. Labelling it "unverified mention" reaches one of the four audited failures; the other three are semantic and no lexical rule reaches them. And it would put a scan-derived link against FIVE shipped statements — the migration guide, the adopter feature doc, the capability ledger, the byte-gated agent-context skill, and the batch plan — which is precisely the two-load-bearing-statements-disagreeing failure that forced the retirement. Shape A + C, not B: an index (1 file, ~11.9K tokens) plus backlinks on existing entity pages (~466 bytes each). Page-per-L1 is dominated on measurement — 19 files, ~30.5K tokens, one page at 86KB. C is also why this beats a standalone script: only something inside the codegen pipeline holds both halves of the model. TOON for the declared-count header, NOT the token saving — measured with the real encoder on 321 rows it is 42.1% smaller than JSON but only 7.3% smaller than a markdown table, which is already header-once. `requirements[321]{...}:` lets a reader verify it got all 321. It reuses what exists: @toon-format/toon is already a cli dependency and toonEncode already established "humans at a terminal get text; pipes/agents get TOON" (format.ts:17-18). TOON quotes every comma-bearing string, which is why prose stays in the markdown and structure in the TOON. Both files emit unconditionally rather than behind --format: `meta docs` writes FILES while --format selects a STDOUT encoding, and a drift gate wants the machine artifact committed regardless of who ran the command. The surface reads metadata and nothing else — docs.ts:1-12 guarantees output "from metadata ALONE", and that one sentence is what rules out the scan. An empty ledger emits NOTHING (not an empty page), which is what makes default-on a no-op for every project without requirements. The test link returns structurally once requirementTests() ships, as a derived path rather than an inference — with the tension stated rather than papered over: the stub path is an app seam living in gen config, which this command does not read. The precedent is the `api` surface, which materialises only with a loadable config (docs.ts:412) — config-gated enrichment on a metadata-alone surface, so rule 1 stays intact instead of quietly eroding. Verification is built around the failure that matters — a doc generator that silently drops rows looks complete and is not: the TOON count must equal rows emitted on a NESTED fixture (a flat one cannot tell a depth-first walk from top-level-only), `notes` must appear in no artifact on a fixture carrying BOTH description and notes (one alone cannot tell suppression from absence), an unclaimed entity's page must stay byte-identical, and no test link may emit even when requirement names exactly match real test files — the case that would regress the ruling silently. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent 415549d commit 3d3695c

1 file changed

Lines changed: 280 additions & 0 deletions

File tree

Lines changed: 280 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,280 @@
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

Comments
 (0)