pi-gauntlet 7.41.2 → 7.42.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 +12 -0
- package/README.md +1 -1
- package/agents/code-reviewer.md +16 -0
- package/agents/implementer.md +16 -0
- package/agents/plan-writer.md +18 -4
- package/agents/spec-reviewer.md +18 -0
- package/extensions/lib/plan-check.test.ts +132 -85
- package/extensions/lib/plan-check.ts +28 -8
- package/extensions/phase-tracker.test.ts +60 -2
- package/extensions/phase-tracker.ts +11 -5
- package/package.json +1 -1
- package/skills/brainstorming/reference/amendment-surface.md +5 -1
- package/skills/gatekeep-pr/reference/batch.md +1 -1
- package/skills/subagent-driven-development/SKILL.md +74 -112
- package/skills/subagent-driven-development/code-quality-reviewer-prompt.md +17 -11
- package/skills/subagent-driven-development/implementer-prompt.md +16 -11
- package/skills/subagent-driven-development/reference/dispatch-contract.md +39 -0
- package/skills/subagent-driven-development/reference/fix-loop.md +6 -4
- package/skills/subagent-driven-development/reference/implement-amendments.md +9 -0
- package/skills/subagent-driven-development/spec-reviewer-prompt.md +13 -10
- package/skills/subagent-driven-development/stop-note.md +3 -1
- package/skills/writing-plans/reference/plan-contract.md +1 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import assert from "node:assert/strict";
|
|
2
2
|
import test from "node:test";
|
|
3
|
-
import { checkPlan, sha256, type FsPort, type PlanCheckFinding } from "./plan-check.ts";
|
|
3
|
+
import { checkPlan, renderManifest, sha256, type FsPort, type PlanCheckFinding } from "./plan-check.ts";
|
|
4
4
|
|
|
5
5
|
const ledgerSpec = `# Spec
|
|
6
6
|
## Acceptance criteria
|
|
@@ -17,7 +17,7 @@ const ledger = (rows = ["1 | open | 0 |", "2 | cut | 0 | deferred: #70", "3 | do
|
|
|
17
17
|
|
|
18
18
|
test("ledger admission bypasses task checks and permits empty open evidence", () => {
|
|
19
19
|
for (const input of [ledger(), ledger().replace("iterations: 0\n", "iterations: 0\ncap: 97\n")]) {
|
|
20
|
-
assert.deepEqual(checkPlan(input, ledgerSpec, { exists: () => false, glob: () => [] }), []);
|
|
20
|
+
assert.deepEqual(checkPlan(input, ledgerSpec, { exists: () => false, glob: () => [] }).findings, []);
|
|
21
21
|
}
|
|
22
22
|
});
|
|
23
23
|
for (const [name, rows] of Object.entries({
|
|
@@ -32,38 +32,38 @@ for (const [name, rows] of Object.entries({
|
|
|
32
32
|
attempts: ["1 | open | -1 |", "2 | cut | 0 | deferred: #70", "3 | open | 0 |"],
|
|
33
33
|
fractional: ["1 | open | 1.5 |", "2 | cut | 0 | deferred: #70", "3 | open | 0 |"],
|
|
34
34
|
})) test(`ledger rejects ${name}`, () => {
|
|
35
|
-
assert.ok(checkPlan(ledger(rows), ledgerSpec, { exists: () => false, glob: () => [] }).length);
|
|
35
|
+
assert.ok(checkPlan(ledger(rows), ledgerSpec, { exists: () => false, glob: () => [] }).findings.length);
|
|
36
36
|
});
|
|
37
37
|
test("ledger discriminant is the exact first line", () => {
|
|
38
|
-
for (const prefix of ["\n", " "]) assert.ok(checkPlan(prefix + ledger(), ledgerSpec, { exists: () => false, glob: () => [] }).some(f => f.check === "input"));
|
|
38
|
+
for (const prefix of ["\n", " "]) assert.ok(checkPlan(prefix + ledger(), ledgerSpec, { exists: () => false, glob: () => [] }).findings.some(f => f.check === "input"));
|
|
39
39
|
});
|
|
40
40
|
for (const disposition of ["in-scope", "venue: browser", "deviates: alternate", "deferred: #70", "elsewhere: #71"]) {
|
|
41
41
|
test(`ledger disposition ${disposition}`, () => {
|
|
42
42
|
const cut = /^(deviates|deferred|elsewhere):/.test(disposition);
|
|
43
43
|
const spec = `## Acceptance criteria\n- [ ] Works\n ${disposition}\n`;
|
|
44
|
-
assert.deepEqual(checkPlan(ledger([`1 | ${cut ? "cut" : "open"} | 0 | ${cut ? disposition : ""}`]), spec, { exists: () => false, glob: () => [] }), []);
|
|
45
|
-
assert.ok(checkPlan(ledger([`1 | ${cut ? "done" : "cut"} | 0 | check passed`]), spec, { exists: () => false, glob: () => [] }).length);
|
|
44
|
+
assert.deepEqual(checkPlan(ledger([`1 | ${cut ? "cut" : "open"} | 0 | ${cut ? disposition : ""}`]), spec, { exists: () => false, glob: () => [] }).findings, []);
|
|
45
|
+
assert.ok(checkPlan(ledger([`1 | ${cut ? "done" : "cut"} | 0 | check passed`]), spec, { exists: () => false, glob: () => [] }).findings.length);
|
|
46
46
|
});
|
|
47
47
|
}
|
|
48
48
|
for (const status of ["done", "failed", "cut"]) test(`ledger empty evidence rejects ${status}`, () => {
|
|
49
49
|
const spec = `## Acceptance criteria\n- [ ] Works\n${status === "cut" ? " deferred: #70\n" : ""}`;
|
|
50
|
-
assert.ok(checkPlan(ledger([`1 | ${status} | 0 |`]), spec, { exists: () => false, glob: () => [] }).some(f => /evidence is required/.test(f.reason)));
|
|
50
|
+
assert.ok(checkPlan(ledger([`1 | ${status} | 0 |`]), spec, { exists: () => false, glob: () => [] }).findings.some(f => /evidence is required/.test(f.reason)));
|
|
51
51
|
});
|
|
52
52
|
for (const evidence of ["<check>", "TODO", "TBD", "etc.", "probably"]) {
|
|
53
53
|
test(`ledger rejects non-open evidence ${JSON.stringify(evidence)}`, () => {
|
|
54
|
-
assert.ok(checkPlan(ledger([`1 | failed | 1 | ${evidence}`, "2 | cut | 0 | deferred: #70", "3 | open | 0 |"]), ledgerSpec, { exists: () => false, glob: () => [] }).length);
|
|
54
|
+
assert.ok(checkPlan(ledger([`1 | failed | 1 | ${evidence}`, "2 | cut | 0 | deferred: #70", "3 | open | 0 |"]), ledgerSpec, { exists: () => false, glob: () => [] }).findings.length);
|
|
55
55
|
});
|
|
56
56
|
}
|
|
57
57
|
|
|
58
58
|
test("ledger keeps nested checkboxes and the parent disposition in one AC row", () => {
|
|
59
59
|
const spec = "## Acceptance criteria\n- [ ] Parent\n - [ ] Nested detail\n deferred: #70\n";
|
|
60
|
-
assert.deepEqual(checkPlan(ledger(["1 | cut | 0 | deferred: #70"]), spec, { exists: () => false, glob: () => [] }), []);
|
|
61
|
-
assert.ok(checkPlan(ledger(["1 | open | 0 |"]), spec, { exists: () => false, glob: () => [] }).some(f => /status does not match/.test(f.reason)));
|
|
60
|
+
assert.deepEqual(checkPlan(ledger(["1 | cut | 0 | deferred: #70"]), spec, { exists: () => false, glob: () => [] }).findings, []);
|
|
61
|
+
assert.ok(checkPlan(ledger(["1 | open | 0 |"]), spec, { exists: () => false, glob: () => [] }).findings.some(f => /status does not match/.test(f.reason)));
|
|
62
62
|
});
|
|
63
63
|
|
|
64
64
|
for (const evidence of ["xxx", "[fill in]", "something like", "check; expected a\\|b; passed"]) {
|
|
65
65
|
test(`ledger permits evidence outside the placeholder vocabulary ${JSON.stringify(evidence)}`, () => {
|
|
66
|
-
assert.deepEqual(checkPlan(ledger([`1 | done | 1 | ${evidence}`, "2 | cut | 0 | deferred: #70", "3 | open | 0 |"]), ledgerSpec, { exists: () => false, glob: () => [] }), []);
|
|
66
|
+
assert.deepEqual(checkPlan(ledger([`1 | done | 1 | ${evidence}`, "2 | cut | 0 | deferred: #70", "3 | open | 0 |"]), ledgerSpec, { exists: () => false, glob: () => [] }).findings, []);
|
|
67
67
|
});
|
|
68
68
|
}
|
|
69
69
|
|
|
@@ -173,13 +173,13 @@ function findingsFor(findings: PlanCheckFinding[], check: string): PlanCheckFind
|
|
|
173
173
|
}
|
|
174
174
|
|
|
175
175
|
test("valid fixture: checkPlan returns no findings (also covers the placeholder exemption case)", () => {
|
|
176
|
-
const findings = checkPlan(VALID_PLAN, SPEC_TEXT, alwaysTruePort());
|
|
176
|
+
const findings = checkPlan(VALID_PLAN, SPEC_TEXT, alwaysTruePort()).findings;
|
|
177
177
|
assert.deepEqual(findings, []);
|
|
178
178
|
});
|
|
179
179
|
|
|
180
180
|
test("no gauntlet grammar at all: emits both an absent-wave-headers and an absent-task-headers finding", () => {
|
|
181
181
|
const plan = "# Just a plan\n\nSome prose with no wave or task headers at all.\n";
|
|
182
|
-
const findings = checkPlan(plan, SPEC_TEXT, alwaysTruePort());
|
|
182
|
+
const findings = checkPlan(plan, SPEC_TEXT, alwaysTruePort()).findings;
|
|
183
183
|
const waveFindings = findings.filter((f) => f.reason.includes("wave headers"));
|
|
184
184
|
const taskFindings = findings.filter((f) => f.reason.includes("task headers"));
|
|
185
185
|
assert.equal(waveFindings.length, 1, "expected exactly one absent-wave-headers finding");
|
|
@@ -188,7 +188,7 @@ test("no gauntlet grammar at all: emits both an absent-wave-headers and an absen
|
|
|
188
188
|
|
|
189
189
|
test("check 1 table-closure: task not covered by any Spec coverage row", () => {
|
|
190
190
|
const mutated = VALID_PLAN.replace('| § "Testing" L9-L9 | banned token literal handling | Task 3 |\n', "");
|
|
191
|
-
const findings = checkPlan(mutated, SPEC_TEXT, alwaysTruePort());
|
|
191
|
+
const findings = checkPlan(mutated, SPEC_TEXT, alwaysTruePort()).findings;
|
|
192
192
|
const tc = findingsFor(findings, "table-closure");
|
|
193
193
|
assert.ok(
|
|
194
194
|
tc.some((f) => f.reason.includes("Task 3") && f.reason.includes("does not appear as an owner")),
|
|
@@ -206,7 +206,7 @@ test("check 1 table-closure: task not covered by any Spec coverage row", () => {
|
|
|
206
206
|
|
|
207
207
|
test("check 1 fail-closed: missing '## Spec coverage' table entirely", () => {
|
|
208
208
|
const mutated = VALID_PLAN.slice(0, VALID_PLAN.indexOf("## Spec coverage"));
|
|
209
|
-
const findings = checkPlan(mutated, SPEC_TEXT, alwaysTruePort());
|
|
209
|
+
const findings = checkPlan(mutated, SPEC_TEXT, alwaysTruePort()).findings;
|
|
210
210
|
const tc = findingsFor(findings, "table-closure");
|
|
211
211
|
assert.equal(tc.length, 1);
|
|
212
212
|
assert.match(tc[0].reason, /no '## Spec coverage' table found/);
|
|
@@ -218,7 +218,7 @@ test("check 2 quote-integrity: required verbatim literal missing from owner task
|
|
|
218
218
|
"- via: `helperFn()`\n\nThis task implements helperFn() for parsing.",
|
|
219
219
|
"- via: `parserSeam()`\n\nThis task implements the helper for parsing.",
|
|
220
220
|
);
|
|
221
|
-
const findings = checkPlan(mutated, SPEC_TEXT, alwaysTruePort());
|
|
221
|
+
const findings = checkPlan(mutated, SPEC_TEXT, alwaysTruePort()).findings;
|
|
222
222
|
const qi = findingsFor(findings, "quote-integrity");
|
|
223
223
|
assert.ok(
|
|
224
224
|
qi.some((f) => f.reason.includes("helperFn()") && f.reason.includes("Task 1")),
|
|
@@ -235,7 +235,7 @@ test("check 1 owner-cell grammar: trailing junk after a Task <n> list is malform
|
|
|
235
235
|
'| § "Design" L4-L6 | parser grammar basics | Task 1 |',
|
|
236
236
|
'| § "Design" L4-L6 | parser grammar basics | Task 1 (see note) |',
|
|
237
237
|
);
|
|
238
|
-
const findings = checkPlan(mutated, SPEC_TEXT, alwaysTruePort());
|
|
238
|
+
const findings = checkPlan(mutated, SPEC_TEXT, alwaysTruePort()).findings;
|
|
239
239
|
const tc = findingsFor(findings, "table-closure");
|
|
240
240
|
assert.ok(
|
|
241
241
|
tc.some(
|
|
@@ -260,7 +260,7 @@ test("check 1 owner-cell grammar: empty waiver reason is malformed, not accepted
|
|
|
260
260
|
'| § "Other" L12-L13 | out of scope thing | waived: out of scope per spec |',
|
|
261
261
|
'| § "Other" L12-L13 | out of scope thing | waived: |',
|
|
262
262
|
);
|
|
263
|
-
const findings = checkPlan(mutated, SPEC_TEXT, alwaysTruePort());
|
|
263
|
+
const findings = checkPlan(mutated, SPEC_TEXT, alwaysTruePort()).findings;
|
|
264
264
|
const tc = findingsFor(findings, "table-closure");
|
|
265
265
|
assert.ok(
|
|
266
266
|
tc.some((f) => f.reason.includes("owner cell is not a 'Task <n>' list, 'Verification', or 'waived: <reason>'")),
|
|
@@ -281,7 +281,7 @@ const WAIVED_ROW = '| § "Other" L12-L13 | out of scope thing | waived: out of s
|
|
|
281
281
|
test("check 9 waiver-literal: waived row whose requirement names a code literal fails", () => {
|
|
282
282
|
const row = '| § "Other" L12-L13 | `protectedPaths: []` stays empty | waived: out of scope |';
|
|
283
283
|
const mutated = VALID_PLAN.replace(WAIVED_ROW, row);
|
|
284
|
-
const findings = checkPlan(mutated, SPEC_TEXT, alwaysTruePort());
|
|
284
|
+
const findings = checkPlan(mutated, SPEC_TEXT, alwaysTruePort()).findings;
|
|
285
285
|
const wl = findingsFor(findings, "waiver-literal");
|
|
286
286
|
assert.equal(wl.length, 1, `expected exactly one waiver-literal finding, got: ${JSON.stringify(wl)}`);
|
|
287
287
|
assert.equal(wl[0].line, lineOf(mutated, row));
|
|
@@ -294,7 +294,7 @@ test("check 9 waiver-literal: same requirement owned by a task passes the whole
|
|
|
294
294
|
'**Spec:** doc/specs/fixture-spec.md § "Design" L4-L6',
|
|
295
295
|
'**Spec:** doc/specs/fixture-spec.md § "Design" L4-L6, § "Other" L12-L13',
|
|
296
296
|
);
|
|
297
|
-
assert.deepEqual(checkPlan(mutated, SPEC_TEXT, alwaysTruePort()), []);
|
|
297
|
+
assert.deepEqual(checkPlan(mutated, SPEC_TEXT, alwaysTruePort()).findings, []);
|
|
298
298
|
});
|
|
299
299
|
|
|
300
300
|
test("check 9 waiver-literal: waived prose requirement without backticks passes the whole plan", () => {
|
|
@@ -302,7 +302,7 @@ test("check 9 waiver-literal: waived prose requirement without backticks passes
|
|
|
302
302
|
WAIVED_ROW,
|
|
303
303
|
'| § "Other" L12-L13 | do not add support for protectedPaths | waived: out of scope per spec |',
|
|
304
304
|
);
|
|
305
|
-
assert.deepEqual(checkPlan(mutated, SPEC_TEXT, alwaysTruePort()), []);
|
|
305
|
+
assert.deepEqual(checkPlan(mutated, SPEC_TEXT, alwaysTruePort()).findings, []);
|
|
306
306
|
});
|
|
307
307
|
|
|
308
308
|
const VERIFICATION_ROW = '| § "Acceptance" L16 | full suite passes | Verification |';
|
|
@@ -317,7 +317,7 @@ function withRow(plan: string, row: string): string {
|
|
|
317
317
|
for (const owner of ["verification", "Verify", "VERIFICATION", "Task 1, Verification"]) {
|
|
318
318
|
test(`Verification owner: '${owner}' is a malformed owner -> table-closure`, () => {
|
|
319
319
|
const row = `| § "Acceptance" L16 | full suite passes | ${owner} |`;
|
|
320
|
-
const findings = checkPlan(withRow(VALID_PLAN, row), SPEC_TEXT, alwaysTruePort());
|
|
320
|
+
const findings = checkPlan(withRow(VALID_PLAN, row), SPEC_TEXT, alwaysTruePort()).findings;
|
|
321
321
|
const tc = findingsFor(findings, "table-closure");
|
|
322
322
|
assert.ok(
|
|
323
323
|
tc.some(
|
|
@@ -332,7 +332,7 @@ for (const owner of ["verification", "Verify", "VERIFICATION", "Task 1, Verifica
|
|
|
332
332
|
|
|
333
333
|
test("Verification owner: unparseable anchor ('-') -> table-closure parseability reason", () => {
|
|
334
334
|
const row = "| - | full suite passes | Verification |";
|
|
335
|
-
const findings = checkPlan(withRow(VALID_PLAN, row), SPEC_TEXT, alwaysTruePort());
|
|
335
|
+
const findings = checkPlan(withRow(VALID_PLAN, row), SPEC_TEXT, alwaysTruePort()).findings;
|
|
336
336
|
const tc = findingsFor(findings, "table-closure");
|
|
337
337
|
assert.ok(
|
|
338
338
|
tc.some(
|
|
@@ -345,7 +345,7 @@ test("Verification owner: unparseable anchor ('-') -> table-closure parseability
|
|
|
345
345
|
|
|
346
346
|
test("Verification owner: anchored lines without a backtick literal -> table-closure", () => {
|
|
347
347
|
const row = '| § "Other" L13 | stuff | Verification |';
|
|
348
|
-
const findings = checkPlan(withRow(VALID_PLAN, row), SPEC_TEXT, alwaysTruePort());
|
|
348
|
+
const findings = checkPlan(withRow(VALID_PLAN, row), SPEC_TEXT, alwaysTruePort()).findings;
|
|
349
349
|
const tc = findingsFor(findings, "table-closure");
|
|
350
350
|
assert.ok(
|
|
351
351
|
tc.some(
|
|
@@ -357,7 +357,7 @@ test("Verification owner: anchored lines without a backtick literal -> table-clo
|
|
|
357
357
|
|
|
358
358
|
test("Verification owner: mechanical row -> table-closure", () => {
|
|
359
359
|
const row = "| - | mechanical: run the suite | Verification |";
|
|
360
|
-
const findings = checkPlan(withRow(VALID_PLAN, row), SPEC_TEXT, alwaysTruePort());
|
|
360
|
+
const findings = checkPlan(withRow(VALID_PLAN, row), SPEC_TEXT, alwaysTruePort()).findings;
|
|
361
361
|
const tc = findingsFor(findings, "table-closure");
|
|
362
362
|
assert.ok(
|
|
363
363
|
tc.some((f) => f.reason === "mechanical row owner must be a Task <n>" && f.text === row),
|
|
@@ -367,7 +367,7 @@ test("Verification owner: mechanical row -> table-closure", () => {
|
|
|
367
367
|
|
|
368
368
|
test("table-closure: multi-owner row with unparseable anchor yields one parseability finding", () => {
|
|
369
369
|
const row = "| L999 | naming details | Task 1, Task 2 |";
|
|
370
|
-
const findings = checkPlan(withRow(VALID_PLAN, row), SPEC_TEXT, alwaysTruePort());
|
|
370
|
+
const findings = checkPlan(withRow(VALID_PLAN, row), SPEC_TEXT, alwaysTruePort()).findings;
|
|
371
371
|
const parseability = findingsFor(findings, "table-closure").filter(
|
|
372
372
|
(f) => f.text === row && f.reason.includes("not a parseable"),
|
|
373
373
|
);
|
|
@@ -379,7 +379,7 @@ test("Verification owner: does not satisfy a task's coverage requirement", () =>
|
|
|
379
379
|
'| § "Testing" L9-L9 | banned token literal handling | Task 3 |\n',
|
|
380
380
|
"",
|
|
381
381
|
);
|
|
382
|
-
const findings = checkPlan(mutated, SPEC_TEXT, alwaysTruePort());
|
|
382
|
+
const findings = checkPlan(mutated, SPEC_TEXT, alwaysTruePort()).findings;
|
|
383
383
|
assert.ok(
|
|
384
384
|
findingsFor(findings, "table-closure").some((f) =>
|
|
385
385
|
f.reason.includes("Task 3 does not appear as an owner"),
|
|
@@ -389,7 +389,7 @@ test("Verification owner: does not satisfy a task's coverage requirement", () =>
|
|
|
389
389
|
|
|
390
390
|
test("Verification quote-integrity: literal missing from header -> quote-integrity on the row", () => {
|
|
391
391
|
const mutated = VALID_PLAN.replace("**Verification:** npm run fixture-verify", "**Verification:** npm run other");
|
|
392
|
-
const findings = checkPlan(mutated, SPEC_TEXT, alwaysTruePort());
|
|
392
|
+
const findings = checkPlan(mutated, SPEC_TEXT, alwaysTruePort()).findings;
|
|
393
393
|
const qi = findingsFor(findings, "quote-integrity");
|
|
394
394
|
const row = '| § "Acceptance" L16 | full suite passes | Verification |';
|
|
395
395
|
assert.ok(
|
|
@@ -408,7 +408,7 @@ test("Verification quote-integrity: multi-command header contains the literal",
|
|
|
408
408
|
"**Verification:** npm run fixture-verify",
|
|
409
409
|
"**Verification:** `npm run fixture-verify && npm run lint`",
|
|
410
410
|
);
|
|
411
|
-
assert.deepEqual(findingsFor(checkPlan(mutated, SPEC_TEXT, alwaysTruePort()), "quote-integrity"), []);
|
|
411
|
+
assert.deepEqual(findingsFor(checkPlan(mutated, SPEC_TEXT, alwaysTruePort()).findings, "quote-integrity"), []);
|
|
412
412
|
});
|
|
413
413
|
|
|
414
414
|
test("Verification quote-integrity: two-span header contains both literals", () => {
|
|
@@ -420,12 +420,12 @@ test("Verification quote-integrity: two-span header contains both literals", ()
|
|
|
420
420
|
"**Verification:** npm run fixture-verify",
|
|
421
421
|
"**Verification:** `npm run fixture-verify`, `npm run lint`",
|
|
422
422
|
);
|
|
423
|
-
assert.deepEqual(findingsFor(checkPlan(mutated, spec, alwaysTruePort()), "quote-integrity"), []);
|
|
423
|
+
assert.deepEqual(findingsFor(checkPlan(mutated, spec, alwaysTruePort()).findings, "quote-integrity"), []);
|
|
424
424
|
});
|
|
425
425
|
|
|
426
426
|
test("Verification quote-integrity: missing header -> quote-integrity per literal plus header-entrypoint", () => {
|
|
427
427
|
const mutated = VALID_PLAN.replace("**Verification:** npm run fixture-verify\n", "");
|
|
428
|
-
const findings = checkPlan(mutated, SPEC_TEXT, alwaysTruePort());
|
|
428
|
+
const findings = checkPlan(mutated, SPEC_TEXT, alwaysTruePort()).findings;
|
|
429
429
|
assert.equal(
|
|
430
430
|
findingsFor(findings, "quote-integrity").filter((f) => f.reason.includes("`npm run fixture-verify`")).length,
|
|
431
431
|
1,
|
|
@@ -438,7 +438,7 @@ test("Verification quote-integrity: task body containing the full header string
|
|
|
438
438
|
"This task handles naming details.",
|
|
439
439
|
"This task handles naming details. Run npm run fixture-verify here.",
|
|
440
440
|
);
|
|
441
|
-
const findings = checkPlan(mutated, SPEC_TEXT, alwaysTruePort());
|
|
441
|
+
const findings = checkPlan(mutated, SPEC_TEXT, alwaysTruePort()).findings;
|
|
442
442
|
assert.ok(findingsFor(findings, "header-entrypoint").some((f) => f.text.includes("Run npm run fixture-verify here")));
|
|
443
443
|
assert.deepEqual(findingsFor(findings, "quote-integrity"), []);
|
|
444
444
|
});
|
|
@@ -448,13 +448,13 @@ test("Verification quote-integrity: task body containing only a sub-command of a
|
|
|
448
448
|
"**Verification:** npm run fixture-verify",
|
|
449
449
|
"**Verification:** `npm run fixture-verify && npm run lint`",
|
|
450
450
|
).replace("This task handles naming details.", "This task handles naming details. Run npm run fixture-verify here.");
|
|
451
|
-
const findings = checkPlan(mutated, SPEC_TEXT, alwaysTruePort());
|
|
451
|
+
const findings = checkPlan(mutated, SPEC_TEXT, alwaysTruePort()).findings;
|
|
452
452
|
assert.deepEqual(findingsFor(findings, "header-entrypoint"), []);
|
|
453
453
|
assert.deepEqual(findingsFor(findings, "quote-integrity"), []);
|
|
454
454
|
});
|
|
455
455
|
|
|
456
|
-
const HE = (plan: string) => findingsFor(checkPlan(plan, SPEC_TEXT, alwaysTruePort()), "header-entrypoint");
|
|
457
|
-
const WD = (plan: string) => findingsFor(checkPlan(plan, SPEC_TEXT, alwaysTruePort()), "wave-file-disjointness");
|
|
456
|
+
const HE = (plan: string) => findingsFor(checkPlan(plan, SPEC_TEXT, alwaysTruePort()).findings, "header-entrypoint");
|
|
457
|
+
const WD = (plan: string) => findingsFor(checkPlan(plan, SPEC_TEXT, alwaysTruePort()).findings, "wave-file-disjointness");
|
|
458
458
|
|
|
459
459
|
test("header-entrypoint: Run: with backticked payload npm test under header npm test && npm run lint fails (regression for the hole)", () => {
|
|
460
460
|
const mutated = VALID_PLAN.replace("**Verification:** npm run fixture-verify", "**Verification:** npm test && npm run lint")
|
|
@@ -487,21 +487,21 @@ test("header-entrypoint: Tests: bullets are judged by tests-block, not here", ()
|
|
|
487
487
|
"**Tests:**\n- `npm run fixture-verify`",
|
|
488
488
|
);
|
|
489
489
|
assert.deepEqual(HE(mutated), []);
|
|
490
|
-
assert.ok(findingsFor(checkPlan(mutated, SPEC_TEXT, alwaysTruePort()), "tests-block").some((f) => f.reason.includes("full-suite command")));
|
|
490
|
+
assert.ok(findingsFor(checkPlan(mutated, SPEC_TEXT, alwaysTruePort()).findings, "tests-block").some((f) => f.reason.includes("full-suite command")));
|
|
491
491
|
});
|
|
492
492
|
|
|
493
493
|
const withHappyPath = (plan: string, line: string) =>
|
|
494
494
|
plan.replace("**Verification:** npm run fixture-verify", `**Verification:** npm run fixture-verify\n\n${line}`);
|
|
495
|
-
const HP = (plan: string) => findingsFor(checkPlan(plan, SPEC_TEXT, alwaysTruePort()), "header-happy-path");
|
|
495
|
+
const HP = (plan: string) => findingsFor(checkPlan(plan, SPEC_TEXT, alwaysTruePort()).findings, "header-happy-path");
|
|
496
496
|
|
|
497
497
|
test("header-happy-path: line with label, backticked command, and (~5m) duration is accepted", () => {
|
|
498
498
|
const plan = withHappyPath(VALID_PLAN, "**Happy path:** stack - `script/e2e-stack` (~5m)");
|
|
499
|
-
assert.deepEqual(checkPlan(plan, SPEC_TEXT, alwaysTruePort()), []);
|
|
499
|
+
assert.deepEqual(checkPlan(plan, SPEC_TEXT, alwaysTruePort()).findings, []);
|
|
500
500
|
});
|
|
501
501
|
|
|
502
502
|
test("header-happy-path: line without a duration is accepted", () => {
|
|
503
503
|
const plan = withHappyPath(VALID_PLAN, "**Happy path:** stack - `script/e2e-stack`");
|
|
504
|
-
assert.deepEqual(checkPlan(plan, SPEC_TEXT, alwaysTruePort()), []);
|
|
504
|
+
assert.deepEqual(checkPlan(plan, SPEC_TEXT, alwaysTruePort()).findings, []);
|
|
505
505
|
});
|
|
506
506
|
|
|
507
507
|
test("header-happy-path: line without label, backticks, or a valid duration is malformed", () => {
|
|
@@ -528,7 +528,7 @@ test("tests-block: Tests: bullet equal to the happy-path command is a full-suite
|
|
|
528
528
|
"**Tests:**\n- `node --test extensions/lib/fixture-task2.test.ts`",
|
|
529
529
|
"**Tests:**\n- `script/e2e-stack`",
|
|
530
530
|
);
|
|
531
|
-
const tb = findingsFor(checkPlan(plan, SPEC_TEXT, alwaysTruePort()), "tests-block");
|
|
531
|
+
const tb = findingsFor(checkPlan(plan, SPEC_TEXT, alwaysTruePort()).findings, "tests-block");
|
|
532
532
|
assert.ok(tb.some((f) => f.reason.includes("full-suite command")), JSON.stringify(tb));
|
|
533
533
|
});
|
|
534
534
|
|
|
@@ -543,7 +543,7 @@ test("header-entrypoint: Run: payload and free text repeating the happy-path com
|
|
|
543
543
|
test("table-closure: owner cell 'Happy path' is malformed under the existing reason", () => {
|
|
544
544
|
const row = '| § "Acceptance" L16 | full suite passes | Happy path |';
|
|
545
545
|
const plan = withHappyPath(withRow(VALID_PLAN, row), "**Happy path:** stack - `script/e2e-stack` (~5m)");
|
|
546
|
-
const tc = findingsFor(checkPlan(plan, SPEC_TEXT, alwaysTruePort()), "table-closure");
|
|
546
|
+
const tc = findingsFor(checkPlan(plan, SPEC_TEXT, alwaysTruePort()).findings, "table-closure");
|
|
547
547
|
assert.ok(tc.some((f) => f.reason === "owner cell is not a 'Task <n>' list, 'Verification', or 'waived: <reason>'" && f.text === row));
|
|
548
548
|
});
|
|
549
549
|
|
|
@@ -565,7 +565,7 @@ test("Verification quote-integrity: task-owned literal check unchanged", () => {
|
|
|
565
565
|
"- via: `helperFn()`\n\nThis task implements helperFn() for parsing.",
|
|
566
566
|
"- via: `parserSeam()`\n\nThis task implements the helper.",
|
|
567
567
|
);
|
|
568
|
-
const qi = findingsFor(checkPlan(mutated, SPEC_TEXT, alwaysTruePort()), "quote-integrity");
|
|
568
|
+
const qi = findingsFor(checkPlan(mutated, SPEC_TEXT, alwaysTruePort()).findings, "quote-integrity");
|
|
569
569
|
assert.ok(qi.some((f) => f.reason.includes("Task 1 body does not contain the required verbatim literal `helperFn()`")));
|
|
570
570
|
});
|
|
571
571
|
|
|
@@ -608,7 +608,7 @@ Run node --test extensions/lib/plan-check.test.ts and confirm green.
|
|
|
608
608
|
`;
|
|
609
609
|
|
|
610
610
|
test("quote-integrity: header entrypoint literal on an anchored line is satisfied by the header, not the task body", () => {
|
|
611
|
-
const findings = checkPlan(ENTRYPOINT_PLAN, ENTRYPOINT_SPEC, alwaysTruePort());
|
|
611
|
+
const findings = checkPlan(ENTRYPOINT_PLAN, ENTRYPOINT_SPEC, alwaysTruePort()).findings;
|
|
612
612
|
assert.deepEqual(findingsFor(findings, "quote-integrity"), []);
|
|
613
613
|
assert.deepEqual(findingsFor(findings, "header-entrypoint"), []);
|
|
614
614
|
});
|
|
@@ -618,20 +618,20 @@ test("quote-integrity: scoped command on the same anchored line is still require
|
|
|
618
618
|
"- Test: extensions/lib/plan-check.test.ts\n\n**Tests:**\n- `node --test extensions/lib/plan-check.test.ts`\n- baseline: abc1234 pass\n\nRun node --test extensions/lib/plan-check.test.ts and confirm green.",
|
|
619
619
|
"- Test: extensions/lib/other.test.ts\n\n**Tests:**\n- `node --test extensions/lib/other.test.ts`\n\nRun the scoped test and confirm green.",
|
|
620
620
|
);
|
|
621
|
-
const qi = findingsFor(checkPlan(mutated, ENTRYPOINT_SPEC, alwaysTruePort()), "quote-integrity");
|
|
621
|
+
const qi = findingsFor(checkPlan(mutated, ENTRYPOINT_SPEC, alwaysTruePort()).findings, "quote-integrity");
|
|
622
622
|
assert.equal(qi.length, 1);
|
|
623
623
|
assert.ok(qi[0].reason.includes("Task 1 body does not contain the required verbatim literal `node --test extensions/lib/plan-check.test.ts`"));
|
|
624
624
|
});
|
|
625
625
|
|
|
626
626
|
test("placeholder-scan: header entrypoint is not a required literal for the task (parity with quote-integrity)", () => {
|
|
627
|
-
const findings = checkPlan(ENTRYPOINT_PLAN, ENTRYPOINT_SPEC, alwaysTruePort());
|
|
627
|
+
const findings = checkPlan(ENTRYPOINT_PLAN, ENTRYPOINT_SPEC, alwaysTruePort()).findings;
|
|
628
628
|
assert.deepEqual(findingsFor(findings, "placeholder-scan"), []);
|
|
629
629
|
assert.deepEqual(findings, []);
|
|
630
630
|
});
|
|
631
631
|
|
|
632
632
|
test("check 3 anchor-resolution: ambiguous heading match (duplicate spec heading)", () => {
|
|
633
633
|
const dupSpec = SPEC_TEXT.replace('## Testing', '## Design\n\nduplicate section body.\n\n## Testing');
|
|
634
|
-
const findings = checkPlan(VALID_PLAN, dupSpec, alwaysTruePort());
|
|
634
|
+
const findings = checkPlan(VALID_PLAN, dupSpec, alwaysTruePort()).findings;
|
|
635
635
|
const ar = findingsFor(findings, "anchor-resolution");
|
|
636
636
|
assert.ok(
|
|
637
637
|
ar.some((f) => f.reason.includes("ambiguous") && f.reason.includes('"Design"')),
|
|
@@ -652,7 +652,7 @@ test("check 3 fail-closed: unparseable **Spec:** anchor line", () => {
|
|
|
652
652
|
'**Spec:** doc/specs/fixture-spec.md § "Design" L4-L6',
|
|
653
653
|
"**Spec:** doc/specs/fixture-spec.md § Design without quotes or line numbers",
|
|
654
654
|
);
|
|
655
|
-
const findings = checkPlan(mutated, SPEC_TEXT, alwaysTruePort());
|
|
655
|
+
const findings = checkPlan(mutated, SPEC_TEXT, alwaysTruePort()).findings;
|
|
656
656
|
const ar = findingsFor(findings, "anchor-resolution");
|
|
657
657
|
assert.equal(ar.length, 1);
|
|
658
658
|
assert.match(ar[0].reason, /unparseable/);
|
|
@@ -664,7 +664,7 @@ test("check 4 paths-exist: Modify: literal path does not exist", () => {
|
|
|
664
664
|
exists: (p) => p !== "extensions/lib/fixture-shared.ts",
|
|
665
665
|
glob: () => [],
|
|
666
666
|
};
|
|
667
|
-
const findings = checkPlan(VALID_PLAN, SPEC_TEXT, port);
|
|
667
|
+
const findings = checkPlan(VALID_PLAN, SPEC_TEXT, port).findings;
|
|
668
668
|
const pe = findingsFor(findings, "paths-exist");
|
|
669
669
|
assert.ok(
|
|
670
670
|
pe.some((f) => f.reason.includes("fixture-shared.ts") && f.reason.includes("does not exist")),
|
|
@@ -681,7 +681,7 @@ test("check 4 paths-exist: backtick-wrapped Modify: path is stripped before the
|
|
|
681
681
|
"- Modify: extensions/lib/fixture-shared.ts",
|
|
682
682
|
"- Modify: `extensions/lib/fixture-shared.ts`",
|
|
683
683
|
);
|
|
684
|
-
const findings = checkPlan(mutated, SPEC_TEXT, alwaysTruePort());
|
|
684
|
+
const findings = checkPlan(mutated, SPEC_TEXT, alwaysTruePort()).findings;
|
|
685
685
|
assert.deepEqual(findingsFor(findings, "paths-exist"), []);
|
|
686
686
|
});
|
|
687
687
|
|
|
@@ -696,7 +696,7 @@ test("check 4 fail-closed: invalid glob (port throws)", () => {
|
|
|
696
696
|
throw new Error("bad pattern");
|
|
697
697
|
},
|
|
698
698
|
};
|
|
699
|
-
const findings = checkPlan(mutated, SPEC_TEXT, port);
|
|
699
|
+
const findings = checkPlan(mutated, SPEC_TEXT, port).findings;
|
|
700
700
|
const pe = findingsFor(findings, "paths-exist");
|
|
701
701
|
assert.ok(
|
|
702
702
|
pe.some((f) => f.reason.includes("invalid") && f.reason.includes("bad pattern")),
|
|
@@ -717,7 +717,7 @@ test("check 4 fail-closed: task missing **Files:** block", () => {
|
|
|
717
717
|
"**Files:**\n- Modify: extensions/lib/fixture-task3.ts\n- Test: extensions/lib/fixture-task3.test.ts\n\n**Tests:**\n- `node --test extensions/lib/fixture-task3.test.ts`\n- baseline: abc1234 pass\n\n",
|
|
718
718
|
"",
|
|
719
719
|
);
|
|
720
|
-
const findings = checkPlan(mutated, SPEC_TEXT, alwaysTruePort());
|
|
720
|
+
const findings = checkPlan(mutated, SPEC_TEXT, alwaysTruePort()).findings;
|
|
721
721
|
const pe = findingsFor(findings, "paths-exist");
|
|
722
722
|
assert.ok(
|
|
723
723
|
pe.some((f) => f.reason.includes("missing a **Files:** block") && f.text.includes("Task 3")),
|
|
@@ -738,7 +738,7 @@ test("check 5 placeholder-scan: banned token flagged outside a required-literal
|
|
|
738
738
|
"This task handles naming details.",
|
|
739
739
|
"This task handles naming details. TODO clean this up.",
|
|
740
740
|
);
|
|
741
|
-
const findings = checkPlan(mutated, SPEC_TEXT, alwaysTruePort());
|
|
741
|
+
const findings = checkPlan(mutated, SPEC_TEXT, alwaysTruePort()).findings;
|
|
742
742
|
const ps = findingsFor(findings, "placeholder-scan");
|
|
743
743
|
assert.ok(
|
|
744
744
|
ps.some((f) => f.reason.includes('"TODO"') && f.line === lineOf(mutated, "TODO clean this up")),
|
|
@@ -751,7 +751,7 @@ test("check 6 wave-file-disjointness: two tasks in the same wave declare the sam
|
|
|
751
751
|
"- Modify: extensions/lib/fixture-other.ts",
|
|
752
752
|
"- Modify: extensions/lib/fixture-shared.ts",
|
|
753
753
|
);
|
|
754
|
-
const findings = checkPlan(mutated, SPEC_TEXT, alwaysTruePort());
|
|
754
|
+
const findings = checkPlan(mutated, SPEC_TEXT, alwaysTruePort()).findings;
|
|
755
755
|
const wfd = findingsFor(findings, "wave-file-disjointness");
|
|
756
756
|
assert.ok(
|
|
757
757
|
wfd.some((f) => f.reason.includes("Task 1") && f.reason.includes("Task 2") && f.reason.includes("fixture-shared.ts")),
|
|
@@ -768,9 +768,9 @@ test("every-wave-singleton plan without Solo: lines passes; legacy Solo: lines a
|
|
|
768
768
|
"### Task 2: Implement naming",
|
|
769
769
|
"## Wave 2 - Naming\n\n### Task 2: Implement naming",
|
|
770
770
|
).replace("## Wave 2 — Solo task", "## Wave 3 - Documentation");
|
|
771
|
-
assert.deepEqual(checkPlan(plan, SPEC_TEXT, alwaysTruePort()), []);
|
|
771
|
+
assert.deepEqual(checkPlan(plan, SPEC_TEXT, alwaysTruePort()).findings, []);
|
|
772
772
|
const legacy = plan.replace("## Wave 2 - Naming", "## Wave 2 - Naming\n\nSolo: Task 2 depends on Wave 1");
|
|
773
|
-
assert.deepEqual(checkPlan(legacy, SPEC_TEXT, alwaysTruePort()), []);
|
|
773
|
+
assert.deepEqual(checkPlan(legacy, SPEC_TEXT, alwaysTruePort()).findings, []);
|
|
774
774
|
});
|
|
775
775
|
|
|
776
776
|
test("check 8 header-entrypoint: verification entrypoint string reused outside the header", () => {
|
|
@@ -778,7 +778,7 @@ test("check 8 header-entrypoint: verification entrypoint string reused outside t
|
|
|
778
778
|
"This task handles naming details.",
|
|
779
779
|
"This task handles naming details. Run `npm run fixture-verify` here too.",
|
|
780
780
|
);
|
|
781
|
-
const findings = checkPlan(mutated, SPEC_TEXT, alwaysTruePort());
|
|
781
|
+
const findings = checkPlan(mutated, SPEC_TEXT, alwaysTruePort()).findings;
|
|
782
782
|
const he = findingsFor(findings, "header-entrypoint");
|
|
783
783
|
assert.ok(
|
|
784
784
|
he.some((f) => f.line === lineOf(mutated, "Run `npm run fixture-verify` here too")),
|
|
@@ -791,7 +791,7 @@ test("check 8 header-entrypoint: entrypoint text inside the '## Spec coverage' t
|
|
|
791
791
|
"parser grammar basics",
|
|
792
792
|
"parser grammar basics npm run fixture-verify",
|
|
793
793
|
);
|
|
794
|
-
const findings = checkPlan(mutated, SPEC_TEXT, alwaysTruePort());
|
|
794
|
+
const findings = checkPlan(mutated, SPEC_TEXT, alwaysTruePort()).findings;
|
|
795
795
|
assert.deepEqual(findingsFor(findings, "header-entrypoint"), []);
|
|
796
796
|
});
|
|
797
797
|
|
|
@@ -834,7 +834,7 @@ The literal TODO is intentionally documented here per spec quote-integrity requi
|
|
|
834
834
|
`;
|
|
835
835
|
|
|
836
836
|
test("check 2/5 exemption: task body carrying the required verbatim TODO literal satisfies quote-integrity and exempts placeholder-scan", () => {
|
|
837
|
-
const findings = checkPlan(EXEMPTION_PLAN, EXEMPTION_SPEC_TEXT, alwaysTruePort());
|
|
837
|
+
const findings = checkPlan(EXEMPTION_PLAN, EXEMPTION_SPEC_TEXT, alwaysTruePort()).findings;
|
|
838
838
|
assert.deepEqual(findings, []);
|
|
839
839
|
});
|
|
840
840
|
|
|
@@ -843,7 +843,7 @@ test("check 2/5 exemption negative: without the task body carrying the literal,
|
|
|
843
843
|
"The literal TODO is intentionally documented here per spec quote-integrity requirement.",
|
|
844
844
|
"This task implements the handling here.",
|
|
845
845
|
);
|
|
846
|
-
const findings = checkPlan(mutated, EXEMPTION_SPEC_TEXT, alwaysTruePort());
|
|
846
|
+
const findings = checkPlan(mutated, EXEMPTION_SPEC_TEXT, alwaysTruePort()).findings;
|
|
847
847
|
const qi = findingsFor(findings, "quote-integrity");
|
|
848
848
|
const ps = findingsFor(findings, "placeholder-scan");
|
|
849
849
|
assert.ok(
|
|
@@ -874,14 +874,14 @@ test("aggregate: independent mutations across two checks are all reported togeth
|
|
|
874
874
|
"- Modify: extensions/lib/fixture-other.ts",
|
|
875
875
|
"- Modify: extensions/lib/fixture-shared.ts",
|
|
876
876
|
);
|
|
877
|
-
const findings = checkPlan(mutated, SPEC_TEXT, alwaysTruePort());
|
|
877
|
+
const findings = checkPlan(mutated, SPEC_TEXT, alwaysTruePort()).findings;
|
|
878
878
|
assert.ok(findingsFor(findings, "quote-integrity").length > 0, "expected a quote-integrity finding");
|
|
879
879
|
assert.ok(findingsFor(findings, "wave-file-disjointness").length > 0, "expected a wave-file-disjointness finding");
|
|
880
880
|
});
|
|
881
881
|
|
|
882
882
|
test("grammarless plan: absent-structure findings do not short-circuit the catalog (placeholder-scan still runs)", () => {
|
|
883
883
|
const grammarless = "# Not a plan\n\nJust some prose with a TODO in it.\n";
|
|
884
|
-
const findings = checkPlan(grammarless, SPEC_TEXT, alwaysTruePort());
|
|
884
|
+
const findings = checkPlan(grammarless, SPEC_TEXT, alwaysTruePort()).findings;
|
|
885
885
|
const input = findingsFor(findings, "input");
|
|
886
886
|
assert.ok(
|
|
887
887
|
input.some((f) => f.reason.includes("no wave headers found")),
|
|
@@ -899,12 +899,12 @@ test("grammarless plan: absent-structure findings do not short-circuit the catal
|
|
|
899
899
|
});
|
|
900
900
|
|
|
901
901
|
test("fail-closed: checkPlan never throws on an empty string", () => {
|
|
902
|
-
assert.doesNotThrow(() => checkPlan("", SPEC_TEXT, alwaysTruePort()));
|
|
902
|
+
assert.doesNotThrow(() => checkPlan("", SPEC_TEXT, alwaysTruePort()).findings);
|
|
903
903
|
});
|
|
904
904
|
|
|
905
905
|
test("fail-closed: checkPlan never throws on a non-markdown garbage blob", () => {
|
|
906
906
|
const garbage = "\u0000\u0001binary\ngarbage\tstuff \u{1F4A9} \"\"\"\n```unterminated fence";
|
|
907
|
-
assert.doesNotThrow(() => checkPlan(garbage, SPEC_TEXT, alwaysTruePort()));
|
|
907
|
+
assert.doesNotThrow(() => checkPlan(garbage, SPEC_TEXT, alwaysTruePort()).findings);
|
|
908
908
|
});
|
|
909
909
|
|
|
910
910
|
test("sha256 is deterministic and content-sensitive", () => {
|
|
@@ -926,7 +926,7 @@ const T2_TESTS = "**Tests:**\n- `node --test extensions/lib/fixture-task2.test.t
|
|
|
926
926
|
const T2_FILES_TEST = "- Test: extensions/lib/fixture-task2.test.ts\n";
|
|
927
927
|
|
|
928
928
|
function tb(plan: string, spec = SPEC_TEXT, fs: FsPort = alwaysTruePort()): PlanCheckFinding[] {
|
|
929
|
-
return findingsFor(checkPlan(plan, spec, fs), "tests-block");
|
|
929
|
+
return findingsFor(checkPlan(plan, spec, fs).findings, "tests-block");
|
|
930
930
|
}
|
|
931
931
|
|
|
932
932
|
test("tests-block: block missing when a task has no **Tests:**", () => {
|
|
@@ -1116,7 +1116,7 @@ test("tests-block: no path normalization - ./x and x differ, as in Files:", () =
|
|
|
1116
1116
|
const BASELINE = "- baseline: abc1234 pass";
|
|
1117
1117
|
const T1_COMMAND = "node --test extensions/lib/fixture-task1.test.ts";
|
|
1118
1118
|
const T1_PATH = "extensions/lib/fixture-task1.test.ts";
|
|
1119
|
-
const baselineFindings = (plan: string) => findingsFor(checkPlan(plan, SPEC_TEXT, alwaysTruePort()), "baseline-line");
|
|
1119
|
+
const baselineFindings = (plan: string) => findingsFor(checkPlan(plan, SPEC_TEXT, alwaysTruePort()).findings, "baseline-line");
|
|
1120
1120
|
|
|
1121
1121
|
for (const [name, evidence] of [
|
|
1122
1122
|
["pass", BASELINE],
|
|
@@ -1196,7 +1196,7 @@ for (const [name, body, count, malformed] of [
|
|
|
1196
1196
|
] as const) {
|
|
1197
1197
|
test(`open-questions ${name}`, () => {
|
|
1198
1198
|
const plan = VALID_PLAN + (body ? "\n## Open Questions\n\n" + body + "\n" : "");
|
|
1199
|
-
const findings = checkPlan(plan, SPEC_TEXT, alwaysTruePort());
|
|
1199
|
+
const findings = checkPlan(plan, SPEC_TEXT, alwaysTruePort()).findings;
|
|
1200
1200
|
const questions = findingsFor(findings, "open-questions");
|
|
1201
1201
|
assert.equal(questions.length, count);
|
|
1202
1202
|
if (malformed) assert.equal(questions[0].reason, "## Open Questions malformed: expected `- ` rows or `- none`");
|
|
@@ -1207,7 +1207,7 @@ for (const [name, body, count, malformed] of [
|
|
|
1207
1207
|
|
|
1208
1208
|
test("placeholder-scan masks only valid question output, not authored fences or continuations", () => {
|
|
1209
1209
|
for (const text of ["```\nTODO\n```", "## Open Questions\n ```\n TODO\n ```", "## Open Questions\n- TODO unresolved", "## Open Questions\n" + FAILURE_ROW + "\n ```\n TODO", "## Open Questions\n" + FAILURE_ROW + "\n ```\nTODO\n ```", "## Open Questions\n" + FAILURE_ROW + "\n ran: `TODO`", "## Open Questions\n" + FAILURE_ROW + "\n blocked: TODO"]) {
|
|
1210
|
-
assert.ok(findingsFor(checkPlan(VALID_PLAN + "\n" + text, SPEC_TEXT, alwaysTruePort()), "placeholder-scan").length);
|
|
1210
|
+
assert.ok(findingsFor(checkPlan(VALID_PLAN + "\n" + text, SPEC_TEXT, alwaysTruePort()).findings, "placeholder-scan").length);
|
|
1211
1211
|
}
|
|
1212
1212
|
});
|
|
1213
1213
|
|
|
@@ -1232,11 +1232,11 @@ const coordinatedLedger = () => ledger(["1 | open | 0 |", "2 | open | 0 |", "3 |
|
|
|
1232
1232
|
+ "\n## Leg mapping\n| row | ac | leg |\n|---|---|---|\n| 1 | 1 | repo-a |\n| 2 | 1 | repo-b |\n| 3 | 2 | repo-a |\n| 4 | 3 | repo-a |\n";
|
|
1233
1233
|
const emptyPort: FsPort = { exists: () => false, glob: () => [] };
|
|
1234
1234
|
test("coordinated ledger expands shared ownership and defaults unlisted ACs home", () => {
|
|
1235
|
-
assert.deepEqual(checkPlan(coordinatedLedger(), coordinatedSpec, emptyPort), []);
|
|
1235
|
+
assert.deepEqual(checkPlan(coordinatedLedger(), coordinatedSpec, emptyPort).findings, []);
|
|
1236
1236
|
});
|
|
1237
1237
|
for (const scope of ["", "scope: AC 1\nscope: AC 2", "scope: AC 1, AC 1", "scope: AC 0", "scope: AC -1", "scope: AC 1.5", "scope: AC 4", "scope: 1", "scope: AC 1-2", "scope: AC 1,", "scope: none, AC 1"]) {
|
|
1238
1238
|
test(`coordinated scope rejects ${JSON.stringify(scope)}`, () => {
|
|
1239
|
-
assert.ok(checkPlan(coordinatedLedger(), coordinatedSpec.replace("scope: AC 1, AC 2", scope), emptyPort).some(f => f.check === "leg-scope"));
|
|
1239
|
+
assert.ok(checkPlan(coordinatedLedger(), coordinatedSpec.replace("scope: AC 1, AC 2", scope), emptyPort).findings.some(f => f.check === "leg-scope"));
|
|
1240
1240
|
});
|
|
1241
1241
|
}
|
|
1242
1242
|
for (const [name, change] of Object.entries({
|
|
@@ -1250,10 +1250,10 @@ for (const [name, change] of Object.entries({
|
|
|
1250
1250
|
foreignTarget: (s: string) => s + "pending: 3 | repo-b | abc1234\n",
|
|
1251
1251
|
duplicatePending: (s: string) => s + "pending: seed | repo-b | abc1234\npending: seed | repo-b | abc1234\n",
|
|
1252
1252
|
})) test(`coordinated ledger rejects ${name}`, () => {
|
|
1253
|
-
assert.ok(checkPlan(change(coordinatedLedger()), coordinatedSpec, emptyPort).length);
|
|
1253
|
+
assert.ok(checkPlan(change(coordinatedLedger()), coordinatedSpec, emptyPort).findings.length);
|
|
1254
1254
|
});
|
|
1255
1255
|
test("coordinated ledger admits partial seeds, recovery tuples and later cap grants", () => {
|
|
1256
|
-
for (const target of ["2", "seed"]) assert.deepEqual(checkPlan(coordinatedLedger().replace("seeded repo-a: no", "seeded repo-a: yes").replace("cap: 20", "cap: 99") + `pending: ${target} | REPO-B | abc1234\n`, coordinatedSpec, emptyPort), []);
|
|
1256
|
+
for (const target of ["2", "seed"]) assert.deepEqual(checkPlan(coordinatedLedger().replace("seeded repo-a: no", "seeded repo-a: yes").replace("cap: 20", "cap: 99") + `pending: ${target} | REPO-B | abc1234\n`, coordinatedSpec, emptyPort).findings, []);
|
|
1257
1257
|
});
|
|
1258
1258
|
|
|
1259
1259
|
const legPlanSpec = SPEC_TEXT + "\n## Legs\n### Leg: repo-a\nscope: none\n### Leg: repo-b\nscope: none\n";
|
|
@@ -1262,28 +1262,28 @@ const legPlan = () => VALID_PLAN.replace("---", "Baseline repo-a: abc1234\nBasel
|
|
|
1262
1262
|
.replace("- baseline: abc1234 pass\n\nThis task handles", "- baseline: def5678 pass\n\nThis task handles");
|
|
1263
1263
|
test("plan accepts distinct leg baselines, case-insensitive membership and absent home Leg", () => {
|
|
1264
1264
|
const probes: (string | undefined)[] = [];
|
|
1265
|
-
assert.deepEqual(checkPlan(legPlan(), legPlanSpec, { exists: (_p, leg) => { probes.push(leg); return true; }, glob: () => [] }), []);
|
|
1265
|
+
assert.deepEqual(checkPlan(legPlan(), legPlanSpec, { exists: (_p, leg) => { probes.push(leg); return true; }, glob: () => [] }).findings, []);
|
|
1266
1266
|
assert.ok(probes.includes("repo-b"));
|
|
1267
1267
|
assert.ok(probes.includes("repo-a"));
|
|
1268
1268
|
});
|
|
1269
1269
|
test("wrong-leg baseline and mixed-leg waves fail", () => {
|
|
1270
|
-
assert.ok(checkPlan(legPlan().replace("def5678 pass", "abc1234 pass"), legPlanSpec, alwaysTruePort()).some(f => f.check === "baseline-line"));
|
|
1271
|
-
assert.ok(checkPlan(legPlan().replace("## Wave 3 - Companion\n\n", ""), legPlanSpec, alwaysTruePort()).some(f => f.check === "wave-leg"));
|
|
1270
|
+
assert.ok(checkPlan(legPlan().replace("def5678 pass", "abc1234 pass"), legPlanSpec, alwaysTruePort()).findings.some(f => f.check === "baseline-line"));
|
|
1271
|
+
assert.ok(checkPlan(legPlan().replace("## Wave 3 - Companion\n\n", ""), legPlanSpec, alwaysTruePort()).findings.some(f => f.check === "wave-leg"));
|
|
1272
1272
|
});
|
|
1273
1273
|
test("unknown legs fail before filesystem probing", () => {
|
|
1274
1274
|
const probes: string[] = [];
|
|
1275
|
-
assert.ok(checkPlan(legPlan().replace("Leg: REPO-B", "Leg: stranger"), legPlanSpec, { exists: (p) => { probes.push(p); return true; }, glob: () => [] }).some(f => f.check === "leg-unknown"));
|
|
1275
|
+
assert.ok(checkPlan(legPlan().replace("Leg: REPO-B", "Leg: stranger"), legPlanSpec, { exists: (p) => { probes.push(p); return true; }, glob: () => [] }).findings.some(f => f.check === "leg-unknown"));
|
|
1276
1276
|
assert.deepEqual(probes, []);
|
|
1277
1277
|
});
|
|
1278
1278
|
test("cross-leg creates do not supply Test paths or skipped baseline evidence", () => {
|
|
1279
1279
|
const plan = legPlan().replaceAll("extensions/lib/fixture-task2.test.ts", "extensions/lib/fixture-task1.ts").replace("def5678 pass", "def5678 skipped - extensions/lib/fixture-task1.ts created by this task");
|
|
1280
|
-
const findings = checkPlan(plan, legPlanSpec, { exists: p => p !== "extensions/lib/fixture-task1.ts", glob: () => [] });
|
|
1280
|
+
const findings = checkPlan(plan, legPlanSpec, { exists: p => p !== "extensions/lib/fixture-task1.ts", glob: () => [] }).findings;
|
|
1281
1281
|
assert.ok(findings.some(f => f.check === "tests-block" && /unknown/.test(f.reason)));
|
|
1282
1282
|
assert.ok(findings.some(f => f.check === "baseline-line" && /not a Create/.test(f.reason)));
|
|
1283
1283
|
});
|
|
1284
1284
|
test("identical paths across legs do not overlap even in rejected mixed waves", () => {
|
|
1285
1285
|
const plan = legPlan().replace("## Wave 3 - Companion\n\n", "").replaceAll("fixture-other.ts", "fixture-shared.ts");
|
|
1286
|
-
assert.deepEqual(checkPlan(plan, legPlanSpec, alwaysTruePort()).filter(f => f.check === "wave-file-disjointness"), []);
|
|
1286
|
+
assert.deepEqual(checkPlan(plan, legPlanSpec, alwaysTruePort()).findings.filter(f => f.check === "wave-file-disjointness"), []);
|
|
1287
1287
|
});
|
|
1288
1288
|
|
|
1289
1289
|
for (const [name, change] of Object.entries({
|
|
@@ -1304,33 +1304,80 @@ for (const [name, change] of Object.entries({
|
|
|
1304
1304
|
unknownMapping: (s: string) => s.replace("| 2 | 1 | repo-b |", "| 2 | 1 | stranger |"),
|
|
1305
1305
|
disposition: (s: string) => s.replace("| 3 | cut | 0 | deferred: #70 |", "| 3 | open | 0 | |"),
|
|
1306
1306
|
})) test(`coordinated metadata rejects ${name}`, () => {
|
|
1307
|
-
assert.ok(checkPlan(change(coordinatedLedger()), coordinatedSpec, emptyPort).length);
|
|
1307
|
+
assert.ok(checkPlan(change(coordinatedLedger()), coordinatedSpec, emptyPort).findings.length);
|
|
1308
1308
|
});
|
|
1309
1309
|
test("scope textual order does not change original AC or member order, and design claims no ownership", () => {
|
|
1310
1310
|
const spec = coordinatedSpec.replace("scope: AC 1, AC 2", "scope: AC 2, AC 1\ndesign: AC 3 touches lib/other.ts");
|
|
1311
|
-
assert.deepEqual(checkPlan(coordinatedLedger().replace("| 4 | open | 0 | |", "| 4 | done | 1 | checked a\\|b |"), spec, emptyPort), []);
|
|
1311
|
+
assert.deepEqual(checkPlan(coordinatedLedger().replace("| 4 | open | 0 | |", "| 4 | done | 1 | checked a\\|b |"), spec, emptyPort).findings, []);
|
|
1312
1312
|
});
|
|
1313
1313
|
test("scope none defaults all original ACs home, preserving cut numbering and ignoring nested checks", () => {
|
|
1314
1314
|
const spec = coordinatedSpec.replace("- [ ] First", "- [ ] First\n - [ ] Nested").replace("scope: AC 1, AC 2", "scope: none").replace("scope: AC 1\n", "scope: none\n");
|
|
1315
1315
|
const input = coordinatedLedger().replace("| 2 | open | 0 | |\n", "").replace("| 3 | cut", "| 2 | cut").replace("| 4 | open", "| 3 | open")
|
|
1316
1316
|
.replace("| 2 | 1 | repo-b |\n", "").replace("| 3 | 2 | repo-a |", "| 2 | 2 | repo-a |").replace("| 4 | 3 | repo-a |", "| 3 | 3 | repo-a |");
|
|
1317
|
-
assert.deepEqual(checkPlan(input, spec, emptyPort), []);
|
|
1317
|
+
assert.deepEqual(checkPlan(input, spec, emptyPort).findings, []);
|
|
1318
1318
|
});
|
|
1319
1319
|
test("single-member ledger preserves legacy headers and four-column bytes", () => {
|
|
1320
|
-
assert.deepEqual(checkPlan(ledger(), ledgerSpec + "\n## Legs\n### Leg: repo-a\n", emptyPort), []);
|
|
1320
|
+
assert.deepEqual(checkPlan(ledger(), ledgerSpec + "\n## Legs\n### Leg: repo-a\n", emptyPort).findings, []);
|
|
1321
1321
|
});
|
|
1322
1322
|
test("leg-local globs and plain Files: resolve at each normalized leg", () => {
|
|
1323
1323
|
const calls: string[] = [];
|
|
1324
1324
|
const plan = legPlan().replaceAll("**Files:**", "Files:").replace("Modify: extensions/lib/fixture-other.ts", "Modify: extensions/lib/companion-*.ts");
|
|
1325
|
-
assert.deepEqual(checkPlan(plan, legPlanSpec, { exists: () => true, glob: (pattern, leg) => { calls.push(`${leg}:${pattern}`); return ["extensions/lib/companion-one.ts"]; } }), []);
|
|
1325
|
+
assert.deepEqual(checkPlan(plan, legPlanSpec, { exists: () => true, glob: (pattern, leg) => { calls.push(`${leg}:${pattern}`); return ["extensions/lib/companion-one.ts"]; } }).findings, []);
|
|
1326
1326
|
assert.deepEqual(calls, ["repo-b:extensions/lib/companion-*.ts"]);
|
|
1327
1327
|
});
|
|
1328
1328
|
test("coordinated scope counts only original unfenced top-level AC rows", () => {
|
|
1329
1329
|
const spec = coordinatedSpec.replace("- [ ] First", "```text\n- [ ] Example, not an AC\n```\n- [ ] First");
|
|
1330
|
-
assert.deepEqual(checkPlan(coordinatedLedger(), spec, emptyPort), []);
|
|
1331
|
-
assert.ok(checkPlan(coordinatedLedger(), spec.replace("scope: AC 1\n", "scope: AC 4\n"), emptyPort).some(f => f.check === "leg-scope"));
|
|
1330
|
+
assert.deepEqual(checkPlan(coordinatedLedger(), spec, emptyPort).findings, []);
|
|
1331
|
+
assert.ok(checkPlan(coordinatedLedger(), spec.replace("scope: AC 1\n", "scope: AC 4\n"), emptyPort).findings.some(f => f.check === "leg-scope"));
|
|
1332
1332
|
});
|
|
1333
1333
|
test("coordinated ledger rejects duplicate mapping sections", () => {
|
|
1334
1334
|
const input = coordinatedLedger();
|
|
1335
|
-
assert.ok(checkPlan(input + input.slice(input.indexOf("## Leg mapping")), coordinatedSpec, emptyPort).length);
|
|
1335
|
+
assert.ok(checkPlan(input + input.slice(input.indexOf("## Leg mapping")), coordinatedSpec, emptyPort).findings.length);
|
|
1336
|
+
});
|
|
1337
|
+
|
|
1338
|
+
|
|
1339
|
+
test("manifest preserves ordered task data and optional coordination fields", () => {
|
|
1340
|
+
const plan = VALID_PLAN.replaceAll("Task 1", "Task 4").replaceAll("Task 2", "Task 9")
|
|
1341
|
+
.replace("Implement helper", 'Implement | "helper"')
|
|
1342
|
+
.replace("extensions/lib/fixture-shared.ts", 'extensions/lib/"shared".ts:12-14')
|
|
1343
|
+
.replace("This task implements", 'Orchestrator: persist the measured value into Task 9\nOrchestrator: ignored second line\nThis task implements')
|
|
1344
|
+
.replace("## Wave 1 — Two parallel tasks", "## Wave 1 - Two parallel tasks (in-place)")
|
|
1345
|
+
.replace("- via: `helperFn()`", "- via: `helperFn()`\n- `node extensions/lib/fixture-task1.test.ts; node extensions/lib/fixture-task1.test.ts | node extensions/lib/fixture-task1.test.ts`\n- baseline: abc1234 pass");
|
|
1346
|
+
const result = checkPlan(plan, SPEC_TEXT, alwaysTruePort());
|
|
1347
|
+
assert.deepEqual(result.findings, []);
|
|
1348
|
+
assert.equal(result.ledger, false);
|
|
1349
|
+
assert.ok(result.parsed);
|
|
1350
|
+
const [header, ...lines] = renderManifest(result.parsed, '/repo/plan.md', '/repo/spec.md').split("\n");
|
|
1351
|
+
assert.equal(header, 'manifest: /repo/plan.md | spec: /repo/spec.md | tasks: 3 | waves: 2');
|
|
1352
|
+
const tasks = lines.map(line => JSON.parse(line));
|
|
1353
|
+
assert.deepEqual(Object.keys(tasks[0]), ['task', 'wave', 'mode', 'heading', 'files', 'tests', 'orchestrator']);
|
|
1354
|
+
assert.equal(tasks[0].task, 4);
|
|
1355
|
+
assert.equal(tasks[0].wave, 1);
|
|
1356
|
+
assert.equal(tasks[0].mode, 'in-place');
|
|
1357
|
+
assert.equal(tasks[0].heading, 'Implement | "helper"');
|
|
1358
|
+
assert.deepEqual(tasks[0].files, [
|
|
1359
|
+
{ kind: 'Create', path: 'extensions/lib/fixture-task1.ts' },
|
|
1360
|
+
{ kind: 'Modify', path: 'extensions/lib/"shared".ts:12-14' },
|
|
1361
|
+
{ kind: 'Test', path: 'extensions/lib/fixture-task1.test.ts' },
|
|
1362
|
+
]);
|
|
1363
|
+
assert.deepEqual(tasks[0].tests, ['node --test extensions/lib/fixture-task1.test.ts', 'node extensions/lib/fixture-task1.test.ts; node extensions/lib/fixture-task1.test.ts | node extensions/lib/fixture-task1.test.ts']);
|
|
1364
|
+
assert.equal(tasks[0].orchestrator, 'persist the measured value into Task 9');
|
|
1365
|
+
assert.equal(tasks[1].task, 9);
|
|
1366
|
+
assert.ok(!('orchestrator' in tasks[1]));
|
|
1367
|
+
assert.ok(!('leg' in tasks[0]));
|
|
1368
|
+
assert.ok(!('mode' in tasks[2]));
|
|
1369
|
+
});
|
|
1370
|
+
|
|
1371
|
+
test("manifest uses normalized legs and omits unmarked prose and no-tests commands", () => {
|
|
1372
|
+
const result = checkPlan(legPlan(), legPlanSpec, alwaysTruePort());
|
|
1373
|
+
assert.deepEqual(result.findings, []);
|
|
1374
|
+
assert.ok(result.parsed);
|
|
1375
|
+
const tasks = renderManifest(result.parsed, '/repo/plan.md', '/repo/spec.md').split('\n').slice(1).map(line => JSON.parse(line));
|
|
1376
|
+
assert.deepEqual(tasks.map(t => t.leg), ['repo-a', 'repo-b', 'repo-a']);
|
|
1377
|
+
const plan = '# Plan\n**Verification:** all-checks\n---\n## Wave 1 - Docs\n### Task 9: Docs\n**Files:**\n- Modify: README.md\n**Tests:**\n- none: docs\nPrimary: persist the value\n## Spec coverage\n| anchor | requirement | owner |\n|---|---|---|\n| - | mechanical: docs | Task 9 |';
|
|
1378
|
+
const noTests = checkPlan(plan, '', alwaysTruePort());
|
|
1379
|
+
assert.deepEqual(noTests.findings, []);
|
|
1380
|
+
assert.ok(noTests.parsed);
|
|
1381
|
+
assert.deepEqual(JSON.parse(renderManifest(noTests.parsed, '/p', '/s').split('\n')[1]), { task: 9, wave: 1, heading: 'Docs', files: [{kind: 'Modify', path: 'README.md'}], tests: [] });
|
|
1382
|
+
assert.equal(checkPlan(ledger(), ledgerSpec, emptyPort).ledger, true);
|
|
1336
1383
|
});
|