@am_shork/attest 0.9.2 → 0.9.4

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,443 @@ 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
+ ## [0.9.4] - 2026-08-19
17
+
18
+ ### Added
19
+
20
+ - **A requirement can say a value is not decided yet, as `open` (`ATX-81`,
21
+ `ATX-82`).** The registry had two states, schema-valid or an ERROR, and there
22
+ was no third — so an author who could not determine a value had nowhere to put
23
+ that fact. What they do instead is pick one, and a scenario written against an
24
+ invented number is drift no gate can see: the single source is perfectly
25
+ consistent and describes a promise nobody made, arriving through the mechanism
26
+ meant to stop it. `open` is a free-text list of what is still undecided about a
27
+ requirement, one entry per question. The static commands report each as a
28
+ `requirement-open` WARNING and keep their exit code; the archive gate refuses
29
+ with `open-unresolved` while any remains. Promoted from *Under consideration*,
30
+ where the blockers were an unmeasured frequency and the permanent surface a
31
+ schema field costs.
32
+
33
+ *The asymmetry is the design, and it is `never-red`'s.* A proposal may be
34
+ legitimately half-finished while it is being written, and the four static
35
+ commands are where an author works during that; failing there would push them
36
+ back to inventing a value, which is the behaviour being replaced. Archiving is
37
+ where "done" acquires a hard definition, and a registry carrying an unanswered
38
+ question is not done.
39
+
40
+ *Re-measuring the entry before implementing it corrected it in four places,
41
+ per this file's own discipline.* **(1)** The proposed "WARNING from `check`,
42
+ ERROR at the gate" cannot be one diagnostic at two levels: `evaluateGate`'s
43
+ structure step already filters `validateStructure` to ERROR, so every WARNING
44
+ is discarded there. It needs its own gate step, which is what `never-red`
45
+ being the stated precedent actually implies. **(2)** The scope is the whole
46
+ merged registry, not the ids the change ADDs — and that is where it parts
47
+ company with `never-red`, whose narrower scope exists because "was ever
48
+ observed failing" is a fact about history that cannot be demanded
49
+ retroactively. An open question is a claim about the present. **(3)** The
50
+ entry over-counted the static extractor, which reads any literal property and
51
+ needed nothing, and under-counted `render`: without a section there, `SPEC.md`
52
+ — the only human review surface a requirement has — would not show that a
53
+ shipped requirement is open.
54
+
55
+ ***(4)* is the one that had to ship in the same change, and it is the
56
+ question `Planned`'s preamble says to ask.** `splice.ts` carries the comment
57
+ *"Any other field is one the schema does not define, and not this module's to
58
+ rewrite or to remove"*, and that sentence is true **only while `open` is off
59
+ the schema**. Adding the field without adding the two writer branches would
60
+ mean a change that *answers* a question is approved by the gate against an end
61
+ state the registry on disk does not match — the merge commits a file still
62
+ carrying the closed question, with nothing anywhere comparing the two. Not a
63
+ path the entry's own reasoning could reach, because it reasons about the field
64
+ being added and this is about the field being removed.
65
+
66
+ *Cost, stated rather than hidden.* One unresolved question anywhere blocks
67
+ every unrelated change from archiving. That is the intended reading of a
68
+ registry that is not done, it is recoverable by answering the question, and it
69
+ is the same all-or-nothing shape `verify` already has. `SCHEMA_VERSION` is
70
+ unchanged: two new `code` values are additive to the `--json` envelope. The
71
+ type surface **does** move — `Requirement` gains a required `open`, so code
72
+ constructing one by hand rather than through `defineRequirements` needs the
73
+ field. In this repository that was 26 sites, every one of them a test literal
74
+ that already spelled `outOfScope: []`; no adopter reading requirements is
75
+ affected, and `defineRequirements` still takes it as optional.
76
+
77
+ - **This repository ran its own `changes/` → `status` → `archive` workflow for
78
+ the first time, and the change above is what went through it.** Every one of
79
+ the 80 requirements before it was written straight into the registry, so
80
+ `archive/` did not exist and `inspectRedEvidence` — shipped in `[0.9.3]` to
81
+ read it — reported all 252 declared scenarios as `unarchived`. The export had
82
+ no consumer that was not a test of itself for the second release running, and
83
+ this time the reason was not that nothing had been built on it but that the
84
+ repository had never produced the input it reads.
85
+
86
+ **The gate's first run failed on two of the five scenarios, and it was right.**
87
+ Both were the negative half of a pair — "says nothing when the list is empty",
88
+ "lets the gate through once every question is closed" — and both passed on
89
+ their first run, because an engine that has never heard of the field satisfies
90
+ "stays silent" trivially. `never-red` named them individually. The repair is
91
+ one scenario asserting the *discrimination* rather than two asserting each
92
+ side, which fails now and still refuses an implementation that warns
93
+ unconditionally later. This is the mechanism catching the exact defect class
94
+ the tautology entry under *Under consideration* is about, on the first change
95
+ it was ever pointed at, in the author's own work.
96
+
97
+ *A second defect surfaced after `--apply` and is worth recording because
98
+ nothing catches it.* The proposed spec read its params through `delta.added`
99
+ rather than by id. A delta is *also* keyed by the ids it ADDs
100
+ (`withProposedRequirements`), which is what makes the import the only line the
101
+ merge has to change — reaching through `added` loads during stage 1 and throws
102
+ once the import is repointed at the registry. The gate cannot see it, because
103
+ the gate runs before the rename; `check` cannot, because it executes nothing.
104
+ It appeared at the next `verify` as `spec-load-failed`. The workflow document
105
+ already shows the by-id accessor; what it does not say is that the other one
106
+ works until it doesn't.
107
+
108
+ *What the archive now reports*, and it is the shape the design predicts rather
109
+ than a backlog: one archived change, 5 `fail` rows, 228 `unarchived`. The two
110
+ retired scenario names remain in `first-run.json` as `pass` and produce no
111
+ rows at all, which is `inspectRedEvidence`'s documented behaviour for a record
112
+ naming a scenario no spec declares any more — measured here for the first time
113
+ rather than reasoned about.
114
+
115
+ ### Changed
116
+
117
+ - **The publish job publishes through `npm` and requests an npm OIDC token, so
118
+ trusted publishing becomes a setting rather than a rewrite.** The provenance
119
+ work in `[0.7.2]` signs the release but does not change what authorises it: a
120
+ long-lived automation token, masked and protected, that publishes as us to
121
+ anyone holding it. This is the half of removing it that lives in the
122
+ repository. Promoted from `Planned`; what remains there is narrowed to the two
123
+ npmjs.com settings that actually retire the credential.
124
+
125
+ *The one thing the entry was promoted with as unmeasured is now measured, and
126
+ it decides the rollout.* The question was whether a failed OIDC exchange falls
127
+ back to the `_authToken` already in `.npmrc` or fails the publish outright.
128
+ It falls back. Read off npm 11.10.1's `lib/utils/oidc.js` rather than inferred:
129
+ the function documents itself as intended never to throw, every failure path
130
+ logs at verbose and returns, the whole body sits inside a `try/catch` that also
131
+ returns, and `opts[authTokenKey]` is assigned only on success. So this lands
132
+ beside the existing token with nothing registered yet, and a failed exchange is
133
+ a quiet fall back rather than a failed release — which is what makes the first
134
+ tagged release a cheap test instead of an irreversible bet.
135
+
136
+ *`pnpm publish` was never going to work, and that is now an observation rather
137
+ than a strong inference.* The entry reasoned from `pnpm/pnpm#11526` that pnpm
138
+ gets out of the way and npm does the work. The call site confirms it:
139
+ `npm/lib/commands/publish.js` invokes `lib/utils/oidc.js`, so the exchange is
140
+ inside npm's publish command and no flag passed to pnpm reaches it. Hence the
141
+ client swap, which is a cost this file has already priced once in the other
142
+ direction.
143
+
144
+ *Two details the swap turned up that the entry had not counted.* `npm publish`
145
+ has no `--no-git-checks` — it warns *"Unknown cli config"* and does no git
146
+ checks at all — so the flag is dropped rather than translated. And the job now
147
+ runs `corepack enable pnpm` rather than the bare `corepack enable` plus the
148
+ `COREPACK_ENABLE_STRICT` override the entry expected to re-add: naming the shim
149
+ leaves the image's own npm alone for `npm i -g npm@^11.5.1` to upgrade, which
150
+ is the same outcome with one fewer environment variable and no refusal that can
151
+ be switched off by accident. `--provenance` stays explicit even though npm's
152
+ OIDC path can enable it by itself, because it only does so while `provenance`
153
+ is at its default — passing it keeps the setting this job's decision rather
154
+ than a side effect of whether the exchange succeeded.
155
+
156
+ *What is not claimed.* Nothing here has run. The job is exercised once per
157
+ release and cannot be tested any other way, which is why the fallback
158
+ measurement came first and why the token and its guard both stay. `NPM_TOKEN`
159
+ is still required and still what authorises the publish until a release is
160
+ observed going out without it.
161
+
162
+
163
+ - **This repository's own judgement table now reads its scenario names through
164
+ `@am_shork/attest/inspect`, and what that measured is the point of the
165
+ change.** `intent-quality.md` names one read as the rot check every hand-judged
166
+ table needs — that a scenario a row claims is one a spec really declares — and
167
+ tells an adopter to get it from `inspectProject`. This repository's table got
168
+ it from `parseSpecs` and `findFiles` directly, because it was written while
169
+ both were internal. So the export shipped in `[0.9.2]` had **no consumer that
170
+ was not a test of itself**: `self/inspect.spec.ts` attests it, and nothing
171
+ built anything on it. Passing its own tests is not evidence that an export is
172
+ sufficient for the thing it was added for, and the only party who could have
173
+ found out otherwise was an adopter.
174
+
175
+ *Equivalence was measured rather than assumed, in both directions.*
176
+ `scanProject` sorts the same `findFiles` walk with the same `isSpecFile`
177
+ predicate, so the two reads should agree — and they do, compared as sorted
178
+ `(reqId, names)` pairs over the whole repo before the old call was deleted.
179
+ Then mutation-checked, because an assertion that still passes when the read
180
+ returns nothing is not reading anything: with the map stubbed empty the
181
+ rot-binding test fails and the other nine pass, which is the blast radius the
182
+ call actually has.
183
+
184
+ **What would not go through the export is the finding, and it is one thing.**
185
+ `Inspection.registry` is merged across registry files, so an id arrives with no
186
+ record of which file declared it. `corpus()` needs that — `source` is the scope
187
+ a cross-requirement rule sees, `divergentParam` keys on it, and the DOMAINS
188
+ completeness check uses it to hold the fixture registries out — and nothing in
189
+ the public export can reconstruct it. That half still deep-imports
190
+ `readRegistrySource`, with the reason written at the call site rather than left
191
+ as an unexplained inconsistency. It is filed as a measurement and **not** as a
192
+ decision to add a field: one caller wanting per-file attribution is one
193
+ adapter, and the entry under *Under consideration* about an outward-facing
194
+ judgement schema is waiting on exactly the second one.
195
+
196
+ *Cost.* `inspectProject` reads the registry as well, which this caller does not
197
+ need — one extra static read on a tree `corpus()` already walks once per call,
198
+ seven times per run. Paid deliberately: the alternative is deep-importing the
199
+ function whose reachability from outside the package is the thing being
200
+ demonstrated.
201
+
202
+ ### Documentation
203
+
204
+ - **Design §11 now states that evidence outranks testimony, and that a
205
+ disagreement between them is a finding.** The registry and the gate's verdict
206
+ are evidence — machine-checked, recomputable, true of the tree as it stands. An
207
+ archived `proposal.md` is testimony: prose about a decision taken on one past
208
+ date that nothing has re-read since. A reader holding both holds claims in two
209
+ tenses with no stated order, and the order matters most exactly when they
210
+ conflict, because a stale proposal is more fluent than a registry and reads as
211
+ the more authoritative of the two. Decided from *Under consideration*, where it
212
+ had sat behind a blocker that was circular.
213
+
214
+ *The blocker was circular and naming the circle is what dissolved it.* The rule
215
+ was held back for describing a workflow the tool does not have — nothing read
216
+ the prose half — and the prose half was read by nothing **because no rule
217
+ existed for reading it**. Neither side moves on its own. The way out is the
218
+ split `[0.9.3]` already made for the propose-stage questions, where two of five
219
+ went to `CLAUDE.md` rather than into the document `init` writes: **this lands in
220
+ §11 only, and deliberately not in the instruction document.** §11 is a permanent
221
+ property of the design and its cost is paid once, here; the §10 document's cost
222
+ is paid in every adopting project's agent context forever, and that cost is what
223
+ the blocker was actually about. It is also the statement any later retrieval
224
+ surface would have to be built against, so writing it first is cheaper than
225
+ writing it after something depends on it.
226
+
227
+ *Re-measuring it before writing found the entry's own subject had stopped being
228
+ hypothetical, and falsified something this file recorded.* `[0.9.3]` states that
229
+ `proposal.md` in this repository "carries a link" — `CLAUDE.md` makes
230
+ `CHANGELOG.md` the single decision record, so a proposal restating an argument
231
+ would be a second store of it with nothing comparing the two — and concludes
232
+ there is nothing in it for an index to offer but its path. The very next change
233
+ to archive carries a proposal of **sixty-nine lines across five sections**, most
234
+ of its argument also present in the changelog entry. So the second store arrived
235
+ by ordinary means, in the first change that could produce one, written by an
236
+ author who knew the rule. The archived file is left as it is rather than trimmed
237
+ to a pointer: it is what the gate approved, and rewriting an archive to agree
238
+ with a claim about it is the failure this section is about, performed on itself.
239
+ §11 cites it as the measurement instead.
240
+
241
+ *What is deliberately not claimed.* The typed half cannot produce this conflict
242
+ — `first-run.json` is an `Outcome` in a schema-validated envelope, and a
243
+ scenario either is in the record or is not, so `inspectRedEvidence` reports an
244
+ absence rather than a contradiction. Nothing enforces the rule and nothing can:
245
+ reconciling prose against a registry is the ground truth §0 refuses to compute.
246
+ What §11 states is only the direction to reconcile in.
247
+
248
+ ## [0.9.3] - 2026-08-18
249
+
250
+ ### Added
251
+
252
+ - **The archive is read back, as `inspectRedEvidence` on `@am_shork/attest/inspect`
253
+ (`ATX-80`).** `archive --apply` moves each merged change into
254
+ `archive/<date>-<name>/`, carrying `proposal.md`, the delta and the record of
255
+ how every one of its scenarios ended the *first* time it ran — and nothing
256
+ read any of it. The only store here that keeps a decision with its evidence
257
+ attached was write-only. It now answers, for each scenario a project declares,
258
+ which of `fail` / `pass` / `unobserved` / `unarchived` the archived records
259
+ place it in. Promoted from `Planned`, where the blockers were an archive
260
+ nobody had produced and an entry point nobody had chosen.
261
+
262
+ *The question the entry was promoted with was the wrong granularity, and
263
+ finding that is most of what this change is.* It asked which **requirements**
264
+ ever had a scenario observed failing. Under the gate as written that is true
265
+ by construction for every archived id — `neverRedIssues` walks *every*
266
+ scenario of every ADDED id and blocks on any one without a recorded red, so a
267
+ change that archived at all archived with the whole set proven. Where the gate
268
+ genuinely stops is one level down, and it leaks two ways: a scenario grown
269
+ onto a requirement after it archived can never acquire evidence, because
270
+ `never-red` fires only on ids a change is currently ADDing and an archived id
271
+ is ADDed by nothing again; and MODIFIED carries no such obligation at all, so
272
+ a requirement whose statement is later rewritten keeps evidence only for the
273
+ form it had when it archived. Both accumulate silently. The report is
274
+ therefore over `(reqId, file, name)`, which the record already keys by.
275
+
276
+ *Four states rather than two, and the two absences are the whole design.*
277
+ `unarchived` — the id is in no archived change at all — is a requirement
278
+ written straight into the registry, which is how existing behaviour is
279
+ described in a brownfield adoption and which design §6 gives no obligation to
280
+ have been red. `unobserved` — the id **is** archived and this scenario is not
281
+ in the record — is the drift above. Folding them together would report a
282
+ design working exactly as intended as though it were a backlog, and on a
283
+ first-day adoption that is every row. `pass` stays separate from both because
284
+ the gate cannot produce it, so an archived one means a hand-edited record or a
285
+ format that moved.
286
+
287
+ *Two things it deliberately does not do.* It does not judge: whether an
288
+ `unobserved` scenario matters is a judgement about that scenario, which is the
289
+ ground truth §0 refuses to compute for someone else. And it does not hand back
290
+ the records — the archive read returns rows, because the record is a nested map
291
+ keyed by ids, paths and scenario names read off disk, and exporting the
292
+ container would export the prototype hazard `red-record.ts` exists to hold.
293
+
294
+ *`proposal.md` is not read, and that is the blocker dissolving rather than
295
+ being cleared.* The prose half was filed as the one carrying the reasoning.
296
+ In this repository it carries a link: `CLAUDE.md` makes `CHANGELOG.md` the
297
+ single decision record, so a proposal restating an argument would be a second
298
+ store of it with nothing comparing the two. There is nothing in it for an
299
+ index to offer but its path.
300
+
301
+ *The three scan exclusions are untouched.* `locate.ts`, `runner.ts` and
302
+ `pipeline.ts` each still exclude `archive/`, and they are §7 and §8 — an
303
+ archived change's requirements and specs must not re-enter a normal run. The
304
+ new read walks the archive for facts *about* archived changes, never to fold
305
+ their contents back into a registry or a plan.
306
+
307
+ *Blocker **(2)** is settled against the fifth static command, on the roster's
308
+ own terms.* `ATX-16`'s subject is reading TypeScript *without evaluating it* —
309
+ its rationale is about a top-level `fetch` in a registry file reaching CI
310
+ secrets on a fork merge request. `first-run.json` is JSON; joining that roster
311
+ would quietly turn a security promise into a list of commands that do not run
312
+ the suite. The trial datum that seemed to point at a command measured demand
313
+ to read the archive **as prose**, which is a different question. And the
314
+ direction is reversible: adding a command later is additive, narrowing a
315
+ requirement's roster back is not. Two things measured while deciding —
316
+ `archive.ts`'s runtime closure names no `vite`/`vitest`, so `ATX-79`'s
317
+ peer-free promise holds over the new reachability; and `intent-quality.md`
318
+ already exists for findings of exactly this kind, where `cli-reference.md`
319
+ has no place to state one.
320
+
321
+ *What it cost elsewhere, because it is the part that does not announce
322
+ itself.* The fixture that exercises all four states adds a registry, and the
323
+ judgement tables in `tests/intent-rule-candidates.spec.ts` score the fixtures
324
+ too — so the corpus moved 87 → 90 and four measurements were re-read rather
325
+ than re-baselined. `ATX-80` is judged compound (2), and `compound-requirement`
326
+ flags it correctly; its quantifier ranges over values, the shape that table
327
+ prefers; `ATX-80.states` is an iterated domain and carries its pin. One new
328
+ divergence needed judging — `states:ATX-32,ATX-80` — and it is worth a line
329
+ because of how it arrived rather than how it reads: two reports each naming
330
+ their own closed set of labels, neither author aware of the other, which is a
331
+ second generic param name starting the climb `code` made to nineteen members.
332
+ The count of judgements a shippable `divergent-param` would demand grows with
333
+ the registry, from nothing anyone did wrong.
334
+
335
+ *Mutation-checked, and one assertion did not survive it.* Every new test was
336
+ run against a deliberately broken implementation. The one pinning archived
337
+ folder order passed with the sort deleted, because `readdir` returns names
338
+ already ordered on Windows — the fixture-with-no-separator shape from the
339
+ `Fixed` entry below, arriving in a new test on the day it was written. Fixed
340
+ by choosing names where the two orders differ, measured rather than assumed,
341
+ with the casing called out in the test so it is not tidied away.
342
+
343
+ *One more scenario, added before cutting a release, and writing it corrected
344
+ the reason it was added.* Every state above was read out of a hand-written
345
+ `first-run.json` at a folder this repository spelled, so the reader had never
346
+ met an archive the tool itself produced. `self/merge.spec.ts` now runs
347
+ `--apply` for real and joins the result: the folder is the one the run's own
348
+ date stamp named, and the report's paths are derived from whatever it picked.
349
+ Mutation-checked to be worth its runtime — an enumeration that finds nothing,
350
+ and the two absences folded into one, each fail it and no other scenario in
351
+ that file.
352
+
353
+ **The risk it was written to close turned out not to exist, which is the more
354
+ useful half.** The reason given for it was the proposed → merged rename: the
355
+ record is written while the spec is `*.proposed.spec.ts` and `--apply` renames
356
+ that file, so `fileKey` is what keeps the two meeting. Reading the code to
357
+ write the assertion showed the archive reader cannot exercise that at all.
358
+ `recordedOutcome` normalises the **ref**, and the record is keyed in merged
359
+ spelling from the start — so by the time a change is archived, every ref this
360
+ reader passes is already post-merge and `fileKey` is the identity on all of
361
+ them. The translation exists for the gate reading a record mid-flight, and the
362
+ archive reader is downstream of the point where it can matter. The scenario
363
+ stays, for what it does cover; the claim it was justified with is written out.
364
+
365
+ *A hazard found by running the mutations, worth recording because nothing
366
+ reports it.* Stubbing `mergedSpecPath` and running the suite **rewrote two
367
+ committed fixtures** — `fixtures/e2e-archive` and `fixtures/stdout-pollution`
368
+ each hold a `first-run.json` that a real archive run rewrites in place, so a
369
+ mutation run leaves the working tree dirty with plausible-looking changes to
370
+ files nobody edited. Caught by `git status`, not by any gate. It also settles
371
+ the write half by accident: those records really are produced through
372
+ `fileKey`, because under the stub they came out keyed by the proposed
373
+ spelling.
374
+
375
+ - **The propose stage now asks three questions, where it previously said how to
376
+ write a delta and nothing about what to check you had thought about.** They sit
377
+ at the end of Stage 1, immediately before the instruction to present and stop:
378
+ what pins each expectation, does each statement carry one obligation, and was
379
+ any of this ever red. Promoted from `Planned`, where the blocker was the
380
+ selection rather than the idea — spec-kit's `clarify` carries nine generic
381
+ categories, and copying them wholesale was the specific thing to avoid, because
382
+ §10's economics put every line of that document into every adopting project's
383
+ agent context forever.
384
+
385
+ *The selection is by what this repository has actually been bitten by*, which
386
+ is a different list from a product-spec taxonomy. **(1)** is the measured one:
387
+ the mine-density band moved 3.3× with the whole suite green because the
388
+ assertion recomputed its expectation from the param the code under test
389
+ consumed — the entry still under `Under consideration`, whose *guidance* half
390
+ shipped in `[0.5.0]` as a statement in *The model*. What is new here is asking
391
+ it as a question at the moment the assertion is being authored, which is a
392
+ different mechanism from stating the rule 80 lines earlier in the same file.
393
+ **(2)** is measured too: 34 of this repository's requirements carry more than
394
+ one obligation, so the question is doing work no shipped diagnostic does — and
395
+ deliberately names no code, because none exists to name. **(3)** answers what
396
+ the archive gate structurally cannot ask: `never-red` demands a recorded
397
+ failing run only for the ids a change ADDs, so a requirement written for
398
+ behaviour already shipped never had a moment when it should have failed.
399
+
400
+ *Two more went to `CLAUDE.md` instead*, and that split is the finding the entry
401
+ did not anticipate — the checklist wanted two destinations, not one. Crossing a
402
+ platform boundary, and whether a change leaves `check`, `cover`, `render` and
403
+ `status` executing nothing, are about this engine's internals; shipping them
404
+ would have been the dead weight the cost argument was worried about without
405
+ being able to name.
406
+
407
+ *Re-measured before building, per the discipline that section runs on, and the
408
+ claim held exactly*: the document was 405 lines with Stage 1 at 87–255, still
409
+ exactly 169. **The cost claim did not hold, and the entry's own number was
410
+ wrong.** It priced this at "three lines rather than nine" by counting one line
411
+ per category, which is what nine generic categories would have cost as a bare
412
+ list. Three questions with the clause each needs to be answerable cost **eight**
413
+ lines: Stage 1 is now 177 and the document 413. Still under half of what the
414
+ nine would have been, so the argument survives its own number being off, but the
415
+ number is corrected here rather than quietly restated.
416
+
417
+ *And the question the entry left open is answered: no new `ATX-n`.* The
418
+ document's content is gated only by `ATX-57`, which asks that every diagnostic
419
+ code it names be one the engine can emit — and asking *what this makes
420
+ reachable for the first time* is what that check turned into here: the only
421
+ hyphenated code-voice token the three questions add is `never-red`, which the
422
+ document already named. What they earn instead is **one scenario on `ATX-27`**,
423
+ which already obliges the body to carry the workflow rather than a pointer to
424
+ it. A requirement of their own would make every rewording of three sentences a
425
+ registry change; no assertion at all would leave a checklist with no diagnostic
426
+ behind it able to be dropped in silence — the same decay shape as the fixture
427
+ assertion fixed below, which is what decided it.
428
+
429
+ ### Fixed
430
+
431
+ - **The assertion guarding the one fixture that exercises a path separator
432
+ passed on a corpus with no separator in it.** `CLAUDE.md` says not to flatten
433
+ `fixtures/nested-spec/`, because its spec is the only one that sits below a
434
+ root and a relative spec path becomes the child run's `include` glob, where a
435
+ backslash is an escape character. What stood behind that instruction was
436
+ `tests/paths.spec.ts` scanning the fixture and asserting that no relative path
437
+ contains a backslash — and a flat filename contains no backslash either, so
438
+ flattening the fixture would have left the test green while removing the only
439
+ thing it was there to exercise. The rule with a written warning against silent
440
+ decay was guarded by an assertion that decayed silently.
441
+
442
+ Now a second assertion requires the scan to find a spec below the root, which
443
+ is the property the fixture exists for. Checked against a flattened copy
444
+ before being written: the predicate is true on the fixture as committed and
445
+ false on the same files laid flat.
446
+
447
+ *Found by the `CLAUDE.md` audit filed under `Considered and rejected`*, whose
448
+ answer to "which of these rules should become requirement ids" was none — this
449
+ is the one actionable thing it turned up, and it is a plain test rather than an
450
+ `ATX-n` because it is a property of this repository's fixture corpus and not a
451
+ promise Attest makes to anyone.
452
+
16
453
  ## [0.9.2] - 2026-08-17
