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,558 @@
1
+ """Pattern-based quick fixes for common errors.
2
+
3
+ Fixes common errors without requiring LLM calls:
4
+ - ModuleNotFoundError → install package
5
+ - ImportError → add import statement
6
+ - NameError → add missing import
7
+ - SyntaxError → common syntax fixes
8
+ - IndentationError → fix indentation
9
+
10
+ This module is headless - no FastAPI or HTTP dependencies.
11
+ """
12
+
13
+ import re
14
+ from dataclasses import dataclass
15
+ from enum import Enum
16
+ from pathlib import Path
17
+ from typing import Optional, Callable
18
+
19
+
20
+ class FixType(str, Enum):
21
+ """Type of fix to apply."""
22
+
23
+ INSTALL_PACKAGE = "install_package"
24
+ ADD_IMPORT = "add_import"
25
+ FIX_SYNTAX = "fix_syntax"
26
+ FIX_INDENTATION = "fix_indentation"
27
+ ADD_MISSING_FILE = "add_missing_file"
28
+ FIX_TYPE_HINT = "fix_type_hint"
29
+
30
+
31
+ @dataclass
32
+ class QuickFix:
33
+ """A quick fix that can be applied without LLM.
34
+
35
+ Attributes:
36
+ fix_type: Category of fix
37
+ description: Human-readable description
38
+ command: Shell command to run (for INSTALL_PACKAGE)
39
+ file_path: File to modify (for code fixes)
40
+ old_content: Content to find/replace
41
+ new_content: Replacement content
42
+ insert_line: Line number to insert at (1-based)
43
+ insert_content: Content to insert
44
+ """
45
+
46
+ fix_type: FixType
47
+ description: str
48
+ command: Optional[str] = None
49
+ file_path: Optional[str] = None
50
+ old_content: Optional[str] = None
51
+ new_content: Optional[str] = None
52
+ insert_line: Optional[int] = None
53
+ insert_content: Optional[str] = None
54
+
55
+
56
+ # Mapping from common import names to package names
57
+ # (when they differ, e.g., import PIL → pip install Pillow)
58
+ PACKAGE_ALIASES = {
59
+ "PIL": "Pillow",
60
+ "cv2": "opencv-python",
61
+ "sklearn": "scikit-learn",
62
+ "yaml": "pyyaml",
63
+ "bs4": "beautifulsoup4",
64
+ "dateutil": "python-dateutil",
65
+ }
66
+
67
+ # Common standard library modules (shouldn't be installed)
68
+ STDLIB_MODULES = {
69
+ "os", "sys", "re", "json", "datetime", "time", "math", "random",
70
+ "collections", "itertools", "functools", "typing", "pathlib",
71
+ "subprocess", "threading", "multiprocessing", "asyncio", "contextlib",
72
+ "dataclasses", "enum", "abc", "copy", "io", "tempfile", "shutil",
73
+ "logging", "unittest", "argparse", "configparser", "hashlib", "hmac",
74
+ "base64", "struct", "pickle", "sqlite3", "csv", "xml", "html",
75
+ "http", "urllib", "email", "mimetypes", "socket", "ssl", "select",
76
+ "signal", "platform", "getpass", "glob", "fnmatch", "textwrap",
77
+ "string", "decimal", "fractions", "statistics", "secrets", "uuid",
78
+ }
79
+
80
+
81
+ def detect_package_manager(repo_path: Path) -> str:
82
+ """Detect the package manager to use.
83
+
84
+ Args:
85
+ repo_path: Path to the repository
86
+
87
+ Returns:
88
+ Package manager command (uv, pip, npm, etc.)
89
+ """
90
+ # Check for Poetry first (poetry.lock is definitive)
91
+ if (repo_path / "poetry.lock").exists():
92
+ return "poetry add"
93
+
94
+ # Check for uv lock file (definitive for uv)
95
+ if (repo_path / "uv.lock").exists():
96
+ return "uv pip install"
97
+
98
+ # Check pyproject.toml for tool-specific markers
99
+ pyproject = repo_path / "pyproject.toml"
100
+ if pyproject.exists():
101
+ content = pyproject.read_text()
102
+ # Check for uv-specific configuration
103
+ if "[tool.uv]" in content:
104
+ return "uv pip install"
105
+ # Check for Poetry-specific configuration
106
+ if "[tool.poetry]" in content:
107
+ return "poetry add"
108
+ # Generic pyproject.toml - default to uv
109
+ return "uv pip install"
110
+
111
+ if (repo_path / "requirements.txt").exists():
112
+ return "pip install"
113
+
114
+ if (repo_path / "Pipfile").exists():
115
+ return "pipenv install"
116
+
117
+ # Check for Node.js package managers
118
+ if (repo_path / "package-lock.json").exists():
119
+ return "npm install"
120
+
121
+ if (repo_path / "yarn.lock").exists():
122
+ return "yarn add"
123
+
124
+ if (repo_path / "pnpm-lock.yaml").exists():
125
+ return "pnpm add"
126
+
127
+ # Default to pip
128
+ return "pip install"
129
+
130
+
131
+ def match_module_not_found(error: str) -> Optional[QuickFix]:
132
+ """Match ModuleNotFoundError and generate install fix.
133
+
134
+ Patterns:
135
+ - ModuleNotFoundError: No module named 'X'
136
+ - ImportError: No module named 'X'
137
+
138
+ Args:
139
+ error: Error message
140
+
141
+ Returns:
142
+ QuickFix if matched, None otherwise
143
+ """
144
+ patterns = [
145
+ r"ModuleNotFoundError: No module named ['\"]([^'\"]+)['\"]",
146
+ r"ImportError: No module named ['\"]([^'\"]+)['\"]",
147
+ r"No module named ['\"]([^'\"]+)['\"]",
148
+ ]
149
+
150
+ for pattern in patterns:
151
+ match = re.search(pattern, error, re.IGNORECASE)
152
+ if match:
153
+ module = match.group(1).split('.')[0] # Get top-level module
154
+
155
+ # Skip standard library modules
156
+ if module in STDLIB_MODULES:
157
+ return None
158
+
159
+ # Get actual package name
160
+ package = PACKAGE_ALIASES.get(module, module)
161
+
162
+ return QuickFix(
163
+ fix_type=FixType.INSTALL_PACKAGE,
164
+ description=f"Install missing package: {package}",
165
+ command=f"{{package_manager}} {package}",
166
+ )
167
+
168
+ return None
169
+
170
+
171
+ def match_import_error(error: str, file_content: Optional[str] = None) -> Optional[QuickFix]:
172
+ """Match ImportError for specific name and suggest import.
173
+
174
+ Patterns:
175
+ - ImportError: cannot import name 'X' from 'Y'
176
+ - cannot import name 'X' from 'Y'
177
+
178
+ Args:
179
+ error: Error message
180
+ file_content: Content of the file with the error
181
+
182
+ Returns:
183
+ QuickFix if matched, None otherwise
184
+ """
185
+ pattern = r"cannot import name ['\"]([^'\"]+)['\"] from ['\"]([^'\"]+)['\"]"
186
+ match = re.search(pattern, error, re.IGNORECASE)
187
+
188
+ if match:
189
+ name = match.group(1)
190
+ module = match.group(2)
191
+
192
+ return QuickFix(
193
+ fix_type=FixType.ADD_IMPORT,
194
+ description=f"Fix import: from {module} import {name}",
195
+ insert_line=1, # Insert at top of file
196
+ insert_content=f"from {module} import {name}\n",
197
+ )
198
+
199
+ return None
200
+
201
+
202
+ def match_name_error(error: str, file_content: Optional[str] = None) -> Optional[QuickFix]:
203
+ """Match NameError and suggest import if it's a known module/type.
204
+
205
+ Patterns:
206
+ - NameError: name 'X' is not defined
207
+ - name 'X' is not defined
208
+
209
+ Args:
210
+ error: Error message
211
+ file_content: Content of the file with the error
212
+
213
+ Returns:
214
+ QuickFix if matched, None otherwise
215
+ """
216
+ pattern = r"(?:NameError: )?name ['\"]([^'\"]+)['\"] is not defined"
217
+ match = re.search(pattern, error, re.IGNORECASE)
218
+
219
+ if match:
220
+ name = match.group(1)
221
+
222
+ # Common imports that cause NameError
223
+ common_imports = {
224
+ # Typing
225
+ "Optional": "from typing import Optional",
226
+ "List": "from typing import List",
227
+ "Dict": "from typing import Dict",
228
+ "Any": "from typing import Any",
229
+ "Union": "from typing import Union",
230
+ "Callable": "from typing import Callable",
231
+ "TypeVar": "from typing import TypeVar",
232
+ # Dataclasses
233
+ "dataclass": "from dataclasses import dataclass",
234
+ "field": "from dataclasses import field",
235
+ # Enum
236
+ "Enum": "from enum import Enum",
237
+ # Path
238
+ "Path": "from pathlib import Path",
239
+ # Datetime
240
+ "datetime": "from datetime import datetime",
241
+ "timedelta": "from datetime import timedelta",
242
+ "timezone": "from datetime import timezone",
243
+ # JSON
244
+ "json": "import json",
245
+ # Re
246
+ "re": "import re",
247
+ # OS
248
+ "os": "import os",
249
+ # Sys
250
+ "sys": "import sys",
251
+ }
252
+
253
+ if name in common_imports:
254
+ return QuickFix(
255
+ fix_type=FixType.ADD_IMPORT,
256
+ description=f"Add missing import for {name}",
257
+ insert_line=1,
258
+ insert_content=common_imports[name] + "\n",
259
+ )
260
+
261
+ return None
262
+
263
+
264
+ def match_syntax_error(error: str, file_content: Optional[str] = None) -> Optional[QuickFix]:
265
+ """Match common SyntaxError patterns and suggest fixes.
266
+
267
+ Patterns handled:
268
+ - Missing colon after def/class/if/for/while
269
+ - Unclosed brackets/parentheses
270
+ - Invalid syntax near common patterns
271
+
272
+ Args:
273
+ error: Error message
274
+ file_content: Content of the file with the error
275
+
276
+ Returns:
277
+ QuickFix if matched, None otherwise
278
+ """
279
+ # Extract line number if present
280
+ line_match = re.search(r'line (\d+)', error, re.IGNORECASE)
281
+ line_num = int(line_match.group(1)) if line_match else None
282
+
283
+ # Missing colon patterns
284
+ if "expected ':'" in error.lower() or "SyntaxError: invalid syntax" in error:
285
+ if file_content and line_num:
286
+ lines = file_content.split('\n')
287
+ if 0 < line_num <= len(lines):
288
+ line = lines[line_num - 1]
289
+ # Check if it's a def/class/if/for/while without colon
290
+ if re.match(r'^\s*(def|class|if|elif|else|for|while|try|except|finally|with)\s+.+[^:]\s*$', line):
291
+ return QuickFix(
292
+ fix_type=FixType.FIX_SYNTAX,
293
+ description=f"Add missing colon at line {line_num}",
294
+ old_content=line,
295
+ new_content=line.rstrip() + ":",
296
+ )
297
+
298
+ # f-string without f prefix
299
+ if "unterminated string literal" in error.lower() or "invalid syntax" in error.lower():
300
+ if file_content and line_num:
301
+ lines = file_content.split('\n')
302
+ if 0 < line_num <= len(lines):
303
+ line = lines[line_num - 1]
304
+ # Pattern to match string literals with optional prefix
305
+ # Captures: (1) prefix like r/u/b/br/fr, (2) opening quote, (3) body, (4) closing quote
306
+ string_pattern = r'([rRuUbBfF]*)(["\'])([^"\']*\{[^}]+\}[^"\']*)\2'
307
+ match = re.search(string_pattern, line)
308
+ if match:
309
+ prefix = match.group(1)
310
+ quote = match.group(2)
311
+ body = match.group(3)
312
+
313
+ # Skip if already has 'f' prefix
314
+ if 'f' in prefix.lower():
315
+ pass # Already an f-string
316
+ # Skip byte strings - can't add 'f' to 'b'
317
+ elif 'b' in prefix.lower():
318
+ pass # Byte string, can't be f-string
319
+ # Skip unicode strings - 'u' prefix is for Python 2 compatibility
320
+ elif 'u' in prefix.lower():
321
+ pass # Unicode string, don't add 'f' prefix
322
+ else:
323
+ # Add 'f' to prefix (before 'r' if present, e.g., 'r' -> 'rf')
324
+ if 'r' in prefix.lower():
325
+ # Keep the case of r, add f before it
326
+ new_prefix = 'f' + prefix
327
+ else:
328
+ new_prefix = 'f' + prefix
329
+ # Build the new string literal
330
+ old_literal = f"{prefix}{quote}{body}{quote}"
331
+ new_literal = f"{new_prefix}{quote}{body}{quote}"
332
+ new_line = line.replace(old_literal, new_literal, 1)
333
+ if new_line != line:
334
+ return QuickFix(
335
+ fix_type=FixType.FIX_SYNTAX,
336
+ description=f"Add f-string prefix at line {line_num}",
337
+ old_content=line,
338
+ new_content=new_line,
339
+ )
340
+
341
+ return None
342
+
343
+
344
+ def match_indentation_error(error: str, file_content: Optional[str] = None) -> Optional[QuickFix]:
345
+ """Match IndentationError and suggest fix.
346
+
347
+ Args:
348
+ error: Error message
349
+ file_content: Content of the file with the error
350
+
351
+ Returns:
352
+ QuickFix if matched, None otherwise
353
+ """
354
+ if "IndentationError" not in error and "indentation" not in error.lower():
355
+ return None
356
+
357
+ line_match = re.search(r'line (\d+)', error, re.IGNORECASE)
358
+ if not line_match or not file_content:
359
+ return None
360
+
361
+ line_num = int(line_match.group(1))
362
+ lines = file_content.split('\n')
363
+
364
+ if not (0 < line_num <= len(lines)):
365
+ return None
366
+
367
+ current_line = lines[line_num - 1]
368
+
369
+ # Detect mixed tabs/spaces
370
+ if '\t' in current_line and ' ' in current_line[:len(current_line) - len(current_line.lstrip())]:
371
+ # Convert to consistent spaces
372
+ leading = current_line[:len(current_line) - len(current_line.lstrip())]
373
+ # Replace tabs with 4 spaces
374
+ new_leading = leading.replace('\t', ' ')
375
+ new_line = new_leading + current_line.lstrip()
376
+
377
+ return QuickFix(
378
+ fix_type=FixType.FIX_INDENTATION,
379
+ description=f"Fix mixed indentation at line {line_num}",
380
+ old_content=current_line,
381
+ new_content=new_line,
382
+ )
383
+
384
+ # Unexpected indent - try to match previous line's indentation
385
+ if "unexpected indent" in error.lower() and line_num > 1:
386
+ prev_line = lines[line_num - 2]
387
+ prev_indent = len(prev_line) - len(prev_line.lstrip())
388
+
389
+ # If previous line ends with colon, add 4 spaces
390
+ if prev_line.rstrip().endswith(':'):
391
+ expected_indent = prev_indent + 4
392
+ else:
393
+ expected_indent = prev_indent
394
+
395
+ new_line = ' ' * expected_indent + current_line.lstrip()
396
+
397
+ return QuickFix(
398
+ fix_type=FixType.FIX_INDENTATION,
399
+ description=f"Fix unexpected indentation at line {line_num}",
400
+ old_content=current_line,
401
+ new_content=new_line,
402
+ )
403
+
404
+ return None
405
+
406
+
407
+ def match_type_error(error: str) -> Optional[QuickFix]:
408
+ """Match TypeError for missing type hints.
409
+
410
+ Args:
411
+ error: Error message
412
+
413
+ Returns:
414
+ QuickFix if matched, None otherwise
415
+ """
416
+ # This is more complex and usually needs LLM help
417
+ # But we can catch some simple cases
418
+ return None
419
+
420
+
421
+ # All pattern matchers in order of precedence
422
+ PATTERN_MATCHERS: list[Callable] = [
423
+ match_module_not_found,
424
+ match_import_error,
425
+ match_name_error,
426
+ match_syntax_error,
427
+ match_indentation_error,
428
+ match_type_error,
429
+ ]
430
+
431
+
432
+ def find_quick_fix(
433
+ error: str,
434
+ file_path: Optional[Path] = None,
435
+ repo_path: Optional[Path] = None,
436
+ ) -> Optional[QuickFix]:
437
+ """Find a quick fix for an error without LLM.
438
+
439
+ Args:
440
+ error: Error message
441
+ file_path: Path to the file with the error (if known)
442
+ repo_path: Path to the repository root
443
+
444
+ Returns:
445
+ QuickFix if a pattern match is found, None otherwise
446
+ """
447
+ # Read file content if we have a path
448
+ file_content = None
449
+ if file_path and file_path.exists():
450
+ try:
451
+ file_content = file_path.read_text()
452
+ except Exception:
453
+ pass
454
+
455
+ # Try each pattern matcher
456
+ for matcher in PATTERN_MATCHERS:
457
+ try:
458
+ # Check if matcher accepts file_content
459
+ import inspect
460
+ sig = inspect.signature(matcher)
461
+ if 'file_content' in sig.parameters:
462
+ fix = matcher(error, file_content)
463
+ else:
464
+ fix = matcher(error)
465
+
466
+ if fix:
467
+ # Fill in package manager if needed
468
+ if fix.command and "{package_manager}" in fix.command and repo_path:
469
+ pm = detect_package_manager(repo_path)
470
+ fix.command = fix.command.replace("{package_manager}", pm)
471
+
472
+ # Set file path if we have it
473
+ if file_path and not fix.file_path:
474
+ fix.file_path = str(file_path)
475
+
476
+ return fix
477
+
478
+ except Exception:
479
+ continue
480
+
481
+ return None
482
+
483
+
484
+ def apply_quick_fix(
485
+ fix: QuickFix,
486
+ repo_path: Path,
487
+ dry_run: bool = False,
488
+ ) -> tuple[bool, str]:
489
+ """Apply a quick fix.
490
+
491
+ Args:
492
+ fix: The fix to apply
493
+ repo_path: Repository root path
494
+ dry_run: If True, don't make changes
495
+
496
+ Returns:
497
+ Tuple of (success, message)
498
+ """
499
+ try:
500
+ if fix.fix_type == FixType.INSTALL_PACKAGE:
501
+ if not fix.command:
502
+ return False, "No install command specified"
503
+
504
+ if dry_run:
505
+ return True, f"Would run: {fix.command}"
506
+
507
+ import subprocess
508
+ result = subprocess.run(
509
+ fix.command.split(),
510
+ cwd=repo_path,
511
+ capture_output=True,
512
+ text=True,
513
+ timeout=120,
514
+ )
515
+
516
+ if result.returncode == 0:
517
+ return True, f"Installed package: {fix.command}"
518
+ else:
519
+ return False, f"Install failed: {result.stderr}"
520
+
521
+ elif fix.fix_type in {FixType.ADD_IMPORT, FixType.FIX_SYNTAX, FixType.FIX_INDENTATION}:
522
+ if not fix.file_path:
523
+ return False, "No file path specified"
524
+
525
+ file_path = repo_path / fix.file_path
526
+ if not file_path.exists():
527
+ return False, f"File not found: {fix.file_path}"
528
+
529
+ content = file_path.read_text()
530
+
531
+ if fix.old_content and fix.new_content:
532
+ # Replace content
533
+ if fix.old_content not in content:
534
+ return False, f"Content to replace not found in {fix.file_path}"
535
+
536
+ if dry_run:
537
+ return True, f"Would replace in {fix.file_path}"
538
+
539
+ new_content = content.replace(fix.old_content, fix.new_content, 1)
540
+ file_path.write_text(new_content)
541
+ return True, f"Fixed: {fix.description}"
542
+
543
+ elif fix.insert_line and fix.insert_content:
544
+ # Insert content at line
545
+ lines = content.split('\n')
546
+ insert_idx = max(0, fix.insert_line - 1)
547
+
548
+ if dry_run:
549
+ return True, f"Would insert at line {fix.insert_line} in {fix.file_path}"
550
+
551
+ lines.insert(insert_idx, fix.insert_content.rstrip('\n'))
552
+ file_path.write_text('\n'.join(lines))
553
+ return True, f"Inserted: {fix.description}"
554
+
555
+ return False, "Unknown fix type or missing parameters"
556
+
557
+ except Exception as e:
558
+ return False, f"Error applying fix: {e}"