@am_shork/attest 0.9.1 → 0.9.3

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,276 @@ 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.3] - 2026-08-18
17
+
18
+ ### Added
19
+
20
+ - **The archive is read back, as `inspectRedEvidence` on `@am_shork/attest/inspect`
21
+ (`ATX-80`).** `archive --apply` moves each merged change into
22
+ `archive/<date>-<name>/`, carrying `proposal.md`, the delta and the record of
23
+ how every one of its scenarios ended the *first* time it ran — and nothing
24
+ read any of it. The only store here that keeps a decision with its evidence
25
+ attached was write-only. It now answers, for each scenario a project declares,
26
+ which of `fail` / `pass` / `unobserved` / `unarchived` the archived records
27
+ place it in. Promoted from `Planned`, where the blockers were an archive
28
+ nobody had produced and an entry point nobody had chosen.
29
+
30
+ *The question the entry was promoted with was the wrong granularity, and
31
+ finding that is most of what this change is.* It asked which **requirements**
32
+ ever had a scenario observed failing. Under the gate as written that is true
33
+ by construction for every archived id — `neverRedIssues` walks *every*
34
+ scenario of every ADDED id and blocks on any one without a recorded red, so a
35
+ change that archived at all archived with the whole set proven. Where the gate
36
+ genuinely stops is one level down, and it leaks two ways: a scenario grown
37
+ onto a requirement after it archived can never acquire evidence, because
38
+ `never-red` fires only on ids a change is currently ADDing and an archived id
39
+ is ADDed by nothing again; and MODIFIED carries no such obligation at all, so
40
+ a requirement whose statement is later rewritten keeps evidence only for the
41
+ form it had when it archived. Both accumulate silently. The report is
42
+ therefore over `(reqId, file, name)`, which the record already keys by.
43
+
44
+ *Four states rather than two, and the two absences are the whole design.*
45
+ `unarchived` — the id is in no archived change at all — is a requirement
46
+ written straight into the registry, which is how existing behaviour is
47
+ described in a brownfield adoption and which design §6 gives no obligation to
48
+ have been red. `unobserved` — the id **is** archived and this scenario is not
49
+ in the record — is the drift above. Folding them together would report a
50
+ design working exactly as intended as though it were a backlog, and on a
51
+ first-day adoption that is every row. `pass` stays separate from both because
52
+ the gate cannot produce it, so an archived one means a hand-edited record or a
53
+ format that moved.
54
+
55
+ *Two things it deliberately does not do.* It does not judge: whether an
56
+ `unobserved` scenario matters is a judgement about that scenario, which is the
57
+ ground truth §0 refuses to compute for someone else. And it does not hand back
58
+ the records — the archive read returns rows, because the record is a nested map
59
+ keyed by ids, paths and scenario names read off disk, and exporting the
60
+ container would export the prototype hazard `red-record.ts` exists to hold.
61
+
62
+ *`proposal.md` is not read, and that is the blocker dissolving rather than
63
+ being cleared.* The prose half was filed as the one carrying the reasoning.
64
+ In this repository it carries a link: `CLAUDE.md` makes `CHANGELOG.md` the
65
+ single decision record, so a proposal restating an argument would be a second
66
+ store of it with nothing comparing the two. There is nothing in it for an
67
+ index to offer but its path.
68
+
69
+ *The three scan exclusions are untouched.* `locate.ts`, `runner.ts` and
70
+ `pipeline.ts` each still exclude `archive/`, and they are §7 and §8 — an
71
+ archived change's requirements and specs must not re-enter a normal run. The
72
+ new read walks the archive for facts *about* archived changes, never to fold
73
+ their contents back into a registry or a plan.
74
+
75
+ *Blocker **(2)** is settled against the fifth static command, on the roster's
76
+ own terms.* `ATX-16`'s subject is reading TypeScript *without evaluating it* —
77
+ its rationale is about a top-level `fetch` in a registry file reaching CI
78
+ secrets on a fork merge request. `first-run.json` is JSON; joining that roster
79
+ would quietly turn a security promise into a list of commands that do not run
80
+ the suite. The trial datum that seemed to point at a command measured demand
81
+ to read the archive **as prose**, which is a different question. And the
82
+ direction is reversible: adding a command later is additive, narrowing a
83
+ requirement's roster back is not. Two things measured while deciding —
84
+ `archive.ts`'s runtime closure names no `vite`/`vitest`, so `ATX-79`'s
85
+ peer-free promise holds over the new reachability; and `intent-quality.md`
86
+ already exists for findings of exactly this kind, where `cli-reference.md`
87
+ has no place to state one.
88
+
89
+ *What it cost elsewhere, because it is the part that does not announce
90
+ itself.* The fixture that exercises all four states adds a registry, and the
91
+ judgement tables in `tests/intent-rule-candidates.spec.ts` score the fixtures
92
+ too — so the corpus moved 87 → 90 and four measurements were re-read rather
93
+ than re-baselined. `ATX-80` is judged compound (2), and `compound-requirement`
94
+ flags it correctly; its quantifier ranges over values, the shape that table
95
+ prefers; `ATX-80.states` is an iterated domain and carries its pin. One new
96
+ divergence needed judging — `states:ATX-32,ATX-80` — and it is worth a line
97
+ because of how it arrived rather than how it reads: two reports each naming
98
+ their own closed set of labels, neither author aware of the other, which is a
99
+ second generic param name starting the climb `code` made to nineteen members.
100
+ The count of judgements a shippable `divergent-param` would demand grows with
101
+ the registry, from nothing anyone did wrong.
102
+
103
+ *Mutation-checked, and one assertion did not survive it.* Every new test was
104
+ run against a deliberately broken implementation. The one pinning archived
105
+ folder order passed with the sort deleted, because `readdir` returns names
106
+ already ordered on Windows — the fixture-with-no-separator shape from the
107
+ `Fixed` entry below, arriving in a new test on the day it was written. Fixed
108
+ by choosing names where the two orders differ, measured rather than assumed,
109
+ with the casing called out in the test so it is not tidied away.
110
+
111
+ *One more scenario, added before cutting a release, and writing it corrected
112
+ the reason it was added.* Every state above was read out of a hand-written
113
+ `first-run.json` at a folder this repository spelled, so the reader had never
114
+ met an archive the tool itself produced. `self/merge.spec.ts` now runs
115
+ `--apply` for real and joins the result: the folder is the one the run's own
116
+ date stamp named, and the report's paths are derived from whatever it picked.
117
+ Mutation-checked to be worth its runtime — an enumeration that finds nothing,
118
+ and the two absences folded into one, each fail it and no other scenario in
119
+ that file.
120
+
121
+ **The risk it was written to close turned out not to exist, which is the more
122
+ useful half.** The reason given for it was the proposed → merged rename: the
123
+ record is written while the spec is `*.proposed.spec.ts` and `--apply` renames
124
+ that file, so `fileKey` is what keeps the two meeting. Reading the code to
125
+ write the assertion showed the archive reader cannot exercise that at all.
126
+ `recordedOutcome` normalises the **ref**, and the record is keyed in merged
127
+ spelling from the start — so by the time a change is archived, every ref this
128
+ reader passes is already post-merge and `fileKey` is the identity on all of
129
+ them. The translation exists for the gate reading a record mid-flight, and the
130
+ archive reader is downstream of the point where it can matter. The scenario
131
+ stays, for what it does cover; the claim it was justified with is written out.
132
+
133
+ *A hazard found by running the mutations, worth recording because nothing
134
+ reports it.* Stubbing `mergedSpecPath` and running the suite **rewrote two
135
+ committed fixtures** — `fixtures/e2e-archive` and `fixtures/stdout-pollution`
136
+ each hold a `first-run.json` that a real archive run rewrites in place, so a
137
+ mutation run leaves the working tree dirty with plausible-looking changes to
138
+ files nobody edited. Caught by `git status`, not by any gate. It also settles
139
+ the write half by accident: those records really are produced through
140
+ `fileKey`, because under the stub they came out keyed by the proposed
141
+ spelling.
142
+
143
+ - **The propose stage now asks three questions, where it previously said how to
144
+ write a delta and nothing about what to check you had thought about.** They sit
145
+ at the end of Stage 1, immediately before the instruction to present and stop:
146
+ what pins each expectation, does each statement carry one obligation, and was
147
+ any of this ever red. Promoted from `Planned`, where the blocker was the
148
+ selection rather than the idea — spec-kit's `clarify` carries nine generic
149
+ categories, and copying them wholesale was the specific thing to avoid, because
150
+ §10's economics put every line of that document into every adopting project's
151
+ agent context forever.
152
+
153
+ *The selection is by what this repository has actually been bitten by*, which
154
+ is a different list from a product-spec taxonomy. **(1)** is the measured one:
155
+ the mine-density band moved 3.3× with the whole suite green because the
156
+ assertion recomputed its expectation from the param the code under test
157
+ consumed — the entry still under `Under consideration`, whose *guidance* half
158
+ shipped in `[0.5.0]` as a statement in *The model*. What is new here is asking
159
+ it as a question at the moment the assertion is being authored, which is a
160
+ different mechanism from stating the rule 80 lines earlier in the same file.
161
+ **(2)** is measured too: 34 of this repository's requirements carry more than
162
+ one obligation, so the question is doing work no shipped diagnostic does — and
163
+ deliberately names no code, because none exists to name. **(3)** answers what
164
+ the archive gate structurally cannot ask: `never-red` demands a recorded
165
+ failing run only for the ids a change ADDs, so a requirement written for
166
+ behaviour already shipped never had a moment when it should have failed.
167
+
168
+ *Two more went to `CLAUDE.md` instead*, and that split is the finding the entry
169
+ did not anticipate — the checklist wanted two destinations, not one. Crossing a
170
+ platform boundary, and whether a change leaves `check`, `cover`, `render` and
171
+ `status` executing nothing, are about this engine's internals; shipping them
172
+ would have been the dead weight the cost argument was worried about without
173
+ being able to name.
174
+
175
+ *Re-measured before building, per the discipline that section runs on, and the
176
+ claim held exactly*: the document was 405 lines with Stage 1 at 87–255, still
177
+ exactly 169. **The cost claim did not hold, and the entry's own number was
178
+ wrong.** It priced this at "three lines rather than nine" by counting one line
179
+ per category, which is what nine generic categories would have cost as a bare
180
+ list. Three questions with the clause each needs to be answerable cost **eight**
181
+ lines: Stage 1 is now 177 and the document 413. Still under half of what the
182
+ nine would have been, so the argument survives its own number being off, but the
183
+ number is corrected here rather than quietly restated.
184
+
185
+ *And the question the entry left open is answered: no new `ATX-n`.* The
186
+ document's content is gated only by `ATX-57`, which asks that every diagnostic
187
+ code it names be one the engine can emit — and asking *what this makes
188
+ reachable for the first time* is what that check turned into here: the only
189
+ hyphenated code-voice token the three questions add is `never-red`, which the
190
+ document already named. What they earn instead is **one scenario on `ATX-27`**,
191
+ which already obliges the body to carry the workflow rather than a pointer to
192
+ it. A requirement of their own would make every rewording of three sentences a
193
+ registry change; no assertion at all would leave a checklist with no diagnostic
194
+ behind it able to be dropped in silence — the same decay shape as the fixture
195
+ assertion fixed below, which is what decided it.
196
+
197
+ ### Fixed
198
+
199
+ - **The assertion guarding the one fixture that exercises a path separator
200
+ passed on a corpus with no separator in it.** `CLAUDE.md` says not to flatten
201
+ `fixtures/nested-spec/`, because its spec is the only one that sits below a
202
+ root and a relative spec path becomes the child run's `include` glob, where a
203
+ backslash is an escape character. What stood behind that instruction was
204
+ `tests/paths.spec.ts` scanning the fixture and asserting that no relative path
205
+ contains a backslash — and a flat filename contains no backslash either, so
206
+ flattening the fixture would have left the test green while removing the only
207
+ thing it was there to exercise. The rule with a written warning against silent
208
+ decay was guarded by an assertion that decayed silently.
209
+
210
+ Now a second assertion requires the scan to find a spec below the root, which
211
+ is the property the fixture exists for. Checked against a flattened copy
212
+ before being written: the predicate is true on the fixture as committed and
213
+ false on the same files laid flat.
214
+
215
+ *Found by the `CLAUDE.md` audit filed under `Considered and rejected`*, whose
216
+ answer to "which of these rules should become requirement ids" was none — this
217
+ is the one actionable thing it turned up, and it is a plain test rather than an
218
+ `ATX-n` because it is a property of this repository's fixture corpus and not a
219
+ promise Attest makes to anyone.
220
+
221
+ ## [0.9.2] - 2026-08-17
222
+
223
+ ### Added
224
+
225
+ - **`@am_shork/attest/inspect`: read what a project declares, without executing
226
+ it and without acquiring the runner peer.** One function,
227
+ `inspectProject(root)`, answering the requirements a project declares, the
228
+ scenarios declared for each, and the issues that stopped a file being read.
229
+ Nothing about the quality of any of it — that is the ground truth this project
230
+ refuses to compute for someone else's requirements (design §0).
231
+
232
+ *Why it exists rather than what it does.* Everything this repository knows
233
+ about the quality of its own intent layer is a hand-judged table in
234
+ `tests/intent-rule-candidates.spec.ts`, and the check that keeps such a table
235
+ from rotting is that a scenario a row names is one a spec really declares. An
236
+ adopter could not write that check: the package exported the authoring API and
237
+ nothing else, so `parseSpecs` and the static registry read were internal and
238
+ **the scenario names of their own project were unreachable** — not
239
+ undocumented, unreachable. This is the enabling half of the judgement-table
240
+ proposal under *Under consideration*, and deliberately only that half: it
241
+ ships no schema and decides nothing, and it is the only thing that can produce
242
+ the second table that proposal is waiting for.
243
+
244
+ *One function, not the four internals it composes,* because `loadRegistry`
245
+ takes a reader and which of the two runs is a security property fixed by the
246
+ command, never chosen by a caller (design §5.1). The static reader is welded
247
+ in and no parameter can change it.
248
+
249
+ *And its import closure names neither runner peer,* so a consumer that only
250
+ analyses acquires neither `vite` nor `vitest` — the same argument `./define`
251
+ was added on (design §10). Measured on the built entry: ten modules, and the
252
+ only bare specifiers in the closure are `typescript`, `zod` and two node
253
+ builtins. That claim is gated over the **built** package rather than the
254
+ source, because `locate.ts` names `loader.ts` in a type-only import that the
255
+ emit erases: the two graphs differ by exactly that edge, and it is the edge
256
+ the promise is about. `tests/import-boundary.spec.ts` holds the source side,
257
+ where a type-only mention still counts as *naming* the runner, and its
258
+ reachability walk now follows runtime edges only for the same reason.
259
+
260
+ Additive: no command, flag, exit code or `--json` field changes, and nothing
261
+ that used to pass now fails.
262
+
263
+ - **"Judging your own intent layer" (`docs/{en,zh}/intent-quality.md`): the four
264
+ things no gate is looking at, and a method for building your own
265
+ counter-pressure out of them.** For an existing adopter, on the back of the
266
+ export above.
267
+
268
+ *A method with no template, and the omission is the point.* The page states
269
+ the four **findings** — coverage is binary so lumping is free, a quantifier is
270
+ unread, an iterated `params` list is a run's domain, an assertion can echo
271
+ rather than pin — and the two **mechanical properties** that make a
272
+ hand-judged record a counter-pressure rather than a note: completeness
273
+ (something enumerable must force a row) and rot-binding (a scenario a row
274
+ names must be asserted to exist). Both generalise without assuming a single
275
+ column. What it withholds is this repository's own answer — the four tables
276
+ in `tests/intent-rule-candidates.spec.ts` — and it asks a reader to reach that
277
+ file only after doing the hand pass, saying why rather than hiding it: there
278
+ is one such table in existence and its author wrote both the schema and every
279
+ answer in it, so a second one built from someone else's corpus is evidence
280
+ while one built from ours is an echo. That is the fourth finding turned on the
281
+ question itself.
282
+ The worked example is deliberately **one column**, so the shape that comes
283
+ back is not a copy of the shape that went out, and the report the page asks
284
+ for names the thing worth most: *the column you needed that we do not have.*
285
+
16
286
  ## [0.9.1] - 2026-08-13
