codegraph-engine 2.3.0__py3-none-any.whl → 3.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 (118) hide show
  1. codegraph/__init__.py +1 -1
  2. codegraph/__main__.py +7 -0
  3. codegraph/agent_brain.py +253 -2
  4. codegraph/agent_capabilities.py +1348 -67
  5. codegraph/agent_rules.py +2 -1
  6. codegraph/api_drift.py +455 -0
  7. codegraph/architecture_rules.py +123 -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/change_impact.py +320 -110
  22. codegraph/changelog.py +170 -0
  23. codegraph/cli.py +1605 -62
  24. codegraph/cli_output.py +17 -2
  25. codegraph/context.py +135 -24
  26. codegraph/context_freshness.py +2 -0
  27. codegraph/database/__init__.py +14 -0
  28. codegraph/database/extractor.py +712 -17
  29. codegraph/database/interrogation.py +388 -2
  30. codegraph/database/schema_drift.py +862 -0
  31. codegraph/dataflow_propagation.py +118 -0
  32. codegraph/dead_code.py +387 -0
  33. codegraph/epistemic.py +15 -0
  34. codegraph/evidence_contract.py +34 -3
  35. codegraph/federation.py +245 -0
  36. codegraph/frameworks.py +234 -12
  37. codegraph/frameworks_nestjs.py +502 -0
  38. codegraph/frameworks_nextjs.py +337 -0
  39. codegraph/frameworks_react.py +457 -0
  40. codegraph/freshness.py +2 -0
  41. codegraph/frontend_html.py +109 -0
  42. codegraph/git.py +2 -0
  43. codegraph/git_state.py +2 -0
  44. codegraph/graph/traversal.py +150 -7
  45. codegraph/graph_csr.py +218 -0
  46. codegraph/incremental.py +150 -0
  47. codegraph/indexing/__init__.py +7 -1
  48. codegraph/indexing/classifier.py +5 -0
  49. codegraph/indexing/indexer.py +292 -38
  50. codegraph/indexing/languages/__init__.py +1 -0
  51. codegraph/indexing/languages/cpp.py +457 -0
  52. codegraph/indexing/languages/go.py +459 -0
  53. codegraph/indexing/languages/jvm.py +583 -0
  54. codegraph/indexing/languages/rust.py +507 -0
  55. codegraph/indexing/models.py +15 -1
  56. codegraph/indexing/parser.py +385 -9
  57. codegraph/indexing/pool.py +136 -0
  58. codegraph/indexing/protocols.py +220 -0
  59. codegraph/indexing/scanner.py +23 -0
  60. codegraph/indexing/templates.py +418 -0
  61. codegraph/interrogation.py +207 -39
  62. codegraph/lsp.py +318 -0
  63. codegraph/lsp_bridge/__init__.py +16 -0
  64. codegraph/lsp_bridge/bridge.py +102 -0
  65. codegraph/lsp_bridge/client.py +207 -0
  66. codegraph/lsp_bridge/models.py +71 -0
  67. codegraph/mcp/server.py +502 -29
  68. codegraph/memory/trajectory.py +282 -0
  69. codegraph/models.py +7 -0
  70. codegraph/monorepo.py +3 -0
  71. codegraph/multi_agent/__init__.py +13 -0
  72. codegraph/multi_agent/coordinator.py +229 -0
  73. codegraph/multi_agent/models.py +76 -0
  74. codegraph/optimizer.py +42 -5
  75. codegraph/polymorphism.py +95 -0
  76. codegraph/pr_review.py +296 -0
  77. codegraph/process_lifecycle.py +50 -12
  78. codegraph/reachability.py +198 -0
  79. codegraph/reachability_index.py +74 -0
  80. codegraph/refactor/__init__.py +48 -0
  81. codegraph/refactor/models.py +114 -0
  82. codegraph/refactor/patcher.py +348 -0
  83. codegraph/refactor/renamer.py +844 -0
  84. codegraph/refactor/transactions.py +414 -0
  85. codegraph/repair.py +233 -0
  86. codegraph/resolver.py +1357 -161
  87. codegraph/retrieval_policy.py +22 -1
  88. codegraph/roblox.py +2174 -0
  89. codegraph/route_composer.py +7 -0
  90. codegraph/route_topology.py +172 -0
  91. codegraph/runtime/__init__.py +25 -0
  92. codegraph/runtime/collector.py +155 -0
  93. codegraph/runtime/db_watcher.py +191 -0
  94. codegraph/runtime/ingestor.py +30 -10
  95. codegraph/runtime/otel_receiver.py +208 -0
  96. codegraph/runtime/port_inspector.py +304 -0
  97. codegraph/runtime/runner.py +155 -6
  98. codegraph/runtime/trace_linker.py +175 -0
  99. codegraph/sandbox.py +215 -0
  100. codegraph/search/hybrid.py +26 -0
  101. codegraph/security/supply_chain.py +188 -0
  102. codegraph/security/taint.py +421 -0
  103. codegraph/semantic_decorators.py +34 -3
  104. codegraph/structural_diff.py +2 -0
  105. codegraph/styles.py +144 -0
  106. codegraph/symbol_history.py +2 -0
  107. codegraph/test_gen.py +437 -0
  108. codegraph/tsconfig.py +236 -0
  109. codegraph/ui/__init__.py +6 -0
  110. codegraph/ui/server.py +876 -0
  111. codegraph/watcher.py +323 -0
  112. {codegraph_engine-2.3.0.dist-info → codegraph_engine-3.0.1.dist-info}/METADATA +365 -67
  113. codegraph_engine-3.0.1.dist-info/RECORD +164 -0
  114. {codegraph_engine-2.3.0.dist-info → codegraph_engine-3.0.1.dist-info}/WHEEL +1 -1
  115. codegraph_engine-2.3.0.dist-info/RECORD +0 -94
  116. {codegraph_engine-2.3.0.dist-info → codegraph_engine-3.0.1.dist-info}/entry_points.txt +0 -0
  117. {codegraph_engine-2.3.0.dist-info → codegraph_engine-3.0.1.dist-info}/licenses/LICENSE +0 -0
  118. {codegraph_engine-2.3.0.dist-info → codegraph_engine-3.0.1.dist-info}/top_level.txt +0 -0
