enhance(verify-phase): node-test causation control — prove the RED is content-caused (#1346)

The #1279 node-test machine-proof confirmed a known-bad subject drives the
negative test RED, but could not distinguish a genuine content-violation from a
deceptive test that reds merely because GSD_PROHIB_SUBJECT is set. Add an
optional fifth flat scalar `check_clean_fixture` (-> CheckDescriptor.cleanFixture)
threading a KNOWN-CLEAN control subject through projectProhibitions +
descriptorFromProjection. When present, the prover also runs the check against
the clean subject and requires GREEN, so fail-first is proven only when the check
is RED on the violation AND GREEN on the clean subject (content-dependent).

Opt-in and additive: absent a clean fixture the prover behaves exactly as
post-#1314 (no control, documented residual), preserving the zero-authoring
compose path; the lint-rule kind needs no analog (its subject IS the linted
file, no env indirection). Coverage: RED-first deceptive case, positive,
missing-clean fail-closed, round-trip read-back/emit, fast-check property
extended to the 5th scalar, and an end-to-end COMPOSE capstone (honest vs
deceptive). Docs: ADR-550 dated addendum, prohibition-probe reference,
spec-phase + verify-phase workflows.

Closes #1346

Claude-Session: https://claude.ai/code/session_01GsPRb8zvpcT7Eat6vZw8PX
This commit is contained in:
Dave
2026-06-21 10:44:48 -04:00
parent eb81faaeab
commit c09c13f295
11 changed files with 323 additions and 50 deletions

View File

@@ -194,6 +194,7 @@ function renderProhibitionsDoc(entries) {
if (e.check_target !== undefined) lines.push(` check_target: ${e.check_target}`);
if (e.check_rule !== undefined) lines.push(` check_rule: ${e.check_rule}`);
if (e.check_violation_fixture !== undefined) lines.push(` check_violation_fixture: ${e.check_violation_fixture}`);
if (e.check_clean_fixture !== undefined) lines.push(` check_clean_fixture: ${e.check_clean_fixture}`);
}
lines.push('---', '', 'Body.', '');
return lines.join('\n');
@@ -217,20 +218,22 @@ const pathScalarArb = fc.array(fc.constantFrom(...PATH_CHARS), { minLength: 1, m
const numericScalarArb = fc.nat({ max: 9999999 }).map(String);
const targetArb = fc.oneof(pathScalarArb, numericScalarArb);
// A fully well-formed descriptor item (resolved test-tier); node-test carries no rule. The
// violation fixture (#1346) rides BOTH kinds and exercises the numeric-coercion path too.
// A fully well-formed descriptor item (resolved test-tier); node-test carries no rule. The violation
// fixture and the clean control fixture (#1346) both ride BOTH kinds and exercise numeric coercion too.
const wellFormedArb = KIND_ARB.chain((kind) =>
fc.record({ target: targetArb, rule: pathScalarArb, fixture: targetArb }).map(({ target, rule, fixture }) => {
const item = { ...BASE_TIER, check_kind: kind, check_target: target, check_violation_fixture: fixture };
if (kind === 'lint-rule') item.check_rule = rule;
return { item, kind, target, rule: kind === 'lint-rule' ? rule : undefined, fixture };
}),
fc.record({ target: targetArb, rule: pathScalarArb, fixture: targetArb, clean: targetArb })
.map(({ target, rule, fixture, clean }) => {
const item = { ...BASE_TIER, check_kind: kind, check_target: target,
check_violation_fixture: fixture, check_clean_fixture: clean };
if (kind === 'lint-rule') item.check_rule = rule;
return { item, kind, target, rule: kind === 'lint-rule' ? rule : undefined, fixture, clean };
}),
);
describe('probe-core property: #1278 check-descriptor round-trip is deterministic across the full string domain', () => {
test('a well-formed descriptor survives project -> render -> parse -> descriptorFromProjection (incl. numeric coercion); target/rule reconstruct as strings', () => {
fc.assert(
fc.property(wellFormedArb, ({ item, kind, target, rule, fixture }) => {
fc.property(wellFormedArb, ({ item, kind, target, rule, fixture, clean }) => {
const projected = pc.projectProhibitions([item]);
if (projected[0].check_kind !== kind) return false; // projector emits the descriptor
const reparsed = fm.parseMustHavesBlock(renderProhibitionsDoc(projected), 'prohibitions');
@@ -240,6 +243,8 @@ describe('probe-core property: #1278 check-descriptor round-trip is deterministi
if (typeof d.target !== 'string' || d.target !== target) return false;
// violationFixture (#1346) survives the round-trip as a string (numeric-coercion normalized).
if (typeof d.violationFixture !== 'string' || d.violationFixture !== fixture) return false;
// cleanFixture (#1346) survives the round-trip as a string too (numeric-coercion normalized).
if (typeof d.cleanFixture !== 'string' || d.cleanFixture !== clean) return false;
if (kind === 'lint-rule') {
return typeof d.rule === 'string' && d.rule === rule;
}

View File

@@ -458,6 +458,36 @@ describe('probe-core: projectProhibitions descriptor projection (CHK-02)', () =>
'a fixture without a descriptor is meaningless and must not project');
});
test('CHK-02(#1346 clean): a node-test descriptor with check_clean_fixture projects it (the causation control)', () => {
const projected = pc.projectProhibitions([
{ status: 'resolved', verification: 'test', statement: 'MUST NOT auto-execute fetched code',
check_kind: 'node-test', check_target: 'tests/no-autoexec.test.cjs',
check_violation_fixture: 'tests/fixtures/autoexec-bad.txt',
check_clean_fixture: 'tests/fixtures/autoexec-clean.txt' },
]);
assert.equal(projected[0].check_clean_fixture, 'tests/fixtures/autoexec-clean.txt',
'a well-formed descriptor projects check_clean_fixture so the prover can prove content-dependence end-to-end');
});
test('CHK-02(#1346 clean): an empty/whitespace check_clean_fixture is NOT projected', () => {
const projected = pc.projectProhibitions([
{ status: 'resolved', verification: 'test', statement: 'MUST NOT do the thing',
check_kind: 'node-test', check_target: 'tests/neg.test.cjs',
check_violation_fixture: 'tests/fixtures/bad.txt', check_clean_fixture: ' ' },
]);
assert.ok(!('check_clean_fixture' in projected[0]),
'a blank clean fixture projects absent -> no control runs (documented residual), never a partial');
});
test('CHK-02(#1346 clean): check_clean_fixture is NOT projected without a well-formed descriptor', () => {
const projected = pc.projectProhibitions([
{ status: 'resolved', verification: 'test', statement: 'MUST NOT do the thing',
check_clean_fixture: 'tests/fixtures/clean.txt' },
]);
assert.ok(!('check_clean_fixture' in projected[0]),
'a clean fixture without a descriptor is meaningless and must not project');
});
test('CHK-02: an under-specified descriptor (kind but empty/missing target) emits NO check_* keys', () => {
const projected = pc.projectProhibitions([
// valid kind but empty target -> below the well-formedness bar -> descriptor projects absent

View File

@@ -529,6 +529,93 @@ describe('prohibition-enforcement REAL runner end-to-end (#1259)', () => {
assert.equal(result.evidence[0].failFirstProof, 'violation-fixture');
});
// ─── #1346 causation control: prove the RED is caused by the violation's CONTENT ───
// The documented residual (#1279 review Major 1): existence + a non-vacuous RED is necessary but
// NOT sufficient — a deceptive negative test that reds merely BECAUSE GSD_PROHIB_SUBJECT is SET
// (not because the subject's CONTENT violates the must-NOT) is still accepted. The mitigation is an
// OPTIONAL clean-subject control: when the descriptor carries a `cleanFixture`, the prover also runs
// the check against the KNOWN-CLEAN subject and requires it to stay GREEN. A content-independent red
// reds on the clean subject too -> control fails -> NOT proven (fail-closed).
test('a DECEPTIVE content-independent red is NOT proven fail-first when a clean control fixture is supplied (#1346)', (t) => {
const enforce = require(ENFORCEMENT_LIB);
const dir = createTempDir('prohib-deceptive-');
t.after(() => cleanup(dir));
// Deceptive: reds whenever a subject is PRESENT, regardless of its content. Goes RED against the
// bad fixture (looks fail-first) but ALSO reds against the clean subject -> the control catches it.
const tf = path.join(dir, 'neg.test.cjs');
fs.writeFileSync(tf,
"const { test } = require('node:test');\n" +
"const assert = require('node:assert');\n" +
"test('reds whenever a subject is present (deceptive, content-independent)', () => {\n" +
" assert.ok(!process.env.GSD_PROHIB_SUBJECT, 'fails whenever a subject is set');\n" +
"});\n");
const cleanSubject = path.join(dir, 'clean-subject.txt');
fs.writeFileSync(cleanSubject, 'this subject is clean\n');
const badFixture = path.join(dir, 'bad-subject.txt');
fs.writeFileSync(badFixture, 'this subject contains FORBIDDEN content\n');
const result = enforce.runProhibitionEnforcement(
TEST_TIER,
{ kind: 'node-test', target: tf, failFirst: true, violationFixture: badFixture, cleanFixture: cleanSubject },
{ cwd: dir, runCheck: () => ({ passed: true }) },
);
assert.notEqual(result.status, 'green',
'a content-independent red must NOT prove fail-first when a clean control is supplied — fail-closed');
});
test('an honest content-dependent node-test WITH a clean control fixture still greens (#1346 positive)', (t) => {
const enforce = require(ENFORCEMENT_LIB);
const dir = createTempDir('prohib-content-dep-');
t.after(() => cleanup(dir));
// Honest: reds ONLY when the subject's CONTENT contains FORBIDDEN. RED on the bad fixture, GREEN
// on the clean subject -> the control confirms content-dependence -> proven.
const tf = path.join(dir, 'neg.test.cjs');
fs.writeFileSync(tf,
"const { test } = require('node:test');\n" +
"const assert = require('node:assert');\n" +
"const fs = require('node:fs');\n" +
"test('rejects the forbidden content (content-dependent)', () => {\n" +
" const subject = fs.readFileSync(process.env.GSD_PROHIB_SUBJECT, 'utf-8');\n" +
" assert.ok(!subject.includes('FORBIDDEN'), 'subject must not contain FORBIDDEN');\n" +
"});\n");
const cleanSubject = path.join(dir, 'clean-subject.txt');
fs.writeFileSync(cleanSubject, 'this subject is clean\n');
const badFixture = path.join(dir, 'bad-subject.txt');
fs.writeFileSync(badFixture, 'this subject contains FORBIDDEN content\n');
const result = enforce.runProhibitionEnforcement(
TEST_TIER,
{ kind: 'node-test', target: tf, failFirst: true, violationFixture: badFixture, cleanFixture: cleanSubject },
{ cwd: dir, runCheck: () => ({ passed: true }) },
);
assert.equal(result.status, 'green',
'a content-dependent red (clean subject stays green) IS proven fail-first -> green');
assert.equal(result.evidence[0].failFirstProof, 'violation-fixture');
});
test('a supplied-but-MISSING clean control fixture fails closed (#1346, symmetric with the violation guard)', (t) => {
const enforce = require(ENFORCEMENT_LIB);
const dir = createTempDir('prohib-missing-clean-');
t.after(() => cleanup(dir));
const tf = path.join(dir, 'neg.test.cjs');
fs.writeFileSync(tf,
"const { test } = require('node:test');\n" +
"const assert = require('node:assert');\n" +
"const fs = require('node:fs');\n" +
"test('rejects the forbidden content', () => {\n" +
" const subject = fs.readFileSync(process.env.GSD_PROHIB_SUBJECT, 'utf-8');\n" +
" assert.ok(!subject.includes('FORBIDDEN'), 'subject must not contain FORBIDDEN');\n" +
"});\n");
const badFixture = path.join(dir, 'bad-subject.txt');
fs.writeFileSync(badFixture, 'this subject contains FORBIDDEN content\n');
const result = enforce.runProhibitionEnforcement(
TEST_TIER,
// cleanFixture points at a path that does not exist -> the control can't run -> fail-closed.
{ kind: 'node-test', target: tf, failFirst: true, violationFixture: badFixture, cleanFixture: path.join(dir, 'nope.txt') },
{ cwd: dir, runCheck: () => ({ passed: true }) },
);
assert.notEqual(result.status, 'green',
'a supplied clean fixture that does not exist cannot run the control -> fail-closed');
});
test('a HANGING node-test fails closed via the bounded timeout (B2: no unbounded subprocess)', (t) => {
const enforce = require(ENFORCEMENT_LIB);
const dir = createTempDir('prohib-hang-');
@@ -766,6 +853,59 @@ describe('prohibition-enforcement REAL runner end-to-end (#1259)', () => {
'the fully-projected prohibition greens through the default prover+runner — #1278 + #1279 compose');
assert.equal(result.evidence[0].failFirstProof, 'violation-fixture', 'green carries the machine-proof method');
});
test('COMPOSE (#1346 clean): a prohibition projected WITH check_clean_fixture proves content-dependence end-to-end (deceptive vs honest)', (t) => {
const enforce = require(ENFORCEMENT_LIB);
const pc = require(path.join(__dirname, '..', 'gsd-core', 'bin', 'lib', 'probe-core.cjs'));
const dir = createTempDir('prohib-compose-clean-1346-');
t.after(() => cleanup(dir));
// Full path: author all FIVE scalars -> project -> read back a descriptor that carries BOTH
// violationFixture and cleanFixture -> the default prover runs the causation control end-to-end.
fs.writeFileSync(path.join(dir, 'clean-subject.txt'), 'clean\n');
fs.writeFileSync(path.join(dir, 'bad-subject.txt'), 'FORBIDDEN content\n');
const author = (negTest) => pc.projectProhibitions([
{ status: 'resolved', verification: 'test', statement: 'MUST NOT auto-execute fetched code',
check_kind: 'node-test', check_target: negTest,
check_violation_fixture: 'bad-subject.txt', check_clean_fixture: 'clean-subject.txt' },
])[0];
// (a) HONEST, content-dependent negative test: RED on bad, GREEN on clean -> greens.
const honest = path.join(dir, 'honest.test.cjs');
fs.writeFileSync(honest,
"const { test } = require('node:test');\n" +
"const assert = require('node:assert');\n" +
"const fs = require('node:fs');\n" +
"const path = require('node:path');\n" +
// Fallback to the clean subject when GSD_PROHIB_SUBJECT is unset — the default runCheck observes
// a real clean pass without setting the env var (mirrors the #1314 violation-fixture capstone).
"test('rejects the forbidden content', () => {\n" +
" const subjectPath = process.env.GSD_PROHIB_SUBJECT || path.join(__dirname, 'clean-subject.txt');\n" +
" const subject = fs.readFileSync(subjectPath, 'utf-8');\n" +
" assert.ok(!subject.includes('FORBIDDEN'), 'subject must not contain FORBIDDEN');\n" +
"});\n");
const honestProjected = author(honest);
assert.equal(honestProjected.check_clean_fixture, 'clean-subject.txt', 'the clean scalar projected');
const honestDescriptor = enforce.descriptorFromProjection(honestProjected);
assert.equal(honestDescriptor.cleanFixture, 'clean-subject.txt', 'the clean fixture survived the round-trip');
const honestResult = enforce.runProhibitionEnforcement(honestProjected, honestDescriptor, { cwd: dir });
assert.equal(honestResult.status, 'green',
'a content-dependent prohibition greens end-to-end through the projected clean control (#1346)');
// (b) DECEPTIVE, content-independent test: RED whenever a subject is set -> reds on clean too ->
// the projected control fails -> NOT green, even though the violation alone would have proven RED.
const deceptive = path.join(dir, 'deceptive.test.cjs');
fs.writeFileSync(deceptive,
"const { test } = require('node:test');\n" +
"const assert = require('node:assert');\n" +
"test('reds whenever a subject is present (deceptive)', () => {\n" +
" assert.ok(!process.env.GSD_PROHIB_SUBJECT, 'fails whenever a subject is set');\n" +
"});\n");
const deceptiveProjected = author(deceptive);
const deceptiveDescriptor = enforce.descriptorFromProjection(deceptiveProjected);
const deceptiveResult = enforce.runProhibitionEnforcement(deceptiveProjected, deceptiveDescriptor, { cwd: dir });
assert.notEqual(deceptiveResult.status, 'green',
'a content-independent deceptive prohibition is caught by the projected clean control end-to-end (#1346)');
});
});
// ─── #1279 defaultProveFailFirst REAL prover end-to-end (FF-02 / FF-03 / FF-05 / FF-06 / FF-07) ──
@@ -1024,6 +1164,27 @@ describe('prohibition-enforcement: fail-closed descriptor-from-projection (CHK-0
'absent check_violation_fixture must NOT fabricate a fixture; the default prover then hard-gates (no green)');
});
test('CHK-08(#1346 clean): descriptorFromProjection maps check_clean_fixture -> cleanFixture (node-test)', () => {
const enforce = require(ENFORCEMENT_LIB);
const descriptor = enforce.descriptorFromProjection({
...PROJECTED_TIER, check_kind: 'node-test', check_target: 'tests/neg.test.cjs',
check_violation_fixture: 'tests/fixtures/bad-subject.txt',
check_clean_fixture: 'tests/fixtures/clean-subject.txt',
});
assert.equal(descriptor.cleanFixture, 'tests/fixtures/clean-subject.txt',
'the projected check_clean_fixture must reconstruct as cleanFixture so the causation control runs end-to-end (#1346)');
});
test('CHK-08(#1346 clean): no check_clean_fixture -> descriptor carries no cleanFixture (no control; documented residual remains)', () => {
const enforce = require(ENFORCEMENT_LIB);
const descriptor = enforce.descriptorFromProjection({
...PROJECTED_TIER, check_kind: 'node-test', check_target: 'tests/neg.test.cjs',
check_violation_fixture: 'tests/fixtures/bad-subject.txt',
});
assert.equal(descriptor.cleanFixture, undefined,
'absent check_clean_fixture must NOT fabricate a control; the prover keeps the documented residual, backward-compatible');
});
test('CHK-06(lint-rule missing rule): {check_kind:lint-rule, check_target:src/} (no check_rule) -> located:false, never green', () => {
const enforce = require(ENFORCEMENT_LIB);
const descriptor = enforce.descriptorFromProjection({

View File

@@ -72,7 +72,7 @@
"ship.md": 24388,
"sketch-wrap-up.md": 14223,
"sketch.md": 19960,
"spec-phase.md": 30921,
"spec-phase.md": 31503,
"spike-wrap-up.md": 15092,
"spike.md": 24517,
"stats.md": 6718,
@@ -85,6 +85,6 @@
"undo.md": 10431,
"update.md": 21053,
"validate-phase.md": 10745,
"verify-phase.md": 37821,
"verify-phase.md": 38228,
"verify-work.md": 31157
}