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,162 @@
|
|
|
1
|
+
"""PROOF9 data models.
|
|
2
|
+
|
|
3
|
+
Defines the requirement, obligation, evidence, and waiver data structures.
|
|
4
|
+
Uses dataclasses following the pattern in core/gates.py.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from dataclasses import dataclass, field
|
|
8
|
+
from datetime import date, datetime
|
|
9
|
+
from enum import Enum
|
|
10
|
+
from typing import Optional
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
# Shared filename for the per-workspace PROOF9 config (issue #556).
|
|
14
|
+
# Lives under workspace.state_dir.
|
|
15
|
+
PROOF_CONFIG_FILENAME = "proof_config.json"
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class Gate(str, Enum):
|
|
19
|
+
"""The 9 proof gates."""
|
|
20
|
+
|
|
21
|
+
UNIT = "unit"
|
|
22
|
+
CONTRACT = "contract"
|
|
23
|
+
E2E = "e2e"
|
|
24
|
+
VISUAL = "visual"
|
|
25
|
+
A11Y = "a11y"
|
|
26
|
+
PERF = "perf"
|
|
27
|
+
SEC = "sec"
|
|
28
|
+
DEMO = "demo"
|
|
29
|
+
MANUAL = "manual"
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
# Canonical PROOF9 gate order (matches the Gate enum declaration).
|
|
33
|
+
PROOF9_GATE_ORDER: tuple[str, ...] = tuple(g.value for g in Gate)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class GlitchType(str, Enum):
|
|
37
|
+
"""Classification of the glitch that produced a requirement."""
|
|
38
|
+
|
|
39
|
+
LOGIC_BUG = "logic_bug"
|
|
40
|
+
INTEGRATION_BUG = "integration_bug"
|
|
41
|
+
UI_WIRING_BUG = "ui_wiring_bug"
|
|
42
|
+
UI_LAYOUT_BUG = "ui_layout_bug"
|
|
43
|
+
A11Y_BUG = "a11y_bug"
|
|
44
|
+
PERF_REGRESSION = "perf_regression"
|
|
45
|
+
SECURITY_ISSUE = "security_issue"
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class ReqStatus(str, Enum):
|
|
49
|
+
"""Lifecycle status of a requirement."""
|
|
50
|
+
|
|
51
|
+
OPEN = "open"
|
|
52
|
+
SATISFIED = "satisfied"
|
|
53
|
+
WAIVED = "waived"
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class Source(str, Enum):
|
|
57
|
+
"""Where the glitch was discovered."""
|
|
58
|
+
|
|
59
|
+
PRODUCTION = "production"
|
|
60
|
+
QA = "qa"
|
|
61
|
+
DOGFOODING = "dogfooding"
|
|
62
|
+
MONITORING = "monitoring"
|
|
63
|
+
USER_REPORT = "user_report"
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class Severity(str, Enum):
|
|
67
|
+
"""Requirement severity level."""
|
|
68
|
+
|
|
69
|
+
CRITICAL = "critical"
|
|
70
|
+
HIGH = "high"
|
|
71
|
+
MEDIUM = "medium"
|
|
72
|
+
LOW = "low"
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
@dataclass
|
|
76
|
+
class RequirementScope:
|
|
77
|
+
"""What code areas a requirement applies to."""
|
|
78
|
+
|
|
79
|
+
routes: list[str] = field(default_factory=list)
|
|
80
|
+
components: list[str] = field(default_factory=list)
|
|
81
|
+
apis: list[str] = field(default_factory=list)
|
|
82
|
+
files: list[str] = field(default_factory=list)
|
|
83
|
+
tags: list[str] = field(default_factory=list)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
@dataclass
|
|
87
|
+
class Obligation:
|
|
88
|
+
"""A single proof obligation attached to a requirement."""
|
|
89
|
+
|
|
90
|
+
gate: Gate
|
|
91
|
+
status: str = "pending" # pending, satisfied, failed
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
@dataclass
|
|
95
|
+
class EvidenceRule:
|
|
96
|
+
"""What counts as satisfying an obligation."""
|
|
97
|
+
|
|
98
|
+
test_id: str
|
|
99
|
+
must_pass: bool = True
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
@dataclass
|
|
103
|
+
class Waiver:
|
|
104
|
+
"""Temporary exemption from an obligation."""
|
|
105
|
+
|
|
106
|
+
reason: str
|
|
107
|
+
expires: Optional[date] = None
|
|
108
|
+
manual_checklist: list[str] = field(default_factory=list)
|
|
109
|
+
approved_by: str = ""
|
|
110
|
+
waived_at: Optional[datetime] = None
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
@dataclass
|
|
114
|
+
class Evidence:
|
|
115
|
+
"""Proof that an obligation was checked."""
|
|
116
|
+
|
|
117
|
+
req_id: str
|
|
118
|
+
gate: Gate
|
|
119
|
+
satisfied: bool
|
|
120
|
+
artifact_path: str
|
|
121
|
+
artifact_checksum: str
|
|
122
|
+
timestamp: datetime
|
|
123
|
+
run_id: str
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
@dataclass
|
|
127
|
+
class ProofRun:
|
|
128
|
+
"""A single proof gate run record."""
|
|
129
|
+
|
|
130
|
+
run_id: str
|
|
131
|
+
workspace_id: str
|
|
132
|
+
started_at: datetime
|
|
133
|
+
completed_at: Optional[datetime]
|
|
134
|
+
triggered_by: str # 'human' | 'auto'
|
|
135
|
+
overall_passed: bool
|
|
136
|
+
duration_ms: Optional[int]
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
@dataclass
|
|
140
|
+
class Requirement:
|
|
141
|
+
"""A proof obligation born from a glitch.
|
|
142
|
+
|
|
143
|
+
The core data unit of PROOF9. Created by `cf proof capture`,
|
|
144
|
+
enforced by `cf proof run`, persisted in the ledger.
|
|
145
|
+
"""
|
|
146
|
+
|
|
147
|
+
id: str
|
|
148
|
+
title: str
|
|
149
|
+
description: str
|
|
150
|
+
severity: Severity
|
|
151
|
+
source: Source
|
|
152
|
+
scope: RequirementScope
|
|
153
|
+
obligations: list[Obligation]
|
|
154
|
+
evidence_rules: list[EvidenceRule]
|
|
155
|
+
status: ReqStatus = ReqStatus.OPEN
|
|
156
|
+
waiver: Optional[Waiver] = None
|
|
157
|
+
created_at: Optional[datetime] = None
|
|
158
|
+
satisfied_at: Optional[datetime] = None
|
|
159
|
+
created_by: str = ""
|
|
160
|
+
source_issue: Optional[str] = None
|
|
161
|
+
related_reqs: list[str] = field(default_factory=list)
|
|
162
|
+
glitch_type: Optional[GlitchType] = None
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
"""PROOF9 obligation mapping engine.
|
|
2
|
+
|
|
3
|
+
Maps glitch types to the minimal set of proof gates that would have
|
|
4
|
+
caught the issue. Uses keyword heuristics for classification (no LLM
|
|
5
|
+
dependency for MVP).
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import re
|
|
9
|
+
|
|
10
|
+
from codeframe.core.proof.models import (
|
|
11
|
+
EvidenceRule,
|
|
12
|
+
Gate,
|
|
13
|
+
GlitchType,
|
|
14
|
+
Obligation,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
OBLIGATION_MAP: dict[GlitchType, list[Gate]] = {
|
|
18
|
+
GlitchType.LOGIC_BUG: [Gate.UNIT, Gate.CONTRACT],
|
|
19
|
+
GlitchType.INTEGRATION_BUG: [Gate.CONTRACT, Gate.E2E],
|
|
20
|
+
GlitchType.UI_WIRING_BUG: [Gate.E2E, Gate.DEMO],
|
|
21
|
+
GlitchType.UI_LAYOUT_BUG: [Gate.VISUAL, Gate.E2E],
|
|
22
|
+
GlitchType.A11Y_BUG: [Gate.A11Y, Gate.E2E],
|
|
23
|
+
GlitchType.PERF_REGRESSION: [Gate.PERF, Gate.E2E],
|
|
24
|
+
GlitchType.SECURITY_ISSUE: [Gate.SEC],
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
# Keywords for heuristic classification
|
|
28
|
+
_KEYWORDS: dict[GlitchType, list[str]] = {
|
|
29
|
+
GlitchType.SECURITY_ISSUE: [
|
|
30
|
+
"security", "xss", "injection", "auth", "csrf", "vulnerability",
|
|
31
|
+
"exploit", "credential", "password", "token", "permission",
|
|
32
|
+
],
|
|
33
|
+
GlitchType.PERF_REGRESSION: [
|
|
34
|
+
"slow", "performance", "latency", "timeout", "memory", "cpu",
|
|
35
|
+
"regression", "benchmark", "load", "throughput",
|
|
36
|
+
],
|
|
37
|
+
GlitchType.A11Y_BUG: [
|
|
38
|
+
"accessibility", "a11y", "screen reader", "aria", "wcag",
|
|
39
|
+
"contrast", "keyboard", "focus", "alt text",
|
|
40
|
+
],
|
|
41
|
+
GlitchType.UI_LAYOUT_BUG: [
|
|
42
|
+
"layout", "css", "style", "render", "display", "overlap",
|
|
43
|
+
"truncat", "responsive", "mobile", "visual",
|
|
44
|
+
],
|
|
45
|
+
GlitchType.UI_WIRING_BUG: [
|
|
46
|
+
"click", "button", "form", "submit", "navigation", "redirect",
|
|
47
|
+
"event", "handler", "callback", "ui",
|
|
48
|
+
],
|
|
49
|
+
GlitchType.INTEGRATION_BUG: [
|
|
50
|
+
"api", "endpoint", "integration", "contract", "schema",
|
|
51
|
+
"request", "response", "http", "grpc", "webhook",
|
|
52
|
+
],
|
|
53
|
+
GlitchType.LOGIC_BUG: [
|
|
54
|
+
"wrong", "incorrect", "error", "bug", "crash", "exception",
|
|
55
|
+
"null", "undefined", "logic", "calculation",
|
|
56
|
+
],
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def classify_glitch(description: str) -> GlitchType:
|
|
61
|
+
"""Classify a glitch description into a GlitchType using keyword heuristics.
|
|
62
|
+
|
|
63
|
+
Scans the description for keywords associated with each glitch type.
|
|
64
|
+
Returns the type with the most keyword matches, defaulting to LOGIC_BUG.
|
|
65
|
+
"""
|
|
66
|
+
text = description.lower()
|
|
67
|
+
scores: dict[GlitchType, int] = {}
|
|
68
|
+
|
|
69
|
+
for glitch_type, keywords in _KEYWORDS.items():
|
|
70
|
+
score = sum(1 for kw in keywords if re.search(r"\b" + re.escape(kw) + r"\b", text))
|
|
71
|
+
if score > 0:
|
|
72
|
+
scores[glitch_type] = score
|
|
73
|
+
|
|
74
|
+
if not scores:
|
|
75
|
+
return GlitchType.LOGIC_BUG
|
|
76
|
+
|
|
77
|
+
return max(scores, key=scores.get) # type: ignore[arg-type]
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def get_obligations(glitch_type: GlitchType) -> list[Obligation]:
|
|
81
|
+
"""Return the minimal gate set for a glitch type as Obligation objects."""
|
|
82
|
+
gates = OBLIGATION_MAP.get(glitch_type, [Gate.UNIT])
|
|
83
|
+
return [Obligation(gate=g) for g in gates]
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def suggest_evidence_rules(gate: Gate, description: str) -> list[EvidenceRule]:
|
|
87
|
+
"""Generate starter evidence rules for an obligation gate."""
|
|
88
|
+
# Create a sensible test ID from the description
|
|
89
|
+
slug = re.sub(r"[^a-z0-9]+", "_", description.lower().strip())[:60].strip("_")
|
|
90
|
+
|
|
91
|
+
prefix_map = {
|
|
92
|
+
Gate.UNIT: "test_unit_",
|
|
93
|
+
Gate.CONTRACT: "test_contract_",
|
|
94
|
+
Gate.E2E: "test_e2e_",
|
|
95
|
+
Gate.VISUAL: "test_visual_",
|
|
96
|
+
Gate.A11Y: "test_a11y_",
|
|
97
|
+
Gate.PERF: "test_perf_",
|
|
98
|
+
Gate.SEC: "test_sec_",
|
|
99
|
+
Gate.DEMO: "test_demo_",
|
|
100
|
+
Gate.MANUAL: "manual_check_",
|
|
101
|
+
}
|
|
102
|
+
prefix = prefix_map.get(gate, "test_")
|
|
103
|
+
return [EvidenceRule(test_id=f"{prefix}{slug}", must_pass=True)]
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
"""PROOF9 runner — executes obligations and collects evidence.
|
|
2
|
+
|
|
3
|
+
Determines which requirements apply to the current changes,
|
|
4
|
+
runs their obligations via the existing gates infrastructure,
|
|
5
|
+
and attaches evidence artifacts.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import json
|
|
9
|
+
import logging
|
|
10
|
+
import uuid
|
|
11
|
+
from datetime import datetime, timezone
|
|
12
|
+
from typing import Optional
|
|
13
|
+
|
|
14
|
+
from codeframe.core.proof import ledger
|
|
15
|
+
from codeframe.core.proof.evidence import attach_evidence
|
|
16
|
+
from codeframe.core.proof.models import (
|
|
17
|
+
PROOF_CONFIG_FILENAME,
|
|
18
|
+
Gate,
|
|
19
|
+
ProofRun,
|
|
20
|
+
ReqStatus,
|
|
21
|
+
)
|
|
22
|
+
from codeframe.core.proof.scope import get_changed_scope, intersects
|
|
23
|
+
from codeframe.core.workspace import Workspace
|
|
24
|
+
|
|
25
|
+
logger = logging.getLogger(__name__)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _load_proof_config(workspace: Workspace) -> tuple[Optional[set[Gate]], str]:
|
|
29
|
+
"""Load (enabled_gates, strictness) from .codeframe/proof_config.json.
|
|
30
|
+
|
|
31
|
+
Returns:
|
|
32
|
+
(enabled_gates, strictness). enabled_gates is None when no config file
|
|
33
|
+
exists (meaning "all gates allowed"); a set of Gate enums otherwise.
|
|
34
|
+
strictness defaults to 'strict' when missing or invalid.
|
|
35
|
+
"""
|
|
36
|
+
path = workspace.state_dir / PROOF_CONFIG_FILENAME
|
|
37
|
+
if not path.exists():
|
|
38
|
+
return None, "strict"
|
|
39
|
+
try:
|
|
40
|
+
data = json.loads(path.read_text())
|
|
41
|
+
except (OSError, json.JSONDecodeError) as exc:
|
|
42
|
+
logger.warning("Invalid %s — using defaults: %s", PROOF_CONFIG_FILENAME, exc)
|
|
43
|
+
return None, "strict"
|
|
44
|
+
|
|
45
|
+
enabled: Optional[set[Gate]] = None
|
|
46
|
+
gates_raw = data.get("enabled_gates")
|
|
47
|
+
if isinstance(gates_raw, list):
|
|
48
|
+
enabled = set()
|
|
49
|
+
valid_values = {g.value for g in Gate}
|
|
50
|
+
for name in gates_raw:
|
|
51
|
+
if name in valid_values:
|
|
52
|
+
enabled.add(Gate(name))
|
|
53
|
+
else:
|
|
54
|
+
logger.warning(
|
|
55
|
+
"Unknown gate name '%s' in %s — skipped (valid: %s)",
|
|
56
|
+
name,
|
|
57
|
+
PROOF_CONFIG_FILENAME,
|
|
58
|
+
valid_values,
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
strictness = data.get("strictness", "strict")
|
|
62
|
+
if strictness not in ("strict", "warn"):
|
|
63
|
+
strictness = "strict"
|
|
64
|
+
return enabled, strictness
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
# Map PROOF9 gates to existing core/gates.py gate names
|
|
68
|
+
_GATE_TO_CORE: dict[Gate, str] = {
|
|
69
|
+
Gate.UNIT: "pytest",
|
|
70
|
+
Gate.CONTRACT: "pytest",
|
|
71
|
+
Gate.SEC: "ruff",
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _run_gate(workspace: Workspace, gate: Gate) -> tuple[bool, str]:
|
|
76
|
+
"""Execute a single gate and return (passed, output).
|
|
77
|
+
|
|
78
|
+
Uses core/gates.py for gates that have direct tool support.
|
|
79
|
+
Returns (True, "") for gates without automated tooling yet.
|
|
80
|
+
"""
|
|
81
|
+
core_gate_name = _GATE_TO_CORE.get(gate)
|
|
82
|
+
if not core_gate_name:
|
|
83
|
+
return False, f"Gate {gate.value} has no automated runner — cannot verify"
|
|
84
|
+
|
|
85
|
+
try:
|
|
86
|
+
from codeframe.core.gates import run as run_gates
|
|
87
|
+
result = run_gates(workspace, gates=[core_gate_name], verbose=False)
|
|
88
|
+
output_parts = [
|
|
89
|
+
f"{check.name}: {check.status.value}" for check in result.checks
|
|
90
|
+
]
|
|
91
|
+
return result.passed, "\n".join(output_parts)
|
|
92
|
+
except Exception as exc:
|
|
93
|
+
logger.warning("Gate %s failed to run: %s", gate.value, exc)
|
|
94
|
+
return False, str(exc)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def run_proof(
|
|
98
|
+
workspace: Workspace,
|
|
99
|
+
*,
|
|
100
|
+
full: bool = False,
|
|
101
|
+
gate_filter: Optional[Gate] = None,
|
|
102
|
+
run_id: Optional[str] = None,
|
|
103
|
+
) -> dict[str, list[tuple[Gate, bool]]]:
|
|
104
|
+
"""Execute proof obligations and collect evidence.
|
|
105
|
+
|
|
106
|
+
Args:
|
|
107
|
+
workspace: Target workspace
|
|
108
|
+
full: If True, run ALL obligations regardless of scope
|
|
109
|
+
gate_filter: If set, only run this specific gate
|
|
110
|
+
run_id: Unique run identifier (auto-generated if not provided)
|
|
111
|
+
|
|
112
|
+
Returns:
|
|
113
|
+
Dict mapping req_id → list of (Gate, satisfied) tuples
|
|
114
|
+
"""
|
|
115
|
+
if not run_id:
|
|
116
|
+
run_id = str(uuid.uuid4())[:8]
|
|
117
|
+
|
|
118
|
+
started_at = datetime.now(timezone.utc)
|
|
119
|
+
|
|
120
|
+
# Load PROOF9 config (enabled gates + strictness)
|
|
121
|
+
enabled_gates, strictness = _load_proof_config(workspace)
|
|
122
|
+
|
|
123
|
+
# Expire any stale waivers
|
|
124
|
+
expired = ledger.check_expired_waivers(workspace)
|
|
125
|
+
if expired:
|
|
126
|
+
logger.info("Expired %d waivers", len(expired))
|
|
127
|
+
|
|
128
|
+
# Get all open requirements
|
|
129
|
+
reqs = ledger.list_requirements(workspace, status=ReqStatus.OPEN)
|
|
130
|
+
if not reqs:
|
|
131
|
+
completed_at = datetime.now(timezone.utc)
|
|
132
|
+
ledger.save_run(
|
|
133
|
+
workspace,
|
|
134
|
+
ProofRun(
|
|
135
|
+
run_id=run_id,
|
|
136
|
+
workspace_id=workspace.id,
|
|
137
|
+
started_at=started_at,
|
|
138
|
+
completed_at=completed_at,
|
|
139
|
+
triggered_by="human",
|
|
140
|
+
overall_passed=True,
|
|
141
|
+
duration_ms=int((completed_at - started_at).total_seconds() * 1000),
|
|
142
|
+
),
|
|
143
|
+
)
|
|
144
|
+
return {}
|
|
145
|
+
|
|
146
|
+
# Warn loudly when config disables every gate — a "vacuous pass" is
|
|
147
|
+
# easy to overlook: nothing runs, overall_passed=True, no evidence.
|
|
148
|
+
if enabled_gates is not None and not enabled_gates:
|
|
149
|
+
logger.warning(
|
|
150
|
+
"Proof run %s: all 9 gates are disabled by proof_config.json — "
|
|
151
|
+
"no obligations will run and the run will pass vacuously",
|
|
152
|
+
run_id,
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
# Get changed scope (skip if running full)
|
|
156
|
+
changed_scope = None
|
|
157
|
+
if not full:
|
|
158
|
+
changed_scope = get_changed_scope(workspace)
|
|
159
|
+
|
|
160
|
+
results: dict[str, list[tuple[Gate, bool]]] = {}
|
|
161
|
+
artifact_dir = workspace.state_dir / "proof_artifacts"
|
|
162
|
+
artifact_dir.mkdir(exist_ok=True)
|
|
163
|
+
|
|
164
|
+
for req in reqs:
|
|
165
|
+
# Check scope intersection (unless full mode or scope detection failed)
|
|
166
|
+
# None changed_scope means "failed to detect" → run everything (fail closed)
|
|
167
|
+
if not full and changed_scope is not None:
|
|
168
|
+
if not intersects(req.scope, changed_scope):
|
|
169
|
+
continue
|
|
170
|
+
|
|
171
|
+
req_results: list[tuple[Gate, bool]] = []
|
|
172
|
+
|
|
173
|
+
for obl in req.obligations:
|
|
174
|
+
# Apply gate filter
|
|
175
|
+
if gate_filter and obl.gate != gate_filter:
|
|
176
|
+
continue
|
|
177
|
+
|
|
178
|
+
# Apply config-driven gate filter (None means "all allowed")
|
|
179
|
+
if enabled_gates is not None and obl.gate not in enabled_gates:
|
|
180
|
+
continue
|
|
181
|
+
|
|
182
|
+
# Run the gate
|
|
183
|
+
passed, output = _run_gate(workspace, obl.gate)
|
|
184
|
+
|
|
185
|
+
# Write artifact
|
|
186
|
+
artifact_path = artifact_dir / f"{req.id}_{obl.gate.value}_{run_id}.txt"
|
|
187
|
+
artifact_path.write_text(output)
|
|
188
|
+
|
|
189
|
+
# Attach evidence
|
|
190
|
+
attach_evidence(
|
|
191
|
+
workspace, req.id, obl.gate,
|
|
192
|
+
str(artifact_path), passed, run_id,
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
# Update obligation status
|
|
196
|
+
obl.status = "satisfied" if passed else "failed"
|
|
197
|
+
req_results.append((obl.gate, passed))
|
|
198
|
+
|
|
199
|
+
if req_results:
|
|
200
|
+
results[req.id] = req_results
|
|
201
|
+
|
|
202
|
+
# Always persist obligation status updates
|
|
203
|
+
all_satisfied = all(passed for _, passed in req_results)
|
|
204
|
+
if all_satisfied and len(req_results) == len(req.obligations):
|
|
205
|
+
req.status = ReqStatus.SATISFIED
|
|
206
|
+
ledger.save_requirement(workspace, req)
|
|
207
|
+
|
|
208
|
+
completed_at = datetime.now(timezone.utc)
|
|
209
|
+
duration_ms = int((completed_at - started_at).total_seconds() * 1000)
|
|
210
|
+
executed = [passed for gate_results in results.values() for _, passed in gate_results]
|
|
211
|
+
all_passed = all(executed) if executed else True
|
|
212
|
+
if not all_passed and strictness == "warn":
|
|
213
|
+
logger.warning(
|
|
214
|
+
"Proof run %s had gate failures but strictness='warn' — overall_passed=True",
|
|
215
|
+
run_id,
|
|
216
|
+
)
|
|
217
|
+
overall_passed = True
|
|
218
|
+
else:
|
|
219
|
+
overall_passed = all_passed
|
|
220
|
+
ledger.save_run(
|
|
221
|
+
workspace,
|
|
222
|
+
ProofRun(
|
|
223
|
+
run_id=run_id,
|
|
224
|
+
workspace_id=workspace.id,
|
|
225
|
+
started_at=started_at,
|
|
226
|
+
completed_at=completed_at,
|
|
227
|
+
triggered_by="human",
|
|
228
|
+
overall_passed=overall_passed,
|
|
229
|
+
duration_ms=duration_ms,
|
|
230
|
+
),
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
return results
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
"""PROOF9 scope intersection engine.
|
|
2
|
+
|
|
3
|
+
Determines which requirements apply to the current set of changes
|
|
4
|
+
by matching requirement scopes against changed files/routes.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import logging
|
|
8
|
+
import re
|
|
9
|
+
|
|
10
|
+
from codeframe.core.proof.models import RequirementScope
|
|
11
|
+
from codeframe.core.workspace import Workspace
|
|
12
|
+
|
|
13
|
+
logger = logging.getLogger(__name__)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def build_scope_from_capture(where: str) -> RequirementScope:
|
|
17
|
+
"""Parse a user-provided location string into a RequirementScope.
|
|
18
|
+
|
|
19
|
+
Heuristics:
|
|
20
|
+
- Starts with / and contains path segments → route
|
|
21
|
+
- Contains file extension → file
|
|
22
|
+
- Contains HTTP method (GET, POST, etc.) → api
|
|
23
|
+
- Otherwise → tag
|
|
24
|
+
"""
|
|
25
|
+
scope = RequirementScope()
|
|
26
|
+
parts = [p.strip() for p in where.split(",")]
|
|
27
|
+
|
|
28
|
+
for part in parts:
|
|
29
|
+
if not part:
|
|
30
|
+
continue
|
|
31
|
+
if re.match(r"^(GET|POST|PUT|DELETE|PATCH)\s+", part, re.IGNORECASE):
|
|
32
|
+
scope.apis.append(part)
|
|
33
|
+
elif re.match(r"^/[\w/\-.*]+$", part):
|
|
34
|
+
scope.routes.append(part)
|
|
35
|
+
elif "." in part and "/" in part:
|
|
36
|
+
scope.files.append(part)
|
|
37
|
+
elif re.match(r"^[\w/]+\.\w+$", part):
|
|
38
|
+
scope.files.append(part)
|
|
39
|
+
else:
|
|
40
|
+
scope.tags.append(part)
|
|
41
|
+
|
|
42
|
+
return scope
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def get_changed_scope(workspace: Workspace) -> "RequirementScope | None":
|
|
46
|
+
"""Detect changed files from git and build a scope.
|
|
47
|
+
|
|
48
|
+
Uses gitpython via core/git.py patterns to get modified files.
|
|
49
|
+
"""
|
|
50
|
+
try:
|
|
51
|
+
from codeframe.core.git import get_status
|
|
52
|
+
status = get_status(workspace)
|
|
53
|
+
all_files = status.modified_files + status.staged_files + status.untracked_files
|
|
54
|
+
scope = RequirementScope(files=list(set(all_files)))
|
|
55
|
+
return scope
|
|
56
|
+
except Exception as exc:
|
|
57
|
+
logger.warning("Could not detect changed files: %s — failing closed (match all)", exc)
|
|
58
|
+
return None # Caller must treat None as "match everything"
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def intersects(req_scope: RequirementScope, changed_scope: RequirementScope) -> bool:
|
|
62
|
+
"""Check if a requirement scope overlaps with changed scope.
|
|
63
|
+
|
|
64
|
+
Returns True if any field has common elements. File matching
|
|
65
|
+
supports prefix matching (req file "src/auth/" matches changed
|
|
66
|
+
file "src/auth/login.py").
|
|
67
|
+
"""
|
|
68
|
+
# Direct set intersection for routes, apis, components, tags
|
|
69
|
+
for field_name in ("routes", "apis", "components", "tags"):
|
|
70
|
+
req_items = set(getattr(req_scope, field_name))
|
|
71
|
+
changed_items = set(getattr(changed_scope, field_name))
|
|
72
|
+
if req_items & changed_items:
|
|
73
|
+
return True
|
|
74
|
+
|
|
75
|
+
# File matching — exact match only (no directory expansion)
|
|
76
|
+
req_files = set(req_scope.files)
|
|
77
|
+
changed_files = set(changed_scope.files)
|
|
78
|
+
if req_files & changed_files:
|
|
79
|
+
return True
|
|
80
|
+
|
|
81
|
+
return False
|