@am_shork/attest 0.6.0 → 0.7.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 +861 -37
- package/README.md +1 -1
- package/dist/cli/index.js +2 -18
- package/dist/cli/json.js +6 -1
- package/dist/cli/report.d.ts +18 -0
- package/dist/cli/report.js +41 -0
- package/dist/core/docs.d.ts +1 -1
- package/dist/core/docs.js +1 -0
- package/dist/core/gate.d.ts +48 -2
- package/dist/core/gate.js +73 -14
- package/dist/core/loader.js +13 -0
- package/dist/core/locate.d.ts +25 -0
- package/dist/core/locate.js +37 -6
- package/dist/core/merge.js +21 -8
- package/dist/core/pipeline.js +143 -31
- package/dist/core/render.js +129 -14
- package/dist/core/req-suite.d.ts +5 -0
- package/dist/core/req-suite.js +27 -0
- package/dist/core/runner.js +26 -8
- package/dist/core/skill.js +6 -2
- package/dist/core/status.js +21 -3
- package/dist/core/terminal.d.ts +23 -0
- package/dist/core/terminal.js +25 -9
- package/dist/core/validator.d.ts +5 -1
- package/dist/core/validator.js +23 -2
- package/dist/runtime.d.ts +20 -0
- package/dist/runtime.js +43 -15
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -13,6 +13,592 @@ input, removes/renames a public API or `--json` field, or changes a default
|
|
|
13
13
|
runtime behavior an existing invocation relies on — diagnostic message text is
|
|
14
14
|
not API.
|
|
15
15
|
|
|
16
|
+
## [0.7.0] - 2026-08-04
|
|
17
|
+
|
|
18
|
+
### Added
|
|
19
|
+
|
|
20
|
+
- **A universally quantified statement now has to say what it quantifies over,
|
|
21
|
+
and the answer is checked.** `tests/intent-rule-candidates.spec.ts` gains a
|
|
22
|
+
`QUANTIFIED` table beside `OBLIGATIONS`: every statement carrying an open
|
|
23
|
+
quantifier is hand-judged into one of three kinds, and a statement flagged with
|
|
24
|
+
no row fails. 23 of the 62 requirements are flagged today.
|
|
25
|
+
*The shape it exists for.* A universal quantifier in the statement, a finite
|
|
26
|
+
set of scenarios under it, and nothing holding the two together. It reads as
|
|
27
|
+
covered — the id has scenarios, `cover` is green, `check` is green — because
|
|
28
|
+
all three mechanisms this project sells operate *below* the quantifier:
|
|
29
|
+
coverage asks whether an id has a scenario, never-red asks whether that
|
|
30
|
+
scenario discriminated, drift asks whether it reads the params. None asks
|
|
31
|
+
whether the scenarios span what the sentence claims. It had already cost
|
|
32
|
+
something — `ATX-37` said "everything the CLI writes to a terminal" while
|
|
33
|
+
`render` built a document by concatenation that never went past the sanitiser,
|
|
34
|
+
which shipped as a fix earlier under this heading without the *shape* being
|
|
35
|
+
addressed.
|
|
36
|
+
**The classification is the part worth keeping, because it predicts which
|
|
37
|
+
statements carry the risk.** `values` — the quantifier ranges over inputs one
|
|
38
|
+
site processes, so growth cannot escape it, there being one site. `roster` — it
|
|
39
|
+
ranges over a set the code itself enumerates and the scenario iterates that
|
|
40
|
+
same set, so a new member is covered by construction. `sites` — it ranges over
|
|
41
|
+
places in the implementation, discharged by one hand-written scenario each, and
|
|
42
|
+
**this is the only kind that can silently stop being true**. Today 14, 2 and 8
|
|
43
|
+
across 24 rows — one more than the 23 flagged, for the reason two paragraphs
|
|
44
|
+
down.
|
|
45
|
+
`ATX-37` was a `sites` row and `render` was the new site, which is the whole
|
|
46
|
+
argument in one line. The `roster` shape is the one to reach for and the repo
|
|
47
|
+
had already found it once without naming it — `self/targets.spec.ts` says "a
|
|
48
|
+
target added without a scenario would otherwise be a target nothing checks".
|
|
49
|
+
**It found a live hole on its first run, on the requirement written to avoid
|
|
50
|
+
exactly this.** `ATX-62` names its surfaces rather than quantifying over them
|
|
51
|
+
("both commands that execute the suite — `verify` and the archive gate"), and
|
|
52
|
+
its rationale explains that choice in these words. Of the three sites it names,
|
|
53
|
+
the archive gate path had **no scenario asserting the diagnosis is emitted** —
|
|
54
|
+
only one asserting it *stands down* for a file `added-id-unmerged` already
|
|
55
|
+
claimed, and one asserting the withdrawal it enables. Both negative.
|
|
56
|
+
*Measured by mutation, because the first statement of this was wrong and the
|
|
57
|
+
correction is the useful part.* Deleting the `specLoadFailedIssues` call
|
|
58
|
+
outright does **not** survive: it sits inside `notRunIssues`, which both
|
|
59
|
+
commands share, so the `verify` scenario kills it. The mutant that isolates the
|
|
60
|
+
gate is the one that matters — `evaluateGate` filtering the diagnosis out of
|
|
61
|
+
what it pushes, which is a gate that drops the finding on its own path while
|
|
62
|
+
`verify` keeps it. That is the *exact* historical shape `ATX-62` exists to
|
|
63
|
+
answer, its rationale naming "the rule holding on one path and not the other".
|
|
64
|
+
Against the old spec that mutant runs **green, 0 errors**; against the new one
|
|
65
|
+
it is red. `self/gate.spec.ts` gains the missing scenario, and the run is now
|
|
66
|
+
62 requirements / 160 scenarios.
|
|
67
|
+
*Two design decisions the table fails without.* **Rows are allowed for ids the
|
|
68
|
+
detector does not flag**, which is why `ATX-62` has one at all: naming the
|
|
69
|
+
surfaces is the *remedy*, and it takes the statement out of the flagged set, so
|
|
70
|
+
a table keyed strictly on the detector would drop the binding at the moment the
|
|
71
|
+
author did the right thing. **Every named scenario is asserted to exist** in
|
|
72
|
+
the specs, by name, for that id — without it the table is one more hand-written
|
|
73
|
+
list that goes on reading as covered after a rename, which is the failure mode
|
|
74
|
+
every other check in that file is written to avoid.
|
|
75
|
+
*And one deliberate gap, carried rather than hidden.* A site the statement
|
|
76
|
+
claims that nothing reaches is recorded as `null` and lands in the snapshot,
|
|
77
|
+
on the same footing as the compound rule's `missed` list. There is one:
|
|
78
|
+
`ATX-53`'s "some of several specs renamed, the rest not" — a state no fixture
|
|
79
|
+
can construct, because every change fixture carries a single spec.
|
|
80
|
+
The detector is the same kind of instrument as `compoundByKeyword` and has the
|
|
81
|
+
same limit, stated rather than tuned away: it catches the declared universal
|
|
82
|
+
and misses the article form ("A registry that cannot be read MUST …"), which is
|
|
83
|
+
a universal too. Two of the 23 rows are the idiom "at all" rather than a
|
|
84
|
+
quantifier, kept as rows because a regex tuned until it stops flagging is the
|
|
85
|
+
measurement bending to the corpus.
|
|
86
|
+
No behaviour change, no new `Issue` code, and no `ATX-n` — repo
|
|
87
|
+
self-discipline, for the reason at the top of that file: knowing which surface
|
|
88
|
+
a sentence names is prose judgement, the ground truth §0 refuses to compute for
|
|
89
|
+
someone else's requirements.
|
|
90
|
+
|
|
91
|
+
- **The eight `sites` rows re-read against `src/`, which is what the table was
|
|
92
|
+
built to make possible.** The rows were hand-judged in one pass, and the value
|
|
93
|
+
of the whole table is concentrated in that kind — so the first use of it is a
|
|
94
|
+
strict read asking one question per row: *is this list of sites complete?* Five
|
|
95
|
+
claimed-but-unattested sites came back, one row was retired, and one was wrong
|
|
96
|
+
in the good direction.
|
|
97
|
+
**`ATX-15` was short by two.** "Every ordering Attest commits to disk" is wider
|
|
98
|
+
than the rendering: `red-record.ts` sorts ids and scenario names into
|
|
99
|
+
`first-run.json`, and `splice.ts` sorts ids and param keys into the registry
|
|
100
|
+
file `--apply` writes. Both are committed and neither is attested. The property
|
|
101
|
+
holds everywhere — every `.sort` in `src/` goes through `byCodeUnit` and no
|
|
102
|
+
`localeCompare` survives — which is precisely the `ATX-37` position: held at
|
|
103
|
+
every site, enumerated at none.
|
|
104
|
+
**`ATX-37` was short by three.** `formatStatus`, `formatCoverage` and
|
|
105
|
+
`formatArchiveVerdict` each interpolate a reqId, a change name or a path taken
|
|
106
|
+
from the project under test, and the statement says *everything* the CLI
|
|
107
|
+
writes. All three call `inline()` today; none is attested. That is the state
|
|
108
|
+
`render` was in on the day `ATX-58` had to be written.
|
|
109
|
+
**`ATX-58` was recorded as weaker than it is, and the correction is the useful
|
|
110
|
+
part.** It was filed as four field-sites carrying `ATX-37`'s growth risk.
|
|
111
|
+
`sanitised()` rebuilds a whole `Requirement` as one typed object literal, so a
|
|
112
|
+
fifth field on that type is a **compile error**, not a silent gap — the axis is
|
|
113
|
+
closed by the compiler rather than by anyone remembering. Five sites collapse
|
|
114
|
+
to two, and this is the strongest of the eight rather than a repeat of the
|
|
115
|
+
weakest.
|
|
116
|
+
**`ATX-53`'s open gap was retired by building it.** The recorded `null` was
|
|
117
|
+
"some of several specs renamed, the rest not", said to be unconstructible
|
|
118
|
+
because every change fixture carries one spec. Constructing it showed the state
|
|
119
|
+
does not exist: a renamed spec is no longer a *proposed* spec, so it leaves
|
|
120
|
+
`claimedSpecs` the moment it is done, and a partial rename re-runs as the state
|
|
121
|
+
already covered with a shorter list. Measured rather than argued — a two-spec
|
|
122
|
+
project with one merged by hand passes against a merge mutated to rename only
|
|
123
|
+
the first claimed spec. So the scenario was **not** kept: it discriminates
|
|
124
|
+
nothing, and design §11 is explicit that a scenario which cannot fail is worse
|
|
125
|
+
than the comment recording why. `self/merge.spec.ts` carries that comment, and
|
|
126
|
+
the two resumption scenarios now share one `spliceByHand` helper instead of two
|
|
127
|
+
copies of the same insertion literal.
|
|
128
|
+
`ATX-40`, `ATX-41` and `ATX-60` came back complete. `ATX-60` with a caveat
|
|
129
|
+
worth its line: every verdict in `src/` goes through `hasError` and the two
|
|
130
|
+
bare spellings left are not verdicts, so the sites are right — but "never by a
|
|
131
|
+
second spelling" is a claim about the whole of `src/` discharged by two
|
|
132
|
+
examples, and nothing sweeps for a fifth.
|
|
133
|
+
*What the read is actually saying, across five of the eight.* The recurring
|
|
134
|
+
answer is **a sweep or a type**. `ATX-58` shows what closed looks like: the
|
|
135
|
+
compiler refuses the new site. `ATX-34` and `ATX-57` show the other closed
|
|
136
|
+
shape: a scenario iterating the roster the code owns. `ATX-15`, `ATX-37` and
|
|
137
|
+
`ATX-60` want one and have neither, which is filed under
|
|
138
|
+
`Under consideration` rather than built here — a rule that fires on correct
|
|
139
|
+
usage is the shape this file has rejected before.
|
|
140
|
+
Bookkeeping only: no behaviour change, no new `Issue` code, and the snapshot
|
|
141
|
+
now carries five unattested sites where it carried one.
|
|
142
|
+
|
|
143
|
+
- **A sketch in the design document is now bound to the file it says it is
|
|
144
|
+
from.** `tests/docs-consistency.spec.ts` takes every fenced `ts` block whose
|
|
145
|
+
first line names a `src/` path — the convention those documents already
|
|
146
|
+
follow — and asserts that every name the block puts at **module scope** still
|
|
147
|
+
exists at module scope in that file. Six blocks today, three per language. The
|
|
148
|
+
scan is over every hand-written page rather than the design document, through
|
|
149
|
+
the same enumeration the other checks use: a sketch is not something only one
|
|
150
|
+
file may hold, and a hardcoded list has already stopped covering a page here
|
|
151
|
+
once, on the day the CLI reference was split out of the README.
|
|
152
|
+
*Why this one, of the three candidates recorded with the entry.* Deleting the
|
|
153
|
+
sketches costs the design document the thing it is for; marking them
|
|
154
|
+
illustrative leaves an argument free to cite state that is gone. The symbol
|
|
155
|
+
check was the only candidate with a hit rate, and the hit rate is now measured
|
|
156
|
+
rather than estimated: replaying the pre-`a5899ac` document against today's
|
|
157
|
+
`src/runtime.ts` reproduces the original failure exactly — `currentReqId`,
|
|
158
|
+
named at module scope by the sketch and nowhere in the file. That replay is
|
|
159
|
+
what turned "would have caught it" into something checked.
|
|
160
|
+
**It found a third instance on its first run**, which is the fourth way this
|
|
161
|
+
file records an entry arriving. §5.4's `runner.ts` sketch declared
|
|
162
|
+
`const REQ_SUITE = /^\[(.+)\]$/` and read it with `task.name.match(...)`; the
|
|
163
|
+
regex moved to `core/req-suite.ts` and `runner.ts` has imported
|
|
164
|
+
`requirementIdOf` from it since. The sketch was not merely stale, it showed the
|
|
165
|
+
design document contradicting the module whose entire reason for existing is
|
|
166
|
+
that *"two spellings of one encoding are two encodings the moment either is
|
|
167
|
+
edited"* — a reader following §5.4 would have written the second spelling. Both
|
|
168
|
+
languages are corrected here.
|
|
169
|
+
*Three scoping decisions, each of which the check fails without.* **Module
|
|
170
|
+
scope, not every identifier the sketch names**: `runtime.ts` explains the
|
|
171
|
+
deleted `currentReqId` in two comments, so a substring scan over the source
|
|
172
|
+
text would have called the stale sketch fine — both sides are reduced to
|
|
173
|
+
declarations, imports and re-exports through the compiler API. **The `src/`
|
|
174
|
+
prefix, not file existence**: existence would make a typo'd path a silently
|
|
175
|
+
skipped block, the failure mode this file guards against everywhere else, so a
|
|
176
|
+
`src/` path that does not resolve fails instead and `requirements/auth.reqs.ts`
|
|
177
|
+
is left alone as what it is, a path in the reader's project. **Fenced blocks,
|
|
178
|
+
not prose**: prose may name a dead symbol legitimately, and §11 now does so on
|
|
179
|
+
purpose, as history.
|
|
180
|
+
**What it does not reach, because the honest count is one of two.** §11's
|
|
181
|
+
ESM-only bullet — the worse of the two instances, since it was an *argument*
|
|
182
|
+
resting on state that no longer existed — is prose, and stays outside. This
|
|
183
|
+
catches the sketch; the reader who comes to fix the sketch is what catches the
|
|
184
|
+
paragraph citing it. A sketch whose module scope survives while its body drifts
|
|
185
|
+
also passes, which is the same limit the byte-equality check already records
|
|
186
|
+
about the prose around a sample.
|
|
187
|
+
No behaviour change, no new `Issue` code, and no `ATX-n`: this is repo
|
|
188
|
+
self-discipline about this repository's own documentation, in the file that
|
|
189
|
+
already holds every other coupling cheap enough to assert.
|
|
190
|
+
|
|
191
|
+
### Changed
|
|
192
|
+
|
|
193
|
+
- **`parseSpecs` held every spec file in memory at once; it now holds 32.** The
|
|
194
|
+
read was `Promise.all(files.map(readFile))`, so every source in the project
|
|
195
|
+
was resolved before the first one was parsed. Each source is now parsed at the
|
|
196
|
+
point it arrives, through a fixed pool, which makes the peak a constant rather
|
|
197
|
+
than the size of the input — the merged plan is the only thing that still grows
|
|
198
|
+
with the tree, and it is the output.
|
|
199
|
+
*Measured, both before and after.* On a synthetic tree of 6000 spec files the
|
|
200
|
+
old shape held ~47 MiB of source at once at a realistic 8 KiB per file;
|
|
201
|
+
`tests/locate-fanout.spec.ts` counts in-flight reads through the filesystem and
|
|
202
|
+
measures 200 before the change and 32 after, on a 200-file input. That test
|
|
203
|
+
lives in its own file because it mocks a Node builtin, and it reads the
|
|
204
|
+
property at `fs` rather than through `parseSpecs`, which is the honest seam:
|
|
205
|
+
peak in-flight reads is a fact about how the function uses the filesystem.
|
|
206
|
+
*What is deliberately **not** changed, and why the two halves are not one
|
|
207
|
+
decision.* `findFiles` fans out over sibling directories with no limit either,
|
|
208
|
+
and it stays that way. The failure it invites is descriptor exhaustion, and no
|
|
209
|
+
measurement on either development platform could produce one — on Windows, Node
|
|
210
|
+
uses Win32 handles rather than POSIX descriptors, so the shell's `ulimit -n`
|
|
211
|
+
does not govern the process, and the one platform where it is plausible is the
|
|
212
|
+
one this measurement could not reach. That half stays under
|
|
213
|
+
`Under consideration` on this file's own standard: no report, and no
|
|
214
|
+
demonstrated failure anywhere. The memory needed no such evidence — it is
|
|
215
|
+
arithmetic, and it is portable.
|
|
216
|
+
No behaviour change and no `ATX-n`: the plan is byte-identical either way, and
|
|
217
|
+
there is no contract here a consumer could branch on. `check` being the command
|
|
218
|
+
this project tells people to run first on an untrusted fork MR is what makes it
|
|
219
|
+
worth doing anyway — the input size is not ours to assume.
|
|
220
|
+
|
|
221
|
+
- **Every static command loaded the test runner it can never start.**
|
|
222
|
+
`pipeline.ts` imported `createLoader` from `loader.ts` and
|
|
223
|
+
`runAndCollect`/`BASE_EXCLUDE` from `runner.ts` at the top level, and
|
|
224
|
+
`cli/index.ts` imports `pipeline.ts` whole — so `vite`, `vitest/node` and
|
|
225
|
+
`vitest/config` were resolved and evaluated before `commander` had parsed an
|
|
226
|
+
argument. `check`, `cover`, `render`, `status` without `--eval`, and `init`
|
|
227
|
+
call none of them. The three are now reached through `await import()` at the
|
|
228
|
+
four `createLoader` sites and the two runner sites, all of which were already
|
|
229
|
+
in `async` functions.
|
|
230
|
+
*Measured on this repository, before and after, on one machine* — the earlier
|
|
231
|
+
figures recorded with this as a plan were taken in a different session and are
|
|
232
|
+
not comparable, which is the whole reason the baseline was re-taken here rather
|
|
233
|
+
than quoted. Importing `dist/core/pipeline.js`: **2133 ms → 1111 ms**.
|
|
234
|
+
`attest --version` end to end: 1700–2219 ms → 954–1220 ms. `attest check .`:
|
|
235
|
+
1991–2215 ms → 1300–1478 ms. Roughly a second, on every invocation of a
|
|
236
|
+
command that could not have used it.
|
|
237
|
+
**This is not the core/adapter package split, and does not substitute for
|
|
238
|
+
one.** `tests/import-boundary.spec.ts` states its own gap in its own words — it
|
|
239
|
+
gates **direct** imports, not reachability from the static commands, because
|
|
240
|
+
`pipeline.ts` holds both halves of the engine — and files the answer under the
|
|
241
|
+
split §11 defers as not earned by one package at this size. That deferral
|
|
242
|
+
stands and that gap is unchanged: what moved is *when* the cost is paid, not
|
|
243
|
+
which module holds what.
|
|
244
|
+
**The one property this could have silently dropped was the boundary itself**,
|
|
245
|
+
and it did not: that gate reads `import(...)` and `require(...)` from the AST
|
|
246
|
+
alongside static imports, and has since it was written, precisely so it could
|
|
247
|
+
not be defeated this way. Both specifiers stay relative, so `ALLOWED` is
|
|
248
|
+
untouched and the reachability walk still traverses them. The comment there
|
|
249
|
+
saying dynamic import was unused in `src/` is now false and has been corrected;
|
|
250
|
+
a gate whose commentary describes a codebase that no longer exists is the
|
|
251
|
+
defect this file's *"six places where the source states a guarantee it does not
|
|
252
|
+
provide"* entry is about.
|
|
253
|
+
*What this does not buy, stated so the next reader does not expect it.* The
|
|
254
|
+
larger single item is `typescript` at ~936 ms, and `check`, `cover` and
|
|
255
|
+
`render` genuinely read the AST, so it stays on their real path — visible in
|
|
256
|
+
`--version` still costing ~1 s. Only `init` and `--version`/`--help` pay it for
|
|
257
|
+
nothing, and separating those means splitting `pipeline.ts` rather than moving
|
|
258
|
+
an import. So this closes the avoidable half and leaves the unavoidable one
|
|
259
|
+
measured.
|
|
260
|
+
**No behaviour change and no new requirement.** No command's output, exit code
|
|
261
|
+
or JSON shape moves, and the property worth stating — *a static command must
|
|
262
|
+
not load the runner* — is deliberately **not** filed as an `ATX-n`: it is not
|
|
263
|
+
falsifiable from inside the engine today, for the same reason the import
|
|
264
|
+
boundary gate cannot assert reachability, and a requirement that cannot go red
|
|
265
|
+
is the defect `archive`'s never-red gate exists to catch. It stays repo
|
|
266
|
+
discipline, in the gate that already holds it.
|
|
267
|
+
|
|
268
|
+
### Fixed
|
|
269
|
+
|
|
270
|
+
- **The workflow `attest init` writes named the special case of a load failure
|
|
271
|
+
and not the general one.** `spec-load-failed` shipped in this cycle, after the
|
|
272
|
+
skill body was last edited, so its stage 2 table listed only
|
|
273
|
+
`added-id-unmerged` — which fires just when the change *also* adds an id the
|
|
274
|
+
registry lacks. An agent hitting a typo in a specifier therefore met an ERROR
|
|
275
|
+
absent from the table it was told to branch on, while the row that used to
|
|
276
|
+
appear, `declared-not-run`, had been withdrawn for exactly that file. The three
|
|
277
|
+
rows now say how they relate: the general case, the specific case that
|
|
278
|
+
supersedes it, and the withdrawal that keeps one file to one finding.
|
|
279
|
+
`ATX-57` could not have caught it. It holds one-directionally — every code the
|
|
280
|
+
workflow names must be emittable — which refuses a misspelling and is silent
|
|
281
|
+
about an omission, and that asymmetry is deliberate: the workflow has no
|
|
282
|
+
business naming every code. The other direction is not a rule anyone can write.
|
|
283
|
+
|
|
284
|
+
- **`check --eval` started a Vite dev server per change, plus one for the
|
|
285
|
+
registry.** `unclaimedProposedSpecIssues` reads every change's delta in a loop,
|
|
286
|
+
and `readDelta` created and closed its own loader on each iteration — so the
|
|
287
|
+
cost was 1 + N servers for a command that needs one. Measured on a project
|
|
288
|
+
with six changes: **7 loaders → 1**, and the command went from ~3.3–3.6 s to
|
|
289
|
+
~1.7–1.9 s. Both readers now borrow a loader when the caller holds one, and
|
|
290
|
+
`runCheck` owns exactly one for the whole command through a `withLoader`
|
|
291
|
+
helper. Ownership sits at the command because neither reader knows how many
|
|
292
|
+
times it is about to be called.
|
|
293
|
+
Static `check` is unaffected in the way that matters: with no `--eval` it
|
|
294
|
+
passes `undefined` down and starts nothing at all, which is the property that
|
|
295
|
+
command is documented on (design §5.1).
|
|
296
|
+
|
|
297
|
+
- **`terminal.ts`'s `control` allocated three times the input to sanitise it.**
|
|
298
|
+
It spread the string into a per-code-point array, mapped, and rejoined, where
|
|
299
|
+
a character class lets the engine scan and — in the case every real registry
|
|
300
|
+
is — return the input untouched. Measured over 120,000 characters: **27×**
|
|
301
|
+
faster on plain ASCII, **54×** with newlines, **84×** on CJK. A payload that
|
|
302
|
+
is *entirely* control bytes is a wash at 0.9×, since then there is nothing to
|
|
303
|
+
fast-path and both spellings build a new string; both are linear either way,
|
|
304
|
+
so this is a constant factor rather than a second `ATX-59`.
|
|
305
|
+
Worth changing because the call count scales with the registry while the
|
|
306
|
+
length scales with whatever the registry chose: `render`'s `sanitised` calls
|
|
307
|
+
it once per statement, rationale, param key, param value and out-of-scope
|
|
308
|
+
entry, on the same reachable `render --check` path `ATX-59` came off.
|
|
309
|
+
**Byte-identical to the spelling it replaces**, which is the only thing that
|
|
310
|
+
mattered here: verified exhaustively over every code unit in the BMP, and over
|
|
311
|
+
200,000 randomised strings mixing control bytes, CJK, astral characters and
|
|
312
|
+
lone surrogates — surrogates being the case the spread existed to get right.
|
|
313
|
+
Newlines survive as they always did, by not being in the class.
|
|
314
|
+
|
|
315
|
+
- **Four comments describing a property the code beside them did not have.**
|
|
316
|
+
This repository keeps its decision record in prose, so a comment claiming
|
|
317
|
+
something the code does not do is the same class of defect as a diagnostic
|
|
318
|
+
pointing at the wrong line: it is what the next reader believes *instead of*
|
|
319
|
+
reading. None of these produced a wrong answer today, and each is now true.
|
|
320
|
+
- `merge.ts`'s `archivePath` is documented as "one function because two places
|
|
321
|
+
ask … a date computed twice could straddle midnight and disagree with
|
|
322
|
+
itself", and called `new Date()` on **every** invocation — four times per
|
|
323
|
+
merge. A merge crossing midnight would have refused against one directory
|
|
324
|
+
and written to another. The stamp is now read once in `applyMerge` and
|
|
325
|
+
passed down, which makes `archivePath` a pure function of its arguments —
|
|
326
|
+
the property that turns "the two cannot disagree" into something the code
|
|
327
|
+
holds rather than something the clock does.
|
|
328
|
+
- `pipeline.ts`'s `claimedSpecsOf` is documented as "built from the plan
|
|
329
|
+
rather than by re-parsing, so the files `--apply` touches are exactly the
|
|
330
|
+
files the gate ran", and its caller handed it a fresh `parseSpecs` over
|
|
331
|
+
every proposed spec — the second such parse in that function, with
|
|
332
|
+
`changeMergedPlan` having done the first. The two agreed because the same
|
|
333
|
+
two functions ran twice, which is agreement by transcription: the thing this
|
|
334
|
+
codebase extracts `declaredNotRunIssues` and `uncoveredIssues` to stop
|
|
335
|
+
relying on. `changeMergedPlan` now hands back the claimed half it already
|
|
336
|
+
built, so there is one parse, one split and one answer — and the archive
|
|
337
|
+
path does one less full parse of every proposed spec.
|
|
338
|
+
- `pipeline.ts`'s `runCover` ordered ids with a bare `.sort()` where the rest
|
|
339
|
+
of the engine goes through `byCodeUnit`. The result is identical on ids that
|
|
340
|
+
match the grammar, which is exactly why it was worth changing: `order.ts`
|
|
341
|
+
exists so ordering has one spelling, and that is a property which survives an
|
|
342
|
+
edit where an identical result does not.
|
|
343
|
+
- `status.ts`'s `statusRows` filtered the whole plan once per added id, the
|
|
344
|
+
pattern `validator.ts`'s `detectPotentialDrift` carries a comment about
|
|
345
|
+
having removed — O(requirements × scenarios), and the plan here is the
|
|
346
|
+
*merged* one, so the inner term is the whole repository's scenario count.
|
|
347
|
+
Indexed once now. The cost was genuinely invisible (20 ids against 2000
|
|
348
|
+
scenarios is 40k comparisons, well under a millisecond), so this lands as
|
|
349
|
+
the inconsistency it was rather than as a slow path; scenario order within a
|
|
350
|
+
row is unchanged, still file then line.
|
|
351
|
+
|
|
352
|
+
- **`orphan-test`'s fix hint was wrong for the case that produces the most of
|
|
353
|
+
them.** "Add it to the registry, or fix the id" is right for an id that is
|
|
354
|
+
genuinely absent and **wrong** for one already sitting in a registry file that
|
|
355
|
+
failed to load: the id is there, the fix is the load error, and following the
|
|
356
|
+
advice adds a duplicate. One unreadable `*.reqs.ts` orphans every scenario of
|
|
357
|
+
every requirement it declared, so the wrong advice was also the loudest thing
|
|
358
|
+
in the report, with `registry-invalid` — the one true finding — pushed to the
|
|
359
|
+
top and out of view.
|
|
360
|
+
**The finding stays; only the advice changes**, and that is the whole of the
|
|
361
|
+
decision. Suppressing `orphan-test` needs a prefix→file mapping for a file
|
|
362
|
+
that never parsed, which is exactly the file no ids are available from; the
|
|
363
|
+
weaker signal (some registry file failed to load, so suppress all of them) is
|
|
364
|
+
a strictly worse `check` on a multi-file registry where the other files are
|
|
365
|
+
fine and their orphans are real. Both remain open under *Under consideration*.
|
|
366
|
+
What is shippable today is that a command which cannot tell the two cases
|
|
367
|
+
apart should say so rather than guess — so when any registry file fails to
|
|
368
|
+
load, the hint names the uncertainty and the order to work in. `Issue.message`
|
|
369
|
+
is prose and never API, so this costs no consumer anything.
|
|
370
|
+
*"A registry file failed to load" is not "loading produced an ERROR", and the
|
|
371
|
+
first draft of this had it wrong.* `duplicate-prefix` and
|
|
372
|
+
`duplicate-requirement` are ERRORs raised **after** a successful read, and the
|
|
373
|
+
ids they are about are present — so keying the new wording on `hasError` told
|
|
374
|
+
a project with a prefix collision that a file had failed to load, which is
|
|
375
|
+
false, about a report where the original advice was correct. `loadRegistry`
|
|
376
|
+
now answers the question it is the only thing in a position to answer: which
|
|
377
|
+
files contributed no ids, taken from the outcome it already branches on rather
|
|
378
|
+
than re-derived from a list of codes a caller would have to keep in step.
|
|
379
|
+
|
|
380
|
+
- **The Vite logger wrapper reported that it had never warned, however much it
|
|
381
|
+
had.** `sanitisedLogger` builds its wrapper by spreading the base logger, and
|
|
382
|
+
`hasWarned` is the one member of Vite's `Logger` that is state rather than
|
|
383
|
+
behaviour — so the spread copied it **by value** at construction. Measured:
|
|
384
|
+
after one `warn` through the wrapper, the base reads `true` and the wrapper
|
|
385
|
+
still reads `false`. An accessor pair makes the wrapper a view of the base
|
|
386
|
+
rather than a snapshot of it, which is what every other member already was.
|
|
387
|
+
|
|
388
|
+
- **`render`'s id comparator was non-transitive on an id it could not take
|
|
389
|
+
apart.** `compareIds` split on `lastIndexOf('-')` with no guard, so an id with
|
|
390
|
+
no dash silently lost its last character (`AUTH` → prefix `AUT`) and produced
|
|
391
|
+
`NaN` for its number. The consequence is worse than a wrong tie: measured on
|
|
392
|
+
`['AUTH-3','AUTH-abc','AUTH-2']` the sort returned `AUTH-3, AUTH-abc, AUTH-2`,
|
|
393
|
+
which is not a sorted permutation of anything. The comparator is now total by
|
|
394
|
+
construction, and falls back to the whole id rather than to the engine's sort
|
|
395
|
+
stability — `renderMarkdown` feeds `--check`, so an order that depends on
|
|
396
|
+
anything but the ids is a freshness gate that can disagree with the run that
|
|
397
|
+
wrote the file.
|
|
398
|
+
**Robustness, not a live defect, and the distinction is worth keeping.** A
|
|
399
|
+
malformed id cannot reach here through any command: `RequirementIdSchema`
|
|
400
|
+
rejects it and `render` returns early on a registry that failed to load —
|
|
401
|
+
confirmed by trying it. What was actually wrong is that two functions held two
|
|
402
|
+
beliefs about one input space: `locate.ts`'s `idPrefix` handles a dashless id
|
|
403
|
+
and records why ("this also runs over ids a delta proposed"), and this one
|
|
404
|
+
did not. They agree now.
|
|
405
|
+
|
|
406
|
+
- **A spec file that failed to load was reported as a file full of skipped
|
|
407
|
+
scenarios, and the file itself was named nowhere.** `verify` on a two-file
|
|
408
|
+
project with one bad import produced three ERRORs: `tests-red`, and one
|
|
409
|
+
`declared-not-run` per scenario in the broken file, each saying *"skipped, or
|
|
410
|
+
excluded by an `.only`?"*. That is a guess at a cause and the wrong one — it
|
|
411
|
+
sends the reader to audit a correct file for a `skip` that is not there, while
|
|
412
|
+
the import error explaining everything is named in no finding at all. The gate
|
|
413
|
+
had a diagnosis for one shape of this (`added-id-unmerged`, when the change
|
|
414
|
+
also adds an unmerged id); every other way a file fails to import had none, on
|
|
415
|
+
either command.
|
|
416
|
+
**The obvious repair was the wrong one, and `gate.ts` had already written down
|
|
417
|
+
why** — three lines from where it would have gone. Suppressing
|
|
418
|
+
`declared-not-run` for unloaded files produces a red report with nothing
|
|
419
|
+
pointing at the file: silence in place of a wrong message, and worse than what
|
|
420
|
+
it replaces. So the replacement comes first. `spec-load-failed` (`ATX-62`)
|
|
421
|
+
names the file on both commands that run a suite, and stands down where a more
|
|
422
|
+
specific diagnosis already named it — one file, one finding, and the one
|
|
423
|
+
carrying a fix wins. It deliberately does **not** restate the import error:
|
|
424
|
+
the run output above the report already has it, and a copy is a second place
|
|
425
|
+
for it to be wrong. No `reqId`, because a file that never loaded has no
|
|
426
|
+
established relation to any requirement.
|
|
427
|
+
**Both commands now ask one function**, `notRunIssues`, rather than each
|
|
428
|
+
spelling out the order themselves — the cause before the absences it caused,
|
|
429
|
+
and never the absences alone. That is how the two came to differ at all: the
|
|
430
|
+
gate learned to withdraw a misleading line and `verify` did not, so one run
|
|
431
|
+
produced a different diagnosis depending on which command asked. It is the
|
|
432
|
+
same argument that extracted `declaredNotRunIssues` from those two commands,
|
|
433
|
+
arriving one level up, and it is what stops this particular divergence from
|
|
434
|
+
being re-openable by an edit to either caller.
|
|
435
|
+
**`ATX-50` reaches `verify` now, and its line did not move.** That requirement
|
|
436
|
+
withdraws the absences a load-failure diagnosis replaces, and its recorded
|
|
437
|
+
condition was always *"a correct diagnosis exists for this file"* rather than
|
|
438
|
+
*"the file failed to load"* — it was scoped to the gate because the gate was
|
|
439
|
+
the only place that condition could hold. It now holds on both. The "and no
|
|
440
|
+
further" half is what an over-broad fix would have cost, and is separately
|
|
441
|
+
attested: a scenario that really was skipped, in a file that loaded, is still
|
|
442
|
+
reported.
|
|
443
|
+
**Behaviour change, for a project that has a spec file failing to load.**
|
|
444
|
+
`verify` and `archive` report one `spec-load-failed` per unloaded file instead
|
|
445
|
+
of a `declared-not-run` per scenario in it. Both are ERRORs and both runs were
|
|
446
|
+
already red, so no green becomes red and no red becomes green; what changes is
|
|
447
|
+
which finding is in the report and what it says. Anything branching on
|
|
448
|
+
`declared-not-run` for such a file — no consumer is known to — sees the new
|
|
449
|
+
code instead. Additive to the JSON surface (a new `code` value), so
|
|
450
|
+
`SCHEMA_VERSION` is unchanged.
|
|
451
|
+
|
|
452
|
+
- **`attest render` let the registry it was reading write to the terminal
|
|
453
|
+
reading it.** ATX-37 put every byte the CLI prints through a sanitiser, and
|
|
454
|
+
the scope line of that requirement says "everything the CLI writes to a
|
|
455
|
+
terminal" — but `render` builds a *document* by string concatenation that
|
|
456
|
+
never went past it. The static reader hands back the **cooked** string, so an
|
|
457
|
+
escape written as an escape in a `*.reqs.ts` is a real control byte by the
|
|
458
|
+
time it is concatenated. *Measured on the built CLI*, with no `--eval` and no
|
|
459
|
+
flag at all: a statement carrying `ESC [2K CR` erases the reviewer's line and
|
|
460
|
+
repaints a verdict, and `ESC ]0;` retitles their window. On a fork merge
|
|
461
|
+
request the author is not the reviewer, and a registry — not a test, not a
|
|
462
|
+
script — is the whole payload.
|
|
463
|
+
**The obvious fix was the wrong one, and the loop said so before it was
|
|
464
|
+
written.** Sanitising the terminal write would have left `--out` putting the
|
|
465
|
+
same bytes in a file that is committed, served, and read again later by `cat`,
|
|
466
|
+
by `less -R`, or by a static site generator. The artifact outlives the run, so
|
|
467
|
+
the run is the wrong place to defend: the obligation is now over the document
|
|
468
|
+
(`ATX-58`), which is also what keeps `--check` honest, since both sides of
|
|
469
|
+
that comparison are built from one function. Newlines survive, because a
|
|
470
|
+
rendered rationale is Markdown prose that may legitimately span lines — and
|
|
471
|
+
they are *not* indented the way ATX-37 indents them, because four spaces in
|
|
472
|
+
Markdown is a code block.
|
|
473
|
+
**Behaviour change, for one shape of project.** A registry that actually
|
|
474
|
+
contains a control character renders to different bytes than before, so a
|
|
475
|
+
committed rendering goes `stale-spec-doc` once and is fixed by regenerating.
|
|
476
|
+
A registry without one is byte-identical: this repo's own `SPEC.md` changed by
|
|
477
|
+
insertion only.
|
|
478
|
+
|
|
479
|
+
- **A statement padded with whitespace cost `render` quadratic time, and
|
|
480
|
+
`render --check` is the gate people put in CI.** The table cell collapsed a
|
|
481
|
+
multi-line statement with a pattern whose leading quantifier is followed by a
|
|
482
|
+
required character, so a whitespace run containing no newline was consumed,
|
|
483
|
+
failed, and re-tried one character shorter from every position in it.
|
|
484
|
+
*Measured through the CLI*: 120,000 spaces in one statement cost 8.6 seconds
|
|
485
|
+
of pure CPU, 320,000 cost 58 — under the **static** reader, so a fork merge
|
|
486
|
+
request can burn a runner without executing a line of anything.
|
|
487
|
+
**The first repair measured slower than the defect**, which is the part worth
|
|
488
|
+
keeping: `[^\S\n]*\n[^\S\n]*`, which stops the class matching the newline,
|
|
489
|
+
is just as quadratic, because the backtracking was never about which
|
|
490
|
+
characters the class held — it was about the quantifier having something after
|
|
491
|
+
it. Matching each maximal whitespace run once and deciding in a callback has
|
|
492
|
+
nothing after the quantifier to fail into. Byte-identical to the old pattern,
|
|
493
|
+
asserted over 200,000 randomised inputs and confirmed on the real corpus, and
|
|
494
|
+
now bounded by `ATX-59` at a budget three orders of magnitude looser than a
|
|
495
|
+
linear implementation needs — a defect detector, not a machine-speed detector.
|
|
496
|
+
|
|
497
|
+
- **Grouping scenarios under a `describe` took the whole spec file down, and
|
|
498
|
+
the static half called that file fully covered.** Vitest defers a nested
|
|
499
|
+
collector until after the parent's callback returns — measured: at the end of
|
|
500
|
+
the outer callback the inner one has not run — so the module-level
|
|
501
|
+
`currentReqId` that `requirement()` set and restored around its body was
|
|
502
|
+
already back to `null` when a grouped `scenario()` ran. It threw, and a throw
|
|
503
|
+
during collection is a *file* error: nothing in that file was collected.
|
|
504
|
+
**The blast radius is the file, not the group.** A correct, ungrouped scenario
|
|
505
|
+
belonging to a *different* requirement in the same file came back
|
|
506
|
+
`declared-not-run` too, blamed on "skipped, or excluded by an `.only`?" —
|
|
507
|
+
sending the reader to audit their spec for something that is not there.
|
|
508
|
+
**And the static commands were not merely misdirected, they were wrong.**
|
|
509
|
+
`check` answered `✓ No issues` and `cover` answered `0 uncovered` for a file
|
|
510
|
+
that could not execute a single line. Those are the two commands documented as
|
|
511
|
+
the fast pre-check that is safe to run first on an untrusted fork, so a
|
|
512
|
+
pipeline running only `check` — a use this reference recommends — would pass a
|
|
513
|
+
change whose specs never run. That is a false green, which is the one failure
|
|
514
|
+
class this tool exists to prevent.
|
|
515
|
+
**Fixed at both ends, because it was one seam disagreeing with itself.**
|
|
516
|
+
`parser.ts` has always recursed to any depth while the task-tree walk counted
|
|
517
|
+
only *direct* `test` children of a `[reqId]` suite, so even a runtime that
|
|
518
|
+
stopped throwing would have reported a grouped scenario that ran and passed as
|
|
519
|
+
never run. The module-level variable is **deleted** rather than repaired — it
|
|
520
|
+
was a second answer to a question the suite name already answers — and both
|
|
521
|
+
sides now go through one `req-suite.ts` that owns the `[id]` encoding.
|
|
522
|
+
**The narrower fix was measured and rejected.** Having the parser refuse a
|
|
523
|
+
`scenario()` that is not a direct child looked smaller and would have closed
|
|
524
|
+
the false green too, but a *synchronous* wrapper — `forEach`, or a local
|
|
525
|
+
helper — works correctly today at both ends, and no static rule separates one
|
|
526
|
+
from a deferred callback. It would have rejected working code to describe a
|
|
527
|
+
runtime property.
|
|
528
|
+
*Behaviour change, two of them.* The nesting contract is now enforced when a
|
|
529
|
+
scenario **runs** rather than when it is collected, so a scenario genuinely
|
|
530
|
+
outside any `requirement()` fails as one test instead of taking its file down.
|
|
531
|
+
Nothing that passed before fails now.
|
|
532
|
+
The second is smaller and out of contract, and is stated because it is silent:
|
|
533
|
+
`scenario` used to hand its function to `it` directly, so Vitest called it with
|
|
534
|
+
the test context, and it is now invoked with no arguments. The declared type
|
|
535
|
+
has always been `() => void | Promise<void>` and the design document has always
|
|
536
|
+
shown it that way, so TypeScript rejected any use of that argument; a
|
|
537
|
+
JavaScript consumer who reached for `ctx.skip()` inside a scenario would find
|
|
538
|
+
it gone. Preserved deliberately as-declared rather than threaded through: the
|
|
539
|
+
contract is the zero-argument one, and adding a parameter to keep an
|
|
540
|
+
undeclared behaviour alive is a hook for a need no requirement has. `docs/*/attest-design.md` §3's runtime sample is
|
|
541
|
+
updated, as is the ESM-only note in §11, whose stated hazard was the very
|
|
542
|
+
module-level state this removes.
|
|
543
|
+
|
|
544
|
+
- **Four verdict predicates that were not `hasError`, one of which would have
|
|
545
|
+
reported a failed gate after the merge had already written the files.**
|
|
546
|
+
`types.ts` states the invariant in as many words — `hasError` is "the single
|
|
547
|
+
predicate the pipeline verdict, the JSON `ok`, and the CLI exit code all read
|
|
548
|
+
from, so those three can never disagree" — and four sites spelled it by hand:
|
|
549
|
+
`archive`'s report and its human rendering both asked `blocking.length === 0`,
|
|
550
|
+
and the merge path asked `issues.some(i => i.level === 'ERROR')` twice.
|
|
551
|
+
**The `archive` one had a consequence its own neighbour predicts.**
|
|
552
|
+
`runArchiveApply` deliberately keeps the gate's non-blocking output, with the
|
|
553
|
+
comment that dropping a WARNING "would make `--apply` quieter than the same
|
|
554
|
+
command without it" — so a WARNING reaching `blocking` is not hypothetical, it
|
|
555
|
+
is planned for. The first one to arrive made the report answer `ok: false`, the
|
|
556
|
+
process exit 1, and the terminal print `✗ Gate failed` **after `--apply` had
|
|
557
|
+
written the registry, renamed the specs and moved the change folder**.
|
|
558
|
+
Reporting failure after a destructive step succeeded is the worst shape a
|
|
559
|
+
verdict can take, and the only thing preventing it was that every issue those
|
|
560
|
+
paths construct happens to carry a literal `ERROR`.
|
|
561
|
+
Now `ATX-60`, stated over the predicate rather than over `archive`, because the
|
|
562
|
+
defect is that the question had more than one spelling and any command could
|
|
563
|
+
grow a fifth — and stated to cover the human verdict as well as the machine
|
|
564
|
+
one, since both were wrong here independently and fixing either alone leaves a
|
|
565
|
+
command whose two surfaces disagree about whether it failed. Distinct from
|
|
566
|
+
`ATX-5`, which promises the implication one way only: `ok` false whenever there
|
|
567
|
+
is an ERROR says nothing about `ok` false when there is not, which is the half
|
|
568
|
+
that broke. No behaviour change today, which is the argument for doing it
|
|
569
|
+
before something has been reported wrong rather than after. **No
|
|
570
|
+
`schemaVersion` bump**: the envelope keeps every field it had and the `Issue`
|
|
571
|
+
interface is untouched — what changed is the value `ok` takes in a case
|
|
572
|
+
nothing can currently produce, and a consumer branching on `ok` and
|
|
573
|
+
`issues[].code` as documented reads the same report it always did.
|
|
574
|
+
*Found by reading, and the count was wrong when it was filed:* the note that
|
|
575
|
+
queued this named three sites; the rendering in `cli/index.ts` was the fourth,
|
|
576
|
+
and it is the one a human would actually have seen.
|
|
577
|
+
|
|
578
|
+
- **Sweeping for a second backtracking site found something that is not
|
|
579
|
+
backtracking.** Every regex `src/` executes was measured against 21
|
|
580
|
+
adversarial input shapes at doubling sizes, with the old table-cell pattern
|
|
581
|
+
left in as a positive control — it was the only superlinear hit, which is what
|
|
582
|
+
makes the other eighteen being linear evidence rather than silence. The
|
|
583
|
+
structural reason is worth keeping: that pattern began with a quantifier that
|
|
584
|
+
can match nothing, so **every index** was a valid start; every other regex here
|
|
585
|
+
begins with a literal or a required class, which caps the starts at how often
|
|
586
|
+
that character occurs.
|
|
587
|
+
What the sweep turned up instead was the code-span fence, which measured the
|
|
588
|
+
longest run of backticks with `Math.max(0, ...runs)` — one stack argument per
|
|
589
|
+
run, so a param holding a few hundred thousand of them exhausted it. Same
|
|
590
|
+
reachable path as the quadratic one (`attest render` and `render --check`,
|
|
591
|
+
static reader, no `--eval`), same shape of defect — a registry choosing what
|
|
592
|
+
rendering costs the person reading it — and a different mechanism entirely.
|
|
593
|
+
`ATX-59` now states both and is counted as two obligations, because a
|
|
594
|
+
quadratic pattern and an unbounded spread regress independently.
|
|
595
|
+
A third candidate was measured and **rejected**: a deeply nested literal does
|
|
596
|
+
overflow the stack, but TypeScript's own parser gives out around depth 1,000
|
|
597
|
+
and this project's AST walk not until 2,000, so the limit belongs to the
|
|
598
|
+
compiler Attest is built on. It also lands where the contract already puts it —
|
|
599
|
+
a well-formed `internal-error` envelope under `--json`, and a sanitised stack
|
|
600
|
+
on the side people read.
|
|
601
|
+
|
|
16
602
|
## [0.6.0] - 2026-08-03
|
|
17
603
|
|
|
18
604
|
### Added
|
|
@@ -2436,11 +3022,30 @@ feedback reports are triaged into issues or `Unreleased` and deliberately not
|
|
|
2436
3022
|
kept in this tree; that rule stands, and holds for anything with a reporter, a
|
|
2437
3023
|
repro, or a state to track. What is kept here is the narrower thing this file
|
|
2438
3024
|
already keeps one section of: a **decision with an argument attached**, where
|
|
2439
|
-
losing the argument would cost more than losing the task. The one
|
|
2440
|
-
of reading another repository against this one — `msw`.
|
|
2441
|
-
|
|
2442
|
-
|
|
2443
|
-
`[0.
|
|
3025
|
+
losing the argument would cost more than losing the task. The one entry left
|
|
3026
|
+
below came out of reading another repository against this one — `msw`.
|
|
3027
|
+
|
|
3028
|
+
Eleven entries have left by being built: three together in `[0.6.0]`, and six
|
|
3029
|
+
under `[0.7.0]` — the verdict predicates, the nested `describe`, the runner
|
|
3030
|
+
the static commands loaded and could not start, the diagnosis for a spec file
|
|
3031
|
+
that failed to load, the six comments describing a property the code lacked, and
|
|
3032
|
+
the sketches now bound to the files they name — which are the first to leave by
|
|
3033
|
+
being **read** rather than by being reported or gated. The last of those left
|
|
3034
|
+
from `Under consideration` rather than from here, which is the move this file
|
|
3035
|
+
describes as going up before going out, and it went up on the strength of one
|
|
3036
|
+
thing: of its three candidates, exactly one could be measured against instances
|
|
3037
|
+
that had already happened.
|
|
3038
|
+
The second of those left *partially*: one of its three parts came back here as an
|
|
3039
|
+
entry of its own, because implementing the other two showed the estimate on it
|
|
3040
|
+
was wrong. An entry leaving is not evidence that all of it shipped, and this is
|
|
3041
|
+
the first time that has mattered. That returned part has now shipped as well, and
|
|
3042
|
+
it closed the way it said it would: the estimate was wrong because the fix was a
|
|
3043
|
+
new `Issue` code rather than a suppression, and what made it so was a decision
|
|
3044
|
+
recorded three lines from where the suppression would have gone. **The thing that
|
|
3045
|
+
cost the most was the thing already written down** — which is an argument for
|
|
3046
|
+
this section, since the note that corrected the estimate survived only because
|
|
3047
|
+
someone had filed the reasoning next to the code.
|
|
3048
|
+
`compiler-unsupported` came from building something queued here,
|
|
2444
3049
|
which is one way an entry arrives: a gate that finds a defect on its first run
|
|
2445
3050
|
files the defect, and the `typescript` matrix `[0.4.2]` added is what found this
|
|
2446
3051
|
one.
|
|
@@ -2456,37 +3061,61 @@ recorded under `[0.6.0]`, and two earlier entries from the same report left
|
|
|
2456
3061
|
by shipping in `[0.5.0]` — a change's specs moving to their merged location, and
|
|
2457
3062
|
the delta reading as the registry of what it adds — their arguments recorded
|
|
2458
3063
|
there, including the part of the first one that turned out to be wrong.
|
|
2459
|
-
|
|
2460
|
-
|
|
2461
|
-
|
|
2462
|
-
|
|
2463
|
-
|
|
2464
|
-
|
|
2465
|
-
|
|
2466
|
-
|
|
2467
|
-
|
|
2468
|
-
|
|
2469
|
-
|
|
2470
|
-
|
|
2471
|
-
|
|
2472
|
-
|
|
2473
|
-
|
|
2474
|
-
|
|
2475
|
-
|
|
2476
|
-
|
|
2477
|
-
|
|
2478
|
-
|
|
2479
|
-
|
|
2480
|
-
|
|
2481
|
-
|
|
2482
|
-
|
|
2483
|
-
|
|
2484
|
-
|
|
2485
|
-
|
|
2486
|
-
|
|
2487
|
-
|
|
2488
|
-
|
|
2489
|
-
|
|
3064
|
+
The entries added after `[0.6.0]` arrived a **fifth** way, and it is the
|
|
3065
|
+
one with no external input at all: a strict read of `src/` with nothing failing.
|
|
3066
|
+
Every gate in this repository was green while all three were true, which is the
|
|
3067
|
+
whole of what that way is for — a defect no test is shaped to ask about is
|
|
3068
|
+
reachable only by someone reading for it. **All three have now shipped under
|
|
3069
|
+
`[0.7.0]`**, and each closed having found something a strict read is
|
|
3070
|
+
apparently good for and a gate is not. The runner one: the gate that would have
|
|
3071
|
+
had to catch it *already* looked for the shape, so what the read bought was not a
|
|
3072
|
+
missing gate but a cost nobody had priced. The load-failure diagnosis: every gate
|
|
3073
|
+
was green because the report was *complete* — three ERRORs on a red run — and
|
|
3074
|
+
what was wrong was which three, a judgement no assertion in this repository is
|
|
3075
|
+
shaped to make. The third, eight items under one heading: six comments naming a
|
|
3076
|
+
property the code lacked and two costs beside them.
|
|
3077
|
+
|
|
3078
|
+
**One entry has now left this section the other way — by being rejected** — and
|
|
3079
|
+
it is the first, which is why it is worth a line. The region-marked samples were
|
|
3080
|
+
decided *yes*, and building the count they rested on is what killed them: the two
|
|
3081
|
+
spans the entry named are not spans, and both carry a mark the entry's own rule
|
|
3082
|
+
excludes. It moved down with that measurement attached. The lesson is not about
|
|
3083
|
+
regions. **An entry can sit in `Planned` for releases on the strength of a figure
|
|
3084
|
+
nobody has checked**, because the section's discipline is aimed at keeping the
|
|
3085
|
+
*argument* rather than at re-testing the evidence under it — so the first act of
|
|
3086
|
+
building one should be re-measuring what it claims, not implementing what it
|
|
3087
|
+
proposes. That is what happened here, and it cost an hour instead of a mechanism.
|
|
3088
|
+
|
|
3089
|
+
*What the fifth way cost and returned, now that all of it is spent.* Sixteen
|
|
3090
|
+
items, no external input, and nothing in the suite shaped to catch any of them.
|
|
3091
|
+
The returns were not evenly distributed and the split is the useful part: two
|
|
3092
|
+
defects a user could hit today (a report that misdiagnosed a load failure, a
|
|
3093
|
+
merge crossing midnight), two costs worth a second each on a real command
|
|
3094
|
+
(`check --eval` at 1 + N Vite servers, `control` at 27–84x), one fixed toll on
|
|
3095
|
+
every static invocation (~1 s), and the rest true-but-unobservable — identical
|
|
3096
|
+
output either way. Only the last group is genuinely optional, and it is also the
|
|
3097
|
+
group that would never have been found any other way, because *no observable
|
|
3098
|
+
symptom* is precisely the definition of what a gate cannot see. The transferable
|
|
3099
|
+
lesson is the batching: individually none of the unobservable ones justifies a
|
|
3100
|
+
commit, and together they cost about an hour.
|
|
3101
|
+
|
|
3102
|
+
- **Binding the `✗` samples to fixtures whose rejection is asserted.** A sample
|
|
3103
|
+
showing input the engine must *reject* can be bound the way a runnable one is,
|
|
3104
|
+
to a fixture whose rejection is the thing under test — which is what
|
|
3105
|
+
`fixtures/eval-invalid-registry` already is. This is the more valuable half of
|
|
3106
|
+
what was once one entry with the region-marking below, because a counterexample
|
|
3107
|
+
that quietly stopped being a counterexample is the one kind of stale sample
|
|
3108
|
+
that actively teaches the wrong thing: a reader shown "this fails" about input
|
|
3109
|
+
the engine now accepts is worse off than one shown nothing. It is also the
|
|
3110
|
+
expensive half, needing a fixture per diagnostic, which is why it was never the
|
|
3111
|
+
free part of that entry and is now the only part left in it.
|
|
3112
|
+
*The mirror rule caps what can be claimed on the Chinese side.* `docs/zh/**`
|
|
3113
|
+
matches `docs/en/**` down to the comments inside its code samples, so
|
|
3114
|
+
byte-equality holds on one side only and the zh blocks stay translated prose
|
|
3115
|
+
around the same code — the parity gate in `[0.4.3]` covers them, and it covers
|
|
3116
|
+
structure, not meaning. The residual risk recorded with the whole-file half in
|
|
3117
|
+
`[0.4.3]` — that the gate and the run proving the fixture still works live in
|
|
3118
|
+
different suites — applies here identically.
|
|
2490
3119
|
|
|
2491
3120
|
## Under consideration
|
|
2492
3121
|
|
|
@@ -2500,6 +3129,41 @@ deletion. What each entry has to carry is the evidence currently blocking the
|
|
|
2500
3129
|
decision, because that is what a re-proposal a year from now would otherwise
|
|
2501
3130
|
have to gather again.
|
|
2502
3131
|
|
|
3132
|
+
- **A static sweep for the two properties claimed over the whole of `src/`.**
|
|
3133
|
+
The quantifier table's first strict read left three rows wanting the same
|
|
3134
|
+
thing, and the repo already owns the idiom: `tests/import-boundary.spec.ts` is
|
|
3135
|
+
a sweep over the import graph asserting a cross-cutting property no runtime
|
|
3136
|
+
test can see, written for the same reason — "only the graph shows it".
|
|
3137
|
+
*The two candidates, and they are not equally shippable.* `ATX-15` is the easy
|
|
3138
|
+
one: **no `.sort` outside `byCodeUnit`, and no `localeCompare`.** Measured over
|
|
3139
|
+
`src/` today — 21 `.sort` call sites, every one reaching `byCodeUnit`, but only
|
|
3140
|
+
19 naming it on the call line: `render.ts` goes through its own `compareIds`,
|
|
3141
|
+
and `merge.ts` through a comparator that spans two lines. The only four
|
|
3142
|
+
occurrences of `localeCompare` are comments explaining why it is not used. A sweep would have **zero false positives on the current corpus**,
|
|
3143
|
+
which is the bar `divergent-param` failed and `compound-requirement` cleared —
|
|
3144
|
+
and those two exceptions are already the warning that this needs the AST rather
|
|
3145
|
+
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
|
|
3146
|
+
string interpolated into a terminal line goes through `inline`", and that is
|
|
3147
|
+
not a token scan. The shape that would work is a scenario driving every
|
|
3148
|
+
exported `format*` in `src/cli/report.ts` with hostile input — a real roster,
|
|
3149
|
+
since the module's exports are what a new formatter joins — but each takes a
|
|
3150
|
+
different argument type, so it needs a hostile fixture per signature rather
|
|
3151
|
+
than one loop.
|
|
3152
|
+
*What blocks the decision.* Two things, one per candidate. The `ATX-15` sweep
|
|
3153
|
+
is cheap and precise and would be **the first rule in this repository that
|
|
3154
|
+
polices how `src/` is written rather than what it does** — a lint rule wearing
|
|
3155
|
+
a scenario's clothes, and the question is whether that belongs in `self/` (as
|
|
3156
|
+
an obligation `ATX-15` already carries) or in `tests/` beside the import
|
|
3157
|
+
boundary. Filing it under `ATX-15` makes it a permanent obligation with a
|
|
3158
|
+
scenario; filing it in `tests/` makes it deletable in a green diff, which
|
|
3159
|
+
`ATX-57`'s rationale argues against in as many words. The `ATX-37` roster is
|
|
3160
|
+
blocked on cost rather than principle: eight formatters, eight fixtures, and
|
|
3161
|
+
the honest count of what it buys is three currently-correct sites.
|
|
3162
|
+
*Why neither is urgent.* Both properties hold everywhere today, verified by
|
|
3163
|
+
reading. What is missing is not correctness but the thing that keeps it — and
|
|
3164
|
+
the measurement above is what a re-proposal would otherwise have to gather
|
|
3165
|
+
again.
|
|
3166
|
+
|
|
2503
3167
|
- **Bounding the fan-out in `findFiles` and `parseSpecs`.** Both walk the
|
|
2504
3168
|
project with no concurrency limit: `findFiles` recurses through
|
|
2505
3169
|
`Promise.all(subdirs.map(walk))`, so every directory in the tree is opened at
|
|
@@ -2604,6 +3268,126 @@ have to gather again.
|
|
|
2604
3268
|
here rather than moving to `Considered and rejected`: documenting a trap is
|
|
2605
3269
|
not the same as deciding not to detect it.
|
|
2606
3270
|
|
|
3271
|
+
- **Whether `check` and `verify` should refuse a half-loaded registry the way
|
|
3272
|
+
`cover` and `render` do.** Those two return early on `hasError(loadIssues)`,
|
|
3273
|
+
and the argument is recorded at both call sites: a report computed from a
|
|
3274
|
+
half-loaded registry is a lie, and a lie that reads as a pass is the worst
|
|
3275
|
+
shape it can take — `cover` in particular used to answer `ok: true` on a repo
|
|
3276
|
+
whose intent layer had stopped being readable, because an empty registry is
|
|
3277
|
+
fully covered. `check` and `verify` carry on regardless.
|
|
3278
|
+
*What that produces, measured* on a project with one unreadable `*.reqs.ts`:
|
|
3279
|
+
a `registry-invalid`, then an `orphan-test` for **every** scenario belonging to
|
|
3280
|
+
that file's requirements, plus `empty-spec`, `tests-red` and a
|
|
3281
|
+
`declared-not-run` per scenario — six derived findings from one cause. Worse,
|
|
3282
|
+
`orphan-test`'s fix hint ("Add it to the registry, or fix the id") is *wrong*
|
|
3283
|
+
for an id already sitting in the registry that failed to load, so the loudest
|
|
3284
|
+
advice in the report points at work that must not be done. In a multi-file
|
|
3285
|
+
registry it scales with the broken file's scenario count, and the one true
|
|
3286
|
+
finding is at the top where a wall of red pushes it out of view.
|
|
3287
|
+
*Why it is not simply the same call as `cover` and `render`.* Each of those
|
|
3288
|
+
produces exactly **one** artifact, and refusing to produce it is the whole of
|
|
3289
|
+
the decision. `check` is the command a pipeline runs first and its contract is
|
|
3290
|
+
breadth — returning three findings instead of nine because one of the nine was
|
|
3291
|
+
a load failure makes it quieter about problems it can still see perfectly well,
|
|
3292
|
+
which is the opposite of what it is for. `verify` has the same shape with a run
|
|
3293
|
+
attached.
|
|
3294
|
+
*What blocks the decision.* The narrower move — keep reporting everything
|
|
3295
|
+
except the findings **derived from** the missing registry, i.e. suppress
|
|
3296
|
+
`orphan-test` for ids whose prefix belongs to a file that failed to load —
|
|
3297
|
+
needs a prefix→file mapping for a file that never parsed, and that is exactly
|
|
3298
|
+
the file `loadRegistry` has no ids from. Recovering the mapping means reading
|
|
3299
|
+
the prefix out of something other than the registry's own keys (the file name,
|
|
3300
|
+
or a partial AST read that survives the failure), and whether either is
|
|
3301
|
+
trustworthy enough to gate a suppression on is unmeasured. Suppressing on the
|
|
3302
|
+
weaker signal "some registry file failed to load, so suppress all
|
|
3303
|
+
`orphan-test`" is the other candidate and is a strictly worse `check` on a
|
|
3304
|
+
multi-file registry, where the other files are fine and their orphans are real.
|
|
3305
|
+
**The wrong *advice* has been separated out and fixed; the noise has not.**
|
|
3306
|
+
`[0.7.0]` changes `orphan-test`'s fix hint when any registry file failed
|
|
3307
|
+
to load, so the loudest line in that report stops telling the reader to add an
|
|
3308
|
+
id that already exists. That needed no mapping, because it claims nothing about
|
|
3309
|
+
*which* orphans came from the broken file — only that the command cannot tell,
|
|
3310
|
+
and what order to work in. It is worth separating because the two halves were
|
|
3311
|
+
filed as one problem and are not: **being wrong** was fixable today at the cost
|
|
3312
|
+
of a sentence, and **being loud** is the part still blocked on the mapping
|
|
3313
|
+
above. The entry stays open on exactly that, with one blocker retired: the
|
|
3314
|
+
measured six derived findings from one cause are still six, and only one of
|
|
3315
|
+
them has stopped pointing at work that must not be done.
|
|
3316
|
+
|
|
3317
|
+
- **A statement that quantifies over a surface, with scenarios covering only the
|
|
3318
|
+
part that existed when it was written.** Found by reading, and it had already
|
|
3319
|
+
cost something: `ATX-37` says "**everything** the CLI writes to a terminal —
|
|
3320
|
+
a report, and the stack of a crash — MUST replace every control character",
|
|
3321
|
+
and `self/report.spec.ts` covers `formatIssues` and `formatCrash`. Those were
|
|
3322
|
+
the two functions that existed when it was written. `render` builds a document
|
|
3323
|
+
by concatenation that never went past the sanitiser, and a registry could drive
|
|
3324
|
+
the reviewer's terminal from `attest render` with no flag at all — shipped
|
|
3325
|
+
under `[0.7.0]` as the fix, but the *shape* is not fixed and is not
|
|
3326
|
+
specific to that requirement.
|
|
3327
|
+
The shape: a universal quantifier in the statement, a finite set of scenarios,
|
|
3328
|
+
and nothing anywhere that holds the two together. It reads as covered — the id
|
|
3329
|
+
has scenarios, `cover` is green, `check` is green — and the quantifier is the
|
|
3330
|
+
part no gate is looking at. Every one of the three mechanisms this project
|
|
3331
|
+
sells operates *below* it: coverage asks whether an id has a scenario,
|
|
3332
|
+
never-red asks whether that scenario discriminated, drift asks whether it reads
|
|
3333
|
+
the params. None asks whether the scenarios span what the sentence claims.
|
|
3334
|
+
*What blocks the decision.* Detecting it means knowing which surface a
|
|
3335
|
+
statement names, which is prose judgement — the same judgement
|
|
3336
|
+
`tests/intent-rule-candidates.spec.ts` records as the reason obligation
|
|
3337
|
+
counting can never be an engine rule ("asserting a ground truth Attest cannot
|
|
3338
|
+
compute for someone else's requirements, which is exactly what §0 refuses").
|
|
3339
|
+
So the shippable form may not exist.
|
|
3340
|
+
**The repo half has shipped, and this entry is now only about the engine
|
|
3341
|
+
half.** `[0.7.0]` adds the `QUANTIFIED` table it described — hand-judged,
|
|
3342
|
+
a row per flagged statement, every named scenario asserted to exist — and it
|
|
3343
|
+
closed the way an entry built from a strict read is supposed to: it found a
|
|
3344
|
+
live hole on `ATX-62`, the one requirement written specifically to avoid this
|
|
3345
|
+
shape, where the archive gate path was claimed by the statement and asserted by
|
|
3346
|
+
nothing. Two things came back that the entry did not predict. The useful one is
|
|
3347
|
+
that "names a surface" splits three ways rather than two — `values`, `roster`,
|
|
3348
|
+
`sites` — and only `sites` carries the risk, because a `roster` scenario
|
|
3349
|
+
iterates the set the code owns and grows with it. That is a sharper statement
|
|
3350
|
+
of the defect than "the scenarios do not span the sentence", and it is a rule
|
|
3351
|
+
an author can act on: **prefer a quantifier a scenario can iterate.** The other
|
|
3352
|
+
is that the remedy — rewriting the statement to name its surfaces, as `ATX-62`
|
|
3353
|
+
did — takes it out of the flagged set, so the table has to allow rows for ids
|
|
3354
|
+
the detector does not flag or it loses the binding exactly when the author does
|
|
3355
|
+
the right thing.
|
|
3356
|
+
*What stays open is the engine half, and nothing above touches it.* The three
|
|
3357
|
+
kinds are prose judgement, the detector misses the article form ("A registry
|
|
3358
|
+
that cannot be read MUST …") which is a universal too, and no rule can tell
|
|
3359
|
+
which surface a sentence claims without being told. The repo half is a
|
|
3360
|
+
counter-pressure, not a detector, and it holds for one registry whose authors
|
|
3361
|
+
agreed to maintain it.
|
|
3362
|
+
|
|
3363
|
+
- **Whether a requirement can ever be retired, and what it costs that none can.**
|
|
3364
|
+
The registry is at 62 requirements and 160 scenarios, roughly 2.6 each, and
|
|
3365
|
+
every one is a permanent obligation: `uncovered-requirement` is an ERROR, so a
|
|
3366
|
+
requirement without a scenario cannot exist, and nothing ever removes one.
|
|
3367
|
+
`delta.removed` exists and `applyDelta` applies it, but `--apply` refuses to
|
|
3368
|
+
write it back, deliberately and with the reason recorded — REMOVED "cannot say
|
|
3369
|
+
which comments belonged to the entry it deletes". So retiring one is a manual
|
|
3370
|
+
registry edit, which is fine as a mechanism and says nothing about whether it
|
|
3371
|
+
is ever the right move.
|
|
3372
|
+
*What the growth actually costs, measured on this session rather than
|
|
3373
|
+
estimated.* One small fix took: a requirement, its scenarios, a hand-judged
|
|
3374
|
+
row in the `OBLIGATIONS` table, two inline snapshots, a `[Unreleased]` entry,
|
|
3375
|
+
History rows in two languages, and a regenerated `SPEC.md`. That is
|
|
3376
|
+
proportionate for a spec framework that has to dogfood — the ceremony *is* the
|
|
3377
|
+
product being used on itself — and it is also a number that only goes up.
|
|
3378
|
+
*What blocks the decision, and it is not the mechanism.* It is that "retire"
|
|
3379
|
+
has at least three meanings this file has never had to separate: a requirement
|
|
3380
|
+
that turned out **wrong** (which should leave a record of being wrong, and
|
|
3381
|
+
`Considered and rejected` is where that already goes); one still **true** but
|
|
3382
|
+
no longer worth a permanently-run scenario; and one **subsumed** by a later
|
|
3383
|
+
requirement, where the scenario should move rather than go. Only the second is
|
|
3384
|
+
really about cost, and it is the one where deleting the intent to save the
|
|
3385
|
+
scenario inverts what the whole tool claims — that intent outlives its tests.
|
|
3386
|
+
Nothing here is urgent while the number is 61. It is filed now because the
|
|
3387
|
+
moment to write the argument down is while the reasoning is available, and
|
|
3388
|
+
because a project whose requirement count only rises should have decided in
|
|
3389
|
+
advance what would make one leave.
|
|
3390
|
+
|
|
2607
3391
|
## Considered and rejected
|
|
2608
3392
|
|
|
2609
3393
|
Decisions **not** to build something, kept where they can be found before the
|
|
@@ -2614,6 +3398,45 @@ it sat between 0.2.0 and 0.1.7 for two releases, where standing still meant
|
|
|
2614
3398
|
sinking one version deeper each time a release was cut above it, and a rejection
|
|
2615
3399
|
filed under a version reads as belonging to it.
|
|
2616
3400
|
|
|
3401
|
+
### Region-marked samples, so a partial quote could be bound too
|
|
3402
|
+
|
|
3403
|
+
Decided **yes** and filed under `Planned`, then killed by building the count it
|
|
3404
|
+
rested on. The whole-file half shipped in `[0.4.3]` — the README's two
|
|
3405
|
+
getting-started blocks are byte-equal to `fixtures/consumer/` — and the proposal
|
|
3406
|
+
was to raise that with `// #region readme:registry` markers inside the fixture,
|
|
3407
|
+
asserting a doc block against the dedented span rather than the whole file. The
|
|
3408
|
+
mechanism is sound and cheap. What it has no supply of is spans to bind.
|
|
3409
|
+
|
|
3410
|
+
**The entry named its own two candidates, and both fail on inspection.** It said
|
|
3411
|
+
`troubleshooting.md`'s single-source pair is "two verbatim lines of
|
|
3412
|
+
`fixtures/consumer/session.spec.ts`": they are lines 8 and 11, so no span
|
|
3413
|
+
contains them, and line 8 reads `// single source, typed \`30\` — no cast` in the
|
|
3414
|
+
fixture against `// ✓ the single source` on the page. It said the `AUTH-3` block
|
|
3415
|
+
is "the registry entry at a different indent": the fixture wraps `statement:`
|
|
3416
|
+
onto a second line and the page does not, and the page's comment carries a `✓`
|
|
3417
|
+
the fixture has no reason to. **Both candidates carry a `✓`, which the same entry
|
|
3418
|
+
excludes by rule** — "anything carrying a `✓`/`✗` is authored commentary and
|
|
3419
|
+
stays a fragment". The entry's estimate contradicted the entry's own rule, and
|
|
3420
|
+
neither half was checked against the files until now.
|
|
3421
|
+
|
|
3422
|
+
Swept the rest rather than stopping at the two: of the 23 fenced `ts` blocks in
|
|
3423
|
+
the README and `docs/en/**`, 2 are already bound whole, 3 are `src/` sketches the
|
|
3424
|
+
module-scope check in `[0.7.0]` covers, and the remaining 18 are elisions
|
|
3425
|
+
(`{ /* … */ }`), `✓`/`✗` fragments, input the engine must reject, or teaching
|
|
3426
|
+
sketches of the *reader's* project carrying inline commentary that is the point
|
|
3427
|
+
of them. **Zero spans.** So building it would ship a gate binding nothing, which
|
|
3428
|
+
this project treats as worse than the comment recording it — the argument §11
|
|
3429
|
+
makes and `ATX-43` and `ATX-44` both decline a promise on.
|
|
3430
|
+
|
|
3431
|
+
What that leaves is the honest general form: **the reach of a byte-equality gate
|
|
3432
|
+
over documentation is set by how the documentation is written, not by the
|
|
3433
|
+
granularity the gate can address.** Raising it would mean rewriting the pages to
|
|
3434
|
+
be quotable, which is the tail wagging the dog — those `✓`/`✗` annotations and
|
|
3435
|
+
elisions are what makes a sample teach. The counterexample half was never
|
|
3436
|
+
dependent on this and stays under `Planned` as its own entry. If a passage is
|
|
3437
|
+
ever written that *is* a verbatim span of a fixture, this is three lines of
|
|
3438
|
+
`fencedBlocks` away and the reasoning above is what to re-read first.
|
|
3439
|
+
|
|
2617
3440
|
### `typescript` as a peer dependency instead of a bundled one
|
|
2618
3441
|
|
|
2619
3442
|
The open question the matrix entry was queued to settle (see `[0.4.2]`), and
|
|
@@ -2924,7 +3747,8 @@ The permanent half goes to design §11 with the params rule, not here.
|
|
|
2924
3747
|
adopter owes their users, whose rows a reviewer would want in the rendered spec.
|
|
2925
3748
|
None has been seen; the one that raised it was tuning.
|
|
2926
3749
|
|
|
2927
|
-
[Unreleased]: https://gitlab.com/Pseudorca/attest/-/compare/v0.
|
|
3750
|
+
[Unreleased]: https://gitlab.com/Pseudorca/attest/-/compare/v0.7.0...main
|
|
3751
|
+
[0.7.0]: https://gitlab.com/Pseudorca/attest/-/tags/v0.7.0
|
|
2928
3752
|
[0.6.0]: https://gitlab.com/Pseudorca/attest/-/tags/v0.6.0
|
|
2929
3753
|
[0.5.0]: https://gitlab.com/Pseudorca/attest/-/tags/v0.5.0
|
|
2930
3754
|
[0.4.3]: https://gitlab.com/Pseudorca/attest/-/tags/v0.4.3
|