@roarpeng/graphflow 1.15.1 → 1.15.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,435 +1,63 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.DEFAULT_MAX_SIBLING_FILES = exports.DEFAULT_MAX_SYMBOLS_PER_FILE = exports.prepareHitsForPackaging = exports.hasModuleFamilyIntent = exports.expandSiblingDirectoryHits = exports.diversifyHitsBySourceFile = exports.extractFileFromSymbolId = exports.deriveModuleId = exports.modulePathKey = exports.markLayerUsed = exports.canUseLayer = exports.classifyLayer = exports.summarizeNodes = exports.estimateTokens = exports.getEncoder = exports.ARCHITECTURE_QUERY = exports.DEFAULT_EXPANSION_RELATIONS = void 0;
4
- exports.isPinnedL3Node = isPinnedL3Node;
5
- exports.expandSubgraph = expandSubgraph;
3
+ exports.DEFAULT_MAX_SIBLING_FILES = exports.DEFAULT_MAX_SYMBOLS_PER_FILE = exports.prepareHitsForPackaging = exports.hasModuleFamilyIntent = exports.expandSiblingDirectoryHits = exports.diversifyHitsBySourceFile = exports.extractFileFromSymbolId = exports.deriveModuleId = exports.modulePathKey = exports.markLayerUsed = exports.canUseLayer = exports.classifyLayer = exports.summarizeNodes = exports.estimateTokens = exports.getEncoder = exports.vectorRecall = exports.expandSubgraph = exports.isPinnedL3Node = exports.ARCHITECTURE_QUERY = exports.DEFAULT_EXPANSION_RELATIONS = void 0;
6
4
  exports.buildContextSlice = buildContextSlice;
7
- exports.vectorRecall = vectorRecall;
8
5
  exports.buildLayeredContextPackage = buildLayeredContextPackage;
9
6
  exports.createContextRefillManager = createContextRefillManager;
10
7
  exports.buildEnhancedContextPackage = buildEnhancedContextPackage;
11
8
  const logger_js_1 = require("../utils/logger.js");
12
- const embeddings_js_1 = require("../learning/embeddings.js");
13
- const graph_utils_js_1 = require("./graph-utils.js");
14
- const query_expand_js_1 = require("./query-expand.js");
15
- const hit_diversify_js_1 = require("./hit-diversify.js");
16
9
  const graph_compression_js_1 = require("./graph-compression.js");
17
10
  const repo_map_js_1 = require("./repo-map.js");
18
11
  const adaptive_budget_js_1 = require("./adaptive-budget.js");
19
12
  const symbol_extract_js_1 = require("./symbol-extract.js");
13
+ const context_slicer_utils_js_1 = require("./context-slicer-utils.js");
14
+ const embeddings_js_1 = require("../learning/embeddings.js");
15
+ const context_package_core_js_1 = require("./context-package-core.js");
16
+ Object.defineProperty(exports, "expandSubgraph", { enumerable: true, get: function () { return context_package_core_js_1.expandSubgraph; } });
17
+ Object.defineProperty(exports, "isPinnedL3Node", { enumerable: true, get: function () { return context_package_core_js_1.isPinnedL3Node; } });
18
+ Object.defineProperty(exports, "vectorRecall", { enumerable: true, get: function () { return context_package_core_js_1.vectorRecall; } });
20
19
  var context_slicer_types_js_1 = require("./context-slicer-types.js");
21
20
  Object.defineProperty(exports, "DEFAULT_EXPANSION_RELATIONS", { enumerable: true, get: function () { return context_slicer_types_js_1.DEFAULT_EXPANSION_RELATIONS; } });
22
21
  Object.defineProperty(exports, "ARCHITECTURE_QUERY", { enumerable: true, get: function () { return context_slicer_types_js_1.ARCHITECTURE_QUERY; } });