17
287
 
18
288
  ### Changed
@@ -5069,6 +5339,10 @@ have to gather again.
5069
5339
  **changing the value of an existing param goes through no gate at all.**
5070
5340
  `archive` gates a change; a param edit is not a change. That is a cleaner
5071
5341
  statement of the gap than "params can be tautological".
5342
+ **That observation is now its own entry at the foot of this section**, split
5343
+ out and measured on 2026-08-18, because it asks what happens when a value
5344
+ moves rather than whether an assertion is a real comparison, and neither
5345
+ answer settles the other. What stays here is the detector.
5072
5346
  *What blocks the decision — three things, all of them real.* **(1)** Every
5073
5347
  mutation above is synthetic. Across 8 commits touching `*.reqs.ts` in that
5074
5348
  repo, **no param value was ever modified** — every `params:` line in every diff
@@ -5082,7 +5356,21 @@ have to gather again.
5082
5356
  far past the two registry readers' current surface. **(3)** No mechanism is yet
5083
5357
  known that separates "the assertion pins this independently" from "the assertion
5084
5358
  echoes it" without executing the scenario twice under different values — which
5085
- is mutation testing, a much larger thing to own.
5359
+ is mutation testing, a much larger thing to own. *One has since been built and
5360
+ scored, and it does not lift this blocker.* The syntactic candidate — flag a
5361
+ scenario whose expectation is derived from the param the code under test
5362
+ consumed, decided inside one body without executing anything — flags 9 params
5363
+ here, of which **4 are real echoes and 5 are pinned by something outside the
5364
+ body it cannot see**. Under half precision, and the failure is the one
5365
+ predicted above rather than a tuning problem: what pins a value is often a
5366
+ fixture or a second derivation in another file, so a single-body detector is
5367
+ looking in the wrong place by construction. The newest false positive is the
5368
+ clearest instance — `ATX-79.subpath` is compared against `package.json`, which
5369
+ is not the code under test and does not read the registry, so the independent
5370
+ term is in another file entirely. Precision has fallen on every addition
5371
+ measured so far, which is the direction that matters. The score is quoted from the
5372
+ suite's snapshot and re-read with the judgement-table entry above, which is
5373
+ where the same measurement is used for a different decision.
5086
5374
  **The guidance half has shipped; this entry is now only about the detector.**
