@am_shork/attest 1.0.0 → 1.1.1

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
@@ -90,6 +90,391 @@ above and 110 stale lines on this one.
90
90
  security hole takes a major, on whatever schedule a hole is found. That price is
91
91
  now stated rather than unknown, which is what made the line decidable at all.
92
92
 
93
+ ## [1.1.1] - 2026-09-03
94
+
95
+ ### Documentation
96
+
97
+ - **`docs/{en,zh}/intent-quality.md` gains a seventh finding: a param no
98
+ statement interpolates changes with nothing outside the line to show it.**
99
+ `render --check` gives a review surface for the 92.1% of this repository's
100
+ params that a statement does interpolate — editing one moves `SPEC.md`. It
101
+ cannot help with the rest: five are structurally uninterpolatable (a
102
+ kind-to-payload table or a list, which `non-scalar-interpolation` refuses in
103
+ a statement) and two are scalars nobody interpolated. For those, every gate
104
+ treats the edit as fine — `check` sees a schema-valid literal,
105
+ `possible-drift` is satisfied by any covering scenario reading the param at
106
+ all, and `verify` stays green wherever the assertion derives its expectation
107
+ from the same value — so a human reading the registry diff is the only thing
108
+ that catches an unintended move. Split out of the *Under consideration* entry
109
+ on changing an existing param's value, as the cheap half it named while the
110
+ WARNING it also considered stays undecided for want of a second corpus.
111
+
112
+ ### Added
113
+
114
+ - **`pre-commit-check` gained §9, a hand sweep for a dependency bump.** Closes
115
+ the *Considered and rejected* entry on a standing `no-deprecated` gate: three
116
+ deprecated call sites across a Zod 3→4, Vitest 3→4 and typescript-eslint
117
+ 8.2→8.65 walk was too small a denominator to carry a third tsconfig or an
118
+ `allowJs` admission into the wider typed project, so the check stays a manual
119
+ step — run `@typescript-eslint/no-deprecated` against a throwaway project that
120
+ also covers the five files no tsconfig in this tree includes
121
+ (`eslint.config.js`, `bin/attest.js`, the three `vitest*.config.ts`) — rather
122
+ than a permanent gate.
123
+
124
+ - **A gate on which files inside this tree may name the package by the name it
125
+ publishes under.** A built `dist/` makes this package resolve *itself*:
126
+ `package.json` names the subpaths in `exports`, so `@am_shork/attest/define`
127
+ resolves to `dist/core/registry.js` on any machine that has run a build. CI's
128
+ lint, test and typecheck jobs run before one, and there it resolves nowhere.
129
+ Measured once, on an archived `requirements.delta.ts` written with the
130
+ specifier the workflow document hands *adopters*: it passed `eslint`, both
131
+ typechecks, the whole suite, `check self`, `verify self` and `archive --apply`
132
+ locally — **six gates** — then failed **three CI jobs** on that one line. This
133
+ is the sibling of the `bin/attest.js` loads `dist/` gotcha and the more
134
+ dangerous half of it: there a stale build runs old code, here the build
135
+ vouches for an import that has no other resolution.
136
+
137
+ `tests/import-boundary.spec.ts` now reads the whole checked tree through the
138
+ same AST walk the runner-boundary tests use, and asserts the set of published
139
+ specifiers against a one-entry allowlist. The single legitimate namer is
140
+ `examples/judgement-table/judgement.spec.ts` at `@am_shork/attest/inspect`,
141
+ and it is legitimate for exactly the reason the rule exists: `./inspect` is
142
+ the only subpath with somewhere to resolve *to* inside this tree, via
143
+ `resolve.alias` in `vitest.config.ts` and the matching `paths` entry in
144
+ `tsconfig.typecheck.json`.
145
+
146
+ *Two things about the walk are load-bearing and neither is obvious.* It does
147
+ not use `findFiles`, because `SKIP_DIRS` excludes `changes/` and `archive/` —
148
+ correctly, for a normal scan — while `tsc` and `eslint` are handed `self/**`
149
+ and walk straight into both, and the import this exists for was in an
150
+ *archived* delta; the engine's own scanner would have produced a gate that
151
+ passes over the only file it was written about, which is what the first draft
152
+ of it did. And it covers the repo root as well as the four directories
153
+ `tsconfig.typecheck.json` includes, because `vitest.config.ts`,
154
+ `vitest.self.config.ts` and `vitest.consumer.config.ts` are named by no
155
+ `include` in the tree yet Vitest resolves them **first**, before it can
156
+ collect a single test — a published specifier there fails CI earlier and
157
+ harder than one under `self/`, and the directory-shaped version of this gate
158
+ would not have looked. Each half is pinned by an instrument guard, so a walk
159
+ that quietly stops reading `examples/` or the root fails on the guard rather
160
+ than passing empty.
161
+
162
+ - **A product identity, as delivery artifacts.** `assets/brand/` carries the
163
+ mark, four icon sizes and four lockups as SVG, with `assets/brand/README.md`
164
+ as the guide: the geometry grid, the mortise-and-tenon overlap that must not
165
+ become clearance, the scaling rules, the type stack and the palette. The
166
+ README header and `package.json`'s keywords are the rest of it.
167
+
168
+ **Nothing in `src/` reads any of it, and that is the shape of the change
169
+ rather than a stage it has not reached yet.** A CLI half was built and dropped
170
+ in the same session — a palette module plus a `--help` banner — and the
171
+ argument that closed it is filed under *Considered and rejected*.
172
+
173
+ ### Changed
174
+
175
+ - **Three deprecated call sites retired, found by turning
176
+ `@typescript-eslint/no-deprecated` on the whole typed tree rather than by
177
+ reading the diff.** Neither was reachable by the repo's standing gates: both
178
+ compiled, linted and ran green, because a deprecation is a JSDoc tag and
179
+ nothing in `eslint.config.js` was asking for it.
180
+
181
+ `src/core/schema.ts` passed `{ message: … }` to the `statement` refinement.
182
+ Zod 4 deprecated that parameter in favour of `error`, and the rest of the file
183
+ had already moved — `paramValue`, `RegistryRecord` and `RegistrySchema` all
184
+ spell it `error` — so this was the one survivor of an incomplete migration,
185
+ not a considered exception. The wording is unchanged, and by the
186
+ `Issue.message` rule it is prose either way, so nothing observable moved:
187
+ parsed against the built schema, the refusal still comes back with the same
188
+ `message` under the same `custom` code. Design §5.1 quotes this schema in both
189
+ languages and carries the same edit.
190
+
191
+ `tests/registry-types.spec-d.ts` used `expectTypeOf(…).toMatchTypeOf<…>()`,
192
+ deprecated in expect-type 1.2.0 and split into two successors. Both sites
193
+ assert *assignability to a supertype* — `Registry` for the whole registry,
194
+ `Requirement` for one entry — which is `toExtend`, the successor that keeps
195
+ the `extends` relation. `toMatchObjectType` is the other one and would have
196
+ been the wrong reading: it checks a strict subset of keys, which is a
197
+ different claim than the one these two tests were making.
198
+
199
+ `eslint.config.js` called `tseslint.config()`, deprecated in
200
+ typescript-eslint 8.65 now that ESLint core provides the same `extends`
201
+ flattening via `defineConfig()`. Swapped for `defineConfig` + `globalIgnores`
202
+ from `eslint/config`. Because both spellings lint clean, *clean was not
203
+ evidence*: equivalence was established by diffing `eslint --print-config` for
204
+ a file in each of the five trees across the swap — byte-identical — plus the
205
+ linted file set and problem count, 166 and 0 on both sides.
206
+
207
+ **The third one is the interesting one, because the first sweep structurally
208
+ could not see it.** `no-deprecated` is type-aware, so it only reaches files
209
+ some project includes — and `tsconfig.typecheck.json` includes `src`, `tests`,
210
+ `self` and `examples`, which leaves `eslint.config.js`, `bin/attest.js` and
211
+ the three `vitest*.config.ts` named by no `include` in the tree. This is the
212
+ same blind spot `tests/import-boundary.spec.ts` was written about from the
213
+ other direction: the root config files are read *first* by the tooling and
214
+ belong to no project last. Re-run against a throwaway project that includes
215
+ them, the remaining four are clean — and that result was itself checked
216
+ against the failure shape this repo keeps meeting, by re-running the scan on
217
+ the pre-fix `eslint.config.js` and confirming it goes red, since a scan
218
+ matching no files is also silent.
219
+
220
+ With that gap closed — and the installed dependency tree carrying no registry
221
+ deprecation, the adopter-facing samples inside `skill.ts` and every code fence
222
+ in `docs/` and the README checked against the deprecated identifiers harvested
223
+ from the dependency typings, `fixtures/**` scanned under a throwaway project,
224
+ and the suite run under `--pending-deprecation` surfacing no runtime `DEP`
225
+ warning — this is the whole inventory as of Zod 4.4, expect-type 1.4,
226
+ typescript-eslint 8.65, Vitest 4.1, TypeScript 5.9 and Node 24.
227
+
228
+ **Two halves of that are unmeasured rather than clean, and are named here
229
+ rather than left to be inferred from the version list.** `package.json`
230
+ declares `typescript` as `^5.5.0 || ^6.0.0` while 5.9.3 is what is installed,
231
+ so anything TypeScript 6 deprecates has not been looked at; and `engines`
232
+ allows Node 20.19 while the sweep ran on 24, so a `DEP` warning that fires
233
+ only on an older runtime would not have appeared. Nothing gates any of it
234
+ going forward — see *Under consideration*.
235
+
236
+ ## [1.1.0] - 2026-09-02
237
+
238
+ ### Added
239
+
240
+ - **`ATX-89`, `ATX-90`, `ATX-91`: `check` reports a workflow document the
241
+ installed version no longer writes (`stale-workflow-doc`, INFO).** The
242
+ document `attest init` writes is a pure function of the installed version, and
243
+ the adopter's tree records nothing about which version wrote it —
244
+ `skillContent()` and `workflowBody()` take no argument, deliberately, because
245
+ that is what makes re-running `init` safe. So the file carries no version
246
+ stamp and a diff against the installed build is the only detector its own
247
+ design permits. Measured on a 0.9.1 → 0.11.0 upgrade report: **110 changed
248
+ lines across 9 hunks**, 403 lines against 458, found only because the reporter
249
+ wrote a script importing `workflowBody()` out of `dist/`, with `check`,
250
+ `cover`, `verify` and `render --check` all green.
251
+ *The level is an asymmetry of obligation rather than a severity judgement, and
252
+ that is the whole argument for it.* Keeping the document current is Attest's
253
+ obligation, because Attest is what changes it; taking the update is nobody
254
+ else's, because an adopter is free to run an older workflow, a modified one, or
255
+ none. A finding whose subject the reader owes nothing about is exactly the
256
+ finding that may only inform — so `ok`, the exit code and every gate are
257
+ untouched, and a reader who ignores this forever pays what they pay today.
258
+ *This is not `init --check`*, which stays rejected below. That gate turns red
259
+ on a diff the person who must commit it has no way to review: a regenerated
260
+ `SPEC.md` shows what **you** changed about your promises, and a regenerated
261
+ `SKILL.md` shows what Attest's authors changed. It also fires in CI, where
262
+ nobody can run `init`, blocking whoever pushes next for an upgrade somebody
263
+ else made.
264
+ *Three requirements because there are three ways to be wrong, and this file
265
+ already carves the identical problem up this way for renderings.* `ATX-89` is
266
+ the report; `ATX-90` is that an **absent** document draws nothing, which is the
267
+ one the obvious implementation breaks — `staleIssue` reports a rendering that
268
+ was never generated under its own code, and copying that here would fire on two
269
+ of this repository's own three targets and on the majority of adopters, since
270
+ `init` writes one target by default and nothing obliges a project to run it;
271
+ `ATX-91` is that the comparison is of the document rather than the bytes, which
272
+ is deliberately the **opposite** of `ATX-84` and could not be had by widening
273
+ `ATX-29`. `ATX-84` compares this repository's copy byte-for-byte and may,
274
+ because `.gitattributes` here is committed and pins `eol=lf` — that file is
275
+ Attest's here and nobody's in an adopter's tree, so one rule cannot cover both.
276
+ *What it costs, measured before it was built:* 0.234 ms for all three target
277
+ comparisons, plus 4.19 ms to import `targets.ts` and the 26 KB template behind
278
+ it, against a `check self --json` of 814–1,183 ms — **~0.5%**. No
279
+ `SCHEMA_VERSION` bump: the field exists, the value is new, and `ok` is
280
+ untouched because INFO is below `hasError`.
281
+ *Two things the build found that the proposal did not contain.* The fix hint
282
+ had to name what `init` does to the file and not only the command — `runInit`
283
+ writes with `writeAtomic`, no merge and no backup, so an adopter who edited
284
+ these instructions is being pointed at something destructive. `targets.ts`
285
+ rule 1 makes that defensible, Attest owning the path being the whole reason it
286
+ may overwrite, and it was nowhere stated to the reader. And the two negative
287
+ scenarios had to be **paired with the case that must fire**, in the same
288
+ scenario, because an absence assertion alone is green in a tree where nothing
289
+ is implemented — which is the defect `never-red` exists to name, arriving in
290
+ the scenarios written to attest a rule about not firing.
291
+ *One cost that is not paid and is stated rather than left to be discovered:*
292
+ this repository cannot dogfood it. `check` reads the project root, which here
293
+ is `self/`, while the document sits at the repository root — so `check self`
294
+ finds it absent and is correctly silent. `ATX-84`'s scenario reaches two
295
+ directories up because it is a test; a command cannot. All three scenarios
296
+ build a temporary project, and `self/` gives this rule none of the continuous
297
+ exercise `ATX-84` gets.
298
+ *What the judgement tables did with the three ids, since that is the
299
+ counter-pressure earning its place rather than a chore paid to it.* All four
300
+ demanded rows and got them. `compound-requirement` **missed `ATX-89`** — two
301
+ obligations, "reports it" and "at that level", and the keyword heuristic sees
302
+ one — which is the "never wrong, nearly blind" reading holding at 18 of 42
303
+ rather than a new finding. `divergent-param` produced a **new false positive
304
+ and re-keyed an old one**: `level` joins `code`, `file`, `flag` and `field` as
305
+ a fifth natural word for a kind of constant, and `level:ATX-60,ATX-89` is the
306
+ second key where the two requirements genuinely *are* related — both about the
307
+ severity ladder and the `hasError` predicate — while the rule relates them for
308
+ a reason that is not that. The echo detector flagged both of `ATX-89`'s params
309
+ and both were measured `independent` by mutation, one `verify self` each,
310
+ taking it to 4 true positives of 11.
311
+ - **`ATX-88`: `tests-red` names the timeout the run applied and how many
312
+ scenarios reached it, so a suite that went red on the clock can be told from
313
+ one that went red on its code.** Filed from a 0.9.1 → 0.11.0 upgrade report
314
+ where `npx vitest run` was 170/170 green on the same tree on which `verify`
315
+ was red two runs in three. The standing answer — run the suite yourself under
316
+ your own reporter — is circular in exactly that case, because the
317
+ disagreement between the two runs *is* the failure; what the reporter did
318
+ instead was time every scenario by hand and cross-reference which of eleven
319
+ over four seconds declared no `timeoutMs`. **This is deliberately not the
320
+ pass-through**, which stays open at the foot of this file: carrying the
321
+ child's failures means new fields, a `SCHEMA_VERSION` question and a third
322
+ party's assertion wording in the field consumers are told not to branch on.
323
+ It is the narrow half that is not blocked — the isolated child run drops the
324
+ config that would have raised the timeout, so *did anything run out the clock*
325
+ is the one question a green direct run cannot answer and this one can, and it
326
+ needs nothing the report does not already carry. A count and never a list, for
327
+ the same reason.
328
+ *The number is read from the resolved child config rather than written down,
329
+ and that is the half a constant would have got wrong.* It is the one value
330
+ here Attest does not choose: Vitest's own default when the run is isolated,
331
+ the supplied config's value when `--vitest-config` raised it. `ATX-88.params`
332
+ pins the isolated default at 5000 because two documents quote it in prose —
333
+ the workflow document `init` writes and the troubleshooting page — and until
334
+ now nothing would have failed if a Vitest release moved it.
335
+ *Detected by duration and never by the child's error text.* Vitest does say
336
+ "Test timed out in 5000ms." and matching that would be shorter; it is also the
337
+ category error this project refuses in its own diagnostics, since a message is
338
+ prose in somebody else's package too. Measured rather than assumed: a scenario
339
+ that reaches the ceiling ends `fail` a hair past it — 5015 ms against 5000 —
340
+ while an assertion failure ends `fail` in under a millisecond, so `>=` needs no
341
+ tolerance and a tolerance would start claiming timeouts for slow assertions
342
+ that failed honestly. A missing duration reads as *not* a timeout, which is the
343
+ direction that fails quietly; the other reports every red as a timeout, which
344
+ is a wrong diagnosis printed with confidence.
345
+ *Stated over the run rather than over the two commands that render it*, unlike
346
+ `ATX-51` beside it, and by this project's own rule rather than by preference:
347
+ what `verify` and `archive` add is a sentence in `Issue.message`, which is
348
+ prose and never API, so a scenario asserting it would be asserting wording.
349
+ Additive to `--json` only in the sense that nothing there moved at all — no
350
+ field, no code, no `schemaVersion` question.
351
+
352
+ - **`examples/judgement-table/` states the counting rule, which is the one piece
353
+ of content that decides nothing and without which two tables cannot be
354
+ compared.** Decided under `Planned` on 2026-08-31 on the first table built from
355
+ that example, which distrusted its own obligation column above all its others
356
+ and said why: two tables that count differently are not a second measurement of
357
+ the same thing, they are a second vocabulary. The rule is not a column and
358
+ answers nothing — what a requirement carries is still the adopter's judgement —
359
+ so it sits on the same side of the split as the two mechanical properties, and
360
+ was being withheld along with the answers, which was the wrong side. It fixes
361
+ the test (two clauses are two obligations when an implementation can satisfy
362
+ one completely and fail the other), the six kinds of clause that read as a
363
+ second obligation and are not (scope, criterion, purpose, the other side, a
364
+ consequence, and the states a scenario happens to construct), the
365
+ one-promise-per-emitted-artifact case, and the tiebreak — perturb one half and
366
+ see whether the other half's scenario stays green. Extracted from the labels in
367
+ `tests/intent-rule-candidates.spec.ts` rather than invented, which is why it is
368
+ a description of a rule already in use and not a proposal. It also names what it
369
+ does **not** ask: whether the obligations belong under one id is a different
370
+ question, and keeping the two apart is what lets the count stay a measurement.
371
+
372
+ ### Changed
373
+
374
+ - **The example's anchor is no longer a field to be typed, because the first
375
+ table built from that file filled it in for all 80 of its rows mechanically.**
376
+ `Judgement` shipped with an optional `pin`, documented as not a judgement
377
+ column and nevertheless the only concrete field in the interface — and a field
378
+ gets populated. Copying 80 scenario names out of the AST and back into the
379
+ table is not judgement, and it is exactly the form-filling the empty frame
380
+ exists to prevent. `pin` is now out of `Judgement` entirely and sits in a
381
+ sibling `PINS` map, and an anchor on an id declaring fewer than two scenarios is
382
+ **refused**: with one scenario the anchor is a function of the project,
383
+ `inspectProject` already computes it, and the line naming it is transcription
384
+ wearing a judgement's clothes. What survives is the case where the anchor is a
385
+ real choice, which is the case a machine cannot make. *The cost is named rather
386
+ than hidden:* a sole scenario deleted and replaced under a different name is a
387
+ revisit `PINS` will not force. That is what not asking anyone to transcribe
388
+ costs, and it is paid where a rename cannot change which scenario the row meant.
389
+ - **The frame's emptiness is now typed, which the obvious implementation of this
390
+ fix would have left unenforced.** Deleting `pin` leaves `interface Judgement {}`,
391
+ and an empty interface is not an empty row type — it admits `0` and `""`, so
392
+ every row could quietly acquire anything at all while the file claimed to ship
393
+ no fields. `Record<string, never>` is the type that means what the sentence
394
+ says. Caught by `eslint` rather than by reasoning, which is `How an entry is
395
+ re-measured`'s question about what a repair makes reachable arriving one step to
396
+ the side of where it was expected. Measured both directions: a row carrying an
397
+ undeclared field does not compile, and replacing the line with an interface
398
+ holding one **required** column errors on every existing row.
399
+ - **The example's own subject gained a second scenario**, so `EX-1`'s anchor is a
400
+ choice among several rather than a value the machine could have derived — the
401
+ refusal above has nothing to demonstrate on a project where every id declares
402
+ one scenario. It is also the counting rule standing on its own subject: two
403
+ scenarios, one obligation, because "before it is stored" is the criterion for
404
+ "rejected" and not a second promise. All three assertions in the anchor test
405
+ were shown to fail before this was called done — a derivable anchor, a renamed
406
+ scenario, and an anchor with no row.
407
+
408
+ ### Documentation
409
+
410
+ - **Three places carried a backstop claim that is now false — `ATX-84`'s
411
+ rationale, `skill.ts`'s header, and design §9 in both languages — and all
412
+ three withdraw it.** The claim is that every mistake a stale workflow
413
+ document can cause is a diagnostic carrying a fix hint the agent corrects
414
+ itself from, and it is one of the two arguments for shipping no freshness gate
415
+ to adopters. An upgrade report measured the counterexample: a
416
+ copy six weeks and one upgrade behind still described `never-red`'s ordering
417
+ rule, which `[0.10.0]` archived as `evidence-not-ordering` because the
418
+ ordering was never in the engine. An agent reading it does something the
419
+ engine does not require — more conservative, not wrong-shaped — so **no issue
420
+ fires at any severity** and there is nothing to correct itself from. The
421
+ backstop covers a stale document producing bad input; it does not cover one
422
+ producing unnecessary work, and the second is what the drift produced.
423
+ *All three, because they are one argument stored three times and this
424
+ repository's whole subject is what happens when those disagree.* Two were
425
+ known from the entry that priced this; the third was found by grepping for the
426
+ claim rather than for the files that discuss it, and design §9 is the copy
427
+ with the widest audience. `ATX-84`'s rationale cites "the decision in
428
+ `skill.ts`", which cites the design — so correcting any one of them alone
429
+ leaves a citation pointing at a document that says something else. The
430
+ workflow document's own bytes are untouched — the claim lives in the module
431
+ header, above `BODY` — so no `init` re-run is owed and `ATX-84` stays green
432
+ byte-for-byte.
433
+ *What does not change is the requirement.* `ATX-84` gates this repository's
434
+ copy and is not adopter-facing, and the surviving objection — that a gate
435
+ charges every adopter a diff they did not ask for — carries it alone. What the
436
+ withdrawal reopens is the adopter-facing question, which is priced under
437
+ `Under consideration` rather than restated in a rationale, since a second copy
438
+ of an argument that is still moving is the failure this entry is about.
439
+ - **The workflow document named one of the two things the isolated child run
440
+ withholds, and the troubleshooting page named both.** Re-measuring this before
441
+ building it is what found that: the ask was filed as "have the workflow
442
+ document say that a scenario near the default needs an explicit `timeoutMs`",
443
+ and that sentence already existed — in `tests-red`'s troubleshooting section,
444
+ worked example included. What did not exist was the pointer. The workflow
445
+ document's own bullet on why a green direct run does not predict the gate lists
446
+ aliases, transforms and a DOM, and stops there; an agent reading it was told
447
+ about the environment and not about the clock, while the section it links to
448
+ covers both. So what was owed was a second bullet rather than a new
449
+ explanation, which is cheaper than the entry priced it.
450
+ - **`tests-red`'s troubleshooting section stops saying the report carries
451
+ nothing about the failure**, in both languages, because it now carries exactly
452
+ one thing. The sentence that had to change is the one that reads as a general
453
+ promise — "not from this report, because it does not carry them" — and the
454
+ worked example beside it says which half crossed and why the other half is a
455
+ decision rather than an omission. History rows on `verify` and `archive` in
456
+ both references, since a reader on 1.0.0 gets the older report.
457
+ - **`CLAUDE.md`'s "it will not tell you why" gotcha gains its exception.** The
458
+ gotcha is still true of the file, the scenario and the assertion; it is no
459
+ longer true of the clock, and a gotcha that overstates itself is how somebody
460
+ stops reading the report.
461
+
462
+ - **`intent-quality.md` step 2 points at the counting rule, and says why that
463
+ one pointer does not contradict the page's own sequencing.** The page asks a
464
+ reader to do step 2 before reading our table, because a table built by someone
465
+ who read ours first is an echo of ours. A rule that fixes the *unit* is the
466
+ opposite case — sharing it is what makes a second table comparable, where
467
+ sharing an answer is what makes it a copy — and a page that withheld both
468
+ without distinguishing them would have been asking for an incomparable
469
+ measurement on principle. Both languages, and a line added to *Read our table
470
+ after step 2, not before* so the exemption is stated where the rule it excepts
471
+ is. The History row is owed for the second reason rather than the first: step 4
472
+ tells a reader to **copy** `examples/judgement-table/`, and that file changed
473
+ under this version, so a copy taken at `[1.0.0]` is no longer the file this
474
+ page describes — which is exactly what that table is for and the kind of row
475
+ easiest to miss, because nothing on the page announces that it hands out a
476
+ file.
477
+
93
478
  ## [1.0.0] - 2026-09-01
