@am_shork/attest 0.2.0 → 0.2.2

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 CHANGED
@@ -15,6 +15,170 @@ not API.
15
15
 
16
16
  ## [Unreleased]
17
17
 
18
+ ## [0.2.2] - 2026-07-26
19
+
20
+ A wall of identical diagnostics now says once that it is one cause, and the
21
+ per-issue lines it summarises are unchanged.
22
+
23
+ ### Added
24
+
25
+ - **A report of N issues sharing one `code` now says so once.** The 0.1.1 → 0.2.0
26
+ upgrade printed six `registry-not-static` ERRORs in a row — one per registry
27
+ file, every one caused by the same prose written as `'…' + '…'` — and left the
28
+ reader to work out that they were one cause and one mechanical fix. Every
29
+ command that prints issues (`check`, `verify`, `cover`, `render`, `archive`)
30
+ now adds one line above the counts when a code repeats:
31
+
32
+ ```
33
+ Repeated: 6 × ERROR registry-not-static, 4 × WARNING rationale-placeholder
34
+ ```
35
+
36
+ The per-issue lines are untouched — they are what makes a diagnostic
37
+ clickable — and this is the last entry from the skills-manager adoption report
38
+ (0.2.0, 41 requirements / 106 scenarios, plain-TS), so the
39
+ `Under consideration` section is now empty and gone. Five decisions, with the
40
+ reasoning, because each of them could plausibly have gone the other way:
41
+
42
+ **The `code` names the shared cause, and there is no table.** The obvious
43
+ reading of "name the cause" is a `code` → prose map: `registry-not-static` →
44
+ "a registry was built by concatenation". That map would have to be extended
45
+ with every new code, and the failure mode of forgetting is silent — the line
46
+ keeps printing, just vaguer. So the summary states structure only, how many
47
+ issues carry the same code, and leaves the explaining to the per-issue
48
+ `message`s directly above it. A `code` already *is* the stable name of the
49
+ shared rule; `message` is prose and not API, which is exactly why the summary
50
+ quotes neither.
51
+
52
+ **Three, not two.** Two adjacent lines with the same code are not a wall — the
53
+ repetition is visible at a glance, and summarising it would charge every
54
+ ordinary double error a line to state what the reader can already see.
55
+
56
+ **Below the issues, beside the counts, not merged into them.** The clickable
57
+ lines land first and the aggregate reads as a conclusion about them rather than
58
+ a preamble to issues not yet seen. It stays a separate line because
59
+ `— N error, M warning, K info` is a fixed shape and the only stable thing to
60
+ `grep` at the end of a report; a conditional clause inside it would make it
61
+ variable-length.
62
+
63
+ **Every level, grouped by level *and* code.** A pile of `rationale-placeholder`
64
+ WARNINGs is the same reading problem as a pile of ERRORs. The grouping key is
65
+ the pair because a count that silently spanned two levels would give one number
66
+ for two different dispositions — in practice every issue site writes a literal
67
+ level, and the composite key is what makes that not need to be true.
68
+
69
+ **Presentation, not a report change.** `SCHEMA_VERSION` is untouched and
70
+ `--json` is byte-identical: the summary is not an `Issue`, so it carries no
71
+ `code` into `issues[]`, adds nothing to `summary`, and cannot move `ok`. Split
72
+ across two self-requirements (ATX-23, ATX-24) rather than one, because the
73
+ cheapest implementation — push one more `Issue` — satisfies the human half
74
+ completely while breaking the contract half, and at ERROR level would flip the
75
+ verdict of a report that found nothing new. That needs a scenario that fails,
76
+ not a clause in a sentence. `docs/en/attest-design.md` §5.3 is deliberately
77
+ unchanged: it specifies the `Issue` contract and the grading, and documenting a
78
+ terminal nicety there would imply this is part of it.
79
+
80
+ ## [0.2.1] - 2026-07-26
81
+
82
+ The type layer stopped charging for the one behaviour the tool rewards, and
83
+ three limits an adoption had to find on its own are now written down.
84
+
85
+ ### Added
86
+
87
+ - **Reading a param cost a cast, and the cast was charged on the one behaviour
88
+ the tool rewards.** `defineRequirements` returned `Registry`, so every param
89
+ was the full `string | number | boolean | Array<…>` union and
90
+ `reqs[id].params.x` had to be converged at each read site — the reporting
91
+ adopter wrote `str()` / `strList()` helpers to do it. `possible-drift` goes
92
+ quiet precisely when a scenario reads `.params`, so the type layer was taxing
93
+ the behaviour the anti-drift heuristic exists to reward. It is now generic
94
+ over its argument with a `const` type parameter: `params.idleTimeoutMin` is
95
+ `30`, `params.vendorDirs` is an array of the strings written there, and a
96
+ declared ID is known to exist, so `reqs['AUTH-3']` needs no `!` under
97
+ `noUncheckedIndexedAccess`. Removing the casts from Attest's own `self/`
98
+ specs closed 21 latent type errors there. No runtime behaviour and no `--json`
99
+ change: since 0.2.0 the analysing commands read the registry from the AST, so
100
+ this type has no bearing on anything the engine sees.
101
+ **A widening, deliberately.** A `const` type parameter would infer array
102
+ literals as tuples — `readonly` ones under a constraint that admits them —
103
+ and neither is assignable to the mutable `Array<number | string | boolean>`
104
+ the schema parses to, which would have broken `const reqs: Registry =
105
+ defineRequirements({…})` and every `const xs: string[] = req.params.foo`; the
106
+ mapped type collapses them back to `E[]`. The result is still intersected with
107
+ `Registry`, so its index signature survives and `reqs[id]` with an `id: string`
108
+ — a shared test helper — still resolves, as does a param the registry does not
109
+ declare. **What that costs**, against the intent recorded when this was queued:
110
+ a mistyped literal `reqs['TYPO-9']` is *not* rejected outright. Rejecting an
111
+ unknown key and accepting a `string` one are the same question asked of one
112
+ index signature, and it cannot answer both ways; the fallback is what keeps
113
+ this non-breaking, so it wins. The weaker half is real — an unknown ID misses
114
+ the declared keys and resolves through the index signature, so under
115
+ `noUncheckedIndexedAccess` reading through it is an error where `reqs['AUTH-3']`
116
+ no longer is — and a wrong ID is an `unknown-req-id` ERROR from `check`
117
+ regardless, which reads registry and specs together. The type layer is the
118
+ second reader here, not the only one.
119
+ - **Three things an adoption had to discover for itself.** Documentation only —
120
+ no behaviour changed, and each is a property the design deliberately has.
121
+ **`check` has no detection power over a params change**: editing a value
122
+ (`'main'` → `'trunk'`, an entry dropped from a nine-element list) leaves it at
123
+ `✓ No issues`, because `check` executes nothing and nothing became unbound or
124
+ uncovered. The single source makes the value impossible to *diverge*, which is
125
+ a different and stronger claim than announcing that it *moved* — but "params
126
+ are the single source for values that change" reads easily as "`check` guards
127
+ them", and what actually guards them is a scenario asserting on the value it
128
+ read. Now stated where params are introduced (README, design §2, the prompt
129
+ guide) and recorded as a limit in design §11, next to the others.
130
+ **`verify` does not read your `vitest.config.ts`**: the child run starts
131
+ isolated, so a verdict never depends on ambient configuration — which also
132
+ means framework code gets no transforms, no DOM and no aliases until
133
+ `--vitest-config` points it at one. The flag was documented in the README; the
134
+ trade-off behind it now sits in design §5.4, where the run scope is described,
135
+ because that is where a reader meets the isolation.
136
+ **A template literal keeps its whitespace**: one with no substitution reads
137
+ statically and is accepted, but preserves every newline and space of
138
+ indentation, and `render` copies the statement into the Markdown as written —
139
+ so editor-friendly wrapping lands in the generated document. This is the
140
+ reason registry prose is written as one long single-line string, which was
141
+ folklore until now.
142
+ - **A codemod for the 0.2.0 `registry-not-static` migration.** Collapsing
143
+ multi-line `'…' + '…'` prose into one literal is mechanical and was done by
144
+ hand across 21 requirements in the first reported adoption. The 0.2.0 entry
145
+ now carries a compiler-API script that does it, tested on the shapes that make
146
+ it awkward: mixed quote styles, an apostrophe inside the prose, a template
147
+ literal in the chain, and a numeric `1 + 2` it must leave alone.
148
+ - **Type tests, and a suite that runs them.** The repo had none, and this change
149
+ is entirely type-level: no runtime test and no `verify self` run could observe
150
+ a regression in it. `tests/registry-types.spec-d.ts` pins the narrowing, both
151
+ compatibility promises and the trade-off above; `vitest run` now typechecks in
152
+ the same pass (`tsconfig.typecheck.json`, which unlike the build project covers
153
+ `tests/` and `self/` — neither had ever been through a compiler). `DefinedRegistry`
154
+ is exported for consumers that need to name the type.
155
+
156
+ - **A `{placeholder}` in a `rationale` reached the document unread.** `render`
157
+ interpolates `statement` and nothing else, and `unbound-param` only scans
158
+ statements, so braces written into a rationale passed through every check and
159
+ landed verbatim in the generated Markdown. The mistyped placeholder is the
160
+ obvious case; the one that misleads is the **correctly spelled** one, where
161
+ the author declared the param and simply picked the wrong field — nothing was
162
+ unbound, so nothing fired, and the document reviewers and audit read showed a
163
+ placeholder where a value was meant. A new `rationale-placeholder` WARNING now
164
+ reports any `{name}` in a rationale, bound or not.
165
+ Deliberately not the two alternatives. Extending `unbound-param` to rationales
166
+ would cover only the undeclared half — the half that misleads least. And
167
+ making `render` interpolate rationales would change the bytes of every
168
+ committed rendering, turning `render --check` red on an upgrade where the user
169
+ moved no intent, which this file already treats as breaking for that surface.
170
+ **A placeholder inside a Markdown code span is not reported.** That exclusion
171
+ came from dogfooding: the first run against Attest's own registry flagged
172
+ ATX-14, whose rationale explains that `` `{toString}` `` *looks* bound — prose
173
+ about a placeholder, not a misfiled one, and the only way to write a
174
+ requirement whose subject is the syntax itself. A rationale is Markdown and
175
+ `render` leaves a code span exactly as written, so a backticked token is
176
+ already marked as quoted.
177
+ WARNING sits below the `hasError` predicate that `ok`, `--json` and the exit
178
+ code all read from, so an existing green registry with braces in a rationale
179
+ gains a line of output and keeps its verdict. A new `code` on the existing
180
+ `Issue` shape is additive: `schemaVersion` stays `1`.
181
+
18
182
  ## [0.2.0] - 2026-07-26