5087
5375
  "Read the param inside the assertion" was insufficient and half-wrong as
5088
5376
  written, and `[0.5.0]` replaces it everywhere it appeared — README, the
@@ -5200,7 +5488,7 @@ have to gather again.
5200
5488
  pass and separating them is the finding.
5201
5489
 
5202
5490
  - **Whether a requirement can ever be retired, and what it costs that none can.**
5203
- The registry is at **69 requirements and 192 scenarios, 2.78 each**, and
5491
+ The registry is at **79 requirements and 221 scenarios, 2.80 each**, and
5204
5492
  every one is a permanent obligation: `uncovered-requirement` is an ERROR, so a
5205
5493
  requirement without a scenario cannot exist, and nothing ever removes one.
5206
5494
  `delta.removed` exists and `applyDelta` applies it, but `--apply` refuses to
@@ -5245,6 +5533,19 @@ have to gather again.
5245
5533
  the ratio: 2.56 → 2.78. The cost of a requirement is not flat and it is not
5246
5534
  falling, which is the premise the whole entry rests on, and it is the first
5247
5535
  time this file has had two measurements of it to compare.
5536
+ *A third measurement, on 2026-08-18, and it points the other way from the
5537
+ second.* **79 requirements and 221 scenarios**, counted the way `verify self`
5538
+ prints them, which is the control the 2026-08-08 restatement established. The
5539
+ ratio runs 2.56 → 2.78 → **2.80**, so the step that carried the argument was
5540
+ the first one and the second is **+0.015**, which is flat. The premise this
5541
+ entry rests on now has one measurement for it and one against, and the honest
5542
+ statement is the narrower one: per-requirement cost has held at about 2.8
5543
+ scenarios across ten days and ten requirements. What is unchanged is the
5544
+ absolute total, which only rises — that half of the argument is untouched, and
5545
+ it was always the half about permanence rather than about acceleration. What is
5546
+ no longer supported is the acceleration. **Reopening on cost needs the ratio to
5547
+ move again**; reopening on the three meanings of "retire" needs none of these
5548
+ figures, and that is still the part nobody has separated.
5248
5549
 
5249
5550
  - **Running `verify` over a subset of the scenarios, and what a partial run may
5250
5551
  be allowed to say.** `verify` is all-or-nothing and `check` executes nothing,
@@ -5378,6 +5679,283 @@ have to gather again.
5378
5679
  and the second report of the same shape, which this measurement does not
5379
5680
  supply and does not replace.
5380
5681
 
