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,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
|
+
}
|