19
183
 
20
184
  Three things that could each end a run with the wrong verdict: a registry read
@@ -63,6 +227,47 @@ never pointed at.
63
227
  A registry file may still be written as `const reqs = defineRequirements({…});
64
228
  export default reqs;`, and may import `defineRequirements` under any alias or
65
229
  through a namespace.
230
+ **Migrating concatenated prose.** The common shape is a statement or rationale
231
+ wrapped across lines as `'…' + '…'`, which is a computation and so no longer
232
+ reads. Collapsing it is purely mechanical — 21 requirements by hand in the
233
+ first reported adoption — so it is worth a codemod. Save this at the project
234
+ root and run it over the registry files; it rewrites only concatenations whose
235
+ every operand is a string (or substitution-free template) literal, leaving
236
+ `1 + 2` and anything with an identifier in it alone:
237
+
238
+ ```js
239
+ // flatten-reqs.mjs — node flatten-reqs.mjs requirements/*.reqs.ts
240
+ import ts from 'typescript';
241
+ import { readFileSync, writeFileSync } from 'node:fs';
242
+
243
+ const flat = (n) =>
244
+ ts.isBinaryExpression(n) && n.operatorToken.kind === ts.SyntaxKind.PlusToken
245
+ ? [flat(n.left), flat(n.right)].every(Boolean) && flat(n.left) + flat(n.right)
246
+ : (ts.isStringLiteral(n) || ts.isNoSubstitutionTemplateLiteral(n)) && n.text;
247
+
248
+ for (const file of process.argv.slice(2)) {
249
+ const src = readFileSync(file, 'utf8');
250
+ const sf = ts.createSourceFile(file, src, ts.ScriptTarget.Latest, true);
251
+ const edits = [];
252
+ const visit = (n) => {
253
+ const text = flat(n);
254
+ // the whole chain is one edit, so do not descend into it
255
+ if (text !== false && ts.isBinaryExpression(n)) return void edits.push([n.getStart(sf), n.getEnd(), text]);
256
+ n.forEachChild(visit);
257
+ };
258
+ visit(sf);
259
+ let out = src;
260
+ for (const [start, end, text] of edits.reverse())
261
+ out = `${out.slice(0, start)}${JSON.stringify(text)}${out.slice(end)}`;
262
+ if (edits.length) writeFileSync(file, out), console.log(`${file}: ${edits.length} collapsed`);
263
+ }
264
+ ```
265
+
266
+ It emits double-quoted strings (`JSON.stringify` does the escaping, so prose
267
+ containing an apostrophe survives); run the formatter afterwards. Write the
268
+ result as one long line rather than re-wrapping it — a template literal reads
269
+ statically but keeps its newlines and indentation, and `render` copies the
270
+ statement into the Markdown exactly as written.
66
271
  The `--json` envelope is unchanged: `registry-not-static` is a new `code` on
67
272
  the existing `Issue` shape, which is additive, so `schemaVersion` stays `1`.
68
273
  `vite` remains a peer dependency — `verify` and `archive` run the suite through
@@ -174,12 +379,13 @@ not move when one is cut.
174
379
  when a requirement is added without a hand-labelled obligation count, because
175
380
  the reach figure once quoted here went stale exactly that way.
