🚀 Socket Launch Week Day 5:Introducing Repository Access Permissions and Custom Roles.Learn more
Sign In

geraph

Package Overview
Dependencies
Maintainers
1
Versions
8
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

geraph - npm Package Compare versions

Comparing version
0.3.0
to
0.4.0
+12
dist/analyze-V3KBFH2R.js
import {
analyzeGraph,
detectCommunities,
findGodNodes,
findSurprisingConnections
} from "./chunk-JK3K5KL7.js";
export {
analyzeGraph,
detectCommunities,
findGodNodes,
findSurprisingConnections
};
// src/core/analyze.ts
import louvain from "graphology-communities-louvain";
var louvainAlgorithm = louvain;
function isStructuralNoise(graph, nodeId) {
const data = graph.getNodeAttributes(nodeId);
if (data.type === "intent") return true;
if (nodeId.startsWith("import::")) return true;
if (nodeId.startsWith("unresolved_")) return true;
return false;
}
function findGodNodes(graph, topN = 10) {
const ranked = [];
graph.forEachNode((nodeId, data) => {
if (isStructuralNoise(graph, nodeId)) return;
ranked.push({
id: nodeId,
name: data.name,
type: data.type,
degree: graph.degree(nodeId)
});
});
ranked.sort((a, b) => b.degree - a.degree);
return ranked.slice(0, topN);
}
function detectCommunities(graph) {
if (graph.order === 0) return [];
let communityMap = {};
let seed = 123456789;
const rng = () => {
seed = (seed * 9301 + 49297) % 233280;
return seed / 233280;
};
try {
communityMap = louvainAlgorithm(graph, { rng });
} catch {
graph.forEachNode((nodeId) => {
communityMap[nodeId] = 0;
});
}
const groups = /* @__PURE__ */ new Map();
for (const [nodeId, cid] of Object.entries(communityMap)) {
if (!groups.has(cid)) groups.set(cid, []);
groups.get(cid).push(nodeId);
}
const communities = [];
for (const [cid, nodes] of groups.entries()) {
communities.push({
id: cid,
nodes,
cohesion: cohesionScore(graph, nodes)
});
}
communities.sort((a, b) => b.nodes.length - a.nodes.length);
return communities;
}
function cohesionScore(graph, communityNodes) {
const n = communityNodes.length;
if (n <= 1) return 1;
const nodeSet = new Set(communityNodes);
let internalEdges = 0;
graph.forEachEdge((_edgeId, _data, source, target) => {
if (nodeSet.has(source) && nodeSet.has(target)) {
internalEdges++;
}
});
const possible = n * (n - 1);
return possible > 0 ? Math.round(internalEdges / possible * 100) / 100 : 0;
}
function findSurprisingConnections(graph, communities, topN = 5) {
const nodeCommunity = /* @__PURE__ */ new Map();
for (const comm of communities) {
for (const nodeId of comm.nodes) {
nodeCommunity.set(nodeId, comm.id);
}
}
const candidates = [];
graph.forEachEdge((_edgeId, edgeData, source, target) => {
const srcComm = nodeCommunity.get(source);
const tgtComm = nodeCommunity.get(target);
if (srcComm === void 0 || tgtComm === void 0) return;
if (srcComm === tgtComm) return;
if (isStructuralNoise(graph, source) || isStructuralNoise(graph, target))
return;
if (edgeData.type === "imports" || edgeData.type === "defines") return;
const srcData = graph.getNodeAttributes(source);
const tgtData = graph.getNodeAttributes(target);
let score = 1;
const reasons = [];
reasons.push(`bridges community ${srcComm} \u2192 community ${tgtComm}`);
if (edgeData.type === "references") {
score += 2;
reasons.push(`${edgeData.type} relationship (deep AST coupling)`);
}
const srcDeg = graph.degree(source);
const tgtDeg = graph.degree(target);
if (Math.min(srcDeg, tgtDeg) <= 2 && Math.max(srcDeg, tgtDeg) >= 5) {
score += 1;
const peripheral = srcDeg <= 2 ? srcData.name : tgtData.name;
const hub = srcDeg <= 2 ? tgtData.name : srcData.name;
reasons.push(
`peripheral node "${peripheral}" unexpectedly reaches hub "${hub}"`
);
}
if (edgeData.confidence === "AMBIGUOUS") {
score += 3;
reasons.push("ambiguous connection \u2014 needs verification");
} else if (edgeData.confidence === "INFERRED") {
score += 2;
reasons.push("inferred connection \u2014 not explicitly stated in source");
}
candidates.push({
source,
sourceName: srcData.name,
target,
targetName: tgtData.name,
edgeType: edgeData.type,
confidence: edgeData.confidence,
sourceCommunity: srcComm,
targetCommunity: tgtComm,
why: reasons.join("; "),
_score: score
});
});
candidates.sort((a, b) => b._score - a._score);
const seenPairs = /* @__PURE__ */ new Set();
const results = [];
for (const c of candidates) {
const pair = [c.sourceCommunity, c.targetCommunity].sort().join("-");
if (seenPairs.has(pair)) continue;
seenPairs.add(pair);
const { _score, ...connection } = c;
results.push(connection);
if (results.length >= topN) break;
}
return results;
}
function analyzeGraph(graph) {
const godNodes = findGodNodes(graph);
const communities = detectCommunities(graph);
const surprisingConnections = findSurprisingConnections(
graph,
communities
);
const knowledgeGaps = [];
graph.forEachNode((nodeId) => {
if (graph.degree(nodeId) <= 1 && !isStructuralNoise(graph, nodeId)) {
knowledgeGaps.push(nodeId);
}
});
const questions = [];
if (surprisingConnections.length > 0) {
questions.push("Why are these distinct communities connected via Surprising Connections?");
}
if (godNodes.length > 0) {
questions.push(`How would the system react if the core logic in '${godNodes[0]?.name}' was refactored?`);
}
if (knowledgeGaps.length > 5) {
questions.push("There are several isolated modules; are these dead code or missing integration tests?");
}
if (communities.length > 1) {
questions.push("Are the boundaries between these communities enforced, or is there hidden leakage?");
}
return {
godNodes,
communities,
surprisingConnections,
nodeCount: graph.order,
edgeCount: graph.size,
knowledgeGaps: knowledgeGaps.slice(0, 10),
// Limit to top 10
suggestedQuestions: questions
};
}
export {
findGodNodes,
detectCommunities,
findSurprisingConnections,
analyzeGraph
};
// src/core/mcp.ts
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import {
CallToolRequestSchema,
ListToolsRequestSchema,
ListResourcesRequestSchema,
ReadResourceRequestSchema
} from "@modelcontextprotocol/sdk/types.js";
import fs from "fs";
import path from "path";
async function runMcpServer(graph, targetDir) {
const server = new Server(
{
name: "geraph",
version: "0.4.0"
},
{
capabilities: {
tools: {},
resources: {}
}
}
);
server.setRequestHandler(ListToolsRequestSchema, async () => {
return {
tools: [
{
name: "search_graph",
description: "Search for nodes in the knowledge graph by partial name. Useful to find exact node IDs. Supports pagination. You can also search for a file by its path (e.g., 'src/auth.ts') using type 'file', because node IDs contain file paths. (CLI Alternative: 'geraph search <term>')",
inputSchema: {
type: "object",
properties: {
name: {
type: "string",
description: "The partial name to search for (e.g. 'auth')"
},
type: {
type: "string",
description: "Optional filter by node type (e.g. 'function', 'class')"
},
page: {
type: "number",
description: "Page number for pagination (default: 1)"
},
limit: {
type: "number",
description: "Number of results per page (default: 20)"
}
},
required: ["name"]
}
},
{
name: "get_node",
description: "Get detailed metadata for a specific node by its exact ID or fuzzy symbol name. (CLI Alternative: 'geraph node <symbol>')",
inputSchema: {
type: "object",
properties: {
symbol: {
type: "string",
description: "The exact node ID or symbol name"
},
type: {
type: "string",
description: "Optional filter by node type (e.g., 'interface', 'function')"
},
source: {
type: "string",
description: "Optional filter by source file path (e.g., 'auth.ts')"
}
},
required: ["symbol"]
}
},
{
name: "get_neighbors",
description: "Get all incoming and outgoing edges for a specific node to trace its direct dependencies. Supports pagination. (CLI Alternative: 'geraph neighbors <symbol>')",
inputSchema: {
type: "object",
properties: {
symbol: {
type: "string",
description: "The exact node ID or symbol name"
},
type: {
type: "string",
description: "Optional filter by node type (e.g., 'interface', 'function')"
},
source: {
type: "string",
description: "Optional filter by source file path (e.g., 'auth.ts')"
},
page: {
type: "number",
description: "Page number for pagination (default: 1)"
},
limit: {
type: "number",
description: "Number of edges per direction per page (default: 20)"
}
},
required: ["symbol"]
}
},
{
name: "shortest_path",
description: "Find the shortest sequence of edges connecting two nodes using fuzzy symbol/ID lookup. (CLI Alternative: 'geraph path <source> <target>')",
inputSchema: {
type: "object",
properties: {
source: {
type: "string",
description: "The fuzzy starting node ID or symbol name"
},
target: {
type: "string",
description: "The fuzzy destination node ID or symbol name"
},
max_hops: {
type: "number",
description: "Maximum hops to consider (default: 8)"
}
},
required: ["source", "target"]
}
},
{
name: "god_nodes",
description: "Return the most connected nodes \u2014 the core architectural pillars of the codebase. Supports pagination. (CLI Alternative: 'geraph god')",
inputSchema: {
type: "object",
properties: {
page: {
type: "number",
description: "Page number for pagination (default: 1)"
},
limit: {
type: "number",
description: "Number of results per page (default: 10)"
}
}
}
},
{
name: "get_community",
description: "Get all nodes in a community by community ID. Supports pagination. (CLI Alternative: 'geraph community <id>')",
inputSchema: {
type: "object",
properties: {
community_id: {
type: "number",
description: "Community ID (0-indexed by size)"
},
page: {
type: "number",
description: "Page number for pagination (default: 1)"
},
limit: {
type: "number",
description: "Number of results per page (default: 20)"
}
},
required: ["community_id"]
}
},
{
name: "get_surprises",
description: "Discover surprising cross-community couplings that link otherwise independent modules. Supports pagination. (CLI Alternative: 'geraph surprises')",
inputSchema: {
type: "object",
properties: {
page: {
type: "number",
description: "Page number for pagination (default: 1)"
},
limit: {
type: "number",
description: "Number of results per page (default: 20)"
}
}
}
},
{
name: "query_graph",
description: "Search the AST graph using BFS or DFS traversal. Returns a compact context representation. Supports natural language questions or keywords. (CLI Alternative: 'geraph query <symbol-or-question>')",
inputSchema: {
type: "object",
properties: {
symbol: {
type: "string",
description: "Fuzzy starting symbol or node ID, or natural language question"
},
question: {
type: "string",
description: "Natural language question or keywords (for Graphify parity)"
},
mode: {
type: "string",
enum: ["bfs", "dfs"],
default: "bfs",
description: "Traversal mode: bfs (breadth) or dfs (depth)"
},
depth: {
type: "number",
default: 3,
description: "Traversal depth limit"
},
token_budget: {
type: "number",
default: 2e3,
description: "Estimated output token limit"
}
}
}
},
{
name: "graph_stats",
description: "Return summary statistics of the graph: node count, edge count, community count, and extraction confidence percentage breakdown. (CLI Alternative: 'geraph stats')",
inputSchema: {
type: "object",
properties: {}
}
},
{
name: "scan_graph",
description: "Triggers a full rebuild of the Geraph AST graph. Use this after making significant code modifications or pushing git commits to ensure your structural memory is up-to-date. (CLI Alternative: 'geraph scan')",
inputSchema: {
type: "object",
properties: {}
}
}
]
};
});
server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
if (!args) {
throw new Error("Arguments are required");
}
try {
if (name === "search_graph") {
const queryName = args.name;
const typeFilter = args.type;
const page = args.page;
const limit = args.limit;
const { searchGraph } = await import("./query-FGSLUNHR.js");
const matches = await searchGraph(
graph,
queryName,
typeFilter,
page,
limit
);
return {
content: [{ type: "text", text: JSON.stringify(matches, null, 2) }]
};
}
if (name === "get_node") {
const symbol = args.symbol;
const typeFilter = args.type;
const sourceFilter = args.source;
const { getNode } = await import("./query-FGSLUNHR.js");
try {
const result = await getNode(graph, symbol, typeFilter, sourceFilter);
return {
content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
};
} catch (error) {
return {
content: [
{
type: "text",
text: JSON.stringify({
error: error instanceof Error ? error.message : String(error)
})
}
],
isError: true
};
}
}
if (name === "get_neighbors") {
const symbol = args.symbol;
const typeFilter = args.type;
const sourceFilter = args.source;
const page = args.page;
const limit = args.limit;
const { getNeighbors } = await import("./query-FGSLUNHR.js");
try {
const result = await getNeighbors(
graph,
symbol,
typeFilter,
sourceFilter,
page,
limit
);
return {
content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
};
} catch (error) {
return {
content: [
{
type: "text",
text: JSON.stringify({
error: error instanceof Error ? error.message : String(error)
})
}
],
isError: true
};
}
}
if (name === "shortest_path") {
const source = args.source;
const target = args.target;
const maxHops = args.max_hops !== void 0 ? Number(args.max_hops) : 8;
const { shortestPath } = await import("./query-FGSLUNHR.js");
try {
const result = await shortestPath(graph, source, target, maxHops);
return {
content: [
{
type: "text",
text: result
}
]
};
} catch (error) {
return {
content: [
{
type: "text",
text: JSON.stringify({
error: error instanceof Error ? error.message : String(error)
})
}
],
isError: true
};
}
}
if (name === "god_nodes") {
const page = args.page || 1;
const limit = args.limit || 10;
const { getGodNodes } = await import("./query-FGSLUNHR.js");
try {
const result = await getGodNodes(graph, page, limit);
return {
content: [{ type: "text", text: result }]
};
} catch (error) {
return {
content: [{ type: "text", text: String(error) }],
isError: true
};
}
}
if (name === "get_community") {
const communityId = Number(args.community_id);
const page = args.page || 1;
const limit = args.limit || 20;
const { getCommunityNodes } = await import("./query-FGSLUNHR.js");
try {
const result = await getCommunityNodes(
graph,
communityId,
page,
limit
);
return {
content: [{ type: "text", text: result }]
};
} catch (error) {
return {
content: [{ type: "text", text: String(error) }],
isError: true
};
}
}
if (name === "get_surprises") {
const page = args.page || 1;
const limit = args.limit || 20;
const { getSurprisingConnections } = await import("./query-FGSLUNHR.js");
try {
const result = await getSurprisingConnections(graph, page, limit);
return {
content: [{ type: "text", text: result }]
};
} catch (error) {
return {
content: [{ type: "text", text: String(error) }],
isError: true
};
}
}
if (name === "query_graph") {
const symbol = args.symbol || args.question;
if (!symbol) {
throw new Error("Either 'symbol' or 'question' parameter is required");
}
const mode = args.mode || "bfs";
const depth = Number(args.depth ?? 3);
const tokenBudget = Number(args.token_budget ?? 2e3);
const { queryGraph } = await import("./query-FGSLUNHR.js");
try {
const result = await queryGraph(
graph,
symbol,
mode,
depth,
tokenBudget
);
return {
content: [{ type: "text", text: result }]
};
} catch (error) {
return {
content: [{ type: "text", text: String(error) }],
isError: true
};
}
}
if (name === "graph_stats") {
const { getGraphStats } = await import("./query-FGSLUNHR.js");
try {
const result = await getGraphStats(graph);
return {
content: [{ type: "text", text: result }]
};
} catch (error) {
return {
content: [{ type: "text", text: String(error) }],
isError: true
};
}
}
if (name === "scan_graph") {
try {
const { exec } = await import("child_process");
const { promisify } = await import("util");
const execAsync = promisify(exec);
await execAsync("geraph scan", { cwd: targetDir });
const { loadGraph } = await import("./query-FGSLUNHR.js");
const newGraph = loadGraph(targetDir);
graph.clear();
newGraph.forEachNode((node, attr) => graph.addNode(node, attr));
newGraph.forEachEdge(
(edge, attr, source, target) => graph.addEdgeWithKey(edge, source, target, attr)
);
let communitiesCount = 0;
try {
const { readFileSync } = await import("fs");
const { join } = await import("path");
const graphJsonPath = join(targetDir, ".geraph", "graph.json");
const raw = readFileSync(graphJsonPath, "utf8");
const data = JSON.parse(raw);
communitiesCount = data.analysis?.communities?.length || 0;
} catch {
}
return {
content: [
{
type: "text",
text: `Graph successfully scanned and memory updated. Discovered ${graph.order} nodes, ${graph.size} edges, and ${communitiesCount} communities.`
}
]
};
} catch (error) {
const msg = error instanceof Error ? error.message : String(error);
return {
content: [{ type: "text", text: `Error scanning graph: ${msg}` }],
isError: true
};
}
}
throw new Error(`Unknown tool: ${name}`);
} catch (error) {
const err = error;
return {
content: [
{ type: "text", text: JSON.stringify({ error: err.message }) }
],
isError: true
};
}
});
server.setRequestHandler(ListResourcesRequestSchema, async () => {
return {
resources: [
{
uri: "geraph://report",
name: "Graph Report",
description: "Full GRAPH_REPORT.md",
mimeType: "text/markdown"
},
{
uri: "geraph://stats",
name: "Graph Stats",
description: "Node/edge/community counts and confidence breakdown",
mimeType: "text/plain"
},
{
uri: "geraph://god-nodes",
name: "God Nodes",
description: "Top 10 most-connected nodes",
mimeType: "text/plain"
},
{
uri: "geraph://surprises",
name: "Surprising Connections",
description: "Cross-community surprising connections",
mimeType: "text/plain"
},
{
uri: "geraph://audit",
name: "Confidence Audit",
description: "EXTRACTED/INFERRED/AMBIGUOUS edge breakdown",
mimeType: "text/plain"
}
]
};
});
server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
const { uri } = request.params;
if (uri === "geraph://report") {
const reportPath = path.join(targetDir, ".geraph", "GRAPH_REPORT.md");
if (fs.existsSync(reportPath)) {
return {
contents: [
{
uri,
mimeType: "text/markdown",
text: fs.readFileSync(reportPath, "utf-8")
}
]
};
}
return {
contents: [
{
uri,
mimeType: "text/markdown",
text: "GRAPH_REPORT.md not found. Run geraph scan first."
}
]
};
}
if (uri === "geraph://stats") {
const { getGraphStats } = await import("./query-FGSLUNHR.js");
const text = await getGraphStats(graph);
return {
contents: [{ uri, mimeType: "text/plain", text }]
};
}
if (uri === "geraph://god-nodes") {
const { getGodNodes } = await import("./query-FGSLUNHR.js");
const text = await getGodNodes(graph, 1, 10);
return {
contents: [{ uri, mimeType: "text/plain", text }]
};
}
if (uri === "geraph://surprises") {
const { getSurprisingConnections } = await import("./query-FGSLUNHR.js");
const text = await getSurprisingConnections(graph, 1, 10);
return {
contents: [{ uri, mimeType: "text/plain", text }]
};
}
if (uri === "geraph://audit") {
let extractedCount = 0;
let inferredCount = 0;
let ambiguousCount = 0;
graph.forEachEdge((edgeId, attr) => {
if (attr.confidence === "EXTRACTED") extractedCount++;
else if (attr.confidence === "INFERRED") inferredCount++;
else if (attr.confidence === "AMBIGUOUS") ambiguousCount++;
});
const total = graph.size || 1;
const extPct = Math.round(extractedCount / total * 100);
const infPct = Math.round(inferredCount / total * 100);
const ambPct = Math.round(ambiguousCount / total * 100);
const text = [
`Total edges: ${total}`,
`EXTRACTED: ${extractedCount} (${extPct}%)`,
`INFERRED: ${inferredCount} (${infPct}%)`,
`AMBIGUOUS: ${ambiguousCount} (${ambPct}%)`
].join("\n");
return {
contents: [{ uri, mimeType: "text/plain", text }]
};
}
throw new Error(`Unknown resource: ${uri}`);
});
const transport = new StdioServerTransport();
await server.connect(transport);
console.error("Geraph MCP Server is running over stdio");
}
export {
runMcpServer
};
// src/core/query.ts
import fs from "fs";
import path from "path";
import { MultiDirectedGraph } from "graphology";
function normalizeId(id) {
return id.replace(/\\/g, "/");
}
function loadGraph(targetDir) {
const graphPath = path.join(targetDir, ".geraph", "graph.json");
if (!fs.existsSync(graphPath)) {
throw new Error(
`Graph data not found in ${targetDir}. Run 'geraph scan' first.`
);
}
const rawData = JSON.parse(fs.readFileSync(graphPath, "utf-8"));
const graph = new MultiDirectedGraph();
const nodes = rawData.nodes || [];
const edges = rawData.edges || [];
const communities = rawData.analysis?.communities || [];
const nodeToCommunity = /* @__PURE__ */ new Map();
communities.forEach((c) => {
const members = c.members || c.nodes || [];
members.forEach((nodeId) => {
nodeToCommunity.set(normalizeId(nodeId), Number(c.id));
});
});
nodes.forEach((n) => {
const nid = normalizeId(n.id);
if (!graph.hasNode(nid)) {
const { id, name, type, file, startLine, ...metadata } = n;
const communityId = nodeToCommunity.get(nid);
graph.addNode(nid, {
name: name || "",
type,
file: normalizeId(file || ""),
startLine: startLine || 0,
metadata: {
...metadata,
...communityId !== void 0 ? { community: communityId } : {}
}
});
}
});
edges.forEach((e) => {
const source = normalizeId(e.source);
const target = normalizeId(e.target);
if (graph.hasNode(source) && graph.hasNode(target)) {
graph.addEdge(source, target, {
type: e.relation || e.type,
confidence: e.confidence,
metadata: e.metadata || {}
});
}
});
return graph;
}
async function searchGraph(targetDirOrGraph, term, targetType, page = 1, limit = 20) {
const graph = typeof targetDirOrGraph === "string" ? loadGraph(targetDirOrGraph) : targetDirOrGraph;
const lowerTerm = term.toLowerCase();
const results = [];
graph.forEachNode((nodeId, attr) => {
if (targetType && attr.type !== targetType) return;
if (nodeId.toLowerCase().includes(lowerTerm) || attr.name && attr.name.toLowerCase().includes(lowerTerm)) {
results.push({
id: nodeId,
name: attr.name,
type: attr.type,
file: attr.file,
links: graph.degree(nodeId)
});
}
});
results.sort((a, b) => b.links - a.links);
const total = results.length;
const totalPages = Math.ceil(total / limit) || 1;
const start = (page - 1) * limit;
const end = start + limit;
return {
data: results.slice(start, end),
meta: {
page,
limit,
total,
totalPages
}
};
}
function resolveTargetNodeId(graph, symbol, targetType, targetSource) {
const normSymbol = normalizeId(symbol);
let targetNodeId = graph.hasNode(normSymbol) ? normSymbol : null;
if (targetNodeId && (targetType || targetSource)) {
const attr = graph.getNodeAttributes(targetNodeId);
if (targetType && attr.type !== targetType) {
targetNodeId = null;
}
if (targetNodeId && targetSource) {
const normSource = normalizeId(targetSource);
if (!attr.file.toLowerCase().endsWith(normSource.toLowerCase())) {
targetNodeId = null;
}
}
}
if (!targetNodeId) {
targetNodeId = graph.findNode((nodeId, attr) => {
if (targetType && attr.type !== targetType) return false;
if (targetSource) {
const normSource = normalizeId(targetSource);
if (!attr.file.endsWith(normSource)) return false;
}
return attr && attr.name && attr.name === symbol || nodeId === normSymbol || nodeId.endsWith("/" + normSymbol) || nodeId.endsWith("::" + normSymbol);
}) ?? null;
}
if (!targetNodeId) {
targetNodeId = graph.findNode((nodeId, attr) => {
if (targetType && attr.type !== targetType) return false;
if (targetSource) {
const normSource = normalizeId(targetSource);
if (!attr.file.toLowerCase().endsWith(normSource.toLowerCase()))
return false;
}
return attr && attr.name && attr.name.toLowerCase() === symbol.toLowerCase() || nodeId.toLowerCase() === normSymbol.toLowerCase() || nodeId.toLowerCase().endsWith("/" + normSymbol.toLowerCase()) || nodeId.toLowerCase().endsWith("::" + normSymbol.toLowerCase());
}) ?? null;
}
if (!targetNodeId) {
const typeMsg = targetType ? ` of type '${targetType}'` : "";
const sourceMsg = targetSource ? ` in source '${targetSource}'` : "";
throw new Error(
`Symbol '${symbol}'${typeMsg}${sourceMsg} not found in the graph.`
);
}
return targetNodeId;
}
async function getNode(targetDirOrGraph, symbol, targetType, targetSource) {
const graph = typeof targetDirOrGraph === "string" ? loadGraph(targetDirOrGraph) : targetDirOrGraph;
const targetNodeId = resolveTargetNodeId(
graph,
symbol,
targetType,
targetSource
);
const targetAttr = graph.getNodeAttributes(targetNodeId);
return {
id: targetNodeId,
name: targetAttr.name,
type: targetAttr.type,
file: targetAttr.file,
line: targetAttr.startLine,
metadata: targetAttr.metadata,
links: {
incoming: graph.inDegree(targetNodeId),
outgoing: graph.outDegree(targetNodeId)
}
};
}
async function getNeighbors(targetDirOrGraph, symbol, targetType, targetSource, page = 1, limit = 20) {
const graph = typeof targetDirOrGraph === "string" ? loadGraph(targetDirOrGraph) : targetDirOrGraph;
const targetNodeId = resolveTargetNodeId(
graph,
symbol,
targetType,
targetSource
);
const targetAttr = graph.getNodeAttributes(targetNodeId);
const result = {
target: {
id: targetNodeId,
name: targetAttr.name,
type: targetAttr.type,
file: targetAttr.file,
line: targetAttr.startLine
},
incoming: [],
outgoing: [],
meta: {
page,
limit,
totalIncoming: 0,
totalOutgoing: 0,
totalPages: 1
}
};
const collectEdges = (nodeId, isOutgoing, seenKeys) => {
const iterator = isOutgoing ? graph.forEachOutEdge.bind(graph) : graph.forEachInEdge.bind(graph);
iterator(nodeId, (edge, attr, source, target) => {
const neighborId = isOutgoing ? target : source;
if (neighborId === targetNodeId) return;
const key = `${neighborId}:${attr.type}`;
if (seenKeys.has(key)) return;
seenKeys.add(key);
const neighborAttr = graph.getNodeAttributes(neighborId);
const nodeInfo = {
id: neighborId,
name: neighborAttr.name,
type: neighborAttr.type,
file: neighborAttr.file,
line: neighborAttr.startLine
};
if (isOutgoing) {
result.outgoing.push({
relation: attr.type,
confidence: attr.confidence,
target: nodeInfo
});
} else {
result.incoming.push({
relation: attr.type,
confidence: attr.confidence,
source: nodeInfo
});
}
});
};
const seenIn = /* @__PURE__ */ new Set();
const seenOut = /* @__PURE__ */ new Set();
collectEdges(targetNodeId, false, seenIn);
collectEdges(targetNodeId, true, seenOut);
const sortEdges = (a, b) => {
const nodeA = a.source || a.target;
const nodeB = b.source || b.target;
if (nodeA.type === "intent" && nodeB.type !== "intent") return -1;
if (nodeB.type === "intent" && nodeA.type !== "intent") return 1;
const degreeA = graph.degree(nodeA.id) || 0;
const degreeB = graph.degree(nodeB.id) || 0;
return degreeB - degreeA;
};
result.incoming.sort(sortEdges);
result.outgoing.sort(sortEdges);
const totalIncoming = result.incoming.length;
const totalOutgoing = result.outgoing.length;
const maxTotal = Math.max(totalIncoming, totalOutgoing);
const totalPages = Math.ceil(maxTotal / limit) || 1;
const start = (page - 1) * limit;
const end = start + limit;
result.incoming = result.incoming.slice(start, end);
result.outgoing = result.outgoing.slice(start, end);
result.meta = {
page,
limit,
totalIncoming,
totalOutgoing,
totalPages
};
return result;
}
function undirectedShortestPath(graph, source, target) {
if (source === target) return [source];
const queue = [source];
const visited = /* @__PURE__ */ new Set([source]);
const parent = /* @__PURE__ */ new Map();
while (queue.length > 0) {
const current = queue.shift();
if (current === target) break;
graph.forEachNeighbor(current, (neighbor) => {
if (!visited.has(neighbor)) {
visited.add(neighbor);
parent.set(neighbor, current);
queue.push(neighbor);
}
});
}
if (!parent.has(target)) return null;
const path2 = [];
let curr = target;
while (curr !== source) {
path2.unshift(curr);
curr = parent.get(curr);
}
path2.unshift(source);
return path2;
}
async function shortestPath(targetDirOrGraph, sourceSymbol, targetSymbol, maxHops = 8) {
const graph = typeof targetDirOrGraph === "string" ? loadGraph(targetDirOrGraph) : targetDirOrGraph;
const normSource = resolveTargetNodeId(graph, sourceSymbol);
const normTarget = resolveTargetNodeId(graph, targetSymbol);
if (normSource === normTarget) {
throw new Error(`Source and target nodes are identical: '${sourceSymbol}'`);
}
const path2 = undirectedShortestPath(graph, normSource, normTarget);
if (!path2) {
throw new Error("No path exists between the given nodes");
}
const hops = path2.length - 1;
if (hops > maxHops) {
return `Path exceeds max_hops=${maxHops} (${hops} hops found).`;
}
const segments = [];
for (let i = 0; i < path2.length - 1; i++) {
const u = path2[i];
const v = path2[i + 1];
let forward = true;
let edgeId;
if (graph.hasDirectedEdge(u, v)) {
edgeId = graph.edges(u, v)[0];
} else if (graph.hasDirectedEdge(v, u)) {
edgeId = graph.edges(v, u)[0];
forward = false;
}
const edata = edgeId ? graph.getEdgeAttributes(edgeId) : void 0;
const rel = edata?.type || "";
const conf = edata?.confidence || "";
const confStr = conf ? ` [${conf}]` : "";
const uLabel = String(graph.getNodeAttribute(u, "name") || u);
const vLabel = String(graph.getNodeAttribute(v, "name") || v);
if (i === 0) {
segments.push(uLabel);
}
if (forward) {
segments.push(`--${rel}${confStr}--> ${vLabel}`);
} else {
segments.push(`<--${rel}${confStr}-- ${vLabel}`);
}
}
return `Shortest path (${hops} hops):
` + segments.join(" ");
}
async function getGodNodes(targetDirOrGraph, page = 1, limit = 10) {
const graph = typeof targetDirOrGraph === "string" ? loadGraph(targetDirOrGraph) : targetDirOrGraph;
const { findGodNodes } = await import("./analyze-V3KBFH2R.js");
const allGods = findGodNodes(graph, graph.order);
const total = allGods.length;
const totalPages = Math.ceil(total / limit) || 1;
const start = (page - 1) * limit;
const end = start + limit;
const paginated = allGods.slice(start, end);
const lines = ["God nodes (most connected):"];
paginated.forEach((n, idx) => {
const globalIdx = start + idx + 1;
lines.push(` ${globalIdx}. ${n.name} [id: ${n.id}] - ${n.degree} edges`);
});
if (totalPages > 1) {
lines.push(`
[Page ${page} of ${totalPages} | Total: ${total} nodes]`);
}
return lines.join("\n");
}
async function getCommunityNodes(targetDirOrGraph, communityId, page = 1, limit = 20) {
const graph = typeof targetDirOrGraph === "string" ? loadGraph(targetDirOrGraph) : targetDirOrGraph;
const { detectCommunities } = await import("./analyze-V3KBFH2R.js");
const communities = detectCommunities(graph);
const targetCommunity = communities.find((c) => c.id === communityId);
if (!targetCommunity) {
throw new Error(`Community ${communityId} not found.`);
}
const total = targetCommunity.nodes.length;
const totalPages = Math.ceil(total / limit) || 1;
const start = (page - 1) * limit;
const end = start + limit;
const paginatedNodes = targetCommunity.nodes.slice(start, end);
const lines = [`Community ${communityId} (${total} nodes):`];
paginatedNodes.forEach((nodeId) => {
const attr = graph.getNodeAttributes(nodeId);
const label = attr.name || nodeId;
const sourceFile = attr.file || "";
lines.push(` ${label} [${sourceFile}]`);
});
if (totalPages > 1) {
lines.push(`
[Page ${page} of ${totalPages} | Total: ${total} nodes]`);
}
return lines.join("\n");
}
async function getSurprisingConnections(targetDirOrGraph, page = 1, limit = 20) {
const graph = typeof targetDirOrGraph === "string" ? loadGraph(targetDirOrGraph) : targetDirOrGraph;
const { detectCommunities, findSurprisingConnections } = await import("./analyze-V3KBFH2R.js");
const communities = detectCommunities(graph);
const surprises = findSurprisingConnections(graph, communities, graph.size);
const total = surprises.length;
const totalPages = Math.ceil(total / limit) || 1;
const start = (page - 1) * limit;
const end = start + limit;
const paginated = surprises.slice(start, end);
if (total === 0) {
return "No surprising connections found.";
}
const lines = ["Surprising cross-community connections:"];
paginated.forEach((s) => {
lines.push(
` ${s.sourceName} <-> ${s.targetName} [${s.edgeType}] - ${s.why}`
);
});
if (totalPages > 1) {
lines.push(
`
[Page ${page} of ${totalPages} | Total: ${total} connections]`
);
}
return lines.join("\n");
}
function scoreNodes(graph, terms) {
const EXACT_MATCH_BONUS = 1e3;
const PREFIX_MATCH_BONUS = 100;
const SUBSTRING_MATCH_BONUS = 1;
const SOURCE_MATCH_BONUS = 0.5;
const scored = [];
graph.forEachNode((nodeId, attr) => {
const name = (attr.name || "").toLowerCase();
const source = (attr.file || "").toLowerCase();
const nidLower = nodeId.toLowerCase();
let score = 0;
for (const t of terms) {
if (t === name || t === nidLower) {
score += EXACT_MATCH_BONUS;
} else if (name.startsWith(t) || nidLower.startsWith(t)) {
score += PREFIX_MATCH_BONUS;
} else if (name.includes(t) || nidLower.includes(t)) {
score += SUBSTRING_MATCH_BONUS;
}
if (source.includes(t)) {
score += SOURCE_MATCH_BONUS;
}
}
if (score > 0) {
scored.push([score, nodeId]);
}
});
return scored.sort((a, b) => b[0] - a[0]);
}
async function queryGraph(targetDirOrGraph, symbol, mode = "bfs", depth = 3, tokenBudget = 2e3) {
const graph = typeof targetDirOrGraph === "string" ? loadGraph(targetDirOrGraph) : targetDirOrGraph;
const terms = symbol.split(/\s+/).map((t) => t.replace(/[?,.:;!]/g, "").toLowerCase().trim()).filter((t) => t.length > 2);
let startNodes = [];
if (terms.length > 0) {
const scored = scoreNodes(graph, terms);
startNodes = scored.slice(0, 3).map((item) => item[1]);
}
if (startNodes.length === 0) {
try {
const fallbackId = resolveTargetNodeId(graph, symbol);
startNodes = [fallbackId];
} catch {
throw new Error(`No matching nodes found for query: '${symbol}'`);
}
}
const degrees = [];
graph.forEachNode((nodeId) => {
degrees.push(graph.degree(nodeId));
});
let hubThreshold = 50;
if (degrees.length > 0) {
const sorted = [...degrees].sort((a, b) => a - b);
const p99Idx = Math.floor(sorted.length * 0.99);
hubThreshold = Math.max(50, sorted[p99Idx] || 50);
}
const seedSet = new Set(startNodes);
const visited = new Set(startNodes);
const edges = [];
if (mode === "bfs") {
const queue = startNodes.map((n) => [n, 0]);
while (queue.length > 0) {
const [curr, d] = queue.shift();
if (d >= depth) continue;
if (!seedSet.has(curr) && graph.degree(curr) >= hubThreshold) {
continue;
}
graph.forEachNeighbor(curr, (neighbor) => {
if (!visited.has(neighbor)) {
visited.add(neighbor);
queue.push([neighbor, d + 1]);
}
edges.push([curr, neighbor]);
});
}
} else {
const stack = [...startNodes].reverse().map((n) => [n, 0]);
while (stack.length > 0) {
const [curr, d] = stack.pop();
if (d >= depth) continue;
if (!seedSet.has(curr) && graph.degree(curr) >= hubThreshold) {
continue;
}
graph.forEachNeighbor(curr, (neighbor) => {
if (!visited.has(neighbor)) {
visited.add(neighbor);
stack.push([neighbor, d + 1]);
}
edges.push([curr, neighbor]);
});
}
}
const traversedNodes = Array.from(visited);
const remainingNodes = traversedNodes.filter((n) => !seedSet.has(n));
remainingNodes.sort((a, b) => graph.degree(b) - graph.degree(a));
const orderedNodes = startNodes.filter((n) => visited.has(n)).concat(remainingNodes);
const seenEdges = /* @__PURE__ */ new Set();
const finalEdges = [];
edges.forEach(([u, v]) => {
if (visited.has(u) && visited.has(v)) {
let edgeId;
let forward = true;
if (graph.hasDirectedEdge(u, v)) {
edgeId = graph.edges(u, v)[0];
} else if (graph.hasDirectedEdge(v, u)) {
edgeId = graph.edges(v, u)[0];
forward = false;
}
if (edgeId) {
const key = forward ? `${u}->${v}` : `${v}->${u}`;
if (!seenEdges.has(key)) {
seenEdges.add(key);
const attr = graph.getEdgeAttributes(edgeId);
finalEdges.push({
u: forward ? u : v,
v: forward ? v : u,
rel: attr.type || "",
conf: attr.confidence || ""
});
}
}
}
});
const startNames = startNodes.map(
(n) => graph.getNodeAttribute(n, "name") || n
);
const header = `Traversal: ${mode.toUpperCase()} depth=${depth} | Start: [${startNames.join(", ")}] | ${orderedNodes.length} nodes found
`;
const charBudget = tokenBudget * 3;
const lines = [];
orderedNodes.forEach((nid) => {
const attr = graph.getNodeAttributes(nid);
const name = attr.name || nid;
const file = attr.file || "";
const loc = attr.startLine || 0;
const community = attr.metadata?.community !== void 0 ? attr.metadata.community : "";
lines.push(`NODE ${name} [src=${file} loc=${loc} community=${community}]`);
});
finalEdges.forEach(({ u, v, rel, conf }) => {
const uLabel = graph.getNodeAttribute(u, "name") || u;
const vLabel = graph.getNodeAttribute(v, "name") || v;
const confStr = conf ? ` [${conf}]` : "";
lines.push(`EDGE ${uLabel} --${rel}${confStr}--> ${vLabel}`);
});
let output = header + lines.join("\n");
if (output.length > charBudget) {
output = output.slice(0, charBudget) + `
... (truncated to ~${tokenBudget} token budget)`;
}
return output;
}
async function getGraphStats(targetDirOrGraph) {
const graph = typeof targetDirOrGraph === "string" ? loadGraph(targetDirOrGraph) : targetDirOrGraph;
let extractedCount = 0;
let inferredCount = 0;
let ambiguousCount = 0;
graph.forEachEdge((edgeId, attr) => {
if (attr.confidence === "EXTRACTED") extractedCount++;
else if (attr.confidence === "INFERRED") inferredCount++;
else if (attr.confidence === "AMBIGUOUS") ambiguousCount++;
});
const total = graph.size || 1;
const extPct = Math.round(extractedCount / total * 100);
const infPct = Math.round(inferredCount / total * 100);
const ambPct = Math.round(ambiguousCount / total * 100);
let communitiesCount = 0;
try {
const { detectCommunities } = await import("./analyze-V3KBFH2R.js");
communitiesCount = detectCommunities(graph).length;
} catch {
}
return [
`Nodes: ${graph.order}`,
`Edges: ${graph.size}`,
`Communities: ${communitiesCount}`,
`EXTRACTED: ${extPct}%`,
`INFERRED: ${infPct}%`,
`AMBIGUOUS: ${ambPct}%`
].join("\n");
}
export {
getCommunityNodes,
getGodNodes,
getGraphStats,
getNeighbors,
getNode,
getSurprisingConnections,
loadGraph,
queryGraph,
searchGraph,
shortestPath
};
MIT License
Copyright (c) 2026 Rupam Mondal
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+94
-9