94
479
 
95
480
  ### Added
@@ -6506,211 +6891,6 @@ re-measuring an entry before building it is at the foot of the file, under
6506
6891
  `How an entry is re-measured` — it applies to all three, and was filed here only
6507
6892
  because the first of it was written here.
6508
6893
 
6509
- - **An outward-facing form of the judgement tables, so an adopter can run this
6510
- counter-pressure on their own registry.**
6511
- `tests/intent-rule-candidates.spec.ts` maintains four hand-judged tables over
6512
- this repo's registry and enforces their completeness. Design §11 records the
6513
- properties they defend and that nothing enforces any of them; what the tables
6514
- add is a counter-pressure holding for **one** registry whose authors agreed to
6515
- maintain it. An adopter got none of it, and the reason was sharper
6516
- than "nobody wrote it for them": the package exported `requirement`,
6517
- `scenario`, `defineRequirements`, `delta` and types, and nothing else.
6518
- `parseSpecs`, `findFiles` and `readRegistrySource` were internal, so an
6519
- adopter **could not enumerate their own scenario names** — the table's central
6520
- rot check, that a claimed scenario is one a spec really declares, was not
6521
- merely unwritten downstream but unreachable. **That half shipped under
6522
- `[0.9.2]` as `@am_shork/attest/inspect`** and is recorded at the foot of
6523
- this entry; what stays here is the schema built on top of it, and the three
6524
- blockers below are untouched by the export.
6525
- *The shape that makes it shippable at all is a split, not a feature.* The
6526
- engine can compute which ids exist, which statements match the universal
6527
- quantifier regex, which params are list-valued, and whether a named scenario
6528
- is declared — all four already, all statically. It cannot compute how many
6529
- obligations a requirement carries, what a quantifier ranges over, or whether a
6530
- scenario iterates a param. So the shippable object is **the enforcement, never
6531
- the judgement**: an opt-in `*.judge.ts` read by the same static reader and
6532
- under the same literal-only rule the registry is (it is metadata about
6533
- requirements under review, so `check` must execute nothing to read it), whose
6534
- contents are entirely the adopter's. Opting in is creating the file; once it
6535
- exists, `requirement-unjudged`, `orphan-judgement`, `unbound-site` and
6536
- `quantifier-unjudged` make it complete and keep it from rotting. The
6537
- asymmetry is the only way this can exist without Attest asserting a ground
6538
- truth it refuses to compute (§0), and it is the same asymmetry `never-red`
6539
- already runs on: the archive gate would owe judgement only for ids a change
6540
- ADDs, so a brownfield adoption is not blocked wholesale.
6541
- *One part of the existing table cannot ship in any form.* `DOMAINS.pin` comes
6542
- in two kinds — a scenario name, checkable against the AST, and a **source-text
6543
- fragment** grepped out of the spec file. The second is asserting on the
6544
- wording of somebody's test, which is the `Issue.message` category error under
6545
- another name. Only the scenario-name kind is exportable, and what that costs
6546
- is real: the grep is what caught a deleted pin that `toContain` had passed.
6547
- *Why no detector ships with it, and this is the part that is already
6548
- measured.* Three rule families were built and scored against the whole corpus
6549
- (86 entries, counting a fixture id once per registry file) before any was
6550
- written into the engine, which is the order `divergent-param` was originally
6551
- designed in the wrong way round. Every figure below is a quotation of the
6552
- suite's inline snapshots; the snapshot is the fresh copy, and this paragraph
6553
- is only ever as fresh as its last edit.
6554
- **(1) `compound-requirement`** — 34 requirements carry more than one
6555
- obligation, it flags 16, is **wrong about none**, and misses 18. Never wrong
6556
- and nearly blind, and the 18 are the form the coverage incentive actually
6557
- rewards: extra clauses under a single keyword. **(2) The widened variant** —
6558
- 29 correct, but 14 wrong and still 5 missed, and it is silenced by deleting an
6559
- "and". A rule whose cheapest fix degrades the artifact it protects does not
6560
- ship, and its precision falls as the registry grows. **(3) `divergent-param`**
6561
- — 5 divergences on this repo, **all 5 judged wrong**, zero true positives ever
6562
- recorded. The `code` group now spans 19 ids, and the failure is structural
6563
- rather than a matter of naming: the key is built by discarding the evidence of
6564
- relatedness, so the one genuinely paired divergence in the group is the one it
6565
- cannot show, and the key has churned three times under `flag` on additions
6566
- that changed no verdict. A fourth candidate — the echo detector, scored at
6567
- half precision — belongs to the tautology-detector entry now under
6568
- *Considered and rejected* and is not re-quoted here, because a figure stated in two places goes stale in one. Taken together
6569
- these are the measured range of what a token or single-body detector over
6570
- prose can do here, and they are why the proposal is enforcement-only.
6571
- *What blocks the decision — three things.* **(1) One adapter.** This
6572
- repository's table is the only one that exists, and its author wrote both the
6573
- schema and every answer in it. By this project's own rule a second table is
6574
- what makes the seam real, and freezing a `Judgement` type now is deciding for
6575
- everyone from 34 requirements written by one person. **(2) The breaking form
6576
- of this was already rejected** — see *Countable obligations* below — and one
6577
- of its three reasons survives the separate-file shape intact: **nothing can
6578
- force the count to be true.** The other two do not survive, which is what is
6579
- new here: a separate artifact touches no `--json` field, no delta apply and no
6580
- `render` output, and completeness *across ids* is computable in a way
6581
- completeness *within* an obligation array never was. So this is a re-proposal
6582
- with one of three objections answered and the sharpest one standing.
6583
- **(3) `quantifier-unjudged` reuses a rejected detector as its trigger.**
6584
- Firing on the quantifier token was rejected below at 43% reach and 70% wrong;
6585
- the claim here is that the same regex is acceptable when it asks a question an
6586
- author discharges in one line (`over: 'values'`) rather than issuing a
6587
- verdict. That claim is untested on anyone who did not also write the regex.
6588
- *The half that is not blocked has shipped, under `[0.9.2]`.*
6589
- `@am_shork/attest/inspect` is one function answering what a project declares —
6590
- AST-only, no runner peer, landing on the existing static/executing boundary.
6591
- It ships no schema and decides nothing, which is why it could go ahead of the
6592
- three blockers rather than waiting on them. What it changes about this entry
6593
- is the *kind* of thing that is now missing: the mechanism is no longer
6594
- unreachable, only unwritten, so a second table is something an adopter can
6595
- build rather than something only this repository could.
6596
- *What would decide it:* one judgement table maintained by someone who did not
6597
- design it. If it comes back the same shape, the seam is real and the schema
6598
- follows; if it comes back different, `inspect` was the whole answer and the
6599
- schema never needs to exist. *(One came back on 2026-08-31, and the disjunction
6600
- above is too clean to survive it: partly the same shape, partly not, and the
6601
- same half is the contaminated half. The close of this entry is where that is
6602
- read.)*
6603
- **Nothing currently makes that table arrive, which is a separate problem from
6604
- the three blockers and the one that gates all of them.** Raised in a product
6605
- review of `[0.9.1]`–`[0.9.3]` on 2026-08-19. The deciding evidence is an
6606
- artifact only an adopter can produce, and the path to producing it is: a row in
6607
- the README's document table, then a 214-line page, then a hand pass over four
6608
- questions, then writing the two mechanical properties from a prose
6609
- description, then choosing to report back. Five voluntary steps with no
6610
- scaffold at any of them, against a decision this entry has been waiting on
6611
- since `[0.9.2]`. An entry blocked on evidence that no shipped surface asks for
6612
- is blocked indefinitely, and the wait is not itself evidence of anything.
6613
- *The proposal is scaffolding the mechanism while still withholding the answer,*
6614
- which is the same split `[0.9.2]` already made and not a retreat from it: an
6615
- `examples/` directory holding the completeness and rot-binding assertions with
6616
- **no columns** — the enumeration that forces a row, and the check that a named
6617
- scenario is one `inspectProject` really declares — and the judgement columns
6618
- left for the adopter to add. What the page withholds is this repository's four
6619
- columns, and that stays withheld; what it currently also withholds is the two
6620
- properties it names as *mechanical*, and those are the half where a copy is not
6621
- an echo, because their content is fixed and the fourth finding does not apply
6622
- to them.
6623
- *What blocks it.* Whether a skeleton with no columns is still a shape that
6624
- comes back as a copy — the page's argument is that a template makes the answer
6625
- return its own question, and it is untested whether an adopter reads an empty
6626
- frame as an invitation or as a form to fill in. And it is a second artifact
6627
- claiming to be runnable consumer usage of the public API, which `pre-commit-check`
6628
- §6 already records as the category no gate reaches: the page's three samples
6629
- are unasserted today, and a fourth in a directory that looks executable would
6630
- be worse than a fenced block, not better, unless something runs it.
6631
- **Built under `[0.11.0]`, and the second of those two blockers was the
6632
- buildable one.** `examples/judgement-table/` is in the default suite, the
6633
- specifier is aliased rather than relativised so the file stays copyable
6634
- verbatim, and both assertions were shown to fail before it was called done.
6635
- So the category `pre-commit-check` §6 names now has exactly one member that a
6636
- gate reaches, and the page's three prose samples are still not it.
6637
- *Two things the build found that the proposal did not contain.* A third
6638
- assertion — `issues` empty, before either of the other two — which is not a
6639
- refinement but a hole: an unread registry file and a smaller project are the
6640
- same value, and the two completeness directions pass over both. It was found
6641
- by an invalid mutation during the bite test rather than by reasoning, which is
6642
- `How an entry is re-measured`'s question about what a repair makes reachable,
6643
- arriving from the other end. And a **type-level** forcing move the runtime
6644
- assertion cannot make: a required field added to `Judgement` fails every
6645
- existing row at compile time, so the schema-shaped half of "a row cannot
6646
- arrive unjudged" costs one line and no engine rule. That is worth noting
6647
- against blocker **(1)** above, which is about freezing a `Judgement` type:
6648
- what shipped freezes nothing, because the type is in the adopter's copy.
6649
- **What is untouched is the first blocker, and it is now the only thing between
6650
- this entry and its evidence.** Whether an empty frame reads as an invitation
6651
- or as a form to fill in is still untested, and cannot be tested here — the
6652
- people who would answer it are the ones who have not arrived. What the build
6653
- changes is only that the five voluntary steps are now four, and the one
6654
- removed was the one with a fixed answer. If the next report is still nobody,
6655
- the scaffold was not what was missing, and that is worth knowing too.
6656
- **Decided 2026-08-31: build the counting rule, and not the schema.** The
6657
- blocker was whether an empty frame reads as an invitation or as a form to fill
6658
- in, it could not be tested here, and a 0.9.1 → 0.11.0 upgrade report from
6659
- `mine-capablanca` tested it. The answer is **invitation**, and the report names
6660
- the sentence that did it: not the empty `interface Judgement`, which reads as a
6661
- stub and sent the reporter looking for what belonged in it, but the comment
6662
- above it — *"It ships with no columns, and that is the finished state of this
6663
- file rather than a gap in it."* Declaring the absence finished is what turns a
6664
- blank into a question, and it is the cheapest sentence in the example.
6665
- *The forcing move was one the build found rather than one it proposed.* The
6666
- instruction to add the first column as a **required** field made all 80 rows
6667
- stop compiling at once, and there was then no path back to green that did not
6668
- go through having an opinion about all 80 — *"a form gets partially filled in;
6669
- a type does not"*. That is the type-level forcing move described above,
6670
- working on somebody who did not write it.
6671
- *And the frame leaks in one place, which is a defect in the example rather
6672
- than in the idea.* `pin` is documented as not a judgement column and is the
6673
- only concrete field in the interface, so the reporter's first act was to
6674
- populate it for all 80 rows mechanically — *"there is a field, populate it"*.
6675
- Defensible, since it makes rot-binding bite for 80 rows instead of none, and
6676
- exactly the form-filling behaviour the frame exists to prevent. Generating
6677
- `pin` from `inspectProject` rather than leaving a field to be typed is the fix,
6678
- and it belongs in the example.
6679
- *What does **not** follow, and the report is why it does not.* Two of the three
6680
- columns that came back are the same shape as two of this repository's —
6681
- `clauses` is `OBLIGATIONS`, `quantifier` is a flattened `QUANTIFIED` — and it
6682
- is tempting to read that as the seam being real. It cannot be read that way:
6683
- the reporter names the `attest init` workflow document as prior influence, and
6684
- that document discusses quantifiers and param pinning at length. So the
6685
- convergence says **something** transmits those two ideas, and the document is
6686
- at least as likely a channel as the frame. What is uncontaminated is the third
6687
- column, `venue`, which has no counterpart here: nine of its rows record a
6688
- promise held by a Playwright suite Attest's scope does not reach. A column this
6689
- repository could not have written came back, which is a seam behaving like one.
6690
- *What to build is smaller than a schema, and the three things the reporter
6691
- wanted and did not get are its specification.* **(1) A counting rule for
6692
- "obligation" precise enough that two people count the same.** The report
6693
- distrusts its own `clauses` column above all the others and says why: two
6694
- tables that count differently are not comparable, which defeats the point of
6695
- there being a second one. A counting rule is content that does **not** depend
6696
- on the adopter — the same category as the two mechanical assertions — and it is
6697
- currently withheld along with the columns, which is the wrong side of the
6698
- split. **(2) A way to bind a clause to a scenario rather than a row.**
6699
- Rot-binding binds a row to *a* scenario; nothing binds the sixth obligation of
6700
- a six-clause requirement to anything, so a six-clauses-three-scenarios row is a
6701
- number a reader has to notice instead of an assertion that fails. **(3)
6702
- Somewhere for a row to say the promise is held where Attest cannot see it.**
6703
- Nine `ui-node` rows and a 902-line Playwright suite, citable only in prose.
6704
- *So what is planned is (1) and the `pin` fix, and (2) and (3) explicitly are
6705
- not.* (1) is a paragraph in the example, costs no engine surface, and is the
6706
- one thing that makes a second table comparable to the first — without it two
6707
- tables are two vocabularies and the seam cannot be read either way. (2) and (3)
6708
- are both new schema, and blocker **(1)** of the proposal above — one adapter —
6709
- is not answered by a report that is the second adapter's first hour. Two hours
6710
- of one agent is not two adapters. **The `*.judge.ts` schema stays undecided**,
6711
- and when it moves it will move on a second table that somebody has maintained
6712
- rather than built, which is a different measurement from the one that arrived.
6713
-
6714
6894
  ## Under consideration
