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.
Files changed (197) hide show
  1. codeframe/__init__.py +11 -0
  2. codeframe/__main__.py +20 -0
  3. codeframe/adapters/__init__.py +5 -0
  4. codeframe/adapters/e2b/__init__.py +13 -0
  5. codeframe/adapters/e2b/adapter.py +342 -0
  6. codeframe/adapters/e2b/budget.py +71 -0
  7. codeframe/adapters/e2b/credential_scanner.py +134 -0
  8. codeframe/adapters/llm/__init__.py +92 -0
  9. codeframe/adapters/llm/anthropic.py +414 -0
  10. codeframe/adapters/llm/base.py +444 -0
  11. codeframe/adapters/llm/mock.py +281 -0
  12. codeframe/adapters/llm/openai.py +483 -0
  13. codeframe/agents/__init__.py +8 -0
  14. codeframe/agents/dependency_resolver.py +714 -0
  15. codeframe/auth/__init__.py +16 -0
  16. codeframe/auth/api_key_router.py +238 -0
  17. codeframe/auth/api_keys.py +156 -0
  18. codeframe/auth/dependencies.py +358 -0
  19. codeframe/auth/manager.py +178 -0
  20. codeframe/auth/models.py +30 -0
  21. codeframe/auth/router.py +93 -0
  22. codeframe/auth/schemas.py +15 -0
  23. codeframe/auth/scopes.py +53 -0
  24. codeframe/cli/__init__.py +12 -0
  25. codeframe/cli/__main__.py +20 -0
  26. codeframe/cli/api_client.py +275 -0
  27. codeframe/cli/app.py +5688 -0
  28. codeframe/cli/auth.py +122 -0
  29. codeframe/cli/auth_commands.py +958 -0
  30. codeframe/cli/commands/__init__.py +5 -0
  31. codeframe/cli/config_commands.py +79 -0
  32. codeframe/cli/dashboard_commands.py +67 -0
  33. codeframe/cli/engines_commands.py +205 -0
  34. codeframe/cli/env_commands.py +409 -0
  35. codeframe/cli/helpers.py +56 -0
  36. codeframe/cli/hooks_commands.py +208 -0
  37. codeframe/cli/import_commands.py +129 -0
  38. codeframe/cli/pr_commands.py +549 -0
  39. codeframe/cli/proof_commands.py +415 -0
  40. codeframe/cli/stats_commands.py +311 -0
  41. codeframe/cli/telemetry_runtime.py +153 -0
  42. codeframe/cli/validators.py +123 -0
  43. codeframe/config/rate_limits.py +165 -0
  44. codeframe/core/__init__.py +15 -0
  45. codeframe/core/adapters/__init__.py +43 -0
  46. codeframe/core/adapters/agent_adapter.py +114 -0
  47. codeframe/core/adapters/builtin.py +326 -0
  48. codeframe/core/adapters/claude_code.py +62 -0
  49. codeframe/core/adapters/codex.py +393 -0
  50. codeframe/core/adapters/git_utils.py +40 -0
  51. codeframe/core/adapters/kilocode.py +126 -0
  52. codeframe/core/adapters/opencode.py +48 -0
  53. codeframe/core/adapters/streaming_chat.py +483 -0
  54. codeframe/core/adapters/subprocess_adapter.py +213 -0
  55. codeframe/core/adapters/verification_wrapper.py +269 -0
  56. codeframe/core/agent.py +2183 -0
  57. codeframe/core/agents_config.py +569 -0
  58. codeframe/core/api_key_service.py +211 -0
  59. codeframe/core/artifacts.py +428 -0
  60. codeframe/core/blocker_detection.py +218 -0
  61. codeframe/core/blockers.py +433 -0
  62. codeframe/core/checkpoints.py +481 -0
  63. codeframe/core/conductor.py +2255 -0
  64. codeframe/core/config.py +827 -0
  65. codeframe/core/config_watcher.py +268 -0
  66. codeframe/core/context.py +542 -0
  67. codeframe/core/context_packager.py +234 -0
  68. codeframe/core/credentials.py +735 -0
  69. codeframe/core/dependency_analyzer.py +229 -0
  70. codeframe/core/dependency_graph.py +290 -0
  71. codeframe/core/diagnostic_agent.py +712 -0
  72. codeframe/core/diagnostics.py +616 -0
  73. codeframe/core/editor.py +556 -0
  74. codeframe/core/engine_registry.py +256 -0
  75. codeframe/core/engine_stats.py +231 -0
  76. codeframe/core/environment.py +697 -0
  77. codeframe/core/events.py +375 -0
  78. codeframe/core/executor.py +1005 -0
  79. codeframe/core/fix_tracker.py +480 -0
  80. codeframe/core/gates.py +1322 -0
  81. codeframe/core/git.py +477 -0
  82. codeframe/core/github_connect_service.py +178 -0
  83. codeframe/core/github_integration_config.py +118 -0
  84. codeframe/core/github_issues_service.py +449 -0
  85. codeframe/core/hooks.py +184 -0
  86. codeframe/core/importers/__init__.py +1 -0
  87. codeframe/core/importers/ralph.py +540 -0
  88. codeframe/core/installer.py +650 -0
  89. codeframe/core/models.py +1026 -0
  90. codeframe/core/notifications_config.py +183 -0
  91. codeframe/core/planner.py +437 -0
  92. codeframe/core/prd.py +670 -0
  93. codeframe/core/prd_discovery.py +1118 -0
  94. codeframe/core/prd_stress_test.py +499 -0
  95. codeframe/core/progress.py +126 -0
  96. codeframe/core/proof/__init__.py +34 -0
  97. codeframe/core/proof/capture.py +79 -0
  98. codeframe/core/proof/evidence.py +56 -0
  99. codeframe/core/proof/ledger.py +574 -0
  100. codeframe/core/proof/models.py +162 -0
  101. codeframe/core/proof/obligations.py +103 -0
  102. codeframe/core/proof/runner.py +233 -0
  103. codeframe/core/proof/scope.py +81 -0
  104. codeframe/core/proof/stubs.py +156 -0
  105. codeframe/core/quick_fixes.py +558 -0
  106. codeframe/core/react_agent.py +1650 -0
  107. codeframe/core/reconciliation.py +183 -0
  108. codeframe/core/replay.py +788 -0
  109. codeframe/core/review.py +285 -0
  110. codeframe/core/runtime.py +1134 -0
  111. codeframe/core/sandbox/__init__.py +27 -0
  112. codeframe/core/sandbox/context.py +98 -0
  113. codeframe/core/sandbox/worktree.py +20 -0
  114. codeframe/core/schedule.py +396 -0
  115. codeframe/core/stall_detector.py +71 -0
  116. codeframe/core/stall_monitor.py +134 -0
  117. codeframe/core/state_machine.py +121 -0
  118. codeframe/core/streaming.py +502 -0
  119. codeframe/core/task_tree.py +400 -0
  120. codeframe/core/tasks.py +1022 -0
  121. codeframe/core/telemetry.py +232 -0
  122. codeframe/core/templates.py +221 -0
  123. codeframe/core/tools.py +942 -0
  124. codeframe/core/workspace.py +887 -0
  125. codeframe/core/worktrees.py +276 -0
  126. codeframe/git/__init__.py +5 -0
  127. codeframe/git/github_integration.py +505 -0
  128. codeframe/lib/__init__.py +0 -0
  129. codeframe/lib/audit_logger.py +248 -0
  130. codeframe/lib/metrics_tracker.py +800 -0
  131. codeframe/lib/quality/__init__.py +7 -0
  132. codeframe/lib/quality/complexity_analyzer.py +316 -0
  133. codeframe/lib/quality/owasp_patterns.py +284 -0
  134. codeframe/lib/quality/security_scanner.py +250 -0
  135. codeframe/lib/rate_limiter.py +312 -0
  136. codeframe/notifications/__init__.py +0 -0
  137. codeframe/notifications/webhook.py +380 -0
  138. codeframe/planning/__init__.py +30 -0
  139. codeframe/planning/issue_generator.py +219 -0
  140. codeframe/planning/prd_template_functions.py +137 -0
  141. codeframe/planning/prd_templates.py +975 -0
  142. codeframe/planning/task_scheduler.py +511 -0
  143. codeframe/planning/task_templates.py +533 -0
  144. codeframe/platform_store/__init__.py +5 -0
  145. codeframe/platform_store/database.py +277 -0
  146. codeframe/platform_store/repositories/__init__.py +24 -0
  147. codeframe/platform_store/repositories/api_key_repository.py +245 -0
  148. codeframe/platform_store/repositories/audit_repository.py +67 -0
  149. codeframe/platform_store/repositories/base.py +295 -0
  150. codeframe/platform_store/repositories/interactive_sessions.py +165 -0
  151. codeframe/platform_store/repositories/token_repository.py +598 -0
  152. codeframe/platform_store/repositories/workspace_registry_repository.py +175 -0
  153. codeframe/platform_store/schema_manager.py +321 -0
  154. codeframe/templates/AGENTS.md.default +94 -0
  155. codeframe/tui/__init__.py +5 -0
  156. codeframe/tui/app.py +256 -0
  157. codeframe/tui/data_service.py +103 -0
  158. codeframe/ui/__init__.py +0 -0
  159. codeframe/ui/dependencies.py +103 -0
  160. codeframe/ui/models.py +999 -0
  161. codeframe/ui/response_models.py +201 -0
  162. codeframe/ui/routers/__init__.py +5 -0
  163. codeframe/ui/routers/_helpers.py +29 -0
  164. codeframe/ui/routers/batches_v2.py +315 -0
  165. codeframe/ui/routers/blockers_v2.py +320 -0
  166. codeframe/ui/routers/checkpoints_v2.py +310 -0
  167. codeframe/ui/routers/costs_v2.py +322 -0
  168. codeframe/ui/routers/diagnose_v2.py +225 -0
  169. codeframe/ui/routers/discovery_v2.py +417 -0
  170. codeframe/ui/routers/environment_v2.py +284 -0
  171. codeframe/ui/routers/events_v2.py +75 -0
  172. codeframe/ui/routers/gates_v2.py +166 -0
  173. codeframe/ui/routers/git_v2.py +284 -0
  174. codeframe/ui/routers/github_integrations_v2.py +532 -0
  175. codeframe/ui/routers/interactive_sessions_v2.py +238 -0
  176. codeframe/ui/routers/pr_v2.py +709 -0
  177. codeframe/ui/routers/prd_v2.py +695 -0
  178. codeframe/ui/routers/proof_v2.py +755 -0
  179. codeframe/ui/routers/review_v2.py +360 -0
  180. codeframe/ui/routers/schedule_v2.py +214 -0
  181. codeframe/ui/routers/session_chat_ws.py +354 -0
  182. codeframe/ui/routers/settings_v2.py +562 -0
  183. codeframe/ui/routers/streaming_v2.py +155 -0
  184. codeframe/ui/routers/tasks_v2.py +1098 -0
  185. codeframe/ui/routers/templates_v2.py +232 -0
  186. codeframe/ui/routers/terminal_ws.py +267 -0
  187. codeframe/ui/routers/workspace_v2.py +527 -0
  188. codeframe/ui/server.py +568 -0
  189. codeframe/ui/shared.py +241 -0
  190. codeframe/workspace/__init__.py +5 -0
  191. codeframe/workspace/manager.py +249 -0
  192. codeframe_ai-0.9.0.dist-info/METADATA +517 -0
  193. codeframe_ai-0.9.0.dist-info/RECORD +197 -0
  194. codeframe_ai-0.9.0.dist-info/WHEEL +5 -0
  195. codeframe_ai-0.9.0.dist-info/entry_points.txt +3 -0
  196. codeframe_ai-0.9.0.dist-info/licenses/LICENSE +661 -0
  197. codeframe_ai-0.9.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,79 @@
