rho-graph 0.1.0
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.
- package/README.md +277 -0
- package/bin/rho-graph.js +2 -0
- package/dist/cli/commands/index-cmd.d.ts +2 -0
- package/dist/cli/commands/index-cmd.js +45 -0
- package/dist/cli/commands/index-cmd.js.map +1 -0
- package/dist/cli/commands/init.d.ts +3 -0
- package/dist/cli/commands/init.js +55 -0
- package/dist/cli/commands/init.js.map +1 -0
- package/dist/cli/commands/install-hook.d.ts +3 -0
- package/dist/cli/commands/install-hook.js +37 -0
- package/dist/cli/commands/install-hook.js.map +1 -0
- package/dist/cli/commands/install-mcp.d.ts +3 -0
- package/dist/cli/commands/install-mcp.js +32 -0
- package/dist/cli/commands/install-mcp.js.map +1 -0
- package/dist/cli/commands/query.d.ts +2 -0
- package/dist/cli/commands/query.js +92 -0
- package/dist/cli/commands/query.js.map +1 -0
- package/dist/cli/commands/setup.d.ts +2 -0
- package/dist/cli/commands/setup.js +15 -0
- package/dist/cli/commands/setup.js.map +1 -0
- package/dist/cli/commands/status.d.ts +2 -0
- package/dist/cli/commands/status.js +40 -0
- package/dist/cli/commands/status.js.map +1 -0
- package/dist/cli/commands/visualize.d.ts +2 -0
- package/dist/cli/commands/visualize.js +45 -0
- package/dist/cli/commands/visualize.js.map +1 -0
- package/dist/cli/index.d.ts +1 -0
- package/dist/cli/index.js +32 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/config.d.ts +22 -0
- package/dist/config.js +95 -0
- package/dist/config.js.map +1 -0
- package/dist/db/connection.d.ts +13 -0
- package/dist/db/connection.js +25 -0
- package/dist/db/connection.js.map +1 -0
- package/dist/db/queries.d.ts +106 -0
- package/dist/db/queries.js +247 -0
- package/dist/db/queries.js.map +1 -0
- package/dist/db/schema.d.ts +2 -0
- package/dist/db/schema.js +22 -0
- package/dist/db/schema.js.map +1 -0
- package/dist/docker/neo4j.d.ts +5 -0
- package/dist/docker/neo4j.js +85 -0
- package/dist/docker/neo4j.js.map +1 -0
- package/dist/indexer/batch-writer.d.ts +35 -0
- package/dist/indexer/batch-writer.js +202 -0
- package/dist/indexer/batch-writer.js.map +1 -0
- package/dist/indexer/extractor.d.ts +35 -0
- package/dist/indexer/extractor.js +141 -0
- package/dist/indexer/extractor.js.map +1 -0
- package/dist/indexer/graph-writer.d.ts +12 -0
- package/dist/indexer/graph-writer.js +75 -0
- package/dist/indexer/graph-writer.js.map +1 -0
- package/dist/indexer/import-resolver.d.ts +8 -0
- package/dist/indexer/import-resolver.js +80 -0
- package/dist/indexer/import-resolver.js.map +1 -0
- package/dist/indexer/index.d.ts +21 -0
- package/dist/indexer/index.js +262 -0
- package/dist/indexer/index.js.map +1 -0
- package/dist/indexer/language-map.json +101 -0
- package/dist/indexer/parallel-pipeline.d.ts +41 -0
- package/dist/indexer/parallel-pipeline.js +82 -0
- package/dist/indexer/parallel-pipeline.js.map +1 -0
- package/dist/indexer/parser.d.ts +9 -0
- package/dist/indexer/parser.js +85 -0
- package/dist/indexer/parser.js.map +1 -0
- package/dist/indexer/staleness.d.ts +12 -0
- package/dist/indexer/staleness.js +60 -0
- package/dist/indexer/staleness.js.map +1 -0
- package/dist/mcp/index.d.ts +1 -0
- package/dist/mcp/index.js +38 -0
- package/dist/mcp/index.js.map +1 -0
- package/dist/mcp/staleness-check.d.ts +7 -0
- package/dist/mcp/staleness-check.js +31 -0
- package/dist/mcp/staleness-check.js.map +1 -0
- package/dist/mcp/tools/get-callees.d.ts +3 -0
- package/dist/mcp/tools/get-callees.js +34 -0
- package/dist/mcp/tools/get-callees.js.map +1 -0
- package/dist/mcp/tools/get-callers.d.ts +3 -0
- package/dist/mcp/tools/get-callers.js +34 -0
- package/dist/mcp/tools/get-callers.js.map +1 -0
- package/dist/mcp/tools/get-class.d.ts +3 -0
- package/dist/mcp/tools/get-class.js +42 -0
- package/dist/mcp/tools/get-class.js.map +1 -0
- package/dist/mcp/tools/get-dependencies.d.ts +3 -0
- package/dist/mcp/tools/get-dependencies.js +26 -0
- package/dist/mcp/tools/get-dependencies.js.map +1 -0
- package/dist/mcp/tools/get-dependents.d.ts +3 -0
- package/dist/mcp/tools/get-dependents.js +26 -0
- package/dist/mcp/tools/get-dependents.js.map +1 -0
- package/dist/mcp/tools/get-file-structure.d.ts +3 -0
- package/dist/mcp/tools/get-file-structure.js +33 -0
- package/dist/mcp/tools/get-file-structure.js.map +1 -0
- package/dist/mcp/tools/get-function.d.ts +3 -0
- package/dist/mcp/tools/get-function.js +34 -0
- package/dist/mcp/tools/get-function.js.map +1 -0
- package/dist/mcp/tools/get-repo-structure.d.ts +3 -0
- package/dist/mcp/tools/get-repo-structure.js +39 -0
- package/dist/mcp/tools/get-repo-structure.js.map +1 -0
- package/dist/mcp/tools/reindex.d.ts +4 -0
- package/dist/mcp/tools/reindex.js +27 -0
- package/dist/mcp/tools/reindex.js.map +1 -0
- package/dist/mcp/tools/search-code.d.ts +3 -0
- package/dist/mcp/tools/search-code.js +43 -0
- package/dist/mcp/tools/search-code.js.map +1 -0
- package/dist/visualize/public/graph.js +445 -0
- package/dist/visualize/public/index.html +88 -0
- package/dist/visualize/queries.d.ts +14 -0
- package/dist/visualize/queries.js +84 -0
- package/dist/visualize/queries.js.map +1 -0
- package/dist/visualize/server.d.ts +19 -0
- package/dist/visualize/server.js +293 -0
- package/dist/visualize/server.js.map +1 -0
- package/docker-compose.yml +16 -0
- package/package.json +69 -0
- package/src/indexer/language-map.json +128 -0
- package/src/visualize/public/graph.js +445 -0
- package/src/visualize/public/index.html +88 -0
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { indexRepository } from "../../indexer/index.js";
|
|
3
|
+
export function registerReindex(server, db, config, repoPath) {
|
|
4
|
+
server.tool("reindex", "Trigger re-indexing of the repository or a specific path. Use this when you know files have changed and the graph may be stale.", {
|
|
5
|
+
path: z.string().optional().describe("Specific path to reindex (defaults to changed files only)"),
|
|
6
|
+
}, async ({ path }) => {
|
|
7
|
+
const result = await indexRepository(db, repoPath, config.index, {
|
|
8
|
+
changedOnly: !path,
|
|
9
|
+
specificPath: path,
|
|
10
|
+
});
|
|
11
|
+
return {
|
|
12
|
+
content: [
|
|
13
|
+
{
|
|
14
|
+
type: "text",
|
|
15
|
+
text: JSON.stringify({
|
|
16
|
+
filesIndexed: result.filesIndexed,
|
|
17
|
+
functionsFound: result.functionsFound,
|
|
18
|
+
classesFound: result.classesFound,
|
|
19
|
+
orphansRemoved: result.orphansRemoved,
|
|
20
|
+
errors: result.errors.length,
|
|
21
|
+
}, null, 2),
|
|
22
|
+
},
|
|
23
|
+
],
|
|
24
|
+
};
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
//# sourceMappingURL=reindex.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"reindex.js","sourceRoot":"","sources":["../../../src/mcp/tools/reindex.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAGxB,OAAO,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AAEzD,MAAM,UAAU,eAAe,CAC7B,MAAiB,EACjB,EAAgB,EAChB,MAAc,EACd,QAAgB;IAEhB,MAAM,CAAC,IAAI,CACT,SAAS,EACT,iIAAiI,EACjI;QACE,IAAI,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,2DAA2D,CAAC;KAClG,EACD,KAAK,EAAE,EAAE,IAAI,EAAE,EAAE,EAAE;QACjB,MAAM,MAAM,GAAG,MAAM,eAAe,CAAC,EAAE,EAAE,QAAQ,EAAE,MAAM,CAAC,KAAK,EAAE;YAC/D,WAAW,EAAE,CAAC,IAAI;YAClB,YAAY,EAAE,IAAI;SACnB,CAAC,CAAC;QACH,OAAO;YACL,OAAO,EAAE;gBACP;oBACE,IAAI,EAAE,MAAM;oBACZ,IAAI,EAAE,IAAI,CAAC,SAAS,CAClB;wBACE,YAAY,EAAE,MAAM,CAAC,YAAY;wBACjC,cAAc,EAAE,MAAM,CAAC,cAAc;wBACrC,YAAY,EAAE,MAAM,CAAC,YAAY;wBACjC,cAAc,EAAE,MAAM,CAAC,cAAc;wBACrC,MAAM,EAAE,MAAM,CAAC,MAAM,CAAC,MAAM;qBAC7B,EACD,IAAI,EACJ,CAAC,CACF;iBACF;aACF;SACF,CAAC;IACJ,CAAC,CACF,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
export function registerSearchCode(server, db) {
|
|
3
|
+
server.tool("search_code", "Search for functions and classes by name, signature, or code content. Use this tool INSTEAD OF reading files directly when looking for code.", {
|
|
4
|
+
query: z.string().describe("Search query — function name, keyword, or code fragment"),
|
|
5
|
+
language: z.string().optional().describe("Filter by language (e.g. typescript, python)"),
|
|
6
|
+
repo: z.string().optional().describe("Filter by repository path"),
|
|
7
|
+
limit: z.number().optional().default(20).describe("Max results to return"),
|
|
8
|
+
}, async ({ query, language, repo, limit }) => {
|
|
9
|
+
const session = db.session();
|
|
10
|
+
try {
|
|
11
|
+
let cypher = `
|
|
12
|
+
CALL db.index.fulltext.queryNodes("code_search", $query)
|
|
13
|
+
YIELD node, score
|
|
14
|
+
WHERE (node:Function OR node:Class)
|
|
15
|
+
`;
|
|
16
|
+
const params = { query, limit: limit ?? 20 };
|
|
17
|
+
if (language) {
|
|
18
|
+
cypher += ` AND node.filePath ENDS WITH $languageExt`;
|
|
19
|
+
params.languageExt = `.${language}`;
|
|
20
|
+
}
|
|
21
|
+
if (repo) {
|
|
22
|
+
cypher += ` AND node.filePath STARTS WITH $repo`;
|
|
23
|
+
params.repo = repo;
|
|
24
|
+
}
|
|
25
|
+
cypher += `
|
|
26
|
+
RETURN node.name AS name, node.filePath AS filePath,
|
|
27
|
+
node.signature AS signature, node.snippet AS snippet,
|
|
28
|
+
node.startLine AS startLine, node.endLine AS endLine,
|
|
29
|
+
labels(node)[0] AS type, score
|
|
30
|
+
ORDER BY score DESC LIMIT $limit
|
|
31
|
+
`;
|
|
32
|
+
const result = await session.run(cypher, params);
|
|
33
|
+
const records = result.records.map((r) => r.toObject());
|
|
34
|
+
return {
|
|
35
|
+
content: [{ type: "text", text: JSON.stringify(records, null, 2) }],
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
finally {
|
|
39
|
+
await session.close();
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
//# sourceMappingURL=search-code.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"search-code.js","sourceRoot":"","sources":["../../../src/mcp/tools/search-code.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAGxB,MAAM,UAAU,kBAAkB,CAAC,MAAiB,EAAE,EAAgB;IACpE,MAAM,CAAC,IAAI,CACT,aAAa,EACb,8IAA8I,EAC9I;QACE,KAAK,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,CAAC,yDAAyD,CAAC;QACrF,QAAQ,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,8CAA8C,CAAC;QACxF,IAAI,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,2BAA2B,CAAC;QACjE,KAAK,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC,QAAQ,CAAC,uBAAuB,CAAC;KAC3E,EACD,KAAK,EAAE,EAAE,KAAK,EAAE,QAAQ,EAAE,IAAI,EAAE,KAAK,EAAE,EAAE,EAAE;QACzC,MAAM,OAAO,GAAG,EAAE,CAAC,OAAO,EAAE,CAAC;QAC7B,IAAI,CAAC;YACH,IAAI,MAAM,GAAG;;;;SAIZ,CAAC;YACF,MAAM,MAAM,GAA4B,EAAE,KAAK,EAAE,KAAK,EAAE,KAAK,IAAI,EAAE,EAAE,CAAC;YACtE,IAAI,QAAQ,EAAE,CAAC;gBACb,MAAM,IAAI,2CAA2C,CAAC;gBACtD,MAAM,CAAC,WAAW,GAAG,IAAI,QAAQ,EAAE,CAAC;YACtC,CAAC;YACD,IAAI,IAAI,EAAE,CAAC;gBACT,MAAM,IAAI,sCAAsC,CAAC;gBACjD,MAAM,CAAC,IAAI,GAAG,IAAI,CAAC;YACrB,CAAC;YACD,MAAM,IAAI;;;;;;SAMT,CAAC;YACF,MAAM,MAAM,GAAG,MAAM,OAAO,CAAC,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;YACjD,MAAM,OAAO,GAAG,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,QAAQ,EAAE,CAAC,CAAC;YACxD,OAAO;gBACL,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,OAAO,EAAE,IAAI,EAAE,CAAC,CAAC,EAAE,CAAC;aACpE,CAAC;QACJ,CAAC;gBAAS,CAAC;YACT,MAAM,OAAO,CAAC,KAAK,EAAE,CAAC;QACxB,CAAC;IACH,CAAC,CACF,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,445 @@
|
|
|
1
|
+
// src/visualize/public/graph.js
|
|
2
|
+
//
|
|
3
|
+
// Sections:
|
|
4
|
+
// STATE — viewState + loadedSet
|
|
5
|
+
// API — fetch wrappers
|
|
6
|
+
// MERGE — server response → loadedSet
|
|
7
|
+
// RENDER — vis.Network init + applyViewFilters
|
|
8
|
+
// EVENTS — DOM/network event handlers
|
|
9
|
+
// BOOT — entry point
|
|
10
|
+
|
|
11
|
+
// === STATE ===
|
|
12
|
+
const GROUP_COLORS = {
|
|
13
|
+
Repository: { background: "#1f6feb", border: "#388bfd" },
|
|
14
|
+
File: { background: "#1a7f37", border: "#2ea043" },
|
|
15
|
+
Class: { background: "#8957e5", border: "#a371f7" },
|
|
16
|
+
Function: { background: "#da3633", border: "#f85149" },
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const EDGE_STYLES = {
|
|
20
|
+
CONTAINS_FILE: { color: "#1f6feb", width: 1 },
|
|
21
|
+
CONTAINS: { color: "#1a7f37", width: 1 },
|
|
22
|
+
HAS_METHOD: { color: "#8957e5", width: 1 },
|
|
23
|
+
IMPORTS: { color: "#d29922", width: 1.5 },
|
|
24
|
+
CALLS: { color: "#da3633", width: 1 },
|
|
25
|
+
IMPORTS_SYMBOL: { color: "#6e7681", width: 1, dashes: true },
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const viewState = {
|
|
29
|
+
search: "",
|
|
30
|
+
visibleNodeTypes: new Set(["Repository", "File", "Class", "Function"]),
|
|
31
|
+
visibleEdgeTypes: new Set([
|
|
32
|
+
"CONTAINS_FILE",
|
|
33
|
+
"CONTAINS",
|
|
34
|
+
"HAS_METHOD",
|
|
35
|
+
"IMPORTS",
|
|
36
|
+
"CALLS",
|
|
37
|
+
]),
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const loadedSet = {
|
|
41
|
+
nodes: new vis.DataSet(),
|
|
42
|
+
edges: new vis.DataSet(),
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
let network = null;
|
|
46
|
+
let edgeIdCounter = 0;
|
|
47
|
+
const pendingExpands = new Set();
|
|
48
|
+
|
|
49
|
+
// === API ===
|
|
50
|
+
function parseUrlFilters() {
|
|
51
|
+
const p = new URLSearchParams(window.location.search);
|
|
52
|
+
const out = {};
|
|
53
|
+
for (const k of ["repo", "file", "function"]) {
|
|
54
|
+
const v = p.get(k);
|
|
55
|
+
if (v) out[k] = v;
|
|
56
|
+
}
|
|
57
|
+
return out;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async function fetchGraph(filters) {
|
|
61
|
+
const qs = new URLSearchParams(filters).toString();
|
|
62
|
+
const url = qs ? `/api/graph?${qs}` : "/api/graph";
|
|
63
|
+
const resp = await fetch(url);
|
|
64
|
+
if (!resp.ok) {
|
|
65
|
+
let detail = "";
|
|
66
|
+
try { detail = (await resp.json()).error ?? ""; } catch {}
|
|
67
|
+
throw new Error(`fetchGraph failed: ${detail || resp.statusText}`);
|
|
68
|
+
}
|
|
69
|
+
return resp.json();
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async function fetchSearch(q) {
|
|
73
|
+
const resp = await fetch(`/api/search?q=${encodeURIComponent(q)}`);
|
|
74
|
+
if (!resp.ok) throw new Error("search failed");
|
|
75
|
+
return resp.json();
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async function fetchExpand(type, params) {
|
|
79
|
+
const qs = new URLSearchParams({ type, ...params }).toString();
|
|
80
|
+
const resp = await fetch(`/api/expand?${qs}`);
|
|
81
|
+
if (!resp.ok) {
|
|
82
|
+
let detail = "";
|
|
83
|
+
try { detail = (await resp.json()).error ?? ""; } catch {}
|
|
84
|
+
throw new Error(`expand failed: ${detail || resp.statusText}`);
|
|
85
|
+
}
|
|
86
|
+
return resp.json();
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// === MERGE ===
|
|
90
|
+
function mergeIntoLoaded({ nodes, edges }, expandedFromId) {
|
|
91
|
+
for (const n of nodes) {
|
|
92
|
+
const existing = loadedSet.nodes.get(n.id);
|
|
93
|
+
if (!existing) {
|
|
94
|
+
loadedSet.nodes.add({
|
|
95
|
+
id: n.id,
|
|
96
|
+
label: n.label,
|
|
97
|
+
color: GROUP_COLORS[n.group] ?? { background: "#6e7681", border: "#8b949e" },
|
|
98
|
+
title: n.group,
|
|
99
|
+
font: { color: "#c9d1d9" },
|
|
100
|
+
_properties: n.properties,
|
|
101
|
+
_group: n.group,
|
|
102
|
+
_expanded: false,
|
|
103
|
+
_expandedBy: expandedFromId ? new Set([expandedFromId]) : new Set(),
|
|
104
|
+
});
|
|
105
|
+
} else if (expandedFromId) {
|
|
106
|
+
// Mark this node as also reachable from expandedFromId
|
|
107
|
+
existing._expandedBy.add(expandedFromId);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
// Build a fingerprint set of existing edges to avoid duplicates
|
|
111
|
+
const existingEdgeKeys = new Set();
|
|
112
|
+
loadedSet.edges.forEach((e) => existingEdgeKeys.add(`${e.from}|${e.to}|${e._type}`));
|
|
113
|
+
|
|
114
|
+
for (const e of edges) {
|
|
115
|
+
const key = `${e.from}|${e.to}|${e.label}`;
|
|
116
|
+
if (existingEdgeKeys.has(key)) continue;
|
|
117
|
+
existingEdgeKeys.add(key);
|
|
118
|
+
const style = EDGE_STYLES[e.label] ?? { color: "#30363d", width: 1 };
|
|
119
|
+
loadedSet.edges.add({
|
|
120
|
+
id: edgeIdCounter++,
|
|
121
|
+
from: e.from,
|
|
122
|
+
to: e.to,
|
|
123
|
+
label: e.label,
|
|
124
|
+
arrows: "to",
|
|
125
|
+
color: { color: style.color, highlight: "#58a6ff" },
|
|
126
|
+
width: style.width,
|
|
127
|
+
dashes: style.dashes ?? false,
|
|
128
|
+
font: { color: "#8b949e", size: 10 },
|
|
129
|
+
_type: e.label,
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// === RENDER ===
|
|
135
|
+
function initNetwork() {
|
|
136
|
+
const container = document.getElementById("graph");
|
|
137
|
+
network = new vis.Network(
|
|
138
|
+
container,
|
|
139
|
+
{ nodes: loadedSet.nodes, edges: loadedSet.edges },
|
|
140
|
+
{
|
|
141
|
+
physics: {
|
|
142
|
+
solver: "forceAtlas2Based",
|
|
143
|
+
forceAtlas2Based: {
|
|
144
|
+
gravitationalConstant: -50,
|
|
145
|
+
centralGravity: 0.01,
|
|
146
|
+
springLength: 150,
|
|
147
|
+
springConstant: 0.08,
|
|
148
|
+
damping: 0.4,
|
|
149
|
+
avoidOverlap: 0.5,
|
|
150
|
+
},
|
|
151
|
+
stabilization: { iterations: 250, updateInterval: 25 },
|
|
152
|
+
minVelocity: 0.5,
|
|
153
|
+
},
|
|
154
|
+
edges: {
|
|
155
|
+
smooth: { type: "continuous", roundness: 0.2 },
|
|
156
|
+
font: { size: 0, strokeWidth: 0 },
|
|
157
|
+
arrows: { to: { scaleFactor: 0.6 } },
|
|
158
|
+
},
|
|
159
|
+
interaction: { hover: true, hoverConnectedEdges: true },
|
|
160
|
+
}
|
|
161
|
+
);
|
|
162
|
+
|
|
163
|
+
network.on("click", onNodeClick);
|
|
164
|
+
network.on("doubleClick", onNodeDoubleClick);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function applyViewFilters() {
|
|
168
|
+
const nodeUpdates = [];
|
|
169
|
+
const edgeUpdates = [];
|
|
170
|
+
const q = viewState.search.trim().toLowerCase();
|
|
171
|
+
|
|
172
|
+
loadedSet.nodes.forEach((n) => {
|
|
173
|
+
const typeHidden = !viewState.visibleNodeTypes.has(n._group);
|
|
174
|
+
const matches = !q || (n.label && n.label.toLowerCase().includes(q));
|
|
175
|
+
const hidden = typeHidden;
|
|
176
|
+
const borderWidth = q && matches ? 4 : 1;
|
|
177
|
+
const update = {};
|
|
178
|
+
if (n.hidden !== hidden) update.hidden = hidden;
|
|
179
|
+
if ((n.borderWidth ?? 1) !== borderWidth) update.borderWidth = borderWidth;
|
|
180
|
+
if (Object.keys(update).length) {
|
|
181
|
+
update.id = n.id;
|
|
182
|
+
nodeUpdates.push(update);
|
|
183
|
+
}
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
loadedSet.edges.forEach((e) => {
|
|
187
|
+
const typeHidden = !viewState.visibleEdgeTypes.has(e._type);
|
|
188
|
+
const fromNode = loadedSet.nodes.get(e.from);
|
|
189
|
+
const toNode = loadedSet.nodes.get(e.to);
|
|
190
|
+
const endpointHidden =
|
|
191
|
+
(fromNode && fromNode.hidden) || (toNode && toNode.hidden);
|
|
192
|
+
const hidden = typeHidden || !!endpointHidden;
|
|
193
|
+
if (e.hidden !== hidden) {
|
|
194
|
+
edgeUpdates.push({ id: e.id, hidden });
|
|
195
|
+
}
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
if (nodeUpdates.length) loadedSet.nodes.update(nodeUpdates);
|
|
199
|
+
if (edgeUpdates.length) loadedSet.edges.update(edgeUpdates);
|
|
200
|
+
updateLoadedCounter();
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function updateLoadedCounter() {
|
|
204
|
+
const visibleNodes = loadedSet.nodes.get({ filter: (n) => !n.hidden }).length;
|
|
205
|
+
const visibleEdges = loadedSet.edges.get({ filter: (e) => !e.hidden }).length;
|
|
206
|
+
const el = document.getElementById("loaded-counter");
|
|
207
|
+
if (el) {
|
|
208
|
+
el.textContent = `Loaded: ${loadedSet.nodes.length} nodes, ${loadedSet.edges.length} edges (${visibleNodes}/${visibleEdges} visible)`;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function degreeToSize(degree) {
|
|
213
|
+
return Math.min(35, 12 + Math.sqrt(degree) * 4);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function recomputeNodeSizes() {
|
|
217
|
+
const degree = new Map();
|
|
218
|
+
loadedSet.edges.forEach((e) => {
|
|
219
|
+
degree.set(e.from, (degree.get(e.from) ?? 0) + 1);
|
|
220
|
+
degree.set(e.to, (degree.get(e.to) ?? 0) + 1);
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
const updates = [];
|
|
224
|
+
loadedSet.nodes.forEach((n) => {
|
|
225
|
+
const newSize = degreeToSize(degree.get(n.id) ?? 0);
|
|
226
|
+
if (n.size !== newSize) {
|
|
227
|
+
updates.push({ id: n.id, size: newSize });
|
|
228
|
+
}
|
|
229
|
+
});
|
|
230
|
+
if (updates.length) loadedSet.nodes.update(updates);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// === EVENTS ===
|
|
234
|
+
async function onNodeClick(params) {
|
|
235
|
+
if (params.nodes.length === 0) return;
|
|
236
|
+
const nodeId = params.nodes[0];
|
|
237
|
+
const node = loadedSet.nodes.get(nodeId);
|
|
238
|
+
|
|
239
|
+
// Always update the detail panel
|
|
240
|
+
document.getElementById("panel-hint").style.display = "none";
|
|
241
|
+
const content = document.getElementById("panel-content");
|
|
242
|
+
content.style.display = "block";
|
|
243
|
+
content.textContent = `[${node._group}] ${node.label}\n\n${JSON.stringify(node._properties, null, 2)}`;
|
|
244
|
+
|
|
245
|
+
// Expand if not already expanded and not already in flight
|
|
246
|
+
if (node._expanded || pendingExpands.has(nodeId)) return;
|
|
247
|
+
|
|
248
|
+
if (node._group === "File") {
|
|
249
|
+
pendingExpands.add(nodeId);
|
|
250
|
+
try {
|
|
251
|
+
const data = await fetchExpand("file", { filePath: node._properties.path });
|
|
252
|
+
mergeIntoLoaded(data, nodeId);
|
|
253
|
+
recomputeNodeSizes();
|
|
254
|
+
// Mark file as expanded
|
|
255
|
+
loadedSet.nodes.update({ id: nodeId, _expanded: true });
|
|
256
|
+
applyViewFilters();
|
|
257
|
+
} catch (err) {
|
|
258
|
+
document.getElementById("status").textContent = err.message;
|
|
259
|
+
} finally {
|
|
260
|
+
pendingExpands.delete(nodeId);
|
|
261
|
+
}
|
|
262
|
+
} else if (node._group === "Function") {
|
|
263
|
+
pendingExpands.add(nodeId);
|
|
264
|
+
try {
|
|
265
|
+
const data = await fetchExpand("function", {
|
|
266
|
+
name: node._properties.name,
|
|
267
|
+
filePath: node._properties.filePath,
|
|
268
|
+
});
|
|
269
|
+
mergeIntoLoaded(data, nodeId);
|
|
270
|
+
recomputeNodeSizes();
|
|
271
|
+
loadedSet.nodes.update({ id: nodeId, _expanded: true });
|
|
272
|
+
applyViewFilters();
|
|
273
|
+
} catch (err) {
|
|
274
|
+
document.getElementById("status").textContent = err.message;
|
|
275
|
+
} finally {
|
|
276
|
+
pendingExpands.delete(nodeId);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
// Repository and Class clicks: detail panel only, no expand.
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function onNodeDoubleClick(params) {
|
|
283
|
+
if (params.nodes.length === 0) return;
|
|
284
|
+
const nodeId = params.nodes[0];
|
|
285
|
+
const node = loadedSet.nodes.get(nodeId);
|
|
286
|
+
if (!node || !node._expanded) return;
|
|
287
|
+
|
|
288
|
+
// Find every node that was loaded *because of* nodeId
|
|
289
|
+
const candidatesToRemove = loadedSet.nodes.get({
|
|
290
|
+
filter: (n) => n._expandedBy && n._expandedBy.has(nodeId),
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
const removeIds = [];
|
|
294
|
+
for (const cand of candidatesToRemove) {
|
|
295
|
+
cand._expandedBy.delete(nodeId);
|
|
296
|
+
if (cand._expandedBy.size === 0) {
|
|
297
|
+
removeIds.push(cand.id);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
if (removeIds.length > 0) {
|
|
302
|
+
// Remove edges touching removed nodes
|
|
303
|
+
const removeIdsSet = new Set(removeIds);
|
|
304
|
+
const edgeIds = loadedSet.edges.get({
|
|
305
|
+
filter: (e) => removeIdsSet.has(e.from) || removeIdsSet.has(e.to),
|
|
306
|
+
fields: ["id"],
|
|
307
|
+
}).map((e) => e.id);
|
|
308
|
+
|
|
309
|
+
loadedSet.edges.remove(edgeIds);
|
|
310
|
+
loadedSet.nodes.remove(removeIds);
|
|
311
|
+
recomputeNodeSizes();
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
loadedSet.nodes.update({ id: nodeId, _expanded: false });
|
|
315
|
+
applyViewFilters();
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
function bindSidebarEvents() {
|
|
319
|
+
document.querySelectorAll('input[data-node-type]').forEach((input) => {
|
|
320
|
+
input.addEventListener("change", (e) => {
|
|
321
|
+
const t = e.target.dataset.nodeType;
|
|
322
|
+
if (e.target.checked) {
|
|
323
|
+
viewState.visibleNodeTypes.add(t);
|
|
324
|
+
} else {
|
|
325
|
+
viewState.visibleNodeTypes.delete(t);
|
|
326
|
+
}
|
|
327
|
+
applyViewFilters();
|
|
328
|
+
});
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
document.querySelectorAll('input[data-edge-type]').forEach((input) => {
|
|
332
|
+
input.addEventListener("change", (e) => {
|
|
333
|
+
const t = e.target.dataset.edgeType;
|
|
334
|
+
if (e.target.checked) {
|
|
335
|
+
viewState.visibleEdgeTypes.add(t);
|
|
336
|
+
} else {
|
|
337
|
+
viewState.visibleEdgeTypes.delete(t);
|
|
338
|
+
}
|
|
339
|
+
applyViewFilters();
|
|
340
|
+
});
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
let searchTimer = null;
|
|
344
|
+
const searchInput = document.getElementById("search-input");
|
|
345
|
+
const searchResult = document.getElementById("search-result");
|
|
346
|
+
searchInput.addEventListener("input", (e) => {
|
|
347
|
+
viewState.search = e.target.value;
|
|
348
|
+
applyViewFilters();
|
|
349
|
+
|
|
350
|
+
// Count local matches
|
|
351
|
+
const q = viewState.search.trim().toLowerCase();
|
|
352
|
+
if (!q) {
|
|
353
|
+
searchResult.textContent = "";
|
|
354
|
+
return;
|
|
355
|
+
}
|
|
356
|
+
const localHits = loadedSet.nodes.get({
|
|
357
|
+
filter: (n) => n.label && n.label.toLowerCase().includes(q),
|
|
358
|
+
}).length;
|
|
359
|
+
searchResult.textContent = `${localHits} local match${localHits === 1 ? "" : "es"}`;
|
|
360
|
+
|
|
361
|
+
// Debounced server fallback for "load missing matches"
|
|
362
|
+
clearTimeout(searchTimer);
|
|
363
|
+
searchTimer = setTimeout(async () => {
|
|
364
|
+
if (viewState.search.trim().length < 2) return;
|
|
365
|
+
try {
|
|
366
|
+
const data = await fetchSearch(viewState.search.trim());
|
|
367
|
+
if (data.nodes && data.nodes.length > 0) {
|
|
368
|
+
mergeIntoLoaded(data);
|
|
369
|
+
recomputeNodeSizes();
|
|
370
|
+
applyViewFilters();
|
|
371
|
+
searchResult.textContent = `${localHits} local + ${data.nodes.length} server match${data.nodes.length === 1 ? "" : "es"}`;
|
|
372
|
+
} else if (localHits === 0) {
|
|
373
|
+
searchResult.textContent = "No matches";
|
|
374
|
+
}
|
|
375
|
+
} catch {
|
|
376
|
+
// Quietly ignore search failures — user is still typing
|
|
377
|
+
}
|
|
378
|
+
}, 350);
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
document.getElementById("reset-view-btn").addEventListener("click", () => {
|
|
382
|
+
viewState.search = "";
|
|
383
|
+
viewState.visibleNodeTypes = new Set(["Repository", "File", "Class", "Function"]);
|
|
384
|
+
viewState.visibleEdgeTypes = new Set([
|
|
385
|
+
"CONTAINS_FILE", "CONTAINS", "HAS_METHOD", "IMPORTS", "CALLS",
|
|
386
|
+
]);
|
|
387
|
+
document.getElementById("search-input").value = "";
|
|
388
|
+
document.getElementById("search-result").textContent = "";
|
|
389
|
+
document.querySelectorAll('input[data-node-type]').forEach((i) => { i.checked = true; });
|
|
390
|
+
document.querySelectorAll('input[data-edge-type]').forEach((i) => {
|
|
391
|
+
i.checked = i.dataset.edgeType !== "IMPORTS_SYMBOL";
|
|
392
|
+
});
|
|
393
|
+
applyViewFilters();
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
document.getElementById("reset-all-btn").addEventListener("click", async () => {
|
|
397
|
+
loadedSet.nodes.clear();
|
|
398
|
+
loadedSet.edges.clear();
|
|
399
|
+
edgeIdCounter = 0;
|
|
400
|
+
try {
|
|
401
|
+
const data = await fetchGraph(parseUrlFilters());
|
|
402
|
+
mergeIntoLoaded(data);
|
|
403
|
+
recomputeNodeSizes();
|
|
404
|
+
applyViewFilters();
|
|
405
|
+
} catch (err) {
|
|
406
|
+
document.getElementById("status").textContent = err.message;
|
|
407
|
+
}
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
let physicsEnabled = true;
|
|
411
|
+
document.getElementById("freeze-btn").addEventListener("click", (e) => {
|
|
412
|
+
physicsEnabled = !physicsEnabled;
|
|
413
|
+
network.setOptions({ physics: { enabled: physicsEnabled } });
|
|
414
|
+
e.target.textContent = physicsEnabled ? "Freeze layout" : "Unfreeze layout";
|
|
415
|
+
});
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
// === BOOT ===
|
|
419
|
+
async function boot() {
|
|
420
|
+
const status = document.getElementById("status");
|
|
421
|
+
status.textContent = "Fetching graph data...";
|
|
422
|
+
|
|
423
|
+
initNetwork();
|
|
424
|
+
bindSidebarEvents();
|
|
425
|
+
|
|
426
|
+
let data;
|
|
427
|
+
try {
|
|
428
|
+
data = await fetchGraph(parseUrlFilters());
|
|
429
|
+
} catch (err) {
|
|
430
|
+
status.textContent = err.message;
|
|
431
|
+
return;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
if (data.nodes.length === 0) {
|
|
435
|
+
status.textContent = "No graph data — run `code-graph-rag index` first, then refresh.";
|
|
436
|
+
return;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
mergeIntoLoaded(data);
|
|
440
|
+
recomputeNodeSizes();
|
|
441
|
+
applyViewFilters();
|
|
442
|
+
status.textContent = `${loadedSet.nodes.length} nodes, ${loadedSet.edges.length} edges`;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
boot().catch(console.error);
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<title>Code Graph RAG — Visualization</title>
|
|
7
|
+
<script src="https://unpkg.com/vis-network/standalone/umd/vis-network.min.js"></script>
|
|
8
|
+
<style>
|
|
9
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
10
|
+
body { font-family: monospace; background: #0d1117; color: #c9d1d9; height: 100vh; display: flex; flex-direction: column; }
|
|
11
|
+
#header { padding: 12px 16px; background: #161b22; border-bottom: 1px solid #30363d; font-size: 14px; }
|
|
12
|
+
#header h1 { font-size: 16px; color: #58a6ff; }
|
|
13
|
+
#main { display: flex; flex: 1; overflow: hidden; }
|
|
14
|
+
|
|
15
|
+
#sidebar { width: 240px; background: #161b22; border-right: 1px solid #30363d; padding: 16px; overflow-y: auto; font-size: 12px; }
|
|
16
|
+
#sidebar h3 { font-size: 11px; color: #58a6ff; margin-bottom: 6px; margin-top: 14px; text-transform: uppercase; letter-spacing: 0.5px; }
|
|
17
|
+
#sidebar h3:first-child { margin-top: 0; }
|
|
18
|
+
#sidebar input[type="text"] {
|
|
19
|
+
width: 100%; padding: 6px 8px; background: #0d1117; color: #c9d1d9;
|
|
20
|
+
border: 1px solid #30363d; border-radius: 4px; font-family: inherit; font-size: 12px;
|
|
21
|
+
}
|
|
22
|
+
#sidebar label { display: flex; align-items: center; gap: 6px; padding: 3px 0; cursor: pointer; }
|
|
23
|
+
#sidebar label input { cursor: pointer; }
|
|
24
|
+
#sidebar .legend-row { display: flex; align-items: center; gap: 6px; padding: 2px 0; }
|
|
25
|
+
#sidebar .legend-swatch { width: 10px; height: 10px; border-radius: 50%; }
|
|
26
|
+
#sidebar button {
|
|
27
|
+
width: 100%; padding: 6px 8px; margin-top: 4px;
|
|
28
|
+
background: #21262d; color: #c9d1d9; border: 1px solid #30363d; border-radius: 4px;
|
|
29
|
+
font-family: inherit; font-size: 12px; cursor: pointer;
|
|
30
|
+
}
|
|
31
|
+
#sidebar button:hover { background: #30363d; }
|
|
32
|
+
#sidebar #search-result { color: #8b949e; font-size: 11px; margin-top: 4px; min-height: 14px; }
|
|
33
|
+
#sidebar #loaded-counter { color: #8b949e; margin-top: 12px; font-size: 11px; }
|
|
34
|
+
|
|
35
|
+
#graph { flex: 1; }
|
|
36
|
+
#panel { width: 300px; background: #161b22; border-left: 1px solid #30363d; padding: 16px; overflow-y: auto; font-size: 12px; }
|
|
37
|
+
#panel h2 { font-size: 14px; color: #58a6ff; margin-bottom: 8px; }
|
|
38
|
+
#panel pre { white-space: pre-wrap; word-break: break-all; color: #8b949e; }
|
|
39
|
+
#status { color: #8b949e; }
|
|
40
|
+
</style>
|
|
41
|
+
</head>
|
|
42
|
+
<body>
|
|
43
|
+
<div id="header">
|
|
44
|
+
<h1>Code Graph RAG</h1>
|
|
45
|
+
<span id="status">Loading graph...</span>
|
|
46
|
+
</div>
|
|
47
|
+
<div id="main">
|
|
48
|
+
<div id="sidebar">
|
|
49
|
+
<h3>Search</h3>
|
|
50
|
+
<input id="search-input" type="text" placeholder="function or class name…" autocomplete="off">
|
|
51
|
+
<div id="search-result"></div>
|
|
52
|
+
|
|
53
|
+
<h3>Show node types</h3>
|
|
54
|
+
<label><input type="checkbox" data-node-type="Repository" checked> Repository</label>
|
|
55
|
+
<label><input type="checkbox" data-node-type="File" checked> File</label>
|
|
56
|
+
<label><input type="checkbox" data-node-type="Class" checked> Class</label>
|
|
57
|
+
<label><input type="checkbox" data-node-type="Function" checked> Function</label>
|
|
58
|
+
|
|
59
|
+
<h3>Show edge types</h3>
|
|
60
|
+
<label><input type="checkbox" data-edge-type="CONTAINS_FILE" checked> CONTAINS_FILE</label>
|
|
61
|
+
<label><input type="checkbox" data-edge-type="CONTAINS" checked> CONTAINS</label>
|
|
62
|
+
<label><input type="checkbox" data-edge-type="HAS_METHOD" checked> HAS_METHOD</label>
|
|
63
|
+
<label><input type="checkbox" data-edge-type="IMPORTS" checked> IMPORTS</label>
|
|
64
|
+
<label><input type="checkbox" data-edge-type="CALLS" checked> CALLS</label>
|
|
65
|
+
<label><input type="checkbox" data-edge-type="IMPORTS_SYMBOL"> IMPORTS_SYMBOL</label>
|
|
66
|
+
|
|
67
|
+
<h3>Legend</h3>
|
|
68
|
+
<div class="legend-row"><span class="legend-swatch" style="background:#1f6feb"></span> Repository</div>
|
|
69
|
+
<div class="legend-row"><span class="legend-swatch" style="background:#1a7f37"></span> File</div>
|
|
70
|
+
<div class="legend-row"><span class="legend-swatch" style="background:#8957e5"></span> Class</div>
|
|
71
|
+
<div class="legend-row"><span class="legend-swatch" style="background:#da3633"></span> Function</div>
|
|
72
|
+
|
|
73
|
+
<button id="freeze-btn">Freeze layout</button>
|
|
74
|
+
<button id="reset-view-btn">Reset view filters</button>
|
|
75
|
+
<button id="reset-all-btn">Reset everything</button>
|
|
76
|
+
<div id="loaded-counter">Loaded: 0 nodes, 0 edges</div>
|
|
77
|
+
</div>
|
|
78
|
+
|
|
79
|
+
<div id="graph"></div>
|
|
80
|
+
<div id="panel">
|
|
81
|
+
<h2>Details</h2>
|
|
82
|
+
<p id="panel-hint" style="color:#8b949e">Click a node to see details</p>
|
|
83
|
+
<pre id="panel-content" style="display:none"></pre>
|
|
84
|
+
</div>
|
|
85
|
+
</div>
|
|
86
|
+
<script src="graph.js"></script>
|
|
87
|
+
</body>
|
|
88
|
+
</html>
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export interface CypherQuery {
|
|
2
|
+
cypher: string;
|
|
3
|
+
params: Record<string, unknown>;
|
|
4
|
+
}
|
|
5
|
+
export declare const INITIAL_LIMIT = 500;
|
|
6
|
+
export declare const EXPAND_FILE_LIMIT = 100;
|
|
7
|
+
export declare const EXPAND_FUNCTION_LIMIT = 50;
|
|
8
|
+
export declare const SEARCH_LIMIT = 25;
|
|
9
|
+
export declare function repoOverview(repoName?: string): CypherQuery;
|
|
10
|
+
export declare function filterByFile(relativePath: string): CypherQuery;
|
|
11
|
+
export declare function filterByFunction(name: string): CypherQuery;
|
|
12
|
+
export declare function expandFile(filePath: string): CypherQuery;
|
|
13
|
+
export declare function expandFunction(name: string, filePath: string): CypherQuery;
|
|
14
|
+
export declare function searchByName(prefix: string): CypherQuery;
|