@am_shork/attest 0.5.0 → 0.6.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 +209 -113
- package/README.md +1 -1
- package/dist/cli/index.js +21 -3
- package/dist/core/compiler.d.ts +40 -0
- package/dist/core/compiler.js +64 -0
- package/dist/core/docs.d.ts +1 -1
- package/dist/core/docs.js +5 -0
- package/dist/core/locate.d.ts +37 -0
- package/dist/core/locate.js +58 -6
- package/dist/core/merge.d.ts +54 -0
- package/dist/core/merge.js +244 -0
- package/dist/core/pipeline.d.ts +13 -0
- package/dist/core/pipeline.js +181 -14
- package/dist/core/skill.js +46 -10
- package/dist/core/splice.d.ts +52 -0
- package/dist/core/splice.js +189 -0
- package/dist/core/static-registry.d.ts +30 -0
- package/dist/core/static-registry.js +36 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -13,6 +13,193 @@ input, removes/renames a public API or `--json` field, or changes a default
|
|
|
13
13
|
runtime behavior an existing invocation relies on — diagnostic message text is
|
|
14
14
|
not API.
|
|
15
15
|
|
|
16
|
+
## [0.6.0] - 2026-08-03
|
|
17
|
+
|
|
18
|
+
### Added
|
|
19
|
+
|
|
20
|
+
- **The workflow `attest init` writes now describes this release, and one line
|
|
21
|
+
of it was telling agents to undo correct work.** The skill is the document an
|
|
22
|
+
agent reads without a human in the loop, and it had drifted twice.
|
|
23
|
+
**The wrong line.** Its merge step said the specs are renamed in place with
|
|
24
|
+
*"no import changes; if you find yourself editing a specifier, the spec was not
|
|
25
|
+
written at its merged location and stage 1 was the place to fix that."* Sixty
|
|
26
|
+
lines above, the same document teaches a stage-1 scenario to read its proposed
|
|
27
|
+
params from the delta — `import reqs from '../../changes/<name>/requirements.delta.ts'`
|
|
28
|
+
— with the comment *"only this line changes at merge"*. So the file contradicted
|
|
29
|
+
itself, and an agent following it either left a merged spec importing a path
|
|
30
|
+
that step 3 had moved into `archive/`, or concluded a correctly-written spec was
|
|
31
|
+
wrong and moved it, producing the very rewrite the sentence claimed to prevent.
|
|
32
|
+
Exactly the defect `--apply` hit while being built, sitting in the document that
|
|
33
|
+
is trusted furthest.
|
|
34
|
+
The merge step is now `attest archive <name> --apply`, with the by-hand path
|
|
35
|
+
kept for the operations it refuses and stated correctly: one specifier changes,
|
|
36
|
+
and it changes because the delta moves, not because the spec was misplaced.
|
|
37
|
+
Two prevention lines added where they cost nothing — a spec under `changes/`
|
|
38
|
+
is `spec-in-change-dir`, and a proposed name whose merged form is taken is
|
|
39
|
+
`proposed-spec-name-taken`, which is an authoring-time choice and so belongs in
|
|
40
|
+
the authoring guide rather than only in a diagnostic. `compiler-unsupported` is
|
|
41
|
+
deliberately **not** added to the stage-2 table: that table answers "what does
|
|
42
|
+
this mean for my change in flight", and a toolchain failure is not something an
|
|
43
|
+
agent fixes by editing the change.
|
|
44
|
+
**The gate that was missing** is now `ATX-57`. Troubleshooting is asserted
|
|
45
|
+
against `ISSUE_CODES` in both languages and the CLI reference against this file,
|
|
46
|
+
while the skill's content was held by four `toContain` calls and otherwise by
|
|
47
|
+
eye — the wrong footing for the one document whose reader is not a person and
|
|
48
|
+
who branches on the codes in it. Every code-shaped token it names is checked
|
|
49
|
+
against the roster, one-directionally: the workflow has no business naming every
|
|
50
|
+
code, so the obligation is only that what it does name exists. `ATX-40` is the
|
|
51
|
+
same shape pointing the other way over the other document. Given a requirement
|
|
52
|
+
rather than left a plain test for the reason this project exists: a gate that is
|
|
53
|
+
not itself gated is a gate someone deletes, and without one over it, removing
|
|
54
|
+
the check is a green diff.
|
|
55
|
+
`attest init` is idempotent and takes no input, so re-running it after upgrading
|
|
56
|
+
is the whole update mechanism; nothing here changes that.
|
|
57
|
+
|
|
58
|
+
- **A compiler with no AST API is now a diagnosis rather than a crash
|
|
59
|
+
(`compiler-unsupported`).** Discovered by building the `typescript` matrix in
|
|
60
|
+
`[0.4.2]`, which is what a matrix is for. `typescript@7` is the native port: its
|
|
61
|
+
package resolves, imports cleanly and still answers `version`, while the AST
|
|
62
|
+
moved behind `typescript/unstable/ast*` — so `createSourceFile` and every
|
|
63
|
+
`ts.isX` guard both registry readers and the spec parser call is `undefined`,
|
|
64
|
+
and the first one touched throws. Measured: `attest check` died with
|
|
65
|
+
`TypeError: Cannot read properties of undefined (reading 'Latest')` and a raw
|
|
66
|
+
stack — no code, no fix hint, and nothing saying which compiler it wanted, on
|
|
67
|
+
the crash path `[0.4.2]` had just finished sanitising.
|
|
68
|
+
**The declared range excludes 7, and that is not the same as being safe from
|
|
69
|
+
it.** A caret is a request, not a constraint an adopter cannot override:
|
|
70
|
+
`pnpm.overrides` and Yarn `resolutions` both pin a transitive dependency across
|
|
71
|
+
a whole tree, and a repo standardising on one compiler is exactly the repo that
|
|
72
|
+
uses them — this one has an `overrides` block of its own. So the reachable case
|
|
73
|
+
is a deliberate, reasonable act answered with a stack trace.
|
|
74
|
+
Every AST-reading command now refuses before it opens a file, naming the version
|
|
75
|
+
it found and the supported range. The range lives in `ATX-56`'s `params` and its
|
|
76
|
+
scenario compares that against `package.json`: a diagnostic naming a range that
|
|
77
|
+
has drifted from the dependency it describes is worse than one naming none.
|
|
78
|
+
The refusal takes a compiler rather than reaching for the import, because the
|
|
79
|
+
condition exists only on a compiler this repository cannot install beside the
|
|
80
|
+
one it builds with — and a check nothing can make fail is the `never-red` shape
|
|
81
|
+
the gate exists to catch.
|
|
82
|
+
*What it does not do is make Attest work on TypeScript 7.* That is a migration
|
|
83
|
+
to `typescript/unstable/ast`, against an API whose own name says not to depend
|
|
84
|
+
on it yet. The honest answer for now is a supported ceiling that says so, and
|
|
85
|
+
`troubleshooting.md` carries the override-scoping escape for anyone pinning the
|
|
86
|
+
compiler tree-wide.
|
|
87
|
+
This is a behaviour change only in the sense that a crash became an exit code:
|
|
88
|
+
nothing that worked before reports it.
|
|
89
|
+
|
|
90
|
+
- **`attest archive <change> --apply` finishes the merge its gate approved.**
|
|
91
|
+
The gate proved a delta green, covered and drift-free, and a human then
|
|
92
|
+
transcribed it by hand with nothing checking the transcription — the verdict did
|
|
93
|
+
not cover the step acting on it. Reported by `mine-capablanca`: eight changes,
|
|
94
|
+
eight hand-written Python scripts, one of which located the delta's block by
|
|
95
|
+
`split('added: {', 1)`, trimmed two spaces of indent per line and re-terminated
|
|
96
|
+
the registry on `assert t.endswith('});')`.
|
|
97
|
+
Three steps, and the gate runs first in the same invocation: splice each ADDED
|
|
98
|
+
requirement into the registry file owning its prefix, repoint and rename the
|
|
99
|
+
change's proposed specs, move `changes/<name>/` to `archive/<date>-<name>/`.
|
|
100
|
+
Every path touched is printed. A red gate writes nothing, and there is no flag
|
|
101
|
+
that skips it — a merge acting on an earlier run's verdict could file an
|
|
102
|
+
unfinished change as done, which is the one thing this tool must not ship.
|
|
103
|
+
**Why this is not the `AGENTS.md` merge tool, which stays rejected.** That was
|
|
104
|
+
refused because "each of its failure modes is destructive on a file the user
|
|
105
|
+
cannot regenerate". The registry is a literal Attest defines, statically
|
|
106
|
+
parseable, with two readers already asserted to agree on it — so the result of an
|
|
107
|
+
edit is checkable by re-reading it. That clause only holds if the edit is a
|
|
108
|
+
**pure insertion**, so nothing renders a registry back out of a `Registry`
|
|
109
|
+
object: doing so would be easier and would drop every comment and layout choice
|
|
110
|
+
in a hand-written file while passing a re-read with flying colours, because the
|
|
111
|
+
values all survived. `tests/splice.spec.ts` asserts the insertion is pure by
|
|
112
|
+
reconstructing the original from the result, rather than by looking for the
|
|
113
|
+
comments — the comments are a sample of the property, not the property.
|
|
114
|
+
**Re-runnable rather than atomic, which is what pinned the step order.** No
|
|
115
|
+
primitive spans one edit, N renames and a directory move, and a scratch copy of
|
|
116
|
+
the project root would have to be swapped back through the same non-atomic set
|
|
117
|
+
again. So every step is derived from the tree as it currently is, and a partial
|
|
118
|
+
application is finished by running the same command again. `ATX-53` states that
|
|
119
|
+
and deliberately **not** the order it is bought with: an order is not observable —
|
|
120
|
+
every ordering agrees on the happy path — so a requirement about it could not be
|
|
121
|
+
falsified, which is the `never-red` shape the gate exists to catch. Resumability
|
|
122
|
+
can be falsified, and it fails exactly when the order is wrong: move the folder
|
|
123
|
+
first and the recovery has no delta left to re-derive from; rename before
|
|
124
|
+
splicing and the intermediate tree has merged specs reading a registry without
|
|
125
|
+
their ids.
|
|
126
|
+
**ADDED only, and refused whole otherwise** (`apply-unsupported-delta`,
|
|
127
|
+
`apply-no-prefix-owner`). The gate still applies all four operations in memory,
|
|
128
|
+
so nothing becomes unverifiable — only the write-back stops, because REMOVED
|
|
129
|
+
cannot say which comments belonged to the entry it deletes and MODIFIED is a
|
|
130
|
+
formatting-preserving edit inside an existing literal. Both are the shape the
|
|
131
|
+
rejection named. Whole rather than partial because an implementation that merged
|
|
132
|
+
the half it understood would hand back a change the author believes is done.
|
|
133
|
+
**The collision that would have destroyed work** ships with it as
|
|
134
|
+
`proposed-spec-name-taken`, reported by `check`. `isSpecFile` and
|
|
135
|
+
`isProposedSpecFile` are disjoint, so `session.spec.ts` and
|
|
136
|
+
`session.proposed.spec.ts` coexist with nothing objecting — and the rename would
|
|
137
|
+
overwrite the first. Not a corner: a proposed spec is written beside the code it
|
|
138
|
+
attests, so a change to an already-tested module reaches for exactly that name.
|
|
139
|
+
Held in `check` rather than only refused at merge time, so the fix is a rename
|
|
140
|
+
while renaming is still free.
|
|
141
|
+
**One clause of this entry's own argument was wrong, and building it is what
|
|
142
|
+
found out.** It recorded that `[0.5.0]` had removed the import-rewriting blocker
|
|
143
|
+
outright, because a proposed spec sits at its merged location and merging it is
|
|
144
|
+
"a rename in place". True of its *location* — but `[0.5.0]` also made a delta
|
|
145
|
+
read as the registry it proposes, so a stage-1 scenario imports the delta, and
|
|
146
|
+
step 3 moves the delta away. The first working version of this command moved all
|
|
147
|
+
three things correctly and left every merged spec importing a path that no longer
|
|
148
|
+
existed; `check` was silent, and only running the merged suite showed it. So one
|
|
149
|
+
specifier is repointed after all — located by AST, replaced as text, with the
|
|
150
|
+
expression around it untouched because `reqs['AUTH-7'].params.x` reads the same
|
|
151
|
+
on both sides. Far smaller than "rewriting arbitrary TypeScript", but not the
|
|
152
|
+
zero this file claimed. `ATX-52` therefore attests that the *merged project
|
|
153
|
+
verifies clean*, not that three files moved: the broken version would have passed
|
|
154
|
+
the second.
|
|
155
|
+
**It does not re-render a committed `SPEC.md`,** and cannot: nothing in a project
|
|
156
|
+
records where its rendering lives, so the entry listing that as a fourth step was
|
|
157
|
+
describing something with no input. `render --out` afterwards, which a pipeline
|
|
158
|
+
running `render --check` already asks for.
|
|
159
|
+
`ATX-52`–`ATX-55`, with eight scenarios. `--apply` is additive — no exit code
|
|
160
|
+
moves for anyone not passing it — but `proposed-spec-name-taken` is a new ERROR
|
|
161
|
+
on `check` and **will turn some CI red**: a layout that passed before now fails,
|
|
162
|
+
and everything it fails would have been destroyed by the first merge.
|
|
163
|
+
|
|
164
|
+
- **`check` now names a spec left under `changes/`, where nothing walks it
|
|
165
|
+
(`spec-in-change-dir`).** `[0.5.0]` moved a change's specs to their merged
|
|
166
|
+
location and told anyone with an open change directory to move each file,
|
|
167
|
+
rename it `*.proposed.spec.ts` and fix its imports once. It did not account for
|
|
168
|
+
the state *before* that is carried out. `changes` is in the project walk's
|
|
169
|
+
skip list, and `[0.5.0]` removed `changeExcludeGlobs` — the mechanism by which
|
|
170
|
+
`attest archive <name>` used to include `changes/<name>/specs/` explicitly.
|
|
171
|
+
Both halves are right on their own; together they mean the old location is
|
|
172
|
+
walked by no command.
|
|
173
|
+
**Measured before it was fixed**, on a copy of `fixtures/e2e-archive` with its
|
|
174
|
+
proposed spec put back at `changes/add-totp/specs/totp.spec.ts` and its two
|
|
175
|
+
specifiers re-depthed: `attest check` exited **0** with `✓ No issues.`, and the
|
|
176
|
+
only command that spoke was the gate — `uncovered-requirement`, verbatim
|
|
177
|
+
*"Requirement "AUTH-7" has no scenario attesting it. Add a scenario, or mark it
|
|
178
|
+
deferred."* The author had written two, and both were correct. So the one
|
|
179
|
+
diagnostic available told them to write what they had written, or to defer a
|
|
180
|
+
requirement they had finished. That is the misdiagnosis shape `ATX-49` records,
|
|
181
|
+
arriving from the other direction — there the verdict pointed away from a load
|
|
182
|
+
failure, here away from a path — and in both cases it spends the reader's
|
|
183
|
+
attention on files that are fine.
|
|
184
|
+
In `check` rather than the gate: the gate at least fails, while `check` is both
|
|
185
|
+
the command a pipeline runs first and the one that said nothing at all. Both
|
|
186
|
+
spellings are reported, because a `*.proposed.spec.ts` under `changes/` is a
|
|
187
|
+
migration done halfway — the right marker at the wrong path — and no more
|
|
188
|
+
reachable than one never begun. An ERROR rather than a warning for `ATX-47`'s
|
|
189
|
+
reason: a spec that runs nowhere is a total failure, not a misplaced one.
|
|
190
|
+
*It is not transitional and does not expire.* After `[0.5.0]` a spec under
|
|
191
|
+
`changes/` is wrong permanently; catching an unfinished upgrade is what it is
|
|
192
|
+
worth first, not what it is for.
|
|
193
|
+
**This is a behaviour change and turns some CI red** — a layout that passed
|
|
194
|
+
`check` in `[0.5.0]` now fails it. That is the point, and everything it fails
|
|
195
|
+
was already running nowhere. `ATX-51`, with three scenarios: the pre-move
|
|
196
|
+
spelling, the half-migrated one, and a proposed spec at its merged location
|
|
197
|
+
that must *not* be reported, because a check firing on the documented workflow
|
|
198
|
+
would be §9's "usually wrong about staleness" failure again.
|
|
199
|
+
Found while asking whether a *different* proposed check — the merged-name
|
|
200
|
+
collision under `attest archive --apply`, still in `Planned` — would help
|
|
201
|
+
anyone upgrading. It would not; this does.
|
|
202
|
+
|
|
16
203
|
## [0.5.0] - 2026-08-03
|
|
17
204
|
|
|
18
205
|
### Changed
|
|
@@ -2249,44 +2436,26 @@ feedback reports are triaged into issues or `Unreleased` and deliberately not
|
|
|
2249
2436
|
kept in this tree; that rule stands, and holds for anything with a reporter, a
|
|
2250
2437
|
repro, or a state to track. What is kept here is the narrower thing this file
|
|
2251
2438
|
already keeps one section of: a **decision with an argument attached**, where
|
|
2252
|
-
losing the argument would cost more than losing the task. The
|
|
2253
|
-
of
|
|
2254
|
-
|
|
2255
|
-
|
|
2256
|
-
|
|
2257
|
-
which is
|
|
2258
|
-
|
|
2259
|
-
|
|
2260
|
-
|
|
2261
|
-
|
|
2262
|
-
|
|
2263
|
-
|
|
2264
|
-
|
|
2265
|
-
|
|
2266
|
-
|
|
2267
|
-
|
|
2268
|
-
|
|
2269
|
-
|
|
2270
|
-
|
|
2271
|
-
|
|
2272
|
-
no fix hint, and nothing saying which compiler it wanted.
|
|
2273
|
-
**The range excludes 7, and that is not the same as being safe from it.** A
|
|
2274
|
-
caret is a request, not a constraint the adopter cannot override: `pnpm.overrides`
|
|
2275
|
-
and Yarn `resolutions` both pin a transitive dependency across the whole tree,
|
|
2276
|
-
and a repo standardising on one compiler is exactly the repo that uses them —
|
|
2277
|
-
this one has an `overrides` block of its own. So the reachable case is a
|
|
2278
|
-
deliberate, reasonable act by an adopter, answered with a stack trace.
|
|
2279
|
-
What is planned is a check of the API surface where the readers are entered,
|
|
2280
|
-
reported as an ERROR naming the resolved version and the supported range. Cheap,
|
|
2281
|
-
and it converts the one dependency failure this tool cannot survive from a crash
|
|
2282
|
-
into a diagnosis. *Deliberately not folded into the matrix commit*: a new ERROR
|
|
2283
|
-
is a behaviour change, it needs an issue code, a troubleshooting section in both
|
|
2284
|
-
languages and an `ATX-n` with a scenario, and none of that belongs in a commit
|
|
2285
|
-
whose whole claim is that nothing about behaviour moved.
|
|
2286
|
-
*What it cannot do:* make Attest work on TypeScript 7. That is a migration to
|
|
2287
|
-
`typescript/unstable/ast`, against an API whose own name says not to depend on
|
|
2288
|
-
it yet — a separate decision, with the honest answer for now being that the
|
|
2289
|
-
supported range has a ceiling and says so.
|
|
2439
|
+
losing the argument would cost more than losing the task. The one below came out
|
|
2440
|
+
of reading another repository against this one — `msw`.
|
|
2441
|
+
|
|
2442
|
+
Five entries have left by being built, and the last three left together in
|
|
2443
|
+
`[0.6.0]`. `compiler-unsupported` came from building something queued here,
|
|
2444
|
+
which is one way an entry arrives: a gate that finds a defect on its first run
|
|
2445
|
+
files the defect, and the `typescript` matrix `[0.4.2]` added is what found this
|
|
2446
|
+
one.
|
|
2447
|
+
`--apply` came from an adoption report (`mine-capablanca`, 0.4.2, 34 requirements
|
|
2448
|
+
/ 56 scenarios / greenfield) — a third way an entry arrives: not a defect, but a
|
|
2449
|
+
shape the tool made an adopter work around eight times without ever erroring. It
|
|
2450
|
+
took `spec-in-change-dir` out with it, by a fourth way worth naming: designing one
|
|
2451
|
+
of `--apply`'s checks surfaced a different one, against a migration this file had
|
|
2452
|
+
itself shipped. **What a release costs its adopters is not fully known on the day
|
|
2453
|
+
it is cut** — and building the entry is what finds out, which is also how
|
|
2454
|
+
`--apply` discovered that one clause of its own argument here was wrong. Both are
|
|
2455
|
+
recorded under `[0.6.0]`, and two earlier entries from the same report left
|
|
2456
|
+
by shipping in `[0.5.0]` — a change's specs moving to their merged location, and
|
|
2457
|
+
the delta reading as the registry of what it adds — their arguments recorded
|
|
2458
|
+
there, including the part of the first one that turned out to be wrong.
|
|
2290
2459
|
|
|
2291
2460
|
- **Region-marked samples, so a partial quote can be bound too.** The whole-file
|
|
2292
2461
|
half of this shipped in `[0.4.3]`: the README's two getting-started blocks
|
|
@@ -2435,80 +2604,6 @@ have to gather again.
|
|
|
2435
2604
|
here rather than moving to `Considered and rejected`: documenting a trap is
|
|
2436
2605
|
not the same as deciding not to detect it.
|
|
2437
2606
|
|
|
2438
|
-
- **`attest archive <change> --apply`, finishing the merge the gate approved.**
|
|
2439
|
-
`mine-capablanca`'s single loudest item: eight changes, eight hand-written
|
|
2440
|
-
Python scripts, and the report quotes one — it locates the delta's block by
|
|
2441
|
-
`split('added: {', 1)`, trims two spaces of indent per line, and re-terminates
|
|
2442
|
-
the registry on `assert t.endswith('});')`. After the gate goes green the
|
|
2443
|
-
adopter hand-splices the delta into the registry, moves specs into the suite,
|
|
2444
|
-
rewrites their imports, moves the folder to `archive/` and re-renders `SPEC.md`.
|
|
2445
|
-
**The cost is the smaller half of the argument. The hole is that the gate's
|
|
2446
|
-
verdict does not cover the step that acts on it** — `archive` proves the delta
|
|
2447
|
-
is green, covered and drift-free, and then a human transcribes it by hand with
|
|
2448
|
-
nothing checking the transcription. A delta can be spliced in wrong, partially,
|
|
2449
|
-
or twice, and the next `check` sees only whatever ended up in the file.
|
|
2450
|
-
**The objection that has to be answered is the `AGENTS.md` block under
|
|
2451
|
-
`Considered and rejected`.**
|
|
2452
|
-
The `AGENTS.md` block was rejected because "that is a merge tool, and each of
|
|
2453
|
-
its failure modes is destructive on a file the user cannot regenerate". The
|
|
2454
|
-
argument for why it does not transfer, which needs to be right before this
|
|
2455
|
-
ships: there, Attest would edit prose whose grammar it does not own; here the
|
|
2456
|
-
registry is a **literal** Attest defines, statically parseable, with a
|
|
2457
|
-
differential suite already asserting two readers agree on it — so the result of
|
|
2458
|
-
a splice is checkable by re-reading it, and the delta was validated by the gate
|
|
2459
|
-
that just ran. Idempotence is verifiable rather than assumed. That reasoning
|
|
2460
|
-
covers delta→registry, the folder move and the re-render. It used **not** to
|
|
2461
|
-
cover import rewriting, which is editing arbitrary TypeScript and is squarely
|
|
2462
|
-
the rejected shape.
|
|
2463
|
-
*That blocker is gone, and it was removed rather than solved.* Proposed specs
|
|
2464
|
-
now live at their merged location under `*.proposed.spec.ts` (`[0.5.0]`), so
|
|
2465
|
-
the merge step for a spec is a rename in place — there are no specifiers to
|
|
2466
|
-
rewrite, and `--apply` no longer inherits the one part of the job that could
|
|
2467
|
-
not be made safe. What is left for it is the delta→registry splice, the rename,
|
|
2468
|
-
the folder move to `archive/` and the re-render, all of which the paragraph
|
|
2469
|
-
above argues are checkable by re-reading the result.
|
|
2470
|
-
*What still blocks the decision:* two questions with no answer yet — what
|
|
2471
|
-
`--apply` does when the working tree is dirty (`archive` already has a position
|
|
2472
|
-
on this; whether a file-mutating flag takes the same one is not obvious), and
|
|
2473
|
-
whether a partially applied merge is recoverable or whether the flag must be
|
|
2474
|
-
all-or-nothing against a scratch copy.
|
|
2475
|
-
*The frequency blocker has moved.* This entry used to close on "one report, one
|
|
2476
|
-
repo: the cost evidence is strong and the frequency evidence is a single
|
|
2477
|
-
adopter." It is now two. This project's own maintainer proposed the same command
|
|
2478
|
-
independently, without the entry in front of them, having felt the same step by
|
|
2479
|
-
hand — which is the second data point that clause was waiting for. Not
|
|
2480
|
-
conclusive about frequency in the field, and it does not touch either question
|
|
2481
|
-
above, but it removes the reason to keep waiting for someone else to ask.
|
|
2482
|
-
|
|
2483
|
-
*A narrower shape was proposed with it and is rejected — `attest merge <change>`,
|
|
2484
|
-
moving the change folder to `archive/<date>-<name>/` and nothing else.* Three
|
|
2485
|
-
reasons, and the third is the one that decides it.
|
|
2486
|
-
**(1) It targets the cheap half.** The expensive step in the report is the
|
|
2487
|
-
delta→registry splice — the quoted Python locates the block by
|
|
2488
|
-
`split('added: {', 1)`, trims two spaces per line, and re-terminates on
|
|
2489
|
-
`assert t.endswith('});')`. The folder move is `mv`. A new command that saves a
|
|
2490
|
-
shell invocation and leaves the TypeScript-literal surgery is the wrong half to
|
|
2491
|
-
automate. The date it would stamp is real ergonomics but small: `archive/` is
|
|
2492
|
-
named in three places and all three are exclusions (`locate.ts`'s SKIP_DIRS,
|
|
2493
|
-
and the two exclude globs), so nothing reads the directory and a mis-stamped
|
|
2494
|
-
date has no mechanical consequence at all.
|
|
2495
|
-
**(2) It would make two command names mislead instead of one.** `attest archive`
|
|
2496
|
-
archives nothing today — it is purely the gate. Adding a second verb that does
|
|
2497
|
-
the archiving leaves `archive` still lying and `merge` doing its job; `--apply`
|
|
2498
|
-
goes the other way and makes the existing verb finally mean what it says.
|
|
2499
|
-
**(3) A move that does not re-run the gate can file an unfinished change as
|
|
2500
|
-
done.** That is the one thing this tool must not ship: the whole claim is that
|
|
2501
|
-
"done" has a hard definition, and a command able to mark completion around that
|
|
2502
|
-
definition removes it. The alternative is that `merge` re-runs the gate — at
|
|
2503
|
-
which point it *is* `--apply` and should be spelled that way. There is no third
|
|
2504
|
-
option, which is why this is a rejection of the shape rather than of the idea.
|
|
2505
|
-
*Recorded here rather than under `Considered and rejected`,* because nothing
|
|
2506
|
-
was decided against: the idea is this entry, and only the narrow spelling of it
|
|
2507
|
-
died. Also worth stating because it needed no release to ride — a new command
|
|
2508
|
-
changes no exit code, rejects no previously-valid input, and removes no API, so
|
|
2509
|
-
it is additive under this file's own Versioning rule and was never blocked on a
|
|
2510
|
-
version bump.
|
|
2511
|
-
|
|
2512
2607
|
## Considered and rejected
|
|
2513
2608
|
|
|
2514
2609
|
Decisions **not** to build something, kept where they can be found before the
|
|
@@ -2829,7 +2924,8 @@ The permanent half goes to design §11 with the params rule, not here.
|
|
|
2829
2924
|
adopter owes their users, whose rows a reviewer would want in the rendered spec.
|
|
2830
2925
|
None has been seen; the one that raised it was tuning.
|
|
2831
2926
|
|
|
2832
|
-
[Unreleased]: https://gitlab.com/Pseudorca/attest/-/compare/v0.
|
|
2927
|
+
[Unreleased]: https://gitlab.com/Pseudorca/attest/-/compare/v0.6.0...main
|
|
2928
|
+
[0.6.0]: https://gitlab.com/Pseudorca/attest/-/tags/v0.6.0
|
|
2833
2929
|
[0.5.0]: https://gitlab.com/Pseudorca/attest/-/tags/v0.5.0
|
|
2834
2930
|
[0.4.3]: https://gitlab.com/Pseudorca/attest/-/tags/v0.4.3
|
|
2835
2931
|
[0.4.2]: https://gitlab.com/Pseudorca/attest/-/tags/v0.4.2
|
package/README.md
CHANGED
|
@@ -157,7 +157,7 @@ Every diagnostic carries a `code`, and every code has a section in
|
|
|
157
157
|
```
|
|
158
158
|
ERROR registry-not-static (requirements/upload.reqs.ts:5)
|
|
159
159
|
Value is not a literal.
|
|
160
|
-
→ https://gitlab.com/Pseudorca/attest/-/blob/v0.
|
|
160
|
+
→ https://gitlab.com/Pseudorca/attest/-/blob/v0.6.0/docs/en/troubleshooting.md#registry-not-static
|
|
161
161
|
```
|
|
162
162
|
|
|
163
163
|
The anchor **is** the code, so the link cannot point somewhere the section
|
package/dist/cli/index.js
CHANGED
|
@@ -11,12 +11,17 @@ import { Command } from 'commander';
|
|
|
11
11
|
import chalk from 'chalk';
|
|
12
12
|
import { resolve } from 'node:path';
|
|
13
13
|
import { writeAtomic } from '../core/write.js';
|
|
14
|
-
import { runCheck, runVerify, runCover, runArchive, runInit, runRender, runRenderCheck, runStatus, DEFAULT_TARGET, TARGET_NAMES, } from '../core/pipeline.js';
|
|
14
|
+
import { runCheck, runVerify, runCover, runArchive, runArchiveApply, runInit, runRender, runRenderCheck, runStatus, DEFAULT_TARGET, TARGET_NAMES, } from '../core/pipeline.js';
|
|
15
15
|
import { hasError } from '../core/types.js';
|
|
16
16
|
import { formatCrash, formatIssues, formatScope, summarize, formatCoverage, formatStatus, } from './report.js';
|
|
17
17
|
import { archiveReport, checkReport, coverReport, errorReport, initReport, renderJson, renderReport, statusReport, verifyReport, } from './json.js';
|
|
18
18
|
import { packageVersion } from '../core/version.js';
|
|
19
19
|
const VERSION = packageVersion();
|
|
20
|
+
/** `runArchive` in the shape `--apply` returns, so the two share one call site. */
|
|
21
|
+
const gateOnly = async (dir, change, options) => ({
|
|
22
|
+
issues: await runArchive(dir, change, options),
|
|
23
|
+
written: [],
|
|
24
|
+
});
|
|
20
25
|
const program = new Command();
|
|
21
26
|
program
|
|
22
27
|
.name('attest')
|
|
@@ -267,14 +272,27 @@ program
|
|
|
267
272
|
.argument('[dir]', 'project root', undefined)
|
|
268
273
|
.option(JSON_FLAG, JSON_HELP)
|
|
269
274
|
.option(VITEST_CONFIG_FLAG, VITEST_CONFIG_HELP)
|
|
275
|
+
.option('--apply', 'if the gate passes, merge the change: splice its ADDED requirements into the registry, rename its proposed specs, and move the change folder to archive/')
|
|
270
276
|
.description('Run the archive gate; merge only if it passes.')
|
|
271
277
|
.action((change, dir, opts) => runAction('archive', opts, async () => {
|
|
272
|
-
const
|
|
278
|
+
const run = opts.apply ? runArchiveApply : gateOnly;
|
|
279
|
+
const { issues: blocking, written } = await run(root(dir), change, {
|
|
280
|
+
vitestConfig: vitestConfig(opts),
|
|
281
|
+
});
|
|
273
282
|
return {
|
|
274
283
|
report: archiveReport(VERSION, change, blocking),
|
|
275
284
|
human: () => {
|
|
276
285
|
if (blocking.length === 0) {
|
|
277
|
-
console.log(chalk.green(
|
|
286
|
+
console.log(chalk.green(opts.apply
|
|
287
|
+
? `✓ Gate passed and merged: change "${change}".`
|
|
288
|
+
: `✓ Gate passed: change "${change}" can be archived.`));
|
|
289
|
+
// Every path the merge touched, because this is the command that
|
|
290
|
+
// edits files the user cannot regenerate and they need to know which
|
|
291
|
+
// ones to read before committing. It is also what a working-tree
|
|
292
|
+
// check would only have approximated, and it works for someone not
|
|
293
|
+
// using version control at all.
|
|
294
|
+
for (const path of written)
|
|
295
|
+
console.log(chalk.dim(` ${path}`));
|
|
278
296
|
}
|
|
279
297
|
else {
|
|
280
298
|
console.log(chalk.red(`✗ Gate failed: change "${change}"`));
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { Issue } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* The range the readers are written against.
|
|
4
|
+
*
|
|
5
|
+
* Stated here rather than read from the manifest at runtime, because the
|
|
6
|
+
* manifest is not on disk beside a bundled build and a diagnostic that cannot
|
|
7
|
+
* name the range is most of the diagnostic gone. `ATX-56` keeps the two honest:
|
|
8
|
+
* its `params` carry this string, and its scenario compares them with
|
|
9
|
+
* `package.json`, so the constant cannot drift from the dependency it describes.
|
|
10
|
+
*/
|
|
11
|
+
export declare const SUPPORTED_TYPESCRIPT = "^5.5.0 || ^6.0.0";
|
|
12
|
+
/**
|
|
13
|
+
* The part of the compiler surface every AST reader in this project touches.
|
|
14
|
+
*
|
|
15
|
+
* Deliberately the two members that are reached *first* rather than an
|
|
16
|
+
* exhaustive list: `createSourceFile` and `ScriptTarget` are what
|
|
17
|
+
* `parser.ts`, `static-registry.ts` and `splice.ts` all open with, so a compiler
|
|
18
|
+
* that has them has the shape this check is about, and one that lacks either
|
|
19
|
+
* cannot get far enough for a longer list to matter.
|
|
20
|
+
*/
|
|
21
|
+
interface CompilerSurface {
|
|
22
|
+
version?: unknown;
|
|
23
|
+
createSourceFile?: unknown;
|
|
24
|
+
ScriptTarget?: unknown;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* An ERROR when the resolved compiler has no AST API, or `undefined` when it
|
|
28
|
+
* does.
|
|
29
|
+
*
|
|
30
|
+
* Takes the compiler rather than reaching for the import directly, so the
|
|
31
|
+
* refusal can be attested against a stand-in: the condition only exists on a
|
|
32
|
+
* compiler this repository cannot install alongside the one it builds with, and
|
|
33
|
+
* a check nothing can make fail is the shape `never-red` exists to catch.
|
|
34
|
+
*
|
|
35
|
+
* No `reqId` and no `file`: the finding is about the toolchain, and every
|
|
36
|
+
* requirement and every file in the project is equally unreadable because of it.
|
|
37
|
+
*/
|
|
38
|
+
export declare function compilerIssue(compiler?: CompilerSurface): Issue | undefined;
|
|
39
|
+
export {};
|
|
40
|
+
//# sourceMappingURL=compiler.d.ts.map
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
// Whether the resolved TypeScript can be read from at all (design §5.1).
|
|
2
|
+
//
|
|
3
|
+
// Both registry readers and the spec parser walk the AST with the compiler API,
|
|
4
|
+
// which makes `typescript` the one dependency this tool cannot survive the
|
|
5
|
+
// absence of — and "absent" is not the shape it arrives in. `typescript@7` is
|
|
6
|
+
// the native port: its package resolves, imports cleanly, and answers `version`
|
|
7
|
+
// and `versionMajorMinor`, while `createSourceFile` and every `ts.isX` guard
|
|
8
|
+
// moved behind `typescript/unstable/ast*`. So the members are `undefined` rather
|
|
9
|
+
// than missing, and the first thing to touch one throws.
|
|
10
|
+
//
|
|
11
|
+
// Measured: `attest check` on it died with
|
|
12
|
+
// `TypeError: Cannot read properties of undefined (reading 'Latest')` and a raw
|
|
13
|
+
// stack — no issue code, no fix hint, and nothing saying which compiler it
|
|
14
|
+
// wanted, on the crash path `[0.4.2]` had just finished sanitising.
|
|
15
|
+
//
|
|
16
|
+
// **The declared range excludes 7, and that is not the same as being safe from
|
|
17
|
+
// it.** A caret is a request, not a constraint the adopter cannot override:
|
|
18
|
+
// `pnpm.overrides` and Yarn `resolutions` both pin a transitive dependency
|
|
19
|
+
// across a whole tree, and a repo standardising on one compiler is exactly the
|
|
20
|
+
// repo that uses them — this one has an `overrides` block of its own. So the
|
|
21
|
+
// reachable case is a deliberate, reasonable act by an adopter, answered with a
|
|
22
|
+
// stack trace.
|
|
23
|
+
//
|
|
24
|
+
// What this cannot do is make Attest work on TypeScript 7. That is a migration
|
|
25
|
+
// to `typescript/unstable/ast`, against an API whose own name says not to depend
|
|
26
|
+
// on it yet. The honest answer for now is that the supported range has a ceiling
|
|
27
|
+
// and says so.
|
|
28
|
+
import ts from 'typescript';
|
|
29
|
+
/**
|
|
30
|
+
* The range the readers are written against.
|
|
31
|
+
*
|
|
32
|
+
* Stated here rather than read from the manifest at runtime, because the
|
|
33
|
+
* manifest is not on disk beside a bundled build and a diagnostic that cannot
|
|
34
|
+
* name the range is most of the diagnostic gone. `ATX-56` keeps the two honest:
|
|
35
|
+
* its `params` carry this string, and its scenario compares them with
|
|
36
|
+
* `package.json`, so the constant cannot drift from the dependency it describes.
|
|
37
|
+
*/
|
|
38
|
+
export const SUPPORTED_TYPESCRIPT = '^5.5.0 || ^6.0.0';
|
|
39
|
+
/**
|
|
40
|
+
* An ERROR when the resolved compiler has no AST API, or `undefined` when it
|
|
41
|
+
* does.
|
|
42
|
+
*
|
|
43
|
+
* Takes the compiler rather than reaching for the import directly, so the
|
|
44
|
+
* refusal can be attested against a stand-in: the condition only exists on a
|
|
45
|
+
* compiler this repository cannot install alongside the one it builds with, and
|
|
46
|
+
* a check nothing can make fail is the shape `never-red` exists to catch.
|
|
47
|
+
*
|
|
48
|
+
* No `reqId` and no `file`: the finding is about the toolchain, and every
|
|
49
|
+
* requirement and every file in the project is equally unreadable because of it.
|
|
50
|
+
*/
|
|
51
|
+
export function compilerIssue(compiler = ts) {
|
|
52
|
+
if (typeof compiler.createSourceFile === 'function' && compiler.ScriptTarget !== undefined) {
|
|
53
|
+
return undefined;
|
|
54
|
+
}
|
|
55
|
+
const version = typeof compiler.version === 'string' ? compiler.version : 'unknown';
|
|
56
|
+
return {
|
|
57
|
+
level: 'ERROR',
|
|
58
|
+
code: 'compiler-unsupported',
|
|
59
|
+
message: `The resolved TypeScript (${version}) does not expose the AST API Attest reads registries and specs with. ` +
|
|
60
|
+
`Attest supports ${SUPPORTED_TYPESCRIPT}. ` +
|
|
61
|
+
`If a resolution or override pins the compiler for the whole tree, exclude Attest from it or move that pin back into the supported range.`,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
//# sourceMappingURL=compiler.js.map
|
package/dist/core/docs.d.ts
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* and the `##` headings of both language documents, so landing here cannot
|
|
9
9
|
* produce a dead link.
|
|
10
10
|
*/
|
|
11
|
-
export declare const ISSUE_CODES: readonly ["add-conflict", "add-invalid", "added-id-unmerged", "change-not-found", "declared-not-run", "duplicate-prefix", "duplicate-requirement", "empty-spec", "internal-error", "invalid-change-name", "missing-spec-doc", "modify-invalid", "modify-missing", "never-red", "orphan-test", "possible-drift", "proposed-spec-unclaimed", "rationale-placeholder", "registry-invalid", "registry-no-default", "registry-not-static", "rename-source-missing", "rename-target-exists", "rename-target-invalid", "stale-spec-doc", "tests-red", "unbound-param", "uncovered-requirement", "unknown-target"];
|
|
11
|
+
export declare const ISSUE_CODES: readonly ["add-conflict", "add-invalid", "added-id-unmerged", "apply-no-prefix-owner", "apply-unsupported-delta", "change-not-found", "compiler-unsupported", "declared-not-run", "duplicate-prefix", "duplicate-requirement", "empty-spec", "internal-error", "invalid-change-name", "missing-spec-doc", "modify-invalid", "modify-missing", "never-red", "orphan-test", "possible-drift", "proposed-spec-name-taken", "proposed-spec-unclaimed", "rationale-placeholder", "registry-invalid", "registry-no-default", "registry-not-static", "rename-source-missing", "rename-target-exists", "rename-target-invalid", "spec-in-change-dir", "stale-spec-doc", "tests-red", "unbound-param", "uncovered-requirement", "unknown-target"];
|
|
12
12
|
export type IssueCode = (typeof ISSUE_CODES)[number];
|
|
13
13
|
/**
|
|
14
14
|
* The page explaining `code`, or `undefined` when nothing explains it.
|
package/dist/core/docs.js
CHANGED
|
@@ -22,7 +22,10 @@ export const ISSUE_CODES = [
|
|
|
22
22
|
'add-conflict',
|
|
23
23
|
'add-invalid',
|
|
24
24
|
'added-id-unmerged',
|
|
25
|
+
'apply-no-prefix-owner',
|
|
26
|
+
'apply-unsupported-delta',
|
|
25
27
|
'change-not-found',
|
|
28
|
+
'compiler-unsupported',
|
|
26
29
|
'declared-not-run',
|
|
27
30
|
'duplicate-prefix',
|
|
28
31
|
'duplicate-requirement',
|
|
@@ -35,6 +38,7 @@ export const ISSUE_CODES = [
|
|
|
35
38
|
'never-red',
|
|
36
39
|
'orphan-test',
|
|
37
40
|
'possible-drift',
|
|
41
|
+
'proposed-spec-name-taken',
|
|
38
42
|
'proposed-spec-unclaimed',
|
|
39
43
|
'rationale-placeholder',
|
|
40
44
|
'registry-invalid',
|
|
@@ -43,6 +47,7 @@ export const ISSUE_CODES = [
|
|
|
43
47
|
'rename-source-missing',
|
|
44
48
|
'rename-target-exists',
|
|
45
49
|
'rename-target-invalid',
|
|
50
|
+
'spec-in-change-dir',
|
|
46
51
|
'stale-spec-doc',
|
|
47
52
|
'tests-red',
|
|
48
53
|
'unbound-param',
|
package/dist/core/locate.d.ts
CHANGED
|
@@ -98,11 +98,28 @@ export declare function staticReader(): RegistryReader;
|
|
|
98
98
|
* prefix a duplicate-prefix ERROR.
|
|
99
99
|
*
|
|
100
100
|
* Pass `files` to reuse a {@link scanProject} result instead of re-walking.
|
|
101
|
+
*
|
|
102
|
+
* `prefixOwners` comes back with the registry because the rule deciding it —
|
|
103
|
+
* first claim wins, in sorted file order — must have exactly one spelling. It is
|
|
104
|
+
* what `duplicate-prefix` is computed from here, and what tells `--apply` which
|
|
105
|
+
* file an ADDED id belongs in (design §7); a second walk arriving at its own
|
|
106
|
+
* answer would be a second rule the moment either was edited.
|
|
101
107
|
*/
|
|
102
108
|
export declare function loadRegistry(root: string, reader: RegistryReader, files?: string[]): Promise<{
|
|
103
109
|
registry: Registry;
|
|
104
110
|
issues: Issue[];
|
|
111
|
+
prefixOwners: Record<string, string>;
|
|
105
112
|
}>;
|
|
113
|
+
/**
|
|
114
|
+
* The part of an id that names the space it lives in — `AUTH` of `AUTH-3`.
|
|
115
|
+
*
|
|
116
|
+
* The prefix is the only unit above the requirement (design §11) and two things
|
|
117
|
+
* now depend on agreeing about it: `duplicate-prefix`, and which file `--apply`
|
|
118
|
+
* writes an ADDED requirement into. An id with no dash is its own prefix, which
|
|
119
|
+
* cannot arise from `RequirementIdSchema` and is handled anyway because this
|
|
120
|
+
* also runs over ids a delta proposed.
|
|
121
|
+
*/
|
|
122
|
+
export declare function idPrefix(id: string): string;
|
|
106
123
|
/**
|
|
107
124
|
* Parse the given spec files into one merged plan (paths shown relative to
|
|
108
125
|
* `displayRoot`). Files are read concurrently; the merge follows the input
|
|
@@ -115,6 +132,26 @@ export declare function loadRegistry(root: string, reader: RegistryReader, files
|
|
|
115
132
|
export declare function parseSpecs(files: string[], displayRoot: string): Promise<AttestPlan>;
|
|
116
133
|
/** Parse every `*.spec.ts` under root into one merged plan (file paths shown relative to root). */
|
|
117
134
|
export declare function parseAllSpecFiles(root: string): Promise<AttestPlan>;
|
|
135
|
+
/**
|
|
136
|
+
* Spec-shaped files sitting under `root/changes` — the location a change's
|
|
137
|
+
* specs used to live at, and which nothing walks any more (design §7).
|
|
138
|
+
*
|
|
139
|
+
* Two correct decisions compose into a blind spot. `changes` is in `SKIP_DIRS`,
|
|
140
|
+
* so `scanProject` cannot reach these; and the explicit include `attest archive`
|
|
141
|
+
* once applied to `changes/<name>/specs/` went away with `changeExcludeGlobs`
|
|
142
|
+
* when specs moved to their merged location. A spec left here therefore executes
|
|
143
|
+
* in no suite and no gate, and the only command that says anything is the gate,
|
|
144
|
+
* blaming coverage for a file it cannot see.
|
|
145
|
+
*
|
|
146
|
+
* Walks through `findFiles` rather than a second walker: `SKIP_DIRS` is consulted
|
|
147
|
+
* for *sub*directories only, so starting the walk at `changes` itself both
|
|
148
|
+
* reaches these files and keeps skipping `node_modules` beneath them.
|
|
149
|
+
*
|
|
150
|
+
* Both spellings are wrong here and both are returned. A `*.spec.ts` is the
|
|
151
|
+
* pre-move layout; a `*.proposed.spec.ts` is the right marker at the wrong path,
|
|
152
|
+
* which is the half-done migration and no more visible than the other.
|
|
153
|
+
*/
|
|
154
|
+
export declare function findChangeDirSpecs(root: string): Promise<string[]>;
|
|
118
155
|
/** List the names of proposed changes under `root/changes`. */
|
|
119
156
|
export declare function listChangeNames(root: string): Promise<string[]>;
|
|
120
157
|
//# sourceMappingURL=locate.d.ts.map
|