@am_shork/attest 0.7.0 → 0.7.2
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 +562 -112
- package/README.md +4 -4
- package/bin/attest.js +0 -0
- package/dist/cli/action.d.ts +48 -0
- package/dist/cli/action.js +100 -0
- package/dist/cli/index.js +11 -32
- package/dist/cli/report.js +9 -1
- package/dist/core/apply.js +7 -10
- package/dist/core/docs.d.ts +1 -1
- package/dist/core/docs.js +2 -0
- package/dist/core/locate.d.ts +9 -10
- package/dist/core/locate.js +58 -15
- package/dist/core/merge.js +27 -2
- package/dist/core/order.d.ts +17 -0
- package/dist/core/order.js +25 -0
- package/dist/core/pipeline.js +34 -14
- package/dist/core/render.js +90 -50
- package/dist/core/runner.js +4 -6
- package/dist/core/schema.d.ts +13 -6
- package/dist/core/schema.js +54 -18
- package/dist/core/splice.d.ts +13 -12
- package/dist/core/splice.js +59 -18
- package/dist/core/static-registry.js +6 -0
- package/dist/core/status.js +4 -9
- package/dist/core/terminal.d.ts +9 -23
- package/dist/core/terminal.js +9 -23
- package/dist/core/types.d.ts +1 -1
- package/dist/core/validator.d.ts +1 -0
- package/dist/core/validator.js +37 -0
- package/package.json +29 -17
package/CHANGELOG.md
CHANGED
|
@@ -13,6 +13,338 @@ 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.2] - 2026-08-07
|
|
17
|
+
|
|
18
|
+
### Added
|
|
19
|
+
|
|
20
|
+
- **Releases carry a provenance attestation, and the publish job proves it was
|
|
21
|
+
served rather than trusting its own exit code.** `npm publish --provenance`
|
|
22
|
+
exchanges a GitLab OIDC token for a Sigstore certificate and records, in the
|
|
23
|
+
public transparency log, that this tarball was built by this pipeline from
|
|
24
|
+
this commit — checkable by anyone with `npm audit signatures` or the package
|
|
25
|
+
page, without trusting us. Nothing about the package's *contents* changes:
|
|
26
|
+
same `files`, same tarball, same `prepack`/`prepublishOnly` gate.
|
|
27
|
+
*It is `npm` in a repository whose every other job is `pnpm`, and that is not
|
|
28
|
+
drift.* pnpm's publish has no provenance support at all — checked against the
|
|
29
|
+
pinned 10.28.0, there is no such flag — so the choice was npm for this one
|
|
30
|
+
command or no attestation. Two consequences worth writing down. `--no-git-checks`
|
|
31
|
+
disappears with it: that was a pnpm flag, needed only because pnpm inspects
|
|
32
|
+
the branch and CI builds tags in detached HEAD. And `COREPACK_ENABLE_STRICT=0`
|
|
33
|
+
is now required to call `npm` at all — `corepack enable` shims npm too, and
|
|
34
|
+
`packageManager` pins pnpm, so a bare `npm` aborts with "this project is
|
|
35
|
+
configured to use pnpm". Job-local rather than narrowing the shim in `.base`,
|
|
36
|
+
because every other job wants exactly that strictness.
|
|
37
|
+
*The check after the publish, and the one that looked right and was worthless.*
|
|
38
|
+
The obvious step is `npm audit signatures`, and here it would have been
|
|
39
|
+
decoration of precisely the kind the `audit` job's comment refuses: run in this
|
|
40
|
+
repository it audits *our own installed dependency tree*, which never contains
|
|
41
|
+
`@am_shork/attest` — green while attesting nothing. What runs instead reads the
|
|
42
|
+
registry's attestation endpoint for the version just published and fails unless
|
|
43
|
+
a `slsa.dev/provenance/` predicate is actually there. That is the consumer's
|
|
44
|
+
view, and it is what separates "npm accepted the tarball" from "the statement
|
|
45
|
+
is served". It retries for propagation, because it runs *after* an
|
|
46
|
+
irreversible step: the publish has happened, and a rerun cannot clear a red
|
|
47
|
+
since the republish fails `EPUBLISHCONFLICT` on a version that now exists — so
|
|
48
|
+
a registry that had not caught up would pin the release job red with nothing
|
|
49
|
+
wrong. A served response missing the predicate fails immediately; that answer
|
|
50
|
+
will not change. Both paths were run against the live registry — green on a
|
|
51
|
+
package known to carry provenance, red on `0.7.1`, which does not.
|
|
52
|
+
*Three preconditions, and npm rejects the publish rather than quietly shipping
|
|
53
|
+
unsigned if any is missing.* An `id_tokens` audience of exactly `sigstore`; a
|
|
54
|
+
`repository.url` matching `CI_PROJECT_URL`, so a repository rename fails here
|
|
55
|
+
first; and a public package on registry.npmjs.org, which
|
|
56
|
+
`publishConfig.access=public` already settles. The token is still an npm
|
|
57
|
+
automation token — the OIDC exchange signs the release, it does not authorise
|
|
58
|
+
it (see *Under consideration*).
|
|
59
|
+
*What is unmeasured, stated rather than implied.* There is no dry run for
|
|
60
|
+
provenance — `--dry-run` mints no token exchange — so none of this has executed
|
|
61
|
+
against a real tag pipeline. The next release is its first run. The commit SHA,
|
|
62
|
+
tag, pipeline URL and repository path become public record; all four are
|
|
63
|
+
already public here.
|
|
64
|
+
|
|
65
|
+
### Fixed
|
|
66
|
+
|
|
67
|
+
- **One unreadable file took the whole of `check` down with it.** A `params`
|
|
68
|
+
value nested 20,000 deep exhausts the call stack inside `ts.createSourceFile`
|
|
69
|
+
— TypeScript's own recursive-descent parser, so `literalValue`'s recursion is
|
|
70
|
+
never reached and hardening it would have changed nothing. Measured: `attest
|
|
71
|
+
check` answered a single `internal-error`, exit 1. It failed *closed*, which
|
|
72
|
+
is why this is a repair and not an emergency — but `check` is the command this
|
|
73
|
+
project tells people to run first, in a pipeline, on a merge request whose
|
|
74
|
+
contents they do not control, and one file an attacker owns could reduce the
|
|
75
|
+
entire report to that one line, permanently. Breadth is what `check` trades
|
|
76
|
+
for running no project code; a report of one crash is not a degraded `check`,
|
|
77
|
+
it is a different command.
|
|
78
|
+
Now a file that cannot be parsed is reported as **`unreadable-file`** and
|
|
79
|
+
skipped, and everything else in the run is still reported.
|
|
80
|
+
*Two sites, and a guard on one of them fixes nothing.* `static-registry.ts`
|
|
81
|
+
and `parser.ts` both open with the same call, so the spec parse takes it too —
|
|
82
|
+
measured separately, and a registry-only guard leaves `attest check` dying on
|
|
83
|
+
a hostile `*.spec.ts` exactly as before. The registry half sits at the loop
|
|
84
|
+
that reads the files rather than inside either reader, because what is being
|
|
85
|
+
kept is a property of the *run* — one file's failure is not the run's failure
|
|
86
|
+
— and both readers need it; `readRegistry` folds them with `Promise.all`,
|
|
87
|
+
where one rejection ends the command.
|
|
88
|
+
*Its own code, not `registry-invalid`.* A file that blew the parser's stack
|
|
89
|
+
did not fail schema validation, and a diagnostic saying it did sends the
|
|
90
|
+
reader to the wrong page. The archive gate refuses to run at all when a spec
|
|
91
|
+
is unreadable, rather than grading a change against a plan known to be short.
|
|
92
|
+
*A third read site turned up while reading the diff, and it was answering
|
|
93
|
+
wrongly rather than crashing.* An unreadable **proposed** spec declares no
|
|
94
|
+
scenarios, so the unclaimed check found it claimed by nothing and reported
|
|
95
|
+
`proposed-spec-unclaimed` — a true sentence about a file whose actual problem
|
|
96
|
+
is that it could not be read, and a fix hint pointing at work that must not be
|
|
97
|
+
done. That is the shape `[0.7.0]` fixed for `orphan-test` after a registry
|
|
98
|
+
fails to load, arriving again at a different site. It now reports
|
|
99
|
+
`unreadable-file` and stands down.
|
|
100
|
+
`parseSpecs` now returns its issues alongside the plan; `parseAllSpecFiles`
|
|
101
|
+
was deleted in passing, having had no callers.
|
|
102
|
+
**No `SCHEMA_VERSION` bump.** A new `code` value is additive to the `--json`
|
|
103
|
+
envelope, and a consumer that branches on codes it knows is unaffected — the
|
|
104
|
+
rule this project already applies to every new diagnostic.
|
|
105
|
+
|
|
106
|
+
- **`attest status` printed one line the sanitiser never saw.** The `Change:`
|
|
107
|
+
header was built and printed in the CLI shell, three lines above a closing
|
|
108
|
+
line that put the same value through `inline()` — so a change name holding
|
|
109
|
+
control characters repainted the terminal from the header while the footer was
|
|
110
|
+
clean. A change name is a directory name in the repository under test, and the
|
|
111
|
+
workflow `init` writes tells an agent to run `attest status <name>`.
|
|
112
|
+
The header moved into `formatStatus`, which is a fix rather than a tidy-up: a
|
|
113
|
+
write that lives in `cli/index.ts` cannot be attested at all, because that
|
|
114
|
+
module runs the CLI at import — so the site was not merely unsanitised, it was
|
|
115
|
+
unreachable by any scenario. `ATX-37`'s site list gained the kind that had
|
|
116
|
+
been missing from it, **a write in the shell**, and the two `--out` echoes are
|
|
117
|
+
the remainder: sanitised now, and still carried as unattested, because the
|
|
118
|
+
trigger needs a filename holding a control character — which NTFS refuses and
|
|
119
|
+
Linux permits, so that half is unmeasured here.
|
|
120
|
+
|
|
121
|
+
- **A param that is a scenario's *domain* could be shortened, and the run just
|
|
122
|
+
covered less.** Nothing in this repo executes here — it is the self-registry
|
|
123
|
+
and its measurement suite — but three of the holes were real, and one of them
|
|
124
|
+
was a security check. Found by re-measuring the tautology entry below after
|
|
125
|
+
`[0.7.1]`: twelve mutations of this registry, each followed by the whole gate.
|
|
126
|
+
Four went green, and three of those were **not** the tautology that experiment
|
|
127
|
+
was looking for. When a scenario loops over a list it read from `params`, that
|
|
128
|
+
list is the set of cases the run covers, so removing a member removes a case
|
|
129
|
+
and every assertion inside the loop still passes over what is left. The
|
|
130
|
+
expectation stays perfectly independent; what moved is the quantifier's
|
|
131
|
+
domain, which is why the rule shipped in `[0.5.0]` — *the expectation must not
|
|
132
|
+
be a function of the param the code under test consumed* — does not cover it.
|
|
133
|
+
What each one cost, measured: dropping `../../evil` from `ATX-13.rejectedNames`
|
|
134
|
+
stopped testing the change-name guard against path traversal; `ATX-40.languages: 1`
|
|
135
|
+
stopped checking `docs/zh/troubleshooting.md` **entirely**, which is the
|
|
136
|
+
mirror rule `CLAUDE.md` calls mandatory losing its only gate; and
|
|
137
|
+
`ATX-64.childRunCommands: ['verify']` stopped checking `archive` for the
|
|
138
|
+
stdout hole `ATX-64` exists to close — filed in the same release, three
|
|
139
|
+
scenarios deep, and silent.
|
|
140
|
+
*The repo had already invented the defence and not named it.* `ATX-16` asserts
|
|
141
|
+
its list against a literal and its comment says why — "so the spec cannot
|
|
142
|
+
quietly cover fewer of them than the intent promises" — and it was one of the
|
|
143
|
+
two lists that went red. A literal is the only independent term available when
|
|
144
|
+
what is at risk is the **size** of the set rather than a value in it, which is
|
|
145
|
+
also why this reads as a violation of "do not hardcode a value that lives in
|
|
146
|
+
`params`" and is not one: the pin asserts what the intent *is*, not what the
|
|
147
|
+
system does.
|
|
148
|
+
Every iterated list now carries that line. `ATX-40` needed both halves and
|
|
149
|
+
that is the instructive one: `toHaveLength(languages)` already caught the
|
|
150
|
+
count going *up* without a document, and could never catch it going down,
|
|
151
|
+
because the slice shrinks with it and the assertion stays true.
|
|
152
|
+
*The counter-pressure, and one thing it cannot reach.* A `DOMAINS` table in
|
|
153
|
+
`tests/intent-rule-candidates.spec.ts` now judges every list-valued param as
|
|
154
|
+
`iterated` or `inert`, with completeness swept from the registry so a new list
|
|
155
|
+
fails until someone decides, and each `iterated` row naming the pin that
|
|
156
|
+
guards it. The `QUANTIFIED` taxonomy gained the distinction this turned on:
|
|
157
|
+
a `roster` owned by the **code** grows with the code and is safe by
|
|
158
|
+
construction, a roster owned by the **registry** is data an author can shorten
|
|
159
|
+
— `ATX-64` was filed as the former and was the latter. What the sweep does not
|
|
160
|
+
reach is `ATX-40`, whose domain size is a *scalar*; it is judged by hand, and
|
|
161
|
+
a future scalar used that way would not be forced into the table by anything.
|
|
162
|
+
*The rot check needed a second attempt, which is worth recording.* Written
|
|
163
|
+
first as "the pin appears somewhere in `self/`", it passed with `ATX-64`'s pin
|
|
164
|
+
deleted — two requirements pin a list read into a local named `commands`, and
|
|
165
|
+
the grep matched the other one. It asserts **exactly one** match now. A check
|
|
166
|
+
satisfiable by an unrelated line is the failure it exists to prevent.
|
|
167
|
+
|
|
168
|
+
- **A project under test could put its own verdict on stdout ahead of Attest's,
|
|
169
|
+
under `--json`.** `runAndCollect` starts Vitest with the parent's fd 1 in
|
|
170
|
+
play, and `quiet: true` does not close it: `silent: true` suppresses Vitest's
|
|
171
|
+
*console interception*, and a bare `process.stdout.write` in a spec never
|
|
172
|
+
enters it — measured at collection time and inside a scenario body, both. So
|
|
173
|
+
a spec writing `{"ok":true,…}` put that document **first** on the stream: a
|
|
174
|
+
consumer calling `JSON.parse` on the whole of stdout got a parse error at
|
|
175
|
+
position 46, and one reading line-wise or taking the first object got the
|
|
176
|
+
attacker's verdict instead of the report. `check`, `cover`, `render` and
|
|
177
|
+
`status` were never exposed — no Attest code was on the path at all, which is
|
|
178
|
+
precisely why `core/terminal.ts` and `sanitisedLogger` did not help.
|
|
179
|
+
*Why this is worse than the terminal half of the same hole.* Repainting a
|
|
180
|
+
terminal — `ESC[2K CR`, a forged `ERROR` header at column 0, an `OSC 0` window
|
|
181
|
+
retitle, all of which also arrive intact — is not a capability `verify`
|
|
182
|
+
lacked, because it executes project code by design. Forging the *machine*
|
|
183
|
+
verdict is different in kind: it inverts what the report is for, since the
|
|
184
|
+
commands carrying the risk are exactly the ones that run the repository being
|
|
185
|
+
gated. §9.1 recorded the machine surface as never exposed because
|
|
186
|
+
`JSON.stringify` escapes every C0 character — true, and about the bytes Attest
|
|
187
|
+
writes; it says nothing about bytes Attest does not write.
|
|
188
|
+
Under `--json`, stdout is now diverted to stderr for the duration of the
|
|
189
|
+
command and released before the report is rendered. Diverted rather than
|
|
190
|
+
dropped: a human debugging a red pipeline still needs the run output, and
|
|
191
|
+
stderr is the stream no consumer parses. Without `--json` nothing changes —
|
|
192
|
+
the promise is the flag's, and diverting unconditionally would take the child
|
|
193
|
+
run's output away from the reader it exists for.
|
|
194
|
+
*The fix is `runAction`'s, not `runner.ts`'s* — the same argument that put
|
|
195
|
+
`formatCrash` in `cli/report.ts` rather than a copy in each `catch`.
|
|
196
|
+
`runAction` is already the single convergence point for one output and one
|
|
197
|
+
exit code; `runner.ts` would have been made to hold a contract it does not
|
|
198
|
+
own. It moved to **`src/cli/action.ts`** to be testable at all: `cli/index.ts`
|
|
199
|
+
calls `program.parseAsync()` at import time, so importing it to cross the seam
|
|
200
|
+
would run the CLI. No behaviour of `index.ts` moved with it.
|
|
201
|
+
*What the regression test had to be, because the first one was wrong.* The
|
|
202
|
+
scenarios under **`ATX-64`** spawn the real CLI and read its pipes. The
|
|
203
|
+
in-process version looks equivalent and is not: they run inside a Vitest
|
|
204
|
+
worker, which patches `console` and re-routes worker output, so intercepting
|
|
205
|
+
`process.stdout.write` there observed neither the report nor the pollution and
|
|
206
|
+
went green against the unfixed code for three of three scenarios. What the
|
|
207
|
+
requirement is about is the bytes on the pipe, and only a real pipe has them.
|
|
208
|
+
The statement names its commands in `params` rather than quantifying over
|
|
209
|
+
"every command that executes project code" — a `roster` row rather than a
|
|
210
|
+
`sites` one, so a third such command cannot join without joining the
|
|
211
|
+
assertion.
|
|
212
|
+
|
|
213
|
+
## [0.7.1] - 2026-08-05
|
|
214
|
+
|
|
215
|
+
### Added
|
|
216
|
+
|
|
217
|
+
- **`params` takes any JSON value, and the constraint moved to the place it was
|
|
218
|
+
actually protecting.** A param was a scalar or a list of scalars. The argument
|
|
219
|
+
for admitting lists — the values most likely to drift are the composite ones,
|
|
220
|
+
and rejecting them left exactly those outside the one mechanism built to pin
|
|
221
|
+
them — does not stop at a list: a `kind -> payload` table drifts harder than a
|
|
222
|
+
flat list of names, and it was the one shape still excluded. `null` comes with
|
|
223
|
+
it, because refusing it forces a sentinel (`''`, `-1`) that means something
|
|
224
|
+
else to every later reader.
|
|
225
|
+
What the old union was defending was not the registry but the **rendering**:
|
|
226
|
+
`{payloadKinds}` interpolated into a statement as `[object Object]`, in a
|
|
227
|
+
document reviewers and audit read as the system's promise. That is a property
|
|
228
|
+
of the interpolation point, so it is stated there — a new
|
|
229
|
+
**`non-scalar-interpolation`** ERROR from `check` and `verify`. Narrower and
|
|
230
|
+
truer: it refuses what actually breaks and admits the data a scenario reads,
|
|
231
|
+
which the schema rule could not tell apart. What the schema keeps is what does
|
|
232
|
+
not survive being written down — a function, a `Date`, a class instance, an
|
|
233
|
+
object whose prototype was swapped.
|
|
234
|
+
*The part that is not a schema edit.* Depth. Four obligations had exactly one
|
|
235
|
+
level of reach, which is correct only while a param cannot nest, and none of
|
|
236
|
+
them fails loudly: the canonical form behind `add-conflict` (an identical copy
|
|
237
|
+
written with its inner keys in another order would have conflicted with
|
|
238
|
+
itself), the ATX-58 control-character stripping (nested strings would have
|
|
239
|
+
gone unsanitised, reopening the hole `[0.7.0]` closed), the key order behind a
|
|
240
|
+
byte-stable rendering, and `splice.ts`, which writes a requirement back out as
|
|
241
|
+
TypeScript source and would have committed `[object Object]` into a
|
|
242
|
+
`*.reqs.ts` through `archive --apply`. Three of the four are ordering, and are
|
|
243
|
+
now one shared `sortDeep` in `order.ts` rather than three sorts a level deep —
|
|
244
|
+
they wanted the same guarantee for the same reason, which is what makes it one
|
|
245
|
+
function and not a helper each.
|
|
246
|
+
*Two divergences between the readers, which is what the differential suite is
|
|
247
|
+
for.* `null` is a keyword rather than a literal node, so the AST reader fell
|
|
248
|
+
off the end of `literalValue` and reported `registry-not-static` for the whole
|
|
249
|
+
file while the evaluating reader accepted it. And `{ __proto__: … }` swaps the
|
|
250
|
+
prototype instead of creating a key, so `z.record` copies own keys into a fresh
|
|
251
|
+
object and the taint is invisible one step later — the static reader refuses
|
|
252
|
+
that source at any depth, and the schema now refuses the object the evaluator
|
|
253
|
+
builds from it, both spellings (a literal `__proto__:` and the own key
|
|
254
|
+
`JSON.parse` produces).
|
|
255
|
+
*Rendering.* A structured param gets a fenced JSON block below the params
|
|
256
|
+
table, keys sorted at every depth (ATX-10: the same registry renders the same
|
|
257
|
+
bytes) with the fence measured against backtick runs in the value, as `code()`
|
|
258
|
+
already did. It is below the table rather than in it because a fence cannot
|
|
259
|
+
live in a table cell. And `render` defends the document on its own: it reads
|
|
260
|
+
the registry with no spec parse, so no `AttestPlan` and no `validateStructure`
|
|
261
|
+
— `attest render` runs to completion on a registry `check` refuses. Leaning on
|
|
262
|
+
the diagnostic would have left the document defended by a command nobody is
|
|
263
|
+
obliged to run first.
|
|
264
|
+
*The writing side of the `__proto__` rule.* `splice.ts` turns a requirement
|
|
265
|
+
back into TypeScript source for `--apply`, and it is the only site that does.
|
|
266
|
+
Extending it to depth meant it would have emitted a `__proto__` key as a bare
|
|
267
|
+
property — text whose *evaluation* swaps a prototype rather than being the
|
|
268
|
+
value it was handed. Unreachable through any command (the schema rejects a
|
|
269
|
+
nested `__proto__` and `z.record` drops a top-level one), and refused anyway
|
|
270
|
+
with a new `UnwritableValue`, reported as `internal-error` by the merge with
|
|
271
|
+
its account of what it had already written intact. Quoting is not the repair —
|
|
272
|
+
a quoted `__proto__` swaps the prototype in a literal exactly as the bare form
|
|
273
|
+
does, and the bracketed form that would create an own property is a computed
|
|
274
|
+
key no static reader will read. The reader's guard is worth nothing if the
|
|
275
|
+
writer can produce the file it exists to refuse.
|
|
276
|
+
*Not breaking, with one edge worth naming.* No previously-green registry goes
|
|
277
|
+
red — a structured param was `registry-invalid` before, so nothing that
|
|
278
|
+
compiled and passed now fails — and an all-scalar registry renders
|
|
279
|
+
byte-identical, so a committed `render --check` stays green across the
|
|
280
|
+
upgrade. What did widen is the exported type in a **read** position:
|
|
281
|
+
`Requirement['params'][string]` is now the recursive JSON value. The const
|
|
282
|
+
generic is untouched, so `reqs['AUTH-3'].params.idleTimeoutMin` is still the
|
|
283
|
+
literal `30` and every anti-drift test keeps its narrow type. A consumer only
|
|
284
|
+
notices if they assign an *undeclared* key to the old scalar-or-list union
|
|
285
|
+
(`Type 'null' is not assignable`), or wrote their own handler over that type
|
|
286
|
+
and does `String(v)` in it — which is the `[object Object]` defect this entry
|
|
287
|
+
fixes in `render` and `splice`, seen from outside.
|
|
288
|
+
*This overturns a standing rejection, and not on its stated terms.* *Nested
|
|
289
|
+
objects in `params`* sat under **Considered and rejected** since `[0.4.2]`,
|
|
290
|
+
raised by `mine-capablanca`, and that entry is removed by this release. Its
|
|
291
|
+
argument was that a param has two properties a table lacks: rendered as a value
|
|
292
|
+
a human checks at a glance, and read by a scenario as the one place the value
|
|
293
|
+
lives. The first is answered rather than waived — `non-scalar-interpolation`
|
|
294
|
+
keeps a table out of the *sentence*, so what a reader takes in at a glance is
|
|
295
|
+
still glance-sized, and the table is rendered below it as a block. The second
|
|
296
|
+
is answered by the criterion that admitted arrays in the first place: a
|
|
297
|
+
`kind -> payload` table drifts *harder* than a flat list, so the reason arrays
|
|
298
|
+
were let in is the reason this shape should have been.
|
|
299
|
+
**The reopening condition it named was never met, and that is worth stating
|
|
300
|
+
plainly.** The entry said it reopens on "a report where the value is genuinely
|
|
301
|
+
promised — a table an adopter owes their users"; none has been seen. This
|
|
302
|
+
shipped on the strength of the argument alone. What changed is the recognition
|
|
303
|
+
that the schema was answering the wrong question: *params for promises, plain
|
|
304
|
+
constants for tuning* is the rule, and a type can only ever check shape, so it
|
|
305
|
+
refused a promised table while admitting a list of tuning knobs. Design §11
|
|
306
|
+
now carries that as a judgement rather than a type, which is the permanent half
|
|
307
|
+
— and it is weaker than what it replaces, because nothing enforces a judgement.
|
|
308
|
+
New requirements: `ATX-7` rewritten, `ATX-63` added.
|
|
309
|
+
|
|
310
|
+
### Changed
|
|
311
|
+
|
|
312
|
+
- **Rationale that only a comment held now has a design section, and the comment
|
|
313
|
+
cites it.** Measured over the diffs in three eras split at `[0.3.0]` and
|
|
314
|
+
`[0.5.0]`: comment blocks got better by every specificity signal — naming a
|
|
315
|
+
symbol 50 → 60%, a measured number 3 → 13%, a causal argument 26 → 54% — and
|
|
316
|
+
roughly doubled per line of code, while the share citing a `§` went 15 → 12 →
|
|
317
|
+
11% and the design document stopped growing (sixteen sections since `[0.3.0]`,
|
|
318
|
+
105 lines against `src/`'s 2,862). **The anchoring did not fall; it never
|
|
319
|
+
scaled**, and a `§` is the only part of a comment any gate reads.
|
|
320
|
+
Nineteen blocks written since `[0.5.0]` carried the cost — ten lines or more,
|
|
321
|
+
fully argued, unanchored — and they split three ways. *Six were properties a
|
|
322
|
+
rewrite would have to preserve*, and now sit in the document: a new **§9.1**
|
|
323
|
+
for the artifacts Attest writes, an extension to **§8** (`--apply` acts on this
|
|
324
|
+
run's verdict), and one to **§5.4** (the two readers of one plan must descend
|
|
325
|
+
the same way). *Four were history* — a quadratic backtrack, a 27–84x constant
|
|
326
|
+
factor, a 47 MiB peak, an indexing cost filed as invisible — and were
|
|
327
|
+
compressed to the invariant a maintainer would otherwise re-break; every
|
|
328
|
+
measurement in them was already under `[0.7.0]`, so this deleted duplicates
|
|
329
|
+
rather than moving anything. *Ten were correct as they stood and were left
|
|
330
|
+
alone*, which is the half worth naming: `§` is low because the design document
|
|
331
|
+
stalled, not because comments are too long, so a pass that shortened comments
|
|
332
|
+
and left it at one in nine would have moved the wrong quantity and called it
|
|
333
|
+
done. Net `src/`: 29 lines removed, all of them comments and no code touched,
|
|
334
|
+
which is also how the deletion was checked.
|
|
335
|
+
**One of the six corrected the document rather than extending it.** §8
|
|
336
|
+
summarised a merge as renaming a proposed spec "in place", which is true of its
|
|
337
|
+
location and not of its imports — the delta it reads its proposed params from
|
|
338
|
+
is what moves to `archive/`, so the sentence hid the one specifier the merge
|
|
339
|
+
has to repoint.
|
|
340
|
+
*What the measurement cost.* A first pass reported the `§` share as halving,
|
|
341
|
+
25 → 12%, an artefact of bucketing by `git blame` over the current tree: that
|
|
342
|
+
sees only surviving lines, and `§`-citing comments survive better, being tied
|
|
343
|
+
to sections that do not move, so the early era was measured on its most durable
|
|
344
|
+
residue. Re-measuring from the diffs before building is what caught it.
|
|
345
|
+
The routing test this produced is in `pre-commit-check` §6, and the obligation
|
|
346
|
+
a new diagnostic code owes the CLI reference is now in §2.
|
|
347
|
+
|
|
16
348
|
## [0.7.0] - 2026-08-04
|
|
17
349
|
|
|
18
350
|
### Added
|
|
@@ -3022,82 +3354,46 @@ feedback reports are triaged into issues or `Unreleased` and deliberately not
|
|
|
3022
3354
|
kept in this tree; that rule stands, and holds for anything with a reporter, a
|
|
3023
3355
|
repro, or a state to track. What is kept here is the narrower thing this file
|
|
3024
3356
|
already keeps one section of: a **decision with an argument attached**, where
|
|
3025
|
-
losing the argument would cost more than losing the task.
|
|
3026
|
-
|
|
3027
|
-
|
|
3028
|
-
|
|
3029
|
-
|
|
3030
|
-
|
|
3031
|
-
|
|
3032
|
-
|
|
3033
|
-
|
|
3034
|
-
|
|
3035
|
-
|
|
3036
|
-
|
|
3037
|
-
|
|
3038
|
-
|
|
3039
|
-
|
|
3040
|
-
|
|
3041
|
-
|
|
3042
|
-
|
|
3043
|
-
|
|
3044
|
-
|
|
3045
|
-
|
|
3046
|
-
|
|
3047
|
-
|
|
3048
|
-
|
|
3049
|
-
|
|
3050
|
-
|
|
3051
|
-
|
|
3052
|
-
|
|
3053
|
-
|
|
3054
|
-
|
|
3055
|
-
|
|
3056
|
-
|
|
3057
|
-
|
|
3058
|
-
|
|
3059
|
-
|
|
3060
|
-
|
|
3061
|
-
|
|
3062
|
-
|
|
3063
|
-
|
|
3064
|
-
|
|
3065
|
-
one with no external input at all: a strict read of `src/` with nothing failing.
|
|
3066
|
-
Every gate in this repository was green while all three were true, which is the
|
|
3067
|
-
whole of what that way is for — a defect no test is shaped to ask about is
|
|
3068
|
-
reachable only by someone reading for it. **All three have now shipped under
|
|
3069
|
-
`[0.7.0]`**, and each closed having found something a strict read is
|
|
3070
|
-
apparently good for and a gate is not. The runner one: the gate that would have
|
|
3071
|
-
had to catch it *already* looked for the shape, so what the read bought was not a
|
|
3072
|
-
missing gate but a cost nobody had priced. The load-failure diagnosis: every gate
|
|
3073
|
-
was green because the report was *complete* — three ERRORs on a red run — and
|
|
3074
|
-
what was wrong was which three, a judgement no assertion in this repository is
|
|
3075
|
-
shaped to make. The third, eight items under one heading: six comments naming a
|
|
3076
|
-
property the code lacked and two costs beside them.
|
|
3077
|
-
|
|
3078
|
-
**One entry has now left this section the other way — by being rejected** — and
|
|
3079
|
-
it is the first, which is why it is worth a line. The region-marked samples were
|
|
3080
|
-
decided *yes*, and building the count they rested on is what killed them: the two
|
|
3081
|
-
spans the entry named are not spans, and both carry a mark the entry's own rule
|
|
3082
|
-
excludes. It moved down with that measurement attached. The lesson is not about
|
|
3083
|
-
regions. **An entry can sit in `Planned` for releases on the strength of a figure
|
|
3084
|
-
nobody has checked**, because the section's discipline is aimed at keeping the
|
|
3085
|
-
*argument* rather than at re-testing the evidence under it — so the first act of
|
|
3086
|
-
building one should be re-measuring what it claims, not implementing what it
|
|
3087
|
-
proposes. That is what happened here, and it cost an hour instead of a mechanism.
|
|
3088
|
-
|
|
3089
|
-
*What the fifth way cost and returned, now that all of it is spent.* Sixteen
|
|
3090
|
-
items, no external input, and nothing in the suite shaped to catch any of them.
|
|
3091
|
-
The returns were not evenly distributed and the split is the useful part: two
|
|
3092
|
-
defects a user could hit today (a report that misdiagnosed a load failure, a
|
|
3093
|
-
merge crossing midnight), two costs worth a second each on a real command
|
|
3094
|
-
(`check --eval` at 1 + N Vite servers, `control` at 27–84x), one fixed toll on
|
|
3095
|
-
every static invocation (~1 s), and the rest true-but-unobservable — identical
|
|
3096
|
-
output either way. Only the last group is genuinely optional, and it is also the
|
|
3097
|
-
group that would never have been found any other way, because *no observable
|
|
3098
|
-
symptom* is precisely the definition of what a gate cannot see. The transferable
|
|
3099
|
-
lesson is the batching: individually none of the unobservable ones justifies a
|
|
3100
|
-
commit, and together they cost about an hour.
|
|
3357
|
+
losing the argument would cost more than losing the task.
|
|
3358
|
+
|
|
3359
|
+
**Where an entry went is recorded under the version that shipped it.** This
|
|
3360
|
+
section keeps no ledger of its own departures — one was maintained here by hand
|
|
3361
|
+
for several releases, and a count kept in prose with nothing to check it is a
|
|
3362
|
+
claim that goes wrong quietly. What is worth keeping is what the section learned
|
|
3363
|
+
about itself, which is three things.
|
|
3364
|
+
|
|
3365
|
+
**An entry can sit here for releases on the strength of a figure nobody has
|
|
3366
|
+
checked.** The discipline aims at keeping the *argument*, not at re-testing the
|
|
3367
|
+
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 twice: once
|
|
3369
|
+
killing an entry outright, when the spans it rested on turned out not to be
|
|
3370
|
+
spans; once correcting an entry's headline number before a line of it was
|
|
3371
|
+
written. **What a release costs its adopters is not known on the day it is cut**
|
|
3372
|
+
— several entries arrived from one adoption report, about shapes the tool made
|
|
3373
|
+
someone work around without ever erroring. And **the fixes with no observable
|
|
3374
|
+
symptom are worth batching**: individually none justifies a commit, together they
|
|
3375
|
+
cost about an hour, and they are the group no gate could have found, since having
|
|
3376
|
+
no symptom is the definition of what a gate cannot see.
|
|
3377
|
+
|
|
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.
|
|
3101
3397
|
|
|
3102
3398
|
- **Binding the `✗` samples to fixtures whose rejection is asserted.** A sample
|
|
3103
3399
|
showing input the engine must *reject* can be bound the way a runnable one is,
|
|
@@ -3117,6 +3413,43 @@ commit, and together they cost about an hour.
|
|
|
3117
3413
|
`[0.4.3]` — that the gate and the run proving the fixture still works live in
|
|
3118
3414
|
different suites — applies here identically.
|
|
3119
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
|
+
|
|
3120
3453
|
## Under consideration
|
|
3121
3454
|
|
|
3122
3455
|
Candidates with **no decision yet**, between the two sections either side of it
|
|
@@ -3129,6 +3462,25 @@ deletion. What each entry has to carry is the evidence currently blocking the
|
|
|
3129
3462
|
decision, because that is what a re-proposal a year from now would otherwise
|
|
3130
3463
|
have to gather again.
|
|
3131
3464
|
|
|
3465
|
+
- **Trusted publishing, which would delete `NPM_TOKEN` rather than add to it.**
|
|
3466
|
+
The provenance work in `[Unreleased]` signs the release but does not change
|
|
3467
|
+
what authorises it: a long-lived automation token, masked and protected, that
|
|
3468
|
+
publishes as us to anyone holding it. npm's trusted publishing exchanges the
|
|
3469
|
+
same OIDC identity the attestation already uses for a short-lived credential,
|
|
3470
|
+
which would remove the stored secret entirely and grant provenance as a
|
|
3471
|
+
property of the flow rather than a flag on the command. The deletion test
|
|
3472
|
+
points the right way for once: what it removes is the only long-lived
|
|
3473
|
+
credential this repository has.
|
|
3474
|
+
*What blocks the decision, and it is evidence rather than principle.* Whether
|
|
3475
|
+
npm's trusted publishers accept a **gitlab.com-hosted** project on the plan
|
|
3476
|
+
this package publishes under has not been verified against this project — it
|
|
3477
|
+
was not checked when the provenance job was written, and an entry claiming it
|
|
3478
|
+
works would be exactly the unverified confidence the rest of this file avoids.
|
|
3479
|
+
It also cannot be tested except by publishing: as with provenance, there is no
|
|
3480
|
+
dry run, so a failed experiment is a release. The cheap next step is reading
|
|
3481
|
+
npm's current trusted-publisher documentation for GitLab support before
|
|
3482
|
+
writing any YAML.
|
|
3483
|
+
|
|
3132
3484
|
- **A static sweep for the two properties claimed over the whole of `src/`.**
|
|
3133
3485
|
The quantifier table's first strict read left three rows wanting the same
|
|
3134
3486
|
thing, and the repo already owns the idiom: `tests/import-boundary.spec.ts` is
|
|
@@ -3267,6 +3619,103 @@ have to gather again.
|
|
|
3267
3619
|
assertion is still green, still silent, and still reachable — so this stays
|
|
3268
3620
|
here rather than moving to `Considered and rejected`: documenting a trap is
|
|
3269
3621
|
not the same as deciding not to detect it.
|
|
3622
|
+
**Re-measured after `[0.7.1]` widened `params`, on this registry rather than
|
|
3623
|
+
the reporter's — the first time this experiment has been run here.** Twelve
|
|
3624
|
+
mutations, each followed by the whole gate. Two results matter, and they point
|
|
3625
|
+
opposite ways.
|
|
3626
|
+
*The shape is now reachable here, and the widening is why.* `self/render.spec.ts`
|
|
3627
|
+
asserts `expect(md).toContain(JSON.stringify(kinds))` over the same
|
|
3628
|
+
`payloadKinds` it feeds to the renderer — the expectation is a function of the
|
|
3629
|
+
value under test, verbatim row 3, and it could not have been written before
|
|
3630
|
+
the widening because the param could not be an object. The mechanism is
|
|
3631
|
+
sharper than a frequency count and is the strongest thing this entry has
|
|
3632
|
+
gained: **the prescribed remedy costs in proportion to the value's
|
|
3633
|
+
complexity.** Same repo, adjacent files, same value shape — `ATX-7` pins two
|
|
3634
|
+
numbers with literals and goes red; `ATX-63` would have had to hand-write a
|
|
3635
|
+
rendered JSON block, so it echoed. Depth raises the defect rate and the cost
|
|
3636
|
+
of avoiding it along one axis.
|
|
3637
|
+
*And its cost here is nil, which is the honest other half.* Both composite
|
|
3638
|
+
params are fixtures for the renderer and the validator; the numbers stand for
|
|
3639
|
+
nothing, so the green is arguably correct. Blocker **(1)** is therefore only
|
|
3640
|
+
half retired — the shape is reproduced in a second, mature repo, and the count
|
|
3641
|
+
of *harmful* instances is still zero. Blocker **(2)** is reinforced rather than
|
|
3642
|
+
weakened: all four green mutations were on params `src/` never reads, so the
|
|
3643
|
+
reporter's "read from both a spec and a non-spec file" detector would have
|
|
3644
|
+
flagged none of them. Blocker **(3)** is untouched.
|
|
3645
|
+
*The frequency is no longer unmeasured, and it points away from building
|
|
3646
|
+
anything.* Blocker **(1)** rested on 8 commits of one greenfield repo. Parsed
|
|
3647
|
+
at every one of the **38** revisions that have touched this registry — all 38
|
|
3648
|
+
readable by the current static reader, so nothing was skipped — the history is
|
|
3649
|
+
**57 param keys added, 0 removed, and exactly 1 value changed**: `ATX-16.staticCommands`
|
|
3650
|
+
gaining `status`. That single edit is a roster *widening*, the safe direction,
|
|
3651
|
+
and it is on the one list that was already pinned, so it could not have been
|
|
3652
|
+
made without updating the pin. Two independent codebases, one greenfield and
|
|
3653
|
+
one at 64 requirements, and the event this whole entry is about has happened
|
|
3654
|
+
zero times in either. What that argues is not that the hole is unreal — it is
|
|
3655
|
+
reachable, demonstrated above — but that it is **rare and silent**, which is
|
|
3656
|
+
the profile `divergent-param` was rejected on rather than the profile that
|
|
3657
|
+
justifies an engine rule.
|
|
3658
|
+
*The same number says the split-out fix below has also never been triggered.*
|
|
3659
|
+
0 removals ever means no domain has ever been shortened here. Those pins close
|
|
3660
|
+
a live *exposure*, not a live *loss*, and the case for them is that they cost
|
|
3661
|
+
one line each against a failure with no symptom — not that anything was
|
|
3662
|
+
currently broken.
|
|
3663
|
+
*Blocker **(3)** assumed mutation testing was the only separator. A cheaper
|
|
3664
|
+
candidate existed, has now been scored, and does not survive it.* The idea was
|
|
3665
|
+
a **syntactic** discriminator over one test body — flag an assertion whose
|
|
3666
|
+
expected value derives from a param that also reached the arrange/act part —
|
|
3667
|
+
which is exactly the difference between the two cases the widening produced:
|
|
3668
|
+
`renderMarkdown({params: {payloadKinds: kinds}})` … `toContain(JSON.stringify(kinds))`
|
|
3669
|
+
against `expect(kinds.json.maxBytes).toBe(1_048_576)`. Scored the way
|
|
3670
|
+
`divergent-param` should have been and was not: implemented in
|
|
3671
|
+
`tests/intent-rule-candidates.spec.ts`, run over the specs that attest this
|
|
3672
|
+
repo, and then **every param it fires on mutated with the whole gate run —
|
|
3673
|
+
fifteen of them, one gate run each. Four went green; eleven went red.**
|
|
3674
|
+
*Then the detector was found to be wrong, which is the more useful half.* Its
|
|
3675
|
+
first implementation counted an identifier in any position as a read of a
|
|
3676
|
+
local with that name, so `i.code` in `issues.map((i) => i.code)` looked like a
|
|
3677
|
+
use of a param named `code` — and `code` is this repo's commonest param name.
|
|
3678
|
+
Corrected to count reference positions only, it flags **8, not 15**, and the
|
|
3679
|
+
seven it drops are exactly seven the mutations had already shown to be pinned
|
|
3680
|
+
by `src/`. The honest score is therefore **4 of 8, not 4 of 15** — precision
|
|
3681
|
+
50%, not 27%. The conclusion does not move, because it never rested on the
|
|
3682
|
+
precision figure; what moves is how much of the original number was the
|
|
3683
|
+
candidate failing and how much was the measurement being wrong, and that is
|
|
3684
|
+
worth writing down rather than quietly restating.
|
|
3685
|
+
The remaining false positives are not near-misses. They are independent terms
|
|
3686
|
+
the detector cannot see: **the runtime is the term** — `ATX-14`'s
|
|
3687
|
+
`inheritedKey` is `'toString'`, and a mutated spelling is not a prototype key
|
|
3688
|
+
at all, so the behaviour under test changes; **`src/` is the term** —
|
|
3689
|
+
`ATX-51`'s `code` is a second copy of a string the engine emits as a literal;
|
|
3690
|
+
and **arithmetic is the term** — `threshold + 3` builds a derived expectation
|
|
3691
|
+
that mutation breaks.
|
|
3692
|
+
And the four true positives fail the second test, which is the one that
|
|
3693
|
+
decides it: **none is harmful.** `ATX-10` and `ATX-3` assert that a value was
|
|
3694
|
+
substituted or applied, where the identity of the number is not the
|
|
3695
|
+
obligation; `ATX-51`'s is a path prefix and `ATX-63`'s a rendering fixture.
|
|
3696
|
+
Telling those from a mine-density band that must not move is a judgement about
|
|
3697
|
+
whether the value *means* anything — the ground truth §0 refuses to compute
|
|
3698
|
+
for someone else's registry. **50% precision on the shape, 0% on the harm.**
|
|
3699
|
+
Two limits on that number, stated because a score with an unstated method is
|
|
3700
|
+
what this file exists to prevent. The mutation has **one direction per kind**
|
|
3701
|
+
— numbers +1, lists drop-last, strings suffixed — and direction can decide the
|
|
3702
|
+
verdict: `ATX-40.languages` is red at 2→3 and was green at 2→1 before it was
|
|
3703
|
+
pinned, so the true-positive count is a floor. And **recall is unmeasured**:
|
|
3704
|
+
the eight params mutated earlier that this detector does not flag all went
|
|
3705
|
+
red, which is eight correct negatives and not a recall figure.
|
|
3706
|
+
The detector and its verdict table are kept in the measurement suite, with the
|
|
3707
|
+
reach recomputed on every run and the table asserted to cover exactly what
|
|
3708
|
+
fires — because the reach figure quoted for `compound-requirement` went stale
|
|
3709
|
+
in this very file for want of that.
|
|
3710
|
+
*A scoping limit on all of the above.* This repo cannot settle blocker **(2)**.
|
|
3711
|
+
`src/` reads no param at all — every `reqs[…].params` in it is a comment or a
|
|
3712
|
+
doc string — so params here are test vocabulary, while in the reporter's repo
|
|
3713
|
+
they are application constants the app itself consumes. The reporter's
|
|
3714
|
+
detector keys on exactly that difference, so it is inert here and this corpus
|
|
3715
|
+
is evidence neither for nor against it.
|
|
3716
|
+
*What the re-measurement did find is a different defect, and it has been split
|
|
3717
|
+
out below* — the entry is kept whole here because the two were measured in one
|
|
3718
|
+
pass and separating them is the finding.
|
|
3270
3719
|
|
|
3271
3720
|
- **Whether `check` and `verify` should refuse a half-loaded registry the way
|
|
3272
3721
|
`cover` and `render` do.** Those two return early on `hasError(loadIssues)`,
|
|
@@ -3388,6 +3837,41 @@ have to gather again.
|
|
|
3388
3837
|
because a project whose requirement count only rises should have decided in
|
|
3389
3838
|
advance what would make one leave.
|
|
3390
3839
|
|
|
3840
|
+
- **The `cmd.exe` quoting in `tests/consumer.spec.ts` guards one character of
|
|
3841
|
+
several.** `shellArg` asserts the argument holds no `"` and then quotes only
|
|
3842
|
+
when it holds whitespace, so an argument with none goes to the shell bare —
|
|
3843
|
+
and `&`, `|`, `^`, `<`, `>`, `(` are metacharacters there that the assertion
|
|
3844
|
+
does not name. The fix is one character class, which is why the entry is not
|
|
3845
|
+
about the fix.
|
|
3846
|
+
*What blocks the decision is that no input reaches it.* Both arguments are
|
|
3847
|
+
either a literal flag or a path from `mkdtemp`, `NEEDS_SHELL` is win32 only
|
|
3848
|
+
and CI is Linux, and the one path an attacker could plausibly influence — the
|
|
3849
|
+
checkout directory on a fork MR pipeline — comes from a GitLab project path,
|
|
3850
|
+
whose character set excludes every metacharacter above. So this is an
|
|
3851
|
+
observation with no attack path, and the honest question is not whether to
|
|
3852
|
+
widen the class but whether the comment above it should stop claiming more
|
|
3853
|
+
than the assertion covers: it says "the assertion keeps that true" about
|
|
3854
|
+
arguments generally, where what it checks is one character. Widening the class
|
|
3855
|
+
and leaving the prose is the change that would look like a fix and settle
|
|
3856
|
+
nothing.
|
|
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.
|
|
3874
|
+
|
|
3391
3875
|
## Considered and rejected
|
|
3392
3876
|
|
|
3393
3877
|
Decisions **not** to build something, kept where they can be found before the
|
|
@@ -3712,42 +4196,8 @@ requirement, and human review at propose is still the whole answer. What is
|
|
|
3712
4196
|
rejected is grouping as a way to assist it, and this reopens only on a
|
|
3713
4197
|
contradiction that grouping would have caught.
|
|
3714
4198
|
|
|
3715
|
-
|
|
3716
|
-
|
|
3717
|
-
Raised by `mine-capablanca` (0.4.2) and explicitly not as a request, which is
|
|
3718
|
-
why it is recorded with its own answer rather than left as a complaint about a
|
|
3719
|
-
message. A `params` value is a scalar or a list of scalars; a table — piece kind
|
|
3720
|
-
to weight, tier to limit — is refused, and the only way to register one is to
|
|
3721
|
-
split it across parallel arrays, which reads worse in the rendered spec than it
|
|
3722
|
-
buys. *What rejected it: the two properties a param has, and a table has
|
|
3723
|
-
neither.* A param is **rendered into `SPEC.md` as a value a human checks at a
|
|
3724
|
-
glance**, and it is **read by a scenario as the one place that value lives**. A
|
|
3725
|
-
nested object fails the first by construction — there is no reading of a
|
|
3726
|
-
five-row table that a reviewer verifies the way they verify `30`. It fails the
|
|
3727
|
-
second more quietly: a table has many readers by definition, so "the one place"
|
|
3728
|
-
becomes "the one file", which is what an ordinary module already is. The
|
|
3729
|
-
schema's own comment records where the line was drawn and why arrays were let
|
|
3730
|
-
in — list constants are the most drift-prone values, and an array still has one
|
|
3731
|
-
owner read by one scenario. A table does not clear that bar; the reason arrays
|
|
3732
|
-
did is exactly the reason it does not.
|
|
3733
|
-
*The reporter's own resolution is the argument's strongest form.* The table went
|
|
3734
|
-
to a shared module, is not registry-owned, and **nothing was lost** — because a
|
|
3735
|
-
piece-value table is tuning, not a promise. That is the rule design §11 now
|
|
3736
|
-
states — *params for promises, plain constants for tuning* — derived
|
|
3737
|
-
independently by the same adopter before it was written anywhere. Under it this
|
|
3738
|
-
candidate is not a limitation of `params` at all: it is a value that was never a
|
|
3739
|
-
requirement's to hold, and §11 carries this rejection's own argument in the same
|
|
3740
|
-
paragraph, as the rule stated over the type rather than over the value.
|
|
3741
|
-
*What ships instead*, in `[0.5.0]`: the refusal now says what is accepted
|
|
3742
|
-
instead of `Invalid input`, and `troubleshooting.md` states the constraint and
|
|
3743
|
-
where such a value belongs — the reporter's actual ask, which was for the
|
|
3744
|
-
decision to be visible rather than discovered by reading the shipped `.d.ts`.
|
|
3745
|
-
The permanent half goes to design §11 with the params rule, not here.
|
|
3746
|
-
*This reopens on* a report where the value is genuinely promised — a table an
|
|
3747
|
-
adopter owes their users, whose rows a reviewer would want in the rendered spec.
|
|
3748
|
-
None has been seen; the one that raised it was tuning.
|
|
3749
|
-
|
|
3750
|
-
[Unreleased]: https://gitlab.com/Pseudorca/attest/-/compare/v0.7.0...main
|
|
4199
|
+
[0.7.2]: https://gitlab.com/Pseudorca/attest/-/tags/v0.7.2
|
|
4200
|
+
[0.7.1]: https://gitlab.com/Pseudorca/attest/-/tags/v0.7.1
|
|
3751
4201
|
[0.7.0]: https://gitlab.com/Pseudorca/attest/-/tags/v0.7.0
|
|
3752
4202
|
[0.6.0]: https://gitlab.com/Pseudorca/attest/-/tags/v0.6.0
|
|
3753
4203
|
[0.5.0]: https://gitlab.com/Pseudorca/attest/-/tags/v0.5.0
|