176
381
  *`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.
382
+ in one `statement` is objectively more than one obligation. Measured: of the 7
383
+ requirements in the corpus carrying more than one, it flags 2 (`ATX-10`,
384
+ `ATX-21`) and misses `ATX-5`, `ATX-9`, `ATX-13`, `ATX-14`, `ATX-17` — every
385
+ one that packs clauses under a single keyword, which is the form the coverage
386
+ incentive actually rewards. A wider variant (keyword plus a coordinating
387
+ `and`/`while`/`;`) reaches 6 of 7 with one wrong hit, so reach is not the
388
+ deciding argument.
183
389
  This is: both variants are silenced by deleting the word that triggered them,
184
390
  and neither can tell that deletion from a real split. `SHALL do A and MUST do
185
391
  B` clears the warning as `SHALL do A and do B` — same two obligations, one
@@ -189,12 +395,13 @@ not move when one is cut.
189
395
  different values in two requirements sharing an id prefix. Measured: it fires
190
396
  twice on this repo and both firings are wrong — `idleTimeoutMin` (`15` in
191
397
  `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
398
+ (`registry-not-static` in `ATX-17`, `empty-spec` in `ATX-18`,
399
+ `rationale-placeholder` in `ATX-21`), diagnostics that could not possibly
400
+ agree. The second arrived on its own when `ATX-18` was added and grew a third
401
+ member on its own again when `ATX-21` was, which is the more damning half: a
402
+ generic param name collides more often as a registry grows, so the
403
+ false-positive rate rises with adoption the opposite of what a shippable rule
404
+ does. Still zero true positives across 25 requirements. The escape hatch it was blocked on has since been designed
198
405
  and does not rescue it: with no config file the only workable shape is an
