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.
- misterdev/__init__.py +3 -0
- misterdev/agent.py +2166 -0
- misterdev/agent_helpers.py +194 -0
- misterdev/analyzers/__init__.py +0 -0
- misterdev/analyzers/project_analyzer/__init__.py +246 -0
- misterdev/analyzers/project_analyzer/detection.py +146 -0
- misterdev/analyzers/project_analyzer/merge.py +137 -0
- misterdev/analyzers/project_analyzer/overview.py +312 -0
- misterdev/analyzers/project_analyzer/prompts.py +83 -0
- misterdev/cli.py +370 -0
- misterdev/config.py +521 -0
- misterdev/core/__init__.py +0 -0
- misterdev/core/audit.py +88 -0
- misterdev/core/config.py +10 -0
- misterdev/core/context/__init__.py +0 -0
- misterdev/core/context/change_tracker.py +218 -0
- misterdev/core/context/contracts/__init__.py +63 -0
- misterdev/core/context/contracts/_log.py +9 -0
- misterdev/core/context/contracts/_text.py +12 -0
- misterdev/core/context/contracts/extraction.py +30 -0
- misterdev/core/context/contracts/python_generic.py +42 -0
- misterdev/core/context/contracts/registry.py +127 -0
- misterdev/core/context/contracts/rust_line.py +225 -0
- misterdev/core/context/contracts/rust_tree_sitter.py +141 -0
- misterdev/core/context/lsp.py +174 -0
- misterdev/core/context/scratchpad.py +111 -0
- misterdev/core/context/topography/__init__.py +43 -0
- misterdev/core/context/topography/_log.py +10 -0
- misterdev/core/context/topography/cache.py +91 -0
- misterdev/core/context/topography/engine.py +172 -0
- misterdev/core/context/topography/graph.py +675 -0
- misterdev/core/context/topography/nodes.py +55 -0
- misterdev/core/context/topography/parsers.py +95 -0
- misterdev/core/context/topography/syntax.py +54 -0
- misterdev/core/economics/__init__.py +0 -0
- misterdev/core/economics/context_budget.py +175 -0
- misterdev/core/economics/embeddings.py +232 -0
- misterdev/core/economics/free_models.py +108 -0
- misterdev/core/economics/llm_cache.py +105 -0
- misterdev/core/economics/model_catalog.py +79 -0
- misterdev/core/economics/model_ledger.py +331 -0
- misterdev/core/economics/model_selector.py +281 -0
- misterdev/core/execution/__init__.py +0 -0
- misterdev/core/execution/bounded.py +50 -0
- misterdev/core/execution/container.py +221 -0
- misterdev/core/execution/error_classifier.py +366 -0
- misterdev/core/execution/error_resolver.py +201 -0
- misterdev/core/execution/governance.py +283 -0
- misterdev/core/execution/outcomes.py +50 -0
- misterdev/core/execution/progress.py +120 -0
- misterdev/core/execution/project.py +231 -0
- misterdev/core/execution/registry.py +97 -0
- misterdev/core/execution/runtime.py +279 -0
- misterdev/core/gitcmd.py +39 -0
- misterdev/core/integration/__init__.py +0 -0
- misterdev/core/integration/mcp.py +368 -0
- misterdev/core/integration/mcp_gather.py +186 -0
- misterdev/core/models.py +35 -0
- misterdev/core/modes.py +184 -0
- misterdev/core/planning/__init__.py +0 -0
- misterdev/core/planning/advisor.py +89 -0
- misterdev/core/planning/assessment.py +135 -0
- misterdev/core/planning/decomposer.py +387 -0
- misterdev/core/planning/metacognition.py +103 -0
- misterdev/core/planning/sovereign.py +308 -0
- misterdev/core/planning/targets.py +201 -0
- misterdev/core/reporting/__init__.py +0 -0
- misterdev/core/reporting/report.py +377 -0
- misterdev/core/reporting/report_view.py +151 -0
- misterdev/core/task.py +163 -0
- misterdev/core/verification/__init__.py +0 -0
- misterdev/core/verification/claim_verifier.py +210 -0
- misterdev/core/verification/critic.py +324 -0
- misterdev/core/verification/gatekeeper/__init__.py +631 -0
- misterdev/core/verification/gatekeeper/constants.py +138 -0
- misterdev/core/verification/gatekeeper/helpers.py +28 -0
- misterdev/core/verification/goal_check.py +219 -0
- misterdev/core/verification/independent.py +68 -0
- misterdev/core/verification/mutation_gate.py +221 -0
- misterdev/core/verification/preflight.py +95 -0
- misterdev/core/verification/spec_tests.py +175 -0
- misterdev/core/verification/validator.py +495 -0
- misterdev/core/verification/vision_verify.py +185 -0
- misterdev/core/verification/web_verify.py +408 -0
- misterdev/environments/__init__.py +0 -0
- misterdev/environments/base_env.py +18 -0
- misterdev/environments/container_env.py +87 -0
- misterdev/environments/venv_env.py +42 -0
- misterdev/llm/__init__.py +0 -0
- misterdev/llm/client/__init__.py +152 -0
- misterdev/llm/client/base.py +382 -0
- misterdev/llm/client/edits.py +70 -0
- misterdev/llm/client/embeddings.py +121 -0
- misterdev/llm/client/errors.py +134 -0
- misterdev/llm/client/providers.py +535 -0
- misterdev/llm/client/response.py +24 -0
- misterdev/llm/prompt_manager.py +82 -0
- misterdev/llm/responses/__init__.py +34 -0
- misterdev/llm/responses/apply.py +131 -0
- misterdev/llm/responses/json_extract.py +80 -0
- misterdev/llm/responses/models.py +43 -0
- misterdev/llm/responses/parsing.py +494 -0
- misterdev/logging_setup.py +20 -0
- misterdev/mcp_server.py +208 -0
- misterdev/nl_cli.py +149 -0
- misterdev/plugins.py +115 -0
- misterdev/py.typed +0 -0
- misterdev/task_executors/__init__.py +0 -0
- misterdev/task_executors/base_executor.py +10 -0
- misterdev/task_executors/markdown_plan_executor/__init__.py +90 -0
- misterdev/task_executors/markdown_plan_executor/commands_mixin.py +82 -0
- misterdev/task_executors/markdown_plan_executor/context_mixin.py +221 -0
- misterdev/task_executors/markdown_plan_executor/critic_spec_mixin.py +174 -0
- misterdev/task_executors/markdown_plan_executor/edits_mixin.py +251 -0
- misterdev/task_executors/markdown_plan_executor/execute_mixin.py +727 -0
- misterdev/task_executors/markdown_plan_executor/gates_mixin.py +203 -0
- misterdev/task_executors/markdown_plan_executor/git_mixin.py +219 -0
- misterdev/task_executors/markdown_plan_executor/helpers.py +521 -0
- misterdev/task_executors/markdown_plan_executor/llm_mixin.py +238 -0
- misterdev/task_executors/markdown_plan_executor/results_mixin.py +23 -0
- misterdev/tools/__init__.py +19 -0
- misterdev/tools/base_tool.py +14 -0
- misterdev/tools/command.py +75 -0
- misterdev/tools/file_io.py +69 -0
- misterdev/tools/formatter.py +26 -0
- misterdev/tools/git_tool.py +90 -0
- misterdev/utils/__init__.py +0 -0
- misterdev/utils/file_utils.py +169 -0
- misterdev/utils/process.py +23 -0
- misterdev-0.2.0.dist-info/METADATA +326 -0
- misterdev-0.2.0.dist-info/RECORD +136 -0
- misterdev-0.2.0.dist-info/WHEEL +5 -0
- misterdev-0.2.0.dist-info/entry_points.txt +3 -0
- misterdev-0.2.0.dist-info/licenses/COMMERCIAL_LICENSE.md +34 -0
- misterdev-0.2.0.dist-info/licenses/LICENSE +661 -0
- 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)
|