codegraph-engine 2.2.1__py3-none-any.whl → 3.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 (82) hide show
  1. codegraph/__init__.py +1 -1
  2. codegraph/agent_brain.py +172 -2
  3. codegraph/agent_capabilities.py +564 -6
  4. codegraph/agent_rules.py +1 -1
  5. codegraph/api_drift.py +455 -0
  6. codegraph/async_queue/__init__.py +50 -0
  7. codegraph/async_queue/detectors/__init__.py +48 -0
  8. codegraph/async_queue/detectors/celery.py +160 -0
  9. codegraph/async_queue/detectors/kafka.py +168 -0
  10. codegraph/async_queue/detectors/rabbitmq.py +116 -0
  11. codegraph/async_queue/detectors/redis.py +130 -0
  12. codegraph/async_queue/detectors/sqs.py +135 -0
  13. codegraph/async_queue/models.py +161 -0
  14. codegraph/async_queue/otel_models.py +74 -0
  15. codegraph/async_queue/otel_parser.py +337 -0
  16. codegraph/async_queue/reconciler.py +427 -0
  17. codegraph/async_queue/scanner.py +291 -0
  18. codegraph/async_queue/traversal.py +447 -0
  19. codegraph/branch_comparison.py +197 -0
  20. codegraph/change_impact.py +547 -0
  21. codegraph/cli.py +992 -3
  22. codegraph/cli_output.py +17 -2
  23. codegraph/context.py +38 -11
  24. codegraph/context_freshness.py +351 -0
  25. codegraph/database/__init__.py +14 -0
  26. codegraph/database/extractor.py +712 -17
  27. codegraph/database/interrogation.py +388 -2
  28. codegraph/database/schema_drift.py +862 -0
  29. codegraph/epistemic.py +7 -0
  30. codegraph/evidence_contract.py +21 -0
  31. codegraph/frameworks.py +171 -6
  32. codegraph/frameworks_nestjs.py +502 -0
  33. codegraph/frameworks_nextjs.py +337 -0
  34. codegraph/frameworks_react.py +457 -0
  35. codegraph/freshness.py +2 -0
  36. codegraph/frontend_html.py +109 -0
  37. codegraph/git.py +2 -0
  38. codegraph/git_state.py +545 -0
  39. codegraph/indexing/__init__.py +7 -1
  40. codegraph/indexing/indexer.py +291 -38
  41. codegraph/indexing/models.py +6 -0
  42. codegraph/indexing/parser.py +239 -0
  43. codegraph/indexing/pool.py +136 -0
  44. codegraph/indexing/scanner.py +6 -0
  45. codegraph/indexing/templates.py +418 -0
  46. codegraph/interrogation.py +128 -20
  47. codegraph/mcp/server.py +258 -27
  48. codegraph/models.py +7 -0
  49. codegraph/monorepo.py +3 -0
  50. codegraph/optimizer.py +42 -5
  51. codegraph/process_lifecycle.py +31 -13
  52. codegraph/refactor/__init__.py +36 -0
  53. codegraph/refactor/models.py +114 -0
  54. codegraph/refactor/renamer.py +844 -0
  55. codegraph/refactor/transactions.py +403 -0
  56. codegraph/resolver.py +1202 -159
  57. codegraph/retrieval_policy.py +6 -0
  58. codegraph/route_topology.py +172 -0
  59. codegraph/runtime/__init__.py +19 -0
  60. codegraph/runtime/collector.py +155 -0
  61. codegraph/runtime/db_watcher.py +191 -0
  62. codegraph/runtime/ingestor.py +30 -10
  63. codegraph/runtime/port_inspector.py +304 -0
  64. codegraph/runtime/runner.py +155 -6
  65. codegraph/runtime/trace_linker.py +175 -0
  66. codegraph/search/hybrid.py +26 -0
  67. codegraph/semantic_conflicts.py +270 -0
  68. codegraph/semantic_decorators.py +34 -3
  69. codegraph/structural_diff.py +542 -0
  70. codegraph/styles.py +144 -0
  71. codegraph/symbol_history.py +277 -0
  72. codegraph/tsconfig.py +236 -0
  73. codegraph/ui/__init__.py +6 -0
  74. codegraph/ui/server.py +715 -0
  75. codegraph/watcher.py +323 -0
  76. {codegraph_engine-2.2.1.dist-info → codegraph_engine-3.0.0.dist-info}/METADATA +257 -39
  77. codegraph_engine-3.0.0.dist-info/RECORD +129 -0
  78. codegraph_engine-2.2.1.dist-info/RECORD +0 -87
  79. {codegraph_engine-2.2.1.dist-info → codegraph_engine-3.0.0.dist-info}/WHEEL +0 -0
  80. {codegraph_engine-2.2.1.dist-info → codegraph_engine-3.0.0.dist-info}/entry_points.txt +0 -0
  81. {codegraph_engine-2.2.1.dist-info → codegraph_engine-3.0.0.dist-info}/licenses/LICENSE +0 -0
  82. {codegraph_engine-2.2.1.dist-info → codegraph_engine-3.0.0.dist-info}/top_level.txt +0 -0