23
- const L3_PIN_HINT = /alignment|deviation|goal/i;
24
- /**
25
- * Governance pins for L3: Decision/goal nodes that packing must prefer so
26
- * budget truncation cannot drop alignment / deviation / goal constraints.
27
- */
28
- function isPinnedL3Node(node) {
29
- if (node.id.startsWith("goal:")) {
30
- return true;
31
- }
32
- if (node.type !== "Decision") {
33
- return false;
34
- }
35
- if (L3_PIN_HINT.test(node.content)) {
36
- return true;
37
- }
38
- if (node.metadata && L3_PIN_HINT.test(JSON.stringify(node.metadata))) {
39
- return true;
40
- }
41
- return false;
42
- }
43
- function collectPinnedL3Nodes(ranked, snapshotNodes, added) {
44
- const pins = [];
45
- const seen = new Set();
46
- const consider = (node) => {
47
- if (added.has(node.id) || seen.has(node.id) || !isPinnedL3Node(node)) {
48
- return;
49
- }
50
- seen.add(node.id);
51
- pins.push(node);
52
- };
53
- for (const node of ranked)
54
- consider(node);
55
- if (snapshotNodes) {
56
- for (const node of snapshotNodes)
57
- consider(node);
58
- }
59
- return pins;
60
- }
61
- function tryAppendL3Node(node, ctx, acc) {
62
- if (ctx.maxAnchors !== undefined && ctx.anchorChannel.length >= ctx.maxAnchors) {
63
- return "skip";
64
- }
65
- if (ctx.added.has(node.id)) {
66
- return "skip";
67
- }
68
- if (!(0, context_slicer_utils_js_2.canUseLayer)("L3", ctx.quota, ctx.used)) {
69
- return "skip";
70
- }
71
- const summary = `${node.type}: ${node.content}`;
72
- const estimate = (0, context_slicer_utils_js_2.estimateTokens)(summary);
73
- if (acc.tokens + estimate > ctx.maxTokens) {
74
- acc.truncated = true;
75
- return "budget";
76
- }
77
- ctx.summaryChannel.push(summary);
78
- ctx.anchorChannel.push({ id: node.id, type: node.type, layer: "L3" });
79
- ctx.added.add(node.id);
80
- acc.tokens += estimate;
81
- (0, context_slicer_utils_js_2.markLayerUsed)("L3", ctx.used);
82
- return "packed";
83
- }
84
- // ───────────────── Conversation Graph W2a: dialogue nodes in L3 packing ─────────────────
85
- /** Max matched dialogue turns packed per preview (after effective-turns filter). */
86
- const DIALOGUE_PACK_MAX_TURNS = 3;
87
- /**
88
- * Collect dialogue-turn context for a query (Conversation Graph W2a).
89
- *
90
- * - Matches effective (non-superseded) turns whose query/title/summary tokens
91
- * overlap the search tokens; never within the same recording session id
92
- * chain — matched turns are historical Q&A, including other sessions.
93
- * - Each packed turn carries an optional correction annotation when the
94
- * turn itself supersedes an earlier one ("结论 X 已被修正为 Y").
95
- * - Falls back to [] on any failure — dialogue packing is additive.
96
- */
97
- async function collectDialogueContextLines(client, query, options) {
98
- try {
99
- const { listDialogueTurns, effectiveTurns, formatSupersessionLine } = await import("../learning/dialogue-thread.js");
100
- const allTurns = await listDialogueTurns(client, { limit: 60 });
101
- const turns = effectiveTurns(allTurns);
102
- if (turns.length === 0)
103
- return [];
104
- const scoreTokens = (0, graph_utils_js_1.buildSearchScoreTokens)(query, options?.englishQuery);
105
- if (scoreTokens.length === 0)
106
- return [];
107
- const tokenSet = new Set(scoreTokens.map((t) => t.toLowerCase()));
108
- const scored = turns
109
- .map((turn) => {
110
- const haystack = `${turn.userQuery} ${turn.title ?? ""} ${turn.summary ?? ""}`.toLowerCase();
111
- let hits = 0;
112
- for (const token of tokenSet) {
113
- if (haystack.includes(token))
114
- hits += 1;
115
- }
116
- return { turn, hits };
117
- })
118
- .filter((s) => s.hits > 0)
119
- .sort((a, b) => b.hits - a.hits || b.turn.updatedAt - a.turn.updatedAt)
120
- .slice(0, DIALOGUE_PACK_MAX_TURNS);
121
- if (scored.length === 0)
122
- return [];
123
- // Correction chain lookup uses ALL turns (the superseded one is off the
124
- // effective list by design — we need it to render "was corrected").
125
- const byId = new Map(allTurns.map((t) => [t.id, t]));
126
- const lines = [];
127
- for (const { turn } of scored) {
128
- const correctionTargets = (turn.supersedesTurnIds ?? [])
129
- .map((id) => byId.get(id))
130
- .filter((t) => Boolean(t));
131
- const correctionLine = correctionTargets.length > 0
132
- ? formatSupersessionLine(correctionTargets[correctionTargets.length - 1], turn)
133
- : undefined;
134
- lines.push({
135
- kind: "turn",
136
- text: turn.title ?? turn.userQuery,
137
- node: {
138
- id: turn.id,
139
- type: "Decision",
140
- content: `dialogue-turn #${turn.seq} Q: ${turn.userQuery}`,
141
- metadata: { kind: "dialogue-turn", record: JSON.stringify(turn) },
142
- },
143
- ...(correctionLine ? { correctionLine } : {}),
144
- });
145
- }
146
- return lines;
147
- }
148
- catch {
149
- return [];
150
- }
151
- }
152
22
  // Re-export utility functions that were previously exported
