Skip to content

feat(api): PROOF9 REST API router — /api/v2/proof endpoints - #459

Merged
frankbria merged 3 commits into
mainfrom
feat/proof-v2-router-456
Mar 20, 2026
Merged

feat(api): PROOF9 REST API router — /api/v2/proof endpoints#459
frankbria merged 3 commits into
mainfrom
feat/proof-v2-router-456

Conversation

@frankbria

@frankbria frankbria commented Mar 20, 2026

Copy link
Copy Markdown
Owner

Summary

Closes #456.

Adds proof_v2.py — the missing v2 REST API router for the PROOF9 quality system. Follows the thin-adapter pattern used by all other 18 v2 routers.

  • 7 endpoints mapping to core/proof/{capture,ledger,runner}.py functions
  • Inline Pydantic models (pattern matches tasks_v2.py, blockers_v2.py)
  • Rate limiting: @rate_limit_standard() for reads/writes, @rate_limit_ai() for POST /run (gate execution is compute-heavy)
  • OpenAPI tag proof-v2 added — proof endpoints now appear in /docs
  • Server registration in server.py

Endpoints

Method Path Core function
POST /api/v2/proof/requirements capture_requirement()
GET /api/v2/proof/requirements list_requirements()
GET /api/v2/proof/requirements/{id} get_requirement()
POST /api/v2/proof/run run_proof()
POST /api/v2/proof/requirements/{id}/waive waive_requirement()
GET /api/v2/proof/status list_requirements() aggregated
GET /api/v2/proof/requirements/{id}/evidence list_evidence()