17
454
 
18
455
  ### Added
@@ -4989,7 +5426,7 @@ time**.
4989
5426
 
4990
5427
  *The first time that question was asked in advance, it paid, and how it paid is
4991
5428
  worth a line because it is not what the two cases above would predict.* The
4992
- `check` `empty-spec` entry — `[Unreleased]`, the third entry to reproduce exactly
5429
+ `check` `empty-spec` entry — `[0.9.0]`, the third entry to reproduce exactly
4993
5430
  on its reported half — was not wrong about its own repair. Asking the question
4994
5431
  anyway found the defect one step to the side: the fix it proposed was correct,
4995
5432
  and the *obvious implementation of it* — make `check` agree with `verify` —
@@ -5000,82 +5437,32 @@ path the repair makes reachable; it is about **everything the repair would make
5000
5437
  `check` agree with**, and an entry that names the right fix can still be
5001
5438
  implemented wrongly by copying the half nobody has audited.
5002
5439
 
5003
- - **Trusted publishing, which would delete `NPM_TOKEN` rather than add to it.**
5004
- The provenance work in `[0.7.2]` signs the release but does not change
5005
- what authorises it: a long-lived automation token, masked and protected, that
5006
- publishes as us to anyone holding it. npm's trusted publishing exchanges the
5007
- same OIDC identity the attestation already uses for a short-lived credential,
5008
- which would remove the stored secret entirely and grant provenance as a
5009
- property of the flow rather than a flag on the command. The deletion test
5010
- points the right way for once: what it removes is the only long-lived
5011
- credential this repository has.
5012
- **The blocker this entry was filed with is retired, and it was the wrong
5013
- question** checked against npm's documentation on 2026-08-08. It asked
5014
- whether npm's trusted publishers accept a **gitlab.com-hosted** project on the
5015
- plan this package publishes under, and both halves come back clean: GitLab
5016
- CI/CD is a first-class provider alongside GitHub Actions, and no plan
5017
- requirement is stated anywhere in the documentation. The four fields it wants
5018
- are `Pseudorca`, `attest`, the top-level CI file path (`.gitlab-ci.yml`, and it
5019
- must carry the extension) and the allowed actions; environment name is optional
5020
- and this project uses no GitLab environments. Shared runners are the only ones
5021
- supported and the publish job carries no `tags:`, so it is already on them. The
5022
- `repository.url` cross-check needs no separate verification provenance
5023
- already passes it with the same identity.
5024
- Two things the entry did not anticipate, both of which change the shape of the
5025
- change rather than blocking it. **`SIGSTORE_ID_TOKEN` is not replaced**: the
5026
- documented GitLab snippet declares `NPM_ID_TOKEN` with
5027
- `aud: "npm:registry.npmjs.org"` *beside* it, so this is an addition to the
5028
- `id_tokens` block, not a swap. And **the YAML is not what deletes the
5029
- credential** — enabling a trusted publisher leaves the automation token just as
5030
- able to publish; what retires it is `Require two-factor authentication and
5031
- disallow tokens` on the package, which npm states does not affect trusted
5032
- publishers. That setting is the one that closes the loop this job's own comment
5033
- opens, where an automation token is used *because* a granular token fails
5034
- against 2FA on writes: trusted publishing does not bypass the prompt, it stops
5035
- being a token.
5036
- *What blocks the decision now is `pnpm`, and it is not a version to wait for.*
5037
- **pnpm does not implement the OIDC exchange at all.** `pnpm/pnpm#11526` — the
5038
- fix for the pnpm 11 regression that made this look like a supported path —
5039
- shows what its "OIDC support" is: `actions/setup-node` writes
5040
- `_authToken=${NODE_AUTH_TOKEN}` into `.npmrc`, pnpm used to pass the unresolved
5041
- placeholder through verbatim, and the fix treats it as empty so that **npm's**
5042
- credential exchange can be the sole auth source. pnpm gets out of the way; npm
5043
- does the work. That matches npm's own wording, which requires publishing
5044
- through npm directly or indirectly, and it matches the reports: every account
5045
- of pnpm publishing under OIDC is GitHub Actions, and there is **no record of
5046
- pnpm + GitLab + OIDC succeeding**. Stated at the strength the evidence carries:
5047
- this is a strong negative inference, not a documented refusal.
5048
- So the real cost is a tooling swap — `pnpm publish` back to `npm publish` in
5049
- this one job — and it is a swap this file has already priced once in the other
5050
- direction. The comment above records that moving to pnpm took
5051
- `COREPACK_ENABLE_STRICT` with it, that override having existed only so a bare
5052
- `npm` could run under a `packageManager` pinning pnpm; going back re-adds it.
5053
- It also needs npm ≥ 11.5.1 where `node:22-slim` ships 10.x, so the job gains an
5054
- npm upgrade step. None of that is prohibitive. It is simply a different
5055
- decision from the one this entry was filed as, which is why it stays here.
5056
- *One correction to the entry's own risk framing.* "It cannot be tested except
5057
- by publishing" is half wrong: registering a trusted publisher on npmjs.com is
5058
- free and reversible, and `NPM_TOKEN` can stay in place while it is tried. What
5059
- decides whether this can be rolled out incrementally is whether a failed OIDC
5060
- exchange falls back to the `_authToken` already in `.npmrc` or fails the
5061
- publish outright — unmeasured, and the thing to establish before touching the
5062
- job.
5063
- *Method note, because it is the reusable part.* The pinned pnpm bundle was the
5064
- obvious place to look and was the wrong one — `pnpm publish --help` on 10.28.0
5065
- omits the flags it accepts, exactly as this job's comment already records for
5066
- `--provenance`, so neither the help text nor the shipped bundle would have
5067
- answered it. The answer was in the upstream pull request.
5068
- **Promoted from `Under consideration` on 2026-08-08**, and what moved it is
5069
- that the question it was filed as is answered. It asked whether npm's trusted
5070
- publishers would accept this project at all; they will. What is left is a
5071
- known tooling swap with a priced cost, which is a decision to build rather
5072
- than a question — the preamble's own line, *a decision with an argument
5073
- attached*. One thing stays unmeasured and is the first act of building it, per
5074
- the discipline three paragraphs up: whether a failed OIDC exchange falls back
5075
- to the `_authToken` already in `.npmrc` or fails the publish outright. That
5076
- decides whether this rolls out incrementally beside the existing token or in
5077
- one cut, and it is cheap to establish because registering a trusted publisher
5078
- is free and reversible.
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.**
5079
5466
 
