@am_shork/attest 0.2.2 → 0.3.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 +593 -61
- package/README.md +121 -18
- package/dist/cli/index.js +91 -21
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/json.d.ts +52 -5
- package/dist/cli/json.d.ts.map +1 -1
- package/dist/cli/json.js +50 -2
- package/dist/cli/json.js.map +1 -1
- package/dist/cli/report.d.ts +11 -1
- package/dist/cli/report.d.ts.map +1 -1
- package/dist/cli/report.js +52 -0
- package/dist/cli/report.js.map +1 -1
- package/dist/core/apply.d.ts +8 -0
- package/dist/core/apply.d.ts.map +1 -1
- package/dist/core/apply.js +10 -0
- package/dist/core/apply.js.map +1 -1
- package/dist/core/gate.d.ts +27 -1
- package/dist/core/gate.d.ts.map +1 -1
- package/dist/core/gate.js +48 -1
- package/dist/core/gate.js.map +1 -1
- package/dist/core/loader.d.ts +8 -0
- package/dist/core/loader.d.ts.map +1 -1
- package/dist/core/loader.js +1 -0
- package/dist/core/loader.js.map +1 -1
- package/dist/core/locate.d.ts +4 -0
- package/dist/core/locate.d.ts.map +1 -1
- package/dist/core/locate.js +8 -3
- package/dist/core/locate.js.map +1 -1
- package/dist/core/paths.d.ts +15 -0
- package/dist/core/paths.d.ts.map +1 -0
- package/dist/core/paths.js +36 -0
- package/dist/core/paths.js.map +1 -0
- package/dist/core/pipeline.d.ts +63 -2
- package/dist/core/pipeline.d.ts.map +1 -1
- package/dist/core/pipeline.js +190 -34
- package/dist/core/pipeline.js.map +1 -1
- package/dist/core/red-record.d.ts +50 -0
- package/dist/core/red-record.d.ts.map +1 -0
- package/dist/core/red-record.js +148 -0
- package/dist/core/red-record.js.map +1 -0
- package/dist/core/render.d.ts +1 -1
- package/dist/core/render.d.ts.map +1 -1
- package/dist/core/render.js +16 -2
- package/dist/core/render.js.map +1 -1
- package/dist/core/runner.d.ts.map +1 -1
- package/dist/core/runner.js +16 -6
- package/dist/core/runner.js.map +1 -1
- package/dist/core/skill.d.ts +23 -0
- package/dist/core/skill.d.ts.map +1 -0
- package/dist/core/skill.js +276 -0
- package/dist/core/skill.js.map +1 -0
- package/dist/core/static-registry.d.ts +28 -0
- package/dist/core/static-registry.d.ts.map +1 -1
- package/dist/core/static-registry.js +86 -25
- package/dist/core/static-registry.js.map +1 -1
- package/dist/core/status.d.ts +39 -0
- package/dist/core/status.d.ts.map +1 -0
- package/dist/core/status.js +64 -0
- package/dist/core/status.js.map +1 -0
- package/dist/core/targets.d.ts +52 -0
- package/dist/core/targets.d.ts.map +1 -0
- package/dist/core/targets.js +141 -0
- package/dist/core/targets.js.map +1 -0
- package/dist/core/types.d.ts +13 -0
- package/dist/core/types.d.ts.map +1 -1
- package/dist/core/types.js.map +1 -1
- package/dist/core/validator.d.ts +11 -0
- package/dist/core/validator.d.ts.map +1 -1
- package/dist/core/validator.js +28 -12
- package/dist/core/validator.js.map +1 -1
- package/dist/core/write.d.ts +2 -0
- package/dist/core/write.d.ts.map +1 -0
- package/dist/core/write.js +21 -0
- package/dist/core/write.js.map +1 -0
- package/dist/runtime.d.ts +9 -1
- package/dist/runtime.d.ts.map +1 -1
- package/dist/runtime.js +10 -2
- package/dist/runtime.js.map +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -15,6 +15,441 @@ not API.
|
|
|
15
15
|
|
|
16
16
|
## [Unreleased]
|
|
17
17
|
|
|
18
|
+
## [0.3.0] - 2026-07-27
|
|
19
|
+
|
|
20
|
+
The workflow half now reaches the agent that will read it, the archive gate can
|
|
21
|
+
tell a test that ever discriminated from one that never could, and a change in
|
|
22
|
+
flight has a reportable state between "not started" and "done". Underneath all
|
|
23
|
+
three: every path Attest derives from a project root now has one spelling on
|
|
24
|
+
every platform, which is what makes them work on Windows at all.
|
|
25
|
+
|
|
26
|
+
### Added
|
|
27
|
+
|
|
28
|
+
- **The workflow reached one agent, and every other agent got the engine without
|
|
29
|
+
it.** `attest init` wrote `.claude/skills/attest/SKILL.md` and nothing else —
|
|
30
|
+
the gap `init` exists to close, still open for every tool but one. It now takes
|
|
31
|
+
`--target`, repeatable, and writes the same document for Claude Code (the
|
|
32
|
+
default), Cursor (`.cursor/rules/attest.mdc`) and GitHub Copilot
|
|
33
|
+
(`.github/instructions/attest.instructions.md`).
|
|
34
|
+
**Two rules decide what may be a target, and the first was already permanent.**
|
|
35
|
+
*Attest owns the path*: each file above exists because Attest exists, is
|
|
36
|
+
written whole, and reads nothing of the user's — which is why an `AGENTS.md` or
|
|
37
|
+
`CLAUDE.md` marked block stays rejected outright (see "Considered and
|
|
38
|
+
rejected"), and why the answer for an agent that reads only such a file is a
|
|
39
|
+
one-line pointer the user writes themselves. *The instructions load on demand*
|
|
40
|
+
is the second, and it was the open question when this was queued: a skill body
|
|
41
|
+
is fetched when its description matches, a Cursor rule when the agent asks for
|
|
42
|
+
it (`alwaysApply: false`, and deliberately no `globs`, which is the auto-attach
|
|
43
|
+
trigger and a different thing), a Copilot instructions file when the open path
|
|
44
|
+
matches its `applyTo`. A carrier that is unconditionally resident charges every
|
|
45
|
+
unrelated request in the repository for a workflow it will not use.
|
|
46
|
+
**One document, three frontmatters, and that split is a requirement (ATX-34).**
|
|
47
|
+
Each target carries the same body byte for byte; only the frontmatter differs,
|
|
48
|
+
because that is the part the tool parses rather than the part the agent
|
|
49
|
+
follows. A copy per tool would be two agents told different things about one
|
|
50
|
+
engine — the drift this framework exists to report, occurring where nothing
|
|
51
|
+
reports it, since no command reads these files back. The requirement is written
|
|
52
|
+
over *every* target rather than over a list of them, so adding one cannot
|
|
53
|
+
satisfy it by being left out. What it costs falls on the targets with no
|
|
54
|
+
description-triggered mode: Copilot's trigger is the path, and `applyTo` names
|
|
55
|
+
the registries, the specs and `changes/`, so the instructions arrive when the
|
|
56
|
+
agent is editing intent and not when it is editing an implementation to make a
|
|
57
|
+
scenario pass — which is exactly when "do not edit the scenario instead" would
|
|
58
|
+
be worth reading. Widening that glob to `**` buys the missing case at the price
|
|
59
|
+
rule 2 declines to pay.
|
|
60
|
+
**Naming no target writes what it wrote before; an unknown name writes nothing
|
|
61
|
+
at all (ATX-35).** Writing every target by default would scatter configuration
|
|
62
|
+
for editors nobody in the project uses. Refusing the whole set on an
|
|
63
|
+
unrecognised name is the ATX-13 argument under a different name: a name already
|
|
64
|
+
known to be unusable must not cause a single write, or a failing run leaves a
|
|
65
|
+
repository carrying instructions for some agents and no statement of which.
|
|
66
|
+
Two requirements rather than one, for the ATX-23/ATX-24 reason — an
|
|
67
|
+
implementation that writes every known target on every run carries one document
|
|
68
|
+
faithfully, and one that gets the requested set right can still fail on an
|
|
69
|
+
unknown name after writing half of it. `unknown-target` is a new `code` on the
|
|
70
|
+
existing `Issue` shape.
|
|
71
|
+
**Codex was evaluated and is not a target**, because the tool offers no path
|
|
72
|
+
Attest can own: `AGENTS.md`, or prompts under `~/.codex/prompts`, outside the
|
|
73
|
+
project root entirely. Recorded under "Considered and rejected", where it stays
|
|
74
|
+
findable once this release is history.
|
|
75
|
+
**It shipped without the evidence the queued candidate demanded.** That
|
|
76
|
+
candidate named one line in one adoption report naming a tool as its trigger,
|
|
77
|
+
and no such line arrived: the three targets are a judgement about which tools
|
|
78
|
+
adopters use, and should be read as one. What limits the cost of being wrong is
|
|
79
|
+
that the guess is now priced per row — the document, the writer and the CLI are
|
|
80
|
+
general over the table, so a target is a path, a frontmatter and a scenario —
|
|
81
|
+
and that the two rules above are what keep the table from growing into a
|
|
82
|
+
distribution tail. The measurement behind that phrase, kept because it is the
|
|
83
|
+
argument for where the next effort goes: OpenSpec 1.6.0 publishes 25,733 lines
|
|
84
|
+
of `dist/` against this package's 2,513 lines of `src/`, roughly four fifths of
|
|
85
|
+
it distribution rather than engine (29 per-tool adapters, 1,169 lines of
|
|
86
|
+
`init.js` and `update.js`, three shell-completion installers) — while its engine
|
|
87
|
+
is the weaker half on every axis this project was built to win: a `verify` that
|
|
88
|
+
asks the agent for "reasonable inference" where §8's gate takes a set
|
|
89
|
+
difference against the Vitest task tree, a requirement primary key that is the
|
|
90
|
+
Markdown header text, and a Markdown reader carrying a visible repair history
|
|
91
|
+
that an AST does not pay. The gap was never engine quality; it was that
|
|
92
|
+
`openspec init` is one command and this was a paste. Twenty-nine adapters
|
|
93
|
+
against three targets is what remains of that comparison, and widening it needs
|
|
94
|
+
a named tool with a reader blocked on it, not a count of tools that exist.
|
|
95
|
+
*Additive:* `attest init` with no flags writes the same bytes to the same path
|
|
96
|
+
as before. `--json` gains `outFiles` — every file written, since a run can now
|
|
97
|
+
write several — and keeps `outFile` whenever exactly one was, which the default
|
|
98
|
+
invocation still is, so `schemaVersion` stays `1`. The only new exit-1 path is
|
|
99
|
+
reached through a flag that did not exist before this release.
|
|
100
|
+
Two measurements moved. `divergent-param` collected a **seventh** member on
|
|
101
|
+
`code` (ATX-35), still with zero true positives. And `compound-requirement`'s
|
|
102
|
+
reach passed its blind spot for the first time: the suite asserted
|
|
103
|
+
`missed.length >= correct.length`, and ATX-35 falsified it by arriving, like
|
|
104
|
+
ATX-33, with both obligations declared under two keywords. `missed` lost no
|
|
105
|
+
member — the rule sees no further, and what moved is how carefully the corpus
|
|
106
|
+
was written. Rewriting that assertion to fit would have been the measurement
|
|
107
|
+
bending to the corpus, so it now asserts what actually blocks the rule (the
|
|
108
|
+
blind spot never empties), with the movement in the inline snapshot.
|
|
109
|
+
|
|
110
|
+
- **A change had one touchpoint, and it only ever answered "not yet".**
|
|
111
|
+
`attest archive <change>` decides whether a change is done; between "not
|
|
112
|
+
started" and "archivable" there was no reportable state at all, so asking what
|
|
113
|
+
remained meant reading the delta, cross-referencing the specs and inferring —
|
|
114
|
+
work an agent redoes on every turn and a person does badly. `attest status
|
|
115
|
+
<change>` reports each requirement the change ADDs as one of three states:
|
|
116
|
+
`no-scenario`, `unproven` (a scenario exists but has never been seen failing)
|
|
117
|
+
or `proven`. It runs nothing, reading the delta, the specs and
|
|
118
|
+
`changes/<name>/first-run.json`.
|
|
119
|
+
**Its blocker was that there was nothing to report, and mechanism 2 removed
|
|
120
|
+
it.** This was queued under "Under consideration" with exactly that objection:
|
|
121
|
+
Attest has no artifact graph and should not grow one, so the only progress
|
|
122
|
+
signal worth printing is per-id — which added requirements have a scenario, and
|
|
123
|
+
whether that scenario has been red — and the red half did not exist. Shipping
|
|
124
|
+
mechanism 2 in this same release made `first-run.json` precisely that per-id
|
|
125
|
+
state, which reduced the entry to a presentation question. What decided it is
|
|
126
|
+
that the fork the entry named is now cheap on one side: a `status` that runs the
|
|
127
|
+
suite is `verify` with a filter, and the one that does not is a small file read
|
|
128
|
+
plus a static plan.
|
|
129
|
+
**It is a projection of the gate, not a second gate.** Each state is named
|
|
130
|
+
after the issue `archive` will raise — `no-scenario` is
|
|
131
|
+
`uncovered-requirement`, `unproven` is `never-red` — and the decision runs
|
|
132
|
+
through the gate's own predicate, extracted as `hasRecordedRed`, for the reason
|
|
133
|
+
`declaredNotRunIssues` and `uncoveredIssues` were extracted before it:
|
|
134
|
+
agreement between two verdicts should be structural, not clerical. The plan and
|
|
135
|
+
the added-id set come from the same two functions `archive` uses, so a progress
|
|
136
|
+
report cannot be computed over a different spec set than the gate's.
|
|
137
|
+
**Progress never reaches `ok`, and that is a requirement rather than a
|
|
138
|
+
detail.** The cheapest implementation is to push the existing `uncovered-` and
|
|
139
|
+
`never-red` issues into `issues[]` — which satisfies the reporting obligation
|
|
140
|
+
completely while putting them in `summary.error`, where the shared `hasError`
|
|
141
|
+
verdict turns them into `ok: false` and exit 1. That is the normal state of
|
|
142
|
+
every change in flight, so a CI step or an agent loop wrapping `status` would
|
|
143
|
+
read "failing" for the change's whole life. Split across two self-requirements
|
|
144
|
+
(ATX-32, ATX-33) for the same reason ATX-23/ATX-24 were: the wrong
|
|
145
|
+
implementation passes the first and needs a scenario, not a clause, to catch.
|
|
146
|
+
The one thing that does make it fail is intent it could not read — a missing or
|
|
147
|
+
unreadable delta — because a command that answered ok over an empty table would
|
|
148
|
+
be the vacuous green ATX-18 and ATX-26 each closed elsewhere.
|
|
149
|
+
**What it cannot answer is `tests-red`**, and the human rendering closes by
|
|
150
|
+
saying so (`Not a verdict: run attest archive <name>`). A column of green ticks
|
|
151
|
+
here means the readable obligations are met, and a report that can be mistaken
|
|
152
|
+
for a verdict is worse than no report.
|
|
153
|
+
**It reads the delta from source, so `status` joins ATX-16's static commands.**
|
|
154
|
+
That requirement's `staticCommands` param grows to four and its statement now
|
|
155
|
+
says "the intent they report on, whether a registry or a change delta" — the
|
|
156
|
+
registry extractor was generalised over the authoring function's name rather
|
|
157
|
+
than copied, so `delta({ … })` goes through the same one walker that decides
|
|
158
|
+
whether a value is fixed by the source text. A delta that is not a literal is
|
|
159
|
+
`registry-not-static` and a file that exports no delta is `change-not-found`:
|
|
160
|
+
both are codes `archive` already emits, so no consumer learns new vocabulary.
|
|
161
|
+
`--eval` is the same named way back it is everywhere else. The exposure this
|
|
162
|
+
closes is worse than the registry's, not better — `changes/` holds a proposal
|
|
163
|
+
still under review, and a reporting command is the last thing that should
|
|
164
|
+
execute it.
|
|
165
|
+
The design document carries it in the four places that were describing the old
|
|
166
|
+
set rather than only in §9: §2 and §5.1 name the reading commands, §7 states
|
|
167
|
+
for the first time that a delta is a literal on the same terms as a registry —
|
|
168
|
+
an authoring rule that existed the moment `status` read one — and §8 says which
|
|
169
|
+
of its own four checks the projection covers, and which one only the suite can
|
|
170
|
+
answer. §11's "Attest's own 29 requirements" was the kind of live count that
|
|
171
|
+
goes stale in silence, and is now phrased so it cannot. The README carried the
|
|
172
|
+
same three-command list in two places and an "all three commands" that had
|
|
173
|
+
become four, both now corrected, and it states the delta rule where it states
|
|
174
|
+
the registry one.
|
|
175
|
+
*Additive only:* a new `command` value and the new optional `progress` /
|
|
176
|
+
`added` fields, so `schemaVersion` stays `1`. No existing command's output
|
|
177
|
+
changes. The skill `attest init` writes gains the command, which re-running
|
|
178
|
+
`init` adopts.
|
|
179
|
+
One measurement moved with this change, recorded because it is evidence about a
|
|
180
|
+
*rejected* rule: `divergent-param` collected a **sixth** member on `code`
|
|
181
|
+
(ATX-33 joining ATX-18, ATX-21, ATX-25, ATX-26 and ATX-31), still with zero
|
|
182
|
+
true positives, and `compound-requirement`'s reach reached parity with its
|
|
183
|
+
blind spot for the first time (5 correct, 5 missed) — not because the rule
|
|
184
|
+
improved, since `missed` lost no member, but because ATX-33 was written with
|
|
185
|
+
both of its obligations declared. A rule whose score rises when the corpus is
|
|
186
|
+
written more carefully is measuring the authors, which is the objection to
|
|
187
|
+
shipping it, now stated by the measurement rather than in prose.
|
|
188
|
+
|
|
189
|
+
- **The archive gate certified that a change's tests pass, never that they ever
|
|
190
|
+
discriminated.** §6 describes three anti-drift mechanisms; mechanism 2 —
|
|
191
|
+
red/green expectation tracking — was written as shipped behaviour and had
|
|
192
|
+
never been built (0.2.0 corrected the document and named the gap). The gate
|
|
193
|
+
required green at archive time and nothing else, so a scenario that asserts
|
|
194
|
+
nothing has been green since the commit that added it and cleared the gate
|
|
195
|
+
exactly like one that drove an implementation. It ships now, and the anti-drift
|
|
196
|
+
budget is three mechanisms rather than two.
|
|
197
|
+
What made it buildable is that **the observation already existed and was being
|
|
198
|
+
thrown away**. The workflow has stage 1 run `attest archive <name>` and *expect*
|
|
199
|
+
`tests-red` — the skill `attest init` writes says so, and then says "nothing in
|
|
200
|
+
the engine will catch that later. This paragraph is the mechanism." A paragraph
|
|
201
|
+
asking a reader to be diligent is the thing this framework exists to replace,
|
|
202
|
+
and the engine had seen every one of those reds. `archive` now records how each
|
|
203
|
+
of a change's scenarios ended on its first run, into
|
|
204
|
+
`changes/<name>/first-run.json`, and blocks with a `never-red` ERROR on any
|
|
205
|
+
requirement the delta ADDs whose scenarios have no recorded failing run.
|
|
206
|
+
**A committed generated file, which this project otherwise treats as a
|
|
207
|
+
liability** — the argument against `init --check`, the reason `render` takes no
|
|
208
|
+
formatting options. The difference is lifetime: `changes/<name>/` is temporary
|
|
209
|
+
by construction and goes away when the change is archived, so this file has
|
|
210
|
+
exactly the change's lifetime and never becomes a permanent asset that can
|
|
211
|
+
drift. It is committed rather than kept in a scratch directory because the gate
|
|
212
|
+
has to reach the same verdict on a CI runner's fresh checkout as on the
|
|
213
|
+
author's machine — and because it is evidence, which belongs in the diff a
|
|
214
|
+
reviewer reads.
|
|
215
|
+
**The record is monotonic toward failure, and both halves of that are
|
|
216
|
+
load-bearing.** A recorded failure is permanent: by the time a change is
|
|
217
|
+
archivable everything is green, so a record that kept the latest outcome would
|
|
218
|
+
say "green" about a scenario that spent the whole change red. A recorded *pass*
|
|
219
|
+
is deliberately not permanent — plain first-write-wins was the first design
|
|
220
|
+
here, and it makes the defect this mechanism exists to catch unrecoverable,
|
|
221
|
+
since the fix for a scenario that asserts nothing is to give it a real
|
|
222
|
+
assertion and watch it fail, an observation that could then never be recorded.
|
|
223
|
+
The only way out would have been deleting the evidence by hand, which would
|
|
224
|
+
equally delete a genuine red. A later failure replacing an earlier pass cannot
|
|
225
|
+
be abused, because producing one requires the scenario to actually fail.
|
|
226
|
+
**Scoped to ADDED requirements only, and §11 now records what that leaves.** A
|
|
227
|
+
requirement written straight into the base registry describes behaviour that
|
|
228
|
+
already exists — the brownfield adoption, and most of this repo — so there was
|
|
229
|
+
never a moment when its scenario should have failed; blocking those would make
|
|
230
|
+
the mechanism fire hardest on the honest case. The guarantee is therefore a
|
|
231
|
+
property of the `changes/` workflow, not of the registry as a whole.
|
|
232
|
+
*Behaviour change, for a project mid-change:* a change whose scenarios were
|
|
233
|
+
never observed failing flips from archivable to blocked, which is the point,
|
|
234
|
+
since nothing had shown its tests could fail. The remedy is one gate run before
|
|
235
|
+
implementing. `never-red` is a new `code` on the existing `Issue` shape, and
|
|
236
|
+
`--json` gains no field, so `schemaVersion` stays `1`. Two self-requirements
|
|
237
|
+
(ATX-30, ATX-31) rather than one: a recorder that writes a perfect history and
|
|
238
|
+
never blocks satisfies the first completely while leaving the gate exactly as
|
|
239
|
+
weak as it was, and a gate can equally block on an absent record without
|
|
240
|
+
recording anything.
|
|
241
|
+
The rejected `divergent-param` rule gained a **fifth** member on `code` and,
|
|
242
|
+
for the first time, a collision on a **second** param name (`file`, ATX-27's
|
|
243
|
+
skill path against ATX-30's record path) — see "Considered and rejected", where
|
|
244
|
+
that second name is what closes off "pick a less generic name" as a workaround.
|
|
245
|
+
|
|
246
|
+
- **`attest init` — the workflow half of the framework had no entry point.**
|
|
247
|
+
Everything the engine does has been reachable from the CLI since 0.1.0. The
|
|
248
|
+
workflow — agree on intent, write the delta, drive the scenarios red, implement
|
|
249
|
+
to green — was a document (`docs/*/using-attest.prompt.md`) the user had to
|
|
250
|
+
find and paste into their agent themselves. Everything downstream of that paste
|
|
251
|
+
worked; nothing led to it. `attest init` writes one file,
|
|
252
|
+
`.claude/skills/attest/SKILL.md`, carrying that workflow as a **skill** — its
|
|
253
|
+
description sits in the agent's context, so the agent loads it when the work
|
|
254
|
+
starts rather than waiting to be handed something. That is the difference that
|
|
255
|
+
closes the gap: a file the user must remember to invoke would only replace
|
|
256
|
+
"find the document" with "know what to type". `init` is idempotent — re-run it
|
|
257
|
+
after upgrading, which is the entire update mechanism.
|
|
258
|
+
|
|
259
|
+
**The freshness contract, which the queued entry said had to be designed rather
|
|
260
|
+
than assumed: there isn't one, and that is the design.** A committed
|
|
261
|
+
instruction file is a function of the installed version, so it does go stale on
|
|
262
|
+
upgrade — but staleness only *costs* anything if the tool also ships a gate to
|
|
263
|
+
turn red about it, and `init --check` is rejected on its own terms under
|
|
264
|
+
"Considered and rejected", together with the exposure that leaves and the
|
|
265
|
+
backstop that keeps it small. What belongs here is what the absence of a gate
|
|
266
|
+
bought: with none to serve there is no reason to keep the prose out of the file
|
|
267
|
+
either, so it lives in the skill, where an agent reads it with no extra call,
|
|
268
|
+
and a skill body is loaded on demand — a long document costs context only when
|
|
269
|
+
it is actually needed.
|
|
270
|
+
|
|
271
|
+
**`init` deliberately scaffolds nothing else:** no `requirements/*.reqs.ts`, no
|
|
272
|
+
`package.json` scripts, no `changes/` skeleton. A generated registry can only
|
|
273
|
+
be empty — which since 0.2.0 is an `empty-spec` ERROR handed to the user on day
|
|
274
|
+
one — or invented, and invented intent is worse than none in the one layer
|
|
275
|
+
whose value is that it is real. What belongs there is the agent's judgement,
|
|
276
|
+
with the project in front of it, which is what the skill describes.
|
|
277
|
+
**This is the one thing Attest does not dogfood, and saying otherwise would
|
|
278
|
+
have been the wrong kind of reassurance.** The skill exists for a project
|
|
279
|
+
*adopting* Attest — an agent meeting a registry it did not write, needing to be
|
|
280
|
+
told what a delta is and why its scenarios must be red. This repo is the
|
|
281
|
+
framework, not an adopter: its requirements describe behaviour that already
|
|
282
|
+
exists and go straight into the base registry, so it does not run the
|
|
283
|
+
`changes/` workflow the skill teaches, and a committed copy here would be a
|
|
284
|
+
file nothing reads. What guards the text instead is `tests/skill.spec.ts`,
|
|
285
|
+
which asserts the shape an agent depends on — that it parses as a skill, that
|
|
286
|
+
the description names `*.reqs.ts` so it loads at the right moment, and that the
|
|
287
|
+
body still teaches the codes a reader will actually hit. That is a weaker
|
|
288
|
+
guarantee than dogfooding and is named as such: nothing here proves the
|
|
289
|
+
workflow is followable, only that the file describing it is well-formed.
|
|
290
|
+
**One correction to the queued entry:** it predicted a `files` allowlist change
|
|
291
|
+
and therefore a `pnpm run test:consumer` obligation. There is none — the text
|
|
292
|
+
is a TypeScript module compiled into `dist/`, not a template file, so the
|
|
293
|
+
published surface is unchanged. `--json` gains `init` as a `command` value and
|
|
294
|
+
widens the existing `outFile` field to it; both are additive, so
|
|
295
|
+
`schemaVersion` stays `1`.
|
|
296
|
+
|
|
297
|
+
- **`scenario` accepts a timeout.** `scenario(name, fn, timeoutMs)`, passed
|
|
298
|
+
straight through to Vitest's `it`. `verify` runs the suite isolated (design
|
|
299
|
+
§5.4) — no `vitest.config.ts` unless `--vitest-config` names one — so every
|
|
300
|
+
scenario got Vitest's 5-second default and a scenario that drives a
|
|
301
|
+
subprocess, a container or a build had no way to say otherwise. Attest's own
|
|
302
|
+
suite is the demonstration: two `self/` scenarios each start four child Vitest
|
|
303
|
+
runs, and they fit inside 5 seconds on Linux and not on Windows. The
|
|
304
|
+
alternatives are both worse — raising the default for everyone hides a
|
|
305
|
+
genuinely hung test, and a config file sets one number for what is a property
|
|
306
|
+
of one scenario — the same shape of objection that rules a CLI flag out as
|
|
307
|
+
`divergent-param`'s escape hatch under "Considered and rejected", where a
|
|
308
|
+
per-invocation switch is asked to carry a permanent property of the source.
|
|
309
|
+
Additive: existing two-argument calls are unchanged. Deliberately **no
|
|
310
|
+
self-requirement**: the contract here is Vitest's
|
|
311
|
+
own signature and this is one argument forwarded to it, and the only scenario
|
|
312
|
+
that would truly discriminate — one that outlives the default — would spend
|
|
313
|
+
five seconds of every run proving a number Attest does not own. Its absence is
|
|
314
|
+
observable anyway, as the two `self/` scenarios above.
|
|
315
|
+
|
|
316
|
+
### Changed
|
|
317
|
+
|
|
318
|
+
- **BREAKING: `cover` reported a registry it could not read as a registry with
|
|
319
|
+
nothing in it — and nothing is fully covered.** It was the one reader that
|
|
320
|
+
discarded the issues from loading the registry (`runCheck` and `runRender`
|
|
321
|
+
both keep them), so a `registry-not-static` file, a renamed `requirements/`
|
|
322
|
+
folder, or a moved root left an empty registry behind, and an empty registry
|
|
323
|
+
has no uncovered requirement in it. The command answered `✓`, `ok: true`,
|
|
324
|
+
`0 requirements, 0 uncovered`, exit `0` — on precisely the repo whose intent
|
|
325
|
+
layer had stopped being readable, and in the command a pipeline is most likely
|
|
326
|
+
to run first, because it neither runs the suite nor executes project code.
|
|
327
|
+
`attest check` on the same project reported the ERROR correctly, which is what
|
|
328
|
+
made the gap survivable and also what made it invisible: the two commands
|
|
329
|
+
disagreed about whether the project was in a reportable state at all.
|
|
330
|
+
`cover` now carries the load issues, so the verdict follows `hasError` like
|
|
331
|
+
every other command, and it emits **no rows at all** when the registry failed
|
|
332
|
+
to load. The second half is deliberate and is the reason this is one change
|
|
333
|
+
rather than a one-line fix: reporting the error while still printing a table
|
|
334
|
+
built from whichever registry files did parse would let a partial read pass
|
|
335
|
+
for the whole. `render` already refuses to emit a document under the same
|
|
336
|
+
condition, for the same reason — a report computed from a half-loaded registry
|
|
337
|
+
is a lie, and a lie shaped like a pass is the worst form it can take.
|
|
338
|
+
*Breaking:* a project whose registry does not read flips from exit `0` to exit
|
|
339
|
+
`1` — which is the point, since it was never coverable. The human output for
|
|
340
|
+
that project changes from `(the registry contains no requirements)` to the
|
|
341
|
+
load error itself; every project whose registry does read is byte-identical.
|
|
342
|
+
`--json` gains no field (`issues` and `summary` already exist), so
|
|
343
|
+
`schemaVersion` stays `1`.
|
|
344
|
+
|
|
345
|
+
### Removed
|
|
346
|
+
|
|
347
|
+
- **`docs/en/using-attest.prompt.md` and `docs/zh/using-attest.prompt.md`.** The
|
|
348
|
+
workflow half of these is what `attest init` now writes into the project; the
|
|
349
|
+
adoption half is the README's Getting started, which npm publishes regardless
|
|
350
|
+
of the `files` allowlist and so reaches a reader who has installed nothing.
|
|
351
|
+
Deleted rather than left as a page pointing at either, because a stub is still
|
|
352
|
+
a file two languages have to keep in sync, and the delivery problem was never
|
|
353
|
+
the prompt's wording — it was that the prompt lived in the docs directory of a
|
|
354
|
+
repository the reader may never have cloned. Nothing in the package linked to
|
|
355
|
+
them. Documentation only: no package surface, and no `files` change (they were
|
|
356
|
+
never in the tarball).
|
|
357
|
+
|
|
358
|
+
### Fixed
|
|
359
|
+
|
|
360
|
+
- **A spec file below the project root never ran on Windows.** `verify` and
|
|
361
|
+
`archive` derive the child run's `include` globs from the plan's file paths
|
|
362
|
+
(0.2.0), and those paths came from `path.relative` — so on Windows they were
|
|
363
|
+
spelled `deep\er\x.spec.ts`. A backslash is an **escape character** in glob
|
|
364
|
+
syntax, so the pattern matched nothing and the file was dropped from the run.
|
|
365
|
+
The scenarios in it then surfaced as `declared-not-run`, which is at least
|
|
366
|
+
loud rather than a false green, but the practical effect was that only a flat
|
|
367
|
+
layout worked at all — and a flat layout is not what a real project has.
|
|
368
|
+
The reason it survived this long is a sampling accident: every fixture in this
|
|
369
|
+
repo keeps its specs at the project root, and so does `self/`, so the relative
|
|
370
|
+
paths had no separator in them. The single exception —
|
|
371
|
+
`fixtures/e2e-archive/changes/add-totp/specs/totp.spec.ts` — was the one
|
|
372
|
+
failing test, which is why this read as "`archive` is broken on Windows"
|
|
373
|
+
rather than as what it was.
|
|
374
|
+
Every path derived from the project root now has one spelling (`/`) on every
|
|
375
|
+
platform, normalised once where it is produced rather than where it is
|
|
376
|
+
consumed, because the same relative path is also what a report carries: a
|
|
377
|
+
diagnostic that differs by operating system makes two CI runs impossible to
|
|
378
|
+
diff. The conversion is conditional on the host separator, never
|
|
379
|
+
unconditional — a backslash is a legal filename character on POSIX, and
|
|
380
|
+
rewriting it there would rename a file that exists.
|
|
381
|
+
*For anyone parsing `--json` on Windows:* `issues[].file` changes from
|
|
382
|
+
`deep\er\x.spec.ts` to `deep/er/x.spec.ts`. No exit code changes except the
|
|
383
|
+
ones that were wrong; nothing previously valid is rejected; no field is
|
|
384
|
+
removed. `schemaVersion` stays `1`. New fixture `fixtures/nested-spec/`, whose
|
|
385
|
+
only spec is two directories down, is the end-to-end guard (ATX-28).
|
|
386
|
+
- **`render --check` was permanently red on a Windows checkout.** `render`
|
|
387
|
+
writes `\n`; Git hands the file back as `\r\n` wherever `core.autocrlf` is
|
|
388
|
+
enabled, which is the installer default on Windows. The freshness check
|
|
389
|
+
compared bytes, so a fresh clone reported `stale-spec-doc` against a registry
|
|
390
|
+
nobody had touched, and the fix hint named a command that regenerates a file
|
|
391
|
+
which is already correct. Any adopter committing a rendering hit this the
|
|
392
|
+
first time a second machine — or a CI runner — checked the repo out.
|
|
393
|
+
The check now compares the *document*: a difference of line endings alone is
|
|
394
|
+
not staleness. This is the same standard 0.1.6 set when it gave `render` no
|
|
395
|
+
formatting options at all — a gate that can be wrong about staleness is worse
|
|
396
|
+
than no gate — and it costs nothing, because no carriage return in that file
|
|
397
|
+
ever came from the registry. Content drift is still reported, CRLF or not
|
|
398
|
+
(ATX-29). This repo also gained a `.gitattributes` pinning the working tree to
|
|
399
|
+
LF, so a regeneration is not a whole-file diff on one platform and no diff on
|
|
400
|
+
the other.
|
|
401
|
+
- **Three of this repo's own tests could not pass on Windows, and one could not
|
|
402
|
+
fail.** `tests/locate.spec.ts` took basenames with `split('/')`, which returns
|
|
403
|
+
the whole path when the separator is a backslash.
|
|
404
|
+
`tests/docs-consistency.spec.ts` filtered source files with
|
|
405
|
+
`path.startsWith(`${root}src/`)`, splicing a forward slash onto a native root
|
|
406
|
+
— it matched nothing, so the check that every `§n` citation resolves was
|
|
407
|
+
passing on an empty set. `self/registry.spec.ts` compared a native path
|
|
408
|
+
against `ts.Diagnostic.file.fileName`, which TypeScript normalises to forward
|
|
409
|
+
slashes, so the type-level assertion measured zero diagnostics. Same class of
|
|
410
|
+
bug as the two above, in the tests rather than the engine.
|
|
411
|
+
`tests/loader.spec.ts` was separately fragile on every platform: it counted
|
|
412
|
+
`attest-loader-*` directories in the OS temp dir, making its verdict a
|
|
413
|
+
function of every other loader alive at that moment. It now asserts against
|
|
414
|
+
the loader's own `scratchDir`, and a third case pins that concurrent loaders
|
|
415
|
+
do not share one.
|
|
416
|
+
|
|
417
|
+
- **`cover --json` could answer `ok: false` with an empty `issues` array.** Three
|
|
418
|
+
places state one rule for machine consumers — `src/cli/json.ts`'s header, the
|
|
419
|
+
README, and 0.1.2's entry below — and all three say the same thing: branch on
|
|
420
|
+
`ok` and `issues[].code`, never on `message`. `cover` was the one command that
|
|
421
|
+
broke it. Its verdict was computed from the coverage rows
|
|
422
|
+
(`covered === rows.length`) while its envelope carried no issue at all, so a
|
|
423
|
+
consumer that followed the documented rule got a failing verdict, a summary of
|
|
424
|
+
three zeroes, and nothing saying what to fix; recovering the reason meant
|
|
425
|
+
knowing to read `coverage[]` instead — the out-of-band knowledge the contract
|
|
426
|
+
exists to make unnecessary. An uncovered requirement now reaches the report
|
|
427
|
+
twice, as a row and as an `uncovered-requirement` ERROR.
|
|
428
|
+
**The issue comes from the function `check` already uses.** `uncoveredIssues`
|
|
429
|
+
is extracted from `validateStructure` and called by both, so the two commands
|
|
430
|
+
cannot state one fact in two shapes — the same reason `declaredNotRunIssues`
|
|
431
|
+
was extracted in 0.1.7, and the same argument: agreement between two verdicts
|
|
432
|
+
should be structural, not clerical. `cover`'s `ok` now derives from the shared
|
|
433
|
+
`hasError` predicate like every other command, rather than from a second rule
|
|
434
|
+
that happened to agree with it.
|
|
435
|
+
*Behaviour change, for anyone already parsing `cover --json`:* a report on a
|
|
436
|
+
registry with uncovered requirements gains `issues[]` entries and a non-zero
|
|
437
|
+
`summary.error` where both were empty. `ok` and the exit code are unchanged for
|
|
438
|
+
every input — an uncovered requirement is the only issue this command produces,
|
|
439
|
+
so the new predicate returns the boolean the old one did. The human output is
|
|
440
|
+
also unchanged: the coverage table already renders each uncovered requirement
|
|
441
|
+
as a red `✗` row, and printing the same fact again underneath it would be noise
|
|
442
|
+
for the one reader who can see it. No shape change either — `issues` and
|
|
443
|
+
`summary` are existing envelope fields — so `schemaVersion` stays `1`.
|
|
444
|
+
The rejected `divergent-param` rule grew a **fourth** member on this change
|
|
445
|
+
(`code`, shared at that point by ATX-17, ATX-18, ATX-21 and ATX-25), which is
|
|
446
|
+
the measurement its entry under "Considered and rejected" predicted: a generic
|
|
447
|
+
param name collides more often as the registry grows, so the rule's
|
|
448
|
+
false-positive rate rises with adoption. Still zero true positives. ATX-26,
|
|
449
|
+
later in this same release, then pushed ATX-17 *out* of the set by naming the
|
|
450
|
+
same diagnostic — the sharper version of the same point, recorded in full
|
|
451
|
+
under "Considered and rejected" rather than restated here.
|
|
452
|
+
|
|
18
453
|
## [0.2.2] - 2026-07-26
|
|
19
454
|
|
|
20
455
|
A wall of identical diagnostics now says once that it is one cause, and the
|
|
@@ -361,66 +796,6 @@ that identifies itself as attest output must carry the current `version` and
|
|
|
361
796
|
`schemaVersion`. Neither can catch a section that describes something unbuilt —
|
|
362
797
|
that is what the inline marker is for.
|
|
363
798
|
|
|
364
|
-
## Considered and rejected
|
|
365
|
-
|
|
366
|
-
Decisions **not** to build something, kept where they can be found before the
|
|
367
|
-
same candidate is proposed again. Nothing here shipped, so nothing here belongs
|
|
368
|
-
to a release — this section is deliberately outside the version history and does
|
|
369
|
-
not move when one is cut.
|
|
370
|
-
|
|
371
|
-
- **Two gaps in the intent layer: nothing resists a bloated requirement, and
|
|
372
|
-
nothing resists a duplicated one.** The gaps themselves are described in design
|
|
373
|
-
§11, which is where a permanent property of the design belongs. What is kept
|
|
374
|
-
here is the list of mechanisms tried and what killed each, so the same three
|
|
375
|
-
are not proposed again without new evidence. All three were scored against
|
|
376
|
-
every registry this repo ships *before* any was written into the engine — the
|
|
377
|
-
order `divergent-param` was originally designed in the wrong way round. The
|
|
378
|
-
measurement is runnable in `tests/intent-rule-candidates.spec.ts` and fails
|
|
379
|
-
when a requirement is added without a hand-labelled obligation count, because
|
|
380
|
-
the reach figure once quoted here went stale exactly that way.
|
|
381
|
-
*`compound-requirement` (WARNING) — rejected.* More than one RFC-2119 keyword
|
|
382
|
-
in one `statement` is objectively more than one obligation. Measured: of the 7
|
|
383
|
-
requirements in the corpus carrying more than one, it flags 2 (`ATX-10`,
|
|
384
|
-
`ATX-21`) and misses `ATX-5`, `ATX-9`, `ATX-13`, `ATX-14`, `ATX-17` — every
|
|
385
|
-
one that packs clauses under a single keyword, which is the form the coverage
|
|
386
|
-
incentive actually rewards. A wider variant (keyword plus a coordinating
|
|
387
|
-
`and`/`while`/`;`) reaches 6 of 7 with one wrong hit, so reach is not the
|
|
388
|
-
deciding argument.
|
|
389
|
-
This is: both variants are silenced by deleting the word that triggered them,
|
|
390
|
-
and neither can tell that deletion from a real split. `SHALL do A and MUST do
|
|
391
|
-
B` clears the warning as `SHALL do A and do B` — same two obligations, one
|
|
392
|
-
fewer normative keyword. A rule whose cheapest fix degrades the artifact it
|
|
393
|
-
protects does not ship.
|
|
394
|
-
*`divergent-param` (WARNING) — rejected.* One param name declared with
|
|
395
|
-
different values in two requirements sharing an id prefix. Measured: it fires
|
|
396
|
-
twice on this repo and both firings are wrong — `idleTimeoutMin` (`15` in
|
|
397
|
-
`ATX-3`, `30` in `ATX-10`), unrelated illustrative fixtures, and `code`
|
|
398
|
-
(`registry-not-static` in `ATX-17`, `empty-spec` in `ATX-18`,
|
|
399
|
-
`rationale-placeholder` in `ATX-21`), diagnostics that could not possibly
|
|
400
|
-
agree. The second arrived on its own when `ATX-18` was added and grew a third
|
|
401
|
-
member on its own again when `ATX-21` was, which is the more damning half: a
|
|
402
|
-
generic param name collides more often as a registry grows, so the
|
|
403
|
-
false-positive rate rises with adoption — the opposite of what a shippable rule
|
|
404
|
-
does. Still zero true positives across 25 requirements. The escape hatch it was blocked on has since been designed
|
|
405
|
-
and does not rescue it: with no config file the only workable shape is an
|
|
406
|
-
additive `independentParams?: string[]` on the requirement (a CLI flag is
|
|
407
|
-
per-invocation for what is a permanent property of two requirements; changing
|
|
408
|
-
the `params` shape rejects every existing registry). Viable, but it is
|
|
409
|
-
permanent schema surface for a rule with no demonstrated true positive, and in
|
|
410
|
-
practice it would be written *after* the warning fires — a one-line silencer.
|
|
411
|
-
*Countable obligations (breaking) — rejected.* An `obligations: string[]` per
|
|
412
|
-
requirement, with coverage counted per obligation, removing the lumping
|
|
413
|
-
incentive at the source instead of detecting it afterwards. Rejected because
|
|
414
|
-
the **id is already that unit**: splitting into two ids costs two scenarios and
|
|
415
|
-
buys the same incentive with no new concept (`ATX-11`/`ATX-12` are exactly that
|
|
416
|
-
split). It would touch the parser, coverage, `render`, `SPEC.md`, delta apply
|
|
417
|
-
and the `--json` schema, and it relocates the incentive rather than removing
|
|
418
|
-
it — nothing can force the array to be complete.
|
|
419
|
-
*Not considered:* similarity matching between statements (token overlap,
|
|
420
|
-
embeddings). That reintroduces the fuzzy comparison design §0 exists to remove;
|
|
421
|
-
real semantic duplication needs judgement and belongs to human review at the
|
|
422
|
-
propose stage.
|
|
423
|
-
|
|
424
799
|
## [0.1.7] - 2026-07-26
|
|
425
800
|
|
|
426
801
|
A change name, a prototype key and a collation quirk each got to decide a
|
|
@@ -741,7 +1116,164 @@ Initial release.
|
|
|
741
1116
|
(MIT), whose four-stage engine and diff-first change model Attest's
|
|
742
1117
|
architecture is adapted from (re-implemented from scratch, no source copied).
|
|
743
1118
|
|
|
744
|
-
|
|
1119
|
+
## Considered and rejected
|
|
1120
|
+
|
|
1121
|
+
Decisions **not** to build something, kept where they can be found before the
|
|
1122
|
+
same candidate is proposed again. Nothing here shipped, so nothing here belongs
|
|
1123
|
+
to a release — this section is deliberately outside the version history and does
|
|
1124
|
+
not move when one is cut. It lives at the **foot** of the file for that reason:
|
|
1125
|
+
it sat between 0.2.0 and 0.1.7 for two releases, where standing still meant
|
|
1126
|
+
sinking one version deeper each time a release was cut above it, and a rejection
|
|
1127
|
+
filed under a version reads as belonging to it.
|
|
1128
|
+
|
|
1129
|
+
- **Two gaps in the intent layer: nothing resists a bloated requirement, and
|
|
1130
|
+
nothing resists a duplicated one.** The gaps themselves are described in design
|
|
1131
|
+
§11, which is where a permanent property of the design belongs. What is kept
|
|
1132
|
+
here is the list of mechanisms tried and what killed each, so the same three
|
|
1133
|
+
are not proposed again without new evidence. All three were scored against
|
|
1134
|
+
every registry this repo ships *before* any was written into the engine — the
|
|
1135
|
+
order `divergent-param` was originally designed in the wrong way round. The
|
|
1136
|
+
measurement is runnable in `tests/intent-rule-candidates.spec.ts` and fails
|
|
1137
|
+
when a requirement is added without a hand-labelled obligation count, because
|
|
1138
|
+
the reach figure once quoted here went stale exactly that way. The guard makes
|
|
1139
|
+
the *suite* re-count; it cannot make this file re-read, so every figure below
|
|
1140
|
+
is a quotation of that snapshot and is only ever as fresh as the last edit
|
|
1141
|
+
here — read the snapshot when the number is what matters.
|
|
1142
|
+
*`compound-requirement` (WARNING) — rejected.* More than one RFC-2119 keyword
|
|
1143
|
+
in one `statement` is objectively more than one obligation. Measured (figures
|
|
1144
|
+
re-read from the suite, which is the only place they are not prose): of the 8
|
|
1145
|
+
requirements in the corpus carrying more than one, it flags 3 (`ATX-10`,
|
|
1146
|
+
`ATX-21`, `ATX-26`) and misses `ATX-5`, `ATX-9`, `ATX-13`, `ATX-14`, `ATX-17`
|
|
1147
|
+
— every one that packs clauses under a single keyword, which is the form the
|
|
1148
|
+
coverage incentive actually rewards. A wider variant (keyword plus a
|
|
1149
|
+
coordinating `and`/`while`/`;`) reaches 7 of 8, at 3 wrong hits (`ATX-6`,
|
|
1150
|
+
`ATX-22`, `ATX-23`) rather than the 1 recorded when it was first scored — so
|
|
1151
|
+
reach is not the deciding argument, and the precision it trades away grows
|
|
1152
|
+
with the registry.
|
|
1153
|
+
This is: both variants are silenced by deleting the word that triggered them,
|
|
1154
|
+
and neither can tell that deletion from a real split. `SHALL do A and MUST do
|
|
1155
|
+
B` clears the warning as `SHALL do A and do B` — same two obligations, one
|
|
1156
|
+
fewer normative keyword. A rule whose cheapest fix degrades the artifact it
|
|
1157
|
+
protects does not ship.
|
|
1158
|
+
*`divergent-param` (WARNING) — rejected.* One param name declared with
|
|
1159
|
+
different values in two requirements sharing an id prefix. Measured: it fires
|
|
1160
|
+
twice on this repo and both firings are wrong — `idleTimeoutMin` (`15` in
|
|
1161
|
+
`ATX-3`, `30` in `ATX-10`), unrelated illustrative fixtures, and `code`
|
|
1162
|
+
(`registry-not-static` in `ATX-17`, `empty-spec` in `ATX-18`,
|
|
1163
|
+
`rationale-placeholder` in `ATX-21`), diagnostics that could not possibly
|
|
1164
|
+
agree. The second arrived on its own when `ATX-18` was added and grew a third
|
|
1165
|
+
member on its own again when `ATX-21` was, then `ATX-25`, then `ATX-26`, which
|
|
1166
|
+
is the more damning half: a generic param name collides more often as a
|
|
1167
|
+
registry grows, so the false-positive rate rises with adoption — the opposite
|
|
1168
|
+
of what a shippable rule does. `ATX-26` added the sharper version of the same
|
|
1169
|
+
point: it names the *same* diagnostic as `ATX-17`, so the two now agree by
|
|
1170
|
+
coincidence and `ATX-17` drops out of the set the rule reports — a rule that
|
|
1171
|
+
reads agreement as safety falls silent on a pair it had been flagging, for a
|
|
1172
|
+
reason that has nothing to do with either requirement.
|
|
1173
|
+
**A second param name now collides, and that is what closes the last defence
|
|
1174
|
+
of the rule.** `file` names a path Attest writes — the agent skill in `ATX-27`,
|
|
1175
|
+
a change's first-run record in `ATX-30` — two unrelated constants, a third
|
|
1176
|
+
wrong firing. Until it appeared, every false positive had been on the single
|
|
1177
|
+
name `code`, which left "pick a less generic param name" open as a repo-side
|
|
1178
|
+
workaround and therefore left the rule arguably salvageable. It is not the
|
|
1179
|
+
word: two requirements owning unrelated constants of the same *kind* is the
|
|
1180
|
+
structure, and any registry large enough to be worth linting has it.
|
|
1181
|
+
Still zero true positives, now across a 34-entry corpus (the `corpus` figure in
|
|
1182
|
+
the suite's snapshot, which counts a fixture id once per registry file that
|
|
1183
|
+
declares it).
|
|
1184
|
+
The escape hatch it was blocked on has since been designed
|
|
1185
|
+
and does not rescue it: with no config file the only workable shape is an
|
|
1186
|
+
additive `independentParams?: string[]` on the requirement (a CLI flag is
|
|
1187
|
+
per-invocation for what is a permanent property of two requirements; changing
|
|
1188
|
+
the `params` shape rejects every existing registry). Viable, but it is
|
|
1189
|
+
permanent schema surface for a rule with no demonstrated true positive, and in
|
|
1190
|
+
practice it would be written *after* the warning fires — a one-line silencer.
|
|
1191
|
+
*Countable obligations (breaking) — rejected.* An `obligations: string[]` per
|
|
1192
|
+
requirement, with coverage counted per obligation, removing the lumping
|
|
1193
|
+
incentive at the source instead of detecting it afterwards. Rejected because
|
|
1194
|
+
the **id is already that unit**: splitting into two ids costs two scenarios and
|
|
1195
|
+
buys the same incentive with no new concept (`ATX-11`/`ATX-12` are exactly that
|
|
1196
|
+
split). It would touch the parser, coverage, `render`, `SPEC.md`, delta apply
|
|
1197
|
+
and the `--json` schema, and it relocates the incentive rather than removing
|
|
1198
|
+
it — nothing can force the array to be complete.
|
|
1199
|
+
*Not considered:* similarity matching between statements (token overlap,
|
|
1200
|
+
embeddings). That reintroduces the fuzzy comparison design §0 exists to remove;
|
|
1201
|
+
real semantic duplication needs judgement and belongs to human review at the
|
|
1202
|
+
propose stage.
|
|
1203
|
+
|
|
1204
|
+
- **A freshness gate on the file `attest init` writes (`init --check`).**
|
|
1205
|
+
`render --check` is the obvious precedent and is the opposite case. A rendering
|
|
1206
|
+
is a function of the *user's registry*, so its staleness means the user moved
|
|
1207
|
+
intent without regenerating — a real signal, with the fix in their hands. The
|
|
1208
|
+
skill is a function of the *installed version*, so a gate on it turns red on a
|
|
1209
|
+
change the user did not make, and is cleared by a commit containing no
|
|
1210
|
+
decision. That is a chore wearing a diagnostic's clothes, and a gate that
|
|
1211
|
+
cannot be about anything the reader did is worse than no gate — the same
|
|
1212
|
+
standard 0.1.6 set when it gave `render` no formatting options at all.
|
|
1213
|
+
An earlier draft of `attest init` spent a second command, this gate and a
|
|
1214
|
+
second self-requirement on it. What that would have bought is immunity to one
|
|
1215
|
+
honest and small exposure: an old file describes an old workflow to an agent
|
|
1216
|
+
that trusts it. The backstop is that every mistake it can cause is already a
|
|
1217
|
+
diagnostic with a fix hint — a registry written the old way is
|
|
1218
|
+
`registry-not-static` — so the agent corrects itself from the report, and
|
|
1219
|
+
`init` is idempotent, which makes re-running it the entire update mechanism.
|
|
1220
|
+
Recorded here rather than left in the 0.3.0 entry that shipped `init`, because
|
|
1221
|
+
the queue above depends on it: a gate is the first thing proposed each time a
|
|
1222
|
+
new agent target is considered, and the answer must not need re-deciding per
|
|
1223
|
+
target.
|
|
1224
|
+
|
|
1225
|
+
- **A marked block in the adopter's `AGENTS.md` / `CLAUDE.md`.** The cross-tool
|
|
1226
|
+
convention Codex, Cursor and Copilot read, and the reason it looks like the
|
|
1227
|
+
obvious next target after the skill is that it reaches the most agents per unit
|
|
1228
|
+
of work. Rejected on three counts, none of which is about effort.
|
|
1229
|
+
**It is the only file `init` would write that Attest does not own.**
|
|
1230
|
+
`.claude/skills/attest/SKILL.md` exists because Attest exists; `AGENTS.md`
|
|
1231
|
+
exists because the *project* does, and what is in it is prose its maintainer
|
|
1232
|
+
wrote and curated. Writing a block into it means an idempotent
|
|
1233
|
+
read-modify-write on someone else's document — find the markers, preserve
|
|
1234
|
+
everything outside them, and then handle the markers being absent, duplicated,
|
|
1235
|
+
hand-edited, moved, the file missing, the file being `CLAUDE.md` instead, or
|
|
1236
|
+
both existing. That is a merge tool, and each of its failure modes is
|
|
1237
|
+
destructive on a file the user cannot regenerate.
|
|
1238
|
+
**It would remove the property that makes `init` safe to automate.**
|
|
1239
|
+
`skillContent()` takes no argument — the content is a pure function of nothing
|
|
1240
|
+
at all — so `init` is one atomic write and its idempotence holds *by
|
|
1241
|
+
construction*, which is what lets "re-run it after upgrading" be the whole
|
|
1242
|
+
update mechanism and be safe in a script. A marked block makes the output a
|
|
1243
|
+
function of the file already there, demoting idempotence from a property of the
|
|
1244
|
+
design to a behaviour that needs test coverage, on the one path where being
|
|
1245
|
+
wrong destroys input.
|
|
1246
|
+
**It is a worse carrier for the same text.** An `AGENTS.md` block is
|
|
1247
|
+
unconditionally resident in the agent's context, where a skill body is loaded
|
|
1248
|
+
when its description matches — so every unrelated request in that repo pays for
|
|
1249
|
+
a workflow it will not use. It was never a port of the skill; it is a second,
|
|
1250
|
+
weaker capability wearing the same words.
|
|
1251
|
+
The interaction with the entry above is what settles it: a user-edited block
|
|
1252
|
+
cannot be replaced on upgrade without discarding the edit, so this shape needs
|
|
1253
|
+
either the freshness gate rejected above or a three-way merge. *What replaces
|
|
1254
|
+
it*, for an adopter whose agent reads `AGENTS.md`: one line they add
|
|
1255
|
+
themselves, pointing at the skill file. A pointer is cheap enough to be
|
|
1256
|
+
resident, it is their edit in their file, and it cannot go stale — the file it
|
|
1257
|
+
names is the one `init` keeps current.
|
|
1258
|
+
|
|
1259
|
+
- **A Codex target for `attest init`.** Evaluated when `init` grew `--target` and
|
|
1260
|
+
shipped for Claude Code, Cursor and Copilot; rejected because the tool offers
|
|
1261
|
+
nothing to write, not because of what writing it would cost. Codex's
|
|
1262
|
+
project-scoped instruction surface is `AGENTS.md`, which the entry above
|
|
1263
|
+
rejects permanently, and its custom prompts live in `~/.codex/prompts` — per
|
|
1264
|
+
machine, outside the project root `init` is pointed at, and not a thing a
|
|
1265
|
+
project can commit or a CI checkout can carry. That leaves no path that is both
|
|
1266
|
+
**Attest's own** and **read by the tool**, so the two rules a target has to
|
|
1267
|
+
satisfy are not merely unmet here, they are unmeetable with what exists: any
|
|
1268
|
+
Codex-shaped implementation would be either the rejected merge or a file
|
|
1269
|
+
nothing reads, and the second is worse, because it looks like coverage.
|
|
1270
|
+
Filed here rather than queued as a candidate, since nothing about the effort
|
|
1271
|
+
or the evidence is what is holding it — the pointer line above is the whole
|
|
1272
|
+
answer for a Codex user, and this reopens only if Codex grows a project-scoped
|
|
1273
|
+
path of its own.
|
|
1274
|
+
|
|
1275
|
+
[Unreleased]: https://gitlab.com/Pseudorca/attest/-/compare/v0.3.0...main
|
|
1276
|
+
[0.3.0]: https://gitlab.com/Pseudorca/attest/-/tags/v0.3.0
|
|
745
1277
|
[0.2.2]: https://gitlab.com/Pseudorca/attest/-/tags/v0.2.2
|
|
746
1278
|
[0.2.1]: https://gitlab.com/Pseudorca/attest/-/tags/v0.2.1
|
|
747
1279
|
[0.2.0]: https://gitlab.com/Pseudorca/attest/-/tags/v0.2.0
|