@am_shork/attest 0.5.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +1063 -143
- package/README.md +1 -1
- package/dist/cli/index.js +13 -11
- package/dist/cli/json.js +6 -1
- package/dist/cli/report.d.ts +18 -0
- package/dist/cli/report.js +41 -0
- package/dist/core/compiler.d.ts +40 -0
- package/dist/core/compiler.js +64 -0
- package/dist/core/docs.d.ts +1 -1
- package/dist/core/docs.js +6 -0
- package/dist/core/gate.d.ts +48 -2
- package/dist/core/gate.js +73 -14
- package/dist/core/loader.js +13 -0
- package/dist/core/locate.d.ts +62 -0
- package/dist/core/locate.js +94 -11
- package/dist/core/merge.d.ts +54 -0
- package/dist/core/merge.js +257 -0
- package/dist/core/pipeline.d.ts +13 -0
- package/dist/core/pipeline.js +319 -40
- package/dist/core/render.js +129 -14
- package/dist/core/req-suite.d.ts +5 -0
- package/dist/core/req-suite.js +27 -0
- package/dist/core/runner.js +26 -8
- package/dist/core/skill.js +52 -12
- package/dist/core/splice.d.ts +52 -0
- package/dist/core/splice.js +189 -0
- package/dist/core/static-registry.d.ts +30 -0
- package/dist/core/static-registry.js +36 -0
- package/dist/core/status.js +21 -3
- package/dist/core/terminal.d.ts +23 -0
- package/dist/core/terminal.js +25 -9
- package/dist/core/validator.d.ts +5 -1
- package/dist/core/validator.js +23 -2
- package/dist/runtime.d.ts +20 -0
- package/dist/runtime.js +43 -15
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -157,7 +157,7 @@ Every diagnostic carries a `code`, and every code has a section in
|
|
|
157
157
|
```
|
|
158
158
|
ERROR registry-not-static (requirements/upload.reqs.ts:5)
|
|
159
159
|
Value is not a literal.
|
|
160
|
-
→ https://gitlab.com/Pseudorca/attest/-/blob/v0.
|
|
160
|
+
→ https://gitlab.com/Pseudorca/attest/-/blob/v0.7.0/docs/en/troubleshooting.md#registry-not-static
|
|
161
161
|
```
|
|
162
162
|
|
|
163
163
|
The anchor **is** the code, so the link cannot point somewhere the section
|
package/dist/cli/index.js
CHANGED
|
@@ -11,12 +11,17 @@ import { Command } from 'commander';
|
|
|
11
11
|
import chalk from 'chalk';
|
|
12
12
|
import { resolve } from 'node:path';
|
|
13
13
|
import { writeAtomic } from '../core/write.js';
|
|
14
|
-
import { runCheck, runVerify, runCover, runArchive, runInit, runRender, runRenderCheck, runStatus, DEFAULT_TARGET, TARGET_NAMES, } from '../core/pipeline.js';
|
|
14
|
+
import { runCheck, runVerify, runCover, runArchive, runArchiveApply, runInit, runRender, runRenderCheck, runStatus, DEFAULT_TARGET, TARGET_NAMES, } from '../core/pipeline.js';
|
|
15
15
|
import { hasError } from '../core/types.js';
|
|
16
|
-
import { formatCrash, formatIssues, formatScope, summarize, formatCoverage, formatStatus, } from './report.js';
|
|
16
|
+
import { formatArchiveVerdict, formatCrash, formatIssues, formatScope, summarize, formatCoverage, formatStatus, } from './report.js';
|
|
17
17
|
import { archiveReport, checkReport, coverReport, errorReport, initReport, renderJson, renderReport, statusReport, verifyReport, } from './json.js';
|
|
18
18
|
import { packageVersion } from '../core/version.js';
|
|
19
19
|
const VERSION = packageVersion();
|
|
20
|
+
/** `runArchive` in the shape `--apply` returns, so the two share one call site. */
|
|
21
|
+
const gateOnly = async (dir, change, options) => ({
|
|
22
|
+
issues: await runArchive(dir, change, options),
|
|
23
|
+
written: [],
|
|
24
|
+
});
|
|
20
25
|
const program = new Command();
|
|
21
26
|
program
|
|
22
27
|
.name('attest')
|
|
@@ -267,20 +272,17 @@ program
|
|
|
267
272
|
.argument('[dir]', 'project root', undefined)
|
|
268
273
|
.option(JSON_FLAG, JSON_HELP)
|
|
269
274
|
.option(VITEST_CONFIG_FLAG, VITEST_CONFIG_HELP)
|
|
275
|
+
.option('--apply', 'if the gate passes, merge the change: splice its ADDED requirements into the registry, rename its proposed specs, and move the change folder to archive/')
|
|
270
276
|
.description('Run the archive gate; merge only if it passes.')
|
|
271
277
|
.action((change, dir, opts) => runAction('archive', opts, async () => {
|
|
272
|
-
const
|
|
278
|
+
const run = opts.apply ? runArchiveApply : gateOnly;
|
|
279
|
+
const { issues: blocking, written } = await run(root(dir), change, {
|
|
280
|
+
vitestConfig: vitestConfig(opts),
|
|
281
|
+
});
|
|
273
282
|
return {
|
|
274
283
|
report: archiveReport(VERSION, change, blocking),
|
|
275
284
|
human: () => {
|
|
276
|
-
|
|
277
|
-
console.log(chalk.green(`✓ Gate passed: change "${change}" can be archived.`));
|
|
278
|
-
}
|
|
279
|
-
else {
|
|
280
|
-
console.log(chalk.red(`✗ Gate failed: change "${change}"`));
|
|
281
|
-
console.log(formatIssues(blocking));
|
|
282
|
-
console.log(summarize(blocking));
|
|
283
|
-
}
|
|
285
|
+
console.log(formatArchiveVerdict(change, blocking, { applied: opts.apply === true, written }));
|
|
284
286
|
},
|
|
285
287
|
};
|
|
286
288
|
}));
|
package/dist/cli/json.js
CHANGED
|
@@ -112,7 +112,12 @@ export function archiveReport(version, change, blocking) {
|
|
|
112
112
|
...envelope({
|
|
113
113
|
version,
|
|
114
114
|
command: 'archive',
|
|
115
|
-
|
|
115
|
+
// `hasError`, not `blocking.length === 0` (ATX-60). The gate deliberately
|
|
116
|
+
// keeps its non-blocking output through `--apply`, so a WARNING in here
|
|
117
|
+
// is planned for — and counting one as a failure made this command answer
|
|
118
|
+
// not-ok, exit 1, and print a failed gate *after* the merge had written
|
|
119
|
+
// the files. Every other command already asks the question this way.
|
|
120
|
+
ok: !hasError(blocking),
|
|
116
121
|
issues: blocking,
|
|
117
122
|
}),
|
|
118
123
|
change,
|
package/dist/cli/report.d.ts
CHANGED
|
@@ -57,5 +57,23 @@ export declare function formatScope(scope: VerifyCounts): string;
|
|
|
57
57
|
* the argument that keeps `render --check` comparing documents rather than bytes.
|
|
58
58
|
*/
|
|
59
59
|
export declare function formatStatus(result: StatusResult): string;
|
|
60
|
+
/**
|
|
61
|
+
* The verdict `attest archive` prints, and the lines under it.
|
|
62
|
+
*
|
|
63
|
+
* Here rather than inline in the CLI action for two reasons, and the second is
|
|
64
|
+
* the one that matters. The shell is a thin veneer that renders what the core
|
|
65
|
+
* decided; and while this lived in a closure it was the *only* copy of the
|
|
66
|
+
* verdict nothing could test — so when it asked `blocking.length === 0` of its
|
|
67
|
+
* own accord, no scenario could see that the line above the issues could call
|
|
68
|
+
* the gate failed while the JSON beside it called the command ok (ATX-60).
|
|
69
|
+
*
|
|
70
|
+
* `hasError`, the same predicate `archiveReport` uses. Not a second reading of
|
|
71
|
+
* the same array so much as the same question asked once and rendered twice —
|
|
72
|
+
* which is what the requirement is about.
|
|
73
|
+
*/
|
|
74
|
+
export declare function formatArchiveVerdict(change: string, blocking: Issue[], merge: {
|
|
75
|
+
applied: boolean;
|
|
76
|
+
written: readonly string[];
|
|
77
|
+
}): string;
|
|
60
78
|
export declare function formatCoverage(rows: CoverageRow[]): string;
|
|
61
79
|
//# sourceMappingURL=report.d.ts.map
|
package/dist/cli/report.js
CHANGED
|
@@ -4,6 +4,7 @@ import chalk from 'chalk';
|
|
|
4
4
|
import { docsUrl } from '../core/docs.js';
|
|
5
5
|
import { byCodeUnit } from '../core/order.js';
|
|
6
6
|
import { INDENT, block, inline } from '../core/terminal.js';
|
|
7
|
+
import { hasError } from '../core/types.js';
|
|
7
8
|
const LEVEL_TAG = {
|
|
8
9
|
ERROR: (s) => chalk.red.bold(s),
|
|
9
10
|
WARNING: (s) => chalk.yellow.bold(s),
|
|
@@ -184,6 +185,46 @@ export function formatStatus(result) {
|
|
|
184
185
|
lines.push(chalk.dim(`Not a verdict: run \`attest archive ${inline(result.change)}\` to run the suite.`));
|
|
185
186
|
return lines.join('\n');
|
|
186
187
|
}
|
|
188
|
+
/**
|
|
189
|
+
* The verdict `attest archive` prints, and the lines under it.
|
|
190
|
+
*
|
|
191
|
+
* Here rather than inline in the CLI action for two reasons, and the second is
|
|
192
|
+
* the one that matters. The shell is a thin veneer that renders what the core
|
|
193
|
+
* decided; and while this lived in a closure it was the *only* copy of the
|
|
194
|
+
* verdict nothing could test — so when it asked `blocking.length === 0` of its
|
|
195
|
+
* own accord, no scenario could see that the line above the issues could call
|
|
196
|
+
* the gate failed while the JSON beside it called the command ok (ATX-60).
|
|
197
|
+
*
|
|
198
|
+
* `hasError`, the same predicate `archiveReport` uses. Not a second reading of
|
|
199
|
+
* the same array so much as the same question asked once and rendered twice —
|
|
200
|
+
* which is what the requirement is about.
|
|
201
|
+
*/
|
|
202
|
+
export function formatArchiveVerdict(change, blocking, merge) {
|
|
203
|
+
if (hasError(blocking)) {
|
|
204
|
+
return [
|
|
205
|
+
chalk.red(`✗ Gate failed: change "${inline(change)}"`),
|
|
206
|
+
formatIssues(blocking),
|
|
207
|
+
summarize(blocking),
|
|
208
|
+
].join('\n');
|
|
209
|
+
}
|
|
210
|
+
const lines = [
|
|
211
|
+
chalk.green(merge.applied
|
|
212
|
+
? `✓ Gate passed and merged: change "${inline(change)}".`
|
|
213
|
+
: `✓ Gate passed: change "${inline(change)}" can be archived.`),
|
|
214
|
+
];
|
|
215
|
+
// Every path the merge touched, because this is the command that edits files
|
|
216
|
+
// the user cannot regenerate and they need to know which ones to read before
|
|
217
|
+
// committing. It is also what a working-tree check would only have
|
|
218
|
+
// approximated, and it works for someone not using version control at all.
|
|
219
|
+
for (const path of merge.written)
|
|
220
|
+
lines.push(chalk.dim(` ${inline(path)}`));
|
|
221
|
+
// A WARNING that did not fail the gate still has to be shown, or `--apply`
|
|
222
|
+
// would be quieter than the same command without it — the property
|
|
223
|
+
// `runArchiveApply` keeps its non-blocking output for.
|
|
224
|
+
if (blocking.length > 0)
|
|
225
|
+
lines.push(formatIssues(blocking), summarize(blocking));
|
|
226
|
+
return lines.join('\n');
|
|
227
|
+
}
|
|
187
228
|
export function formatCoverage(rows) {
|
|
188
229
|
if (rows.length === 0)
|
|
189
230
|
return chalk.dim('(the registry contains no requirements)');
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { Issue } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* The range the readers are written against.
|
|
4
|
+
*
|
|
5
|
+
* Stated here rather than read from the manifest at runtime, because the
|
|
6
|
+
* manifest is not on disk beside a bundled build and a diagnostic that cannot
|
|
7
|
+
* name the range is most of the diagnostic gone. `ATX-56` keeps the two honest:
|
|
8
|
+
* its `params` carry this string, and its scenario compares them with
|
|
9
|
+
* `package.json`, so the constant cannot drift from the dependency it describes.
|
|
10
|
+
*/
|
|
11
|
+
export declare const SUPPORTED_TYPESCRIPT = "^5.5.0 || ^6.0.0";
|
|
12
|
+
/**
|
|
13
|
+
* The part of the compiler surface every AST reader in this project touches.
|
|
14
|
+
*
|
|
15
|
+
* Deliberately the two members that are reached *first* rather than an
|
|
16
|
+
* exhaustive list: `createSourceFile` and `ScriptTarget` are what
|
|
17
|
+
* `parser.ts`, `static-registry.ts` and `splice.ts` all open with, so a compiler
|
|
18
|
+
* that has them has the shape this check is about, and one that lacks either
|
|
19
|
+
* cannot get far enough for a longer list to matter.
|
|
20
|
+
*/
|
|
21
|
+
interface CompilerSurface {
|
|
22
|
+
version?: unknown;
|
|
23
|
+
createSourceFile?: unknown;
|
|
24
|
+
ScriptTarget?: unknown;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* An ERROR when the resolved compiler has no AST API, or `undefined` when it
|
|
28
|
+
* does.
|
|
29
|
+
*
|
|
30
|
+
* Takes the compiler rather than reaching for the import directly, so the
|
|
31
|
+
* refusal can be attested against a stand-in: the condition only exists on a
|
|
32
|
+
* compiler this repository cannot install alongside the one it builds with, and
|
|
33
|
+
* a check nothing can make fail is the shape `never-red` exists to catch.
|
|
34
|
+
*
|
|
35
|
+
* No `reqId` and no `file`: the finding is about the toolchain, and every
|
|
36
|
+
* requirement and every file in the project is equally unreadable because of it.
|
|
37
|
+
*/
|
|
38
|
+
export declare function compilerIssue(compiler?: CompilerSurface): Issue | undefined;
|
|
39
|
+
export {};
|
|
40
|
+
//# sourceMappingURL=compiler.d.ts.map
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
// Whether the resolved TypeScript can be read from at all (design §5.1).
|
|
2
|
+
//
|
|
3
|
+
// Both registry readers and the spec parser walk the AST with the compiler API,
|
|
4
|
+
// which makes `typescript` the one dependency this tool cannot survive the
|
|
5
|
+
// absence of — and "absent" is not the shape it arrives in. `typescript@7` is
|
|
6
|
+
// the native port: its package resolves, imports cleanly, and answers `version`
|
|
7
|
+
// and `versionMajorMinor`, while `createSourceFile` and every `ts.isX` guard
|
|
8
|
+
// moved behind `typescript/unstable/ast*`. So the members are `undefined` rather
|
|
9
|
+
// than missing, and the first thing to touch one throws.
|
|
10
|
+
//
|
|
11
|
+
// Measured: `attest check` on it died with
|
|
12
|
+
// `TypeError: Cannot read properties of undefined (reading 'Latest')` and a raw
|
|
13
|
+
// stack — no issue code, no fix hint, and nothing saying which compiler it
|
|
14
|
+
// wanted, on the crash path `[0.4.2]` had just finished sanitising.
|
|
15
|
+
//
|
|
16
|
+
// **The declared range excludes 7, and that is not the same as being safe from
|
|
17
|
+
// it.** A caret is a request, not a constraint the adopter cannot override:
|
|
18
|
+
// `pnpm.overrides` and Yarn `resolutions` both pin a transitive dependency
|
|
19
|
+
// across a whole tree, and a repo standardising on one compiler is exactly the
|
|
20
|
+
// repo that uses them — this one has an `overrides` block of its own. So the
|
|
21
|
+
// reachable case is a deliberate, reasonable act by an adopter, answered with a
|
|
22
|
+
// stack trace.
|
|
23
|
+
//
|
|
24
|
+
// What this cannot do is make Attest work on TypeScript 7. That is a migration
|
|
25
|
+
// to `typescript/unstable/ast`, against an API whose own name says not to depend
|
|
26
|
+
// on it yet. The honest answer for now is that the supported range has a ceiling
|
|
27
|
+
// and says so.
|
|
28
|
+
import ts from 'typescript';
|
|
29
|
+
/**
|
|
30
|
+
* The range the readers are written against.
|
|
31
|
+
*
|
|
32
|
+
* Stated here rather than read from the manifest at runtime, because the
|
|
33
|
+
* manifest is not on disk beside a bundled build and a diagnostic that cannot
|
|
34
|
+
* name the range is most of the diagnostic gone. `ATX-56` keeps the two honest:
|
|
35
|
+
* its `params` carry this string, and its scenario compares them with
|
|
36
|
+
* `package.json`, so the constant cannot drift from the dependency it describes.
|
|
37
|
+
*/
|
|
38
|
+
export const SUPPORTED_TYPESCRIPT = '^5.5.0 || ^6.0.0';
|
|
39
|
+
/**
|
|
40
|
+
* An ERROR when the resolved compiler has no AST API, or `undefined` when it
|
|
41
|
+
* does.
|
|
42
|
+
*
|
|
43
|
+
* Takes the compiler rather than reaching for the import directly, so the
|
|
44
|
+
* refusal can be attested against a stand-in: the condition only exists on a
|
|
45
|
+
* compiler this repository cannot install alongside the one it builds with, and
|
|
46
|
+
* a check nothing can make fail is the shape `never-red` exists to catch.
|
|
47
|
+
*
|
|
48
|
+
* No `reqId` and no `file`: the finding is about the toolchain, and every
|
|
49
|
+
* requirement and every file in the project is equally unreadable because of it.
|
|
50
|
+
*/
|
|
51
|
+
export function compilerIssue(compiler = ts) {
|
|
52
|
+
if (typeof compiler.createSourceFile === 'function' && compiler.ScriptTarget !== undefined) {
|
|
53
|
+
return undefined;
|
|
54
|
+
}
|
|
55
|
+
const version = typeof compiler.version === 'string' ? compiler.version : 'unknown';
|
|
56
|
+
return {
|
|
57
|
+
level: 'ERROR',
|
|
58
|
+
code: 'compiler-unsupported',
|
|
59
|
+
message: `The resolved TypeScript (${version}) does not expose the AST API Attest reads registries and specs with. ` +
|
|
60
|
+
`Attest supports ${SUPPORTED_TYPESCRIPT}. ` +
|
|
61
|
+
`If a resolution or override pins the compiler for the whole tree, exclude Attest from it or move that pin back into the supported range.`,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
//# sourceMappingURL=compiler.js.map
|
package/dist/core/docs.d.ts
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* and the `##` headings of both language documents, so landing here cannot
|
|
9
9
|
* produce a dead link.
|
|
10
10
|
*/
|
|
11
|
-
export declare const ISSUE_CODES: readonly ["add-conflict", "add-invalid", "added-id-unmerged", "change-not-found", "declared-not-run", "duplicate-prefix", "duplicate-requirement", "empty-spec", "internal-error", "invalid-change-name", "missing-spec-doc", "modify-invalid", "modify-missing", "never-red", "orphan-test", "possible-drift", "proposed-spec-unclaimed", "rationale-placeholder", "registry-invalid", "registry-no-default", "registry-not-static", "rename-source-missing", "rename-target-exists", "rename-target-invalid", "stale-spec-doc", "tests-red", "unbound-param", "uncovered-requirement", "unknown-target"];
|
|
11
|
+
export declare const ISSUE_CODES: readonly ["add-conflict", "add-invalid", "added-id-unmerged", "apply-no-prefix-owner", "apply-unsupported-delta", "change-not-found", "compiler-unsupported", "declared-not-run", "duplicate-prefix", "duplicate-requirement", "empty-spec", "internal-error", "invalid-change-name", "missing-spec-doc", "modify-invalid", "modify-missing", "never-red", "orphan-test", "possible-drift", "proposed-spec-name-taken", "proposed-spec-unclaimed", "rationale-placeholder", "registry-invalid", "registry-no-default", "registry-not-static", "rename-source-missing", "rename-target-exists", "rename-target-invalid", "spec-in-change-dir", "spec-load-failed", "stale-spec-doc", "tests-red", "unbound-param", "uncovered-requirement", "unknown-target"];
|
|
12
12
|
export type IssueCode = (typeof ISSUE_CODES)[number];
|
|
13
13
|
/**
|
|
14
14
|
* The page explaining `code`, or `undefined` when nothing explains it.
|
package/dist/core/docs.js
CHANGED
|
@@ -22,7 +22,10 @@ export const ISSUE_CODES = [
|
|
|
22
22
|
'add-conflict',
|
|
23
23
|
'add-invalid',
|
|
24
24
|
'added-id-unmerged',
|
|
25
|
+
'apply-no-prefix-owner',
|
|
26
|
+
'apply-unsupported-delta',
|
|
25
27
|
'change-not-found',
|
|
28
|
+
'compiler-unsupported',
|
|
26
29
|
'declared-not-run',
|
|
27
30
|
'duplicate-prefix',
|
|
28
31
|
'duplicate-requirement',
|
|
@@ -35,6 +38,7 @@ export const ISSUE_CODES = [
|
|
|
35
38
|
'never-red',
|
|
36
39
|
'orphan-test',
|
|
37
40
|
'possible-drift',
|
|
41
|
+
'proposed-spec-name-taken',
|
|
38
42
|
'proposed-spec-unclaimed',
|
|
39
43
|
'rationale-placeholder',
|
|
40
44
|
'registry-invalid',
|
|
@@ -43,6 +47,8 @@ export const ISSUE_CODES = [
|
|
|
43
47
|
'rename-source-missing',
|
|
44
48
|
'rename-target-exists',
|
|
45
49
|
'rename-target-invalid',
|
|
50
|
+
'spec-in-change-dir',
|
|
51
|
+
'spec-load-failed',
|
|
46
52
|
'stale-spec-doc',
|
|
47
53
|
'tests-red',
|
|
48
54
|
'unbound-param',
|
package/dist/core/gate.d.ts
CHANGED
|
@@ -56,6 +56,51 @@ export declare function declaredNotRunIssues(plan: AttestPlan, run: RunResult):
|
|
|
56
56
|
* rather than asserting a cause.
|
|
57
57
|
*/
|
|
58
58
|
export declare function unmergedIdIssues(run: RunResult, unmergedAddedIds: readonly string[]): Issue[];
|
|
59
|
+
/**
|
|
60
|
+
* A spec file the runtime could not load, with no more specific cause known
|
|
61
|
+
* (design §5.4).
|
|
62
|
+
*
|
|
63
|
+
* The general case of `unmergedIdIssues`, and it exists because that one is a
|
|
64
|
+
* conjunction: it speaks only when the change also adds an id the registry on
|
|
65
|
+
* disk lacks. Every other way a spec file fails to import — a typo in a
|
|
66
|
+
* specifier, a module that throws at load, a dependency that is not installed —
|
|
67
|
+
* produced no finding naming the file at all. What the reader got instead was a
|
|
68
|
+
* `declared-not-run` per scenario in it, saying "skipped, or excluded by an
|
|
69
|
+
* `.only`?", which is a guess at a cause and the wrong one: it sends someone to
|
|
70
|
+
* audit a file for a `skip` that is not there, while the import error that
|
|
71
|
+
* explains everything is named nowhere in the report.
|
|
72
|
+
*
|
|
73
|
+
* `already` is the files a more specific diagnosis has claimed. One file gets
|
|
74
|
+
* one finding, and the specific one wins, because `added-id-unmerged` carries a
|
|
75
|
+
* fix and this carries only a fact. That is also why this could not simply be
|
|
76
|
+
* `declared-not-run` suppression, which is what it looked like from a distance:
|
|
77
|
+
* suppressing with nothing to put in its place trades a wrong message for
|
|
78
|
+
* silence, and a wrong message beats silence. The replacement had to come first.
|
|
79
|
+
*
|
|
80
|
+
* No `reqId`. A file that never loaded has no established relationship to any
|
|
81
|
+
* requirement — the ids it *would* have attested are readable from the static
|
|
82
|
+
* plan, but stating one here would name a requirement as implicated when the
|
|
83
|
+
* failure may have nothing to do with it, and a file is what did or did not
|
|
84
|
+
* load.
|
|
85
|
+
*/
|
|
86
|
+
export declare function specLoadFailedIssues(run: RunResult, already?: ReadonlySet<string>): Issue[];
|
|
87
|
+
/**
|
|
88
|
+
* Everything a report says about scenarios that did not run: the files that
|
|
89
|
+
* failed to load, then the absences those files do not already explain.
|
|
90
|
+
*
|
|
91
|
+
* One function because two commands ask, and the answer has an *order* in it —
|
|
92
|
+
* the cause before the absences it caused, and never the absences on their own.
|
|
93
|
+
* `verify` and the gate each spelling that out is how the two came to differ in
|
|
94
|
+
* the first place: the gate learned to withdraw a misleading line and `verify`
|
|
95
|
+
* did not, so the same run produced a different diagnosis depending on which
|
|
96
|
+
* command asked. That is the clerical agreement `declaredNotRunIssues` was
|
|
97
|
+
* extracted to stop, arriving one level up.
|
|
98
|
+
*
|
|
99
|
+
* `specific` is any more precise diagnosis the caller has already emitted for
|
|
100
|
+
* these files — `added-id-unmerged` on the gate path, nothing on `verify`'s.
|
|
101
|
+
* Those files are left alone here, so one file carries one finding.
|
|
102
|
+
*/
|
|
103
|
+
export declare function notRunIssues(plan: AttestPlan, run: RunResult, specific?: readonly Issue[]): Issue[];
|
|
59
104
|
/**
|
|
60
105
|
* Never-red: a scenario attesting a requirement this change ADDs, whose first
|
|
61
106
|
* observed run did not fail (design §6, mechanism 2).
|
|
@@ -85,8 +130,9 @@ export declare function neverRedIssues(plan: AttestPlan, addedIds: readonly stri
|
|
|
85
130
|
* 2. Executable: all tests green.
|
|
86
131
|
* 3. A spec file that failed to load while the change adds an id the registry
|
|
87
132
|
* on disk lacks — the reason, named before the absences it causes.
|
|
88
|
-
* 4.
|
|
89
|
-
*
|
|
133
|
+
* 4. Everything else about what did not run, from `notRunIssues`: the load
|
|
134
|
+
* failures step 3 did not claim, then static-vs-runtime coverage for the
|
|
135
|
+
* files that did load (catches skip/only false coverage).
|
|
90
136
|
* 5. Never-red: every scenario of an ADDED requirement failed on its first
|
|
91
137
|
* recorded run (design §6, mechanism 2).
|
|
92
138
|
*/
|
package/dist/core/gate.js
CHANGED
|
@@ -66,6 +66,71 @@ export function unmergedIdIssues(run, unmergedAddedIds) {
|
|
|
66
66
|
`Merge the added requirement into the registry and run the gate again: applying a delta whose ADDED entry already exists with identical content is a no-op, so the change still documents the intent.`,
|
|
67
67
|
}));
|
|
68
68
|
}
|
|
69
|
+
/**
|
|
70
|
+
* A spec file the runtime could not load, with no more specific cause known
|
|
71
|
+
* (design §5.4).
|
|
72
|
+
*
|
|
73
|
+
* The general case of `unmergedIdIssues`, and it exists because that one is a
|
|
74
|
+
* conjunction: it speaks only when the change also adds an id the registry on
|
|
75
|
+
* disk lacks. Every other way a spec file fails to import — a typo in a
|
|
76
|
+
* specifier, a module that throws at load, a dependency that is not installed —
|
|
77
|
+
* produced no finding naming the file at all. What the reader got instead was a
|
|
78
|
+
* `declared-not-run` per scenario in it, saying "skipped, or excluded by an
|
|
79
|
+
* `.only`?", which is a guess at a cause and the wrong one: it sends someone to
|
|
80
|
+
* audit a file for a `skip` that is not there, while the import error that
|
|
81
|
+
* explains everything is named nowhere in the report.
|
|
82
|
+
*
|
|
83
|
+
* `already` is the files a more specific diagnosis has claimed. One file gets
|
|
84
|
+
* one finding, and the specific one wins, because `added-id-unmerged` carries a
|
|
85
|
+
* fix and this carries only a fact. That is also why this could not simply be
|
|
86
|
+
* `declared-not-run` suppression, which is what it looked like from a distance:
|
|
87
|
+
* suppressing with nothing to put in its place trades a wrong message for
|
|
88
|
+
* silence, and a wrong message beats silence. The replacement had to come first.
|
|
89
|
+
*
|
|
90
|
+
* No `reqId`. A file that never loaded has no established relationship to any
|
|
91
|
+
* requirement — the ids it *would* have attested are readable from the static
|
|
92
|
+
* plan, but stating one here would name a requirement as implicated when the
|
|
93
|
+
* failure may have nothing to do with it, and a file is what did or did not
|
|
94
|
+
* load.
|
|
95
|
+
*/
|
|
96
|
+
export function specLoadFailedIssues(run, already = new Set()) {
|
|
97
|
+
return run.unloadedFiles
|
|
98
|
+
.filter((file) => !already.has(file))
|
|
99
|
+
.map((file) => ({
|
|
100
|
+
level: 'ERROR',
|
|
101
|
+
code: 'spec-load-failed',
|
|
102
|
+
file,
|
|
103
|
+
message: `${file} failed to load, so none of the scenarios in it ran. ` +
|
|
104
|
+
`The run output above carries the import error itself; this reports which file it stopped, because a file that never loaded reports no failures of its own. ` +
|
|
105
|
+
`Fix the import and run again — the scenarios in this file are neither passing nor failing until it loads.`,
|
|
106
|
+
}));
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Everything a report says about scenarios that did not run: the files that
|
|
110
|
+
* failed to load, then the absences those files do not already explain.
|
|
111
|
+
*
|
|
112
|
+
* One function because two commands ask, and the answer has an *order* in it —
|
|
113
|
+
* the cause before the absences it caused, and never the absences on their own.
|
|
114
|
+
* `verify` and the gate each spelling that out is how the two came to differ in
|
|
115
|
+
* the first place: the gate learned to withdraw a misleading line and `verify`
|
|
116
|
+
* did not, so the same run produced a different diagnosis depending on which
|
|
117
|
+
* command asked. That is the clerical agreement `declaredNotRunIssues` was
|
|
118
|
+
* extracted to stop, arriving one level up.
|
|
119
|
+
*
|
|
120
|
+
* `specific` is any more precise diagnosis the caller has already emitted for
|
|
121
|
+
* these files — `added-id-unmerged` on the gate path, nothing on `verify`'s.
|
|
122
|
+
* Those files are left alone here, so one file carries one finding.
|
|
123
|
+
*/
|
|
124
|
+
export function notRunIssues(plan, run, specific = []) {
|
|
125
|
+
const named = new Set(specific.flatMap((i) => (i.file ? [i.file] : [])));
|
|
126
|
+
// Every unloaded file now carries a diagnosis: `specific` covers `named`, and
|
|
127
|
+
// the call below covers the rest by construction.
|
|
128
|
+
const diagnosed = new Set(run.unloadedFiles);
|
|
129
|
+
return [
|
|
130
|
+
...specLoadFailedIssues(run, named),
|
|
131
|
+
...declaredNotRunIssues(plan, run).filter((i) => !i.file || !diagnosed.has(i.file)),
|
|
132
|
+
];
|
|
133
|
+
}
|
|
69
134
|
/**
|
|
70
135
|
* Never-red: a scenario attesting a requirement this change ADDs, whose first
|
|
71
136
|
* observed run did not fail (design §6, mechanism 2).
|
|
@@ -116,8 +181,9 @@ export function neverRedIssues(plan, addedIds, firstRun) {
|
|
|
116
181
|
* 2. Executable: all tests green.
|
|
117
182
|
* 3. A spec file that failed to load while the change adds an id the registry
|
|
118
183
|
* on disk lacks — the reason, named before the absences it causes.
|
|
119
|
-
* 4.
|
|
120
|
-
*
|
|
184
|
+
* 4. Everything else about what did not run, from `notRunIssues`: the load
|
|
185
|
+
* failures step 3 did not claim, then static-vs-runtime coverage for the
|
|
186
|
+
* files that did load (catches skip/only false coverage).
|
|
121
187
|
* 5. Never-red: every scenario of an ADDED requirement failed on its first
|
|
122
188
|
* recorded run (design §6, mechanism 2).
|
|
123
189
|
*/
|
|
@@ -133,19 +199,12 @@ export function evaluateGate({ registry, plan, run, addedIds, unmergedAddedIds,
|
|
|
133
199
|
message: 'Some tests are failing; the change cannot be archived.',
|
|
134
200
|
});
|
|
135
201
|
}
|
|
136
|
-
// 3)
|
|
202
|
+
// 3) and 4) in one push, because the order between them is not this
|
|
203
|
+
// function's to choose: the gate's own diagnosis — the one with a cause and a
|
|
204
|
+
// fix attached — and then everything `notRunIssues` says about the rest. The
|
|
205
|
+
// gate's only extra input is having that more specific diagnosis to offer.
|
|
137
206
|
const unmerged = unmergedIdIssues(run, unmergedAddedIds ?? []);
|
|
138
|
-
blocking.push(...unmerged);
|
|
139
|
-
// 4) Declared-not-run: static coverage claimed, runtime never executed it.
|
|
140
|
-
//
|
|
141
|
-
// Scenarios in a file diagnosed just above are left out. Not because they
|
|
142
|
-
// ran — they could not have — but because "declared but never executed
|
|
143
|
-
// (skipped, or excluded by an .only?)" is one fact restated as a guess about
|
|
144
|
-
// a cause the line above has already established. Only when that line was
|
|
145
|
-
// emitted: with no diagnosis to replace it, a wrong message still beats
|
|
146
|
-
// silence, which is why `verify` keeps reporting them.
|
|
147
|
-
const diagnosed = new Set(unmerged.map((i) => i.file));
|
|
148
|
-
blocking.push(...declaredNotRunIssues(plan, run).filter((i) => !diagnosed.has(i.file)));
|
|
207
|
+
blocking.push(...unmerged, ...notRunIssues(plan, run, unmerged));
|
|
149
208
|
// 5) Never-red: the added scenarios have to have discriminated once.
|
|
150
209
|
if (addedIds && addedIds.length > 0) {
|
|
151
210
|
blocking.push(...neverRedIssues(plan, addedIds, firstRun ?? {}));
|
package/dist/core/loader.js
CHANGED
|
@@ -35,6 +35,19 @@ import { block } from './terminal.js';
|
|
|
35
35
|
export function sanitisedLogger(base = createLogger('error')) {
|
|
36
36
|
return {
|
|
37
37
|
...base,
|
|
38
|
+
// `hasWarned` is the one member of `Logger` that is state rather than
|
|
39
|
+
// behaviour, and the spread above copies it *by value* — so the wrapper was
|
|
40
|
+
// born `false` and stayed `false` however many warnings went through it,
|
|
41
|
+
// while the base it delegates to flipped to `true`. Measured: after one
|
|
42
|
+
// `warn`, base `true` / wrapper `false`. An accessor pair makes the wrapper
|
|
43
|
+
// a view of the base rather than a snapshot of it, which is what every
|
|
44
|
+
// other member already is.
|
|
45
|
+
get hasWarned() {
|
|
46
|
+
return base.hasWarned;
|
|
47
|
+
},
|
|
48
|
+
set hasWarned(value) {
|
|
49
|
+
base.hasWarned = value;
|
|
50
|
+
},
|
|
38
51
|
info: (msg, opts) => base.info(block(msg), opts),
|
|
39
52
|
warn: (msg, opts) => base.warn(block(msg), opts),
|
|
40
53
|
warnOnce: (msg, opts) => base.warnOnce(block(msg), opts),
|
package/dist/core/locate.d.ts
CHANGED
|
@@ -98,11 +98,40 @@ export declare function staticReader(): RegistryReader;
|
|
|
98
98
|
* prefix a duplicate-prefix ERROR.
|
|
99
99
|
*
|
|
100
100
|
* Pass `files` to reuse a {@link scanProject} result instead of re-walking.
|
|
101
|
+
*
|
|
102
|
+
* `prefixOwners` comes back with the registry because the rule deciding it —
|
|
103
|
+
* first claim wins, in sorted file order — must have exactly one spelling. It is
|
|
104
|
+
* what `duplicate-prefix` is computed from here, and what tells `--apply` which
|
|
105
|
+
* file an ADDED id belongs in (design §7); a second walk arriving at its own
|
|
106
|
+
* answer would be a second rule the moment either was edited.
|
|
101
107
|
*/
|
|
102
108
|
export declare function loadRegistry(root: string, reader: RegistryReader, files?: string[]): Promise<{
|
|
103
109
|
registry: Registry;
|
|
104
110
|
issues: Issue[];
|
|
111
|
+
prefixOwners: Record<string, string>;
|
|
112
|
+
/**
|
|
113
|
+
* Registry files that contributed **no ids**, relative to `root`.
|
|
114
|
+
*
|
|
115
|
+
* Not the same question as "did loading produce an ERROR": `duplicate-prefix`
|
|
116
|
+
* and `duplicate-requirement` are ERRORs raised *after* a successful read, and
|
|
117
|
+
* the ids they are about are present. Only a file whose outcome carried an
|
|
118
|
+
* issue instead of a registry is missing from the result, and a caller that
|
|
119
|
+
* re-derived that from the issue codes would be maintaining a second answer
|
|
120
|
+
* to a question this loop already knows — the mistake `prefixOwners` is here
|
|
121
|
+
* to avoid one shape of.
|
|
122
|
+
*/
|
|
123
|
+
unreadableFiles: string[];
|
|
105
124
|
}>;
|
|
125
|
+
/**
|
|
126
|
+
* The part of an id that names the space it lives in — `AUTH` of `AUTH-3`.
|
|
127
|
+
*
|
|
128
|
+
* The prefix is the only unit above the requirement (design §11) and two things
|
|
129
|
+
* now depend on agreeing about it: `duplicate-prefix`, and which file `--apply`
|
|
130
|
+
* writes an ADDED requirement into. An id with no dash is its own prefix, which
|
|
131
|
+
* cannot arise from `RequirementIdSchema` and is handled anyway because this
|
|
132
|
+
* also runs over ids a delta proposed.
|
|
133
|
+
*/
|
|
134
|
+
export declare function idPrefix(id: string): string;
|
|
106
135
|
/**
|
|
107
136
|
* Parse the given spec files into one merged plan (paths shown relative to
|
|
108
137
|
* `displayRoot`). Files are read concurrently; the merge follows the input
|
|
@@ -111,10 +140,43 @@ export declare function loadRegistry(root: string, reader: RegistryReader, files
|
|
|
111
140
|
* The paths are POSIX on every platform (see `paths.ts`): they are not only
|
|
112
141
|
* displayed, they become the child run's `include` globs, where a Windows
|
|
113
142
|
* separator would silently match nothing.
|
|
143
|
+
*
|
|
144
|
+
* Each source is parsed as it arrives rather than after all of them. The
|
|
145
|
+
* previous `Promise.all(files.map(readFile))` held every spec file in memory at
|
|
146
|
+
* once — measured at ~47 MiB on a synthetic tree of 6000 files — for input size
|
|
147
|
+
* that is not ours to choose, since `check` is the command this project tells
|
|
148
|
+
* people to run first on an untrusted fork MR. Parsing at the point of arrival
|
|
149
|
+
* makes the peak `PARSE_CONCURRENCY` sources instead of `files.length`, and the
|
|
150
|
+
* plan is the only thing that still grows with the tree.
|
|
151
|
+
*
|
|
152
|
+
* `findFiles` above is deliberately left unbounded: its fan-out is real, but the
|
|
153
|
+
* failure it invites is descriptor exhaustion, which no measurement on either
|
|
154
|
+
* development platform could produce (see CHANGELOG.md, `Under consideration`).
|
|
155
|
+
* The memory here needed no such evidence — it is arithmetic, and portable.
|
|
114
156
|
*/
|
|
115
157
|
export declare function parseSpecs(files: string[], displayRoot: string): Promise<AttestPlan>;
|
|
116
158
|
/** Parse every `*.spec.ts` under root into one merged plan (file paths shown relative to root). */
|
|
117
159
|
export declare function parseAllSpecFiles(root: string): Promise<AttestPlan>;
|
|
160
|
+
/**
|
|
161
|
+
* Spec-shaped files sitting under `root/changes` — the location a change's
|
|
162
|
+
* specs used to live at, and which nothing walks any more (design §7).
|
|
163
|
+
*
|
|
164
|
+
* Two correct decisions compose into a blind spot. `changes` is in `SKIP_DIRS`,
|
|
165
|
+
* so `scanProject` cannot reach these; and the explicit include `attest archive`
|
|
166
|
+
* once applied to `changes/<name>/specs/` went away with `changeExcludeGlobs`
|
|
167
|
+
* when specs moved to their merged location. A spec left here therefore executes
|
|
168
|
+
* in no suite and no gate, and the only command that says anything is the gate,
|
|
169
|
+
* blaming coverage for a file it cannot see.
|
|
170
|
+
*
|
|
171
|
+
* Walks through `findFiles` rather than a second walker: `SKIP_DIRS` is consulted
|
|
172
|
+
* for *sub*directories only, so starting the walk at `changes` itself both
|
|
173
|
+
* reaches these files and keeps skipping `node_modules` beneath them.
|
|
174
|
+
*
|
|
175
|
+
* Both spellings are wrong here and both are returned. A `*.spec.ts` is the
|
|
176
|
+
* pre-move layout; a `*.proposed.spec.ts` is the right marker at the wrong path,
|
|
177
|
+
* which is the half-done migration and no more visible than the other.
|
|
178
|
+
*/
|
|
179
|
+
export declare function findChangeDirSpecs(root: string): Promise<string[]>;
|
|
118
180
|
/** List the names of proposed changes under `root/changes`. */
|
|
119
181
|
export declare function listChangeNames(root: string): Promise<string[]>;
|
|
120
182
|
//# sourceMappingURL=locate.d.ts.map
|