k-cli-for-devs 1.0.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 (75) hide show
  1. k_cli/__init__.py +77 -0
  2. k_cli/agents/__init__.py +0 -0
  3. k_cli/agents/adversarial_swarm.py +338 -0
  4. k_cli/agents/agent_core.py +255 -0
  5. k_cli/agents/background_daemon.py +141 -0
  6. k_cli/agents/orchestrator.py +376 -0
  7. k_cli/agents/persona.py +649 -0
  8. k_cli/agents/scaffold_engine.py +121 -0
  9. k_cli/agents/strands_agent.py +832 -0
  10. k_cli/agents/subagents.py +1496 -0
  11. k_cli/cli.py +3297 -0
  12. k_cli/core/__init__.py +0 -0
  13. k_cli/core/airgap.py +95 -0
  14. k_cli/core/credentials.py +548 -0
  15. k_cli/core/intent_sensor.py +177 -0
  16. k_cli/core/llm_driver.py +1028 -0
  17. k_cli/core/model_manager.py +1109 -0
  18. k_cli/core/models_hub.py +913 -0
  19. k_cli/core/prompting.py +41 -0
  20. k_cli/core/sdk.py +322 -0
  21. k_cli/core/session.py +826 -0
  22. k_cli/core/smart_router.py +230 -0
  23. k_cli/core/storage_manager.py +176 -0
  24. k_cli/core/viewport_engine.py +117 -0
  25. k_cli/demo/demo_runner.py +579 -0
  26. k_cli/git/__init__.py +0 -0
  27. k_cli/git/ai_bisect.py +208 -0
  28. k_cli/git/conflict_resolver.py +1039 -0
  29. k_cli/git/git_guard.py +417 -0
  30. k_cli/git/patcher.py +1175 -0
  31. k_cli/git/repo_map.py +1780 -0
  32. k_cli/git/smart_git.py +928 -0
  33. k_cli/git/verifier.py +969 -0
  34. k_cli/github/__init__.py +0 -0
  35. k_cli/github/dedup_engine.py +787 -0
  36. k_cli/github/github_client.py +1702 -0
  37. k_cli/github/github_engine.py +641 -0
  38. k_cli/github/local_hub.py +209 -0
  39. k_cli/github/pr_watcher.py +129 -0
  40. k_cli/github/trending.py +205 -0
  41. k_cli/tools/__init__.py +0 -0
  42. k_cli/tools/audit.py +79 -0
  43. k_cli/tools/chaos_immunity.py +377 -0
  44. k_cli/tools/codebase_qa.py +106 -0
  45. k_cli/tools/command_runner.py +256 -0
  46. k_cli/tools/diagram_generator.py +547 -0
  47. k_cli/tools/doc_retriever.py +1332 -0
  48. k_cli/tools/feature.py +105 -0
  49. k_cli/tools/ghost_daemon.py +122 -0
  50. k_cli/tools/incident_triage.py +1365 -0
  51. k_cli/tools/mcp_client.py +1846 -0
  52. k_cli/tools/repo_gardener.py +142 -0
  53. k_cli/tools/rules.py +109 -0
  54. k_cli/tools/security.py +52 -0
  55. k_cli/tools/security_healer.py +999 -0
  56. k_cli/tools/synapse_graph.py +155 -0
  57. k_cli/tui/__init__.py +0 -0
  58. k_cli/tui/diff_viewer.py +223 -0
  59. k_cli/tui/tui.py +1145 -0
  60. k_cli/tui/tui_animations.py +648 -0
  61. k_cli/tui/tui_app.py +2788 -0
  62. k_cli/ui/__init__.py +10 -0
  63. k_cli/ui/simple_repl.py +315 -0
  64. k_cli/web/__init__.py +7 -0
  65. k_cli/web/server.py +624 -0
  66. k_cli/web/static/app.js +830 -0
  67. k_cli/web/static/index.html +495 -0
  68. k_cli/web/static/monitor.html +189 -0
  69. k_cli/web/static/style.css +838 -0
  70. k_cli_for_devs-1.0.0.dist-info/METADATA +461 -0
  71. k_cli_for_devs-1.0.0.dist-info/RECORD +75 -0
  72. k_cli_for_devs-1.0.0.dist-info/WHEEL +5 -0
  73. k_cli_for_devs-1.0.0.dist-info/entry_points.txt +2 -0
  74. k_cli_for_devs-1.0.0.dist-info/licenses/LICENSE +21 -0
  75. k_cli_for_devs-1.0.0.dist-info/top_level.txt +1 -0
