@dreamtree-org/graphify 1.1.2 → 1.1.3
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-PXVI2L45.js → chunk-NS5HB65S.js} +116 -42
- package/dist/chunk-NS5HB65S.js.map +1 -0
- package/dist/{chunk-YT7B6DOD.js → chunk-ZK3TA6FW.js} +33 -7
- package/dist/chunk-ZK3TA6FW.js.map +1 -0
- package/dist/cli/index.cjs +149 -48
- package/dist/cli/index.cjs.map +1 -1
- package/dist/cli/index.js +4 -3
- package/dist/cli/index.js.map +1 -1
- package/dist/index.cjs +147 -47
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +16 -1
- package/dist/index.d.ts +16 -1
- package/dist/index.js +2 -2
- package/dist/mcp/server.cjs +26 -5
- package/dist/mcp/server.cjs.map +1 -1
- package/dist/mcp/server.js +1 -1
- package/package.json +1 -1
- package/dist/chunk-PXVI2L45.js.map +0 -1
- package/dist/chunk-YT7B6DOD.js.map +0 -1
|
@@ -116,7 +116,10 @@ function scoreNodes(graph, query) {
|
|
|
116
116
|
}
|
|
117
117
|
if (score > 0) matches.push({ id, label, score });
|
|
118
118
|
});
|
|
119
|
-
|
|
119
|
+
const placeholderRank = (id) => id.startsWith("module:") || id.startsWith("external:") ? 1 : 0;
|
|
120
|
+
matches.sort(
|
|
121
|
+
(a, b) => b.score - a.score || placeholderRank(a.id) - placeholderRank(b.id) || a.id.localeCompare(b.id)
|
|
122
|
+
);
|
|
120
123
|
return matches;
|
|
121
124
|
}
|
|
122
125
|
function resolveNode(graph, query) {
|
|
@@ -299,6 +302,7 @@ function affectedBy(graph, nodeQuery, options = {}) {
|
|
|
299
302
|
if (!target) return null;
|
|
300
303
|
const maxDepth = options.maxDepth ?? DEFAULT_IMPACT_DEPTH;
|
|
301
304
|
const limit = options.limit ?? DEFAULT_IMPACT_LIMIT;
|
|
305
|
+
const followed = options.relations ? new Set(options.relations) : DEPENDENCY_RELATIONS;
|
|
302
306
|
const affected = [];
|
|
303
307
|
const visited = /* @__PURE__ */ new Set([target.id]);
|
|
304
308
|
let frontier = [target.id];
|
|
@@ -309,7 +313,7 @@ function affectedBy(graph, nodeQuery, options = {}) {
|
|
|
309
313
|
graph.forEachInEdge(current, (_edge, edgeAttrs, source) => {
|
|
310
314
|
if (visited.has(source)) return;
|
|
311
315
|
const relation = edgeAttrs.relation;
|
|
312
|
-
if (!
|
|
316
|
+
if (!followed.has(relation)) return;
|
|
313
317
|
const confidence = edgeAttrs.confidence;
|
|
314
318
|
const existing = nextFrontier.get(source);
|
|
315
319
|
if (existing && CONFIDENCE_RANK[existing.via.confidence] >= CONFIDENCE_RANK[confidence]) return;
|
|
@@ -493,11 +497,12 @@ function isTestFile(path2) {
|
|
|
493
497
|
}
|
|
494
498
|
var TEST_REACH_DEPTH = 4;
|
|
495
499
|
var TEST_REACH_LIMIT = 2e3;
|
|
496
|
-
|
|
500
|
+
var USAGE_RELATIONS = ["calls", "inherits", "implements", "mixes_in", "embeds", "references"];
|
|
501
|
+
function selectionFromImpact(graph, targetIds, targetLabel, relations) {
|
|
497
502
|
const best = /* @__PURE__ */ new Map();
|
|
498
503
|
let affectedNodeCount = 0;
|
|
499
504
|
for (const id of targetIds) {
|
|
500
|
-
const impact = affectedBy(graph, id, { maxDepth: TEST_REACH_DEPTH, limit: TEST_REACH_LIMIT });
|
|
505
|
+
const impact = affectedBy(graph, id, { maxDepth: TEST_REACH_DEPTH, limit: TEST_REACH_LIMIT, relations });
|
|
501
506
|
if (!impact) continue;
|
|
502
507
|
affectedNodeCount += impact.affected.length;
|
|
503
508
|
for (const node of impact.affected) {
|
|
@@ -509,11 +514,27 @@ function selectionFromImpact(graph, targetIds, targetLabel) {
|
|
|
509
514
|
}
|
|
510
515
|
}
|
|
511
516
|
const testFiles = [...best.entries()].map(([file, info]) => ({ file, depth: info.depth, via: info.via })).sort((a, b) => a.depth - b.depth || a.file.localeCompare(b.file));
|
|
512
|
-
return {
|
|
517
|
+
return {
|
|
518
|
+
target: targetLabel,
|
|
519
|
+
testFiles,
|
|
520
|
+
affectedNodeCount,
|
|
521
|
+
precision: relations ? "usage" : "import-reachability"
|
|
522
|
+
};
|
|
523
|
+
}
|
|
524
|
+
function entityRootsOf(graph, id) {
|
|
525
|
+
if (id.includes("::")) return [id];
|
|
526
|
+
const roots = [];
|
|
527
|
+
graph.forEachOutEdge(id, (_edge, attrs, _source, target) => {
|
|
528
|
+
if (String(attrs.relation) === "contains") roots.push(target);
|
|
529
|
+
});
|
|
530
|
+
roots.sort((a, b) => a.localeCompare(b));
|
|
531
|
+
return roots.length > 0 ? roots : [id];
|
|
513
532
|
}
|
|
514
533
|
function testsForNode(graph, nodeQuery) {
|
|
515
534
|
const match = resolveNode(graph, nodeQuery);
|
|
516
535
|
if (!match) return null;
|
|
536
|
+
const precise = selectionFromImpact(graph, entityRootsOf(graph, match.id), match.id, USAGE_RELATIONS);
|
|
537
|
+
if (precise.testFiles.length > 0) return precise;
|
|
517
538
|
return selectionFromImpact(graph, [match.id], match.id);
|
|
518
539
|
}
|
|
519
540
|
function testsForChangedFiles(graph, changedFiles) {
|
|
@@ -526,7 +547,12 @@ function testsForChangedFiles(graph, changedFiles) {
|
|
|
526
547
|
}
|
|
527
548
|
if (graph.hasNode(file)) fileIds.push(file);
|
|
528
549
|
}
|
|
529
|
-
const
|
|
550
|
+
const label = changedFiles.join(", ");
|
|
551
|
+
const preciseRoots = fileIds.flatMap((f) => entityRootsOf(graph, f));
|
|
552
|
+
let selection = selectionFromImpact(graph, preciseRoots, label, USAGE_RELATIONS);
|
|
553
|
+
if (selection.testFiles.length === 0) {
|
|
554
|
+
selection = selectionFromImpact(graph, fileIds, label);
|
|
555
|
+
}
|
|
530
556
|
for (const [file, info] of selfSelected) {
|
|
531
557
|
if (!selection.testFiles.some((t) => t.file === file)) {
|
|
532
558
|
selection.testFiles.push({ file, depth: info.depth, via: info.via });
|
|
@@ -551,4 +577,4 @@ export {
|
|
|
551
577
|
testsForNode,
|
|
552
578
|
testsForChangedFiles
|
|
553
579
|
};
|
|
554
|
-
//# sourceMappingURL=chunk-
|
|
580
|
+
//# sourceMappingURL=chunk-ZK3TA6FW.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/graphStore.ts","../src/query.ts","../src/impact.ts","../src/context.ts","../src/testmap.ts"],"sourcesContent":["import * as fs from 'node:fs/promises';\nimport * as path from 'node:path';\nimport Graph from 'graphology';\nimport { validateGraphPath } from './security.js';\n\n/**\n * Load a previously-exported graph.json back into a graphology Graph.\n * Always goes through security.validateGraphPath() (path-traversal guard —\n * see SECURITY.md) so a caller-supplied `outDir`/CLI arg can never make\n * this read outside the graphify-out/ directory it resolves to.\n */\nexport async function loadGraph(outDir: string = path.join(process.cwd(), 'graphify-out')): Promise<Graph> {\n const jsonPath = validateGraphPath('graph.json', outDir);\n const raw = await fs.readFile(jsonPath, 'utf-8');\n const data: unknown = JSON.parse(raw);\n return Graph.from(data as Parameters<typeof Graph.from>[0]);\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"]}
|
package/dist/cli/index.cjs
CHANGED
|
@@ -332,7 +332,10 @@ function scoreNodes(graph, query) {
|
|
|
332
332
|
}
|
|
333
333
|
if (score > 0) matches.push({ id, label, score });
|
|
334
334
|
});
|
|
335
|
-
|
|
335
|
+
const placeholderRank = (id) => id.startsWith("module:") || id.startsWith("external:") ? 1 : 0;
|
|
336
|
+
matches.sort(
|
|
337
|
+
(a, b) => b.score - a.score || placeholderRank(a.id) - placeholderRank(b.id) || a.id.localeCompare(b.id)
|
|
338
|
+
);
|
|
336
339
|
return matches;
|
|
337
340
|
}
|
|
338
341
|
function resolveNode(graph, query) {
|
|
@@ -542,6 +545,7 @@ function affectedBy(graph, nodeQuery, options = {}) {
|
|
|
542
545
|
if (!target) return null;
|
|
543
546
|
const maxDepth = options.maxDepth ?? DEFAULT_IMPACT_DEPTH;
|
|
544
547
|
const limit = options.limit ?? DEFAULT_IMPACT_LIMIT;
|
|
548
|
+
const followed = options.relations ? new Set(options.relations) : DEPENDENCY_RELATIONS;
|
|
545
549
|
const affected = [];
|
|
546
550
|
const visited = /* @__PURE__ */ new Set([target.id]);
|
|
547
551
|
let frontier = [target.id];
|
|
@@ -552,7 +556,7 @@ function affectedBy(graph, nodeQuery, options = {}) {
|
|
|
552
556
|
graph.forEachInEdge(current, (_edge, edgeAttrs, source) => {
|
|
553
557
|
if (visited.has(source)) return;
|
|
554
558
|
const relation = edgeAttrs.relation;
|
|
555
|
-
if (!
|
|
559
|
+
if (!followed.has(relation)) return;
|
|
556
560
|
const confidence = edgeAttrs.confidence;
|
|
557
561
|
const existing = nextFrontier.get(source);
|
|
558
562
|
if (existing && CONFIDENCE_RANK2[existing.via.confidence] >= CONFIDENCE_RANK2[confidence]) return;
|
|
@@ -611,11 +615,11 @@ var init_impact = __esm({
|
|
|
611
615
|
function isTestFile(path18) {
|
|
612
616
|
return TEST_FILE_PATTERNS.some((p) => p.test(path18));
|
|
613
617
|
}
|
|
614
|
-
function selectionFromImpact(graph, targetIds, targetLabel) {
|
|
618
|
+
function selectionFromImpact(graph, targetIds, targetLabel, relations) {
|
|
615
619
|
const best = /* @__PURE__ */ new Map();
|
|
616
620
|
let affectedNodeCount = 0;
|
|
617
621
|
for (const id of targetIds) {
|
|
618
|
-
const impact = affectedBy(graph, id, { maxDepth: TEST_REACH_DEPTH, limit: TEST_REACH_LIMIT });
|
|
622
|
+
const impact = affectedBy(graph, id, { maxDepth: TEST_REACH_DEPTH, limit: TEST_REACH_LIMIT, relations });
|
|
619
623
|
if (!impact) continue;
|
|
620
624
|
affectedNodeCount += impact.affected.length;
|
|
621
625
|
for (const node of impact.affected) {
|
|
@@ -627,11 +631,27 @@ function selectionFromImpact(graph, targetIds, targetLabel) {
|
|
|
627
631
|
}
|
|
628
632
|
}
|
|
629
633
|
const testFiles = [...best.entries()].map(([file, info]) => ({ file, depth: info.depth, via: info.via })).sort((a, b) => a.depth - b.depth || a.file.localeCompare(b.file));
|
|
630
|
-
return {
|
|
634
|
+
return {
|
|
635
|
+
target: targetLabel,
|
|
636
|
+
testFiles,
|
|
637
|
+
affectedNodeCount,
|
|
638
|
+
precision: relations ? "usage" : "import-reachability"
|
|
639
|
+
};
|
|
640
|
+
}
|
|
641
|
+
function entityRootsOf(graph, id) {
|
|
642
|
+
if (id.includes("::")) return [id];
|
|
643
|
+
const roots = [];
|
|
644
|
+
graph.forEachOutEdge(id, (_edge, attrs, _source, target) => {
|
|
645
|
+
if (String(attrs.relation) === "contains") roots.push(target);
|
|
646
|
+
});
|
|
647
|
+
roots.sort((a, b) => a.localeCompare(b));
|
|
648
|
+
return roots.length > 0 ? roots : [id];
|
|
631
649
|
}
|
|
632
650
|
function testsForNode(graph, nodeQuery) {
|
|
633
651
|
const match = resolveNode(graph, nodeQuery);
|
|
634
652
|
if (!match) return null;
|
|
653
|
+
const precise = selectionFromImpact(graph, entityRootsOf(graph, match.id), match.id, USAGE_RELATIONS);
|
|
654
|
+
if (precise.testFiles.length > 0) return precise;
|
|
635
655
|
return selectionFromImpact(graph, [match.id], match.id);
|
|
636
656
|
}
|
|
637
657
|
function testsForChangedFiles(graph, changedFiles) {
|
|
@@ -644,7 +664,12 @@ function testsForChangedFiles(graph, changedFiles) {
|
|
|
644
664
|
}
|
|
645
665
|
if (graph.hasNode(file)) fileIds.push(file);
|
|
646
666
|
}
|
|
647
|
-
const
|
|
667
|
+
const label = changedFiles.join(", ");
|
|
668
|
+
const preciseRoots = fileIds.flatMap((f) => entityRootsOf(graph, f));
|
|
669
|
+
let selection = selectionFromImpact(graph, preciseRoots, label, USAGE_RELATIONS);
|
|
670
|
+
if (selection.testFiles.length === 0) {
|
|
671
|
+
selection = selectionFromImpact(graph, fileIds, label);
|
|
672
|
+
}
|
|
648
673
|
for (const [file, info] of selfSelected) {
|
|
649
674
|
if (!selection.testFiles.some((t) => t.file === file)) {
|
|
650
675
|
selection.testFiles.push({ file, depth: info.depth, via: info.via });
|
|
@@ -653,7 +678,7 @@ function testsForChangedFiles(graph, changedFiles) {
|
|
|
653
678
|
selection.testFiles.sort((a, b) => a.depth - b.depth || a.file.localeCompare(b.file));
|
|
654
679
|
return selection;
|
|
655
680
|
}
|
|
656
|
-
var TEST_FILE_PATTERNS, TEST_REACH_DEPTH, TEST_REACH_LIMIT;
|
|
681
|
+
var TEST_FILE_PATTERNS, TEST_REACH_DEPTH, TEST_REACH_LIMIT, USAGE_RELATIONS;
|
|
657
682
|
var init_testmap = __esm({
|
|
658
683
|
"src/testmap.ts"() {
|
|
659
684
|
"use strict";
|
|
@@ -676,6 +701,7 @@ var init_testmap = __esm({
|
|
|
676
701
|
];
|
|
677
702
|
TEST_REACH_DEPTH = 4;
|
|
678
703
|
TEST_REACH_LIMIT = 2e3;
|
|
704
|
+
USAGE_RELATIONS = ["calls", "inherits", "implements", "mixes_in", "embeds", "references"];
|
|
679
705
|
}
|
|
680
706
|
});
|
|
681
707
|
|
|
@@ -3213,13 +3239,11 @@ async function extractTypeScript(filePath, displayPath) {
|
|
|
3213
3239
|
});
|
|
3214
3240
|
}
|
|
3215
3241
|
function importTargetId(binding, property) {
|
|
3216
|
-
if (
|
|
3217
|
-
|
|
3218
|
-
|
|
3219
|
-
|
|
3220
|
-
|
|
3221
|
-
return entityId(binding.ref.id, property);
|
|
3222
|
-
}
|
|
3242
|
+
if (binding.kind === "named" && binding.importedName) {
|
|
3243
|
+
return entityId(binding.ref.id, binding.importedName);
|
|
3244
|
+
}
|
|
3245
|
+
if (binding.kind === "namespace" && property) {
|
|
3246
|
+
return entityId(binding.ref.id, property);
|
|
3223
3247
|
}
|
|
3224
3248
|
return binding.ref.id;
|
|
3225
3249
|
}
|
|
@@ -3257,24 +3281,35 @@ async function extractTypeScript(filePath, displayPath) {
|
|
|
3257
3281
|
}
|
|
3258
3282
|
}
|
|
3259
3283
|
}
|
|
3284
|
+
function unwrapExpression(node) {
|
|
3285
|
+
let current = node;
|
|
3286
|
+
while (current.type === "as_expression" || current.type === "satisfies_expression" || current.type === "non_null_expression" || current.type === "parenthesized_expression") {
|
|
3287
|
+
const inner = namedChildren(current)[0];
|
|
3288
|
+
if (!inner) break;
|
|
3289
|
+
current = inner;
|
|
3290
|
+
}
|
|
3291
|
+
return current;
|
|
3292
|
+
}
|
|
3260
3293
|
function handleTopLevelVariableFunctions(node) {
|
|
3294
|
+
const isExported = node.parent?.type === "export_statement";
|
|
3261
3295
|
for (const declarator of namedChildren(node)) {
|
|
3262
3296
|
if (declarator.type !== "variable_declarator") continue;
|
|
3263
3297
|
const value = declarator.childForFieldName("value");
|
|
3264
3298
|
if (!value) continue;
|
|
3265
|
-
|
|
3299
|
+
const core = unwrapExpression(value);
|
|
3300
|
+
if (core.type === "arrow_function" || core.type === "function_expression") {
|
|
3266
3301
|
const name = declarator.childForFieldName("name")?.text;
|
|
3267
3302
|
if (!name) continue;
|
|
3268
3303
|
const id = entityId(sourceFile, name);
|
|
3269
3304
|
builder.addNode({ id, label: `function ${name}`, sourceFile, sourceLocation: lineOf(declarator) });
|
|
3270
3305
|
builder.addEdge({ source: fileId, target: id, relation: "contains", confidence: "EXTRACTED" });
|
|
3271
3306
|
nameIndex.set(name, id);
|
|
3272
|
-
functionNodeMap.set(
|
|
3307
|
+
functionNodeMap.set(core.id, id);
|
|
3273
3308
|
continue;
|
|
3274
3309
|
}
|
|
3275
|
-
if (
|
|
3276
|
-
const callee =
|
|
3277
|
-
const specifier = callee?.type === "identifier" && callee.text === "require" ? firstStringArgument(
|
|
3310
|
+
if (core.type === "call_expression") {
|
|
3311
|
+
const callee = core.childForFieldName("function");
|
|
3312
|
+
const specifier = callee?.type === "identifier" && callee.text === "require" ? firstStringArgument(core) : null;
|
|
3278
3313
|
if (specifier) {
|
|
3279
3314
|
const nameNode = declarator.childForFieldName("name");
|
|
3280
3315
|
if (nameNode) {
|
|
@@ -3282,6 +3317,21 @@ async function extractTypeScript(filePath, displayPath) {
|
|
|
3282
3317
|
addModuleNode(ref);
|
|
3283
3318
|
registerRequireBinding(nameNode, ref);
|
|
3284
3319
|
}
|
|
3320
|
+
continue;
|
|
3321
|
+
}
|
|
3322
|
+
}
|
|
3323
|
+
if (isExported) {
|
|
3324
|
+
const name = declarator.childForFieldName("name")?.text;
|
|
3325
|
+
if (!name) continue;
|
|
3326
|
+
const id = entityId(sourceFile, name);
|
|
3327
|
+
builder.addNode({ id, label: `const ${name}`, sourceFile, sourceLocation: lineOf(declarator) });
|
|
3328
|
+
builder.addEdge({ source: fileId, target: id, relation: "contains", confidence: "EXTRACTED" });
|
|
3329
|
+
nameIndex.set(name, id);
|
|
3330
|
+
if (core.type === "identifier") {
|
|
3331
|
+
const aliased = nameIndex.get(core.text);
|
|
3332
|
+
if (aliased !== void 0 && aliased !== id) {
|
|
3333
|
+
builder.addEdge({ source: id, target: aliased, relation: "references", confidence: "EXTRACTED" });
|
|
3334
|
+
}
|
|
3285
3335
|
}
|
|
3286
3336
|
}
|
|
3287
3337
|
}
|
|
@@ -3661,13 +3711,12 @@ function candidatePaths(guessed) {
|
|
|
3661
3711
|
}
|
|
3662
3712
|
return candidates.filter((c) => c !== guessed);
|
|
3663
3713
|
}
|
|
3664
|
-
function uniqueMatch(
|
|
3714
|
+
function uniqueMatch(candidates, real) {
|
|
3665
3715
|
const matches = /* @__PURE__ */ new Set();
|
|
3666
3716
|
for (const candidate of candidates) {
|
|
3667
|
-
if (
|
|
3717
|
+
if (real.has(candidate)) matches.add(candidate);
|
|
3668
3718
|
}
|
|
3669
|
-
|
|
3670
|
-
return [...matches][0];
|
|
3719
|
+
return matches.size === 1 ? [...matches][0] : null;
|
|
3671
3720
|
}
|
|
3672
3721
|
function resolveCrossFileReferences(extractions, options = {}) {
|
|
3673
3722
|
const selfNames = options.selfNames ?? [];
|
|
@@ -3688,20 +3737,24 @@ function resolveCrossFileReferences(extractions, options = {}) {
|
|
|
3688
3737
|
}
|
|
3689
3738
|
}
|
|
3690
3739
|
const remap = /* @__PURE__ */ new Map();
|
|
3691
|
-
|
|
3692
|
-
|
|
3693
|
-
|
|
3694
|
-
|
|
3695
|
-
|
|
3696
|
-
|
|
3697
|
-
|
|
3698
|
-
|
|
3699
|
-
|
|
3700
|
-
|
|
3740
|
+
let resolvedEndpoints = 0;
|
|
3741
|
+
const applyRemap = (resolveId) => {
|
|
3742
|
+
for (const extraction of extractions) {
|
|
3743
|
+
for (const edge of extraction.edges) {
|
|
3744
|
+
const source = resolveId(edge.source);
|
|
3745
|
+
if (source !== null) {
|
|
3746
|
+
edge.source = source;
|
|
3747
|
+
resolvedEndpoints++;
|
|
3748
|
+
}
|
|
3749
|
+
const target = resolveId(edge.target);
|
|
3750
|
+
if (target !== null) {
|
|
3751
|
+
edge.target = target;
|
|
3752
|
+
resolvedEndpoints++;
|
|
3701
3753
|
}
|
|
3702
3754
|
}
|
|
3703
|
-
return null;
|
|
3704
3755
|
}
|
|
3756
|
+
};
|
|
3757
|
+
const resolvePathId = (id) => {
|
|
3705
3758
|
if (isOpaque(id)) return null;
|
|
3706
3759
|
const cached = remap.get(id);
|
|
3707
3760
|
if (cached !== void 0) return cached;
|
|
@@ -3711,30 +3764,77 @@ function resolveCrossFileReferences(extractions, options = {}) {
|
|
|
3711
3764
|
if (realIds.has(id)) return null;
|
|
3712
3765
|
const guessedPath = id.slice(0, sep3);
|
|
3713
3766
|
const name = id.slice(sep3);
|
|
3714
|
-
|
|
3715
|
-
resolved = uniqueMatch(id, candidates, realIds);
|
|
3767
|
+
resolved = uniqueMatch(candidatePaths(guessedPath).map((p) => `${p}${name}`), realIds);
|
|
3716
3768
|
} else {
|
|
3717
3769
|
if (realFiles.has(id)) return null;
|
|
3718
|
-
resolved = uniqueMatch(
|
|
3770
|
+
resolved = uniqueMatch(candidatePaths(id), realFiles);
|
|
3719
3771
|
}
|
|
3720
3772
|
if (resolved !== null) remap.set(id, resolved);
|
|
3721
3773
|
return resolved;
|
|
3722
3774
|
};
|
|
3723
|
-
|
|
3775
|
+
applyRemap(resolvePathId);
|
|
3776
|
+
const namedAlias = /* @__PURE__ */ new Map();
|
|
3777
|
+
const starTargets = /* @__PURE__ */ new Map();
|
|
3724
3778
|
for (const extraction of extractions) {
|
|
3725
3779
|
for (const edge of extraction.edges) {
|
|
3726
|
-
|
|
3727
|
-
|
|
3728
|
-
|
|
3729
|
-
|
|
3730
|
-
}
|
|
3731
|
-
|
|
3732
|
-
|
|
3733
|
-
edge.
|
|
3734
|
-
resolvedEndpoints++;
|
|
3780
|
+
if (edge.relation !== "re_exports" || !realFiles.has(edge.source)) continue;
|
|
3781
|
+
const sep3 = edge.target.lastIndexOf("::");
|
|
3782
|
+
if (sep3 > 0 && realIds.has(edge.target)) {
|
|
3783
|
+
namedAlias.set(`${edge.source}|${edge.target.slice(sep3 + 2)}`, edge.target);
|
|
3784
|
+
} else if (sep3 < 0 && realFiles.has(edge.target)) {
|
|
3785
|
+
const targets = starTargets.get(edge.source) ?? [];
|
|
3786
|
+
targets.push(edge.target);
|
|
3787
|
+
starTargets.set(edge.source, targets);
|
|
3735
3788
|
}
|
|
3736
3789
|
}
|
|
3737
3790
|
}
|
|
3791
|
+
const throughBarrel = (file, name) => {
|
|
3792
|
+
const direct = `${file}::${name}`;
|
|
3793
|
+
if (realIds.has(direct)) return direct;
|
|
3794
|
+
const aliased = namedAlias.get(`${file}|${name}`);
|
|
3795
|
+
if (aliased !== void 0) return aliased;
|
|
3796
|
+
const viaStar = (starTargets.get(file) ?? []).map((sf) => `${sf}::${name}`).filter((id) => realIds.has(id));
|
|
3797
|
+
return viaStar.length === 1 ? viaStar[0] : null;
|
|
3798
|
+
};
|
|
3799
|
+
const resolveBarrelId = (id) => {
|
|
3800
|
+
const cached = remap.get(id);
|
|
3801
|
+
if (cached !== void 0) return cached;
|
|
3802
|
+
if (id.startsWith("module:")) {
|
|
3803
|
+
const spec = id.slice("module:".length);
|
|
3804
|
+
const sep4 = spec.indexOf("::");
|
|
3805
|
+
const moduleSpec = sep4 > 0 ? spec.slice(0, sep4) : spec;
|
|
3806
|
+
const name = sep4 > 0 ? spec.slice(sep4 + 2) : null;
|
|
3807
|
+
const candidates = selfImportCandidates(moduleSpec, selfNames);
|
|
3808
|
+
if (candidates === null) {
|
|
3809
|
+
if (name === null) return null;
|
|
3810
|
+
remap.set(id, `module:${moduleSpec}`);
|
|
3811
|
+
return `module:${moduleSpec}`;
|
|
3812
|
+
}
|
|
3813
|
+
const file = uniqueMatch(candidates, realFiles);
|
|
3814
|
+
let resolved = null;
|
|
3815
|
+
if (file !== null) {
|
|
3816
|
+
resolved = name !== null ? throughBarrel(file, name) ?? file : file;
|
|
3817
|
+
} else if (name !== null) {
|
|
3818
|
+
resolved = `module:${moduleSpec}`;
|
|
3819
|
+
}
|
|
3820
|
+
if (resolved !== null) remap.set(id, resolved);
|
|
3821
|
+
return resolved;
|
|
3822
|
+
}
|
|
3823
|
+
const sep3 = id.indexOf("::");
|
|
3824
|
+
if (sep3 > 0 && !realIds.has(id)) {
|
|
3825
|
+
const guessedPath = id.slice(0, sep3);
|
|
3826
|
+
const name = id.slice(sep3 + 2);
|
|
3827
|
+
const files = [guessedPath, ...candidatePaths(guessedPath)].filter((f) => realFiles.has(f));
|
|
3828
|
+
const resolvedSet = new Set(files.map((f) => throughBarrel(f, name)).filter((r) => r !== null));
|
|
3829
|
+
if (resolvedSet.size === 1) {
|
|
3830
|
+
const resolved = [...resolvedSet][0];
|
|
3831
|
+
remap.set(id, resolved);
|
|
3832
|
+
return resolved;
|
|
3833
|
+
}
|
|
3834
|
+
}
|
|
3835
|
+
return null;
|
|
3836
|
+
};
|
|
3837
|
+
applyRemap(resolveBarrelId);
|
|
3738
3838
|
let droppedPlaceholders = 0;
|
|
3739
3839
|
for (const extraction of extractions) {
|
|
3740
3840
|
const before = extraction.nodes.length;
|
|
@@ -4261,7 +4361,8 @@ async function runTests(nodeQuery, options = {}) {
|
|
|
4261
4361
|
);
|
|
4262
4362
|
return;
|
|
4263
4363
|
}
|
|
4264
|
-
|
|
4364
|
+
const how = selection.precision === "usage" ? "selected by actual usage (calls/inheritance)" : "selected by import reachability \u2014 may over-include files sharing an entry point";
|
|
4365
|
+
console.log(`Test files for ${selection.target} (${how}; most direct first):`);
|
|
4265
4366
|
for (const test of selection.testFiles) {
|
|
4266
4367
|
const via = test.depth === 0 ? test.via : `depth ${test.depth}, via ${test.via}`;
|
|
4267
4368
|
console.log(` ${test.file} (${via})`);
|