@am_shork/attest 0.7.4 → 0.8.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 +761 -131
- package/README.md +1 -1
- package/dist/cli/index.js +11 -8
- package/dist/cli/json.d.ts +26 -1
- package/dist/cli/json.js +28 -2
- package/dist/core/gate.js +3 -3
- package/dist/core/locate.js +4 -4
- package/dist/core/pipeline.d.ts +17 -1
- package/dist/core/pipeline.js +104 -12
- package/dist/core/red-record.d.ts +14 -5
- package/dist/core/red-record.js +82 -24
- package/dist/core/registry-issues.d.ts +30 -0
- package/dist/core/registry-issues.js +26 -0
- package/dist/core/registry.d.ts +12 -5
- package/dist/core/registry.js +10 -8
- package/dist/core/runner.js +21 -9
- package/dist/core/schema.d.ts +32 -27
- package/dist/core/schema.js +33 -5
- package/dist/core/skill.js +13 -0
- package/dist/core/splice.js +45 -14
- package/dist/core/static-registry.js +5 -4
- package/dist/core/status.js +2 -2
- package/dist/core/terminal.js +5 -2
- package/dist/core/types.d.ts +39 -10
- package/package.json +2 -2
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
// The shape of a registry schema failure, and the one rendering of it
|
|
2
|
+
// (design §5.1, §10).
|
|
3
|
+
//
|
|
4
|
+
// Separate from registry.ts because `./define` maps to that whole module, so
|
|
5
|
+
// everything it exports is a public contract. What has to be public here is the
|
|
6
|
+
// element type of `RegistryValidationError.issues`; the formatter does not, and
|
|
7
|
+
// both of its callers are internal.
|
|
8
|
+
/**
|
|
9
|
+
* The message `RegistryValidationError` carries, available without one.
|
|
10
|
+
*
|
|
11
|
+
* Three sites report `registry-invalid` — the throwing path in
|
|
12
|
+
* `defineRequirements`, and the two readers in `locate.ts` and
|
|
13
|
+
* `static-registry.ts` that report it without throwing — and their messages
|
|
14
|
+
* have to be identical rather than merely similar. Sharing this function is
|
|
15
|
+
* what makes that structural rather than a coincidence maintained by hand, so
|
|
16
|
+
* a new site formats through here and does not grow a fourth spelling.
|
|
17
|
+
*/
|
|
18
|
+
export function registryValidationMessage(issues) {
|
|
19
|
+
// `.map(String)` and not a bare `.join('.')`: under `PropertyKey[]` a segment
|
|
20
|
+
// can be a symbol, and `join` throws rather than converting one.
|
|
21
|
+
const fields = issues
|
|
22
|
+
.map((i) => ` - ${i.path.map(String).join('.') || '(root)'}: ${i.message}`)
|
|
23
|
+
.join('\n');
|
|
24
|
+
return `Invalid requirement registry:\n${fields}`;
|
|
25
|
+
}
|
|
26
|
+
//# sourceMappingURL=registry-issues.js.map
|
package/dist/core/registry.d.ts
CHANGED
|
@@ -1,10 +1,18 @@
|
|
|
1
|
-
import { z } from 'zod';
|
|
2
1
|
import { type RegistryInput, type RequirementInput } from './schema.js';
|
|
2
|
+
import { type RegistryValidationIssue } from './registry-issues.js';
|
|
3
3
|
import type { Registry, Requirement } from './types.js';
|
|
4
|
-
|
|
4
|
+
export type { RegistryValidationIssue } from './registry-issues.js';
|
|
5
|
+
/**
|
|
6
|
+
* Thrown when a registry fails structural validation (design §5.1).
|
|
7
|
+
*
|
|
8
|
+
* `issues` is typed as this project's own {@link RegistryValidationIssue} and
|
|
9
|
+
* not the validator's, because this class is reached through the `./define`
|
|
10
|
+
* subpath and the field is therefore a public contract — the same boundary
|
|
11
|
+
* `Issue.message` holds, with the difference that `Issue` was designed with it.
|
|
12
|
+
*/
|
|
5
13
|
export declare class RegistryValidationError extends Error {
|
|
6
|
-
readonly issues:
|
|
7
|
-
constructor(issues:
|
|
14
|
+
readonly issues: RegistryValidationIssue[];
|
|
15
|
+
constructor(issues: RegistryValidationIssue[]);
|
|
8
16
|
}
|
|
9
17
|
/**
|
|
10
18
|
* A `const` type parameter infers an array literal as a tuple, and would infer
|
|
@@ -139,5 +147,4 @@ export declare function delta<const T extends RegistryDelta>(d: T): DefinedDelta
|
|
|
139
147
|
* caller wrote is still the literal they hold.
|
|
140
148
|
*/
|
|
141
149
|
export declare function withProposedRequirements<T extends RegistryDelta>(d: T): T;
|
|
142
|
-
export {};
|
|
143
150
|
//# sourceMappingURL=registry.d.ts.map
|
package/dist/core/registry.js
CHANGED
|
@@ -1,22 +1,24 @@
|
|
|
1
1
|
// Requirement registry authoring API (design §2, §5.1, §7).
|
|
2
2
|
// defineRequirements validates against RegistrySchema and applies defaults;
|
|
3
3
|
// delta() declares a change delta, applied by applyDelta (see apply.ts).
|
|
4
|
-
import { z } from 'zod';
|
|
5
4
|
import { RegistrySchema, } from './schema.js';
|
|
6
|
-
|
|
5
|
+
import { registryValidationMessage, } from './registry-issues.js';
|
|
6
|
+
/**
|
|
7
|
+
* Thrown when a registry fails structural validation (design §5.1).
|
|
8
|
+
*
|
|
9
|
+
* `issues` is typed as this project's own {@link RegistryValidationIssue} and
|
|
10
|
+
* not the validator's, because this class is reached through the `./define`
|
|
11
|
+
* subpath and the field is therefore a public contract — the same boundary
|
|
12
|
+
* `Issue.message` holds, with the difference that `Issue` was designed with it.
|
|
13
|
+
*/
|
|
7
14
|
export class RegistryValidationError extends Error {
|
|
8
15
|
issues;
|
|
9
16
|
constructor(issues) {
|
|
10
|
-
super(
|
|
17
|
+
super(registryValidationMessage(issues));
|
|
11
18
|
this.issues = issues;
|
|
12
19
|
this.name = 'RegistryValidationError';
|
|
13
20
|
}
|
|
14
21
|
}
|
|
15
|
-
function formatIssues(issues) {
|
|
16
|
-
return issues
|
|
17
|
-
.map((i) => ` - ${i.path.join('.') || '(root)'}: ${i.message}`)
|
|
18
|
-
.join('\n');
|
|
19
|
-
}
|
|
20
22
|
/**
|
|
21
23
|
* Declare a requirement registry. Validates structure against RegistrySchema
|
|
22
24
|
* (SHALL/MUST keyword, non-empty rationale, well-formed IDs, scalar params) and
|
package/dist/core/runner.js
CHANGED
|
@@ -70,12 +70,19 @@ export async function runAndCollect(options = {}) {
|
|
|
70
70
|
const runtimeCoverage = new Map();
|
|
71
71
|
const outcomes = new Map();
|
|
72
72
|
// Reconstruct coverage from the task tree (not an in-process singleton).
|
|
73
|
-
|
|
73
|
+
//
|
|
74
|
+
// `file` is threaded down from the file task rather than read off the suite,
|
|
75
|
+
// because it is the only level that carries a path — and it is what keeps
|
|
76
|
+
// two spec files declaring the same scenario name under one `requirement()`
|
|
77
|
+
// from collapsing into a single entry that either of them could satisfy.
|
|
78
|
+
const walk = (task, file) => {
|
|
74
79
|
if (task.type === 'suite') {
|
|
75
80
|
const id = requirementIdOf(task.name);
|
|
76
81
|
if (id !== undefined) {
|
|
77
|
-
const
|
|
78
|
-
const
|
|
82
|
+
const byFile = runtimeCoverage.get(id) ?? new Map();
|
|
83
|
+
const outcomesByFile = outcomes.get(id) ?? new Map();
|
|
84
|
+
const set = byFile.get(file) ?? new Set();
|
|
85
|
+
const byName = outcomesByFile.get(file) ?? new Map();
|
|
79
86
|
for (const c of scenariosUnder(task)) {
|
|
80
87
|
// A scenario counts as covered only if it actually executed —
|
|
81
88
|
// skipped/todo scenarios have no run result (enables §8's
|
|
@@ -87,12 +94,14 @@ export async function runAndCollect(options = {}) {
|
|
|
87
94
|
set.add(c.name);
|
|
88
95
|
byName.set(c.name, outcome);
|
|
89
96
|
}
|
|
90
|
-
|
|
91
|
-
|
|
97
|
+
byFile.set(file, set);
|
|
98
|
+
outcomesByFile.set(file, byName);
|
|
99
|
+
runtimeCoverage.set(id, byFile);
|
|
100
|
+
outcomes.set(id, outcomesByFile);
|
|
92
101
|
}
|
|
93
102
|
}
|
|
94
103
|
for (const c of task.tasks ?? [])
|
|
95
|
-
walk(c);
|
|
104
|
+
walk(c, file);
|
|
96
105
|
};
|
|
97
106
|
// Assigned, never cast. `TaskLike` is the narrow view this walk needs, and
|
|
98
107
|
// Vitest's `File` satisfies it structurally — so the compiler is the thing
|
|
@@ -101,12 +110,15 @@ export async function runAndCollect(options = {}) {
|
|
|
101
110
|
// exactly the change that would silently empty the runtime coverage.
|
|
102
111
|
const unloadedFiles = [];
|
|
103
112
|
for (const file of vitest.state.getFiles()) {
|
|
104
|
-
walk(file);
|
|
105
113
|
// Relative and POSIX for the reason everything derived from the root is
|
|
106
114
|
// (`paths.ts`): this becomes an `Issue.file`, which a `--json` consumer
|
|
107
|
-
// diffs across two CI runs that may not share an operating system
|
|
115
|
+
// diffs across two CI runs that may not share an operating system — and,
|
|
116
|
+
// since the coverage maps are keyed by it, a comparison key against a plan
|
|
117
|
+
// built the same way.
|
|
118
|
+
const relative = relativePath(options.root ?? process.cwd(), file.filepath);
|
|
119
|
+
walk(file, relative);
|
|
108
120
|
if (failedToLoad(file)) {
|
|
109
|
-
unloadedFiles.push(
|
|
121
|
+
unloadedFiles.push(relative);
|
|
110
122
|
}
|
|
111
123
|
}
|
|
112
124
|
unloadedFiles.sort(byCodeUnit);
|
package/dist/core/schema.d.ts
CHANGED
|
@@ -1,27 +1,17 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
/** A scalar param value. `null` is included: it is how an author writes "empty". */
|
|
3
|
-
declare const scalar: z.ZodUnion<[z.ZodNumber, z.ZodString, z.ZodBoolean, z.ZodNull]>;
|
|
3
|
+
declare const scalar: z.ZodUnion<readonly [z.ZodNumber, z.ZodString, z.ZodBoolean, z.ZodNull]>;
|
|
4
4
|
/** Any JSON value — what a param may be. */
|
|
5
5
|
export type ParamValue = z.infer<typeof scalar> | ParamValue[] | {
|
|
6
6
|
[key: string]: ParamValue;
|
|
7
7
|
};
|
|
8
8
|
/** A single behavioural contract (design §2). */
|
|
9
9
|
export declare const RequirementSchema: z.ZodObject<{
|
|
10
|
-
statement: z.
|
|
10
|
+
statement: z.ZodString;
|
|
11
11
|
rationale: z.ZodString;
|
|
12
|
-
params: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodType<ParamValue, z.
|
|
13
|
-
outOfScope: z.ZodDefault<z.ZodArray<z.ZodString
|
|
14
|
-
},
|
|
15
|
-
params: Record<string, ParamValue>;
|
|
16
|
-
statement: string;
|
|
17
|
-
rationale: string;
|
|
18
|
-
outOfScope: string[];
|
|
19
|
-
}, {
|
|
20
|
-
statement: string;
|
|
21
|
-
rationale: string;
|
|
22
|
-
params?: Record<string, ParamValue> | undefined;
|
|
23
|
-
outOfScope?: string[] | undefined;
|
|
24
|
-
}>;
|
|
12
|
+
params: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodType<ParamValue, unknown, z.core.$ZodTypeInternals<ParamValue, unknown>>>>;
|
|
13
|
+
outOfScope: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
14
|
+
}, z.core.$strip>;
|
|
25
15
|
/**
|
|
26
16
|
* The id grammar, on its own.
|
|
27
17
|
*
|
|
@@ -34,23 +24,38 @@ export declare const RequirementSchema: z.ZodObject<{
|
|
|
34
24
|
* two grammars as soon as one of them is edited.
|
|
35
25
|
*/
|
|
36
26
|
export declare const RequirementIdSchema: z.ZodString;
|
|
37
|
-
/**
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
27
|
+
/**
|
|
28
|
+
* The requirement registry: stable ID -> requirement (design §2, §5.1).
|
|
29
|
+
*
|
|
30
|
+
* Guarded by `isPlainObject` for the same reason `jsonObject` is, and it is the
|
|
31
|
+
* *evaluating* reader this protects: `{ '__proto__': { … } }` swaps the
|
|
32
|
+
* prototype rather than creating a key, so the registry a loader builds from
|
|
33
|
+
* that file comes from the prototype and not from anything the file declares.
|
|
34
|
+
* The static reader refuses such a source outright (`registry-not-static`), and
|
|
35
|
+
* the two readers agreeing is a security property (design §5.2), not a
|
|
36
|
+
* convenience — so the schema has to refuse it rather than enumerate it.
|
|
37
|
+
*
|
|
38
|
+
* The guard is written over `RegistryRecord`'s input so `z.input` still
|
|
39
|
+
* describes the authoring shape: `defineRequirements` constrains its `const`
|
|
40
|
+
* type parameter to `RegistryInput`, and a guard typed as a bare record would
|
|
41
|
+
* erase every literal that constraint exists to capture.
|
|
42
|
+
*/
|
|
43
|
+
export declare const RegistrySchema: z.ZodPipe<z.ZodCustom<Record<string, {
|
|
45
44
|
statement: string;
|
|
46
45
|
rationale: string;
|
|
47
|
-
|
|
48
|
-
|
|
46
|
+
params?: Record<string, unknown> | undefined;
|
|
47
|
+
outOfScope?: string[] | undefined;
|
|
48
|
+
}>, Record<string, {
|
|
49
49
|
statement: string;
|
|
50
50
|
rationale: string;
|
|
51
|
-
params?: Record<string,
|
|
51
|
+
params?: Record<string, unknown> | undefined;
|
|
52
52
|
outOfScope?: string[] | undefined;
|
|
53
|
-
}
|
|
53
|
+
}>>, z.ZodRecord<z.ZodString, z.ZodObject<{
|
|
54
|
+
statement: z.ZodString;
|
|
55
|
+
rationale: z.ZodString;
|
|
56
|
+
params: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodType<ParamValue, unknown, z.core.$ZodTypeInternals<ParamValue, unknown>>>>;
|
|
57
|
+
outOfScope: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
58
|
+
}, z.core.$strip>>>;
|
|
54
59
|
/** Parsed (output) shapes — defaults applied. */
|
|
55
60
|
export type Requirement = z.infer<typeof RequirementSchema>;
|
|
56
61
|
export type Registry = z.infer<typeof RegistrySchema>;
|
package/dist/core/schema.js
CHANGED
|
@@ -35,9 +35,7 @@ const jsonObject = z
|
|
|
35
35
|
.custom(isPlainObject)
|
|
36
36
|
.pipe(z.record(z.string(), z.lazy(() => paramValue)));
|
|
37
37
|
const paramValue = z.lazy(() => z.union([scalar, z.array(paramValue), jsonObject], {
|
|
38
|
-
|
|
39
|
-
message: 'expected JSON data (no functions, dates, or class instances)',
|
|
40
|
-
}),
|
|
38
|
+
error: 'expected JSON data (no functions, dates, or class instances)',
|
|
41
39
|
}));
|
|
42
40
|
/** A single behavioural contract (design §2). */
|
|
43
41
|
export const RequirementSchema = z.object({
|
|
@@ -80,6 +78,36 @@ export const RequirementSchema = z.object({
|
|
|
80
78
|
export const RequirementIdSchema = z
|
|
81
79
|
.string()
|
|
82
80
|
.regex(/^[A-Z]+-\d+$/, 'id must look like AUTH-3');
|
|
83
|
-
/**
|
|
84
|
-
|
|
81
|
+
/**
|
|
82
|
+
* The registry's own record, before the plain-object guard below.
|
|
83
|
+
*
|
|
84
|
+
* `error` is carried explicitly because a record reports a key failure as
|
|
85
|
+
* "Invalid key in record" and does not surface the key schema's own message —
|
|
86
|
+
* and a mistyped id is the commonest way to fail this schema, so the grammar is
|
|
87
|
+
* the one thing the author needs to be told.
|
|
88
|
+
*/
|
|
89
|
+
const RegistryRecord = z.record(RequirementIdSchema, RequirementSchema, {
|
|
90
|
+
error: 'id must look like AUTH-3',
|
|
91
|
+
});
|
|
92
|
+
/**
|
|
93
|
+
* The requirement registry: stable ID -> requirement (design §2, §5.1).
|
|
94
|
+
*
|
|
95
|
+
* Guarded by `isPlainObject` for the same reason `jsonObject` is, and it is the
|
|
96
|
+
* *evaluating* reader this protects: `{ '__proto__': { … } }` swaps the
|
|
97
|
+
* prototype rather than creating a key, so the registry a loader builds from
|
|
98
|
+
* that file comes from the prototype and not from anything the file declares.
|
|
99
|
+
* The static reader refuses such a source outright (`registry-not-static`), and
|
|
100
|
+
* the two readers agreeing is a security property (design §5.2), not a
|
|
101
|
+
* convenience — so the schema has to refuse it rather than enumerate it.
|
|
102
|
+
*
|
|
103
|
+
* The guard is written over `RegistryRecord`'s input so `z.input` still
|
|
104
|
+
* describes the authoring shape: `defineRequirements` constrains its `const`
|
|
105
|
+
* type parameter to `RegistryInput`, and a guard typed as a bare record would
|
|
106
|
+
* erase every literal that constraint exists to capture.
|
|
107
|
+
*/
|
|
108
|
+
export const RegistrySchema = z
|
|
109
|
+
.custom(isPlainObject, {
|
|
110
|
+
error: 'a registry must be an object literal',
|
|
111
|
+
})
|
|
112
|
+
.pipe(RegistryRecord);
|
|
85
113
|
//# sourceMappingURL=schema.js.map
|
package/dist/core/skill.js
CHANGED
|
@@ -102,6 +102,19 @@ promises is a two-stage workflow, and the stages are separate on purpose.
|
|
|
102
102
|
rule forbids, and the distinction is the whole of it — that rule is about the
|
|
103
103
|
**expectation** the system is measured against; this pin asserts what the
|
|
104
104
|
**intent** is.
|
|
105
|
+
- **Prefer a quantifier a scenario can iterate.** A statement that says *every*,
|
|
106
|
+
*all* or *any* has a finite set of scenarios under it and nothing holding the
|
|
107
|
+
two together — the id is covered, \`check\` is green, and whether those
|
|
108
|
+
scenarios span what the sentence claims is the part no gate looks at. What
|
|
109
|
+
decides the risk is *what* the quantifier ranges over. Over the inputs one
|
|
110
|
+
place processes, representative inputs settle it. Over a list the code itself
|
|
111
|
+
enumerates, a scenario looping that same list covers a new member by
|
|
112
|
+
construction — **this is the shape to write**, and when that list lives in
|
|
113
|
+
\`params\` rather than in the code, the pin above is what keeps it honest, since
|
|
114
|
+
an author can shorten it. Over *places in the implementation*, each needing its
|
|
115
|
+
own hand-written scenario, a new place is a new obligation and nothing
|
|
116
|
+
enumerates them: that is the one that silently stops being true. When you cannot avoid it, name the places in the statement instead
|
|
117
|
+
of quantifying over them, so a reader can count what is missing.
|
|
105
118
|
|
|
106
119
|
## Rules the engine enforces
|
|
107
120
|
|
package/dist/core/splice.js
CHANGED
|
@@ -25,23 +25,26 @@ import { registryInsertionPoint } from './static-registry.js';
|
|
|
25
25
|
import { toPosixPath } from './paths.js';
|
|
26
26
|
import { byCodeUnit, sortDeep } from './order.js';
|
|
27
27
|
/**
|
|
28
|
-
*
|
|
28
|
+
* The *inside* of a TypeScript string literal quoted with `quote`, holding
|
|
29
|
+
* exactly `value`.
|
|
29
30
|
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
31
|
+
* Split out from `tsString` because the other site that writes into a string
|
|
32
|
+
* literal — `repointImport` — writes between quotes that are already in the
|
|
33
|
+
* file, whose character is the file's choice and not this module's. Escaping
|
|
34
|
+
* has to answer to that character: `'` needs no escape inside `"…"`, and
|
|
35
|
+
* escaping it there would be a wrong byte rather than a safe one.
|
|
36
|
+
*
|
|
37
|
+
* Control characters go out as `\uXXXX` rather than raw, so a value someone
|
|
38
|
+
* pasted a newline into cannot produce a file that no longer parses.
|
|
36
39
|
*/
|
|
37
|
-
function
|
|
38
|
-
let out =
|
|
40
|
+
function tsStringBody(value, quote) {
|
|
41
|
+
let out = '';
|
|
39
42
|
for (const ch of value) {
|
|
40
43
|
const code = ch.codePointAt(0) ?? 0;
|
|
41
44
|
if (ch === '\\')
|
|
42
45
|
out += '\\\\';
|
|
43
|
-
else if (ch ===
|
|
44
|
-
out +=
|
|
46
|
+
else if (ch === quote)
|
|
47
|
+
out += `\\${ch}`;
|
|
45
48
|
else if (ch === '\n')
|
|
46
49
|
out += '\\n';
|
|
47
50
|
else if (ch === '\r')
|
|
@@ -53,7 +56,18 @@ function tsString(value) {
|
|
|
53
56
|
else
|
|
54
57
|
out += ch;
|
|
55
58
|
}
|
|
56
|
-
return
|
|
59
|
+
return out;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* A TypeScript single-quoted string literal holding exactly `value`.
|
|
63
|
+
*
|
|
64
|
+
* Hand-escaped rather than `JSON.stringify`, for one reason that is not style:
|
|
65
|
+
* the registries this writes into are single-quoted throughout, and a merged
|
|
66
|
+
* entry that arrives double-quoted is a diff hunk about quotation marks in the
|
|
67
|
+
* middle of a merge the reviewer is trying to read.
|
|
68
|
+
*/
|
|
69
|
+
function tsString(value) {
|
|
70
|
+
return `'${tsStringBody(value, "'")}'`;
|
|
57
71
|
}
|
|
58
72
|
/**
|
|
59
73
|
* Thrown when a value reaches the emitter that cannot be written as source
|
|
@@ -205,10 +219,27 @@ export function repointImport(file, source, from, to) {
|
|
|
205
219
|
// A bare `x.reqs.js` is a package specifier, not a sibling file.
|
|
206
220
|
if (!target.startsWith('.'))
|
|
207
221
|
target = `./${target}`;
|
|
208
|
-
// Inside the quotes
|
|
209
|
-
|
|
222
|
+
// Inside the quotes, so the file's own quote style is left exactly as it
|
|
223
|
+
// was — which is why the escaping is told which quote it writes between.
|
|
224
|
+
// `to` is the registry file `--apply` chose, making this the one value in
|
|
225
|
+
// the emitter the checked repository names, and a name carrying that quote
|
|
226
|
+
// would otherwise close the literal and put what follows into a committed
|
|
227
|
+
// `*.spec.ts` as code (§7). Escaped rather than refused, unlike the
|
|
228
|
+
// `__proto__` key above: a specifier is a string, and every string has a
|
|
229
|
+
// correct spelling as one.
|
|
230
|
+
const quote = source[spec.getStart(sf)];
|
|
231
|
+
edits.push({
|
|
232
|
+
start: spec.getStart(sf) + 1,
|
|
233
|
+
end: spec.getEnd() - 1,
|
|
234
|
+
text: tsStringBody(target, quote),
|
|
235
|
+
});
|
|
210
236
|
}
|
|
211
237
|
let out = source;
|
|
238
|
+
// Back to front, which is what makes a correction term unnecessary: an edit
|
|
239
|
+
// cannot move an offset that lies before it, so every `start`/`end` above
|
|
240
|
+
// stays valid as the string is rewritten under them. Applying these in source
|
|
241
|
+
// order works only by carrying a running delta and adding it to each
|
|
242
|
+
// subsequent pair — the same result, one more thing to get wrong.
|
|
212
243
|
for (const edit of edits.reverse()) {
|
|
213
244
|
out = out.slice(0, edit.start) + edit.text + out.slice(edit.end);
|
|
214
245
|
}
|
|
@@ -21,7 +21,8 @@
|
|
|
21
21
|
// to "is this a literal", and those two answers must not be able to disagree.
|
|
22
22
|
import ts from 'typescript';
|
|
23
23
|
import { parseSource } from './compiler.js';
|
|
24
|
-
import {
|
|
24
|
+
import { withProposedRequirements } from './registry.js';
|
|
25
|
+
import { registryValidationMessage } from './registry-issues.js';
|
|
25
26
|
import { RegistrySchema } from './schema.js';
|
|
26
27
|
/** The authoring function a registry file must default-export the result of. */
|
|
27
28
|
const DEFINE = 'defineRequirements';
|
|
@@ -128,12 +129,12 @@ export function readRegistrySource(file, source) {
|
|
|
128
129
|
const result = RegistrySchema.safeParse(extracted.value);
|
|
129
130
|
if (!result.success) {
|
|
130
131
|
// defineRequirements throws RegistryValidationError and loadRegistry wraps
|
|
131
|
-
// it;
|
|
132
|
-
//
|
|
132
|
+
// it; sharing one formatter keeps the message identical rather than merely
|
|
133
|
+
// similar.
|
|
133
134
|
return {
|
|
134
135
|
ok: false,
|
|
135
136
|
code: 'registry-invalid',
|
|
136
|
-
message: `Failed to load registry: ${
|
|
137
|
+
message: `Failed to load registry: ${registryValidationMessage(result.error.issues)}`,
|
|
137
138
|
};
|
|
138
139
|
}
|
|
139
140
|
return { ok: true, registry: result.data };
|
package/dist/core/status.js
CHANGED
|
@@ -49,7 +49,7 @@ export function statusRows(addedIds, plan, firstRun) {
|
|
|
49
49
|
name: s.name,
|
|
50
50
|
file: s.file,
|
|
51
51
|
line: s.line,
|
|
52
|
-
firstRun: recordedOutcome(firstRun, reqId, s.name) ?? null,
|
|
52
|
+
firstRun: recordedOutcome(firstRun, { reqId, file: s.file, name: s.name }) ?? null,
|
|
53
53
|
}));
|
|
54
54
|
return { reqId, state: obligationState(reqId, scenarios, firstRun), scenarios };
|
|
55
55
|
});
|
|
@@ -59,7 +59,7 @@ function obligationState(reqId, scenarios, firstRun) {
|
|
|
59
59
|
return 'no-scenario';
|
|
60
60
|
// Every scenario, not any: the gate raises never-red per scenario, so one
|
|
61
61
|
// proven scenario beside an unproven one is still a blocked change.
|
|
62
|
-
return scenarios.every((s) => hasRecordedRed(firstRun, reqId, s.name))
|
|
62
|
+
return scenarios.every((s) => hasRecordedRed(firstRun, { reqId, file: s.file, name: s.name }))
|
|
63
63
|
? 'proven'
|
|
64
64
|
: 'unproven';
|
|
65
65
|
}
|
package/dist/core/terminal.js
CHANGED
|
@@ -12,8 +12,11 @@
|
|
|
12
12
|
// write to the terminal of whoever is checking it. On a fork MR the author is
|
|
13
13
|
// not the reviewer, and the payload is a scenario name or an exception: erase
|
|
14
14
|
// the lines above, repaint a red verdict green, rewrite the window title. The
|
|
15
|
-
// `--json` path was never exposed
|
|
16
|
-
//
|
|
15
|
+
// `--json` path was recorded here as never exposed, on the grounds that
|
|
16
|
+
// `JSON.stringify` escapes every C0 character — true, and narrower than the
|
|
17
|
+
// class below, which also holds DEL and C1. `cli/json.ts` now applies the same
|
|
18
|
+
// rule at the point that document is serialised (ATX-74); this still has to
|
|
19
|
+
// hold independently, because a stream has no such point.
|
|
17
20
|
//
|
|
18
21
|
// **In `core/` rather than in the CLI**, though the CLI is its main caller: the
|
|
19
22
|
// loader has to sanitise Vite's log output for the same reason and cannot
|
package/dist/core/types.d.ts
CHANGED
|
@@ -1,11 +1,22 @@
|
|
|
1
1
|
export type { Requirement, Registry, ParamValue } from './schema.js';
|
|
2
2
|
export type { IssueCode } from './docs.js';
|
|
3
3
|
import type { IssueCode } from './docs.js';
|
|
4
|
-
/**
|
|
5
|
-
|
|
4
|
+
/**
|
|
5
|
+
* What identifies one scenario, everywhere the engine compares one (design §5.4).
|
|
6
|
+
*
|
|
7
|
+
* All three, and as a bundle rather than three positional strings: the file was
|
|
8
|
+
* missing from the key that `declared-not-run` and the first-run record are
|
|
9
|
+
* decided by, and adding it as a fourth `string` argument beside `reqId` and
|
|
10
|
+
* `name` would have made two adjacent parameters of one type that a caller can
|
|
11
|
+
* silently transpose. The compiler cannot catch that; a field name can.
|
|
12
|
+
*/
|
|
13
|
+
export interface ScenarioRef {
|
|
6
14
|
reqId: string;
|
|
7
|
-
name: string;
|
|
8
15
|
file: string;
|
|
16
|
+
name: string;
|
|
17
|
+
}
|
|
18
|
+
/** A scenario extracted statically from a spec file (design §5.2). */
|
|
19
|
+
export interface ParsedScenario extends ScenarioRef {
|
|
9
20
|
line: number;
|
|
10
21
|
}
|
|
11
22
|
/**
|
|
@@ -31,15 +42,33 @@ export type Outcome = 'pass' | 'fail';
|
|
|
31
42
|
export interface RunResult {
|
|
32
43
|
/** True when no test failed. */
|
|
33
44
|
passed: boolean;
|
|
34
|
-
/** reqId -> set of scenario names that actually executed (from the task tree). */
|
|
35
|
-
runtimeCoverage: Map<string, Set<string>>;
|
|
36
45
|
/**
|
|
37
|
-
* reqId ->
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
46
|
+
* reqId -> spec file -> the scenario names that actually executed (from the
|
|
47
|
+
* task tree).
|
|
48
|
+
*
|
|
49
|
+
* **The file is part of the key, and its absence was a false green.** Keyed by
|
|
50
|
+
* `(reqId, name)` alone, two spec files declaring the same scenario name under
|
|
51
|
+
* the same `requirement()` are one entry, so whichever of them executed
|
|
52
|
+
* vouched for the other and `declared-not-run` stayed silent about a scenario
|
|
53
|
+
* that never ran — the one report this tool exists to refuse. The static plan
|
|
54
|
+
* carries `file` on every `ParsedScenario` and the task tree carries it on
|
|
55
|
+
* every file task, so only this map ever forgot it.
|
|
56
|
+
*
|
|
57
|
+
* Project-relative POSIX, like `unloadedFiles` and for the same reason: it is
|
|
58
|
+
* compared against a plan built that way (`paths.ts`).
|
|
59
|
+
*/
|
|
60
|
+
runtimeCoverage: Map<string, Map<string, Set<string>>>;
|
|
61
|
+
/**
|
|
62
|
+
* reqId -> spec file -> scenario name -> how it ended. The same task-tree walk
|
|
63
|
+
* that fills `runtimeCoverage` already had to tell `pass` from `fail` to
|
|
64
|
+
* decide whether a scenario executed at all; this keeps that distinction
|
|
65
|
+
* instead of discarding it, which is what §6's mechanism 2 records.
|
|
66
|
+
*
|
|
67
|
+
* Keyed identically, and for the identical reason: the same collapse reaches
|
|
68
|
+
* `mergeRedRecord`, where a base scenario's recorded `fail` satisfied a
|
|
69
|
+
* same-named proposed scenario's first-red obligation.
|
|
41
70
|
*/
|
|
42
|
-
outcomes: Map<string, Map<string, Outcome
|
|
71
|
+
outcomes: Map<string, Map<string, Map<string, Outcome>>>;
|
|
43
72
|
/**
|
|
44
73
|
* Spec files that failed to *load*, as project-relative POSIX paths.
|
|
45
74
|
*
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@am_shork/attest",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
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": {
|
|
@@ -48,7 +48,7 @@
|
|
|
48
48
|
"chalk": "^5.3.0",
|
|
49
49
|
"commander": "^12.1.0",
|
|
50
50
|
"typescript": "^5.5.0 || ^6.0.0",
|
|
51
|
-
"zod": "^
|
|
51
|
+
"zod": "^4.4.3"
|
|
52
52
|
},
|
|
53
53
|
"peerDependencies": {
|
|
54
54
|
"vite": "^8.0.0",
|