5682
+ - **An outward-facing form of the judgement tables, so an adopter can run this
5683
+ counter-pressure on their own registry.**
5684
+ `tests/intent-rule-candidates.spec.ts` maintains four hand-judged tables over
5685
+ this repo's registry and enforces their completeness. Design §11 records the
5686
+ properties they defend and that nothing enforces any of them; what the tables
5687
+ add is a counter-pressure holding for **one** registry whose authors agreed to
5688
+ maintain it. An adopter got none of it, and the reason was sharper
5689
+ than "nobody wrote it for them": the package exported `requirement`,
5690
+ `scenario`, `defineRequirements`, `delta` and types, and nothing else.
5691
+ `parseSpecs`, `findFiles` and `readRegistrySource` were internal, so an
5692
+ adopter **could not enumerate their own scenario names** — the table's central
5693
+ rot check, that a claimed scenario is one a spec really declares, was not
5694
+ merely unwritten downstream but unreachable. **That half shipped under
5695
+ `[0.9.2]` as `@am_shork/attest/inspect`** and is recorded at the foot of
5696
+ this entry; what stays here is the schema built on top of it, and the three
5697
+ blockers below are untouched by the export.
5698
+ *The shape that makes it shippable at all is a split, not a feature.* The
5699
+ engine can compute which ids exist, which statements match the universal
5700
+ quantifier regex, which params are list-valued, and whether a named scenario
5701
+ is declared — all four already, all statically. It cannot compute how many
5702
+ obligations a requirement carries, what a quantifier ranges over, or whether a
5703
+ scenario iterates a param. So the shippable object is **the enforcement, never
5704
+ the judgement**: an opt-in `*.judge.ts` read by the same static reader and
5705
+ under the same literal-only rule the registry is (it is metadata about
5706
+ requirements under review, so `check` must execute nothing to read it), whose
5707
+ contents are entirely the adopter's. Opting in is creating the file; once it
5708
+ exists, `requirement-unjudged`, `orphan-judgement`, `unbound-site` and
5709
+ `quantifier-unjudged` make it complete and keep it from rotting. The
5710
+ asymmetry is the only way this can exist without Attest asserting a ground
5711
+ truth it refuses to compute (§0), and it is the same asymmetry `never-red`
5712
+ already runs on: the archive gate would owe judgement only for ids a change
5713
+ ADDs, so a brownfield adoption is not blocked wholesale.
5714
+ *One part of the existing table cannot ship in any form.* `DOMAINS.pin` comes
5715
+ in two kinds — a scenario name, checkable against the AST, and a **source-text
5716
+ fragment** grepped out of the spec file. The second is asserting on the
5717
+ wording of somebody's test, which is the `Issue.message` category error under
5718
+ another name. Only the scenario-name kind is exportable, and what that costs
5719
+ is real: the grep is what caught a deleted pin that `toContain` had passed.
5720
+ *Why no detector ships with it, and this is the part that is already
5721
+ measured.* Three rule families were built and scored against the whole corpus
5722
+ (86 entries, counting a fixture id once per registry file) before any was
5723
+ written into the engine, which is the order `divergent-param` was originally
5724
+ designed in the wrong way round. Every figure below is a quotation of the
5725
+ suite's inline snapshots; the snapshot is the fresh copy, and this paragraph
5726
+ is only ever as fresh as its last edit.
5727
+ **(1) `compound-requirement`** — 34 requirements carry more than one
5728
+ obligation, it flags 16, is **wrong about none**, and misses 18. Never wrong
5729
+ and nearly blind, and the 18 are the form the coverage incentive actually
5730
+ rewards: extra clauses under a single keyword. **(2) The widened variant** —
5731
+ 29 correct, but 14 wrong and still 5 missed, and it is silenced by deleting an
5732
+ "and". A rule whose cheapest fix degrades the artifact it protects does not
5733
+ ship, and its precision falls as the registry grows. **(3) `divergent-param`**
5734
+ — 5 divergences on this repo, **all 5 judged wrong**, zero true positives ever
5735
+ recorded. The `code` group now spans 19 ids, and the failure is structural
5736
+ rather than a matter of naming: the key is built by discarding the evidence of
5737
+ relatedness, so the one genuinely paired divergence in the group is the one it
5738
+ cannot show, and the key has churned three times under `flag` on additions
5739
+ that changed no verdict. A fourth candidate — the echo detector, scored at
5740
+ half precision — belongs to the tautology entry above and is not re-quoted
5741
+ here, because a figure stated in two places goes stale in one. Taken together
5742
+ these are the measured range of what a token or single-body detector over
5743
+ prose can do here, and they are why the proposal is enforcement-only.
5744
+ *What blocks the decision — three things.* **(1) One adapter.** This
5745
+ repository's table is the only one that exists, and its author wrote both the
5746
+ schema and every answer in it. By this project's own rule a second table is
5747
+ what makes the seam real, and freezing a `Judgement` type now is deciding for
5748
+ everyone from 34 requirements written by one person. **(2) The breaking form
5749
+ of this was already rejected** — see *Countable obligations* below — and one
5750
+ of its three reasons survives the separate-file shape intact: **nothing can
5751
+ force the count to be true.** The other two do not survive, which is what is
5752
+ new here: a separate artifact touches no `--json` field, no delta apply and no
5753
+ `render` output, and completeness *across ids* is computable in a way
5754
+ completeness *within* an obligation array never was. So this is a re-proposal
5755
+ with one of three objections answered and the sharpest one standing.
5756
+ **(3) `quantifier-unjudged` reuses a rejected detector as its trigger.**
5757
+ Firing on the quantifier token was rejected below at 43% reach and 70% wrong;
5758
+ the claim here is that the same regex is acceptable when it asks a question an
5759
+ author discharges in one line (`over: 'values'`) rather than issuing a
5760
+ verdict. That claim is untested on anyone who did not also write the regex.
5761
+ *The half that is not blocked has shipped, under `[0.9.2]`.*
5762
+ `@am_shork/attest/inspect` is one function answering what a project declares —
5763
+ AST-only, no runner peer, landing on the existing static/executing boundary.
5764
+ It ships no schema and decides nothing, which is why it could go ahead of the
5765
+ three blockers rather than waiting on them. What it changes about this entry
5766
+ is the *kind* of thing that is now missing: the mechanism is no longer
5767
+ unreachable, only unwritten, so a second table is something an adopter can
5768
+ build rather than something only this repository could.
5769
+ *What would decide it:* one judgement table maintained by someone who did not
5770
+ design it. If it comes back the same shape, the seam is real and the schema
5771
+ follows; if it comes back different, `inspect` was the whole answer and the
5772
+ schema never needs to exist.
5773
+
5774
+ - **A registry cannot say a value is deliberately undecided, and an agent that
5775
+ cannot determine one will invent it.** From a read of GitHub's spec-kit
5776
+ against this design — the first time another SDD toolkit has been compared
5777
+ here rather than OpenSpec, and the source of this entry and the three that
5778
+ followed it — of which two have since been decided: the audit of this
5779
+ repository's own rules is rejected below, and the propose-stage coverage scan
5780
+ shipped under `[0.9.3]`, as three questions rather than nine categories.
5781
+ spec-kit marks an unresolved decision inline as `[NEEDS CLARIFICATION: …]`,
5782
+ greppable, with a command whose whole job is converging them. Attest's
5783
+ registry has two states: schema-valid, or an ERROR. There is no third.
5784
+ *Why that matters here specifically, rather than as a general nicety.* The
5785
+ workflow is written for an agent to follow — that is what `init` writes — and
5786
+ an agent drafting a delta that cannot determine a timeout does not leave the
5787
+ field out, because the schema will not let it. It picks a number. A fabricated
5788
+ `params` value, then pinned by a scenario written to match it, is exactly the
5789
+ drift this project exists to prevent, arriving through the mechanism meant to
5790
+ stop it: the single source is perfectly consistent and describes a promise
5791
+ nobody made.
5792
+ *A precedent that is not one, recorded because it reads like one.*
5793
+ `rationale-placeholder` already warns about authoring sloppiness in a prose
5794
+ field, and the `init` document already teaches it — but it is about
5795
+ interpolation syntax written into the wrong field, not about a value nobody
5796
+ has decided. The engine's willingness to warn on prose is the precedent; the
5797
+ diagnostic is not the feature.
5798
+ *The gap is narrower than "the registry cannot express doubt".* `changes/` is
5799
+ already the place where a whole proposal is undecided, and the archive gate is
5800
+ what settles it. What has no expression is **one field** of an otherwise
5801
+ well-formed requirement.
5802
+ *Shape, if it is built:* an additive `open?: string[]`, naming the fields or
5803
+ questions still unresolved; a WARNING from `check`, so a proposal may be
5804
+ legitimately half-finished, and an ERROR at the archive gate, so "done" is not
5805
+ diluted. The same asymmetry `never-red` already runs on.
5806
+ *What blocks the decision — three things.* **(1)** The frequency is entirely
5807
+ unmeasured. No adoption report has recorded an agent inventing a param; the
5808
+ argument above is structural, and this project has twice been wrong about a
5809
+ hazard it reasoned to rather than measured. **(2)** The cheap half already
5810
+ works: writing the uncertainty into `rationale` as prose costs nothing, and
5811
+ the propose stage is human-reviewed regardless. What that does not buy is a
5812
+ gate, which is the whole question. **(3)** It is permanent surface — a schema
5813
+ field, a code, two troubleshooting sections, a History row, an `ATX-n` and its
5814
+ scenarios forever — for a hazard nobody has yet reported.
5815
+
5816
+ - **Prefix ownership is inferred rather than declared, and the cost arrives only
5817
+ when adoption succeeds.** Design §11 records that the id grammar has no level
5818
+ above the requirement: the prefix is a convention, nothing allocates it, and
5819
+ `duplicate-prefix` can only report a collision that already happened.
5820
+ spec-kit's answer is a roadmap table — a stable id, an intent, a scope
5821
+ boundary, dependencies — authored above the specs it decomposes into.
5822
+ *Two things have changed since that reading, and they point opposite ways.*
5823
+ The proposal is **smaller** than it looked: the mechanism partly exists.
5824
+ `loadRegistry` returns `prefixOwners` — first claim in sorted file order —
5825
+ `merge.ts` uses it to decide which file an ADDED id belongs in, and
5826
+ `apply-no-prefix-owner` already refuses a prefix nobody owns. So what is
5827
+ proposed is declaration replacing inference, not a new layer. And it is
5828
+ **weaker**: `/inspect` shipped in `[0.9.2]`, so the cross-repository half —
5829
+ the one §11 says no command can see across — is now something an adopter
5830
+ computes from two registries in about ten lines. Attest does not have to own
5831
+ it to make it possible.
5832
+ *What blocks the decision:* nobody has the problem. §11 states the cost
5833
+ arrives "exactly when adoption succeeds: two teams sharing a registry have
5834
+ someone owning prefix allocation by hand", and no such adoption has been
5835
+ reported. Building the declared form now would be deciding prefix semantics
5836
+ for everyone from one repository whose five prefixes — `ATX` and the four
5837
+ fixture ones — are each declared by exactly one file, so the allocation
5838
+ question this would answer has never been asked here.
5839
+
5840
+
5841
+ - **A merged requirement carries no trace of the change that produced it, and
5842
+ the rename chain is the part actually lost.** `RequirementSchema` is
5843
+ `statement`, `rationale`, `params`, `outOfScope`; the splice writes those and
5844
+ nothing else, so after `--apply` no field and no sidecar records which change
5845
+ introduced an id, which modified it, or that one id used to be another. The
5846
+ delta that knew is in `archive/`, which nothing reads. Proposed from
5847
+ OpenViking's `.relations.json`: links between memories live in a
5848
+ directory-level sidecar, deliberately kept out of the content, and are
5849
+ rendered on demand at retrieval time (`docs/design/memory-link-design.md`).
5850
+ *The argument for the sidecar over a field is in their competitor analysis
5851
+ rather than their design.* The same document reads GBrain, which writes links
5852
+ into the page body, and records what that costs: rename a page and every
5853
+ inbound link still points at the old slug, with fuzzy matching at the read end
5854
+ as the only mitigation. A `supersedes:` or `introducedBy:` field on a
5855
+ requirement would be that mistake in this repository's terms — the registry is
5856
+ a literal and a single source (§2), and a field naming a folder that a later
5857
+ rename can move is a second source with nothing comparing it against the
5858
+ first. A derived index has no such copy to go stale: it is recomputed from the
5859
+ archived deltas every time it is read.
5860
+ *What blocks the decision:* the empty archive — the subject of the entry that
5861
+ shipped under `[0.9.3]`, whose promotion this one does **not** inherit,
5862
+ because the two changes that produced that corpus were ADDED-only and
5863
+ exercised no rename at all — which makes a backlink index testable only
5864
+ against fixtures; and one question the OpenViking reading
5865
+ does not answer, which is whether the chain is wanted at all. §7 applies
5866
+ `RENAMED` to a `Map<id, Requirement>` and the old id is then simply gone —
5867
+ deliberately, as far as anything written says. The case for recovering it is an
5868
+ external reference that outlives the rename, a ticket or a commit message
5869
+ naming a retired id, and nobody has reported one.
5870
+
5871
+ - **The instruction document says how to write intent and nothing about how to
5872
+ read the record back.** §10's one-document-many-targets file is entirely about
5873
+ authoring, and the moment anything reads `archive/` — the entry above, and the
5874
+ one that shipped under `[0.9.3]` — an
5875
+ agent is holding prose from a proposal agreed on some past date against a
5876
+ registry that is current, with no stated order between them. OpenViking states
5877
+ one for its own store in the skill it ships to coding agents
5878
+ (`agent-plugins/skills/openviking-memory/SKILL.md`): retrieved memory is
5879
+ advisory, and the order is system and developer instructions, then the current
5880
+ request, then current environment and tool evidence, then memory — with "prior
5881
+ success never authorizes a destructive action now", and "store conclusions,
5882
+ not scrollback".
5883
+ *Here the same rule has a harder form, because the authoritative half is
5884
+ machine-checked.* The registry and the gate's verdict are evidence; an
5885
+ archived proposal is testimony about a decision taken once. Where they
5886
+ disagree, the disagreement is a finding — the exact thing this tool exists to
5887
+ report — and not something for a reader to reconcile quietly in favour of
5888
+ whichever it read last. That is a property of the design rather than of a
5889
+ file, so if it is taken it lands in design §11 first and in the instruction
5890
+ document second.
5891
+ *What blocks the decision:* it is a rule for reading a store nothing reads yet,
5892
+ so on its own it would document a hazard that cannot currently arise. It is the
5893
+ cheapest of the three and could ship first, at the price of being the only
5894
+ paragraph in that file describing a workflow the tool does not have — and §10's
5895
+ economics make that price the same in every adopter's agent instructions at
5896
+ once.
5897
+ **That blocker is retired, and what replaced it is narrower than the entry
5898
+ assumed.** Something reads `archive/` as of `[0.9.3]`, so the hazard can
5899
+ now arise. But what shipped reads the **typed** half — `first-run.json`, whose
5900
+ every value is an `Outcome` in a schema-validated envelope — and evidence that
5901
+ parses cannot disagree with the registry in the way testimony can: a scenario
5902
+ either is in the record or is not. The rule this entry wants is about the prose
5903
+ half, and the prose half is still read by nothing. So the store is no longer
5904
+ write-only and the entry's own subject is still hypothetical, which is a
5905
+ different blocker from the one it was filed with rather than none.
5906
+
5907
+ - **Changing the value of an existing param passes no gate, and for seven of
5908
+ them there is no record either.** Split out of the tautology entry at the
5909
+ head of this section on 2026-08-18, where it was one sentence inside the
5910
+ reframing paragraph. It is a different question: that entry asks whether an
5911
+ assertion is a real comparison, this one asks what happens when the number
5912
+ itself moves, and neither answer settles the other. `archive` gates a
5913
+ **change** — a delta, proposed specs, a recorded failing run — and a param edit
5914
+ is none of those. It is one literal in a merged `*.reqs.ts`, and every gate
5915
+ reads it as fine: `check` finds it schema-valid, because a param is any JSON
5916
+ value; `possible-drift` is quiet by construction, since `ATX-8` makes it ask
5917
+ whether *any* covering scenario reads the params at all rather than whether a
5918
+ value moved; and `verify` stays green wherever the assertion derives its
5919
+ expectation from the same param, which is the pair of green rows measured in
5920
+ the entry this was split from.
5921
+ *What is left is `render`, and it was measured here rather than assumed.*
5922
+ Across **89 params on 59 of this repository's 79 requirements, 82 — 92.1% —
5923
+ are interpolated into their own statement**, so editing one changes
5924
+ `self/requirements/SPEC.md` and `render --check` fails until the document is
5925
+ regenerated. That is a review surface and not a gate: the new value lands in a
5926
+ committed generated file where a reader can see it, and nothing anywhere asks
5927
+ whether the move was intended.
5928
+ *The other seven leave no trace outside the line that was edited, and which
5929
+ seven they are is the finding rather than the count.* Five are **structurally
5930
+ uninterpolatable**: `ATX-7.payloadKinds` and `ATX-63.payloadKinds` are
5931
+ kind → payload tables, and `ATX-15.sortMethods`, `ATX-15.bannedApis` and
5932
+ `ATX-72.quotes` are lists. A statement cannot carry any of them —
5933
+ `non-scalar-interpolation` is the diagnostic that refuses it — so the params
5934
+ with no record are disproportionately the shape §2 widened the params rule to
5935
+ cover **precisely because lists and tables are the most drift-prone constants
5936
+ there are**. The remaining two, `ATX-15.comparator` and `ATX-26.code`, are
5937
+ scalars their author simply did not interpolate.
5938
+ *What this is not.* It is not a re-proposal of the committed params snapshot
5939
+ rejected below. That was rejected on the argument that the mechanism guarding a
5940
+ param is a scenario reading it rather than a file recording it, and nothing
5941
+ here disputes that or needs a second copy of the values. The observation is
5942
+ narrower: the **edit** is unremarkable to every gate, which is a different
5943
+ claim from the values being unguarded.
5944
+ *What blocks the decision — and the first is the one that blocks the entry this
5945
+ came from.* **(1)** Frequency is unmeasured, and the one corpus anybody has
5946
+ looked at points at zero: across 8 commits touching `*.reqs.ts` in the
5947
+ reporting repository, no param value was ever modified — every `params:` line
5948
+ in every diff is an addition. A defect nobody has committed is a hazard rather
5949
+ than a report. **(2)** No shape is obvious. A param edit is not a change, so
5950
+ `archive` is the wrong place; making `check` notice one needs a previous value
5951
+ to compare against, which is the rejected snapshot wearing a different hat. The
5952
+ only mechanism needing no stored copy is review, and review already sees the
5953
+ `SPEC.md` diff for 82 of the 89. **(3)** What is left after that is seven
5954
+ params in one repository, and the right answer for the five that matter may be
5955
+ nothing more than a line in `intent-quality.md` — the four things no gate is
5956
+ looking at already being that page's subject, and this being a fifth of the
5957
+ same kind.
5958
+
5381
5959
  ## Considered and rejected
