| // 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 | ||
| }; |
+94
-50
@@ -531,3 +531,6 @@ #!/usr/bin/env node | ||
| `; | ||
| const fileNodes = graph.nodes().filter((n) => graph.getNodeAttribute(n, "type") === "file"); | ||
| md += `*Note: Only the first 100 files and their primary members are listed here. Use 'query' for full details.* | ||
| `; | ||
| const fileNodes = graph.nodes().filter((n) => graph.getNodeAttribute(n, "type") === "file").slice(0, 100); | ||
| for (const file of fileNodes) { | ||
@@ -541,3 +544,4 @@ const data = graph.getNodeAttributes(file); | ||
| }); | ||
| for (const edgeId of definesEdges) { | ||
| const membersToShow = definesEdges.slice(0, 5); | ||
| for (const edgeId of membersToShow) { | ||
| const target = graph.target(edgeId); | ||
@@ -548,2 +552,6 @@ const targetData = graph.getNodeAttributes(target); | ||
| } | ||
| if (definesEdges.length > 5) { | ||
| md += ` - ... and ${definesEdges.length - 5} more | ||
| `; | ||
| } | ||
| } | ||
@@ -619,3 +627,9 @@ if (analysis && analysis.godNodes.length > 0) { | ||
| `; | ||
| const intentNodes = graph.nodes().filter((n) => graph.getNodeAttribute(n, "type") === "intent"); | ||
| const intentNodes = graph.nodes().filter((n) => graph.getNodeAttribute(n, "type") === "intent").sort((a, b) => { | ||
| const metaA = graph.getNodeAttribute(a, "metadata"); | ||
| const metaB = graph.getNodeAttribute(b, "metadata"); | ||
| const dateA = metaA?.date || ""; | ||
| const dateB = metaB?.date || ""; | ||
| return dateB.localeCompare(dateA); | ||
| }).slice(0, 50); | ||
| if (intentNodes.length > 0) { | ||
@@ -625,5 +639,8 @@ md += ` | ||
| `; | ||
| md += `*Showing the 50 most recent architectural commits. Use 'query' on a specific symbol to see its full history.* | ||
| `; | ||
| for (const intent of intentNodes) { | ||
| const data = graph.getNodeAttributes(intent); | ||
| const msg = data.metadata?.message || ""; | ||
| const msg = (data.metadata?.message || "").split("\n")[0]; | ||
| md += `- **${data.name}**: ${msg} | ||
@@ -940,6 +957,12 @@ `; | ||
| 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. | ||
| - If the geraph CLI is available, utilize \`npx geraph query "<symbol>"\` for precise architecture navigation instead of falling back to \`grep\`. | ||
| - After modifying code files in this session, run \`npx geraph scan\` to keep the graph current. | ||
| - 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. | ||
| [GLOBAL_POINTER] | ||
| `; | ||
@@ -954,16 +977,14 @@ var COPILOT_RULES = `## geraph | ||
| NEVER attempt to read the massive \`.geraph/graph.json\` file directly. Use the CLI instead. | ||
| Use \`npx geraph query "<symbol>"\` to trace dependencies, and \`npx geraph scan\` to update the graph after writing code. | ||
| `; | ||
| var CURSOR_RULE = `--- | ||
| description: geraph codebase knowledge graph | ||
| alwaysApply: true | ||
| --- | ||
| If running any \`geraph\` command returns a 'command not found' error, you MUST install it globally using \`npm install -g geraph\` before proceeding. | ||
| This project uses a geraph knowledge graph. | ||
| 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\` | ||
| - 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 cross-module "how does X relate to Y" questions or dependency tracing, prefer \`npx geraph query "<symbol>"\` over grep \u2014 it guarantees precision. | ||
| - After modifying code files in this session, run \`npx 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. | ||
| [GLOBAL_POINTER] | ||
| `; | ||
@@ -977,3 +998,3 @@ var ANTIGRAVITY_WORKFLOW = `--- | ||
| Follow the geraph skill installed at ~/.agent/skills/geraph/SKILL.md to run queries. | ||
| [GLOBAL_POINTER] | ||
| `; | ||
@@ -997,3 +1018,8 @@ var PLATFORMS = { | ||
| path: ".cursor/rules/geraph.mdc", | ||
| content: CURSOR_RULE, | ||
| content: `--- | ||
| description: geraph codebase knowledge graph | ||
| alwaysApply: true | ||
| --- | ||
| [GERAPH_SKILL_CONTENT_PLACEHOLDER]`, | ||
| inject: false | ||
@@ -1088,6 +1114,19 @@ } | ||
| } | ||
| let fileContentToInject = localFile.content || skillContent; | ||
| fileContentToInject = fileContentToInject.replace( | ||
| "[GERAPH_SKILL_CONTENT_PLACEHOLDER]", | ||
| skillContent | ||
| ); | ||
| if (platform.globalPath) { | ||
| fileContentToInject = fileContentToInject.replace( | ||
| "[GLOBAL_POINTER]", | ||
| `Before answering, you MUST read the [Geraph Skill](${platform.globalPath}) operational manual.` | ||
| ); | ||
| } else { | ||
| fileContentToInject = fileContentToInject.replace("[GLOBAL_POINTER]", ""); | ||
| } | ||
| if (localFile.inject) { | ||
| const injection = ` | ||
| ${START_MARKER} | ||
| ${(localFile.content || skillContent).trim()} | ||
| ${fileContentToInject.trim()} | ||
| ${END_MARKER} | ||
@@ -1112,3 +1151,3 @@ `; | ||
| } else { | ||
| fs4.writeFileSync(fullPath, localFile.content || ""); | ||
| fs4.writeFileSync(fullPath, fileContentToInject); | ||
| results.push(`${localFile.path} created/updated`); | ||
@@ -1317,23 +1356,5 @@ } | ||
| console.log(chalk3.dim(` - Time: ${durationSeconds}s`)); | ||
| if (analysis.godNodes.length > 0) { | ||
| console.log(); | ||
| console.log(chalk3.bold("God Nodes (architectural pillars):")); | ||
| for (const god of analysis.godNodes.slice(0, 5)) { | ||
| console.log( | ||
| chalk3.yellow( | ||
| ` \u2605 ${god.name} (${god.type}, ${god.degree} connections)` | ||
| ) | ||
| ); | ||
| } | ||
| } | ||
| if (analysis.surprisingConnections.length > 0) { | ||
| console.log(); | ||
| console.log(chalk3.bold("Surprising Connections:")); | ||
| for (const s of analysis.surprisingConnections.slice(0, 3)) { | ||
| console.log( | ||
| chalk3.magenta(` \u26A1 ${s.sourceName} \u2194 ${s.targetName}: ${s.why}`) | ||
| ); | ||
| } | ||
| } | ||
| console.log(); | ||
| console.log(chalk3.cyan(`Type '/geraph' in your AI chat to begin.`)); | ||
| console.log(); | ||
| } catch (error) { | ||
@@ -1361,3 +1382,3 @@ spinner.fail(chalk3.red("Failed to scan directory.")); | ||
| chalk3.gray( | ||
| ` Run 'npx geraph install' to install the default AGENTS.md for basic LLM support. | ||
| ` Run 'geraph install' to install the default AGENTS.md for basic LLM support. | ||
| ` | ||
@@ -1391,3 +1412,3 @@ ) | ||
| chalk3.cyan( | ||
| "Next Step: Run 'npx geraph scan' to build the graphical knowledge base." | ||
| "Next Step: Run 'geraph scan' to build the graphical knowledge base." | ||
| ) | ||
@@ -1449,6 +1470,30 @@ ); | ||
| }); | ||
| program.command("query <symbol>").description( | ||
| "Query the knowledge graph for a specific symbol's relationships" | ||
| ).action(async (symbol) => { | ||
| program.command("search <term>").description("Discover multiple nodes matching a partial term").option("-t, --type <type>", "Filter results by node type (e.g., 'interface', 'class', 'function', 'file')").action(async (term, options) => { | ||
| const spinner = ora({ | ||
| text: chalk3.gray(`Searching graph for: ${term}...`), | ||
| color: "blue", | ||
| spinner: "dots" | ||
| }).start(); | ||
| try { | ||
| const { searchGraph } = await import("./query-ALVJVDRK.js"); | ||
| const results = await searchGraph(process.cwd(), term, options.type); | ||
| spinner.stop(); | ||
| if (results.length === 0) { | ||
| console.error(chalk3.yellow(`No nodes found matching '${term}'`)); | ||
| } else { | ||
| console.log(JSON.stringify(results, null, 2)); | ||
| console.error(chalk3.gray(` | ||
| Found ${results.length} nodes. Use 'geraph query <id>' to inspect a specific node.`)); | ||
| } | ||
| } catch (error) { | ||
| spinner.stop(); | ||
| console.error( | ||
| chalk3.red("?? Search failed:"), | ||
| error instanceof Error ? error.message : String(error) | ||
| ); | ||
| process.exit(1); | ||
| } | ||
| }); | ||
| program.command("query <symbol>").description("Query the knowledge graph for a specific symbol's relationships").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')").action(async (symbol, options) => { | ||
| const spinner = ora({ | ||
| text: chalk3.gray(`Querying relationships for: ${symbol}...`), | ||
@@ -1459,7 +1504,6 @@ color: "blue", | ||
| try { | ||
| const { queryGraph } = await import("./query-KYZCIKSV.js"); | ||
| const result = await queryGraph(process.cwd(), symbol); | ||
| const { queryGraph } = await import("./query-ALVJVDRK.js"); | ||
| const result = await queryGraph(process.cwd(), symbol, options.type, options.source); | ||
| spinner.stop(); | ||
| console.log(JSON.stringify(result, null, 2)); | ||
| console.log(); | ||
| } catch (error) { | ||
@@ -1466,0 +1510,0 @@ spinner.fail( |
+65
-50
@@ -15,64 +15,79 @@ --- | ||
| ### Key Outputs | ||
| - **`.geraph/GRAPH_REPORT.md`**: A human-readable architectural summary containing project stats, core architectural pillars ("God Nodes"), and community clustering. **Always read this file first** to understand the high-level architecture before answering complex questions. | ||
| - **`.geraph/graph.json`**: The raw serialized graph data used by the CLI. | ||
| - **`.geraph/GRAPH_REPORT.md`**: A human-readable architectural summary containing project stats, core architectural pillars ("God Nodes"), and community clustering. **Always read this file first** to understand the high-level architecture. Note that for large projects, this report is truncated to the "Top 100" files and "Top 50" recent commits. Use the `query` command for surgical precision and complete history. | ||
| - **`.geraph/graph.html`**: An interactive 2D/3D visualization of the codebase. Useful for humans to spot clusters and flow, but not for AI agents. | ||
| - **`.geraph/graph.json`**: The raw serialized graph data. **CRITICAL**: NEVER attempt to read this file directly as it can be massive and will overflow your context window. Always use the `query` command to fetch filtered, token-efficient data. | ||
| ## Operational Protocol | ||
| ### 1. Tracing Dependencies (The Query Command) | ||
| **CRITICAL RULE**: NEVER attempt to read `.geraph/graph.json` or `.geraph/graph.html` directly. These are massive serialized files. You MUST use the CLI `query` command to fetch crisp, lightweight results. | ||
| - **CRITICAL RULE**: NEVER read `.geraph/graph.json` or `.geraph/graph.html` directly. Always use the CLI. | ||
| - **COMMAND NOT FOUND**: If the terminal returns a 'command not found' error for any `geraph` command, you MUST install it globally using `npm install -g geraph` before proceeding. | ||
| - **TERMINAL OUTPUT BUG**: If your agent environment/terminal fails to capture stdout (e.g., it returns "No output" or the command silently hangs), **DO NOT GIVE UP.** Run the command again and redirect the output to a file inside the `.geraph` directory: `geraph query 'Symbol' > .geraph/out.json`, then read that file. NEVER write output files to the project root. | ||
| - **CRITICAL RULE**: DO NOT use `| head` or `| grep`. The CLI is already token-optimized and piping hides crucial error messages. | ||
| - **MANDATORY**: You MUST wait for the terminal response after running any Geraph command. Do not hallucinate results. | ||
| If you need to know who calls a function, what dependencies a file has, or what a symbol does, use the CLI. | ||
| **Syntax**: `npx geraph query '<symbol>'` | ||
| - **Mandatory Quoting**: Always wrap the query in single quotes to prevent terminal expansion. | ||
| - **Search by Name First**: E.g., `npx geraph query 'saveCache'`. | ||
| - **Search by ID Next**: The initial query will return unique IDs (e.g., `src/core/git.ts::saveCache`). For 100% surgical precision in subsequent lookups, query the exact ID. | ||
| - **Empty Results**: If a query returns nothing, the symbol does not exist in the scanned scope. Check for typos or try querying the file name instead. | ||
| ### Command Reference | ||
| #### Understanding Query Results | ||
| The `query` command outputs a JSON object with three main keys: | ||
| 1. **`target`**: The node you searched for. Pay special attention to the `metadata` object inside it. | ||
| - `metadata.doc`: The full JSDoc block comment for the node. **ALWAYS read this** to understand the "Why" and the intended usage of the function/class. | ||
| - `metadata.deprecated`: If `true`, NEVER use or recommend this node in new code. | ||
| - `line` & `metadata.endLine`: The exact line range of the definition. | ||
| - `links`: The total count of `incoming` and `outgoing` relationships this node has in the full graph. High counts indicate "Hub" or "God" nodes. | ||
| 2. **`incoming`**: Nodes that depend on the target (e.g., functions that call it, or files that import it). | ||
| 3. **`outgoing`**: Nodes that the target depends on (e.g., what functions it calls internally). | ||
| | Command | Syntax | When to Use | | ||
| |---|---|---| | ||
| | **Search** | `geraph search '<term>' [--type <type>]` | For broad/fuzzy discovery. Use when you only know part of a name or want to see all nodes matching a concept (e.g., `search 'auth'`). Returns a lightweight array of matching IDs. | | ||
| | **Query** | `geraph query '<symbol>' [--type <type>] [--source <file>]` | For deep inspection. Use when you know the exact ID or exact name of a symbol. Returns full dependencies (`incoming`/`outgoing`) and metadata. | | ||
| | **Scan** | `geraph scan` | Run this IMMEDIATELY after you make any type of change in the codebase to ensure the graph is up to date. | | ||
| ### 2. After Modifying Code (The Scan Command) | ||
| Geraph tracks the codebase statically. If you add, delete, or rename files, functions, or classes, the graph will become out of date. | ||
| **Syntax**: `npx geraph scan` | ||
| - **When to run**: IMMEDIATELY after you complete any structural modifications or refactoring in the current session. This ensures subsequent queries are accurate. | ||
| *Note on Flags: All command options/flags are optional, but it is highly recommended to use them if you know the exact type or source, as it guarantees precise results. `--type` and `--source` are the ONLY valid flags. NEVER invent flags like `--limit` or `--dfs`.* | ||
| ## Standard Workflows | ||
| ### JSON Response Schema | ||
| When you run a command, it returns pure JSON on stdout. Here is how to interpret the fields: | ||
| **Scenario A: "What does this function do?"** | ||
| - Run `npx geraph query '<function_name>'`. | ||
| - Analyze `outgoing` connections to see its internal dependencies. | ||
| - Analyze `incoming` connections to see where it is used. | ||
| - Use the `file` and `line` metadata in the output to directly read the implementation. | ||
| **`search` output**: Returns an array of matching node objects, sorted by connection count (most connected first). Each object contains: | ||
| - `id`: The unique node identifier (format: `filePath::symbolName` for code symbols, `commit::hash` for intents, or a raw file path for files). | ||
| - `name`: The human-readable name of the symbol. | ||
| - `type`: The node type (e.g., `function`, `class`, `interface`, `file`, `intent`). | ||
| - `file`: The source file where this node is defined. | ||
| - `links`: Total number of connections (incoming + outgoing). Higher means more architecturally significant. | ||
| **Scenario B: "Change this component"** | ||
| - Query the component. | ||
| - Analyze `incoming` edges to identify all dependents. **You must ensure your changes do not break these callers.** | ||
| **`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. | ||
| ## Geraph Glossary | ||
| ### 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`). | ||
| ### Node Types | ||
| - `file`: A source code file. | ||
| - `function`: A standard function/method. | ||
| - `class`: A class definition. | ||
| - `interface` / `type` / `enum`: TypeScript type definitions. | ||
| - `[script] filename.ts`: The top-level execution block of a file (code outside any function/class). Query this to see what a file does upon import/execution. | ||
| - `intent`: A Git commit explaining why a node exists. | ||
| ### Standard Workflows | ||
| ### Edge Types | ||
| - `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. | ||
| - `explains`: A Git commit provides historical context for a node. | ||
| | Scenario | Action / Command | Why | | ||
| |---|---|---| | ||
| | **"How does [Concept] work?"** | 1. Read `GRAPH_REPORT.md` to find God Nodes.<br>2. `geraph search '<concept>'` | Geraph is an AST graph. It does not understand English words like 'auth' or 'database'. You MUST use `search` first to find the actual code symbols (e.g. `authSlice.ts`), and then `query` those symbols. NEVER `query` a raw concept. | | ||
| | **User mentions a file (e.g. @file:xyz.ts)** | `geraph query '<filepath>' --type file` | ALWAYS query a mentioned file first. Analyzing its `outgoing` connections instantly reveals all classes/functions defined inside it, so you don't have to guess symbol names. | | ||
| | **"What does this function do?"**| `geraph query '<funcName>' --type function` | Read `target.metadata.doc` for intent. Look at `outgoing` for what it calls, and `incoming` for who calls it. | | ||
| | **"Impact of changing a field/property?"** | `geraph query '<ContainerName>'` | Geraph DOES NOT index individual fields (like `avatar`). You MUST query the Interface/Class that contains the field (e.g., `UserState`), then analyze its `incoming` edges. NEVER query the field name directly. | | ||
| | **"Impact of changing a class/function?"** | `geraph query '<symbolName>'` | Analyze the `incoming` array. These are the exact entities that depend on your target and might break. | | ||
| | **"Query Failed / Not Found"** | `geraph search '<symbolName>'` | Do NOT fallback to `grep`. If the terminal fails to capture output, redirect to a file (`> .geraph/out.json`) and read it. If it returns a genuine "Not found" error, use the `geraph search` command to find the correct naming. | | ||
| ### Confidence Levels | ||
| - `EXTRACTED`: Perfect confidence (100%). | ||
| - `INFERRED`: High confidence (heuristics). | ||
| - `AMBIGUOUS`: Moderate confidence (dynamic calls/overlapping names). | ||
| ### Geraph Glossary | ||
| | Node Type | Description | | ||
| |---|---| | ||
| | `file` | A source code file. | | ||
| | `function` | A standard function/method. | | ||
| | `class` | A class definition. | | ||
| | `interface`/`type`/`enum`| TypeScript type definitions. | | ||
| | `[script] <name>` | The top-level execution block of a file (code outside any function/class). | | ||
| | `intent` | A Git commit explaining why a node exists. | | ||
| | Edge Type | 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. | | ||
| | `explains` | A Git commit provides historical context for a node. | |
+6
-6
| { | ||
| "name": "geraph", | ||
| "version": "0.1.0", | ||
| "version": "0.2.0", | ||
| "description": "Structural memory for AI agents. Build semantic knowledge graphs from your codebase for surgical code modifications.", | ||
| "main": "./dist/index.js", | ||
| "module": "./dist/index.js", | ||
| "types": "./dist/index.d.ts", | ||
| "main": "dist/index.js", | ||
| "module": "dist/index.js", | ||
| "types": "dist/index.d.ts", | ||
| "bin": { | ||
| "geraph": "./dist/index.js" | ||
| "geraph": "dist/index.js" | ||
| }, | ||
@@ -30,3 +30,3 @@ "scripts": { | ||
| "type": "git", | ||
| "url": "https://github.com/rupam2232/geraph" | ||
| "url": "git+https://github.com/rupam2232/geraph.git" | ||
| }, | ||
@@ -33,0 +33,0 @@ "publishConfig": { |
+8
-5
@@ -9,10 +9,13 @@ # Geraph | ||
| The fastest way to use Geraph is via `npx`: | ||
| Run the following commands to install geraph cli, platform specific rules and build the graph: | ||
| ```bash | ||
| # 1. Setup your favorite AI assistant | ||
| npx geraph install claude # or antigravity, vscode, cursor | ||
| # 1. Install globally | ||
| npm install -g geraph | ||
| # 2. Map your project | ||
| npx geraph scan | ||
| # 2. Setup your favorite AI assistant | ||
| geraph install claude # or antigravity, vscode, cursor | ||
| # 3. Map your project | ||
| geraph scan | ||
| ``` | ||
@@ -19,0 +22,0 @@ |
| // src/core/query.ts | ||
| import fs from "fs"; | ||
| import path from "path"; | ||
| import { MultiDirectedGraph } from "graphology"; | ||
| function normalizeId(id) { | ||
| return id.replace(/\\/g, "/"); | ||
| } | ||
| async function queryGraph(targetDir, symbol) { | ||
| const graphPath = path.join(targetDir, ".geraph", "graph.json"); | ||
| if (!fs.existsSync(graphPath)) { | ||
| throw new Error("Graph data not found. Run 'npx 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 { name, type, file, startLine, ...metadata } = n; | ||
| graph.addNode(nid, { | ||
| name: name || "", | ||
| type, | ||
| file: 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 || {} | ||
| }); | ||
| } | ||
| }); | ||
| const normSymbol = normalizeId(symbol); | ||
| let targetNodeId = graph.hasNode(normSymbol) ? normSymbol : null; | ||
| if (!targetNodeId) { | ||
| targetNodeId = graph.findNode( | ||
| (nodeId, attr) => 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) { | ||
| throw new Error(`Symbol '${symbol}' 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 | ||
| }; |
Long strings
Supply chain riskContains long string literals, which may be a sign of obfuscated or packed code.
94390
9.34%2408
3.93%33
10%16
14.29%