gdmcode 0.1.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.
- gdmcode-0.1.0.dist-info/METADATA +240 -0
- gdmcode-0.1.0.dist-info/RECORD +131 -0
- gdmcode-0.1.0.dist-info/WHEEL +4 -0
- gdmcode-0.1.0.dist-info/entry_points.txt +2 -0
- src/__init__.py +1 -0
- src/_internal/__init__.py +0 -0
- src/_internal/constants.py +244 -0
- src/_internal/domain_skills.py +339 -0
- src/agent/__init__.py +0 -0
- src/agent/commit_classifier.py +91 -0
- src/agent/context_budget.py +391 -0
- src/agent/daemon.py +681 -0
- src/agent/dag_validator.py +153 -0
- src/agent/debug_loop.py +473 -0
- src/agent/impact_analyzer.py +149 -0
- src/agent/impact_graph.py +117 -0
- src/agent/loop.py +1410 -0
- src/agent/orchestrator.py +141 -0
- src/agent/regression_guard.py +251 -0
- src/agent/review_gate.py +648 -0
- src/agent/risk_scorer.py +169 -0
- src/agent/self_healing.py +145 -0
- src/agent/smart_test_selector.py +89 -0
- src/agent/system_prompt.py +226 -0
- src/agent/task_tracker.py +320 -0
- src/agent/test_validator.py +210 -0
- src/agent/tool_orchestrator.py +402 -0
- src/agent/transcript.py +230 -0
- src/agent/verification_loop.py +133 -0
- src/agent/work_director.py +136 -0
- src/agent/worktree_manager.py +53 -0
- src/artifacts/__init__.py +16 -0
- src/artifacts/artifact_store.py +456 -0
- src/artifacts/verification_graph.py +75 -0
- src/auth.py +411 -0
- src/cli.py +1290 -0
- src/commands.py +1398 -0
- src/config.py +762 -0
- src/cost_tracker.py +348 -0
- src/db/__init__.py +4 -0
- src/db/migrations.py +337 -0
- src/enterprise/__init__.py +3 -0
- src/enterprise/audit_log.py +182 -0
- src/enterprise/identity.py +90 -0
- src/enterprise/rbac.py +100 -0
- src/enterprise/team_config.py +125 -0
- src/enterprise/usage_analytics.py +261 -0
- src/exceptions.py +207 -0
- src/git_workflow.py +651 -0
- src/integrations/__init__.py +6 -0
- src/integrations/github_actions.py +106 -0
- src/integrations/mcp_server.py +333 -0
- src/integrations/sentry_integration.py +100 -0
- src/integrations/sentry_server.py +82 -0
- src/integrations/webhook_security.py +19 -0
- src/main.py +27 -0
- src/memory/__init__.py +0 -0
- src/memory/code_index.py +376 -0
- src/memory/compressor.py +378 -0
- src/memory/context_memory.py +135 -0
- src/memory/continuous_memory.py +234 -0
- src/memory/conventions.py +495 -0
- src/memory/db.py +1119 -0
- src/memory/document_index.py +205 -0
- src/memory/file_cache.py +128 -0
- src/memory/project_scanner.py +178 -0
- src/memory/session_store.py +201 -0
- src/models/__init__.py +0 -0
- src/models/client.py +715 -0
- src/models/definitions.py +459 -0
- src/models/router.py +418 -0
- src/models/schemas.py +389 -0
- src/permissions.py +294 -0
- src/remote/__init__.py +5 -0
- src/remote/command_filter.py +33 -0
- src/remote/models.py +31 -0
- src/remote/permission_handler.py +79 -0
- src/remote/phone_ui.py +48 -0
- src/remote/protocol.py +59 -0
- src/remote/qr.py +65 -0
- src/remote/server.py +586 -0
- src/remote/token_manager.py +61 -0
- src/remote/tunnel.py +212 -0
- src/repl.py +475 -0
- src/runtime/__init__.py +1 -0
- src/runtime/branch_farm.py +372 -0
- src/runtime/replay.py +351 -0
- src/sandbox/__init__.py +2 -0
- src/sandbox/hermetic.py +214 -0
- src/sandbox/policy.py +44 -0
- src/sdk/__init__.py +3 -0
- src/sdk/plugin_base.py +39 -0
- src/sdk/plugin_host.py +100 -0
- src/sdk/plugin_loader.py +101 -0
- src/security.py +409 -0
- src/server/__init__.py +7 -0
- src/server/bridge.py +427 -0
- src/server/bridge_cli.py +103 -0
- src/server/bridge_client.py +170 -0
- src/server/protocol_version.py +103 -0
- src/session/__init__.py +10 -0
- src/session/event_fanout.py +46 -0
- src/session/input_broker.py +38 -0
- src/session/permission_bridge.py +100 -0
- src/tools/__init__.py +160 -0
- src/tools/_atomic.py +72 -0
- src/tools/agent_tools.py +423 -0
- src/tools/ask_user_tool.py +83 -0
- src/tools/bash_tool.py +384 -0
- src/tools/browser_tool.py +352 -0
- src/tools/browser_tools.py +179 -0
- src/tools/dep_tools.py +210 -0
- src/tools/document_reader.py +167 -0
- src/tools/document_tool.py +240 -0
- src/tools/document_writer.py +171 -0
- src/tools/impact_tools.py +240 -0
- src/tools/playwright_tool.py +172 -0
- src/tools/quality_tools.py +366 -0
- src/tools/read_tools.py +318 -0
- src/tools/result_cache.py +157 -0
- src/tools/search_tools.py +310 -0
- src/tools/shell_tools.py +311 -0
- src/tools/write_tools.py +337 -0
- src/voice/__init__.py +25 -0
- src/voice/audio_capture.py +92 -0
- src/voice/audio_playback.py +68 -0
- src/voice/errors.py +14 -0
- src/voice/models.py +35 -0
- src/voice/providers.py +143 -0
- src/voice/vad.py +55 -0
- src/voice/voice_loop.py +156 -0
src/memory/code_index.py
ADDED
|
@@ -0,0 +1,376 @@
|
|
|
1
|
+
"""Code index — Tree-sitter AST symbol index for $0/query codebase search.
|
|
2
|
+
|
|
3
|
+
Builds a structural index of all source files in the project:
|
|
4
|
+
- Python: function defs, class defs, method defs
|
|
5
|
+
- TypeScript: function/arrow decls, class defs, interface defs
|
|
6
|
+
- JavaScript: function/arrow decls, class defs
|
|
7
|
+
|
|
8
|
+
Stored in gdm.db code_index table. Incremental: only re-indexes files
|
|
9
|
+
whose mtime changed since last index run.
|
|
10
|
+
|
|
11
|
+
Query API:
|
|
12
|
+
find_symbol(name) → exact name match across project
|
|
13
|
+
find_callers(symbol) → files that call/import symbol
|
|
14
|
+
get_symbol_signature(path, sym) → return function signature line
|
|
15
|
+
search(query) → keyword grep fallback
|
|
16
|
+
"""
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import logging
|
|
20
|
+
import re
|
|
21
|
+
import time
|
|
22
|
+
from dataclasses import dataclass
|
|
23
|
+
from pathlib import Path
|
|
24
|
+
from typing import Any
|
|
25
|
+
|
|
26
|
+
__all__ = ["CodeIndex", "SymbolMatch", "ReferenceMatch"]
|
|
27
|
+
|
|
28
|
+
log = logging.getLogger(__name__)
|
|
29
|
+
|
|
30
|
+
_SUPPORTED_SUFFIXES: frozenset[str] = frozenset({".py", ".ts", ".tsx", ".js", ".jsx"})
|
|
31
|
+
_MAX_FILE_SIZE_BYTES: int = 500_000
|
|
32
|
+
_INDEX_TIMEOUT_SECS: float = 60.0
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
# ---------------------------------------------------------------------------
|
|
36
|
+
# Data classes
|
|
37
|
+
# ---------------------------------------------------------------------------
|
|
38
|
+
|
|
39
|
+
@dataclass
|
|
40
|
+
class SymbolMatch:
|
|
41
|
+
"""A single symbol found in the index."""
|
|
42
|
+
|
|
43
|
+
symbol: str
|
|
44
|
+
kind: str # "function" | "class" | "method" | "interface" | "arrow"
|
|
45
|
+
file: str # relative path from project root
|
|
46
|
+
line: int
|
|
47
|
+
signature: str # e.g. "def validate_user(email: str) -> bool:"
|
|
48
|
+
|
|
49
|
+
def format(self, root: Path) -> str:
|
|
50
|
+
"""Human-readable match line for display."""
|
|
51
|
+
return f"{self.file}:{self.line} [{self.kind}] {self.signature}"
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@dataclass
|
|
55
|
+
class ReferenceMatch:
|
|
56
|
+
"""A single reference to a symbol — either a definition or a call site."""
|
|
57
|
+
|
|
58
|
+
symbol: str
|
|
59
|
+
file: str # relative path from project root
|
|
60
|
+
line: int
|
|
61
|
+
kind: str # "definition" | "caller"
|
|
62
|
+
snippet: str # the source line (stripped)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
# ---------------------------------------------------------------------------
|
|
66
|
+
# Language-specific extractors (regex-based, fast, no grammar loading needed)
|
|
67
|
+
# ---------------------------------------------------------------------------
|
|
68
|
+
|
|
69
|
+
_PY_FUNC = re.compile(
|
|
70
|
+
r"^(?P<indent>\s*)(?:async\s+)?def\s+(?P<name>\w+)\s*\((?P<args>[^)]*)\)"
|
|
71
|
+
r"(?:\s*->\s*[^:]+)?:",
|
|
72
|
+
re.MULTILINE,
|
|
73
|
+
)
|
|
74
|
+
_PY_CLASS = re.compile(r"^class\s+(?P<name>\w+)", re.MULTILINE)
|
|
75
|
+
|
|
76
|
+
_TS_FUNC = re.compile(
|
|
77
|
+
r"(?:export\s+)?(?:async\s+)?function\s+(?P<name>\w+)\s*\(",
|
|
78
|
+
re.MULTILINE,
|
|
79
|
+
)
|
|
80
|
+
_TS_ARROW = re.compile(
|
|
81
|
+
r"(?:export\s+)?(?:const|let)\s+(?P<name>\w+)\s*=\s*(?:async\s+)?\(",
|
|
82
|
+
re.MULTILINE,
|
|
83
|
+
)
|
|
84
|
+
_TS_CLASS = re.compile(r"(?:export\s+)?(?:abstract\s+)?class\s+(?P<name>\w+)", re.MULTILINE)
|
|
85
|
+
_TS_IFACE = re.compile(r"(?:export\s+)?interface\s+(?P<name>\w+)", re.MULTILINE)
|
|
86
|
+
_TS_METHOD = re.compile(
|
|
87
|
+
r"^\s+(?:public|private|protected|static|async|\s)*"
|
|
88
|
+
r"(?P<name>\w+)\s*\([^)]*\)\s*(?::\s*\S+\s*)?[{;]",
|
|
89
|
+
re.MULTILINE,
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _extract_python(text: str, rel_path: str) -> list[SymbolMatch]:
|
|
94
|
+
"""Extract symbols from Python source text."""
|
|
95
|
+
lines = text.splitlines()
|
|
96
|
+
matches: list[SymbolMatch] = []
|
|
97
|
+
|
|
98
|
+
for m in _PY_CLASS.finditer(text):
|
|
99
|
+
line_num = text[: m.start()].count("\n") + 1
|
|
100
|
+
matches.append(SymbolMatch(
|
|
101
|
+
symbol=m.group("name"), kind="class", file=rel_path,
|
|
102
|
+
line=line_num, signature=lines[line_num - 1].strip(),
|
|
103
|
+
))
|
|
104
|
+
|
|
105
|
+
for m in _PY_FUNC.finditer(text):
|
|
106
|
+
line_num = text[: m.start()].count("\n") + 1
|
|
107
|
+
name = m.group("name")
|
|
108
|
+
if name.startswith("__") and name.endswith("__"):
|
|
109
|
+
kind = "method"
|
|
110
|
+
elif m.group("indent"):
|
|
111
|
+
kind = "method"
|
|
112
|
+
else:
|
|
113
|
+
kind = "function"
|
|
114
|
+
sig = lines[line_num - 1].strip()
|
|
115
|
+
matches.append(SymbolMatch(
|
|
116
|
+
symbol=name, kind=kind, file=rel_path, line=line_num, signature=sig,
|
|
117
|
+
))
|
|
118
|
+
|
|
119
|
+
return matches
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def _extract_typescript(text: str, rel_path: str) -> list[SymbolMatch]:
|
|
123
|
+
"""Extract symbols from TypeScript/JavaScript source text."""
|
|
124
|
+
lines = text.splitlines()
|
|
125
|
+
matches: list[SymbolMatch] = []
|
|
126
|
+
|
|
127
|
+
for pat, kind in [(_TS_CLASS, "class"), (_TS_IFACE, "interface")]:
|
|
128
|
+
for m in pat.finditer(text):
|
|
129
|
+
line_num = text[: m.start()].count("\n") + 1
|
|
130
|
+
matches.append(SymbolMatch(
|
|
131
|
+
symbol=m.group("name"), kind=kind, file=rel_path,
|
|
132
|
+
line=line_num, signature=lines[line_num - 1].strip()[:120],
|
|
133
|
+
))
|
|
134
|
+
|
|
135
|
+
for pat, kind in [(_TS_FUNC, "function"), (_TS_ARROW, "arrow")]:
|
|
136
|
+
for m in pat.finditer(text):
|
|
137
|
+
line_num = text[: m.start()].count("\n") + 1
|
|
138
|
+
matches.append(SymbolMatch(
|
|
139
|
+
symbol=m.group("name"), kind=kind, file=rel_path,
|
|
140
|
+
line=line_num, signature=lines[line_num - 1].strip()[:120],
|
|
141
|
+
))
|
|
142
|
+
|
|
143
|
+
return matches
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
# ---------------------------------------------------------------------------
|
|
147
|
+
# CodeIndex
|
|
148
|
+
# ---------------------------------------------------------------------------
|
|
149
|
+
|
|
150
|
+
class CodeIndex:
|
|
151
|
+
"""Structural code index for fast $0/query symbol search.
|
|
152
|
+
|
|
153
|
+
Usage::
|
|
154
|
+
|
|
155
|
+
index = CodeIndex(db, project_id, project_root)
|
|
156
|
+
index.build() # full scan (incremental-safe)
|
|
157
|
+
matches = index.find_symbol("validateUser")
|
|
158
|
+
sig = index.get_symbol_signature("src/auth.py", "validate_user")
|
|
159
|
+
"""
|
|
160
|
+
|
|
161
|
+
def __init__(self, db: Any, project_id: str, project_root: Path) -> None:
|
|
162
|
+
self._db = db
|
|
163
|
+
self._project_id = project_id
|
|
164
|
+
self._root = project_root
|
|
165
|
+
self._ensure_schema()
|
|
166
|
+
|
|
167
|
+
def _ensure_schema(self) -> None:
|
|
168
|
+
"""Add mtime column to code_index if the table predates migration v10."""
|
|
169
|
+
try:
|
|
170
|
+
self._db.execute(
|
|
171
|
+
"ALTER TABLE code_index ADD COLUMN mtime REAL DEFAULT 0", ()
|
|
172
|
+
)
|
|
173
|
+
except Exception:
|
|
174
|
+
pass # Column already exists
|
|
175
|
+
|
|
176
|
+
# ------------------------------------------------------------------
|
|
177
|
+
# Build / update index
|
|
178
|
+
# ------------------------------------------------------------------
|
|
179
|
+
|
|
180
|
+
def build(self, *, force: bool = False) -> int:
|
|
181
|
+
"""Build or update the index. Returns count of files indexed."""
|
|
182
|
+
files = self._collect_source_files()
|
|
183
|
+
indexed = 0
|
|
184
|
+
start = time.monotonic()
|
|
185
|
+
|
|
186
|
+
for path in files:
|
|
187
|
+
if time.monotonic() - start > _INDEX_TIMEOUT_SECS:
|
|
188
|
+
log.warning("Code index build timed out after %.0fs", _INDEX_TIMEOUT_SECS)
|
|
189
|
+
break
|
|
190
|
+
try:
|
|
191
|
+
if force or self._needs_indexing(path):
|
|
192
|
+
self._index_file(path)
|
|
193
|
+
indexed += 1
|
|
194
|
+
except Exception as exc: # noqa: BLE001
|
|
195
|
+
log.debug("Skipping %s: %s", path, exc)
|
|
196
|
+
|
|
197
|
+
log.info("Code index: %d files indexed", indexed)
|
|
198
|
+
return indexed
|
|
199
|
+
|
|
200
|
+
def index_file(self, path: Path) -> int:
|
|
201
|
+
"""Index a single file (call after any write). Returns symbol count."""
|
|
202
|
+
try:
|
|
203
|
+
return self._index_file(path)
|
|
204
|
+
except Exception as exc: # noqa: BLE001
|
|
205
|
+
log.warning("Failed to index %s: %s", path, exc)
|
|
206
|
+
return 0
|
|
207
|
+
|
|
208
|
+
# ------------------------------------------------------------------
|
|
209
|
+
# Query API
|
|
210
|
+
# ------------------------------------------------------------------
|
|
211
|
+
|
|
212
|
+
def find_symbol(self, name: str) -> list[SymbolMatch]:
|
|
213
|
+
"""Find all symbols with this exact name across the project."""
|
|
214
|
+
rows = self._db.fetchall(
|
|
215
|
+
"SELECT file, symbol, kind, line, signature FROM code_index "
|
|
216
|
+
"WHERE project_id = ? AND symbol = ? ORDER BY file, line",
|
|
217
|
+
(self._project_id, name),
|
|
218
|
+
)
|
|
219
|
+
return [_row_to_match(r) for r in rows]
|
|
220
|
+
|
|
221
|
+
def search(self, query: str) -> list[SymbolMatch]:
|
|
222
|
+
"""Substring search on symbol names (case-insensitive)."""
|
|
223
|
+
rows = self._db.fetchall(
|
|
224
|
+
"SELECT file, symbol, kind, line, signature FROM code_index "
|
|
225
|
+
"WHERE project_id = ? AND symbol LIKE ? ORDER BY file, line LIMIT 30",
|
|
226
|
+
(self._project_id, f"%{query}%"),
|
|
227
|
+
)
|
|
228
|
+
return [_row_to_match(r) for r in rows]
|
|
229
|
+
|
|
230
|
+
def find_callers(self, symbol: str) -> list[tuple[str, int]]:
|
|
231
|
+
"""Find files + lines that reference *symbol* by grep. Returns (file, line) pairs."""
|
|
232
|
+
results: list[tuple[str, int]] = []
|
|
233
|
+
pattern = re.compile(r"\b" + re.escape(symbol) + r"\b")
|
|
234
|
+
for path in self._collect_source_files():
|
|
235
|
+
try:
|
|
236
|
+
text = path.read_text(encoding="utf-8", errors="replace")
|
|
237
|
+
for i, line in enumerate(text.splitlines(), 1):
|
|
238
|
+
if pattern.search(line):
|
|
239
|
+
rel = str(path.relative_to(self._root))
|
|
240
|
+
results.append((rel, i))
|
|
241
|
+
if len(results) >= 50:
|
|
242
|
+
return results
|
|
243
|
+
except OSError:
|
|
244
|
+
pass
|
|
245
|
+
return results
|
|
246
|
+
|
|
247
|
+
def get_symbol_signature(self, path: Path, symbol: str) -> str | None:
|
|
248
|
+
"""Return the signature line for *symbol* in *path*. Returns None if not found."""
|
|
249
|
+
rel = str(path.relative_to(self._root)) if path.is_absolute() else str(path)
|
|
250
|
+
row = self._db.fetchone(
|
|
251
|
+
"SELECT signature FROM code_index WHERE project_id = ? AND file = ? AND symbol = ?",
|
|
252
|
+
(self._project_id, rel, symbol),
|
|
253
|
+
)
|
|
254
|
+
return row["signature"] if row else None
|
|
255
|
+
|
|
256
|
+
def find_all_references(self, symbol: str) -> list[ReferenceMatch]:
|
|
257
|
+
"""Return all definitions and call-sites for *symbol*, capped at 200.
|
|
258
|
+
|
|
259
|
+
Definitions come first (from the DB); callers are appended (from grep).
|
|
260
|
+
Deduplicates by (file, line).
|
|
261
|
+
"""
|
|
262
|
+
seen: dict[tuple[str, int], ReferenceMatch] = {}
|
|
263
|
+
|
|
264
|
+
# 1. DB definitions
|
|
265
|
+
for row in self._db.fetchall(
|
|
266
|
+
"SELECT file, symbol, kind, line, signature FROM code_index "
|
|
267
|
+
"WHERE project_id = ? AND symbol = ? ORDER BY file, line",
|
|
268
|
+
(self._project_id, symbol),
|
|
269
|
+
):
|
|
270
|
+
key = (row["file"], row["line"])
|
|
271
|
+
if key not in seen:
|
|
272
|
+
seen[key] = ReferenceMatch(
|
|
273
|
+
symbol=symbol,
|
|
274
|
+
file=row["file"],
|
|
275
|
+
line=row["line"],
|
|
276
|
+
kind="definition",
|
|
277
|
+
snippet=row["signature"] or "",
|
|
278
|
+
)
|
|
279
|
+
|
|
280
|
+
# 2. Grep callers
|
|
281
|
+
for file_path, lineno in self.find_callers(symbol):
|
|
282
|
+
key = (file_path, lineno)
|
|
283
|
+
if key not in seen:
|
|
284
|
+
# Read the actual line for the snippet
|
|
285
|
+
abs_path = self._root / file_path
|
|
286
|
+
snippet = ""
|
|
287
|
+
try:
|
|
288
|
+
lines = abs_path.read_text(encoding="utf-8", errors="replace").splitlines()
|
|
289
|
+
if 0 < lineno <= len(lines):
|
|
290
|
+
snippet = lines[lineno - 1].strip()
|
|
291
|
+
except OSError:
|
|
292
|
+
pass
|
|
293
|
+
seen[key] = ReferenceMatch(
|
|
294
|
+
symbol=symbol,
|
|
295
|
+
file=file_path,
|
|
296
|
+
line=lineno,
|
|
297
|
+
kind="caller",
|
|
298
|
+
snippet=snippet,
|
|
299
|
+
)
|
|
300
|
+
if len(seen) >= 200:
|
|
301
|
+
break
|
|
302
|
+
|
|
303
|
+
results = list(seen.values())
|
|
304
|
+
results.sort(key=lambda r: (0 if r.kind == "definition" else 1, r.file, r.line))
|
|
305
|
+
return results[:200]
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
def _collect_source_files(self) -> list[Path]:
|
|
310
|
+
"""Return all indexable source files under project root."""
|
|
311
|
+
files: list[Path] = []
|
|
312
|
+
skip = {".git", "node_modules", "__pycache__", ".venv", "venv", "dist", "build"}
|
|
313
|
+
for path in self._root.rglob("*"):
|
|
314
|
+
if any(p in path.parts for p in skip):
|
|
315
|
+
continue
|
|
316
|
+
if path.suffix in _SUPPORTED_SUFFIXES and path.is_file():
|
|
317
|
+
if path.stat().st_size <= _MAX_FILE_SIZE_BYTES:
|
|
318
|
+
files.append(path)
|
|
319
|
+
return files
|
|
320
|
+
|
|
321
|
+
def _needs_indexing(self, path: Path) -> bool:
|
|
322
|
+
"""True if file has not been indexed yet or its mtime changed."""
|
|
323
|
+
rel = str(path.relative_to(self._root))
|
|
324
|
+
row = self._db.fetchone(
|
|
325
|
+
"SELECT mtime FROM code_index WHERE project_id = ? AND file = ? LIMIT 1",
|
|
326
|
+
(self._project_id, rel),
|
|
327
|
+
)
|
|
328
|
+
if row is None:
|
|
329
|
+
return True
|
|
330
|
+
return path.stat().st_mtime != row["mtime"]
|
|
331
|
+
|
|
332
|
+
def _index_file(self, path: Path) -> int:
|
|
333
|
+
"""Extract symbols from *path* and upsert into code_index."""
|
|
334
|
+
rel = str(path.relative_to(self._root))
|
|
335
|
+
try:
|
|
336
|
+
text = path.read_text(encoding="utf-8", errors="replace")
|
|
337
|
+
except OSError:
|
|
338
|
+
return 0
|
|
339
|
+
|
|
340
|
+
mtime = path.stat().st_mtime
|
|
341
|
+
|
|
342
|
+
suffix = path.suffix.lower()
|
|
343
|
+
if suffix == ".py":
|
|
344
|
+
symbols = _extract_python(text, rel)
|
|
345
|
+
elif suffix in (".ts", ".tsx", ".js", ".jsx"):
|
|
346
|
+
symbols = _extract_typescript(text, rel)
|
|
347
|
+
else:
|
|
348
|
+
return 0
|
|
349
|
+
|
|
350
|
+
# Clear old entries for this file then insert fresh
|
|
351
|
+
self._db.execute(
|
|
352
|
+
"DELETE FROM code_index WHERE project_id = ? AND file = ?",
|
|
353
|
+
(self._project_id, rel),
|
|
354
|
+
)
|
|
355
|
+
for sym in symbols:
|
|
356
|
+
self._db.execute(
|
|
357
|
+
"INSERT INTO code_index (project_id, file, symbol, kind, line, signature, mtime) "
|
|
358
|
+
"VALUES (?, ?, ?, ?, ?, ?, ?)",
|
|
359
|
+
(self._project_id, rel, sym.symbol, sym.kind, sym.line, sym.signature, mtime),
|
|
360
|
+
)
|
|
361
|
+
|
|
362
|
+
return len(symbols)
|
|
363
|
+
|
|
364
|
+
|
|
365
|
+
# ---------------------------------------------------------------------------
|
|
366
|
+
# Helpers
|
|
367
|
+
# ---------------------------------------------------------------------------
|
|
368
|
+
|
|
369
|
+
def _row_to_match(row: Any) -> SymbolMatch:
|
|
370
|
+
return SymbolMatch(
|
|
371
|
+
symbol=row["symbol"],
|
|
372
|
+
kind=row["kind"],
|
|
373
|
+
file=row["file"],
|
|
374
|
+
line=row["line"],
|
|
375
|
+
signature=row["signature"] or "",
|
|
376
|
+
)
|