5382
5960
 
5383
5961
  Decisions **not** to build something, kept where they can be found before the
@@ -5864,24 +6442,27 @@ quotation of that snapshot and is only ever as fresh as the last edit here —
5864
6442
  read the snapshot when the number is what matters. *`compound-requirement`
5865
6443
  (WARNING) — rejected.* More than one RFC-2119 keyword in one `statement` is
5866
6444
  objectively more than one obligation. Measured (figures re-read from the suite,
5867
- which is the only place they are not prose): of the 8 requirements in the corpus
5868
- carrying more than one, it flags 3 (`ATX-10`, `ATX-21`, `ATX-26`) and misses
5869
- `ATX-5`, `ATX-9`, `ATX-13`, `ATX-14`, `ATX-17` every one that packs clauses
5870
- under a single keyword, which is the form the coverage incentive actually
5871
- rewards. A wider variant (keyword plus a coordinating `and`/`while`/`;`) reaches
5872
- 7 of 8, at 3 wrong hits (`ATX-6`, `ATX-22`, `ATX-23`) rather than the 1 recorded
5873
- when it was first scored so reach is not the deciding argument, and the
5874
- precision it trades away grows with the registry. This is: both variants are
6445
+ which is the only place they are not prose): of the 34 requirements in the
6446
+ corpus carrying more than one, it flags 16 and misses 18 every one of those 18
6447
+ packs clauses under a single keyword, which is the form the coverage incentive
6448
+ actually rewards. A wider variant (keyword plus a coordinating `and`/`while`/`;`)
6449
+ reaches 29 of 34, at 14 wrong hits rather than the 3 quoted here when the corpus
6450
+ was half this size and the 1 recorded when it was first scored so reach is not
6451
+ the deciding argument, and the precision it trades away grows with the registry,
6452
+ which three successive re-readings of this paragraph have now measured rather
6453
+ than predicted. This is: both variants are
5875
6454
  silenced by deleting the word that triggered them, and neither can tell that
