@am_shork/attest 0.1.7 → 0.2.1
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 +372 -110
- package/README.md +83 -6
- package/dist/cli/index.js +17 -9
- 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/locate.d.ts +33 -4
- package/dist/core/locate.d.ts.map +1 -1
- package/dist/core/locate.js +73 -34
- package/dist/core/locate.js.map +1 -1
- package/dist/core/pipeline.d.ts +29 -6
- package/dist/core/pipeline.d.ts.map +1 -1
- package/dist/core/pipeline.js +101 -54
- package/dist/core/pipeline.js.map +1 -1
- package/dist/core/registry.d.ts +51 -2
- package/dist/core/registry.d.ts.map +1 -1
- package/dist/core/registry.js +3 -0
- package/dist/core/registry.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 +3 -0
- package/dist/core/validator.d.ts.map +1 -1
- package/dist/core/validator.js +26 -0
- package/dist/core/validator.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/package.json +2 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,118 +5,378 @@ 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
|
-
### Under consideration
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
18
|
+
### Under consideration
|
|
19
|
+
|
|
20
|
+
Queued from the skills-manager adoption report (0.2.0, 41 requirements / 106
|
|
21
|
+
scenarios, plain-TS). **Nothing below is built.** An entry here is a decision to
|
|
22
|
+
ship, not a claim that it ships; each moves into `Added` / `Changed` / `Fixed`
|
|
23
|
+
when it does, and this heading is deliberately not one of those, so no reader
|
|
24
|
+
can take the section for a release note. The report's other proposals were
|
|
25
|
+
declined and are not recorded.
|
|
26
|
+
|
|
27
|
+
- **A report of N issues sharing one `code` should say so once.** The 0.1.1 →
|
|
28
|
+
0.2.0 upgrade printed six `registry-not-static` ERRORs in a row — one per
|
|
29
|
+
registry file, every one caused by the same prose written as `'…' + '…'` — and
|
|
30
|
+
left the reader to work out that they were one cause and one mechanical fix.
|
|
31
|
+
A summary line will name the shared cause when a code repeats; the per-file
|
|
32
|
+
lines stay exactly as they are, because they are what makes a diagnostic
|
|
33
|
+
clickable. Human output only — a summary that became an `Issue` would carry a
|
|
34
|
+
`code` into `--json`, `summary` and `hasError`, which is a report-contract
|
|
35
|
+
change and not what this is.
|
|
36
|
+
|
|
37
|
+
## [0.2.1] - 2026-07-26
|
|
38
|
+
|
|
39
|
+
The type layer stopped charging for the one behaviour the tool rewards, and
|
|
40
|
+
three limits an adoption had to find on its own are now written down.
|
|
41
|
+
|
|
42
|
+
### Added
|
|
43
|
+
|
|
44
|
+
- **Reading a param cost a cast, and the cast was charged on the one behaviour
|
|
45
|
+
the tool rewards.** `defineRequirements` returned `Registry`, so every param
|
|
46
|
+
was the full `string | number | boolean | Array<…>` union and
|
|
47
|
+
`reqs[id].params.x` had to be converged at each read site — the reporting
|
|
48
|
+
adopter wrote `str()` / `strList()` helpers to do it. `possible-drift` goes
|
|
49
|
+
quiet precisely when a scenario reads `.params`, so the type layer was taxing
|
|
50
|
+
the behaviour the anti-drift heuristic exists to reward. It is now generic
|
|
51
|
+
over its argument with a `const` type parameter: `params.idleTimeoutMin` is
|
|
52
|
+
`30`, `params.vendorDirs` is an array of the strings written there, and a
|
|
53
|
+
declared ID is known to exist, so `reqs['AUTH-3']` needs no `!` under
|
|
54
|
+
`noUncheckedIndexedAccess`. Removing the casts from Attest's own `self/`
|
|
55
|
+
specs closed 21 latent type errors there. No runtime behaviour and no `--json`
|
|
56
|
+
change: since 0.2.0 the analysing commands read the registry from the AST, so
|
|
57
|
+
this type has no bearing on anything the engine sees.
|
|
58
|
+
**A widening, deliberately.** A `const` type parameter would infer array
|
|
59
|
+
literals as tuples — `readonly` ones under a constraint that admits them —
|
|
60
|
+
and neither is assignable to the mutable `Array<number | string | boolean>`
|
|
61
|
+
the schema parses to, which would have broken `const reqs: Registry =
|
|
62
|
+
defineRequirements({…})` and every `const xs: string[] = req.params.foo`; the
|
|
63
|
+
mapped type collapses them back to `E[]`. The result is still intersected with
|
|
64
|
+
`Registry`, so its index signature survives and `reqs[id]` with an `id: string`
|
|
65
|
+
— a shared test helper — still resolves, as does a param the registry does not
|
|
66
|
+
declare. **What that costs**, against the intent recorded when this was queued:
|
|
67
|
+
a mistyped literal `reqs['TYPO-9']` is *not* rejected outright. Rejecting an
|
|
68
|
+
unknown key and accepting a `string` one are the same question asked of one
|
|
69
|
+
index signature, and it cannot answer both ways; the fallback is what keeps
|
|
70
|
+
this non-breaking, so it wins. The weaker half is real — an unknown ID misses
|
|
71
|
+
the declared keys and resolves through the index signature, so under
|
|
72
|
+
`noUncheckedIndexedAccess` reading through it is an error where `reqs['AUTH-3']`
|
|
73
|
+
no longer is — and a wrong ID is an `unknown-req-id` ERROR from `check`
|
|
74
|
+
regardless, which reads registry and specs together. The type layer is the
|
|
75
|
+
second reader here, not the only one.
|
|
76
|
+
- **Three things an adoption had to discover for itself.** Documentation only —
|
|
77
|
+
no behaviour changed, and each is a property the design deliberately has.
|
|
78
|
+
**`check` has no detection power over a params change**: editing a value
|
|
79
|
+
(`'main'` → `'trunk'`, an entry dropped from a nine-element list) leaves it at
|
|
80
|
+
`✓ No issues`, because `check` executes nothing and nothing became unbound or
|
|
81
|
+
uncovered. The single source makes the value impossible to *diverge*, which is
|
|
82
|
+
a different and stronger claim than announcing that it *moved* — but "params
|
|
83
|
+
are the single source for values that change" reads easily as "`check` guards
|
|
84
|
+
them", and what actually guards them is a scenario asserting on the value it
|
|
85
|
+
read. Now stated where params are introduced (README, design §2, the prompt
|
|
86
|
+
guide) and recorded as a limit in design §11, next to the others.
|
|
87
|
+
**`verify` does not read your `vitest.config.ts`**: the child run starts
|
|
88
|
+
isolated, so a verdict never depends on ambient configuration — which also
|
|
89
|
+
means framework code gets no transforms, no DOM and no aliases until
|
|
90
|
+
`--vitest-config` points it at one. The flag was documented in the README; the
|
|
91
|
+
trade-off behind it now sits in design §5.4, where the run scope is described,
|
|
92
|
+
because that is where a reader meets the isolation.
|
|
93
|
+
**A template literal keeps its whitespace**: one with no substitution reads
|
|
94
|
+
statically and is accepted, but preserves every newline and space of
|
|
95
|
+
indentation, and `render` copies the statement into the Markdown as written —
|
|
96
|
+
so editor-friendly wrapping lands in the generated document. This is the
|
|
97
|
+
reason registry prose is written as one long single-line string, which was
|
|
98
|
+
folklore until now.
|
|
99
|
+
- **A codemod for the 0.2.0 `registry-not-static` migration.** Collapsing
|
|
100
|
+
multi-line `'…' + '…'` prose into one literal is mechanical and was done by
|
|
101
|
+
hand across 21 requirements in the first reported adoption. The 0.2.0 entry
|
|
102
|
+
now carries a compiler-API script that does it, tested on the shapes that make
|
|
103
|
+
it awkward: mixed quote styles, an apostrophe inside the prose, a template
|
|
104
|
+
literal in the chain, and a numeric `1 + 2` it must leave alone.
|
|
105
|
+
- **Type tests, and a suite that runs them.** The repo had none, and this change
|
|
106
|
+
is entirely type-level: no runtime test and no `verify self` run could observe
|
|
107
|
+
a regression in it. `tests/registry-types.spec-d.ts` pins the narrowing, both
|
|
108
|
+
compatibility promises and the trade-off above; `vitest run` now typechecks in
|
|
109
|
+
the same pass (`tsconfig.typecheck.json`, which unlike the build project covers
|
|
110
|
+
`tests/` and `self/` — neither had ever been through a compiler). `DefinedRegistry`
|
|
111
|
+
is exported for consumers that need to name the type.
|
|
112
|
+
|
|
113
|
+
- **A `{placeholder}` in a `rationale` reached the document unread.** `render`
|
|
114
|
+
interpolates `statement` and nothing else, and `unbound-param` only scans
|
|
115
|
+
statements, so braces written into a rationale passed through every check and
|
|
116
|
+
landed verbatim in the generated Markdown. The mistyped placeholder is the
|
|
117
|
+
obvious case; the one that misleads is the **correctly spelled** one, where
|
|
118
|
+
the author declared the param and simply picked the wrong field — nothing was
|
|
119
|
+
unbound, so nothing fired, and the document reviewers and audit read showed a
|
|
120
|
+
placeholder where a value was meant. A new `rationale-placeholder` WARNING now
|
|
121
|
+
reports any `{name}` in a rationale, bound or not.
|
|
122
|
+
Deliberately not the two alternatives. Extending `unbound-param` to rationales
|
|
123
|
+
would cover only the undeclared half — the half that misleads least. And
|
|
124
|
+
making `render` interpolate rationales would change the bytes of every
|
|
125
|
+
committed rendering, turning `render --check` red on an upgrade where the user
|
|
126
|
+
moved no intent, which this file already treats as breaking for that surface.
|
|
127
|
+
**A placeholder inside a Markdown code span is not reported.** That exclusion
|
|
128
|
+
came from dogfooding: the first run against Attest's own registry flagged
|
|
129
|
+
ATX-14, whose rationale explains that `` `{toString}` `` *looks* bound — prose
|
|
130
|
+
about a placeholder, not a misfiled one, and the only way to write a
|
|
131
|
+
requirement whose subject is the syntax itself. A rationale is Markdown and
|
|
132
|
+
`render` leaves a code span exactly as written, so a backticked token is
|
|
133
|
+
already marked as quoted.
|
|
134
|
+
WARNING sits below the `hasError` predicate that `ok`, `--json` and the exit
|
|
135
|
+
code all read from, so an existing green registry with braces in a rationale
|
|
136
|
+
gains a line of output and keeps its verdict. A new `code` on the existing
|
|
137
|
+
`Issue` shape is additive: `schemaVersion` stays `1`.
|
|
138
|
+
|
|
139
|
+
## [0.2.0] - 2026-07-26
|
|
140
|
+
|
|
141
|
+
Three things that could each end a run with the wrong verdict: a registry read
|
|
142
|
+
by executing it, a green that attested nothing, and a red about code Attest was
|
|
143
|
+
never pointed at.
|
|
144
|
+
|
|
145
|
+
### Security
|
|
146
|
+
|
|
147
|
+
- **`attest check` described itself as static validation while executing every
|
|
148
|
+
`*.reqs.ts` it found.** Reading a registry meant evaluating the module, via
|
|
149
|
+
Vite, anywhere under the project root — the same threat model as
|
|
150
|
+
`vitest run`, behind wording ("static structural validation (fast CI
|
|
151
|
+
pre-check)") that is exactly what gets a command scheduled ahead of, or
|
|
152
|
+
outside, whatever sandbox the test run is given. On a fork MR, a `*.reqs.ts`
|
|
153
|
+
with a top-level `fetch` was enough to reach CI secrets.
|
|
154
|
+
`check`, `cover` and `render` now read the registry from the **AST** — the
|
|
155
|
+
compiler API was already the parser layer's tool — and run no line of the
|
|
156
|
+
project. `verify` and `archive` still evaluate: they run the whole suite
|
|
157
|
+
anyway, so declining to run one more module would buy them nothing.
|
|
158
|
+
The extracted object literal is handed to the same `RegistrySchema`
|
|
159
|
+
`defineRequirements` calls, so a literal registry keeps its behaviour, its
|
|
160
|
+
codes and its diagnostic wording unchanged. That equivalence is not an
|
|
161
|
+
assertion: a differential suite reads every `*.reqs.ts` in the repo, and every
|
|
162
|
+
literal form that could be misread (`-1`, `1_000`, `0x10`, `30.0`, `1e3`, a
|
|
163
|
+
template literal with no substitution, `as const`, `satisfies`), through both
|
|
164
|
+
readers and requires the two registries to be deeply equal — down to key
|
|
165
|
+
order.
|
|
166
|
+
It also turns `render`'s purity from a convention into a structural
|
|
167
|
+
guarantee: a registry can no longer contain a `Date.now()` that makes
|
|
168
|
+
`render --check` fail forever.
|
|
169
|
+
|
|
170
|
+
### Changed
|
|
171
|
+
|
|
172
|
+
- **BREAKING: a registry must be a literal.** A registry `check`, `cover` or
|
|
173
|
+
`render` cannot read from the source is a new `registry-not-static` ERROR,
|
|
174
|
+
pointing at the line that stopped it. There is deliberately no fallback to
|
|
175
|
+
evaluation — a reader that quietly runs the file when extraction fails
|
|
176
|
+
guarantees nothing — and deliberately no warning period, which would spend
|
|
177
|
+
the whole transition still evaluating. `--eval` is the named way back, on all
|
|
178
|
+
three commands, and restores the old behaviour exactly.
|
|
179
|
+
This makes "a registry is a literal" a design contract rather than a tooling
|
|
180
|
+
limitation: `params: { maxMb: MAX_MB }` imported from application code already
|
|
181
|
+
violates the single-source rule the framework exists to enforce, because the
|
|
182
|
+
value's real owner is elsewhere. Affected repos have two migrations, and
|
|
183
|
+
inlining the value is the one that fixes the underlying problem.
|
|
184
|
+
A registry file may still be written as `const reqs = defineRequirements({…});
|
|
185
|
+
export default reqs;`, and may import `defineRequirements` under any alias or
|
|
186
|
+
through a namespace.
|
|
187
|
+
**Migrating concatenated prose.** The common shape is a statement or rationale
|
|
188
|
+
wrapped across lines as `'…' + '…'`, which is a computation and so no longer
|
|
189
|
+
reads. Collapsing it is purely mechanical — 21 requirements by hand in the
|
|
190
|
+
first reported adoption — so it is worth a codemod. Save this at the project
|
|
191
|
+
root and run it over the registry files; it rewrites only concatenations whose
|
|
192
|
+
every operand is a string (or substitution-free template) literal, leaving
|
|
193
|
+
`1 + 2` and anything with an identifier in it alone:
|
|
194
|
+
|
|
195
|
+
```js
|
|
196
|
+
// flatten-reqs.mjs — node flatten-reqs.mjs requirements/*.reqs.ts
|
|
197
|
+
import ts from 'typescript';
|
|
198
|
+
import { readFileSync, writeFileSync } from 'node:fs';
|
|
199
|
+
|
|
200
|
+
const flat = (n) =>
|
|
201
|
+
ts.isBinaryExpression(n) && n.operatorToken.kind === ts.SyntaxKind.PlusToken
|
|
202
|
+
? [flat(n.left), flat(n.right)].every(Boolean) && flat(n.left) + flat(n.right)
|
|
203
|
+
: (ts.isStringLiteral(n) || ts.isNoSubstitutionTemplateLiteral(n)) && n.text;
|
|
204
|
+
|
|
205
|
+
for (const file of process.argv.slice(2)) {
|
|
206
|
+
const src = readFileSync(file, 'utf8');
|
|
207
|
+
const sf = ts.createSourceFile(file, src, ts.ScriptTarget.Latest, true);
|
|
208
|
+
const edits = [];
|
|
209
|
+
const visit = (n) => {
|
|
210
|
+
const text = flat(n);
|
|
211
|
+
// the whole chain is one edit, so do not descend into it
|
|
212
|
+
if (text !== false && ts.isBinaryExpression(n)) return void edits.push([n.getStart(sf), n.getEnd(), text]);
|
|
213
|
+
n.forEachChild(visit);
|
|
214
|
+
};
|
|
215
|
+
visit(sf);
|
|
216
|
+
let out = src;
|
|
217
|
+
for (const [start, end, text] of edits.reverse())
|
|
218
|
+
out = `${out.slice(0, start)}${JSON.stringify(text)}${out.slice(end)}`;
|
|
219
|
+
if (edits.length) writeFileSync(file, out), console.log(`${file}: ${edits.length} collapsed`);
|
|
220
|
+
}
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
It emits double-quoted strings (`JSON.stringify` does the escaping, so prose
|
|
224
|
+
containing an apostrophe survives); run the formatter afterwards. Write the
|
|
225
|
+
result as one long line rather than re-wrapping it — a template literal reads
|
|
226
|
+
statically but keeps its newlines and indentation, and `render` copies the
|
|
227
|
+
statement into the Markdown exactly as written.
|
|
228
|
+
The `--json` envelope is unchanged: `registry-not-static` is a new `code` on
|
|
229
|
+
the existing `Issue` shape, which is additive, so `schemaVersion` stays `1`.
|
|
230
|
+
`vite` remains a peer dependency — `verify` and `archive` run the suite through
|
|
231
|
+
it — but `check`, `cover` and `render` no longer start a Vite server at all.
|
|
232
|
+
- **BREAKING: a vacuous green stops passing.** `attest verify` on a root with no
|
|
233
|
+
requirements printed `✓ No issues.` and exited `0`, because `passed` is "no
|
|
234
|
+
failing tests" and a run with nothing in it has none. It is now an `empty-spec`
|
|
235
|
+
ERROR, so `ok` becomes false and the exit code follows the existing `hasError`
|
|
236
|
+
path. The empty directory is the harmless version; the one that matters is the
|
|
237
|
+
repo whose registry stopped being found — a renamed folder, a moved root — where
|
|
238
|
+
the tests still run, still pass, and the report is indistinguishable from a real
|
|
239
|
+
green. Deliberately narrower than "no scenario executed": with at least one
|
|
240
|
+
requirement, a missing scenario is already `uncovered-requirement` and a declared
|
|
241
|
+
scenario that never ran is already `declared-not-run`, so firing on those would
|
|
242
|
+
only double-report a run that is red anyway. *Breaking:* a root that attests
|
|
243
|
+
nothing flips from exit 0 to exit 1 — which is the point, since it attested
|
|
244
|
+
nothing.
|
|
245
|
+
- **BREAKING: a run executes only the spec files that declare a `requirement()`.**
|
|
246
|
+
`verify` swept every `*.spec.ts` under the root into a child run with no
|
|
247
|
+
aliases, no DOM and no plugins, so pointing it at the root of a repo that
|
|
248
|
+
already had a suite produced a guaranteed red about code Attest was never asked
|
|
249
|
+
to attest. That was the first thing a mid-project adopter saw, and it was not a
|
|
250
|
+
finding about their intent layer at all. The run scope is now derived from the
|
|
251
|
+
static plan, which already records which files declare intent. `archive` gets
|
|
252
|
+
the same scope, for the same reason: a change must not fail its gate because an
|
|
253
|
+
unrelated suite shares its root. *Breaking:* a repo relying on the sweep to run
|
|
254
|
+
requirement-less specs sees a different set execute — and `attest` was never the
|
|
255
|
+
right thing to run them with. Unchanged for `check`, `cover` and `render`, which
|
|
256
|
+
only ever counted `requirement()`/`scenario()` calls and so never had the bug.
|
|
257
|
+
|
|
258
|
+
### Added
|
|
259
|
+
|
|
260
|
+
- **`verify` reports what it looked at.** The human summary leads with
|
|
261
|
+
`— 20 requirements / 41 scenarios, 0 error, …` and is preceded by the run
|
|
262
|
+
scope (`Running 12 spec files`, plus how many were located and skipped for
|
|
263
|
+
declaring no `requirement()`). `--json` carries the same four numbers under
|
|
264
|
+
`counts`. This is the companion the change above needs: scoping the run means a
|
|
265
|
+
file whose `requirement()` the parser fails to see is now dropped from the run
|
|
266
|
+
rather than failing in it, and the gap between "located" and "in scope" is where
|
|
267
|
+
that would show. It is also a second line of defence against a vacuous green — a
|
|
268
|
+
report that says `0 requirements` cannot be misread as a full pass. `counts` is
|
|
269
|
+
a new optional field on an existing envelope, so `schemaVersion` stays `1`.
|
|
270
|
+
|
|
271
|
+
### Fixed
|
|
272
|
+
|
|
273
|
+
- **The design document described three anti-drift mechanisms; two exist.**
|
|
274
|
+
§6's mechanism 2, "red/green expectation tracking" — a new requirement's
|
|
275
|
+
scenarios must be red while a change is in progress and green at archive time,
|
|
276
|
+
with the framework blocking on a mismatch — was written as shipped behaviour,
|
|
277
|
+
with a strength rating, next to two mechanisms that are real. It was never
|
|
278
|
+
built. What ships is the weaker half: §8's gate requires green at archive time,
|
|
279
|
+
but nothing records or requires the earlier red, so a scenario that was green
|
|
280
|
+
from the start — asserting nothing — passes it. Anyone who budgeted their own
|
|
281
|
+
review against three mechanisms was covered by two. Now marked
|
|
282
|
+
**(not implemented)** inline, with the gap named. Same treatment for
|
|
283
|
+
`/atx:propose` and `/atx:apply` in §9, which are likewise designed and unbuilt;
|
|
284
|
+
the manual path (`changes/<name>/` + `attest archive`) is spelled out beside
|
|
285
|
+
them.
|
|
286
|
+
- **Documentation described an API that does not exist.** §1's architecture
|
|
287
|
+
diagram — the first thing a reader meets — showed `@covers('AUTH-3')` and a
|
|
288
|
+
`covers(id)` back-link, an early spelling of what shipped as
|
|
289
|
+
`requirement(id)` / `scenario(name)`, with coverage reconstructed from the
|
|
290
|
+
`[reqId]` suite name. §5.4 already described the real mechanism, so the
|
|
291
|
+
document contradicted itself. The registry and spec samples also imported from
|
|
292
|
+
a bare `'attest'`, and §10 declared the package as `attest` exporting only
|
|
293
|
+
`.` — the published package is `@am_shork/attest` and has exported `./define`
|
|
294
|
+
since 0.1.2, which is the import path README tells users to write. These are
|
|
295
|
+
samples meant to be copied, and this repo's docs are also fed to agents, so a
|
|
296
|
+
stale name becomes generated code.
|
|
297
|
+
- **Smaller documentation corrections.** §1 called its own three-row table "the
|
|
298
|
+
two core reports"; the Coverage row attributed coverage to runtime collection
|
|
299
|
+
alone, when `check`/`cover` read it statically. §4 listed the SHALL/MUST rule
|
|
300
|
+
under structural validation, where it is enforced by the schema one layer up.
|
|
301
|
+
§5.3's `Issue` sample omitted the `reqId` field the same snippet sets. §7
|
|
302
|
+
cited "§6" for a section of OpenSpec's analysis, not this document's §6.
|
|
303
|
+
The README's `--json` sample claimed `"version": "0.1.2"`, and its
|
|
304
|
+
prerequisites section was headed "Requirements" — a term this project already
|
|
305
|
+
uses for the intent layer.
|
|
306
|
+
- **The feedback template and the GitLab issue template each claimed to be the
|
|
307
|
+
other.** They are not identical, and should not be: `outcome:` is for
|
|
308
|
+
maintainer-authored reports and `verdict:` for outside ones, so the issue form
|
|
309
|
+
is deliberately the outside-reporter subset. Both now say so, the key rule is
|
|
310
|
+
stated where it can be read, and the issue form gained the mid-project
|
|
311
|
+
follow-up its own `adoption: brownfield` field was asking for and never posed.
|
|
312
|
+
|
|
313
|
+
Two checks now hold the parts of this that can be held mechanically, in
|
|
314
|
+
`tests/docs-consistency.spec.ts`: the source cites design sections by number in
|
|
315
|
+
58 places, and every citation must resolve in both languages, so inserting a
|
|
316
|
+
section can no longer silently repoint them; and any JSON sample in the README
|
|
317
|
+
that identifies itself as attest output must carry the current `version` and
|
|
318
|
+
`schemaVersion`. Neither can catch a section that describes something unbuilt —
|
|
319
|
+
that is what the inline marker is for.
|
|
320
|
+
|
|
321
|
+
## Considered and rejected
|
|
322
|
+
|
|
323
|
+
Decisions **not** to build something, kept where they can be found before the
|
|
324
|
+
same candidate is proposed again. Nothing here shipped, so nothing here belongs
|
|
325
|
+
to a release — this section is deliberately outside the version history and does
|
|
326
|
+
not move when one is cut.
|
|
327
|
+
|
|
38
328
|
- **Two gaps in the intent layer: nothing resists a bloated requirement, and
|
|
39
|
-
nothing resists a duplicated one.**
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
and
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
`
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
`
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
*
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
under 0.2.0 for that reason.
|
|
91
|
-
|
|
92
|
-
The breaking items below are held for **0.2.0** (under 0.x SemVer a breaking
|
|
93
|
-
change bumps the minor). "Breaking" means it changes the exit code of an existing
|
|
94
|
-
valid setup, rejects previously-valid input, removes/renames a public API or
|
|
95
|
-
`--json` field, or changes a default runtime behavior an existing invocation
|
|
96
|
-
relies on — diagnostic message text is not API.
|
|
97
|
-
|
|
98
|
-
### Planned for 0.2.0 (breaking)
|
|
99
|
-
|
|
100
|
-
- **A vacuous green stops passing.** Today `attest verify` on a spec that ran no
|
|
101
|
-
scenarios (an empty or requirement-less directory) prints
|
|
102
|
-
`✓ No issues. — 0 error, …` and exits `0`, because `runAndCollect` reads "no
|
|
103
|
-
failing tests" as `passed`. `verify` will instead emit a new `empty-spec`
|
|
104
|
-
ERROR when zero scenarios executed (or the registry has zero requirements),
|
|
105
|
-
so `ok` becomes `false` and the exit code follows the existing `hasError`
|
|
106
|
-
path. Wires up `hasSpecs()` (present in `pipeline.ts`, never called today).
|
|
107
|
-
*Breaking:* an "empty directory → exit 0" run flips to exit 1 — which is the
|
|
108
|
-
point, since that run attests nothing. *Non-breaking companion (may land
|
|
109
|
-
earlier in 0.1.x):* requirement/scenario counts in the summary line
|
|
110
|
-
(`— 33 requirements / 59 scenarios, 0 error, …`).
|
|
111
|
-
- **Default spec discovery ignores files with no `requirement()`.** `verify` at
|
|
112
|
-
a repo root currently sweeps every `**/*.spec.ts` — including a pre-existing
|
|
113
|
-
suite — into a runner with no aliases/DOM, a guaranteed red unrelated to the
|
|
114
|
-
code. The default will change to run only spec files that declare at least one
|
|
115
|
-
`requirement()`, so an incumbent suite is left untouched without forcing a
|
|
116
|
-
directory layout. *Breaking:* a repo relying on the root scan to pick up
|
|
117
|
-
requirement-less specs sees a different set run. *Non-breaking companion (may
|
|
118
|
-
land earlier in 0.1.x):* a pre-run report of how many spec files were located
|
|
119
|
-
and how many contain a `requirement()` call.
|
|
329
|
+
nothing resists a duplicated one.** The gaps themselves are described in design
|
|
330
|
+
§11, which is where a permanent property of the design belongs. What is kept
|
|
331
|
+
here is the list of mechanisms tried and what killed each, so the same three
|
|
332
|
+
are not proposed again without new evidence. All three were scored against
|
|
333
|
+
every registry this repo ships *before* any was written into the engine — the
|
|
334
|
+
order `divergent-param` was originally designed in the wrong way round. The
|
|
335
|
+
measurement is runnable in `tests/intent-rule-candidates.spec.ts` and fails
|
|
336
|
+
when a requirement is added without a hand-labelled obligation count, because
|
|
337
|
+
the reach figure once quoted here went stale exactly that way.
|
|
338
|
+
*`compound-requirement` (WARNING) — rejected.* More than one RFC-2119 keyword
|
|
339
|
+
in one `statement` is objectively more than one obligation. Measured: of the 7
|
|
340
|
+
requirements in the corpus carrying more than one, it flags 2 (`ATX-10`,
|
|
341
|
+
`ATX-21`) and misses `ATX-5`, `ATX-9`, `ATX-13`, `ATX-14`, `ATX-17` — every
|
|
342
|
+
one that packs clauses under a single keyword, which is the form the coverage
|
|
343
|
+
incentive actually rewards. A wider variant (keyword plus a coordinating
|
|
344
|
+
`and`/`while`/`;`) reaches 6 of 7 with one wrong hit, so reach is not the
|
|
345
|
+
deciding argument.
|
|
346
|
+
This is: both variants are silenced by deleting the word that triggered them,
|
|
347
|
+
and neither can tell that deletion from a real split. `SHALL do A and MUST do
|
|
348
|
+
B` clears the warning as `SHALL do A and do B` — same two obligations, one
|
|
349
|
+
fewer normative keyword. A rule whose cheapest fix degrades the artifact it
|
|
350
|
+
protects does not ship.
|
|
351
|
+
*`divergent-param` (WARNING) — rejected.* One param name declared with
|
|
352
|
+
different values in two requirements sharing an id prefix. Measured: it fires
|
|
353
|
+
twice on this repo and both firings are wrong — `idleTimeoutMin` (`15` in
|
|
354
|
+
`ATX-3`, `30` in `ATX-10`), unrelated illustrative fixtures, and `code`
|
|
355
|
+
(`registry-not-static` in `ATX-17`, `empty-spec` in `ATX-18`,
|
|
356
|
+
`rationale-placeholder` in `ATX-21`), diagnostics that could not possibly
|
|
357
|
+
agree. The second arrived on its own when `ATX-18` was added and grew a third
|
|
358
|
+
member on its own again when `ATX-21` was, which is the more damning half: a
|
|
359
|
+
generic param name collides more often as a registry grows, so the
|
|
360
|
+
false-positive rate rises with adoption — the opposite of what a shippable rule
|
|
361
|
+
does. Still zero true positives across 25 requirements. The escape hatch it was blocked on has since been designed
|
|
362
|
+
and does not rescue it: with no config file the only workable shape is an
|
|
363
|
+
additive `independentParams?: string[]` on the requirement (a CLI flag is
|
|
364
|
+
per-invocation for what is a permanent property of two requirements; changing
|
|
365
|
+
the `params` shape rejects every existing registry). Viable, but it is
|
|
366
|
+
permanent schema surface for a rule with no demonstrated true positive, and in
|
|
367
|
+
practice it would be written *after* the warning fires — a one-line silencer.
|
|
368
|
+
*Countable obligations (breaking) — rejected.* An `obligations: string[]` per
|
|
369
|
+
requirement, with coverage counted per obligation, removing the lumping
|
|
370
|
+
incentive at the source instead of detecting it afterwards. Rejected because
|
|
371
|
+
the **id is already that unit**: splitting into two ids costs two scenarios and
|
|
372
|
+
buys the same incentive with no new concept (`ATX-11`/`ATX-12` are exactly that
|
|
373
|
+
split). It would touch the parser, coverage, `render`, `SPEC.md`, delta apply
|
|
374
|
+
and the `--json` schema, and it relocates the incentive rather than removing
|
|
375
|
+
it — nothing can force the array to be complete.
|
|
376
|
+
*Not considered:* similarity matching between statements (token overlap,
|
|
377
|
+
embeddings). That reintroduces the fuzzy comparison design §0 exists to remove;
|
|
378
|
+
real semantic duplication needs judgement and belongs to human review at the
|
|
379
|
+
propose stage.
|
|
120
380
|
|
|
121
381
|
## [0.1.7] - 2026-07-26
|
|
122
382
|
|
|
@@ -438,7 +698,9 @@ Initial release.
|
|
|
438
698
|
(MIT), whose four-stage engine and diff-first change model Attest's
|
|
439
699
|
architecture is adapted from (re-implemented from scratch, no source copied).
|
|
440
700
|
|
|
441
|
-
[Unreleased]: https://gitlab.com/Pseudorca/attest/-/compare/v0.1
|
|
701
|
+
[Unreleased]: https://gitlab.com/Pseudorca/attest/-/compare/v0.2.1...main
|
|
702
|
+
[0.2.1]: https://gitlab.com/Pseudorca/attest/-/tags/v0.2.1
|
|
703
|
+
[0.2.0]: https://gitlab.com/Pseudorca/attest/-/tags/v0.2.0
|
|
442
704
|
[0.1.7]: https://gitlab.com/Pseudorca/attest/-/tags/v0.1.7
|
|
443
705
|
[0.1.6]: https://gitlab.com/Pseudorca/attest/-/tags/v0.1.6
|
|
444
706
|
[0.1.5]: https://gitlab.com/Pseudorca/attest/-/tags/v0.1.5
|