enhance(#1279): project check_violation_fixture scalar — #1278 locate + #1279 proof compose end-to-end (#1346)

Delivers option (a) from the #1314 maintainer review: thread a fourth flat
scalar check_violation_fixture through the projection so a prohibition authored
at spec-phase machine-proves fail-first and greens through the deterministic
path alone — zero hand-authoring at verify time.

- src/probe-core.cts: Prohibition gains check_violation_fixture?; projectProhibitions
  emits it (both kinds) ONLY for a well-formed descriptor and ONLY when non-empty
  (blank/absent -> projects absent -> producer hard-gates, never a partial green).
- src/prohibition-enforcement.cts: descriptorFromProjection reads it back into
  violationFixture via the same numeric-coercion-safe scalar() normalizer.
- Tests (RED-first, proven non-vacuous by reverting both src edits): CHK-02(#1346)
  projection emit, CHK-08(#1346) read-back, CHK-03(D) example round-trip, the
  fast-check round-trip property extended to the 4th scalar (the contract trek-e
  blocked #1301 on), and a real-subprocess COMPOSE capstone greening end-to-end
  through project -> descriptorFromProjection -> default prover+runner.
- Docs flipped from 'hard-gates until #1346' to 'composes end-to-end': verify-phase.md,
  prohibition-probe.md, spec-phase.md authoring, ADR-550 addendum, changeset.
  #1346 now tracks only the node-test causation residual.

190 affected-suite tests green; eslint + tsc clean; size baseline regenerated.
This commit is contained in:
Dave
2026-06-16 14:00:49 -04:00
parent 91bc49c9f1
commit 56d4a1bf39
12 changed files with 190 additions and 37 deletions

View File

@@ -5,6 +5,6 @@ pr: 1314
**Test-tier prohibition fail-first is now MACHINE-PROVEN, not caller-attested** — the deferred literal `regression-must-fail-first` property of ADR-550 Decision 4 (the gap #1259 / PR #1273 left as a tracked follow-up) has landed. The `check prohibition-enforcement` producer (`src/prohibition-enforcement.cts`, compiled by `build:lib` to the gitignored `gsd-core/bin/lib/prohibition-enforcement.cjs`) no longer trusts the caller's `failFirst` attestation: before a clean, non-vacuous pass can dispose a `test`-tier prohibition green, the new `defaultProveFailFirst` prover independently RUNS the wired check against a KNOWN VIOLATION and confirms it goes RED. Attestation is gone from the green AND (`passed = proof.provenFailFirst === true && run.passed === true`); any other outcome — passes-on-violation, can't-prove, throws, times out, or no violation source — **hard-gates in BOTH interactive and autonomous modes** (ADR-550 D4 / D3). The evidence record gains a `failFirstProof` field recording HOW fail-first was proven (FF-07). A caller can no longer green a toothless check. **Test-tier prohibition fail-first is now MACHINE-PROVEN, not caller-attested** — the deferred literal `regression-must-fail-first` property of ADR-550 Decision 4 (the gap #1259 / PR #1273 left as a tracked follow-up) has landed. The `check prohibition-enforcement` producer (`src/prohibition-enforcement.cts`, compiled by `build:lib` to the gitignored `gsd-core/bin/lib/prohibition-enforcement.cjs`) no longer trusts the caller's `failFirst` attestation: before a clean, non-vacuous pass can dispose a `test`-tier prohibition green, the new `defaultProveFailFirst` prover independently RUNS the wired check against a KNOWN VIOLATION and confirms it goes RED. Attestation is gone from the green AND (`passed = proof.provenFailFirst === true && run.passed === true`); any other outcome — passes-on-violation, can't-prove, throws, times out, or no violation source — **hard-gates in BOTH interactive and autonomous modes** (ADR-550 D4 / D3). The evidence record gains a `failFirstProof` field recording HOW fail-first was proven (FF-07). A caller can no longer green a toothless check.
The violation is sourced from a new descriptor field, **`CheckDescriptor.violationFixture`** — an author-supplied path to a known-bad subject. For a `lint-rule` the prover lints that fixture and requires the rule id to appear in the JSON report (the rule must have teeth); for a `node-test` the prover spawns the negative test with the subject injected through the **`GSD_PROHIB_SUBJECT`** env convention and requires a NON-VACUOUS red — `# fail >= 1` AND a failing test named distinctly from the file (`isNonVacuousNodeTestRed`), so a violation fixture that merely CRASHES the test at load is not mistaken for the negative assertion firing red (symmetric with the clean-pass non-vacuity guard). The node-test prover also requires the `violationFixture` to EXIST (resolved against `cwd`) before spawning — a missing/typo'd path fail-CLOSES rather than letting an honest test's ENOENT crash forge a green (symmetric with the lint path's file-result guard). A documented residual — proving the red was *caused by* the violation rather than by the env merely being set — and threading a `check_violation_fixture` scalar through the #1278 projection so the deterministic path can green end-to-end are both tracked as follow-up **#1346**. The `lint-rule` path is fully shippable now and is dogfooded against the in-tree `local/no-source-grep` rule; the `node-test` path ships its mechanism (a fixture-bearing descriptor IS machine-proven) and is exercised by SYNTHETIC temp fixtures — there is no live in-tree `node --test` prohibition to dogfood. `CheckDescriptor.failFirst` is **DEMOTED, not removed** (FF-08): it is kept as a non-authoritative hint so the #1259 route-JSON shape and the `CheckDescriptor` type stay backward-compatible mid-migration, but no path greens on it alone. The green/fail-closed policy in `src/probe-core.cts` (`dispositionForProhibition`, reads only `evidence.length > 0`) is untouched; the evidence array shape is additive. This closes ADR-550's D5d follow-up — see the dated 2026-06-15 ADR-550 addendum. (#1279) The violation is sourced from a new descriptor field, **`CheckDescriptor.violationFixture`** — an author-supplied path to a known-bad subject. For a `lint-rule` the prover lints that fixture and requires the rule id to appear in the JSON report (the rule must have teeth); for a `node-test` the prover spawns the negative test with the subject injected through the **`GSD_PROHIB_SUBJECT`** env convention and requires a NON-VACUOUS red — `# fail >= 1` AND a failing test named distinctly from the file (`isNonVacuousNodeTestRed`), so a violation fixture that merely CRASHES the test at load is not mistaken for the negative assertion firing red (symmetric with the clean-pass non-vacuity guard). The node-test prover also requires the `violationFixture` to EXIST (resolved against `cwd`) before spawning — a missing/typo'd path fail-CLOSES rather than letting an honest test's ENOENT crash forge a green (symmetric with the lint path's file-result guard). The deterministic spec→verify path composes end-to-end: a fourth flat scalar **`check_violation_fixture`** is projected by `projectProhibitions` and read back by `descriptorFromProjection` (rides both kinds), so a prohibition authored with all four `check_*` scalars machine-proves fail-first and greens through the projection alone — zero hand-authoring at verify time (#1278 + #1279 + #1346; round-trip pinned by a fast-check property + CHK-03(D) + an end-to-end COMPOSE capstone). One documented residual remains under **#1346**: the node-test proof confirms the fixture exists and the check reds, but cannot generically prove the red was *caused by* the subject's content rather than by the env merely being set. The `lint-rule` path is fully shippable now and is dogfooded against the in-tree `local/no-source-grep` rule; the `node-test` path ships its mechanism (a fixture-bearing descriptor IS machine-proven) and is exercised by SYNTHETIC temp fixtures — there is no live in-tree `node --test` prohibition to dogfood. `CheckDescriptor.failFirst` is **DEMOTED, not removed** (FF-08): it is kept as a non-authoritative hint so the #1259 route-JSON shape and the `CheckDescriptor` type stay backward-compatible mid-migration, but no path greens on it alone. The green/fail-closed policy in `src/probe-core.cts` (`dispositionForProhibition`, reads only `evidence.length > 0`) is untouched; the evidence array shape is additive. This closes ADR-550's D5d follow-up — see the dated 2026-06-15 ADR-550 addendum. (#1279)
**PR-review flag — `GSD_PROHIB_SUBJECT` + `violationFixture` are PROPOSED, renamable conventions.** Both are net-new surface with ZERO live in-tree consumers (no in-tree node-test prohibition yet; node-test proof runs only on synthetic test fixtures, the real dogfood stays the lint-rule). They are forward-looking scaffolding, so a later rename — or replacing the env var with an argv — is a mechanical, zero-migration find/replace. Surfacing them here so the maintainer can **ratify, rename, or replace them at PR review** with no migration cost, exactly as #1278's ADR addendum was reviewed at PR time. The `failFirst` demotion is likewise open to weighing outright removal; the keep-as-hint rationale is recorded in the ADR addendum. **PR-review flag — `GSD_PROHIB_SUBJECT` + `violationFixture` are PROPOSED, renamable conventions.** Both are net-new surface with ZERO live in-tree consumers (no in-tree node-test prohibition yet; node-test proof runs only on synthetic test fixtures, the real dogfood stays the lint-rule). They are forward-looking scaffolding, so a later rename — or replacing the env var with an argv — is a mechanical, zero-migration find/replace. Surfacing them here so the maintainer can **ratify, rename, or replace them at PR review** with no migration cost, exactly as #1278's ADR addendum was reviewed at PR time. The `failFirst` demotion is likewise open to weighing outright removal; the keep-as-hint rationale is recorded in the ADR addendum.

View File

@@ -113,9 +113,9 @@ 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. 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, tracked:** **Review corrections (#1314 maintainer review) — two soundness items:**
- **node-test fixture-existence guard (was fail-OPEN).** 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(fixture)` before spawning (symmetric fail-closed). **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). **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.
- **`violationFixture` has no projection source (#1278 ↔ #1279 do not yet compose).** `descriptorFromProjection` reconstructs only `{ kind, target, rule? }`; the projected `check_*` scalars carry **no** `violationFixture`. Since green now *requires* a fixture, a prohibition wired purely through the #1278 deterministic path **always hard-gates (fail-closed, safe)** until a `check_violation_fixture` scalar is threaded through — tracked as **#1346**. Until then green requires a hand-supplied `violationFixture`; `verify-phase.md` now states this explicitly rather than implying the projected path produces greens. - **`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-15): optional `check` descriptor on the prohibition item — D3 shape extension (#1278) ## Addendum (2026-06-15): optional `check` descriptor on the prohibition item — D3 shape extension (#1278)

View File

@@ -151,38 +151,39 @@ Splitting these axes keeps the lifecycle enum free of a verification fact and le
prohibition adapter declare `test | judgment` without forking the shared lifecycle enum that prohibition adapter declare `test | judgment` without forking the shared lifecycle enum that
the edge-probe's `explicit | backstop` also uses. the edge-probe's `explicit | backstop` also uses.
## Optional wired-check descriptor (deterministic locate, #1278) ## Optional wired-check descriptor (deterministic locate + machine-proof, #1278 + #1346)
A `resolved`/`test`-tier prohibition MAY carry an **optional `check` descriptor** that names A `resolved`/`test`-tier prohibition MAY carry an **optional `check` descriptor** that names
the wired mechanical check, so verify-phase locates it deterministically instead of inventing 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 — `{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 the author wires it when the negative test or lint rule already exists) and is represented as
**three flat scalar keys** on the `must_haves.prohibitions` item — never a nested `check: {}` **four flat scalar keys** on the `must_haves.prohibitions` item — never a nested `check: {}`
object: object:
- `check_kind` — `node-test` | `lint-rule` (which producer mechanism runs the check). - `check_kind` — `node-test` | `lint-rule` (which producer mechanism runs the check).
- `check_target` — the test file (`node-test`) or the file the rule runs against (`lint-rule`). - `check_target` — the test file (`node-test`) or the file the rule runs against (`lint-rule`).
- `check_rule` — the `ruleId` to filter on, **lint-rule only** (absent for `node-test`). - `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`).
The flat-scalar shape is load-bearing: the shared `parseMustHavesBlock` is a flat parser and a 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 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** 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), and (valid `check_kind` + non-empty `check_target`; `check_rule` only on the lint-rule path;
verify-phase reads them back via `descriptorFromProjection` into the `CheckDescriptor` handed to `check_violation_fixture` only when non-empty), and verify-phase reads them back via
`check prohibition-enforcement`. This closes the **locate** half with **zero manual descriptor `descriptorFromProjection` into the `CheckDescriptor` handed to `check prohibition-enforcement`. This
authoring**. Note the projection carries **no `violationFixture`** (`descriptorFromProjection` closes **both** the locate (#1278) and the machine-proof-fixture (#1346) halves with **zero manual
reconstructs only `{ kind, target, rule? }`); because #1279 machine-proves fail-first against a descriptor authoring**: a prohibition authored with all four scalars greens end-to-end through the
fixture, a prohibition wired purely through this projected path **hard-gates (fail-closed)** until a projection alone.
`check_violation_fixture` scalar is threaded through — tracked as **#1346**. Until then green needs a
hand-supplied `violationFixture`.
**Fail-closed + backward-compat.** A partial descriptor (`lint-rule` missing `check_rule`), an **Fail-closed + backward-compat.** A partial descriptor (`lint-rule` missing `check_rule`), an
unknown `check_kind`, or an **absent** descriptor on a test-tier prohibition falls through to the unknown `check_kind`, an **absent** descriptor, OR a descriptor with **no `check_violation_fixture`**
producer's existing fail-closed locate — never a silent green. A prohibition with no descriptor falls through to the producer's fail-closed paths (`located: false`, or located-but-unprovable) —
parses and disposes byte-identically to today. `failFirst` is **not** sourced from the never a silent green. A prohibition with no descriptor parses and disposes byte-identically to today.
descriptor and is now **demoted** (machine-proven fail-first DELIVERED in #1279 — no path greens on `failFirst` is **not** sourced from the descriptor and is **demoted** (machine-proven fail-first
attestation alone, FF-08); the `dispositionForProhibition` policy is unchanged. The field that gates DELIVERED in #1279 — no path greens on attestation alone, FF-08); the `dispositionForProhibition`
green and is **not yet projected** is `violationFixture` (follow-up #1346). 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.
## Output schema ## Output schema

View File

@@ -365,11 +365,16 @@ 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 - `check_target` — the negative-test file path (for `node-test`), or the path to lint
(for `lint-rule`). (for `lint-rule`).
- `check_rule` — the eslint rule id (e.g. `local/no-source-grep`); `lint-rule` only. - `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
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.
This is a **SOFT capture (CHK-04): a `test`-tier prohibition WITHOUT a descriptor is still 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 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 proceed. It is NOT a hard authoring block; the item simply stays fail-closed/flagged
downstream (an absent/partial descriptor → `descriptorFromProjection` null/under-specified downstream (an absent/partial descriptor — or one with no `check_violation_fixture` —
→ producer fail-closed locate, never green). Do NOT capture `failFirst` here — it is a → `descriptorFromProjection` null/under-specified/fixture-less → producer fail-closed
locate-or-unprovable, never green). Do NOT capture `failFirst` here — it is a
verify-time caller attestation, not a spec-authored field (#1279). verify-time caller attestation, not a spec-authored field (#1279).
- **Dismiss (reason)** → mark `dismissed` with a REQUIRED non-empty reason (PROB-05). The - **Dismiss (reason)** → mark `dismissed` with a REQUIRED non-empty reason (PROB-05). The
reason string is the audit trail; silence is not a valid dismissal. reason string is the audit trail; silence is not a valid dismissal.
@@ -390,8 +395,8 @@ 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 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, 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` / the load-bearing safety property). On a `test`-tier auto-resolution, capture the `check_kind` /
`check_target` / `check_rule` descriptor **only when a wired check is unambiguous**; otherwise `check_target` / `check_rule` / `check_violation_fixture` descriptor **only when a wired check is unambiguous**; otherwise
leave it empty — `--auto` NEVER fabricates a check path (a wrong locate is re-validated and 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: fails closed at the producer, but a fabricated path is still noise to avoid). Log:
`[auto] prohibitions: R resolved, U unresolved`. `[auto] prohibitions: R resolved, U unresolved`.
@@ -402,8 +407,8 @@ runs identically for non-Claude / text-mode hosts.
Populate the `## Prohibitions` section of SPEC.md from the resolved prohibitions (each Populate the `## Prohibitions` section of SPEC.md from the resolved prohibitions (each
`resolved`/`test` row is a checkable negative acceptance criterion; `resolved`/`judgment` `resolved`/`test` row is a checkable negative acceptance criterion; `resolved`/`judgment`
rows route to judgment review; `⚠ UNRESOLVED` rows are flagged as assumptions). A 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` `resolved`/`test` row ALSO carries its captured `check_kind` / `check_target` / `check_rule` /
descriptor when present (so the projection feeds `verify-phase`'s deterministic locate, #1278); `check_violation_fixture` descriptor when present (so the projection feeds `verify-phase`'s deterministic locate + machine-proof, #1278 + #1346);
a `test` row with no captured descriptor is still valid — it stays fail-closed/flagged a `test` row with no captured descriptor is still valid — it stays fail-closed/flagged
downstream rather than blocking authoring. downstream rather than blocking authoring.

View File

@@ -70,17 +70,17 @@ Aggregate all must_haves across plans for phase-level verification.
**Prohibitions (`must_haves.prohibitions`, ADR-550 D3 — the must-NOT sibling block):** When a plan carries `must_haves.prohibitions`, extract each `{ statement, status, verification }` item and route it by `verification` tier in verdict assembly (ADR-550 D4, "B-with-guard", 2026-06-12 maintainer decision). These are NEGATIVE checks (the must-NOT must NOT have happened), distinct from positive `truths`: **Prohibitions (`must_haves.prohibitions`, ADR-550 D3 — the must-NOT sibling block):** When a plan carries `must_haves.prohibitions`, extract each `{ statement, status, verification }` item and route it by `verification` tier in verdict assembly (ADR-550 D4, "B-with-guard", 2026-06-12 maintainer decision). These are NEGATIVE checks (the must-NOT must NOT have happened), distinct from positive `truths`:
- **judgment-tier → mode-dependent soft-gate.** Interactive verify defers each item to the end-of-phase human checkpoint (`human_verify_mode: end-of-phase`). Autonomous verify records a NON-AUTHORITATIVE LLM-judge verdict + a prominent `unverified-prohibition — human review recommended` flag (autonomous completion reads "complete with N flagged prohibitions"). NEVER a silent pass; NEVER a hard halt of an AFK run. - **judgment-tier → mode-dependent soft-gate.** Interactive verify defers each item to the end-of-phase human checkpoint (`human_verify_mode: end-of-phase`). Autonomous verify records a NON-AUTHORITATIVE LLM-judge verdict + a prominent `unverified-prohibition — human review recommended` flag (autonomous completion reads "complete with N flagged prohibitions"). NEVER a silent pass; NEVER a hard halt of an AFK run.
- **test-tier → ENFORCED via `check prohibition-enforcement` (green on pass, hard-gate on miss/fail).** Accept the `verification: test` value (the SPEC↔must_haves.prohibitions projection contract holds — no forced schema change later). For each test-tier item, the verifier builds `request.check` **DETERMINISTICALLY from the projected descriptor** — it does NOT invent `{ kind, target, rule }`. Read the flat scalar keys `check_kind` / `check_target` / `check_rule` off the `must_haves.prohibitions` item and reconstruct the `CheckDescriptor` via the `descriptorFromProjection` adapter in `prohibition-enforcement` (`descriptorFromProjection(projectedItem)` → `{ kind: check_kind, target: check_target, rule?: check_rule }`). Supply a `violationFixture` (a path to a KNOWN-BAD subject) in the request — it is the field that gates **green**, and it is **NOT sourced from the projection today** (the projected `check_*` scalars carry no fixture; threading a `check_violation_fixture` scalar through is the tracked follow-up **#1346**). So with projection alone the item **hard-gates fail-closed** — green currently requires a hand-supplied `violationFixture`. Do NOT rely on `failFirst`: it is DEMOTED (#1279) and greens nothing on its own. Invoke the producer (CLI surface unchanged): - **test-tier → ENFORCED via `check prohibition-enforcement` (green on pass, hard-gate on miss/fail).** Accept the `verification: test` value (the SPEC↔must_haves.prohibitions projection contract holds — no forced schema change later). For each test-tier item, the verifier builds `request.check` **DETERMINISTICALLY from the projected descriptor** — it does NOT invent `{ kind, target, rule }`. Read the flat scalar keys `check_kind` / `check_target` / `check_rule` / `check_violation_fixture` off the `must_haves.prohibitions` item and reconstruct the `CheckDescriptor` via the `descriptorFromProjection` adapter in `prohibition-enforcement` (`descriptorFromProjection(projectedItem)` → `{ kind: check_kind, target: check_target, rule?: check_rule, violationFixture?: check_violation_fixture }`). The `violationFixture` (a path to a KNOWN-BAD subject) is the field that gates **green** and it is **now projected** (`check_violation_fixture`, #1346) — so a prohibition authored with all four scalars greens through the projection alone, **zero hand-authoring at verify time**. Do NOT rely on `failFirst`: it is DEMOTED (#1279) and greens nothing on its own; an item with no projected fixture hard-gates fail-closed. Invoke the producer (CLI surface unchanged):
```bash ```bash
gsd_run check prohibition-enforcement <request.json> 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` now sourced from the projected `check_*` scalars (not author/verifier invention — #1278). 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` is the author-supplied 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, 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:
- **`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**. - **`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`). - **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 (#1278, DELIVERED).** The `check` descriptor's `{ kind, target, rule }` is now sourced **deterministically from the projected `check_kind` / `check_target` / `check_rule` scalars** on the `must_haves.prohibitions` item (authored at `/gsd:spec-phase`, projected by `projectProhibitions`, read back via the `descriptorFromProjection` adapter). So the **locate** half closes with **zero manual descriptor authoring** — the verifier no longer invents `{ kind, target, rule }` (removing the spoofable invent-at-verify-time surface; ADR-857 §147 exogenous grading). **Fail-closed is preserved:** an item with NO projected descriptor — or a PARTIAL one (e.g. a `lint-rule` missing `check_rule`) — makes `descriptorFromProjection` return `null` / an under-specified descriptor, which falls through to the producer's existing fail-closed LOCATE (`located: false`) → flagged-unverified, NEVER green, in BOTH modes. **Important — the projection does NOT yet carry a `violationFixture`** (`descriptorFromProjection` reconstructs only `{ kind, target, rule? }`). Because #1279 now machine-proves fail-first against a `violationFixture`, a prohibition wired **purely through this projected path always hard-gates** (fail-closed, safe) until a `check_violation_fixture` scalar is threaded through — the **tracked follow-up #1346**. Until then a green requires the verifier to hand-supply `violationFixture`; `failFirst` is demoted and greens nothing (#1279, DELIVERED). > **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.
**Option B: Use Success Criteria from ROADMAP.md** **Option B: Use Success Criteria from ROADMAP.md**

View File

@@ -299,6 +299,10 @@ export interface Prohibition {
check_kind?: 'node-test' | 'lint-rule'; check_kind?: 'node-test' | 'lint-rule';
check_target?: string; check_target?: string;
check_rule?: string; check_rule?: string;
// Optional 4th flat scalar (#1346): the path to a KNOWN-BAD subject the #1279 prover runs the check
// 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;
} }
/** /**
@@ -376,6 +380,13 @@ export function projectProhibitions(
if (kind === 'lint-rule' && typeof p.check_rule === 'string' && p.check_rule.trim() !== '') { if (kind === 'lint-rule' && typeof p.check_rule === 'string' && p.check_rule.trim() !== '') {
entry.check_rule = String(p.check_rule); entry.check_rule = String(p.check_rule);
} }
// `check_violation_fixture` (#1346) rides BOTH kinds — it's what the #1279 prover machine-proves
// fail-first against. Emit ONLY a non-empty fixture (a blank one projects absent so green still
// hard-gates downstream — never a partial green); meaningless without the descriptor, so it lives
// inside this well-formed-descriptor branch.
if (typeof p.check_violation_fixture === 'string' && p.check_violation_fixture.trim() !== '') {
entry.check_violation_fixture = String(p.check_violation_fixture);
}
} }
out.push(entry); out.push(entry);
} }

View File

@@ -90,7 +90,8 @@ export interface CheckDescriptor {
* - `null`/`undefined`/non-object input -> `null`. * - `null`/`undefined`/non-object input -> `null`.
* - `check_kind` ABSENT -> `null` (no descriptor -> producer locates nothing -> fail-closed). * - `check_kind` ABSENT -> `null` (no descriptor -> producer locates nothing -> fail-closed).
* - `check_kind` present -> `{ kind: check_kind, target: check_target }`, adding `rule: check_rule` * - `check_kind` present -> `{ kind: check_kind, target: check_target }`, adding `rule: check_rule`
* ONLY when `check_rule` is a non-empty string. * 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).
* - `failFirst` is NEVER sourced from the projection — it stays a verify-time caller attestation * - `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`. * (#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 * - The adapter does NOT strictly validate kind/target/rule: it faithfully reconstructs whatever
@@ -121,6 +122,12 @@ export function descriptorFromProjection(
const rule = scalar(projected.check_rule); const rule = scalar(projected.check_rule);
if (rule.trim().length > 0) descriptor.rule = rule; if (rule.trim().length > 0) descriptor.rule = rule;
} }
// `violationFixture` (#1346) rides BOTH kinds — reconstruct it from `check_violation_fixture` so the
// deterministic #1278 locate path and the #1279 machine-proof COMPOSE: a projected fixture lets the
// default prover green end-to-end with zero hand-authoring. Absent/blank -> no fixture -> the prover
// 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;
return descriptor; return descriptor;
} }

View File

@@ -193,6 +193,7 @@ function renderProhibitionsDoc(entries) {
if (e.check_kind !== undefined) lines.push(` check_kind: ${e.check_kind}`); if (e.check_kind !== undefined) lines.push(` check_kind: ${e.check_kind}`);
if (e.check_target !== undefined) lines.push(` check_target: ${e.check_target}`); 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_rule !== undefined) lines.push(` check_rule: ${e.check_rule}`);
if (e.check_violation_fixture !== undefined) lines.push(` check_violation_fixture: ${e.check_violation_fixture}`);
} }
lines.push('---', '', 'Body.', ''); lines.push('---', '', 'Body.', '');
return lines.join('\n'); return lines.join('\n');
@@ -216,19 +217,20 @@ const pathScalarArb = fc.array(fc.constantFrom(...PATH_CHARS), { minLength: 1, m
const numericScalarArb = fc.nat({ max: 9999999 }).map(String); const numericScalarArb = fc.nat({ max: 9999999 }).map(String);
const targetArb = fc.oneof(pathScalarArb, numericScalarArb); const targetArb = fc.oneof(pathScalarArb, numericScalarArb);
// A fully well-formed descriptor item (resolved test-tier); node-test carries no rule. // 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.
const wellFormedArb = KIND_ARB.chain((kind) => const wellFormedArb = KIND_ARB.chain((kind) =>
fc.record({ target: targetArb, rule: pathScalarArb }).map(({ target, rule }) => { fc.record({ target: targetArb, rule: pathScalarArb, fixture: targetArb }).map(({ target, rule, fixture }) => {
const item = { ...BASE_TIER, check_kind: kind, check_target: target }; const item = { ...BASE_TIER, check_kind: kind, check_target: target, check_violation_fixture: fixture };
if (kind === 'lint-rule') item.check_rule = rule; if (kind === 'lint-rule') item.check_rule = rule;
return { item, kind, target, rule: kind === 'lint-rule' ? rule : undefined }; return { item, kind, target, rule: kind === 'lint-rule' ? rule : undefined, fixture };
}), }),
); );
describe('probe-core property: #1278 check-descriptor round-trip is deterministic across the full string domain', () => { 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', () => { test('a well-formed descriptor survives project -> render -> parse -> descriptorFromProjection (incl. numeric coercion); target/rule reconstruct as strings', () => {
fc.assert( fc.assert(
fc.property(wellFormedArb, ({ item, kind, target, rule }) => { fc.property(wellFormedArb, ({ item, kind, target, rule, fixture }) => {
const projected = pc.projectProhibitions([item]); const projected = pc.projectProhibitions([item]);
if (projected[0].check_kind !== kind) return false; // projector emits the descriptor if (projected[0].check_kind !== kind) return false; // projector emits the descriptor
const reparsed = fm.parseMustHavesBlock(renderProhibitionsDoc(projected), 'prohibitions'); const reparsed = fm.parseMustHavesBlock(renderProhibitionsDoc(projected), 'prohibitions');
@@ -236,6 +238,8 @@ describe('probe-core property: #1278 check-descriptor round-trip is deterministi
if (!d || d.kind !== kind) return false; if (!d || d.kind !== kind) return false;
// target is string-normalized even when parseMustHavesBlock numerically coerced it. // target is string-normalized even when parseMustHavesBlock numerically coerced it.
if (typeof d.target !== 'string' || d.target !== target) return false; 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;
if (kind === 'lint-rule') { if (kind === 'lint-rule') {
return typeof d.rule === 'string' && d.rule === rule; return typeof d.rule === 'string' && d.rule === rule;
} }

View File

@@ -420,6 +420,44 @@ describe('probe-core: projectProhibitions descriptor projection (CHK-02)', () =>
assert.ok(!('check_rule' in projected[0]), 'a lint-rule with no rule projects check_rule absent'); assert.ok(!('check_rule' in projected[0]), 'a lint-rule with no rule projects check_rule absent');
}); });
test('CHK-02(#1346): a node-test descriptor with check_violation_fixture projects it (compose with #1279 proof)', () => {
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' },
]);
assert.equal(projected[0].check_violation_fixture, 'tests/fixtures/autoexec-bad.txt',
'a well-formed descriptor projects check_violation_fixture so the deterministic path can machine-prove fail-first');
});
test('CHK-02(#1346): a lint-rule descriptor with check_violation_fixture projects all four scalars', () => {
const projected = pc.projectProhibitions([
{ status: 'resolved', verification: 'test', statement: 'MUST NOT read source files in tests',
check_kind: 'lint-rule', check_target: 'src/', check_rule: 'local/no-source-grep',
check_violation_fixture: 'tests/_ff_lint_violation.cjs' },
]);
assert.equal(projected[0].check_violation_fixture, 'tests/_ff_lint_violation.cjs');
});
test('CHK-02(#1346): an empty/whitespace check_violation_fixture is NOT projected (fails closed downstream)', () => {
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: ' ' },
]);
assert.ok(!('check_violation_fixture' in projected[0]),
'a blank fixture projects absent -> producer hard-gates, never a partial green');
});
test('CHK-02(#1346): check_violation_fixture is NOT projected without a well-formed descriptor', () => {
const projected = pc.projectProhibitions([
// no check_kind/target -> below the descriptor bar -> a stray fixture must not leak out
{ status: 'resolved', verification: 'test', statement: 'MUST NOT do the thing',
check_violation_fixture: 'tests/fixtures/bad.txt' },
]);
assert.ok(!('check_violation_fixture' in projected[0]),
'a 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', () => { test('CHK-02: an under-specified descriptor (kind but empty/missing target) emits NO check_* keys', () => {
const projected = pc.projectProhibitions([ const projected = pc.projectProhibitions([
// valid kind but empty target -> below the well-formedness bar -> descriptor projects absent // valid kind but empty target -> below the well-formedness bar -> descriptor projects absent

View File

@@ -731,6 +731,41 @@ describe('prohibition-enforcement REAL runner end-to-end (#1259)', () => {
assert.equal(result.located, true, 'the descriptor was located; it just could not be proven fail-first'); assert.equal(result.located, true, 'the descriptor was located; it just could not be proven fail-first');
assert.equal(result.evidence.length, 0, 'no enforcement evidence on a hard-gate'); assert.equal(result.evidence.length, 0, 'no enforcement evidence on a hard-gate');
}); });
test('COMPOSE (#1346): a prohibition projected WITH check_violation_fixture greens end-to-end through the DEFAULT prover+runner (zero hand-authoring)', (t) => {
const enforce = require(ENFORCEMENT_LIB);
const pc = require(path.join(__dirname, '..', 'gsd-core', 'bin', 'lib', 'probe-core.cjs'));
const dir = createTempDir('prohib-compose-1346-');
t.after(() => cleanup(dir));
// The #1278 deterministic-locate path + the #1279 machine-proof now COMPOSE: a prohibition item
// authored with the four flat scalars projects -> reads back into a descriptor that ALREADY carries
// violationFixture -> the default prover greens it with NO hand-supplied fixture in the request.
const negTest = path.join(dir, 'neg.test.cjs');
fs.writeFileSync(negTest,
"const { test } = require('node:test');\n" +
"const assert = require('node:assert');\n" +
"const fs = require('node:fs');\n" +
"const path = require('node:path');\n" +
"test('guards the must-NOT: subject is clean', () => {\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");
fs.writeFileSync(path.join(dir, 'clean-subject.txt'), 'clean\n');
fs.writeFileSync(path.join(dir, 'bad-subject.txt'), 'FORBIDDEN content\n');
// Author the prohibition with all four scalars, then go through the REAL projection + read-back.
const projected = 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' },
])[0];
const descriptor = enforce.descriptorFromProjection(projected);
assert.equal(descriptor.violationFixture, 'bad-subject.txt', 'the projected fixture survived the round-trip');
// NO failFirst, NO hand-supplied violationFixture beyond what the projection carried.
const result = enforce.runProhibitionEnforcement(projected, descriptor, { cwd: dir });
assert.equal(result.status, 'green',
'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');
});
}); });
// ─── #1279 defaultProveFailFirst REAL prover end-to-end (FF-02 / FF-03 / FF-05 / FF-06 / FF-07) ── // ─── #1279 defaultProveFailFirst REAL prover end-to-end (FF-02 / FF-03 / FF-05 / FF-06 / FF-07) ──
@@ -957,6 +992,38 @@ describe('prohibition-enforcement: fail-closed descriptor-from-projection (CHK-0
assert.ok(Array.isArray(result.evidence) && result.evidence.length === 0, 'no evidence on an absent descriptor'); assert.ok(Array.isArray(result.evidence) && result.evidence.length === 0, 'no evidence on an absent descriptor');
}); });
test('CHK-08(#1346): descriptorFromProjection maps check_violation_fixture -> violationFixture (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',
});
assert.equal(descriptor.kind, 'node-test');
assert.equal(descriptor.target, 'tests/neg.test.cjs');
assert.equal(descriptor.violationFixture, 'tests/fixtures/bad-subject.txt',
'the projected check_violation_fixture must reconstruct as violationFixture so #1278 locate + #1279 proof compose');
});
test('CHK-08(#1346): descriptorFromProjection maps check_violation_fixture -> violationFixture (lint-rule)', () => {
const enforce = require(ENFORCEMENT_LIB);
const descriptor = enforce.descriptorFromProjection({
...PROJECTED_TIER, check_kind: 'lint-rule', check_target: 'src/', check_rule: 'local/no-source-grep',
check_violation_fixture: 'tests/_ff_lint_violation.cjs',
});
assert.equal(descriptor.kind, 'lint-rule');
assert.equal(descriptor.rule, 'local/no-source-grep');
assert.equal(descriptor.violationFixture, 'tests/_ff_lint_violation.cjs');
});
test('CHK-08(#1346): no check_violation_fixture -> descriptor carries no violationFixture (fail-closed: green needs a fixture)', () => {
const enforce = require(ENFORCEMENT_LIB);
const descriptor = enforce.descriptorFromProjection({
...PROJECTED_TIER, check_kind: 'node-test', check_target: 'tests/neg.test.cjs',
});
assert.equal(descriptor.violationFixture, undefined,
'absent check_violation_fixture must NOT fabricate a fixture; the default prover then hard-gates (no green)');
});
test('CHK-06(lint-rule missing rule): {check_kind:lint-rule, check_target:src/} (no check_rule) -> located:false, never green', () => { 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 enforce = require(ENFORCEMENT_LIB);
const descriptor = enforce.descriptorFromProjection({ const descriptor = enforce.descriptorFromProjection({

View File

@@ -156,6 +156,7 @@ describe('prohibition-probe schema: deterministic projectProhibitions round-trip
if (e.check_kind !== undefined) lines.push(` check_kind: ${e.check_kind}`); if (e.check_kind !== undefined) lines.push(` check_kind: ${e.check_kind}`);
if (e.check_target !== undefined) lines.push(` check_target: ${e.check_target}`); 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_rule !== undefined) lines.push(` check_rule: ${e.check_rule}`);
if (e.check_violation_fixture !== undefined) lines.push(` check_violation_fixture: ${e.check_violation_fixture}`);
} }
lines.push('---', '', 'Body.', ''); lines.push('---', '', 'Body.', '');
return lines.join('\n'); return lines.join('\n');
@@ -237,6 +238,25 @@ describe('prohibition-probe schema: deterministic projectProhibitions round-trip
'a lint-rule descriptor must survive the writer<->reader bijection with check_kind/target/rule intact'); 'a lint-rule descriptor must survive the writer<->reader bijection with check_kind/target/rule intact');
}); });
test('CHK-03(D) (#1346): a descriptor WITH check_violation_fixture round-trips all four scalars (compose with #1279)', () => {
const pc = require(PROBE_CORE_LIB);
const fm = require(FRONTMATTER_LIB);
const items = [
{
requirement_id: 'R1', category: 'safety', status: 'resolved', verification: 'test',
resolution: null, reason: null, 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',
},
];
const projected = pc.projectProhibitions(items);
assert.equal(projected[0].check_violation_fixture, 'tests/fixtures/autoexec-bad.txt',
'CHK-03(D) RED trigger: projectProhibitions must emit check_violation_fixture so the deterministic path can machine-prove fail-first');
const reparsed = fm.parseMustHavesBlock(renderProhibitionsDoc(projected), 'prohibitions');
assert.deepEqual(reparsed, projected,
'check_violation_fixture must survive project -> write -> parseMustHavesBlock unchanged (the 4th flat scalar)');
});
test('CHK-03(C): a mixed list — descriptor test-tier, descriptor-less judgment, dismissed — all round-trip; the descriptor-less item gains NO check_* keys', () => { test('CHK-03(C): a mixed list — descriptor test-tier, descriptor-less judgment, dismissed — all round-trip; the descriptor-less item gains NO check_* keys', () => {
const pc = require(PROBE_CORE_LIB); const pc = require(PROBE_CORE_LIB);
const fm = require(FRONTMATTER_LIB); const fm = require(FRONTMATTER_LIB);

View File

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