5876
6455
  deletion from a real split. `SHALL do A and MUST do B` clears the warning as
5877
6456
  `SHALL do A and do B` — same two obligations, one fewer normative keyword. A
5878
6457
  rule whose cheapest fix degrades the artifact it protects does not ship.
5879
6458
  *`divergent-param` (WARNING) — rejected.* One param name declared with different
5880
- values in two requirements sharing an id prefix. Measured: it fires twice on
5881
- this repo and both firings are wrong — `idleTimeoutMin` (`15` in `ATX-3`, `30`
5882
- in `ATX-10`), unrelated illustrative fixtures, and `code` (`registry-not-static`
5883
- in `ATX-17`, `empty-spec` in `ATX-18`, `rationale-placeholder` in `ATX-21`),
5884
- diagnostics that could not possibly agree. The second arrived on its own when
6459
+ values in two requirements sharing an id prefix. Measured: it fires 5 times on
6460
+ this repo and **every firing is judged wrong** — `idleTimeoutMin` (`15` in
6461
+ `ATX-3`, `30` in `ATX-10`), unrelated illustrative fixtures, and `code`
6462
+ (`registry-not-static` in `ATX-17`, `empty-spec` in `ATX-18`,
6463
+ `rationale-placeholder` in `ATX-21`), diagnostics that could not possibly agree.
6464
+ That second group has since grown to span **19 ids**, and two more param names
6465
+ have joined it on the same structure (`flag`, `payloadKinds`). The second arrived on its own when
5885
6466
  `ATX-18` was added and grew a third member on its own again when `ATX-21` was,