6715
6895
 
6716
6896
  Candidates with **no decision yet**, between the two sections either side of it
@@ -6939,6 +7119,31 @@ have to gather again.
6939
7119
  *What would move it:* a red `verify` on an adopter's tree whose cause is not a
6940
7120
  timeout. Every instance anybody has is this one.
6941
7121
 
7122
+ **Blocker (3)'s two cheap answers are built under `[1.1.0]`, and the
7123
+ entry stays open on the other two.** `ATX-88` has `tests-red` name the timeout
7124
+ that applied and how many scenarios reached it, and the workflow document now
7125
+ says the isolated run drops your `testTimeout` along with the rest of your
7126
+ config. Neither touches (1) or (2): no field was added, no `SCHEMA_VERSION`
7127
+ question was asked, and no assertion text from the child crossed into an
7128
+ `Issue`. What the entry proposes — passing the child's failures through — is
7129
+ unchanged and undecided.
7130
+ *Two things the re-measure corrected, and one is the second cheap answer's
7131
+ whole shape.* The workflow-document line was filed as content to write, and it
7132
+ was already written: `tests-red`'s troubleshooting section carries it, worked
7133
+ example included. What was missing was a pointer from the workflow document's
7134
+ own bullet about why a green direct run does not predict the gate, which lists
7135
+ aliases, transforms and a DOM and stops before the clock. And the first
7136
+ answer's phrasing — "the timeout it **imposed**" — was wrong about who imposes
7137
+ it: Attest sets no timeout, it inherits Vitest's default by isolating, so the
7138
+ number has to be read off the resolved child config rather than stated. That
7139
+ distinction is what makes the answer correct under `--vitest-config` too,
7140
+ where the ceiling is the adopter's.
7141
+ *What this does not do, stated so the entry is not read as smaller than it
7142
+ is.* It answers whether the clock was involved. It does not name the scenario,
7143
+ the file or the assertion, so a red `verify` whose cause is not a timeout is
7144
+ exactly as opaque as it was — which is the case this entry is still waiting on
7145
+ an instance of.
7146
+
6942
7147
  - **Changing the value of an existing param passes no gate, and for seven of
