@am_shork/attest 0.1.5 → 0.1.7
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 +246 -1
- package/README.md +56 -1
- package/dist/cli/index.js +81 -4
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/json.d.ts +8 -1
- package/dist/cli/json.d.ts.map +1 -1
- package/dist/cli/json.js +8 -0
- package/dist/cli/json.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 +29 -2
- package/dist/core/locate.d.ts.map +1 -1
- package/dist/core/locate.js +71 -28
- 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 +33 -0
- package/dist/core/pipeline.d.ts.map +1 -1
- package/dist/core/pipeline.js +103 -24
- package/dist/core/pipeline.js.map +1 -1
- package/dist/core/render.d.ts +39 -0
- package/dist/core/render.d.ts.map +1 -0
- package/dist/core/render.js +168 -0
- package/dist/core/render.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 +4 -2
package/dist/core/pipeline.js
CHANGED
|
@@ -1,20 +1,23 @@
|
|
|
1
1
|
// High-level operations composing the core layers (design §9). These are
|
|
2
2
|
// tool-agnostic; the CLI is a thin shell that calls them and renders the result.
|
|
3
3
|
import { createLoader } from './loader.js';
|
|
4
|
-
import { loadRegistry,
|
|
4
|
+
import { loadRegistry, parseSpecs, parseChangeSpecs, listChangeNames, scanProject, } from './locate.js';
|
|
5
5
|
import { validateStructure, detectPotentialDrift } from './validator.js';
|
|
6
6
|
import { runAndCollect } from './runner.js';
|
|
7
7
|
import { applyDelta } from './apply.js';
|
|
8
|
-
import { evaluateGate } from './gate.js';
|
|
8
|
+
import { evaluateGate, declaredNotRunIssues } from './gate.js';
|
|
9
|
+
import { renderMarkdown, staleIssue } from './render.js';
|
|
10
|
+
import { readFile } from 'node:fs/promises';
|
|
9
11
|
import { join, relative } from 'node:path';
|
|
10
12
|
import { configDefaults } from 'vitest/config';
|
|
11
13
|
import { hasError } from './types.js';
|
|
12
14
|
/** Static structural check (design §9: `attest check`). */
|
|
13
15
|
export async function runCheck(root) {
|
|
16
|
+
const scan = await scanProject(root);
|
|
14
17
|
const loader = await createLoader();
|
|
15
18
|
try {
|
|
16
|
-
const { registry, issues } = await loadRegistry(root, loader);
|
|
17
|
-
const plan = await
|
|
19
|
+
const { registry, issues } = await loadRegistry(root, loader, scan.reqsFiles);
|
|
20
|
+
const plan = await parseSpecs(scan.specFiles, root);
|
|
18
21
|
return [
|
|
19
22
|
...issues,
|
|
20
23
|
...validateStructure(registry, plan),
|
|
@@ -27,15 +30,16 @@ export async function runCheck(root) {
|
|
|
27
30
|
}
|
|
28
31
|
/** Executable verification (design §9: `attest verify`). */
|
|
29
32
|
export async function runVerify(root, options = {}) {
|
|
33
|
+
const scan = await scanProject(root);
|
|
30
34
|
const loader = await createLoader();
|
|
31
35
|
let registry;
|
|
32
36
|
let plan;
|
|
33
37
|
const issues = [];
|
|
34
38
|
try {
|
|
35
|
-
const loaded = await loadRegistry(root, loader);
|
|
39
|
+
const loaded = await loadRegistry(root, loader, scan.reqsFiles);
|
|
36
40
|
registry = loaded.registry;
|
|
37
41
|
issues.push(...loaded.issues);
|
|
38
|
-
plan = await
|
|
42
|
+
plan = await parseSpecs(scan.specFiles, root);
|
|
39
43
|
}
|
|
40
44
|
finally {
|
|
41
45
|
await loader.close();
|
|
@@ -46,26 +50,16 @@ export async function runVerify(root, options = {}) {
|
|
|
46
50
|
if (!run.passed) {
|
|
47
51
|
issues.push({ level: 'ERROR', code: 'tests-red', message: 'Some tests are failing.' });
|
|
48
52
|
}
|
|
49
|
-
|
|
50
|
-
if (!run.runtimeCoverage.get(s.reqId)?.has(s.name)) {
|
|
51
|
-
issues.push({
|
|
52
|
-
level: 'ERROR',
|
|
53
|
-
code: 'declared-not-run',
|
|
54
|
-
reqId: s.reqId,
|
|
55
|
-
file: s.file,
|
|
56
|
-
line: s.line,
|
|
57
|
-
message: `scenario "${s.name}" statically declares it attests ${s.reqId}, but never executed (skipped, or excluded by an .only?).`,
|
|
58
|
-
});
|
|
59
|
-
}
|
|
60
|
-
}
|
|
53
|
+
issues.push(...declaredNotRunIssues(plan, run));
|
|
61
54
|
return { issues, passed: run.passed, ok: run.passed && !hasError(issues) };
|
|
62
55
|
}
|
|
63
56
|
/** Coverage-only report (design §9: `attest cover`). */
|
|
64
57
|
export async function runCover(root) {
|
|
58
|
+
const scan = await scanProject(root);
|
|
65
59
|
const loader = await createLoader();
|
|
66
60
|
try {
|
|
67
|
-
const { registry } = await loadRegistry(root, loader);
|
|
68
|
-
const plan = await
|
|
61
|
+
const { registry } = await loadRegistry(root, loader, scan.reqsFiles);
|
|
62
|
+
const plan = await parseSpecs(scan.specFiles, root);
|
|
69
63
|
const counts = new Map();
|
|
70
64
|
for (const s of plan.scenarios) {
|
|
71
65
|
counts.set(s.reqId, (counts.get(s.reqId) ?? 0) + 1);
|
|
@@ -82,11 +76,96 @@ export async function runCover(root) {
|
|
|
82
76
|
await loader.close();
|
|
83
77
|
}
|
|
84
78
|
}
|
|
79
|
+
/**
|
|
80
|
+
* Markdown projection of the intent layer (design §9: `attest render`).
|
|
81
|
+
*
|
|
82
|
+
* Reads the registry and nothing else — no spec parse, no run — because the
|
|
83
|
+
* document is intent only. That is what keeps it a pure function of the
|
|
84
|
+
* `*.reqs.ts` files, so a committed rendering can be gated without going stale
|
|
85
|
+
* every time a line moves in a test file.
|
|
86
|
+
*/
|
|
87
|
+
export async function runRender(root) {
|
|
88
|
+
const loader = await createLoader();
|
|
89
|
+
try {
|
|
90
|
+
const { registry, issues } = await loadRegistry(root, loader);
|
|
91
|
+
// A registry that failed to load yields a document that silently omits
|
|
92
|
+
// requirements. An incomplete spec doc is worse than none, so refuse.
|
|
93
|
+
if (hasError(issues))
|
|
94
|
+
return { markdown: '', issues };
|
|
95
|
+
return { markdown: renderMarkdown(registry), issues };
|
|
96
|
+
}
|
|
97
|
+
finally {
|
|
98
|
+
await loader.close();
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Freshness gate for a committed rendering (`attest render --check`).
|
|
103
|
+
*
|
|
104
|
+
* A generated file checked into the repo is itself a drift surface — the exact
|
|
105
|
+
* failure mode Attest exists to remove — so the projection only earns its place
|
|
106
|
+
* alongside a gate that fails when the file no longer matches its source.
|
|
107
|
+
*/
|
|
108
|
+
export async function runRenderCheck(root, outFile, target) {
|
|
109
|
+
const { markdown, issues } = await runRender(root);
|
|
110
|
+
if (hasError(issues))
|
|
111
|
+
return issues;
|
|
112
|
+
const current = await readFile(outFile, 'utf8').catch(() => undefined);
|
|
113
|
+
const stale = staleIssue(target, current, markdown);
|
|
114
|
+
return stale ? [...issues, stale] : issues;
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* A change name must name one directory inside `changes/`, and nothing else.
|
|
118
|
+
*
|
|
119
|
+
* The name is interpolated into a path whose module is then *executed*, and
|
|
120
|
+
* `join` normalises `..` — so an unvalidated name loads code from anywhere on
|
|
121
|
+
* disk. That is reachable from CI the moment the name comes from a branch or
|
|
122
|
+
* MR title.
|
|
123
|
+
*
|
|
124
|
+
* The test is path safety, not a character whitelist: the name never reaches a
|
|
125
|
+
* glob (only *sibling* names do, and changeExcludeGlobs escapes those), so
|
|
126
|
+
* `feat(auth)` or a name with a space is a perfectly good directory and there
|
|
127
|
+
* is no reason for the gate to refuse it. Rejecting only what can escape the
|
|
128
|
+
* directory keeps the guard exactly as wide as the danger.
|
|
129
|
+
*/
|
|
130
|
+
function isSafeChangeName(name) {
|
|
131
|
+
return (name !== '' &&
|
|
132
|
+
name !== '.' &&
|
|
133
|
+
name !== '..' &&
|
|
134
|
+
!name.includes('/') &&
|
|
135
|
+
!name.includes('\\') &&
|
|
136
|
+
!name.includes('\0'));
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* The globs that keep *other* proposals out of a change's gate run.
|
|
140
|
+
*
|
|
141
|
+
* Sibling names come from `readdir`, not from the guard above, so they are
|
|
142
|
+
* escaped: a directory called `feat(auth)` pasted in raw is a *pattern*, it
|
|
143
|
+
* matches nothing, and that sibling's specs silently join the run — quietly
|
|
144
|
+
* widening the scope of the one check that decides "done".
|
|
145
|
+
*/
|
|
146
|
+
export function changeExcludeGlobs(others) {
|
|
147
|
+
return others.map((n) => `**/changes/${escapeGlob(n)}/**`);
|
|
148
|
+
}
|
|
149
|
+
function escapeGlob(name) {
|
|
150
|
+
return name.replace(/[*?[\]{}()!+@|^$]/g, '\\$&');
|
|
151
|
+
}
|
|
85
152
|
/** Archive gate for a change (design §8, §9: `attest archive <change>`). */
|
|
86
153
|
export async function runArchive(root, changeName, options = {}) {
|
|
154
|
+
// Checked before any loader starts: nothing should be resolved, let alone
|
|
155
|
+
// evaluated, on behalf of a name we have already rejected.
|
|
156
|
+
if (!isSafeChangeName(changeName)) {
|
|
157
|
+
return [
|
|
158
|
+
{
|
|
159
|
+
level: 'ERROR',
|
|
160
|
+
code: 'invalid-change-name',
|
|
161
|
+
message: `Invalid change name ${JSON.stringify(changeName)}. A change name must be one directory inside changes/: it cannot be empty, "." or "..", or contain a path separator.`,
|
|
162
|
+
},
|
|
163
|
+
];
|
|
164
|
+
}
|
|
165
|
+
const scan = await scanProject(root);
|
|
87
166
|
const loader = await createLoader();
|
|
88
167
|
try {
|
|
89
|
-
const { registry: base, issues } = await loadRegistry(root, loader);
|
|
168
|
+
const { registry: base, issues } = await loadRegistry(root, loader, scan.reqsFiles);
|
|
90
169
|
if (issues.some((i) => i.level === 'ERROR'))
|
|
91
170
|
return issues;
|
|
92
171
|
const deltaPath = join(root, 'changes', changeName, 'requirements.delta.ts');
|
|
@@ -109,7 +188,7 @@ export async function runArchive(root, changeName, options = {}) {
|
|
|
109
188
|
if (applied.issues.length > 0)
|
|
110
189
|
return applied.issues;
|
|
111
190
|
// Static plan = merged base suite + this change's specs (design §8).
|
|
112
|
-
const basePlan = await
|
|
191
|
+
const basePlan = await parseSpecs(scan.specFiles, root);
|
|
113
192
|
const changePlan = await parseChangeSpecs(root, changeName);
|
|
114
193
|
const plan = {
|
|
115
194
|
scenarios: [...basePlan.scenarios, ...changePlan.scenarios],
|
|
@@ -120,7 +199,7 @@ export async function runArchive(root, changeName, options = {}) {
|
|
|
120
199
|
const exclude = [
|
|
121
200
|
...configDefaults.exclude,
|
|
122
201
|
'**/archive/**',
|
|
123
|
-
...others
|
|
202
|
+
...changeExcludeGlobs(others),
|
|
124
203
|
];
|
|
125
204
|
const run = await runAndCollect({ root, quiet: true, exclude, vitestConfig: options.vitestConfig });
|
|
126
205
|
return evaluateGate({ registry: applied.registry, plan, run });
|
|
@@ -131,6 +210,6 @@ export async function runArchive(root, changeName, options = {}) {
|
|
|
131
210
|
}
|
|
132
211
|
/** True when a spec suite exists at all (used by the CLI to warn on empty roots). */
|
|
133
212
|
export async function hasSpecs(root) {
|
|
134
|
-
return (await
|
|
213
|
+
return (await scanProject(root)).specFiles.length > 0;
|
|
135
214
|
}
|
|
136
215
|
//# sourceMappingURL=pipeline.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"pipeline.js","sourceRoot":"","sources":["../../src/core/pipeline.ts"],"names":[],"mappings":"AAAA,yEAAyE;AACzE,iFAAiF;AAEjF,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAC3C,OAAO,EACL,YAAY,EACZ,
|
|
1
|
+
{"version":3,"file":"pipeline.js","sourceRoot":"","sources":["../../src/core/pipeline.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { Issue, Registry } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Render the registry as a standalone Markdown document.
|
|
4
|
+
*
|
|
5
|
+
* Deliberately takes no formatting options (not even a title): every knob that
|
|
6
|
+
* changes the output is a way for `--check` to disagree with the run that
|
|
7
|
+
* generated the file, and a freshness gate that can be wrong about staleness is
|
|
8
|
+
* worse than no gate.
|
|
9
|
+
*/
|
|
10
|
+
export declare function renderMarkdown(registry: Registry): string;
|
|
11
|
+
/**
|
|
12
|
+
* How to talk about the file being gated. Both strings come from the caller
|
|
13
|
+
* rather than being derived from the paths: a path relative to the *project
|
|
14
|
+
* root* is not the path the user typed, and a command rebuilt from it drops the
|
|
15
|
+
* `[dir]` argument — which made the fix hint name a different registry than the
|
|
16
|
+
* one being checked.
|
|
17
|
+
*/
|
|
18
|
+
export interface RenderTarget {
|
|
19
|
+
/** The output path as the user wrote it. */
|
|
20
|
+
display: string;
|
|
21
|
+
/** The exact command that regenerates the file. */
|
|
22
|
+
command: string;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* The freshness verdict for a committed rendering: `undefined` when the file on
|
|
26
|
+
* disk is exactly what the registry renders to now, an ERROR otherwise.
|
|
27
|
+
*
|
|
28
|
+
* The two failures get distinct codes — `missing-spec-doc` for a rendering that
|
|
29
|
+
* was never generated, `stale-spec-doc` for one whose bytes drifted — even
|
|
30
|
+
* though the fix is the same command. `message` is prose and not part of the
|
|
31
|
+
* API, so a code shared between the two would leave a consumer that cares about
|
|
32
|
+
* the difference (a first-time setup versus a document going out of date) with
|
|
33
|
+
* nothing to branch on but the wording.
|
|
34
|
+
*
|
|
35
|
+
* Kept here rather than in the pipeline, and pure, because this is the verdict
|
|
36
|
+
* that makes a generated file safe to commit: a byte comparison, no heuristics.
|
|
37
|
+
*/
|
|
38
|
+
export declare function staleIssue(target: RenderTarget, current: string | undefined, fresh: string): Issue | undefined;
|
|
39
|
+
//# sourceMappingURL=render.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"render.d.ts","sourceRoot":"","sources":["../../src/core/render.ts"],"names":[],"mappings":"AAwBA,OAAO,KAAK,EAAE,KAAK,EAAE,QAAQ,EAAe,MAAM,YAAY,CAAC;AAQ/D;;;;;;;GAOG;AACH,wBAAgB,cAAc,CAAC,QAAQ,EAAE,QAAQ,GAAG,MAAM,CAezD;AAED;;;;;;GAMG;AACH,MAAM,WAAW,YAAY;IAC3B,4CAA4C;IAC5C,OAAO,EAAE,MAAM,CAAC;IAChB,mDAAmD;IACnD,OAAO,EAAE,MAAM,CAAC;CACjB;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,UAAU,CACxB,MAAM,EAAE,YAAY,EACpB,OAAO,EAAE,MAAM,GAAG,SAAS,EAC3B,KAAK,EAAE,MAAM,GACZ,KAAK,GAAG,SAAS,CAYnB"}
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
// Human-readable projection of the intent layer (design §0: Markdown is an
|
|
2
|
+
// *output*, never an input).
|
|
3
|
+
//
|
|
4
|
+
// The registry is the only source of truth; this module renders it for people
|
|
5
|
+
// who do not read TypeScript — reviewers, QA, audit. Nothing here is ever read
|
|
6
|
+
// back, so the direction OpenSpec's pure-Markdown model went (Markdown as
|
|
7
|
+
// truth, and the free-form drift that comes with it) stays closed.
|
|
8
|
+
//
|
|
9
|
+
// Three properties this file must keep:
|
|
10
|
+
// - **Intent only.** The document says what the system promises, never what is
|
|
11
|
+
// proven or green: coverage and results are verdicts, and verdicts belong to
|
|
12
|
+
// `cover` / `verify`, which recompute them on demand. Putting them here also
|
|
13
|
+
// made the document churn on code that changed no intent at all — a line
|
|
14
|
+
// shifting in a spec file rewrote it — and in a green repo the coverage was
|
|
15
|
+
// constant anyway (an uncovered requirement is a `check` ERROR), so it was
|
|
16
|
+
// diff noise carrying no information.
|
|
17
|
+
// - **A pure function of the registry.** No parse, no run, no options. The
|
|
18
|
+
// rendering therefore changes if and only if a `*.reqs.ts` changes, which is
|
|
19
|
+
// what makes `--check` a signal instead of a chore.
|
|
20
|
+
// - **Params interpolated into the statement.** The source says
|
|
21
|
+
// "{idleTimeoutMin} minutes"; a human reader wants "30 minutes". This is the
|
|
22
|
+
// one thing the projection gives that reading the source does not.
|
|
23
|
+
import { byCodeUnit } from './order.js';
|
|
24
|
+
const BANNER = '<!-- Generated by `attest render` — do not edit. Edit the `*.reqs.ts` registry and regenerate. -->';
|
|
25
|
+
/**
|
|
26
|
+
* Render the registry as a standalone Markdown document.
|
|
27
|
+
*
|
|
28
|
+
* Deliberately takes no formatting options (not even a title): every knob that
|
|
29
|
+
* changes the output is a way for `--check` to disagree with the run that
|
|
30
|
+
* generated the file, and a freshness gate that can be wrong about staleness is
|
|
31
|
+
* worse than no gate.
|
|
32
|
+
*/
|
|
33
|
+
export function renderMarkdown(registry) {
|
|
34
|
+
const ids = Object.keys(registry).sort(compareIds);
|
|
35
|
+
const out = [BANNER, '', '# Requirements', ''];
|
|
36
|
+
if (ids.length === 0) {
|
|
37
|
+
out.push('_No requirements are defined yet._', '');
|
|
38
|
+
return out.join('\n');
|
|
39
|
+
}
|
|
40
|
+
out.push(...overviewTable(registry, ids), '');
|
|
41
|
+
for (const id of ids) {
|
|
42
|
+
out.push(...section(id, registry[id]), '');
|
|
43
|
+
}
|
|
44
|
+
return out.join('\n');
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* The freshness verdict for a committed rendering: `undefined` when the file on
|
|
48
|
+
* disk is exactly what the registry renders to now, an ERROR otherwise.
|
|
49
|
+
*
|
|
50
|
+
* The two failures get distinct codes — `missing-spec-doc` for a rendering that
|
|
51
|
+
* was never generated, `stale-spec-doc` for one whose bytes drifted — even
|
|
52
|
+
* though the fix is the same command. `message` is prose and not part of the
|
|
53
|
+
* API, so a code shared between the two would leave a consumer that cares about
|
|
54
|
+
* the difference (a first-time setup versus a document going out of date) with
|
|
55
|
+
* nothing to branch on but the wording.
|
|
56
|
+
*
|
|
57
|
+
* Kept here rather than in the pipeline, and pure, because this is the verdict
|
|
58
|
+
* that makes a generated file safe to commit: a byte comparison, no heuristics.
|
|
59
|
+
*/
|
|
60
|
+
export function staleIssue(target, current, fresh) {
|
|
61
|
+
if (current === fresh)
|
|
62
|
+
return undefined;
|
|
63
|
+
const missing = current === undefined;
|
|
64
|
+
const why = missing
|
|
65
|
+
? `No rendering at "${target.display}".`
|
|
66
|
+
: `"${target.display}" no longer matches the registry.`;
|
|
67
|
+
return {
|
|
68
|
+
level: 'ERROR',
|
|
69
|
+
code: missing ? 'missing-spec-doc' : 'stale-spec-doc',
|
|
70
|
+
file: target.display,
|
|
71
|
+
message: `${why} Regenerate it with: ${target.command}`,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Order ids the way a reader expects: prefix alphabetically, then the number
|
|
76
|
+
* numerically. A plain string sort puts ATX-10 between ATX-1 and ATX-2, which
|
|
77
|
+
* scrambles the document as soon as a registry reaches ten requirements.
|
|
78
|
+
*/
|
|
79
|
+
function compareIds(a, b) {
|
|
80
|
+
const split = (id) => {
|
|
81
|
+
const at = id.lastIndexOf('-');
|
|
82
|
+
return [id.slice(0, at), Number(id.slice(at + 1))];
|
|
83
|
+
};
|
|
84
|
+
const [prefixA, numA] = split(a);
|
|
85
|
+
const [prefixB, numB] = split(b);
|
|
86
|
+
// byCodeUnit, not localeCompare: the ordering has to be identical on every
|
|
87
|
+
// machine, or `--check` would flap with the runner's locale.
|
|
88
|
+
if (prefixA !== prefixB)
|
|
89
|
+
return byCodeUnit(prefixA, prefixB);
|
|
90
|
+
return numA - numB;
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* An index of the whole registry: id + the promise itself. Deliberately carries
|
|
94
|
+
* no counts — a roll-up would conflict in every pair of parallel branches that
|
|
95
|
+
* each add a requirement, for a number the table below already shows.
|
|
96
|
+
*/
|
|
97
|
+
function overviewTable(registry, ids) {
|
|
98
|
+
const rows = ids.map((id) => {
|
|
99
|
+
const req = registry[id];
|
|
100
|
+
return `| [${id}](${anchor(id)}) | ${cell(interpolate(req.statement, req.params))} |`;
|
|
101
|
+
});
|
|
102
|
+
return ['| ID | Requirement |', '| --- | --- |', ...rows];
|
|
103
|
+
}
|
|
104
|
+
function section(id, req) {
|
|
105
|
+
const out = [
|
|
106
|
+
`## ${id}`,
|
|
107
|
+
'',
|
|
108
|
+
interpolate(req.statement, req.params),
|
|
109
|
+
'',
|
|
110
|
+
`**Why:** ${req.rationale}`,
|
|
111
|
+
];
|
|
112
|
+
const params = Object.entries(req.params);
|
|
113
|
+
if (params.length > 0) {
|
|
114
|
+
out.push('', '| Param | Value |', '| --- | --- |', ...params.map(([name, value]) => `| ${code(name)} | ${cell(formatValue(value))} |`));
|
|
115
|
+
}
|
|
116
|
+
if (req.outOfScope.length > 0) {
|
|
117
|
+
out.push('', '**Out of scope**', '', ...req.outOfScope.map((s) => `- ${s}`));
|
|
118
|
+
}
|
|
119
|
+
return out;
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Substitute `{param}` placeholders with the actual value, emphasised so a
|
|
123
|
+
* reader can see which words in the sentence are pinned to a single source.
|
|
124
|
+
* An unbound placeholder is left verbatim — `check` already reports it as an
|
|
125
|
+
* `unbound-param` error, and silently swallowing it here would hide that.
|
|
126
|
+
*/
|
|
127
|
+
function interpolate(statement, params) {
|
|
128
|
+
// `Object.hasOwn`, never `in`: `'toString' in {}` is true, and an `in` probe
|
|
129
|
+
// would splice `function toString() { [native code] }` into a document that
|
|
130
|
+
// reviewers and audit read as the system's promise.
|
|
131
|
+
return statement.replace(/\{(\w+)\}/g, (whole, name) => Object.hasOwn(params, name) ? `**${plain(params[name])}**` : whole);
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* A param value as prose, for inline use inside a statement. Escaped, unlike
|
|
135
|
+
* the statement and rationale around it: those are prose an author may want to
|
|
136
|
+
* mark up, a param value is data and must survive verbatim — an unescaped `*`
|
|
137
|
+
* would otherwise break out of the emphasis it is wrapped in.
|
|
138
|
+
*/
|
|
139
|
+
function plain(value) {
|
|
140
|
+
const one = (v) => v.replace(/([*_`[\]\\])/g, '\\$1');
|
|
141
|
+
return Array.isArray(value) ? value.map((v) => one(String(v))).join(', ') : one(String(value));
|
|
142
|
+
}
|
|
143
|
+
/** A param value as code, for the params table. */
|
|
144
|
+
function formatValue(value) {
|
|
145
|
+
return Array.isArray(value)
|
|
146
|
+
? value.map((v) => code(String(v))).join(', ')
|
|
147
|
+
: code(String(value));
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Wrap text in a code span that survives backticks in the value: the fence has
|
|
151
|
+
* to be longer than the longest run inside it, and a value touching a backtick
|
|
152
|
+
* at either end needs padding spaces.
|
|
153
|
+
*/
|
|
154
|
+
function code(value) {
|
|
155
|
+
const longest = Math.max(0, ...[...value.matchAll(/`+/g)].map((m) => m[0].length));
|
|
156
|
+
const fence = '`'.repeat(longest + 1);
|
|
157
|
+
const pad = value.startsWith('`') || value.endsWith('`') ? ' ' : '';
|
|
158
|
+
return `${fence}${pad}${value}${pad}${fence}`;
|
|
159
|
+
}
|
|
160
|
+
/** Make prose safe inside a table cell: no row-breaking pipes, no newlines. */
|
|
161
|
+
function cell(text) {
|
|
162
|
+
return text.replace(/\s*\n\s*/g, ' ').replace(/\|/g, '\\|');
|
|
163
|
+
}
|
|
164
|
+
/** GitHub/GitLab slug for a `## AUTH-3` heading. */
|
|
165
|
+
function anchor(id) {
|
|
166
|
+
return `#${id.toLowerCase()}`;
|
|
167
|
+
}
|
|
168
|
+
//# sourceMappingURL=render.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"render.js","sourceRoot":"","sources":["../../src/core/render.ts"],"names":[],"mappings":"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"}
|
|
@@ -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,
|
|
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"}
|
package/dist/core/validator.js
CHANGED
|
@@ -35,10 +35,13 @@ export function validateStructure(registry, plan) {
|
|
|
35
35
|
}
|
|
36
36
|
}
|
|
37
37
|
// unbound-param: statement uses {param} but params does not define it.
|
|
38
|
+
// `Object.hasOwn`, never `in`: `'toString' in {}` is true, so an `in` probe
|
|
39
|
+
// silently accepts placeholders no author declared — and render.ts would then
|
|
40
|
+
// interpolate the prototype method into the document.
|
|
38
41
|
for (const [id, req] of Object.entries(registry)) {
|
|
39
42
|
for (const m of req.statement.matchAll(/\{(\w+)\}/g)) {
|
|
40
43
|
const name = m[1];
|
|
41
|
-
if (!(
|
|
44
|
+
if (!Object.hasOwn(req.params, name)) {
|
|
42
45
|
issues.push({
|
|
43
46
|
level: 'ERROR',
|
|
44
47
|
code: 'unbound-param',
|
|
@@ -61,15 +64,24 @@ export function validateStructure(registry, plan) {
|
|
|
61
64
|
export function detectPotentialDrift(registry, plan, refs) {
|
|
62
65
|
const readsParams = new Set(refs.map((r) => r.reqId));
|
|
63
66
|
const issues = [];
|
|
67
|
+
// Index the scenarios once. Filtering the whole plan per requirement made
|
|
68
|
+
// this O(requirements x scenarios), which is the product that grows fastest
|
|
69
|
+
// in exactly the repos where `check` needs to stay fast.
|
|
70
|
+
const firstCovering = new Map();
|
|
71
|
+
for (const s of plan.scenarios) {
|
|
72
|
+
if (!firstCovering.has(s.reqId))
|
|
73
|
+
firstCovering.set(s.reqId, s);
|
|
74
|
+
}
|
|
64
75
|
for (const [id, req] of Object.entries(registry)) {
|
|
65
76
|
if (Object.keys(req.params).length === 0)
|
|
66
77
|
continue;
|
|
67
|
-
|
|
68
|
-
if (covering.length === 0 || readsParams.has(id))
|
|
78
|
+
if (readsParams.has(id))
|
|
69
79
|
continue;
|
|
70
80
|
// Anchor the hint at the first covering scenario so it stays clickable,
|
|
71
81
|
// even though the verdict is now about the requirement as a whole.
|
|
72
|
-
const at =
|
|
82
|
+
const at = firstCovering.get(id);
|
|
83
|
+
if (!at)
|
|
84
|
+
continue;
|
|
73
85
|
issues.push({
|
|
74
86
|
level: 'INFO',
|
|
75
87
|
code: 'possible-drift',
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"validator.js","sourceRoot":"","sources":["../../src/core/validator.ts"],"names":[],"mappings":"AAAA,kEAAkE;AAClE,8EAA8E;AAI9E;;;;;GAKG;AACH,MAAM,UAAU,iBAAiB,CAAC,QAAkB,EAAE,IAAgB;IACpE,MAAM,MAAM,GAAY,EAAE,CAAC;IAC3B,MAAM,QAAQ,GAAG,IAAI,GAAG,CAAC,MAAM,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC,CAAC;IAChD,MAAM,UAAU,GAAG,IAAI,GAAG,CAAC,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC;IAE/D,yDAAyD;IACzD,KAAK,MAAM,CAAC,IAAI,IAAI,CAAC,SAAS,EAAE,CAAC;QAC/B,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,KAAK,CAAC,EAAE,CAAC;YAC3B,MAAM,CAAC,IAAI,CAAC;gBACV,KAAK,EAAE,OAAO;gBACd,IAAI,EAAE,aAAa;gBACnB,KAAK,EAAE,CAAC,CAAC,KAAK;gBACd,IAAI,EAAE,CAAC,CAAC,IAAI;gBACZ,IAAI,EAAE,CAAC,CAAC,IAAI;gBACZ,OAAO,EAAE,aAAa,CAAC,CAAC,IAAI,kCAAkC,CAAC,CAAC,KAAK,2CAA2C;aACjH,CAAC,CAAC;QACL,CAAC;IACH,CAAC;IAED,mEAAmE;IACnE,KAAK,MAAM,EAAE,IAAI,QAAQ,EAAE,CAAC;QAC1B,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,EAAE,CAAC,EAAE,CAAC;YACxB,MAAM,CAAC,IAAI,CAAC;gBACV,KAAK,EAAE,OAAO;gBACd,IAAI,EAAE,uBAAuB;gBAC7B,KAAK,EAAE,EAAE;gBACT,OAAO,EAAE,gBAAgB,EAAE,sEAAsE;aAClG,CAAC,CAAC;QACL,CAAC;IACH,CAAC;IAED,uEAAuE;IACvE,KAAK,MAAM,CAAC,EAAE,EAAE,GAAG,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,QAAQ,CAAC,EAAE,CAAC;QACjD,KAAK,MAAM,CAAC,IAAI,GAAG,CAAC,SAAS,CAAC,QAAQ,CAAC,YAAY,CAAC,EAAE,CAAC;YACrD,MAAM,IAAI,GAAG,CAAC,CAAC,CAAC,CAAE,CAAC;YACnB,IAAI,CAAC,CAAC,
|
|
1
|
+
{"version":3,"file":"validator.js","sourceRoot":"","sources":["../../src/core/validator.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@am_shork/attest",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.7",
|
|
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": {
|
|
@@ -71,6 +71,8 @@
|
|
|
71
71
|
"attest": "node bin/attest.js",
|
|
72
72
|
"check:self": "node bin/attest.js check self",
|
|
73
73
|
"verify:self": "node bin/attest.js verify self",
|
|
74
|
-
"cover:self": "node bin/attest.js cover self"
|
|
74
|
+
"cover:self": "node bin/attest.js cover self",
|
|
75
|
+
"render:self": "node bin/attest.js render self --out self/requirements/SPEC.md",
|
|
76
|
+
"render:self:check": "node bin/attest.js render self --out self/requirements/SPEC.md --check"
|
|
75
77
|
}
|
|
76
78
|
}
|