@@ -16,6 +16,7 @@ import json
16
16
  import posixpath
17
17
  import re
18
18
  from dataclasses import asdict, dataclass
19
+ from typing import cast
19
20
 
20
21
  from codegraph.frameworks import (
21
22
  AnalysisContext,
@@ -27,6 +28,9 @@ from codegraph.frameworks import (
27
28
  analyze_js_ts_mounts,
28
29
  get_python_analyzers,
29
30
  )
31
+ from codegraph.frameworks_nestjs import analyze_nestjs_file
32
+ from codegraph.frameworks_nextjs import analyze_nextjs_file
33
+ from codegraph.frameworks_react import analyze_react_file
30
34
  from codegraph.semantic_decorators import analyze_symbol_decorators
31
35
 
32
36
  from .models import (
@@ -215,6 +219,30 @@ def parse(content: str, language: str, file_path: str) -> ParseResult:
215
219
  """Parse a source file in a single scoped pass."""
216
220
  if language == "python":
217
221
  return _parse_python(content, file_path)
222
+ if language == "luau" or file_path.lower().endswith((".lua", ".luau")):
223
+ from codegraph.roblox import parse_luau
224
+ return parse_luau(content, file_path)
225
+ if language == "go" or file_path.lower().endswith(".go"):
226
+ from codegraph.indexing.languages.go import parse_go
227
+ return cast(ParseResult, parse_go(content, file_path))
228
+ if language == "rust" or file_path.lower().endswith(".rs"):
229
+ from codegraph.indexing.languages.rust import parse_rust
230
+ return cast(ParseResult, parse_rust(content, file_path))
231
+ if language in ("java", "kotlin") or file_path.lower().endswith((".java", ".kt", ".kts")):
232
+ from codegraph.indexing.languages.jvm import parse_jvm
233
+ return cast(ParseResult, parse_jvm(content, file_path))
234
+ if language in ("c", "cpp", "c_header", "cpp_header") or file_path.lower().endswith((".c", ".cc", ".cpp", ".cxx", ".h", ".hpp", ".hxx")):
235
+ from codegraph.indexing.languages.cpp import parse_cpp
236
+ return cast(ParseResult, parse_cpp(content, file_path))
237
+ if language == "proto" or file_path.lower().endswith(".proto"):
238
+ from codegraph.indexing.protocols import parse_proto
239
+ return cast(ParseResult, parse_proto(content, file_path))
240
+ if language == "graphql" or file_path.lower().endswith((".graphql", ".gql")):
241
+ from codegraph.indexing.protocols import parse_graphql
242
+ return cast(ParseResult, parse_graphql(content, file_path))
243
+ if language == "html" or file_path.lower().endswith((".html", ".htm", ".jinja", ".jinja2", ".njk", ".ejs")):
244
+ from codegraph.indexing.templates import parse_html_template
245
+ return parse_html_template(content, file_path)
218
246
  if language in ("sql", "prisma"):
219
247
  source_hash = hashlib.sha256(content.encode("utf-8", errors="replace")).hexdigest()
220
248
  return ParseResult(
@@ -434,6 +462,7 @@ class _PythonScopeVisitor(ast.NodeVisitor):
434
462
  self.bindings: list[BindingRef] = []
435
463
  self.exports: list[str] = []
436
464
  self.conditional_depth: int = 0
465
+ self.di_type_aliases: dict[str, str] = {}
437
466
 
438
467
  # Scope stack: (name, kind, qualified_name, canonical_id)
439
468
  self.scope_stack: list[tuple[str, str, str, str]] = []
@@ -825,6 +854,36 @@ class _PythonScopeVisitor(ast.NodeVisitor):
825
854
  dep_expr = ast.unparse(elts[0]).split("[")[0].strip()
826
855
  break
827
856
 
857
+ # Check if annotation refers to an Annotated type alias: e.g. session: SessionDep, current_user: CurrentUser
858
+ if not dep_expr and arg.annotation and hasattr(ast, "unparse"):
859
+ ann_text = ast.unparse(arg.annotation).strip()
860
+ ann_short = ann_text.split("[")[0].strip().split(".")[-1]
861
+ if ann_short in self.di_type_aliases:
862
+ dep_expr = self.di_type_aliases[ann_short]
863
+ elif (
864
+ ann_short not in _PYTHON_BUILTINS
865
+ and not ann_short.startswith("_")
866
+ and ann_short not in (
867
+ "Any", "Optional", "Union", "List", "Dict", "Set", "Tuple",
868
+ "int", "str", "float", "bool", "bytes", "dict", "list", "set", "tuple",
869
+ )
870
+ ):
871
+ self.bindings.append(
872
+ BindingRef(
873
+ target_name=canon_id,
874
+ file_path=self.file_path,
875
+ line=node.lineno,
876
+ column=getattr(node, "col_offset", None),
877
+ scope=self.current_scope_qname,
878
+ expr_kind="PARAM_TYPE_DI",
879
+ source_expr=ann_text,
880
+ is_conditional=is_cond,
881
+ base_expr="fastapi",
882
+ attr_name="Depends",
883
+ subscript_key=json.dumps({"param": arg.arg, "alias": ann_short, "framework": "fastapi"}),
884
+ )
885
+ )
886
+
828
887
  if dep_expr:
829
888
  self.bindings.append(
830
889
  BindingRef(
@@ -1354,12 +1413,104 @@ class _PythonScopeVisitor(ast.NodeVisitor):
1354
1413
  )
1355
1414
  )
1356
1415
 
1416
+ # 10. Modern FastAPI / typing Annotated DI alias
1417
+ # e.g., SessionDep = Annotated[Session, Depends(get_db)]
1418
+ # CurrentUser = Annotated[User, Depends(get_current_user)]
1419
+ # TokenDep = Annotated[str, Depends(reusable_oauth2)]
1420
+ if isinstance(value_node, ast.Subscript) and hasattr(ast, "unparse"):
1421
+ val_id = getattr(value_node.value, "id", None) or (
1422
+ getattr(value_node.value, "attr", None) if isinstance(value_node.value, ast.Attribute) else ""
1423
+ )
1424
+ val_unparse = ast.unparse(value_node.value) if hasattr(ast, "unparse") else ""
1425
+ if val_id in ("Annotated", "typing.Annotated", "typing_extensions.Annotated") or "Annotated" in val_unparse:
1426
+ slice_node = value_node.slice
1427
+ elts = slice_node.elts if isinstance(slice_node, (ast.Tuple, ast.List)) else [slice_node]
1428
+ for elt in elts[1:]:
1429
+ if isinstance(elt, ast.Call):
1430
+ f_nm = getattr(elt.func, "id", None) or (
1431
+ getattr(elt.func, "attr", None) if isinstance(elt.func, ast.Attribute) else ""
1432
+ )
1433
+ if f_nm == "Depends":
1434
+ elt_arg: ast.expr | None = elt.args[0] if elt.args else None
1435
+ if elt_arg is None:
1436
+ elt_kw = next((kw for kw in elt.keywords if kw.arg == "dependency"), None)
1437
+ if elt_kw is not None:
1438
+ elt_arg = elt_kw.value
1439
+ dep_alias_str: str | None = None
1440
+ if elt_arg is not None:
1441
+ if isinstance(elt_arg, (ast.Name, ast.Attribute)):
1442
+ dep_alias_str = ast.unparse(elt_arg)
1443
+ else:
1444
+ dep_alias_str = "<DYNAMIC>"
1445
+ elif hasattr(ast, "unparse"):
1446
+ dep_alias_str = ast.unparse(elts[0]).split("[")[0].strip()
1447
+ if dep_alias_str:
1448
+ self.di_type_aliases[target_name] = dep_alias_str
1449
+ self.bindings.append(
1450
+ BindingRef(
1451
+ target_name=target_name,
1452
+ file_path=self.file_path,
1453
+ line=line,
1454
+ column=col,
1455
+ scope=scope,
1456
+ expr_kind="DI_ALIAS_INJECTS",
1457
+ source_expr=dep_alias_str,
1458
+ is_conditional=is_cond,
1459
+ base_expr="fastapi",
1460
+ attr_name="Depends",
1461
+ subscript_key=json.dumps({"alias": target_name, "framework": "fastapi"}),
1462
+ )
1463
+ )
1464
+ break
1465
+
1357
1466
  def visit_Assign(self, node: ast.Assign) -> None:
1358
1467
  for target in node.targets:
1359
1468
  if isinstance(target, ast.Name):
1360
1469
  self._extract_binding_from_assign(
1361
1470
  target.id, node.value, node.lineno, getattr(node, "col_offset", None)
1362
1471
  )
1472
+ if target.id in ("template_name", "template") and isinstance(node.value, ast.Constant) and isinstance(node.value.value, str):
1473
+ self.bindings.append(
1474
+ BindingRef(
1475
+ target_name=node.value.value,
1476
+ file_path=self.file_path,
1477
+ line=node.lineno,
1478
+ column=getattr(node, "col_offset", None),
1479
+ scope=self.current_scope_qname or "",
1480
+ expr_kind="RENDERS_TEMPLATE",
1481
+ base_expr=self.current_scope_canonical_id or "",
1482
+ source_expr=node.value.value,
1483
+ )
1484
+ )
1485
+ elif isinstance(node.value, ast.BinOp) and isinstance(node.value.op, ast.BitOr):
1486
+ # Unfold bitwise OR chain (e.g. prompt | llm | output_parser)
1487
+ pipe_steps: list[str] = []
1488
+ curr: ast.expr = node.value
1489
+ is_valid_pipe = True
1490
+ while isinstance(curr, ast.BinOp) and isinstance(curr.op, ast.BitOr):
1491
+ r_text = ast.unparse(curr.right).strip() if hasattr(ast, "unparse") else ""
1492
+ if not r_text or isinstance(curr.right, ast.Constant):
1493
+ is_valid_pipe = False
1494
+ break
1495
+ pipe_steps.append(r_text)
1496
+ curr = curr.left
1497
+ l_text = ast.unparse(curr).strip() if hasattr(ast, "unparse") else ""
1498
+ if is_valid_pipe and l_text and not isinstance(curr, ast.Constant):
1499
+ pipe_steps.append(l_text)
1500
+ pipe_steps.reverse()
1501
+ if len(pipe_steps) >= 2:
1502
+ self.bindings.append(
1503
+ BindingRef(
1504
+ target_name=target.id,
1505
+ file_path=self.file_path,
1506
+ line=node.lineno,
1507
+ column=getattr(node, "col_offset", None),
1508
+ scope=self.current_scope_qname or "",
1509
+ expr_kind="LCEL_PIPELINE",
1510
+ base_expr="|".join(pipe_steps),
1511
+ source_expr=json.dumps(pipe_steps),
1512
+ )
1513
+ )
1363
1514
  elif (
1364
1515
  isinstance(target, ast.Attribute)
1365
1516
  and isinstance(target.value, ast.Name)
@@ -1575,6 +1726,39 @@ class _PythonScopeVisitor(ast.NodeVisitor):
1575
1726
  caller_canon = self.current_scope_canonical_id
1576
1727
  end_ln = node.end_lineno or node.lineno
1577
1728
 
1729
+ # Python View -> Template rendering (Flask render_template, Django render, FastAPI TemplateResponse)
1730
+ template_name: str | None = None
1731
+ if isinstance(node.func, ast.Name):
1732
+ if node.func.id == "render_template" and node.args:
1733
+ if isinstance(node.args[0], ast.Constant) and isinstance(node.args[0].value, str):
1734
+ template_name = node.args[0].value
1735
+ elif node.func.id == "render" and len(node.args) >= 2:
1736
+ if isinstance(node.args[1], ast.Constant) and isinstance(node.args[1].value, str):
1737
+ template_name = node.args[1].value
1738
+ elif isinstance(node.func, ast.Attribute):
1739
+ if node.func.attr == "TemplateResponse" and node.args:
1740
+ if isinstance(node.args[0], ast.Constant) and isinstance(node.args[0].value, str):
1741
+ template_name = node.args[0].value
1742
+ elif len(node.args) >= 2 and isinstance(node.args[1], ast.Constant) and isinstance(node.args[1].value, str):
1743
+ template_name = node.args[1].value
1744
+ for kw in node.keywords:
1745
+ if kw.arg in ("name", "template_name") and isinstance(kw.value, ast.Constant) and isinstance(kw.value.value, str):
1746
+ template_name = kw.value.value
1747
+
1748
+ if template_name:
1749
+ self.bindings.append(
1750
+ BindingRef(
1751
+ target_name=template_name,
1752
+ file_path=self.file_path,
1753
+ line=node.lineno,
1754
+ column=getattr(node, "col_offset", None),
1755
+ scope=caller_qname or "",
1756
+ expr_kind="RENDERS_TEMPLATE",
1757
+ base_expr=caller_canon or "",
1758
+ source_expr=template_name,
1759
+ )
1760
+ )
1761
+
1578
1762
  if isinstance(node.func, ast.Name):
1579
1763
  callee_name = node.func.id
1580
1764
  if callee_name == "subscribe" and len(node.args) >= 2 and hasattr(ast, "unparse"):
@@ -2010,6 +2194,106 @@ class _PythonScopeVisitor(ast.NodeVisitor):
2010
2194
  subscript_key=ev_str,
2011
2195
  )
2012
2196
  )
2197
+ # Celery / Background task / Ray remote task invocation (.delay(), .apply_async(), .s(), .si(), .signature(), .remote())
2198
+ if attr_name in ("delay", "apply_async", "s", "si", "signature", "remote"):
2199
+ task_callee = recv_clean.split(".")[-1]
2200
+ if task_callee and task_callee not in ("self", "cls"):
2201
+ self.calls.append(
2202
+ CallRef(
2203
+ callee=task_callee,
2204
+ qualified_callee=recv_clean,
2205
+ line=node.lineno,
2206
+ end_line=end_ln,
2207
+ source_file=self.file_path,
2208
+ confidence="HIGH",
2209
+ caller_symbol=caller_qname,
2210
+ caller_canonical_id=caller_canon,
2211
+ receiver=recv_clean if "." in recv_clean else None,
2212
+ )
2213
+ )
2214
+ dispatch_kind = "RAY_REMOTE" if attr_name == "remote" else "CELERY_DISPATCH"
2215
+ self.bindings.append(
2216
+ BindingRef(
2217
+ target_name=task_callee,
2218
+ file_path=self.file_path,
2219
+ line=node.lineno,
2220
+ column=getattr(node, "col_offset", None),
2221
+ scope=caller_qname or "",
2222
+ expr_kind=dispatch_kind,
2223
+ base_expr=attr_name,
2224
+ source_expr=recv_clean,
2225
+ )
2226
+ )
2227
+ elif attr_name == "send_task" and node.args and isinstance(node.args[0], ast.Constant) and isinstance(node.args[0].value, str):
2228
+ full_task = node.args[0].value
2229
+ task_short = full_task.split(".")[-1]
2230
+ self.calls.append(
2231
+ CallRef(
2232
+ callee=task_short,
2233
+ qualified_callee=full_task,
2234
+ line=node.lineno,
2235
+ end_line=end_ln,
2236
+ source_file=self.file_path,
2237
+ confidence="HIGH",
2238
+ caller_symbol=caller_qname,
2239
+ caller_canonical_id=caller_canon,
2240
+ receiver=full_task if "." in full_task else None,
2241
+ )
2242
+ )
2243
+ self.bindings.append(
2244
+ BindingRef(
2245
+ target_name=task_short,
2246
+ file_path=self.file_path,
2247
+ line=node.lineno,
2248
+ column=getattr(node, "col_offset", None),
2249
+ scope=caller_qname or "",
2250
+ expr_kind="CELERY_DISPATCH",
2251
+ base_expr="send_task",
2252
+ source_expr=full_task,
2253
+ )
2254
+ )
2255
+ elif attr_name == "connect" and node.args and hasattr(ast, "unparse"):
2256
+ receiver_node = node.args[0]
2257
+ receiver_name = ast.unparse(receiver_node).strip()
2258
+ sender_name = ""
2259
+ for kw in node.keywords:
2260
+ if kw.arg == "sender":
2261
+ if isinstance(kw.value, ast.Constant) and isinstance(kw.value.value, str):
2262
+ sender_name = kw.value.value
2263
+ elif isinstance(kw.value, (ast.Name, ast.Attribute)):
2264
+ sender_name = ast.unparse(kw.value).strip()
2265
+ self.bindings.append(
2266
+ BindingRef(
2267
+ target_name=receiver_name,
2268
+ file_path=self.file_path,
2269
+ line=node.lineno,
2270
+ column=getattr(node, "col_offset", None),
2271
+ scope=self.current_scope_qname or "",
2272
+ expr_kind="DJANGO_SIGNAL_RECEIVER",
2273
+ base_expr=recv_clean,
2274
+ source_expr=sender_name,
2275
+ )
2276
+ )
2277
+ elif attr_name in ("send", "send_robust") and hasattr(ast, "unparse"):
2278
+ sender_name = ""
2279
+ for kw in node.keywords:
2280
+ if kw.arg == "sender":
2281
+ if isinstance(kw.value, ast.Constant) and isinstance(kw.value.value, str):
2282
+ sender_name = kw.value.value
2283
+ elif isinstance(kw.value, (ast.Name, ast.Attribute)):
2284
+ sender_name = ast.unparse(kw.value).strip()
2285
+ self.bindings.append(
2286
+ BindingRef(
2287
+ target_name=recv_clean,
2288
+ file_path=self.file_path,
2289
+ line=node.lineno,
2290
+ column=getattr(node, "col_offset", None),
2291
+ scope=self.current_scope_qname or "",
2292
+ expr_kind="DJANGO_SIGNAL_SEND",
2293
+ base_expr=attr_name,
2294
+ source_expr=sender_name,
2295
+ )
2296
+ )
2013
2297
 
2014
2298
  q_callee = f"{resolved_recv}.{attr_name}" if resolved_recv else attr_name
2015
2299
  self.calls.append(
@@ -2042,6 +2326,26 @@ class _PythonScopeVisitor(ast.NodeVisitor):
2042
2326
  )
2043
2327
  )
2044
2328
 
2329
+ # Agent tool registration (e.g. AgentExecutor(tools=[...]), create_openai_tools_agent(..., tools=[...]))
2330
+ for kw in node.keywords:
2331
+ if kw.arg in ("tools", "functions") and isinstance(kw.value, (ast.List, ast.Tuple)):
2332
+ func_label = ast.unparse(node.func).strip() if hasattr(ast, "unparse") else ""
2333
+ for elt in kw.value.elts:
2334
+ t_name = ast.unparse(elt).strip() if hasattr(ast, "unparse") else ""
2335
+ if t_name:
2336
+ self.bindings.append(
2337
+ BindingRef(
2338
+ target_name=t_name,
2339
+ file_path=self.file_path,
2340
+ line=node.lineno,
2341
+ column=getattr(node, "col_offset", None),
2342
+ scope=caller_qname or "",
2343
+ expr_kind="AGENT_TOOL_REGISTRATION",
2344
+ base_expr=func_label,
2345
+ source_expr=t_name,
2346
+ )
2347
+ )
2348
+
2045
2349
  self.generic_visit(node)
2046
2350
 
2047
2351
 
@@ -2096,19 +2400,47 @@ def _extract_all_python_imports(
2096
2400
  return imports
2097
2401
 
2098
2402
 
2403
+ def _repair_python_syntax(content: str) -> str:
2404
+ """Repair common syntax patterns from newer or experimental Python versions (e.g. unparenthesized multiple except)."""
2405
+ lines = content.splitlines()
2406
+ repaired = False
2407
+ new_lines = []
2408
+ for line in lines:
2409
+ # Match 'except A, B:' or 'except A, B as e:' or 'except* A, B:' without enclosing parentheses
2410
+ m = re.match(
2411
+ r'^(\s*except\*?\s+)([A-Za-z0-9_.\s]+,\s*[A-Za-z0-9_.,\s]+?)(\s+as\s+[A-Za-z0-9_]+)?(\s*:.*)$',
2412
+ line,
2413
+ )
2414
+ if m:
2415
+ prefix, types, as_part, suffix = m.group(1), m.group(2).strip(), m.group(3) or "", m.group(4)
2416
+ if not types.startswith("("):
2417
+ line = f"{prefix}({types}){as_part}{suffix}"
2418
+ repaired = True
2419
+ new_lines.append(line)
2420
+ return "\n".join(new_lines) if repaired else content
2421
+
2422
+
2099
2423
  def _parse_python(content: str, file_path: str) -> ParseResult:
2100
2424
  source_hash = hashlib.sha256(content.encode("utf-8", errors="replace")).hexdigest()
2425
+ tree: ast.Module | None = None
2101
2426
  try:
2102
2427
  tree = ast.parse(content)
2103
2428
  except SyntaxError as exc:
2104
- return ParseResult(
2105
- path=file_path,
2106
- language="python",
2107
- source_hash=source_hash,
2108
- imports=tuple(_py_imports_fallback(content, file_path)),
2109
- parse_failed=True,
2110
- parse_error=f"SyntaxError at line {exc.lineno}: {exc.msg}",
2111
- )
2429
+ repaired = _repair_python_syntax(content)
2430
+ if repaired != content:
2431
+ try:
2432
+ tree = ast.parse(repaired)
2433
+ except SyntaxError:
2434
+ tree = None
2435
+ if tree is None:
2436
+ return ParseResult(
2437
+ path=file_path,
2438
+ language="python",
2439
+ source_hash=source_hash,
2440
+ imports=tuple(_py_imports_fallback(content, file_path)),
2441
+ parse_failed=True,
2442
+ parse_error=f"SyntaxError at line {exc.lineno}: {exc.msg}",
2443
+ )
2112
2444
 
2113
2445
  # First collect import module names cheaply from top-level imports to configure analyzers
2114
2446
  import_mods: set[str] = set()
@@ -2732,7 +3064,7 @@ def _parse_js_ts(content: str, language: str, file_path: str) -> ParseResult:
2732
3064
  )
2733
3065
 
2734
3066
  # Route detection hook per line (Express and Next.js)
2735
- detected_routes = analyze_js_ts_line(line, lineno, file_path, module, import_mods_set)
3067
+ detected_routes = analyze_js_ts_line(line, lineno, file_path, module, import_mods_set, lines, idx)
2736
3068
  if detected_routes:
2737
3069
  routes.extend(detected_routes)
2738
3070
  detected_mounts = analyze_js_ts_mounts(line, lineno, file_path, module)
@@ -3013,6 +3345,50 @@ def _parse_js_ts(content: str, language: str, file_path: str) -> ParseResult:
3013
3345
  )