6943
7148
  them there is no record either.** Split out of the tautology entry that
6944
7149
  headed this section on 2026-08-18, and which is now under *Considered and
@@ -7036,6 +7241,15 @@ have to gather again.
7036
7241
  `intent-quality.md`, the four things no gate is looking at already being that
7037
7242
  page's subject and this being a fifth of the same kind.
7038
7243
 
7244
+ **The cheap half is written under `[Unreleased]`, and the decision above is
7245
+ unchanged by it.** `docs/{en,zh}/intent-quality.md` now carries a seventh
7246
+ finding: a param no statement interpolates changes with nothing outside the
7247
+ line to show it, so the discipline that catches an unintended move there is a
7248
+ human reading the registry diff. That costs no engine surface and settles
7249
+ nothing this entry is still waiting on — the WARNING in blocker **(2)** is
7250
+ still a rule with no measured population, and this is the fallback the entry
7251
+ itself named for exactly that state, not a smaller version of the rule.
7252
+
7039
7253
  - **The intent layer is separable from the repository it describes, and the
7040
7254
  verification layer is not — so a third party can audit intent and never
7041
7255
  obtain a verdict.** Filed 2026-08-22 from a design evaluation of using Attest
@@ -7138,6 +7352,124 @@ have to gather again.
7138
7352
  yet followed a stale instruction and paid for it, and the rejection below is
