@am_shork/attest 0.9.3 → 0.10.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 +1027 -168
- package/README.md +10 -25
- package/bin/attest.js +0 -0
- 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/apply.js +5 -0
- package/dist/core/archive.js +26 -3
- package/dist/core/docs.d.ts +1 -1
- package/dist/core/docs.js +2 -0
- package/dist/core/gate.d.ts +8 -0
- package/dist/core/gate.js +38 -0
- 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.js +18 -8
- package/dist/core/render.js +8 -0
- package/dist/core/schema.d.ts +4 -0
- package/dist/core/schema.js +11 -0
- package/dist/core/skill.js +25 -0
- package/dist/core/splice.d.ts +6 -6
- package/dist/core/splice.js +51 -14
- package/dist/core/static-registry.d.ts +46 -1
- package/dist/core/static-registry.js +59 -4
- package/dist/core/validator.d.ts +2 -1
- package/dist/core/validator.js +20 -1
- package/package.json +31 -17
package/CHANGELOG.md
CHANGED
|
@@ -13,6 +13,759 @@ input, removes/renames a public API or `--json` field, or changes a default
|
|
|
13
13
|
runtime behavior an existing invocation relies on — diagnostic message text is
|
|
14
14
|
not API.
|
|
15
15
|
|
|
16
|
+
## [0.10.0] - 2026-08-22
|
|
17
|
+
|
|
18
|
+
### Changed
|
|
19
|
+
|
|
20
|
+
- **The same mutation pass over the verdict and its evidence: 24 mutations of
|
|
21
|
+
`gate.ts` and `red-record.ts`, 21 dead, and two of the three survivors closed.**
|
|
22
|
+
Every step of `evaluateGate` dies when removed, including scoping
|
|
23
|
+
`open-unresolved` back to the added ids — the cut that separates it from
|
|
24
|
+
`never-red`. So do both directions of `keepOutcome`: last-write-wins, where the
|
|
25
|
+
green run at the end erases the red it exists to remember, and first-write-wins,
|
|
26
|
+
where a recorded pass can never be corrected. The 46 lines at the top of
|
|
27
|
+
`red-record.ts` arguing for that asymmetry turn out to be held from both sides.
|
|
28
|
+
|
|
29
|
+
Two gaps were real, and both are cases where an existing test passes with the
|
|
30
|
+
guard removed:
|
|
31
|
+
|
|
32
|
+
- **A record whose version is not 2 was discarded by the *schema*, not by the
|
|
33
|
+
version check.** The case that covers this uses a version-1 record keyed
|
|
34
|
+
`reqId -> name`, which fails the shape as well — so the verdict is identical
|
|
35
|
+
either way, exactly as the guard's own comment predicts. Isolating the
|
|
36
|
+
version needs a record the schema would take: a valid version-2 shape under
|
|
37
|
+
version 3. Without that, "discarded because the format moved" and "discarded
|
|
38
|
+
because the file is corrupt" were the same observation, which the comment
|
|
39
|
+
calls out as how a format change gets made without anyone deciding to.
|
|
40
|
+
- **Nothing noticed if the record invented a `pass` for a scenario that never
|
|
41
|
+
executed.** `ATX-30` says the file records *how each scenario ended on its
|
|
42
|
+
first run*; a skipped scenario has no ending, so a `pass` there is an
|
|
43
|
+
observation no run produced — in a file that is evidence a reviewer reads and
|
|
44
|
+
that cannot be recomputed once the change is green. The gate still blocks
|
|
45
|
+
either way, so no verdict moves; what moves is the advice, since `never-red`
|
|
46
|
+
picks between "rewrite the assertion" and "run the gate before implementing"
|
|
47
|
+
on exactly this distinction.
|
|
48
|
+
|
|
49
|
+
The third survivor is left alone: `open-unresolved` emitting one error per
|
|
50
|
+
requirement rather than per question breaks nothing anybody promised. `ATX-81`
|
|
51
|
+
says the static commands report **each one**, and is pinned per question;
|
|
52
|
+
`ATX-82` says the gate **blocks**, and says nothing about granularity. Two paths
|
|
53
|
+
of one shape, one held and one not, and the difference is the two sentences.
|
|
54
|
+
|
|
55
|
+
*The harness failed twice more, and the second failure is the one worth
|
|
56
|
+
keeping.* Mutating a **writer** rather than a reader damages files outside the
|
|
57
|
+
source it was applied to: `archive` writes `first-run.json` into the e2e
|
|
58
|
+
fixtures, so a mutant that flipped `keepOutcome` left a committed fixture
|
|
59
|
+
saying `pass` where it had said `fail`, and every mutant after it read as
|
|
60
|
+
killed for that reason rather than its own. Restoring tracked fixtures fixed
|
|
61
|
+
the leak that had been found; a blanket check that the working tree is clean
|
|
62
|
+
after every mutant then immediately found the one that had not been — an
|
|
63
|
+
*untracked* record a mutant created, which `git checkout` cannot remove. The
|
|
64
|
+
broad guard earned its place over the narrow fix by catching what the narrow
|
|
65
|
+
fix was not written for.
|
|
66
|
+
|
|
67
|
+
- **Two guards in the registry reader had nothing holding them, found by
|
|
68
|
+
mutating every decision in `static-registry.ts` and watching what stayed
|
|
69
|
+
green.** Twenty mutations of the module's semantically load-bearing branches;
|
|
70
|
+
fourteen died to `tests/static-registry.spec.ts` and `tests/splice.spec.ts`,
|
|
71
|
+
and six survived the whole suite *and* `self/`. Two of the six were worth
|
|
72
|
+
closing, and both make the two readers answer **differently** rather than
|
|
73
|
+
merely reporting differently:
|
|
74
|
+
|
|
75
|
+
- `let reqs = defineRequirements({…}); reqs = …; export default reqs` — with
|
|
76
|
+
the `const` check removed the static reader answers the first registry while
|
|
77
|
+
the evaluator answers the second. A wrong answer rather than a refusal, which
|
|
78
|
+
is worse than any code it could have reported.
|
|
79
|
+
- `export = defineRequirements({…})` — with the `isExportEquals` check removed
|
|
80
|
+
the static reader accepts it, while the evaluator cannot run it at all.
|
|
81
|
+
|
|
82
|
+
Both now have a case in *"a registry that is not a literal"*, and both mutants
|
|
83
|
+
die to it. The other four survivors are left alone deliberately: one guard is
|
|
84
|
+
defence-in-depth behind a refusal that already happened upstream (`__proto__`
|
|
85
|
+
as an entry id, which `readRegistrySource` rejects before a merge can reach the
|
|
86
|
+
layout), one is a deliberate looseness whose comment says why, and two forbid
|
|
87
|
+
shapes that cannot compile in the first place. A test contrived to reach them
|
|
88
|
+
would assert that something upstream is still doing its job.
|
|
89
|
+
|
|
90
|
+
*The harness had to be fixed before any of it could be believed, and that is
|
|
91
|
+
the part worth carrying forward.* Its first run reported all six survivors as
|
|
92
|
+
killed. The cause was the mutation runner inheriting a working directory with
|
|
93
|
+
no test files in it, so `vitest` exited non-zero with "No test files found" and
|
|
94
|
+
every mutant read as covered — a green instrument reporting on nothing, which
|
|
95
|
+
is the exact failure `ATX-15`'s sweep guards against in as many words: *"a walk
|
|
96
|
+
that found no files satisfies 'no site orders by locale' while proving the
|
|
97
|
+
opposite of what it claims."* The runner now pins its working directory and
|
|
98
|
+
refuses to report at all unless the unmutated tree passes first.
|
|
99
|
+
|
|
100
|
+
- **The differential corpus now drives the two registry *writers*, not only the
|
|
101
|
+
two readers.** `tests/static-registry.spec.ts` exists because reading a
|
|
102
|
+
registry from the AST instead of evaluating it needs the two readers held
|
|
103
|
+
against each other — but the literal they walk has a third and a fourth
|
|
104
|
+
consumer, `registryInsertionPoint` and `registryEntryLayouts`, and the table
|
|
105
|
+
was never pointed at them.
|
|
106
|
+
|
|
107
|
+
*Every registry-source defect in this release had its trigger already sitting
|
|
108
|
+
in that table.* `as-const`, `satisfies` and `parenthesised` are the three
|
|
109
|
+
spellings whose literal the writers could not locate; `key-forms` carries the
|
|
110
|
+
duplicate key whose span they located at the wrong offset, under a comment
|
|
111
|
+
that has said *"a duplicate key: last wins"* since the row was written. The
|
|
112
|
+
corpus knew about all four inputs. Nothing asked the writers about them.
|
|
113
|
+
Reverting either fix now turns the table red — measured: the unwrap on all
|
|
114
|
+
three wrapper rows, the layout guard on `key-forms`.
|
|
115
|
+
|
|
116
|
+
**What it asserts is not that every case is writable.** A source the readers
|
|
117
|
+
accept may be one the writers must refuse, and `key-forms` now is. The
|
|
118
|
+
property is that a writer's answer is always *located* or *refused for a
|
|
119
|
+
stated reason*, never the third thing — located, wrongly — and which cases are
|
|
120
|
+
refused is a table someone has to edit rather than a branch that can drift.
|
|
121
|
+
|
|
122
|
+
*Widened while pointing it: the `satisfies` row did not cover the spelling
|
|
123
|
+
that broke.* It applied the keyword to a param value, where the value reader
|
|
124
|
+
sees it, and not to the registry argument, where the walk that finds the
|
|
125
|
+
literal does — so it stayed green through a defect named after it. It now
|
|
126
|
+
carries both positions, as the `as-const` row beside it always did.
|
|
127
|
+
|
|
128
|
+
- **`readArchivedChanges`' fan-out is bounded — the fourth one, and the first
|
|
129
|
+
outside `locate.ts`.** `archive.ts` read every archived first-run record
|
|
130
|
+
through a `Promise.all(names.map(…))`, so a project with N merged changes held
|
|
131
|
+
N reads and N parsed `RedRecord`s at once. It is the same shape as the other
|
|
132
|
+
three against an input nobody here chooses — an `archive/` grows by one folder
|
|
133
|
+
per change merged and is never pruned, so it is the longest-lived of the four
|
|
134
|
+
lists a project's shape decides the length of. Measured in
|
|
135
|
+
`tests/locate-fanout.spec.ts`, which now counts a fourth pool: **64 before, 32
|
|
136
|
+
after**, on 64 archived changes. Unbounded, the figure is the count of the
|
|
137
|
+
folders; bounded, it is the constant.
|
|
138
|
+
|
|
139
|
+
*Why it hid is that the pool's own doc comment names the rule and this caller
|
|
140
|
+
is not in the file that has it.* `forEachBounded` says "anything here that
|
|
141
|
+
reads a list the project's tree decides the length of belongs in this pool,
|
|
142
|
+
whatever the function around it is called" — and `readArchivedChanges` is
|
|
143
|
+
exactly that, one module over. The nearest comment to it reasons carefully
|
|
144
|
+
about concurrency and still says nothing about a bound: `inspectRedEvidence`
|
|
145
|
+
explains why it runs the archive read *after* `inspectProject` rather than
|
|
146
|
+
beside it — "overlapping them buys nothing worth a second fan-out" — which is
|
|
147
|
+
a correct answer about sequencing two reads and not a claim about what happens
|
|
148
|
+
inside either. That is the third time in this catalogue a considered comment
|
|
149
|
+
about concurrency has stood where a bound was missing. `forEachBounded` is now
|
|
150
|
+
exported for it, with a fourth constant on the reason `WALK_CONCURRENCY`
|
|
151
|
+
already gives.
|
|
152
|
+
|
|
153
|
+
**The failure this invites is memory, not descriptors — and that corrects the
|
|
154
|
+
three entries above.** Each of those closed by calling descriptor exhaustion
|
|
155
|
+
the invited failure and leaving it unmeasured. Measured now, on Linux: it is
|
|
156
|
+
not reachable through this shape at all. `fs.promises.readFile` does
|
|
157
|
+
open-read-close inside one libuv threadpool work item, so concurrent
|
|
158
|
+
*descriptors* are bounded by the threadpool and never by the fan-out — 3,000
|
|
159
|
+
unbounded in-flight reads peaked at **53** entries in `/proc/self/fd`, and the
|
|
160
|
+
read completed cleanly under `ulimit -n 256`, and again under `ulimit -n 128`
|
|
161
|
+
with `UV_THREADPOOL_SIZE=1024`. What does track the input is heap: on 2,000
|
|
162
|
+
changes of a 45 KB record, peak `heapUsed` was **352 MB unbounded against 177
|
|
163
|
+
MB bounded**, over interleaved runs — and the unbounded figure wandered from
|
|
164
|
+
329 to 580 MB across rounds while the bounded one held 177–181. The stable
|
|
165
|
+
number is the point; the retained result is the floor, and what the bound
|
|
166
|
+
removes is the transient pile on top of it. This is the portable-arithmetic
|
|
167
|
+
standard the three earlier halves were accepted on, so it needs no incident
|
|
168
|
+
report — but the *reason* is memory, and future sweeps should stop reaching
|
|
169
|
+
for `ulimit`, which does not govern this on either platform: Windows uses
|
|
170
|
+
Win32 handles, and on Linux the threadpool gets there first.
|
|
171
|
+
|
|
172
|
+
Reached only from `inspectRedEvidence` and so from `@am_shork/attest/inspect`,
|
|
173
|
+
the published subpath — no CLI command reads the archive, which is why the
|
|
174
|
+
static-command roster is untouched by this. No behaviour change and no
|
|
175
|
+
`ATX-n`, on the precedent of the other three: the changes, their order, their
|
|
176
|
+
paths and the merged record are identical, asserted beside the peak. The
|
|
177
|
+
result is built by preallocate-and-index rather than by arrival, which is what
|
|
178
|
+
keeps the sorted folder order true through a pool that completes out of order.
|
|
179
|
+
|
|
180
|
+
- **The publish job runs on protected tags only, and it says so in the file
|
|
181
|
+
rather than in a settings page.** `rules` was `if: $CI_COMMIT_TAG`; it is now
|
|
182
|
+
`if: $CI_COMMIT_TAG && $CI_COMMIT_REF_PROTECTED == "true"`.
|
|
183
|
+
|
|
184
|
+
*The reason it needed to be in the file is that the comment beside it claimed
|
|
185
|
+
a defence that does not exist.* It said the reason to protect release tags had
|
|
186
|
+
changed — from exposing a masked variable to `id_tokens` being "issued to
|
|
187
|
+
protected refs only". That is not how GitLab issues them. Every job gets an ID
|
|
188
|
+
token, and the protection state travels *inside* it as the `ref_protected`
|
|
189
|
+
claim; a design that reports protection as a payload field is one where an
|
|
190
|
+
unprotected ref is handed a token too. Nothing on npm's end narrows it either:
|
|
191
|
+
a GitLab trusted publisher is namespace + project + top-level CI file path +
|
|
192
|
+
optional environment, with no ref condition available to configure.
|
|
193
|
+
|
|
194
|
+
**So the entire ref authorisation was the `rules` line — evaluated from the
|
|
195
|
+
`.gitlab-ci.yml` at the pushed tag, which is a file the pusher had just
|
|
196
|
+
written.** "Can push a tag" was therefore "can publish `@am_shork/attest` with
|
|
197
|
+
a valid provenance attestation", and the `NPM_ID_TOKEN` guard added in the
|
|
198
|
+
entry below cannot narrow it: that guard asks whether a token exists, never
|
|
199
|
+
who caused one to be minted. Protected tags in *Settings -> Repository* were
|
|
200
|
+
the only thing standing there, described by the comment as a belt-and-braces
|
|
201
|
+
extra.
|
|
202
|
+
|
|
203
|
+
This is the shape this repository keeps finding — a defence that reads as
|
|
204
|
+
present and is not, alongside `bin/attest.js`'s static import, the `hasWarned`
|
|
205
|
+
spread and `hmr: false` — arriving this time in the sentence explaining a
|
|
206
|
+
defence rather than in the code implementing one. Keep the protected-tag rule
|
|
207
|
+
in Settings as well: only that side stops the tag being created, and this side
|
|
208
|
+
only declines to publish once it has been. *Found by review, not by a run —
|
|
209
|
+
no pipeline was executed to confirm it, and the two vendor behaviours above
|
|
210
|
+
are read off GitLab's ID-token documentation and npm's trusted-publisher
|
|
211
|
+
documentation rather than measured here.*
|
|
212
|
+
|
|
213
|
+
- **The publish job has no stored credential left: `NPM_TOKEN` and the
|
|
214
|
+
`_authToken` line are gone, and the guard is on `NPM_ID_TOKEN` instead.**
|
|
215
|
+
Completes what `[0.9.4]` set up. The deletion test pointed here from the
|
|
216
|
+
start — what it removes is the only long-lived credential this repository had.
|
|
217
|
+
|
|
218
|
+
*It went in on a measurement, and the measurement is the part worth keeping.*
|
|
219
|
+
The v0.9.4 release published through the trusted publisher, not the token:
|
|
220
|
+
`_npmUser` on that version is
|
|
221
|
+
`{"name":"GitLab CI/CD","trustedPublisher":{"id":"gitlab","oidcConfigId":"oidc:…"}}`,
|
|
222
|
+
by npm 11.19.0. The entry that proposed this recorded that **no account existed
|
|
223
|
+
anywhere of publishing from GitLab under OIDC** — every report was GitHub
|
|
224
|
+
Actions — and stated the inference at the strength the evidence carried. That
|
|
225
|
+
release is the first record. The token was present and unused: `oidc()`
|
|
226
|
+
overwrites the auth key on success, so the `.npmrc` line never applied.
|
|
227
|
+
|
|
228
|
+
**The job's own log could not have told us, and that is why the fallback had
|
|
229
|
+
to go.** Every path in npm's `oidc.js` logs at `verbose` or `silly` while npm's
|
|
230
|
+
default level is `notice`, so a successful exchange and a failed one produce
|
|
231
|
+
byte-identical job output — the v0.9.4 log contains no OIDC line of either
|
|
232
|
+
kind. Combine that with a fallback written never to throw and the failure mode
|
|
233
|
+
is exact: an exchange that stops working keeps publishing, on the credential
|
|
234
|
+
the exchange exists to retire, silently, with the difference visible only in
|
|
235
|
+
registry metadata nobody reads. **The property that made the rollout cheap is
|
|
236
|
+
the wrong one to keep**, and this is the first time in this file that a
|
|
237
|
+
mechanism has been removed for succeeding rather than for failing.
|
|
238
|
+
|
|
239
|
+
So the check moves in front of the irreversible step, which is the rule the
|
|
240
|
+
removed post-publish attestation check failed and this file already argued:
|
|
241
|
+
verification belongs where it can act on the answer. A missing `NPM_ID_TOKEN`
|
|
242
|
+
now fails before anything is published; a failed exchange fails the publish
|
|
243
|
+
outright, loudly and recoverably, because nothing was written.
|
|
244
|
+
|
|
245
|
+
*What is not in this repository, and is not done by this entry.* One act on
|
|
246
|
+
npmjs.com: *Require two-factor authentication and disallow tokens* on the
|
|
247
|
+
package, which is what stops a leaked token publishing it and which npm states
|
|
248
|
+
does not affect trusted publishers. Not a commit and not verifiable from here.
|
|
249
|
+
|
|
250
|
+
*The `NPM_TOKEN` CI variable stays, deliberately, and the reasoning is worth
|
|
251
|
+
recording because the first version of this entry had it wrong.* Deleting it
|
|
252
|
+
was filed beside the npm setting as though the two were equal acts. They are
|
|
253
|
+
not: the setting removes the **capability** — a token can no longer publish this
|
|
254
|
+
package — while deleting the variable removes **one copy** of a credential that
|
|
255
|
+
goes on existing in the npm account either way. Revoking it there is the act
|
|
256
|
+
that destroys it; removing it from GitLab is housekeeping. The variable is
|
|
257
|
+
masked, protected, exposed only to protected refs, and now read by no job, so
|
|
258
|
+
what it costs to keep is nothing. What the setting does not cover is measured
|
|
259
|
+
rather than assumed: it is per-package, and `@am_shork/attest` is the only
|
|
260
|
+
package in the scope, so today it covers the account's entire published
|
|
261
|
+
surface exactly. The gap it leaves is future-conditional — a second package
|
|
262
|
+
would start without the setting — and that is an obligation attached to
|
|
263
|
+
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
|
|
264
|
+
v0.9.4 was the test of the last one — except that this time a regression fails
|
|
265
|
+
the release instead of quietly publishing. That is the intended trade.
|
|
266
|
+
|
|
267
|
+
### Added
|
|
268
|
+
|
|
269
|
+
- **`ATX-83` states the three states of that field.** What was missing was never
|
|
270
|
+
the field; it was a sentence the suite could falsify about *when* the field is
|
|
271
|
+
there. A requirement saying only "the report carries the paths the merge wrote"
|
|
272
|
+
is satisfied completely by an implementation that always emits `[]`, and that
|
|
273
|
+
implementation is wrong in exactly the case the field is reached for — a re-run
|
|
274
|
+
after a completed merge.
|
|
275
|
+
|
|
276
|
+
*Written straight into the registry, not through `changes/`.* That is how
|
|
277
|
+
existing behaviour is described here, and it is the reading `ATX-80`'s
|
|
278
|
+
`unarchived` state exists for: `never-red` asks for a recorded failing run only
|
|
279
|
+
of the ids a change is currently ADDing, so a requirement catching up to
|
|
280
|
+
behaviour that already shipped carries no such obligation and gains nothing
|
|
281
|
+
from a workflow whose point is to make a scenario fail before the code exists.
|
|
282
|
+
The scenario lives beside the other `archiveReport` ones in
|
|
283
|
+
`self/json.spec.ts`, where a reader looking at that function finds all of them.
|
|
284
|
+
Nothing records that it discriminates, which is what `unarchived` costs and is
|
|
285
|
+
worth saying rather than leaving implicit — so it was checked by hand instead:
|
|
286
|
+
with the one line that spreads the field into the report removed, the scenario
|
|
287
|
+
fails, and `verify self` reports `tests-red`.
|
|
288
|
+
|
|
289
|
+
The measurement in `tests/intent-rule-candidates.spec.ts` moved by exactly one
|
|
290
|
+
requirement — corpus 93 → 94, universal quantifiers 40 → 41 — and no verdict
|
|
291
|
+
in it flipped. It also produced the `flag:` divergence key's **fourth**
|
|
292
|
+
re-keying in four releases, every one triggered by an addition that agrees
|
|
293
|
+
with a member already there, and the `field:` key's first *extension* rather
|
|
294
|
+
than re-keying; both are noted where the rule is judged, because which of the
|
|
295
|
+
two an addition causes turns on vocabulary rather than on how related the
|
|
296
|
+
requirements are.
|
|
297
|
+
|
|
298
|
+
- **`archive --apply --json` reports the paths the merge wrote.** The human
|
|
299
|
+
rendering has listed them since `--apply` shipped, on the grounds that this is
|
|
300
|
+
the command that edits files the user cannot regenerate and the reader needs to
|
|
301
|
+
know which ones to look at before committing. The `--json` reader — the one
|
|
302
|
+
most likely to be automating around the merge — got the change name and nothing
|
|
303
|
+
else. `written[]` closes that, and both renderings are now handed the same
|
|
304
|
+
bundle from one place in the shell rather than each being told separately,
|
|
305
|
+
which is the split that let the verdict disagree with itself in `[0.7.0]`.
|
|
306
|
+
|
|
307
|
+
**Absent without `--apply`, `[]` with it.** The two are different facts — no
|
|
308
|
+
merge was attempted, against a merge that ran and had nothing left to write,
|
|
309
|
+
which is the ordinary state of a re-run after a completed one — and a field
|
|
310
|
+
that spelled both `[]` could not say which. Additive, so no `SCHEMA_VERSION`
|
|
311
|
+
bump: a consumer that has never heard of the field reads the report it always
|
|
312
|
+
did. ATX-74's sweep covers it like every other string a report quotes out of
|
|
313
|
+
the checked project.
|
|
314
|
+
|
|
315
|
+
### Fixed
|
|
316
|
+
|
|
317
|
+
- **`first-run.json` committed different bytes for the same evidence, depending
|
|
318
|
+
on how many runs it took to gather.** `mergeRedRecord` rebuilds the record so
|
|
319
|
+
key order is a function of the data rather than of insertion — and did that at
|
|
320
|
+
two of its three levels. The rebuild sorts the requirement ids the record
|
|
321
|
+
already held; the loop after it *appends* the ids this run is the first to
|
|
322
|
+
observe, in plan order; and the pass at the bottom re-sorted the file and
|
|
323
|
+
scenario levels while walking the ids in whatever order they were in, because
|
|
324
|
+
reassigning `record[id]` does not move a key.
|
|
325
|
+
|
|
326
|
+
So a change that grew its record over several stage-1 runs committed
|
|
327
|
+
`{"Z-9": …, "A-1": …}` where the same observations reached in one run committed
|
|
328
|
+
`{"A-1": …, "Z-9": …}`. Nothing about the outcomes differs; the file does. That
|
|
329
|
+
matters twice over for this particular file: it is evidence a reviewer reads in
|
|
330
|
+
a diff, and the gate has to reach the same verdict on a CI checkout as on the
|
|
331
|
+
author's machine. It also went unnoticed once written — `changed` is decided by
|
|
332
|
+
the outcomes alone, so an unchanged record is never rewritten and a
|
|
333
|
+
wrongly-ordered one stays that way.
|
|
334
|
+
|
|
335
|
+
*ATX-15 already said this and could not catch it.* The requirement quantifies
|
|
336
|
+
over "every ordering Attest commits to disk", and the sweep attesting it is
|
|
337
|
+
deliberately narrower than its own statement: it checks *how* a site orders —
|
|
338
|
+
every `.sort` in `src/` through `byCodeUnit` — and says so, *"a `red-record.ts`
|
|
339
|
+
that stopped sorting at all is still green here, and the scenarios above are
|
|
340
|
+
what speak to the result"*. There was no such scenario for this file. There is
|
|
341
|
+
now, in `self/red-record.spec.ts`: it reaches the same evidence by both routes
|
|
342
|
+
and compares the serialised bytes. `archive.ts`'s `sortRecord` had all three
|
|
343
|
+
levels right and is what this now matches.
|
|
344
|
+
|
|
345
|
+
- **`--apply` refused a registry whose literal is written with `as const`,
|
|
346
|
+
`satisfies`, or a bare pair of parentheses.** The reader takes all three —
|
|
347
|
+
`unwrap` strips the type-level wrappers before `literalValue` reads the object,
|
|
348
|
+
because none of them changes the value — so the gate read such a file, proved
|
|
349
|
+
the change green, and handed it to a merge that asked
|
|
350
|
+
`ts.isObjectLiteralExpression` of the wrapper and got `false`. Both writers
|
|
351
|
+
refused: the whole change stopped with `internal-error`, whose own message says
|
|
352
|
+
the file *"read as a registry for the gate but not for the merge"* and whose
|
|
353
|
+
comment calls the condition unreachable through the command. It was reachable
|
|
354
|
+
by writing `defineRequirements(({ … }))`.
|
|
355
|
+
|
|
356
|
+
*The fix is where the two answers diverged, not where the symptom showed.*
|
|
357
|
+
`authoringCall` is the single function both sides use to find the registry
|
|
358
|
+
literal — the module's opening note says a second walker would be a second
|
|
359
|
+
answer to "is this a literal" — and it handed back the argument node as
|
|
360
|
+
written, leaving each caller to decide whether to unwrap. The reader did and
|
|
361
|
+
the writers did not, so the disagreement the shared function exists to prevent
|
|
362
|
+
was inside it. It now unwraps once, and `literalValue`'s own call becomes the
|
|
363
|
+
no-op it should always have been.
|
|
364
|
+
|
|
365
|
+
The parenthesis is the reachable spelling and the reason this is a fix rather
|
|
366
|
+
than a nicety: `as const` and `satisfies` are opted into, while nothing about
|
|
367
|
+
`(({ … }))` looks like a type annotation anyone chose. No `SCHEMA_VERSION`
|
|
368
|
+
bump and no new code — a change that used to fail now succeeds, which is the
|
|
369
|
+
benign direction for an exit code, and the History row is what tells a reader
|
|
370
|
+
on an older build which half of the page applies to them.
|
|
371
|
+
|
|
372
|
+
- **A registry key written twice made `archive --apply` write bytes outside the
|
|
373
|
+
value it was replacing — and, measured, outside the registry literal
|
|
374
|
+
altogether.** `registryEntryLayouts` keys each value's source span by name in
|
|
375
|
+
a `Map`, and `spliceModifications` walks that map to get its edits in
|
|
376
|
+
ascending offset order, which is what lets the pass at the bottom be a
|
|
377
|
+
`reverse()` rather than a sort. `Map.set` on a key it already holds keeps the
|
|
378
|
+
**first** insertion's position and takes the **last** call's value — so an
|
|
379
|
+
entry writing `params: { limit: 1, other: 'x', limit: 1 }` describes `limit`
|
|
380
|
+
at position one carrying the span of the occurrence *after* `other`, and the
|
|
381
|
+
offsets stop ascending. The earlier edit then moves the bytes under the later
|
|
382
|
+
one. On a registry the gate had just proved green, changing both values landed
|
|
383
|
+
the second one past the file's closing `});`.
|
|
384
|
+
|
|
385
|
+
*Nothing upstream refuses such a file, which is why this reached the one
|
|
386
|
+
command that writes.* A duplicate key is a TypeScript **semantic** error, and
|
|
387
|
+
`parseSource` asks only the syntactic question (`[0.9.0]`, ATX-69) — so both
|
|
388
|
+
registry readers take the file and hand back the last occurrence, `check` is
|
|
389
|
+
silent, and the gate passes. The reachable route is an ordinary copy-paste in
|
|
390
|
+
a hand-written registry, in a project that does not typecheck its
|
|
391
|
+
`*.reqs.ts`.
|
|
392
|
+
|
|
393
|
+
**Refused rather than described, at all three levels.** An entry whose keys
|
|
394
|
+
repeat is left out of the layout with the repeated key's path recorded beside
|
|
395
|
+
it, and `spliceModifications` turns that into an `apply-unsupported-delta`
|
|
396
|
+
refusal — whole, with nothing written — before a single edit is pushed. The
|
|
397
|
+
id level is not the mild one and is refused for the same reason rather than a
|
|
398
|
+
weaker one: two entries under a single id put a *later* entry's spans at an
|
|
399
|
+
*earlier* entry's position, so the disorder is between entries rather than
|
|
400
|
+
inside one. The guard stays exactly as wide as the danger — an unrelated id in
|
|
401
|
+
the same file is still written — because a merge that stopped over something
|
|
402
|
+
it was not asked to touch is the thing `--apply` refuses to do everywhere
|
|
403
|
+
else.
|
|
404
|
+
|
|
405
|
+
*The sibling defect is already in this file and did not generalise on its
|
|
406
|
+
own.* `[0.8.0]` fixed an integer-like `params` key for the identical
|
|
407
|
+
invariant — JavaScript hoists it to the front of every object, so a plain
|
|
408
|
+
object could not carry the file's order — and the repair was to move all three
|
|
409
|
+
levels to `Map`s. That answered the container and left the *keys*: a `Map`
|
|
410
|
+
keeps insertion order for every key type, and the one thing it still cannot
|
|
411
|
+
represent is one key written twice. The test added then, `rewrites two params
|
|
412
|
+
when one of them has an integer-like key`, is the shape of the three added
|
|
413
|
+
here, down to needing two changed values of different lengths — one edit
|
|
414
|
+
cannot be out of order with itself.
|
|
415
|
+
|
|
416
|
+
`verifyWritten` did catch the result and stop the merge, so the failure was
|
|
417
|
+
never silent; the file had already been overwritten by then, which is the
|
|
418
|
+
"destructive on a file the user cannot regenerate" shape the `AGENTS.md` merge
|
|
419
|
+
tool was rejected for and the reason a post-hoc check is not where this
|
|
420
|
+
belongs.
|
|
421
|
+
|
|
422
|
+
**Breaking, and narrowly so — this is the entry that decides the version
|
|
423
|
+
number.** The common case is not: a repeated key usually made the merge write
|
|
424
|
+
outside the value and the re-read then failed it, so the run already exited 1
|
|
425
|
+
and only the `code` moves — `internal-error`, raised after the registry had
|
|
426
|
+
been overwritten, becomes `apply-unsupported-delta`, raised before anything is
|
|
427
|
+
written.
|
|
428
|
+
|
|
429
|
+
But the offsets only go wrong when an earlier edit *changes length*. A
|
|
430
|
+
modification that replaces a value with one the same size shifts nothing, so
|
|
431
|
+
on `[0.9.4]` such a merge completed and completed **correctly**. Measured on a
|
|
432
|
+
registry with a duplicated `statement` and a `budgetMb: 1` → `2` patch: 0.9.4
|
|
433
|
+
merges it and it reads back as `{"budgetMb":2}`; today it refuses. That is
|
|
434
|
+
previously-valid input now rejected, which is this file's own definition of
|
|
435
|
+
breaking, so it takes the **minor** rather than the patch.
|
|
436
|
+
|
|
437
|
+
The refusal is still right — the same file with a length-changing patch is the
|
|
438
|
+
corruption above, and which of the two a merge gets is not something the author
|
|
439
|
+
chose. What the narrowness costs is that an adopter whose registry happens to
|
|
440
|
+
carry a duplicate key sees a working command start refusing, so it belongs in a
|
|
441
|
+
minor with the reason stated rather than in a patch nobody reads.
|
|
442
|
+
|
|
443
|
+
No `SCHEMA_VERSION` bump: the envelope, the `Issue` interface and the roster in
|
|
444
|
+
`core/docs.ts` are all unchanged — the refusal reuses the code `--apply`
|
|
445
|
+
already raises for a delta it will not write.
|
|
446
|
+
|
|
447
|
+
**`ATX-76` is widened to say what it always meant**, through this
|
|
448
|
+
repository's own workflow — `self/archive/2026-08-21-widen-atx-76`, the second
|
|
449
|
+
change to go through it and the first carrying a MODIFIED. Its refusal clause
|
|
450
|
+
named one instance of a class: *"when a span it would overwrite carries a
|
|
451
|
+
comment"*. Refusing a comment inside a span is not a rule about comments, it is
|
|
452
|
+
the rule that a replacement may only ever write the value it was asked to
|
|
453
|
+
write, stated at the one place that value's boundary was known to be in doubt.
|
|
454
|
+
A repeated name puts it in doubt a second way, so the clause now reads
|
|
455
|
+
*"wherever the file leaves that span uncertain"* and lists both. What is
|
|
456
|
+
**not** stated is the refusal as a mechanism: refusing is how *this*
|
|
457
|
+
implementation keeps the first clause, and a rewrite could keep it by ordering
|
|
458
|
+
the edits instead — a requirement pinned to the remedy would forbid that repair
|
|
459
|
+
for no reason.
|
|
460
|
+
|
|
461
|
+
*The defect was never that the rule was absent.* The first clause — the unit of
|
|
462
|
+
the edit is the value — has said this since `[0.9.0]`. What was missing was a
|
|
463
|
+
sentence the suite could falsify in this direction: both existing scenarios
|
|
464
|
+
assert something about a **comment**, and neither can go red when the failure
|
|
465
|
+
is that the span located was the wrong one. `self/merge.spec.ts` now carries a
|
|
466
|
+
third, looping the three sites a name can repeat with the extent pinned beside
|
|
467
|
+
the loop. Measured with the fix reverted, at the `spliceModifications` seam:
|
|
468
|
+
all three wrote without refusing; with it, all three refuse.
|
|
469
|
+
|
|
470
|
+
### Documentation
|
|
471
|
+
|
|
472
|
+
- **The CLI reference told readers the `--json` guarantee rests on
|
|
473
|
+
`JSON.stringify`, which is the reasoning `[0.8.0]` retired.** *Output is safe
|
|
474
|
+
to read* closed with "`--json` was never exposed: serialising escapes the same
|
|
475
|
+
characters" — true of C0 and of nothing else, so DEL and the C1 range were
|
|
476
|
+
never covered by it. The code has been right since `[0.8.0]`, where the
|
|
477
|
+
replacer in `renderJson` began stripping every string in the document as it
|
|
478
|
+
serialises one; the page kept the retired argument, in both languages, three
|
|
479
|
+
hundred lines above its own History row saying the opposite.
|
|
480
|
+
|
|
481
|
+
*The comment at the crash site in `cli/action.ts` said it too*, and that copy
|
|
482
|
+
is the one worth naming: it sat directly above `renderJson(errorReport(…))`
|
|
483
|
+
and read as a settled argument for why sanitising there was unnecessary. The
|
|
484
|
+
replacer it argues against is what makes that line safe. Nothing was
|
|
485
|
+
exploitable — this is a false rationale standing beside a working defence,
|
|
486
|
+
which is the configuration that gets a defence deleted by someone tidying up.
|
|
487
|
+
|
|
488
|
+
Found by an attack-surface review rather than by a gate, and no gate could
|
|
489
|
+
have found it: the suite checks that a `§n` a comment cites exists and that a
|
|
490
|
+
version a History table names is a heading here, never whether a sentence is
|
|
491
|
+
true. Three copies of one retired claim is what that gap looks like from
|
|
492
|
+
inside. No behaviour changed and no version moved, so there is no new History
|
|
493
|
+
row — the `[0.8.0]` row was correct all along and is what the prose now agrees
|
|
494
|
+
with.
|
|
495
|
+
|
|
496
|
+
- **The method page says which shape a composite param is best at, and the
|
|
497
|
+
sample is a fixture that runs.** `intent-quality.md` already carried both
|
|
498
|
+
hazards a list-valued param has — an expectation recomputed from the param it
|
|
499
|
+
is checking, and a loop whose extent nobody pinned — as the third and fourth
|
|
500
|
+
of its five findings, named and left there with no shape attached. A **decision table**, rules pairing conditions with the
|
|
501
|
+
outputs the requirement promises, is where both arrive at once and where the
|
|
502
|
+
single source pays most, so it is now named. The section states the property
|
|
503
|
+
the shape actually rests on, which is narrower than "use a table": the
|
|
504
|
+
implementation reads only the *condition* columns, which leaves the output
|
|
505
|
+
columns an independent term — and it stops being true the moment the system
|
|
506
|
+
under test is itself driven by the same rules.
|
|
507
|
+
|
|
508
|
+
*The sample is `fixtures/consumer/requirements/cart.reqs.ts` and
|
|
509
|
+
`fixtures/consumer/cart.spec.ts`*, added here and registered in
|
|
510
|
+
`PUBLISHED_SAMPLES`, because every `ts` block this page publishes was already
|
|
511
|
+
a file the packaging test installs from a tarball and runs, and half-real
|
|
512
|
+
samples teach a reader to check which — after which they stop. **Making it
|
|
513
|
+
real is what found the constraint the section now documents**: the statement
|
|
514
|
+
cannot interpolate the table. A `{placeholder}` resolves to a scalar or a list
|
|
515
|
+
of scalars, so `{rules}` is `non-scalar-interpolation`, and the statement has
|
|
516
|
+
to name the behaviour in prose while the rules stay a param the scenario reads.
|
|
517
|
+
A draft written only as prose had it wrong and nothing would have said so.
|
|
518
|
+
|
|
519
|
+
It also landed on an existing row rather than needing a new rule:
|
|
520
|
+
`intent-rule-candidates.spec.ts` classifies it `over: 'roster'`,
|
|
521
|
+
`owner: 'registry'`, which is the branch that owes a `pin` — a registry-owned
|
|
522
|
+
list is data an author can shorten, and every assertion inside the loop still
|
|
523
|
+
passes over the shorter one. The published scenario carries that pin
|
|
524
|
+
(`expect(rules).toHaveLength(6)`), so a reader who copies the block copies the
|
|
525
|
+
counter-pressure with it.
|
|
526
|
+
|
|
527
|
+
*It was written into the README first, and has moved.* The README is the
|
|
528
|
+
pitch, the five-minute start and navigation; a hundred lines on how to write a
|
|
529
|
+
param that pins something is the method page's job, beside the two findings it
|
|
530
|
+
repairs. The README keeps one paragraph pointing at it. **The move is also
|
|
531
|
+
what showed `PUBLISHED_SAMPLES` could not say where a sample is published** —
|
|
532
|
+
it read the README and only the README, which was right while every sample
|
|
533
|
+
lived there and an obstacle the moment one did not. Each entry now carries its
|
|
534
|
+
page, so moving a sample between pages is a roster edit rather than a silent
|
|
535
|
+
loss of the gate that keeps it runnable.
|
|
536
|
+
|
|
537
|
+
## [0.9.4] - 2026-08-19
|
|
538
|
+
|
|
539
|
+
### Added
|
|
540
|
+
|
|
541
|
+
- **A requirement can say a value is not decided yet, as `open` (`ATX-81`,
|
|
542
|
+
`ATX-82`).** The registry had two states, schema-valid or an ERROR, and there
|
|
543
|
+
was no third — so an author who could not determine a value had nowhere to put
|
|
544
|
+
that fact. What they do instead is pick one, and a scenario written against an
|
|
545
|
+
invented number is drift no gate can see: the single source is perfectly
|
|
546
|
+
consistent and describes a promise nobody made, arriving through the mechanism
|
|
547
|
+
meant to stop it. `open` is a free-text list of what is still undecided about a
|
|
548
|
+
requirement, one entry per question. The static commands report each as a
|
|
549
|
+
`requirement-open` WARNING and keep their exit code; the archive gate refuses
|
|
550
|
+
with `open-unresolved` while any remains. Promoted from *Under consideration*,
|
|
551
|
+
where the blockers were an unmeasured frequency and the permanent surface a
|
|
552
|
+
schema field costs.
|
|
553
|
+
|
|
554
|
+
*The asymmetry is the design, and it is `never-red`'s.* A proposal may be
|
|
555
|
+
legitimately half-finished while it is being written, and the four static
|
|
556
|
+
commands are where an author works during that; failing there would push them
|
|
557
|
+
back to inventing a value, which is the behaviour being replaced. Archiving is
|
|
558
|
+
where "done" acquires a hard definition, and a registry carrying an unanswered
|
|
559
|
+
question is not done.
|
|
560
|
+
|
|
561
|
+
*Re-measuring the entry before implementing it corrected it in four places,
|
|
562
|
+
per this file's own discipline.* **(1)** The proposed "WARNING from `check`,
|
|
563
|
+
ERROR at the gate" cannot be one diagnostic at two levels: `evaluateGate`'s
|
|
564
|
+
structure step already filters `validateStructure` to ERROR, so every WARNING
|
|
565
|
+
is discarded there. It needs its own gate step, which is what `never-red`
|
|
566
|
+
being the stated precedent actually implies. **(2)** The scope is the whole
|
|
567
|
+
merged registry, not the ids the change ADDs — and that is where it parts
|
|
568
|
+
company with `never-red`, whose narrower scope exists because "was ever
|
|
569
|
+
observed failing" is a fact about history that cannot be demanded
|
|
570
|
+
retroactively. An open question is a claim about the present. **(3)** The
|
|
571
|
+
entry over-counted the static extractor, which reads any literal property and
|
|
572
|
+
needed nothing, and under-counted `render`: without a section there, `SPEC.md`
|
|
573
|
+
— the only human review surface a requirement has — would not show that a
|
|
574
|
+
shipped requirement is open.
|
|
575
|
+
|
|
576
|
+
***(4)* is the one that had to ship in the same change, and it is the
|
|
577
|
+
question `Planned`'s preamble says to ask.** `splice.ts` carries the comment
|
|
578
|
+
*"Any other field is one the schema does not define, and not this module's to
|
|
579
|
+
rewrite or to remove"*, and that sentence is true **only while `open` is off
|
|
580
|
+
the schema**. Adding the field without adding the two writer branches would
|
|
581
|
+
mean a change that *answers* a question is approved by the gate against an end
|
|
582
|
+
state the registry on disk does not match — the merge commits a file still
|
|
583
|
+
carrying the closed question, with nothing anywhere comparing the two. Not a
|
|
584
|
+
path the entry's own reasoning could reach, because it reasons about the field
|
|
585
|
+
being added and this is about the field being removed.
|
|
586
|
+
|
|
587
|
+
*Cost, stated rather than hidden.* One unresolved question anywhere blocks
|
|
588
|
+
every unrelated change from archiving. That is the intended reading of a
|
|
589
|
+
registry that is not done, it is recoverable by answering the question, and it
|
|
590
|
+
is the same all-or-nothing shape `verify` already has. `SCHEMA_VERSION` is
|
|
591
|
+
unchanged: two new `code` values are additive to the `--json` envelope. The
|
|
592
|
+
type surface **does** move — `Requirement` gains a required `open`, so code
|
|
593
|
+
constructing one by hand rather than through `defineRequirements` needs the
|
|
594
|
+
field. In this repository that was 26 sites, every one of them a test literal
|
|
595
|
+
that already spelled `outOfScope: []`; no adopter reading requirements is
|
|
596
|
+
affected, and `defineRequirements` still takes it as optional.
|
|
597
|
+
|
|
598
|
+
- **This repository ran its own `changes/` → `status` → `archive` workflow for
|
|
599
|
+
the first time, and the change above is what went through it.** Every one of
|
|
600
|
+
the 80 requirements before it was written straight into the registry, so
|
|
601
|
+
`archive/` did not exist and `inspectRedEvidence` — shipped in `[0.9.3]` to
|
|
602
|
+
read it — reported all 252 declared scenarios as `unarchived`. The export had
|
|
603
|
+
no consumer that was not a test of itself for the second release running, and
|
|
604
|
+
this time the reason was not that nothing had been built on it but that the
|
|
605
|
+
repository had never produced the input it reads.
|
|
606
|
+
|
|
607
|
+
**The gate's first run failed on two of the five scenarios, and it was right.**
|
|
608
|
+
Both were the negative half of a pair — "says nothing when the list is empty",
|
|
609
|
+
"lets the gate through once every question is closed" — and both passed on
|
|
610
|
+
their first run, because an engine that has never heard of the field satisfies
|
|
611
|
+
"stays silent" trivially. `never-red` named them individually. The repair is
|
|
612
|
+
one scenario asserting the *discrimination* rather than two asserting each
|
|
613
|
+
side, which fails now and still refuses an implementation that warns
|
|
614
|
+
unconditionally later. This is the mechanism catching the exact defect class
|
|
615
|
+
the tautology entry under *Under consideration* is about, on the first change
|
|
616
|
+
it was ever pointed at, in the author's own work.
|
|
617
|
+
|
|
618
|
+
*A second defect surfaced after `--apply` and is worth recording because
|
|
619
|
+
nothing catches it.* The proposed spec read its params through `delta.added`
|
|
620
|
+
rather than by id. A delta is *also* keyed by the ids it ADDs
|
|
621
|
+
(`withProposedRequirements`), which is what makes the import the only line the
|
|
622
|
+
merge has to change — reaching through `added` loads during stage 1 and throws
|
|
623
|
+
once the import is repointed at the registry. The gate cannot see it, because
|
|
624
|
+
the gate runs before the rename; `check` cannot, because it executes nothing.
|
|
625
|
+
It appeared at the next `verify` as `spec-load-failed`. The workflow document
|
|
626
|
+
already shows the by-id accessor; what it does not say is that the other one
|
|
627
|
+
works until it doesn't.
|
|
628
|
+
|
|
629
|
+
*What the archive now reports*, and it is the shape the design predicts rather
|
|
630
|
+
than a backlog: one archived change, 5 `fail` rows, 228 `unarchived`. The two
|
|
631
|
+
retired scenario names remain in `first-run.json` as `pass` and produce no
|
|
632
|
+
rows at all, which is `inspectRedEvidence`'s documented behaviour for a record
|
|
633
|
+
naming a scenario no spec declares any more — measured here for the first time
|
|
634
|
+
rather than reasoned about.
|
|
635
|
+
|
|
636
|
+
### Changed
|
|
637
|
+
|
|
638
|
+
- **The publish job publishes through `npm` and requests an npm OIDC token, so
|
|
639
|
+
trusted publishing becomes a setting rather than a rewrite.** The provenance
|
|
640
|
+
work in `[0.7.2]` signs the release but does not change what authorises it: a
|
|
641
|
+
long-lived automation token, masked and protected, that publishes as us to
|
|
642
|
+
anyone holding it. This is the half of removing it that lives in the
|
|
643
|
+
repository. Promoted from `Planned`; what remains there is narrowed to the two
|
|
644
|
+
npmjs.com settings that actually retire the credential.
|
|
645
|
+
|
|
646
|
+
*The one thing the entry was promoted with as unmeasured is now measured, and
|
|
647
|
+
it decides the rollout.* The question was whether a failed OIDC exchange falls
|
|
648
|
+
back to the `_authToken` already in `.npmrc` or fails the publish outright.
|
|
649
|
+
It falls back. Read off npm 11.10.1's `lib/utils/oidc.js` rather than inferred:
|
|
650
|
+
the function documents itself as intended never to throw, every failure path
|
|
651
|
+
logs at verbose and returns, the whole body sits inside a `try/catch` that also
|
|
652
|
+
returns, and `opts[authTokenKey]` is assigned only on success. So this lands
|
|
653
|
+
beside the existing token with nothing registered yet, and a failed exchange is
|
|
654
|
+
a quiet fall back rather than a failed release — which is what makes the first
|
|
655
|
+
tagged release a cheap test instead of an irreversible bet.
|
|
656
|
+
|
|
657
|
+
*`pnpm publish` was never going to work, and that is now an observation rather
|
|
658
|
+
than a strong inference.* The entry reasoned from `pnpm/pnpm#11526` that pnpm
|
|
659
|
+
gets out of the way and npm does the work. The call site confirms it:
|
|
660
|
+
`npm/lib/commands/publish.js` invokes `lib/utils/oidc.js`, so the exchange is
|
|
661
|
+
inside npm's publish command and no flag passed to pnpm reaches it. Hence the
|
|
662
|
+
client swap, which is a cost this file has already priced once in the other
|
|
663
|
+
direction.
|
|
664
|
+
|
|
665
|
+
*Two details the swap turned up that the entry had not counted.* `npm publish`
|
|
666
|
+
has no `--no-git-checks` — it warns *"Unknown cli config"* and does no git
|
|
667
|
+
checks at all — so the flag is dropped rather than translated. And the job now
|
|
668
|
+
runs `corepack enable pnpm` rather than the bare `corepack enable` plus the
|
|
669
|
+
`COREPACK_ENABLE_STRICT` override the entry expected to re-add: naming the shim
|
|
670
|
+
leaves the image's own npm alone for `npm i -g npm@^11.5.1` to upgrade, which
|
|
671
|
+
is the same outcome with one fewer environment variable and no refusal that can
|
|
672
|
+
be switched off by accident. `--provenance` stays explicit even though npm's
|
|
673
|
+
OIDC path can enable it by itself, because it only does so while `provenance`
|
|
674
|
+
is at its default — passing it keeps the setting this job's decision rather
|
|
675
|
+
than a side effect of whether the exchange succeeded.
|
|
676
|
+
|
|
677
|
+
*What is not claimed.* Nothing here has run. The job is exercised once per
|
|
678
|
+
release and cannot be tested any other way, which is why the fallback
|
|
679
|
+
measurement came first and why the token and its guard both stay. `NPM_TOKEN`
|
|
680
|
+
is still required and still what authorises the publish until a release is
|
|
681
|
+
observed going out without it.
|
|
682
|
+
|
|
683
|
+
|
|
684
|
+
- **This repository's own judgement table now reads its scenario names through
|
|
685
|
+
`@am_shork/attest/inspect`, and what that measured is the point of the
|
|
686
|
+
change.** `intent-quality.md` names one read as the rot check every hand-judged
|
|
687
|
+
table needs — that a scenario a row claims is one a spec really declares — and
|
|
688
|
+
tells an adopter to get it from `inspectProject`. This repository's table got
|
|
689
|
+
it from `parseSpecs` and `findFiles` directly, because it was written while
|
|
690
|
+
both were internal. So the export shipped in `[0.9.2]` had **no consumer that
|
|
691
|
+
was not a test of itself**: `self/inspect.spec.ts` attests it, and nothing
|
|
692
|
+
built anything on it. Passing its own tests is not evidence that an export is
|
|
693
|
+
sufficient for the thing it was added for, and the only party who could have
|
|
694
|
+
found out otherwise was an adopter.
|
|
695
|
+
|
|
696
|
+
*Equivalence was measured rather than assumed, in both directions.*
|
|
697
|
+
`scanProject` sorts the same `findFiles` walk with the same `isSpecFile`
|
|
698
|
+
predicate, so the two reads should agree — and they do, compared as sorted
|
|
699
|
+
`(reqId, names)` pairs over the whole repo before the old call was deleted.
|
|
700
|
+
Then mutation-checked, because an assertion that still passes when the read
|
|
701
|
+
returns nothing is not reading anything: with the map stubbed empty the
|
|
702
|
+
rot-binding test fails and the other nine pass, which is the blast radius the
|
|
703
|
+
call actually has.
|
|
704
|
+
|
|
705
|
+
**What would not go through the export is the finding, and it is one thing.**
|
|
706
|
+
`Inspection.registry` is merged across registry files, so an id arrives with no
|
|
707
|
+
record of which file declared it. `corpus()` needs that — `source` is the scope
|
|
708
|
+
a cross-requirement rule sees, `divergentParam` keys on it, and the DOMAINS
|
|
709
|
+
completeness check uses it to hold the fixture registries out — and nothing in
|
|
710
|
+
the public export can reconstruct it. That half still deep-imports
|
|
711
|
+
`readRegistrySource`, with the reason written at the call site rather than left
|
|
712
|
+
as an unexplained inconsistency. It is filed as a measurement and **not** as a
|
|
713
|
+
decision to add a field: one caller wanting per-file attribution is one
|
|
714
|
+
adapter, and the entry under *Under consideration* about an outward-facing
|
|
715
|
+
judgement schema is waiting on exactly the second one.
|
|
716
|
+
|
|
717
|
+
*Cost.* `inspectProject` reads the registry as well, which this caller does not
|
|
718
|
+
need — one extra static read on a tree `corpus()` already walks once per call,
|
|
719
|
+
seven times per run. Paid deliberately: the alternative is deep-importing the
|
|
720
|
+
function whose reachability from outside the package is the thing being
|
|
721
|
+
demonstrated.
|
|
722
|
+
|
|
723
|
+
### Documentation
|
|
724
|
+
|
|
725
|
+
- **Design §11 now states that evidence outranks testimony, and that a
|
|
726
|
+
disagreement between them is a finding.** The registry and the gate's verdict
|
|
727
|
+
are evidence — machine-checked, recomputable, true of the tree as it stands. An
|
|
728
|
+
archived `proposal.md` is testimony: prose about a decision taken on one past
|
|
729
|
+
date that nothing has re-read since. A reader holding both holds claims in two
|
|
730
|
+
tenses with no stated order, and the order matters most exactly when they
|
|
731
|
+
conflict, because a stale proposal is more fluent than a registry and reads as
|
|
732
|
+
the more authoritative of the two. Decided from *Under consideration*, where it
|
|
733
|
+
had sat behind a blocker that was circular.
|
|
734
|
+
|
|
735
|
+
*The blocker was circular and naming the circle is what dissolved it.* The rule
|
|
736
|
+
was held back for describing a workflow the tool does not have — nothing read
|
|
737
|
+
the prose half — and the prose half was read by nothing **because no rule
|
|
738
|
+
existed for reading it**. Neither side moves on its own. The way out is the
|
|
739
|
+
split `[0.9.3]` already made for the propose-stage questions, where two of five
|
|
740
|
+
went to `CLAUDE.md` rather than into the document `init` writes: **this lands in
|
|
741
|
+
§11 only, and deliberately not in the instruction document.** §11 is a permanent
|
|
742
|
+
property of the design and its cost is paid once, here; the §10 document's cost
|
|
743
|
+
is paid in every adopting project's agent context forever, and that cost is what
|
|
744
|
+
the blocker was actually about. It is also the statement any later retrieval
|
|
745
|
+
surface would have to be built against, so writing it first is cheaper than
|
|
746
|
+
writing it after something depends on it.
|
|
747
|
+
|
|
748
|
+
*Re-measuring it before writing found the entry's own subject had stopped being
|
|
749
|
+
hypothetical, and falsified something this file recorded.* `[0.9.3]` states that
|
|
750
|
+
`proposal.md` in this repository "carries a link" — `CLAUDE.md` makes
|
|
751
|
+
`CHANGELOG.md` the single decision record, so a proposal restating an argument
|
|
752
|
+
would be a second store of it with nothing comparing the two — and concludes
|
|
753
|
+
there is nothing in it for an index to offer but its path. The very next change
|
|
754
|
+
to archive carries a proposal of **sixty-nine lines across five sections**, most
|
|
755
|
+
of its argument also present in the changelog entry. So the second store arrived
|
|
756
|
+
by ordinary means, in the first change that could produce one, written by an
|
|
757
|
+
author who knew the rule. The archived file is left as it is rather than trimmed
|
|
758
|
+
to a pointer: it is what the gate approved, and rewriting an archive to agree
|
|
759
|
+
with a claim about it is the failure this section is about, performed on itself.
|
|
760
|
+
§11 cites it as the measurement instead.
|
|
761
|
+
|
|
762
|
+
*What is deliberately not claimed.* The typed half cannot produce this conflict
|
|
763
|
+
— `first-run.json` is an `Outcome` in a schema-validated envelope, and a
|
|
764
|
+
scenario either is in the record or is not, so `inspectRedEvidence` reports an
|
|
765
|
+
absence rather than a contradiction. Nothing enforces the rule and nothing can:
|
|
766
|
+
reconciling prose against a registry is the ground truth §0 refuses to compute.
|
|
767
|
+
What §11 states is only the direction to reconcile in.
|
|
768
|
+
|
|
16
769
|
## [0.9.3] - 2026-08-18
|
|
17
770
|
|
|
18
771
|
### Added
|
|
@@ -5194,7 +5947,7 @@ time**.
|
|
|
5194
5947
|
|
|
5195
5948
|
*The first time that question was asked in advance, it paid, and how it paid is
|
|
5196
5949
|
worth a line because it is not what the two cases above would predict.* The
|
|
5197
|
-
`check` `empty-spec` entry — `[
|
|
5950
|
+
`check` `empty-spec` entry — `[0.9.0]`, the third entry to reproduce exactly
|
|
5198
5951
|
on its reported half — was not wrong about its own repair. Asking the question
|
|
5199
5952
|
anyway found the defect one step to the side: the fix it proposed was correct,
|
|
5200
5953
|
and the *obvious implementation of it* — make `check` agree with `verify` —
|
|
@@ -5205,83 +5958,6 @@ path the repair makes reachable; it is about **everything the repair would make
|
|
|
5205
5958
|
`check` agree with**, and an entry that names the right fix can still be
|
|
5206
5959
|
implemented wrongly by copying the half nobody has audited.
|
|
5207
5960
|
|
|
5208
|
-
- **Trusted publishing, which would delete `NPM_TOKEN` rather than add to it.**
|
|
5209
|
-
The provenance work in `[0.7.2]` signs the release but does not change
|
|
5210
|
-
what authorises it: a long-lived automation token, masked and protected, that
|
|
5211
|
-
publishes as us to anyone holding it. npm's trusted publishing exchanges the
|
|
5212
|
-
same OIDC identity the attestation already uses for a short-lived credential,
|
|
5213
|
-
which would remove the stored secret entirely and grant provenance as a
|
|
5214
|
-
property of the flow rather than a flag on the command. The deletion test
|
|
5215
|
-
points the right way for once: what it removes is the only long-lived
|
|
5216
|
-
credential this repository has.
|
|
5217
|
-
**The blocker this entry was filed with is retired, and it was the wrong
|
|
5218
|
-
question** — checked against npm's documentation on 2026-08-08. It asked
|
|
5219
|
-
whether npm's trusted publishers accept a **gitlab.com-hosted** project on the
|
|
5220
|
-
plan this package publishes under, and both halves come back clean: GitLab
|
|
5221
|
-
CI/CD is a first-class provider alongside GitHub Actions, and no plan
|
|
5222
|
-
requirement is stated anywhere in the documentation. The four fields it wants
|
|
5223
|
-
are `Pseudorca`, `attest`, the top-level CI file path (`.gitlab-ci.yml`, and it
|
|
5224
|
-
must carry the extension) and the allowed actions; environment name is optional
|
|
5225
|
-
and this project uses no GitLab environments. Shared runners are the only ones
|
|
5226
|
-
supported and the publish job carries no `tags:`, so it is already on them. The
|
|
5227
|
-
`repository.url` cross-check needs no separate verification — provenance
|
|
5228
|
-
already passes it with the same identity.
|
|
5229
|
-
Two things the entry did not anticipate, both of which change the shape of the
|
|
5230
|
-
change rather than blocking it. **`SIGSTORE_ID_TOKEN` is not replaced**: the
|
|
5231
|
-
documented GitLab snippet declares `NPM_ID_TOKEN` with
|
|
5232
|
-
`aud: "npm:registry.npmjs.org"` *beside* it, so this is an addition to the
|
|
5233
|
-
`id_tokens` block, not a swap. And **the YAML is not what deletes the
|
|
5234
|
-
credential** — enabling a trusted publisher leaves the automation token just as
|
|
5235
|
-
able to publish; what retires it is `Require two-factor authentication and
|
|
5236
|
-
disallow tokens` on the package, which npm states does not affect trusted
|
|
5237
|
-
publishers. That setting is the one that closes the loop this job's own comment
|
|
5238
|
-
opens, where an automation token is used *because* a granular token fails
|
|
5239
|
-
against 2FA on writes: trusted publishing does not bypass the prompt, it stops
|
|
5240
|
-
being a token.
|
|
5241
|
-
*What blocks the decision now is `pnpm`, and it is not a version to wait for.*
|
|
5242
|
-
**pnpm does not implement the OIDC exchange at all.** `pnpm/pnpm#11526` — the
|
|
5243
|
-
fix for the pnpm 11 regression that made this look like a supported path —
|
|
5244
|
-
shows what its "OIDC support" is: `actions/setup-node` writes
|
|
5245
|
-
`_authToken=${NODE_AUTH_TOKEN}` into `.npmrc`, pnpm used to pass the unresolved
|
|
5246
|
-
placeholder through verbatim, and the fix treats it as empty so that **npm's**
|
|
5247
|
-
credential exchange can be the sole auth source. pnpm gets out of the way; npm
|
|
5248
|
-
does the work. That matches npm's own wording, which requires publishing
|
|
5249
|
-
through npm directly or indirectly, and it matches the reports: every account
|
|
5250
|
-
of pnpm publishing under OIDC is GitHub Actions, and there is **no record of
|
|
5251
|
-
pnpm + GitLab + OIDC succeeding**. Stated at the strength the evidence carries:
|
|
5252
|
-
this is a strong negative inference, not a documented refusal.
|
|
5253
|
-
So the real cost is a tooling swap — `pnpm publish` back to `npm publish` in
|
|
5254
|
-
this one job — and it is a swap this file has already priced once in the other
|
|
5255
|
-
direction. The comment above records that moving to pnpm took
|
|
5256
|
-
`COREPACK_ENABLE_STRICT` with it, that override having existed only so a bare
|
|
5257
|
-
`npm` could run under a `packageManager` pinning pnpm; going back re-adds it.
|
|
5258
|
-
It also needs npm ≥ 11.5.1 where `node:22-slim` ships 10.x, so the job gains an
|
|
5259
|
-
npm upgrade step. None of that is prohibitive. It is simply a different
|
|
5260
|
-
decision from the one this entry was filed as, which is why it stays here.
|
|
5261
|
-
*One correction to the entry's own risk framing.* "It cannot be tested except
|
|
5262
|
-
by publishing" is half wrong: registering a trusted publisher on npmjs.com is
|
|
5263
|
-
free and reversible, and `NPM_TOKEN` can stay in place while it is tried. What
|
|
5264
|
-
decides whether this can be rolled out incrementally is whether a failed OIDC
|
|
5265
|
-
exchange falls back to the `_authToken` already in `.npmrc` or fails the
|
|
5266
|
-
publish outright — unmeasured, and the thing to establish before touching the
|
|
5267
|
-
job.
|
|
5268
|
-
*Method note, because it is the reusable part.* The pinned pnpm bundle was the
|
|
5269
|
-
obvious place to look and was the wrong one — `pnpm publish --help` on 10.28.0
|
|
5270
|
-
omits the flags it accepts, exactly as this job's comment already records for
|
|
5271
|
-
`--provenance`, so neither the help text nor the shipped bundle would have
|
|
5272
|
-
answered it. The answer was in the upstream pull request.
|
|
5273
|
-
**Promoted from `Under consideration` on 2026-08-08**, and what moved it is
|
|
5274
|
-
that the question it was filed as is answered. It asked whether npm's trusted
|
|
5275
|
-
publishers would accept this project at all; they will. What is left is a
|
|
5276
|
-
known tooling swap with a priced cost, which is a decision to build rather
|
|
5277
|
-
than a question — the preamble's own line, *a decision with an argument
|
|
5278
|
-
attached*. One thing stays unmeasured and is the first act of building it, per
|
|
5279
|
-
the discipline three paragraphs up: whether a failed OIDC exchange falls back
|
|
5280
|
-
to the `_authToken` already in `.npmrc` or fails the publish outright. That
|
|
5281
|
-
decides whether this rolls out incrementally beside the existing token or in
|
|
5282
|
-
one cut, and it is cheap to establish because registering a trusted publisher
|
|
5283
|
-
is free and reversible.
|
|
5284
|
-
|
|
5285
5961
|
## Under consideration
|
|
5286
5962
|
|
|
5287
5963
|
Candidates with **no decision yet**, between the two sections either side of it
|
|
@@ -5770,55 +6446,45 @@ have to gather again.
|
|
|
5770
6446
|
design it. If it comes back the same shape, the seam is real and the schema
|
|
5771
6447
|
follows; if it comes back different, `inspect` was the whole answer and the
|
|
5772
6448
|
schema never needs to exist.
|
|
5773
|
-
|
|
5774
|
-
|
|
5775
|
-
|
|
5776
|
-
|
|
5777
|
-
|
|
5778
|
-
|
|
5779
|
-
|
|
5780
|
-
|
|
5781
|
-
|
|
5782
|
-
|
|
5783
|
-
|
|
5784
|
-
|
|
5785
|
-
|
|
5786
|
-
|
|
5787
|
-
|
|
5788
|
-
|
|
5789
|
-
|
|
5790
|
-
|
|
5791
|
-
|
|
5792
|
-
|
|
5793
|
-
|
|
5794
|
-
|
|
5795
|
-
|
|
5796
|
-
|
|
5797
|
-
|
|
5798
|
-
|
|
5799
|
-
|
|
5800
|
-
|
|
5801
|
-
well-formed requirement.
|
|
5802
|
-
*Shape, if it is built:* an additive `open?: string[]`, naming the fields or
|
|
5803
|
-
questions still unresolved; a WARNING from `check`, so a proposal may be
|
|
5804
|
-
legitimately half-finished, and an ERROR at the archive gate, so "done" is not
|
|
5805
|
-
diluted. The same asymmetry `never-red` already runs on.
|
|
5806
|
-
*What blocks the decision — three things.* **(1)** The frequency is entirely
|
|
5807
|
-
unmeasured. No adoption report has recorded an agent inventing a param; the
|
|
5808
|
-
argument above is structural, and this project has twice been wrong about a
|
|
5809
|
-
hazard it reasoned to rather than measured. **(2)** The cheap half already
|
|
5810
|
-
works: writing the uncertainty into `rationale` as prose costs nothing, and
|
|
5811
|
-
the propose stage is human-reviewed regardless. What that does not buy is a
|
|
5812
|
-
gate, which is the whole question. **(3)** It is permanent surface — a schema
|
|
5813
|
-
field, a code, two troubleshooting sections, a History row, an `ATX-n` and its
|
|
5814
|
-
scenarios forever — for a hazard nobody has yet reported.
|
|
6449
|
+
**Nothing currently makes that table arrive, which is a separate problem from
|
|
6450
|
+
the three blockers and the one that gates all of them.** Raised in a product
|
|
6451
|
+
review of `[0.9.1]`–`[0.9.3]` on 2026-08-19. The deciding evidence is an
|
|
6452
|
+
artifact only an adopter can produce, and the path to producing it is: a row in
|
|
6453
|
+
the README's document table, then a 214-line page, then a hand pass over four
|
|
6454
|
+
questions, then writing the two mechanical properties from a prose
|
|
6455
|
+
description, then choosing to report back. Five voluntary steps with no
|
|
6456
|
+
scaffold at any of them, against a decision this entry has been waiting on
|
|
6457
|
+
since `[0.9.2]`. An entry blocked on evidence that no shipped surface asks for
|
|
6458
|
+
is blocked indefinitely, and the wait is not itself evidence of anything.
|
|
6459
|
+
*The proposal is scaffolding the mechanism while still withholding the answer,*
|
|
6460
|
+
which is the same split `[0.9.2]` already made and not a retreat from it: an
|
|
6461
|
+
`examples/` directory holding the completeness and rot-binding assertions with
|
|
6462
|
+
**no columns** — the enumeration that forces a row, and the check that a named
|
|
6463
|
+
scenario is one `inspectProject` really declares — and the judgement columns
|
|
6464
|
+
left for the adopter to add. What the page withholds is this repository's four
|
|
6465
|
+
columns, and that stays withheld; what it currently also withholds is the two
|
|
6466
|
+
properties it names as *mechanical*, and those are the half where a copy is not
|
|
6467
|
+
an echo, because their content is fixed and the fourth finding does not apply
|
|
6468
|
+
to them.
|
|
6469
|
+
*What blocks it.* Whether a skeleton with no columns is still a shape that
|
|
6470
|
+
comes back as a copy — the page's argument is that a template makes the answer
|
|
6471
|
+
return its own question, and it is untested whether an adopter reads an empty
|
|
6472
|
+
frame as an invitation or as a form to fill in. And it is a second artifact
|
|
6473
|
+
claiming to be runnable consumer usage of the public API, which `pre-commit-check`
|
|
6474
|
+
§6 already records as the category no gate reaches: the page's three samples
|
|
6475
|
+
are unasserted today, and a fourth in a directory that looks executable would
|
|
6476
|
+
be worse than a fenced block, not better, unless something runs it.
|
|
5815
6477
|
|
|
5816
6478
|
- **Prefix ownership is inferred rather than declared, and the cost arrives only
|
|
5817
6479
|
when adoption succeeds.** Design §11 records that the id grammar has no level
|
|
5818
6480
|
above the requirement: the prefix is a convention, nothing allocates it, and
|
|
5819
6481
|
`duplicate-prefix` can only report a collision that already happened.
|
|
5820
6482
|
spec-kit's answer is a roadmap table — a stable id, an intent, a scope
|
|
5821
|
-
boundary, dependencies — authored above the specs it decomposes into.
|
|
6483
|
+
boundary, dependencies — authored above the specs it decomposes into. That
|
|
6484
|
+
comparison is stated here rather than referred to, because the entry that
|
|
6485
|
+
introduced the spec-kit reading has since been built and now sits under
|
|
6486
|
+
`[0.9.4]` as the `open` field: an entry whose evidence rests on a
|
|
6487
|
+
neighbour is one a promotion silently empties.
|
|
5822
6488
|
*Two things have changed since that reading, and they point opposite ways.*
|
|
5823
6489
|
The proposal is **smaller** than it looked: the mechanism partly exists.
|
|
5824
6490
|
`loadRegistry` returns `prefixOwners` — first claim in sorted file order —
|
|
@@ -5857,53 +6523,26 @@ have to gather again.
|
|
|
5857
6523
|
rename can move is a second source with nothing comparing it against the
|
|
5858
6524
|
first. A derived index has no such copy to go stale: it is recomputed from the
|
|
5859
6525
|
archived deltas every time it is read.
|
|
5860
|
-
*What blocks the decision
|
|
5861
|
-
|
|
5862
|
-
|
|
5863
|
-
|
|
5864
|
-
|
|
5865
|
-
|
|
6526
|
+
*What blocks the decision, restated on 2026-08-19 because half of what was
|
|
6527
|
+
filed here is no longer true.* This entry was blocked on the empty archive and
|
|
6528
|
+
on the corpus being rename-free, and only the second still holds. `archive/`
|
|
6529
|
+
now has a real change in it — the `open` field, put through the workflow the
|
|
6530
|
+
same day — so the store this would index exists. Its delta carries `added` and
|
|
6531
|
+
nothing else, measured rather than assumed, so a backlink index is still
|
|
6532
|
+
testable only against fixtures for the operation it is actually about. Note
|
|
6533
|
+
what that says about the odds of the blocker clearing on its own: `--apply`
|
|
6534
|
+
refuses RENAMED outright, so no change this repository archives can ever
|
|
6535
|
+
exercise a rename until that refusal is lifted, and lifting it is a separate
|
|
6536
|
+
decision with its own reason (`splice.ts` — deleting an entry has no smaller
|
|
6537
|
+
span to fall back to). The blocker is therefore not "waiting for a rename to
|
|
6538
|
+
happen"; it is downstream of a refusal nobody has proposed removing. And one
|
|
6539
|
+
question the OpenViking reading does not answer, which is whether the chain is
|
|
6540
|
+
wanted at all. §7 applies
|
|
5866
6541
|
`RENAMED` to a `Map<id, Requirement>` and the old id is then simply gone —
|
|
5867
6542
|
deliberately, as far as anything written says. The case for recovering it is an
|
|
5868
6543
|
external reference that outlives the rename, a ticket or a commit message
|
|
5869
6544
|
naming a retired id, and nobody has reported one.
|
|
5870
6545
|
|
|
5871
|
-
- **The instruction document says how to write intent and nothing about how to
|
|
5872
|
-
read the record back.** §10's one-document-many-targets file is entirely about
|
|
5873
|
-
authoring, and the moment anything reads `archive/` — the entry above, and the
|
|
5874
|
-
one that shipped under `[0.9.3]` — an
|
|
5875
|
-
agent is holding prose from a proposal agreed on some past date against a
|
|
5876
|
-
registry that is current, with no stated order between them. OpenViking states
|
|
5877
|
-
one for its own store in the skill it ships to coding agents
|
|
5878
|
-
(`agent-plugins/skills/openviking-memory/SKILL.md`): retrieved memory is
|
|
5879
|
-
advisory, and the order is system and developer instructions, then the current
|
|
5880
|
-
request, then current environment and tool evidence, then memory — with "prior
|
|
5881
|
-
success never authorizes a destructive action now", and "store conclusions,
|
|
5882
|
-
not scrollback".
|
|
5883
|
-
*Here the same rule has a harder form, because the authoritative half is
|
|
5884
|
-
machine-checked.* The registry and the gate's verdict are evidence; an
|
|
5885
|
-
archived proposal is testimony about a decision taken once. Where they
|
|
5886
|
-
disagree, the disagreement is a finding — the exact thing this tool exists to
|
|
5887
|
-
report — and not something for a reader to reconcile quietly in favour of
|
|
5888
|
-
whichever it read last. That is a property of the design rather than of a
|
|
5889
|
-
file, so if it is taken it lands in design §11 first and in the instruction
|
|
5890
|
-
document second.
|
|
5891
|
-
*What blocks the decision:* it is a rule for reading a store nothing reads yet,
|
|
5892
|
-
so on its own it would document a hazard that cannot currently arise. It is the
|
|
5893
|
-
cheapest of the three and could ship first, at the price of being the only
|
|
5894
|
-
paragraph in that file describing a workflow the tool does not have — and §10's
|
|
5895
|
-
economics make that price the same in every adopter's agent instructions at
|
|
5896
|
-
once.
|
|
5897
|
-
**That blocker is retired, and what replaced it is narrower than the entry
|
|
5898
|
-
assumed.** Something reads `archive/` as of `[0.9.3]`, so the hazard can
|
|
5899
|
-
now arise. But what shipped reads the **typed** half — `first-run.json`, whose
|
|
5900
|
-
every value is an `Outcome` in a schema-validated envelope — and evidence that
|
|
5901
|
-
parses cannot disagree with the registry in the way testimony can: a scenario
|
|
5902
|
-
either is in the record or is not. The rule this entry wants is about the prose
|
|
5903
|
-
half, and the prose half is still read by nothing. So the store is no longer
|
|
5904
|
-
write-only and the entry's own subject is still hypothetical, which is a
|
|
5905
|
-
different blocker from the one it was filed with rather than none.
|
|
5906
|
-
|
|
5907
6546
|
- **Changing the value of an existing param passes no gate, and for seven of
|
|
5908
6547
|
them there is no record either.** Split out of the tautology entry at the
|
|
5909
6548
|
head of this section on 2026-08-18, where it was one sentence inside the
|
|
@@ -5956,6 +6595,189 @@ have to gather again.
|
|
|
5956
6595
|
looking at already being that page's subject, and this being a fifth of the
|
|
5957
6596
|
same kind.
|
|
5958
6597
|
|
|
6598
|
+
- **`/inspect` returns five exported shapes and the page telling people to build
|
|
6599
|
+
on them says nothing about whether they move.** Filed 2026-08-19 from a product
|
|
6600
|
+
review of `[0.9.1]`–`[0.9.3]`. `inspect.ts` exports `Inspection`,
|
|
6601
|
+
`ScenarioEvidence`, `EvidenceRow`, `ArchivedChangeRef` and
|
|
6602
|
+
`RedEvidenceInspection`, and `intent-quality.md` carries three samples
|
|
6603
|
+
destructuring them. `SCHEMA_VERSION` governs the `--json` envelope and nothing
|
|
6604
|
+
governs these.
|
|
6605
|
+
*The policy is not missing — its statement is.* The Versioning section at the
|
|
6606
|
+
head of this file already binds them: removing or renaming a public API bumps
|
|
6607
|
+
the minor, and under 0.x that is the breaking bump. So a reader who finds that
|
|
6608
|
+
section is already told what they need. The gap is that the reader this export
|
|
6609
|
+
was written for arrives through `intent-quality.md`, which is a method page
|
|
6610
|
+
reached from the README's document table, and nothing on the path between the
|
|
6611
|
+
two states it.
|
|
6612
|
+
*Why it is worth a decision rather than a one-line edit.* The two candidate
|
|
6613
|
+
sentences say opposite things to the person this needs. Restating the existing
|
|
6614
|
+
promise reads as a stability guarantee and is the honest summary of current
|
|
6615
|
+
policy; saying the shape may move before 1.0 is the more useful warning and
|
|
6616
|
+
discourages exactly the adopter whose table would decide the judgement-schema
|
|
6617
|
+
entry above. Which one is right depends on the 1.0 entry below, which is why
|
|
6618
|
+
this is filed rather than fixed.
|
|
6619
|
+
*One thing already measured, in the other direction.* `pre-commit-check` §6
|
|
6620
|
+
records that changing what `/inspect` returns breaks three usage samples that
|
|
6621
|
+
no gate can see. The internal awareness of this surface's fragility therefore
|
|
6622
|
+
exists and is written down; the outward-facing half is what does not.
|
|
6623
|
+
|
|
6624
|
+
- **The store worth retrieving is this file, not `archive/`, and no shape has
|
|
6625
|
+
been proposed for either.** Filed 2026-08-19 from a product review of
|
|
6626
|
+
`[0.9.1]`–`[0.9.3]`, and it is the OpenViking reading turned back on the
|
|
6627
|
+
repository that did the reading. Everything that came out of it is about
|
|
6628
|
+
`archive/`, and the entries are no longer counted here — the number was wrong
|
|
6629
|
+
on the day it was written, was corrected on 2026-08-19, and was wrong again by
|
|
6630
|
+
the next commit, when the precedence rule left for design §11. Three strikes on
|
|
6631
|
+
one figure is the section's own preamble arriving in the first person: a count
|
|
6632
|
+
kept in prose with nothing to check it goes wrong quietly, and the repair is to
|
|
6633
|
+
stop keeping one rather than to keep fixing it. But `archive/` holds `first-run.json` — typed,
|
|
6634
|
+
low-information, and now read — beside a `proposal.md` that in this repository
|
|
6635
|
+
carries a link, for the reason `[0.9.3]` gives: `CLAUDE.md` makes this file the
|
|
6636
|
+
single decision record. **So the store that actually holds decisions with their
|
|
6637
|
+
evidence attached is `CHANGELOG.md`, at 7,100 lines with the three foot
|
|
6638
|
+
sections past 1,700 of them,** and the property that makes it valuable — a
|
|
6639
|
+
design evaluated, priced and killed, with the reasoning that killed it — is
|
|
6640
|
+
exactly the property that makes it unreadable in one pass. It is append-only
|
|
6641
|
+
with no retrieval, which is the shape the OpenViking reading was about.
|
|
6642
|
+
*What an agent would actually consume is a composition, not a new store.*
|
|
6643
|
+
Given an `ATX-n`: its statement and params, the scenarios covering it, the
|
|
6644
|
+
first-run evidence for each, and the paths to the change that introduced it.
|
|
6645
|
+
Four of the five are already computable — `inspectProject` answers two,
|
|
6646
|
+
`inspectRedEvidence` the third and the archived refs the fourth — and the fifth,
|
|
6647
|
+
the reasoning, is the one with no index and no key. That asymmetry is the
|
|
6648
|
+
finding: the retrieval problem is not in the half that was just made readable.
|
|
6649
|
+
*One thing to unmerge before deciding.* `[0.9.3]` settled a question against
|
|
6650
|
+
the fifth static command, and it settled it correctly and on the right
|
|
6651
|
+
grounds — `first-run.json` is JSON, and `ATX-16`'s subject is reading
|
|
6652
|
+
TypeScript without evaluating it, so joining that roster would turn a security
|
|
6653
|
+
promise into a list of commands that do not run the suite. But that is a
|
|
6654
|
+
ruling about **roster membership**, and it is being carried as though it also
|
|
6655
|
+
ruled on whether such a command should exist. Those are separate, and only the
|
|
6656
|
+
first was argued.
|
|
6657
|
+
*What blocks the decision, and what would cheaply unblock it.* No measurement
|
|
6658
|
+
exists of whether a bundle like this changes what an agent does — the one datum
|
|
6659
|
+
quoted in `[0.9.3]` measured demand to read the archive **as prose**, which is
|
|
6660
|
+
a third question again. The proposal is therefore a probe and not a build,
|
|
6661
|
+
per this section's own discipline of re-measuring before implementing: assemble
|
|
6662
|
+
the bundle by hand for one requirement, run one real change against it, and
|
|
6663
|
+
record whether it was used, ignored, or used wrongly. One session, and it
|
|
6664
|
+
decides between a command, a skill surface, and nothing. Building any of the
|
|
6665
|
+
three before that measurement is the order `divergent-param` was designed in
|
|
6666
|
+
and the order this file has already paid for once.
|
|
6667
|
+
|
|
6668
|
+
- **A stage-1 spec can read its params through `delta.added` instead of by id,
|
|
6669
|
+
and the merge that repoints its import cannot repoint that.** Filed 2026-08-19
|
|
6670
|
+
from the first change this repository put through its own `changes/` workflow,
|
|
6671
|
+
where it was written by the author of the workflow's own tooling — which is
|
|
6672
|
+
most of why it is worth an entry rather than a fix.
|
|
6673
|
+
`delta()` returns the delta *and* the registry of what it adds
|
|
6674
|
+
(`withProposedRequirements`), so `reqs['ATX-81'].params.field` reads a proposed
|
|
6675
|
+
requirement before the merge and a merged one after it. That is the whole
|
|
6676
|
+
reason merging a spec changes its import and nothing else. Reaching through
|
|
6677
|
+
`added` — `d.added['ATX-81'].params.field` — is equally correct during stage 1
|
|
6678
|
+
and throws the moment `--apply` repoints the specifier at the registry, because
|
|
6679
|
+
a registry has no `added`.
|
|
6680
|
+
**Nothing catches it, and the reason is structural rather than an oversight.**
|
|
6681
|
+
The archive gate runs *before* the rename, so the spec it graded is not the
|
|
6682
|
+
spec the merge commits; `check` executes nothing, so it never evaluates the
|
|
6683
|
+
accessor; and the type checker is satisfied on both sides of the merge only
|
|
6684
|
+
because it is run against whichever half is currently on disk. It surfaced at
|
|
6685
|
+
the next `verify`, as `spec-load-failed` — a correct diagnosis of the symptom
|
|
6686
|
+
that names the file and not the cause. The window between `--apply` and the
|
|
6687
|
+
next `verify` is the whole exposure, and in a workflow that ends at `--apply`
|
|
6688
|
+
it is unbounded.
|
|
6689
|
+
**The guidance already exists and did not prevent it, which is the finding.**
|
|
6690
|
+
`registry.ts`'s doc comment on `delta` states the intended expression and the
|
|
6691
|
+
rejected one, in those terms, on the function itself; the document `init`
|
|
6692
|
+
writes shows the by-id form in its worked example. So this is not a gap in what
|
|
6693
|
+
is written. It is a case of the correct form and an incorrect one being
|
|
6694
|
+
*indistinguishable at the moment of writing* — both compile, both pass, both go
|
|
6695
|
+
red for the right reason — with the difference appearing one command later.
|
|
6696
|
+
*Two shapes, and they point opposite ways, which is why this is filed rather
|
|
6697
|
+
than fixed.* **(1)** A `check` diagnostic: `parser.ts` already walks property
|
|
6698
|
+
accesses inside spec bodies to find `.params`, so finding `.added` on a binding
|
|
6699
|
+
imported from a `requirements.delta` path is the same walk and the same file.
|
|
6700
|
+
It has to be a WARNING, because enumerating what a change adds is a legitimate
|
|
6701
|
+
thing for a spec to do, and a WARNING is exactly the strength that did not stop
|
|
6702
|
+
the author here. **(2)** A type-level refusal: have `delta()` return a type
|
|
6703
|
+
without `added`, so `typecheck:all` refuses it with no new diagnostic, no
|
|
6704
|
+
roster entry and no runtime cost. `DefinedDelta<T> = T & ProposedRequirements<T>`
|
|
6705
|
+
carries `added` because `T` does, so this means `Omit<T, 'added'>` — and that
|
|
6706
|
+
breaks the evaluating loader, which hands the same default export to
|
|
6707
|
+
`applyDelta` typed as `RegistryDelta`. Whether that break is real or just needs
|
|
6708
|
+
the internal path to name a different type is **unmeasured, and is the first
|
|
6709
|
+
thing to establish**; it decides between the two shapes rather than being a
|
|
6710
|
+
detail of one.
|
|
6711
|
+
*What blocks the decision:* frequency, and it is the same blocker the `open`
|
|
6712
|
+
entry carried before it — one occurrence, in one repository, on the first
|
|
6713
|
+
change ever run through the workflow. That is a report rather than a
|
|
6714
|
+
hypothetical, which is more than most entries here start with, and it is still
|
|
6715
|
+
n=1. What would settle it cheaply is the next change: if the by-id form is
|
|
6716
|
+
written without prompting, the guidance is sufficient and this is an anecdote;
|
|
6717
|
+
if the reach-through appears again, the two shapes above are worth pricing.
|
|
6718
|
+
|
|
6719
|
+
- **Nothing is frozen, and the evidence this repository is waiting for requires
|
|
6720
|
+
somebody to build on something.** Filed 2026-08-19 from a product review of
|
|
6721
|
+
`[0.9.1]`–`[0.9.3]`. Three releases in five days each added public surface, and
|
|
6722
|
+
as of `[0.9.3]` the CLI, `./define`, `./inspect`, the archive layout, the
|
|
6723
|
+
judgement schema and any retrieval surface are all simultaneously open. Every
|
|
6724
|
+
entry in this section that is blocked on an adopter — the judgement table above
|
|
6725
|
+
being the sharpest — is asking someone to invest in a surface that is
|
|
6726
|
+
documented as movable. That is a coherent thing to ask under 0.x and it is also
|
|
6727
|
+
the reason the ask may never be answered.
|
|
6728
|
+
*The proposal is a line rather than a milestone:* 1.0 freezes the CLI, `./define`
|
|
6729
|
+
and `./inspect`, and everything currently blocked on an adopter's evidence —
|
|
6730
|
+
the judgement schema, the backlink index, any archive retrieval — is explicitly
|
|
6731
|
+
1.x. What that buys is not a version number; it is the only condition under
|
|
6732
|
+
which "build your own counter-pressure on our export" is a reasonable thing to
|
|
6733
|
+
ask of somebody.
|
|
6734
|
+
*What blocks it.* Freezing `./inspect` five days after shipping it is freezing
|
|
6735
|
+
on one adapter, which is the objection this repository raises against every
|
|
6736
|
+
other premature interface and does not get to waive for its own. And the
|
|
6737
|
+
release-notes discipline cuts both ways here: a 1.0 is a claim about what the
|
|
6738
|
+
cost of adoption will be, and `Planned`'s own preamble records that **what a
|
|
6739
|
+
release costs its adopters is not known on the day it is cut**. There is no
|
|
6740
|
+
measurement to date of anyone adopting either export.
|
|
6741
|
+
|
|
6742
|
+
- **When one requirement carries several scenarios, some of those sets are
|
|
6743
|
+
differential evidence and nothing in the model says which.** Prompted by an
|
|
6744
|
+
external write-up (`columns.chicken-house.net`, 2025-11-10, "AI-First Testing
|
|
6745
|
+
Workflow"), which ran one test case against a system's REST API and against its
|
|
6746
|
+
web UI and found the empty-basket check implemented in the UI and absent from
|
|
6747
|
+
the API. Neither run failed against its own expectation; the finding was that
|
|
6748
|
+
two runs of the *same* case disagreed.
|
|
6749
|
+
|
|
6750
|
+
Attest already permits the arrangement — a `reqId` takes any number of
|
|
6751
|
+
attesting scenarios, and `cover` asks only for ≥1. What it cannot see is the
|
|
6752
|
+
difference between three scenarios covering three edges of one requirement and
|
|
6753
|
+
three scenarios crossing three interfaces at the *same* edge. The second set
|
|
6754
|
+
can be asserted to agree; the first cannot, and there is nothing to compare.
|
|
6755
|
+
This repository runs the pattern on itself — `tests/static-registry.spec.ts`
|
|
6756
|
+
asserts the two registry readers agree — so the structure is known to work
|
|
6757
|
+
where the two adapters are known to be two adapters.
|
|
6758
|
+
|
|
6759
|
+
*What blocks it, in the order that decides it.* **The seam is unmeasured.** One
|
|
6760
|
+
adapter is a hypothetical seam and two is a real one; nothing has been walked
|
|
6761
|
+
to establish whether any adopted registry has a `reqId` whose scenarios
|
|
6762
|
+
genuinely cross interfaces rather than covering one surface repeatedly. Until
|
|
6763
|
+
that is counted this is a pattern observed in somebody else's codebase, and it
|
|
6764
|
+
is cheap to count. **The fact is not in the model.** Which interface a scenario
|
|
6765
|
+
crosses is recorded nowhere the engine reads, so supplying it means new
|
|
6766
|
+
registry or spec surface — a per-scenario axis — paid for by every adopter
|
|
6767
|
+
including all of the ones with a single interface, in the shape of a thing they
|
|
6768
|
+
must learn not to write. Whatever carried it would also have to be a literal in
|
|
6769
|
+
the AST, because `cover` executes nothing and that is not negotiable for it.
|
|
6770
|
+
**There is no verdict to emit.** The engine cannot perform the comparison;
|
|
6771
|
+
agreement between two runs is an assertion and assertions belong to the test.
|
|
6772
|
+
The most it could report is that a requirement has scenarios on more than one
|
|
6773
|
+
axis, which is information with no pass or fail attached — a new output
|
|
6774
|
+
category rather than a new check. **And the prose alternative is untried.**
|
|
6775
|
+
Apply the deletion test before the field: if the workflow document `init`
|
|
6776
|
+
writes (§9) simply says that two scenarios crossing one requirement at
|
|
6777
|
+
different interfaces should assert the same expectations, does the practice
|
|
6778
|
+
happen anyway? If it does, the engine gained nothing and charged everyone a
|
|
6779
|
+
field for it.
|
|
6780
|
+
|
|
5959
6781
|
## Considered and rejected
|
|
5960
6782
|
|
|
5961
6783
|
Decisions **not** to build something, kept where they can be found before the
|
|
@@ -5966,6 +6788,41 @@ it sat between 0.2.0 and 0.1.7 for two releases, where standing still meant
|
|
|
5966
6788
|
sinking one version deeper each time a release was cut above it, and a rejection
|
|
5967
6789
|
filed under a version reads as belonging to it.
|
|
5968
6790
|
|
|
6791
|
+
### Rewriting the placeholder scan in `prose` to drop its tail slice
|
|
6792
|
+
|
|
6793
|
+
Priced on 2026-08-21, during a standing sweep for more of the ATX-59 shapes, and
|
|
6794
|
+
rejected by the measurement that was supposed to confirm it.
|
|
6795
|
+
|
|
6796
|
+
`prose` (`core/render.ts`) walks a statement character by character, and at every
|
|
6797
|
+
`{` it runs `/^\{\w+\}/.exec(text.slice(i))`. Read as source that is the §2
|
|
6798
|
+
quadratic-scan shape exactly: one fresh copy of the remaining string per `{`, so
|
|
6799
|
+
a statement of n braces copies n²/2 bytes — and a statement is prose a fork
|
|
6800
|
+
contributor writes and `render --check` reads under the static reader, which is
|
|
6801
|
+
the class that has actually shipped defects here. The fix was to be a sticky
|
|
6802
|
+
`/\{\w+\}/y` with `lastIndex = i`, which allocates nothing.
|
|
6803
|
+
|
|
6804
|
+
**It is not quadratic, because V8 does not copy.** `String.prototype.slice` on a
|
|
6805
|
+
string past a small threshold returns a *sliced string* — a view onto the parent
|
|
6806
|
+
with an offset, made in constant time — and an anchored regex over it reads only
|
|
6807
|
+
the few characters it needs, so the copy the source implies never happens.
|
|
6808
|
+
Measured through `renderMarkdown` on the built output, three adversarial shapes
|
|
6809
|
+
(all braces; alternating `{a`; a brace run followed by a word run), at 100k
|
|
6810
|
+
through 800k characters: every one of them scaled at a ratio of **1.77–1.89 per
|
|
6811
|
+
doubling**, i.e. linear, and 800,000 braces rendered in **106 ms** against
|
|
6812
|
+
ATX-59's 1,000 ms budget. A quadratic implementation at that size would be tens
|
|
6813
|
+
of seconds; this is the same order as the linear cases beside it.
|
|
6814
|
+
|
|
6815
|
+
So there is no defect and no change. What the sticky rewrite would buy is a
|
|
6816
|
+
constant factor nobody has shown a need for, against §5's rule that
|
|
6817
|
+
micro-optimisation with no named input is out of scope — the input *is* named
|
|
6818
|
+
here, and it is already absorbed. Filed rather than dropped because the site
|
|
6819
|
+
reads like the defect on inspection and will be proposed again by the next sweep
|
|
6820
|
+
that greps for `.slice(` near a loop: the answer is that V8's representation is
|
|
6821
|
+
load-bearing, the measurement is the evidence, and the thing to re-run is the
|
|
6822
|
+
ratio rather than the reasoning. Note the limit of that claim — it rests on a V8
|
|
6823
|
+
representation rather than on anything the language promises, which is safe for a
|
|
6824
|
+
Node CLI and would not transfer to a runtime that flattens eagerly.
|
|
6825
|
+
|
|
5969
6826
|
### Deferring the `typescript` import so `init` and `--version` do not pay it
|
|
5970
6827
|
|
|
5971
6828
|
Priced on 2026-08-13, in the session that enabled the compile cache above, and
|
|
@@ -6138,7 +6995,7 @@ weaker signal "some registry file failed to load, so suppress all
|
|
|
6138
6995
|
`orphan-test`" is the other candidate and is a strictly worse `check` on a
|
|
6139
6996
|
multi-file registry, where the other files are fine and their orphans are real.
|
|
6140
6997
|
**The wrong *advice* was separated out and fixed in `[0.7.0]`; the noise is
|
|
6141
|
-
fixed under `[
|
|
6998
|
+
fixed under `[0.7.4]`, and the blocker above was the wrong question.**
|
|
6142
6999
|
The mapping this entry called for could not be recovered, and did not have to
|
|
6143
7000
|
be: what a suppression needs is not the broken file's *prefix* but the ids it
|
|
6144
7001
|
declares, and those are written in its source. Reading them back is a source
|
|
@@ -6164,7 +7021,7 @@ breadth argument recorded above, which points the other way for both commands.
|
|
|
6164
7021
|
*What rejected it is that the argument for it was entirely the noise, and the
|
|
6165
7022
|
noise is gone.* Refusing was never justified by the load failure itself — it was
|
|
6166
7023
|
justified by the wall of derived findings burying it, and the collapse under
|
|
6167
|
-
`[
|
|
7024
|
+
`[0.7.4]` removed the wall without touching either command's breadth. What
|
|
6168
7025
|
is left on the table is the breadth argument alone, and it points the other way
|
|
6169
7026
|
for both: `check` is the command a pipeline runs first, its contract is seeing
|
|
6170
7027
|
everything it can still see, and answering with three findings instead of nine
|
|
@@ -6265,7 +7122,7 @@ measurement** saying parsing costs anything here — this reader is
|
|
|
6265
7122
|
`createSourceFile` per file with no type checking, which is the cheap half of
|
|
6266
7123
|
what arktype pays for, and the fan-out measurement — filed as an entry in
|
|
6267
7124
|
`Under consideration`, and now shipped as the two concurrency bounds in
|
|
6268
|
-
`[0.7.0]` and `[
|
|
7125
|
+
`[0.7.0]` and `[0.9.0]` — clocked `attest check` over 6000 spec files at
|
|
6269
7126
|
1.99 s total. Worse, a cache is a correctness hazard
|
|
6270
7127
|
of precisely the kind this tool exists to detect: a stale analysis makes a
|
|
6271
7128
|
**drift detector** report drift that has already been fixed, or miss drift
|
|
@@ -6287,7 +7144,7 @@ to be stronger than an mtime before the trade is even worth pricing.
|
|
|
6287
7144
|
|
|
6288
7145
|
### Zod Mini (`zod/v4-mini`) for the registry schema
|
|
6289
7146
|
|
|
6290
|
-
Evaluated alongside v4 (which shipped under `[
|
|
7147
|
+
Evaluated alongside v4 (which shipped under `[0.8.0]`), on the same
|
|
6291
7148
|
schema and the same 69-requirement registry, and it wins both numbers it is
|
|
6292
7149
|
supposed to win:
|
|
6293
7150
|
**0.249 ms** per parse against v3's 3.47 ms, and — the one v4 does not deliver —
|
|
@@ -6569,7 +7426,7 @@ current.
|
|
|
6569
7426
|
|
|
6570
7427
|
The mechanism `ATX-37` sat under `Under consideration` waiting for, kept there
|
|
6571
7428
|
on cost, and killed by measuring that cost rather than by paying it. The three
|
|
6572
|
-
sites it was aimed at are closed under `[
|
|
7429
|
+
sites it was aimed at are closed under `[0.7.4]` by hand-written scenarios;
|
|
6573
7430
|
what is rejected is the roster, and the argument is that **`src/cli/report.ts`'s
|
|
6574
7431
|
exports are the wrong roster in both directions at once.**
|
|
6575
7432
|
|
|
@@ -6860,6 +7717,8 @@ rather than a scorer, which ranks.
|
|
|
6860
7717
|
|
|
6861
7718
|
## Releases
|
|
6862
7719
|
|
|
7720
|
+
[0.10.0]: https://gitlab.com/Pseudorca/attest/-/tags/v0.10.0
|
|
7721
|
+
[0.9.4]: https://gitlab.com/Pseudorca/attest/-/tags/v0.9.4
|
|
6863
7722
|
[0.9.3]: https://gitlab.com/Pseudorca/attest/-/tags/v0.9.3
|
|
6864
7723
|
[0.9.2]: https://gitlab.com/Pseudorca/attest/-/tags/v0.9.2
|
|
6865
7724
|
[0.9.1]: https://gitlab.com/Pseudorca/attest/-/tags/v0.9.1
|