* docs(#3186): record the disk-strict completion decision in ADR-3180 7.4 The maintainer decided #2957 on 2026-08-08: disk state is authoritative and a ROADMAP checkbox is a human annotation with no machine authority. Section 7.4 still carried the OPEN QUESTION and was marked blocked, so the contract said one thing and the tracker another. Recorded per section 7's own rule - a behavior not stated there is not decided, and amending a rule is an ADR amendment rather than a code change with a comment. The decision comment names Phase 4's PR as the carrier of this edit and makes it an acceptance criterion that the text be in the tree before implementation begins, so this lands first, alone, ahead of any code. Also clears the stale blocked-on-2957 row in the guard roster. * refactor(#3186): one shared phase-completion predicate, disk-strict isPhaseComplete in verification.cts becomes the single owner. It calls readVerificationStatus UNCONDITIONALLY - plan count is not a precondition - so a zero-plan phase with a passing VERIFICATION.md is complete. That is #3168: init gated the read on a plan count and synthesized a not_required sentinel, so phase.complete succeeded while init.manager reported incomplete for the same phase. The guard, built and run before scope was fixed per Amendment 3, found 9 re-derivations where the ADR named 3. Four were unnamed, including one in the prompt layer: mvp-phase.md ORed a ticked checkbox with disk status, which under disk-strict is the divergence itself. Per the #2957 decision, a ticked ROADMAP checkbox is a human annotation with no machine authority. The overrides in roadmap analyze and init manager are deleted rather than generalized; the user's checkbox stays in ROADMAP.md, only its authority goes. scanPhasePlans.completed and buildWorkstreamInventory are deliberately NOT folded - they answer 'are all plans summarized', which is a different question, and folding them would either over-report completion or invert the dependency direction between Phase 1's owner and this one. Verified on the remote runner. * fix(#3186): close seven review findings and record the missing-verdict rule The isolated review reproduced a write-path regression I introduced: migrating cmdRoadmapUpdatePlanProgress dropped its summaryCount>=planCount gate, so a phase with a fresh passing verification plus a newly-added unsummarized plan reported complete AND wrote a checkbox into ROADMAP.md while phase complete refused. The owner stays right per 7.4 - plan count is not a completion precondition - so the gate is restored at the write site as an explicit composition, mirroring the separate 2648 unexecuted-plan gate cmdPhaseComplete already carries. The spec axis was right that my 0.x-split reasoning was too permissive. The 2957 decision names buildStateFrontmatter as one of the three that must converge, and buildWorkstreamInventory combined a summaries-met local with verification data to decide the same verdict - Decision 4(c)'s named bypass, and it reproduced 3168 in a third surface. Both now route through the owner. The raw scanPhasePlans helper stays: it answers are-plans-summarized, which genuinely is a different question. Maintainer decision recorded in 7.4: a missing verdict is not a passing one, so an absent VERIFICATION.md means not complete everywhere. That retires 2645's verifier-disabled tolerance and inverts its Goodhart incentive - deleting the evidence now lowers completion instead of raising it. Guard hardened: block-form count gates and algebraic restatements are caught, and the header now discloses its remaining limits instead of overclaiming. Verified on the remote runner. * fix(#3186): route state sync through the owner and catch bare completed reads The matrix found 52 failures. 51 were fixtures asserting the old semantics: a phase with plans and summaries but no VERIFICATION.md used to count complete and correctly no longer does. Each fixture now carries a passing verification where that is what the test was actually about, rather than having its assertion weakened. The 52nd was a real 10th re-derivation the guard could not see. cmdStateSync destructured scanPhasePlans().completed directly - a bare field read, not a comparison - and used it as a completion verdict, so state sync and state json disagreed on completed_phases for identical disk state. Routed through the owner. Guard gains shape (d): any read of .completed off a scanPhasePlans() result outside plan-scan.cts, in chained, destructured and indirect forms, function scoped with no line window. It cannot tell a summaries-met read from a completion read - that is data flow - so it flags every one and requires a written-reason exemption, which is the same discipline shapes a-c already use. The blind spot is disclosed in the header rather than overclaimed. The emitted-attribution failure was also mine, not pre-existing: the mvp-phase.md checkbox-OR removal moves emitted bytes, acknowledged in tests/emitted-drift-acks. Verified on the remote runner. * test(#3186): give the nested-plans sync fixture a passing verification Last 3 matrix failures were one failure echoing up two describe levels. Phase 01-alpha had plans and summaries but no VERIFICATION.md, so under disk-strict completed stayed 0 and no Progress change was emitted - correct new behavior, not a regression. Added the passing verification rather than dropping the Progress expectation, so the test still covers what #3257 is about: that a nested plans/ layout is counted and not undercounted. Probe against the built lib confirms Progress: 0% -> 50% alongside Total Plans in Phase: 0 -> 3. * chore(#3186): backfill changeset PR number pr:0 placeholder replaced with the real number now that #3306 exists. --------- Co-authored-by: sim <sim@local>
628 lines
31 KiB
JavaScript
628 lines
31 KiB
JavaScript
'use strict';
|
||
process.env.GSD_TEST_MODE = '1';
|
||
|
||
// allow-test-rule: structural-regression-guard, see #3186
|
||
// D3 below reads src/plan-scan.cts / gsd-core/bin/lib/plan-scan.cjs and
|
||
// src/verification.cts and regex-tests them for require/import statements.
|
||
// This asserts a DEPENDENCY-DIRECTION invariant (the owner consumes plan
|
||
// counts, never the reverse — ADR-3180 §7.4 HARD CONSTRAINT) that has no
|
||
// behavioral/runtime surface: `require`-ing plan-scan.cjs in-process cannot
|
||
// distinguish "verification.cjs is absent from its dependency graph" from
|
||
// "verification.cjs happens to already be in require.cache because an
|
||
// earlier test in this same file required it directly" (line 36 above does
|
||
// exactly that) — only source inspection can tell which import edge exists.
|
||
// #3186 review finding 6(a).
|
||
|
||
/**
|
||
* Unit + whole-repo coverage for the PHASE-COMPLETION drift guard
|
||
* (scripts/lint-completion-predicate-drift.cjs, epic #3180, issue #3186,
|
||
* ADR-3180 §7.4, Decision 4). Modelled on tests/milestone-window-drift-guard.test.cjs
|
||
* / tests/completion-ratio-single-owner.test.cjs's guard sections: behavioral
|
||
* throughout — every assertion drives the guard's exported pure functions
|
||
* directly, never `readFileSync().includes()`.
|
||
*
|
||
* Covers 50-test-matrix.md section E (the guard) plus D3 (the dependency-
|
||
* direction guard: plan-scan.cts does not import verification.cts).
|
||
*/
|
||
|
||
const { test, describe } = require('node:test');
|
||
const assert = require('node:assert/strict');
|
||
const fs = require('node:fs');
|
||
const path = require('node:path');
|
||
|
||
const drift = require('../scripts/lint-completion-predicate-drift.cjs');
|
||
const { sanitizeForReport } = require('../scripts/lib/drift-scan.cjs');
|
||
const { createTempDir, cleanup } = require('./helpers.cjs');
|
||
|
||
const ROOT = path.join(__dirname, '..');
|
||
const OWNER_RELPATH = drift.OWNER_FILE; // path.join('src', 'verification.cts')
|
||
|
||
// ═════════════════════════════════════════════════════════════════════════
|
||
// E1 — the real repo tree, post-migration: 0 violations, earned per shape.
|
||
// ═════════════════════════════════════════════════════════════════════════
|
||
|
||
describe('E1 — scanRepo(repoRoot) against the real repo: earned zero', () => {
|
||
test('zero violations across the whole scan surface (src/ + prompt layer)', () => {
|
||
const violations = drift.scanRepo(ROOT);
|
||
assert.deepStrictEqual(
|
||
violations,
|
||
[],
|
||
'unsanctioned phase-completion re-derivation(s) — route through src/verification.cts '
|
||
+ '`isPhaseComplete` (issue #3186, ADR-3180 §7.4):\n'
|
||
+ violations.map((d) => ` ${d.file}:${d.line} [shape ${d.shape}] ${d.found}`).join('\n'),
|
||
);
|
||
});
|
||
|
||
test('per-shape proof: a deliberate (a)/(b)/(c) fixture is NOT silently swallowed by scanRepo', (t) => {
|
||
// Distinguishes "0 because nothing to find" from "0 because the detector
|
||
// is broken" — scanRepo on a synthetic tree carrying all three shapes
|
||
// must report exactly 3, proving the same code path scanRepo(ROOT) took
|
||
// is capable of finding violations at all.
|
||
const root = createTempDir('gsd-completion-predicate-drift-');
|
||
t.after(() => cleanup(root));
|
||
fs.mkdirSync(path.join(root, 'src'), { recursive: true });
|
||
fs.writeFileSync(
|
||
path.join(root, 'src', 'fake.cts'),
|
||
[
|
||
'function fakeConsumer(roadmapComplete, planCount, summaryCount) {',
|
||
" let status = 'pending';",
|
||
' if (roadmapComplete && status !== \'complete\') {',
|
||
" status = 'complete';",
|
||
' }',
|
||
' const verificationStatus = planCount > 0',
|
||
' ? readVerificationStatus(phaseDir)',
|
||
" : { status: 'not_required' };",
|
||
' const done = summaryCount >= planCount && planCount > 0;',
|
||
' return { status, verificationStatus, done };',
|
||
'}',
|
||
].join('\n'),
|
||
);
|
||
const violations = drift.scanRepo(root);
|
||
const shapes = violations.map((v) => v.shape).sort();
|
||
assert.deepStrictEqual(shapes, ['a', 'b', 'c']);
|
||
});
|
||
});
|
||
|
||
// ═════════════════════════════════════════════════════════════════════════
|
||
// E2 — fixture reintroducing shapes (a), (b), (c): each flagged, one hit
|
||
// apiece.
|
||
// ═════════════════════════════════════════════════════════════════════════
|
||
|
||
describe('E2 — each shape flagged in isolation, exactly one hit apiece', () => {
|
||
test('shape (a): checkbox-derived completion override', () => {
|
||
const text = [
|
||
'function fakeConsumer(roadmapComplete) {',
|
||
" let diskStatus = 'planned';",
|
||
' if (roadmapComplete && diskStatus !== \'complete\') {',
|
||
" diskStatus = 'complete';",
|
||
' }',
|
||
' return diskStatus;',
|
||
'}',
|
||
].join('\n');
|
||
const out = drift.findCompletionPredicateDrift(text, path.join('src', 'unrelated.cts'));
|
||
assert.strictEqual(out.length, 1);
|
||
assert.strictEqual(out[0].shape, 'a');
|
||
});
|
||
|
||
test('shape (b): plan-count precondition gating a verification read', () => {
|
||
const text = [
|
||
'function fakeConsumer(planCount, phaseDir) {',
|
||
' return planCount > 0',
|
||
' ? readVerificationStatus(phaseDir)',
|
||
" : { status: 'not_required' };",
|
||
'}',
|
||
].join('\n');
|
||
const out = drift.findCompletionPredicateDrift(text, path.join('src', 'unrelated.cts'));
|
||
assert.strictEqual(out.length, 1);
|
||
assert.strictEqual(out[0].shape, 'b');
|
||
});
|
||
|
||
test('shape (c): local re-implementation of "complete" from counts', () => {
|
||
const text = [
|
||
'function fakeConsumer(summaryCount, planCount) {',
|
||
' return summaryCount >= planCount && planCount > 0;',
|
||
'}',
|
||
].join('\n');
|
||
const out = drift.findCompletionPredicateDrift(text, path.join('src', 'unrelated.cts'));
|
||
assert.strictEqual(out.length, 1);
|
||
assert.strictEqual(out[0].shape, 'c');
|
||
});
|
||
|
||
test('shape (c): the reversed operand order (planCount <= summaryCount) is also flagged', () => {
|
||
const text = [
|
||
'function fakeConsumer(summaryCount, planCount) {',
|
||
' return planCount <= summaryCount;',
|
||
'}',
|
||
].join('\n');
|
||
const out = drift.findCompletionPredicateDrift(text, path.join('src', 'unrelated.cts'));
|
||
assert.strictEqual(out.length, 1);
|
||
assert.strictEqual(out[0].shape, 'c');
|
||
});
|
||
});
|
||
|
||
// ═════════════════════════════════════════════════════════════════════════
|
||
// E3 — shape (a) with a nested-paren if-condition: flagged. (The first draft
|
||
// missed exactly this on cmdInitManager's `(completion.phase_complete ||
|
||
// planCount === 0)` nested group.)
|
||
// ═════════════════════════════════════════════════════════════════════════
|
||
|
||
describe('E3 — shape (a): nested-paren if-condition', () => {
|
||
test('a second parenthesised group inside the if-condition is still detected', () => {
|
||
const text = [
|
||
'function fakeConsumer(roadmapComplete, completion, planCount) {',
|
||
" let diskStatus = 'planned';",
|
||
' if (roadmapComplete && (completion.phase_complete || planCount === 0) && diskStatus !== \'complete\') {',
|
||
" diskStatus = 'complete';",
|
||
' }',
|
||
' return diskStatus;',
|
||
'}',
|
||
].join('\n');
|
||
const out = drift.findCompletionPredicateDrift(text, path.join('src', 'unrelated.cts'));
|
||
assert.strictEqual(out.length, 1);
|
||
assert.strictEqual(out[0].shape, 'a');
|
||
});
|
||
});
|
||
|
||
// ═════════════════════════════════════════════════════════════════════════
|
||
// E4 — an unconditional readVerificationStatus( call (the cmdPhaseComplete
|
||
// shape): NOT flagged.
|
||
// ═════════════════════════════════════════════════════════════════════════
|
||
|
||
describe('E4 — an unconditional readVerificationStatus( call is not flagged', () => {
|
||
test('no ternary gate on the call, no count-gate in the function: clean', () => {
|
||
const text = [
|
||
'function fakeConsumer(phaseDir) {',
|
||
' const verificationStatus = readVerificationStatus(phaseDir, { runtime: \'claude\' });',
|
||
' return verificationStatus;',
|
||
'}',
|
||
].join('\n');
|
||
const out = drift.findCompletionPredicateDrift(text, path.join('src', 'unrelated.cts'));
|
||
assert.deepStrictEqual(out, []);
|
||
});
|
||
|
||
test('a count-gate present elsewhere in the SAME function but the read is unconditional: still clean', () => {
|
||
// Shape (b) requires the readVerificationStatus( call ITSELF to be
|
||
// ternary-gated on the same line — a count-gate merely coexisting with
|
||
// an unconditional call must not fire.
|
||
const text = [
|
||
'function fakeConsumer(phaseDir, retryCount) {',
|
||
' if (retryCount > 0) { /* retry bookkeeping, unrelated */ }',
|
||
' const verificationStatus = readVerificationStatus(phaseDir);',
|
||
' return verificationStatus;',
|
||
'}',
|
||
].join('\n');
|
||
const out = drift.findCompletionPredicateDrift(text, path.join('src', 'unrelated.cts'));
|
||
assert.deepStrictEqual(out, []);
|
||
});
|
||
});
|
||
|
||
// ═════════════════════════════════════════════════════════════════════════
|
||
// E5 — all three shapes inside `//` and `/* */` comments: NOT flagged.
|
||
// ═════════════════════════════════════════════════════════════════════════
|
||
|
||
describe('E5 — commented-out shapes are not flagged', () => {
|
||
test('shape (a) inside a // line comment', () => {
|
||
const text = [
|
||
"// if (roadmapComplete && diskStatus !== 'complete') diskStatus = 'complete';",
|
||
].join('\n');
|
||
assert.deepStrictEqual(drift.findCompletionPredicateDrift(text, path.join('src', 'unrelated.cts')), []);
|
||
});
|
||
|
||
test('shape (b) inside a /* */ block comment', () => {
|
||
const text = [
|
||
'/*',
|
||
' const v = planCount > 0 ? readVerificationStatus(phaseDir) : { status: "not_required" };',
|
||
'*/',
|
||
].join('\n');
|
||
assert.deepStrictEqual(drift.findCompletionPredicateDrift(text, path.join('src', 'unrelated.cts')), []);
|
||
});
|
||
|
||
test('shape (c) inside a JSDoc continuation comment', () => {
|
||
const text = [
|
||
'/**',
|
||
' * e.g. `summaryCount >= planCount && planCount > 0` is the old shape.',
|
||
' */',
|
||
].join('\n');
|
||
assert.deepStrictEqual(drift.findCompletionPredicateDrift(text, path.join('src', 'unrelated.cts')), []);
|
||
});
|
||
});
|
||
|
||
// ═════════════════════════════════════════════════════════════════════════
|
||
// E6 — retryCount > 0 and other unrelated count gates: NOT flagged.
|
||
// ═════════════════════════════════════════════════════════════════════════
|
||
|
||
describe('E6 — unrelated count gates alone are not flagged', () => {
|
||
test('retryCount > 0 with no readVerificationStatus( call anywhere in the function', () => {
|
||
const text = [
|
||
'function fakeRetry(retryCount) {',
|
||
' if (retryCount > 0) return doRetry();',
|
||
' return doOnce();',
|
||
'}',
|
||
].join('\n');
|
||
assert.deepStrictEqual(drift.findCompletionPredicateDrift(text, path.join('src', 'unrelated.cts')), []);
|
||
});
|
||
|
||
test('itemCount > 0 gating an unrelated ternary (no verification call at all)', () => {
|
||
const text = [
|
||
'function fakeList(itemCount) {',
|
||
" return itemCount > 0 ? 'has items' : 'empty';",
|
||
'}',
|
||
].join('\n');
|
||
assert.deepStrictEqual(drift.findCompletionPredicateDrift(text, path.join('src', 'unrelated.cts')), []);
|
||
});
|
||
});
|
||
|
||
// ═════════════════════════════════════════════════════════════════════════
|
||
// E7 — owner file: `isPhaseComplete` exempt BY FUNCTION NAME, never whole-file.
|
||
// ═════════════════════════════════════════════════════════════════════════
|
||
|
||
describe('E7 — owner-file exemption is function-scoped, not file-scoped', () => {
|
||
test('the canonical isPhaseComplete body is exempt in the owner file', () => {
|
||
const text = [
|
||
'function isPhaseComplete(phaseDir, deps) {',
|
||
' const verification = readVerificationStatus(phaseDir, deps);',
|
||
" return { value: { complete: verification.status === 'passed', verification }, scope: SCOPE.COMPLETE };",
|
||
'}',
|
||
].join('\n');
|
||
assert.deepStrictEqual(drift.findCompletionPredicateDrift(text, OWNER_RELPATH), []);
|
||
});
|
||
|
||
test('a differently-named function in the SAME owner file carrying shape (c) IS flagged', () => {
|
||
const text = [
|
||
'function isPhaseComplete(phaseDir) { return true; }',
|
||
'',
|
||
'function someOtherHelper(summaryCount, planCount) {',
|
||
' return summaryCount >= planCount && planCount > 0;',
|
||
'}',
|
||
].join('\n');
|
||
const out = drift.findCompletionPredicateDrift(text, OWNER_RELPATH);
|
||
assert.strictEqual(out.length, 1);
|
||
assert.strictEqual(out[0].fn, 'someOtherHelper');
|
||
});
|
||
});
|
||
|
||
// ═════════════════════════════════════════════════════════════════════════
|
||
// E8 — a SECOND, unrelated predicate added elsewhere IN THE OWNER FILE:
|
||
// flagged (the Amendment-4 blind spot — a whole-file exemption on the owner
|
||
// is precisely how a prior guard's owner grew an invisible second copy).
|
||
// ═════════════════════════════════════════════════════════════════════════
|
||
|
||
describe('E8 — a second predicate elsewhere in the owner file is flagged (Amendment-4 blind spot)', () => {
|
||
test('shape (a) added to a non-canonical function in src/verification.cts is caught', () => {
|
||
const text = [
|
||
'function isPhaseComplete(phaseDir) { return true; }',
|
||
'',
|
||
'function cmdSomeNewVerb(roadmapComplete) {',
|
||
" let status = 'pending';",
|
||
' if (roadmapComplete && status !== \'complete\') {',
|
||
" status = 'complete';",
|
||
' }',
|
||
' return status;',
|
||
'}',
|
||
].join('\n');
|
||
const out = drift.findCompletionPredicateDrift(text, OWNER_RELPATH);
|
||
assert.strictEqual(out.length, 1);
|
||
assert.strictEqual(out[0].shape, 'a');
|
||
assert.strictEqual(out[0].fn, 'cmdSomeNewVerb');
|
||
});
|
||
});
|
||
|
||
// ═════════════════════════════════════════════════════════════════════════
|
||
// E9 — prompt-layer fixture reintroducing the checkbox OR: flagged (the
|
||
// scan surface really covers gsd-core/workflows, not just src/).
|
||
// ═════════════════════════════════════════════════════════════════════════
|
||
|
||
describe('E9 — prompt-layer checkbox-OR re-derivation is flagged', () => {
|
||
test('findPromptCompletionDrift flags the two-line assign/test pairing', () => {
|
||
const lines = [
|
||
'PHASE_COMPLETE=$(echo "$PHASE_INFO" | jq -r \'.roadmap_complete // false\')',
|
||
'DISK_STATUS=$(echo "$ANALYZE" | jq -r \'.disk_status\')',
|
||
'if [[ "$DISK_STATUS" == "complete" || "$PHASE_COMPLETE" == "true" ]]; then',
|
||
' STATUS="completed"',
|
||
'fi',
|
||
].join('\n');
|
||
const out = drift.findPromptCompletionDrift(lines, path.join('gsd-core', 'workflows', 'fake.md'));
|
||
assert.strictEqual(out.length, 1);
|
||
assert.strictEqual(out[0].shape, 'a');
|
||
});
|
||
|
||
test('scanRepo finds the same fixture when it is a real .md file under gsd-core/workflows', (t) => {
|
||
const root = createTempDir('gsd-completion-predicate-drift-');
|
||
t.after(() => cleanup(root));
|
||
fs.mkdirSync(path.join(root, 'gsd-core', 'workflows'), { recursive: true });
|
||
fs.writeFileSync(
|
||
path.join(root, 'gsd-core', 'workflows', 'fake.md'),
|
||
[
|
||
'PHASE_COMPLETE=$(echo "$PHASE_INFO" | jq -r \'.roadmap_complete // false\')',
|
||
'if [[ "$DISK_STATUS" == "complete" || "$PHASE_COMPLETE" == "true" ]]; then',
|
||
' STATUS="completed"',
|
||
'fi',
|
||
].join('\n'),
|
||
);
|
||
const violations = drift.scanRepo(root);
|
||
assert.strictEqual(violations.length, 1);
|
||
assert.strictEqual(violations[0].shape, 'a');
|
||
});
|
||
|
||
test('a DISK_STATUS-only check (no checkbox OR) is not flagged', () => {
|
||
const lines = [
|
||
'if [[ "$DISK_STATUS" == "complete" ]]; then',
|
||
' STATUS="completed"',
|
||
'fi',
|
||
].join('\n');
|
||
assert.deepStrictEqual(drift.findPromptCompletionDrift(lines, path.join('gsd-core', 'workflows', 'fake.md')), []);
|
||
});
|
||
});
|
||
|
||
// ═════════════════════════════════════════════════════════════════════════
|
||
// E10 — filename with control bytes / bidi: sanitized in report output.
|
||
// ═════════════════════════════════════════════════════════════════════════
|
||
|
||
describe('E10 — sanitizeForReport neutralizes control bytes and bidi overrides', () => {
|
||
test('a C0 control byte in a violation "found" fragment is escaped, not passed through raw', () => {
|
||
const raw = 'diskStatus = \x1b[31m\'complete\'\x1b[0m;';
|
||
const sanitized = sanitizeForReport(raw);
|
||
assert.ok(!sanitized.includes('\x1b'), 'raw ESC byte must not survive sanitization');
|
||
assert.ok(sanitized.includes('\\x1b'), 'ESC byte must be rendered as a visible \\xNN escape');
|
||
});
|
||
|
||
test('a bidi right-to-left override codepoint in a reported file path is escaped', () => {
|
||
const raw = 'src/evil.cts';
|
||
const sanitized = sanitizeForReport(raw);
|
||
assert.ok(!sanitized.includes(''), 'raw RLO codepoint must not survive sanitization');
|
||
assert.ok(sanitized.includes('\\u202e'), 'RLO codepoint must be rendered as a visible \\uNNNN escape');
|
||
});
|
||
});
|
||
|
||
// ═════════════════════════════════════════════════════════════════════════
|
||
// E11 — #3186 review finding 4: two evasion shapes the pre-review guard
|
||
// produced ZERO hits on, now caught.
|
||
// ═════════════════════════════════════════════════════════════════════════
|
||
|
||
describe('E11 — finding 4(a): the BLOCK form of shape (b) is now caught', () => {
|
||
test('if (planCount > 0) { … readVerificationStatus(…) … } is flagged (was zero hits before)', () => {
|
||
const text = [
|
||
'function fakeConsumer(planCount, phaseDir) {',
|
||
' let verificationStatus = { status: \'not_required\' };',
|
||
' if (planCount > 0) {',
|
||
' verificationStatus = readVerificationStatus(phaseDir);',
|
||
' }',
|
||
' return verificationStatus;',
|
||
'}',
|
||
].join('\n');
|
||
const out = drift.findCompletionPredicateDrift(text, path.join('src', 'unrelated.cts'));
|
||
assert.strictEqual(out.length, 1);
|
||
assert.strictEqual(out[0].shape, 'b');
|
||
});
|
||
|
||
test('an unrelated if-block (no count-gate condition) wrapping an unconditional call stays clean', () => {
|
||
// A generic nested-brace check (not "is this specifically an if-block
|
||
// whose OWN condition is a count-gate") would false-positive here.
|
||
const text = [
|
||
'function fakeConsumer(phaseDir, flag) {',
|
||
' let verificationStatus = null;',
|
||
' if (flag) {',
|
||
' verificationStatus = readVerificationStatus(phaseDir);',
|
||
' }',
|
||
' return verificationStatus;',
|
||
'}',
|
||
].join('\n');
|
||
assert.deepStrictEqual(drift.findCompletionPredicateDrift(text, path.join('src', 'unrelated.cts')), []);
|
||
});
|
||
|
||
test('a non-conditional wrapper (a callback passed to another function) is NOT treated as gating', () => {
|
||
// Regression guard for the naive "any brace nesting deeper than the
|
||
// function's own top level = gated" approach, which false-positived on
|
||
// cmdPhaseComplete's real `withPlanningLock(cwd, () => { … })` shape:
|
||
// an UNCONDITIONAL readVerificationStatus( call wrapped only in a
|
||
// callback, with an unrelated count-gate elsewhere in the function.
|
||
const text = [
|
||
'function fakeConsumer(phaseDir, retryCount) {',
|
||
' if (retryCount > 0) { /* unrelated */ }',
|
||
' return withPlanningLock(phaseDir, () => {',
|
||
' return readVerificationStatus(phaseDir);',
|
||
' });',
|
||
'}',
|
||
].join('\n');
|
||
assert.deepStrictEqual(drift.findCompletionPredicateDrift(text, path.join('src', 'unrelated.cts')), []);
|
||
});
|
||
});
|
||
|
||
describe('E11 — finding 4(b): the algebraic-restatement evasion of shape (c) is now caught', () => {
|
||
test('summaryCount - planCount >= 0 is flagged (was zero hits before)', () => {
|
||
const text = [
|
||
'function fakeConsumer(summaryCount, planCount) {',
|
||
' return summaryCount - planCount >= 0;',
|
||
'}',
|
||
].join('\n');
|
||
const out = drift.findCompletionPredicateDrift(text, path.join('src', 'unrelated.cts'));
|
||
assert.strictEqual(out.length, 1);
|
||
assert.strictEqual(out[0].shape, 'c');
|
||
});
|
||
|
||
test('the mirrored form planCount - summaryCount <= 0 is also flagged', () => {
|
||
const text = [
|
||
'function fakeConsumer(summaryCount, planCount) {',
|
||
' return planCount - summaryCount <= 0;',
|
||
'}',
|
||
].join('\n');
|
||
const out = drift.findCompletionPredicateDrift(text, path.join('src', 'unrelated.cts'));
|
||
assert.strictEqual(out.length, 1);
|
||
assert.strictEqual(out[0].shape, 'c');
|
||
});
|
||
|
||
test('an unrelated count-difference comparison (not summary/plan) stays clean', () => {
|
||
const text = [
|
||
'function fakeConsumer(retryCount, maxCount) {',
|
||
' return retryCount - maxCount >= 0;',
|
||
'}',
|
||
].join('\n');
|
||
assert.deepStrictEqual(drift.findCompletionPredicateDrift(text, path.join('src', 'unrelated.cts')), []);
|
||
});
|
||
});
|
||
|
||
// ═════════════════════════════════════════════════════════════════════════
|
||
// E12 — shape (d): a bare `.completed` read off a `scanPhasePlans(` result,
|
||
// used as a completion verdict outside the owner (src/plan-scan.cts). The
|
||
// #3186 remote-matrix finding: cmdStateSync (src/state.cts, now fixed)
|
||
// destructured `scanPhasePlans(dirPath).completed` directly with no
|
||
// comparison for shapes (a)/(b)/(c) to catch.
|
||
// ═════════════════════════════════════════════════════════════════════════
|
||
|
||
describe('E12 — shape (d): scanPhasePlans(...).completed read as a completion verdict', () => {
|
||
test('direct chained form: scanPhasePlans(dir).completed is flagged', () => {
|
||
const text = [
|
||
'function cmdSomeVerb(dirPath) {',
|
||
' return scanPhasePlans(dirPath).completed;',
|
||
'}',
|
||
].join('\n');
|
||
const out = drift.findCompletionPredicateDrift(text, path.join('src', 'unrelated.cts'));
|
||
assert.strictEqual(out.length, 1);
|
||
assert.strictEqual(out[0].shape, 'd');
|
||
assert.strictEqual(out[0].fn, 'cmdSomeVerb');
|
||
});
|
||
|
||
test('direct chained form with a nested-paren call argument is still flagged (path.join(...) inside the call)', () => {
|
||
// A naive `[^)]*` regex would stop at path.join(...)'s OWN closing paren
|
||
// and miss the `.completed` that follows the call's TRUE closing paren —
|
||
// the real shape most scanPhasePlans( call sites in this tree use.
|
||
const text = [
|
||
'function cmdSomeVerb(phasesDir, dir) {',
|
||
' return scanPhasePlans(path.join(phasesDir, dir)).completed;',
|
||
'}',
|
||
].join('\n');
|
||
const out = drift.findCompletionPredicateDrift(text, path.join('src', 'unrelated.cts'));
|
||
assert.strictEqual(out.length, 1);
|
||
assert.strictEqual(out[0].shape, 'd');
|
||
});
|
||
|
||
test('destructured form: const { completed } = scanPhasePlans(dirPath) is flagged (the exact #3186 cmdStateSync shape)', () => {
|
||
const text = [
|
||
'function cmdStateSyncLike(dirPath) {',
|
||
' const { completed } = scanPhasePlans(dirPath);',
|
||
' return completed;',
|
||
'}',
|
||
].join('\n');
|
||
const out = drift.findCompletionPredicateDrift(text, path.join('src', 'unrelated.cts'));
|
||
assert.strictEqual(out.length, 1);
|
||
assert.strictEqual(out[0].shape, 'd');
|
||
assert.strictEqual(out[0].fn, 'cmdStateSyncLike');
|
||
});
|
||
|
||
test('destructured renamed-alias form: const { completed: isDone } = scanPhasePlans(dirPath) is flagged', () => {
|
||
const text = [
|
||
'function cmdSomeVerb(dirPath) {',
|
||
' const { completed: isDone } = scanPhasePlans(dirPath);',
|
||
' return isDone;',
|
||
'}',
|
||
].join('\n');
|
||
const out = drift.findCompletionPredicateDrift(text, path.join('src', 'unrelated.cts'));
|
||
assert.strictEqual(out.length, 1);
|
||
assert.strictEqual(out[0].shape, 'd');
|
||
});
|
||
|
||
test('indirect form: the call and the .completed read sit on DIFFERENT lines in the SAME function — flagged, proving no line window', () => {
|
||
const text = [
|
||
'function cmdSomeVerb(dirPath) {',
|
||
' const scan = scanPhasePlans(dirPath);',
|
||
' const summaryCount = scan.summaryFiles.length;',
|
||
' const planCount = scan.planFiles.length;',
|
||
' // several unrelated lines of bookkeeping in between',
|
||
' const x = summaryCount + planCount;',
|
||
' const y = x * 2;',
|
||
' return scan.completed;',
|
||
'}',
|
||
].join('\n');
|
||
const out = drift.findCompletionPredicateDrift(text, path.join('src', 'unrelated.cts'));
|
||
assert.strictEqual(out.length, 1);
|
||
assert.strictEqual(out[0].shape, 'd');
|
||
assert.strictEqual(out[0].line, 8);
|
||
});
|
||
|
||
test('a `.completed` read inside src/plan-scan.cts itself (the owner, exempt function) is NOT flagged', () => {
|
||
const text = [
|
||
'function scanPhasePlans(phaseDir) {',
|
||
' const inner = scanPhasePlans(phaseDir);',
|
||
' return { completed: inner.completed, extra: true };',
|
||
'}',
|
||
].join('\n');
|
||
assert.deepStrictEqual(drift.findCompletionPredicateDrift(text, path.join('src', 'plan-scan.cts')), []);
|
||
});
|
||
|
||
test('an exempted function is not flagged, but a DIFFERENT function in the SAME file still is (function-scoped, never whole-file)', () => {
|
||
// OWNER_RELPATH (src/verification.cts) has `isPhaseComplete` exempt in
|
||
// FUNCTION_SCOPED_EXEMPTIONS — reused here to prove shape (d) shares that
|
||
// same per-function map rather than a whole-file allowlist.
|
||
const text = [
|
||
'function isPhaseComplete(phaseDir) {',
|
||
' const scan = scanPhasePlans(phaseDir);',
|
||
' return scan.completed;',
|
||
'}',
|
||
'',
|
||
'function cmdSomeNewVerb(phaseDir) {',
|
||
' return scanPhasePlans(phaseDir).completed;',
|
||
'}',
|
||
].join('\n');
|
||
const out = drift.findCompletionPredicateDrift(text, OWNER_RELPATH);
|
||
assert.strictEqual(out.length, 1);
|
||
assert.strictEqual(out[0].shape, 'd');
|
||
assert.strictEqual(out[0].fn, 'cmdSomeNewVerb');
|
||
});
|
||
|
||
test('an unrelated .completed property on a non-scanPhasePlans object is NOT flagged', () => {
|
||
const text = [
|
||
'function cmdSomeVerb(job) {',
|
||
' const result = someOtherFunction(job);',
|
||
' return result.completed;',
|
||
'}',
|
||
].join('\n');
|
||
assert.deepStrictEqual(drift.findCompletionPredicateDrift(text, path.join('src', 'unrelated.cts')), []);
|
||
});
|
||
|
||
test('scanRepo against the real repo tree is capable of finding a deliberate shape (d) fixture (not silently swallowed)', (t) => {
|
||
const root = createTempDir('gsd-completion-predicate-drift-');
|
||
t.after(() => cleanup(root));
|
||
fs.mkdirSync(path.join(root, 'src'), { recursive: true });
|
||
fs.writeFileSync(
|
||
path.join(root, 'src', 'fake-shape-d.cts'),
|
||
[
|
||
'function cmdSomeVerb(dirPath) {',
|
||
' const { completed } = scanPhasePlans(dirPath);',
|
||
' return completed;',
|
||
'}',
|
||
].join('\n'),
|
||
);
|
||
const violations = drift.scanRepo(root);
|
||
assert.strictEqual(violations.length, 1);
|
||
assert.strictEqual(violations[0].shape, 'd');
|
||
});
|
||
});
|
||
|
||
// ═════════════════════════════════════════════════════════════════════════
|
||
// D3 — the 0.x-split over-consolidation guard: plan-scan.cts does NOT
|
||
// import verification.cts (the owner consumes plan counts, never the
|
||
// reverse — ADR-3180 §7.4 HARD CONSTRAINT).
|
||
// ═════════════════════════════════════════════════════════════════════════
|
||
|
||
describe('D3 — dependency direction: plan-scan.cts does not import verification.cts', () => {
|
||
test('the compiled plan-scan.cjs source contains no reference to verification.cjs', () => {
|
||
const compiled = fs.readFileSync(path.join(ROOT, 'gsd-core', 'bin', 'lib', 'plan-scan.cjs'), 'utf-8');
|
||
assert.ok(!/require\(['"]\.\/verification(\.cjs)?['"]\)/.test(compiled), 'plan-scan.cjs must not require verification.cjs');
|
||
});
|
||
|
||
test('the plan-scan.cts source has no import/require of verification.cjs/.cts (a prose comment MENTIONING it, e.g. explaining why a field is not routed through it, is not an import and must not false-positive)', () => {
|
||
const source = fs.readFileSync(path.join(ROOT, 'src', 'plan-scan.cts'), 'utf-8');
|
||
assert.ok(
|
||
!/\b(?:require|import)\s*(?:\(|\{|[A-Za-z_$][\w$]*\s*=)[^;\r\n]*verification\.c(?:j|t)s/.test(source),
|
||
'src/plan-scan.cts must not import/require verification.cts/.cjs',
|
||
);
|
||
});
|
||
|
||
test('src/verification.cts DOES import plan-scan.cjs (the direction that is allowed: owner consumes counts)', () => {
|
||
// Documents the ALLOWED direction so the pair of assertions above reads
|
||
// as a genuine one-way constraint, not an accidental total decoupling.
|
||
const source = fs.readFileSync(path.join(ROOT, 'src', 'verification.cts'), 'utf-8');
|
||
assert.ok(/require\(['"]\.\/plan-scan\.cjs['"]\)/.test(source), 'src/verification.cts is expected to import plan-scan.cjs (for staleness-check summary listing, pre-existing/unrelated to isPhaseComplete)');
|
||
});
|
||
});
|