@dreamtree-org/graphify 1.3.0 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{chunk-K322XB4U.js → chunk-7LTO76UD.js} +2 -1
- package/dist/chunk-7LTO76UD.js.map +1 -0
- package/dist/{chunk-UBXYMMXJ.js → chunk-QEB7A5KB.js} +2 -2
- package/dist/cli/index.cjs.map +1 -1
- package/dist/cli/index.js +2 -2
- package/dist/index.cjs +137 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +68 -1
- package/dist/index.d.ts +68 -1
- package/dist/index.js +139 -2
- package/dist/index.js.map +1 -1
- package/dist/mcp/server.cjs.map +1 -1
- package/dist/mcp/server.js +1 -1
- package/package.json +1 -1
- package/dist/chunk-K322XB4U.js.map +0 -1
- /package/dist/{chunk-UBXYMMXJ.js.map → chunk-QEB7A5KB.js.map} +0 -0
package/dist/mcp/server.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/mcp/server.ts","../../src/query.ts","../../src/context.ts","../../src/graphStore.ts","../../src/store/localFile.ts","../../src/security.ts","../../src/store/serialize.ts","../../src/impact.ts","../../src/testmap.ts"],"sourcesContent":["/**\n * MCP stdio server exposing query/path/explain as tools against an existing\n * graphify-out/graph.json. Stdio only — never opens a network listener.\n * All graph file paths go through security.validateGraphPath().\n *\n * This module is a pure library: importing it has no side effects (it does\n * not read argv or start a server on import). `startServer()` is called\n * explicitly by `bin/graphify-mcp.js` (the standalone `graphify-mcp` CLI\n * entry point) and by `graphify --mcp` (see src/cli/index.ts).\n */\nimport * as fs from 'node:fs/promises';\nimport * as path from 'node:path';\nimport { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';\nimport { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';\nimport type { Transport } from '@modelcontextprotocol/sdk/shared/transport.js';\nimport { z } from 'zod';\nimport { buildContextPack, renderContextPack } from '../context.js';\nimport { loadGraph } from '../graphStore.js';\nimport { affectedBy } from '../impact.js';\nimport { explainNode, queryGraph, shortestPath } from '../query.js';\nimport { testsForNode } from '../testmap.js';\nimport { validateGraphPath } from '../security.js';\n\nfunction textResult(text: string): { content: Array<{ type: 'text'; text: string }> } {\n return { content: [{ type: 'text', text }] };\n}\n\n/**\n * Build the McpServer instance (tools registered, nothing connected yet).\n * Split out from startServer() so tests can connect it to an in-memory\n * transport instead of real stdio (see tests/mcp/server.test.ts).\n */\nexport function createGraphifyMcpServer(outDir: string): McpServer {\n const server = new McpServer({ name: 'graphify', version: '0.1.0' });\n\n server.registerTool(\n 'query',\n {\n description:\n 'BFS (default) or DFS traversal of the graphify knowledge graph, seeded from nodes ' +\n 'whose label matches the question. Returns the matched seeds, the visited node ' +\n 'context, and the edges among them — broad context for answering a structural question.',\n inputSchema: {\n question: z.string().describe('Natural-language question or keywords to search the graph for.'),\n dfs: z.boolean().optional().describe('Use depth-first traversal instead of the default breadth-first.'),\n budget: z.number().int().positive().optional().describe('Hard cap on how many nodes to include.'),\n tokenBudget: z\n .number()\n .int()\n .positive()\n .optional()\n .describe('Approximate cap, in tokens, on the serialized size of the result (default 2000).'),\n },\n },\n async ({ question, dfs, budget, tokenBudget }) => {\n const graph = await loadGraph(outDir);\n const result = queryGraph(graph, question, { dfs, budget, tokenBudget });\n return textResult(JSON.stringify(result, null, 2));\n },\n );\n\n server.registerTool(\n 'path',\n {\n description: 'Shortest path between two named nodes in the graphify knowledge graph.',\n inputSchema: {\n from: z.string().describe('Name/label of the starting node.'),\n to: z.string().describe('Name/label of the destination node.'),\n },\n },\n async ({ from, to }) => {\n const graph = await loadGraph(outDir);\n const result = shortestPath(graph, from, to);\n if (!result) {\n return textResult(`Could not resolve \"${from}\" and/or \"${to}\" to a node in the graph.`);\n }\n return textResult(JSON.stringify(result, null, 2));\n },\n );\n\n server.registerTool(\n 'explain',\n {\n description:\n 'Plain-language-ready explanation of a single node: its source location, community, ' +\n 'and incoming/outgoing edges.',\n inputSchema: {\n node: z.string().describe('Name/label (or id) of the node to explain.'),\n },\n },\n async ({ node }) => {\n const graph = await loadGraph(outDir);\n const result = explainNode(graph, node);\n if (!result) {\n return textResult(`No node matched \"${node}\".`);\n }\n return textResult(JSON.stringify(result, null, 2));\n },\n );\n\n server.registerTool(\n 'affected',\n {\n description:\n 'Reverse impact analysis — \"what breaks if I change X\". Walks incoming dependency ' +\n 'edges (calls/imports/inherits/...) transitively and returns everything that depends ' +\n 'on the node, grouped by depth, with per-confidence blast-radius counts.',\n inputSchema: {\n node: z.string().describe('Name/label (or id) of the node being changed.'),\n depth: z.number().int().positive().optional().describe('How many reverse hops to follow (default 3).'),\n limit: z.number().int().positive().optional().describe('Cap on affected nodes reported (default 200).'),\n },\n },\n async ({ node, depth, limit }) => {\n const graph = await loadGraph(outDir);\n const result = affectedBy(graph, node, { maxDepth: depth, limit });\n if (!result) {\n return textResult(`No node matched \"${node}\".`);\n }\n return textResult(JSON.stringify(result, null, 2));\n },\n );\n\n server.registerTool(\n 'context',\n {\n description:\n 'Token-budgeted working-set pack: the actual code snippets relevant to a task, ' +\n 'graph-ranked and packed to a budget. Call this FIRST when starting a task — it ' +\n 'replaces query-then-read-whole-files.',\n inputSchema: {\n task: z.string().describe('What you are about to do, in natural language.'),\n budget: z.number().int().positive().optional().describe('Approximate token cap (default 4000).'),\n },\n },\n async ({ task, budget }) => {\n const graph = await loadGraph(outDir);\n const pack = await buildContextPack(graph, task, (p) => fs.readFile(p, 'utf-8'), {\n tokenBudget: budget,\n });\n return textResult(renderContextPack(pack));\n },\n );\n\n server.registerTool(\n 'tests',\n {\n description:\n 'Structural test selection: the minimal test files worth running to verify a change ' +\n 'to the given node, via reverse dependency reachability — no coverage instrumentation.',\n inputSchema: {\n node: z.string().describe('Name/label (or id) of the changed node.'),\n },\n },\n async ({ node }) => {\n const graph = await loadGraph(outDir);\n const result = testsForNode(graph, node);\n if (!result) {\n return textResult(`No node matched \"${node}\".`);\n }\n return textResult(JSON.stringify(result, null, 2));\n },\n );\n\n return server;\n}\n\n/**\n * Start the MCP server, serving `query`/`path`/`explain` tools against the\n * graph.json found in `graphPath`'s directory. `graphPath` is validated\n * (via security.validateGraphPath()) up front so a bad path fails\n * immediately with a clear error instead of the first time a client calls\n * a tool. `transport` defaults to real stdio; tests substitute an\n * in-memory transport.\n */\nexport async function startServer(graphPath: string, transport: Transport = new StdioServerTransport()): Promise<void> {\n const outDir = path.dirname(path.resolve(graphPath));\n const fileName = path.basename(graphPath);\n // Fail fast (and clearly) if the graph path escapes/doesn't exist, rather\n // than only surfacing that the first time a tool is called.\n validateGraphPath(fileName, outDir);\n\n const server = createGraphifyMcpServer(outDir);\n await server.connect(transport);\n}\n\n/** Entry point used by bin/graphify-mcp.js. */\nexport async function main(argv: string[] = process.argv): Promise<void> {\n const graphPath = argv[2] ?? path.join(process.cwd(), 'graphify-out', 'graph.json');\n await startServer(graphPath);\n}\n","import type Graph from 'graphology';\n\n/**\n * Library-level graph queries shared by the CLI (`graphify query/path/\n * explain`) and the MCP server tools of the same name. None of this is an\n * LLM call — it's lexical node matching + graph traversal, so it works\n * fully offline and deterministically.\n */\n\nconst STOPWORDS = new Set([\n 'a', 'an', 'the', 'is', 'are', 'was', 'were', 'do', 'does', 'did', 'how',\n 'what', 'where', 'when', 'why', 'who', 'which', 'to', 'of', 'in', 'on',\n 'for', 'and', 'or', 'this', 'that', 'it', 'does', 'that\\'s',\n]);\n\n/** Insert spaces at camelCase boundaries so identifier words become separate tokens. */\nfunction splitCamelCase(text: string): string {\n return text\n .replace(/([a-z0-9])([A-Z])/g, '$1 $2')\n .replace(/([A-Z]+)([A-Z][a-z])/g, '$1 $2');\n}\n\nfunction tokenize(text: string): string[] {\n return splitCamelCase(text)\n .toLowerCase()\n .split(/[^a-z0-9]+/)\n .filter((token) => token.length > 1 && !STOPWORDS.has(token));\n}\n\n/**\n * Split an identifier-ish string (node label/id) into its word subtokens:\n * camelCase, snake_case, kebab-case, dots, and path separators all become\n * boundaries, so `src/a/fileStorage.js::saveAttachment` yields\n * `src, file, storage, js, save, attachment`.\n */\nfunction subtokenize(text: string): string[] {\n return splitCamelCase(text)\n .toLowerCase()\n .split(/[^a-z0-9]+/)\n .filter((token) => token.length > 1);\n}\n\n/** Don't run the fuzzy tier on tokens this short — too many false positives. */\nconst FUZZY_MIN_TOKEN_LEN = 3;\n/** Minimum similarity for a fuzzy match to count at all. */\nconst FUZZY_THRESHOLD = 0.7;\nconst SUBTOKEN_MATCH_SCORE = 1;\nconst SUBSTRING_MATCH_SCORE = 0.6;\nconst FUZZY_MATCH_WEIGHT = 0.5;\n\n/**\n * Optimal-string-alignment Damerau-Levenshtein distance. Chosen over\n * bigram-set similarity because the common code-search typo is a\n * transposition (\"sorceNodes\"), which OSA counts as one edit but bigram\n * overlap punishes brutally.\n */\nfunction osaDistance(a: string, b: string): number {\n let prev2: number[] = [];\n let prev: number[] = Array.from({ length: b.length + 1 }, (_, j) => j);\n let curr: number[] = new Array<number>(b.length + 1).fill(0);\n\n for (let i = 1; i <= a.length; i++) {\n curr[0] = i;\n for (let j = 1; j <= b.length; j++) {\n const substitution = a[i - 1] === b[j - 1] ? 0 : 1;\n let best = Math.min(\n (prev[j] as number) + 1,\n (curr[j - 1] as number) + 1,\n (prev[j - 1] as number) + substitution,\n );\n if (i > 1 && j > 1 && a[i - 1] === b[j - 2] && a[i - 2] === b[j - 1]) {\n best = Math.min(best, (prev2[j - 2] as number) + 1);\n }\n curr[j] = best;\n }\n [prev2, prev, curr] = [prev, curr, new Array<number>(b.length + 1).fill(0)];\n }\n return prev[b.length] as number;\n}\n\n/** Normalized similarity in [0, 1]; short-circuits pairs whose length gap alone puts them under FUZZY_THRESHOLD. */\nfunction fuzzySimilarity(a: string, b: string): number {\n if (a === b) return 1;\n const maxLen = Math.max(a.length, b.length);\n if (maxLen === 0) return 1;\n if (Math.abs(a.length - b.length) / maxLen > 1 - FUZZY_THRESHOLD) return 0;\n return 1 - osaDistance(a, b) / maxLen;\n}\n\nexport interface NodeMatch {\n id: string;\n label: string;\n score: number;\n}\n\n/**\n * Score every node against the query's tokens. Identifier-aware and\n * typo-tolerant, in three tiers per token (best tier wins):\n *\n * 1. exact subtoken match (camelCase/snake_case-split word of the label\n * or id) — \"storage\" matches `fileStorage.js`;\n * 2. plain substring of the label/id (the original v1 behavior);\n * 3. fuzzy (Damerau-Levenshtein) match against a subtoken — \"sorce\"\n * still finds `scoreNodes`.\n *\n * Still fully lexical and deterministic — no LLM, no randomness.\n */\nexport function scoreNodes(graph: Graph, query: string): NodeMatch[] {\n const tokens = tokenize(query);\n const matches: NodeMatch[] = [];\n\n graph.forEachNode((id, attributes) => {\n const label = (attributes.label as string | undefined) ?? id;\n const haystack = `${label} ${id}`.toLowerCase();\n const subtokens = new Set(subtokenize(`${label} ${id}`));\n let score = 0;\n for (const token of tokens) {\n if (subtokens.has(token)) {\n score += SUBTOKEN_MATCH_SCORE;\n continue;\n }\n if (haystack.includes(token)) {\n score += SUBSTRING_MATCH_SCORE;\n continue;\n }\n if (token.length >= FUZZY_MIN_TOKEN_LEN) {\n let best = 0;\n for (const subtoken of subtokens) {\n const similarity = fuzzySimilarity(token, subtoken);\n if (similarity > best) best = similarity;\n }\n if (best >= FUZZY_THRESHOLD) score += best * FUZZY_MATCH_WEIGHT;\n }\n }\n if (score > 0) matches.push({ id, label, score });\n });\n\n // On equal score, real source nodes beat module:/external: placeholders —\n // \"devtools\" should resolve to the devtools middleware, not the\n // @redux-devtools package node that happens to share a subtoken.\n const placeholderRank = (id: string): number =>\n id.startsWith('module:') || id.startsWith('external:') ? 1 : 0;\n matches.sort(\n (a, b) =>\n b.score - a.score || placeholderRank(a.id) - placeholderRank(b.id) || a.id.localeCompare(b.id),\n );\n return matches;\n}\n\n/**\n * Best-effort single-node resolution: the highest-scoring lexical match,\n * or null if nothing in the graph matches at all. Deliberately does *not*\n * fall back to \"the most connected node\" — path/explain name a specific\n * node by intent, and silently substituting an unrelated one would be\n * more misleading than clearly saying \"no match\".\n */\nexport function resolveNode(graph: Graph, query: string): NodeMatch | null {\n const matches = scoreNodes(graph, query);\n return matches[0] ?? null;\n}\n\nexport interface VisitedNode {\n id: string;\n label: string;\n sourceFile: string;\n sourceLocation: string;\n depth: number;\n}\n\nexport interface TraversalEdge {\n source: string;\n target: string;\n relation: string;\n confidence: string;\n}\n\nexport interface QueryResult {\n seeds: NodeMatch[];\n visited: VisitedNode[];\n edges: TraversalEdge[];\n}\n\nexport interface QueryOptions {\n dfs?: boolean;\n /** Hard cap on how many nodes to include. Defaults to a generous ceiling derived from `tokenBudget`. */\n budget?: number;\n /** Approximate cap, in tokens (~4 chars each), on the serialized size of the result. Default 2000. */\n tokenBudget?: number;\n maxDepth?: number;\n maxSeeds?: number;\n}\n\nconst DEFAULT_MAX_DEPTH = 2;\nconst DEFAULT_MAX_SEEDS = 5;\nconst DEFAULT_BUDGET = 40;\nconst DEFAULT_TOKEN_BUDGET = 2000;\n/** When only tokenBudget is given, allow up to this many nodes per budgeted token-decile as a safety ceiling. */\nconst NODES_PER_TOKEN = 1 / 10;\n\n/**\n * Estimated token contribution of one node to the rendered result: its own\n * line (label, file, location) plus a rough allowance for the edge lines\n * that mention its id. Chars/4 is the usual serviceable approximation.\n */\nfunction nodeTokenCost(graph: Graph, id: string): number {\n const attrs = graph.getNodeAttributes(id);\n const label = (attrs.label as string | undefined) ?? id;\n const sourceFile = (attrs.sourceFile as string | undefined) ?? '';\n return Math.ceil((id.length * 2 + label.length + sourceFile.length + 24) / 4);\n}\n\n/**\n * BFS (default, broad context) or DFS (--dfs, trace a specific path)\n * traversal from the nodes whose label best matches `question`'s\n * keywords. This is lexical retrieval, not natural-language\n * understanding — it surfaces the neighborhood of the graph an agent (or\n * a human) should look at to answer the question, it does not itself\n * compose an answer.\n */\nexport function queryGraph(graph: Graph, question: string, options: QueryOptions = {}): QueryResult {\n const maxDepth = options.maxDepth ?? DEFAULT_MAX_DEPTH;\n const maxSeeds = options.maxSeeds ?? DEFAULT_MAX_SEEDS;\n const tokenBudget = options.tokenBudget ?? DEFAULT_TOKEN_BUDGET;\n const budget = options.budget ?? Math.max(DEFAULT_BUDGET, Math.ceil(tokenBudget * NODES_PER_TOKEN));\n\n const allMatches = scoreNodes(graph, question);\n const seeds = allMatches.length > 0 ? allMatches.slice(0, maxSeeds) : [];\n\n const visited = new Map<string, number>(); // id -> depth\n const frontier: Array<{ id: string; depth: number }> = seeds.map((s) => ({ id: s.id, depth: 0 }));\n let spentTokens = 0;\n for (const seed of seeds) {\n visited.set(seed.id, 0);\n spentTokens += nodeTokenCost(graph, seed.id);\n }\n\n while (frontier.length > 0 && visited.size < budget && spentTokens < tokenBudget) {\n const current = options.dfs ? (frontier.pop() as { id: string; depth: number }) : (frontier.shift() as { id: string; depth: number });\n if (current.depth >= maxDepth) continue;\n\n const neighbors = [...graph.neighbors(current.id)].sort((a, b) => a.localeCompare(b));\n for (const neighbor of neighbors) {\n if (visited.has(neighbor) || visited.size >= budget || spentTokens >= tokenBudget) continue;\n visited.set(neighbor, current.depth + 1);\n spentTokens += nodeTokenCost(graph, neighbor);\n frontier.push({ id: neighbor, depth: current.depth + 1 });\n }\n }\n\n const visitedNodes: VisitedNode[] = [...visited.entries()]\n .map(([id, depth]) => {\n const attrs = graph.getNodeAttributes(id);\n return {\n id,\n label: (attrs.label as string | undefined) ?? id,\n sourceFile: (attrs.sourceFile as string | undefined) ?? '',\n sourceLocation: (attrs.sourceLocation as string | undefined) ?? '',\n depth,\n };\n })\n .sort((a, b) => a.depth - b.depth || a.id.localeCompare(b.id));\n\n const edges: TraversalEdge[] = [];\n graph.forEachEdge((_edge, attrs, source, target) => {\n if (visited.has(source) && visited.has(target)) {\n edges.push({\n source,\n target,\n relation: String(attrs.relation),\n confidence: String(attrs.confidence),\n });\n }\n });\n edges.sort(\n (a, b) => a.source.localeCompare(b.source) || a.target.localeCompare(b.target) || a.relation.localeCompare(b.relation),\n );\n\n return enforceTokenBudget({ seeds, visited: visitedNodes, edges }, tokenBudget);\n}\n\n/**\n * The traversal-time cost estimate can't see how densely connected the\n * visited neighborhood is — a hub-heavy subgraph induces far more edge\n * lines than nodes. Enforce the budget on the *actual* result: drop the\n * deepest/last nodes (and the edges that touched them) until the\n * serialized size fits. Seeds are never dropped.\n */\nfunction enforceTokenBudget(result: QueryResult, tokenBudget: number): QueryResult {\n const cost = (value: unknown): number => Math.ceil(JSON.stringify(value).length / 4);\n\n const visited = [...result.visited];\n let edges = [...result.edges];\n let total = cost(result.seeds) + visited.reduce((s, n) => s + cost(n), 0) + edges.reduce((s, e) => s + cost(e), 0);\n\n const seedIds = new Set(result.seeds.map((s) => s.id));\n // visited is sorted by (depth, id) — pop from the end so the deepest,\n // least-central context goes first and the result stays deterministic.\n while (total > tokenBudget && visited.length > 0) {\n const last = visited[visited.length - 1] as VisitedNode;\n if (seedIds.has(last.id)) break;\n visited.pop();\n total -= cost(last);\n const remaining: TraversalEdge[] = [];\n for (const edge of edges) {\n if (edge.source === last.id || edge.target === last.id) {\n total -= cost(edge);\n } else {\n remaining.push(edge);\n }\n }\n edges = remaining;\n }\n\n return { seeds: result.seeds, visited, edges };\n}\n\nexport interface PathResult {\n from: NodeMatch;\n to: NodeMatch;\n found: boolean;\n path: string[];\n edges: TraversalEdge[];\n}\n\n/** Shortest path between the nodes that best match `fromQuery` and `toQuery` (undirected BFS — connectivity, not call direction). */\nexport function shortestPath(graph: Graph, fromQuery: string, toQuery: string): PathResult | null {\n const from = resolveNode(graph, fromQuery);\n const to = resolveNode(graph, toQuery);\n if (!from || !to) return null;\n\n if (from.id === to.id) {\n return { from, to, found: true, path: [from.id], edges: [] };\n }\n\n const predecessor = new Map<string, string>();\n const visited = new Set<string>([from.id]);\n const queue: string[] = [from.id];\n\n while (queue.length > 0) {\n const current = queue.shift() as string;\n if (current === to.id) break;\n const neighbors = [...graph.neighbors(current)].sort((a, b) => a.localeCompare(b));\n for (const neighbor of neighbors) {\n if (visited.has(neighbor)) continue;\n visited.add(neighbor);\n predecessor.set(neighbor, current);\n queue.push(neighbor);\n }\n }\n\n if (!visited.has(to.id)) {\n return { from, to, found: false, path: [], edges: [] };\n }\n\n const path: string[] = [to.id];\n let cursor = to.id;\n while (cursor !== from.id) {\n const prev = predecessor.get(cursor);\n if (!prev) break;\n path.unshift(prev);\n cursor = prev;\n }\n\n const edges: TraversalEdge[] = [];\n for (let i = 0; i < path.length - 1; i++) {\n const a = path[i] as string;\n const b = path[i + 1] as string;\n const key = graph.edges(a, b)[0] ?? graph.edges(b, a)[0];\n if (key) {\n const attrs = graph.getEdgeAttributes(key);\n edges.push({ source: a, target: b, relation: String(attrs.relation), confidence: String(attrs.confidence) });\n }\n }\n\n return { from, to, found: true, path, edges };\n}\n\nexport interface ExplainResult {\n id: string;\n label: string;\n sourceFile: string;\n sourceLocation: string;\n communityLabel?: string;\n outgoing: TraversalEdge[];\n incoming: TraversalEdge[];\n}\n\n/** Plain-language-ready explanation of the node that best matches `query`. */\nexport function explainNode(graph: Graph, query: string): ExplainResult | null {\n const match = resolveNode(graph, query);\n if (!match) return null;\n\n const attrs = graph.getNodeAttributes(match.id);\n const outgoing: TraversalEdge[] = [];\n const incoming: TraversalEdge[] = [];\n\n graph.forEachOutEdge(match.id, (_edge, edgeAttrs, source, target) => {\n outgoing.push({ source, target, relation: String(edgeAttrs.relation), confidence: String(edgeAttrs.confidence) });\n });\n graph.forEachInEdge(match.id, (_edge, edgeAttrs, source, target) => {\n incoming.push({ source, target, relation: String(edgeAttrs.relation), confidence: String(edgeAttrs.confidence) });\n });\n\n outgoing.sort((a, b) => a.target.localeCompare(b.target) || a.relation.localeCompare(b.relation));\n incoming.sort((a, b) => a.source.localeCompare(b.source) || a.relation.localeCompare(b.relation));\n\n return {\n id: match.id,\n label: match.label,\n sourceFile: (attrs.sourceFile as string | undefined) ?? '',\n sourceLocation: (attrs.sourceLocation as string | undefined) ?? '',\n communityLabel: attrs.communityLabel as string | undefined,\n outgoing,\n incoming,\n };\n}\n","import type Graph from 'graphology';\nimport { scoreNodes } from './query.js';\n\n/**\n * `graphify context` — the working-set pack. Instead of returning node\n * names for the agent to chase (query -> then read whole files anyway),\n * this fuses the two steps: the graph knows every entity's file and line,\n * so rank the nodes relevant to the task, read just the relevant line\n * ranges, and pack the actual code into a token budget. One call returns\n * the code an agent needs to start a task.\n */\n\nexport interface ContextSnippet {\n nodeId: string;\n label: string;\n file: string;\n startLine: number;\n endLine: number;\n code: string;\n /** Why this snippet is in the pack (lexical match / neighbor-of relation). */\n reason: string;\n score: number;\n}\n\nexport interface ContextPack {\n task: string;\n snippets: ContextSnippet[];\n /** Approximate tokens used out of the budget. */\n tokens: number;\n tokenBudget: number;\n /** Ranked nodes that didn't fit the budget — the agent can pull them explicitly. */\n overflow: Array<{ nodeId: string; file: string }>;\n}\n\nexport interface ContextOptions {\n /** Approximate token cap for the pack (default 4000). */\n tokenBudget?: number;\n maxSeeds?: number;\n /** Cap on snippet length in lines (default 60). */\n maxSnippetLines?: number;\n}\n\nexport type FileReader = (path: string) => Promise<string>;\n\nconst DEFAULT_CONTEXT_BUDGET = 4000;\nconst DEFAULT_MAX_SEEDS = 8;\nconst DEFAULT_MAX_SNIPPET_LINES = 60;\n/** A neighbor inherits this fraction of the score of the node that pulled it in. */\nconst NEIGHBOR_DECAY = 0.4;\n\nconst tokensOf = (text: string): number => Math.ceil(text.length / 4);\n\nfunction lineNumberOf(sourceLocation: string): number | null {\n const match = /^L(\\d+)$/.exec(sourceLocation);\n return match ? Number(match[1]) : null;\n}\n\nfunction isReadableFile(sourceFile: string): boolean {\n return sourceFile !== '' && !sourceFile.startsWith('<') && !sourceFile.includes('://');\n}\n\ninterface RankedNode {\n id: string;\n score: number;\n reason: string;\n}\n\n/**\n * Rank nodes for the task: lexical seeds first (their match score), then\n * their graph neighbors at a decayed score, labeled with the relation that\n * connects them. Deterministic (score desc, id asc).\n */\nexport function rankForTask(graph: Graph, task: string, maxSeeds = DEFAULT_MAX_SEEDS): RankedNode[] {\n const seeds = scoreNodes(graph, task).slice(0, maxSeeds);\n const ranked = new Map<string, RankedNode>();\n for (const seed of seeds) {\n ranked.set(seed.id, { id: seed.id, score: seed.score, reason: 'matches the task' });\n }\n\n for (const seed of seeds) {\n graph.forEachEdge(seed.id, (_edge, attrs, source, target) => {\n const neighbor = source === seed.id ? target : source;\n if (ranked.has(neighbor)) return;\n const relation = String(attrs.relation);\n const direction = source === seed.id ? `${relation} ->` : `<- ${relation}`;\n ranked.set(neighbor, {\n id: neighbor,\n score: seed.score * NEIGHBOR_DECAY,\n reason: `${direction} ${seed.label}`,\n });\n });\n }\n\n return [...ranked.values()].sort((a, b) => b.score - a.score || a.id.localeCompare(b.id));\n}\n\n/**\n * Extract the snippet for a node: from its declaration line to just before\n * the next known entity in the same file (capped), so snippets align with\n * real code block boundaries without needing a parser here.\n */\nfunction snippetRange(\n startLine: number,\n fileLineCount: number,\n entityStartsInFile: number[],\n maxLines: number,\n): { start: number; end: number } {\n const nextStart = entityStartsInFile.find((l) => l > startLine);\n const hardEnd = nextStart !== undefined ? nextStart - 1 : fileLineCount;\n return { start: startLine, end: Math.min(hardEnd, startLine + maxLines - 1) };\n}\n\nexport async function buildContextPack(\n graph: Graph,\n task: string,\n readFile: FileReader,\n options: ContextOptions = {},\n): Promise<ContextPack> {\n const tokenBudget = options.tokenBudget ?? DEFAULT_CONTEXT_BUDGET;\n const maxSnippetLines = options.maxSnippetLines ?? DEFAULT_MAX_SNIPPET_LINES;\n const ranked = rankForTask(graph, task, options.maxSeeds);\n\n // Every known entity start line per file — used to end snippets at the\n // next declaration instead of an arbitrary window.\n const entityStarts = new Map<string, number[]>();\n graph.forEachNode((_id, attrs) => {\n const file = attrs.sourceFile as string | undefined;\n const line = lineNumberOf((attrs.sourceLocation as string | undefined) ?? '');\n if (file && line !== null) {\n const starts = entityStarts.get(file) ?? [];\n starts.push(line);\n entityStarts.set(file, starts);\n }\n });\n for (const starts of entityStarts.values()) starts.sort((a, b) => a - b);\n\n const fileCache = new Map<string, string[] | null>();\n const readLines = async (file: string): Promise<string[] | null> => {\n const cached = fileCache.get(file);\n if (cached !== undefined) return cached;\n let lines: string[] | null;\n try {\n lines = (await readFile(file)).split('\\n');\n } catch {\n lines = null;\n }\n fileCache.set(file, lines);\n return lines;\n };\n\n const snippets: ContextSnippet[] = [];\n const overflow: Array<{ nodeId: string; file: string }> = [];\n const coveredRanges = new Map<string, Array<{ start: number; end: number }>>();\n let tokens = 0;\n\n for (const node of ranked) {\n const attrs = graph.getNodeAttributes(node.id);\n const file = (attrs.sourceFile as string | undefined) ?? '';\n const startLine = lineNumberOf((attrs.sourceLocation as string | undefined) ?? '');\n if (!isReadableFile(file) || startLine === null) continue;\n\n const lines = await readLines(file);\n if (lines === null) continue;\n\n const { start, end } = snippetRange(startLine, lines.length, entityStarts.get(file) ?? [], maxSnippetLines);\n\n // Skip if an already-packed snippet from the same file covers this range.\n const covered = (coveredRanges.get(file) ?? []).some((r) => start >= r.start && end <= r.end);\n if (covered) continue;\n\n const code = lines.slice(start - 1, end).join('\\n');\n const cost = tokensOf(code) + 15; // header overhead\n if (tokens + cost > tokenBudget) {\n overflow.push({ nodeId: node.id, file: `${file}:L${startLine}` });\n continue;\n }\n\n tokens += cost;\n snippets.push({\n nodeId: node.id,\n label: (attrs.label as string | undefined) ?? node.id,\n file,\n startLine: start,\n endLine: end,\n code,\n reason: node.reason,\n score: node.score,\n });\n const ranges = coveredRanges.get(file) ?? [];\n ranges.push({ start, end });\n coveredRanges.set(file, ranges);\n }\n\n return { task, snippets, tokens, tokenBudget, overflow };\n}\n\n/** Render a pack as agent-ready markdown. */\nexport function renderContextPack(pack: ContextPack): string {\n const lines: string[] = [\n `# Context pack: ${pack.task}`,\n '',\n `_~${pack.tokens} of ${pack.tokenBudget} token budget, ${pack.snippets.length} snippet(s)._`,\n '',\n ];\n\n if (pack.snippets.length === 0) {\n lines.push('No matching code found — try different keywords.');\n return lines.join('\\n');\n }\n\n for (const snippet of pack.snippets) {\n lines.push(`## ${snippet.label} — \\`${snippet.file}:L${snippet.startLine}-L${snippet.endLine}\\``);\n lines.push(`_${snippet.reason}_`);\n lines.push('```');\n lines.push(snippet.code);\n lines.push('```');\n lines.push('');\n }\n\n if (pack.overflow.length > 0) {\n lines.push(`## Didn't fit the budget (${pack.overflow.length})`);\n for (const item of pack.overflow.slice(0, 15)) {\n lines.push(`- ${item.nodeId} (${item.file})`);\n }\n lines.push('');\n }\n\n return lines.join('\\n');\n}\n","import * as path from 'node:path';\nimport type Graph from 'graphology';\nimport { LocalFileGraphStore } from './store/localFile.js';\n\n/**\n * Load a previously-exported graph.json back into a graphology Graph.\n * Thin wrapper over LocalFileGraphStore (which enforces the path-traversal\n * guard — see SECURITY.md) that throws when no graph exists, because every\n * CLI/MCP caller treats \"no graph yet\" as a hard error. Library callers who\n * want the null-on-missing contract should use a GraphStore directly.\n */\nexport async function loadGraph(outDir: string = path.join(process.cwd(), 'graphify-out')): Promise<Graph> {\n const graph = await new LocalFileGraphStore().load(outDir);\n if (graph === null) {\n throw new Error(`No graph found in ${outDir} — run \\`graphify <path>\\` to build one first.`);\n }\n return graph;\n}\n","import * as fs from 'node:fs/promises';\nimport type Graph from 'graphology';\nimport { validateGraphPath } from '../security.js';\nimport { deserializeGraph, serializeGraph } from './serialize.js';\nimport type { GraphStore, SerializedGraph } from './types.js';\n\n/**\n * The reference GraphStore: one graph.json per directory, `ref` = the\n * outDir path. Reproduces the classic graphify-out/ behavior byte-for-byte\n * (same pretty-printed JSON exportGraph() writes) and routes every path\n * through security.validateGraphPath(), so a caller-supplied ref — or a\n * symlinked graph.json — can never read or write outside the directory it\n * resolves to.\n */\nexport class LocalFileGraphStore implements GraphStore {\n async load(ref: string): Promise<Graph | null> {\n let jsonPath: string;\n try {\n jsonPath = validateGraphPath('graph.json', ref);\n } catch (error) {\n // A ref whose directory doesn't exist simply has no graph yet;\n // traversal/symlink-escape rejections must still propagate.\n if ((error as Error).message.startsWith('Base directory does not exist')) return null;\n throw error;\n }\n\n let raw: string;\n try {\n raw = await fs.readFile(jsonPath, 'utf-8');\n } catch (error) {\n if ((error as NodeJS.ErrnoException).code === 'ENOENT') return null;\n throw error;\n }\n return deserializeGraph(JSON.parse(raw) as SerializedGraph);\n }\n\n async save(ref: string, graph: Graph): Promise<void> {\n await fs.mkdir(ref, { recursive: true });\n const jsonPath = validateGraphPath('graph.json', ref);\n await fs.writeFile(jsonPath, JSON.stringify(serializeGraph(graph), null, 2), 'utf-8');\n }\n}\n","/**\n * Security helpers — URL validation, SSRF-guarded fetch, path guards, label\n * sanitization, prompt-injection defenses. Every control listed in\n * SECURITY.md must have a corresponding function here and a unit test in\n * tests/security.test.ts. See the master prompt §5 for the full checklist.\n */\n\nimport { createHash } from 'node:crypto';\nimport * as dns from 'node:dns';\nimport * as http from 'node:http';\nimport * as https from 'node:https';\nimport * as fs from 'node:fs';\nimport * as net from 'node:net';\nimport * as nodePath from 'node:path';\n\nconst ALLOWED_SCHEMES = new Set(['http:', 'https:']);\nconst MAX_FETCH_BYTES = 50 * 1024 * 1024;\nconst MAX_TEXT_BYTES = 10 * 1024 * 1024;\nconst MAX_LABEL_LEN = 256;\nconst MAX_REDIRECTS = 5;\nconst REQUEST_TIMEOUT_MS = 15_000;\n\n/** Hostnames that must never be reachable, regardless of what they resolve to. */\nconst BLOCKED_HOSTNAMES = new Set([\n 'metadata.google.internal',\n 'metadata.internal',\n 'metadata',\n 'metadata.azure.com',\n 'instance-data',\n 'instance-data.ec2.internal',\n]);\n\n// ---------------------------------------------------------------------------\n// IP range checks (SSRF guard core). Implemented without extra dependencies.\n// ---------------------------------------------------------------------------\n\nfunction ipv4ToInt(ip: string): number | null {\n const parts = ip.split('.');\n if (parts.length !== 4) return null;\n let result = 0;\n for (const part of parts) {\n if (!/^\\d{1,3}$/.test(part)) return null;\n const n = Number(part);\n if (n > 255) return null;\n result = (result << 8) | n;\n }\n return result >>> 0;\n}\n\nfunction ipv4InCidr(ipInt: number, cidr: string): boolean {\n const [base, prefixStr] = cidr.split('/');\n const prefix = Number(prefixStr);\n const baseInt = ipv4ToInt(base ?? '');\n if (baseInt === null) return false;\n const mask = prefix === 0 ? 0 : (~0 << (32 - prefix)) >>> 0;\n return (ipInt & mask) >>> 0 === (baseInt & mask) >>> 0;\n}\n\n/** IPv4 ranges that must never be connected to from a server-side fetch. */\nconst BLOCKED_IPV4_CIDRS = [\n '0.0.0.0/8', // \"this\" network\n '10.0.0.0/8', // private\n '100.64.0.0/10', // carrier-grade NAT (CGN)\n '127.0.0.0/8', // loopback\n '169.254.0.0/16', // link-local (covers 169.254.169.254 cloud metadata)\n '172.16.0.0/12', // private\n '192.0.0.0/24', // IETF protocol assignments\n '192.0.2.0/24', // TEST-NET-1\n '192.168.0.0/16', // private\n '198.18.0.0/15', // benchmarking\n '198.51.100.0/24', // TEST-NET-2\n '203.0.113.0/24', // TEST-NET-3\n '224.0.0.0/4', // multicast\n '240.0.0.0/4', // reserved\n '255.255.255.255/32', // broadcast\n];\n\nexport function isForbiddenIpv4(ip: string): boolean {\n const ipInt = ipv4ToInt(ip);\n if (ipInt === null) return true; // malformed -> treat as forbidden, fail closed\n return BLOCKED_IPV4_CIDRS.some((cidr) => ipv4InCidr(ipInt, cidr));\n}\n\n/** Expand a (possibly `::`-compressed) IPv6 address into 8 16-bit groups. */\nfunction expandIpv6(ip: string): number[] | null {\n const withoutZone = ip.split('%')[0] ?? '';\n const parts = withoutZone.split('::');\n if (parts.length > 2) return null;\n\n const head = parts[0] ? parts[0].split(':').filter((s) => s.length > 0) : [];\n const tail = parts.length === 2 && parts[1] ? parts[1].split(':').filter((s) => s.length > 0) : [];\n\n let missing = 0;\n if (parts.length === 2) {\n missing = 8 - head.length - tail.length;\n if (missing < 0) return null;\n } else if (head.length + tail.length !== 8) {\n return null;\n }\n\n const groupsHex = [...head, ...Array(missing).fill('0'), ...tail];\n if (groupsHex.length !== 8) return null;\n\n const groups: number[] = [];\n for (const g of groupsHex) {\n if (!/^[0-9a-fA-F]{1,4}$/.test(g)) return null;\n groups.push(parseInt(g, 16));\n }\n return groups;\n}\n\nexport function isForbiddenIpv6(ip: string): boolean {\n const lower = ip.toLowerCase();\n if (lower === '::1' || lower === '::') return true;\n\n const mapped = /^::ffff:(\\d+\\.\\d+\\.\\d+\\.\\d+)$/.exec(lower);\n if (mapped) return isForbiddenIpv4(mapped[1] as string);\n\n const groups = expandIpv6(lower);\n if (groups === null) return true; // unparsable -> fail closed\n const first = groups[0] as number;\n if ((first & 0xffc0) === 0xfe80) return true; // fe80::/10 link-local\n if ((first & 0xfe00) === 0xfc00) return true; // fc00::/7 unique local\n if ((first & 0xff00) === 0xff00) return true; // ff00::/8 multicast\n if (groups.every((g) => g === 0)) return true; // unspecified/all-zero\n return false;\n}\n\nexport function isForbiddenIp(ip: string): boolean {\n return net.isIPv6(ip) ? isForbiddenIpv6(ip) : isForbiddenIpv4(ip);\n}\n\n// ---------------------------------------------------------------------------\n// URL validation\n// ---------------------------------------------------------------------------\n\n/**\n * Validate a URL is http/https and, when the hostname is itself a literal\n * IP or a known cloud-metadata hostname, reject it synchronously. DNS-bound\n * hostnames are re-validated at connect time by safeFetch() (see below) —\n * this function alone cannot rule out DNS rebinding for a hostname that\n * currently resolves to a public IP.\n */\nexport function validateUrl(url: string): string {\n let parsed: URL;\n try {\n parsed = new URL(url);\n } catch {\n throw new Error(`Invalid URL: ${url}`);\n }\n\n if (!ALLOWED_SCHEMES.has(parsed.protocol)) {\n throw new Error(\n `URL scheme not allowed: \"${parsed.protocol}\" (allowed: ${[...ALLOWED_SCHEMES].join(', ')})`,\n );\n }\n\n const hostname = parsed.hostname.toLowerCase();\n if (BLOCKED_HOSTNAMES.has(hostname)) {\n throw new Error(`URL targets a blocked host: ${hostname}`);\n }\n\n // WHATWG URL keeps the brackets on an IPv6 literal host (e.g. \"[::1]\") —\n // strip them before checking net.isIP()/isForbiddenIp().\n const bareHost =\n hostname.startsWith('[') && hostname.endsWith(']') ? hostname.slice(1, -1) : hostname;\n if (net.isIP(bareHost) && isForbiddenIp(bareHost)) {\n throw new Error(`URL targets a forbidden IP address: ${bareHost}`);\n }\n\n return parsed.toString();\n}\n\n// ---------------------------------------------------------------------------\n// safeFetch — SSRF-guarded, size-capped, redirect-revalidating fetch\n// ---------------------------------------------------------------------------\n\nexport type LookupFn = (hostname: string) => Promise<{ address: string; family: number }>;\n\n/**\n * Resolve a hostname (or pass through a literal IP) and validate the\n * result. `lookup` defaults to the real `dns.promises.lookup` — tests\n * substitute a fake to exercise the rebind guard deterministically without\n * making a real DNS query.\n */\nexport async function resolveAndValidate(\n hostname: string,\n lookup: LookupFn = dns.promises.lookup,\n): Promise<{ address: string; family: number }> {\n if (net.isIP(hostname)) {\n if (isForbiddenIp(hostname)) {\n throw new Error(`Refusing to connect to forbidden IP: ${hostname}`);\n }\n return { address: hostname, family: net.isIPv6(hostname) ? 6 : 4 };\n }\n const result = await lookup(hostname);\n if (isForbiddenIp(result.address)) {\n throw new Error(`Refusing to connect to forbidden IP: ${hostname} -> ${result.address}`);\n }\n return result;\n}\n\nexport interface RawResponse {\n statusCode: number;\n headers: http.IncomingHttpHeaders;\n body: Buffer;\n}\n\n/** Minimal shape of the `http`/`https` modules that requestOnce() needs — narrow on purpose so tests can substitute a fake transport instead of mocking network I/O. */\nexport type RequestTransport = Pick<typeof http, 'request'>;\n\n/**\n * Issue a single HTTP(S) request, connecting to `resolvedAddress` directly\n * (via a `lookup` override) rather than re-resolving DNS at connect time —\n * this is what prevents a DNS-rebind attacker from swapping the target\n * between validation and connection (TOCTOU). Streams the body and aborts\n * once it exceeds the byte cap for its declared content type.\n */\nexport function requestOnce(\n target: URL,\n resolvedAddress: string,\n resolvedFamily: number,\n transport: RequestTransport = target.protocol === 'https:' ? https : http,\n): Promise<RawResponse> {\n return new Promise((resolve, reject) => {\n const req = transport.request(\n {\n protocol: target.protocol,\n hostname: target.hostname,\n host: target.hostname,\n port: target.port || (target.protocol === 'https:' ? 443 : 80),\n path: `${target.pathname}${target.search}`,\n method: 'GET',\n timeout: REQUEST_TIMEOUT_MS,\n headers: { 'user-agent': 'graphify/0.1', accept: '*/*' },\n // Force the connection to the address we already validated — do not\n // let Node re-resolve `target.hostname` at connect time.\n lookup: (\n _hostname: string,\n options: unknown,\n callback: (err: NodeJS.ErrnoException | null, address: string, family: number) => void,\n ) => {\n callback(null, resolvedAddress, resolvedFamily);\n },\n } as http.RequestOptions,\n (res) => {\n const statusCode = res.statusCode ?? 0;\n const contentType = String(res.headers['content-type'] ?? '');\n const isTextual = /^text\\/|json|xml|html|charset=/i.test(contentType);\n const cap = isTextual ? MAX_TEXT_BYTES : MAX_FETCH_BYTES;\n\n const chunks: Buffer[] = [];\n let total = 0;\n let aborted = false;\n\n res.on('data', (chunk: Buffer) => {\n total += chunk.length;\n if (total > cap) {\n aborted = true;\n res.destroy();\n req.destroy();\n reject(new Error(`Response exceeded ${cap} byte cap (content-type: ${contentType})`));\n return;\n }\n chunks.push(chunk);\n });\n res.on('end', () => {\n if (aborted) return;\n resolve({ statusCode, headers: res.headers, body: Buffer.concat(chunks) });\n });\n res.on('error', (err) => {\n if (!aborted) reject(err);\n });\n },\n );\n\n req.on('timeout', () => {\n req.destroy(new Error(`Request timed out after ${REQUEST_TIMEOUT_MS}ms`));\n });\n req.on('error', reject);\n req.end();\n });\n}\n\nexport interface SafeFetchDeps {\n /** Override DNS resolution + IP validation (default: resolveAndValidate). */\n resolve?: (hostname: string) => Promise<{ address: string; family: number }>;\n /** Override the actual request (default: requestOnce against real http/https). */\n request?: (target: URL, address: string, family: number) => Promise<RawResponse>;\n}\n\n/**\n * Fetch a URL with the SSRF guards from validateUrl() plus streaming size\n * caps (MAX_FETCH_BYTES / MAX_TEXT_BYTES) and a hard error on non-2xx\n * status. Every redirect hop is independently validated and re-resolved —\n * a redirect to a private/loopback/metadata address is rejected exactly\n * like a direct request would be.\n *\n * `deps` exists so tests can substitute the DNS/network collaborators with\n * deterministic fakes (see tests/security.test.ts) instead of mocking\n * Node's core modules or hitting real sockets — production callers should\n * never need to pass it.\n */\nexport async function safeFetch(url: string, deps: SafeFetchDeps = {}): Promise<Buffer> {\n const resolve = deps.resolve ?? resolveAndValidate;\n const doRequest = deps.request ?? requestOnce;\n\n let current = validateUrl(url);\n for (let hop = 0; hop <= MAX_REDIRECTS; hop++) {\n const target = new URL(current);\n const { address, family } = await resolve(target.hostname);\n const response = await doRequest(target, address, family);\n\n if (response.statusCode >= 300 && response.statusCode < 400) {\n const location = response.headers.location;\n if (!location) {\n throw new Error(`Redirect response (${response.statusCode}) missing Location header`);\n }\n const next = new URL(location, target).toString();\n current = validateUrl(next);\n continue;\n }\n\n if (response.statusCode < 200 || response.statusCode >= 300) {\n throw new Error(`Non-2xx response: ${response.statusCode}`);\n }\n\n return response.body;\n }\n throw new Error(`Too many redirects (> ${MAX_REDIRECTS}) while fetching ${url}`);\n}\n\n// ---------------------------------------------------------------------------\n// Database DSN validation (MySQL schema extraction)\n// ---------------------------------------------------------------------------\n\nexport interface ValidatedDsn {\n /**\n * Credential-free rendering (`mysql://host:port/db`). This is the ONLY\n * form that may ever appear in graph nodes, reports, logs, or errors —\n * the password exists solely inside `connection`.\n */\n safeDisplay: string;\n connection: {\n host: string;\n port: number;\n user: string;\n password: string;\n database: string;\n };\n}\n\nconst DEFAULT_MYSQL_PORT = 3306;\n\n/**\n * Parse and validate a `mysql://user:pass@host:port/database` DSN.\n *\n * Deliberately does NOT apply the SSRF IP blocklist that safeFetch()\n * enforces: connecting to a localhost/private-network database is the\n * primary legitimate use of an explicitly user-supplied DSN, unlike a URL\n * scraped out of corpus content. The security property that matters here\n * is credential containment — see ValidatedDsn.safeDisplay.\n */\nexport function validateDsn(dsn: string): ValidatedDsn {\n let parsed: URL;\n try {\n parsed = new URL(dsn);\n } catch {\n throw new Error('Invalid DSN — expected mysql://user:pass@host:port/database');\n }\n\n if (parsed.protocol !== 'mysql:') {\n throw new Error(`DSN scheme not supported: \"${parsed.protocol}\" (only mysql: is supported)`);\n }\n\n const database = decodeURIComponent(parsed.pathname.replace(/^\\//, ''));\n if (!database || database.includes('/')) {\n throw new Error('DSN must name exactly one database, e.g. mysql://localhost:3306/mydb');\n }\n if (!parsed.hostname) {\n throw new Error('DSN must include a host, e.g. mysql://localhost:3306/mydb');\n }\n\n const port = parsed.port ? Number(parsed.port) : DEFAULT_MYSQL_PORT;\n return {\n safeDisplay: `mysql://${parsed.hostname}:${port}/${database}`,\n connection: {\n host: parsed.hostname,\n port,\n user: decodeURIComponent(parsed.username) || 'root',\n password: decodeURIComponent(parsed.password),\n database,\n },\n };\n}\n\n// ---------------------------------------------------------------------------\n// Path traversal guard\n// ---------------------------------------------------------------------------\n\n/**\n * Resolve `path` and require it stays inside `base` (defaults to\n * `<cwd>/graphify-out`). `base` must already exist. Throws on traversal\n * attempts (`../`, absolute escapes, symlink-resolved escapes).\n */\nexport function validateGraphPath(path: string, base?: string): string {\n const baseDir = base ?? nodePath.join(process.cwd(), 'graphify-out');\n\n let resolvedBase: string;\n try {\n resolvedBase = fs.realpathSync(baseDir);\n } catch {\n throw new Error(`Base directory does not exist: ${baseDir}`);\n }\n\n const candidate = nodePath.isAbsolute(path) ? path : nodePath.join(resolvedBase, path);\n const resolvedCandidate = nodePath.resolve(candidate);\n\n // Resolve symlinks for whichever is the deepest existing ancestor, so a\n // symlink inside an otherwise-valid path can't escape the base dir either.\n let realCandidate = resolvedCandidate;\n try {\n realCandidate = fs.realpathSync(resolvedCandidate);\n } catch {\n // Path (or part of it) may not exist yet (e.g. a file we're about to\n // write) — fall back to the lexically-resolved path for the check.\n }\n\n const relative = nodePath.relative(resolvedBase, realCandidate);\n const escapes = relative === '..' || relative.startsWith(`..${nodePath.sep}`) || nodePath.isAbsolute(relative);\n if (escapes) {\n throw new Error(`Path escapes graphify-out/: ${path}`);\n }\n\n return realCandidate;\n}\n\n// ---------------------------------------------------------------------------\n// Label sanitization (unchanged reference implementation) + HTML escaping\n// ---------------------------------------------------------------------------\n\n/** Strip control characters, cap length. Apply to every node/edge label. */\nexport function sanitizeLabel(text: string | null | undefined): string {\n if (text == null) return '';\n // eslint-disable-next-line no-control-regex -- intentional: stripping raw control chars\n const stripped = String(text).replace(/[\\x00-\\x1f\\x7f]/g, '');\n return stripped.slice(0, MAX_LABEL_LEN);\n}\n\n/**\n * HTML-escape a string. Callers must run sanitizeLabel() first for\n * length/control-char stripping, then escapeHtml() immediately before\n * embedding into graph.html (XSS control, see SECURITY.md).\n */\nexport function escapeHtml(text: string): string {\n return text\n .replace(/&/g, '&')\n .replace(/</g, '<')\n .replace(/>/g, '>')\n .replace(/\"/g, '"')\n .replace(/'/g, ''');\n}\n\n// ---------------------------------------------------------------------------\n// Prompt-injection defenses for LLM-bound file content\n// ---------------------------------------------------------------------------\n\n/**\n * Known jailbreak / chat-template sentinels that must never reach a prompt\n * unescaped, whether they occur in source files or are forged to spoof our\n * own <untrusted_source> delimiter.\n */\nconst SENTINEL_PATTERNS: RegExp[] = [\n /<\\|im_start\\|>/gi,\n /<\\|im_end\\|>/gi,\n /<\\|system\\|>/gi,\n /\\[INST\\]/gi,\n /\\[\\/INST\\]/gi,\n /<<SYS>>/gi,\n /<<\\/SYS>>/gi,\n /<\\/untrusted_source>/gi,\n /<untrusted_source/gi,\n];\n\n/**\n * Render brackets as fullwidth lookalikes (< > [ ]) so the defanged\n * echo is human-visible but no longer byte-identical to the original\n * sentinel — a naive downstream scan for the literal delimiter/sentinel\n * text will not re-match our own \"we found one\" marker.\n */\nfunction toFullwidthBrackets(text: string): string {\n return text\n .replace(/</g, '<')\n .replace(/>/g, '>')\n .replace(/\\[/g, '[')\n .replace(/\\]/g, ']');\n}\n\n/**\n * Replace known jailbreak/chat-template sentinels with a visibly defanged\n * form — never silently stripped (silent stripping can itself be an\n * injection vector if it changes meaning unexpectedly).\n */\nfunction defangSentinels(content: string): string {\n let result = content;\n for (const pattern of SENTINEL_PATTERNS) {\n result = result.replace(pattern, (match) => `[DEFANGED:${toFullwidthBrackets(match)}]`);\n }\n return result;\n}\n\n/**\n * Wrap file content for LLM prompts in a hash-stamped untrusted-source\n * delimiter and neutralize known jailbreak/chat-template sentinels. This\n * raises the bar against prompt injection; it does not eliminate it —\n * document that plainly wherever this is referenced.\n */\nexport function wrapUntrustedSource(path: string, content: string): string {\n const sha256 = createHash('sha256').update(content, 'utf8').digest('hex');\n const safePath = escapeHtml(sanitizeLabel(path));\n const defanged = defangSentinels(content);\n return `<untrusted_source path=\"${safePath}\" sha256=\"${sha256}\">\\n${defanged}\\n</untrusted_source>`;\n}\n","import Graph from 'graphology';\nimport type { SerializedGraph } from './types.js';\n\n/**\n * The stable serialization contract for graphs at rest: graphology's\n * node-link JSON. exportGraph() has always written exactly this shape to\n * graph.json, so anything serialized here stays loadable by every existing\n * consumer (and vice versa).\n */\nexport function serializeGraph(graph: Graph): SerializedGraph {\n return graph.export();\n}\n\nexport function deserializeGraph(data: SerializedGraph): Graph {\n return Graph.from(data);\n}\n","import type Graph from 'graphology';\nimport { resolveNode, type NodeMatch } from './query.js';\nimport type { Confidence, Relation } from './types.js';\n\n/**\n * Reverse impact analysis — \"what breaks if I change X\". Walks *incoming*\n * dependency edges (who calls / imports / inherits from the target)\n * transitively, so depth 1 is the direct blast radius and deeper levels are\n * ripple effects. Like everything in query.ts this is lexical + structural,\n * fully offline and deterministic.\n */\n\n/**\n * Relations where `source --rel--> target` means \"source depends on target\",\n * i.e. changing the target can break the source. `contains`/`method` are\n * included because changing an entity plausibly affects its container file\n * or class signature consumers see.\n */\nconst DEPENDENCY_RELATIONS: ReadonlySet<Relation> = new Set([\n 'calls',\n 'imports',\n 'imports_from',\n 'inherits',\n 'implements',\n 'mixes_in',\n 'embeds',\n 'references',\n 're_exports',\n 'method',\n 'contains',\n] satisfies Relation[]);\n\nexport interface AffectedNode {\n id: string;\n label: string;\n sourceFile: string;\n sourceLocation: string;\n /** 1 = depends on the target directly, 2 = one step removed, ... */\n depth: number;\n /** The dependency edge that pulled this node in (this node -> something already affected). */\n via: {\n relation: Relation;\n confidence: Confidence;\n /** The already-affected node this one depends on. */\n dependsOn: string;\n };\n}\n\nexport interface ImpactResult {\n target: NodeMatch;\n affected: AffectedNode[];\n /** How many affected nodes there are per confidence tier of the edge that pulled each one in. */\n byConfidence: Record<Confidence, number>;\n maxDepth: number;\n truncated: boolean;\n}\n\nexport interface ImpactOptions {\n /** How many reverse hops to follow (default 3). */\n maxDepth?: number;\n /** Hard cap on affected nodes reported (default 200). */\n limit?: number;\n /** Restrict the traversal to these relations (default: every dependency-carrying relation). */\n relations?: Relation[];\n}\n\nconst DEFAULT_IMPACT_DEPTH = 3;\nconst DEFAULT_IMPACT_LIMIT = 200;\n\nconst CONFIDENCE_RANK: Record<Confidence, number> = { EXTRACTED: 3, INFERRED: 2, AMBIGUOUS: 1 };\n\n/**\n * Resolve `nodeQuery` to its best-matching node, then reverse-BFS over\n * incoming dependency edges. Returns null when nothing in the graph matches\n * the query at all (same contract as explainNode()).\n */\nexport function affectedBy(graph: Graph, nodeQuery: string, options: ImpactOptions = {}): ImpactResult | null {\n const target = resolveNode(graph, nodeQuery);\n if (!target) return null;\n\n const maxDepth = options.maxDepth ?? DEFAULT_IMPACT_DEPTH;\n const limit = options.limit ?? DEFAULT_IMPACT_LIMIT;\n const followed: ReadonlySet<Relation> = options.relations ? new Set(options.relations) : DEPENDENCY_RELATIONS;\n\n const affected: AffectedNode[] = [];\n const visited = new Set<string>([target.id]);\n let frontier: string[] = [target.id];\n let truncated = false;\n\n for (let depth = 1; depth <= maxDepth && frontier.length > 0 && !truncated; depth++) {\n // Collect every dependent of the current frontier before descending, so\n // each node's reported depth is its *shortest* reverse distance.\n const nextFrontier = new Map<string, AffectedNode>();\n\n for (const current of [...frontier].sort((a, b) => a.localeCompare(b))) {\n graph.forEachInEdge(current, (_edge, edgeAttrs, source) => {\n if (visited.has(source)) return;\n const relation = edgeAttrs.relation as Relation;\n if (!followed.has(relation)) return;\n\n const confidence = edgeAttrs.confidence as Confidence;\n const existing = nextFrontier.get(source);\n // Same node reachable via several edges this round: keep the\n // strongest-confidence one, mirroring buildGraph()'s merge rule.\n if (existing && CONFIDENCE_RANK[existing.via.confidence] >= CONFIDENCE_RANK[confidence]) return;\n\n const attrs = graph.getNodeAttributes(source);\n nextFrontier.set(source, {\n id: source,\n label: (attrs.label as string | undefined) ?? source,\n sourceFile: (attrs.sourceFile as string | undefined) ?? '',\n sourceLocation: (attrs.sourceLocation as string | undefined) ?? '',\n depth,\n via: { relation, confidence, dependsOn: current },\n });\n });\n }\n\n const roundNodes = [...nextFrontier.values()].sort((a, b) => a.id.localeCompare(b.id));\n for (const node of roundNodes) {\n if (affected.length >= limit) {\n truncated = true;\n break;\n }\n visited.add(node.id);\n affected.push(node);\n }\n frontier = roundNodes.filter((n) => visited.has(n.id)).map((n) => n.id);\n }\n\n const byConfidence: Record<Confidence, number> = { EXTRACTED: 0, INFERRED: 0, AMBIGUOUS: 0 };\n for (const node of affected) byConfidence[node.via.confidence] += 1;\n\n return { target, affected, byConfidence, maxDepth, truncated };\n}\n","import type Graph from 'graphology';\nimport { affectedBy } from './impact.js';\nimport { resolveNode } from './query.js';\nimport type { Relation } from './types.js';\n\n/**\n * `graphify tests` — structural test selection. Coverage tools need\n * instrumentation and a full test run to know what covers what; the graph\n * already knows, statically: a test file imports/calls the code it\n * exercises, so the test files inside a change's reverse blast radius are\n * exactly the ones worth running. Cross-language, no instrumentation.\n */\n\n/** Test-file naming conventions across the supported languages. */\nconst TEST_FILE_PATTERNS: RegExp[] = [\n /(^|\\/)tests?\\//, // tests/ or test/ directory\n /(^|\\/)__tests__\\//,\n /(^|\\/)spec\\//,\n /\\.test\\.[cm]?[jt]sx?$/,\n /\\.spec\\.[cm]?[jt]sx?$/,\n /(^|\\/)test_[^/]+\\.py$/,\n /_test\\.py$/,\n /_test\\.go$/,\n /Tests?\\.(java|cs)$/,\n /_spec\\.rb$/,\n /_test\\.rb$/,\n];\n\nexport function isTestFile(path: string): boolean {\n return TEST_FILE_PATTERNS.some((p) => p.test(path));\n}\n\nexport interface TestSelection {\n /** What the selection was computed for (resolved node id, or the changed files). */\n target: string;\n /** Unique test files, most directly affected first. */\n testFiles: Array<{ file: string; depth: number; via: string }>;\n /** How many non-test nodes were in the blast radius (context for confidence). */\n affectedNodeCount: number;\n /**\n * `usage` — selected by following actual use (calls/inheritance), tight;\n * `import-reachability` — the broad fallback: anything that can reach the\n * target through imports/re-exports, which over-includes files that share\n * a barrel entry with the target.\n */\n precision: 'usage' | 'import-reachability';\n}\n\nconst TEST_REACH_DEPTH = 4;\nconst TEST_REACH_LIMIT = 2000;\n\n/** Relations that mean \"actually uses it\", as opposed to \"can merely reach it through an import\". */\nconst USAGE_RELATIONS: Relation[] = ['calls', 'inherits', 'implements', 'mixes_in', 'embeds', 'references'];\n\nfunction selectionFromImpact(\n graph: Graph,\n targetIds: string[],\n targetLabel: string,\n relations?: Relation[],\n): TestSelection {\n const best = new Map<string, { depth: number; via: string }>();\n let affectedNodeCount = 0;\n\n for (const id of targetIds) {\n const impact = affectedBy(graph, id, { maxDepth: TEST_REACH_DEPTH, limit: TEST_REACH_LIMIT, relations });\n if (!impact) continue;\n affectedNodeCount += impact.affected.length;\n for (const node of impact.affected) {\n if (!isTestFile(node.sourceFile)) continue;\n const existing = best.get(node.sourceFile);\n if (!existing || node.depth < existing.depth) {\n best.set(node.sourceFile, { depth: node.depth, via: node.via.dependsOn });\n }\n }\n }\n\n const testFiles = [...best.entries()]\n .map(([file, info]) => ({ file, depth: info.depth, via: info.via }))\n .sort((a, b) => a.depth - b.depth || a.file.localeCompare(b.file));\n\n return {\n target: targetLabel,\n testFiles,\n affectedNodeCount,\n precision: relations ? 'usage' : 'import-reachability',\n };\n}\n\n/** A file node's entities (so a usage-relation traversal has call targets to start from). */\nfunction entityRootsOf(graph: Graph, id: string): string[] {\n if (id.includes('::')) return [id];\n const roots: string[] = [];\n graph.forEachOutEdge(id, (_edge, attrs, _source, target) => {\n if (String(attrs.relation) === 'contains') roots.push(target);\n });\n roots.sort((a, b) => a.localeCompare(b));\n return roots.length > 0 ? roots : [id];\n}\n\n/**\n * Test files reachable (reverse) from the node best matching `nodeQuery`.\n * Tries a tight usage-relation pass first (tests that actually CALL the\n * target — precise even when many files share a barrel entry point); falls\n * back to full import-reachability when usage finds nothing. Null if\n * nothing matches the query.\n */\nexport function testsForNode(graph: Graph, nodeQuery: string): TestSelection | null {\n const match = resolveNode(graph, nodeQuery);\n if (!match) return null;\n\n const precise = selectionFromImpact(graph, entityRootsOf(graph, match.id), match.id, USAGE_RELATIONS);\n if (precise.testFiles.length > 0) return precise;\n\n return selectionFromImpact(graph, [match.id], match.id);\n}\n\n/**\n * Test files reachable from any of the given changed files (as reported by\n * `git diff --name-only`). Changed test files select themselves. Unknown\n * files (not in the graph) are skipped.\n */\nexport function testsForChangedFiles(graph: Graph, changedFiles: string[]): TestSelection {\n const fileIds: string[] = [];\n const selfSelected = new Map<string, { depth: number; via: string }>();\n\n for (const file of changedFiles) {\n if (isTestFile(file)) {\n selfSelected.set(file, { depth: 0, via: '(changed directly)' });\n continue;\n }\n if (graph.hasNode(file)) fileIds.push(file);\n }\n\n const label = changedFiles.join(', ');\n const preciseRoots = fileIds.flatMap((f) => entityRootsOf(graph, f));\n let selection = selectionFromImpact(graph, preciseRoots, label, USAGE_RELATIONS);\n if (selection.testFiles.length === 0) {\n selection = selectionFromImpact(graph, fileIds, label);\n }\n for (const [file, info] of selfSelected) {\n if (!selection.testFiles.some((t) => t.file === file)) {\n selection.testFiles.push({ file, depth: info.depth, via: info.via });\n }\n }\n selection.testFiles.sort((a, b) => a.depth - b.depth || a.file.localeCompare(b.file));\n return selection;\n}\n"],"mappings":"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","names":["fs","path","path","DEFAULT_MAX_SEEDS","readFile","fs","path","relative","Graph","path"]}
|
|
1
|
+
{"version":3,"sources":["../../src/mcp/server.ts","../../src/query.ts","../../src/context.ts","../../src/graphStore.ts","../../src/store/localFile.ts","../../src/security.ts","../../src/store/serialize.ts","../../src/impact.ts","../../src/testmap.ts"],"sourcesContent":["/**\n * MCP stdio server exposing query/path/explain as tools against an existing\n * graphify-out/graph.json. Stdio only — never opens a network listener.\n * All graph file paths go through security.validateGraphPath().\n *\n * This module is a pure library: importing it has no side effects (it does\n * not read argv or start a server on import). `startServer()` is called\n * explicitly by `bin/graphify-mcp.js` (the standalone `graphify-mcp` CLI\n * entry point) and by `graphify --mcp` (see src/cli/index.ts).\n */\nimport * as fs from 'node:fs/promises';\nimport * as path from 'node:path';\nimport { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';\nimport { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';\nimport type { Transport } from '@modelcontextprotocol/sdk/shared/transport.js';\nimport { z } from 'zod';\nimport { buildContextPack, renderContextPack } from '../context.js';\nimport { loadGraph } from '../graphStore.js';\nimport { affectedBy } from '../impact.js';\nimport { explainNode, queryGraph, shortestPath } from '../query.js';\nimport { testsForNode } from '../testmap.js';\nimport { validateGraphPath } from '../security.js';\n\nfunction textResult(text: string): { content: Array<{ type: 'text'; text: string }> } {\n return { content: [{ type: 'text', text }] };\n}\n\n/**\n * Build the McpServer instance (tools registered, nothing connected yet).\n * Split out from startServer() so tests can connect it to an in-memory\n * transport instead of real stdio (see tests/mcp/server.test.ts).\n */\nexport function createGraphifyMcpServer(outDir: string): McpServer {\n const server = new McpServer({ name: 'graphify', version: '0.1.0' });\n\n server.registerTool(\n 'query',\n {\n description:\n 'BFS (default) or DFS traversal of the graphify knowledge graph, seeded from nodes ' +\n 'whose label matches the question. Returns the matched seeds, the visited node ' +\n 'context, and the edges among them — broad context for answering a structural question.',\n inputSchema: {\n question: z.string().describe('Natural-language question or keywords to search the graph for.'),\n dfs: z.boolean().optional().describe('Use depth-first traversal instead of the default breadth-first.'),\n budget: z.number().int().positive().optional().describe('Hard cap on how many nodes to include.'),\n tokenBudget: z\n .number()\n .int()\n .positive()\n .optional()\n .describe('Approximate cap, in tokens, on the serialized size of the result (default 2000).'),\n },\n },\n async ({ question, dfs, budget, tokenBudget }) => {\n const graph = await loadGraph(outDir);\n const result = queryGraph(graph, question, { dfs, budget, tokenBudget });\n return textResult(JSON.stringify(result, null, 2));\n },\n );\n\n server.registerTool(\n 'path',\n {\n description: 'Shortest path between two named nodes in the graphify knowledge graph.',\n inputSchema: {\n from: z.string().describe('Name/label of the starting node.'),\n to: z.string().describe('Name/label of the destination node.'),\n },\n },\n async ({ from, to }) => {\n const graph = await loadGraph(outDir);\n const result = shortestPath(graph, from, to);\n if (!result) {\n return textResult(`Could not resolve \"${from}\" and/or \"${to}\" to a node in the graph.`);\n }\n return textResult(JSON.stringify(result, null, 2));\n },\n );\n\n server.registerTool(\n 'explain',\n {\n description:\n 'Plain-language-ready explanation of a single node: its source location, community, ' +\n 'and incoming/outgoing edges.',\n inputSchema: {\n node: z.string().describe('Name/label (or id) of the node to explain.'),\n },\n },\n async ({ node }) => {\n const graph = await loadGraph(outDir);\n const result = explainNode(graph, node);\n if (!result) {\n return textResult(`No node matched \"${node}\".`);\n }\n return textResult(JSON.stringify(result, null, 2));\n },\n );\n\n server.registerTool(\n 'affected',\n {\n description:\n 'Reverse impact analysis — \"what breaks if I change X\". Walks incoming dependency ' +\n 'edges (calls/imports/inherits/...) transitively and returns everything that depends ' +\n 'on the node, grouped by depth, with per-confidence blast-radius counts.',\n inputSchema: {\n node: z.string().describe('Name/label (or id) of the node being changed.'),\n depth: z.number().int().positive().optional().describe('How many reverse hops to follow (default 3).'),\n limit: z.number().int().positive().optional().describe('Cap on affected nodes reported (default 200).'),\n },\n },\n async ({ node, depth, limit }) => {\n const graph = await loadGraph(outDir);\n const result = affectedBy(graph, node, { maxDepth: depth, limit });\n if (!result) {\n return textResult(`No node matched \"${node}\".`);\n }\n return textResult(JSON.stringify(result, null, 2));\n },\n );\n\n server.registerTool(\n 'context',\n {\n description:\n 'Token-budgeted working-set pack: the actual code snippets relevant to a task, ' +\n 'graph-ranked and packed to a budget. Call this FIRST when starting a task — it ' +\n 'replaces query-then-read-whole-files.',\n inputSchema: {\n task: z.string().describe('What you are about to do, in natural language.'),\n budget: z.number().int().positive().optional().describe('Approximate token cap (default 4000).'),\n },\n },\n async ({ task, budget }) => {\n const graph = await loadGraph(outDir);\n const pack = await buildContextPack(graph, task, (p) => fs.readFile(p, 'utf-8'), {\n tokenBudget: budget,\n });\n return textResult(renderContextPack(pack));\n },\n );\n\n server.registerTool(\n 'tests',\n {\n description:\n 'Structural test selection: the minimal test files worth running to verify a change ' +\n 'to the given node, via reverse dependency reachability — no coverage instrumentation.',\n inputSchema: {\n node: z.string().describe('Name/label (or id) of the changed node.'),\n },\n },\n async ({ node }) => {\n const graph = await loadGraph(outDir);\n const result = testsForNode(graph, node);\n if (!result) {\n return textResult(`No node matched \"${node}\".`);\n }\n return textResult(JSON.stringify(result, null, 2));\n },\n );\n\n return server;\n}\n\n/**\n * Start the MCP server, serving `query`/`path`/`explain` tools against the\n * graph.json found in `graphPath`'s directory. `graphPath` is validated\n * (via security.validateGraphPath()) up front so a bad path fails\n * immediately with a clear error instead of the first time a client calls\n * a tool. `transport` defaults to real stdio; tests substitute an\n * in-memory transport.\n */\nexport async function startServer(graphPath: string, transport: Transport = new StdioServerTransport()): Promise<void> {\n const outDir = path.dirname(path.resolve(graphPath));\n const fileName = path.basename(graphPath);\n // Fail fast (and clearly) if the graph path escapes/doesn't exist, rather\n // than only surfacing that the first time a tool is called.\n validateGraphPath(fileName, outDir);\n\n const server = createGraphifyMcpServer(outDir);\n await server.connect(transport);\n}\n\n/** Entry point used by bin/graphify-mcp.js. */\nexport async function main(argv: string[] = process.argv): Promise<void> {\n const graphPath = argv[2] ?? path.join(process.cwd(), 'graphify-out', 'graph.json');\n await startServer(graphPath);\n}\n","import type Graph from 'graphology';\n\n/**\n * Library-level graph queries shared by the CLI (`graphify query/path/\n * explain`) and the MCP server tools of the same name. None of this is an\n * LLM call — it's lexical node matching + graph traversal, so it works\n * fully offline and deterministically.\n */\n\nconst STOPWORDS = new Set([\n 'a', 'an', 'the', 'is', 'are', 'was', 'were', 'do', 'does', 'did', 'how',\n 'what', 'where', 'when', 'why', 'who', 'which', 'to', 'of', 'in', 'on',\n 'for', 'and', 'or', 'this', 'that', 'it', 'does', 'that\\'s',\n]);\n\n/** Insert spaces at camelCase boundaries so identifier words become separate tokens. */\nfunction splitCamelCase(text: string): string {\n return text\n .replace(/([a-z0-9])([A-Z])/g, '$1 $2')\n .replace(/([A-Z]+)([A-Z][a-z])/g, '$1 $2');\n}\n\nfunction tokenize(text: string): string[] {\n return splitCamelCase(text)\n .toLowerCase()\n .split(/[^a-z0-9]+/)\n .filter((token) => token.length > 1 && !STOPWORDS.has(token));\n}\n\n/**\n * Split an identifier-ish string (node label/id) into its word subtokens:\n * camelCase, snake_case, kebab-case, dots, and path separators all become\n * boundaries, so `src/a/fileStorage.js::saveAttachment` yields\n * `src, file, storage, js, save, attachment`.\n */\nfunction subtokenize(text: string): string[] {\n return splitCamelCase(text)\n .toLowerCase()\n .split(/[^a-z0-9]+/)\n .filter((token) => token.length > 1);\n}\n\n/** Don't run the fuzzy tier on tokens this short — too many false positives. */\nconst FUZZY_MIN_TOKEN_LEN = 3;\n/** Minimum similarity for a fuzzy match to count at all. */\nconst FUZZY_THRESHOLD = 0.7;\nconst SUBTOKEN_MATCH_SCORE = 1;\nconst SUBSTRING_MATCH_SCORE = 0.6;\nconst FUZZY_MATCH_WEIGHT = 0.5;\n\n/**\n * Optimal-string-alignment Damerau-Levenshtein distance. Chosen over\n * bigram-set similarity because the common code-search typo is a\n * transposition (\"sorceNodes\"), which OSA counts as one edit but bigram\n * overlap punishes brutally.\n */\nfunction osaDistance(a: string, b: string): number {\n let prev2: number[] = [];\n let prev: number[] = Array.from({ length: b.length + 1 }, (_, j) => j);\n let curr: number[] = new Array<number>(b.length + 1).fill(0);\n\n for (let i = 1; i <= a.length; i++) {\n curr[0] = i;\n for (let j = 1; j <= b.length; j++) {\n const substitution = a[i - 1] === b[j - 1] ? 0 : 1;\n let best = Math.min(\n (prev[j] as number) + 1,\n (curr[j - 1] as number) + 1,\n (prev[j - 1] as number) + substitution,\n );\n if (i > 1 && j > 1 && a[i - 1] === b[j - 2] && a[i - 2] === b[j - 1]) {\n best = Math.min(best, (prev2[j - 2] as number) + 1);\n }\n curr[j] = best;\n }\n [prev2, prev, curr] = [prev, curr, new Array<number>(b.length + 1).fill(0)];\n }\n return prev[b.length] as number;\n}\n\n/** Normalized similarity in [0, 1]; short-circuits pairs whose length gap alone puts them under FUZZY_THRESHOLD. */\nfunction fuzzySimilarity(a: string, b: string): number {\n if (a === b) return 1;\n const maxLen = Math.max(a.length, b.length);\n if (maxLen === 0) return 1;\n if (Math.abs(a.length - b.length) / maxLen > 1 - FUZZY_THRESHOLD) return 0;\n return 1 - osaDistance(a, b) / maxLen;\n}\n\nexport interface NodeMatch {\n id: string;\n label: string;\n score: number;\n}\n\n/**\n * Score every node against the query's tokens. Identifier-aware and\n * typo-tolerant, in three tiers per token (best tier wins):\n *\n * 1. exact subtoken match (camelCase/snake_case-split word of the label\n * or id) — \"storage\" matches `fileStorage.js`;\n * 2. plain substring of the label/id (the original v1 behavior);\n * 3. fuzzy (Damerau-Levenshtein) match against a subtoken — \"sorce\"\n * still finds `scoreNodes`.\n *\n * Still fully lexical and deterministic — no LLM, no randomness.\n */\nexport function scoreNodes(graph: Graph, query: string): NodeMatch[] {\n const tokens = tokenize(query);\n const matches: NodeMatch[] = [];\n\n graph.forEachNode((id, attributes) => {\n const label = (attributes.label as string | undefined) ?? id;\n const haystack = `${label} ${id}`.toLowerCase();\n const subtokens = new Set(subtokenize(`${label} ${id}`));\n let score = 0;\n for (const token of tokens) {\n if (subtokens.has(token)) {\n score += SUBTOKEN_MATCH_SCORE;\n continue;\n }\n if (haystack.includes(token)) {\n score += SUBSTRING_MATCH_SCORE;\n continue;\n }\n if (token.length >= FUZZY_MIN_TOKEN_LEN) {\n let best = 0;\n for (const subtoken of subtokens) {\n const similarity = fuzzySimilarity(token, subtoken);\n if (similarity > best) best = similarity;\n }\n if (best >= FUZZY_THRESHOLD) score += best * FUZZY_MATCH_WEIGHT;\n }\n }\n if (score > 0) matches.push({ id, label, score });\n });\n\n // On equal score, real source nodes beat module:/external: placeholders —\n // \"devtools\" should resolve to the devtools middleware, not the\n // @redux-devtools package node that happens to share a subtoken.\n const placeholderRank = (id: string): number =>\n id.startsWith('module:') || id.startsWith('external:') ? 1 : 0;\n matches.sort(\n (a, b) =>\n b.score - a.score || placeholderRank(a.id) - placeholderRank(b.id) || a.id.localeCompare(b.id),\n );\n return matches;\n}\n\n/**\n * Best-effort single-node resolution: the highest-scoring lexical match,\n * or null if nothing in the graph matches at all. Deliberately does *not*\n * fall back to \"the most connected node\" — path/explain name a specific\n * node by intent, and silently substituting an unrelated one would be\n * more misleading than clearly saying \"no match\".\n */\nexport function resolveNode(graph: Graph, query: string): NodeMatch | null {\n const matches = scoreNodes(graph, query);\n return matches[0] ?? null;\n}\n\nexport interface VisitedNode {\n id: string;\n label: string;\n sourceFile: string;\n sourceLocation: string;\n depth: number;\n}\n\nexport interface TraversalEdge {\n source: string;\n target: string;\n relation: string;\n confidence: string;\n}\n\nexport interface QueryResult {\n seeds: NodeMatch[];\n visited: VisitedNode[];\n edges: TraversalEdge[];\n}\n\nexport interface QueryOptions {\n dfs?: boolean;\n /** Hard cap on how many nodes to include. Defaults to a generous ceiling derived from `tokenBudget`. */\n budget?: number;\n /** Approximate cap, in tokens (~4 chars each), on the serialized size of the result. Default 2000. */\n tokenBudget?: number;\n maxDepth?: number;\n maxSeeds?: number;\n}\n\nconst DEFAULT_MAX_DEPTH = 2;\nconst DEFAULT_MAX_SEEDS = 5;\nconst DEFAULT_BUDGET = 40;\nconst DEFAULT_TOKEN_BUDGET = 2000;\n/** When only tokenBudget is given, allow up to this many nodes per budgeted token-decile as a safety ceiling. */\nconst NODES_PER_TOKEN = 1 / 10;\n\n/**\n * Estimated token contribution of one node to the rendered result: its own\n * line (label, file, location) plus a rough allowance for the edge lines\n * that mention its id. Chars/4 is the usual serviceable approximation.\n */\nexport function nodeTokenCost(graph: Graph, id: string): number {\n const attrs = graph.getNodeAttributes(id);\n const label = (attrs.label as string | undefined) ?? id;\n const sourceFile = (attrs.sourceFile as string | undefined) ?? '';\n return Math.ceil((id.length * 2 + label.length + sourceFile.length + 24) / 4);\n}\n\n/**\n * BFS (default, broad context) or DFS (--dfs, trace a specific path)\n * traversal from the nodes whose label best matches `question`'s\n * keywords. This is lexical retrieval, not natural-language\n * understanding — it surfaces the neighborhood of the graph an agent (or\n * a human) should look at to answer the question, it does not itself\n * compose an answer.\n */\nexport function queryGraph(graph: Graph, question: string, options: QueryOptions = {}): QueryResult {\n const maxDepth = options.maxDepth ?? DEFAULT_MAX_DEPTH;\n const maxSeeds = options.maxSeeds ?? DEFAULT_MAX_SEEDS;\n const tokenBudget = options.tokenBudget ?? DEFAULT_TOKEN_BUDGET;\n const budget = options.budget ?? Math.max(DEFAULT_BUDGET, Math.ceil(tokenBudget * NODES_PER_TOKEN));\n\n const allMatches = scoreNodes(graph, question);\n const seeds = allMatches.length > 0 ? allMatches.slice(0, maxSeeds) : [];\n\n const visited = new Map<string, number>(); // id -> depth\n const frontier: Array<{ id: string; depth: number }> = seeds.map((s) => ({ id: s.id, depth: 0 }));\n let spentTokens = 0;\n for (const seed of seeds) {\n visited.set(seed.id, 0);\n spentTokens += nodeTokenCost(graph, seed.id);\n }\n\n while (frontier.length > 0 && visited.size < budget && spentTokens < tokenBudget) {\n const current = options.dfs ? (frontier.pop() as { id: string; depth: number }) : (frontier.shift() as { id: string; depth: number });\n if (current.depth >= maxDepth) continue;\n\n const neighbors = [...graph.neighbors(current.id)].sort((a, b) => a.localeCompare(b));\n for (const neighbor of neighbors) {\n if (visited.has(neighbor) || visited.size >= budget || spentTokens >= tokenBudget) continue;\n visited.set(neighbor, current.depth + 1);\n spentTokens += nodeTokenCost(graph, neighbor);\n frontier.push({ id: neighbor, depth: current.depth + 1 });\n }\n }\n\n const visitedNodes: VisitedNode[] = [...visited.entries()]\n .map(([id, depth]) => {\n const attrs = graph.getNodeAttributes(id);\n return {\n id,\n label: (attrs.label as string | undefined) ?? id,\n sourceFile: (attrs.sourceFile as string | undefined) ?? '',\n sourceLocation: (attrs.sourceLocation as string | undefined) ?? '',\n depth,\n };\n })\n .sort((a, b) => a.depth - b.depth || a.id.localeCompare(b.id));\n\n const edges: TraversalEdge[] = [];\n graph.forEachEdge((_edge, attrs, source, target) => {\n if (visited.has(source) && visited.has(target)) {\n edges.push({\n source,\n target,\n relation: String(attrs.relation),\n confidence: String(attrs.confidence),\n });\n }\n });\n edges.sort(\n (a, b) => a.source.localeCompare(b.source) || a.target.localeCompare(b.target) || a.relation.localeCompare(b.relation),\n );\n\n return enforceTokenBudget({ seeds, visited: visitedNodes, edges }, tokenBudget);\n}\n\n/**\n * The traversal-time cost estimate can't see how densely connected the\n * visited neighborhood is — a hub-heavy subgraph induces far more edge\n * lines than nodes. Enforce the budget on the *actual* result: drop the\n * deepest/last nodes (and the edges that touched them) until the\n * serialized size fits. Seeds are never dropped.\n */\nfunction enforceTokenBudget(result: QueryResult, tokenBudget: number): QueryResult {\n const cost = (value: unknown): number => Math.ceil(JSON.stringify(value).length / 4);\n\n const visited = [...result.visited];\n let edges = [...result.edges];\n let total = cost(result.seeds) + visited.reduce((s, n) => s + cost(n), 0) + edges.reduce((s, e) => s + cost(e), 0);\n\n const seedIds = new Set(result.seeds.map((s) => s.id));\n // visited is sorted by (depth, id) — pop from the end so the deepest,\n // least-central context goes first and the result stays deterministic.\n while (total > tokenBudget && visited.length > 0) {\n const last = visited[visited.length - 1] as VisitedNode;\n if (seedIds.has(last.id)) break;\n visited.pop();\n total -= cost(last);\n const remaining: TraversalEdge[] = [];\n for (const edge of edges) {\n if (edge.source === last.id || edge.target === last.id) {\n total -= cost(edge);\n } else {\n remaining.push(edge);\n }\n }\n edges = remaining;\n }\n\n return { seeds: result.seeds, visited, edges };\n}\n\nexport interface PathResult {\n from: NodeMatch;\n to: NodeMatch;\n found: boolean;\n path: string[];\n edges: TraversalEdge[];\n}\n\n/** Shortest path between the nodes that best match `fromQuery` and `toQuery` (undirected BFS — connectivity, not call direction). */\nexport function shortestPath(graph: Graph, fromQuery: string, toQuery: string): PathResult | null {\n const from = resolveNode(graph, fromQuery);\n const to = resolveNode(graph, toQuery);\n if (!from || !to) return null;\n\n if (from.id === to.id) {\n return { from, to, found: true, path: [from.id], edges: [] };\n }\n\n const predecessor = new Map<string, string>();\n const visited = new Set<string>([from.id]);\n const queue: string[] = [from.id];\n\n while (queue.length > 0) {\n const current = queue.shift() as string;\n if (current === to.id) break;\n const neighbors = [...graph.neighbors(current)].sort((a, b) => a.localeCompare(b));\n for (const neighbor of neighbors) {\n if (visited.has(neighbor)) continue;\n visited.add(neighbor);\n predecessor.set(neighbor, current);\n queue.push(neighbor);\n }\n }\n\n if (!visited.has(to.id)) {\n return { from, to, found: false, path: [], edges: [] };\n }\n\n const path: string[] = [to.id];\n let cursor = to.id;\n while (cursor !== from.id) {\n const prev = predecessor.get(cursor);\n if (!prev) break;\n path.unshift(prev);\n cursor = prev;\n }\n\n const edges: TraversalEdge[] = [];\n for (let i = 0; i < path.length - 1; i++) {\n const a = path[i] as string;\n const b = path[i + 1] as string;\n const key = graph.edges(a, b)[0] ?? graph.edges(b, a)[0];\n if (key) {\n const attrs = graph.getEdgeAttributes(key);\n edges.push({ source: a, target: b, relation: String(attrs.relation), confidence: String(attrs.confidence) });\n }\n }\n\n return { from, to, found: true, path, edges };\n}\n\nexport interface ExplainResult {\n id: string;\n label: string;\n sourceFile: string;\n sourceLocation: string;\n communityLabel?: string;\n outgoing: TraversalEdge[];\n incoming: TraversalEdge[];\n}\n\n/** Plain-language-ready explanation of the node that best matches `query`. */\nexport function explainNode(graph: Graph, query: string): ExplainResult | null {\n const match = resolveNode(graph, query);\n if (!match) return null;\n\n const attrs = graph.getNodeAttributes(match.id);\n const outgoing: TraversalEdge[] = [];\n const incoming: TraversalEdge[] = [];\n\n graph.forEachOutEdge(match.id, (_edge, edgeAttrs, source, target) => {\n outgoing.push({ source, target, relation: String(edgeAttrs.relation), confidence: String(edgeAttrs.confidence) });\n });\n graph.forEachInEdge(match.id, (_edge, edgeAttrs, source, target) => {\n incoming.push({ source, target, relation: String(edgeAttrs.relation), confidence: String(edgeAttrs.confidence) });\n });\n\n outgoing.sort((a, b) => a.target.localeCompare(b.target) || a.relation.localeCompare(b.relation));\n incoming.sort((a, b) => a.source.localeCompare(b.source) || a.relation.localeCompare(b.relation));\n\n return {\n id: match.id,\n label: match.label,\n sourceFile: (attrs.sourceFile as string | undefined) ?? '',\n sourceLocation: (attrs.sourceLocation as string | undefined) ?? '',\n communityLabel: attrs.communityLabel as string | undefined,\n outgoing,\n incoming,\n };\n}\n","import type Graph from 'graphology';\nimport { scoreNodes } from './query.js';\n\n/**\n * `graphify context` — the working-set pack. Instead of returning node\n * names for the agent to chase (query -> then read whole files anyway),\n * this fuses the two steps: the graph knows every entity's file and line,\n * so rank the nodes relevant to the task, read just the relevant line\n * ranges, and pack the actual code into a token budget. One call returns\n * the code an agent needs to start a task.\n */\n\nexport interface ContextSnippet {\n nodeId: string;\n label: string;\n file: string;\n startLine: number;\n endLine: number;\n code: string;\n /** Why this snippet is in the pack (lexical match / neighbor-of relation). */\n reason: string;\n score: number;\n}\n\nexport interface ContextPack {\n task: string;\n snippets: ContextSnippet[];\n /** Approximate tokens used out of the budget. */\n tokens: number;\n tokenBudget: number;\n /** Ranked nodes that didn't fit the budget — the agent can pull them explicitly. */\n overflow: Array<{ nodeId: string; file: string }>;\n}\n\nexport interface ContextOptions {\n /** Approximate token cap for the pack (default 4000). */\n tokenBudget?: number;\n maxSeeds?: number;\n /** Cap on snippet length in lines (default 60). */\n maxSnippetLines?: number;\n}\n\nexport type FileReader = (path: string) => Promise<string>;\n\nconst DEFAULT_CONTEXT_BUDGET = 4000;\nconst DEFAULT_MAX_SEEDS = 8;\nconst DEFAULT_MAX_SNIPPET_LINES = 60;\n/** A neighbor inherits this fraction of the score of the node that pulled it in. */\nconst NEIGHBOR_DECAY = 0.4;\n\nconst tokensOf = (text: string): number => Math.ceil(text.length / 4);\n\nfunction lineNumberOf(sourceLocation: string): number | null {\n const match = /^L(\\d+)$/.exec(sourceLocation);\n return match ? Number(match[1]) : null;\n}\n\nfunction isReadableFile(sourceFile: string): boolean {\n return sourceFile !== '' && !sourceFile.startsWith('<') && !sourceFile.includes('://');\n}\n\ninterface RankedNode {\n id: string;\n score: number;\n reason: string;\n}\n\n/**\n * Rank nodes for the task: lexical seeds first (their match score), then\n * their graph neighbors at a decayed score, labeled with the relation that\n * connects them. Deterministic (score desc, id asc).\n */\nexport function rankForTask(graph: Graph, task: string, maxSeeds = DEFAULT_MAX_SEEDS): RankedNode[] {\n const seeds = scoreNodes(graph, task).slice(0, maxSeeds);\n const ranked = new Map<string, RankedNode>();\n for (const seed of seeds) {\n ranked.set(seed.id, { id: seed.id, score: seed.score, reason: 'matches the task' });\n }\n\n for (const seed of seeds) {\n graph.forEachEdge(seed.id, (_edge, attrs, source, target) => {\n const neighbor = source === seed.id ? target : source;\n if (ranked.has(neighbor)) return;\n const relation = String(attrs.relation);\n const direction = source === seed.id ? `${relation} ->` : `<- ${relation}`;\n ranked.set(neighbor, {\n id: neighbor,\n score: seed.score * NEIGHBOR_DECAY,\n reason: `${direction} ${seed.label}`,\n });\n });\n }\n\n return [...ranked.values()].sort((a, b) => b.score - a.score || a.id.localeCompare(b.id));\n}\n\n/**\n * Extract the snippet for a node: from its declaration line to just before\n * the next known entity in the same file (capped), so snippets align with\n * real code block boundaries without needing a parser here.\n */\nfunction snippetRange(\n startLine: number,\n fileLineCount: number,\n entityStartsInFile: number[],\n maxLines: number,\n): { start: number; end: number } {\n const nextStart = entityStartsInFile.find((l) => l > startLine);\n const hardEnd = nextStart !== undefined ? nextStart - 1 : fileLineCount;\n return { start: startLine, end: Math.min(hardEnd, startLine + maxLines - 1) };\n}\n\nexport async function buildContextPack(\n graph: Graph,\n task: string,\n readFile: FileReader,\n options: ContextOptions = {},\n): Promise<ContextPack> {\n const tokenBudget = options.tokenBudget ?? DEFAULT_CONTEXT_BUDGET;\n const maxSnippetLines = options.maxSnippetLines ?? DEFAULT_MAX_SNIPPET_LINES;\n const ranked = rankForTask(graph, task, options.maxSeeds);\n\n // Every known entity start line per file — used to end snippets at the\n // next declaration instead of an arbitrary window.\n const entityStarts = new Map<string, number[]>();\n graph.forEachNode((_id, attrs) => {\n const file = attrs.sourceFile as string | undefined;\n const line = lineNumberOf((attrs.sourceLocation as string | undefined) ?? '');\n if (file && line !== null) {\n const starts = entityStarts.get(file) ?? [];\n starts.push(line);\n entityStarts.set(file, starts);\n }\n });\n for (const starts of entityStarts.values()) starts.sort((a, b) => a - b);\n\n const fileCache = new Map<string, string[] | null>();\n const readLines = async (file: string): Promise<string[] | null> => {\n const cached = fileCache.get(file);\n if (cached !== undefined) return cached;\n let lines: string[] | null;\n try {\n lines = (await readFile(file)).split('\\n');\n } catch {\n lines = null;\n }\n fileCache.set(file, lines);\n return lines;\n };\n\n const snippets: ContextSnippet[] = [];\n const overflow: Array<{ nodeId: string; file: string }> = [];\n const coveredRanges = new Map<string, Array<{ start: number; end: number }>>();\n let tokens = 0;\n\n for (const node of ranked) {\n const attrs = graph.getNodeAttributes(node.id);\n const file = (attrs.sourceFile as string | undefined) ?? '';\n const startLine = lineNumberOf((attrs.sourceLocation as string | undefined) ?? '');\n if (!isReadableFile(file) || startLine === null) continue;\n\n const lines = await readLines(file);\n if (lines === null) continue;\n\n const { start, end } = snippetRange(startLine, lines.length, entityStarts.get(file) ?? [], maxSnippetLines);\n\n // Skip if an already-packed snippet from the same file covers this range.\n const covered = (coveredRanges.get(file) ?? []).some((r) => start >= r.start && end <= r.end);\n if (covered) continue;\n\n const code = lines.slice(start - 1, end).join('\\n');\n const cost = tokensOf(code) + 15; // header overhead\n if (tokens + cost > tokenBudget) {\n overflow.push({ nodeId: node.id, file: `${file}:L${startLine}` });\n continue;\n }\n\n tokens += cost;\n snippets.push({\n nodeId: node.id,\n label: (attrs.label as string | undefined) ?? node.id,\n file,\n startLine: start,\n endLine: end,\n code,\n reason: node.reason,\n score: node.score,\n });\n const ranges = coveredRanges.get(file) ?? [];\n ranges.push({ start, end });\n coveredRanges.set(file, ranges);\n }\n\n return { task, snippets, tokens, tokenBudget, overflow };\n}\n\n/** Render a pack as agent-ready markdown. */\nexport function renderContextPack(pack: ContextPack): string {\n const lines: string[] = [\n `# Context pack: ${pack.task}`,\n '',\n `_~${pack.tokens} of ${pack.tokenBudget} token budget, ${pack.snippets.length} snippet(s)._`,\n '',\n ];\n\n if (pack.snippets.length === 0) {\n lines.push('No matching code found — try different keywords.');\n return lines.join('\\n');\n }\n\n for (const snippet of pack.snippets) {\n lines.push(`## ${snippet.label} — \\`${snippet.file}:L${snippet.startLine}-L${snippet.endLine}\\``);\n lines.push(`_${snippet.reason}_`);\n lines.push('```');\n lines.push(snippet.code);\n lines.push('```');\n lines.push('');\n }\n\n if (pack.overflow.length > 0) {\n lines.push(`## Didn't fit the budget (${pack.overflow.length})`);\n for (const item of pack.overflow.slice(0, 15)) {\n lines.push(`- ${item.nodeId} (${item.file})`);\n }\n lines.push('');\n }\n\n return lines.join('\\n');\n}\n","import * as path from 'node:path';\nimport type Graph from 'graphology';\nimport { LocalFileGraphStore } from './store/localFile.js';\n\n/**\n * Load a previously-exported graph.json back into a graphology Graph.\n * Thin wrapper over LocalFileGraphStore (which enforces the path-traversal\n * guard — see SECURITY.md) that throws when no graph exists, because every\n * CLI/MCP caller treats \"no graph yet\" as a hard error. Library callers who\n * want the null-on-missing contract should use a GraphStore directly.\n */\nexport async function loadGraph(outDir: string = path.join(process.cwd(), 'graphify-out')): Promise<Graph> {\n const graph = await new LocalFileGraphStore().load(outDir);\n if (graph === null) {\n throw new Error(`No graph found in ${outDir} — run \\`graphify <path>\\` to build one first.`);\n }\n return graph;\n}\n","import * as fs from 'node:fs/promises';\nimport type Graph from 'graphology';\nimport { validateGraphPath } from '../security.js';\nimport { deserializeGraph, serializeGraph } from './serialize.js';\nimport type { GraphStore, SerializedGraph } from './types.js';\n\n/**\n * The reference GraphStore: one graph.json per directory, `ref` = the\n * outDir path. Reproduces the classic graphify-out/ behavior byte-for-byte\n * (same pretty-printed JSON exportGraph() writes) and routes every path\n * through security.validateGraphPath(), so a caller-supplied ref — or a\n * symlinked graph.json — can never read or write outside the directory it\n * resolves to.\n */\nexport class LocalFileGraphStore implements GraphStore {\n async load(ref: string): Promise<Graph | null> {\n let jsonPath: string;\n try {\n jsonPath = validateGraphPath('graph.json', ref);\n } catch (error) {\n // A ref whose directory doesn't exist simply has no graph yet;\n // traversal/symlink-escape rejections must still propagate.\n if ((error as Error).message.startsWith('Base directory does not exist')) return null;\n throw error;\n }\n\n let raw: string;\n try {\n raw = await fs.readFile(jsonPath, 'utf-8');\n } catch (error) {\n if ((error as NodeJS.ErrnoException).code === 'ENOENT') return null;\n throw error;\n }\n return deserializeGraph(JSON.parse(raw) as SerializedGraph);\n }\n\n async save(ref: string, graph: Graph): Promise<void> {\n await fs.mkdir(ref, { recursive: true });\n const jsonPath = validateGraphPath('graph.json', ref);\n await fs.writeFile(jsonPath, JSON.stringify(serializeGraph(graph), null, 2), 'utf-8');\n }\n}\n","/**\n * Security helpers — URL validation, SSRF-guarded fetch, path guards, label\n * sanitization, prompt-injection defenses. Every control listed in\n * SECURITY.md must have a corresponding function here and a unit test in\n * tests/security.test.ts. See the master prompt §5 for the full checklist.\n */\n\nimport { createHash } from 'node:crypto';\nimport * as dns from 'node:dns';\nimport * as http from 'node:http';\nimport * as https from 'node:https';\nimport * as fs from 'node:fs';\nimport * as net from 'node:net';\nimport * as nodePath from 'node:path';\n\nconst ALLOWED_SCHEMES = new Set(['http:', 'https:']);\nconst MAX_FETCH_BYTES = 50 * 1024 * 1024;\nconst MAX_TEXT_BYTES = 10 * 1024 * 1024;\nconst MAX_LABEL_LEN = 256;\nconst MAX_REDIRECTS = 5;\nconst REQUEST_TIMEOUT_MS = 15_000;\n\n/** Hostnames that must never be reachable, regardless of what they resolve to. */\nconst BLOCKED_HOSTNAMES = new Set([\n 'metadata.google.internal',\n 'metadata.internal',\n 'metadata',\n 'metadata.azure.com',\n 'instance-data',\n 'instance-data.ec2.internal',\n]);\n\n// ---------------------------------------------------------------------------\n// IP range checks (SSRF guard core). Implemented without extra dependencies.\n// ---------------------------------------------------------------------------\n\nfunction ipv4ToInt(ip: string): number | null {\n const parts = ip.split('.');\n if (parts.length !== 4) return null;\n let result = 0;\n for (const part of parts) {\n if (!/^\\d{1,3}$/.test(part)) return null;\n const n = Number(part);\n if (n > 255) return null;\n result = (result << 8) | n;\n }\n return result >>> 0;\n}\n\nfunction ipv4InCidr(ipInt: number, cidr: string): boolean {\n const [base, prefixStr] = cidr.split('/');\n const prefix = Number(prefixStr);\n const baseInt = ipv4ToInt(base ?? '');\n if (baseInt === null) return false;\n const mask = prefix === 0 ? 0 : (~0 << (32 - prefix)) >>> 0;\n return (ipInt & mask) >>> 0 === (baseInt & mask) >>> 0;\n}\n\n/** IPv4 ranges that must never be connected to from a server-side fetch. */\nconst BLOCKED_IPV4_CIDRS = [\n '0.0.0.0/8', // \"this\" network\n '10.0.0.0/8', // private\n '100.64.0.0/10', // carrier-grade NAT (CGN)\n '127.0.0.0/8', // loopback\n '169.254.0.0/16', // link-local (covers 169.254.169.254 cloud metadata)\n '172.16.0.0/12', // private\n '192.0.0.0/24', // IETF protocol assignments\n '192.0.2.0/24', // TEST-NET-1\n '192.168.0.0/16', // private\n '198.18.0.0/15', // benchmarking\n '198.51.100.0/24', // TEST-NET-2\n '203.0.113.0/24', // TEST-NET-3\n '224.0.0.0/4', // multicast\n '240.0.0.0/4', // reserved\n '255.255.255.255/32', // broadcast\n];\n\nexport function isForbiddenIpv4(ip: string): boolean {\n const ipInt = ipv4ToInt(ip);\n if (ipInt === null) return true; // malformed -> treat as forbidden, fail closed\n return BLOCKED_IPV4_CIDRS.some((cidr) => ipv4InCidr(ipInt, cidr));\n}\n\n/** Expand a (possibly `::`-compressed) IPv6 address into 8 16-bit groups. */\nfunction expandIpv6(ip: string): number[] | null {\n const withoutZone = ip.split('%')[0] ?? '';\n const parts = withoutZone.split('::');\n if (parts.length > 2) return null;\n\n const head = parts[0] ? parts[0].split(':').filter((s) => s.length > 0) : [];\n const tail = parts.length === 2 && parts[1] ? parts[1].split(':').filter((s) => s.length > 0) : [];\n\n let missing = 0;\n if (parts.length === 2) {\n missing = 8 - head.length - tail.length;\n if (missing < 0) return null;\n } else if (head.length + tail.length !== 8) {\n return null;\n }\n\n const groupsHex = [...head, ...Array(missing).fill('0'), ...tail];\n if (groupsHex.length !== 8) return null;\n\n const groups: number[] = [];\n for (const g of groupsHex) {\n if (!/^[0-9a-fA-F]{1,4}$/.test(g)) return null;\n groups.push(parseInt(g, 16));\n }\n return groups;\n}\n\nexport function isForbiddenIpv6(ip: string): boolean {\n const lower = ip.toLowerCase();\n if (lower === '::1' || lower === '::') return true;\n\n const mapped = /^::ffff:(\\d+\\.\\d+\\.\\d+\\.\\d+)$/.exec(lower);\n if (mapped) return isForbiddenIpv4(mapped[1] as string);\n\n const groups = expandIpv6(lower);\n if (groups === null) return true; // unparsable -> fail closed\n const first = groups[0] as number;\n if ((first & 0xffc0) === 0xfe80) return true; // fe80::/10 link-local\n if ((first & 0xfe00) === 0xfc00) return true; // fc00::/7 unique local\n if ((first & 0xff00) === 0xff00) return true; // ff00::/8 multicast\n if (groups.every((g) => g === 0)) return true; // unspecified/all-zero\n return false;\n}\n\nexport function isForbiddenIp(ip: string): boolean {\n return net.isIPv6(ip) ? isForbiddenIpv6(ip) : isForbiddenIpv4(ip);\n}\n\n// ---------------------------------------------------------------------------\n// URL validation\n// ---------------------------------------------------------------------------\n\n/**\n * Validate a URL is http/https and, when the hostname is itself a literal\n * IP or a known cloud-metadata hostname, reject it synchronously. DNS-bound\n * hostnames are re-validated at connect time by safeFetch() (see below) —\n * this function alone cannot rule out DNS rebinding for a hostname that\n * currently resolves to a public IP.\n */\nexport function validateUrl(url: string): string {\n let parsed: URL;\n try {\n parsed = new URL(url);\n } catch {\n throw new Error(`Invalid URL: ${url}`);\n }\n\n if (!ALLOWED_SCHEMES.has(parsed.protocol)) {\n throw new Error(\n `URL scheme not allowed: \"${parsed.protocol}\" (allowed: ${[...ALLOWED_SCHEMES].join(', ')})`,\n );\n }\n\n const hostname = parsed.hostname.toLowerCase();\n if (BLOCKED_HOSTNAMES.has(hostname)) {\n throw new Error(`URL targets a blocked host: ${hostname}`);\n }\n\n // WHATWG URL keeps the brackets on an IPv6 literal host (e.g. \"[::1]\") —\n // strip them before checking net.isIP()/isForbiddenIp().\n const bareHost =\n hostname.startsWith('[') && hostname.endsWith(']') ? hostname.slice(1, -1) : hostname;\n if (net.isIP(bareHost) && isForbiddenIp(bareHost)) {\n throw new Error(`URL targets a forbidden IP address: ${bareHost}`);\n }\n\n return parsed.toString();\n}\n\n// ---------------------------------------------------------------------------\n// safeFetch — SSRF-guarded, size-capped, redirect-revalidating fetch\n// ---------------------------------------------------------------------------\n\nexport type LookupFn = (hostname: string) => Promise<{ address: string; family: number }>;\n\n/**\n * Resolve a hostname (or pass through a literal IP) and validate the\n * result. `lookup` defaults to the real `dns.promises.lookup` — tests\n * substitute a fake to exercise the rebind guard deterministically without\n * making a real DNS query.\n */\nexport async function resolveAndValidate(\n hostname: string,\n lookup: LookupFn = dns.promises.lookup,\n): Promise<{ address: string; family: number }> {\n if (net.isIP(hostname)) {\n if (isForbiddenIp(hostname)) {\n throw new Error(`Refusing to connect to forbidden IP: ${hostname}`);\n }\n return { address: hostname, family: net.isIPv6(hostname) ? 6 : 4 };\n }\n const result = await lookup(hostname);\n if (isForbiddenIp(result.address)) {\n throw new Error(`Refusing to connect to forbidden IP: ${hostname} -> ${result.address}`);\n }\n return result;\n}\n\nexport interface RawResponse {\n statusCode: number;\n headers: http.IncomingHttpHeaders;\n body: Buffer;\n}\n\n/** Minimal shape of the `http`/`https` modules that requestOnce() needs — narrow on purpose so tests can substitute a fake transport instead of mocking network I/O. */\nexport type RequestTransport = Pick<typeof http, 'request'>;\n\n/**\n * Issue a single HTTP(S) request, connecting to `resolvedAddress` directly\n * (via a `lookup` override) rather than re-resolving DNS at connect time —\n * this is what prevents a DNS-rebind attacker from swapping the target\n * between validation and connection (TOCTOU). Streams the body and aborts\n * once it exceeds the byte cap for its declared content type.\n */\nexport function requestOnce(\n target: URL,\n resolvedAddress: string,\n resolvedFamily: number,\n transport: RequestTransport = target.protocol === 'https:' ? https : http,\n): Promise<RawResponse> {\n return new Promise((resolve, reject) => {\n const req = transport.request(\n {\n protocol: target.protocol,\n hostname: target.hostname,\n host: target.hostname,\n port: target.port || (target.protocol === 'https:' ? 443 : 80),\n path: `${target.pathname}${target.search}`,\n method: 'GET',\n timeout: REQUEST_TIMEOUT_MS,\n headers: { 'user-agent': 'graphify/0.1', accept: '*/*' },\n // Force the connection to the address we already validated — do not\n // let Node re-resolve `target.hostname` at connect time.\n lookup: (\n _hostname: string,\n options: unknown,\n callback: (err: NodeJS.ErrnoException | null, address: string, family: number) => void,\n ) => {\n callback(null, resolvedAddress, resolvedFamily);\n },\n } as http.RequestOptions,\n (res) => {\n const statusCode = res.statusCode ?? 0;\n const contentType = String(res.headers['content-type'] ?? '');\n const isTextual = /^text\\/|json|xml|html|charset=/i.test(contentType);\n const cap = isTextual ? MAX_TEXT_BYTES : MAX_FETCH_BYTES;\n\n const chunks: Buffer[] = [];\n let total = 0;\n let aborted = false;\n\n res.on('data', (chunk: Buffer) => {\n total += chunk.length;\n if (total > cap) {\n aborted = true;\n res.destroy();\n req.destroy();\n reject(new Error(`Response exceeded ${cap} byte cap (content-type: ${contentType})`));\n return;\n }\n chunks.push(chunk);\n });\n res.on('end', () => {\n if (aborted) return;\n resolve({ statusCode, headers: res.headers, body: Buffer.concat(chunks) });\n });\n res.on('error', (err) => {\n if (!aborted) reject(err);\n });\n },\n );\n\n req.on('timeout', () => {\n req.destroy(new Error(`Request timed out after ${REQUEST_TIMEOUT_MS}ms`));\n });\n req.on('error', reject);\n req.end();\n });\n}\n\nexport interface SafeFetchDeps {\n /** Override DNS resolution + IP validation (default: resolveAndValidate). */\n resolve?: (hostname: string) => Promise<{ address: string; family: number }>;\n /** Override the actual request (default: requestOnce against real http/https). */\n request?: (target: URL, address: string, family: number) => Promise<RawResponse>;\n}\n\n/**\n * Fetch a URL with the SSRF guards from validateUrl() plus streaming size\n * caps (MAX_FETCH_BYTES / MAX_TEXT_BYTES) and a hard error on non-2xx\n * status. Every redirect hop is independently validated and re-resolved —\n * a redirect to a private/loopback/metadata address is rejected exactly\n * like a direct request would be.\n *\n * `deps` exists so tests can substitute the DNS/network collaborators with\n * deterministic fakes (see tests/security.test.ts) instead of mocking\n * Node's core modules or hitting real sockets — production callers should\n * never need to pass it.\n */\nexport async function safeFetch(url: string, deps: SafeFetchDeps = {}): Promise<Buffer> {\n const resolve = deps.resolve ?? resolveAndValidate;\n const doRequest = deps.request ?? requestOnce;\n\n let current = validateUrl(url);\n for (let hop = 0; hop <= MAX_REDIRECTS; hop++) {\n const target = new URL(current);\n const { address, family } = await resolve(target.hostname);\n const response = await doRequest(target, address, family);\n\n if (response.statusCode >= 300 && response.statusCode < 400) {\n const location = response.headers.location;\n if (!location) {\n throw new Error(`Redirect response (${response.statusCode}) missing Location header`);\n }\n const next = new URL(location, target).toString();\n current = validateUrl(next);\n continue;\n }\n\n if (response.statusCode < 200 || response.statusCode >= 300) {\n throw new Error(`Non-2xx response: ${response.statusCode}`);\n }\n\n return response.body;\n }\n throw new Error(`Too many redirects (> ${MAX_REDIRECTS}) while fetching ${url}`);\n}\n\n// ---------------------------------------------------------------------------\n// Database DSN validation (MySQL schema extraction)\n// ---------------------------------------------------------------------------\n\nexport interface ValidatedDsn {\n /**\n * Credential-free rendering (`mysql://host:port/db`). This is the ONLY\n * form that may ever appear in graph nodes, reports, logs, or errors —\n * the password exists solely inside `connection`.\n */\n safeDisplay: string;\n connection: {\n host: string;\n port: number;\n user: string;\n password: string;\n database: string;\n };\n}\n\nconst DEFAULT_MYSQL_PORT = 3306;\n\n/**\n * Parse and validate a `mysql://user:pass@host:port/database` DSN.\n *\n * Deliberately does NOT apply the SSRF IP blocklist that safeFetch()\n * enforces: connecting to a localhost/private-network database is the\n * primary legitimate use of an explicitly user-supplied DSN, unlike a URL\n * scraped out of corpus content. The security property that matters here\n * is credential containment — see ValidatedDsn.safeDisplay.\n */\nexport function validateDsn(dsn: string): ValidatedDsn {\n let parsed: URL;\n try {\n parsed = new URL(dsn);\n } catch {\n throw new Error('Invalid DSN — expected mysql://user:pass@host:port/database');\n }\n\n if (parsed.protocol !== 'mysql:') {\n throw new Error(`DSN scheme not supported: \"${parsed.protocol}\" (only mysql: is supported)`);\n }\n\n const database = decodeURIComponent(parsed.pathname.replace(/^\\//, ''));\n if (!database || database.includes('/')) {\n throw new Error('DSN must name exactly one database, e.g. mysql://localhost:3306/mydb');\n }\n if (!parsed.hostname) {\n throw new Error('DSN must include a host, e.g. mysql://localhost:3306/mydb');\n }\n\n const port = parsed.port ? Number(parsed.port) : DEFAULT_MYSQL_PORT;\n return {\n safeDisplay: `mysql://${parsed.hostname}:${port}/${database}`,\n connection: {\n host: parsed.hostname,\n port,\n user: decodeURIComponent(parsed.username) || 'root',\n password: decodeURIComponent(parsed.password),\n database,\n },\n };\n}\n\n// ---------------------------------------------------------------------------\n// Path traversal guard\n// ---------------------------------------------------------------------------\n\n/**\n * Resolve `path` and require it stays inside `base` (defaults to\n * `<cwd>/graphify-out`). `base` must already exist. Throws on traversal\n * attempts (`../`, absolute escapes, symlink-resolved escapes).\n */\nexport function validateGraphPath(path: string, base?: string): string {\n const baseDir = base ?? nodePath.join(process.cwd(), 'graphify-out');\n\n let resolvedBase: string;\n try {\n resolvedBase = fs.realpathSync(baseDir);\n } catch {\n throw new Error(`Base directory does not exist: ${baseDir}`);\n }\n\n const candidate = nodePath.isAbsolute(path) ? path : nodePath.join(resolvedBase, path);\n const resolvedCandidate = nodePath.resolve(candidate);\n\n // Resolve symlinks for whichever is the deepest existing ancestor, so a\n // symlink inside an otherwise-valid path can't escape the base dir either.\n let realCandidate = resolvedCandidate;\n try {\n realCandidate = fs.realpathSync(resolvedCandidate);\n } catch {\n // Path (or part of it) may not exist yet (e.g. a file we're about to\n // write) — fall back to the lexically-resolved path for the check.\n }\n\n const relative = nodePath.relative(resolvedBase, realCandidate);\n const escapes = relative === '..' || relative.startsWith(`..${nodePath.sep}`) || nodePath.isAbsolute(relative);\n if (escapes) {\n throw new Error(`Path escapes graphify-out/: ${path}`);\n }\n\n return realCandidate;\n}\n\n// ---------------------------------------------------------------------------\n// Label sanitization (unchanged reference implementation) + HTML escaping\n// ---------------------------------------------------------------------------\n\n/** Strip control characters, cap length. Apply to every node/edge label. */\nexport function sanitizeLabel(text: string | null | undefined): string {\n if (text == null) return '';\n // eslint-disable-next-line no-control-regex -- intentional: stripping raw control chars\n const stripped = String(text).replace(/[\\x00-\\x1f\\x7f]/g, '');\n return stripped.slice(0, MAX_LABEL_LEN);\n}\n\n/**\n * HTML-escape a string. Callers must run sanitizeLabel() first for\n * length/control-char stripping, then escapeHtml() immediately before\n * embedding into graph.html (XSS control, see SECURITY.md).\n */\nexport function escapeHtml(text: string): string {\n return text\n .replace(/&/g, '&')\n .replace(/</g, '<')\n .replace(/>/g, '>')\n .replace(/\"/g, '"')\n .replace(/'/g, ''');\n}\n\n// ---------------------------------------------------------------------------\n// Prompt-injection defenses for LLM-bound file content\n// ---------------------------------------------------------------------------\n\n/**\n * Known jailbreak / chat-template sentinels that must never reach a prompt\n * unescaped, whether they occur in source files or are forged to spoof our\n * own <untrusted_source> delimiter.\n */\nconst SENTINEL_PATTERNS: RegExp[] = [\n /<\\|im_start\\|>/gi,\n /<\\|im_end\\|>/gi,\n /<\\|system\\|>/gi,\n /\\[INST\\]/gi,\n /\\[\\/INST\\]/gi,\n /<<SYS>>/gi,\n /<<\\/SYS>>/gi,\n /<\\/untrusted_source>/gi,\n /<untrusted_source/gi,\n];\n\n/**\n * Render brackets as fullwidth lookalikes (< > [ ]) so the defanged\n * echo is human-visible but no longer byte-identical to the original\n * sentinel — a naive downstream scan for the literal delimiter/sentinel\n * text will not re-match our own \"we found one\" marker.\n */\nfunction toFullwidthBrackets(text: string): string {\n return text\n .replace(/</g, '<')\n .replace(/>/g, '>')\n .replace(/\\[/g, '[')\n .replace(/\\]/g, ']');\n}\n\n/**\n * Replace known jailbreak/chat-template sentinels with a visibly defanged\n * form — never silently stripped (silent stripping can itself be an\n * injection vector if it changes meaning unexpectedly).\n */\nfunction defangSentinels(content: string): string {\n let result = content;\n for (const pattern of SENTINEL_PATTERNS) {\n result = result.replace(pattern, (match) => `[DEFANGED:${toFullwidthBrackets(match)}]`);\n }\n return result;\n}\n\n/**\n * Wrap file content for LLM prompts in a hash-stamped untrusted-source\n * delimiter and neutralize known jailbreak/chat-template sentinels. This\n * raises the bar against prompt injection; it does not eliminate it —\n * document that plainly wherever this is referenced.\n */\nexport function wrapUntrustedSource(path: string, content: string): string {\n const sha256 = createHash('sha256').update(content, 'utf8').digest('hex');\n const safePath = escapeHtml(sanitizeLabel(path));\n const defanged = defangSentinels(content);\n return `<untrusted_source path=\"${safePath}\" sha256=\"${sha256}\">\\n${defanged}\\n</untrusted_source>`;\n}\n","import Graph from 'graphology';\nimport type { SerializedGraph } from './types.js';\n\n/**\n * The stable serialization contract for graphs at rest: graphology's\n * node-link JSON. exportGraph() has always written exactly this shape to\n * graph.json, so anything serialized here stays loadable by every existing\n * consumer (and vice versa).\n */\nexport function serializeGraph(graph: Graph): SerializedGraph {\n return graph.export();\n}\n\nexport function deserializeGraph(data: SerializedGraph): Graph {\n return Graph.from(data);\n}\n","import type Graph from 'graphology';\nimport { resolveNode, type NodeMatch } from './query.js';\nimport type { Confidence, Relation } from './types.js';\n\n/**\n * Reverse impact analysis — \"what breaks if I change X\". Walks *incoming*\n * dependency edges (who calls / imports / inherits from the target)\n * transitively, so depth 1 is the direct blast radius and deeper levels are\n * ripple effects. Like everything in query.ts this is lexical + structural,\n * fully offline and deterministic.\n */\n\n/**\n * Relations where `source --rel--> target` means \"source depends on target\",\n * i.e. changing the target can break the source. `contains`/`method` are\n * included because changing an entity plausibly affects its container file\n * or class signature consumers see.\n */\nconst DEPENDENCY_RELATIONS: ReadonlySet<Relation> = new Set([\n 'calls',\n 'imports',\n 'imports_from',\n 'inherits',\n 'implements',\n 'mixes_in',\n 'embeds',\n 'references',\n 're_exports',\n 'method',\n 'contains',\n] satisfies Relation[]);\n\nexport interface AffectedNode {\n id: string;\n label: string;\n sourceFile: string;\n sourceLocation: string;\n /** 1 = depends on the target directly, 2 = one step removed, ... */\n depth: number;\n /** The dependency edge that pulled this node in (this node -> something already affected). */\n via: {\n relation: Relation;\n confidence: Confidence;\n /** The already-affected node this one depends on. */\n dependsOn: string;\n };\n}\n\nexport interface ImpactResult {\n target: NodeMatch;\n affected: AffectedNode[];\n /** How many affected nodes there are per confidence tier of the edge that pulled each one in. */\n byConfidence: Record<Confidence, number>;\n maxDepth: number;\n truncated: boolean;\n}\n\nexport interface ImpactOptions {\n /** How many reverse hops to follow (default 3). */\n maxDepth?: number;\n /** Hard cap on affected nodes reported (default 200). */\n limit?: number;\n /** Restrict the traversal to these relations (default: every dependency-carrying relation). */\n relations?: Relation[];\n}\n\nconst DEFAULT_IMPACT_DEPTH = 3;\nconst DEFAULT_IMPACT_LIMIT = 200;\n\nconst CONFIDENCE_RANK: Record<Confidence, number> = { EXTRACTED: 3, INFERRED: 2, AMBIGUOUS: 1 };\n\n/**\n * Resolve `nodeQuery` to its best-matching node, then reverse-BFS over\n * incoming dependency edges. Returns null when nothing in the graph matches\n * the query at all (same contract as explainNode()).\n */\nexport function affectedBy(graph: Graph, nodeQuery: string, options: ImpactOptions = {}): ImpactResult | null {\n const target = resolveNode(graph, nodeQuery);\n if (!target) return null;\n\n const maxDepth = options.maxDepth ?? DEFAULT_IMPACT_DEPTH;\n const limit = options.limit ?? DEFAULT_IMPACT_LIMIT;\n const followed: ReadonlySet<Relation> = options.relations ? new Set(options.relations) : DEPENDENCY_RELATIONS;\n\n const affected: AffectedNode[] = [];\n const visited = new Set<string>([target.id]);\n let frontier: string[] = [target.id];\n let truncated = false;\n\n for (let depth = 1; depth <= maxDepth && frontier.length > 0 && !truncated; depth++) {\n // Collect every dependent of the current frontier before descending, so\n // each node's reported depth is its *shortest* reverse distance.\n const nextFrontier = new Map<string, AffectedNode>();\n\n for (const current of [...frontier].sort((a, b) => a.localeCompare(b))) {\n graph.forEachInEdge(current, (_edge, edgeAttrs, source) => {\n if (visited.has(source)) return;\n const relation = edgeAttrs.relation as Relation;\n if (!followed.has(relation)) return;\n\n const confidence = edgeAttrs.confidence as Confidence;\n const existing = nextFrontier.get(source);\n // Same node reachable via several edges this round: keep the\n // strongest-confidence one, mirroring buildGraph()'s merge rule.\n if (existing && CONFIDENCE_RANK[existing.via.confidence] >= CONFIDENCE_RANK[confidence]) return;\n\n const attrs = graph.getNodeAttributes(source);\n nextFrontier.set(source, {\n id: source,\n label: (attrs.label as string | undefined) ?? source,\n sourceFile: (attrs.sourceFile as string | undefined) ?? '',\n sourceLocation: (attrs.sourceLocation as string | undefined) ?? '',\n depth,\n via: { relation, confidence, dependsOn: current },\n });\n });\n }\n\n const roundNodes = [...nextFrontier.values()].sort((a, b) => a.id.localeCompare(b.id));\n for (const node of roundNodes) {\n if (affected.length >= limit) {\n truncated = true;\n break;\n }\n visited.add(node.id);\n affected.push(node);\n }\n frontier = roundNodes.filter((n) => visited.has(n.id)).map((n) => n.id);\n }\n\n const byConfidence: Record<Confidence, number> = { EXTRACTED: 0, INFERRED: 0, AMBIGUOUS: 0 };\n for (const node of affected) byConfidence[node.via.confidence] += 1;\n\n return { target, affected, byConfidence, maxDepth, truncated };\n}\n","import type Graph from 'graphology';\nimport { affectedBy } from './impact.js';\nimport { resolveNode } from './query.js';\nimport type { Relation } from './types.js';\n\n/**\n * `graphify tests` — structural test selection. Coverage tools need\n * instrumentation and a full test run to know what covers what; the graph\n * already knows, statically: a test file imports/calls the code it\n * exercises, so the test files inside a change's reverse blast radius are\n * exactly the ones worth running. Cross-language, no instrumentation.\n */\n\n/** Test-file naming conventions across the supported languages. */\nconst TEST_FILE_PATTERNS: RegExp[] = [\n /(^|\\/)tests?\\//, // tests/ or test/ directory\n /(^|\\/)__tests__\\//,\n /(^|\\/)spec\\//,\n /\\.test\\.[cm]?[jt]sx?$/,\n /\\.spec\\.[cm]?[jt]sx?$/,\n /(^|\\/)test_[^/]+\\.py$/,\n /_test\\.py$/,\n /_test\\.go$/,\n /Tests?\\.(java|cs)$/,\n /_spec\\.rb$/,\n /_test\\.rb$/,\n];\n\nexport function isTestFile(path: string): boolean {\n return TEST_FILE_PATTERNS.some((p) => p.test(path));\n}\n\nexport interface TestSelection {\n /** What the selection was computed for (resolved node id, or the changed files). */\n target: string;\n /** Unique test files, most directly affected first. */\n testFiles: Array<{ file: string; depth: number; via: string }>;\n /** How many non-test nodes were in the blast radius (context for confidence). */\n affectedNodeCount: number;\n /**\n * `usage` — selected by following actual use (calls/inheritance), tight;\n * `import-reachability` — the broad fallback: anything that can reach the\n * target through imports/re-exports, which over-includes files that share\n * a barrel entry with the target.\n */\n precision: 'usage' | 'import-reachability';\n}\n\nconst TEST_REACH_DEPTH = 4;\nconst TEST_REACH_LIMIT = 2000;\n\n/** Relations that mean \"actually uses it\", as opposed to \"can merely reach it through an import\". */\nconst USAGE_RELATIONS: Relation[] = ['calls', 'inherits', 'implements', 'mixes_in', 'embeds', 'references'];\n\nfunction selectionFromImpact(\n graph: Graph,\n targetIds: string[],\n targetLabel: string,\n relations?: Relation[],\n): TestSelection {\n const best = new Map<string, { depth: number; via: string }>();\n let affectedNodeCount = 0;\n\n for (const id of targetIds) {\n const impact = affectedBy(graph, id, { maxDepth: TEST_REACH_DEPTH, limit: TEST_REACH_LIMIT, relations });\n if (!impact) continue;\n affectedNodeCount += impact.affected.length;\n for (const node of impact.affected) {\n if (!isTestFile(node.sourceFile)) continue;\n const existing = best.get(node.sourceFile);\n if (!existing || node.depth < existing.depth) {\n best.set(node.sourceFile, { depth: node.depth, via: node.via.dependsOn });\n }\n }\n }\n\n const testFiles = [...best.entries()]\n .map(([file, info]) => ({ file, depth: info.depth, via: info.via }))\n .sort((a, b) => a.depth - b.depth || a.file.localeCompare(b.file));\n\n return {\n target: targetLabel,\n testFiles,\n affectedNodeCount,\n precision: relations ? 'usage' : 'import-reachability',\n };\n}\n\n/** A file node's entities (so a usage-relation traversal has call targets to start from). */\nfunction entityRootsOf(graph: Graph, id: string): string[] {\n if (id.includes('::')) return [id];\n const roots: string[] = [];\n graph.forEachOutEdge(id, (_edge, attrs, _source, target) => {\n if (String(attrs.relation) === 'contains') roots.push(target);\n });\n roots.sort((a, b) => a.localeCompare(b));\n return roots.length > 0 ? roots : [id];\n}\n\n/**\n * Test files reachable (reverse) from the node best matching `nodeQuery`.\n * Tries a tight usage-relation pass first (tests that actually CALL the\n * target — precise even when many files share a barrel entry point); falls\n * back to full import-reachability when usage finds nothing. Null if\n * nothing matches the query.\n */\nexport function testsForNode(graph: Graph, nodeQuery: string): TestSelection | null {\n const match = resolveNode(graph, nodeQuery);\n if (!match) return null;\n\n const precise = selectionFromImpact(graph, entityRootsOf(graph, match.id), match.id, USAGE_RELATIONS);\n if (precise.testFiles.length > 0) return precise;\n\n return selectionFromImpact(graph, [match.id], match.id);\n}\n\n/**\n * Test files reachable from any of the given changed files (as reported by\n * `git diff --name-only`). Changed test files select themselves. Unknown\n * files (not in the graph) are skipped.\n */\nexport function testsForChangedFiles(graph: Graph, changedFiles: string[]): TestSelection {\n const fileIds: string[] = [];\n const selfSelected = new Map<string, { depth: number; via: string }>();\n\n for (const file of changedFiles) {\n if (isTestFile(file)) {\n selfSelected.set(file, { depth: 0, via: '(changed directly)' });\n continue;\n }\n if (graph.hasNode(file)) fileIds.push(file);\n }\n\n const label = changedFiles.join(', ');\n const preciseRoots = fileIds.flatMap((f) => entityRootsOf(graph, f));\n let selection = selectionFromImpact(graph, preciseRoots, label, USAGE_RELATIONS);\n if (selection.testFiles.length === 0) {\n selection = selectionFromImpact(graph, fileIds, label);\n }\n for (const [file, info] of selfSelected) {\n if (!selection.testFiles.some((t) => t.file === file)) {\n selection.testFiles.push({ file, depth: info.depth, via: info.via });\n }\n }\n selection.testFiles.sort((a, b) => a.depth - b.depth || a.file.localeCompare(b.file));\n return selection;\n}\n"],"mappings":"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","names":["fs","path","path","DEFAULT_MAX_SEEDS","readFile","fs","path","relative","Graph","path"]}
|
package/dist/mcp/server.js
CHANGED
package/package.json
CHANGED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/store/serialize.ts","../src/store/localFile.ts","../src/graphStore.ts","../src/query.ts","../src/impact.ts","../src/context.ts","../src/testmap.ts"],"sourcesContent":["import Graph from 'graphology';\nimport type { SerializedGraph } from './types.js';\n\n/**\n * The stable serialization contract for graphs at rest: graphology's\n * node-link JSON. exportGraph() has always written exactly this shape to\n * graph.json, so anything serialized here stays loadable by every existing\n * consumer (and vice versa).\n */\nexport function serializeGraph(graph: Graph): SerializedGraph {\n return graph.export();\n}\n\nexport function deserializeGraph(data: SerializedGraph): Graph {\n return Graph.from(data);\n}\n","import * as fs from 'node:fs/promises';\nimport type Graph from 'graphology';\nimport { validateGraphPath } from '../security.js';\nimport { deserializeGraph, serializeGraph } from './serialize.js';\nimport type { GraphStore, SerializedGraph } from './types.js';\n\n/**\n * The reference GraphStore: one graph.json per directory, `ref` = the\n * outDir path. Reproduces the classic graphify-out/ behavior byte-for-byte\n * (same pretty-printed JSON exportGraph() writes) and routes every path\n * through security.validateGraphPath(), so a caller-supplied ref — or a\n * symlinked graph.json — can never read or write outside the directory it\n * resolves to.\n */\nexport class LocalFileGraphStore implements GraphStore {\n async load(ref: string): Promise<Graph | null> {\n let jsonPath: string;\n try {\n jsonPath = validateGraphPath('graph.json', ref);\n } catch (error) {\n // A ref whose directory doesn't exist simply has no graph yet;\n // traversal/symlink-escape rejections must still propagate.\n if ((error as Error).message.startsWith('Base directory does not exist')) return null;\n throw error;\n }\n\n let raw: string;\n try {\n raw = await fs.readFile(jsonPath, 'utf-8');\n } catch (error) {\n if ((error as NodeJS.ErrnoException).code === 'ENOENT') return null;\n throw error;\n }\n return deserializeGraph(JSON.parse(raw) as SerializedGraph);\n }\n\n async save(ref: string, graph: Graph): Promise<void> {\n await fs.mkdir(ref, { recursive: true });\n const jsonPath = validateGraphPath('graph.json', ref);\n await fs.writeFile(jsonPath, JSON.stringify(serializeGraph(graph), null, 2), 'utf-8');\n }\n}\n","import * as path from 'node:path';\nimport type Graph from 'graphology';\nimport { LocalFileGraphStore } from './store/localFile.js';\n\n/**\n * Load a previously-exported graph.json back into a graphology Graph.\n * Thin wrapper over LocalFileGraphStore (which enforces the path-traversal\n * guard — see SECURITY.md) that throws when no graph exists, because every\n * CLI/MCP caller treats \"no graph yet\" as a hard error. Library callers who\n * want the null-on-missing contract should use a GraphStore directly.\n */\nexport async function loadGraph(outDir: string = path.join(process.cwd(), 'graphify-out')): Promise<Graph> {\n const graph = await new LocalFileGraphStore().load(outDir);\n if (graph === null) {\n throw new Error(`No graph found in ${outDir} — run \\`graphify <path>\\` to build one first.`);\n }\n return graph;\n}\n","import type Graph from 'graphology';\n\n/**\n * Library-level graph queries shared by the CLI (`graphify query/path/\n * explain`) and the MCP server tools of the same name. None of this is an\n * LLM call — it's lexical node matching + graph traversal, so it works\n * fully offline and deterministically.\n */\n\nconst STOPWORDS = new Set([\n 'a', 'an', 'the', 'is', 'are', 'was', 'were', 'do', 'does', 'did', 'how',\n 'what', 'where', 'when', 'why', 'who', 'which', 'to', 'of', 'in', 'on',\n 'for', 'and', 'or', 'this', 'that', 'it', 'does', 'that\\'s',\n]);\n\n/** Insert spaces at camelCase boundaries so identifier words become separate tokens. */\nfunction splitCamelCase(text: string): string {\n return text\n .replace(/([a-z0-9])([A-Z])/g, '$1 $2')\n .replace(/([A-Z]+)([A-Z][a-z])/g, '$1 $2');\n}\n\nfunction tokenize(text: string): string[] {\n return splitCamelCase(text)\n .toLowerCase()\n .split(/[^a-z0-9]+/)\n .filter((token) => token.length > 1 && !STOPWORDS.has(token));\n}\n\n/**\n * Split an identifier-ish string (node label/id) into its word subtokens:\n * camelCase, snake_case, kebab-case, dots, and path separators all become\n * boundaries, so `src/a/fileStorage.js::saveAttachment` yields\n * `src, file, storage, js, save, attachment`.\n */\nfunction subtokenize(text: string): string[] {\n return splitCamelCase(text)\n .toLowerCase()\n .split(/[^a-z0-9]+/)\n .filter((token) => token.length > 1);\n}\n\n/** Don't run the fuzzy tier on tokens this short — too many false positives. */\nconst FUZZY_MIN_TOKEN_LEN = 3;\n/** Minimum similarity for a fuzzy match to count at all. */\nconst FUZZY_THRESHOLD = 0.7;\nconst SUBTOKEN_MATCH_SCORE = 1;\nconst SUBSTRING_MATCH_SCORE = 0.6;\nconst FUZZY_MATCH_WEIGHT = 0.5;\n\n/**\n * Optimal-string-alignment Damerau-Levenshtein distance. Chosen over\n * bigram-set similarity because the common code-search typo is a\n * transposition (\"sorceNodes\"), which OSA counts as one edit but bigram\n * overlap punishes brutally.\n */\nfunction osaDistance(a: string, b: string): number {\n let prev2: number[] = [];\n let prev: number[] = Array.from({ length: b.length + 1 }, (_, j) => j);\n let curr: number[] = new Array<number>(b.length + 1).fill(0);\n\n for (let i = 1; i <= a.length; i++) {\n curr[0] = i;\n for (let j = 1; j <= b.length; j++) {\n const substitution = a[i - 1] === b[j - 1] ? 0 : 1;\n let best = Math.min(\n (prev[j] as number) + 1,\n (curr[j - 1] as number) + 1,\n (prev[j - 1] as number) + substitution,\n );\n if (i > 1 && j > 1 && a[i - 1] === b[j - 2] && a[i - 2] === b[j - 1]) {\n best = Math.min(best, (prev2[j - 2] as number) + 1);\n }\n curr[j] = best;\n }\n [prev2, prev, curr] = [prev, curr, new Array<number>(b.length + 1).fill(0)];\n }\n return prev[b.length] as number;\n}\n\n/** Normalized similarity in [0, 1]; short-circuits pairs whose length gap alone puts them under FUZZY_THRESHOLD. */\nfunction fuzzySimilarity(a: string, b: string): number {\n if (a === b) return 1;\n const maxLen = Math.max(a.length, b.length);\n if (maxLen === 0) return 1;\n if (Math.abs(a.length - b.length) / maxLen > 1 - FUZZY_THRESHOLD) return 0;\n return 1 - osaDistance(a, b) / maxLen;\n}\n\nexport interface NodeMatch {\n id: string;\n label: string;\n score: number;\n}\n\n/**\n * Score every node against the query's tokens. Identifier-aware and\n * typo-tolerant, in three tiers per token (best tier wins):\n *\n * 1. exact subtoken match (camelCase/snake_case-split word of the label\n * or id) — \"storage\" matches `fileStorage.js`;\n * 2. plain substring of the label/id (the original v1 behavior);\n * 3. fuzzy (Damerau-Levenshtein) match against a subtoken — \"sorce\"\n * still finds `scoreNodes`.\n *\n * Still fully lexical and deterministic — no LLM, no randomness.\n */\nexport function scoreNodes(graph: Graph, query: string): NodeMatch[] {\n const tokens = tokenize(query);\n const matches: NodeMatch[] = [];\n\n graph.forEachNode((id, attributes) => {\n const label = (attributes.label as string | undefined) ?? id;\n const haystack = `${label} ${id}`.toLowerCase();\n const subtokens = new Set(subtokenize(`${label} ${id}`));\n let score = 0;\n for (const token of tokens) {\n if (subtokens.has(token)) {\n score += SUBTOKEN_MATCH_SCORE;\n continue;\n }\n if (haystack.includes(token)) {\n score += SUBSTRING_MATCH_SCORE;\n continue;\n }\n if (token.length >= FUZZY_MIN_TOKEN_LEN) {\n let best = 0;\n for (const subtoken of subtokens) {\n const similarity = fuzzySimilarity(token, subtoken);\n if (similarity > best) best = similarity;\n }\n if (best >= FUZZY_THRESHOLD) score += best * FUZZY_MATCH_WEIGHT;\n }\n }\n if (score > 0) matches.push({ id, label, score });\n });\n\n // On equal score, real source nodes beat module:/external: placeholders —\n // \"devtools\" should resolve to the devtools middleware, not the\n // @redux-devtools package node that happens to share a subtoken.\n const placeholderRank = (id: string): number =>\n id.startsWith('module:') || id.startsWith('external:') ? 1 : 0;\n matches.sort(\n (a, b) =>\n b.score - a.score || placeholderRank(a.id) - placeholderRank(b.id) || a.id.localeCompare(b.id),\n );\n return matches;\n}\n\n/**\n * Best-effort single-node resolution: the highest-scoring lexical match,\n * or null if nothing in the graph matches at all. Deliberately does *not*\n * fall back to \"the most connected node\" — path/explain name a specific\n * node by intent, and silently substituting an unrelated one would be\n * more misleading than clearly saying \"no match\".\n */\nexport function resolveNode(graph: Graph, query: string): NodeMatch | null {\n const matches = scoreNodes(graph, query);\n return matches[0] ?? null;\n}\n\nexport interface VisitedNode {\n id: string;\n label: string;\n sourceFile: string;\n sourceLocation: string;\n depth: number;\n}\n\nexport interface TraversalEdge {\n source: string;\n target: string;\n relation: string;\n confidence: string;\n}\n\nexport interface QueryResult {\n seeds: NodeMatch[];\n visited: VisitedNode[];\n edges: TraversalEdge[];\n}\n\nexport interface QueryOptions {\n dfs?: boolean;\n /** Hard cap on how many nodes to include. Defaults to a generous ceiling derived from `tokenBudget`. */\n budget?: number;\n /** Approximate cap, in tokens (~4 chars each), on the serialized size of the result. Default 2000. */\n tokenBudget?: number;\n maxDepth?: number;\n maxSeeds?: number;\n}\n\nconst DEFAULT_MAX_DEPTH = 2;\nconst DEFAULT_MAX_SEEDS = 5;\nconst DEFAULT_BUDGET = 40;\nconst DEFAULT_TOKEN_BUDGET = 2000;\n/** When only tokenBudget is given, allow up to this many nodes per budgeted token-decile as a safety ceiling. */\nconst NODES_PER_TOKEN = 1 / 10;\n\n/**\n * Estimated token contribution of one node to the rendered result: its own\n * line (label, file, location) plus a rough allowance for the edge lines\n * that mention its id. Chars/4 is the usual serviceable approximation.\n */\nfunction nodeTokenCost(graph: Graph, id: string): number {\n const attrs = graph.getNodeAttributes(id);\n const label = (attrs.label as string | undefined) ?? id;\n const sourceFile = (attrs.sourceFile as string | undefined) ?? '';\n return Math.ceil((id.length * 2 + label.length + sourceFile.length + 24) / 4);\n}\n\n/**\n * BFS (default, broad context) or DFS (--dfs, trace a specific path)\n * traversal from the nodes whose label best matches `question`'s\n * keywords. This is lexical retrieval, not natural-language\n * understanding — it surfaces the neighborhood of the graph an agent (or\n * a human) should look at to answer the question, it does not itself\n * compose an answer.\n */\nexport function queryGraph(graph: Graph, question: string, options: QueryOptions = {}): QueryResult {\n const maxDepth = options.maxDepth ?? DEFAULT_MAX_DEPTH;\n const maxSeeds = options.maxSeeds ?? DEFAULT_MAX_SEEDS;\n const tokenBudget = options.tokenBudget ?? DEFAULT_TOKEN_BUDGET;\n const budget = options.budget ?? Math.max(DEFAULT_BUDGET, Math.ceil(tokenBudget * NODES_PER_TOKEN));\n\n const allMatches = scoreNodes(graph, question);\n const seeds = allMatches.length > 0 ? allMatches.slice(0, maxSeeds) : [];\n\n const visited = new Map<string, number>(); // id -> depth\n const frontier: Array<{ id: string; depth: number }> = seeds.map((s) => ({ id: s.id, depth: 0 }));\n let spentTokens = 0;\n for (const seed of seeds) {\n visited.set(seed.id, 0);\n spentTokens += nodeTokenCost(graph, seed.id);\n }\n\n while (frontier.length > 0 && visited.size < budget && spentTokens < tokenBudget) {\n const current = options.dfs ? (frontier.pop() as { id: string; depth: number }) : (frontier.shift() as { id: string; depth: number });\n if (current.depth >= maxDepth) continue;\n\n const neighbors = [...graph.neighbors(current.id)].sort((a, b) => a.localeCompare(b));\n for (const neighbor of neighbors) {\n if (visited.has(neighbor) || visited.size >= budget || spentTokens >= tokenBudget) continue;\n visited.set(neighbor, current.depth + 1);\n spentTokens += nodeTokenCost(graph, neighbor);\n frontier.push({ id: neighbor, depth: current.depth + 1 });\n }\n }\n\n const visitedNodes: VisitedNode[] = [...visited.entries()]\n .map(([id, depth]) => {\n const attrs = graph.getNodeAttributes(id);\n return {\n id,\n label: (attrs.label as string | undefined) ?? id,\n sourceFile: (attrs.sourceFile as string | undefined) ?? '',\n sourceLocation: (attrs.sourceLocation as string | undefined) ?? '',\n depth,\n };\n })\n .sort((a, b) => a.depth - b.depth || a.id.localeCompare(b.id));\n\n const edges: TraversalEdge[] = [];\n graph.forEachEdge((_edge, attrs, source, target) => {\n if (visited.has(source) && visited.has(target)) {\n edges.push({\n source,\n target,\n relation: String(attrs.relation),\n confidence: String(attrs.confidence),\n });\n }\n });\n edges.sort(\n (a, b) => a.source.localeCompare(b.source) || a.target.localeCompare(b.target) || a.relation.localeCompare(b.relation),\n );\n\n return enforceTokenBudget({ seeds, visited: visitedNodes, edges }, tokenBudget);\n}\n\n/**\n * The traversal-time cost estimate can't see how densely connected the\n * visited neighborhood is — a hub-heavy subgraph induces far more edge\n * lines than nodes. Enforce the budget on the *actual* result: drop the\n * deepest/last nodes (and the edges that touched them) until the\n * serialized size fits. Seeds are never dropped.\n */\nfunction enforceTokenBudget(result: QueryResult, tokenBudget: number): QueryResult {\n const cost = (value: unknown): number => Math.ceil(JSON.stringify(value).length / 4);\n\n const visited = [...result.visited];\n let edges = [...result.edges];\n let total = cost(result.seeds) + visited.reduce((s, n) => s + cost(n), 0) + edges.reduce((s, e) => s + cost(e), 0);\n\n const seedIds = new Set(result.seeds.map((s) => s.id));\n // visited is sorted by (depth, id) — pop from the end so the deepest,\n // least-central context goes first and the result stays deterministic.\n while (total > tokenBudget && visited.length > 0) {\n const last = visited[visited.length - 1] as VisitedNode;\n if (seedIds.has(last.id)) break;\n visited.pop();\n total -= cost(last);\n const remaining: TraversalEdge[] = [];\n for (const edge of edges) {\n if (edge.source === last.id || edge.target === last.id) {\n total -= cost(edge);\n } else {\n remaining.push(edge);\n }\n }\n edges = remaining;\n }\n\n return { seeds: result.seeds, visited, edges };\n}\n\nexport interface PathResult {\n from: NodeMatch;\n to: NodeMatch;\n found: boolean;\n path: string[];\n edges: TraversalEdge[];\n}\n\n/** Shortest path between the nodes that best match `fromQuery` and `toQuery` (undirected BFS — connectivity, not call direction). */\nexport function shortestPath(graph: Graph, fromQuery: string, toQuery: string): PathResult | null {\n const from = resolveNode(graph, fromQuery);\n const to = resolveNode(graph, toQuery);\n if (!from || !to) return null;\n\n if (from.id === to.id) {\n return { from, to, found: true, path: [from.id], edges: [] };\n }\n\n const predecessor = new Map<string, string>();\n const visited = new Set<string>([from.id]);\n const queue: string[] = [from.id];\n\n while (queue.length > 0) {\n const current = queue.shift() as string;\n if (current === to.id) break;\n const neighbors = [...graph.neighbors(current)].sort((a, b) => a.localeCompare(b));\n for (const neighbor of neighbors) {\n if (visited.has(neighbor)) continue;\n visited.add(neighbor);\n predecessor.set(neighbor, current);\n queue.push(neighbor);\n }\n }\n\n if (!visited.has(to.id)) {\n return { from, to, found: false, path: [], edges: [] };\n }\n\n const path: string[] = [to.id];\n let cursor = to.id;\n while (cursor !== from.id) {\n const prev = predecessor.get(cursor);\n if (!prev) break;\n path.unshift(prev);\n cursor = prev;\n }\n\n const edges: TraversalEdge[] = [];\n for (let i = 0; i < path.length - 1; i++) {\n const a = path[i] as string;\n const b = path[i + 1] as string;\n const key = graph.edges(a, b)[0] ?? graph.edges(b, a)[0];\n if (key) {\n const attrs = graph.getEdgeAttributes(key);\n edges.push({ source: a, target: b, relation: String(attrs.relation), confidence: String(attrs.confidence) });\n }\n }\n\n return { from, to, found: true, path, edges };\n}\n\nexport interface ExplainResult {\n id: string;\n label: string;\n sourceFile: string;\n sourceLocation: string;\n communityLabel?: string;\n outgoing: TraversalEdge[];\n incoming: TraversalEdge[];\n}\n\n/** Plain-language-ready explanation of the node that best matches `query`. */\nexport function explainNode(graph: Graph, query: string): ExplainResult | null {\n const match = resolveNode(graph, query);\n if (!match) return null;\n\n const attrs = graph.getNodeAttributes(match.id);\n const outgoing: TraversalEdge[] = [];\n const incoming: TraversalEdge[] = [];\n\n graph.forEachOutEdge(match.id, (_edge, edgeAttrs, source, target) => {\n outgoing.push({ source, target, relation: String(edgeAttrs.relation), confidence: String(edgeAttrs.confidence) });\n });\n graph.forEachInEdge(match.id, (_edge, edgeAttrs, source, target) => {\n incoming.push({ source, target, relation: String(edgeAttrs.relation), confidence: String(edgeAttrs.confidence) });\n });\n\n outgoing.sort((a, b) => a.target.localeCompare(b.target) || a.relation.localeCompare(b.relation));\n incoming.sort((a, b) => a.source.localeCompare(b.source) || a.relation.localeCompare(b.relation));\n\n return {\n id: match.id,\n label: match.label,\n sourceFile: (attrs.sourceFile as string | undefined) ?? '',\n sourceLocation: (attrs.sourceLocation as string | undefined) ?? '',\n communityLabel: attrs.communityLabel as string | undefined,\n outgoing,\n incoming,\n };\n}\n","import type Graph from 'graphology';\nimport { resolveNode, type NodeMatch } from './query.js';\nimport type { Confidence, Relation } from './types.js';\n\n/**\n * Reverse impact analysis — \"what breaks if I change X\". Walks *incoming*\n * dependency edges (who calls / imports / inherits from the target)\n * transitively, so depth 1 is the direct blast radius and deeper levels are\n * ripple effects. Like everything in query.ts this is lexical + structural,\n * fully offline and deterministic.\n */\n\n/**\n * Relations where `source --rel--> target` means \"source depends on target\",\n * i.e. changing the target can break the source. `contains`/`method` are\n * included because changing an entity plausibly affects its container file\n * or class signature consumers see.\n */\nconst DEPENDENCY_RELATIONS: ReadonlySet<Relation> = new Set([\n 'calls',\n 'imports',\n 'imports_from',\n 'inherits',\n 'implements',\n 'mixes_in',\n 'embeds',\n 'references',\n 're_exports',\n 'method',\n 'contains',\n] satisfies Relation[]);\n\nexport interface AffectedNode {\n id: string;\n label: string;\n sourceFile: string;\n sourceLocation: string;\n /** 1 = depends on the target directly, 2 = one step removed, ... */\n depth: number;\n /** The dependency edge that pulled this node in (this node -> something already affected). */\n via: {\n relation: Relation;\n confidence: Confidence;\n /** The already-affected node this one depends on. */\n dependsOn: string;\n };\n}\n\nexport interface ImpactResult {\n target: NodeMatch;\n affected: AffectedNode[];\n /** How many affected nodes there are per confidence tier of the edge that pulled each one in. */\n byConfidence: Record<Confidence, number>;\n maxDepth: number;\n truncated: boolean;\n}\n\nexport interface ImpactOptions {\n /** How many reverse hops to follow (default 3). */\n maxDepth?: number;\n /** Hard cap on affected nodes reported (default 200). */\n limit?: number;\n /** Restrict the traversal to these relations (default: every dependency-carrying relation). */\n relations?: Relation[];\n}\n\nconst DEFAULT_IMPACT_DEPTH = 3;\nconst DEFAULT_IMPACT_LIMIT = 200;\n\nconst CONFIDENCE_RANK: Record<Confidence, number> = { EXTRACTED: 3, INFERRED: 2, AMBIGUOUS: 1 };\n\n/**\n * Resolve `nodeQuery` to its best-matching node, then reverse-BFS over\n * incoming dependency edges. Returns null when nothing in the graph matches\n * the query at all (same contract as explainNode()).\n */\nexport function affectedBy(graph: Graph, nodeQuery: string, options: ImpactOptions = {}): ImpactResult | null {\n const target = resolveNode(graph, nodeQuery);\n if (!target) return null;\n\n const maxDepth = options.maxDepth ?? DEFAULT_IMPACT_DEPTH;\n const limit = options.limit ?? DEFAULT_IMPACT_LIMIT;\n const followed: ReadonlySet<Relation> = options.relations ? new Set(options.relations) : DEPENDENCY_RELATIONS;\n\n const affected: AffectedNode[] = [];\n const visited = new Set<string>([target.id]);\n let frontier: string[] = [target.id];\n let truncated = false;\n\n for (let depth = 1; depth <= maxDepth && frontier.length > 0 && !truncated; depth++) {\n // Collect every dependent of the current frontier before descending, so\n // each node's reported depth is its *shortest* reverse distance.\n const nextFrontier = new Map<string, AffectedNode>();\n\n for (const current of [...frontier].sort((a, b) => a.localeCompare(b))) {\n graph.forEachInEdge(current, (_edge, edgeAttrs, source) => {\n if (visited.has(source)) return;\n const relation = edgeAttrs.relation as Relation;\n if (!followed.has(relation)) return;\n\n const confidence = edgeAttrs.confidence as Confidence;\n const existing = nextFrontier.get(source);\n // Same node reachable via several edges this round: keep the\n // strongest-confidence one, mirroring buildGraph()'s merge rule.\n if (existing && CONFIDENCE_RANK[existing.via.confidence] >= CONFIDENCE_RANK[confidence]) return;\n\n const attrs = graph.getNodeAttributes(source);\n nextFrontier.set(source, {\n id: source,\n label: (attrs.label as string | undefined) ?? source,\n sourceFile: (attrs.sourceFile as string | undefined) ?? '',\n sourceLocation: (attrs.sourceLocation as string | undefined) ?? '',\n depth,\n via: { relation, confidence, dependsOn: current },\n });\n });\n }\n\n const roundNodes = [...nextFrontier.values()].sort((a, b) => a.id.localeCompare(b.id));\n for (const node of roundNodes) {\n if (affected.length >= limit) {\n truncated = true;\n break;\n }\n visited.add(node.id);\n affected.push(node);\n }\n frontier = roundNodes.filter((n) => visited.has(n.id)).map((n) => n.id);\n }\n\n const byConfidence: Record<Confidence, number> = { EXTRACTED: 0, INFERRED: 0, AMBIGUOUS: 0 };\n for (const node of affected) byConfidence[node.via.confidence] += 1;\n\n return { target, affected, byConfidence, maxDepth, truncated };\n}\n","import type Graph from 'graphology';\nimport { scoreNodes } from './query.js';\n\n/**\n * `graphify context` — the working-set pack. Instead of returning node\n * names for the agent to chase (query -> then read whole files anyway),\n * this fuses the two steps: the graph knows every entity's file and line,\n * so rank the nodes relevant to the task, read just the relevant line\n * ranges, and pack the actual code into a token budget. One call returns\n * the code an agent needs to start a task.\n */\n\nexport interface ContextSnippet {\n nodeId: string;\n label: string;\n file: string;\n startLine: number;\n endLine: number;\n code: string;\n /** Why this snippet is in the pack (lexical match / neighbor-of relation). */\n reason: string;\n score: number;\n}\n\nexport interface ContextPack {\n task: string;\n snippets: ContextSnippet[];\n /** Approximate tokens used out of the budget. */\n tokens: number;\n tokenBudget: number;\n /** Ranked nodes that didn't fit the budget — the agent can pull them explicitly. */\n overflow: Array<{ nodeId: string; file: string }>;\n}\n\nexport interface ContextOptions {\n /** Approximate token cap for the pack (default 4000). */\n tokenBudget?: number;\n maxSeeds?: number;\n /** Cap on snippet length in lines (default 60). */\n maxSnippetLines?: number;\n}\n\nexport type FileReader = (path: string) => Promise<string>;\n\nconst DEFAULT_CONTEXT_BUDGET = 4000;\nconst DEFAULT_MAX_SEEDS = 8;\nconst DEFAULT_MAX_SNIPPET_LINES = 60;\n/** A neighbor inherits this fraction of the score of the node that pulled it in. */\nconst NEIGHBOR_DECAY = 0.4;\n\nconst tokensOf = (text: string): number => Math.ceil(text.length / 4);\n\nfunction lineNumberOf(sourceLocation: string): number | null {\n const match = /^L(\\d+)$/.exec(sourceLocation);\n return match ? Number(match[1]) : null;\n}\n\nfunction isReadableFile(sourceFile: string): boolean {\n return sourceFile !== '' && !sourceFile.startsWith('<') && !sourceFile.includes('://');\n}\n\ninterface RankedNode {\n id: string;\n score: number;\n reason: string;\n}\n\n/**\n * Rank nodes for the task: lexical seeds first (their match score), then\n * their graph neighbors at a decayed score, labeled with the relation that\n * connects them. Deterministic (score desc, id asc).\n */\nexport function rankForTask(graph: Graph, task: string, maxSeeds = DEFAULT_MAX_SEEDS): RankedNode[] {\n const seeds = scoreNodes(graph, task).slice(0, maxSeeds);\n const ranked = new Map<string, RankedNode>();\n for (const seed of seeds) {\n ranked.set(seed.id, { id: seed.id, score: seed.score, reason: 'matches the task' });\n }\n\n for (const seed of seeds) {\n graph.forEachEdge(seed.id, (_edge, attrs, source, target) => {\n const neighbor = source === seed.id ? target : source;\n if (ranked.has(neighbor)) return;\n const relation = String(attrs.relation);\n const direction = source === seed.id ? `${relation} ->` : `<- ${relation}`;\n ranked.set(neighbor, {\n id: neighbor,\n score: seed.score * NEIGHBOR_DECAY,\n reason: `${direction} ${seed.label}`,\n });\n });\n }\n\n return [...ranked.values()].sort((a, b) => b.score - a.score || a.id.localeCompare(b.id));\n}\n\n/**\n * Extract the snippet for a node: from its declaration line to just before\n * the next known entity in the same file (capped), so snippets align with\n * real code block boundaries without needing a parser here.\n */\nfunction snippetRange(\n startLine: number,\n fileLineCount: number,\n entityStartsInFile: number[],\n maxLines: number,\n): { start: number; end: number } {\n const nextStart = entityStartsInFile.find((l) => l > startLine);\n const hardEnd = nextStart !== undefined ? nextStart - 1 : fileLineCount;\n return { start: startLine, end: Math.min(hardEnd, startLine + maxLines - 1) };\n}\n\nexport async function buildContextPack(\n graph: Graph,\n task: string,\n readFile: FileReader,\n options: ContextOptions = {},\n): Promise<ContextPack> {\n const tokenBudget = options.tokenBudget ?? DEFAULT_CONTEXT_BUDGET;\n const maxSnippetLines = options.maxSnippetLines ?? DEFAULT_MAX_SNIPPET_LINES;\n const ranked = rankForTask(graph, task, options.maxSeeds);\n\n // Every known entity start line per file — used to end snippets at the\n // next declaration instead of an arbitrary window.\n const entityStarts = new Map<string, number[]>();\n graph.forEachNode((_id, attrs) => {\n const file = attrs.sourceFile as string | undefined;\n const line = lineNumberOf((attrs.sourceLocation as string | undefined) ?? '');\n if (file && line !== null) {\n const starts = entityStarts.get(file) ?? [];\n starts.push(line);\n entityStarts.set(file, starts);\n }\n });\n for (const starts of entityStarts.values()) starts.sort((a, b) => a - b);\n\n const fileCache = new Map<string, string[] | null>();\n const readLines = async (file: string): Promise<string[] | null> => {\n const cached = fileCache.get(file);\n if (cached !== undefined) return cached;\n let lines: string[] | null;\n try {\n lines = (await readFile(file)).split('\\n');\n } catch {\n lines = null;\n }\n fileCache.set(file, lines);\n return lines;\n };\n\n const snippets: ContextSnippet[] = [];\n const overflow: Array<{ nodeId: string; file: string }> = [];\n const coveredRanges = new Map<string, Array<{ start: number; end: number }>>();\n let tokens = 0;\n\n for (const node of ranked) {\n const attrs = graph.getNodeAttributes(node.id);\n const file = (attrs.sourceFile as string | undefined) ?? '';\n const startLine = lineNumberOf((attrs.sourceLocation as string | undefined) ?? '');\n if (!isReadableFile(file) || startLine === null) continue;\n\n const lines = await readLines(file);\n if (lines === null) continue;\n\n const { start, end } = snippetRange(startLine, lines.length, entityStarts.get(file) ?? [], maxSnippetLines);\n\n // Skip if an already-packed snippet from the same file covers this range.\n const covered = (coveredRanges.get(file) ?? []).some((r) => start >= r.start && end <= r.end);\n if (covered) continue;\n\n const code = lines.slice(start - 1, end).join('\\n');\n const cost = tokensOf(code) + 15; // header overhead\n if (tokens + cost > tokenBudget) {\n overflow.push({ nodeId: node.id, file: `${file}:L${startLine}` });\n continue;\n }\n\n tokens += cost;\n snippets.push({\n nodeId: node.id,\n label: (attrs.label as string | undefined) ?? node.id,\n file,\n startLine: start,\n endLine: end,\n code,\n reason: node.reason,\n score: node.score,\n });\n const ranges = coveredRanges.get(file) ?? [];\n ranges.push({ start, end });\n coveredRanges.set(file, ranges);\n }\n\n return { task, snippets, tokens, tokenBudget, overflow };\n}\n\n/** Render a pack as agent-ready markdown. */\nexport function renderContextPack(pack: ContextPack): string {\n const lines: string[] = [\n `# Context pack: ${pack.task}`,\n '',\n `_~${pack.tokens} of ${pack.tokenBudget} token budget, ${pack.snippets.length} snippet(s)._`,\n '',\n ];\n\n if (pack.snippets.length === 0) {\n lines.push('No matching code found — try different keywords.');\n return lines.join('\\n');\n }\n\n for (const snippet of pack.snippets) {\n lines.push(`## ${snippet.label} — \\`${snippet.file}:L${snippet.startLine}-L${snippet.endLine}\\``);\n lines.push(`_${snippet.reason}_`);\n lines.push('```');\n lines.push(snippet.code);\n lines.push('```');\n lines.push('');\n }\n\n if (pack.overflow.length > 0) {\n lines.push(`## Didn't fit the budget (${pack.overflow.length})`);\n for (const item of pack.overflow.slice(0, 15)) {\n lines.push(`- ${item.nodeId} (${item.file})`);\n }\n lines.push('');\n }\n\n return lines.join('\\n');\n}\n","import type Graph from 'graphology';\nimport { affectedBy } from './impact.js';\nimport { resolveNode } from './query.js';\nimport type { Relation } from './types.js';\n\n/**\n * `graphify tests` — structural test selection. Coverage tools need\n * instrumentation and a full test run to know what covers what; the graph\n * already knows, statically: a test file imports/calls the code it\n * exercises, so the test files inside a change's reverse blast radius are\n * exactly the ones worth running. Cross-language, no instrumentation.\n */\n\n/** Test-file naming conventions across the supported languages. */\nconst TEST_FILE_PATTERNS: RegExp[] = [\n /(^|\\/)tests?\\//, // tests/ or test/ directory\n /(^|\\/)__tests__\\//,\n /(^|\\/)spec\\//,\n /\\.test\\.[cm]?[jt]sx?$/,\n /\\.spec\\.[cm]?[jt]sx?$/,\n /(^|\\/)test_[^/]+\\.py$/,\n /_test\\.py$/,\n /_test\\.go$/,\n /Tests?\\.(java|cs)$/,\n /_spec\\.rb$/,\n /_test\\.rb$/,\n];\n\nexport function isTestFile(path: string): boolean {\n return TEST_FILE_PATTERNS.some((p) => p.test(path));\n}\n\nexport interface TestSelection {\n /** What the selection was computed for (resolved node id, or the changed files). */\n target: string;\n /** Unique test files, most directly affected first. */\n testFiles: Array<{ file: string; depth: number; via: string }>;\n /** How many non-test nodes were in the blast radius (context for confidence). */\n affectedNodeCount: number;\n /**\n * `usage` — selected by following actual use (calls/inheritance), tight;\n * `import-reachability` — the broad fallback: anything that can reach the\n * target through imports/re-exports, which over-includes files that share\n * a barrel entry with the target.\n */\n precision: 'usage' | 'import-reachability';\n}\n\nconst TEST_REACH_DEPTH = 4;\nconst TEST_REACH_LIMIT = 2000;\n\n/** Relations that mean \"actually uses it\", as opposed to \"can merely reach it through an import\". */\nconst USAGE_RELATIONS: Relation[] = ['calls', 'inherits', 'implements', 'mixes_in', 'embeds', 'references'];\n\nfunction selectionFromImpact(\n graph: Graph,\n targetIds: string[],\n targetLabel: string,\n relations?: Relation[],\n): TestSelection {\n const best = new Map<string, { depth: number; via: string }>();\n let affectedNodeCount = 0;\n\n for (const id of targetIds) {\n const impact = affectedBy(graph, id, { maxDepth: TEST_REACH_DEPTH, limit: TEST_REACH_LIMIT, relations });\n if (!impact) continue;\n affectedNodeCount += impact.affected.length;\n for (const node of impact.affected) {\n if (!isTestFile(node.sourceFile)) continue;\n const existing = best.get(node.sourceFile);\n if (!existing || node.depth < existing.depth) {\n best.set(node.sourceFile, { depth: node.depth, via: node.via.dependsOn });\n }\n }\n }\n\n const testFiles = [...best.entries()]\n .map(([file, info]) => ({ file, depth: info.depth, via: info.via }))\n .sort((a, b) => a.depth - b.depth || a.file.localeCompare(b.file));\n\n return {\n target: targetLabel,\n testFiles,\n affectedNodeCount,\n precision: relations ? 'usage' : 'import-reachability',\n };\n}\n\n/** A file node's entities (so a usage-relation traversal has call targets to start from). */\nfunction entityRootsOf(graph: Graph, id: string): string[] {\n if (id.includes('::')) return [id];\n const roots: string[] = [];\n graph.forEachOutEdge(id, (_edge, attrs, _source, target) => {\n if (String(attrs.relation) === 'contains') roots.push(target);\n });\n roots.sort((a, b) => a.localeCompare(b));\n return roots.length > 0 ? roots : [id];\n}\n\n/**\n * Test files reachable (reverse) from the node best matching `nodeQuery`.\n * Tries a tight usage-relation pass first (tests that actually CALL the\n * target — precise even when many files share a barrel entry point); falls\n * back to full import-reachability when usage finds nothing. Null if\n * nothing matches the query.\n */\nexport function testsForNode(graph: Graph, nodeQuery: string): TestSelection | null {\n const match = resolveNode(graph, nodeQuery);\n if (!match) return null;\n\n const precise = selectionFromImpact(graph, entityRootsOf(graph, match.id), match.id, USAGE_RELATIONS);\n if (precise.testFiles.length > 0) return precise;\n\n return selectionFromImpact(graph, [match.id], match.id);\n}\n\n/**\n * Test files reachable from any of the given changed files (as reported by\n * `git diff --name-only`). Changed test files select themselves. Unknown\n * files (not in the graph) are skipped.\n */\nexport function testsForChangedFiles(graph: Graph, changedFiles: string[]): TestSelection {\n const fileIds: string[] = [];\n const selfSelected = new Map<string, { depth: number; via: string }>();\n\n for (const file of changedFiles) {\n if (isTestFile(file)) {\n selfSelected.set(file, { depth: 0, via: '(changed directly)' });\n continue;\n }\n if (graph.hasNode(file)) fileIds.push(file);\n }\n\n const label = changedFiles.join(', ');\n const preciseRoots = fileIds.flatMap((f) => entityRootsOf(graph, f));\n let selection = selectionFromImpact(graph, preciseRoots, label, USAGE_RELATIONS);\n if (selection.testFiles.length === 0) {\n selection = selectionFromImpact(graph, fileIds, label);\n }\n for (const [file, info] of selfSelected) {\n if (!selection.testFiles.some((t) => t.file === file)) {\n selection.testFiles.push({ file, depth: info.depth, via: info.via });\n }\n }\n selection.testFiles.sort((a, b) => a.depth - b.depth || a.file.localeCompare(b.file));\n return selection;\n}\n"],"mappings":"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","names":["path","DEFAULT_MAX_SEEDS","readFile","path"]}
|
|
File without changes
|