Phase 4 of #3464, following #3465, #3466 and #3502. Those cut the ceiling 305 -> 278 and made the rule accurate. This closes the remaining structural weakness: suppression was FILE-WIDE, so a single justified exemption silently absolved every other source-grep in that file, forever, including ones added later by someone else. hasAllowAnnotation did comments.some(...) over the whole file and returned {} early. A marker is now checked per report: a violation is suppressed only by a marker on its own line, or on a line above it with nothing but blank lines and other comments in between, bounded by MAX_MARKER_LOOKAHEAD_LINES = 8. The bound is comment-purity rather than raw distance, and that distinction is load-bearing: an intervening line of real code (a `test(...)` opener, say) ends the window even when the marker is physically close. Chosen from the actual placements in the affected files rather than picked a priori -- the repo's convention puts several lines of prose rationale between the marker and the code, so a tighter rule would have invalidated legitimate existing markers and forced churn for no correctness gain. Measured before writing any code, by running the real rule with the suppression check neutralized across all 1194 files its globs match: 14 violation sites in 8 files, and ZERO in files carrying no marker -- so the green build was legitimate, and the entire migration surface was those 14. 11 sites were mechanical: an existing marker already stated the right reason, it just sat too far away. Those were relocated to their call sites with the original #NNN citations preserved. Three were orphans -- the file's markers were about an entirely different concern and nobody had ever justified these reads. All three are fixed BEHAVIORALLY, with no new markers: install-minimal-hooks.test.cjs:975 asserted src.includes('gsd-update-check') && src.includes('replace(') against bin/install.js. It now calls the exported stripStaleGsdHookBlocks() on a legacy TOML fixture and asserts the actual stripped output. This is the case this phase was opened around: it could be added with no review friction and stay invisible indefinitely under file-wide amnesty. config.test.cjs:1917 regex-tested src/init.cts for detectGitCreateTag. It now drives `init complete-milestone` and asserts the git_create_tag field. config-schema.property.test.cjs:1107 did the same for detectFallowConfig; it now drives `init code-review` and asserts fallow_enabled. Each was proven RED against a broken production file and GREEN against the real one, with src/init.cts and bin/install.js confirmed byte-identical afterwards. Marker lines in the 8 files went 20 -> 24, against a filed expectation of "must not increase" (projected 14). That projection was wrong and is corrected on #3508 rather than met by deletion. It assumed every existing marker was a distant blanket that site-scoping would consolidate. Some are already site-adjacent and guard real source-greps the rule CANNOT detect -- verified in install-minimal-hooks.test.cjs:2686-2757, where seven markers each sit directly above a readFileSync(reloadScript) + .includes() pair reading hooks/gsd-config-reload.js. Removing them to hit a number would have repeated the Phase 1 mistake: deleting markers on "the rule doesn't fire" evidence when the rule provably cannot see the violation. An earlier revision of this commit message attributed that invisibility to the #3502 dynamic-path blind spot, on the grounds that reloadScript is a variable. Adversarial review caught that as a false causal claim and it is corrected here. looksLikeSourcePath's hasSourceDir regex is /['"](?:bin|lib|gsd-core|src)['"]/i, and those reads target hooks/ -- so a fully literal path.join(ROOT,'hooks','gsd-config-reload.js') is equally invisible. The variable indirection is irrelevant. This is a FIFTH, distinct blind spot: the source-dir allowlist omits hooks/, which is a real shipped production directory (eslint.config.mjs registers its own rule block for hooks/**/*.js). Recorded in 40-design.md Known limits and left for a follow-on phase -- widening the allowlist is unmeasured, and measuring before widening is the discipline #3502 established. The conclusion was right; the stated mechanism was not, and asserting an unverified cause is the error being corrected. The honest metric is not fewer markers. It is that every marker now sits adjacent to the specific read it justifies instead of absolving a whole file. Site-scoping turns one blanket marker covering N sites into N site markers by design; the count rising is the mechanism working. A second review finding is fixed here too. Suppression originally keyed only off the text-search line, so a marker placed directly above the readFileSync() call -- the intuitive place to annotate "this read is fine" -- did NOT suppress when the search sat on the following line, because the read's own assignment line breaks comment-purity. It failed safe (a loud error, never silent suppression), but it was a trap contributors would hit, and it contradicted this change's own claim that the placement rule would not force churn. A violation is now suppressed by a marker adjacent to EITHER the search site or the originating read. The violation is fundamentally the read+search pair, so annotating either half is legitimate, and it stays strictly site-scoped -- the decisive isolation row still holds. 17 RuleTester rows cover the new semantics. The decisive one asserts that a marker adjacent to one violation does NOT suppress an unrelated violation elsewhere in the same file -- exactly 1 error, reported at the second site. Teeth-checked by reverting the predicate to file-wide, confirming that row and two others flip pass->fail, then restoring. Two pre-existing RuleTester cases that asserted the old file-wide semantics were corrected. Compatibility held where it matters: 277 marker-bearing files have no detectable violation at all, and site-scoping makes their markers no-ops rather than errors. All stay green, untouched. Ceiling unchanged at 278; lint-allow-test-rule-refs reports 278/278. Deliberately not done here, and recorded for the follow-on phase: the same measurement found only 8 of 285 marker-bearing files contain a detectable violation. That suggests a large honest ceiling drop, but "the rule doesn't fire" is the unsound oracle that forced the Phase 1 revert of 295 files, and the rule still has documented blind spots -- as install-minimal-hooks itself demonstrates above. It needs two independent signals agreeing, which is only credible now that the rule is accurate. With suppression site-scoped, "an effective exemption" is finally well-defined, which is what makes re-pointing the ratchet at effective exemptions -- rather than at marker-text presence -- the natural next step. Closes #3508 Co-authored-by: sim <sim@local> Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
630 lines
32 KiB
JavaScript
630 lines
32 KiB
JavaScript
'use strict';
|
||
process.env.GSD_TEST_MODE = '1';
|
||
|
||
// 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');
|
||
// allow-test-rule: structural-regression-guard — D3 reads compiled plan-scan.cjs and regex-tests it for a require() of verification.cjs; asserts a dependency-direction invariant (ADR-3180 §7.4) with no runtime/behavioral surface (see #3186)
|
||
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(
|
||
// allow-test-rule: structural-regression-guard — same D3 dependency-direction invariant as above, checked against the .cts source instead of the compiled .cjs (see #3186)
|
||
!/\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');
|
||
// allow-test-rule: structural-regression-guard — documents the ALLOWED direction of the D3 dependency invariant: verification.cts consumes plan-scan.cjs (see #3186)
|
||
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)');
|
||
});
|
||
});
|