@am_shork/attest 0.1.6 → 0.2.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 +293 -83
- package/README.md +73 -5
- package/dist/cli/index.js +38 -13
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/json.d.ts +4 -2
- package/dist/cli/json.d.ts.map +1 -1
- package/dist/cli/json.js +2 -2
- package/dist/cli/json.js.map +1 -1
- package/dist/cli/report.d.ts +10 -2
- package/dist/cli/report.d.ts.map +1 -1
- package/dist/cli/report.js +18 -2
- package/dist/cli/report.js.map +1 -1
- package/dist/core/apply.d.ts.map +1 -1
- package/dist/core/apply.js +7 -1
- package/dist/core/apply.js.map +1 -1
- package/dist/core/gate.d.ts +9 -0
- package/dist/core/gate.d.ts.map +1 -1
- package/dist/core/gate.js +25 -12
- package/dist/core/gate.js.map +1 -1
- package/dist/core/loader.d.ts.map +1 -1
- package/dist/core/loader.js +16 -1
- package/dist/core/loader.js.map +1 -1
- package/dist/core/locate.d.ts +61 -5
- package/dist/core/locate.d.ts.map +1 -1
- package/dist/core/locate.js +130 -48
- package/dist/core/locate.js.map +1 -1
- package/dist/core/order.d.ts +3 -0
- package/dist/core/order.d.ts.map +1 -0
- package/dist/core/order.js +13 -0
- package/dist/core/order.js.map +1 -0
- package/dist/core/pipeline.d.ts +38 -6
- package/dist/core/pipeline.d.ts.map +1 -1
- package/dist/core/pipeline.js +156 -69
- package/dist/core/pipeline.js.map +1 -1
- package/dist/core/render.d.ts.map +1 -1
- package/dist/core/render.js +8 -4
- package/dist/core/render.js.map +1 -1
- package/dist/core/runner.d.ts.map +1 -1
- package/dist/core/runner.js +6 -0
- package/dist/core/runner.js.map +1 -1
- package/dist/core/static-registry.d.ts +14 -0
- package/dist/core/static-registry.d.ts.map +1 -0
- package/dist/core/static-registry.js +255 -0
- package/dist/core/static-registry.js.map +1 -0
- package/dist/core/validator.d.ts.map +1 -1
- package/dist/core/validator.js +16 -4
- package/dist/core/validator.js.map +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,92 +5,300 @@ All notable changes to this project are documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## Versioning
|
|
9
|
+
|
|
10
|
+
Under 0.x SemVer a breaking change bumps the **minor**. "Breaking" means it
|
|
11
|
+
changes the exit code of an existing valid setup, rejects previously-valid
|
|
12
|
+
input, removes/renames a public API or `--json` field, or changes a default
|
|
13
|
+
runtime behavior an existing invocation relies on — diagnostic message text is
|
|
14
|
+
not API.
|
|
15
|
+
|
|
8
16
|
## [Unreleased]
|
|
9
17
|
|
|
10
|
-
|
|
18
|
+
## [0.2.0] - 2026-07-26
|
|
19
|
+
|
|
20
|
+
Three things that could each end a run with the wrong verdict: a registry read
|
|
21
|
+
by executing it, a green that attested nothing, and a red about code Attest was
|
|
22
|
+
never pointed at.
|
|
23
|
+
|
|
24
|
+
### Security
|
|
25
|
+
|
|
26
|
+
- **`attest check` described itself as static validation while executing every
|
|
27
|
+
`*.reqs.ts` it found.** Reading a registry meant evaluating the module, via
|
|
28
|
+
Vite, anywhere under the project root — the same threat model as
|
|
29
|
+
`vitest run`, behind wording ("static structural validation (fast CI
|
|
30
|
+
pre-check)") that is exactly what gets a command scheduled ahead of, or
|
|
31
|
+
outside, whatever sandbox the test run is given. On a fork MR, a `*.reqs.ts`
|
|
32
|
+
with a top-level `fetch` was enough to reach CI secrets.
|
|
33
|
+
`check`, `cover` and `render` now read the registry from the **AST** — the
|
|
34
|
+
compiler API was already the parser layer's tool — and run no line of the
|
|
35
|
+
project. `verify` and `archive` still evaluate: they run the whole suite
|
|
36
|
+
anyway, so declining to run one more module would buy them nothing.
|
|
37
|
+
The extracted object literal is handed to the same `RegistrySchema`
|
|
38
|
+
`defineRequirements` calls, so a literal registry keeps its behaviour, its
|
|
39
|
+
codes and its diagnostic wording unchanged. That equivalence is not an
|
|
40
|
+
assertion: a differential suite reads every `*.reqs.ts` in the repo, and every
|
|
41
|
+
literal form that could be misread (`-1`, `1_000`, `0x10`, `30.0`, `1e3`, a
|
|
42
|
+
template literal with no substitution, `as const`, `satisfies`), through both
|
|
43
|
+
readers and requires the two registries to be deeply equal — down to key
|
|
44
|
+
order.
|
|
45
|
+
It also turns `render`'s purity from a convention into a structural
|
|
46
|
+
guarantee: a registry can no longer contain a `Date.now()` that makes
|
|
47
|
+
`render --check` fail forever.
|
|
48
|
+
|
|
49
|
+
### Changed
|
|
50
|
+
|
|
51
|
+
- **BREAKING: a registry must be a literal.** A registry `check`, `cover` or
|
|
52
|
+
`render` cannot read from the source is a new `registry-not-static` ERROR,
|
|
53
|
+
pointing at the line that stopped it. There is deliberately no fallback to
|
|
54
|
+
evaluation — a reader that quietly runs the file when extraction fails
|
|
55
|
+
guarantees nothing — and deliberately no warning period, which would spend
|
|
56
|
+
the whole transition still evaluating. `--eval` is the named way back, on all
|
|
57
|
+
three commands, and restores the old behaviour exactly.
|
|
58
|
+
This makes "a registry is a literal" a design contract rather than a tooling
|
|
59
|
+
limitation: `params: { maxMb: MAX_MB }` imported from application code already
|
|
60
|
+
violates the single-source rule the framework exists to enforce, because the
|
|
61
|
+
value's real owner is elsewhere. Affected repos have two migrations, and
|
|
62
|
+
inlining the value is the one that fixes the underlying problem.
|
|
63
|
+
A registry file may still be written as `const reqs = defineRequirements({…});
|
|
64
|
+
export default reqs;`, and may import `defineRequirements` under any alias or
|
|
65
|
+
through a namespace.
|
|
66
|
+
The `--json` envelope is unchanged: `registry-not-static` is a new `code` on
|
|
67
|
+
the existing `Issue` shape, which is additive, so `schemaVersion` stays `1`.
|
|
68
|
+
`vite` remains a peer dependency — `verify` and `archive` run the suite through
|
|
69
|
+
it — but `check`, `cover` and `render` no longer start a Vite server at all.
|
|
70
|
+
- **BREAKING: a vacuous green stops passing.** `attest verify` on a root with no
|
|
71
|
+
requirements printed `✓ No issues.` and exited `0`, because `passed` is "no
|
|
72
|
+
failing tests" and a run with nothing in it has none. It is now an `empty-spec`
|
|
73
|
+
ERROR, so `ok` becomes false and the exit code follows the existing `hasError`
|
|
74
|
+
path. The empty directory is the harmless version; the one that matters is the
|
|
75
|
+
repo whose registry stopped being found — a renamed folder, a moved root — where
|
|
76
|
+
the tests still run, still pass, and the report is indistinguishable from a real
|
|
77
|
+
green. Deliberately narrower than "no scenario executed": with at least one
|
|
78
|
+
requirement, a missing scenario is already `uncovered-requirement` and a declared
|
|
79
|
+
scenario that never ran is already `declared-not-run`, so firing on those would
|
|
80
|
+
only double-report a run that is red anyway. *Breaking:* a root that attests
|
|
81
|
+
nothing flips from exit 0 to exit 1 — which is the point, since it attested
|
|
82
|
+
nothing.
|
|
83
|
+
- **BREAKING: a run executes only the spec files that declare a `requirement()`.**
|
|
84
|
+
`verify` swept every `*.spec.ts` under the root into a child run with no
|
|
85
|
+
aliases, no DOM and no plugins, so pointing it at the root of a repo that
|
|
86
|
+
already had a suite produced a guaranteed red about code Attest was never asked
|
|
87
|
+
to attest. That was the first thing a mid-project adopter saw, and it was not a
|
|
88
|
+
finding about their intent layer at all. The run scope is now derived from the
|
|
89
|
+
static plan, which already records which files declare intent. `archive` gets
|
|
90
|
+
the same scope, for the same reason: a change must not fail its gate because an
|
|
91
|
+
unrelated suite shares its root. *Breaking:* a repo relying on the sweep to run
|
|
92
|
+
requirement-less specs sees a different set execute — and `attest` was never the
|
|
93
|
+
right thing to run them with. Unchanged for `check`, `cover` and `render`, which
|
|
94
|
+
only ever counted `requirement()`/`scenario()` calls and so never had the bug.
|
|
95
|
+
|
|
96
|
+
### Added
|
|
97
|
+
|
|
98
|
+
- **`verify` reports what it looked at.** The human summary leads with
|
|
99
|
+
`— 20 requirements / 41 scenarios, 0 error, …` and is preceded by the run
|
|
100
|
+
scope (`Running 12 spec files`, plus how many were located and skipped for
|
|
101
|
+
declaring no `requirement()`). `--json` carries the same four numbers under
|
|
102
|
+
`counts`. This is the companion the change above needs: scoping the run means a
|
|
103
|
+
file whose `requirement()` the parser fails to see is now dropped from the run
|
|
104
|
+
rather than failing in it, and the gap between "located" and "in scope" is where
|
|
105
|
+
that would show. It is also a second line of defence against a vacuous green — a
|
|
106
|
+
report that says `0 requirements` cannot be misread as a full pass. `counts` is
|
|
107
|
+
a new optional field on an existing envelope, so `schemaVersion` stays `1`.
|
|
108
|
+
|
|
109
|
+
### Fixed
|
|
110
|
+
|
|
111
|
+
- **The design document described three anti-drift mechanisms; two exist.**
|
|
112
|
+
§6's mechanism 2, "red/green expectation tracking" — a new requirement's
|
|
113
|
+
scenarios must be red while a change is in progress and green at archive time,
|
|
114
|
+
with the framework blocking on a mismatch — was written as shipped behaviour,
|
|
115
|
+
with a strength rating, next to two mechanisms that are real. It was never
|
|
116
|
+
built. What ships is the weaker half: §8's gate requires green at archive time,
|
|
117
|
+
but nothing records or requires the earlier red, so a scenario that was green
|
|
118
|
+
from the start — asserting nothing — passes it. Anyone who budgeted their own
|
|
119
|
+
review against three mechanisms was covered by two. Now marked
|
|
120
|
+
**(not implemented)** inline, with the gap named. Same treatment for
|
|
121
|
+
`/atx:propose` and `/atx:apply` in §9, which are likewise designed and unbuilt;
|
|
122
|
+
the manual path (`changes/<name>/` + `attest archive`) is spelled out beside
|
|
123
|
+
them.
|
|
124
|
+
- **Documentation described an API that does not exist.** §1's architecture
|
|
125
|
+
diagram — the first thing a reader meets — showed `@covers('AUTH-3')` and a
|
|
126
|
+
`covers(id)` back-link, an early spelling of what shipped as
|
|
127
|
+
`requirement(id)` / `scenario(name)`, with coverage reconstructed from the
|
|
128
|
+
`[reqId]` suite name. §5.4 already described the real mechanism, so the
|
|
129
|
+
document contradicted itself. The registry and spec samples also imported from
|
|
130
|
+
a bare `'attest'`, and §10 declared the package as `attest` exporting only
|
|
131
|
+
`.` — the published package is `@am_shork/attest` and has exported `./define`
|
|
132
|
+
since 0.1.2, which is the import path README tells users to write. These are
|
|
133
|
+
samples meant to be copied, and this repo's docs are also fed to agents, so a
|
|
134
|
+
stale name becomes generated code.
|
|
135
|
+
- **Smaller documentation corrections.** §1 called its own three-row table "the
|
|
136
|
+
two core reports"; the Coverage row attributed coverage to runtime collection
|
|
137
|
+
alone, when `check`/`cover` read it statically. §4 listed the SHALL/MUST rule
|
|
138
|
+
under structural validation, where it is enforced by the schema one layer up.
|
|
139
|
+
§5.3's `Issue` sample omitted the `reqId` field the same snippet sets. §7
|
|
140
|
+
cited "§6" for a section of OpenSpec's analysis, not this document's §6.
|
|
141
|
+
The README's `--json` sample claimed `"version": "0.1.2"`, and its
|
|
142
|
+
prerequisites section was headed "Requirements" — a term this project already
|
|
143
|
+
uses for the intent layer.
|
|
144
|
+
- **The feedback template and the GitLab issue template each claimed to be the
|
|
145
|
+
other.** They are not identical, and should not be: `outcome:` is for
|
|
146
|
+
maintainer-authored reports and `verdict:` for outside ones, so the issue form
|
|
147
|
+
is deliberately the outside-reporter subset. Both now say so, the key rule is
|
|
148
|
+
stated where it can be read, and the issue form gained the mid-project
|
|
149
|
+
follow-up its own `adoption: brownfield` field was asking for and never posed.
|
|
150
|
+
|
|
151
|
+
Two checks now hold the parts of this that can be held mechanically, in
|
|
152
|
+
`tests/docs-consistency.spec.ts`: the source cites design sections by number in
|
|
153
|
+
58 places, and every citation must resolve in both languages, so inserting a
|
|
154
|
+
section can no longer silently repoint them; and any JSON sample in the README
|
|
155
|
+
that identifies itself as attest output must carry the current `version` and
|
|
156
|
+
`schemaVersion`. Neither can catch a section that describes something unbuilt —
|
|
157
|
+
that is what the inline marker is for.
|
|
158
|
+
|
|
159
|
+
## Considered and rejected
|
|
160
|
+
|
|
161
|
+
Decisions **not** to build something, kept where they can be found before the
|
|
162
|
+
same candidate is proposed again. Nothing here shipped, so nothing here belongs
|
|
163
|
+
to a release — this section is deliberately outside the version history and does
|
|
164
|
+
not move when one is cut.
|
|
11
165
|
|
|
12
166
|
- **Two gaps in the intent layer: nothing resists a bloated requirement, and
|
|
13
|
-
nothing resists a duplicated one.**
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
`
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
different values
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
*
|
|
59
|
-
embeddings). That reintroduces the fuzzy
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
path
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
- **
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
167
|
+
nothing resists a duplicated one.** The gaps themselves are described in design
|
|
168
|
+
§11, which is where a permanent property of the design belongs. What is kept
|
|
169
|
+
here is the list of mechanisms tried and what killed each, so the same three
|
|
170
|
+
are not proposed again without new evidence. All three were scored against
|
|
171
|
+
every registry this repo ships *before* any was written into the engine — the
|
|
172
|
+
order `divergent-param` was originally designed in the wrong way round. The
|
|
173
|
+
measurement is runnable in `tests/intent-rule-candidates.spec.ts` and fails
|
|
174
|
+
when a requirement is added without a hand-labelled obligation count, because
|
|
175
|
+
the reach figure once quoted here went stale exactly that way.
|
|
176
|
+
*`compound-requirement` (WARNING) — rejected.* More than one RFC-2119 keyword
|
|
177
|
+
in one `statement` is objectively more than one obligation. Measured: of the 6
|
|
178
|
+
requirements in the corpus carrying more than one, it flags 1 (`ATX-10`) and
|
|
179
|
+
misses `ATX-5`, `ATX-9`, `ATX-13`, `ATX-14`, `ATX-17` — every one that packs
|
|
180
|
+
clauses under a single keyword, which is the form the coverage incentive
|
|
181
|
+
actually rewards. A wider variant (keyword plus a coordinating `and`/`while`/
|
|
182
|
+
`;`) reaches 5 of 6 with one wrong hit, so reach is not the deciding argument.
|
|
183
|
+
This is: both variants are silenced by deleting the word that triggered them,
|
|
184
|
+
and neither can tell that deletion from a real split. `SHALL do A and MUST do
|
|
185
|
+
B` clears the warning as `SHALL do A and do B` — same two obligations, one
|
|
186
|
+
fewer normative keyword. A rule whose cheapest fix degrades the artifact it
|
|
187
|
+
protects does not ship.
|
|
188
|
+
*`divergent-param` (WARNING) — rejected.* One param name declared with
|
|
189
|
+
different values in two requirements sharing an id prefix. Measured: it fires
|
|
190
|
+
twice on this repo and both firings are wrong — `idleTimeoutMin` (`15` in
|
|
191
|
+
`ATX-3`, `30` in `ATX-10`), unrelated illustrative fixtures, and `code`
|
|
192
|
+
(`registry-not-static` in `ATX-17`, `empty-spec` in `ATX-18`), two diagnostics
|
|
193
|
+
that could not possibly agree. The second arrived on its own when `ATX-18` was
|
|
194
|
+
added, which is the more damning half: a generic param name collides more often
|
|
195
|
+
as a registry grows, so the false-positive rate rises with adoption — the
|
|
196
|
+
opposite of what a shippable rule does. Still zero true positives across 24
|
|
197
|
+
requirements. The escape hatch it was blocked on has since been designed
|
|
198
|
+
and does not rescue it: with no config file the only workable shape is an
|
|
199
|
+
additive `independentParams?: string[]` on the requirement (a CLI flag is
|
|
200
|
+
per-invocation for what is a permanent property of two requirements; changing
|
|
201
|
+
the `params` shape rejects every existing registry). Viable, but it is
|
|
202
|
+
permanent schema surface for a rule with no demonstrated true positive, and in
|
|
203
|
+
practice it would be written *after* the warning fires — a one-line silencer.
|
|
204
|
+
*Countable obligations (breaking) — rejected.* An `obligations: string[]` per
|
|
205
|
+
requirement, with coverage counted per obligation, removing the lumping
|
|
206
|
+
incentive at the source instead of detecting it afterwards. Rejected because
|
|
207
|
+
the **id is already that unit**: splitting into two ids costs two scenarios and
|
|
208
|
+
buys the same incentive with no new concept (`ATX-11`/`ATX-12` are exactly that
|
|
209
|
+
split). It would touch the parser, coverage, `render`, `SPEC.md`, delta apply
|
|
210
|
+
and the `--json` schema, and it relocates the incentive rather than removing
|
|
211
|
+
it — nothing can force the array to be complete.
|
|
212
|
+
*Not considered:* similarity matching between statements (token overlap,
|
|
213
|
+
embeddings). That reintroduces the fuzzy comparison design §0 exists to remove;
|
|
214
|
+
real semantic duplication needs judgement and belongs to human review at the
|
|
215
|
+
propose stage.
|
|
216
|
+
|
|
217
|
+
## [0.1.7] - 2026-07-26
|
|
218
|
+
|
|
219
|
+
A change name, a prototype key and a collation quirk each got to decide a
|
|
220
|
+
verdict; none of them do now.
|
|
221
|
+
|
|
222
|
+
### Security
|
|
223
|
+
|
|
224
|
+
- **`attest archive <change>` validated the change name only by using it.** The
|
|
225
|
+
name was interpolated straight into
|
|
226
|
+
`changes/<name>/requirements.delta.ts`, and that module is *executed* — while
|
|
227
|
+
path joining normalises `..`, so `attest archive '../../../tmp/evil'` loaded
|
|
228
|
+
and ran a module from outside `changes/` entirely. Harmless when you type it
|
|
229
|
+
yourself; not harmless the moment the name comes from a branch name or an MR
|
|
230
|
+
title, which is the natural way to wire the gate into CI. The name is now
|
|
231
|
+
checked **before a loader is even created** — a name already known to be
|
|
232
|
+
invalid must not cause a single resolution attempt — and rejected with a new
|
|
233
|
+
`invalid-change-name` ERROR.
|
|
234
|
+
The test is path safety, not a character whitelist: empty, `.`, `..`, or
|
|
235
|
+
anything containing a path separator or NUL is refused, and every other name a
|
|
236
|
+
directory may legitimately carry is admitted. A whitelist would have cost
|
|
237
|
+
adopters real change directories (`feat(auth)`, a name with a space) to buy no
|
|
238
|
+
additional safety, because the change name never reaches a glob.
|
|
239
|
+
- **A sibling proposal with a glob metacharacter in its name silently widened
|
|
240
|
+
the archive gate.** The globs that keep *other* proposals out of a gate run
|
|
241
|
+
were built by pasting directory names read from disk straight into
|
|
242
|
+
`**/changes/<name>/**`. A change called `feat(auth)` is a *pattern*, so it
|
|
243
|
+
matched nothing, and that proposal's specs joined the run — quietly enlarging
|
|
244
|
+
the scope of the single check that decides whether a change is done. Sibling
|
|
245
|
+
names are now escaped.
|
|
246
|
+
|
|
247
|
+
### Fixed
|
|
248
|
+
|
|
249
|
+
- **A statement placeholder could bind to a prototype method instead of a
|
|
250
|
+
param.** Both the validator and the renderer probed with `name in req.params`,
|
|
251
|
+
and `'toString' in {}` is `true`. So `{toString}` in a statement looked bound:
|
|
252
|
+
the `unbound-param` ERROR that exists to stop an undeclared placeholder never
|
|
253
|
+
fired, and `render` then interpolated
|
|
254
|
+
`function toString() { [native code] }` into the document that reviewers and
|
|
255
|
+
audit read as the system's promise. Both now use `Object.hasOwn`, so the
|
|
256
|
+
intent layer resolves only to what an author actually wrote. An own param
|
|
257
|
+
whose name happens to shadow a prototype key keeps working.
|
|
258
|
+
*Behaviour change:* a registry with such a statement newly fails `check`, and
|
|
259
|
+
its rendering changes (the placeholder is now left verbatim), which
|
|
260
|
+
`render --check` reports as stale. Both are the reports that should always
|
|
261
|
+
have been there.
|
|
262
|
+
- **The canonical form used for content comparison encoded the order params
|
|
263
|
+
happened to be written in.** `applyDelta` sorted param keys with
|
|
264
|
+
`localeCompare` before comparing, and `localeCompare` returns `0` for strings
|
|
265
|
+
that are *distinct* — a precomposed `ä` against its combining-mark spelling.
|
|
266
|
+
`Array#sort` is stable, so keys it called equal kept their insertion order,
|
|
267
|
+
and two requirements with identical content but differently ordered params
|
|
268
|
+
canonicalised differently: `add-conflict` reporting a requirement as
|
|
269
|
+
conflicting with an identical copy of itself. Which strings collate equal is
|
|
270
|
+
ICU-dependent on top of that, so the answer also varied with the Node build.
|
|
271
|
+
Ordering now goes through one shared code-unit comparator (`src/core/order.ts`),
|
|
272
|
+
which `render`'s id ordering — already code-unit for the same reason — now
|
|
273
|
+
shares.
|
|
274
|
+
- **The loader never removed its scratch directory.** Every `attest`
|
|
275
|
+
invocation created one under the OS temp dir to hold the vitest stub, and
|
|
276
|
+
`close()` shut the Vite server without deleting it: one directory per CI
|
|
277
|
+
build, accumulating forever. `close()` now removes it, and is idempotent.
|
|
278
|
+
|
|
279
|
+
### Changed
|
|
280
|
+
|
|
281
|
+
- **`check` walked the project tree twice and `archive` four times**, over a
|
|
282
|
+
tree that cannot change in between. One traversal now collects registry and
|
|
283
|
+
spec files together (`scanProject`). Directory recursion, spec reads, and
|
|
284
|
+
registry module evaluation all run concurrently; results are folded in sorted
|
|
285
|
+
file order, so reports stay byte-identical regardless of I/O timing. The
|
|
286
|
+
drift heuristic indexes scenarios once instead of re-filtering the whole plan
|
|
287
|
+
per requirement, which was the product that grows fastest in exactly the repos
|
|
288
|
+
where `check` needs to stay fast.
|
|
289
|
+
- **`verify` and `archive` now share one `declared-not-run` implementation.**
|
|
290
|
+
Each owned a verbatim copy, so the two verdicts agreed only by transcription —
|
|
291
|
+
in a tool whose entire argument is that agreement should be structural rather
|
|
292
|
+
than clerical.
|
|
293
|
+
- `attest <cmd> [dir]` now resolves `[dir]` to an absolute path. A relative one
|
|
294
|
+
previously worked only because Vite happens to default its own root to the
|
|
295
|
+
current directory.
|
|
296
|
+
- `render --out` writes through a temporary file and renames, so an interrupted
|
|
297
|
+
write can no longer leave a truncated document in a file that is meant to be
|
|
298
|
+
committed.
|
|
299
|
+
|
|
300
|
+
No `schemaVersion` change: the report envelope and the `Issue` shape are
|
|
301
|
+
untouched, and `invalid-change-name` is a new `code` value, which is additive.
|
|
94
302
|
|
|
95
303
|
## [0.1.6] - 2026-07-25
|
|
96
304
|
|
|
@@ -326,7 +534,9 @@ Initial release.
|
|
|
326
534
|
(MIT), whose four-stage engine and diff-first change model Attest's
|
|
327
535
|
architecture is adapted from (re-implemented from scratch, no source copied).
|
|
328
536
|
|
|
329
|
-
[Unreleased]: https://gitlab.com/Pseudorca/attest/-/compare/v0.
|
|
537
|
+
[Unreleased]: https://gitlab.com/Pseudorca/attest/-/compare/v0.2.0...main
|
|
538
|
+
[0.2.0]: https://gitlab.com/Pseudorca/attest/-/tags/v0.2.0
|
|
539
|
+
[0.1.7]: https://gitlab.com/Pseudorca/attest/-/tags/v0.1.7
|
|
330
540
|
[0.1.6]: https://gitlab.com/Pseudorca/attest/-/tags/v0.1.6
|
|
331
541
|
[0.1.5]: https://gitlab.com/Pseudorca/attest/-/tags/v0.1.5
|
|
332
542
|
[0.1.4]: https://gitlab.com/Pseudorca/attest/-/tags/v0.1.4
|
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@ A TDD-native spec framework. **Tests are the source of truth for verification;
|
|
|
4
4
|
ID-bound requirements are the source of truth for intent.** Attest binds the two
|
|
5
5
|
by a stable ID and continuously detects drift.
|
|
6
6
|
|
|
7
|
-
- **Coverage** — does every requirement have ≥1 scenario? (from
|
|
7
|
+
- **Coverage** — does every requirement have ≥1 scenario? (statically, or from the run)
|
|
8
8
|
- **Result** — is every test green? (from the test runner)
|
|
9
9
|
- **Drift** — do intent and assertions still agree? (static + runtime cross-check)
|
|
10
10
|
|
|
@@ -16,7 +16,7 @@ sets) get the same single source as a lone number.
|
|
|
16
16
|
|
|
17
17
|
See the authoritative design — [English](https://gitlab.com/Pseudorca/attest/-/blob/main/docs/en/attest-design.md) · [中文](https://gitlab.com/Pseudorca/attest/-/blob/main/docs/zh/attest-design.md) — and the rest of the [docs](https://gitlab.com/Pseudorca/attest/-/tree/main/docs).
|
|
18
18
|
|
|
19
|
-
##
|
|
19
|
+
## Prerequisites
|
|
20
20
|
|
|
21
21
|
- Node ≥ 20.19
|
|
22
22
|
- pnpm, plus `vitest` + `vite` (peer dependencies)
|
|
@@ -30,7 +30,9 @@ pnpm add -D @am_shork/attest vitest vite
|
|
|
30
30
|
```
|
|
31
31
|
|
|
32
32
|
**1. Declare a requirement** (`requirements/auth.reqs.ts`) — import the intent API
|
|
33
|
-
from the vitest-free `/define` subpath so registry loading never touches the runtime
|
|
33
|
+
from the vitest-free `/define` subpath so registry loading never touches the runtime.
|
|
34
|
+
A registry is a **literal**: every value is written where you can read it (see
|
|
35
|
+
[A registry is a literal](#a-registry-is-a-literal)):
|
|
34
36
|
|
|
35
37
|
```ts
|
|
36
38
|
import { defineRequirements } from '@am_shork/attest/define';
|
|
@@ -67,12 +69,52 @@ requirement('AUTH-3', () => {
|
|
|
67
69
|
|
|
68
70
|
```bash
|
|
69
71
|
attest check # static: orphan tests, uncovered requirements, unbound params
|
|
72
|
+
# (reads your registry; runs none of your code)
|
|
70
73
|
attest verify # run tests + coverage + drift, graded report
|
|
74
|
+
# (runs only the spec files that declare a requirement())
|
|
71
75
|
attest cover # which requirements lack a scenario
|
|
72
76
|
attest render # the requirements as Markdown, for people who don't read TS
|
|
73
77
|
attest archive <change> # gate a proposed change: green + covered + no drift
|
|
74
78
|
```
|
|
75
79
|
|
|
80
|
+
`<change>` names one directory inside `changes/` — it cannot be empty, `.`,
|
|
81
|
+
`..`, or contain a path separator, since the gate loads and runs that change's
|
|
82
|
+
delta. Any other directory name is fine.
|
|
83
|
+
|
|
84
|
+
### A registry is a literal
|
|
85
|
+
|
|
86
|
+
`check`, `cover` and `render` read your `*.reqs.ts` files from their **source**,
|
|
87
|
+
with the TypeScript compiler API. They never execute them. That is what makes
|
|
88
|
+
`attest check` safe to run where its description implies you can — first in the
|
|
89
|
+
pipeline, on an untrusted fork MR, outside whatever sandbox you give the tests.
|
|
90
|
+
(`verify` and `archive` do run your suite, so they read the registry by
|
|
91
|
+
evaluating it: there is nothing left to protect at that point.)
|
|
92
|
+
|
|
93
|
+
The cost is a contract: every value in a registry must be written in the
|
|
94
|
+
registry.
|
|
95
|
+
|
|
96
|
+
```ts
|
|
97
|
+
import { MAX_MB } from '../src/config.js';
|
|
98
|
+
|
|
99
|
+
export default defineRequirements({
|
|
100
|
+
'UP-1': {
|
|
101
|
+
statement: 'An upload SHALL be rejected above {maxMb} MB.',
|
|
102
|
+
rationale: 'Protect the storage tier from unbounded writes.',
|
|
103
|
+
params: { maxMb: MAX_MB }, // ✗ registry-not-static
|
|
104
|
+
},
|
|
105
|
+
});
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
This is reported as a `registry-not-static` ERROR pointing at the line. It is
|
|
109
|
+
not only a tooling limit: the single-source rule says the requirement owns that
|
|
110
|
+
number, and importing it says the application owns it. Inline the value and read
|
|
111
|
+
it from the registry in your code — that is the fix. If you need the old
|
|
112
|
+
behaviour instead, `--eval` restores it on all three commands:
|
|
113
|
+
|
|
114
|
+
```bash
|
|
115
|
+
attest check --eval # reads the registry by executing every *.reqs.ts
|
|
116
|
+
```
|
|
117
|
+
|
|
76
118
|
### A readable spec for humans (`attest render`)
|
|
77
119
|
|
|
78
120
|
The intent layer lives in TypeScript, which means a reviewer, a QA engineer, or
|
|
@@ -110,6 +152,31 @@ Three properties this deliberately keeps:
|
|
|
110
152
|
The Markdown is prose for humans and its formatting is not a stable contract —
|
|
111
153
|
don't parse it. `--json` is the machine surface.
|
|
112
154
|
|
|
155
|
+
### What a run actually runs
|
|
156
|
+
|
|
157
|
+
`verify` executes **only the spec files that declare a `requirement()`**. A repo
|
|
158
|
+
that already has a test suite keeps it: those files are located, reported, and
|
|
159
|
+
left alone — Attest is not a general test runner, and running someone's suite in
|
|
160
|
+
a child process with no aliases and no DOM only ever produced a red about code it
|
|
161
|
+
was never pointed at. `archive` uses the same scope.
|
|
162
|
+
|
|
163
|
+
Every run says so, before the issues:
|
|
164
|
+
|
|
165
|
+
```
|
|
166
|
+
Running 12 spec files (30 without a requirement(), not run)
|
|
167
|
+
✓ No issues.
|
|
168
|
+
— 20 requirements / 41 scenarios, 0 error, 0 warning, 0 info
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
The gap between the two numbers is worth watching: a spec file whose
|
|
172
|
+
`requirement()` call Attest cannot see statically is skipped rather than failed,
|
|
173
|
+
and that is where it shows. The same four numbers are in `--json` under `counts`.
|
|
174
|
+
|
|
175
|
+
A run with **no requirements** under the root is an `empty-spec` ERROR, not a
|
|
176
|
+
pass. "No failing tests" is not the same claim as "the requirements hold", and
|
|
177
|
+
the case that matters is not the empty directory — it is the repo whose registry
|
|
178
|
+
quietly stopped being found, where the tests still run and still go green.
|
|
179
|
+
|
|
113
180
|
### Framework code (`.vue`, DOM, path aliases)
|
|
114
181
|
|
|
115
182
|
The child run isolates from any ambient config by default, so specs run in a bare
|
|
@@ -141,7 +208,7 @@ attest check --json
|
|
|
141
208
|
{
|
|
142
209
|
"tool": "attest",
|
|
143
210
|
"schemaVersion": 1,
|
|
144
|
-
"version": "0.
|
|
211
|
+
"version": "0.2.0",
|
|
145
212
|
"command": "check",
|
|
146
213
|
"ok": false,
|
|
147
214
|
"summary": { "error": 1, "warning": 0, "info": 0 },
|
|
@@ -159,7 +226,8 @@ attest check --json
|
|
|
159
226
|
```
|
|
160
227
|
|
|
161
228
|
`verify` adds `passed` (was the test run itself green, independent of the
|
|
162
|
-
overall verdict)
|
|
229
|
+
overall verdict) and `counts` (`requirements`, `scenarios`, `specFiles`,
|
|
230
|
+
`attesting`); `cover` adds `coverage[]` — one `{ reqId, covered,
|
|
163
231
|
scenarioCount }` row per requirement — plus a `requirements` roll-up;
|
|
164
232
|
`archive` adds `change`; `render` adds `outFile` (and never the document itself).
|
|
165
233
|
`schemaVersion` is bumped on any breaking change to
|