7139
7353
  entitled to that until somebody has.
7140
7354
 
7355
+ **Priced on 2026-09-02, per `How an entry is re-measured`, and the three
7356
+ questions have answers: `check`, `INFO`, and no — the chore is the exit code
7357
+ rather than the line.**
7358
+ *The re-measure corrected the entry's framing of the detector first.* "A diff
7359
+ against the installed build is the only detector the file's own design
7360
+ permits" reproduces exactly, and what it makes reachable is the opposite of
7361
+ how it reads: the comparison needs no version stamp, because `skillContent()`
7362
+ is already inside the process that would report. Measured against this
7363
+ repository's own copy — byte-equal and document-equal at 26,584 bytes both
7364
+ sides — and against the other two targets, which are **absent**, that being
7365
+ the population the notice has to be right about. Cost is **0.234 ms** per run
7366
+ for all three comparisons plus **4.19 ms** to import `targets.ts` and through
7367
+ it the 26 KB template in `skill.ts`, which `check` does not import today;
7368
+ `check self --json` is 814–1,183 ms over three runs, so the notice is **~0.5%**
7369
+ of the command it rides on.
7370
+ *Which command, and the objection to it is answered by precedent rather than
7371
+ by argument.* `check` is the only command an adopter runs unconditionally and
7372
+ is the one the workflow document tells the agent to run. `status` takes a
7373
+ `<change>`, so it is silent during exactly the brownfield adoption whose
7374
+ document is stalest; `cover` and `render` are narrower; `verify` and `archive`
7375
+ are the expensive verdicts, where a non-verdict line is noise at the worst
7376
+ available price. The objection is that this would be `check`'s first finding
7377
+ that is not about the project's intent layer — and it would not be:
7378
+ `compilerIssue()` is the first statement of `runCheck`, and
7379
+ `compiler-unsupported` is a finding about the *installed environment*, at
7380
+ ERROR. What ATX-16 scopes `check` to is executing nothing, never intent only.
7381
+ *What level, and `WARNING` is wrong for a reason the roster states.* All three
7382
+ existing WARNINGs — `rationale-placeholder`, `requirement-open`,
7383
+ `orphan-from-failed-registry` — are defects in the reader's own intent layer
7384
+ that the static half declines to fail on. A stale document is neither a defect
7385
+ nor theirs. `INFO` has exactly one member, `possible-drift`, and it is this
7386
+ shape precisely: something here may be drifting and nothing can prove it cost
7387
+ you anything — which is also what this entry's own unmeasured half says.
7388
+ `ERROR` is `init --check`, and that stays rejected.
7389
+ *The chore question dissolves at that level, on a fact about when the drift
7390
+ appears.* The rejection's standard is a gate that "turns red on a change the
7391
+ user did not make". Drift appears only when the installed version moves, which
7392
+ is a lockfile edit and is a change they made; a fresh clone at a pinned version
7393
+ compares equal and is silent, so the failure mode that forced `render --check`
7394
+ off byte comparison — calling a fresh clone stale — cannot occur here. What
7395
+ makes a thing a chore is the exit code, not the line, and at INFO there is
7396
+ nothing to clear: a reader who ignores it forever pays what they pay today.
7397
+ *The repair makes one thing reachable that the entry does not contain, which
7398
+ is the question the method exists to ask.* `runInit` writes with `writeAtomic`
7399
+ unconditionally — no merge, no backup — and the notice cannot tell a stale file
7400
+ from an edited one. Today that overwrite happens only when the user chooses to
7401
+ run `init`; a notice prompts them into it on the tool's schedule, so the fix
7402
+ hint has to say that `init` rewrites the file rather than merely naming the
7403
+ command. `targets.ts` rule 1 makes that defensible — Attest owns the path, and
7404
+ that is the whole reason it may overwrite — but defensible and unstated are
7405
+ different things.
7406
+ *And the obvious implementation is wrong twice, both times by copying
7407
+ `staleIssue`, which is the reference implementation this entry points at and
7408
+ therefore the one nobody re-reads.* **(a)** It emits `missing-spec-doc` for an
7409
+ absent file. Here an absent file is a project that never ran `init` — two of
7410
+ this repository's own three targets — and firing on it would be the tool
7411
+ scattering findings about editors nobody uses, which is the argument
7412
+ `DEFAULT_TARGET` already makes. Absent must be silent, so this takes one code
7413
+ and not two. **(b)** `sameDocument` normalises CRLF and has to here, for the
7414
+ reason `render --check` needs it: Attest does not own an adopter's
7415
+ `.gitattributes`. That is the **opposite** of ATX-84, which compares this
7416
+ repository's copy byte-for-byte, and whose rationale already argues why the two
7417
+ must differ. The comparisons are deliberately not the same one.
7418
+ *What the re-measure found that belongs to no proposal: ATX-84's rationale
7419
+ carries the standing version of this argument, and one clause of it is now
7420
+ false.* It records that a gate here does not reverse `skill.ts`'s decision
7421
+ against shipping one, on the grounds that "every mistake an old document can
7422
+ cause is a diagnostic carrying a fix hint the agent corrects itself from".
7423
+ That is the clause the `mine-capablanca` report falsified — the withdrawn
7424
+ `never-red` ordering produces unnecessary work rather than bad input, and
7425
+ nothing fires at any severity. **Withdrawn under `[1.1.0]` on 2026-09-02,
7426
+ in all three places that carried it**, since the correction was owed whether or not
7427
+ the notice ships; what the withdrawal leaves standing is the diff-nobody-asked-for
7428
+ objection alone, which is the argument this entry now has to beat.
7429
+ *One cost the entry did not price: this repository cannot dogfood it.* `check`
7430
+ reads the project root, which here is `self/`, and the document is at the repo
7431
+ root — so `check self` would find the file absent and be correctly silent.
7432
+ ATX-84's scenario reaches two directories up because it is a test; a command
7433
+ cannot. The scenario would land on a fixture, and `self/` would give the notice
7434
+ none of the continuous exercise ATX-84's rationale calls the only detector this
7435
+ document has.
7436
+ *What it does not need, priced because the entries around it spend most of
7437
+ their length on exactly this:* no `SCHEMA_VERSION` bump — the field exists, the
7438
+ value is new, and `ok` is untouched because INFO is below `hasError`, the same
7439
+ reading that has held the version at 1 across all 41 codes. One roster entry
7440
+ and one troubleshooting section per language, both made mandatory by
7441
+ `docs-consistency.spec.ts` rather than by remembering.
7442
+ *The one form still worth arguing is the cheapest one, and it loses on the
7443
+ exposure's own shape.* A dim line after the report — `init`'s own "Re-run
7444
+ `attest init` after upgrading", moved to where it is actionable — needs no
7445
+ code, no roster entry and no section in two languages. It is also invisible
7446
+ under `--json`, and the drift in that report was found by a reader writing a
7447
+ script against `dist/`. A detector a machine cannot read reproduces the
7448
+ invisibility it exists to remove.
7449
+
7450
+ **Built under `[1.1.0]` on 2026-09-02, and the entry stays here because
7451
+ what was built is not what it names.** The priced form shipped exactly as
7452
+ priced — `check`, INFO, one code, silent on an absent file, document rather
7453
+ than bytes — as `ATX-89`, `ATX-90` and `ATX-91`. The re-measure corrected none
7454
+ of the three answers, which is worth recording against the two cases in `How
7455
+ an entry is re-measured` where it corrected an entry outright.
7456
+ *What the build added that the pricing did not have is the argument the
7457
+ approver gave, and it is better than the one written here.* This entry argues
7458
+ the level from the negative — a gate is a chore, so do not gate. The reason
7459
+ actually adopted is positive and is an **asymmetry of obligation**: keeping
7460
+ this document current is Attest's obligation, because Attest is what changes
7461
+ it, and taking the update is nobody else's. A finding whose subject the reader
7462
+ owes nothing about is exactly the finding that may only inform. That reaches
7463
+ the same level by a route that does not depend on calling anything a chore,
7464
+ and it is what `ATX-89`'s rationale carries.
7465
+ *What is left of this entry is the gate alone, and it is unchanged.* A notice
7466
+ is not a weaker gate, it is a different artifact: it can be ignored, and by a
7467
+ CI-only adopter who never reads `check` output it certainly will be. That is
7468
+ the cost of the choice rather than an oversight — the trade taken was "never
7469
+ blocks anyone" against "guaranteed to be seen". The thing that would move the
7470
+ gate is what it always was, and the notice does not supply it: an adopter who
7471
+ followed a stale instruction and paid for it. Nobody has.
7472
+
7141
7473
  The rejection, kept as it was written:
