@dreamtree-org/graphify 1.3.0 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{chunk-K322XB4U.js → chunk-7LTO76UD.js} +2 -1
- package/dist/chunk-7LTO76UD.js.map +1 -0
- package/dist/{chunk-UBXYMMXJ.js → chunk-QEB7A5KB.js} +2 -2
- package/dist/cli/index.cjs.map +1 -1
- package/dist/cli/index.js +2 -2
- package/dist/index.cjs +137 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +68 -1
- package/dist/index.d.ts +68 -1
- package/dist/index.js +139 -2
- package/dist/index.js.map +1 -1
- package/dist/mcp/server.cjs.map +1 -1
- package/dist/mcp/server.js +1 -1
- package/package.json +1 -1
- package/dist/chunk-K322XB4U.js.map +0 -1
- /package/dist/{chunk-UBXYMMXJ.js.map → chunk-QEB7A5KB.js.map} +0 -0
package/dist/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-
|
|
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-
|
|
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":[]}
|