codecortex-context-engine 0.1.0a1__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 (90) hide show
  1. codecortex/__init__.py +3 -0
  2. codecortex/architecture/__init__.py +23 -0
  3. codecortex/architecture/drift.py +182 -0
  4. codecortex/architecture/inference.py +160 -0
  5. codecortex/backends/__init__.py +35 -0
  6. codecortex/backends/base.py +38 -0
  7. codecortex/backends/context.py +118 -0
  8. codecortex/backends/contracts.py +65 -0
  9. codecortex/backends/factory.py +60 -0
  10. codecortex/backends/graph.py +107 -0
  11. codecortex/backends/manager.py +303 -0
  12. codecortex/backends/mcp_client.py +196 -0
  13. codecortex/backends/pool.py +128 -0
  14. codecortex/backends/spec.py +83 -0
  15. codecortex/backends/symbols.py +189 -0
  16. codecortex/benchmark.py +211 -0
  17. codecortex/cli.py +409 -0
  18. codecortex/config.py +27 -0
  19. codecortex/context/__init__.py +13 -0
  20. codecortex/context/budget.py +62 -0
  21. codecortex/context/integrated.py +60 -0
  22. codecortex/context/pipeline.py +223 -0
  23. codecortex/core/__init__.py +1 -0
  24. codecortex/core/contracts.py +45 -0
  25. codecortex/core/errors.py +17 -0
  26. codecortex/core/models.py +69 -0
  27. codecortex/dashboard.py +259 -0
  28. codecortex/editing.py +41 -0
  29. codecortex/engines/__init__.py +5 -0
  30. codecortex/engines/builtin/__init__.py +5 -0
  31. codecortex/engines/builtin/factory.py +26 -0
  32. codecortex/engines/builtin/memory.py +35 -0
  33. codecortex/engines/builtin/repository.py +73 -0
  34. codecortex/engines/builtin/symbols.py +89 -0
  35. codecortex/engines/builtin/validation.py +54 -0
  36. codecortex/engines/registry.py +26 -0
  37. codecortex/entrypoint.py +266 -0
  38. codecortex/evaluation/__init__.py +55 -0
  39. codecortex/evaluation/external.py +265 -0
  40. codecortex/evaluation/production.py +670 -0
  41. codecortex/evaluation/regression.py +188 -0
  42. codecortex/gateway.py +38 -0
  43. codecortex/git_intelligence.py +252 -0
  44. codecortex/indexing/__init__.py +6 -0
  45. codecortex/indexing/graph.py +78 -0
  46. codecortex/indexing/impact.py +127 -0
  47. codecortex/indexing/incremental.py +163 -0
  48. codecortex/indexing/incremental_graph.py +189 -0
  49. codecortex/indexing/indexer.py +172 -0
  50. codecortex/indexing/relationships.py +179 -0
  51. codecortex/indexing/resolution.py +88 -0
  52. codecortex/integrations/__init__.py +5 -0
  53. codecortex/integrations/agents.py +235 -0
  54. codecortex/interfaces/__init__.py +1 -0
  55. codecortex/interfaces/mcp_bridge.py +66 -0
  56. codecortex/languages/__init__.py +5 -0
  57. codecortex/languages/native.py +166 -0
  58. codecortex/languages/registry.py +232 -0
  59. codecortex/mcp/__init__.py +5 -0
  60. codecortex/mcp/extended.py +114 -0
  61. codecortex/mcp/server.py +473 -0
  62. codecortex/memory/__init__.py +11 -0
  63. codecortex/memory/json_store.py +52 -0
  64. codecortex/memory/knowledge.py +193 -0
  65. codecortex/memory/team_store.py +193 -0
  66. codecortex/orchestrator.py +153 -0
  67. codecortex/pr_intelligence.py +214 -0
  68. codecortex/retrieval/__init__.py +16 -0
  69. codecortex/retrieval/hybrid.py +67 -0
  70. codecortex/retrieval/index.py +135 -0
  71. codecortex/retrieval/providers.py +67 -0
  72. codecortex/retrieval/repository.py +94 -0
  73. codecortex/router/__init__.py +5 -0
  74. codecortex/router/router.py +79 -0
  75. codecortex/runtime.py +69 -0
  76. codecortex/setup.py +100 -0
  77. codecortex/symbols/__init__.py +5 -0
  78. codecortex/symbols/providers.py +192 -0
  79. codecortex/telemetry/__init__.py +5 -0
  80. codecortex/telemetry/collector.py +43 -0
  81. codecortex/tracing/__init__.py +9 -0
  82. codecortex/tracing/task_trace.py +235 -0
  83. codecortex/workspace/__init__.py +9 -0
  84. codecortex/workspace/federation.py +173 -0
  85. codecortex_context_engine-0.1.0a1.dist-info/METADATA +381 -0
  86. codecortex_context_engine-0.1.0a1.dist-info/RECORD +90 -0
  87. codecortex_context_engine-0.1.0a1.dist-info/WHEEL +4 -0
  88. codecortex_context_engine-0.1.0a1.dist-info/entry_points.txt +3 -0
  89. codecortex_context_engine-0.1.0a1.dist-info/licenses/LICENSE +201 -0
  90. codecortex_context_engine-0.1.0a1.dist-info/licenses/NOTICE +2 -0
