@am_shork/attest 0.11.0 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -7,11 +7,24 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## Versioning
9
9
 
10
- Under 0.x SemVer a breaking change bumps the **minor**. "Breaking" means it
10
+ From `[1.0.0]` a breaking change bumps the **major**. "Breaking" means it
11
11
  changes the exit code of an existing valid setup, rejects previously-valid
12
12
  input, removes/renames a public API or `--json` field, or changes a default
13
13
  runtime behavior an existing invocation relies on — diagnostic message text is
14
- not API.
14
+ not API. The definition has not moved; what moved is the bump it earns, and the
15
+ whole history below `[1.0.0]` was written under the 0.x rule, where the same
16
+ definition earned a **minor**.
17
+
18
+ **One surface keeps the 0.x rule, and it is named rather than inferred.**
19
+ `./inspect` is excluded from the freeze below, so a breaking change to it is a
20
+ minor until a second adopter's usage exists. Everything else the next section
21
+ lists takes the major.
22
+
23
+ `schemaVersion` in the `--json` report is a **separate axis** and not a
24
+ restatement of this one: it is bumped whenever the emitted shape changes
25
+ incompatibly, and an additive field bumps neither it nor the package. Removing
26
+ or renaming a `--json` field is breaking by the definition above and is
27
+ therefore both — a `schemaVersion` bump and, from 1.0, a major.
15
28
 
16
29
  **A narrowing that closes a security hole is breaking by that definition, and
17
30
  takes the bump that definition gives it — including a major.** There is no
@@ -35,6 +48,466 @@ invites is the other one: a fix weakened until it fits a bump somebody
35
48
  preferred. A narrowing that ships smaller than the defect it closes is what this
36
49
  paragraph exists to make visible.
37
50
 
51
+ ## What 1.0 freezes
52
+
53
+ Decided 2026-08-31 and binding since `[1.0.0]`, where the argument, the
54
+ measurement it rests on and what is still unmeasured are recorded. The line
55
+ exists because every open entry waiting on an adopter is asking somebody to build
56
+ on a surface documented as movable, and a line is the only thing that makes that
57
+ a reasonable ask. It is here rather than only there because a stability promise a
58
+ reader cannot find is not one.
59
+
60
+ **Frozen at 1.0: the CLI — its commands, options and exit codes — and the root
61
+ export `.`.** Both are frozen on evidence that they have stopped moving rather
62
+ than on intention: the CLI's last change of shape was `--apply` on 2026-08-03,
63
+ twelve releases ago, and `src/index.ts` has had its export list edited in three
64
+ commits ever, the last on 2026-07-26.
65
+
66
+ **Explicitly not frozen at 1.0: `./inspect`.** It shipped in `[0.9.2]` and has
67
+ two releases and one adapter behind it. Freezing it would be freezing on one
68
+ adapter, which is the objection this project raises against every premature
69
+ interface and does not get to waive for its own. It stays under the 0.x rule
70
+ above until a second adopter's usage exists — stated here, rather than left to
71
+ be inferred from a version number that would say otherwise. `./define` sits
72
+ between the two and is frozen with the CLI, on seven releases.
73
+
74
+ **1.x, not 1.0:** everything currently blocked on an adopter's evidence — the
75
+ judgement schema, any retrieval surface over the archive. The freeze is not a
76
+ claim that those are settled. (A backlink index over `archive/` was named here
77
+ until 2026-09-01, when it was rejected outright; it is not deferred to 1.x, and
78
+ what closed it is at the foot of this file.)
79
+
80
+ **Not a frozen surface, and deliberately: the workflow document `attest init`
81
+ writes into the adopter's repository.** It is a function of the installed version
82
+ — which is what makes re-running `init` after an upgrade the whole update
83
+ mechanism — so freezing it would freeze the workflow the tool teaches. What 1.0
84
+ does not do is make its drift detectable, and that is a live question at the foot
85
+ of this file rather than something this line settles. It is named here because
86
+ the first adoption measured across an upgrade paid nothing on the four surfaces
87
+ above and 110 stale lines on this one.
88
+
89
+ *What this costs is the paragraph above.* Under 1.0 a narrowing that closes a
90
+ security hole takes a major, on whatever schedule a hole is found. That price is
91
+ now stated rather than unknown, which is what made the line decidable at all.
92
+
93
+ ## [1.1.0] - 2026-09-02
94
+
95
+ ### Added
96
+
97
+ - **`ATX-89`, `ATX-90`, `ATX-91`: `check` reports a workflow document the
98
+ installed version no longer writes (`stale-workflow-doc`, INFO).** The
99
+ document `attest init` writes is a pure function of the installed version, and
100
+ the adopter's tree records nothing about which version wrote it —
101
+ `skillContent()` and `workflowBody()` take no argument, deliberately, because
102
+ that is what makes re-running `init` safe. So the file carries no version
103
+ stamp and a diff against the installed build is the only detector its own
104
+ design permits. Measured on a 0.9.1 → 0.11.0 upgrade report: **110 changed
105
+ lines across 9 hunks**, 403 lines against 458, found only because the reporter
106
+ wrote a script importing `workflowBody()` out of `dist/`, with `check`,
107
+ `cover`, `verify` and `render --check` all green.
108
+ *The level is an asymmetry of obligation rather than a severity judgement, and
109
+ that is the whole argument for it.* Keeping the document current is Attest's
110
+ obligation, because Attest is what changes it; taking the update is nobody
111
+ else's, because an adopter is free to run an older workflow, a modified one, or
112
+ none. A finding whose subject the reader owes nothing about is exactly the
113
+ finding that may only inform — so `ok`, the exit code and every gate are
114
+ untouched, and a reader who ignores this forever pays what they pay today.
115
+ *This is not `init --check`*, which stays rejected below. That gate turns red
116
+ on a diff the person who must commit it has no way to review: a regenerated
117
+ `SPEC.md` shows what **you** changed about your promises, and a regenerated
118
+ `SKILL.md` shows what Attest's authors changed. It also fires in CI, where
119
+ nobody can run `init`, blocking whoever pushes next for an upgrade somebody
120
+ else made.
121
+ *Three requirements because there are three ways to be wrong, and this file
122
+ already carves the identical problem up this way for renderings.* `ATX-89` is
123
+ the report; `ATX-90` is that an **absent** document draws nothing, which is the
124
+ one the obvious implementation breaks — `staleIssue` reports a rendering that
125
+ was never generated under its own code, and copying that here would fire on two
126
+ of this repository's own three targets and on the majority of adopters, since
127
+ `init` writes one target by default and nothing obliges a project to run it;
128
+ `ATX-91` is that the comparison is of the document rather than the bytes, which
129
+ is deliberately the **opposite** of `ATX-84` and could not be had by widening
130
+ `ATX-29`. `ATX-84` compares this repository's copy byte-for-byte and may,
131
+ because `.gitattributes` here is committed and pins `eol=lf` — that file is
132
+ Attest's here and nobody's in an adopter's tree, so one rule cannot cover both.
133
+ *What it costs, measured before it was built:* 0.234 ms for all three target
134
+ comparisons, plus 4.19 ms to import `targets.ts` and the 26 KB template behind
135
+ it, against a `check self --json` of 814–1,183 ms — **~0.5%**. No
136
+ `SCHEMA_VERSION` bump: the field exists, the value is new, and `ok` is
137
+ untouched because INFO is below `hasError`.
138
+ *Two things the build found that the proposal did not contain.* The fix hint
139
+ had to name what `init` does to the file and not only the command — `runInit`
140
+ writes with `writeAtomic`, no merge and no backup, so an adopter who edited
141
+ these instructions is being pointed at something destructive. `targets.ts`
142
+ rule 1 makes that defensible, Attest owning the path being the whole reason it
143
+ may overwrite, and it was nowhere stated to the reader. And the two negative
144
+ scenarios had to be **paired with the case that must fire**, in the same
145
+ scenario, because an absence assertion alone is green in a tree where nothing
146
+ is implemented — which is the defect `never-red` exists to name, arriving in
147
+ the scenarios written to attest a rule about not firing.
148
+ *One cost that is not paid and is stated rather than left to be discovered:*
149
+ this repository cannot dogfood it. `check` reads the project root, which here
150
+ is `self/`, while the document sits at the repository root — so `check self`
151
+ finds it absent and is correctly silent. `ATX-84`'s scenario reaches two
152
+ directories up because it is a test; a command cannot. All three scenarios
153
+ build a temporary project, and `self/` gives this rule none of the continuous
154
+ exercise `ATX-84` gets.
155
+ *What the judgement tables did with the three ids, since that is the
156
+ counter-pressure earning its place rather than a chore paid to it.* All four
157
+ demanded rows and got them. `compound-requirement` **missed `ATX-89`** — two
158
+ obligations, "reports it" and "at that level", and the keyword heuristic sees
159
+ one — which is the "never wrong, nearly blind" reading holding at 18 of 42
160
+ rather than a new finding. `divergent-param` produced a **new false positive
161
+ and re-keyed an old one**: `level` joins `code`, `file`, `flag` and `field` as
162
+ a fifth natural word for a kind of constant, and `level:ATX-60,ATX-89` is the
163
+ second key where the two requirements genuinely *are* related — both about the
164
+ severity ladder and the `hasError` predicate — while the rule relates them for
165
+ a reason that is not that. The echo detector flagged both of `ATX-89`'s params
166
+ and both were measured `independent` by mutation, one `verify self` each,
167
+ taking it to 4 true positives of 11.
168
+ - **`ATX-88`: `tests-red` names the timeout the run applied and how many
169
+ scenarios reached it, so a suite that went red on the clock can be told from
170
+ one that went red on its code.** Filed from a 0.9.1 → 0.11.0 upgrade report
171
+ where `npx vitest run` was 170/170 green on the same tree on which `verify`
172
+ was red two runs in three. The standing answer — run the suite yourself under
173
+ your own reporter — is circular in exactly that case, because the
174
+ disagreement between the two runs *is* the failure; what the reporter did
175
+ instead was time every scenario by hand and cross-reference which of eleven
176
+ over four seconds declared no `timeoutMs`. **This is deliberately not the
177
+ pass-through**, which stays open at the foot of this file: carrying the
178
+ child's failures means new fields, a `SCHEMA_VERSION` question and a third
179
+ party's assertion wording in the field consumers are told not to branch on.
180
+ It is the narrow half that is not blocked — the isolated child run drops the
181
+ config that would have raised the timeout, so *did anything run out the clock*
182
+ is the one question a green direct run cannot answer and this one can, and it
183
+ needs nothing the report does not already carry. A count and never a list, for
184
+ the same reason.
185
+ *The number is read from the resolved child config rather than written down,
186
+ and that is the half a constant would have got wrong.* It is the one value
187
+ here Attest does not choose: Vitest's own default when the run is isolated,
188
+ the supplied config's value when `--vitest-config` raised it. `ATX-88.params`
189
+ pins the isolated default at 5000 because two documents quote it in prose —
190
+ the workflow document `init` writes and the troubleshooting page — and until
191
+ now nothing would have failed if a Vitest release moved it.
192
+ *Detected by duration and never by the child's error text.* Vitest does say
193
+ "Test timed out in 5000ms." and matching that would be shorter; it is also the
194
+ category error this project refuses in its own diagnostics, since a message is
195
+ prose in somebody else's package too. Measured rather than assumed: a scenario
196
+ that reaches the ceiling ends `fail` a hair past it — 5015 ms against 5000 —
197
+ while an assertion failure ends `fail` in under a millisecond, so `>=` needs no
198
+ tolerance and a tolerance would start claiming timeouts for slow assertions
199
+ that failed honestly. A missing duration reads as *not* a timeout, which is the
200
+ direction that fails quietly; the other reports every red as a timeout, which
201
+ is a wrong diagnosis printed with confidence.
202
+ *Stated over the run rather than over the two commands that render it*, unlike
203
+ `ATX-51` beside it, and by this project's own rule rather than by preference:
204
+ what `verify` and `archive` add is a sentence in `Issue.message`, which is
205
+ prose and never API, so a scenario asserting it would be asserting wording.
206
+ Additive to `--json` only in the sense that nothing there moved at all — no
207
+ field, no code, no `schemaVersion` question.
208
+
209
+ - **`examples/judgement-table/` states the counting rule, which is the one piece
210
+ of content that decides nothing and without which two tables cannot be
211
+ compared.** Decided under `Planned` on 2026-08-31 on the first table built from
212
+ that example, which distrusted its own obligation column above all its others
213
+ and said why: two tables that count differently are not a second measurement of
214
+ the same thing, they are a second vocabulary. The rule is not a column and
215
+ answers nothing — what a requirement carries is still the adopter's judgement —
216
+ so it sits on the same side of the split as the two mechanical properties, and
217
+ was being withheld along with the answers, which was the wrong side. It fixes
218
+ the test (two clauses are two obligations when an implementation can satisfy
219
+ one completely and fail the other), the six kinds of clause that read as a
220
+ second obligation and are not (scope, criterion, purpose, the other side, a
221
+ consequence, and the states a scenario happens to construct), the
222
+ one-promise-per-emitted-artifact case, and the tiebreak — perturb one half and
223
+ see whether the other half's scenario stays green. Extracted from the labels in
224
+ `tests/intent-rule-candidates.spec.ts` rather than invented, which is why it is
225
+ a description of a rule already in use and not a proposal. It also names what it
226
+ does **not** ask: whether the obligations belong under one id is a different
227
+ question, and keeping the two apart is what lets the count stay a measurement.
228
+
229
+ ### Changed
230
+
231
+ - **The example's anchor is no longer a field to be typed, because the first
232
+ table built from that file filled it in for all 80 of its rows mechanically.**
233
+ `Judgement` shipped with an optional `pin`, documented as not a judgement
234
+ column and nevertheless the only concrete field in the interface — and a field
235
+ gets populated. Copying 80 scenario names out of the AST and back into the
236
+ table is not judgement, and it is exactly the form-filling the empty frame
237
+ exists to prevent. `pin` is now out of `Judgement` entirely and sits in a
238
+ sibling `PINS` map, and an anchor on an id declaring fewer than two scenarios is
239
+ **refused**: with one scenario the anchor is a function of the project,
240
+ `inspectProject` already computes it, and the line naming it is transcription
241
+ wearing a judgement's clothes. What survives is the case where the anchor is a
242
+ real choice, which is the case a machine cannot make. *The cost is named rather
243
+ than hidden:* a sole scenario deleted and replaced under a different name is a
244
+ revisit `PINS` will not force. That is what not asking anyone to transcribe
245
+ costs, and it is paid where a rename cannot change which scenario the row meant.
246
+ - **The frame's emptiness is now typed, which the obvious implementation of this
247
+ fix would have left unenforced.** Deleting `pin` leaves `interface Judgement {}`,
248
+ and an empty interface is not an empty row type — it admits `0` and `""`, so
249
+ every row could quietly acquire anything at all while the file claimed to ship
250
+ no fields. `Record<string, never>` is the type that means what the sentence
251
+ says. Caught by `eslint` rather than by reasoning, which is `How an entry is
252
+ re-measured`'s question about what a repair makes reachable arriving one step to
253
+ the side of where it was expected. Measured both directions: a row carrying an
254
+ undeclared field does not compile, and replacing the line with an interface
255
+ holding one **required** column errors on every existing row.
256
+ - **The example's own subject gained a second scenario**, so `EX-1`'s anchor is a
257
+ choice among several rather than a value the machine could have derived — the
258
+ refusal above has nothing to demonstrate on a project where every id declares
259
+ one scenario. It is also the counting rule standing on its own subject: two
260
+ scenarios, one obligation, because "before it is stored" is the criterion for
261
+ "rejected" and not a second promise. All three assertions in the anchor test
262
+ were shown to fail before this was called done — a derivable anchor, a renamed
263
+ scenario, and an anchor with no row.
264
+
265
+ ### Documentation
266
+
267
+ - **Three places carried a backstop claim that is now false — `ATX-84`'s
268
+ rationale, `skill.ts`'s header, and design §9 in both languages — and all
269
+ three withdraw it.** The claim is that every mistake a stale workflow
270
+ document can cause is a diagnostic carrying a fix hint the agent corrects
271
+ itself from, and it is one of the two arguments for shipping no freshness gate
272
+ to adopters. An upgrade report measured the counterexample: a
273
+ copy six weeks and one upgrade behind still described `never-red`'s ordering
274
+ rule, which `[0.10.0]` archived as `evidence-not-ordering` because the
275
+ ordering was never in the engine. An agent reading it does something the
276
+ engine does not require — more conservative, not wrong-shaped — so **no issue
277
+ fires at any severity** and there is nothing to correct itself from. The
278
+ backstop covers a stale document producing bad input; it does not cover one
279
+ producing unnecessary work, and the second is what the drift produced.
280
+ *All three, because they are one argument stored three times and this
281
+ repository's whole subject is what happens when those disagree.* Two were
282
+ known from the entry that priced this; the third was found by grepping for the
283
+ claim rather than for the files that discuss it, and design §9 is the copy
284
+ with the widest audience. `ATX-84`'s rationale cites "the decision in
285
+ `skill.ts`", which cites the design — so correcting any one of them alone
286
+ leaves a citation pointing at a document that says something else. The
287
+ workflow document's own bytes are untouched — the claim lives in the module
288
+ header, above `BODY` — so no `init` re-run is owed and `ATX-84` stays green
289
+ byte-for-byte.
290
+ *What does not change is the requirement.* `ATX-84` gates this repository's
291
+ copy and is not adopter-facing, and the surviving objection — that a gate
292
+ charges every adopter a diff they did not ask for — carries it alone. What the
293
+ withdrawal reopens is the adopter-facing question, which is priced under
294
+ `Under consideration` rather than restated in a rationale, since a second copy
295
+ of an argument that is still moving is the failure this entry is about.
296
+ - **The workflow document named one of the two things the isolated child run
297
+ withholds, and the troubleshooting page named both.** Re-measuring this before
298
+ building it is what found that: the ask was filed as "have the workflow
299
+ document say that a scenario near the default needs an explicit `timeoutMs`",
300
+ and that sentence already existed — in `tests-red`'s troubleshooting section,
301
+ worked example included. What did not exist was the pointer. The workflow
302
+ document's own bullet on why a green direct run does not predict the gate lists
303
+ aliases, transforms and a DOM, and stops there; an agent reading it was told
304
+ about the environment and not about the clock, while the section it links to
305
+ covers both. So what was owed was a second bullet rather than a new
306
+ explanation, which is cheaper than the entry priced it.
307
+ - **`tests-red`'s troubleshooting section stops saying the report carries
308
+ nothing about the failure**, in both languages, because it now carries exactly
309
+ one thing. The sentence that had to change is the one that reads as a general
310
+ promise — "not from this report, because it does not carry them" — and the
311
+ worked example beside it says which half crossed and why the other half is a
312
+ decision rather than an omission. History rows on `verify` and `archive` in
313
+ both references, since a reader on 1.0.0 gets the older report.
314
+ - **`CLAUDE.md`'s "it will not tell you why" gotcha gains its exception.** The
315
+ gotcha is still true of the file, the scenario and the assertion; it is no
316
+ longer true of the clock, and a gotcha that overstates itself is how somebody
317
+ stops reading the report.
318
+
319
+ - **`intent-quality.md` step 2 points at the counting rule, and says why that
320
+ one pointer does not contradict the page's own sequencing.** The page asks a
321
+ reader to do step 2 before reading our table, because a table built by someone
322
+ who read ours first is an echo of ours. A rule that fixes the *unit* is the
323
+ opposite case — sharing it is what makes a second table comparable, where
324
+ sharing an answer is what makes it a copy — and a page that withheld both
325
+ without distinguishing them would have been asking for an incomparable
326
+ measurement on principle. Both languages, and a line added to *Read our table
327
+ after step 2, not before* so the exemption is stated where the rule it excepts
328
+ is. The History row is owed for the second reason rather than the first: step 4
329
+ tells a reader to **copy** `examples/judgement-table/`, and that file changed
330
+ under this version, so a copy taken at `[1.0.0]` is no longer the file this
331
+ page describes — which is exactly what that table is for and the kind of row
332
+ easiest to miss, because nothing on the page announces that it hands out a
333
+ file.
334
+
335
+ ## [1.0.0] - 2026-09-01
336
+
337
+ ### Added
338
+
339
+ - **`ATX-87` gates the CLI's command and option roster, which is the other half
340
+ of the same freeze.** `What 1.0 freezes` names two surfaces in one sentence —
341
+ the CLI, its commands, options and exit codes, and the root export `.` — and
342
+ after `ATX-86` three of those four halves were held: the export by `ATX-86`,
343
+ the exit codes by the requirement making `hasError` the single predicate the
344
+ verdict, the JSON `ok` and the process status all read from, and by the one
345
+ keeping the report alone on stdout. **The roster was held by nothing.** Seven
346
+ commands and seventeen options, and no test anywhere enumerated either, so
347
+ deleting a flag or renaming a command broke nothing a contributor runs.
348
+ `ATX-16.staticCommands` reads like the missing gate and is not: it names four
349
+ commands in order to say they execute no project code, which is a claim about
350
+ behaviour rather than a claim that the roster is closed.
351
+
352
+ **Read out of `--help` rather than out of `cli/index.ts`, and that is forced
353
+ rather than preferred.** Every option string in the source is a constant —
354
+ `JSON_FLAG`, `EVAL_FLAG`, `VITEST_CONFIG_FLAG`, `TARGET_FLAG` — so a static
355
+ walk for literals sees the command names and almost none of the flags, and
356
+ resolving the constants would mean reimplementing enough of the emitter to
357
+ guess what it emits. The built module cannot be introspected either:
358
+ `program.parseAsync()` runs at module scope and `program` is never exported,
359
+ so importing `dist/cli/index.js` runs the CLI instead of describing it. What
360
+ is left is the artifact a consumer actually reads, and it is the right ground
361
+ truth in both directions — a flag that works and is absent from the help is
362
+ not on the published surface, and one documented there is, whatever the source
363
+ says.
364
+
365
+ **commander's `-h, --help`, `-V, --version` and `help` command are excluded,
366
+ and matched by name rather than by shape.** They are not this project's to
367
+ freeze, and a gate that reddens because a dependency reworded its own built-in
368
+ is red for a change nobody here decided — the shape this repository refused
369
+ once already, when it rejected a freshness gate that could fail a fresh clone
370
+ on somebody else's release. Matching them by long name is what keeps the
371
+ exclusion from hiding one of ours: `-h, --help` and a hypothetical `-j,
372
+ --json` are the same shape, so a shape test would silently drop a real flag
373
+ the day anybody adds a short one.
374
+
375
+ The two rosters are asserted apart because they are separately falsifiable: a
376
+ command list that is exactly right can belong to commands whose flags have all
377
+ been renamed, and every flag can be correct on a command that should not
378
+ exist. Measured — perturbing the CLI with one added command *and* one added
379
+ flag failed both scenarios, while an added command alone would have failed
380
+ only the first, since the option scenario iterates the registry's list and
381
+ would never have visited it. Eight child processes, one per help page,
382
+ spawning `process.execPath` with `bin/attest.js` rather than the installed
383
+ shim, so no shell is involved on either platform; both scenarios carry an
384
+ explicit `timeoutMs` because the CLI's start-up cost puts the option scenario
385
+ over Vitest's 5 s default.
386
+
387
+ No engine code changed, no command or flag moved, and no `--json` field or
388
+ issue `code` was added: this is a self-requirement and a spec file.
389
+
390
+ - **`ATX-86` gates the shape of what `.` exports, ahead of the release that
391
+ freezes it.** Nothing in the suite enumerated the public surface, and the
392
+ three things that looked as though they did each answered a different
393
+ question: `tests/consumer.spec.ts` asserts that every specifier in the
394
+ `exports` map *resolves*, which is reachability rather than shape;
395
+ `tests/smoke.spec.ts` asserts four runtime exports are functions, and is
396
+ silent about the eleven type-only names beside them and about an export
397
+ arriving that nobody intended; and the four `*.spec-d.ts` files pin the
398
+ behaviour of particular types against `src/` rather than against the emitted
399
+ declarations. So dropping a type export, or widening `Issue.code` back to
400
+ `string`, broke nothing a contributor runs — on the surface `What 1.0 freezes`
401
+ promises not to move.
402
+
403
+ **Stated over the built package and attested over its two artifacts
404
+ separately**, which is `ATX-79`'s shape and is not an analogy to it. `export
405
+ type` is erased whole at emit, so `Issue` is a name in `dist/index.d.ts` and is
406
+ absent from `dist/index.js`, and a consumer importing it as a value gets
407
+ nothing. That asymmetry is the claim rather than an accident of it, and it is
408
+ invisible to any walk over `src/index.ts`, where both kinds are one export
409
+ list. The declared lists live in `params`, so each scenario compares the
410
+ registry against the artifact and the two terms are independent in both
411
+ directions: a barrel the registry does not follow goes red, and so does the
412
+ reverse.
413
+
414
+ **What it does not close, stated here rather than found later.** An edit that
415
+ removes a name from the barrel *and* the registry moves both terms together
416
+ and stays green. That is a deliberate registry edit, visible as a `SPEC.md`
417
+ diff under `render --check`, and review is the gate for it — the same answer
418
+ the foot of this file reaches for a param whose value moves. The count pinned
419
+ beside each comparison is the independent term against the careless half of
420
+ that case and is not a claim to have closed it.
421
+
422
+ No engine code changed, no command or flag moved, and no `--json` field or
423
+ issue `code` was added: this is a self-requirement and a spec file. The entry
424
+ that proposed it was filed under `Under consideration` on 2026-09-01 from a
425
+ comparative read of `nostics` 1.2.0, promoted to `Planned` the same day once
426
+ re-measuring corrected its cost, and the correction is the reason it was
427
+ buildable immediately — it had priced a build coupling that `self/` already
428
+ carries, having considered only `tests/` and the consumer suite as homes.
429
+
430
+ ### Changed
431
+
432
+ - **`uncovered-requirement` stops advertising an affordance that has never
433
+ existed.** The message read *"Add a scenario, or mark it deferred"*, and
434
+ `deferred` appears nowhere else in the project: not a field of
435
+ `RequirementSchema`, not a flag on any command, not a key in the report. The
436
+ troubleshooting section for the code quoted the message as its symptom and
437
+ then explained only the first half, because there is no second half to
438
+ explain. So the one place a reader looks when they hit this error named an
439
+ escape hatch, in shipped output, in every release to date. It now names the
440
+ two exits that exist — write a scenario, or delete the entry — and
441
+ troubleshooting says why there is no third: an id Attest holds is an
442
+ obligation it holds permanently.
443
+
444
+ **The trace of the affordance having been intended is kept rather than
445
+ deleted**, which is why this waited. `Considered and rejected` gains the
446
+ entry that found it (*Retiring a requirement*), and design §11 gains the
447
+ permanence as a property to design against rather than a gap awaiting a
448
+ mechanism. Rewording the message alone would have removed the only written
449
+ evidence that anybody ever meant to build the thing.
450
+
451
+ `Issue.message` is prose and not API, so no consumer branches on this and no
452
+ exit code moves; there is no `code`, `level` or `--json` field change and
453
+ nothing to bump.
454
+
455
+ - **1.0: the CLI and `.` are frozen, and the freeze ships with the gates that
456
+ enforce it.** The line itself is at the head of this file under `What 1.0
457
+ freezes` — a stability promise a reader cannot find is not one — and this is
458
+ the release that makes it binding. Decided 2026-08-31, cut 2026-09-01.
459
+
460
+ **Frozen: the CLI — its commands, options and exit codes — and the root export
461
+ `.`.** `./define` is frozen with them, on seven releases. Both are frozen on
462
+ evidence that they had stopped moving rather than on intention: the CLI's last
463
+ change of shape was `--apply` on 2026-08-03, twelve releases before the
464
+ decision, and `src/index.ts` has had its export list edited in three commits
465
+ ever.
466
+
467
+ **Not frozen, explicitly: `./inspect`.** It shipped in `[0.9.2]` with two
468
+ releases and one adapter behind it, and freezing on one adapter is the
469
+ objection this project raises against every premature interface and does not
470
+ get to waive for its own. Stating the exclusion is the point: a version number
471
+ alone would have said the opposite.
472
+
473
+ **What the freeze costs is stated rather than discovered later.** Under 1.0 a
474
+ narrowing that closes a security hole takes a major, on whatever schedule a
475
+ hole is found. `[0.11.0]` is what made that price knowable — it recorded that
476
+ such a narrowing takes the bump its definition gives it, and that the
477
+ `callsAuthoringFn` fix would have been a 2.0 twenty-five days after a 1.0 cut
478
+ at the fourth breaking commit.
479
+
480
+ **The line was decided with neither frozen surface enforced by anything, and
481
+ that is no longer true.** A frozen surface with no gate on its shape is a
482
+ promise nothing enforces, and the measurement behind the decision recorded when
483
+ each surface last *moved*, never what would notice if it moved again. `ATX-86`
484
+ and `ATX-87` in this release close both halves: the eleven type-only names and
485
+ four values of the root export, read from the emitted declaration file and the
486
+ emitted JavaScript separately, and the seven commands and seventeen options of
487
+ the CLI, read from the help it prints. Neither existed when the line was
488
+ decided.
489
+
490
+ **The exclusion list is one item longer than the four surfaces, and the fifth
491
+ is the one that moved.** The workflow document `attest init` writes into an
492
+ adopter's repository is deliberately not frozen — it is a function of the
493
+ installed version, so freezing it would freeze the workflow the tool teaches.
494
+ A 0.9.1 → 0.11.0 upgrade report measured it at **110 changed lines over 9
495
+ hunks**, still stating a `never-red` ordering rule `[0.10.0]` withdrew, with
496
+ four gates green and nothing reporting it. What 1.0 does **not** do is make
497
+ that drift detectable; the question is open at the foot of this file, and the
498
+ release is explicit about it rather than quiet.
499
+
500
+ **1.0 settles what an adopter may build on and unblocks nothing else.** The
501
+ judgement schema and any retrieval surface over the archive stay under `Under
502
+ consideration` and are explicitly 1.x. The one adoption crossing both of
503
+ `[0.11.0]`'s breaking changes paid nothing — no file edited, no peer upgrade,
504
+ no gate going red — but that sample dodged both narrowings, so what is measured
505
+ is that the release cost a *conforming* adopter zero, not that either narrowing
506
+ is cheap. And what a release costs its adopters is not known on the day it is
507
+ cut, which the foot of this file records as a general rule and which applies to
508
+ this one: the freeze is cut on the strength of the line being **correct**, not
509
+ on evidence that it is **sufficient**.
510
+
38
511
  ## [0.11.0] - 2026-08-28
39
512
 
40
513
  ### Added
@@ -985,7 +1458,7 @@ paragraph exists to make visible.
985
1458
  one scenario asserting the *discrimination* rather than two asserting each
986
1459
  side, which fails now and still refuses an implementation that warns
987
1460
  unconditionally later. This is the mechanism catching the exact defect class
988
- the tautology entry under *Under consideration* is about, on the first change
1461
+ the tautology entry at the foot of this file is about, on the first change
989
1462
  it was ever pointed at, in the author's own work.
990
1463
 
991
1464
  *A second defect surfaced after `--apply` and is worth recording because
@@ -4579,7 +5052,8 @@ field moved.
4579
5052
  the record would not need to exist at all. So it is the one thing Attest writes
4580
5053
  that cannot be checked by regenerating it, and what stands in for that check is
4581
5054
  a reviewer. (The workflow document has no gate either, but that is a decision —
4582
- `init --check` was rejected, see below not an impossibility.)
5055
+ `init --check` was rejected, and has since been reopened at the foot of this
5056
+ file — not an impossibility.)
4583
5057
  Landed in `troubleshooting.md#never-red` and under `attest archive` in the CLI