@@ -27,6 +27,9 @@ from codegraph.frameworks import (
27
27
  analyze_js_ts_mounts,
28
28
  get_python_analyzers,
29
29
  )
30
+ from codegraph.frameworks_nestjs import analyze_nestjs_file
31
+ from codegraph.frameworks_nextjs import analyze_nextjs_file
32
+ from codegraph.frameworks_react import analyze_react_file
30
33
  from codegraph.semantic_decorators import analyze_symbol_decorators
31
34
 
32
35
  from .models import (
@@ -215,6 +218,9 @@ def parse(content: str, language: str, file_path: str) -> ParseResult:
215
218
  """Parse a source file in a single scoped pass."""
216
219
  if language == "python":
217
220
  return _parse_python(content, file_path)
221
+ if language == "html" or file_path.lower().endswith((".html", ".htm", ".jinja", ".jinja2", ".njk", ".ejs")):
222
+ from codegraph.indexing.templates import parse_html_template
223
+ return parse_html_template(content, file_path)
218
224
  if language in ("sql", "prisma"):
219
225
  source_hash = hashlib.sha256(content.encode("utf-8", errors="replace")).hexdigest()
220
226
  return ParseResult(
@@ -1360,6 +1366,48 @@ class _PythonScopeVisitor(ast.NodeVisitor):
1360
1366
  self._extract_binding_from_assign(
1361
1367
  target.id, node.value, node.lineno, getattr(node, "col_offset", None)
1362
1368
  )
1369
+ if target.id in ("template_name", "template") and isinstance(node.value, ast.Constant) and isinstance(node.value.value, str):
1370
+ self.bindings.append(
1371
+ BindingRef(
1372
+ target_name=node.value.value,
1373
+ file_path=self.file_path,
1374
+ line=node.lineno,
1375
+ column=getattr(node, "col_offset", None),
1376
+ scope=self.current_scope_qname or "",
1377
+ expr_kind="RENDERS_TEMPLATE",
1378
+ base_expr=self.current_scope_canonical_id or "",
1379
+ source_expr=node.value.value,
1380
+ )
1381
+ )
1382
+ elif isinstance(node.value, ast.BinOp) and isinstance(node.value.op, ast.BitOr):
1383
+ # Unfold bitwise OR chain (e.g. prompt | llm | output_parser)
1384
+ pipe_steps: list[str] = []
1385
+ curr: ast.expr = node.value
1386
+ is_valid_pipe = True
1387
+ while isinstance(curr, ast.BinOp) and isinstance(curr.op, ast.BitOr):
1388
+ r_text = ast.unparse(curr.right).strip() if hasattr(ast, "unparse") else ""
1389
+ if not r_text or isinstance(curr.right, ast.Constant):
1390
+ is_valid_pipe = False
1391
+ break
1392
+ pipe_steps.append(r_text)
1393
+ curr = curr.left
1394
+ l_text = ast.unparse(curr).strip() if hasattr(ast, "unparse") else ""
1395
+ if is_valid_pipe and l_text and not isinstance(curr, ast.Constant):
1396
+ pipe_steps.append(l_text)
1397
+ pipe_steps.reverse()
1398
+ if len(pipe_steps) >= 2:
1399
+ self.bindings.append(
1400
+ BindingRef(
1401
+ target_name=target.id,
1402
+ file_path=self.file_path,
1403
+ line=node.lineno,
1404
+ column=getattr(node, "col_offset", None),
1405
+ scope=self.current_scope_qname or "",
1406
+ expr_kind="LCEL_PIPELINE",
1407
+ base_expr="|".join(pipe_steps),
1408
+ source_expr=json.dumps(pipe_steps),
1409
+ )
1410
+ )
1363
1411
  elif (
1364
1412
  isinstance(target, ast.Attribute)
1365
1413
  and isinstance(target.value, ast.Name)
@@ -1575,6 +1623,39 @@ class _PythonScopeVisitor(ast.NodeVisitor):
1575
1623
  caller_canon = self.current_scope_canonical_id
1576
1624
  end_ln = node.end_lineno or node.lineno
1577
1625
 
1626
+ # Python View -> Template rendering (Flask render_template, Django render, FastAPI TemplateResponse)
1627
+ template_name: str | None = None
1628
+ if isinstance(node.func, ast.Name):
1629
+ if node.func.id == "render_template" and node.args:
1630
+ if isinstance(node.args[0], ast.Constant) and isinstance(node.args[0].value, str):
1631
+ template_name = node.args[0].value
1632
+ elif node.func.id == "render" and len(node.args) >= 2:
1633
+ if isinstance(node.args[1], ast.Constant) and isinstance(node.args[1].value, str):
1634
+ template_name = node.args[1].value
1635
+ elif isinstance(node.func, ast.Attribute):
1636
+ if node.func.attr == "TemplateResponse" and node.args:
1637
+ if isinstance(node.args[0], ast.Constant) and isinstance(node.args[0].value, str):
1638
+ template_name = node.args[0].value
1639
+ elif len(node.args) >= 2 and isinstance(node.args[1], ast.Constant) and isinstance(node.args[1].value, str):
1640
+ template_name = node.args[1].value
1641
+ for kw in node.keywords:
1642
+ if kw.arg in ("name", "template_name") and isinstance(kw.value, ast.Constant) and isinstance(kw.value.value, str):
1643
+ template_name = kw.value.value
1644
+
1645
+ if template_name:
1646
+ self.bindings.append(
1647
+ BindingRef(
1648
+ target_name=template_name,
1649
+ file_path=self.file_path,
1650
+ line=node.lineno,
1651
+ column=getattr(node, "col_offset", None),
1652
+ scope=caller_qname or "",
1653
+ expr_kind="RENDERS_TEMPLATE",
1654
+ base_expr=caller_canon or "",
1655
+ source_expr=template_name,
1656
+ )
1657
+ )
1658
+
1578
1659
  if isinstance(node.func, ast.Name):
1579
1660
  callee_name = node.func.id
1580
1661
  if callee_name == "subscribe" and len(node.args) >= 2 and hasattr(ast, "unparse"):
@@ -2010,6 +2091,106 @@ class _PythonScopeVisitor(ast.NodeVisitor):
2010
2091
  subscript_key=ev_str,
2011
2092
  )
2012
2093
  )
2094
+ # Celery / Background task / Ray remote task invocation (.delay(), .apply_async(), .s(), .si(), .signature(), .remote())
2095
+ if attr_name in ("delay", "apply_async", "s", "si", "signature", "remote"):
2096
+ task_callee = recv_clean.split(".")[-1]
2097
+ if task_callee and task_callee not in ("self", "cls"):
2098
+ self.calls.append(
2099
+ CallRef(
2100
+ callee=task_callee,
2101
+ qualified_callee=recv_clean,
2102
+ line=node.lineno,
2103
+ end_line=end_ln,
2104
+ source_file=self.file_path,
2105
+ confidence="HIGH",
2106
+ caller_symbol=caller_qname,
2107
+ caller_canonical_id=caller_canon,
2108
+ receiver=recv_clean if "." in recv_clean else None,
2109
+ )
2110
+ )
2111
+ dispatch_kind = "RAY_REMOTE" if attr_name == "remote" else "CELERY_DISPATCH"
2112
+ self.bindings.append(
2113
+ BindingRef(
2114
+ target_name=task_callee,
2115
+ file_path=self.file_path,
2116
+ line=node.lineno,
2117
+ column=getattr(node, "col_offset", None),
2118
+ scope=caller_qname or "",
2119
+ expr_kind=dispatch_kind,
2120
+ base_expr=attr_name,
2121
+ source_expr=recv_clean,
2122
+ )
2123
+ )
2124
+ elif attr_name == "send_task" and node.args and isinstance(node.args[0], ast.Constant) and isinstance(node.args[0].value, str):
2125
+ full_task = node.args[0].value
2126
+ task_short = full_task.split(".")[-1]
2127
+ self.calls.append(
2128
+ CallRef(
2129
+ callee=task_short,
2130
+ qualified_callee=full_task,
2131
+ line=node.lineno,
2132
+ end_line=end_ln,
2133
+ source_file=self.file_path,
2134
+ confidence="HIGH",
2135
+ caller_symbol=caller_qname,
2136
+ caller_canonical_id=caller_canon,
2137
+ receiver=full_task if "." in full_task else None,
2138
+ )
2139
+ )
2140
+ self.bindings.append(
2141
+ BindingRef(
2142
+ target_name=task_short,
2143
+ file_path=self.file_path,
2144
+ line=node.lineno,
2145
+ column=getattr(node, "col_offset", None),
2146
+ scope=caller_qname or "",
2147
+ expr_kind="CELERY_DISPATCH",
2148
+ base_expr="send_task",
2149
+ source_expr=full_task,
2150
+ )
2151
+ )
2152
+ elif attr_name == "connect" and node.args and hasattr(ast, "unparse"):
2153
+ receiver_node = node.args[0]
2154
+ receiver_name = ast.unparse(receiver_node).strip()
2155
+ sender_name = ""
2156
+ for kw in node.keywords:
2157
+ if kw.arg == "sender":
2158
+ if isinstance(kw.value, ast.Constant) and isinstance(kw.value.value, str):
2159
+ sender_name = kw.value.value
2160
+ elif isinstance(kw.value, (ast.Name, ast.Attribute)):
2161
+ sender_name = ast.unparse(kw.value).strip()
2162
+ self.bindings.append(
2163
+ BindingRef(
2164
+ target_name=receiver_name,
2165
+ file_path=self.file_path,
2166
+ line=node.lineno,
2167
+ column=getattr(node, "col_offset", None),
2168
+ scope=self.current_scope_qname or "",
2169
+ expr_kind="DJANGO_SIGNAL_RECEIVER",
2170
+ base_expr=recv_clean,
2171
+ source_expr=sender_name,
2172
+ )
2173
+ )
2174
+ elif attr_name in ("send", "send_robust") and hasattr(ast, "unparse"):
2175
+ sender_name = ""
2176
+ for kw in node.keywords:
2177
+ if kw.arg == "sender":
2178
+ if isinstance(kw.value, ast.Constant) and isinstance(kw.value.value, str):
2179
+ sender_name = kw.value.value
2180
+ elif isinstance(kw.value, (ast.Name, ast.Attribute)):
2181
+ sender_name = ast.unparse(kw.value).strip()
2182
+ self.bindings.append(
2183
+ BindingRef(
2184
+ target_name=recv_clean,
2185
+ file_path=self.file_path,
2186
+ line=node.lineno,
2187
+ column=getattr(node, "col_offset", None),
2188
+ scope=self.current_scope_qname or "",
2189
+ expr_kind="DJANGO_SIGNAL_SEND",
2190
+ base_expr=attr_name,
2191
+ source_expr=sender_name,
2192
+ )
2193
+ )
2013
2194
 
2014
2195
  q_callee = f"{resolved_recv}.{attr_name}" if resolved_recv else attr_name
2015
2196
  self.calls.append(
@@ -2042,6 +2223,26 @@ class _PythonScopeVisitor(ast.NodeVisitor):
2042
2223
  )
2043
2224
  )
