feat(proof): report why a proof run produced no results (#1138) - #1150
Conversation
`run_proof` returned only `dict[req_id, [(gate, outcome)]]`, and an empty dict has six causes. The runner computed them and threw them away — `scope_skipped` got as far as a log line — so `cf proof run` had to infer the reason from the flags. Four consecutive review rounds on #1137 caught it inferring wrong, each time about a different cause. `run_proof_with_diagnostics` returns `(results, ProofRunDiagnostics)`. `run_proof` stays as a one-line wrapper, so the ~60 call sites that only want the dict are untouched — the additive approach the issue asks for. `EmptyReason` distinguishes: no requirements, excluded by status, excluded by scope, excluded by the --gate filter, excluded by enabled_gates, excluded by the two filters jointly, no obligations defined, and MIXED. Diagnostics carries the per-bucket requirement ids, not just a verdict: with three requirements dropped for three reasons, any single reason is wrong, so `reason` collapses only when the collapse is honest and `describe()` enumerates otherwise. The CLI now names the cause and gives the matching remedy instead of printing a candidate list. `_report_scope_skipped` is unchanged (#922's warning still fires); the empty-ledger exit-2 behaviour from #1118 is unchanged. The joint-filter case came from review and is worth spelling out: with obligations {unit, sec}, `--gate unit` and `enabled_gates: ["sec"]`, NEITHER filter excludes everything on its own, so both `all(...)` checks were false and the requirement fell into no bucket — surfacing as an unclassified MIXED with no hint at all. Each filter is now tested for sufficiency separately, and the joint case has its own reason and its own remedy. Updating tests/cli/test_proof_empty_ledger_1118.py was not optional: it patched `runner.run_proof`, which the CLI no longer calls, so the patch quietly stopped applying and the tests began asserting against a live run. That failed loudly here, but it is the same class of thing as #1077 — a guard patching a function nothing calls. Most of those cases are now driven for real instead: a waived ledger, a --gate that matches no obligation, and a requirement with no obligations are all reachable states. Only the scope filter still needs a patch, because with no working-tree changes the detector fails closed.
|
Claude finished @frankbria's task in 6m 28s —— View job Bug-hunting review complete
Scope: concrete defects only (logic errors, crashes, data loss, contract breaks, security). Style/coverage/architecture is covered by the general review. Method: read the full diff, then opened Areas scrutinized and cleared:
✅ GLM review: no defects found. |
Review:
|
Demo — each reason named, on a real workspaceNot asserted from tests: 1. Empty ledger — unchanged #1118 behaviour, exit 2: 2. Out of scope — the requirement is scoped to Before this PR that same state printed 3. 4. All waived — scope is never evaluated here, because the runner short-circuits before it. Naming scope was one of the four wrong guesses: Each output names one cause and one remedy. No candidate lists. Acceptance criteria
Checks
|
Closes #1138.
What
run_proofreturned onlydict[req_id, [(gate, outcome)]], and an empty dict has six causes. The runner computed every one of them and threw them away —scope_skippedgot as far as a log line — socf proof runhad to infer the reason from the flags. Four consecutive review rounds on #1137 caught it inferring wrong, each time about a different cause. It ended up printing a candidate list: honest, and useless.run_proof_with_diagnostics(...) → (results, ProofRunDiagnostics).run_proofstays as a one-line wrapper, so the ~60 call sites that only want the dict are untouched — the additive approach the issue asks for.The reasons
EmptyReasonNO_REQUIREMENTSEXCLUDED_BY_STATUSEXCLUDED_BY_SCOPEEXCLUDED_BY_GATE_FILTER--gateexcluded every obligationEXCLUDED_BY_CONFIGenabled_gatesdidEXCLUDED_BY_FILTER_COMBINATIONNO_OBLIGATIONSMIXEDDiagnostics carries the per-bucket requirement ids, not just a verdict. With three requirements dropped for three reasons any single reason is wrong, so
reasoncollapses only when the collapse is honest, anddescribe()enumerates otherwise.The CLI names the cause and gives the matching remedy.
_report_scope_skippedis unchanged (#922's warning still fires) and #1118's empty-ledger exit-2 is unchanged.Review finding, worth spelling out
codex reviewcaught a real gap in the first version:Correct. It surfaced as an unclassified
MIXEDwith no hint at all — the exact failure this PR exists to remove, in a case I had not thought of. Each filter is now tested for sufficiency separately, and the joint case gets its own reason and its own remedy ("drop--gate, or add that gate toenabled_gates— each filter alone leaves work to do, but they do not overlap"). Three tests cover it, including one asserting the joint bucket does not swallow the simple single-filter cases.A test file that had stopped testing
tests/cli/test_proof_empty_ledger_1118.pypatchedrunner.run_proof, which the CLI no longer calls. The patch quietly stopped applying and those tests began asserting against a live run. It failed loudly here, but it is the same class as #1077 — a guard patching a function nothing calls.Most of them are now driven for real rather than patched: a waived ledger, a
--gatethat matches no obligation, and a requirement with no obligations are all reachable states. Only the scope filter still needs a patch, because with no working-tree changes the detector fails closed and evaluates everything.Testing
tests/core/test_proof_run_diagnostics_1138.py— one test per reason, as the AC asks, plus the joint-filter case, the mixed case, and the back-compat wrappertests/cli/test_proof_empty_ledger_1118.py— rewritten for the new contract; 34 tests pass across both filesruffreported in a commentKnown limitation
The v2 proof router still returns results only. It does not currently explain an empty run to the web UI either, but that is a separate surface with its own response model and no acceptance criterion here — the issue's ask is
run_proofandcf proof run. The diagnostics object is what a follow-up would serialise.