153
- var context_slicer_utils_js_1 = require("./context-slicer-utils.js");
154
- Object.defineProperty(exports, "getEncoder", { enumerable: true, get: function () { return context_slicer_utils_js_1.getEncoder; } });
155
- Object.defineProperty(exports, "estimateTokens", { enumerable: true, get: function () { return context_slicer_utils_js_1.estimateTokens; } });
156
- Object.defineProperty(exports, "summarizeNodes", { enumerable: true, get: function () { return context_slicer_utils_js_1.summarizeNodes; } });
157
- Object.defineProperty(exports, "classifyLayer", { enumerable: true, get: function () { return context_slicer_utils_js_1.classifyLayer; } });
158
- Object.defineProperty(exports, "canUseLayer", { enumerable: true, get: function () { return context_slicer_utils_js_1.canUseLayer; } });
159
- Object.defineProperty(exports, "markLayerUsed", { enumerable: true, get: function () { return context_slicer_utils_js_1.markLayerUsed; } });
160
- Object.defineProperty(exports, "modulePathKey", { enumerable: true, get: function () { return context_slicer_utils_js_1.modulePathKey; } });
161
- Object.defineProperty(exports, "deriveModuleId", { enumerable: true, get: function () { return context_slicer_utils_js_1.deriveModuleId; } });
162
- Object.defineProperty(exports, "extractFileFromSymbolId", { enumerable: true, get: function () { return context_slicer_utils_js_1.extractFileFromSymbolId; } });
163
- var hit_diversify_js_2 = require("./hit-diversify.js");
164
- Object.defineProperty(exports, "diversifyHitsBySourceFile", { enumerable: true, get: function () { return hit_diversify_js_2.diversifyHitsBySourceFile; } });
165
- Object.defineProperty(exports, "expandSiblingDirectoryHits", { enumerable: true, get: function () { return hit_diversify_js_2.expandSiblingDirectoryHits; } });
166
- Object.defineProperty(exports, "hasModuleFamilyIntent", { enumerable: true, get: function () { return hit_diversify_js_2.hasModuleFamilyIntent; } });
167
- Object.defineProperty(exports, "prepareHitsForPackaging", { enumerable: true, get: function () { return hit_diversify_js_2.prepareHitsForPackaging; } });
168
- Object.defineProperty(exports, "DEFAULT_MAX_SYMBOLS_PER_FILE", { enumerable: true, get: function () { return hit_diversify_js_2.DEFAULT_MAX_SYMBOLS_PER_FILE; } });
169
- Object.defineProperty(exports, "DEFAULT_MAX_SIBLING_FILES", { enumerable: true, get: function () { return hit_diversify_js_2.DEFAULT_MAX_SIBLING_FILES; } });
170
- // Import internal helpers (not re-exported)
171
- const context_slicer_utils_js_2 = require("./context-slicer-utils.js");
172
- const context_slicer_types_js_2 = require("./context-slicer-types.js");
173
- async function expandSubgraph(client, seedIds, options) {
174
- if (typeof client.getNeighbors !== "function")
175
- return [];
176
- const hops = options?.hops ?? 1;
177
- const maxNodes = options?.maxNodes ?? 20;
178
- const relations = options?.relations ?? context_slicer_types_js_2.DEFAULT_EXPANSION_RELATIONS;
179
- const excluded = new Set(seedIds);
180
- const collected = new Map();
181
- let frontier = [...seedIds];
182
- for (let depth = 0; depth < hops; depth += 1) {
183
- if (frontier.length === 0)
184
- break;
185
- const neighbors = await client.getNeighbors(frontier, relations, "both");
186
- const nextFrontier = [];
187
- for (const { node } of neighbors) {
188
- if (excluded.has(node.id) || collected.has(node.id))
189
- continue;
190
- collected.set(node.id, node);
191
- nextFrontier.push(node.id);
192
- if (collected.size >= maxNodes)
193
- break;
194
- }
195
- if (collected.size >= maxNodes)
196
- break;
197
- frontier = nextFrontier;
198
- }
199
- return Array.from(collected.values());
200
- }
23
+ var context_slicer_utils_js_2 = require("./context-slicer-utils.js");
24
+ Object.defineProperty(exports, "getEncoder", { enumerable: true, get: function () { return context_slicer_utils_js_2.getEncoder; } });
25
+ Object.defineProperty(exports, "estimateTokens", { enumerable: true, get: function () { return context_slicer_utils_js_2.estimateTokens; } });
26
+ Object.defineProperty(exports, "summarizeNodes", { enumerable: true, get: function () { return context_slicer_utils_js_2.summarizeNodes; } });
27
+ Object.defineProperty(exports, "classifyLayer", { enumerable: true, get: function () { return context_slicer_utils_js_2.classifyLayer; } });
28
+ Object.defineProperty(exports, "canUseLayer", { enumerable: true, get: function () { return context_slicer_utils_js_2.canUseLayer; } });
29
+ Object.defineProperty(exports, "markLayerUsed", { enumerable: true, get: function () { return context_slicer_utils_js_2.markLayerUsed; } });
30
+ Object.defineProperty(exports, "modulePathKey", { enumerable: true, get: function () { return context_slicer_utils_js_2.modulePathKey; } });
31
+ Object.defineProperty(exports, "deriveModuleId", { enumerable: true, get: function () { return context_slicer_utils_js_2.deriveModuleId; } });
32
+ Object.defineProperty(exports, "extractFileFromSymbolId", { enumerable: true, get: function () { return context_slicer_utils_js_2.extractFileFromSymbolId; } });
33
+ var hit_diversify_js_1 = require("./hit-diversify.js");
34
+ Object.defineProperty(exports, "diversifyHitsBySourceFile", { enumerable: true, get: function () { return hit_diversify_js_1.diversifyHitsBySourceFile; } });
35
+ Object.defineProperty(exports, "expandSiblingDirectoryHits", { enumerable: true, get: function () { return hit_diversify_js_1.expandSiblingDirectoryHits; } });
36
+ Object.defineProperty(exports, "hasModuleFamilyIntent", { enumerable: true, get: function () { return hit_diversify_js_1.hasModuleFamilyIntent; } });
37
+ Object.defineProperty(exports, "prepareHitsForPackaging", { enumerable: true, get: function () { return hit_diversify_js_1.prepareHitsForPackaging; } });
38
+ Object.defineProperty(exports, "DEFAULT_MAX_SYMBOLS_PER_FILE", { enumerable: true, get: function () { return hit_diversify_js_1.DEFAULT_MAX_SYMBOLS_PER_FILE; } });
39
+ Object.defineProperty(exports, "DEFAULT_MAX_SIBLING_FILES", { enumerable: true, get: function () { return hit_diversify_js_1.DEFAULT_MAX_SIBLING_FILES; } });
201
40
  async function buildContextSlice(client, query, maxTokens) {
202
41
  const pkg = await buildLayeredContextPackage(client, query, maxTokens);
203
42
  return { items: pkg.summaryChannel, tokenEstimate: pkg.tokenEstimate };
204
43
  }