5080
5467
  ## Under consideration
5081
5468
 
@@ -5134,6 +5521,10 @@ have to gather again.
5134
5521
  **changing the value of an existing param goes through no gate at all.**
5135
5522
  `archive` gates a change; a param edit is not a change. That is a cleaner
5136
5523
  statement of the gap than "params can be tautological".
5524
+ **That observation is now its own entry at the foot of this section**, split
5525
+ out and measured on 2026-08-18, because it asks what happens when a value
5526
+ moves rather than whether an assertion is a real comparison, and neither
5527
+ answer settles the other. What stays here is the detector.
5137
5528
  *What blocks the decision — three things, all of them real.* **(1)** Every
5138
5529
  mutation above is synthetic. Across 8 commits touching `*.reqs.ts` in that
5139
5530
  repo, **no param value was ever modified** — every `params:` line in every diff
@@ -5279,7 +5670,7 @@ have to gather again.
5279
5670
  pass and separating them is the finding.
5280
5671
 
5281
5672
  - **Whether a requirement can ever be retired, and what it costs that none can.**
5282
- The registry is at **69 requirements and 192 scenarios, 2.78 each**, and
5673
+ The registry is at **79 requirements and 221 scenarios, 2.80 each**, and
5283
5674
  every one is a permanent obligation: `uncovered-requirement` is an ERROR, so a
