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
@@ -548,6 +548,8 @@ _TEXT_LANGUAGE_MAP: dict[str, str] = {
548
548
  ".sql": "sql",
549
549
  ".prisma": "prisma",
550
550
  ".sh": "shell",
551
+ ".lua": "luau",
552
+ ".luau": "luau",
551
553
  }
552
554
 
553
555
 
@@ -1500,7 +1502,7 @@ def trace_path(
1500
1502
  "path": [],
1501
1503
  }
1502
1504
 
1503
- # Resolve from and to targets (prefer exact canonical_id or qualified_name)
1505
+ # Resolve from and to targets (prefer exact canonical_id or qualified_name, or virtual graph_edges nodes like Remotes)
1504
1506
  from_row = con.execute(
1505
1507
  "SELECT canonical_id, qualified_name, name FROM symbols "
1506
1508
  "WHERE canonical_id=? OR qualified_name=? OR name=? "
@@ -1515,8 +1517,27 @@ def trace_path(
1515
1517
  (clean_to, clean_to, clean_to, clean_to, clean_to),
1516
1518
  ).fetchone()
1517
1519
 
1518
- if not from_row or not to_row:
1519
- missing = clean_from if not from_row else clean_to
1520
+ # Fallback: check if clean_from or clean_to is a virtual node in graph_edges (e.g. ReplicatedStorage.Remotes.Inventory or persistence.roblox.datastore.PlayerData)
1521
+ from_virtual: str | None = None
1522
+ if not from_row:
1523
+ ge_from = con.execute(
1524
+ "SELECT source, target FROM graph_edges WHERE source=? OR target=? LIMIT 1",
1525
+ (clean_from, clean_from),
1526
+ ).fetchone()
1527
+ if ge_from:
1528
+ from_virtual = clean_from
1529
+
1530
+ to_virtual: str | None = None
1531
+ if not to_row:
1532
+ ge_to = con.execute(
1533
+ "SELECT source, target FROM graph_edges WHERE source=? OR target=? LIMIT 1",
1534
+ (clean_to, clean_to),
1535
+ ).fetchone()
1536
+ if ge_to:
1537
+ to_virtual = clean_to
1538
+
1539
+ if (not from_row and not from_virtual) or (not to_row and not to_virtual):
1540
+ missing = clean_from if (not from_row and not from_virtual) else clean_to
1520
1541
  return {
1521
1542
  "status": "not_found",
1522
1543
  "error": {
@@ -1533,11 +1554,24 @@ def trace_path(
1533
1554
  "path": [],
1534
1555
  }
1535
1556
 
1536
- start_canon = from_row["canonical_id"]
1537
- target_canon = to_row["canonical_id"]
1538
- target_names = {to_row["canonical_id"], to_row["qualified_name"]}
1539
- if "." not in clean_to:
1557
+ start_canon = from_row["canonical_id"] if from_row else str(from_virtual)
1558
+ target_canon = to_row["canonical_id"] if to_row else str(to_virtual)
1559
+ target_names = (
1560
+ {to_row["canonical_id"], to_row["qualified_name"]}
1561
+ if to_row
1562
+ else {target_canon}
1563
+ )
1564
+ if to_row and "." not in clean_to:
1540
1565
  target_names.add(to_row["name"])
1566
+ # Also include child methods of target_canon if target_canon is a class/service table
1567
+ if to_row:
1568
+ child_rows = con.execute(
1569
+ "SELECT canonical_id, qualified_name FROM symbols WHERE parent_symbol_id=?",
1570
+ (target_canon,),
1571
+ ).fetchall()
1572
+ for cr in child_rows:
1573
+ target_names.add(str(cr["canonical_id"]))
1574
+ target_names.add(str(cr["qualified_name"]))
1541
1575
 
1542
1576
  if start_canon == target_canon:
1543
1577
  return {
@@ -1558,40 +1592,57 @@ def trace_path(
1558
1592
  if len(path) >= bounded_depth:
1559
1593
  continue
1560
1594
 
1595
+ # Expand curr_sym to include its direct child methods when curr_sym is a class/table
1596
+ curr_candidates = [curr_sym]
1597
+ child_syms = con.execute(
1598
+ "SELECT canonical_id FROM symbols WHERE parent_symbol_id=?",
1599
+ (curr_sym,),
1600
+ ).fetchall()
1601
+ for cs in child_syms:
1602
+ curr_candidates.append(str(cs["canonical_id"]))
1603
+ ph_curr = ",".join("?" for _ in curr_candidates)
1604
+
1561
1605
  # 1. Query outgoing edges from graph_edges
1562
1606
  graph_rows = con.execute(
1563
- "SELECT target, relationship, confidence, evidence_class, file, start_line, end_line "
1607
+ "SELECT source, target, relationship, confidence, evidence_class, file, start_line, end_line "
1564
1608
  "FROM graph_edges "
1565
- "WHERE source=? AND relationship IN ("
1609
+ f"WHERE source IN ({ph_curr}) AND relationship IN ("
1566
1610
  "'CALLS', 'DEFINES', 'CONTAINS', 'IMPORTS', 'HANDLED_BY', 'ROUTES_TO', "
1567
- "'PROVIDES', 'INJECTS', 'RESOLVES_DEPENDENCY', 'DISPATCHES_TO', 'REGISTERS'"
1611
+ "'PROVIDES', 'INJECTS', 'RESOLVES_DEPENDENCY', 'DISPATCHES_TO', 'REGISTERS', "
1612
+ "'PRODUCES_TO_QUEUE', 'CONSUMES_FROM_QUEUE', 'STATIC_QUEUE_LINKED', 'OTEL_DISTRIBUTED_VERIFIED', "
1613
+ "'DISPATCHES_TASK', 'TRIGGERS_SIGNAL', 'HANDLES_SIGNAL', 'RENDERS_TEMPLATE', "
1614
+ "'DISPATCHES_FORWARD', 'TOOL_HANDLER', 'PIPELINE_STEP', "
1615
+ "'REQUIRES_MODULE', 'CLIENT_DISPATCHES_REMOTE', 'SERVER_HANDLES_REMOTE', "
1616
+ "'GETS_SERVICE', 'PROVIDES_SERVICE', 'READS_PERSISTENCE', 'WRITES_PERSISTENCE', 'CONFIGURES_PERSISTENCE'"
1568
1617
  ") AND confidence IN ('HIGH', 'MEDIUM') "
1569
1618
  "ORDER BY relationship ASC, target ASC",
1570
- (curr_sym,),
1619
+ curr_candidates,
1571
1620
  ).fetchall()
1572
1621
 
1573
1622
  # 2. Query outgoing verified CALLS from references
1574
1623
  ref_rows = con.execute(
1575
- "SELECT target_symbol_id, relationship, confidence, path, start_line, end_line, evidence "
1624
+ "SELECT source_symbol_id, target_symbol_id, relationship, confidence, path, start_line, end_line, evidence "
1576
1625
  "FROM 'references' "
1577
- "WHERE source_symbol_id=? AND relationship='CALLS' AND confidence IN ('HIGH', 'MEDIUM') AND target_symbol_id IS NOT NULL "
1626
+ f"WHERE source_symbol_id IN ({ph_curr}) AND relationship='CALLS' AND confidence IN ('HIGH', 'MEDIUM') AND target_symbol_id IS NOT NULL "
1578
1627
  "ORDER BY path ASC, start_line ASC",
1579
- (curr_sym,),
1628
+ curr_candidates,
1580
1629
  ).fetchall()
1581
1630
 
1582
1631
  # 3. Query outgoing resolved calls from calls table
1583
1632
  edges = con.execute(
1584
1633
  "SELECT c.callee, c.qualified_callee, c.resolved_symbol_id, c.confidence, c.source_path, c.line "
1585
1634
  "FROM calls c "
1586
- "WHERE c.source_symbol_id=? AND (c.resolved_symbol_id IS NOT NULL OR c.confidence IN ('HIGH', 'MEDIUM')) "
1635
+ f"WHERE c.source_symbol_id IN ({ph_curr}) AND (c.resolved_symbol_id IS NOT NULL OR c.confidence IN ('HIGH', 'MEDIUM')) "
1587
1636
  "ORDER BY c.source_path ASC, c.line ASC",
1588
- (curr_sym,),
1637
+ curr_candidates,
1589
1638
  ).fetchall()
1590
1639
 
1591
1640
  step_map: dict[tuple[str, str], tuple[str, str, str, str, str, int, int]] = {}
1592
1641
  for g in graph_rows:
1593
1642
  dest = str(g["target"])
1594
1643
  rel = str(g["relationship"])
1644
+ if rel == "CONTAINS" and dest in curr_candidates:
1645
+ continue
1595
1646
  conf = str(g["confidence"] or "HIGH").upper()
1596
1647
  ev_cls = str(g["evidence_class"] or "AST_VERIFIED").upper()
1597
1648
  step_map[(dest, rel)] = (dest, rel, conf, ev_cls, str(g["file"] or ""), int(g["start_line"] or 1), int(g["end_line"] or g["start_line"] or 1))
@@ -1615,17 +1666,26 @@ def trace_path(
1615
1666
 
1616
1667
  for dest_sym, rel, conf, ev_cls, s_file, s_line, e_line in all_steps:
1617
1668
  dest_row = con.execute(
1618
- "SELECT canonical_id FROM symbols "
1669
+ "SELECT canonical_id, parent_symbol_id FROM symbols "
1619
1670
  "WHERE canonical_id=? OR qualified_name=? OR name=? "
1620
1671
  "ORDER BY (canonical_id=?) DESC, (qualified_name=?) DESC LIMIT 1",
1621
1672
  (dest_sym, dest_sym, dest_sym, dest_sym, dest_sym),
1622
1673
  ).fetchone()
1623
1674
  dest_canon = dest_row["canonical_id"] if dest_row else dest_sym
1675
+ dest_parent = dest_row["parent_symbol_id"] if dest_row else None
1676
+
1677
+ matched_target = (
1678
+ dest_canon == target_canon
1679
+ or dest_sym in target_names
1680
+ or dest_canon in target_names
1681
+ or (dest_parent is not None and dest_parent == target_canon)
1682
+ )
1683
+ effective_target = target_canon if matched_target else dest_canon
1624
1684
 
1625
1685
  new_path = list(path) + [
1626
1686
  {
1627
1687
  "source": curr_sym,
1628
- "target": dest_canon,
1688
+ "target": effective_target,
1629
1689
  "relationship": rel,
1630
1690
  "confidence": conf,
1631
1691
  "evidence_class": ev_cls,
@@ -1636,12 +1696,12 @@ def trace_path(
1636
1696
  start_line=s_line,
1637
1697
  end_line=e_line,
1638
1698
  evidence_type="path_edge",
1639
- canonical_id=dest_canon,
1699
+ canonical_id=effective_target,
1640
1700
  ),
1641
1701
  }
1642
1702
  ]
1643
1703
 
1644
- if dest_canon == target_canon or dest_sym in target_names:
1704
+ if matched_target:
1645
1705
  return {
1646
1706
  "status": "ok",
1647
1707
  "from": clean_from,
@@ -1656,6 +1716,9 @@ def trace_path(
1656
1716
  if dest_canon and dest_canon not in visited:
1657
1717
  visited.add(dest_canon)
1658
1718
  queue.append((dest_canon, new_path))
1719
+ if dest_parent and dest_parent not in visited:
1720
+ visited.add(dest_parent)
1721
+ queue.append((dest_parent, new_path))
1659
1722
 
1660
1723
  return {
1661
1724
  "status": "not_found",
@@ -2010,25 +2073,130 @@ def list_routes(
2010
2073
  except sqlite3.OperationalError:
2011
2074
  rows = []
2012
2075
 
2013
- routes = [
2014
- {
2015
- "method": r["http_method"],
2016
- "path": r["route_path"],
2017
- "handler": r["handler_canonical_id"] or r["handler_name"],
2018
- "file": r["file_path"],
2019
- "start_line": r["line"],
2020
- "end_line": r["line"],
2021
- "framework": r["framework"],
2022
- "evidence": make_evidence(
2023
- file=r["file_path"],
2024
- start_line=r["line"],
2025
- end_line=r["line"],
2026
- evidence_type="route",
2027
- canonical_id=r["handler_canonical_id"],
2028
- ),
2029
- }
2030
- for r in rows
2031
- ]
2076
+ # Fetch HTMX requests and template rendering edges if available
2077
+ htmx_rows: list[sqlite3.Row] = []
2078
+ try:
2079
+ htmx_rows = con.execute(
2080
+ "SELECT target_name, file_path, line, base_expr, dict_entries_json "
2081
+ "FROM local_bindings WHERE expr_kind='HTMX_REQUEST'"
2082
+ ).fetchall()
2083
+ except sqlite3.OperationalError:
2084
+ pass
2085
+
2086
+ template_edges: dict[str, list[dict[str, Any]]] = {}
2087
+ try:
2088
+ t_rows = con.execute(
2089
+ "SELECT source, target, file, start_line FROM graph_edges WHERE relationship='RENDERS_TEMPLATE'"
2090
+ ).fetchall()
2091
+ for tr in t_rows:
2092
+ template_edges.setdefault(tr["source"], []).append(
2093
+ {
2094
+ "template": tr["target"],
2095
+ "file": tr["file"],
2096
+ "line": tr["start_line"],
2097
+ }
2098
+ )
2099
+ except sqlite3.OperationalError:
2100
+ pass
2101
+
2102
+ def _route_matches_url(route_path: str, url: str, handler_name: str = "", handler_canon: str = "") -> bool:
2103
+ if not route_path or not url:
2104
+ return False
2105
+ if handler_name and (url == handler_name or url.endswith(f".{handler_name}")):
2106
+ return True
2107
+ if handler_canon and url in handler_canon:
2108
+ return True
2109
+ rp = route_path.rstrip("/") or "/"
2110
+ u = url.rstrip("/") or "/"
2111
+ if rp == u:
2112
+ return True
2113
+ u_clean = u.split("?")[0]
2114
+ if rp == u_clean:
2115
+ return True
2116
+ import re
2117
+ pattern = re.sub(r"<[^>]+>", r"[^/]+", rp)
2118
+ pattern = re.sub(r"\{[^}]+\}", r"[^/]+", pattern)
2119
+ pattern = re.sub(r":[\w]+", r"[^/]+", pattern)
2120
+ pattern = f"^{pattern}$"
2121
+ try:
2122
+ return bool(re.match(pattern, u_clean))
2123
+ except Exception:
2124
+ return False
2125
+
2126
+ routes = []
2127
+ for r in rows:
2128
+ r_path = r["route_path"]
2129
+ r_method = (r["http_method"] or "").upper()
2130
+ h_canon = r["handler_canonical_id"] or ""
2131
+ h_name = r["handler_name"] or ""
2132
+
2133
+ # Match HTMX callers
2134
+ htmx_callers: list[dict[str, Any]] = []
2135
+ for hb in htmx_rows:
2136
+ url = hb["target_name"]
2137
+ hb_method = (hb["base_expr"] or "").upper()
2138
+ if (not hb_method or hb_method == r_method or r_method in ("ALL", "ANY")) and _route_matches_url(
2139
+ r_path, url, h_name, h_canon
2140
+ ):
2141
+ import json
2142
+ meta_dict: dict[str, Any] = {}
2143
+ try:
2144
+ raw_entries = json.loads(hb["dict_entries_json"] or "[]")
2145
+ if isinstance(raw_entries, list):
2146
+ meta_dict = dict(raw_entries)
2147
+ elif isinstance(raw_entries, dict):
2148
+ meta_dict = raw_entries
2149
+ except Exception:
2150
+ pass
2151
+ htmx_callers.append(
2152
+ {
2153
+ "url": url,
2154
+ "method": hb_method,
2155
+ "file": hb["file_path"],
2156
+ "line": hb["line"],
2157
+ "target": meta_dict.get("target"),
2158
+ "trigger": meta_dict.get("trigger"),
2159
+ "swap": meta_dict.get("swap"),
2160
+ "element": meta_dict.get("element"),
2161
+ }
2162
+ )
2163
+
2164
+ rendered = template_edges.get(h_canon, []) or template_edges.get(h_name, [])
2165
+
2166
+ lineage: dict[str, Any] = {}
2167
+ try:
2168
+ from codegraph.database.interrogation import get_route_db_lineage
2169
+
2170
+ lineage = get_route_db_lineage(
2171
+ con,
2172
+ route_or_handler=h_canon or h_name or r_path,
2173
+ method=r_method,
2174
+ )
2175
+ except Exception:
2176
+ lineage = {}
2177
+
2178
+ routes.append(
2179
+ {
2180
+ "method": r["http_method"],
2181
+ "path": r["route_path"],
2182
+ "handler": r["handler_canonical_id"] or r["handler_name"],
2183
+ "file": r["file_path"],
2184
+ "start_line": r["line"],
2185
+ "end_line": r["line"],
2186
+ "framework": r["framework"],
2187
+ "htmx_callers": htmx_callers,
2188
+ "templates_rendered": rendered,
2189
+ "db_reads": lineage.get("db_reads", []),
2190
+ "db_writes": lineage.get("db_writes", []),
2191
+ "evidence": make_evidence(
2192
+ file=r["file_path"],
2193
+ start_line=r["line"],
2194
+ end_line=r["line"],
2195
+ evidence_type="route",
2196
+ canonical_id=r["handler_canonical_id"],
2197
+ ),
2198
+ }
2199
+ )
2032
2200
 
2033
2201
  total_count = len(routes)
2034
2202
  if limit is not None:
codegraph/lsp.py ADDED
@@ -0,0 +1,318 @@
1
+ """Language Server Protocol (LSP) Bridge for CodeGraph MCP.
2
+
3
+ Exposes CodeGraph's deterministic, SQLite-backed repository graph as a
4
+ standard Language Server Protocol server (JSON-RPC 2.0 over stdio).
5
+
6
+ Capabilities:
7
+ - `textDocument/definition`: Jump to definition across Python, TypeScript, and Luau.
8
+ - `textDocument/references`: Find all verified AST callers and references across the repository.
9
+ - `textDocument/codeLens`: Display live caller counts (e.g. `▲ 4 callers`) and remote links above functions.
10
+ - `textDocument/documentSymbol`: Outline symbols in the current file.
11
+ """
12
+ from __future__ import annotations
13
+
14
+ import io
15
+ import json
16
+ import sys
17
+ import urllib.parse
18
+ from pathlib import Path
19
+ from typing import Any
20
+
21
+ from codegraph.indexing.indexer import Indexer
22
+
23
+
24
+ def uri_to_path(uri: str) -> Path:
25
+ """Convert file:// URI to Path."""
26
+ parsed = urllib.parse.urlparse(uri)
27
+ if parsed.scheme == "file":
28
+ path_str = urllib.parse.unquote(parsed.path)
29
+ # Handle Windows drive letters like /C:/path
30
+ if sys.platform == "win32" and path_str.startswith("/") and len(path_str) > 2 and path_str[2] == ":":
31
+ path_str = path_str[1:]
32
+ return Path(path_str)
33
+ return Path(uri)
34
+
35
+
36
+ def path_to_uri(path: Path | str) -> str:
37
+ """Convert Path to file:// URI."""
38
+ p = Path(path).resolve()
39
+ return p.as_uri()
40
+
41
+
42
+ class LSPServer:
43
+ """Lightweight stdio JSON-RPC Language Server Protocol handler."""
44
+
45
+ def __init__(self, repository: Path) -> None:
46
+ self.repository = repository.resolve()
47
+ self.indexer = Indexer(self.repository)
48
+
49
+ def handle_request(self, method: str, params: dict[str, Any]) -> Any:
50
+ """Dispatch JSON-RPC request to internal handler."""
51
+ if method == "initialize":
52
+ return self._handle_initialize(params)
53
+ elif method == "textDocument/definition":
54
+ return self._handle_definition(params)
55
+ elif method == "textDocument/references":
56
+ return self._handle_references(params)
57
+ elif method == "textDocument/codeLens":
58
+ return self._handle_codelens(params)
59
+ elif method == "textDocument/documentSymbol":
60
+ return self._handle_document_symbol(params)
61
+ elif method == "shutdown":
62
+ return None
63
+ return None
64
+
65
+ def _handle_initialize(self, params: dict[str, Any]) -> dict[str, Any]:
66
+ return {
67
+ "capabilities": {
68
+ "definitionProvider": True,
69
+ "referencesProvider": True,
70
+ "codeLensProvider": {
71
+ "resolveProvider": False,
72
+ },
73
+ "documentSymbolProvider": True,
74
+ },
75
+ "serverInfo": {
76
+ "name": "codegraph-lsp",
77
+ "version": "3.0.1",
78
+ },
79
+ }
80
+
81
+ def _handle_definition(self, params: dict[str, Any]) -> list[dict[str, Any]]:
82
+ text_doc = params.get("textDocument", {})
83
+ pos = params.get("position", {})
84
+ uri = text_doc.get("uri", "")
85
+ line_0 = pos.get("line", 0)
86
+ target_line = line_0 + 1
87
+
88
+ file_path = str(uri_to_path(uri).relative_to(self.repository)).replace("\\", "/")
89
+
90
+ with self.indexer.session() as con:
91
+ # Check if this line makes a call or reference
92
+ row = con.execute(
93
+ """
94
+ SELECT target, relationship FROM graph_edges
95
+ WHERE file = ? AND start_line <= ? AND end_line >= ?
96
+ LIMIT 1
97
+ """,
98
+ (file_path, target_line, target_line),
99
+ ).fetchone()
100
+
101
+ if not row:
102
+ return []
103
+
104
+ target_sym = row[0]
105
+
106
+ # Find target symbol definition
107
+ target_def = con.execute(
108
+ """
109
+ SELECT path, start_line, end_line
110
+ FROM symbols
111
+ WHERE canonical_id = ? OR name = ?
112
+ LIMIT 1
113
+ """,
114
+ (target_sym, target_sym.split(".")[-1]),
115
+ ).fetchone()
116
+
117
+ if not target_def:
118
+ return []
119
+
120
+ def_path = self.repository / target_def[0]
121
+ start_l = max(0, int(target_def[1]) - 1)
122
+ end_l = max(0, int(target_def[2]) - 1)
123
+
124
+ return [
125
+ {
126
+ "uri": path_to_uri(def_path),
127
+ "range": {
128
+ "start": {"line": start_l, "character": 0},
129
+ "end": {"line": end_l, "character": 0},
130
+ },
131
+ }
132
+ ]
133
+
134
+ def _handle_references(self, params: dict[str, Any]) -> list[dict[str, Any]]:
135
+ text_doc = params.get("textDocument", {})
136
+ pos = params.get("position", {})
137
+ uri = text_doc.get("uri", "")
138
+ line_0 = pos.get("line", 0)
139
+ target_line = line_0 + 1
140
+
141
+ file_path = str(uri_to_path(uri).relative_to(self.repository)).replace("\\", "/")
142
+
143
+ with self.indexer.session() as con:
144
+ # Find symbol defined or located at position
145
+ sym_row = con.execute(
146
+ """
147
+ SELECT canonical_id, name FROM symbols
148
+ WHERE path = ? AND start_line <= ? AND end_line >= ?
149
+ ORDER BY start_line DESC
150
+ LIMIT 1
151
+ """,
152
+ (file_path, target_line, target_line),
153
+ ).fetchone()
154
+
155
+ if not sym_row:
156
+ return []
157
+
158
+ can_id, name = sym_row[0], sym_row[1]
159
+
160
+ # Find all callers and references
161
+ edges = con.execute(
162
+ """
163
+ SELECT DISTINCT file, start_line, end_line FROM graph_edges
164
+ WHERE (target = ? OR target = ?)
165
+ AND relationship IN ('CALLS', 'INSTANTIATES', 'IMPORTS', 'REFERENCES')
166
+ ORDER BY file, start_line
167
+ LIMIT 50
168
+ """,
169
+ (can_id, name),
170
+ ).fetchall()
171
+
172
+ locations: list[dict[str, Any]] = []
173
+ for f, sl, el in edges:
174
+ loc_path = self.repository / f
175
+ locations.append(
176
+ {
177
+ "uri": path_to_uri(loc_path),
178
+ "range": {
179
+ "start": {"line": max(0, int(sl) - 1), "character": 0},
180
+ "end": {"line": max(0, int(el) - 1), "character": 0},
181
+ },
182
+ }
183
+ )
184
+ return locations
185
+
186
+ def _handle_codelens(self, params: dict[str, Any]) -> list[dict[str, Any]]:
187
+ text_doc = params.get("textDocument", {})
188
+ uri = text_doc.get("uri", "")
189
+ file_path = str(uri_to_path(uri).relative_to(self.repository)).replace("\\", "/")
190
+
191
+ with self.indexer.session() as con:
192
+ symbols = con.execute(
193
+ """
194
+ SELECT name, canonical_id, kind, start_line, end_line
195
+ FROM symbols
196
+ WHERE path = ? AND kind IN ('function', 'method', 'class')
197
+ """,
198
+ (file_path,),
199
+ ).fetchall()
200
+
201
+ lenses: list[dict[str, Any]] = []
202
+ for name, can_id, _kind, start_line, _end_line in symbols:
203
+ # Count callers
204
+ caller_count = con.execute(
205
+ """
206
+ SELECT COUNT(DISTINCT source) FROM graph_edges
207
+ WHERE (target = ? OR target = ?)
208
+ AND relationship IN ('CALLS', 'INSTANTIATES')
209
+ """,
210
+ (can_id, name),
211
+ ).fetchone()[0]
212
+
213
+ line_0 = max(0, int(start_line) - 1)
214
+ lenses.append(
215
+ {
216
+ "range": {
217
+ "start": {"line": line_0, "character": 0},
218
+ "end": {"line": line_0, "character": 0},
219
+ },
220
+ "command": {
221
+ "title": f"▲ {caller_count} callers",
222
+ "command": "codegraph.showReferences",
223
+ "arguments": [uri, {"line": line_0, "character": 0}],
224
+ },
225
+ }
226
+ )
227
+ return lenses
228
+
229
+ def _handle_document_symbol(self, params: dict[str, Any]) -> list[dict[str, Any]]:
230
+ text_doc = params.get("textDocument", {})
231
+ uri = text_doc.get("uri", "")
232
+ file_path = str(uri_to_path(uri).relative_to(self.repository)).replace("\\", "/")
233
+
234
+ with self.indexer.session() as con:
235
+ rows = con.execute(
236
+ """
237
+ SELECT name, kind, start_line, end_line
238
+ FROM symbols
239
+ WHERE path = ?
240
+ """,
241
+ (file_path,),
242
+ ).fetchall()
243
+
244
+ kind_mapping = {
245
+ "function": 12,
246
+ "method": 6,
247
+ "class": 5,
248
+ "variable": 13,
249
+ }
250
+
251
+ symbols: list[dict[str, Any]] = []
252
+ for name, kind, sl, el in rows:
253
+ start_l = max(0, int(sl) - 1)
254
+ end_l = max(0, int(el) - 1)
255
+ symbols.append(
256
+ {
257
+ "name": name,
258
+ "kind": kind_mapping.get(kind.lower(), 12),
259
+ "range": {
260
+ "start": {"line": start_l, "character": 0},
261
+ "end": {"line": end_l, "character": 0},
262
+ },
263
+ "selectionRange": {
264
+ "start": {"line": start_l, "character": 0},
265
+ "end": {"line": start_l, "character": len(name)},
266
+ },
267
+ }
268
+ )
269
+ return symbols
270
+
271
+
272
+ def run_stdio_lsp_server(repository: Path, input_stream: io.BufferedReader | None = None, output_stream: io.BufferedWriter | None = None) -> None:
273
+ """Run JSON-RPC 2.0 LSP server over stdio streams."""
274
+ server = LSPServer(repository)
275
+ r = input_stream or sys.stdin.buffer
276
+ w = output_stream or sys.stdout.buffer
277
+
278
+ while True:
279
+ # Read Content-Length header
280
+ content_length = 0
281
+ while True:
282
+ line = r.readline()
283
+ if not line:
284
+ return
285
+ line_str = line.decode("ascii", errors="replace").strip()
286
+ if not line_str:
287
+ break
288
+ if line_str.lower().startswith("content-length:"):
289
+ content_length = int(line_str.split(":", 1)[1].strip())
290
+
291
+ if content_length <= 0:
292
+ continue
293
+
294
+ body_bytes = r.read(content_length)
295
+ if not body_bytes:
296
+ break
297
+
298
+ try:
299
+ req = json.loads(body_bytes.decode("utf-8"))
300
+ except Exception:
301
+ continue
302
+
303
+ req_id = req.get("id")
304
+ method = req.get("method", "")
305
+ params = req.get("params", {})
306
+
307
+ result = server.handle_request(method, params)
308
+
309
+ if req_id is not None:
310
+ resp = {
311
+ "jsonrpc": "2.0",
312
+ "id": req_id,
313
+ "result": result,
314
+ }
315
+ resp_bytes = json.dumps(resp).encode("utf-8")
316
+ header = f"Content-Length: {len(resp_bytes)}\r\n\r\n".encode("ascii")
317
+ w.write(header + resp_bytes)
318
+ w.flush()
@@ -0,0 +1,16 @@
1
+ """Language Server Protocol (LSP) Bridge for CodeGraph."""
2
+ from __future__ import annotations
3
+
4
+ from .bridge import collect_lsp_diagnostics, detect_lsp_servers
5
+ from .client import LSPClient
6
+ from .models import DiagnosticSeverity, LSPBridgeReport, LSPDiagnostic, LSPLocation
7
+
8
+ __all__ = [
9
+ "DiagnosticSeverity",
10
+ "LSPBridgeReport",
11
+ "LSPClient",
12
+ "LSPDiagnostic",
13
+ "LSPLocation",
14
+ "collect_lsp_diagnostics",
15
+ "detect_lsp_servers",
16
+ ]