@am_shork/attest 0.9.4 → 0.11.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
@@ -13,6 +13,900 @@ 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
14
  not API.
15
15
 
16
+ **A narrowing that closes a security hole is breaking by that definition, and
17
+ takes the bump that definition gives it — including a major.** There is no
18
+ carve-out for it and no flag. The two alternatives were priced and both cost
19
+ more than the version number: an exemption needs a test for what counts as a
20
+ security narrowing, and nothing here can hold a prose rule to one, so the label
21
+ would be available to every inconvenient narrowing and judged by the person
22
+ making it. Shipping the new refusal behind an opt-in that defaults off is worse
23
+ for the case it is meant to serve — the four commands documented as safe on an
24
+ untrusted fork MR are the ones a reviewer runs on a file they have not read, and
25
+ a default that keeps answering wrong unless the reviewer knows to pass a flag
26
+ has not been fixed.
27
+
28
+ *What that costs is stated rather than hidden.* Under 0.x it costs nothing: the
29
+ same fix is a minor either way, which is why this could go undecided for so
30
+ long. Under 1.0 it is a major on whatever schedule a hole is found — the
31
+ `callsAuthoringFn` binding fix, the fifth breaking commit in this project's
32
+ history, would have been a 2.0 twenty-five days after a 1.0 cut at the fourth.
33
+ The pressure that creates lands on the version number, and the failure it
34
+ invites is the other one: a fix weakened until it fits a bump somebody
35
+ preferred. A narrowing that ships smaller than the defect it closes is what this
36
+ paragraph exists to make visible.
37
+
38
+ ## [0.11.0] - 2026-08-28
39
+
40
+ ### Added
41
+
42
+ - **`examples/judgement-table/` — the enforcement half of the intent-quality
43
+ method, as one runnable file with no columns.** `intent-quality.md` has told
44
+ an adopter to build a judgement table since `[0.9.2]` shipped `/inspect`, and
45
+ the path to producing one was five voluntary steps with a scaffold at none of
46
+ them: a row in a document table, a 214-line page, a hand pass over four
47
+ questions, then writing the two *mechanical* properties from a prose
48
+ description. The last of those is the step that did not need to be voluntary.
49
+ Completeness and rot-binding do not depend on what the adopter decided to
50
+ measure — their content is fixed — so they are the half where a copy is not an
51
+ echo, and the four judgement columns stay withheld for the reason `[0.9.2]`
52
+ gave: a second table built by copying ours is evidence about nothing.
53
+
54
+ **The columns are absent by construction rather than left blank.**
55
+ `Judgement` ships one optional `pin` — the anchor rot-binding needs, holding a
56
+ scenario name rather than a judgement about one — and adding a required field
57
+ to it makes **every existing row fail to compile**, measured rather than
58
+ claimed: one added field, `TS2741` on both rows. So the type does at author
59
+ time what the completeness assertion does at run time, and a row cannot be
60
+ added without a decision.
61
+
62
+ **It runs in the default suite, and that is the point rather than a detail.**
63
+ `pre-commit-check` §6 already records that the three usage samples on
64
+ `intent-quality.md` are consumer usage of the public API that no gate reaches;
65
+ a fourth copy of them in a directory that *looks* executable would be worse
66
+ than a fenced block, not better. So `vitest.config.ts` includes
67
+ `examples/**/*.spec.ts`, the specifier `@am_shork/attest/inspect` is aliased to
68
+ source there and in `tsconfig.typecheck.json`, and both assertions were shown
69
+ to bite before this was called done — a third requirement added to the sample
70
+ fails completeness with `[ 'EX-3' ]`, and renaming the pinned scenario fails
71
+ rot-binding by name. What the alias deliberately does not check is that the
72
+ specifier resolves in an installed package; `tests/consumer.spec.ts` owns that
73
+ against a real `npm install`, and the two together are the whole claim.
74
+
75
+ A third assertion is in the skeleton that is in neither the page nor this
76
+ entry's proposal: `expect(issues).toEqual([])`, **before** the other two. A
77
+ registry file that will not read statically contributes nothing to `registry`,
78
+ so an unread file is indistinguishable from a project with fewer requirements
79
+ and both completeness directions pass over it. It was not written from
80
+ reasoning — an invalid mutation during the bite test produced exactly that
81
+ state, `registry: {}` with one `registry-invalid` issue, and the guard is what
82
+ caught it.
83
+
84
+ *What it cost elsewhere, because it is the part that does not announce
85
+ itself.* The sample project is a registry, and `tests/intent-rule-candidates.spec.ts`
86
+ walks every `*.reqs.ts` in the tree — so the corpus moved **95 → 97** and the
87
+ `compound-requirement` snapshot was re-read rather than re-baselined:
88
+ `compound`, `correct` and `missed` are all unchanged, so no rate quoted
89
+ anywhere moved and only the denominator did. Those two ids are deliberately
90
+ dull, which is a biased sample, and it is admitted in the snapshot's comment
91
+ rather than held out — one example directory is one adapter, and a hold-out is
92
+ a seam this repository does not buy until something varies across it.
93
+
94
+ - **This repository now carries the workflow document it ships (`ATX-84`).**
95
+ `attest init` has written `.claude/skills/attest/SKILL.md` since 0.6.0, and this
96
+ repository had never run it: `.claude/skills/` held three hand-written skills and
97
+ no `attest/`, so the workflow half was reachable here only by opening
98
+ `src/core/skill.ts` — the exact gap `ATX-27` exists to close, open in the project
99
+ that ships the fix. Nothing could report it, because an absent file is
100
+ indistinguishable from a repository that has not adopted Attest. `.gitattributes`
101
+ has named the file among the generated files it pins since `fcd6140`, a sentence
102
+ false on the day it was written.
103
+
104
+ The requirement holds the committed file byte-identical to `skillContent()`, so
105
+ editing the workflow text turns `attest verify self` red until `attest init` is
106
+ re-run. That is deliberately *not* the freshness gate `src/core/skill.ts` argues
107
+ against shipping: that argument is about adopters, whose copy goes stale only at
108
+ an upgrade and whose every resulting mistake is a diagnostic carrying a fix hint.
109
+ This is the one place where `skillContent()` changes while the file exists, so
110
+ staleness is an ordinary edit rather than an upgrade event, and the gate costs one
111
+ string comparison. Byte equality rather than `ATX-29`'s document comparison for
112
+ the same reason the distinction runs the other way for `render --check`: that gate
113
+ runs in repositories where Attest does not own `.gitattributes`, this one only
114
+ here, where `eol=lf` is committed.
115
+
116
+ No engine behaviour changes. The document is unchanged, `init` is unchanged, and
117
+ nothing an adopter runs is affected.
118
+
119
+ ### Changed
120
+
121
+ - **`delta()` no longer offers the requirements it adds under `added`, because
122
+ that was the one spelling stage 1 could not tell from the right one
123
+ (`ATX-85`).** `delta()` returns the delta *and* the registry of what it
124
+ proposes, so `d.added['AUTH-7'].params.x` and `d['AUTH-7'].params.x` are both
125
+ correct while a change is being authored — and only the second survives
126
+ `--apply`, which repoints the spec's import at the merged registry, and a
127
+ registry has no `added`. Nothing in the pipeline could separate them: the
128
+ archive gate runs *before* the rename, so the spec it graded is not the spec
129
+ the merge commits; `check` executes nothing, so it never evaluates the
130
+ accessor; and the compiler is satisfied on either side because it only ever
131
+ sees whichever half is on disk. The failure arrived one command later as a
132
+ `spec-load-failed` naming the file and not the cause.
133
+
134
+ **The guidance already existed and did not prevent it, which is why this is a
135
+ type and not a warning.** `registry.ts` documents the intended expression and
136
+ the rejected one on the function itself, and the document `init` writes shows
137
+ the by-id form in its worked example; the reach-through was written anyway, by
138
+ the author of the workflow tooling, on the first change this repository put
139
+ through its own flow. The alternative priced against it was a `check`
140
+ WARNING — which would have had to be a warning, since enumerating what a
141
+ change adds is legitimate, and a warning is exactly the strength that already
142
+ failed. `DefinedDelta` withholds the key instead: no diagnostic, no roster
143
+ entry, no runtime cost.
144
+
145
+ **`added?: never` rather than a bare `Omit`, and the difference is nine call
146
+ sites.** `RegistryDelta`'s four fields are all optional, so an added-only delta
147
+ with `added` removed shares *no* property with it and TypeScript's weak-type
148
+ check refuses `applyDelta(base, delta({ added: … }))` outright — measured at 11
149
+ errors, of which 9 are that shape and every one correct. An optional `never`
150
+ restores the overlap and still refuses the read, past both escape hatches:
151
+ `d.added!` and `d.added?.[…]` bottom out in `never`, and only an explicit
152
+ `as any` compiles. That last property is the whole reason this beats the
153
+ WARNING, so it is pinned in `tests/delta-types.spec-d.ts` rather than left to
154
+ hold by luck — a type has no runtime footprint, so a regression here would
155
+ otherwise ship green.
156
+
157
+ **Breaking, and it takes the bump this file's Versioning section gives it.** A
158
+ stage-1 spec reading through `added` stops compiling. The type also now
159
+ deliberately understates its own value — `withProposedRequirements` still
160
+ spreads `added`, and `applyDelta` and the loader still read it through
161
+ `RegistryDelta` — so what is withheld is the authoring surface and not the
162
+ data; `self/registry.spec.ts` keeps asserting exactly that, through a cast that
163
+ is itself the assertion.
164
+
165
+ *Re-measured before implementing, per `How an entry is re-measured`, and the
166
+ filed figures reproduced exactly* — 11 errors in the same two kinds, 11 → 2
167
+ under the variant, `typecheck` clean throughout, all three escape-hatch results
168
+ as recorded. **What the re-measure added was a surface the entry had not
169
+ counted, and it falsified this requirement's first draft before it shipped.**
170
+ `modified`, `removed` and `renamed` vanish at the merge too, and reading any of
171
+ them compiles on both sides — so the statement first drafted for `ATX-85`, that
172
+ withholding `added` leaves the by-id read "the only expression that resolves",
173
+ was false. They are left readable on purpose: none has a by-id equivalent, so
174
+ there is no correct spelling to redirect an author to, and a spec reading one
175
+ is reading delta-only data rather than misspelling a right thing. The narrowed
176
+ statement claims only what the refusal actually buys.
177
+
178
+ *And the exposure claim was one notch too wide.* The entry recorded the window
179
+ between `--apply` and the next `verify` as the whole exposure. In this
180
+ repository it is not: post-merge, reading through any of the four fields fails
181
+ to compile under `noUncheckedIndexedAccess`, so `typecheck:all` bounds it. The
182
+ unbounded form is an adopter's, whose spec files need be in no `tsc` project at
183
+ all — which is the case the refusal is for.
184
+
185
+ *Two things this change made the existing counter-pressure say.* The judgement
186
+ tables refused the new id until it carried a hand-judged row, which is that
187
+ mechanism working on its own author rather than on a hypothetical adopter — and
188
+ the corpus moved 97 → 98 with no rate disturbed. And `divergent-param` churned
189
+ its `field` key a **fourth** time — `ATX-85`'s `added` joining ATX-20, ATX-82
190
+ and ATX-83 — changing no verdict again, which is one more measurement for the
191
+ entry recording that this rule keys on the evidence it discards.
192
+
193
+ ### Security
194
+
195
+ - **The static reader matched the authoring call by spelling, so a file could
196
+ shadow `defineRequirements` and hand the two readers different registries.**
197
+ `callsAuthoringFn` accepted any identifier named `defineRequirements` — the
198
+ bare name was added unconditionally as a fallback for a global or a harness —
199
+ and any property access whose *last* segment was that name. So a
200
+ `*.reqs.ts` opening with `const defineRequirements = (r) => ({ ...r,
201
+ 'ADMIN-1': … })`, or with a local object carrying a method of that name, was
202
+ read by parsing it as the literal argument in front of the call, while the
203
+ evaluator answered with whatever that function returned.
204
+
205
+ Not a refusal but a **wrong answer**, and pointed at under-reporting: `check`
206
+ called the registry covered, `cover` counted rows that were not the rows, and
207
+ `render` wrote a document with an id missing from it. The reach is what makes
208
+ it worth a section — `check`, `cover`, `render` and `status` are the commands
209
+ documented as safe on an untrusted fork MR *because they execute nothing*
210
+ (`ATX-16`), so they are exactly the ones a reviewer runs on a file they have
211
+ not read, and the registry they report is the one nothing else will mention.
212
+ Two readers of one file disagreeing is the condition the differential suite
213
+ exists to prevent (design §5.2), and it could not have caught this: every
214
+ registry it runs over calls the real function.
215
+
216
+ The rule is now about the **binding** rather than the spelling. An import
217
+ binding is trusted, a name the file never binds is trusted — that is what the
218
+ bare-name fallback has always been for — and a `const` / `let` / `var` /
219
+ `function` / `class` in the file itself is not, whether the callee is that
220
+ name or an object property reached through it. A file that contradicts itself
221
+ becomes `registry-not-static`, with `--eval` named as the way back, which is
222
+ `ATX-17` unchanged rather than a new promise.
223
+
224
+ **What this still does not see is the module graph, and it is not written
225
+ to.** `import { defineRequirements } from './wrapper.js'` binds the name by an
226
+ import and is accepted; what that module exports is not a question the source
227
+ text of *this* file answers, and resolving it would mean reading and then
228
+ trusting another file, which is the evaluator's job. The line drawn is what
229
+ the file contradicts about itself.
230
+
231
+ Rejects input that was previously accepted, so it is breaking under this
232
+ project's 0.x rule — though the shapes it rejects are ones no working registry
233
+ has: the four accepted spellings (the import, an alias, a namespace or default
234
+ import's property, and the bare name) all still read, and each is pinned.
235
+
236
+ - **`repointImport` wrote back at offsets taken from a source it never checked
237
+ compiles.** `ATX-69` states that a source that does not compile must be
238
+ refused rather than read out of whatever the parser recovered, and names the
239
+ three readers that open with `createSourceFile`. This was the fourth site and
240
+ the only one that *writes*: `--apply` repoints a proposed spec's import of the
241
+ delta at the registry, and it opened the compiler itself, so it asked nobody.
242
+ `ts.createSourceFile` recovers — a half-saved spec still yields an import
243
+ declaration and a set of offsets — and those offsets were then written over
244
+ the bytes of a `*.spec.ts` the user cannot regenerate.
245
+
246
+ Unreachable through the command today, because the gate refuses a spec it
247
+ could not parse before a merge starts, and fixed for that reason rather than
248
+ in spite of it: a defence that holds only because something upstream holds is
249
+ not a defence, which is the standard `keySource` in the same file is written
250
+ to. A source that does not compile now comes back unchanged, so `applyMerge`
251
+ — which writes only when the text moved — renames the spec with the import it
252
+ already had, and a re-run finds and reports that state instead of a file
253
+ rewritten at offsets nobody can trust.
254
+
255
+ ### Documentation
256
+
257
+ - **`## Versioning` now says what a security narrowing costs a consumer, which
258
+ was undefined and which 0.x was the only reason nobody had to ask.** The
259
+ definition at the head of this file counts *rejecting previously-valid input*
260
+ as breaking and says nothing about security, so under 0.x the question was
261
+ invisible: the same fix is a minor either way. It stopped being hypothetical in
262
+ the same week it was measured — the `callsAuthoringFn` binding fix below is
263
+ breaking precisely *because* it narrows what a static reader accepts, and it
264
+ closes a wrong-answer hole in the four commands documented as safe to run on an
265
+ untrusted fork MR.
266
+
267
+ **Decided: such a fix takes the bump the existing definition gives it,
268
+ including a major.** The three candidate answers are not equivalent and only
269
+ one needed no new machinery. A carve-out naming security narrowings as exempt
270
+ requires a test for what counts as one, and this repository has already
271
+ rejected that shape under another name — *Countable obligations*, killed on the
272
+ argument that **nothing can force the count to be true**; a label nothing can
273
+ hold is available to every inconvenient narrowing and judged by the person
274
+ making it. Reaching the same end additively — the new refusal behind a flag,
275
+ defaulting off until the next major — is the only answer that costs the
276
+ consumer nothing, and it is disqualified by the case it would have to serve
277
+ first: a default that keeps answering wrong unless a reviewer knows to pass a
278
+ flag has not fixed a hole in the commands a reviewer runs on a file they have
279
+ not read.
280
+
281
+ *What it is not.* Not a vulnerability-disclosure policy and not a security
282
+ section for the README — one paragraph in `## Versioning`, in the vocabulary
283
+ that section already uses. And it was needed either way: if 1.0 is never cut
284
+ the paragraph documents a real property of the tool, and if it is, the
285
+ paragraph is a precondition, because a 1.0 without it promises something whose
286
+ price nobody has computed. That is why this could be settled while the 1.0
287
+ entry below stays open — it is decidable from this repository alone, which is
288
+ the property that entry is missing.
289
+
290
+ - **`intent-quality.md` tells people to build on `/inspect` and never said
291
+ whether the shapes move.** `inspect.ts` exports five of them and the page
292
+ carries three samples destructuring them; `SCHEMA_VERSION` governs the `--json`
293
+ envelope and governs none of these. **The policy was not missing — its
294
+ statement was.** The Versioning section already binds them: removing or
295
+ renaming a public API is breaking, and under 0.x that is the minor bump. The
296
+ gap is that the reader this export was written for arrives through a method
297
+ page reached from the README's document table, and nothing on that path said
298
+ so.
299
+
300
+ **Which of two sentences to write was the whole decision, and it turned on
301
+ 1.0.** Restating the existing promise reads as a stability guarantee and is the
302
+ honest summary of current policy; saying the shape may move before 1.0 is the
303
+ more useful warning and discourages exactly the adopter whose judgement table
304
+ would settle the schema entry below. With 1.0 not being cut for this release,
305
+ the second is the true one, and the cost is accepted rather than hidden: the
306
+ page now warns off the reader it most needs. It is written as a promise about
307
+ being *told* rather than about nothing changing, which is what the versioning
308
+ policy actually offers.
309
+
310
+ - **`tests-red` told you to read the failures out of a report that does not carry
311
+ them.** The troubleshooting page said "`attest verify` gives you the same run
312
+ with the individual failures". It does not, and never did: `verify` and
313
+ `archive` both run the child suite with `quiet: true` because the CLI renders
314
+ its own report, so the whole output is one `tests-red` — no file, no scenario
315
+ name, no assertion. A reader following that sentence runs the command it names
316
+ and learns nothing new. Both languages now say to run the spec directly with
317
+ your own test command, and say why that is safe (a direct run writes nothing,
318
+ so it cannot touch `first-run.json`) and why it is not a verdict.
319
+
320
+ Found by dogfooding `ATX-84`: making the installed workflow document stale on
321
+ purpose, to check that the scenario's failure carries a fix hint, produced a
322
+ `verify self` output with no failure in it at all.
323
+
324
+ ## [0.10.0] - 2026-08-22
325
+
326
+ ### Changed
327
+
328
+ - **`never-red` no longer claims to be a rule about writing the test first.**
329
+ Archived as `evidence-not-ordering`. The obligation is unchanged in every
330
+ observable way — same code, same scoping to the ids a change ADDs, same
331
+ blocking condition — and what moved is the registry's account of it, plus
332
+ every document that had turned "red before you implement" into the rule.
333
+
334
+ **The finding is that the ordering was never in the engine.** `keepOutcome` is
335
+ monotonic toward failure in one direction only: a recorded fail is permanent, a
336
+ recorded **pass is provisional**. Its motive was to let an author fix a
337
+ scenario that passed on its first run rather than hand-delete the evidence, but
338
+ the consequence is larger than the motive — a failure observed *after* the
339
+ implementation exists replaces the pass and satisfies `neverRedIssues`
340
+ identically to one observed before it. `self/red-record.spec.ts` has asserted
341
+ that at the record level since the mechanism shipped. So the gate has always
342
+ taken the evidence without caring when it was obtained, and three pieces of
343
+ prose said otherwise: `ATX-30`'s statement stopped at "its first run",
344
+ `ATX-31`'s rationale named running the gate first as *the* remedy, and the
345
+ workflow document said "Run this before you implement anything", which is
346
+ false as written.
347
+
348
+ **Removing it closes nothing, which is the argument for removing it.** A
349
+ stage-1 red can come from a typo in the spec, a missing import, or a fixture
350
+ that is not there yet — none of which is the requirement being unimplemented.
351
+ A failure observed before the implementation was therefore never *better*
352
+ evidence than one observed after it, only earlier. What the gate can actually
353
+ demand is that the scenario was seen to fail with its subject absent, and both
354
+ routes to that produce the same fact by the same command: run the gate before
355
+ writing the implementation, or remove the implementation, run the gate, and
356
+ restore it. The second route is now documented, with the `git stash` shape
357
+ spelled out in `troubleshooting.md`.
358
+
359
+ **Why this is worth the churn rather than a footnote.** The rule as written
360
+ declared malformed the one workflow that is becoming ordinary — intent,
361
+ scenario and implementation authored in one pass — and told those authors to
362
+ redo finished work in a different order to satisfy a gate that would have
363
+ accepted their evidence anyway. Separating the discipline (*an assertion has
364
+ to be able to fail*) from the habit (*write it first*) is what keeps the
365
+ strong half enforceable and stops the weak half from being enforced by
366
+ documentation the engine does not back.
367
+
368
+ `ATX-30` gains the clause the suite already checked — a recorded pass is
369
+ replaced by any later failure — and stops calling the record a snapshot of the
370
+ first run. `ATX-31` keeps its statement, which was always order-free, and
371
+ loses the ordering from its rationale. `ATX-48`, `ATX-53` and `ATX-56` carry a
372
+ one-phrase rationale patch each, because all three named it "the
373
+ red-before-implement gate" and a registry that contradicts itself is the drift
374
+ this tool exists to report. **`first-run.json` keeps its name**: archived
375
+ records already carry it, `inspectRedEvidence` reads them back, and spending
376
+ that to correct a word is the wrong trade — the sentence is where the meaning
377
+ lives.
378
+
379
+ One side effect worth reading: the reworded `ATX-30` statement moved from
380
+ `correct` to `missed` in the compound-requirement measurement, on both
381
+ variants, and the snapshot in `tests/intent-rule-candidates.spec.ts` carries
382
+ it. The requirement did not get less compound — the second clause became a
383
+ participle instead of a second `MUST` — so this is new evidence about *why*
384
+ that rule sees almost nothing, filed with the rejection it belongs to.
385
+
386
+ Two diagnostic messages are reworded and no `code` moves, so `schemaVersion`
387
+ does not move either; `Issue.message` is prose, and this is exactly the kind of
388
+ rewording that contract exists to allow. One assertion in `tests/skill.spec.ts`
389
+ moved from the phrase `must be red` to the code `never-red` — a test pinned to
390
+ a sentence the document is free to reword, which is the same mistake one level
391
+ down.
392
+
393
+ - **The same mutation pass over the verdict and its evidence: 24 mutations of
394
+ `gate.ts` and `red-record.ts`, 21 dead, and two of the three survivors closed.**
395
+ Every step of `evaluateGate` dies when removed, including scoping
396
+ `open-unresolved` back to the added ids — the cut that separates it from
397
+ `never-red`. So do both directions of `keepOutcome`: last-write-wins, where the
398
+ green run at the end erases the red it exists to remember, and first-write-wins,
399
+ where a recorded pass can never be corrected. The 46 lines at the top of
400
+ `red-record.ts` arguing for that asymmetry turn out to be held from both sides.
401
+
402
+ Two gaps were real, and both are cases where an existing test passes with the
403
+ guard removed:
404
+
405
+ - **A record whose version is not 2 was discarded by the *schema*, not by the
406
+ version check.** The case that covers this uses a version-1 record keyed
407
+ `reqId -> name`, which fails the shape as well — so the verdict is identical
408
+ either way, exactly as the guard's own comment predicts. Isolating the
409
+ version needs a record the schema would take: a valid version-2 shape under
410
+ version 3. Without that, "discarded because the format moved" and "discarded
411
+ because the file is corrupt" were the same observation, which the comment
412
+ calls out as how a format change gets made without anyone deciding to.
413
+ - **Nothing noticed if the record invented a `pass` for a scenario that never
414
+ executed.** `ATX-30` says the file records *how each scenario ended on its
415
+ first run*; a skipped scenario has no ending, so a `pass` there is an
416
+ observation no run produced — in a file that is evidence a reviewer reads and
417
+ that cannot be recomputed once the change is green. The gate still blocks
418
+ either way, so no verdict moves; what moves is the advice, since `never-red`
419
+ picks between "rewrite the assertion" and "run the gate before implementing"
420
+ on exactly this distinction.
421
+
422
+ The third survivor is left alone: `open-unresolved` emitting one error per
423
+ requirement rather than per question breaks nothing anybody promised. `ATX-81`
424
+ says the static commands report **each one**, and is pinned per question;
425
+ `ATX-82` says the gate **blocks**, and says nothing about granularity. Two paths
426
+ of one shape, one held and one not, and the difference is the two sentences.
427
+
428
+ *The harness failed twice more, and the second failure is the one worth
429
+ keeping.* Mutating a **writer** rather than a reader damages files outside the
430
+ source it was applied to: `archive` writes `first-run.json` into the e2e
431
+ fixtures, so a mutant that flipped `keepOutcome` left a committed fixture
432
+ saying `pass` where it had said `fail`, and every mutant after it read as
433
+ killed for that reason rather than its own. Restoring tracked fixtures fixed
434
+ the leak that had been found; a blanket check that the working tree is clean
435
+ after every mutant then immediately found the one that had not been — an
436
+ *untracked* record a mutant created, which `git checkout` cannot remove. The
437
+ broad guard earned its place over the narrow fix by catching what the narrow
438
+ fix was not written for.
439
+
440
+ - **Two guards in the registry reader had nothing holding them, found by
441
+ mutating every decision in `static-registry.ts` and watching what stayed
442
+ green.** Twenty mutations of the module's semantically load-bearing branches;
443
+ fourteen died to `tests/static-registry.spec.ts` and `tests/splice.spec.ts`,
444
+ and six survived the whole suite *and* `self/`. Two of the six were worth
445
+ closing, and both make the two readers answer **differently** rather than
446
+ merely reporting differently:
447
+
448
+ - `let reqs = defineRequirements({…}); reqs = …; export default reqs` — with
449
+ the `const` check removed the static reader answers the first registry while
450
+ the evaluator answers the second. A wrong answer rather than a refusal, which
451
+ is worse than any code it could have reported.
452
+ - `export = defineRequirements({…})` — with the `isExportEquals` check removed
453
+ the static reader accepts it, while the evaluator cannot run it at all.
454
+
455
+ Both now have a case in *"a registry that is not a literal"*, and both mutants
456
+ die to it. The other four survivors are left alone deliberately: one guard is
457
+ defence-in-depth behind a refusal that already happened upstream (`__proto__`
458
+ as an entry id, which `readRegistrySource` rejects before a merge can reach the
459
+ layout), one is a deliberate looseness whose comment says why, and two forbid
460
+ shapes that cannot compile in the first place. A test contrived to reach them
461
+ would assert that something upstream is still doing its job.
462
+
463
+ *The harness had to be fixed before any of it could be believed, and that is
464
+ the part worth carrying forward.* Its first run reported all six survivors as
465
+ killed. The cause was the mutation runner inheriting a working directory with
466
+ no test files in it, so `vitest` exited non-zero with "No test files found" and
467
+ every mutant read as covered — a green instrument reporting on nothing, which
468
+ is the exact failure `ATX-15`'s sweep guards against in as many words: *"a walk
469
+ that found no files satisfies 'no site orders by locale' while proving the
470
+ opposite of what it claims."* The runner now pins its working directory and
471
+ refuses to report at all unless the unmutated tree passes first.
472
+
473
+ - **The differential corpus now drives the two registry *writers*, not only the
474
+ two readers.** `tests/static-registry.spec.ts` exists because reading a
475
+ registry from the AST instead of evaluating it needs the two readers held
476
+ against each other — but the literal they walk has a third and a fourth
477
+ consumer, `registryInsertionPoint` and `registryEntryLayouts`, and the table
478
+ was never pointed at them.
479
+
480
+ *Every registry-source defect in this release had its trigger already sitting
481
+ in that table.* `as-const`, `satisfies` and `parenthesised` are the three
482
+ spellings whose literal the writers could not locate; `key-forms` carries the
483
+ duplicate key whose span they located at the wrong offset, under a comment
484
+ that has said *"a duplicate key: last wins"* since the row was written. The
485
+ corpus knew about all four inputs. Nothing asked the writers about them.
486
+ Reverting either fix now turns the table red — measured: the unwrap on all
487
+ three wrapper rows, the layout guard on `key-forms`.
488
+
489
+ **What it asserts is not that every case is writable.** A source the readers
490
+ accept may be one the writers must refuse, and `key-forms` now is. The
491
+ property is that a writer's answer is always *located* or *refused for a
492
+ stated reason*, never the third thing — located, wrongly — and which cases are
493
+ refused is a table someone has to edit rather than a branch that can drift.
494
+
495
+ *Widened while pointing it: the `satisfies` row did not cover the spelling
496
+ that broke.* It applied the keyword to a param value, where the value reader
497
+ sees it, and not to the registry argument, where the walk that finds the
498
+ literal does — so it stayed green through a defect named after it. It now
499
+ carries both positions, as the `as-const` row beside it always did.
500
+
501
+ - **`readArchivedChanges`' fan-out is bounded — the fourth one, and the first
502
+ outside `locate.ts`.** `archive.ts` read every archived first-run record
503
+ through a `Promise.all(names.map(…))`, so a project with N merged changes held
504
+ N reads and N parsed `RedRecord`s at once. It is the same shape as the other
505
+ three against an input nobody here chooses — an `archive/` grows by one folder
506
+ per change merged and is never pruned, so it is the longest-lived of the four
507
+ lists a project's shape decides the length of. Measured in
508
+ `tests/locate-fanout.spec.ts`, which now counts a fourth pool: **64 before, 32
509
+ after**, on 64 archived changes. Unbounded, the figure is the count of the
510
+ folders; bounded, it is the constant.
511
+
512
+ *Why it hid is that the pool's own doc comment names the rule and this caller
513
+ is not in the file that has it.* `forEachBounded` says "anything here that
514
+ reads a list the project's tree decides the length of belongs in this pool,
515
+ whatever the function around it is called" — and `readArchivedChanges` is
516
+ exactly that, one module over. The nearest comment to it reasons carefully
517
+ about concurrency and still says nothing about a bound: `inspectRedEvidence`
518
+ explains why it runs the archive read *after* `inspectProject` rather than
519
+ beside it — "overlapping them buys nothing worth a second fan-out" — which is
520
+ a correct answer about sequencing two reads and not a claim about what happens
521
+ inside either. That is the third time in this catalogue a considered comment
522
+ about concurrency has stood where a bound was missing. `forEachBounded` is now
523
+ exported for it, with a fourth constant on the reason `WALK_CONCURRENCY`
524
+ already gives.
525
+
526
+ **The failure this invites is memory, not descriptors — and that corrects the
527
+ three entries above.** Each of those closed by calling descriptor exhaustion
528
+ the invited failure and leaving it unmeasured. Measured now, on Linux: it is
529
+ not reachable through this shape at all. `fs.promises.readFile` does
530
+ open-read-close inside one libuv threadpool work item, so concurrent
531
+ *descriptors* are bounded by the threadpool and never by the fan-out — 3,000
532
+ unbounded in-flight reads peaked at **53** entries in `/proc/self/fd`, and the
533
+ read completed cleanly under `ulimit -n 256`, and again under `ulimit -n 128`
534
+ with `UV_THREADPOOL_SIZE=1024`. What does track the input is heap: on 2,000
535
+ changes of a 45 KB record, peak `heapUsed` was **352 MB unbounded against 177
536
+ MB bounded**, over interleaved runs — and the unbounded figure wandered from
537
+ 329 to 580 MB across rounds while the bounded one held 177–181. The stable
538
+ number is the point; the retained result is the floor, and what the bound
539
+ removes is the transient pile on top of it. This is the portable-arithmetic
540
+ standard the three earlier halves were accepted on, so it needs no incident
541
+ report — but the *reason* is memory, and future sweeps should stop reaching
542
+ for `ulimit`, which does not govern this on either platform: Windows uses
543
+ Win32 handles, and on Linux the threadpool gets there first.
544
+
545
+ Reached only from `inspectRedEvidence` and so from `@am_shork/attest/inspect`,
546
+ the published subpath — no CLI command reads the archive, which is why the
547
+ static-command roster is untouched by this. No behaviour change and no
548
+ `ATX-n`, on the precedent of the other three: the changes, their order, their
549
+ paths and the merged record are identical, asserted beside the peak. The
550
+ result is built by preallocate-and-index rather than by arrival, which is what
551
+ keeps the sorted folder order true through a pool that completes out of order.
552
+
553
+ - **The publish job runs on protected tags only, and it says so in the file
554
+ rather than in a settings page.** `rules` was `if: $CI_COMMIT_TAG`; it is now
555
+ `if: $CI_COMMIT_TAG && $CI_COMMIT_REF_PROTECTED == "true"`.
556
+
557
+ *The reason it needed to be in the file is that the comment beside it claimed
558
+ a defence that does not exist.* It said the reason to protect release tags had
559
+ changed — from exposing a masked variable to `id_tokens` being "issued to
560
+ protected refs only". That is not how GitLab issues them. Every job gets an ID
561
+ token, and the protection state travels *inside* it as the `ref_protected`
562
+ claim; a design that reports protection as a payload field is one where an
563
+ unprotected ref is handed a token too. Nothing on npm's end narrows it either:
564
+ a GitLab trusted publisher is namespace + project + top-level CI file path +
565
+ optional environment, with no ref condition available to configure.
566
+
567
+ **So the entire ref authorisation was the `rules` line — evaluated from the
568
+ `.gitlab-ci.yml` at the pushed tag, which is a file the pusher had just
569
+ written.** "Can push a tag" was therefore "can publish `@am_shork/attest` with
570
+ a valid provenance attestation", and the `NPM_ID_TOKEN` guard added in the
571
+ entry below cannot narrow it: that guard asks whether a token exists, never
572
+ who caused one to be minted. Protected tags in *Settings -> Repository* were
573
+ the only thing standing there, described by the comment as a belt-and-braces
574
+ extra.
575
+
576
+ This is the shape this repository keeps finding — a defence that reads as
577
+ present and is not, alongside `bin/attest.js`'s static import, the `hasWarned`
578
+ spread and `hmr: false` — arriving this time in the sentence explaining a
579
+ defence rather than in the code implementing one. Keep the protected-tag rule
580
+ in Settings as well: only that side stops the tag being created, and this side
581
+ only declines to publish once it has been. *Found by review, not by a run —
582
+ no pipeline was executed to confirm it, and the two vendor behaviours above
583
+ are read off GitLab's ID-token documentation and npm's trusted-publisher
584
+ documentation rather than measured here.*
585
+
586
+ - **The publish job has no stored credential left: `NPM_TOKEN` and the
587
+ `_authToken` line are gone, and the guard is on `NPM_ID_TOKEN` instead.**
588
+ Completes what `[0.9.4]` set up. The deletion test pointed here from the
589
+ start — what it removes is the only long-lived credential this repository had.
590
+
591
+ *It went in on a measurement, and the measurement is the part worth keeping.*
592
+ The v0.9.4 release published through the trusted publisher, not the token:
593
+ `_npmUser` on that version is
594
+ `{"name":"GitLab CI/CD","trustedPublisher":{"id":"gitlab","oidcConfigId":"oidc:…"}}`,
595
+ by npm 11.19.0. The entry that proposed this recorded that **no account existed
596
+ anywhere of publishing from GitLab under OIDC** — every report was GitHub
597
+ Actions — and stated the inference at the strength the evidence carried. That
598
+ release is the first record. The token was present and unused: `oidc()`
599
+ overwrites the auth key on success, so the `.npmrc` line never applied.
600
+
601
+ **The job's own log could not have told us, and that is why the fallback had
602
+ to go.** Every path in npm's `oidc.js` logs at `verbose` or `silly` while npm's
603
+ default level is `notice`, so a successful exchange and a failed one produce
604
+ byte-identical job output — the v0.9.4 log contains no OIDC line of either
605
+ kind. Combine that with a fallback written never to throw and the failure mode
606
+ is exact: an exchange that stops working keeps publishing, on the credential
607
+ the exchange exists to retire, silently, with the difference visible only in
608
+ registry metadata nobody reads. **The property that made the rollout cheap is
609
+ the wrong one to keep**, and this is the first time in this file that a
610
+ mechanism has been removed for succeeding rather than for failing.
611
+
612
+ So the check moves in front of the irreversible step, which is the rule the
613
+ removed post-publish attestation check failed and this file already argued:
614
+ verification belongs where it can act on the answer. A missing `NPM_ID_TOKEN`
615
+ now fails before anything is published; a failed exchange fails the publish
616
+ outright, loudly and recoverably, because nothing was written.
617
+
618
+ *What is not in this repository, and is not done by this entry.* One act on
619
+ npmjs.com: *Require two-factor authentication and disallow tokens* on the
620
+ package, which is what stops a leaked token publishing it and which npm states
621
+ does not affect trusted publishers. Not a commit and not verifiable from here.
622
+
623
+ *The `NPM_TOKEN` CI variable stays, deliberately, and the reasoning is worth
624
+ recording because the first version of this entry had it wrong.* Deleting it
625
+ was filed beside the npm setting as though the two were equal acts. They are
626
+ not: the setting removes the **capability** — a token can no longer publish this
627
+ package — while deleting the variable removes **one copy** of a credential that
628
+ goes on existing in the npm account either way. Revoking it there is the act
629
+ that destroys it; removing it from GitLab is housekeeping. The variable is
630
+ masked, protected, exposed only to protected refs, and now read by no job, so
631
+ what it costs to keep is nothing. What the setting does not cover is measured
632
+ rather than assumed: it is per-package, and `@am_shork/attest` is the only
633
+ package in the scope, so today it covers the account's entire published
634
+ surface exactly. The gap it leaves is future-conditional — a second package
635
+ would start without the setting — and that is an obligation attached to
636
+ publishing a new package, not a reason to move a variable. *And the next tagged release is the test of this change*, the same way
637
+ v0.9.4 was the test of the last one — except that this time a regression fails
638
+ the release instead of quietly publishing. That is the intended trade.
639
+
640
+ ### Added
641
+
642
+ - **`ATX-83` states the three states of that field.** What was missing was never
643
+ the field; it was a sentence the suite could falsify about *when* the field is
644
+ there. A requirement saying only "the report carries the paths the merge wrote"
645
+ is satisfied completely by an implementation that always emits `[]`, and that
646
+ implementation is wrong in exactly the case the field is reached for — a re-run
647
+ after a completed merge.
648
+
649
+ *Written straight into the registry, not through `changes/`.* That is how
650
+ existing behaviour is described here, and it is the reading `ATX-80`'s
651
+ `unarchived` state exists for: `never-red` asks for a recorded failing run only
652
+ of the ids a change is currently ADDing, so a requirement catching up to
653
+ behaviour that already shipped carries no such obligation and gains nothing
654
+ from a workflow whose point is to make a scenario fail before the code exists.
655
+ The scenario lives beside the other `archiveReport` ones in
656
+ `self/json.spec.ts`, where a reader looking at that function finds all of them.
657
+ Nothing records that it discriminates, which is what `unarchived` costs and is
658
+ worth saying rather than leaving implicit — so it was checked by hand instead:
659
+ with the one line that spreads the field into the report removed, the scenario
660
+ fails, and `verify self` reports `tests-red`.
661
+
662
+ The measurement in `tests/intent-rule-candidates.spec.ts` moved by exactly one
663
+ requirement — corpus 93 → 94, universal quantifiers 40 → 41 — and no verdict
664
+ in it flipped. It also produced the `flag:` divergence key's **fourth**
665
+ re-keying in four releases, every one triggered by an addition that agrees
666
+ with a member already there, and the `field:` key's first *extension* rather
667
+ than re-keying; both are noted where the rule is judged, because which of the
668
+ two an addition causes turns on vocabulary rather than on how related the
669
+ requirements are.
670
+
671
+ - **`archive --apply --json` reports the paths the merge wrote.** The human
672
+ rendering has listed them since `--apply` shipped, on the grounds that this is
673
+ the command that edits files the user cannot regenerate and the reader needs to
674
+ know which ones to look at before committing. The `--json` reader — the one
675
+ most likely to be automating around the merge — got the change name and nothing
676
+ else. `written[]` closes that, and both renderings are now handed the same
677
+ bundle from one place in the shell rather than each being told separately,
678
+ which is the split that let the verdict disagree with itself in `[0.7.0]`.
679
+
680
+ **Absent without `--apply`, `[]` with it.** The two are different facts — no
681
+ merge was attempted, against a merge that ran and had nothing left to write,
682
+ which is the ordinary state of a re-run after a completed one — and a field
683
+ that spelled both `[]` could not say which. Additive, so no `SCHEMA_VERSION`
684
+ bump: a consumer that has never heard of the field reads the report it always
685
+ did. ATX-74's sweep covers it like every other string a report quotes out of
686
+ the checked project.
687
+
688
+ ### Fixed
689
+
690
+ - **`first-run.json` committed different bytes for the same evidence, depending
691
+ on how many runs it took to gather.** `mergeRedRecord` rebuilds the record so
692
+ key order is a function of the data rather than of insertion — and did that at
693
+ two of its three levels. The rebuild sorts the requirement ids the record
694
+ already held; the loop after it *appends* the ids this run is the first to
695
+ observe, in plan order; and the pass at the bottom re-sorted the file and
696
+ scenario levels while walking the ids in whatever order they were in, because
697
+ reassigning `record[id]` does not move a key.
698
+
699
+ So a change that grew its record over several stage-1 runs committed
700
+ `{"Z-9": …, "A-1": …}` where the same observations reached in one run committed
701
+ `{"A-1": …, "Z-9": …}`. Nothing about the outcomes differs; the file does. That
702
+ matters twice over for this particular file: it is evidence a reviewer reads in
703
+ a diff, and the gate has to reach the same verdict on a CI checkout as on the
704
+ author's machine. It also went unnoticed once written — `changed` is decided by
705
+ the outcomes alone, so an unchanged record is never rewritten and a
706
+ wrongly-ordered one stays that way.
707
+
708
+ *ATX-15 already said this and could not catch it.* The requirement quantifies
709
+ over "every ordering Attest commits to disk", and the sweep attesting it is
710
+ deliberately narrower than its own statement: it checks *how* a site orders —
711
+ every `.sort` in `src/` through `byCodeUnit` — and says so, *"a `red-record.ts`
712
+ that stopped sorting at all is still green here, and the scenarios above are
713
+ what speak to the result"*. There was no such scenario for this file. There is
714
+ now, in `self/red-record.spec.ts`: it reaches the same evidence by both routes
715
+ and compares the serialised bytes. `archive.ts`'s `sortRecord` had all three
716
+ levels right and is what this now matches.
717
+
718
+ - **`--apply` refused a registry whose literal is written with `as const`,
719
+ `satisfies`, or a bare pair of parentheses.** The reader takes all three —
720
+ `unwrap` strips the type-level wrappers before `literalValue` reads the object,
721
+ because none of them changes the value — so the gate read such a file, proved
722
+ the change green, and handed it to a merge that asked
723
+ `ts.isObjectLiteralExpression` of the wrapper and got `false`. Both writers
724
+ refused: the whole change stopped with `internal-error`, whose own message says
725
+ the file *"read as a registry for the gate but not for the merge"* and whose
726
+ comment calls the condition unreachable through the command. It was reachable
727
+ by writing `defineRequirements(({ … }))`.
728
+
729
+ *The fix is where the two answers diverged, not where the symptom showed.*
730
+ `authoringCall` is the single function both sides use to find the registry
731
+ literal — the module's opening note says a second walker would be a second
732
+ answer to "is this a literal" — and it handed back the argument node as
733
+ written, leaving each caller to decide whether to unwrap. The reader did and
734
+ the writers did not, so the disagreement the shared function exists to prevent
735
+ was inside it. It now unwraps once, and `literalValue`'s own call becomes the
736
+ no-op it should always have been.
737
+
738
+ The parenthesis is the reachable spelling and the reason this is a fix rather
739
+ than a nicety: `as const` and `satisfies` are opted into, while nothing about
740
+ `(({ … }))` looks like a type annotation anyone chose. No `SCHEMA_VERSION`
741
+ bump and no new code — a change that used to fail now succeeds, which is the
742
+ benign direction for an exit code, and the History row is what tells a reader
743
+ on an older build which half of the page applies to them.
744
+
745
+ - **A registry key written twice made `archive --apply` write bytes outside the
746
+ value it was replacing — and, measured, outside the registry literal
747
+ altogether.** `registryEntryLayouts` keys each value's source span by name in
748
+ a `Map`, and `spliceModifications` walks that map to get its edits in
749
+ ascending offset order, which is what lets the pass at the bottom be a
750
+ `reverse()` rather than a sort. `Map.set` on a key it already holds keeps the
751
+ **first** insertion's position and takes the **last** call's value — so an
752
+ entry writing `params: { limit: 1, other: 'x', limit: 1 }` describes `limit`
753
+ at position one carrying the span of the occurrence *after* `other`, and the
754
+ offsets stop ascending. The earlier edit then moves the bytes under the later
755
+ one. On a registry the gate had just proved green, changing both values landed
756
+ the second one past the file's closing `});`.
757
+
758
+ *Nothing upstream refuses such a file, which is why this reached the one
759
+ command that writes.* A duplicate key is a TypeScript **semantic** error, and
760
+ `parseSource` asks only the syntactic question (`[0.9.0]`, ATX-69) — so both
761
+ registry readers take the file and hand back the last occurrence, `check` is
762
+ silent, and the gate passes. The reachable route is an ordinary copy-paste in
763
+ a hand-written registry, in a project that does not typecheck its
764
+ `*.reqs.ts`.
765
+
766
+ **Refused rather than described, at all three levels.** An entry whose keys
767
+ repeat is left out of the layout with the repeated key's path recorded beside
768
+ it, and `spliceModifications` turns that into an `apply-unsupported-delta`
769
+ refusal — whole, with nothing written — before a single edit is pushed. The
770
+ id level is not the mild one and is refused for the same reason rather than a
771
+ weaker one: two entries under a single id put a *later* entry's spans at an
772
+ *earlier* entry's position, so the disorder is between entries rather than
773
+ inside one. The guard stays exactly as wide as the danger — an unrelated id in
774
+ the same file is still written — because a merge that stopped over something
775
+ it was not asked to touch is the thing `--apply` refuses to do everywhere
776
+ else.
777
+
778
+ *The sibling defect is already in this file and did not generalise on its
779
+ own.* `[0.8.0]` fixed an integer-like `params` key for the identical
780
+ invariant — JavaScript hoists it to the front of every object, so a plain
781
+ object could not carry the file's order — and the repair was to move all three
782
+ levels to `Map`s. That answered the container and left the *keys*: a `Map`
783
+ keeps insertion order for every key type, and the one thing it still cannot
784
+ represent is one key written twice. The test added then, `rewrites two params
785
+ when one of them has an integer-like key`, is the shape of the three added
786
+ here, down to needing two changed values of different lengths — one edit
787
+ cannot be out of order with itself.
788
+
789
+ `verifyWritten` did catch the result and stop the merge, so the failure was
790
+ never silent; the file had already been overwritten by then, which is the
791
+ "destructive on a file the user cannot regenerate" shape the `AGENTS.md` merge
792
+ tool was rejected for and the reason a post-hoc check is not where this
793
+ belongs.
794
+
795
+ **Breaking, and narrowly so — this is the entry that decides the version
796
+ number.** The common case is not: a repeated key usually made the merge write
797
+ outside the value and the re-read then failed it, so the run already exited 1
798
+ and only the `code` moves — `internal-error`, raised after the registry had
799
+ been overwritten, becomes `apply-unsupported-delta`, raised before anything is
800
+ written.
801
+
802
+ But the offsets only go wrong when an earlier edit *changes length*. A
803
+ modification that replaces a value with one the same size shifts nothing, so
804
+ on `[0.9.4]` such a merge completed and completed **correctly**. Measured on a
805
+ registry with a duplicated `statement` and a `budgetMb: 1` → `2` patch: 0.9.4
806
+ merges it and it reads back as `{"budgetMb":2}`; today it refuses. That is
807
+ previously-valid input now rejected, which is this file's own definition of
808
+ breaking, so it takes the **minor** rather than the patch.
809
+
810
+ The refusal is still right — the same file with a length-changing patch is the
811
+ corruption above, and which of the two a merge gets is not something the author
812
+ chose. What the narrowness costs is that an adopter whose registry happens to
813
+ carry a duplicate key sees a working command start refusing, so it belongs in a
814
+ minor with the reason stated rather than in a patch nobody reads.
815
+
816
+ No `SCHEMA_VERSION` bump: the envelope, the `Issue` interface and the roster in
817
+ `core/docs.ts` are all unchanged — the refusal reuses the code `--apply`
818
+ already raises for a delta it will not write.
819
+
820
+ **`ATX-76` is widened to say what it always meant**, through this
821
+ repository's own workflow — `self/archive/2026-08-21-widen-atx-76`, the second
822
+ change to go through it and the first carrying a MODIFIED. Its refusal clause
823
+ named one instance of a class: *"when a span it would overwrite carries a
824
+ comment"*. Refusing a comment inside a span is not a rule about comments, it is
825
+ the rule that a replacement may only ever write the value it was asked to
826
+ write, stated at the one place that value's boundary was known to be in doubt.
827
+ A repeated name puts it in doubt a second way, so the clause now reads
828
+ *"wherever the file leaves that span uncertain"* and lists both. What is
829
+ **not** stated is the refusal as a mechanism: refusing is how *this*
830
+ implementation keeps the first clause, and a rewrite could keep it by ordering
831
+ the edits instead — a requirement pinned to the remedy would forbid that repair
832
+ for no reason.
833
+
834
+ *The defect was never that the rule was absent.* The first clause — the unit of
835
+ the edit is the value — has said this since `[0.9.0]`. What was missing was a
836
+ sentence the suite could falsify in this direction: both existing scenarios
837
+ assert something about a **comment**, and neither can go red when the failure
838
+ is that the span located was the wrong one. `self/merge.spec.ts` now carries a
839
+ third, looping the three sites a name can repeat with the extent pinned beside
840
+ the loop. Measured with the fix reverted, at the `spliceModifications` seam:
841
+ all three wrote without refusing; with it, all three refuse.
842
+
843
+ ### Documentation
844
+
845
+ - **The CLI reference told readers the `--json` guarantee rests on
846
+ `JSON.stringify`, which is the reasoning `[0.8.0]` retired.** *Output is safe
847
+ to read* closed with "`--json` was never exposed: serialising escapes the same
848
+ characters" — true of C0 and of nothing else, so DEL and the C1 range were
849
+ never covered by it. The code has been right since `[0.8.0]`, where the
850
+ replacer in `renderJson` began stripping every string in the document as it
851
+ serialises one; the page kept the retired argument, in both languages, three
852
+ hundred lines above its own History row saying the opposite.
853
+
854
+ *The comment at the crash site in `cli/action.ts` said it too*, and that copy
855
+ is the one worth naming: it sat directly above `renderJson(errorReport(…))`
856
+ and read as a settled argument for why sanitising there was unnecessary. The
857
+ replacer it argues against is what makes that line safe. Nothing was
858
+ exploitable — this is a false rationale standing beside a working defence,
859
+ which is the configuration that gets a defence deleted by someone tidying up.
860
+
861
+ Found by an attack-surface review rather than by a gate, and no gate could
862
+ have found it: the suite checks that a `§n` a comment cites exists and that a
863
+ version a History table names is a heading here, never whether a sentence is
864
+ true. Three copies of one retired claim is what that gap looks like from
865
+ inside. No behaviour changed and no version moved, so there is no new History
866
+ row — the `[0.8.0]` row was correct all along and is what the prose now agrees
867
+ with.
868
+
869
+ - **The method page says which shape a composite param is best at, and the
870
+ sample is a fixture that runs.** `intent-quality.md` already carried both
871
+ hazards a list-valued param has — an expectation recomputed from the param it
872
+ is checking, and a loop whose extent nobody pinned — as the third and fourth
873
+ of its five findings, named and left there with no shape attached. A **decision table**, rules pairing conditions with the
874
+ outputs the requirement promises, is where both arrive at once and where the
875
+ single source pays most, so it is now named. The section states the property
876
+ the shape actually rests on, which is narrower than "use a table": the
877
+ implementation reads only the *condition* columns, which leaves the output
878
+ columns an independent term — and it stops being true the moment the system
879
+ under test is itself driven by the same rules.
880
+
881
+ *The sample is `fixtures/consumer/requirements/cart.reqs.ts` and
882
+ `fixtures/consumer/cart.spec.ts`*, added here and registered in
883
+ `PUBLISHED_SAMPLES`, because every `ts` block this page publishes was already
884
+ a file the packaging test installs from a tarball and runs, and half-real
885
+ samples teach a reader to check which — after which they stop. **Making it
886
+ real is what found the constraint the section now documents**: the statement
887
+ cannot interpolate the table. A `{placeholder}` resolves to a scalar or a list
888
+ of scalars, so `{rules}` is `non-scalar-interpolation`, and the statement has
889
+ to name the behaviour in prose while the rules stay a param the scenario reads.
890
+ A draft written only as prose had it wrong and nothing would have said so.
891
+
892
+ It also landed on an existing row rather than needing a new rule:
893
+ `intent-rule-candidates.spec.ts` classifies it `over: 'roster'`,
894
+ `owner: 'registry'`, which is the branch that owes a `pin` — a registry-owned
895
+ list is data an author can shorten, and every assertion inside the loop still
896
+ passes over the shorter one. The published scenario carries that pin
897
+ (`expect(rules).toHaveLength(6)`), so a reader who copies the block copies the
898
+ counter-pressure with it.
899
+
900
+ *It was written into the README first, and has moved.* The README is the
901
+ pitch, the five-minute start and navigation; a hundred lines on how to write a
902
+ param that pins something is the method page's job, beside the two findings it
903
+ repairs. The README keeps one paragraph pointing at it. **The move is also
904
+ what showed `PUBLISHED_SAMPLES` could not say where a sample is published** —
905
+ it read the README and only the README, which was right while every sample
906
+ lived there and an obstacle the moment one did not. Each entry now carries its
907
+ page, so moving a sample between pages is a roster edit rather than a silent
908
+ loss of the gate that keeps it runnable.
909
+
16
910
  ## [0.9.4] - 2026-08-19
17
911
 
18
912
  ### Added
@@ -5373,96 +6267,10 @@ losing the argument would cost more than losing the task.
5373
6267
  **Where an entry went is recorded under the version that shipped it.** This
5374
6268
  section keeps no ledger of its own departures — one was maintained here by hand
5375
6269
  for several releases, and a count kept in prose with nothing to check it is a
5376
- claim that goes wrong quietly. What is worth keeping is what the section learned
5377
- about itself, which is three things.
5378
-
5379
- **An entry can sit here for releases on the strength of a figure nobody has
5380
- checked.** The discipline aims at keeping the *argument*, not at re-testing the
5381
- evidence under it, so the first act of building one is re-measuring what it
5382
- claims rather than implementing what it proposes. That has paid three times:
5383
- once killing an entry outright, when the spans it rested on turned out not to be
5384
- spans; four times correcting an entry's own numbers or scope before a line of it
5385
- was written — a call-site count that a regex had inflated, a cost filed as "a
5386
- fixture per diagnostic" that the corpus put at one fixture, an injection whose
5387
- payload turned out to need the target file's *own* quote character, which is the
5388
- difference between escaping one quote and escaping the right one, and a fix
5389
- filed as a map over `issues` that the report's own shape moved to the
5390
- serialiser, four fields and one nesting level further out. **What a release costs its adopters is not known on the day it is cut**
5391
- — several entries arrived from one adoption report, about shapes the tool made
5392
- someone work around without ever erroring. And **the fixes with no observable
5393
- symptom are worth batching**: individually none justifies a commit, together they
5394
- cost about an hour, and they are the group no gate could have found, since having
5395
- no symptom is the definition of what a gate cannot see.
5396
-
5397
- *The five entries this section carried before the ones below* were built and are
5398
- recorded under `[0.8.0]`. The second is where this preamble's own
5399
- discipline paid again: it went in on an entry whose headline type
5400
- measurement — instantiations unchanged — was false on re-measure, and the
5401
- re-measure is the only reason the release notes do not repeat it. The fourth is
5402
- the other outcome the discipline has, and worth a line because the first three
5403
- were all corrections: the `render --out` symlink measurement reproduced exactly
5404
- as filed, down to the leaf being safe for the reason given. What re-measuring
5405
- added was not a correction but a **surface the entry had not counted** —
5406
- `--check` reads the same destination, so the same link makes a freshness gate
5407
- date a document outside the project. Re-measuring an entry is therefore not only
5408
- a test of the number it quotes; walking the code the claim is about is what
5409
- shows the claim's own edges. The fifth is that lesson arriving where an entry
5410
- had already named its own fix: the C1 measurement reproduced exactly, and the
5411
- repair it proposed — a map over `issues` — was still the wrong half, because
5412
- four more fields and a nested scenario name carry the same untrusted text and
5413
- never pass the envelope. An entry that ends with a fix is the one least likely
5414
- to have its fix re-examined, which is the case worth naming.
5415
-
5416
- *That has now happened twice, so it is a pattern rather than an incident.* The
5417
- extensionless-import entry likewise reproduced exactly on its reported half and
5418
- was wrong about its own repair — it recorded that the writing half needed
5419
- nothing, and the writing half was guarded by a condition that was correct only
5420
- while the reading half stayed broken. Both times the error had the same shape: an
5421
- entry reasons about the code path it can see failing, and the repair makes a
5422
- second path reachable that nobody has looked at because nothing could reach it.
5423
- So the re-measure that matters is not "does the reported symptom reproduce" —
5424
- both did, exactly — but **what does the proposed fix make reachable for the first
5425
- time**.
5426
-
5427
- *The first time that question was asked in advance, it paid, and how it paid is
5428
- worth a line because it is not what the two cases above would predict.* The
5429
- `check` `empty-spec` entry — `[0.9.0]`, the third entry to reproduce exactly
5430
- on its reported half — was not wrong about its own repair. Asking the question
5431
- anyway found the defect one step to the side: the fix it proposed was correct,
5432
- and the *obvious implementation of it* — make `check` agree with `verify` —
5433
- would have copied a misdiagnosis `verify` had been emitting unnoticed, because
5434
- the reference implementation an entry points at is not itself re-read. So the
5435
- question generalises past the shape it was learned from. It is not only about a
5436
- path the repair makes reachable; it is about **everything the repair would make
5437
- `check` agree with**, and an entry that names the right fix can still be
5438
- implemented wrongly by copying the half nobody has audited.
5439
-
5440
- - **Retiring `NPM_TOKEN`, which is now two settings on npmjs.com rather than
5441
- anything in this repository.** The mechanism landed under `[0.9.4]`: the
5442
- publish job requests an npm OIDC token and publishes through `npm`, which is
5443
- the only client that performs the exchange. What that did **not** do is remove
5444
- the credential, and the distinction is the point — enabling a trusted publisher
5445
- leaves an automation token just as able to publish. Two acts remain, neither of
5446
- them a commit:
5447
- **(1)** register the trusted publisher on the package — `Pseudorca`, `attest`,
5448
- `.gitlab-ci.yml` with the extension, the allowed actions; environment name is
5449
- optional and this project uses none, and shared runners are the only supported
5450
- kind, which the job already uses because it carries no `tags:`. Free and
5451
- reversible, and safe to do at any time: a failed exchange falls back to the
5452
- token rather than failing the publish (measured — see the `[0.9.4]`
5453
- entry). **(2)** set *Require two-factor authentication and disallow tokens* on
5454
- the package, which npm states does not affect trusted publishers. That is the
5455
- one that closes the loop the job's own comment opens, where an automation token
5456
- is used *because* a granular token fails against 2FA on writes: trusted
5457
- publishing does not bypass the prompt, it stops being a token.
5458
- *What is unmeasured, and it is not measurable from here.* Whether the exchange
5459
- actually succeeds against a gitlab.com project. Every documented ingredient
5460
- checks out and no plan requirement is stated anywhere, but there is no record
5461
- of anyone publishing from GitLab under OIDC — every account is GitHub Actions.
5462
- The first tagged release after the `[0.9.4]` change is the test, and the
5463
- fallback is what makes running it cheap: if the exchange fails, the release
5464
- still publishes and the job's verbose log says why. **Do not delete the token
5465
- before a release has been observed publishing without it.**
6270
+ claim that goes wrong quietly. What these sections have learned about
6271
+ re-measuring an entry before building it is at the foot of the file, under
6272
+ `How an entry is re-measured` — it applies to all three, and was filed here only
6273
+ because the first of it was written here.
5466
6274
 
5467
6275
  ## Under consideration
5468
6276
 
@@ -5515,12 +6323,20 @@ have to gather again.
5515
6323
  tautology.
5516
6324
  **The framing that probably resolves it: this is `never-red` wearing different
5517
6325
  clothes.** A tautological assertion is a scenario that cannot fail, which is
5518
- the exact defect `archive`'s red-before-implement gate exists to catch — and it
5519
- did catch two of them in this very repo (`CHECK-2`, `FOG-3`, both confirmed
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
5520
6328
  correct by the reporter). The reason it did not catch these is structural:
5521
6329
  **changing the value of an existing param goes through no gate at all.**
5522
6330
  `archive` gates a change; a param edit is not a change. That is a cleaner
5523
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.
5524
6340
  **That observation is now its own entry at the foot of this section**, split
5525
6341
  out and measured on 2026-08-18, because it asks what happens when a value
5526
6342
  moves rather than whether an assertion is a real comparison, and neither
@@ -5670,7 +6486,7 @@ have to gather again.
5670
6486
  pass and separating them is the finding.
5671
6487
 
5672
6488
  - **Whether a requirement can ever be retired, and what it costs that none can.**
5673
- The registry is at **79 requirements and 221 scenarios, 2.80 each**, and
6489
+ The registry is at **85 requirements and 240 scenarios, 2.82 each**, and
5674
6490
  every one is a permanent obligation: `uncovered-requirement` is an ERROR, so a
5675
6491
  requirement without a scenario cannot exist, and nothing ever removes one.
5676
6492
  `delta.removed` exists and `applyDelta` applies it, but `--apply` refuses to
@@ -5692,7 +6508,7 @@ have to gather again.
5692
6508
  requirement, where the scenario should move rather than go. Only the second is
5693
6509
  really about cost, and it is the one where deleting the intent to save the
5694
6510
  scenario inverts what the whole tool claims — that intent outlives its tests.
5695
- Nothing here is urgent at 69. It is filed now because the moment to write the
6511
+ Nothing here is urgent at 85. It is filed now because the moment to write the
5696
6512
  argument down is while the reasoning is available, and because a project whose
5697
6513
  requirement count only rises should have decided in advance what would make
5698
6514
  one leave.
@@ -5728,6 +6544,56 @@ have to gather again.
5728
6544
  no longer supported is the acceleration. **Reopening on cost needs the ratio to
5729
6545
  move again**; reopening on the three meanings of "retire" needs none of these
5730
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.
5731
6597
 
5732
6598
  - **Running `verify` over a subset of the scenarios, and what a partial run may
5733
6599
  be allowed to say.** `verify` is all-or-nothing and `check` executes nothing,
@@ -5762,8 +6628,8 @@ have to gather again.
5762
6628
  slow half is smaller, so the question is a selector's ergonomics rather than
5763
6629
  one project's gates; and a scored answer to whether a scope-carrying report can
5764
6630
  be stated in a way that a `--json` consumer written before it cannot misread.
5765
- **Measured on 2026-08-12, before building anything, per this section's
5766
- discipline. The second of those two is now answered, and the answer is a
6631
+ **Measured on 2026-08-12, before building anything, per `How an entry is
6632
+ re-measured`. The second of those two is now answered, and the answer is a
5767
6633
  refusal; the first blocker was wrong; and the whole feature turns out to be a
5768
6634
  different, much cheaper one.**
5769
6635
  *The cost claim reproduces on a second corpus.* `verify self`: the static half
@@ -5980,6 +6846,31 @@ have to gather again.
5980
6846
  §6 already records as the category no gate reaches: the page's three samples
5981
6847
  are unasserted today, and a fourth in a directory that looks executable would
5982
6848
  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
6850
+ buildable one.** `examples/judgement-table/` is in the default suite, the
6851
+ specifier is aliased rather than relativised so the file stays copyable
6852
+ verbatim, and both assertions were shown to fail before it was called done.
6853
+ So the category `pre-commit-check` §6 names now has exactly one member that a
6854
+ gate reaches, and the page's three prose samples are still not it.
6855
+ *Two things the build found that the proposal did not contain.* A third
6856
+ assertion — `issues` empty, before either of the other two — which is not a
6857
+ refinement but a hole: an unread registry file and a smaller project are the
6858
+ same value, and the two completeness directions pass over both. It was found
6859
+ by an invalid mutation during the bite test rather than by reasoning, which is
6860
+ `How an entry is re-measured`'s question about what a repair makes reachable,
6861
+ arriving from the other end. And a **type-level** forcing move the runtime
6862
+ assertion cannot make: a required field added to `Judgement` fails every
6863
+ existing row at compile time, so the schema-shaped half of "a row cannot
6864
+ arrive unjudged" costs one line and no engine rule. That is worth noting
6865
+ against blocker **(1)** above, which is about freezing a `Judgement` type:
6866
+ what shipped freezes nothing, because the type is in the adopter's copy.
6867
+ **What is untouched is the first blocker, and it is now the only thing between
6868
+ this entry and its evidence.** Whether an empty frame reads as an invitation
6869
+ or as a form to fill in is still untested, and cannot be tested here — the
6870
+ people who would answer it are the ones who have not arrived. What the build
6871
+ changes is only that the five voluntary steps are now four, and the one
6872
+ removed was the one with a fixed answer. If the next report is still nobody,
6873
+ the scaffold was not what was missing, and that is worth knowing too.
5983
6874
 
5984
6875
  - **Prefix ownership is inferred rather than declared, and the cost arrives only
5985
6876
  when adoption succeeds.** Design §11 records that the id grammar has no level
@@ -6101,127 +6992,6 @@ have to gather again.
6101
6992
  looking at already being that page's subject, and this being a fifth of the
6102
6993
  same kind.
6103
6994
 
6104
- - **`/inspect` returns five exported shapes and the page telling people to build
6105
- on them says nothing about whether they move.** Filed 2026-08-19 from a product
6106
- review of `[0.9.1]`–`[0.9.3]`. `inspect.ts` exports `Inspection`,
6107
- `ScenarioEvidence`, `EvidenceRow`, `ArchivedChangeRef` and
6108
- `RedEvidenceInspection`, and `intent-quality.md` carries three samples
6109
- destructuring them. `SCHEMA_VERSION` governs the `--json` envelope and nothing
6110
- governs these.
6111
- *The policy is not missing — its statement is.* The Versioning section at the
6112
- head of this file already binds them: removing or renaming a public API bumps
6113
- the minor, and under 0.x that is the breaking bump. So a reader who finds that
6114
- section is already told what they need. The gap is that the reader this export
6115
- was written for arrives through `intent-quality.md`, which is a method page
6116
- reached from the README's document table, and nothing on the path between the
6117
- two states it.
6118
- *Why it is worth a decision rather than a one-line edit.* The two candidate
6119
- sentences say opposite things to the person this needs. Restating the existing
6120
- promise reads as a stability guarantee and is the honest summary of current
6121
- policy; saying the shape may move before 1.0 is the more useful warning and
6122
- discourages exactly the adopter whose table would decide the judgement-schema
6123
- entry above. Which one is right depends on the 1.0 entry below, which is why
6124
- this is filed rather than fixed.
6125
- *One thing already measured, in the other direction.* `pre-commit-check` §6
6126
- records that changing what `/inspect` returns breaks three usage samples that
6127
- no gate can see. The internal awareness of this surface's fragility therefore
6128
- exists and is written down; the outward-facing half is what does not.
6129
-
6130
- - **The store worth retrieving is this file, not `archive/`, and no shape has
6131
- been proposed for either.** Filed 2026-08-19 from a product review of
6132
- `[0.9.1]`–`[0.9.3]`, and it is the OpenViking reading turned back on the
6133
- repository that did the reading. Everything that came out of it is about
6134
- `archive/`, and the entries are no longer counted here — the number was wrong
6135
- on the day it was written, was corrected on 2026-08-19, and was wrong again by
6136
- the next commit, when the precedence rule left for design §11. Three strikes on
6137
- one figure is the section's own preamble arriving in the first person: a count
6138
- kept in prose with nothing to check it goes wrong quietly, and the repair is to
6139
- stop keeping one rather than to keep fixing it. But `archive/` holds `first-run.json` — typed,
6140
- low-information, and now read — beside a `proposal.md` that in this repository
6141
- carries a link, for the reason `[0.9.3]` gives: `CLAUDE.md` makes this file the
6142
- single decision record. **So the store that actually holds decisions with their
6143
- evidence attached is `CHANGELOG.md`, at 7,100 lines with the three foot
6144
- sections past 1,700 of them,** and the property that makes it valuable — a
6145
- design evaluated, priced and killed, with the reasoning that killed it — is
6146
- exactly the property that makes it unreadable in one pass. It is append-only
6147
- with no retrieval, which is the shape the OpenViking reading was about.
6148
- *What an agent would actually consume is a composition, not a new store.*
6149
- Given an `ATX-n`: its statement and params, the scenarios covering it, the
6150
- first-run evidence for each, and the paths to the change that introduced it.
6151
- Four of the five are already computable — `inspectProject` answers two,
6152
- `inspectRedEvidence` the third and the archived refs the fourth — and the fifth,
6153
- the reasoning, is the one with no index and no key. That asymmetry is the
6154
- finding: the retrieval problem is not in the half that was just made readable.
6155
- *One thing to unmerge before deciding.* `[0.9.3]` settled a question against
6156
- the fifth static command, and it settled it correctly and on the right
6157
- grounds — `first-run.json` is JSON, and `ATX-16`'s subject is reading
6158
- TypeScript without evaluating it, so joining that roster would turn a security
6159
- promise into a list of commands that do not run the suite. But that is a
6160
- ruling about **roster membership**, and it is being carried as though it also
6161
- ruled on whether such a command should exist. Those are separate, and only the
6162
- first was argued.
6163
- *What blocks the decision, and what would cheaply unblock it.* No measurement
6164
- exists of whether a bundle like this changes what an agent does — the one datum
6165
- quoted in `[0.9.3]` measured demand to read the archive **as prose**, which is
6166
- a third question again. The proposal is therefore a probe and not a build,
6167
- per this section's own discipline of re-measuring before implementing: assemble
6168
- the bundle by hand for one requirement, run one real change against it, and
6169
- record whether it was used, ignored, or used wrongly. One session, and it
6170
- decides between a command, a skill surface, and nothing. Building any of the
6171
- three before that measurement is the order `divergent-param` was designed in
6172
- and the order this file has already paid for once.
6173
-
6174
- - **A stage-1 spec can read its params through `delta.added` instead of by id,
6175
- and the merge that repoints its import cannot repoint that.** Filed 2026-08-19
6176
- from the first change this repository put through its own `changes/` workflow,
6177
- where it was written by the author of the workflow's own tooling — which is
6178
- most of why it is worth an entry rather than a fix.
6179
- `delta()` returns the delta *and* the registry of what it adds
6180
- (`withProposedRequirements`), so `reqs['ATX-81'].params.field` reads a proposed
6181
- requirement before the merge and a merged one after it. That is the whole
6182
- reason merging a spec changes its import and nothing else. Reaching through
6183
- `added` — `d.added['ATX-81'].params.field` — is equally correct during stage 1
6184
- and throws the moment `--apply` repoints the specifier at the registry, because
6185
- a registry has no `added`.
6186
- **Nothing catches it, and the reason is structural rather than an oversight.**
6187
- The archive gate runs *before* the rename, so the spec it graded is not the
6188
- spec the merge commits; `check` executes nothing, so it never evaluates the
6189
- accessor; and the type checker is satisfied on both sides of the merge only
6190
- because it is run against whichever half is currently on disk. It surfaced at
6191
- the next `verify`, as `spec-load-failed` — a correct diagnosis of the symptom
6192
- that names the file and not the cause. The window between `--apply` and the
6193
- next `verify` is the whole exposure, and in a workflow that ends at `--apply`
6194
- it is unbounded.
6195
- **The guidance already exists and did not prevent it, which is the finding.**
6196
- `registry.ts`'s doc comment on `delta` states the intended expression and the
6197
- rejected one, in those terms, on the function itself; the document `init`
6198
- writes shows the by-id form in its worked example. So this is not a gap in what
6199
- is written. It is a case of the correct form and an incorrect one being
6200
- *indistinguishable at the moment of writing* — both compile, both pass, both go
6201
- red for the right reason — with the difference appearing one command later.
6202
- *Two shapes, and they point opposite ways, which is why this is filed rather
6203
- than fixed.* **(1)** A `check` diagnostic: `parser.ts` already walks property
6204
- accesses inside spec bodies to find `.params`, so finding `.added` on a binding
6205
- imported from a `requirements.delta` path is the same walk and the same file.
6206
- It has to be a WARNING, because enumerating what a change adds is a legitimate
6207
- thing for a spec to do, and a WARNING is exactly the strength that did not stop
6208
- the author here. **(2)** A type-level refusal: have `delta()` return a type
6209
- without `added`, so `typecheck:all` refuses it with no new diagnostic, no
6210
- roster entry and no runtime cost. `DefinedDelta<T> = T & ProposedRequirements<T>`
6211
- carries `added` because `T` does, so this means `Omit<T, 'added'>` — and that
6212
- breaks the evaluating loader, which hands the same default export to
6213
- `applyDelta` typed as `RegistryDelta`. Whether that break is real or just needs
6214
- the internal path to name a different type is **unmeasured, and is the first
6215
- thing to establish**; it decides between the two shapes rather than being a
6216
- detail of one.
6217
- *What blocks the decision:* frequency, and it is the same blocker the `open`
6218
- entry carried before it — one occurrence, in one repository, on the first
6219
- change ever run through the workflow. That is a report rather than a
6220
- hypothetical, which is more than most entries here start with, and it is still
6221
- n=1. What would settle it cheaply is the next change: if the by-id form is
6222
- written without prompting, the guidance is sufficient and this is an anecdote;
6223
- if the reach-through appears again, the two shapes above are worth pricing.
6224
-
6225
6995
  - **Nothing is frozen, and the evidence this repository is waiting for requires
6226
6996
  somebody to build on something.** Filed 2026-08-19 from a product review of
6227
6997
  `[0.9.1]`–`[0.9.3]`. Three releases in five days each added public surface, and
@@ -6241,10 +7011,107 @@ have to gather again.
6241
7011
  on one adapter, which is the objection this repository raises against every
6242
7012
  other premature interface and does not get to waive for its own. And the
6243
7013
  release-notes discipline cuts both ways here: a 1.0 is a claim about what the
6244
- cost of adoption will be, and `Planned`'s own preamble records that **what a
6245
- release costs its adopters is not known on the day it is cut**. There is no
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
6246
7016
  measurement to date of anyone adopting either export.
6247
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.
7114
+
6248
7115
  ## Considered and rejected
6249
7116
 
6250
7117
  Decisions **not** to build something, kept where they can be found before the
@@ -6255,6 +7122,233 @@ it sat between 0.2.0 and 0.1.7 for two releases, where standing still meant
6255
7122
  sinking one version deeper each time a release was cut above it, and a rejection
6256
7123
  filed under a version reads as belonging to it.
6257
7124
 
7125
+ ### An id-keyed retrieval bundle over `archive/` and this file
7126
+
7127
+ Proposed 2026-08-19 inside the entry now titled *The decisions worth retrieving
7128
+ are in this file*: given an `ATX-n`, assemble its statement and params, the
7129
+ scenarios covering it, the first-run evidence for each, and the paths to the
7130
+ change that introduced it — the reasoning being the fifth part and, as filed,
7131
+ "the one with no index and no key". The entry declined to build it and specified
7132
+ a probe instead: assemble the bundle by hand for one requirement, run one real
7133
+ change against it, and record whether it was used, ignored, or used wrongly.
7134
+ **One session, and it decides between a command, a skill surface, and nothing.**
7135
+
7136
+ Run on 2026-08-28 on `ATX-79`, against the `examples/judgement-table/` change
7137
+ shipped in the same pass. **The answer is nothing.**
7138
+
7139
+ **Four of the five parts came back in one script**, as the entry predicted:
7140
+ statement, params and `outOfScope` from `inspectProject`; three scenarios with
7141
+ their evidence states and four archived change refs from `inspectRedEvidence`.
7142
+ No new surface was needed to assemble it. A command would therefore be a wrapper
7143
+ over two functions that already ship, which is the shallow-module case rather
7144
+ than a retrieval mechanism.
7145
+
7146
+ **The row that did the work is a plain field, and not the one this was about.**
7147
+ `outOfScope: ['judging the quality of what it reports', …]` settled the change's
7148
+ central design question — ship the enforcement, withhold the columns — and it
7149
+ has been a field of `inspectProject` since `[0.9.2]`. What the composition added
7150
+ was that it was *present at the moment of the decision*, which is a claim about
7151
+ assembly rather than about a missing store, and assembly is what a caller with
7152
+ two function calls already has.
7153
+
7154
+ **The evidence half is structurally empty for most ids here.** All three rows
7155
+ came back `unarchived`, which is correct — `ATX-79` was written straight into
7156
+ the registry — and which `ATX-80` defines as **not a finding**. So for any
7157
+ brownfield id that quarter of the bundle carries nothing, and brownfield is most
7158
+ of this registry. The bundle's information content varies enormously by which id
7159
+ is asked about, which nothing in the proposal anticipated.
7160
+
7161
+ **And the fifth part is not where the proposal placed it.** The reasoning *for a
7162
+ requirement* is keyed twice over: `rationale` is a field of `inspectProject`, so
7163
+ it was already part one, and design §10 carries the argument under a section
7164
+ number. What `CHANGELOG.md` contributed to this bundle was **two grep hits for
7165
+ `ATX-79`, neither of them about `ATX-79`** — one a parenthetical inside
7166
+ `[0.9.2]` about a different requirement's import closure, the other a false
7167
+ positive inside the tautology entry, where `ATX-79.subpath` appears as an
7168
+ example of a detector firing wrongly. Zero useful rows and two misleading ones.
7169
+
7170
+ So what this rejects is not only the command but the **key**. A design that was
7171
+ evaluated and killed has no `ATX-n`, so an id-keyed index over the decision
7172
+ store returns the entries that were not rejected — the opposite of the content
7173
+ the original entry correctly identified as the valuable part. That half is not
7174
+ rejected and stays open above, without a key and without a proposed mechanism.
7175
+
7176
+ *Labelled for what it is.* n=1, one requirement, one change, and the person who
7177
+ assembled the bundle is the person who made the change — so "was it used" is a
7178
+ self-report, and the honest reading is that one row was load-bearing and one
7179
+ quarter was empty by construction. That is a thin basis for building something
7180
+ and an adequate one for not building it, which is the asymmetry the probe was
7181
+ specified to exploit.
7182
+
7183
+ ### A retrieval mechanism over this file's decision sections
7184
+
7185
+ Proposed inside *The decisions worth retrieving are in this file*, which stayed
7186
+ open on the grounds that **no key had been proposed, and that was the whole of
7187
+ the problem rather than a step before it**. The entry specified the inverse of
7188
+ the probe that killed the id-keyed bundle above: take an entry from this section
7189
+ that was re-proposed and caught here, and ask what somebody would have had to
7190
+ type to find it before proposing it again. One session, no build. It was run on
7191
+ 2026-08-28 and it produced a key — which already exists, in this file, and is the
7192
+ reason nothing needs building.
7193
+
7194
+ *The store is 24 entries, not 8,300 lines.* That is the measurement the entry's
7195
+ premise was missing. `Considered and rejected` holds **24 `###` entries over
7196
+ 1,086 lines**, and every plausible search term a re-proposer would reach for
7197
+ narrows to a handful: `obligations` → 2 entries, `judge` → 5, `compound` → 2,
7198
+ `countable` → 1. Grep over one section already retrieves. An index would be a
7199
+ second copy of a table of contents the headings are.
7200
+
7201
+ ***And 23 of the 24 headings name the artifact or the mechanism*** — `zod/v4-mini`,
7202
+ `isSafeChangeName`, `typescript` as a peer dependency, a freshness gate on the
7203
+ file `init` writes, grouping `render`'s output by prefix. A proposer arrives
7204
+ holding the artifact they are about to touch, which is why the heading is the
7205
+ key: it is the one field written in the vocabulary the next person will have.
7206
+
7207
+ *The exception is the finding.* The single heading that names a **problem**
7208
+ rather than an artifact — *Nothing resists a bloated requirement, and nothing
7209
+ resists a duplicated one* — is the entry that was re-proposed and caught, as the
7210
+ judgement-schema entry's blocker (2). The second documented catch, the quantifier
7211
+ detector reused as `quantifier-unjudged`, is filed under *An engine rule for a
7212
+ statement whose scenarios do not span its quantifier*, which is problem-shaped
7213
+ too. The one artifact-shaped heading anybody has been recorded checking against —
7214
+ *A committed snapshot of the params* — was found by its re-proposer unaided, and
7215
+ they said so in the entry. Four events, so this is a correlation and not a rate;
7216
+ what makes it worth recording is that it points at a rule with no mechanism
7217
+ behind it.
7218
+
7219
+ *So what replaces the proposal is a sentence about headings, not a feature.* A
7220
+ rejection is filed under the name of the thing it declined to build. That is
7221
+ already true of 23 of 24 and needs enforcing by nobody, because it is what an
7222
+ author writing a heading naturally does when the thing has a name — the
7223
+ exception is the entry that declined **three** mechanisms at once and had no
7224
+ single artifact to name, which is the case to watch rather than a rule to state.
7225
+
7226
+ *What the probe also confirmed, in passing, is the claim the entry rested on.*
7227
+ **15 of the 24 rejected entries carry no `ATX-n` at all**; the nine that do
7228
+ mention one as evidence inside an argument about something else. A design that
7229
+ was evaluated and killed has no id because nothing was built to carry one, so an
7230
+ id-keyed index retrieves precisely the entries that were *not* rejected. That
7231
+ half of the entry was right and is now measured rather than reasoned.
7232
+
7233
+ *What is not solved, and it is next door.* `Under consideration` has no headings
7234
+ at all — its twelve entries are bullets whose first bolded sentence is the only
7235
+ handle — so the key found here exists for one of the three foot sections and not
7236
+ for the section most likely to be read before a proposal. Nothing was changed
7237
+ about that here: restructuring twelve open entries is a larger edit than the
7238
+ finding justifies, and the finding is only four events old.
7239
+
7240
+ ### A per-scenario axis recording which interface a scenario crosses
7241
+
7242
+ Prompted by an external write-up (`columns.chicken-house.net`, 2025-11-10, "AI-First
7243
+ Testing Workflow"), which ran one test case against a system's REST API and
7244
+ against its web UI and found the empty-basket check implemented in the UI and
7245
+ absent from the API. Neither run failed against its own expectation; the finding
7246
+ was that two runs of the *same* case disagreed. Attest already permits the
7247
+ arrangement — a `reqId` takes any number of attesting scenarios — but cannot
7248
+ tell three scenarios covering three edges of one requirement from three
7249
+ scenarios crossing three interfaces at the *same* edge. The second set can be
7250
+ asserted to agree; the first has nothing to compare. The proposal was to put
7251
+ that fact in the model, as a per-scenario axis that would have to be a literal
7252
+ in the AST, because `cover` executes nothing.
7253
+
7254
+ Filed 2026-08-22 with four blockers, and rejected on 2026-08-28. **The blocker
7255
+ that decides it needed no measurement, and the one that was measured came back
7256
+ at the floor.**
7257
+
7258
+ **There is no verdict to emit, and that is fatal on its own.** The engine cannot
7259
+ perform the comparison: agreement between two runs is an assertion, and
7260
+ assertions belong to the test. The most any axis could report is that a
7261
+ requirement has scenarios on more than one of them — information with no pass or
7262
+ fail attached, a new output *category* rather than a new check. So the field
7263
+ would be a thing every adopter has to learn about, including all of the ones
7264
+ with a single interface, in exchange for a report that decides nothing. This
7265
+ holds whatever the frequency turns out to be, which is why it is stated first.
7266
+
7267
+ **And the frequency, counted because the entry said it was cheap to count and it
7268
+ was.** Over `self/` through `inspectProject`: 84 requirements, all covered, **69
7269
+ carrying two or more scenarios**, of which 6 have scenarios in more than one
7270
+ spec file. Over the seventeen fixture projects: 15 ids, 6 multi-scenario, **0**
7271
+ cross-file. Cross-file is only a proxy — nothing records which interface a
7272
+ scenario crosses, which is the entry's own point — so all 6 were read by hand,
7273
+ and **exactly one is the shape**: `ATX-60`, whose statement says the `--json`
7274
+ `ok` and the human verdict printed beside it are decided by one predicate
7275
+ "never by a second spelling of that question", with `json.spec.ts` and
7276
+ `report.spec.ts` asserting the two surfaces separately.
7277
+
7278
+ *How the other five fail is the useful half.* `ATX-14` and `ATX-63` pair the
7279
+ validator with the renderer, which is two **different obligations** of one
7280
+ requirement — one reports the bad placeholder, the other omits it — so there is
7281
+ nothing to compare. `ATX-15` is five edges of one ordering rule. `ATX-80` is one
7282
+ interface run against a fixture and against a change the suite really archived.
7283
+ `ATX-25` is the near-miss worth naming: `pipeline.spec.ts` checks the producer
7284
+ and `json.spec.ts` the serialised report, which is one pipeline observed at two
7285
+ **layers** — downstream-coupled, so they cannot independently disagree, and
7286
+ independence is the property differential evidence is made of.
7287
+
7288
+ *The finding that carries more than the count.* This repository's own canonical
7289
+ instance of the pattern — `tests/static-registry.spec.ts`, the two registry
7290
+ readers required to agree, cited in `CLAUDE.md` as what makes that seam real —
7291
+ is bound to **no requirement id at all**. It is in the engine suite, outside
7292
+ `self/`'s registry, and the only `requirement(` in it is a string fixture. A
7293
+ repository that runs the pattern deliberately, and says so in its own design
7294
+ vocabulary, contributes zero rows to a count of it. The rate is 1 in 69, and the
7295
+ one is weaker than the pair that prompted the entry.
7296
+
7297
+ **What survives, and it is the part that costs nothing.** The deletion test the
7298
+ entry proposed is untried and is now the cheaper half by a wide margin: if the
7299
+ workflow document `init` writes (§9) simply says that two scenarios crossing one
7300
+ requirement at different interfaces should assert the same expectations, the
7301
+ practice may happen anyway — in which case the engine would have gained nothing
7302
+ and charged everyone a field for it. That sentence needs no decision
7303
+ infrastructure and is not what this rejects. The permanent property the
7304
+ rejection leaves in place — that Attest cannot distinguish covering N edges from
7305
+ crossing N interfaces, so a fully-covered requirement says nothing about how
7306
+ many surfaces were asked — is recorded in design §11 rather than only here.
7307
+
7308
+ **What this rejection does not claim.** One corpus, and the limit `[0.9.2]`
7309
+ states about this repository applies: the two-adapter arrangements it has are
7310
+ engine-internal, so the count measures Attest's habits rather than an adopter's.
7311
+ The prompting report is from a real system nobody here can walk. What is
7312
+ corpus-independent is the first argument — no verdict to emit — and that is what
7313
+ this is rejected on. A reporter whose registry has the shape at a rate that
7314
+ makes a **descriptive** output worth having would be re-opening a different
7315
+ proposal, not this one.
7316
+
7317
+ ### Rewriting the placeholder scan in `prose` to drop its tail slice
7318
+
7319
+ Priced on 2026-08-21, during a standing sweep for more of the ATX-59 shapes, and
7320
+ rejected by the measurement that was supposed to confirm it.
7321
+
7322
+ `prose` (`core/render.ts`) walks a statement character by character, and at every
7323
+ `{` it runs `/^\{\w+\}/.exec(text.slice(i))`. Read as source that is the §2
7324
+ quadratic-scan shape exactly: one fresh copy of the remaining string per `{`, so
7325
+ a statement of n braces copies n²/2 bytes — and a statement is prose a fork
7326
+ contributor writes and `render --check` reads under the static reader, which is
7327
+ the class that has actually shipped defects here. The fix was to be a sticky
7328
+ `/\{\w+\}/y` with `lastIndex = i`, which allocates nothing.
7329
+
7330
+ **It is not quadratic, because V8 does not copy.** `String.prototype.slice` on a
7331
+ string past a small threshold returns a *sliced string* — a view onto the parent
7332
+ with an offset, made in constant time — and an anchored regex over it reads only
7333
+ the few characters it needs, so the copy the source implies never happens.
7334
+ Measured through `renderMarkdown` on the built output, three adversarial shapes
7335
+ (all braces; alternating `{a`; a brace run followed by a word run), at 100k
7336
+ through 800k characters: every one of them scaled at a ratio of **1.77–1.89 per
7337
+ doubling**, i.e. linear, and 800,000 braces rendered in **106 ms** against
7338
+ ATX-59's 1,000 ms budget. A quadratic implementation at that size would be tens
7339
+ of seconds; this is the same order as the linear cases beside it.
7340
+
7341
+ So there is no defect and no change. What the sticky rewrite would buy is a
7342
+ constant factor nobody has shown a need for, against §5's rule that
7343
+ micro-optimisation with no named input is out of scope — the input *is* named
7344
+ here, and it is already absorbed. Filed rather than dropped because the site
7345
+ reads like the defect on inspection and will be proposed again by the next sweep
7346
+ that greps for `.slice(` near a loop: the answer is that V8's representation is
7347
+ load-bearing, the measurement is the evidence, and the thing to re-run is the
7348
+ ratio rather than the reasoning. Note the limit of that claim — it rests on a V8
7349
+ representation rather than on anything the language promises, which is safe for a
7350
+ Node CLI and would not transfer to a runtime that flattens eagerly.
7351
+
6258
7352
  ### Deferring the `typescript` import so `init` and `--version` do not pay it
6259
7353
 
6260
7354
  Priced on 2026-08-13, in the session that enabled the compile cache above, and
@@ -6442,7 +7536,7 @@ releases behind a blocker stated as "the mapping is unavailable", which was
6442
7536
  true and was about a mapping nothing needed; the evidence it carried was about
6443
7537
  the right problem and the mechanism it proposed was not, and nothing rereads a
6444
7538
  blocker that has already been written down. That is the same failure the
6445
- section's own preamble records about a figure nobody re-checks, one level up:
7539
+ `How an entry is re-measured` records about a figure nobody re-checks:
6446
7540
  the *shape* of the proposal goes stale as quietly as its numbers do.
6447
7541
  *What is left of this entry is only the original question* — whether `check`
6448
7542
  and `verify` should refuse a half-loaded registry outright, the way `cover`
@@ -6800,6 +7894,19 @@ matching between statements (token overlap, embeddings). That reintroduces the
6800
7894
  fuzzy comparison design §0 exists to remove; real semantic duplication needs
6801
7895
  judgement and belongs to human review at the propose stage.
6802
7896
 
7897
+ **New evidence for "sees almost nothing", from `evidence-not-ordering`
7898
+ (2026-08-28), and it is about *why*.** `ATX-30`'s statement was reworded — same
7899
+ three obligations, one of them stated for the first time — and the detector
7900
+ moved it from `correct` to `missed` on both variants. Nothing about the
7901
+ requirement got less compound; what changed is that the second clause is now a
7902
+ participle (*"keeping a recorded failure … and replacing a recorded pass"*)
7903
+ rather than a second `MUST`. So the rule is not weakly detecting compoundness,
7904
+ it is detecting one **grammatical spelling** of it, and an author's ordinary
7905
+ choice between two ways of writing the same sentence decides the verdict. That
7906
+ is the "cleared by deleting the word that triggered it" failure arriving without
7907
+ anyone trying to clear it, which strengthens the rejection rather than
7908
+ qualifying it.
7909
+
6803
7910
  ### A freshness gate on the file `attest init` writes (`init --check`)
6804
7911
 
6805
7912
  `render --check` is the obvious precedent and is the opposite case. A rendering
@@ -7147,8 +8254,85 @@ and `grep` genuinely stop working — but the first thing to try then is the
7147
8254
  declared prefix ownership already under consideration above, which addresses,
7148
8255
  rather than a scorer, which ranks.
7149
8256
 
8257
+ ## How an entry is re-measured
8258
+
8259
+ What the three sections above have learned about themselves, kept apart from all
8260
+ three because it belongs to none of them. Every lesson below was learned on an
8261
+ entry that has since left the section it was written in — five built entries now
8262
+ recorded under `[0.8.0]`, the `check` `empty-spec` entry under `[0.9.0]`,
8263
+ `divergent-param` and the committed params snapshot down in `Considered and
8264
+ rejected`. That is the argument rather than a coincidence: the method outlives
8265
+ every entry it was learned from, and filed under `Planned` it is unreachable in
8266
+ the stretches when `Planned` is empty, which is the state it is in as this is
8267
+ written. It was there because that is where the first of it happened to be
8268
+ written, which is not a reason.
8269
+
8270
+ **An entry can sit in these sections for releases on the strength of a figure
8271
+ nobody has checked.** The discipline aims at keeping the *argument*, not at re-testing the
8272
+ evidence under it, so the first act of building one is re-measuring what it
8273
+ claims rather than implementing what it proposes. It has paid in two ways:
8274
+ once killing an entry outright, when the spans it rested on turned out not to be
8275
+ spans; and five times correcting an entry's own numbers or scope before a line
8276
+ of it was written — a call-site count that a regex had inflated, a cost filed as
8277
+ "a fixture per diagnostic" that the corpus put at one fixture, an injection whose
8278
+ payload turned out to need the target file's *own* quote character, which is the
8279
+ difference between escaping one quote and escaping the right one, a fix
8280
+ filed as a map over `issues` that the report's own shape moved to the
8281
+ serialiser, four fields and one nesting level further out, and a type-level
8282
+ refusal filed as breaking the evaluating loader, which turned out not to touch
8283
+ it and to break the one delta shape the entry was about instead. **What a release costs its adopters is not known on the day it is cut**
8284
+ — several entries arrived from one adoption report, about shapes the tool made
8285
+ someone work around without ever erroring. And **the fixes with no observable
8286
+ symptom are worth batching**: individually none justifies a commit, together they
8287
+ cost about an hour, and they are the group no gate could have found, since having
8288
+ no symptom is the definition of what a gate cannot see.
8289
+
8290
+ *The five entries `Planned` carried before it emptied* were built and are
8291
+ recorded under `[0.8.0]`. The second is where this
8292
+ discipline paid again: it went in on an entry whose headline type
8293
+ measurement — instantiations unchanged — was false on re-measure, and the
8294
+ re-measure is the only reason the release notes do not repeat it. The fourth is
8295
+ the other outcome the discipline has, and worth a line because the first three
8296
+ were all corrections: the `render --out` symlink measurement reproduced exactly
8297
+ as filed, down to the leaf being safe for the reason given. What re-measuring
8298
+ added was not a correction but a **surface the entry had not counted** —
8299
+ `--check` reads the same destination, so the same link makes a freshness gate
8300
+ date a document outside the project. Re-measuring an entry is therefore not only
8301
+ a test of the number it quotes; walking the code the claim is about is what
8302
+ shows the claim's own edges. The fifth is that lesson arriving where an entry
8303
+ had already named its own fix: the C1 measurement reproduced exactly, and the
8304
+ repair it proposed — a map over `issues` — was still the wrong half, because
8305
+ four more fields and a nested scenario name carry the same untrusted text and
8306
+ never pass the envelope. An entry that ends with a fix is the one least likely
8307
+ to have its fix re-examined, which is the case worth naming.
8308
+
8309
+ *That has now happened twice, so it is a pattern rather than an incident.* The
8310
+ extensionless-import entry likewise reproduced exactly on its reported half and
8311
+ was wrong about its own repair — it recorded that the writing half needed
8312
+ nothing, and the writing half was guarded by a condition that was correct only
8313
+ while the reading half stayed broken. Both times the error had the same shape: an
8314
+ entry reasons about the code path it can see failing, and the repair makes a
8315
+ second path reachable that nobody has looked at because nothing could reach it.
8316
+ So the re-measure that matters is not "does the reported symptom reproduce" —
8317
+ both did, exactly — but **what does the proposed fix make reachable for the first
8318
+ time**.
8319
+
8320
+ *The first time that question was asked in advance, it paid, and how it paid is
8321
+ worth a line because it is not what the two cases above would predict.* The
8322
+ `check` `empty-spec` entry — `[0.9.0]`, the third entry to reproduce exactly
8323
+ on its reported half — was not wrong about its own repair. Asking the question
8324
+ anyway found the defect one step to the side: the fix it proposed was correct,
8325
+ and the *obvious implementation of it* — make `check` agree with `verify` —
8326
+ would have copied a misdiagnosis `verify` had been emitting unnoticed, because
8327
+ the reference implementation an entry points at is not itself re-read. So the
8328
+ question generalises past the shape it was learned from. It is not only about a
8329
+ path the repair makes reachable; it is about **everything the repair would make
8330
+ `check` agree with**, and an entry that names the right fix can still be
8331
+
7150
8332
  ## Releases
7151
8333
 
8334
+ [0.11.0]: https://gitlab.com/Pseudorca/attest/-/tags/v0.11.0
8335
+ [0.10.0]: https://gitlab.com/Pseudorca/attest/-/tags/v0.10.0
7152
8336
  [0.9.4]: https://gitlab.com/Pseudorca/attest/-/tags/v0.9.4
7153
8337
  [0.9.3]: https://gitlab.com/Pseudorca/attest/-/tags/v0.9.3
7154
8338
  [0.9.2]: https://gitlab.com/Pseudorca/attest/-/tags/v0.9.2