2044
2225
 
2226
+ # Agent tool registration (e.g. AgentExecutor(tools=[...]), create_openai_tools_agent(..., tools=[...]))
2227
+ for kw in node.keywords:
2228
+ if kw.arg in ("tools", "functions") and isinstance(kw.value, (ast.List, ast.Tuple)):
2229
+ func_label = ast.unparse(node.func).strip() if hasattr(ast, "unparse") else ""
2230
+ for elt in kw.value.elts:
2231
+ t_name = ast.unparse(elt).strip() if hasattr(ast, "unparse") else ""
2232
+ if t_name:
2233
+ self.bindings.append(
2234
+ BindingRef(
2235
+ target_name=t_name,
2236
+ file_path=self.file_path,
2237
+ line=node.lineno,
2238
+ column=getattr(node, "col_offset", None),
2239
+ scope=caller_qname or "",
2240
+ expr_kind="AGENT_TOOL_REGISTRATION",
2241
+ base_expr=func_label,
2242
+ source_expr=t_name,
2243
+ )
2244
+ )
2245
+
2045
2246
  self.generic_visit(node)
2046
2247
 
2047
2248
 
@@ -3013,6 +3214,44 @@ def _parse_js_ts(content: str, language: str, file_path: str) -> ParseResult:
3013
3214
  )