@@ -232,2 +232,30 @@ // src/core/worker.ts

]);
function findNearestPackageJson(dir) {
let current = dir;
while (true) {
const candidate = path.join(current, "package.json");
if (fs.existsSync(candidate)) return candidate;
const parent = path.dirname(current);
if (parent === current) break;
current = parent;
}
return "";
}
function getBasePackageName(importPath) {
if (importPath.startsWith(".") || importPath.startsWith("/")) {
return importPath;
}
const normalized = importPath.replace(/^node:/, "");
const parts = normalized.split("/");
if (normalized.startsWith("@")) {
if (parts.length >= 2) {
return `${parts[0]}/${parts[1]}`;
}
} else {
if (parts.length >= 1) {
return parts[0];
}
}
return importPath;
}
function resolveImportToNode(importPath, sourceFilePath, aliases = []) {

@@ -302,2 +330,3 @@ const checkCandidates = (resolvedBase) => {

(import_statement source: (string) @import_source)
(import) @import_source

@@ -387,2 +416,43 @@ (call_expression

currentSource = capture.node.text.replace(/['"]/g, "");
if (capture.node.parent?.type === "import_expression" || capture.node.parent?.type === "call_expression") {
let parent = capture.node.parent;
while (parent && parent.type !== "variable_declarator") {
parent = parent.parent;
}
if (parent) {
const nameNode = parent.childForFieldName("name");
if (nameNode) {
if (nameNode.type === "identifier") {
importMap.set(nameNode.text, currentSource);
} else if (nameNode.type === "object_pattern") {
const identifiers = [];
const extractIds = (n) => {
if (n.type === "identifier" || n.type === "shorthand_property_identifier") {
identifiers.push(n.text);
}
for (let i = 0; i < n.namedChildCount; i++) {
const child = n.namedChild(i);
if (child) extractIds(child);
}
};
extractIds(nameNode);
for (const id of identifiers) {
importMap.set(id, currentSource);
}
}
}
}
}
} else if (capture.name === "require_source") {
currentSource = capture.node.text.replace(/['"]/g, "");
let parent = capture.node.parent;
while (parent && parent.type !== "variable_declarator") {
parent = parent.parent;
}
if (parent) {
const nameNode = parent.childForFieldName("name");
if (nameNode && nameNode.type === "identifier") {
importMap.set(nameNode.text, currentSource);
}
}
}

@@ -425,9 +495,11 @@ }

const resolvedId = resolveImportToNode(importPath, filePath, aliases);
const targetNodeId = resolvedId || `import::${importPath}`;
const basePackage = getBasePackageName(importPath);
const targetNodeId = resolvedId || `import::${basePackage}`;
if (!graph.hasNode(targetNodeId)) {
const nearestPkgJson = findNearestPackageJson(path.dirname(filePath));
graph.addNode(targetNodeId, {
type: "file",
name: resolvedId ? path.basename(resolvedId) : importPath,
file: resolvedId || filePath,
startLine: resolvedId ? 0 : importLine,
name: resolvedId ? path.basename(resolvedId) : basePackage,
file: resolvedId || nearestPkgJson || filePath,
startLine: 0,
metadata: resolvedId ? {} : { external: true, callerFile: filePath, callerLine: importLine }

@@ -562,6 +634,14 @@ });

if (importSource) {
const baseImportSource = getBasePackageName(importSource);
const resolvedSource = resolveImportToNode(importSource, filePath, aliases) || importSource;
const importNodeId = resolveImportToNode(importSource, filePath, aliases) || `import::${importSource}`;
const importNodeId = resolveImportToNode(importSource, filePath, aliases) || `import::${baseImportSource}`;
if (!graph.hasNode(importNodeId)) {
graph.addNode(importNodeId, { type: "file", name: importSource, file: resolvedSource, startLine: 0, metadata: { external: true } });
const nearestPkgJson = findNearestPackageJson(path.dirname(filePath));
graph.addNode(importNodeId, {
type: "file",
name: resolvedSource ? path.basename(resolvedSource) : baseImportSource,
file: resolvedSource || nearestPkgJson || filePath,
startLine: 0,
metadata: { external: true }
});
}

@@ -591,2 +671,5 @@ if (!graph.hasEdge(importNodeId, targetId)) {

const baseName = objectName || calledName;
if (objectName && !importMap.has(objectName) && !localDefinitions.has(objectName)) {
continue;
}
if (BUILT_INS.has(baseName) && !localDefinitions.has(baseName) && !importMap.has(baseName)) continue;

@@ -629,9 +712,11 @@ const callerId = ensureScopeNode(node);

if (importSource) {
const baseImportSource = getBasePackageName(importSource);
const resolvedSource = resolveImportToNode(importSource, filePath, aliases) || importSource;
const importNodeId = resolveImportToNode(importSource, filePath, aliases) || `import::${importSource}`;
const importNodeId = resolveImportToNode(importSource, filePath, aliases) || `import::${baseImportSource}`;
if (!graph.hasNode(importNodeId)) {
const nearestPkgJson = findNearestPackageJson(path.dirname(filePath));
graph.addNode(importNodeId, {
type: "file",
name: importSource,
file: resolvedSource,
name: resolvedSource ? path.basename(resolvedSource) : baseImportSource,
file: resolvedSource || nearestPkgJson || filePath,
startLine: 0,

@@ -638,0 +723,0 @@ metadata: { external: true }

+284
-227
#!/usr/bin/env node
import {
analyzeGraph
} from "./chunk-JK3K5KL7.js";

@@ -440,176 +443,2 @@ // src/cli.ts

// src/core/analyze.ts
import louvain from "graphology-communities-louvain";
var louvainAlgorithm = louvain;
function isStructuralNoise(graph, nodeId) {
const data = graph.getNodeAttributes(nodeId);
if (data.type === "intent") return true;
if (nodeId.startsWith("import::")) return true;
if (nodeId.startsWith("unresolved_")) return true;
return false;
}
function findGodNodes(graph, topN = 10) {
const ranked = [];
graph.forEachNode((nodeId, data) => {
if (isStructuralNoise(graph, nodeId)) return;
ranked.push({
id: nodeId,
name: data.name,
type: data.type,
degree: graph.degree(nodeId)
});
});
ranked.sort((a, b) => b.degree - a.degree);
return ranked.slice(0, topN);
}
function detectCommunities(graph) {
if (graph.order === 0) return [];
let communityMap = {};
let seed = 123456789;
const rng = () => {
seed = (seed * 9301 + 49297) % 233280;
return seed / 233280;
};
try {
communityMap = louvainAlgorithm(graph, { rng });
} catch {
graph.forEachNode((nodeId) => {
communityMap[nodeId] = 0;
});
}
const groups = /* @__PURE__ */ new Map();
for (const [nodeId, cid] of Object.entries(communityMap)) {
if (!groups.has(cid)) groups.set(cid, []);
groups.get(cid).push(nodeId);
}
const communities = [];
for (const [cid, nodes] of groups.entries()) {
communities.push({
id: cid,
nodes,
cohesion: cohesionScore(graph, nodes)
});
}
communities.sort((a, b) => b.nodes.length - a.nodes.length);
return communities;
}
function cohesionScore(graph, communityNodes) {
const n = communityNodes.length;
if (n <= 1) return 1;
const nodeSet = new Set(communityNodes);
let internalEdges = 0;
graph.forEachEdge((_edgeId, _data, source, target) => {
if (nodeSet.has(source) && nodeSet.has(target)) {
internalEdges++;
}
});
const possible = n * (n - 1);
return possible > 0 ? Math.round(internalEdges / possible * 100) / 100 : 0;
}
function findSurprisingConnections(graph, communities, topN = 5) {
const nodeCommunity = /* @__PURE__ */ new Map();
for (const comm of communities) {
for (const nodeId of comm.nodes) {
nodeCommunity.set(nodeId, comm.id);
}
}
const candidates = [];
graph.forEachEdge((_edgeId, edgeData, source, target) => {
const srcComm = nodeCommunity.get(source);
const tgtComm = nodeCommunity.get(target);
if (srcComm === void 0 || tgtComm === void 0) return;
if (srcComm === tgtComm) return;
if (isStructuralNoise(graph, source) || isStructuralNoise(graph, target))
return;
if (edgeData.type === "imports" || edgeData.type === "defines") return;
const srcData = graph.getNodeAttributes(source);
const tgtData = graph.getNodeAttributes(target);
let score = 1;
const reasons = [];
reasons.push(`bridges community ${srcComm} \u2192 community ${tgtComm}`);
if (edgeData.type === "references" || edgeData.type === "extends" || edgeData.type === "implements") {
score += 2;
reasons.push(`${edgeData.type} relationship (deep OOP coupling)`);
}
const srcDeg = graph.degree(source);
const tgtDeg = graph.degree(target);
if (Math.min(srcDeg, tgtDeg) <= 2 && Math.max(srcDeg, tgtDeg) >= 5) {
score += 1;
const peripheral = srcDeg <= 2 ? srcData.name : tgtData.name;
const hub = srcDeg <= 2 ? tgtData.name : srcData.name;
reasons.push(
`peripheral node "${peripheral}" unexpectedly reaches hub "${hub}"`
);
}
if (edgeData.confidence === "AMBIGUOUS") {
score += 3;
reasons.push("ambiguous connection \u2014 needs verification");
} else if (edgeData.confidence === "INFERRED") {
score += 2;
reasons.push("inferred connection \u2014 not explicitly stated in source");
}
candidates.push({
source,
sourceName: srcData.name,
target,
targetName: tgtData.name,
edgeType: edgeData.type,
confidence: edgeData.confidence,
sourceCommunity: srcComm,
targetCommunity: tgtComm,
why: reasons.join("; "),
_score: score
});
});
candidates.sort((a, b) => b._score - a._score);
const seenPairs = /* @__PURE__ */ new Set();
const results = [];
for (const c of candidates) {
const pair = [c.sourceCommunity, c.targetCommunity].sort().join("-");
if (seenPairs.has(pair)) continue;
seenPairs.add(pair);
const { _score, ...connection } = c;
results.push(connection);
if (results.length >= topN) break;
}
return results;
}
function analyzeGraph(graph) {
const godNodes = findGodNodes(graph);
const communities = detectCommunities(graph);
const surprisingConnections = findSurprisingConnections(
graph,
communities
);
const knowledgeGaps = [];
graph.forEachNode((nodeId) => {
if (graph.degree(nodeId) <= 1 && !isStructuralNoise(graph, nodeId)) {
knowledgeGaps.push(nodeId);
}
});
const questions = [];
if (surprisingConnections.length > 0) {
questions.push("Why are these distinct communities connected via Surprising Connections?");
}
if (godNodes.length > 0) {
questions.push(`How would the system react if the core logic in '${godNodes[0]?.name}' was refactored?`);
}
if (knowledgeGaps.length > 5) {
questions.push("There are several isolated modules; are these dead code or missing integration tests?");
}
if (communities.length > 1) {
questions.push("Are the boundaries between these communities enforced, or is there hidden leakage?");
}
return {
godNodes,
communities,
surprisingConnections,
nodeCount: graph.order,
edgeCount: graph.size,
knowledgeGaps: knowledgeGaps.slice(0, 10),
// Limit to top 10
suggestedQuestions: questions
};
}
// src/core/alias.ts

@@ -745,10 +574,11 @@ import fs4 from "fs";

`;
md += `*Note: Only the first 100 files and their primary members are listed here. Use 'query' for full details.*
md += `*Note: Only the first 100 files and their primary members are listed here. For full data, use the \`search_graph\` or \`query_graph\` MCP tools (recommended) or corresponding CLI commands.*
`;
const fileNodes = graph.nodes().filter((n) => graph.getNodeAttribute(n, "type") === "file").slice(0, 100);
const allFileNodes = graph.nodes().filter((n) => graph.getNodeAttribute(n, "type") === "file");
const fileNodes = allFileNodes.slice(0, 100);
for (const file of fileNodes) {
const data = graph.getNodeAttributes(file);
if (data.metadata?.external) continue;
md += `- **${data.name}**
md += `- **${data.name}** [id: \`${file}\`]
`;

@@ -762,3 +592,3 @@ const definesEdges = graph.outEdges(file).filter((edgeId) => {

const targetData = graph.getNodeAttributes(target);
md += ` - \`${targetData.type} ${targetData.name}\`
md += ` - \`${targetData.type} ${targetData.name}\` [id: \`${target}\`]
`;

@@ -771,11 +601,22 @@ }

}
if (allFileNodes.length > 100) {
md += `
- *... and ${allFileNodes.length - 100} more file nodes. Use \`search_graph\` or \`query_graph\` MCP tools (recommended) to explore them.*
`;
}
if (analysis && analysis.godNodes.length > 0) {
const realNodesCount = graph.nodes().filter((n) => {
const type = graph.getNodeAttribute(n, "type");
return type !== "intent" && !n.startsWith("import::") && !n.startsWith("unresolved_");
}).length;
const title = realNodesCount > 10 ? "Top 10 God Nodes (Architectural Pillars)" : "God Nodes (Architectural Pillars)";
md += `
## God Nodes (Architectural Pillars)
## ${title}
`;
md += `These are the most-connected entities in the codebase. Changes to these nodes have the largest ripple effect.
md += `These are the most-connected entities in the codebase. Changes to these nodes have the largest ripple effect. For full data, use the \`god_nodes\` MCP tool (recommended) or \`geraph god\` CLI command.
`;
for (const god of analysis.godNodes) {
md += `- **${god.name}** (\`${god.type}\`, ${god.degree} connections)
const godsToShow = analysis.godNodes.slice(0, 10);
for (const god of godsToShow) {
md += `- **${god.name}** (type: \`${god.type}\`, id: \`${god.id}\`, ${god.degree} connections)
`;

@@ -788,23 +629,35 @@ }

`;
md += `The codebase clusters into ${analysis.communities.length} communities of related code.
md += `The codebase clusters into ${analysis.communities.length} communities of related code. For full community membership, use the \`get_community\` MCP tool (recommended) or \`geraph community <id>\` CLI command.
`;
for (const comm of analysis.communities) {
const topMembers = comm.nodes.filter((n) => !n.startsWith("commit::") && !n.startsWith("import::") && !n.startsWith("unresolved_")).slice(0, 5).map((n) => {
const realNodes = comm.nodes.filter(
(n) => !n.startsWith("commit::") && !n.startsWith("import::") && !n.startsWith("unresolved_") && graph.hasNode(n)
);
realNodes.sort((a, b) => graph.degree(b) - graph.degree(a));
const top5 = realNodes.slice(0, 5);
const memberStrings = top5.map((n) => {
const data = graph.getNodeAttributes(n);
return data ? data.name : n;
const name = data ? data.name : n;
return `**${name}** [id: \`${n}\`]`;
});
md += `- **Community ${comm.id}** (${comm.nodes.length} nodes, cohesion: ${comm.cohesion}) \u2014 ${topMembers.join(", ")}
`;
let communityLine = `- **Community ${comm.id}** (${comm.nodes.length} nodes, cohesion: ${comm.cohesion}) \u2014 ${memberStrings.join(", ")}`;
if (realNodes.length > 5) {
communityLine += `, and ${realNodes.length - 5} more.`;
}
md += communityLine + "\n";
}
}
if (analysis && analysis.surprisingConnections.length > 0) {
const totalSurprises = analysis.surprisingConnections.length;
const title = totalSurprises > 10 ? "Top 10 Surprising Connections" : "Surprising Connections";
md += `
## Surprising Connections
## ${title}
`;
md += `Non-obvious couplings that bridge different parts of the architecture.
md += `Non-obvious couplings that bridge different parts of the architecture. For full data, use the \`get_surprises\` MCP tool (recommended) or \`geraph surprises\` CLI command.
`;
for (const s of analysis.surprisingConnections) {
md += `- **${s.sourceName}** \u2194 **${s.targetName}** (\`${s.edgeType}\`): ${s.why}
const surprisesToShow = analysis.surprisingConnections.slice(0, 10);
for (const s of surprisesToShow) {
md += `- **${s.sourceName}** [id: \`${s.source}\`] \u2194 **${s.targetName}** [id: \`${s.target}\`] (\`${s.edgeType}\`): ${s.why}
`;

@@ -1167,13 +1020,15 @@ }

This project uses a geraph knowledge graph.
This project uses Geraph knowledge graph to track codebase relationships.
**CRITICAL:** Before answering any architectural, structural, or dependency questions, you MUST read the comprehensive Geraph Skill.
Rules:
- If running any \`geraph\` command returns 'command not found', you MUST install it globally using \`npm install -g geraph\` before proceeding.
- Before answering architecture or codebase questions, read \`.geraph/GRAPH_REPORT.md\` for god nodes and community structure.
- NEVER try to read the massive \`.geraph/graph.json\` file directly. It is for internal CLI use only.
- For discovery, use \`geraph search '<term>' [--type <type>]\`.
- For precise architecture navigation, use \`geraph query '<symbol>' [--type <type>] [--source <file>]\` instead of falling back to \`grep\`.
- After modifying code files in this session, run \`geraph scan\` to keep the graph current.
- NEVER use \`grep\`, \`rg\`, \`find\`, or any text-search tool for architecture questions when geraph is available. Always use \`geraph search\` or \`geraph query\` instead.
- **NOTE**: There are NO extra or hidden options for these commands. DO NOT hallucinate flags. Only use the options explicitly mentioned here.
- Before answering, read \`.geraph/GRAPH_REPORT.md\` (or read the \`geraph://report\` MCP resource) for a high-level overview.
- NEVER try to read the massive \`.geraph/graph.json\` or the \`.geraph/graph.html\` files directly. They will overflow your context window and crash your session.
- NEVER read any files inside the \`.geraph/cache\` directory (like \`git-cache.json\`) directly as they contain massive raw blame indexes that will overflow your context.
- Prioritize using the native Geraph MCP tools and resources (if available) over terminal CLI commands.
- **MCP Resources:** Access high-level statistics, reports, and god nodes directly via read-only resources: \`geraph://report\`, \`geraph://stats\`, \`geraph://god-nodes\`, \`geraph://surprises\`, and \`geraph://audit\`.
- After modifying files or pushing a Git commit, you MUST run the \`scan_graph\` MCP tool or the \`geraph scan\` CLI command to sync the graph.
- NEVER use \`grep\`, \`rg\`, or \`find\` for codebase architecture exploration when Geraph is available.

@@ -1184,19 +1039,16 @@ [GLOBAL_POINTER]

For any question about this repo's architecture, structure, components, or how to add/modify/find code, your **first action must be** to read \`.geraph/GRAPH_REPORT.md\` (if it exists).
For any question about this repo's architecture, components, or dependency structure, your **first action** must be to consult Geraph.
Triggers: "how do I...", "where is...", "what does ... do", "add/modify a <component>", "explain the architecture", or anything that depends on how files or classes relate.
**CRITICAL:** Before answering, you MUST read the comprehensive Geraph Skill.
After reading the report, answer using the graph context. Only read source files when (a) modifying/debugging specific code, (b) the graph lacks the needed detail, or (c) the graph is missing or stale.
Rules:
- If running any \`geraph\` command returns 'command not found', you MUST install it globally using \`npm install -g geraph\` before proceeding.
- Before answering, read \`.geraph/GRAPH_REPORT.md\` (or read the \`geraph://report\` MCP resource) for a high-level overview.
- NEVER try to read the massive \`.geraph/graph.json\` or the \`.geraph/graph.html\` files directly. They will overflow your context window.
- NEVER read any files inside the \`.geraph/cache\` directory (like \`git-cache.json\`) directly as they contain massive raw blame indexes that will overflow your context.
- Prioritize using the native Geraph MCP tools and resources (if available) over terminal CLI commands.
- **MCP Resources:** Access high-level statistics, reports, and god nodes directly via read-only resources: \`geraph://report\`, \`geraph://stats\`, \`geraph://god-nodes\`, \`geraph://surprises\`, and \`geraph://audit\`.
- After modifying files or pushing a Git commit, you MUST run the \`scan_graph\` MCP tool or the \`geraph scan\` CLI command to sync the graph.
- NEVER use \`grep\`, \`rg\`, or \`find\` for codebase architecture exploration when Geraph is available.
If running any \`geraph\` command returns a 'command not found' error, you MUST install it globally using \`npm install -g geraph\` before proceeding.
NEVER attempt to read the massive \`.geraph/graph.json\` file directly. Use the CLI instead:
- **Discover nodes**: \`geraph search '<term>' [--type <type>]\`
- **Trace dependencies**: \`geraph query '<symbol>' [--type <type>] [--source <file>]\`
- **Update graph**: \`geraph scan\`
NEVER use \`grep\`, \`rg\`, \`find\`, or any text-search tool for architecture questions when geraph is available. Always use \`geraph search\` or \`geraph query\` instead.
**NOTE**: There are NO extra or hidden options for these commands. DO NOT hallucinate flags. Only use the options explicitly mentioned here.
[GLOBAL_POINTER]

@@ -1447,3 +1299,3 @@ `;

var program = new Command();
program.name("geraph").description(chalk4.blue("Geraph: Structural memory for AI agents")).version("0.3.0", "-v, --version", "output the current version");
program.name("geraph").description(chalk4.blue("Geraph: Structural memory for AI agents")).version("0.4.0", "-v, --version", "output the current version");
program.command("scan").description("Scan the current directory and build the Knowledge Graph").action(async () => {

@@ -1617,3 +1469,3 @@ const targetDir = process.cwd();

"Filter results by node type (e.g., 'interface', 'class', 'function', 'file')"
).action(async (term, options) => {
).option("-p, --page <number>", "Page number for pagination", "1").option("-l, --limit <number>", "Number of results per page", "20").action(async (term, options) => {
const spinner = ora({

@@ -1625,7 +1477,17 @@ text: chalk4.gray(`Searching graph for: ${term}...`),

try {
const { searchGraph } = await import("./query-ALVJVDRK.js");
const results = await searchGraph(process.cwd(), term, options.type);
const { searchGraph } = await import("./query-FGSLUNHR.js");
const results = await searchGraph(
process.cwd(),
term,
options.type,
Number(options.page),
Number(options.limit)
);
spinner.stop();
if (results.length === 0) {
console.error(chalk4.yellow(`No nodes found matching '${term}'`));
if (results.data.length === 0) {
console.error(
chalk4.yellow(
`No nodes found matching '${term}' on page ${options.page}`
)
);
} else {

@@ -1636,3 +1498,3 @@ console.log(JSON.stringify(results, null, 2));

`
Found ${results.length} nodes. Use 'geraph query <id>' to inspect a specific node.`
Found ${results.meta.total} nodes across ${results.meta.totalPages} pages. Displaying page ${results.meta.page}.`
)

@@ -1644,3 +1506,3 @@ );

console.error(
chalk4.red("?? Search failed:"),
chalk4.red("\u274C Search failed:"),
error instanceof Error ? error.message : String(error)

@@ -1651,5 +1513,3 @@ );

});
program.command("query <symbol>").description(
"Query the knowledge graph for a specific symbol's relationships"
).option(
program.command("node <symbol>").description("Fetch detailed metadata for a specific node").option(
"-t, --type <type>",

@@ -1662,3 +1522,3 @@ "Filter results by node type (e.g., 'interface', 'class', 'function', 'file')"

const spinner = ora({
text: chalk4.gray(`Querying relationships for: ${symbol}...`),
text: chalk4.gray(`Querying node: ${symbol}...`),
color: "blue",

@@ -1668,4 +1528,4 @@ spinner: "dots"

try {
const { queryGraph } = await import("./query-ALVJVDRK.js");
const result = await queryGraph(
const { getNode } = await import("./query-FGSLUNHR.js");
const result = await getNode(
process.cwd(),

@@ -1687,4 +1547,201 @@ symbol,

});
program.command("neighbors <symbol>").description("Fetch all incoming and outgoing edges for a specific node").option(
"-t, --type <type>",
"Filter results by node type (e.g., 'interface', 'class', 'function', 'file')"
).option(
"-s, --source <path>",
"Filter results by source file path (e.g., 'auth.ts')"
).option("-p, --page <number>", "Page number for pagination", "1").option("-l, --limit <number>", "Number of edges per page", "20").action(async (symbol, options) => {
const spinner = ora({
text: chalk4.gray(`Querying neighbors for: ${symbol}...`),
color: "blue",
spinner: "dots"
}).start();
try {
const { getNeighbors } = await import("./query-FGSLUNHR.js");
const result = await getNeighbors(
process.cwd(),
symbol,
options.type,
options.source,
Number(options.page),
Number(options.limit)
);
spinner.stop();
console.log(JSON.stringify(result, null, 2));
} catch (error) {
spinner.fail(
chalk4.red(
`Query failed: ${error instanceof Error ? error.message : String(error)}`
)
);
process.exit(1);
}
});
program.command("path <source> <target>").description(
"Find the shortest path between two nodes using fuzzy symbol/ID lookup"
).option("-m, --max-hops <number>", "Maximum hops to consider", "8").action(async (source, target, options) => {
const spinner = ora({
text: chalk4.gray(`Finding shortest path...`),
color: "blue",
spinner: "dots"
}).start();
try {
const { shortestPath } = await import("./query-FGSLUNHR.js");
const maxHops = Number(options.maxHops || 8);
const result = await shortestPath(process.cwd(), source, target, maxHops);
spinner.stop();
console.log(result);
} catch (error) {
spinner.fail(
chalk4.red(
`Path failed: ${error instanceof Error ? error.message : String(error)}`
)
);
process.exit(1);
}
});
program.command("god").description(
"Return the most connected nodes \u2014 the core architectural pillars of the codebase"
).option("-p, --page <number>", "Page number for pagination", "1").option("-l, --limit <number>", "Number of results per page", "10").action(async (options) => {
const spinner = ora({
text: chalk4.gray(`Fetching core nodes...`),
color: "blue",
spinner: "dots"
}).start();
try {
const { getGodNodes } = await import("./query-FGSLUNHR.js");
const result = await getGodNodes(
process.cwd(),
Number(options.page),
Number(options.limit)
);
spinner.stop();
console.log(result);
} catch (error) {
spinner.fail(
chalk4.red(
`Failed to fetch god nodes: ${error instanceof Error ? error.message : String(error)}`
)
);
process.exit(1);
}
});
program.command("community <id>").description("Get all nodes in a community by community ID").option("-p, --page <number>", "Page number for pagination", "1").option("-l, --limit <number>", "Number of results per page", "20").action(async (id, options) => {
const spinner = ora({
text: chalk4.gray(`Fetching nodes in community ${id}...`),
color: "blue",
spinner: "dots"
}).start();
try {
const { getCommunityNodes } = await import("./query-FGSLUNHR.js");
const result = await getCommunityNodes(
process.cwd(),
Number(id),
Number(options.page),
Number(options.limit)
);
spinner.stop();
console.log(result);
} catch (error) {
spinner.fail(
chalk4.red(
`Failed to fetch community: ${error instanceof Error ? error.message : String(error)}`
)
);
process.exit(1);
}
});
program.command("surprises").description(
"Discover surprising cross-community couplings that link otherwise independent modules"
).option("-p, --page <number>", "Page number for pagination", "1").option("-l, --limit <number>", "Number of results per page", "20").action(async (options) => {
const spinner = ora({
text: chalk4.gray(`Querying surprising connections...`),
color: "blue",
spinner: "dots"
}).start();
try {
const { getSurprisingConnections } = await import("./query-FGSLUNHR.js");
const result = await getSurprisingConnections(
process.cwd(),
Number(options.page),
Number(options.limit)
);
spinner.stop();
console.log(result);
} catch (error) {
spinner.fail(
chalk4.red(
`Failed to fetch surprises: ${error instanceof Error ? error.message : String(error)}`
)
);
process.exit(1);
}
});
program.command("query <symbol-or-question>").description(
"Search the AST graph using BFS or DFS traversal with keyword or natural language support, returning a compact context representation"
).option("-m, --mode <mode>", "Traversal mode (bfs or dfs)", "bfs").option("-d, --depth <number>", "Traversal depth limit", "3").option("-b, --budget <number>", "Estimated output token limit", "2000").action(async (symbolOrQuestion, options) => {
const spinner = ora({
text: chalk4.gray(`Traversing AST graph from ${symbolOrQuestion}...`),
color: "blue",
spinner: "dots"
}).start();
try {
const { queryGraph } = await import("./query-FGSLUNHR.js");
const result = await queryGraph(
process.cwd(),
symbolOrQuestion,
options.mode,
Number(options.depth),
Number(options.budget)
);
spinner.stop();
console.log(result);
} catch (error) {
spinner.fail(
chalk4.red(
`Query failed: ${error instanceof Error ? error.message : String(error)}`
)
);
process.exit(1);
}
});
program.command("stats").description(
"Return summary statistics of the graph: node count, edge count, community count, and extraction confidence percentage breakdown"
).action(async () => {
const spinner = ora({
text: chalk4.gray(`Querying graph statistics...`),
color: "blue",
spinner: "dots"
}).start();
try {
const { getGraphStats } = await import("./query-FGSLUNHR.js");
const result = await getGraphStats(process.cwd());
spinner.stop();
console.log(result);
} catch (error) {
spinner.fail(
chalk4.red(
`Failed to fetch stats: ${error instanceof Error ? error.message : String(error)}`
)
);
process.exit(1);
}
});
program.command("mcp [dir]").description(
"Starts the JSON-RPC Model Context Protocol (MCP) server over stdio"
).action(async (dir) => {
try {
const { loadGraph } = await import("./query-FGSLUNHR.js");
const targetDir = dir ? path7.resolve(process.cwd(), dir) : process.cwd();
const graph = loadGraph(targetDir);
const { runMcpServer } = await import("./mcp-HCUAGMFP.js");
await runMcpServer(graph, targetDir);
} catch (err) {
console.error(err instanceof Error ? err.message : String(err));
process.exit(1);
}
});
// src/index.ts
program.parse(process.argv);
---
name: geraph
description: "Structural Memory for AI Agents. Navigate the codebase via AST graph."
description: "Structural Memory for AI Agents. Navigate and query the codebase via AST-based semantic knowledge graph."
trigger: /geraph

@@ -9,110 +9,244 @@ ---

Geraph is a structural memory engine that tracks dependencies, function calls, imports, and historical context (Git commits) across the codebase. It builds a graph mapping all relationships without executing any code.
Geraph is a structural memory engine that tracks codebase abstractions, function calls, module imports, and historical context (Git commits). It constructs a semantic knowledge graph mapping all dependencies without executing any code.
## What You Must Do When Invoked
As an AI agent, you MUST follow this operational manual to navigate the codebase using Geraph.
When you need to understand the architecture, find where a component is, or trace dependencies, follow this strict protocol:
---
### Step 1: Read the Global Architecture Report
Before answering architecture or codebase questions, **always read `.geraph/GRAPH_REPORT.md` first**. It contains project stats, core architectural pillars ("God Nodes"), and community clustering.
## 1. Primary Directives & Workflow
### Step 2: Use `search` for Fuzzy Discovery
If you don't know the exact ID or exact name of a symbol, you MUST use `search` first.
```bash
geraph search '<term>' [--type <type>]
```
- **Inputs**: Use a broad concept, a partial filename, or a partial function name. For example: `search 'auth'`, `search 'database'`, `search 'User'`.
- **Options**: You can filter by `--type file` or `--type function` if you know what you are looking for.
- **Output**: Returns a lightweight list of matching Node IDs, sorted by significance.
When you need to understand the architecture, locate code definitions, trace dependencies, or perform impact analysis, execute these steps:
### Step 3: Use `query` for Deep Inspection
Once you have found the exact Node ID or exact Symbol Name from Step 2 (or if the user explicitly provided one), use `query` to fetch its full dependencies.
```bash
geraph query '<symbol_or_id>' [--type <type>] [--source <file>]
```
- **Inputs**: The exact Node ID (e.g., `src/auth/session.ts::ValidateToken`) or the exact symbol name (e.g., `ValidateToken`). If you are exploring a specific file, you can query its filepath directly (e.g., `geraph query 'src/app.ts' --type file`).
- **Options**:
- `--type <type>` (e.g., `file`, `function`, `class`, `interface`, `intent`). Use this to resolve naming conflicts (e.g., if there is a function and an interface both named "User").
- `--source <file>` (e.g., `src/auth/session.ts`). Use this if you only know the symbol name, to ensure Geraph finds the definition in the correct file.
- **Why use options?**: Always use `--type` and `--source` if you know them. They strictly reduce token bloat and guarantee you get the exact node you want.
### Step 0: MCP First, CLI as Fallback
* **MCP Server (Recommended):** If the Geraph MCP server is active, always prioritize Geraph MCP tools and resources over terminal commands.
* **CLI Wrapper:** If MCP is not available, execute the equivalent terminal command: `geraph <command>`.
* **Global Install Rule:** If running any Geraph CLI command returns a 'command not found' error, you MUST first install it globally using:
```bash
npm install -g geraph
```
* **Standardized Terminal Bug Fallback:** If the Geraph CLI commands continuously fail to run (e.g. CLI crashes, execution environments fail — **not** "symbol not found" errors):
1. Immediately check if Geraph MCP is configured. If it is, switch entirely to MCP tools.
2. If Geraph MCP is **not** configured, immediately stop your work and ask the user to set up the MCP server by following the official documentation at: [https://github.com/rupam2232/geraph#mcp-server-recommended](https://github.com/rupam2232/geraph#mcp-server-recommended)
3. **CRITICAL:** Do NOT fallback to default general search tools like `grep`, `rg`, or `find`. Halt execution until MCP is set up.
### Step 4: Trace the Context ("The Why")
When you run `query`, you will see `incoming` and `outgoing` edges.
- **Impact Analysis**: Use `incoming` edges to see who depends on this node.
- **Dependencies**: Use `outgoing` edges to see what this node uses.
- **Historical Context ("The Why")**: If you see an `intent` type node connected to your target, **query that `intent` node**. Look at its `metadata.message`. This is a Git commit message that explains *why* the code exists or was changed.
### Step 1: Read the Global Architecture Report / Load Resources
Before answering architecture or codebase questions, **always load the Geraph Report first**. It provides a compact structural overview, including the top files, communities, god nodes, surprising couplings, and temporal commit history.
* **Via MCP:** Read the `geraph://report` resource.
* **Via CLI:** Read the `.geraph/GRAPH_REPORT.md` file directly.
### Step 5: Keep the Graph Current
Geraph must stay in sync with the codebase.
- You MUST run `geraph scan` after you modify any code files.
- You MUST run `geraph scan` after committing a change to the repository.
### Critical Rules
> [!IMPORTANT]
> **NEVER Read Raw Graph Files:** You MUST never attempt to view or read the massive `.geraph/graph.json` or `.geraph/graph.html` files directly. Doing so will instantly overflow your context window, bloat token consumption, and freeze or crash your session. Use Geraph's tools or resources to inspect the graph.
> [!IMPORTANT]
> **NEVER Read Cache Files:** You MUST never try to read any files inside the `.geraph/cache` directory (such as `git-cache.json`). These files store massive raw metadata indexes (e.g. Git blame history) that will instantly blow up your context window, waste millions of tokens, and crash your session.
> [!IMPORTANT]
> **Scan on Modification:** Immediately after making any code changes, creating/deleting files, or committing code, you MUST run the `scan_graph` MCP tool or the `geraph scan` CLI command to rebuild the AST graph and sync Geraph's memory with the active state of the code.
---
## CRITICAL RULES
## 2. Fuzzy Search & Node ID Resolution Mechanics
1. **NEVER read `.geraph/graph.json` directly.** It is massive and will overflow your context window. Always use the CLI.
2. **NO EXTRA FLAGS**: There are NO extra or hidden options for `search` or `query`. DO NOT hallucinate flags like `--limit` or `--dfs`. Only use `--type` and `--source`.
3. **COMMAND NOT FOUND**: If the terminal returns a 'command not found' error, you MUST install it globally using `npm install -g geraph` before proceeding.
4. **TERMINAL OUTPUT BUG**: If your terminal fails to capture stdout (e.g., it hangs or returns "No output"), DO NOT GIVE UP. Redirect the output to a file: `geraph query 'Symbol' > .geraph/out.json`, then read that file.
5. **NEVER use `grep`, `rg`, or `find`** for architecture questions when Geraph is available.
To choose the correct search terms and resolve nodes successfully, you must understand how Geraph names nodes and resolves fuzzy inputs:
### A. Deterministic Node ID Format
Every node in Geraph has a unique, deterministic ID generated as follows:
* **For a File Node:** The normalized absolute or relative workspace path to the file.
- *Example:* `services/billing/invoicing.ts` (or `E:\coding\project\services\billing\invoicing.ts`)
* **For a Symbol/Entity Node (class, function, interface, etc.):** The containing file path followed by `::` and the symbol name.
- *Format:* `{containing_file_path}::{symbolName}`
- *Example:* `services/billing/invoicing.ts::InvoiceProcessor`
### B. Fuzzy Resolution Protocol
When you pass a search string to `shortest_path` (or `geraph path`), `get_node` (or `geraph node`), `get_neighbors` (or `geraph neighbors`), or `query_graph` (or `geraph query`), Geraph resolves it using a multi-tiered fuzzy search:
1. **Exact ID Match:** Matches the exact qualified ID first (e.g. `services/billing/invoicing.ts::InvoiceProcessor`).
2. **Exact Symbol Attribute Match:** Matches the raw name attribute exactly (e.g. matching `InvoiceProcessor` to `attr.name === "InvoiceProcessor"`).
3. **Suffix Matching:** Resolves inputs by checking if the Node ID ends with `/{symbol}` (for files) or `::{symbol}` (for members), e.g. querying `invoicing.ts::InvoiceProcessor` or `invoicing.ts/InvoiceProcessor` will resolve successfully.
4. **Case-Insensitive Match:** Repeats all matching tiers case-insensitively (e.g. matching `invoiceprocessor` to `InvoiceProcessor`).
5. **Optional Filters:** In all matching phases, Geraph optionally filters results by AST node `type` (e.g., `function`) and/or containing `source` file path segment if those parameters are explicitly provided.
### C. Geraph MCP Resource Pointers
If MCP is active, you can load these read-only URIs directly as resources to quickly get high-level overviews without running tools:
* `geraph://report` : The complete `.geraph/GRAPH_REPORT.md` file.
* `geraph://stats` : General stats (node/edge/community counts and confidence breakdown).
* `geraph://god-nodes` : Top 10 most-connected core abstractions.
* `geraph://surprises` : Top 10 surprising cross-community couplings.
* `geraph://audit` : Extraction confidence audit counts.
---
## JSON Response Interpretation
## 3. Navigating with Pagination
**`search` output**: Returns an array of matching node objects.
- `id`: The unique node identifier. Use this exact string for subsequent `query` calls.
- `name`: The human-readable name.
- `type`: The node type (see Glossary below).
- `file`: The source file.
- `links`: Total connections. Higher means more architecturally significant.
To avoid context-window bloat, Geraph enforces strict pagination on all list-based queries. Every paginated tool/command accepts **two optional parameters**:
* `page`: The page number to retrieve (Default: `1`).
* `limit`: The number of items to show per page (Default: `20` for neighbors/searches, `10` for god nodes).
**`query` output**: Returns a detailed object with `target`, `incoming`, and `outgoing`:
- `target`: The queried node's full details:
- `id`, `name`, `type`, `file`, `line`: Identity and location.
- `metadata.doc`: Contains extracted JSDoc/comments. Read this to understand the purpose and intent of the symbol.
- `metadata.deprecated`: Boolean flag. If `true`, this symbol is marked `@deprecated`.
- `metadata.message`: (*Only on `intent` type nodes*) The Git commit message explaining why this node was created or changed.
- `metadata.author`, `metadata.date`: (*Only on `intent` type nodes*) Commit author and timestamp.
- `links.incoming` / `links.outgoing`: Count of connections in each direction.
- `incoming`: Array of edges pointing **to** this node. Each entry has `source` (the neighbor node), `relation` (edge type), and `confidence`. Use this for **Impact Analysis** — these are the entities that depend on and will break if you change the target.
- `outgoing`: Array of edges pointing **out** from this node. Each entry has `target` (the neighbor node), `relation`, and `confidence`. Use this to see what the node **depends on** — what it calls, imports, or references.
### Understanding the Pagination Payload
When a query returns, inspect the metadata to determine if you need to fetch more pages:
### Query Resolution Priority
When you `query` a symbol name (e.g., `geraph query 'userState'`), Geraph resolves it in this strict order:
1. **Exact ID Match**: Perfect match on the unique Node ID.
2. **Case-Sensitive Match**: Matches the exact capitalization (finds the variable `userState` but ignores the interface `UserState`).
3. **Case-Insensitive Fallback**: If no exact case match exists, it returns the case-insensitive match (returns the interface `UserState`).
#### For JSON Responses (e.g., `search_graph` or `get_neighbors` MCP/CLI):
Look at the `meta` block:
```json
"meta": {
"page": 1,
"limit": 20,
"total": 97,
"totalPages": 5
}
```
* **`page`:** The current page you are viewing.
* **`totalPages`:** The total pages available. If `page < totalPages`, you must make subsequent queries with `page: page + 1` to read the remaining data.
#### For Formatted Text Responses (e.g. `god_nodes`, `get_community`, `get_surprises`):
Inspect the bracketed line at the very bottom:
`[Page 1 of 5 | Total: 97 nodes]`
* If the page count is greater than 1, you must increment the `page` argument in your next tool call to view the next chunk of nodes.
---
## Geraph Glossary
## 4. Tool & CLI Command Reference
Use this glossary to understand the types of nodes and edges in the graph, and to accurately choose your `--type` flag for queries.
All query parameters are optional and have robust defaults. If you omit an argument, the server resolves it automatically.
### Node Types
| Type | Description |
|---|---|
| `file` | A source code file. |
| `function` | A standard function, method, or arrow function. |
| `class` | A class definition. |
| `interface`/`type`/`enum`| TypeScript type definitions. |
| `intent` | A Git commit explaining why a node exists. |
### A. Core Discovery & Abstractions
### Edge Types (`relation`)
| Relation | Description |
|---|---|
| `imports` | File A depends on File B. |
| `calls` | Function A executes Function B. |
| `defines` | A file contains a function/class. |
| `references` | A function uses a specific type or interface. |
| `explains` | A Git commit (`intent` node) provides historical context for a specific code node. |
#### 1. Fuzzy Search
Search for nodes by partial symbol name or file path.
* **MCP Tool:** `search_graph`
- *Parameters:* `name` (Required), `type` (Optional), `page` (Optional), `limit` (Optional)
* **CLI Command:** `geraph search <term>`
- *Syntax:* `geraph search <term> [--type <type>] [--page <number>] [--limit <number>]`
- *Example:* `geraph search Invoice --type class --page 1 --limit 10`
* **Returned Fields & Meaning:**
- `id`: The unique absolute node ID (use this for subsequent deep inspection calls).
- `name`: Raw symbol name.
- `type`: Node category (e.g., `class`, `function`).
- `file`: Containing file path.
- `links`: Connection count (degree). High degree = high architectural importance.
#### 2. God Nodes
Find the most connected real nodes in the codebase (excluding noise).
* **MCP Tool:** `god_nodes`
- *Parameters:* `page` (Optional), `limit` (Optional, Default: 10)
* **CLI Command:** `geraph god`
- *Syntax:* `geraph god [--page <number>] [--limit <number>]`
* **Output Format:**
` {index}. {symbol_name} [id: {node_id}] - {degree} edges`
#### 3. Community Nodes
Fetch all nodes clustered within a specific Louvain community ID.
* **MCP Tool:** `get_community`
- *Parameters:* `community_id` (Required), `page` (Optional), `limit` (Optional, Default: 20)
* **CLI Command:** `geraph community <id>`
- *Syntax:* `geraph community <id> [--page <number>] [--limit <number>]`
* **Output Format:**
` {symbol_name} [{containing_file_path}]`
#### 4. Surprising Connections
Fetch surprising cross-community couplings that link independent subsystems.
* **MCP Tool:** `get_surprises`
- *Parameters:* `page` (Optional), `limit` (Optional, Default: 20)
* **CLI Command:** `geraph surprises`
- *Syntax:* `geraph surprises [--page <number>] [--limit <number>]`
* **Output Format:**
` {source_name} <-> {target_name} [{edge_type}] - {explanation}`
---
### B. Deep Inspection & Traversal
#### 5. Get Node Detail
Fetch metadata for a single specific symbol or file path.
* **MCP Tool:** `get_node`
- *Parameters:* `symbol` (Required), `type` (Optional), `source` (Optional)
* **CLI Command:** `geraph node <symbol>`
- *Syntax:* `geraph node <symbol> [--type <type>] [--source <path>]`
- *Example:* `geraph node InvoiceProcessor --type class --source invoicing.ts`
* **Returned Fields & Meaning:**
- `id`: Unique absolute identifier.
- `name`: Symbol name.
- `type`: Node category.
- `file`: Containing file path.
- `line`: Starting line number in the source file.
- `links.incoming` / `links.outgoing`: Direct dependency counts.
- `metadata.doc`: Extracted JSDoc/comments (contains design rationale).
- `metadata.community`: Louvain community ID (cluster subsystem).
#### 6. Get Neighbors
Trace all incoming and outgoing dependencies of a symbol.
* **MCP Tool:** `get_neighbors`
- *Parameters:* `symbol` (Required), `type` (Optional), `source` (Optional), `page` (Optional), `limit` (Optional, Default: 20)
* **CLI Command:** `geraph neighbors <symbol>`
- *Syntax:* `geraph neighbors <symbol> [--type <type>] [--source <path>] [--page <number>] [--limit <number>]`
* **Returned Fields & Meaning:**
- `incoming[]`: Symbols that call, import, or extend this target. Use this for **Impact Analysis** (what will break if you modify this node).
- `outgoing[]`: Symbols this target calls, imports, or references. Use this to trace **dependency requirements**.
- `relation`: Nature of connection (e.g. `calls`, `imports`, `defines`).
- `confidence`: Confidence levels (`EXTRACTED` = 100% AST certain; `INFERRED` = high structural heuristic probability; `AMBIGUOUS` = requires review).
#### 7. Shortest Path
Find the shortest chain of code relationships linking two nodes.
* **MCP Tool:** `shortest_path`
- *Parameters:* `source` (Required), `target` (Required), `max_hops` (Optional, Default: 8)
* **CLI Command:** `geraph path <source> <target>`
- *Syntax:* `geraph path <source> <target> [--max-hops <number>]`
- *Example:* `geraph path InvoiceProcessor DatabaseClient --max-hops 5`
* **Output Format:** Shows hops and directions between nodes:
`Shortest path (H hops): SourceSymbol --imports--> Middleware <--calls-- DestinationSymbol`
#### 8. Compact Graph Traversal
Runs a localized BFS/DFS crawl fanning out from the most relevant seed nodes to return a compact context map. It supports fuzzy symbol names, lists of keywords, or full natural language questions.
* **MCP Tool:** `query_graph`
- *Parameters:* `symbol` or `question` (Required), `mode` (Optional, Default: 'bfs'), `depth` (Optional, Default: 3), `token_budget` (Optional, Default: 2000)
* **CLI Command:** `geraph query <symbol-or-question>`
- *Syntax:* `geraph query <symbol-or-question> [--mode <bfs|dfs>] [--depth <number>] [--budget <number>]`
- *Example:* `geraph query "how does InvoiceProcessor write to the database" --depth 2 --budget 1500`
* **Output Format:** Compact text map listing traversed nodes (`NODE`) and edges (`EDGE`):
`NODE server.js [src=packages/services/server.js loc=0 community=2]`
`EDGE server.js --imports--> invoicing.py`
#### 9. Graph Statistics
Get summary statistics of the graph (node/edge/community counts and confidence breakdown).
* **MCP Tool:** `graph_stats`
- *Parameters:* None
* **CLI Command:** `geraph stats`
- *Syntax:* `geraph stats`
* **Output Format:**
`Nodes: 256`
`Edges: 798`
`Communities: 9`
`EXTRACTED: 80%`
`INFERRED: 15%`
`AMBIGUOUS: 5%`
#### 10. Rebuild Graph
Triggers a full scan of the directory to rebuild the knowledge graph.
* **MCP Tool:** `scan_graph`
- *Parameters:* None
* **CLI Command:** `geraph scan`
- *Syntax:* `geraph scan`
---
## 5. Geraph Glossary
### AST Node Types
* `file`: A source code file.
* `function`: A function, method, or arrow function definition.
* `class`: A class definition.
* `interface` / `type` / `enum`: TypeScript definition declarations.
* `intent`: A Git commit explaining why a node exists (query its `metadata.message` for history).
### AST Edge Types (`relation`)
* `imports`: File A depends on File B.
* `calls`: Function/Method A invokes Function/Method B.
* `defines`: File A contains or defines symbol B.
* `references`: A symbol uses a type, interface, or variable.
* `explains`: A Git commit (`intent` node) provides historical context for a specific code node.
### Confidence Scores
Every edge has a `confidence` level:
| Confidence | Description |
|---|---|
| `EXTRACTED` | 100% deterministic. Found directly by the AST parser (e.g., an explicit function call). |
| `INFERRED` | High probability. Deduced via structural heuristics or indirect relationships. |
| `AMBIGUOUS` | Uncertain connection. Requires human/agent verification. |
* `EXTRACTED`: 100% deterministic AST parser extraction (e.g. direct function call or explicit import statement).
* `INFERRED`: High probability structural heuristic coupling.
* `AMBIGUOUS`: Uncertain connection that needs manual verification.
{
"name": "geraph",
"version": "0.3.0",
"version": "0.4.0",
"description": "Structural memory for AI agents. Build semantic knowledge graphs from your codebase for surgical code modifications.",

@@ -24,2 +24,10 @@ "main": "dist/index.js",

"cli",
"agentic-coding",
"tree-sitter",
"skills",
"cursor",
"claude",
"ai-agents",
"semantic-knowledge-graph",
"structural-memory",
"agentic-coding"

@@ -52,2 +60,3 @@ ],

"dependencies": {
"@modelcontextprotocol/sdk": "^1.29.0",
"chalk": "^4.1.2",

@@ -59,2 +68,3 @@ "commander": "^4.1.1",

"graphology-metrics": "^2.4.0",
"graphology-shortest-path": "^2.1.0",
"ignore": "^5.3.2",

@@ -61,0 +71,0 @@ "ora": "^9.4.0",

@@ -32,4 +32,53 @@ # Geraph

## MCP Server (Recommended)
Geraph features a fully local Model Context Protocol (MCP) server that operates completely over `stdio`. **Using the MCP server is highly recommended** over running terminal CLI commands for LLMs.
**For a project-level local setup:**
Add the following configuration to your MCP-compatible client (e.g. Cursor or Antigravity IDE):
```json
{
"mcpServers": {
"geraph": {
"command": "geraph",
"args": ["mcp"]
}
}
}
```
**For a global setup:**
If you configure the MCP server globally for your IDE, you must tell the server where your project is located. You can do this by setting the `cwd` field to your project path. If your IDE/platform doesn't support the `cwd` field, you can pass the project path as an argument instead:
```json
{
"mcpServers": {
"geraph": {
"command": "geraph",
"args": [
"mcp"
],
"cwd": "<path-to-your-project>"
}
}
}
```
If cwd is not supported:
```json
{
"mcpServers": {
"geraph": {
"command": "geraph",
"args": [
"mcp",
"<path-to-your-project>"
]
}
}
}
```
## Detailed Documentation
For a full guide on workflows, agent integration, and advanced features, visit the **[Main Project Documentation](https://github.com/rupam2232/geraph)**.
// src/core/query.ts
import fs from "fs";
import path from "path";
import { MultiDirectedGraph } from "graphology";
function normalizeId(id) {
return id.replace(/\\/g, "/");
}
function loadGraph(targetDir) {
const graphPath = path.join(targetDir, ".geraph", "graph.json");
if (!fs.existsSync(graphPath)) {
throw new Error("Graph data not found. Run 'geraph scan' first.");
}
const rawData = JSON.parse(fs.readFileSync(graphPath, "utf-8"));
const graph = new MultiDirectedGraph();
const nodes = rawData.nodes || [];
const edges = rawData.edges || [];
nodes.forEach((n) => {
const nid = normalizeId(n.id);
if (!graph.hasNode(nid)) {
const { id, name, type, file, startLine, ...metadata } = n;
graph.addNode(nid, {
name: name || "",
type,
file: normalizeId(file || ""),
startLine: startLine || 0,
metadata: metadata || {}
});
}
});
edges.forEach((e) => {
const source = normalizeId(e.source);
const target = normalizeId(e.target);
if (graph.hasNode(source) && graph.hasNode(target)) {
graph.addEdge(source, target, {
type: e.relation || e.type,
confidence: e.confidence,
metadata: e.metadata || {}
});
}
});
return graph;
}
async function searchGraph(targetDir, term, targetType) {
const graph = loadGraph(targetDir);
const lowerTerm = term.toLowerCase();
const results = [];
graph.forEachNode((nodeId, attr) => {
if (targetType && attr.type !== targetType) return;
if (nodeId.toLowerCase().includes(lowerTerm) || attr.name && attr.name.toLowerCase().includes(lowerTerm)) {
results.push({
id: nodeId,
name: attr.name,
type: attr.type,
file: attr.file,
links: graph.degree(nodeId)
});
}
});
return results.sort((a, b) => b.links - a.links);
}
async function queryGraph(targetDir, symbol, targetType, targetSource) {
const graph = loadGraph(targetDir);
const normSymbol = normalizeId(symbol);
let targetNodeId = graph.hasNode(normSymbol) ? normSymbol : null;
if (targetNodeId && (targetType || targetSource)) {
const attr = graph.getNodeAttributes(targetNodeId);
if (targetType && attr.type !== targetType) {
targetNodeId = null;
}
if (targetNodeId && targetSource) {
const normSource = normalizeId(targetSource);
if (!attr.file.toLowerCase().endsWith(normSource.toLowerCase())) {
targetNodeId = null;
}
}
}
if (!targetNodeId) {
targetNodeId = graph.findNode((nodeId, attr) => {
if (targetType && attr.type !== targetType) return false;
if (targetSource) {
const normSource = normalizeId(targetSource);
if (!attr.file.endsWith(normSource)) return false;
}
return attr && attr.name && attr.name === symbol || nodeId === normSymbol || nodeId.endsWith("/" + normSymbol) || nodeId.endsWith("::" + normSymbol);
}) ?? null;
}
if (!targetNodeId) {
targetNodeId = graph.findNode((nodeId, attr) => {
if (targetType && attr.type !== targetType) return false;
if (targetSource) {
const normSource = normalizeId(targetSource);
if (!attr.file.toLowerCase().endsWith(normSource.toLowerCase())) return false;
}
return attr && attr.name && attr.name.toLowerCase() === symbol.toLowerCase() || nodeId.toLowerCase() === normSymbol.toLowerCase() || nodeId.toLowerCase().endsWith("/" + normSymbol.toLowerCase()) || nodeId.toLowerCase().endsWith("::" + normSymbol.toLowerCase());
}) ?? null;
}
if (!targetNodeId) {
const typeMsg = targetType ? ` of type '${targetType}'` : "";
const sourceMsg = targetSource ? ` in source '${targetSource}'` : "";
throw new Error(`Symbol '${symbol}'${typeMsg}${sourceMsg} not found in the graph.`);
}
const targetAttr = graph.getNodeAttributes(targetNodeId);
const result = {
target: {
id: targetNodeId,
name: targetAttr.name,
type: targetAttr.type,
file: targetAttr.file,
line: targetAttr.startLine,
metadata: targetAttr.metadata,
links: {
incoming: graph.inDegree(targetNodeId),
outgoing: graph.outDegree(targetNodeId)
}
},
incoming: [],
outgoing: []
};
const collectEdges = (nodeId, isOutgoing, seenKeys) => {
const iterator = isOutgoing ? graph.forEachOutEdge.bind(graph) : graph.forEachInEdge.bind(graph);
iterator(nodeId, (edge, attr, source, target) => {
const neighborId = isOutgoing ? target : source;
if (neighborId === targetNodeId) return;
const key = `${neighborId}:${attr.type}`;
if (seenKeys.has(key)) return;
seenKeys.add(key);
const neighborAttr = graph.getNodeAttributes(neighborId);
const nodeInfo = {
id: neighborId,
name: neighborAttr.name,
type: neighborAttr.type,
file: neighborAttr.file,
line: neighborAttr.startLine
};
if (isOutgoing) {
result.outgoing.push({
relation: attr.type,
confidence: attr.confidence,
target: nodeInfo
});
} else {
result.incoming.push({
relation: attr.type,
confidence: attr.confidence,
source: nodeInfo
});
}
});
};
const seenIn = /* @__PURE__ */ new Set();
const seenOut = /* @__PURE__ */ new Set();
collectEdges(targetNodeId, false, seenIn);
collectEdges(targetNodeId, true, seenOut);
return result;
}
export {
queryGraph,
searchGraph
};