@am_shork/attest 0.10.0 → 1.0.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 +2103 -739
- package/README.md +14 -7
- package/dist/core/gate.d.ts +16 -6
- package/dist/core/gate.js +18 -8
- package/dist/core/red-record.d.ts +4 -4
- package/dist/core/red-record.js +12 -3
- package/dist/core/registry.d.ts +31 -1
- package/dist/core/registry.js +4 -0
- package/dist/core/skill.js +49 -28
- package/dist/core/splice.d.ts +20 -0
- package/dist/core/splice.js +25 -1
- package/dist/core/static-registry.js +81 -17
- package/dist/core/validator.js +1 -1
- package/package.json +2 -1
package/CHANGELOG.md
CHANGED
|
@@ -7,16 +7,620 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## Versioning
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
From `[1.0.0]` a breaking change bumps the **major**. "Breaking" means it
|
|
11
11
|
changes the exit code of an existing valid setup, rejects previously-valid
|
|
12
12
|
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
|
-
not API.
|
|
14
|
+
not API. The definition has not moved; what moved is the bump it earns, and the
|
|
15
|
+
whole history below `[1.0.0]` was written under the 0.x rule, where the same
|
|
16
|
+
definition earned a **minor**.
|
|
17
|
+
|
|
18
|
+
**One surface keeps the 0.x rule, and it is named rather than inferred.**
|
|
19
|
+
`./inspect` is excluded from the freeze below, so a breaking change to it is a
|
|
20
|
+
minor until a second adopter's usage exists. Everything else the next section
|
|
21
|
+
lists takes the major.
|
|
22
|
+
|
|
23
|
+
`schemaVersion` in the `--json` report is a **separate axis** and not a
|
|
24
|
+
restatement of this one: it is bumped whenever the emitted shape changes
|
|
25
|
+
incompatibly, and an additive field bumps neither it nor the package. Removing
|
|
26
|
+
or renaming a `--json` field is breaking by the definition above and is
|
|
27
|
+
therefore both — a `schemaVersion` bump and, from 1.0, a major.
|
|
28
|
+
|
|
29
|
+
**A narrowing that closes a security hole is breaking by that definition, and
|
|
30
|
+
takes the bump that definition gives it — including a major.** There is no
|
|
31
|
+
carve-out for it and no flag. The two alternatives were priced and both cost
|
|
32
|
+
more than the version number: an exemption needs a test for what counts as a
|
|
33
|
+
security narrowing, and nothing here can hold a prose rule to one, so the label
|
|
34
|
+
would be available to every inconvenient narrowing and judged by the person
|
|
35
|
+
making it. Shipping the new refusal behind an opt-in that defaults off is worse
|
|
36
|
+
for the case it is meant to serve — the four commands documented as safe on an
|
|
37
|
+
untrusted fork MR are the ones a reviewer runs on a file they have not read, and
|
|
38
|
+
a default that keeps answering wrong unless the reviewer knows to pass a flag
|
|
39
|
+
has not been fixed.
|
|
40
|
+
|
|
41
|
+
*What that costs is stated rather than hidden.* Under 0.x it costs nothing: the
|
|
42
|
+
same fix is a minor either way, which is why this could go undecided for so
|
|
43
|
+
long. Under 1.0 it is a major on whatever schedule a hole is found — the
|
|
44
|
+
`callsAuthoringFn` binding fix, the fifth breaking commit in this project's
|
|
45
|
+
history, would have been a 2.0 twenty-five days after a 1.0 cut at the fourth.
|
|
46
|
+
The pressure that creates lands on the version number, and the failure it
|
|
47
|
+
invites is the other one: a fix weakened until it fits a bump somebody
|
|
48
|
+
preferred. A narrowing that ships smaller than the defect it closes is what this
|
|
49
|
+
paragraph exists to make visible.
|
|
50
|
+
|
|
51
|
+
## What 1.0 freezes
|
|
52
|
+
|
|
53
|
+
Decided 2026-08-31 and binding since `[1.0.0]`, where the argument, the
|
|
54
|
+
measurement it rests on and what is still unmeasured are recorded. The line
|
|
55
|
+
exists because every open entry waiting on an adopter is asking somebody to build
|
|
56
|
+
on a surface documented as movable, and a line is the only thing that makes that
|
|
57
|
+
a reasonable ask. It is here rather than only there because a stability promise a
|
|
58
|
+
reader cannot find is not one.
|
|
59
|
+
|
|
60
|
+
**Frozen at 1.0: the CLI — its commands, options and exit codes — and the root
|
|
61
|
+
export `.`.** Both are frozen on evidence that they have stopped moving rather
|
|
62
|
+
than on intention: the CLI's last change of shape was `--apply` on 2026-08-03,
|
|
63
|
+
twelve releases ago, and `src/index.ts` has had its export list edited in three
|
|
64
|
+
commits ever, the last on 2026-07-26.
|
|
65
|
+
|
|
66
|
+
**Explicitly not frozen at 1.0: `./inspect`.** It shipped in `[0.9.2]` and has
|
|
67
|
+
two releases and one adapter behind it. Freezing it would be freezing on one
|
|
68
|
+
adapter, which is the objection this project raises against every premature
|
|
69
|
+
interface and does not get to waive for its own. It stays under the 0.x rule
|
|
70
|
+
above until a second adopter's usage exists — stated here, rather than left to
|
|
71
|
+
be inferred from a version number that would say otherwise. `./define` sits
|
|
72
|
+
between the two and is frozen with the CLI, on seven releases.
|
|
73
|
+
|
|
74
|
+
**1.x, not 1.0:** everything currently blocked on an adopter's evidence — the
|
|
75
|
+
judgement schema, any retrieval surface over the archive. The freeze is not a
|
|
76
|
+
claim that those are settled. (A backlink index over `archive/` was named here
|
|
77
|
+
until 2026-09-01, when it was rejected outright; it is not deferred to 1.x, and
|
|
78
|
+
what closed it is at the foot of this file.)
|
|
79
|
+
|
|
80
|
+
**Not a frozen surface, and deliberately: the workflow document `attest init`
|
|
81
|
+
writes into the adopter's repository.** It is a function of the installed version
|
|
82
|
+
— which is what makes re-running `init` after an upgrade the whole update
|
|
83
|
+
mechanism — so freezing it would freeze the workflow the tool teaches. What 1.0
|
|
84
|
+
does not do is make its drift detectable, and that is a live question at the foot
|
|
85
|
+
of this file rather than something this line settles. It is named here because
|
|
86
|
+
the first adoption measured across an upgrade paid nothing on the four surfaces
|
|
87
|
+
above and 110 stale lines on this one.
|
|
88
|
+
|
|
89
|
+
*What this costs is the paragraph above.* Under 1.0 a narrowing that closes a
|
|
90
|
+
security hole takes a major, on whatever schedule a hole is found. That price is
|
|
91
|
+
now stated rather than unknown, which is what made the line decidable at all.
|
|
92
|
+
|
|
93
|
+
## [1.0.0] - 2026-09-01
|
|
94
|
+
|
|
95
|
+
### Added
|
|
96
|
+
|
|
97
|
+
- **`ATX-87` gates the CLI's command and option roster, which is the other half
|
|
98
|
+
of the same freeze.** `What 1.0 freezes` names two surfaces in one sentence —
|
|
99
|
+
the CLI, its commands, options and exit codes, and the root export `.` — and
|
|
100
|
+
after `ATX-86` three of those four halves were held: the export by `ATX-86`,
|
|
101
|
+
the exit codes by the requirement making `hasError` the single predicate the
|
|
102
|
+
verdict, the JSON `ok` and the process status all read from, and by the one
|
|
103
|
+
keeping the report alone on stdout. **The roster was held by nothing.** Seven
|
|
104
|
+
commands and seventeen options, and no test anywhere enumerated either, so
|
|
105
|
+
deleting a flag or renaming a command broke nothing a contributor runs.
|
|
106
|
+
`ATX-16.staticCommands` reads like the missing gate and is not: it names four
|
|
107
|
+
commands in order to say they execute no project code, which is a claim about
|
|
108
|
+
behaviour rather than a claim that the roster is closed.
|
|
109
|
+
|
|
110
|
+
**Read out of `--help` rather than out of `cli/index.ts`, and that is forced
|
|
111
|
+
rather than preferred.** Every option string in the source is a constant —
|
|
112
|
+
`JSON_FLAG`, `EVAL_FLAG`, `VITEST_CONFIG_FLAG`, `TARGET_FLAG` — so a static
|
|
113
|
+
walk for literals sees the command names and almost none of the flags, and
|
|
114
|
+
resolving the constants would mean reimplementing enough of the emitter to
|
|
115
|
+
guess what it emits. The built module cannot be introspected either:
|
|
116
|
+
`program.parseAsync()` runs at module scope and `program` is never exported,
|
|
117
|
+
so importing `dist/cli/index.js` runs the CLI instead of describing it. What
|
|
118
|
+
is left is the artifact a consumer actually reads, and it is the right ground
|
|
119
|
+
truth in both directions — a flag that works and is absent from the help is
|
|
120
|
+
not on the published surface, and one documented there is, whatever the source
|
|
121
|
+
says.
|
|
122
|
+
|
|
123
|
+
**commander's `-h, --help`, `-V, --version` and `help` command are excluded,
|
|
124
|
+
and matched by name rather than by shape.** They are not this project's to
|
|
125
|
+
freeze, and a gate that reddens because a dependency reworded its own built-in
|
|
126
|
+
is red for a change nobody here decided — the shape this repository refused
|
|
127
|
+
once already, when it rejected a freshness gate that could fail a fresh clone
|
|
128
|
+
on somebody else's release. Matching them by long name is what keeps the
|
|
129
|
+
exclusion from hiding one of ours: `-h, --help` and a hypothetical `-j,
|
|
130
|
+
--json` are the same shape, so a shape test would silently drop a real flag
|
|
131
|
+
the day anybody adds a short one.
|
|
132
|
+
|
|
133
|
+
The two rosters are asserted apart because they are separately falsifiable: a
|
|
134
|
+
command list that is exactly right can belong to commands whose flags have all
|
|
135
|
+
been renamed, and every flag can be correct on a command that should not
|
|
136
|
+
exist. Measured — perturbing the CLI with one added command *and* one added
|
|
137
|
+
flag failed both scenarios, while an added command alone would have failed
|
|
138
|
+
only the first, since the option scenario iterates the registry's list and
|
|
139
|
+
would never have visited it. Eight child processes, one per help page,
|
|
140
|
+
spawning `process.execPath` with `bin/attest.js` rather than the installed
|
|
141
|
+
shim, so no shell is involved on either platform; both scenarios carry an
|
|
142
|
+
explicit `timeoutMs` because the CLI's start-up cost puts the option scenario
|
|
143
|
+
over Vitest's 5 s default.
|
|
144
|
+
|
|
145
|
+
No engine code changed, no command or flag moved, and no `--json` field or
|
|
146
|
+
issue `code` was added: this is a self-requirement and a spec file.
|
|
147
|
+
|
|
148
|
+
- **`ATX-86` gates the shape of what `.` exports, ahead of the release that
|
|
149
|
+
freezes it.** Nothing in the suite enumerated the public surface, and the
|
|
150
|
+
three things that looked as though they did each answered a different
|
|
151
|
+
question: `tests/consumer.spec.ts` asserts that every specifier in the
|
|
152
|
+
`exports` map *resolves*, which is reachability rather than shape;
|
|
153
|
+
`tests/smoke.spec.ts` asserts four runtime exports are functions, and is
|
|
154
|
+
silent about the eleven type-only names beside them and about an export
|
|
155
|
+
arriving that nobody intended; and the four `*.spec-d.ts` files pin the
|
|
156
|
+
behaviour of particular types against `src/` rather than against the emitted
|
|
157
|
+
declarations. So dropping a type export, or widening `Issue.code` back to
|
|
158
|
+
`string`, broke nothing a contributor runs — on the surface `What 1.0 freezes`
|
|
159
|
+
promises not to move.
|
|
160
|
+
|
|
161
|
+
**Stated over the built package and attested over its two artifacts
|
|
162
|
+
separately**, which is `ATX-79`'s shape and is not an analogy to it. `export
|
|
163
|
+
type` is erased whole at emit, so `Issue` is a name in `dist/index.d.ts` and is
|
|
164
|
+
absent from `dist/index.js`, and a consumer importing it as a value gets
|
|
165
|
+
nothing. That asymmetry is the claim rather than an accident of it, and it is
|
|
166
|
+
invisible to any walk over `src/index.ts`, where both kinds are one export
|
|
167
|
+
list. The declared lists live in `params`, so each scenario compares the
|
|
168
|
+
registry against the artifact and the two terms are independent in both
|
|
169
|
+
directions: a barrel the registry does not follow goes red, and so does the
|
|
170
|
+
reverse.
|
|
171
|
+
|
|
172
|
+
**What it does not close, stated here rather than found later.** An edit that
|
|
173
|
+
removes a name from the barrel *and* the registry moves both terms together
|
|
174
|
+
and stays green. That is a deliberate registry edit, visible as a `SPEC.md`
|
|
175
|
+
diff under `render --check`, and review is the gate for it — the same answer
|
|
176
|
+
the foot of this file reaches for a param whose value moves. The count pinned
|
|
177
|
+
beside each comparison is the independent term against the careless half of
|
|
178
|
+
that case and is not a claim to have closed it.
|
|
179
|
+
|
|
180
|
+
No engine code changed, no command or flag moved, and no `--json` field or
|
|
181
|
+
issue `code` was added: this is a self-requirement and a spec file. The entry
|
|
182
|
+
that proposed it was filed under `Under consideration` on 2026-09-01 from a
|
|
183
|
+
comparative read of `nostics` 1.2.0, promoted to `Planned` the same day once
|
|
184
|
+
re-measuring corrected its cost, and the correction is the reason it was
|
|
185
|
+
buildable immediately — it had priced a build coupling that `self/` already
|
|
186
|
+
carries, having considered only `tests/` and the consumer suite as homes.
|
|
187
|
+
|
|
188
|
+
### Changed
|
|
189
|
+
|
|
190
|
+
- **`uncovered-requirement` stops advertising an affordance that has never
|
|
191
|
+
existed.** The message read *"Add a scenario, or mark it deferred"*, and
|
|
192
|
+
`deferred` appears nowhere else in the project: not a field of
|
|
193
|
+
`RequirementSchema`, not a flag on any command, not a key in the report. The
|
|
194
|
+
troubleshooting section for the code quoted the message as its symptom and
|
|
195
|
+
then explained only the first half, because there is no second half to
|
|
196
|
+
explain. So the one place a reader looks when they hit this error named an
|
|
197
|
+
escape hatch, in shipped output, in every release to date. It now names the
|
|
198
|
+
two exits that exist — write a scenario, or delete the entry — and
|
|
199
|
+
troubleshooting says why there is no third: an id Attest holds is an
|
|
200
|
+
obligation it holds permanently.
|
|
201
|
+
|
|
202
|
+
**The trace of the affordance having been intended is kept rather than
|
|
203
|
+
deleted**, which is why this waited. `Considered and rejected` gains the
|
|
204
|
+
entry that found it (*Retiring a requirement*), and design §11 gains the
|
|
205
|
+
permanence as a property to design against rather than a gap awaiting a
|
|
206
|
+
mechanism. Rewording the message alone would have removed the only written
|
|
207
|
+
evidence that anybody ever meant to build the thing.
|
|
208
|
+
|
|
209
|
+
`Issue.message` is prose and not API, so no consumer branches on this and no
|
|
210
|
+
exit code moves; there is no `code`, `level` or `--json` field change and
|
|
211
|
+
nothing to bump.
|
|
212
|
+
|
|
213
|
+
- **1.0: the CLI and `.` are frozen, and the freeze ships with the gates that
|
|
214
|
+
enforce it.** The line itself is at the head of this file under `What 1.0
|
|
215
|
+
freezes` — a stability promise a reader cannot find is not one — and this is
|
|
216
|
+
the release that makes it binding. Decided 2026-08-31, cut 2026-09-01.
|
|
217
|
+
|
|
218
|
+
**Frozen: the CLI — its commands, options and exit codes — and the root export
|
|
219
|
+
`.`.** `./define` is frozen with them, on seven releases. Both are frozen on
|
|
220
|
+
evidence that they had stopped moving rather than on intention: the CLI's last
|
|
221
|
+
change of shape was `--apply` on 2026-08-03, twelve releases before the
|
|
222
|
+
decision, and `src/index.ts` has had its export list edited in three commits
|
|
223
|
+
ever.
|
|
224
|
+
|
|
225
|
+
**Not frozen, explicitly: `./inspect`.** It shipped in `[0.9.2]` with two
|
|
226
|
+
releases and one adapter behind it, and freezing on one adapter is the
|
|
227
|
+
objection this project raises against every premature interface and does not
|
|
228
|
+
get to waive for its own. Stating the exclusion is the point: a version number
|
|
229
|
+
alone would have said the opposite.
|
|
230
|
+
|
|
231
|
+
**What the freeze costs is stated rather than discovered later.** Under 1.0 a
|
|
232
|
+
narrowing that closes a security hole takes a major, on whatever schedule a
|
|
233
|
+
hole is found. `[0.11.0]` is what made that price knowable — it recorded that
|
|
234
|
+
such a narrowing takes the bump its definition gives it, and that the
|
|
235
|
+
`callsAuthoringFn` fix would have been a 2.0 twenty-five days after a 1.0 cut
|
|
236
|
+
at the fourth breaking commit.
|
|
237
|
+
|
|
238
|
+
**The line was decided with neither frozen surface enforced by anything, and
|
|
239
|
+
that is no longer true.** A frozen surface with no gate on its shape is a
|
|
240
|
+
promise nothing enforces, and the measurement behind the decision recorded when
|
|
241
|
+
each surface last *moved*, never what would notice if it moved again. `ATX-86`
|
|
242
|
+
and `ATX-87` in this release close both halves: the eleven type-only names and
|
|
243
|
+
four values of the root export, read from the emitted declaration file and the
|
|
244
|
+
emitted JavaScript separately, and the seven commands and seventeen options of
|
|
245
|
+
the CLI, read from the help it prints. Neither existed when the line was
|
|
246
|
+
decided.
|
|
247
|
+
|
|
248
|
+
**The exclusion list is one item longer than the four surfaces, and the fifth
|
|
249
|
+
is the one that moved.** The workflow document `attest init` writes into an
|
|
250
|
+
adopter's repository is deliberately not frozen — it is a function of the
|
|
251
|
+
installed version, so freezing it would freeze the workflow the tool teaches.
|
|
252
|
+
A 0.9.1 → 0.11.0 upgrade report measured it at **110 changed lines over 9
|
|
253
|
+
hunks**, still stating a `never-red` ordering rule `[0.10.0]` withdrew, with
|
|
254
|
+
four gates green and nothing reporting it. What 1.0 does **not** do is make
|
|
255
|
+
that drift detectable; the question is open at the foot of this file, and the
|
|
256
|
+
release is explicit about it rather than quiet.
|
|
257
|
+
|
|
258
|
+
**1.0 settles what an adopter may build on and unblocks nothing else.** The
|
|
259
|
+
judgement schema and any retrieval surface over the archive stay under `Under
|
|
260
|
+
consideration` and are explicitly 1.x. The one adoption crossing both of
|
|
261
|
+
`[0.11.0]`'s breaking changes paid nothing — no file edited, no peer upgrade,
|
|
262
|
+
no gate going red — but that sample dodged both narrowings, so what is measured
|
|
263
|
+
is that the release cost a *conforming* adopter zero, not that either narrowing
|
|
264
|
+
is cheap. And what a release costs its adopters is not known on the day it is
|
|
265
|
+
cut, which the foot of this file records as a general rule and which applies to
|
|
266
|
+
this one: the freeze is cut on the strength of the line being **correct**, not
|
|
267
|
+
on evidence that it is **sufficient**.
|
|
268
|
+
|
|
269
|
+
## [0.11.0] - 2026-08-28
|
|
270
|
+
|
|
271
|
+
### Added
|
|
272
|
+
|
|
273
|
+
- **`examples/judgement-table/` — the enforcement half of the intent-quality
|
|
274
|
+
method, as one runnable file with no columns.** `intent-quality.md` has told
|
|
275
|
+
an adopter to build a judgement table since `[0.9.2]` shipped `/inspect`, and
|
|
276
|
+
the path to producing one was five voluntary steps with a scaffold at none of
|
|
277
|
+
them: a row in a document table, a 214-line page, a hand pass over four
|
|
278
|
+
questions, then writing the two *mechanical* properties from a prose
|
|
279
|
+
description. The last of those is the step that did not need to be voluntary.
|
|
280
|
+
Completeness and rot-binding do not depend on what the adopter decided to
|
|
281
|
+
measure — their content is fixed — so they are the half where a copy is not an
|
|
282
|
+
echo, and the four judgement columns stay withheld for the reason `[0.9.2]`
|
|
283
|
+
gave: a second table built by copying ours is evidence about nothing.
|
|
284
|
+
|
|
285
|
+
**The columns are absent by construction rather than left blank.**
|
|
286
|
+
`Judgement` ships one optional `pin` — the anchor rot-binding needs, holding a
|
|
287
|
+
scenario name rather than a judgement about one — and adding a required field
|
|
288
|
+
to it makes **every existing row fail to compile**, measured rather than
|
|
289
|
+
claimed: one added field, `TS2741` on both rows. So the type does at author
|
|
290
|
+
time what the completeness assertion does at run time, and a row cannot be
|
|
291
|
+
added without a decision.
|
|
292
|
+
|
|
293
|
+
**It runs in the default suite, and that is the point rather than a detail.**
|
|
294
|
+
`pre-commit-check` §6 already records that the three usage samples on
|
|
295
|
+
`intent-quality.md` are consumer usage of the public API that no gate reaches;
|
|
296
|
+
a fourth copy of them in a directory that *looks* executable would be worse
|
|
297
|
+
than a fenced block, not better. So `vitest.config.ts` includes
|
|
298
|
+
`examples/**/*.spec.ts`, the specifier `@am_shork/attest/inspect` is aliased to
|
|
299
|
+
source there and in `tsconfig.typecheck.json`, and both assertions were shown
|
|
300
|
+
to bite before this was called done — a third requirement added to the sample
|
|
301
|
+
fails completeness with `[ 'EX-3' ]`, and renaming the pinned scenario fails
|
|
302
|
+
rot-binding by name. What the alias deliberately does not check is that the
|
|
303
|
+
specifier resolves in an installed package; `tests/consumer.spec.ts` owns that
|
|
304
|
+
against a real `npm install`, and the two together are the whole claim.
|
|
305
|
+
|
|
306
|
+
A third assertion is in the skeleton that is in neither the page nor this
|
|
307
|
+
entry's proposal: `expect(issues).toEqual([])`, **before** the other two. A
|
|
308
|
+
registry file that will not read statically contributes nothing to `registry`,
|
|
309
|
+
so an unread file is indistinguishable from a project with fewer requirements
|
|
310
|
+
and both completeness directions pass over it. It was not written from
|
|
311
|
+
reasoning — an invalid mutation during the bite test produced exactly that
|
|
312
|
+
state, `registry: {}` with one `registry-invalid` issue, and the guard is what
|
|
313
|
+
caught it.
|
|
314
|
+
|
|
315
|
+
*What it cost elsewhere, because it is the part that does not announce
|
|
316
|
+
itself.* The sample project is a registry, and `tests/intent-rule-candidates.spec.ts`
|
|
317
|
+
walks every `*.reqs.ts` in the tree — so the corpus moved **95 → 97** and the
|
|
318
|
+
`compound-requirement` snapshot was re-read rather than re-baselined:
|
|
319
|
+
`compound`, `correct` and `missed` are all unchanged, so no rate quoted
|
|
320
|
+
anywhere moved and only the denominator did. Those two ids are deliberately
|
|
321
|
+
dull, which is a biased sample, and it is admitted in the snapshot's comment
|
|
322
|
+
rather than held out — one example directory is one adapter, and a hold-out is
|
|
323
|
+
a seam this repository does not buy until something varies across it.
|
|
324
|
+
|
|
325
|
+
- **This repository now carries the workflow document it ships (`ATX-84`).**
|
|
326
|
+
`attest init` has written `.claude/skills/attest/SKILL.md` since 0.6.0, and this
|
|
327
|
+
repository had never run it: `.claude/skills/` held three hand-written skills and
|
|
328
|
+
no `attest/`, so the workflow half was reachable here only by opening
|
|
329
|
+
`src/core/skill.ts` — the exact gap `ATX-27` exists to close, open in the project
|
|
330
|
+
that ships the fix. Nothing could report it, because an absent file is
|
|
331
|
+
indistinguishable from a repository that has not adopted Attest. `.gitattributes`
|
|
332
|
+
has named the file among the generated files it pins since `fcd6140`, a sentence
|
|
333
|
+
false on the day it was written.
|
|
334
|
+
|
|
335
|
+
The requirement holds the committed file byte-identical to `skillContent()`, so
|
|
336
|
+
editing the workflow text turns `attest verify self` red until `attest init` is
|
|
337
|
+
re-run. That is deliberately *not* the freshness gate `src/core/skill.ts` argues
|
|
338
|
+
against shipping: that argument is about adopters, whose copy goes stale only at
|
|
339
|
+
an upgrade and whose every resulting mistake is a diagnostic carrying a fix hint.
|
|
340
|
+
This is the one place where `skillContent()` changes while the file exists, so
|
|
341
|
+
staleness is an ordinary edit rather than an upgrade event, and the gate costs one
|
|
342
|
+
string comparison. Byte equality rather than `ATX-29`'s document comparison for
|
|
343
|
+
the same reason the distinction runs the other way for `render --check`: that gate
|
|
344
|
+
runs in repositories where Attest does not own `.gitattributes`, this one only
|
|
345
|
+
here, where `eol=lf` is committed.
|
|
346
|
+
|
|
347
|
+
No engine behaviour changes. The document is unchanged, `init` is unchanged, and
|
|
348
|
+
nothing an adopter runs is affected.
|
|
349
|
+
|
|
350
|
+
### Changed
|
|
351
|
+
|
|
352
|
+
- **`delta()` no longer offers the requirements it adds under `added`, because
|
|
353
|
+
that was the one spelling stage 1 could not tell from the right one
|
|
354
|
+
(`ATX-85`).** `delta()` returns the delta *and* the registry of what it
|
|
355
|
+
proposes, so `d.added['AUTH-7'].params.x` and `d['AUTH-7'].params.x` are both
|
|
356
|
+
correct while a change is being authored — and only the second survives
|
|
357
|
+
`--apply`, which repoints the spec's import at the merged registry, and a
|
|
358
|
+
registry has no `added`. Nothing in the pipeline could separate them: the
|
|
359
|
+
archive gate runs *before* the rename, so the spec it graded is not the spec
|
|
360
|
+
the merge commits; `check` executes nothing, so it never evaluates the
|
|
361
|
+
accessor; and the compiler is satisfied on either side because it only ever
|
|
362
|
+
sees whichever half is on disk. The failure arrived one command later as a
|
|
363
|
+
`spec-load-failed` naming the file and not the cause.
|
|
364
|
+
|
|
365
|
+
**The guidance already existed and did not prevent it, which is why this is a
|
|
366
|
+
type and not a warning.** `registry.ts` documents the intended expression and
|
|
367
|
+
the rejected one on the function itself, and the document `init` writes shows
|
|
368
|
+
the by-id form in its worked example; the reach-through was written anyway, by
|
|
369
|
+
the author of the workflow tooling, on the first change this repository put
|
|
370
|
+
through its own flow. The alternative priced against it was a `check`
|
|
371
|
+
WARNING — which would have had to be a warning, since enumerating what a
|
|
372
|
+
change adds is legitimate, and a warning is exactly the strength that already
|
|
373
|
+
failed. `DefinedDelta` withholds the key instead: no diagnostic, no roster
|
|
374
|
+
entry, no runtime cost.
|
|
375
|
+
|
|
376
|
+
**`added?: never` rather than a bare `Omit`, and the difference is nine call
|
|
377
|
+
sites.** `RegistryDelta`'s four fields are all optional, so an added-only delta
|
|
378
|
+
with `added` removed shares *no* property with it and TypeScript's weak-type
|
|
379
|
+
check refuses `applyDelta(base, delta({ added: … }))` outright — measured at 11
|
|
380
|
+
errors, of which 9 are that shape and every one correct. An optional `never`
|
|
381
|
+
restores the overlap and still refuses the read, past both escape hatches:
|
|
382
|
+
`d.added!` and `d.added?.[…]` bottom out in `never`, and only an explicit
|
|
383
|
+
`as any` compiles. That last property is the whole reason this beats the
|
|
384
|
+
WARNING, so it is pinned in `tests/delta-types.spec-d.ts` rather than left to
|
|
385
|
+
hold by luck — a type has no runtime footprint, so a regression here would
|
|
386
|
+
otherwise ship green.
|
|
387
|
+
|
|
388
|
+
**Breaking, and it takes the bump this file's Versioning section gives it.** A
|
|
389
|
+
stage-1 spec reading through `added` stops compiling. The type also now
|
|
390
|
+
deliberately understates its own value — `withProposedRequirements` still
|
|
391
|
+
spreads `added`, and `applyDelta` and the loader still read it through
|
|
392
|
+
`RegistryDelta` — so what is withheld is the authoring surface and not the
|
|
393
|
+
data; `self/registry.spec.ts` keeps asserting exactly that, through a cast that
|
|
394
|
+
is itself the assertion.
|
|
395
|
+
|
|
396
|
+
*Re-measured before implementing, per `How an entry is re-measured`, and the
|
|
397
|
+
filed figures reproduced exactly* — 11 errors in the same two kinds, 11 → 2
|
|
398
|
+
under the variant, `typecheck` clean throughout, all three escape-hatch results
|
|
399
|
+
as recorded. **What the re-measure added was a surface the entry had not
|
|
400
|
+
counted, and it falsified this requirement's first draft before it shipped.**
|
|
401
|
+
`modified`, `removed` and `renamed` vanish at the merge too, and reading any of
|
|
402
|
+
them compiles on both sides — so the statement first drafted for `ATX-85`, that
|
|
403
|
+
withholding `added` leaves the by-id read "the only expression that resolves",
|
|
404
|
+
was false. They are left readable on purpose: none has a by-id equivalent, so
|
|
405
|
+
there is no correct spelling to redirect an author to, and a spec reading one
|
|
406
|
+
is reading delta-only data rather than misspelling a right thing. The narrowed
|
|
407
|
+
statement claims only what the refusal actually buys.
|
|
408
|
+
|
|
409
|
+
*And the exposure claim was one notch too wide.* The entry recorded the window
|
|
410
|
+
between `--apply` and the next `verify` as the whole exposure. In this
|
|
411
|
+
repository it is not: post-merge, reading through any of the four fields fails
|
|
412
|
+
to compile under `noUncheckedIndexedAccess`, so `typecheck:all` bounds it. The
|
|
413
|
+
unbounded form is an adopter's, whose spec files need be in no `tsc` project at
|
|
414
|
+
all — which is the case the refusal is for.
|
|
415
|
+
|
|
416
|
+
*Two things this change made the existing counter-pressure say.* The judgement
|
|
417
|
+
tables refused the new id until it carried a hand-judged row, which is that
|
|
418
|
+
mechanism working on its own author rather than on a hypothetical adopter — and
|
|
419
|
+
the corpus moved 97 → 98 with no rate disturbed. And `divergent-param` churned
|
|
420
|
+
its `field` key a **fourth** time — `ATX-85`'s `added` joining ATX-20, ATX-82
|
|
421
|
+
and ATX-83 — changing no verdict again, which is one more measurement for the
|
|
422
|
+
entry recording that this rule keys on the evidence it discards.
|
|
423
|
+
|
|
424
|
+
### Security
|
|
425
|
+
|
|
426
|
+
- **The static reader matched the authoring call by spelling, so a file could
|
|
427
|
+
shadow `defineRequirements` and hand the two readers different registries.**
|
|
428
|
+
`callsAuthoringFn` accepted any identifier named `defineRequirements` — the
|
|
429
|
+
bare name was added unconditionally as a fallback for a global or a harness —
|
|
430
|
+
and any property access whose *last* segment was that name. So a
|
|
431
|
+
`*.reqs.ts` opening with `const defineRequirements = (r) => ({ ...r,
|
|
432
|
+
'ADMIN-1': … })`, or with a local object carrying a method of that name, was
|
|
433
|
+
read by parsing it as the literal argument in front of the call, while the
|
|
434
|
+
evaluator answered with whatever that function returned.
|
|
435
|
+
|
|
436
|
+
Not a refusal but a **wrong answer**, and pointed at under-reporting: `check`
|
|
437
|
+
called the registry covered, `cover` counted rows that were not the rows, and
|
|
438
|
+
`render` wrote a document with an id missing from it. The reach is what makes
|
|
439
|
+
it worth a section — `check`, `cover`, `render` and `status` are the commands
|
|
440
|
+
documented as safe on an untrusted fork MR *because they execute nothing*
|
|
441
|
+
(`ATX-16`), so they are exactly the ones a reviewer runs on a file they have
|
|
442
|
+
not read, and the registry they report is the one nothing else will mention.
|
|
443
|
+
Two readers of one file disagreeing is the condition the differential suite
|
|
444
|
+
exists to prevent (design §5.2), and it could not have caught this: every
|
|
445
|
+
registry it runs over calls the real function.
|
|
446
|
+
|
|
447
|
+
The rule is now about the **binding** rather than the spelling. An import
|
|
448
|
+
binding is trusted, a name the file never binds is trusted — that is what the
|
|
449
|
+
bare-name fallback has always been for — and a `const` / `let` / `var` /
|
|
450
|
+
`function` / `class` in the file itself is not, whether the callee is that
|
|
451
|
+
name or an object property reached through it. A file that contradicts itself
|
|
452
|
+
becomes `registry-not-static`, with `--eval` named as the way back, which is
|
|
453
|
+
`ATX-17` unchanged rather than a new promise.
|
|
454
|
+
|
|
455
|
+
**What this still does not see is the module graph, and it is not written
|
|
456
|
+
to.** `import { defineRequirements } from './wrapper.js'` binds the name by an
|
|
457
|
+
import and is accepted; what that module exports is not a question the source
|
|
458
|
+
text of *this* file answers, and resolving it would mean reading and then
|
|
459
|
+
trusting another file, which is the evaluator's job. The line drawn is what
|
|
460
|
+
the file contradicts about itself.
|
|
461
|
+
|
|
462
|
+
Rejects input that was previously accepted, so it is breaking under this
|
|
463
|
+
project's 0.x rule — though the shapes it rejects are ones no working registry
|
|
464
|
+
has: the four accepted spellings (the import, an alias, a namespace or default
|
|
465
|
+
import's property, and the bare name) all still read, and each is pinned.
|
|
466
|
+
|
|
467
|
+
- **`repointImport` wrote back at offsets taken from a source it never checked
|
|
468
|
+
compiles.** `ATX-69` states that a source that does not compile must be
|
|
469
|
+
refused rather than read out of whatever the parser recovered, and names the
|
|
470
|
+
three readers that open with `createSourceFile`. This was the fourth site and
|
|
471
|
+
the only one that *writes*: `--apply` repoints a proposed spec's import of the
|
|
472
|
+
delta at the registry, and it opened the compiler itself, so it asked nobody.
|
|
473
|
+
`ts.createSourceFile` recovers — a half-saved spec still yields an import
|
|
474
|
+
declaration and a set of offsets — and those offsets were then written over
|
|
475
|
+
the bytes of a `*.spec.ts` the user cannot regenerate.
|
|
476
|
+
|
|
477
|
+
Unreachable through the command today, because the gate refuses a spec it
|
|
478
|
+
could not parse before a merge starts, and fixed for that reason rather than
|
|
479
|
+
in spite of it: a defence that holds only because something upstream holds is
|
|
480
|
+
not a defence, which is the standard `keySource` in the same file is written
|
|
481
|
+
to. A source that does not compile now comes back unchanged, so `applyMerge`
|
|
482
|
+
— which writes only when the text moved — renames the spec with the import it
|
|
483
|
+
already had, and a re-run finds and reports that state instead of a file
|
|
484
|
+
rewritten at offsets nobody can trust.
|
|
485
|
+
|
|
486
|
+
### Documentation
|
|
487
|
+
|
|
488
|
+
- **`## Versioning` now says what a security narrowing costs a consumer, which
|
|
489
|
+
was undefined and which 0.x was the only reason nobody had to ask.** The
|
|
490
|
+
definition at the head of this file counts *rejecting previously-valid input*
|
|
491
|
+
as breaking and says nothing about security, so under 0.x the question was
|
|
492
|
+
invisible: the same fix is a minor either way. It stopped being hypothetical in
|
|
493
|
+
the same week it was measured — the `callsAuthoringFn` binding fix below is
|
|
494
|
+
breaking precisely *because* it narrows what a static reader accepts, and it
|
|
495
|
+
closes a wrong-answer hole in the four commands documented as safe to run on an
|
|
496
|
+
untrusted fork MR.
|
|
497
|
+
|
|
498
|
+
**Decided: such a fix takes the bump the existing definition gives it,
|
|
499
|
+
including a major.** The three candidate answers are not equivalent and only
|
|
500
|
+
one needed no new machinery. A carve-out naming security narrowings as exempt
|
|
501
|
+
requires a test for what counts as one, and this repository has already
|
|
502
|
+
rejected that shape under another name — *Countable obligations*, killed on the
|
|
503
|
+
argument that **nothing can force the count to be true**; a label nothing can
|
|
504
|
+
hold is available to every inconvenient narrowing and judged by the person
|
|
505
|
+
making it. Reaching the same end additively — the new refusal behind a flag,
|
|
506
|
+
defaulting off until the next major — is the only answer that costs the
|
|
507
|
+
consumer nothing, and it is disqualified by the case it would have to serve
|
|
508
|
+
first: a default that keeps answering wrong unless a reviewer knows to pass a
|
|
509
|
+
flag has not fixed a hole in the commands a reviewer runs on a file they have
|
|
510
|
+
not read.
|
|
511
|
+
|
|
512
|
+
*What it is not.* Not a vulnerability-disclosure policy and not a security
|
|
513
|
+
section for the README — one paragraph in `## Versioning`, in the vocabulary
|
|
514
|
+
that section already uses. And it was needed either way: if 1.0 is never cut
|
|
515
|
+
the paragraph documents a real property of the tool, and if it is, the
|
|
516
|
+
paragraph is a precondition, because a 1.0 without it promises something whose
|
|
517
|
+
price nobody has computed. That is why this could be settled while the 1.0
|
|
518
|
+
entry below stays open — it is decidable from this repository alone, which is
|
|
519
|
+
the property that entry is missing.
|
|
520
|
+
|
|
521
|
+
- **`intent-quality.md` tells people to build on `/inspect` and never said
|
|
522
|
+
whether the shapes move.** `inspect.ts` exports five of them and the page
|
|
523
|
+
carries three samples destructuring them; `SCHEMA_VERSION` governs the `--json`
|
|
524
|
+
envelope and governs none of these. **The policy was not missing — its
|
|
525
|
+
statement was.** The Versioning section already binds them: removing or
|
|
526
|
+
renaming a public API is breaking, and under 0.x that is the minor bump. The
|
|
527
|
+
gap is that the reader this export was written for arrives through a method
|
|
528
|
+
page reached from the README's document table, and nothing on that path said
|
|
529
|
+
so.
|
|
530
|
+
|
|
531
|
+
**Which of two sentences to write was the whole decision, and it turned on
|
|
532
|
+
1.0.** Restating the existing promise reads as a stability guarantee and is the
|
|
533
|
+
honest summary of current policy; saying the shape may move before 1.0 is the
|
|
534
|
+
more useful warning and discourages exactly the adopter whose judgement table
|
|
535
|
+
would settle the schema entry below. With 1.0 not being cut for this release,
|
|
536
|
+
the second is the true one, and the cost is accepted rather than hidden: the
|
|
537
|
+
page now warns off the reader it most needs. It is written as a promise about
|
|
538
|
+
being *told* rather than about nothing changing, which is what the versioning
|
|
539
|
+
policy actually offers.
|
|
540
|
+
|
|
541
|
+
- **`tests-red` told you to read the failures out of a report that does not carry
|
|
542
|
+
them.** The troubleshooting page said "`attest verify` gives you the same run
|
|
543
|
+
with the individual failures". It does not, and never did: `verify` and
|
|
544
|
+
`archive` both run the child suite with `quiet: true` because the CLI renders
|
|
545
|
+
its own report, so the whole output is one `tests-red` — no file, no scenario
|
|
546
|
+
name, no assertion. A reader following that sentence runs the command it names
|
|
547
|
+
and learns nothing new. Both languages now say to run the spec directly with
|
|
548
|
+
your own test command, and say why that is safe (a direct run writes nothing,
|
|
549
|
+
so it cannot touch `first-run.json`) and why it is not a verdict.
|
|
550
|
+
|
|
551
|
+
Found by dogfooding `ATX-84`: making the installed workflow document stale on
|
|
552
|
+
purpose, to check that the scenario's failure carries a fix hint, produced a
|
|
553
|
+
`verify self` output with no failure in it at all.
|
|
15
554
|
|
|
16
555
|
## [0.10.0] - 2026-08-22
|
|
17
556
|
|
|
18
557
|
### Changed
|
|
19
558
|
|
|
559
|
+
- **`never-red` no longer claims to be a rule about writing the test first.**
|
|
560
|
+
Archived as `evidence-not-ordering`. The obligation is unchanged in every
|
|
561
|
+
observable way — same code, same scoping to the ids a change ADDs, same
|
|
562
|
+
blocking condition — and what moved is the registry's account of it, plus
|
|
563
|
+
every document that had turned "red before you implement" into the rule.
|
|
564
|
+
|
|
565
|
+
**The finding is that the ordering was never in the engine.** `keepOutcome` is
|
|
566
|
+
monotonic toward failure in one direction only: a recorded fail is permanent, a
|
|
567
|
+
recorded **pass is provisional**. Its motive was to let an author fix a
|
|
568
|
+
scenario that passed on its first run rather than hand-delete the evidence, but
|
|
569
|
+
the consequence is larger than the motive — a failure observed *after* the
|
|
570
|
+
implementation exists replaces the pass and satisfies `neverRedIssues`
|
|
571
|
+
identically to one observed before it. `self/red-record.spec.ts` has asserted
|
|
572
|
+
that at the record level since the mechanism shipped. So the gate has always
|
|
573
|
+
taken the evidence without caring when it was obtained, and three pieces of
|
|
574
|
+
prose said otherwise: `ATX-30`'s statement stopped at "its first run",
|
|
575
|
+
`ATX-31`'s rationale named running the gate first as *the* remedy, and the
|
|
576
|
+
workflow document said "Run this before you implement anything", which is
|
|
577
|
+
false as written.
|
|
578
|
+
|
|
579
|
+
**Removing it closes nothing, which is the argument for removing it.** A
|
|
580
|
+
stage-1 red can come from a typo in the spec, a missing import, or a fixture
|
|
581
|
+
that is not there yet — none of which is the requirement being unimplemented.
|
|
582
|
+
A failure observed before the implementation was therefore never *better*
|
|
583
|
+
evidence than one observed after it, only earlier. What the gate can actually
|
|
584
|
+
demand is that the scenario was seen to fail with its subject absent, and both
|
|
585
|
+
routes to that produce the same fact by the same command: run the gate before
|
|
586
|
+
writing the implementation, or remove the implementation, run the gate, and
|
|
587
|
+
restore it. The second route is now documented, with the `git stash` shape
|
|
588
|
+
spelled out in `troubleshooting.md`.
|
|
589
|
+
|
|
590
|
+
**Why this is worth the churn rather than a footnote.** The rule as written
|
|
591
|
+
declared malformed the one workflow that is becoming ordinary — intent,
|
|
592
|
+
scenario and implementation authored in one pass — and told those authors to
|
|
593
|
+
redo finished work in a different order to satisfy a gate that would have
|
|
594
|
+
accepted their evidence anyway. Separating the discipline (*an assertion has
|
|
595
|
+
to be able to fail*) from the habit (*write it first*) is what keeps the
|
|
596
|
+
strong half enforceable and stops the weak half from being enforced by
|
|
597
|
+
documentation the engine does not back.
|
|
598
|
+
|
|
599
|
+
`ATX-30` gains the clause the suite already checked — a recorded pass is
|
|
600
|
+
replaced by any later failure — and stops calling the record a snapshot of the
|
|
601
|
+
first run. `ATX-31` keeps its statement, which was always order-free, and
|
|
602
|
+
loses the ordering from its rationale. `ATX-48`, `ATX-53` and `ATX-56` carry a
|
|
603
|
+
one-phrase rationale patch each, because all three named it "the
|
|
604
|
+
red-before-implement gate" and a registry that contradicts itself is the drift
|
|
605
|
+
this tool exists to report. **`first-run.json` keeps its name**: archived
|
|
606
|
+
records already carry it, `inspectRedEvidence` reads them back, and spending
|
|
607
|
+
that to correct a word is the wrong trade — the sentence is where the meaning
|
|
608
|
+
lives.
|
|
609
|
+
|
|
610
|
+
One side effect worth reading: the reworded `ATX-30` statement moved from
|
|
611
|
+
`correct` to `missed` in the compound-requirement measurement, on both
|
|
612
|
+
variants, and the snapshot in `tests/intent-rule-candidates.spec.ts` carries
|
|
613
|
+
it. The requirement did not get less compound — the second clause became a
|
|
614
|
+
participle instead of a second `MUST` — so this is new evidence about *why*
|
|
615
|
+
that rule sees almost nothing, filed with the rejection it belongs to.
|
|
616
|
+
|
|
617
|
+
Two diagnostic messages are reworded and no `code` moves, so `schemaVersion`
|
|
618
|
+
does not move either; `Issue.message` is prose, and this is exactly the kind of
|
|
619
|
+
rewording that contract exists to allow. One assertion in `tests/skill.spec.ts`
|
|
620
|
+
moved from the phrase `must be red` to the code `never-red` — a test pinned to
|
|
621
|
+
a sentence the document is free to reword, which is the same mistake one level
|
|
622
|
+
down.
|
|
623
|
+
|
|
20
624
|
- **The same mutation pass over the verdict and its evidence: 24 mutations of
|
|
21
625
|
`gate.ts` and `red-record.ts`, 21 dead, and two of the three survivors closed.**
|
|
22
626
|
Every step of `evaluateGate` dies when removed, including scoping
|
|
@@ -612,7 +1216,7 @@ not API.
|
|
|
612
1216
|
one scenario asserting the *discrimination* rather than two asserting each
|
|
613
1217
|
side, which fails now and still refuses an implementation that warns
|
|
614
1218
|
unconditionally later. This is the mechanism catching the exact defect class
|
|
615
|
-
the tautology entry
|
|
1219
|
+
the tautology entry at the foot of this file is about, on the first change
|
|
616
1220
|
it was ever pointed at, in the author's own work.
|
|
617
1221
|
|
|
618
1222
|
*A second defect surfaced after `--apply` and is worth recording because
|
|
@@ -4206,7 +4810,8 @@ field moved.
|
|
|
4206
4810
|
the record would not need to exist at all. So it is the one thing Attest writes
|
|
4207
4811
|
that cannot be checked by regenerating it, and what stands in for that check is
|
|
4208
4812
|
a reviewer. (The workflow document has no gate either, but that is a decision —
|
|
4209
|
-
`init --check` was rejected,
|
|
4813
|
+
`init --check` was rejected, and has since been reopened at the foot of this
|
|
4814
|
+
file — not an impossibility.)
|
|
4210
4815
|
Landed in `troubleshooting.md#never-red` and under `attest archive` in the CLI
|
|
4211
4816
|
reference (both languages), and in the workflow document `attest init` writes —
|
|
4212
4817
|
where it also became a fourth entry in "things you must not do", beside editing
|
|
@@ -4418,9 +5023,10 @@ field moved.
|
|
|
4418
5023
|
the first fix written: it makes both maps resolve, at 195 kB and 148 files, to
|
|
4419
5024
|
serve stepping into the engine from an adopter's `node_modules` — which
|
|
4420
5025
|
nothing has asked for, and which anyone auditing the reader would do from the
|
|
4421
|
-
repository instead. This file has rejected `
|
|
4422
|
-
|
|
4423
|
-
|
|
5026
|
+
repository instead. This file has rejected `divergent-param` and countable
|
|
5027
|
+
obligations for costing more than a demonstrated need — and rejected
|
|
5028
|
+
`init --check` on the same standard, an entry since reopened on measured
|
|
5029
|
+
drift — and that standard does not bend for a packaging decision. Turning the two compiler
|
|
4424
5030
|
options off is the cleanest published output, since it leaves no dangling
|
|
4425
5031
|
`sourceMappingURL` comment, but it also removes the maps from `dist/` here,
|
|
4426
5032
|
where `verify self` runs through the built CLI and they are the only thing
|
|
@@ -5016,9 +5622,10 @@ every platform, which is what makes them work on Windows at all.
|
|
|
5016
5622
|
than assumed: there isn't one, and that is the design.** A committed
|
|
5017
5623
|
instruction file is a function of the installed version, so it does go stale on
|
|
5018
5624
|
upgrade — but staleness only *costs* anything if the tool also ships a gate to
|
|
5019
|
-
turn red about it, and `init --check`
|
|
5020
|
-
|
|
5021
|
-
|
|
5625
|
+
turn red about it, and `init --check` was rejected on its own terms, together
|
|
5626
|
+
with the exposure that leaves and the backstop that keeps it small — an entry
|
|
5627
|
+
since reopened at the foot of this file, on the first measurement of that
|
|
5628
|
+
exposure and the failure of that backstop. What belongs here is what the absence of a gate
|
|
5022
5629
|
bought: with none to serve there is no reason to keep the prose out of the file
|
|
5023
5630
|
either, so it lives in the skill, where an agent reads it with no extra call,
|
|
5024
5631
|
and a skill body is loaded on demand — a long document costs context only when
|
|
@@ -5894,69 +6501,215 @@ losing the argument would cost more than losing the task.
|
|
|
5894
6501
|
**Where an entry went is recorded under the version that shipped it.** This
|
|
5895
6502
|
section keeps no ledger of its own departures — one was maintained here by hand
|
|
5896
6503
|
for several releases, and a count kept in prose with nothing to check it is a
|
|
5897
|
-
claim that goes wrong quietly. What
|
|
5898
|
-
|
|
6504
|
+
claim that goes wrong quietly. What these sections have learned about
|
|
6505
|
+
re-measuring an entry before building it is at the foot of the file, under
|
|
6506
|
+
`How an entry is re-measured` — it applies to all three, and was filed here only
|
|
6507
|
+
because the first of it was written here.
|
|
5899
6508
|
|
|
5900
|
-
**An
|
|
5901
|
-
|
|
5902
|
-
|
|
5903
|
-
|
|
5904
|
-
|
|
5905
|
-
|
|
5906
|
-
|
|
5907
|
-
|
|
5908
|
-
|
|
5909
|
-
|
|
5910
|
-
|
|
5911
|
-
|
|
5912
|
-
|
|
5913
|
-
|
|
5914
|
-
|
|
5915
|
-
|
|
5916
|
-
|
|
5917
|
-
|
|
5918
|
-
|
|
5919
|
-
|
|
5920
|
-
|
|
5921
|
-
|
|
5922
|
-
|
|
5923
|
-
|
|
5924
|
-
|
|
5925
|
-
|
|
5926
|
-
|
|
5927
|
-
|
|
5928
|
-
|
|
5929
|
-
|
|
5930
|
-
|
|
5931
|
-
|
|
5932
|
-
|
|
5933
|
-
|
|
5934
|
-
|
|
5935
|
-
|
|
5936
|
-
|
|
5937
|
-
|
|
5938
|
-
|
|
5939
|
-
|
|
5940
|
-
|
|
5941
|
-
|
|
5942
|
-
|
|
5943
|
-
|
|
5944
|
-
|
|
5945
|
-
|
|
5946
|
-
|
|
5947
|
-
|
|
5948
|
-
|
|
5949
|
-
|
|
5950
|
-
|
|
5951
|
-
|
|
5952
|
-
|
|
5953
|
-
|
|
5954
|
-
|
|
5955
|
-
the
|
|
5956
|
-
|
|
5957
|
-
|
|
5958
|
-
|
|
5959
|
-
|
|
6509
|
+
- **An outward-facing form of the judgement tables, so an adopter can run this
|
|
6510
|
+
counter-pressure on their own registry.**
|
|
6511
|
+
`tests/intent-rule-candidates.spec.ts` maintains four hand-judged tables over
|
|
6512
|
+
this repo's registry and enforces their completeness. Design §11 records the
|
|
6513
|
+
properties they defend and that nothing enforces any of them; what the tables
|
|
6514
|
+
add is a counter-pressure holding for **one** registry whose authors agreed to
|
|
6515
|
+
maintain it. An adopter got none of it, and the reason was sharper
|
|
6516
|
+
than "nobody wrote it for them": the package exported `requirement`,
|
|
6517
|
+
`scenario`, `defineRequirements`, `delta` and types, and nothing else.
|
|
6518
|
+
`parseSpecs`, `findFiles` and `readRegistrySource` were internal, so an
|
|
6519
|
+
adopter **could not enumerate their own scenario names** — the table's central
|
|
6520
|
+
rot check, that a claimed scenario is one a spec really declares, was not
|
|
6521
|
+
merely unwritten downstream but unreachable. **That half shipped under
|
|
6522
|
+
`[0.9.2]` as `@am_shork/attest/inspect`** and is recorded at the foot of
|
|
6523
|
+
this entry; what stays here is the schema built on top of it, and the three
|
|
6524
|
+
blockers below are untouched by the export.
|
|
6525
|
+
*The shape that makes it shippable at all is a split, not a feature.* The
|
|
6526
|
+
engine can compute which ids exist, which statements match the universal
|
|
6527
|
+
quantifier regex, which params are list-valued, and whether a named scenario
|
|
6528
|
+
is declared — all four already, all statically. It cannot compute how many
|
|
6529
|
+
obligations a requirement carries, what a quantifier ranges over, or whether a
|
|
6530
|
+
scenario iterates a param. So the shippable object is **the enforcement, never
|
|
6531
|
+
the judgement**: an opt-in `*.judge.ts` read by the same static reader and
|
|
6532
|
+
under the same literal-only rule the registry is (it is metadata about
|
|
6533
|
+
requirements under review, so `check` must execute nothing to read it), whose
|
|
6534
|
+
contents are entirely the adopter's. Opting in is creating the file; once it
|
|
6535
|
+
exists, `requirement-unjudged`, `orphan-judgement`, `unbound-site` and
|
|
6536
|
+
`quantifier-unjudged` make it complete and keep it from rotting. The
|
|
6537
|
+
asymmetry is the only way this can exist without Attest asserting a ground
|
|
6538
|
+
truth it refuses to compute (§0), and it is the same asymmetry `never-red`
|
|
6539
|
+
already runs on: the archive gate would owe judgement only for ids a change
|
|
6540
|
+
ADDs, so a brownfield adoption is not blocked wholesale.
|
|
6541
|
+
*One part of the existing table cannot ship in any form.* `DOMAINS.pin` comes
|
|
6542
|
+
in two kinds — a scenario name, checkable against the AST, and a **source-text
|
|
6543
|
+
fragment** grepped out of the spec file. The second is asserting on the
|
|
6544
|
+
wording of somebody's test, which is the `Issue.message` category error under
|
|
6545
|
+
another name. Only the scenario-name kind is exportable, and what that costs
|
|
6546
|
+
is real: the grep is what caught a deleted pin that `toContain` had passed.
|
|
6547
|
+
*Why no detector ships with it, and this is the part that is already
|
|
6548
|
+
measured.* Three rule families were built and scored against the whole corpus
|
|
6549
|
+
(86 entries, counting a fixture id once per registry file) before any was
|
|
6550
|
+
written into the engine, which is the order `divergent-param` was originally
|
|
6551
|
+
designed in the wrong way round. Every figure below is a quotation of the
|
|
6552
|
+
suite's inline snapshots; the snapshot is the fresh copy, and this paragraph
|
|
6553
|
+
is only ever as fresh as its last edit.
|
|
6554
|
+
**(1) `compound-requirement`** — 34 requirements carry more than one
|
|
6555
|
+
obligation, it flags 16, is **wrong about none**, and misses 18. Never wrong
|
|
6556
|
+
and nearly blind, and the 18 are the form the coverage incentive actually
|
|
6557
|
+
rewards: extra clauses under a single keyword. **(2) The widened variant** —
|
|
6558
|
+
29 correct, but 14 wrong and still 5 missed, and it is silenced by deleting an
|
|
6559
|
+
"and". A rule whose cheapest fix degrades the artifact it protects does not
|
|
6560
|
+
ship, and its precision falls as the registry grows. **(3) `divergent-param`**
|
|
6561
|
+
— 5 divergences on this repo, **all 5 judged wrong**, zero true positives ever
|
|
6562
|
+
recorded. The `code` group now spans 19 ids, and the failure is structural
|
|
6563
|
+
rather than a matter of naming: the key is built by discarding the evidence of
|
|
6564
|
+
relatedness, so the one genuinely paired divergence in the group is the one it
|
|
6565
|
+
cannot show, and the key has churned three times under `flag` on additions
|
|
6566
|
+
that changed no verdict. A fourth candidate — the echo detector, scored at
|
|
6567
|
+
half precision — belongs to the tautology-detector entry now under
|
|
6568
|
+
*Considered and rejected* and is not re-quoted here, because a figure stated in two places goes stale in one. Taken together
|
|
6569
|
+
these are the measured range of what a token or single-body detector over
|
|
6570
|
+
prose can do here, and they are why the proposal is enforcement-only.
|
|
6571
|
+
*What blocks the decision — three things.* **(1) One adapter.** This
|
|
6572
|
+
repository's table is the only one that exists, and its author wrote both the
|
|
6573
|
+
schema and every answer in it. By this project's own rule a second table is
|
|
6574
|
+
what makes the seam real, and freezing a `Judgement` type now is deciding for
|
|
6575
|
+
everyone from 34 requirements written by one person. **(2) The breaking form
|
|
6576
|
+
of this was already rejected** — see *Countable obligations* below — and one
|
|
6577
|
+
of its three reasons survives the separate-file shape intact: **nothing can
|
|
6578
|
+
force the count to be true.** The other two do not survive, which is what is
|
|
6579
|
+
new here: a separate artifact touches no `--json` field, no delta apply and no
|
|
6580
|
+
`render` output, and completeness *across ids* is computable in a way
|
|
6581
|
+
completeness *within* an obligation array never was. So this is a re-proposal
|
|
6582
|
+
with one of three objections answered and the sharpest one standing.
|
|
6583
|
+
**(3) `quantifier-unjudged` reuses a rejected detector as its trigger.**
|
|
6584
|
+
Firing on the quantifier token was rejected below at 43% reach and 70% wrong;
|
|
6585
|
+
the claim here is that the same regex is acceptable when it asks a question an
|
|
6586
|
+
author discharges in one line (`over: 'values'`) rather than issuing a
|
|
6587
|
+
verdict. That claim is untested on anyone who did not also write the regex.
|
|
6588
|
+
*The half that is not blocked has shipped, under `[0.9.2]`.*
|
|
6589
|
+
`@am_shork/attest/inspect` is one function answering what a project declares —
|
|
6590
|
+
AST-only, no runner peer, landing on the existing static/executing boundary.
|
|
6591
|
+
It ships no schema and decides nothing, which is why it could go ahead of the
|
|
6592
|
+
three blockers rather than waiting on them. What it changes about this entry
|
|
6593
|
+
is the *kind* of thing that is now missing: the mechanism is no longer
|
|
6594
|
+
unreachable, only unwritten, so a second table is something an adopter can
|
|
6595
|
+
build rather than something only this repository could.
|
|
6596
|
+
*What would decide it:* one judgement table maintained by someone who did not
|
|
6597
|
+
design it. If it comes back the same shape, the seam is real and the schema
|
|
6598
|
+
follows; if it comes back different, `inspect` was the whole answer and the
|
|
6599
|
+
schema never needs to exist. *(One came back on 2026-08-31, and the disjunction
|
|
6600
|
+
above is too clean to survive it: partly the same shape, partly not, and the
|
|
6601
|
+
same half is the contaminated half. The close of this entry is where that is
|
|
6602
|
+
read.)*
|
|
6603
|
+
**Nothing currently makes that table arrive, which is a separate problem from
|
|
6604
|
+
the three blockers and the one that gates all of them.** Raised in a product
|
|
6605
|
+
review of `[0.9.1]`–`[0.9.3]` on 2026-08-19. The deciding evidence is an
|
|
6606
|
+
artifact only an adopter can produce, and the path to producing it is: a row in
|
|
6607
|
+
the README's document table, then a 214-line page, then a hand pass over four
|
|
6608
|
+
questions, then writing the two mechanical properties from a prose
|
|
6609
|
+
description, then choosing to report back. Five voluntary steps with no
|
|
6610
|
+
scaffold at any of them, against a decision this entry has been waiting on
|
|
6611
|
+
since `[0.9.2]`. An entry blocked on evidence that no shipped surface asks for
|
|
6612
|
+
is blocked indefinitely, and the wait is not itself evidence of anything.
|
|
6613
|
+
*The proposal is scaffolding the mechanism while still withholding the answer,*
|
|
6614
|
+
which is the same split `[0.9.2]` already made and not a retreat from it: an
|
|
6615
|
+
`examples/` directory holding the completeness and rot-binding assertions with
|
|
6616
|
+
**no columns** — the enumeration that forces a row, and the check that a named
|
|
6617
|
+
scenario is one `inspectProject` really declares — and the judgement columns
|
|
6618
|
+
left for the adopter to add. What the page withholds is this repository's four
|
|
6619
|
+
columns, and that stays withheld; what it currently also withholds is the two
|
|
6620
|
+
properties it names as *mechanical*, and those are the half where a copy is not
|
|
6621
|
+
an echo, because their content is fixed and the fourth finding does not apply
|
|
6622
|
+
to them.
|
|
6623
|
+
*What blocks it.* Whether a skeleton with no columns is still a shape that
|
|
6624
|
+
comes back as a copy — the page's argument is that a template makes the answer
|
|
6625
|
+
return its own question, and it is untested whether an adopter reads an empty
|
|
6626
|
+
frame as an invitation or as a form to fill in. And it is a second artifact
|
|
6627
|
+
claiming to be runnable consumer usage of the public API, which `pre-commit-check`
|
|
6628
|
+
§6 already records as the category no gate reaches: the page's three samples
|
|
6629
|
+
are unasserted today, and a fourth in a directory that looks executable would
|
|
6630
|
+
be worse than a fenced block, not better, unless something runs it.
|
|
6631
|
+
**Built under `[0.11.0]`, and the second of those two blockers was the
|
|
6632
|
+
buildable one.** `examples/judgement-table/` is in the default suite, the
|
|
6633
|
+
specifier is aliased rather than relativised so the file stays copyable
|
|
6634
|
+
verbatim, and both assertions were shown to fail before it was called done.
|
|
6635
|
+
So the category `pre-commit-check` §6 names now has exactly one member that a
|
|
6636
|
+
gate reaches, and the page's three prose samples are still not it.
|
|
6637
|
+
*Two things the build found that the proposal did not contain.* A third
|
|
6638
|
+
assertion — `issues` empty, before either of the other two — which is not a
|
|
6639
|
+
refinement but a hole: an unread registry file and a smaller project are the
|
|
6640
|
+
same value, and the two completeness directions pass over both. It was found
|
|
6641
|
+
by an invalid mutation during the bite test rather than by reasoning, which is
|
|
6642
|
+
`How an entry is re-measured`'s question about what a repair makes reachable,
|
|
6643
|
+
arriving from the other end. And a **type-level** forcing move the runtime
|
|
6644
|
+
assertion cannot make: a required field added to `Judgement` fails every
|
|
6645
|
+
existing row at compile time, so the schema-shaped half of "a row cannot
|
|
6646
|
+
arrive unjudged" costs one line and no engine rule. That is worth noting
|
|
6647
|
+
against blocker **(1)** above, which is about freezing a `Judgement` type:
|
|
6648
|
+
what shipped freezes nothing, because the type is in the adopter's copy.
|
|
6649
|
+
**What is untouched is the first blocker, and it is now the only thing between
|
|
6650
|
+
this entry and its evidence.** Whether an empty frame reads as an invitation
|
|
6651
|
+
or as a form to fill in is still untested, and cannot be tested here — the
|
|
6652
|
+
people who would answer it are the ones who have not arrived. What the build
|
|
6653
|
+
changes is only that the five voluntary steps are now four, and the one
|
|
6654
|
+
removed was the one with a fixed answer. If the next report is still nobody,
|
|
6655
|
+
the scaffold was not what was missing, and that is worth knowing too.
|
|
6656
|
+
**Decided 2026-08-31: build the counting rule, and not the schema.** The
|
|
6657
|
+
blocker was whether an empty frame reads as an invitation or as a form to fill
|
|
6658
|
+
in, it could not be tested here, and a 0.9.1 → 0.11.0 upgrade report from
|
|
6659
|
+
`mine-capablanca` tested it. The answer is **invitation**, and the report names
|
|
6660
|
+
the sentence that did it: not the empty `interface Judgement`, which reads as a
|
|
6661
|
+
stub and sent the reporter looking for what belonged in it, but the comment
|
|
6662
|
+
above it — *"It ships with no columns, and that is the finished state of this
|
|
6663
|
+
file rather than a gap in it."* Declaring the absence finished is what turns a
|
|
6664
|
+
blank into a question, and it is the cheapest sentence in the example.
|
|
6665
|
+
*The forcing move was one the build found rather than one it proposed.* The
|
|
6666
|
+
instruction to add the first column as a **required** field made all 80 rows
|
|
6667
|
+
stop compiling at once, and there was then no path back to green that did not
|
|
6668
|
+
go through having an opinion about all 80 — *"a form gets partially filled in;
|
|
6669
|
+
a type does not"*. That is the type-level forcing move described above,
|
|
6670
|
+
working on somebody who did not write it.
|
|
6671
|
+
*And the frame leaks in one place, which is a defect in the example rather
|
|
6672
|
+
than in the idea.* `pin` is documented as not a judgement column and is the
|
|
6673
|
+
only concrete field in the interface, so the reporter's first act was to
|
|
6674
|
+
populate it for all 80 rows mechanically — *"there is a field, populate it"*.
|
|
6675
|
+
Defensible, since it makes rot-binding bite for 80 rows instead of none, and
|
|
6676
|
+
exactly the form-filling behaviour the frame exists to prevent. Generating
|
|
6677
|
+
`pin` from `inspectProject` rather than leaving a field to be typed is the fix,
|
|
6678
|
+
and it belongs in the example.
|
|
6679
|
+
*What does **not** follow, and the report is why it does not.* Two of the three
|
|
6680
|
+
columns that came back are the same shape as two of this repository's —
|
|
6681
|
+
`clauses` is `OBLIGATIONS`, `quantifier` is a flattened `QUANTIFIED` — and it
|
|
6682
|
+
is tempting to read that as the seam being real. It cannot be read that way:
|
|
6683
|
+
the reporter names the `attest init` workflow document as prior influence, and
|
|
6684
|
+
that document discusses quantifiers and param pinning at length. So the
|
|
6685
|
+
convergence says **something** transmits those two ideas, and the document is
|
|
6686
|
+
at least as likely a channel as the frame. What is uncontaminated is the third
|
|
6687
|
+
column, `venue`, which has no counterpart here: nine of its rows record a
|
|
6688
|
+
promise held by a Playwright suite Attest's scope does not reach. A column this
|
|
6689
|
+
repository could not have written came back, which is a seam behaving like one.
|
|
6690
|
+
*What to build is smaller than a schema, and the three things the reporter
|
|
6691
|
+
wanted and did not get are its specification.* **(1) A counting rule for
|
|
6692
|
+
"obligation" precise enough that two people count the same.** The report
|
|
6693
|
+
distrusts its own `clauses` column above all the others and says why: two
|
|
6694
|
+
tables that count differently are not comparable, which defeats the point of
|
|
6695
|
+
there being a second one. A counting rule is content that does **not** depend
|
|
6696
|
+
on the adopter — the same category as the two mechanical assertions — and it is
|
|
6697
|
+
currently withheld along with the columns, which is the wrong side of the
|
|
6698
|
+
split. **(2) A way to bind a clause to a scenario rather than a row.**
|
|
6699
|
+
Rot-binding binds a row to *a* scenario; nothing binds the sixth obligation of
|
|
6700
|
+
a six-clause requirement to anything, so a six-clauses-three-scenarios row is a
|
|
6701
|
+
number a reader has to notice instead of an assertion that fails. **(3)
|
|
6702
|
+
Somewhere for a row to say the promise is held where Attest cannot see it.**
|
|
6703
|
+
Nine `ui-node` rows and a 902-line Playwright suite, citable only in prose.
|
|
6704
|
+
*So what is planned is (1) and the `pin` fix, and (2) and (3) explicitly are
|
|
6705
|
+
not.* (1) is a paragraph in the example, costs no engine surface, and is the
|
|
6706
|
+
one thing that makes a second table comparable to the first — without it two
|
|
6707
|
+
tables are two vocabularies and the seam cannot be read either way. (2) and (3)
|
|
6708
|
+
are both new schema, and blocker **(1)** of the proposal above — one adapter —
|
|
6709
|
+
is not answered by a report that is the second adapter's first hour. Two hours
|
|
6710
|
+
of one agent is not two adapters. **The `*.judge.ts` schema stays undecided**,
|
|
6711
|
+
and when it moves it will move on a second table that somebody has maintained
|
|
6712
|
+
rather than built, which is a different measurement from the one that arrived.
|
|
5960
6713
|
|
|
5961
6714
|
## Under consideration
|
|
5962
6715
|
|
|
@@ -5970,259 +6723,6 @@ deletion. What each entry has to carry is the evidence currently blocking the
|
|
|
5970
6723
|
decision, because that is what a re-proposal a year from now would otherwise
|
|
5971
6724
|
have to gather again.
|
|
5972
6725
|
|
|
5973
|
-
- **An assertion that recomputes its expectation from the param the code under
|
|
5974
|
-
test just read is a tautology, and nothing says so.** Reported by
|
|
5975
|
-
`mine-capablanca`, which probed it by mutating the registry and running the
|
|
5976
|
-
full gate on each mutation:
|
|
5977
|
-
|
|
5978
|
-
| mutation | who reads it | `check` | `verify` |
|
|
5979
|
-
|---|---|---|---|
|
|
5980
|
-
| `GEN-1.maxCandidatesPerPiece` 64 → 4 | scenario only | ok | **red** |
|
|
5981
|
-
| `BOARD-1.boardSize` 10 → 12 | app + fixtures | ok | **red** |
|
|
5982
|
-
| `BOARD-2.maxMines` 12 → **40** | app + assertion | ok | **green** |
|
|
5983
|
-
| `BOARD-2.minMines` 9 → **1** | app + assertion | ok | **green** |
|
|
5984
|
-
|
|
5985
|
-
The mine-density band — the one number deciding whether that game is playable —
|
|
5986
|
-
moved by 3.3× with the whole suite green, `check` silent, and `possible-drift`
|
|
5987
|
-
correctly not firing, because a covering scenario *does* read the param.
|
|
5988
|
-
**Read the third and fourth rows against the second, because that is the
|
|
5989
|
-
finding.** The reporter's diagnosis was "app and spec both read it", but row 2
|
|
5990
|
-
is app-and-spec and goes red. What distinguishes the green rows is narrower and
|
|
5991
|
-
more useful: the assertion **derives its expectation from the same param the
|
|
5992
|
-
code under test used**, so both sides move together and the comparison has no
|
|
5993
|
-
independent term. Row 2's expectation lives in fixtures, which do not move when
|
|
5994
|
-
the param does, and the mechanism works. So the failing condition is a property
|
|
5995
|
-
of the assertion, not of who reads the value — which matters, because it means
|
|
5996
|
-
the answer is a rule about how to write the assertion, and *not* the detector
|
|
5997
|
-
the reporter proposed.
|
|
5998
|
-
**What this falsifies is written in this file.** The rejection of a committed
|
|
5999
|
-
params snapshot, below, closes on *"the mechanism that guards a param is a
|
|
6000
|
-
scenario reading it, not a file recording it"* — a sufficiency claim, and rows
|
|
6001
|
-
3 and 4 are counterexamples to it. A scenario reading the param is necessary
|
|
6002
|
-
and not sufficient. That entry stays rejected on its own argument, which was
|
|
6003
|
-
never about detection power, but its fallback clause is now known incomplete
|
|
6004
|
-
and is annotated accordingly. The README's §"killer move" has the same shape:
|
|
6005
|
-
it says noticing a param *moved* is `verify`'s job "only when a scenario asserts
|
|
6006
|
-
on the value it read from `params`" — literally a necessary condition, and read
|
|
6007
|
-
by an adopter as a sufficient one, with the very next line (*"Read the param
|
|
6008
|
-
inside the assertion, not beside it"*) being the instruction that produces the
|
|
6009
|
-
tautology.
|
|
6010
|
-
**The framing that probably resolves it: this is `never-red` wearing different
|
|
6011
|
-
clothes.** A tautological assertion is a scenario that cannot fail, which is
|
|
6012
|
-
the exact defect `archive`'s red-before-implement gate exists to catch — and it
|
|
6013
|
-
did catch two of them in this very repo (`CHECK-2`, `FOG-3`, both confirmed
|
|
6014
|
-
correct by the reporter). The reason it did not catch these is structural:
|
|
6015
|
-
**changing the value of an existing param goes through no gate at all.**
|
|
6016
|
-
`archive` gates a change; a param edit is not a change. That is a cleaner
|
|
6017
|
-
statement of the gap than "params can be tautological".
|
|
6018
|
-
**That observation is now its own entry at the foot of this section**, split
|
|
6019
|
-
out and measured on 2026-08-18, because it asks what happens when a value
|
|
6020
|
-
moves rather than whether an assertion is a real comparison, and neither
|
|
6021
|
-
answer settles the other. What stays here is the detector.
|
|
6022
|
-
*What blocks the decision — three things, all of them real.* **(1)** Every
|
|
6023
|
-
mutation above is synthetic. Across 8 commits touching `*.reqs.ts` in that
|
|
6024
|
-
repo, **no param value was ever modified** — every `params:` line in every diff
|
|
6025
|
-
is an addition. So the hole is proven as a logical property and its frequency is
|
|
6026
|
-
entirely unmeasured, from one greenfield repo. **(2)** The reporter's proposed
|
|
6027
|
-
mechanism — a WARNING when one param id is read from both a spec and a non-spec
|
|
6028
|
-
file — is the wrong shape twice over: it fires on row 2, which is correct usage
|
|
6029
|
-
and goes red, and it would fire on the arrangement the README actively
|
|
6030
|
-
recommends, which is §9's "usually wrong about staleness" failure mode again.
|
|
6031
|
-
It also needs a static sweep of the whole project for `reqs['X'].params.y`,
|
|
6032
|
-
far past the two registry readers' current surface. **(3)** No mechanism is yet
|
|
6033
|
-
known that separates "the assertion pins this independently" from "the assertion
|
|
6034
|
-
echoes it" without executing the scenario twice under different values — which
|
|
6035
|
-
is mutation testing, a much larger thing to own. *One has since been built and
|
|
6036
|
-
scored, and it does not lift this blocker.* The syntactic candidate — flag a
|
|
6037
|
-
scenario whose expectation is derived from the param the code under test
|
|
6038
|
-
consumed, decided inside one body without executing anything — flags 9 params
|
|
6039
|
-
here, of which **4 are real echoes and 5 are pinned by something outside the
|
|
6040
|
-
body it cannot see**. Under half precision, and the failure is the one
|
|
6041
|
-
predicted above rather than a tuning problem: what pins a value is often a
|
|
6042
|
-
fixture or a second derivation in another file, so a single-body detector is
|
|
6043
|
-
looking in the wrong place by construction. The newest false positive is the
|
|
6044
|
-
clearest instance — `ATX-79.subpath` is compared against `package.json`, which
|
|
6045
|
-
is not the code under test and does not read the registry, so the independent
|
|
6046
|
-
term is in another file entirely. Precision has fallen on every addition
|
|
6047
|
-
measured so far, which is the direction that matters. The score is quoted from the
|
|
6048
|
-
suite's snapshot and re-read with the judgement-table entry above, which is
|
|
6049
|
-
where the same measurement is used for a different decision.
|
|
6050
|
-
**The guidance half has shipped; this entry is now only about the detector.**
|
|
6051
|
-
"Read the param inside the assertion" was insufficient and half-wrong as
|
|
6052
|
-
written, and `[0.5.0]` replaces it everywhere it appeared — README, the
|
|
6053
|
-
`init` skill, `possible-drift`, and design §11 — with what the evidence
|
|
6054
|
-
supports: **the expectation must not be a function of the param the code under
|
|
6055
|
-
test consumed**, pinned in a fixture, a literal, or a second independently
|
|
6056
|
-
derived value. Row 2 is the worked example, and the reporter's own
|
|
6057
|
-
`CHECK-2`/`FOG-3` fixes are the same move. It was held back once because
|
|
6058
|
-
rewording the central claim of the tool is not a line appended; that is what
|
|
6059
|
-
was then done, deliberately and in one pass.
|
|
6060
|
-
**What stays undecided is the mechanism**, and all three blockers above are
|
|
6061
|
-
untouched by the rewording: the frequency is still unmeasured from one
|
|
6062
|
-
greenfield repo, the reporter's proposed detector is still the wrong shape
|
|
6063
|
-
twice over, and nothing short of mutation testing separates an independent pin
|
|
6064
|
-
from an echo. What the guidance does not do is close the hole — a tautological
|
|
6065
|
-
assertion is still green, still silent, and still reachable — so this stays
|
|
6066
|
-
here rather than moving to `Considered and rejected`: documenting a trap is
|
|
6067
|
-
not the same as deciding not to detect it.
|
|
6068
|
-
**Re-measured after `[0.7.1]` widened `params`, on this registry rather than
|
|
6069
|
-
the reporter's — the first time this experiment has been run here.** Twelve
|
|
6070
|
-
mutations, each followed by the whole gate. Two results matter, and they point
|
|
6071
|
-
opposite ways.
|
|
6072
|
-
*The shape is now reachable here, and the widening is why.* `self/render.spec.ts`
|
|
6073
|
-
asserts `expect(md).toContain(JSON.stringify(kinds))` over the same
|
|
6074
|
-
`payloadKinds` it feeds to the renderer — the expectation is a function of the
|
|
6075
|
-
value under test, verbatim row 3, and it could not have been written before
|
|
6076
|
-
the widening because the param could not be an object. The mechanism is
|
|
6077
|
-
sharper than a frequency count and is the strongest thing this entry has
|
|
6078
|
-
gained: **the prescribed remedy costs in proportion to the value's
|
|
6079
|
-
complexity.** Same repo, adjacent files, same value shape — `ATX-7` pins two
|
|
6080
|
-
numbers with literals and goes red; `ATX-63` would have had to hand-write a
|
|
6081
|
-
rendered JSON block, so it echoed. Depth raises the defect rate and the cost
|
|
6082
|
-
of avoiding it along one axis.
|
|
6083
|
-
*And its cost here is nil, which is the honest other half.* Both composite
|
|
6084
|
-
params are fixtures for the renderer and the validator; the numbers stand for
|
|
6085
|
-
nothing, so the green is arguably correct. Blocker **(1)** is therefore only
|
|
6086
|
-
half retired — the shape is reproduced in a second, mature repo, and the count
|
|
6087
|
-
of *harmful* instances is still zero. Blocker **(2)** is reinforced rather than
|
|
6088
|
-
weakened: all four green mutations were on params `src/` never reads, so the
|
|
6089
|
-
reporter's "read from both a spec and a non-spec file" detector would have
|
|
6090
|
-
flagged none of them. Blocker **(3)** is untouched.
|
|
6091
|
-
*The frequency is no longer unmeasured, and it points away from building
|
|
6092
|
-
anything.* Blocker **(1)** rested on 8 commits of one greenfield repo. Parsed
|
|
6093
|
-
at every one of the **38** revisions that have touched this registry — all 38
|
|
6094
|
-
readable by the current static reader, so nothing was skipped — the history is
|
|
6095
|
-
**57 param keys added, 0 removed, and exactly 1 value changed**: `ATX-16.staticCommands`
|
|
6096
|
-
gaining `status`. That single edit is a roster *widening*, the safe direction,
|
|
6097
|
-
and it is on the one list that was already pinned, so it could not have been
|
|
6098
|
-
made without updating the pin. Two independent codebases, one greenfield and
|
|
6099
|
-
one at 64 requirements, and the event this whole entry is about has happened
|
|
6100
|
-
zero times in either. What that argues is not that the hole is unreal — it is
|
|
6101
|
-
reachable, demonstrated above — but that it is **rare and silent**, which is
|
|
6102
|
-
the profile `divergent-param` was rejected on rather than the profile that
|
|
6103
|
-
justifies an engine rule.
|
|
6104
|
-
*The same number says the split-out fix below has also never been triggered.*
|
|
6105
|
-
0 removals ever means no domain has ever been shortened here. Those pins close
|
|
6106
|
-
a live *exposure*, not a live *loss*, and the case for them is that they cost
|
|
6107
|
-
one line each against a failure with no symptom — not that anything was
|
|
6108
|
-
currently broken.
|
|
6109
|
-
*Blocker **(3)** assumed mutation testing was the only separator. A cheaper
|
|
6110
|
-
candidate existed, has now been scored, and does not survive it.* The idea was
|
|
6111
|
-
a **syntactic** discriminator over one test body — flag an assertion whose
|
|
6112
|
-
expected value derives from a param that also reached the arrange/act part —
|
|
6113
|
-
which is exactly the difference between the two cases the widening produced:
|
|
6114
|
-
`renderMarkdown({params: {payloadKinds: kinds}})` … `toContain(JSON.stringify(kinds))`
|
|
6115
|
-
against `expect(kinds.json.maxBytes).toBe(1_048_576)`. Scored the way
|
|
6116
|
-
`divergent-param` should have been and was not: implemented in
|
|
6117
|
-
`tests/intent-rule-candidates.spec.ts`, run over the specs that attest this
|
|
6118
|
-
repo, and then **every param it fires on mutated with the whole gate run —
|
|
6119
|
-
fifteen of them, one gate run each. Four went green; eleven went red.**
|
|
6120
|
-
*Then the detector was found to be wrong, which is the more useful half.* Its
|
|
6121
|
-
first implementation counted an identifier in any position as a read of a
|
|
6122
|
-
local with that name, so `i.code` in `issues.map((i) => i.code)` looked like a
|
|
6123
|
-
use of a param named `code` — and `code` is this repo's commonest param name.
|
|
6124
|
-
Corrected to count reference positions only, it flags **8, not 15**, and the
|
|
6125
|
-
seven it drops are exactly seven the mutations had already shown to be pinned
|
|
6126
|
-
by `src/`. The honest score is therefore **4 of 8, not 4 of 15** — precision
|
|
6127
|
-
50%, not 27%. The conclusion does not move, because it never rested on the
|
|
6128
|
-
precision figure; what moves is how much of the original number was the
|
|
6129
|
-
candidate failing and how much was the measurement being wrong, and that is
|
|
6130
|
-
worth writing down rather than quietly restating.
|
|
6131
|
-
The remaining false positives are not near-misses. They are independent terms
|
|
6132
|
-
the detector cannot see: **the runtime is the term** — `ATX-14`'s
|
|
6133
|
-
`inheritedKey` is `'toString'`, and a mutated spelling is not a prototype key
|
|
6134
|
-
at all, so the behaviour under test changes; **`src/` is the term** —
|
|
6135
|
-
`ATX-51`'s `code` is a second copy of a string the engine emits as a literal;
|
|
6136
|
-
and **arithmetic is the term** — `threshold + 3` builds a derived expectation
|
|
6137
|
-
that mutation breaks.
|
|
6138
|
-
And the four true positives fail the second test, which is the one that
|
|
6139
|
-
decides it: **none is harmful.** `ATX-10` and `ATX-3` assert that a value was
|
|
6140
|
-
substituted or applied, where the identity of the number is not the
|
|
6141
|
-
obligation; `ATX-51`'s is a path prefix and `ATX-63`'s a rendering fixture.
|
|
6142
|
-
Telling those from a mine-density band that must not move is a judgement about
|
|
6143
|
-
whether the value *means* anything — the ground truth §0 refuses to compute
|
|
6144
|
-
for someone else's registry. **50% precision on the shape, 0% on the harm.**
|
|
6145
|
-
Two limits on that number, stated because a score with an unstated method is
|
|
6146
|
-
what this file exists to prevent. The mutation has **one direction per kind**
|
|
6147
|
-
— numbers +1, lists drop-last, strings suffixed — and direction can decide the
|
|
6148
|
-
verdict: `ATX-40.languages` is red at 2→3 and was green at 2→1 before it was
|
|
6149
|
-
pinned, so the true-positive count is a floor. And **recall is unmeasured**:
|
|
6150
|
-
the eight params mutated earlier that this detector does not flag all went
|
|
6151
|
-
red, which is eight correct negatives and not a recall figure.
|
|
6152
|
-
The detector and its verdict table are kept in the measurement suite, with the
|
|
6153
|
-
reach recomputed on every run and the table asserted to cover exactly what
|
|
6154
|
-
fires — because the reach figure quoted for `compound-requirement` went stale
|
|
6155
|
-
in this very file for want of that.
|
|
6156
|
-
*A scoping limit on all of the above.* This repo cannot settle blocker **(2)**.
|
|
6157
|
-
`src/` reads no param at all — every `reqs[…].params` in it is a comment or a
|
|
6158
|
-
doc string — so params here are test vocabulary, while in the reporter's repo
|
|
6159
|
-
they are application constants the app itself consumes. The reporter's
|
|
6160
|
-
detector keys on exactly that difference, so it is inert here and this corpus
|
|
6161
|
-
is evidence neither for nor against it.
|
|
6162
|
-
*What the re-measurement did find is a different defect, and it has been split
|
|
6163
|
-
out below* — the entry is kept whole here because the two were measured in one
|
|
6164
|
-
pass and separating them is the finding.
|
|
6165
|
-
|
|
6166
|
-
- **Whether a requirement can ever be retired, and what it costs that none can.**
|
|
6167
|
-
The registry is at **79 requirements and 221 scenarios, 2.80 each**, and
|
|
6168
|
-
every one is a permanent obligation: `uncovered-requirement` is an ERROR, so a
|
|
6169
|
-
requirement without a scenario cannot exist, and nothing ever removes one.
|
|
6170
|
-
`delta.removed` exists and `applyDelta` applies it, but `--apply` refuses to
|
|
6171
|
-
write it back, deliberately and with the reason recorded — REMOVED "cannot say
|
|
6172
|
-
which comments belonged to the entry it deletes". So retiring one is a manual
|
|
6173
|
-
registry edit, which is fine as a mechanism and says nothing about whether it
|
|
6174
|
-
is ever the right move.
|
|
6175
|
-
*What the growth actually costs, measured on this session rather than
|
|
6176
|
-
estimated.* One small fix took: a requirement, its scenarios, a hand-judged
|
|
6177
|
-
row in the `OBLIGATIONS` table, two inline snapshots, a `[Unreleased]` entry,
|
|
6178
|
-
History rows in two languages, and a regenerated `SPEC.md`. That is
|
|
6179
|
-
proportionate for a spec framework that has to dogfood — the ceremony *is* the
|
|
6180
|
-
product being used on itself — and it is also a number that only goes up.
|
|
6181
|
-
*What blocks the decision, and it is not the mechanism.* It is that "retire"
|
|
6182
|
-
has at least three meanings this file has never had to separate: a requirement
|
|
6183
|
-
that turned out **wrong** (which should leave a record of being wrong, and
|
|
6184
|
-
`Considered and rejected` is where that already goes); one still **true** but
|
|
6185
|
-
no longer worth a permanently-run scenario; and one **subsumed** by a later
|
|
6186
|
-
requirement, where the scenario should move rather than go. Only the second is
|
|
6187
|
-
really about cost, and it is the one where deleting the intent to save the
|
|
6188
|
-
scenario inverts what the whole tool claims — that intent outlives its tests.
|
|
6189
|
-
Nothing here is urgent at 69. It is filed now because the moment to write the
|
|
6190
|
-
argument down is while the reasoning is available, and because a project whose
|
|
6191
|
-
requirement count only rises should have decided in advance what would make
|
|
6192
|
-
one leave.
|
|
6193
|
-
*The figures above were restated on 2026-08-08, and how they were wrong is
|
|
6194
|
-
itself the section's own warning happening.* As filed on 2026-08-03 this entry
|
|
6195
|
-
said "62 requirements and 160 scenarios, roughly 2.6 each" while closing on
|
|
6196
|
-
"the number is 61" — an inconsistency inside one entry, and the closing half
|
|
6197
|
-
was the right one. Re-measured at the entry's own commit with the current
|
|
6198
|
-
parser: **61 requirements, 156 scenarios**, so both opening figures were wrong
|
|
6199
|
-
on the day they were written, and only the ratio survived. Measured the same
|
|
6200
|
-
way today it is 69 and 192, and the method is the same one `verify self`
|
|
6201
|
-
prints — the control is that today's `self/` counted this way returns exactly
|
|
6202
|
-
what the runner does, which is what makes the 2026-08-03 number trustworthy. The
|
|
6203
|
-
cheap way — `grep -c 'scenario('` — answers 211 against today's true 192, and
|
|
6204
|
-
is what would have been reached for: comments and prose count, which is the
|
|
6205
|
-
`ATX-15` error again.
|
|
6206
|
-
**What moved in five days sharpens the argument rather than dating it.** +8
|
|
6207
|
-
requirements and +36 scenarios, concentrated in three sessions rather than
|
|
6208
|
-
accrued at a rate, so nothing here is a trend line. The part worth keeping is
|
|
6209
|
-
the ratio: 2.56 → 2.78. The cost of a requirement is not flat and it is not
|
|
6210
|
-
falling, which is the premise the whole entry rests on, and it is the first
|
|
6211
|
-
time this file has had two measurements of it to compare.
|
|
6212
|
-
*A third measurement, on 2026-08-18, and it points the other way from the
|
|
6213
|
-
second.* **79 requirements and 221 scenarios**, counted the way `verify self`
|
|
6214
|
-
prints them, which is the control the 2026-08-08 restatement established. The
|
|
6215
|
-
ratio runs 2.56 → 2.78 → **2.80**, so the step that carried the argument was
|
|
6216
|
-
the first one and the second is **+0.015**, which is flat. The premise this
|
|
6217
|
-
entry rests on now has one measurement for it and one against, and the honest
|
|
6218
|
-
statement is the narrower one: per-requirement cost has held at about 2.8
|
|
6219
|
-
scenarios across ten days and ten requirements. What is unchanged is the
|
|
6220
|
-
absolute total, which only rises — that half of the argument is untouched, and
|
|
6221
|
-
it was always the half about permanence rather than about acceleration. What is
|
|
6222
|
-
no longer supported is the acceleration. **Reopening on cost needs the ratio to
|
|
6223
|
-
move again**; reopening on the three meanings of "retire" needs none of these
|
|
6224
|
-
figures, and that is still the part nobody has separated.
|
|
6225
|
-
|
|
6226
6726
|
- **Running `verify` over a subset of the scenarios, and what a partial run may
|
|
6227
6727
|
be allowed to say.** `verify` is all-or-nothing and `check` executes nothing,
|
|
6228
6728
|
so there is no middle. Reported by an adoption where `verify` takes **7m33s**
|
|
@@ -6256,8 +6756,8 @@ have to gather again.
|
|
|
6256
6756
|
slow half is smaller, so the question is a selector's ergonomics rather than
|
|
6257
6757
|
one project's gates; and a scored answer to whether a scope-carrying report can
|
|
6258
6758
|
be stated in a way that a `--json` consumer written before it cannot misread.
|
|
6259
|
-
**Measured on 2026-08-12, before building anything, per
|
|
6260
|
-
|
|
6759
|
+
**Measured on 2026-08-12, before building anything, per `How an entry is
|
|
6760
|
+
re-measured`. The second of those two is now answered, and the answer is a
|
|
6261
6761
|
refusal; the first blocker was wrong; and the whole feature turns out to be a
|
|
6262
6762
|
different, much cheaper one.**
|
|
6263
6763
|
*The cost claim reproduces on a second corpus.* `verify self`: the static half
|
|
@@ -6355,197 +6855,94 @@ have to gather again.
|
|
|
6355
6855
|
and the second report of the same shape, which this measurement does not
|
|
6356
6856
|
supply and does not replace.
|
|
6357
6857
|
|
|
6358
|
-
|
|
6359
|
-
|
|
6360
|
-
`
|
|
6361
|
-
|
|
6362
|
-
|
|
6363
|
-
|
|
6364
|
-
|
|
6365
|
-
|
|
6366
|
-
|
|
6367
|
-
|
|
6368
|
-
|
|
6369
|
-
|
|
6370
|
-
|
|
6371
|
-
|
|
6372
|
-
|
|
6373
|
-
|
|
6374
|
-
|
|
6375
|
-
|
|
6376
|
-
|
|
6377
|
-
|
|
6378
|
-
|
|
6379
|
-
|
|
6380
|
-
|
|
6381
|
-
|
|
6382
|
-
|
|
6383
|
-
|
|
6384
|
-
|
|
6385
|
-
|
|
6386
|
-
|
|
6387
|
-
|
|
6388
|
-
|
|
6389
|
-
|
|
6390
|
-
|
|
6391
|
-
|
|
6392
|
-
|
|
6393
|
-
|
|
6394
|
-
|
|
6395
|
-
|
|
6396
|
-
|
|
6397
|
-
|
|
6398
|
-
|
|
6399
|
-
|
|
6400
|
-
|
|
6401
|
-
|
|
6402
|
-
|
|
6403
|
-
|
|
6404
|
-
|
|
6405
|
-
|
|
6406
|
-
|
|
6407
|
-
|
|
6408
|
-
|
|
6409
|
-
|
|
6410
|
-
|
|
6411
|
-
|
|
6412
|
-
|
|
6413
|
-
|
|
6414
|
-
|
|
6415
|
-
|
|
6416
|
-
|
|
6417
|
-
|
|
6418
|
-
|
|
6419
|
-
|
|
6420
|
-
|
|
6421
|
-
|
|
6422
|
-
|
|
6423
|
-
|
|
6424
|
-
|
|
6425
|
-
|
|
6426
|
-
|
|
6427
|
-
|
|
6428
|
-
|
|
6429
|
-
|
|
6430
|
-
|
|
6431
|
-
|
|
6432
|
-
**(3)
|
|
6433
|
-
|
|
6434
|
-
|
|
6435
|
-
|
|
6436
|
-
|
|
6437
|
-
|
|
6438
|
-
|
|
6439
|
-
|
|
6440
|
-
|
|
6441
|
-
three blockers rather than waiting on them. What it changes about this entry
|
|
6442
|
-
is the *kind* of thing that is now missing: the mechanism is no longer
|
|
6443
|
-
unreachable, only unwritten, so a second table is something an adopter can
|
|
6444
|
-
build rather than something only this repository could.
|
|
6445
|
-
*What would decide it:* one judgement table maintained by someone who did not
|
|
6446
|
-
design it. If it comes back the same shape, the seam is real and the schema
|
|
6447
|
-
follows; if it comes back different, `inspect` was the whole answer and the
|
|
6448
|
-
schema never needs to exist.
|
|
6449
|
-
**Nothing currently makes that table arrive, which is a separate problem from
|
|
6450
|
-
the three blockers and the one that gates all of them.** Raised in a product
|
|
6451
|
-
review of `[0.9.1]`–`[0.9.3]` on 2026-08-19. The deciding evidence is an
|
|
6452
|
-
artifact only an adopter can produce, and the path to producing it is: a row in
|
|
6453
|
-
the README's document table, then a 214-line page, then a hand pass over four
|
|
6454
|
-
questions, then writing the two mechanical properties from a prose
|
|
6455
|
-
description, then choosing to report back. Five voluntary steps with no
|
|
6456
|
-
scaffold at any of them, against a decision this entry has been waiting on
|
|
6457
|
-
since `[0.9.2]`. An entry blocked on evidence that no shipped surface asks for
|
|
6458
|
-
is blocked indefinitely, and the wait is not itself evidence of anything.
|
|
6459
|
-
*The proposal is scaffolding the mechanism while still withholding the answer,*
|
|
6460
|
-
which is the same split `[0.9.2]` already made and not a retreat from it: an
|
|
6461
|
-
`examples/` directory holding the completeness and rot-binding assertions with
|
|
6462
|
-
**no columns** — the enumeration that forces a row, and the check that a named
|
|
6463
|
-
scenario is one `inspectProject` really declares — and the judgement columns
|
|
6464
|
-
left for the adopter to add. What the page withholds is this repository's four
|
|
6465
|
-
columns, and that stays withheld; what it currently also withholds is the two
|
|
6466
|
-
properties it names as *mechanical*, and those are the half where a copy is not
|
|
6467
|
-
an echo, because their content is fixed and the fourth finding does not apply
|
|
6468
|
-
to them.
|
|
6469
|
-
*What blocks it.* Whether a skeleton with no columns is still a shape that
|
|
6470
|
-
comes back as a copy — the page's argument is that a template makes the answer
|
|
6471
|
-
return its own question, and it is untested whether an adopter reads an empty
|
|
6472
|
-
frame as an invitation or as a form to fill in. And it is a second artifact
|
|
6473
|
-
claiming to be runnable consumer usage of the public API, which `pre-commit-check`
|
|
6474
|
-
§6 already records as the category no gate reaches: the page's three samples
|
|
6475
|
-
are unasserted today, and a fourth in a directory that looks executable would
|
|
6476
|
-
be worse than a fenced block, not better, unless something runs it.
|
|
6477
|
-
|
|
6478
|
-
- **Prefix ownership is inferred rather than declared, and the cost arrives only
|
|
6479
|
-
when adoption succeeds.** Design §11 records that the id grammar has no level
|
|
6480
|
-
above the requirement: the prefix is a convention, nothing allocates it, and
|
|
6481
|
-
`duplicate-prefix` can only report a collision that already happened.
|
|
6482
|
-
spec-kit's answer is a roadmap table — a stable id, an intent, a scope
|
|
6483
|
-
boundary, dependencies — authored above the specs it decomposes into. That
|
|
6484
|
-
comparison is stated here rather than referred to, because the entry that
|
|
6485
|
-
introduced the spec-kit reading has since been built and now sits under
|
|
6486
|
-
`[0.9.4]` as the `open` field: an entry whose evidence rests on a
|
|
6487
|
-
neighbour is one a promotion silently empties.
|
|
6488
|
-
*Two things have changed since that reading, and they point opposite ways.*
|
|
6489
|
-
The proposal is **smaller** than it looked: the mechanism partly exists.
|
|
6490
|
-
`loadRegistry` returns `prefixOwners` — first claim in sorted file order —
|
|
6491
|
-
`merge.ts` uses it to decide which file an ADDED id belongs in, and
|
|
6492
|
-
`apply-no-prefix-owner` already refuses a prefix nobody owns. So what is
|
|
6493
|
-
proposed is declaration replacing inference, not a new layer. And it is
|
|
6494
|
-
**weaker**: `/inspect` shipped in `[0.9.2]`, so the cross-repository half —
|
|
6495
|
-
the one §11 says no command can see across — is now something an adopter
|
|
6496
|
-
computes from two registries in about ten lines. Attest does not have to own
|
|
6497
|
-
it to make it possible.
|
|
6498
|
-
*What blocks the decision:* nobody has the problem. §11 states the cost
|
|
6499
|
-
arrives "exactly when adoption succeeds: two teams sharing a registry have
|
|
6500
|
-
someone owning prefix allocation by hand", and no such adoption has been
|
|
6501
|
-
reported. Building the declared form now would be deciding prefix semantics
|
|
6502
|
-
for everyone from one repository whose five prefixes — `ATX` and the four
|
|
6503
|
-
fixture ones — are each declared by exactly one file, so the allocation
|
|
6504
|
-
question this would answer has never been asked here.
|
|
6505
|
-
|
|
6506
|
-
|
|
6507
|
-
- **A merged requirement carries no trace of the change that produced it, and
|
|
6508
|
-
the rename chain is the part actually lost.** `RequirementSchema` is
|
|
6509
|
-
`statement`, `rationale`, `params`, `outOfScope`; the splice writes those and
|
|
6510
|
-
nothing else, so after `--apply` no field and no sidecar records which change
|
|
6511
|
-
introduced an id, which modified it, or that one id used to be another. The
|
|
6512
|
-
delta that knew is in `archive/`, which nothing reads. Proposed from
|
|
6513
|
-
OpenViking's `.relations.json`: links between memories live in a
|
|
6514
|
-
directory-level sidecar, deliberately kept out of the content, and are
|
|
6515
|
-
rendered on demand at retrieval time (`docs/design/memory-link-design.md`).
|
|
6516
|
-
*The argument for the sidecar over a field is in their competitor analysis
|
|
6517
|
-
rather than their design.* The same document reads GBrain, which writes links
|
|
6518
|
-
into the page body, and records what that costs: rename a page and every
|
|
6519
|
-
inbound link still points at the old slug, with fuzzy matching at the read end
|
|
6520
|
-
as the only mitigation. A `supersedes:` or `introducedBy:` field on a
|
|
6521
|
-
requirement would be that mistake in this repository's terms — the registry is
|
|
6522
|
-
a literal and a single source (§2), and a field naming a folder that a later
|
|
6523
|
-
rename can move is a second source with nothing comparing it against the
|
|
6524
|
-
first. A derived index has no such copy to go stale: it is recomputed from the
|
|
6525
|
-
archived deltas every time it is read.
|
|
6526
|
-
*What blocks the decision, restated on 2026-08-19 because half of what was
|
|
6527
|
-
filed here is no longer true.* This entry was blocked on the empty archive and
|
|
6528
|
-
on the corpus being rename-free, and only the second still holds. `archive/`
|
|
6529
|
-
now has a real change in it — the `open` field, put through the workflow the
|
|
6530
|
-
same day — so the store this would index exists. Its delta carries `added` and
|
|
6531
|
-
nothing else, measured rather than assumed, so a backlink index is still
|
|
6532
|
-
testable only against fixtures for the operation it is actually about. Note
|
|
6533
|
-
what that says about the odds of the blocker clearing on its own: `--apply`
|
|
6534
|
-
refuses RENAMED outright, so no change this repository archives can ever
|
|
6535
|
-
exercise a rename until that refusal is lifted, and lifting it is a separate
|
|
6536
|
-
decision with its own reason (`splice.ts` — deleting an entry has no smaller
|
|
6537
|
-
span to fall back to). The blocker is therefore not "waiting for a rename to
|
|
6538
|
-
happen"; it is downstream of a refusal nobody has proposed removing. And one
|
|
6539
|
-
question the OpenViking reading does not answer, which is whether the chain is
|
|
6540
|
-
wanted at all. §7 applies
|
|
6541
|
-
`RENAMED` to a `Map<id, Requirement>` and the old id is then simply gone —
|
|
6542
|
-
deliberately, as far as anything written says. The case for recovering it is an
|
|
6543
|
-
external reference that outlives the rename, a ticket or a commit message
|
|
6544
|
-
naming a retired id, and nobody has reported one.
|
|
6858
|
+
*The second report arrived on 2026-08-31 and it is not the second report this
|
|
6859
|
+
entry asked for*, which is itself the finding. A 0.9.1 → 0.11.0 upgrade report
|
|
6860
|
+
from `mine-capablanca` — the same adoption this entry was filed on, six weeks
|
|
6861
|
+
later, now at 46 spec files, 170 scenarios and a `verify` median of **564
|
|
6862
|
+
seconds** against the 7m33s originally reported — asks for nothing named here.
|
|
6863
|
+
It does not ask for a selector.
|
|
6864
|
+
*The cost claim reproduces a third time, and the speed argument is finished.*
|
|
6865
|
+
`check` against `verify` on that project is **3,403 ms against 564,279 ms**,
|
|
6866
|
+
and the reporter is careful that this is a lower bound on engine share rather
|
|
6867
|
+
than an estimate of it, since `check` also pays a process start `verify` pays
|
|
6868
|
+
once. Either way the direction is settled on a corpus eighteen times slower
|
|
6869
|
+
than this one: over 99% of a `verify` there is that project's own search engine
|
|
6870
|
+
running. A selector saves no engine work anywhere, and now on two registries.
|
|
6871
|
+
*The parity argument — the one this entry called the strongest available —
|
|
6872
|
+
could not be tested there, and why it could not is a fact about it.* That
|
|
6873
|
+
project has **no `vitest.config.ts` and no `vite.config.ts`**, no spec
|
|
6874
|
+
importing through an alias, and no DOM anywhere in its specs, so `config:
|
|
6875
|
+
false` withholds nothing a direct run would have had. `--vitest-config` is
|
|
6876
|
+
passed by no script and no pipeline; it never became necessary. That does not
|
|
6877
|
+
refute the argument, it **bounds** it: the parity gap is real only for an
|
|
6878
|
+
adopter carrying a config, the population is therefore smaller than "adopters",
|
|
6879
|
+
and nobody has yet reported being in it.
|
|
6880
|
+
*What that adoption does have is a divergence, and it is not the one this entry
|
|
6881
|
+
predicted.* On the same tree, `npx vitest run` was 170/170 green while `attest
|
|
6882
|
+
verify` reported `tests-red` on **two runs out of three**. The mechanism the
|
|
6883
|
+
reporter reaches — labelled there as the strongest available inference and not
|
|
6884
|
+
as an observation — is one scenario measured at **4,971 ms against Vitest's
|
|
6885
|
+
undeclared 5,000 ms default**: 1.3 s alone, four times slower under suite
|
|
6886
|
+
contention, and the only one of eleven scenarios over four seconds that
|
|
6887
|
+
declares no explicit `timeoutMs`. That is `CLAUDE.md`'s own timing gotcha
|
|
6888
|
+
arriving in somebody else's repository, where no `CLAUDE.md` carries it.
|
|
6889
|
+
*So the residue of this entry is a different entry.* What that reporter needed
|
|
6890
|
+
was not a way to run less; it was a way to learn **which** of 170 scenarios was
|
|
6891
|
+
red, and `verify` cannot say. That is filed separately below rather than folded
|
|
6892
|
+
in here, because a selector and a diagnosis are different artifacts and this
|
|
6893
|
+
entry has already been narrowed twice by refusing to let them merge.
|
|
6894
|
+
*What is left blocking this one, restated:* an adopter who carries a Vitest
|
|
6895
|
+
config, and whose fast loop and verdict actually diverge because of it. That is
|
|
6896
|
+
the whole of it. The stage-2 cost argument above is unchanged and still comes
|
|
6897
|
+
from this repository rather than from a user.
|
|
6898
|
+
|
|
6899
|
+
- **`verify` reports that the suite is red and cannot say which scenario, and
|
|
6900
|
+
the documented way to find out fails in exactly the case that needs it.**
|
|
6901
|
+
Filed 2026-08-31 from a 0.9.1 → 0.11.0 upgrade report from `mine-capablanca`.
|
|
6902
|
+
`verify` and `archive` run the child suite with `quiet: true` because the CLI
|
|
6903
|
+
renders its own report, so the whole of a failure is one `tests-red` issue with
|
|
6904
|
+
no file, no scenario and no assertion — in the human output and in `--json`
|
|
6905
|
+
alike. The standing answer is to run the suite yourself under Vitest's
|
|
6906
|
+
reporter, which is what this repository does with `pnpm debug:self`.
|
|
6907
|
+
*The report is that this answer is circular where it is needed.* On that
|
|
6908
|
+
project `npx vitest run` was green, twice, on the same tree on which `verify`
|
|
6909
|
+
was red two runs in three. Running the suite yourself reproduces nothing,
|
|
6910
|
+
because the disagreement between the two runs **is** the failure. What the
|
|
6911
|
+
reporter did instead was time every scenario in the suite, compare alone
|
|
6912
|
+
against in-suite durations, and cross-reference which of the eleven scenarios
|
|
6913
|
+
over four seconds declared no timeout. That is a defensible inference and the
|
|
6914
|
+
report labels it as one; it is not a thing a tool should require.
|
|
6915
|
+
*What separates this from the entry above is that nothing here is a selector.*
|
|
6916
|
+
No scope, no partial verdict, no `--json` field naming what ran, no
|
|
6917
|
+
`SCHEMA_VERSION` question. The run is exactly the run it already is, and what
|
|
6918
|
+
changes is only how much of the child's output survives into the report.
|
|
6919
|
+
*What blocks it — and the first blocker is the reason `quiet: true` is there.*
|
|
6920
|
+
**(1)** The CLI renders its own report, and a child suite printing its own on
|
|
6921
|
+
top of that is two reports disagreeing about their own format. Passing the
|
|
6922
|
+
child's failures through means deciding what a `tests-red` issue carries — a
|
|
6923
|
+
file, a scenario name, a message — and each is a new field with a
|
|
6924
|
+
`SCHEMA_VERSION` question attached, which is the cheapest part to price first
|
|
6925
|
+
because it decides whether this is additive at all. **(2)** A failure message
|
|
6926
|
+
is somebody else's assertion text, and this project's rule is that
|
|
6927
|
+
`Issue.message` is prose and never API. Carrying a Vitest assertion into an
|
|
6928
|
+
`Issue` puts a third party's wording in the field consumers are told not to
|
|
6929
|
+
branch on. That is either harmless or a category error and nobody has decided
|
|
6930
|
+
which, and the decision is not obvious in either direction: the wording is not
|
|
6931
|
+
Attest's to keep stable, which is an argument both for carrying it and against.
|
|
6932
|
+
**(3)** The instance that produced this report may not be a diagnosis problem
|
|
6933
|
+
at all. A scenario 29 ms under a default it never declared is a known trap with
|
|
6934
|
+
a known fix, and the cheaper answers are for `verify` to name **the timeout it
|
|
6935
|
+
imposed** when a scenario fails on time, or for the workflow document to say
|
|
6936
|
+
that a scenario anywhere near the default needs an explicit `timeoutMs`. Both
|
|
6937
|
+
are much smaller than passing failures through, and whether either covers the
|
|
6938
|
+
general case is unmeasured, because n=1.
|
|
6939
|
+
*What would move it:* a red `verify` on an adopter's tree whose cause is not a
|
|
6940
|
+
timeout. Every instance anybody has is this one.
|
|
6545
6941
|
|
|
6546
6942
|
- **Changing the value of an existing param passes no gate, and for seven of
|
|
6547
|
-
them there is no record either.** Split out of the tautology entry
|
|
6548
|
-
|
|
6943
|
+
them there is no record either.** Split out of the tautology entry that
|
|
6944
|
+
headed this section on 2026-08-18, and which is now under *Considered and
|
|
6945
|
+
rejected*, where it was one sentence inside the
|
|
6549
6946
|
reframing paragraph. It is a different question: that entry asks whether an
|
|
6550
6947
|
assertion is a real comparison, this one asks what happens when the number
|
|
6551
6948
|
itself moves, and neither answer settles the other. `archive` gates a
|
|
@@ -6580,203 +6977,288 @@ have to gather again.
|
|
|
6580
6977
|
here disputes that or needs a second copy of the values. The observation is
|
|
6581
6978
|
narrower: the **edit** is unremarkable to every gate, which is a different
|
|
6582
6979
|
claim from the values being unguarded.
|
|
6583
|
-
*
|
|
6584
|
-
|
|
6585
|
-
looked at
|
|
6586
|
-
reporting repository,
|
|
6587
|
-
|
|
6588
|
-
|
|
6589
|
-
|
|
6590
|
-
|
|
6591
|
-
|
|
6592
|
-
|
|
6593
|
-
|
|
6594
|
-
|
|
6595
|
-
|
|
6596
|
-
|
|
6597
|
-
|
|
6598
|
-
|
|
6599
|
-
|
|
6600
|
-
|
|
6601
|
-
|
|
6602
|
-
|
|
6603
|
-
|
|
6604
|
-
|
|
6605
|
-
*
|
|
6606
|
-
|
|
6607
|
-
|
|
6608
|
-
|
|
6609
|
-
|
|
6610
|
-
|
|
6611
|
-
|
|
6612
|
-
|
|
6613
|
-
|
|
6614
|
-
|
|
6615
|
-
|
|
6616
|
-
|
|
6617
|
-
|
|
6618
|
-
|
|
6619
|
-
|
|
6620
|
-
|
|
6621
|
-
|
|
6622
|
-
|
|
6623
|
-
|
|
6624
|
-
-
|
|
6625
|
-
|
|
6626
|
-
`
|
|
6627
|
-
|
|
6628
|
-
`
|
|
6629
|
-
|
|
6630
|
-
|
|
6631
|
-
|
|
6632
|
-
|
|
6633
|
-
|
|
6634
|
-
|
|
6635
|
-
|
|
6636
|
-
|
|
6637
|
-
|
|
6638
|
-
|
|
6639
|
-
|
|
6640
|
-
|
|
6641
|
-
|
|
6642
|
-
|
|
6643
|
-
|
|
6644
|
-
|
|
6645
|
-
|
|
6646
|
-
|
|
6647
|
-
|
|
6648
|
-
|
|
6649
|
-
|
|
6650
|
-
|
|
6651
|
-
|
|
6652
|
-
|
|
6653
|
-
|
|
6654
|
-
|
|
6655
|
-
|
|
6656
|
-
|
|
6657
|
-
|
|
6658
|
-
|
|
6659
|
-
|
|
6660
|
-
|
|
6661
|
-
|
|
6662
|
-
|
|
6663
|
-
|
|
6664
|
-
|
|
6665
|
-
|
|
6666
|
-
|
|
6667
|
-
|
|
6668
|
-
|
|
6669
|
-
|
|
6670
|
-
|
|
6671
|
-
|
|
6672
|
-
|
|
6673
|
-
|
|
6674
|
-
|
|
6675
|
-
|
|
6676
|
-
|
|
6677
|
-
`
|
|
6678
|
-
|
|
6679
|
-
a
|
|
6680
|
-
|
|
6681
|
-
|
|
6682
|
-
|
|
6683
|
-
|
|
6684
|
-
|
|
6685
|
-
the
|
|
6686
|
-
|
|
6687
|
-
|
|
6688
|
-
|
|
6689
|
-
|
|
6690
|
-
|
|
6691
|
-
|
|
6692
|
-
|
|
6693
|
-
is
|
|
6694
|
-
|
|
6695
|
-
|
|
6696
|
-
|
|
6697
|
-
|
|
6698
|
-
|
|
6699
|
-
|
|
6700
|
-
|
|
6701
|
-
|
|
6702
|
-
|
|
6703
|
-
|
|
6704
|
-
|
|
6705
|
-
|
|
6706
|
-
|
|
6707
|
-
|
|
6708
|
-
|
|
6709
|
-
|
|
6710
|
-
|
|
6711
|
-
|
|
6712
|
-
|
|
6713
|
-
|
|
6714
|
-
|
|
6715
|
-
|
|
6716
|
-
|
|
6717
|
-
|
|
6718
|
-
|
|
6719
|
-
|
|
6720
|
-
|
|
6721
|
-
|
|
6722
|
-
|
|
6723
|
-
|
|
6724
|
-
|
|
6725
|
-
|
|
6726
|
-
|
|
6727
|
-
the
|
|
6728
|
-
|
|
6729
|
-
|
|
6730
|
-
|
|
6731
|
-
|
|
6732
|
-
|
|
6733
|
-
|
|
6734
|
-
|
|
6735
|
-
|
|
6736
|
-
|
|
6737
|
-
|
|
6738
|
-
|
|
6739
|
-
|
|
6740
|
-
|
|
6741
|
-
|
|
6742
|
-
|
|
6743
|
-
|
|
6744
|
-
|
|
6745
|
-
|
|
6746
|
-
|
|
6747
|
-
|
|
6748
|
-
|
|
6749
|
-
|
|
6750
|
-
|
|
6751
|
-
|
|
6752
|
-
|
|
6753
|
-
|
|
6754
|
-
|
|
6755
|
-
|
|
6756
|
-
|
|
6757
|
-
|
|
6758
|
-
|
|
6759
|
-
|
|
6760
|
-
|
|
6761
|
-
|
|
6762
|
-
|
|
6763
|
-
|
|
6764
|
-
|
|
6765
|
-
|
|
6766
|
-
|
|
6767
|
-
|
|
6768
|
-
|
|
6769
|
-
|
|
6770
|
-
|
|
6771
|
-
|
|
6772
|
-
|
|
6773
|
-
|
|
6774
|
-
|
|
6775
|
-
|
|
6776
|
-
|
|
6777
|
-
|
|
6778
|
-
|
|
6779
|
-
|
|
6980
|
+
*Blocker **(1)** was answered on 2026-08-31, in the direction that opens the
|
|
6981
|
+
entry rather than closing it.* It read that frequency is unmeasured and that
|
|
6982
|
+
the one corpus anybody had looked at pointed at zero: 8 commits touching
|
|
6983
|
+
`*.reqs.ts` in the reporting repository, every `params:` line in every diff an
|
|
6984
|
+
addition. A 0.9.1 → 0.11.0 upgrade report from `mine-capablanca` re-ran that
|
|
6985
|
+
count over the **41 revisions** that have touched the same registry, evaluating
|
|
6986
|
+
the file at each revision and comparing key by key rather than reading diffs,
|
|
6987
|
+
and it reads **2 keys added, 1 removed and 7 values changed**. The 8-commit
|
|
6988
|
+
figure was a sample and not an answer; it is withdrawn here and in the
|
|
6989
|
+
tautology entry under `Considered and rejected` that quoted it.
|
|
6990
|
+
*One of the seven landed with nothing capable of objecting, and it is the
|
|
6991
|
+
general case rather than an accident of that commit.* A single commit moved
|
|
6992
|
+
`AI-14.sampledCandidates` from 8 to 3 and `AI-14.samplingTemperature` from 5 to
|
|
6993
|
+
20, and its entire diff is a proposal, a delta, the registry and the
|
|
6994
|
+
regenerated `SPEC.md`. **No spec file and no implementation file.** Nine
|
|
6995
|
+
implementation modules in that project import the registry and read params out
|
|
6996
|
+
of it at runtime, so the opponent's behaviour on the easy setting changed with
|
|
6997
|
+
the suite green — the scenarios read the same two values the implementation
|
|
6998
|
+
reads, so both sides moved together. `render --check` did not object and
|
|
6999
|
+
structurally cannot: it enforces that the rendering *moved*, never that the
|
|
7000
|
+
movement was intended. What caught it was a reviewer on ten lines of `SPEC.md`
|
|
7001
|
+
diff, with a 63-line proposal beside it arguing for exactly that move.
|
|
7002
|
+
*So review is the gate, and the second corpus says it holds for a reason
|
|
7003
|
+
nothing enforces.* That registry is **58 of 58** params interpolated into their
|
|
7004
|
+
own statement, against 82 of 89 — 92.1% — here. Every param edit there moves a
|
|
7005
|
+
line of `SPEC.md`, which is why the commit above was visible at all, and the
|
|
7006
|
+
reporter is explicit that nothing makes this true: `check` is at zero issues,
|
|
7007
|
+
and a declared key that no statement interpolates is not a problem at any
|
|
7008
|
+
severity. A 100% discipline held over 41 revisions with nothing behind it is
|
|
7009
|
+
one careless commit away from being 98%.
|
|
7010
|
+
*What blocks the decision now — and **(2)** has become the opposite of what it
|
|
7011
|
+
said.* **(2) A shape is obvious, and it is small.** `check` already reports
|
|
7012
|
+
`unbound-param` for a placeholder with no key; the reverse — a declared key no
|
|
7013
|
+
statement interpolates — is reported at no severity, and it is the exact static
|
|
7014
|
+
test for "this param's edit leaves no trace outside the line that changed". It
|
|
7015
|
+
needs no previous value, so it is not the rejected snapshot wearing a hat: it
|
|
7016
|
+
is a property of one revision. The nearest precedent is `rationale-placeholder`,
|
|
7017
|
+
a WARNING deliberately below `hasError` so that an existing green registry
|
|
7018
|
+
gains a line of output and keeps its exit code, and that is the level this would
|
|
7019
|
+
take. **(3) What it would fire on here is the objection.** Five of this
|
|
7020
|
+
repository's seven uninterpolated params are **structurally uninterpolatable** —
|
|
7021
|
+
`ATX-7.payloadKinds` and `ATX-63.payloadKinds` are kind → payload tables,
|
|
7022
|
+
`ATX-15.sortMethods`, `ATX-15.bannedApis` and `ATX-72.quotes` are lists, and
|
|
7023
|
+
`non-scalar-interpolation` is the diagnostic that refuses a statement carrying
|
|
7024
|
+
any of them. So the rule must exempt non-scalars, and what is left is
|
|
7025
|
+
`ATX-15.comparator` and `ATX-26.code`: two params, in one repository, that
|
|
7026
|
+
their author simply did not interpolate. A warning firing twice on its own
|
|
7027
|
+
corpus and zero times on the only other one measured is a rule looking for a
|
|
7028
|
+
population. And the exemption is the hole rather than a detail — the five it
|
|
7029
|
+
must skip are precisely the shape §2 widened the params rule to cover
|
|
7030
|
+
**because lists and tables are the most drift-prone constants there are**.
|
|
7031
|
+
*What would decide it:* an adopter whose interpolation rate is below 100% and
|
|
7032
|
+
whose uninterpolated params are scalars. Both corpora sit at the extremes —
|
|
7033
|
+
58 of 58 there, and here the gap is almost entirely the shape the rule cannot
|
|
7034
|
+
see — so nobody has yet measured the population it would serve. Until then the
|
|
7035
|
+
answer for the five that matter may still be nothing more than a line in
|
|
7036
|
+
`intent-quality.md`, the four things no gate is looking at already being that
|
|
7037
|
+
page's subject and this being a fifth of the same kind.
|
|
7038
|
+
|
|
7039
|
+
- **The intent layer is separable from the repository it describes, and the
|
|
7040
|
+
verification layer is not — so a third party can audit intent and never
|
|
7041
|
+
obtain a verdict.** Filed 2026-08-22 from a design evaluation of using Attest
|
|
7042
|
+
as an external audit tool that touches none of the auditee's implementation.
|
|
7043
|
+
The separability is already bought and is stronger than it looks: §2 requires
|
|
7044
|
+
a registry to be a *literal*, no imported constant and no computed value, so
|
|
7045
|
+
`*.reqs.ts` names nothing in the project it is about and is a file another
|
|
7046
|
+
party could own outright. The four static commands execute none of the
|
|
7047
|
+
auditee's code, which is the posture an auditor needs and is already the
|
|
7048
|
+
documented reason for the split. The arrangement that follows is a two-party
|
|
7049
|
+
contract with `check` as its interface: the auditing party writes the SHALL,
|
|
7050
|
+
the developing party owes each id a scenario, and `uncovered-requirement` and
|
|
7051
|
+
`orphan-test` are the two directions that contract can be broken in.
|
|
7052
|
+
|
|
7053
|
+
*What blocks it, in the order that decides it.* **The verdict half cannot
|
|
7054
|
+
cross the boundary, and that is §1 rather than a gap.** `verify` and `archive`
|
|
7055
|
+
run the suite, the suite's specs import the implementation, and the environment
|
|
7056
|
+
those specs need is the auditee's — reachable only by trusting their
|
|
7057
|
+
`--vitest-config`. So what a decoupled auditor can obtain is the readable
|
|
7058
|
+
obligations and never `tests-red`, which is the same asymmetry `status` already
|
|
7059
|
+
carries and states about itself. That would be tolerable if the static report
|
|
7060
|
+
read as procedural, and it does not: an audit reader takes a full-coverage
|
|
7061
|
+
`check` as a claim about behaviour, while §11 records that coverage is binary,
|
|
7062
|
+
that an assertion can echo the param it is measuring, that shortening a
|
|
7063
|
+
list-valued param silently removes cases, that a quantifier is covered only as
|
|
7064
|
+
far as hand-written scenarios reach, and that two contradictory ids under
|
|
7065
|
+
different prefixes both pass. **The mechanism assumes one tree.** `locate.ts`
|
|
7066
|
+
scans a single project root, and mechanism 1 needs the spec to `import` the
|
|
7067
|
+
registry, so the externally-owned file has to be physically in the tree before
|
|
7068
|
+
any command runs — a checkout step, not a flag. Naming a registry outside the
|
|
7069
|
+
root would be a second answer to where intent lives, which is the shape this
|
|
7070
|
+
repository refuses elsewhere. **The flat ID space bills exactly here.** §11
|
|
7071
|
+
already records that nothing allocates a prefix and that the cost arrives when
|
|
7072
|
+
adoption succeeds; two parties sharing one registry *is* that case, and
|
|
7073
|
+
`duplicate-prefix` can only report a collision after both files were read.
|
|
7074
|
+
**And `--apply` writes into the other party's file.** The merge splices ADDED
|
|
7075
|
+
requirements into the registry, so the one command that edits a file the user
|
|
7076
|
+
cannot regenerate would be editing a document whose owner is not in the
|
|
7077
|
+
repository. Nothing about that is unsafe; it is that the ownership the
|
|
7078
|
+
arrangement exists to establish is the thing the merge step quietly crosses.
|
|
7079
|
+
|
|
7080
|
+
*What would settle it, and what it costs to try.* The whole arrangement is
|
|
7081
|
+
derivable from the design and has been run by nobody, so this is n=0 and cheap
|
|
7082
|
+
to move: the checkout-step form needs no engine change at all, and one
|
|
7083
|
+
adopter running it would say whether the two-party report is read as procedural
|
|
7084
|
+
or over-read as a behavioural claim. That is the measurement, and it is a
|
|
7085
|
+
question about the reader rather than about the code.
|
|
7086
|
+
|
|
7087
|
+
- **A freshness gate on the file `attest init` writes (`init --check`).**
|
|
7088
|
+
Rejected 2026-08-19, and **reopened 2026-08-31 by the first measurement of the
|
|
7089
|
+
exposure it was rejected on.** The rejection is kept below in full because it
|
|
7090
|
+
is still the argument to beat, and what beat one of its two load-bearing claims
|
|
7091
|
+
is a 0.9.1 → 0.11.0 upgrade report from `mine-capablanca`: the committed
|
|
7092
|
+
`.claude/skills/attest/SKILL.md` in that repository, written by `init` at
|
|
7093
|
+
0.9.1, differs from the 0.11.0 body by **9 hunks and 110 changed lines, 403
|
|
7094
|
+
lines against 458**. It was found only because the reporter wrote a script
|
|
7095
|
+
importing `workflowBody()` out of the installed build and diffed it. `check`,
|
|
7096
|
+
`cover`, `verify` and `render --check` were all green with the document stale.
|
|
7097
|
+
*The backstop is what failed, and it failed on the worst available case.* The
|
|
7098
|
+
rejection's second claim is that every mistake a stale document can cause is
|
|
7099
|
+
already a diagnostic with a fix hint — "a registry written the old way is
|
|
7100
|
+
`registry-not-static`", so the agent corrects itself from the report. The stale
|
|
7101
|
+
text here is `never-red`'s ordering rule, which `[0.10.0]` archived as
|
|
7102
|
+
`evidence-not-ordering` on the grounds that the ordering was never in the
|
|
7103
|
+
engine. An agent reading that document is told to do something the engine does
|
|
7104
|
+
not require. Its behaviour is more conservative, not wrong-shaped, so **no
|
|
7105
|
+
issue fires at any severity** — there is nothing to correct itself from,
|
|
7106
|
+
because nothing it does is an error. The backstop covers a stale document
|
|
7107
|
+
producing bad input. It does not cover a stale document producing unnecessary
|
|
7108
|
+
work, and the second is what six weeks of drift actually produced.
|
|
7109
|
+
*The first claim survives, and it is still the objection.* A gate on this file
|
|
7110
|
+
does turn red on a change the user did not make and is cleared by a commit
|
|
7111
|
+
containing no decision. Nothing in the report disputes that. What the report
|
|
7112
|
+
supplies is the other side of the ledger, which the rejection priced as "one
|
|
7113
|
+
honest and small exposure": 110 lines, over one upgrade, undetectable without
|
|
7114
|
+
writing a script against `dist/`.
|
|
7115
|
+
*And the property that makes `init` idempotent is the same one that makes the
|
|
7116
|
+
drift invisible.* `skillContent()` and `workflowBody()` take no argument — not
|
|
7117
|
+
the version, not the project — deliberately, so that re-running `init` after an
|
|
7118
|
+
upgrade is always safe. The consequence is that the file carries **no version
|
|
7119
|
+
stamp**: nothing in the adopter's repository records which version wrote it,
|
|
7120
|
+
and *"Re-run `attest init` after upgrading"* — which the CLI prints once, at
|
|
7121
|
+
`init` time, to a reader who is not upgrading — is only actionable by somebody
|
|
7122
|
+
who already knows they need to. A diff against the installed build is not one
|
|
7123
|
+
detector among several. It is the only one the file's own design permits.
|
|
7124
|
+
*What is undecided is the shape, and the rejection narrows it rather than being
|
|
7125
|
+
answered by it.* A red `init --check` in CI is the thing that was rejected, and
|
|
7126
|
+
the report does not rehabilitate it: it would fail a fresh clone on somebody
|
|
7127
|
+
else's release, which is the same standard `render --check` was held to when a
|
|
7128
|
+
freshness gate that calls a fresh clone stale was called worse than no gate.
|
|
7129
|
+
What was never proposed, and is not what the heading names, is a **notice on a
|
|
7130
|
+
command the adopter is already running** — the same information at the cost of
|
|
7131
|
+
no gate and no chore commit. Pricing that is the next step; it is not a
|
|
7132
|
+
decision already taken, and it needs its own answer to which command, at what
|
|
7133
|
+
level, and whether a WARNING nobody can clear without running `init` is a
|
|
7134
|
+
chore in a diagnostic's clothes after all.
|
|
7135
|
+
*What is still unmeasured:* whether the drift cost that reporter anything. 110
|
|
7136
|
+
changed lines measures the document, not the work, and the report is explicit
|
|
7137
|
+
that it did not run `init` and did not act on the withdrawn rule. Nobody has
|
|
7138
|
+
yet followed a stale instruction and paid for it, and the rejection below is
|
|
7139
|
+
entitled to that until somebody has.
|
|
7140
|
+
|
|
7141
|
+
The rejection, kept as it was written:
|
|
7142
|
+
|
|
7143
|
+
`render --check` is the obvious precedent and is the opposite case. A rendering
|
|
7144
|
+
is a function of the *user's registry*, so its staleness means the user moved
|
|
7145
|
+
intent without regenerating — a real signal, with the fix in their hands. The
|
|
7146
|
+
skill is a function of the *installed version*, so a gate on it turns red on a
|
|
7147
|
+
change the user did not make, and is cleared by a commit containing no decision.
|
|
7148
|
+
That is a chore wearing a diagnostic's clothes, and a gate that cannot be about
|
|
7149
|
+
anything the reader did is worse than no gate — the same standard 0.1.6 set when
|
|
7150
|
+
it gave `render` no formatting options at all. An earlier draft of `attest init`
|
|
7151
|
+
spent a second command, this gate and a second self-requirement on it. What that
|
|
7152
|
+
would have bought is immunity to one honest and small exposure: an old file
|
|
7153
|
+
describes an old workflow to an agent that trusts it. The backstop is that every
|
|
7154
|
+
mistake it can cause is already a diagnostic with a fix hint — a registry
|
|
7155
|
+
written the old way is `registry-not-static` — so the agent corrects itself from
|
|
7156
|
+
the report, and `init` is idempotent, which makes re-running it the entire
|
|
7157
|
+
update mechanism. Recorded here rather than left in the 0.3.0 entry that shipped
|
|
7158
|
+
`init`, because the queue above depends on it: a gate is the first thing
|
|
7159
|
+
proposed each time a new agent target is considered, and the answer must not
|
|
7160
|
+
need re-deciding per target.
|
|
7161
|
+
|
|
7162
|
+
*One locator moved with the entry.* "The queue above" was written from inside
|
|
7163
|
+
`Considered and rejected` and means the queue of agent targets, not the entries
|
|
7164
|
+
now above this one; the point it makes — that the answer must not need
|
|
7165
|
+
re-deciding per target — is unaffected by where the entry sits.
|
|
7166
|
+
|
|
7167
|
+
- **A `fix` field on `Issue`, derived from `code` the way `docsUrl` is.** From
|
|
7168
|
+
`nostics` 1.2.0 (vercel-labs), read 2026-09-01, whose `Diagnostic` carries
|
|
7169
|
+
`why` / `fix` / `docs` as three fields rather than one prose message. Most of
|
|
7170
|
+
what that library is for is already here and stricter — its docs link is
|
|
7171
|
+
authored per code with a `docs: false` opt-out, ours is derived from the code
|
|
7172
|
+
at render time and cannot be forgotten or disagreed with — so the `why`/`fix`
|
|
7173
|
+
split is the one half of it this file has no answer for.
|
|
7174
|
+
*What the roster says, and it is not what the proposal assumed.* The claim
|
|
7175
|
+
filed was that a fix is either buried in prose or absent. Reading all 41
|
|
7176
|
+
rendered messages in `docs/en/troubleshooting.md`: **11 state an action to
|
|
7177
|
+
take** (`uncovered-requirement`, `never-red`, `orphan-test`, `open-unresolved`
|
|
7178
|
+
and seven more), **5 state the constraint but not the action**
|
|
7179
|
+
(`registry-no-default`, `non-scalar-interpolation`, `unknown-target`,
|
|
7180
|
+
`compiler-unsupported`, `invalid-change-name`), and **25 state only what is
|
|
7181
|
+
wrong** — `registry-not-static` is the whole of "Value is not a literal.", and
|
|
7182
|
+
`tests-red`, `unbound-param`, `duplicate-requirement` and `possible-drift` are
|
|
7183
|
+
the same shape. So the distribution is uneven and unenforced rather than
|
|
7184
|
+
absent, which is a weaker premise than the one filed but a more precise one:
|
|
7185
|
+
what is missing is not the text, it is anything that makes the text obligatory.
|
|
7186
|
+
(Classified by reading. A first pass by pattern undercounted — it missed the
|
|
7187
|
+
lowercase mid-sentence imperatives in `never-red` and `empty-spec` — and its
|
|
7188
|
+
number is deliberately not quoted here.)
|
|
7189
|
+
*The derivation argument holds, and it is the reason to file this at all.* 31
|
|
7190
|
+
of the 41 codes appear as a `code: '…'` literal, across 43 sites; the other 10
|
|
7191
|
+
are built by a ternary or handed through a variable, which is the shape
|
|
7192
|
+
`types.ts` narrows `code` to `IssueCode` for rather than policing with a regex.
|
|
7193
|
+
A `fix` authored at issue sites inherits that split and can be forgotten at any
|
|
7194
|
+
of the 43. A `fix` keyed on `code` and resolved when a report is rendered
|
|
7195
|
+
cannot be — it covers all 41 including the 10, which is exactly the argument
|
|
7196
|
+
`docs.ts` already makes for the link, applied to the other half of the line.
|
|
7197
|
+
*What blocks it is that the text would then exist four times.* Every one of the
|
|
7198
|
+
41 codes already has a `**Solution**` block in `docs/en/troubleshooting.md` and
|
|
7199
|
+
its counterpart in `docs/zh` — 41 sections each, checked by
|
|
7200
|
+
`docs-consistency.spec.ts` for existence and by nothing for content. A `fix`
|
|
7201
|
+
string makes a fourth statement of the same thing, and no gate can check the
|
|
7202
|
+
four agree. That is the failure mode `docsUrl` was designed to avoid, arriving
|
|
7203
|
+
through the field added next to it.
|
|
7204
|
+
*And a surface the proposal had not counted: 80 Symptom blocks.* Each of the 41
|
|
7205
|
+
sections opens with a fenced block quoting real output, in both languages — 82
|
|
7206
|
+
blocks, of which **80 quote the terminal rendering** and would be dated the
|
|
7207
|
+
moment `formatIssues` gains a `fix:` line. (The other two are `internal-error`,
|
|
7208
|
+
whose Symptom is a `--json` document, so it is the one section a rendering
|
|
7209
|
+
change leaves alone. It is worth naming rather than rounding away: it is the
|
|
7210
|
+
same carve-out `docs-consistency.spec.ts` already makes for that code.) **No
|
|
7211
|
+
test reads any of them** — that suite asserts the code-to-section
|
|
7212
|
+
correspondence and the pinned docs URLs, and has no assertion over a Symptom
|
|
7213
|
+
block at all — so nothing would demand the 80 edits and nothing would catch
|
|
7214
|
+
them being skipped. The documentation cost is larger than the feature.
|
|
7215
|
+
*What would move it.* Either a gate binding a code's `fix` to its `Solution`
|
|
7216
|
+
section, so the fourth copy is checked rather than merely written — which would
|
|
7217
|
+
also retire the 82-block exposure by making one of the two a function of the
|
|
7218
|
+
other — or an adoption report that one of the 25 action-less messages actually
|
|
7219
|
+
cost somebody a round trip. Without the first, this trades a derivable field for
|
|
7220
|
+
an underivable duplicate.
|
|
7221
|
+
|
|
7222
|
+
- **A Stop hook that will not let an agent end a turn while `check self` is red.**
|
|
7223
|
+
From the same read: `nostics` ships `hooks/consume-diagnostic.mjs`, a Claude
|
|
7224
|
+
Code Stop hook that reads the log its dev-server collector writes and returns
|
|
7225
|
+
`{"decision": "block"}` with the diagnostic as the reason, so the agent keeps
|
|
7226
|
+
working rather than stopping on a red application. Three details there are the
|
|
7227
|
+
design and not the plumbing, and are the transferable part: it guards on
|
|
7228
|
+
`stop_hook_active` so a blocked stop cannot loop; it feeds back **one**
|
|
7229
|
+
diagnostic per turn rather than the whole log; and when it consumes one it drops
|
|
7230
|
+
every other entry sharing that code and file, so a fault at twenty call sites is
|
|
7231
|
+
one round trip and not twenty.
|
|
7232
|
+
*Why the shape fits this repository specifically.* `CLAUDE.md`'s "Before
|
|
7233
|
+
committing" and the whole of `pre-commit-check` are discipline: nothing runs
|
|
7234
|
+
`check self` unless a person or an agent remembers to, and the repeat-grouping
|
|
7235
|
+
that hook does by hand is something `formatIssues` already computes. `.claude/`
|
|
7236
|
+
here holds five skills, no hooks and no `settings.json`, so this would be the
|
|
7237
|
+
first of its kind rather than an extension of anything.
|
|
7238
|
+
*Measured cost, 2026-09-01.* `node bin/attest.js check self --json` runs in
|
|
7239
|
+
**1.04–1.57 s** over three consecutive runs; `status self --json` in 0.78 s.
|
|
7240
|
+
That is paid at the end of every turn, which is the number to weigh rather than
|
|
7241
|
+
the wall clock of a single invocation.
|
|
7242
|
+
*What blocks it is the stale-`dist/` trap, and a hook is the worst place to meet
|
|
7243
|
+
it.* `bin/attest.js` loads `dist/`, not `src/` — the Gotcha this repository
|
|
7244
|
+
already keeps — so a Stop hook reports on the last build. After a `src/` edit it
|
|
7245
|
+
blocks on a diagnostic the working tree has already fixed, or clears while the
|
|
7246
|
+
tree is broken, and in both directions it is a gate arguing about a state nobody
|
|
7247
|
+
is in. Either the hook builds first, adding seconds to every turn, or it runs the
|
|
7248
|
+
engine from source and stops being the thing an adopter could copy.
|
|
7249
|
+
*And the larger question it sits in front of.* If this is right here it is right
|
|
7250
|
+
for an adopter, which makes it a question about what `attest init` writes rather
|
|
7251
|
+
than about `.claude/` in this repository. A hook that can block an agent's stop
|
|
7252
|
+
is a much larger commitment than a document, and the rejection quoted in full
|
|
7253
|
+
inside the `init --check` entry in this section — the gate on the installed
|
|
7254
|
+
skill's freshness — turned on exactly the distinction it needs:
|
|
7255
|
+
a signal about work the reader actually did, against a chore the tool imposes. A
|
|
7256
|
+
block on `check` is the first kind, which is the argument for it, and that
|
|
7257
|
+
argument should be made explicitly rather than arrived at by shipping.
|
|
7258
|
+
*What would move it.* Running it here, unshipped and repo-local, for a few weeks,
|
|
7259
|
+
and counting how often it blocks on something that would otherwise have been
|
|
7260
|
+
committed. If that count is zero the discipline was already working and the hook
|
|
7261
|
+
is overhead; if it is not, the number is the case for `init` writing one.
|
|
6780
7262
|
|
|
6781
7263
|
## Considered and rejected
|
|
6782
7264
|
|
|
@@ -6788,6 +7270,814 @@ it sat between 0.2.0 and 0.1.7 for two releases, where standing still meant
|
|
|
6788
7270
|
sinking one version deeper each time a release was cut above it, and a rejection
|
|
6789
7271
|
filed under a version reads as belonging to it.
|
|
6790
7272
|
|
|
7273
|
+
### A backlink index over `archive/`, recording which change introduced or renamed an id
|
|
7274
|
+
|
|
7275
|
+
Proposed from OpenViking's `.relations.json`, restated twice on measurement, and
|
|
7276
|
+
rejected 2026-09-01. It is kept as it was written under *Under consideration*,
|
|
7277
|
+
because what closed it is the argument the entry had already assembled against
|
|
7278
|
+
itself rather than anything that arrived afterwards; the closing paragraphs are
|
|
7279
|
+
at the foot of the entry.
|
|
7280
|
+
|
|
7281
|
+
**A merged requirement carries no trace of the change that produced it, and
|
|
7282
|
+
the rename chain is the part actually lost.** `RequirementSchema` is
|
|
7283
|
+
`statement`, `rationale`, `params`, `outOfScope`; the splice writes those and
|
|
7284
|
+
nothing else, so after `--apply` no field and no sidecar records which change
|
|
7285
|
+
introduced an id, which modified it, or that one id used to be another. The
|
|
7286
|
+
delta that knew is in `archive/`, which nothing reads. Proposed from
|
|
7287
|
+
OpenViking's `.relations.json`: links between memories live in a
|
|
7288
|
+
directory-level sidecar, deliberately kept out of the content, and are
|
|
7289
|
+
rendered on demand at retrieval time (`docs/design/memory-link-design.md`).
|
|
7290
|
+
*The argument for the sidecar over a field is in their competitor analysis
|
|
7291
|
+
rather than their design.* The same document reads GBrain, which writes links
|
|
7292
|
+
into the page body, and records what that costs: rename a page and every
|
|
7293
|
+
inbound link still points at the old slug, with fuzzy matching at the read end
|
|
7294
|
+
as the only mitigation. A `supersedes:` or `introducedBy:` field on a
|
|
7295
|
+
requirement would be that mistake in this repository's terms — the registry is
|
|
7296
|
+
a literal and a single source (§2), and a field naming a folder that a later
|
|
7297
|
+
rename can move is a second source with nothing comparing it against the
|
|
7298
|
+
first. A derived index has no such copy to go stale: it is recomputed from the
|
|
7299
|
+
archived deltas every time it is read.
|
|
7300
|
+
*What blocks the decision, restated on 2026-08-19 because half of what was
|
|
7301
|
+
filed here is no longer true.* This entry was blocked on the empty archive and
|
|
7302
|
+
on the corpus being rename-free, and only the second still holds. `archive/`
|
|
7303
|
+
now has a real change in it — the `open` field, put through the workflow the
|
|
7304
|
+
same day — so the store this would index exists. Its delta carries `added` and
|
|
7305
|
+
nothing else, measured rather than assumed, so a backlink index is still
|
|
7306
|
+
testable only against fixtures for the operation it is actually about. Note
|
|
7307
|
+
what that says about the odds of the blocker clearing on its own: `--apply`
|
|
7308
|
+
refuses RENAMED outright, so no change this repository archives can ever
|
|
7309
|
+
exercise a rename until that refusal is lifted, and lifting it is a separate
|
|
7310
|
+
decision with its own reason (`splice.ts` — deleting an entry has no smaller
|
|
7311
|
+
span to fall back to). The blocker is therefore not "waiting for a rename to
|
|
7312
|
+
happen"; it is downstream of a refusal nobody has proposed removing. And one
|
|
7313
|
+
question the OpenViking reading does not answer, which is whether the chain is
|
|
7314
|
+
wanted at all. §7 applies
|
|
7315
|
+
`RENAMED` to a `Map<id, Requirement>` and the old id is then simply gone —
|
|
7316
|
+
deliberately, as far as anything written says. The case for recovering it is an
|
|
7317
|
+
external reference that outlives the rename, a ticket or a commit message
|
|
7318
|
+
naming a retired id, and nobody has reported one.
|
|
7319
|
+
*Measured against a second corpus on 2026-08-31, and it points the same way.* A
|
|
7320
|
+
0.9.1 → 0.11.0 upgrade report from `mine-capablanca` walked all 41 revisions of
|
|
7321
|
+
that project's single registry and all 49 of its archived changes: **80 ids have
|
|
7322
|
+
ever appeared in it and 80 are in it now — zero removed, zero renamed** — and
|
|
7323
|
+
neither `removed:` nor `renamed:` appears in non-empty form in any delta.
|
|
7324
|
+
Commit messages on every branch, the whole tree and the issue tracker were
|
|
7325
|
+
searched for id-shaped tokens the registry does not define. The external
|
|
7326
|
+
reference this entry waits for did not turn up in sixteen days and forty-nine
|
|
7327
|
+
changes.
|
|
7328
|
+
*One id did outlive itself, from the other direction, and the reason it was left
|
|
7329
|
+
alone is a mechanism this entry did not have.* `AI-10` has never been in that
|
|
7330
|
+
registry, and two committed documents discuss the gap it leaves. The archived
|
|
7331
|
+
proposal's stated reason for not renumbering to close it is that doing so "would
|
|
7332
|
+
mean re-recording a failing run against an implementation that already exists".
|
|
7333
|
+
So `never-red` prices a rename directly: an id cannot be renumbered without
|
|
7334
|
+
re-earning its evidence, and an author facing that bill chose to write about the
|
|
7335
|
+
hole instead. That is a second reason the corpus is rename-free, independent of
|
|
7336
|
+
`--apply`'s refusal, and it argues the rate **stays** at zero rather than that
|
|
7337
|
+
nobody has got round to it.
|
|
7338
|
+
*What this does to the entry:* nothing is refuted, and the blocker is now
|
|
7339
|
+
blocked twice over. It was already downstream of a refusal nobody has proposed
|
|
7340
|
+
removing; it is also downstream of an obligation that makes the operation
|
|
7341
|
+
expensive on purpose. An index over an event that two independent mechanisms
|
|
7342
|
+
discourage is an index over an empty set.
|
|
7343
|
+
*Rejected 2026-09-01, on the entry's own last sentence rather than on anything
|
|
7344
|
+
new.* What closes it is that the two mechanisms are not a coincidence of one
|
|
7345
|
+
corpus and not a stage adoption passes through. `--apply` refuses RENAMED for a
|
|
7346
|
+
reason recorded in `splice.ts` — deleting an entry has no smaller span to fall
|
|
7347
|
+
back to — and `never-red` prices the operation independently, since an id cannot
|
|
7348
|
+
be renumbered without re-earning its evidence. The second is the stronger of the
|
|
7349
|
+
two because it does not depend on a refusal anybody could lift: an author who
|
|
7350
|
+
faced that bill wrote about the hole `AI-10` leaves instead of closing it, which
|
|
7351
|
+
is the mechanism arguing the rate **stays** at zero rather than that nobody has
|
|
7352
|
+
got round to it. An index whose subject two independent mechanisms discourage is
|
|
7353
|
+
not waiting for evidence; it is waiting for a decision nobody has proposed, taken
|
|
7354
|
+
for reasons that have nothing to do with this.
|
|
7355
|
+
|
|
7356
|
+
*What was measured, and it is the whole of the case against.* Across the second
|
|
7357
|
+
corpus: 80 ids have ever appeared and 80 are in it now, over 49 archived changes
|
|
7358
|
+
— zero removed, zero renamed — with neither `removed:` nor `renamed:` appearing
|
|
7359
|
+
in non-empty form in any delta. Commit messages on every branch, the whole tree
|
|
7360
|
+
and the issue tracker were searched for id-shaped tokens the registry does not
|
|
7361
|
+
define. The external reference this entry existed to serve is the one thing that
|
|
7362
|
+
would have justified it, and sixteen days and forty-nine changes produced none.
|
|
7363
|
+
|
|
7364
|
+
*One structural consequence, recorded because it is a change to somebody else's
|
|
7365
|
+
entry.* Two open entries rested on `--apply`'s refusal of RENAMED; closing this
|
|
7366
|
+
one leaves **one**, the third meaning under *Retiring a requirement* below. That
|
|
7367
|
+
matters in the direction of not lifting the refusal: a decision with two
|
|
7368
|
+
dependents is easier to justify than a decision with one, and the count is now
|
|
7369
|
+
one. Both cross-references have been corrected rather than left to be inferred.
|
|
7370
|
+
|
|
7371
|
+
*What would reopen it.* Not a report of the drift this entry describes — that
|
|
7372
|
+
report is what sixteen days and two corpora failed to produce. It reopens if
|
|
7373
|
+
`--apply`'s refusal of RENAMED is lifted for its own reason, because the
|
|
7374
|
+
operation then becomes available and the corpus stops being evidence about a
|
|
7375
|
+
population that cannot exist. That is the one event that changes the argument,
|
|
7376
|
+
and it is not this entry's to trigger.
|
|
7377
|
+
|
|
7378
|
+
### A detector for an assertion that recomputes its expectation from the param under test
|
|
7379
|
+
|
|
7380
|
+
Proposed 2026-08-03 from `mine-capablanca`'s report, carried through three
|
|
7381
|
+
re-measurements, and rejected 2026-08-31. The defect it describes is real and
|
|
7382
|
+
stays undetected: an assertion deriving its expectation from the same param the
|
|
7383
|
+
code under test consumed has no independent term, so the value can move with the
|
|
7384
|
+
suite green, `check` silent and `possible-drift` correctly not firing. *What
|
|
7385
|
+
killed it is the detector's own score, and that is now the whole of the case.*
|
|
7386
|
+
The one anybody built scores **50% on the shape and 0% on the harm**: of 8 flags, 4 are real echoes and
|
|
7387
|
+
none of the 4 is harmful, while the false positives are pinned by a term in
|
|
7388
|
+
another file that a single-body detector cannot see by construction. Precision
|
|
7389
|
+
fell on every addition measured.
|
|
7390
|
+
|
|
7391
|
+
*The frequency half of this rejection was wrong on the day it was written, and it
|
|
7392
|
+
is withdrawn.* It read that frequency is "approximately zero across two
|
|
7393
|
+
codebases", on 8 commits in the reporting repository across which no param value
|
|
7394
|
+
was ever modified. A 0.9.1 → 0.11.0 upgrade report from `mine-capablanca` on
|
|
7395
|
+
2026-08-31 re-ran that count properly — all **41 revisions** of the same
|
|
7396
|
+
registry, the file evaluated at each and compared key by key rather than read out
|
|
7397
|
+
of diff hunks — and it comes back with **seven values changed**. Not
|
|
7398
|
+
approximately zero. The breakdown belongs to the param-value entry under `Under
|
|
7399
|
+
consideration` and is not re-quoted here, because a figure stated in two places
|
|
7400
|
+
goes stale in one. The 8-commit sample was what was reachable on the day and not
|
|
7401
|
+
an answer to the question, and quoting it as one is the failure this file's own
|
|
7402
|
+
method section warns about. 38 revisions of *this* registry with 57 keys added, 0
|
|
7403
|
+
removed and exactly 1 value changed still stands, and is now the outlier rather
|
|
7404
|
+
than the confirmation.
|
|
7405
|
+
|
|
7406
|
+
*What that leaves is a rejection standing on one leg, deliberately.* A defect
|
|
7407
|
+
occurring seven times in six weeks in one adopting repository is not a hazard
|
|
7408
|
+
nobody has committed; it is a thing that happens. The detector is still refused,
|
|
7409
|
+
because scoring 0% on harm is a property of the mechanism that no frequency
|
|
7410
|
+
improves — a rule that flags four true echoes and is right about the danger of
|
|
7411
|
+
none of them is not made useful by there being more echoes to flag. Where the
|
|
7412
|
+
frequency finding goes instead is the param-value entry under `Under
|
|
7413
|
+
consideration`, which was always the entry about the move rather than about the
|
|
7414
|
+
assertion, and which the same report reinforces rather than settles.
|
|
7415
|
+
|
|
7416
|
+
*And the reopening condition was very nearly met, which is worth recording
|
|
7417
|
+
precisely.* It asks for a param whose value moved, whose assertion echoed it, and
|
|
7418
|
+
where the move was **harmful**. The report supplies the first two in one commit:
|
|
7419
|
+
two `AI-14` params changed, the scenarios reading the same values the
|
|
7420
|
+
implementation reads, the whole suite green, and a diff containing no spec file
|
|
7421
|
+
and no implementation file at all. The third leg fails, and fails cleanly — that
|
|
7422
|
+
move was the entire point of the change, argued for in a 63-line proposal
|
|
7423
|
+
committed beside it. So the closest instance anybody has is a **correct** edit
|
|
7424
|
+
that no gate could have objected to, which is evidence about the gate and not
|
|
7425
|
+
about the assertion.
|
|
7426
|
+
|
|
7427
|
+
*The guidance half is not what is rejected here.* `[0.5.0]` replaced "read the
|
|
7428
|
+
param inside the assertion" everywhere it appeared with what the evidence
|
|
7429
|
+
supports — the expectation must not be a function of the param the code under
|
|
7430
|
+
test consumed — and that shipped. What is rejected is the mechanism, and the
|
|
7431
|
+
hole stays open: a tautological assertion is still green and still silent.
|
|
7432
|
+
Documenting a trap is not detecting it, and this section is where that
|
|
7433
|
+
difference is recorded rather than hidden.
|
|
7434
|
+
|
|
7435
|
+
*Two things this leaves standing, neither of them a mechanism.* Design §11
|
|
7436
|
+
carries the property in the reader's form, so an adopter meets it where the
|
|
7437
|
+
method is taught. And the detector stays in
|
|
7438
|
+
`tests/intent-rule-candidates.spec.ts` with its verdict table recomputed on
|
|
7439
|
+
every run: it is the evidence that killed this, and deleting it would leave the
|
|
7440
|
+
rejection resting on a figure nothing checks — the exact failure this file has
|
|
7441
|
+
already had once, when the reach quoted for `compound-requirement` went stale in
|
|
7442
|
+
it.
|
|
7443
|
+
|
|
7444
|
+
*Reopening needs the one thing neither corpus supplied:* a param whose value
|
|
7445
|
+
moved, whose assertion echoed it, and where the move was **harmful**. Precision
|
|
7446
|
+
on the shape is a tuning question. Precision on the harm is the whole question,
|
|
7447
|
+
and telling a rendering fixture from a mine-density band is a judgement about
|
|
7448
|
+
whether the value *means* anything — which is the ground truth §0 refuses to
|
|
7449
|
+
compute for somebody else's registry. Nothing short of running each scenario
|
|
7450
|
+
twice under different values separates an independent pin from an echo, and that
|
|
7451
|
+
is mutation testing, a much larger thing to own.
|
|
7452
|
+
|
|
7453
|
+
The claim and the measurements, kept as they were taken:
|
|
7454
|
+
|
|
7455
|
+
**An assertion that recomputes its expectation from the param the code under
|
|
7456
|
+
test just read is a tautology, and nothing says so.** Reported by
|
|
7457
|
+
`mine-capablanca`, which probed it by mutating the registry and running the
|
|
7458
|
+
full gate on each mutation:
|
|
7459
|
+
|
|
7460
|
+
| mutation | who reads it | `check` | `verify` |
|
|
7461
|
+
|---|---|---|---|
|
|
7462
|
+
| `GEN-1.maxCandidatesPerPiece` 64 → 4 | scenario only | ok | **red** |
|
|
7463
|
+
| `BOARD-1.boardSize` 10 → 12 | app + fixtures | ok | **red** |
|
|
7464
|
+
| `BOARD-2.maxMines` 12 → **40** | app + assertion | ok | **green** |
|
|
7465
|
+
| `BOARD-2.minMines` 9 → **1** | app + assertion | ok | **green** |
|
|
7466
|
+
|
|
7467
|
+
The mine-density band — the one number deciding whether that game is playable —
|
|
7468
|
+
moved by 3.3× with the whole suite green, `check` silent, and `possible-drift`
|
|
7469
|
+
correctly not firing, because a covering scenario *does* read the param.
|
|
7470
|
+
**Read the third and fourth rows against the second, because that is the
|
|
7471
|
+
finding.** The reporter's diagnosis was "app and spec both read it", but row 2
|
|
7472
|
+
is app-and-spec and goes red. What distinguishes the green rows is narrower and
|
|
7473
|
+
more useful: the assertion **derives its expectation from the same param the
|
|
7474
|
+
code under test used**, so both sides move together and the comparison has no
|
|
7475
|
+
independent term. Row 2's expectation lives in fixtures, which do not move when
|
|
7476
|
+
the param does, and the mechanism works. So the failing condition is a property
|
|
7477
|
+
of the assertion, not of who reads the value — which matters, because it means
|
|
7478
|
+
the answer is a rule about how to write the assertion, and *not* the detector
|
|
7479
|
+
the reporter proposed.
|
|
7480
|
+
**What this falsifies is written in this file.** The rejection of a committed
|
|
7481
|
+
params snapshot, below, closes on *"the mechanism that guards a param is a
|
|
7482
|
+
scenario reading it, not a file recording it"* — a sufficiency claim, and rows
|
|
7483
|
+
3 and 4 are counterexamples to it. A scenario reading the param is necessary
|
|
7484
|
+
and not sufficient. That entry stays rejected on its own argument, which was
|
|
7485
|
+
never about detection power, but its fallback clause is now known incomplete
|
|
7486
|
+
and is annotated accordingly. The README's §"killer move" has the same shape:
|
|
7487
|
+
it says noticing a param *moved* is `verify`'s job "only when a scenario asserts
|
|
7488
|
+
on the value it read from `params`" — literally a necessary condition, and read
|
|
7489
|
+
by an adopter as a sufficient one, with the very next line (*"Read the param
|
|
7490
|
+
inside the assertion, not beside it"*) being the instruction that produces the
|
|
7491
|
+
tautology.
|
|
7492
|
+
**The framing that probably resolves it: this is `never-red` wearing different
|
|
7493
|
+
clothes.** A tautological assertion is a scenario that cannot fail, which is
|
|
7494
|
+
the exact defect `archive`'s `never-red` gate exists to catch — and it did
|
|
7495
|
+
catch two of them in this very repo (`CHECK-2`, `FOG-3`, both confirmed
|
|
7496
|
+
correct by the reporter). The reason it did not catch these is structural:
|
|
7497
|
+
**changing the value of an existing param goes through no gate at all.**
|
|
7498
|
+
`archive` gates a change; a param edit is not a change. That is a cleaner
|
|
7499
|
+
statement of the gap than "params can be tautological".
|
|
7500
|
+
*Re-read against `evidence-not-ordering`, which separated that gate's
|
|
7501
|
+
obligation from the ordering it used to be described by.* Nothing here moves:
|
|
7502
|
+
the blocker was never that the observation had to be taken first, it is that a
|
|
7503
|
+
param edit produces no change folder for any observation to be recorded
|
|
7504
|
+
against. If anything the framing gets sharper — what `never-red` demands is
|
|
7505
|
+
evidence that a scenario can fail, which is precisely what is missing for a
|
|
7506
|
+
tautological assertion, and the reason it goes undemanded is still the absence
|
|
7507
|
+
of a gate rather than the absence of a route to the evidence.
|
|
7508
|
+
**That observation is now its own entry at the foot of this section**, split
|
|
7509
|
+
out and measured on 2026-08-18, because it asks what happens when a value
|
|
7510
|
+
moves rather than whether an assertion is a real comparison, and neither
|
|
7511
|
+
answer settles the other. What stays here is the detector.
|
|
7512
|
+
*What blocks the decision — three things, all of them real.* **(1)** Every
|
|
7513
|
+
mutation above is synthetic. Across 8 commits touching `*.reqs.ts` in that
|
|
7514
|
+
repo, **no param value was ever modified** — every `params:` line in every diff
|
|
7515
|
+
is an addition. So the hole is proven as a logical property and its frequency is
|
|
7516
|
+
entirely unmeasured, from one greenfield repo. **(2)** The reporter's proposed
|
|
7517
|
+
mechanism — a WARNING when one param id is read from both a spec and a non-spec
|
|
7518
|
+
file — is the wrong shape twice over: it fires on row 2, which is correct usage
|
|
7519
|
+
and goes red, and it would fire on the arrangement the README actively
|
|
7520
|
+
recommends, which is §9's "usually wrong about staleness" failure mode again.
|
|
7521
|
+
It also needs a static sweep of the whole project for `reqs['X'].params.y`,
|
|
7522
|
+
far past the two registry readers' current surface. **(3)** No mechanism is yet
|
|
7523
|
+
known that separates "the assertion pins this independently" from "the assertion
|
|
7524
|
+
echoes it" without executing the scenario twice under different values — which
|
|
7525
|
+
is mutation testing, a much larger thing to own. *One has since been built and
|
|
7526
|
+
scored, and it does not lift this blocker.* The syntactic candidate — flag a
|
|
7527
|
+
scenario whose expectation is derived from the param the code under test
|
|
7528
|
+
consumed, decided inside one body without executing anything — flags 9 params
|
|
7529
|
+
here, of which **4 are real echoes and 5 are pinned by something outside the
|
|
7530
|
+
body it cannot see**. Under half precision, and the failure is the one
|
|
7531
|
+
predicted above rather than a tuning problem: what pins a value is often a
|
|
7532
|
+
fixture or a second derivation in another file, so a single-body detector is
|
|
7533
|
+
looking in the wrong place by construction. The newest false positive is the
|
|
7534
|
+
clearest instance — `ATX-79.subpath` is compared against `package.json`, which
|
|
7535
|
+
is not the code under test and does not read the registry, so the independent
|
|
7536
|
+
term is in another file entirely. Precision has fallen on every addition
|
|
7537
|
+
measured so far, which is the direction that matters. The score is quoted from the
|
|
7538
|
+
suite's snapshot and re-read with the judgement-table entry above, which is
|
|
7539
|
+
where the same measurement is used for a different decision.
|
|
7540
|
+
**The guidance half has shipped; this entry is now only about the detector.**
|
|
7541
|
+
"Read the param inside the assertion" was insufficient and half-wrong as
|
|
7542
|
+
written, and `[0.5.0]` replaces it everywhere it appeared — README, the
|
|
7543
|
+
`init` skill, `possible-drift`, and design §11 — with what the evidence
|
|
7544
|
+
supports: **the expectation must not be a function of the param the code under
|
|
7545
|
+
test consumed**, pinned in a fixture, a literal, or a second independently
|
|
7546
|
+
derived value. Row 2 is the worked example, and the reporter's own
|
|
7547
|
+
`CHECK-2`/`FOG-3` fixes are the same move. It was held back once because
|
|
7548
|
+
rewording the central claim of the tool is not a line appended; that is what
|
|
7549
|
+
was then done, deliberately and in one pass.
|
|
7550
|
+
**What stays undecided is the mechanism**, and all three blockers above are
|
|
7551
|
+
untouched by the rewording: the frequency is still unmeasured from one
|
|
7552
|
+
greenfield repo, the reporter's proposed detector is still the wrong shape
|
|
7553
|
+
twice over, and nothing short of mutation testing separates an independent pin
|
|
7554
|
+
from an echo. What the guidance does not do is close the hole — a tautological
|
|
7555
|
+
assertion is still green, still silent, and still reachable — so this stays
|
|
7556
|
+
here rather than moving to `Considered and rejected`: documenting a trap is
|
|
7557
|
+
not the same as deciding not to detect it.
|
|
7558
|
+
**Re-measured after `[0.7.1]` widened `params`, on this registry rather than
|
|
7559
|
+
the reporter's — the first time this experiment has been run here.** Twelve
|
|
7560
|
+
mutations, each followed by the whole gate. Two results matter, and they point
|
|
7561
|
+
opposite ways.
|
|
7562
|
+
*The shape is now reachable here, and the widening is why.* `self/render.spec.ts`
|
|
7563
|
+
asserts `expect(md).toContain(JSON.stringify(kinds))` over the same
|
|
7564
|
+
`payloadKinds` it feeds to the renderer — the expectation is a function of the
|
|
7565
|
+
value under test, verbatim row 3, and it could not have been written before
|
|
7566
|
+
the widening because the param could not be an object. The mechanism is
|
|
7567
|
+
sharper than a frequency count and is the strongest thing this entry has
|
|
7568
|
+
gained: **the prescribed remedy costs in proportion to the value's
|
|
7569
|
+
complexity.** Same repo, adjacent files, same value shape — `ATX-7` pins two
|
|
7570
|
+
numbers with literals and goes red; `ATX-63` would have had to hand-write a
|
|
7571
|
+
rendered JSON block, so it echoed. Depth raises the defect rate and the cost
|
|
7572
|
+
of avoiding it along one axis.
|
|
7573
|
+
*And its cost here is nil, which is the honest other half.* Both composite
|
|
7574
|
+
params are fixtures for the renderer and the validator; the numbers stand for
|
|
7575
|
+
nothing, so the green is arguably correct. Blocker **(1)** is therefore only
|
|
7576
|
+
half retired — the shape is reproduced in a second, mature repo, and the count
|
|
7577
|
+
of *harmful* instances is still zero. Blocker **(2)** is reinforced rather than
|
|
7578
|
+
weakened: all four green mutations were on params `src/` never reads, so the
|
|
7579
|
+
reporter's "read from both a spec and a non-spec file" detector would have
|
|
7580
|
+
flagged none of them. Blocker **(3)** is untouched.
|
|
7581
|
+
*The frequency is no longer unmeasured, and it points away from building
|
|
7582
|
+
anything.* Blocker **(1)** rested on 8 commits of one greenfield repo. Parsed
|
|
7583
|
+
at every one of the **38** revisions that have touched this registry — all 38
|
|
7584
|
+
readable by the current static reader, so nothing was skipped — the history is
|
|
7585
|
+
**57 param keys added, 0 removed, and exactly 1 value changed**: `ATX-16.staticCommands`
|
|
7586
|
+
gaining `status`. That single edit is a roster *widening*, the safe direction,
|
|
7587
|
+
and it is on the one list that was already pinned, so it could not have been
|
|
7588
|
+
made without updating the pin. Two independent codebases, one greenfield and
|
|
7589
|
+
one at 64 requirements, and the event this whole entry is about has happened
|
|
7590
|
+
zero times in either. What that argues is not that the hole is unreal — it is
|
|
7591
|
+
reachable, demonstrated above — but that it is **rare and silent**, which is
|
|
7592
|
+
the profile `divergent-param` was rejected on rather than the profile that
|
|
7593
|
+
justifies an engine rule.
|
|
7594
|
+
*The same number says the split-out fix below has also never been triggered.*
|
|
7595
|
+
0 removals ever means no domain has ever been shortened here. Those pins close
|
|
7596
|
+
a live *exposure*, not a live *loss*, and the case for them is that they cost
|
|
7597
|
+
one line each against a failure with no symptom — not that anything was
|
|
7598
|
+
currently broken.
|
|
7599
|
+
*Blocker **(3)** assumed mutation testing was the only separator. A cheaper
|
|
7600
|
+
candidate existed, has now been scored, and does not survive it.* The idea was
|
|
7601
|
+
a **syntactic** discriminator over one test body — flag an assertion whose
|
|
7602
|
+
expected value derives from a param that also reached the arrange/act part —
|
|
7603
|
+
which is exactly the difference between the two cases the widening produced:
|
|
7604
|
+
`renderMarkdown({params: {payloadKinds: kinds}})` … `toContain(JSON.stringify(kinds))`
|
|
7605
|
+
against `expect(kinds.json.maxBytes).toBe(1_048_576)`. Scored the way
|
|
7606
|
+
`divergent-param` should have been and was not: implemented in
|
|
7607
|
+
`tests/intent-rule-candidates.spec.ts`, run over the specs that attest this
|
|
7608
|
+
repo, and then **every param it fires on mutated with the whole gate run —
|
|
7609
|
+
fifteen of them, one gate run each. Four went green; eleven went red.**
|
|
7610
|
+
*Then the detector was found to be wrong, which is the more useful half.* Its
|
|
7611
|
+
first implementation counted an identifier in any position as a read of a
|
|
7612
|
+
local with that name, so `i.code` in `issues.map((i) => i.code)` looked like a
|
|
7613
|
+
use of a param named `code` — and `code` is this repo's commonest param name.
|
|
7614
|
+
Corrected to count reference positions only, it flags **8, not 15**, and the
|
|
7615
|
+
seven it drops are exactly seven the mutations had already shown to be pinned
|
|
7616
|
+
by `src/`. The honest score is therefore **4 of 8, not 4 of 15** — precision
|
|
7617
|
+
50%, not 27%. The conclusion does not move, because it never rested on the
|
|
7618
|
+
precision figure; what moves is how much of the original number was the
|
|
7619
|
+
candidate failing and how much was the measurement being wrong, and that is
|
|
7620
|
+
worth writing down rather than quietly restating.
|
|
7621
|
+
The remaining false positives are not near-misses. They are independent terms
|
|
7622
|
+
the detector cannot see: **the runtime is the term** — `ATX-14`'s
|
|
7623
|
+
`inheritedKey` is `'toString'`, and a mutated spelling is not a prototype key
|
|
7624
|
+
at all, so the behaviour under test changes; **`src/` is the term** —
|
|
7625
|
+
`ATX-51`'s `code` is a second copy of a string the engine emits as a literal;
|
|
7626
|
+
and **arithmetic is the term** — `threshold + 3` builds a derived expectation
|
|
7627
|
+
that mutation breaks.
|
|
7628
|
+
And the four true positives fail the second test, which is the one that
|
|
7629
|
+
decides it: **none is harmful.** `ATX-10` and `ATX-3` assert that a value was
|
|
7630
|
+
substituted or applied, where the identity of the number is not the
|
|
7631
|
+
obligation; `ATX-51`'s is a path prefix and `ATX-63`'s a rendering fixture.
|
|
7632
|
+
Telling those from a mine-density band that must not move is a judgement about
|
|
7633
|
+
whether the value *means* anything — the ground truth §0 refuses to compute
|
|
7634
|
+
for someone else's registry. **50% precision on the shape, 0% on the harm.**
|
|
7635
|
+
Two limits on that number, stated because a score with an unstated method is
|
|
7636
|
+
what this file exists to prevent. The mutation has **one direction per kind**
|
|
7637
|
+
— numbers +1, lists drop-last, strings suffixed — and direction can decide the
|
|
7638
|
+
verdict: `ATX-40.languages` is red at 2→3 and was green at 2→1 before it was
|
|
7639
|
+
pinned, so the true-positive count is a floor. And **recall is unmeasured**:
|
|
7640
|
+
the eight params mutated earlier that this detector does not flag all went
|
|
7641
|
+
red, which is eight correct negatives and not a recall figure.
|
|
7642
|
+
The detector and its verdict table are kept in the measurement suite, with the
|
|
7643
|
+
reach recomputed on every run and the table asserted to cover exactly what
|
|
7644
|
+
fires — because the reach figure quoted for `compound-requirement` went stale
|
|
7645
|
+
in this very file for want of that.
|
|
7646
|
+
*A scoping limit on all of the above.* This repo cannot settle blocker **(2)**.
|
|
7647
|
+
`src/` reads no param at all — every `reqs[…].params` in it is a comment or a
|
|
7648
|
+
doc string — so params here are test vocabulary, while in the reporter's repo
|
|
7649
|
+
they are application constants the app itself consumes. The reporter's
|
|
7650
|
+
detector keys on exactly that difference, so it is inert here and this corpus
|
|
7651
|
+
is evidence neither for nor against it.
|
|
7652
|
+
*What the re-measurement did find is a different defect, and it has been split
|
|
7653
|
+
out below* — the entry is kept whole here because the two were measured in one
|
|
7654
|
+
pass and separating them is the finding.
|
|
7655
|
+
|
|
7656
|
+
### Retiring a requirement
|
|
7657
|
+
|
|
7658
|
+
Filed 2026-08-03 on the cost of permanence, re-measured four times, and rejected
|
|
7659
|
+
2026-08-31 **by its own reopening condition**. The entry nominated the test —
|
|
7660
|
+
reopen if the cost per requirement moves — and the ratio has read 2.56 → 2.78
|
|
7661
|
+
→ 2.80 → 2.82 scenarios per requirement across twenty-five days and
|
|
7662
|
+
twenty-four requirements. Three consecutive readings within 0.05 of each other
|
|
7663
|
+
are not a curve about to move, and the condition has now failed to trigger
|
|
7664
|
+
twice. The cost argument is therefore not waiting for evidence; it was answered
|
|
7665
|
+
in the negative by the thing the entry itself chose to be judged on.
|
|
7666
|
+
|
|
7667
|
+
*The three meanings it ended by separating are why the rest closes rather than
|
|
7668
|
+
merely goes quiet.* **(1) A requirement that turned out wrong** was never a
|
|
7669
|
+
retirement — the record of being wrong belongs in this section, which is what
|
|
7670
|
+
this section is, and removing the id is a manual registry edit nobody has argued
|
|
7671
|
+
against. **(3) A requirement subsumed by a later one** is a rename, blocked
|
|
7672
|
+
downstream of `--apply`'s refusal of RENAMED (`splice.ts`: deleting an entry has
|
|
7673
|
+
no smaller span to fall back to). That is somebody else's blocker, and lifting it
|
|
7674
|
+
is a decision which is not this one. It was shared with the backlink-index entry
|
|
7675
|
+
until 2026-09-01, when that entry was rejected; this is now the only thing
|
|
7676
|
+
resting on the refusal. **(2) A requirement still true and no
|
|
7677
|
+
longer worth a permanently-run scenario** was the only one ever about cost, and
|
|
7678
|
+
it is the one the ratio answers.
|
|
7679
|
+
|
|
7680
|
+
*What (2) left behind was a defect rather than a mechanism, and it is fixed.*
|
|
7681
|
+
`uncovered-requirement` read "Add a scenario, or **mark it deferred**", and
|
|
7682
|
+
`deferred` appeared nowhere else in the project — not a field, not a flag, not
|
|
7683
|
+
a schema key — so the escape hatch this meaning needs had been named in shipped
|
|
7684
|
+
output, in the one place a reader looks when they hit the error, since before
|
|
7685
|
+
anybody asked the question. `[Unreleased]` rewords it, troubleshooting now says
|
|
7686
|
+
what the second exit actually is, and design §11 records the permanence as a
|
|
7687
|
+
property to design against rather than a gap awaiting a mechanism. **This entry
|
|
7688
|
+
is where the trace of the affordance having been intended now lives**, which is
|
|
7689
|
+
why the rewording waited for the rejection instead of going in as a stray fix:
|
|
7690
|
+
changing the message first would have deleted the only written evidence that
|
|
7691
|
+
anybody ever meant to build it.
|
|
7692
|
+
|
|
7693
|
+
*The permanence itself is not rejected and is not a defect.* Deleting an intent
|
|
7694
|
+
in order to save the scenario attesting it inverts the claim the whole tool
|
|
7695
|
+
exists to make. Reopening needs the case (2) names — a requirement whose intent
|
|
7696
|
+
is still true and whose scenario is genuinely not worth running — together with
|
|
7697
|
+
a mechanism that keeps the first while dropping the second. Nobody has proposed
|
|
7698
|
+
one, and the message that implied there already was one is the reason the
|
|
7699
|
+
question looked bigger than it is.
|
|
7700
|
+
|
|
7701
|
+
The measurements, kept as they were taken:
|
|
7702
|
+
|
|
7703
|
+
**Whether a requirement can ever be retired, and what it costs that none can.**
|
|
7704
|
+
The registry is at **85 requirements and 240 scenarios, 2.82 each**, and
|
|
7705
|
+
every one is a permanent obligation: `uncovered-requirement` is an ERROR, so a
|
|
7706
|
+
requirement without a scenario cannot exist, and nothing ever removes one.
|
|
7707
|
+
`delta.removed` exists and `applyDelta` applies it, but `--apply` refuses to
|
|
7708
|
+
write it back, deliberately and with the reason recorded — REMOVED "cannot say
|
|
7709
|
+
which comments belonged to the entry it deletes". So retiring one is a manual
|
|
7710
|
+
registry edit, which is fine as a mechanism and says nothing about whether it
|
|
7711
|
+
is ever the right move.
|
|
7712
|
+
*What the growth actually costs, measured on this session rather than
|
|
7713
|
+
estimated.* One small fix took: a requirement, its scenarios, a hand-judged
|
|
7714
|
+
row in the `OBLIGATIONS` table, two inline snapshots, a `[Unreleased]` entry,
|
|
7715
|
+
History rows in two languages, and a regenerated `SPEC.md`. That is
|
|
7716
|
+
proportionate for a spec framework that has to dogfood — the ceremony *is* the
|
|
7717
|
+
product being used on itself — and it is also a number that only goes up.
|
|
7718
|
+
*What blocks the decision, and it is not the mechanism.* It is that "retire"
|
|
7719
|
+
has at least three meanings this file has never had to separate: a requirement
|
|
7720
|
+
that turned out **wrong** (which should leave a record of being wrong, and
|
|
7721
|
+
`Considered and rejected` is where that already goes); one still **true** but
|
|
7722
|
+
no longer worth a permanently-run scenario; and one **subsumed** by a later
|
|
7723
|
+
requirement, where the scenario should move rather than go. Only the second is
|
|
7724
|
+
really about cost, and it is the one where deleting the intent to save the
|
|
7725
|
+
scenario inverts what the whole tool claims — that intent outlives its tests.
|
|
7726
|
+
Nothing here is urgent at 85. It is filed now because the moment to write the
|
|
7727
|
+
argument down is while the reasoning is available, and because a project whose
|
|
7728
|
+
requirement count only rises should have decided in advance what would make
|
|
7729
|
+
one leave.
|
|
7730
|
+
*The figures above were restated on 2026-08-08, and how they were wrong is
|
|
7731
|
+
itself the section's own warning happening.* As filed on 2026-08-03 this entry
|
|
7732
|
+
said "62 requirements and 160 scenarios, roughly 2.6 each" while closing on
|
|
7733
|
+
"the number is 61" — an inconsistency inside one entry, and the closing half
|
|
7734
|
+
was the right one. Re-measured at the entry's own commit with the current
|
|
7735
|
+
parser: **61 requirements, 156 scenarios**, so both opening figures were wrong
|
|
7736
|
+
on the day they were written, and only the ratio survived. Measured the same
|
|
7737
|
+
way today it is 69 and 192, and the method is the same one `verify self`
|
|
7738
|
+
prints — the control is that today's `self/` counted this way returns exactly
|
|
7739
|
+
what the runner does, which is what makes the 2026-08-03 number trustworthy. The
|
|
7740
|
+
cheap way — `grep -c 'scenario('` — answers 211 against today's true 192, and
|
|
7741
|
+
is what would have been reached for: comments and prose count, which is the
|
|
7742
|
+
`ATX-15` error again.
|
|
7743
|
+
**What moved in five days sharpens the argument rather than dating it.** +8
|
|
7744
|
+
requirements and +36 scenarios, concentrated in three sessions rather than
|
|
7745
|
+
accrued at a rate, so nothing here is a trend line. The part worth keeping is
|
|
7746
|
+
the ratio: 2.56 → 2.78. The cost of a requirement is not flat and it is not
|
|
7747
|
+
falling, which is the premise the whole entry rests on, and it is the first
|
|
7748
|
+
time this file has had two measurements of it to compare.
|
|
7749
|
+
*A third measurement, on 2026-08-18, and it points the other way from the
|
|
7750
|
+
second.* **79 requirements and 221 scenarios**, counted the way `verify self`
|
|
7751
|
+
prints them, which is the control the 2026-08-08 restatement established. The
|
|
7752
|
+
ratio runs 2.56 → 2.78 → **2.80**, so the step that carried the argument was
|
|
7753
|
+
the first one and the second is **+0.015**, which is flat. The premise this
|
|
7754
|
+
entry rests on now has one measurement for it and one against, and the honest
|
|
7755
|
+
statement is the narrower one: per-requirement cost has held at about 2.8
|
|
7756
|
+
scenarios across ten days and ten requirements. What is unchanged is the
|
|
7757
|
+
absolute total, which only rises — that half of the argument is untouched, and
|
|
7758
|
+
it was always the half about permanence rather than about acceleration. What is
|
|
7759
|
+
no longer supported is the acceleration. **Reopening on cost needs the ratio to
|
|
7760
|
+
move again**; reopening on the three meanings of "retire" needs none of these
|
|
7761
|
+
figures, and that is still the part nobody has separated.
|
|
7762
|
+
*A fourth measurement, on 2026-08-28, and the cost half is now closed rather
|
|
7763
|
+
than merely unsupported.* **85 requirements and 240 scenarios**, counted the
|
|
7764
|
+
way `verify self` prints them: the ratio runs 2.56 → 2.78 → 2.80 → **2.82**.
|
|
7765
|
+
Three consecutive readings within 0.05 of each other, across twenty-five days
|
|
7766
|
+
and twenty-four requirements, are not a curve that is about to move. The
|
|
7767
|
+
reopening condition this entry set itself has now failed to trigger twice, so
|
|
7768
|
+
the cost argument is not waiting for evidence — it has been answered, in the
|
|
7769
|
+
negative, by its own test. What survives untouched is the half that was never
|
|
7770
|
+
about the ratio: the absolute total only rises, and permanence is the claim.
|
|
7771
|
+
|
|
7772
|
+
**The three meanings, separated — which is what the entry has been asking for
|
|
7773
|
+
and needs none of the figures above.** They are not one question, and only one
|
|
7774
|
+
of the three is open.
|
|
7775
|
+
|
|
7776
|
+
**(1) A requirement that turned out wrong.** Not a retirement at all, and the
|
|
7777
|
+
mechanism already exists: the record of being wrong belongs in `Considered and
|
|
7778
|
+
rejected`, which is what that section is, and removing the id is a manual
|
|
7779
|
+
registry edit nobody has argued against. Nothing is missing here. Filing it
|
|
7780
|
+
under "retire" was what made the question look bigger than it is.
|
|
7781
|
+
|
|
7782
|
+
**(2) A requirement still true but no longer worth a permanently-run
|
|
7783
|
+
scenario.** The only one of the three that is about cost, and the only one with
|
|
7784
|
+
no mechanism — and the sharpest thing this separation turned up is that the
|
|
7785
|
+
tool has been *advertising* one. `uncovered-requirement` reads "Add a scenario,
|
|
7786
|
+
or **mark it deferred**", and `deferred` appears nowhere else in `src/`: not a
|
|
7787
|
+
field, not a flag, not a schema key. The troubleshooting section for that code
|
|
7788
|
+
quotes the message as its symptom and then explains only the first half,
|
|
7789
|
+
because there is no second half to explain. So the escape hatch this meaning
|
|
7790
|
+
needs has been named in shipped output, and in the one place a reader looks
|
|
7791
|
+
when they hit the error, since before anyone asked the question. That is a
|
|
7792
|
+
defect in its own right — the message is prose and free to reword — but it is
|
|
7793
|
+
recorded here rather than fixed there, because rewording it would delete the
|
|
7794
|
+
only written trace that this affordance was ever intended, which is the thing
|
|
7795
|
+
this entry exists to keep. *That held until the entry itself closed, and no
|
|
7796
|
+
longer applies:* the trace is the paragraph above, the rejection is what makes
|
|
7797
|
+
it durable, and `[Unreleased]` rewords the message. The condition on the
|
|
7798
|
+
rewording was never that it stay wrong — it was that somewhere else had to say
|
|
7799
|
+
the affordance had been meant first.
|
|
7800
|
+
|
|
7801
|
+
**(3) A requirement subsumed by a later one**, where the scenario should move
|
|
7802
|
+
rather than go. That is a rename, and `--apply` refuses RENAMED outright, with
|
|
7803
|
+
the reason recorded in `splice.ts` — deleting an entry has no smaller span to
|
|
7804
|
+
fall back to. So it is blocked downstream of a refusal nobody has proposed
|
|
7805
|
+
lifting, which is the *same* blocker the backlink-index entry above carried until
|
|
7806
|
+
that entry was rejected on 2026-09-01. Two open entries rested on that one
|
|
7807
|
+
refusal and now **one** does, which is worth knowing and cuts the other way from
|
|
7808
|
+
how it was first written: a decision with two dependents is easier to justify
|
|
7809
|
+
than a decision with one, so the case for ever lifting the refusal got weaker
|
|
7810
|
+
rather than the blocker getting closer to clearing. This entry is not the reason
|
|
7811
|
+
the refusal exists, and it is now the only thing waiting on it.
|
|
7812
|
+
|
|
7813
|
+
*So the entry narrows to one meaning.* (1) is answered, (3) is somebody else's
|
|
7814
|
+
blocker, and what is actually open is (2) — whether a requirement can stop
|
|
7815
|
+
owing a permanently-run scenario while keeping its intent, which is the one
|
|
7816
|
+
case where deleting the intent to save the scenario inverts what the whole tool
|
|
7817
|
+
claims. Nothing here says it should be built; what changes is that the question
|
|
7818
|
+
is now one question.
|
|
7819
|
+
|
|
7820
|
+
### Declared prefix ownership instead of inferred
|
|
7821
|
+
|
|
7822
|
+
Filed 2026-08-19 from a spec-kit reading, rejected 2026-08-31 on the two things
|
|
7823
|
+
that moved under it. *What rejected it: the proposal is smaller than it looked,
|
|
7824
|
+
weaker than it looked, and answers a question nobody here has asked.*
|
|
7825
|
+
`loadRegistry` already returns `prefixOwners`, `merge.ts` already uses it to
|
|
7826
|
+
place an ADDED id, and `apply-no-prefix-owner` already refuses a prefix nobody
|
|
7827
|
+
owns — so what was proposed is declaration replacing inference, not a new
|
|
7828
|
+
layer. And `/inspect` shipped in `[0.9.2]`, so the cross-repository half — the
|
|
7829
|
+
one §11 says no command can see across — is now about ten lines of an
|
|
7830
|
+
adopter's own code over two registries. Attest does not have to own it to make
|
|
7831
|
+
it possible.
|
|
7832
|
+
|
|
7833
|
+
The third is what decides it. This repository's five prefixes — `ATX` and the
|
|
7834
|
+
four fixture ones — are each declared by exactly one file, so the allocation
|
|
7835
|
+
question a declaration would answer has never arisen here. Building it now is
|
|
7836
|
+
deciding prefix semantics for everyone from a corpus in which those semantics
|
|
7837
|
+
are unexercised, which is the objection this project raises against every
|
|
7838
|
+
premature interface.
|
|
7839
|
+
|
|
7840
|
+
§11 keeps the property in place unchanged: one flat global id space, the
|
|
7841
|
+
prefix a convention rather than a scope, and `duplicate-prefix` able only to
|
|
7842
|
+
report a collision that already happened. **The cost still arrives exactly when
|
|
7843
|
+
adoption succeeds** — two teams sharing a registry have somebody allocating
|
|
7844
|
+
prefixes by hand — and that is the reopening condition. It is also the
|
|
7845
|
+
condition the two-party audit entry under consideration would satisfy, so the
|
|
7846
|
+
first adopter who shares a registry decides both.
|
|
7847
|
+
|
|
7848
|
+
The reading this was filed from, kept as it was written:
|
|
7849
|
+
|
|
7850
|
+
**Prefix ownership is inferred rather than declared, and the cost arrives only
|
|
7851
|
+
when adoption succeeds.** Design §11 records that the id grammar has no level
|
|
7852
|
+
above the requirement: the prefix is a convention, nothing allocates it, and
|
|
7853
|
+
`duplicate-prefix` can only report a collision that already happened.
|
|
7854
|
+
spec-kit's answer is a roadmap table — a stable id, an intent, a scope
|
|
7855
|
+
boundary, dependencies — authored above the specs it decomposes into. That
|
|
7856
|
+
comparison is stated here rather than referred to, because the entry that
|
|
7857
|
+
introduced the spec-kit reading has since been built and now sits under
|
|
7858
|
+
`[0.9.4]` as the `open` field: an entry whose evidence rests on a
|
|
7859
|
+
neighbour is one a promotion silently empties.
|
|
7860
|
+
*Two things have changed since that reading, and they point opposite ways.*
|
|
7861
|
+
The proposal is **smaller** than it looked: the mechanism partly exists.
|
|
7862
|
+
`loadRegistry` returns `prefixOwners` — first claim in sorted file order —
|
|
7863
|
+
`merge.ts` uses it to decide which file an ADDED id belongs in, and
|
|
7864
|
+
`apply-no-prefix-owner` already refuses a prefix nobody owns. So what is
|
|
7865
|
+
proposed is declaration replacing inference, not a new layer. And it is
|
|
7866
|
+
**weaker**: `/inspect` shipped in `[0.9.2]`, so the cross-repository half —
|
|
7867
|
+
the one §11 says no command can see across — is now something an adopter
|
|
7868
|
+
computes from two registries in about ten lines. Attest does not have to own
|
|
7869
|
+
it to make it possible.
|
|
7870
|
+
*What blocks the decision:* nobody has the problem. §11 states the cost
|
|
7871
|
+
arrives "exactly when adoption succeeds: two teams sharing a registry have
|
|
7872
|
+
someone owning prefix allocation by hand", and no such adoption has been
|
|
7873
|
+
reported. Building the declared form now would be deciding prefix semantics
|
|
7874
|
+
for everyone from one repository whose five prefixes — `ATX` and the four
|
|
7875
|
+
fixture ones — are each declared by exactly one file, so the allocation
|
|
7876
|
+
question this would answer has never been asked here.
|
|
7877
|
+
|
|
7878
|
+
### An id-keyed retrieval bundle over `archive/` and this file
|
|
7879
|
+
|
|
7880
|
+
Proposed 2026-08-19 inside the entry now titled *The decisions worth retrieving
|
|
7881
|
+
are in this file*: given an `ATX-n`, assemble its statement and params, the
|
|
7882
|
+
scenarios covering it, the first-run evidence for each, and the paths to the
|
|
7883
|
+
change that introduced it — the reasoning being the fifth part and, as filed,
|
|
7884
|
+
"the one with no index and no key". The entry declined to build it and specified
|
|
7885
|
+
a probe instead: assemble the bundle by hand for one requirement, run one real
|
|
7886
|
+
change against it, and record whether it was used, ignored, or used wrongly.
|
|
7887
|
+
**One session, and it decides between a command, a skill surface, and nothing.**
|
|
7888
|
+
|
|
7889
|
+
Run on 2026-08-28 on `ATX-79`, against the `examples/judgement-table/` change
|
|
7890
|
+
shipped in the same pass. **The answer is nothing.**
|
|
7891
|
+
|
|
7892
|
+
**Four of the five parts came back in one script**, as the entry predicted:
|
|
7893
|
+
statement, params and `outOfScope` from `inspectProject`; three scenarios with
|
|
7894
|
+
their evidence states and four archived change refs from `inspectRedEvidence`.
|
|
7895
|
+
No new surface was needed to assemble it. A command would therefore be a wrapper
|
|
7896
|
+
over two functions that already ship, which is the shallow-module case rather
|
|
7897
|
+
than a retrieval mechanism.
|
|
7898
|
+
|
|
7899
|
+
**The row that did the work is a plain field, and not the one this was about.**
|
|
7900
|
+
`outOfScope: ['judging the quality of what it reports', …]` settled the change's
|
|
7901
|
+
central design question — ship the enforcement, withhold the columns — and it
|
|
7902
|
+
has been a field of `inspectProject` since `[0.9.2]`. What the composition added
|
|
7903
|
+
was that it was *present at the moment of the decision*, which is a claim about
|
|
7904
|
+
assembly rather than about a missing store, and assembly is what a caller with
|
|
7905
|
+
two function calls already has.
|
|
7906
|
+
|
|
7907
|
+
**The evidence half is structurally empty for most ids here.** All three rows
|
|
7908
|
+
came back `unarchived`, which is correct — `ATX-79` was written straight into
|
|
7909
|
+
the registry — and which `ATX-80` defines as **not a finding**. So for any
|
|
7910
|
+
brownfield id that quarter of the bundle carries nothing, and brownfield is most
|
|
7911
|
+
of this registry. The bundle's information content varies enormously by which id
|
|
7912
|
+
is asked about, which nothing in the proposal anticipated.
|
|
7913
|
+
|
|
7914
|
+
**And the fifth part is not where the proposal placed it.** The reasoning *for a
|
|
7915
|
+
requirement* is keyed twice over: `rationale` is a field of `inspectProject`, so
|
|
7916
|
+
it was already part one, and design §10 carries the argument under a section
|
|
7917
|
+
number. What `CHANGELOG.md` contributed to this bundle was **two grep hits for
|
|
7918
|
+
`ATX-79`, neither of them about `ATX-79`** — one a parenthetical inside
|
|
7919
|
+
`[0.9.2]` about a different requirement's import closure, the other a false
|
|
7920
|
+
positive inside the tautology entry, where `ATX-79.subpath` appears as an
|
|
7921
|
+
example of a detector firing wrongly. Zero useful rows and two misleading ones.
|
|
7922
|
+
|
|
7923
|
+
So what this rejects is not only the command but the **key**. A design that was
|
|
7924
|
+
evaluated and killed has no `ATX-n`, so an id-keyed index over the decision
|
|
7925
|
+
store returns the entries that were not rejected — the opposite of the content
|
|
7926
|
+
the original entry correctly identified as the valuable part. That half is not
|
|
7927
|
+
rejected and stays open above, without a key and without a proposed mechanism.
|
|
7928
|
+
|
|
7929
|
+
*Labelled for what it is.* n=1, one requirement, one change, and the person who
|
|
7930
|
+
assembled the bundle is the person who made the change — so "was it used" is a
|
|
7931
|
+
self-report, and the honest reading is that one row was load-bearing and one
|
|
7932
|
+
quarter was empty by construction. That is a thin basis for building something
|
|
7933
|
+
and an adequate one for not building it, which is the asymmetry the probe was
|
|
7934
|
+
specified to exploit.
|
|
7935
|
+
|
|
7936
|
+
### A retrieval mechanism over this file's decision sections
|
|
7937
|
+
|
|
7938
|
+
Proposed inside *The decisions worth retrieving are in this file*, which stayed
|
|
7939
|
+
open on the grounds that **no key had been proposed, and that was the whole of
|
|
7940
|
+
the problem rather than a step before it**. The entry specified the inverse of
|
|
7941
|
+
the probe that killed the id-keyed bundle above: take an entry from this section
|
|
7942
|
+
that was re-proposed and caught here, and ask what somebody would have had to
|
|
7943
|
+
type to find it before proposing it again. One session, no build. It was run on
|
|
7944
|
+
2026-08-28 and it produced a key — which already exists, in this file, and is the
|
|
7945
|
+
reason nothing needs building.
|
|
7946
|
+
|
|
7947
|
+
*The store is 24 entries, not 8,300 lines.* That is the measurement the entry's
|
|
7948
|
+
premise was missing. `Considered and rejected` holds **24 `###` entries over
|
|
7949
|
+
1,086 lines**, and every plausible search term a re-proposer would reach for
|
|
7950
|
+
narrows to a handful: `obligations` → 2 entries, `judge` → 5, `compound` → 2,
|
|
7951
|
+
`countable` → 1. Grep over one section already retrieves. An index would be a
|
|
7952
|
+
second copy of a table of contents the headings are.
|
|
7953
|
+
|
|
7954
|
+
***And 23 of the 24 headings name the artifact or the mechanism*** — `zod/v4-mini`,
|
|
7955
|
+
`isSafeChangeName`, `typescript` as a peer dependency, a freshness gate on the
|
|
7956
|
+
file `init` writes, grouping `render`'s output by prefix. A proposer arrives
|
|
7957
|
+
holding the artifact they are about to touch, which is why the heading is the
|
|
7958
|
+
key: it is the one field written in the vocabulary the next person will have.
|
|
7959
|
+
|
|
7960
|
+
*The exception is the finding.* The single heading that names a **problem**
|
|
7961
|
+
rather than an artifact — *Nothing resists a bloated requirement, and nothing
|
|
7962
|
+
resists a duplicated one* — is the entry that was re-proposed and caught, as the
|
|
7963
|
+
judgement-schema entry's blocker (2). The second documented catch, the quantifier
|
|
7964
|
+
detector reused as `quantifier-unjudged`, is filed under *An engine rule for a
|
|
7965
|
+
statement whose scenarios do not span its quantifier*, which is problem-shaped
|
|
7966
|
+
too. The one artifact-shaped heading anybody has been recorded checking against —
|
|
7967
|
+
*A committed snapshot of the params* — was found by its re-proposer unaided, and
|
|
7968
|
+
they said so in the entry. Four events, so this is a correlation and not a rate;
|
|
7969
|
+
what makes it worth recording is that it points at a rule with no mechanism
|
|
7970
|
+
behind it.
|
|
7971
|
+
|
|
7972
|
+
*So what replaces the proposal is a sentence about headings, not a feature.* A
|
|
7973
|
+
rejection is filed under the name of the thing it declined to build. That is
|
|
7974
|
+
already true of 23 of 24 and needs enforcing by nobody, because it is what an
|
|
7975
|
+
author writing a heading naturally does when the thing has a name — the
|
|
7976
|
+
exception is the entry that declined **three** mechanisms at once and had no
|
|
7977
|
+
single artifact to name, which is the case to watch rather than a rule to state.
|
|
7978
|
+
|
|
7979
|
+
*What the probe also confirmed, in passing, is the claim the entry rested on.*
|
|
7980
|
+
**15 of the 24 rejected entries carry no `ATX-n` at all**; the nine that do
|
|
7981
|
+
mention one as evidence inside an argument about something else. A design that
|
|
7982
|
+
was evaluated and killed has no id because nothing was built to carry one, so an
|
|
7983
|
+
id-keyed index retrieves precisely the entries that were *not* rejected. That
|
|
7984
|
+
half of the entry was right and is now measured rather than reasoned.
|
|
7985
|
+
|
|
7986
|
+
*What is not solved, and it is next door.* `Under consideration` has no headings
|
|
7987
|
+
at all — its twelve entries are bullets whose first bolded sentence is the only
|
|
7988
|
+
handle — so the key found here exists for one of the three foot sections and not
|
|
7989
|
+
for the section most likely to be read before a proposal. Nothing was changed
|
|
7990
|
+
about that here: restructuring twelve open entries is a larger edit than the
|
|
7991
|
+
finding justifies, and the finding is only four events old.
|
|
7992
|
+
|
|
7993
|
+
*The counts above are the 2026-08-28 measurement and have since moved; recounted
|
|
7994
|
+
2026-08-31.* `Considered and rejected` holds **27 `###` entries**, not 24 —
|
|
7995
|
+
entries were decided into it in the meantime, and `init --check` left it,
|
|
7996
|
+
reopened — and `Under consideration` holds **six** bullets, not twelve. The
|
|
7997
|
+
heading ratio is deliberately not recounted: judging a heading artifact-shaped is
|
|
7998
|
+
a hand call, so re-running it would be a new measurement rather than a repair of
|
|
7999
|
+
this one, and the entry rests on the argument rather than on the ratio. What the
|
|
8000
|
+
recount does settle is the aside above about the section next door — at six
|
|
8001
|
+
bullets the case for giving it headings is weaker than it was at twelve, not
|
|
8002
|
+
stronger.
|
|
8003
|
+
|
|
8004
|
+
### A per-scenario axis recording which interface a scenario crosses
|
|
8005
|
+
|
|
8006
|
+
Prompted by an external write-up (`columns.chicken-house.net`, 2025-11-10, "AI-First
|
|
8007
|
+
Testing Workflow"), which ran one test case against a system's REST API and
|
|
8008
|
+
against its web UI and found the empty-basket check implemented in the UI and
|
|
8009
|
+
absent from the API. Neither run failed against its own expectation; the finding
|
|
8010
|
+
was that two runs of the *same* case disagreed. Attest already permits the
|
|
8011
|
+
arrangement — a `reqId` takes any number of attesting scenarios — but cannot
|
|
8012
|
+
tell three scenarios covering three edges of one requirement from three
|
|
8013
|
+
scenarios crossing three interfaces at the *same* edge. The second set can be
|
|
8014
|
+
asserted to agree; the first has nothing to compare. The proposal was to put
|
|
8015
|
+
that fact in the model, as a per-scenario axis that would have to be a literal
|
|
8016
|
+
in the AST, because `cover` executes nothing.
|
|
8017
|
+
|
|
8018
|
+
Filed 2026-08-22 with four blockers, and rejected on 2026-08-28. **The blocker
|
|
8019
|
+
that decides it needed no measurement, and the one that was measured came back
|
|
8020
|
+
at the floor.**
|
|
8021
|
+
|
|
8022
|
+
**There is no verdict to emit, and that is fatal on its own.** The engine cannot
|
|
8023
|
+
perform the comparison: agreement between two runs is an assertion, and
|
|
8024
|
+
assertions belong to the test. The most any axis could report is that a
|
|
8025
|
+
requirement has scenarios on more than one of them — information with no pass or
|
|
8026
|
+
fail attached, a new output *category* rather than a new check. So the field
|
|
8027
|
+
would be a thing every adopter has to learn about, including all of the ones
|
|
8028
|
+
with a single interface, in exchange for a report that decides nothing. This
|
|
8029
|
+
holds whatever the frequency turns out to be, which is why it is stated first.
|
|
8030
|
+
|
|
8031
|
+
**And the frequency, counted because the entry said it was cheap to count and it
|
|
8032
|
+
was.** Over `self/` through `inspectProject`: 84 requirements, all covered, **69
|
|
8033
|
+
carrying two or more scenarios**, of which 6 have scenarios in more than one
|
|
8034
|
+
spec file. Over the seventeen fixture projects: 15 ids, 6 multi-scenario, **0**
|
|
8035
|
+
cross-file. Cross-file is only a proxy — nothing records which interface a
|
|
8036
|
+
scenario crosses, which is the entry's own point — so all 6 were read by hand,
|
|
8037
|
+
and **exactly one is the shape**: `ATX-60`, whose statement says the `--json`
|
|
8038
|
+
`ok` and the human verdict printed beside it are decided by one predicate
|
|
8039
|
+
"never by a second spelling of that question", with `json.spec.ts` and
|
|
8040
|
+
`report.spec.ts` asserting the two surfaces separately.
|
|
8041
|
+
|
|
8042
|
+
*How the other five fail is the useful half.* `ATX-14` and `ATX-63` pair the
|
|
8043
|
+
validator with the renderer, which is two **different obligations** of one
|
|
8044
|
+
requirement — one reports the bad placeholder, the other omits it — so there is
|
|
8045
|
+
nothing to compare. `ATX-15` is five edges of one ordering rule. `ATX-80` is one
|
|
8046
|
+
interface run against a fixture and against a change the suite really archived.
|
|
8047
|
+
`ATX-25` is the near-miss worth naming: `pipeline.spec.ts` checks the producer
|
|
8048
|
+
and `json.spec.ts` the serialised report, which is one pipeline observed at two
|
|
8049
|
+
**layers** — downstream-coupled, so they cannot independently disagree, and
|
|
8050
|
+
independence is the property differential evidence is made of.
|
|
8051
|
+
|
|
8052
|
+
*The finding that carries more than the count.* This repository's own canonical
|
|
8053
|
+
instance of the pattern — `tests/static-registry.spec.ts`, the two registry
|
|
8054
|
+
readers required to agree, cited in `CLAUDE.md` as what makes that seam real —
|
|
8055
|
+
is bound to **no requirement id at all**. It is in the engine suite, outside
|
|
8056
|
+
`self/`'s registry, and the only `requirement(` in it is a string fixture. A
|
|
8057
|
+
repository that runs the pattern deliberately, and says so in its own design
|
|
8058
|
+
vocabulary, contributes zero rows to a count of it. The rate is 1 in 69, and the
|
|
8059
|
+
one is weaker than the pair that prompted the entry.
|
|
8060
|
+
|
|
8061
|
+
**What survives, and it is the part that costs nothing.** The deletion test the
|
|
8062
|
+
entry proposed is untried and is now the cheaper half by a wide margin: if the
|
|
8063
|
+
workflow document `init` writes (§9) simply says that two scenarios crossing one
|
|
8064
|
+
requirement at different interfaces should assert the same expectations, the
|
|
8065
|
+
practice may happen anyway — in which case the engine would have gained nothing
|
|
8066
|
+
and charged everyone a field for it. That sentence needs no decision
|
|
8067
|
+
infrastructure and is not what this rejects. The permanent property the
|
|
8068
|
+
rejection leaves in place — that Attest cannot distinguish covering N edges from
|
|
8069
|
+
crossing N interfaces, so a fully-covered requirement says nothing about how
|
|
8070
|
+
many surfaces were asked — is recorded in design §11 rather than only here.
|
|
8071
|
+
|
|
8072
|
+
**What this rejection does not claim.** One corpus, and the limit `[0.9.2]`
|
|
8073
|
+
states about this repository applies: the two-adapter arrangements it has are
|
|
8074
|
+
engine-internal, so the count measures Attest's habits rather than an adopter's.
|
|
8075
|
+
The prompting report is from a real system nobody here can walk. What is
|
|
8076
|
+
corpus-independent is the first argument — no verdict to emit — and that is what
|
|
8077
|
+
this is rejected on. A reporter whose registry has the shape at a rate that
|
|
8078
|
+
makes a **descriptive** output worth having would be re-opening a different
|
|
8079
|
+
proposal, not this one.
|
|
8080
|
+
|
|
6791
8081
|
### Rewriting the placeholder scan in `prose` to drop its tail slice
|
|
6792
8082
|
|
|
6793
8083
|
Priced on 2026-08-21, during a standing sweep for more of the ATX-59 shapes, and
|
|
@@ -7010,7 +8300,7 @@ releases behind a blocker stated as "the mapping is unavailable", which was
|
|
|
7010
8300
|
true and was about a mapping nothing needed; the evidence it carried was about
|
|
7011
8301
|
the right problem and the mechanism it proposed was not, and nothing rereads a
|
|
7012
8302
|
blocker that has already been written down. That is the same failure the
|
|
7013
|
-
|
|
8303
|
+
`How an entry is re-measured` records about a figure nobody re-checks:
|
|
7014
8304
|
the *shape* of the proposal goes stale as quietly as its numbers do.
|
|
7015
8305
|
*What is left of this entry is only the original question* — whether `check`
|
|
7016
8306
|
and `verify` should refuse a half-loaded registry outright, the way `cover`
|
|
@@ -7368,26 +8658,18 @@ matching between statements (token overlap, embeddings). That reintroduces the
|
|
|
7368
8658
|
fuzzy comparison design §0 exists to remove; real semantic duplication needs
|
|
7369
8659
|
judgement and belongs to human review at the propose stage.
|
|
7370
8660
|
|
|
7371
|
-
|
|
7372
|
-
|
|
7373
|
-
|
|
7374
|
-
|
|
7375
|
-
|
|
7376
|
-
|
|
7377
|
-
|
|
7378
|
-
|
|
7379
|
-
|
|
7380
|
-
|
|
7381
|
-
|
|
7382
|
-
|
|
7383
|
-
describes an old workflow to an agent that trusts it. The backstop is that every
|
|
7384
|
-
mistake it can cause is already a diagnostic with a fix hint — a registry
|
|
7385
|
-
written the old way is `registry-not-static` — so the agent corrects itself from
|
|
7386
|
-
the report, and `init` is idempotent, which makes re-running it the entire
|
|
7387
|
-
update mechanism. Recorded here rather than left in the 0.3.0 entry that shipped
|
|
7388
|
-
`init`, because the queue above depends on it: a gate is the first thing
|
|
7389
|
-
proposed each time a new agent target is considered, and the answer must not
|
|
7390
|
-
need re-deciding per target.
|
|
8661
|
+
**New evidence for "sees almost nothing", from `evidence-not-ordering`
|
|
8662
|
+
(2026-08-28), and it is about *why*.** `ATX-30`'s statement was reworded — same
|
|
8663
|
+
three obligations, one of them stated for the first time — and the detector
|
|
8664
|
+
moved it from `correct` to `missed` on both variants. Nothing about the
|
|
8665
|
+
requirement got less compound; what changed is that the second clause is now a
|
|
8666
|
+
participle (*"keeping a recorded failure … and replacing a recorded pass"*)
|
|
8667
|
+
rather than a second `MUST`. So the rule is not weakly detecting compoundness,
|
|
8668
|
+
it is detecting one **grammatical spelling** of it, and an author's ordinary
|
|
8669
|
+
choice between two ways of writing the same sentence decides the verdict. That
|
|
8670
|
+
is the "cleared by deleting the word that triggered it" failure arriving without
|
|
8671
|
+
anyone trying to clear it, which strengthens the rejection rather than
|
|
8672
|
+
qualifying it.
|
|
7391
8673
|
|
|
7392
8674
|
### A marked block in the adopter's `AGENTS.md` / `CLAUDE.md`
|
|
7393
8675
|
|
|
@@ -7518,9 +8800,13 @@ was written expecting the uncovered case and the demonstrated one has a covering
|
|
|
7518
8800
|
scenario. **What that changes here is nothing**: this entry was rejected on the
|
|
7519
8801
|
population a snapshot fires on — every legitimate param edit, fix a commit with
|
|
7520
8802
|
no decision in it — and that objection is untouched by the new evidence, which
|
|
7521
|
-
concerns detection the snapshot was never denied having. The live question
|
|
7522
|
-
to `Under consideration
|
|
7523
|
-
|
|
8803
|
+
concerns detection the snapshot was never denied having. The live question
|
|
8804
|
+
moved to `Under consideration`, because the mechanism then in question was a
|
|
8805
|
+
rule about assertions rather than a file beside the registry, and it has since
|
|
8806
|
+
been rejected in its own right — above in this section, on a frequency of about
|
|
8807
|
+
zero across two codebases and a detector scoring 0% on harm. So the hole both
|
|
8808
|
+
entries describe is now recorded as open with nothing watching it, which is a
|
|
8809
|
+
weaker position than either entry claimed on its own and is the honest one.
|
|
7524
8810
|
|
|
7525
8811
|
### Grouping `render`'s output by prefix
|
|
7526
8812
|
|
|
@@ -7712,11 +8998,89 @@ fallback to evaluation. The same answer applies here for the same reason.
|
|
|
7712
8998
|
|
|
7713
8999
|
Worth reopening if an adopter's registry reaches a size where prefix grouping
|
|
7714
9000
|
and `grep` genuinely stop working — but the first thing to try then is the
|
|
7715
|
-
declared prefix ownership
|
|
7716
|
-
|
|
9001
|
+
declared prefix ownership recorded above in this section — rejected on there
|
|
9002
|
+
being no adoption that needs it, which is the same condition this sentence
|
|
9003
|
+
supposes — which addresses, rather than a scorer, which ranks.
|
|
9004
|
+
|
|
9005
|
+
## How an entry is re-measured
|
|
9006
|
+
|
|
9007
|
+
What the three sections above have learned about themselves, kept apart from all
|
|
9008
|
+
three because it belongs to none of them. Every lesson below was learned on an
|
|
9009
|
+
entry that has since left the section it was written in — five built entries now
|
|
9010
|
+
recorded under `[0.8.0]`, the `check` `empty-spec` entry under `[0.9.0]`,
|
|
9011
|
+
`divergent-param` and the committed params snapshot down in `Considered and
|
|
9012
|
+
rejected`. That is the argument rather than a coincidence: the method outlives
|
|
9013
|
+
every entry it was learned from, and filed under `Planned` it is unreachable in
|
|
9014
|
+
the stretches when `Planned` is empty, which is the state it is in as this is
|
|
9015
|
+
written. It was there because that is where the first of it happened to be
|
|
9016
|
+
written, which is not a reason.
|
|
9017
|
+
|
|
9018
|
+
**An entry can sit in these sections for releases on the strength of a figure
|
|
9019
|
+
nobody has checked.** The discipline aims at keeping the *argument*, not at re-testing the
|
|
9020
|
+
evidence under it, so the first act of building one is re-measuring what it
|
|
9021
|
+
claims rather than implementing what it proposes. It has paid in two ways:
|
|
9022
|
+
once killing an entry outright, when the spans it rested on turned out not to be
|
|
9023
|
+
spans; and five times correcting an entry's own numbers or scope before a line
|
|
9024
|
+
of it was written — a call-site count that a regex had inflated, a cost filed as
|
|
9025
|
+
"a fixture per diagnostic" that the corpus put at one fixture, an injection whose
|
|
9026
|
+
payload turned out to need the target file's *own* quote character, which is the
|
|
9027
|
+
difference between escaping one quote and escaping the right one, a fix
|
|
9028
|
+
filed as a map over `issues` that the report's own shape moved to the
|
|
9029
|
+
serialiser, four fields and one nesting level further out, and a type-level
|
|
9030
|
+
refusal filed as breaking the evaluating loader, which turned out not to touch
|
|
9031
|
+
it and to break the one delta shape the entry was about instead. **What a release costs its adopters is not known on the day it is cut**
|
|
9032
|
+
— several entries arrived from one adoption report, about shapes the tool made
|
|
9033
|
+
someone work around without ever erroring. And **the fixes with no observable
|
|
9034
|
+
symptom are worth batching**: individually none justifies a commit, together they
|
|
9035
|
+
cost about an hour, and they are the group no gate could have found, since having
|
|
9036
|
+
no symptom is the definition of what a gate cannot see.
|
|
9037
|
+
|
|
9038
|
+
*The five entries `Planned` carried before it emptied* were built and are
|
|
9039
|
+
recorded under `[0.8.0]`. The second is where this
|
|
9040
|
+
discipline paid again: it went in on an entry whose headline type
|
|
9041
|
+
measurement — instantiations unchanged — was false on re-measure, and the
|
|
9042
|
+
re-measure is the only reason the release notes do not repeat it. The fourth is
|
|
9043
|
+
the other outcome the discipline has, and worth a line because the first three
|
|
9044
|
+
were all corrections: the `render --out` symlink measurement reproduced exactly
|
|
9045
|
+
as filed, down to the leaf being safe for the reason given. What re-measuring
|
|
9046
|
+
added was not a correction but a **surface the entry had not counted** —
|
|
9047
|
+
`--check` reads the same destination, so the same link makes a freshness gate
|
|
9048
|
+
date a document outside the project. Re-measuring an entry is therefore not only
|
|
9049
|
+
a test of the number it quotes; walking the code the claim is about is what
|
|
9050
|
+
shows the claim's own edges. The fifth is that lesson arriving where an entry
|
|
9051
|
+
had already named its own fix: the C1 measurement reproduced exactly, and the
|
|
9052
|
+
repair it proposed — a map over `issues` — was still the wrong half, because
|
|
9053
|
+
four more fields and a nested scenario name carry the same untrusted text and
|
|
9054
|
+
never pass the envelope. An entry that ends with a fix is the one least likely
|
|
9055
|
+
to have its fix re-examined, which is the case worth naming.
|
|
9056
|
+
|
|
9057
|
+
*That has now happened twice, so it is a pattern rather than an incident.* The
|
|
9058
|
+
extensionless-import entry likewise reproduced exactly on its reported half and
|
|
9059
|
+
was wrong about its own repair — it recorded that the writing half needed
|
|
9060
|
+
nothing, and the writing half was guarded by a condition that was correct only
|
|
9061
|
+
while the reading half stayed broken. Both times the error had the same shape: an
|
|
9062
|
+
entry reasons about the code path it can see failing, and the repair makes a
|
|
9063
|
+
second path reachable that nobody has looked at because nothing could reach it.
|
|
9064
|
+
So the re-measure that matters is not "does the reported symptom reproduce" —
|
|
9065
|
+
both did, exactly — but **what does the proposed fix make reachable for the first
|
|
9066
|
+
time**.
|
|
9067
|
+
|
|
9068
|
+
*The first time that question was asked in advance, it paid, and how it paid is
|
|
9069
|
+
worth a line because it is not what the two cases above would predict.* The
|
|
9070
|
+
`check` `empty-spec` entry — `[0.9.0]`, the third entry to reproduce exactly
|
|
9071
|
+
on its reported half — was not wrong about its own repair. Asking the question
|
|
9072
|
+
anyway found the defect one step to the side: the fix it proposed was correct,
|
|
9073
|
+
and the *obvious implementation of it* — make `check` agree with `verify` —
|
|
9074
|
+
would have copied a misdiagnosis `verify` had been emitting unnoticed, because
|
|
9075
|
+
the reference implementation an entry points at is not itself re-read. So the
|
|
9076
|
+
question generalises past the shape it was learned from. It is not only about a
|
|
9077
|
+
path the repair makes reachable; it is about **everything the repair would make
|
|
9078
|
+
`check` agree with**, and an entry that names the right fix can still be
|
|
7717
9079
|
|
|
7718
9080
|
## Releases
|
|
7719
9081
|
|
|
9082
|
+
[1.0.0]: https://gitlab.com/Pseudorca/attest/-/tags/v1.0.0
|
|
9083
|
+
[0.11.0]: https://gitlab.com/Pseudorca/attest/-/tags/v0.11.0
|
|
7720
9084
|
[0.10.0]: https://gitlab.com/Pseudorca/attest/-/tags/v0.10.0
|
|
7721
9085
|
[0.9.4]: https://gitlab.com/Pseudorca/attest/-/tags/v0.9.4
|
|
7722
9086
|
[0.9.3]: https://gitlab.com/Pseudorca/attest/-/tags/v0.9.3
|