codeframe-ai 0.9.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- codeframe/__init__.py +11 -0
- codeframe/__main__.py +20 -0
- codeframe/adapters/__init__.py +5 -0
- codeframe/adapters/e2b/__init__.py +13 -0
- codeframe/adapters/e2b/adapter.py +342 -0
- codeframe/adapters/e2b/budget.py +71 -0
- codeframe/adapters/e2b/credential_scanner.py +134 -0
- codeframe/adapters/llm/__init__.py +92 -0
- codeframe/adapters/llm/anthropic.py +414 -0
- codeframe/adapters/llm/base.py +444 -0
- codeframe/adapters/llm/mock.py +281 -0
- codeframe/adapters/llm/openai.py +483 -0
- codeframe/agents/__init__.py +8 -0
- codeframe/agents/dependency_resolver.py +714 -0
- codeframe/auth/__init__.py +16 -0
- codeframe/auth/api_key_router.py +238 -0
- codeframe/auth/api_keys.py +156 -0
- codeframe/auth/dependencies.py +358 -0
- codeframe/auth/manager.py +178 -0
- codeframe/auth/models.py +30 -0
- codeframe/auth/router.py +93 -0
- codeframe/auth/schemas.py +15 -0
- codeframe/auth/scopes.py +53 -0
- codeframe/cli/__init__.py +12 -0
- codeframe/cli/__main__.py +20 -0
- codeframe/cli/api_client.py +275 -0
- codeframe/cli/app.py +5688 -0
- codeframe/cli/auth.py +122 -0
- codeframe/cli/auth_commands.py +958 -0
- codeframe/cli/commands/__init__.py +5 -0
- codeframe/cli/config_commands.py +79 -0
- codeframe/cli/dashboard_commands.py +67 -0
- codeframe/cli/engines_commands.py +205 -0
- codeframe/cli/env_commands.py +409 -0
- codeframe/cli/helpers.py +56 -0
- codeframe/cli/hooks_commands.py +208 -0
- codeframe/cli/import_commands.py +129 -0
- codeframe/cli/pr_commands.py +549 -0
- codeframe/cli/proof_commands.py +415 -0
- codeframe/cli/stats_commands.py +311 -0
- codeframe/cli/telemetry_runtime.py +153 -0
- codeframe/cli/validators.py +123 -0
- codeframe/config/rate_limits.py +165 -0
- codeframe/core/__init__.py +15 -0
- codeframe/core/adapters/__init__.py +43 -0
- codeframe/core/adapters/agent_adapter.py +114 -0
- codeframe/core/adapters/builtin.py +326 -0
- codeframe/core/adapters/claude_code.py +62 -0
- codeframe/core/adapters/codex.py +393 -0
- codeframe/core/adapters/git_utils.py +40 -0
- codeframe/core/adapters/kilocode.py +126 -0
- codeframe/core/adapters/opencode.py +48 -0
- codeframe/core/adapters/streaming_chat.py +483 -0
- codeframe/core/adapters/subprocess_adapter.py +213 -0
- codeframe/core/adapters/verification_wrapper.py +269 -0
- codeframe/core/agent.py +2183 -0
- codeframe/core/agents_config.py +569 -0
- codeframe/core/api_key_service.py +211 -0
- codeframe/core/artifacts.py +428 -0
- codeframe/core/blocker_detection.py +218 -0
- codeframe/core/blockers.py +433 -0
- codeframe/core/checkpoints.py +481 -0
- codeframe/core/conductor.py +2255 -0
- codeframe/core/config.py +827 -0
- codeframe/core/config_watcher.py +268 -0
- codeframe/core/context.py +542 -0
- codeframe/core/context_packager.py +234 -0
- codeframe/core/credentials.py +735 -0
- codeframe/core/dependency_analyzer.py +229 -0
- codeframe/core/dependency_graph.py +290 -0
- codeframe/core/diagnostic_agent.py +712 -0
- codeframe/core/diagnostics.py +616 -0
- codeframe/core/editor.py +556 -0
- codeframe/core/engine_registry.py +256 -0
- codeframe/core/engine_stats.py +231 -0
- codeframe/core/environment.py +697 -0
- codeframe/core/events.py +375 -0
- codeframe/core/executor.py +1005 -0
- codeframe/core/fix_tracker.py +480 -0
- codeframe/core/gates.py +1322 -0
- codeframe/core/git.py +477 -0
- codeframe/core/github_connect_service.py +178 -0
- codeframe/core/github_integration_config.py +118 -0
- codeframe/core/github_issues_service.py +449 -0
- codeframe/core/hooks.py +184 -0
- codeframe/core/importers/__init__.py +1 -0
- codeframe/core/importers/ralph.py +540 -0
- codeframe/core/installer.py +650 -0
- codeframe/core/models.py +1026 -0
- codeframe/core/notifications_config.py +183 -0
- codeframe/core/planner.py +437 -0
- codeframe/core/prd.py +670 -0
- codeframe/core/prd_discovery.py +1118 -0
- codeframe/core/prd_stress_test.py +499 -0
- codeframe/core/progress.py +126 -0
- codeframe/core/proof/__init__.py +34 -0
- codeframe/core/proof/capture.py +79 -0
- codeframe/core/proof/evidence.py +56 -0
- codeframe/core/proof/ledger.py +574 -0
- codeframe/core/proof/models.py +162 -0
- codeframe/core/proof/obligations.py +103 -0
- codeframe/core/proof/runner.py +233 -0
- codeframe/core/proof/scope.py +81 -0
- codeframe/core/proof/stubs.py +156 -0
- codeframe/core/quick_fixes.py +558 -0
- codeframe/core/react_agent.py +1650 -0
- codeframe/core/reconciliation.py +183 -0
- codeframe/core/replay.py +788 -0
- codeframe/core/review.py +285 -0
- codeframe/core/runtime.py +1134 -0
- codeframe/core/sandbox/__init__.py +27 -0
- codeframe/core/sandbox/context.py +98 -0
- codeframe/core/sandbox/worktree.py +20 -0
- codeframe/core/schedule.py +396 -0
- codeframe/core/stall_detector.py +71 -0
- codeframe/core/stall_monitor.py +134 -0
- codeframe/core/state_machine.py +121 -0
- codeframe/core/streaming.py +502 -0
- codeframe/core/task_tree.py +400 -0
- codeframe/core/tasks.py +1022 -0
- codeframe/core/telemetry.py +232 -0
- codeframe/core/templates.py +221 -0
- codeframe/core/tools.py +942 -0
- codeframe/core/workspace.py +887 -0
- codeframe/core/worktrees.py +276 -0
- codeframe/git/__init__.py +5 -0
- codeframe/git/github_integration.py +505 -0
- codeframe/lib/__init__.py +0 -0
- codeframe/lib/audit_logger.py +248 -0
- codeframe/lib/metrics_tracker.py +800 -0
- codeframe/lib/quality/__init__.py +7 -0
- codeframe/lib/quality/complexity_analyzer.py +316 -0
- codeframe/lib/quality/owasp_patterns.py +284 -0
- codeframe/lib/quality/security_scanner.py +250 -0
- codeframe/lib/rate_limiter.py +312 -0
- codeframe/notifications/__init__.py +0 -0
- codeframe/notifications/webhook.py +380 -0
- codeframe/planning/__init__.py +30 -0
- codeframe/planning/issue_generator.py +219 -0
- codeframe/planning/prd_template_functions.py +137 -0
- codeframe/planning/prd_templates.py +975 -0
- codeframe/planning/task_scheduler.py +511 -0
- codeframe/planning/task_templates.py +533 -0
- codeframe/platform_store/__init__.py +5 -0
- codeframe/platform_store/database.py +277 -0
- codeframe/platform_store/repositories/__init__.py +24 -0
- codeframe/platform_store/repositories/api_key_repository.py +245 -0
- codeframe/platform_store/repositories/audit_repository.py +67 -0
- codeframe/platform_store/repositories/base.py +295 -0
- codeframe/platform_store/repositories/interactive_sessions.py +165 -0
- codeframe/platform_store/repositories/token_repository.py +598 -0
- codeframe/platform_store/repositories/workspace_registry_repository.py +175 -0
- codeframe/platform_store/schema_manager.py +321 -0
- codeframe/templates/AGENTS.md.default +94 -0
- codeframe/tui/__init__.py +5 -0
- codeframe/tui/app.py +256 -0
- codeframe/tui/data_service.py +103 -0
- codeframe/ui/__init__.py +0 -0
- codeframe/ui/dependencies.py +103 -0
- codeframe/ui/models.py +999 -0
- codeframe/ui/response_models.py +201 -0
- codeframe/ui/routers/__init__.py +5 -0
- codeframe/ui/routers/_helpers.py +29 -0
- codeframe/ui/routers/batches_v2.py +315 -0
- codeframe/ui/routers/blockers_v2.py +320 -0
- codeframe/ui/routers/checkpoints_v2.py +310 -0
- codeframe/ui/routers/costs_v2.py +322 -0
- codeframe/ui/routers/diagnose_v2.py +225 -0
- codeframe/ui/routers/discovery_v2.py +417 -0
- codeframe/ui/routers/environment_v2.py +284 -0
- codeframe/ui/routers/events_v2.py +75 -0
- codeframe/ui/routers/gates_v2.py +166 -0
- codeframe/ui/routers/git_v2.py +284 -0
- codeframe/ui/routers/github_integrations_v2.py +532 -0
- codeframe/ui/routers/interactive_sessions_v2.py +238 -0
- codeframe/ui/routers/pr_v2.py +709 -0
- codeframe/ui/routers/prd_v2.py +695 -0
- codeframe/ui/routers/proof_v2.py +755 -0
- codeframe/ui/routers/review_v2.py +360 -0
- codeframe/ui/routers/schedule_v2.py +214 -0
- codeframe/ui/routers/session_chat_ws.py +354 -0
- codeframe/ui/routers/settings_v2.py +562 -0
- codeframe/ui/routers/streaming_v2.py +155 -0
- codeframe/ui/routers/tasks_v2.py +1098 -0
- codeframe/ui/routers/templates_v2.py +232 -0
- codeframe/ui/routers/terminal_ws.py +267 -0
- codeframe/ui/routers/workspace_v2.py +527 -0
- codeframe/ui/server.py +568 -0
- codeframe/ui/shared.py +241 -0
- codeframe/workspace/__init__.py +5 -0
- codeframe/workspace/manager.py +249 -0
- codeframe_ai-0.9.0.dist-info/METADATA +517 -0
- codeframe_ai-0.9.0.dist-info/RECORD +197 -0
- codeframe_ai-0.9.0.dist-info/WHEEL +5 -0
- codeframe_ai-0.9.0.dist-info/entry_points.txt +3 -0
- codeframe_ai-0.9.0.dist-info/licenses/LICENSE +661 -0
- codeframe_ai-0.9.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,755 @@
|
|
|
1
|
+
"""PROOF9 REST API router — thin adapter over codeframe.core.proof.
|
|
2
|
+
|
|
3
|
+
Maps HTTP endpoints to core proof functions (capture, list, get, run, waive,
|
|
4
|
+
status, evidence). No business logic lives here.
|
|
5
|
+
|
|
6
|
+
Routes:
|
|
7
|
+
POST /api/v2/proof/requirements capture_requirement()
|
|
8
|
+
GET /api/v2/proof/requirements list_requirements()
|
|
9
|
+
GET /api/v2/proof/requirements/{req_id} get_requirement()
|
|
10
|
+
POST /api/v2/proof/run run_proof()
|
|
11
|
+
POST /api/v2/proof/requirements/{req_id}/waive waive_requirement()
|
|
12
|
+
GET /api/v2/proof/status aggregated status
|
|
13
|
+
GET /api/v2/proof/requirements/{req_id}/evidence list_evidence()
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
import json
|
|
17
|
+
import logging
|
|
18
|
+
import time
|
|
19
|
+
import uuid
|
|
20
|
+
from datetime import date
|
|
21
|
+
from typing import Any, Literal, Optional
|
|
22
|
+
|
|
23
|
+
from fastapi import APIRouter, Depends, HTTPException, Query, Request
|
|
24
|
+
from pydantic import BaseModel, Field, ValidationError, field_validator
|
|
25
|
+
|
|
26
|
+
from codeframe.core.proof.capture import capture_requirement
|
|
27
|
+
from codeframe.core.proof.ledger import (
|
|
28
|
+
get_requirement,
|
|
29
|
+
get_run,
|
|
30
|
+
get_run_evidence,
|
|
31
|
+
list_evidence,
|
|
32
|
+
list_requirements,
|
|
33
|
+
list_runs,
|
|
34
|
+
waive_requirement,
|
|
35
|
+
)
|
|
36
|
+
from codeframe.core.proof.models import (
|
|
37
|
+
PROOF9_GATE_ORDER,
|
|
38
|
+
PROOF_CONFIG_FILENAME,
|
|
39
|
+
Gate,
|
|
40
|
+
ReqStatus,
|
|
41
|
+
Severity,
|
|
42
|
+
Source,
|
|
43
|
+
Waiver,
|
|
44
|
+
)
|
|
45
|
+
from codeframe.core.proof.runner import run_proof
|
|
46
|
+
from codeframe.core.workspace import Workspace
|
|
47
|
+
from codeframe.lib.rate_limiter import rate_limit_ai, rate_limit_standard
|
|
48
|
+
from codeframe.ui.dependencies import get_v2_workspace
|
|
49
|
+
from codeframe.ui.response_models import ErrorCodes, api_error
|
|
50
|
+
from codeframe.ui.routers._helpers import atomic_write_json
|
|
51
|
+
|
|
52
|
+
logger = logging.getLogger(__name__)
|
|
53
|
+
|
|
54
|
+
router = APIRouter(prefix="/api/v2/proof", tags=["proof-v2"])
|
|
55
|
+
|
|
56
|
+
# Module-level cache: (workspace_path, run_id) → {results, passed, message, _ts}
|
|
57
|
+
# Bounded to _CACHE_MAX_SIZE entries; entries expire after _CACHE_TTL_SECONDS.
|
|
58
|
+
# NOTE: in-memory only — a process running multiple uvicorn workers will have
|
|
59
|
+
# separate caches per worker. Suitable for single-worker dev/demo deployments.
|
|
60
|
+
_run_cache: dict[tuple[str, str], dict] = {}
|
|
61
|
+
_CACHE_MAX_SIZE = 100
|
|
62
|
+
_CACHE_TTL_SECONDS = 300 # 5 minutes
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _evict_run_cache() -> None:
|
|
66
|
+
"""Remove expired entries and trim to max size (oldest first)."""
|
|
67
|
+
now = time.time()
|
|
68
|
+
expired = [k for k, v in _run_cache.items() if now - v["_ts"] > _CACHE_TTL_SECONDS]
|
|
69
|
+
for k in expired:
|
|
70
|
+
del _run_cache[k]
|
|
71
|
+
if len(_run_cache) > _CACHE_MAX_SIZE:
|
|
72
|
+
oldest = sorted(_run_cache, key=lambda k: _run_cache[k]["_ts"])
|
|
73
|
+
for k in oldest[: len(_run_cache) - _CACHE_MAX_SIZE]:
|
|
74
|
+
del _run_cache[k]
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
# ============================================================================
|
|
78
|
+
# Request / Response Models
|
|
79
|
+
# ============================================================================
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
class CaptureRequirementRequest(BaseModel):
|
|
83
|
+
"""Request body for capturing a requirement from a glitch."""
|
|
84
|
+
|
|
85
|
+
title: str = Field(..., min_length=1, description="Short title of the glitch")
|
|
86
|
+
description: str = Field(..., min_length=1, description="Detailed description for glitch classification")
|
|
87
|
+
where: str = Field(..., min_length=1, description="Location (file, route, API, tag) where glitch occurred")
|
|
88
|
+
severity: Severity = Field(..., description="Severity: critical, high, medium, low")
|
|
89
|
+
source: Source = Field(..., description="Source: production, qa, dogfooding, monitoring, user_report")
|
|
90
|
+
created_by: str = Field(default="human", description="Who captured this requirement")
|
|
91
|
+
source_issue: Optional[str] = Field(default=None, description="External issue reference (e.g. GH-123)")
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
class WaiveRequirementRequest(BaseModel):
|
|
95
|
+
"""Request body for waiving a requirement."""
|
|
96
|
+
|
|
97
|
+
reason: str = Field(..., min_length=1, description="Why this requirement is being waived")
|
|
98
|
+
expires: Optional[date] = Field(default=None, description="ISO date when waiver expires (e.g. 2026-06-01)")
|
|
99
|
+
manual_checklist: list[str] = Field(default_factory=list, description="Manual verification steps")
|
|
100
|
+
approved_by: str = Field(default="", description="Who approved this waiver")
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
class RunProofRequest(BaseModel):
|
|
104
|
+
"""Request body for running proof obligations."""
|
|
105
|
+
|
|
106
|
+
full: bool = Field(default=False, description="Run ALL obligations regardless of scope")
|
|
107
|
+
gate: Optional[Gate] = Field(default=None, description="Run only this gate (unit, sec, contract, etc.)")
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
class ScopeOut(BaseModel):
|
|
111
|
+
"""Serialized requirement scope."""
|
|
112
|
+
|
|
113
|
+
routes: list[str] = Field(default_factory=list)
|
|
114
|
+
components: list[str] = Field(default_factory=list)
|
|
115
|
+
apis: list[str] = Field(default_factory=list)
|
|
116
|
+
files: list[str] = Field(default_factory=list)
|
|
117
|
+
tags: list[str] = Field(default_factory=list)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
class ObligationOut(BaseModel):
|
|
121
|
+
"""Serialized proof obligation."""
|
|
122
|
+
|
|
123
|
+
gate: str
|
|
124
|
+
status: str
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
class EvidenceRuleOut(BaseModel):
|
|
128
|
+
"""Serialized evidence rule."""
|
|
129
|
+
|
|
130
|
+
test_id: str
|
|
131
|
+
must_pass: bool
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
class WaiverOut(BaseModel):
|
|
135
|
+
"""Serialized waiver."""
|
|
136
|
+
|
|
137
|
+
reason: str
|
|
138
|
+
expires: Optional[str]
|
|
139
|
+
manual_checklist: list[str]
|
|
140
|
+
approved_by: str
|
|
141
|
+
waived_at: Optional[str] = None
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
class RequirementResponse(BaseModel):
|
|
145
|
+
"""Full requirement response."""
|
|
146
|
+
|
|
147
|
+
id: str
|
|
148
|
+
title: str
|
|
149
|
+
description: str
|
|
150
|
+
severity: str
|
|
151
|
+
source: str
|
|
152
|
+
status: str
|
|
153
|
+
glitch_type: Optional[str]
|
|
154
|
+
obligations: list[ObligationOut]
|
|
155
|
+
evidence_rules: list[EvidenceRuleOut]
|
|
156
|
+
waiver: Optional[WaiverOut]
|
|
157
|
+
created_at: Optional[str]
|
|
158
|
+
satisfied_at: Optional[str]
|
|
159
|
+
created_by: str
|
|
160
|
+
source_issue: Optional[str]
|
|
161
|
+
related_reqs: list[str]
|
|
162
|
+
scope: Optional[ScopeOut] = None
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
class CaptureRequirementResponse(RequirementResponse):
|
|
166
|
+
"""Capture response adds stubs_count."""
|
|
167
|
+
|
|
168
|
+
stubs_count: int = Field(description="Number of test stub files generated")
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
class RequirementListResponse(BaseModel):
|
|
172
|
+
"""Response for list/filter endpoints."""
|
|
173
|
+
|
|
174
|
+
requirements: list[RequirementResponse]
|
|
175
|
+
total: int
|
|
176
|
+
by_status: dict[str, int]
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
class RunProofResponse(BaseModel):
|
|
180
|
+
"""Response for POST /run."""
|
|
181
|
+
|
|
182
|
+
success: bool
|
|
183
|
+
run_id: str
|
|
184
|
+
results: dict[str, list[dict[str, Any]]]
|
|
185
|
+
message: str
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
class RunStatusResponse(BaseModel):
|
|
189
|
+
"""Response for GET /runs/{run_id} — poll a completed run.
|
|
190
|
+
|
|
191
|
+
status is currently always "complete" because POST /run executes
|
|
192
|
+
synchronously before returning run_id. The "running" value is reserved
|
|
193
|
+
for a future async execution model.
|
|
194
|
+
"""
|
|
195
|
+
|
|
196
|
+
run_id: str
|
|
197
|
+
status: str # "complete" (currently); "running" reserved for future async
|
|
198
|
+
results: dict[str, list[dict[str, Any]]]
|
|
199
|
+
passed: bool
|
|
200
|
+
message: str
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
class ProofStatusResponse(BaseModel):
|
|
204
|
+
"""Aggregated proof status response."""
|
|
205
|
+
|
|
206
|
+
total: int
|
|
207
|
+
open: int
|
|
208
|
+
satisfied: int
|
|
209
|
+
waived: int
|
|
210
|
+
requirements: list[RequirementResponse]
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
class EvidenceResponse(BaseModel):
|
|
214
|
+
"""Serialized evidence record."""
|
|
215
|
+
|
|
216
|
+
req_id: str
|
|
217
|
+
gate: str
|
|
218
|
+
satisfied: bool
|
|
219
|
+
artifact_path: str
|
|
220
|
+
artifact_checksum: str
|
|
221
|
+
timestamp: str
|
|
222
|
+
run_id: str
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
class EvidenceWithContentResponse(EvidenceResponse):
|
|
226
|
+
"""Evidence record including artifact file contents."""
|
|
227
|
+
|
|
228
|
+
artifact_text: Optional[str] = None
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
class ProofRunSummaryResponse(BaseModel):
|
|
232
|
+
"""Summary of a single proof gate run."""
|
|
233
|
+
|
|
234
|
+
run_id: str
|
|
235
|
+
started_at: str
|
|
236
|
+
completed_at: Optional[str]
|
|
237
|
+
triggered_by: str
|
|
238
|
+
overall_passed: bool
|
|
239
|
+
duration_ms: Optional[int]
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
class ProofRunDetailResponse(ProofRunSummaryResponse):
|
|
243
|
+
"""Proof run detail including per-gate evidence with artifact content."""
|
|
244
|
+
|
|
245
|
+
evidence: list[EvidenceWithContentResponse]
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
# ============================================================================
|
|
249
|
+
# Helper
|
|
250
|
+
# ============================================================================
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
def _req_to_response(req) -> RequirementResponse:
|
|
254
|
+
"""Convert a core Requirement dataclass to RequirementResponse."""
|
|
255
|
+
return RequirementResponse(
|
|
256
|
+
id=req.id,
|
|
257
|
+
title=req.title,
|
|
258
|
+
description=req.description,
|
|
259
|
+
severity=req.severity.value,
|
|
260
|
+
source=req.source.value,
|
|
261
|
+
status=req.status.value,
|
|
262
|
+
glitch_type=req.glitch_type.value if req.glitch_type else None,
|
|
263
|
+
obligations=[
|
|
264
|
+
ObligationOut(gate=o.gate.value, status=o.status)
|
|
265
|
+
for o in req.obligations
|
|
266
|
+
],
|
|
267
|
+
evidence_rules=[
|
|
268
|
+
EvidenceRuleOut(test_id=r.test_id, must_pass=r.must_pass)
|
|
269
|
+
for r in req.evidence_rules
|
|
270
|
+
],
|
|
271
|
+
waiver=WaiverOut(
|
|
272
|
+
reason=req.waiver.reason,
|
|
273
|
+
expires=req.waiver.expires.isoformat() if req.waiver.expires else None,
|
|
274
|
+
manual_checklist=req.waiver.manual_checklist,
|
|
275
|
+
approved_by=req.waiver.approved_by,
|
|
276
|
+
waived_at=req.waiver.waived_at.isoformat() if req.waiver.waived_at else None,
|
|
277
|
+
) if req.waiver else None,
|
|
278
|
+
created_at=req.created_at.isoformat() if req.created_at else None,
|
|
279
|
+
satisfied_at=req.satisfied_at.isoformat() if req.satisfied_at else None,
|
|
280
|
+
created_by=req.created_by,
|
|
281
|
+
source_issue=req.source_issue,
|
|
282
|
+
related_reqs=req.related_reqs,
|
|
283
|
+
scope=ScopeOut(
|
|
284
|
+
routes=req.scope.routes,
|
|
285
|
+
components=req.scope.components,
|
|
286
|
+
apis=req.scope.apis,
|
|
287
|
+
files=req.scope.files,
|
|
288
|
+
tags=req.scope.tags,
|
|
289
|
+
) if req.scope else None,
|
|
290
|
+
)
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
def _count_by_status(reqs) -> dict[str, int]:
|
|
294
|
+
"""Aggregate requirement counts by status value."""
|
|
295
|
+
counts: dict[str, int] = {s.value: 0 for s in ReqStatus}
|
|
296
|
+
for req in reqs:
|
|
297
|
+
counts[req.status.value] = counts.get(req.status.value, 0) + 1
|
|
298
|
+
return counts
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
# ============================================================================
|
|
302
|
+
# Endpoints
|
|
303
|
+
# ============================================================================
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
@router.post("/requirements", response_model=CaptureRequirementResponse, status_code=201)
|
|
307
|
+
@rate_limit_standard()
|
|
308
|
+
async def capture_requirement_endpoint(
|
|
309
|
+
request: Request,
|
|
310
|
+
body: CaptureRequirementRequest,
|
|
311
|
+
workspace: Workspace = Depends(get_v2_workspace),
|
|
312
|
+
) -> CaptureRequirementResponse:
|
|
313
|
+
"""Capture a requirement from a glitch report.
|
|
314
|
+
|
|
315
|
+
Classifies the glitch, derives proof obligations, generates test stubs,
|
|
316
|
+
and persists the requirement to the ledger.
|
|
317
|
+
"""
|
|
318
|
+
try:
|
|
319
|
+
req, stubs = capture_requirement(
|
|
320
|
+
workspace,
|
|
321
|
+
title=body.title,
|
|
322
|
+
description=body.description,
|
|
323
|
+
where=body.where,
|
|
324
|
+
severity=body.severity,
|
|
325
|
+
source=body.source,
|
|
326
|
+
created_by=body.created_by,
|
|
327
|
+
source_issue=body.source_issue,
|
|
328
|
+
)
|
|
329
|
+
resp = _req_to_response(req)
|
|
330
|
+
return CaptureRequirementResponse(**resp.model_dump(), stubs_count=len(stubs))
|
|
331
|
+
except Exception as e:
|
|
332
|
+
logger.error("Failed to capture requirement: %s", e, exc_info=True)
|
|
333
|
+
raise HTTPException(
|
|
334
|
+
status_code=500,
|
|
335
|
+
detail=api_error("Failed to capture requirement", ErrorCodes.EXECUTION_FAILED, str(e)),
|
|
336
|
+
)
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
@router.get("/requirements", response_model=RequirementListResponse)
|
|
340
|
+
@rate_limit_standard()
|
|
341
|
+
async def list_requirements_endpoint(
|
|
342
|
+
request: Request,
|
|
343
|
+
status: Optional[str] = Query(None, description="Filter by status: open, satisfied, waived"),
|
|
344
|
+
workspace: Workspace = Depends(get_v2_workspace),
|
|
345
|
+
) -> RequirementListResponse:
|
|
346
|
+
"""List all requirements, optionally filtered by status."""
|
|
347
|
+
status_filter = None
|
|
348
|
+
if status:
|
|
349
|
+
try:
|
|
350
|
+
status_filter = ReqStatus(status.lower())
|
|
351
|
+
except ValueError:
|
|
352
|
+
raise HTTPException(
|
|
353
|
+
status_code=400,
|
|
354
|
+
detail=api_error(
|
|
355
|
+
f"Invalid status: {status}",
|
|
356
|
+
ErrorCodes.VALIDATION_ERROR,
|
|
357
|
+
f"Valid values: {[s.value for s in ReqStatus]}",
|
|
358
|
+
),
|
|
359
|
+
)
|
|
360
|
+
|
|
361
|
+
reqs = list_requirements(workspace, status=status_filter)
|
|
362
|
+
all_reqs = list_requirements(workspace) if status_filter else reqs
|
|
363
|
+
|
|
364
|
+
return RequirementListResponse(
|
|
365
|
+
requirements=[_req_to_response(r) for r in reqs],
|
|
366
|
+
total=len(reqs),
|
|
367
|
+
by_status=_count_by_status(all_reqs),
|
|
368
|
+
)
|
|
369
|
+
|
|
370
|
+
|
|
371
|
+
@router.get("/requirements/{req_id}", response_model=RequirementResponse)
|
|
372
|
+
@rate_limit_standard()
|
|
373
|
+
async def get_requirement_endpoint(
|
|
374
|
+
request: Request,
|
|
375
|
+
req_id: str,
|
|
376
|
+
workspace: Workspace = Depends(get_v2_workspace),
|
|
377
|
+
) -> RequirementResponse:
|
|
378
|
+
"""Get a single requirement by ID."""
|
|
379
|
+
req = get_requirement(workspace, req_id)
|
|
380
|
+
if not req:
|
|
381
|
+
raise HTTPException(
|
|
382
|
+
status_code=404,
|
|
383
|
+
detail=api_error(
|
|
384
|
+
f"Requirement not found: {req_id}",
|
|
385
|
+
ErrorCodes.NOT_FOUND,
|
|
386
|
+
f"No requirement with id {req_id}",
|
|
387
|
+
),
|
|
388
|
+
)
|
|
389
|
+
return _req_to_response(req)
|
|
390
|
+
|
|
391
|
+
|
|
392
|
+
@router.post("/run", response_model=RunProofResponse)
|
|
393
|
+
@rate_limit_ai()
|
|
394
|
+
async def run_proof_endpoint(
|
|
395
|
+
request: Request,
|
|
396
|
+
body: RunProofRequest,
|
|
397
|
+
workspace: Workspace = Depends(get_v2_workspace),
|
|
398
|
+
) -> RunProofResponse:
|
|
399
|
+
"""Execute proof obligations and collect evidence.
|
|
400
|
+
|
|
401
|
+
Runs gate checks (pytest, ruff, etc.) for open requirements and records
|
|
402
|
+
evidence artifacts. Use full=True to run all obligations regardless of
|
|
403
|
+
changed scope.
|
|
404
|
+
"""
|
|
405
|
+
try:
|
|
406
|
+
# Generate run_id before calling run_proof so the response ID matches evidence records
|
|
407
|
+
run_id = str(uuid.uuid4())[:8]
|
|
408
|
+
results = run_proof(
|
|
409
|
+
workspace,
|
|
410
|
+
full=body.full,
|
|
411
|
+
gate_filter=body.gate,
|
|
412
|
+
run_id=run_id,
|
|
413
|
+
)
|
|
414
|
+
# Serialize: dict[req_id → list[tuple[Gate, bool]]] → JSON-safe
|
|
415
|
+
serialized = {
|
|
416
|
+
req_id: [{"gate": gate.value, "satisfied": satisfied} for gate, satisfied in gate_results]
|
|
417
|
+
for req_id, gate_results in results.items()
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
# Use the strictness-aware overall_passed that run_proof() persisted,
|
|
421
|
+
# so warn-mode does not surface as failure via the cached run status.
|
|
422
|
+
# Fallback note: if save_run() ever silently failed, the raw `all(...)`
|
|
423
|
+
# below ignores strictness — accepted because that path indicates a
|
|
424
|
+
# deeper persistence bug we'd want to surface as a hard failure anyway.
|
|
425
|
+
persisted_run = get_run(workspace, run_id)
|
|
426
|
+
if persisted_run is not None:
|
|
427
|
+
passed = persisted_run.overall_passed
|
|
428
|
+
else:
|
|
429
|
+
passed = all(
|
|
430
|
+
satisfied
|
|
431
|
+
for gate_results in results.values()
|
|
432
|
+
for _, satisfied in gate_results
|
|
433
|
+
)
|
|
434
|
+
response = RunProofResponse(
|
|
435
|
+
success=True,
|
|
436
|
+
run_id=run_id,
|
|
437
|
+
results=serialized,
|
|
438
|
+
message=f"Proof run complete: {len(results)} requirement(s) evaluated.",
|
|
439
|
+
)
|
|
440
|
+
_evict_run_cache()
|
|
441
|
+
_run_cache[(str(workspace.repo_path), run_id)] = {
|
|
442
|
+
"results": serialized,
|
|
443
|
+
"passed": passed,
|
|
444
|
+
"message": response.message,
|
|
445
|
+
"_ts": time.time(),
|
|
446
|
+
}
|
|
447
|
+
return response
|
|
448
|
+
except Exception as e:
|
|
449
|
+
logger.error("Proof run failed: %s", e, exc_info=True)
|
|
450
|
+
raise HTTPException(
|
|
451
|
+
status_code=500,
|
|
452
|
+
detail=api_error("Proof run failed", ErrorCodes.EXECUTION_FAILED, str(e)),
|
|
453
|
+
)
|
|
454
|
+
|
|
455
|
+
|
|
456
|
+
@router.get("/runs/{run_id}", response_model=RunStatusResponse)
|
|
457
|
+
@rate_limit_standard()
|
|
458
|
+
async def get_run_status_endpoint(
|
|
459
|
+
request: Request,
|
|
460
|
+
run_id: str,
|
|
461
|
+
workspace: Workspace = Depends(get_v2_workspace),
|
|
462
|
+
) -> RunStatusResponse:
|
|
463
|
+
"""Get the status of a completed proof run by run_id.
|
|
464
|
+
|
|
465
|
+
Since POST /run is synchronous, a run is always complete immediately after
|
|
466
|
+
the POST returns. Returns 404 if run_id is unknown.
|
|
467
|
+
"""
|
|
468
|
+
cached = _run_cache.get((str(workspace.repo_path), run_id))
|
|
469
|
+
if cached is None:
|
|
470
|
+
raise HTTPException(
|
|
471
|
+
status_code=404,
|
|
472
|
+
detail=api_error(
|
|
473
|
+
f"Run not found: {run_id}",
|
|
474
|
+
ErrorCodes.NOT_FOUND,
|
|
475
|
+
f"No proof run with id {run_id}",
|
|
476
|
+
),
|
|
477
|
+
)
|
|
478
|
+
return RunStatusResponse(
|
|
479
|
+
run_id=run_id,
|
|
480
|
+
status="complete",
|
|
481
|
+
results=cached["results"],
|
|
482
|
+
passed=cached["passed"],
|
|
483
|
+
message=cached["message"],
|
|
484
|
+
)
|
|
485
|
+
|
|
486
|
+
|
|
487
|
+
@router.post("/requirements/{req_id}/waive", response_model=RequirementResponse)
|
|
488
|
+
@rate_limit_standard()
|
|
489
|
+
async def waive_requirement_endpoint(
|
|
490
|
+
request: Request,
|
|
491
|
+
req_id: str,
|
|
492
|
+
body: WaiveRequirementRequest,
|
|
493
|
+
workspace: Workspace = Depends(get_v2_workspace),
|
|
494
|
+
) -> RequirementResponse:
|
|
495
|
+
"""Waive a requirement with a reason and optional expiry date."""
|
|
496
|
+
existing = get_requirement(workspace, req_id)
|
|
497
|
+
if not existing:
|
|
498
|
+
raise HTTPException(
|
|
499
|
+
status_code=404,
|
|
500
|
+
detail=api_error(
|
|
501
|
+
f"Requirement not found: {req_id}",
|
|
502
|
+
ErrorCodes.NOT_FOUND,
|
|
503
|
+
f"No requirement with id {req_id}",
|
|
504
|
+
),
|
|
505
|
+
)
|
|
506
|
+
|
|
507
|
+
waiver = Waiver(
|
|
508
|
+
reason=body.reason,
|
|
509
|
+
expires=body.expires,
|
|
510
|
+
manual_checklist=body.manual_checklist,
|
|
511
|
+
approved_by=body.approved_by,
|
|
512
|
+
)
|
|
513
|
+
updated = waive_requirement(workspace, req_id, waiver)
|
|
514
|
+
return _req_to_response(updated)
|
|
515
|
+
|
|
516
|
+
|
|
517
|
+
@router.get("/status", response_model=ProofStatusResponse)
|
|
518
|
+
@rate_limit_standard()
|
|
519
|
+
async def proof_status_endpoint(
|
|
520
|
+
request: Request,
|
|
521
|
+
workspace: Workspace = Depends(get_v2_workspace),
|
|
522
|
+
) -> ProofStatusResponse:
|
|
523
|
+
"""Get aggregated proof status: totals by status and full requirement list."""
|
|
524
|
+
reqs = list_requirements(workspace)
|
|
525
|
+
counts = _count_by_status(reqs)
|
|
526
|
+
|
|
527
|
+
return ProofStatusResponse(
|
|
528
|
+
total=len(reqs),
|
|
529
|
+
open=counts.get("open", 0),
|
|
530
|
+
satisfied=counts.get("satisfied", 0),
|
|
531
|
+
waived=counts.get("waived", 0),
|
|
532
|
+
requirements=[_req_to_response(r) for r in reqs],
|
|
533
|
+
)
|
|
534
|
+
|
|
535
|
+
|
|
536
|
+
@router.get("/runs", response_model=list[ProofRunSummaryResponse])
|
|
537
|
+
@rate_limit_standard()
|
|
538
|
+
async def list_runs_endpoint(
|
|
539
|
+
request: Request,
|
|
540
|
+
limit: int = Query(default=5, ge=1, le=50, description="Maximum number of runs to return"),
|
|
541
|
+
workspace: Workspace = Depends(get_v2_workspace),
|
|
542
|
+
) -> list[ProofRunSummaryResponse]:
|
|
543
|
+
"""List the most recent proof gate runs for this workspace."""
|
|
544
|
+
runs = list_runs(workspace, limit=limit)
|
|
545
|
+
return [
|
|
546
|
+
ProofRunSummaryResponse(
|
|
547
|
+
run_id=r.run_id,
|
|
548
|
+
started_at=r.started_at.isoformat(),
|
|
549
|
+
completed_at=r.completed_at.isoformat() if r.completed_at else None,
|
|
550
|
+
triggered_by=r.triggered_by,
|
|
551
|
+
overall_passed=r.overall_passed,
|
|
552
|
+
duration_ms=r.duration_ms,
|
|
553
|
+
)
|
|
554
|
+
for r in runs
|
|
555
|
+
]
|
|
556
|
+
|
|
557
|
+
|
|
558
|
+
_ARTIFACT_LINE_LIMIT = 200
|
|
559
|
+
|
|
560
|
+
|
|
561
|
+
def _read_artifact_text(artifact_path: str, max_lines: int = _ARTIFACT_LINE_LIMIT) -> Optional[str]:
|
|
562
|
+
"""Read artifact file content up to max_lines, returning None if the file is missing."""
|
|
563
|
+
from pathlib import Path
|
|
564
|
+
try:
|
|
565
|
+
p = Path(artifact_path)
|
|
566
|
+
if not p.exists():
|
|
567
|
+
return None
|
|
568
|
+
lines = p.read_text(errors="replace").splitlines(keepends=True)
|
|
569
|
+
return "".join(lines[:max_lines])
|
|
570
|
+
except Exception:
|
|
571
|
+
return None
|
|
572
|
+
|
|
573
|
+
|
|
574
|
+
@router.get("/runs/{run_id}/evidence", response_model=ProofRunDetailResponse)
|
|
575
|
+
@rate_limit_standard()
|
|
576
|
+
async def get_run_evidence_endpoint(
|
|
577
|
+
request: Request,
|
|
578
|
+
run_id: str,
|
|
579
|
+
workspace: Workspace = Depends(get_v2_workspace),
|
|
580
|
+
) -> ProofRunDetailResponse:
|
|
581
|
+
"""Get per-gate evidence with artifact content for a completed proof run."""
|
|
582
|
+
# Try to get run metadata from DB first; fall back to in-memory cache
|
|
583
|
+
run = get_run(workspace, run_id)
|
|
584
|
+
|
|
585
|
+
if run is None:
|
|
586
|
+
# Fall back to cache for very recent runs not yet in DB
|
|
587
|
+
cached = _run_cache.get((str(workspace.repo_path), run_id))
|
|
588
|
+
if cached is None:
|
|
589
|
+
raise HTTPException(
|
|
590
|
+
status_code=404,
|
|
591
|
+
detail=api_error(
|
|
592
|
+
f"Run not found: {run_id}",
|
|
593
|
+
ErrorCodes.NOT_FOUND,
|
|
594
|
+
f"No proof run with id {run_id}",
|
|
595
|
+
),
|
|
596
|
+
)
|
|
597
|
+
# Build a minimal response from cache
|
|
598
|
+
evidence_list: list[EvidenceWithContentResponse] = []
|
|
599
|
+
for req_id, gate_results in cached["results"].items():
|
|
600
|
+
for gate_result in gate_results:
|
|
601
|
+
evidence_list.append(EvidenceWithContentResponse(
|
|
602
|
+
req_id=req_id,
|
|
603
|
+
gate=gate_result["gate"],
|
|
604
|
+
satisfied=gate_result["satisfied"],
|
|
605
|
+
artifact_path="",
|
|
606
|
+
artifact_checksum="",
|
|
607
|
+
timestamp="",
|
|
608
|
+
run_id=run_id,
|
|
609
|
+
artifact_text=None,
|
|
610
|
+
))
|
|
611
|
+
import time as _time
|
|
612
|
+
ts = cached.get("_ts", _time.time())
|
|
613
|
+
from datetime import datetime as _dt, timezone as _tz
|
|
614
|
+
ts_str = _dt.fromtimestamp(ts, tz=_tz.utc).isoformat()
|
|
615
|
+
return ProofRunDetailResponse(
|
|
616
|
+
run_id=run_id,
|
|
617
|
+
started_at=ts_str,
|
|
618
|
+
completed_at=ts_str,
|
|
619
|
+
triggered_by="human",
|
|
620
|
+
overall_passed=cached["passed"],
|
|
621
|
+
duration_ms=None,
|
|
622
|
+
evidence=evidence_list,
|
|
623
|
+
)
|
|
624
|
+
|
|
625
|
+
evidence_records = get_run_evidence(workspace, run_id)
|
|
626
|
+
evidence_out = [
|
|
627
|
+
EvidenceWithContentResponse(
|
|
628
|
+
req_id=e.req_id,
|
|
629
|
+
gate=e.gate.value,
|
|
630
|
+
satisfied=e.satisfied,
|
|
631
|
+
artifact_path=e.artifact_path,
|
|
632
|
+
artifact_checksum=e.artifact_checksum,
|
|
633
|
+
timestamp=e.timestamp.isoformat(),
|
|
634
|
+
run_id=e.run_id,
|
|
635
|
+
artifact_text=_read_artifact_text(e.artifact_path),
|
|
636
|
+
)
|
|
637
|
+
for e in evidence_records
|
|
638
|
+
]
|
|
639
|
+
return ProofRunDetailResponse(
|
|
640
|
+
run_id=run.run_id,
|
|
641
|
+
started_at=run.started_at.isoformat(),
|
|
642
|
+
completed_at=run.completed_at.isoformat() if run.completed_at else None,
|
|
643
|
+
triggered_by=run.triggered_by,
|
|
644
|
+
overall_passed=run.overall_passed,
|
|
645
|
+
duration_ms=run.duration_ms,
|
|
646
|
+
evidence=evidence_out,
|
|
647
|
+
)
|
|
648
|
+
|
|
649
|
+
|
|
650
|
+
# ============================================================================
|
|
651
|
+
# PROOF9 Config (issue #556)
|
|
652
|
+
#
|
|
653
|
+
# Persists which gates are enabled by default and the strictness setting
|
|
654
|
+
# (strict vs warn) to .codeframe/proof_config.json.
|
|
655
|
+
# ============================================================================
|
|
656
|
+
|
|
657
|
+
|
|
658
|
+
_VALID_GATES = {g.value for g in Gate}
|
|
659
|
+
|
|
660
|
+
|
|
661
|
+
def _proof_config_path(workspace: Workspace):
|
|
662
|
+
return workspace.state_dir / PROOF_CONFIG_FILENAME
|
|
663
|
+
|
|
664
|
+
|
|
665
|
+
def _default_proof_config() -> dict:
|
|
666
|
+
return {"enabled_gates": list(PROOF9_GATE_ORDER), "strictness": "strict"}
|
|
667
|
+
|
|
668
|
+
|
|
669
|
+
class ProofConfigResponse(BaseModel):
|
|
670
|
+
enabled_gates: list[str]
|
|
671
|
+
strictness: Literal["strict", "warn"]
|
|
672
|
+
|
|
673
|
+
|
|
674
|
+
class UpdateProofConfigRequest(BaseModel):
|
|
675
|
+
enabled_gates: list[str]
|
|
676
|
+
strictness: Literal["strict", "warn"]
|
|
677
|
+
|
|
678
|
+
@field_validator("enabled_gates")
|
|
679
|
+
@classmethod
|
|
680
|
+
def _validate_gates(cls, v: list[str]) -> list[str]:
|
|
681
|
+
unknown = [g for g in v if g not in _VALID_GATES]
|
|
682
|
+
if unknown:
|
|
683
|
+
raise ValueError(
|
|
684
|
+
f"Unknown gate(s): {unknown}. Valid: {list(PROOF9_GATE_ORDER)}"
|
|
685
|
+
)
|
|
686
|
+
# De-dupe while preserving submission order so the stored file never
|
|
687
|
+
# carries the same gate twice.
|
|
688
|
+
return list(dict.fromkeys(v))
|
|
689
|
+
|
|
690
|
+
|
|
691
|
+
@router.get("/config", response_model=ProofConfigResponse)
|
|
692
|
+
@rate_limit_standard()
|
|
693
|
+
async def get_proof_config(
|
|
694
|
+
request: Request,
|
|
695
|
+
workspace: Workspace = Depends(get_v2_workspace),
|
|
696
|
+
) -> ProofConfigResponse:
|
|
697
|
+
"""Load PROOF9 defaults for this workspace.
|
|
698
|
+
|
|
699
|
+
Returns the all-gates-enabled + strict defaults if no config file exists.
|
|
700
|
+
"""
|
|
701
|
+
path = _proof_config_path(workspace)
|
|
702
|
+
if path.exists():
|
|
703
|
+
try:
|
|
704
|
+
data = json.loads(path.read_text())
|
|
705
|
+
return ProofConfigResponse(**data)
|
|
706
|
+
except (OSError, json.JSONDecodeError, ValueError, ValidationError) as e:
|
|
707
|
+
logger.warning("Invalid proof_config.json — falling back to defaults: %s", e)
|
|
708
|
+
return ProofConfigResponse(**_default_proof_config())
|
|
709
|
+
|
|
710
|
+
|
|
711
|
+
@router.put("/config", response_model=ProofConfigResponse)
|
|
712
|
+
@rate_limit_standard()
|
|
713
|
+
async def update_proof_config(
|
|
714
|
+
request: Request,
|
|
715
|
+
body: UpdateProofConfigRequest,
|
|
716
|
+
workspace: Workspace = Depends(get_v2_workspace),
|
|
717
|
+
) -> ProofConfigResponse:
|
|
718
|
+
"""Persist PROOF9 defaults to .codeframe/proof_config.json."""
|
|
719
|
+
payload = {"enabled_gates": body.enabled_gates, "strictness": body.strictness}
|
|
720
|
+
atomic_write_json(_proof_config_path(workspace), payload)
|
|
721
|
+
return ProofConfigResponse(**payload)
|
|
722
|
+
|
|
723
|
+
|
|
724
|
+
@router.get("/requirements/{req_id}/evidence", response_model=list[EvidenceResponse])
|
|
725
|
+
@rate_limit_standard()
|
|
726
|
+
async def list_evidence_endpoint(
|
|
727
|
+
request: Request,
|
|
728
|
+
req_id: str,
|
|
729
|
+
workspace: Workspace = Depends(get_v2_workspace),
|
|
730
|
+
) -> list[EvidenceResponse]:
|
|
731
|
+
"""List all evidence records for a requirement."""
|
|
732
|
+
req = get_requirement(workspace, req_id)
|
|
733
|
+
if not req:
|
|
734
|
+
raise HTTPException(
|
|
735
|
+
status_code=404,
|
|
736
|
+
detail=api_error(
|
|
737
|
+
f"Requirement not found: {req_id}",
|
|
738
|
+
ErrorCodes.NOT_FOUND,
|
|
739
|
+
f"No requirement with id {req_id}",
|
|
740
|
+
),
|
|
741
|
+
)
|
|
742
|
+
|
|
743
|
+
evidence = list_evidence(workspace, req_id)
|
|
744
|
+
return [
|
|
745
|
+
EvidenceResponse(
|
|
746
|
+
req_id=e.req_id,
|
|
747
|
+
gate=e.gate.value,
|
|
748
|
+
satisfied=e.satisfied,
|
|
749
|
+
artifact_path=e.artifact_path,
|
|
750
|
+
artifact_checksum=e.artifact_checksum,
|
|
751
|
+
timestamp=e.timestamp.isoformat(),
|
|
752
|
+
run_id=e.run_id,
|
|
753
|
+
)
|
|
754
|
+
for e in evidence
|
|
755
|
+
]
|