5284
5675
  requirement without a scenario cannot exist, and nothing ever removes one.
5285
5676
  `delta.removed` exists and `applyDelta` applies it, but `--apply` refuses to
@@ -5324,6 +5715,19 @@ have to gather again.
5324
5715
  the ratio: 2.56 → 2.78. The cost of a requirement is not flat and it is not
5325
5716
  falling, which is the premise the whole entry rests on, and it is the first
5326
5717
  time this file has had two measurements of it to compare.
5718
+ *A third measurement, on 2026-08-18, and it points the other way from the
5719
+ second.* **79 requirements and 221 scenarios**, counted the way `verify self`
5720
+ prints them, which is the control the 2026-08-08 restatement established. The
5721
+ ratio runs 2.56 → 2.78 → **2.80**, so the step that carried the argument was
5722
+ the first one and the second is **+0.015**, which is flat. The premise this
5723
+ entry rests on now has one measurement for it and one against, and the honest
5724
+ statement is the narrower one: per-requirement cost has held at about 2.8
5725
+ scenarios across ten days and ten requirements. What is unchanged is the
5726
+ absolute total, which only rises — that half of the argument is untouched, and
5727
+ it was always the half about permanence rather than about acceleration. What is
5728
+ no longer supported is the acceleration. **Reopening on cost needs the ratio to
5729
+ move again**; reopening on the three meanings of "retire" needs none of these
5730
+ figures, and that is still the part nobody has separated.
5327
5731
 