3014
3215
  )
3015
3216
 
3217
+ # Framework file-level enhancements (NestJS and Next.js)
3218
+ if "@" in content:
3219
+ nest_res = analyze_nestjs_file(content, file_path, module)
3220
+ if nest_res.routes:
3221
+ routes.extend(nest_res.routes)
3222
+ if nest_res.mounts:
3223
+ mounts.extend(nest_res.mounts)
3224
+ if nest_res.bindings:
3225
+ bindings.extend(nest_res.bindings)
3226
+
3227
+ next_res = analyze_nextjs_file(content, file_path, module)
3228
+ if next_res.routes:
3229
+ seen_ep_ids = {r.endpoint_id for r in routes}
3230
+ for nr in next_res.routes:
3231
+ if nr.endpoint_id not in seen_ep_ids:
3232
+ routes.append(nr)
3233
+ seen_ep_ids.add(nr.endpoint_id)
3234
+ if next_res.bindings:
3235
+ bindings.extend(next_res.bindings)
3236
+
3237
+ # React and JSX component architecture (Pillar 4)
3238
+ react_res = analyze_react_file(content, file_path, module)
3239
+ sym_indices = {s.canonical_id: i for i, s in enumerate(symbols)}
3240
+ for sym in react_res.components:
3241
+ if sym.canonical_id in sym_indices:
3242
+ symbols[sym_indices[sym.canonical_id]] = sym
3243
+ else:
3244
+ seen_canonical.add(sym.canonical_id)
3245
+ symbols.append(sym)
3246
+ for h_sym in react_res.hooks:
3247
+ if h_sym.canonical_id in sym_indices:
3248
+ symbols[sym_indices[h_sym.canonical_id]] = h_sym
3249
+ else:
3250
+ seen_canonical.add(h_sym.canonical_id)
3251
+ symbols.append(h_sym)
3252
+ if react_res.bindings:
3253
+ bindings.extend(react_res.bindings)
3254
+
3016
3255
  symbols.sort(key=lambda s: (s.start_line, s.canonical_id))
