Skip to content

fix(proof): enforce EvidenceRule.test_id/must_pass in proof runner (#729) - #802

Merged
frankbria merged 3 commits into
mainfrom
fix/729-enforce-evidence-rules
Jul 3, 2026
Merged

fix(proof): enforce EvidenceRule.test_id/must_pass in proof runner (#729)#802
frankbria merged 3 commits into
mainfrom
fix/729-enforce-evidence-rules

Conversation

@frankbria

@frankbria frankbria commented Jul 3, 2026

Copy link
Copy Markdown
Owner

Closes #729

Problem

Capture generates per-requirement evidence rules and the ledger serializes them, but _run_gate just ran the whole gate suite — a requirement was SATISFIED by any green pytest run even if the named regression test was never written. The "enforced forever" loop enforced nothing specific.

Changes

  • EvidenceRule.gate: each rule now carries its owning gate (stamped in suggest_evidence_rules, persisted in the ledger; legacy rows derive it from the test_id prefix convention, unknown prefixes stay None/unenforced).
  • gates.run / _run_pytest test_selector: optional pytest -k expression; with a selector, exit 5 ("no tests matched") is FAILED, not an empty-suite pass.
  • _run_gate(workspace, gate, rules): every must_pass rule with a pytest-style test_id runs as its own scoped pytest invocation — missing → FAILED — named test missing, failing → FAILED, and only passing named tests satisfy. The gate's own runner still runs when it isn't pytest (SEC = ruff and its test_sec_* rules). must_pass=False rules are informational. Gates with no rules keep prior behavior; unmapped gates stay UNVERIFIABLE ([P1.1] PROOF9: give the 6 unrunnable gates real runners or an explicit UNVERIFIABLE state (not perpetual fail) #728).
  • Stub/rule alignment: UNIT stub now defines test_unit_<slug> (was test_<slug>, which could never satisfy its own rule), and stubs share obligations.slugify with rules (slug truncation was [:50] vs [:60]).

Design notes (divergence from the CodeRabbit issue plan)

No pytest-json-report: one scoped pytest run per rule gives the full signal from exit codes alone (5=missing, 1=failing, 0=passing), avoiding the json-report flakiness the plan itself warned about.

Review

Pre-PR cross-family review (codex) found 2 issues, both fixed and covered by tests:

  • High: SEC gate (ruff-backed) silently ignored its must_pass pytest rules
  • Medium: UNIT stub function name didn't match its evidence rule's test_id

Tests

19 new tests in tests/core/test_proof_evidence_rules.py (missing/passing/failing named test, must_pass=False, SEC dual enforcement, stub/rule alignment, legacy hydration, -k threading). Existing _run_gate stubs updated for the new signature. Full non-e2e suite: 3980+ passed; ruff clean.

Known limitations

  • -k matching is substring-based: a rule named test_unit_login also matches test_unit_login_extra. Acceptable for presence-detection; exact node IDs would require tests to exist at capture time.
  • Legacy rules whose test_id has no recognizable gate prefix remain unenforced (safe fallback, logged in output as absent).
  • E2E/VISUAL/A11Y/PERF/DEMO/MANUAL rules stay UNVERIFIABLE per [P1.1] PROOF9: give the 6 unrunnable gates real runners or an explicit UNVERIFIABLE state (not perpetual fail) #728 — enforcement applies to runnable gates only.
  • Requirements captured before this PR may pair a 60-char rule slug with an old 50-char stub function name (titles >50 chars): enforcement will correctly report "named test missing" until the stub function is renamed to the rule's test_id. A per-requirement warning also lists any rules whose gate could not be resolved.

Summary by CodeRabbit

  • New Features

    • Added support for running a selected subset of tests, making targeted checks easier.
    • Requirement evidence now carries through more consistently, including preserved behavior for older stored records.
  • Bug Fixes

    • Improved handling when a selected test cannot be found, so missing matches are reported as failures instead of being treated as acceptable.
    • Strengthened requirement validation so named regression tests must actually exist and pass when required.

)

A requirement is no longer SATISFIED by any green pytest run — each
must_pass evidence rule is enforced by a pytest run scoped to its
test_id (-k), and a named test that was never written is a FAILED
obligation.

- EvidenceRule gains an owning gate field, stamped at capture,
  persisted in the ledger; legacy rows derive it from the test_id
  prefix convention
- gates.run/_run_pytest accept a test_selector; with a selector,
  "no tests matched" (exit 5) is a failure, not an empty suite
- _run_gate enforces pytest-style rules on every runnable gate: SEC
  runs ruff AND its test_sec_* rules (codex review finding)
- UNIT stub now defines test_unit_<slug> to match its rule, and stub
  slugs share obligations.slugify with rules (was [:50] vs [:60])
- drop unused pytest import in tests/test_new_feature.py (pre-existing
  ruff failure)
@coderabbitai

coderabbitai Bot commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

You’ve reached a temporary PR review limit under our Fair Usage Limits Policy.

Your recent review volume is higher than typical usage, so adaptive limits are currently applied.

Next review available in: 48 minutes

Your organization has reached its usage spending cap. Adjust your spending cap in the billing tab.

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: d12e9170-f147-43d3-b676-6dee26b32af0

📥 Commits

Reviewing files that changed from the base of the PR and between 2e4c07c and aa40ea8.

📒 Files selected for processing (3)
  • codeframe/core/proof/ledger.py
  • codeframe/core/proof/runner.py
  • tests/core/test_proof_evidence_rules.py

Walkthrough

This PR implements enforcement of EvidenceRule.test_id/must_pass in the proof runner (issue #729). It adds a test_selector parameter to pytest gate execution, persists a gate field on EvidenceRule, adds shared slug/gate-derivation helpers, and updates the runner to enforce named tests per rule instead of accepting any green whole-suite run.

Changes

Evidence Rule Enforcement

Layer / File(s) Summary
pytest gate test_selector support
codeframe/core/gates.py
run()/_run_pytest() accept test_selector for -k scoped runs; exit code 5 becomes FAILED when a selector was given.
EvidenceRule gate field and helpers
codeframe/core/proof/models.py, codeframe/core/proof/obligations.py, codeframe/core/proof/stubs.py
Adds EvidenceRule.gate, TEST_ID_PREFIXES, gate_from_test_id, slugify; suggest_evidence_rules and stub generation use these helpers.
EvidenceRule gate persistence
codeframe/core/proof/ledger.py
Serializes/deserializes gate, deriving it from test_id for legacy rows.
Runner enforcement of evidence rules
codeframe/core/proof/runner.py
_run_gate accepts rules, runs scoped pytest per enforced rule, treats missing named tests as failures, warns on unresolved gates, computes GateOutcome from enforced results.
Test updates and new coverage
tests/cli/test_proof_commands.py, tests/core/test_proof_runner_outcomes.py, tests/core/test_proof_evidence_rules.py
Updates mock signatures for _run_gate(rules) and adds a comprehensive new test suite for evidence rule enforcement end-to-end.

Estimated code review effort: 3 (Moderate) | ~30 minutes

Sequence Diagram(s)

sequenceDiagram
  participant run_proof
  participant _run_gate
  participant pytest_runner as gates.run (pytest)
  participant GateRunner

  run_proof->>run_proof: compute gate_rules for obligation gate
  run_proof->>_run_gate: _run_gate(workspace, gate, gate_rules)
  loop each enforced must_pass pytest rule
    _run_gate->>pytest_runner: run(test_selector=rule.test_id)
    pytest_runner-->>_run_gate: exit_code, output
    alt exit_code == 5
      _run_gate->>_run_gate: mark rule FAILED (missing test)
    end
  end
  alt no pytest-backed rules enforced for this gate
    _run_gate->>GateRunner: run whole gate suite
    GateRunner-->>_run_gate: result
  end
  _run_gate-->>run_proof: GateOutcome (PASSED/FAILED)
  run_proof->>run_proof: update requirement status
Loading

Possibly related PRs

  • frankbria/codeframe#386: Both PRs modify pytest gate execution and exit-code-5 handling in codeframe/core/gates.py.
  • frankbria/codeframe#447: This PR extends the PROOF9 subsystem introduced there with fuller evidence-rule enforcement in the runner and ledger.

Poem

A rabbit hops through gates once loose,
Now -k selects the test in use.
No named test? No satisfied claim —
gate field remembers whence rules came.
Enforced at last, obligations true,
Thump thump — the proof runner's brand new! 🐇✅

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Linked Issues check ✅ Passed The PR satisfies #729 by running scoped pytest checks per EvidenceRule.test_id and failing when the named test is missing.
Out of Scope Changes check ✅ Passed The extra changes support the same enforcement flow and do not appear unrelated to the linked issue.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly matches the main change: proof runner enforcement of EvidenceRule.test_id and must_pass.
📋 Issue Planner

Built with CodeRabbit's Coding Plans for faster development and fewer bugs.

View plan used: #729

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/729-enforce-evidence-rules

Comment @coderabbitai help to get the list of available commands.

@frankbria

Copy link
Copy Markdown
Owner Author

Demo verification (Phase 11 — outcome evidence)

Throwaway project with a green suite (test_unrelated_always_green passes) and one captured requirement (cf proof capture → REQ-0001, LOGIC_BUG → unit+contract obligations, rules test_unit_login_rejects_empty_password / test_contract_...).

1. Named test missing → obligation FAILS despite green suite (the acceptance criterion):

│ REQ-0001 │ unit     │ FAIL   │
│ REQ-0001 │ contract │ FAIL   │

Evidence artifact (REQ-0001_unit_*.txt):

test_unit_login_rejects_empty_password: FAILED — named test missing (not collected)

2. Named tests written → obligations PASS, requirement SATISFIED:

│ REQ-0001 │ unit     │ PASS   │
│ REQ-0001 │ contract │ PASS   │
  Open: 0 / Satisfied: 1 / Waived: 0

Artifact now reads test_unit_login_rejects_empty_password: passed.

Before this PR, step 1 reported PASS (whole-suite green satisfied the requirement).

@claude

claude Bot commented Jul 3, 2026

Copy link
Copy Markdown

Code Review: fix(proof) enforce EvidenceRule.test_id/must_pass (#729)

This is a clean, well-targeted fix that addresses the core soundness hole: a green whole-suite pytest run was satisfying any PROOF9 obligation, even one whose named regression test was never written. The design (one scoped pytest invocation per rule, exit-code-5 = FAILED) avoids external JSON-report tooling and maps directly onto pytest semantics.

What is working well

  • Core logic is correct. The gate-to-rule mapping in run_proof (gate_rules = [r for r in req.evidence_rules if r.gate == obl.gate]) cleanly scopes each gate to only its own rules.
  • SEC dual enforcement is explicitly covered. The branch core_gate_name != "pytest" or not enforced lets ruff still run for SEC even after named pytest rules pass, and the test test_sec_gate_enforces_pytest_rule_alongside_ruff pins this.
  • Backward compat is thought through. Legacy rows with no gate field derive it from test_id prefix; unrecognizable prefixes get gate=None (safe fallback, not enforced).
  • Slug unification fixes a real bug. The [:50] vs [:60] drift between stubs._slugify and obligations.suggest_evidence_rules meant generated stubs could never satisfy the named test requirement. Delegating to one obligations.slugify closes that.
  • 19 new tests. Coverage spans missing/failing/passing named test, must_pass=False, multi-rule mixed outcomes, SEC dual path, legacy hydration, and the run_proof end-to-end case.

Issues and suggestions

1. Silent unenforcement of gate=None rules

In run_proof, gate_rules is filtered by r.gate == obl.gate. Any rule with gate=None (legacy with unrecognizable prefix) silently drops out of every gate enforcement pass. The PR description says these are a safe fallback, but the output artifact and logs give no indication that rules were skipped. A logger.warning before iterating obligations would make this visible:

unresolved = [r for r in req.evidence_rules if r.gate is None]
if unresolved:
    logger.warning(
        "REQ %s: %d evidence rule(s) with unresolvable gate prefix skipped: %s",
        req.id, len(unresolved), [r.test_id for r in unresolved]
    )

2. The check is None branch in _run_gate is a latent false-positive path

check = result.checks[0] if result.checks else None
if check is not None and check.exit_code == 5:
    ...
elif result.passed:   # reached when check is None
    lines.append(f"{rule.test_id}: passed")

If core_gates.run ever returns an empty checks list, a rule is marked passed based on GateResult.passed alone with no exit-code validation. Since exit-code precision is the whole point of this PR, an explicit guard is worth adding:

elif check is None:
    lines.append(f"{rule.test_id}: FAILED - no gate check returned")
    all_passed = False

3. Pre-PR stub/rule slug length mismatch is not mitigated

The PR correctly unifies slug truncation at 60 chars going forward. But requirements captured before this PR have evidence rules with 60-char slugs (from obligations.py) paired with stubs using 50-char slugs (from the old stubs._slugify). For descriptions longer than 50 chars, existing stubs define functions that will never match their rule, and enforcement will report "named test missing" on the next cf proof run. A note in the PR body or a runtime warning would help existing workspace users.

4. Sequential subprocesses per rule

One pytest subprocess per enforced rule is correct for isolation. For the typical 1-3 rules case this is fine. Worth a brief note in the _run_gate docstring so a future reader does not try to optimize by collapsing them back into a single run (which would break per-rule outcome tracking).

5. Missing test: gate=None rule behavior in full run_proof path

test_legacy_rules_derive_gate_from_prefix covers ledger deserialization. There is no test confirming that a rule with gate=None is silently ignored (not errored, not double-counted) when run_proof iterates obligations. A short test pinning the no-op behavior would complete the coverage story for the safe-fallback claim.


Nits (non-blocking)

  • The variable result is reused inside the for rule in enforced loop and again in the outer if core_gate_name != "pytest" or not enforced block. Renaming the inner one rule_result would reduce re-read friction.
  • gate_from_test_id iterates TEST_ID_PREFIXES on every legacy deserialization call. The dict iteration order is now load-bearing (first match wins if two prefixes ever share a stem). A comment noting this would help.

Summary

The fix is correct and the test coverage is solid. Most actionable before merge: #1 (log unresolved rules) and #2 (guard the check is None path) - both are small, self-contained changes. Issue #3 (slug migration) is worth a note somewhere even if not a code change. Everything else is informational.

…e=None no-op

Addresses claude-review findings on #802: explicit FAILED when gates.run
returns no checks, per-requirement warning for rules with no resolvable
gate, docstring note on per-rule subprocesses, and a test pinning that
gate=None rules fall back to whole-suite behavior.
@claude

claude Bot commented Jul 3, 2026

Copy link
Copy Markdown

Follow-up Review

The second commit (2e4c07cc -- "review: guard empty gate checks, warn on unenforceable rules...") directly addresses the actionable issues from my previous review. Running through them:

Issue 1 -- Silent unenforcement of gate=None rules -- addressed

The warning block is now present in run_proof before obligation iteration. Operators will now see these in logs when legacy rules cannot be mapped to a gate.

Issue 2 -- Latent false-positive on empty checks list -- addressed

The check is None guard is now an explicit FAILED branch, not a fall-through to result.passed. Exit-code precision is preserved end-to-end.

Issue 4 -- Sequential subprocess rationale -- addressed

The _run_gate docstring now explicitly documents why rules must not be collapsed into a single -k "a or b" run: per-rule exit codes are what distinguish "named test missing" (exit 5) from "collected but failing" (exit 1).

Issue 5 -- Missing test for gate=None in full run_proof path -- addressed

test_gate_none_rules_are_ignored_not_errored in TestRunProofEnforcement verifies no-crash, no scoped runs, and whole-suite PASS -- the safe-fallback contract is now pinned.


Still open from previous review

Issue 3 -- Slug migration gap for pre-PR workspaces

Acknowledged in the PR description's "Known limitations" but no runtime signal. Requirements captured before this PR with stubs generated by the old [:50] truncation will silently report "named test missing" on the next cf proof run if the description was longer than 50 chars. This is safe (conservative, not wrong), but the cause would be easy to miss. Worth tracking in a follow-up issue; a hint in the exit-5 log line would save a debugging session.


Remaining nits (non-blocking)

  • result variable reuse: the loop variable result in for rule in enforced shares a name with the outer result in the non-pytest gate runner block. Renaming the inner one rule_result would reduce re-read friction.
  • gate_from_test_id dict ordering: first-match-wins iteration over TEST_ID_PREFIXES is currently safe because no prefix is a substring of another. A brief comment would help a future maintainer adding a new gate prefix.

Overall

The core fix is sound and the follow-up commit closes all three high-priority items from my previous review. The test suite is comprehensive -- missing/passing/failing named test, must_pass=False, SEC dual path, multi-rule mixed outcome, legacy hydration, gate=None no-op. Ready to merge.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
codeframe/core/gates.py (1)

482-505: 🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

-k can pass the wrong test here. pytest -k is substring-based, so rule.test_id can match a different test and make the gate pass even when the exact named test is missing. Pass a full node ID or do an exact collect check instead.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@codeframe/core/gates.py` around lines 482 - 505, The _run_pytest gate
currently uses pytest -k via test_selector, which can match the wrong test by
substring and falsely pass the gate. Update _run_pytest to avoid -k for exact
test validation: either pass a full pytest node id when selecting a specific
test, or perform an exact collect check before running pytest. Keep the change
localized around the test_selector handling in _run_pytest so rule.test_id
cannot match a different test.
🧹 Nitpick comments (2)
tests/core/test_proof_evidence_rules.py (1)

149-269: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

LGTM! Enforcement scenarios (missing/passing/failing named tests, must_pass=False, mixed rules, SEC+ruff coexistence, unmapped gate) thoroughly exercise _run_gate. Note: these mocks return GateStatus.FAILED/PASSED explicitly and never exercise the SKIPPED path flagged in runner.py's _run_gate review — consider adding a case mocking pytest-unavailable (SKIPPED, exit_code=None) to lock in the fix suggested there.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/core/test_proof_evidence_rules.py` around lines 149 - 269, Add a test
that exercises the `_run_gate` SKIPPED branch in `codeframe.core.proof.runner`
by mocking `codeframe.core.gates.run` to return a pytest-unavailable result with
`GateStatus.SKIPPED` and `exit_code=None`. Update `TestRunGateEnforcement` with
a case similar to the existing `_rule`-driven tests, and assert that `_run_gate`
handles the skipped result as expected rather than treating it like a normal
pass/fail outcome. This will lock in the behavior for the `SKIPPED` path
referenced in `_run_gate`.
codeframe/core/proof/runner.py (1)

123-129: 🚀 Performance & Scalability | 🔵 Trivial

Each enforced rule triggers a full core_gates.run() preflight (dependency install check) plus its own GATES_STARTED/GATES_COMPLETED events.

For a requirement with several must_pass rules on the same gate, this multiplies dependency-install checks and event-log writes N times per proof run (on top of N per requirement across the whole run). The per-rule-subprocess design is explicitly justified in the docstring for exit-code precision, but the dependency preflight and event emission overhead look like an unintended side effect of reusing the full core_gates.run() entry point rather than a lower-level runner. Consider hoisting a single dependency check per run_proof invocation (or passing auto_install_deps=False for the scoped per-rule calls once dependencies are known to be installed) to avoid repeated subprocess overhead and event-log noise.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@codeframe/core/proof/runner.py` around lines 123 - 129, Each enforced rule is
currently invoking the full core_gates.run() path, which repeats dependency
preflight and emits GATES_STARTED/GATES_COMPLETED events for every rule. In
run_proof, update the per-rule loop over enforced to avoid redoing that shared
setup: either perform one dependency-install check once before the loop and
reuse it, or call core_gates.run() with auto_install_deps disabled for the
scoped rule runs after dependencies are known to be installed. Keep the existing
per-rule subprocess behavior for exit-code precision, but reduce the repeated
overhead and event noise.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@codeframe/core/proof/ledger.py`:
- Around line 199-211: The persisted gate parsing in _evidence_rules_from_json
is too strict and can crash requirement loading when a stored gate string is
invalid. Update the EvidenceRule construction to defensively handle bad
d["gate"] values by catching invalid Gate(...) conversion and falling back to
gate_from_test_id(d["test_id"]) just like the legacy path, so
_row_to_requirement and get_requirement can continue loading other rows.

In `@codeframe/core/proof/runner.py`:
- Line 118: The runner logic in runner.py drops must_pass rules whose test_id
does not start with "test_", so they are neither enforced nor reported. Update
the enforcement path around enforced/all_passed to detect any must_pass rule
that is not eligible for execution, and treat it as a failure or explicit
unenforceable condition instead of silently skipping it. Make sure the status is
reflected in the artifact/log output alongside the existing rule handling in
run_proof and the informational loop so the rule is visible even when its
test_id prefix is nonstandard.
- Around line 123-141: The proof enforcement loop in runner.py is using
result.passed for each enforced rule, which can incorrectly treat SKIPPED checks
as satisfied; update the logic in the rule-processing block inside the
enforcement loop to require an explicit GateStatus.PASSED on the selected check
instead of relying on core_gates.run()’s aggregate passed flag, and make any
non-PASSED status (including SKIPPED) fail the rule while preserving the
existing missing-check and exit_code handling.

---

Outside diff comments:
In `@codeframe/core/gates.py`:
- Around line 482-505: The _run_pytest gate currently uses pytest -k via
test_selector, which can match the wrong test by substring and falsely pass the
gate. Update _run_pytest to avoid -k for exact test validation: either pass a
full pytest node id when selecting a specific test, or perform an exact collect
check before running pytest. Keep the change localized around the test_selector
handling in _run_pytest so rule.test_id cannot match a different test.

---

Nitpick comments:
In `@codeframe/core/proof/runner.py`:
- Around line 123-129: Each enforced rule is currently invoking the full
core_gates.run() path, which repeats dependency preflight and emits
GATES_STARTED/GATES_COMPLETED events for every rule. In run_proof, update the
per-rule loop over enforced to avoid redoing that shared setup: either perform
one dependency-install check once before the loop and reuse it, or call
core_gates.run() with auto_install_deps disabled for the scoped rule runs after
dependencies are known to be installed. Keep the existing per-rule subprocess
behavior for exit-code precision, but reduce the repeated overhead and event
noise.

In `@tests/core/test_proof_evidence_rules.py`:
- Around line 149-269: Add a test that exercises the `_run_gate` SKIPPED branch
in `codeframe.core.proof.runner` by mocking `codeframe.core.gates.run` to return
a pytest-unavailable result with `GateStatus.SKIPPED` and `exit_code=None`.
Update `TestRunGateEnforcement` with a case similar to the existing
`_rule`-driven tests, and assert that `_run_gate` handles the skipped result as
expected rather than treating it like a normal pass/fail outcome. This will lock
in the behavior for the `SKIPPED` path referenced in `_run_gate`.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 93d500b4-1ab5-43bc-b6d7-c2cf26104941

📥 Commits

Reviewing files that changed from the base of the PR and between 8a98e63 and 2e4c07c.

📒 Files selected for processing (9)
  • codeframe/core/gates.py
  • codeframe/core/proof/ledger.py
  • codeframe/core/proof/models.py
  • codeframe/core/proof/obligations.py
  • codeframe/core/proof/runner.py
  • codeframe/core/proof/stubs.py
  • tests/cli/test_proof_commands.py
  • tests/core/test_proof_evidence_rules.py
  • tests/core/test_proof_runner_outcomes.py

Comment thread codeframe/core/proof/ledger.py
Comment thread codeframe/core/proof/runner.py
Comment thread codeframe/core/proof/runner.py
- invalid persisted gate values fall back to test_id prefix derivation
  instead of crashing requirement load
- SKIPPED/ERROR pytest checks fail enforcement (pytest unavailable is
  not proof)
- must_pass rules without a pytest-style test_id fail loudly instead of
  vanishing silently
@frankbria
frankbria merged commit 73eaf75 into main Jul 3, 2026
10 checks passed
@frankbria
frankbria deleted the fix/729-enforce-evidence-rules branch July 3, 2026 21:45
@claude

claude Bot commented Jul 3, 2026

Copy link
Copy Markdown

Follow-up Review (Round 3)

Previous rounds closed Issues 1, 2, 4, 5 — the warning on gate=None rules, the explicit empty-checks guard, the subprocess rationale docstring, and the gate=None integration test. Good work closing those quickly.

Three items remain open from the earlier reviews and CodeRabbit's formal pass.


1. SKIPPED path still has no test (CodeRabbit nitpick, still open)

_run_gate treats any result where check.status is not PASSED as FAILED — which includes GateStatus.SKIPPED (the "pytest not installed" path). The existing mocks never return SKIPPED, so that branch is untested. It's a two-line addition:

# In TestRunGateEnforcement
def test_must_pass_rule_fails_when_gate_is_skipped(self, tmp_path):
    ws = _make_workspace(tmp_path)
    rule = EvidenceRule(test_id="test_unit_login", must_pass=True, gate=Gate.UNIT)
    skipped_result = MagicMock()
    skipped_result.checks = [MagicMock(status=GateStatus.SKIPPED, exit_code=None, name="pytest")]
    skipped_result.passed = False
    with patch("codeframe.core.proof.runner.core_gates.run", return_value=skipped_result):
        outcome, output = _run_gate(ws, Gate.UNIT, [rule])
    assert outcome == GateOutcome.FAILED

This should be a quick addition before merge.


2. Repeated preflight overhead (CodeRabbit nitpick)

For a requirement with N must_pass rules on the same gate, the current loop calls core_gates.run() N times — meaning the auto_install_deps check, the GATES_STARTED/GATES_COMPLETED event emissions, and any other gate preamble run N times. For most repos this is minor noise, but it does mean the event log gets N duplicate start/complete pairs per gate per requirement.

The targeted fix is to pass auto_install_deps=False on all scoped runs after the first (or hoist a single dependency preflight before the loop). This is not a correctness issue, but worth tracking as a follow-up if not addressed here.


3. Pre-PR slug migration gap (from Round 1, still open)

Requirements captured before this PR that have titles longer than 50 chars will silently remain unenforced — the old stubs used a 50-char slug, the new EvidenceRule.test_id uses 60 chars, so the names don't match. The code path handles this safely (gate=None → warning log → not enforced), but a user running cf proof run on an older workspace gets no actionable signal.

The PR description notes this limitation. If there's no plan to add a migration or CLI hint, a tracking issue would keep it visible for users who hit this after upgrading.


Summary

The core enforcement logic is solid and the fix for #729 is correct. The SKIPPED path test (Item 1) is the only remaining change I'd want to see before merge — it covers a real code branch added in this PR. Items 2 and 3 are non-blocking but worth filing as follow-ups.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[P1.2] PROOF9: enforce EvidenceRule.test_id/must_pass (currently persisted but never read)

1 participant