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