misterdev 0.2.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 (136) hide show
  1. misterdev/__init__.py +3 -0
  2. misterdev/agent.py +2166 -0
  3. misterdev/agent_helpers.py +194 -0
  4. misterdev/analyzers/__init__.py +0 -0
  5. misterdev/analyzers/project_analyzer/__init__.py +246 -0
  6. misterdev/analyzers/project_analyzer/detection.py +146 -0
  7. misterdev/analyzers/project_analyzer/merge.py +137 -0
  8. misterdev/analyzers/project_analyzer/overview.py +312 -0
  9. misterdev/analyzers/project_analyzer/prompts.py +83 -0
  10. misterdev/cli.py +370 -0
  11. misterdev/config.py +521 -0
  12. misterdev/core/__init__.py +0 -0
  13. misterdev/core/audit.py +88 -0
  14. misterdev/core/config.py +10 -0
  15. misterdev/core/context/__init__.py +0 -0
  16. misterdev/core/context/change_tracker.py +218 -0
  17. misterdev/core/context/contracts/__init__.py +63 -0
  18. misterdev/core/context/contracts/_log.py +9 -0
  19. misterdev/core/context/contracts/_text.py +12 -0
  20. misterdev/core/context/contracts/extraction.py +30 -0
  21. misterdev/core/context/contracts/python_generic.py +42 -0
  22. misterdev/core/context/contracts/registry.py +127 -0
  23. misterdev/core/context/contracts/rust_line.py +225 -0
  24. misterdev/core/context/contracts/rust_tree_sitter.py +141 -0
  25. misterdev/core/context/lsp.py +174 -0
  26. misterdev/core/context/scratchpad.py +111 -0
  27. misterdev/core/context/topography/__init__.py +43 -0
  28. misterdev/core/context/topography/_log.py +10 -0
  29. misterdev/core/context/topography/cache.py +91 -0
  30. misterdev/core/context/topography/engine.py +172 -0
  31. misterdev/core/context/topography/graph.py +675 -0
  32. misterdev/core/context/topography/nodes.py +55 -0
  33. misterdev/core/context/topography/parsers.py +95 -0
  34. misterdev/core/context/topography/syntax.py +54 -0
  35. misterdev/core/economics/__init__.py +0 -0
  36. misterdev/core/economics/context_budget.py +175 -0
  37. misterdev/core/economics/embeddings.py +232 -0
  38. misterdev/core/economics/free_models.py +108 -0
  39. misterdev/core/economics/llm_cache.py +105 -0
  40. misterdev/core/economics/model_catalog.py +79 -0
  41. misterdev/core/economics/model_ledger.py +331 -0
  42. misterdev/core/economics/model_selector.py +281 -0
  43. misterdev/core/execution/__init__.py +0 -0
  44. misterdev/core/execution/bounded.py +50 -0
  45. misterdev/core/execution/container.py +221 -0
  46. misterdev/core/execution/error_classifier.py +366 -0
  47. misterdev/core/execution/error_resolver.py +201 -0
  48. misterdev/core/execution/governance.py +283 -0
  49. misterdev/core/execution/outcomes.py +50 -0
  50. misterdev/core/execution/progress.py +120 -0
  51. misterdev/core/execution/project.py +231 -0
  52. misterdev/core/execution/registry.py +97 -0
  53. misterdev/core/execution/runtime.py +279 -0
  54. misterdev/core/gitcmd.py +39 -0
  55. misterdev/core/integration/__init__.py +0 -0
  56. misterdev/core/integration/mcp.py +368 -0
  57. misterdev/core/integration/mcp_gather.py +186 -0
  58. misterdev/core/models.py +35 -0
  59. misterdev/core/modes.py +184 -0
  60. misterdev/core/planning/__init__.py +0 -0
  61. misterdev/core/planning/advisor.py +89 -0
  62. misterdev/core/planning/assessment.py +135 -0
  63. misterdev/core/planning/decomposer.py +387 -0
  64. misterdev/core/planning/metacognition.py +103 -0
  65. misterdev/core/planning/sovereign.py +308 -0
  66. misterdev/core/planning/targets.py +201 -0
  67. misterdev/core/reporting/__init__.py +0 -0
  68. misterdev/core/reporting/report.py +377 -0
  69. misterdev/core/reporting/report_view.py +151 -0
  70. misterdev/core/task.py +163 -0
  71. misterdev/core/verification/__init__.py +0 -0
  72. misterdev/core/verification/claim_verifier.py +210 -0
  73. misterdev/core/verification/critic.py +324 -0
  74. misterdev/core/verification/gatekeeper/__init__.py +631 -0
  75. misterdev/core/verification/gatekeeper/constants.py +138 -0
  76. misterdev/core/verification/gatekeeper/helpers.py +28 -0
  77. misterdev/core/verification/goal_check.py +219 -0
  78. misterdev/core/verification/independent.py +68 -0
  79. misterdev/core/verification/mutation_gate.py +221 -0
  80. misterdev/core/verification/preflight.py +95 -0
  81. misterdev/core/verification/spec_tests.py +175 -0
  82. misterdev/core/verification/validator.py +495 -0
  83. misterdev/core/verification/vision_verify.py +185 -0
  84. misterdev/core/verification/web_verify.py +408 -0
  85. misterdev/environments/__init__.py +0 -0
  86. misterdev/environments/base_env.py +18 -0
  87. misterdev/environments/container_env.py +87 -0
  88. misterdev/environments/venv_env.py +42 -0
  89. misterdev/llm/__init__.py +0 -0
  90. misterdev/llm/client/__init__.py +152 -0
  91. misterdev/llm/client/base.py +382 -0
  92. misterdev/llm/client/edits.py +70 -0
  93. misterdev/llm/client/embeddings.py +121 -0
  94. misterdev/llm/client/errors.py +134 -0
  95. misterdev/llm/client/providers.py +535 -0
  96. misterdev/llm/client/response.py +24 -0
  97. misterdev/llm/prompt_manager.py +82 -0
  98. misterdev/llm/responses/__init__.py +34 -0
  99. misterdev/llm/responses/apply.py +131 -0
  100. misterdev/llm/responses/json_extract.py +80 -0
  101. misterdev/llm/responses/models.py +43 -0
  102. misterdev/llm/responses/parsing.py +494 -0
  103. misterdev/logging_setup.py +20 -0
  104. misterdev/mcp_server.py +208 -0
  105. misterdev/nl_cli.py +149 -0
  106. misterdev/plugins.py +115 -0
  107. misterdev/py.typed +0 -0
  108. misterdev/task_executors/__init__.py +0 -0
  109. misterdev/task_executors/base_executor.py +10 -0
  110. misterdev/task_executors/markdown_plan_executor/__init__.py +90 -0
  111. misterdev/task_executors/markdown_plan_executor/commands_mixin.py +82 -0
  112. misterdev/task_executors/markdown_plan_executor/context_mixin.py +221 -0
  113. misterdev/task_executors/markdown_plan_executor/critic_spec_mixin.py +174 -0
  114. misterdev/task_executors/markdown_plan_executor/edits_mixin.py +251 -0
  115. misterdev/task_executors/markdown_plan_executor/execute_mixin.py +727 -0
  116. misterdev/task_executors/markdown_plan_executor/gates_mixin.py +203 -0
  117. misterdev/task_executors/markdown_plan_executor/git_mixin.py +219 -0
  118. misterdev/task_executors/markdown_plan_executor/helpers.py +521 -0
  119. misterdev/task_executors/markdown_plan_executor/llm_mixin.py +238 -0
  120. misterdev/task_executors/markdown_plan_executor/results_mixin.py +23 -0
  121. misterdev/tools/__init__.py +19 -0
  122. misterdev/tools/base_tool.py +14 -0
  123. misterdev/tools/command.py +75 -0
  124. misterdev/tools/file_io.py +69 -0
  125. misterdev/tools/formatter.py +26 -0
  126. misterdev/tools/git_tool.py +90 -0
  127. misterdev/utils/__init__.py +0 -0
  128. misterdev/utils/file_utils.py +169 -0
  129. misterdev/utils/process.py +23 -0
  130. misterdev-0.2.0.dist-info/METADATA +326 -0
  131. misterdev-0.2.0.dist-info/RECORD +136 -0
  132. misterdev-0.2.0.dist-info/WHEEL +5 -0
  133. misterdev-0.2.0.dist-info/entry_points.txt +3 -0
  134. misterdev-0.2.0.dist-info/licenses/COMMERCIAL_LICENSE.md +34 -0
  135. misterdev-0.2.0.dist-info/licenses/LICENSE +661 -0
  136. misterdev-0.2.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,366 @@
