@am_shork/attest 0.7.2 → 0.7.4
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 +608 -166
- package/README.md +10 -1
- package/bin/attest.js +0 -0
- package/dist/cli/json.d.ts +3 -2
- package/dist/cli/json.js +3 -2
- package/dist/core/compiler.d.ts +32 -0
- package/dist/core/compiler.js +78 -0
- package/dist/core/docs.d.ts +1 -1
- package/dist/core/docs.js +2 -0
- package/dist/core/loader.js +63 -33
- package/dist/core/locate.d.ts +22 -2
- package/dist/core/locate.js +29 -4
- package/dist/core/parser.d.ts +12 -0
- package/dist/core/parser.js +18 -2
- package/dist/core/paths.d.ts +16 -0
- package/dist/core/paths.js +20 -1
- package/dist/core/pipeline.js +72 -9
- package/dist/core/render.js +190 -21
- package/dist/core/skill.js +19 -0
- package/dist/core/static-registry.d.ts +34 -2
- package/dist/core/static-registry.js +133 -8
- package/dist/core/validator.d.ts +9 -4
- package/dist/core/validator.js +74 -21
- package/package.json +17 -29
package/CHANGELOG.md
CHANGED
|
@@ -13,6 +13,514 @@ 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.7.4] - 2026-08-08
|
|
17
|
+
|
|
18
|
+
### Added
|
|
19
|
+
|
|
20
|
+
- **`ATX-37`'s three unattested sites are closed, and the sweep that was going
|
|
21
|
+
to close them is rejected instead.** The requirement says *everything* the CLI
|
|
22
|
+
writes to a terminal replaces the control characters in text it quotes from
|
|
23
|
+
the project under test, and the `QUANTIFIED` table had carried four `null`
|
|
24
|
+
rows against it since `[0.7.0]`: the status rows, the coverage table, the
|
|
25
|
+
archive verdict, and the path `--out` echoes. Three of those are formatters
|
|
26
|
+
interpolating a reqId, a change name or a written path — correct by reading,
|
|
27
|
+
attested by nothing, which is the state a `sites` quantifier is the only kind
|
|
28
|
+
able to reach in silence. Three scenarios now drive them with hostile input,
|
|
29
|
+
covering six interpolations, and each was **proven able to go red by injection
|
|
30
|
+
before being believed**: `inline` removed from the status row's id, from the
|
|
31
|
+
merged path, from the archive verdict's change name, and from the coverage
|
|
32
|
+
row's id — four defects, each failing exactly one scenario and no other. The
|
|
33
|
+
fourth `null` stays: `--out` needs a filename holding a control character,
|
|
34
|
+
which NTFS refuses and Linux permits, and an unmeasured half is what a `null`
|
|
35
|
+
records.
|
|
36
|
+
**A fifth injection is why the archive verdict is asserted three times.**
|
|
37
|
+
Reading the diff caught what the site list does not resolve to: `change` is
|
|
38
|
+
interpolated by three separate expressions in that one function — merged,
|
|
39
|
+
archivable, and gate-failed — and the first draft covered two of them, leaving
|
|
40
|
+
`inline` deletable from the third with the whole suite green. A `sites` row
|
|
41
|
+
names sites at the granularity a sentence uses, and a branch is finer than
|
|
42
|
+
that; the injection is what tells the two apart.
|
|
43
|
+
**Re-measuring the proposal before building it is what split it in two**, and
|
|
44
|
+
the figures moved in both directions. `src/cli/report.ts` has 8 exports of
|
|
45
|
+
which **7** are `format*` — the eighth is `summarize`, so the entry's "eight
|
|
46
|
+
formatters" was already one off its own wording. The "eight fixtures, one per
|
|
47
|
+
signature" was worse: there are 7 distinct parameter types, `Issue[]` is
|
|
48
|
+
shared by three functions, and the honest cost of the fix turned out to be two
|
|
49
|
+
fixtures extending ones `self/report.spec.ts` already builds. And the entry
|
|
50
|
+
**undercounted its own benefit** — "three currently-correct sites" is three
|
|
51
|
+
*formatters* carrying six interpolations.
|
|
52
|
+
|
|
53
|
+
- **The `✗` samples are bound to a fixture whose rejection the suite asserts.**
|
|
54
|
+
A page showing input the engine must *reject* was the one kind of sample
|
|
55
|
+
nothing could see going stale: it carries no version, names no `src/` symbol,
|
|
56
|
+
and keeps its page's structure perfectly, so every gate in
|
|
57
|
+
`tests/docs-consistency.spec.ts` passed while the claim underneath it aged.
|
|
58
|
+
The failure that mattered is asymmetric — a stale runnable sample stops
|
|
59
|
+
working when a reader pastes it, while a stale counterexample is a reader
|
|
60
|
+
taught a rule that is no longer a rule, which is worse than teaching nothing.
|
|
61
|
+
A `✗` annotated with a code the engine can emit is now bound to a fixture,
|
|
62
|
+
byte-equal the way `fixtures/consumer/` is bound to the README, and the
|
|
63
|
+
fixture is **run through the reader the documented command uses** — the code
|
|
64
|
+
and the line it produces are compared with the code and the line the page
|
|
65
|
+
shows. The line is the half that catches the shape a code-only assertion would
|
|
66
|
+
not: a fixture still rejected, for a different reason, having quietly stopped
|
|
67
|
+
demonstrating what it is quoted for.
|
|
68
|
+
**Re-measuring the entry before building it moved its cost by a factor.** It
|
|
69
|
+
was filed as the expensive half, "a fixture per diagnostic". Of the 33 fenced
|
|
70
|
+
`ts` blocks in the README and `docs/en/**`, 8 carry a `✗`, 3 name a code the
|
|
71
|
+
engine can emit, and 1 of those 3 is elided (`duplicate-prefix`, taught as two
|
|
72
|
+
one-line registries whose bodies are `{ /* … */ }`) — leaving one sample,
|
|
73
|
+
quoted on two pages, bound to one fixture. The count was never the reason it
|
|
74
|
+
sat: what it was really waiting for is the thing that makes the roster hold,
|
|
75
|
+
which is that the binding is a **sweep** rather than a list. A new `✗ code`
|
|
76
|
+
block on any English page is bound to a fixture or fails as `unbound`, so the
|
|
77
|
+
next diagnostic pays a fixture because it is obliged to, not because someone
|
|
78
|
+
remembered the roster existed.
|
|
79
|
+
**Proven able to go red before being believed**, since a gate over samples
|
|
80
|
+
that are all currently correct has never failed and cannot be trusted until it
|
|
81
|
+
does: four mutations, one per way the binding can rot — the page edited away
|
|
82
|
+
from the fixture, the fixture made static so the engine accepts it, a `✗`
|
|
83
|
+
sample added bound to nothing, and the defect moved to a line the annotation
|
|
84
|
+
does not mark. Each was reported naming the page or the fixture.
|
|
85
|
+
*Two limits, both structural.* **English only**: `docs/zh/**` matches
|
|
86
|
+
`docs/en/**` down to the comments inside its samples, so byte-equality is a
|
|
87
|
+
claim only one side can carry, and what covers the Chinese side is the parity
|
|
88
|
+
gate from `[0.4.3]` holding the code-sample sequences equal — structure, not
|
|
89
|
+
meaning. And an **elided block is excluded by rule**, not by omission from the
|
|
90
|
+
roster: `{ /* … */ }` is what makes that sample readable and also what makes it
|
|
91
|
+
unrunnable. The residual risk recorded in `[0.4.3]` does *not* apply here —
|
|
92
|
+
the byte gate and the run proving the fixture still fails are in the same
|
|
93
|
+
file, so a fixture that stopped being rejected cannot pass by living in a
|
|
94
|
+
suite nobody connected to the page.
|
|
95
|
+
|
|
96
|
+
- **`ATX-15` is swept over `src/` from the AST, rather than attested one site at
|
|
97
|
+
a time.** The requirement quantifies over *sites* — every ordering committed to
|
|
98
|
+
disk or compared for equality — and `sites` is the one shape the quantifier
|
|
99
|
+
table records as able to stop being true in silence: a new site is a new
|
|
100
|
+
obligation and nothing enumerates them. Two of its five sites were `null`:
|
|
101
|
+
`red-record.ts` sorts ids and scenario names into `first-run.json`, and
|
|
102
|
+
`splice.ts` sorts ids and param keys into the registry file `--apply` writes.
|
|
103
|
+
Both are committed to disk, both were correct, and neither had a scenario. A
|
|
104
|
+
scenario per site would have closed those two and left the shape untouched,
|
|
105
|
+
because site six would have been unattested the day it was added. The sweep
|
|
106
|
+
instead makes the roster one the **code** owns — every `.sort`/`.toSorted` in
|
|
107
|
+
`src/` is a member — so a new ordering site is covered by construction. The
|
|
108
|
+
sanctioned comparator, the sort methods and the banned APIs are `ATX-15`
|
|
109
|
+
params, read by the scenario, pinned against literals for the reason the
|
|
110
|
+
domain table states: shrinking a list a scenario iterates removes a case and
|
|
111
|
+
everything left still passes.
|
|
112
|
+
**Stated narrowly enough to be true: it constrains how a site orders, not that
|
|
113
|
+
it orders.** A `red-record.ts` that stopped sorting altogether is still green
|
|
114
|
+
here, and the three hand-written scenarios are what speak to the result. That
|
|
115
|
+
is the honest limit of what a sweep can prove, and the table records it rather
|
|
116
|
+
than letting the row read as fully discharged.
|
|
117
|
+
**It was proven able to go red against the real corpus before being believed** —
|
|
118
|
+
three injected defects, one per shape the rule admits: a `localeCompare`
|
|
119
|
+
comparator in `red-record.ts`, a bare `.sort()` in `splice.ts`, and a
|
|
120
|
+
hand-rolled comparator in `merge.ts` that never reaches `byCodeUnit`. Each was
|
|
121
|
+
reported with its file, line and offending source. A gate asserting a property
|
|
122
|
+
that already holds everywhere has never been red and cannot become so until it
|
|
123
|
+
fires, so a negative control is not optional for it; a second scenario keeps
|
|
124
|
+
one in the suite permanently, including the case a line-wise scan gets wrong.
|
|
125
|
+
**Re-measuring the proposal's figures before building it changed two of
|
|
126
|
+
them**, which is the section's own rule and the reason it is worth restating:
|
|
127
|
+
the entry claimed 21 `.sort` call sites (there are **20** — the 21st was a
|
|
128
|
+
comment, which a regex counts and an AST does not), and two exceptions to
|
|
129
|
+
"names `byCodeUnit` on the call line" (there are **six**: one delegating to a
|
|
130
|
+
local `compareIds`, five inline arrows). The exception count being 3× what was
|
|
131
|
+
written is what made following local delegation part of the rule rather than a
|
|
132
|
+
footnote. The `localeCompare` figure moved the same way and in the direction
|
|
133
|
+
that matters most: **both occurrences in `src/` are comments explaining why it
|
|
134
|
+
is not used**, so a line-wise regex would have opened at 100% false positives.
|
|
135
|
+
Reading from the AST was the entry's own recommendation; it turns out to have
|
|
136
|
+
been the difference between a gate and a nuisance.
|
|
137
|
+
One shape the rule admits was not in the proposal and is worth naming:
|
|
138
|
+
`report.ts` sorts by `b.count - a.count || byCodeUnit(…)`. A numeric term is
|
|
139
|
+
legitimate, so the rule is "every comparator **reaches** the sanctioned one",
|
|
140
|
+
never "sorts only by code unit" — a stricter rule would have been red on
|
|
141
|
+
correct code the day it was written.
|
|
142
|
+
|
|
143
|
+
### Fixed
|
|
144
|
+
|
|
145
|
+
- **`attest check` answered `✓ No issues.` for a `*.reqs.ts` that does not
|
|
146
|
+
compile.** TypeScript's parser is error-tolerant by design: handed a file
|
|
147
|
+
truncated before its closing `});` it recovers the object literal and returns a
|
|
148
|
+
tree anyway, so the static reader read it as a *complete* registry and reported
|
|
149
|
+
nothing at all — while `--eval` on the same bytes said `registry-invalid`. Two
|
|
150
|
+
readers of one file disagreeing about whether it is even a file is the
|
|
151
|
+
condition the differential suite exists to prevent, and that suite could not
|
|
152
|
+
have caught it: it runs over registries that all compile. Nothing was looking
|
|
153
|
+
at `parseDiagnostics`, which is the one field that says so.
|
|
154
|
+
**The reach is three readers, not one**, because the tolerance belongs to the
|
|
155
|
+
compiler rather than to any reader: `parser.ts`, `static-registry.ts` and
|
|
156
|
+
`splice.ts` all open with the same `createSourceFile` call. The same half-saved
|
|
157
|
+
file was also a spec whose scenarios above the break still counted as coverage,
|
|
158
|
+
a delta whose ids `status` still reported, and — the site that writes rather
|
|
159
|
+
than reports — an insertion offset `attest apply` would have spliced a new
|
|
160
|
+
requirement into. All four now refuse it: the three readers as
|
|
161
|
+
`unreadable-file`, and the insertion point by returning the `undefined` it
|
|
162
|
+
already documents as its refusal to guess, because a write path that throws
|
|
163
|
+
mid-merge is worse than one that declines.
|
|
164
|
+
The diagnostic carries the **line** the syntax error is on, through both
|
|
165
|
+
shapes of refusal — the readers return it, and the spec parser carries it
|
|
166
|
+
through the throw its guard already catches. Without that second half the same
|
|
167
|
+
code would have had a line for a registry and none for a spec, for one
|
|
168
|
+
condition.
|
|
169
|
+
`unreadable-file` rather than a new code: `ATX-65` already states this
|
|
170
|
+
obligation for a file the parser cannot read, and the reader's situation is
|
|
171
|
+
identical — nothing in the file was checked, everything else in the run still
|
|
172
|
+
was. What `ATX-65` did not state is the *detection*, which is the whole of what
|
|
173
|
+
was missing; it was written for a parser that throws, and a parser that quietly
|
|
174
|
+
succeeds needs a different question asked of it.
|
|
175
|
+
**Syntactic only, and that is what makes it safe rather than merely narrow.**
|
|
176
|
+
`parseDiagnostics` is the parser's own list, so a type error, an unresolved
|
|
177
|
+
import and a name that does not exist are all absent from it — Attest does not
|
|
178
|
+
typecheck an adopter's project and this does not start. Measured over the 129
|
|
179
|
+
TypeScript files in this repository with every fixture included: zero carry a
|
|
180
|
+
parse diagnostic, so nothing valid today starts failing. The one reader left
|
|
181
|
+
deliberately tolerant is the id recovery added above, which runs only on files
|
|
182
|
+
a reader has already refused.
|
|
183
|
+
|
|
184
|
+
- **Two scenarios named by a template literal were invisible to the static
|
|
185
|
+
plan.** `self/half-loaded.spec.ts` generated one scenario per command, and the
|
|
186
|
+
parser reads names from the AST — so `cover` counted one scenario where two
|
|
187
|
+
ran, and `archive`'s never-red gate could never have seen the second. Found
|
|
188
|
+
while binding the new requirement's roster to a scenario the plan declares.
|
|
189
|
+
Both are now one scenario looping the roster, which is the shape `ATX-64`
|
|
190
|
+
already used; the label on each `expect` is what the generated name was buying,
|
|
191
|
+
and it survives a failure. **The engine is not changed**: a name a static
|
|
192
|
+
reader cannot resolve is not a defect in the reader, and the failure it
|
|
193
|
+
produces on a requirement covered *only* that way is `uncovered-requirement`,
|
|
194
|
+
which is loud.
|
|
195
|
+
|
|
196
|
+
### Changed
|
|
197
|
+
|
|
198
|
+
- **One unreadable `*.reqs.ts` reported a finding per scenario that attested it,
|
|
199
|
+
and the finding was about the file.** The ids that file declares are missing
|
|
200
|
+
from the run, so every scenario covering one of them was an `orphan-test` —
|
|
201
|
+
measured on a registry of three requirements: six ERRORs, of which one was the
|
|
202
|
+
cause and five were its shadow, with the true finding at the top where the wall
|
|
203
|
+
of red pushes it out of view. Those scenarios are now reported once per
|
|
204
|
+
registry file as `orphan-from-failed-registry` (WARNING), naming the file, the
|
|
205
|
+
count and the ids; the same case is now two ERRORs and one WARNING. A WARNING
|
|
206
|
+
because the load failure is already an ERROR and is present whenever this is,
|
|
207
|
+
so no verdict moves and this cannot make a broken registry pass.
|
|
208
|
+
**What unblocked it was that the question was framed wrongly**, and the entry
|
|
209
|
+
under `Under consideration` had carried that framing for two releases: it asked
|
|
210
|
+
for a prefix→file mapping, which is exactly what a file that never parsed
|
|
211
|
+
cannot give — and could only have been recovered from the file *name*, which
|
|
212
|
+
this repository is its own counterexample to (`attest.reqs.ts` holds `ATX-*`,
|
|
213
|
+
the evidence that killed the prefix-matches-filename rule). The **ids** are
|
|
214
|
+
written in the source, and reading them back needs no naming convention that
|
|
215
|
+
nothing enforces. It is also strictly more precise than the prefix would have
|
|
216
|
+
been: a genuine typo sharing a prefix with the broken file stays reported.
|
|
217
|
+
Recall is partial and the direction of the miss is why that is acceptable — an
|
|
218
|
+
id not recovered keeps its `orphan-test`, so the report stays noisy and never
|
|
219
|
+
wrong. Measured over the ways a registry fails: a truncated file, a missing
|
|
220
|
+
default export, a non-literal value, a schema-invalid entry, a module that
|
|
221
|
+
throws at import and a spread from another module all yield their ids; only a
|
|
222
|
+
registry the file *computes* yields none, and that source contains no id to
|
|
223
|
+
find. The recovery is a source read that returns ids and nothing else — it
|
|
224
|
+
builds no registry, so it is not a second reader and cannot become the
|
|
225
|
+
evaluation fallback `registry-not-static` refuses.
|
|
226
|
+
Stated as a replacement rather than a suppression for the reason `ATX-62`
|
|
227
|
+
records about `spec-load-failed`: suppressing with nothing in its place trades
|
|
228
|
+
a wrong message for silence, and a wrong message beats silence.
|
|
229
|
+
|
|
230
|
+
- **The hedged `orphan-test` advice added in `[0.7.0]` was applied to every
|
|
231
|
+
orphan in the report, including the ones it does not describe.** Any registry
|
|
232
|
+
file failing to load switched *all* of them to "a registry file failed to load
|
|
233
|
+
— fix that first, and add this id only if it is still unknown afterwards",
|
|
234
|
+
so a genuine typo was told to wait for work that has nothing to do with it.
|
|
235
|
+
The hedge now fires only where the uncertainty is real: when a failed file's
|
|
236
|
+
ids could not be recovered. When they all could, an orphan none of them claims
|
|
237
|
+
is genuinely unknown and gets the plain advice back.
|
|
238
|
+
|
|
239
|
+
`SCHEMA_VERSION` is unchanged: a new `code` value is additive, and no envelope
|
|
240
|
+
field moved.
|
|
241
|
+
|
|
242
|
+
## [0.7.3] - 2026-08-07
|
|
243
|
+
|
|
244
|
+
### Fixed
|
|
245
|
+
|
|
246
|
+
- **A loader that never opened left its scratch directory behind — one per
|
|
247
|
+
`attest` invocation, i.e. one per CI build.** `createLoader` creates the
|
|
248
|
+
directory and writes the stub, and the only `rm` of it lives inside the `close`
|
|
249
|
+
of the object it returns — so nothing between those two points can be cleaned
|
|
250
|
+
up by anyone, because until the caller holds that object no `close` exists to
|
|
251
|
+
call. It is the exact leak the comment on `close` records as already fixed,
|
|
252
|
+
surviving on the other side of the same function. The window is now closed by
|
|
253
|
+
the function that opened it.
|
|
254
|
+
*Stated over the window rather than over `createServer`, which is where it was
|
|
255
|
+
found.* `writeFileSync` is inside it too, and a full disk or a temp directory
|
|
256
|
+
that turns read-only between the two calls reaches it with no project
|
|
257
|
+
involved — so scoping the repair to the call it was noticed at would have left
|
|
258
|
+
half of it.
|
|
259
|
+
*The trigger this was filed on does not reproduce, and that is the useful
|
|
260
|
+
half.* The entry proposed a malformed `package.json` at the root, on the
|
|
261
|
+
reasoning that `configFile: false` still leaves Vite reading it for `type`.
|
|
262
|
+
Measured: it starts a server cleanly, as do eight other shapes — a
|
|
263
|
+
`package.json` holding an array, one that is a directory, an unknown `type`, a
|
|
264
|
+
`node_modules` occupied by a file, a `node_modules/.vite` occupied by a file, a
|
|
265
|
+
`.env` that is a directory, a deleted root, and a well-formed control. Nine
|
|
266
|
+
candidates, zero throws. **`configFile: false` is why**, and it is worth
|
|
267
|
+
recording as a property rather than an accident: the option set chosen to stop
|
|
268
|
+
the loader reading a project's Vite config, and to stop it opening a socket,
|
|
269
|
+
also leaves almost no project I/O that can fail hard. The throw surface was
|
|
270
|
+
closed as a side effect of decisions taken for other reasons.
|
|
271
|
+
*So the repair is justified by ownership rather than by a symptom*, which is a
|
|
272
|
+
weaker case than this file usually accepts and is why the alternative was to
|
|
273
|
+
reject the entry outright. What decides it is the asymmetry of cost: the fan-out
|
|
274
|
+
entry below stays open because bounding concurrency changes how every run
|
|
275
|
+
behaves, whereas a `try` around a window nothing currently reaches changes
|
|
276
|
+
nothing observable on any path that exists. A resource whose ownership transfer
|
|
277
|
+
is conditional on nothing throwing is a shape, and the shape is what recurs.
|
|
278
|
+
*A cleanup that fails must not replace the error that caused it*, so the `rm`
|
|
279
|
+
swallows its own failure and the original throw propagates unchanged. The
|
|
280
|
+
regression test asserts the identity of what comes out, not its wording.
|
|
281
|
+
**`close` had the same hazard and now makes the same choice, which is a change
|
|
282
|
+
to an existing path rather than a new one.** Its `rm` runs in a `finally`, so a
|
|
283
|
+
failed cleanup replaced whatever `close` was doing — and all five callers in
|
|
284
|
+
`pipeline.ts` call `close` from a `finally` of their own, where the damage is
|
|
285
|
+
larger than symmetry: a real error on the way out is lost, and a *successful*
|
|
286
|
+
command becomes a crash about a temp directory. What that reader is handed is a
|
|
287
|
+
misdiagnosis pointing at their own correct files, which this project already
|
|
288
|
+
treats as worse than the gap it fills — the argument `ATX-49` records against
|
|
289
|
+
reporting a load failure as a wall of skipped scenarios, in a much smaller
|
|
290
|
+
place. Given up with it: the one signal a failed cleanup could have raised,
|
|
291
|
+
which no caller could have acted on.
|
|
292
|
+
*The test goes red without the fix, which is the only reason it is worth
|
|
293
|
+
having.* Verified by reverting `src/` and re-running: `expected [
|
|
294
|
+
'attest-loader-z7jhoK' ] to deeply equal []`, the leaked directory named in the
|
|
295
|
+
failure. It injects the throw at the module boundary — no project input could
|
|
296
|
+
produce one — and mocks `tmpdir` to a directory of its own, because on the
|
|
297
|
+
throwing path there is no loader to ask for its `scratchDir` and counting
|
|
298
|
+
`attest-loader-*` in the shared temp directory is the flake the lifecycle test
|
|
299
|
+
beside it already documents.
|
|
300
|
+
*It is its own file*, which is a rule this repo already wrote down and which
|
|
301
|
+
the first draft broke: `tests/locate-fanout.spec.ts` is separate because
|
|
302
|
+
`vi.mock` is file-scoped and it mocks a Node builtin, and the four lifecycle
|
|
303
|
+
tests boot the *real* Vite loader — sharing a module registry with a
|
|
304
|
+
`createServer` that throws is exactly what that convention exists to prevent.
|
|
305
|
+
The first attempt scoped the mocks with `doMock` and `resetModules` inside the
|
|
306
|
+
shared file instead, which is machinery bought to work around a constraint the
|
|
307
|
+
repo had already decided how to solve. Following it deleted the machinery.
|
|
308
|
+
*No new requirement, and the precedent cuts the other way, so it is worth
|
|
309
|
+
saying why.* `ATX-36` is the sibling property — what a live loader leaves on the
|
|
310
|
+
user's machine, stated as a count, with a scenario in `self/` — and "leaves no
|
|
311
|
+
scratch directory" is the same shape. What decides against filing one here is
|
|
312
|
+
that the success-path half has never had a requirement either, so a new one
|
|
313
|
+
covering only the throwing path would state half a contract and cost a
|
|
314
|
+
permanent scenario for the half nothing reaches. If it is ever filed it should
|
|
315
|
+
be filed whole, and that is a larger change than this fix.
|
|
316
|
+
|
|
317
|
+
- **The `cmd.exe` guard in `tests/consumer.spec.ts` checked one character of a
|
|
318
|
+
class, and the comment above it claimed the quoting was doing work it cannot
|
|
319
|
+
do here.** `shellArg` asserted the argument held no `"` and then quoted only
|
|
320
|
+
when it held whitespace, so an argument with neither went to the shell bare
|
|
321
|
+
with `&`, `|`, `^`, `<`, `>`, `(` unexamined. Measured against the twelve
|
|
322
|
+
shapes: the old assertion let **11 of 12** through.
|
|
323
|
+
*What decided it, and it is the thing the entry this replaces did not have.*
|
|
324
|
+
That entry framed the choice as "widen the class, or fix the prose", and
|
|
325
|
+
argued that widening while leaving the prose is the change that looks like a
|
|
326
|
+
fix and settles nothing. Both halves turn on a fact neither named: **every
|
|
327
|
+
command this suite spawns is a `.cmd`**, which is the whole reason
|
|
328
|
+
`NEEDS_SHELL` exists, and a batch file expands `%*` into its own line *before*
|
|
329
|
+
anything parses it — so a metacharacter survives double quotes and is acted on
|
|
330
|
+
at the far end. That is the argument-injection hole Node closed in 22.x,
|
|
331
|
+
arrived at the long way round. So the quoting is not a defence that the
|
|
332
|
+
assertion merely backstops; **the assertion is the only defence**, and a
|
|
333
|
+
defence that covers one character of its class is the defect rather than the
|
|
334
|
+
prose being generous. Both are fixed together: the class is named, and the
|
|
335
|
+
comment now says the quotes do whitespace and nothing else.
|
|
336
|
+
*`%` and `!` are in the class for a different reason* — expansion rather than
|
|
337
|
+
syntax, and `!` only under delayed expansion, which the spawned shell does not
|
|
338
|
+
enable but a batch file can enable for itself. Neither costs anything to
|
|
339
|
+
refuse.
|
|
340
|
+
*The cost, which is real and is the right cost.* Windows permits `&`, `(` and
|
|
341
|
+
`%` in a user name, and `os.tmpdir()` sits under it — so on such a machine this
|
|
342
|
+
suite now stops with *"argument would need escaping, not quoting"* instead of
|
|
343
|
+
running. That is correct rather than a false red: through a batch target those
|
|
344
|
+
arguments genuinely are not safe, and the invariant the assertion guards is
|
|
345
|
+
that every argument is a literal flag or a path from `mkdtemp`. Stopping is
|
|
346
|
+
right even where quoting would have survived it, because the value is then not
|
|
347
|
+
the kind of value the helper accepts.
|
|
348
|
+
*What is measured, and what is not.* `pnpm test:consumer` was run on Windows,
|
|
349
|
+
where `NEEDS_SHELL` is true and the changed path actually executes: 11 tests
|
|
350
|
+
passed, none skipped — the distinction that matters, since the failure this
|
|
351
|
+
file's own comment records reported ten tests as *skipped* at zero run. CI is
|
|
352
|
+
Linux, where `shellArg` returns before the assertion, so the pipeline says
|
|
353
|
+
nothing about this either way. Not measured end to end: an argument carrying a
|
|
354
|
+
metacharacter actually reaching the guard, because no call site can produce
|
|
355
|
+
one — the class is verified as a predicate over the twelve shapes above and
|
|
356
|
+
the sixteen arguments the suite really passes, not by driving the suite red.
|
|
357
|
+
|
|
358
|
+
- **A statement or rationale can no longer become HTML, or open a section of
|
|
359
|
+
its own.** `sanitised` is the entry §9.1 names and it strips control
|
|
360
|
+
characters and nothing else, so `statement` and `rationale` reached the
|
|
361
|
+
Markdown by concatenation exactly as written. Measured: a `rationale`
|
|
362
|
+
containing a newline and `## ATX-999` rendered a whole section — heading,
|
|
363
|
+
statement, `**Why:**` — for a requirement no registry holds, sitting between
|
|
364
|
+
two real ones and worded identically, with the overview table above it listing
|
|
365
|
+
three ids and the body carrying four sections. `<script>` and `<img onerror>`
|
|
366
|
+
survived the same way into a file that is committed and served. `check` and
|
|
367
|
+
`cover` saw nothing, because the id is in no registry, and `render --check`
|
|
368
|
+
could not object, because that *was* what the registry rendered to.
|
|
369
|
+
*Which of the three is intended, which is the part the fix had to decide.*
|
|
370
|
+
Marking prose up stays — `plain` escapes a param value precisely because the
|
|
371
|
+
value is data and the sentence around it is not — and that decision had never
|
|
372
|
+
been separated from allowing raw HTML or a heading. It is now a **fifth
|
|
373
|
+
obligation in design §9.1** rather than a patch at an emitter: prose may be
|
|
374
|
+
marked up, it may not become a tag, and it may not open a section. Stated as a
|
|
375
|
+
count over the document — one heading per requirement id — because what a
|
|
376
|
+
reader trusts is that a section means a requirement (`ATX-67`).
|
|
377
|
+
*Escaped rather than refused, on an asymmetry.* A rule that misses something
|
|
378
|
+
ships the document it ships today; a rule that fires wrongly makes a
|
|
379
|
+
legitimate rationale a hard error in the one command whose value is that it
|
|
380
|
+
always produces the document — the `ATX-29` argument about `--check` seen from
|
|
381
|
+
the other side. So there is no new diagnostic and no exit code moves.
|
|
382
|
+
*And escaped for the position, which is what keeps it lossless.* A code span
|
|
383
|
+
is literal text in every renderer, so `<` written inside one is **visible**
|
|
384
|
+
— Markdown does not decode entities there, and this repository documents
|
|
385
|
+
`attest status <change>` in a statement. The scan therefore tracks spans and
|
|
386
|
+
leaves their contents alone, conservatively: a backtick run opens a span only
|
|
387
|
+
if a run of exactly that length closes it, and an unmatched run is text with
|
|
388
|
+
the defence still applied after it.
|
|
389
|
+
*One rendering change beyond the fix, and it is a repair.* A param
|
|
390
|
+
substituted inside the author's own code span was emitted as `**value**`,
|
|
391
|
+
which in a code span is two asterisks around the value; knowing the position
|
|
392
|
+
is what removes them. `SPEC.md` in this repository moves by 12 lines, all of
|
|
393
|
+
that shape — `` `**changes/<name>/first-run.json**` `` becomes
|
|
394
|
+
`` `changes/<name>/first-run.json` `` — and no `<` appears anywhere in it,
|
|
395
|
+
which is the measurement that the span exception works on real prose.
|
|
396
|
+
*The defence had the defect it was written next to, and `ATX-59` gained a
|
|
397
|
+
third shape because of it.* The first code-span scan searched forward from
|
|
398
|
+
each backtick run for a run of the same length, so a rationale whose runs are
|
|
399
|
+
all of *different* lengths paid a scan to the end of the string for every one
|
|
400
|
+
of them: 500 KB cost 1.6 s and 2 MB cost 15 s, from `attest render` with no
|
|
401
|
+
flag, on text the registry chooses — the same "a registry must not decide what
|
|
402
|
+
rendering costs" defect the padded statement and the backtick-run param
|
|
403
|
+
already stand for. The runs are now read once and indexed by length, each
|
|
404
|
+
length holding a forward-only cursor, which is linear: 40 ms at 2 MB. It is
|
|
405
|
+
filed under `ATX-59` rather than here because the obligation is that one, and
|
|
406
|
+
it is worth saying where the site came from — the *defence another requirement
|
|
407
|
+
added*, which is the direction the next one will arrive from too.
|
|
408
|
+
*This leaves `Planned`.* The forged section was reproduced first, then the
|
|
409
|
+
four scenarios were run against the pre-fix renderer: three went red, and the
|
|
410
|
+
fourth — the setext underline, which heads the line *above* it — went green,
|
|
411
|
+
because counting `##` lines cannot see a heading that has no `##` in it. That
|
|
412
|
+
scenario was rewritten to assert the neutralised underline instead. A scenario
|
|
413
|
+
that cannot fail is the defect `archive` exists to catch, and it was caught
|
|
414
|
+
here only by running it against the old code.
|
|
415
|
+
|
|
416
|
+
- **`attest init` no longer follows a symbolic link out of the project, and
|
|
417
|
+
refuses the whole run when a destination leads there.** `runInit` did
|
|
418
|
+
`join(root, target.file)`, `mkdir -p` and then the write, and nothing asked
|
|
419
|
+
whether the path was still under `root` once the filesystem had had its say.
|
|
420
|
+
A `.claude/skills/attest` planted in a repository as a link to anywhere the
|
|
421
|
+
user can write was followed by both: measured on Windows with a junction, a
|
|
422
|
+
file at the far end holding `USER OWNED FILE - DO NOT OVERWRITE` came back
|
|
423
|
+
holding the skill, and the report printed the relative path, so nothing on
|
|
424
|
+
screen said where it had gone. Every destination is now resolved segment by
|
|
425
|
+
segment before the first byte of any of them is written, and one that escapes
|
|
426
|
+
is an `unsafe-target-path` ERROR that writes nothing at all (`ATX-66`).
|
|
427
|
+
*What the exposure was, stated precisely, because two nearby things are not
|
|
428
|
+
it.* `writeAtomic`'s exclusive creation is not the gap and was never scoped to
|
|
429
|
+
be one: it defends the *temporary* path against a planted link, and it does
|
|
430
|
+
that perfectly in whichever directory the destination turns out to be. And
|
|
431
|
+
`workflowBody()` is a pure function of nothing, so no byte of the checked
|
|
432
|
+
project reaches the file — there is nothing to exfiltrate. What was exposed is
|
|
433
|
+
the **path**, which is `targets.ts`'s first scoping rule ("Attest owns the
|
|
434
|
+
path") having been an assumption rather than a check.
|
|
435
|
+
*Why the check is a resolution rather than a comparison.* `join` normalises
|
|
436
|
+
`..`, so a lexical containment test was already satisfied — the escape is a
|
|
437
|
+
segment that *is* a link, which only `realpath` can see. Containment itself
|
|
438
|
+
goes through `isInside` in `src/core/paths.ts`, asked as a relative path
|
|
439
|
+
rather than a string prefix: `/repo-backup` starts with `/repo`, and on
|
|
440
|
+
Windows a path on another drive has no relative spelling at all, so
|
|
441
|
+
`path.relative` answers with an absolute one instead of a chain of `..`. Both
|
|
442
|
+
read as contained under `startsWith`.
|
|
443
|
+
*The limit, recorded rather than papered over.* A link planted between the
|
|
444
|
+
resolution and the write is not caught, and no check outside the filesystem
|
|
445
|
+
can catch it. The exposure this closes is one committed to a repository, which
|
|
446
|
+
is the one a fresh clone and every merge-request checkout hands you.
|
|
447
|
+
*Behaviour change:* an `init` that used to write through such a link now
|
|
448
|
+
fails with exit 1. The only input that reaches it is a path that was already
|
|
449
|
+
leaving the project. The JSON surface is unchanged beyond one more `code`
|
|
450
|
+
value, which is additive — no `SCHEMA_VERSION` bump.
|
|
451
|
+
*This leaves `Planned`*, where it was filed with the measurement above; the
|
|
452
|
+
first act of building it was re-running that measurement, and it reproduced.
|
|
453
|
+
|
|
454
|
+
### Changed
|
|
455
|
+
|
|
456
|
+
- **The guidance now draws the line between a literal that is drift and a literal
|
|
457
|
+
that is a pin — the shipped text forbade the only defence against a shortened
|
|
458
|
+
domain.** `[0.7.2]` fixed the repo half: every param a scenario iterates gained
|
|
459
|
+
a literal pinning its extent, after three of that release's four green
|
|
460
|
+
mutations turned out to be a domain silently losing cases rather than the
|
|
461
|
+
tautology the experiment was looking for. The user-facing half was held back,
|
|
462
|
+
and it was not merely absent. The `init` skill's "Four things you must not do"
|
|
463
|
+
says **"Do not hardcode a value that lives in `params`"**, and the pin every one
|
|
464
|
+
of those fixes adds is, read literally, exactly that. An adopter who writes a
|
|
465
|
+
roster param — which the design actively pushes them toward, since a quantifier
|
|
466
|
+
a scenario can iterate is the shape `QUANTIFIED` recommends — was being handed
|
|
467
|
+
an instruction that forbids the only thing standing between them and a run that
|
|
468
|
+
covers less every time someone edits the list.
|
|
469
|
+
*The distinction is real and one sentence long*, and it is now drawn in all four
|
|
470
|
+
places the claim is made: the rule is about the **expectation** the system is
|
|
471
|
+
measured against, and the pin asserts what the **intent** is. A list a scenario
|
|
472
|
+
loops over is not a value under test — it is the set of cases the run covers,
|
|
473
|
+
so dropping a member removes a case while every assertion inside still passes,
|
|
474
|
+
with the expectation as independent as it ever was. That is why the `[0.5.0]`
|
|
475
|
+
rule (*the expectation must not be a function of the param the code under test
|
|
476
|
+
consumed*) does not reach it, and why a literal is the **only** independent term
|
|
477
|
+
available: anything derived from the list shrinks with it, so a length assertion
|
|
478
|
+
written as a function of the list is true at every length.
|
|
479
|
+
*Where it landed, and why it is four documents rather than one line appended.*
|
|
480
|
+
README §"killer move", the `init` skill (a sibling bullet in the model section
|
|
481
|
+
and a carve-out inside rule 3 itself, where the contradiction was), the
|
|
482
|
+
`possible-drift` troubleshooting section with a worked ✓/✗ pair, and design §11
|
|
483
|
+
as a residual weakness beside the tautology bullet it shares a gap with — both
|
|
484
|
+
languages. `[0.5.0]` records the reason this shape of change is done in one
|
|
485
|
+
deliberate pass rather than incrementally: it is the central claim of the tool
|
|
486
|
+
being reworded, and the last time it was split the halves disagreed.
|
|
487
|
+
*No engine change, and none is proposed here.* Nothing in this is detectable —
|
|
488
|
+
which surface a statement claims is prose judgement, the same judgement §11
|
|
489
|
+
already records as the reason obligation counting can never be an engine rule.
|
|
490
|
+
The counter-pressure that exists is a hand-judged table, and §11 now says so,
|
|
491
|
+
including what it cannot reach: a domain whose size is carried by a scalar.
|
|
492
|
+
*This leaves `Planned`*, where it was filed with the measurement above; the
|
|
493
|
+
measurement was already taken, and what it needed was the pass.
|
|
494
|
+
|
|
495
|
+
- **The release publishes through `pnpm` again, and the check that read the
|
|
496
|
+
attestation back is gone.** Two corrections to the provenance job shipped in
|
|
497
|
+
`[0.7.2]`, both from what the first real tag pipeline showed.
|
|
498
|
+
*`pnpm publish --provenance` works on the pinned 10.28.0; the claim that it
|
|
499
|
+
did not was read off `pnpm publish --help`, where the flag is absent.* The
|
|
500
|
+
parser knows it — `--provenanceX` is rejected with *"Did you mean
|
|
501
|
+
'provenance'?"*, and `--provenance --dry-run` runs clean. A help text is a
|
|
502
|
+
document, not the interface; checking a flag means asking the parser. So the
|
|
503
|
+
job is pnpm like every other job here, `--no-git-checks` comes back with it
|
|
504
|
+
(pnpm inspects the branch, CI builds tags in detached HEAD), and
|
|
505
|
+
`COREPACK_ENABLE_STRICT=0` goes away, since it existed only so a bare `npm`
|
|
506
|
+
could run under a `packageManager` that pins pnpm.
|
|
507
|
+
*The post-publish check failed the 0.7.2 release, and the publish it failed
|
|
508
|
+
was correctly signed.* Six attempts over a minute all got a 404 from the
|
|
509
|
+
registry's attestation endpoint; the same endpoint now serves
|
|
510
|
+
`slsa.dev/provenance/v0.2` for `0.7.2`. Lengthening the budget was the wrong
|
|
511
|
+
repair, and the entry it replaces already contains the reason without drawing
|
|
512
|
+
the conclusion: the check runs *after* an irreversible step, so its red cannot
|
|
513
|
+
be cleared — the republish that would rerun it fails `EPUBLISHCONFLICT` on a
|
|
514
|
+
version that now exists. Every failure it can reach is therefore either a
|
|
515
|
+
false one or an unfixable one, on a job nobody can make green again, which
|
|
516
|
+
makes it a machine for teaching that red is routine — the exact habit the
|
|
517
|
+
`audit` job's comment refuses. A gate has to sit where the answer can still
|
|
518
|
+
change what happens. Verification moves to where it can: `npm audit
|
|
519
|
+
signatures` in an installing project, or the package page's Provenance panel.
|
|
520
|
+
*What is unchanged:* the attestation itself, the three preconditions, and the
|
|
521
|
+
fact that npm rejects the publish rather than degrading to an unsigned one if
|
|
522
|
+
any is missing. `0.7.2` is signed, and shipped signed, despite the red job.
|
|
523
|
+
|
|
16
524
|
## [0.7.2] - 2026-08-07
|
|
17
525
|
|
|
18
526
|
### Added
|
|
@@ -3365,91 +3873,17 @@ about itself, which is three things.
|
|
|
3365
3873
|
**An entry can sit here for releases on the strength of a figure nobody has
|
|
3366
3874
|
checked.** The discipline aims at keeping the *argument*, not at re-testing the
|
|
3367
3875
|
evidence under it, so the first act of building one is re-measuring what it
|
|
3368
|
-
claims rather than implementing what it proposes. That has paid
|
|
3369
|
-
killing an entry outright, when the spans it rested on turned out not to be
|
|
3370
|
-
spans;
|
|
3371
|
-
|
|
3876
|
+
claims rather than implementing what it proposes. That has paid three times:
|
|
3877
|
+
once killing an entry outright, when the spans it rested on turned out not to be
|
|
3878
|
+
spans; twice correcting an entry's own numbers before a line of it was written —
|
|
3879
|
+
a call-site count that a regex had inflated, and a cost filed as "a fixture per
|
|
3880
|
+
diagnostic" that the corpus put at one fixture. **What a release costs its adopters is not known on the day it is cut**
|
|
3372
3881
|
— several entries arrived from one adoption report, about shapes the tool made
|
|
3373
3882
|
someone work around without ever erroring. And **the fixes with no observable
|
|
3374
3883
|
symptom are worth batching**: individually none justifies a commit, together they
|
|
3375
3884
|
cost about an hour, and they are the group no gate could have found, since having
|
|
3376
3885
|
no symptom is the definition of what a gate cannot see.
|
|
3377
3886
|
|
|
3378
|
-
- **Telling adopters about the domain trap, which the shipped guidance
|
|
3379
|
-
currently walks them into.** The repo half landed under `[Unreleased]`; the
|
|
3380
|
-
user-facing half has not, and it is not merely absent. `skill.ts`'s "Four
|
|
3381
|
-
things you must not do" says **"Do not hardcode a value that lives in
|
|
3382
|
-
`params`"**, and the pin every fix in that entry adds is, read literally,
|
|
3383
|
-
exactly that. An adopter who writes a roster param — which the design
|
|
3384
|
-
actively pushes them toward, since a quantifier a scenario can iterate is the
|
|
3385
|
-
shape `QUANTIFIED` recommends — gets an instruction that forbids the only
|
|
3386
|
-
defence against shortening it. The distinction is real and one sentence long:
|
|
3387
|
-
the rule is about the **expectation** the system is measured against, and the
|
|
3388
|
-
pin asserts what the **intent** is. Nothing shipped draws it.
|
|
3389
|
-
*Why it is filed rather than done in the same pass.* It reaches README §"killer
|
|
3390
|
-
move", the `init` skill, `possible-drift`'s troubleshooting section and design
|
|
3391
|
-
§11, in both languages — the same surface `[0.5.0]` rewrote, and that entry
|
|
3392
|
-
records the lesson: rewording the central claim of the tool is not a line
|
|
3393
|
-
appended, and it was held back once precisely so it could be done deliberately
|
|
3394
|
-
and in one pass. The measurement it needs is already taken; what it needs next
|
|
3395
|
-
is the pass. *Not* a `possible-drift` change: nothing here is detectable by
|
|
3396
|
-
the engine, for the reason the entry below gives about prose judgement.
|
|
3397
|
-
|
|
3398
|
-
- **Binding the `✗` samples to fixtures whose rejection is asserted.** A sample
|
|
3399
|
-
showing input the engine must *reject* can be bound the way a runnable one is,
|
|
3400
|
-
to a fixture whose rejection is the thing under test — which is what
|
|
3401
|
-
`fixtures/eval-invalid-registry` already is. This is the more valuable half of
|
|
3402
|
-
what was once one entry with the region-marking below, because a counterexample
|
|
3403
|
-
that quietly stopped being a counterexample is the one kind of stale sample
|
|
3404
|
-
that actively teaches the wrong thing: a reader shown "this fails" about input
|
|
3405
|
-
the engine now accepts is worse off than one shown nothing. It is also the
|
|
3406
|
-
expensive half, needing a fixture per diagnostic, which is why it was never the
|
|
3407
|
-
free part of that entry and is now the only part left in it.
|
|
3408
|
-
*The mirror rule caps what can be claimed on the Chinese side.* `docs/zh/**`
|
|
3409
|
-
matches `docs/en/**` down to the comments inside its code samples, so
|
|
3410
|
-
byte-equality holds on one side only and the zh blocks stay translated prose
|
|
3411
|
-
around the same code — the parity gate in `[0.4.3]` covers them, and it covers
|
|
3412
|
-
structure, not meaning. The residual risk recorded with the whole-file half in
|
|
3413
|
-
`[0.4.3]` — that the gate and the run proving the fixture still works live in
|
|
3414
|
-
different suites — applies here identically.
|
|
3415
|
-
|
|
3416
|
-
- **`render`'s document can carry raw HTML and a forged requirement section.**
|
|
3417
|
-
`sanitised` is the entry §9.1 names and it strips control characters and
|
|
3418
|
-
nothing else, so `statement` and `rationale` reach the Markdown by
|
|
3419
|
-
concatenation exactly as written. Measured: a `rationale` containing newlines
|
|
3420
|
-
and `## ATX-999` renders a whole section — heading, statement, `**Why:**` —
|
|
3421
|
-
for a requirement no registry holds, sitting between two real ones and worded
|
|
3422
|
-
identically; `<script>` and `<img onerror>` survive the same way. `check` and
|
|
3423
|
-
`cover` see only the real id, and `render --check` cannot object, because that
|
|
3424
|
-
*is* what the registry renders to now.
|
|
3425
|
-
*Not an accepted risk, which is the part worth pinning.* §9.1 and the
|
|
3426
|
-
reference both scope their guarantee to control characters, and the
|
|
3427
|
-
reference's heading over that paragraph is "Safe to read, and safe to keep",
|
|
3428
|
-
with "a site generator" named in the threat it describes. What *is* a decision
|
|
3429
|
-
is that prose may be marked up — `plain()` escapes a param value precisely
|
|
3430
|
-
because it is data while the statement around it is not — and that decision
|
|
3431
|
-
has never been separated from allowing raw HTML or a heading. So the entry is
|
|
3432
|
-
as much about stating which of the three is intended as about the escaping,
|
|
3433
|
-
and if the answer is that Markdown stays and HTML and headings go, that is a
|
|
3434
|
-
fourth obligation in §9.1 rather than a patch at an emitter.
|
|
3435
|
-
|
|
3436
|
-
- **`attest init` follows a symlink out of the project root.** `runInit` does
|
|
3437
|
-
`mkdir(dirname(dest), { recursive: true })` and then `writeAtomic`, and
|
|
3438
|
-
neither asks whether `dest` is still under `root`. A `.claude/skills/attest`
|
|
3439
|
-
planted in the repository as a link to anywhere the user can write is followed
|
|
3440
|
-
by both: measured on Windows with a junction, a file at the target holding
|
|
3441
|
-
`USER OWNED FILE - DO NOT OVERWRITE` came back holding the skill. The report
|
|
3442
|
-
prints the relative path, so nothing on screen says where it went.
|
|
3443
|
-
*`write.ts` is not the gap and neither is `targets.ts`'s content.* The `wx`
|
|
3444
|
-
flag defends the *temporary* path against a planted link and does exactly
|
|
3445
|
-
that; the destination's directory was never in its scope. And `workflowBody()`
|
|
3446
|
-
is a pure function of nothing, so no byte of the checked project reaches the
|
|
3447
|
-
file — the exposure is the path, not the content, which is `targets.ts`'s
|
|
3448
|
-
first rule ("Attest owns the path") being an assumption rather than a check.
|
|
3449
|
-
The check belongs beside `resolveTargets`, which is already all-or-nothing for
|
|
3450
|
-
the same reason: a name known to be unwritable must not leave half a set
|
|
3451
|
-
behind.
|
|
3452
|
-
|
|
3453
3887
|
## Under consideration
|
|
3454
3888
|
|
|
3455
3889
|
Candidates with **no decision yet**, between the two sections either side of it
|
|
@@ -3463,7 +3897,7 @@ decision, because that is what a re-proposal a year from now would otherwise
|
|
|
3463
3897
|
have to gather again.
|
|
3464
3898
|
|
|
3465
3899
|
- **Trusted publishing, which would delete `NPM_TOKEN` rather than add to it.**
|
|
3466
|
-
The provenance work in `[
|
|
3900
|
+
The provenance work in `[0.7.2]` signs the release but does not change
|
|
3467
3901
|
what authorises it: a long-lived automation token, masked and protected, that
|
|
3468
3902
|
publishes as us to anyone holding it. npm's trusted publishing exchanges the
|
|
3469
3903
|
same OIDC identity the attestation already uses for a short-lived credential,
|
|
@@ -3481,41 +3915,6 @@ have to gather again.
|
|
|
3481
3915
|
npm's current trusted-publisher documentation for GitLab support before
|
|
3482
3916
|
writing any YAML.
|
|
3483
3917
|
|
|
3484
|
-
- **A static sweep for the two properties claimed over the whole of `src/`.**
|
|
3485
|
-
The quantifier table's first strict read left three rows wanting the same
|
|
3486
|
-
thing, and the repo already owns the idiom: `tests/import-boundary.spec.ts` is
|
|
3487
|
-
a sweep over the import graph asserting a cross-cutting property no runtime
|
|
3488
|
-
test can see, written for the same reason — "only the graph shows it".
|
|
3489
|
-
*The two candidates, and they are not equally shippable.* `ATX-15` is the easy
|
|
3490
|
-
one: **no `.sort` outside `byCodeUnit`, and no `localeCompare`.** Measured over
|
|
3491
|
-
`src/` today — 21 `.sort` call sites, every one reaching `byCodeUnit`, but only
|
|
3492
|
-
19 naming it on the call line: `render.ts` goes through its own `compareIds`,
|
|
3493
|
-
and `merge.ts` through a comparator that spans two lines. The only four
|
|
3494
|
-
occurrences of `localeCompare` are comments explaining why it is not used. A sweep would have **zero false positives on the current corpus**,
|
|
3495
|
-
which is the bar `divergent-param` failed and `compound-requirement` cleared —
|
|
3496
|
-
and those two exceptions are already the warning that this needs the AST rather
|
|
3497
|
-
than a line-wise regex, plus a rule for a comparator that delegates. `ATX-37` is the hard one: what it needs is "every project-derived
|
|
3498
|
-
string interpolated into a terminal line goes through `inline`", and that is
|
|
3499
|
-
not a token scan. The shape that would work is a scenario driving every
|
|
3500
|
-
exported `format*` in `src/cli/report.ts` with hostile input — a real roster,
|
|
3501
|
-
since the module's exports are what a new formatter joins — but each takes a
|
|
3502
|
-
different argument type, so it needs a hostile fixture per signature rather
|
|
3503
|
-
than one loop.
|
|
3504
|
-
*What blocks the decision.* Two things, one per candidate. The `ATX-15` sweep
|
|
3505
|
-
is cheap and precise and would be **the first rule in this repository that
|
|
3506
|
-
polices how `src/` is written rather than what it does** — a lint rule wearing
|
|
3507
|
-
a scenario's clothes, and the question is whether that belongs in `self/` (as
|
|
3508
|
-
an obligation `ATX-15` already carries) or in `tests/` beside the import
|
|
3509
|
-
boundary. Filing it under `ATX-15` makes it a permanent obligation with a
|
|
3510
|
-
scenario; filing it in `tests/` makes it deletable in a green diff, which
|
|
3511
|
-
`ATX-57`'s rationale argues against in as many words. The `ATX-37` roster is
|
|
3512
|
-
blocked on cost rather than principle: eight formatters, eight fixtures, and
|
|
3513
|
-
the honest count of what it buys is three currently-correct sites.
|
|
3514
|
-
*Why neither is urgent.* Both properties hold everywhere today, verified by
|
|
3515
|
-
reading. What is missing is not correctness but the thing that keeps it — and
|
|
3516
|
-
the measurement above is what a re-proposal would otherwise have to gather
|
|
3517
|
-
again.
|
|
3518
|
-
|
|
3519
3918
|
- **Bounding the fan-out in `findFiles` and `parseSpecs`.** Both walk the
|
|
3520
3919
|
project with no concurrency limit: `findFiles` recurses through
|
|
3521
3920
|
`Promise.all(subdirs.map(walk))`, so every directory in the tree is opened at
|
|
@@ -3751,17 +4150,34 @@ have to gather again.
|
|
|
3751
4150
|
weaker signal "some registry file failed to load, so suppress all
|
|
3752
4151
|
`orphan-test`" is the other candidate and is a strictly worse `check` on a
|
|
3753
4152
|
multi-file registry, where the other files are fine and their orphans are real.
|
|
3754
|
-
**The wrong *advice*
|
|
3755
|
-
`[
|
|
3756
|
-
|
|
3757
|
-
|
|
3758
|
-
|
|
3759
|
-
|
|
3760
|
-
|
|
3761
|
-
|
|
3762
|
-
|
|
3763
|
-
|
|
3764
|
-
|
|
4153
|
+
**The wrong *advice* was separated out and fixed in `[0.7.0]`; the noise is
|
|
4154
|
+
fixed under `[Unreleased]`, and the blocker above was the wrong question.**
|
|
4155
|
+
The mapping this entry called for could not be recovered, and did not have to
|
|
4156
|
+
be: what a suppression needs is not the broken file's *prefix* but the ids it
|
|
4157
|
+
declares, and those are written in its source. Reading them back is a source
|
|
4158
|
+
read that returns ids and builds nothing, so the failure modes that matter —
|
|
4159
|
+
a truncated file, a missing default export, a non-literal value, a
|
|
4160
|
+
schema-invalid entry, a module that throws at import — all give them up. The
|
|
4161
|
+
six derived findings from one cause are now two ERRORs and one WARNING, and
|
|
4162
|
+
the collapse is per id rather than per prefix, which is strictly more precise
|
|
4163
|
+
than what was blocked: a genuine typo sharing a prefix with the broken file
|
|
4164
|
+
stays reported. **The lesson is the entry, not the fix.** It sat here for two
|
|
4165
|
+
releases behind a blocker stated as "the mapping is unavailable", which was
|
|
4166
|
+
true and was about a mapping nothing needed; the evidence it carried was about
|
|
4167
|
+
the right problem and the mechanism it proposed was not, and nothing rereads a
|
|
4168
|
+
blocker that has already been written down. That is the same failure the
|
|
4169
|
+
section's own preamble records about a figure nobody re-checks, one level up:
|
|
4170
|
+
the *shape* of the proposal goes stale as quietly as its numbers do.
|
|
4171
|
+
*What is left of this entry is only the original question* — whether `check`
|
|
4172
|
+
and `verify` should refuse a half-loaded registry outright, the way `cover`
|
|
4173
|
+
and `render` do — **and the evidence that motivated it has been spent.** The
|
|
4174
|
+
argument for refusing was never the load failure itself; it was the wall of
|
|
4175
|
+
derived findings burying it, and that wall is gone. What remains is the
|
|
4176
|
+
breadth argument recorded above, which points the other way for both commands.
|
|
4177
|
+
It stays here rather than moving down because "the reason to do it was
|
|
4178
|
+
removed" is not the same as "it was decided against", and the case would come
|
|
4179
|
+
back the moment another finding is found to scale with a broken registry
|
|
4180
|
+
rather than with the number of problems.
|
|
3765
4181
|
|
|
3766
4182
|
- **A statement that quantifies over a surface, with scenarios covering only the
|
|
3767
4183
|
part that existed when it was written.** Found by reading, and it had already
|
|
@@ -3810,7 +4226,7 @@ have to gather again.
|
|
|
3810
4226
|
agreed to maintain it.
|
|
3811
4227
|
|
|
3812
4228
|
- **Whether a requirement can ever be retired, and what it costs that none can.**
|
|
3813
|
-
The registry is at
|
|
4229
|
+
The registry is at **69 requirements and 192 scenarios, 2.78 each**, and
|
|
3814
4230
|
every one is a permanent obligation: `uncovered-requirement` is an ERROR, so a
|
|
3815
4231
|
requirement without a scenario cannot exist, and nothing ever removes one.
|
|
3816
4232
|
`delta.removed` exists and `applyDelta` applies it, but `--apply` refuses to
|
|
@@ -3832,45 +4248,29 @@ have to gather again.
|
|
|
3832
4248
|
requirement, where the scenario should move rather than go. Only the second is
|
|
3833
4249
|
really about cost, and it is the one where deleting the intent to save the
|
|
3834
4250
|
scenario inverts what the whole tool claims — that intent outlives its tests.
|
|
3835
|
-
Nothing here is urgent
|
|
3836
|
-
|
|
3837
|
-
|
|
3838
|
-
|
|
3839
|
-
|
|
3840
|
-
|
|
3841
|
-
|
|
3842
|
-
|
|
3843
|
-
|
|
3844
|
-
|
|
3845
|
-
|
|
3846
|
-
|
|
3847
|
-
|
|
3848
|
-
|
|
3849
|
-
|
|
3850
|
-
|
|
3851
|
-
|
|
3852
|
-
|
|
3853
|
-
|
|
3854
|
-
|
|
3855
|
-
|
|
3856
|
-
|
|
3857
|
-
|
|
3858
|
-
- **A loader that fails to start leaves its scratch directory behind.**
|
|
3859
|
-
`createLoader` calls `mkdtempSync` and writes the stub *before* `createServer`,
|
|
3860
|
-
and the only `rm` of that directory is inside the `close` of the object
|
|
3861
|
-
`createServer` returns — so a throw from it leaks one `attest-loader-*` per
|
|
3862
|
-
invocation, which on a CI runner is one per build. That is the exact failure
|
|
3863
|
-
shape the comment on `close` records as already fixed, surviving on the other
|
|
3864
|
-
path.
|
|
3865
|
-
*What blocks it is that nothing has been made to throw there.* With
|
|
3866
|
-
`configFile: false` Vite still reads the `package.json` at the root to decide
|
|
3867
|
-
`type`, so a malformed one in the checked project is the obvious candidate and
|
|
3868
|
-
it is the project's to write — but it was not tried, and an entry claiming a
|
|
3869
|
-
trigger it has not seen is the thing this section exists to not accumulate.
|
|
3870
|
-
The repair is three lines (a `try` around the two calls that `rm`s and
|
|
3871
|
-
rethrows) and is not what the decision turns on; measuring whether the throw
|
|
3872
|
-
is reachable at all is, because if it is not, this is a leak with no input
|
|
3873
|
-
that produces it.
|
|
4251
|
+
Nothing here is urgent at 69. It is filed now because the moment to write the
|
|
4252
|
+
argument down is while the reasoning is available, and because a project whose
|
|
4253
|
+
requirement count only rises should have decided in advance what would make
|
|
4254
|
+
one leave.
|
|
4255
|
+
*The figures above were restated on 2026-08-08, and how they were wrong is
|
|
4256
|
+
itself the section's own warning happening.* As filed on 2026-08-03 this entry
|
|
4257
|
+
said "62 requirements and 160 scenarios, roughly 2.6 each" while closing on
|
|
4258
|
+
"the number is 61" — an inconsistency inside one entry, and the closing half
|
|
4259
|
+
was the right one. Re-measured at the entry's own commit with the current
|
|
4260
|
+
parser: **61 requirements, 156 scenarios**, so both opening figures were wrong
|
|
4261
|
+
on the day they were written, and only the ratio survived. Measured the same
|
|
4262
|
+
way today it is 69 and 192, and the method is the same one `verify self`
|
|
4263
|
+
prints — the control is that today's `self/` counted this way returns exactly
|
|
4264
|
+
what the runner does, which is what makes the 2026-08-03 number trustworthy. The
|
|
4265
|
+
cheap way — `grep -c 'scenario('` — answers 211 against today's true 192, and
|
|
4266
|
+
is what would have been reached for: comments and prose count, which is the
|
|
4267
|
+
`ATX-15` error again.
|
|
4268
|
+
**What moved in five days sharpens the argument rather than dating it.** +8
|
|
4269
|
+
requirements and +36 scenarios, concentrated in three sessions rather than
|
|
4270
|
+
accrued at a rate, so nothing here is a trend line. The part worth keeping is
|
|
4271
|
+
the ratio: 2.56 → 2.78. The cost of a requirement is not flat and it is not
|
|
4272
|
+
falling, which is the premise the whole entry rests on, and it is the first
|
|
4273
|
+
time this file has had two measurements of it to compare.
|
|
3874
4274
|
|
|
3875
4275
|
## Considered and rejected
|
|
3876
4276
|
|
|
@@ -4112,6 +4512,46 @@ skill file. A pointer is cheap enough to be resident, it is their edit in their
|
|
|
4112
4512
|
file, and it cannot go stale — the file it names is the one `init` keeps
|
|
4113
4513
|
current.
|
|
4114
4514
|
|
|
4515
|
+
### A sweep driving every exported `format*` with hostile input
|
|
4516
|
+
|
|
4517
|
+
The mechanism `ATX-37` sat under `Under consideration` waiting for, kept there
|
|
4518
|
+
on cost, and killed by measuring that cost rather than by paying it. The three
|
|
4519
|
+
sites it was aimed at are closed under `[Unreleased]` by hand-written scenarios;
|
|
4520
|
+
what is rejected is the roster, and the argument is that **`src/cli/report.ts`'s
|
|
4521
|
+
exports are the wrong roster in both directions at once.**
|
|
4522
|
+
|
|
4523
|
+
*Too wide.* Three of the eight exports cannot carry project-derived text at all:
|
|
4524
|
+
`summarize` and `formatScope` take `VerifyCounts`, which is four numbers, and
|
|
4525
|
+
`formatRepeats` prints a count, a `Level` and an `IssueCode` — both fixed
|
|
4526
|
+
rosters. A hostile fixture for those has nothing hostile to put in it, so a loop
|
|
4527
|
+
over the exports either forces three members that **cannot go red**, which is
|
|
4528
|
+
the defect this project ships a gate against, or carries an exclusion list —
|
|
4529
|
+
and an exclusion list is precisely what `ATX-15`'s sweep avoided by making the
|
|
4530
|
+
roster one the code owns.
|
|
4531
|
+
|
|
4532
|
+
*Too narrow.* The statement is over everything the CLI writes to a terminal, and
|
|
4533
|
+
every site that has escaped it so far was outside this module: the `status`
|
|
4534
|
+
header was built and printed in `cli/index.ts`, the loader's writes are in
|
|
4535
|
+
`core/loader.ts`, and the `render` document that `ATX-58` was carved out for is
|
|
4536
|
+
in `core/render.ts`. A roster of formatters is neither necessary nor sufficient
|
|
4537
|
+
for the sentence, and the reading that made it look sufficient — that the
|
|
4538
|
+
formatters are the whole surface — is the exact assumption the `status` header
|
|
4539
|
+
falsified when it was found.
|
|
4540
|
+
|
|
4541
|
+
*And the general form is the §0 wall.* "Every project-derived string
|
|
4542
|
+
interpolated into a terminal line goes through `inline`" needs to know which
|
|
4543
|
+
values are project-derived, which is prose judgement over a data flow. That is
|
|
4544
|
+
the ground truth `tests/intent-rule-candidates.spec.ts` records as the reason
|
|
4545
|
+
none of this can ever be an engine rule, and it does not become computable by
|
|
4546
|
+
being asked about this repository instead of someone else's.
|
|
4547
|
+
|
|
4548
|
+
*What keeps the property instead* is the `QUANTIFIED` row itself — hand-judged,
|
|
4549
|
+
one entry per site, every named scenario asserted to exist. That is a
|
|
4550
|
+
counter-pressure rather than a detector, it holds for one registry whose authors
|
|
4551
|
+
agreed to maintain it, and after this change exactly one of its `ATX-37` sites
|
|
4552
|
+
is open. This reopens only if a site escapes that a roster of formatters would
|
|
4553
|
+
have caught, which none of the four so far would have been.
|
|
4554
|
+
|
|
4115
4555
|
### A Codex target for `attest init`
|
|
4116
4556
|
|
|
4117
4557
|
Evaluated when `init` grew `--target` and shipped for Claude Code, Cursor and
|
|
@@ -4196,6 +4636,8 @@ requirement, and human review at propose is still the whole answer. What is
|
|
|
4196
4636
|
rejected is grouping as a way to assist it, and this reopens only on a
|
|
4197
4637
|
contradiction that grouping would have caught.
|
|
4198
4638
|
|
|
4639
|
+
[0.7.4]: https://gitlab.com/Pseudorca/attest/-/tags/v0.7.4
|
|
4640
|
+
[0.7.3]: https://gitlab.com/Pseudorca/attest/-/tags/v0.7.3
|
|
4199
4641
|
[0.7.2]: https://gitlab.com/Pseudorca/attest/-/tags/v0.7.2
|
|
4200
4642
|
[0.7.1]: https://gitlab.com/Pseudorca/attest/-/tags/v0.7.1
|
|
4201
4643
|
[0.7.0]: https://gitlab.com/Pseudorca/attest/-/tags/v0.7.0
|