3017
3256
  return ParseResult(
3018
3257
  path=file_path,
@@ -0,0 +1,136 @@
1
+ """Thread-safe SQLite connection pool for concurrent MCP and CLI execution."""
2
+ from __future__ import annotations
3
+
4
+ import queue
5
+ import sqlite3
6
+ import threading
7
+ from collections.abc import Callable
8
+ from pathlib import Path
9
+
10
+ from .classifier import make_compat_row
11
+
12
+
13
+ class SQLiteConnectionPool:
14
+ """Thread-safe, re-entrant connection pool for SQLite databases.
15
+
16
+ Features:
17
+ - Thread-safe LIFO pool to maximize SQLite cache reuse.
18
+ - WAL mode, synchronous=NORMAL, foreign_keys=ON, busy_timeout=30000ms.
19
+ - check_same_thread=False for cross-thread pool handoff.
20
+ - Re-entrant per-thread acquisition (nesting depth tracking via thread-local state).
21
+ - Optional initializer hook for idempotent schema / setup execution.
22
+ """
23
+
24
+ def __init__(
25
+ self,
26
+ db_path: Path | str,
27
+ max_size: int = 16,
28
+ timeout: float = 30.0,
29
+ initializer: Callable[[sqlite3.Connection], None] | None = None,
30
+ ) -> None:
31
+ self.db_path = Path(db_path) if isinstance(db_path, str) else db_path
32
+ self.max_size = max(1, max_size)
33
+ self.timeout = timeout
34
+ self.initializer = initializer
35
+ self._pool: queue.LifoQueue[sqlite3.Connection] = queue.LifoQueue()
36
+ self._created = 0
37
+ self._lock = threading.Lock()
38
+ self._local = threading.local()
39
+
40
+ def _create_connection(self) -> sqlite3.Connection:
41
+ con = sqlite3.connect(
42
+ self.db_path,
43
+ check_same_thread=False,
44
+ timeout=self.timeout,
45
+ )
46
+ con.row_factory = make_compat_row
47
+ con.execute("PRAGMA journal_mode = WAL")
48
+ con.execute("PRAGMA synchronous = NORMAL")
49
+ con.execute("PRAGMA foreign_keys = ON")
50
+ con.execute("PRAGMA busy_timeout = 30000")
51
+ con.execute("PRAGMA cache_size = -64000")
52
+ con.execute("PRAGMA temp_store = MEMORY")
53
+ if self.initializer is not None:
54
+ self.initializer(con)
55
+ return con
56
+
57
+ def acquire(self) -> tuple[sqlite3.Connection, bool]:
58
+ """Acquire a connection. Returns (connection, is_owner).
59
+
60
+ If the current thread already holds a connection from this pool,
61
+ it increments the re-entrancy depth and returns (connection, False).
62
+ """
63
+ active_con = getattr(self._local, "con", None)
64
+ if active_con is not None:
65
+ self._local.nesting = getattr(self._local, "nesting", 0) + 1
66
+ return active_con, False
67
+
68
+ con: sqlite3.Connection | None = None
69
+ try:
70
+ con = self._pool.get_nowait()
71
+ except queue.Empty:
72
+ with self._lock:
73
+ if self._created < self.max_size:
74
+ con = self._create_connection()
75
+ self._created += 1
76
+
77
+ if con is None:
78
+ con = self._pool.get(timeout=self.timeout)
79
+
80
+ self._local.con = con
81
+ self._local.nesting = 1
82
+ return con, True
83
+
84
+ def release(
85
+ self,
86
+ con: sqlite3.Connection,
87
+ is_owner: bool,
88
+ rollback: bool = False,
89
+ ) -> None:
90
+ """Release a connection back to the pool."""
91
+ if not is_owner:
92
+ cur_nesting = getattr(self._local, "nesting", 1) - 1
93
+ self._local.nesting = max(0, cur_nesting)
94
+ if rollback:
95
+ try:
96
+ con.rollback()
97
+ except Exception:
98
+ pass
99
+ return
100
+
101
+ self._local.con = None
102
+ self._local.nesting = 0
103
+ if rollback:
104
+ try:
105
+ con.rollback()
106
+ except Exception:
107
+ pass
108
+ else:
109
+ try:
110
+ con.commit()
111
+ except Exception:
112
+ pass
113
+
114
+ try:
115
+ self._pool.put_nowait(con)
116
+ except Exception:
117
+ try:
118
+ con.close()
119
+ except Exception:
120
+ pass
121
+
122
+ def close_all(self) -> None:
123
+ """Close and dispose all connections in the pool."""
124
+ with self._lock:
125
+ while True:
126
+ try:
127
+ con = self._pool.get_nowait()
128
+ try:
129
+ con.close()
130
+ except Exception:
131
+ pass
132
+ except queue.Empty:
133
+ break
134
+ self._created = 0
135
+ self._local.con = None
136
+ self._local.nesting = 0
@@ -43,6 +43,12 @@ LANGUAGES = {
43
43
  ".tsx": "typescript",
44
44
  ".sql": "sql",
45
45
  ".prisma": "prisma",
46
+ ".html": "html",
47
+ ".htm": "html",
48
+ ".jinja": "html",
49
+ ".jinja2": "html",
50
+ ".njk": "html",
51
+ ".ejs": "html",
46
52
  }
47
53
 
48
54