@am_shork/attest 0.4.2 → 0.5.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 +615 -55
- package/README.md +32 -11
- package/dist/core/apply.d.ts +15 -0
- package/dist/core/apply.js +43 -1
- package/dist/core/docs.d.ts +1 -1
- package/dist/core/docs.js +2 -0
- package/dist/core/gate.d.ts +40 -3
- package/dist/core/gate.js +56 -6
- package/dist/core/locate.d.ts +25 -4
- package/dist/core/locate.js +34 -11
- package/dist/core/parser.js +0 -0
- package/dist/core/pipeline.d.ts +0 -9
- package/dist/core/pipeline.js +117 -41
- 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 +62 -7
- package/dist/core/static-registry.js +6 -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
|
|
|
@@ -67,13 +75,13 @@ export default defineRequirements({
|
|
|
67
75
|
});
|
|
68
76
|
```
|
|
69
77
|
|
|
70
|
-
**2. Attest it with
|
|
78
|
+
**2. Attest it with scenarios** (`session.spec.ts`):
|
|
71
79
|
|
|
72
80
|
```ts
|
|
73
81
|
import { expect } from 'vitest';
|
|
74
82
|
import { requirement, scenario } from '@am_shork/attest';
|
|
75
|
-
import reqs from '
|
|
76
|
-
import { createSession, advance, isValid } from './session.js';
|
|
83
|
+
import reqs from './requirements/auth.reqs.js';
|
|
84
|
+
import { createSession, advance, touch, isValid } from './session.js';
|
|
77
85
|
|
|
78
86
|
requirement('AUTH-3', () => {
|
|
79
87
|
scenario('idle timeout invalidates the session', () => {
|
|
@@ -82,9 +90,22 @@ requirement('AUTH-3', () => {
|
|
|
82
90
|
advance(s, t + 1, 'minutes');
|
|
83
91
|
expect(isValid(s, t)).toBe(false);
|
|
84
92
|
});
|
|
93
|
+
|
|
94
|
+
scenario('activity resets the idle timer', () => {
|
|
95
|
+
const t = reqs['AUTH-3'].params.idleTimeoutMin;
|
|
96
|
+
const s = createSession();
|
|
97
|
+
advance(s, t - 1, 'minutes');
|
|
98
|
+
touch(s);
|
|
99
|
+
advance(s, t - 1, 'minutes');
|
|
100
|
+
expect(isValid(s, t)).toBe(true);
|
|
101
|
+
});
|
|
85
102
|
});
|
|
86
103
|
```
|
|
87
104
|
|
|
105
|
+
Both files above are quoted from `fixtures/consumer/`, which the packaging test
|
|
106
|
+
installs from a real tarball and runs — a test asserts the quotes are byte-equal
|
|
107
|
+
to the files, so a sample the engine would now reject cannot survive here.
|
|
108
|
+
|
|
88
109
|
**3. Run the engine:**
|
|
89
110
|
|
|
90
111
|
```bash
|
|
@@ -136,7 +157,7 @@ Every diagnostic carries a `code`, and every code has a section in
|
|
|
136
157
|
```
|
|
137
158
|
ERROR registry-not-static (requirements/upload.reqs.ts:5)
|
|
138
159
|
Value is not a literal.
|
|
139
|
-
→ https://gitlab.com/Pseudorca/attest/-/blob/v0.
|
|
160
|
+
→ https://gitlab.com/Pseudorca/attest/-/blob/v0.5.0/docs/en/troubleshooting.md#registry-not-static
|
|
140
161
|
```
|
|
141
162
|
|
|
142
163
|
The anchor **is** the code, so the link cannot point somewhere the section
|
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) {
|
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", "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"];
|
|
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,6 +21,7 @@ import { packageVersion } from './version.js';
|
|
|
21
21
|
export const ISSUE_CODES = [
|
|
22
22
|
'add-conflict',
|
|
23
23
|
'add-invalid',
|
|
24
|
+
'added-id-unmerged',
|
|
24
25
|
'change-not-found',
|
|
25
26
|
'declared-not-run',
|
|
26
27
|
'duplicate-prefix',
|
|
@@ -34,6 +35,7 @@ export const ISSUE_CODES = [
|
|
|
34
35
|
'never-red',
|
|
35
36
|
'orphan-test',
|
|
36
37
|
'possible-drift',
|
|
38
|
+
'proposed-spec-unclaimed',
|
|
37
39
|
'rationale-placeholder',
|
|
38
40
|
'registry-invalid',
|
|
39
41
|
'registry-no-default',
|
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. */
|
|
@@ -92,8 +115,6 @@ export declare function loadRegistry(root: string, reader: RegistryReader, files
|
|
|
92
115
|
export declare function parseSpecs(files: string[], displayRoot: string): Promise<AttestPlan>;
|
|
93
116
|
/** Parse every `*.spec.ts` under root into one merged plan (file paths shown relative to root). */
|
|
94
117
|
export declare function parseAllSpecFiles(root: string): Promise<AttestPlan>;
|
|
95
|
-
/** Parse the spec files belonging to a single proposed change (design §8). */
|
|
96
|
-
export declare function parseChangeSpecs(root: string, changeName: string): Promise<AttestPlan>;
|
|
97
118
|
/** List the names of proposed changes under `root/changes`. */
|
|
98
119
|
export declare function listChangeNames(root: string): Promise<string[]>;
|
|
99
120
|
//# sourceMappingURL=locate.d.ts.map
|
package/dist/core/locate.js
CHANGED
|
@@ -20,7 +20,25 @@ const SKIP_DIRS = new Set([
|
|
|
20
20
|
'archive',
|
|
21
21
|
]);
|
|
22
22
|
export const isReqsFile = (name) => name.endsWith('.reqs.ts');
|
|
23
|
-
|
|
23
|
+
/**
|
|
24
|
+
* A spec belonging to a change that has not been agreed yet (design §7).
|
|
25
|
+
*
|
|
26
|
+
* The marker is in the **name** rather than the directory because the file
|
|
27
|
+
* already sits where it will live once the change is merged: a proposal's spec
|
|
28
|
+
* is written next to the code it attests, so its relative imports resolve
|
|
29
|
+
* identically before and after the merge, and merging renames it in place
|
|
30
|
+
* instead of moving it up a tree and rewriting every specifier.
|
|
31
|
+
*/
|
|
32
|
+
export const isProposedSpecFile = (name) => name.endsWith('.proposed.spec.ts');
|
|
33
|
+
/**
|
|
34
|
+
* A spec belonging to the merged suite.
|
|
35
|
+
*
|
|
36
|
+
* Proposed specs are excluded here rather than at each call site, so the base
|
|
37
|
+
* scan, the base run and every reporting command are wrong together or not at
|
|
38
|
+
* all: a red scenario for behaviour nobody has implemented must not reach any
|
|
39
|
+
* of them until its gate passes.
|
|
40
|
+
*/
|
|
41
|
+
export const isSpecFile = (name) => name.endsWith('.spec.ts') && !isProposedSpecFile(name);
|
|
24
42
|
/**
|
|
25
43
|
* Recursively find files under root whose basename matches `match`.
|
|
26
44
|
*
|
|
@@ -53,19 +71,29 @@ export async function findFiles(root, match) {
|
|
|
53
71
|
* Find the registry and spec files under root in a **single** traversal.
|
|
54
72
|
*
|
|
55
73
|
* Calling findFiles once per pattern meant `check` walked the tree twice and
|
|
56
|
-
* `archive` four times, over a tree that cannot change in between.
|
|
74
|
+
* `archive` four times, over a tree that cannot change in between. Proposed
|
|
75
|
+
* specs are collected in the same pass for that reason and kept in their own
|
|
76
|
+
* list: they are found everywhere the merged ones are, and no caller may reach
|
|
77
|
+
* them by accident.
|
|
57
78
|
*/
|
|
58
79
|
export async function scanProject(root) {
|
|
59
|
-
const files = await findFiles(root, (n) => isReqsFile(n) || isSpecFile(n));
|
|
80
|
+
const files = await findFiles(root, (n) => isReqsFile(n) || isSpecFile(n) || isProposedSpecFile(n));
|
|
60
81
|
const reqsFiles = [];
|
|
61
82
|
const specFiles = [];
|
|
83
|
+
const proposedSpecFiles = [];
|
|
62
84
|
for (const f of files) {
|
|
63
|
-
|
|
85
|
+
const name = basename(f);
|
|
86
|
+
// Sorted by `isSpecFile` rather than by `isProposedSpecFile`, so what makes
|
|
87
|
+
// a spec part of the merged suite is decided in exactly one place: the two
|
|
88
|
+
// predicates would otherwise both have to be right about the same file.
|
|
89
|
+
if (isReqsFile(name))
|
|
64
90
|
reqsFiles.push(f);
|
|
65
|
-
else
|
|
91
|
+
else if (isSpecFile(name))
|
|
66
92
|
specFiles.push(f);
|
|
93
|
+
else
|
|
94
|
+
proposedSpecFiles.push(f);
|
|
67
95
|
}
|
|
68
|
-
return { reqsFiles, specFiles };
|
|
96
|
+
return { reqsFiles, specFiles, proposedSpecFiles };
|
|
69
97
|
}
|
|
70
98
|
/**
|
|
71
99
|
* Read registries by **executing** the module through the Vite loader.
|
|
@@ -251,11 +279,6 @@ export async function parseSpecs(files, displayRoot) {
|
|
|
251
279
|
export async function parseAllSpecFiles(root) {
|
|
252
280
|
return parseSpecs(await findFiles(root, isSpecFile), root);
|
|
253
281
|
}
|
|
254
|
-
/** Parse the spec files belonging to a single proposed change (design §8). */
|
|
255
|
-
export async function parseChangeSpecs(root, changeName) {
|
|
256
|
-
const dir = join(root, 'changes', changeName);
|
|
257
|
-
return parseSpecs(await findFiles(dir, isSpecFile), root);
|
|
258
|
-
}
|
|
259
282
|
/** List the names of proposed changes under `root/changes`. */
|
|
260
283
|
export async function listChangeNames(root) {
|
|
261
284
|
try {
|
package/dist/core/parser.js
CHANGED
|
Binary file
|
package/dist/core/pipeline.d.ts
CHANGED
|
@@ -120,15 +120,6 @@ export declare function runInit(root: string, names: readonly string[]): Promise
|
|
|
120
120
|
files: string[];
|
|
121
121
|
issues: Issue[];
|
|
122
122
|
}>;
|
|
123
|
-
/**
|
|
124
|
-
* The globs that keep *other* proposals out of a change's gate run.
|
|
125
|
-
*
|
|
126
|
-
* Sibling names come from `readdir`, not from the guard above, so they are
|
|
127
|
-
* escaped: a directory called `feat(auth)` pasted in raw is a *pattern*, it
|
|
128
|
-
* matches nothing, and that sibling's specs silently join the run — quietly
|
|
129
|
-
* widening the scope of the one check that decides "done".
|
|
130
|
-
*/
|
|
131
|
-
export declare function changeExcludeGlobs(others: string[]): string[];
|
|
132
123
|
export interface StatusResult {
|
|
133
124
|
change: string;
|
|
134
125
|
/** One row per added id, in id order. */
|