205
- function vectorRecall(nodes, queryEmbedding, topK, minSimilarity) {
206
- if (!queryEmbedding || queryEmbedding.length === 0)
207
- return [];
208
- const scored = [];
209
- for (const node of nodes) {
210
- const emb = (0, embeddings_js_1.extractEmbedding)(node);
211
- if (!emb)
212
- continue;
213
- const sim = (0, embeddings_js_1.cosineSimilarity)(queryEmbedding, emb);
214
- if (sim >= minSimilarity)
215
- scored.push({ node, sim });
216
- }
217
- scored.sort((a, b) => b.sim - a.sim);
218
- return scored.slice(0, topK).map((s) => s.node);
219
- }
220
- function collectVectorRecallCandidates(client, keywordHits, enableFullGraphVectorRecall) {
221
- if (!enableFullGraphVectorRecall) {
222
- return keywordHits;
223
- }
224
- const byId = new Map();
225
- for (const hit of keywordHits) {
226
- if ((0, embeddings_js_1.extractEmbedding)(hit)) {
227
- byId.set(hit.id, hit);
228
- }
229
- }
230
- const snapshot = client.readSnapshot?.();
231
- if (!snapshot) {
232
- return keywordHits;
233
- }
234
- for (const node of snapshot.nodes) {
235
- if ((0, embeddings_js_1.extractEmbedding)(node)) {
236
- byId.set(node.id, node);
237
- }
238
- }
239
- return byId.size > 0 ? Array.from(byId.values()) : keywordHits;
240
- }
241
- async function hnswVectorRecall(nodes, queryEmbedding, topK, minSimilarity, hnswIndexPath) {
242
- // Memoized per-process + optional disk-persisted index: avoids re-extracting
243
- // embeddings for every query and speeds up MCP server restarts.
244
- const { getSharedVectorIndex } = await import("../learning/hnsw-index.js");
245
- const { index } = getSharedVectorIndex(nodes, hnswIndexPath);
246
- const results = await index.search(queryEmbedding, topK, minSimilarity);
247
- return results.map((result) => result.node);
248
- }
249
44
  async function buildLayeredContextPackage(client, query, maxTokens, options) {
250
- const keywordHits = await (0, query_expand_js_1.collectExpandedKeywordHits)(client, query, options?.workspaceRoot, options?.englishQuery);
45
+ const keywordHits = await (0, context_package_core_js_1.collectKeywordHits)(client, query, options);
251
46
  let hits = keywordHits;
252
- if (options?.enableVectorRecall === true && options.embeddingProvider) {
253
- try {
254
- const queryEmbedding = await options.embeddingProvider.embed(query);
255
- const topK = options.vectorTopK ?? 8;
256
- const minSim = options.vectorMinSimilarity ?? 0.05;
257
- const vectorCandidates = collectVectorRecallCandidates(client, keywordHits, options.enableFullGraphVectorRecall === true);
258
- const vectorHits = await hnswVectorRecall(vectorCandidates, queryEmbedding, topK, minSim, options?.hnswIndexPath);
259
- hits = (0, embeddings_js_1.reciprocalRankFusion)([keywordHits, vectorHits]);
260
- }
261
- catch (error) {
262
- logger_js_1.logger.warn({ error }, "Vector recall failed in buildLayeredContextPackage");
263
- hits = keywordHits;
264
- }
47
+ const fused = await (0, context_package_core_js_1.fuseVectorRecallIfEnabled)(client, query, keywordHits, options, "buildLayeredContextPackage");
48
+ if (fused) {
49
+ hits = fused;
265
50
  }
266
51
  const snapshotNodes = client.readSnapshot?.()?.nodes;
267
- hits = (0, hit_diversify_js_1.prepareHitsForPackaging)(hits, {
268
- query,
269
- ...(options?.englishQuery !== undefined ? { englishQuery: options.englishQuery } : {}),
270
- ...(snapshotNodes !== undefined ? { allNodes: snapshotNodes } : {}),
271
- });
272
- const summaryChannel = [];
273
- const anchorChannel = [];
274
- let tokens = 0;
275
- let truncated = false;
276
- const quota = {
277
- l1: options?.layerQuota?.l1 ?? Number.POSITIVE_INFINITY,
278
- l2: options?.layerQuota?.l2 ?? Number.POSITIVE_INFINITY,
279
- l3: options?.layerQuota?.l3 ?? Number.POSITIVE_INFINITY,
280
- };
281
- const used = { l1: 0, l2: 0, l3: 0 };
282
- const added = new Set();
283
- const hitById = new Map();
284
- for (const hit of hits) {
285
- hitById.set(hit.id, hit);
286
- const layer = (0, context_slicer_utils_js_2.classifyLayer)(hit);
287
- if (!(0, context_slicer_utils_js_2.canUseLayer)(layer, quota, used)) {
288
- continue;
289
- }
290
- const summary = `${hit.type}: ${hit.content}`;
291
- const estimate = (0, context_slicer_utils_js_2.estimateTokens)(summary);
292
- if (tokens + estimate > maxTokens) {
293
- truncated = true;
294
- break;
295
- }
296
- summaryChannel.push(summary);
297
- anchorChannel.push({ id: hit.id, type: hit.type, layer });
298
- added.add(hit.id);
299
- tokens += estimate;
300
- (0, context_slicer_utils_js_2.markLayerUsed)(layer, used);
301
- }
302
- if (!truncated) {
303
- const moduleIds = new Set();
304
- for (const anchor of anchorChannel) {
305
- if (anchor.layer !== "L1" || (anchor.type !== "File" && anchor.type !== "Symbol")) {
306
- continue;
307
- }
308
- const moduleId = (0, context_slicer_utils_js_2.deriveModuleId)(anchor, hitById.get(anchor.id));
309
- if (moduleId) {
310
- moduleIds.add(moduleId);
311
- }
312
- }
313
- let moduleNodes = [];
314
- if (moduleIds.size > 0 && typeof client.getNodesByIds === "function") {
315
- moduleNodes = await client.getNodesByIds(Array.from(moduleIds));
316
- }
317
- for (const moduleId of moduleIds) {
318
- if (added.has(moduleId))
319
- continue;
320
- if (!(0, context_slicer_utils_js_2.canUseLayer)("L2", quota, used))
321
- break;
322
- const node = moduleNodes.find((n) => n.id === moduleId) ??
323
- ({ id: moduleId, type: "Module", content: moduleId.slice("module:".length) });
324
- const summary = `${node.type}: ${node.content}`;
325
- const estimate = (0, context_slicer_utils_js_2.estimateTokens)(summary);
326
- if (tokens + estimate > maxTokens) {
327
- truncated = true;
328
- break;
329
- }
330
- summaryChannel.push(summary);
331
- anchorChannel.push({ id: node.id, type: node.type, layer: "L2" });
332
- added.add(node.id);
333
- tokens += estimate;
334
- (0, context_slicer_utils_js_2.markLayerUsed)("L2", used);
335
- }
336
- }
337
- const archIntent = context_slicer_types_js_2.ARCHITECTURE_QUERY.test((0, graph_utils_js_1.composeContextQuery)(query, options?.englishQuery));
338
- if (options?.enableAlwaysOnLayers || archIntent) {
339
- const l3Candidates = (0, graph_utils_js_1.rankNodesForContextQuery)((await client.queryByKeyword(query)).filter((n) => n.type === "Skill" || n.type === "Decision"), query, {
340
- scoreTokens: (0, graph_utils_js_1.buildSearchScoreTokens)(query, options?.englishQuery),
341
- matchQueries: options?.englishQuery?.trim()
342
- ? [query, options.englishQuery.trim()]
343
- : [query],
344
- ...(options?.englishQuery !== undefined ? { englishQuery: options.englishQuery } : {}),
345
- });
346
- const snapshotForPins = client.readSnapshot?.()?.nodes;
347
- const acc = { tokens, truncated };
348
- const packCtx = { summaryChannel, anchorChannel, added, quota, used, maxTokens };
349
- const pins = collectPinnedL3Nodes(l3Candidates, snapshotForPins, added);
350
- for (const node of pins) {
351
- if (tryAppendL3Node(node, packCtx, acc) === "budget")
352
- break;
353
- }
354
- tokens = acc.tokens;
355
- truncated = acc.truncated;
356
- if (!truncated) {
357
- let l3Added = pins.filter((n) => added.has(n.id)).length;
358
- for (const node of l3Candidates) {
359
- if (l3Added >= 2)
360
- break;
361
- if (isPinnedL3Node(node))
362
- continue;
363
- const result = tryAppendL3Node(node, packCtx, acc);
364
- if (result === "budget")
365
- break;
366
- if (result === "packed")
367
- l3Added += 1;
368
- }
369
- tokens = acc.tokens;
370
- truncated = acc.truncated;
371
- }
372
- }
373
- // Conversation Graph W2a: pack matched dialogue turns (effective ones plus
374
- // a correction-chain annotation) under the same L3 quota + token budget as
375
- // every other node — dialogue anchors are never exempt from budgeting.
376
- if (!truncated) {
377
- const dialogueAcc = { tokens, truncated };
378
- const dialoguePackCtx = { summaryChannel, anchorChannel, added, quota, used, maxTokens };
379
- const dialogueLines = await collectDialogueContextLines(client, query, options);
380
- for (const line of dialogueLines) {
381
- if (line.kind === "summary") {
382
- const estimate = (0, context_slicer_utils_js_2.estimateTokens)(line.text);
383
- if (dialogueAcc.tokens + estimate > maxTokens) {
384
- dialogueAcc.truncated = true;
385
- break;
386
- }
387
- dialogueAcc.tokens += estimate;
388
- }
389
- else if (line.kind === "turn" && line.node) {
390
- if (tryAppendL3Node(line.node, dialoguePackCtx, dialogueAcc) === "budget") {
391
- break;
392
- }
393
- if (line.correctionLine) {
394
- const estimate = (0, context_slicer_utils_js_2.estimateTokens)(line.correctionLine);
395
- if (dialogueAcc.tokens + estimate > maxTokens) {
396
- dialogueAcc.truncated = true;
397
- break;
398
- }
399
- dialoguePackCtx.summaryChannel.push(line.correctionLine);
400
- dialogueAcc.tokens += estimate;
401
- }
402
- }
403
- }
404
- tokens = dialogueAcc.tokens;
405
- truncated = dialogueAcc.truncated;
406
- }
407
- const enableExpansion = options?.enableEdgeExpansion !== false;
408
- if (enableExpansion && !truncated && typeof client.getNeighbors === "function") {
409
- const seedIds = anchorChannel.slice(0, 5).map((a) => a.id);
410
- if (seedIds.length > 0) {
411
- const expanded = await expandSubgraph(client, seedIds, { hops: 1 });
412
- for (const node of expanded) {
413
- if (added.has(node.id))
414
- continue;
415
- const layer = (0, context_slicer_utils_js_2.classifyLayer)(node);
416
- if (!(0, context_slicer_utils_js_2.canUseLayer)(layer, quota, used))
417
- continue;
418
- const summary = `${node.type}: ${node.content}`;
419
- const estimate = (0, context_slicer_utils_js_2.estimateTokens)(summary);
420
- if (tokens + estimate > maxTokens) {
421
- truncated = true;
422
- break;
423
- }
424
- summaryChannel.push(summary);
425
- anchorChannel.push({ id: node.id, type: node.type, layer });
426
- added.add(node.id);
427
- tokens += estimate;
428
- (0, context_slicer_utils_js_2.markLayerUsed)(layer, used);
429
- }
430
- }
431
- }
432
- return { summaryChannel, anchorChannel, tokenEstimate: tokens, truncated };
52
+ hits = (0, context_package_core_js_1.preparePackageHits)(hits, query, options, snapshotNodes);
53
+ const state = (0, context_package_core_js_1.createPackState)(maxTokens, options);
54
+ const budget = { tokens: 0, truncated: false };
55
+ (0, context_package_core_js_1.packPrimaryHits)(hits, state, budget);
56
+ await (0, context_package_core_js_1.injectL2Modules)(client, hits, state, budget, "continue");
57
+ await (0, context_package_core_js_1.injectL3SkillsAndPins)(client, query, options, snapshotNodes, state, budget);
58
+ await (0, context_package_core_js_1.injectDialogueTurns)(client, query, options, state, budget);
59
+ await (0, context_package_core_js_1.injectNeighborExpansion)(client, options, state, budget);
60
+ return (0, context_package_core_js_1.toLayeredPackage)(state, budget);
433
61
  }
434
62
  function createContextRefillManager(client, maxTokens, options) {
435
63
  const seenAnchors = new Set();
@@ -451,7 +79,7 @@ function createContextRefillManager(client, maxTokens, options) {
451
79
  continue;
452
80
  }
453
81
  const line = `${hit.id} | ${hit.type}: ${hit.content}`;
454
- const estimate = (0, context_slicer_utils_js_2.estimateTokens)(line);
82
+ const estimate = (0, context_slicer_utils_js_1.estimateTokens)(line);
455
83
  if (tokens + estimate > maxTokens) {
456
84
  return items;
457
85
  }
@@ -474,6 +102,8 @@ function createContextRefillManager(client, maxTokens, options) {
474
102
  * 3. Graph compression: connected subgraph + PageRank re-ranking
475
103
  * 4. Layer quotas + token budgeting
476
104
  * 5. Edge expansion
105
+ *
106
+ * Shared recall/pack steps live in `context-package-core` (same as layered).
477
107
  */
478
108
  async function buildEnhancedContextPackage(client, query, task, maxTokens, options) {
479
109
  // Step 0: Adaptive budget estimation.
@@ -487,7 +117,7 @@ async function buildEnhancedContextPackage(client, query, task, maxTokens, optio
487
117
  try {
488
118
  const repoMap = await (0, repo_map_js_1.buildRepoMap)(client);
489
119
  const mapStr = (0, repo_map_js_1.formatRepoMapString)(repoMap);
490
- const tokens = (0, context_slicer_utils_js_2.estimateTokens)(mapStr);
120
+ const tokens = (0, context_slicer_utils_js_1.estimateTokens)(mapStr);
491
121
  if (tokens < maxTokens) {
492
122
  return {
493
123
  summaryChannel: [mapStr],
@@ -502,7 +132,7 @@ async function buildEnhancedContextPackage(client, query, task, maxTokens, optio
502
132
  }
503
133
  }
504
134
  // Step 2: Keyword retrieval (CJK-aware multi-query RRF when workspaceRoot is set).
505
- const keywordHits = await (0, query_expand_js_1.collectExpandedKeywordHits)(client, query, options?.workspaceRoot, options?.englishQuery);
135
+ const keywordHits = await (0, context_package_core_js_1.collectKeywordHits)(client, query, options);
506
136
  let hits = keywordHits;
507
137
  // Step 2b: Symbol-aware boosting — extract camelCase/PascalCase tokens from
508
138
  // the task description and fetch matching symbol nodes to improve recall.
@@ -513,19 +143,9 @@ async function buildEnhancedContextPackage(client, query, task, maxTokens, optio
513
143
  hits = (0, embeddings_js_1.reciprocalRankFusion)([hits, symbolHits]);
514
144
  }
515
145
  }
516
- if (options?.enableVectorRecall === true && options.embeddingProvider) {
517
- try {
518
- const queryEmbedding = await options.embeddingProvider.embed(query);
519
- const topK = options.vectorTopK ?? 8;
520
- const minSim = options.vectorMinSimilarity ?? 0.05;
521
- const vectorCandidates = collectVectorRecallCandidates(client, keywordHits, options.enableFullGraphVectorRecall === true);
522
- const vectorHits = await hnswVectorRecall(vectorCandidates, queryEmbedding, topK, minSim, options?.hnswIndexPath);
523
- hits = (0, embeddings_js_1.reciprocalRankFusion)([keywordHits, vectorHits]);
524
- }
525
- catch (error) {
526
- logger_js_1.logger.warn({ error }, "Vector recall failed in buildEnhancedContextPackage");
527
- hits = keywordHits;
528
- }
146
+ const fused = await (0, context_package_core_js_1.fuseVectorRecallIfEnabled)(client, query, keywordHits, options, "buildEnhancedContextPackage");
147
+ if (fused) {
148
+ hits = fused;
529
149
  }
530
150
  if (options?.enableGraphCompression && hits.length > 0) {
531
151
  try {
@@ -544,217 +164,15 @@ async function buildEnhancedContextPackage(client, query, task, maxTokens, optio
544
164
  }
545
165
  }
546
166
  const snapshotNodes = client.readSnapshot?.()?.nodes;
547
- hits = (0, hit_diversify_js_1.prepareHitsForPackaging)(hits, {
548
- query,
549
- ...(options?.englishQuery !== undefined ? { englishQuery: options.englishQuery } : {}),
550
- ...(snapshotNodes !== undefined ? { allNodes: snapshotNodes } : {}),
551
- });
552
- // Step 4: Build layered package with anchor cap.
167
+ hits = (0, context_package_core_js_1.preparePackageHits)(hits, query, options, snapshotNodes);
553
168
  const maxAnchors = options?.maxAnchors ?? 15;
554
- const summaryChannel = [];
555
- const anchorChannel = [];
556
- let tokens = 0;
557
- let truncated = false;
558
- const quota = {
559
- l1: options?.layerQuota?.l1 ?? Number.POSITIVE_INFINITY,
560
- l2: options?.layerQuota?.l2 ?? Number.POSITIVE_INFINITY,
561
- l3: options?.layerQuota?.l3 ?? Number.POSITIVE_INFINITY,
562
- };
563
- const used = { l1: 0, l2: 0, l3: 0 };
564
- const added = new Set();
565
- const hitById = new Map();
566
- for (const hit of hits)
567
- hitById.set(hit.id, hit);
568
- for (const hit of hits) {
569
- if (anchorChannel.length >= maxAnchors) {
570
- truncated = true;
571
- break;
572
- }
573
- const layer = (0, context_slicer_utils_js_2.classifyLayer)(hit);
574
- if (!(0, context_slicer_utils_js_2.canUseLayer)(layer, quota, used))
575
- continue;
576
- const summary = `${hit.type}: ${hit.content}`;
577
- const estimate = (0, context_slicer_utils_js_2.estimateTokens)(summary);
578
- if (tokens + estimate > maxTokens) {
579
- truncated = true;
580
- break;
581
- }
582
- summaryChannel.push(summary);
583
- anchorChannel.push({ id: hit.id, type: hit.type, layer });
584
- added.add(hit.id);
585
- tokens += estimate;
586
- (0, context_slicer_utils_js_2.markLayerUsed)(layer, used);
587
- }
588
- // Step 4b: L2 module injection — aggregate file/symbol hits into module overviews.
589
- if (!truncated && anchorChannel.length < maxAnchors) {
590
- const moduleIds = new Set();
591
- for (const anchor of anchorChannel) {
592
- if (anchor.layer !== "L1" || (anchor.type !== "File" && anchor.type !== "Symbol"))
593
- continue;
594
- const moduleId = (0, context_slicer_utils_js_2.deriveModuleId)(anchor, hitById.get(anchor.id));
595
- if (moduleId)
596
- moduleIds.add(moduleId);
597
- }
598
- let moduleNodes = [];
599
- if (moduleIds.size > 0 && typeof client.getNodesByIds === "function") {
600
- moduleNodes = await client.getNodesByIds(Array.from(moduleIds));
601
- }
602
- for (const moduleId of moduleIds) {
603
- if (added.has(moduleId) || anchorChannel.length >= maxAnchors)
604
- break;
605
- if (!(0, context_slicer_utils_js_2.canUseLayer)("L2", quota, used))
606
- break;
607
- const node = moduleNodes.find((n) => n.id === moduleId) ??
608
- ({ id: moduleId, type: "Module", content: moduleId.slice("module:".length) });
609
- const summary = `${node.type}: ${node.content}`;
610
- const estimate = (0, context_slicer_utils_js_2.estimateTokens)(summary);
611
- if (tokens + estimate > maxTokens) {
612
- truncated = true;
613
- break;
614
- }
615
- summaryChannel.push(summary);
616
- anchorChannel.push({ id: node.id, type: node.type, layer: "L2" });
617
- added.add(node.id);
618
- tokens += estimate;
619
- (0, context_slicer_utils_js_2.markLayerUsed)("L2", used);
620
- }
621
- }
622
- // Step 4c: L3 skill/decision injection — always-on when enableAlwaysOnLayers,
623
- // otherwise only for architecture queries. Pin goal/alignment/deviation
624
- // Decision nodes first so budget truncation cannot drop them.
625
- const archIntent = context_slicer_types_js_2.ARCHITECTURE_QUERY.test((0, graph_utils_js_1.composeContextQuery)(query, options?.englishQuery));
626
- if (anchorChannel.length < maxAnchors && (options?.enableAlwaysOnLayers || archIntent)) {
627
- const l3Candidates = (0, graph_utils_js_1.rankNodesForContextQuery)((await client.queryByKeyword(query)).filter((n) => n.type === "Skill" || n.type === "Decision"), query, {
628
- scoreTokens: (0, graph_utils_js_1.buildSearchScoreTokens)(query, options?.englishQuery),
629
- matchQueries: options?.englishQuery?.trim()
630
- ? [query, options.englishQuery.trim()]
631
- : [query],
632
- ...(options?.englishQuery !== undefined ? { englishQuery: options.englishQuery } : {}),
633
- });
634
- const acc = { tokens, truncated };
635
- const packCtx = {
636
- summaryChannel,
637
- anchorChannel,
638
- added,
639
- quota,
640
- used,
641
- maxTokens,
642
- maxAnchors,
643
- };
644
- const pins = collectPinnedL3Nodes(l3Candidates, snapshotNodes, added);
645
- for (const node of pins) {
646
- if (tryAppendL3Node(node, packCtx, acc) === "budget")
647
- break;
648
- }
649
- tokens = acc.tokens;
650
- truncated = acc.truncated;
651
- if (!truncated) {
652
- let l3Added = pins.filter((n) => added.has(n.id)).length;
653
- for (const node of l3Candidates) {
654
- if (l3Added >= 2 || anchorChannel.length >= maxAnchors)
655
- break;
656
- if (isPinnedL3Node(node))
657
- continue;
658
- const result = tryAppendL3Node(node, packCtx, acc);
659
- if (result === "budget")
660
- break;
661
- if (result === "packed")
662
- l3Added += 1;
663
- }
664
- tokens = acc.tokens;
665
- truncated = acc.truncated;
666
- }
667
- }
668
- // Step 5: Import-based multi-hop expansion + same-file symbol boosting.
669
- // Find files that import the anchored files, AND add symbols from anchored
670
- // files to improve symbol-level recall.
671
- if (!truncated && anchorChannel.length < maxAnchors) {
672
- const seedFilePaths = [];
673
- for (const anchor of anchorChannel) {
674
- if (anchor.type === "File" && anchor.id.startsWith("file:")) {
675
- seedFilePaths.push(anchor.id.slice("file:".length));
676
- }
677
- else if (anchor.type === "Symbol") {
678
- const fp = (0, context_slicer_utils_js_2.extractFileFromSymbolId)(anchor.id);
679
- if (fp)
680
- seedFilePaths.push(fp);
681
- }
682
- }
683
- // 5a: Same-file symbol boosting — add symbols from anchored files
684
- if (seedFilePaths.length > 0 && snapshotNodes) {
685
- for (const node of snapshotNodes) {
686
- if (added.has(node.id) || anchorChannel.length >= maxAnchors)
687
- continue;
688
- if (node.type !== "Symbol")
689
- continue;
690
- const nodeFile = (0, context_slicer_utils_js_2.extractFileFromSymbolId)(node.id);
691
- if (!nodeFile)
692
- continue;
693
- const inSeedFile = seedFilePaths.some(sp => nodeFile.includes(sp.replace(/\.(ts|tsx|js|jsx|py|go|rs)$/, "")) ||
694
- sp.replace(/\.(ts|tsx|js|jsx|py|go|rs)$/, "").includes(nodeFile.replace(/\.(ts|tsx|js|jsx|py|go|rs)$/, "")));
695
- if (!inSeedFile)
696
- continue;
697
- const layer = (0, context_slicer_utils_js_2.classifyLayer)(node);
698
- const summary = `${node.type}: ${node.content}`;
699
- const estimate = (0, context_slicer_utils_js_2.estimateTokens)(summary);
700
- if (tokens + estimate > maxTokens) {
701
- truncated = true;
702
- break;
703
- }
704
- summaryChannel.push(summary);
705
- anchorChannel.push({ id: node.id, type: node.type, layer });
706
- added.add(node.id);
707
- tokens += estimate;
708
- (0, context_slicer_utils_js_2.markLayerUsed)(layer, used);
709
- }
710
- }
711
- // 5b: Import-based expansion
712
- if (seedFilePaths.length > 0 && anchorChannel.length < maxAnchors) {
713
- const importHits = await (0, symbol_extract_js_1.collectImportRelatedFiles)(client, seedFilePaths, 5);
714
- for (const node of importHits) {
715
- if (added.has(node.id) || anchorChannel.length >= maxAnchors)
716
- continue;
717
- const layer = (0, context_slicer_utils_js_2.classifyLayer)(node);
718
- const summary = `${node.type}: ${node.content}`;
719
- const estimate = (0, context_slicer_utils_js_2.estimateTokens)(summary);
720
- if (tokens + estimate > maxTokens) {
721
- truncated = true;
722
- break;
723
- }
724
- summaryChannel.push(summary);
725
- anchorChannel.push({ id: node.id, type: node.type, layer });
726
- added.add(node.id);
727
- tokens += estimate;
728
- (0, context_slicer_utils_js_2.markLayerUsed)(layer, used);
729
- }
730
- }
731
- }
732
- // Step 6: Edge expansion (respects anchor cap).
733
- const enableExpansion = options?.enableEdgeExpansion !== false;
734
- if (enableExpansion && !truncated && anchorChannel.length < maxAnchors && typeof client.getNeighbors === "function") {
735
- const seedIds = anchorChannel.slice(0, 5).map((a) => a.id);
736
- if (seedIds.length > 0) {
737
- const expanded = await expandSubgraph(client, seedIds, { hops: 1 });
738
- for (const node of expanded) {
739
- if (added.has(node.id) || anchorChannel.length >= maxAnchors)
740
- continue;
741
- const layer = (0, context_slicer_utils_js_2.classifyLayer)(node);
742
- if (!(0, context_slicer_utils_js_2.canUseLayer)(layer, quota, used))
743
- continue;
744
- const summary = `${node.type}: ${node.content}`;
745
- const estimate = (0, context_slicer_utils_js_2.estimateTokens)(summary);
746
- if (tokens + estimate > maxTokens) {
747
- truncated = true;
748
- break;
749
- }
750
- summaryChannel.push(summary);
751
- anchorChannel.push({ id: node.id, type: node.type, layer });
752
- added.add(node.id);
753
- tokens += estimate;
754
- (0, context_slicer_utils_js_2.markLayerUsed)(layer, used);
755
- }
756
- }
757
- }
758
- return { summaryChannel, anchorChannel, tokenEstimate: tokens, truncated };
169
+ const state = (0, context_package_core_js_1.createPackState)(maxTokens, options, maxAnchors);
170
+ const budget = { tokens: 0, truncated: false };
171
+ (0, context_package_core_js_1.packPrimaryHits)(hits, state, budget);
172
+ await (0, context_package_core_js_1.injectL2Modules)(client, hits, state, budget, "break");
173
+ await (0, context_package_core_js_1.injectL3SkillsAndPins)(client, query, options, snapshotNodes, state, budget);
174
+ await (0, context_package_core_js_1.injectSameFileAndImportExpansion)(client, snapshotNodes, state, budget);
175
+ await (0, context_package_core_js_1.injectNeighborExpansion)(client, options, state, budget);
176
+ return (0, context_package_core_js_1.toLayeredPackage)(state, budget);
759
177
  }
760
178
  //# sourceMappingURL=context-slicer.js.map