+62
-33
@@ -232,26 +232,42 @@ // src/core/worker.ts | ||
| ]); | ||
| function resolveImportToNode(importPath, sourceFilePath) { | ||
| if (!importPath.startsWith(".")) return null; | ||
| const sourceDir = path.dirname(sourceFilePath); | ||
| function resolveImportToNode(importPath, sourceFilePath, aliases = []) { | ||
| const checkCandidates = (resolvedBase) => { | ||
| const candidates = [ | ||
| resolvedBase + ".ts", | ||
| resolvedBase + ".tsx", | ||
| resolvedBase + "/index.ts", | ||
| resolvedBase + "/index.tsx", | ||
| resolvedBase + ".js", | ||
| resolvedBase + ".jsx", | ||
| resolvedBase + "/index.js", | ||
| resolvedBase + "/index.jsx" | ||
| ]; | ||
| for (const candidate of candidates) { | ||
| if (fs.existsSync(candidate)) return candidate; | ||
| } | ||
| return null; | ||
| }; | ||
| const normalizedImport = importPath.replace(/\.js$/, ""); | ||
| const resolvedBase = path.resolve(sourceDir, normalizedImport); | ||
| const candidates = [ | ||
| resolvedBase + ".ts", | ||
| resolvedBase + ".tsx", | ||
| resolvedBase + "/index.ts", | ||
| resolvedBase + "/index.tsx", | ||
| resolvedBase + ".js", | ||
| resolvedBase + ".jsx", | ||
| resolvedBase + "/index.js", | ||
| resolvedBase + "/index.jsx" | ||
| ]; | ||
| for (const candidate of candidates) { | ||
| if (fs.existsSync(candidate)) return candidate; | ||
| if (importPath.startsWith(".")) { | ||
| const sourceDir = path.dirname(sourceFilePath); | ||
| const resolvedBase = path.resolve(sourceDir, normalizedImport); | ||
| return checkCandidates(resolvedBase); | ||
| } | ||
| if (aliases && aliases.length > 0) { | ||
| for (const alias of aliases) { | ||
| if (importPath.startsWith(alias.prefix)) { | ||
| const remainder = normalizedImport.slice(alias.prefix.length); | ||
| for (const target of alias.targets) { | ||
| const resolvedBase = path.join(target, remainder); | ||
| const found = checkCandidates(resolvedBase); | ||
| if (found) return found; | ||
| } | ||
| } | ||
| } | ||
| } | ||
| return null; | ||
| } | ||
| function parseTypeScript(filePath, graph) { | ||
| function parseTypeScript(filePath, graph, aliases = []) { | ||
| const isTs = filePath.endsWith(".ts") || filePath.endsWith(".tsx"); | ||
| const isTsx = filePath.endsWith(".tsx"); | ||
| const basename = path.basename(filePath); | ||
| let language; | ||
@@ -351,7 +367,8 @@ if (isTs) { | ||
| const scope = getEnclosingScope(node); | ||
| const id = scope ? `${filePath}::${scope.name}` : `${filePath}::top-level`; | ||
| if (!scope) return filePath; | ||
| const id = `${filePath}::${scope.name}`; | ||
| if (!graph.hasNode(id)) { | ||
| graph.addNode(id, { | ||
| type: scope ? scope.type : "function", | ||
| name: scope ? scope.name : `[script] ${basename}`, | ||
| type: scope.type, | ||
| name: scope.name, | ||
| file: filePath, | ||
@@ -406,3 +423,3 @@ startLine: node.startPosition.row + 1, | ||
| if (!importPath.startsWith(".") && !importPath.startsWith("/") && (NODE_CORE_MODULES.has(normalizedPath) || NODE_CORE_MODULES.has(rootModule))) continue; | ||
| const resolvedId = resolveImportToNode(importPath, filePath); | ||
| const resolvedId = resolveImportToNode(importPath, filePath, aliases); | ||
| const targetNodeId = resolvedId || `import::${importPath}`; | ||
@@ -489,3 +506,3 @@ if (!graph.hasNode(targetNodeId)) { | ||
| if (link.startsWith(".")) { | ||
| targetPath = resolveImportToNode(link, filePath) || link; | ||
| targetPath = resolveImportToNode(link, filePath, aliases) || link; | ||
| } | ||
@@ -519,3 +536,3 @@ if (!graph.hasNode(targetPath)) { | ||
| } else if (importSource) { | ||
| const resolvedSource = resolveImportToNode(importSource, filePath) || importSource; | ||
| const resolvedSource = resolveImportToNode(importSource, filePath, aliases) || importSource; | ||
| targetId = `${resolvedSource}::${referencedTypeName}`; | ||
@@ -530,3 +547,3 @@ } else { | ||
| name: referencedTypeName, | ||
| file: importSource && resolveImportToNode(importSource, filePath) || importSource || filePath, | ||
| file: importSource && resolveImportToNode(importSource, filePath, aliases) || importSource || filePath, | ||
| startLine: 0, | ||
@@ -547,4 +564,4 @@ metadata: { | ||
| if (importSource) { | ||
| const resolvedSource = resolveImportToNode(importSource, filePath) || importSource; | ||
| const importNodeId = resolveImportToNode(importSource, filePath) || `import::${importSource}`; | ||
| const resolvedSource = resolveImportToNode(importSource, filePath, aliases) || importSource; | ||
| const importNodeId = resolveImportToNode(importSource, filePath, aliases) || `import::${importSource}`; | ||
| if (!graph.hasNode(importNodeId)) { | ||
@@ -585,3 +602,3 @@ graph.addNode(importNodeId, { type: "file", name: importSource, file: resolvedSource, startLine: 0, metadata: { external: true } }); | ||
| } else if (importSource) { | ||
| const resolvedSource = resolveImportToNode(importSource, filePath) || importSource; | ||
| const resolvedSource = resolveImportToNode(importSource, filePath, aliases) || importSource; | ||
| symId = `${resolvedSource}::${calledName}`; | ||
@@ -594,3 +611,3 @@ } else { | ||
| const callLine = node.startPosition.row + 1; | ||
| const resolvedSource = importSource && resolveImportToNode(importSource, filePath) || importSource; | ||
| const resolvedSource = importSource && resolveImportToNode(importSource, filePath, aliases) || importSource; | ||
| graph.addNode(symId, { | ||
@@ -615,4 +632,4 @@ type: name === "constructor_name" ? "class" : "function", | ||
| if (importSource) { | ||
| const resolvedSource = resolveImportToNode(importSource, filePath) || importSource; | ||
| const importNodeId = resolveImportToNode(importSource, filePath) || `import::${importSource}`; | ||
| const resolvedSource = resolveImportToNode(importSource, filePath, aliases) || importSource; | ||
| const importNodeId = resolveImportToNode(importSource, filePath, aliases) || `import::${importSource}`; | ||
| if (!graph.hasNode(importNodeId)) { | ||
@@ -774,4 +791,15 @@ graph.addNode(importNodeId, { | ||
| ]); | ||
| function getContextualAliases(filePath, aliasMap) { | ||
| let current = path5.dirname(filePath); | ||
| while (current) { | ||
| const aliases = aliasMap[current]; | ||
| if (aliases) return aliases; | ||
| const parent = path5.dirname(current); | ||
| if (parent === current) break; | ||
| current = parent; | ||
| } | ||
| return []; | ||
| } | ||
| parentPort?.on("message", async (msg) => { | ||
| const { filePath } = msg; | ||
| const { filePath, aliasMap } = msg; | ||
| const localGraph = new MultiDirectedGraph(); | ||
@@ -790,3 +818,4 @@ try { | ||
| if (ext === "ts" || ext === "js" || ext === "tsx" || ext === "jsx") { | ||
| parseTypeScript(filePath, localGraph); | ||
| const aliases = getContextualAliases(filePath, aliasMap || {}); | ||
| parseTypeScript(filePath, localGraph, aliases); | ||
| } else if (ext === "json") { | ||
@@ -793,0 +822,0 @@ parseJson(filePath, localGraph); |
+359
-189
@@ -5,8 +5,5 @@ #!/usr/bin/env node | ||
| import { Command } from "commander"; | ||
| import chalk3 from "chalk"; | ||
| import chalk4 from "chalk"; | ||
| import ora from "ora"; | ||
| import path5 from "path"; | ||
| import { availableParallelism } from "os"; | ||
| import { fileURLToPath } from "url"; | ||
| import { Worker } from "worker_threads"; | ||
| import path7 from "path"; | ||
@@ -50,3 +47,3 @@ // src/core/scanner.ts | ||
| } | ||
| ig.add(["node_modules", "dist", ".git", ".turbo", "build"]); | ||
| ig.add(["node_modules", "dist", ".git", ".turbo", "build", ".geraph"]); | ||
| const globPatterns = SUPPORTED_EXTENSIONS.map((ext) => `**/*.${ext}`); | ||
@@ -60,5 +57,145 @@ const normalizedCwd = targetDir.split(path.sep).join("/"); | ||
| const validRelativeFiles = ig.filter(allFiles); | ||
| return validRelativeFiles.map((file) => path.resolve(targetDir, file)); | ||
| return validRelativeFiles.sort().map((file) => path.resolve(targetDir, file)); | ||
| } | ||
| // src/core/ast.ts | ||
| import { availableParallelism } from "os"; | ||
| import path2 from "path"; | ||
| import fs2 from "fs"; | ||
| import { fileURLToPath } from "url"; | ||
| import { Worker } from "worker_threads"; | ||
| import crypto from "crypto"; | ||
| import chalk from "chalk"; | ||
| function getFileHash(filePath, rootDir, aliasMap) { | ||
| const content = fs2.readFileSync(filePath); | ||
| const relPath = path2.relative(rootDir, filePath).toLowerCase(); | ||
| const h = crypto.createHash("sha256"); | ||
| h.update(content); | ||
| h.update(Buffer.from("\0")); | ||
| h.update(Buffer.from(relPath)); | ||
| h.update(Buffer.from("\0")); | ||
| h.update(Buffer.from(JSON.stringify(aliasMap))); | ||
| return h.digest("hex"); | ||
| } | ||
| async function extractAst(files, graph, targetDir, aliasMap, spinner) { | ||
| const astCacheDir = path2.join(targetDir, ".geraph", "cache", "ast"); | ||
| if (!fs2.existsSync(astCacheDir)) | ||
| fs2.mkdirSync(astCacheDir, { recursive: true }); | ||
| const queue = []; | ||
| let cachedCount = 0; | ||
| const resultsMap = /* @__PURE__ */ new Map(); | ||
| for (const file of files) { | ||
| try { | ||
| const hash = getFileHash(file, targetDir, aliasMap); | ||
| const cachePath = path2.join(astCacheDir, `${hash}.json`); | ||
| if (fs2.existsSync(cachePath)) { | ||
| const cached = JSON.parse(fs2.readFileSync(cachePath, "utf-8")); | ||
| resultsMap.set(file, { nodes: cached.nodes, edges: cached.edges }); | ||
| cachedCount++; | ||
| } else { | ||
| queue.push({ file, hash }); | ||
| } | ||
| } catch { | ||
| queue.push({ file, hash: "" }); | ||
| } | ||
| } | ||
| if (cachedCount === files.length) { | ||
| spinner.stop(); | ||
| console.log( | ||
| chalk.green(`\u26A1 Fully cached! No files needed re-parsing.`) | ||
| ); | ||
| spinner.start(); | ||
| } else { | ||
| spinner.text = chalk.gray( | ||
| `Parsing AST for ${queue.length} files (${cachedCount} cached)...` | ||
| ); | ||
| } | ||
| if (queue.length > 0) { | ||
| const numWorkers = Math.min(availableParallelism(), queue.length); | ||
| const workerPath = path2.join( | ||
| path2.dirname(fileURLToPath(import.meta.url)), | ||
| "core", | ||
| "worker.js" | ||
| ); | ||
| let parsedCount = 0; | ||
| const workers = Array.from( | ||
| { length: numWorkers }, | ||
| () => new Worker(workerPath) | ||
| ); | ||
| await Promise.all( | ||
| workers.map(async (worker) => { | ||
| while (queue.length > 0) { | ||
| const item = queue.shift(); | ||
| if (!item) break; | ||
| const { file, hash } = item; | ||
| await new Promise((resolve) => { | ||
| const onMessage = (msg) => { | ||
| if (msg.error) { | ||
| console.error( | ||
| chalk.yellow( | ||
| ` | ||
| \u26A0\u3000Worker error for ${file}: ${msg.error}` | ||
| ) | ||
| ); | ||
| } else { | ||
| resultsMap.set(file, { nodes: msg.nodes, edges: msg.edges }); | ||
| if (hash) { | ||
| try { | ||
| const cachePath = path2.join(astCacheDir, `${hash}.json`); | ||
| fs2.writeFileSync( | ||
| cachePath, | ||
| JSON.stringify({ nodes: msg.nodes, edges: msg.edges }) | ||
| ); | ||
| } catch { | ||
| } | ||
| } | ||
| } | ||
| parsedCount++; | ||
| spinner.text = chalk.gray( | ||
| `Parsing AST: ${parsedCount}/${files.length - cachedCount} files...` | ||
| ); | ||
| worker.off("message", onMessage); | ||
| worker.off("error", onError); | ||
| resolve(); | ||
| }; | ||
| const onError = (err) => { | ||
| console.error( | ||
| chalk.red(`Worker error on ${file}: ${err.message}`) | ||
| ); | ||
| parsedCount++; | ||
| worker.off("message", onMessage); | ||
| worker.off("error", onError); | ||
| resolve(); | ||
| }; | ||
| worker.on("message", onMessage); | ||
| worker.on("error", onError); | ||
| worker.postMessage({ | ||
| filePath: file, | ||
| projectRoot: targetDir, | ||
| aliasMap | ||
| }); | ||
| }); | ||
| } | ||
| await worker.terminate(); | ||
| }) | ||
| ); | ||
| } | ||
| for (const file of files) { | ||
| const res = resultsMap.get(file); | ||
| if (!res) continue; | ||
| res.nodes?.forEach((n) => { | ||
| if (!graph.hasNode(n.id)) { | ||
| graph.addNode(n.id, n.attr); | ||
| } else if (n.attr.type !== "file") { | ||
| graph.mergeNodeAttributes(n.id, n.attr); | ||
| } | ||
| }); | ||
| res.edges?.forEach((e) => { | ||
| if (!graph.hasEdge(e.source, e.target)) { | ||
| graph.addEdge(e.source, e.target, e.attr); | ||
| } | ||
| }); | ||
| } | ||
| } | ||
| // src/core/graph.ts | ||
@@ -99,5 +236,5 @@ import { MultiDirectedGraph } from "graphology"; | ||
| import { simpleGit } from "simple-git"; | ||
| import path2 from "path"; | ||
| import fs2 from "fs"; | ||
| import chalk from "chalk"; | ||
| import path3 from "path"; | ||
| import fs3 from "fs"; | ||
| import chalk2 from "chalk"; | ||
| var CACHE_VERSION = "1"; | ||
@@ -122,3 +259,3 @@ var GIT_ENRICH_SKIP = /* @__PURE__ */ new Set([ | ||
| try { | ||
| const raw = fs2.readFileSync(cacheFile, "utf-8"); | ||
| const raw = fs3.readFileSync(cacheFile, "utf-8"); | ||
| const parsed = JSON.parse(raw); | ||
@@ -133,3 +270,3 @@ if (parsed.version !== CACHE_VERSION) return null; | ||
| try { | ||
| fs2.writeFileSync(cacheFile, JSON.stringify(cache, null, 2), "utf-8"); | ||
| fs3.writeFileSync(cacheFile, JSON.stringify(cache, null, 2), "utf-8"); | ||
| } catch { | ||
@@ -146,3 +283,3 @@ } | ||
| if (isShallow) { | ||
| console.warn(chalk.yellow("\n\u26A0\u3000Shallow Git repository detected. Skipping Git enrichment to save time.")); | ||
| console.warn(chalk2.yellow("\n\u26A0\u3000Shallow Git repository detected. Skipping Git enrichment to save time.")); | ||
| return; | ||
@@ -153,5 +290,6 @@ } | ||
| } | ||
| const outDir = path2.join(targetDir, ".geraph"); | ||
| if (!fs2.existsSync(outDir)) fs2.mkdirSync(outDir, { recursive: true }); | ||
| const cacheFile = path2.join(outDir, "git-cache.json"); | ||
| const outDir = path3.join(targetDir, ".geraph"); | ||
| const cacheDir = path3.join(outDir, "cache"); | ||
| if (!fs3.existsSync(cacheDir)) fs3.mkdirSync(cacheDir, { recursive: true }); | ||
| const cacheFile = path3.join(cacheDir, "git-cache.json"); | ||
| const cache = loadCache(cacheFile) ?? { | ||
@@ -178,3 +316,3 @@ version: CACHE_VERSION, | ||
| if (data.type === "file" && !data.metadata?.external) { | ||
| const basename = path2.basename(nodeId); | ||
| const basename = path3.basename(nodeId); | ||
| if (GIT_ENRICH_SKIP.has(basename)) continue; | ||
@@ -194,4 +332,4 @@ if (!nodesByFile.has(nodeId)) { | ||
| try { | ||
| const normalizedPath = path2.normalize(filePath); | ||
| const stat = fs2.statSync(filePath); | ||
| const normalizedPath = path3.normalize(filePath); | ||
| const stat = fs3.statSync(filePath); | ||
| const currentMtime = stat.mtimeMs; | ||
@@ -278,3 +416,3 @@ const cachedBlame = cache.blame[normalizedPath]; | ||
| } catch { | ||
| console.warn(chalk.yellow("\n\u26A0\u3000Warning: Failed to bulk-fetch metadata for some commits.")); | ||
| console.warn(chalk2.yellow("\n\u26A0\u3000Warning: Failed to bulk-fetch metadata for some commits.")); | ||
| } | ||
@@ -340,4 +478,9 @@ } | ||
| let communityMap = {}; | ||
| let seed = 123456789; | ||
| const rng = () => { | ||
| seed = (seed * 9301 + 49297) % 233280; | ||
| return seed / 233280; | ||
| }; | ||
| try { | ||
| communityMap = louvainAlgorithm(graph); | ||
| communityMap = louvainAlgorithm(graph, { rng }); | ||
| } catch { | ||
@@ -483,8 +626,78 @@ graph.forEachNode((nodeId) => { | ||
| // src/core/alias.ts | ||
| import fs4 from "fs"; | ||
| import path4 from "path"; | ||
| function parseTsConfig(filePath, visited = /* @__PURE__ */ new Set()) { | ||
| if (visited.has(filePath)) return {}; | ||
| visited.add(filePath); | ||
| if (!fs4.existsSync(filePath)) return {}; | ||
| try { | ||
| const content = fs4.readFileSync(filePath, "utf-8"); | ||
| const stripped = content.replace( | ||
| /\\"|"(?:\\"|[^"])*"|(\/\/.*|\/\*[\s\S]*?\*\/)/g, | ||
| (m, g) => g ? "" : m | ||
| ); | ||
| const cleanJson = stripped.replace(/,\s*([\]}])/g, "$1"); | ||
| const json = JSON.parse(cleanJson); | ||
| let base = {}; | ||
| if (json.extends) { | ||
| const extendsPaths = Array.isArray(json.extends) ? json.extends : [json.extends]; | ||
| for (const extPath of extendsPaths) { | ||
| let resolvedExtPath = path4.resolve(path4.dirname(filePath), extPath); | ||
| if (!fs4.existsSync(resolvedExtPath) && !resolvedExtPath.endsWith(".json")) { | ||
| resolvedExtPath += ".json"; | ||
| } | ||
| const extConfig = parseTsConfig(resolvedExtPath, visited); | ||
| base = mergeConfigs(base, extConfig); | ||
| } | ||
| } | ||
| return mergeConfigs(base, json); | ||
| } catch { | ||
| return {}; | ||
| } | ||
| } | ||
| function mergeConfigs(base, child) { | ||
| return { | ||
| ...base, | ||
| ...child, | ||
| compilerOptions: { | ||
| ...base.compilerOptions || {}, | ||
| ...child.compilerOptions || {} | ||
| } | ||
| }; | ||
| } | ||
| function buildAliasMap(files) { | ||
| const map = {}; | ||
| const configFiles = files.filter( | ||
| (f) => f.endsWith("tsconfig.json") || f.endsWith("jsconfig.json") | ||
| ); | ||
| for (const file of configFiles) { | ||
| const config = parseTsConfig(file); | ||
| const compilerOptions = config.compilerOptions; | ||
| if (compilerOptions && compilerOptions.paths) { | ||
| const baseUrl = compilerOptions.baseUrl || "."; | ||
| const dir = path4.dirname(file); | ||
| const aliases = []; | ||
| for (const [key, targets] of Object.entries(compilerOptions.paths)) { | ||
| const prefix = key.replace(/\*$/, ""); | ||
| const resolvedTargets = targets.map( | ||
| (t) => path4.join(dir, baseUrl, t.replace(/\*$/, "")) | ||
| ); | ||
| aliases.push({ prefix, targets: resolvedTargets }); | ||
| } | ||
| aliases.sort((a, b) => b.prefix.length - a.prefix.length); | ||
| if (aliases.length > 0) { | ||
| map[dir] = aliases; | ||
| } | ||
| } | ||
| } | ||
| return map; | ||
| } | ||
| // src/core/serializer.ts | ||
| import fs3 from "fs"; | ||
| import path3 from "path"; | ||
| import fs5 from "fs"; | ||
| import path5 from "path"; | ||
| function exportGraphJson(graph, outDir, analysis) { | ||
| if (!fs3.existsSync(outDir)) { | ||
| fs3.mkdirSync(outDir, { recursive: true }); | ||
| if (!fs5.existsSync(outDir)) { | ||
| fs5.mkdirSync(outDir, { recursive: true }); | ||
| } | ||
@@ -531,9 +744,9 @@ const nodes = graph.nodes().map((nodeId) => { | ||
| } | ||
| const jsonPath = path3.join(outDir, "graph.json"); | ||
| fs3.writeFileSync(jsonPath, JSON.stringify(payload, null, 2), "utf-8"); | ||
| const jsonPath = path5.join(outDir, "graph.json"); | ||
| fs5.writeFileSync(jsonPath, JSON.stringify(payload, null, 2), "utf-8"); | ||
| return jsonPath; | ||
| } | ||
| function exportReportMarkdown(graph, outDir, analysis) { | ||
| if (!fs3.existsSync(outDir)) { | ||
| fs3.mkdirSync(outDir, { recursive: true }); | ||
| if (!fs5.existsSync(outDir)) { | ||
| fs5.mkdirSync(outDir, { recursive: true }); | ||
| } | ||
@@ -618,3 +831,3 @@ let md = `# Geraph Codebase Report | ||
| const data = graph.getNodeAttributes(gap); | ||
| md += `- \`${data.type} ${data.name}\` (isolated in ${path3.basename(gap.split("::")[0] || gap)}) | ||
| md += `- \`${data.type} ${data.name}\` (isolated in ${path5.basename(gap.split("::")[0] || gap)}) | ||
| `; | ||
@@ -660,9 +873,9 @@ } | ||
| } | ||
| const mdPath = path3.join(outDir, "GRAPH_REPORT.md"); | ||
| fs3.writeFileSync(mdPath, md, "utf-8"); | ||
| const mdPath = path5.join(outDir, "GRAPH_REPORT.md"); | ||
| fs5.writeFileSync(mdPath, md, "utf-8"); | ||
| return mdPath; | ||
| } | ||
| function exportGraphHtml(graph, outDir) { | ||
| if (!fs3.existsSync(outDir)) { | ||
| fs3.mkdirSync(outDir, { recursive: true }); | ||
| if (!fs5.existsSync(outDir)) { | ||
| fs5.mkdirSync(outDir, { recursive: true }); | ||
| } | ||
@@ -951,4 +1164,4 @@ const counts = { | ||
| </html>`; | ||
| const htmlPath = path3.join(outDir, "graph.html"); | ||
| fs3.writeFileSync(htmlPath, htmlContent, "utf-8"); | ||
| const htmlPath = path5.join(outDir, "graph.html"); | ||
| fs5.writeFileSync(htmlPath, htmlContent, "utf-8"); | ||
| return htmlPath; | ||
@@ -958,6 +1171,6 @@ } | ||
| // src/core/install.ts | ||
| import fs4 from "fs"; | ||
| import path4 from "path"; | ||
| import fs6 from "fs"; | ||
| import path6 from "path"; | ||
| import os from "os"; | ||
| import chalk2 from "chalk"; | ||
| import chalk3 from "chalk"; | ||
| var START_MARKER = "<!-- GERAPH_START -->"; | ||
@@ -1015,3 +1228,3 @@ var END_MARKER = "<!-- GERAPH_END -->"; | ||
| localFiles: [{ path: "CLAUDE.md", content: SHARED_RULES, inject: true }], | ||
| globalPath: path4.join( | ||
| globalPath: path6.join( | ||
| os.homedir(), | ||
@@ -1053,3 +1266,3 @@ ".claude", | ||
| ], | ||
| globalPath: path4.join( | ||
| globalPath: path6.join( | ||
| os.homedir(), | ||
@@ -1075,3 +1288,3 @@ ".agent", | ||
| ], | ||
| globalPath: path4.join( | ||
| globalPath: path6.join( | ||
| os.homedir(), | ||
@@ -1093,3 +1306,3 @@ ".copilot", | ||
| ], | ||
| globalPath: path4.join( | ||
| globalPath: path6.join( | ||
| os.homedir(), | ||
@@ -1111,4 +1324,4 @@ ".copilot", | ||
| const normalizedPath = process.platform === "win32" ? currentFilePath.substring(1) : currentFilePath; | ||
| const templatePath = path4.resolve( | ||
| path4.dirname(normalizedPath), | ||
| const templatePath = path6.resolve( | ||
| path6.dirname(normalizedPath), | ||
| ".", | ||
@@ -1119,10 +1332,10 @@ "templates", | ||
| let skillContent = ""; | ||
| if (fs4.existsSync(templatePath)) { | ||
| skillContent = fs4.readFileSync(templatePath, "utf-8"); | ||
| if (fs6.existsSync(templatePath)) { | ||
| skillContent = fs6.readFileSync(templatePath, "utf-8"); | ||
| } | ||
| for (const localFile of platform.localFiles) { | ||
| const fullPath = path4.join(targetDir, localFile.path); | ||
| const dir = path4.dirname(fullPath); | ||
| if (!fs4.existsSync(dir)) { | ||
| fs4.mkdirSync(dir, { recursive: true }); | ||
| const fullPath = path6.join(targetDir, localFile.path); | ||
| const dir = path6.dirname(fullPath); | ||
| if (!fs6.existsSync(dir)) { | ||
| fs6.mkdirSync(dir, { recursive: true }); | ||
| } | ||
@@ -1135,5 +1348,6 @@ let fileContentToInject = localFile.content || skillContent; | ||
| if (platform.globalPath) { | ||
| const homeRelPath = platform.globalPath.replace(os.homedir(), "~").replace(/\\/g, "/"); | ||
| fileContentToInject = fileContentToInject.replace( | ||
| "[GLOBAL_POINTER]", | ||
| `Before answering, you MUST read the [Geraph Skill](${platform.globalPath}) operational manual.` | ||
| `Before answering, you MUST read the [Geraph Skill](${homeRelPath}) operational manual.` | ||
| ); | ||
@@ -1149,4 +1363,4 @@ } else { | ||
| `; | ||
| if (fs4.existsSync(fullPath)) { | ||
| let content = fs4.readFileSync(fullPath, "utf-8"); | ||
| if (fs6.existsSync(fullPath)) { | ||
| let content = fs6.readFileSync(fullPath, "utf-8"); | ||
| const startIndex = content.indexOf(START_MARKER); | ||
@@ -1156,14 +1370,14 @@ const endIndex = content.indexOf(END_MARKER); | ||
| content = content.substring(0, startIndex) + injection.trim() + content.substring(endIndex + END_MARKER.length); | ||
| fs4.writeFileSync(fullPath, content); | ||
| fs6.writeFileSync(fullPath, content); | ||
| results.push(`${localFile.path} updated (existing section replaced)`); | ||
| } else { | ||
| fs4.writeFileSync(fullPath, content.trim() + "\n" + injection); | ||
| fs6.writeFileSync(fullPath, content.trim() + "\n" + injection); | ||
| results.push(`${localFile.path} updated (section appended)`); | ||
| } | ||
| } else { | ||
| fs4.writeFileSync(fullPath, injection); | ||
| fs6.writeFileSync(fullPath, injection); | ||
| results.push(`${localFile.path} created`); | ||
| } | ||
| } else { | ||
| fs4.writeFileSync(fullPath, fileContentToInject); | ||
| fs6.writeFileSync(fullPath, fileContentToInject); | ||
| results.push(`${localFile.path} created/updated`); | ||
@@ -1173,11 +1387,11 @@ } | ||
| if (skillContent && platform.globalPath) { | ||
| const globalDir = path4.dirname(platform.globalPath); | ||
| if (!fs4.existsSync(globalDir)) { | ||
| fs4.mkdirSync(globalDir, { recursive: true }); | ||
| const globalDir = path6.dirname(platform.globalPath); | ||
| if (!fs6.existsSync(globalDir)) { | ||
| fs6.mkdirSync(globalDir, { recursive: true }); | ||
| } | ||
| fs4.writeFileSync(platform.globalPath, skillContent); | ||
| fs6.writeFileSync(platform.globalPath, skillContent); | ||
| results.push(`Global skill installed at ${platform.globalPath}`); | ||
| } else if (!skillContent && platform.globalPath) { | ||
| console.log( | ||
| chalk2.red(`Failed to install global skill at ${platform.globalPath}.`) | ||
| chalk3.red(`Failed to install global skill at ${platform.globalPath}.`) | ||
| ); | ||
@@ -1201,6 +1415,6 @@ } | ||
| for (const localFile of platform.localFiles) { | ||
| const fullPath = path4.join(targetDir, localFile.path); | ||
| if (fs4.existsSync(fullPath)) { | ||
| const fullPath = path6.join(targetDir, localFile.path); | ||
| if (fs6.existsSync(fullPath)) { | ||
| if (localFile.inject) { | ||
| const content = fs4.readFileSync(fullPath, "utf-8"); | ||
| const content = fs6.readFileSync(fullPath, "utf-8"); | ||
| const startIndex = content.indexOf(START_MARKER); | ||
@@ -1213,6 +1427,6 @@ const endIndex = content.indexOf(END_MARKER); | ||
| if (cleaned === "") { | ||
| fs4.unlinkSync(fullPath); | ||
| fs6.unlinkSync(fullPath); | ||
| results.push(`${localFile.path} deleted (empty after cleanup)`); | ||
| } else { | ||
| fs4.writeFileSync(fullPath, cleaned + "\n"); | ||
| fs6.writeFileSync(fullPath, cleaned + "\n"); | ||
| results.push( | ||
@@ -1224,3 +1438,3 @@ `${localFile.path} cleaned (geraph section removed)` | ||
| } else { | ||
| fs4.unlinkSync(fullPath); | ||
| fs6.unlinkSync(fullPath); | ||
| results.push(`${localFile.path} deleted`); | ||
@@ -1230,14 +1444,14 @@ } | ||
| } | ||
| if (platform.globalPath && fs4.existsSync(platform.globalPath)) { | ||
| fs4.unlinkSync(platform.globalPath); | ||
| if (platform.globalPath && fs6.existsSync(platform.globalPath)) { | ||
| fs6.unlinkSync(platform.globalPath); | ||
| results.push(`Global skill removed from ${platform.globalPath}`); | ||
| let currentDir = path4.dirname(platform.globalPath); | ||
| let currentDir = path6.dirname(platform.globalPath); | ||
| while (currentDir !== os.homedir()) { | ||
| if (path4.basename(currentDir) !== "geraph") { | ||
| if (path6.basename(currentDir) !== "geraph") { | ||
| break; | ||
| } | ||
| try { | ||
| if (fs4.readdirSync(currentDir).length === 0) { | ||
| fs4.rmdirSync(currentDir); | ||
| currentDir = path4.dirname(currentDir); | ||
| if (fs6.readdirSync(currentDir).length === 0) { | ||
| fs6.rmdirSync(currentDir); | ||
| currentDir = path6.dirname(currentDir); | ||
| } else { | ||
@@ -1256,10 +1470,9 @@ break; | ||
| // src/cli.ts | ||
| import fs5 from "fs"; | ||
| import fs7 from "fs"; | ||
| var program = new Command(); | ||
| program.name("geraph").description(chalk3.blue("Geraph: Structural memory for AI agents")).version("0.0.0", "-v, --version", "output the current version"); | ||
| program.name("geraph").description(chalk4.blue("Geraph: Structural memory for AI agents")).version("0.3.0", "-v, --version", "output the current version"); | ||
| program.command("scan").description("Scan the current directory and build the Knowledge Graph").action(async () => { | ||
| console.log(chalk3.cyan("\nGeraph Engine Started\n")); | ||
| const targetDir = process.cwd(); | ||
| const spinner = ora({ | ||
| text: chalk3.gray(`Scanning codebase in ${targetDir}...`), | ||
| text: chalk4.gray(`Scanning codebase in ${targetDir}...`), | ||
| color: "cyan", | ||
@@ -1271,3 +1484,3 @@ spinner: "dots" | ||
| const files = await scanDirectory(targetDir); | ||
| spinner.text = chalk3.gray("Initializing Knowledge Graph..."); | ||
| spinner.text = chalk4.gray("Initializing Knowledge Graph..."); | ||
| const graph = createKnowledgeGraph(); | ||
@@ -1278,7 +1491,7 @@ for (const file of files) { | ||
| type: "file", | ||
| name: path5.basename(file), | ||
| name: path7.basename(file), | ||
| file, | ||
| startLine: 0, | ||
| metadata: { | ||
| extension: path5.extname(file) | ||
| extension: path7.extname(file) | ||
| } | ||
@@ -1288,78 +1501,15 @@ }); | ||
| } | ||
| spinner.text = chalk3.gray(`Parsing AST for ${files.length} files...`); | ||
| const numWorkers = Math.min(availableParallelism(), files.length); | ||
| const workerPath = path5.join( | ||
| path5.dirname(fileURLToPath(import.meta.url)), | ||
| "core", | ||
| "worker.js" | ||
| ); | ||
| let parsedCount = 0; | ||
| const workers = Array.from( | ||
| { length: numWorkers }, | ||
| () => new Worker(workerPath) | ||
| ); | ||
| const queue = [...files]; | ||
| await Promise.all( | ||
| workers.map(async (worker) => { | ||
| while (queue.length > 0) { | ||
| const file = queue.shift(); | ||
| if (!file) break; | ||
| await new Promise((resolve) => { | ||
| const onMessage = (msg) => { | ||
| if (msg.error) { | ||
| console.error( | ||
| chalk3.yellow( | ||
| ` | ||
| \u26A0\u3000Worker error for ${file}: ${msg.error}` | ||
| ) | ||
| ); | ||
| } else { | ||
| msg.nodes?.forEach((n) => { | ||
| if (!graph.hasNode(n.id)) { | ||
| graph.addNode(n.id, n.attr); | ||
| } else if (n.attr.type !== "file") { | ||
| graph.mergeNodeAttributes(n.id, n.attr); | ||
| } | ||
| }); | ||
| msg.edges?.forEach((e) => { | ||
| if (!graph.hasEdge(e.source, e.target)) { | ||
| graph.addEdge(e.source, e.target, e.attr); | ||
| } | ||
| }); | ||
| } | ||
| parsedCount++; | ||
| spinner.text = chalk3.gray( | ||
| `Parsing AST: ${parsedCount}/${files.length} files...` | ||
| ); | ||
| worker.off("message", onMessage); | ||
| worker.off("error", onError); | ||
| resolve(); | ||
| }; | ||
| const onError = (err) => { | ||
| console.error( | ||
| chalk3.red(`Worker error on ${file}: ${err.message}`) | ||
| ); | ||
| parsedCount++; | ||
| worker.off("message", onMessage); | ||
| worker.off("error", onError); | ||
| resolve(); | ||
| }; | ||
| worker.on("message", onMessage); | ||
| worker.on("error", onError); | ||
| worker.postMessage({ filePath: file }); | ||
| }); | ||
| } | ||
| await worker.terminate(); | ||
| }) | ||
| ); | ||
| spinner.text = chalk3.gray("Resolving call graph..."); | ||
| spinner.text = chalk4.gray("Mapping path aliases..."); | ||
| const aliasMap = buildAliasMap(files); | ||
| await extractAst(files, graph, targetDir, aliasMap, spinner); | ||
| spinner.text = chalk4.gray("Resolving call graph..."); | ||
| resolveCallGraph(graph); | ||
| spinner.text = chalk3.gray( | ||
| spinner.text = chalk4.gray( | ||
| "Extracting Temporal Facts from Git history..." | ||
| ); | ||
| await enrichWithGit(graph, targetDir); | ||
| spinner.text = chalk3.gray("Analyzing graph structure..."); | ||
| spinner.text = chalk4.gray("Analyzing graph structure..."); | ||
| const analysis = analyzeGraph(graph); | ||
| spinner.text = chalk3.gray("Compressing graph into Caveman Mode..."); | ||
| const outDir = path5.join(targetDir, ".geraph"); | ||
| spinner.text = chalk4.gray("Compressing graph into Caveman Mode..."); | ||
| const outDir = path7.join(targetDir, ".geraph"); | ||
| exportGraphJson(graph, outDir, analysis); | ||
@@ -1371,3 +1521,3 @@ exportReportMarkdown(graph, outDir, analysis); | ||
| spinner.succeed( | ||
| chalk3.green( | ||
| chalk4.green( | ||
| `Successfully scanned and parsed ${files.length} target files.` | ||
@@ -1377,14 +1527,14 @@ ) | ||
| console.log(); | ||
| console.log(chalk3.bold("Graph Stats:")); | ||
| console.log(chalk3.dim(` - Nodes: ${graph.order}`)); | ||
| console.log(chalk3.dim(` - Edges: ${graph.size}`)); | ||
| console.log(chalk3.dim(` - Communities: ${analysis.communities.length}`)); | ||
| console.log(chalk3.dim(` - Time: ${durationSeconds}s`)); | ||
| console.log(chalk4.bold("Graph Stats:")); | ||
| console.log(chalk4.dim(` - Nodes: ${graph.order}`)); | ||
| console.log(chalk4.dim(` - Edges: ${graph.size}`)); | ||
| console.log(chalk4.dim(` - Communities: ${analysis.communities.length}`)); | ||
| console.log(chalk4.dim(` - Time: ${durationSeconds}s`)); | ||
| console.log(); | ||
| console.log(chalk3.cyan(`Type '/geraph' in your AI chat to begin.`)); | ||
| console.log(chalk4.cyan(`Type '/geraph' in your AI chat to begin.`)); | ||
| console.log(); | ||
| } catch (error) { | ||
| spinner.fail(chalk3.red("Failed to scan directory.")); | ||
| spinner.fail(chalk4.red("Failed to scan directory.")); | ||
| if (error instanceof Error) { | ||
| console.error(chalk3.red(`Error: ${error.message}`)); | ||
| console.error(chalk4.red(`Error: ${error.message}`)); | ||
| } | ||
@@ -1403,7 +1553,7 @@ process.exit(1); | ||
| console.log( | ||
| chalk3.yellow(` | ||
| chalk4.yellow(` | ||
| \u26A0\u3000Platform '${pName}' is not supported.`) | ||
| ); | ||
| console.log( | ||
| chalk3.gray( | ||
| chalk4.gray( | ||
| ` Run 'geraph install' to install the default AGENTS.md for basic LLM support. | ||
@@ -1416,3 +1566,3 @@ ` | ||
| const spinner = ora({ | ||
| text: chalk3.gray(`Installing Geraph rules for ${pName}...`), | ||
| text: chalk4.gray(`Installing Geraph rules for ${pName}...`), | ||
| color: "blue", | ||
@@ -1423,11 +1573,11 @@ spinner: "dots" | ||
| const platformResults = await installGeraph(targetDir, pName); | ||
| spinner.succeed(chalk3.green(`Installed Geraph rules for ${pName}.`)); | ||
| spinner.succeed(chalk4.green(`Installed Geraph rules for ${pName}.`)); | ||
| console.log(); | ||
| platformResults.forEach((r) => console.log(chalk3.dim(` - ${r}`))); | ||
| platformResults.forEach((r) => console.log(chalk4.dim(` - ${r}`))); | ||
| console.log(); | ||
| results.push(...platformResults); | ||
| } catch (error) { | ||
| spinner.fail(chalk3.red(`Failed to install rules for ${pName}.`)); | ||
| spinner.fail(chalk4.red(`Failed to install rules for ${pName}.`)); | ||
| if (error instanceof Error) { | ||
| console.error(chalk3.red(`Error: ${error.message}`)); | ||
| console.error(chalk4.red(`Error: ${error.message}`)); | ||
| } | ||
@@ -1437,5 +1587,5 @@ } | ||
| if (results.length > 0) { | ||
| if (!fs5.existsSync(path5.join(process.cwd(), ".geraph/graph.json"))) { | ||
| if (!fs7.existsSync(path7.join(process.cwd(), ".geraph/graph.json"))) { | ||
| console.log( | ||
| chalk3.cyan( | ||
| chalk4.cyan( | ||
| "Next Step: Run 'geraph scan' to build the graphical knowledge base." | ||
@@ -1455,3 +1605,3 @@ ) | ||
| console.log( | ||
| chalk3.yellow( | ||
| chalk4.yellow( | ||
| ` | ||
@@ -1465,3 +1615,3 @@ \u26A0\u3000Platform '${pName}' is not recognized. Skipping. | ||
| const spinner = ora({ | ||
| text: chalk3.gray( | ||
| text: chalk4.gray( | ||
| pName ? `Removing rules for ${pName}...` : "Removing all Geraph rules..." | ||
@@ -1477,3 +1627,3 @@ ), | ||
| spinner.succeed( | ||
| chalk3.green( | ||
| chalk4.green( | ||
| pName ? `Successfully removed rules for ${pName}.` : "Successfully removed all Geraph rules." | ||
@@ -1483,3 +1633,3 @@ ) | ||
| console.log(); | ||
| platformResults.forEach((r) => console.log(chalk3.dim(` - ${r}`))); | ||
| platformResults.forEach((r) => console.log(chalk4.dim(` - ${r}`))); | ||
| console.log(); | ||
@@ -1491,6 +1641,6 @@ } else { | ||
| spinner.fail( | ||
| chalk3.red(`Failed to remove rules for ${pName || "all"}.`) | ||
| chalk4.red(`Failed to remove rules for ${pName || "all"}.`) | ||
| ); | ||
| if (error instanceof Error) { | ||
| console.error(chalk3.red(`Error: ${error.message}`)); | ||
| console.error(chalk4.red(`Error: ${error.message}`)); | ||
| } | ||
@@ -1500,8 +1650,11 @@ } | ||
| if (results.length === 0) { | ||
| console.log(chalk3.yellow("\nNo Geraph rules found to remove.\n")); | ||
| console.log(chalk4.yellow("\nNo Geraph rules found to remove.\n")); | ||
| } | ||
| }); | ||
| 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) => { | ||
| 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}...`), | ||
| text: chalk4.gray(`Searching graph for: ${term}...`), | ||
| color: "blue", | ||
@@ -1515,7 +1668,11 @@ spinner: "dots" | ||
| if (results.length === 0) { | ||
| console.error(chalk3.yellow(`No nodes found matching '${term}'`)); | ||
| console.error(chalk4.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.`)); | ||
| console.error( | ||
| chalk4.gray( | ||
| ` | ||
| Found ${results.length} nodes. Use 'geraph query <id>' to inspect a specific node.` | ||
| ) | ||
| ); | ||
| } | ||
@@ -1525,3 +1682,3 @@ } catch (error) { | ||
| console.error( | ||
| chalk3.red("?? Search failed:"), | ||
| chalk4.red("?? Search failed:"), | ||
| error instanceof Error ? error.message : String(error) | ||
@@ -1532,5 +1689,13 @@ ); | ||
| }); | ||
| 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) => { | ||
| 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}...`), | ||
| text: chalk4.gray(`Querying relationships for: ${symbol}...`), | ||
| color: "blue", | ||
@@ -1541,3 +1706,8 @@ spinner: "dots" | ||
| const { queryGraph } = await import("./query-ALVJVDRK.js"); | ||
| const result = await queryGraph(process.cwd(), symbol, options.type, options.source); | ||
| const result = await queryGraph( | ||
| process.cwd(), | ||
| symbol, | ||
| options.type, | ||
| options.source | ||
| ); | ||
| spinner.stop(); | ||
@@ -1547,3 +1717,3 @@ console.log(JSON.stringify(result, null, 2)); | ||
| spinner.fail( | ||
| chalk3.red( | ||
| chalk4.red( | ||
| `Query failed: ${error instanceof Error ? error.message : String(error)}` | ||
@@ -1550,0 +1720,0 @@ ) |
+69
-44
@@ -9,40 +9,63 @@ --- | ||
| Geraph is a structural memory engine that tracks dependencies, function calls, imports, and historical context (Git commits) across the codebase. It eliminates the need to rely on `grep` or blind text searches. | ||
| 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. | ||
| ## How Geraph Works | ||
| When executed, Geraph parses the codebase AST (Abstract Syntax Tree) without executing any code. It builds a graph mapping all relationships and outputs its data into the `.geraph/` directory. | ||
| ## What You Must Do When Invoked | ||
| ### 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. 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. | ||
| When you need to understand the architecture, find where a component is, or trace dependencies, follow this strict protocol: | ||
| ## Operational 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. | ||
| - **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. | ||
| ### 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. | ||
| ### Command Reference | ||
| ### 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. | ||
| | 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. | | ||
| ### 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. | ||
| *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`.* | ||
| ### 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. | ||
| ### JSON Response Schema | ||
| When you run a command, it returns pure JSON on stdout. Here is how to interpret the fields: | ||
| --- | ||
| **`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. | ||
| ## CRITICAL RULES | ||
| 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. | ||
| --- | ||
| ## JSON Response Interpretation | ||
| **`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. | ||
| **`query` output**: Returns a detailed object with `target`, `incoming`, and `outgoing`: | ||
@@ -65,25 +88,19 @@ - `target`: The queried node's full details: | ||
| ### Standard Workflows | ||
| --- | ||
| | 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. | | ||
| ## Geraph Glossary | ||
| ### Geraph Glossary | ||
| Use this glossary to understand the types of nodes and edges in the graph, and to accurately choose your `--type` flag for queries. | ||
| | Node Type | Description | | ||
| ### Node Types | ||
| | Type | Description | | ||
| |---|---| | ||
| | `file` | A source code file. | | ||
| | `function` | A standard function/method. | | ||
| | `function` | A standard function, method, or arrow function. | | ||
| | `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 | | ||
| ### Edge Types (`relation`) | ||
| | Relation | Description | | ||
| |---|---| | ||
@@ -93,3 +110,11 @@ | `imports` | File A depends on File 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. | | ||
| | `references` | A function uses a specific type or interface. | | ||
| | `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. | |
+3
-2
| { | ||
| "name": "geraph", | ||
| "version": "0.2.0", | ||
| "version": "0.3.0", | ||
| "description": "Structural memory for AI agents. Build semantic knowledge graphs from your codebase for surgical code modifications.", | ||
@@ -32,2 +32,3 @@ "main": "dist/index.js", | ||
| }, | ||
| "funding": "https://github.com/sponsors/rupam2232", | ||
| "publishConfig": { | ||
@@ -40,3 +41,3 @@ "access": "public" | ||
| "engines": { | ||
| "node": ">=18.0.0" | ||
| "node": ">=18.14.0" | ||
| }, | ||
@@ -43,0 +44,0 @@ "type": "module", |
+2
-0
@@ -9,2 +9,4 @@ # Geraph | ||
| **Prerequisite:** Node.js `>=18.14.0` is required. | ||
| Run the following commands to install geraph cli, platform specific rules and build the graph: | ||
@@ -11,0 +13,0 @@ |
Filesystem access
Supply chain riskAccesses the file system, and could potentially read sensitive data.
99531
5.45%2604
8.14%35
6.06%18
12.5%