5328
5732
  - **Running `verify` over a subset of the scenarios, and what a partial run may
5329
5733
  be allowed to say.** `verify` is all-or-nothing and `check` executes nothing,
@@ -5548,6 +5952,298 @@ have to gather again.
5548
5952
  design it. If it comes back the same shape, the seam is real and the schema
5549
5953
  follows; if it comes back different, `inspect` was the whole answer and the
5550
5954
  schema never needs to exist.
5955
+ **Nothing currently makes that table arrive, which is a separate problem from
5956
+ the three blockers and the one that gates all of them.** Raised in a product
5957
+ review of `[0.9.1]`–`[0.9.3]` on 2026-08-19. The deciding evidence is an
5958
+ artifact only an adopter can produce, and the path to producing it is: a row in
5959
+ the README's document table, then a 214-line page, then a hand pass over four
5960
+ questions, then writing the two mechanical properties from a prose
5961
+ description, then choosing to report back. Five voluntary steps with no
5962
+ scaffold at any of them, against a decision this entry has been waiting on
5963
+ since `[0.9.2]`. An entry blocked on evidence that no shipped surface asks for
5964
+ is blocked indefinitely, and the wait is not itself evidence of anything.
5965
+ *The proposal is scaffolding the mechanism while still withholding the answer,*
5966
+ which is the same split `[0.9.2]` already made and not a retreat from it: an
5967
+ `examples/` directory holding the completeness and rot-binding assertions with
5968
+ **no columns** — the enumeration that forces a row, and the check that a named
5969
+ scenario is one `inspectProject` really declares — and the judgement columns
5970
+ left for the adopter to add. What the page withholds is this repository's four
5971
+ columns, and that stays withheld; what it currently also withholds is the two
5972
+ properties it names as *mechanical*, and those are the half where a copy is not
5973
+ an echo, because their content is fixed and the fourth finding does not apply
5974
+ to them.
5975
+ *What blocks it.* Whether a skeleton with no columns is still a shape that
5976
+ comes back as a copy — the page's argument is that a template makes the answer
5977
+ return its own question, and it is untested whether an adopter reads an empty
5978
+ frame as an invitation or as a form to fill in. And it is a second artifact
5979
+ claiming to be runnable consumer usage of the public API, which `pre-commit-check`
5980
+ §6 already records as the category no gate reaches: the page's three samples
5981
+ are unasserted today, and a fourth in a directory that looks executable would
5982
+ be worse than a fenced block, not better, unless something runs it.
5983
+
5984
+ - **Prefix ownership is inferred rather than declared, and the cost arrives only
5985
+ when adoption succeeds.** Design §11 records that the id grammar has no level
5986
+ above the requirement: the prefix is a convention, nothing allocates it, and
5987
+ `duplicate-prefix` can only report a collision that already happened.
5988
+ spec-kit's answer is a roadmap table — a stable id, an intent, a scope
5989
+ boundary, dependencies — authored above the specs it decomposes into. That
5990
+ comparison is stated here rather than referred to, because the entry that
5991
+ introduced the spec-kit reading has since been built and now sits under
5992
+ `[0.9.4]` as the `open` field: an entry whose evidence rests on a
5993
+ neighbour is one a promotion silently empties.
5994
+ *Two things have changed since that reading, and they point opposite ways.*
5995
+ The proposal is **smaller** than it looked: the mechanism partly exists.
5996
+ `loadRegistry` returns `prefixOwners` — first claim in sorted file order —
5997
+ `merge.ts` uses it to decide which file an ADDED id belongs in, and
5998
+ `apply-no-prefix-owner` already refuses a prefix nobody owns. So what is
5999
+ proposed is declaration replacing inference, not a new layer. And it is
6000
+ **weaker**: `/inspect` shipped in `[0.9.2]`, so the cross-repository half —
6001
+ the one §11 says no command can see across — is now something an adopter
6002
+ computes from two registries in about ten lines. Attest does not have to own
6003
+ it to make it possible.
6004
+ *What blocks the decision:* nobody has the problem. §11 states the cost
6005
+ arrives "exactly when adoption succeeds: two teams sharing a registry have
6006
+ someone owning prefix allocation by hand", and no such adoption has been
6007
+ reported. Building the declared form now would be deciding prefix semantics
6008
+ for everyone from one repository whose five prefixes — `ATX` and the four
6009
+ fixture ones — are each declared by exactly one file, so the allocation
6010
+ question this would answer has never been asked here.
6011
+
6012
+
6013
+ - **A merged requirement carries no trace of the change that produced it, and
6014
+ the rename chain is the part actually lost.** `RequirementSchema` is
6015
+ `statement`, `rationale`, `params`, `outOfScope`; the splice writes those and
6016
+ nothing else, so after `--apply` no field and no sidecar records which change
6017
+ introduced an id, which modified it, or that one id used to be another. The
6018
+ delta that knew is in `archive/`, which nothing reads. Proposed from
6019
+ OpenViking's `.relations.json`: links between memories live in a
6020
+ directory-level sidecar, deliberately kept out of the content, and are
6021
+ rendered on demand at retrieval time (`docs/design/memory-link-design.md`).
6022
+ *The argument for the sidecar over a field is in their competitor analysis
6023
+ rather than their design.* The same document reads GBrain, which writes links
6024
+ into the page body, and records what that costs: rename a page and every
6025
+ inbound link still points at the old slug, with fuzzy matching at the read end
6026
+ as the only mitigation. A `supersedes:` or `introducedBy:` field on a
6027
+ requirement would be that mistake in this repository's terms — the registry is
6028
+ a literal and a single source (§2), and a field naming a folder that a later
6029
+ rename can move is a second source with nothing comparing it against the
6030
+ first. A derived index has no such copy to go stale: it is recomputed from the
6031
+ archived deltas every time it is read.
6032
+ *What blocks the decision, restated on 2026-08-19 because half of what was
6033
+ filed here is no longer true.* This entry was blocked on the empty archive and
6034
+ on the corpus being rename-free, and only the second still holds. `archive/`
6035
+ now has a real change in it — the `open` field, put through the workflow the
6036
+ same day — so the store this would index exists. Its delta carries `added` and
6037
+ nothing else, measured rather than assumed, so a backlink index is still
6038
+ testable only against fixtures for the operation it is actually about. Note
6039
+ what that says about the odds of the blocker clearing on its own: `--apply`
6040
+ refuses RENAMED outright, so no change this repository archives can ever
6041
+ exercise a rename until that refusal is lifted, and lifting it is a separate
6042
+ decision with its own reason (`splice.ts` — deleting an entry has no smaller
6043
+ span to fall back to). The blocker is therefore not "waiting for a rename to
6044
+ happen"; it is downstream of a refusal nobody has proposed removing. And one
6045
+ question the OpenViking reading does not answer, which is whether the chain is
6046
+ wanted at all. §7 applies
6047
+ `RENAMED` to a `Map<id, Requirement>` and the old id is then simply gone —
6048
+ deliberately, as far as anything written says. The case for recovering it is an
6049
+ external reference that outlives the rename, a ticket or a commit message
6050
+ naming a retired id, and nobody has reported one.
6051
+
6052
+ - **Changing the value of an existing param passes no gate, and for seven of
6053
+ them there is no record either.** Split out of the tautology entry at the
6054
+ head of this section on 2026-08-18, where it was one sentence inside the
6055
+ reframing paragraph. It is a different question: that entry asks whether an
6056
+ assertion is a real comparison, this one asks what happens when the number
6057
+ itself moves, and neither answer settles the other. `archive` gates a
6058
+ **change** — a delta, proposed specs, a recorded failing run — and a param edit
6059
+ is none of those. It is one literal in a merged `*.reqs.ts`, and every gate
6060
+ reads it as fine: `check` finds it schema-valid, because a param is any JSON
6061
+ value; `possible-drift` is quiet by construction, since `ATX-8` makes it ask
6062
+ whether *any* covering scenario reads the params at all rather than whether a
6063
+ value moved; and `verify` stays green wherever the assertion derives its
6064
+ expectation from the same param, which is the pair of green rows measured in
6065
+ the entry this was split from.
6066
+ *What is left is `render`, and it was measured here rather than assumed.*
6067
+ Across **89 params on 59 of this repository's 79 requirements, 82 — 92.1% —
6068
+ are interpolated into their own statement**, so editing one changes
6069
+ `self/requirements/SPEC.md` and `render --check` fails until the document is
6070
+ regenerated. That is a review surface and not a gate: the new value lands in a
6071
+ committed generated file where a reader can see it, and nothing anywhere asks
6072
+ whether the move was intended.
6073
+ *The other seven leave no trace outside the line that was edited, and which
6074
+ seven they are is the finding rather than the count.* Five are **structurally
6075
+ uninterpolatable**: `ATX-7.payloadKinds` and `ATX-63.payloadKinds` are
6076
+ kind → payload tables, and `ATX-15.sortMethods`, `ATX-15.bannedApis` and
6077
+ `ATX-72.quotes` are lists. A statement cannot carry any of them —
6078
+ `non-scalar-interpolation` is the diagnostic that refuses it — so the params
6079
+ with no record are disproportionately the shape §2 widened the params rule to
6080
+ cover **precisely because lists and tables are the most drift-prone constants
6081
+ there are**. The remaining two, `ATX-15.comparator` and `ATX-26.code`, are
6082
+ scalars their author simply did not interpolate.
6083
+ *What this is not.* It is not a re-proposal of the committed params snapshot
6084
+ rejected below. That was rejected on the argument that the mechanism guarding a
6085
+ param is a scenario reading it rather than a file recording it, and nothing
6086
+ here disputes that or needs a second copy of the values. The observation is
6087
+ narrower: the **edit** is unremarkable to every gate, which is a different
6088
+ claim from the values being unguarded.
6089
+ *What blocks the decision — and the first is the one that blocks the entry this
6090
+ came from.* **(1)** Frequency is unmeasured, and the one corpus anybody has
6091
+ looked at points at zero: across 8 commits touching `*.reqs.ts` in the
6092
+ reporting repository, no param value was ever modified — every `params:` line
6093
+ in every diff is an addition. A defect nobody has committed is a hazard rather
6094
+ than a report. **(2)** No shape is obvious. A param edit is not a change, so
6095
+ `archive` is the wrong place; making `check` notice one needs a previous value
6096
+ to compare against, which is the rejected snapshot wearing a different hat. The
6097
+ only mechanism needing no stored copy is review, and review already sees the
6098
+ `SPEC.md` diff for 82 of the 89. **(3)** What is left after that is seven
6099
+ params in one repository, and the right answer for the five that matter may be
6100
+ nothing more than a line in `intent-quality.md` — the four things no gate is
6101
+ looking at already being that page's subject, and this being a fifth of the
6102
+ same kind.
6103
+
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
+ - **Nothing is frozen, and the evidence this repository is waiting for requires
6226
+ somebody to build on something.** Filed 2026-08-19 from a product review of
6227
+ `[0.9.1]`–`[0.9.3]`. Three releases in five days each added public surface, and
6228
+ as of `[0.9.3]` the CLI, `./define`, `./inspect`, the archive layout, the
6229
+ judgement schema and any retrieval surface are all simultaneously open. Every
6230
+ entry in this section that is blocked on an adopter — the judgement table above
6231
+ being the sharpest — is asking someone to invest in a surface that is
6232
+ documented as movable. That is a coherent thing to ask under 0.x and it is also
6233
+ the reason the ask may never be answered.
6234
+ *The proposal is a line rather than a milestone:* 1.0 freezes the CLI, `./define`
6235
+ and `./inspect`, and everything currently blocked on an adopter's evidence —
6236
+ the judgement schema, the backlink index, any archive retrieval — is explicitly
6237
+ 1.x. What that buys is not a version number; it is the only condition under
6238
+ which "build your own counter-pressure on our export" is a reasonable thing to
6239
+ ask of somebody.
6240
+ *What blocks it.* Freezing `./inspect` five days after shipping it is freezing
6241
+ on one adapter, which is the objection this repository raises against every
6242
+ other premature interface and does not get to waive for its own. And the
6243
+ 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
6246
+ measurement to date of anyone adopting either export.
5551
6247
 