3014
3346
  )
3015
3347
 
3348
+ # Framework file-level enhancements (NestJS and Next.js)
3349
+ if "@" in content:
3350
+ nest_res = analyze_nestjs_file(content, file_path, module)
3351
+ if nest_res.routes:
3352
+ routes.extend(nest_res.routes)
3353
+ if nest_res.mounts:
3354
+ mounts.extend(nest_res.mounts)
3355
+ if nest_res.bindings:
3356
+ bindings.extend(nest_res.bindings)
3357
+
3358
+ next_res = analyze_nextjs_file(content, file_path, module)
3359
+ if next_res.routes:
3360
+ seen_ep_ids = {r.endpoint_id for r in routes}
3361
+ for nr in next_res.routes:
3362
+ if nr.endpoint_id not in seen_ep_ids:
3363
+ routes.append(nr)
3364
+ seen_ep_ids.add(nr.endpoint_id)
3365
+ if next_res.bindings:
3366
+ bindings.extend(next_res.bindings)
3367
+
3368
+ # React and JSX component architecture (Pillar 4)
3369
+ react_res = analyze_react_file(content, file_path, module)
3370
+ sym_indices = {s.canonical_id: i for i, s in enumerate(symbols)}
3371
+ for sym in react_res.components:
3372
+ if sym.canonical_id in sym_indices:
3373
+ symbols[sym_indices[sym.canonical_id]] = sym
3374
+ else:
3375
+ seen_canonical.add(sym.canonical_id)
3376
+ symbols.append(sym)
3377
+ for h_sym in react_res.hooks:
3378
+ if h_sym.canonical_id in sym_indices:
3379
+ symbols[sym_indices[h_sym.canonical_id]] = h_sym
3380
+ else:
3381
+ seen_canonical.add(h_sym.canonical_id)
3382
+ symbols.append(h_sym)
3383
+ if react_res.bindings:
3384
+ bindings.extend(react_res.bindings)
3385
+
3386
+ if language == "typescript" and ("@Service" in content or "@Controller" in content or "Dependency<" in content):
3387
+ from codegraph.roblox import extract_flamework_bindings
3388
+ fw_bindings = extract_flamework_bindings(content, file_path, module)
3389
+ if fw_bindings:
3390
+ bindings.extend(fw_bindings)
3391
+
3016
3392
  symbols.sort(key=lambda s: (s.start_line, s.canonical_id))
3017
3393
  return ParseResult(
3018
3394
  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