Skip to content

feat(pr-history): PR history view with proof snapshots (#572)#585

Merged
frankbria merged 3 commits intomainfrom
feature/issue-572-pr-history-proof-snapshots
Apr 15, 2026
Merged

feat(pr-history): PR history view with proof snapshots (#572)#585
frankbria merged 3 commits intomainfrom
feature/issue-572-pr-history-proof-snapshots

Conversation

@frankbria
Copy link
Copy Markdown
Owner

@frankbria frankbria commented Apr 15, 2026

Summary

  • Backend: GET /api/v2/pr/history endpoint returning recently merged PRs with PROOF9 gate snapshots captured at PR creation time. New pr_proof_snapshots SQLite table in the workspace DB, author field on PRDetails dataclass.
  • Frontend: PRHistoryPanel component on the Review page showing merged PRs with proof badge (gates passed/total), expandable gate-by-gate breakdown, empty/loading/error states.
  • Tests: 11 Python tests (8 endpoint + 3 ledger), 13 Jest tests (rendering, expand/collapse, SWR integration).

Closes #572

Test plan

  • uv run pytest tests/ui/test_pr_history.py tests/core/test_proof_snapshot.py — 11 passed
  • cd web-ui && npm test — 773 passed (60 suites), including 13 PRHistoryPanel tests
  • cd web-ui && npm run build — succeeds
  • uv run ruff check — all passed
  • Full uv run pytest — 3158 passed (1 pre-existing flaky stall_monitor timing test)

Summary by CodeRabbit

  • New Features
    • PR history view showing merged pull requests with merge timestamps, authors, and links.
    • Persistent proof snapshots captured for PRs and surfaced as "gates passed/total" and per‑gate breakdown.
    • New PR History panel with expandable rows to view gate-by-gate proof details and empty/error/loading states.

Test User added 2 commits April 14, 2026 16:01
Add GET /api/v2/pr/history returning recently merged PRs with PROOF9
gate snapshots captured at PR creation time. Includes pr_proof_snapshots
SQLite table, author field on PRDetails, and full test coverage.
Add PRHistoryPanel component to the Review page showing recently merged
PRs with PROOF9 gate snapshot badges. Includes expandable gate breakdown,
TypeScript types, API client method, and 13 Jest tests.
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Apr 15, 2026

Walkthrough

Adds PR-history support: stores PR proof snapshots in SQLite, exposes GET /api/v2/pr/history (joins GitHub merged PRs with stored snapshots), captures snapshots on PR creation, extends GitHub PR model with author, and adds frontend components/tests to display merged PRs with gate breakdowns.

Changes

Cohort / File(s) Summary
Proof Ledger Storage
codeframe/core/proof/ledger.py, tests/core/test_proof_snapshot.py
Create pr_proof_snapshots table; add save_pr_proof_snapshot() and get_pr_proof_snapshot(); tests for init, save/retrieve, overwrite, and missing snapshot.
GitHub Integration
codeframe/git/github_integration.py
PRDetails dataclass now includes optional author extracted from GitHub PR payload.
PR History API
codeframe/ui/routers/pr_v2.py, tests/ui/test_pr_history.py
Added GET /api/v2/pr/history endpoint and Pydantic response models (GateBreakdownItem, ProofSnapshotOut, PRHistoryItem, PRHistoryResponse); loads merged PRs from GitHub, attaches snapshots via ledger, and limits/sorts results. Also capture snapshot on PR creation (non-fatal on error). Tests cover filtering, sorting, snapshots, limit, error passthrough, and client cleanup.
Frontend History Panel
web-ui/src/components/review/PRHistoryPanel.tsx, web-ui/src/__tests__/components/review/PRHistoryPanel.test.tsx
New PRHistoryPanel component showing merged PRs, proof badges, expandable gate breakdowns, and loading/error/empty states; tests for SWR integration and interactions.
Frontend Integration & Types
web-ui/src/app/review/page.tsx, web-ui/src/lib/api.ts, web-ui/src/types/index.ts
Integrate PRHistoryPanel into review page; add prApi.getHistory() client method and TypeScript interfaces (GateBreakdownItem, ProofSnapshot, PRHistoryItem, PRHistoryResponse).
Icon Mock
web-ui/__mocks__/@hugeicons/react.js
Added ArrowUpRight01Icon mock export for tests.

Sequence Diagram(s)

sequenceDiagram
    actor Browser
    participant Frontend as "Web UI (PRHistoryPanel)"
    participant API as "Backend /api/v2/pr/history"
    participant GH as "GitHub Client"
    participant DB as "SQLite (pr_proof_snapshots)"

    Browser->>Frontend: load PR History (workspacePath)
    Frontend->>API: GET /api/v2/pr/history?workspace=...
    API->>GH: list_pull_requests(state=closed)
    GH-->>API: list of PRs (with merged_at, user)
    API->>DB: get_pr_proof_snapshot(workspace, pr_number) for each merged PR
    DB-->>API: snapshot or null
    API-->>Frontend: PRHistoryResponse (merged PRs + snapshots)
    Frontend-->>Browser: render list, badges, expandable breakdowns
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Poem

🐰 I dug a cozy SQLite den tonight,

Saved PR snapshots by moonlit byte,
Merged PRs parade with gates in tow,
Click to expand, the gate-by-gate glow,
Hooray — history hops into sight!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 69.44% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title clearly and concisely describes the main feature being added: a PR history view with proof snapshots, which directly aligns with the changeset's core objective.
Linked Issues check ✅ Passed The PR successfully implements all coding requirements from #572: backend endpoint with merged PR + proof snapshot data, SQLite table for snapshots, PRDetails extended with author field, frontend PR history panel with proof badge and expandable gate breakdown, and proper empty/loading/error state handling.
Out of Scope Changes check ✅ Passed All changes are scoped to the PR history feature: database layer for proof snapshots, GitHub author field, API endpoint and models, frontend component and tests, with no unrelated modifications detected.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/issue-572-pr-history-proof-snapshots

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

@claude
Copy link
Copy Markdown

claude bot commented Apr 15, 2026

PR #585 Review — feat(pr-history): PR history view with proof snapshots

Overall: Well-structured addition that follows project conventions (core-first, thin UI adapter, SWR, Shadcn). Test coverage is solid and the test plan checks out. There is one critical bug and a few lower-priority issues to address.


🔴 Critical: Gate status string mismatch

The backend saves obligation status verbatim from the domain model (which uses \"passed\"), but the frontend icon renderer checks for \"satisfied\":

codeframe/ui/routers/pr_v2.py (create_pull_request snapshot capture)

gate_breakdown.append({
    \"gate\": ob.gate.value,
    \"status\": ob.status,   # stores \"passed\" / \"failed\" / ...
})

web-ui/src/components/review/PRHistoryPanel.tsx

{gate.status === 'satisfied' ? (
  <CheckmarkCircle01Icon className=\"h-3.5 w-3.5 text-green-600\" />
) : (
  <Cancel01Icon className=\"h-3.5 w-3.5 text-red-600\" />
)}

This means every gate will render the red ✗ icon regardless of actual pass/fail — the green ✓ is unreachable in production. The mismatch is also visible in the tests: test_proof_snapshot.py uses \"passed\" while PRHistoryPanel.test.tsx uses \"satisfied\". They test in isolation so both pass, but the integration is broken.

Fix options:

  • Normalize to \"passed\" everywhere and update the frontend check to === 'passed', or
  • Map at save time: \"status\": \"satisfied\" if ob.status == \"passed\" else ob.status

🟡 Performance: Full closed-PR fetch then in-memory filter

get_pr_history calls client.list_pull_requests(state=\"closed\") which fetches all closed PRs from GitHub (subject to GitHub's default page size of 30), then filters to merged-only in Python. On repos with many closed-but-unmerged PRs this wastes API quota and may miss older merged PRs once the page fills with unmerged ones.

Consider using state=\"closed\" with a sort=\"updated\" or (if the GitHub client supports it) filtering by merged query parameter, or at minimum documenting the known limitation in the docstring.


🟡 _ensure_tables: second check runs outside with conn: block

# Original code (inside with conn: block):
    with conn:
        cursor.execute(\"SELECT ... 'proof_runs'\")
        missing = not cursor.fetchone()
# New code lands OUTSIDE the context manager:
    if not missing:
        cursor.execute(\"SELECT ... 'pr_proof_snapshots'\")
        missing = not cursor.fetchone()
    conn.close()

The SELECT works correctly outside the with block in SQLite, but the style inconsistency is confusing and could become a real issue if the logic is ever extended. Move both checks inside a single with conn: block for clarity.


🟡 Edge case: 0/0 gates renders as green

function proofBadgeClasses(snapshot: ProofSnapshot | null): string {
  if (!snapshot) return 'text-muted-foreground bg-muted';
  if (snapshot.gates_passed === snapshot.gates_total) {  // true when both are 0
    return 'text-green-600 bg-green-50';
  }
  ...
}

A workspace with no obligations would show a green \"0/0 gates\" badge, implying perfect proof coverage when there's actually none. Guard with gates_total > 0.


🟢 Minor / Low priority

  • total is redundant: PRHistoryResponse.total is always len(pull_requests). The frontend can use data.pull_requests.length. Not blocking but adds noise to the contract.
  • isLoading not destructured from SWR: The loading skeleton uses !data && !error. SWR v2 exposes isLoading directly — using it avoids the brief skeleton flash during background revalidation when cached data exists.
  • Missing migration test: No test verifies that _ensure_tables correctly triggers init_proof_tables when proof_runs exists but pr_proof_snapshots does not (the core scenario for the new check). Worth adding to test_proof_snapshot.py.

✅ What's done well

  • Architecture correctly keeps the snapshot ledger in core/proof/ (headless) and wraps it via the router.
  • save_pr_proof_snapshot uses INSERT OR REPLACE — correct semantics for idempotent re-creation if a PR is force-pushed and re-created.
  • Snapshot capture failure is non-fatal (wrapped in try/except Warning) — the PR creation still succeeds.
  • PRHistoryPanel handles all three SWR states (loading, error, empty) cleanly.
  • e.stopPropagation() on the GitHub link anchor correctly prevents the expand/collapse toggle from firing.
  • Test coverage for the ledger functions and the endpoint is thorough and follows project patterns.

The status mismatch is the only blocker. Everything else is minor polish.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🧹 Nitpick comments (2)
web-ui/src/__tests__/components/review/PRHistoryPanel.test.tsx (1)

139-178: Add an explicit assertion for PR GitHub link rendering.

The test data includes url, but link/href behavior isn’t validated yet.

Suggested test addition
   describe('PR list rendering', () => {
@@
     it('renders PR titles', () => {
       withData(SAMPLE_HISTORY);
       render(<PRHistoryPanel workspacePath={WORKSPACE} />);
@@
       expect(screen.getByText('fix: resolve login bug')).toBeInTheDocument();
     });
+
+    it('renders GitHub PR link', () => {
+      withData(SAMPLE_HISTORY);
+      render(<PRHistoryPanel workspacePath={WORKSPACE} />);
+
+      expect(screen.getByRole('link', { name: /feat: add user auth/i }))
+        .toHaveAttribute('href', 'https://github.com/owner/repo/pull/10');
+    });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web-ui/src/__tests__/components/review/PRHistoryPanel.test.tsx` around lines
139 - 178, Add an assertion that the PRHistoryPanel renders the PR GitHub link
using the `url` from the fixture (e.g., SAMPLE_HISTORY / ALL_PASSED entries) by
locating the PR title element rendered by PRHistoryPanel and asserting its
enclosing anchor has the expected href; for example, after
render(<PRHistoryPanel workspacePath={WORKSPACE} />) find the title (or use
getByRole('link', { name: /feat: add user auth/i })) and assert the link's href
equals the corresponding SAMPLE_HISTORY[...].url value so the test verifies
link/href behavior.
tests/ui/test_pr_history.py (1)

162-178: Strengthen limit test to verify which PRs are returned, not only count.

Right now this passes even if the endpoint returns the wrong two items.

Suggested assertion tightening
         assert resp.status_code == 200
-        assert len(resp.json()["pull_requests"]) == 2
+        items = resp.json()["pull_requests"]
+        assert len(items) == 2
+        assert [pr["number"] for pr in items] == [3, 2]
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/ui/test_pr_history.py` around lines 162 - 178, The test only checks the
count; update test_limit_parameter to also assert which PRs are returned by
extracting numbers from resp.json()["pull_requests"] and comparing them to the
expected PR numbers from the prs list (e.g., the two most-recent merged PRs
created via _make_pr), ensuring both order and identity match (use the prs
fixture values or expected_numbers like [3,2] derived from the prs list) so the
endpoint returns the correct items rather than any two.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@codeframe/ui/routers/pr_v2.py`:
- Around line 474-486: The current loop over reqs builds gates_total and
gate_breakdown from every req.obligations entry, causing duplicate gates;
instead aggregate obligations by ob.gate: iterate reqs and req.obligations to
build a dict keyed by ob.gate (use ob.gate.value or ob.gate as key) and compute
a single status per gate (e.g., mark "passed" if any obligation for that gate
has status == "passed", otherwise the most severe status), then set gates_total
= number of unique keys and build gate_breakdown from the aggregated dict;
update the code paths that reference gates_total and gate_breakdown accordingly.
- Around line 344-349: The current logic only inspects the first page returned
by client.list_pull_requests(state="closed") and then sorts that subset, which
can miss recently merged PRs; change list_pull_requests usage to paginate (fetch
subsequent pages) until you've collected at least `limit` PRs with pr.merged_at
not None or there are no more pages, accumulate merged PRs across pages into
`merged`, then sort merged by pr.merged_at descending and truncate to `limit`;
update the code that builds `merged` (the list comprehension and subsequent
sort) and the call site of client.list_pull_requests to support pagination.
- Around line 463-494: The snapshot is being taken immediately after
create_pull_request; instead, remove the proof-capture block (init_proof_tables,
list_requirements, gate counting and save_pr_proof_snapshot) from the create PR
flow and relocate it to the PR merge flow so snapshots reflect final merged
state; implement the same logic (call init_proof_tables(workspace), compute
gates_total/gates_passed/gate_breakdown from list_requirements(workspace) and
call save_pr_proof_snapshot(workspace, pr_number=pr.number, ...)) in the merge
handler (the function that performs PR merge/merge_pull_request) so the proof
snapshot is created/updated at merge time rather than on create.

In `@web-ui/src/components/review/PRHistoryPanel.tsx`:
- Around line 90-124: The GitHub anchor is nested inside the toggle <button>,
which is invalid; refactor the JSX so the toggle button (the element that calls
toggleExpand and uses expandedPR/ pr.number and contains the left-side content
and the Badge using proofBadgeClasses/proofBadgeText) is one sibling and the
external link anchor (using pr.url, rel, target, aria-label and
ArrowUpRight01Icon) is a separate sibling element next to it inside the same
container; keep the button's aria-expanded and focus styles, preserve the
anchor's classes and aria-label, and keep or reapply the onClick stopPropagation
logic on the anchor (or remove it if no longer needed) so clicking the external
link does not toggle the row.

---

Nitpick comments:
In `@tests/ui/test_pr_history.py`:
- Around line 162-178: The test only checks the count; update
test_limit_parameter to also assert which PRs are returned by extracting numbers
from resp.json()["pull_requests"] and comparing them to the expected PR numbers
from the prs list (e.g., the two most-recent merged PRs created via _make_pr),
ensuring both order and identity match (use the prs fixture values or
expected_numbers like [3,2] derived from the prs list) so the endpoint returns
the correct items rather than any two.

In `@web-ui/src/__tests__/components/review/PRHistoryPanel.test.tsx`:
- Around line 139-178: Add an assertion that the PRHistoryPanel renders the PR
GitHub link using the `url` from the fixture (e.g., SAMPLE_HISTORY / ALL_PASSED
entries) by locating the PR title element rendered by PRHistoryPanel and
asserting its enclosing anchor has the expected href; for example, after
render(<PRHistoryPanel workspacePath={WORKSPACE} />) find the title (or use
getByRole('link', { name: /feat: add user auth/i })) and assert the link's href
equals the corresponding SAMPLE_HISTORY[...].url value so the test verifies
link/href behavior.
🪄 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: b59fad2c-5a87-40a1-83ab-7821dbbd15ff

📥 Commits

Reviewing files that changed from the base of the PR and between f67caa3 and 1e5669e.

📒 Files selected for processing (11)
  • codeframe/core/proof/ledger.py
  • codeframe/git/github_integration.py
  • codeframe/ui/routers/pr_v2.py
  • tests/core/test_proof_snapshot.py
  • tests/ui/test_pr_history.py
  • web-ui/__mocks__/@hugeicons/react.js
  • web-ui/src/__tests__/components/review/PRHistoryPanel.test.tsx
  • web-ui/src/app/review/page.tsx
  • web-ui/src/components/review/PRHistoryPanel.tsx
  • web-ui/src/lib/api.ts
  • web-ui/src/types/index.ts

Comment on lines +344 to +349
prs = await client.list_pull_requests(state="closed")

# Filter to only merged PRs and sort newest first.
merged = [pr for pr in prs if pr.merged_at is not None]
merged.sort(key=lambda pr: pr.merged_at, reverse=True)
merged = merged[:limit]
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.

⚠️ Potential issue | 🟠 Major

/history can miss the actual latest merged PRs.

This only examines GitHub's first page of state="closed" PRs and then sorts that subset by merged_at. A long-lived PR merged recently can fall off page 1, so the history view can omit the real latest merged PRs. Paginate until you collect limit merged PRs, or query GitHub by merge time directly.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@codeframe/ui/routers/pr_v2.py` around lines 344 - 349, The current logic only
inspects the first page returned by client.list_pull_requests(state="closed")
and then sorts that subset, which can miss recently merged PRs; change
list_pull_requests usage to paginate (fetch subsequent pages) until you've
collected at least `limit` PRs with pr.merged_at not None or there are no more
pages, accumulate merged PRs across pages into `merged`, then sort merged by
pr.merged_at descending and truncate to `limit`; update the code that builds
`merged` (the list comprehension and subsequent sort) and the call site of
client.list_pull_requests to support pagination.

Comment on lines +463 to +494
# Capture proof snapshot at PR creation time.
try:
from codeframe.core.proof.ledger import (
init_proof_tables,
list_requirements,
save_pr_proof_snapshot,
)

init_proof_tables(workspace)
reqs = list_requirements(workspace)

gates_total = 0
gates_passed = 0
gate_breakdown: list[dict] = []
for req in reqs:
for ob in req.obligations:
gates_total += 1
passed = ob.status == "passed"
if passed:
gates_passed += 1
gate_breakdown.append({
"gate": ob.gate.value,
"status": ob.status,
})

save_pr_proof_snapshot(
workspace,
pr_number=pr.number,
gates_passed=gates_passed,
gates_total=gates_total,
gate_breakdown=gate_breakdown,
)
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.

⚠️ Potential issue | 🟠 Major

The snapshot is captured at PR creation, not at merge.

This stores proof data immediately after create_pull_request(), but the feature is exposing proof state for merged PR history. Any gates that change while the PR is open will make the history snapshot stale and misleading. Capture or refresh the snapshot in the merge flow instead.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@codeframe/ui/routers/pr_v2.py` around lines 463 - 494, The snapshot is being
taken immediately after create_pull_request; instead, remove the proof-capture
block (init_proof_tables, list_requirements, gate counting and
save_pr_proof_snapshot) from the create PR flow and relocate it to the PR merge
flow so snapshots reflect final merged state; implement the same logic (call
init_proof_tables(workspace), compute gates_total/gates_passed/gate_breakdown
from list_requirements(workspace) and call save_pr_proof_snapshot(workspace,
pr_number=pr.number, ...)) in the merge handler (the function that performs PR
merge/merge_pull_request) so the proof snapshot is created/updated at merge time
rather than on create.

Comment on lines +474 to +486
gates_total = 0
gates_passed = 0
gate_breakdown: list[dict] = []
for req in reqs:
for ob in req.obligations:
gates_total += 1
passed = ob.status == "passed"
if passed:
gates_passed += 1
gate_breakdown.append({
"gate": ob.gate.value,
"status": ob.status,
})
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.

⚠️ Potential issue | 🟠 Major

This counts requirement obligations, not unique gates.

gates_total and gate_breakdown are built from every req.obligations entry, so the same PROOF9 gate can appear multiple times across requirements. That inflates the badge and produces repeated rows instead of a gate-by-gate snapshot. Aggregate by ob.gate before persisting.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@codeframe/ui/routers/pr_v2.py` around lines 474 - 486, The current loop over
reqs builds gates_total and gate_breakdown from every req.obligations entry,
causing duplicate gates; instead aggregate obligations by ob.gate: iterate reqs
and req.obligations to build a dict keyed by ob.gate (use ob.gate.value or
ob.gate as key) and compute a single status per gate (e.g., mark "passed" if any
obligation for that gate has status == "passed", otherwise the most severe
status), then set gates_total = number of unique keys and build gate_breakdown
from the aggregated dict; update the code paths that reference gates_total and
gate_breakdown accordingly.

- Fix gate status comparison: use "satisfied" (domain model) not "passed"
- Fix 0/0 gates edge case rendering as green instead of yellow
- Fix nested <a> inside <button> (invalid HTML, accessibility issue)
- Align test fixtures to use "satisfied" status string
@claude
Copy link
Copy Markdown

claude bot commented Apr 15, 2026

Review: PR History with Proof Snapshots (#572)

Overall this is a well-structured feature — clear layer separation (ledger → router → component), good test coverage across all three layers, and proper handling of loading/empty/error states in the UI. A few things worth addressing before merge, some of which overlap with CodeRabbit's flags:


Issues to address

1. Gate deduplication (overlaps CodeRabbit flag)

In pr_v2.py (create PR flow, ~line 473), the loop builds gate_breakdown by iterating every req.obligations across all requirements. If the same gate (e.g., unit_tests) appears in multiple requirements, it gets counted multiple times — both in gates_total and in gate_breakdown. The snapshot will over-count and show duplicate gate rows in the UI.

Fix: aggregate by ob.gate.value (last-wins, or "passed" wins over "failed"), then compute gates_total = len(aggregated).

2. Snapshot captured at PR create, not merge

The proof snapshot fires inside create_pull_request — before the PR is reviewed or merged. This means the snapshot always reflects the state at PR open time, not the final merge state. If any gates get fixed after the PR is opened, the snapshot won't reflect that. The PR description says "captured at PR creation time" which makes this intentional, but it's worth a comment in the code (or a decision to move it to a merge hook in a follow-up) so the next reader doesn't assume this is a merge-time snapshot.

3. total field semantics

PRHistoryResponse.total is always len(pull_requests) — it's redundant with the list length and doesn't represent the total across all GitHub pages. This is fine for now, but it creates a false impression of "total available". Consider removing it or renaming to returned until pagination is added.

4. gate.status rendering — only \"satisfied\" is checked

PRHistoryPanel.tsx line 1102: gate.status === 'satisfied' → green checkmark, everything else → red X. But PROOF9 has additional statuses (\"waived\", \"not_applicable\", etc.). A waived gate showing as a red X would be misleading. Consider handling the known statuses explicitly or mapping to a neutral icon for unknown ones.

5. Redundant init_proof_tables call in hot path

Inside the create PR proof-capture block (pr_v2.py ~line 467), init_proof_tables(workspace) is called explicitly. But save_pr_proof_snapshot already calls _ensure_tables(workspace) internally, which calls init_proof_tables if needed. The explicit call is harmless but redundant — remove it to keep the caller simple.

6. SWR key / fetcher alignment

const swrKey = `/api/v2/pr/history?workspace_path=${encodeURIComponent(workspacePath)}`;
const { data } = useSWR<PRHistoryResponse>(
  swrKey,
  () => prApi.getHistory(workspacePath)  // constructs URL differently, ignores limit
);

The SWR key and the fetcher construct different URLs. If prApi.getHistory is later called with a limit argument, the cache key won't include it and stale data will be returned. Either encode limit into the key or use the key URL directly as the fetcher argument (e.g., pass swrKey as the SWR key and have the fetcher accept it).


Nitpicks (non-blocking)

  • _ensure_tables double-query: The new check for pr_proof_snapshots only runs when proof_runs already exists. This is logically correct (both are created together by init_proof_tables) but the branching is subtle. A comment explaining the invariant would help future readers.

  • limit not forwarded from component: PRHistoryPanel calls prApi.getHistory(workspacePath) without forwarding any limit prop, so the API default (10) is always used. Fine for now, but document or expose this if a "Load more" pattern is intended.

  • Test: limit test identity (CodeRabbit also flagged): test_limit_parameter only checks count, not which PRs are returned. Since PRs are sorted newest-first, assert [pr["number"] for pr in items] == [3, 2].


What's working well

  • INSERT OR REPLACE + isolated workspace_id on the primary key is the right approach for snapshot upserts.
  • The Optional[str] author field defaults gracefully for PRs where the user object is missing.
  • Error propagation from GitHubAPIError is consistent with the rest of pr_v2.py.
  • Frontend loading/empty/error/data states are all covered in the Jest suite.
  • pytestmark = pytest.mark.v2 is correctly applied on both test modules.

The gate deduplication (#1) and status rendering (#4) are the most important to address before this goes in. The rest can be sequenced as follow-ups if needed.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (2)
web-ui/src/components/review/PRHistoryPanel.tsx (1)

138-152: Potential duplicate React keys if backend sends duplicate gates.

Using gate.gate as the key assumes unique gate names in gate_breakdown. If the backend sends duplicate gate entries (related to the obligation counting issue flagged in the backend review), React will warn about duplicate keys and may exhibit rendering issues.

Consider adding an index suffix as a fallback or addressing deduplication on the backend.

♻️ Defensive key using index
-                    {pr.proof_snapshot.gate_breakdown.map((gate: GateBreakdownItem) => (
+                    {pr.proof_snapshot.gate_breakdown.map((gate: GateBreakdownItem, idx: number) => (
                       <div
-                        key={gate.gate}
+                        key={`${gate.gate}-${idx}`}
                         className="flex items-center gap-2 text-xs"
                       >
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web-ui/src/components/review/PRHistoryPanel.tsx` around lines 138 - 152, The
current mapping over pr.proof_snapshot.gate_breakdown uses gate.gate as the
React key which can collide if duplicate GateBreakdownItem.gate values are
returned; change the key to a stable composite or index-fallback (e.g., use
`${gate.gate}-${i}` or similar) when rendering the list in the component that
maps pr.proof_snapshot.gate_breakdown so each element has a unique key, or
deduplicate/normalize entries upstream before mapping to avoid duplicates.
codeframe/ui/routers/pr_v2.py (1)

340-353: Synchronous SQLite call in async endpoint may block the event loop.

get_pr_proof_snapshot performs synchronous database I/O. While the impact is minimal for SQLite on fast storage, this pattern can block other coroutines under load. Consider wrapping in asyncio.to_thread() for consistency with async best practices.

♻️ Optional improvement using asyncio.to_thread
         for pr in merged:
-            snapshot = get_pr_proof_snapshot(workspace, pr.number)
+            snapshot = await asyncio.to_thread(get_pr_proof_snapshot, workspace, pr.number)
             proof_snapshot = None
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@codeframe/ui/routers/pr_v2.py` around lines 340 - 353, The call to the
synchronous DB helper get_pr_proof_snapshot(workspace, pr.number) in the async
handler should be moved off the event loop: import asyncio and replace the
direct call with an awaited asyncio.to_thread(...) invocation (e.g., snapshot =
await asyncio.to_thread(get_pr_proof_snapshot, workspace, pr.number)) so the
synchronous SQLite I/O does not block other coroutines while constructing
PRHistoryItem entries.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@codeframe/ui/routers/pr_v2.py`:
- Around line 340-353: The call to the synchronous DB helper
get_pr_proof_snapshot(workspace, pr.number) in the async handler should be moved
off the event loop: import asyncio and replace the direct call with an awaited
asyncio.to_thread(...) invocation (e.g., snapshot = await
asyncio.to_thread(get_pr_proof_snapshot, workspace, pr.number)) so the
synchronous SQLite I/O does not block other coroutines while constructing
PRHistoryItem entries.

In `@web-ui/src/components/review/PRHistoryPanel.tsx`:
- Around line 138-152: The current mapping over pr.proof_snapshot.gate_breakdown
uses gate.gate as the React key which can collide if duplicate
GateBreakdownItem.gate values are returned; change the key to a stable composite
or index-fallback (e.g., use `${gate.gate}-${i}` or similar) when rendering the
list in the component that maps pr.proof_snapshot.gate_breakdown so each element
has a unique key, or deduplicate/normalize entries upstream before mapping to
avoid duplicates.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: dbf7014e-607a-4b90-b48b-bfabb3dec451

📥 Commits

Reviewing files that changed from the base of the PR and between 1e5669e and 89c4430.

📒 Files selected for processing (4)
  • codeframe/ui/routers/pr_v2.py
  • tests/core/test_proof_snapshot.py
  • tests/ui/test_pr_history.py
  • web-ui/src/components/review/PRHistoryPanel.tsx
✅ Files skipped from review due to trivial changes (2)
  • tests/core/test_proof_snapshot.py
  • tests/ui/test_pr_history.py

@frankbria frankbria merged commit 2df7918 into main Apr 15, 2026
12 checks passed
@frankbria frankbria deleted the feature/issue-572-pr-history-proof-snapshots branch April 15, 2026 03:58
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.

[Phase 4B] Post-merge: PR history view with proof snapshots

1 participant