Skip to content

fix(proof): explicit UNVERIFIABLE state for gates without runners (#728)#801

Merged
frankbria merged 3 commits into
mainfrom
fix/728-proof9-unverifiable-state
Jul 3, 2026
Merged

fix(proof): explicit UNVERIFIABLE state for gates without runners (#728)#801
frankbria merged 3 commits into
mainfrom
fix/728-proof9-unverifiable-state

Conversation

@frankbria

@frankbria frankbria commented Jul 3, 2026

Copy link
Copy Markdown
Owner

Closes #728.

Problem

_GATE_TO_CORE only maps UNIT/CONTRACT→pytest and SEC→ruff; the other 6 gates (e2e, visual, a11y, perf, demo, manual) returned (False, "no automated runner — cannot verify"), so most captured glitches could never be SATISFIED and "cannot verify" was indistinguishable from "ran and failed".

Approach

Per the issue's acceptance criteria "OR" clause: an explicit UNVERIFIABLE tri-state (CodeRabbit's plan, adapted), not 6 new runners.

  • Core: new GateOutcome enum (passed/failed/unverifiable); _run_gate returns the tri-state (stale docstring fixed); run_proof maps obligation status three-way, keeps requirements OPEN (waivable) on unverifiable, and excludes unverifiable from overall_passed — a run never fails solely because a gate couldn't be verified.
  • Evidence/ledger: Evidence.status persists the outcome. proof_evidence gains a nullable status TEXT column with a PRAGMA table_info-guarded ALTER TABLE migration (legacy rows read back status=None, all consumers fall back to the satisfied bool).
  • API (proof_v2): run results carry {gate, satisfied, status} (satisfied kept for compat); EvidenceResponse gains optional status; passed aggregation mirrors the runner rule.
  • CLI: yellow UNVERIFIABLE cell; unverifiable-only run exits 0 with a summary naming the unverifiable gates and a cf proof waive hint; real FAILs still exit 1.
  • TUI: distinct ❓ icon.
  • Web UI: 'unverifiable' added to GateRunStatus; amber badge variant; amber banner branch ("Gates passed — N gate(s) could not be verified"); amber "cannot verify" pills in evidence panels; /proof/[req_id] evidence history renders/filters/sorts the tri-state (incl. a new "Cannot verify" filter option); gate.run.failed notifications no longer fire for unverifiable gates; help section documents the amber state. ProofReqStatus (open/satisfied/waived) intentionally unchanged — this is a gate/obligation-level state.

Acceptance criteria

  • ✅ Unrunnable gates return an explicit UNVERIFIABLE/waivable state distinct from "ran and failed"
  • /proof UI and run results distinguish "cannot verify" from "failed"
  • ✅ Stale _run_gate docstring corrected

Testing

  • Backend: full CI-gate scope uv run pytest tests/ --ignore=tests/e2e -m "not lifecycle"3964 passed; ruff check . clean. New tests/core/test_proof_runner_outcomes.py (+14) plus extensions in test_proof9 (+4), CLI (+2), proof_v2 (+3), incl. a legacy-DB column-migration test.
  • Web UI: npm test → 95 suites / 1051+ tests green (new GateRunBanner suite, useProofRun tri-state cases, evidence-history filter regression test); npm run build compiles.
  • Cross-family review: codex review — its one P2 (evidence history treating unverifiable as fail) fixed with a regression test.

Known limitations

  • PR-merge-gate semantics unchanged by design: pr_v2 snapshots treat unverifiable as not passed (not proven ≠ proven); the raw status string flows into gate_breakdown for display.
  • The 6 gates still have no real runners — that's future work (Playwright/axe/lighthouse wiring); this PR makes their state honest and waivable instead of falsely failing.
  • CONTRACT still maps to pytest (same as UNIT); distinguishing it needs a contract-test corpus, out of scope here.

Summary by CodeRabbit

  • New Features

    • Proof runs and evidence now show a third result state: Cannot verify / Unverifiable.
    • The UI includes new icons, badges, filters, and banner messaging for unverifiable outcomes.
  • Bug Fixes

    • Proof runs now distinguish true failures from unverifiable checks, improving overall run status accuracy.
    • Legacy evidence still displays correctly when the new status field is missing.
  • Documentation

    • Updated in-app proof terminology to describe pass, fail, and cannot verify results.

The 6 PROOF9 gates with no automated runner (e2e, visual, a11y, perf,
demo, manual) previously collapsed to hard FAIL, so most captured
glitches could never be SATISFIED and every run failed perpetually.

- core: new GateOutcome enum (passed/failed/unverifiable); _run_gate
  returns the tri-state and its stale docstring is corrected; run_proof
  maps obligations three-way, keeps requirements OPEN (waivable) on
  unverifiable, and excludes unverifiable from overall_passed
- evidence/ledger: Evidence.status persists the outcome; nullable
  status column added with a PRAGMA-guarded ALTER TABLE migration so
  legacy DBs keep working (old rows read back status=None)
- api: run results carry {gate, satisfied, status}; EvidenceResponse
  gains optional status; passed aggregation mirrors the runner rule
- cli: yellow UNVERIFIABLE cell, waive hint, exit 0 when nothing failed
- tui: distinct ❓ icon for unverifiable obligations
- web-ui: 'unverifiable' GateRunStatus + amber badge/banner/pills on
  /proof and /proof/[req_id] (incl. evidence history filter + sort);
  gate.run.failed notifications no longer fire for unverifiable gates
@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: 40 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: cef1d147-69e2-4a84-927b-a44d76fbf9c9

📥 Commits

Reviewing files that changed from the base of the PR and between b65a437 and eaf4904.

📒 Files selected for processing (4)
  • codeframe/core/proof/ledger.py
  • web-ui/src/app/proof/[req_id]/page.tsx
  • web-ui/src/app/proof/page.tsx
  • web-ui/src/components/ui/badge.tsx

Walkthrough

This PR replaces boolean pass/fail gate results with a tri-state GateOutcome (PASSED/FAILED/UNVERIFIABLE) across the PROOF9 stack: core models, SQLite ledger persistence with migration, gate runner logic, CLI output, TUI icons, the proof v2 API, and web UI types, hooks, components, and pages, with matching tests.

Changes

GateOutcome tri-state rollout

Layer / File(s) Summary
GateOutcome enum and ledger schema/migration
codeframe/core/proof/models.py, codeframe/core/proof/ledger.py
Adds GateOutcome enum and Evidence.status field; extends proof_evidence table with a status column, adds a migration, and updates save/list/get_run_evidence to persist and read status.
Evidence attachment and gate runner logic
codeframe/core/proof/evidence.py, codeframe/core/proof/runner.py, tests/core/test_proof9.py, tests/core/test_proof_runner_config.py, tests/core/test_proof_runner_outcomes.py
attach_evidence now accepts GateOutcome; _run_gate/run_proof compute and propagate outcomes, treat UNVERIFIABLE as neutral for pass/fail, and tests validate these behaviors including a new outcomes test module.
CLI proof run/show commands
codeframe/cli/proof_commands.py, tests/cli/test_proof_commands.py
cf proof run distinguishes FAIL/UNVERIFIABLE outcomes and exit codes; cf proof show displays UNVERIFIABLE evidence status; tests updated accordingly.
TUI obligation icon
codeframe/tui/app.py
Adds an "unverifiable" icon entry to the obligation status icon mapping.
Proof v2 API tri-state serialization
codeframe/ui/routers/proof_v2.py, tests/ui/test_proof_config.py, tests/ui/test_proof_v2.py
EvidenceResponse gains a status field; run and evidence endpoints serialize per-gate status and recompute fallback passed excluding UNVERIFIABLE outcomes.
Web UI types and hook
web-ui/src/types/index.ts, web-ui/src/hooks/useProofRun.ts, web-ui/src/__tests__/hooks/useProofRun.test.ts
Adds optional status fields to response/evidence types and extends GateRunStatus; useProofRun prefers item.status with fallback to item.satisfied.
Web UI components
web-ui/src/components/proof/GateEvidencePanel.tsx, GateRunBanner.tsx, GateRunPanel.tsx, web-ui/src/components/ui/badge.tsx, related tests
Renders an amber "cannot verify" state; GateRunBanner gains an unverifiableCount prop.
Web UI proof pages
web-ui/src/app/proof/[req_id]/page.tsx, web-ui/src/app/proof/page.tsx, web-ui/src/__tests__/components/proof/ProofDetailPage.test.tsx
Adds tri-state evidence result derivation, filtering, sorting, badge display, and passes unverifiable counts through the UI.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant proof_v2_router
  participant run_proof
  participant _run_gate
  participant ledger

  Client->>proof_v2_router: POST /api/v2/proof/run
  proof_v2_router->>run_proof: execute requirement obligations
  run_proof->>_run_gate: run gate for obligation
  _run_gate-->>run_proof: GateOutcome (PASSED/FAILED/UNVERIFIABLE)
  run_proof->>ledger: attach_evidence(outcome), save obligation status
  run_proof-->>proof_v2_router: outcomes per gate
  proof_v2_router-->>Client: results with satisfied and status
  Client->>proof_v2_router: GET .../evidence
  proof_v2_router->>ledger: fetch stored evidence
  ledger-->>proof_v2_router: Evidence(status)
  proof_v2_router-->>Client: EvidenceResponse(status)
Loading

Possibly related PRs

  • frankbria/codeframe#447: Directly refactors the same PROOF9 core/CLI code paths (models, evidence, ledger, runner, CLI) that previously used boolean satisfied/passed.
  • frankbria/codeframe#574: Modifies the same proof v2 run-gates flow (proof_v2.py, useProofRun, gate-result typing) that this PR layers tri-state status onto.
  • frankbria/codeframe#589: Modifies the same run_proof gate execution and overall_passed/strictness logic in runner.py.

Poem

A gate that couldn't run before,
now says "unverifiable," not fail no more!
Amber pills where red once glowed,
❓ hops in where doubt was sowed.
Hop, hop, hooray — the proof is clear,
tri-state truth has landed here! 🐇✨

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: adding an explicit UNVERIFIABLE state for unrunnable proof gates.
Linked Issues check ✅ Passed The PR satisfies #728 by making unrunnable gates explicit UNVERIFIABLE, distinguishing them from failures, and updating the UI/results.
Out of Scope Changes check ✅ Passed The changes stay focused on proof gate outcomes, persistence, APIs, and UI/tests needed for the UNVERIFIABLE state.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
📋 Issue Planner

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

View plan used: #728

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/728-proof9-unverifiable-state

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

@claude

claude Bot commented Jul 3, 2026

Copy link
Copy Markdown

Code Review - fix(proof): explicit UNVERIFIABLE state (728)

Overall: Clean, well-scoped implementation of the tri-state UNVERIFIABLE outcome. The design decision ("OR clause": honest state rather than 6 new runners) is correct for the current phase, the backward-compat story (keeping satisfied alongside status) is well-executed, and test coverage is excellent across all layers. A few things to address before merge:


Medium - Connection leak in migration

conn = get_db_connection(workspace)
cursor = conn.cursor()
cursor.execute("PRAGMA table_info(proof_evidence)")
...
conn.close()

If the PRAGMA or ALTER TABLE raises, conn.close() is never called. Wrap with try/finally:

conn = get_db_connection(workspace)
try:
    cursor = conn.cursor()
    cursor.execute("PRAGMA table_info(proof_evidence)")
    columns = {row[1] for row in cursor.fetchall()}
    if columns and "status" not in columns:
        cursor.execute("ALTER TABLE proof_evidence ADD COLUMN status TEXT")
        conn.commit()
finally:
    conn.close()

Medium - Migration PRAGMA runs on every DB access

_ensure_tables (called on every ledger operation) now calls _migrate_evidence_status_column, which issues PRAGMA table_info every time. Once the column exists this is a no-op but it is still a round-trip per DB call. A simple module-level set caches the result:

_migrated_workspaces: set[str] = set()

def _migrate_evidence_status_column(workspace: Workspace) -> None:
    if workspace.id in _migrated_workspaces:
        return
    ...
    _migrated_workspaces.add(workspace.id)

Minor - evidenceResult called 3x per row in the detail page

In web-ui/src/app/proof/[req_id]/page.tsx around the evidence table cell:

<span className={RESULT_CLASS[evidenceResult(ev)]}>
  {evidenceResult(ev) === 'unverifiable' ? 'cannot verify' : evidenceResult(ev)}
</span>

evidenceResult is pure but called three times per row. Compute it once:

const result = evidenceResult(ev);
<span className={RESULT_CLASS[result]}>
  {result === 'unverifiable' ? 'cannot verify' : result}
</span>

Minor - unverifiable badge variant is visually identical to in-progress

In badge.tsx:

'in-progress': 'border-transparent bg-amber-100 text-amber-900',
unverifiable:  'border-transparent bg-amber-100 text-amber-900',

They are pixel-identical. During a live run, gates briefly show in-progress then flip to unverifiable - the badge does not change, which is visually silent. Even a small difference (e.g. bg-amber-50 text-amber-800 for unverifiable) would make the transition noticeable.


Minor - Awkward grammar in the help copy

web-ui/src/app/proof/page.tsx:

"or be could not be verified" reads oddly. Suggest: "or unverifiable (amber) - the check could not run".


Already-correct items worth noting for reviewers

  • check_obligation_satisfied in evidence.py checks ev.satisfied which is False for UNVERIFIABLE (since satisfied = outcome == GateOutcome.PASSED). Unverifiable evidence correctly never satisfies an obligation without needing an extra status check.
  • Notification suppression in proof/page.tsx line 133 (entry.status === 'failed') correctly excludes 'unverifiable' from firing gate.run.failed notifications.
  • The migration guard if columns and "status" not in columns correctly handles the empty-set case (table does not exist yet), avoiding a spurious ALTER on a new DB.
  • The fallback chain in useProofRun.ts (item.status ?? (item.satisfied ? 'passed' : 'failed')) and in GateEvidencePanel (ev.status === 'unverifiable' before the ev.satisfied path) correctly handle legacy cached run data.

Verdict: Feature logic is sound. Fix the connection leak before merge. The badge-identity and help-copy issues are minor but straightforward to fix in the same PR.

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

🧹 Nitpick comments (3)
web-ui/src/app/proof/[req_id]/page.tsx (1)

342-383: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Duplicated tri-state classification logic.

The latestEv.status === 'unverifiable' ? ... : latestEv.satisfied ? ... : ... pattern is repeated here and again for the run-id link (Lines 371-375), separate from the evidenceResult()/RESULT_CLASS helper already introduced for the evidence table. Consider extracting a single helper (e.g., classifyGateOutcome(ev)) shared across all three render sites to avoid drift if the tri-state rule changes again.

♻️ Example consolidation
-const effectiveStatus = latestEv
-  ? latestEv.status === 'unverifiable'
-    ? 'unverifiable'
-    : latestEv.satisfied ? 'satisfied' : 'failed'
-  : ob.status;
+function obligationOutcome(ev: ProofEvidence): 'satisfied' | 'failed' | 'unverifiable' {
+  if (ev.status === 'unverifiable') return 'unverifiable';
+  return ev.satisfied ? 'satisfied' : 'failed';
+}
+const effectiveStatus = latestEv ? obligationOutcome(latestEv) : ob.status;
🤖 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 `@web-ui/src/app/proof/`[req_id]/page.tsx around lines 342 - 383, The tri-state
status/color mapping is duplicated in the obligations table and the run-id link,
which can drift from the existing evidence-table helpers. Extract a shared
helper such as classifyGateOutcome(ev) (or reuse evidenceResult()/RESULT_CLASS
if appropriate) in proof/[req_id]/page.tsx and use it for both the
effectiveStatus calculation and the button className so all render sites follow
one source of truth.
web-ui/src/components/ui/badge.tsx (1)

19-21: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

unverifiable badge is visually identical to in-progress.

Both variants use bg-amber-100 text-amber-900, so if these ever appear together (e.g. in a status list or filter), users cannot visually distinguish "in progress" from "cannot verify." Consider a distinct hue to avoid ambiguity.

♻️ Suggested distinct styling
-        unverifiable: 'border-transparent bg-amber-100 text-amber-900',
+        unverifiable: 'border-transparent bg-yellow-100 text-yellow-900',
🤖 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 `@web-ui/src/components/ui/badge.tsx` around lines 19 - 21, The `Badge` variant
styles in `badge.tsx` make `unverifiable` identical to `in-progress`, so update
the `badgeVariants` mapping to give `unverifiable` a distinct color treatment.
Keep the `in-progress` amber styling, but change the `unverifiable` entry to a
different hue so the two statuses are visually distinguishable wherever the
`Badge` component is used.
codeframe/cli/proof_commands.py (1)

342-348: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Consider missing test coverage for the new UNVERIFIABLE branch in show.

The logic itself is correct (comparing to "unverifiable" works since GateOutcome is a str Enum), but tests/cli/test_proof_commands.py only adds coverage for run's tri-state output — no test exercises show's new ev.status == "unverifiable" branch. Given this is a newly introduced display path, a regression here (e.g. a typo in the string, wrong precedence with elif ev.satisfied) would go unnoticed.

🤖 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/cli/proof_commands.py` around lines 342 - 348, Add test coverage
for the new UNVERIFIABLE display path in show within
proof_commands/proof_commands.py. Extend tests/cli/test_proof_commands.py with a
case that exercises show’s ev.status == "unverifiable" branch and verifies it
renders [yellow]UNVERIFIABLE[/yellow], alongside existing PASS/FAIL behavior.
Use the show command path and the GateOutcome/status handling in
proof_commands.show so a typo or precedence regression in the new branch would
be caught.
🤖 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 128-140: The _migrate_evidence_status_column migration is
vulnerable to a race during concurrent first-run because it checks
proof_evidence schema and then alters it in separate steps. Update this
migration to be safe when multiple workers call _ensure_tables at once by
handling the duplicate-column case around the ALTER TABLE in
_migrate_evidence_status_column (or otherwise serializing the schema change), so
a second worker can proceed cleanly if another already added status.

---

Nitpick comments:
In `@codeframe/cli/proof_commands.py`:
- Around line 342-348: Add test coverage for the new UNVERIFIABLE display path
in show within proof_commands/proof_commands.py. Extend
tests/cli/test_proof_commands.py with a case that exercises show’s ev.status ==
"unverifiable" branch and verifies it renders [yellow]UNVERIFIABLE[/yellow],
alongside existing PASS/FAIL behavior. Use the show command path and the
GateOutcome/status handling in proof_commands.show so a typo or precedence
regression in the new branch would be caught.

In `@web-ui/src/app/proof/`[req_id]/page.tsx:
- Around line 342-383: The tri-state status/color mapping is duplicated in the
obligations table and the run-id link, which can drift from the existing
evidence-table helpers. Extract a shared helper such as classifyGateOutcome(ev)
(or reuse evidenceResult()/RESULT_CLASS if appropriate) in
proof/[req_id]/page.tsx and use it for both the effectiveStatus calculation and
the button className so all render sites follow one source of truth.

In `@web-ui/src/components/ui/badge.tsx`:
- Around line 19-21: The `Badge` variant styles in `badge.tsx` make
`unverifiable` identical to `in-progress`, so update the `badgeVariants` mapping
to give `unverifiable` a distinct color treatment. Keep the `in-progress` amber
styling, but change the `unverifiable` entry to a different hue so the two
statuses are visually distinguishable wherever the `Badge` component is used.
🪄 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: f83b2254-4e09-4a34-aa0f-1139ec3911d0

📥 Commits

Reviewing files that changed from the base of the PR and between 62586b4 and b65a437.

📒 Files selected for processing (25)
  • codeframe/cli/proof_commands.py
  • codeframe/core/proof/evidence.py
  • codeframe/core/proof/ledger.py
  • codeframe/core/proof/models.py
  • codeframe/core/proof/runner.py
  • codeframe/tui/app.py
  • codeframe/ui/routers/proof_v2.py
  • tests/cli/test_proof_commands.py
  • tests/core/test_proof9.py
  • tests/core/test_proof_runner_config.py
  • tests/core/test_proof_runner_outcomes.py
  • tests/ui/test_proof_config.py
  • tests/ui/test_proof_v2.py
  • web-ui/src/__tests__/components/proof/GateEvidencePanel.test.tsx
  • web-ui/src/__tests__/components/proof/GateRunBanner.test.tsx
  • web-ui/src/__tests__/components/proof/ProofDetailPage.test.tsx
  • web-ui/src/__tests__/hooks/useProofRun.test.ts
  • web-ui/src/app/proof/[req_id]/page.tsx
  • web-ui/src/app/proof/page.tsx
  • web-ui/src/components/proof/GateEvidencePanel.tsx
  • web-ui/src/components/proof/GateRunBanner.tsx
  • web-ui/src/components/proof/GateRunPanel.tsx
  • web-ui/src/components/ui/badge.tsx
  • web-ui/src/hooks/useProofRun.ts
  • web-ui/src/types/index.ts

Comment thread codeframe/core/proof/ledger.py Outdated
@claude

claude Bot commented Jul 3, 2026

Copy link
Copy Markdown

Follow-up Review

Checking the current code against my previous review.

Fixed

  • Help copyweb-ui/src/app/proof/page.tsx now reads "the check couldn't run, so it never fails a run and stays waivable". Clear and correct.

Still outstanding

1. Connection leak in migration (Medium) — ledger.py:128-140 is unchanged. If the PRAGMA or ALTER TABLE raises, conn.close() is skipped. Needs try/finally:

conn = get_db_connection(workspace)
try:
    cursor = conn.cursor()
    cursor.execute("PRAGMA table_info(proof_evidence)")
    columns = {row[1] for row in cursor.fetchall()}
    if columns and "status" not in columns:
        cursor.execute("ALTER TABLE proof_evidence ADD COLUMN status TEXT")
        conn.commit()
finally:
    conn.close()

2. PRAGMA on every DB call (Medium) — _migrate_evidence_status_column is still called unconditionally from _ensure_tables with no short-circuit once migrated. A module-level set avoids the per-call round-trip:

_migrated_workspaces: set[str] = set()

def _migrate_evidence_status_column(workspace: Workspace) -> None:
    if workspace.id in _migrated_workspaces:
        return
    # ... existing logic ...
    _migrated_workspaces.add(workspace.id)

3. evidenceResult called 3x per render (Minor) — proof/[req_id]/page.tsx lines 511-512 still call the function three times per table row. One local variable eliminates the redundancy:

const result = evidenceResult(ev);
<span className={RESULT_CLASS[result]}>
  {result === 'unverifiable' ? 'cannot verify' : result}
</span>

4. unverifiable badge indistinguishable from in-progress (Minor) — badge.tsx lines 20-21 still share identical Tailwind classes. A subtle distinction (e.g. bg-amber-50 text-amber-800 border-amber-200) would make the state transition visible during a live run.


Items 1 and 2 are worth fixing before merge — the connection leak is a correctness issue and the PRAGMA-per-call will show up in profiling once workspaces accumulate evidence. Items 3 and 4 are cosmetic and can follow in a cleanup PR if preferred.

- ledger: try/finally around the migration connection, tolerate the
  concurrent-first-run duplicate-column race, and memoize per workspace
  so the PRAGMA doesn't run on every DB access (CodeRabbit + claude-review)
- web-ui: compute evidenceResult once per row; give the unverifiable
  badge distinct styling from in-progress so live runs visibly flip
@frankbria

Copy link
Copy Markdown
Owner Author

Review triage (commit eaf4904):

  • CodeRabbit — migration race on concurrent first-run (ledger.py): fixed. The ALTER now tolerates sqlite3.OperationalError: duplicate column name from a concurrent worker; anything else re-raises.
  • claude-review Medium — connection leak in migration: fixed with try/finally around the migration connection.
  • claude-review Medium — PRAGMA on every DB access: fixed via a module-level per-workspace memo; the check runs once per process per workspace.
  • claude-review Minor — evidenceResult called 3× per row: fixed (computed once per row).
  • claude-review Minor — unverifiable badge identical to in-progress: fixed (border-amber-300 bg-amber-50 text-amber-800), so a running gate visibly flips when it lands on unverifiable.
  • claude-review Minor — help copy grammar: fixed in 747fbaa ("or unverifiable (amber)").

Verified: tests/core/test_proof9.py 46 passed, ruff clean, web-ui 1052 tests + build green.

@claude

claude Bot commented Jul 3, 2026

Copy link
Copy Markdown

Follow-up review: all prior concerns resolved. All five items I flagged in my previous review are addressed in the latest commit.\n\nConnection leak in migration - Fixed. try/finally wraps the connection; conn.close() now runs even if PRAGMA or ALTER raises.\n\nPRAGMA on every DB access - Fixed. _status_migrated_workspaces: set[str] caches at module level. Per-process, per-workspace is the correct tradeoff. Each worker process still runs one PRAGMA on first access per workspace, which is expected and acceptable.\n\nevidenceResult called 3x per row - Fixed. const result = evidenceResult(ev) computed once, reused for both the class and the label.\n\nBadge variants indistinguishable - Fixed. unverifiable now uses border-amber-300 bg-amber-50 text-amber-800 (visible border, lighter background) vs in-progress which uses bg-amber-100 text-amber-900. The inline comment explaining the visual distinction is a nice touch.\n\nHelp copy grammar - Fixed. New text ("the check could not run, so it never fails a run and stays waivable") is clear and accurate.\n\nOne minor new observation (non-blocking):\n\nThe amber GateRunBanner (shown when all verifiable gates pass but some are unverifiable) does not include the retry button that the green/red banners expose. That is arguably the right call since re-running will not un-unverify a gate, but if a deep-link to "cf proof waive" is ever added, this banner is the natural place for it.\n\nAlso, proof_v2.py lines 438-445 duplicate the runner all_passed calculation for the rare edge case where persisted_run is None. Both copies are correct; if the unverifiable exclusion rule ever changes there are two places to update.\n\nShip it. The tri-state design is clean, the backward-compat story (satisfied kept alongside status) is solid, test coverage is thorough (outcomes test module, migration test, filter regression), and all layers (core, CLI, API, web UI) are consistent.

@frankbria frankbria merged commit 8a98e63 into main Jul 3, 2026
11 checks passed
@frankbria frankbria deleted the fix/728-proof9-unverifiable-state branch July 3, 2026 20:49
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.1] PROOF9: give the 6 unrunnable gates real runners or an explicit UNVERIFIABLE state (not perpetual fail)

1 participant