5886
6467
  then `ATX-25`, then `ATX-26`, which is the more damning half: a generic param
5887
6468
  name collides more often as a registry grows, so the false-positive rate rises
@@ -5898,8 +6479,19 @@ firing. Until it appeared, every false positive had been on the single name
5898
6479
  workaround and therefore left the rule arguably salvageable. It is not the word:
5899
6480
  two requirements owning unrelated constants of the same *kind* is the structure,
5900
6481
  and any registry large enough to be worth linting has it. Still zero true
5901
- positives, now across a 34-entry corpus (the `corpus` figure in the suite's
6482
+ positives, now across an 86-entry corpus (the `corpus` figure in the suite's
5902
6483
  snapshot, which counts a fixture id once per registry file that declares it).
6484
+ **Two failure modes have been measured since, and both are about the key rather
6485
+ than the threshold.** The grouping keeps one id per distinct value, so two
6486
+ requirements naming the *same* constant — the case a reader would most want
6487
+ compared — are the pair the key deduplicates away; that has now happened twice,
6488
+ most sharply on `ATX-18`/`ATX-77`, a pair split deliberately because each is
6489
+ falsifiable in a direction the other is blind to. And the key's *identity*
6490
+ moves when a requirement agreeing with an existing member is added, so a finding
6491
+ has to be re-judged by hand while nothing about it changed — three re-keyings
6492
+ across three releases under `flag` alone, none of which moved a verdict. A rule
6493
+ whose output churns on additions that agree with it is one whose maintenance
6494
+ cost is a function of registry growth, on top of the zero precision above.
5903
6495
  The escape hatch it was blocked on has since been designed and does not rescue
5904
6496
  it: with no config file the only workable shape is an additive
5905
6497
  `independentParams?: string[]` on the requirement (a CLI flag is per-invocation
@@ -6097,6 +6689,180 @@ requirement, and human review at propose is still the whole answer. What is
6097
6689
  rejected is grouping as a way to assist it, and this reopens only on a
6098
6690
  contradiction that grouping would have caught.
6099
6691
 
6692
+ ### An append-only writer for `status`, following spec-kit's `converge`
6693
+
6694
+ spec-kit's `converge` assesses the codebase against a feature's artifacts and
6695
+ appends the remaining work as new tasks, under a strict contract: its only write
6696
+ is a new section at the foot of `tasks.md`, and it may not rewrite, renumber,
6697
+ reorder or delete anything already there. Proposed here as the shape `status`
6698
+ would need if it ever produced todos rather than a report, on the grounds that
6699
+ append-only is the safe way to write into a file a human owns.
6700
+
6701
+ *Rejected because this repository already has a strictly finer instrument, and
6702
+ the first reading missed it by comparing the wrong pair.* `ATX-76`: with
6703
+ `--apply`, a MODIFIED requirement is written back by replacing the source span
6704
+ of **each value that changed** — never the entry holding them — and the whole
6705
+ merge is refused, with nothing written, when a span it would overwrite carries a
6706
+ comment. That is a smaller unit than "append at the foot", and it solves the
6707
+ harder problem: editing in place without destroying what a person wrote around
6708
+ it. Append-only avoids the question by never touching existing text; `--apply`
6709
+ answers it. The comparison that produced the proposal was `converge` against
6710
+ `status`, and it never looked at what the command that actually writes already
6711
+ does.
6712
+
6713
+ *And the premise is unbuilt anyway.* `status` is a report, deliberately — it
6714
+ answers what the gate still wants, from the delta, the specs and the record,
6715
+ running nothing. No adoption report, and nobody here, has asked it to become a
6716
+ writer. A write discipline for a command that does not write is a solution
6717
+ looking for its problem, which is what `Speculative Generality` names.
6718
+
6719
+ *Reopening this needs `status` to have a reason to write at all* — and if that
6720
+ ever happens, the discipline to reach for is `ATX-76`'s, not this one.
6721
+
6722
+ ### A Sync Impact Report at the head of a governance document
6723
+
6724
+ spec-kit's `constitution.md` opens with a comment block recording what a change
6725
+ to it touched: the version bump and its rationale, the principles defined, and a
6726
+ checklist of every downstream template reviewed for alignment, each carrying a
6727
+ note on what was checked. Proposed here as a general form of the rule that a `docs/en` change
6728
+ carries its `docs/zh` counterpart in the same commit — a persisted record, per
6729
+ change, of what was checked downstream.
6730
+
6731
+ *Rejected because `pre-commit-check` already produces exactly that, per commit,
6732
+ and produces it where it is useful.* The skill's opening instruction is to
6733
+ report which items applied and which did not, saying plainly when something was
6734
+ skipped and why, because "a silent omission is the failure mode this exists to
6735
+ prevent". That output is a Sync Impact Report under another name, produced at
6736
+ the moment of the change rather than pinned to the head of a file, and it covers
6737
+ more than one document's downstream — packaging, the `Issue` code contract,
6738
+ `SCHEMA_VERSION`, both language trees.
6739
+
6740
+ *What persisting a second copy would cost is the thing this project has been
6741
+ most consistent about avoiding:* it splits a store deliberately kept single. The
6742
+ same argument keeps decisions in this file rather than an ADR directory, and it
6743
+ applies with more force here, because the two copies would disagree the first
6744
+ time a commit's report says something the header block does not.
6745
+
6746
+ *The other half of the constitution reading* — that this repository's
6747
+ load-bearing rules have no ids and nothing reads them — survived the rejection of
6748
+ the document that carried it and was filed under `Under consideration`. Its audit
6749
+ was done on 2026-08-18 and it is now rejected too, in the section directly below
6750
+ this one, on an argument neither half of the reading predicted.
6751
+
6752
+ ### Promoting this repository's own rules in `CLAUDE.md` to requirement ids
6753
+
6754
+ The audit this entry was filed for was done on 2026-08-18 and its answer is
6755
+ **none**. The entry named its own first step — an audit whose primary output is a
6756
+ rejection list — and asked which `CLAUDE.md` rules should become ids, on the
6757
+ observation that rules whose violation has already produced silent defects are
6758
+ gated by nothing.
6759
+
6760
+ *Three were already ids, which the entry knew.* `ATX-28` is the path-spelling
6761
+ rule, `ATX-29` the line-endings rule in the form that matters (the freshness
6762
+ check compares the document rather than its bytes), and `ATX-16` with `ATX-17`
6763
+ the two-readers rule: the four-command static roster, and the refusal to fall
6764
+ back to evaluating a registry that will not read.
6765
+
6766
+ *One is already gated and the entry did not know it.* `docs/zh/**` mirroring
6767
+ `docs/en/**` one-for-one is asserted twice in `tests/docs-consistency.spec.ts` —
6768
+ every English page has exactly one Chinese counterpart, and each pair shares a
6769
+ heading tree and a code-sample sequence. What has no gate is whether the two
6770
+ still *say* the same thing, which is what `CLAUDE.md` and `pre-commit-check` §6
6771
+ both mean by "checked by eye only": the structural half is held, the semantic
6772
+ half cannot be.
6773
+
6774
+ *The rest fall into three groups and none of them wants an id.* **Six are
6775
+ working habits** — `vitest run` does not run `self/`; it also runs the type
6776
+ tests; `bin/attest.js` loads `dist/`; some fixtures fail on purpose; a scenario
6777
+ starting a child run needs an explicit timeout; a package manager is spawned only
6778
+ through the one helper in `tests/consumer.spec.ts`. Each is about how to work in
6779
+ this repository rather than what Attest promises anyone, and §1 is decisive:
6780
+ a requirement costs a mandatory scenario forever, so these would be six scenarios
6781
+ asserting facts about a maintainer's habits. **Two are the design vocabulary and
6782
+ the filing rule**, which are definitions and a destination — there is nothing to
6783
+ assert. **And one constrains a consumer rather than the engine**, which is the
6784
+ finding worth keeping.
6785
+
6786
+ *`Issue.message` is prose, never API — the most load-bearing ungated rule in the
6787
+ file, and no id can hold it.* The promise is that a consumer may branch on `code`
6788
+ and `reqId` and must not branch on wording, so the obligation falls on the
6789
+ consumer's tests and not on any behaviour a run of this engine can exhibit. The
6790
+ checkable half is already structural: `code` is typed to the roster in
6791
+ `src/core/docs.ts`, and `ATX-40` obliges every code to carry a troubleshooting
6792
+ section in both languages. A requirement stating the rest would either restate a
6793
+ type or assert something no run can observe. **This is the audit's general
6794
+ result rather than one rule's exception** — the file is mostly a working
6795
+ agreement with this repository's maintainers, and the three rules in it that were
6796
+ contracts Attest owes an adopter had all been promoted before the question was
6797
+ asked.
6798
+
6799
+ *The same argument retires the entry's own sharpener.* `[0.9.2]` edited
6800
+ `.claude/skills/pre-commit-check/SKILL.md` with no gate having an opinion about
6801
+ its contents, and that is true — but measured, the machine-checkable surface of
6802
+ that document is **one token**: it names exactly one issue code,
6803
+ `spec-load-failed`, and that code exists. An `ATX-57` analogue over it would gate
6804
+ a roster of one. Everything else in the document is judgement, which is both what
6805
+ a gate cannot hold and the reason the document is prose.
6806
+
6807
+ *One actionable finding survives, and it is not an id.* `CLAUDE.md` says not to
6808
+ flatten `fixtures/nested-spec/`, because its spec is the only one below a root
6809
+ and therefore the only thing exercising a separator. The guard is
6810
+ `tests/paths.spec.ts`, which scans that fixture and asserts that no relative path
6811
+ contains a backslash — **and that assertion passes on a flattened corpus**,
6812
+ because a flat filename contains no backslash either. The rule carrying a written
6813
+ warning against silent decay is guarded by an assertion that decays silently. The
6814
+ fix is one line in that test, requiring at least one scanned path to sit below the
6815
+ root, and it is a plain test rather than a requirement for the same reason as
6816
+ everything else here: it is a property of this repository's fixture corpus, not a
6817
+ promise about Attest's behaviour. **Done under `[0.9.3]`**, and the guard
6818
+ was checked against a flattened copy of the fixture before it was written — the
6819
+ predicate reads true on `fixtures/nested-spec/` and false on the same files laid
6820
+ flat, which is the difference the old assertion could not see.
6821
+
6822
+ *Worth reopening only for a rule that is a contract* — one Attest owes an
6823
+ adopter, added to that file later. It belongs in the registry when it is written,
6824
+ not in a second sweep of prose.
6825
+
6826
+ ### Semantic retrieval over the intent layer
6827
+
6828
+ Priced on 2026-08-18 while reading OpenViking (volcengine/OpenViking, AGPL-3.0,
6829
+ at `87d5142e`) for what a context database for agents has that this repository
6830
+ does not. The proposal was its retrieval path
6831
+ (`docs/en/concepts/07-retrieval.md`): embed requirements, specs and archived
6832
+ proposals, then answer a question with an LLM intent analysis that emits zero to
6833
+ five typed queries, a priority-queue recursion down the directory tree with
6834
+ score propagation, and a rerank pass over the survivors.
6835
+
6836
+ Rejected on what that machinery is *for*. Every part of it is a way to find
6837
+ things in a corpus that has lost its structure — free text, no ids, no grammar,
6838
+ tens of thousands of entries. This one has 79 requirements in a single file,
6839
+ each with an id matching `/^[A-Z]+-\d+$/`, grouped by a prefix, carrying a
6840
+ statement written to contain SHALL or MUST. Lookup by id is exact and free;
6841
+ `grep` over the statements answers the rest. Ranking is what you reach for when
6842
+ you cannot address; addressing is the thing this design already has, and §5.4
6843
+ extends it to evidence by keying the red record on requirement id, spec file and
6844
+ scenario name.
6845
+
6846
+ The second reason is the one that would survive a bigger corpus. A retriever of
6847
+ that shape puts a non-deterministic, network-dependent, unfalsifiable scorer
6848
+ beside four commands whose defining property is that they execute nothing and
6849
+ whose output is a verdict a reader has to be able to reproduce. Their own
6850
+ document specifies the degradation frankly — "if rerank returns an invalid
6851
+ result or the API call fails, retrieval falls back to vector scores" — and a
6852
+ silent fallback to a weaker mechanism is precisely what §5.1 refuses when it
6853
+ makes a non-static registry a `registry-not-static` ERROR rather than a
6854
+ fallback to evaluation. The same answer applies here for the same reason.
6855
+
6856
+ Worth reopening if an adopter's registry reaches a size where prefix grouping
6857
+ and `grep` genuinely stop working — but the first thing to try then is the
6858
+ declared prefix ownership already under consideration above, which addresses,
6859
+ rather than a scorer, which ranks.
6860
+
6861
+ ## Releases
6862
+
6863
+ [0.9.3]: https://gitlab.com/Pseudorca/attest/-/tags/v0.9.3
6864
+ [0.9.2]: https://gitlab.com/Pseudorca/attest/-/tags/v0.9.2
6865
+ [0.9.1]: https://gitlab.com/Pseudorca/attest/-/tags/v0.9.1
6100
6866
  [0.9.0]: https://gitlab.com/Pseudorca/attest/-/tags/v0.9.0
6101
6867
  [0.8.0]: https://gitlab.com/Pseudorca/attest/-/tags/v0.8.0
6102
6868
  [0.7.4]: https://gitlab.com/Pseudorca/attest/-/tags/v0.7.4