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.
Files changed (131) hide show
  1. gdmcode-0.1.0.dist-info/METADATA +240 -0
  2. gdmcode-0.1.0.dist-info/RECORD +131 -0
  3. gdmcode-0.1.0.dist-info/WHEEL +4 -0
  4. gdmcode-0.1.0.dist-info/entry_points.txt +2 -0
  5. src/__init__.py +1 -0
  6. src/_internal/__init__.py +0 -0
  7. src/_internal/constants.py +244 -0
  8. src/_internal/domain_skills.py +339 -0
  9. src/agent/__init__.py +0 -0
  10. src/agent/commit_classifier.py +91 -0
  11. src/agent/context_budget.py +391 -0
  12. src/agent/daemon.py +681 -0
  13. src/agent/dag_validator.py +153 -0
  14. src/agent/debug_loop.py +473 -0
  15. src/agent/impact_analyzer.py +149 -0
  16. src/agent/impact_graph.py +117 -0
  17. src/agent/loop.py +1410 -0
  18. src/agent/orchestrator.py +141 -0
  19. src/agent/regression_guard.py +251 -0
  20. src/agent/review_gate.py +648 -0
  21. src/agent/risk_scorer.py +169 -0
  22. src/agent/self_healing.py +145 -0
  23. src/agent/smart_test_selector.py +89 -0
  24. src/agent/system_prompt.py +226 -0
  25. src/agent/task_tracker.py +320 -0
  26. src/agent/test_validator.py +210 -0
  27. src/agent/tool_orchestrator.py +402 -0
  28. src/agent/transcript.py +230 -0
  29. src/agent/verification_loop.py +133 -0
  30. src/agent/work_director.py +136 -0
  31. src/agent/worktree_manager.py +53 -0
  32. src/artifacts/__init__.py +16 -0
  33. src/artifacts/artifact_store.py +456 -0
  34. src/artifacts/verification_graph.py +75 -0
  35. src/auth.py +411 -0
  36. src/cli.py +1290 -0
  37. src/commands.py +1398 -0
  38. src/config.py +762 -0
  39. src/cost_tracker.py +348 -0
  40. src/db/__init__.py +4 -0
  41. src/db/migrations.py +337 -0
  42. src/enterprise/__init__.py +3 -0
  43. src/enterprise/audit_log.py +182 -0
  44. src/enterprise/identity.py +90 -0
  45. src/enterprise/rbac.py +100 -0
  46. src/enterprise/team_config.py +125 -0
  47. src/enterprise/usage_analytics.py +261 -0
  48. src/exceptions.py +207 -0
  49. src/git_workflow.py +651 -0
  50. src/integrations/__init__.py +6 -0
  51. src/integrations/github_actions.py +106 -0
  52. src/integrations/mcp_server.py +333 -0
  53. src/integrations/sentry_integration.py +100 -0
  54. src/integrations/sentry_server.py +82 -0
  55. src/integrations/webhook_security.py +19 -0
  56. src/main.py +27 -0
  57. src/memory/__init__.py +0 -0
  58. src/memory/code_index.py +376 -0
  59. src/memory/compressor.py +378 -0
  60. src/memory/context_memory.py +135 -0
  61. src/memory/continuous_memory.py +234 -0
  62. src/memory/conventions.py +495 -0
  63. src/memory/db.py +1119 -0
  64. src/memory/document_index.py +205 -0
  65. src/memory/file_cache.py +128 -0
  66. src/memory/project_scanner.py +178 -0
  67. src/memory/session_store.py +201 -0
  68. src/models/__init__.py +0 -0
  69. src/models/client.py +715 -0
  70. src/models/definitions.py +459 -0
  71. src/models/router.py +418 -0
  72. src/models/schemas.py +389 -0
  73. src/permissions.py +294 -0
  74. src/remote/__init__.py +5 -0
  75. src/remote/command_filter.py +33 -0
  76. src/remote/models.py +31 -0
  77. src/remote/permission_handler.py +79 -0
  78. src/remote/phone_ui.py +48 -0
  79. src/remote/protocol.py +59 -0
  80. src/remote/qr.py +65 -0
  81. src/remote/server.py +586 -0
  82. src/remote/token_manager.py +61 -0
  83. src/remote/tunnel.py +212 -0
  84. src/repl.py +475 -0
  85. src/runtime/__init__.py +1 -0
  86. src/runtime/branch_farm.py +372 -0
  87. src/runtime/replay.py +351 -0
  88. src/sandbox/__init__.py +2 -0
  89. src/sandbox/hermetic.py +214 -0
  90. src/sandbox/policy.py +44 -0
  91. src/sdk/__init__.py +3 -0
  92. src/sdk/plugin_base.py +39 -0
  93. src/sdk/plugin_host.py +100 -0
  94. src/sdk/plugin_loader.py +101 -0
  95. src/security.py +409 -0
  96. src/server/__init__.py +7 -0
  97. src/server/bridge.py +427 -0
  98. src/server/bridge_cli.py +103 -0
  99. src/server/bridge_client.py +170 -0
  100. src/server/protocol_version.py +103 -0
  101. src/session/__init__.py +10 -0
  102. src/session/event_fanout.py +46 -0
  103. src/session/input_broker.py +38 -0
  104. src/session/permission_bridge.py +100 -0
  105. src/tools/__init__.py +160 -0
  106. src/tools/_atomic.py +72 -0
  107. src/tools/agent_tools.py +423 -0
  108. src/tools/ask_user_tool.py +83 -0
  109. src/tools/bash_tool.py +384 -0
  110. src/tools/browser_tool.py +352 -0
  111. src/tools/browser_tools.py +179 -0
  112. src/tools/dep_tools.py +210 -0
  113. src/tools/document_reader.py +167 -0
  114. src/tools/document_tool.py +240 -0
  115. src/tools/document_writer.py +171 -0
  116. src/tools/impact_tools.py +240 -0
  117. src/tools/playwright_tool.py +172 -0
  118. src/tools/quality_tools.py +366 -0
  119. src/tools/read_tools.py +318 -0
  120. src/tools/result_cache.py +157 -0
  121. src/tools/search_tools.py +310 -0
  122. src/tools/shell_tools.py +311 -0
  123. src/tools/write_tools.py +337 -0
  124. src/voice/__init__.py +25 -0
  125. src/voice/audio_capture.py +92 -0
  126. src/voice/audio_playback.py +68 -0
  127. src/voice/errors.py +14 -0
  128. src/voice/models.py +35 -0
  129. src/voice/providers.py +143 -0
  130. src/voice/vad.py +55 -0
  131. src/voice/voice_loop.py +156 -0
@@ -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
+ )