5552
6248
  ## Considered and rejected
5553
6249
 
@@ -5731,7 +6427,7 @@ weaker signal "some registry file failed to load, so suppress all
5731
6427
  `orphan-test`" is the other candidate and is a strictly worse `check` on a
5732
6428
  multi-file registry, where the other files are fine and their orphans are real.
5733
6429
  **The wrong *advice* was separated out and fixed in `[0.7.0]`; the noise is
5734
- fixed under `[Unreleased]`, and the blocker above was the wrong question.**
6430
+ fixed under `[0.7.4]`, and the blocker above was the wrong question.**
5735
6431
  The mapping this entry called for could not be recovered, and did not have to
5736
6432
  be: what a suppression needs is not the broken file's *prefix* but the ids it
5737
6433
  declares, and those are written in its source. Reading them back is a source
@@ -5757,7 +6453,7 @@ breadth argument recorded above, which points the other way for both commands.
5757
6453
  *What rejected it is that the argument for it was entirely the noise, and the
5758
6454
  noise is gone.* Refusing was never justified by the load failure itself — it was
5759
6455
  justified by the wall of derived findings burying it, and the collapse under
5760
- `[Unreleased]` removed the wall without touching either command's breadth. What
6456
+ `[0.7.4]` removed the wall without touching either command's breadth. What
5761
6457
  is left on the table is the breadth argument alone, and it points the other way