7142
7474
 
7143
7475
  `render --check` is the obvious precedent and is the opposite case. A rendering
@@ -7219,46 +7551,230 @@ have to gather again.
7219
7551
  cost somebody a round trip. Without the first, this trades a derivable field for
7220
7552
  an underivable duplicate.
7221
7553
 
7222
- - **A Stop hook that will not let an agent end a turn while `check self` is red.**
7223
- From the same read: `nostics` ships `hooks/consume-diagnostic.mjs`, a Claude
7224
- Code Stop hook that reads the log its dev-server collector writes and returns
7225
- `{"decision": "block"}` with the diagnostic as the reason, so the agent keeps
7226
- working rather than stopping on a red application. Three details there are the
7227
- design and not the plumbing, and are the transferable part: it guards on
7228
- `stop_hook_active` so a blocked stop cannot loop; it feeds back **one**
7229
- diagnostic per turn rather than the whole log; and when it consumes one it drops
7230
- every other entry sharing that code and file, so a fault at twenty call sites is
7231
- one round trip and not twenty.
7232
- *Why the shape fits this repository specifically.* `CLAUDE.md`'s "Before
7233
- committing" and the whole of `pre-commit-check` are discipline: nothing runs
7234
- `check self` unless a person or an agent remembers to, and the repeat-grouping
7235
- that hook does by hand is something `formatIssues` already computes. `.claude/`
7236
- here holds five skills, no hooks and no `settings.json`, so this would be the
7237
- first of its kind rather than an extension of anything.
7238
- *Measured cost, 2026-09-01.* `node bin/attest.js check self --json` runs in
7239
- **1.04–1.57 s** over three consecutive runs; `status self --json` in 0.78 s.
7240
- That is paid at the end of every turn, which is the number to weigh rather than
7241
- the wall clock of a single invocation.
7242
- *What blocks it is the stale-`dist/` trap, and a hook is the worst place to meet
7243
- it.* `bin/attest.js` loads `dist/`, not `src/` the Gotcha this repository
7244
- already keeps so a Stop hook reports on the last build. After a `src/` edit it
7245
- blocks on a diagnostic the working tree has already fixed, or clears while the
7246
- tree is broken, and in both directions it is a gate arguing about a state nobody
7247
- is in. Either the hook builds first, adding seconds to every turn, or it runs the
7248
- engine from source and stops being the thing an adopter could copy.
7249
- *And the larger question it sits in front of.* If this is right here it is right
7250
- for an adopter, which makes it a question about what `attest init` writes rather
7251
- than about `.claude/` in this repository. A hook that can block an agent's stop
7252
- is a much larger commitment than a document, and the rejection quoted in full
7253
- inside the `init --check` entry in this section — the gate on the installed
7254
- skill's freshness turned on exactly the distinction it needs:
7255
- a signal about work the reader actually did, against a chore the tool imposes. A
7256
- block on `check` is the first kind, which is the argument for it, and that
7257
- argument should be made explicitly rather than arrived at by shipping.
7258
- *What would move it.* Running it here, unshipped and repo-local, for a few weeks,
7259
- and counting how often it blocks on something that would otherwise have been
7260
- committed. If that count is zero the discipline was already working and the hook
7261
- is overhead; if it is not, the number is the case for `init` writing one.
7554
+ - **An outward-facing form of the judgement tables, so an adopter can run this
7555
+ counter-pressure on their own registry.**
7556
+ `tests/intent-rule-candidates.spec.ts` maintains four hand-judged tables over
7557
+ this repo's registry and enforces their completeness. Design §11 records the
7558
+ properties they defend and that nothing enforces any of them; what the tables
7559
+ add is a counter-pressure holding for **one** registry whose authors agreed to
7560
+ maintain it. An adopter got none of it, and the reason was sharper
7561
+ than "nobody wrote it for them": the package exported `requirement`,
7562
+ `scenario`, `defineRequirements`, `delta` and types, and nothing else.
7563
+ `parseSpecs`, `findFiles` and `readRegistrySource` were internal, so an
7564
+ adopter **could not enumerate their own scenario names** — the table's central
7565
+ rot check, that a claimed scenario is one a spec really declares, was not
7566
+ merely unwritten downstream but unreachable. **That half shipped under
7567
+ `[0.9.2]` as `@am_shork/attest/inspect`** and is recorded at the foot of
7568
+ this entry; what stays here is the schema built on top of it, and the three
7569
+ blockers below are untouched by the export.
7570
+ *The shape that makes it shippable at all is a split, not a feature.* The
7571
+ engine can compute which ids exist, which statements match the universal
7572
+ quantifier regex, which params are list-valued, and whether a named scenario
7573
+ is declared all four already, all statically. It cannot compute how many
7574
+ obligations a requirement carries, what a quantifier ranges over, or whether a
7575
+ scenario iterates a param. So the shippable object is **the enforcement, never
7576
+ the judgement**: an opt-in `*.judge.ts` read by the same static reader and
7577
+ under the same literal-only rule the registry is (it is metadata about
7578
+ requirements under review, so `check` must execute nothing to read it), whose
7579
+ contents are entirely the adopter's. Opting in is creating the file; once it
7580
+ exists, `requirement-unjudged`, `orphan-judgement`, `unbound-site` and
7581
+ `quantifier-unjudged` make it complete and keep it from rotting. The
7582
+ asymmetry is the only way this can exist without Attest asserting a ground
7583
+ truth it refuses to compute (§0), and it is the same asymmetry `never-red`
7584
+ already runs on: the archive gate would owe judgement only for ids a change
7585
+ ADDs, so a brownfield adoption is not blocked wholesale.
7586
+ *One part of the existing table cannot ship in any form.* `DOMAINS.pin` comes
7587
+ in two kinds a scenario name, checkable against the AST, and a **source-text
7588
+ fragment** grepped out of the spec file. The second is asserting on the
7589
+ wording of somebody's test, which is the `Issue.message` category error under
7590
+ another name. Only the scenario-name kind is exportable, and what that costs
7591
+ is real: the grep is what caught a deleted pin that `toContain` had passed.
7592
+ *Why no detector ships with it, and this is the part that is already
7593
+ measured.* Three rule families were built and scored against the whole corpus
7594
+ (104 entries as of 1.1.0, counting a fixture id once per registry file) before any was
7595
+ written into the engine, which is the order `divergent-param` was originally
7596
+ designed in the wrong way round. Every figure below is a quotation of the
7597
+ suite's inline snapshots; the snapshot is the fresh copy, and this paragraph
7598
+ is only ever as fresh as its last edit.
7599
+ **(1) `compound-requirement`** — 42 requirements carry more than one
7600
+ obligation, it flags 18, is **wrong about none**, and misses 24. Never wrong
7601
+ and nearly blind, and the 24 are the form the coverage incentive actually
7602
+ rewards: extra clauses under a single keyword. **(2) The widened variant** —
7603
+ 36 correct, but 14 wrong and still 6 missed, and it is silenced by deleting an
7604
+ "and". A rule whose cheapest fix degrades the artifact it protects does not
7605
+ ship, and its precision falls as the registry grows. **(3) `divergent-param`**
7606
+ — 9 divergences on this repo, **all 9 judged wrong**, zero true positives ever
7607
+ recorded. The `code` group now spans 22 ids, and the failure is structural
7608
+ rather than a matter of naming: the key is built by discarding the evidence of
7609
+ relatedness, so the one genuinely paired divergence in the group is the one it
7610
+ cannot show, and the key has churned three times under `flag` on additions
7611
+ that changed no verdict, and once more under `code` at 1.1.0. `level` arriving
7612
+ there as a fifth param name is the second key whose two requirements genuinely
7613
+ *are* related — `ATX-60` and `ATX-89` are both about the severity ladder — and
7614
+ the rule still relates them by the one thing that is not the relation. A fourth candidate — the echo detector, scored at
7615
+ half precision — belongs to the tautology-detector entry now under
7616
+ *Considered and rejected* and is not re-quoted here, because a figure stated in two places goes stale in one. Taken together
7617
+ these are the measured range of what a token or single-body detector over
7618
+ prose can do here, and they are why the proposal is enforcement-only.
7619
+ *What blocks the decision — three things.* **(1) One adapter.** This
7620
+ repository's table is the only one that exists, and its author wrote both the
7621
+ schema and every answer in it. By this project's own rule a second table is
7622
+ what makes the seam real, and freezing a `Judgement` type now is deciding for
7623
+ everyone from 34 requirements written by one person. **(2) The breaking form
7624
+ of this was already rejected** — see *Countable obligations* below — and one
7625
+ of its three reasons survives the separate-file shape intact: **nothing can
7626
+ force the count to be true.** The other two do not survive, which is what is
7627
+ new here: a separate artifact touches no `--json` field, no delta apply and no
7628
+ `render` output, and completeness *across ids* is computable in a way
7629
+ completeness *within* an obligation array never was. So this is a re-proposal
7630
+ with one of three objections answered and the sharpest one standing.
7631
+ **(3) `quantifier-unjudged` reuses a rejected detector as its trigger.**
7632
+ Firing on the quantifier token was rejected below at 43% reach and 70% wrong;
7633
+ the claim here is that the same regex is acceptable when it asks a question an
7634
+ author discharges in one line (`over: 'values'`) rather than issuing a
7635
+ verdict. That claim is untested on anyone who did not also write the regex.
7636
+ *The half that is not blocked has shipped, under `[0.9.2]`.*
7637
+ `@am_shork/attest/inspect` is one function answering what a project declares —
7638
+ AST-only, no runner peer, landing on the existing static/executing boundary.
7639
+ It ships no schema and decides nothing, which is why it could go ahead of the
7640
+ three blockers rather than waiting on them. What it changes about this entry
7641
+ is the *kind* of thing that is now missing: the mechanism is no longer
7642
+ unreachable, only unwritten, so a second table is something an adopter can
7643
+ build rather than something only this repository could.
7644
+ *What would decide it:* one judgement table maintained by someone who did not
7645
+ design it. If it comes back the same shape, the seam is real and the schema
7646
+ follows; if it comes back different, `inspect` was the whole answer and the
7647
+ schema never needs to exist. *(One came back on 2026-08-31, and the disjunction
7648
+ above is too clean to survive it: partly the same shape, partly not, and the
7649
+ same half is the contaminated half. The close of this entry is where that is
7650
+ read.)*
7651
+ **Nothing currently makes that table arrive, which is a separate problem from
7652
+ the three blockers and the one that gates all of them.** Raised in a product
7653
+ review of `[0.9.1]`–`[0.9.3]` on 2026-08-19. The deciding evidence is an
7654
+ artifact only an adopter can produce, and the path to producing it is: a row in
7655
+ the README's document table, then a 214-line page, then a hand pass over four
7656
+ questions, then writing the two mechanical properties from a prose
7657
+ description, then choosing to report back. Five voluntary steps with no
7658
+ scaffold at any of them, against a decision this entry has been waiting on
7659
+ since `[0.9.2]`. An entry blocked on evidence that no shipped surface asks for
7660
+ is blocked indefinitely, and the wait is not itself evidence of anything.
7661
+ *The proposal is scaffolding the mechanism while still withholding the answer,*
7662
+ which is the same split `[0.9.2]` already made and not a retreat from it: an
7663
+ `examples/` directory holding the completeness and rot-binding assertions with
7664
+ **no columns** — the enumeration that forces a row, and the check that a named
7665
+ scenario is one `inspectProject` really declares — and the judgement columns
7666
+ left for the adopter to add. What the page withholds is this repository's four
7667
+ columns, and that stays withheld; what it currently also withholds is the two
7668
+ properties it names as *mechanical*, and those are the half where a copy is not
7669
+ an echo, because their content is fixed and the fourth finding does not apply
7670
+ to them.
7671
+ *What blocks it.* Whether a skeleton with no columns is still a shape that
7672
+ comes back as a copy — the page's argument is that a template makes the answer
7673
+ return its own question, and it is untested whether an adopter reads an empty
7674
+ frame as an invitation or as a form to fill in. And it is a second artifact
7675
+ claiming to be runnable consumer usage of the public API, which `pre-commit-check`
7676
+ §6 already records as the category no gate reaches: the page's three samples
7677
+ are unasserted today, and a fourth in a directory that looks executable would
7678
+ be worse than a fenced block, not better, unless something runs it.
7679
+ **Built under `[0.11.0]`, and the second of those two blockers was the
7680
+ buildable one.** `examples/judgement-table/` is in the default suite, the
7681
+ specifier is aliased rather than relativised so the file stays copyable
7682
+ verbatim, and both assertions were shown to fail before it was called done.
7683
+ So the category `pre-commit-check` §6 names now has exactly one member that a
7684
+ gate reaches, and the page's three prose samples are still not it.
7685
+ *Two things the build found that the proposal did not contain.* A third
7686
+ assertion — `issues` empty, before either of the other two — which is not a
7687
+ refinement but a hole: an unread registry file and a smaller project are the
7688
+ same value, and the two completeness directions pass over both. It was found
7689
+ by an invalid mutation during the bite test rather than by reasoning, which is
7690
+ `How an entry is re-measured`'s question about what a repair makes reachable,
7691
+ arriving from the other end. And a **type-level** forcing move the runtime
7692
+ assertion cannot make: a required field added to `Judgement` fails every
7693
+ existing row at compile time, so the schema-shaped half of "a row cannot
7694
+ arrive unjudged" costs one line and no engine rule. That is worth noting
7695
+ against blocker **(1)** above, which is about freezing a `Judgement` type:
7696
+ what shipped freezes nothing, because the type is in the adopter's copy.
7697
+ **What is untouched is the first blocker, and it is now the only thing between
7698
+ this entry and its evidence.** Whether an empty frame reads as an invitation
7699
+ or as a form to fill in is still untested, and cannot be tested here — the
7700
+ people who would answer it are the ones who have not arrived. What the build
7701
+ changes is only that the five voluntary steps are now four, and the one
7702
+ removed was the one with a fixed answer. If the next report is still nobody,
7703
+ the scaffold was not what was missing, and that is worth knowing too.
7704
+ **Decided 2026-08-31: build the counting rule, and not the schema.** The
7705
+ blocker was whether an empty frame reads as an invitation or as a form to fill
7706
+ in, it could not be tested here, and a 0.9.1 → 0.11.0 upgrade report from
7707
+ `mine-capablanca` tested it. The answer is **invitation**, and the report names
7708
+ the sentence that did it: not the empty `interface Judgement`, which reads as a
7709
+ stub and sent the reporter looking for what belonged in it, but the comment
7710
+ above it — *"It ships with no columns, and that is the finished state of this
7711
+ file rather than a gap in it."* Declaring the absence finished is what turns a
7712
+ blank into a question, and it is the cheapest sentence in the example.
7713
+ *The forcing move was one the build found rather than one it proposed.* The
7714
+ instruction to add the first column as a **required** field made all 80 rows
7715
+ stop compiling at once, and there was then no path back to green that did not
7716
+ go through having an opinion about all 80 — *"a form gets partially filled in;
7717
+ a type does not"*. That is the type-level forcing move described above,
7718
+ working on somebody who did not write it.
7719
+ *And the frame leaks in one place, which is a defect in the example rather
7720
+ than in the idea.* `pin` is documented as not a judgement column and is the
7721
+ only concrete field in the interface, so the reporter's first act was to
7722
+ populate it for all 80 rows mechanically — *"there is a field, populate it"*.
7723
+ Defensible, since it makes rot-binding bite for 80 rows instead of none, and
7724
+ exactly the form-filling behaviour the frame exists to prevent. Generating
7725
+ `pin` from `inspectProject` rather than leaving a field to be typed is the fix,
7726
+ and it belongs in the example.
7727
+ *What does **not** follow, and the report is why it does not.* Two of the three
7728
+ columns that came back are the same shape as two of this repository's —
7729
+ `clauses` is `OBLIGATIONS`, `quantifier` is a flattened `QUANTIFIED` — and it
7730
+ is tempting to read that as the seam being real. It cannot be read that way:
7731
+ the reporter names the `attest init` workflow document as prior influence, and
7732
+ that document discusses quantifiers and param pinning at length. So the
7733
+ convergence says **something** transmits those two ideas, and the document is
7734
+ at least as likely a channel as the frame. What is uncontaminated is the third
7735
+ column, `venue`, which has no counterpart here: nine of its rows record a
7736
+ promise held by a Playwright suite Attest's scope does not reach. A column this
7737
+ repository could not have written came back, which is a seam behaving like one.
7738
+ *What to build is smaller than a schema, and the three things the reporter
7739
+ wanted and did not get are its specification.* **(1) A counting rule for
7740
+ "obligation" precise enough that two people count the same.** The report
7741
+ distrusts its own `clauses` column above all the others and says why: two
7742
+ tables that count differently are not comparable, which defeats the point of
7743
+ there being a second one. A counting rule is content that does **not** depend
7744
+ on the adopter — the same category as the two mechanical assertions — and it is
7745
+ currently withheld along with the columns, which is the wrong side of the
7746
+ split. **(2) A way to bind a clause to a scenario rather than a row.**
7747
+ Rot-binding binds a row to *a* scenario; nothing binds the sixth obligation of
7748
+ a six-clause requirement to anything, so a six-clauses-three-scenarios row is a
7749
+ number a reader has to notice instead of an assertion that fails. **(3)
7750
+ Somewhere for a row to say the promise is held where Attest cannot see it.**
7751
+ Nine `ui-node` rows and a 902-line Playwright suite, citable only in prose.
7752
+ *So what is planned is (1) and the `pin` fix, and (2) and (3) explicitly are
7753
+ not.* (1) is a paragraph in the example, costs no engine surface, and is the
7754
+ one thing that makes a second table comparable to the first — without it two
7755
+ tables are two vocabularies and the seam cannot be read either way. (2) and (3)
7756
+ are both new schema, and blocker **(1)** of the proposal above — one adapter —
7757
+ is not answered by a report that is the second adapter's first hour. Two hours
7758
+ of one agent is not two adapters. **The `*.judge.ts` schema stays undecided**,
7759
+ and when it moves it will move on a second table that somebody has maintained
7760
+ rather than built, which is a different measurement from the one that arrived.
7761
+ **Both are built under `[1.1.0]`, and moving here on 2026-09-01 is what
7762
+ follows from that.** The counting rule and the anchor fix are the whole of what
7763
+ was decided on 2026-08-31, and they are recorded there. The re-measure before
7764
+ building corrected nothing this entry claims — the report's three asks
7765
+ reproduce as filed — and what it added is the finding the two cases in `How an
7766
+ entry is re-measured` would predict: the obvious implementation of "ship no
7767
+ fields" is an empty interface, and an empty interface admits `0` and `""`, so
7768
+ the frame would have gone on claiming an emptiness its own type did not hold.
7769
+ *The move is the entry's own state changing, not bookkeeping.* `Planned` means
7770
+ decided and unbuilt, and nothing left here is that: the schema, the
7771
+ clause-to-scenario binding and the out-of-scope `venue` column are all waiting
7772
+ on a second table somebody has maintained, which is the definition of this
7773
+ section rather than the one above. What it costs is that `Planned` is empty
7774
+ again — the state that moved `How an entry is re-measured` out of it, so that
7775
+ the method would not go unreachable whenever this happened — and that is the
7776
+ honest reading of where the project stands rather than a gap in the file:
7777
+ nothing is currently decided and unbuilt.
7262
7778
 
