codegraph-engine 3.0.0__1-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 (130) hide show
  1. codegraph/__init__.py +37 -0
  2. codegraph/agent.py +26 -0
  3. codegraph/agent_brain.py +2212 -0
  4. codegraph/agent_capabilities.py +4058 -0
  5. codegraph/agent_rules.py +531 -0
  6. codegraph/api_drift.py +455 -0
  7. codegraph/architecture.py +371 -0
  8. codegraph/async_queue/__init__.py +50 -0
  9. codegraph/async_queue/detectors/__init__.py +48 -0
  10. codegraph/async_queue/detectors/celery.py +160 -0
  11. codegraph/async_queue/detectors/kafka.py +168 -0
  12. codegraph/async_queue/detectors/rabbitmq.py +116 -0
  13. codegraph/async_queue/detectors/redis.py +130 -0
  14. codegraph/async_queue/detectors/sqs.py +135 -0
  15. codegraph/async_queue/models.py +161 -0
  16. codegraph/async_queue/otel_models.py +74 -0
  17. codegraph/async_queue/otel_parser.py +337 -0
  18. codegraph/async_queue/reconciler.py +427 -0
  19. codegraph/async_queue/scanner.py +291 -0
  20. codegraph/async_queue/traversal.py +447 -0
  21. codegraph/audit.py +106 -0
  22. codegraph/binding_resolver.py +616 -0
  23. codegraph/branch_comparison.py +197 -0
  24. codegraph/cache.py +95 -0
  25. codegraph/change_impact.py +547 -0
  26. codegraph/cli.py +2436 -0
  27. codegraph/cli_output.py +205 -0
  28. codegraph/config.py +45 -0
  29. codegraph/constraints.py +238 -0
  30. codegraph/context.py +2182 -0
  31. codegraph/context_freshness.py +351 -0
  32. codegraph/database/__init__.py +72 -0
  33. codegraph/database/extractor.py +3651 -0
  34. codegraph/database/interrogation.py +1494 -0
  35. codegraph/database/models.py +310 -0
  36. codegraph/database/schema_drift.py +862 -0
  37. codegraph/epistemic.py +200 -0
  38. codegraph/errors.py +282 -0
  39. codegraph/evidence/__init__.py +15 -0
  40. codegraph/evidence/citations.py +418 -0
  41. codegraph/evidence_contract.py +486 -0
  42. codegraph/frameworks.py +960 -0
  43. codegraph/frameworks_nestjs.py +502 -0
  44. codegraph/frameworks_nextjs.py +337 -0
  45. codegraph/frameworks_react.py +457 -0
  46. codegraph/freshness.py +335 -0
  47. codegraph/frontend_html.py +109 -0
  48. codegraph/git.py +362 -0
  49. codegraph/git_state.py +545 -0
  50. codegraph/graph/__init__.py +46 -0
  51. codegraph/graph/models.py +56 -0
  52. codegraph/graph/traversal.py +1993 -0
  53. codegraph/indexing/__init__.py +24 -0
  54. codegraph/indexing/classifier.py +564 -0
  55. codegraph/indexing/indexer.py +2578 -0
  56. codegraph/indexing/models.py +427 -0
  57. codegraph/indexing/parser.py +3404 -0
  58. codegraph/indexing/pool.py +136 -0
  59. codegraph/indexing/scanner.py +211 -0
  60. codegraph/indexing/telemetry.py +326 -0
  61. codegraph/indexing/templates.py +418 -0
  62. codegraph/indexing/test_framework.py +116 -0
  63. codegraph/installer.py +1702 -0
  64. codegraph/interrogation.py +2355 -0
  65. codegraph/llm/__init__.py +3 -0
  66. codegraph/llm/base.py +15 -0
  67. codegraph/llm/context.py +20 -0
  68. codegraph/mcp/__init__.py +3 -0
  69. codegraph/mcp/server.py +1476 -0
  70. codegraph/mcp_diagnostics.py +882 -0
  71. codegraph/memory/__init__.py +3 -0
  72. codegraph/memory/store.py +46 -0
  73. codegraph/models.py +310 -0
  74. codegraph/monorepo.py +925 -0
  75. codegraph/observability.py +262 -0
  76. codegraph/optimizer.py +1014 -0
  77. codegraph/planner.py +417 -0
  78. codegraph/process_lifecycle.py +1153 -0
  79. codegraph/py.typed +1 -0
  80. codegraph/query_expansion.py +199 -0
  81. codegraph/ranking.py +408 -0
  82. codegraph/refactor/__init__.py +36 -0
  83. codegraph/refactor/models.py +114 -0
  84. codegraph/refactor/renamer.py +844 -0
  85. codegraph/refactor/transactions.py +414 -0
  86. codegraph/resolver.py +3968 -0
  87. codegraph/resources/__init__.py +46 -0
  88. codegraph/resources/cache.py +117 -0
  89. codegraph/resources/coalescer.py +83 -0
  90. codegraph/resources/debouncer.py +98 -0
  91. codegraph/resources/governor.py +219 -0
  92. codegraph/resources/monitor.py +102 -0
  93. codegraph/resources/policy.py +123 -0
  94. codegraph/retrieval_policy.py +320 -0
  95. codegraph/roblox.py +2174 -0
  96. codegraph/route_composer.py +363 -0
  97. codegraph/route_topology.py +172 -0
  98. codegraph/runtime/__init__.py +51 -0
  99. codegraph/runtime/collector.py +155 -0
  100. codegraph/runtime/db_watcher.py +191 -0
  101. codegraph/runtime/ingestor.py +692 -0
  102. codegraph/runtime/models.py +130 -0
  103. codegraph/runtime/port_inspector.py +304 -0
  104. codegraph/runtime/reconciliation.py +492 -0
  105. codegraph/runtime/runner.py +353 -0
  106. codegraph/runtime/trace_linker.py +175 -0
  107. codegraph/search/__init__.py +25 -0
  108. codegraph/search/hybrid.py +846 -0
  109. codegraph/search/semantic.py +28 -0
  110. codegraph/security/__init__.py +51 -0
  111. codegraph/security/paths.py +189 -0
  112. codegraph/security/redaction.py +633 -0
  113. codegraph/semantic_conflicts.py +270 -0
  114. codegraph/semantic_decorators.py +372 -0
  115. codegraph/structural_diff.py +542 -0
  116. codegraph/styles.py +144 -0
  117. codegraph/symbol_history.py +277 -0
  118. codegraph/target_resolver.py +348 -0
  119. codegraph/task.py +637 -0
  120. codegraph/tool_selection_eval.py +1158 -0
  121. codegraph/tsconfig.py +236 -0
  122. codegraph/ui/__init__.py +6 -0
  123. codegraph/ui/server.py +755 -0
  124. codegraph/watcher.py +323 -0
  125. codegraph_engine-3.0.0.dist-info/METADATA +859 -0
  126. codegraph_engine-3.0.0.dist-info/RECORD +130 -0
  127. codegraph_engine-3.0.0.dist-info/WHEEL +6 -0
  128. codegraph_engine-3.0.0.dist-info/entry_points.txt +2 -0
  129. codegraph_engine-3.0.0.dist-info/licenses/LICENSE +21 -0
  130. codegraph_engine-3.0.0.dist-info/top_level.txt +1 -0
