@am_shork/attest 0.4.3 → 0.6.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 +541 -32
- package/README.md +16 -8
- package/dist/cli/index.js +21 -3
- package/dist/core/apply.d.ts +15 -0
- package/dist/core/apply.js +43 -1
- 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 +7 -0
- package/dist/core/gate.d.ts +40 -3
- package/dist/core/gate.js +56 -6
- package/dist/core/locate.d.ts +62 -4
- package/dist/core/locate.js +91 -16
- package/dist/core/merge.d.ts +54 -0
- package/dist/core/merge.js +244 -0
- package/dist/core/pipeline.d.ts +13 -9
- package/dist/core/pipeline.js +289 -46
- package/dist/core/registry.d.ts +63 -4
- package/dist/core/registry.js +30 -4
- package/dist/core/runner.js +41 -4
- package/dist/core/schema.js +9 -1
- package/dist/core/skill.js +104 -13
- 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 +42 -2
- package/dist/core/types.d.ts +10 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -8,18 +8,26 @@ by a stable ID and continuously detects drift.
|
|
|
8
8
|
- **Result** — is every test green? (from the test runner)
|
|
9
9
|
- **Drift** — do intent and assertions still agree? (static + runtime cross-check)
|
|
10
10
|
|
|
11
|
-
The killer move against drift: values
|
|
12
|
-
in
|
|
13
|
-
physically impossible to drift between the spec and the assertion.
|
|
14
|
-
|
|
15
|
-
|
|
11
|
+
The killer move against drift: values a requirement **promises** (timeouts,
|
|
12
|
+
limits, budgets) live **once** in its `params`, and tests read them from there —
|
|
13
|
+
so a number is physically impossible to drift between the spec and the assertion.
|
|
14
|
+
Values that merely tune behaviour stay ordinary constants; nothing is owed to
|
|
15
|
+
anyone when a tuning knob changes. A param may be a scalar or an array of
|
|
16
|
+
scalars, so list-shaped constants (vendor blacklists, id sets) get the same
|
|
17
|
+
single source as a lone number.
|
|
16
18
|
|
|
17
19
|
What that does not buy is a warning when you change the value. `check` runs
|
|
18
20
|
nothing, so editing a param leaves it at `✓ No issues` — nothing became unbound,
|
|
19
21
|
nothing became uncovered. The value cannot *diverge* from the assertion, which is
|
|
20
22
|
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`.
|
|
22
|
-
|
|
23
|
+
a scenario asserts on the value it read from `params`.
|
|
24
|
+
|
|
25
|
+
Reading the param is necessary and **not sufficient**, which is worth knowing
|
|
26
|
+
before you rely on it: an assertion that recomputes its expectation from the same
|
|
27
|
+
param the code under test just read has no independent term, so both sides move
|
|
28
|
+
together and the test stays green through any edit. Pin the expectation to
|
|
29
|
+
something that does not move with the param — a fixture, a literal in the test,
|
|
30
|
+
or a second independently derived value.
|
|
23
31
|
|
|
24
32
|
## Prerequisites
|
|
25
33
|
|
|
@@ -149,7 +157,7 @@ Every diagnostic carries a `code`, and every code has a section in
|
|
|
149
157
|
```
|
|
150
158
|
ERROR registry-not-static (requirements/upload.reqs.ts:5)
|
|
151
159
|
Value is not a literal.
|
|
152
|
-
→ https://gitlab.com/Pseudorca/attest/-/blob/v0.
|
|
160
|
+
→ https://gitlab.com/Pseudorca/attest/-/blob/v0.6.0/docs/en/troubleshooting.md#registry-not-static
|
|
153
161
|
```
|
|
154
162
|
|
|
155
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
16
|
import { 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,14 +272,27 @@ 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
285
|
if (blocking.length === 0) {
|
|
277
|
-
console.log(chalk.green(
|
|
286
|
+
console.log(chalk.green(opts.apply
|
|
287
|
+
? `✓ Gate passed and merged: change "${change}".`
|
|
288
|
+
: `✓ Gate passed: change "${change}" can be archived.`));
|
|
289
|
+
// Every path the merge touched, because this is the command that
|
|
290
|
+
// edits files the user cannot regenerate and they need to know which
|
|
291
|
+
// ones to read before committing. It is also what a working-tree
|
|
292
|
+
// check would only have approximated, and it works for someone not
|
|
293
|
+
// using version control at all.
|
|
294
|
+
for (const path of written)
|
|
295
|
+
console.log(chalk.dim(` ${path}`));
|
|
278
296
|
}
|
|
279
297
|
else {
|
|
280
298
|
console.log(chalk.red(`✗ Gate failed: change "${change}"`));
|
package/dist/core/apply.d.ts
CHANGED
|
@@ -12,6 +12,21 @@ export interface ApplyResult {
|
|
|
12
12
|
* than off `delta.added`, so the two can never be scoped differently.
|
|
13
13
|
*/
|
|
14
14
|
export declare function addedIds(d: RegistryDelta): string[];
|
|
15
|
+
/**
|
|
16
|
+
* The ids a delta **claims**: what it adds, renames to, or modifies.
|
|
17
|
+
*
|
|
18
|
+
* This is how a proposed spec is attributed to a change (design §7). A spec
|
|
19
|
+
* sitting at its merged location carries no change name, so the delta names its
|
|
20
|
+
* specs the only way that cannot drift from them — by the requirements they
|
|
21
|
+
* declare a scenario for. Wider than `addedIds` on purpose: a delta that
|
|
22
|
+
* retunes a param or renames an id may need a scenario of its own, and one that
|
|
23
|
+
* could not be claimed would be a spec the gate never ran.
|
|
24
|
+
*
|
|
25
|
+
* REMOVED ids are absent. A scenario for a requirement the change deletes is
|
|
26
|
+
* not proposed behaviour, and claiming it would put a spec into the gate run
|
|
27
|
+
* whose requirement the same delta has just taken away.
|
|
28
|
+
*/
|
|
29
|
+
export declare function claimedIds(d: RegistryDelta): string[];
|
|
15
30
|
/**
|
|
16
31
|
* Apply a change delta to a base registry, returning the merged registry plus
|
|
17
32
|
* any apply-level issues (conflicts, missing targets, invalid results). The
|
package/dist/core/apply.js
CHANGED
|
@@ -14,6 +14,27 @@ import { byCodeUnit } from './order.js';
|
|
|
14
14
|
export function addedIds(d) {
|
|
15
15
|
return Object.keys(d.added ?? {});
|
|
16
16
|
}
|
|
17
|
+
/**
|
|
18
|
+
* The ids a delta **claims**: what it adds, renames to, or modifies.
|
|
19
|
+
*
|
|
20
|
+
* This is how a proposed spec is attributed to a change (design §7). A spec
|
|
21
|
+
* sitting at its merged location carries no change name, so the delta names its
|
|
22
|
+
* specs the only way that cannot drift from them — by the requirements they
|
|
23
|
+
* declare a scenario for. Wider than `addedIds` on purpose: a delta that
|
|
24
|
+
* retunes a param or renames an id may need a scenario of its own, and one that
|
|
25
|
+
* could not be claimed would be a spec the gate never ran.
|
|
26
|
+
*
|
|
27
|
+
* REMOVED ids are absent. A scenario for a requirement the change deletes is
|
|
28
|
+
* not proposed behaviour, and claiming it would put a spec into the gate run
|
|
29
|
+
* whose requirement the same delta has just taken away.
|
|
30
|
+
*/
|
|
31
|
+
export function claimedIds(d) {
|
|
32
|
+
return [
|
|
33
|
+
...Object.keys(d.added ?? {}),
|
|
34
|
+
...(d.renamed ?? []).map((r) => r.to),
|
|
35
|
+
...Object.keys(d.modified ?? {}),
|
|
36
|
+
].sort(byCodeUnit);
|
|
37
|
+
}
|
|
17
38
|
/**
|
|
18
39
|
* Apply a change delta to a base registry, returning the merged registry plus
|
|
19
40
|
* any apply-level issues (conflicts, missing targets, invalid results). The
|
|
@@ -132,8 +153,29 @@ function introducedIdIssue(id, code, prefix) {
|
|
|
132
153
|
function err(code, message, reqId) {
|
|
133
154
|
return { level: 'ERROR', code, ...(reqId ? { reqId } : {}), message };
|
|
134
155
|
}
|
|
156
|
+
/**
|
|
157
|
+
* The first schema failure, as `path: message`.
|
|
158
|
+
*
|
|
159
|
+
* The path is the half that was being dropped. A requirement carries a record —
|
|
160
|
+
* `params` — so "invalid" on its own does not say *which* param, and the value
|
|
161
|
+
* that most often fails is one the author has to find by reading the schema's
|
|
162
|
+
* `.d.ts`. `RegistryValidationError` has spelled a path `a.b.c` since the
|
|
163
|
+
* registry shipped, and `troubleshooting.md` has been quoting `rationale:
|
|
164
|
+
* Required` for `add-invalid` the whole time — a form this function could not
|
|
165
|
+
* produce. So this is the spelling matching what is already documented, not a
|
|
166
|
+
* new one.
|
|
167
|
+
*
|
|
168
|
+
* Omitted rather than rendered as `(root)` when the path is empty, because the
|
|
169
|
+
* one caller that gets an empty path is `introducedIdIssue`, whose prefix has
|
|
170
|
+
* already named the thing: `Added requirement "auth-7" is invalid: id must look
|
|
171
|
+
* like AUTH-3` reads correctly and `(root): id must look like AUTH-3` does not.
|
|
172
|
+
*/
|
|
135
173
|
function firstMessage(error) {
|
|
136
|
-
|
|
174
|
+
const first = error.issues[0];
|
|
175
|
+
if (!first)
|
|
176
|
+
return 'unknown error';
|
|
177
|
+
const path = first.path.map(String).join('.');
|
|
178
|
+
return path ? `${path}: ${first.message}` : first.message;
|
|
137
179
|
}
|
|
138
180
|
/** Content equality via canonical JSON (params key order does not matter). */
|
|
139
181
|
function sameRequirement(a, b) {
|
|
@@ -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", "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", "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", "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
|
@@ -21,7 +21,11 @@ import { packageVersion } from './version.js';
|
|
|
21
21
|
export const ISSUE_CODES = [
|
|
22
22
|
'add-conflict',
|
|
23
23
|
'add-invalid',
|
|
24
|
+
'added-id-unmerged',
|
|
25
|
+
'apply-no-prefix-owner',
|
|
26
|
+
'apply-unsupported-delta',
|
|
24
27
|
'change-not-found',
|
|
28
|
+
'compiler-unsupported',
|
|
25
29
|
'declared-not-run',
|
|
26
30
|
'duplicate-prefix',
|
|
27
31
|
'duplicate-requirement',
|
|
@@ -34,6 +38,8 @@ export const ISSUE_CODES = [
|
|
|
34
38
|
'never-red',
|
|
35
39
|
'orphan-test',
|
|
36
40
|
'possible-drift',
|
|
41
|
+
'proposed-spec-name-taken',
|
|
42
|
+
'proposed-spec-unclaimed',
|
|
37
43
|
'rationale-placeholder',
|
|
38
44
|
'registry-invalid',
|
|
39
45
|
'registry-no-default',
|
|
@@ -41,6 +47,7 @@ export const ISSUE_CODES = [
|
|
|
41
47
|
'rename-source-missing',
|
|
42
48
|
'rename-target-exists',
|
|
43
49
|
'rename-target-invalid',
|
|
50
|
+
'spec-in-change-dir',
|
|
44
51
|
'stale-spec-doc',
|
|
45
52
|
'tests-red',
|
|
46
53
|
'unbound-param',
|
package/dist/core/gate.d.ts
CHANGED
|
@@ -9,6 +9,15 @@ export interface GateInputs {
|
|
|
9
9
|
run: RunResult;
|
|
10
10
|
/** Ids this change ADDs — the scenarios that carry a first-red obligation. */
|
|
11
11
|
addedIds?: readonly string[];
|
|
12
|
+
/**
|
|
13
|
+
* Of those, the ids the registry **on disk** does not have yet.
|
|
14
|
+
*
|
|
15
|
+
* Computed against the base registry rather than the applied one, because
|
|
16
|
+
* that is the registry the child run imports: the gate applies the delta in
|
|
17
|
+
* memory, and the suite is a separate process reading `*.reqs.ts` off the
|
|
18
|
+
* filesystem. The two disagreeing is the whole content of `added-id-unmerged`.
|
|
19
|
+
*/
|
|
20
|
+
unmergedAddedIds?: readonly string[];
|
|
12
21
|
/** First observed outcome per scenario, already merged with this run. */
|
|
13
22
|
firstRun?: RedRecord;
|
|
14
23
|
}
|
|
@@ -21,6 +30,32 @@ export interface GateInputs {
|
|
|
21
30
|
* this framework is that agreement should be structural, not clerical.
|
|
22
31
|
*/
|
|
23
32
|
export declare function declaredNotRunIssues(plan: AttestPlan, run: RunResult): Issue[];
|
|
33
|
+
/**
|
|
34
|
+
* A spec file that failed to load, while the change adds ids the registry on
|
|
35
|
+
* disk does not have yet (design §8).
|
|
36
|
+
*
|
|
37
|
+
* The gate applies the delta in memory and hands the result to itself; the
|
|
38
|
+
* suite is a child process that imports `*.reqs.ts` from the filesystem. So
|
|
39
|
+
* implementation code doing exactly what the workflow requires —
|
|
40
|
+
* `reqs['FOG-4'].params.modestRisk` for an id the change *adds* — throws at
|
|
41
|
+
* import, every spec file transitively importing that module fails to load, and
|
|
42
|
+
* what the gate could see was `tests-red` plus a `declared-not-run` per
|
|
43
|
+
* scenario, whose message sends the reader to look for a `skip` or an `.only`
|
|
44
|
+
* that is not there. The change could not be made green by the documented
|
|
45
|
+
* workflow, and the verdict pointed away from the reason.
|
|
46
|
+
*
|
|
47
|
+
* This names the reason instead. It is a diagnosis, not a fix: the run still
|
|
48
|
+
* fails, and the way forward is still to merge the added requirement into the
|
|
49
|
+
* registry before running the gate. What it buys is that the reader is told
|
|
50
|
+
* that, rather than sent to audit their own spec files for a skip.
|
|
51
|
+
*
|
|
52
|
+
* The conjunction is the whole test, and it is deliberately a heuristic: a
|
|
53
|
+
* module can fail to import for reasons that have nothing to do with a
|
|
54
|
+
* requirement id. Naming an unmerged id when one exists is more useful than
|
|
55
|
+
* silence, so the message states the two facts and the inference between them
|
|
56
|
+
* rather than asserting a cause.
|
|
57
|
+
*/
|
|
58
|
+
export declare function unmergedIdIssues(run: RunResult, unmergedAddedIds: readonly string[]): Issue[];
|
|
24
59
|
/**
|
|
25
60
|
* Never-red: a scenario attesting a requirement this change ADDs, whose first
|
|
26
61
|
* observed run did not fail (design §6, mechanism 2).
|
|
@@ -48,10 +83,12 @@ export declare function neverRedIssues(plan: AttestPlan, addedIds: readonly stri
|
|
|
48
83
|
* the registry+plan here are already the applied result, so one pass
|
|
49
84
|
* validates the end state.
|
|
50
85
|
* 2. Executable: all tests green.
|
|
51
|
-
* 3.
|
|
86
|
+
* 3. A spec file that failed to load while the change adds an id the registry
|
|
87
|
+
* on disk lacks — the reason, named before the absences it causes.
|
|
88
|
+
* 4. Static coverage vs runtime coverage: every declared scenario actually
|
|
52
89
|
* ran (catches skip/only false coverage).
|
|
53
|
-
*
|
|
90
|
+
* 5. Never-red: every scenario of an ADDED requirement failed on its first
|
|
54
91
|
* recorded run (design §6, mechanism 2).
|
|
55
92
|
*/
|
|
56
|
-
export declare function evaluateGate({ registry, plan, run, addedIds, firstRun }: GateInputs): Issue[];
|
|
93
|
+
export declare function evaluateGate({ registry, plan, run, addedIds, unmergedAddedIds, firstRun, }: GateInputs): Issue[];
|
|
57
94
|
//# sourceMappingURL=gate.d.ts.map
|
package/dist/core/gate.js
CHANGED
|
@@ -29,6 +29,43 @@ export function declaredNotRunIssues(plan, run) {
|
|
|
29
29
|
}
|
|
30
30
|
return issues;
|
|
31
31
|
}
|
|
32
|
+
/**
|
|
33
|
+
* A spec file that failed to load, while the change adds ids the registry on
|
|
34
|
+
* disk does not have yet (design §8).
|
|
35
|
+
*
|
|
36
|
+
* The gate applies the delta in memory and hands the result to itself; the
|
|
37
|
+
* suite is a child process that imports `*.reqs.ts` from the filesystem. So
|
|
38
|
+
* implementation code doing exactly what the workflow requires —
|
|
39
|
+
* `reqs['FOG-4'].params.modestRisk` for an id the change *adds* — throws at
|
|
40
|
+
* import, every spec file transitively importing that module fails to load, and
|
|
41
|
+
* what the gate could see was `tests-red` plus a `declared-not-run` per
|
|
42
|
+
* scenario, whose message sends the reader to look for a `skip` or an `.only`
|
|
43
|
+
* that is not there. The change could not be made green by the documented
|
|
44
|
+
* workflow, and the verdict pointed away from the reason.
|
|
45
|
+
*
|
|
46
|
+
* This names the reason instead. It is a diagnosis, not a fix: the run still
|
|
47
|
+
* fails, and the way forward is still to merge the added requirement into the
|
|
48
|
+
* registry before running the gate. What it buys is that the reader is told
|
|
49
|
+
* that, rather than sent to audit their own spec files for a skip.
|
|
50
|
+
*
|
|
51
|
+
* The conjunction is the whole test, and it is deliberately a heuristic: a
|
|
52
|
+
* module can fail to import for reasons that have nothing to do with a
|
|
53
|
+
* requirement id. Naming an unmerged id when one exists is more useful than
|
|
54
|
+
* silence, so the message states the two facts and the inference between them
|
|
55
|
+
* rather than asserting a cause.
|
|
56
|
+
*/
|
|
57
|
+
export function unmergedIdIssues(run, unmergedAddedIds) {
|
|
58
|
+
if (unmergedAddedIds.length === 0)
|
|
59
|
+
return [];
|
|
60
|
+
return run.unloadedFiles.map((file) => ({
|
|
61
|
+
level: 'ERROR',
|
|
62
|
+
code: 'added-id-unmerged',
|
|
63
|
+
file,
|
|
64
|
+
message: `${file} failed to load, and this change adds ${unmergedAddedIds.join(', ')}, which the registry on disk does not have yet. ` +
|
|
65
|
+
`The suite runs against the registry files, not the applied registry the gate computed, so code reading a requirement this change adds throws at import. ` +
|
|
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
|
+
}));
|
|
68
|
+
}
|
|
32
69
|
/**
|
|
33
70
|
* Never-red: a scenario attesting a requirement this change ADDs, whose first
|
|
34
71
|
* observed run did not fail (design §6, mechanism 2).
|
|
@@ -77,12 +114,14 @@ export function neverRedIssues(plan, addedIds, firstRun) {
|
|
|
77
114
|
* the registry+plan here are already the applied result, so one pass
|
|
78
115
|
* validates the end state.
|
|
79
116
|
* 2. Executable: all tests green.
|
|
80
|
-
* 3.
|
|
117
|
+
* 3. A spec file that failed to load while the change adds an id the registry
|
|
118
|
+
* on disk lacks — the reason, named before the absences it causes.
|
|
119
|
+
* 4. Static coverage vs runtime coverage: every declared scenario actually
|
|
81
120
|
* ran (catches skip/only false coverage).
|
|
82
|
-
*
|
|
121
|
+
* 5. Never-red: every scenario of an ADDED requirement failed on its first
|
|
83
122
|
* recorded run (design §6, mechanism 2).
|
|
84
123
|
*/
|
|
85
|
-
export function evaluateGate({ registry, plan, run, addedIds, firstRun }) {
|
|
124
|
+
export function evaluateGate({ registry, plan, run, addedIds, unmergedAddedIds, firstRun, }) {
|
|
86
125
|
const blocking = [];
|
|
87
126
|
// 1) Structure (end-state re-validation).
|
|
88
127
|
blocking.push(...validateStructure(registry, plan).filter((i) => i.level === 'ERROR'));
|
|
@@ -94,9 +133,20 @@ export function evaluateGate({ registry, plan, run, addedIds, firstRun }) {
|
|
|
94
133
|
message: 'Some tests are failing; the change cannot be archived.',
|
|
95
134
|
});
|
|
96
135
|
}
|
|
97
|
-
// 3)
|
|
98
|
-
|
|
99
|
-
|
|
136
|
+
// 3) A file that failed to load, named before the absences it produces.
|
|
137
|
+
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)));
|
|
149
|
+
// 5) Never-red: the added scenarios have to have discriminated once.
|
|
100
150
|
if (addedIds && addedIds.length > 0) {
|
|
101
151
|
blocking.push(...neverRedIssues(plan, addedIds, firstRun ?? {}));
|
|
102
152
|
}
|
package/dist/core/locate.d.ts
CHANGED
|
@@ -1,6 +1,24 @@
|
|
|
1
1
|
import type { Loader } from './loader.js';
|
|
2
2
|
import type { AttestPlan, Issue, Registry } from './types.js';
|
|
3
3
|
export declare const isReqsFile: (name: string) => boolean;
|
|
4
|
+
/**
|
|
5
|
+
* A spec belonging to a change that has not been agreed yet (design §7).
|
|
6
|
+
*
|
|
7
|
+
* The marker is in the **name** rather than the directory because the file
|
|
8
|
+
* already sits where it will live once the change is merged: a proposal's spec
|
|
9
|
+
* is written next to the code it attests, so its relative imports resolve
|
|
10
|
+
* identically before and after the merge, and merging renames it in place
|
|
11
|
+
* instead of moving it up a tree and rewriting every specifier.
|
|
12
|
+
*/
|
|
13
|
+
export declare const isProposedSpecFile: (name: string) => boolean;
|
|
14
|
+
/**
|
|
15
|
+
* A spec belonging to the merged suite.
|
|
16
|
+
*
|
|
17
|
+
* Proposed specs are excluded here rather than at each call site, so the base
|
|
18
|
+
* scan, the base run and every reporting command are wrong together or not at
|
|
19
|
+
* all: a red scenario for behaviour nobody has implemented must not reach any
|
|
20
|
+
* of them until its gate passes.
|
|
21
|
+
*/
|
|
4
22
|
export declare const isSpecFile: (name: string) => boolean;
|
|
5
23
|
/**
|
|
6
24
|
* Recursively find files under root whose basename matches `match`.
|
|
@@ -11,16 +29,21 @@ export declare const isSpecFile: (name: string) => boolean;
|
|
|
11
29
|
* order never depends on which `readdir` happened to resolve first.
|
|
12
30
|
*/
|
|
13
31
|
export declare function findFiles(root: string, match: (name: string) => boolean): Promise<string[]>;
|
|
14
|
-
/** The
|
|
32
|
+
/** The file sets every command needs, collected in one pass. */
|
|
15
33
|
export interface ProjectScan {
|
|
16
34
|
reqsFiles: string[];
|
|
17
35
|
specFiles: string[];
|
|
36
|
+
/** Specs of changes still under review; never part of the base suite. */
|
|
37
|
+
proposedSpecFiles: string[];
|
|
18
38
|
}
|
|
19
39
|
/**
|
|
20
40
|
* Find the registry and spec files under root in a **single** traversal.
|
|
21
41
|
*
|
|
22
42
|
* Calling findFiles once per pattern meant `check` walked the tree twice and
|
|
23
|
-
* `archive` four times, over a tree that cannot change in between.
|
|
43
|
+
* `archive` four times, over a tree that cannot change in between. Proposed
|
|
44
|
+
* specs are collected in the same pass for that reason and kept in their own
|
|
45
|
+
* list: they are found everywhere the merged ones are, and no caller may reach
|
|
46
|
+
* them by accident.
|
|
24
47
|
*/
|
|
25
48
|
export declare function scanProject(root: string): Promise<ProjectScan>;
|
|
26
49
|
/** One file's worth of registry, or the single issue that stopped it. */
|
|
@@ -75,11 +98,28 @@ export declare function staticReader(): RegistryReader;
|
|
|
75
98
|
* prefix a duplicate-prefix ERROR.
|
|
76
99
|
*
|
|
77
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.
|
|
78
107
|
*/
|
|
79
108
|
export declare function loadRegistry(root: string, reader: RegistryReader, files?: string[]): Promise<{
|
|
80
109
|
registry: Registry;
|
|
81
110
|
issues: Issue[];
|
|
111
|
+
prefixOwners: Record<string, string>;
|
|
82
112
|
}>;
|
|
113
|
+
/**
|
|
114
|
+
* The part of an id that names the space it lives in — `AUTH` of `AUTH-3`.
|
|
115
|
+
*
|
|
116
|
+
* The prefix is the only unit above the requirement (design §11) and two things
|
|
117
|
+
* now depend on agreeing about it: `duplicate-prefix`, and which file `--apply`
|
|
118
|
+
* writes an ADDED requirement into. An id with no dash is its own prefix, which
|
|
119
|
+
* cannot arise from `RequirementIdSchema` and is handled anyway because this
|
|
120
|
+
* also runs over ids a delta proposed.
|
|
121
|
+
*/
|
|
122
|
+
export declare function idPrefix(id: string): string;
|
|
83
123
|
/**
|
|
84
124
|
* Parse the given spec files into one merged plan (paths shown relative to
|
|
85
125
|
* `displayRoot`). Files are read concurrently; the merge follows the input
|
|
@@ -92,8 +132,26 @@ export declare function loadRegistry(root: string, reader: RegistryReader, files
|
|
|
92
132
|
export declare function parseSpecs(files: string[], displayRoot: string): Promise<AttestPlan>;
|
|
93
133
|
/** Parse every `*.spec.ts` under root into one merged plan (file paths shown relative to root). */
|
|
94
134
|
export declare function parseAllSpecFiles(root: string): Promise<AttestPlan>;
|
|
95
|
-
/**
|
|
96
|
-
|
|
135
|
+
/**
|
|
136
|
+
* Spec-shaped files sitting under `root/changes` — the location a change's
|
|
137
|
+
* specs used to live at, and which nothing walks any more (design §7).
|
|
138
|
+
*
|
|
139
|
+
* Two correct decisions compose into a blind spot. `changes` is in `SKIP_DIRS`,
|
|
140
|
+
* so `scanProject` cannot reach these; and the explicit include `attest archive`
|
|
141
|
+
* once applied to `changes/<name>/specs/` went away with `changeExcludeGlobs`
|
|
142
|
+
* when specs moved to their merged location. A spec left here therefore executes
|
|
143
|
+
* in no suite and no gate, and the only command that says anything is the gate,
|
|
144
|
+
* blaming coverage for a file it cannot see.
|
|
145
|
+
*
|
|
146
|
+
* Walks through `findFiles` rather than a second walker: `SKIP_DIRS` is consulted
|
|
147
|
+
* for *sub*directories only, so starting the walk at `changes` itself both
|
|
148
|
+
* reaches these files and keeps skipping `node_modules` beneath them.
|
|
149
|
+
*
|
|
150
|
+
* Both spellings are wrong here and both are returned. A `*.spec.ts` is the
|
|
151
|
+
* pre-move layout; a `*.proposed.spec.ts` is the right marker at the wrong path,
|
|
152
|
+
* which is the half-done migration and no more visible than the other.
|
|
153
|
+
*/
|
|
154
|
+
export declare function findChangeDirSpecs(root: string): Promise<string[]>;
|
|
97
155
|
/** List the names of proposed changes under `root/changes`. */
|
|
98
156
|
export declare function listChangeNames(root: string): Promise<string[]>;
|
|
99
157
|
//# sourceMappingURL=locate.d.ts.map
|