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,675 @@
1
+ """The scope-aware tree-sitter symbol graph."""
2
+
3
+ import os
4
+ import re
5
+ from pathlib import Path
6
+ from typing import Dict, List, Optional, Set, Any
7
+
8
+ from misterdev.utils.file_utils import read_file, is_golden_path
9
+
10
+ from ._log import logger
11
+ from .nodes import SymbolNode
12
+ from .parsers import _get_ts_parsers, _EXT_TO_LANG, _node_text
13
+ from .cache import _TopographyCache
14
+
15
+ # Whole-identifier call detection: an identifier directly followed by "(" and
16
+ # not preceded by an identifier char, so "reparse(" yields "reparse" — never a
17
+ # spurious "parse" — unlike the old `f"{name}(" in content` substring test.
18
+ _CALL_PATTERN = re.compile(r"(?<![A-Za-z0-9_])([A-Za-z_]\w*)\s*\(")
19
+
20
+
21
+ class SymbolGraph:
22
+ """Robust dependency graph with scope-aware parsing."""
23
+
24
+ def __init__(self, project_path: Path, golden_paths=None):
25
+ self.project_path = project_path
26
+ self.symbols: Dict[str, SymbolNode] = {}
27
+ self.parsers = _get_ts_parsers()
28
+ self.golden_paths = golden_paths or []
29
+ self.cache_path = Path(project_path) / ".orchestrator" / "topography_cache.json"
30
+
31
+ def build(self):
32
+ logger.info(f"Building symbol graph for {self.project_path}")
33
+ self.symbols.clear()
34
+
35
+ if not self.parsers:
36
+ logger.info("Tree-sitter not available; symbol graph disabled")
37
+ return
38
+
39
+ # Skip dependency/build-output dirs across toolchains so the symbol map
40
+ # reflects real source, not generated artifacts (e.g. Swift's `.build`
41
+ # derived sources and headers, Xcode `DerivedData`, CocoaPods, wasm-pack
42
+ # `pkg`). Without this a frontend task gets generated junk as context.
43
+ _skip = frozenset(
44
+ (
45
+ ".venv",
46
+ ".git",
47
+ "__pycache__",
48
+ "node_modules",
49
+ "target",
50
+ "build",
51
+ ".build",
52
+ "dist",
53
+ "pkg",
54
+ "Pods",
55
+ "DerivedData",
56
+ ".gradle",
57
+ ".next",
58
+ "vendor",
59
+ )
60
+ )
61
+ supported_exts = {
62
+ ext for ext, lang in _EXT_TO_LANG.items() if lang in self.parsers
63
+ }
64
+
65
+ # Walk with in-place directory pruning so we never DESCEND into skipped
66
+ # or hidden dirs (node_modules, .git, .claude, …). rglob would walk all
67
+ # of node_modules before filtering — slow on a real frontend repo — and a
68
+ # large dot-dir like .claude could otherwise crowd real source out of the
69
+ # outline's file cap, leaving the decomposer with no code to ground on.
70
+ source_files: List[Path] = []
71
+ for dirpath, dirnames, filenames in os.walk(self.project_path):
72
+ dirnames[:] = [
73
+ d for d in dirnames if d not in _skip and not d.startswith(".")
74
+ ]
75
+ for fn in filenames:
76
+ f = Path(dirpath) / fn
77
+ if f.suffix not in supported_exts:
78
+ continue
79
+ if is_golden_path(
80
+ str(f.relative_to(self.project_path)), self.golden_paths
81
+ ):
82
+ continue
83
+ source_files.append(f)
84
+
85
+ if not source_files:
86
+ logger.info("No supported source files found; symbol graph will be empty")
87
+ return
88
+
89
+ cache = _TopographyCache(self.cache_path)
90
+ cache.load()
91
+ live_paths: Set[str] = set()
92
+
93
+ for src_file in source_files:
94
+ lang = _EXT_TO_LANG.get(src_file.suffix)
95
+ if not (lang and lang in self.parsers):
96
+ continue
97
+ rel_path = str(src_file.relative_to(self.project_path))
98
+ live_paths.add(rel_path)
99
+
100
+ # Content-hash key: an edit changes the bytes and so the key, which
101
+ # is the cache-invalidation signal (mtime is never trusted).
102
+ content_bytes = self._read_bytes(src_file)
103
+ key = (
104
+ _TopographyCache.make_key(content_bytes, lang)
105
+ if content_bytes is not None
106
+ else None
107
+ )
108
+
109
+ cached = cache.get(rel_path, key) if key is not None else None
110
+ if cached is not None:
111
+ for sym in cached:
112
+ self.symbols[f"{sym.file_path}:{sym.name}"] = sym
113
+ continue
114
+
115
+ file_symbols = self._parse_file(src_file, lang, content_bytes=content_bytes)
116
+ for sym in file_symbols:
117
+ self.symbols[f"{sym.file_path}:{sym.name}"] = sym
118
+ # Only cache a clean parse: a read failure yields no key, so we never
119
+ # persist a wrong (empty) result under a real file's slot.
120
+ if key is not None:
121
+ cache.put(rel_path, key, file_symbols)
122
+
123
+ cache.prune(live_paths)
124
+ cache.save()
125
+
126
+ self._resolve_references()
127
+ logger.info(f"Symbol graph complete: {len(self.symbols)} symbols.")
128
+
129
+ def _read_bytes(self, file_path: Path) -> Optional[bytes]:
130
+ """Read a file's UTF-8 bytes, or None on any I/O/decode error.
131
+
132
+ A None result forces a (failed) parse path with no caching, so a
133
+ transiently unreadable file never poisons the cache with empty symbols.
134
+ """
135
+ try:
136
+ return read_file(file_path).encode("utf-8")
137
+ except (OSError, UnicodeError) as e:
138
+ logger.debug(f"Topography could not read {file_path}: {e}")
139
+ return None
140
+
141
+ def _parse_file(
142
+ self, file_path: Path, lang: str, content_bytes: Optional[bytes] = None
143
+ ) -> List[SymbolNode]:
144
+ # tree-sitter reports BYTE offsets; slice the UTF-8 bytes (not the str)
145
+ # so non-ASCII content before a symbol doesn't shift and mangle names.
146
+ content = (
147
+ content_bytes if content_bytes is not None else self._read_bytes(file_path)
148
+ )
149
+ if content is None:
150
+ return []
151
+ parser = self.parsers[lang]
152
+ tree = parser.parse(content)
153
+ rel_path = str(file_path.relative_to(self.project_path))
154
+
155
+ # Parse into a private dict so the symbols for THIS file are isolated
156
+ # (for caching) without disturbing the graph-wide self.symbols.
157
+ prev_symbols = self.symbols
158
+ self.symbols = {}
159
+ try:
160
+ self._dispatch_traverse(tree.root_node, content, rel_path, lang)
161
+ parsed = list(self.symbols.values())
162
+ finally:
163
+ self.symbols = prev_symbols
164
+ return parsed
165
+
166
+ def _dispatch_traverse(self, root: Any, content: bytes, rel_path: str, lang: str):
167
+ if lang == "rust":
168
+ self._traverse_rust(root, content, rel_path)
169
+ elif lang in ("typescript", "tsx", "javascript"):
170
+ self._traverse_typescript(root, content, rel_path)
171
+ elif lang == "kotlin":
172
+ self._traverse_kotlin(root, content, rel_path)
173
+ elif lang in ("c", "cpp"):
174
+ self._traverse_clike(root, content, rel_path)
175
+ elif lang == "swift":
176
+ self._traverse_swift(root, content, rel_path)
177
+ elif lang == "csharp":
178
+ self._traverse_csharp(root, content, rel_path)
179
+ else:
180
+ self._traverse_python(root, content, rel_path)
181
+
182
+ def _traverse_python(
183
+ self,
184
+ node: Any,
185
+ content: str,
186
+ file_path: str,
187
+ parent_class: Optional[str] = None,
188
+ ):
189
+ if node.type == "function_definition":
190
+ name_node = node.child_by_field_name("name")
191
+ if name_node:
192
+ name = _node_text(content, name_node)
193
+ full_name = f"{parent_class}.{name}" if parent_class else name
194
+ self._add_symbol(
195
+ full_name,
196
+ file_path,
197
+ "method" if parent_class else "function",
198
+ node,
199
+ content,
200
+ )
201
+
202
+ elif node.type == "class_definition":
203
+ name_node = node.child_by_field_name("name")
204
+ if name_node:
205
+ name = _node_text(content, name_node)
206
+ self._add_symbol(name, file_path, "class", node, content)
207
+ body_node = node.child_by_field_name("body")
208
+ if body_node:
209
+ for child in body_node.children:
210
+ self._traverse_python(
211
+ child, content, file_path, parent_class=name
212
+ )
213
+ return
214
+
215
+ for child in node.children:
216
+ self._traverse_python(child, content, file_path, parent_class)
217
+
218
+ def _traverse_rust(
219
+ self, node: Any, content: str, file_path: str, parent: Optional[str] = None
220
+ ):
221
+ if node.type == "function_item":
222
+ name_node = node.child_by_field_name("name")
223
+ if name_node:
224
+ name = _node_text(content, name_node)
225
+ full_name = f"{parent}::{name}" if parent else name
226
+ kind = "method" if parent else "function"
227
+ self._add_symbol(full_name, file_path, kind, node, content)
228
+
229
+ elif node.type == "struct_item":
230
+ name_node = node.child_by_field_name("name")
231
+ if name_node:
232
+ name = _node_text(content, name_node)
233
+ self._add_symbol(name, file_path, "struct", node, content)
234
+
235
+ elif node.type == "enum_item":
236
+ name_node = node.child_by_field_name("name")
237
+ if name_node:
238
+ name = _node_text(content, name_node)
239
+ self._add_symbol(name, file_path, "enum", node, content)
240
+
241
+ elif node.type == "trait_item":
242
+ name_node = node.child_by_field_name("name")
243
+ if name_node:
244
+ name = _node_text(content, name_node)
245
+ self._add_symbol(name, file_path, "trait", node, content)
246
+
247
+ elif node.type == "impl_item":
248
+ type_node = node.child_by_field_name("type")
249
+ if type_node:
250
+ impl_name = _node_text(content, type_node)
251
+ body_node = node.child_by_field_name("body")
252
+ if body_node:
253
+ for child in body_node.children:
254
+ self._traverse_rust(child, content, file_path, parent=impl_name)
255
+ return
256
+
257
+ for child in node.children:
258
+ self._traverse_rust(child, content, file_path, parent)
259
+
260
+ def _traverse_typescript(
261
+ self,
262
+ node: Any,
263
+ content: str,
264
+ file_path: str,
265
+ parent_class: Optional[str] = None,
266
+ ):
267
+ t = node.type
268
+ if t == "function_declaration":
269
+ name_node = node.child_by_field_name("name")
270
+ if name_node:
271
+ name = _node_text(content, name_node)
272
+ full = f"{parent_class}.{name}" if parent_class else name
273
+ self._add_symbol(
274
+ full,
275
+ file_path,
276
+ "method" if parent_class else "function",
277
+ node,
278
+ content,
279
+ )
280
+
281
+ elif t in ("class_declaration", "interface_declaration"):
282
+ name_node = node.child_by_field_name("name")
283
+ if name_node:
284
+ name = _node_text(content, name_node)
285
+ kind = "interface" if t == "interface_declaration" else "class"
286
+ self._add_symbol(name, file_path, kind, node, content)
287
+ body_node = node.child_by_field_name("body")
288
+ if body_node:
289
+ for child in body_node.children:
290
+ self._traverse_typescript(
291
+ child, content, file_path, parent_class=name
292
+ )
293
+ return
294
+
295
+ elif t == "enum_declaration":
296
+ name_node = node.child_by_field_name("name")
297
+ if name_node:
298
+ self._add_symbol(
299
+ _node_text(content, name_node), file_path, "enum", node, content
300
+ )
301
+
302
+ elif t in ("lexical_declaration", "variable_declaration"):
303
+ # const/let X = () => {...} or function(){...}: how React components
304
+ # and most modern TS/JS functions are written. Capture the binding.
305
+ for decl in node.children:
306
+ if decl.type != "variable_declarator":
307
+ continue
308
+ value = decl.child_by_field_name("value")
309
+ name_node = decl.child_by_field_name("name")
310
+ if (
311
+ name_node
312
+ and value is not None
313
+ and value.type in ("arrow_function", "function_expression")
314
+ ):
315
+ name = _node_text(content, name_node)
316
+ full = f"{parent_class}.{name}" if parent_class else name
317
+ self._add_symbol(
318
+ full,
319
+ file_path,
320
+ "method" if parent_class else "function",
321
+ node,
322
+ content,
323
+ )
324
+
325
+ elif t == "method_definition":
326
+ name_node = node.child_by_field_name("name")
327
+ if name_node and parent_class:
328
+ name = _node_text(content, name_node)
329
+ self._add_symbol(
330
+ f"{parent_class}.{name}", file_path, "method", node, content
331
+ )
332
+
333
+ elif t == "type_alias_declaration":
334
+ name_node = node.child_by_field_name("name")
335
+ if name_node:
336
+ name = _node_text(content, name_node)
337
+ self._add_symbol(name, file_path, "type", node, content)
338
+
339
+ elif t == "export_statement":
340
+ for child in node.children:
341
+ self._traverse_typescript(child, content, file_path, parent_class)
342
+ return
343
+
344
+ for child in node.children:
345
+ self._traverse_typescript(child, content, file_path, parent_class)
346
+
347
+ def _traverse_clike(
348
+ self, node: Any, content: str, file_path: str, parent: Optional[str] = None
349
+ ):
350
+ t = node.type
351
+ if t == "function_definition":
352
+ decl = node.child_by_field_name("declarator")
353
+ name = self._descend_identifier(decl, content) if decl else None
354
+ if name:
355
+ full = f"{parent}::{name}" if parent else name
356
+ self._add_symbol(
357
+ full, file_path, "method" if parent else "function", node, content
358
+ )
359
+ elif t in ("struct_specifier", "class_specifier", "enum_specifier"):
360
+ name_node = node.child_by_field_name("name")
361
+ if name_node:
362
+ name = _node_text(content, name_node)
363
+ kind = (
364
+ "class"
365
+ if t == "class_specifier"
366
+ else ("enum" if t == "enum_specifier" else "struct")
367
+ )
368
+ self._add_symbol(name, file_path, kind, node, content)
369
+ body = node.child_by_field_name("body")
370
+ if body:
371
+ for child in body.children:
372
+ self._traverse_clike(child, content, file_path, parent=name)
373
+ return
374
+ elif t == "type_definition":
375
+ name_node = self._last_child_of_type(node, "type_identifier")
376
+ if name_node:
377
+ name = _node_text(content, name_node)
378
+ self._add_symbol(name, file_path, "type", node, content)
379
+ elif t == "namespace_definition":
380
+ body = node.child_by_field_name("body")
381
+ if body:
382
+ for child in body.children:
383
+ self._traverse_clike(child, content, file_path, parent)
384
+ return
385
+
386
+ for child in node.children:
387
+ self._traverse_clike(child, content, file_path, parent)
388
+
389
+ def _traverse_swift(
390
+ self, node: Any, content: str, file_path: str, parent: Optional[str] = None
391
+ ):
392
+ t = node.type
393
+ if t == "function_declaration":
394
+ name = self._first_child_text(node, content, ("simple_identifier",))
395
+ if name:
396
+ full = f"{parent}.{name}" if parent else name
397
+ self._add_symbol(
398
+ full, file_path, "method" if parent else "function", node, content
399
+ )
400
+ elif t in ("class_declaration", "protocol_declaration"):
401
+ name = self._first_child_text(node, content, ("type_identifier",))
402
+ if name:
403
+ # The Swift grammar models struct as a class_declaration with a
404
+ # `struct` keyword child; distinguish so the kind is accurate.
405
+ kinds = {c.type for c in node.children}
406
+ kind = (
407
+ "protocol"
408
+ if t == "protocol_declaration"
409
+ else "struct"
410
+ if "struct" in kinds
411
+ else "class"
412
+ )
413
+ self._add_symbol(name, file_path, kind, node, content)
414
+ for child in node.children:
415
+ if child.type in ("class_body", "protocol_body"):
416
+ for member in child.children:
417
+ self._traverse_swift(
418
+ member, content, file_path, parent=name
419
+ )
420
+ return
421
+
422
+ for child in node.children:
423
+ self._traverse_swift(child, content, file_path, parent)
424
+
425
+ _CSHARP_TYPE_KINDS = {
426
+ "class_declaration": "class",
427
+ "struct_declaration": "struct",
428
+ "interface_declaration": "interface",
429
+ "enum_declaration": "enum",
430
+ "record_declaration": "record",
431
+ }
432
+
433
+ def _traverse_csharp(
434
+ self, node: Any, content: str, file_path: str, parent: Optional[str] = None
435
+ ):
436
+ t = node.type
437
+ if t in self._CSHARP_TYPE_KINDS:
438
+ name_node = node.child_by_field_name("name")
439
+ if name_node:
440
+ name = _node_text(content, name_node)
441
+ self._add_symbol(
442
+ name, file_path, self._CSHARP_TYPE_KINDS[t], node, content
443
+ )
444
+ body = node.child_by_field_name("body")
445
+ if body is None:
446
+ body = self._first_child_of_type(node, "declaration_list")
447
+ if body:
448
+ for child in body.children:
449
+ self._traverse_csharp(child, content, file_path, parent=name)
450
+ return
451
+ elif t in ("method_declaration", "constructor_declaration"):
452
+ name_node = node.child_by_field_name("name")
453
+ if name_node:
454
+ name = _node_text(content, name_node)
455
+ full = f"{parent}.{name}" if parent else name
456
+ self._add_symbol(
457
+ full, file_path, "method" if parent else "function", node, content
458
+ )
459
+ elif t == "property_declaration":
460
+ name_node = node.child_by_field_name("name")
461
+ if name_node:
462
+ name = _node_text(content, name_node)
463
+ full = f"{parent}.{name}" if parent else name
464
+ self._add_symbol(full, file_path, "property", node, content)
465
+
466
+ for child in node.children:
467
+ self._traverse_csharp(child, content, file_path, parent)
468
+
469
+ def _traverse_kotlin(
470
+ self, node: Any, content: str, file_path: str, parent: Optional[str] = None
471
+ ):
472
+ # Best-effort: the Kotlin grammar reports ERROR nodes on some valid
473
+ # code, so always recurse all children (declarations under an ERROR
474
+ # node are still captured) and never use Kotlin for the syntax gate.
475
+ t = node.type
476
+ if t == "function_declaration":
477
+ name_node = node.child_by_field_name("name")
478
+ if name_node:
479
+ name = _node_text(content, name_node)
480
+ full = f"{parent}.{name}" if parent else name
481
+ self._add_symbol(
482
+ full, file_path, "method" if parent else "function", node, content
483
+ )
484
+ elif t in ("class_declaration", "object_declaration"):
485
+ name_node = node.child_by_field_name("name") or self._first_child_of_type(
486
+ node, "type_identifier"
487
+ )
488
+ if name_node:
489
+ name = _node_text(content, name_node)
490
+ kind = "object" if t == "object_declaration" else "class"
491
+ self._add_symbol(name, file_path, kind, node, content)
492
+ for child in node.children:
493
+ self._traverse_kotlin(child, content, file_path, parent=name)
494
+ return
495
+
496
+ for child in node.children:
497
+ self._traverse_kotlin(child, content, file_path, parent)
498
+
499
+ @staticmethod
500
+ def _first_child_of_type(node: Any, type_name: str) -> Any:
501
+ for c in node.children:
502
+ if c.type == type_name:
503
+ return c
504
+ return None
505
+
506
+ @staticmethod
507
+ def _descend_identifier(node: Any, content: str) -> Optional[str]:
508
+ """Breadth-first search for the first declarator identifier.
509
+
510
+ A C/C++ function name is wrapped in declarator nodes (pointer, array,
511
+ parenthesized); the shallowest ``identifier``/``field_identifier`` on
512
+ the name path is the symbol name. BFS finds it before parameter names.
513
+ """
514
+ queue = [node]
515
+ while queue:
516
+ n = queue.pop(0)
517
+ if n.type in ("identifier", "field_identifier"):
518
+ return _node_text(content, n)
519
+ queue.extend(n.children)
520
+ return None
521
+
522
+ @staticmethod
523
+ def _first_child_text(node: Any, content: str, types: tuple) -> Optional[str]:
524
+ for c in node.children:
525
+ if c.type in types:
526
+ return _node_text(content, c)
527
+ return None
528
+
529
+ @staticmethod
530
+ def _last_child_of_type(node: Any, type_name: str) -> Any:
531
+ found = None
532
+ for c in node.children:
533
+ if c.type == type_name:
534
+ found = c
535
+ return found
536
+
537
+ def _add_symbol(
538
+ self, name: str, file_path: str, kind: str, node: Any, content: str
539
+ ):
540
+ key = f"{file_path}:{name}"
541
+ symbol_content = _node_text(content, node)
542
+ self.symbols[key] = SymbolNode(
543
+ name,
544
+ file_path,
545
+ kind,
546
+ node.start_point.row,
547
+ node.end_point.row,
548
+ symbol_content,
549
+ )
550
+
551
+ def _resolve_references(self):
552
+ # Names can collide across files (two functions both named ``run``).
553
+ # Keying by name alone collapsed them, so every ``run()`` call resolved to
554
+ # one arbitrary definition — a misattributed edge. Resolve scope-aware
555
+ # instead: prefer a definition in the caller's OWN file, then a
556
+ # globally-unique definition; when several files define the name and none
557
+ # is local, the call is genuinely ambiguous without import resolution, so
558
+ # we add no edge rather than guess wrong.
559
+ name_to_keys: Dict[str, List[str]] = {}
560
+ for key, s in self.symbols.items():
561
+ name_to_keys.setdefault(s.name, []).append(key)
562
+ for key, symbol in self.symbols.items():
563
+ called = {m.group(1) for m in _CALL_PATTERN.finditer(symbol.content)}
564
+ for name in called:
565
+ candidates = name_to_keys.get(name)
566
+ if not candidates:
567
+ continue
568
+ same_file = [
569
+ k
570
+ for k in candidates
571
+ if k != key and self.symbols[k].file_path == symbol.file_path
572
+ ]
573
+ if same_file:
574
+ targets = same_file
575
+ elif len(candidates) == 1 and candidates[0] != key:
576
+ targets = candidates
577
+ else:
578
+ continue
579
+ for other_key in targets:
580
+ symbol.outgoing_calls.add(other_key)
581
+ self.symbols[other_key].incoming_calls.add(key)
582
+
583
+ def file_symbols(self, file_path: str) -> List[SymbolNode]:
584
+ """Symbols defined in one file, ordered by start line."""
585
+ return sorted(
586
+ (s for s in self.symbols.values() if s.file_path == file_path),
587
+ key=lambda s: s.start_line,
588
+ )
589
+
590
+ def _match_files(self, file_path: str) -> Set[str]:
591
+ """Graph file paths an error path refers to: an exact match when present,
592
+ else a UNIQUE suffix match so a path reported relative to a sub-target's
593
+ cwd (``src/app.ts``) still resolves against the root-relative key
594
+ (``frontend/src/app.ts``). An ambiguous suffix yields no match rather
595
+ than a wrong one."""
596
+ exact = {s.file_path for s in self.symbols.values() if s.file_path == file_path}
597
+ if exact:
598
+ return exact
599
+ suffix = "/" + file_path
600
+ matches = {
601
+ s.file_path for s in self.symbols.values() if s.file_path.endswith(suffix)
602
+ }
603
+ return matches if len(matches) == 1 else set()
604
+
605
+ def symbol_at_line(self, file_path: str, line: int) -> Optional[str]:
606
+ """Key of the narrowest symbol enclosing a 1-indexed source line, or None.
607
+
608
+ ``start_line``/``end_line`` are 0-indexed tree-sitter rows (rendered
609
+ ``+1`` for humans), so a 1-indexed error line maps to row ``line - 1``.
610
+ The narrowest enclosing span wins so a method attributes to itself, not
611
+ its containing class.
612
+ """
613
+ files = self._match_files(file_path)
614
+ if not files:
615
+ return None
616
+ row = line - 1
617
+ best_key: Optional[str] = None
618
+ best_span: Optional[int] = None
619
+ for key, sym in self.symbols.items():
620
+ if sym.file_path not in files:
621
+ continue
622
+ if sym.start_line <= row <= sym.end_line:
623
+ span = sym.end_line - sym.start_line
624
+ if best_span is None or span < best_span:
625
+ best_span = span
626
+ best_key = key
627
+ return best_key
628
+
629
+ def callers_of(self, key: str) -> List[str]:
630
+ """Names of the symbols that call the symbol identified by ``key``
631
+ (capped). Keyed by the unique ``file_path:name`` id, so same-named
632
+ symbols in other files are never conflated."""
633
+ sym = self.symbols.get(key)
634
+ if sym is None:
635
+ return []
636
+ callers: List[str] = []
637
+ for caller_key in sym.incoming_calls:
638
+ caller = self.symbols.get(caller_key)
639
+ if caller and caller.name not in callers:
640
+ callers.append(caller.name)
641
+ return callers[:5]
642
+
643
+ def file_outline(self, file_path: str) -> str:
644
+ """A compact table of contents for one file: each symbol with its lines.
645
+
646
+ Lets the model navigate a large file it is editing and place precise
647
+ SEARCH anchors without scanning the whole body line by line.
648
+ """
649
+ return "\n".join(
650
+ f" L{s.start_line + 1}-{s.end_line + 1}: {s.kind} {s.name}"
651
+ for s in self.file_symbols(file_path)
652
+ )
653
+
654
+ def project_outline(self, max_files: int = 300, max_syms_per_file: int = 60) -> str:
655
+ """A whole-project structural map: every file with its top-level symbols.
656
+
657
+ Far denser than reading file heads — it conveys the architecture (what
658
+ exists, where) within a small token budget so planning and editing are
659
+ grounded in the entire project, not a first-N-lines slice.
660
+ """
661
+ by_file: Dict[str, List[SymbolNode]] = {}
662
+ for s in self.symbols.values():
663
+ by_file.setdefault(s.file_path, []).append(s)
664
+ if not by_file:
665
+ return ""
666
+ out = []
667
+ for path in sorted(by_file)[:max_files]:
668
+ syms = sorted(by_file[path], key=lambda s: s.start_line)
669
+ shown = ", ".join(f"{s.kind} {s.name}" for s in syms[:max_syms_per_file])
670
+ if len(syms) > max_syms_per_file:
671
+ shown += f", +{len(syms) - max_syms_per_file} more"
672
+ out.append(f"{path}: {shown}")
673
+ if len(by_file) > max_files:
674
+ out.append(f"(... {len(by_file) - max_files} more files)")
675
+ return "\n".join(out)