k_cli/git/repo_map.py ADDED
@@ -0,0 +1,1780 @@
1
+ """
2
+ repo_map.py - Advanced Semantic Codebase Repository Map & AST Navigator for K-CLI.
3
+
4
+ Features:
5
+ 1. Deep AST & Regex Symbol Extraction across Python, JS/TS, C/C++, Rust, and Go
6
+ (Classes, Methods, Async Functions, Structs, Enums, Traits, Interfaces, Type Signatures).
7
+ 2. Dependency Graph Analysis (Import tree resolution, caller-callee mapping, cyclic import detection).
8
+ 3. Compact Token-Optimized Topological Summary for LLM context injection.
9
+ 4. Incremental Hashing Cache (mtime + blake2b/sha256 hashing) for sub-millisecond updates.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import ast
15
+ import hashlib
16
+ import logging
17
+ import os
18
+ import re
19
+ from collections import defaultdict, deque
20
+ from dataclasses import dataclass, field
21
+ from pathlib import Path
22
+ from typing import Any, Dict, List, Optional, Set, Tuple
23
+
24
+ logger = logging.getLogger(__name__)
25
+
26
+
27
+ @dataclass
28
+ class CacheEntry:
29
+ """Internal cache entry storing file metadata, content hash, and extracted AST symbols."""
30
+ mtime: float
31
+ size: int
32
+ content_hash: str
33
+ symbols: List[Dict[str, Any]]
34
+ file_info: Dict[str, Any]
35
+
36
+
37
+ class RepoMap:
38
+ """
39
+ AST-driven Codebase Repository Map extractor, dependency analyzer, and ranker.
40
+
41
+ Provides fast, memory-efficient multi-language symbol extraction, dependency graph
42
+ analysis (caller-callee, cyclic imports), topological summary generation, and
43
+ incremental hashing cache for sub-millisecond latency.
44
+ """
45
+
46
+ DEFAULT_IGNORED_DIRS: Set[str] = {
47
+ ".git",
48
+ ".agents",
49
+ ".pytest_cache",
50
+ ".venv",
51
+ "k_cli_env",
52
+ "venv",
53
+ "env",
54
+ "data",
55
+ ".pytest_cache",
56
+ "__pycache__",
57
+ "build",
58
+ "dist",
59
+ "node_modules",
60
+ ".tox",
61
+ ".idea",
62
+ ".vscode",
63
+ ".mypy_cache",
64
+ ".ruff_cache",
65
+ "site-packages",
66
+ ".eggs",
67
+ "target",
68
+ "vendor",
69
+ ".next",
70
+ ".turbo",
71
+ ".cache",
72
+ ".agents",
73
+ }
74
+
75
+ SUPPORTED_EXTENSIONS: Set[str] = {
76
+ ".py",
77
+ ".pyi",
78
+ ".js",
79
+ ".jsx",
80
+ ".ts",
81
+ ".tsx",
82
+ ".mjs",
83
+ ".cjs",
84
+ ".c",
85
+ ".cc",
86
+ ".cpp",
87
+ ".cxx",
88
+ ".h",
89
+ ".hh",
90
+ ".hpp",
91
+ ".hxx",
92
+ ".rs",
93
+ ".go",
94
+ }
95
+
96
+ def __init__(
97
+ self,
98
+ root_dir: str = ".",
99
+ ignored_dirs: Optional[Set[str]] = None,
100
+ supported_extensions: Optional[Set[str]] = None,
101
+ ) -> None:
102
+ """
103
+ Initializes the RepoMap with a workspace root directory.
104
+
105
+ Args:
106
+ root_dir: Path to the workspace root directory.
107
+ ignored_dirs: Optional set of directory names to ignore during scanning.
108
+ supported_extensions: Optional set of file extensions to include.
109
+ """
110
+ self.root_dir = Path(root_dir).resolve()
111
+ if ignored_dirs is not None:
112
+ self.ignored_dirs = set(ignored_dirs)
113
+ else:
114
+ self.ignored_dirs = set(self.DEFAULT_IGNORED_DIRS)
115
+
116
+ if supported_extensions is not None:
117
+ self.supported_extensions = set(supported_extensions)
118
+ else:
119
+ self.supported_extensions = set(self.SUPPORTED_EXTENSIONS)
120
+
121
+ # Incremental Cache: str_path -> CacheEntry
122
+ self._cache: Dict[str, CacheEntry] = {}
123
+ self._cache_hits: int = 0
124
+ self._cache_misses: int = 0
125
+
126
+ # ==========================================================================
127
+ # Workspace Traversal & File Filtering
128
+ # ==========================================================================
129
+
130
+ def _should_skip_dir(self, dir_name: str, full_dir_path: Optional[str] = None) -> bool:
131
+ """Determines if a directory should be skipped during workspace traversal."""
132
+ if dir_name.startswith("."):
133
+ return True
134
+ if dir_name.endswith(".egg-info"):
135
+ return True
136
+ if dir_name in self.ignored_dirs:
137
+ return True
138
+ if full_dir_path:
139
+ # Detect virtualenvs dynamically by checking common venv signatures
140
+ if os.path.isfile(os.path.join(full_dir_path, "pyvenv.cfg")):
141
+ return True
142
+ if os.path.isfile(os.path.join(full_dir_path, "bin", "activate")) or os.path.isfile(
143
+ os.path.join(full_dir_path, "Scripts", "activate")
144
+ ):
145
+ return True
146
+ return False
147
+
148
+ def _should_skip_file(self, file_name: str) -> bool:
149
+ """Determines if a file should be skipped."""
150
+ if file_name.startswith("."):
151
+ return True
152
+ _, ext = os.path.splitext(file_name)
153
+ return ext.lower() not in self.supported_extensions
154
+
155
+ def _is_binary(self, bytes_sample: bytes) -> bool:
156
+ """Checks if a byte sample contains null bytes or binary characters."""
157
+ return b"\x00" in bytes_sample
158
+
159
+ def scan_workspace_files(self) -> List[str]:
160
+ """
161
+ Recursively scans workspace directory for candidate source code files,
162
+ skipping ignored directories and hidden/binary files.
163
+
164
+ Returns:
165
+ Sorted list of absolute file paths to valid source files.
166
+ """
167
+ if not self.root_dir.exists() or not self.root_dir.is_dir():
168
+ return []
169
+
170
+ source_files: List[str] = []
171
+ for root, dirs, files in os.walk(str(self.root_dir)):
172
+ # Filter dirs in-place to prevent walking ignored subtrees
173
+ dirs[:] = [d for d in dirs if not self._should_skip_dir(d, os.path.join(root, d))]
174
+ for file_name in files:
175
+ if not self._should_skip_file(file_name):
176
+ full_path = os.path.join(root, file_name)
177
+ source_files.append(full_path)
178
+
179
+ source_files.sort()
180
+ return source_files
181
+
182
+ # ==========================================================================
183
+ # Incremental Cache Management
184
+ # ==========================================================================
185
+
186
+ def invalidate_cache(self, file_path: Optional[str] = None) -> None:
187
+ """Invalidates cache for a specific file or clears entire cache if None."""
188
+ if file_path is None:
189
+ self._cache.clear()
190
+ self._cache_hits = 0
191
+ self._cache_misses = 0
192
+ else:
193
+ path = Path(file_path)
194
+ if not path.is_absolute():
195
+ path = (self.root_dir / path).resolve()
196
+ self._cache.pop(str(path), None)
197
+
198
+ def clear_cache(self) -> None:
199
+ """Clears all cached symbols and statistics."""
200
+ self.invalidate_cache(None)
201
+
202
+ def get_cache_stats(self) -> Dict[str, int]:
203
+ """Returns cache hit/miss statistics and entry count."""
204
+ return {
205
+ "hits": self._cache_hits,
206
+ "misses": self._cache_misses,
207
+ "cached_files": len(self._cache),
208
+ }
209
+
210
+ # ==========================================================================
211
+ # Symbol & Metadata Extraction (Multi-Language)
212
+ # ==========================================================================
213
+
214
+ def extract_symbols(self, file_path: str) -> List[Dict[str, Any]]:
215
+ """
216
+ Extracts structured symbol metadata from a source file.
217
+
218
+ Args:
219
+ file_path: Relative or absolute path to the source file.
220
+
221
+ Returns:
222
+ List of symbol dictionaries for classes, structs, methods, functions,
223
+ enums, traits, interfaces, and type aliases.
224
+ Returns an empty list on syntax errors, missing files, or binary files.
225
+ """
226
+ symbols, _ = self._extract_file_info(file_path)
227
+ return symbols
228
+
229
+ def _extract_file_info(self, file_path: str) -> Tuple[List[Dict[str, Any]], Dict[str, Any]]:
230
+ """
231
+ Extracts symbols and cross-file reference metadata from a file.
232
+ Uses dual-tier fast stat + content hash caching for sub-millisecond lookups.
233
+ """
234
+ path = Path(file_path)
235
+ if not path.is_absolute():
236
+ path = (self.root_dir / path).resolve()
237
+
238
+ if not path.exists() or not path.is_file():
239
+ return [], {}
240
+
241
+ try:
242
+ stat_res = path.stat()
243
+ mtime = stat_res.st_mtime
244
+ size = stat_res.st_size
245
+ except OSError:
246
+ return [], {}
247
+
248
+ str_path = str(path)
249
+ cached = self._cache.get(str_path)
250
+
251
+ # Tier 1: Fast stat check (sub-millisecond)
252
+ if cached is not None and cached.mtime == mtime and cached.size == size:
253
+ self._cache_hits += 1
254
+ return cached.symbols, cached.file_info
255
+
256
+ # Read content safely
257
+ try:
258
+ with open(path, "rb") as f:
259
+ raw_bytes = f.read()
260
+ except OSError:
261
+ return [], {}
262
+
263
+ if self._is_binary(raw_bytes[:8192]):
264
+ return [], {}
265
+
266
+ # Tier 2: Content Hash check (SHA-256 / Blake2b)
267
+ content_hash = hashlib.sha256(raw_bytes).hexdigest()
268
+ if cached is not None and cached.content_hash == content_hash:
269
+ self._cache_hits += 1
270
+ # Update mtime and size in cache without re-parsing
271
+ cached.mtime = mtime
272
+ cached.size = size
273
+ return cached.symbols, cached.file_info
274
+
275
+ self._cache_misses += 1
276
+
277
+ try:
278
+ source = raw_bytes.decode("utf-8-sig")
279
+ except UnicodeDecodeError:
280
+ try:
281
+ source = raw_bytes.decode("latin-1")
282
+ except Exception:
283
+ return [], {}
284
+
285
+ source = source.lstrip("\ufeff")
286
+ if not source.strip():
287
+ entry = CacheEntry(
288
+ mtime=mtime,
289
+ size=size,
290
+ content_hash=content_hash,
291
+ symbols=[],
292
+ file_info={},
293
+ )
294
+ self._cache[str_path] = entry
295
+ return [], {}
296
+
297
+ ext = path.suffix.lower()
298
+ symbols: List[Dict[str, Any]] = []
299
+ file_info: Dict[str, Any] = {}
300
+
301
+ if ext in (".py", ".pyi"):
302
+ symbols, file_info = self._parse_python(source, str_path)
303
+ elif ext in (".js", ".jsx", ".ts", ".tsx", ".mjs", ".cjs"):
304
+ symbols, file_info = self._parse_javascript_typescript(source, str_path, ext)
305
+ elif ext == ".rs":
306
+ symbols, file_info = self._parse_rust(source, str_path)
307
+ elif ext == ".go":
308
+ symbols, file_info = self._parse_go(source, str_path)
309
+ elif ext in (".c", ".cc", ".cpp", ".cxx", ".h", ".hh", ".hpp", ".hxx"):
310
+ symbols, file_info = self._parse_cpp(source, str_path)
311
+ else:
312
+ symbols, file_info = [], {}
313
+
314
+ entry = CacheEntry(
315
+ mtime=mtime,
316
+ size=size,
317
+ content_hash=content_hash,
318
+ symbols=symbols,
319
+ file_info=file_info,
320
+ )
321
+ self._cache[str_path] = entry
322
+ return symbols, file_info
323
+
324
+ # --------------------------------------------------------------------------
325
+ # Language Parsers
326
+ # --------------------------------------------------------------------------
327
+
328
+ def _parse_python(self, source: str, str_path: str) -> Tuple[List[Dict[str, Any]], Dict[str, Any]]:
329
+ """Parses Python source using the native `ast` module."""
330
+ try:
331
+ tree = ast.parse(source, filename=str_path)
332
+ except (SyntaxError, ValueError, MemoryError, RecursionError) as e:
333
+ logger.debug("Skipping syntax error / invalid AST in %s: %s", str_path, e)
334
+ return [], {}
335
+
336
+ symbols: List[Dict[str, Any]] = []
337
+ defined_names: Set[str] = set()
338
+ imported_names: Set[str] = set()
339
+ referenced_names: Set[str] = set()
340
+ raw_imports: List[str] = []
341
+ caller_callees: Dict[str, Set[str]] = defaultdict(set)
342
+
343
+ # Collect references, imports, and calls from full AST
344
+ for node in ast.walk(tree):
345
+ if isinstance(node, ast.Import):
346
+ for alias in node.names:
347
+ imported_names.add(alias.name.split(".")[0])
348
+ raw_imports.append(alias.name)
349
+ if alias.asname:
350
+ imported_names.add(alias.asname)
351
+ elif isinstance(node, ast.ImportFrom):
352
+ mod = node.module or ""
353
+ if mod:
354
+ imported_names.add(mod.split(".")[0])
355
+ raw_imports.append(mod)
356
+ for alias in node.names:
357
+ imported_names.add(alias.name)
358
+ if mod:
359
+ raw_imports.append(f"{mod}.{alias.name}")
360
+ if alias.asname:
361
+ imported_names.add(alias.asname)
362
+ elif isinstance(node, ast.Name):
363
+ referenced_names.add(node.id)
364
+ elif isinstance(node, ast.Attribute):
365
+ referenced_names.add(node.attr)
366
+
367
+ # Helper to extract calls inside a function or method body
368
+ def _extract_body_calls(body_nodes: List[ast.stmt]) -> Set[str]:
369
+ calls: Set[str] = set()
370
+ for b_node in body_nodes:
371
+ for sub in ast.walk(b_node):
372
+ if isinstance(sub, ast.Call):
373
+ if isinstance(sub.func, ast.Name):
374
+ calls.add(sub.func.id)
375
+ elif isinstance(sub.func, ast.Attribute):
376
+ calls.add(sub.func.attr)
377
+ return calls
378
+
379
+ # Traverse top-level nodes for symbol extraction
380
+ for node in tree.body:
381
+ if isinstance(node, ast.ClassDef):
382
+ class_sym, class_methods = self._parse_python_class(node)
383
+ symbols.append(class_sym)
384
+ symbols.extend(class_methods)
385
+ defined_names.add(node.name)
386
+ for m in node.body:
387
+ if isinstance(m, (ast.FunctionDef, ast.AsyncFunctionDef)):
388
+ full_name = f"{node.name}.{m.name}"
389
+ defined_names.add(m.name)
390
+ defined_names.add(full_name)
391
+ caller_callees[full_name].update(_extract_body_calls(m.body))
392
+ elif isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
393
+ func_sym = self._parse_python_func(node, parent_class=None)
394
+ symbols.append(func_sym)
395
+ defined_names.add(node.name)
396
+ caller_callees[node.name].update(_extract_body_calls(node.body))
397
+
398
+ file_info: Dict[str, Any] = {
399
+ "defined_names": defined_names,
400
+ "imported_names": imported_names,
401
+ "referenced_names": referenced_names,
402
+ "raw_imports": raw_imports,
403
+ "caller_callee": {k: sorted(v) for k, v in caller_callees.items()},
404
+ "line_count": len(source.splitlines()),
405
+ }
406
+
407
+ return symbols, file_info
408
+
409
+ def _parse_python_class(self, node: ast.ClassDef) -> Tuple[Dict[str, Any], List[Dict[str, Any]]]:
410
+ """Parses a Python ClassDef AST node into a class symbol and method symbols."""
411
+ class_name = node.name
412
+ lineno = node.lineno
413
+ end_lineno = getattr(node, "end_lineno", lineno)
414
+ docstring = ast.get_docstring(node)
415
+ bases = [ast.unparse(b) for b in node.bases]
416
+ decorators = [ast.unparse(d) for d in node.decorator_list]
417
+
418
+ bases_suffix = f"({', '.join(bases)})" if bases else ""
419
+ signature = f"class {class_name}{bases_suffix}:"
420
+
421
+ methods: List[Dict[str, Any]] = []
422
+ for item in node.body:
423
+ if isinstance(item, (ast.FunctionDef, ast.AsyncFunctionDef)):
424
+ method_sym = self._parse_python_func(item, parent_class=class_name)
425
+ methods.append(method_sym)
426
+
427
+ class_sym: Dict[str, Any] = {
428
+ "name": class_name,
429
+ "type": "class",
430
+ "parent": None,
431
+ "class_name": None,
432
+ "lineno": lineno,
433
+ "line_number": lineno,
434
+ "end_lineno": end_lineno,
435
+ "signature": signature,
436
+ "docstring": docstring,
437
+ "bases": bases,
438
+ "decorators": decorators,
439
+ "is_async": False,
440
+ "methods": methods,
441
+ }
442
+
443
+ return class_sym, methods
444
+
445
+ def _parse_python_func(
446
+ self,
447
+ node: ast.FunctionDef | ast.AsyncFunctionDef,
448
+ parent_class: Optional[str] = None,
449
+ ) -> Dict[str, Any]:
450
+ """Parses a Python FunctionDef or AsyncFunctionDef AST node."""
451
+ is_async = isinstance(node, ast.AsyncFunctionDef)
452
+ name = node.name
453
+ lineno = node.lineno
454
+ end_lineno = getattr(node, "end_lineno", lineno)
455
+ docstring = ast.get_docstring(node)
456
+ decorators = [ast.unparse(d) for d in node.decorator_list]
457
+
458
+ args_str = ast.unparse(node.args)
459
+ ret_str = f" -> {ast.unparse(node.returns)}" if node.returns else ""
460
+ prefix = "async def" if is_async else "def"
461
+ signature = f"{prefix} {name}({args_str}){ret_str}:"
462
+
463
+ all_args = [a.arg for a in node.args.posonlyargs + node.args.args + node.args.kwonlyargs]
464
+ return_type = ast.unparse(node.returns) if node.returns else None
465
+
466
+ node_type: str
467
+ if parent_class:
468
+ node_type = "async_method" if is_async else "method"
469
+ else:
470
+ node_type = "async_function" if is_async else "function"
471
+
472
+ return {
473
+ "name": name,
474
+ "type": node_type,
475
+ "parent": parent_class,
476
+ "class_name": parent_class,
477
+ "lineno": lineno,
478
+ "line_number": lineno,
479
+ "end_lineno": end_lineno,
480
+ "signature": signature,
481
+ "docstring": docstring,
482
+ "decorators": decorators,
483
+ "is_async": is_async,
484
+ "args": all_args,
485
+ "return_type": return_type,
486
+ }
487
+
488
+ # --------------------------------------------------------------------------
489
+ # JavaScript & TypeScript Parser
490
+ # --------------------------------------------------------------------------
491
+
492
+ def _parse_javascript_typescript(
493
+ self, source: str, str_path: str, ext: str
494
+ ) -> Tuple[List[Dict[str, Any]], Dict[str, Any]]:
495
+ """Parses JavaScript / TypeScript source for classes, interfaces, enums, functions, and imports."""
496
+ symbols: List[Dict[str, Any]] = []
497
+ defined_names: Set[str] = set()
498
+ imported_names: Set[str] = set()
499
+ referenced_names: Set[str] = set()
500
+ raw_imports: List[str] = []
501
+ caller_callees: Dict[str, Set[str]] = defaultdict(set)
502
+
503
+ lines = source.splitlines()
504
+
505
+ import_from_re = re.compile(r"""(?:import|export)\s+(?:(?:(?:\*\s+as\s+[\w$]+|[\w$,\s{}]+)\s+from\s+)?['"]([^'"]+)['"]|['"]([^'"]+)['"])""")
506
+ require_re = re.compile(r"""(?:const|let|var)\s+(?:[\w$,\s{}]+)\s*=\s*require\s*\(\s*['"]([^'"]+)['"]\s*\)""")
507
+
508
+ for line in lines:
509
+ for match in import_from_re.finditer(line):
510
+ mod = match.group(1) or match.group(2)
511
+ if mod:
512
+ raw_imports.append(mod)
513
+ imported_names.add(os.path.basename(mod).split(".")[0])
514
+ for match in require_re.finditer(line):
515
+ mod = match.group(1)
516
+ if mod:
517
+ raw_imports.append(mod)
518
+ imported_names.add(os.path.basename(mod).split(".")[0])
519
+
520
+ class_re = re.compile(
521
+ r"^\s*(?:export\s+)?(?:default\s+)?(?:abstract\s+)?class\s+([A-Za-z0-9_$]+)(?:<[^>]+>)?(?:\s+extends\s+([A-Za-z0-9_$.<>]+))?(?:\s+implements\s+([A-Za-z0-9_$,.<>\s]+))?"
522
+ )
523
+ interface_re = re.compile(
524
+ r"^\s*(?:export\s+)?interface\s+([A-Za-z0-9_$]+)(?:<[^>]+>)?(?:\s+extends\s+([^{]+))?"
525
+ )
526
+ enum_re = re.compile(r"^\s*(?:export\s+)?(?:const\s+)?enum\s+([A-Za-z0-9_$]+)")
527
+ type_alias_re = re.compile(r"^\s*(?:export\s+)?type\s+([A-Za-z0-9_$]+)(?:<[^>]+>)?\s*=")
528
+ func_re = re.compile(
529
+ r"^\s*(?:export\s+)?(?:default\s+)?(?:async\s+)?function\s*(?:\*\s*)?([A-Za-z0-9_$]+)\s*(?:<[^>]+>)?\s*\(([^)]*)\)(?:\s*:\s*([^{;]+))?"
530
+ )
531
+ arrow_func_re = re.compile(
532
+ r"^\s*(?:export\s+)?(?:const|let|var)\s+([A-Za-z0-9_$]+)\s*(?::\s*[^=]+)?\s*=\s*(?:async\s*)?(?:\(([^)]*)\)|([A-Za-z0-9_$]+))(?:\s*:\s*(.*?))?\s*=>"
533
+ )
534
+ method_re = re.compile(
535
+ r"^\s*(?:(?:public|private|protected|static|readonly|override|async)\s+)*(?:get\s+|set\s+)?([A-Za-z0-9_$]+)\s*(?:<[^>]+>)?\s*\(([^)]*)\)(?:\s*:\s*([^{;]+))?"
536
+ )
537
+
538
+ non_methods = {
539
+ "if", "for", "while", "switch", "catch", "return", "super", "require",
540
+ "import", "export", "typeof", "delete", "this", "new", "throw", "yield", "await"
541
+ }
542
+
543
+ current_class: Optional[Dict[str, Any]] = None
544
+ current_class_methods: List[Dict[str, Any]] = []
545
+ brace_depth = 0
546
+ class_brace_start = 0
547
+
548
+ for i, line in enumerate(lines, start=1):
549
+ trimmed = line.strip()
550
+ if not trimmed or trimmed.startswith("//") or trimmed.startswith("/*") or trimmed.startswith("*"):
551
+ continue
552
+
553
+ # Check for Class
554
+ m_class = class_re.match(line)
555
+ if m_class and not current_class:
556
+ name = m_class.group(1)
557
+ base = m_class.group(2)
558
+ impl = m_class.group(3)
559
+ bases = []
560
+ if base:
561
+ bases.append(base.strip())
562
+ if impl:
563
+ bases.extend([x.strip() for x in impl.split(",") if x.strip()])
564
+ bases_str = f" extends {base.strip()}" if base else ""
565
+ sig = f"class {name}{bases_str}:"
566
+ current_class = {
567
+ "name": name,
568
+ "type": "class",
569
+ "parent": None,
570
+ "class_name": None,
571
+ "lineno": i,
572
+ "line_number": i,
573
+ "end_lineno": i,
574
+ "signature": sig,
575
+ "docstring": None,
576
+ "bases": bases,
577
+ "decorators": [],
578
+ "is_async": False,
579
+ "methods": [],
580
+ }
581
+ defined_names.add(name)
582
+ current_class_methods = []
583
+ class_brace_start = brace_depth + 1
584
+ brace_depth += line.count("{") - line.count("}")
585
+ continue
586
+
587
+ # Check for Interface
588
+ m_iface = interface_re.match(line)
589
+ if m_iface and not current_class:
590
+ name = m_iface.group(1)
591
+ ext_str = m_iface.group(2)
592
+ bases = [x.strip() for x in ext_str.split(",") if x.strip()] if ext_str else []
593
+ sig = f"interface {name}:"
594
+ symbols.append({
595
+ "name": name,
596
+ "type": "interface",
597
+ "parent": None,
598
+ "class_name": None,
599
+ "lineno": i,
600
+ "line_number": i,
601
+ "end_lineno": i,
602
+ "signature": sig,
603
+ "docstring": None,
604
+ "bases": bases,
605
+ "decorators": [],
606
+ "is_async": False,
607
+ "methods": [],
608
+ })
609
+ defined_names.add(name)
610
+
611
+ # Check for Enum
612
+ m_enum = enum_re.match(line)
613
+ if m_enum and not current_class:
614
+ name = m_enum.group(1)
615
+ symbols.append({
616
+ "name": name,
617
+ "type": "enum",
618
+ "parent": None,
619
+ "class_name": None,
620
+ "lineno": i,
621
+ "line_number": i,
622
+ "end_lineno": i,
623
+ "signature": f"enum {name}:",
624
+ "docstring": None,
625
+ "bases": [],
626
+ "decorators": [],
627
+ "is_async": False,
628
+ "methods": [],
629
+ })
630
+ defined_names.add(name)
631
+
632
+ # Check for Type Alias
633
+ m_type = type_alias_re.match(line)
634
+ if m_type and not current_class:
635
+ name = m_type.group(1)
636
+ symbols.append({
637
+ "name": name,
638
+ "type": "type_alias",
639
+ "parent": None,
640
+ "class_name": None,
641
+ "lineno": i,
642
+ "line_number": i,
643
+ "end_lineno": i,
644
+ "signature": f"type {name} = ...",
645
+ "docstring": None,
646
+ "bases": [],
647
+ "decorators": [],
648
+ "is_async": False,
649
+ "methods": [],
650
+ })
651
+ defined_names.add(name)
652
+
653
+ # Check for Methods inside current class (only at class scope)
654
+ if current_class and brace_depth == class_brace_start:
655
+ m_meth = method_re.match(line)
656
+ if m_meth:
657
+ m_name = m_meth.group(1)
658
+ if m_name not in non_methods:
659
+ args_raw = m_meth.group(2) or ""
660
+ ret_raw = m_meth.group(3)
661
+ is_async = "async " in line[: line.find(m_name)]
662
+ ret_str = f" -> {ret_raw.strip()}" if ret_raw else ""
663
+ prefix = "async def" if is_async else "def"
664
+ sig = f"{prefix} {m_name}({args_raw.strip()}){ret_str}:"
665
+ method_sym = {
666
+ "name": m_name,
667
+ "type": "async_method" if is_async else "method",
668
+ "parent": current_class["name"],
669
+ "class_name": current_class["name"],
670
+ "lineno": i,
671
+ "line_number": i,
672
+ "end_lineno": i,
673
+ "signature": sig,
674
+ "docstring": None,
675
+ "decorators": [],
676
+ "is_async": is_async,
677
+ "args": [a.split(":")[0].strip() for a in args_raw.split(",") if a.strip()],
678
+ "return_type": ret_raw.strip() if ret_raw else None,
679
+ }
680
+ current_class_methods.append(method_sym)
681
+ defined_names.add(f"{current_class['name']}.{m_name}")
682
+ defined_names.add(m_name)
683
+
684
+ # Check for Top-Level Functions
685
+ if not current_class:
686
+ m_fn = func_re.match(line)
687
+ if m_fn:
688
+ name = m_fn.group(1)
689
+ args_raw = m_fn.group(2) or ""
690
+ ret_raw = m_fn.group(3)
691
+ is_async = "async " in line[: line.find(name)]
692
+ ret_str = f" -> {ret_raw.strip()}" if ret_raw else ""
693
+ prefix = "async def" if is_async else "def"
694
+ sig = f"{prefix} {name}({args_raw.strip()}){ret_str}:"
695
+ symbols.append({
696
+ "name": name,
697
+ "type": "async_function" if is_async else "function",
698
+ "parent": None,
699
+ "class_name": None,
700
+ "lineno": i,
701
+ "line_number": i,
702
+ "end_lineno": i,
703
+ "signature": sig,
704
+ "docstring": None,
705
+ "decorators": [],
706
+ "is_async": is_async,
707
+ "args": [a.split(":")[0].strip() for a in args_raw.split(",") if a.strip()],
708
+ "return_type": ret_raw.strip() if ret_raw else None,
709
+ })
710
+ defined_names.add(name)
711
+
712
+ # Check for Arrow Functions
713
+ m_arrow = arrow_func_re.match(line)
714
+ if m_arrow:
715
+ name = m_arrow.group(1)
716
+ args_raw = m_arrow.group(2) or m_arrow.group(3) or ""
717
+ ret_raw = m_arrow.group(4)
718
+ is_async = "async" in line[: line.find("=>")]
719
+ ret_str = f" -> {ret_raw.strip()}" if ret_raw else ""
720
+ prefix = "async def" if is_async else "def"
721
+ sig = f"{prefix} {name}({args_raw.strip()}){ret_str}:"
722
+ symbols.append({
723
+ "name": name,
724
+ "type": "async_function" if is_async else "function",
725
+ "parent": None,
726
+ "class_name": None,
727
+ "lineno": i,
728
+ "line_number": i,
729
+ "end_lineno": i,
730
+ "signature": sig,
731
+ "docstring": None,
732
+ "decorators": [],
733
+ "is_async": is_async,
734
+ "args": [a.split(":")[0].strip() for a in args_raw.split(",") if a.strip()],
735
+ "return_type": ret_raw.strip() if ret_raw else None,
736
+ })
737
+ defined_names.add(name)
738
+
739
+ # Update brace depth and check for class end
740
+ brace_depth += line.count("{") - line.count("}")
741
+ if current_class and brace_depth < class_brace_start:
742
+ current_class["end_lineno"] = i
743
+ current_class["methods"] = current_class_methods
744
+ symbols.append(current_class)
745
+ symbols.extend(current_class_methods)
746
+ current_class = None
747
+ current_class_methods = []
748
+
749
+ if current_class:
750
+ current_class["end_lineno"] = len(lines)
751
+ current_class["methods"] = current_class_methods
752
+ symbols.append(current_class)
753
+ symbols.extend(current_class_methods)
754
+
755
+ # Collect identifier references
756
+ ident_re = re.compile(r"\b([A-Za-z_$][A-Za-z0-9_$]*)\b")
757
+ for word in ident_re.findall(source):
758
+ referenced_names.add(word)
759
+
760
+ file_info = {
761
+ "defined_names": defined_names,
762
+ "imported_names": imported_names,
763
+ "referenced_names": referenced_names,
764
+ "raw_imports": raw_imports,
765
+ "caller_callee": {k: sorted(v) for k, v in caller_callees.items()},
766
+ "line_count": len(lines),
767
+ }
768
+
769
+ return symbols, file_info
770
+
771
+ # --------------------------------------------------------------------------
772
+ # Rust Parser
773
+ # --------------------------------------------------------------------------
774
+
775
+ def _parse_rust(self, source: str, str_path: str) -> Tuple[List[Dict[str, Any]], Dict[str, Any]]:
776
+ """Parses Rust source for structs, enums, traits, impls, functions, and use statements."""
777
+ symbols: List[Dict[str, Any]] = []
778
+ defined_names: Set[str] = set()
779
+ imported_names: Set[str] = set()
780
+ referenced_names: Set[str] = set()
781
+ raw_imports: List[str] = []
782
+ caller_callees: Dict[str, Set[str]] = defaultdict(set)
783
+
784
+ lines = source.splitlines()
785
+
786
+ use_re = re.compile(r"^\s*use\s+([^;]+);")
787
+ mod_re = re.compile(r"^\s*(?:pub(?:\([^)]+\))?\s+)?mod\s+([A-Za-z0-9_]+);")
788
+
789
+ for line in lines:
790
+ m_use = use_re.match(line)
791
+ if m_use:
792
+ raw_imports.append(m_use.group(1).strip())
793
+ imported_names.add(m_use.group(1).split("::")[-1].strip())
794
+ m_mod = mod_re.match(line)
795
+ if m_mod:
796
+ raw_imports.append(m_mod.group(1).strip())
797
+ imported_names.add(m_mod.group(1).strip())
798
+
799
+ struct_re = re.compile(r"^\s*(?:pub(?:\([^)]+\))?\s+)?struct\s+([A-Za-z0-9_]+)(?:<[^>]+>)?")
800
+ enum_re = re.compile(r"^\s*(?:pub(?:\([^)]+\))?\s+)?enum\s+([A-Za-z0-9_]+)(?:<[^>]+>)?")
801
+ trait_re = re.compile(r"^\s*(?:pub(?:\([^)]+\))?\s+)?trait\s+([A-Za-z0-9_]+)(?:<[^>]+>)?")
802
+ impl_re = re.compile(r"^\s*impl(?:<[^>]+>)?\s+(?:([A-Za-z0-9_:]+(?:<[^>]+>)?)\s+for\s+)?([A-Za-z0-9_:]+(?:<[^>]+>)?)(?:\s+where\s+[^{]+)?\s*\{")
803
+ fn_re = re.compile(r"^\s*(?:pub(?:\([^)]+\))?\s+)?(?:async\s+)?(?:unsafe\s+)?(?:extern(?:\s+\"[^\"]+\")?\s+)?(?:const\s+)?fn\s+([A-Za-z0-9_]+)(?:<[^>]+>)?\s*\(([^)]*)\)(?:\s*->\s*([^{;]+))?")
804
+
805
+ current_impl_target: Optional[str] = None
806
+ brace_depth = 0
807
+ impl_brace_start = 0
808
+
809
+ struct_map: Dict[str, Dict[str, Any]] = {}
810
+
811
+ for i, line in enumerate(lines, start=1):
812
+ trimmed = line.strip()
813
+ if not trimmed or trimmed.startswith("//") or trimmed.startswith("/*") or trimmed.startswith("*"):
814
+ continue
815
+
816
+ # Struct
817
+ m_struct = struct_re.match(line)
818
+ if m_struct:
819
+ name = m_struct.group(1)
820
+ struct_sym = {
821
+ "name": name,
822
+ "type": "struct",
823
+ "parent": None,
824
+ "class_name": None,
825
+ "lineno": i,
826
+ "line_number": i,
827
+ "end_lineno": i,
828
+ "signature": f"struct {name}:",
829
+ "docstring": None,
830
+ "bases": [],
831
+ "decorators": [],
832
+ "is_async": False,
833
+ "methods": [],
834
+ }
835
+ symbols.append(struct_sym)
836
+ struct_map[name] = struct_sym
837
+ defined_names.add(name)
838
+
839
+ # Enum
840
+ m_enum = enum_re.match(line)
841
+ if m_enum:
842
+ name = m_enum.group(1)
843
+ symbols.append({
844
+ "name": name,
845
+ "type": "enum",
846
+ "parent": None,
847
+ "class_name": None,
848
+ "lineno": i,
849
+ "line_number": i,
850
+ "end_lineno": i,
851
+ "signature": f"enum {name}:",
852
+ "docstring": None,
853
+ "bases": [],
854
+ "decorators": [],
855
+ "is_async": False,
856
+ "methods": [],
857
+ })
858
+ defined_names.add(name)
859
+
860
+ # Trait
861
+ m_trait = trait_re.match(line)
862
+ if m_trait:
863
+ name = m_trait.group(1)
864
+ symbols.append({
865
+ "name": name,
866
+ "type": "trait",
867
+ "parent": None,
868
+ "class_name": None,
869
+ "lineno": i,
870
+ "line_number": i,
871
+ "end_lineno": i,
872
+ "signature": f"trait {name}:",
873
+ "docstring": None,
874
+ "bases": [],
875
+ "decorators": [],
876
+ "is_async": False,
877
+ "methods": [],
878
+ })
879
+ defined_names.add(name)
880
+
881
+ # Impl Block
882
+ m_impl = impl_re.match(line)
883
+ if m_impl:
884
+ trait_name = m_impl.group(1)
885
+ target_type = m_impl.group(2).split("<")[0].strip()
886
+ current_impl_target = target_type
887
+ impl_brace_start = brace_depth + line.count("{") - line.count("}")
888
+
889
+ # Function / Method
890
+ m_fn = fn_re.match(line)
891
+ if m_fn:
892
+ name = m_fn.group(1)
893
+ args_raw = m_fn.group(2) or ""
894
+ ret_raw = m_fn.group(3)
895
+ is_async = "async " in line[: line.find(name)]
896
+ ret_str = f" -> {ret_raw.strip()}" if ret_raw else ""
897
+ prefix = "async fn" if is_async else "fn"
898
+ sig = f"{prefix} {name}({args_raw.strip()}){ret_str}:"
899
+
900
+ if current_impl_target:
901
+ # Method inside impl
902
+ method_sym = {
903
+ "name": name,
904
+ "type": "async_method" if is_async else "method",
905
+ "parent": current_impl_target,
906
+ "class_name": current_impl_target,
907
+ "lineno": i,
908
+ "line_number": i,
909
+ "end_lineno": i,
910
+ "signature": sig,
911
+ "docstring": None,
912
+ "decorators": [],
913
+ "is_async": is_async,
914
+ "args": [a.split(":")[0].strip() for a in args_raw.split(",") if a.strip()],
915
+ "return_type": ret_raw.strip() if ret_raw else None,
916
+ }
917
+ symbols.append(method_sym)
918
+ if current_impl_target in struct_map:
919
+ struct_map[current_impl_target]["methods"].append(method_sym)
920
+ defined_names.add(f"{current_impl_target}.{name}")
921
+ defined_names.add(name)
922
+ else:
923
+ # Free Function
924
+ symbols.append({
925
+ "name": name,
926
+ "type": "async_function" if is_async else "function",
927
+ "parent": None,
928
+ "class_name": None,
929
+ "lineno": i,
930
+ "line_number": i,
931
+ "end_lineno": i,
932
+ "signature": sig,
933
+ "docstring": None,
934
+ "decorators": [],
935
+ "is_async": is_async,
936
+ "args": [a.split(":")[0].strip() for a in args_raw.split(",") if a.strip()],
937
+ "return_type": ret_raw.strip() if ret_raw else None,
938
+ })
939
+ defined_names.add(name)
940
+
941
+ brace_depth += line.count("{") - line.count("}")
942
+ if current_impl_target and brace_depth < impl_brace_start:
943
+ current_impl_target = None
944
+
945
+ # Identifiers
946
+ ident_re = re.compile(r"\b([A-Za-z_][A-Za-z0-9_]*)\b")
947
+ for word in ident_re.findall(source):
948
+ referenced_names.add(word)
949
+
950
+ file_info = {
951
+ "defined_names": defined_names,
952
+ "imported_names": imported_names,
953
+ "referenced_names": referenced_names,
954
+ "raw_imports": raw_imports,
955
+ "caller_callee": {k: sorted(v) for k, v in caller_callees.items()},
956
+ "line_count": len(lines),
957
+ }
958
+
959
+ return symbols, file_info
960
+
961
+ # --------------------------------------------------------------------------
962
+ # Go Parser
963
+ # --------------------------------------------------------------------------
964
+
965
+ def _parse_go(self, source: str, str_path: str) -> Tuple[List[Dict[str, Any]], Dict[str, Any]]:
966
+ """Parses Go source for structs, interfaces, methods, functions, and imports."""
967
+ symbols: List[Dict[str, Any]] = []
968
+ defined_names: Set[str] = set()
969
+ imported_names: Set[str] = set()
970
+ referenced_names: Set[str] = set()
971
+ raw_imports: List[str] = []
972
+ caller_callees: Dict[str, Set[str]] = defaultdict(set)
973
+
974
+ lines = source.splitlines()
975
+
976
+ in_import_block = False
977
+ import_line_re = re.compile(r'^\s*(?:[A-Za-z0-9_.]+\s+)?"([^"]+)"')
978
+
979
+ for line in lines:
980
+ trimmed = line.strip()
981
+ if trimmed.startswith("import ("):
982
+ in_import_block = True
983
+ continue
984
+ if in_import_block:
985
+ if trimmed.startswith(")"):
986
+ in_import_block = False
987
+ else:
988
+ m = import_line_re.match(trimmed)
989
+ if m:
990
+ pkg = m.group(1)
991
+ raw_imports.append(pkg)
992
+ imported_names.add(os.path.basename(pkg))
993
+ elif trimmed.startswith("import "):
994
+ m = import_line_re.match(trimmed[7:].strip())
995
+ if m:
996
+ pkg = m.group(1)
997
+ raw_imports.append(pkg)
998
+ imported_names.add(os.path.basename(pkg))
999
+
1000
+ struct_re = re.compile(r"^\s*type\s+([A-Za-z0-9_]+)\s+struct\s*\{")
1001
+ iface_re = re.compile(r"^\s*type\s+([A-Za-z0-9_]+)\s+interface\s*\{")
1002
+ type_re = re.compile(r"^\s*type\s+([A-Za-z0-9_]+)\s+([^{;=]+)")
1003
+ method_re = re.compile(
1004
+ r"^\s*func\s*\(\s*(?:[A-Za-z0-9_]+\s+)?\*?([A-Za-z0-9_]+)\s*\)\s*([A-Za-z0-9_]+)\s*\(([^)]*)\)(?:\s*(?:\(([^)]*)\)|([^{;]+)))?"
1005
+ )
1006
+ func_re = re.compile(
1007
+ r"^\s*func\s+([A-Za-z0-9_]+)\s*\(([^)]*)\)(?:\s*(?:\(([^)]*)\)|([^{;]+)))?"
1008
+ )
1009
+
1010
+ struct_map: Dict[str, Dict[str, Any]] = {}
1011
+
1012
+ for i, line in enumerate(lines, start=1):
1013
+ trimmed = line.strip()
1014
+ if not trimmed or trimmed.startswith("//") or trimmed.startswith("/*"):
1015
+ continue
1016
+
1017
+ # Struct
1018
+ m_struct = struct_re.match(line)
1019
+ if m_struct:
1020
+ name = m_struct.group(1)
1021
+ struct_sym = {
1022
+ "name": name,
1023
+ "type": "struct",
1024
+ "parent": None,
1025
+ "class_name": None,
1026
+ "lineno": i,
1027
+ "line_number": i,
1028
+ "end_lineno": i,
1029
+ "signature": f"type {name} struct:",
1030
+ "docstring": None,
1031
+ "bases": [],
1032
+ "decorators": [],
1033
+ "is_async": False,
1034
+ "methods": [],
1035
+ }
1036
+ symbols.append(struct_sym)
1037
+ struct_map[name] = struct_sym
1038
+ defined_names.add(name)
1039
+ continue
1040
+
1041
+ # Interface
1042
+ m_iface = iface_re.match(line)
1043
+ if m_iface:
1044
+ name = m_iface.group(1)
1045
+ symbols.append({
1046
+ "name": name,
1047
+ "type": "interface",
1048
+ "parent": None,
1049
+ "class_name": None,
1050
+ "lineno": i,
1051
+ "line_number": i,
1052
+ "end_lineno": i,
1053
+ "signature": f"type {name} interface:",
1054
+ "docstring": None,
1055
+ "bases": [],
1056
+ "decorators": [],
1057
+ "is_async": False,
1058
+ "methods": [],
1059
+ })
1060
+ defined_names.add(name)
1061
+ continue
1062
+
1063
+ # Method (with receiver)
1064
+ m_meth = method_re.match(line)
1065
+ if m_meth:
1066
+ recv = m_meth.group(1)
1067
+ name = m_meth.group(2)
1068
+ args_raw = m_meth.group(3) or ""
1069
+ ret_raw = m_meth.group(4) or m_meth.group(5)
1070
+ ret_str = f" -> {ret_raw.strip()}" if ret_raw else ""
1071
+ sig = f"func (r *{recv}) {name}({args_raw.strip()}){ret_str}:"
1072
+ method_sym = {
1073
+ "name": name,
1074
+ "type": "method",
1075
+ "parent": recv,
1076
+ "class_name": recv,
1077
+ "lineno": i,
1078
+ "line_number": i,
1079
+ "end_lineno": i,
1080
+ "signature": sig,
1081
+ "docstring": None,
1082
+ "decorators": [],
1083
+ "is_async": False,
1084
+ "args": [a.split()[0].strip() for a in args_raw.split(",") if a.strip()],
1085
+ "return_type": ret_raw.strip() if ret_raw else None,
1086
+ }
1087
+ symbols.append(method_sym)
1088
+ if recv in struct_map:
1089
+ struct_map[recv]["methods"].append(method_sym)
1090
+ defined_names.add(f"{recv}.{name}")
1091
+ defined_names.add(name)
1092
+ continue
1093
+
1094
+ # Function
1095
+ m_fn = func_re.match(line)
1096
+ if m_fn:
1097
+ name = m_fn.group(1)
1098
+ args_raw = m_fn.group(2) or ""
1099
+ ret_raw = m_fn.group(3) or m_fn.group(4)
1100
+ ret_str = f" -> {ret_raw.strip()}" if ret_raw else ""
1101
+ sig = f"func {name}({args_raw.strip()}){ret_str}:"
1102
+ symbols.append({
1103
+ "name": name,
1104
+ "type": "function",
1105
+ "parent": None,
1106
+ "class_name": None,
1107
+ "lineno": i,
1108
+ "line_number": i,
1109
+ "end_lineno": i,
1110
+ "signature": sig,
1111
+ "docstring": None,
1112
+ "decorators": [],
1113
+ "is_async": False,
1114
+ "args": [a.split()[0].strip() for a in args_raw.split(",") if a.strip()],
1115
+ "return_type": ret_raw.strip() if ret_raw else None,
1116
+ })
1117
+ defined_names.add(name)
1118
+
1119
+ # Identifiers
1120
+ ident_re = re.compile(r"\b([A-Za-z_][A-Za-z0-9_]*)\b")
1121
+ for word in ident_re.findall(source):
1122
+ referenced_names.add(word)
1123
+
1124
+ file_info = {
1125
+ "defined_names": defined_names,
1126
+ "imported_names": imported_names,
1127
+ "referenced_names": referenced_names,
1128
+ "raw_imports": raw_imports,
1129
+ "caller_callee": {k: sorted(v) for k, v in caller_callees.items()},
1130
+ "line_count": len(lines),
1131
+ }
1132
+
1133
+ return symbols, file_info
1134
+
1135
+ # --------------------------------------------------------------------------
1136
+ # C / C++ Parser
1137
+ # --------------------------------------------------------------------------
1138
+
1139
+ def _parse_cpp(self, source: str, str_path: str) -> Tuple[List[Dict[str, Any]], Dict[str, Any]]:
1140
+ """Parses C/C++ source for classes, structs, enums, functions, and includes."""
1141
+ symbols: List[Dict[str, Any]] = []
1142
+ defined_names: Set[str] = set()
1143
+ imported_names: Set[str] = set()
1144
+ referenced_names: Set[str] = set()
1145
+ raw_imports: List[str] = []
1146
+ caller_callees: Dict[str, Set[str]] = defaultdict(set)
1147
+
1148
+ lines = source.splitlines()
1149
+
1150
+ include_re = re.compile(r'^\s*#include\s*["<]([^">]+)[">]')
1151
+ for line in lines:
1152
+ m_inc = include_re.match(line)
1153
+ if m_inc:
1154
+ inc_target = m_inc.group(1)
1155
+ raw_imports.append(inc_target)
1156
+ imported_names.add(os.path.basename(inc_target).split(".")[0])
1157
+
1158
+ class_re = re.compile(
1159
+ r"^\s*(?:template\s*<[^>]+>\s*)?(class|struct)\s+([A-Za-z0-9_]+)(?:\s*:\s*([^{;]+))?\s*\{"
1160
+ )
1161
+ enum_re = re.compile(
1162
+ r"^\s*enum\s+(?:class\s+|struct\s+)?([A-Za-z0-9_]+)(?:\s*:\s*[^{;]+)?\s*\{"
1163
+ )
1164
+ fn_re = re.compile(
1165
+ r"^\s*(?:template\s*<[^>]+>\s*)?(?:(?:static|virtual|inline|explicit|constexpr|friend|extern)\s+)*([A-Za-z0-9_<>:~&*]+)\s+([A-Za-z0-9_~]+)\s*\(([^)]*)\)(?:\s*const)?(?:\s*override|\s*final|\s*noexcept)?(?:\s*=\s*0)?\s*[{;]"
1166
+ )
1167
+
1168
+ current_class: Optional[Dict[str, Any]] = None
1169
+ current_class_methods: List[Dict[str, Any]] = []
1170
+ brace_depth = 0
1171
+ class_brace_start = 0
1172
+
1173
+ for i, line in enumerate(lines, start=1):
1174
+ trimmed = line.strip()
1175
+ if not trimmed or trimmed.startswith("//") or trimmed.startswith("/*") or trimmed.startswith("*") or trimmed.startswith("#"):
1176
+ continue
1177
+
1178
+ # Class / Struct
1179
+ m_cls = class_re.match(line)
1180
+ if m_cls and not current_class:
1181
+ kind = m_cls.group(1)
1182
+ name = m_cls.group(2)
1183
+ bases_raw = m_cls.group(3)
1184
+ bases = [b.strip().split()[-1] for b in bases_raw.split(",") if b.strip()] if bases_raw else []
1185
+ bases_str = f"({', '.join(bases)})" if bases else ""
1186
+ sig = f"{kind} {name}{bases_str}:"
1187
+ current_class = {
1188
+ "name": name,
1189
+ "type": "class" if kind == "class" else "struct",
1190
+ "parent": None,
1191
+ "class_name": None,
1192
+ "lineno": i,
1193
+ "line_number": i,
1194
+ "end_lineno": i,
1195
+ "signature": sig,
1196
+ "docstring": None,
1197
+ "bases": bases,
1198
+ "decorators": [],
1199
+ "is_async": False,
1200
+ "methods": [],
1201
+ }
1202
+ defined_names.add(name)
1203
+ current_class_methods = []
1204
+ class_brace_start = brace_depth + line.count("{") - line.count("}")
1205
+ brace_depth += line.count("{") - line.count("}")
1206
+ continue
1207
+
1208
+ # Enum
1209
+ m_enum = enum_re.match(line)
1210
+ if m_enum and not current_class:
1211
+ name = m_enum.group(1)
1212
+ symbols.append({
1213
+ "name": name,
1214
+ "type": "enum",
1215
+ "parent": None,
1216
+ "class_name": None,
1217
+ "lineno": i,
1218
+ "line_number": i,
1219
+ "end_lineno": i,
1220
+ "signature": f"enum {name}:",
1221
+ "docstring": None,
1222
+ "bases": [],
1223
+ "decorators": [],
1224
+ "is_async": False,
1225
+ "methods": [],
1226
+ })
1227
+ defined_names.add(name)
1228
+
1229
+ # Function / Method
1230
+ m_fn = fn_re.match(line)
1231
+ if m_fn:
1232
+ ret_type = m_fn.group(1)
1233
+ name = m_fn.group(2)
1234
+ args_raw = m_fn.group(3) or ""
1235
+ if name not in ("if", "for", "while", "switch", "catch", "return") and ret_type not in ("return", "else", "new"):
1236
+ sig = f"{ret_type} {name}({args_raw.strip()}):"
1237
+ if current_class:
1238
+ method_sym = {
1239
+ "name": name,
1240
+ "type": "method",
1241
+ "parent": current_class["name"],
1242
+ "class_name": current_class["name"],
1243
+ "lineno": i,
1244
+ "line_number": i,
1245
+ "end_lineno": i,
1246
+ "signature": sig,
1247
+ "docstring": None,
1248
+ "decorators": [],
1249
+ "is_async": False,
1250
+ "args": [a.split()[-1].lstrip("*&").strip() for a in args_raw.split(",") if a.strip()],
1251
+ "return_type": ret_type,
1252
+ }
1253
+ current_class_methods.append(method_sym)
1254
+ defined_names.add(f"{current_class['name']}.{name}")
1255
+ defined_names.add(name)
1256
+ else:
1257
+ symbols.append({
1258
+ "name": name,
1259
+ "type": "function",
1260
+ "parent": None,
1261
+ "class_name": None,
1262
+ "lineno": i,
1263
+ "line_number": i,
1264
+ "end_lineno": i,
1265
+ "signature": sig,
1266
+ "docstring": None,
1267
+ "decorators": [],
1268
+ "is_async": False,
1269
+ "args": [a.split()[-1].lstrip("*&").strip() for a in args_raw.split(",") if a.strip()],
1270
+ "return_type": ret_type,
1271
+ })
1272
+ defined_names.add(name)
1273
+
1274
+ brace_depth += line.count("{") - line.count("}")
1275
+ if current_class and brace_depth <= class_brace_start - 1:
1276
+ current_class["end_lineno"] = i
1277
+ current_class["methods"] = current_class_methods
1278
+ symbols.append(current_class)
1279
+ symbols.extend(current_class_methods)
1280
+ current_class = None
1281
+ current_class_methods = []
1282
+
1283
+ if current_class:
1284
+ current_class["end_lineno"] = len(lines)
1285
+ current_class["methods"] = current_class_methods
1286
+ symbols.append(current_class)
1287
+ symbols.extend(current_class_methods)
1288
+
1289
+ ident_re = re.compile(r"\b([A-Za-z_][A-Za-z0-9_]*)\b")
1290
+ for word in ident_re.findall(source):
1291
+ referenced_names.add(word)
1292
+
1293
+ file_info = {
1294
+ "defined_names": defined_names,
1295
+ "imported_names": imported_names,
1296
+ "referenced_names": referenced_names,
1297
+ "raw_imports": raw_imports,
1298
+ "caller_callee": {k: sorted(v) for k, v in caller_callees.items()},
1299
+ "line_count": len(lines),
1300
+ }
1301
+
1302
+ return symbols, file_info
1303
+
1304
+ # ==========================================================================
1305
+ # Dependency Graph & Topological Analysis
1306
+ # ==========================================================================
1307
+
1308
+ def _resolve_relative_path(self, abs_path: str) -> str:
1309
+ """Converts an absolute path to a normalized relative workspace path."""
1310
+ try:
1311
+ return os.path.relpath(abs_path, str(self.root_dir)).replace("\\", "/")
1312
+ except ValueError:
1313
+ return abs_path.replace("\\", "/")
1314
+
1315
+ def _build_workspace_metadata(self) -> List[Dict[str, Any]]:
1316
+ """Scans workspace and gathers symbols and metadata for all supported files."""
1317
+ files = self.scan_workspace_files()
1318
+ file_data: List[Dict[str, Any]] = []
1319
+
1320
+ for fpath in files:
1321
+ rel_path = self._resolve_relative_path(fpath)
1322
+ symbols, info = self._extract_file_info(fpath)
1323
+ file_data.append({
1324
+ "full_path": fpath,
1325
+ "rel_path": rel_path,
1326
+ "symbols": symbols,
1327
+ "info": info,
1328
+ })
1329
+
1330
+ return file_data
1331
+
1332
+ def get_dependency_graph(self) -> Dict[str, List[str]]:
1333
+ """
1334
+ Builds the workspace forward dependency graph (file -> list of files it imports).
1335
+
1336
+ Returns:
1337
+ Dictionary mapping relative file paths to lists of imported relative file paths.
1338
+ """
1339
+ file_data = self._build_workspace_metadata()
1340
+ all_rel_paths = {item["rel_path"] for item in file_data}
1341
+
1342
+ # Build module resolution index
1343
+ module_index: Dict[str, str] = {}
1344
+ for rel in all_rel_paths:
1345
+ base, _ = os.path.splitext(rel)
1346
+ module_index[rel] = rel
1347
+ module_index[base] = rel
1348
+ module_index[base.replace("/", ".")] = rel
1349
+ module_index[os.path.basename(base)] = rel
1350
+
1351
+ dep_graph: Dict[str, Set[str]] = {item["rel_path"]: set() for item in file_data}
1352
+
1353
+ for item in file_data:
1354
+ src_rel = item["rel_path"]
1355
+ src_dir = os.path.dirname(src_rel)
1356
+ raw_imports = item["info"].get("raw_imports", [])
1357
+ imported_names = item["info"].get("imported_names", set())
1358
+
1359
+ # 1. Resolve raw import strings
1360
+ for raw in raw_imports:
1361
+ clean_raw = raw.strip().lstrip("./").replace("\\", "/")
1362
+ # Try relative to src_dir
1363
+ rel_target = os.path.normpath(os.path.join(src_dir, clean_raw)).replace("\\", "/")
1364
+ resolved = None
1365
+ if rel_target in module_index:
1366
+ resolved = module_index[rel_target]
1367
+ elif clean_raw in module_index:
1368
+ resolved = module_index[clean_raw]
1369
+ elif raw.replace(".", "/") in module_index:
1370
+ resolved = module_index[raw.replace(".", "/")]
1371
+
1372
+ if resolved and resolved != src_rel and resolved in all_rel_paths:
1373
+ dep_graph[src_rel].add(resolved)
1374
+
1375
+ # 2. Resolve cross-file referenced symbols if not already captured
1376
+ for other in file_data:
1377
+ other_rel = other["rel_path"]
1378
+ if other_rel == src_rel:
1379
+ continue
1380
+ other_defs = other["info"].get("defined_names", set())
1381
+ if other_defs & imported_names:
1382
+ dep_graph[src_rel].add(other_rel)
1383
+
1384
+ return {k: sorted(v) for k, v in dep_graph.items()}
1385
+
1386
+ def get_reverse_dependency_graph(self) -> Dict[str, List[str]]:
1387
+ """
1388
+ Builds the workspace reverse dependency graph (file -> list of files that depend on it).
1389
+
1390
+ Returns:
1391
+ Dictionary mapping relative file paths to lists of dependent relative file paths.
1392
+ """
1393
+ dep_graph = self.get_dependency_graph()
1394
+ rev_graph: Dict[str, Set[str]] = {k: set() for k in dep_graph}
1395
+
1396
+ for src, targets in dep_graph.items():
1397
+ for tgt in targets:
1398
+ if tgt in rev_graph:
1399
+ rev_graph[tgt].add(src)
1400
+
1401
+ return {k: sorted(v) for k, v in rev_graph.items()}
1402
+
1403
+ def get_import_tree(self, file_path: Optional[str] = None) -> Dict[str, Any]:
1404
+ """
1405
+ Returns the import tree for a specific file or the whole workspace.
1406
+ """
1407
+ dep_graph = self.get_dependency_graph()
1408
+ if file_path:
1409
+ norm = self._resolve_relative_path(file_path)
1410
+ return {norm: dep_graph.get(norm, [])}
1411
+ return dep_graph
1412
+
1413
+ def get_caller_callee_map(self) -> Dict[str, List[str]]:
1414
+ """
1415
+ Builds a mapping of functions/methods to the list of symbols they call.
1416
+
1417
+ Returns:
1418
+ Dictionary mapping caller symbol name to list of callee symbol names.
1419
+ """
1420
+ file_data = self._build_workspace_metadata()
1421
+ all_callers: Dict[str, Set[str]] = defaultdict(set)
1422
+
1423
+ for item in file_data:
1424
+ cc = item["info"].get("caller_callee", {})
1425
+ for caller, callees in cc.items():
1426
+ all_callers[caller].update(callees)
1427
+
1428
+ return {k: sorted(v) for k, v in all_callers.items()}
1429
+
1430
+ def detect_cyclic_imports(self) -> List[List[str]]:
1431
+ """
1432
+ Detects circular/cyclic import dependencies across workspace files.
1433
+
1434
+ Returns:
1435
+ List of detected cycles represented as lists of relative file paths.
1436
+ """
1437
+ dep_graph = self.get_dependency_graph()
1438
+ visited: Dict[str, int] = {} # 0: unvisited, 1: visiting, 2: visited
1439
+ cycles: List[List[str]] = []
1440
+
1441
+ def dfs(node: str, path: List[str]):
1442
+ visited[node] = 1
1443
+ path.append(node)
1444
+
1445
+ for neighbor in dep_graph.get(node, []):
1446
+ if visited.get(neighbor, 0) == 1:
1447
+ # Cycle found!
1448
+ try:
1449
+ idx = path.index(neighbor)
1450
+ cycle = path[idx:] + [neighbor]
1451
+ cycles.append(cycle)
1452
+ except ValueError:
1453
+ pass
1454
+ elif visited.get(neighbor, 0) == 0:
1455
+ dfs(neighbor, path)
1456
+
1457
+ path.pop()
1458
+ visited[node] = 2
1459
+
1460
+ for node in dep_graph:
1461
+ if visited.get(node, 0) == 0:
1462
+ dfs(node, [])
1463
+
1464
+ # Deduplicate and normalize cycles
1465
+ unique_cycles: List[List[str]] = []
1466
+ seen_cycle_keys: Set[str] = set()
1467
+
1468
+ for c in cycles:
1469
+ inner = c[:-1]
1470
+ if not inner:
1471
+ continue
1472
+ min_idx = inner.index(min(inner))
1473
+ canonical = inner[min_idx:] + inner[:min_idx]
1474
+ key = "->".join(canonical)
1475
+ if key not in seen_cycle_keys:
1476
+ seen_cycle_keys.add(key)
1477
+ unique_cycles.append(canonical + [canonical[0]])
1478
+
1479
+ return unique_cycles
1480
+
1481
+ def get_symbol_references(self, symbol_name: str) -> List[str]:
1482
+ """
1483
+ Finds all workspace files referencing a given symbol.
1484
+ """
1485
+ file_data = self._build_workspace_metadata()
1486
+ referencing_files: List[str] = []
1487
+
1488
+ for item in file_data:
1489
+ ref_names = item["info"].get("referenced_names", set())
1490
+ imp_names = item["info"].get("imported_names", set())
1491
+ if symbol_name in ref_names or symbol_name in imp_names:
1492
+ referencing_files.append(item["rel_path"])
1493
+
1494
+ referencing_files.sort()
1495
+ return referencing_files
1496
+
1497
+ # ==========================================================================
1498
+ # Map Generation & Token Budgeting
1499
+ # ==========================================================================
1500
+
1501
+ def _matches_focus(self, rel_path: str, full_path: str, focus_files: Optional[List[str]]) -> bool:
1502
+ """Checks if a relative or absolute file path matches any focus file pattern."""
1503
+ if not focus_files:
1504
+ return False
1505
+ rel_norm = rel_path.replace("\\", "/").strip()
1506
+ full_norm = full_path.replace("\\", "/").strip()
1507
+ base_name = os.path.basename(rel_norm)
1508
+
1509
+ for f in focus_files:
1510
+ f_norm = f.replace("\\", "/").strip()
1511
+ if not f_norm:
1512
+ continue
1513
+ if (
1514
+ rel_norm == f_norm
1515
+ or rel_norm.endswith("/" + f_norm)
1516
+ or base_name == f_norm
1517
+ or full_norm == f_norm
1518
+ or full_norm.endswith("/" + f_norm)
1519
+ ):
1520
+ return True
1521
+ return False
1522
+
1523
+ def get_repo_map(
1524
+ self,
1525
+ max_tokens: int = 400,
1526
+ focus_files: Optional[List[str]] = None,
1527
+ ) -> str:
1528
+ """
1529
+ Generates a compact hierarchical tree view of workspace symbols
1530
+ bounded strictly to < max_tokens words/tokens.
1531
+
1532
+ Prioritizes focus files and architecturally significant symbols.
1533
+
1534
+ Args:
1535
+ max_tokens: Maximum token/word budget for the returned map text.
1536
+ focus_files: Optional list of file paths to prioritize.
1537
+
1538
+ Returns:
1539
+ Compact string representation of the codebase map.
1540
+ """
1541
+ if max_tokens <= 0:
1542
+ return ""
1543
+
1544
+ file_data = self._build_workspace_metadata()
1545
+ if not file_data:
1546
+ return ""
1547
+
1548
+ # Score and rank files
1549
+ rev_graph = self.get_reverse_dependency_graph()
1550
+
1551
+ for item in file_data:
1552
+ rel_path = item["rel_path"]
1553
+ full_path = item["full_path"]
1554
+ score = 0.0
1555
+
1556
+ # 1. Focus file priority boost (+10000)
1557
+ if self._matches_focus(rel_path, full_path, focus_files):
1558
+ score += 10000.0
1559
+
1560
+ # 2. In-degree / Cross-file references score
1561
+ dependents = rev_graph.get(rel_path, [])
1562
+ score += len(dependents) * 20.0
1563
+
1564
+ # 3. Architectural significance
1565
+ base_name = os.path.basename(rel_path)
1566
+ if base_name in {
1567
+ "main.py",
1568
+ "app.py",
1569
+ "cli.py",
1570
+ "orchestrator.py",
1571
+ "service.py",
1572
+ "models.py",
1573
+ "core.py",
1574
+ "verifier.py",
1575
+ "llm_driver.py",
1576
+ "index.ts",
1577
+ "main.rs",
1578
+ "main.go",
1579
+ "main.cpp",
1580
+ }:
1581
+ score += 10.0
1582
+ elif base_name in {"__init__.py", "mod.rs"}:
1583
+ score += 2.0
1584
+
1585
+ # 4. Symbol count contribution
1586
+ score += len(item["symbols"]) * 1.5
1587
+
1588
+ # 5. Path depth penalty (shallow files ranked slightly higher)
1589
+ depth = rel_path.count("/")
1590
+ score -= depth * 0.5
1591
+
1592
+ item["score"] = score
1593
+
1594
+ # Sort files by descending importance score
1595
+ file_data.sort(key=lambda x: (-x["score"], x["rel_path"]))
1596
+
1597
+ # Build hierarchical representation respecting token budget
1598
+ return self._render_tree(file_data, max_tokens)
1599
+
1600
+ def _render_tree(self, file_data: List[Dict[str, Any]], max_tokens: int) -> str:
1601
+ """
1602
+ Renders a compact hierarchical tree string from sorted file metadata,
1603
+ strictly enforcing the token budget.
1604
+ """
1605
+ output_lines: List[str] = []
1606
+ current_words = 0
1607
+
1608
+ for item in file_data:
1609
+ symbols = item["symbols"]
1610
+ if not symbols:
1611
+ continue
1612
+
1613
+ rel_path = item["rel_path"]
1614
+ file_header = f"{rel_path}:"
1615
+ header_words = len(file_header.split())
1616
+
1617
+ if current_words + header_words > max_tokens:
1618
+ break
1619
+
1620
+ file_lines: List[str] = [file_header]
1621
+
1622
+ classes: List[Dict[str, Any]] = [
1623
+ s for s in symbols if s.get("type") in ("class", "struct", "interface", "trait", "enum")
1624
+ ]
1625
+ top_funcs: List[Dict[str, Any]] = [
1626
+ s for s in symbols if s.get("type") in ("function", "async_function")
1627
+ ]
1628
+
1629
+ for cls in classes:
1630
+ cls_sig = cls.get("signature", f"class {cls['name']}:")
1631
+ file_lines.append(f" {cls_sig}")
1632
+ for m in cls.get("methods", []):
1633
+ m_sig = m.get("signature", f"def {m['name']}(...):")
1634
+ file_lines.append(f" {m_sig}")
1635
+
1636
+ for fn in top_funcs:
1637
+ fn_sig = fn.get("signature", f"def {fn['name']}(...):")
1638
+ file_lines.append(f" {fn_sig}")
1639
+
1640
+ # Append lines while within budget
1641
+ for line in file_lines:
1642
+ line_words = len(line.split())
1643
+ if current_words + line_words <= max_tokens:
1644
+ output_lines.append(line)
1645
+ current_words += line_words
1646
+ else:
1647
+ break
1648
+
1649
+ result = "\n".join(output_lines)
1650
+ return result
1651
+
1652
+ # ==========================================================================
1653
+ # Compact Topological Summary for LLM Context Injection
1654
+ # ==========================================================================
1655
+
1656
+ def get_topological_summary(
1657
+ self,
1658
+ max_tokens: int = 400,
1659
+ focus_files: Optional[List[str]] = None,
1660
+ ) -> str:
1661
+ """
1662
+ Generates a token-optimized topological summary of the codebase,
1663
+ ordering files from foundational dependencies up to high-level entrypoints.
1664
+
1665
+ Args:
1666
+ max_tokens: Maximum word/token budget for the summary.
1667
+ focus_files: Optional list of focus files to prioritize.
1668
+
1669
+ Returns:
1670
+ Compact topological summary string suitable for LLM system prompt injection.
1671
+ """
1672
+ if max_tokens <= 0:
1673
+ return ""
1674
+
1675
+ file_data = self._build_workspace_metadata()
1676
+ if not file_data:
1677
+ return ""
1678
+
1679
+ dep_graph = self.get_dependency_graph()
1680
+ cycles = self.detect_cyclic_imports()
1681
+
1682
+ # Compute In-degrees (how many files depend on this file)
1683
+ in_degrees: Dict[str, int] = {item["rel_path"]: 0 for item in file_data}
1684
+ for src, tgts in dep_graph.items():
1685
+ for t in tgts:
1686
+ if t in in_degrees:
1687
+ in_degrees[t] += 1
1688
+
1689
+ # Layer computation: Leaves (0 outgoing deps in workspace) -> Layer 0
1690
+ layers: Dict[int, List[Dict[str, Any]]] = defaultdict(list)
1691
+ depth_map: Dict[str, int] = {}
1692
+
1693
+ def get_depth(node: str, visited: Set[str]) -> int:
1694
+ if node in depth_map:
1695
+ return depth_map[node]
1696
+ if node in visited:
1697
+ return 0
1698
+ visited.add(node)
1699
+ deps = [d for d in dep_graph.get(node, []) if d != node]
1700
+ if not deps:
1701
+ depth_map[node] = 0
1702
+ return 0
1703
+ max_d = 1 + max(get_depth(d, visited) for d in deps)
1704
+ depth_map[node] = max_d
1705
+ return max_d
1706
+
1707
+ for item in file_data:
1708
+ rel = item["rel_path"]
1709
+ d = get_depth(rel, set())
1710
+ layers[d].append(item)
1711
+
1712
+ output_lines: List[str] = ["[Topological Architecture Map]"]
1713
+ current_words = len(output_lines[0].split())
1714
+
1715
+ # Cycle notice if any
1716
+ if cycles:
1717
+ cycle_str = f"[!] Cyclic Imports Detected: {len(cycles)} cycle(s)"
1718
+ if current_words + len(cycle_str.split()) <= max_tokens:
1719
+ output_lines.append(cycle_str)
1720
+ current_words += len(cycle_str.split())
1721
+
1722
+ # Render layer by layer
1723
+ layer_names = {
1724
+ 0: "Base / Foundation Modules",
1725
+ 1: "Core Components & Utilities",
1726
+ 2: "Services & Domain Logic",
1727
+ 3: "Application Orchestrators & APIs",
1728
+ 4: "Entrypoints & CLI",
1729
+ }
1730
+
1731
+ for layer_idx in sorted(layers.keys()):
1732
+ layer_items = layers[layer_idx]
1733
+ # Prioritize focus files and high in-degree within layer
1734
+ layer_items.sort(
1735
+ key=lambda x: (
1736
+ -1000 if self._matches_focus(x["rel_path"], x["full_path"], focus_files) else 0,
1737
+ -in_degrees.get(x["rel_path"], 0),
1738
+ x["rel_path"],
1739
+ )
1740
+ )
1741
+
1742
+ layer_title = layer_names.get(layer_idx, f"Layer {layer_idx}")
1743
+ header = f"\n=== {layer_title} ==="
1744
+ header_words = len(header.split())
1745
+ if current_words + header_words > max_tokens:
1746
+ break
1747
+ output_lines.append(header)
1748
+ current_words += header_words
1749
+
1750
+ for item in layer_items:
1751
+ rel = item["rel_path"]
1752
+ deps = dep_graph.get(rel, [])
1753
+ deps_suffix = f" (deps: {', '.join(deps)})" if deps else ""
1754
+ file_line = f" • {rel}{deps_suffix}"
1755
+ file_words = len(file_line.split())
1756
+ if current_words + file_words > max_tokens:
1757
+ break
1758
+ output_lines.append(file_line)
1759
+ current_words += file_words
1760
+
1761
+ # Add compact symbol summary
1762
+ classes = [s for s in item["symbols"] if s.get("type") in ("class", "struct", "interface", "trait", "enum")]
1763
+ funcs = [s for s in item["symbols"] if s.get("type") in ("function", "async_function")]
1764
+
1765
+ for cls in classes:
1766
+ cls_line = f" - {cls.get('signature', cls['name'])}"
1767
+ cls_words = len(cls_line.split())
1768
+ if current_words + cls_words <= max_tokens:
1769
+ output_lines.append(cls_line)
1770
+ current_words += cls_words
1771
+
1772
+ for fn in funcs:
1773
+ fn_line = f" - {fn.get('signature', fn['name'])}"
1774
+ fn_words = len(fn_line.split())
1775
+ if current_words + fn_words <= max_tokens:
1776
+ output_lines.append(fn_line)
1777
+ current_words += fn_words
1778
+
1779
+ return "\n".join(output_lines).strip()
1780
+