@@ -0,0 +1,88 @@
1
+ """Cross-file symbol resolution with explicit confidence and ambiguity."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+ from pathlib import PurePosixPath
7
+
8
+ from codecortex.indexing.graph import GraphNode
9
+
10
+
11
+ @dataclass(frozen=True, slots=True)
12
+ class ResolutionCandidate:
13
+ node_id: str
14
+ score: float
15
+ reasons: tuple[str, ...]
16
+
17
+
18
+ @dataclass(frozen=True, slots=True)
19
+ class ResolutionResult:
20
+ target_id: str | None
21
+ confidence: float
22
+ ambiguity: float
23
+ candidates: tuple[ResolutionCandidate, ...]
24
+
25
+
26
+ class CrossFileResolver:
27
+ """Rank same-name symbols while preserving uncertainty for downstream agents."""
28
+
29
+ def resolve(
30
+ self,
31
+ name: str,
32
+ source_path: str,
33
+ candidates: list[GraphNode],
34
+ relation_kind: str,
35
+ ) -> ResolutionResult:
36
+ ranked: list[ResolutionCandidate] = []
37
+ source = PurePosixPath(source_path)
38
+ for node in candidates:
39
+ if node.path is None:
40
+ continue
41
+ target = PurePosixPath(node.path)
42
+ score = 0.35
43
+ reasons = ["exact_symbol_name"]
44
+ if target == source:
45
+ score += 0.35
46
+ reasons.append("same_file")
47
+ elif target.parent == source.parent:
48
+ score += 0.20
49
+ reasons.append("same_directory")
50
+ else:
51
+ shared = len(set(source.parts[:-1]) & set(target.parts[:-1]))
52
+ if shared:
53
+ score += min(0.12, shared * 0.03)
54
+ reasons.append("shared_package_path")
55
+ if relation_kind == "calls" and node.kind in {
56
+ "function",
57
+ "async_function",
58
+ "method",
59
+ }:
60
+ score += 0.08
61
+ reasons.append("callable_kind")
62
+ if relation_kind in {"inherits", "implements"} and node.kind in {
63
+ "class",
64
+ "interface",
65
+ }:
66
+ score += 0.08
67
+ reasons.append("type_kind")
68
+ ranked.append(
69
+ ResolutionCandidate(
70
+ node_id=node.id,
71
+ score=min(1.0, score),
72
+ reasons=tuple(reasons),
73
+ )
74
+ )
75
+ ranked.sort(key=lambda item: (-item.score, item.node_id))
76
+ if not ranked:
77
+ return ResolutionResult(None, 0.0, 1.0, ())
78
+ best = ranked[0]
79
+ second = ranked[1].score if len(ranked) > 1 else 0.0
80
+ margin = max(0.0, best.score - second)
81
+ ambiguity = 0.0 if len(ranked) == 1 else max(0.0, min(1.0, 1.0 - margin))
82
+ confidence = max(0.0, min(1.0, best.score * (1.0 - 0.45 * ambiguity)))
83
+ return ResolutionResult(
84
+ target_id=best.node_id,
85
+ confidence=confidence,
86
+ ambiguity=ambiguity,
87
+ candidates=tuple(ranked[:5]),
88
+ )
@@ -0,0 +1,5 @@
1
+ """Coding-agent integration helpers."""
2
+
3
+ from codecortex.integrations.agents import AgentConfigurator, AgentMutation, AgentTarget
4
+
5
+ __all__ = ["AgentConfigurator", "AgentMutation", "AgentTarget"]
@@ -0,0 +1,235 @@
1
+ """Merge-safe project configuration for MCP-capable coding agents."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import os
7
+ import re
8
+ import shutil
9
+ import tempfile
10
+ from dataclasses import dataclass
11
+ from datetime import UTC, datetime
12
+ from enum import StrEnum
13
+ from pathlib import Path
14
+ from typing import Any
15
+
16
+
17
+ class AgentTarget(StrEnum):
18
+ CLAUDE = "claude"
19
+ CODEX = "codex"
20
+ CURSOR = "cursor"
21
+ GEMINI = "gemini"
22
+ OPENCODE = "opencode"
23
+
24
+
25
+ @dataclass(frozen=True, slots=True)
26
+ class AgentMutation:
27
+ target: AgentTarget
28
+ path: Path
29
+ detected: bool
30
+ changed: bool
31
+ backup: Path | None = None
32
+ detail: str = ""
33
+
34
+
35
+ class AgentConfigurationError(RuntimeError):
36
+ pass
37
+
38
+
39
+ class AgentConfigurator:
40
+ """Configure project-local integrations without clobbering user-owned settings."""
41
+
42
+ COMMANDS = {
43
+ AgentTarget.CLAUDE: ("claude",),
44
+ AgentTarget.CODEX: ("codex",),
45
+ AgentTarget.GEMINI: ("gemini",),
46
+ AgentTarget.OPENCODE: ("opencode",),
47
+ }
48
+
49
+ def __init__(self, root: Path, *, executable: str = "cortex") -> None:
50
+ self.root = root.expanduser().resolve()
51
+ self.executable = executable
52
+
53
+ def detect(self) -> tuple[AgentTarget, ...]:
54
+ found: set[AgentTarget] = set()
55
+ for target, commands in self.COMMANDS.items():
56
+ if any(shutil.which(command) for command in commands):
57
+ found.add(target)
58
+ if (self.root / ".cursor").exists() or shutil.which("cursor"):
59
+ found.add(AgentTarget.CURSOR)
60
+ if (self.root / ".mcp.json").exists():
61
+ found.add(AgentTarget.CLAUDE)
62
+ if (self.root / ".gemini").exists():
63
+ found.add(AgentTarget.GEMINI)
64
+ if (self.root / ".codex").exists():
65
+ found.add(AgentTarget.CODEX)
66
+ if (self.root / "opencode.json").exists():
67
+ found.add(AgentTarget.OPENCODE)
68
+ return tuple(sorted(found, key=str))
69
+
70
+ def configure(
71
+ self,
72
+ targets: tuple[AgentTarget, ...] | None = None,
73
+ *,
74
+ dry_run: bool = False,
75
+ ) -> tuple[AgentMutation, ...]:
76
+ selected = targets or self.detect()
77
+ return tuple(self.configure_one(target, dry_run=dry_run) for target in selected)
78
+
79
+ def configure_one(self, target: AgentTarget, *, dry_run: bool = False) -> AgentMutation:
80
+ detected = target in self.detect()
81
+ if target == AgentTarget.CLAUDE:
82
+ return self._configure_json(
83
+ target,
84
+ self.root / ".mcp.json",
85
+ ("mcpServers",),
86
+ self._stdio_json(),
87
+ detected,
88
+ dry_run,
89
+ )
90
+ if target == AgentTarget.CURSOR:
91
+ return self._configure_json(
92
+ target,
93
+ self.root / ".cursor" / "mcp.json",
94
+ ("mcpServers",),
95
+ self._stdio_json(),
96
+ detected,
97
+ dry_run,
98
+ )
99
+ if target == AgentTarget.GEMINI:
100
+ return self._configure_json(
101
+ target,
102
+ self.root / ".gemini" / "settings.json",
103
+ ("mcpServers",),
104
+ self._stdio_json(),
105
+ detected,
106
+ dry_run,
107
+ )
108
+ if target == AgentTarget.OPENCODE:
109
+ return self._configure_json(
110
+ target,
111
+ self.root / "opencode.json",
112
+ ("mcp", "servers"),
113
+ {
114
+ "type": "local",
115
+ "command": [self.executable, "mcp", "--path", str(self.root)],
116
+ },
117
+ detected,
118
+ dry_run,
119
+ defaults={"$schema": "https://opencode.ai/config.json"},
120
+ )
121
+ if target == AgentTarget.CODEX:
122
+ return self._configure_codex(detected=detected, dry_run=dry_run)
123
+ raise ValueError(target)
124
+
125
+ def _stdio_json(self) -> dict[str, Any]:
126
+ return {
127
+ "command": self.executable,
128
+ "args": ["mcp", "--path", str(self.root)],
129
+ "env": {"CODECORTEX_BACKENDS": "auto"},
130
+ }
131
+
132
+ def _configure_json(
133
+ self,
134
+ target: AgentTarget,
135
+ path: Path,
136
+ container_keys: tuple[str, ...],
137
+ server: dict[str, Any],
138
+ detected: bool,
139
+ dry_run: bool,
140
+ *,
141
+ defaults: dict[str, Any] | None = None,
142
+ ) -> AgentMutation:
143
+ payload: dict[str, Any] = dict(defaults or {})
144
+ if path.exists():
145
+ try:
146
+ loaded = json.loads(path.read_text(encoding="utf-8"))
147
+ except json.JSONDecodeError as exc:
148
+ raise AgentConfigurationError(f"refusing to modify invalid JSON: {path}: {exc}") from exc
149
+ if not isinstance(loaded, dict):
150
+ raise AgentConfigurationError(f"expected a JSON object: {path}")
151
+ payload = loaded
152
+ for key, value in (defaults or {}).items():
153
+ payload.setdefault(key, value)
154
+ container: dict[str, Any] = payload
155
+ for key in container_keys:
156
+ child = container.get(key)
157
+ if child is None:
158
+ child = {}
159
+ container[key] = child
160
+ if not isinstance(child, dict):
161
+ raise AgentConfigurationError(f"cannot merge CodeCortex into non-object {'.'.join(container_keys)} in {path}")
162
+ container = child
163
+ previous = container.get("codecortex")
164
+ container["codecortex"] = server
165
+ changed = previous != server or not path.exists()
166
+ if not changed or dry_run:
167
+ return AgentMutation(target, path, detected, changed, detail="dry-run" if dry_run and changed else "already configured")
168
+ backup = self._backup(path)
169
+ self._atomic_write(path, json.dumps(payload, ensure_ascii=False, indent=2) + "\n")
170
+ return AgentMutation(target, path, detected, True, backup=backup, detail="configured")
171
+
172
+ def _configure_codex(self, *, detected: bool, dry_run: bool) -> AgentMutation:
173
+ path = self.root / ".codex" / "config.toml"
174
+ begin = "# >>> codecortex managed mcp >>>"
175
+ end = "# <<< codecortex managed mcp <<<"
176
+ args = ["mcp", "--path", str(self.root)]
177
+ block = "\n".join(
178
+ [
179
+ begin,
180
+ "[mcp_servers.codecortex]",
181
+ f"command = {json.dumps(self.executable)}",
182
+ f"args = {json.dumps(args)}",
183
+ "env = { CODECORTEX_BACKENDS = \"auto\" }",
184
+ "enabled = true",
185
+ "startup_timeout_sec = 30",
186
+ "tool_timeout_sec = 120",
187
+ end,
188
+ ]
189
+ )
190
+ existing = path.read_text(encoding="utf-8") if path.exists() else ""
191
+ managed = re.compile(re.escape(begin) + r".*?" + re.escape(end), re.DOTALL)
192
+ if managed.search(existing):
193
+ # A callable replacement keeps Windows backslashes literal instead of
194
+ # letting re.sub interpret them as replacement-string escapes.
195
+ updated = managed.sub(lambda _match: block, existing)
196
+ else:
197
+ if re.search(r"(?m)^\s*\[mcp_servers\.codecortex\]\s*$", existing):
198
+ raise AgentConfigurationError(
199
+ f"refusing to overwrite an unmanaged [mcp_servers.codecortex] table in {path}"
200
+ )
201
+ separator = "\n\n" if existing.strip() else ""
202
+ updated = existing.rstrip() + separator + block + "\n"
203
+ changed = updated != existing
204
+ if not changed or dry_run:
205
+ return AgentMutation(AgentTarget.CODEX, path, detected, changed, detail="dry-run" if dry_run and changed else "already configured")
206
+ backup = self._backup(path)
207
+ self._atomic_write(path, updated)
208
+ return AgentMutation(AgentTarget.CODEX, path, detected, True, backup=backup, detail="configured")
209
+
210
+ @staticmethod
211
+ def _backup(path: Path) -> Path | None:
212
+ if not path.exists():
213
+ return None
214
+ stamp = datetime.now(UTC).strftime("%Y%m%d-%H%M%S-%f")
215
+ backup = path.with_name(f"{path.name}.codecortex-{stamp}.bak")
216
+ backup.parent.mkdir(parents=True, exist_ok=True)
217
+ shutil.copy2(path, backup)
218
+ return backup
219
+
220
+ @staticmethod
221
+ def _atomic_write(path: Path, content: str) -> None:
222
+ path.parent.mkdir(parents=True, exist_ok=True)
223
+ fd, temp_name = tempfile.mkstemp(prefix=f".{path.name}.", suffix=".tmp", dir=path.parent)
224
+ try:
225
+ with os.fdopen(fd, "w", encoding="utf-8", newline="\n") as handle:
226
+ handle.write(content)
227
+ handle.flush()
228
+ os.fsync(handle.fileno())
229
+ os.replace(temp_name, path)
230
+ except Exception:
231
+ try:
232
+ os.unlink(temp_name)
233
+ except OSError:
234
+ pass
235
+ raise
@@ -0,0 +1 @@
1
+ """External interface adapters."""
@@ -0,0 +1,66 @@
1
+ """Protocol-neutral tool bridge for MCP-compatible hosts."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+ from codecortex.gateway import CodeCortexGateway
8
+
9
+
10
+ class MCPBridge:
11
+ """Expose stable tool definitions without coupling the core to one transport."""
12
+
13
+ def __init__(self, gateway: CodeCortexGateway) -> None:
14
+ self.gateway = gateway
15
+
16
+ def tool_definitions(self) -> list[dict[str, Any]]:
17
+ return [
18
+ {
19
+ "name": "cortex_route",
20
+ "description": "Classify a coding request and return the selected capabilities.",
21
+ "inputSchema": {
22
+ "type": "object",
23
+ "properties": {"query": {"type": "string"}},
24
+ "required": ["query"],
25
+ },
26
+ },
27
+ {
28
+ "name": "cortex_query",
29
+ "description": "Run repository intelligence for a coding request.",
30
+ "inputSchema": {
31
+ "type": "object",
32
+ "properties": {"query": {"type": "string"}},
33
+ "required": ["query"],
34
+ },
35
+ },
36
+ {
37
+ "name": "cortex_remember",
38
+ "description": "Save a project decision or reusable fact.",
39
+ "inputSchema": {
40
+ "type": "object",
41
+ "properties": {
42
+ "key": {"type": "string"},
43
+ "value": {"type": "string"},
44
+ },
45
+ "required": ["key", "value"],
46
+ },
47
+ },
48
+ {
49
+ "name": "cortex_health",
50
+ "description": "Return CodeCortex engine health.",
51
+ "inputSchema": {"type": "object", "properties": {}},
52
+ },
53
+ ]
54
+
55
+ async def call(self, name: str, arguments: dict[str, Any]) -> dict[str, Any]:
56
+ if name == "cortex_route":
57
+ return self.gateway.route(str(arguments["query"])).model_dump(mode="json")
58
+ if name == "cortex_query":
59
+ result = await self.gateway.query(str(arguments["query"]))
60
+ return result.model_dump(mode="json")
61
+ if name == "cortex_remember":
62
+ await self.gateway.remember(str(arguments["key"]), str(arguments["value"]))
63
+ return {"saved": True}
64
+ if name == "cortex_health":
65
+ return await self.gateway.health()
66
+ raise KeyError(f"Unknown tool: {name}")
@@ -0,0 +1,5 @@
1
+ """Language intelligence primitives."""
2
+
3
+ from codecortex.languages.registry import LanguageRegistry, LanguageSpec, ParsedUnit
4
+
5
+ __all__ = ["LanguageRegistry", "LanguageSpec", "ParsedUnit"]
@@ -0,0 +1,166 @@
1
+ """Optional Tree-sitter parser provider for production polyglot structure."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+ from typing import Any
7
+
8
+
9
+ @dataclass(frozen=True, slots=True)
10
+ class NativeUnit:
11
+ name: str
12
+ kind: str
13
+ line: int
14
+ end_line: int
15
+ signature: str | None = None
16
+ return_type: str | None = None
17
+ bases: tuple[str, ...] = ()
18
+ references: tuple[str, ...] = ()
19
+
20
+
21
+ class TreeSitterParserProvider:
22
+ """Parse major languages with native grammars when the parser extra is installed."""
23
+
24
+ aliases = {
25
+ "typescript": "typescript",
26
+ "javascript": "javascript",
27
+ "go": "go",
28
+ "rust": "rust",
29
+ "java": "java",
30
+ "c": "c",
31
+ "cpp": "cpp",
32
+ "csharp": "csharp",
33
+ "php": "php",
34
+ "ruby": "ruby",
35
+ }
36
+
37
+ kinds = {
38
+ "class_declaration": "class",
39
+ "class_definition": "class",
40
+ "class_specifier": "class",
41
+ "interface_declaration": "interface",
42
+ "trait_item": "interface",
43
+ "struct_item": "struct",
44
+ "struct_specifier": "struct",
45
+ "enum_item": "enum",
46
+ "enum_declaration": "enum",
47
+ "function_declaration": "function",
48
+ "function_definition": "function",
49
+ "function_item": "function",
50
+ "method_declaration": "method",
51
+ "method_definition": "method",
52
+ "constructor_declaration": "constructor",
53
+ "singleton_method": "method",
54
+ }
55
+
56
+ def __init__(self) -> None:
57
+ try:
58
+ from tree_sitter_language_pack import get_parser
59
+ except ImportError as exc:
60
+ raise RuntimeError("install CodeCortex with the `parsers` extra") from exc
61
+ self._get_parser = get_parser
62
+
63
+ @classmethod
64
+ def available(cls) -> bool:
65
+ try:
66
+ import tree_sitter_language_pack # noqa: F401
67
+ except ImportError:
68
+ return False
69
+ return True
70
+
71
+ def parse(self, language: str, source: str) -> list[NativeUnit]:
72
+ alias = self.aliases.get(language)
73
+ if alias is None:
74
+ return []
75
+ parser = self._get_parser(alias)
76
+ tree = parser.parse(source.encode("utf-8"))
77
+ root = tree.root_node
78
+ source_bytes = source.encode("utf-8")
79
+ units: list[NativeUnit] = []
80
+ stack = [root]
81
+ while stack:
82
+ node = stack.pop()
83
+ stack.extend(reversed(list(getattr(node, "children", ()))))
84
+ kind = self.kinds.get(str(getattr(node, "type", "")))
85
+ if kind is None:
86
+ continue
87
+ name_node = self._field(node, "name") or self._first_identifier(node)
88
+ if name_node is None:
89
+ continue
90
+ name = self._text(name_node, source_bytes).strip()
91
+ if not name:
92
+ continue
93
+ return_node = self._field(node, "return_type") or self._field(node, "type")
94
+ bases = self._bases(node, source_bytes)
95
+ refs = self._references(node, source_bytes, name)
96
+ units.append(
97
+ NativeUnit(
98
+ name=name,
99
+ kind=kind,
100
+ line=int(node.start_point.row) + 1,
101
+ end_line=int(node.end_point.row) + 1,
102
+ signature=self._signature(node, source_bytes),
103
+ return_type=(
104
+ self._text(return_node, source_bytes).strip()
105
+ if return_node is not None
106
+ else None
107
+ ),
108
+ bases=bases,
109
+ references=refs,
110
+ )
111
+ )
112
+ units.sort(key=lambda item: (item.line, item.end_line, item.name))
113
+ return units
114
+
115
+ @staticmethod
116
+ def _field(node: Any, name: str) -> Any | None:
117
+ method = getattr(node, "child_by_field_name", None)
118
+ return method(name) if callable(method) else None
119
+
120
+ @staticmethod
121
+ def _text(node: Any, source: bytes) -> str:
122
+ return source[int(node.start_byte) : int(node.end_byte)].decode("utf-8", errors="replace")
123
+
124
+ def _first_identifier(self, node: Any) -> Any | None:
125
+ stack = list(getattr(node, "children", ()))
126
+ while stack:
127
+ child = stack.pop(0)
128
+ if str(getattr(child, "type", "")) in {
129
+ "identifier",
130
+ "type_identifier",
131
+ "constant",
132
+ "name",
133
+ }:
134
+ return child
135
+ stack[0:0] = list(getattr(child, "children", ()))
136
+ return None
137
+
138
+ def _signature(self, node: Any, source: bytes) -> str | None:
139
+ body = self._field(node, "body")
140
+ end = int(body.start_byte) if body is not None else int(node.end_byte)
141
+ text = source[int(node.start_byte) : end].decode("utf-8", errors="replace").strip()
142
+ text = " ".join(text.split())
143
+ return text[:800] or None
144
+
145
+ def _bases(self, node: Any, source: bytes) -> tuple[str, ...]:
146
+ values: list[str] = []
147
+ for field in ("superclass", "interfaces", "base", "type_parameters"):
148
+ child = self._field(node, field)
149
+ if child is not None:
150
+ value = self._text(child, source).strip()
151
+ if value:
152
+ values.append(value)
153
+ return tuple(dict.fromkeys(values))
154
+
155
+ def _references(self, node: Any, source: bytes, own_name: str) -> tuple[str, ...]:
156
+ values: list[str] = []
157
+ stack = list(getattr(node, "children", ()))
158
+ while stack and len(values) < 128:
159
+ child = stack.pop()
160
+ child_type = str(getattr(child, "type", ""))
161
+ if child_type in {"identifier", "type_identifier", "constant"}:
162
+ value = self._text(child, source).strip()
163
+ if value and value != own_name and value not in values:
164
+ values.append(value)
165
+ stack.extend(getattr(child, "children", ()))
166
+ return tuple(values)