5762
6458
  for both: `check` is the command a pipeline runs first, its contract is seeing
5763
6459
  everything it can still see, and answering with three findings instead of nine
@@ -5858,7 +6554,7 @@ measurement** saying parsing costs anything here — this reader is
5858
6554
  `createSourceFile` per file with no type checking, which is the cheap half of
5859
6555
  what arktype pays for, and the fan-out measurement — filed as an entry in
5860
6556
  `Under consideration`, and now shipped as the two concurrency bounds in
5861
- `[0.7.0]` and `[Unreleased]` — clocked `attest check` over 6000 spec files at
6557
+ `[0.7.0]` and `[0.9.0]` — clocked `attest check` over 6000 spec files at
5862
6558
  1.99 s total. Worse, a cache is a correctness hazard
5863
6559
  of precisely the kind this tool exists to detect: a stale analysis makes a
5864
6560
  **drift detector** report drift that has already been fixed, or miss drift
@@ -5880,7 +6576,7 @@ to be stronger than an mtime before the trade is even worth pricing.
5880
6576
 
5881
6577
  ### Zod Mini (`zod/v4-mini`) for the registry schema
5882
6578
 
5883
- Evaluated alongside v4 (which shipped under `[Unreleased]`), on the same
6579
+ Evaluated alongside v4 (which shipped under `[0.8.0]`), on the same
5884
6580
  schema and the same 69-requirement registry, and it wins both numbers it is
5885
6581
  supposed to win:
5886
6582
  **0.249 ms** per parse against v3's 3.47 ms, and — the one v4 does not deliver —
@@ -6162,7 +6858,7 @@ current.
6162
6858
 
6163
6859
  The mechanism `ATX-37` sat under `Under consideration` waiting for, kept there
6164
6860
  on cost, and killed by measuring that cost rather than by paying it. The three
6165
- sites it was aimed at are closed under `[Unreleased]` by hand-written scenarios;
6861
+ sites it was aimed at are closed under `[0.7.4]` by hand-written scenarios;
6166
6862
  what is rejected is the roster, and the argument is that **`src/cli/report.ts`'s
6167
6863
  exports are the wrong roster in both directions at once.**
6168
6864
 
@@ -6282,6 +6978,181 @@ requirement, and human review at propose is still the whole answer. What is
6282
6978
  rejected is grouping as a way to assist it, and this reopens only on a
6283
6979
  contradiction that grouping would have caught.
6284
6980
 
