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.
- codegraph/__init__.py +1 -1
- codegraph/__main__.py +7 -0
- codegraph/agent_brain.py +253 -2
- codegraph/agent_capabilities.py +1348 -67
- codegraph/agent_rules.py +2 -1
- codegraph/api_drift.py +455 -0
- codegraph/architecture_rules.py +123 -0
- codegraph/async_queue/__init__.py +50 -0
- codegraph/async_queue/detectors/__init__.py +48 -0
- codegraph/async_queue/detectors/celery.py +160 -0
- codegraph/async_queue/detectors/kafka.py +168 -0
- codegraph/async_queue/detectors/rabbitmq.py +116 -0
- codegraph/async_queue/detectors/redis.py +130 -0
- codegraph/async_queue/detectors/sqs.py +135 -0
- codegraph/async_queue/models.py +161 -0
- codegraph/async_queue/otel_models.py +74 -0
- codegraph/async_queue/otel_parser.py +337 -0
- codegraph/async_queue/reconciler.py +427 -0
- codegraph/async_queue/scanner.py +291 -0
- codegraph/async_queue/traversal.py +447 -0
- codegraph/change_impact.py +320 -110
- codegraph/changelog.py +170 -0
- codegraph/cli.py +1605 -62
- codegraph/cli_output.py +17 -2
- codegraph/context.py +135 -24
- codegraph/context_freshness.py +2 -0
- codegraph/database/__init__.py +14 -0
- codegraph/database/extractor.py +712 -17
- codegraph/database/interrogation.py +388 -2
- codegraph/database/schema_drift.py +862 -0
- codegraph/dataflow_propagation.py +118 -0
- codegraph/dead_code.py +387 -0
- codegraph/epistemic.py +15 -0
- codegraph/evidence_contract.py +34 -3
- codegraph/federation.py +245 -0
- codegraph/frameworks.py +234 -12
- codegraph/frameworks_nestjs.py +502 -0
- codegraph/frameworks_nextjs.py +337 -0
- codegraph/frameworks_react.py +457 -0
- codegraph/freshness.py +2 -0
- codegraph/frontend_html.py +109 -0
- codegraph/git.py +2 -0
- codegraph/git_state.py +2 -0
- codegraph/graph/traversal.py +150 -7
- codegraph/graph_csr.py +218 -0
- codegraph/incremental.py +150 -0
- codegraph/indexing/__init__.py +7 -1
- codegraph/indexing/classifier.py +5 -0
- codegraph/indexing/indexer.py +292 -38
- codegraph/indexing/languages/__init__.py +1 -0
- codegraph/indexing/languages/cpp.py +457 -0
- codegraph/indexing/languages/go.py +459 -0
- codegraph/indexing/languages/jvm.py +583 -0
- codegraph/indexing/languages/rust.py +507 -0
- codegraph/indexing/models.py +15 -1
- codegraph/indexing/parser.py +385 -9
- codegraph/indexing/pool.py +136 -0
- codegraph/indexing/protocols.py +220 -0
- codegraph/indexing/scanner.py +23 -0
- codegraph/indexing/templates.py +418 -0
- codegraph/interrogation.py +207 -39
- codegraph/lsp.py +318 -0
- codegraph/lsp_bridge/__init__.py +16 -0
- codegraph/lsp_bridge/bridge.py +102 -0
- codegraph/lsp_bridge/client.py +207 -0
- codegraph/lsp_bridge/models.py +71 -0
- codegraph/mcp/server.py +502 -29
- codegraph/memory/trajectory.py +282 -0
- codegraph/models.py +7 -0
- codegraph/monorepo.py +3 -0
- codegraph/multi_agent/__init__.py +13 -0
- codegraph/multi_agent/coordinator.py +229 -0
- codegraph/multi_agent/models.py +76 -0
- codegraph/optimizer.py +42 -5
- codegraph/polymorphism.py +95 -0
- codegraph/pr_review.py +296 -0
- codegraph/process_lifecycle.py +50 -12
- codegraph/reachability.py +198 -0
- codegraph/reachability_index.py +74 -0
- codegraph/refactor/__init__.py +48 -0
- codegraph/refactor/models.py +114 -0
- codegraph/refactor/patcher.py +348 -0
- codegraph/refactor/renamer.py +844 -0
- codegraph/refactor/transactions.py +414 -0
- codegraph/repair.py +233 -0
- codegraph/resolver.py +1357 -161
- codegraph/retrieval_policy.py +22 -1
- codegraph/roblox.py +2174 -0
- codegraph/route_composer.py +7 -0
- codegraph/route_topology.py +172 -0
- codegraph/runtime/__init__.py +25 -0
- codegraph/runtime/collector.py +155 -0
- codegraph/runtime/db_watcher.py +191 -0
- codegraph/runtime/ingestor.py +30 -10
- codegraph/runtime/otel_receiver.py +208 -0
- codegraph/runtime/port_inspector.py +304 -0
- codegraph/runtime/runner.py +155 -6
- codegraph/runtime/trace_linker.py +175 -0
- codegraph/sandbox.py +215 -0
- codegraph/search/hybrid.py +26 -0
- codegraph/security/supply_chain.py +188 -0
- codegraph/security/taint.py +421 -0
- codegraph/semantic_decorators.py +34 -3
- codegraph/structural_diff.py +2 -0
- codegraph/styles.py +144 -0
- codegraph/symbol_history.py +2 -0
- codegraph/test_gen.py +437 -0
- codegraph/tsconfig.py +236 -0
- codegraph/ui/__init__.py +6 -0
- codegraph/ui/server.py +876 -0
- codegraph/watcher.py +323 -0
- {codegraph_engine-2.3.0.dist-info → codegraph_engine-3.0.1.dist-info}/METADATA +365 -67
- codegraph_engine-3.0.1.dist-info/RECORD +164 -0
- {codegraph_engine-2.3.0.dist-info → codegraph_engine-3.0.1.dist-info}/WHEEL +1 -1
- codegraph_engine-2.3.0.dist-info/RECORD +0 -94
- {codegraph_engine-2.3.0.dist-info → codegraph_engine-3.0.1.dist-info}/entry_points.txt +0 -0
- {codegraph_engine-2.3.0.dist-info → codegraph_engine-3.0.1.dist-info}/licenses/LICENSE +0 -0
- {codegraph_engine-2.3.0.dist-info → codegraph_engine-3.0.1.dist-info}/top_level.txt +0 -0
codegraph/interrogation.py
CHANGED
|
@@ -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
|
|
1519
|
-
|
|
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 =
|
|
1539
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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":
|
|
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=
|
|
1699
|
+
canonical_id=effective_target,
|
|
1640
1700
|
),
|
|
1641
1701
|
}
|
|
1642
1702
|
]
|
|
1643
1703
|
|
|
1644
|
-
if
|
|
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
|
-
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
"
|
|
2018
|
-
"
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
|
|
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
|
+
]
|