@am_shork/attest 0.7.3 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +1053 -172
- package/README.md +1 -1
- package/dist/cli/index.js +11 -8
- package/dist/cli/json.d.ts +26 -1
- package/dist/cli/json.js +28 -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 +1 -0
- package/dist/core/gate.js +3 -3
- package/dist/core/locate.d.ts +22 -2
- package/dist/core/locate.js +33 -8
- package/dist/core/parser.d.ts +12 -0
- package/dist/core/parser.js +18 -2
- package/dist/core/pipeline.d.ts +17 -1
- package/dist/core/pipeline.js +109 -17
- package/dist/core/red-record.d.ts +14 -5
- package/dist/core/red-record.js +82 -24
- package/dist/core/registry-issues.d.ts +30 -0
- package/dist/core/registry-issues.js +26 -0
- package/dist/core/registry.d.ts +12 -5
- package/dist/core/registry.js +10 -8
- package/dist/core/runner.js +21 -9
- package/dist/core/schema.d.ts +32 -27
- package/dist/core/schema.js +33 -5
- package/dist/core/skill.js +13 -0
- package/dist/core/splice.js +45 -14
- package/dist/core/static-registry.d.ts +34 -2
- package/dist/core/static-registry.js +138 -12
- package/dist/core/status.js +2 -2
- package/dist/core/terminal.js +5 -2
- package/dist/core/types.d.ts +39 -10
- package/dist/core/validator.d.ts +9 -4
- package/dist/core/validator.js +74 -21
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -13,6 +13,648 @@ 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.8.0] - 2026-08-09
|
|
17
|
+
|
|
18
|
+
### Security
|
|
19
|
+
|
|
20
|
+
- **The `--json` document escaped every C0 control and no C1, while every other
|
|
21
|
+
surface stripped both.** `control()` (`core/terminal.ts`) covers
|
|
22
|
+
`\x00-\x1f`, DEL and `\x80-\x9f`, the last of those held deliberately for
|
|
23
|
+
8-bit CSI; the machine path rested on `JSON.stringify`, and the claim written
|
|
24
|
+
above it — that serialising to JSON escapes the same characters — is true of
|
|
25
|
+
C0 and of nothing else. Re-measured before a line was written, as this file's
|
|
26
|
+
discipline asks: `U+009B` and `U+007F` both leave `renderJson` verbatim.
|
|
27
|
+
Reachable from every `--json` command including `check --json`, which executes
|
|
28
|
+
nothing and still quotes scenario names, file paths and reqIds out of the
|
|
29
|
+
repository being checked; under `verify` and `archive` a thrown message joins
|
|
30
|
+
them.
|
|
31
|
+
**Filed at the strength the evidence carries, and that has not changed.**
|
|
32
|
+
Exploiting it needs a terminal that treats UTF-8-decoded C1 as control —
|
|
33
|
+
believed to split VTE from xterm's default, still **not measured on either** —
|
|
34
|
+
and a reader looking at the JSON rather than parsing it. What makes it worth
|
|
35
|
+
closing is not the size of that window but the asymmetry: design §9.1 is
|
|
36
|
+
stated as a property of every artifact Attest writes, and one emitter opting
|
|
37
|
+
out through a true-but-narrower guarantee is exactly how two `catch` blocks
|
|
38
|
+
came to print a raw stack beside sanitised diagnostics once already.
|
|
39
|
+
**Re-measuring moved where the fix goes.** The entry proposed a map over
|
|
40
|
+
`issues`, on the `withDocs` model. Walking the report showed that is the wrong
|
|
41
|
+
half: `change`, `outFile`, `outFiles` and — two levels down — a status row's
|
|
42
|
+
scenario name and file all carry text from the project under test, and none of
|
|
43
|
+
them passes through the envelope. Enumerating them is the arrangement §9.1
|
|
44
|
+
already names as the cause of the defect, so the rule sits at serialisation
|
|
45
|
+
instead, which is the *document* arrangement the section describes — one point
|
|
46
|
+
of assembly, before any of it is seen, covering a field nobody has added yet.
|
|
47
|
+
`outFiles` and `docsUrl` were both added after the envelope and would each
|
|
48
|
+
have needed remembering. `reqId` needed none of this and is worth recording as
|
|
49
|
+
the case that is safe by construction: `RegistryIdSchema` holds every id to
|
|
50
|
+
`^[A-Z]+-\d+$` on both readers, which is the same argument `render` uses for
|
|
51
|
+
not sanitising ids.
|
|
52
|
+
No `SCHEMA_VERSION` bump: the shape is unchanged, and the field values a
|
|
53
|
+
consumer parses differ only where they previously carried a control character.
|
|
54
|
+
Attested as `ATX-74` rather than by widening `ATX-37`, which claimed this
|
|
55
|
+
surface was exempt — the two are separately falsifiable in the direction that
|
|
56
|
+
matters, since `ATX-37` holds at every print site today while this one failed.
|
|
57
|
+
That rationale, and the same claim in `core/terminal.ts`, are corrected in
|
|
58
|
+
place.
|
|
59
|
+
|
|
60
|
+
- **`render --out` wrote through a committed symlink, because the per-segment
|
|
61
|
+
resolution `init` performs had exactly one caller.** `resolveDest` resolves
|
|
62
|
+
every segment of an `init` destination and refuses with `unsafe-target-path`;
|
|
63
|
+
the `--out` path was `resolve(cwd, opts.out)` and went straight to
|
|
64
|
+
`writeAtomic`. Measured before a line was written, as this file's discipline
|
|
65
|
+
asks, and the filed claim held: with `docs` committed as a symlink to a
|
|
66
|
+
sibling directory, `attest render <dir> --out <dir>/docs/SPEC.md` reported
|
|
67
|
+
`✓ Wrote …` and the document appeared outside the root. The victim's `--out`
|
|
68
|
+
is the one their own README or CI documents, and the content redirected is the
|
|
69
|
+
rendered document — the statements and rationales their repository wrote.
|
|
70
|
+
**The leaf was already safe, which is why the gap was exactly the directory
|
|
71
|
+
segments.** `writeAtomic` lands by `rename`, which replaces a symlink instead
|
|
72
|
+
of following it — measured: a `SPEC.md` symlinked at a file outside the root
|
|
73
|
+
left that file's bytes untouched. `wx` does not reach this either and was
|
|
74
|
+
never scoped to: it guards the temporary path, and the temporary path is
|
|
75
|
+
`dirname(dest)`, i.e. already inside whatever directory the link chose. So
|
|
76
|
+
both held perfectly while the whole write happened somewhere it should not.
|
|
77
|
+
**The scope is the part that is not inherited from `init`.** `init` owns its
|
|
78
|
+
destinations, so containment there is unconditional; `--out` is an argument,
|
|
79
|
+
and `--out ../site/SPEC.md` is a destination the caller can see they chose.
|
|
80
|
+
Refusing it would be refusing the flag. The check therefore applies exactly
|
|
81
|
+
where the argument stops being evidence of where the file goes: a path that
|
|
82
|
+
reads as staying inside the project. It is asked against the root in both
|
|
83
|
+
spellings, lexical and resolved, because they differ whenever the project
|
|
84
|
+
itself sits under a link — testing only the resolved one would skip the check
|
|
85
|
+
for every such checkout, failing open on precisely the layout that has links
|
|
86
|
+
in it.
|
|
87
|
+
**`--check` goes through the same resolution**, which the filed entry did not
|
|
88
|
+
cover. It reads the destination rather than writing it, and a freshness gate
|
|
89
|
+
comparing a fresh document against a file outside the project answers about
|
|
90
|
+
the wrong document — quietly, since both verdicts it can reach are ones it
|
|
91
|
+
reports normally.
|
|
92
|
+
So this is `resolveDest` gaining its second caller rather than a new
|
|
93
|
+
indirection; by this repository's own rule it was a hypothetical seam with one
|
|
94
|
+
adapter and is now a real one. `changes/<name>/` remains the candidate third
|
|
95
|
+
and stays unmeasured.
|
|
96
|
+
**The repair moved the write, not just the check, and the reason is the shape
|
|
97
|
+
of the defect rather than tidiness.** `render`'s `writeAtomic` was the only
|
|
98
|
+
call to it outside `core/` — `runInit` writes in the pipeline, `merge.ts`
|
|
99
|
+
writes in the pipeline, and this one write sat in the CLI, which is exactly
|
|
100
|
+
how it reached the filesystem without the resolution `runInit` had performed
|
|
101
|
+
since `[0.7.3]`. Guarding it in the shell would have left that intact: the
|
|
102
|
+
obligation to call the check would still belong to a caller who can forget it,
|
|
103
|
+
which is the situation `pipeline.spec.ts`'s compiler-guard gate already exists
|
|
104
|
+
to catch elsewhere ("delete the line and every scenario stays green"). So
|
|
105
|
+
`runRender` takes the destination and owns the write, `runRenderCheck`
|
|
106
|
+
resolves the path it reads, and the resolution is **private to the module** —
|
|
107
|
+
there is no longer an entry point that writes a document without resolving
|
|
108
|
+
where it goes. The CLI lost its `writeAtomic` import, and with it the last
|
|
109
|
+
policy decision it was making; `hasError` before the write is now what stops a
|
|
110
|
+
broken registry overwriting a good committed document, stated where the write
|
|
111
|
+
is. No existing test changed: `runRenderCheck` keeps its signature, because
|
|
112
|
+
`target` already carried the spelling a diagnostic echoes and asking a caller
|
|
113
|
+
for it twice is a clump waiting to disagree with itself.
|
|
114
|
+
Two things the move surfaced that a guard bolted onto the shell would have
|
|
115
|
+
hidden. **The compiler guard has to stay the first statement**, which
|
|
116
|
+
delegation gave `runRenderCheck` for free and a resolution placed above it
|
|
117
|
+
quietly took away — an unsupported TypeScript would have come back as a path
|
|
118
|
+
refusal. Both entry points call `compilerIssue` first now, and
|
|
119
|
+
`runRenderCheck` leaves the exemption table in `tests/pipeline.spec.ts`, which
|
|
120
|
+
is what that table is for: a list of exemptions exists to be decided about
|
|
121
|
+
rather than to drift. And **"never overwrite a good document with the output
|
|
122
|
+
of a broken registry" was a comment with nothing behind it** — the write lived
|
|
123
|
+
in the CLI, where no test could reach it. It is asserted now, where the write
|
|
124
|
+
is, and is red without the guard: a half-loaded registry renders a document
|
|
125
|
+
that silently omits requirements, so overwriting the committed one destroys
|
|
126
|
+
the only correct copy and the next `--check` passes against the damage. The ATX-73 scenarios drive
|
|
127
|
+
the binary, so they are what proves the move changed no behaviour, and three
|
|
128
|
+
mutants confirm they still bite: writing without resolving, refusing every
|
|
129
|
+
path that leaves the root, and `--check` skipping the resolution are each red.
|
|
130
|
+
Stated as `ATX-73` with three scenarios: the write path and the `--check` path
|
|
131
|
+
both driven through the real CLI against a project carrying a planted link —
|
|
132
|
+
spawned rather than called, because the defect was never in the resolution but
|
|
133
|
+
in the write not going through one, so a scenario calling `resolveOutFile`
|
|
134
|
+
directly would have passed against the broken build — and a third holding the
|
|
135
|
+
scope, that a destination spelled outside the project is still written where
|
|
136
|
+
it says. All three are red against the previous build.
|
|
137
|
+
One further behaviour change falls out of resolving rather than checking, and
|
|
138
|
+
it is a repair rather than a cost: a link *inside* the project is now
|
|
139
|
+
**followed** instead of replaced. `rename` used to overwrite `docs/SPEC.md ->
|
|
140
|
+
build/SPEC.md` with a regular file while `--check` read straight through it,
|
|
141
|
+
so the two halves of the gate disagreed about which file the document was;
|
|
142
|
+
measured, the link now survives and `build/SPEC.md` receives the bytes.
|
|
143
|
+
**This turns a passing run red for anyone whose `--out` currently resolves
|
|
144
|
+
through a link out of the project**, which is the behaviour change that
|
|
145
|
+
matters and is the point of it. No
|
|
146
|
+
`SCHEMA_VERSION` bump: `unsafe-target-path` is an existing code reported by an
|
|
147
|
+
additional command, `src/cli/json.ts` is untouched, and no field moves.
|
|
148
|
+
|
|
149
|
+
- **`--apply` wrote the registry's own file name into a spec's import as text
|
|
150
|
+
rather than as a string, so a registry whose name carried a quote closed the
|
|
151
|
+
literal and everything after it became code.** `splice.ts` states that every
|
|
152
|
+
generated byte is escaped — `tsString` for statement, rationale and
|
|
153
|
+
`outOfScope`, `keySource` for param keys, a `throw` rather than a guess for
|
|
154
|
+
`__proto__` — and `repointImport` was the one site that interpolated instead.
|
|
155
|
+
What it interpolated is the value the *checked repository* names: `to` is the
|
|
156
|
+
registry file `--apply` decided the ids belong in, and `isReqsFile` tests only
|
|
157
|
+
the suffix.
|
|
158
|
+
**The deletion test says what this was worth, and it is not execution.**
|
|
159
|
+
`archive --apply` runs the suite in the same invocation, so the attacker
|
|
160
|
+
already executes there. What the injection added is **persistence**: the
|
|
161
|
+
payload lands in a `*.spec.ts` that the merge then commits, and runs on every
|
|
162
|
+
later clone and CI job, inside a diff `--apply` advertises as a rename plus one
|
|
163
|
+
specifier. **Not platform-scoped** — Windows rejects `\ / : * ? " < > |` in a
|
|
164
|
+
file name and neither `'` nor `;` is among them.
|
|
165
|
+
Re-measured before a line was written, as this file's discipline asks, and the
|
|
166
|
+
filed claim held with one correction it did not have: the payload must carry
|
|
167
|
+
**the file's own quote character**, so the double-quoted spec survives a `'`
|
|
168
|
+
and vice versa. That is why the repair is not "escape `'`". `tsString` splits
|
|
169
|
+
into a `tsStringBody(value, quote)` that is *told* which quote it writes
|
|
170
|
+
between, and the specifier edit passes the quote already in the file — the
|
|
171
|
+
file's quote style stays exactly as it was, which is the property that ruled
|
|
172
|
+
out `JSON.stringify` here in the first place. No refusal path was needed:
|
|
173
|
+
escaping has an answer for every name, since a specifier is a string and
|
|
174
|
+
`tsStringBody` can spell any string as one, so `repointImport` still cannot
|
|
175
|
+
throw and `merge.ts` keeps the one `UnwritableValue` site it had.
|
|
176
|
+
The regression is asserted as a **round-trip** rather than against escaped
|
|
177
|
+
bytes — the file still parses, carries its original four statements, and the
|
|
178
|
+
specifier reads back as the path — under both quote styles, because escaping
|
|
179
|
+
the wrong quote is as wrong as escaping neither. Both cases are red against
|
|
180
|
+
the previous emitter.
|
|
181
|
+
Stated as `ATX-72` with a scenario per quote style, driving the real
|
|
182
|
+
`--apply` over a project whose registry file is named with the payload. Its
|
|
183
|
+
first draft is worth recording because a gate caught it and not a reviewer:
|
|
184
|
+
written as a quantifier over every value the emitter writes, the `sites` table
|
|
185
|
+
in `tests/intent-rule-candidates.spec.ts` demanded a scenario per site and
|
|
186
|
+
found three the sentence claimed which no `self/` scenario reaches. The hole
|
|
187
|
+
was in the *statement*, not the coverage — the other three sites are attested
|
|
188
|
+
in `tests/splice.spec.ts` and were never part of this change — so the repair
|
|
189
|
+
was to narrow the sentence to the one value the checked repository names.
|
|
190
|
+
§7 gains the general form, since it already carried the emitter's rule ("it
|
|
191
|
+
may emit no text whose evaluation differs from the value it was handed") and
|
|
192
|
+
carried it as a rule about the *registry*: `--apply` writes a second file, and
|
|
193
|
+
a file name is not something a tool is in a position to restrict, so it is
|
|
194
|
+
something the writer is obliged to escape.
|
|
195
|
+
No `SCHEMA_VERSION` bump: `src/cli/json.ts` is untouched, no code or field
|
|
196
|
+
moves, and no valid input changes behaviour — for any registry not named to
|
|
197
|
+
attack, the bytes `--apply` writes are the bytes it wrote before.
|
|
198
|
+
|
|
199
|
+
- **A registry file that swaps its prototype was read as the requirements the
|
|
200
|
+
prototype carried, under the reader that evaluates.** `{ '__proto__': { … } }`
|
|
201
|
+
written as a literal is a swap, not a key: the object has **no own keys at
|
|
202
|
+
all**, so what the loader reported never depended on the swap but on what
|
|
203
|
+
happened to be behind it. The static reader refuses such a source outright as
|
|
204
|
+
`registry-not-static` and always did — the two readers agreeing is a security
|
|
205
|
+
property, not a convenience — so this was the two of them disagreeing about
|
|
206
|
+
whether a file is a registry at all.
|
|
207
|
+
**A fixture for exactly this existed and was passing for the wrong reason.**
|
|
208
|
+
`tests/static-registry.spec.ts` put *malformed* ids on the prototype, and zod
|
|
209
|
+
v3's record enumerates with `for…in`, which walks the chain — so those
|
|
210
|
+
inherited keys were tested against the id grammar and failed it. The red was
|
|
211
|
+
incidental to the swap, and the whole guarantee rested on it.
|
|
212
|
+
Put a **well-formed** id on the prototype instead and the same shape is
|
|
213
|
+
**accepted**, with the entire registry built out of a prototype the source
|
|
214
|
+
does not declare. That is measured on the zod this release ships with, not
|
|
215
|
+
inferred: the new differential case is red without the guard and the old
|
|
216
|
+
fixture stays green beside it, which is the defect and its camouflage in one
|
|
217
|
+
run. Found while evaluating the move to zod v4 — which enumerates own keys
|
|
218
|
+
only, so the swapped object is simply empty and even the malformed fixture
|
|
219
|
+
goes green — but the acceptance is reachable today and is fixed here, ahead of
|
|
220
|
+
that port and independently of it.
|
|
221
|
+
Refused now by the `isPlainObject` guard `params` has carried since a param
|
|
222
|
+
became any JSON value, applied one level up to the registry itself — **stated
|
|
223
|
+
over the shape rather than over the name `__proto__`**, because a blocklist of
|
|
224
|
+
names is a second answer to "is this data" and the guard is already the first.
|
|
225
|
+
A class instance and a `Date` fail it for the same reason. `registry-invalid`
|
|
226
|
+
rather than a code of its own: the obligation is identical to every other way
|
|
227
|
+
the schema refuses a registry, and so is the reader's situation.
|
|
228
|
+
Stated as `ATX-70` with three scenarios, and the fixture the suite was missing
|
|
229
|
+
— the well-formed-id swap — is now the one the differential suite runs.
|
|
230
|
+
|
|
231
|
+
### Changed
|
|
232
|
+
|
|
233
|
+
- **The guidance now says which quantifier to write, where before it only said
|
|
234
|
+
what a scenario must read.** `[0.7.0]` added the `QUANTIFIED` table after
|
|
235
|
+
`ATX-37` — "**everything** the CLI writes to a terminal MUST replace every
|
|
236
|
+
control character" — was found to have scenarios for the two formatters that
|
|
237
|
+
existed the day it was written, while `render` built a document by
|
|
238
|
+
concatenation that never went past the sanitiser. What the table taught was
|
|
239
|
+
never written down anywhere an author would meet it, and it is the more useful
|
|
240
|
+
half of that work: **which surface a quantifier ranges over decides whether
|
|
241
|
+
scenarios can keep up with it**, and there are three, not the two the original
|
|
242
|
+
proposal assumed. Over *values* one site processes, representative inputs
|
|
243
|
+
settle it. Over a *roster* the code enumerates, a scenario looping that roster
|
|
244
|
+
covers a new member by construction. Over *sites* in the implementation, each
|
|
245
|
+
needing a hand-written scenario, a new site is a new obligation and nothing
|
|
246
|
+
enumerates them — the only one of the three that can silently stop being true,
|
|
247
|
+
and what `ATX-37` was. So the rule is *prefer a quantifier a scenario can
|
|
248
|
+
iterate*.
|
|
249
|
+
*Where it landed.* Design §11 as a residual weakness, beside the two `params`
|
|
250
|
+
bullets it completes — those say what an assertion must do with a value it
|
|
251
|
+
read; this says what the statement above them should quantify over in the first
|
|
252
|
+
place. The `init` skill gets the adopter-facing form in the same model section
|
|
253
|
+
as those two rules, so a project adopting Attest is told before it writes the
|
|
254
|
+
statement rather than after the gap is found by reading. Both languages, one
|
|
255
|
+
commit, per the reason `[0.5.0]` records for doing this shape of change in one
|
|
256
|
+
pass.
|
|
257
|
+
*No engine change, and the entry stays open for the one that is still not
|
|
258
|
+
proposed.* Which surface a sentence names is prose judgement — the ground truth
|
|
259
|
+
§0 refuses to compute for someone else's requirements — and the detector that
|
|
260
|
+
exists flags the quantifier only, missing the article form ("A registry that
|
|
261
|
+
cannot be read MUST …"). What ships here is counter-pressure, which is what §11
|
|
262
|
+
now says: a hand-judged table for this registry, plus a rule an author can
|
|
263
|
+
follow. The carve-out is carried with it, because it is the failure mode of any
|
|
264
|
+
such table: rewriting a statement to name its surfaces takes it back out of the
|
|
265
|
+
flagged set, so the table admits rows for ids nothing flags.
|
|
266
|
+
|
|
267
|
+
- **Zod moves from v3 to v4 (`zod@^3.23.0` → `^4.4.3`).** The reason is supply,
|
|
268
|
+
not speed: v3 is in maintenance, so fixes land on v4. The port of the schema as
|
|
269
|
+
it stood was the **one API change** the plan predicted — `errorMap` → `error` —
|
|
270
|
+
with `z.custom().pipe()`, `z.record`, `z.lazy` and the recursive `paramValue`
|
|
271
|
+
all carrying over unedited, and `red-record.ts`'s `z.ZodType` untouched. A
|
|
272
|
+
`as z.ZodType<ParamValue>` on `paramValue` became unnecessary and is gone. The
|
|
273
|
+
count is one *for the schema the plan measured*: the prototype guard added in
|
|
274
|
+
the commit before this one brought a second options bag with it, so the port
|
|
275
|
+
actually touched two — which is the ordinary way a plan's cost estimate goes
|
|
276
|
+
stale, by the code moving under it rather than by the estimate being wrong.
|
|
277
|
+
**The plan's "no `package.json` change" framing was about the evaluation, and
|
|
278
|
+
taking it as the destination would have shipped a broken package.** It rested
|
|
279
|
+
on `zod@3.25.76` exporting a `./v4` subpath — true of the *installed* version
|
|
280
|
+
and not of the *declared range*: `zod@3.23.8` is inside `^3.23.0` and exports
|
|
281
|
+
no `./v4` at all, so importing `zod/v4` while keeping that range yields a
|
|
282
|
+
package that fails to resolve on a legitimate install of its own dependency.
|
|
283
|
+
It is a real bump or it is nothing.
|
|
284
|
+
*Re-measured here, and the entry's central type claim does not survive.* The
|
|
285
|
+
plan recorded instantiations as **unchanged** (27,681 → 27,671). On this
|
|
286
|
+
codebase they go **36,211 → 89,831**, about 2.5×, with build check time
|
|
287
|
+
0.58 s → 0.71 s. Controlled for the prototype guard, which accounts for 292 of
|
|
288
|
+
that — the growth is the port. The consumer-side `.d.ts` footprint is worse
|
|
289
|
+
than predicted too: **14 files / 68 KiB → 80 files / 217 KiB** (the plan said
|
|
290
|
+
66 / 181 KiB), and a consumer's own instantiations 71,935 → 131,044. Runtime
|
|
291
|
+
goes the other way and by less than advertised: parsing this registry is
|
|
292
|
+
**0.873 ms → 0.143 ms**, 6.1× rather than 13×, and worth ~0.7 ms against the
|
|
293
|
+
hundreds `attest check self` takes. None of it blocks a migration whose reason
|
|
294
|
+
is supply, and all of it is recorded because "v4 is also faster to typecheck"
|
|
295
|
+
is the sentence that would otherwise reach the commit message — and on this
|
|
296
|
+
schema it is false in a way the plan did not have: the plan called the type
|
|
297
|
+
side neutral, and it is a cost.
|
|
298
|
+
*One diagnosis regression, found and repaired.* A v4 record reports a bad key
|
|
299
|
+
as "Invalid key in record" and does not surface the key schema's own message,
|
|
300
|
+
so a mistyped requirement id — the commonest way to fail this schema — stopped
|
|
301
|
+
being told the grammar it had missed. The record now carries an explicit
|
|
302
|
+
`error`, which restores it.
|
|
303
|
+
|
|
304
|
+
- **`RegistryValidationError.issues` has a shape of this project's own, and no
|
|
305
|
+
longer exposes `z.ZodIssue`.** Zod was otherwise fully absorbed — `Requirement`
|
|
306
|
+
and `Registry` are `z.infer` aliases, so a consumer resolves them to plain
|
|
307
|
+
object types and never names the library. This field was the single exception,
|
|
308
|
+
and it was the load-bearing one: it is reached through the `./define` subpath
|
|
309
|
+
that `defineRequirements` is imported from, and `ZodIssue` is a discriminated
|
|
310
|
+
union zod reshaped between v3 and v4. So a dependency bump this project should
|
|
311
|
+
have been free to make was a **breaking change for anyone who had destructured
|
|
312
|
+
the field**. The element type is now `RegistryValidationIssue` — `path:
|
|
313
|
+
PropertyKey[]` and `message: string`, exported from the same subpath — which is
|
|
314
|
+
the same boundary `Issue.message` already holds, with the difference that
|
|
315
|
+
`Issue` was designed with the boundary and this was not.
|
|
316
|
+
**Breaking, for the narrow surface that read past those two fields**, which is
|
|
317
|
+
why it is a minor under 0.x. Nothing in this repository was in that surface:
|
|
318
|
+
re-measured before a line was written, the only two readers are `formatIssues`
|
|
319
|
+
in `registry.ts` and `firstMessage` in `apply.ts`, and **nothing anywhere reads
|
|
320
|
+
`code`, `expected`, `received` or `fatal`** — which is what made this a rename
|
|
321
|
+
rather than a redesign. A consumer that only formats what it is given, as both
|
|
322
|
+
of those do, sees no change at all.
|
|
323
|
+
*`path` is `PropertyKey[]` rather than `(string | number)[]` deliberately.* It
|
|
324
|
+
is the wider of the two spellings zod has used, so a validator failure assigns
|
|
325
|
+
to it under either — which is the whole point, since the release that moves
|
|
326
|
+
that type is the one this change exists for.
|
|
327
|
+
*One thing the plan glossed, found by doing it.* The two formatters were said
|
|
328
|
+
to read the same two fields, and they did — but not the same way: `apply.ts`
|
|
329
|
+
spelled the path `.map(String).join('.')` and `registry.ts` spelled it
|
|
330
|
+
`.join('.')`. Under `(string | number)[]` those are identical; under
|
|
331
|
+
`PropertyKey[]` the second **throws** on a symbol key, because `join` cannot
|
|
332
|
+
convert one. The shared formatter uses the safe spelling. `ATX-44` is what made
|
|
333
|
+
the rest checkable rather than hopeful: the `a.b.c` path spelling is a stated
|
|
334
|
+
requirement with a scenario on it, so the property that had to survive already
|
|
335
|
+
had something asserting it.
|
|
336
|
+
|
|
337
|
+
- **`changes/<name>/first-run.json` moves to `version: 2`, and a version-1 file
|
|
338
|
+
is discarded whole.** The record now nests the scenario name under the spec
|
|
339
|
+
file, so the old shape cannot be read as the new one. Discarding is the safe
|
|
340
|
+
direction — an empty record makes every obligation "never observed", which the
|
|
341
|
+
gate blocks on — and the cost is one stage-1 run to re-observe, for a change
|
|
342
|
+
in flight across this upgrade only. The version is *checked* rather than
|
|
343
|
+
inferred from the old shape failing the schema: it would fail it anyway, but
|
|
344
|
+
"discarded because the format moved" and "discarded because the file is
|
|
345
|
+
corrupt" are different facts, and reading one as the other is how a format
|
|
346
|
+
change happens without anyone deciding to make it. Unrelated to the report
|
|
347
|
+
`schemaVersion`, which does not move — `src/cli/json.ts` is untouched, and no
|
|
348
|
+
envelope, field or `code` changed.
|
|
349
|
+
|
|
350
|
+
### Fixed
|
|
351
|
+
|
|
352
|
+
- **A scenario that never ran was reported as attested, when another spec file
|
|
353
|
+
declared a scenario of the same name under the same `requirement()`.** Runtime
|
|
354
|
+
coverage was keyed `(reqId, scenario name)` with no file, so two spec files
|
|
355
|
+
declaring `scenario('x')` under `requirement('APP-1')` were one entry, and
|
|
356
|
+
whichever of them executed vouched for both. `declared-not-run` — the check
|
|
357
|
+
whose whole job is to catch a declared scenario that did not execute — was
|
|
358
|
+
answering about the wrong scenario, and answering "fine".
|
|
359
|
+
Reproduced on 2026-08-09 with no flag, no `--eval` and default configuration:
|
|
360
|
+
a `real.spec.ts` whose `scenario('x')` runs beside a `ghost.spec.ts` whose
|
|
361
|
+
`scenario('x')` sits in a branch that never registers gives
|
|
362
|
+
`✓ No issues. — 1 requirement / 2 scenarios`. Renaming the ghost to `'y'`
|
|
363
|
+
produces the `declared-not-run` naming it, so the collision was the whole of
|
|
364
|
+
the difference — the check was present and was looking at the wrong key.
|
|
365
|
+
**This is the failure this tool exists to refuse**, which is what put it ahead
|
|
366
|
+
of everything else in flight: a report that says a scenario attested a
|
|
367
|
+
requirement when the scenario did not execute.
|
|
368
|
+
**The same key reached the first-run record, and there it is worse.** The
|
|
369
|
+
record is monotonic toward `fail`, so two scenarios of one added requirement
|
|
370
|
+
sharing a name meant one going red satisfied the other's `never-red`
|
|
371
|
+
obligation — mechanism 2 certifying a discrimination that never happened.
|
|
372
|
+
That half was inferred from the shared key when this was filed and is measured
|
|
373
|
+
here: both halves go red independently under control, which is why `ATX-71` is
|
|
374
|
+
two obligations rather than one.
|
|
375
|
+
Fixed by carrying the file into the key rather than by adding a check. `walk`
|
|
376
|
+
already descends from the file task, which is the only level holding a path,
|
|
377
|
+
and `ParsedScenario` has carried `file` since it existed — so both sides of
|
|
378
|
+
design §5.4's one cross-process channel had the field and only the maps
|
|
379
|
+
forgot it. It goes through `relativePath` for the reason `unloadedFiles` does:
|
|
380
|
+
the key is compared against a plan built on POSIX paths, and a native
|
|
381
|
+
separator on Windows would turn this false green into a false red.
|
|
382
|
+
Nested (`reqId -> file -> name`) rather than a composite key string, at both
|
|
383
|
+
ends: no separator has to be chosen that a path or a scenario name could
|
|
384
|
+
contain, and `first-run.json` stays a file a reviewer reads.
|
|
385
|
+
**What the fix cost, and the alternative that was rejected.** Keyed by the
|
|
386
|
+
name on disk, the record broke `--apply`'s resume: step 2 renames
|
|
387
|
+
`x.proposed.spec.ts` to `x.spec.ts`, so a resume after that rename looked up a
|
|
388
|
+
key written under the proposed name, found nothing, and blocked a change whose
|
|
389
|
+
scenarios *had* been observed red — the gate refusing its own evidence, caught
|
|
390
|
+
by `ATX-53`'s two resume scenarios. Rewriting the record beside the rename was
|
|
391
|
+
the obvious repair and is worse: two files that must move together, in a
|
|
392
|
+
design whose entire resume story is that each step is separately idempotent.
|
|
393
|
+
So the record keys by the **merged** spelling from the start, which the rename
|
|
394
|
+
cannot move. It still separates two spec files, which is the collapse the key
|
|
395
|
+
exists to prevent; what it deliberately does not separate is a file from its
|
|
396
|
+
own proposed name, because those are the same file.
|
|
397
|
+
Not claimed: that a scenario name is unique within one file. It need not be,
|
|
398
|
+
and this key does not make two same-named scenarios in one file
|
|
399
|
+
distinguishable. What it closes is the cross-*file* case, which is the one
|
|
400
|
+
reachable by ordinary means — two authors adding a scenario to the same
|
|
401
|
+
requirement from two files is how a requirement grows, while a duplicate name
|
|
402
|
+
inside one file is visible in the file being read.
|
|
403
|
+
Measured on Linux; **Windows is unmeasured**, and the path-shaped half of the
|
|
404
|
+
fix is exactly what differs there.
|
|
405
|
+
|
|
406
|
+
- **An error class was being used as a formatter, at two sites that never threw
|
|
407
|
+
it.** `locate.ts` and `static-registry.ts` each constructed a
|
|
408
|
+
`RegistryValidationError` purely to borrow its `.message`, because both report
|
|
409
|
+
the same `registry-invalid` as the throwing path and the message has to be
|
|
410
|
+
identical rather than merely similar. The formatter is what those sites wanted
|
|
411
|
+
and it is now the thing they can ask for — `registryValidationMessage` in the
|
|
412
|
+
new `core/registry-issues.ts`, which all three sites share. That sharing is
|
|
413
|
+
what makes the messages structurally identical instead of a coincidence
|
|
414
|
+
maintained by hand.
|
|
415
|
+
The module is separate from `registry.ts` rather than added to it because
|
|
416
|
+
`./define` maps to the whole of `registry.ts`, so everything that file exports
|
|
417
|
+
is a public contract. The issue *type* has to be public; the formatter does
|
|
418
|
+
not, and both of its callers are internal.
|
|
419
|
+
|
|
420
|
+
### Added
|
|
421
|
+
|
|
422
|
+
- **`tests/registry-error-types.spec-d.ts` pins the shape that was just
|
|
423
|
+
narrowed.** The property is entirely type-level — it has no runtime footprint,
|
|
424
|
+
and the engine reads registries from the AST — so a `.spec-d.ts` is the only
|
|
425
|
+
thing that can catch a regression in it, and a green suite would otherwise
|
|
426
|
+
carry one through to users untouched. It pins both halves: that the element
|
|
427
|
+
type is exactly the two fields (`toEqualTypeOf`, so a future edit widening it
|
|
428
|
+
back to a union fails), and that a real parse failure still assigns to it with
|
|
429
|
+
no cast. Confirmed to go red on the exact regression it exists to catch by
|
|
430
|
+
reverting the field to `z.ZodIssue[]` and re-running it.
|
|
431
|
+
|
|
432
|
+
## [0.7.4] - 2026-08-08
|
|
433
|
+
|
|
434
|
+
### Added
|
|
435
|
+
|
|
436
|
+
- **`ATX-37`'s three unattested sites are closed, and the sweep that was going
|
|
437
|
+
to close them is rejected instead.** The requirement says *everything* the CLI
|
|
438
|
+
writes to a terminal replaces the control characters in text it quotes from
|
|
439
|
+
the project under test, and the `QUANTIFIED` table had carried four `null`
|
|
440
|
+
rows against it since `[0.7.0]`: the status rows, the coverage table, the
|
|
441
|
+
archive verdict, and the path `--out` echoes. Three of those are formatters
|
|
442
|
+
interpolating a reqId, a change name or a written path — correct by reading,
|
|
443
|
+
attested by nothing, which is the state a `sites` quantifier is the only kind
|
|
444
|
+
able to reach in silence. Three scenarios now drive them with hostile input,
|
|
445
|
+
covering six interpolations, and each was **proven able to go red by injection
|
|
446
|
+
before being believed**: `inline` removed from the status row's id, from the
|
|
447
|
+
merged path, from the archive verdict's change name, and from the coverage
|
|
448
|
+
row's id — four defects, each failing exactly one scenario and no other. The
|
|
449
|
+
fourth `null` stays: `--out` needs a filename holding a control character,
|
|
450
|
+
which NTFS refuses and Linux permits, and an unmeasured half is what a `null`
|
|
451
|
+
records.
|
|
452
|
+
**A fifth injection is why the archive verdict is asserted three times.**
|
|
453
|
+
Reading the diff caught what the site list does not resolve to: `change` is
|
|
454
|
+
interpolated by three separate expressions in that one function — merged,
|
|
455
|
+
archivable, and gate-failed — and the first draft covered two of them, leaving
|
|
456
|
+
`inline` deletable from the third with the whole suite green. A `sites` row
|
|
457
|
+
names sites at the granularity a sentence uses, and a branch is finer than
|
|
458
|
+
that; the injection is what tells the two apart.
|
|
459
|
+
**Re-measuring the proposal before building it is what split it in two**, and
|
|
460
|
+
the figures moved in both directions. `src/cli/report.ts` has 8 exports of
|
|
461
|
+
which **7** are `format*` — the eighth is `summarize`, so the entry's "eight
|
|
462
|
+
formatters" was already one off its own wording. The "eight fixtures, one per
|
|
463
|
+
signature" was worse: there are 7 distinct parameter types, `Issue[]` is
|
|
464
|
+
shared by three functions, and the honest cost of the fix turned out to be two
|
|
465
|
+
fixtures extending ones `self/report.spec.ts` already builds. And the entry
|
|
466
|
+
**undercounted its own benefit** — "three currently-correct sites" is three
|
|
467
|
+
*formatters* carrying six interpolations.
|
|
468
|
+
|
|
469
|
+
- **The `✗` samples are bound to a fixture whose rejection the suite asserts.**
|
|
470
|
+
A page showing input the engine must *reject* was the one kind of sample
|
|
471
|
+
nothing could see going stale: it carries no version, names no `src/` symbol,
|
|
472
|
+
and keeps its page's structure perfectly, so every gate in
|
|
473
|
+
`tests/docs-consistency.spec.ts` passed while the claim underneath it aged.
|
|
474
|
+
The failure that mattered is asymmetric — a stale runnable sample stops
|
|
475
|
+
working when a reader pastes it, while a stale counterexample is a reader
|
|
476
|
+
taught a rule that is no longer a rule, which is worse than teaching nothing.
|
|
477
|
+
A `✗` annotated with a code the engine can emit is now bound to a fixture,
|
|
478
|
+
byte-equal the way `fixtures/consumer/` is bound to the README, and the
|
|
479
|
+
fixture is **run through the reader the documented command uses** — the code
|
|
480
|
+
and the line it produces are compared with the code and the line the page
|
|
481
|
+
shows. The line is the half that catches the shape a code-only assertion would
|
|
482
|
+
not: a fixture still rejected, for a different reason, having quietly stopped
|
|
483
|
+
demonstrating what it is quoted for.
|
|
484
|
+
**Re-measuring the entry before building it moved its cost by a factor.** It
|
|
485
|
+
was filed as the expensive half, "a fixture per diagnostic". Of the 33 fenced
|
|
486
|
+
`ts` blocks in the README and `docs/en/**`, 8 carry a `✗`, 3 name a code the
|
|
487
|
+
engine can emit, and 1 of those 3 is elided (`duplicate-prefix`, taught as two
|
|
488
|
+
one-line registries whose bodies are `{ /* … */ }`) — leaving one sample,
|
|
489
|
+
quoted on two pages, bound to one fixture. The count was never the reason it
|
|
490
|
+
sat: what it was really waiting for is the thing that makes the roster hold,
|
|
491
|
+
which is that the binding is a **sweep** rather than a list. A new `✗ code`
|
|
492
|
+
block on any English page is bound to a fixture or fails as `unbound`, so the
|
|
493
|
+
next diagnostic pays a fixture because it is obliged to, not because someone
|
|
494
|
+
remembered the roster existed.
|
|
495
|
+
**Proven able to go red before being believed**, since a gate over samples
|
|
496
|
+
that are all currently correct has never failed and cannot be trusted until it
|
|
497
|
+
does: four mutations, one per way the binding can rot — the page edited away
|
|
498
|
+
from the fixture, the fixture made static so the engine accepts it, a `✗`
|
|
499
|
+
sample added bound to nothing, and the defect moved to a line the annotation
|
|
500
|
+
does not mark. Each was reported naming the page or the fixture.
|
|
501
|
+
*Two limits, both structural.* **English only**: `docs/zh/**` matches
|
|
502
|
+
`docs/en/**` down to the comments inside its samples, so byte-equality is a
|
|
503
|
+
claim only one side can carry, and what covers the Chinese side is the parity
|
|
504
|
+
gate from `[0.4.3]` holding the code-sample sequences equal — structure, not
|
|
505
|
+
meaning. And an **elided block is excluded by rule**, not by omission from the
|
|
506
|
+
roster: `{ /* … */ }` is what makes that sample readable and also what makes it
|
|
507
|
+
unrunnable. The residual risk recorded in `[0.4.3]` does *not* apply here —
|
|
508
|
+
the byte gate and the run proving the fixture still fails are in the same
|
|
509
|
+
file, so a fixture that stopped being rejected cannot pass by living in a
|
|
510
|
+
suite nobody connected to the page.
|
|
511
|
+
|
|
512
|
+
- **`ATX-15` is swept over `src/` from the AST, rather than attested one site at
|
|
513
|
+
a time.** The requirement quantifies over *sites* — every ordering committed to
|
|
514
|
+
disk or compared for equality — and `sites` is the one shape the quantifier
|
|
515
|
+
table records as able to stop being true in silence: a new site is a new
|
|
516
|
+
obligation and nothing enumerates them. Two of its five sites were `null`:
|
|
517
|
+
`red-record.ts` sorts ids and scenario names into `first-run.json`, and
|
|
518
|
+
`splice.ts` sorts ids and param keys into the registry file `--apply` writes.
|
|
519
|
+
Both are committed to disk, both were correct, and neither had a scenario. A
|
|
520
|
+
scenario per site would have closed those two and left the shape untouched,
|
|
521
|
+
because site six would have been unattested the day it was added. The sweep
|
|
522
|
+
instead makes the roster one the **code** owns — every `.sort`/`.toSorted` in
|
|
523
|
+
`src/` is a member — so a new ordering site is covered by construction. The
|
|
524
|
+
sanctioned comparator, the sort methods and the banned APIs are `ATX-15`
|
|
525
|
+
params, read by the scenario, pinned against literals for the reason the
|
|
526
|
+
domain table states: shrinking a list a scenario iterates removes a case and
|
|
527
|
+
everything left still passes.
|
|
528
|
+
**Stated narrowly enough to be true: it constrains how a site orders, not that
|
|
529
|
+
it orders.** A `red-record.ts` that stopped sorting altogether is still green
|
|
530
|
+
here, and the three hand-written scenarios are what speak to the result. That
|
|
531
|
+
is the honest limit of what a sweep can prove, and the table records it rather
|
|
532
|
+
than letting the row read as fully discharged.
|
|
533
|
+
**It was proven able to go red against the real corpus before being believed** —
|
|
534
|
+
three injected defects, one per shape the rule admits: a `localeCompare`
|
|
535
|
+
comparator in `red-record.ts`, a bare `.sort()` in `splice.ts`, and a
|
|
536
|
+
hand-rolled comparator in `merge.ts` that never reaches `byCodeUnit`. Each was
|
|
537
|
+
reported with its file, line and offending source. A gate asserting a property
|
|
538
|
+
that already holds everywhere has never been red and cannot become so until it
|
|
539
|
+
fires, so a negative control is not optional for it; a second scenario keeps
|
|
540
|
+
one in the suite permanently, including the case a line-wise scan gets wrong.
|
|
541
|
+
**Re-measuring the proposal's figures before building it changed two of
|
|
542
|
+
them**, which is the section's own rule and the reason it is worth restating:
|
|
543
|
+
the entry claimed 21 `.sort` call sites (there are **20** — the 21st was a
|
|
544
|
+
comment, which a regex counts and an AST does not), and two exceptions to
|
|
545
|
+
"names `byCodeUnit` on the call line" (there are **six**: one delegating to a
|
|
546
|
+
local `compareIds`, five inline arrows). The exception count being 3× what was
|
|
547
|
+
written is what made following local delegation part of the rule rather than a
|
|
548
|
+
footnote. The `localeCompare` figure moved the same way and in the direction
|
|
549
|
+
that matters most: **both occurrences in `src/` are comments explaining why it
|
|
550
|
+
is not used**, so a line-wise regex would have opened at 100% false positives.
|
|
551
|
+
Reading from the AST was the entry's own recommendation; it turns out to have
|
|
552
|
+
been the difference between a gate and a nuisance.
|
|
553
|
+
One shape the rule admits was not in the proposal and is worth naming:
|
|
554
|
+
`report.ts` sorts by `b.count - a.count || byCodeUnit(…)`. A numeric term is
|
|
555
|
+
legitimate, so the rule is "every comparator **reaches** the sanctioned one",
|
|
556
|
+
never "sorts only by code unit" — a stricter rule would have been red on
|
|
557
|
+
correct code the day it was written.
|
|
558
|
+
|
|
559
|
+
### Fixed
|
|
560
|
+
|
|
561
|
+
- **`attest check` answered `✓ No issues.` for a `*.reqs.ts` that does not
|
|
562
|
+
compile.** TypeScript's parser is error-tolerant by design: handed a file
|
|
563
|
+
truncated before its closing `});` it recovers the object literal and returns a
|
|
564
|
+
tree anyway, so the static reader read it as a *complete* registry and reported
|
|
565
|
+
nothing at all — while `--eval` on the same bytes said `registry-invalid`. Two
|
|
566
|
+
readers of one file disagreeing about whether it is even a file is the
|
|
567
|
+
condition the differential suite exists to prevent, and that suite could not
|
|
568
|
+
have caught it: it runs over registries that all compile. Nothing was looking
|
|
569
|
+
at `parseDiagnostics`, which is the one field that says so.
|
|
570
|
+
**The reach is three readers, not one**, because the tolerance belongs to the
|
|
571
|
+
compiler rather than to any reader: `parser.ts`, `static-registry.ts` and
|
|
572
|
+
`splice.ts` all open with the same `createSourceFile` call. The same half-saved
|
|
573
|
+
file was also a spec whose scenarios above the break still counted as coverage,
|
|
574
|
+
a delta whose ids `status` still reported, and — the site that writes rather
|
|
575
|
+
than reports — an insertion offset `attest apply` would have spliced a new
|
|
576
|
+
requirement into. All four now refuse it: the three readers as
|
|
577
|
+
`unreadable-file`, and the insertion point by returning the `undefined` it
|
|
578
|
+
already documents as its refusal to guess, because a write path that throws
|
|
579
|
+
mid-merge is worse than one that declines.
|
|
580
|
+
The diagnostic carries the **line** the syntax error is on, through both
|
|
581
|
+
shapes of refusal — the readers return it, and the spec parser carries it
|
|
582
|
+
through the throw its guard already catches. Without that second half the same
|
|
583
|
+
code would have had a line for a registry and none for a spec, for one
|
|
584
|
+
condition.
|
|
585
|
+
`unreadable-file` rather than a new code: `ATX-65` already states this
|
|
586
|
+
obligation for a file the parser cannot read, and the reader's situation is
|
|
587
|
+
identical — nothing in the file was checked, everything else in the run still
|
|
588
|
+
was. What `ATX-65` did not state is the *detection*, which is the whole of what
|
|
589
|
+
was missing; it was written for a parser that throws, and a parser that quietly
|
|
590
|
+
succeeds needs a different question asked of it.
|
|
591
|
+
**Syntactic only, and that is what makes it safe rather than merely narrow.**
|
|
592
|
+
`parseDiagnostics` is the parser's own list, so a type error, an unresolved
|
|
593
|
+
import and a name that does not exist are all absent from it — Attest does not
|
|
594
|
+
typecheck an adopter's project and this does not start. Measured over the 129
|
|
595
|
+
TypeScript files in this repository with every fixture included: zero carry a
|
|
596
|
+
parse diagnostic, so nothing valid today starts failing. The one reader left
|
|
597
|
+
deliberately tolerant is the id recovery added above, which runs only on files
|
|
598
|
+
a reader has already refused.
|
|
599
|
+
|
|
600
|
+
- **Two scenarios named by a template literal were invisible to the static
|
|
601
|
+
plan.** `self/half-loaded.spec.ts` generated one scenario per command, and the
|
|
602
|
+
parser reads names from the AST — so `cover` counted one scenario where two
|
|
603
|
+
ran, and `archive`'s never-red gate could never have seen the second. Found
|
|
604
|
+
while binding the new requirement's roster to a scenario the plan declares.
|
|
605
|
+
Both are now one scenario looping the roster, which is the shape `ATX-64`
|
|
606
|
+
already used; the label on each `expect` is what the generated name was buying,
|
|
607
|
+
and it survives a failure. **The engine is not changed**: a name a static
|
|
608
|
+
reader cannot resolve is not a defect in the reader, and the failure it
|
|
609
|
+
produces on a requirement covered *only* that way is `uncovered-requirement`,
|
|
610
|
+
which is loud.
|
|
611
|
+
|
|
612
|
+
### Changed
|
|
613
|
+
|
|
614
|
+
- **One unreadable `*.reqs.ts` reported a finding per scenario that attested it,
|
|
615
|
+
and the finding was about the file.** The ids that file declares are missing
|
|
616
|
+
from the run, so every scenario covering one of them was an `orphan-test` —
|
|
617
|
+
measured on a registry of three requirements: six ERRORs, of which one was the
|
|
618
|
+
cause and five were its shadow, with the true finding at the top where the wall
|
|
619
|
+
of red pushes it out of view. Those scenarios are now reported once per
|
|
620
|
+
registry file as `orphan-from-failed-registry` (WARNING), naming the file, the
|
|
621
|
+
count and the ids; the same case is now two ERRORs and one WARNING. A WARNING
|
|
622
|
+
because the load failure is already an ERROR and is present whenever this is,
|
|
623
|
+
so no verdict moves and this cannot make a broken registry pass.
|
|
624
|
+
**What unblocked it was that the question was framed wrongly**, and the entry
|
|
625
|
+
under `Under consideration` had carried that framing for two releases: it asked
|
|
626
|
+
for a prefix→file mapping, which is exactly what a file that never parsed
|
|
627
|
+
cannot give — and could only have been recovered from the file *name*, which
|
|
628
|
+
this repository is its own counterexample to (`attest.reqs.ts` holds `ATX-*`,
|
|
629
|
+
the evidence that killed the prefix-matches-filename rule). The **ids** are
|
|
630
|
+
written in the source, and reading them back needs no naming convention that
|
|
631
|
+
nothing enforces. It is also strictly more precise than the prefix would have
|
|
632
|
+
been: a genuine typo sharing a prefix with the broken file stays reported.
|
|
633
|
+
Recall is partial and the direction of the miss is why that is acceptable — an
|
|
634
|
+
id not recovered keeps its `orphan-test`, so the report stays noisy and never
|
|
635
|
+
wrong. Measured over the ways a registry fails: a truncated file, a missing
|
|
636
|
+
default export, a non-literal value, a schema-invalid entry, a module that
|
|
637
|
+
throws at import and a spread from another module all yield their ids; only a
|
|
638
|
+
registry the file *computes* yields none, and that source contains no id to
|
|
639
|
+
find. The recovery is a source read that returns ids and nothing else — it
|
|
640
|
+
builds no registry, so it is not a second reader and cannot become the
|
|
641
|
+
evaluation fallback `registry-not-static` refuses.
|
|
642
|
+
Stated as a replacement rather than a suppression for the reason `ATX-62`
|
|
643
|
+
records about `spec-load-failed`: suppressing with nothing in its place trades
|
|
644
|
+
a wrong message for silence, and a wrong message beats silence.
|
|
645
|
+
|
|
646
|
+
- **The hedged `orphan-test` advice added in `[0.7.0]` was applied to every
|
|
647
|
+
orphan in the report, including the ones it does not describe.** Any registry
|
|
648
|
+
file failing to load switched *all* of them to "a registry file failed to load
|
|
649
|
+
— fix that first, and add this id only if it is still unknown afterwards",
|
|
650
|
+
so a genuine typo was told to wait for work that has nothing to do with it.
|
|
651
|
+
The hedge now fires only where the uncertainty is real: when a failed file's
|
|
652
|
+
ids could not be recovered. When they all could, an orphan none of them claims
|
|
653
|
+
is genuinely unknown and gets the plain advice back.
|
|
654
|
+
|
|
655
|
+
`SCHEMA_VERSION` is unchanged: a new `code` value is additive, and no envelope
|
|
656
|
+
field moved.
|
|
657
|
+
|
|
16
658
|
## [0.7.3] - 2026-08-07
|
|
17
659
|
|
|
18
660
|
### Fixed
|
|
@@ -3647,33 +4289,116 @@ about itself, which is three things.
|
|
|
3647
4289
|
**An entry can sit here for releases on the strength of a figure nobody has
|
|
3648
4290
|
checked.** The discipline aims at keeping the *argument*, not at re-testing the
|
|
3649
4291
|
evidence under it, so the first act of building one is re-measuring what it
|
|
3650
|
-
claims rather than implementing what it proposes. That has paid
|
|
3651
|
-
killing an entry outright, when the spans it rested on turned out not to be
|
|
3652
|
-
spans;
|
|
3653
|
-
written
|
|
4292
|
+
claims rather than implementing what it proposes. That has paid three times:
|
|
4293
|
+
once killing an entry outright, when the spans it rested on turned out not to be
|
|
4294
|
+
spans; four times correcting an entry's own numbers or scope before a line of it
|
|
4295
|
+
was written — a call-site count that a regex had inflated, a cost filed as "a
|
|
4296
|
+
fixture per diagnostic" that the corpus put at one fixture, an injection whose
|
|
4297
|
+
payload turned out to need the target file's *own* quote character, which is the
|
|
4298
|
+
difference between escaping one quote and escaping the right one, and a fix
|
|
4299
|
+
filed as a map over `issues` that the report's own shape moved to the
|
|
4300
|
+
serialiser, four fields and one nesting level further out. **What a release costs its adopters is not known on the day it is cut**
|
|
3654
4301
|
— several entries arrived from one adoption report, about shapes the tool made
|
|
3655
4302
|
someone work around without ever erroring. And **the fixes with no observable
|
|
3656
4303
|
symptom are worth batching**: individually none justifies a commit, together they
|
|
3657
4304
|
cost about an hour, and they are the group no gate could have found, since having
|
|
3658
4305
|
no symptom is the definition of what a gate cannot see.
|
|
3659
4306
|
|
|
3660
|
-
|
|
3661
|
-
|
|
3662
|
-
|
|
3663
|
-
|
|
3664
|
-
|
|
3665
|
-
|
|
3666
|
-
|
|
3667
|
-
|
|
3668
|
-
|
|
3669
|
-
|
|
3670
|
-
|
|
3671
|
-
|
|
3672
|
-
|
|
3673
|
-
|
|
3674
|
-
|
|
3675
|
-
|
|
3676
|
-
|
|
4307
|
+
*The five entries this section carried before the one below* were built and are
|
|
4308
|
+
recorded under `[Unreleased]`. The second is where this preamble's own
|
|
4309
|
+
discipline paid again: it went in on an entry whose headline type
|
|
4310
|
+
measurement — instantiations unchanged — was false on re-measure, and the
|
|
4311
|
+
re-measure is the only reason the release notes do not repeat it. The fourth is
|
|
4312
|
+
the other outcome the discipline has, and worth a line because the first three
|
|
4313
|
+
were all corrections: the `render --out` symlink measurement reproduced exactly
|
|
4314
|
+
as filed, down to the leaf being safe for the reason given. What re-measuring
|
|
4315
|
+
added was not a correction but a **surface the entry had not counted** —
|
|
4316
|
+
`--check` reads the same destination, so the same link makes a freshness gate
|
|
4317
|
+
date a document outside the project. Re-measuring an entry is therefore not only
|
|
4318
|
+
a test of the number it quotes; walking the code the claim is about is what
|
|
4319
|
+
shows the claim's own edges. The fifth is that lesson arriving where an entry
|
|
4320
|
+
had already named its own fix: the C1 measurement reproduced exactly, and the
|
|
4321
|
+
repair it proposed — a map over `issues` — was still the wrong half, because
|
|
4322
|
+
four more fields and a nested scenario name carry the same untrusted text and
|
|
4323
|
+
never pass the envelope. An entry that ends with a fix is the one least likely
|
|
4324
|
+
to have its fix re-examined, which is the case worth naming.
|
|
4325
|
+
|
|
4326
|
+
- **Trusted publishing, which would delete `NPM_TOKEN` rather than add to it.**
|
|
4327
|
+
The provenance work in `[0.7.2]` signs the release but does not change
|
|
4328
|
+
what authorises it: a long-lived automation token, masked and protected, that
|
|
4329
|
+
publishes as us to anyone holding it. npm's trusted publishing exchanges the
|
|
4330
|
+
same OIDC identity the attestation already uses for a short-lived credential,
|
|
4331
|
+
which would remove the stored secret entirely and grant provenance as a
|
|
4332
|
+
property of the flow rather than a flag on the command. The deletion test
|
|
4333
|
+
points the right way for once: what it removes is the only long-lived
|
|
4334
|
+
credential this repository has.
|
|
4335
|
+
**The blocker this entry was filed with is retired, and it was the wrong
|
|
4336
|
+
question** — checked against npm's documentation on 2026-08-08. It asked
|
|
4337
|
+
whether npm's trusted publishers accept a **gitlab.com-hosted** project on the
|
|
4338
|
+
plan this package publishes under, and both halves come back clean: GitLab
|
|
4339
|
+
CI/CD is a first-class provider alongside GitHub Actions, and no plan
|
|
4340
|
+
requirement is stated anywhere in the documentation. The four fields it wants
|
|
4341
|
+
are `Pseudorca`, `attest`, the top-level CI file path (`.gitlab-ci.yml`, and it
|
|
4342
|
+
must carry the extension) and the allowed actions; environment name is optional
|
|
4343
|
+
and this project uses no GitLab environments. Shared runners are the only ones
|
|
4344
|
+
supported and the publish job carries no `tags:`, so it is already on them. The
|
|
4345
|
+
`repository.url` cross-check needs no separate verification — provenance
|
|
4346
|
+
already passes it with the same identity.
|
|
4347
|
+
Two things the entry did not anticipate, both of which change the shape of the
|
|
4348
|
+
change rather than blocking it. **`SIGSTORE_ID_TOKEN` is not replaced**: the
|
|
4349
|
+
documented GitLab snippet declares `NPM_ID_TOKEN` with
|
|
4350
|
+
`aud: "npm:registry.npmjs.org"` *beside* it, so this is an addition to the
|
|
4351
|
+
`id_tokens` block, not a swap. And **the YAML is not what deletes the
|
|
4352
|
+
credential** — enabling a trusted publisher leaves the automation token just as
|
|
4353
|
+
able to publish; what retires it is `Require two-factor authentication and
|
|
4354
|
+
disallow tokens` on the package, which npm states does not affect trusted
|
|
4355
|
+
publishers. That setting is the one that closes the loop this job's own comment
|
|
4356
|
+
opens, where an automation token is used *because* a granular token fails
|
|
4357
|
+
against 2FA on writes: trusted publishing does not bypass the prompt, it stops
|
|
4358
|
+
being a token.
|
|
4359
|
+
*What blocks the decision now is `pnpm`, and it is not a version to wait for.*
|
|
4360
|
+
**pnpm does not implement the OIDC exchange at all.** `pnpm/pnpm#11526` — the
|
|
4361
|
+
fix for the pnpm 11 regression that made this look like a supported path —
|
|
4362
|
+
shows what its "OIDC support" is: `actions/setup-node` writes
|
|
4363
|
+
`_authToken=${NODE_AUTH_TOKEN}` into `.npmrc`, pnpm used to pass the unresolved
|
|
4364
|
+
placeholder through verbatim, and the fix treats it as empty so that **npm's**
|
|
4365
|
+
credential exchange can be the sole auth source. pnpm gets out of the way; npm
|
|
4366
|
+
does the work. That matches npm's own wording, which requires publishing
|
|
4367
|
+
through npm directly or indirectly, and it matches the reports: every account
|
|
4368
|
+
of pnpm publishing under OIDC is GitHub Actions, and there is **no record of
|
|
4369
|
+
pnpm + GitLab + OIDC succeeding**. Stated at the strength the evidence carries:
|
|
4370
|
+
this is a strong negative inference, not a documented refusal.
|
|
4371
|
+
So the real cost is a tooling swap — `pnpm publish` back to `npm publish` in
|
|
4372
|
+
this one job — and it is a swap this file has already priced once in the other
|
|
4373
|
+
direction. The comment above records that moving to pnpm took
|
|
4374
|
+
`COREPACK_ENABLE_STRICT` with it, that override having existed only so a bare
|
|
4375
|
+
`npm` could run under a `packageManager` pinning pnpm; going back re-adds it.
|
|
4376
|
+
It also needs npm ≥ 11.5.1 where `node:22-slim` ships 10.x, so the job gains an
|
|
4377
|
+
npm upgrade step. None of that is prohibitive. It is simply a different
|
|
4378
|
+
decision from the one this entry was filed as, which is why it stays here.
|
|
4379
|
+
*One correction to the entry's own risk framing.* "It cannot be tested except
|
|
4380
|
+
by publishing" is half wrong: registering a trusted publisher on npmjs.com is
|
|
4381
|
+
free and reversible, and `NPM_TOKEN` can stay in place while it is tried. What
|
|
4382
|
+
decides whether this can be rolled out incrementally is whether a failed OIDC
|
|
4383
|
+
exchange falls back to the `_authToken` already in `.npmrc` or fails the
|
|
4384
|
+
publish outright — unmeasured, and the thing to establish before touching the
|
|
4385
|
+
job.
|
|
4386
|
+
*Method note, because it is the reusable part.* The pinned pnpm bundle was the
|
|
4387
|
+
obvious place to look and was the wrong one — `pnpm publish --help` on 10.28.0
|
|
4388
|
+
omits the flags it accepts, exactly as this job's comment already records for
|
|
4389
|
+
`--provenance`, so neither the help text nor the shipped bundle would have
|
|
4390
|
+
answered it. The answer was in the upstream pull request.
|
|
4391
|
+
**Promoted from `Under consideration` on 2026-08-08**, and what moved it is
|
|
4392
|
+
that the question it was filed as is answered. It asked whether npm's trusted
|
|
4393
|
+
publishers would accept this project at all; they will. What is left is a
|
|
4394
|
+
known tooling swap with a priced cost, which is a decision to build rather
|
|
4395
|
+
than a question — the preamble's own line, *a decision with an argument
|
|
4396
|
+
attached*. One thing stays unmeasured and is the first act of building it, per
|
|
4397
|
+
the discipline three paragraphs up: whether a failed OIDC exchange falls back
|
|
4398
|
+
to the `_authToken` already in `.npmrc` or fails the publish outright. That
|
|
4399
|
+
decides whether this rolls out incrementally beside the existing token or in
|
|
4400
|
+
one cut, and it is cheap to establish because registering a trusted publisher
|
|
4401
|
+
is free and reversible.
|
|
3677
4402
|
|
|
3678
4403
|
## Under consideration
|
|
3679
4404
|
|
|
@@ -3687,60 +4412,6 @@ deletion. What each entry has to carry is the evidence currently blocking the
|
|
|
3687
4412
|
decision, because that is what a re-proposal a year from now would otherwise
|
|
3688
4413
|
have to gather again.
|
|
3689
4414
|
|
|
3690
|
-
- **Trusted publishing, which would delete `NPM_TOKEN` rather than add to it.**
|
|
3691
|
-
The provenance work in `[0.7.2]` signs the release but does not change
|
|
3692
|
-
what authorises it: a long-lived automation token, masked and protected, that
|
|
3693
|
-
publishes as us to anyone holding it. npm's trusted publishing exchanges the
|
|
3694
|
-
same OIDC identity the attestation already uses for a short-lived credential,
|
|
3695
|
-
which would remove the stored secret entirely and grant provenance as a
|
|
3696
|
-
property of the flow rather than a flag on the command. The deletion test
|
|
3697
|
-
points the right way for once: what it removes is the only long-lived
|
|
3698
|
-
credential this repository has.
|
|
3699
|
-
*What blocks the decision, and it is evidence rather than principle.* Whether
|
|
3700
|
-
npm's trusted publishers accept a **gitlab.com-hosted** project on the plan
|
|
3701
|
-
this package publishes under has not been verified against this project — it
|
|
3702
|
-
was not checked when the provenance job was written, and an entry claiming it
|
|
3703
|
-
works would be exactly the unverified confidence the rest of this file avoids.
|
|
3704
|
-
It also cannot be tested except by publishing: as with provenance, there is no
|
|
3705
|
-
dry run, so a failed experiment is a release. The cheap next step is reading
|
|
3706
|
-
npm's current trusted-publisher documentation for GitLab support before
|
|
3707
|
-
writing any YAML.
|
|
3708
|
-
|
|
3709
|
-
- **A static sweep for the two properties claimed over the whole of `src/`.**
|
|
3710
|
-
The quantifier table's first strict read left three rows wanting the same
|
|
3711
|
-
thing, and the repo already owns the idiom: `tests/import-boundary.spec.ts` is
|
|
3712
|
-
a sweep over the import graph asserting a cross-cutting property no runtime
|
|
3713
|
-
test can see, written for the same reason — "only the graph shows it".
|
|
3714
|
-
*The two candidates, and they are not equally shippable.* `ATX-15` is the easy
|
|
3715
|
-
one: **no `.sort` outside `byCodeUnit`, and no `localeCompare`.** Measured over
|
|
3716
|
-
`src/` today — 21 `.sort` call sites, every one reaching `byCodeUnit`, but only
|
|
3717
|
-
19 naming it on the call line: `render.ts` goes through its own `compareIds`,
|
|
3718
|
-
and `merge.ts` through a comparator that spans two lines. The only four
|
|
3719
|
-
occurrences of `localeCompare` are comments explaining why it is not used. A sweep would have **zero false positives on the current corpus**,
|
|
3720
|
-
which is the bar `divergent-param` failed and `compound-requirement` cleared —
|
|
3721
|
-
and those two exceptions are already the warning that this needs the AST rather
|
|
3722
|
-
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
|
|
3723
|
-
string interpolated into a terminal line goes through `inline`", and that is
|
|
3724
|
-
not a token scan. The shape that would work is a scenario driving every
|
|
3725
|
-
exported `format*` in `src/cli/report.ts` with hostile input — a real roster,
|
|
3726
|
-
since the module's exports are what a new formatter joins — but each takes a
|
|
3727
|
-
different argument type, so it needs a hostile fixture per signature rather
|
|
3728
|
-
than one loop.
|
|
3729
|
-
*What blocks the decision.* Two things, one per candidate. The `ATX-15` sweep
|
|
3730
|
-
is cheap and precise and would be **the first rule in this repository that
|
|
3731
|
-
polices how `src/` is written rather than what it does** — a lint rule wearing
|
|
3732
|
-
a scenario's clothes, and the question is whether that belongs in `self/` (as
|
|
3733
|
-
an obligation `ATX-15` already carries) or in `tests/` beside the import
|
|
3734
|
-
boundary. Filing it under `ATX-15` makes it a permanent obligation with a
|
|
3735
|
-
scenario; filing it in `tests/` makes it deletable in a green diff, which
|
|
3736
|
-
`ATX-57`'s rationale argues against in as many words. The `ATX-37` roster is
|
|
3737
|
-
blocked on cost rather than principle: eight formatters, eight fixtures, and
|
|
3738
|
-
the honest count of what it buys is three currently-correct sites.
|
|
3739
|
-
*Why neither is urgent.* Both properties hold everywhere today, verified by
|
|
3740
|
-
reading. What is missing is not correctness but the thing that keeps it — and
|
|
3741
|
-
the measurement above is what a re-proposal would otherwise have to gather
|
|
3742
|
-
again.
|
|
3743
|
-
|
|
3744
4415
|
- **Bounding the fan-out in `findFiles` and `parseSpecs`.** Both walk the
|
|
3745
4416
|
project with no concurrency limit: `findFiles` recurses through
|
|
3746
4417
|
`Promise.all(subdirs.map(walk))`, so every directory in the tree is opened at
|
|
@@ -3942,100 +4613,8 @@ have to gather again.
|
|
|
3942
4613
|
out below* — the entry is kept whole here because the two were measured in one
|
|
3943
4614
|
pass and separating them is the finding.
|
|
3944
4615
|
|
|
3945
|
-
- **Whether `check` and `verify` should refuse a half-loaded registry the way
|
|
3946
|
-
`cover` and `render` do.** Those two return early on `hasError(loadIssues)`,
|
|
3947
|
-
and the argument is recorded at both call sites: a report computed from a
|
|
3948
|
-
half-loaded registry is a lie, and a lie that reads as a pass is the worst
|
|
3949
|
-
shape it can take — `cover` in particular used to answer `ok: true` on a repo
|
|
3950
|
-
whose intent layer had stopped being readable, because an empty registry is
|
|
3951
|
-
fully covered. `check` and `verify` carry on regardless.
|
|
3952
|
-
*What that produces, measured* on a project with one unreadable `*.reqs.ts`:
|
|
3953
|
-
a `registry-invalid`, then an `orphan-test` for **every** scenario belonging to
|
|
3954
|
-
that file's requirements, plus `empty-spec`, `tests-red` and a
|
|
3955
|
-
`declared-not-run` per scenario — six derived findings from one cause. Worse,
|
|
3956
|
-
`orphan-test`'s fix hint ("Add it to the registry, or fix the id") is *wrong*
|
|
3957
|
-
for an id already sitting in the registry that failed to load, so the loudest
|
|
3958
|
-
advice in the report points at work that must not be done. In a multi-file
|
|
3959
|
-
registry it scales with the broken file's scenario count, and the one true
|
|
3960
|
-
finding is at the top where a wall of red pushes it out of view.
|
|
3961
|
-
*Why it is not simply the same call as `cover` and `render`.* Each of those
|
|
3962
|
-
produces exactly **one** artifact, and refusing to produce it is the whole of
|
|
3963
|
-
the decision. `check` is the command a pipeline runs first and its contract is
|
|
3964
|
-
breadth — returning three findings instead of nine because one of the nine was
|
|
3965
|
-
a load failure makes it quieter about problems it can still see perfectly well,
|
|
3966
|
-
which is the opposite of what it is for. `verify` has the same shape with a run
|
|
3967
|
-
attached.
|
|
3968
|
-
*What blocks the decision.* The narrower move — keep reporting everything
|
|
3969
|
-
except the findings **derived from** the missing registry, i.e. suppress
|
|
3970
|
-
`orphan-test` for ids whose prefix belongs to a file that failed to load —
|
|
3971
|
-
needs a prefix→file mapping for a file that never parsed, and that is exactly
|
|
3972
|
-
the file `loadRegistry` has no ids from. Recovering the mapping means reading
|
|
3973
|
-
the prefix out of something other than the registry's own keys (the file name,
|
|
3974
|
-
or a partial AST read that survives the failure), and whether either is
|
|
3975
|
-
trustworthy enough to gate a suppression on is unmeasured. Suppressing on the
|
|
3976
|
-
weaker signal "some registry file failed to load, so suppress all
|
|
3977
|
-
`orphan-test`" is the other candidate and is a strictly worse `check` on a
|
|
3978
|
-
multi-file registry, where the other files are fine and their orphans are real.
|
|
3979
|
-
**The wrong *advice* has been separated out and fixed; the noise has not.**
|
|
3980
|
-
`[0.7.0]` changes `orphan-test`'s fix hint when any registry file failed
|
|
3981
|
-
to load, so the loudest line in that report stops telling the reader to add an
|
|
3982
|
-
id that already exists. That needed no mapping, because it claims nothing about
|
|
3983
|
-
*which* orphans came from the broken file — only that the command cannot tell,
|
|
3984
|
-
and what order to work in. It is worth separating because the two halves were
|
|
3985
|
-
filed as one problem and are not: **being wrong** was fixable today at the cost
|
|
3986
|
-
of a sentence, and **being loud** is the part still blocked on the mapping
|
|
3987
|
-
above. The entry stays open on exactly that, with one blocker retired: the
|
|
3988
|
-
measured six derived findings from one cause are still six, and only one of
|
|
3989
|
-
them has stopped pointing at work that must not be done.
|
|
3990
|
-
|
|
3991
|
-
- **A statement that quantifies over a surface, with scenarios covering only the
|
|
3992
|
-
part that existed when it was written.** Found by reading, and it had already
|
|
3993
|
-
cost something: `ATX-37` says "**everything** the CLI writes to a terminal —
|
|
3994
|
-
a report, and the stack of a crash — MUST replace every control character",
|
|
3995
|
-
and `self/report.spec.ts` covers `formatIssues` and `formatCrash`. Those were
|
|
3996
|
-
the two functions that existed when it was written. `render` builds a document
|
|
3997
|
-
by concatenation that never went past the sanitiser, and a registry could drive
|
|
3998
|
-
the reviewer's terminal from `attest render` with no flag at all — shipped
|
|
3999
|
-
under `[0.7.0]` as the fix, but the *shape* is not fixed and is not
|
|
4000
|
-
specific to that requirement.
|
|
4001
|
-
The shape: a universal quantifier in the statement, a finite set of scenarios,
|
|
4002
|
-
and nothing anywhere that holds the two together. It reads as covered — the id
|
|
4003
|
-
has scenarios, `cover` is green, `check` is green — and the quantifier is the
|
|
4004
|
-
part no gate is looking at. Every one of the three mechanisms this project
|
|
4005
|
-
sells operates *below* it: coverage asks whether an id has a scenario,
|
|
4006
|
-
never-red asks whether that scenario discriminated, drift asks whether it reads
|
|
4007
|
-
the params. None asks whether the scenarios span what the sentence claims.
|
|
4008
|
-
*What blocks the decision.* Detecting it means knowing which surface a
|
|
4009
|
-
statement names, which is prose judgement — the same judgement
|
|
4010
|
-
`tests/intent-rule-candidates.spec.ts` records as the reason obligation
|
|
4011
|
-
counting can never be an engine rule ("asserting a ground truth Attest cannot
|
|
4012
|
-
compute for someone else's requirements, which is exactly what §0 refuses").
|
|
4013
|
-
So the shippable form may not exist.
|
|
4014
|
-
**The repo half has shipped, and this entry is now only about the engine
|
|
4015
|
-
half.** `[0.7.0]` adds the `QUANTIFIED` table it described — hand-judged,
|
|
4016
|
-
a row per flagged statement, every named scenario asserted to exist — and it
|
|
4017
|
-
closed the way an entry built from a strict read is supposed to: it found a
|
|
4018
|
-
live hole on `ATX-62`, the one requirement written specifically to avoid this
|
|
4019
|
-
shape, where the archive gate path was claimed by the statement and asserted by
|
|
4020
|
-
nothing. Two things came back that the entry did not predict. The useful one is
|
|
4021
|
-
that "names a surface" splits three ways rather than two — `values`, `roster`,
|
|
4022
|
-
`sites` — and only `sites` carries the risk, because a `roster` scenario
|
|
4023
|
-
iterates the set the code owns and grows with it. That is a sharper statement
|
|
4024
|
-
of the defect than "the scenarios do not span the sentence", and it is a rule
|
|
4025
|
-
an author can act on: **prefer a quantifier a scenario can iterate.** The other
|
|
4026
|
-
is that the remedy — rewriting the statement to name its surfaces, as `ATX-62`
|
|
4027
|
-
did — takes it out of the flagged set, so the table has to allow rows for ids
|
|
4028
|
-
the detector does not flag or it loses the binding exactly when the author does
|
|
4029
|
-
the right thing.
|
|
4030
|
-
*What stays open is the engine half, and nothing above touches it.* The three
|
|
4031
|
-
kinds are prose judgement, the detector misses the article form ("A registry
|
|
4032
|
-
that cannot be read MUST …") which is a universal too, and no rule can tell
|
|
4033
|
-
which surface a sentence claims without being told. The repo half is a
|
|
4034
|
-
counter-pressure, not a detector, and it holds for one registry whose authors
|
|
4035
|
-
agreed to maintain it.
|
|
4036
|
-
|
|
4037
4616
|
- **Whether a requirement can ever be retired, and what it costs that none can.**
|
|
4038
|
-
The registry is at
|
|
4617
|
+
The registry is at **69 requirements and 192 scenarios, 2.78 each**, and
|
|
4039
4618
|
every one is a permanent obligation: `uncovered-requirement` is an ERROR, so a
|
|
4040
4619
|
requirement without a scenario cannot exist, and nothing ever removes one.
|
|
4041
4620
|
`delta.removed` exists and `applyDelta` applies it, but `--apply` refuses to
|
|
@@ -4057,10 +4636,53 @@ have to gather again.
|
|
|
4057
4636
|
requirement, where the scenario should move rather than go. Only the second is
|
|
4058
4637
|
really about cost, and it is the one where deleting the intent to save the
|
|
4059
4638
|
scenario inverts what the whole tool claims — that intent outlives its tests.
|
|
4060
|
-
Nothing here is urgent
|
|
4061
|
-
|
|
4062
|
-
|
|
4063
|
-
|
|
4639
|
+
Nothing here is urgent at 69. It is filed now because the moment to write the
|
|
4640
|
+
argument down is while the reasoning is available, and because a project whose
|
|
4641
|
+
requirement count only rises should have decided in advance what would make
|
|
4642
|
+
one leave.
|
|
4643
|
+
*The figures above were restated on 2026-08-08, and how they were wrong is
|
|
4644
|
+
itself the section's own warning happening.* As filed on 2026-08-03 this entry
|
|
4645
|
+
said "62 requirements and 160 scenarios, roughly 2.6 each" while closing on
|
|
4646
|
+
"the number is 61" — an inconsistency inside one entry, and the closing half
|
|
4647
|
+
was the right one. Re-measured at the entry's own commit with the current
|
|
4648
|
+
parser: **61 requirements, 156 scenarios**, so both opening figures were wrong
|
|
4649
|
+
on the day they were written, and only the ratio survived. Measured the same
|
|
4650
|
+
way today it is 69 and 192, and the method is the same one `verify self`
|
|
4651
|
+
prints — the control is that today's `self/` counted this way returns exactly
|
|
4652
|
+
what the runner does, which is what makes the 2026-08-03 number trustworthy. The
|
|
4653
|
+
cheap way — `grep -c 'scenario('` — answers 211 against today's true 192, and
|
|
4654
|
+
is what would have been reached for: comments and prose count, which is the
|
|
4655
|
+
`ATX-15` error again.
|
|
4656
|
+
**What moved in five days sharpens the argument rather than dating it.** +8
|
|
4657
|
+
requirements and +36 scenarios, concentrated in three sessions rather than
|
|
4658
|
+
accrued at a rate, so nothing here is a trend line. The part worth keeping is
|
|
4659
|
+
the ratio: 2.56 → 2.78. The cost of a requirement is not flat and it is not
|
|
4660
|
+
falling, which is the premise the whole entry rests on, and it is the first
|
|
4661
|
+
time this file has had two measurements of it to compare.
|
|
4662
|
+
|
|
4663
|
+
- **Whether `isSafeChangeName` is wide enough on Windows, where the filesystem
|
|
4664
|
+
strips trailing dots and spaces from a path component.** The guard's stated
|
|
4665
|
+
test is path safety rather than a character whitelist, and it refuses `''`,
|
|
4666
|
+
`'.'`, `'..'`, both separators and NUL. Win32 normalises `'.. '` to `'..'`
|
|
4667
|
+
below the API, so a name that passes every one of those clauses could still
|
|
4668
|
+
name the parent — and under `--eval` or `archive` the name reaches a module
|
|
4669
|
+
path that is then executed.
|
|
4670
|
+
**What is not known is whether that reproduces at all**, and it is the reason
|
|
4671
|
+
this is here rather than in `Planned`. `path.win32.join` was checked on Linux
|
|
4672
|
+
on 2026-08-09 and does *not* normalise it — `join('C:\\r\\changes', '.. ')`
|
|
4673
|
+
keeps the space — so the whole question is what the Win32 layer does with the
|
|
4674
|
+
resulting path, which no Linux measurement can answer. The repository's own
|
|
4675
|
+
rule applies: say which half is unmeasured rather than describing it as
|
|
4676
|
+
covered.
|
|
4677
|
+
**Even granting it, the reach is one level.** `changes/<name>/…` with the name
|
|
4678
|
+
eaten gives the project root, and going above it needs a second `..`, which
|
|
4679
|
+
needs a separator the guard already refuses. So the outcome is executing a
|
|
4680
|
+
file at another path *inside* the project, not an escape from it — which is
|
|
4681
|
+
what keeps this an open question about the guard's completeness rather than a
|
|
4682
|
+
filed defect. If it reproduces, the fix is one more clause of the same kind:
|
|
4683
|
+
refuse a name that is empty, `.` or `..` after trailing dots and spaces are
|
|
4684
|
+
trimmed. If it does not, the finding to keep is that the guard's clauses are
|
|
4685
|
+
the POSIX spelling of path safety and the file should say so.
|
|
4064
4686
|
|
|
4065
4687
|
## Considered and rejected
|
|
4066
4688
|
|
|
@@ -4072,6 +4694,223 @@ it sat between 0.2.0 and 0.1.7 for two releases, where standing still meant
|
|
|
4072
4694
|
sinking one version deeper each time a release was cut above it, and a rejection
|
|
4073
4695
|
filed under a version reads as belonging to it.
|
|
4074
4696
|
|
|
4697
|
+
### Refusing a half-loaded registry in `check` and `verify`, the way `cover` and `render` do
|
|
4698
|
+
|
|
4699
|
+
`cover` and `render` return early on `hasError(loadIssues)`,
|
|
4700
|
+
and the argument is recorded at both call sites: a report computed from a
|
|
4701
|
+
half-loaded registry is a lie, and a lie that reads as a pass is the worst
|
|
4702
|
+
shape it can take — `cover` in particular used to answer `ok: true` on a repo
|
|
4703
|
+
whose intent layer had stopped being readable, because an empty registry is
|
|
4704
|
+
fully covered. `check` and `verify` carry on regardless.
|
|
4705
|
+
*What that produces, measured* on a project with one unreadable `*.reqs.ts`:
|
|
4706
|
+
a `registry-invalid`, then an `orphan-test` for **every** scenario belonging to
|
|
4707
|
+
that file's requirements, plus `empty-spec`, `tests-red` and a
|
|
4708
|
+
`declared-not-run` per scenario — six derived findings from one cause. Worse,
|
|
4709
|
+
`orphan-test`'s fix hint ("Add it to the registry, or fix the id") is *wrong*
|
|
4710
|
+
for an id already sitting in the registry that failed to load, so the loudest
|
|
4711
|
+
advice in the report points at work that must not be done. In a multi-file
|
|
4712
|
+
registry it scales with the broken file's scenario count, and the one true
|
|
4713
|
+
finding is at the top where a wall of red pushes it out of view.
|
|
4714
|
+
*Why it is not simply the same call as `cover` and `render`.* Each of those
|
|
4715
|
+
produces exactly **one** artifact, and refusing to produce it is the whole of
|
|
4716
|
+
the decision. `check` is the command a pipeline runs first and its contract is
|
|
4717
|
+
breadth — returning three findings instead of nine because one of the nine was
|
|
4718
|
+
a load failure makes it quieter about problems it can still see perfectly well,
|
|
4719
|
+
which is the opposite of what it is for. `verify` has the same shape with a run
|
|
4720
|
+
attached.
|
|
4721
|
+
*What blocks the decision.* The narrower move — keep reporting everything
|
|
4722
|
+
except the findings **derived from** the missing registry, i.e. suppress
|
|
4723
|
+
`orphan-test` for ids whose prefix belongs to a file that failed to load —
|
|
4724
|
+
needs a prefix→file mapping for a file that never parsed, and that is exactly
|
|
4725
|
+
the file `loadRegistry` has no ids from. Recovering the mapping means reading
|
|
4726
|
+
the prefix out of something other than the registry's own keys (the file name,
|
|
4727
|
+
or a partial AST read that survives the failure), and whether either is
|
|
4728
|
+
trustworthy enough to gate a suppression on is unmeasured. Suppressing on the
|
|
4729
|
+
weaker signal "some registry file failed to load, so suppress all
|
|
4730
|
+
`orphan-test`" is the other candidate and is a strictly worse `check` on a
|
|
4731
|
+
multi-file registry, where the other files are fine and their orphans are real.
|
|
4732
|
+
**The wrong *advice* was separated out and fixed in `[0.7.0]`; the noise is
|
|
4733
|
+
fixed under `[Unreleased]`, and the blocker above was the wrong question.**
|
|
4734
|
+
The mapping this entry called for could not be recovered, and did not have to
|
|
4735
|
+
be: what a suppression needs is not the broken file's *prefix* but the ids it
|
|
4736
|
+
declares, and those are written in its source. Reading them back is a source
|
|
4737
|
+
read that returns ids and builds nothing, so the failure modes that matter —
|
|
4738
|
+
a truncated file, a missing default export, a non-literal value, a
|
|
4739
|
+
schema-invalid entry, a module that throws at import — all give them up. The
|
|
4740
|
+
six derived findings from one cause are now two ERRORs and one WARNING, and
|
|
4741
|
+
the collapse is per id rather than per prefix, which is strictly more precise
|
|
4742
|
+
than what was blocked: a genuine typo sharing a prefix with the broken file
|
|
4743
|
+
stays reported. **The lesson is the entry, not the fix.** It sat here for two
|
|
4744
|
+
releases behind a blocker stated as "the mapping is unavailable", which was
|
|
4745
|
+
true and was about a mapping nothing needed; the evidence it carried was about
|
|
4746
|
+
the right problem and the mechanism it proposed was not, and nothing rereads a
|
|
4747
|
+
blocker that has already been written down. That is the same failure the
|
|
4748
|
+
section's own preamble records about a figure nobody re-checks, one level up:
|
|
4749
|
+
the *shape* of the proposal goes stale as quietly as its numbers do.
|
|
4750
|
+
*What is left of this entry is only the original question* — whether `check`
|
|
4751
|
+
and `verify` should refuse a half-loaded registry outright, the way `cover`
|
|
4752
|
+
and `render` do — **and the evidence that motivated it has been spent.** The
|
|
4753
|
+
argument for refusing was never the load failure itself; it was the wall of
|
|
4754
|
+
derived findings burying it, and that wall is gone. What remains is the
|
|
4755
|
+
breadth argument recorded above, which points the other way for both commands.
|
|
4756
|
+
*What rejected it is that the argument for it was entirely the noise, and the
|
|
4757
|
+
noise is gone.* Refusing was never justified by the load failure itself — it was
|
|
4758
|
+
justified by the wall of derived findings burying it, and the collapse under
|
|
4759
|
+
`[Unreleased]` removed the wall without touching either command's breadth. What
|
|
4760
|
+
is left on the table is the breadth argument alone, and it points the other way
|
|
4761
|
+
for both: `check` is the command a pipeline runs first, its contract is seeing
|
|
4762
|
+
everything it can still see, and answering with three findings instead of nine
|
|
4763
|
+
because one of the nine was a load failure makes it quieter about problems it
|
|
4764
|
+
diagnoses perfectly well. `verify` is the same shape with a run attached.
|
|
4765
|
+
|
|
4766
|
+
*What would reopen it*, stated because a rejection whose condition is unwritten
|
|
4767
|
+
gets re-proposed on a hunch: another finding found to scale with a **broken
|
|
4768
|
+
registry** rather than with the number of real problems. That is the shape that
|
|
4769
|
+
justified this once and could again; a merely noisy report is not.
|
|
4770
|
+
|
|
4771
|
+
### An engine rule for a statement whose scenarios do not span its quantifier
|
|
4772
|
+
|
|
4773
|
+
The shape was found by reading, and it had already
|
|
4774
|
+
cost something: `ATX-37` says "**everything** the CLI writes to a terminal —
|
|
4775
|
+
a report, and the stack of a crash — MUST replace every control character",
|
|
4776
|
+
and `self/report.spec.ts` covers `formatIssues` and `formatCrash`. Those were
|
|
4777
|
+
the two functions that existed when it was written. `render` builds a document
|
|
4778
|
+
by concatenation that never went past the sanitiser, and a registry could drive
|
|
4779
|
+
the reviewer's terminal from `attest render` with no flag at all — shipped
|
|
4780
|
+
under `[0.7.0]` as the fix, but the *shape* is not fixed and is not
|
|
4781
|
+
specific to that requirement.
|
|
4782
|
+
The shape: a universal quantifier in the statement, a finite set of scenarios,
|
|
4783
|
+
and nothing anywhere that holds the two together. It reads as covered — the id
|
|
4784
|
+
has scenarios, `cover` is green, `check` is green — and the quantifier is the
|
|
4785
|
+
part no gate is looking at. Every one of the three mechanisms this project
|
|
4786
|
+
sells operates *below* it: coverage asks whether an id has a scenario,
|
|
4787
|
+
never-red asks whether that scenario discriminated, drift asks whether it reads
|
|
4788
|
+
the params. None asks whether the scenarios span what the sentence claims.
|
|
4789
|
+
**Everything this entry established about the shape has left it, and the two
|
|
4790
|
+
halves left by different doors.** The repo half shipped in `[0.7.0]` as the
|
|
4791
|
+
`QUANTIFIED` table it described — hand-judged, a row per flagged statement,
|
|
4792
|
+
every named scenario asserted to exist — and it closed the way an entry built
|
|
4793
|
+
from a strict read is supposed to: it found a live hole on `ATX-62`, the one
|
|
4794
|
+
requirement written specifically to avoid this shape, where the archive gate
|
|
4795
|
+
path was claimed by the statement and asserted by nothing. What that table
|
|
4796
|
+
taught is now **design §11**, both languages, because it is a permanent
|
|
4797
|
+
property of the design rather than a candidate: which surface a quantifier
|
|
4798
|
+
ranges over — `values`, `roster`, `sites` — decides whether scenarios can keep
|
|
4799
|
+
up with it, only `sites` can silently stop being true, and the rule an author
|
|
4800
|
+
can act on is *prefer a quantifier a scenario can iterate*. The adopter-facing
|
|
4801
|
+
half of that rule is in the `init` skill beside the two `params` rules it
|
|
4802
|
+
completes. Neither was predicted here: the entry proposed a detector and
|
|
4803
|
+
expected a two-way split, and what came back was a three-way one and an
|
|
4804
|
+
authoring rule, which is the more useful object and needed no engine at all.
|
|
4805
|
+
The table's own trap is recorded with it — the remedy of rewriting a statement
|
|
4806
|
+
to name its surfaces takes it out of the flagged set, so the table must admit
|
|
4807
|
+
rows for ids nothing flags, or it loses the binding exactly when the author
|
|
4808
|
+
does the right thing.
|
|
4809
|
+
*What stays here is the engine half alone, and it is undecided rather than
|
|
4810
|
+
blocked-with-evidence.* Detecting this means knowing which surface a statement
|
|
4811
|
+
names, which is prose judgement — the same judgement
|
|
4812
|
+
`tests/intent-rule-candidates.spec.ts` records as the reason obligation
|
|
4813
|
+
counting can never be an engine rule ("asserting a ground truth Attest cannot
|
|
4814
|
+
compute for someone else's requirements, which is exactly what §0 refuses").
|
|
4815
|
+
The detector that exists flags the quantifier and nothing more, and it misses
|
|
4816
|
+
the article form ("A registry that cannot be read MUST …") which is a universal
|
|
4817
|
+
too. So the shippable form may not exist, and what §11 ships instead is a
|
|
4818
|
+
counter-pressure that holds for one registry whose authors agreed to maintain
|
|
4819
|
+
it. *What rejected it is a measurement that was already in the repository when this
|
|
4820
|
+
entry was trimmed, and went unread.* The inline snapshot in
|
|
4821
|
+
`tests/intent-rule-candidates.spec.ts` scores the detector against the real
|
|
4822
|
+
corpus: it flags **30 of 70** requirements, and of those 30, **21 are `values`
|
|
4823
|
+
or `roster`** — correct by construction, nothing for an author to do. A shipped
|
|
4824
|
+
WARNING would therefore fire on 43% of this registry and be wrong about 70% of
|
|
4825
|
+
what it fired on, with no way for the author to discharge it except by rewording
|
|
4826
|
+
the statement.
|
|
4827
|
+
|
|
4828
|
+
*And rewording is both the evasion and the legitimate fix*, which is what makes
|
|
4829
|
+
the precision figure fatal rather than merely bad. `ATX-62` was improved by
|
|
4830
|
+
being rewritten to name its surfaces, and that rewrite takes it out of the
|
|
4831
|
+
flagged set by exactly the same mechanism a lazy reword would. The detector
|
|
4832
|
+
cannot tell them apart, so it grades the wording rather than the coverage. That
|
|
4833
|
+
is the failure design §11 records for every intent-layer detector tried — each
|
|
4834
|
+
"either saw almost nothing, or was cleared by deleting the word that triggered
|
|
4835
|
+
it" — and this one is squarely the second: `compound-requirement` sits
|
|
4836
|
+
at the other pole, never wrong and nearly blind, and the two together are the
|
|
4837
|
+
measured range of what a token detector over prose can do here.
|
|
4838
|
+
|
|
4839
|
+
*What survives is not this proposal.* The three-way classification and the
|
|
4840
|
+
authoring rule are design §11, the adopter-facing form is in the `init` skill,
|
|
4841
|
+
and the hand-judged table is the counter-pressure that did survive measurement.
|
|
4842
|
+
Reopening this needs a signal that is not the quantifier token — the reach limit
|
|
4843
|
+
is the detector's input, not its threshold, so tuning it is the measurement
|
|
4844
|
+
bending to the corpus.
|
|
4845
|
+
|
|
4846
|
+
### A precomputed analysis written to disk for the child run to read
|
|
4847
|
+
|
|
4848
|
+
`@ark/attest` analyses the whole project once, writes it to
|
|
4849
|
+
`.attest/assertions/*.json`, and has the test process do position lookups
|
|
4850
|
+
against that file instead of a compiler — because its assertions need a full
|
|
4851
|
+
`ts.Program`, which cannot be afforded per test. The shape maps onto `verify`
|
|
4852
|
+
exactly: the parent parses registries and specs, then `runAndCollect` spawns a
|
|
4853
|
+
Vitest run that currently shares nothing with that work.
|
|
4854
|
+
*What rejected it is that the case for it was never made and the case against it
|
|
4855
|
+
is structural.* There is **no
|
|
4856
|
+
measurement** saying parsing costs anything here — this reader is
|
|
4857
|
+
`createSourceFile` per file with no type checking, which is the cheap half of
|
|
4858
|
+
what arktype pays for, and the fan-out entry above measured `attest check`
|
|
4859
|
+
over 6000 spec files at 1.99 s total. Worse, a cache is a correctness hazard
|
|
4860
|
+
of precisely the kind this tool exists to detect: a stale analysis makes a
|
|
4861
|
+
**drift detector** report drift that has already been fixed, or miss drift
|
|
4862
|
+
that has just appeared, and it would do so silently. Any version of this needs
|
|
4863
|
+
an invalidation story stronger than a mtime before it is worth the seconds it
|
|
4864
|
+
saves, and the repo already has one cautionary precedent in the same
|
|
4865
|
+
direction — `bin/attest.js` loading `dist/` rather than `src/` is a cache with
|
|
4866
|
+
no invalidation at all, and it is in `CLAUDE.md`'s Gotchas because of how
|
|
4867
|
+
quietly it misleads. Note also what arktype gets wrong in the same file and
|
|
4868
|
+
what not to copy with it: the assertion cache and the queue of pending
|
|
4869
|
+
snapshot writes share one JSON document, so two things with unrelated
|
|
4870
|
+
lifetimes are invalidated together.
|
|
4871
|
+
|
|
4872
|
+
*What would reopen it* is a measurement, and the entry names which one: a
|
|
4873
|
+
parse cost large enough to be worth a cache, on a real project rather than a
|
|
4874
|
+
synthetic tree. Until that exists this is a correctness hazard bought with
|
|
4875
|
+
seconds nobody has shown are being spent, and the invalidation story would have
|
|
4876
|
+
to be stronger than an mtime before the trade is even worth pricing.
|
|
4877
|
+
|
|
4878
|
+
### Zod Mini (`zod/v4-mini`) for the registry schema
|
|
4879
|
+
|
|
4880
|
+
Evaluated alongside v4 (which shipped under `[Unreleased]`), on the same
|
|
4881
|
+
schema and the same 69-requirement registry, and it wins both numbers it is
|
|
4882
|
+
supposed to win:
|
|
4883
|
+
**0.249 ms** per parse against v3's 3.47 ms, and — the one v4 does not deliver —
|
|
4884
|
+
type instantiations down from **27,681 to 6,581**, check time 0.25 s → 0.14 s.
|
|
4885
|
+
|
|
4886
|
+
*What rejected it is that both winners are collected by the wrong party.* Mini
|
|
4887
|
+
exists for tree-shaken bundle size in a browser. This ships as a Node CLI loaded
|
|
4888
|
+
from `dist/`, where bundle size is not a constraint on anything — apply the
|
|
4889
|
+
deletion test to mini's benefit and nothing in this repository changes. The
|
|
4890
|
+
instantiation saving is real but it is paid to **this project's own
|
|
4891
|
+
`pnpm typecheck`**, not to any adopter, and it is 0.11 s off a build already
|
|
4892
|
+
measured in seconds. The runtime saving is 3.2 ms against a 390 ms command.
|
|
4893
|
+
|
|
4894
|
+
*And the cost lands on the file least able to afford it.* Mini's API is
|
|
4895
|
+
functional rather than chained:
|
|
4896
|
+
|
|
4897
|
+
```ts
|
|
4898
|
+
params: z.record(z.string(), paramValue).default({}) // v3 / v4
|
|
4899
|
+
params: z._default(z.record(z.string(), paramValue), {}) // mini
|
|
4900
|
+
```
|
|
4901
|
+
|
|
4902
|
+
`src/core/schema.ts` describes itself as the single source of truth for the
|
|
4903
|
+
shape of the intent layer, and it is written as one — the comments on
|
|
4904
|
+
`isPlainObject` and on the `params` union are longer than the code they sit
|
|
4905
|
+
over, because the file is read as a document by anyone deciding what a
|
|
4906
|
+
requirement may contain. Trading its legibility, and accepting an
|
|
4907
|
+
underscore-prefixed `_default` in it, buys 3 ms on a command nobody is waiting
|
|
4908
|
+
for.
|
|
4909
|
+
|
|
4910
|
+
This reopens only if something makes bundle size a constraint here — a browser
|
|
4911
|
+
or edge build of the registry reader, which nothing currently proposes — and not
|
|
4912
|
+
on a faster benchmark, since the benchmark was never what was in doubt.
|
|
4913
|
+
|
|
4075
4914
|
### Region-marked samples, so a partial quote could be bound too
|
|
4076
4915
|
|
|
4077
4916
|
Decided **yes** and filed under `Planned`, then killed by building the count it
|
|
@@ -4302,6 +5141,46 @@ skill file. A pointer is cheap enough to be resident, it is their edit in their
|
|
|
4302
5141
|
file, and it cannot go stale — the file it names is the one `init` keeps
|
|
4303
5142
|
current.
|
|
4304
5143
|
|
|
5144
|
+
### A sweep driving every exported `format*` with hostile input
|
|
5145
|
+
|
|
5146
|
+
The mechanism `ATX-37` sat under `Under consideration` waiting for, kept there
|
|
5147
|
+
on cost, and killed by measuring that cost rather than by paying it. The three
|
|
5148
|
+
sites it was aimed at are closed under `[Unreleased]` by hand-written scenarios;
|
|
5149
|
+
what is rejected is the roster, and the argument is that **`src/cli/report.ts`'s
|
|
5150
|
+
exports are the wrong roster in both directions at once.**
|
|
5151
|
+
|
|
5152
|
+
*Too wide.* Three of the eight exports cannot carry project-derived text at all:
|
|
5153
|
+
`summarize` and `formatScope` take `VerifyCounts`, which is four numbers, and
|
|
5154
|
+
`formatRepeats` prints a count, a `Level` and an `IssueCode` — both fixed
|
|
5155
|
+
rosters. A hostile fixture for those has nothing hostile to put in it, so a loop
|
|
5156
|
+
over the exports either forces three members that **cannot go red**, which is
|
|
5157
|
+
the defect this project ships a gate against, or carries an exclusion list —
|
|
5158
|
+
and an exclusion list is precisely what `ATX-15`'s sweep avoided by making the
|
|
5159
|
+
roster one the code owns.
|
|
5160
|
+
|
|
5161
|
+
*Too narrow.* The statement is over everything the CLI writes to a terminal, and
|
|
5162
|
+
every site that has escaped it so far was outside this module: the `status`
|
|
5163
|
+
header was built and printed in `cli/index.ts`, the loader's writes are in
|
|
5164
|
+
`core/loader.ts`, and the `render` document that `ATX-58` was carved out for is
|
|
5165
|
+
in `core/render.ts`. A roster of formatters is neither necessary nor sufficient
|
|
5166
|
+
for the sentence, and the reading that made it look sufficient — that the
|
|
5167
|
+
formatters are the whole surface — is the exact assumption the `status` header
|
|
5168
|
+
falsified when it was found.
|
|
5169
|
+
|
|
5170
|
+
*And the general form is the §0 wall.* "Every project-derived string
|
|
5171
|
+
interpolated into a terminal line goes through `inline`" needs to know which
|
|
5172
|
+
values are project-derived, which is prose judgement over a data flow. That is
|
|
5173
|
+
the ground truth `tests/intent-rule-candidates.spec.ts` records as the reason
|
|
5174
|
+
none of this can ever be an engine rule, and it does not become computable by
|
|
5175
|
+
being asked about this repository instead of someone else's.
|
|
5176
|
+
|
|
5177
|
+
*What keeps the property instead* is the `QUANTIFIED` row itself — hand-judged,
|
|
5178
|
+
one entry per site, every named scenario asserted to exist. That is a
|
|
5179
|
+
counter-pressure rather than a detector, it holds for one registry whose authors
|
|
5180
|
+
agreed to maintain it, and after this change exactly one of its `ATX-37` sites
|
|
5181
|
+
is open. This reopens only if a site escapes that a roster of formatters would
|
|
5182
|
+
have caught, which none of the four so far would have been.
|
|
5183
|
+
|
|
4305
5184
|
### A Codex target for `attest init`
|
|
4306
5185
|
|
|
4307
5186
|
Evaluated when `init` grew `--target` and shipped for Claude Code, Cursor and
|
|
@@ -4386,6 +5265,8 @@ requirement, and human review at propose is still the whole answer. What is
|
|
|
4386
5265
|
rejected is grouping as a way to assist it, and this reopens only on a
|
|
4387
5266
|
contradiction that grouping would have caught.
|
|
4388
5267
|
|
|
5268
|
+
[0.8.0]: https://gitlab.com/Pseudorca/attest/-/tags/v0.8.0
|
|
5269
|
+
[0.7.4]: https://gitlab.com/Pseudorca/attest/-/tags/v0.7.4
|
|
4389
5270
|
[0.7.3]: https://gitlab.com/Pseudorca/attest/-/tags/v0.7.3
|
|
4390
5271
|
[0.7.2]: https://gitlab.com/Pseudorca/attest/-/tags/v0.7.2
|
|
4391
5272
|
[0.7.1]: https://gitlab.com/Pseudorca/attest/-/tags/v0.7.1
|