@am_shork/attest 0.3.0 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +394 -2
- package/README.md +47 -264
- package/dist/cli/index.js +1 -1
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/json.d.ts.map +1 -1
- package/dist/cli/json.js +25 -1
- package/dist/cli/json.js.map +1 -1
- package/dist/cli/report.d.ts.map +1 -1
- package/dist/cli/report.js +78 -5
- package/dist/cli/report.js.map +1 -1
- package/dist/core/apply.d.ts.map +1 -1
- package/dist/core/apply.js.map +1 -1
- package/dist/core/docs.d.ts +23 -0
- package/dist/core/docs.d.ts.map +1 -0
- package/dist/core/docs.js +91 -0
- package/dist/core/docs.js.map +1 -0
- package/dist/core/loader.d.ts.map +1 -1
- package/dist/core/loader.js +18 -1
- package/dist/core/loader.js.map +1 -1
- package/dist/core/locate.d.ts +18 -1
- package/dist/core/locate.d.ts.map +1 -1
- package/dist/core/locate.js +68 -4
- package/dist/core/locate.js.map +1 -1
- package/dist/core/pipeline.d.ts +12 -3
- package/dist/core/pipeline.d.ts.map +1 -1
- package/dist/core/pipeline.js +22 -1
- package/dist/core/pipeline.js.map +1 -1
- package/dist/core/red-record.d.ts +6 -0
- package/dist/core/red-record.d.ts.map +1 -1
- package/dist/core/red-record.js +31 -2
- package/dist/core/red-record.js.map +1 -1
- package/dist/core/runner.d.ts +1 -1
- package/dist/core/runner.d.ts.map +1 -1
- package/dist/core/runner.js +20 -5
- package/dist/core/runner.js.map +1 -1
- package/dist/core/static-registry.d.ts.map +1 -1
- package/dist/core/static-registry.js +5 -0
- package/dist/core/static-registry.js.map +1 -1
- package/dist/core/types.d.ts +22 -1
- package/dist/core/types.d.ts.map +1 -1
- package/dist/core/types.js.map +1 -1
- package/dist/core/version.d.ts.map +1 -0
- package/dist/{cli → core}/version.js +5 -1
- package/dist/core/version.js.map +1 -0
- package/package.json +5 -5
- package/dist/cli/version.d.ts.map +0 -1
- package/dist/cli/version.js.map +0 -1
- /package/dist/{cli → core}/version.d.ts +0 -0
package/CHANGELOG.md
CHANGED
|
@@ -13,7 +13,319 @@ 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
|
-
## [
|
|
16
|
+
## [0.4.1] - 2026-07-29
|
|
17
|
+
|
|
18
|
+
Every diagnostic now links to a section that explains it, and the README is a
|
|
19
|
+
README again — the reference it had grown into has a page of its own, in both
|
|
20
|
+
languages, saying which version each part of it arrived in. No behaviour a CI
|
|
21
|
+
step branches on changed; the human report gains one line per issue.
|
|
22
|
+
|
|
23
|
+
### Added
|
|
24
|
+
|
|
25
|
+
- **A troubleshooting document, with one section per issue code — and every
|
|
26
|
+
diagnostic links to its own.** A diagnostic names a rule and states a fix in
|
|
27
|
+
one line, which is the right size for a reader who already knows the rule and
|
|
28
|
+
the wrong size for the one who does not — and the second reader is the one
|
|
29
|
+
adopting Attest, who meets six codes at once on their first run. The knowledge
|
|
30
|
+
was not missing so much as filed where nobody looks during a failure: the
|
|
31
|
+
cross-platform traps (a backslash is an escape in a glob; Vitest's 5-second
|
|
32
|
+
default against a scenario that starts a subprocess) lived in `CLAUDE.md`,
|
|
33
|
+
which ships with no release, and the rationale lived in a 450-line README
|
|
34
|
+
nobody greps mid-failure. Both languages, as always
|
|
35
|
+
(`docs/{en,zh}/troubleshooting.md`).
|
|
36
|
+
|
|
37
|
+
What makes this cheap to keep true is that `code` was already the
|
|
38
|
+
machine-readable name of a rule, so the section heading is the code verbatim
|
|
39
|
+
and the link is *derived*: there is no second string to keep in step, and so
|
|
40
|
+
no way to ship a diagnostic pointing at the wrong section. The remaining
|
|
41
|
+
failure mode — a section that does not exist — is closed from the other end:
|
|
42
|
+
`Issue.code` is narrowed from `string` to the roster in `core/docs.ts`, which
|
|
43
|
+
the suite asserts against both documents. An undocumented diagnostic is now a
|
|
44
|
+
compile error. (That narrowing found two families of codes a regex over the
|
|
45
|
+
source had missed, because they are built through a helper rather than written
|
|
46
|
+
at the issue site.) The two readers that restate a *subset* of the roster —
|
|
47
|
+
`StaticReadCode`, `DeltaReadCode` — are now asserted to be subsets of it in
|
|
48
|
+
`tests/issue-code-types.spec-d.ts`. That was already caught, but downstream
|
|
49
|
+
and by accident: the failure surfaced where the code is assigned into an
|
|
50
|
+
`Issue`, several files from the two lines that disagree.
|
|
51
|
+
|
|
52
|
+
Links are pinned to `v<version>` — the tag of the build that printed the
|
|
53
|
+
diagnostic — not to `main`. The two failure modes are not the same size. An
|
|
54
|
+
unresolved tag exists only between a release commit and its push, which is a
|
|
55
|
+
version nobody has installed, so no build in anyone's `node_modules` prints
|
|
56
|
+
those links. An unpinned link is dereferenced by every installed copy forever,
|
|
57
|
+
and the first breaking change to a code turns years of shipped builds into
|
|
58
|
+
links that miss — or worse, land on a section confidently describing a rule
|
|
59
|
+
that has since been redefined. (`main` remains the fallback for the one case
|
|
60
|
+
where pinning is impossible rather than imprecise: an unreadable manifest.)
|
|
61
|
+
|
|
62
|
+
- **`docsUrl` on every issue in `--json`.** Additive, so no `schemaVersion`
|
|
63
|
+
bump. It is attached in the one envelope every report is built through —
|
|
64
|
+
including the `internal-error` crash path — rather than at the ~26 sites that
|
|
65
|
+
construct an `Issue`, for the same reason `hasError` is one function. It is
|
|
66
|
+
for the human reading a CI log; a consumer should still branch on `code`,
|
|
67
|
+
which is the more precise form of the same information.
|
|
68
|
+
|
|
69
|
+
### Changed
|
|
70
|
+
|
|
71
|
+
- **Every issue in the human report gains a line: `→ <docs url>`, dim, under the
|
|
72
|
+
message.** The one behaviour change an existing invocation can notice. No exit
|
|
73
|
+
code moves, no verdict moves, no input that passed now fails, and `--json` is
|
|
74
|
+
untouched by it — but a snapshot test over `attest`'s stdout will need
|
|
75
|
+
re-recording. That report is prose for humans by the same rule that makes
|
|
76
|
+
`Issue.message` prose (see *Versioning* above); the machine surface is
|
|
77
|
+
`--json`, which is where a CI step should have been looking.
|
|
78
|
+
|
|
79
|
+
- **`Issue.code` is typed `IssueCode`, not `string`.** Breaking only for code
|
|
80
|
+
that *constructs* an `Issue` — reading one, which is what `--json` consumers
|
|
81
|
+
do, gets strictly more type information than before. Nothing in the CLI
|
|
82
|
+
surface, the report shape or the exit codes changes.
|
|
83
|
+
|
|
84
|
+
`SCHEMA_VERSION` stays at 1 for this too, not only for `docsUrl`: the emitted
|
|
85
|
+
values are the same strings they always were, so nothing on the wire changed
|
|
86
|
+
and no consumer can tell the difference. The narrowing is a compile-time
|
|
87
|
+
constraint on this repository, not a report-shape change.
|
|
88
|
+
|
|
89
|
+
- **The README is a README again; the reference moved to
|
|
90
|
+
`docs/{en,zh}/cli-reference.md`.** It had become four documents wearing one
|
|
91
|
+
hat — a pitch, a tutorial, a CLI reference and a running design argument — at
|
|
92
|
+
470 lines, which is long past the point where a newcomer reads it and well
|
|
93
|
+
past where anyone looks up a flag in it. It is now 199: the pitch, a
|
|
94
|
+
five-minute start, and a table of where everything else lives. Nothing was
|
|
95
|
+
deleted. Per-command behaviour, `--eval`, `--vitest-config`, the run scope and
|
|
96
|
+
the whole `--json` shape are in the new reference; the rationale for the
|
|
97
|
+
diagnostics that were argued through in the README (`registry-not-static`,
|
|
98
|
+
`duplicate-prefix`, `empty-spec`, `stale-spec-doc`) already had a better home
|
|
99
|
+
in the troubleshooting document, so the reference states the rule and links to
|
|
100
|
+
it rather than restating the argument a third time.
|
|
101
|
+
|
|
102
|
+
Each section of the reference that has changed since it first shipped now
|
|
103
|
+
carries a collapsed **History** table. Attest is 0.x and moves fast enough
|
|
104
|
+
that a page describes the latest build to a reader who installed an earlier
|
|
105
|
+
one, with nothing to tell them which half applies — `--vitest-config` has
|
|
106
|
+
existed since 0.1.5, the run has been scoped to attesting spec files only
|
|
107
|
+
since 0.2.0, and the registry has had to be a literal only since 0.2.0. The
|
|
108
|
+
rows were written from this file. A gate checks that every version they name
|
|
109
|
+
is a heading in it, so a row cannot cite a version that was never cut; it
|
|
110
|
+
cannot check that a row is *true*, which still needs a reader with the
|
|
111
|
+
changelog open. `Unreleased` counts as a heading while one exists — and stops
|
|
112
|
+
counting when a release removes it, which is what makes renumbering those rows
|
|
113
|
+
part of cutting the release rather than something to remember.
|
|
114
|
+
|
|
115
|
+
The two documentation gates widened with it: they scanned `README.md` alone,
|
|
116
|
+
and the JSON sample they exist to catch had just moved out from under them.
|
|
117
|
+
They now enumerate `README.md` plus every `*.md` in both language directories,
|
|
118
|
+
so a page added later is covered by default rather than by remembering.
|
|
119
|
+
|
|
120
|
+
- **`packageVersion()` moved from `src/cli/version.ts` to `src/core/version.ts`.**
|
|
121
|
+
It answers "which build is this", which `core/docs.ts` now has to ask in order
|
|
122
|
+
to pin a link — not a CLI concern. Neither path is in the `exports` map, so
|
|
123
|
+
nothing installable can have been importing either.
|
|
124
|
+
|
|
125
|
+
## [0.4.0] - 2026-07-28
|
|
126
|
+
|
|
127
|
+
Reading a registry no longer opens a port or lets the repository under test write
|
|
128
|
+
to the reader's terminal, the id prefix finally has something that notices two
|
|
129
|
+
files claiming it, and the peer range now says what CI actually proves rather
|
|
130
|
+
than what the tool was hoped to work on.
|
|
131
|
+
|
|
132
|
+
### Added
|
|
133
|
+
|
|
134
|
+
- **Two registry files claiming one id prefix is now an ERROR.** The id grammar
|
|
135
|
+
is one flat space, `^[A-Z]+-\d+$`, and the prefix inside it was habit rather
|
|
136
|
+
than scope: nothing allocated it, reserved it, or noticed that two files had
|
|
137
|
+
picked the same one (design §11). Every cross-file check there was keys on the
|
|
138
|
+
whole id, so `AUTH-3` in `auth.reqs.ts` and `AUTH-9` in `session.reqs.ts` was
|
|
139
|
+
a clean `check` — two owners of one space, and the first anyone would hear of
|
|
140
|
+
it is the `duplicate-requirement` on the day the counters happen to meet, by
|
|
141
|
+
which point both files have a history. `loadRegistry` (`locate.ts`) already
|
|
142
|
+
held the `(file, id)` pair in the fold where `duplicate-requirement` is
|
|
143
|
+
raised, so the whole mechanism is a prefix→file map in that same loop, and
|
|
144
|
+
because it is that fold, every command that reads intent gets the diagnostic:
|
|
145
|
+
`check`, `cover`, `render`, `status`, `verify`, `archive`.
|
|
146
|
+
**What the issue carries.** `file` is the file that arrived *second* in sorted
|
|
147
|
+
order, matching `duplicate-requirement`, which also files against the file
|
|
148
|
+
being folded — and the file that claimed the prefix first is named in the
|
|
149
|
+
message, because a collision is a fact about a pair, an `Issue` carries one
|
|
150
|
+
path, and an error naming only one end leaves its reader grepping the
|
|
151
|
+
repository to find the other. There is **no `reqId`**: the ids involved are
|
|
152
|
+
individually valid and it is the space above them that has two owners, so
|
|
153
|
+
there is no single requirement the issue is about. It is raised **once per
|
|
154
|
+
colliding prefix per file**, not once per requirement — a registry with forty
|
|
155
|
+
ids under a taken prefix reports one sentence, not forty. Where a duplicate id
|
|
156
|
+
is also present both fire, which is not a restatement: sharing `A-1` implies
|
|
157
|
+
sharing `A-`, and the converse — the case this exists for — does not hold.
|
|
158
|
+
Now a requirement (ATX-39).
|
|
159
|
+
**Not a behaviour change for anything that passes today**, and that was
|
|
160
|
+
checked rather than assumed: all six `*.reqs.ts` in this repo sit in six
|
|
161
|
+
separate project roots holding one registry file each, so no root has two
|
|
162
|
+
files to collide. It is still a new ERROR on input that used to pass, so an
|
|
163
|
+
adopter whose registries share a prefix goes red on upgrade with nothing else
|
|
164
|
+
changing — deliberately, since that is the state the rule exists to report,
|
|
165
|
+
and the fix is a prefix rather than a migration. `SCHEMA_VERSION` stays 1: a
|
|
166
|
+
new `code` value is additive, and neither the envelope nor the `Issue`
|
|
167
|
+
interface moved.
|
|
168
|
+
The other half of the rule this was cut from — that a prefix must match its
|
|
169
|
+
file's name — was **rejected**; see "Considered and rejected".
|
|
170
|
+
|
|
171
|
+
### Security
|
|
172
|
+
|
|
173
|
+
- **Reading a registry opened a network port.** The loader borrows Vite to
|
|
174
|
+
transform and evaluate a `*.reqs.ts`, and `middlewareMode` suppresses Vite's
|
|
175
|
+
HTTP server but not its HMR WebSocket server — so `verify`, `archive`, and any
|
|
176
|
+
read command under `--eval` bound port 24678 on *every* interface for the
|
|
177
|
+
length of the run, on developer machines and CI runners alike, for a channel
|
|
178
|
+
nothing here subscribes to. `server.ws` is now `false`, which is the option
|
|
179
|
+
that actually closes it: `hmr: false` does not, because the socket is created
|
|
180
|
+
before the HMR option is consulted. The visible symptom was two concurrent
|
|
181
|
+
runs colliding and printing `WebSocket server error: Port is already in use`
|
|
182
|
+
into the middle of a report, through a log level that was supposed to have
|
|
183
|
+
silenced the loader. Now a requirement (ATX-36), asserted as a count of
|
|
184
|
+
listening handles rather than a port number, because the port is Vite's to
|
|
185
|
+
change and the property Attest needs is that reading intent is not a network
|
|
186
|
+
operation.
|
|
187
|
+
- **The human report let the repository under test write to the reader's
|
|
188
|
+
terminal.** Scenario names and file paths are quoted verbatim into
|
|
189
|
+
`orphan-test`, `declared-not-run` and `never-red`, and the parser hands back
|
|
190
|
+
the *cooked* string — so an escape written in a spec file arrived at the
|
|
191
|
+
terminal as a real control byte. On a fork merge request the author of that
|
|
192
|
+
string is not the person reading the report, and the payload erases lines,
|
|
193
|
+
repaints a red verdict green, or retitles the window. Every control character
|
|
194
|
+
except the newline is now replaced with a space, in the message, the file
|
|
195
|
+
path, the code, and the ids and change name a `status` report prints. The
|
|
196
|
+
newline survives, because `registry-invalid` legitimately carries a list of
|
|
197
|
+
field errors — but the continuation is indented, so injected text cannot
|
|
198
|
+
occupy the column a genuine header does. `--json` was never affected
|
|
199
|
+
(serialising escapes the same characters), which is exactly why this had to be
|
|
200
|
+
fixed on the side people read. Now a requirement (ATX-37).
|
|
201
|
+
- **Dependency advisories, and a gate so the next ones are not found by hand.**
|
|
202
|
+
The dev toolchain moved to Vite 8 and Vitest 4, clearing a high-severity
|
|
203
|
+
`server.fs.deny` bypass on Windows alternate paths, an NTLMv2 hash disclosure
|
|
204
|
+
via UNC path handling, two moderate path-traversal/dev-server advisories, and
|
|
205
|
+
a critical Vitest UI advisory that this tool never reached but shipped in its
|
|
206
|
+
lockfile regardless. `brace-expansion` is pinned past its DoS advisory through
|
|
207
|
+
a `pnpm.overrides` entry, since it arrives only through ESLint. A new `audit`
|
|
208
|
+
CI job runs `pnpm audit --audit-level=high`, not `allow_failure`: it is the
|
|
209
|
+
one job here that can go red on a commit that changed nothing, which is the
|
|
210
|
+
point — a dependency becomes vulnerable on the day it is disclosed, not on the
|
|
211
|
+
day someone next edits `package.json`. This also closes a real coverage gap:
|
|
212
|
+
`peerDependencies` has claimed Vite 8 and Vitest 4 since 0.3.0 while CI only
|
|
213
|
+
ever ran Vite 5 and Vitest 2.
|
|
214
|
+
|
|
215
|
+
### Fixed
|
|
216
|
+
|
|
217
|
+
- **`--eval` crashed on a registry the reading path diagnoses properly.** The
|
|
218
|
+
evaluating reader called a module's default export a registry because it was
|
|
219
|
+
an object — an assertion standing in for a check, on the one path where the
|
|
220
|
+
value has not already been through `defineRequirements`. A file that
|
|
221
|
+
default-exports a plain literal instead of calling it therefore arrived at the
|
|
222
|
+
validator unchecked, where reading `.statement` off a number threw: `--eval`
|
|
223
|
+
answered with an `internal-error` carrying a raw TypeError, for a file the
|
|
224
|
+
static reader reports with a code, a line and a fix hint — and that fix hint
|
|
225
|
+
is "pass `--eval`", so the documented way out of one diagnostic led into a
|
|
226
|
+
crash. The evaluating path now hands what it loaded to the same
|
|
227
|
+
`RegistrySchema`, so both readers refuse the same file. Behaviour change for
|
|
228
|
+
anyone branching on the code: this input used to produce `internal-error` and
|
|
229
|
+
now produces `registry-invalid`; both are ERRORs, so no exit code moves. It is
|
|
230
|
+
also a stricter read than before — a registry that `--eval` accepted only
|
|
231
|
+
because nothing looked at it is now rejected, and unknown fields are stripped
|
|
232
|
+
by the schema exactly as `defineRequirements` already strips them. Now a
|
|
233
|
+
requirement (ATX-38), kept apart from ATX-17: that one promises the escape
|
|
234
|
+
hatch exists and is honest about its cost, and a reader can satisfy it
|
|
235
|
+
completely while validating nothing it evaluates — which is the state this
|
|
236
|
+
was in.
|
|
237
|
+
- **A corrupt first-run record leaked a value that is not an outcome into the
|
|
238
|
+
report.** Reading `changes/<name>/first-run.json` asserted its contents rather
|
|
239
|
+
than checking them: the guard tested that `firstRun` was an object and was
|
|
240
|
+
*declared* to return the record type, so a hand-edited or half-written file
|
|
241
|
+
put an arbitrary string where `pass`/`fail` is the whole domain. It travelled
|
|
242
|
+
— out through `status`'s `progress[].scenarios[].firstRun`, whose declared
|
|
243
|
+
type is `Outcome | null`, into the `--json` contract, and into a human row
|
|
244
|
+
whose hint matched neither "passed first" nor "never run" and so printed
|
|
245
|
+
blank. The archive gate was never at risk, because `hasRecordedRed` compares
|
|
246
|
+
against `fail` and anything unrecognised blocks exactly like a missing record;
|
|
247
|
+
this closes the report, not the gate. A record that does not validate is now
|
|
248
|
+
discarded whole, which is the existing "no more permissive than a missing
|
|
249
|
+
file" rule one step further: the cost is a stage-1 run to re-observe, and
|
|
250
|
+
keeping the entries that happened to parse would present a partial history as
|
|
251
|
+
if it were the whole one. Behaviour change confined to corrupt files: that
|
|
252
|
+
`--json` field now reads `null` where it used to echo whatever was on disk.
|
|
253
|
+
- **The test verdict could fail open.** `runAndCollect` read the failure count
|
|
254
|
+
through `getCountOfFailedTests?.() ?? 0`, but the method is required on
|
|
255
|
+
Vitest's state — so the guard was dead at the type level, and had it ever
|
|
256
|
+
become live it defaulted the wrong way, reading a missing API as zero failures
|
|
257
|
+
and turning a red suite green. Latent rather than observed: no released
|
|
258
|
+
version reported a wrong verdict because of it. This is the one boolean in the
|
|
259
|
+
engine that may never fail open.
|
|
260
|
+
- **`archive` answered `internal-error` for a file `status` diagnoses
|
|
261
|
+
properly.** A `requirements.delta.ts` that loads but default-exports nothing
|
|
262
|
+
reached `applyDelta` as `undefined`, where reading `.renamed` off it threw a
|
|
263
|
+
TypeError — so the gate crashed out through the `--json` envelope's crash path
|
|
264
|
+
instead of reporting `change-not-found`, and the fix hint went with it.
|
|
265
|
+
`archive` now makes the same check `status` already made. Behaviour change for
|
|
266
|
+
anyone branching on the code: this input used to produce `internal-error` and
|
|
267
|
+
now produces `change-not-found`. Both are ERRORs, so no exit code moves.
|
|
268
|
+
- **A sibling change directory containing a backslash silently widened the
|
|
269
|
+
archive gate.** `escapeGlob` escaped every glob metacharacter except
|
|
270
|
+
glob's *own* escape character, so a name like `a\b` — legal on POSIX — did not
|
|
271
|
+
merely go unescaped: the backslash escaped the character after it, and
|
|
272
|
+
`**/changes/a\b/**` matched `ab` and never the directory. That proposal's
|
|
273
|
+
specs then joined the run of the one check that decides whether a change is
|
|
274
|
+
done. Covered by ATX-13, which already stated the property.
|
|
275
|
+
- **A self scenario that compiled a TypeScript program had no explicit
|
|
276
|
+
timeout**, so it fit inside Vitest's 5-second isolated-run default until the
|
|
277
|
+
toolchain moved and then failed as `tests-red` with no assertion behind it —
|
|
278
|
+
the failure mode the repo's other long scenarios already carry a timeout to
|
|
279
|
+
avoid.
|
|
280
|
+
|
|
281
|
+
### Changed
|
|
282
|
+
|
|
283
|
+
- **BREAKING: the peer range is now `vite ^8` and `vitest ^4`, down from six
|
|
284
|
+
majors.** `^5 || ^6 || ^7 || ^8` and `^2 || ^3 || ^4` was a claim about six
|
|
285
|
+
combinations that no job ever ran end to end. What it looked like from inside
|
|
286
|
+
the repo — the security entry above says as much — was that the *top* was
|
|
287
|
+
untested; the fixture told a different story. `fixtures/consumer` pinned
|
|
288
|
+
`vitest ^2.0.0` / `vite ^5.4.0`, and the consumer job is the **only** thing in
|
|
289
|
+
this pipeline that resolves peers at all, so the two ends were each proven by
|
|
290
|
+
one job apiece and `vite ^6`/`^7` and `vitest ^3` were proven by nothing.
|
|
291
|
+
Advertising a version nobody runs is the same defect the `audit` job was added
|
|
292
|
+
to close, one layer out: the failure surfaces in an adopter's repository, not
|
|
293
|
+
here.
|
|
294
|
+
**Two of them had already stopped being merely untested.** `verify` reads
|
|
295
|
+
`startVitest` as `Promise<Vitest>` — the dead null guard removed above was
|
|
296
|
+
there for the older signature — and ATX-36 asserts a listening-handle count
|
|
297
|
+
against whichever Vite is installed, so on any other major the socket
|
|
298
|
+
guarantee is a claim rather than a measurement. Neither is a defect that would
|
|
299
|
+
announce itself; both fail in the direction of looking fine.
|
|
300
|
+
`fixtures/consumer` moves to `vitest ^4` / `vite ^8` with the range, because
|
|
301
|
+
npm refuses an unmet peer and the fixture exists to install the way a user
|
|
302
|
+
does. That does cost something real and it is worth naming: the consumer job
|
|
303
|
+
no longer exercises a second Vite/Vitest pair, so this trades a spot-check of
|
|
304
|
+
one old combination for a claim that matches the tested one. A matrix is what
|
|
305
|
+
buys the breadth back, and nothing here pretends this is that.
|
|
306
|
+
**Who this breaks:** anyone on Vite 5–7 or Vitest 2–3 fails to install on
|
|
307
|
+
upgrade, with no deprecation window. Under this file's Versioning rule that is
|
|
308
|
+
a minor, which is what 0.4.0 is. The engine may well still work on some of
|
|
309
|
+
them — that is precisely the claim there is no evidence for, and `0.3.x`
|
|
310
|
+
remains the version whose manifest makes it.
|
|
311
|
+
|
|
312
|
+
- **Type-aware linting, and `exactOptionalPropertyTypes`.** The three defects
|
|
313
|
+
above are one shape — an assertion standing in for a check — and the untyped
|
|
314
|
+
ESLint preset cannot see any of them, because deciding whether an `as` or a
|
|
315
|
+
`?.` is doing work needs the checker. `recommendedTypeChecked` now runs over
|
|
316
|
+
`src`, `tests` and `self`, with `no-unnecessary-condition` promoted out of the
|
|
317
|
+
strict preset; the rest of that preset is deliberately not adopted, since its
|
|
318
|
+
bulk is `no-non-null-assertion`, and here `!` is the counterpart of
|
|
319
|
+
`noUncheckedIndexedAccess` — banning it would trade a compiler-enforced
|
|
320
|
+
guarantee for a lint-enforced style. `fixtures/**` stays on the untyped preset
|
|
321
|
+
because several fixtures are invalid on purpose. Turning the rules on removed
|
|
322
|
+
a double cast that hid Vitest's task-tree shape from the compiler, a redundant
|
|
323
|
+
assertion in the delta reader that made an unvalidated value look checked, and
|
|
324
|
+
a dead `startVitest` null guard left over from an older Vitest signature.
|
|
325
|
+
`exactOptionalPropertyTypes` is now on as well: the codebase already followed
|
|
326
|
+
it by hand — the `...(x === undefined ? {} : { key: x })` idiom appears in
|
|
327
|
+
every Issue builder — so this makes an existing discipline the compiler's job
|
|
328
|
+
rather than review's. No `--json` shape changed, so `SCHEMA_VERSION` stays 1.
|
|
17
329
|
|
|
18
330
|
## [0.3.0] - 2026-07-27
|
|
19
331
|
|
|
@@ -1126,6 +1438,31 @@ it sat between 0.2.0 and 0.1.7 for two releases, where standing still meant
|
|
|
1126
1438
|
sinking one version deeper each time a release was cut above it, and a rejection
|
|
1127
1439
|
filed under a version reads as belonging to it.
|
|
1128
1440
|
|
|
1441
|
+
- **A requirement's id prefix must match its registry file's name.** The other
|
|
1442
|
+
half of the rule that shipped as `duplicate-prefix` (see `[0.4.0]`), and
|
|
1443
|
+
the half that does not pay for itself. What killed it is that the corpus
|
|
1444
|
+
already conforms: every fixture registry matches (`auth.reqs.ts` → `AUTH-`,
|
|
1445
|
+
`core.reqs.ts` → `CORE-`), so the rule would detect nothing anywhere it was
|
|
1446
|
+
measured except one file — `self/requirements/attest.reqs.ts`, which holds
|
|
1447
|
+
`ATX-*`. Its first act would therefore be to rename this repo's own registry
|
|
1448
|
+
and every `self/*.spec.ts` that imports it, and its first act in an adopter's
|
|
1449
|
+
repository would be the same thing to theirs. That is rejecting input valid
|
|
1450
|
+
today, a minor bump under this file's Versioning rule, in exchange for making
|
|
1451
|
+
the prefix *guessable from the path* — which matters only to a reader already
|
|
1452
|
+
looking at the file, and who could have read the ids in it. The collision rule
|
|
1453
|
+
delivers what the gap is actually about, which is two owners of one space, and
|
|
1454
|
+
it delivers it without a rename because it fires on a relationship between
|
|
1455
|
+
files rather than on a spelling. Reconsider only with evidence that a reader or
|
|
1456
|
+
a tool needs the prefix before opening the file — a naming *convention* stays
|
|
1457
|
+
the right shape for this, and the convention is what `init` and the docs
|
|
1458
|
+
already teach.
|
|
1459
|
+
What is not a reason to revisit it: the objection that stalled the collision
|
|
1460
|
+
rule while both were queued together — that it guards a convention nothing had
|
|
1461
|
+
yet violated — was answered by shipping it, since a rule that costs no
|
|
1462
|
+
migration and no concept is worth having before the first collision rather
|
|
1463
|
+
than after it. That argument does not transfer here, because this rule's cost
|
|
1464
|
+
is a migration by construction.
|
|
1465
|
+
|
|
1129
1466
|
- **Two gaps in the intent layer: nothing resists a bloated requirement, and
|
|
1130
1467
|
nothing resists a duplicated one.** The gaps themselves are described in design
|
|
1131
1468
|
§11, which is where a permanent property of the design belongs. What is kept
|
|
@@ -1272,7 +1609,62 @@ filed under a version reads as belonging to it.
|
|
|
1272
1609
|
answer for a Codex user, and this reopens only if Codex grows a project-scoped
|
|
1273
1610
|
path of its own.
|
|
1274
1611
|
|
|
1275
|
-
|
|
1612
|
+
- **A committed snapshot of the params, to give `check` detection power over a
|
|
1613
|
+
value that moves.** Design §11 records the measurement this answers: editing a
|
|
1614
|
+
param (`'main'` → `'trunk'`, one entry dropped from a nine-element list) leaves
|
|
1615
|
+
`attest check` at `✓ No issues`, because the single source makes a value
|
|
1616
|
+
impossible to *diverge*, which is not the claim that it is flagged when it
|
|
1617
|
+
*moves*. The candidate was the shape `render --check` already proves — a
|
|
1618
|
+
`params.lock` beside the registry, or `check --params-check` — and the
|
|
1619
|
+
mechanism would have worked.
|
|
1620
|
+
*What rejected it: the precedent is the objection.* §9 states when a freshness
|
|
1621
|
+
gate earns its place, and a rendering earns it because it is a function of the
|
|
1622
|
+
*user's registry*, so staleness means the user moved intent without
|
|
1623
|
+
regenerating — a real signal with the fix in their hands. A params snapshot is
|
|
1624
|
+
a function of the registry too, but the population it fires on is different:
|
|
1625
|
+
**every legitimate param edit turns it red, and the fix is a commit containing
|
|
1626
|
+
no decision.** That is precisely the chore §9 declined to ship for `init`, and
|
|
1627
|
+
it is worse here because of frequency — intent is rendered rarely, while params
|
|
1628
|
+
move whenever a value moves, which is the entire reason they are params. The
|
|
1629
|
+
gate would spend its whole life red about correct edits, and a gate that is
|
|
1630
|
+
usually wrong about staleness is the failure mode §9 names as worse than no
|
|
1631
|
+
gate.
|
|
1632
|
+
*What the gap keeps instead.* §6's mechanism 3 already exists to make this
|
|
1633
|
+
exact absence visible: `possible-drift` fires when a requirement owns params
|
|
1634
|
+
that no covering scenario reads, which is the only condition under which a
|
|
1635
|
+
moved param goes unnoticed. The mechanism that guards a param is a scenario
|
|
1636
|
+
reading it, not a file recording it. This reopens only on an adoption report
|
|
1637
|
+
where a param moved, no scenario caught it, and `possible-drift` was **not**
|
|
1638
|
+
already printing — because if it was printing, what failed was reading the
|
|
1639
|
+
report, and a second gate does not fix that.
|
|
1640
|
+
|
|
1641
|
+
- **Grouping `render`'s output by prefix, so a reviewer sees contradicting
|
|
1642
|
+
requirements together.** The *detection* half of this is already rejected above
|
|
1643
|
+
(every automatic candidate either saw almost nothing or was a similarity
|
|
1644
|
+
matcher, which is what §0 exists to remove), leaving human review at propose
|
|
1645
|
+
(§7) as the answer — and the idea was to help that review by putting
|
|
1646
|
+
requirements about one subject side by side in the one document a human reads
|
|
1647
|
+
end to end.
|
|
1648
|
+
*What rejected it: the ordering already ships, and the adjacency it buys is not
|
|
1649
|
+
where the gap is.* `compareIds` (`render.ts`) has ordered the document by
|
|
1650
|
+
prefix first and number second since `render` shipped, so same-prefix
|
|
1651
|
+
requirements are **already** adjacent; the candidate was never an ordering
|
|
1652
|
+
change, only a heading over an ordering that already holds. And §11's own
|
|
1653
|
+
example of the gap is `AUTH-3` expiring a session against `SESS-7` saying
|
|
1654
|
+
sessions never expire — a contradiction **across** prefixes, which prefix
|
|
1655
|
+
grouping pushes further apart rather than closer. What remains is a reader aid
|
|
1656
|
+
for same-prefix contradictions, which are the pairs already adjacent and
|
|
1657
|
+
therefore the ones review is least likely to miss, bought at the price of
|
|
1658
|
+
changing the bytes of every committed rendering and turning `render --check`
|
|
1659
|
+
red across every adopting repo on upgrade — for a change the user did not make.
|
|
1660
|
+
*The gap itself stays open* and stays where it belongs, in design §11: nothing
|
|
1661
|
+
resists a duplicated or contradictory requirement, and human review at propose
|
|
1662
|
+
is still the whole answer. What is rejected is grouping as a way to assist it,
|
|
1663
|
+
and this reopens only on a contradiction that grouping would have caught.
|
|
1664
|
+
|
|
1665
|
+
[Unreleased]: https://gitlab.com/Pseudorca/attest/-/compare/v0.4.1...main
|
|
1666
|
+
[0.4.1]: https://gitlab.com/Pseudorca/attest/-/tags/v0.4.1
|
|
1667
|
+
[0.4.0]: https://gitlab.com/Pseudorca/attest/-/tags/v0.4.0
|
|
1276
1668
|
[0.3.0]: https://gitlab.com/Pseudorca/attest/-/tags/v0.3.0
|
|
1277
1669
|
[0.2.2]: https://gitlab.com/Pseudorca/attest/-/tags/v0.2.2
|
|
1278
1670
|
[0.2.1]: https://gitlab.com/Pseudorca/attest/-/tags/v0.2.1
|