7263
7779
  ## Considered and rejected
7264
7780
 
@@ -7270,6 +7786,68 @@ it sat between 0.2.0 and 0.1.7 for two releases, where standing still meant
7270
7786
  sinking one version deeper each time a release was cut above it, and a rejection
7271
7787
  filed under a version reads as belonging to it.
7272
7788
 
7789
+ ### A brand palette and banner in the CLI
7790
+
7791
+ Built, measured green on every gate, and dropped 2026-09-03 in the session that
7792
+ built it. Filed here rather than deleted, because the half that was wrong is not
7793
+ the half that looks wrong, and the next person to propose "let's put the brand
7794
+ colours in the CLI" should reach this before rebuilding it.
7795
+
7796
+ **What it was.** `src/cli/brand.ts`: one module holding the palette behind a
7797
+ semantic interface — `pass`, `drift`, `muted`, `note`, `id`, plus the three
7798
+ marks — with `cli/report.ts` and `cli/index.ts` rewritten onto it so `chalk` was
7799
+ imported in exactly one place. Attest Green for a passing verdict, Drift Orange
7800
+ for a WARNING, Slate for every secondary line, and a four-row ASCII mark above
7801
+ `--help` on a TTY.
7802
+
7803
+ **The deletion test passed, which is why this is worth recording.** Delete the
7804
+ module and the same seven colour decisions reappear across two files and thirty
7805
+ call sites, spelled slightly differently each time — that is not a pass-through,
7806
+ and it is the ordinary reason this repository consolidates something. The gates
7807
+ agreed: lint, both typechecks, the full suite, `check self`, `verify self` and
7808
+ `render --check` were all green, and `attest --help` through a pipe was
7809
+ byte-identical, so `self/cli-surface.spec.ts` saw nothing.
7810
+
7811
+ **What it could not answer is who the interface is for.** A module earns its
7812
+ keep by having callers who vary across it, and this one had two callers that
7813
+ never will: there is no second theme, no `--color` flag, no second renderer, and
7814
+ nothing in the roadmap proposes one. *One adapter is a hypothetical seam.* The
7815
+ identity was consolidated at a place where nothing varies, which buys a
7816
+ re-tunability nobody has asked to use, and every colour in it is invisible to
7817
+ every test in the tree — they strip SGR before asserting, correctly, because
7818
+ `Issue.message` is prose and so is its colour. So the module's whole surface is
7819
+ unassertable: **the interface is the test surface, and this interface had none.**
7820
+
7821
+ **Three findings survive the rejection, and they are the reason it is written
7822
+ out.** They cost a session to learn and none of them is recoverable from the
7823
+ diff, which no longer exists.
7824
+
7825
+ - *Frost (`#E6EAF2`) can never be written by a CLI.* The guide defines it as
7826
+ "text on Void", and a terminal's background belongs to the reader — Frost on a
7827
+ light profile is invisible. The terminal's own foreground already plays that
7828
+ role. The same argument retires Void, Surface and Paper: a CLI that paints its
7829
+ own ground fights the profile the reader picked.
7830
+ - *Drift Orange cannot become the ERROR colour.* The palette has no failure
7831
+ colour, and the guide makes Drift Orange's scarcity part of what the product
7832
+ means — it appears when something is wrong. Spending it on every ERROR spends
7833
+ the meaning. The terminal's red is not a placeholder for a brand colour; it is
7834
+ the right answer.
7835
+ - *A monospace mark must be ASCII, and the reason is width, not encoding.*
7836
+ `█`, `╱`, `╲` and `═` are all East Asian **ambiguous** width, so a CJK
7837
+ terminal — which is where this repository is read — renders them two cells
7838
+ wide, and mixing them with a one-cell `=` or space tears a multi-row figure
7839
+ apart. The CLI's existing `✓`, `✗` and `●` are ambiguous too and survive it
7840
+ only because each is alone on its line with nothing aligned under it. This
7841
+ generalises past branding: it applies to any table, tree or rule the CLI ever
7842
+ draws.
7843
+
7844
+ *One thing found on the way is not a branding decision and does not belong to
7845
+ this rejection*: `chalk.dim` is SGR 2, which some profiles render at 40% opacity
7846
+ and others ignore outright, so the same secondary line is either unreadable or
7847
+ undifferentiated depending on the terminal. That is a legibility defect in
7848
+ output that ships today, and it survives this entry as a candidate on its own
7849
+ terms rather than as a reason to bring the palette back.
7850
+
7273
7851
  ### A backlink index over `archive/`, recording which change introduced or renamed an id
