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

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

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

Closes #1346

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

View File

@@ -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)

View File

@@ -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.

View File

@@ -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

View File

@@ -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.

View File

@@ -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**

View File

@@ -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);
}

View File

@@ -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 };

View File

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

View File

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

View File

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

View File

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