199
406
  additive `independentParams?: string[]` on the requirement (a CLI flag is
200
407
  per-invocation for what is a permanent property of two requirements; changing
@@ -534,7 +741,9 @@ Initial release.
534
741
  (MIT), whose four-stage engine and diff-first change model Attest's
535
742
  architecture is adapted from (re-implemented from scratch, no source copied).
536
743
 
537
- [Unreleased]: https://gitlab.com/Pseudorca/attest/-/compare/v0.2.0...main
744
+ [Unreleased]: https://gitlab.com/Pseudorca/attest/-/compare/v0.2.2...main
745
+ [0.2.2]: https://gitlab.com/Pseudorca/attest/-/tags/v0.2.2
746
+ [0.2.1]: https://gitlab.com/Pseudorca/attest/-/tags/v0.2.1
538
747
  [0.2.0]: https://gitlab.com/Pseudorca/attest/-/tags/v0.2.0
539
748
  [0.1.7]: https://gitlab.com/Pseudorca/attest/-/tags/v0.1.7
540
749
  [0.1.6]: https://gitlab.com/Pseudorca/attest/-/tags/v0.1.6
package/README.md CHANGED
@@ -14,6 +14,13 @@ physically impossible to drift between the spec and the assertion. A param may b
14
14
  a scalar or an array of scalars, so list-shaped constants (vendor blacklists, id
15
15
  sets) get the same single source as a lone number.
16
16
 
17
+ What that does not buy is a warning when you change the value. `check` runs
18
+ nothing, so editing a param leaves it at `✓ No issues` — nothing became unbound,
19
+ nothing became uncovered. The value cannot *diverge* from the assertion, which is
20
+ the stronger property; noticing that it *moved* is `verify`'s job, and only when
21
+ a scenario asserts on the value it read from `params`. Read the param inside the
22
+ assertion, not beside it.
23
+
17
24
  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
25
 
19
26
  ## Prerequisites
@@ -57,7 +64,7 @@ import { createSession, advance, isValid } from './session.js';
57
64
 
58
65
  requirement('AUTH-3', () => {
59
66
  scenario('idle timeout invalidates the session', () => {
60
- const t = reqs['AUTH-3'].params.idleTimeoutMin; // read from the single source
67
+ const t = reqs['AUTH-3'].params.idleTimeoutMin; // single source, typed `30` — no cast
61
68
  const s = createSession();
62
69
  advance(s, t + 1, 'minutes');
63
70
  expect(isValid(s, t)).toBe(false);
@@ -115,6 +122,12 @@ behaviour instead, `--eval` restores it on all three commands:
115
122
  attest check --eval # reads the registry by executing every *.reqs.ts
116
123
  ```
117
124
 
125
+ One more thing to know about literals: write registry prose as **one long
126
+ single-line string**. A template literal with no substitution reads statically and
127
+ is accepted — but it keeps every newline and every space of indentation inside it,
128
+ and `render` puts the statement into the Markdown exactly as written, so the
129
+ wrapping that looked tidy in the editor lands in the generated document.
130
+
118
131
  ### A readable spec for humans (`attest render`)
119
132
 
120
133
  The intent layer lives in TypeScript, which means a reviewer, a QA engineer, or
@@ -172,6 +185,17 @@ The gap between the two numbers is worth watching: a spec file whose
172
185
  `requirement()` call Attest cannot see statically is skipped rather than failed,
173
186
  and that is where it shows. The same four numbers are in `--json` under `counts`.
174
187
 
188
+ When three or more issues in one report share a level and a code, one line above
189
+ the counts says so, because a wall of identical codes is one rule and many sites:
190
+
191
+ ```
192
+ Repeated: 6 × ERROR registry-not-static, 4 × WARNING rationale-placeholder
193
+ ```
194
+
195
+ Every per-issue line stays exactly as it is — they are what makes a diagnostic
196
+ clickable — and the summary is terminal output only: it is not an issue, so it
197
+ appears nowhere in `--json`, changes no count, and never moves a verdict.
198
+
175
199
  A run with **no requirements** under the root is an `empty-spec` ERROR, not a
176
200
  pass. "No failing tests" is not the same claim as "the requirements hold", and
177
201
  the case that matters is not the empty directory — it is the repo whose registry
@@ -208,7 +232,7 @@ attest check --json
208
232
  {
209
233
  "tool": "attest",
210
234
  "schemaVersion": 1,
211
- "version": "0.2.0",
235
+ "version": "0.2.2",
212
236
  "command": "check",
213
237
  "ok": false,
214
238
  "summary": { "error": 1, "warning": 0, "info": 0 },
@@ -1,6 +1,23 @@
1
1
  import type { CoverageRow, VerifyCounts } from '../core/pipeline.js';
2
2
  import type { Issue } from '../core/types.js';
3
3
  export declare function formatIssues(issues: Issue[]): string;
4
+ /**
5
+ * A code repeating across files is one rule and many sites, and a reader looking
6
+ * at the wall has to diff the lines to learn that. This says it once.
7
+ *
8
+ * There is deliberately no code → prose table naming the "cause": the `code`
9
+ * already *is* the name of the shared rule, and each site's `message` states the
10
+ * rule in full. A table would also have to be extended with every new code, and
11
+ * forgetting to would degrade this line in silence. So the line states structure
12
+ * only — how many issues carry the same code — and leaves the explaining to the
13
+ * per-site lines above it, which are what makes a diagnostic clickable.
14
+ *
15
+ * Grouped by level *and* code, not code alone: a count that silently spanned two
16
+ * levels would name one number for two different dispositions. Every issue site
17
+ * writes a literal level, so in practice the two groupings agree — the composite
18
+ * key is what makes that not need to be true.
19
+ */
20
+ export declare function formatRepeats(issues: Issue[]): string;
4
21
  export declare function summarize(issues: Issue[], scope?: VerifyCounts): string;
5
22
  /**
6
23
  * The pre-run line for `verify`: how many spec files were located, and how many
@@ -1 +1 @@
1
- {"version":3,"file":"report.d.ts","sourceRoot":"","sources":["../../src/cli/report.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AACrE,OAAO,KAAK,EAAE,KAAK,EAAS,MAAM,kBAAkB,CAAC;AAUrD,wBAAgB,YAAY,CAAC,MAAM,EAAE,KAAK,EAAE,GAAG,MAAM,CAWpD;AAED,wBAAgB,SAAS,CAAC,MAAM,EAAE,KAAK,EAAE,EAAE,KAAK,CAAC,EAAE,YAAY,GAAG,MAAM,CASvE;AAID;;;;;;GAMG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,YAAY,GAAG,MAAM,CAIvD;AAED,wBAAgB,cAAc,CAAC,IAAI,EAAE,WAAW,EAAE,GAAG,MAAM,CAW1D"}
1
+ {"version":3,"file":"report.d.ts","sourceRoot":"","sources":["../../src/cli/report.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AACrE,OAAO,KAAK,EAAE,KAAK,EAAS,MAAM,kBAAkB,CAAC;AAUrD,wBAAgB,YAAY,CAAC,MAAM,EAAE,KAAK,EAAE,GAAG,MAAM,CAYpD;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,aAAa,CAAC,MAAM,EAAE,KAAK,EAAE,GAAG,MAAM,CAkBrD;AAUD,wBAAgB,SAAS,CAAC,MAAM,EAAE,KAAK,EAAE,EAAE,KAAK,CAAC,EAAE,YAAY,GAAG,MAAM,CASvE;AAID;;;;;;GAMG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,YAAY,GAAG,MAAM,CAIvD;AAED,wBAAgB,cAAc,CAAC,IAAI,EAAE,WAAW,EAAE,GAAG,MAAM,CAW1D"}
@@ -1,6 +1,7 @@
1
1
  // Terminal rendering of graded issues (design §5.3 levels). Pure formatting —
2
2
  // no process side effects — so it stays testable and the CLI stays thin.
3
3
  import chalk from 'chalk';
4
+ import { byCodeUnit } from '../core/order.js';
4
5
  const LEVEL_TAG = {
5
6
  ERROR: (s) => chalk.red.bold(s),
6
7
  WARNING: (s) => chalk.yellow.bold(s),
@@ -18,8 +19,54 @@ export function formatIssues(issues) {
18
19
  lines.push(`${LEVEL_TAG[level](level.padEnd(7))} ${chalk.cyan(i.code)}${loc}\n ${i.message}`);
19
20
  }
20
21
  }
21
- return lines.join('\n');
22
+ const repeats = formatRepeats(issues);
23
+ return repeats ? `${lines.join('\n')}\n${repeats}` : lines.join('\n');
22
24
  }
25
+ /**
26
+ * A code repeating across files is one rule and many sites, and a reader looking
27
+ * at the wall has to diff the lines to learn that. This says it once.
28
+ *
29
+ * There is deliberately no code → prose table naming the "cause": the `code`
30
+ * already *is* the name of the shared rule, and each site's `message` states the
31
+ * rule in full. A table would also have to be extended with every new code, and
32
+ * forgetting to would degrade this line in silence. So the line states structure
33
+ * only — how many issues carry the same code — and leaves the explaining to the
34
+ * per-site lines above it, which are what makes a diagnostic clickable.
35
+ *
36
+ * Grouped by level *and* code, not code alone: a count that silently spanned two
37
+ * levels would name one number for two different dispositions. Every issue site
38
+ * writes a literal level, so in practice the two groupings agree — the composite
39
+ * key is what makes that not need to be true.
40
+ */
41
+ export function formatRepeats(issues) {
42
+ /** `${level} ${code}` -> how many issues carry that pair, and its parts. */
43
+ const groups = new Map();
44
+ for (const i of issues) {
45
+ const key = `${i.level} ${i.code}`;
46
+ const seen = groups.get(key);
47
+ if (seen)
48
+ seen.count++;
49
+ else
50
+ groups.set(key, { level: i.level, code: i.code, count: 1 });
51
+ }
52
+ const repeated = [...groups]
53
+ .filter(([, g]) => g.count >= REPEAT_THRESHOLD)
54
+ // Count first, then the key — by code unit, so the line a reader compares
55
+ // between two runs does not depend on the machine's collation.
56
+ .sort(([aKey, a], [bKey, b]) => b.count - a.count || byCodeUnit(aKey, bKey))
57
+ .map(([, g]) => `${g.count} × ${g.level} ${g.code}`);
58
+ if (repeated.length === 0)
59
+ return '';
60
+ // One line however many codes repeat: the point is to shorten the report.
61
+ return chalk.dim(`Repeated: ${repeated.join(', ')}`);
62
+ }
63
+ /**
64
+ * Two adjacent lines with the same code are not a wall — the repetition is
65
+ * visible at a glance, and summarising it would charge every ordinary double
66
+ * error an extra line to say what the reader already knows. Three is where the
67
+ * repetition stops being obvious.
68
+ */
69
+ const REPEAT_THRESHOLD = 3;
23
70
  export function summarize(issues, scope) {
24
71
  const counts = { ERROR: 0, WARNING: 0, INFO: 0 };
25
72
  for (const i of issues)
@@ -1 +1 @@
1
- {"version":3,"file":"report.js","sourceRoot":"","sources":["../../src/cli/report.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"report.js","sourceRoot":"","sources":["../../src/cli/report.ts"],"names":[],"mappings":"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"}
@@ -7,6 +7,15 @@ export interface GateInputs {
7
7
  /** Executable verification result (design §5.4). */
8
8
  run: RunResult;
9
9
  }
10
+ /**
11
+ * Declared-not-run: a scenario the static plan claims attests a requirement,
12
+ * which the runtime never executed (skipped, or excluded by an `.only`).
13
+ *
14
+ * Exported because `verify` and `archive` both make this call. While each owned
15
+ * a copy, the two verdicts agreed only by transcription — and the whole point of
16
+ * this framework is that agreement should be structural, not clerical.
17
+ */
18
+ export declare function declaredNotRunIssues(plan: AttestPlan, run: RunResult): Issue[];
10
19
  /**
11
20
  * Decide whether a change may be archived. Returns the blocking issues; an
12
21
  * empty array means the gate passes (design §8). Checks, in order:
@@ -18,14 +27,5 @@ export interface GateInputs {
18
27
  * 3. Static coverage vs runtime coverage: every declared scenario actually
19
28
  * ran (catches skip/only false coverage).
20
29
  */
21
- /**
22
- * Declared-not-run: a scenario the static plan claims attests a requirement,
23
- * which the runtime never executed (skipped, or excluded by an `.only`).
24
- *
25
- * Exported because `verify` and `archive` both make this call. While each owned
26
- * a copy, the two verdicts agreed only by transcription — and the whole point of
27
- * this framework is that agreement should be structural, not clerical.
28
- */
29
- export declare function declaredNotRunIssues(plan: AttestPlan, run: RunResult): Issue[];
30
30
  export declare function evaluateGate({ registry, plan, run }: GateInputs): Issue[];
31
31
  //# sourceMappingURL=gate.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"gate.d.ts","sourceRoot":"","sources":["../../src/core/gate.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,UAAU,EAAE,KAAK,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAEzE,MAAM,WAAW,UAAU;IACzB,yEAAyE;IACzE,QAAQ,EAAE,QAAQ,CAAC;IACnB,8CAA8C;IAC9C,IAAI,EAAE,UAAU,CAAC;IACjB,oDAAoD;IACpD,GAAG,EAAE,SAAS,CAAC;CAChB;AAED;;;;;;;;;;GAUG;AACH;;;;;;;GAOG;AACH,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,UAAU,EAAE,GAAG,EAAE,SAAS,GAAG,KAAK,EAAE,CAe9E;AAED,wBAAgB,YAAY,CAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,GAAG,EAAE,EAAE,UAAU,GAAG,KAAK,EAAE,CAqBzE"}
1
+ {"version":3,"file":"gate.d.ts","sourceRoot":"","sources":["../../src/core/gate.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,UAAU,EAAE,KAAK,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAEzE,MAAM,WAAW,UAAU;IACzB,yEAAyE;IACzE,QAAQ,EAAE,QAAQ,CAAC;IACnB,8CAA8C;IAC9C,IAAI,EAAE,UAAU,CAAC;IACjB,oDAAoD;IACpD,GAAG,EAAE,SAAS,CAAC;CAChB;AAED;;;;;;;GAOG;AACH,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,UAAU,EAAE,GAAG,EAAE,SAAS,GAAG,KAAK,EAAE,CAe9E;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,YAAY,CAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,GAAG,EAAE,EAAE,UAAU,GAAG,KAAK,EAAE,CAqBzE"}
package/dist/core/gate.js CHANGED
@@ -4,17 +4,6 @@
4
4
  // inputs; the file orchestration (load registry+delta, parse all specs, run)
5
5
  // is wired in the Locate layer / CLI (see pipeline.ts).
6
6
  import { validateStructure } from './validator.js';
7
- /**
8
- * Decide whether a change may be archived. Returns the blocking issues; an
9
- * empty array means the gate passes (design §8). Checks, in order:
10
- * 1. Structural ERRORs on the end-state registry (no orphan / uncovered /
11
- * unbound-param). This is also the design's step-4 final re-validation:
12
- * the registry+plan here are already the applied result, so one pass
13
- * validates the end state.
14
- * 2. Executable: all tests green.
15
- * 3. Static coverage vs runtime coverage: every declared scenario actually
16
- * ran (catches skip/only false coverage).
17
- */
18
7
  /**
19
8
  * Declared-not-run: a scenario the static plan claims attests a requirement,
20
9
  * which the runtime never executed (skipped, or excluded by an `.only`).
@@ -39,6 +28,17 @@ export function declaredNotRunIssues(plan, run) {
39
28
  }
40
29
  return issues;
41
30
  }
31
+ /**
32
+ * Decide whether a change may be archived. Returns the blocking issues; an
33
+ * empty array means the gate passes (design §8). Checks, in order:
34
+ * 1. Structural ERRORs on the end-state registry (no orphan / uncovered /
35
+ * unbound-param). This is also the design's step-4 final re-validation:
36
+ * the registry+plan here are already the applied result, so one pass
37
+ * validates the end state.
38
+ * 2. Executable: all tests green.
39
+ * 3. Static coverage vs runtime coverage: every declared scenario actually
40
+ * ran (catches skip/only false coverage).
41
+ */
42
42
  export function evaluateGate({ registry, plan, run }) {
43
43
  const blocking = [];
44
44
  // 1) Structure (end-state re-validation).
@@ -1 +1 @@
1
- {"version":3,"file":"gate.js","sourceRoot":"","sources":["../../src/core/gate.ts"],"names":[],"mappings":"AAAA,yEAAyE;AACzE,iFAAiF;AACjF,4EAA4E;AAC5E,6EAA6E;AAC7E,wDAAwD;AAExD,OAAO,EAAE,iBAAiB,EAAE,MAAM,gBAAgB,CAAC;AAYnD;;;;;;;;;;GAUG;AACH;;;;;;;GAOG;AACH,MAAM,UAAU,oBAAoB,CAAC,IAAgB,EAAE,GAAc;IACnE,MAAM,MAAM,GAAY,EAAE,CAAC;IAC3B,KAAK,MAAM,CAAC,IAAI,IAAI,CAAC,SAAS,EAAE,CAAC;QAC/B,IAAI,CAAC,GAAG,CAAC,eAAe,CAAC,GAAG,CAAC,CAAC,CAAC,KAAK,CAAC,EAAE,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC;YACnD,MAAM,CAAC,IAAI,CAAC;gBACV,KAAK,EAAE,OAAO;gBACd,IAAI,EAAE,kBAAkB;gBACxB,KAAK,EAAE,CAAC,CAAC,KAAK;gBACd,IAAI,EAAE,CAAC,CAAC,IAAI;gBACZ,IAAI,EAAE,CAAC,CAAC,IAAI;gBACZ,OAAO,EAAE,aAAa,CAAC,CAAC,IAAI,oCAAoC,CAAC,CAAC,KAAK,2DAA2D;aACnI,CAAC,CAAC;QACL,CAAC;IACH,CAAC;IACD,OAAO,MAAM,CAAC;AAChB,CAAC;AAED,MAAM,UAAU,YAAY,CAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,GAAG,EAAc;IAC9D,MAAM,QAAQ,GAAY,EAAE,CAAC;IAE7B,0CAA0C;IAC1C,QAAQ,CAAC,IAAI,CACX,GAAG,iBAAiB,CAAC,QAAQ,EAAE,IAAI,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,KAAK,KAAK,OAAO,CAAC,CACxE,CAAC;IAEF,4BAA4B;IAC5B,IAAI,CAAC,GAAG,CAAC,MAAM,EAAE,CAAC;QAChB,QAAQ,CAAC,IAAI,CAAC;YACZ,KAAK,EAAE,OAAO;YACd,IAAI,EAAE,WAAW;YACjB,OAAO,EAAE,wDAAwD;SAClE,CAAC,CAAC;IACL,CAAC;IAED,2EAA2E;IAC3E,QAAQ,CAAC,IAAI,CAAC,GAAG,oBAAoB,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC,CAAC;IAElD,OAAO,QAAQ,CAAC;AAClB,CAAC"}
1
+ {"version":3,"file":"gate.js","sourceRoot":"","sources":["../../src/core/gate.ts"],"names":[],"mappings":"AAAA,yEAAyE;AACzE,iFAAiF;AACjF,4EAA4E;AAC5E,6EAA6E;AAC7E,wDAAwD;AAExD,OAAO,EAAE,iBAAiB,EAAE,MAAM,gBAAgB,CAAC;AAYnD;;;;;;;GAOG;AACH,MAAM,UAAU,oBAAoB,CAAC,IAAgB,EAAE,GAAc;IACnE,MAAM,MAAM,GAAY,EAAE,CAAC;IAC3B,KAAK,MAAM,CAAC,IAAI,IAAI,CAAC,SAAS,EAAE,CAAC;QAC/B,IAAI,CAAC,GAAG,CAAC,eAAe,CAAC,GAAG,CAAC,CAAC,CAAC,KAAK,CAAC,EAAE,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC;YACnD,MAAM,CAAC,IAAI,CAAC;gBACV,KAAK,EAAE,OAAO;gBACd,IAAI,EAAE,kBAAkB;gBACxB,KAAK,EAAE,CAAC,CAAC,KAAK;gBACd,IAAI,EAAE,CAAC,CAAC,IAAI;gBACZ,IAAI,EAAE,CAAC,CAAC,IAAI;gBACZ,OAAO,EAAE,aAAa,CAAC,CAAC,IAAI,oCAAoC,CAAC,CAAC,KAAK,2DAA2D;aACnI,CAAC,CAAC;QACL,CAAC;IACH,CAAC;IACD,OAAO,MAAM,CAAC;AAChB,CAAC;AAED;;;;;;;;;;GAUG;AACH,MAAM,UAAU,YAAY,CAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,GAAG,EAAc;IAC9D,MAAM,QAAQ,GAAY,EAAE,CAAC;IAE7B,0CAA0C;IAC1C,QAAQ,CAAC,IAAI,CACX,GAAG,iBAAiB,CAAC,QAAQ,EAAE,IAAI,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,KAAK,KAAK,OAAO,CAAC,CACxE,CAAC;IAEF,4BAA4B;IAC5B,IAAI,CAAC,GAAG,CAAC,MAAM,EAAE,CAAC;QAChB,QAAQ,CAAC,IAAI,CAAC;YACZ,KAAK,EAAE,OAAO;YACd,IAAI,EAAE,WAAW;YACjB,OAAO,EAAE,wDAAwD;SAClE,CAAC,CAAC;IACL,CAAC;IAED,2EAA2E;IAC3E,QAAQ,CAAC,IAAI,CAAC,GAAG,oBAAoB,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC,CAAC;IAElD,OAAO,QAAQ,CAAC;AAClB,CAAC"}
@@ -1,18 +1,66 @@
1
1
  import { z } from 'zod';
2
2
  import { type RegistryInput, type RequirementInput } from './schema.js';
3
- import type { Registry } from './types.js';
3
+ import type { Registry, Requirement } from './types.js';
4
4
  /** Thrown when a registry fails structural validation (design §5.1). */
5
5
  export declare class RegistryValidationError extends Error {
6
6
  readonly issues: z.ZodIssue[];
7
7
  constructor(issues: z.ZodIssue[]);
8
8
  }
9
+ /**
10
+ * A `const` type parameter infers an array literal as a tuple, and would infer
11
+ * it as a `readonly` tuple under a constraint that admits one. Neither is what
12
+ * the registry parses to: the schema's array params are plain mutable
13
+ * `Array<scalar>`, and `const xs: string[] = req.params.dirs` has to keep
14
+ * compiling. Collapsing to `E[]` keeps the element types the author wrote —
15
+ * the point of the exercise — without claiming a length or a mutability the
16
+ * parsed value does not have.
17
+ */
18
+ type AsArray<V> = V extends readonly (infer E)[] ? E[] : V;
19
+ /**
20
+ * The params of one requirement as written, over the schema's wider record.
21
+ * The intersection is what keeps this a widening: `params.retries` narrows to
22
+ * the literal `3`, while `params.somethingElse` still resolves through the
23
+ * record, so no read that compiled before stops compiling.
24
+ */
25
+ type DefinedParams<R> = R extends {
26
+ params: infer P;
27
+ } ? {
28
+ -readonly [K in keyof P]: AsArray<P[K]>;
29
+ } & Requirement['params'] : Requirement['params'];
30
+ /**
31
+ * What `defineRequirements` returns for a literal registry: each declared ID
32
+ * keeps the param types its author wrote, so a scenario reads a value without
33
+ * converging the `string | number | boolean | Array<…>` union at the read site
34
+ * (design §6, mechanism 3 — a scenario reading `.params` is what quiets the
35
+ * anti-drift heuristic, so the type layer must not tax the one behaviour the
36
+ * rest of the tool exists to reward).
37
+ *
38
+ * The intersection with {@link Registry} is deliberate and load-bearing: it
39
+ * keeps the index signature, so `reqs[id]` with an `id: string` — a shared test
40
+ * helper — still compiles, and the result is still assignable to `Registry`.
41
+ * The cost is that an index signature admits *every* string, so a mistyped
42
+ * literal `reqs['TYPO-9']` falls through to it instead of being rejected
43
+ * outright: rejecting an unknown key and accepting a `string` one are the same
44
+ * question asked of one index signature, and it cannot answer both ways. What
45
+ * survives is the weaker form — an unknown ID misses the declared keys, so
46
+ * under `noUncheckedIndexedAccess` reading through it is an error where a
47
+ * declared ID is not. Compatibility takes the tie because a wrong ID is
48
+ * already an `unknown-req-id` ERROR from `check`, which reads the registry and
49
+ * the specs together; the type layer is the second reader here, not the only
50
+ * one.
51
+ */
52
+ export type DefinedRegistry<T extends RegistryInput> = {
53
+ [K in keyof T]: Omit<Requirement, 'params'> & {
54
+ params: DefinedParams<T[K]>;
55
+ };
56
+ } & Registry;
9
57
  /**
10
58
  * Declare a requirement registry. Validates structure against RegistrySchema
11
59
  * (SHALL/MUST keyword, non-empty rationale, well-formed IDs, scalar params) and
12
60
  * returns the parsed registry with defaults applied (design §2, §5.1).
13
61
  * Throws {@link RegistryValidationError} on invalid input.
14
62
  */
15
- export declare function defineRequirements(input: RegistryInput): Registry;
63
+ export declare function defineRequirements<const T extends RegistryInput>(input: T): DefinedRegistry<T>;
16
64
  /**
17
65
  * Shape of a change delta over the registry (design §7). Authoring (input)
18
66
  * shapes are used so added / modified entries may omit defaulted fields.
@@ -32,4 +80,5 @@ export interface RegistryDelta {
32
80
  * (RENAMED -> REMOVED -> MODIFIED -> ADDED) lives in applyDelta (design §7).
33
81
  */
34
82
  export declare function delta(d: RegistryDelta): RegistryDelta;
83
+ export {};
35
84
  //# sourceMappingURL=registry.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"registry.d.ts","sourceRoot":"","sources":["../../src/core/registry.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,EAEL,KAAK,aAAa,EAClB,KAAK,gBAAgB,EACtB,MAAM,aAAa,CAAC;AACrB,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAE3C,wEAAwE;AACxE,qBAAa,uBAAwB,SAAQ,KAAK;aACpB,MAAM,EAAE,CAAC,CAAC,QAAQ,EAAE;gBAApB,MAAM,EAAE,CAAC,CAAC,QAAQ,EAAE;CAIjD;AAQD;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,aAAa,GAAG,QAAQ,CAMjE;AAED;;;GAGG;AACH,MAAM,WAAW,aAAa;IAC5B,KAAK,CAAC,EAAE,aAAa,CAAC;IACtB,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,gBAAgB,CAAC,CAAC,CAAC;IACrD,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;IACnB,OAAO,CAAC,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,EAAE,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;CAC1C;AAED;;;;GAIG;AACH,wBAAgB,KAAK,CAAC,CAAC,EAAE,aAAa,GAAG,aAAa,CAErD"}
1
+ {"version":3,"file":"registry.d.ts","sourceRoot":"","sources":["../../src/core/registry.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,EAEL,KAAK,aAAa,EAClB,KAAK,gBAAgB,EACtB,MAAM,aAAa,CAAC;AACrB,OAAO,KAAK,EAAE,QAAQ,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAExD,wEAAwE;AACxE,qBAAa,uBAAwB,SAAQ,KAAK;aACpB,MAAM,EAAE,CAAC,CAAC,QAAQ,EAAE;gBAApB,MAAM,EAAE,CAAC,CAAC,QAAQ,EAAE;CAIjD;AAQD;;;;;;;;GAQG;AACH,KAAK,OAAO,CAAC,CAAC,IAAI,CAAC,SAAS,SAAS,CAAC,MAAM,CAAC,CAAC,EAAE,GAAG,CAAC,EAAE,GAAG,CAAC,CAAC;AAE3D;;;;;GAKG;AACH,KAAK,aAAa,CAAC,CAAC,IAAI,CAAC,SAAS;IAAE,MAAM,EAAE,MAAM,CAAC,CAAA;CAAE,GACjD;IAAE,CAAC,UAAU,CAAC,IAAI,MAAM,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;CAAE,GAAG,WAAW,CAAC,QAAQ,CAAC,GACnE,WAAW,CAAC,QAAQ,CAAC,CAAC;AAE1B;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,MAAM,MAAM,eAAe,CAAC,CAAC,SAAS,aAAa,IAAI;KACpD,CAAC,IAAI,MAAM,CAAC,GAAG,IAAI,CAAC,WAAW,EAAE,QAAQ,CAAC,GAAG;QAAE,MAAM,EAAE,aAAa,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAA;KAAE;CAC9E,GAAG,QAAQ,CAAC;AAEb;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,KAAK,CAAC,CAAC,SAAS,aAAa,EAAE,KAAK,EAAE,CAAC,GAAG,eAAe,CAAC,CAAC,CAAC,CAS9F;AAED;;;GAGG;AACH,MAAM,WAAW,aAAa;IAC5B,KAAK,CAAC,EAAE,aAAa,CAAC;IACtB,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,gBAAgB,CAAC,CAAC,CAAC;IACrD,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;IACnB,OAAO,CAAC,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,EAAE,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;CAC1C;AAED;;;;GAIG;AACH,wBAAgB,KAAK,CAAC,CAAC,EAAE,aAAa,GAAG,aAAa,CAErD"}
@@ -28,6 +28,9 @@ export function defineRequirements(input) {
28
28
  if (!result.success) {
29
29
  throw new RegistryValidationError(result.error.issues);
30
30
  }
31
+ // Zod validates the shape but its output type is the schema's, which has
32
+ // already forgotten the literals `const T` captured. Same object, narrower
33
+ // description of it — the assertion carries the literals across.
31
34
  return result.data;
32
35
  }
33
36
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"registry.js","sourceRoot":"","sources":["../../src/core/registry.ts"],"names":[],"mappings":"AAAA,4DAA4D;AAC5D,4EAA4E;AAC5E,yEAAyE;AAEzE,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,EACL,cAAc,GAGf,MAAM,aAAa,CAAC;AAGrB,wEAAwE;AACxE,MAAM,OAAO,uBAAwB,SAAQ,KAAK;IACpB;IAA5B,YAA4B,MAAoB;QAC9C,KAAK,CAAC,kCAAkC,YAAY,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;QADtC,WAAM,GAAN,MAAM,CAAc;QAE9C,IAAI,CAAC,IAAI,GAAG,yBAAyB,CAAC;IACxC,CAAC;CACF;AAED,SAAS,YAAY,CAAC,MAAoB;IACxC,OAAO,MAAM;SACV,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,QAAQ,KAAK,CAAC,CAAC,OAAO,EAAE,CAAC;SAC/D,IAAI,CAAC,IAAI,CAAC,CAAC;AAChB,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,kBAAkB,CAAC,KAAoB;IACrD,MAAM,MAAM,GAAG,cAAc,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC;IAC/C,IAAI,CAAC,MAAM,CAAC,OAAO,EAAE,CAAC;QACpB,MAAM,IAAI,uBAAuB,CAAC,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC;IACzD,CAAC;IACD,OAAO,MAAM,CAAC,IAAI,CAAC;AACrB,CAAC;AAaD;;;;GAIG;AACH,MAAM,UAAU,KAAK,CAAC,CAAgB;IACpC,OAAO,CAAC,CAAC;AACX,CAAC"}
1
+ {"version":3,"file":"registry.js","sourceRoot":"","sources":["../../src/core/registry.ts"],"names":[],"mappings":"AAAA,4DAA4D;AAC5D,4EAA4E;AAC5E,yEAAyE;AAEzE,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,EACL,cAAc,GAGf,MAAM,aAAa,CAAC;AAGrB,wEAAwE;AACxE,MAAM,OAAO,uBAAwB,SAAQ,KAAK;IACpB;IAA5B,YAA4B,MAAoB;QAC9C,KAAK,CAAC,kCAAkC,YAAY,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;QADtC,WAAM,GAAN,MAAM,CAAc;QAE9C,IAAI,CAAC,IAAI,GAAG,yBAAyB,CAAC;IACxC,CAAC;CACF;AAED,SAAS,YAAY,CAAC,MAAoB;IACxC,OAAO,MAAM;SACV,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,QAAQ,KAAK,CAAC,CAAC,OAAO,EAAE,CAAC;SAC/D,IAAI,CAAC,IAAI,CAAC,CAAC;AAChB,CAAC;AAiDD;;;;;GAKG;AACH,MAAM,UAAU,kBAAkB,CAAgC,KAAQ;IACxE,MAAM,MAAM,GAAG,cAAc,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC;IAC/C,IAAI,CAAC,MAAM,CAAC,OAAO,EAAE,CAAC;QACpB,MAAM,IAAI,uBAAuB,CAAC,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC;IACzD,CAAC;IACD,yEAAyE;IACzE,2EAA2E;IAC3E,iEAAiE;IACjE,OAAO,MAAM,CAAC,IAA0B,CAAC;AAC3C,CAAC;AAaD;;;;GAIG;AACH,MAAM,UAAU,KAAK,CAAC,CAAgB;IACpC,OAAO,CAAC,CAAC;AACX,CAAC"}
@@ -4,6 +4,9 @@ import type { AttestPlan, Issue, ParamRef, Registry } from './types.js';
4
4
  * - orphan-test: a scenario covers an unknown requirement id
5
5
  * - uncovered-requirement: a requirement has no scenario
6
6
  * - unbound-param: a statement placeholder has no matching param
7
+ *
8
+ * and one WARNING:
9
+ * - rationale-placeholder: a `{name}` in a rationale, which is never interpolated
7
10
  */
8
11
  export declare function validateStructure(registry: Registry, plan: AttestPlan): Issue[];
9
12
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"validator.d.ts","sourceRoot":"","sources":["../../src/core/validator.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,UAAU,EAAE,KAAK,EAAE,QAAQ,EAAkB,QAAQ,EAAE,MAAM,YAAY,CAAC;AAExF;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,QAAQ,EAAE,IAAI,EAAE,UAAU,GAAG,KAAK,EAAE,CAkD/E;AAED;;;;;;;GAOG;AACH,wBAAgB,oBAAoB,CAClC,QAAQ,EAAE,QAAQ,EAClB,IAAI,EAAE,UAAU,EAChB,IAAI,EAAE,QAAQ,EAAE,GACf,KAAK,EAAE,CA+BT"}
1
+ {"version":3,"file":"validator.d.ts","sourceRoot":"","sources":["../../src/core/validator.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,UAAU,EAAE,KAAK,EAAE,QAAQ,EAAkB,QAAQ,EAAE,MAAM,YAAY,CAAC;AAExF;;;;;;;;GAQG;AACH,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,QAAQ,EAAE,IAAI,EAAE,UAAU,GAAG,KAAK,EAAE,CA0E/E;AAED;;;;;;;GAOG;AACH,wBAAgB,oBAAoB,CAClC,QAAQ,EAAE,QAAQ,EAClB,IAAI,EAAE,UAAU,EAChB,IAAI,EAAE,QAAQ,EAAE,GACf,KAAK,EAAE,CA+BT"}
@@ -5,6 +5,9 @@
5
5
  * - orphan-test: a scenario covers an unknown requirement id
6
6
  * - uncovered-requirement: a requirement has no scenario
7
7
  * - unbound-param: a statement placeholder has no matching param
8
+ *
9
+ * and one WARNING:
10
+ * - rationale-placeholder: a `{name}` in a rationale, which is never interpolated
8
11
  */
9
12
  export function validateStructure(registry, plan) {
10
13
  const issues = [];
@@ -51,6 +54,29 @@ export function validateStructure(registry, plan) {
51
54
  }
52
55
  }
53
56
  }
57
+ // rationale-placeholder: `{name}` written in a rationale, which render does
58
+ // not interpolate (it substitutes into the statement and nothing else). Both
59
+ // spellings are reported, and the declared one is the reason this is not an
60
+ // extension of unbound-param: an author who declares `maxAttempts` and then
61
+ // writes it into the rationale has made no binding mistake at all, yet the
62
+ // braces still reach the document reviewers and audit read. Fixing render to
63
+ // interpolate here would change the bytes of every existing rendering, so
64
+ // this reports instead. WARNING, deliberately below `hasError`: an existing
65
+ // green registry gains a line of output and keeps its exit code.
66
+ for (const [id, req] of Object.entries(registry)) {
67
+ // Code spans are dropped first: a rationale is rendered as Markdown, so a
68
+ // backticked `{toString}` is a quoted token, not a placeholder its author
69
+ // misfiled — and render leaves a code span exactly as written. Attest's own
70
+ // ATX-14 is that case, which is how this exclusion was found.
71
+ for (const m of req.rationale.replace(/`[^`]*`/g, ' ').matchAll(/\{(\w+)\}/g)) {
72
+ issues.push({
73
+ level: 'WARNING',
74
+ code: 'rationale-placeholder',
75
+ reqId: id,
76
+ message: `Requirement "${id}" writes {${m[1]}} in its rationale, but only the statement is interpolated — the braces reach the rendering verbatim. Move the placeholder into the statement, or write the value out as prose.`,
77
+ });
78
+ }
79
+ }
54
80
  return issues;
55
81
  }
56
82
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"validator.js","sourceRoot":"","sources":["../../src/core/validator.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"validator.js","sourceRoot":"","sources":["../../src/core/validator.ts"],"names":[],"mappings":"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"}
package/dist/index.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  export { requirement, scenario } from './runtime.js';
2
2
  export { defineRequirements, delta } from './core/registry.js';
3
- export type { RegistryDelta } from './core/registry.js';
3
+ export type { RegistryDelta, DefinedRegistry } from './core/registry.js';
4
4
  export type { Requirement, Registry, ParsedScenario, AttestPlan, Level, Issue, } from './core/types.js';
5
5
  export type { JsonReport, JsonSummary, JsonCommand } from './cli/json.js';
6
6
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,WAAW,EAAE,QAAQ,EAAE,MAAM,cAAc,CAAC;AACrD,OAAO,EAAE,kBAAkB,EAAE,KAAK,EAAE,MAAM,oBAAoB,CAAC;AAC/D,YAAY,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AACxD,YAAY,EACV,WAAW,EACX,QAAQ,EACR,cAAc,EACd,UAAU,EACV,KAAK,EACL,KAAK,GACN,MAAM,iBAAiB,CAAC;AAIzB,YAAY,EAAE,UAAU,EAAE,WAAW,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,WAAW,EAAE,QAAQ,EAAE,MAAM,cAAc,CAAC;AACrD,OAAO,EAAE,kBAAkB,EAAE,KAAK,EAAE,MAAM,oBAAoB,CAAC;AAC/D,YAAY,EAAE,aAAa,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAC;AACzE,YAAY,EACV,WAAW,EACX,QAAQ,EACR,cAAc,EACd,UAAU,EACV,KAAK,EACL,KAAK,GACN,MAAM,iBAAiB,CAAC;AAIzB,YAAY,EAAE,UAAU,EAAE,WAAW,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@am_shork/attest",
3
- "version": "0.2.0",
3
+ "version": "0.2.2",
4
4
  "description": "TDD-native spec framework: tests are the source of truth for verification, ID-bound requirements the source of truth for intent.",
5
5
  "type": "module",
6
6
  "engines": {
@@ -64,6 +64,7 @@
64
64
  "scripts": {
65
65
  "build": "tsc -p tsconfig.json",
66
66
  "typecheck": "tsc -p tsconfig.json --noEmit",
67
+ "typecheck:all": "tsc -p tsconfig.typecheck.json",
67
68
  "test": "vitest run",
68
69
  "test:watch": "vitest",
69
70
  "test:consumer": "pnpm run build && vitest run --config vitest.consumer.config.ts",