6981
+ ### An append-only writer for `status`, following spec-kit's `converge`
6982
+
6983
+ spec-kit's `converge` assesses the codebase against a feature's artifacts and
6984
+ appends the remaining work as new tasks, under a strict contract: its only write
6985
+ is a new section at the foot of `tasks.md`, and it may not rewrite, renumber,
6986
+ reorder or delete anything already there. Proposed here as the shape `status`
6987
+ would need if it ever produced todos rather than a report, on the grounds that
6988
+ append-only is the safe way to write into a file a human owns.
6989
+
6990
+ *Rejected because this repository already has a strictly finer instrument, and
6991
+ the first reading missed it by comparing the wrong pair.* `ATX-76`: with
6992
+ `--apply`, a MODIFIED requirement is written back by replacing the source span
6993
+ of **each value that changed** — never the entry holding them — and the whole
6994
+ merge is refused, with nothing written, when a span it would overwrite carries a
6995
+ comment. That is a smaller unit than "append at the foot", and it solves the
6996
+ harder problem: editing in place without destroying what a person wrote around
6997
+ it. Append-only avoids the question by never touching existing text; `--apply`
6998
+ answers it. The comparison that produced the proposal was `converge` against
6999
+ `status`, and it never looked at what the command that actually writes already
7000
+ does.
7001
+
7002
+ *And the premise is unbuilt anyway.* `status` is a report, deliberately — it
7003
+ answers what the gate still wants, from the delta, the specs and the record,
7004
+ running nothing. No adoption report, and nobody here, has asked it to become a
7005
+ writer. A write discipline for a command that does not write is a solution
7006
+ looking for its problem, which is what `Speculative Generality` names.
7007
+
7008
+ *Reopening this needs `status` to have a reason to write at all* — and if that
7009
+ ever happens, the discipline to reach for is `ATX-76`'s, not this one.
7010
+
7011
+ ### A Sync Impact Report at the head of a governance document
7012
+
7013
+ spec-kit's `constitution.md` opens with a comment block recording what a change
7014
+ to it touched: the version bump and its rationale, the principles defined, and a
7015
+ checklist of every downstream template reviewed for alignment, each carrying a
7016
+ note on what was checked. Proposed here as a general form of the rule that a `docs/en` change
7017
+ carries its `docs/zh` counterpart in the same commit — a persisted record, per
7018
+ change, of what was checked downstream.
7019
+
7020
+ *Rejected because `pre-commit-check` already produces exactly that, per commit,
7021
+ and produces it where it is useful.* The skill's opening instruction is to
7022
+ report which items applied and which did not, saying plainly when something was
7023
+ skipped and why, because "a silent omission is the failure mode this exists to
7024
+ prevent". That output is a Sync Impact Report under another name, produced at
7025
+ the moment of the change rather than pinned to the head of a file, and it covers
7026
+ more than one document's downstream — packaging, the `Issue` code contract,
7027
+ `SCHEMA_VERSION`, both language trees.
7028
+
7029
+ *What persisting a second copy would cost is the thing this project has been
7030
+ most consistent about avoiding:* it splits a store deliberately kept single. The
7031
+ same argument keeps decisions in this file rather than an ADR directory, and it
7032
+ applies with more force here, because the two copies would disagree the first
7033
+ time a commit's report says something the header block does not.
7034
+
7035
+ *The other half of the constitution reading* — that this repository's
7036
+ load-bearing rules have no ids and nothing reads them — survived the rejection of
7037
+ the document that carried it and was filed under `Under consideration`. Its audit
7038
+ was done on 2026-08-18 and it is now rejected too, in the section directly below
7039
+ this one, on an argument neither half of the reading predicted.
7040
+
7041
+ ### Promoting this repository's own rules in `CLAUDE.md` to requirement ids
7042
+
7043
+ The audit this entry was filed for was done on 2026-08-18 and its answer is
7044
+ **none**. The entry named its own first step — an audit whose primary output is a
7045
+ rejection list — and asked which `CLAUDE.md` rules should become ids, on the
7046
+ observation that rules whose violation has already produced silent defects are
7047
+ gated by nothing.
7048
+
7049
+ *Three were already ids, which the entry knew.* `ATX-28` is the path-spelling
7050
+ rule, `ATX-29` the line-endings rule in the form that matters (the freshness
7051
+ check compares the document rather than its bytes), and `ATX-16` with `ATX-17`
7052
+ the two-readers rule: the four-command static roster, and the refusal to fall
7053
+ back to evaluating a registry that will not read.
7054
+
7055
+ *One is already gated and the entry did not know it.* `docs/zh/**` mirroring
7056
+ `docs/en/**` one-for-one is asserted twice in `tests/docs-consistency.spec.ts` —
7057
+ every English page has exactly one Chinese counterpart, and each pair shares a
7058
+ heading tree and a code-sample sequence. What has no gate is whether the two
7059
+ still *say* the same thing, which is what `CLAUDE.md` and `pre-commit-check` §6
7060
+ both mean by "checked by eye only": the structural half is held, the semantic
7061
+ half cannot be.
7062
+
7063
+ *The rest fall into three groups and none of them wants an id.* **Six are
7064
+ working habits** — `vitest run` does not run `self/`; it also runs the type
7065
+ tests; `bin/attest.js` loads `dist/`; some fixtures fail on purpose; a scenario
7066
+ starting a child run needs an explicit timeout; a package manager is spawned only
7067
+ through the one helper in `tests/consumer.spec.ts`. Each is about how to work in
7068
+ this repository rather than what Attest promises anyone, and §1 is decisive:
7069
+ a requirement costs a mandatory scenario forever, so these would be six scenarios
7070
+ asserting facts about a maintainer's habits. **Two are the design vocabulary and
7071
+ the filing rule**, which are definitions and a destination — there is nothing to
7072
+ assert. **And one constrains a consumer rather than the engine**, which is the
7073
+ finding worth keeping.
7074
+
7075
+ *`Issue.message` is prose, never API — the most load-bearing ungated rule in the
7076
+ file, and no id can hold it.* The promise is that a consumer may branch on `code`
7077
+ and `reqId` and must not branch on wording, so the obligation falls on the
7078
+ consumer's tests and not on any behaviour a run of this engine can exhibit. The
7079
+ checkable half is already structural: `code` is typed to the roster in
7080
+ `src/core/docs.ts`, and `ATX-40` obliges every code to carry a troubleshooting
7081
+ section in both languages. A requirement stating the rest would either restate a
7082
+ type or assert something no run can observe. **This is the audit's general
7083
+ result rather than one rule's exception** — the file is mostly a working
7084
+ agreement with this repository's maintainers, and the three rules in it that were
7085
+ contracts Attest owes an adopter had all been promoted before the question was
7086
+ asked.
7087
+
7088
+ *The same argument retires the entry's own sharpener.* `[0.9.2]` edited
7089
+ `.claude/skills/pre-commit-check/SKILL.md` with no gate having an opinion about
7090
+ its contents, and that is true — but measured, the machine-checkable surface of
7091
+ that document is **one token**: it names exactly one issue code,
7092
+ `spec-load-failed`, and that code exists. An `ATX-57` analogue over it would gate
7093
+ a roster of one. Everything else in the document is judgement, which is both what
7094
+ a gate cannot hold and the reason the document is prose.
7095
+
7096
+ *One actionable finding survives, and it is not an id.* `CLAUDE.md` says not to
7097
+ flatten `fixtures/nested-spec/`, because its spec is the only one below a root
7098
+ and therefore the only thing exercising a separator. The guard is
7099
+ `tests/paths.spec.ts`, which scans that fixture and asserts that no relative path
7100
+ contains a backslash — **and that assertion passes on a flattened corpus**,
7101
+ because a flat filename contains no backslash either. The rule carrying a written
7102
+ warning against silent decay is guarded by an assertion that decays silently. The
7103
+ fix is one line in that test, requiring at least one scanned path to sit below the
7104
+ root, and it is a plain test rather than a requirement for the same reason as
7105
+ everything else here: it is a property of this repository's fixture corpus, not a
7106
+ promise about Attest's behaviour. **Done under `[0.9.3]`**, and the guard
7107
+ was checked against a flattened copy of the fixture before it was written — the
7108
+ predicate reads true on `fixtures/nested-spec/` and false on the same files laid
7109
+ flat, which is the difference the old assertion could not see.
7110
+
7111
+ *Worth reopening only for a rule that is a contract* — one Attest owes an
7112
+ adopter, added to that file later. It belongs in the registry when it is written,
7113
+ not in a second sweep of prose.
7114
+
7115
+ ### Semantic retrieval over the intent layer
7116
+
7117
+ Priced on 2026-08-18 while reading OpenViking (volcengine/OpenViking, AGPL-3.0,
7118
+ at `87d5142e`) for what a context database for agents has that this repository
7119
+ does not. The proposal was its retrieval path
7120
+ (`docs/en/concepts/07-retrieval.md`): embed requirements, specs and archived
7121
+ proposals, then answer a question with an LLM intent analysis that emits zero to
7122
+ five typed queries, a priority-queue recursion down the directory tree with
7123
+ score propagation, and a rerank pass over the survivors.
7124
+
7125
+ Rejected on what that machinery is *for*. Every part of it is a way to find
7126
+ things in a corpus that has lost its structure — free text, no ids, no grammar,
7127
+ tens of thousands of entries. This one has 79 requirements in a single file,
7128
+ each with an id matching `/^[A-Z]+-\d+$/`, grouped by a prefix, carrying a
7129
+ statement written to contain SHALL or MUST. Lookup by id is exact and free;
7130
+ `grep` over the statements answers the rest. Ranking is what you reach for when
7131
+ you cannot address; addressing is the thing this design already has, and §5.4
7132
+ extends it to evidence by keying the red record on requirement id, spec file and
7133
+ scenario name.
7134
+
7135
+ The second reason is the one that would survive a bigger corpus. A retriever of
7136
+ that shape puts a non-deterministic, network-dependent, unfalsifiable scorer
7137
+ beside four commands whose defining property is that they execute nothing and
7138
+ whose output is a verdict a reader has to be able to reproduce. Their own
7139
+ document specifies the degradation frankly — "if rerank returns an invalid
7140
+ result or the API call fails, retrieval falls back to vector scores" — and a
7141
+ silent fallback to a weaker mechanism is precisely what §5.1 refuses when it
7142
+ makes a non-static registry a `registry-not-static` ERROR rather than a
7143
+ fallback to evaluation. The same answer applies here for the same reason.
7144
+
7145
+ Worth reopening if an adopter's registry reaches a size where prefix grouping
7146
+ and `grep` genuinely stop working — but the first thing to try then is the
7147
+ declared prefix ownership already under consideration above, which addresses,
7148
+ rather than a scorer, which ranks.
7149
+
7150
+ ## Releases
7151
+
7152
+ [0.9.4]: https://gitlab.com/Pseudorca/attest/-/tags/v0.9.4
7153
+ [0.9.3]: https://gitlab.com/Pseudorca/attest/-/tags/v0.9.3
7154
+ [0.9.2]: https://gitlab.com/Pseudorca/attest/-/tags/v0.9.2
7155
+ [0.9.1]: https://gitlab.com/Pseudorca/attest/-/tags/v0.9.1
6285
7156
  [0.9.0]: https://gitlab.com/Pseudorca/attest/-/tags/v0.9.0
6286
7157
  [0.8.0]: https://gitlab.com/Pseudorca/attest/-/tags/v0.8.0
6287
7158
  [0.7.4]: https://gitlab.com/Pseudorca/attest/-/tags/v0.7.4