1
+ """Build and test error classification.
2
+
3
+ Categorizes raw compiler/test output into actionable error types so the
4
+ LLM gets targeted fix guidance instead of raw error dumps.
5
+
6
+ Categories:
7
+ syntax - Parse/syntax errors (fix the malformed code)
8
+ missing_import - Unresolved import/use (add the import or dependency)
9
+ wrong_type - Type mismatch, wrong argument count (fix signature or call site)
10
+ missing_export - Symbol exists but isn't pub/exported (add visibility modifier)
11
+ missing_symbol - Function/struct/field doesn't exist yet (create it or fix the name)
12
+ test_assertion - Test assertion failed (fix logic, not syntax)
13
+ link_error - Linker/unresolved external (fix Cargo.toml or feature flags)
14
+ unknown - Unclassifiable
15
+ """
16
+
17
+ from typing import Dict, List, Tuple
18
+
19
+ from misterdev.logging_setup import setup_logger
20
+
21
+ logger = setup_logger(__name__)
22
+
23
+
24
+ class ErrorCategory:
25
+ SYNTAX = "syntax"
26
+ MISSING_IMPORT = "missing_import"
27
+ WRONG_TYPE = "wrong_type"
28
+ MISSING_EXPORT = "missing_export"
29
+ MISSING_SYMBOL = "missing_symbol"
30
+ TEST_ASSERTION = "test_assertion"
31
+ LINK_ERROR = "link_error"
32
+ MANIFEST = "manifest_error"
33
+ FILE_NOT_FOUND = "file_not_found"
34
+ UNKNOWN = "unknown"
35
+
36
+
37
+ # (category, indicator_strings)
38
+ _INDICATORS: List[Tuple[str, List[str]]] = [
39
+ (
40
+ ErrorCategory.SYNTAX,
41
+ [
42
+ "syntax error",
43
+ "SyntaxError",
44
+ "unexpected token",
45
+ "unexpected eof",
46
+ "unterminated",
47
+ "invalid syntax",
48
+ "expected one of",
49
+ "expected `,`",
50
+ "expected `{`",
51
+ "expected `;`",
52
+ # clang/gcc/swiftc
53
+ "expected ';'",
54
+ "expected expression",
55
+ "expected '}'",
56
+ "expected declaration",
57
+ "extraneous",
58
+ "expected identifier",
59
+ ],
60
+ ),
61
+ (
62
+ ErrorCategory.MISSING_IMPORT,
63
+ [
64
+ "unresolved import",
65
+ "ModuleNotFoundError",
66
+ "No module named",
67
+ "could not find crate",
68
+ "use of undeclared crate",
69
+ "cannot find module",
70
+ "ImportError",
71
+ # swift / C / C++
72
+ "no such module",
73
+ "cannot find interface declaration",
74
+ ],
75
+ ),
76
+ (
77
+ ErrorCategory.MISSING_EXPORT,
78
+ [
79
+ "is private",
80
+ "not accessible",
81
+ "module is private",
82
+ "function is private",
83
+ "struct is private",
84
+ "pub(crate)",
85
+ "inaccessible",
86
+ # swift / C++ / C#
87
+ "is inaccessible due to",
88
+ "marked private",
89
+ "not visible",
90
+ "inaccessible due to its protection level",
91
+ ],
92
+ ),
93
+ (
94
+ ErrorCategory.WRONG_TYPE,
95
+ [
96
+ "type mismatch",
97
+ "expected type",
98
+ "mismatched types",
99
+ "TypeError",
100
+ "incompatible type",
101
+ "cannot convert",
102
+ "expected struct",
103
+ "expected enum",
104
+ "wrong number of",
105
+ "arguments were supplied",
106
+ "the trait bound",
107
+ "is not satisfied",
108
+ # swift / clang
109
+ "cannot convert value of type",
110
+ "cannot initialize",
111
+ "no viable conversion",
112
+ "no matching function for call",
113
+ "argument of type",
114
+ "incompatible pointer",
115
+ # C# / roslyn
116
+ "cannot implicitly convert type",
117
+ "cannot convert from",
118
+ ],
119
+ ),
120
+ (
121
+ ErrorCategory.MISSING_SYMBOL,
122
+ [
123
+ "not found in this scope",
124
+ "cannot find value",
125
+ "cannot find function",
126
+ "cannot find type",
127
+ "no field",
128
+ "no method named",
129
+ "no variant",
130
+ "NameError",
131
+ "AttributeError",
132
+ "has no member",
133
+ "unknown field",
134
+ # swift / clang
135
+ "use of undeclared identifier",
136
+ "use of unresolved identifier",
137
+ "no member named",
138
+ "has no member named",
139
+ # C# / roslyn
140
+ "does not contain a definition for",
141
+ "does not exist in the current context",
142
+ ],
143
+ ),
144
+ (
145
+ ErrorCategory.TEST_ASSERTION,
146
+ [
147
+ "assertion failed",
148
+ "AssertionError",
149
+ "assert_eq",
150
+ "assert_ne",
151
+ "panicked at",
152
+ "test result: FAILED",
153
+ "left:",
154
+ "right:",
155
+ # XCTest / ctest
156
+ "XCTAssert",
157
+ "XCTFail",
158
+ "failed (",
159
+ "tests failed out of",
160
+ ],
161
+ ),
162
+ (
163
+ ErrorCategory.LINK_ERROR,
164
+ [
165
+ "linker",
166
+ "undefined reference",
167
+ "unresolved external",
168
+ "link error",
169
+ "multiple definition",
170
+ "duplicate symbol",
171
+ # apple ld / clang
172
+ "undefined symbols for architecture",
173
+ "symbol(s) not found",
174
+ "ld: ",
175
+ "linker command failed",
176
+ ],
177
+ ),
178
+ (
179
+ ErrorCategory.MANIFEST,
180
+ [
181
+ "failed to parse manifest",
182
+ "could not find `cargo.toml`",
183
+ "missing either a `[package]`",
184
+ "virtual manifest",
185
+ "invalid toml",
186
+ "expected value",
187
+ "duplicate key",
188
+ "error parsing pyproject.toml",
189
+ "invalid package.json",
190
+ # swiftpm / meson / cmake
191
+ "could not find package.swift",
192
+ "manifest parse error",
193
+ "neither directory contains a build file",
194
+ "cmake error",
195
+ "does not appear to contain cmakelists.txt",
196
+ ],
197
+ ),
198
+ (
199
+ ErrorCategory.FILE_NOT_FOUND,
200
+ [
201
+ "no such file or directory",
202
+ "file not found",
203
+ "cannot open",
204
+ "enoent",
205
+ ],
206
+ ),
207
+ ]
208
+
209
+ # Fix guidance per category
210
+ FIX_GUIDANCE = {
211
+ ErrorCategory.SYNTAX: (
212
+ "This is a SYNTAX error. The code is malformed. "
213
+ "Fix the specific line indicated. Do not restructure; just correct the syntax."
214
+ ),
215
+ ErrorCategory.MISSING_IMPORT: (
216
+ "This is a MISSING IMPORT error. A module, crate, or package is referenced but not imported. "
217
+ "Add the correct 'use' statement or dependency. Check Cargo.toml or import statements."
218
+ ),
219
+ ErrorCategory.MISSING_EXPORT: (
220
+ "This is a VISIBILITY error. The symbol exists but is not public. "
221
+ "Add 'pub' to the declaration, or change the import path to use a re-export."
222
+ ),
223
+ ErrorCategory.WRONG_TYPE: (
224
+ "This is a TYPE MISMATCH error. The function signature or argument types don't match the call site. "
225
+ "Check the interface contracts above. Match the exact types expected."
226
+ ),
227
+ ErrorCategory.MISSING_SYMBOL: (
228
+ "This is a MISSING SYMBOL error. A function, struct, field, or method is referenced but doesn't exist. "
229
+ "Either create it, fix the spelling, or check which module it should come from."
230
+ ),
231
+ ErrorCategory.TEST_ASSERTION: (
232
+ "This is a TEST ASSERTION failure. The code compiles and runs but produces wrong results. "
233
+ "Focus on the LOGIC, not the syntax. Check the algorithm, boundary conditions, and data flow."
234
+ ),
235
+ ErrorCategory.LINK_ERROR: (
236
+ "This is a LINKER error. Check that all referenced symbols are defined and that the "
237
+ "needed libraries/dependencies and feature flags are declared in the build manifest "
238
+ "(Cargo.toml, CMakeLists.txt, package config) and linked."
239
+ ),
240
+ ErrorCategory.MANIFEST: (
241
+ "This is a MANIFEST/CONFIG error. The project manifest (Cargo.toml, pyproject.toml, "
242
+ "package.json) is malformed or missing required sections. Ensure required sections exist "
243
+ "(e.g. [package] with name and version for Cargo.toml) and that the file is valid TOML/JSON."
244
+ ),
245
+ ErrorCategory.FILE_NOT_FOUND: (
246
+ "This is a FILE NOT FOUND error. A referenced file or path does not exist. "
247
+ "Create the missing file or correct the path; check for typos and relative-path assumptions."
248
+ ),
249
+ ErrorCategory.UNKNOWN: (
250
+ "Error type could not be classified. Read the full error output carefully and fix the root cause."
251
+ ),
252
+ }
253
+
254
+
255
+ # Rust compiler error codes -> category (authoritative, no fuzzy matching needed)
256
+ _RUST_ERROR_CODES: Dict[str, str] = {
257
+ "E0061": ErrorCategory.WRONG_TYPE, # wrong number of arguments
258
+ "E0106": ErrorCategory.SYNTAX, # missing lifetime specifier
259
+ "E0277": ErrorCategory.WRONG_TYPE, # trait bound not satisfied
260
+ "E0308": ErrorCategory.WRONG_TYPE, # mismatched types
261
+ "E0369": ErrorCategory.WRONG_TYPE, # binary operation not supported
262
+ "E0382": ErrorCategory.WRONG_TYPE, # use of moved value
263
+ "E0412": ErrorCategory.MISSING_SYMBOL, # cannot find type
264
+ "E0422": ErrorCategory.MISSING_SYMBOL, # cannot find struct/variant
265
+ "E0425": ErrorCategory.MISSING_SYMBOL, # cannot find value/function
266
+ "E0432": ErrorCategory.MISSING_IMPORT, # unresolved import
267
+ "E0433": ErrorCategory.MISSING_IMPORT, # failed to resolve: use of undeclared crate
268
+ "E0603": ErrorCategory.MISSING_EXPORT, # private item
269
+ "E0614": ErrorCategory.WRONG_TYPE, # cannot dereference
270
+ "E0615": ErrorCategory.MISSING_SYMBOL, # attempted to take value of method
271
+ "E0624": ErrorCategory.MISSING_EXPORT, # associated item is private
272
+ }
273
+
274
+
275
+ # C# / Roslyn compiler error codes -> category (authoritative)
276
+ _CSHARP_ERROR_CODES: Dict[str, str] = {
277
+ "CS0103": ErrorCategory.MISSING_SYMBOL, # name does not exist in context
278
+ "CS0117": ErrorCategory.MISSING_SYMBOL, # type has no definition for member
279
+ "CS1061": ErrorCategory.MISSING_SYMBOL, # no definition / extension method
280
+ "CS0246": ErrorCategory.MISSING_IMPORT, # type/namespace not found (using?)
281
+ "CS0234": ErrorCategory.MISSING_IMPORT, # namespace member does not exist
282
+ "CS0029": ErrorCategory.WRONG_TYPE, # cannot implicitly convert
283
+ "CS1503": ErrorCategory.WRONG_TYPE, # argument cannot convert from
284
+ "CS0019": ErrorCategory.WRONG_TYPE, # operator cannot be applied
285
+ "CS1002": ErrorCategory.SYNTAX, # ; expected
286
+ "CS1513": ErrorCategory.SYNTAX, # } expected
287
+ "CS1519": ErrorCategory.SYNTAX, # invalid token
288
+ "CS0122": ErrorCategory.MISSING_EXPORT, # inaccessible protection level
289
+ }
290
+
291
+
292
+ # Tie-break priority (lower rank = wins a tie). Categories that block the build
293
+ # outright and are more fundamental come first: a syntax/manifest failure that
294
+ # also trips a downstream type/symbol indicator is the syntax/manifest error.
295
+ # Mirrors the order categories are declared in ``_INDICATORS``.
296
+ _TIE_BREAK_ORDER: List[str] = [cat for cat, _ in _INDICATORS]
297
+
298
+
299
+ def _tie_break_rank(category: str) -> int:
300
+ try:
301
+ return _TIE_BREAK_ORDER.index(category)
302
+ except ValueError:
303
+ return len(_TIE_BREAK_ORDER)
304
+
305
+
306
+ def classify_error(error_output: str) -> str:
307
+ """Classify build/test error output into a category.
308
+
309
+ Checks structured compiler error codes first (Rust, then C#/Roslyn) for an
310
+ exact, authoritative match, then falls back to keyword scoring for other
311
+ languages and test output.
312
+ """
313
+ # Fast path: Rust structured error codes
314
+ for code, category in _RUST_ERROR_CODES.items():
315
+ if f"error[{code}]" in error_output:
316
+ return category
317
+ # Fast path: C#/Roslyn error codes (e.g. "error CS0103:")
318
+ for code, category in _CSHARP_ERROR_CODES.items():
319
+ if f"{code}:" in error_output:
320
+ return category
321
+
322
+ lower = error_output.lower()
323
+
324
+ # Score each category by number of matching indicators
325
+ scores: Dict[str, int] = {}
326
+ for category, indicators in _INDICATORS:
327
+ score = sum(1 for ind in indicators if ind.lower() in lower)
328
+ if score > 0:
329
+ scores[category] = score
330
+
331
+ if not scores:
332
+ return ErrorCategory.UNKNOWN
333
+
334
+ # Highest indicator count wins; ties resolve by an explicit priority (more
335
+ # fundamental, build-blocking categories first) rather than relying on the
336
+ # implicit dict-insertion order, so the result is stable across refactors.
337
+ return max(scores, key=lambda c: (scores[c], -_tie_break_rank(c)))
338
+
339
+
340
+ def classify_and_guide(error_output: str) -> Tuple[str, str]:
341
+ """Classify an error and return (category, fix_guidance)."""
342
+ category = classify_error(error_output)
343
+ guidance = FIX_GUIDANCE.get(category, FIX_GUIDANCE[ErrorCategory.UNKNOWN])
344
+ logger.info(f"Error classified as: {category}")
345
+ return category, guidance
346
+
347
+
348
+ def format_classified_error(error_output: str, max_lines: int = 50) -> str:
349
+ """Classify error and format with guidance for LLM prompt."""
350
+ category, guidance = classify_and_guide(error_output)
351
+
352
+ # Truncate raw output
353
+ lines = error_output.splitlines()
354
+ if len(lines) > max_lines:
355
+ truncated = (
356
+ "\n".join(lines[:max_lines])
357
+ + f"\n... ({len(lines) - max_lines} more lines)"
358
+ )
359
+ else:
360
+ truncated = error_output
361
+
362
+ return (
363
+ f"### Error Classification: {category.upper()}\n"
364
+ f"**Fix Strategy**: {guidance}\n\n"
365
+ f"### Raw Error Output\n```\n{truncated}\n```"
366
+ )
@@ -0,0 +1,201 @@
1
+ """Error attribution and resolution for build/test failures.
2
+
3
+ Parses compiler and test-runner output to locate the source files and
4
+ line numbers responsible for each error, then formats that information
5
+ for injection into LLM prompts.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import re
11
+ from pathlib import Path
12
+ from typing import Any, List, Optional
13
+
14
+ from misterdev.logging_setup import setup_logger
15
+
16
+ logger = setup_logger(__name__)
17
+
18
+ # Patterns that match common error location formats:
19
+ # file.py:42: error message
20
+ # file.py:42:10: error message
21
+ # File "file.py", line 42
22
+ # --> file.py:42:10
23
+ _LOCATION_PATTERNS: list[re.Pattern] = [
24
+ re.compile(
25
+ r"^(?P<file>[^\s:]+\.(?:py|js|ts|rs|go|java|c|cpp|h|rb|cs))"
26
+ r":(?P<line>\d+)(?::\d+)?[:\s]",
27
+ re.MULTILINE,
28
+ ),
29
+ re.compile(r'File "(?P<file>[^"]+)", line (?P<line>\d+)', re.MULTILINE),
30
+ re.compile(r"-->\s*(?P<file>[^\s:]+):(?P<line>\d+):\d+", re.MULTILINE),
31
+ re.compile(r"at (?P<file>[^\s(]+)\(.*:(?P<line>\d+)\)", re.MULTILINE),
32
+ ]
33
+
34
+
35
+ class ErrorLocation:
36
+ """A single attributed error location."""
37
+
38
+ def __init__(
39
+ self,
40
+ file: str,
41
+ line: int,
42
+ snippet: str = "",
43
+ symbol: Optional[str] = None,
44
+ symbol_key: Optional[str] = None,
45
+ ):
46
+ self.file = file
47
+ self.line = line
48
+ self.snippet = snippet
49
+ self.symbol = symbol
50
+ # Unique graph key (``file_path:name``) of the attributed symbol, so
51
+ # caller lookup never conflates same-named symbols in other files.
52
+ self.symbol_key = symbol_key
53
+
54
+ def __repr__(self) -> str: # pragma: no cover
55
+ return f"ErrorLocation({self.file}:{self.line})"
56
+
57
+
58
+ class ErrorResolver:
59
+ """Resolves build/test error output to source locations.
60
+
61
+ Parameters
62
+ ----------
63
+ project_path:
64
+ Root directory of the project (used to read source snippets).
65
+ dependency_graph:
66
+ Optional symbol graph (a ``SymbolGraph`` with a ``symbols`` mapping).
67
+ When supplied, each error location is attributed to its enclosing symbol
68
+ and its callers are surfaced. Pass ``None`` when unavailable; attribution
69
+ then degrades to file:line + snippet only.
70
+ """
71
+
72
+ def __init__(self, project_path: Path, dependency_graph: Optional[Any] = None):
73
+ self.project_path = project_path
74
+ self.graph = dependency_graph
75
+
76
+ # ------------------------------------------------------------------
77
+ # Public API
78
+ # ------------------------------------------------------------------
79
+
80
+ def resolve_errors(self, error_output: str) -> List[ErrorLocation]:
81
+ """Parse *error_output* and return attributed :class:`ErrorLocation` objects."""
82
+ locations: List[ErrorLocation] = []
83
+ seen: set[tuple[str, int]] = set()
84
+
85
+ for pattern in _LOCATION_PATTERNS:
86
+ for match in pattern.finditer(error_output):
87
+ file_str = match.group("file")
88
+ try:
89
+ line_no = int(match.group("line"))
90
+ except (ValueError, IndexError):
91
+ continue
92
+
93
+ key = (file_str, line_no)
94
+ if key in seen:
95
+ continue
96
+ seen.add(key)
97
+
98
+ snippet = self._read_snippet(file_str, line_no)
99
+ symbol_key = self._symbol_key_at(file_str, line_no)
100
+ locations.append(
101
+ ErrorLocation(
102
+ file=file_str,
103
+ line=line_no,
104
+ snippet=snippet,
105
+ symbol=self._symbol_name(symbol_key),
106
+ symbol_key=symbol_key,
107
+ )
108
+ )
109
+
110
+ if not locations:
111
+ logger.debug(
112
+ "ErrorResolver: no structured locations found in error output."
113
+ )
114
+
115
+ return locations
116
+
117
+ def format_for_llm(self, locations: List[ErrorLocation]) -> str:
118
+ """Format attributed locations as a prompt-ready string."""
119
+ if not locations:
120
+ return ""
121
+
122
+ lines = ["## Error Attribution"]
123
+ for loc in locations[:10]: # cap to avoid bloating context
124
+ lines.append(f"\n### {loc.file}:{loc.line}")
125
+ if loc.symbol:
126
+ lines.append(f"- Symbol: `{loc.symbol}`")
127
+ callers = self._callers(loc.symbol_key)
128
+ if callers:
129
+ lines.append(f"- Called by: {', '.join(callers)}")
130
+ if loc.snippet:
131
+ lines.append(f"```\n{loc.snippet}\n```")
132
+
133
+ return "\n".join(lines)
134
+
135
+ # ------------------------------------------------------------------
136
+ # Internal helpers
137
+ # ------------------------------------------------------------------
138
+
139
+ def _read_snippet(self, file_str: str, line_no: int, context: int = 5) -> str:
140
+ """Return a few lines of source around *line_no* for context."""
141
+ # Try relative to project root first, then as absolute path.
142
+ candidates = [
143
+ self.project_path / file_str,
144
+ Path(file_str),
145
+ ]
146
+ for path in candidates:
147
+ if path.exists():
148
+ try:
149
+ source_lines = path.read_text(
150
+ encoding="utf-8", errors="replace"
151
+ ).splitlines()
152
+ start = max(0, line_no - context - 1)
153
+ end = min(len(source_lines), line_no + context)
154
+ numbered = [
155
+ f"{'>' if i + 1 == line_no else ' '} {i + 1:4d} | {source_lines[i]}"
156
+ for i in range(start, end)
157
+ ]
158
+ return "\n".join(numbered)
159
+ except OSError:
160
+ return ""
161
+ return ""
162
+
163
+ def _rel_file(self, file_str: str) -> Optional[str]:
164
+ """Project-relative form of an error path for symbol-graph lookup, or
165
+ None when it lies outside the project."""
166
+ try:
167
+ p = Path(file_str)
168
+ if p.is_absolute():
169
+ if p.is_relative_to(self.project_path):
170
+ return str(p.relative_to(self.project_path))
171
+ return None
172
+ return file_str
173
+ except (ValueError, OSError):
174
+ return None
175
+
176
+ def _symbol_key_at(self, file_str: str, line_no: int) -> Optional[str]:
177
+ """Graph key of the symbol enclosing the error line (the 0-vs-1-indexed
178
+ and sub-target path handling lives on ``SymbolGraph``). None when there
179
+ is no usable graph or the path is outside the project."""
180
+ at = getattr(self.graph, "symbol_at_line", None)
181
+ if at is None:
182
+ return None
183
+ rel = self._rel_file(file_str)
184
+ if rel is None:
185
+ return None
186
+ return at(rel, line_no)
187
+
188
+ def _symbol_name(self, key: Optional[str]) -> Optional[str]:
189
+ """Display name for an attributed symbol key, or None."""
190
+ symbols = getattr(self.graph, "symbols", None)
191
+ if not key or not symbols:
192
+ return None
193
+ node = symbols.get(key)
194
+ return node.name if node else None
195
+
196
+ def _callers(self, key: Optional[str]) -> List[str]:
197
+ """Names of symbols that call the attributed symbol (by unique key)."""
198
+ of = getattr(self.graph, "callers_of", None)
199
+ if of is None or not key:
200
+ return []
201
+ return of(key)