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