ragcode-context-engine 0.1.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/LICENSE +21 -0
- package/README.md +366 -0
- package/README.zh-CN.md +363 -0
- package/dist/src/cli/configure/app.d.ts +6 -0
- package/dist/src/cli/configure/app.js +81 -0
- package/dist/src/cli/configure/run.d.ts +5 -0
- package/dist/src/cli/configure/run.js +85 -0
- package/dist/src/cli/configure/state.d.ts +42 -0
- package/dist/src/cli/configure/state.js +174 -0
- package/dist/src/cli/configure.d.ts +31 -0
- package/dist/src/cli/configure.js +101 -0
- package/dist/src/cli/index.d.ts +2 -0
- package/dist/src/cli/index.js +503 -0
- package/dist/src/cli/tui/index-progress.d.ts +12 -0
- package/dist/src/cli/tui/index-progress.js +49 -0
- package/dist/src/cli/tui/watch-status.d.ts +10 -0
- package/dist/src/cli/tui/watch-status.js +27 -0
- package/dist/src/cli/update.d.ts +18 -0
- package/dist/src/cli/update.js +111 -0
- package/dist/src/config/dotenv.d.ts +1 -0
- package/dist/src/config/dotenv.js +14 -0
- package/dist/src/config/graph-runtime.d.ts +13 -0
- package/dist/src/config/graph-runtime.js +29 -0
- package/dist/src/config/runtime-config.d.ts +87 -0
- package/dist/src/config/runtime-config.js +215 -0
- package/dist/src/config/semantic-runtime.d.ts +24 -0
- package/dist/src/config/semantic-runtime.js +89 -0
- package/dist/src/context/context-builder.d.ts +20 -0
- package/dist/src/context/context-builder.js +277 -0
- package/dist/src/context/expansion-policy.d.ts +6 -0
- package/dist/src/context/expansion-policy.js +49 -0
- package/dist/src/context/skeletonizer.d.ts +2 -0
- package/dist/src/context/skeletonizer.js +79 -0
- package/dist/src/context/snippet-renderer.d.ts +2 -0
- package/dist/src/context/snippet-renderer.js +67 -0
- package/dist/src/core/contracts.d.ts +74 -0
- package/dist/src/core/contracts.js +1 -0
- package/dist/src/core/engine.d.ts +64 -0
- package/dist/src/core/engine.js +442 -0
- package/dist/src/core/types.d.ts +490 -0
- package/dist/src/core/types.js +1 -0
- package/dist/src/diagnostics/doctor.d.ts +66 -0
- package/dist/src/diagnostics/doctor.js +193 -0
- package/dist/src/diagnostics/embedding-test.d.ts +24 -0
- package/dist/src/diagnostics/embedding-test.js +83 -0
- package/dist/src/graph/diff-files.d.ts +1 -0
- package/dist/src/graph/diff-files.js +14 -0
- package/dist/src/graph/impact-report.d.ts +10 -0
- package/dist/src/graph/impact-report.js +173 -0
- package/dist/src/graph/in-memory-graph-store.d.ts +36 -0
- package/dist/src/graph/in-memory-graph-store.js +395 -0
- package/dist/src/graph/owner-ranking.d.ts +2 -0
- package/dist/src/graph/owner-ranking.js +41 -0
- package/dist/src/graph/sqlite-graph-store.d.ts +51 -0
- package/dist/src/graph/sqlite-graph-store.js +724 -0
- package/dist/src/graph/sqlite-statements.d.ts +36 -0
- package/dist/src/graph/sqlite-statements.js +105 -0
- package/dist/src/graph/target-matcher.d.ts +13 -0
- package/dist/src/graph/target-matcher.js +64 -0
- package/dist/src/index.d.ts +32 -0
- package/dist/src/index.js +32 -0
- package/dist/src/indexing/analyzers/fallback-analyzer.d.ts +6 -0
- package/dist/src/indexing/analyzers/fallback-analyzer.js +45 -0
- package/dist/src/indexing/analyzers/go-treesitter-analyzer.d.ts +2 -0
- package/dist/src/indexing/analyzers/go-treesitter-analyzer.js +87 -0
- package/dist/src/indexing/analyzers/java-treesitter-analyzer.d.ts +2 -0
- package/dist/src/indexing/analyzers/java-treesitter-analyzer.js +88 -0
- package/dist/src/indexing/analyzers/python-treesitter-analyzer.d.ts +2 -0
- package/dist/src/indexing/analyzers/python-treesitter-analyzer.js +96 -0
- package/dist/src/indexing/analyzers/registry.d.ts +5 -0
- package/dist/src/indexing/analyzers/registry.js +23 -0
- package/dist/src/indexing/analyzers/rust-treesitter-analyzer.d.ts +2 -0
- package/dist/src/indexing/analyzers/rust-treesitter-analyzer.js +96 -0
- package/dist/src/indexing/analyzers/tree-sitter-base.d.ts +30 -0
- package/dist/src/indexing/analyzers/tree-sitter-base.js +163 -0
- package/dist/src/indexing/analyzers/types.d.ts +17 -0
- package/dist/src/indexing/analyzers/types.js +1 -0
- package/dist/src/indexing/analyzers/typescript-analyzer.d.ts +5 -0
- package/dist/src/indexing/analyzers/typescript-analyzer.js +199 -0
- package/dist/src/indexing/ast-analyzer.d.ts +11 -0
- package/dist/src/indexing/ast-analyzer.js +11 -0
- package/dist/src/indexing/chunker.d.ts +11 -0
- package/dist/src/indexing/chunker.js +157 -0
- package/dist/src/indexing/ignore-policy.d.ts +6 -0
- package/dist/src/indexing/ignore-policy.js +40 -0
- package/dist/src/indexing/indexer.d.ts +13 -0
- package/dist/src/indexing/indexer.js +189 -0
- package/dist/src/indexing/language.d.ts +3 -0
- package/dist/src/indexing/language.js +24 -0
- package/dist/src/indexing/scanner.d.ts +13 -0
- package/dist/src/indexing/scanner.js +87 -0
- package/dist/src/lsp/definition-resolver.d.ts +6 -0
- package/dist/src/lsp/definition-resolver.js +60 -0
- package/dist/src/lsp/typescript-language-service.d.ts +21 -0
- package/dist/src/lsp/typescript-language-service.js +82 -0
- package/dist/src/mcp/server.d.ts +11 -0
- package/dist/src/mcp/server.js +64 -0
- package/dist/src/mcp/tools.d.ts +266 -0
- package/dist/src/mcp/tools.js +309 -0
- package/dist/src/project/project-identity.d.ts +2 -0
- package/dist/src/project/project-identity.js +24 -0
- package/dist/src/project/project-registry.d.ts +12 -0
- package/dist/src/project/project-registry.js +49 -0
- package/dist/src/project/workspace-resolver.d.ts +20 -0
- package/dist/src/project/workspace-resolver.js +62 -0
- package/dist/src/retrieval/graph-reranker.d.ts +11 -0
- package/dist/src/retrieval/graph-reranker.js +0 -0
- package/dist/src/retrieval/hybrid-retriever.d.ts +31 -0
- package/dist/src/retrieval/hybrid-retriever.js +111 -0
- package/dist/src/retrieval/path-classification.d.ts +6 -0
- package/dist/src/retrieval/path-classification.js +22 -0
- package/dist/src/retrieval/query-matching.d.ts +22 -0
- package/dist/src/retrieval/query-matching.js +166 -0
- package/dist/src/retrieval/query-planner.d.ts +5 -0
- package/dist/src/retrieval/query-planner.js +77 -0
- package/dist/src/retrieval/ranking-signals.d.ts +19 -0
- package/dist/src/retrieval/ranking-signals.js +97 -0
- package/dist/src/retrieval/topology-distance.d.ts +21 -0
- package/dist/src/retrieval/topology-distance.js +116 -0
- package/dist/src/reuse/reuse-detector.d.ts +12 -0
- package/dist/src/reuse/reuse-detector.js +564 -0
- package/dist/src/semantic/deterministic-embedding.d.ts +7 -0
- package/dist/src/semantic/deterministic-embedding.js +31 -0
- package/dist/src/semantic/in-memory-semantic-store.d.ts +11 -0
- package/dist/src/semantic/in-memory-semantic-store.js +65 -0
- package/dist/src/semantic/lance-semantic-store.d.ts +131 -0
- package/dist/src/semantic/lance-semantic-store.js +623 -0
- package/dist/src/semantic/openai-compatible-embedding.d.ts +19 -0
- package/dist/src/semantic/openai-compatible-embedding.js +75 -0
- package/dist/src/service/service-identity.d.ts +13 -0
- package/dist/src/service/service-identity.js +48 -0
- package/dist/src/service/service-manager.d.ts +29 -0
- package/dist/src/service/service-manager.js +231 -0
- package/dist/src/service/service-templates.d.ts +22 -0
- package/dist/src/service/service-templates.js +101 -0
- package/dist/src/subgraph/impact-explainer.d.ts +2 -0
- package/dist/src/subgraph/impact-explainer.js +54 -0
- package/dist/src/subgraph/node-expander.d.ts +13 -0
- package/dist/src/subgraph/node-expander.js +139 -0
- package/dist/src/subgraph/output-preset.d.ts +3 -0
- package/dist/src/subgraph/output-preset.js +102 -0
- package/dist/src/subgraph/subgraph-builder.d.ts +17 -0
- package/dist/src/subgraph/subgraph-builder.js +688 -0
- package/dist/src/topology/export-index.d.ts +7 -0
- package/dist/src/topology/export-index.js +14 -0
- package/dist/src/topology/framework-topology.d.ts +3 -0
- package/dist/src/topology/framework-topology.js +460 -0
- package/dist/src/topology/import-resolver.d.ts +2 -0
- package/dist/src/topology/import-resolver.js +29 -0
- package/dist/src/topology/orm-topology.d.ts +3 -0
- package/dist/src/topology/orm-topology.js +200 -0
- package/dist/src/topology/runtime-topology.d.ts +3 -0
- package/dist/src/topology/runtime-topology.js +204 -0
- package/dist/src/topology/symbol-resolver.d.ts +6 -0
- package/dist/src/topology/symbol-resolver.js +74 -0
- package/dist/src/topology/test-topology.d.ts +2 -0
- package/dist/src/topology/test-topology.js +82 -0
- package/dist/src/utils/hash.d.ts +2 -0
- package/dist/src/utils/hash.js +7 -0
- package/dist/src/utils/path.d.ts +2 -0
- package/dist/src/utils/path.js +7 -0
- package/dist/src/watch/event-journal.d.ts +17 -0
- package/dist/src/watch/event-journal.js +81 -0
- package/dist/src/watch/file-event-coalescer.d.ts +9 -0
- package/dist/src/watch/file-event-coalescer.js +39 -0
- package/dist/src/watch/index-scheduler.d.ts +52 -0
- package/dist/src/watch/index-scheduler.js +190 -0
- package/dist/src/watch/watch-daemon.d.ts +73 -0
- package/dist/src/watch/watch-daemon.js +368 -0
- package/dist/src/watch/watcher-liveness.d.ts +47 -0
- package/dist/src/watch/watcher-liveness.js +168 -0
- package/dist/src/web/server.d.ts +1 -0
- package/dist/src/web/server.js +375 -0
- package/package.json +94 -0
|
@@ -0,0 +1,688 @@
|
|
|
1
|
+
import { renderSnippet } from "../context/snippet-renderer.js";
|
|
2
|
+
const DEFAULT_BUDGET_CHARS = 10_000;
|
|
3
|
+
const DEFAULT_MAX_HOPS = 4;
|
|
4
|
+
const MAX_NODES = 32;
|
|
5
|
+
const MAX_EDGES = 48;
|
|
6
|
+
export class SubgraphBuilder {
|
|
7
|
+
build(input) {
|
|
8
|
+
const budgetChars = input.budgetChars ?? DEFAULT_BUDGET_CHARS;
|
|
9
|
+
const maxHops = input.maxHops ?? DEFAULT_MAX_HOPS;
|
|
10
|
+
const symbolsById = new Map(input.symbols.map((symbol) => [symbol.id, symbol]));
|
|
11
|
+
const nodes = new Map();
|
|
12
|
+
const selectedEdges = new Map();
|
|
13
|
+
const pathsByNode = new Map();
|
|
14
|
+
const pathCostByNode = new Map();
|
|
15
|
+
const missingEvidence = [...(input.missingEvidence ?? [])];
|
|
16
|
+
let truncated = false;
|
|
17
|
+
const seedSymbols = uniqueSymbols(input.seedSymbols);
|
|
18
|
+
for (const seed of seedSymbols) {
|
|
19
|
+
const node = nodeFromSymbol(seed, "target", "Matched primary seed symbol.", "high");
|
|
20
|
+
nodes.set(node.id, node);
|
|
21
|
+
pathsByNode.set(node.id, [node.id]);
|
|
22
|
+
pathCostByNode.set(node.id, 0);
|
|
23
|
+
}
|
|
24
|
+
const includeIncoming = input.mode !== "flow";
|
|
25
|
+
const includeOutgoing = true;
|
|
26
|
+
const adjacency = buildAdjacency(input.edges, includeIncoming, includeOutgoing);
|
|
27
|
+
const queue = seedSymbols.map((symbol) => ({
|
|
28
|
+
nodeId: symbol.id,
|
|
29
|
+
path: [symbol.id],
|
|
30
|
+
cost: 0,
|
|
31
|
+
hops: 0
|
|
32
|
+
}));
|
|
33
|
+
while (queue.length > 0) {
|
|
34
|
+
const state = popLowestCost(queue);
|
|
35
|
+
if (!state || state.hops >= maxHops)
|
|
36
|
+
continue;
|
|
37
|
+
if (state.cost > (pathCostByNode.get(state.nodeId) ?? Number.POSITIVE_INFINITY))
|
|
38
|
+
continue;
|
|
39
|
+
const candidates = adjacency.get(state.nodeId) ?? [];
|
|
40
|
+
for (const candidate of candidates) {
|
|
41
|
+
if (shouldSkipUnresolvedCall(candidate.edge, symbolsById))
|
|
42
|
+
continue;
|
|
43
|
+
const nextNodeId = candidate.direction === "outgoing" ? candidate.edge.targetId : candidate.edge.sourceId;
|
|
44
|
+
const hasSource = nodes.has(candidate.edge.sourceId);
|
|
45
|
+
const hasTarget = nodes.has(candidate.edge.targetId);
|
|
46
|
+
if (((!hasSource || !hasTarget) && nodes.size >= MAX_NODES) || selectedEdges.size >= MAX_EDGES) {
|
|
47
|
+
truncated = true;
|
|
48
|
+
break;
|
|
49
|
+
}
|
|
50
|
+
const edge = verifiedEdge(candidate.edge, symbolsById);
|
|
51
|
+
const edgeKey = edgeKeyFor(edge);
|
|
52
|
+
const sourceRole = nodes.get(candidate.edge.sourceId)?.role ?? roleForSource(candidate.edge, candidate.direction);
|
|
53
|
+
const targetRole = nodes.get(candidate.edge.targetId)?.role ?? roleForTarget(candidate.edge, candidate.direction);
|
|
54
|
+
const source = nodeForEndpoint(candidate.edge, "source", symbolsById, sourceRole);
|
|
55
|
+
const target = nodeForEndpoint(candidate.edge, "target", symbolsById, targetRole);
|
|
56
|
+
mergeNode(nodes, source);
|
|
57
|
+
mergeNode(nodes, target);
|
|
58
|
+
if (!selectedEdges.has(edgeKey))
|
|
59
|
+
selectedEdges.set(edgeKey, edge);
|
|
60
|
+
const basePath = pathsByNode.get(state.nodeId) ?? state.path;
|
|
61
|
+
const nextPath = candidate.direction === "outgoing"
|
|
62
|
+
? [...basePath, nextNodeId]
|
|
63
|
+
: [nextNodeId, ...basePath];
|
|
64
|
+
const nextCost = state.cost + edgeTraversalCost(candidate.edge, edge);
|
|
65
|
+
const existingCost = pathCostByNode.get(nextNodeId) ?? Number.POSITIVE_INFINITY;
|
|
66
|
+
if (nextCost < existingCost || (nextCost === existingCost && nextPath.length < (pathsByNode.get(nextNodeId)?.length ?? Number.POSITIVE_INFINITY))) {
|
|
67
|
+
pathCostByNode.set(nextNodeId, nextCost);
|
|
68
|
+
pathsByNode.set(nextNodeId, nextPath);
|
|
69
|
+
queue.push({ nodeId: nextNodeId, path: nextPath, cost: nextCost, hops: state.hops + 1 });
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
if (truncated)
|
|
73
|
+
break;
|
|
74
|
+
}
|
|
75
|
+
if (seedSymbols.length === 0) {
|
|
76
|
+
missingEvidence.push("No indexed primary owner matched the subgraph seed.");
|
|
77
|
+
}
|
|
78
|
+
const orderedNodes = orderNodes([...nodes.values()], pathsByNode);
|
|
79
|
+
const orderedEdges = orderEdges([...selectedEdges.values()], pathsByNode);
|
|
80
|
+
const paths = materializedPaths(orderedNodes, pathsByNode);
|
|
81
|
+
const usedBeforeSnippets = estimateGraphCost(orderedNodes, orderedEdges, paths, missingEvidence);
|
|
82
|
+
const snippetResult = buildSnippets({
|
|
83
|
+
query: input.query,
|
|
84
|
+
mode: input.mode,
|
|
85
|
+
nodes: orderedNodes,
|
|
86
|
+
chunks: input.chunks,
|
|
87
|
+
budgetChars,
|
|
88
|
+
usedChars: usedBeforeSnippets
|
|
89
|
+
});
|
|
90
|
+
truncated = truncated || snippetResult.truncated;
|
|
91
|
+
const coverage = coverageSignals({
|
|
92
|
+
mode: input.mode,
|
|
93
|
+
seedCount: seedSymbols.length,
|
|
94
|
+
edges: orderedEdges,
|
|
95
|
+
truncated
|
|
96
|
+
});
|
|
97
|
+
missingEvidence.push(...missingFromCoverage(coverage, input.query));
|
|
98
|
+
const answerable = orderedNodes.length > 0;
|
|
99
|
+
const confidence = confidenceFor(answerable, orderedEdges, coverage);
|
|
100
|
+
const coverageSummary = summarizeCoverage(coverage, answerable, orderedEdges);
|
|
101
|
+
const whyTheseFiles = summarizeWhyTheseFiles(orderedNodes, orderedEdges);
|
|
102
|
+
return {
|
|
103
|
+
query: input.query,
|
|
104
|
+
repoRoot: input.repoRoot,
|
|
105
|
+
projectId: input.projectId,
|
|
106
|
+
mode: input.mode,
|
|
107
|
+
answerable,
|
|
108
|
+
confidence,
|
|
109
|
+
coverageSummary,
|
|
110
|
+
whyTheseFiles,
|
|
111
|
+
nodes: orderedNodes,
|
|
112
|
+
edges: orderedEdges,
|
|
113
|
+
paths,
|
|
114
|
+
snippets: snippetResult.snippets,
|
|
115
|
+
coverage,
|
|
116
|
+
missingEvidence: [...new Set(missingEvidence)],
|
|
117
|
+
nextQueries: nextQueries(input.query, orderedNodes, coverage),
|
|
118
|
+
budgetChars,
|
|
119
|
+
usedChars: Math.min(budgetChars, usedBeforeSnippets + snippetResult.usedChars)
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
function buildAdjacency(edges, includeIncoming, includeOutgoing) {
|
|
124
|
+
const adjacency = new Map();
|
|
125
|
+
for (const edge of edges) {
|
|
126
|
+
if (!isSubgraphEdge(edge.kind))
|
|
127
|
+
continue;
|
|
128
|
+
if (includeOutgoing)
|
|
129
|
+
addAdjacent(adjacency, edge.sourceId, { edge, direction: "outgoing" });
|
|
130
|
+
if (includeIncoming)
|
|
131
|
+
addAdjacent(adjacency, edge.targetId, { edge, direction: "incoming" });
|
|
132
|
+
}
|
|
133
|
+
for (const candidates of adjacency.values()) {
|
|
134
|
+
candidates.sort((a, b) => edgeTraversalCost(a.edge) - edgeTraversalCost(b.edge) || edgeLabel(a.edge).localeCompare(edgeLabel(b.edge)));
|
|
135
|
+
}
|
|
136
|
+
return adjacency;
|
|
137
|
+
}
|
|
138
|
+
function addAdjacent(adjacency, nodeId, candidate) {
|
|
139
|
+
const candidates = adjacency.get(nodeId) ?? [];
|
|
140
|
+
candidates.push(candidate);
|
|
141
|
+
adjacency.set(nodeId, candidates);
|
|
142
|
+
}
|
|
143
|
+
function popLowestCost(queue) {
|
|
144
|
+
if (queue.length === 0)
|
|
145
|
+
return undefined;
|
|
146
|
+
let bestIndex = 0;
|
|
147
|
+
for (let index = 1; index < queue.length; index += 1) {
|
|
148
|
+
const best = queue[bestIndex];
|
|
149
|
+
const next = queue[index];
|
|
150
|
+
if (next.cost < best.cost || (next.cost === best.cost && next.hops < best.hops))
|
|
151
|
+
bestIndex = index;
|
|
152
|
+
}
|
|
153
|
+
return queue.splice(bestIndex, 1)[0];
|
|
154
|
+
}
|
|
155
|
+
function isSubgraphEdge(kind) {
|
|
156
|
+
return kind === "calls"
|
|
157
|
+
|| kind === "calls_api"
|
|
158
|
+
|| kind === "routes_to"
|
|
159
|
+
|| kind === "tested_by"
|
|
160
|
+
|| kind === "uses_middleware"
|
|
161
|
+
|| kind === "handles_webhook"
|
|
162
|
+
|| kind === "handles_event"
|
|
163
|
+
|| kind === "reads_from"
|
|
164
|
+
|| kind === "writes_to"
|
|
165
|
+
|| kind === "references"
|
|
166
|
+
|| kind === "imports"
|
|
167
|
+
|| kind === "exports"
|
|
168
|
+
|| kind === "contains";
|
|
169
|
+
}
|
|
170
|
+
function edgePriority(edge) {
|
|
171
|
+
if (edge.kind === "calls_api")
|
|
172
|
+
return 100;
|
|
173
|
+
if (edge.kind === "routes_to")
|
|
174
|
+
return 95;
|
|
175
|
+
if (edge.kind === "tested_by")
|
|
176
|
+
return 90;
|
|
177
|
+
if (edge.kind === "handles_webhook")
|
|
178
|
+
return 88;
|
|
179
|
+
if (edge.kind === "uses_middleware")
|
|
180
|
+
return 84;
|
|
181
|
+
if (edge.kind === "writes_to")
|
|
182
|
+
return 78;
|
|
183
|
+
if (edge.kind === "reads_from")
|
|
184
|
+
return 76;
|
|
185
|
+
if (edge.kind === "handles_event")
|
|
186
|
+
return 74;
|
|
187
|
+
if (edge.metadata?.resolution === "resolved_lsp")
|
|
188
|
+
return 72;
|
|
189
|
+
if (edge.metadata?.resolution === "resolved")
|
|
190
|
+
return 70;
|
|
191
|
+
if (edge.kind === "calls")
|
|
192
|
+
return 60;
|
|
193
|
+
if (edge.kind === "contains")
|
|
194
|
+
return 55;
|
|
195
|
+
return 20;
|
|
196
|
+
}
|
|
197
|
+
function edgeTraversalCost(edge, verified) {
|
|
198
|
+
const confidence = verified?.confidence ?? confidenceForEdge(edge, edgeSource(edge), false);
|
|
199
|
+
const confidenceDiscount = confidence === "high" ? 25 : confidence === "medium" ? 12 : 0;
|
|
200
|
+
return Math.max(1, 120 - edgePriority(edge) - confidenceDiscount);
|
|
201
|
+
}
|
|
202
|
+
function verifiedEdge(edge, symbolsById) {
|
|
203
|
+
const source = symbolsById.get(edge.sourceId);
|
|
204
|
+
const target = symbolsById.get(edge.targetId);
|
|
205
|
+
const sourceFile = source?.filePath ?? stringMetadata(edge, "sourceFile");
|
|
206
|
+
const targetFile = target?.filePath ?? stringMetadata(edge, "targetFile");
|
|
207
|
+
const sourceKind = edgeSource(edge);
|
|
208
|
+
return {
|
|
209
|
+
fromNodeId: edge.sourceId,
|
|
210
|
+
toNodeId: edge.targetId,
|
|
211
|
+
kind: edge.kind,
|
|
212
|
+
confidence: confidenceForEdge(edge, sourceKind, Boolean(target)),
|
|
213
|
+
source: sourceKind,
|
|
214
|
+
reason: reasonForEdge(edge, sourceKind),
|
|
215
|
+
sourceFile,
|
|
216
|
+
targetFile,
|
|
217
|
+
line: numberMetadata(edge, "line"),
|
|
218
|
+
targetName: target?.name ?? stringMetadata(edge, "targetName"),
|
|
219
|
+
metadata: subgraphEdgeMetadata(edge)
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
function nodeForEndpoint(edge, endpoint, symbolsById, role) {
|
|
223
|
+
const id = endpoint === "source" ? edge.sourceId : edge.targetId;
|
|
224
|
+
const symbol = symbolsById.get(id);
|
|
225
|
+
if (symbol)
|
|
226
|
+
return nodeFromSymbol(symbol, role, reasonForNodeRole(role, endpoint), confidenceForEdge(edge, edgeSource(edge), true));
|
|
227
|
+
const source = edgeSource(edge);
|
|
228
|
+
const sourceFile = stringMetadata(edge, "sourceFile");
|
|
229
|
+
const targetFile = stringMetadata(edge, "targetFile");
|
|
230
|
+
const targetName = stringMetadata(edge, "targetName");
|
|
231
|
+
const filePath = endpoint === "source"
|
|
232
|
+
? sourceFile ?? targetFile ?? "external"
|
|
233
|
+
: targetFile ?? sourceFile ?? "external";
|
|
234
|
+
return {
|
|
235
|
+
id,
|
|
236
|
+
filePath,
|
|
237
|
+
symbolName: endpoint === "target" ? targetName : undefined,
|
|
238
|
+
kind: "external",
|
|
239
|
+
role,
|
|
240
|
+
confidence: confidenceForEdge(edge, source, false),
|
|
241
|
+
reason: reasonForNodeRole(role, endpoint),
|
|
242
|
+
citation: {
|
|
243
|
+
filePath,
|
|
244
|
+
line: numberMetadata(edge, "line"),
|
|
245
|
+
symbol: endpoint === "target" ? targetName : undefined,
|
|
246
|
+
source
|
|
247
|
+
}
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
function nodeFromSymbol(symbol, role, reason, confidence) {
|
|
251
|
+
return {
|
|
252
|
+
id: symbol.id,
|
|
253
|
+
filePath: symbol.filePath,
|
|
254
|
+
symbolName: symbol.kind === "file" ? undefined : symbol.name,
|
|
255
|
+
kind: symbol.kind,
|
|
256
|
+
role,
|
|
257
|
+
startLine: symbol.startLine,
|
|
258
|
+
endLine: symbol.endLine,
|
|
259
|
+
exported: symbol.exported,
|
|
260
|
+
confidence,
|
|
261
|
+
reason,
|
|
262
|
+
citation: {
|
|
263
|
+
filePath: symbol.filePath,
|
|
264
|
+
line: symbol.startLine,
|
|
265
|
+
symbol: symbol.name,
|
|
266
|
+
source: "ast"
|
|
267
|
+
}
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
function mergeNode(nodes, next) {
|
|
271
|
+
const existing = nodes.get(next.id);
|
|
272
|
+
if (!existing) {
|
|
273
|
+
nodes.set(next.id, next);
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
if (rolePriority(next.role) < rolePriority(existing.role))
|
|
277
|
+
existing.role = next.role;
|
|
278
|
+
if (confidencePriority(next.confidence) > confidencePriority(existing.confidence))
|
|
279
|
+
existing.confidence = next.confidence;
|
|
280
|
+
if (!existing.reason.includes(next.reason))
|
|
281
|
+
existing.reason = `${existing.reason} ${next.reason}`;
|
|
282
|
+
}
|
|
283
|
+
function roleForSource(edge, direction) {
|
|
284
|
+
if (direction === "incoming")
|
|
285
|
+
return "caller";
|
|
286
|
+
if (edge.kind === "tested_by")
|
|
287
|
+
return "target";
|
|
288
|
+
return "target";
|
|
289
|
+
}
|
|
290
|
+
function roleForTarget(edge, direction) {
|
|
291
|
+
if (direction === "incoming")
|
|
292
|
+
return "target";
|
|
293
|
+
if (edge.kind === "contains")
|
|
294
|
+
return "callee";
|
|
295
|
+
if (edge.kind === "tested_by")
|
|
296
|
+
return "test";
|
|
297
|
+
if (edge.kind === "calls_api" || edge.kind === "routes_to" || edge.kind === "handles_webhook")
|
|
298
|
+
return "route";
|
|
299
|
+
if (edge.kind === "uses_middleware")
|
|
300
|
+
return "middleware";
|
|
301
|
+
if (edge.kind === "reads_from" || edge.kind === "writes_to")
|
|
302
|
+
return "resource";
|
|
303
|
+
if (edge.kind === "handles_event")
|
|
304
|
+
return "event";
|
|
305
|
+
return "callee";
|
|
306
|
+
}
|
|
307
|
+
function shouldSkipUnresolvedCall(edge, symbolsById) {
|
|
308
|
+
if (edge.kind !== "calls")
|
|
309
|
+
return false;
|
|
310
|
+
if (symbolsById.has(edge.targetId))
|
|
311
|
+
return false;
|
|
312
|
+
if (edge.metadata?.resolution === "resolved" || edge.metadata?.resolution === "resolved_lsp")
|
|
313
|
+
return false;
|
|
314
|
+
return typeof edge.metadata?.targetFile !== "string";
|
|
315
|
+
}
|
|
316
|
+
function reasonForNodeRole(role, endpoint) {
|
|
317
|
+
if (role === "target")
|
|
318
|
+
return "Primary target or target-side graph endpoint.";
|
|
319
|
+
if (role === "caller")
|
|
320
|
+
return "Incoming caller discovered through graph traversal.";
|
|
321
|
+
if (role === "test")
|
|
322
|
+
return "Related test discovered through tested_by edge.";
|
|
323
|
+
if (role === "route")
|
|
324
|
+
return "Route/API endpoint discovered through framework topology.";
|
|
325
|
+
if (role === "middleware")
|
|
326
|
+
return "Middleware discovered through framework topology.";
|
|
327
|
+
if (role === "resource")
|
|
328
|
+
return "Data resource discovered through static resource topology.";
|
|
329
|
+
if (role === "event")
|
|
330
|
+
return "Event owner discovered through static event topology.";
|
|
331
|
+
return endpoint === "target" ? "Outgoing dependency discovered through graph traversal." : "Graph source endpoint.";
|
|
332
|
+
}
|
|
333
|
+
function edgeSource(edge) {
|
|
334
|
+
const resolution = stringMetadata(edge, "resolution");
|
|
335
|
+
if (resolution === "resolved_lsp")
|
|
336
|
+
return "lsp";
|
|
337
|
+
if (resolution === "resolved")
|
|
338
|
+
return "ast";
|
|
339
|
+
if (resolution === "test_import")
|
|
340
|
+
return "test_import";
|
|
341
|
+
if (resolution === "resource_static")
|
|
342
|
+
return "resource_rule";
|
|
343
|
+
if (resolution === "event_static")
|
|
344
|
+
return "event_rule";
|
|
345
|
+
if (resolution === "framework_static" || resolution === "framework_call_graph" || typeof edge.metadata?.framework === "string")
|
|
346
|
+
return "framework_rule";
|
|
347
|
+
if (edge.kind === "imports" || edge.kind === "exports" || edge.kind === "contains")
|
|
348
|
+
return "ast";
|
|
349
|
+
return "heuristic";
|
|
350
|
+
}
|
|
351
|
+
function confidenceForEdge(edge, source, resolvedTarget) {
|
|
352
|
+
if (source === "lsp" || source === "test_import" || source === "framework_rule")
|
|
353
|
+
return "high";
|
|
354
|
+
if (source === "ast" && (resolvedTarget || edge.metadata?.resolution === "resolved"))
|
|
355
|
+
return "high";
|
|
356
|
+
if ((source === "resource_rule" || source === "event_rule") && numberMetadata(edge, "line") !== undefined)
|
|
357
|
+
return "medium";
|
|
358
|
+
if (stringMetadata(edge, "targetName") || stringMetadata(edge, "targetFile"))
|
|
359
|
+
return "medium";
|
|
360
|
+
return "low";
|
|
361
|
+
}
|
|
362
|
+
function reasonForEdge(edge, source) {
|
|
363
|
+
if (source === "lsp")
|
|
364
|
+
return "Resolved TypeScript Language Service edge with line evidence where available.";
|
|
365
|
+
if (source === "test_import")
|
|
366
|
+
return "Test coverage edge derived from a resolved test import.";
|
|
367
|
+
if (source === "framework_rule")
|
|
368
|
+
return "Framework topology rule produced this edge.";
|
|
369
|
+
if (source === "resource_rule")
|
|
370
|
+
return "Static resource access rule produced this edge.";
|
|
371
|
+
if (source === "event_rule")
|
|
372
|
+
return "Static event subscription rule produced this edge.";
|
|
373
|
+
if (source === "ast")
|
|
374
|
+
return `AST ${edge.kind} edge from indexed structure.`;
|
|
375
|
+
return "Heuristic or unresolved graph edge; verify before editing.";
|
|
376
|
+
}
|
|
377
|
+
function buildSnippets(input) {
|
|
378
|
+
const snippets = [];
|
|
379
|
+
const usedChunkIds = new Set();
|
|
380
|
+
let usedChars = 0;
|
|
381
|
+
let totalUsed = input.usedChars;
|
|
382
|
+
let truncated = false;
|
|
383
|
+
const contextMode = input.mode === "flow" ? "feature" : input.mode === "debug" ? "debug" : "review";
|
|
384
|
+
const safeQuery = input.query.replace(/\b(full body|full source|entire file|完整|全部源码)\b/gi, "");
|
|
385
|
+
for (const node of input.nodes) {
|
|
386
|
+
const chunk = bestChunkForNode(node, input.chunks);
|
|
387
|
+
if (!chunk || usedChunkIds.has(chunk.id))
|
|
388
|
+
continue;
|
|
389
|
+
const snippet = renderSnippet({
|
|
390
|
+
chunk,
|
|
391
|
+
score: scoreForRole(node.role),
|
|
392
|
+
source: "graph",
|
|
393
|
+
reason: `${node.role} node selected by verified subgraph`
|
|
394
|
+
}, safeQuery, contextMode);
|
|
395
|
+
const cost = estimateSnippetCost(snippet);
|
|
396
|
+
if (totalUsed + cost > input.budgetChars) {
|
|
397
|
+
truncated = true;
|
|
398
|
+
continue;
|
|
399
|
+
}
|
|
400
|
+
snippets.push(snippet);
|
|
401
|
+
usedChunkIds.add(chunk.id);
|
|
402
|
+
usedChars += cost;
|
|
403
|
+
totalUsed += cost;
|
|
404
|
+
}
|
|
405
|
+
return { snippets, usedChars, truncated };
|
|
406
|
+
}
|
|
407
|
+
function bestChunkForNode(node, chunks) {
|
|
408
|
+
const sameFile = chunks.filter((chunk) => chunk.filePath === node.filePath);
|
|
409
|
+
if (sameFile.length === 0)
|
|
410
|
+
return undefined;
|
|
411
|
+
if (node.symbolName) {
|
|
412
|
+
const exact = sameFile.find((chunk) => chunk.symbolName === node.symbolName);
|
|
413
|
+
if (exact)
|
|
414
|
+
return exact;
|
|
415
|
+
}
|
|
416
|
+
if (node.startLine !== undefined) {
|
|
417
|
+
const containing = sameFile.find((chunk) => chunk.startLine <= node.startLine && chunk.endLine >= node.startLine);
|
|
418
|
+
if (containing)
|
|
419
|
+
return containing;
|
|
420
|
+
}
|
|
421
|
+
return sameFile[0];
|
|
422
|
+
}
|
|
423
|
+
function coverageSignals(input) {
|
|
424
|
+
const inbound = input.edges.filter((edge) => edge.kind === "calls" || edge.kind === "calls_api" || edge.kind === "routes_to");
|
|
425
|
+
const outbound = input.edges.filter((edge) => isFlowKind(edge.kind));
|
|
426
|
+
const tests = input.edges.filter((edge) => edge.kind === "tested_by");
|
|
427
|
+
const unresolved = input.edges.filter((edge) => edge.confidence === "low" || edge.source === "heuristic");
|
|
428
|
+
return [
|
|
429
|
+
{
|
|
430
|
+
name: "primary_owner_found",
|
|
431
|
+
status: input.seedCount > 0 ? "pass" : "fail",
|
|
432
|
+
detail: input.seedCount > 0 ? `${input.seedCount} primary seed symbol(s) selected.` : "No primary seed symbol was selected."
|
|
433
|
+
},
|
|
434
|
+
{
|
|
435
|
+
name: "inbound_callers_checked",
|
|
436
|
+
status: input.mode === "flow" || inbound.length > 0 ? "pass" : "partial",
|
|
437
|
+
detail: input.mode === "flow" ? "Flow mode does not require inbound caller expansion." : `${inbound.length} inbound/call-chain edge(s) included.`
|
|
438
|
+
},
|
|
439
|
+
{
|
|
440
|
+
name: "outbound_flow_checked",
|
|
441
|
+
status: outbound.length > 0 ? "pass" : "partial",
|
|
442
|
+
detail: outbound.length > 0 ? `${outbound.length} outbound flow edge(s) included.` : "No outbound flow edge was found in the selected graph."
|
|
443
|
+
},
|
|
444
|
+
{
|
|
445
|
+
name: "tests_checked",
|
|
446
|
+
status: tests.length > 0 ? "pass" : "partial",
|
|
447
|
+
detail: tests.length > 0 ? `${tests.length} tested_by edge(s) included.` : "No tested_by edge was found for selected nodes."
|
|
448
|
+
},
|
|
449
|
+
{
|
|
450
|
+
name: "unresolved_edges_present",
|
|
451
|
+
status: unresolved.length > 0 ? "fail" : "pass",
|
|
452
|
+
detail: unresolved.length > 0 ? `${unresolved.length} unresolved or heuristic edge(s) require verification.` : "No low-confidence graph edges were selected."
|
|
453
|
+
},
|
|
454
|
+
{
|
|
455
|
+
name: "budget_truncated",
|
|
456
|
+
status: input.truncated ? "fail" : "pass",
|
|
457
|
+
detail: input.truncated ? "The subgraph or snippets were truncated by budget." : "The selected subgraph fit within budget."
|
|
458
|
+
}
|
|
459
|
+
];
|
|
460
|
+
}
|
|
461
|
+
function missingFromCoverage(coverage, query) {
|
|
462
|
+
const missing = [];
|
|
463
|
+
for (const signal of coverage) {
|
|
464
|
+
if (signal.status === "fail")
|
|
465
|
+
missing.push(signal.detail);
|
|
466
|
+
if (signal.name === "tests_checked" && signal.status !== "pass")
|
|
467
|
+
missing.push(`No explicit related test evidence found for "${query}".`);
|
|
468
|
+
}
|
|
469
|
+
return missing;
|
|
470
|
+
}
|
|
471
|
+
function confidenceFor(answerable, edges, coverage) {
|
|
472
|
+
if (!answerable)
|
|
473
|
+
return "low";
|
|
474
|
+
if (coverage.some((signal) => signal.status === "fail"))
|
|
475
|
+
return "low";
|
|
476
|
+
if (edges.length >= 2 && coverage.every((signal) => signal.status === "pass" || signal.name === "inbound_callers_checked"))
|
|
477
|
+
return "high";
|
|
478
|
+
return "medium";
|
|
479
|
+
}
|
|
480
|
+
function nextQueries(query, nodes, coverage) {
|
|
481
|
+
const queries = new Set();
|
|
482
|
+
for (const node of nodes.slice(0, 5)) {
|
|
483
|
+
queries.add(`expand_node ${node.filePath}${node.symbolName ? `:${node.symbolName}` : ""}`);
|
|
484
|
+
}
|
|
485
|
+
if (coverage.some((signal) => signal.name === "tests_checked" && signal.status !== "pass")) {
|
|
486
|
+
queries.add(`related_tests ${query}`);
|
|
487
|
+
}
|
|
488
|
+
if (coverage.some((signal) => signal.name === "outbound_flow_checked" && signal.status !== "pass")) {
|
|
489
|
+
queries.add(`trace_request_flow ${query}`);
|
|
490
|
+
}
|
|
491
|
+
return [...queries].slice(0, 8);
|
|
492
|
+
}
|
|
493
|
+
function materializedPaths(nodes, pathsByNode) {
|
|
494
|
+
const byId = new Map(nodes.map((node) => [node.id, node]));
|
|
495
|
+
const paths = [...pathsByNode.values()]
|
|
496
|
+
.filter((path) => path.length > 1)
|
|
497
|
+
.sort((a, b) => b.length - a.length || pathLabel(a, byId).localeCompare(pathLabel(b, byId)))
|
|
498
|
+
.slice(0, 12);
|
|
499
|
+
return paths.map((path) => path.map((nodeId) => labelForNode(byId.get(nodeId), nodeId)));
|
|
500
|
+
}
|
|
501
|
+
function orderNodes(nodes, pathsByNode) {
|
|
502
|
+
return nodes.sort((a, b) => rolePriority(a.role) - rolePriority(b.role)
|
|
503
|
+
|| (pathsByNode.get(a.id)?.length ?? 99) - (pathsByNode.get(b.id)?.length ?? 99)
|
|
504
|
+
|| a.filePath.localeCompare(b.filePath)
|
|
505
|
+
|| (a.symbolName ?? "").localeCompare(b.symbolName ?? ""));
|
|
506
|
+
}
|
|
507
|
+
function orderEdges(edges, pathsByNode) {
|
|
508
|
+
return edges.sort((a, b) => (pathsByNode.get(a.toNodeId)?.length ?? 99) - (pathsByNode.get(b.toNodeId)?.length ?? 99)
|
|
509
|
+
|| edgeKindPriority(a.kind) - edgeKindPriority(b.kind)
|
|
510
|
+
|| edgeLabelFromVerified(a).localeCompare(edgeLabelFromVerified(b)));
|
|
511
|
+
}
|
|
512
|
+
function edgeKeyFor(edge) {
|
|
513
|
+
return [edge.kind, edge.fromNodeId, edge.toNodeId, edge.sourceFile, edge.targetFile, edge.targetName].join("::");
|
|
514
|
+
}
|
|
515
|
+
function edgeLabel(edge) {
|
|
516
|
+
return [edge.kind, stringMetadata(edge, "sourceFile"), stringMetadata(edge, "targetFile"), stringMetadata(edge, "targetName")].join("::");
|
|
517
|
+
}
|
|
518
|
+
function edgeLabelFromVerified(edge) {
|
|
519
|
+
return [edge.kind, edge.sourceFile, edge.targetFile, edge.targetName].join("::");
|
|
520
|
+
}
|
|
521
|
+
function pathLabel(path, nodesById) {
|
|
522
|
+
return path.map((nodeId) => labelForNode(nodesById.get(nodeId), nodeId)).join(" -> ");
|
|
523
|
+
}
|
|
524
|
+
function labelForNode(node, fallback) {
|
|
525
|
+
if (!node)
|
|
526
|
+
return fallback;
|
|
527
|
+
return `${node.filePath}${node.symbolName ? `:${node.symbolName}` : ""}`;
|
|
528
|
+
}
|
|
529
|
+
function estimateGraphCost(nodes, edges, paths, missingEvidence) {
|
|
530
|
+
return JSON.stringify({ nodes, edges, paths, missingEvidence }).length;
|
|
531
|
+
}
|
|
532
|
+
function estimateSnippetCost(snippet) {
|
|
533
|
+
return snippet.filePath.length + snippet.reason.length + snippet.content.length + 80;
|
|
534
|
+
}
|
|
535
|
+
function scoreForRole(role) {
|
|
536
|
+
if (role === "target")
|
|
537
|
+
return 3;
|
|
538
|
+
if (role === "test")
|
|
539
|
+
return 2.6;
|
|
540
|
+
if (role === "caller" || role === "route")
|
|
541
|
+
return 2.4;
|
|
542
|
+
return 2;
|
|
543
|
+
}
|
|
544
|
+
function rolePriority(role) {
|
|
545
|
+
if (role === "target")
|
|
546
|
+
return 0;
|
|
547
|
+
if (role === "caller")
|
|
548
|
+
return 1;
|
|
549
|
+
if (role === "route")
|
|
550
|
+
return 2;
|
|
551
|
+
if (role === "callee")
|
|
552
|
+
return 3;
|
|
553
|
+
if (role === "test")
|
|
554
|
+
return 4;
|
|
555
|
+
return 5;
|
|
556
|
+
}
|
|
557
|
+
function confidencePriority(confidence) {
|
|
558
|
+
if (confidence === "high")
|
|
559
|
+
return 3;
|
|
560
|
+
if (confidence === "medium")
|
|
561
|
+
return 2;
|
|
562
|
+
return 1;
|
|
563
|
+
}
|
|
564
|
+
function edgeKindPriority(kind) {
|
|
565
|
+
if (kind === "calls_api")
|
|
566
|
+
return 0;
|
|
567
|
+
if (kind === "routes_to")
|
|
568
|
+
return 1;
|
|
569
|
+
if (kind === "calls")
|
|
570
|
+
return 2;
|
|
571
|
+
if (kind === "tested_by")
|
|
572
|
+
return 3;
|
|
573
|
+
return 4;
|
|
574
|
+
}
|
|
575
|
+
function isFlowKind(kind) {
|
|
576
|
+
return kind === "calls"
|
|
577
|
+
|| kind === "calls_api"
|
|
578
|
+
|| kind === "routes_to"
|
|
579
|
+
|| kind === "handles_webhook"
|
|
580
|
+
|| kind === "handles_event"
|
|
581
|
+
|| kind === "uses_middleware"
|
|
582
|
+
|| kind === "reads_from"
|
|
583
|
+
|| kind === "writes_to";
|
|
584
|
+
}
|
|
585
|
+
function summarizeCoverage(coverage, answerable, edges) {
|
|
586
|
+
const passed = coverage.filter((signal) => signal.status === "pass").length;
|
|
587
|
+
const partial = coverage.filter((signal) => signal.status === "partial").length;
|
|
588
|
+
const failed = coverage.filter((signal) => signal.status === "fail").length;
|
|
589
|
+
const blockingFailed = coverage.filter((signal) => signal.status === "fail" && isBlockingCoverageFailure(signal, edges)).length;
|
|
590
|
+
const softFailed = failed - blockingFailed;
|
|
591
|
+
const verdict = editReadinessFor(answerable, blockingFailed, partial + softFailed, coverage.some((signal) => signal.name === "budget_truncated" && signal.status === "fail"));
|
|
592
|
+
const summary = summaryForVerdict(verdict, passed, partial, failed);
|
|
593
|
+
return { verdict, summary, passed, partial, failed };
|
|
594
|
+
}
|
|
595
|
+
function isBlockingCoverageFailure(signal, edges) {
|
|
596
|
+
if (signal.name === "budget_truncated")
|
|
597
|
+
return false;
|
|
598
|
+
if (signal.name === "unresolved_edges_present")
|
|
599
|
+
return edges.some((edge) => edge.confidence === "low" || edge.source === "heuristic");
|
|
600
|
+
return true;
|
|
601
|
+
}
|
|
602
|
+
function editReadinessFor(answerable, failed, partial, truncated) {
|
|
603
|
+
if (!answerable || failed > 0)
|
|
604
|
+
return "not_enough_context";
|
|
605
|
+
if (truncated)
|
|
606
|
+
return "investigate_only";
|
|
607
|
+
if (partial > 1)
|
|
608
|
+
return "investigate_only";
|
|
609
|
+
return "safe_to_edit_after_reading";
|
|
610
|
+
}
|
|
611
|
+
function summaryForVerdict(verdict, passed, partial, failed) {
|
|
612
|
+
if (verdict === "safe_to_edit_after_reading")
|
|
613
|
+
return `Edit-ready after reading selected snippets: ${passed} checks passed and no blocking evidence is missing.`;
|
|
614
|
+
if (verdict === "investigate_only")
|
|
615
|
+
return `Investigate before editing: ${partial} coverage check(s) are partial even though no blocking failure was found.`;
|
|
616
|
+
return `Not enough verified context to edit safely: ${failed} coverage check(s) failed and ${partial} are partial.`;
|
|
617
|
+
}
|
|
618
|
+
function summarizeWhyTheseFiles(nodes, edges) {
|
|
619
|
+
const byFile = new Map();
|
|
620
|
+
for (const node of nodes) {
|
|
621
|
+
const entry = byFile.get(node.filePath) ?? {
|
|
622
|
+
filePath: node.filePath,
|
|
623
|
+
roles: [],
|
|
624
|
+
confidence: node.confidence,
|
|
625
|
+
reasons: [],
|
|
626
|
+
evidence: []
|
|
627
|
+
};
|
|
628
|
+
if (!entry.roles.includes(node.role))
|
|
629
|
+
entry.roles.push(node.role);
|
|
630
|
+
if (!entry.reasons.includes(node.reason))
|
|
631
|
+
entry.reasons.push(node.reason);
|
|
632
|
+
if (confidencePriority(node.confidence) > confidencePriority(entry.confidence))
|
|
633
|
+
entry.confidence = node.confidence;
|
|
634
|
+
byFile.set(node.filePath, entry);
|
|
635
|
+
}
|
|
636
|
+
for (const edge of edges) {
|
|
637
|
+
for (const filePath of [edge.sourceFile, edge.targetFile]) {
|
|
638
|
+
if (!filePath)
|
|
639
|
+
continue;
|
|
640
|
+
const entry = byFile.get(filePath);
|
|
641
|
+
if (!entry)
|
|
642
|
+
continue;
|
|
643
|
+
if (!entry.reasons.includes(edge.reason))
|
|
644
|
+
entry.reasons.push(edge.reason);
|
|
645
|
+
if (confidencePriority(edge.confidence) > confidencePriority(entry.confidence))
|
|
646
|
+
entry.confidence = edge.confidence;
|
|
647
|
+
entry.evidence.push({
|
|
648
|
+
kind: edge.kind,
|
|
649
|
+
confidence: edge.confidence,
|
|
650
|
+
source: edge.source,
|
|
651
|
+
reason: edge.reason,
|
|
652
|
+
sourceFile: edge.sourceFile,
|
|
653
|
+
targetFile: edge.targetFile,
|
|
654
|
+
line: edge.line,
|
|
655
|
+
targetName: edge.targetName,
|
|
656
|
+
metadata: edge.metadata
|
|
657
|
+
});
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
return [...byFile.values()]
|
|
661
|
+
.map((entry) => ({
|
|
662
|
+
...entry,
|
|
663
|
+
roles: entry.roles.sort((a, b) => rolePriority(a) - rolePriority(b)),
|
|
664
|
+
reasons: entry.reasons.slice(0, 6),
|
|
665
|
+
evidence: entry.evidence.slice(0, 8)
|
|
666
|
+
}))
|
|
667
|
+
.sort((a, b) => rolePriority(a.roles[0] ?? "external") - rolePriority(b.roles[0] ?? "external") || a.filePath.localeCompare(b.filePath));
|
|
668
|
+
}
|
|
669
|
+
function uniqueSymbols(symbols) {
|
|
670
|
+
return [...new Map(symbols.map((symbol) => [symbol.id, symbol])).values()];
|
|
671
|
+
}
|
|
672
|
+
function stringMetadata(edge, key) {
|
|
673
|
+
const value = edge.metadata?.[key];
|
|
674
|
+
return typeof value === "string" ? value : undefined;
|
|
675
|
+
}
|
|
676
|
+
function numberMetadata(edge, key) {
|
|
677
|
+
const value = edge.metadata?.[key];
|
|
678
|
+
return typeof value === "number" ? value : undefined;
|
|
679
|
+
}
|
|
680
|
+
function subgraphEdgeMetadata(edge) {
|
|
681
|
+
const metadata = {};
|
|
682
|
+
for (const key of ["framework", "route", "requestPath", "resource", "model", "operation", "resolution", "dataflowSource", "dataflowKind", "producer"]) {
|
|
683
|
+
const value = edge.metadata?.[key];
|
|
684
|
+
if (value !== undefined)
|
|
685
|
+
metadata[key] = value;
|
|
686
|
+
}
|
|
687
|
+
return Object.keys(metadata).length > 0 ? metadata : undefined;
|
|
688
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { SymbolNode } from "../core/types.js";
|
|
2
|
+
export interface ExportIndex {
|
|
3
|
+
fileSymbols: Map<string, SymbolNode>;
|
|
4
|
+
exportedSymbols: Map<string, SymbolNode>;
|
|
5
|
+
}
|
|
6
|
+
export declare function createExportIndex(symbols: SymbolNode[]): ExportIndex;
|
|
7
|
+
export declare function exportKey(filePath: string, name: string): string;
|