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:
5
.changeset/prohibition-causation-control.md
Normal file
5
.changeset/prohibition-causation-control.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
type: Changed
|
||||
pr: 1346
|
||||
---
|
||||
**verify-phase test-tier prohibition fail-first can now prove the RED is caused by the violation's _content_** — the `node-test` machine-proof (#1279) confirmed a known-bad subject drives the negative test RED, but could not tell a genuine content-violation from a deceptive test that reds merely because `GSD_PROHIB_SUBJECT` is set. An optional fifth flat scalar `check_clean_fixture` (→ `CheckDescriptor.cleanFixture`) threads a KNOWN-CLEAN control subject through `projectProhibitions` + `descriptorFromProjection`; when present the prover also runs the check against it 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). It is opt-in and additive: absent a clean fixture the prover behaves exactly as it did post-#1314 (no control, documented residual), preserving the zero-authoring compose path; the lint-rule kind needs no analog. (#1346)
|
||||
@@ -114,9 +114,19 @@ This addendum ratifies three contract points:
|
||||
Net effect on D4: the *guarantee* ("a `test`-tier prohibition is never a silent pass") was preserved at every step — fail-closed-now (#644), genuine-execution (#1259), and now **machine-proven fail-first (#1279)**. A `test`-tier prohibition reaches `green`/`passed` ONLY when the wired check both genuinely, non-vacuously passes AND is independently proven to fail on a violation; every miss/fail/un-provable hard-gates. The decision also lives in `src/prohibition-enforcement.cts` comments, `gsd-core/references/prohibition-probe.md`, `gsd-core/workflows/verify-phase.md`, and the #1279 changeset.
|
||||
|
||||
**Review corrections (#1314 maintainer review) — two soundness items:**
|
||||
- **node-test fixture-existence guard (was fail-OPEN) — FIXED.** The node-test prover originally guarded only `if (!fixture)`. A missing/typo'd/stale `violationFixture` path made `GSD_PROHIB_SUBJECT` point at a non-existent file; an honest negative test then threw ENOENT *inside its callback* — a failing test named distinctly from the file — which `isNonVacuousNodeTestRed` accepted as proof, **forging a green from a setup crash** (asymmetric with the lint-rule path, which fail-CLOSES on `< 1` file result). Fixed by requiring `fs.existsSync(path.resolve(cwd, fixture))` before spawning (symmetric fail-closed; resolved against the producer's `cwd` to match the child's resolution). **Documented residual (#1346):** existence is necessary but not sufficient — a deceptive test that reds merely *because* `GSD_PROHIB_SUBJECT` is set (not because the subject's CONTENT violates) is still accepted; proving causation generically for an arbitrary author-supplied test is not possible, so it is recorded as a constraint, not implied-solved.
|
||||
- **node-test fixture-existence guard (was fail-OPEN) — FIXED.** The node-test prover originally guarded only `if (!fixture)`. A missing/typo'd/stale `violationFixture` path made `GSD_PROHIB_SUBJECT` point at a non-existent file; an honest negative test then threw ENOENT *inside its callback* — a failing test named distinctly from the file — which `isNonVacuousNodeTestRed` accepted as proof, **forging a green from a setup crash** (asymmetric with the lint-rule path, which fail-CLOSES on `< 1` file result). Fixed by requiring `fs.existsSync(path.resolve(cwd, fixture))` before spawning (symmetric fail-closed; resolved against the producer's `cwd` to match the child's resolution). **Residual (#1346) — now MITIGATED by an optional control; see the 2026-06-21 addendum below:** existence is necessary but not sufficient — a deceptive test that reds merely *because* `GSD_PROHIB_SUBJECT` is set (not because the subject's CONTENT violates) was still accepted; a generic always-on proof is impossible, so #1346 adds an **opt-in clean-subject control** that proves content-dependence when the author supplies one (and the residual remains, documented, only for checks with no control fixture).
|
||||
- **`violationFixture` projection source (#1278 ↔ #1279 now COMPOSE) — DELIVERED.** Initially `descriptorFromProjection` reconstructed only `{ kind, target, rule? }` and the projection carried no fixture, so a prohibition wired purely through the deterministic path always hard-gated. This PR threads a **fourth flat scalar `check_violation_fixture`** through `projectProhibitions` + `descriptorFromProjection` (rides both kinds; mirrors `CheckDescriptor.violationFixture`). A prohibition authored with all four scalars now **machine-proves fail-first and greens end-to-end through the projection alone** (zero hand-authoring) — the round-trip is pinned by a fast-check property + CHK-03(D) + an end-to-end COMPOSE capstone. Fail-closed is preserved: a descriptor with no `check_violation_fixture` (or a blank one) projects absent and hard-gates. The remaining work under #1346 is now just the node-test causation residual above.
|
||||
|
||||
## Addendum (2026-06-21, #1346) — node-test causation control: prove the RED is CONTENT-caused
|
||||
|
||||
The #1314 review left one tracked residual (above): the node-test prover confirms the violation fixture exists and that the negative test goes a non-vacuous RED, but could not prove the RED was caused by the subject's **content** rather than by `GSD_PROHIB_SUBJECT` merely being *set*. A deceptive content-independent test (`assert.ok(!process.env.GSD_PROHIB_SUBJECT)`) was still accepted. A general always-on proof is impossible for an arbitrary author-supplied test, so #1346 closes the gap with an **opt-in control** rather than a forced one.
|
||||
|
||||
This addendum ratifies one contract point:
|
||||
|
||||
- **(d) `CheckDescriptor.cleanFixture?` / `check_clean_fixture` — the causation control (the 5th flat scalar).** An OPTIONAL author-supplied path to a KNOWN-CLEAN control subject. When present, the node-test prover runs the SAME negative test a second time with `GSD_PROHIB_SUBJECT=<cleanFixture>` and requires it to stay a **non-vacuous GREEN**. Fail-first is then proven ONLY when the check is **RED on the violation AND GREEN on the clean subject** — i.e. the red is content-dependent. A deceptive test that reds whenever the env var is set reds on the clean subject too → the control fails → not proven (fail-closed). The scalar rides both kinds through `projectProhibitions` + `descriptorFromProjection` exactly as `check_violation_fixture` does (round-trip pinned by the fast-check property + an end-to-end COMPOSE capstone exercising both the honest and deceptive subjects).
|
||||
|
||||
**Why opt-in, not required:** making the control mandatory would regress the #1314 zero-authoring compose path — every existing node-test prohibition (which carries no clean fixture) would suddenly hard-gate. So **absent `cleanFixture` → no control runs and behavior is byte-identical to post-#1314**; the residual remains a documented permanent constraint *only* for checks whose author did not supply a clean control. An author opts into the stronger machine guarantee by supplying one. The lint-rule kind needs no analog: its "subject" *is* the linted file (no `GSD_PROHIB_SUBJECT` indirection), so the "reds because the env var is set" gap does not exist there. Net effect on D4 is unchanged — every miss/fail/un-provable still hard-gates; this only *tightens* what counts as proven. The mechanism lives in `src/prohibition-enforcement.cts` (`defaultProveFailFirst` node-test branch + the `runNodeTestWithSubject` helper) and `src/probe-core.cts` (`projectProhibitions`), compiled by `build:lib`.
|
||||
|
||||
## Addendum (2026-06-15): optional `check` descriptor on the prohibition item — D3 shape extension (#1278)
|
||||
|
||||
This ratifies the **deterministic SOURCE** for the test-tier `CheckDescriptor` that #1259 (PR #1273) left caller/verifier-supplied. #1259 shipped the PRODUCER (`check prohibition-enforcement`) that *runs* a wired check given a `{kind, target, rule?}` descriptor, but the descriptor itself was invented by the verify-phase LLM each run (the "locate" half). #1278 makes that locate half **deterministic**: an optional `check` descriptor is authored at spec-phase on the resolved `test`-tier prohibition, projected by `projectProhibitions`, and read back by verify-phase — so a wired, passing test closes the gap with **zero manual authoring**. This extends the **Decision 3 prohibition-item shape** (it adds optional keys to that item), so it is ratified here rather than rewriting D3 in place.
|
||||
|
||||
@@ -157,7 +157,7 @@ A `resolved`/`test`-tier prohibition MAY carry an **optional `check` descriptor*
|
||||
the wired mechanical check, so verify-phase locates it deterministically instead of inventing
|
||||
`{kind, target, rule}` each run. The descriptor is captured at spec-phase (soft / optional —
|
||||
the author wires it when the negative test or lint rule already exists) and is represented as
|
||||
**four flat scalar keys** on the `must_haves.prohibitions` item — never a nested `check: {}`
|
||||
**five flat scalar keys** on the `must_haves.prohibitions` item — never a nested `check: {}`
|
||||
object:
|
||||
|
||||
- `check_kind` — `node-test` | `lint-rule` (which producer mechanism runs the check).
|
||||
@@ -165,16 +165,19 @@ object:
|
||||
- `check_rule` — the `ruleId` to filter on, **lint-rule only** (absent for `node-test`).
|
||||
- `check_violation_fixture` — path to a KNOWN-BAD subject the #1279 prover runs the check against to
|
||||
machine-prove fail-first (rides BOTH kinds; for `node-test` it is injected via `GSD_PROHIB_SUBJECT`).
|
||||
- `check_clean_fixture` — **optional** path to a KNOWN-CLEAN control subject (#1346). When present the
|
||||
node-test prover also runs the check against it and requires GREEN, proving the violation's RED is
|
||||
caused by the subject's *content* (not merely by `GSD_PROHIB_SUBJECT` being set). Absent → no control.
|
||||
|
||||
The flat-scalar shape is load-bearing: the shared `parseMustHavesBlock` is a flat parser and a
|
||||
nested object would flatten/mangle the round-trip (ADR-550 2026-06-15 addendum; #644 "no parser
|
||||
rewrite" precedent). `projectProhibitions` emits these keys **only for a well-formed descriptor**
|
||||
(valid `check_kind` + non-empty `check_target`; `check_rule` only on the lint-rule path;
|
||||
`check_violation_fixture` only when non-empty), and verify-phase reads them back via
|
||||
`descriptorFromProjection` into the `CheckDescriptor` handed to `check prohibition-enforcement`. This
|
||||
closes **both** the locate (#1278) and the machine-proof-fixture (#1346) halves with **zero manual
|
||||
descriptor authoring**: a prohibition authored with all four scalars greens end-to-end through the
|
||||
projection alone.
|
||||
`check_violation_fixture` and `check_clean_fixture` only when non-empty), and verify-phase reads them
|
||||
back via `descriptorFromProjection` into the `CheckDescriptor` handed to `check prohibition-enforcement`.
|
||||
This closes the locate (#1278), the machine-proof-fixture (#1279), and the causation-control (#1346)
|
||||
halves with **zero manual descriptor authoring**: a prohibition authored with the scalars greens
|
||||
end-to-end through the projection alone.
|
||||
|
||||
**Fail-closed + backward-compat.** A partial descriptor (`lint-rule` missing `check_rule`), an
|
||||
unknown `check_kind`, an **absent** descriptor, OR a descriptor with **no `check_violation_fixture`**
|
||||
@@ -182,8 +185,11 @@ falls through to the producer's fail-closed paths (`located: false`, or located-
|
||||
never a silent green. A prohibition with no descriptor parses and disposes byte-identically to today.
|
||||
`failFirst` is **not** sourced from the descriptor and is **demoted** (machine-proven fail-first
|
||||
DELIVERED in #1279 — no path greens on attestation alone, FF-08); the `dispositionForProhibition`
|
||||
policy is unchanged. Residual (tracked **#1346**): the node-test proof confirms the fixture exists and
|
||||
the check goes RED, but cannot generically prove the red was *caused by* the subject's content.
|
||||
policy is unchanged. Causation (**#1346**): the node-test proof confirms the fixture exists and the
|
||||
check goes RED; supplying `check_clean_fixture` adds an opt-in control that *also* requires GREEN on a
|
||||
known-clean subject, proving the red is content-caused. With no clean fixture the control cannot run,
|
||||
so that one residual case (a deceptive test reding merely because the env var is set) stays a
|
||||
documented constraint — an author opts into the stronger proof by wiring a clean control subject.
|
||||
|
||||
## Output schema
|
||||
|
||||
@@ -191,7 +197,7 @@ The probe emits, per kept prohibition, an item of the form:
|
||||
|
||||
```
|
||||
{ requirement_id, category, status, verification, resolution, reason, statement,
|
||||
check_kind?, check_target?, check_rule? }
|
||||
check_kind?, check_target?, check_rule?, check_violation_fixture?, check_clean_fixture? }
|
||||
```
|
||||
|
||||
where `statement` is the must-NOT sentence and `category` is the values/safety/ethics class
|
||||
|
||||
@@ -365,10 +365,15 @@ For each Requirement gathered so far, run the two-stage recall→precision pass:
|
||||
- `check_target` — the negative-test file path (for `node-test`), or the path to lint
|
||||
(for `lint-rule`).
|
||||
- `check_rule` — the eslint rule id (e.g. `local/no-source-grep`); `lint-rule` only.
|
||||
- `check_violation_fixture` (#1346) — path to a KNOWN-BAD subject the wired check is run
|
||||
- `check_violation_fixture` (#1279) — path to a KNOWN-BAD subject the wired check is run
|
||||
against to **machine-prove fail-first**; rides BOTH kinds. Capture it to let the item green
|
||||
end-to-end with zero hand-authoring at verify time; for `node-test` the negative test should
|
||||
read its subject from the `GSD_PROHIB_SUBJECT` env var so the prover can inject this fixture.
|
||||
- `check_clean_fixture` (#1346) — **optional** path to a KNOWN-CLEAN control subject. When
|
||||
captured, the `node-test` prover also runs the check against it and requires GREEN — proving
|
||||
the violation's RED is caused by the subject's *content*, not by `GSD_PROHIB_SUBJECT` merely
|
||||
being set. Capture it for a stronger guarantee; omit it and the check still proves fail-first
|
||||
on the violation alone (the content-causation residual stays documented for that case).
|
||||
This is a **SOFT capture (CHK-04): a `test`-tier prohibition WITHOUT a descriptor is still
|
||||
allowed** — if the author cannot yet name the wired check, leave the descriptor empty and
|
||||
proceed. It is NOT a hard authoring block; the item simply stays fail-closed/flagged
|
||||
@@ -395,7 +400,7 @@ For each Requirement gathered so far, run the two-stage recall→precision pass:
|
||||
written (test or judgment tier); otherwise leave `unresolved`. **`--auto` NEVER auto-dismisses
|
||||
a prohibition** — a wrong dismissal is the exact silent failure this probe eliminates (PROB-06,
|
||||
the load-bearing safety property). On a `test`-tier auto-resolution, capture the `check_kind` /
|
||||
`check_target` / `check_rule` / `check_violation_fixture` descriptor **only when a wired check is unambiguous**; otherwise
|
||||
`check_target` / `check_rule` / `check_violation_fixture` / `check_clean_fixture` descriptor **only when a wired check is unambiguous**; otherwise
|
||||
leave it empty — `--auto` NEVER fabricates a check path or fixture (a wrong locate is re-validated and
|
||||
fails closed at the producer, but a fabricated path is still noise to avoid). Log:
|
||||
`[auto] prohibitions: R resolved, U unresolved`.
|
||||
@@ -408,7 +413,7 @@ Populate the `## Prohibitions` section of SPEC.md from the resolved prohibitions
|
||||
`resolved`/`test` row is a checkable negative acceptance criterion; `resolved`/`judgment`
|
||||
rows route to judgment review; `⚠ UNRESOLVED` rows are flagged as assumptions). A
|
||||
`resolved`/`test` row ALSO carries its captured `check_kind` / `check_target` / `check_rule` /
|
||||
`check_violation_fixture` descriptor when present (so the projection feeds `verify-phase`'s deterministic locate + machine-proof, #1278 + #1346);
|
||||
`check_violation_fixture` / `check_clean_fixture` descriptor when present (so the projection feeds `verify-phase`'s deterministic locate + machine-proof + causation control, #1278 + #1279 + #1346);
|
||||
a `test` row with no captured descriptor is still valid — it stays fail-closed/flagged
|
||||
downstream rather than blocking authoring.
|
||||
|
||||
|
||||
@@ -76,11 +76,11 @@ Aggregate all must_haves across plans for phase-level verification.
|
||||
gsd_run check prohibition-enforcement <request.json>
|
||||
```
|
||||
|
||||
where `<request.json>` carries `{ prohibition, check, mode }` — `check` being the wired mechanical-check descriptor `{ kind: 'node-test' | 'lint-rule', target, rule?, violationFixture, failFirst? }`, with `kind`/`target`/`rule`/`violationFixture` now sourced from the projected `check_*` scalars (not author/verifier invention — #1278 + #1346). For `node-test`, `target` (from `check_target`) is the negative-test file path; for `lint-rule`, `target` is the PATH to lint and `rule` (from `check_rule`) is the eslint rule id (e.g. `local/no-source-grep`) — both required (a lint-rule without `rule` is not a valid wired check). `violationFixture` (from `check_violation_fixture`) is the path to a KNOWN-BAD subject the producer runs the check against to **machine-prove fail-first** (for `node-test`, injected via the `GSD_PROHIB_SUBJECT` env convention — #1279); `failFirst` is a DEMOTED, non-authoritative hint kept only for backward route-JSON shape (no path greens on it alone — FF-08). The producer LOCATES the wired check from the projection, **machine-proves it is fail-first** by running it against the violation and confirming it goes RED, RUNS it for a genuine non-vacuous pass, builds `enforcementEvidence`, and emits the `dispositionForProhibition()` verdict (#1259 + #1278 + #1279, ADR-550 D5d). Fail-first is **machine-proven, not caller-attested** — absent a provable violation the producer fails closed, never falling back to attestation. Route the result by its typed fields:
|
||||
where `<request.json>` carries `{ prohibition, check, mode }` — `check` being the wired mechanical-check descriptor `{ kind: 'node-test' | 'lint-rule', target, rule?, violationFixture, cleanFixture?, failFirst? }`, with `kind`/`target`/`rule`/`violationFixture`/`cleanFixture` now sourced from the projected `check_*` scalars (not author/verifier invention — #1278 + #1279 + #1346). For `node-test`, `target` (from `check_target`) is the negative-test file path; for `lint-rule`, `target` is the PATH to lint and `rule` (from `check_rule`) is the eslint rule id (e.g. `local/no-source-grep`) — both required (a lint-rule without `rule` is not a valid wired check). `violationFixture` (from `check_violation_fixture`) is the path to a KNOWN-BAD subject the producer runs the check against to **machine-prove fail-first** (for `node-test`, injected via the `GSD_PROHIB_SUBJECT` env convention — #1279); the optional `cleanFixture` (from `check_clean_fixture`) is a KNOWN-CLEAN control subject the `node-test` prover ALSO requires to stay GREEN, proving the RED is content-caused (#1346); `failFirst` is a DEMOTED, non-authoritative hint kept only for backward route-JSON shape (no path greens on it alone — FF-08). The producer LOCATES the wired check from the projection, **machine-proves it is fail-first** by running it against the violation and confirming it goes RED, RUNS it for a genuine non-vacuous pass, builds `enforcementEvidence`, and emits the `dispositionForProhibition()` verdict (#1259 + #1278 + #1279, ADR-550 D5d). Fail-first is **machine-proven, not caller-attested** — absent a provable violation the producer fails closed, never falling back to attestation. Route the result by its typed fields:
|
||||
- **`status: 'green'`, `flagged: false`** (a genuinely-passing wired negative test / lint rule, `located: true`, non-empty `evidence`) → the item is satisfiable → it can reach **passed**.
|
||||
- **missing, non-attested, or genuinely-non-passing check** (`located: false` OR `status: 'unverified'`, `flagged: true`) → **hard-gate**: disposes flagged-unverified, NEVER green, routing to `gaps_found` in BOTH interactive and autonomous modes (a failing mechanical check blocks even AFK; ADR-550 D4 / D3). The deterministic fail-closed default backing every miss/fail is `dispositionForProhibition()` in probe-core (`status: 'unverified'`, `flagged: true` on empty `enforcementEvidence`).
|
||||
|
||||
> **Descriptor source — deterministic locate + machine-proof compose (#1278 + #1346, DELIVERED).** The `check` descriptor's `{ kind, target, rule, violationFixture }` is now sourced **deterministically from the projected `check_kind` / `check_target` / `check_rule` / `check_violation_fixture` scalars** on the `must_haves.prohibitions` item (authored at `/gsd:spec-phase`, projected by `projectProhibitions`, read back via the `descriptorFromProjection` adapter). So both halves close with **zero manual descriptor authoring** — the verifier neither invents the locate (#1278) nor hand-supplies the violation fixture (#1346): a prohibition authored with all four scalars machine-proves fail-first and greens end-to-end through the projection alone (removing the spoofable invent-at-verify-time surface; ADR-857 §147 exogenous grading). **Fail-closed is preserved:** an item with NO projected descriptor, a PARTIAL one (e.g. a `lint-rule` missing `check_rule`), OR a descriptor with **no `check_violation_fixture`** makes `descriptorFromProjection` return `null` / an under-specified or fixture-less descriptor, which falls through to the producer's fail-closed paths (`located: false`, or located-but-unprovable) → flagged-unverified, NEVER green, in BOTH modes. `failFirst` is demoted and greens nothing on its own (#1279, FF-08). Residual (tracked **#1346**): the node-test proof confirms the fixture exists and the check goes RED, but cannot generically prove the red was *caused by* the subject's content vs the env merely being set.
|
||||
> **Descriptor source — deterministic locate + machine-proof compose (#1278 + #1346, DELIVERED).** The `check` descriptor's `{ kind, target, rule, violationFixture }` is now sourced **deterministically from the projected `check_kind` / `check_target` / `check_rule` / `check_violation_fixture` scalars** on the `must_haves.prohibitions` item (authored at `/gsd:spec-phase`, projected by `projectProhibitions`, read back via the `descriptorFromProjection` adapter). So both halves close with **zero manual descriptor authoring** — the verifier neither invents the locate (#1278) nor hand-supplies the violation fixture (#1346): a prohibition authored with all four scalars machine-proves fail-first and greens end-to-end through the projection alone (removing the spoofable invent-at-verify-time surface; ADR-857 §147 exogenous grading). **Fail-closed is preserved:** an item with NO projected descriptor, a PARTIAL one (e.g. a `lint-rule` missing `check_rule`), OR a descriptor with **no `check_violation_fixture`** makes `descriptorFromProjection` return `null` / an under-specified or fixture-less descriptor, which falls through to the producer's fail-closed paths (`located: false`, or located-but-unprovable) → flagged-unverified, NEVER green, in BOTH modes. `failFirst` is demoted and greens nothing on its own (#1279, FF-08). Causation (**#1346**): supplying `check_clean_fixture` adds an opt-in control — the `node-test` prover also requires GREEN on a known-clean subject, proving the RED is content-caused; with no clean fixture that one residual case (a deceptive test reding merely because the env var is set) stays a documented constraint, an author opting into the stronger proof by wiring a clean control.
|
||||
|
||||
**Option B: Use Success Criteria from ROADMAP.md**
|
||||
|
||||
|
||||
@@ -303,6 +303,11 @@ export interface Prohibition {
|
||||
// against to MACHINE-PROVE fail-first. Projected only alongside a well-formed descriptor; absent ->
|
||||
// the producer hard-gates (green requires a fixture). Mirrors `CheckDescriptor.violationFixture`.
|
||||
check_violation_fixture?: string;
|
||||
// Optional 5th flat scalar (#1346): the path to a KNOWN-CLEAN control subject the prover ALSO runs
|
||||
// the check against, requiring it to stay GREEN — proving the violation RED is caused by the
|
||||
// subject's CONTENT, not merely by GSD_PROHIB_SUBJECT being set. Projected only alongside a
|
||||
// well-formed descriptor; absent -> no control (documented residual). Mirrors `CheckDescriptor.cleanFixture`.
|
||||
check_clean_fixture?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -387,6 +392,13 @@ export function projectProhibitions(
|
||||
if (typeof p.check_violation_fixture === 'string' && p.check_violation_fixture.trim() !== '') {
|
||||
entry.check_violation_fixture = String(p.check_violation_fixture);
|
||||
}
|
||||
// `check_clean_fixture` (#1346) rides BOTH kinds — the KNOWN-CLEAN control subject the prover
|
||||
// requires to stay GREEN (content-dependence proof). Emit ONLY a non-empty fixture (blank ->
|
||||
// absent so no control runs; the documented residual remains). Like the violation fixture it is
|
||||
// meaningless without the descriptor, so it lives inside this well-formed-descriptor branch.
|
||||
if (typeof p.check_clean_fixture === 'string' && p.check_clean_fixture.trim() !== '') {
|
||||
entry.check_clean_fixture = String(p.check_clean_fixture);
|
||||
}
|
||||
}
|
||||
out.push(entry);
|
||||
}
|
||||
|
||||
@@ -76,6 +76,15 @@ export interface CheckDescriptor {
|
||||
* prove fail-first; ABSENT for node-test → the default prover fails closed (never attestation).
|
||||
*/
|
||||
violationFixture?: string;
|
||||
/**
|
||||
* OPTIONAL author-supplied path to a KNOWN-CLEAN control subject (#1346). When present, the prover
|
||||
* runs the check against it as a CAUSATION CONTROL and requires it to stay GREEN — proof that the
|
||||
* RED on `violationFixture` was caused by the subject's CONTENT, not merely by `GSD_PROHIB_SUBJECT`
|
||||
* being set. A deceptive content-independent check reds on the clean subject too → control fails →
|
||||
* not proven. ABSENT → no control runs (the documented residual remains; backward-compatible with
|
||||
* the #1314 zero-authoring compose path). A supplied-but-missing path fails closed.
|
||||
*/
|
||||
cleanFixture?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -90,8 +99,9 @@ export interface CheckDescriptor {
|
||||
* - `null`/`undefined`/non-object input -> `null`.
|
||||
* - `check_kind` ABSENT -> `null` (no descriptor -> producer locates nothing -> fail-closed).
|
||||
* - `check_kind` present -> `{ kind: check_kind, target: check_target }`, adding `rule: check_rule`
|
||||
* ONLY when `check_rule` is a non-empty string, and `violationFixture: check_violation_fixture`
|
||||
* ONLY when that scalar is a non-empty string (#1346 — composes #1278 locate with #1279 proof).
|
||||
* ONLY when `check_rule` is a non-empty string, `violationFixture: check_violation_fixture`
|
||||
* ONLY when that scalar is a non-empty string (composes #1278 locate with #1279 proof), and
|
||||
* `cleanFixture: check_clean_fixture` ONLY when that scalar is non-empty (#1346 causation control).
|
||||
* - `failFirst` is NEVER sourced from the projection — it stays a verify-time caller attestation
|
||||
* (#1279 machine-proves it; out of scope here). The returned descriptor carries no `failFirst`.
|
||||
* - The adapter does NOT strictly validate kind/target/rule: it faithfully reconstructs whatever
|
||||
@@ -128,6 +138,12 @@ export function descriptorFromProjection(
|
||||
// hard-gates (fail-closed; green requires a fixture), never fabricated.
|
||||
const fixture = scalar(projected.check_violation_fixture);
|
||||
if (fixture.trim().length > 0) descriptor.violationFixture = fixture;
|
||||
// `cleanFixture` (#1346) rides BOTH kinds — reconstruct it from `check_clean_fixture` so the
|
||||
// causation control runs end-to-end: when present the prover also requires the check to stay GREEN
|
||||
// against this known-clean subject (proving the violation RED is content-dependent). Absent/blank ->
|
||||
// no control (the documented residual remains; backward-compatible with the #1314 compose path).
|
||||
const clean = scalar(projected.check_clean_fixture);
|
||||
if (clean.trim().length > 0) descriptor.cleanFixture = clean;
|
||||
return descriptor;
|
||||
}
|
||||
|
||||
@@ -438,6 +454,31 @@ function posTimeout(timeoutMs: number | undefined, def: number): number {
|
||||
return typeof timeoutMs === 'number' && timeoutMs > 0 ? timeoutMs : def;
|
||||
}
|
||||
|
||||
/**
|
||||
* Spawn the negative `node --test` against a single subject (set via the `GSD_PROHIB_SUBJECT`
|
||||
* convention, #1279) and return its TAP output. Reuses the bounded-subprocess machinery
|
||||
* (`process.execPath`, arg arrays → no shell, `childEnv`, bounded `timeout`/`maxBuffer`) and NEVER
|
||||
* throws — a RED run exits non-zero, so the partial TAP (with the `# fail` summary) is recovered from
|
||||
* the thrown error's `stdout`. The prover calls this once per subject: the KNOWN-BAD violation fixture
|
||||
* (expect RED) and, for the #1346 causation control, the KNOWN-CLEAN control subject (expect GREEN).
|
||||
*/
|
||||
function runNodeTestWithSubject(check: CheckDescriptor, cwd: string, subject: string, timeoutMs?: number): string {
|
||||
try {
|
||||
return execFileSync(process.execPath, buildNodeTestArgs(check), {
|
||||
cwd,
|
||||
encoding: 'utf-8',
|
||||
stdio: ['ignore', 'pipe', 'pipe'],
|
||||
windowsHide: true,
|
||||
env: { ...childEnv(), GSD_PROHIB_SUBJECT: subject },
|
||||
timeout: posTimeout(timeoutMs, NODE_TEST_TIMEOUT_MS),
|
||||
maxBuffer: CHECK_MAX_BUFFER,
|
||||
});
|
||||
} catch (e) {
|
||||
const stdout = e && typeof e === 'object' && 'stdout' in e ? (e as { stdout?: unknown }).stdout : '';
|
||||
return typeof stdout === 'string' ? stdout : '';
|
||||
}
|
||||
}
|
||||
|
||||
function defaultRunCheck(check: CheckDescriptor, cwd: string, timeoutMs?: number): CheckRunResult {
|
||||
try {
|
||||
if (check.kind === 'node-test') {
|
||||
@@ -554,34 +595,32 @@ function defaultProveFailFirst(check: CheckDescriptor, cwd: string, timeoutMs?:
|
||||
// a setup crash, not from the prohibition firing. Requiring the fixture to exist before spawning
|
||||
// closes the realistic typo/stale-path case (#1279 review, Major 1).
|
||||
//
|
||||
// KNOWN RESIDUAL (documented, fail-open direction, tracked follow-up #1346): existence is
|
||||
// necessary but not sufficient — a deliberately deceptive negative test that reds merely BECAUSE
|
||||
// `GSD_PROHIB_SUBJECT` is set (rather than because the subject's CONTENT violates the must-NOT)
|
||||
// is still accepted. Proving "the red was CAUSED BY the violation" cannot be done generically for
|
||||
// an arbitrary author-supplied test, so it is recorded as a constraint, not silently implied-solved.
|
||||
// CAUSATION (#1346): existence + a non-vacuous red is necessary but not sufficient — a deceptive
|
||||
// negative test that reds merely BECAUSE `GSD_PROHIB_SUBJECT` is set (rather than because the
|
||||
// subject's CONTENT violates the must-NOT) would otherwise be accepted. The OPTIONAL `cleanFixture`
|
||||
// control below proves content-dependence when supplied (red on bad AND green on clean). When NO
|
||||
// clean fixture is authored the control cannot run, so the residual remains a documented constraint
|
||||
// for that case (an author opts into the stronger proof by supplying a known-clean control subject).
|
||||
// Resolve the fixture against `cwd` (NOT the verify process's cwd): the spawned test reads
|
||||
// `GSD_PROHIB_SUBJECT` and resolves a relative subject against `cwd`, so the existence check must
|
||||
// use the SAME base or it could pass here yet ENOENT in the child (re-opening the fail-open hole).
|
||||
if (!fixture || !fs.existsSync(path.resolve(cwd, fixture))) return { provenFailFirst: false };
|
||||
let out = '';
|
||||
try {
|
||||
out = execFileSync(process.execPath, buildNodeTestArgs(check), {
|
||||
cwd,
|
||||
encoding: 'utf-8',
|
||||
stdio: ['ignore', 'pipe', 'pipe'],
|
||||
windowsHide: true,
|
||||
// CONVENTION (#1279): the negative test reads its subject-under-test from this env var.
|
||||
env: { ...childEnv(), GSD_PROHIB_SUBJECT: fixture },
|
||||
timeout: posTimeout(timeoutMs, NODE_TEST_TIMEOUT_MS),
|
||||
maxBuffer: CHECK_MAX_BUFFER,
|
||||
});
|
||||
} catch (e) {
|
||||
// A negative test that goes RED exits non-zero; the partial TAP (with the `# fail` summary)
|
||||
// is on stdout. Parse what we have: a real failure here is the PROOF the test is fail-first.
|
||||
const stdout = e && typeof e === 'object' && 'stdout' in e ? (e as { stdout?: unknown }).stdout : '';
|
||||
out = typeof stdout === 'string' ? stdout : '';
|
||||
// Run the negative test against the KNOWN-BAD subject and require a NON-VACUOUS red.
|
||||
const redOut = runNodeTestWithSubject(check, cwd, fixture, timeoutMs);
|
||||
if (!isNonVacuousNodeTestRed(redOut, check.target)) return { provenFailFirst: false, method: 'violation-fixture' };
|
||||
// #1346 CAUSATION CONTROL (optional): if a clean control subject is supplied, run the SAME test
|
||||
// against it and require it to stay GREEN. This proves the red above was caused by the subject's
|
||||
// CONTENT — a deceptive test that reds merely because GSD_PROHIB_SUBJECT is SET reds here too →
|
||||
// not content-dependent → not proven. Absent → no control (documented residual; backward-compat).
|
||||
const clean = check.cleanFixture;
|
||||
if (clean) {
|
||||
// A supplied-but-missing/typo'd control path can't run the control → fail-closed, symmetric
|
||||
// with the violation-fixture existence guard (resolve against the SAME `cwd` as the child).
|
||||
if (!fs.existsSync(path.resolve(cwd, clean))) return { provenFailFirst: false, method: 'violation-fixture' };
|
||||
const cleanOut = runNodeTestWithSubject(check, cwd, clean, timeoutMs);
|
||||
if (!isNonVacuousNodeTestPass(cleanOut, check.target)) return { provenFailFirst: false, method: 'violation-fixture' };
|
||||
}
|
||||
return { provenFailFirst: isNonVacuousNodeTestRed(out, check.target), method: 'violation-fixture' };
|
||||
return { provenFailFirst: true, method: 'violation-fixture' };
|
||||
}
|
||||
// Unknown kind — defensive; the LOCATE guard already rejects it.
|
||||
return { provenFailFirst: false };
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user