4584
5058
  reference (both languages), and in the workflow document `attest init` writes —
4585
5059
  where it also became a fourth entry in "things you must not do", beside editing
@@ -4791,9 +5265,10 @@ field moved.
4791
5265
  the first fix written: it makes both maps resolve, at 195 kB and 148 files, to
4792
5266
  serve stepping into the engine from an adopter's `node_modules` — which
4793
5267
  nothing has asked for, and which anyone auditing the reader would do from the
4794
- repository instead. This file has rejected `init --check`, `divergent-param`
4795
- and countable obligations for costing more than a demonstrated need, and that
4796
- standard does not bend for a packaging decision. Turning the two compiler
5268
+ repository instead. This file has rejected `divergent-param` and countable
5269
+ obligations for costing more than a demonstrated need and rejected
5270
+ `init --check` on the same standard, an entry since reopened on measured
5271
+ drift — and that standard does not bend for a packaging decision. Turning the two compiler
4797
5272
  options off is the cleanest published output, since it leaves no dangling
4798
5273
  `sourceMappingURL` comment, but it also removes the maps from `dist/` here,
4799
5274
  where `verify self` runs through the built CLI and they are the only thing
@@ -5389,9 +5864,10 @@ every platform, which is what makes them work on Windows at all.
5389
5864
  than assumed: there isn't one, and that is the design.** A committed
5390
5865
  instruction file is a function of the installed version, so it does go stale on
5391
5866
  upgrade — but staleness only *costs* anything if the tool also ships a gate to
5392
- turn red about it, and `init --check` is rejected on its own terms under
5393
- "Considered and rejected", together with the exposure that leaves and the
5394
- backstop that keeps it small. What belongs here is what the absence of a gate
5867
+ turn red about it, and `init --check` was rejected on its own terms, together
5868
+ with the exposure that leaves and the backstop that keeps it small — an entry
5869
+ since reopened at the foot of this file, on the first measurement of that
5870
+ exposure and the failure of that backstop. What belongs here is what the absence of a gate
5395
5871
  bought: with none to serve there is no reason to keep the prose out of the file
5396
5872
  either, so it lives in the skill, where an agent reads it with no extra call,
5397
5873
  and a skill body is loaded on demand — a long document costs context only when
@@ -6284,317 +6760,6 @@ deletion. What each entry has to carry is the evidence currently blocking the
6284
6760
  decision, because that is what a re-proposal a year from now would otherwise
6285
6761
  have to gather again.
6286
6762
 