7274
7852
 
7275
7853
  Proposed from OpenViking's `.relations.json`, restated twice on measurement, and
@@ -9002,6 +9580,47 @@ declared prefix ownership recorded above in this section — rejected on there
9002
9580
  being no adoption that needs it, which is the same condition this sentence
9003
9581
  supposes — which addresses, rather than a scorer, which ranks.
9004
9582
 
9583
+ ### A standing gate on deprecated API usage
9584
+
9585
+ Priced on 2026-09-02, on the sweep that retired three deprecated call sites
9586
+ under `[Unreleased]` — turning `@typescript-eslint/no-deprecated` on by hand
9587
+ over a dependency walk through Zod 3→4, Vitest 3→4 and typescript-eslint
9588
+ 8.2→8.65, none of the three reachable by anything the repo runs on a commit.
9589
+ The obvious move was to promote the rule into `eslint.config.js` beside
9590
+ `no-unnecessary-condition`, itself promoted from `strictTypeChecked` on exactly
9591
+ this argument: it needs the checker, and it found real instances here.
9592
+
9593
+ *What blocked it was not the rule, it was the reach.* `no-deprecated` is
9594
+ type-aware, so it only sees files a project includes, and the one deprecated
9595
+ site with the widest blast radius — `tseslint.config()` in `eslint.config.js`
9596
+ itself — sits in the set no project includes, along with `bin/attest.js` and
9597
+ the three `vitest*.config.ts`. Turning the rule on as-is buys a gate that
9598
+ covers `src`, `tests`, `self` and `examples` and silently *not* the files the
9599
+ tooling reads first, which is the shape this repo has been burned by twice
9600
+ already: `tests/import-boundary.spec.ts` deliberately does not use `findFiles`
9601
+ for the same reason, and its instrument guards exist because a walk that
9602
+ quietly stops reading a tree passes empty. A gate whose coverage is
9603
+ believed-wider-than-it-is is the failure this file keeps recording. Closing the
9604
+ gap meant either growing `tsconfig.typecheck.json` over the root config files —
9605
+ pulling `allowJs` into the wider project for `eslint.config.js` and
9606
+ `bin/attest.js` — or a second small project just for them, a third tsconfig in
9607
+ a repo that deliberately has two and has a comment in each explaining why.
9608
+
9609
+ **Rejected on the denominator.** Three call sites across an entire dependency
9610
+ walk is too small to carry the cost of either tsconfig shape — permanent, on
9611
+ every file the wider project would newly admit — against a defect a hand sweep
9612
+ at upgrade time already catches. `pre-commit-check` §9 records that sweep: run
9613
+ `no-deprecated` against a throwaway project covering what
9614
+ `tsconfig.typecheck.json` does and the five files it does not, and confirm the
9615
+ scan is actually seeing them before trusting a clean result. The check still
9616
+ happens, on the cadence the defect actually arrives on, without a third
9617
+ tsconfig or `allowJs` admitted into a wider project.
9618
+
9619
+ *Worth reopening if the count moves* — a dependency bump that turns up more
9620
+ than a handful of deprecated sites in one sweep is the signal that a hand pass
9621
+ is no longer keeping up, and it is measured the same way this entry was: read
9622
+ the `no-deprecated` output before deciding whether it needs a permanent home.
9623
+
9005
9624
  ## How an entry is re-measured
9006
9625
 
9007
9626
  What the three sections above have learned about themselves, kept apart from all
@@ -9079,6 +9698,7 @@ path the repair makes reachable; it is about **everything the repair would make
9079
9698
 
9080
9699
  ## Releases
9081
9700
 
9701
+ [1.1.0]: https://gitlab.com/Pseudorca/attest/-/tags/v1.1.0
9082
9702
  [1.0.0]: https://gitlab.com/Pseudorca/attest/-/tags/v1.0.0
9083
9703
  [0.11.0]: https://gitlab.com/Pseudorca/attest/-/tags/v0.11.0
9084
9704
  [0.10.0]: https://gitlab.com/Pseudorca/attest/-/tags/v0.10.0