@dreamtree-org/graphify 1.3.0 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.cts CHANGED
@@ -358,6 +358,73 @@ interface ExplainResult {
358
358
  /** Plain-language-ready explanation of the node that best matches `query`. */
359
359
  declare function explainNode(graph: Graph, query: string): ExplainResult | null;
360
360
 
361
+ /**
362
+ * Graph-augmented retrieval (docs/KB-PROVIDER.md §4): the caller has
363
+ * already done similarity search (vector, keyword, hybrid — their
364
+ * pipeline, their embeddings); expandRetrieval() takes those hits and adds
365
+ * what similarity can't see — the nodes *structurally connected* to them.
366
+ * Pure and synchronous like every query function: Graph in, data out; no
367
+ * store access, no embedding calls.
368
+ */
369
+ /**
370
+ * One hit from the caller's own search. `nodeId` when the caller stored
371
+ * graph node ids alongside its records at ingest time; otherwise `text`,
372
+ * resolved against the graph lexically (same resolver queryGraph uses).
373
+ */
374
+ interface RetrievalHit {
375
+ nodeId?: string;
376
+ text?: string;
377
+ /** The caller's similarity score, echoed through to rank seeds. */
378
+ score?: number;
379
+ }
380
+ interface ExpandOptions {
381
+ /** Approximate cap, in tokens (~4 chars each), on the result. Default 2000. */
382
+ tokenBudget?: number;
383
+ maxHops?: number;
384
+ /** Restrict traversal (and reported edges) to these relations, e.g. ['contains', 'references']. */
385
+ relations?: Relation[];
386
+ }
387
+ interface ExpandedNode {
388
+ id: string;
389
+ label: string;
390
+ /** Node kind attr (chunk|section|entity|document|...) — absent on classic code nodes. */
391
+ kind?: string;
392
+ sourceFile: string;
393
+ sourceLocation: string;
394
+ /** 0 = was a seed (or an unresolvable hit echoed through). */
395
+ hops: number;
396
+ /** The edge that first led the traversal here (absent for seeds). */
397
+ via?: TraversalEdge;
398
+ /** The caller's similarity score — seeds only. */
399
+ seedScore?: number;
400
+ }
401
+ interface ExpandedContext {
402
+ /**
403
+ * Union of seed nodes and discovered neighbors, ranked: seeds first (by
404
+ * caller score desc), then by (hops asc, degree desc). Deduped across
405
+ * seeds. Hits that resolve to no graph node are echoed through with
406
+ * hops 0 and no expansion, so adopting this on a partially-ingested
407
+ * corpus never does worse than flat retrieval.
408
+ */
409
+ nodes: ExpandedNode[];
410
+ /** Every edge between included nodes — provenance for citations. */
411
+ edges: TraversalEdge[];
412
+ communities: Array<{
413
+ id: number;
414
+ label: string;
415
+ seedCount: number;
416
+ }>;
417
+ /** True when the token budget stopped the traversal before the frontier was exhausted. */
418
+ truncated: boolean;
419
+ }
420
+ /**
421
+ * Expand the caller's top-K hits through the graph: one joint,
422
+ * token-budgeted BFS seeded from every hit at once, so overlapping
423
+ * neighborhoods dedupe and the budget is shared across seeds. Deterministic
424
+ * (lexical neighbor order, stable ranking) like the rest of the query layer.
425
+ */
426
+ declare function expandRetrieval(graph: Graph, hits: RetrievalHit[], options?: ExpandOptions): ExpandedContext;
427
+
361
428
  interface AffectedNode {
362
429
  id: string;
363
430
  label: string;
@@ -705,4 +772,4 @@ declare class ExtractionValidationError extends Error {
705
772
  */
706
773
  declare function validateExtraction(value: unknown, context?: string): ExtractionResult;
707
774
 
708
- export { type AffectedNode, type Analysis, type ChangedSymbol, type ClusterAlgorithm, type ClusterOptions, type Confidence, type ContextOptions, type ContextPack, type ContextSnippet, type DependencyRule, type DsnQueryFn, EXTRACTOR_REGISTRY, type ExplainResult, type ExportOptions, ExtractionCache, type ExtractionResult, ExtractionResultSchema, ExtractionValidationError, type ExtractionValidationIssue, type FileCategory, type FileManifest, type GraphDiff, type GraphEdge, type GraphNode, type GraphStore, type ImpactOptions, type ImpactResult, LocalFileGraphStore, type MergeEntry, type NodeMatch, type PathResult, type PipelineOptions, type PipelineResult, type QueryOptions, type QueryResult, type ReflectOptions, type Relation, type ResolveStats, type ResultOutcome, type ResultType, type RewiredSymbol, type RulesConfig, type SavedResult, type SerializedGraph, type TestSelection, type TraversalEdge, type Violation, type VisitedNode, affectedBy, analyze, buildContextPack, buildGraph, checkRules, cluster, collectFiles, deserializeGraph, diffGraphs, explainNode, exportGraph, extract, extractMysql, globToRegExp, graphForDirectory, isTestFile, loadGraph, loadResults, mergeGraphs, queryGraph, rankForTask, renderContextPack, renderLessons, renderReport, renderReview, resolveCrossFileReferences, resolveNode, reviewRevisions, runPipeline, saveResult, scoreNodes, serializeGraph, shortestPath, testsForChangedFiles, testsForNode, validateExtraction, validateRules };
775
+ export { type AffectedNode, type Analysis, type ChangedSymbol, type ClusterAlgorithm, type ClusterOptions, type Confidence, type ContextOptions, type ContextPack, type ContextSnippet, type DependencyRule, type DsnQueryFn, EXTRACTOR_REGISTRY, type ExpandOptions, type ExpandedContext, type ExpandedNode, type ExplainResult, type ExportOptions, ExtractionCache, type ExtractionResult, ExtractionResultSchema, ExtractionValidationError, type ExtractionValidationIssue, type FileCategory, type FileManifest, type GraphDiff, type GraphEdge, type GraphNode, type GraphStore, type ImpactOptions, type ImpactResult, LocalFileGraphStore, type MergeEntry, type NodeMatch, type PathResult, type PipelineOptions, type PipelineResult, type QueryOptions, type QueryResult, type ReflectOptions, type Relation, type ResolveStats, type ResultOutcome, type ResultType, type RetrievalHit, type RewiredSymbol, type RulesConfig, type SavedResult, type SerializedGraph, type TestSelection, type TraversalEdge, type Violation, type VisitedNode, affectedBy, analyze, buildContextPack, buildGraph, checkRules, cluster, collectFiles, deserializeGraph, diffGraphs, expandRetrieval, explainNode, exportGraph, extract, extractMysql, globToRegExp, graphForDirectory, isTestFile, loadGraph, loadResults, mergeGraphs, queryGraph, rankForTask, renderContextPack, renderLessons, renderReport, renderReview, resolveCrossFileReferences, resolveNode, reviewRevisions, runPipeline, saveResult, scoreNodes, serializeGraph, shortestPath, testsForChangedFiles, testsForNode, validateExtraction, validateRules };
package/dist/index.d.ts CHANGED
@@ -358,6 +358,73 @@ interface ExplainResult {
358
358
  /** Plain-language-ready explanation of the node that best matches `query`. */
359
359
  declare function explainNode(graph: Graph, query: string): ExplainResult | null;
360
360
 
361
+ /**
362
+ * Graph-augmented retrieval (docs/KB-PROVIDER.md §4): the caller has
363
+ * already done similarity search (vector, keyword, hybrid — their
364
+ * pipeline, their embeddings); expandRetrieval() takes those hits and adds
365
+ * what similarity can't see — the nodes *structurally connected* to them.
366
+ * Pure and synchronous like every query function: Graph in, data out; no
367
+ * store access, no embedding calls.
368
+ */
369
+ /**
370
+ * One hit from the caller's own search. `nodeId` when the caller stored
371
+ * graph node ids alongside its records at ingest time; otherwise `text`,
372
+ * resolved against the graph lexically (same resolver queryGraph uses).
373
+ */
374
+ interface RetrievalHit {
375
+ nodeId?: string;
376
+ text?: string;
377
+ /** The caller's similarity score, echoed through to rank seeds. */
378
+ score?: number;
379
+ }
380
+ interface ExpandOptions {
381
+ /** Approximate cap, in tokens (~4 chars each), on the result. Default 2000. */
382
+ tokenBudget?: number;
383
+ maxHops?: number;
384
+ /** Restrict traversal (and reported edges) to these relations, e.g. ['contains', 'references']. */
385
+ relations?: Relation[];
386
+ }
387
+ interface ExpandedNode {
388
+ id: string;
389
+ label: string;
390
+ /** Node kind attr (chunk|section|entity|document|...) — absent on classic code nodes. */
391
+ kind?: string;
392
+ sourceFile: string;
393
+ sourceLocation: string;
394
+ /** 0 = was a seed (or an unresolvable hit echoed through). */
395
+ hops: number;
396
+ /** The edge that first led the traversal here (absent for seeds). */
397
+ via?: TraversalEdge;
398
+ /** The caller's similarity score — seeds only. */
399
+ seedScore?: number;
400
+ }
401
+ interface ExpandedContext {
402
+ /**
403
+ * Union of seed nodes and discovered neighbors, ranked: seeds first (by
404
+ * caller score desc), then by (hops asc, degree desc). Deduped across
405
+ * seeds. Hits that resolve to no graph node are echoed through with
406
+ * hops 0 and no expansion, so adopting this on a partially-ingested
407
+ * corpus never does worse than flat retrieval.
408
+ */
409
+ nodes: ExpandedNode[];
410
+ /** Every edge between included nodes — provenance for citations. */
411
+ edges: TraversalEdge[];
412
+ communities: Array<{
413
+ id: number;
414
+ label: string;
415
+ seedCount: number;
416
+ }>;
417
+ /** True when the token budget stopped the traversal before the frontier was exhausted. */
418
+ truncated: boolean;
419
+ }
420
+ /**
421
+ * Expand the caller's top-K hits through the graph: one joint,
422
+ * token-budgeted BFS seeded from every hit at once, so overlapping
423
+ * neighborhoods dedupe and the budget is shared across seeds. Deterministic
424
+ * (lexical neighbor order, stable ranking) like the rest of the query layer.
425
+ */
426
+ declare function expandRetrieval(graph: Graph, hits: RetrievalHit[], options?: ExpandOptions): ExpandedContext;
427
+
361
428
  interface AffectedNode {
362
429
  id: string;
363
430
  label: string;
@@ -705,4 +772,4 @@ declare class ExtractionValidationError extends Error {
705
772
  */
706
773
  declare function validateExtraction(value: unknown, context?: string): ExtractionResult;
707
774
 
708
- export { type AffectedNode, type Analysis, type ChangedSymbol, type ClusterAlgorithm, type ClusterOptions, type Confidence, type ContextOptions, type ContextPack, type ContextSnippet, type DependencyRule, type DsnQueryFn, EXTRACTOR_REGISTRY, type ExplainResult, type ExportOptions, ExtractionCache, type ExtractionResult, ExtractionResultSchema, ExtractionValidationError, type ExtractionValidationIssue, type FileCategory, type FileManifest, type GraphDiff, type GraphEdge, type GraphNode, type GraphStore, type ImpactOptions, type ImpactResult, LocalFileGraphStore, type MergeEntry, type NodeMatch, type PathResult, type PipelineOptions, type PipelineResult, type QueryOptions, type QueryResult, type ReflectOptions, type Relation, type ResolveStats, type ResultOutcome, type ResultType, type RewiredSymbol, type RulesConfig, type SavedResult, type SerializedGraph, type TestSelection, type TraversalEdge, type Violation, type VisitedNode, affectedBy, analyze, buildContextPack, buildGraph, checkRules, cluster, collectFiles, deserializeGraph, diffGraphs, explainNode, exportGraph, extract, extractMysql, globToRegExp, graphForDirectory, isTestFile, loadGraph, loadResults, mergeGraphs, queryGraph, rankForTask, renderContextPack, renderLessons, renderReport, renderReview, resolveCrossFileReferences, resolveNode, reviewRevisions, runPipeline, saveResult, scoreNodes, serializeGraph, shortestPath, testsForChangedFiles, testsForNode, validateExtraction, validateRules };
775
+ export { type AffectedNode, type Analysis, type ChangedSymbol, type ClusterAlgorithm, type ClusterOptions, type Confidence, type ContextOptions, type ContextPack, type ContextSnippet, type DependencyRule, type DsnQueryFn, EXTRACTOR_REGISTRY, type ExpandOptions, type ExpandedContext, type ExpandedNode, type ExplainResult, type ExportOptions, ExtractionCache, type ExtractionResult, ExtractionResultSchema, ExtractionValidationError, type ExtractionValidationIssue, type FileCategory, type FileManifest, type GraphDiff, type GraphEdge, type GraphNode, type GraphStore, type ImpactOptions, type ImpactResult, LocalFileGraphStore, type MergeEntry, type NodeMatch, type PathResult, type PipelineOptions, type PipelineResult, type QueryOptions, type QueryResult, type ReflectOptions, type Relation, type ResolveStats, type ResultOutcome, type ResultType, type RetrievalHit, type RewiredSymbol, type RulesConfig, type SavedResult, type SerializedGraph, type TestSelection, type TraversalEdge, type Violation, type VisitedNode, affectedBy, analyze, buildContextPack, buildGraph, checkRules, cluster, collectFiles, deserializeGraph, diffGraphs, expandRetrieval, explainNode, exportGraph, extract, extractMysql, globToRegExp, graphForDirectory, isTestFile, loadGraph, loadResults, mergeGraphs, queryGraph, rankForTask, renderContextPack, renderLessons, renderReport, renderReview, resolveCrossFileReferences, resolveNode, reviewRevisions, runPipeline, saveResult, scoreNodes, serializeGraph, shortestPath, testsForChangedFiles, testsForNode, validateExtraction, validateRules };
package/dist/index.js CHANGED
@@ -24,7 +24,7 @@ import {
24
24
  saveResult,
25
25
  validateExtraction,
26
26
  validateRules
27
- } from "./chunk-UBXYMMXJ.js";
27
+ } from "./chunk-QEB7A5KB.js";
28
28
  import {
29
29
  LocalFileGraphStore,
30
30
  affectedBy,
@@ -33,6 +33,7 @@ import {
33
33
  explainNode,
34
34
  isTestFile,
35
35
  loadGraph,
36
+ nodeTokenCost,
36
37
  queryGraph,
37
38
  rankForTask,
38
39
  renderContextPack,
@@ -42,11 +43,146 @@ import {
42
43
  shortestPath,
43
44
  testsForChangedFiles,
44
45
  testsForNode
45
- } from "./chunk-K322XB4U.js";
46
+ } from "./chunk-7LTO76UD.js";
46
47
  import {
47
48
  extractMysql
48
49
  } from "./chunk-ZPB37LLQ.js";
49
50
  import "./chunk-6JLEILYF.js";
51
+
52
+ // src/retrieval.ts
53
+ var DEFAULT_TOKEN_BUDGET = 2e3;
54
+ var DEFAULT_MAX_HOPS = 2;
55
+ function resolveSeeds(graph, hits) {
56
+ const seeds = /* @__PURE__ */ new Map();
57
+ for (const hit of hits) {
58
+ let id = null;
59
+ if (hit.nodeId !== void 0 && graph.hasNode(hit.nodeId)) {
60
+ id = hit.nodeId;
61
+ } else if (hit.text !== void 0) {
62
+ id = resolveNode(graph, hit.text)?.id ?? null;
63
+ }
64
+ const key = id ?? hit.nodeId ?? hit.text ?? "";
65
+ if (key === "") continue;
66
+ const existing = seeds.get(key);
67
+ if (existing) {
68
+ if (hit.score !== void 0 && (existing.score === void 0 || hit.score > existing.score)) {
69
+ existing.score = hit.score;
70
+ }
71
+ continue;
72
+ }
73
+ seeds.set(key, { id: key, score: hit.score, inGraph: id !== null, text: hit.text });
74
+ }
75
+ return [...seeds.values()];
76
+ }
77
+ function expandRetrieval(graph, hits, options = {}) {
78
+ const tokenBudget = options.tokenBudget ?? DEFAULT_TOKEN_BUDGET;
79
+ const maxHops = options.maxHops ?? DEFAULT_MAX_HOPS;
80
+ const relations = options.relations ? new Set(options.relations) : null;
81
+ const seeds = resolveSeeds(graph, hits);
82
+ const graphSeeds = seeds.filter((s) => s.inGraph);
83
+ const seedScore = new Map(graphSeeds.map((s) => [s.id, s.score]));
84
+ const hops = /* @__PURE__ */ new Map();
85
+ const via = /* @__PURE__ */ new Map();
86
+ const frontier = [];
87
+ let spentTokens = 0;
88
+ for (const seed of graphSeeds) {
89
+ hops.set(seed.id, 0);
90
+ frontier.push(seed.id);
91
+ spentTokens += nodeTokenCost(graph, seed.id);
92
+ }
93
+ let truncated = false;
94
+ while (frontier.length > 0) {
95
+ const current = frontier.shift();
96
+ const currentHops = hops.get(current);
97
+ if (currentHops >= maxHops) continue;
98
+ const candidates = [];
99
+ graph.forEachEdge(current, (_key, attrs, source, target) => {
100
+ const relation = String(attrs.relation);
101
+ if (relations && !relations.has(relation)) return;
102
+ const neighbor = source === current ? target : source;
103
+ candidates.push({
104
+ neighbor,
105
+ edge: { source, target, relation, confidence: String(attrs.confidence) }
106
+ });
107
+ });
108
+ candidates.sort((a, b) => a.neighbor.localeCompare(b.neighbor));
109
+ for (const { neighbor, edge } of candidates) {
110
+ if (hops.has(neighbor)) continue;
111
+ const cost = nodeTokenCost(graph, neighbor);
112
+ if (spentTokens + cost > tokenBudget) {
113
+ truncated = true;
114
+ continue;
115
+ }
116
+ spentTokens += cost;
117
+ hops.set(neighbor, currentHops + 1);
118
+ via.set(neighbor, edge);
119
+ frontier.push(neighbor);
120
+ }
121
+ }
122
+ const degree = (id) => graph.degree(id);
123
+ const nodes = [...hops.entries()].map(([id, hopCount]) => {
124
+ const attrs = graph.getNodeAttributes(id);
125
+ return {
126
+ id,
127
+ label: attrs.label ?? id,
128
+ ...attrs.kind !== void 0 ? { kind: String(attrs.kind) } : {},
129
+ sourceFile: attrs.sourceFile ?? "",
130
+ sourceLocation: attrs.sourceLocation ?? "",
131
+ hops: hopCount,
132
+ ...via.has(id) ? { via: via.get(id) } : {},
133
+ ...hopCount === 0 && seedScore.get(id) !== void 0 ? { seedScore: seedScore.get(id) } : {}
134
+ };
135
+ });
136
+ for (const seed of seeds) {
137
+ if (seed.inGraph) continue;
138
+ nodes.push({
139
+ id: seed.id,
140
+ label: seed.text ?? seed.id,
141
+ sourceFile: "",
142
+ sourceLocation: "",
143
+ hops: 0,
144
+ ...seed.score !== void 0 ? { seedScore: seed.score } : {}
145
+ });
146
+ }
147
+ nodes.sort((a, b) => {
148
+ if (a.hops !== b.hops) return a.hops - b.hops;
149
+ if (a.hops === 0) {
150
+ return (b.seedScore ?? -Infinity) - (a.seedScore ?? -Infinity) || a.id.localeCompare(b.id);
151
+ }
152
+ const degreeDiff = (graph.hasNode(b.id) ? degree(b.id) : 0) - (graph.hasNode(a.id) ? degree(a.id) : 0);
153
+ return degreeDiff || a.id.localeCompare(b.id);
154
+ });
155
+ const included = /* @__PURE__ */ new Set([...hops.keys()]);
156
+ const edges = [];
157
+ graph.forEachEdge((_key, attrs, source, target) => {
158
+ if (!included.has(source) || !included.has(target)) return;
159
+ const relation = String(attrs.relation);
160
+ if (relations && !relations.has(relation)) return;
161
+ edges.push({ source, target, relation, confidence: String(attrs.confidence) });
162
+ });
163
+ edges.sort(
164
+ (a, b) => a.source.localeCompare(b.source) || a.target.localeCompare(b.target) || a.relation.localeCompare(b.relation)
165
+ );
166
+ const communities = /* @__PURE__ */ new Map();
167
+ for (const id of included) {
168
+ const attrs = graph.getNodeAttributes(id);
169
+ const community = attrs.community;
170
+ if (community === void 0) continue;
171
+ const entry = communities.get(community) ?? {
172
+ id: community,
173
+ label: attrs.communityLabel ?? String(community),
174
+ seedCount: 0
175
+ };
176
+ if (hops.get(id) === 0) entry.seedCount += 1;
177
+ communities.set(community, entry);
178
+ }
179
+ return {
180
+ nodes,
181
+ edges,
182
+ communities: [...communities.values()].sort((a, b) => b.seedCount - a.seedCount || a.id - b.id),
183
+ truncated
184
+ };
185
+ }
50
186
  export {
51
187
  EXTRACTOR_REGISTRY,
52
188
  ExtractionCache,
@@ -62,6 +198,7 @@ export {
62
198
  collectFiles,
63
199
  deserializeGraph,
64
200
  diffGraphs,
201
+ expandRetrieval,
65
202
  explainNode,
66
203
  exportGraph,
67
204
  extract,
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
1
+ {"version":3,"sources":["../src/retrieval.ts"],"sourcesContent":["import type Graph from 'graphology';\nimport { nodeTokenCost, resolveNode, type TraversalEdge } from './query.js';\nimport type { Relation } from './types.js';\n\n/**\n * Graph-augmented retrieval (docs/KB-PROVIDER.md §4): the caller has\n * already done similarity search (vector, keyword, hybrid — their\n * pipeline, their embeddings); expandRetrieval() takes those hits and adds\n * what similarity can't see — the nodes *structurally connected* to them.\n * Pure and synchronous like every query function: Graph in, data out; no\n * store access, no embedding calls.\n */\n\n/**\n * One hit from the caller's own search. `nodeId` when the caller stored\n * graph node ids alongside its records at ingest time; otherwise `text`,\n * resolved against the graph lexically (same resolver queryGraph uses).\n */\nexport interface RetrievalHit {\n nodeId?: string;\n text?: string;\n /** The caller's similarity score, echoed through to rank seeds. */\n score?: number;\n}\n\nexport interface ExpandOptions {\n /** Approximate cap, in tokens (~4 chars each), on the result. Default 2000. */\n tokenBudget?: number;\n maxHops?: number;\n /** Restrict traversal (and reported edges) to these relations, e.g. ['contains', 'references']. */\n relations?: Relation[];\n}\n\nexport interface ExpandedNode {\n id: string;\n label: string;\n /** Node kind attr (chunk|section|entity|document|...) — absent on classic code nodes. */\n kind?: string;\n sourceFile: string;\n sourceLocation: string;\n /** 0 = was a seed (or an unresolvable hit echoed through). */\n hops: number;\n /** The edge that first led the traversal here (absent for seeds). */\n via?: TraversalEdge;\n /** The caller's similarity score — seeds only. */\n seedScore?: number;\n}\n\nexport interface ExpandedContext {\n /**\n * Union of seed nodes and discovered neighbors, ranked: seeds first (by\n * caller score desc), then by (hops asc, degree desc). Deduped across\n * seeds. Hits that resolve to no graph node are echoed through with\n * hops 0 and no expansion, so adopting this on a partially-ingested\n * corpus never does worse than flat retrieval.\n */\n nodes: ExpandedNode[];\n /** Every edge between included nodes — provenance for citations. */\n edges: TraversalEdge[];\n communities: Array<{ id: number; label: string; seedCount: number }>;\n /** True when the token budget stopped the traversal before the frontier was exhausted. */\n truncated: boolean;\n}\n\nconst DEFAULT_TOKEN_BUDGET = 2000;\nconst DEFAULT_MAX_HOPS = 2;\n\ninterface Seed {\n id: string;\n score: number | undefined;\n inGraph: boolean;\n /** Fallback label for hits that resolve to no graph node. */\n text?: string;\n}\n\n/**\n * Resolve hits to seed nodes: exact id when it exists in the graph, else\n * lexical resolution of `text`. Duplicate resolutions collapse into one\n * seed keeping the best caller score. Hits that resolve nowhere are kept\n * as out-of-graph seeds so the caller sees every hit accounted for.\n */\nfunction resolveSeeds(graph: Graph, hits: RetrievalHit[]): Seed[] {\n const seeds = new Map<string, Seed>();\n for (const hit of hits) {\n let id: string | null = null;\n if (hit.nodeId !== undefined && graph.hasNode(hit.nodeId)) {\n id = hit.nodeId;\n } else if (hit.text !== undefined) {\n id = resolveNode(graph, hit.text)?.id ?? null;\n }\n\n const key = id ?? hit.nodeId ?? hit.text ?? '';\n if (key === '') continue; // an entirely empty hit carries nothing to resolve or echo\n const existing = seeds.get(key);\n if (existing) {\n if (hit.score !== undefined && (existing.score === undefined || hit.score > existing.score)) {\n existing.score = hit.score;\n }\n continue;\n }\n seeds.set(key, { id: key, score: hit.score, inGraph: id !== null, text: hit.text });\n }\n return [...seeds.values()];\n}\n\n/**\n * Expand the caller's top-K hits through the graph: one joint,\n * token-budgeted BFS seeded from every hit at once, so overlapping\n * neighborhoods dedupe and the budget is shared across seeds. Deterministic\n * (lexical neighbor order, stable ranking) like the rest of the query layer.\n */\nexport function expandRetrieval(graph: Graph, hits: RetrievalHit[], options: ExpandOptions = {}): ExpandedContext {\n const tokenBudget = options.tokenBudget ?? DEFAULT_TOKEN_BUDGET;\n const maxHops = options.maxHops ?? DEFAULT_MAX_HOPS;\n const relations = options.relations ? new Set<string>(options.relations) : null;\n\n const seeds = resolveSeeds(graph, hits);\n const graphSeeds = seeds.filter((s) => s.inGraph);\n const seedScore = new Map(graphSeeds.map((s) => [s.id, s.score]));\n\n const hops = new Map<string, number>(); // id -> hop distance\n const via = new Map<string, TraversalEdge>();\n const frontier: string[] = [];\n let spentTokens = 0;\n for (const seed of graphSeeds) {\n hops.set(seed.id, 0);\n frontier.push(seed.id);\n spentTokens += nodeTokenCost(graph, seed.id);\n }\n\n let truncated = false;\n while (frontier.length > 0) {\n const current = frontier.shift() as string;\n const currentHops = hops.get(current) as number;\n if (currentHops >= maxHops) continue;\n\n // Collect candidate edges (not bare neighbors) so the relations filter\n // applies and `via` can record how each node was reached.\n const candidates: Array<{ neighbor: string; edge: TraversalEdge }> = [];\n graph.forEachEdge(current, (_key, attrs, source, target) => {\n const relation = String(attrs.relation);\n if (relations && !relations.has(relation)) return;\n const neighbor = source === current ? target : source;\n candidates.push({\n neighbor,\n edge: { source, target, relation, confidence: String(attrs.confidence) },\n });\n });\n candidates.sort((a, b) => a.neighbor.localeCompare(b.neighbor));\n\n for (const { neighbor, edge } of candidates) {\n if (hops.has(neighbor)) continue;\n const cost = nodeTokenCost(graph, neighbor);\n if (spentTokens + cost > tokenBudget) {\n truncated = true;\n continue;\n }\n spentTokens += cost;\n hops.set(neighbor, currentHops + 1);\n via.set(neighbor, edge);\n frontier.push(neighbor);\n }\n }\n\n const degree = (id: string): number => graph.degree(id);\n const nodes: ExpandedNode[] = [...hops.entries()].map(([id, hopCount]) => {\n const attrs = graph.getNodeAttributes(id);\n return {\n id,\n label: (attrs.label as string | undefined) ?? id,\n ...(attrs.kind !== undefined ? { kind: String(attrs.kind) } : {}),\n sourceFile: (attrs.sourceFile as string | undefined) ?? '',\n sourceLocation: (attrs.sourceLocation as string | undefined) ?? '',\n hops: hopCount,\n ...(via.has(id) ? { via: via.get(id) as TraversalEdge } : {}),\n ...(hopCount === 0 && seedScore.get(id) !== undefined ? { seedScore: seedScore.get(id) as number } : {}),\n };\n });\n\n // Out-of-graph hits: echoed through so the result never covers less than\n // the caller's own flat retrieval did.\n for (const seed of seeds) {\n if (seed.inGraph) continue;\n nodes.push({\n id: seed.id,\n label: seed.text ?? seed.id,\n sourceFile: '',\n sourceLocation: '',\n hops: 0,\n ...(seed.score !== undefined ? { seedScore: seed.score } : {}),\n });\n }\n\n nodes.sort((a, b) => {\n if (a.hops !== b.hops) return a.hops - b.hops;\n if (a.hops === 0) {\n return (b.seedScore ?? -Infinity) - (a.seedScore ?? -Infinity) || a.id.localeCompare(b.id);\n }\n const degreeDiff = (graph.hasNode(b.id) ? degree(b.id) : 0) - (graph.hasNode(a.id) ? degree(a.id) : 0);\n return degreeDiff || a.id.localeCompare(b.id);\n });\n\n const included = new Set([...hops.keys()]);\n const edges: TraversalEdge[] = [];\n graph.forEachEdge((_key, attrs, source, target) => {\n if (!included.has(source) || !included.has(target)) return;\n const relation = String(attrs.relation);\n if (relations && !relations.has(relation)) return;\n edges.push({ source, target, relation, confidence: String(attrs.confidence) });\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 const communities = new Map<number, { id: number; label: string; seedCount: number }>();\n for (const id of included) {\n const attrs = graph.getNodeAttributes(id);\n const community = attrs.community as number | undefined;\n if (community === undefined) continue;\n const entry = communities.get(community) ?? {\n id: community,\n label: (attrs.communityLabel as string | undefined) ?? String(community),\n seedCount: 0,\n };\n if ((hops.get(id) as number) === 0) entry.seedCount += 1;\n communities.set(community, entry);\n }\n\n return {\n nodes,\n edges,\n communities: [...communities.values()].sort((a, b) => b.seedCount - a.seedCount || a.id - b.id),\n truncated,\n };\n}\n"],"mappings":"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","names":[]}