codegraph/git.py ADDED
@@ -0,0 +1,362 @@
1
+ """Read-only Git intelligence.
2
+
3
+ All Git operations use an explicit allowlist of commands and validated
4
+ arguments. No arbitrary shell execution. No network access.
5
+ """
6
+ from __future__ import annotations
7
+
8
+ import sqlite3
9
+ import subprocess
10
+ from dataclasses import dataclass
11
+ from pathlib import Path
12
+
13
+
14
+ @dataclass
15
+ class CommitInfo:
16
+ sha: str
17
+ author: str
18
+ date: str
19
+ message: str
20
+
21
+ def as_dict(self) -> dict[str, str]:
22
+ return {"sha": self.sha, "author": self.author, "date": self.date, "message": self.message}
23
+
24
+
25
+ @dataclass
26
+ class FileDiff:
27
+ path: str
28
+ status: str # M=modified, A=added, D=deleted, R=renamed
29
+ diff_snippet: str = ""
30
+
31
+ def as_dict(self) -> dict[str, str]:
32
+ return {"path": self.path, "status": self.status, "diff_snippet": self.diff_snippet}
33
+
34
+
35
+ def _git(repository: Path, *args: str, timeout: int = 15) -> str | None:
36
+ """Run a git subprocess with validated arguments only."""
37
+ # Validate: no shell metacharacters, no absolute paths from user input
38
+ for arg in args:
39
+ if any(c in arg for c in (";", "|", "&", "`", "$", "\n", "\r")):
40
+ return None
41
+ try:
42
+ result = subprocess.run(
43
+ ["git", *args],
44
+ cwd=str(repository),
45
+ capture_output=True,
46
+ text=True,
47
+ encoding="utf-8",
48
+ errors="replace",
49
+ timeout=timeout,
50
+ )
51
+ return result.stdout if result.returncode == 0 else None
52
+ except (FileNotFoundError, subprocess.TimeoutExpired, OSError):
53
+ return None
54
+
55
+
56
+ def is_git_repository(repository: Path) -> bool:
57
+ return _git(repository, "rev-parse", "--git-dir") is not None
58
+
59
+
60
+ def current_commit(repository: Path) -> str | None:
61
+ """Return the current HEAD commit SHA, or None if not a git repository."""
62
+ raw = _git(repository, "rev-parse", "HEAD")
63
+ return raw.strip() if raw else None
64
+
65
+
66
+ def recent_commits(
67
+ repository: Path,
68
+ n: int = 10,
69
+ path: str | None = None,
70
+ ) -> list[CommitInfo]:
71
+ """Return the last N commits, optionally scoped to a file path."""
72
+ if not 1 <= n <= 100:
73
+ n = 10
74
+ args = ["log", f"-{n}", "--format=%H\x1f%an\x1f%ai\x1f%s"]
75
+ if path:
76
+ # Sanitize: path must not be absolute or contain traversal
77
+ if ".." in path or path.startswith("/"):
78
+ return []
79
+ args += ["--", path]
80
+ raw = _git(repository, *args)
81
+ if not raw:
82
+ return []
83
+ commits = []
84
+ for line in raw.strip().splitlines():
85
+ parts = line.split("\x1f", 3)
86
+ if len(parts) == 4:
87
+ commits.append(CommitInfo(*parts))
88
+ return commits
89
+
90
+
91
+ def changed_files(
92
+ repository: Path,
93
+ since: str = "HEAD~10",
94
+ until: str = "HEAD",
95
+ ) -> list[FileDiff]:
96
+ """Return files changed between two commits."""
97
+ # Validate refs — allow only hex SHAs, HEAD, HEAD~N, branch names (alphanumeric + /-_.)
98
+ import re
99
+ _REF = re.compile(r"^[A-Za-z0-9_.~^/-]{1,100}$")
100
+ if not _REF.match(since) or not _REF.match(until):
101
+ return []
102
+ raw = _git(repository, "diff", "--name-status", since, until)
103
+ if not raw:
104
+ return []
105
+ diffs: list[FileDiff] = []
106
+ for line in raw.strip().splitlines():
107
+ parts = line.split("\t", 1)
108
+ if len(parts) == 2:
109
+ status, path = parts[0].strip()[0], parts[1].strip()
110
+ diffs.append(FileDiff(path=path, status=status))
111
+ return diffs
112
+
113
+
114
+ def file_diff(
115
+ repository: Path,
116
+ path: str,
117
+ since: str = "HEAD~1",
118
+ until: str = "HEAD",
119
+ max_lines: int = 50,
120
+ ) -> str:
121
+ """Return a bounded unified diff for a single file."""
122
+ import re
123
+ _REF = re.compile(r"^[A-Za-z0-9_.~^/-]{1,100}$")
124
+ if not _REF.match(since) or not _REF.match(until):
125
+ return ""
126
+ if ".." in path or path.startswith("/"):
127
+ return ""
128
+ raw = _git(repository, "diff", since, until, "--", path)
129
+ if not raw:
130
+ return ""
131
+ lines = raw.splitlines()[:max_lines]
132
+ return "\n".join(lines)
133
+
134
+
135
+ def blame_line(
136
+ repository: Path,
137
+ path: str,
138
+ line: int,
139
+ ) -> dict[str, str] | None:
140
+ """Return blame info for a single line."""
141
+ if ".." in path or path.startswith("/"):
142
+ return None
143
+ if not 1 <= line <= 100_000:
144
+ return None
145
+ raw = _git(
146
+ repository, "blame", f"-L{line},{line}", "--porcelain", "--", path
147
+ )
148
+ if not raw:
149
+ return None
150
+ info: dict[str, str] = {}
151
+ lines = raw.splitlines()
152
+ if lines:
153
+ info["sha"] = lines[0].split()[0] if lines else ""
154
+ for bl in lines[1:]:
155
+ if bl.startswith("author "):
156
+ info["author"] = bl[7:]
157
+ elif bl.startswith("author-time "):
158
+ info["timestamp"] = bl[12:]
159
+ elif bl.startswith("summary "):
160
+ info["summary"] = bl[8:]
161
+ return info or None
162
+
163
+
164
+ def changed_symbols_since(
165
+ repository: Path,
166
+ since: str,
167
+ symbol_names: set[str],
168
+ ) -> list[str]:
169
+ """Return subset of symbol_names that appear in changed lines since `since`.
170
+
171
+ This is a heuristic: it checks whether the symbol name appears in the
172
+ diff text. Results are LOW confidence.
173
+ """
174
+ import re
175
+ _REF = re.compile(r"^[A-Za-z0-9_.~^/-]{1,100}$")
176
+ if not _REF.match(since):
177
+ return []
178
+ raw = _git(repository, "diff", since, "HEAD")
179
+ if not raw:
180
+ return []
181
+ found = []
182
+ for name in symbol_names:
183
+ # Only match if the name appears in added/removed diff lines
184
+ if re.search(rf"^[+-].*\b{re.escape(name)}\b", raw, re.MULTILINE):
185
+ found.append(name)
186
+ return found
187
+
188
+
189
+ def get_file_history(
190
+ repository: Path,
191
+ path: str,
192
+ n: int = 10,
193
+ ) -> list[dict[str, str]]:
194
+ """Return commit history for a single file path."""
195
+ commits = recent_commits(repository, n=n, path=path)
196
+ return [c.as_dict() for c in commits]
197
+
198
+
199
+ def get_symbol_history(
200
+ repository: Path,
201
+ path: str,
202
+ symbol: str,
203
+ n: int = 10,
204
+ ) -> list[dict[str, str]]:
205
+ """Return commits where a specific symbol name was modified in a file."""
206
+ all_file_commits = recent_commits(repository, n=n * 2, path=path)
207
+ matching: list[dict[str, str]] = []
208
+ import re
209
+ for c in all_file_commits:
210
+ diff_text = file_diff(repository, path, since=f"{c.sha}~1", until=c.sha, max_lines=200)
211
+ if re.search(rf"\b{re.escape(symbol)}\b", diff_text):
212
+ matching.append(c.as_dict())
213
+ if len(matching) >= n:
214
+ break
215
+ return matching
216
+
217
+
218
+ def get_change_context(
219
+ repository: Path,
220
+ since: str = "HEAD~1",
221
+ until: str = "HEAD",
222
+ ) -> dict[str, object]:
223
+ """Return summary of changed files and bounded snippets."""
224
+ diffs = changed_files(repository, since=since, until=until)
225
+ file_summaries: list[dict[str, str]] = []
226
+ for d in diffs[:20]:
227
+ snippet = file_diff(repository, d.path, since=since, until=until, max_lines=30)
228
+ file_summaries.append(
229
+ {
230
+ "path": d.path,
231
+ "status": d.status,
232
+ "diff_snippet": snippet,
233
+ }
234
+ )
235
+ return {
236
+ "since": since,
237
+ "until": until,
238
+ "total_changed": len(diffs),
239
+ "files": file_summaries,
240
+ }
241
+
242
+
243
+ def analyze_change_impact(
244
+ con: sqlite3.Connection,
245
+ repository: Path,
246
+ since: str = "HEAD~1",
247
+ until: str = "HEAD",
248
+ ) -> dict[str, object]:
249
+ """Analyze downstream callers, routes, providers, and tests affected by changes between since and until."""
250
+ diffs = changed_files(repository, since=since, until=until)
251
+ changed_paths = [d.path for d in diffs]
252
+ changed_symbols: list[str] = []
253
+ removed_symbols: list[str] = []
254
+ affected_callers: list[dict[str, object]] = []
255
+ affected_tests: list[dict[str, object]] = []
256
+ affected_routes: list[dict[str, object]] = []
257
+ affected_providers: list[dict[str, object]] = []
258
+ impact_items: list[dict[str, object]] = []
259
+
260
+ from codegraph.graph.traversal import analyze_impact
261
+
262
+ # Check for removed symbols via git diff
263
+ for d in diffs:
264
+ f_diff = file_diff(repository, d.path, since=since, until=until, max_lines=500)
265
+ for line in f_diff.splitlines():
266
+ if line.startswith("-def ") or line.startswith("-class "):
267
+ rem_name = line[5:].split("(")[0].split(":")[0].strip()
268
+ if rem_name and rem_name not in removed_symbols:
269
+ removed_symbols.append(rem_name)
270
+ impact_items.append({
271
+ "source": rem_name,
272
+ "target": None,
273
+ "relationship": "UNKNOWN",
274
+ "category": "DIRECT",
275
+ "evidence_class": "UNKNOWN",
276
+ "freshness": "STALE",
277
+ "reason": "deleted_symbol",
278
+ "impact_reason": "deleted_symbol",
279
+ "file": d.path,
280
+ "rank": 8,
281
+ })
282
+
283
+ seen_callers: set[str] = set()
284
+ seen_tests: set[str] = set()
285
+
286
+ for p in changed_paths:
287
+ rows = con.execute("SELECT name, qualified_name FROM symbols WHERE path=?", (p,)).fetchall()
288
+ sym_names = {r["name"] for r in rows}
289
+ matched = changed_symbols_since(repository, since, sym_names)
290
+ for s in matched:
291
+ changed_symbols.append(s)
292
+
293
+ # Run rich analyze_impact for each modified symbol
294
+ impact_res = analyze_impact(con, s, max_depth=2, max_results=20)
295
+
296
+ raw_items = impact_res.get("impact_items")
297
+ if isinstance(raw_items, list):
298
+ for item in raw_items:
299
+ if isinstance(item, dict):
300
+ impact_items.append(item)
301
+
302
+ raw_callers = impact_res.get("direct_callers")
303
+ if isinstance(raw_callers, list):
304
+ for c in raw_callers:
305
+ if isinstance(c, dict):
306
+ c_key = f"{c.get('file')}:{c.get('symbol')}"
307
+ if c_key not in seen_callers:
308
+ seen_callers.add(c_key)
309
+ affected_callers.append(c)
310
+
311
+ raw_tests = impact_res.get("related_tests")
312
+ if isinstance(raw_tests, list):
313
+ for t in raw_tests:
314
+ if isinstance(t, dict) and "result" not in t:
315
+ t_key = f"{t.get('file')}:{t.get('symbol')}"
316
+ if t_key not in seen_tests:
317
+ seen_tests.add(t_key)
318
+ affected_tests.append(t)
319
+
320
+ raw_apis = impact_res.get("related_apis")
321
+ if isinstance(raw_apis, list):
322
+ for r in raw_apis:
323
+ if isinstance(r, dict):
324
+ affected_routes.append(r)
325
+
326
+ cats = impact_res.get("categories")
327
+ if isinstance(cats, dict):
328
+ di_list = cats.get("DI")
329
+ if isinstance(di_list, list):
330
+ for di in di_list:
331
+ if isinstance(di, dict):
332
+ affected_providers.append(di)
333
+
334
+ # Monorepo / Package boundary impact analysis
335
+ from codegraph.monorepo import detect_workspace
336
+ ws = detect_workspace(repository, con=con)
337
+ affected_packages: set[str] = set()
338
+ for p in changed_paths:
339
+ pkg = ws.get_package_for_file(p)
340
+ if pkg:
341
+ affected_packages.add(pkg.package_id)
342
+
343
+ affected_dependent_packages: set[str] = set()
344
+ for pkg_id in affected_packages:
345
+ for dep_pkg in ws.get_dependent_packages(pkg_id):
346
+ affected_dependent_packages.add(dep_pkg.package_id)
347
+
348
+ return {
349
+ "since": since,
350
+ "until": until,
351
+ "changed_files": [d.as_dict() for d in diffs],
352
+ "changed_symbols": changed_symbols,
353
+ "removed_symbols": removed_symbols,
354
+ "affected_callers": affected_callers,
355
+ "affected_tests": affected_tests,
356
+ "affected_routes": affected_routes,
357
+ "affected_providers": affected_providers,
358
+ "affected_packages": sorted(affected_packages),
359
+ "affected_dependent_packages": sorted(affected_dependent_packages),
360
+ "impact_items": impact_items,
361
+ }
362
+