Test plan

  • 35 new integration tests in tests/ui/test_proof_v2.py — all passing
  • Existing 42 proof core tests (tests/core/test_proof9.py) — all still passing
  • ruff check clean on both new files
  • Thin adapter: zero business logic in router, all delegation to core/proof/*

Summary by CodeRabbit

  • New Features
    • Added a v2 Proof REST API to capture/list/get requirements, run proofs, view aggregated status, manage waivers, and list evidence; grouped under a new OpenAPI tag with rate limits and structured error responses.
  • Tests
    • Added integration tests covering capture, listing/filtering, retrieval, run, waive, status, evidence endpoints and v2 error/validation behavior.
  • Documentation
    • Updated docs to include the new proof-v2 router and adjust router counts.

Add proof_v2.py router exposing PROOF9 quality system via REST API:

- POST /api/v2/proof/requirements     — capture_requirement() (classify glitch → obligations → stubs)
- GET  /api/v2/proof/requirements     — list_requirements() with optional ?status= filter
- GET  /api/v2/proof/requirements/{id} — get_requirement()
- POST /api/v2/proof/run              — run_proof() with full/gate_filter params
- POST /api/v2/proof/requirements/{id}/waive — waive_requirement()
- GET  /api/v2/proof/status           — aggregated counts + full requirement list
- GET  /api/v2/proof/requirements/{id}/evidence — list_evidence()

Router registered in server.py; OpenAPI tag "proof-v2" added to /docs.
35 new integration tests in tests/ui/test_proof_v2.py (all passing).

Thin-adapter pattern: all logic delegates to codeframe.core.proof.*.
@coderabbitai

coderabbitai Bot commented Mar 20, 2026

Copy link
Copy Markdown
Contributor

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 7f9a7dba-a499-4fe5-8e3a-20f2f94ede9d

📥 Commits

Reviewing files that changed from the base of the PR and between 422c63c and 129052b.

📒 Files selected for processing (1)
  • CLAUDE.md
✅ Files skipped from review due to trivial changes (1)
  • CLAUDE.md

Walkthrough

A new v2 FastAPI router was added at /api/v2/proof exposing endpoints to capture, list, fetch, run, waive, aggregate status, and list evidence for PROOF9; the router delegates to core proof functions, is registered in the server OpenAPI tags, and is covered by integration tests.

Changes

Cohort / File(s) Summary
PROOF9 Router
codeframe/ui/routers/proof_v2.py
New v2 FastAPI router (/api/v2/proof) with Pydantic request/response models, endpoints for requirements (POST/GET/GET by id), run (POST), waive (POST), status (GET), and evidence (GET). Uses get_v2_workspace, maps to core.proof functions, includes validation, 404/500 handling, rate limits, helpers to serialize requirements and count statuses.
Server integration
codeframe/ui/server.py
Registers the new router via app.include_router(proof_v2.router) and adds an proof-v2 OpenAPI tag/description.
Tests
tests/ui/test_proof_v2.py
New FastAPI integration tests mounting the router and overriding get_v2_workspace. Exercises capture, listing/filtering, fetch-by-id, run (including filters and full flag), waive (with expiry), status aggregation, evidence listing, and v2-style error payloads/validation.
Docs
CLAUDE.md
Updated documentation to include the new proof_v2 router and adjust the reported v2 router count.

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant Router as ProofV2 Router
    participant Workspace as Workspace (dep)
    participant Core as core.proof (ledger/runner)

    Client->>Router: POST /api/v2/proof/requirements (capture payload)
    Router->>Workspace: get_v2_workspace()
    Router->>Core: capture_requirement(workspace, payload)
    Core-->>Router: requirement object
    Router-->>Client: 201 Created (RequirementResponse)

    Client->>Router: POST /api/v2/proof/run (RunProofRequest)
    Router->>Workspace: get_v2_workspace()
    Router->>Core: run_proof(workspace, run_id, filters)
    Core-->>Router: run results (gate outcomes)
    Router-->>Client: 200 OK (RunProofResponse with run_id and results)
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐰 I hopped a route from core to sky,

Seven endpoints now dance and vie,
Capture, run, and waive with cheer,
Evidence whispers, "We're all here",
A tiny rabbit claps: "API!"

🚥 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 'feat(api): PROOF9 REST API router — /api/v2/proof endpoints' accurately describes the main change: adding a new v2 REST API router for the PROOF9 quality system with endpoints under /api/v2/proof.
Linked Issues check ✅ Passed All seven required endpoints from issue #456 are implemented (capture, list, show, run, waive, status, evidence); router delegated to core functions; registered in server.py with Pydantic models; integration tests provided; and OpenAPI tags configured.
Out of Scope Changes check ✅ Passed All changes (proof_v2.py router, server.py registration, test_proof_v2.py tests, and CLAUDE.md documentation) are directly scoped to issue #456 requirements for the PROOF9 REST API router implementation.
Docstring Coverage ✅ Passed Docstring coverage is 88.46% which is sufficient. The required threshold is 80.00%.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/proof-v2-router-456
📝 Coding Plan
  • Generate coding plan for human review comments

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

@claude

claude Bot commented Mar 20, 2026

Copy link
Copy Markdown

Review: PROOF9 REST API Router

This is a clean, well-structured addition. The thin-adapter pattern is correctly applied, the rate limiting choices are appropriate (rate_limit_ai on /run is the right call), and the OpenAPI tag registration in server.py follows existing conventions. Test coverage is solid at 35 cases.

A few items worth addressing:

1. import uuid inside function body

In run_proof_endpoint, import uuid is placed inside the function body. Move it to the top-level imports with the other stdlib imports.

2. run_id is ephemeral and untraceable

The generated run_id is a random 8-char UUID suffix that exists only in the HTTP response — not persisted anywhere, so callers can't use it to look up run details. If run_proof() in core returns a run identifier, use that. If not, either omit the field or add a docstring/comment clarifying it's a transient correlation ID only.

3. Double list_requirements call when filtering

When a ?status filter is applied, the ledger is queried twice — once for filtered results and once for the full set (for by_status). Since by_status needs everything anyway, load all requirements once and filter in Python to halve the I/O.

4. total semantics are ambiguous when filtering

When ?status=open is passed, total returns the filtered count, not the overall total. This is surprising for callers doing pagination or progress tracking. Consider renaming to count (filtered) and adding a separate total (all), or document the behavior explicitly in the response model.

5. waive_requirement_endpoint has no error handling

capture_requirement_endpoint and run_proof_endpoint both wrap core calls in try/except. waive_requirement_endpoint does not — if waive_requirement raises or returns an unexpected value, it becomes an unhandled 500. Add a try/except consistent with the other endpoints.

6. Missing auth dependency

Other v2 routers include API key auth via a dependency. These endpoints have none. If this is intentional (e.g., internal use only, or auth added in a follow-up), add a comment explaining why it's omitted so the omission is clearly deliberate rather than accidental.

7. Minor: non-standard client.workspace attribute in tests

test_capture_persists_to_core reads test_client.workspace, which is set as an arbitrary attribute on the TestClient object. The test_workspace fixture is already available in scope — pass it as a direct parameter to the test method instead for clarity and resilience.


Overall this is ready to merge with items 1, 5, and 6 resolved. Items 2–4 are judgment calls that can be deferred to follow-up if preferred.

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

🤖 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/proof_v2.py`:
- Around line 74-78: The schema only declares `gate` but the API contract
expects `gate_filter`; update the RunProofRequest model to include an
Optional[Gate] field named `gate_filter` (use Field(default=None,
description="Run only this gate (unit, sec, contract, etc.)") and consider
adding aliasing/backwards-compatibility if needed), and ensure code that
consumes RunProofRequest (references to `gate`) reads from `gate_filter` when
present (or normalizes into a single internal variable) so POST bodies using
`gate_filter` validate and are handled correctly.
- Line 224: The endpoints currently accept workspace: Workspace =
Depends(get_v2_workspace) where get_v2_workspace allows an optional
workspace_path; update each endpoint that uses this dependency (any function
with the parameter workspace: Workspace = Depends(get_v2_workspace)) to require
an explicit workspace_path query param (e.g., add workspace_path: str =
Query(..., alias="workspace_path") to the endpoint signature) and ensure the
Workspace dependency is resolved using that explicit workspace_path (replace the
bare Depends(get_v2_workspace) usage with a dependency invocation that passes
the required workspace_path into get_v2_workspace or create a small wrapper
dependency that accepts workspace_path and calls
get_v2_workspace(workspace_path)); apply this change to every endpoint currently
using get_v2_workspace so workspace_path is mandatory and unambiguous.
- Around line 318-337: run_proof writes artifacts/evidence and returns the
authoritative run identifier, but the handler currently overwrites it by
generating a new UUID for run_id before returning RunProofResponse; instead,
extract and use the run_id produced by run_proof (from its return value or
metadata it returns) and pass that into RunProofResponse.run_id, removing the
local uuid generation; keep the serialized results construction and only replace
the run_id assignment so clients can correlate the response with the persisted
evidence.
🪄 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: c5810930-b16d-4e74-957b-542628950739

📥 Commits

Reviewing files that changed from the base of the PR and between 4f37b43 and fb75e23.

📒 Files selected for processing (3)
  • codeframe/ui/routers/proof_v2.py
  • codeframe/ui/server.py
  • tests/ui/test_proof_v2.py


logger = logging.getLogger(__name__)

router = APIRouter(prefix="/api/v2/proof", tags=["proof-v2"])

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

Add scoped auth to this router before shipping.

These handlers only apply rate limiting; none of them enforce the required auth scopes. At minimum, the GET routes should require read and the mutating POST routes should require write/admin as appropriate. As written, this router can be mounted and exercised without any router-level authorization boundary, and the current test app does exactly that. As per coding guidelines, API key authentication in routers must validate scopes via auth/api_key_service.py with supported scopes: read, write, admin.

Also applies to: 219-220, 252-253, 284-285, 305-306, 347-348, 377-378, 396-397

Comment on lines +74 to +78
class RunProofRequest(BaseModel):
"""Request body for running proof obligations."""

full: bool = Field(default=False, description="Run ALL obligations regardless of scope")
gate: Optional[Gate] = Field(default=None, description="Run only this gate (unit, sec, contract, etc.)")

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

Expose the documented gate_filter field here.

The PR contract says POST /api/v2/proof/run accepts full and gate_filter, but this schema only accepts gate. Clients following the documented request shape will get a 422, and the current tests won't catch it because they post the same non-contract field name.

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

In `@codeframe/ui/routers/proof_v2.py` around lines 74 - 78, The schema only
declares `gate` but the API contract expects `gate_filter`; update the
RunProofRequest model to include an Optional[Gate] field named `gate_filter`
(use Field(default=None, description="Run only this gate (unit, sec, contract,
etc.)") and consider adding aliasing/backwards-compatibility if needed), and
ensure code that consumes RunProofRequest (references to `gate`) reads from
`gate_filter` when present (or normalizes into a single internal variable) so
POST bodies using `gate_filter` validate and are handled correctly.

async def capture_requirement_endpoint(
request: Request,
body: CaptureRequirementRequest,
workspace: Workspace = Depends(get_v2_workspace),

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

Make workspace_path explicit on every endpoint.

get_v2_workspace declares workspace_path as optional and falls back to the default workspace/CWD, so these routes currently succeed without the query parameter. That violates the v2 router contract and makes the target workspace ambiguous in multi-workspace deployments; the dependency override in tests/ui/test_proof_v2.py also hides the regression. As per coding guidelines, All API endpoints must require workspace_path query parameter.

Also applies to: 257-257, 289-289, 310-310, 353-353, 381-381, 401-401

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

In `@codeframe/ui/routers/proof_v2.py` at line 224, The endpoints currently accept
workspace: Workspace = Depends(get_v2_workspace) where get_v2_workspace allows
an optional workspace_path; update each endpoint that uses this dependency (any
function with the parameter workspace: Workspace = Depends(get_v2_workspace)) to
require an explicit workspace_path query param (e.g., add workspace_path: str =
Query(..., alias="workspace_path") to the endpoint signature) and ensure the
Workspace dependency is resolved using that explicit workspace_path (replace the
bare Depends(get_v2_workspace) usage with a dependency invocation that passes
the required workspace_path into get_v2_workspace or create a small wrapper
dependency that accepts workspace_path and calls
get_v2_workspace(workspace_path)); apply this change to every endpoint currently
using get_v2_workspace so workspace_path is mandatory and unambiguous.

Comment on lines +318 to +337
try:
results = run_proof(
workspace,
full=body.full,
gate_filter=body.gate,
)
# Serialize: dict[req_id → list[tuple[Gate, bool]]] → JSON-safe
serialized = {
req_id: [{"gate": gate.value, "satisfied": satisfied} for gate, satisfied in gate_results]
for req_id, gate_results in results.items()
}
# Derive a run_id from the results or generate one
import uuid
run_id = str(uuid.uuid4())[:8]

return RunProofResponse(
success=True,
run_id=run_id,
results=serialized,
message=f"Proof run complete: {len(results)} requirement(s) evaluated.",

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

Return the same run_id that was persisted.

codeframe.core.proof.runner.run_proof() is the code that writes artifacts and evidence, but Line 331 creates a brand new UUID after that call. The response run_id therefore won't match the evidence rows generated by the run, so clients can't correlate /run with /requirements/{id}/evidence.

💡 Suggested fix
     try:
+        import uuid
+        run_id = str(uuid.uuid4())[:8]
         results = run_proof(
             workspace,
             full=body.full,
             gate_filter=body.gate,
+            run_id=run_id,
         )
         # Serialize: dict[req_id → list[tuple[Gate, bool]]] → JSON-safe
         serialized = {
             req_id: [{"gate": gate.value, "satisfied": satisfied} for gate, satisfied in gate_results]
             for req_id, gate_results in results.items()
         }
-        # Derive a run_id from the results or generate one
-        import uuid
-        run_id = str(uuid.uuid4())[:8]
-
         return RunProofResponse(
             success=True,
             run_id=run_id,
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@codeframe/ui/routers/proof_v2.py` around lines 318 - 337, run_proof writes
artifacts/evidence and returns the authoritative run identifier, but the handler
currently overwrites it by generating a new UUID for run_id before returning
RunProofResponse; instead, extract and use the run_id produced by run_proof
(from its return value or metadata it returns) and pass that into
RunProofResponse.run_id, removing the local uuid generation; keep the serialized
results construction and only replace the run_id assignment so clients can
correlate the response with the persisted evidence.

@frankbria

Copy link
Copy Markdown
Owner Author

CodeRabbit Review Response

Fixed (1 item):

  • run_id mismatch — run_id now generated before calling run_proof() and passed in, so the response ID matches the persisted evidence records.

Deferred (1 item):

Dismissed (2 items):

  • gate vs gate_filtergate matches the implementation plan and run_proof(gate_filter=body.gate) correctly maps it. No documented API contract specifies gate_filter as the request field name.
  • ❌ Explicit workspace_path query param — all 18 existing v2 routers use Depends(get_v2_workspace) without an explicit param. This pattern is established project-wide.

@claude

claude Bot commented Mar 20, 2026

Copy link
Copy Markdown

Code Review — PR #459: PROOF9 REST API Router

Overall this is a solid, well-structured addition. The thin-adapter pattern is followed correctly, core is kept headless, and the test suite is comprehensive. Below are issues grouped by severity.


Critical

run_proof is a synchronous, potentially long-running operation called directly in an async handler

/api/v2/proof/run calls run_proof(workspace, ...) inline inside an async def endpoint. run_proof executes shell gates (pytest, ruff) via subprocess — these are blocking I/O operations. Running them directly inside an async handler will block the entire uvicorn event loop for the duration.

The other compute-heavy endpoints in this repo delegate to background tasks or use asyncio.to_thread. This one should too:

import asyncio
results = await asyncio.run_in_executor(None, lambda: run_proof(workspace, full=body.full, gate_filter=body.gate, run_id=run_id))

Without this, a long proof run will make the server unresponsive to all other requests.


Important

Double list_requirements query in list_requirements_endpoint

reqs = list_requirements(workspace, status=status_filter)
all_reqs = list_requirements(workspace) if status_filter else reqs

When a status filter is active, this issues two sequential database queries — one filtered, one for all rows (to compute by_status). At scale this is wasteful. The by-status count should be computed in a single pass, or list_requirements should return aggregated counts alongside results. The simpler fix is to always fetch all reqs and filter in Python when the count is needed for the response — avoiding the double round-trip.

_count_by_status initialises counts from ReqStatus but then uses .get() — the initialiser is redundant

counts: dict[str, int] = {s.value: 0 for s in ReqStatus}
for req in reqs:
    counts[req.status.value] = counts.get(req.status.value, 0) + 1

The dict comprehension pre-fills all statuses with 0, but then counts.get(req.status.value, 0) is used for the increment — which would also handle keys not in the initial set. The intent is for all statuses to appear in by_status even when zero, which is good. But use counts[req.status.value] += 1 (not .get) to be consistent with that intent; the pre-fill already guarantees the key is present for every valid status.

proof_status_endpoint re-fetches all requirements independently from list_requirements_endpoint

GET /status loads all requirements plus iterates them twice (once in list_requirements, once in _count_by_status). If this endpoint is called frequently for monitoring dashboards it should reuse the same count aggregation that list_requirements_endpoint already produces. Consider sharing the helper or having the status endpoint call list_requirements_endpoint's logic.

Missing waive idempotency / conflict handling

If a requirement is already waived and POST /requirements/{id}/waive is called again, waive_requirement in the ledger will overwrite the existing waiver silently (UPDATE proof_requirements SET status = ?, waiver = ?). There is no 409 conflict response. Callers currently have no way to tell whether they replaced an existing waiver or set a new one. At minimum a warning in the docstring would help; ideally return a 409 or include the previous waiver state in the response.

run_id generated with str(uuid.uuid4())[:8] — collision risk

An 8-character hex prefix of a UUID provides only ~4 billion unique values before truncation artifacts. This is the same pattern used in runner.py itself (so it is consistent), but it is worth flagging: for high-frequency CI runs in the same workspace the probability of a run_id collision within a day is non-negligible. Evidence records are keyed by (req_id, gate, run_id) path, so a collision would silently overwrite an artifact file. Consider using uuid.uuid4().hex[:12] (12 hex chars = 48 bits) as an improvement in both places.


Maintainability / Nice-to-Have

uuid is imported but only used for run_id generation that is then passed to core

runner.py already auto-generates a run_id when none is provided. The router generates one first so the response run_id matches what evidence records store — this is the right approach and the comment explains it. No action needed, but a short inline test asserting the round-trip (router run_id == evidence run_id) would harden this.

ObligationOut.status is typed str, not an enum

Obligation.status in the core model is a raw string ("pending", "satisfied", "failed"). The response model mirrors this as str, which is fine for now. However this will silently pass any string through if the core contract changes. Consider adding a Literal["pending", "satisfied", "failed"] constraint, or adding a ProofObligationStatus enum to core/proof/models.py — this is a broader PROOF9 concern that this PR surfaces but doesn't need to solve.

test_client.workspace is set as an attribute on TestClient

client = TestClient(app)
client.workspace = test_workspace
return client

TestClient does not declare a workspace attribute, so this bypasses type checking. The workspace is only accessed in test_capture_persists_to_core via test_client.workspace. A cleaner approach would be to accept test_workspace as a separate fixture parameter in tests that need it, avoiding duck-typing on TestClient.

list_evidence_endpoint performs a get_requirement existence check before calling list_evidence

This is correct behaviour (return 404 for unknown req_id), but it adds an extra DB round-trip. list_evidence returning an empty list for a non-existent req_id is indistinguishable from a valid req with no evidence. The current design is the right call for UX clarity — just noting this is a deliberate trade-off.

No scope field in RequirementResponse

The core Requirement dataclass includes a scope: RequirementScope field (routes, components, apis, files, tags) that is used by run_proof for scope intersection. This field is intentionally omitted from the response. If API consumers want to query what code areas a requirement covers, they currently cannot. Consider adding it or adding a comment explaining the omission.

Architecture note: no cf proof CLI commands are wired in cli/app.py

CLAUDE.md Rule 0 and the Golden Path require the CLI to work without a server. This PR adds the REST layer but the cf proof capture, cf proof run, and cf proof status CLI commands are not yet wired. The PR description and issue #456 note this is a REST-first delivery — acceptable if the CLI commands are tracked as a follow-up. Worth confirming issue #422 or a child issue covers the CLI side before closing the PROOF9 milestone.


What is done well

  • Architecture boundary is clean: codeframe/ui/routers/proof_v2.py has zero business logic; all delegation is to codeframe/core/proof/*. No FastAPI or HTTP imports leak into core.
  • Rate limiting is applied correctly: @rate_limit_standard() on reads/writes, @rate_limit_ai() on POST /run — the rationale is sound.
  • server.py registration and OpenAPI tag are both present and consistent with the other 18 routers.
  • Test coverage is thorough: 35 tests cover happy path, validation errors (422), not-found (404), bad filter (400), and response shape assertions. pytestmark = pytest.mark.v2 is correctly applied.
  • Structured error responses (api_error with ErrorCodes) are used consistently across all error paths.
  • _req_to_response centralises the conversion from core dataclass to Pydantic model correctly, including enum .value extraction and optional field handling.

Summary: The blocking I/O issue in POST /run is the only change I would require before merge. The double-query in list and the run_id truncation risk are important to track. Everything else is a suggestion.

@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)
codeframe/ui/routers/proof_v2.py (3)

275-282: Avoid double database call when filtering by status.

When status_filter is set, list_requirements is called twice: once filtered (line 275) and once unfiltered (line 276) for aggregate counts. Consider fetching all requirements once and filtering in memory.

♻️ Proposed optimization
-    reqs = list_requirements(workspace, status=status_filter)
-    all_reqs = list_requirements(workspace) if status_filter else reqs
+    all_reqs = list_requirements(workspace)
+    reqs = [r for r in all_reqs if r.status == status_filter] if status_filter else all_reqs

     return RequirementListResponse(
         requirements=[_req_to_response(r) for r in reqs],
         total=len(reqs),
         by_status=_count_by_status(all_reqs),
     )
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@codeframe/ui/routers/proof_v2.py` around lines 275 - 282, The current code
calls list_requirements twice (list_requirements(workspace,
status=status_filter) and list_requirements(workspace)) causing a duplicate DB
call when status_filter is set; instead call list_requirements(workspace) once
to get all_reqs, then derive reqs in-memory by applying the status_filter (e.g.,
filter all_reqs by requirement.status) and pass
RequirementListResponse(requirements=[_req_to_response(r) for r in reqs],
total=len(reqs), by_status=_count_by_status(all_reqs)); update references to
reqs/all_reqs accordingly and keep use of _req_to_response and _count_by_status.

245-250: Broad exception handling may mask validation errors.

Catching all exceptions and returning 500 with EXECUTION_FAILED could hide specific errors from core (e.g., validation failures, classification errors) that might warrant different status codes (400, 422). Consider catching specific exception types or re-raising HTTPException instances.

♻️ Proposed approach
     except Exception as e:
+        if isinstance(e, HTTPException):
+            raise
         logger.error("Failed to capture requirement: %s", e, exc_info=True)
         raise HTTPException(
             status_code=500,
             detail=api_error("Failed to capture requirement", ErrorCodes.EXECUTION_FAILED, str(e)),
         )
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@codeframe/ui/routers/proof_v2.py` around lines 245 - 250, The catch-all
except block in the proof capture flow (the except Exception as e handling that
logs and raises a 500) can mask validation or domain errors; update the handler
in proof_v2.py to catch and handle specific exception types (e.g.,
ValidationError, ValueError, ClassificationError or whatever domain exceptions
your core raises) and return appropriate HTTPException status codes (400/422) or
re-raise existing HTTPException instances unchanged; only use a generic except
Exception as e as a final fallback to log and return the 500 with
ErrorCodes.EXECUTION_FAILED. Ensure you reference the existing logger.error call
and the HTTPException raise site so you modify that block to differentiate and
re-raise HTTPException when encountered.

207-212: Minor: Simplify counter increment.

Since counts is pre-initialized with all ReqStatus values at line 209, counts.get(..., 0) is unnecessary.

♻️ Proposed simplification
 def _count_by_status(reqs) -> dict[str, int]:
     """Aggregate requirement counts by status value."""
     counts: dict[str, int] = {s.value: 0 for s in ReqStatus}
     for req in reqs:
-        counts[req.status.value] = counts.get(req.status.value, 0) + 1
+        counts[req.status.value] += 1
     return counts
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@codeframe/ui/routers/proof_v2.py` around lines 207 - 212, The
_count_by_status function pre-initializes counts for every ReqStatus so using
counts.get(..., 0) is redundant; change the increment inside the loop in
_count_by_status to directly increment the pre-existing key (e.g.,
counts[req.status.value] += 1) and keep the dict[str, int] typing and
initialization using ReqStatus to ensure all statuses exist before counting.
🤖 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/proof_v2.py`:
- Around line 374-375: waive_requirement(workspace, req_id, waiver) can return
None if the requirement was deleted between the earlier existence check and the
waiver attempt, so guard against that before calling _req_to_response; after
calling updated = waive_requirement(...), check if updated is None and return an
appropriate error response (e.g., 404/HTTPException or a JSON error) referencing
req_id and context, otherwise call and return _req_to_response(updated).

---

Nitpick comments:
In `@codeframe/ui/routers/proof_v2.py`:
- Around line 275-282: The current code calls list_requirements twice
(list_requirements(workspace, status=status_filter) and
list_requirements(workspace)) causing a duplicate DB call when status_filter is
set; instead call list_requirements(workspace) once to get all_reqs, then derive
reqs in-memory by applying the status_filter (e.g., filter all_reqs by
requirement.status) and pass
RequirementListResponse(requirements=[_req_to_response(r) for r in reqs],
total=len(reqs), by_status=_count_by_status(all_reqs)); update references to
reqs/all_reqs accordingly and keep use of _req_to_response and _count_by_status.
- Around line 245-250: The catch-all except block in the proof capture flow (the
except Exception as e handling that logs and raises a 500) can mask validation
or domain errors; update the handler in proof_v2.py to catch and handle specific
exception types (e.g., ValidationError, ValueError, ClassificationError or
whatever domain exceptions your core raises) and return appropriate
HTTPException status codes (400/422) or re-raise existing HTTPException
instances unchanged; only use a generic except Exception as e as a final
fallback to log and return the 500 with ErrorCodes.EXECUTION_FAILED. Ensure you
reference the existing logger.error call and the HTTPException raise site so you
modify that block to differentiate and re-raise HTTPException when encountered.
- Around line 207-212: The _count_by_status function pre-initializes counts for
every ReqStatus so using counts.get(..., 0) is redundant; change the increment
inside the loop in _count_by_status to directly increment the pre-existing key
(e.g., counts[req.status.value] += 1) and keep the dict[str, int] typing and
initialization using ReqStatus to ensure all statuses exist before counting.
🪄 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: a9622a7d-2063-402d-955b-41f10f00c711

📥 Commits

Reviewing files that changed from the base of the PR and between fb75e23 and 422c63c.

📒 Files selected for processing (1)
  • codeframe/ui/routers/proof_v2.py

Comment on lines +374 to +375
updated = waive_requirement(workspace, req_id, waiver)
return _req_to_response(updated)

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 | 🟡 Minor

Handle potential None return from waive_requirement.

waive_requirement() returns Optional[Requirement]. If the requirement is deleted between the existence check (line 357) and the waive call (line 374), updated will be None, causing an error in _req_to_response().

🛡️ Proposed defensive check
     updated = waive_requirement(workspace, req_id, waiver)
+    if not updated:
+        raise HTTPException(
+            status_code=404,
+            detail=api_error(
+                f"Requirement not found: {req_id}",
+                ErrorCodes.NOT_FOUND,
+                "Requirement may have been deleted",
+            ),
+        )
     return _req_to_response(updated)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@codeframe/ui/routers/proof_v2.py` around lines 374 - 375,
waive_requirement(workspace, req_id, waiver) can return None if the requirement
was deleted between the earlier existence check and the waiver attempt, so guard
against that before calling _req_to_response; after calling updated =
waive_requirement(...), check if updated is None and return an appropriate error
response (e.g., 404/HTTPException or a JSON error) referencing req_id and
context, otherwise call and return _req_to_response(updated).

@claude

claude Bot commented Mar 20, 2026

Copy link
Copy Markdown

Follow-up to previous reviews

The diff is unchanged from my last review. Two items are still open:

1. Blocking I/O in POST /run — still unresolved (critical)

results = run_proof(workspace, full=body.full, gate_filter=body.gate, run_id=run_id)

This calls blocking subprocess I/O (pytest, ruff) directly inside an async def handler, stalling the event loop for the entire proof run duration. It needs to be offloaded:

import asyncio
results = await asyncio.to_thread(run_proof, workspace, full=body.full, gate_filter=body.gate, run_id=run_id)

asyncio.to_thread (Python 3.9+) is the cleaner form over run_in_executor(None, ...) for this pattern. Either works.

2. waive_requirement_endpoint — still no try/except around core call

The 404 guard exists but the waive_requirement(workspace, req_id, waiver) call below it is unprotected. If the ledger raises (e.g. constraint error, disk I/O), it surfaces as an unhandled 500 with no structured error response. Wrapping it in a try/except consistent with the other endpoints fixes this.

No new issues from the diff — the run_id fix from the first round is correctly in place. Once these two items are addressed this is ready.

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.

PROOF9: REST API router missing — no /api/v2/proof endpoints

1 participant