@am_shork/attest 0.8.0 → 0.9.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +886 -53
- package/README.md +3 -2
- package/bin/attest.js +42 -1
- package/dist/cli/report.js +19 -5
- package/dist/core/apply.d.ts +18 -1
- package/dist/core/apply.js +19 -2
- package/dist/core/locate.d.ts +19 -4
- package/dist/core/locate.js +116 -43
- package/dist/core/merge.js +218 -72
- package/dist/core/pipeline.js +45 -23
- package/dist/core/skill.js +88 -27
- package/dist/core/splice.d.ts +62 -3
- package/dist/core/splice.js +255 -13
- package/dist/core/static-registry.d.ts +55 -0
- package/dist/core/static-registry.js +141 -17
- package/dist/core/validator.d.ts +1 -0
- package/dist/core/validator.js +22 -0
- package/package.json +3 -2
package/CHANGELOG.md
CHANGED
|
@@ -13,6 +13,594 @@ input, removes/renames a public API or `--json` field, or changes a default
|
|
|
13
13
|
runtime behavior an existing invocation relies on — diagnostic message text is
|
|
14
14
|
not API.
|
|
15
15
|
|
|
16
|
+
## [0.9.1] - 2026-08-13
|
|
17
|
+
|
|
18
|
+
### Changed
|
|
19
|
+
|
|
20
|
+
- **Every command starts 11–28% faster, from three lines in `bin/attest.js`.**
|
|
21
|
+
`typescript` is the fixed toll on an `attest` invocation and nothing about the
|
|
22
|
+
design can move it: measured, the CLI's dependency graph costs ~790 ms to load
|
|
23
|
+
and the compiler is ~672 ms of that, against ~28 ms for commander, ~17 ms for
|
|
24
|
+
chalk and ~70 ms for zod. It is also not deferrable — every command except
|
|
25
|
+
`init` reads a registry or a spec through the AST, so the import is needed and
|
|
26
|
+
not merely eager. What *is* movable is the price of the same load, and Node's
|
|
27
|
+
compile cache moves it: V8 keeps its compilation output between runs. Two
|
|
28
|
+
independent A/B rounds, nine interleaved runs each, median — `check self`
|
|
29
|
+
1153 → 970 and 1106 → 971 ms, `cover self` 1122 → 951 and 1346 → 1195 ms,
|
|
30
|
+
`status` 1110 → 923 ms, `init` 904 → 711 and 1365 → 984 ms. The absolute
|
|
31
|
+
numbers move with the machine; the direction did not, in either round or on
|
|
32
|
+
any command.
|
|
33
|
+
**The launcher's import had to become dynamic, and that is the load-bearing
|
|
34
|
+
half.** A static `import` is hoisted and its whole graph evaluated before any
|
|
35
|
+
statement in the file runs, so with one the cache is enabled *after*
|
|
36
|
+
`typescript` has already been compiled and does nothing whatever — while still
|
|
37
|
+
reporting success and leaving every command working. Measured: the static
|
|
38
|
+
spelling came back at 873 ms against an unpatched 897 ms, inside the noise,
|
|
39
|
+
where the dynamic one is 669 ms. A defence that reads as present and is not is
|
|
40
|
+
this repository's dominant failure shape, and it fits in a three-line file.
|
|
41
|
+
No behaviour changes: same output, same exit codes, same files. The cache is
|
|
42
|
+
optional-chained, because `engines` allows Node 20 and the API arrived in
|
|
43
|
+
22.1 — on an older runtime the speed-up is simply absent. It never throws, and
|
|
44
|
+
a cache directory that cannot be written returns a status this ignores, since
|
|
45
|
+
the only thing a failure costs is the saving. It is written under the OS temp
|
|
46
|
+
directory and never into the project, which is the one place every other file
|
|
47
|
+
Attest writes belongs.
|
|
48
|
+
|
|
49
|
+
- **`loadRegistry`'s fan-out is bounded — the third one, where `[0.9.0]` closed
|
|
50
|
+
"both".** That entry is not wrong about what it did; it is wrong about how many
|
|
51
|
+
there were. `locate.ts` reads registry files through a
|
|
52
|
+
`Promise.all(paths.map(…))` of its own, so `check` on a tree carrying N
|
|
53
|
+
`*.reqs.ts` held N reads and N `SourceFile`s at once — the same shape, on the
|
|
54
|
+
same command, against the same input nobody here chooses. Measured in
|
|
55
|
+
`tests/locate-fanout.spec.ts`, which now counts a third pool: **64 before, 32
|
|
56
|
+
after**, on 64 registry files. Unbounded, the figure is the count of the files;
|
|
57
|
+
bounded, it is the constant.
|
|
58
|
+
*Why it hid is the part worth keeping.* The other two live in functions named
|
|
59
|
+
for walking and parsing, and both had a comment about concurrency saying
|
|
60
|
+
nothing about a bound. This one has a comment that reads as a considered
|
|
61
|
+
decision — "read the files concurrently, then fold the results in sorted file
|
|
62
|
+
order: the issue list stays deterministic regardless of which one finished
|
|
63
|
+
first" — and it is a correct, load-bearing answer to a *different* question.
|
|
64
|
+
Something that has visibly been thought about does not get thought about again,
|
|
65
|
+
which is what put a third copy of one shape past a fix that named itself as
|
|
66
|
+
covering all of them.
|
|
67
|
+
*The reader is what makes it more than arithmetic here.* `parseSpecs` bounds
|
|
68
|
+
sources; this bounds whatever the reader holds, which under `--eval`,
|
|
69
|
+
`verify` and `archive` is a Vite SSR module evaluation rather than a string.
|
|
70
|
+
Hence a third constant rather than sharing one, on the reason `WALK_CONCURRENCY`
|
|
71
|
+
already gives: the three bound different resources and cannot be tuned
|
|
72
|
+
together.
|
|
73
|
+
Descriptor exhaustion is still the failure it invites and still unmeasured —
|
|
74
|
+
Windows uses Win32 handles rather than POSIX descriptors, so `ulimit -n` does
|
|
75
|
+
not govern it there, and Linux is the half CI runs. The peak is portable
|
|
76
|
+
arithmetic, which is the standard both earlier halves were accepted on.
|
|
77
|
+
No behaviour change and no `ATX-n`, on that same precedent: the registry, the
|
|
78
|
+
issue list, its order and `prefixOwners` are identical, all asserted beside the
|
|
79
|
+
peak. The fold is now driven by index rather than by arrival, which is what
|
|
80
|
+
keeps "sorted file order" true through a pool that completes out of order —
|
|
81
|
+
the same indexing `parseSpecs` uses, for the same reason.
|
|
82
|
+
|
|
83
|
+
### Fixed
|
|
84
|
+
|
|
85
|
+
- **`--apply` wrote a `*.reqs.ts` that no longer parses, when a MODIFIED entry
|
|
86
|
+
changed two `params` and one of them had an integer-like key.** The write-back
|
|
87
|
+
rests on an ordering property stated in `splice.ts` and nowhere enforced: the
|
|
88
|
+
walk emits its edits in ascending offset order, so the pass at the bottom can
|
|
89
|
+
be a `reverse()` rather than a numeric sort — "an ordering that holds by
|
|
90
|
+
construction beats one restored after the fact". It did not hold by
|
|
91
|
+
construction. `registryEntryLayouts` handed the layout back as plain objects,
|
|
92
|
+
and **JavaScript hoists an integer-like key to the front of every object**, so
|
|
93
|
+
`params: { budgetMb: 12, 10: 'ten' }` was described in an order the file never
|
|
94
|
+
wrote. A `params` key genuinely can be a number: `staticName` spells a numeric
|
|
95
|
+
literal key as `String(value)` and the schema's `z.record(z.string(), …)`
|
|
96
|
+
accepts the result, on both reader paths.
|
|
97
|
+
The consequence is the one shape this command must not have. With the order
|
|
98
|
+
reversed, the lower edit was applied first and every offset after it shifted
|
|
99
|
+
underneath the spans still to be written — measured, changing `budgetMb`
|
|
100
|
+
`12 → 4096` alongside `10: 'ten' → 'a'` produced `params: { budgetMb: 4096,
|
|
101
|
+
10'a'n' },`, off by exactly the two bytes the first replacement grew. The
|
|
102
|
+
registry was **already on disk** by then: `applyMerge` writes each file before
|
|
103
|
+
`verifyWritten` re-reads it, so the re-read caught it and stopped the merge —
|
|
104
|
+
which is what that check is for — but what it stopped at was a hand-written,
|
|
105
|
+
hand-commented registry left unparseable, and the merge's own account of the
|
|
106
|
+
failure is an `internal-error` naming the file.
|
|
107
|
+
The fix is the containers, not a sort. `fields`, `paramKeys` and the layout map
|
|
108
|
+
itself are `Map`s, which keep insertion order for every key type, so the
|
|
109
|
+
property is back to holding by construction rather than by the keys happening
|
|
110
|
+
not to be numbers — the standard `keySource` and `compareIds` are already
|
|
111
|
+
written to, arriving at the one place that had been resting on an upstream
|
|
112
|
+
grammar. Contained to `splice.ts`, which is the only consumer.
|
|
113
|
+
No new requirement: ATX-76 already says the write-back replaces *the source
|
|
114
|
+
span of each value that changed*, and this was a defect against that sentence
|
|
115
|
+
rather than a gap in it. The regression sits in `tests/splice.spec.ts`, at the
|
|
116
|
+
seam the invariant lives on; reaching it through a `self/` scenario would need
|
|
117
|
+
a whole fixture project to express one numeric key, for a weaker signal.
|
|
118
|
+
|
|
119
|
+
- **The package shipped a file its own `exports` map forbade reading.** npm puts
|
|
120
|
+
`package.json` in every tarball whatever `files` says, and the map named only
|
|
121
|
+
`.` and `./define` — so `@am_shork/attest/package.json`, which tooling reaches
|
|
122
|
+
for to read a dependency's version, came back
|
|
123
|
+
`ERR_PACKAGE_PATH_NOT_EXPORTED`. Measured against a real installed tarball,
|
|
124
|
+
not reasoned about. Fixed by naming it, which is the whole change.
|
|
125
|
+
*The test that should have caught it was checking the wrong half.* "Ships the
|
|
126
|
+
entry points the exports map promises" asserted that `dist/index.js`,
|
|
127
|
+
`dist/core/registry.js` and `bin/attest.js` **exist** in the install — which an
|
|
128
|
+
`exports` map blocking every subpath would leave exactly as true. Existence and
|
|
129
|
+
reachability are decided by different fields, `files` shipping the bytes and
|
|
130
|
+
`exports` deciding what may address them, so one cannot stand in for the other.
|
|
131
|
+
`tests/consumer.spec.ts` now resolves each promised specifier from inside the
|
|
132
|
+
consumer project, asserting line by line so the diff names the one that
|
|
133
|
+
stopped. It went red on `./package.json` and green on the other two, which is
|
|
134
|
+
what says the new test measures the thing rather than the fix.
|
|
135
|
+
|
|
136
|
+
- **`cli/report.ts` was carrying a false reason for a defence, and it
|
|
137
|
+
contradicted `render.ts`.** The comment on `formatCoverage` justified
|
|
138
|
+
sanitising a requirement id with "under `--eval` the registry is whatever the
|
|
139
|
+
module exported, and `evalReader` checks that it is an object and nothing
|
|
140
|
+
further" — true when it was written, and untrue since ATX-38 made the
|
|
141
|
+
evaluating reader validate against the same schema as the static one. So
|
|
142
|
+
`^[A-Z]+-\d+$` holds on both paths, `render.ts` says so explicitly, and the two
|
|
143
|
+
modules read as disagreeing about whether a registry id can carry a control
|
|
144
|
+
character.
|
|
145
|
+
The sanitising stays and the reason is corrected: the grammar is held
|
|
146
|
+
somewhere else, `CoverageRow.reqId` is a `string`, and the function is
|
|
147
|
+
exported — a defence that lapses when the other one does is not a defence,
|
|
148
|
+
which is the standard `splice.ts`'s `keySource` is already written to. No
|
|
149
|
+
behaviour change; the call was, and remains, one that cannot fire through any
|
|
150
|
+
command. Recorded because in this repository a comment is the decision record,
|
|
151
|
+
and a false one is read as evidence the next time the question comes up.
|
|
152
|
+
|
|
153
|
+
## [0.9.0] - 2026-08-12
|
|
154
|
+
|
|
155
|
+
### Added
|
|
156
|
+
|
|
157
|
+
- **`--apply` writes MODIFIED requirements back, at the granularity of a value
|
|
158
|
+
rather than an entry.** `merge.ts` refused any delta carrying `modified` for
|
|
159
|
+
five releases, and what that refusal cost is on the record rather than
|
|
160
|
+
estimated: in one adopting project **5 of 19 changes carried `modified`**, each
|
|
161
|
+
merged by hand with a script the author rewrote five times in a session, which
|
|
162
|
+
located ` 'AUTH-7': { … },` in the registry **by regex**. `[0.6.0]` cites
|
|
163
|
+
eight hand-written Python scripts — one of them that same regex locator — as
|
|
164
|
+
the evidence that this step must not be left to the adopter, so the refusal had
|
|
165
|
+
reproduced the tool it was meant to retire, inside a project that had `--apply`
|
|
166
|
+
available.
|
|
167
|
+
**The argument the refusal rested on was subtly wrong, and correcting it is
|
|
168
|
+
what made this buildable.** `--apply` is allowed to edit a hand-written
|
|
169
|
+
registry because "the result is checkable by re-reading it", and the entry
|
|
170
|
+
proposing this narrowed on the wrong axis in consequence — accept an entry only
|
|
171
|
+
when its whole span is comment-free, which lowers the *rate* of destruction and
|
|
172
|
+
not its *size*. A re-read compares values and is blind to what was **lost**: a
|
|
173
|
+
replacement that ate a comment passes one cleanly. So the unit of the edit is
|
|
174
|
+
the value, not the entry. `statement` and `rationale` replace a string
|
|
175
|
+
literal's span, where a comment cannot be present by construction; a `params`
|
|
176
|
+
key replaces that key's value span and a new key is a pure insertion;
|
|
177
|
+
`outOfScope` replaces the array literal. Everything else in the entry — the
|
|
178
|
+
keys, the commas, the layout, every comment on a field that did not move — is
|
|
179
|
+
bytes the edit never addresses.
|
|
180
|
+
**Refused, still whole, when a span that would be overwritten carries a
|
|
181
|
+
comment**, as `apply-unsupported-delta` naming the requirement and the field.
|
|
182
|
+
Measured over this repository's own registry before it was built, on
|
|
183
|
+
2026-08-11: **75 entries, 3 carrying a comment anywhere inside them, all three
|
|
184
|
+
inside a `params` value**, and none at all inside a statement or a rationale.
|
|
185
|
+
So the refusal is the rare case rather than the categorical one, and every
|
|
186
|
+
refusal is still a merge nobody has to trust.
|
|
187
|
+
**RENAMED and REMOVED are deliberately still refused, and the granularity is
|
|
188
|
+
the reason they are now separable.** "REMOVED cannot say which comments
|
|
189
|
+
belonged to the entry it deletes" is the sentence the two shared, and it
|
|
190
|
+
survives untouched: deleting an entry has no smaller span to fall back to,
|
|
191
|
+
while the same question one level in is answered by not replacing that level.
|
|
192
|
+
**Two things this brought with it.** The re-read is now performed rather than
|
|
193
|
+
argued — after writing, the registry is read back and every id the edit was
|
|
194
|
+
answerable for is compared against what the gate proved, ADDED included, since
|
|
195
|
+
a check covering only the newer half would leave the older claim exactly as it
|
|
196
|
+
was. And every registry edit is computed before any file is written, so
|
|
197
|
+
"refused whole" is true of the write-back and not only of the delta's shape;
|
|
198
|
+
before this, the one thing that could fail during text generation — a
|
|
199
|
+
`__proto__` param key — failed one file at a time.
|
|
200
|
+
Held as `ATX-76`, whose scenarios read the registry as **text**: a merge that
|
|
201
|
+
regenerated the entry would produce a file reading back as exactly the same
|
|
202
|
+
requirement, so nothing about the parsed end state can tell a value-sized edit
|
|
203
|
+
from a rewritten one. `ATX-54` keeps the refusal half over what is left.
|
|
204
|
+
*One limit, recorded so nobody assumes otherwise:* a patch cannot express
|
|
205
|
+
**deleting** a `params` key — `applyDelta` merges keys and never drops one — so
|
|
206
|
+
write-back does not cover that, and no amount of span work would.
|
|
207
|
+
|
|
208
|
+
- **The workflow document now describes an inner loop for stage 2.** Its only
|
|
209
|
+
instruction there was `attest archive`, which applies the delta and runs the
|
|
210
|
+
base suite, so an agent iterating toward green paid the full gate after every
|
|
211
|
+
edit — and `status` cannot help, its two obligations both being settled at the
|
|
212
|
+
end of stage 1. The addition needs no engine change: run the proposed spec file
|
|
213
|
+
directly while you are still making it pass, keep the gate for the verdict.
|
|
214
|
+
Measured before it was written — a `*.proposed.spec.ts` runs under a plain
|
|
215
|
+
Vitest invocation, delta import included — and it carries the two things that
|
|
216
|
+
make it safe and the one that makes it not a verdict: nothing but the gate ever
|
|
217
|
+
writes `first-run.json`, `status` still answers the readable half, and a green
|
|
218
|
+
file does not predict the gate, because the direct run gets the project's
|
|
219
|
+
environment and the gate's child run is isolated. That last point is the
|
|
220
|
+
`tests-red` section's existing subject, so it is pointed at rather than
|
|
221
|
+
restated.
|
|
222
|
+
|
|
223
|
+
- **`status` applies the change's delta and refuses the report when it does not
|
|
224
|
+
fit, which no static command checked at all.** `applyDelta` produces six
|
|
225
|
+
diagnostics — a rename whose target exists or whose source does not, a
|
|
226
|
+
modification of an absent id or one leaving the requirement invalid, an
|
|
227
|
+
addition that is invalid or conflicts — and nothing reached them without
|
|
228
|
+
running the suite first. `check` reads deltas, but only to attribute proposed
|
|
229
|
+
specs; it never applies one. So a delta wrong in any of those six ways was
|
|
230
|
+
first heard from `archive`, in the workflow position where the specs and the
|
|
231
|
+
implementation are already written, about a fact that was available the moment
|
|
232
|
+
the delta was saved.
|
|
233
|
+
Refusal rather than a row, because there is no row that could be right: the
|
|
234
|
+
states this command reports are progress toward an *applied* registry, and a
|
|
235
|
+
delta that cannot be applied has none to be progress toward — an `unproven`
|
|
236
|
+
against it is a true-looking sentence about a change that cannot exist.
|
|
237
|
+
Held as `ATX-78` rather than under `ATX-33`, which the cheap implementation
|
|
238
|
+
makes clear: `ATX-33` constrains what an ERROR from this command *means*, and
|
|
239
|
+
an implementation that never detects an unapplicable delta satisfies it
|
|
240
|
+
completely, vacuously, having no ERROR to constrain. This is the detection —
|
|
241
|
+
the same split `ATX-69` records against `ATX-65`.
|
|
242
|
+
**Behaviour change, and the reader it costs is not the one being fixed.**
|
|
243
|
+
Applying a delta needs the registry, which this command did not read before, so
|
|
244
|
+
a project whose registry is not a literal now gets `registry-not-static` and no
|
|
245
|
+
report where it got a report, and gets it back only under `--eval`. That is the
|
|
246
|
+
trade `ATX-16` already makes for `check`, `cover` and `render`, arriving at the
|
|
247
|
+
fourth static command. The registry is read through whichever reader the
|
|
248
|
+
command was asked for, so `--eval` moves both halves together rather than
|
|
249
|
+
quietly evaluating one to make the other work.
|
|
250
|
+
|
|
251
|
+
### Fixed
|
|
252
|
+
|
|
253
|
+
- **`status` told the reader to write a scenario that was already on disk.**
|
|
254
|
+
`changeMergedPlan` returns the specs neither parse could read, and says in
|
|
255
|
+
place why they block: an unreadable proposed spec is a scenario the gate would
|
|
256
|
+
otherwise report as absent. `archive` acts on that list before it runs
|
|
257
|
+
anything, on the argument that a plan known to be short cannot grade a change
|
|
258
|
+
(ATX-65). `status` took the plan from the same call and destructured only
|
|
259
|
+
`merged`, dropping the issues — and every state it prints is computed *from*
|
|
260
|
+
that plan, so a claimed `*.proposed.spec.ts` that failed to parse left its
|
|
261
|
+
requirement with no scenarios in the plan and the row came back
|
|
262
|
+
`no-scenario`, with `issues` empty and `ok` true.
|
|
263
|
+
**Not an under-report but the inverse of one**, which is why it is a fix rather
|
|
264
|
+
than a widening: the command whose stated contract is to be a projection of the
|
|
265
|
+
gate answered the opposite of the gate about the same change, and the advice it
|
|
266
|
+
produced — write the scenario — is work the reader has already done. The one
|
|
267
|
+
thing that makes it recoverable is that `status` writes nothing.
|
|
268
|
+
**Behaviour change, and it moves an exit code.** A change in that state now
|
|
269
|
+
fails with `unreadable-file` and exits 1 where it printed a table and exited 0.
|
|
270
|
+
Anything wrapping `status` in a loop sees red on input that used to pass — but
|
|
271
|
+
the input is a spec file that does not parse, and the table it used to print
|
|
272
|
+
was wrong about it.
|
|
273
|
+
Held under `ATX-33` rather than as a new requirement: the rule it is an
|
|
274
|
+
exception to is untouched — an unmet obligation is still a row, never a verdict
|
|
275
|
+
— and what moved is which input that exception covers.
|
|
276
|
+
**Stating it exposed that the exception could not be enumerated, so it is now a
|
|
277
|
+
class and the code list is gone rather than extended.** `ATX-33` had named the
|
|
278
|
+
one code that could make `status` fail, and adding a second is what showed the
|
|
279
|
+
shape: the roster it can *actually* fail with was already longer than the list
|
|
280
|
+
on the day the list was written — a rejected change name and an unusable
|
|
281
|
+
compiler were never in it — and it grows with every input the command learns to
|
|
282
|
+
read. A param extended per failure path is a name for the implementation rather
|
|
283
|
+
than a contract, and it fails in the direction that hides, since a code missing
|
|
284
|
+
from it makes the requirement quietly false rather than red. What the statement
|
|
285
|
+
carries instead is the property worth preserving: **an ERROR from `status`
|
|
286
|
+
always means there is no report**, so a reader who sees one never has to ask
|
|
287
|
+
whether progress was being scored. That is **the first `params` key ever
|
|
288
|
+
removed from this registry**, against 57 added and one value changed; the
|
|
289
|
+
removal is the safe direction for the same reason those additions were, since
|
|
290
|
+
what the scenarios assert is broader than what the list named, not narrower.
|
|
291
|
+
**The measurement suite priced it in two places and one of them was a
|
|
292
|
+
finding.** The restated sentence is universally quantified, so
|
|
293
|
+
`tests/intent-rule-candidates.spec.ts` demanded a `QUANTIFIED` row and got the
|
|
294
|
+
`sites` shape — the members are early returns in one function, so nothing
|
|
295
|
+
enumerates them and no scenario can iterate them, which is the `ATX-37`
|
|
296
|
+
position that table exists to record. Two sites were unreached; one is closed
|
|
297
|
+
here by a scenario, and **the compiler path stays open as a measured gap,
|
|
298
|
+
taking that table from one to two**. It was not created here: `status` could
|
|
299
|
+
always fail that way with nothing attesting it, and naming the failing half as
|
|
300
|
+
a class is what made the site visible at all.
|
|
301
|
+
No `SCHEMA_VERSION` bump: `issues[]` already carries this code and the envelope
|
|
302
|
+
is unchanged. `unreadable-file` is not a new code either — what is new is a
|
|
303
|
+
fourth command that can raise it, which is why the troubleshooting section now
|
|
304
|
+
says which commands stop on it and which keep going.
|
|
305
|
+
|
|
306
|
+
- **The workflow document said a proposed spec stays out of a plain `vitest run`,
|
|
307
|
+
and it does not.** `*.proposed.spec.ts` ends in `.spec.ts`, so an ordinary
|
|
308
|
+
`**/*.spec.ts` include — Vitest's own default among them — matches it;
|
|
309
|
+
confirmed by running one. Only *Attest's* scope excludes it. The claim was
|
|
310
|
+
wrong in the direction that surprises: during stage 1 those scenarios are red
|
|
311
|
+
by construction, so an adopter following this document is told their own
|
|
312
|
+
`npm test` will be unaffected and then watches it go red, at the exact moment
|
|
313
|
+
the framework is asking them to trust a workflow they have just met. The
|
|
314
|
+
sentence now claims only what is true, and the optional `exclude` is shown —
|
|
315
|
+
as the reader's decision about their own suite, which is not one Attest makes
|
|
316
|
+
for them.
|
|
317
|
+
*That sample spreads `configDefaults.exclude`, and the reason is worth the
|
|
318
|
+
line it costs.* Vitest's `exclude` **replaces** its defaults rather than
|
|
319
|
+
extending them, so the obvious one-liner silently un-excludes
|
|
320
|
+
`**/node_modules/**` — measured, not inferred: a bare
|
|
321
|
+
`exclude: ['**/*.proposed.spec.ts']` collects and runs a `*.spec.ts` planted
|
|
322
|
+
inside `node_modules`. This repository's own `vitest.config.ts` has always
|
|
323
|
+
re-added those globs by hand, so the trap was already known here and would
|
|
324
|
+
still have shipped in advice written for someone else. A sample in a document
|
|
325
|
+
is executed by whoever copies it, and this one was two lines from turning a
|
|
326
|
+
cosmetic complaint into a broken suite.
|
|
327
|
+
|
|
328
|
+
- **The workflow document contradicted itself about `apply-unsupported-delta`,
|
|
329
|
+
and the half an agent branches on was the stale half.** The commit that taught
|
|
330
|
+
`--apply` to write MODIFIED back edited the prose in `skill.ts` and left the
|
|
331
|
+
code table eleven lines above it reading `--apply` writes back ADDED only, and
|
|
332
|
+
this delta carries more. So an agent meeting that code looked it up, concluded
|
|
333
|
+
the delta carried something unsupported, and would have gone off splitting
|
|
334
|
+
MODIFIED out of it — when the actual cause is now most often the other one the
|
|
335
|
+
same code covers, a comment sitting inside the span a MODIFIED value would be
|
|
336
|
+
written over. **A wrong cause costs more than a missing one**, because it sends
|
|
337
|
+
the reader to a repair that cannot work; the row now names both.
|
|
338
|
+
*What this says about the gate that exists.* `ATX-57` holds that every code the
|
|
339
|
+
workflow names is one the engine can emit, and its rationale records the
|
|
340
|
+
one-directionality as deliberate — the document has no business naming every
|
|
341
|
+
code, so the obligation is only that what it names exists. That is untouched
|
|
342
|
+
here and still right: the code existed, and what moved was its *meaning*.
|
|
343
|
+
`skill.ts`'s own note said "read this file when a release adds a diagnostic",
|
|
344
|
+
which is the rule that would not have caught this. It now says *or changes what
|
|
345
|
+
one means*, and to grep the body for the code rather than trusting that the
|
|
346
|
+
section just edited was its only mention.
|
|
347
|
+
*No `ATX-n`.* The obligation this would state — every description in the
|
|
348
|
+
workflow matches what the engine does — is the one nothing can check, which is
|
|
349
|
+
why `ATX-57` stops where it does. Adding a requirement that no scenario can
|
|
350
|
+
falsify would be a green tick over an unguarded property.
|
|
351
|
+
|
|
352
|
+
- **`attest status`'s documented surface was wider than the command, in three
|
|
353
|
+
places at once.** All three say some version of "`status` reports what the gate
|
|
354
|
+
still wants, and the one thing it cannot see is whether the tests pass". It is
|
|
355
|
+
a strict subset of that: `statusRows` projects the *uncovered* half of gate
|
|
356
|
+
check 1 and the whole of check 4, both restricted to the ids a change ADDs.
|
|
357
|
+
Check 3 reads `runtimeCoverage`, which only the suite produces — so a scenario
|
|
358
|
+
left `skip`ped blocks `archive` with the suite fully green and `status` fully
|
|
359
|
+
clean, which is exactly the conclusion the sentence invites a reader to rule
|
|
360
|
+
out. Orphans and unbound params are not projected either. Corrected in design
|
|
361
|
+
§8 (both languages), in the workflow document, in that document's command
|
|
362
|
+
table, and in the README's command list — four copies of one gloss, which is
|
|
363
|
+
the count worth recording: the phrase was not repeated because anyone reasoned
|
|
364
|
+
it four times, and correcting three of them would have left the repo saying two
|
|
365
|
+
different things about one command.
|
|
366
|
+
**Design §8's sentence was wrong on the day it was written, not stale, and the
|
|
367
|
+
difference was worth checking rather than assuming.** The tidy explanation was
|
|
368
|
+
that `declared-not-run` had been inserted as check 3 and renumbered the list
|
|
369
|
+
under a sentence nobody re-read — this file's own recurring lesson, and it fits
|
|
370
|
+
so well it was nearly written down. `git log -S` says otherwise: the numbering
|
|
371
|
+
has had check 3 reading `runtimeCoverage` since 2026-07-21, and the sentence
|
|
372
|
+
claiming check 3 is decidable without running anything arrived six days later.
|
|
373
|
+
So the mechanism here is not drift at all. **A sentence that enumerates a list
|
|
374
|
+
written a few lines above it is not checked against that list by anyone,
|
|
375
|
+
including its author**, and no gate reads prose.
|
|
376
|
+
*Separately, `status`'s History table was missing `0.8.0`.* Moving the
|
|
377
|
+
first-run record to `version: 2` — filed against `archive`, where the change
|
|
378
|
+
was made — also moved `status`'s output, because the two read that file with
|
|
379
|
+
the same predicate. Measured: the same change reports `✓ AUTH-7 2 scenarios,
|
|
380
|
+
seen red / 1 ready to archive` on a version-2 record and `● AUTH-7 2 never
|
|
381
|
+
run / 0 ready to archive` on the version-1 record a 0.7.x change carries. The
|
|
382
|
+
row is owed by the rule that a History table is the only thing that can tell a
|
|
383
|
+
reader on an older build which half of the page applies to them, and it is the
|
|
384
|
+
shape that hides: `status` exits 0 either way, so nothing anywhere turned red.
|
|
385
|
+
|
|
386
|
+
- **The workflow document never said a delta must be a literal.** It states the
|
|
387
|
+
rule for `*.reqs.ts` and stops there, while `check` and `status` read
|
|
388
|
+
`requirements.delta.ts` through the same static reader — so an agent lifting a
|
|
389
|
+
`const` out of a delta, which is the ordinary thing to do when two params share
|
|
390
|
+
a value, meets `registry-not-static` pointing at a file the rule it was given
|
|
391
|
+
never mentioned. Confirmed by running it rather than by reading the reader:
|
|
392
|
+
`attest status` on a delta whose param is an identifier reports
|
|
393
|
+
`registry-not-static` at that line. `changes/` is also the more exposed of the
|
|
394
|
+
two files, being by definition the one still under review, so it is the worse
|
|
395
|
+
of the two to leave undocumented.
|
|
396
|
+
*The same roster was short in `CLAUDE.md`*, which named three commands that
|
|
397
|
+
execute nothing where `ATX-16.staticCommands` names four — `status` being the
|
|
398
|
+
one missing, for the same reason: what it reads statically is a delta rather
|
|
399
|
+
than a registry, so it falls outside a sentence scoped to `*.reqs.ts` while
|
|
400
|
+
being squarely inside the security property that sentence exists to teach.
|
|
401
|
+
|
|
402
|
+
- **`check` had no `empty-spec` guard for four releases, and the reference said
|
|
403
|
+
it did.** A root with zero requirements answered `✓ No issues.` and exit 0,
|
|
404
|
+
while `verify` reported it as an ERROR. Reported by an adoption on 0.8.0,
|
|
405
|
+
which reached it the way it will usually be reached: `check` is the cheap CI
|
|
406
|
+
pre-check, so it is the command most likely to be pointed at a directory a
|
|
407
|
+
moved registry or a wrong `dir` argument has emptied — and the expensive gate
|
|
408
|
+
that would have caught it runs minutes later, or on a matrix leg that does not
|
|
409
|
+
run at all. What makes it a defect rather than a proposal is that the contract
|
|
410
|
+
was already written down and the implementation was what disagreed with it:
|
|
411
|
+
`cli-reference.md` lists `empty-spec` among the codes **`check`** reports, and
|
|
412
|
+
its own History row credits `0.2.0` with adding it, so the `0.2.0` change
|
|
413
|
+
landed on one of the two commands the reference describes it on and nothing
|
|
414
|
+
compared them since. `troubleshooting.md` closes the loop by telling a reader
|
|
415
|
+
who meets this to run `attest check .` — advice to a command that could not
|
|
416
|
+
say it.
|
|
417
|
+
**The fix is a layer move, not a branch added to a second command**, and that
|
|
418
|
+
is what settles the ambiguity the entry was filed with. The rule needs nothing
|
|
419
|
+
executed to decide it — the registry alone answers it — so it belongs to
|
|
420
|
+
structural validation, which is where `validateStructure` already lives and
|
|
421
|
+
which both commands already call with the same three arguments. Both design
|
|
422
|
+
documents said this twice and did not agree: the sentence stating the rule sat
|
|
423
|
+
in **§5.4**, under "what a run runs, and what `passed` may not mean", while
|
|
424
|
+
the account of what `init` writes cited **§5.3** for the same code in a
|
|
425
|
+
context where nothing has run at all. §5.3 is now the one place it is stated,
|
|
426
|
+
which makes that citation correct for the first time.
|
|
427
|
+
**Re-measured before a line was written, and the re-measure found a case the
|
|
428
|
+
entry had not counted** — the section's own discipline, and the third time it
|
|
429
|
+
has paid on the *reachability* question rather than on a number. The reported
|
|
430
|
+
half reproduced exactly. What the walk added is that `verify` fires
|
|
431
|
+
`empty-spec` **beside** `registry-invalid` when the registry was found and
|
|
432
|
+
failed to load, and its message tells the reader to point Attest at the
|
|
433
|
+
directory holding the `*.reqs.ts` files — a file that is sitting right there,
|
|
434
|
+
where the fix is the load error already in the report and following the hint
|
|
435
|
+
would move a path that was correct. So the obvious implementation, "make
|
|
436
|
+
`check` agree with `verify`", would have imported a latent misdiagnosis into
|
|
437
|
+
the command adopters run first. `empty-spec` is now withdrawn when a registry
|
|
438
|
+
file failed to load, on both commands: the same withdrawal `validateStructure`
|
|
439
|
+
already makes for `orphan-test`, against the same input, for the same reason.
|
|
440
|
+
A registry that could not be read is not an absent one, and the two take
|
|
441
|
+
opposite repairs.
|
|
442
|
+
**Stated beside the findings it explains, not instead of them.** The louder
|
|
443
|
+
symptom in the report was measured at 135 `orphan-test` ERRORs, one distinct
|
|
444
|
+
code, and the actual diagnosis — there is no registry under this root — not
|
|
445
|
+
among them. Each orphan is true of the root as given, so the alternative is
|
|
446
|
+
rejected explicitly rather than by omission: what was missing is the single
|
|
447
|
+
line saying why they are all true at once. `verify` already had it this way,
|
|
448
|
+
which answered the question by measurement rather than by argument.
|
|
449
|
+
Held as `ATX-18`, widened from `verify` to the layer, and `ATX-77` for the
|
|
450
|
+
withdrawal — kept apart because the two are falsifiable in opposite
|
|
451
|
+
directions, and both were observed doing exactly that before the branch was
|
|
452
|
+
written: reporting nothing satisfies `ATX-77` and fails `ATX-18`'s four
|
|
453
|
+
scenarios, reporting unconditionally satisfies `ATX-18` and fails `ATX-77`'s
|
|
454
|
+
two.
|
|
455
|
+
*A root that reported success now reports an error, which this file's
|
|
456
|
+
Versioning section makes a **minor** bump.*
|
|
457
|
+
|
|
458
|
+
- **`--apply` skipped the delta import it is supposed to repoint whenever the
|
|
459
|
+
specifier carried no file extension, and reported success.** `[0.6.0]` records
|
|
460
|
+
this exact failure being found and fixed — a merged spec left importing a path
|
|
461
|
+
step 3 has just moved to `archive/`, `check` silent, only the merged suite
|
|
462
|
+
showing it — and the fix covered the two spellings *this* repository writes.
|
|
463
|
+
`resolvesTo` (`core/splice.ts`) accepted the delta's own path or the NodeNext
|
|
464
|
+
`.js`-for-`.ts` spelling of it, and nothing else. A project on bundler
|
|
465
|
+
resolution writes neither: every import in it is extensionless, including the
|
|
466
|
+
one this is looking for. Reported by an adoption that is extensionless
|
|
467
|
+
throughout — application modules and all 35 spec files — so it hit this on
|
|
468
|
+
**every** applied change, nine in a row, and after the first one pre-empted it
|
|
469
|
+
by hand. That is the shape worth recording rather than the bug: the workaround
|
|
470
|
+
is cheap enough to become habit, so the defect stops producing reports while
|
|
471
|
+
continuing to happen.
|
|
472
|
+
**Re-measured before a line was written, and the entry's own account of the
|
|
473
|
+
repair was wrong.** The reading half reproduced exactly as filed, against the
|
|
474
|
+
current source rather than the `dist/` the report used. The writing half was
|
|
475
|
+
filed as needing nothing — "extensionless in, extensionless out falls out of
|
|
476
|
+
the empty case" — and the empty case sat behind `if (ext)`, so it left the
|
|
477
|
+
target's `.ts` in place. That guard was correct only while the empty case was
|
|
478
|
+
unreachable, which is precisely what widening the match changes. Fixing the
|
|
479
|
+
reported half alone would have written an extension into a project that writes
|
|
480
|
+
none: the reading half repaired, the writing half newly broken, and the
|
|
481
|
+
uniformity the whole fix exists to respect broken by the fix for it. This is
|
|
482
|
+
the second time an entry that named its own fix had the fix turn out to be the
|
|
483
|
+
wrong half, after the C1 stripping in `[0.8.0]`, and it is the case least
|
|
484
|
+
likely to be re-examined for exactly that reason.
|
|
485
|
+
Held as `ATX-75` with a scenario per spelling rather than by widening
|
|
486
|
+
`ATX-52`, which attests that the merged project verifies clean: a merged
|
|
487
|
+
project whose spec never imported the delta verifies clean either way. The
|
|
488
|
+
assertions are on the specifier the command wrote, not on the health of what
|
|
489
|
+
surrounds it, which is what makes the two separately falsifiable in the
|
|
490
|
+
direction that matters.
|
|
491
|
+
|
|
492
|
+
- **`ATX-72`'s second scenario could not run on Windows at all, and the
|
|
493
|
+
requirement's own rationale said it could.** The injection needs the registry
|
|
494
|
+
file's name to carry the *specifier's own* quote — that is what makes the two
|
|
495
|
+
quote styles two scenarios rather than a loop — and the rationale argued the
|
|
496
|
+
name was "legal on Windows too since neither is among the characters it
|
|
497
|
+
reserves". True of the apostrophe, and false of the double quote, which is in
|
|
498
|
+
the set that same sentence enumerates. So `writeFile` threw before the emitter
|
|
499
|
+
was reached and `verify self` was red on win32, from `[0.8.0]`.
|
|
500
|
+
**What hid it is worth more than the fix.** CI is Linux, where both names are
|
|
501
|
+
legal, so no pipeline could have reported it — `CLAUDE.md`'s two-platform rule
|
|
502
|
+
arriving on a requirement that had already written the platform argument down
|
|
503
|
+
and got it half right. The argument was written while this requirement covered
|
|
504
|
+
one quote; widening it to two left the reasoning for the first standing over
|
|
505
|
+
both, unre-examined. A rationale is not re-read when the thing it justifies
|
|
506
|
+
grows.
|
|
507
|
+
The obligation is unchanged and holds on both platforms, because it is about
|
|
508
|
+
what the emitter writes. What is platform-limited is the end-to-end route to
|
|
509
|
+
it, so the win32 run now reaches the same assertion through `repointImport`
|
|
510
|
+
directly rather than skipping — a skipped scenario is `declared-not-run`
|
|
511
|
+
(`ATX-71`), which would trade a red suite for a red gate. The honest reading,
|
|
512
|
+
recorded in the requirement: a repository holding that name cannot be checked
|
|
513
|
+
out on Windows at all, so the exposure the end-to-end case stands for is
|
|
514
|
+
POSIX-only.
|
|
515
|
+
|
|
516
|
+
### Changed
|
|
517
|
+
|
|
518
|
+
- **`status`'s closing line names `check` as well as `archive`.** It said what
|
|
519
|
+
this report is short of on one side — the verdict — and nothing about the
|
|
520
|
+
other: a proposed spec no delta claims, or one whose merged name is already
|
|
521
|
+
taken, is a static fact about this change that `check` already reports. Naming
|
|
522
|
+
it is deliberately the whole of the fix. Reporting those here too would put a
|
|
523
|
+
second answer to one question in the tree, which is the arrangement this
|
|
524
|
+
project takes apart everywhere else, and the reader who wanted one command was
|
|
525
|
+
going to run `archive` anyway.
|
|
526
|
+
|
|
527
|
+
- **`divergent-param` re-keyed a fourth time, and this one carries something the
|
|
528
|
+
first three did not.** `ATX-77` writes `ATX-18` out of the `code` group by
|
|
529
|
+
carrying the same `empty-spec`, on the rule's usual arithmetic — one id per
|
|
530
|
+
distinct value, later writer wins. The churn itself is now unremarkable and
|
|
531
|
+
that is the point of counting it. What is new is *which* pair it hid: `ATX-18`
|
|
532
|
+
and `ATX-77` were split deliberately, because each is falsifiable in a
|
|
533
|
+
direction the other is blind to, which makes them the most tightly related
|
|
534
|
+
pair in this registry — and the group can show only one of them. That is the
|
|
535
|
+
second time the one real relationship in this group has been the invisible
|
|
536
|
+
one, after `ATX-66`/`ATX-73`, and twice makes the mechanism legible rather
|
|
537
|
+
than the coincidence: two ids are related when they name the **same**
|
|
538
|
+
constant, which is exactly the case the deduplication discards. The rule is
|
|
539
|
+
not blind to relatedness by omission — its key is built by throwing the
|
|
540
|
+
evidence of it away.
|
|
541
|
+
|
|
542
|
+
- **`divergent-param`'s finding for `flag` had to be re-keyed a second time**, on
|
|
543
|
+
a requirement carrying the same `--apply` as the member it displaced. The
|
|
544
|
+
divergence itself did not change — the same three unrelated constants under
|
|
545
|
+
the same param name — but the grouping keeps one id per distinct *value* and
|
|
546
|
+
the later writer wins, so the row's identity moved and a human had to re-judge
|
|
547
|
+
a finding about which nothing was new. Once was a curiosity worth a line; twice
|
|
548
|
+
makes it the candidate rule's behaviour, and names a maintenance cost paid per
|
|
549
|
+
addition on top of the zero precision already recorded. Note which additions
|
|
550
|
+
trigger it: the ones that **agree** with an existing member.
|
|
551
|
+
|
|
552
|
+
- **`findFiles` opened one directory per directory in the tree; it now opens
|
|
553
|
+
32.** The recursion was `Promise.all(subdirs.map(walk))`, so every sibling at a
|
|
554
|
+
level was in flight at once and each of their children after them. The walk is
|
|
555
|
+
now a bounded pool a level at a time, which makes the peak a constant — the
|
|
556
|
+
same figure and the same shape as `parseSpecs`, whose bound `[0.7.0]` added.
|
|
557
|
+
*The entry this closes was half stale, and that is the part worth recording.*
|
|
558
|
+
`[0.7.0]` fixed the `parseSpecs` half and wrote down, in this file, that the
|
|
559
|
+
`findFiles` half was deliberately left; the `Under consideration` entry was
|
|
560
|
+
never narrowed to match, so it went on describing `parseSpecs` as reading every
|
|
561
|
+
source into memory for four releases after that stopped being true. An entry
|
|
562
|
+
whose other half ships is not re-read either — the same shape as a rationale
|
|
563
|
+
not re-read when the thing it justifies grows (`ATX-72`, above), arriving on
|
|
564
|
+
the section whose own discipline is to re-measure before building. Re-measuring
|
|
565
|
+
is what found it: the memory arithmetic the entry rested on was the half
|
|
566
|
+
already fixed.
|
|
567
|
+
*What retires the blocker is that it named the wrong failure.* `[0.7.0]` held
|
|
568
|
+
this back on "no report, and no demonstrated failure anywhere", where the
|
|
569
|
+
failure it meant was descriptor exhaustion — unreachable on either development
|
|
570
|
+
platform, and on Windows not even governed by `ulimit -n`, since Node uses
|
|
571
|
+
Win32 handles there. That is still true and still unmeasured. But it is not the
|
|
572
|
+
only cost: the peak itself is portable arithmetic, which is the standard the
|
|
573
|
+
`parseSpecs` half was accepted on. Measured in `tests/locate-fanout.spec.ts`,
|
|
574
|
+
which now counts in-flight `readdir` as well as in-flight `readFile`, on a
|
|
575
|
+
64-wide tree two levels deep: **64 before, 32 after** — and 200 before on a
|
|
576
|
+
200-wide one, which is the point. Unbounded, the figure is the width of the
|
|
577
|
+
level; the tree is the attacker's to choose, and `check` is the command this
|
|
578
|
+
project tells people to run first on an untrusted fork MR.
|
|
579
|
+
*The 200-wide tree is not what ships, and why is worth a line.* Building it
|
|
580
|
+
timed this file's `beforeAll` out on win32 under the parallel suite,
|
|
581
|
+
intermittently — two runs in three — and the failure arrived in the shape
|
|
582
|
+
`CLAUDE.md` records for `tests/consumer.spec.ts`: a throw in a hook reports
|
|
583
|
+
every test in the file as **skipped**, so the count moves from 385 passed to
|
|
584
|
+
383 passed and 2 skipped, and nothing says the property went unchecked. The
|
|
585
|
+
tree is now 64 wide, built concurrently, with an explicit hook timeout. A test
|
|
586
|
+
that is sometimes not run is worth less than a smaller one that always is.
|
|
587
|
+
*Both fan-outs now go through one `forEachBounded`*, extracted at the second
|
|
588
|
+
call site rather than the first: `parseSpecs` had the pool written inline, and
|
|
589
|
+
a shape written twice is the one a later fix reaches only one copy of — which
|
|
590
|
+
is, in miniature, what this entry is about. `parseSpecs` keeps its own
|
|
591
|
+
constant rather than sharing one, because the two bound different resources
|
|
592
|
+
and a shared figure could not be tuned for either. Its peak is unchanged at 32,
|
|
593
|
+
re-measured through the helper rather than assumed.
|
|
594
|
+
*A level at a time rather than one pool over a growing queue*, because a queue
|
|
595
|
+
fed by its own workers has to keep them alive while it is momentarily empty and
|
|
596
|
+
another worker may still push — a termination condition worth not owning for a
|
|
597
|
+
barrier paid in tree *depth*, which the filesystem bounds, against a fan-out
|
|
598
|
+
paid in tree *width*, which nothing does.
|
|
599
|
+
No behaviour change and no `ATX-n`, on the precedent `[0.7.0]` set for the
|
|
600
|
+
other half: the returned list is byte-identical, sorted the same way, with
|
|
601
|
+
`SKIP_DIRS` excluded the same way — all three asserted beside the peak — so
|
|
602
|
+
there is no contract here a consumer could branch on.
|
|
603
|
+
|
|
16
604
|
## [0.8.0] - 2026-08-09
|
|
17
605
|
|
|
18
606
|
### Security
|
|
@@ -4304,8 +4892,8 @@ symptom are worth batching**: individually none justifies a commit, together the
|
|
|
4304
4892
|
cost about an hour, and they are the group no gate could have found, since having
|
|
4305
4893
|
no symptom is the definition of what a gate cannot see.
|
|
4306
4894
|
|
|
4307
|
-
*The five entries this section carried before the
|
|
4308
|
-
recorded under `[
|
|
4895
|
+
*The five entries this section carried before the ones below* were built and are
|
|
4896
|
+
recorded under `[0.8.0]`. The second is where this preamble's own
|
|
4309
4897
|
discipline paid again: it went in on an entry whose headline type
|
|
4310
4898
|
measurement — instantiations unchanged — was false on re-measure, and the
|
|
4311
4899
|
re-measure is the only reason the release notes do not repeat it. The fourth is
|
|
@@ -4323,6 +4911,30 @@ four more fields and a nested scenario name carry the same untrusted text and
|
|
|
4323
4911
|
never pass the envelope. An entry that ends with a fix is the one least likely
|
|
4324
4912
|
to have its fix re-examined, which is the case worth naming.
|
|
4325
4913
|
|
|
4914
|
+
*That has now happened twice, so it is a pattern rather than an incident.* The
|
|
4915
|
+
extensionless-import entry likewise reproduced exactly on its reported half and
|
|
4916
|
+
was wrong about its own repair — it recorded that the writing half needed
|
|
4917
|
+
nothing, and the writing half was guarded by a condition that was correct only
|
|
4918
|
+
while the reading half stayed broken. Both times the error had the same shape: an
|
|
4919
|
+
entry reasons about the code path it can see failing, and the repair makes a
|
|
4920
|
+
second path reachable that nobody has looked at because nothing could reach it.
|
|
4921
|
+
So the re-measure that matters is not "does the reported symptom reproduce" —
|
|
4922
|
+
both did, exactly — but **what does the proposed fix make reachable for the first
|
|
4923
|
+
time**.
|
|
4924
|
+
|
|
4925
|
+
*The first time that question was asked in advance, it paid, and how it paid is
|
|
4926
|
+
worth a line because it is not what the two cases above would predict.* The
|
|
4927
|
+
`check` `empty-spec` entry — `[Unreleased]`, the third entry to reproduce exactly
|
|
4928
|
+
on its reported half — was not wrong about its own repair. Asking the question
|
|
4929
|
+
anyway found the defect one step to the side: the fix it proposed was correct,
|
|
4930
|
+
and the *obvious implementation of it* — make `check` agree with `verify` —
|
|
4931
|
+
would have copied a misdiagnosis `verify` had been emitting unnoticed, because
|
|
4932
|
+
the reference implementation an entry points at is not itself re-read. So the
|
|
4933
|
+
question generalises past the shape it was learned from. It is not only about a
|
|
4934
|
+
path the repair makes reachable; it is about **everything the repair would make
|
|
4935
|
+
`check` agree with**, and an entry that names the right fix can still be
|
|
4936
|
+
implemented wrongly by copying the half nobody has audited.
|
|
4937
|
+
|
|
4326
4938
|
- **Trusted publishing, which would delete `NPM_TOKEN` rather than add to it.**
|
|
4327
4939
|
The provenance work in `[0.7.2]` signs the release but does not change
|
|
4328
4940
|
what authorises it: a long-lived automation token, masked and protected, that
|
|
@@ -4412,32 +5024,6 @@ deletion. What each entry has to carry is the evidence currently blocking the
|
|
|
4412
5024
|
decision, because that is what a re-proposal a year from now would otherwise
|
|
4413
5025
|
have to gather again.
|
|
4414
5026
|
|
|
4415
|
-
- **Bounding the fan-out in `findFiles` and `parseSpecs`.** Both walk the
|
|
4416
|
-
project with no concurrency limit: `findFiles` recurses through
|
|
4417
|
-
`Promise.all(subdirs.map(walk))`, so every directory in the tree is opened at
|
|
4418
|
-
once, and `parseSpecs` does `Promise.all(files.map(readFile))`, which reads
|
|
4419
|
-
every spec file into memory before parsing any of them. Neither is bounded by
|
|
4420
|
-
anything, and `check` is the command this repository tells people to run first
|
|
4421
|
-
in a pipeline on an untrusted fork MR — so the input size is not ours to
|
|
4422
|
-
assume.
|
|
4423
|
-
*The evidence, and why it is not enough to act on.* Measured on a synthetic
|
|
4424
|
-
tree of 6000 spec files: the fan-out is real and unbounded — peak in-flight
|
|
4425
|
-
`readFile` calls came back as exactly 6000 — and `attest check` finished in
|
|
4426
|
-
1.99 s without erroring. It could not be made to fail. Windows is why: Node
|
|
4427
|
-
uses Win32 handles there rather than POSIX descriptors, so the `ulimit -n` of
|
|
4428
|
-
3200 that the shell reports does not govern the process, and the one platform
|
|
4429
|
-
where descriptor exhaustion is plausible is the one CI runs and this
|
|
4430
|
-
measurement could not reach (`CLAUDE.md`'s two-platform rule, pointing the
|
|
4431
|
-
unusual way round). What *is* portable is the memory: `parseSpecs` holds every
|
|
4432
|
-
source at once, which at a realistic 8 KiB per spec file is ~47 MiB at this
|
|
4433
|
-
count — noticeable, not fatal.
|
|
4434
|
-
So the fix is known and cheap — a concurrency limit of ~32 at both sites, and
|
|
4435
|
-
parsing each source as it arrives rather than after all of them, which makes
|
|
4436
|
-
the peak constant — and it is held back by this file's own standard: no report,
|
|
4437
|
-
and no demonstrated failure on any platform. Recorded here so that the day
|
|
4438
|
-
someone hits an `EMFILE` from `attest check`, the diagnosis is already written
|
|
4439
|
-
down rather than rediscovered.
|
|
4440
|
-
|
|
4441
5027
|
- **An assertion that recomputes its expectation from the param the code under
|
|
4442
5028
|
test just read is a tautology, and nothing says so.** Reported by
|
|
4443
5029
|
`mine-capablanca`, which probed it by mutating the registry and running the
|
|
@@ -4660,29 +5246,137 @@ have to gather again.
|
|
|
4660
5246
|
falling, which is the premise the whole entry rests on, and it is the first
|
|
4661
5247
|
time this file has had two measurements of it to compare.
|
|
4662
5248
|
|
|
4663
|
-
- **
|
|
4664
|
-
|
|
4665
|
-
|
|
4666
|
-
|
|
4667
|
-
|
|
4668
|
-
|
|
4669
|
-
|
|
4670
|
-
|
|
4671
|
-
|
|
4672
|
-
|
|
4673
|
-
|
|
4674
|
-
|
|
4675
|
-
|
|
4676
|
-
|
|
4677
|
-
|
|
4678
|
-
|
|
4679
|
-
|
|
4680
|
-
|
|
4681
|
-
|
|
4682
|
-
|
|
4683
|
-
|
|
4684
|
-
|
|
4685
|
-
|
|
5249
|
+
- **Running `verify` over a subset of the scenarios, and what a partial run may
|
|
5250
|
+
be allowed to say.** `verify` is all-or-nothing and `check` executes nothing,
|
|
5251
|
+
so there is no middle. Reported by an adoption where `verify` takes **7m33s**
|
|
5252
|
+
wall clock — three requirements are strength gates that play twelve long games
|
|
5253
|
+
each — with the consequence stated as a change in *when* the tool gets used:
|
|
5254
|
+
`verify` became a run-at-the-end command, and `archive` became the everyday
|
|
5255
|
+
one, paying the full suite on every invocation because its verdict was the one
|
|
5256
|
+
needed. The ask is a selector — `--only`/`--skip` over ids or tags, or simply
|
|
5257
|
+
honouring a Vitest name filter — with the partial scope reported loudly enough
|
|
5258
|
+
that a subset pass cannot be read as a pass.
|
|
5259
|
+
*The cost is not Attest's, and that is the first thing the decision turns on.*
|
|
5260
|
+
Every expensive scenario in that report is behavioural: the engine's own
|
|
5261
|
+
overhead is not what makes 7m33s. So this is a selector over someone else's
|
|
5262
|
+
suite, and Vitest already has one — the question is whether Attest reporting
|
|
5263
|
+
*over* a filtered run is worth owning, not whether the run can be filtered.
|
|
5264
|
+
*What blocks it — three, and they are about the report rather than the run.*
|
|
5265
|
+
**(1)** Coverage is computed against the whole registry: under a filter every
|
|
5266
|
+
unselected requirement has no scenario that ran, which is `uncovered-requirement`
|
|
5267
|
+
and `declared-not-run` firing on requirements that are fine. Either those codes
|
|
5268
|
+
gain a meaning that depends on which flags the run carried — and a `code` is
|
|
5269
|
+
the one thing in a report consumers are told to branch on — or the report has to carry its scope
|
|
5270
|
+
explicitly and the codes have to be computed within it. **(2)** `archive`'s
|
|
5271
|
+
gate cannot accept a partial run at all: "done" having a hard definition is the
|
|
5272
|
+
claim the tool is for. So the flag has to be refused there, which means two
|
|
5273
|
+
shapes of `verify` and a rule for which commands may carry it. **(3)** `--json`
|
|
5274
|
+
is where a partial run does its damage — a consumer branching on `ok` must not
|
|
5275
|
+
read a subset pass as a pass, so this needs a field naming the scope and a
|
|
5276
|
+
decision about `SCHEMA_VERSION`, and that is the cheapest part to price first
|
|
5277
|
+
because it decides whether the feature is additive at all.
|
|
5278
|
+
*What would move it.* A second report of the same shape, ideally one where the
|
|
5279
|
+
slow half is smaller, so the question is a selector's ergonomics rather than
|
|
5280
|
+
one project's gates; and a scored answer to whether a scope-carrying report can
|
|
5281
|
+
be stated in a way that a `--json` consumer written before it cannot misread.
|
|
5282
|
+
**Measured on 2026-08-12, before building anything, per this section's
|
|
5283
|
+
discipline. The second of those two is now answered, and the answer is a
|
|
5284
|
+
refusal; the first blocker was wrong; and the whole feature turns out to be a
|
|
5285
|
+
different, much cheaper one.**
|
|
5286
|
+
*The cost claim reproduces on a second corpus.* `verify self`: the static half
|
|
5287
|
+
— scan, registry load, `parseSpecs` — is **79 ms of 30,539 ms, 0.26%**. So a
|
|
5288
|
+
selector cannot save engine work anywhere; it can only save scenarios, and the
|
|
5289
|
+
entry was right that this is a selector over someone else's suite.
|
|
5290
|
+
*Blocker **(1)** conflates two codes with different inputs, and no arrangement
|
|
5291
|
+
fires both.* `uncovered-requirement` reads registry × plan and is purely
|
|
5292
|
+
static; `declared-not-run` reads plan × runtime coverage. Measured over 77
|
|
5293
|
+
requirements and 214 scenarios, selecting one requirement: filtering the **run
|
|
5294
|
+
scope** while leaving the plan whole gives 209–213 spurious `declared-not-run`
|
|
5295
|
+
and **zero** `uncovered-requirement`; filtering the **plan** gives 76
|
|
5296
|
+
`uncovered-requirement` and **zero** `declared-not-run`. The choice of where
|
|
5297
|
+
the filter goes decides which one misfires, and the entry priced a case that
|
|
5298
|
+
cannot occur.
|
|
5299
|
+
*And on the run-scope arrangement it does not misfire at all, which is the
|
|
5300
|
+
finding that collapses the rest.* A run that did not execute a declared
|
|
5301
|
+
scenario really has not attested it, so `declared-not-run` is **true** under a
|
|
5302
|
+
filter — only its message guesses the wrong cause ("skipped, or excluded by an
|
|
5303
|
+
`.only`?"). The premise under "the codes have to be computed within [the
|
|
5304
|
+
scope]" is therefore false: the report is already honest, and a subset run is
|
|
5305
|
+
already loudly not a pass. 209 ERRORs on a one-requirement filter is the
|
|
5306
|
+
loudness the entry asked to build.
|
|
5307
|
+
*Blocker **(3)**, scored, comes back **impossible rather than expensive**.* The
|
|
5308
|
+
contract `cli-reference.md` publishes to consumers is that "a consumer can
|
|
5309
|
+
always parse stdout and branch on `ok`"; the next paragraph describes
|
|
5310
|
+
`schemaVersion` as a producer-side promise — what the emitter bumps — and
|
|
5311
|
+
checked across the whole tree rather than that page, every other mention of it
|
|
5312
|
+
is producer-side too, `ATX-5` included, which obliges the report to *declare*
|
|
5313
|
+
it. Nothing tells a consumer to gate on it before trusting `ok`. A consumer
|
|
5314
|
+
written before a `scope` field does not read new fields — that is what makes it
|
|
5315
|
+
a consumer written before them — so no
|
|
5316
|
+
additive shape can protect it, and a `SCHEMA_VERSION` bump is a signal to a
|
|
5317
|
+
reader who is not looking. Exactly one design is safe: a subset run never
|
|
5318
|
+
reports `ok: true`. That is what already happens, for free, by the paragraph
|
|
5319
|
+
above. So the correct answer to "does this need a scope field and a schema
|
|
5320
|
+
decision" is **neither**.
|
|
5321
|
+
*Blocker **(2)** dissolves into that.* `archive` refusing a partial run stops
|
|
5322
|
+
being a rule to design and becomes a consequence: a partial run cannot be
|
|
5323
|
+
`ok`, and the gate already refuses what is not.
|
|
5324
|
+
*What the measurement also shows is that the feature as asked would not have
|
|
5325
|
+
helped the reporter.* Their report says `archive` became the everyday command
|
|
5326
|
+
**because its verdict was the one needed**. A subset run cannot carry a
|
|
5327
|
+
verdict — that is the whole of blocker (3) — so a selector would have given
|
|
5328
|
+
them a fast non-verdict, which they already have by running Vitest directly.
|
|
5329
|
+
*The argument that survives is one the entry does not make, and it is the
|
|
5330
|
+
strongest available.* The child run is `config: false` — no aliases, no DOM, no
|
|
5331
|
+
plugins — so `npx vitest run one.spec.ts` executes in a **different
|
|
5332
|
+
environment** from the one the verdict comes from, and a green there does not
|
|
5333
|
+
predict `verify`. Owning a selector is the only way to run a subset in Attest's
|
|
5334
|
+
environment. That moves the justification off speed, where Vitest is the
|
|
5335
|
+
incumbent and wins, and onto parity between the fast loop and the verdict,
|
|
5336
|
+
where nothing else can serve.
|
|
5337
|
+
*So the thing to build, if a second report arrives, is not what this entry
|
|
5338
|
+
proposed.* Not a scoped verdict but a **development-loop command**: runs a
|
|
5339
|
+
subset in Attest's environment, reports what did not run as `declared-not-run`
|
|
5340
|
+
because that is true, and exits non-zero always. It claims no pass, so it never
|
|
5341
|
+
touches the impossibility above; it needs no field, no `SCHEMA_VERSION`
|
|
5342
|
+
decision, and no code whose meaning depends on a flag.
|
|
5343
|
+
*A second argument arrived on 2026-08-12, from the author's reason for building
|
|
5344
|
+
`status` rather than from an adoption, and it says a hole exists at all — which
|
|
5345
|
+
is the half the parity argument above does not supply.* `status` was added so
|
|
5346
|
+
that when collaborating with an agent there would be a fast way to know where a
|
|
5347
|
+
change stands without paying `verify` every turn. It delivers that for **stage
|
|
5348
|
+
1 only**, and structurally rather than by omission: its two obligations — has
|
|
5349
|
+
each ADDed id a scenario, has each been recorded failing — are both settled by
|
|
5350
|
+
the end of stage 1 and never move again, so it reads all-`proven` for the whole
|
|
5351
|
+
of stage 2. Stage 2 is the long half, and the workflow document tells the agent
|
|
5352
|
+
to run `attest archive` there every iteration, correctly, because
|
|
5353
|
+
`*.proposed.spec.ts` is deliberately outside `verify`'s scope: the gate is the
|
|
5354
|
+
only Attest command that runs a change's own scenarios at all. So the everyday
|
|
5355
|
+
full-gate run in that adoption report was not a habit — it is what this
|
|
5356
|
+
project's own shipped instructions require.
|
|
5357
|
+
**The cost curve is per-turn, not per-CI-run**, which is what makes this
|
|
5358
|
+
different from the report the entry was filed on. A saving that looks small
|
|
5359
|
+
against one pipeline run is multiplied by every iteration of a conversation, and
|
|
5360
|
+
the agent is the consumer this framework writes a document *for*.
|
|
5361
|
+
*Labelled for what it is:* not the second adoption report this entry asks for,
|
|
5362
|
+
but a structural argument from this repository's own workflow, which is a
|
|
5363
|
+
different kind of evidence — no user was surveyed, and nothing here measures how
|
|
5364
|
+
much stage 2 actually costs anyone. What it does is narrow the thing to build
|
|
5365
|
+
one more notch. The development-loop command above is a **stage-2** tool, and
|
|
5366
|
+
its user is mostly an agent.
|
|
5367
|
+
*The cheap interim was written into the workflow document rather than left for
|
|
5368
|
+
this entry to deliver*, since it needs no engine change: run the proposed spec
|
|
5369
|
+
file directly for the inner loop, keep the gate for the verdict. Measured
|
|
5370
|
+
first — a `*.proposed.spec.ts` runs under a plain Vitest invocation, delta
|
|
5371
|
+
import and all — and it is safe in the one way that matters, because only the
|
|
5372
|
+
gate writes `first-run.json`. It is *not* a substitute, for the parity reason
|
|
5373
|
+
above, and that is why the entry stays open: the interim buys speed by leaving
|
|
5374
|
+
Attest's environment, which is precisely what a real selector would not do.
|
|
5375
|
+
*Still unmeasured, and both need the reporter's repo rather than this one:*
|
|
5376
|
+
whether their fast loop and their verdict actually diverge under `config:
|
|
5377
|
+
false`, which is what decides whether the surviving argument is real for them;
|
|
5378
|
+
and the second report of the same shape, which this measurement does not
|
|
5379
|
+
supply and does not replace.
|
|
4686
5380
|
|
|
4687
5381
|
## Considered and rejected
|
|
4688
5382
|
|
|
@@ -4694,6 +5388,142 @@ it sat between 0.2.0 and 0.1.7 for two releases, where standing still meant
|
|
|
4694
5388
|
sinking one version deeper each time a release was cut above it, and a rejection
|
|
4695
5389
|
filed under a version reads as belonging to it.
|
|
4696
5390
|
|
|
5391
|
+
### Deferring the `typescript` import so `init` and `--version` do not pay it
|
|
5392
|
+
|
|
5393
|
+
Priced on 2026-08-13, in the session that enabled the compile cache above, and
|
|
5394
|
+
rejected on the size of what it actually reaches. The proposal was the move
|
|
5395
|
+
`pipeline.ts` already makes for the runner half — `vite`/`vitest` behind an
|
|
5396
|
+
`await import(...)`, on the argument that a short-lived process pays cold start
|
|
5397
|
+
as the whole bill — applied a second time to the AST half.
|
|
5398
|
+
|
|
5399
|
+
The two are not the same shape, and the measurement is why. The runner lazy-load
|
|
5400
|
+
took a cost off five of seven commands, because `check`, `cover`, `render`,
|
|
5401
|
+
`status` and `init` genuinely cannot start a run. The AST half has one
|
|
5402
|
+
beneficiary: **every command except `init` reads a registry or a spec through the
|
|
5403
|
+
compiler**, so for `check`, `cover`, `render`, `status`, `verify` and `archive`
|
|
5404
|
+
deferring the import moves when the 672 ms is paid and not whether. What is left
|
|
5405
|
+
is `init`, which is run once per project, and `--version` / `--help`. Against the
|
|
5406
|
+
compile cache — which took 11–28% off *all* of them for three lines — the
|
|
5407
|
+
remaining prize is roughly a further 260 ms on `init` alone.
|
|
5408
|
+
|
|
5409
|
+
The structural price is not small either, and it is concentrated in the wrong
|
|
5410
|
+
place. Everything funnels into the compiler through `pipeline.ts`'s static graph,
|
|
5411
|
+
including two edges that are easy to miss: `status` reaches it via
|
|
5412
|
+
`red-record.ts` → `merge.ts` → `splice.ts`, for the single path function
|
|
5413
|
+
`mergedSpecPath`. So deferring it is not one thunk beside the existing two; it is
|
|
5414
|
+
`runInit` and its destination resolution leaving `pipeline.ts`, and
|
|
5415
|
+
`resolveOutFile`'s stated property — private to that module "rather than
|
|
5416
|
+
something a shell is trusted to call" — weakening to pay for it. A once-per-project
|
|
5417
|
+
command is not worth that.
|
|
5418
|
+
|
|
5419
|
+
Worth reopening if either half of the arithmetic moves: a `typescript` whose AST
|
|
5420
|
+
surface can be imported without the checker (the `typescript/unstable/ast*` entry
|
|
5421
|
+
points `core/compiler.ts` already names, once they stop saying unstable), or a
|
|
5422
|
+
second command that needs none of it. `mergedSpecPath` sitting in the heavy
|
|
5423
|
+
module is independently worth fixing whether or not this is.
|
|
5424
|
+
|
|
5425
|
+
### Widening `status` to the rest of the gate's static half
|
|
5426
|
+
|
|
5427
|
+
Scoped and decided on 2026-08-12, in the session that gave `status` its two
|
|
5428
|
+
refusals (an input it could not read, and a delta that will not apply). The
|
|
5429
|
+
question behind both was whether `status` should report *everything* about a
|
|
5430
|
+
change that is decidable without running anything — the gate has a static half,
|
|
5431
|
+
and `status` was projecting two of its checks. Four candidates were priced. Two
|
|
5432
|
+
shipped; these two did not, for opposite reasons, and the reasons are worth
|
|
5433
|
+
keeping because the shape of the proposal will recur.
|
|
5434
|
+
|
|
5435
|
+
*`validateStructure`'s ERRORs over the merged plan — already decided, and not
|
|
5436
|
+
here.* The proposal was to project the gate's first check whole: `orphan-test`,
|
|
5437
|
+
duplicate ids, and `uncovered-requirement` for every id rather than only the
|
|
5438
|
+
ADDed ones. Design §8 had settled it before the proposal was written — `status`
|
|
5439
|
+
projects "the *uncovered* half of check 1 and the whole of check 4, both
|
|
5440
|
+
restricted to the ids the change ADDs", and orphans and unbound params are named
|
|
5441
|
+
as **not** projected, "being facts about the whole applied registry rather than
|
|
5442
|
+
progress on an added id". So this was not a new decision to make but an old one
|
|
5443
|
+
the proposal had not read, and the finding is procedural rather than technical:
|
|
5444
|
+
the design document answers scoping questions about `status` that a reading of
|
|
5445
|
+
`status.ts` alone will not. Rebuilding it means changing §8 first and arguing
|
|
5446
|
+
against that sentence, which nothing here does.
|
|
5447
|
+
|
|
5448
|
+
*The proposed-spec findings — the only genuinely new shape, rejected on
|
|
5449
|
+
duplication.* `proposed-spec-unclaimed`, `proposed-spec-name-taken` and
|
|
5450
|
+
`spec-in-change-dir` are static facts about *this change*, and unlike the
|
|
5451
|
+
refusals that shipped they leave the report perfectly computable — every row
|
|
5452
|
+
stays correct. So they are neither progress nor a failure to compute, which is
|
|
5453
|
+
the one case the existing `issues` contract has no room for, and pricing that
|
|
5454
|
+
room was expected to be the expensive part. It never had to be priced: **`check`
|
|
5455
|
+
already reports all three**, and already reads each change's delta to attribute
|
|
5456
|
+
proposed specs, so `status` reporting them would put a second answer to one
|
|
5457
|
+
question in the tree — the arrangement this project takes apart everywhere else,
|
|
5458
|
+
and the reason `declaredNotRunIssues` and `hasError` are each one function. What
|
|
5459
|
+
was built instead is one line: `status`'s closing line now names `check`
|
|
5460
|
+
alongside `archive`. The cost is real and is not hidden — an agent in stage 2
|
|
5461
|
+
runs two commands where it wanted one — and naming it is the whole of the
|
|
5462
|
+
compensation.
|
|
5463
|
+
|
|
5464
|
+
*The consequence worth recording is that the `--json` question dissolved.* The
|
|
5465
|
+
open design decision that had been sequenced ahead of all four candidates was
|
|
5466
|
+
what shape new `status` findings take in the report: whether `issues` widens
|
|
5467
|
+
from "the report could not be computed" to "here are findings", or a second
|
|
5468
|
+
field arrives, and whether either moves `SCHEMA_VERSION`. After these two
|
|
5469
|
+
rejections there is nothing left to decide, because everything that survives is
|
|
5470
|
+
either a refusal — existing `issues` semantics, `ok: false`, no new field — or
|
|
5471
|
+
`check`'s to report. A blocking decision that evaporates once the candidates
|
|
5472
|
+
either side of it are settled is a sequencing error, not a hard question, and
|
|
5473
|
+
that is the transferable part: the shape of a report is decided by what is left
|
|
5474
|
+
to put in it, so the scoping goes first.
|
|
5475
|
+
|
|
5476
|
+
*What would reopen either.* For the first, an argument against design §8's
|
|
5477
|
+
sentence, not a fresh proposal that has not met it. For the second, a measured
|
|
5478
|
+
cost of the two-command loop — the same evidence the `Under consideration` entry
|
|
5479
|
+
on a stage-2 development-loop command is waiting for, and it would arrive from
|
|
5480
|
+
the same place.
|
|
5481
|
+
|
|
5482
|
+
### A trailing-dot-and-space clause in `isSafeChangeName`, for a Windows normalisation that does not happen
|
|
5483
|
+
|
|
5484
|
+
Filed under `Under consideration` on 2026-08-09 and rejected on 2026-08-11 by
|
|
5485
|
+
the measurement it was waiting for. The premise was that Win32 strips trailing
|
|
5486
|
+
dots and spaces from a path component below the API, so `'.. '` — which passes
|
|
5487
|
+
every clause the guard has, being neither `''`, `'.'`, `'..'`, a separator nor
|
|
5488
|
+
NUL — would name `changes/`'s parent, and under `--eval` or `archive` that name
|
|
5489
|
+
reaches a module path that is then executed. The entry recorded that no Linux
|
|
5490
|
+
measurement could answer it and named both outcomes in advance: a fourth clause
|
|
5491
|
+
if it reproduced, or a note about the clauses' character if it did not.
|
|
5492
|
+
|
|
5493
|
+
*It does not reproduce.* Measured on Windows 11 26200, Node v22.23.2, NTFS.
|
|
5494
|
+
`mkdir` of `changes/.. ` creates a **literal directory named `.. `** —
|
|
5495
|
+
`readdir` returns `[".. "]` — and a write through the name lands inside it,
|
|
5496
|
+
leaving a file planted at the project root untouched. End to end through the
|
|
5497
|
+
CLI with a payload delta at the root, all of `status '.. '`, `status '.. '
|
|
5498
|
+
--eval` and `archive '.. '` fail with `change-not-found` naming
|
|
5499
|
+
`changes/.. /requirements.delta.ts`, and the payload never executes. Both
|
|
5500
|
+
readers were exercised deliberately, because they resolve differently and only
|
|
5501
|
+
one of them is the dangerous path: the static read and the Vite loader keep the
|
|
5502
|
+
trailing space alike. `'.. '`, `'.. .'`, `'...'` and `'. '` behave the same.
|
|
5503
|
+
|
|
5504
|
+
*The neighbouring win32-specific vectors were measured in the same pass*, since
|
|
5505
|
+
the guard's clauses are POSIX-shaped and this was the moment to ask what else
|
|
5506
|
+
they cannot see. `'C:'`, `'C:x'` and `'a:b'` pass the guard, and `join` splices
|
|
5507
|
+
no drive: every one stays under `changes/` and fails `ENOENT` at the colon.
|
|
5508
|
+
`'CON'` and `'NUL'` create ordinary directories. `'..x'` and `'x..'` are
|
|
5509
|
+
literal. Nothing escaped, and the root file was intact after all of them.
|
|
5510
|
+
|
|
5511
|
+
**What the entry's own fallback asks to keep, now earned rather than assumed:
|
|
5512
|
+
the clauses are the POSIX spelling of path safety, and on this platform that is
|
|
5513
|
+
the complete spelling.** The guard's doc comment says the test is path safety
|
|
5514
|
+
rather than a character whitelist — that claim survives the measurement, and it
|
|
5515
|
+
survives it on the platform most likely to have broken it.
|
|
5516
|
+
|
|
5517
|
+
*Rejected at the strength the evidence carries.* This is one Windows build and
|
|
5518
|
+
one Node version, so what is refuted is the general premise, not every future
|
|
5519
|
+
libuv. **The reach was always one level** — the name eaten gives the project
|
|
5520
|
+
root, and going above it needs a second `..`, which needs a separator the guard
|
|
5521
|
+
already refuses — so even a reproduction would have meant executing a file
|
|
5522
|
+
elsewhere *inside* the project. That bound is why a negative result closes this
|
|
5523
|
+
rather than merely deferring it: the cost of being wrong is bounded and the
|
|
5524
|
+
reopen condition is cheap, being a single `mkdir` on a machine where it behaves
|
|
5525
|
+
differently.
|
|
5526
|
+
|
|
4697
5527
|
### Refusing a half-loaded registry in `check` and `verify`, the way `cover` and `render` do
|
|
4698
5528
|
|
|
4699
5529
|
`cover` and `render` return early on `hasError(loadIssues)`,
|
|
@@ -4855,8 +5685,10 @@ Vitest run that currently shares nothing with that work.
|
|
|
4855
5685
|
is structural.* There is **no
|
|
4856
5686
|
measurement** saying parsing costs anything here — this reader is
|
|
4857
5687
|
`createSourceFile` per file with no type checking, which is the cheap half of
|
|
4858
|
-
what arktype pays for, and the fan-out
|
|
4859
|
-
|
|
5688
|
+
what arktype pays for, and the fan-out measurement — filed as an entry in
|
|
5689
|
+
`Under consideration`, and now shipped as the two concurrency bounds in
|
|
5690
|
+
`[0.7.0]` and `[Unreleased]` — clocked `attest check` over 6000 spec files at
|
|
5691
|
+
1.99 s total. Worse, a cache is a correctness hazard
|
|
4860
5692
|
of precisely the kind this tool exists to detect: a stale analysis makes a
|
|
4861
5693
|
**drift detector** report drift that has already been fixed, or miss drift
|
|
4862
5694
|
that has just appeared, and it would do so silently. Any version of this needs
|
|
@@ -5265,6 +6097,7 @@ requirement, and human review at propose is still the whole answer. What is
|
|
|
5265
6097
|
rejected is grouping as a way to assist it, and this reopens only on a
|
|
5266
6098
|
contradiction that grouping would have caught.
|
|
5267
6099
|
|
|
6100
|
+
[0.9.0]: https://gitlab.com/Pseudorca/attest/-/tags/v0.9.0
|
|
5268
6101
|
[0.8.0]: https://gitlab.com/Pseudorca/attest/-/tags/v0.8.0
|
|
5269
6102
|
[0.7.4]: https://gitlab.com/Pseudorca/attest/-/tags/v0.7.4
|
|
5270
6103
|
[0.7.3]: https://gitlab.com/Pseudorca/attest/-/tags/v0.7.3
|