6287
- - **An assertion that recomputes its expectation from the param the code under
6288
- test just read is a tautology, and nothing says so.** Reported by
6289
- `mine-capablanca`, which probed it by mutating the registry and running the
6290
- full gate on each mutation:
6291
-
6292
- | mutation | who reads it | `check` | `verify` |
6293
- |---|---|---|---|
6294
- | `GEN-1.maxCandidatesPerPiece` 64 → 4 | scenario only | ok | **red** |
6295
- | `BOARD-1.boardSize` 10 → 12 | app + fixtures | ok | **red** |
6296
- | `BOARD-2.maxMines` 12 → **40** | app + assertion | ok | **green** |
6297
- | `BOARD-2.minMines` 9 → **1** | app + assertion | ok | **green** |
6298
-
6299
- The mine-density band — the one number deciding whether that game is playable —
6300
- moved by 3.3× with the whole suite green, `check` silent, and `possible-drift`
6301
- correctly not firing, because a covering scenario *does* read the param.
6302
- **Read the third and fourth rows against the second, because that is the
6303
- finding.** The reporter's diagnosis was "app and spec both read it", but row 2
6304
- is app-and-spec and goes red. What distinguishes the green rows is narrower and
6305
- more useful: the assertion **derives its expectation from the same param the
6306
- code under test used**, so both sides move together and the comparison has no
6307
- independent term. Row 2's expectation lives in fixtures, which do not move when
6308
- the param does, and the mechanism works. So the failing condition is a property
6309
- of the assertion, not of who reads the value — which matters, because it means
6310
- the answer is a rule about how to write the assertion, and *not* the detector
6311
- the reporter proposed.
6312
- **What this falsifies is written in this file.** The rejection of a committed
6313
- params snapshot, below, closes on *"the mechanism that guards a param is a
6314
- scenario reading it, not a file recording it"* — a sufficiency claim, and rows
6315
- 3 and 4 are counterexamples to it. A scenario reading the param is necessary
6316
- and not sufficient. That entry stays rejected on its own argument, which was
6317
- never about detection power, but its fallback clause is now known incomplete
6318
- and is annotated accordingly. The README's §"killer move" has the same shape:
6319
- it says noticing a param *moved* is `verify`'s job "only when a scenario asserts
6320
- on the value it read from `params`" — literally a necessary condition, and read
6321
- by an adopter as a sufficient one, with the very next line (*"Read the param
6322
- inside the assertion, not beside it"*) being the instruction that produces the
6323
- tautology.
6324
- **The framing that probably resolves it: this is `never-red` wearing different
6325
- clothes.** A tautological assertion is a scenario that cannot fail, which is
6326
- the exact defect `archive`'s `never-red` gate exists to catch — and it did
6327
- catch two of them in this very repo (`CHECK-2`, `FOG-3`, both confirmed
6328
- correct by the reporter). The reason it did not catch these is structural:
6329
- **changing the value of an existing param goes through no gate at all.**
6330
- `archive` gates a change; a param edit is not a change. That is a cleaner
6331
- statement of the gap than "params can be tautological".
6332
- *Re-read against `evidence-not-ordering`, which separated that gate's
6333
- obligation from the ordering it used to be described by.* Nothing here moves:
6334
- the blocker was never that the observation had to be taken first, it is that a
6335
- param edit produces no change folder for any observation to be recorded
6336
- against. If anything the framing gets sharper — what `never-red` demands is
6337
- evidence that a scenario can fail, which is precisely what is missing for a
6338
- tautological assertion, and the reason it goes undemanded is still the absence
6339
- of a gate rather than the absence of a route to the evidence.
6340
- **That observation is now its own entry at the foot of this section**, split
6341
- out and measured on 2026-08-18, because it asks what happens when a value
6342
- moves rather than whether an assertion is a real comparison, and neither
6343
- answer settles the other. What stays here is the detector.
6344
- *What blocks the decision — three things, all of them real.* **(1)** Every
6345
- mutation above is synthetic. Across 8 commits touching `*.reqs.ts` in that
6346
- repo, **no param value was ever modified** — every `params:` line in every diff
6347
- is an addition. So the hole is proven as a logical property and its frequency is
6348
- entirely unmeasured, from one greenfield repo. **(2)** The reporter's proposed
6349
- mechanism — a WARNING when one param id is read from both a spec and a non-spec
6350
- file — is the wrong shape twice over: it fires on row 2, which is correct usage
6351
- and goes red, and it would fire on the arrangement the README actively
6352
- recommends, which is §9's "usually wrong about staleness" failure mode again.
6353
- It also needs a static sweep of the whole project for `reqs['X'].params.y`,
6354
- far past the two registry readers' current surface. **(3)** No mechanism is yet
6355
- known that separates "the assertion pins this independently" from "the assertion
6356
- echoes it" without executing the scenario twice under different values — which
6357
- is mutation testing, a much larger thing to own. *One has since been built and
6358
- scored, and it does not lift this blocker.* The syntactic candidate — flag a
6359
- scenario whose expectation is derived from the param the code under test
6360
- consumed, decided inside one body without executing anything — flags 9 params
6361
- here, of which **4 are real echoes and 5 are pinned by something outside the
6362
- body it cannot see**. Under half precision, and the failure is the one
6363
- predicted above rather than a tuning problem: what pins a value is often a
6364
- fixture or a second derivation in another file, so a single-body detector is
6365
- looking in the wrong place by construction. The newest false positive is the
6366
- clearest instance — `ATX-79.subpath` is compared against `package.json`, which
6367
- is not the code under test and does not read the registry, so the independent
6368
- term is in another file entirely. Precision has fallen on every addition
6369
- measured so far, which is the direction that matters. The score is quoted from the
6370
- suite's snapshot and re-read with the judgement-table entry above, which is
6371
- where the same measurement is used for a different decision.
6372
- **The guidance half has shipped; this entry is now only about the detector.**
6373
- "Read the param inside the assertion" was insufficient and half-wrong as
6374
- written, and `[0.5.0]` replaces it everywhere it appeared — README, the
6375
- `init` skill, `possible-drift`, and design §11 — with what the evidence
6376
- supports: **the expectation must not be a function of the param the code under
6377
- test consumed**, pinned in a fixture, a literal, or a second independently
6378
- derived value. Row 2 is the worked example, and the reporter's own
6379
- `CHECK-2`/`FOG-3` fixes are the same move. It was held back once because
6380
- rewording the central claim of the tool is not a line appended; that is what
6381
- was then done, deliberately and in one pass.
6382
- **What stays undecided is the mechanism**, and all three blockers above are
6383
- untouched by the rewording: the frequency is still unmeasured from one
6384
- greenfield repo, the reporter's proposed detector is still the wrong shape
6385
- twice over, and nothing short of mutation testing separates an independent pin
6386
- from an echo. What the guidance does not do is close the hole — a tautological
6387
- assertion is still green, still silent, and still reachable — so this stays
6388
- here rather than moving to `Considered and rejected`: documenting a trap is
6389
- not the same as deciding not to detect it.
6390
- **Re-measured after `[0.7.1]` widened `params`, on this registry rather than
6391
- the reporter's — the first time this experiment has been run here.** Twelve
6392
- mutations, each followed by the whole gate. Two results matter, and they point
6393
- opposite ways.
6394
- *The shape is now reachable here, and the widening is why.* `self/render.spec.ts`
6395
- asserts `expect(md).toContain(JSON.stringify(kinds))` over the same
6396
- `payloadKinds` it feeds to the renderer — the expectation is a function of the
6397
- value under test, verbatim row 3, and it could not have been written before
6398
- the widening because the param could not be an object. The mechanism is
6399
- sharper than a frequency count and is the strongest thing this entry has
6400
- gained: **the prescribed remedy costs in proportion to the value's
6401
- complexity.** Same repo, adjacent files, same value shape — `ATX-7` pins two
6402
- numbers with literals and goes red; `ATX-63` would have had to hand-write a
6403
- rendered JSON block, so it echoed. Depth raises the defect rate and the cost
6404
- of avoiding it along one axis.
6405
- *And its cost here is nil, which is the honest other half.* Both composite
6406
- params are fixtures for the renderer and the validator; the numbers stand for
6407
- nothing, so the green is arguably correct. Blocker **(1)** is therefore only
6408
- half retired — the shape is reproduced in a second, mature repo, and the count
6409
- of *harmful* instances is still zero. Blocker **(2)** is reinforced rather than
6410
- weakened: all four green mutations were on params `src/` never reads, so the
6411
- reporter's "read from both a spec and a non-spec file" detector would have
6412
- flagged none of them. Blocker **(3)** is untouched.
6413
- *The frequency is no longer unmeasured, and it points away from building
6414
- anything.* Blocker **(1)** rested on 8 commits of one greenfield repo. Parsed
6415
- at every one of the **38** revisions that have touched this registry — all 38
6416
- readable by the current static reader, so nothing was skipped — the history is
6417
- **57 param keys added, 0 removed, and exactly 1 value changed**: `ATX-16.staticCommands`
6418
- gaining `status`. That single edit is a roster *widening*, the safe direction,
6419
- and it is on the one list that was already pinned, so it could not have been
6420
- made without updating the pin. Two independent codebases, one greenfield and
6421
- one at 64 requirements, and the event this whole entry is about has happened
6422
- zero times in either. What that argues is not that the hole is unreal — it is
6423
- reachable, demonstrated above — but that it is **rare and silent**, which is
6424
- the profile `divergent-param` was rejected on rather than the profile that
6425
- justifies an engine rule.
6426
- *The same number says the split-out fix below has also never been triggered.*
6427
- 0 removals ever means no domain has ever been shortened here. Those pins close
6428
- a live *exposure*, not a live *loss*, and the case for them is that they cost
6429
- one line each against a failure with no symptom — not that anything was
6430
- currently broken.
6431
- *Blocker **(3)** assumed mutation testing was the only separator. A cheaper
6432
- candidate existed, has now been scored, and does not survive it.* The idea was
6433
- a **syntactic** discriminator over one test body — flag an assertion whose
6434
- expected value derives from a param that also reached the arrange/act part —
6435
- which is exactly the difference between the two cases the widening produced:
6436
- `renderMarkdown({params: {payloadKinds: kinds}})` … `toContain(JSON.stringify(kinds))`
6437
- against `expect(kinds.json.maxBytes).toBe(1_048_576)`. Scored the way
6438
- `divergent-param` should have been and was not: implemented in
6439
- `tests/intent-rule-candidates.spec.ts`, run over the specs that attest this
6440
- repo, and then **every param it fires on mutated with the whole gate run —
6441
- fifteen of them, one gate run each. Four went green; eleven went red.**
6442
- *Then the detector was found to be wrong, which is the more useful half.* Its
6443
- first implementation counted an identifier in any position as a read of a
6444
- local with that name, so `i.code` in `issues.map((i) => i.code)` looked like a
6445
- use of a param named `code` — and `code` is this repo's commonest param name.
6446
- Corrected to count reference positions only, it flags **8, not 15**, and the
6447
- seven it drops are exactly seven the mutations had already shown to be pinned
6448
- by `src/`. The honest score is therefore **4 of 8, not 4 of 15** — precision
6449
- 50%, not 27%. The conclusion does not move, because it never rested on the
6450
- precision figure; what moves is how much of the original number was the
6451
- candidate failing and how much was the measurement being wrong, and that is
6452
- worth writing down rather than quietly restating.
6453
- The remaining false positives are not near-misses. They are independent terms
6454
- the detector cannot see: **the runtime is the term** — `ATX-14`'s
6455
- `inheritedKey` is `'toString'`, and a mutated spelling is not a prototype key
6456
- at all, so the behaviour under test changes; **`src/` is the term** —
6457
- `ATX-51`'s `code` is a second copy of a string the engine emits as a literal;
6458
- and **arithmetic is the term** — `threshold + 3` builds a derived expectation
6459
- that mutation breaks.
6460
- And the four true positives fail the second test, which is the one that
6461
- decides it: **none is harmful.** `ATX-10` and `ATX-3` assert that a value was
6462
- substituted or applied, where the identity of the number is not the
6463
- obligation; `ATX-51`'s is a path prefix and `ATX-63`'s a rendering fixture.
6464
- Telling those from a mine-density band that must not move is a judgement about
6465
- whether the value *means* anything — the ground truth §0 refuses to compute
6466
- for someone else's registry. **50% precision on the shape, 0% on the harm.**
6467
- Two limits on that number, stated because a score with an unstated method is
6468
- what this file exists to prevent. The mutation has **one direction per kind**
6469
- — numbers +1, lists drop-last, strings suffixed — and direction can decide the
6470
- verdict: `ATX-40.languages` is red at 2→3 and was green at 2→1 before it was
6471
- pinned, so the true-positive count is a floor. And **recall is unmeasured**:
6472
- the eight params mutated earlier that this detector does not flag all went
6473
- red, which is eight correct negatives and not a recall figure.
6474
- The detector and its verdict table are kept in the measurement suite, with the
6475
- reach recomputed on every run and the table asserted to cover exactly what
6476
- fires — because the reach figure quoted for `compound-requirement` went stale
6477
- in this very file for want of that.
6478
- *A scoping limit on all of the above.* This repo cannot settle blocker **(2)**.
6479
- `src/` reads no param at all — every `reqs[…].params` in it is a comment or a
6480
- doc string — so params here are test vocabulary, while in the reporter's repo
6481
- they are application constants the app itself consumes. The reporter's
6482
- detector keys on exactly that difference, so it is inert here and this corpus
6483
- is evidence neither for nor against it.
6484
- *What the re-measurement did find is a different defect, and it has been split
6485
- out below* — the entry is kept whole here because the two were measured in one
6486
- pass and separating them is the finding.
6487
-
6488
- - **Whether a requirement can ever be retired, and what it costs that none can.**
6489
- The registry is at **85 requirements and 240 scenarios, 2.82 each**, and
6490
- every one is a permanent obligation: `uncovered-requirement` is an ERROR, so a
6491
- requirement without a scenario cannot exist, and nothing ever removes one.
6492
- `delta.removed` exists and `applyDelta` applies it, but `--apply` refuses to
6493
- write it back, deliberately and with the reason recorded — REMOVED "cannot say
6494
- which comments belonged to the entry it deletes". So retiring one is a manual
6495
- registry edit, which is fine as a mechanism and says nothing about whether it
6496
- is ever the right move.
6497
- *What the growth actually costs, measured on this session rather than
6498
- estimated.* One small fix took: a requirement, its scenarios, a hand-judged
6499
- row in the `OBLIGATIONS` table, two inline snapshots, a `[Unreleased]` entry,
6500
- History rows in two languages, and a regenerated `SPEC.md`. That is
6501
- proportionate for a spec framework that has to dogfood — the ceremony *is* the
6502
- product being used on itself — and it is also a number that only goes up.
6503
- *What blocks the decision, and it is not the mechanism.* It is that "retire"
6504
- has at least three meanings this file has never had to separate: a requirement
6505
- that turned out **wrong** (which should leave a record of being wrong, and
6506
- `Considered and rejected` is where that already goes); one still **true** but
6507
- no longer worth a permanently-run scenario; and one **subsumed** by a later
6508
- requirement, where the scenario should move rather than go. Only the second is
6509
- really about cost, and it is the one where deleting the intent to save the
6510
- scenario inverts what the whole tool claims — that intent outlives its tests.
6511
- Nothing here is urgent at 85. It is filed now because the moment to write the
6512
- argument down is while the reasoning is available, and because a project whose
6513
- requirement count only rises should have decided in advance what would make
6514
- one leave.
6515
- *The figures above were restated on 2026-08-08, and how they were wrong is
6516
- itself the section's own warning happening.* As filed on 2026-08-03 this entry
6517
- said "62 requirements and 160 scenarios, roughly 2.6 each" while closing on
6518
- "the number is 61" — an inconsistency inside one entry, and the closing half
6519
- was the right one. Re-measured at the entry's own commit with the current
6520
- parser: **61 requirements, 156 scenarios**, so both opening figures were wrong
6521
- on the day they were written, and only the ratio survived. Measured the same
6522
- way today it is 69 and 192, and the method is the same one `verify self`
6523
- prints — the control is that today's `self/` counted this way returns exactly
6524
- what the runner does, which is what makes the 2026-08-03 number trustworthy. The
6525
- cheap way — `grep -c 'scenario('` — answers 211 against today's true 192, and
6526
- is what would have been reached for: comments and prose count, which is the
6527
- `ATX-15` error again.
6528
- **What moved in five days sharpens the argument rather than dating it.** +8
6529
- requirements and +36 scenarios, concentrated in three sessions rather than
6530
- accrued at a rate, so nothing here is a trend line. The part worth keeping is
6531
- the ratio: 2.56 → 2.78. The cost of a requirement is not flat and it is not
6532
- falling, which is the premise the whole entry rests on, and it is the first
6533
- time this file has had two measurements of it to compare.
6534
- *A third measurement, on 2026-08-18, and it points the other way from the
6535
- second.* **79 requirements and 221 scenarios**, counted the way `verify self`
6536
- prints them, which is the control the 2026-08-08 restatement established. The
6537
- ratio runs 2.56 → 2.78 → **2.80**, so the step that carried the argument was
6538
- the first one and the second is **+0.015**, which is flat. The premise this
6539
- entry rests on now has one measurement for it and one against, and the honest
6540
- statement is the narrower one: per-requirement cost has held at about 2.8
6541
- scenarios across ten days and ten requirements. What is unchanged is the
6542
- absolute total, which only rises — that half of the argument is untouched, and
6543
- it was always the half about permanence rather than about acceleration. What is
6544
- no longer supported is the acceleration. **Reopening on cost needs the ratio to
6545
- move again**; reopening on the three meanings of "retire" needs none of these
6546
- figures, and that is still the part nobody has separated.
6547
- *A fourth measurement, on 2026-08-28, and the cost half is now closed rather
6548
- than merely unsupported.* **85 requirements and 240 scenarios**, counted the
6549
- way `verify self` prints them: the ratio runs 2.56 → 2.78 → 2.80 → **2.82**.
6550
- Three consecutive readings within 0.05 of each other, across twenty-five days
6551
- and twenty-four requirements, are not a curve that is about to move. The
6552
- reopening condition this entry set itself has now failed to trigger twice, so
6553
- the cost argument is not waiting for evidence — it has been answered, in the
6554
- negative, by its own test. What survives untouched is the half that was never
6555
- about the ratio: the absolute total only rises, and permanence is the claim.
6556
-
6557
- **The three meanings, separated — which is what the entry has been asking for
6558
- and needs none of the figures above.** They are not one question, and only one
6559
- of the three is open.
6560
-
6561
- **(1) A requirement that turned out wrong.** Not a retirement at all, and the
6562
- mechanism already exists: the record of being wrong belongs in `Considered and
6563
- rejected`, which is what that section is, and removing the id is a manual
6564
- registry edit nobody has argued against. Nothing is missing here. Filing it
6565
- under "retire" was what made the question look bigger than it is.
6566
-
6567
- **(2) A requirement still true but no longer worth a permanently-run
6568
- scenario.** The only one of the three that is about cost, and the only one with
6569
- no mechanism — and the sharpest thing this separation turned up is that the
6570
- tool has been *advertising* one. `uncovered-requirement` reads "Add a scenario,
6571
- or **mark it deferred**", and `deferred` appears nowhere else in `src/`: not a
6572
- field, not a flag, not a schema key. The troubleshooting section for that code
6573
- quotes the message as its symptom and then explains only the first half,
6574
- because there is no second half to explain. So the escape hatch this meaning
6575
- needs has been named in shipped output, and in the one place a reader looks
6576
- when they hit the error, since before anyone asked the question. That is a
6577
- defect in its own right — the message is prose and free to reword — but it is
6578
- recorded here rather than fixed there, because rewording it would delete the
6579
- only written trace that this affordance was ever intended, which is the thing
6580
- this entry exists to keep.
6581
-
6582
- **(3) A requirement subsumed by a later one**, where the scenario should move
6583
- rather than go. That is a rename, and `--apply` refuses RENAMED outright, with
6584
- the reason recorded in `splice.ts` — deleting an entry has no smaller span to
6585
- fall back to. So it is blocked downstream of a refusal nobody has proposed
6586
- lifting, which is the *same* blocker the backlink-index entry above carries.
6587
- Two open entries now rest on that one refusal, which is worth knowing: it makes
6588
- lifting it a decision with two dependents rather than one, and neither of them
6589
- is the reason it exists.
6590
-
6591
- *So the entry narrows to one meaning.* (1) is answered, (3) is somebody else's
6592
- blocker, and what is actually open is (2) — whether a requirement can stop
6593
- owing a permanently-run scenario while keeping its intent, which is the one
6594
- case where deleting the intent to save the scenario inverts what the whole tool
6595
- claims. Nothing here says it should be built; what changes is that the question
6596
- is now one question.
6597
-
6598
6763
  - **Running `verify` over a subset of the scenarios, and what a partial run may
6599
6764
  be allowed to say.** `verify` is all-or-nothing and `check` executes nothing,
6600
6765
  so there is no middle. Reported by an adoption where `verify` takes **7m33s**
@@ -6727,6 +6892,554 @@ have to gather again.
6727
6892
  and the second report of the same shape, which this measurement does not
6728
6893
  supply and does not replace.
6729
6894
 
6895
+ *The second report arrived on 2026-08-31 and it is not the second report this
6896
+ entry asked for*, which is itself the finding. A 0.9.1 → 0.11.0 upgrade report
6897
+ from `mine-capablanca` — the same adoption this entry was filed on, six weeks
6898
+ later, now at 46 spec files, 170 scenarios and a `verify` median of **564
6899
+ seconds** against the 7m33s originally reported — asks for nothing named here.
6900
+ It does not ask for a selector.
6901
+ *The cost claim reproduces a third time, and the speed argument is finished.*
6902
+ `check` against `verify` on that project is **3,403 ms against 564,279 ms**,
6903
+ and the reporter is careful that this is a lower bound on engine share rather
6904
+ than an estimate of it, since `check` also pays a process start `verify` pays
6905
+ once. Either way the direction is settled on a corpus eighteen times slower
6906
+ than this one: over 99% of a `verify` there is that project's own search engine
6907
+ running. A selector saves no engine work anywhere, and now on two registries.
6908
+ *The parity argument — the one this entry called the strongest available —
6909
+ could not be tested there, and why it could not is a fact about it.* That
6910
+ project has **no `vitest.config.ts` and no `vite.config.ts`**, no spec
6911
+ importing through an alias, and no DOM anywhere in its specs, so `config:
6912
+ false` withholds nothing a direct run would have had. `--vitest-config` is
6913
+ passed by no script and no pipeline; it never became necessary. That does not
6914
+ refute the argument, it **bounds** it: the parity gap is real only for an
6915
+ adopter carrying a config, the population is therefore smaller than "adopters",
6916
+ and nobody has yet reported being in it.
6917
+ *What that adoption does have is a divergence, and it is not the one this entry
6918
+ predicted.* On the same tree, `npx vitest run` was 170/170 green while `attest
6919
+ verify` reported `tests-red` on **two runs out of three**. The mechanism the
6920
+ reporter reaches — labelled there as the strongest available inference and not
6921
+ as an observation — is one scenario measured at **4,971 ms against Vitest's
6922
+ undeclared 5,000 ms default**: 1.3 s alone, four times slower under suite
6923
+ contention, and the only one of eleven scenarios over four seconds that
6924
+ declares no explicit `timeoutMs`. That is `CLAUDE.md`'s own timing gotcha
6925
+ arriving in somebody else's repository, where no `CLAUDE.md` carries it.
6926
+ *So the residue of this entry is a different entry.* What that reporter needed
6927
+ was not a way to run less; it was a way to learn **which** of 170 scenarios was
6928
+ red, and `verify` cannot say. That is filed separately below rather than folded
6929
+ in here, because a selector and a diagnosis are different artifacts and this
6930
+ entry has already been narrowed twice by refusing to let them merge.
6931
+ *What is left blocking this one, restated:* an adopter who carries a Vitest
6932
+ config, and whose fast loop and verdict actually diverge because of it. That is
6933
+ the whole of it. The stage-2 cost argument above is unchanged and still comes
6934
+ from this repository rather than from a user.
6935
+
6936
+ - **`verify` reports that the suite is red and cannot say which scenario, and
6937
+ the documented way to find out fails in exactly the case that needs it.**
6938
+ Filed 2026-08-31 from a 0.9.1 → 0.11.0 upgrade report from `mine-capablanca`.
6939
+ `verify` and `archive` run the child suite with `quiet: true` because the CLI
6940
+ renders its own report, so the whole of a failure is one `tests-red` issue with
6941
+ no file, no scenario and no assertion — in the human output and in `--json`
6942
+ alike. The standing answer is to run the suite yourself under Vitest's
6943
+ reporter, which is what this repository does with `pnpm debug:self`.
6944
+ *The report is that this answer is circular where it is needed.* On that
6945
+ project `npx vitest run` was green, twice, on the same tree on which `verify`
6946
+ was red two runs in three. Running the suite yourself reproduces nothing,
6947
+ because the disagreement between the two runs **is** the failure. What the
6948
+ reporter did instead was time every scenario in the suite, compare alone
6949
+ against in-suite durations, and cross-reference which of the eleven scenarios
6950
+ over four seconds declared no timeout. That is a defensible inference and the
6951
+ report labels it as one; it is not a thing a tool should require.
6952
+ *What separates this from the entry above is that nothing here is a selector.*
6953
+ No scope, no partial verdict, no `--json` field naming what ran, no
6954
+ `SCHEMA_VERSION` question. The run is exactly the run it already is, and what
6955
+ changes is only how much of the child's output survives into the report.
6956
+ *What blocks it — and the first blocker is the reason `quiet: true` is there.*
6957
+ **(1)** The CLI renders its own report, and a child suite printing its own on
6958
+ top of that is two reports disagreeing about their own format. Passing the
6959
+ child's failures through means deciding what a `tests-red` issue carries — a
6960
+ file, a scenario name, a message — and each is a new field with a
6961
+ `SCHEMA_VERSION` question attached, which is the cheapest part to price first
6962
+ because it decides whether this is additive at all. **(2)** A failure message
6963
+ is somebody else's assertion text, and this project's rule is that
6964
+ `Issue.message` is prose and never API. Carrying a Vitest assertion into an
6965
+ `Issue` puts a third party's wording in the field consumers are told not to
6966
+ branch on. That is either harmless or a category error and nobody has decided
6967
+ which, and the decision is not obvious in either direction: the wording is not
6968
+ Attest's to keep stable, which is an argument both for carrying it and against.
6969
+ **(3)** The instance that produced this report may not be a diagnosis problem
6970
+ at all. A scenario 29 ms under a default it never declared is a known trap with
6971
+ a known fix, and the cheaper answers are for `verify` to name **the timeout it
6972
+ imposed** when a scenario fails on time, or for the workflow document to say
6973
+ that a scenario anywhere near the default needs an explicit `timeoutMs`. Both
6974
+ are much smaller than passing failures through, and whether either covers the
6975
+ general case is unmeasured, because n=1.
6976
+ *What would move it:* a red `verify` on an adopter's tree whose cause is not a
6977
+ timeout. Every instance anybody has is this one.
6978
+
6979
+ **Blocker (3)'s two cheap answers are built under `[1.1.0]`, and the
6980
+ entry stays open on the other two.** `ATX-88` has `tests-red` name the timeout
6981
+ that applied and how many scenarios reached it, and the workflow document now
6982
+ says the isolated run drops your `testTimeout` along with the rest of your
6983
+ config. Neither touches (1) or (2): no field was added, no `SCHEMA_VERSION`
6984
+ question was asked, and no assertion text from the child crossed into an
6985
+ `Issue`. What the entry proposes — passing the child's failures through — is
6986
+ unchanged and undecided.
6987
+ *Two things the re-measure corrected, and one is the second cheap answer's
6988
+ whole shape.* The workflow-document line was filed as content to write, and it
6989
+ was already written: `tests-red`'s troubleshooting section carries it, worked
6990
+ example included. What was missing was a pointer from the workflow document's
6991
+ own bullet about why a green direct run does not predict the gate, which lists
6992
+ aliases, transforms and a DOM and stops before the clock. And the first
6993
+ answer's phrasing — "the timeout it **imposed**" — was wrong about who imposes
6994
+ it: Attest sets no timeout, it inherits Vitest's default by isolating, so the
6995
+ number has to be read off the resolved child config rather than stated. That
6996
+ distinction is what makes the answer correct under `--vitest-config` too,
6997
+ where the ceiling is the adopter's.
6998
+ *What this does not do, stated so the entry is not read as smaller than it
6999
+ is.* It answers whether the clock was involved. It does not name the scenario,
7000
+ the file or the assertion, so a red `verify` whose cause is not a timeout is
7001
+ exactly as opaque as it was — which is the case this entry is still waiting on
7002
+ an instance of.
7003
+
7004
+ - **Changing the value of an existing param passes no gate, and for seven of
7005
+ them there is no record either.** Split out of the tautology entry that
7006
+ headed this section on 2026-08-18, and which is now under *Considered and
7007
+ rejected*, where it was one sentence inside the
7008
+ reframing paragraph. It is a different question: that entry asks whether an
7009
+ assertion is a real comparison, this one asks what happens when the number
7010
+ itself moves, and neither answer settles the other. `archive` gates a
7011
+ **change** — a delta, proposed specs, a recorded failing run — and a param edit
7012
+ is none of those. It is one literal in a merged `*.reqs.ts`, and every gate
7013
+ reads it as fine: `check` finds it schema-valid, because a param is any JSON
7014
+ value; `possible-drift` is quiet by construction, since `ATX-8` makes it ask
7015
+ whether *any* covering scenario reads the params at all rather than whether a
7016
+ value moved; and `verify` stays green wherever the assertion derives its
7017
+ expectation from the same param, which is the pair of green rows measured in
7018
+ the entry this was split from.
7019
+ *What is left is `render`, and it was measured here rather than assumed.*
7020
+ Across **89 params on 59 of this repository's 79 requirements, 82 — 92.1% —
7021
+ are interpolated into their own statement**, so editing one changes
7022
+ `self/requirements/SPEC.md` and `render --check` fails until the document is
7023
+ regenerated. That is a review surface and not a gate: the new value lands in a
7024
+ committed generated file where a reader can see it, and nothing anywhere asks
7025
+ whether the move was intended.
7026
+ *The other seven leave no trace outside the line that was edited, and which
7027
+ seven they are is the finding rather than the count.* Five are **structurally
7028
+ uninterpolatable**: `ATX-7.payloadKinds` and `ATX-63.payloadKinds` are
7029
+ kind → payload tables, and `ATX-15.sortMethods`, `ATX-15.bannedApis` and
7030
+ `ATX-72.quotes` are lists. A statement cannot carry any of them —
7031
+ `non-scalar-interpolation` is the diagnostic that refuses it — so the params
7032
+ with no record are disproportionately the shape §2 widened the params rule to
7033
+ cover **precisely because lists and tables are the most drift-prone constants
7034
+ there are**. The remaining two, `ATX-15.comparator` and `ATX-26.code`, are
7035
+ scalars their author simply did not interpolate.
7036
+ *What this is not.* It is not a re-proposal of the committed params snapshot
7037
+ rejected below. That was rejected on the argument that the mechanism guarding a
7038
+ param is a scenario reading it rather than a file recording it, and nothing
7039
+ here disputes that or needs a second copy of the values. The observation is
7040
+ narrower: the **edit** is unremarkable to every gate, which is a different
7041
+ claim from the values being unguarded.
7042
+ *Blocker **(1)** was answered on 2026-08-31, in the direction that opens the
7043
+ entry rather than closing it.* It read that frequency is unmeasured and that
7044
+ the one corpus anybody had looked at pointed at zero: 8 commits touching
7045
+ `*.reqs.ts` in the reporting repository, every `params:` line in every diff an
7046
+ addition. A 0.9.1 → 0.11.0 upgrade report from `mine-capablanca` re-ran that
7047
+ count over the **41 revisions** that have touched the same registry, evaluating
7048
+ the file at each revision and comparing key by key rather than reading diffs,
7049
+ and it reads **2 keys added, 1 removed and 7 values changed**. The 8-commit
7050
+ figure was a sample and not an answer; it is withdrawn here and in the
7051
+ tautology entry under `Considered and rejected` that quoted it.
7052
+ *One of the seven landed with nothing capable of objecting, and it is the
7053
+ general case rather than an accident of that commit.* A single commit moved
7054
+ `AI-14.sampledCandidates` from 8 to 3 and `AI-14.samplingTemperature` from 5 to
7055
+ 20, and its entire diff is a proposal, a delta, the registry and the
7056
+ regenerated `SPEC.md`. **No spec file and no implementation file.** Nine
7057
+ implementation modules in that project import the registry and read params out
7058
+ of it at runtime, so the opponent's behaviour on the easy setting changed with
7059
+ the suite green — the scenarios read the same two values the implementation
7060
+ reads, so both sides moved together. `render --check` did not object and
7061
+ structurally cannot: it enforces that the rendering *moved*, never that the
7062
+ movement was intended. What caught it was a reviewer on ten lines of `SPEC.md`
7063
+ diff, with a 63-line proposal beside it arguing for exactly that move.
7064
+ *So review is the gate, and the second corpus says it holds for a reason
7065
+ nothing enforces.* That registry is **58 of 58** params interpolated into their
7066
+ own statement, against 82 of 89 — 92.1% — here. Every param edit there moves a
7067
+ line of `SPEC.md`, which is why the commit above was visible at all, and the
7068
+ reporter is explicit that nothing makes this true: `check` is at zero issues,
7069
+ and a declared key that no statement interpolates is not a problem at any
7070
+ severity. A 100% discipline held over 41 revisions with nothing behind it is
7071
+ one careless commit away from being 98%.
7072
+ *What blocks the decision now — and **(2)** has become the opposite of what it
7073
+ said.* **(2) A shape is obvious, and it is small.** `check` already reports
7074
+ `unbound-param` for a placeholder with no key; the reverse — a declared key no
7075
+ statement interpolates — is reported at no severity, and it is the exact static
7076
+ test for "this param's edit leaves no trace outside the line that changed". It
7077
+ needs no previous value, so it is not the rejected snapshot wearing a hat: it
7078
+ is a property of one revision. The nearest precedent is `rationale-placeholder`,
7079
+ a WARNING deliberately below `hasError` so that an existing green registry
7080
+ gains a line of output and keeps its exit code, and that is the level this would
7081
+ take. **(3) What it would fire on here is the objection.** Five of this
7082
+ repository's seven uninterpolated params are **structurally uninterpolatable** —
7083
+ `ATX-7.payloadKinds` and `ATX-63.payloadKinds` are kind → payload tables,
7084
+ `ATX-15.sortMethods`, `ATX-15.bannedApis` and `ATX-72.quotes` are lists, and
7085
+ `non-scalar-interpolation` is the diagnostic that refuses a statement carrying
7086
+ any of them. So the rule must exempt non-scalars, and what is left is
7087
+ `ATX-15.comparator` and `ATX-26.code`: two params, in one repository, that
7088
+ their author simply did not interpolate. A warning firing twice on its own
7089
+ corpus and zero times on the only other one measured is a rule looking for a
7090
+ population. And the exemption is the hole rather than a detail — the five it
7091
+ must skip are precisely the shape §2 widened the params rule to cover
7092
+ **because lists and tables are the most drift-prone constants there are**.
7093
+ *What would decide it:* an adopter whose interpolation rate is below 100% and
7094
+ whose uninterpolated params are scalars. Both corpora sit at the extremes —
7095
+ 58 of 58 there, and here the gap is almost entirely the shape the rule cannot
7096
+ see — so nobody has yet measured the population it would serve. Until then the
7097
+ answer for the five that matter may still be nothing more than a line in
7098
+ `intent-quality.md`, the four things no gate is looking at already being that
7099
+ page's subject and this being a fifth of the same kind.
7100
+
7101
+ - **The intent layer is separable from the repository it describes, and the
7102
+ verification layer is not — so a third party can audit intent and never
7103
+ obtain a verdict.** Filed 2026-08-22 from a design evaluation of using Attest
7104
+ as an external audit tool that touches none of the auditee's implementation.
7105
+ The separability is already bought and is stronger than it looks: §2 requires
7106
+ a registry to be a *literal*, no imported constant and no computed value, so
7107
+ `*.reqs.ts` names nothing in the project it is about and is a file another
7108
+ party could own outright. The four static commands execute none of the
7109
+ auditee's code, which is the posture an auditor needs and is already the
7110
+ documented reason for the split. The arrangement that follows is a two-party
7111
+ contract with `check` as its interface: the auditing party writes the SHALL,
7112
+ the developing party owes each id a scenario, and `uncovered-requirement` and
7113
+ `orphan-test` are the two directions that contract can be broken in.
7114
+
7115
+ *What blocks it, in the order that decides it.* **The verdict half cannot
7116
+ cross the boundary, and that is §1 rather than a gap.** `verify` and `archive`
7117
+ run the suite, the suite's specs import the implementation, and the environment
7118
+ those specs need is the auditee's — reachable only by trusting their
7119
+ `--vitest-config`. So what a decoupled auditor can obtain is the readable
7120
+ obligations and never `tests-red`, which is the same asymmetry `status` already
7121
+ carries and states about itself. That would be tolerable if the static report
7122
+ read as procedural, and it does not: an audit reader takes a full-coverage
7123
+ `check` as a claim about behaviour, while §11 records that coverage is binary,
7124
+ that an assertion can echo the param it is measuring, that shortening a
7125
+ list-valued param silently removes cases, that a quantifier is covered only as
7126
+ far as hand-written scenarios reach, and that two contradictory ids under
7127
+ different prefixes both pass. **The mechanism assumes one tree.** `locate.ts`
7128
+ scans a single project root, and mechanism 1 needs the spec to `import` the
7129
+ registry, so the externally-owned file has to be physically in the tree before
7130
+ any command runs — a checkout step, not a flag. Naming a registry outside the
7131
+ root would be a second answer to where intent lives, which is the shape this
7132
+ repository refuses elsewhere. **The flat ID space bills exactly here.** §11
7133
+ already records that nothing allocates a prefix and that the cost arrives when
7134
+ adoption succeeds; two parties sharing one registry *is* that case, and
7135
+ `duplicate-prefix` can only report a collision after both files were read.
7136
+ **And `--apply` writes into the other party's file.** The merge splices ADDED
7137
+ requirements into the registry, so the one command that edits a file the user
7138
+ cannot regenerate would be editing a document whose owner is not in the
7139
+ repository. Nothing about that is unsafe; it is that the ownership the
7140
+ arrangement exists to establish is the thing the merge step quietly crosses.
7141
+
7142
+ *What would settle it, and what it costs to try.* The whole arrangement is
7143
+ derivable from the design and has been run by nobody, so this is n=0 and cheap
7144
+ to move: the checkout-step form needs no engine change at all, and one
7145
+ adopter running it would say whether the two-party report is read as procedural
7146
+ or over-read as a behavioural claim. That is the measurement, and it is a
7147
+ question about the reader rather than about the code.
7148
+
7149
+ - **A freshness gate on the file `attest init` writes (`init --check`).**
7150
+ Rejected 2026-08-19, and **reopened 2026-08-31 by the first measurement of the
7151
+ exposure it was rejected on.** The rejection is kept below in full because it
7152
+ is still the argument to beat, and what beat one of its two load-bearing claims
7153
+ is a 0.9.1 → 0.11.0 upgrade report from `mine-capablanca`: the committed
7154
+ `.claude/skills/attest/SKILL.md` in that repository, written by `init` at
7155
+ 0.9.1, differs from the 0.11.0 body by **9 hunks and 110 changed lines, 403
7156
+ lines against 458**. It was found only because the reporter wrote a script
7157
+ importing `workflowBody()` out of the installed build and diffed it. `check`,
7158
+ `cover`, `verify` and `render --check` were all green with the document stale.
7159
+ *The backstop is what failed, and it failed on the worst available case.* The
7160
+ rejection's second claim is that every mistake a stale document can cause is
7161
+ already a diagnostic with a fix hint — "a registry written the old way is
7162
+ `registry-not-static`", so the agent corrects itself from the report. The stale
7163
+ text here is `never-red`'s ordering rule, which `[0.10.0]` archived as
7164
+ `evidence-not-ordering` on the grounds that the ordering was never in the
7165
+ engine. An agent reading that document is told to do something the engine does
7166
+ not require. Its behaviour is more conservative, not wrong-shaped, so **no
7167
+ issue fires at any severity** — there is nothing to correct itself from,
7168
+ because nothing it does is an error. The backstop covers a stale document
7169
+ producing bad input. It does not cover a stale document producing unnecessary
7170
+ work, and the second is what six weeks of drift actually produced.
7171
+ *The first claim survives, and it is still the objection.* A gate on this file
7172
+ does turn red on a change the user did not make and is cleared by a commit
7173
+ containing no decision. Nothing in the report disputes that. What the report
7174
+ supplies is the other side of the ledger, which the rejection priced as "one
7175
+ honest and small exposure": 110 lines, over one upgrade, undetectable without
7176
+ writing a script against `dist/`.
7177
+ *And the property that makes `init` idempotent is the same one that makes the
7178
+ drift invisible.* `skillContent()` and `workflowBody()` take no argument — not
7179
+ the version, not the project — deliberately, so that re-running `init` after an
7180
+ upgrade is always safe. The consequence is that the file carries **no version
7181
+ stamp**: nothing in the adopter's repository records which version wrote it,
7182
+ and *"Re-run `attest init` after upgrading"* — which the CLI prints once, at
7183
+ `init` time, to a reader who is not upgrading — is only actionable by somebody
7184
+ who already knows they need to. A diff against the installed build is not one
7185
+ detector among several. It is the only one the file's own design permits.
7186
+ *What is undecided is the shape, and the rejection narrows it rather than being
7187
+ answered by it.* A red `init --check` in CI is the thing that was rejected, and
7188
+ the report does not rehabilitate it: it would fail a fresh clone on somebody
7189
+ else's release, which is the same standard `render --check` was held to when a
7190
+ freshness gate that calls a fresh clone stale was called worse than no gate.
7191
+ What was never proposed, and is not what the heading names, is a **notice on a
7192
+ command the adopter is already running** — the same information at the cost of
7193
+ no gate and no chore commit. Pricing that is the next step; it is not a
7194
+ decision already taken, and it needs its own answer to which command, at what
7195
+ level, and whether a WARNING nobody can clear without running `init` is a
7196
+ chore in a diagnostic's clothes after all.
7197
+ *What is still unmeasured:* whether the drift cost that reporter anything. 110
7198
+ changed lines measures the document, not the work, and the report is explicit
7199
+ that it did not run `init` and did not act on the withdrawn rule. Nobody has
7200
+ yet followed a stale instruction and paid for it, and the rejection below is
7201
+ entitled to that until somebody has.
7202
+
7203
+ **Priced on 2026-09-02, per `How an entry is re-measured`, and the three
7204
+ questions have answers: `check`, `INFO`, and no — the chore is the exit code
7205
+ rather than the line.**
7206
+ *The re-measure corrected the entry's framing of the detector first.* "A diff
7207
+ against the installed build is the only detector the file's own design
7208
+ permits" reproduces exactly, and what it makes reachable is the opposite of
7209
+ how it reads: the comparison needs no version stamp, because `skillContent()`
7210
+ is already inside the process that would report. Measured against this
7211
+ repository's own copy — byte-equal and document-equal at 26,584 bytes both
7212
+ sides — and against the other two targets, which are **absent**, that being
7213
+ the population the notice has to be right about. Cost is **0.234 ms** per run
7214
+ for all three comparisons plus **4.19 ms** to import `targets.ts` and through
7215
+ it the 26 KB template in `skill.ts`, which `check` does not import today;
7216
+ `check self --json` is 814–1,183 ms over three runs, so the notice is **~0.5%**
7217
+ of the command it rides on.
7218
+ *Which command, and the objection to it is answered by precedent rather than
7219
+ by argument.* `check` is the only command an adopter runs unconditionally and
7220
+ is the one the workflow document tells the agent to run. `status` takes a
7221
+ `<change>`, so it is silent during exactly the brownfield adoption whose
7222
+ document is stalest; `cover` and `render` are narrower; `verify` and `archive`
7223
+ are the expensive verdicts, where a non-verdict line is noise at the worst
7224
+ available price. The objection is that this would be `check`'s first finding
7225
+ that is not about the project's intent layer — and it would not be:
7226
+ `compilerIssue()` is the first statement of `runCheck`, and
7227
+ `compiler-unsupported` is a finding about the *installed environment*, at
7228
+ ERROR. What ATX-16 scopes `check` to is executing nothing, never intent only.
7229
+ *What level, and `WARNING` is wrong for a reason the roster states.* All three
7230
+ existing WARNINGs — `rationale-placeholder`, `requirement-open`,
7231
+ `orphan-from-failed-registry` — are defects in the reader's own intent layer
7232
+ that the static half declines to fail on. A stale document is neither a defect
7233
+ nor theirs. `INFO` has exactly one member, `possible-drift`, and it is this
7234
+ shape precisely: something here may be drifting and nothing can prove it cost
7235
+ you anything — which is also what this entry's own unmeasured half says.
7236
+ `ERROR` is `init --check`, and that stays rejected.
7237
+ *The chore question dissolves at that level, on a fact about when the drift
7238
+ appears.* The rejection's standard is a gate that "turns red on a change the
7239
+ user did not make". Drift appears only when the installed version moves, which
7240
+ is a lockfile edit and is a change they made; a fresh clone at a pinned version
7241
+ compares equal and is silent, so the failure mode that forced `render --check`
7242
+ off byte comparison — calling a fresh clone stale — cannot occur here. What
7243
+ makes a thing a chore is the exit code, not the line, and at INFO there is
7244
+ nothing to clear: a reader who ignores it forever pays what they pay today.
7245
+ *The repair makes one thing reachable that the entry does not contain, which
7246
+ is the question the method exists to ask.* `runInit` writes with `writeAtomic`
7247
+ unconditionally — no merge, no backup — and the notice cannot tell a stale file
7248
+ from an edited one. Today that overwrite happens only when the user chooses to
7249
+ run `init`; a notice prompts them into it on the tool's schedule, so the fix
7250
+ hint has to say that `init` rewrites the file rather than merely naming the
7251
+ command. `targets.ts` rule 1 makes that defensible — Attest owns the path, and
7252
+ that is the whole reason it may overwrite — but defensible and unstated are
7253
+ different things.
7254
+ *And the obvious implementation is wrong twice, both times by copying
7255
+ `staleIssue`, which is the reference implementation this entry points at and
7256
+ therefore the one nobody re-reads.* **(a)** It emits `missing-spec-doc` for an
7257
+ absent file. Here an absent file is a project that never ran `init` — two of
7258
+ this repository's own three targets — and firing on it would be the tool
7259
+ scattering findings about editors nobody uses, which is the argument
7260
+ `DEFAULT_TARGET` already makes. Absent must be silent, so this takes one code
7261
+ and not two. **(b)** `sameDocument` normalises CRLF and has to here, for the
7262
+ reason `render --check` needs it: Attest does not own an adopter's
7263
+ `.gitattributes`. That is the **opposite** of ATX-84, which compares this
7264
+ repository's copy byte-for-byte, and whose rationale already argues why the two
7265
+ must differ. The comparisons are deliberately not the same one.
7266
+ *What the re-measure found that belongs to no proposal: ATX-84's rationale
7267
+ carries the standing version of this argument, and one clause of it is now
7268
+ false.* It records that a gate here does not reverse `skill.ts`'s decision
7269
+ against shipping one, on the grounds that "every mistake an old document can
7270
+ cause is a diagnostic carrying a fix hint the agent corrects itself from".
7271
+ That is the clause the `mine-capablanca` report falsified — the withdrawn
7272
+ `never-red` ordering produces unnecessary work rather than bad input, and
7273
+ nothing fires at any severity. **Withdrawn under `[1.1.0]` on 2026-09-02,
7274
+ in all three places that carried it**, since the correction was owed whether or not
7275
+ the notice ships; what the withdrawal leaves standing is the diff-nobody-asked-for
7276
+ objection alone, which is the argument this entry now has to beat.
7277
+ *One cost the entry did not price: this repository cannot dogfood it.* `check`
7278
+ reads the project root, which here is `self/`, and the document is at the repo
7279
+ root — so `check self` would find the file absent and be correctly silent.
7280
+ ATX-84's scenario reaches two directories up because it is a test; a command
7281
+ cannot. The scenario would land on a fixture, and `self/` would give the notice
7282
+ none of the continuous exercise ATX-84's rationale calls the only detector this
7283
+ document has.
7284
+ *What it does not need, priced because the entries around it spend most of
7285
+ their length on exactly this:* no `SCHEMA_VERSION` bump — the field exists, the
7286
+ value is new, and `ok` is untouched because INFO is below `hasError`, the same
7287
+ reading that has held the version at 1 across all 41 codes. One roster entry
7288
+ and one troubleshooting section per language, both made mandatory by
7289
+ `docs-consistency.spec.ts` rather than by remembering.
7290
+ *The one form still worth arguing is the cheapest one, and it loses on the
7291
+ exposure's own shape.* A dim line after the report — `init`'s own "Re-run
7292
+ `attest init` after upgrading", moved to where it is actionable — needs no
7293
+ code, no roster entry and no section in two languages. It is also invisible
7294
+ under `--json`, and the drift in that report was found by a reader writing a
7295
+ script against `dist/`. A detector a machine cannot read reproduces the
7296
+ invisibility it exists to remove.
7297
+
7298
+ **Built under `[1.1.0]` on 2026-09-02, and the entry stays here because
7299
+ what was built is not what it names.** The priced form shipped exactly as
7300
+ priced — `check`, INFO, one code, silent on an absent file, document rather
7301
+ than bytes — as `ATX-89`, `ATX-90` and `ATX-91`. The re-measure corrected none
7302
+ of the three answers, which is worth recording against the two cases in `How
7303
+ an entry is re-measured` where it corrected an entry outright.
7304
+ *What the build added that the pricing did not have is the argument the
7305
+ approver gave, and it is better than the one written here.* This entry argues
7306
+ the level from the negative — a gate is a chore, so do not gate. The reason
7307
+ actually adopted is positive and is an **asymmetry of obligation**: keeping
7308
+ this document current is Attest's obligation, because Attest is what changes
7309
+ it, and taking the update is nobody else's. A finding whose subject the reader
7310
+ owes nothing about is exactly the finding that may only inform. That reaches
7311
+ the same level by a route that does not depend on calling anything a chore,
7312
+ and it is what `ATX-89`'s rationale carries.
7313
+ *What is left of this entry is the gate alone, and it is unchanged.* A notice
7314
+ is not a weaker gate, it is a different artifact: it can be ignored, and by a
7315
+ CI-only adopter who never reads `check` output it certainly will be. That is
7316
+ the cost of the choice rather than an oversight — the trade taken was "never
7317
+ blocks anyone" against "guaranteed to be seen". The thing that would move the
7318
+ gate is what it always was, and the notice does not supply it: an adopter who
7319
+ followed a stale instruction and paid for it. Nobody has.
7320
+
7321
+ The rejection, kept as it was written:
7322
+
7323
+ `render --check` is the obvious precedent and is the opposite case. A rendering
7324
+ is a function of the *user's registry*, so its staleness means the user moved
7325
+ intent without regenerating — a real signal, with the fix in their hands. The
7326
+ skill is a function of the *installed version*, so a gate on it turns red on a
7327
+ change the user did not make, and is cleared by a commit containing no decision.
7328
+ That is a chore wearing a diagnostic's clothes, and a gate that cannot be about
7329
+ anything the reader did is worse than no gate — the same standard 0.1.6 set when
7330
+ it gave `render` no formatting options at all. An earlier draft of `attest init`
7331
+ spent a second command, this gate and a second self-requirement on it. What that
7332
+ would have bought is immunity to one honest and small exposure: an old file
7333
+ describes an old workflow to an agent that trusts it. The backstop is that every
7334
+ mistake it can cause is already a diagnostic with a fix hint — a registry
7335
+ written the old way is `registry-not-static` — so the agent corrects itself from
7336
+ the report, and `init` is idempotent, which makes re-running it the entire
7337
+ update mechanism. Recorded here rather than left in the 0.3.0 entry that shipped
7338
+ `init`, because the queue above depends on it: a gate is the first thing
7339
+ proposed each time a new agent target is considered, and the answer must not
7340
+ need re-deciding per target.
7341
+
7342
+ *One locator moved with the entry.* "The queue above" was written from inside
7343
+ `Considered and rejected` and means the queue of agent targets, not the entries
7344
+ now above this one; the point it makes — that the answer must not need
7345
+ re-deciding per target — is unaffected by where the entry sits.
7346
+
7347
+ - **A `fix` field on `Issue`, derived from `code` the way `docsUrl` is.** From
7348
+ `nostics` 1.2.0 (vercel-labs), read 2026-09-01, whose `Diagnostic` carries
7349
+ `why` / `fix` / `docs` as three fields rather than one prose message. Most of
7350
+ what that library is for is already here and stricter — its docs link is
7351
+ authored per code with a `docs: false` opt-out, ours is derived from the code
7352
+ at render time and cannot be forgotten or disagreed with — so the `why`/`fix`
7353
+ split is the one half of it this file has no answer for.
7354
+ *What the roster says, and it is not what the proposal assumed.* The claim
7355
+ filed was that a fix is either buried in prose or absent. Reading all 41
7356
+ rendered messages in `docs/en/troubleshooting.md`: **11 state an action to
7357
+ take** (`uncovered-requirement`, `never-red`, `orphan-test`, `open-unresolved`
7358
+ and seven more), **5 state the constraint but not the action**
7359
+ (`registry-no-default`, `non-scalar-interpolation`, `unknown-target`,
7360
+ `compiler-unsupported`, `invalid-change-name`), and **25 state only what is
7361
+ wrong** — `registry-not-static` is the whole of "Value is not a literal.", and
7362
+ `tests-red`, `unbound-param`, `duplicate-requirement` and `possible-drift` are
7363
+ the same shape. So the distribution is uneven and unenforced rather than
7364
+ absent, which is a weaker premise than the one filed but a more precise one:
7365
+ what is missing is not the text, it is anything that makes the text obligatory.
7366
+ (Classified by reading. A first pass by pattern undercounted — it missed the
7367
+ lowercase mid-sentence imperatives in `never-red` and `empty-spec` — and its
7368
+ number is deliberately not quoted here.)
7369
+ *The derivation argument holds, and it is the reason to file this at all.* 31
7370
+ of the 41 codes appear as a `code: '…'` literal, across 43 sites; the other 10
7371
+ are built by a ternary or handed through a variable, which is the shape
7372
+ `types.ts` narrows `code` to `IssueCode` for rather than policing with a regex.
7373
+ A `fix` authored at issue sites inherits that split and can be forgotten at any
7374
+ of the 43. A `fix` keyed on `code` and resolved when a report is rendered
7375
+ cannot be — it covers all 41 including the 10, which is exactly the argument
7376
+ `docs.ts` already makes for the link, applied to the other half of the line.
7377
+ *What blocks it is that the text would then exist four times.* Every one of the
7378
+ 41 codes already has a `**Solution**` block in `docs/en/troubleshooting.md` and
7379
+ its counterpart in `docs/zh` — 41 sections each, checked by
7380
+ `docs-consistency.spec.ts` for existence and by nothing for content. A `fix`
7381
+ string makes a fourth statement of the same thing, and no gate can check the
7382
+ four agree. That is the failure mode `docsUrl` was designed to avoid, arriving
7383
+ through the field added next to it.
7384
+ *And a surface the proposal had not counted: 80 Symptom blocks.* Each of the 41
7385
+ sections opens with a fenced block quoting real output, in both languages — 82
7386
+ blocks, of which **80 quote the terminal rendering** and would be dated the
7387
+ moment `formatIssues` gains a `fix:` line. (The other two are `internal-error`,
7388
+ whose Symptom is a `--json` document, so it is the one section a rendering
7389
+ change leaves alone. It is worth naming rather than rounding away: it is the
7390
+ same carve-out `docs-consistency.spec.ts` already makes for that code.) **No
7391
+ test reads any of them** — that suite asserts the code-to-section
7392
+ correspondence and the pinned docs URLs, and has no assertion over a Symptom
7393
+ block at all — so nothing would demand the 80 edits and nothing would catch
7394
+ them being skipped. The documentation cost is larger than the feature.
7395
+ *What would move it.* Either a gate binding a code's `fix` to its `Solution`
7396
+ section, so the fourth copy is checked rather than merely written — which would
7397
+ also retire the 82-block exposure by making one of the two a function of the
7398
+ other — or an adoption report that one of the 25 action-less messages actually
7399
+ cost somebody a round trip. Without the first, this trades a derivable field for
7400
+ an underivable duplicate.
7401
+
7402
+ - **A Stop hook that will not let an agent end a turn while `check self` is red.**
7403
+ From the same read: `nostics` ships `hooks/consume-diagnostic.mjs`, a Claude
7404
+ Code Stop hook that reads the log its dev-server collector writes and returns
7405
+ `{"decision": "block"}` with the diagnostic as the reason, so the agent keeps
7406
+ working rather than stopping on a red application. Three details there are the
7407
+ design and not the plumbing, and are the transferable part: it guards on
7408
+ `stop_hook_active` so a blocked stop cannot loop; it feeds back **one**
7409
+ diagnostic per turn rather than the whole log; and when it consumes one it drops
7410
+ every other entry sharing that code and file, so a fault at twenty call sites is
7411
+ one round trip and not twenty.
7412
+ *Why the shape fits this repository specifically.* `CLAUDE.md`'s "Before
7413
+ committing" and the whole of `pre-commit-check` are discipline: nothing runs
7414
+ `check self` unless a person or an agent remembers to, and the repeat-grouping
7415
+ that hook does by hand is something `formatIssues` already computes. `.claude/`
7416
+ here holds five skills, no hooks and no `settings.json`, so this would be the
7417
+ first of its kind rather than an extension of anything.
7418
+ *Measured cost, 2026-09-01.* `node bin/attest.js check self --json` runs in
7419
+ **1.04–1.57 s** over three consecutive runs; `status self --json` in 0.78 s.
7420
+ That is paid at the end of every turn, which is the number to weigh rather than
7421
+ the wall clock of a single invocation.
7422
+ *What blocks it is the stale-`dist/` trap, and a hook is the worst place to meet
7423
+ it.* `bin/attest.js` loads `dist/`, not `src/` — the Gotcha this repository
7424
+ already keeps — so a Stop hook reports on the last build. After a `src/` edit it
7425
+ blocks on a diagnostic the working tree has already fixed, or clears while the
7426
+ tree is broken, and in both directions it is a gate arguing about a state nobody
7427
+ is in. Either the hook builds first, adding seconds to every turn, or it runs the
7428
+ engine from source and stops being the thing an adopter could copy.
7429
+ *And the larger question it sits in front of.* If this is right here it is right
7430
+ for an adopter, which makes it a question about what `attest init` writes rather
7431
+ than about `.claude/` in this repository. A hook that can block an agent's stop
7432
+ is a much larger commitment than a document, and the rejection quoted in full
7433
+ inside the `init --check` entry in this section — the gate on the installed
7434
+ skill's freshness — turned on exactly the distinction it needs:
7435
+ a signal about work the reader actually did, against a chore the tool imposes. A
7436
+ block on `check` is the first kind, which is the argument for it, and that
7437
+ argument should be made explicitly rather than arrived at by shipping.
7438
+ *What would move it.* Running it here, unshipped and repo-local, for a few weeks,
7439
+ and counting how often it blocks on something that would otherwise have been
7440
+ committed. If that count is zero the discipline was already working and the hook
7441
+ is overhead; if it is not, the number is the case for `init` writing one.
7442
+
6730
7443
  - **An outward-facing form of the judgement tables, so an adopter can run this
6731
7444
  counter-pressure on their own registry.**
6732
7445
  `tests/intent-rule-candidates.spec.ts` maintains four hand-judged tables over
@@ -6767,26 +7480,29 @@ have to gather again.
6767
7480
  is real: the grep is what caught a deleted pin that `toContain` had passed.
6768
7481
  *Why no detector ships with it, and this is the part that is already
6769
7482
  measured.* Three rule families were built and scored against the whole corpus
6770
- (86 entries, counting a fixture id once per registry file) before any was
7483
+ (104 entries as of 1.1.0, counting a fixture id once per registry file) before any was
6771
7484
  written into the engine, which is the order `divergent-param` was originally
6772
7485
  designed in the wrong way round. Every figure below is a quotation of the
6773
7486
  suite's inline snapshots; the snapshot is the fresh copy, and this paragraph
6774
7487
  is only ever as fresh as its last edit.
6775
- **(1) `compound-requirement`** — 34 requirements carry more than one
6776
- obligation, it flags 16, is **wrong about none**, and misses 18. Never wrong
6777
- and nearly blind, and the 18 are the form the coverage incentive actually
7488
+ **(1) `compound-requirement`** — 42 requirements carry more than one
7489
+ obligation, it flags 18, is **wrong about none**, and misses 24. Never wrong
7490
+ and nearly blind, and the 24 are the form the coverage incentive actually
6778
7491
  rewards: extra clauses under a single keyword. **(2) The widened variant** —
6779
- 29 correct, but 14 wrong and still 5 missed, and it is silenced by deleting an
7492
+ 36 correct, but 14 wrong and still 6 missed, and it is silenced by deleting an
6780
7493
  "and". A rule whose cheapest fix degrades the artifact it protects does not
6781
7494
  ship, and its precision falls as the registry grows. **(3) `divergent-param`**
6782
- 5 divergences on this repo, **all 5 judged wrong**, zero true positives ever
6783
- recorded. The `code` group now spans 19 ids, and the failure is structural
7495
+ 9 divergences on this repo, **all 9 judged wrong**, zero true positives ever
7496
+ recorded. The `code` group now spans 22 ids, and the failure is structural
6784
7497
  rather than a matter of naming: the key is built by discarding the evidence of
6785
7498
  relatedness, so the one genuinely paired divergence in the group is the one it
6786
7499
  cannot show, and the key has churned three times under `flag` on additions
6787
- that changed no verdict. A fourth candidate the echo detector, scored at
6788
- half precision belongs to the tautology entry above and is not re-quoted
6789
- here, because a figure stated in two places goes stale in one. Taken together
7500
+ that changed no verdict, and once more under `code` at 1.1.0. `level` arriving
7501
+ there as a fifth param name is the second key whose two requirements genuinely
7502
+ *are* related `ATX-60` and `ATX-89` are both about the severity ladder and
7503
+ the rule still relates them by the one thing that is not the relation. A fourth candidate — the echo detector, scored at
7504
+ half precision — belongs to the tautology-detector entry now under
7505
+ *Considered and rejected* and is not re-quoted here, because a figure stated in two places goes stale in one. Taken together
6790
7506
  these are the measured range of what a token or single-body detector over
6791
7507
  prose can do here, and they are why the proposal is enforcement-only.
6792
7508
  *What blocks the decision — three things.* **(1) One adapter.** This
@@ -6817,7 +7533,10 @@ have to gather again.
6817
7533
  *What would decide it:* one judgement table maintained by someone who did not
6818
7534
  design it. If it comes back the same shape, the seam is real and the schema
6819
7535
  follows; if it comes back different, `inspect` was the whole answer and the
6820
- schema never needs to exist.
7536
+ schema never needs to exist. *(One came back on 2026-08-31, and the disjunction
7537
+ above is too clean to survive it: partly the same shape, partly not, and the
7538
+ same half is the contaminated half. The close of this entry is where that is
7539
+ read.)*
6821
7540
  **Nothing currently makes that table arrive, which is a separate problem from
6822
7541
  the three blockers and the one that gates all of them.** Raised in a product
6823
7542
  review of `[0.9.1]`–`[0.9.3]` on 2026-08-19. The deciding evidence is an
@@ -6846,7 +7565,7 @@ have to gather again.
6846
7565
  §6 already records as the category no gate reaches: the page's three samples
6847
7566
  are unasserted today, and a fourth in a directory that looks executable would
6848
7567
  be worse than a fenced block, not better, unless something runs it.
6849
- **Built under `[Unreleased]`, and the second of those two blockers was the
7568
+ **Built under `[0.11.0]`, and the second of those two blockers was the
6850
7569
  buildable one.** `examples/judgement-table/` is in the default suite, the
6851
7570
  specifier is aliased rather than relativised so the file stays copyable
6852
7571
  verbatim, and both assertions were shown to fail before it was called done.
@@ -6871,246 +7590,80 @@ have to gather again.
6871
7590
  changes is only that the five voluntary steps are now four, and the one
6872
7591
  removed was the one with a fixed answer. If the next report is still nobody,
6873
7592
  the scaffold was not what was missing, and that is worth knowing too.
6874
-
6875
- - **Prefix ownership is inferred rather than declared, and the cost arrives only
6876
- when adoption succeeds.** Design §11 records that the id grammar has no level
6877
- above the requirement: the prefix is a convention, nothing allocates it, and
6878
- `duplicate-prefix` can only report a collision that already happened.
6879
- spec-kit's answer is a roadmap table a stable id, an intent, a scope
6880
- boundary, dependenciesauthored above the specs it decomposes into. That
6881
- comparison is stated here rather than referred to, because the entry that
6882
- introduced the spec-kit reading has since been built and now sits under
6883
- `[0.9.4]` as the `open` field: an entry whose evidence rests on a
6884
- neighbour is one a promotion silently empties.
6885
- *Two things have changed since that reading, and they point opposite ways.*
6886
- The proposal is **smaller** than it looked: the mechanism partly exists.
6887
- `loadRegistry` returns `prefixOwners` first claim in sorted file order
6888
- `merge.ts` uses it to decide which file an ADDED id belongs in, and
6889
- `apply-no-prefix-owner` already refuses a prefix nobody owns. So what is
6890
- proposed is declaration replacing inference, not a new layer. And it is
6891
- **weaker**: `/inspect` shipped in `[0.9.2]`, so the cross-repository half
6892
- the one §11 says no command can see across is now something an adopter
6893
- computes from two registries in about ten lines. Attest does not have to own
6894
- it to make it possible.
6895
- *What blocks the decision:* nobody has the problem. §11 states the cost
6896
- arrives "exactly when adoption succeeds: two teams sharing a registry have
6897
- someone owning prefix allocation by hand", and no such adoption has been
6898
- reported. Building the declared form now would be deciding prefix semantics
6899
- for everyone from one repository whose five prefixes`ATX` and the four
6900
- fixture ones are each declared by exactly one file, so the allocation
6901
- question this would answer has never been asked here.
6902
-
6903
-
6904
- - **A merged requirement carries no trace of the change that produced it, and
6905
- the rename chain is the part actually lost.** `RequirementSchema` is
6906
- `statement`, `rationale`, `params`, `outOfScope`; the splice writes those and
6907
- nothing else, so after `--apply` no field and no sidecar records which change
6908
- introduced an id, which modified it, or that one id used to be another. The
6909
- delta that knew is in `archive/`, which nothing reads. Proposed from
6910
- OpenViking's `.relations.json`: links between memories live in a
6911
- directory-level sidecar, deliberately kept out of the content, and are
6912
- rendered on demand at retrieval time (`docs/design/memory-link-design.md`).
6913
- *The argument for the sidecar over a field is in their competitor analysis
6914
- rather than their design.* The same document reads GBrain, which writes links
6915
- into the page body, and records what that costs: rename a page and every
6916
- inbound link still points at the old slug, with fuzzy matching at the read end
6917
- as the only mitigation. A `supersedes:` or `introducedBy:` field on a
6918
- requirement would be that mistake in this repository's terms the registry is
6919
- a literal and a single source (§2), and a field naming a folder that a later
6920
- rename can move is a second source with nothing comparing it against the
6921
- first. A derived index has no such copy to go stale: it is recomputed from the
6922
- archived deltas every time it is read.
6923
- *What blocks the decision, restated on 2026-08-19 because half of what was
6924
- filed here is no longer true.* This entry was blocked on the empty archive and
6925
- on the corpus being rename-free, and only the second still holds. `archive/`
6926
- now has a real change in it the `open` field, put through the workflow the
6927
- same day so the store this would index exists. Its delta carries `added` and
6928
- nothing else, measured rather than assumed, so a backlink index is still
6929
- testable only against fixtures for the operation it is actually about. Note
6930
- what that says about the odds of the blocker clearing on its own: `--apply`
6931
- refuses RENAMED outright, so no change this repository archives can ever
6932
- exercise a rename until that refusal is lifted, and lifting it is a separate
6933
- decision with its own reason (`splice.ts` deleting an entry has no smaller
6934
- span to fall back to). The blocker is therefore not "waiting for a rename to
6935
- happen"; it is downstream of a refusal nobody has proposed removing. And one
6936
- question the OpenViking reading does not answer, which is whether the chain is
6937
- wanted at all. §7 applies
6938
- `RENAMED` to a `Map<id, Requirement>` and the old id is then simply gone
6939
- deliberately, as far as anything written says. The case for recovering it is an
6940
- external reference that outlives the rename, a ticket or a commit message
6941
- naming a retired id, and nobody has reported one.
6942
-
6943
- - **Changing the value of an existing param passes no gate, and for seven of
6944
- them there is no record either.** Split out of the tautology entry at the
6945
- head of this section on 2026-08-18, where it was one sentence inside the
6946
- reframing paragraph. It is a different question: that entry asks whether an
6947
- assertion is a real comparison, this one asks what happens when the number
6948
- itself moves, and neither answer settles the other. `archive` gates a
6949
- **change** — a delta, proposed specs, a recorded failing run — and a param edit
6950
- is none of those. It is one literal in a merged `*.reqs.ts`, and every gate
6951
- reads it as fine: `check` finds it schema-valid, because a param is any JSON
6952
- value; `possible-drift` is quiet by construction, since `ATX-8` makes it ask
6953
- whether *any* covering scenario reads the params at all rather than whether a
6954
- value moved; and `verify` stays green wherever the assertion derives its
6955
- expectation from the same param, which is the pair of green rows measured in
6956
- the entry this was split from.
6957
- *What is left is `render`, and it was measured here rather than assumed.*
6958
- Across **89 params on 59 of this repository's 79 requirements, 82 — 92.1% —
6959
- are interpolated into their own statement**, so editing one changes
6960
- `self/requirements/SPEC.md` and `render --check` fails until the document is
6961
- regenerated. That is a review surface and not a gate: the new value lands in a
6962
- committed generated file where a reader can see it, and nothing anywhere asks
6963
- whether the move was intended.
6964
- *The other seven leave no trace outside the line that was edited, and which
6965
- seven they are is the finding rather than the count.* Five are **structurally
6966
- uninterpolatable**: `ATX-7.payloadKinds` and `ATX-63.payloadKinds` are
6967
- kind → payload tables, and `ATX-15.sortMethods`, `ATX-15.bannedApis` and
6968
- `ATX-72.quotes` are lists. A statement cannot carry any of them —
6969
- `non-scalar-interpolation` is the diagnostic that refuses it — so the params
6970
- with no record are disproportionately the shape §2 widened the params rule to
6971
- cover **precisely because lists and tables are the most drift-prone constants
6972
- there are**. The remaining two, `ATX-15.comparator` and `ATX-26.code`, are
6973
- scalars their author simply did not interpolate.
6974
- *What this is not.* It is not a re-proposal of the committed params snapshot
6975
- rejected below. That was rejected on the argument that the mechanism guarding a
6976
- param is a scenario reading it rather than a file recording it, and nothing
6977
- here disputes that or needs a second copy of the values. The observation is
6978
- narrower: the **edit** is unremarkable to every gate, which is a different
6979
- claim from the values being unguarded.
6980
- *What blocks the decision — and the first is the one that blocks the entry this
6981
- came from.* **(1)** Frequency is unmeasured, and the one corpus anybody has
6982
- looked at points at zero: across 8 commits touching `*.reqs.ts` in the
6983
- reporting repository, no param value was ever modified — every `params:` line
6984
- in every diff is an addition. A defect nobody has committed is a hazard rather
6985
- than a report. **(2)** No shape is obvious. A param edit is not a change, so
6986
- `archive` is the wrong place; making `check` notice one needs a previous value
6987
- to compare against, which is the rejected snapshot wearing a different hat. The
6988
- only mechanism needing no stored copy is review, and review already sees the
6989
- `SPEC.md` diff for 82 of the 89. **(3)** What is left after that is seven
6990
- params in one repository, and the right answer for the five that matter may be
6991
- nothing more than a line in `intent-quality.md` — the four things no gate is
6992
- looking at already being that page's subject, and this being a fifth of the
6993
- same kind.
6994
-
6995
- - **Nothing is frozen, and the evidence this repository is waiting for requires
6996
- somebody to build on something.** Filed 2026-08-19 from a product review of
6997
- `[0.9.1]`–`[0.9.3]`. Three releases in five days each added public surface, and
6998
- as of `[0.9.3]` the CLI, `./define`, `./inspect`, the archive layout, the
6999
- judgement schema and any retrieval surface are all simultaneously open. Every
7000
- entry in this section that is blocked on an adopter — the judgement table above
7001
- being the sharpest — is asking someone to invest in a surface that is
7002
- documented as movable. That is a coherent thing to ask under 0.x and it is also
7003
- the reason the ask may never be answered.
7004
- *The proposal is a line rather than a milestone:* 1.0 freezes the CLI, `./define`
7005
- and `./inspect`, and everything currently blocked on an adopter's evidence —
7006
- the judgement schema, the backlink index, any archive retrieval — is explicitly
7007
- 1.x. What that buys is not a version number; it is the only condition under
7008
- which "build your own counter-pressure on our export" is a reasonable thing to
7009
- ask of somebody.
7010
- *What blocks it.* Freezing `./inspect` five days after shipping it is freezing
7011
- on one adapter, which is the objection this repository raises against every
7012
- other premature interface and does not get to waive for its own. And the
7013
- release-notes discipline cuts both ways here: a 1.0 is a claim about what the
7014
- cost of adoption will be, and `How an entry is re-measured` records that **what
7015
- a release costs its adopters is not known on the day it is cut**. There is no
7016
- measurement to date of anyone adopting either export.
7017
-
7018
- **Measured on 2026-08-28, before deciding anything. The one-adapter objection
7019
- splits rather than holds, and a prerequisite this entry does not name has
7020
- arrived on its own.**
7021
-
7022
- *The surface has not been moving, which is not what "three releases in five
7023
- days each added public surface" suggests.* Each entry point's last change to
7024
- its exported shape, and how many releases have been cut since without touching
7025
- it:
7026
-
7027
- | surface | last shape change | releases since |
7028
- |---|---|---|
7029
- | CLI commands and options | 2026-08-03 (`--apply`) | 12 |
7030
- | `.` — the index exports | 2026-07-26 | 19 |
7031
- | `./define` — public signatures | 2026-08-08 | 7 |
7032
- | `./inspect` | 2026-08-18 | 2 |
7033
-
7034
- `src/index.ts` has had its export list edited in **three commits ever**, the
7035
- last of them five weeks ago. So "freezing on one adapter" is weak for the CLI
7036
- and `.` — nineteen and twelve releases of evidence that they have stopped
7037
- moving — and strong exactly where this entry puts it, on `./inspect`, which
7038
- has two releases, one adapter, and whose single post-shipping change was
7039
- additive. That argues for a line this entry does not currently offer:
7040
- **freeze what has stopped moving and explicitly do not freeze `./inspect`.**
7041
-
7042
- *And the cost of any such line was demonstrated this week, unprompted.* The
7043
- Versioning section at the head of this file defines breaking to include
7044
- **rejecting previously-valid input**. There have been four breaking commits in
7045
- this project's history, the last on 2026-08-03 — and a fifth is sitting in
7046
- `[Unreleased]` as of 2026-08-28: the `callsAuthoringFn` binding fix, which is
7047
- breaking *because* it narrows what a static reader accepts, and which says so
7048
- about itself. Under 1.0 that is a **major** bump. A fix closing a wrong-answer
7049
- hole in the four commands documented as safe on an untrusted fork MR would
7050
- have had to ship as 2.0, twenty-five days after 1.0 — or not ship as written.
7051
-
7052
- *So the decision has a prerequisite, and the prerequisite left this section.*
7053
- What a narrowing that closes a security hole costs a consumer is undefined,
7054
- and it is needed whether or not 1.0 is ever cut — so it is under `Planned`
7055
- above rather than here, where it would have been blocked on everything this
7056
- entry is blocked on. Until it exists a 1.0 would be promising something whose
7057
- price nobody has computed; what it does **not** do is unblock the rest of this
7058
- entry, which turns on evidence no measurement here can supply.
7059
-
7060
- *What is still unmeasured is unchanged, and it is the whole of the rest.* No
7061
- adopter of either export, so the thing 1.0 is meant to buy — making "build
7062
- your own counter-pressure on our export" a reasonable ask — has no measurement
7063
- behind it in either direction. The `examples/` scaffold under `[Unreleased]`
7064
- is the other half of that ask and was the cheaper half. Whether the missing
7065
- half is the freeze, or was never the freeze, is what the next report answers.
7066
-
7067
- - **The intent layer is separable from the repository it describes, and the
7068
- verification layer is not — so a third party can audit intent and never
7069
- obtain a verdict.** Filed 2026-08-22 from a design evaluation of using Attest
7070
- as an external audit tool that touches none of the auditee's implementation.
7071
- The separability is already bought and is stronger than it looks: §2 requires
7072
- a registry to be a *literal*, no imported constant and no computed value, so
7073
- `*.reqs.ts` names nothing in the project it is about and is a file another
7074
- party could own outright. The four static commands execute none of the
7075
- auditee's code, which is the posture an auditor needs and is already the
7076
- documented reason for the split. The arrangement that follows is a two-party
7077
- contract with `check` as its interface: the auditing party writes the SHALL,
7078
- the developing party owes each id a scenario, and `uncovered-requirement` and
7079
- `orphan-test` are the two directions that contract can be broken in.
7080
-
7081
- *What blocks it, in the order that decides it.* **The verdict half cannot
7082
- cross the boundary, and that is §1 rather than a gap.** `verify` and `archive`
7083
- run the suite, the suite's specs import the implementation, and the environment
7084
- those specs need is the auditee's — reachable only by trusting their
7085
- `--vitest-config`. So what a decoupled auditor can obtain is the readable
7086
- obligations and never `tests-red`, which is the same asymmetry `status` already
7087
- carries and states about itself. That would be tolerable if the static report
7088
- read as procedural, and it does not: an audit reader takes a full-coverage
7089
- `check` as a claim about behaviour, while §11 records that coverage is binary,
7090
- that an assertion can echo the param it is measuring, that shortening a
7091
- list-valued param silently removes cases, that a quantifier is covered only as
7092
- far as hand-written scenarios reach, and that two contradictory ids under
7093
- different prefixes both pass. **The mechanism assumes one tree.** `locate.ts`
7094
- scans a single project root, and mechanism 1 needs the spec to `import` the
7095
- registry, so the externally-owned file has to be physically in the tree before
7096
- any command runs — a checkout step, not a flag. Naming a registry outside the
7097
- root would be a second answer to where intent lives, which is the shape this
7098
- repository refuses elsewhere. **The flat ID space bills exactly here.** §11
7099
- already records that nothing allocates a prefix and that the cost arrives when
7100
- adoption succeeds; two parties sharing one registry *is* that case, and
7101
- `duplicate-prefix` can only report a collision after both files were read.
7102
- **And `--apply` writes into the other party's file.** The merge splices ADDED
7103
- requirements into the registry, so the one command that edits a file the user
7104
- cannot regenerate would be editing a document whose owner is not in the
7105
- repository. Nothing about that is unsafe; it is that the ownership the
7106
- arrangement exists to establish is the thing the merge step quietly crosses.
7107
-
7108
- *What would settle it, and what it costs to try.* The whole arrangement is
7109
- derivable from the design and has been run by nobody, so this is n=0 and cheap
7110
- to move: the checkout-step form needs no engine change at all, and one
7111
- adopter running it would say whether the two-party report is read as procedural
7112
- or over-read as a behavioural claim. That is the measurement, and it is a
7113
- question about the reader rather than about the code.
7593
+ **Decided 2026-08-31: build the counting rule, and not the schema.** The
7594
+ blocker was whether an empty frame reads as an invitation or as a form to fill
7595
+ in, it could not be tested here, and a 0.9.1 0.11.0 upgrade report from
7596
+ `mine-capablanca` tested it. The answer is **invitation**, and the report names
7597
+ the sentence that did it: not the empty `interface Judgement`, which reads as a
7598
+ stub and sent the reporter looking for what belonged in it, but the comment
7599
+ above it*"It ships with no columns, and that is the finished state of this
7600
+ file rather than a gap in it."* Declaring the absence finished is what turns a
7601
+ blank into a question, and it is the cheapest sentence in the example.
7602
+ *The forcing move was one the build found rather than one it proposed.* The
7603
+ instruction to add the first column as a **required** field made all 80 rows
7604
+ stop compiling at once, and there was then no path back to green that did not
7605
+ go through having an opinion about all 80 *"a form gets partially filled in;
7606
+ a type does not"*. That is the type-level forcing move described above,
7607
+ working on somebody who did not write it.
7608
+ *And the frame leaks in one place, which is a defect in the example rather
7609
+ than in the idea.* `pin` is documented as not a judgement column and is the
7610
+ only concrete field in the interface, so the reporter's first act was to
7611
+ populate it for all 80 rows mechanically *"there is a field, populate it"*.
7612
+ Defensible, since it makes rot-binding bite for 80 rows instead of none, and
7613
+ exactly the form-filling behaviour the frame exists to prevent. Generating
7614
+ `pin` from `inspectProject` rather than leaving a field to be typed is the fix,
7615
+ and it belongs in the example.
7616
+ *What does **not** follow, and the report is why it does not.* Two of the three
7617
+ columns that came back are the same shape as two of this repository's
7618
+ `clauses` is `OBLIGATIONS`, `quantifier` is a flattened `QUANTIFIED` — and it
7619
+ is tempting to read that as the seam being real. It cannot be read that way:
7620
+ the reporter names the `attest init` workflow document as prior influence, and
7621
+ that document discusses quantifiers and param pinning at length. So the
7622
+ convergence says **something** transmits those two ideas, and the document is
7623
+ at least as likely a channel as the frame. What is uncontaminated is the third
7624
+ column, `venue`, which has no counterpart here: nine of its rows record a
7625
+ promise held by a Playwright suite Attest's scope does not reach. A column this
7626
+ repository could not have written came back, which is a seam behaving like one.
7627
+ *What to build is smaller than a schema, and the three things the reporter
7628
+ wanted and did not get are its specification.* **(1) A counting rule for
7629
+ "obligation" precise enough that two people count the same.** The report
7630
+ distrusts its own `clauses` column above all the others and says why: two
7631
+ tables that count differently are not comparable, which defeats the point of
7632
+ there being a second one. A counting rule is content that does **not** depend
7633
+ on the adopter the same category as the two mechanical assertions — and it is
7634
+ currently withheld along with the columns, which is the wrong side of the
7635
+ split. **(2) A way to bind a clause to a scenario rather than a row.**
7636
+ Rot-binding binds a row to *a* scenario; nothing binds the sixth obligation of
7637
+ a six-clause requirement to anything, so a six-clauses-three-scenarios row is a
7638
+ number a reader has to notice instead of an assertion that fails. **(3)
7639
+ Somewhere for a row to say the promise is held where Attest cannot see it.**
7640
+ Nine `ui-node` rows and a 902-line Playwright suite, citable only in prose.
7641
+ *So what is planned is (1) and the `pin` fix, and (2) and (3) explicitly are
7642
+ not.* (1) is a paragraph in the example, costs no engine surface, and is the
7643
+ one thing that makes a second table comparable to the first without it two
7644
+ tables are two vocabularies and the seam cannot be read either way. (2) and (3)
7645
+ are both new schema, and blocker **(1)** of the proposal above one adapter
7646
+ is not answered by a report that is the second adapter's first hour. Two hours
7647
+ of one agent is not two adapters. **The `*.judge.ts` schema stays undecided**,
7648
+ and when it moves it will move on a second table that somebody has maintained
7649
+ rather than built, which is a different measurement from the one that arrived.
7650
+ **Both are built under `[1.1.0]`, and moving here on 2026-09-01 is what
7651
+ follows from that.** The counting rule and the anchor fix are the whole of what
7652
+ was decided on 2026-08-31, and they are recorded there. The re-measure before
7653
+ building corrected nothing this entry claims the report's three asks
7654
+ reproduce as filed — and what it added is the finding the two cases in `How an
7655
+ entry is re-measured` would predict: the obvious implementation of "ship no
7656
+ fields" is an empty interface, and an empty interface admits `0` and `""`, so
7657
+ the frame would have gone on claiming an emptiness its own type did not hold.
7658
+ *The move is the entry's own state changing, not bookkeeping.* `Planned` means
7659
+ decided and unbuilt, and nothing left here is that: the schema, the
7660
+ clause-to-scenario binding and the out-of-scope `venue` column are all waiting
7661
+ on a second table somebody has maintained, which is the definition of this
7662
+ section rather than the one above. What it costs is that `Planned` is empty
7663
+ again the state that moved `How an entry is re-measured` out of it, so that
7664
+ the method would not go unreachable whenever this happened and that is the
7665
+ honest reading of where the project stands rather than a gap in the file:
7666
+ nothing is currently decided and unbuilt.
7114
7667
 
7115
7668
  ## Considered and rejected
7116
7669
 
@@ -7122,6 +7675,611 @@ it sat between 0.2.0 and 0.1.7 for two releases, where standing still meant
7122
7675
  sinking one version deeper each time a release was cut above it, and a rejection
7123
7676
  filed under a version reads as belonging to it.
7124
7677
 
7678
+ ### A backlink index over `archive/`, recording which change introduced or renamed an id
7679
+
7680
+ Proposed from OpenViking's `.relations.json`, restated twice on measurement, and
7681
+ rejected 2026-09-01. It is kept as it was written under *Under consideration*,
7682
+ because what closed it is the argument the entry had already assembled against
7683
+ itself rather than anything that arrived afterwards; the closing paragraphs are
7684
+ at the foot of the entry.
7685
+
7686
+ **A merged requirement carries no trace of the change that produced it, and
7687
+ the rename chain is the part actually lost.** `RequirementSchema` is
7688
+ `statement`, `rationale`, `params`, `outOfScope`; the splice writes those and
7689
+ nothing else, so after `--apply` no field and no sidecar records which change
7690
+ introduced an id, which modified it, or that one id used to be another. The
7691
+ delta that knew is in `archive/`, which nothing reads. Proposed from
7692
+ OpenViking's `.relations.json`: links between memories live in a
7693
+ directory-level sidecar, deliberately kept out of the content, and are
7694
+ rendered on demand at retrieval time (`docs/design/memory-link-design.md`).
7695
+ *The argument for the sidecar over a field is in their competitor analysis
7696
+ rather than their design.* The same document reads GBrain, which writes links
7697
+ into the page body, and records what that costs: rename a page and every
7698
+ inbound link still points at the old slug, with fuzzy matching at the read end
7699
+ as the only mitigation. A `supersedes:` or `introducedBy:` field on a
7700
+ requirement would be that mistake in this repository's terms — the registry is
7701
+ a literal and a single source (§2), and a field naming a folder that a later
7702
+ rename can move is a second source with nothing comparing it against the
7703
+ first. A derived index has no such copy to go stale: it is recomputed from the
7704
+ archived deltas every time it is read.
7705
+ *What blocks the decision, restated on 2026-08-19 because half of what was
7706
+ filed here is no longer true.* This entry was blocked on the empty archive and
7707
+ on the corpus being rename-free, and only the second still holds. `archive/`
7708
+ now has a real change in it — the `open` field, put through the workflow the
7709
+ same day — so the store this would index exists. Its delta carries `added` and
7710
+ nothing else, measured rather than assumed, so a backlink index is still
7711
+ testable only against fixtures for the operation it is actually about. Note
7712
+ what that says about the odds of the blocker clearing on its own: `--apply`
7713
+ refuses RENAMED outright, so no change this repository archives can ever
7714
+ exercise a rename until that refusal is lifted, and lifting it is a separate
7715
+ decision with its own reason (`splice.ts` — deleting an entry has no smaller
7716
+ span to fall back to). The blocker is therefore not "waiting for a rename to
7717
+ happen"; it is downstream of a refusal nobody has proposed removing. And one
7718
+ question the OpenViking reading does not answer, which is whether the chain is
7719
+ wanted at all. §7 applies
7720
+ `RENAMED` to a `Map<id, Requirement>` and the old id is then simply gone —
7721
+ deliberately, as far as anything written says. The case for recovering it is an
7722
+ external reference that outlives the rename, a ticket or a commit message
7723
+ naming a retired id, and nobody has reported one.
7724
+ *Measured against a second corpus on 2026-08-31, and it points the same way.* A
7725
+ 0.9.1 → 0.11.0 upgrade report from `mine-capablanca` walked all 41 revisions of
7726
+ that project's single registry and all 49 of its archived changes: **80 ids have
7727
+ ever appeared in it and 80 are in it now — zero removed, zero renamed** — and
7728
+ neither `removed:` nor `renamed:` appears in non-empty form in any delta.
7729
+ Commit messages on every branch, the whole tree and the issue tracker were
7730
+ searched for id-shaped tokens the registry does not define. The external
7731
+ reference this entry waits for did not turn up in sixteen days and forty-nine
7732
+ changes.
7733
+ *One id did outlive itself, from the other direction, and the reason it was left
7734
+ alone is a mechanism this entry did not have.* `AI-10` has never been in that
7735
+ registry, and two committed documents discuss the gap it leaves. The archived
7736
+ proposal's stated reason for not renumbering to close it is that doing so "would
7737
+ mean re-recording a failing run against an implementation that already exists".
7738
+ So `never-red` prices a rename directly: an id cannot be renumbered without
7739
+ re-earning its evidence, and an author facing that bill chose to write about the
7740
+ hole instead. That is a second reason the corpus is rename-free, independent of
7741
+ `--apply`'s refusal, and it argues the rate **stays** at zero rather than that
7742
+ nobody has got round to it.
7743
+ *What this does to the entry:* nothing is refuted, and the blocker is now
7744
+ blocked twice over. It was already downstream of a refusal nobody has proposed
7745
+ removing; it is also downstream of an obligation that makes the operation
7746
+ expensive on purpose. An index over an event that two independent mechanisms
7747
+ discourage is an index over an empty set.
7748
+ *Rejected 2026-09-01, on the entry's own last sentence rather than on anything
7749
+ new.* What closes it is that the two mechanisms are not a coincidence of one
7750
+ corpus and not a stage adoption passes through. `--apply` refuses RENAMED for a
7751
+ reason recorded in `splice.ts` — deleting an entry has no smaller span to fall
7752
+ back to — and `never-red` prices the operation independently, since an id cannot
7753
+ be renumbered without re-earning its evidence. The second is the stronger of the
7754
+ two because it does not depend on a refusal anybody could lift: an author who
7755
+ faced that bill wrote about the hole `AI-10` leaves instead of closing it, which
7756
+ is the mechanism arguing the rate **stays** at zero rather than that nobody has
7757
+ got round to it. An index whose subject two independent mechanisms discourage is
7758
+ not waiting for evidence; it is waiting for a decision nobody has proposed, taken
7759
+ for reasons that have nothing to do with this.
7760
+
7761
+ *What was measured, and it is the whole of the case against.* Across the second
7762
+ corpus: 80 ids have ever appeared and 80 are in it now, over 49 archived changes
7763
+ — zero removed, zero renamed — with neither `removed:` nor `renamed:` appearing
7764
+ in non-empty form in any delta. Commit messages on every branch, the whole tree
7765
+ and the issue tracker were searched for id-shaped tokens the registry does not
7766
+ define. The external reference this entry existed to serve is the one thing that
7767
+ would have justified it, and sixteen days and forty-nine changes produced none.
7768
+
7769
+ *One structural consequence, recorded because it is a change to somebody else's
7770
+ entry.* Two open entries rested on `--apply`'s refusal of RENAMED; closing this
7771
+ one leaves **one**, the third meaning under *Retiring a requirement* below. That
7772
+ matters in the direction of not lifting the refusal: a decision with two
7773
+ dependents is easier to justify than a decision with one, and the count is now
7774
+ one. Both cross-references have been corrected rather than left to be inferred.
7775
+
7776
+ *What would reopen it.* Not a report of the drift this entry describes — that
7777
+ report is what sixteen days and two corpora failed to produce. It reopens if
7778
+ `--apply`'s refusal of RENAMED is lifted for its own reason, because the
7779
+ operation then becomes available and the corpus stops being evidence about a
7780
+ population that cannot exist. That is the one event that changes the argument,
7781
+ and it is not this entry's to trigger.
7782
+
7783
+ ### A detector for an assertion that recomputes its expectation from the param under test
7784
+
7785
+ Proposed 2026-08-03 from `mine-capablanca`'s report, carried through three
7786
+ re-measurements, and rejected 2026-08-31. The defect it describes is real and
7787
+ stays undetected: an assertion deriving its expectation from the same param the
7788
+ code under test consumed has no independent term, so the value can move with the
7789
+ suite green, `check` silent and `possible-drift` correctly not firing. *What
7790
+ killed it is the detector's own score, and that is now the whole of the case.*
7791
+ The one anybody built scores **50% on the shape and 0% on the harm**: of 8 flags, 4 are real echoes and
7792
+ none of the 4 is harmful, while the false positives are pinned by a term in
7793
+ another file that a single-body detector cannot see by construction. Precision
7794
+ fell on every addition measured.
7795
+
7796
+ *The frequency half of this rejection was wrong on the day it was written, and it
7797
+ is withdrawn.* It read that frequency is "approximately zero across two
7798
+ codebases", on 8 commits in the reporting repository across which no param value
7799
+ was ever modified. A 0.9.1 → 0.11.0 upgrade report from `mine-capablanca` on
7800
+ 2026-08-31 re-ran that count properly — all **41 revisions** of the same
7801
+ registry, the file evaluated at each and compared key by key rather than read out
7802
+ of diff hunks — and it comes back with **seven values changed**. Not
7803
+ approximately zero. The breakdown belongs to the param-value entry under `Under
7804
+ consideration` and is not re-quoted here, because a figure stated in two places
7805
+ goes stale in one. The 8-commit sample was what was reachable on the day and not
7806
+ an answer to the question, and quoting it as one is the failure this file's own
7807
+ method section warns about. 38 revisions of *this* registry with 57 keys added, 0
7808
+ removed and exactly 1 value changed still stands, and is now the outlier rather
7809
+ than the confirmation.
7810
+
7811
+ *What that leaves is a rejection standing on one leg, deliberately.* A defect
7812
+ occurring seven times in six weeks in one adopting repository is not a hazard
7813
+ nobody has committed; it is a thing that happens. The detector is still refused,
7814
+ because scoring 0% on harm is a property of the mechanism that no frequency
7815
+ improves — a rule that flags four true echoes and is right about the danger of
7816
+ none of them is not made useful by there being more echoes to flag. Where the
7817
+ frequency finding goes instead is the param-value entry under `Under
7818
+ consideration`, which was always the entry about the move rather than about the
7819
+ assertion, and which the same report reinforces rather than settles.
7820
+
7821
+ *And the reopening condition was very nearly met, which is worth recording
7822
+ precisely.* It asks for a param whose value moved, whose assertion echoed it, and
7823
+ where the move was **harmful**. The report supplies the first two in one commit:
7824
+ two `AI-14` params changed, the scenarios reading the same values the
7825
+ implementation reads, the whole suite green, and a diff containing no spec file
7826
+ and no implementation file at all. The third leg fails, and fails cleanly — that
7827
+ move was the entire point of the change, argued for in a 63-line proposal
7828
+ committed beside it. So the closest instance anybody has is a **correct** edit
7829
+ that no gate could have objected to, which is evidence about the gate and not
7830
+ about the assertion.
7831
+
7832
+ *The guidance half is not what is rejected here.* `[0.5.0]` replaced "read the
7833
+ param inside the assertion" everywhere it appeared with what the evidence
7834
+ supports — the expectation must not be a function of the param the code under
7835
+ test consumed — and that shipped. What is rejected is the mechanism, and the
7836
+ hole stays open: a tautological assertion is still green and still silent.
7837
+ Documenting a trap is not detecting it, and this section is where that
7838
+ difference is recorded rather than hidden.
7839
+
7840
+ *Two things this leaves standing, neither of them a mechanism.* Design §11
7841
+ carries the property in the reader's form, so an adopter meets it where the
7842
+ method is taught. And the detector stays in
7843
+ `tests/intent-rule-candidates.spec.ts` with its verdict table recomputed on
7844
+ every run: it is the evidence that killed this, and deleting it would leave the
7845
+ rejection resting on a figure nothing checks — the exact failure this file has
7846
+ already had once, when the reach quoted for `compound-requirement` went stale in
7847
+ it.
7848
+
7849
+ *Reopening needs the one thing neither corpus supplied:* a param whose value
7850
+ moved, whose assertion echoed it, and where the move was **harmful**. Precision
7851
+ on the shape is a tuning question. Precision on the harm is the whole question,
7852
+ and telling a rendering fixture from a mine-density band is a judgement about
7853
+ whether the value *means* anything — which is the ground truth §0 refuses to
7854
+ compute for somebody else's registry. Nothing short of running each scenario
7855
+ twice under different values separates an independent pin from an echo, and that
7856
+ is mutation testing, a much larger thing to own.
7857
+
7858
+ The claim and the measurements, kept as they were taken:
7859
+
7860
+ **An assertion that recomputes its expectation from the param the code under
7861
+ test just read is a tautology, and nothing says so.** Reported by
7862
+ `mine-capablanca`, which probed it by mutating the registry and running the
7863
+ full gate on each mutation:
7864
+
7865
+ | mutation | who reads it | `check` | `verify` |
7866
+ |---|---|---|---|
7867
+ | `GEN-1.maxCandidatesPerPiece` 64 → 4 | scenario only | ok | **red** |
7868
+ | `BOARD-1.boardSize` 10 → 12 | app + fixtures | ok | **red** |
7869
+ | `BOARD-2.maxMines` 12 → **40** | app + assertion | ok | **green** |
7870
+ | `BOARD-2.minMines` 9 → **1** | app + assertion | ok | **green** |
7871
+
7872
+ The mine-density band — the one number deciding whether that game is playable —
7873
+ moved by 3.3× with the whole suite green, `check` silent, and `possible-drift`
7874
+ correctly not firing, because a covering scenario *does* read the param.
7875
+ **Read the third and fourth rows against the second, because that is the
7876
+ finding.** The reporter's diagnosis was "app and spec both read it", but row 2
7877
+ is app-and-spec and goes red. What distinguishes the green rows is narrower and
7878
+ more useful: the assertion **derives its expectation from the same param the
7879
+ code under test used**, so both sides move together and the comparison has no
7880
+ independent term. Row 2's expectation lives in fixtures, which do not move when
7881
+ the param does, and the mechanism works. So the failing condition is a property
7882
+ of the assertion, not of who reads the value — which matters, because it means
7883
+ the answer is a rule about how to write the assertion, and *not* the detector
7884
+ the reporter proposed.
7885
+ **What this falsifies is written in this file.** The rejection of a committed
7886
+ params snapshot, below, closes on *"the mechanism that guards a param is a
7887
+ scenario reading it, not a file recording it"* — a sufficiency claim, and rows
7888
+ 3 and 4 are counterexamples to it. A scenario reading the param is necessary
7889
+ and not sufficient. That entry stays rejected on its own argument, which was
7890
+ never about detection power, but its fallback clause is now known incomplete
7891
+ and is annotated accordingly. The README's §"killer move" has the same shape:
7892
+ it says noticing a param *moved* is `verify`'s job "only when a scenario asserts
7893
+ on the value it read from `params`" — literally a necessary condition, and read
7894
+ by an adopter as a sufficient one, with the very next line (*"Read the param
7895
+ inside the assertion, not beside it"*) being the instruction that produces the
7896
+ tautology.
7897
+ **The framing that probably resolves it: this is `never-red` wearing different
7898
+ clothes.** A tautological assertion is a scenario that cannot fail, which is
7899
+ the exact defect `archive`'s `never-red` gate exists to catch — and it did
7900
+ catch two of them in this very repo (`CHECK-2`, `FOG-3`, both confirmed
7901
+ correct by the reporter). The reason it did not catch these is structural:
7902
+ **changing the value of an existing param goes through no gate at all.**
7903
+ `archive` gates a change; a param edit is not a change. That is a cleaner
7904
+ statement of the gap than "params can be tautological".
7905
+ *Re-read against `evidence-not-ordering`, which separated that gate's
7906
+ obligation from the ordering it used to be described by.* Nothing here moves:
7907
+ the blocker was never that the observation had to be taken first, it is that a
7908
+ param edit produces no change folder for any observation to be recorded
7909
+ against. If anything the framing gets sharper — what `never-red` demands is
7910
+ evidence that a scenario can fail, which is precisely what is missing for a
7911
+ tautological assertion, and the reason it goes undemanded is still the absence
7912
+ of a gate rather than the absence of a route to the evidence.
7913
+ **That observation is now its own entry at the foot of this section**, split
7914
+ out and measured on 2026-08-18, because it asks what happens when a value
7915
+ moves rather than whether an assertion is a real comparison, and neither
7916
+ answer settles the other. What stays here is the detector.
7917
+ *What blocks the decision — three things, all of them real.* **(1)** Every
7918
+ mutation above is synthetic. Across 8 commits touching `*.reqs.ts` in that
7919
+ repo, **no param value was ever modified** — every `params:` line in every diff
7920
+ is an addition. So the hole is proven as a logical property and its frequency is
7921
+ entirely unmeasured, from one greenfield repo. **(2)** The reporter's proposed
7922
+ mechanism — a WARNING when one param id is read from both a spec and a non-spec
7923
+ file — is the wrong shape twice over: it fires on row 2, which is correct usage
7924
+ and goes red, and it would fire on the arrangement the README actively
7925
+ recommends, which is §9's "usually wrong about staleness" failure mode again.
7926
+ It also needs a static sweep of the whole project for `reqs['X'].params.y`,
7927
+ far past the two registry readers' current surface. **(3)** No mechanism is yet
7928
+ known that separates "the assertion pins this independently" from "the assertion
7929
+ echoes it" without executing the scenario twice under different values — which
7930
+ is mutation testing, a much larger thing to own. *One has since been built and
7931
+ scored, and it does not lift this blocker.* The syntactic candidate — flag a
7932
+ scenario whose expectation is derived from the param the code under test
7933
+ consumed, decided inside one body without executing anything — flags 9 params
7934
+ here, of which **4 are real echoes and 5 are pinned by something outside the
7935
+ body it cannot see**. Under half precision, and the failure is the one
7936
+ predicted above rather than a tuning problem: what pins a value is often a
7937
+ fixture or a second derivation in another file, so a single-body detector is
7938
+ looking in the wrong place by construction. The newest false positive is the
7939
+ clearest instance — `ATX-79.subpath` is compared against `package.json`, which
7940
+ is not the code under test and does not read the registry, so the independent
7941
+ term is in another file entirely. Precision has fallen on every addition
7942
+ measured so far, which is the direction that matters. The score is quoted from the
7943
+ suite's snapshot and re-read with the judgement-table entry above, which is
7944
+ where the same measurement is used for a different decision.
7945
+ **The guidance half has shipped; this entry is now only about the detector.**
7946
+ "Read the param inside the assertion" was insufficient and half-wrong as
7947
+ written, and `[0.5.0]` replaces it everywhere it appeared — README, the
7948
+ `init` skill, `possible-drift`, and design §11 — with what the evidence
7949
+ supports: **the expectation must not be a function of the param the code under
7950
+ test consumed**, pinned in a fixture, a literal, or a second independently
7951
+ derived value. Row 2 is the worked example, and the reporter's own
7952
+ `CHECK-2`/`FOG-3` fixes are the same move. It was held back once because
7953
+ rewording the central claim of the tool is not a line appended; that is what
7954
+ was then done, deliberately and in one pass.
7955
+ **What stays undecided is the mechanism**, and all three blockers above are
7956
+ untouched by the rewording: the frequency is still unmeasured from one
7957
+ greenfield repo, the reporter's proposed detector is still the wrong shape
7958
+ twice over, and nothing short of mutation testing separates an independent pin
7959
+ from an echo. What the guidance does not do is close the hole — a tautological
7960
+ assertion is still green, still silent, and still reachable — so this stays
7961
+ here rather than moving to `Considered and rejected`: documenting a trap is
7962
+ not the same as deciding not to detect it.
7963
+ **Re-measured after `[0.7.1]` widened `params`, on this registry rather than
7964
+ the reporter's — the first time this experiment has been run here.** Twelve
7965
+ mutations, each followed by the whole gate. Two results matter, and they point
7966
+ opposite ways.
7967
+ *The shape is now reachable here, and the widening is why.* `self/render.spec.ts`
7968
+ asserts `expect(md).toContain(JSON.stringify(kinds))` over the same
7969
+ `payloadKinds` it feeds to the renderer — the expectation is a function of the
7970
+ value under test, verbatim row 3, and it could not have been written before
7971
+ the widening because the param could not be an object. The mechanism is
7972
+ sharper than a frequency count and is the strongest thing this entry has
7973
+ gained: **the prescribed remedy costs in proportion to the value's
7974
+ complexity.** Same repo, adjacent files, same value shape — `ATX-7` pins two
7975
+ numbers with literals and goes red; `ATX-63` would have had to hand-write a
7976
+ rendered JSON block, so it echoed. Depth raises the defect rate and the cost
7977
+ of avoiding it along one axis.
7978
+ *And its cost here is nil, which is the honest other half.* Both composite
7979
+ params are fixtures for the renderer and the validator; the numbers stand for
7980
+ nothing, so the green is arguably correct. Blocker **(1)** is therefore only
7981
+ half retired — the shape is reproduced in a second, mature repo, and the count
7982
+ of *harmful* instances is still zero. Blocker **(2)** is reinforced rather than
7983
+ weakened: all four green mutations were on params `src/` never reads, so the
7984
+ reporter's "read from both a spec and a non-spec file" detector would have
7985
+ flagged none of them. Blocker **(3)** is untouched.
7986
+ *The frequency is no longer unmeasured, and it points away from building
7987
+ anything.* Blocker **(1)** rested on 8 commits of one greenfield repo. Parsed
7988
+ at every one of the **38** revisions that have touched this registry — all 38
7989
+ readable by the current static reader, so nothing was skipped — the history is
7990
+ **57 param keys added, 0 removed, and exactly 1 value changed**: `ATX-16.staticCommands`
7991
+ gaining `status`. That single edit is a roster *widening*, the safe direction,
7992
+ and it is on the one list that was already pinned, so it could not have been
7993
+ made without updating the pin. Two independent codebases, one greenfield and
7994
+ one at 64 requirements, and the event this whole entry is about has happened
7995
+ zero times in either. What that argues is not that the hole is unreal — it is
7996
+ reachable, demonstrated above — but that it is **rare and silent**, which is
7997
+ the profile `divergent-param` was rejected on rather than the profile that
7998
+ justifies an engine rule.
7999
+ *The same number says the split-out fix below has also never been triggered.*
8000
+ 0 removals ever means no domain has ever been shortened here. Those pins close
8001
+ a live *exposure*, not a live *loss*, and the case for them is that they cost
8002
+ one line each against a failure with no symptom — not that anything was
8003
+ currently broken.
8004
+ *Blocker **(3)** assumed mutation testing was the only separator. A cheaper
8005
+ candidate existed, has now been scored, and does not survive it.* The idea was
8006
+ a **syntactic** discriminator over one test body — flag an assertion whose
8007
+ expected value derives from a param that also reached the arrange/act part —
8008
+ which is exactly the difference between the two cases the widening produced:
8009
+ `renderMarkdown({params: {payloadKinds: kinds}})` … `toContain(JSON.stringify(kinds))`
8010
+ against `expect(kinds.json.maxBytes).toBe(1_048_576)`. Scored the way
8011
+ `divergent-param` should have been and was not: implemented in
8012
+ `tests/intent-rule-candidates.spec.ts`, run over the specs that attest this
8013
+ repo, and then **every param it fires on mutated with the whole gate run —
8014
+ fifteen of them, one gate run each. Four went green; eleven went red.**
8015
+ *Then the detector was found to be wrong, which is the more useful half.* Its
8016
+ first implementation counted an identifier in any position as a read of a
8017
+ local with that name, so `i.code` in `issues.map((i) => i.code)` looked like a
8018
+ use of a param named `code` — and `code` is this repo's commonest param name.
8019
+ Corrected to count reference positions only, it flags **8, not 15**, and the
8020
+ seven it drops are exactly seven the mutations had already shown to be pinned
8021
+ by `src/`. The honest score is therefore **4 of 8, not 4 of 15** — precision
8022
+ 50%, not 27%. The conclusion does not move, because it never rested on the
8023
+ precision figure; what moves is how much of the original number was the
8024
+ candidate failing and how much was the measurement being wrong, and that is
8025
+ worth writing down rather than quietly restating.
8026
+ The remaining false positives are not near-misses. They are independent terms
8027
+ the detector cannot see: **the runtime is the term** — `ATX-14`'s
8028
+ `inheritedKey` is `'toString'`, and a mutated spelling is not a prototype key
8029
+ at all, so the behaviour under test changes; **`src/` is the term** —
8030
+ `ATX-51`'s `code` is a second copy of a string the engine emits as a literal;
8031
+ and **arithmetic is the term** — `threshold + 3` builds a derived expectation
8032
+ that mutation breaks.
8033
+ And the four true positives fail the second test, which is the one that
8034
+ decides it: **none is harmful.** `ATX-10` and `ATX-3` assert that a value was
8035
+ substituted or applied, where the identity of the number is not the
8036
+ obligation; `ATX-51`'s is a path prefix and `ATX-63`'s a rendering fixture.
8037
+ Telling those from a mine-density band that must not move is a judgement about
8038
+ whether the value *means* anything — the ground truth §0 refuses to compute
8039
+ for someone else's registry. **50% precision on the shape, 0% on the harm.**
8040
+ Two limits on that number, stated because a score with an unstated method is
8041
+ what this file exists to prevent. The mutation has **one direction per kind**
8042
+ — numbers +1, lists drop-last, strings suffixed — and direction can decide the
8043
+ verdict: `ATX-40.languages` is red at 2→3 and was green at 2→1 before it was
8044
+ pinned, so the true-positive count is a floor. And **recall is unmeasured**:
8045
+ the eight params mutated earlier that this detector does not flag all went
8046
+ red, which is eight correct negatives and not a recall figure.
8047
+ The detector and its verdict table are kept in the measurement suite, with the
8048
+ reach recomputed on every run and the table asserted to cover exactly what
8049
+ fires — because the reach figure quoted for `compound-requirement` went stale
8050
+ in this very file for want of that.
8051
+ *A scoping limit on all of the above.* This repo cannot settle blocker **(2)**.
8052
+ `src/` reads no param at all — every `reqs[…].params` in it is a comment or a
8053
+ doc string — so params here are test vocabulary, while in the reporter's repo
8054
+ they are application constants the app itself consumes. The reporter's
8055
+ detector keys on exactly that difference, so it is inert here and this corpus
8056
+ is evidence neither for nor against it.
8057
+ *What the re-measurement did find is a different defect, and it has been split
8058
+ out below* — the entry is kept whole here because the two were measured in one
8059
+ pass and separating them is the finding.
8060
+
8061
+ ### Retiring a requirement
8062
+
8063
+ Filed 2026-08-03 on the cost of permanence, re-measured four times, and rejected
8064
+ 2026-08-31 **by its own reopening condition**. The entry nominated the test —
8065
+ reopen if the cost per requirement moves — and the ratio has read 2.56 → 2.78
8066
+ → 2.80 → 2.82 scenarios per requirement across twenty-five days and
8067
+ twenty-four requirements. Three consecutive readings within 0.05 of each other
8068
+ are not a curve about to move, and the condition has now failed to trigger
8069
+ twice. The cost argument is therefore not waiting for evidence; it was answered
8070
+ in the negative by the thing the entry itself chose to be judged on.
8071
+
8072
+ *The three meanings it ended by separating are why the rest closes rather than
8073
+ merely goes quiet.* **(1) A requirement that turned out wrong** was never a
8074
+ retirement — the record of being wrong belongs in this section, which is what
8075
+ this section is, and removing the id is a manual registry edit nobody has argued
8076
+ against. **(3) A requirement subsumed by a later one** is a rename, blocked
8077
+ downstream of `--apply`'s refusal of RENAMED (`splice.ts`: deleting an entry has
8078
+ no smaller span to fall back to). That is somebody else's blocker, and lifting it
8079
+ is a decision which is not this one. It was shared with the backlink-index entry
8080
+ until 2026-09-01, when that entry was rejected; this is now the only thing
8081
+ resting on the refusal. **(2) A requirement still true and no
8082
+ longer worth a permanently-run scenario** was the only one ever about cost, and
8083
+ it is the one the ratio answers.
8084
+
8085
+ *What (2) left behind was a defect rather than a mechanism, and it is fixed.*
8086
+ `uncovered-requirement` read "Add a scenario, or **mark it deferred**", and
8087
+ `deferred` appeared nowhere else in the project — not a field, not a flag, not
8088
+ a schema key — so the escape hatch this meaning needs had been named in shipped
8089
+ output, in the one place a reader looks when they hit the error, since before
8090
+ anybody asked the question. `[Unreleased]` rewords it, troubleshooting now says
8091
+ what the second exit actually is, and design §11 records the permanence as a
8092
+ property to design against rather than a gap awaiting a mechanism. **This entry
8093
+ is where the trace of the affordance having been intended now lives**, which is
8094
+ why the rewording waited for the rejection instead of going in as a stray fix:
8095
+ changing the message first would have deleted the only written evidence that
8096
+ anybody ever meant to build it.
8097
+
8098
+ *The permanence itself is not rejected and is not a defect.* Deleting an intent
8099
+ in order to save the scenario attesting it inverts the claim the whole tool
8100
+ exists to make. Reopening needs the case (2) names — a requirement whose intent
8101
+ is still true and whose scenario is genuinely not worth running — together with
8102
+ a mechanism that keeps the first while dropping the second. Nobody has proposed
8103
+ one, and the message that implied there already was one is the reason the
8104
+ question looked bigger than it is.
8105
+
8106
+ The measurements, kept as they were taken:
8107
+
8108
+ **Whether a requirement can ever be retired, and what it costs that none can.**
8109
+ The registry is at **85 requirements and 240 scenarios, 2.82 each**, and
8110
+ every one is a permanent obligation: `uncovered-requirement` is an ERROR, so a
8111
+ requirement without a scenario cannot exist, and nothing ever removes one.
8112
+ `delta.removed` exists and `applyDelta` applies it, but `--apply` refuses to
8113
+ write it back, deliberately and with the reason recorded — REMOVED "cannot say
8114
+ which comments belonged to the entry it deletes". So retiring one is a manual
8115
+ registry edit, which is fine as a mechanism and says nothing about whether it
8116
+ is ever the right move.
8117
+ *What the growth actually costs, measured on this session rather than
8118
+ estimated.* One small fix took: a requirement, its scenarios, a hand-judged
8119
+ row in the `OBLIGATIONS` table, two inline snapshots, a `[Unreleased]` entry,
8120
+ History rows in two languages, and a regenerated `SPEC.md`. That is
8121
+ proportionate for a spec framework that has to dogfood — the ceremony *is* the
8122
+ product being used on itself — and it is also a number that only goes up.
8123
+ *What blocks the decision, and it is not the mechanism.* It is that "retire"
8124
+ has at least three meanings this file has never had to separate: a requirement
8125
+ that turned out **wrong** (which should leave a record of being wrong, and
8126
+ `Considered and rejected` is where that already goes); one still **true** but
8127
+ no longer worth a permanently-run scenario; and one **subsumed** by a later
8128
+ requirement, where the scenario should move rather than go. Only the second is
8129
+ really about cost, and it is the one where deleting the intent to save the
8130
+ scenario inverts what the whole tool claims — that intent outlives its tests.
8131
+ Nothing here is urgent at 85. It is filed now because the moment to write the
8132
+ argument down is while the reasoning is available, and because a project whose
8133
+ requirement count only rises should have decided in advance what would make
8134
+ one leave.
8135
+ *The figures above were restated on 2026-08-08, and how they were wrong is
8136
+ itself the section's own warning happening.* As filed on 2026-08-03 this entry
8137
+ said "62 requirements and 160 scenarios, roughly 2.6 each" while closing on
8138
+ "the number is 61" — an inconsistency inside one entry, and the closing half
8139
+ was the right one. Re-measured at the entry's own commit with the current
8140
+ parser: **61 requirements, 156 scenarios**, so both opening figures were wrong
8141
+ on the day they were written, and only the ratio survived. Measured the same
8142
+ way today it is 69 and 192, and the method is the same one `verify self`
8143
+ prints — the control is that today's `self/` counted this way returns exactly
8144
+ what the runner does, which is what makes the 2026-08-03 number trustworthy. The
8145
+ cheap way — `grep -c 'scenario('` — answers 211 against today's true 192, and
8146
+ is what would have been reached for: comments and prose count, which is the
8147
+ `ATX-15` error again.
8148
+ **What moved in five days sharpens the argument rather than dating it.** +8
8149
+ requirements and +36 scenarios, concentrated in three sessions rather than
8150
+ accrued at a rate, so nothing here is a trend line. The part worth keeping is
8151
+ the ratio: 2.56 → 2.78. The cost of a requirement is not flat and it is not
8152
+ falling, which is the premise the whole entry rests on, and it is the first
8153
+ time this file has had two measurements of it to compare.
8154
+ *A third measurement, on 2026-08-18, and it points the other way from the
8155
+ second.* **79 requirements and 221 scenarios**, counted the way `verify self`
8156
+ prints them, which is the control the 2026-08-08 restatement established. The
8157
+ ratio runs 2.56 → 2.78 → **2.80**, so the step that carried the argument was
8158
+ the first one and the second is **+0.015**, which is flat. The premise this
8159
+ entry rests on now has one measurement for it and one against, and the honest
8160
+ statement is the narrower one: per-requirement cost has held at about 2.8
8161
+ scenarios across ten days and ten requirements. What is unchanged is the
8162
+ absolute total, which only rises — that half of the argument is untouched, and
8163
+ it was always the half about permanence rather than about acceleration. What is
8164
+ no longer supported is the acceleration. **Reopening on cost needs the ratio to
8165
+ move again**; reopening on the three meanings of "retire" needs none of these
8166
+ figures, and that is still the part nobody has separated.
8167
+ *A fourth measurement, on 2026-08-28, and the cost half is now closed rather
8168
+ than merely unsupported.* **85 requirements and 240 scenarios**, counted the
8169
+ way `verify self` prints them: the ratio runs 2.56 → 2.78 → 2.80 → **2.82**.
8170
+ Three consecutive readings within 0.05 of each other, across twenty-five days
8171
+ and twenty-four requirements, are not a curve that is about to move. The
8172
+ reopening condition this entry set itself has now failed to trigger twice, so
8173
+ the cost argument is not waiting for evidence — it has been answered, in the
8174
+ negative, by its own test. What survives untouched is the half that was never
8175
+ about the ratio: the absolute total only rises, and permanence is the claim.
8176
+
8177
+ **The three meanings, separated — which is what the entry has been asking for
8178
+ and needs none of the figures above.** They are not one question, and only one
8179
+ of the three is open.
8180
+
8181
+ **(1) A requirement that turned out wrong.** Not a retirement at all, and the
8182
+ mechanism already exists: the record of being wrong belongs in `Considered and
8183
+ rejected`, which is what that section is, and removing the id is a manual
8184
+ registry edit nobody has argued against. Nothing is missing here. Filing it
8185
+ under "retire" was what made the question look bigger than it is.
8186
+
8187
+ **(2) A requirement still true but no longer worth a permanently-run
8188
+ scenario.** The only one of the three that is about cost, and the only one with
8189
+ no mechanism — and the sharpest thing this separation turned up is that the
8190
+ tool has been *advertising* one. `uncovered-requirement` reads "Add a scenario,
8191
+ or **mark it deferred**", and `deferred` appears nowhere else in `src/`: not a
8192
+ field, not a flag, not a schema key. The troubleshooting section for that code
8193
+ quotes the message as its symptom and then explains only the first half,
8194
+ because there is no second half to explain. So the escape hatch this meaning
8195
+ needs has been named in shipped output, and in the one place a reader looks
8196
+ when they hit the error, since before anyone asked the question. That is a
8197
+ defect in its own right — the message is prose and free to reword — but it is
8198
+ recorded here rather than fixed there, because rewording it would delete the
8199
+ only written trace that this affordance was ever intended, which is the thing
8200
+ this entry exists to keep. *That held until the entry itself closed, and no
8201
+ longer applies:* the trace is the paragraph above, the rejection is what makes
8202
+ it durable, and `[Unreleased]` rewords the message. The condition on the
8203
+ rewording was never that it stay wrong — it was that somewhere else had to say
8204
+ the affordance had been meant first.
8205
+
8206
+ **(3) A requirement subsumed by a later one**, where the scenario should move
8207
+ rather than go. That is a rename, and `--apply` refuses RENAMED outright, with
8208
+ the reason recorded in `splice.ts` — deleting an entry has no smaller span to
8209
+ fall back to. So it is blocked downstream of a refusal nobody has proposed
8210
+ lifting, which is the *same* blocker the backlink-index entry above carried until
8211
+ that entry was rejected on 2026-09-01. Two open entries rested on that one
8212
+ refusal and now **one** does, which is worth knowing and cuts the other way from
8213
+ how it was first written: a decision with two dependents is easier to justify
8214
+ than a decision with one, so the case for ever lifting the refusal got weaker
8215
+ rather than the blocker getting closer to clearing. This entry is not the reason
8216
+ the refusal exists, and it is now the only thing waiting on it.
8217
+
8218
+ *So the entry narrows to one meaning.* (1) is answered, (3) is somebody else's
8219
+ blocker, and what is actually open is (2) — whether a requirement can stop
8220
+ owing a permanently-run scenario while keeping its intent, which is the one
8221
+ case where deleting the intent to save the scenario inverts what the whole tool
8222
+ claims. Nothing here says it should be built; what changes is that the question
8223
+ is now one question.
8224
+
8225
+ ### Declared prefix ownership instead of inferred
8226
+
8227
+ Filed 2026-08-19 from a spec-kit reading, rejected 2026-08-31 on the two things
8228
+ that moved under it. *What rejected it: the proposal is smaller than it looked,
8229
+ weaker than it looked, and answers a question nobody here has asked.*
8230
+ `loadRegistry` already returns `prefixOwners`, `merge.ts` already uses it to
8231
+ place an ADDED id, and `apply-no-prefix-owner` already refuses a prefix nobody
8232
+ owns — so what was proposed is declaration replacing inference, not a new
8233
+ layer. And `/inspect` shipped in `[0.9.2]`, so the cross-repository half — the
8234
+ one §11 says no command can see across — is now about ten lines of an
8235
+ adopter's own code over two registries. Attest does not have to own it to make
8236
+ it possible.
8237
+
8238
+ The third is what decides it. This repository's five prefixes — `ATX` and the
8239
+ four fixture ones — are each declared by exactly one file, so the allocation
8240
+ question a declaration would answer has never arisen here. Building it now is
8241
+ deciding prefix semantics for everyone from a corpus in which those semantics
8242
+ are unexercised, which is the objection this project raises against every
8243
+ premature interface.
8244
+
8245
+ §11 keeps the property in place unchanged: one flat global id space, the
8246
+ prefix a convention rather than a scope, and `duplicate-prefix` able only to
8247
+ report a collision that already happened. **The cost still arrives exactly when
8248
+ adoption succeeds** — two teams sharing a registry have somebody allocating
8249
+ prefixes by hand — and that is the reopening condition. It is also the
8250
+ condition the two-party audit entry under consideration would satisfy, so the
8251
+ first adopter who shares a registry decides both.
8252
+
8253
+ The reading this was filed from, kept as it was written:
8254
+
8255
+ **Prefix ownership is inferred rather than declared, and the cost arrives only
8256
+ when adoption succeeds.** Design §11 records that the id grammar has no level
8257
+ above the requirement: the prefix is a convention, nothing allocates it, and
8258
+ `duplicate-prefix` can only report a collision that already happened.
8259
+ spec-kit's answer is a roadmap table — a stable id, an intent, a scope
8260
+ boundary, dependencies — authored above the specs it decomposes into. That
8261
+ comparison is stated here rather than referred to, because the entry that
8262
+ introduced the spec-kit reading has since been built and now sits under
8263
+ `[0.9.4]` as the `open` field: an entry whose evidence rests on a
8264
+ neighbour is one a promotion silently empties.
8265
+ *Two things have changed since that reading, and they point opposite ways.*
8266
+ The proposal is **smaller** than it looked: the mechanism partly exists.
8267
+ `loadRegistry` returns `prefixOwners` — first claim in sorted file order —
8268
+ `merge.ts` uses it to decide which file an ADDED id belongs in, and
8269
+ `apply-no-prefix-owner` already refuses a prefix nobody owns. So what is
8270
+ proposed is declaration replacing inference, not a new layer. And it is
8271
+ **weaker**: `/inspect` shipped in `[0.9.2]`, so the cross-repository half —
8272
+ the one §11 says no command can see across — is now something an adopter
8273
+ computes from two registries in about ten lines. Attest does not have to own
8274
+ it to make it possible.
8275
+ *What blocks the decision:* nobody has the problem. §11 states the cost
8276
+ arrives "exactly when adoption succeeds: two teams sharing a registry have
8277
+ someone owning prefix allocation by hand", and no such adoption has been
8278
+ reported. Building the declared form now would be deciding prefix semantics
8279
+ for everyone from one repository whose five prefixes — `ATX` and the four
8280
+ fixture ones — are each declared by exactly one file, so the allocation
8281
+ question this would answer has never been asked here.
8282
+
7125
8283
  ### An id-keyed retrieval bundle over `archive/` and this file
7126
8284
 
7127
8285
  Proposed 2026-08-19 inside the entry now titled *The decisions worth retrieving
@@ -7237,6 +8395,17 @@ for the section most likely to be read before a proposal. Nothing was changed
7237
8395
  about that here: restructuring twelve open entries is a larger edit than the
7238
8396
  finding justifies, and the finding is only four events old.
7239
8397
 
8398
+ *The counts above are the 2026-08-28 measurement and have since moved; recounted
8399
+ 2026-08-31.* `Considered and rejected` holds **27 `###` entries**, not 24 —
8400
+ entries were decided into it in the meantime, and `init --check` left it,
8401
+ reopened — and `Under consideration` holds **six** bullets, not twelve. The
8402
+ heading ratio is deliberately not recounted: judging a heading artifact-shaped is
8403
+ a hand call, so re-running it would be a new measurement rather than a repair of
8404
+ this one, and the entry rests on the argument rather than on the ratio. What the
8405
+ recount does settle is the aside above about the section next door — at six
8406
+ bullets the case for giving it headings is weaker than it was at twelve, not
8407
+ stronger.
8408
+
7240
8409
  ### A per-scenario axis recording which interface a scenario crosses
7241
8410
 
7242
8411
  Prompted by an external write-up (`columns.chicken-house.net`, 2025-11-10, "AI-First
@@ -7907,27 +9076,6 @@ is the "cleared by deleting the word that triggered it" failure arriving without
7907
9076
  anyone trying to clear it, which strengthens the rejection rather than
7908
9077
  qualifying it.
7909
9078
 
7910
- ### A freshness gate on the file `attest init` writes (`init --check`)
7911
-
7912
- `render --check` is the obvious precedent and is the opposite case. A rendering
7913
- is a function of the *user's registry*, so its staleness means the user moved
7914
- intent without regenerating — a real signal, with the fix in their hands. The
7915
- skill is a function of the *installed version*, so a gate on it turns red on a
7916
- change the user did not make, and is cleared by a commit containing no decision.
7917
- That is a chore wearing a diagnostic's clothes, and a gate that cannot be about
7918
- anything the reader did is worse than no gate — the same standard 0.1.6 set when
7919
- it gave `render` no formatting options at all. An earlier draft of `attest init`
7920
- spent a second command, this gate and a second self-requirement on it. What that
7921
- would have bought is immunity to one honest and small exposure: an old file
7922
- describes an old workflow to an agent that trusts it. The backstop is that every
7923
- mistake it can cause is already a diagnostic with a fix hint — a registry
7924
- written the old way is `registry-not-static` — so the agent corrects itself from
7925
- the report, and `init` is idempotent, which makes re-running it the entire
7926
- update mechanism. Recorded here rather than left in the 0.3.0 entry that shipped
7927
- `init`, because the queue above depends on it: a gate is the first thing
7928
- proposed each time a new agent target is considered, and the answer must not
7929
- need re-deciding per target.
7930
-
7931
9079
  ### A marked block in the adopter's `AGENTS.md` / `CLAUDE.md`
7932
9080
 
7933
9081
  The cross-tool convention Codex, Cursor and Copilot read, and the reason it
@@ -8057,9 +9205,13 @@ was written expecting the uncovered case and the demonstrated one has a covering
8057
9205
  scenario. **What that changes here is nothing**: this entry was rejected on the
8058
9206
  population a snapshot fires on — every legitimate param edit, fix a commit with
8059
9207
  no decision in it — and that objection is untouched by the new evidence, which
8060
- concerns detection the snapshot was never denied having. The live question moved
8061
- to `Under consideration` above, where it belongs, because the mechanism now in
8062
- question is a rule about assertions rather than a file beside the registry.
9208
+ concerns detection the snapshot was never denied having. The live question
9209
+ moved to `Under consideration`, because the mechanism then in question was a
9210
+ rule about assertions rather than a file beside the registry, and it has since
9211
+ been rejected in its own right — above in this section, on a frequency of about
9212
+ zero across two codebases and a detector scoring 0% on harm. So the hole both
9213
+ entries describe is now recorded as open with nothing watching it, which is a
9214
+ weaker position than either entry claimed on its own and is the honest one.
8063
9215
 
8064
9216
  ### Grouping `render`'s output by prefix
8065
9217
 
@@ -8251,8 +9403,9 @@ fallback to evaluation. The same answer applies here for the same reason.
8251
9403
 
8252
9404
  Worth reopening if an adopter's registry reaches a size where prefix grouping
8253
9405
  and `grep` genuinely stop working — but the first thing to try then is the
8254
- declared prefix ownership already under consideration above, which addresses,
8255
- rather than a scorer, which ranks.
9406
+ declared prefix ownership recorded above in this section — rejected on there
9407
+ being no adoption that needs it, which is the same condition this sentence
9408
+ supposes — which addresses, rather than a scorer, which ranks.
8256
9409
 
8257
9410
  ## How an entry is re-measured
8258
9411
 
@@ -8331,6 +9484,8 @@ path the repair makes reachable; it is about **everything the repair would make
8331
9484
 
8332
9485
  ## Releases
8333
9486
 
9487
+ [1.1.0]: https://gitlab.com/Pseudorca/attest/-/tags/v1.1.0
9488
+ [1.0.0]: https://gitlab.com/Pseudorca/attest/-/tags/v1.0.0
8334
9489
  [0.11.0]: https://gitlab.com/Pseudorca/attest/-/tags/v0.11.0
8335
9490
  [0.10.0]: https://gitlab.com/Pseudorca/attest/-/tags/v0.10.0
8336
9491
  [0.9.4]: https://gitlab.com/Pseudorca/attest/-/tags/v0.9.4