1
+ """PROOF9 capture logic.
2
+
3
+ Orchestrates the creation of a new requirement from a glitch report:
4
+ classify → obligations → scope → save → generate stubs.
5
+ """
6
+
7
+ from datetime import datetime, timezone
8
+ from typing import Optional
9
+
10
+ from codeframe.core.proof import ledger
11
+ from codeframe.core.proof.models import (
12
+ Gate,
13
+ Requirement,
14
+ Severity,
15
+ Source,
16
+ )
17
+ from codeframe.core.proof.obligations import (
18
+ classify_glitch,
19
+ get_obligations,
20
+ suggest_evidence_rules,
21
+ )
22
+ from codeframe.core.proof.scope import build_scope_from_capture
23
+ from codeframe.core.proof.stubs import generate_stubs
24
+ from codeframe.core.workspace import Workspace
25
+
26
+
27
+ def capture_requirement(
28
+ workspace: Workspace,
29
+ *,
30
+ title: str,
31
+ description: str,
32
+ where: str,
33
+ severity: Severity,
34
+ source: Source,
35
+ created_by: str = "human",
36
+ source_issue: Optional[str] = None,
37
+ ) -> tuple[Requirement, dict[Gate, str]]:
38
+ """Create a new requirement from a glitch report.
39
+
40
+ Returns the saved Requirement and a dict of Gate → stub content.
41
+ """
42
+ # 1. Classify the glitch
43
+ glitch_type = classify_glitch(description)
44
+
45
+ # 2. Get obligation set
46
+ obligations = get_obligations(glitch_type)
47
+
48
+ # 3. Build scope from user-provided location
49
+ scope = build_scope_from_capture(where)
50
+
51
+ # 4. Generate evidence rules for each obligation
52
+ evidence_rules = []
53
+ for obl in obligations:
54
+ evidence_rules.extend(suggest_evidence_rules(obl.gate, title))
55
+
56
+ # 5. Create the requirement
57
+ req_id = ledger.next_req_id(workspace)
58
+ req = Requirement(
59
+ id=req_id,
60
+ title=title,
61
+ description=description,
62
+ severity=severity,
63
+ source=source,
64
+ scope=scope,
65
+ obligations=obligations,
66
+ evidence_rules=evidence_rules,
67
+ created_at=datetime.now(timezone.utc),
68
+ created_by=created_by,
69
+ source_issue=source_issue,
70
+ glitch_type=glitch_type,
71
+ )
72
+
73
+ # 6. Persist
74
+ ledger.save_requirement(workspace, req)
75
+
76
+ # 7. Generate test stubs
77
+ stubs = generate_stubs(req)
78
+
79
+ return req, stubs
@@ -0,0 +1,56 @@
1
+ """PROOF9 evidence attachment and verification.
2
+
3
+ Attaches evidence artifacts (test results, screenshots, reports)
4
+ to requirements with SHA-256 checksums for integrity.
5
+ """
6
+
7
+ import hashlib
8
+ from datetime import datetime, timezone
9
+ from pathlib import Path
10
+
11
+ from codeframe.core.proof import ledger
12
+ from codeframe.core.proof.models import Evidence, Gate, Requirement
13
+ from codeframe.core.workspace import Workspace
14
+
15
+
16
+ def _sha256(file_path: str) -> str:
17
+ """Compute SHA-256 checksum of a file. Raises if file missing."""
18
+ path = Path(file_path)
19
+ if not path.exists():
20
+ raise FileNotFoundError(f"Artifact not found: {file_path}")
21
+ h = hashlib.sha256()
22
+ h.update(path.read_bytes())
23
+ return h.hexdigest()
24
+
25
+
26
+ def attach_evidence(
27
+ workspace: Workspace,
28
+ req_id: str,
29
+ gate: Gate,
30
+ artifact_path: str,
31
+ satisfied: bool,
32
+ run_id: str,
33
+ ) -> Evidence:
34
+ """Create and persist an evidence record with artifact checksum."""
35
+ evidence = Evidence(
36
+ req_id=req_id,
37
+ gate=gate,
38
+ satisfied=satisfied,
39
+ artifact_path=artifact_path,
40
+ artifact_checksum=_sha256(artifact_path),
41
+ timestamp=datetime.now(timezone.utc),
42
+ run_id=run_id,
43
+ )
44
+ ledger.save_evidence(workspace, evidence)
45
+ return evidence
46
+
47
+
48
+ def check_obligation_satisfied(
49
+ workspace: Workspace, req: Requirement, gate: Gate
50
+ ) -> bool:
51
+ """Check if a gate obligation has passing evidence."""
52
+ evidence_list = ledger.list_evidence(workspace, req.id)
53
+ for ev in evidence_list:
54
+ if ev.gate == gate and ev.satisfied:
55
+ return True
56
+ return False
@@ -0,0 +1,574 @@
1
+ """PROOF9 ledger — SQLite storage for requirements and evidence.
2
+
3
+ Uses raw sqlite3 following the workspace pattern in core/prd.py.
4
+ Tables live in the workspace's state.db alongside PRDs and tasks.
5
+ """
6
+
7
+ import json
8
+ from datetime import date, datetime, timezone
9
+ from typing import Optional
10
+
11
+ from codeframe.core.proof.models import (
12
+ Evidence,
13
+ EvidenceRule,
14
+ Gate,
15
+ GlitchType,
16
+ Obligation,
17
+ ProofRun,
18
+ ReqStatus,
19
+ Requirement,
20
+ RequirementScope,
21
+ Severity,
22
+ Source,
23
+ Waiver,
24
+ )
25
+ from codeframe.core.workspace import Workspace, get_db_connection
26
+
27
+
28
+ def _utc_now() -> datetime:
29
+ return datetime.now(timezone.utc)
30
+
31
+
32
+ def init_proof_tables(workspace: Workspace) -> None:
33
+ """Create proof tables if they don't exist."""
34
+ conn = get_db_connection(workspace)
35
+ cursor = conn.cursor()
36
+
37
+ cursor.execute("""
38
+ CREATE TABLE IF NOT EXISTS proof_requirements (
39
+ id TEXT PRIMARY KEY,
40
+ title TEXT NOT NULL,
41
+ description TEXT NOT NULL,
42
+ severity TEXT NOT NULL,
43
+ source TEXT NOT NULL,
44
+ scope TEXT NOT NULL,
45
+ obligations TEXT NOT NULL,
46
+ evidence_rules TEXT NOT NULL,
47
+ status TEXT NOT NULL DEFAULT 'open',
48
+ waiver TEXT,
49
+ created_at TEXT NOT NULL,
50
+ satisfied_at TEXT,
51
+ created_by TEXT NOT NULL DEFAULT '',
52
+ source_issue TEXT,
53
+ related_reqs TEXT NOT NULL DEFAULT '[]',
54
+ glitch_type TEXT,
55
+ workspace_id TEXT NOT NULL
56
+ )
57
+ """)
58
+
59
+ cursor.execute("""
60
+ CREATE TABLE IF NOT EXISTS proof_evidence (
61
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
62
+ req_id TEXT NOT NULL,
63
+ gate TEXT NOT NULL,
64
+ satisfied INTEGER NOT NULL,
65
+ artifact_path TEXT NOT NULL,
66
+ artifact_checksum TEXT NOT NULL,
67
+ timestamp TEXT NOT NULL,
68
+ run_id TEXT NOT NULL,
69
+ workspace_id TEXT NOT NULL,
70
+ FOREIGN KEY (req_id) REFERENCES proof_requirements(id)
71
+ )
72
+ """)
73
+
74
+ cursor.execute("""
75
+ CREATE TABLE IF NOT EXISTS proof_runs (
76
+ run_id TEXT NOT NULL,
77
+ workspace_id TEXT NOT NULL,
78
+ started_at TEXT NOT NULL,
79
+ completed_at TEXT,
80
+ triggered_by TEXT NOT NULL DEFAULT 'human',
81
+ overall_passed INTEGER NOT NULL DEFAULT 0,
82
+ duration_ms INTEGER,
83
+ PRIMARY KEY (run_id, workspace_id)
84
+ )
85
+ """)
86
+
87
+ cursor.execute("""
88
+ CREATE TABLE IF NOT EXISTS pr_proof_snapshots (
89
+ pr_number INTEGER NOT NULL,
90
+ workspace_id TEXT NOT NULL,
91
+ gates_passed INTEGER NOT NULL,
92
+ gates_total INTEGER NOT NULL,
93
+ gate_breakdown TEXT NOT NULL,
94
+ snapshotted_at TEXT NOT NULL,
95
+ PRIMARY KEY (pr_number, workspace_id)
96
+ )
97
+ """)
98
+
99
+ conn.commit()
100
+ conn.close()
101
+
102
+
103
+ def _ensure_tables(workspace: Workspace) -> None:
104
+ """Lazily init tables on first access."""
105
+ conn = get_db_connection(workspace)
106
+ cursor = conn.cursor()
107
+ cursor.execute(
108
+ "SELECT name FROM sqlite_master WHERE type='table' AND name='proof_requirements'"
109
+ )
110
+ missing = not cursor.fetchone()
111
+ if not missing:
112
+ cursor.execute(
113
+ "SELECT name FROM sqlite_master WHERE type='table' AND name='proof_runs'"
114
+ )
115
+ missing = not cursor.fetchone()
116
+ if not missing:
117
+ cursor.execute(
118
+ "SELECT name FROM sqlite_master WHERE type='table' AND name='pr_proof_snapshots'"
119
+ )
120
+ missing = not cursor.fetchone()
121
+ conn.close()
122
+ if missing:
123
+ init_proof_tables(workspace)
124
+
125
+
126
+ # --- Serialization helpers ---
127
+
128
+ def _scope_to_json(scope: RequirementScope) -> str:
129
+ return json.dumps({
130
+ "routes": scope.routes,
131
+ "components": scope.components,
132
+ "apis": scope.apis,
133
+ "files": scope.files,
134
+ "tags": scope.tags,
135
+ })
136
+
137
+
138
+ def _scope_from_json(raw: str) -> RequirementScope:
139
+ data = json.loads(raw)
140
+ return RequirementScope(
141
+ routes=data.get("routes", []),
142
+ components=data.get("components", []),
143
+ apis=data.get("apis", []),
144
+ files=data.get("files", []),
145
+ tags=data.get("tags", []),
146
+ )
147
+
148
+
149
+ def _obligations_to_json(obligations: list[Obligation]) -> str:
150
+ return json.dumps([{"gate": o.gate.value, "status": o.status} for o in obligations])
151
+
152
+
153
+ def _obligations_from_json(raw: str) -> list[Obligation]:
154
+ data = json.loads(raw)
155
+ return [Obligation(gate=Gate(d["gate"]), status=d.get("status", "pending")) for d in data]
156
+
157
+
158
+ def _evidence_rules_to_json(rules: list[EvidenceRule]) -> str:
159
+ return json.dumps([{"test_id": r.test_id, "must_pass": r.must_pass} for r in rules])
160
+
161
+
162
+ def _evidence_rules_from_json(raw: str) -> list[EvidenceRule]:
163
+ data = json.loads(raw)
164
+ return [EvidenceRule(test_id=d["test_id"], must_pass=d.get("must_pass", True)) for d in data]
165
+
166
+
167
+ def _waiver_to_json(waiver: Optional[Waiver]) -> Optional[str]:
168
+ if waiver is None:
169
+ return None
170
+ return json.dumps({
171
+ "reason": waiver.reason,
172
+ "expires": waiver.expires.isoformat() if waiver.expires else None,
173
+ "manual_checklist": waiver.manual_checklist,
174
+ "approved_by": waiver.approved_by,
175
+ "waived_at": waiver.waived_at.isoformat() if waiver.waived_at else None,
176
+ })
177
+
178
+
179
+ def _waiver_from_json(raw: Optional[str]) -> Optional[Waiver]:
180
+ if not raw:
181
+ return None
182
+ data = json.loads(raw)
183
+ waived_at_raw = data.get("waived_at")
184
+ return Waiver(
185
+ reason=data["reason"],
186
+ expires=date.fromisoformat(data["expires"]) if data.get("expires") else None,
187
+ manual_checklist=data.get("manual_checklist", []),
188
+ approved_by=data.get("approved_by", ""),
189
+ waived_at=datetime.fromisoformat(waived_at_raw) if waived_at_raw else None,
190
+ )
191
+
192
+
193
+ def _row_to_requirement(row: tuple) -> Requirement:
194
+ return Requirement(
195
+ id=row[0],
196
+ title=row[1],
197
+ description=row[2],
198
+ severity=Severity(row[3]),
199
+ source=Source(row[4]),
200
+ scope=_scope_from_json(row[5]),
201
+ obligations=_obligations_from_json(row[6]),
202
+ evidence_rules=_evidence_rules_from_json(row[7]),
203
+ status=ReqStatus(row[8]),
204
+ waiver=_waiver_from_json(row[9]),
205
+ created_at=datetime.fromisoformat(row[10]),
206
+ satisfied_at=datetime.fromisoformat(row[11]) if row[11] else None,
207
+ created_by=row[12],
208
+ source_issue=row[13],
209
+ related_reqs=json.loads(row[14]) if row[14] else [],
210
+ glitch_type=GlitchType(row[15]) if row[15] else None,
211
+ )
212
+
213
+
214
+ # --- CRUD ---
215
+
216
+ def save_requirement(workspace: Workspace, req: Requirement) -> None:
217
+ """Insert or replace a requirement in the ledger."""
218
+ _ensure_tables(workspace)
219
+ conn = get_db_connection(workspace)
220
+ cursor = conn.cursor()
221
+ cursor.execute(
222
+ """INSERT OR REPLACE INTO proof_requirements
223
+ (id, title, description, severity, source, scope, obligations,
224
+ evidence_rules, status, waiver, created_at, satisfied_at,
225
+ created_by, source_issue, related_reqs, glitch_type, workspace_id)
226
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)""",
227
+ (
228
+ req.id, req.title, req.description, req.severity.value,
229
+ req.source.value, _scope_to_json(req.scope),
230
+ _obligations_to_json(req.obligations),
231
+ _evidence_rules_to_json(req.evidence_rules),
232
+ req.status.value, _waiver_to_json(req.waiver),
233
+ (req.created_at or _utc_now()).isoformat(),
234
+ req.satisfied_at.isoformat() if req.satisfied_at else None,
235
+ req.created_by, req.source_issue,
236
+ json.dumps(req.related_reqs),
237
+ req.glitch_type.value if req.glitch_type else None,
238
+ workspace.id,
239
+ ),
240
+ )
241
+ conn.commit()
242
+ conn.close()
243
+
244
+
245
+ def get_requirement(workspace: Workspace, req_id: str) -> Optional[Requirement]:
246
+ """Fetch a single requirement by ID."""
247
+ _ensure_tables(workspace)
248
+ conn = get_db_connection(workspace)
249
+ cursor = conn.cursor()
250
+ cursor.execute(
251
+ """SELECT id, title, description, severity, source, scope, obligations,
252
+ evidence_rules, status, waiver, created_at, satisfied_at,
253
+ created_by, source_issue, related_reqs, glitch_type
254
+ FROM proof_requirements WHERE id = ? AND workspace_id = ?""",
255
+ (req_id, workspace.id),
256
+ )
257
+ row = cursor.fetchone()
258
+ conn.close()
259
+ return _row_to_requirement(row) if row else None
260
+
261
+
262
+ def list_requirements(
263
+ workspace: Workspace, status: Optional[ReqStatus] = None
264
+ ) -> list[Requirement]:
265
+ """List all requirements, optionally filtered by status."""
266
+ _ensure_tables(workspace)
267
+ conn = get_db_connection(workspace)
268
+ cursor = conn.cursor()
269
+ if status:
270
+ cursor.execute(
271
+ """SELECT id, title, description, severity, source, scope, obligations,
272
+ evidence_rules, status, waiver, created_at, satisfied_at,
273
+ created_by, source_issue, related_reqs, glitch_type
274
+ FROM proof_requirements WHERE workspace_id = ? AND status = ?
275
+ ORDER BY created_at DESC""",
276
+ (workspace.id, status.value),
277
+ )
278
+ else:
279
+ cursor.execute(
280
+ """SELECT id, title, description, severity, source, scope, obligations,
281
+ evidence_rules, status, waiver, created_at, satisfied_at,
282
+ created_by, source_issue, related_reqs, glitch_type
283
+ FROM proof_requirements WHERE workspace_id = ?
284
+ ORDER BY created_at DESC""",
285
+ (workspace.id,),
286
+ )
287
+ rows = cursor.fetchall()
288
+ conn.close()
289
+ return [_row_to_requirement(r) for r in rows]
290
+
291
+
292
+ def next_req_id(workspace: Workspace) -> str:
293
+ """Generate the next sequential REQ-#### ID.
294
+
295
+ Uses MAX(id) to avoid collisions from deleted requirements.
296
+ """
297
+ _ensure_tables(workspace)
298
+ conn = get_db_connection(workspace)
299
+ cursor = conn.cursor()
300
+ cursor.execute(
301
+ "SELECT MAX(CAST(SUBSTR(id, 5) AS INTEGER)) FROM proof_requirements WHERE workspace_id = ?",
302
+ (workspace.id,),
303
+ )
304
+ row = cursor.fetchone()
305
+ max_num = row[0] if row and row[0] is not None else 0
306
+ conn.close()
307
+ return f"REQ-{max_num + 1:04d}"
308
+
309
+
310
+ def save_evidence(workspace: Workspace, evidence: Evidence) -> None:
311
+ """Store an evidence record."""
312
+ _ensure_tables(workspace)
313
+ conn = get_db_connection(workspace)
314
+ cursor = conn.cursor()
315
+ cursor.execute(
316
+ """INSERT INTO proof_evidence
317
+ (req_id, gate, satisfied, artifact_path, artifact_checksum,
318
+ timestamp, run_id, workspace_id)
319
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)""",
320
+ (
321
+ evidence.req_id, evidence.gate.value, int(evidence.satisfied),
322
+ evidence.artifact_path, evidence.artifact_checksum,
323
+ evidence.timestamp.isoformat(), evidence.run_id, workspace.id,
324
+ ),
325
+ )
326
+ conn.commit()
327
+ conn.close()
328
+
329
+
330
+ def list_evidence(workspace: Workspace, req_id: str) -> list[Evidence]:
331
+ """List all evidence for a requirement."""
332
+ _ensure_tables(workspace)
333
+ conn = get_db_connection(workspace)
334
+ cursor = conn.cursor()
335
+ cursor.execute(
336
+ """SELECT req_id, gate, satisfied, artifact_path, artifact_checksum,
337
+ timestamp, run_id
338
+ FROM proof_evidence WHERE req_id = ? AND workspace_id = ?
339
+ ORDER BY timestamp DESC""",
340
+ (req_id, workspace.id),
341
+ )
342
+ rows = cursor.fetchall()
343
+ conn.close()
344
+ return [
345
+ Evidence(
346
+ req_id=r[0], gate=Gate(r[1]), satisfied=bool(r[2]),
347
+ artifact_path=r[3], artifact_checksum=r[4],
348
+ timestamp=datetime.fromisoformat(r[5]), run_id=r[6],
349
+ )
350
+ for r in rows
351
+ ]
352
+
353
+
354
+ def waive_requirement(
355
+ workspace: Workspace, req_id: str, waiver: Waiver
356
+ ) -> Optional[Requirement]:
357
+ """Waive a requirement with reason and optional expiry."""
358
+ if waiver.waived_at is None:
359
+ waiver = Waiver(
360
+ reason=waiver.reason,
361
+ expires=waiver.expires,
362
+ manual_checklist=waiver.manual_checklist,
363
+ approved_by=waiver.approved_by,
364
+ waived_at=datetime.now(timezone.utc),
365
+ )
366
+ _ensure_tables(workspace)
367
+ conn = get_db_connection(workspace)
368
+ cursor = conn.cursor()
369
+ cursor.execute(
370
+ """UPDATE proof_requirements SET status = ?, waiver = ?
371
+ WHERE id = ? AND workspace_id = ?""",
372
+ (ReqStatus.WAIVED.value, _waiver_to_json(waiver), req_id, workspace.id),
373
+ )
374
+ conn.commit()
375
+ conn.close()
376
+ return get_requirement(workspace, req_id)
377
+
378
+
379
+ def save_run(workspace: Workspace, run: ProofRun) -> None:
380
+ """Insert or replace a proof run record."""
381
+ _ensure_tables(workspace)
382
+ conn = get_db_connection(workspace)
383
+ cursor = conn.cursor()
384
+ cursor.execute(
385
+ """INSERT OR REPLACE INTO proof_runs
386
+ (run_id, workspace_id, started_at, completed_at, triggered_by,
387
+ overall_passed, duration_ms)
388
+ VALUES (?, ?, ?, ?, ?, ?, ?)""",
389
+ (
390
+ run.run_id, workspace.id,
391
+ run.started_at.isoformat(),
392
+ run.completed_at.isoformat() if run.completed_at else None,
393
+ run.triggered_by,
394
+ int(run.overall_passed),
395
+ run.duration_ms,
396
+ ),
397
+ )
398
+ conn.commit()
399
+ conn.close()
400
+
401
+
402
+ def get_run(workspace: Workspace, run_id: str) -> Optional[ProofRun]:
403
+ """Fetch a single proof run by run_id."""
404
+ _ensure_tables(workspace)
405
+ conn = get_db_connection(workspace)
406
+ cursor = conn.cursor()
407
+ cursor.execute(
408
+ """SELECT run_id, workspace_id, started_at, completed_at, triggered_by,
409
+ overall_passed, duration_ms
410
+ FROM proof_runs WHERE run_id = ? AND workspace_id = ?""",
411
+ (run_id, workspace.id),
412
+ )
413
+ row = cursor.fetchone()
414
+ conn.close()
415
+ if not row:
416
+ return None
417
+ return ProofRun(
418
+ run_id=row[0],
419
+ workspace_id=row[1],
420
+ started_at=datetime.fromisoformat(row[2]),
421
+ completed_at=datetime.fromisoformat(row[3]) if row[3] else None,
422
+ triggered_by=row[4],
423
+ overall_passed=bool(row[5]),
424
+ duration_ms=row[6],
425
+ )
426
+
427
+
428
+ def list_runs(workspace: Workspace, limit: int = 5) -> list[ProofRun]:
429
+ """List the most recent proof runs for this workspace."""
430
+ _ensure_tables(workspace)
431
+ conn = get_db_connection(workspace)
432
+ cursor = conn.cursor()
433
+ cursor.execute(
434
+ """SELECT run_id, workspace_id, started_at, completed_at, triggered_by,
435
+ overall_passed, duration_ms
436
+ FROM proof_runs WHERE workspace_id = ?
437
+ ORDER BY started_at DESC LIMIT ?""",
438
+ (workspace.id, limit),
439
+ )
440
+ rows = cursor.fetchall()
441
+ conn.close()
442
+ return [
443
+ ProofRun(
444
+ run_id=r[0],
445
+ workspace_id=r[1],
446
+ started_at=datetime.fromisoformat(r[2]),
447
+ completed_at=datetime.fromisoformat(r[3]) if r[3] else None,
448
+ triggered_by=r[4],
449
+ overall_passed=bool(r[5]),
450
+ duration_ms=r[6],
451
+ )
452
+ for r in rows
453
+ ]
454
+
455
+
456
+ def get_run_evidence(workspace: Workspace, run_id: str) -> list[Evidence]:
457
+ """List all evidence records for a specific run_id."""
458
+ _ensure_tables(workspace)
459
+ conn = get_db_connection(workspace)
460
+ cursor = conn.cursor()
461
+ cursor.execute(
462
+ """SELECT req_id, gate, satisfied, artifact_path, artifact_checksum,
463
+ timestamp, run_id
464
+ FROM proof_evidence WHERE run_id = ? AND workspace_id = ?
465
+ ORDER BY timestamp ASC""",
466
+ (run_id, workspace.id),
467
+ )
468
+ rows = cursor.fetchall()
469
+ conn.close()
470
+ return [
471
+ Evidence(
472
+ req_id=r[0], gate=Gate(r[1]), satisfied=bool(r[2]),
473
+ artifact_path=r[3], artifact_checksum=r[4],
474
+ timestamp=datetime.fromisoformat(r[5]), run_id=r[6],
475
+ )
476
+ for r in rows
477
+ ]
478
+
479
+
480
+ def check_expired_waivers(workspace: Workspace) -> list[Requirement]:
481
+ """Find and revert expired waivers to open status."""
482
+ _ensure_tables(workspace)
483
+ today = date.today().isoformat()
484
+ conn = get_db_connection(workspace)
485
+ cursor = conn.cursor()
486
+
487
+ # Find waived reqs
488
+ cursor.execute(
489
+ """SELECT id, title, description, severity, source, scope, obligations,
490
+ evidence_rules, status, waiver, created_at, satisfied_at,
491
+ created_by, source_issue, related_reqs, glitch_type
492
+ FROM proof_requirements WHERE workspace_id = ? AND status = 'waived'""",
493
+ (workspace.id,),
494
+ )
495
+ rows = cursor.fetchall()
496
+ expired = []
497
+
498
+ for row in rows:
499
+ req = _row_to_requirement(row)
500
+ if req.waiver and req.waiver.expires and req.waiver.expires.isoformat() <= today:
501
+ cursor.execute(
502
+ "UPDATE proof_requirements SET status = 'open', waiver = NULL WHERE id = ?",
503
+ (req.id,),
504
+ )
505
+ req.status = ReqStatus.OPEN
506
+ req.waiver = None
507
+ expired.append(req)
508
+
509
+ if expired:
510
+ conn.commit()
511
+ conn.close()
512
+ return expired
513
+
514
+
515
+ # --- PR Proof Snapshots ---
516
+
517
+
518
+ def save_pr_proof_snapshot(
519
+ workspace: Workspace,
520
+ pr_number: int,
521
+ gates_passed: int,
522
+ gates_total: int,
523
+ gate_breakdown: list[dict],
524
+ ) -> None:
525
+ """Save a proof snapshot for a PR at creation time."""
526
+ _ensure_tables(workspace)
527
+ conn = get_db_connection(workspace)
528
+ cursor = conn.cursor()
529
+ cursor.execute(
530
+ """INSERT OR REPLACE INTO pr_proof_snapshots
531
+ (pr_number, workspace_id, gates_passed, gates_total,
532
+ gate_breakdown, snapshotted_at)
533
+ VALUES (?, ?, ?, ?, ?, ?)""",
534
+ (
535
+ pr_number,
536
+ workspace.id,
537
+ gates_passed,
538
+ gates_total,
539
+ json.dumps(gate_breakdown),
540
+ _utc_now().isoformat(),
541
+ ),
542
+ )
543
+ conn.commit()
544
+ conn.close()
545
+
546
+
547
+ def get_pr_proof_snapshot(
548
+ workspace: Workspace, pr_number: int
549
+ ) -> Optional[dict]:
550
+ """Fetch a proof snapshot for a PR.
551
+
552
+ Returns:
553
+ Dict with pr_number, gates_passed, gates_total, gate_breakdown,
554
+ snapshotted_at — or None if not found.
555
+ """
556
+ _ensure_tables(workspace)
557
+ conn = get_db_connection(workspace)
558
+ cursor = conn.cursor()
559
+ cursor.execute(
560
+ """SELECT pr_number, gates_passed, gates_total, gate_breakdown, snapshotted_at
561
+ FROM pr_proof_snapshots WHERE pr_number = ? AND workspace_id = ?""",
562
+ (pr_number, workspace.id),
563
+ )
564
+ row = cursor.fetchone()
565
+ conn.close()
566
+ if not row:
567
+ return None
568
+ return {
569
+ "pr_number": row[0],
570
+ "gates_passed": row[1],
571
+ "gates_total": row[2],
572
+ "gate_breakdown": json.loads(row[3]),
573
+ "snapshotted_at": row[4],
574
+ }