@devflow-tools/context-engine 0.17.8 → 0.18.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.
Files changed (99) hide show
  1. package/LICENSE +21 -0
  2. package/dist/code-evidence-producer.d.ts +29 -0
  3. package/dist/code-evidence-producer.d.ts.map +1 -0
  4. package/dist/code-evidence-producer.js +522 -0
  5. package/dist/code-evidence-producer.js.map +1 -0
  6. package/dist/code-evidence-types.d.ts +26 -0
  7. package/dist/code-evidence-types.d.ts.map +1 -0
  8. package/dist/code-evidence-types.js +2 -0
  9. package/dist/code-evidence-types.js.map +1 -0
  10. package/dist/compressor.d.ts.map +1 -1
  11. package/dist/compressor.js +2 -3
  12. package/dist/compressor.js.map +1 -1
  13. package/dist/context-budget-allocator.d.ts +5 -1
  14. package/dist/context-budget-allocator.d.ts.map +1 -1
  15. package/dist/context-budget-allocator.js +25 -3
  16. package/dist/context-budget-allocator.js.map +1 -1
  17. package/dist/context-engine.d.ts +19 -43
  18. package/dist/context-engine.d.ts.map +1 -1
  19. package/dist/context-engine.js +203 -1702
  20. package/dist/context-engine.js.map +1 -1
  21. package/dist/create-engines.d.ts +15 -19
  22. package/dist/create-engines.d.ts.map +1 -1
  23. package/dist/create-engines.js +39 -106
  24. package/dist/create-engines.js.map +1 -1
  25. package/dist/index.d.ts +7 -22
  26. package/dist/index.d.ts.map +1 -1
  27. package/dist/index.js +5 -15
  28. package/dist/index.js.map +1 -1
  29. package/dist/symbol-embedder.d.ts +1 -1
  30. package/dist/symbol-embedder.d.ts.map +1 -1
  31. package/dist/symbol-embedder.js +17 -12
  32. package/dist/symbol-embedder.js.map +1 -1
  33. package/dist/symbol-embedding-document.d.ts +4 -0
  34. package/dist/symbol-embedding-document.d.ts.map +1 -0
  35. package/dist/symbol-embedding-document.js +39 -0
  36. package/dist/symbol-embedding-document.js.map +1 -0
  37. package/dist/symbol-vector-store.d.ts +13 -1
  38. package/dist/symbol-vector-store.d.ts.map +1 -1
  39. package/dist/symbol-vector-store.js +45 -16
  40. package/dist/symbol-vector-store.js.map +1 -1
  41. package/dist/types.d.ts +1 -0
  42. package/dist/types.d.ts.map +1 -1
  43. package/package.json +12 -9
  44. package/dist/artifact-inventory.d.ts +0 -39
  45. package/dist/artifact-inventory.d.ts.map +0 -1
  46. package/dist/artifact-inventory.js +0 -224
  47. package/dist/artifact-inventory.js.map +0 -1
  48. package/dist/channel-query-planner.d.ts +0 -9
  49. package/dist/channel-query-planner.d.ts.map +0 -1
  50. package/dist/channel-query-planner.js +0 -92
  51. package/dist/channel-query-planner.js.map +0 -1
  52. package/dist/intent-parser.d.ts +0 -15
  53. package/dist/intent-parser.d.ts.map +0 -1
  54. package/dist/intent-parser.js +0 -388
  55. package/dist/intent-parser.js.map +0 -1
  56. package/dist/retrieval-gap-detector.d.ts +0 -10
  57. package/dist/retrieval-gap-detector.d.ts.map +0 -1
  58. package/dist/retrieval-gap-detector.js +0 -85
  59. package/dist/retrieval-gap-detector.js.map +0 -1
  60. package/dist/retrieval-handlers.d.ts +0 -73
  61. package/dist/retrieval-handlers.d.ts.map +0 -1
  62. package/dist/retrieval-handlers.js +0 -289
  63. package/dist/retrieval-handlers.js.map +0 -1
  64. package/dist/retrieval-ledger.d.ts +0 -3
  65. package/dist/retrieval-ledger.d.ts.map +0 -1
  66. package/dist/retrieval-ledger.js +0 -156
  67. package/dist/retrieval-ledger.js.map +0 -1
  68. package/dist/retrieval-session-controller.d.ts +0 -6
  69. package/dist/retrieval-session-controller.d.ts.map +0 -1
  70. package/dist/retrieval-session-controller.js +0 -372
  71. package/dist/retrieval-session-controller.js.map +0 -1
  72. package/dist/retrieval-task-plan.d.ts +0 -28
  73. package/dist/retrieval-task-plan.d.ts.map +0 -1
  74. package/dist/retrieval-task-plan.js +0 -232
  75. package/dist/retrieval-task-plan.js.map +0 -1
  76. package/dist/semantic-authority-merger.d.ts +0 -11
  77. package/dist/semantic-authority-merger.d.ts.map +0 -1
  78. package/dist/semantic-authority-merger.js +0 -176
  79. package/dist/semantic-authority-merger.js.map +0 -1
  80. package/dist/semantic-resolver.d.ts +0 -19
  81. package/dist/semantic-resolver.d.ts.map +0 -1
  82. package/dist/semantic-resolver.js +0 -49
  83. package/dist/semantic-resolver.js.map +0 -1
  84. package/dist/semantic-route-catalog.d.ts +0 -22
  85. package/dist/semantic-route-catalog.d.ts.map +0 -1
  86. package/dist/semantic-route-catalog.js +0 -53
  87. package/dist/semantic-route-catalog.js.map +0 -1
  88. package/dist/semantic-router.d.ts +0 -39
  89. package/dist/semantic-router.d.ts.map +0 -1
  90. package/dist/semantic-router.js +0 -153
  91. package/dist/semantic-router.js.map +0 -1
  92. package/dist/semantic-structural-evidence.d.ts +0 -20
  93. package/dist/semantic-structural-evidence.d.ts.map +0 -1
  94. package/dist/semantic-structural-evidence.js +0 -84
  95. package/dist/semantic-structural-evidence.js.map +0 -1
  96. package/dist/task-retrieval-orchestrator.d.ts +0 -46
  97. package/dist/task-retrieval-orchestrator.d.ts.map +0 -1
  98. package/dist/task-retrieval-orchestrator.js +0 -418
  99. package/dist/task-retrieval-orchestrator.js.map +0 -1
@@ -1,1741 +1,242 @@
1
- import CGModule from "@colbymchenry/codegraph";
2
- import { parseIntent, expandQuery } from "./intent-parser.js";
3
- import { buildRetrievalTaskPlan } from './retrieval-task-plan.js';
4
- import { fuzzySearch, shouldFallback } from "./fuzzy-fallback.js";
5
- import { compressToTokenLimit } from "./compressor.js";
6
- import { claudeRerank } from "./claude-rerank.js";
7
- import { invalidateContextPrefetchCache, QueryCache } from "./query-cache.js";
8
- import { rankOptionalContextFiles, } from "./ranker.js";
9
- import { describeLayerEvidence, detectLanguageFromPath, } from "./layer-rules.js";
10
- import { reachableCodeGraphNodeIds, sanitizeCodeGraphEdges, } from './graph-edge-quality.js';
11
- import { assembleContextFileRoles } from './context-file-roles.js';
12
- import { buildArtifactInventory } from './artifact-inventory.js';
13
- import { buildTaskContextEnvelope } from './task-retrieval-orchestrator.js';
14
- import { recordTaskEnvelopeLedger } from './retrieval-ledger.js';
15
- import { selectGraphEvidence } from './graph-evidence-selector.js';
1
+ import { CodeEvidenceRuntime } from './code-evidence-producer.js';
16
2
  import { allocateContextFileBudget } from './context-budget-allocator.js';
17
- import { emitSemanticControlLog } from '@devflow-tools/telemetry';
18
- const CG = CGModule.default ?? CGModule;
19
- const SYMBOL_VECTOR_WARNING = "Symbol vector search is not ready; using CodeGraph/BM25 results. Run `devflow doctor` for details.";
20
- // ── RRF (Reciprocal Rank Fusion) ─────────────────────────────────
21
- const RRF_K = 60;
22
- function rrfFusion(fts5Nodes, vectorResults, cg) {
23
- const scores = new Map();
24
- const merged = new Map(fts5Nodes);
25
- // FTS5 rank signals
26
- let rank = 1;
27
- for (const id of fts5Nodes.keys()) {
28
- scores.set(id, 0.5 / (RRF_K + rank));
29
- rank++;
30
- }
31
- // Vector rank signals + merge
32
- rank = 1;
33
- for (const { id } of vectorResults) {
34
- const existing = scores.get(id) ?? 0;
35
- scores.set(id, existing + 0.5 / (RRF_K + rank));
36
- rank++;
37
- if (!merged.has(id)) {
38
- const node = cg.getNode(id);
39
- if (node)
40
- merged.set(id, node);
41
- }
42
- }
43
- // Sort by score descending
44
- const sorted = [...scores.entries()].sort((a, b) => b[1] - a[1]);
45
- const result = new Map();
46
- for (const [id] of sorted) {
47
- const node = merged.get(id);
48
- if (node)
49
- result.set(id, node);
50
- }
51
- return result;
52
- }
53
3
  /**
54
- * Merge the original query with L1 dict expansion and Agent-provided English terms.
55
- * Deduplicates terms before sending to codegraph.
4
+ * Code-only facade for callers outside canonical cross-channel retrieval.
5
+ *
6
+ * Cross-channel retrieval is owned by retrieval-engine. This class does not
7
+ * classify prompts, read Memory/Knowledge, persist runtime state, or assemble
8
+ * composite receipts. New task-aware callers use CodeEvidenceProducer.
56
9
  */
57
- function mergeQueries(original, l1, agentTerms) {
58
- const terms = new Set();
59
- for (const q of [original, l1, agentTerms]) {
60
- if (!q)
61
- continue;
62
- for (const w of q.split(/\s+/)) {
63
- if (w.length >= 2)
64
- terms.add(w.toLowerCase());
65
- }
66
- }
67
- return [...terms].join(" ");
68
- }
69
- function sigmoid(x, midpoint = 0.5, steepness = 6) {
70
- return 1 / (1 + Math.exp(-steepness * (x - midpoint)));
71
- }
72
- function calculateConfidence(input) {
73
- // CodeGraph score: based on provided confidence, node count, and structural signals
74
- let cgScore = typeof input.cgConfidence === "number" ? input.cgConfidence : 0;
75
- if (cgScore === 0 && input.nodeCount > 0) {
76
- // Derive from node count when explicit confidence unavailable
77
- cgScore = sigmoid(Math.min(input.nodeCount, 30) / 30, 0.3, 8);
78
- }
79
- // Bonus for structural richness
80
- if (input.hasEdges)
81
- cgScore = Math.min(1, cgScore + 0.1);
82
- if (input.hasExactMatches)
83
- cgScore = Math.min(1, cgScore + 0.15);
84
- // Knowledge score: diminishing returns after 5 hits
85
- const knowledgeScore = sigmoid(input.knowledgeHitCount / 10, 0.3, 5);
86
- // Memory score: diminishing returns
87
- const memoryScore = sigmoid(input.memoryHitCount / 10, 0.25, 5);
88
- // Node count score
89
- const nodeScore = sigmoid(input.nodeCount / 30, 0.3, 5);
90
- // Intent confidence
91
- const intentScore = input.intentConfidence;
92
- const weighted = cgScore * 0.35 +
93
- knowledgeScore * 0.25 +
94
- memoryScore * 0.2 +
95
- nodeScore * 0.1 +
96
- intentScore * 0.1;
97
- let confidence = weighted;
98
- if (typeof input.targetPrecision === 'number') {
99
- confidence *= 0.55 + 0.45 * Math.max(0, Math.min(1, input.targetPrecision));
100
- }
101
- // Diversity bonus: all five signals present
102
- const activeSignals = [cgScore > 0, input.knowledgeHitCount > 0, input.memoryHitCount > 0, input.nodeCount > 0, input.intentConfidence > 0].filter(Boolean).length;
103
- if (activeSignals >= 4)
104
- confidence += 0.05;
105
- // Penalty: very low node count with no other signals
106
- if (input.nodeCount < 3 && input.knowledgeHitCount === 0 && input.memoryHitCount === 0) {
107
- confidence *= 0.5;
108
- }
109
- return Math.round(Math.min(1, Math.max(0, confidence)) * 100) / 100;
110
- }
111
- function getAdaptiveParams(fileCount) {
112
- if (fileCount <= 200)
113
- return { depth: 1, searchLimit: 5, maxNodes: 10 };
114
- if (fileCount <= 1000)
115
- return { depth: 2, searchLimit: 10, maxNodes: 20 };
116
- return { depth: 3, searchLimit: 15, maxNodes: 30 };
117
- }
118
- const RISK_PATTERNS = [
119
- { pattern: /(main|index|app|entry|bootstrap)\.(ts|tsx|js|jsx)$/i, level: "high", category: "main_flow" },
120
- { pattern: /(config|settings?|env|constants?)\.(ts|tsx|js|jsx|json)$/i, level: "medium", category: "config" },
121
- { pattern: /(store|state|context|provider|redux)\.(ts|tsx|js|jsx)$/i, level: "high", category: "global_state" },
122
- { pattern: /(api|route|controller|handler|service|middleware)\.(ts|tsx|js|jsx)$/i, level: "medium", category: "api_contract" },
123
- ];
124
- function buildGraphEvidence(nodes, rootIds, edges, _cg) {
125
- const nodeIds = new Set(nodes.map(node => node.id));
126
- const adjacency = new Map();
127
- const reasons = new Map();
128
- const connect = (source, target, reason) => {
129
- if (!nodeIds.has(source) || !nodeIds.has(target))
130
- return;
131
- const targets = adjacency.get(source) ?? new Set();
132
- targets.add(target);
133
- adjacency.set(source, targets);
134
- const sourceReasons = reasons.get(target) ?? [];
135
- if (!sourceReasons.includes(reason))
136
- sourceReasons.push(reason);
137
- reasons.set(target, sourceReasons);
138
- };
139
- for (const edge of edges)
140
- connect(edge.source, edge.target, `${edge.source} -> ${edge.target} graph edge`);
141
- const distances = new Map();
142
- const queue = [];
143
- for (const root of rootIds) {
144
- if (!nodeIds.has(root))
145
- continue;
146
- distances.set(root, 0);
147
- queue.push(root);
148
- }
149
- while (queue.length > 0) {
150
- const current = queue.shift();
151
- const distance = distances.get(current);
152
- for (const target of adjacency.get(current) ?? []) {
153
- if (!distances.has(target)) {
154
- distances.set(target, distance + 1);
155
- queue.push(target);
156
- }
157
- }
158
- for (const [source, targets] of adjacency) {
159
- if (targets.has(current) && !distances.has(source)) {
160
- distances.set(source, distance + 1);
161
- queue.push(source);
162
- }
163
- }
164
- }
165
- for (const root of rootIds)
166
- reasons.set(root, ["matched query symbol"]);
167
- return { distances, reasons, roots: rootIds };
168
- }
169
- function generateRiskHints(nodes, graphEvidence) {
170
- const hints = [];
171
- const seen = new Set();
172
- for (const node of nodes) {
173
- const distance = graphEvidence.distances.get(node.id);
174
- if (distance === undefined)
175
- continue;
176
- for (const { pattern, level, category } of RISK_PATTERNS) {
177
- if (pattern.test(node.filePath)) {
178
- const key = `${category}:${node.filePath}`;
179
- if (seen.has(key))
180
- continue;
181
- seen.add(key);
182
- const existing = hints.find(h => h.category === category && h.level === level);
183
- if (existing) {
184
- if (!existing.affectedFiles.includes(node.filePath)) {
185
- existing.affectedFiles.push(node.filePath);
186
- }
187
- }
188
- else {
189
- hints.push({
190
- level,
191
- category,
192
- description: `${category.replace(/_/g, " ")} file: ${node.filePath}; ${distance === 0 ? "matched target" : `reachable in ${distance} graph hop${distance === 1 ? "" : "s"}`}`,
193
- affectedFiles: [node.filePath],
194
- score: distance === 0 ? 1 : Math.max(0.35, 1 - distance * 0.2),
195
- evidence: graphEvidence.reasons.get(node.id) ?? [],
196
- });
197
- }
198
- }
199
- }
200
- }
201
- // Add cross-layer hint if files span multiple directory levels
202
- const reachableNodes = nodes.filter(node => graphEvidence.distances.has(node.id));
203
- const layers = new Set(reachableNodes.map(n => n.filePath.split("/")[0]));
204
- if (layers.size >= 3) {
205
- hints.push({
206
- level: "medium",
207
- category: "cross_layer",
208
- description: `Changes span ${layers.size} top-level directories`,
209
- affectedFiles: reachableNodes.slice(0, 5).map(n => n.filePath),
210
- score: Math.min(1, 0.4 + layers.size * 0.1),
211
- evidence: ["reachable files span multiple top-level directories"],
212
- });
213
- }
214
- return hints.slice(0, 10);
215
- }
216
- const TASK_WORKFLOW_MAP = {
217
- debug: ["debug"],
218
- feature: ["feature"],
219
- refactor: ["review"],
220
- review: ["review"],
221
- understand: ["scan"],
222
- unknown: ["scan"],
223
- };
224
- function generateNextActions(taskType, topSymbols) {
225
- const actions = [];
226
- const workflows = TASK_WORKFLOW_MAP[taskType] ?? ["scan"];
227
- for (const w of workflows) {
228
- actions.push({ kind: "workflow", label: `Run ${w} workflow`, payload: w });
229
- }
230
- if (topSymbols.length > 0) {
231
- actions.push({
232
- kind: "graph_query",
233
- label: `Explore ${topSymbols[0].name}`,
234
- payload: topSymbols[0].name,
235
- });
236
- }
237
- actions.push({
238
- kind: "symbol_search",
239
- label: "Search related symbols",
240
- payload: topSymbols.slice(0, 3).map(s => s.name).join(" "),
241
- });
242
- return actions;
243
- }
244
- function extractTargetAnchors(query) {
245
- const pathAnchors = new Set();
246
- for (const match of query.matchAll(/(?:^|\s)([a-zA-Z0-9_.-]+(?:\/[a-zA-Z0-9_.-]+)+)/g)) {
247
- if (match[1])
248
- pathAnchors.add(match[1].replace(/^\.\//, '').replace(/\/+$/, '').toLowerCase());
249
- }
250
- if (pathAnchors.size > 0) {
251
- const directories = [...pathAnchors]
252
- .filter(anchor => /\.[a-z0-9]+$/iu.test(anchor))
253
- .map(anchor => anchor.split('/').slice(0, -1).join('/'))
254
- .filter(Boolean);
255
- const existingNames = new Set([...pathAnchors].map(anchor => anchor.split('/').at(-1)));
256
- for (const match of query.matchAll(/\b([A-Za-z0-9_.-]+\.(?:[cm]?[jt]sx?|vue|svelte|json|ya?ml|md))\b/gu)) {
257
- const fileName = match[1]?.toLowerCase();
258
- if (!fileName || existingNames.has(fileName))
259
- continue;
260
- if (directories.length === 1)
261
- pathAnchors.add(`${directories[0]}/${fileName}`);
262
- }
263
- return [...pathAnchors];
264
- }
265
- const anchors = new Set();
266
- for (const match of query.matchAll(/([a-zA-Z0-9]+(?:-[a-zA-Z0-9]+)+)\s+(?:component|module|file|组件|模块|文件)/gi)) {
267
- if (match[1])
268
- anchors.add(match[1].toLowerCase());
269
- }
270
- return [...anchors];
271
- }
272
- function fileMatchesTargetAnchor(filePath, anchors) {
273
- const normalized = filePath.replace(/\\/g, '/').toLowerCase();
274
- return anchors.some(anchor => normalized === anchor
275
- || normalized.startsWith(`${anchor}/`)
276
- || normalized.includes(`/${anchor}/`)
277
- || normalized.includes(anchor));
278
- }
279
10
  export class ContextEngine {
280
- cg;
281
- data;
282
- graph;
283
- indexer;
284
- memory;
285
- knowledge;
286
- embedder;
11
+ dependencies;
287
12
  symbolStore;
288
- symbolEmbedder;
289
- constructor(deps) {
290
- this.cg = deps.cg;
291
- this.data = deps.data;
292
- this.graph = deps.graph;
293
- this.indexer = deps.indexer;
294
- this.memory = deps.memory;
295
- this.knowledge = deps.knowledge;
296
- this.embedder = deps.embedder;
297
- this.symbolStore = deps.symbolStore;
298
- this.symbolEmbedder = deps.symbolEmbedder;
13
+ runtime;
14
+ constructor(dependencies) {
15
+ this.dependencies = dependencies;
16
+ this.symbolStore = dependencies.symbolStore;
17
+ this.runtime = new CodeEvidenceRuntime({
18
+ codegraph: dependencies.cg,
19
+ indexer: dependencies.indexer,
20
+ ...(dependencies.embedder ? { embedder: dependencies.embedder } : {}),
21
+ ...(dependencies.symbolStore ? { symbolStore: dependencies.symbolStore } : {}),
22
+ });
299
23
  }
300
24
  close() {
301
25
  this.symbolStore?.close();
302
26
  this.symbolStore = undefined;
303
27
  }
304
- queryCache = new QueryCache(60_000, 100);
305
- async recordPrefetchedContextSelection(input) {
306
- await this.memory.recordContextEnvelopeSelection?.(input);
307
- }
308
28
  async index(_projectRoot) {
309
- await this.indexer.reconcile();
29
+ await this.dependencies.indexer.reconcile();
310
30
  }
311
- getKnowledgeSearchOptions(query, limit, options = {}, taskPlan, totalBudget = 8_000) {
312
- const stats = this.cg.getStats();
313
- const frameworks = typeof this.cg.getDetectedFrameworks === "function"
314
- ? this.cg.getDetectedFrameworks()
315
- : [];
316
- const languages = Object.keys(stats.filesByLanguage ?? {});
31
+ getStatus() {
32
+ const stats = this.dependencies.cg.getStats();
317
33
  return {
318
- limit,
319
- retrievalIntent: 'automatic_context',
320
- budgetTokens: Math.max(0, Math.floor(totalBudget * 0.25)),
321
- activeSkill: options.activeSkill,
322
- relevantFiles: options.relevantFiles,
323
- action: taskPlan?.action,
324
- knowledgeNeeds: taskPlan?.knowledgeNeeds,
325
- semanticNeedsResolved: (options.intentArtifact?.version ?? 1) >= 2,
326
- sessionId: options.sessionId,
327
- executionId: options.executionId,
328
- requestId: options.requestId,
329
- turnId: options.turnId,
330
- intentArtifactHash: options.intentArtifact?.artifactHash,
331
- queryPlanHash: options.queryPlan?.planHash,
332
- projectSignals: {
333
- frameworks,
334
- languages,
335
- activeSkill: options.activeSkill,
336
- relevantFiles: options.relevantFiles,
337
- },
34
+ totalFiles: stats.fileCount,
35
+ totalSymbols: stats.nodeCount,
36
+ lastIndexTime: stats.lastUpdated,
37
+ indexing: this.dependencies.cg.isIndexing(),
338
38
  };
339
39
  }
340
- async searchKnowledgeWithStatus(query, options) {
341
- const knowledge = this.knowledge;
342
- if (typeof knowledge.searchWithStatus === 'function') {
343
- const result = await knowledge.searchWithStatus(query, options);
344
- return {
345
- entries: result.entries,
346
- warnings: result.warnings ?? [],
347
- rejectedLowScore: result.rejectedLowScore,
348
- rejectedIncompatibleSources: result.rejectedIncompatibleSources,
349
- retrievalDiagnostics: result.retrievalDiagnostics,
350
- };
351
- }
352
- return { entries: await knowledge.search(query, options), warnings: [] };
353
- }
354
- getFileLanguage(filePath) {
355
- const file = this.cg.getFile?.(filePath);
356
- return typeof file?.language === "string" ? file.language : detectLanguageFromPath(filePath);
357
- }
358
40
  async build(request) {
359
- const warnings = this.scheduleSymbolVectorBuild();
360
- const maxTokens = request.maxTokens ?? 8000;
361
- const intent = parseIntent(request.query);
362
- const retrievalTaskPlan = buildRetrievalTaskPlan({
363
- query: request.query,
364
- parsedIntent: intent,
365
- targetAnchors: request.relevantFiles,
366
- activeSkill: request.activeSkill,
367
- });
368
- const stats = this.cg.getStats();
369
- const adaptive = getAdaptiveParams(stats.fileCount ?? 500);
370
- const depth = request.mode === "deep" ? 3 : adaptive.depth;
371
- // Layer 1: ZH_EN_MAP dict expansion (offline, ~5ms)
372
- const l1Query = expandQuery(request.query);
373
- // Merge: original query + L1 dict terms + Agent-provided English terms
374
- const combinedQuery = mergeQueries(request.query, l1Query, request.expandedTerms);
375
- // 1. CodeGraph semantic search + graph traversal
376
- const cgResult = await this.cg.findRelevantContext(combinedQuery, {
377
- searchLimit: adaptive.searchLimit,
378
- traversalDepth: depth,
379
- maxNodes: request.mode === "deep" ? 30 : adaptive.maxNodes,
380
- });
381
- // Layer 3: Fuzzy fallback if primary search returned low confidence or too few results
382
- if (shouldFallback(cgResult.nodes.size, cgResult.confidence)) {
383
- const fallbackTerms = fuzzySearch(this.cg, combinedQuery, 10);
384
- for (const term of fallbackTerms) {
385
- const hits = this.cg.searchNodes(term, { limit: 3 });
386
- for (const hit of hits) {
387
- if (!cgResult.nodes.has(hit.node.id)) {
388
- cgResult.nodes.set(hit.node.id, hit.node);
389
- if (!cgResult.roots.includes(hit.node.id)) {
390
- cgResult.roots.push(hit.node.id);
391
- }
392
- }
393
- }
394
- }
395
- }
396
- const vectorEmbedder = this.embedder;
397
- const symbolStore = this.symbolStore;
398
- // Vector recall: semantic search across all indexed symbols (bypasses FTS5 tokenization)
399
- if (vectorEmbedder
400
- && symbolStore
401
- && this.isSymbolVectorReady(warnings)) {
402
- try {
403
- const queryEmbedding = await vectorEmbedder.embed(combinedQuery);
404
- const vectorResults = symbolStore.search(queryEmbedding, 50);
405
- if (vectorResults.length > 0) {
406
- const fused = rrfFusion(cgResult.nodes, vectorResults, this.cg);
407
- cgResult.nodes = fused;
408
- cgResult.roots = [...fused.keys()].slice(0, 3);
409
- }
410
- }
411
- catch {
412
- this.addSymbolVectorWarning(warnings);
413
- }
414
- }
415
- // Embedding reranking (if available)
416
- if (vectorEmbedder && this.isSymbolVectorReady(warnings)) {
417
- try {
418
- const nodesList = Array.from(cgResult.nodes.values());
419
- const nodeTexts = nodesList.map(n => `${n.name} ${n.signature ?? ''} ${n.kind} ${n.filePath}`);
420
- const queryEmbedding = await vectorEmbedder.embed(combinedQuery);
421
- const nodeEmbeddings = await vectorEmbedder.embedBatch(nodeTexts);
422
- const scoredNodes = nodesList.map((node, i) => {
423
- const nodeEmb = nodeEmbeddings[i];
424
- let dot = 0, normA = 0, normB = 0;
425
- for (let j = 0; j < queryEmbedding.length; j++) {
426
- dot += queryEmbedding[j] * nodeEmb[j];
427
- normA += queryEmbedding[j] * queryEmbedding[j];
428
- normB += nodeEmb[j] * nodeEmb[j];
429
- }
430
- const score = dot / (Math.sqrt(normA) * Math.sqrt(normB));
431
- return { node, score };
432
- });
433
- scoredNodes.sort((a, b) => b.score - a.score);
434
- cgResult.nodes = new Map();
435
- const topK = Math.min(scoredNodes.length, request.mode === "deep" ? 30 : adaptive.maxNodes);
436
- for (const { node } of scoredNodes.slice(0, topK)) {
437
- cgResult.nodes.set(node.id, node);
438
- }
439
- cgResult.roots = scoredNodes.slice(0, 3).map(s => s.node.id);
440
- }
441
- catch {
442
- this.addSymbolVectorWarning(warnings);
443
- }
444
- }
445
- // Claude rerank (second pass, if available)
446
- const allNodesList = Array.from(cgResult.nodes.values());
447
- if (allNodesList.length > 30) {
448
- try {
449
- const reranked = await claudeRerank(combinedQuery, allNodesList.map(n => ({ id: n.id, name: n.name, snippet: n.signature ?? '', file: n.filePath })), { topK: 30 });
450
- cgResult.nodes = new Map(reranked.map(r => [r.id, allNodesList.find(n => n.id === r.id)]));
451
- cgResult.roots = reranked.slice(0, 3).map(r => r.id);
452
- }
453
- catch { /* keep embedding/bm25 ranking */ }
454
- }
455
- // 2. Knowledge + Memory in parallel
456
- const [kResultsRaw, memResultsRaw] = await Promise.all([
457
- this.searchKnowledgeWithStatus(combinedQuery, this.getKnowledgeSearchOptions(combinedQuery, 10, { activeSkill: request.activeSkill, relevantFiles: request.relevantFiles }, retrievalTaskPlan, maxTokens)).catch(() => ({ entries: [], warnings: [] })),
458
- this.memory.search(request.query, {
459
- limit: 5,
460
- purpose: intent.intent,
461
- scope: this.cg.getProjectRoot?.() ?? process.cwd(),
462
- action: retrievalTaskPlan.action,
463
- requiredPolicyFacets: retrievalTaskPlan.requiredPolicyFacets,
464
- }).catch(() => []),
465
- ]);
466
- if (kResultsRaw.warnings.length > 0)
467
- warnings.push(...kResultsRaw.warnings);
468
- const knowledgeHits = kResultsRaw.entries;
469
- const memoryContext = memResultsRaw.length > 0 ? {
470
- relevant: memResultsRaw.map((r) => ({
471
- content: r.entry.content,
472
- category: r.entry.category,
473
- score: r.score,
474
- })),
475
- } : undefined;
476
- // 4. Build file snippets, layer by quality
477
- const MIN_CORE_SCORE = 0.7;
478
- const MIN_WEAK_SCORE = 0.4;
479
- const L1_TOKEN_RATIO = 0.6;
480
- const L2_TOKEN_RATIO = 0.3;
481
- const ESTIMATED_TOKENS_PER_CHAR = 0.25;
482
- function estimateTokens(text) {
483
- return Math.ceil(text.length * ESTIMATED_TOKENS_PER_CHAR);
484
- }
485
- function diversityTrim(items, maxPerDir) {
486
- const dirCount = new Map();
487
- return items.filter(f => {
488
- const dir = f.path.split('/').slice(0, -1).join('/') || f.path;
489
- const count = dirCount.get(dir) ?? 0;
490
- if (count >= maxPerDir)
491
- return false;
492
- dirCount.set(dir, count + 1);
493
- return true;
494
- });
495
- }
496
- // Group by file, classify into L1/L2
497
- const fileMap = new Map();
498
- for (const [id, node] of cgResult.nodes) {
499
- const fp = node.filePath;
500
- const isRoot = cgResult.roots.includes(id);
501
- if (fileMap.has(fp)) {
502
- const existing = fileMap.get(fp);
503
- existing.symbols.push(node.name);
504
- existing.snippetParts.push(`// ${node.kind}: ${node.name}${node.signature ? ` ${node.signature}` : ""}`);
505
- if (isRoot)
506
- existing.bestScore = 1.0;
507
- }
508
- else {
509
- fileMap.set(fp, {
510
- symbols: [node.name],
511
- snippetParts: [`// ${node.kind}: ${node.name}${node.signature ? ` ${node.signature}` : ""}`],
512
- bestScore: isRoot ? 1.0 : 0.5,
513
- });
514
- }
515
- }
516
- const l1Files = [];
517
- const l2Files = [];
518
- for (const [fp, data] of fileMap) {
519
- const snippet = {
520
- path: fp,
521
- snippet: data.snippetParts.join('\n'),
522
- symbols: data.symbols,
523
- score: data.bestScore,
524
- };
525
- if (data.bestScore >= MIN_CORE_SCORE) {
526
- l1Files.push(snippet);
527
- }
528
- else if (data.bestScore >= MIN_WEAK_SCORE) {
529
- l2Files.push(snippet);
530
- }
531
- }
532
- // L3: knowledge hits (max 3)
533
- const l3Files = knowledgeHits.slice(0, 3).map(kh => ({
534
- path: kh.source ?? 'knowledge',
535
- snippet: kh.content ?? '',
536
- symbols: [kh.title ?? 'knowledge'],
537
- score: 0.6,
538
- }));
539
- // Token budget allocation
540
- const allFiles = [];
541
- let remaining = maxTokens;
542
- const l1Cap = Math.floor(maxTokens * L1_TOKEN_RATIO);
543
- const l2Cap = Math.floor(maxTokens * L2_TOKEN_RATIO);
544
- // L1: by score descending
545
- let l1Used = 0;
546
- for (const f of l1Files.sort((a, b) => (b.score ?? 0) - (a.score ?? 0))) {
547
- const tokens = estimateTokens(f.snippet);
548
- if (tokens <= remaining && l1Used + tokens <= l1Cap) {
549
- allFiles.push(f);
550
- remaining -= tokens;
551
- l1Used += tokens;
552
- }
553
- }
554
- // L2: diversity trim, then fill
555
- let l2Used = 0;
556
- for (const f of diversityTrim(l2Files, 2)) {
557
- const tokens = Math.min(estimateTokens(f.snippet), 300);
558
- if (remaining < tokens)
559
- break;
560
- if (l2Used + tokens > l2Cap)
561
- continue;
562
- allFiles.push(f);
563
- remaining -= tokens;
564
- l2Used += tokens;
565
- }
566
- // fallback: if nothing made it in, push top L1
567
- if (allFiles.length === 0 && l1Files.length > 0) {
568
- allFiles.push(l1Files.sort((a, b) => (b.score ?? 0) - (a.score ?? 0))[0]);
569
- }
570
- const tokenCount = allFiles.reduce((s, f) => s + estimateTokens(f.snippet), 0);
571
- // Build graph output from subgraph
572
- let graphOutput;
573
- if (request.includeGraph) {
574
- const graphNodes = [];
575
- for (const n of cgResult.nodes.values()) {
576
- graphNodes.push({
577
- id: n.id,
578
- name: n.name,
579
- type: n.kind,
580
- file: n.filePath,
581
- });
582
- }
583
- graphOutput = {
584
- nodes: graphNodes,
585
- edges: cgResult.edges.map((e) => ({
586
- type: "depends_on",
587
- from: e.source,
588
- to: e.target,
589
- })),
590
- };
591
- }
592
- return {
593
- files: allFiles,
594
- knowledgeFiles: l3Files.length > 0 ? l3Files : undefined,
595
- graph: graphOutput,
596
- memory: memoryContext,
597
- confidence: intent.confidence,
598
- tokenCount,
599
- warnings: warnings.length > 0 ? warnings : undefined,
600
- };
601
- }
602
- async buildTaskContext(query, options) {
603
- const warnings = this.scheduleSymbolVectorBuild();
604
- const maxTokens = options?.maxTokens ?? 8000;
605
- const mode = options?.mode ?? "fast";
606
- const memoryRevision = this.memory.getRetrievalRevision?.() ?? 0;
607
- const cacheKey = `ctx:${query}:${mode}:${options?.activeSkill ?? ""}:${(options?.relevantFiles ?? []).join(",")}:memory:${memoryRevision}`;
608
- let cached = warnings.length === 0 && !options?.requestId
609
- ? this.queryCache.get(cacheKey)
610
- : undefined;
611
- if (cached) {
612
- const changed = typeof this.cg.getChangedFiles === 'function'
613
- ? this.cg.getChangedFiles()
614
- : {};
615
- const changedPaths = [...(changed.added ?? []), ...(changed.modified ?? []), ...(changed.removed ?? [])];
616
- const cachedPaths = [
617
- ...getGroupFilePaths(cached.requiredFiles),
618
- ...getGroupFilePaths(cached.optionalFiles),
619
- ];
620
- const freshness = this.indexer?.ensureFilesCurrent
621
- ? await this.indexer.ensureFilesCurrent(cachedPaths, { refresh: false })
622
- : null;
623
- if (changedPaths.length > 0 || freshness?.files.some(file => !file.current)) {
624
- this.queryCache.invalidatePaths(new Set([...changedPaths, ...(freshness?.stale ?? []), ...(freshness?.missing ?? [])]));
625
- invalidateContextPrefetchCache(this.cg.getProjectRoot?.() ?? process.cwd());
626
- cached = undefined;
627
- }
628
- }
629
- if (cached) {
630
- return {
631
- ...cached,
632
- warnings: warnings.length > 0 ? warnings : undefined,
633
- };
634
- }
635
- const intent = parseIntent(query);
636
- const taskType = intent.intent;
637
- const combinedQuery = options?.queryPlan?.code[0]?.query
638
- ?? mergeQueries(query, expandQuery(query), options?.expandedTerms);
639
- const explicitTargetAnchors = options?.intentArtifact?.targetAnchors ?? extractTargetAnchors(query);
640
- const initialTaskPlan = buildRetrievalTaskPlan({
641
- query,
642
- parsedIntent: intent,
643
- targetAnchors: explicitTargetAnchors,
644
- activeSkill: options?.activeSkill,
645
- command: options?.command,
646
- authoritativeArtifact: options?.intentArtifact,
647
- });
648
- const projectRoot = this.cg.getProjectRoot?.() ?? process.cwd();
649
- const logIdentity = {
650
- projectRoot,
651
- sessionId: options?.sessionId,
652
- turnId: options?.turnId,
653
- requestId: options?.requestId,
654
- executionId: options?.executionId,
655
- intentArtifactHash: options?.intentArtifact?.artifactHash,
656
- queryPlanHash: options?.queryPlan?.planHash,
41
+ const query = {
42
+ query: request.query.slice(0, 2_000),
43
+ targetAnchors: request.relevantFiles ?? [],
44
+ symbols: [],
45
+ relationKinds: ['dependency'],
46
+ maxDepth: request.mode === 'deep' ? 3 : 1,
657
47
  };
658
- let artifactInventory = this.indexer
659
- ? buildArtifactInventory({
660
- codegraph: this.cg,
661
- indexer: this.indexer,
662
- query: combinedQuery,
663
- explicitTargetAnchors,
664
- action: initialTaskPlan.action,
665
- identity: logIdentity,
666
- })
667
- : { entries: [], operationTargets: [], missingExplicitTargets: [], ambiguous: false };
668
- const staleInventoryTargets = artifactInventory.operationTargets
669
- .filter(entry => entry.freshness.exists && !entry.freshness.current)
670
- .map(entry => entry.path);
671
- if (this.indexer && staleInventoryTargets.length > 0) {
672
- await this.indexer.ensureFilesCurrent(staleInventoryTargets, { refresh: true, timeoutMs: 5_000 });
673
- artifactInventory = buildArtifactInventory({
674
- codegraph: this.cg,
675
- indexer: this.indexer,
676
- query: combinedQuery,
677
- explicitTargetAnchors,
678
- action: initialTaskPlan.action,
679
- identity: logIdentity,
680
- });
681
- }
682
- const targetAnchors = [...new Set([
683
- ...explicitTargetAnchors,
684
- ...artifactInventory.operationTargets
685
- .filter(entry => entry.freshness.current)
686
- .map(entry => entry.path),
687
- ])];
688
- const retrievalTaskPlan = buildRetrievalTaskPlan({
48
+ const output = await this.runtime.search({
689
49
  query,
690
- parsedIntent: intent,
691
- targetAnchors,
692
- activeSkill: options?.activeSkill,
693
- command: options?.command,
694
- authoritativeArtifact: options?.intentArtifact,
50
+ projectId: projectId(this.dependencies.cg),
51
+ contextHash: 'diagnostic-code-search',
52
+ tokenBudget: request.maxTokens ?? 8_000,
53
+ now: Date.now(),
695
54
  });
696
- if (artifactInventory.missingExplicitTargets.length > 0) {
697
- warnings.push(`explicit-target-missing:${artifactInventory.missingExplicitTargets.length}`);
698
- }
699
- if (artifactInventory.ambiguous)
700
- warnings.push('artifact-target-ambiguous');
701
- const timing = {};
702
- const t0 = Date.now();
703
- const [cgResult, knowledgeResult, memoryHits] = await Promise.all([
704
- typeof this.cg.buildContext === "function"
705
- ? Promise.race([
706
- this.cg.buildContext(combinedQuery, {
707
- searchLimit: mode === "deep" ? 10 : 5,
708
- traversalDepth: mode === "deep" ? 3 : 2,
709
- maxNodes: mode === "deep" ? 50 : 20,
710
- includeCode: false,
711
- format: "json",
712
- }),
713
- new Promise((_, reject) => setTimeout(() => reject(new Error("buildContext timeout")), 15000)),
714
- ]).catch((err) => {
715
- emitSemanticControlLog({
716
- event: 'context.build.degraded', identity: logIdentity, level: 'warn', timestamp: Date.now(),
717
- data: { stage: 'codegraph_context', reason: err.message, fallback: 'manual_pipeline' },
718
- });
719
- return null;
720
- })
721
- : Promise.resolve(null),
722
- Promise.race([
723
- this.searchKnowledgeWithStatus(combinedQuery, this.getKnowledgeSearchOptions(combinedQuery, 5, options, retrievalTaskPlan, maxTokens)),
724
- new Promise((_, reject) => setTimeout(() => reject(new Error("knowledge timeout")), 5000)),
725
- ]).catch((err) => {
726
- emitSemanticControlLog({
727
- event: 'knowledge.search.degraded', identity: logIdentity, level: 'warn', timestamp: Date.now(),
728
- data: { reason: err.message, fallback: 'knowledge_empty' },
729
- });
730
- return { entries: [], warnings: [`knowledge-search-failed: ${err.message}`] };
731
- }),
732
- typeof this.memory.search === "function"
733
- ? Promise.race([
734
- this.memory.search(query, {
735
- limit: 5,
736
- purpose: taskType,
737
- recordAccess: false,
738
- scope: this.cg.getProjectRoot?.() ?? process.cwd(),
739
- action: retrievalTaskPlan.action,
740
- requiredPolicyFacets: retrievalTaskPlan.requiredPolicyFacets,
741
- }),
742
- new Promise((_, reject) => setTimeout(() => reject(new Error("memory timeout")), 5000)),
743
- ]).catch((err) => {
744
- emitSemanticControlLog({
745
- event: 'memory.search.completed', identity: logIdentity, level: 'warn', timestamp: Date.now(),
746
- data: { status: 'degraded', reason: err.message, selected: 0, fallback: 'memory_empty' },
747
- });
748
- return [];
749
- })
750
- : Promise.resolve([]),
751
- ]);
752
- timing["1_promise_all"] = Date.now() - t0;
753
- const knowledgeHits = knowledgeResult.entries;
754
- if (knowledgeResult.warnings.length > 0)
755
- warnings.push(...knowledgeResult.warnings);
756
- if (!cgResult) {
757
- emitSemanticControlLog({
758
- event: 'context.build.degraded', identity: logIdentity, level: 'warn', timestamp: Date.now(),
759
- data: { stage: 'codegraph_context', reason: 'context_unavailable', fallback: 'manual_pipeline' },
760
- });
761
- const result = await this.buildTaskContextLegacy(query, options);
762
- result._devflow = { fallback: "buildContext-unavailable" };
763
- return result;
764
- }
765
- // buildContext returns JSON string when format="json" — parse it
766
- let ctx;
767
- if (typeof cgResult === "string") {
768
- try {
769
- ctx = JSON.parse(cgResult);
770
- }
771
- catch {
772
- emitSemanticControlLog({
773
- event: 'context.build.degraded', identity: logIdentity, level: 'warn', timestamp: Date.now(),
774
- data: { stage: 'codegraph_parse', reason: 'unparseable_context', fallback: 'manual_pipeline' },
775
- });
776
- return this.buildTaskContextLegacy(query, options);
777
- }
778
- }
779
- else {
780
- ctx = cgResult;
781
- }
782
- ctx.nodes = Array.isArray(ctx.nodes) ? ctx.nodes : [];
783
- ctx.entryPoints = Array.isArray(ctx.entryPoints) ? ctx.entryPoints : [];
784
- const inventoryNodeIds = new Set(ctx.nodes.map((node) => String(node.id ?? '')));
785
- for (const target of artifactInventory.operationTargets.filter(entry => entry.freshness.current)) {
786
- const nodes = this.cg.getNodesInFile(target.path)
787
- .filter(node => node.kind !== 'file' && node.kind !== 'import');
788
- for (const node of nodes) {
789
- if (!inventoryNodeIds.has(node.id)) {
790
- ctx.nodes.push(node);
791
- inventoryNodeIds.add(node.id);
792
- }
793
- }
794
- const root = nodes.find(node => target.symbols.includes(node.name)) ?? nodes[0];
795
- if (root && !ctx.entryPoints.some((entry) => entry.id === root.id))
796
- ctx.entryPoints.unshift(root);
797
- }
798
- await this.applyTaskSymbolVectorRecall(ctx, combinedQuery, warnings);
799
- this.clearRecoveredSymbolWarning(warnings);
800
- // Fallback: direct searchNodes for query terms that buildContext may have missed
801
- // (e.g. hyphenated terms like "post-tool-use" get tokenized and lost in FTS noise)
802
- const tFallback = Date.now();
803
- const existingFiles = new Set((ctx.nodes ?? []).map((n) => n.filePath));
804
- const existingIds = new Set((ctx.nodes ?? []).map((n) => n.id));
805
- const fallbackTerms = combinedQuery.split(/\s+/).filter(w => w.length > 2).sort((a, b) => b.length - a.length).slice(0, 5);
806
- for (const term of fallbackTerms) {
807
- const hits = this.cg.searchNodes(term, { limit: 3 });
808
- for (const hit of hits) {
809
- if (!existingFiles.has(hit.node.filePath)) {
810
- const fileNodes = this.cg.getNodesInFile(hit.node.filePath);
811
- let addedCount = 0;
812
- const MAX_PER_FILE = 5;
813
- // Only add the matching symbol + a few high-value symbols (functions, components, classes)
814
- for (const fn of fileNodes) {
815
- if (fn.kind === "file" || fn.kind === "import" || existingIds.has(fn.id))
816
- continue;
817
- const isHighValue = fn.kind === "function" || fn.kind === "class" || fn.kind === "component" || fn.kind === "method";
818
- if (addedCount >= MAX_PER_FILE && !isHighValue)
819
- continue;
820
- ctx.nodes.push(fn);
821
- existingIds.add(fn.id);
822
- addedCount++;
823
- }
824
- existingFiles.add(hit.node.filePath);
825
- // Lexical fallback is supporting evidence, not a graph root. Promoting
826
- // it to an entry point turns weak term matches into required files.
827
- }
828
- }
829
- }
830
- timing["2_fallback_loop"] = Date.now() - tFallback;
831
- const fileFreshnessByPath = new Map();
832
- if (this.indexer?.ensureFilesCurrent) {
833
- const candidatePaths = [...new Set([
834
- ...ctx.nodes.map((node) => String(node.filePath ?? '')).filter(Boolean),
835
- ...ctx.entryPoints.map((entry) => String(entry.filePath ?? '')).filter(Boolean),
836
- ])];
837
- const freshness = await this.indexer.ensureFilesCurrent(candidatePaths, { refresh: true, timeoutMs: 5_000 });
838
- for (const file of freshness.files)
839
- fileFreshnessByPath.set(file.path, file);
840
- const rejectedPaths = new Set([...freshness.missing, ...freshness.stale]);
841
- if (rejectedPaths.size > 0 || freshness.refreshed.length > 0) {
842
- this.queryCache.invalidatePaths(new Set([...rejectedPaths, ...freshness.refreshed]));
843
- invalidateContextPrefetchCache(this.cg.getProjectRoot?.() ?? process.cwd());
844
- }
845
- if (freshness.missing.length > 0)
846
- warnings.push(`codegraph-missing-files:${freshness.missing.length}`);
847
- if (freshness.stale.length > 0)
848
- warnings.push(`codegraph-stale-degraded:${freshness.stale.length}`);
849
- ctx.nodes = ctx.nodes.filter((node) => !rejectedPaths.has(node.filePath));
850
- ctx.entryPoints = ctx.entryPoints.filter((entry) => !rejectedPaths.has(entry.filePath));
851
- if (Array.isArray(ctx.edges) && rejectedPaths.size > 0) {
852
- const retainedIds = new Set(ctx.nodes.map((node) => node.id));
853
- ctx.edges = ctx.edges.filter((edge) => retainedIds.has(edge.source) && retainedIds.has(edge.target));
854
- }
855
- }
856
- const edgeQuality = sanitizeCodeGraphEdges(this.cg, Array.isArray(ctx.edges) ? ctx.edges : []);
857
- ctx.edges = edgeQuality.accepted;
858
- if (edgeQuality.rejected.length > 0) {
859
- warnings.push(`codegraph-relations-rejected:${edgeQuality.rejected.length}`);
860
- emitSemanticControlLog({
861
- event: 'codegraph.edge.rejected',
862
- identity: logIdentity,
863
- level: 'warn',
864
- timestamp: Date.now(),
865
- data: {
866
- rejectedCount: edgeQuality.rejected.length,
867
- reasons: countValues(edgeQuality.rejected.map(edge => edge.reason)),
868
- examples: edgeQuality.rejected.slice(0, 8).map(edge => ({
869
- kind: edge.kind,
870
- sourceFile: edge.sourceFile,
871
- targetFile: edge.targetFile,
872
- reason: edge.reason,
55
+ const selected = packCandidates(output.candidates, request.maxTokens ?? 8_000);
56
+ return {
57
+ files: selected.map(toSnippet),
58
+ ...(request.includeGraph ? {
59
+ graph: {
60
+ nodes: selected.map(candidate => ({
61
+ id: candidate.evidence.sourceId,
62
+ type: String(candidate.payload.kind ?? 'unknown'),
63
+ name: String(candidate.payload.symbol ?? ''),
64
+ file: String(candidate.payload.path ?? ''),
65
+ metadata: {},
873
66
  })),
67
+ edges: [],
874
68
  },
875
- });
876
- }
877
- const tSnippet = Date.now();
878
- let allNodes = ctx.nodes ?? [];
879
- const originalEntryPoints = (ctx.entryPoints ?? []);
880
- const anchoredEntryPoints = targetAnchors.length > 0
881
- ? originalEntryPoints.filter(entry => fileMatchesTargetAnchor(entry.filePath, targetAnchors))
882
- : originalEntryPoints;
883
- const graphSelection = selectGraphEvidence({
884
- codegraph: this.cg,
885
- candidates: originalEntryPoints,
886
- symbols: retrievalTaskPlan.symbols,
887
- targetAnchors,
888
- });
889
- const evidenceEntryPoints = graphSelection.selected.length > 0
890
- ? graphSelection.selected
891
- : originalEntryPoints;
892
- const selectedAnchoredEntryPoints = targetAnchors.length > 0
893
- ? evidenceEntryPoints.filter(entry => fileMatchesTargetAnchor(entry.filePath, targetAnchors))
894
- : evidenceEntryPoints;
895
- const effectiveEntryPoints = selectedAnchoredEntryPoints.length > 0
896
- ? selectedAnchoredEntryPoints
897
- : anchoredEntryPoints.length > 0 ? anchoredEntryPoints : evidenceEntryPoints;
898
- if (graphSelection.ambiguities.length > 0) {
899
- warnings.push(`codegraph-symbol-ambiguity:${graphSelection.ambiguities.length}`);
900
- }
901
- const rootIds = new Set(effectiveEntryPoints.map((ep) => ep.id));
902
- const reachableNodeIds = reachableCodeGraphNodeIds(rootIds, edgeQuality.accepted);
903
- const entryFiles = new Set(effectiveEntryPoints.map((entry) => entry.filePath));
904
- const protectedTerms = combinedQuery.toLocaleLowerCase('en-US')
905
- .split(/\s+/u)
906
- .filter(term => term.length >= 4);
907
- allNodes = allNodes.filter(node => {
908
- if (reachableNodeIds.has(node.id) || entryFiles.has(node.filePath))
909
- return true;
910
- const text = `${node.name ?? ''} ${node.filePath ?? ''}`.toLocaleLowerCase('en-US');
911
- return protectedTerms.some(term => text.includes(term));
912
- });
913
- ctx.nodes = allNodes;
914
- const requiredFileSet = new Set();
915
- const optionalFileSet = new Set();
916
- const keySymbols = [];
917
- const entryFileSet = new Set(effectiveEntryPoints.map((ep) => ep.filePath));
918
- const graphEvidence = buildGraphEvidence(allNodes.filter(node => node.id && node.filePath), rootIds, Array.isArray(ctx.edges) ? ctx.edges : [], this.cg);
919
- const perFileCount = new Map();
920
- const MAX_SYMBOLS_PER_FILE = 5;
921
- const optionalCandidates = new Map();
922
- const graphOptionalPaths = new Set();
923
- const queryTerms = combinedQuery.split(/\s+/).map(term => term.toLowerCase()).filter(term => term.length >= 3);
924
- for (const target of artifactInventory.operationTargets) {
925
- if (target.freshness.current)
926
- requiredFileSet.add(target.path);
927
- }
928
- for (const node of allNodes) {
929
- // Skip file wrappers and import edges — they're not actionable symbols
930
- if (node.kind === "file" || node.kind === "import")
931
- continue;
932
- const isDirect = targetAnchors.length > 0
933
- ? fileMatchesTargetAnchor(node.filePath, targetAnchors)
934
- : rootIds.has(node.id) || entryFileSet.has(node.filePath);
935
- if (isDirect) {
936
- requiredFileSet.add(node.filePath);
937
- }
938
- else {
939
- const distance = graphEvidence.distances.get(node.id);
940
- const text = `${node.name} ${node.signature ?? ""} ${node.filePath}`.toLowerCase();
941
- const lexicalMatches = queryTerms.filter(term => text.includes(term));
942
- const evidence = [
943
- ...(graphEvidence.reasons.get(node.id) ?? []),
944
- ...(lexicalMatches.length > 0 ? [`matched query term${lexicalMatches.length > 1 ? "s" : ""}: ${lexicalMatches.join(", ")}`] : []),
945
- ];
946
- // A path-anchored request already identifies the target precisely. Only
947
- // direct graph neighbours are useful supporting context; wider graph
948
- // traversal and generic lexical matches reintroduce unrelated modules.
949
- const hasStrongTargetEvidence = targetAnchors.length === 0
950
- ? evidence.length > 0
951
- : distance !== undefined && distance <= 1;
952
- if (hasStrongTargetEvidence) {
953
- const graphScore = distance === undefined ? 0 : Math.max(0.35, 0.8 - distance * 0.15);
954
- const lexicalScore = Math.min(0.3, lexicalMatches.length * 0.1);
955
- const score = Math.min(1, graphScore + lexicalScore);
956
- const existing = optionalCandidates.get(node.filePath);
957
- if (!existing || score > existing.score) {
958
- optionalCandidates.set(node.filePath, {
959
- path: node.filePath,
960
- score,
961
- evidence,
962
- symbols: [],
963
- snippet: "",
964
- distance,
965
- });
966
- graphOptionalPaths.add(node.filePath);
967
- }
968
- }
969
- }
970
- const count = perFileCount.get(node.filePath) ?? 0;
971
- if (count < MAX_SYMBOLS_PER_FILE && (isDirect || optionalCandidates.has(node.filePath))) {
972
- perFileCount.set(node.filePath, count + 1);
973
- keySymbols.push({
974
- name: node.name,
975
- file: node.filePath,
976
- kind: node.kind,
977
- relevance: isDirect ? "direct" : "indirect",
978
- });
979
- }
980
- }
981
- const fileRoleByPath = new Map();
982
- for (const artifact of artifactInventory.entries.filter(entry => entry.freshness.current)) {
983
- fileRoleByPath.set(artifact.path, {
984
- path: artifact.path,
985
- role: artifact.artifactRole === 'operation_target' ? 'target' : 'dependency',
986
- artifactRole: artifact.artifactRole,
987
- score: artifact.score,
988
- evidence: artifact.evidence,
989
- });
990
- fileFreshnessByPath.set(artifact.path, artifact.freshness);
991
- }
992
- const roleCandidates = this.indexer
993
- ? assembleContextFileRoles({
994
- codegraph: this.cg,
995
- indexer: this.indexer,
996
- targetFiles: [...requiredFileSet],
997
- taskType,
998
- })
999
- : [];
1000
- for (const candidate of roleCandidates) {
1001
- if (this.indexer?.getFileFreshness) {
1002
- fileFreshnessByPath.set(candidate.path, this.indexer.getFileFreshness(candidate.path));
1003
- }
1004
- const inventoryRole = fileRoleByPath.get(candidate.path);
1005
- fileRoleByPath.set(candidate.path, inventoryRole && inventoryRole.score >= candidate.score
1006
- ? inventoryRole
1007
- : candidate);
1008
- if (candidate.role === 'target') {
1009
- requiredFileSet.add(candidate.path);
1010
- continue;
1011
- }
1012
- const existing = optionalCandidates.get(candidate.path);
1013
- if (!existing || candidate.score > existing.score) {
1014
- optionalCandidates.set(candidate.path, {
1015
- path: candidate.path,
1016
- score: candidate.score,
1017
- evidence: candidate.evidence,
1018
- symbols: [],
1019
- snippet: '',
1020
- distance: candidate.role === 'caller' ? 1 : undefined,
1021
- });
1022
- }
1023
- }
1024
- let rankedOptionalFiles = rankOptionalContextFiles([...optionalCandidates.values()], taskType);
1025
- const codeBudget = allocateContextFileBudget({
1026
- requiredPaths: requiredFileSet,
1027
- candidates: rankedOptionalFiles.map(candidate => ({
1028
- path: candidate.path,
1029
- score: candidate.score,
1030
- source: fileRoleByPath.has(candidate.path) && fileRoleByPath.get(candidate.path)?.role !== 'dependency'
1031
- ? 'structural'
1032
- : graphOptionalPaths.has(candidate.path) ? 'graph' : 'structural',
1033
- })),
1034
- tokenBudget: Math.floor(maxTokens * 0.45),
1035
- graphRatio: 0.2,
1036
- });
1037
- rankedOptionalFiles = rankedOptionalFiles.filter(candidate => codeBudget.selectedPaths.has(candidate.path));
1038
- if (codeBudget.rejectedPaths.size > 0)
1039
- warnings.push(`codegraph-budget-rejected:${codeBudget.rejectedPaths.size}`);
1040
- for (const candidate of rankedOptionalFiles)
1041
- optionalFileSet.add(candidate.path);
1042
- if (this.indexer?.getFileFreshness) {
1043
- for (const path of [...requiredFileSet, ...optionalFileSet]) {
1044
- const freshness = fileFreshnessByPath.get(path) ?? this.indexer.getFileFreshness(path);
1045
- fileFreshnessByPath.set(path, freshness);
1046
- if (freshness.current)
1047
- continue;
1048
- requiredFileSet.delete(path);
1049
- optionalFileSet.delete(path);
1050
- fileRoleByPath.delete(path);
1051
- }
1052
- rankedOptionalFiles = rankedOptionalFiles.filter(candidate => optionalFileSet.has(candidate.path));
1053
- }
1054
- const optionalCandidateByPath = new Map(rankedOptionalFiles.map(candidate => [candidate.path, candidate]));
1055
- const retainedFiles = new Set([...requiredFileSet, ...optionalFileSet]);
1056
- for (let index = keySymbols.length - 1; index >= 0; index--) {
1057
- if (!retainedFiles.has(keySymbols[index].file))
1058
- keySymbols.splice(index, 1);
1059
- }
1060
- // Fallback: when code graph returns no nodes but knowledge/memory have hits,
1061
- // the query is likely a non-code question (e.g. "how to run the project").
1062
- // Surface knowledge and memory results as the primary context.
1063
- const isNonCodeQuery = allNodes.length === 0 && (knowledgeHits.length > 0 || memoryHits.length > 0);
1064
- const cgReasoning = isNonCodeQuery ? [{
1065
- source: "graph_root",
1066
- fileGroup: "knowledge-memory",
1067
- explanation: `No code symbols matched — query interpreted as a non-code question. Falling back to project knowledge (${knowledgeHits.length} hits) and memory (${memoryHits.length} hits).`,
1068
- }] : [{
1069
- source: "graph_root",
1070
- fileGroup: "codegraph-buildContext",
1071
- explanation: ctx.summary
1072
- ? `CodeGraph: ${ctx.summary}`
1073
- : `CodeGraph matched ${allNodes.length} nodes across ${requiredFileSet.size + optionalFileSet.size} files`,
1074
- }];
1075
- const knowledgeReasoning = knowledgeHits.map((k) => ({
1076
- source: "knowledge",
1077
- fileGroup: k.source ?? "knowledge",
1078
- explanation: `Knowledge entry "${k.title || k.source || "source"}" matched query${typeof k.score === "number" ? ` with score ${k.score.toFixed(2)}` : ""}`,
1079
- }));
1080
- const memoryReasoning = memoryHits.map((m) => ({
1081
- source: "memory",
1082
- fileGroup: m.entry?.category ?? "memory",
1083
- explanation: `Memory entry matched: ${String(m.entry?.content ?? "").slice(0, 100)}`,
1084
- }));
1085
- const allFileSnippets = [];
1086
- for (const fp of [...requiredFileSet].sort()) {
1087
- const nodesInFile = allNodes.filter(n => n.filePath === fp);
1088
- allFileSnippets.push({
1089
- path: fp,
1090
- snippet: nodesInFile.map(n => `// ${n.kind}: ${n.name}${n.signature ? ` ${n.signature}` : ""}`).join("\n"),
1091
- symbols: nodesInFile.map(n => n.name),
1092
- score: 1.0,
1093
- });
1094
- }
1095
- for (const fp of [...optionalFileSet].sort()) {
1096
- const nodesInFile = allNodes.filter(n => n.filePath === fp);
1097
- allFileSnippets.push({
1098
- path: fp,
1099
- snippet: nodesInFile.map(n => `// ${n.kind}: ${n.name}${n.signature ? ` ${n.signature}` : ""}`).join("\n"),
1100
- symbols: nodesInFile.map(n => n.name),
1101
- score: 0.5,
1102
- });
1103
- }
1104
- const compressed = compressToTokenLimit(allFileSnippets, Math.floor(maxTokens * 0.45));
1105
- if (compressed.overBudget)
1106
- warnings.push("context-compressor-budget-exceeded");
1107
- timing["3_snippet_compress"] = Date.now() - tSnippet;
1108
- const riskHints = generateRiskHints(allNodes.filter(node => node.kind !== "file" && node.kind !== "import"), graphEvidence);
1109
- const nextActions = generateNextActions(taskType, keySymbols.slice(0, 5));
1110
- const rootPrecision = targetAnchors.length === 0
1111
- ? 1
1112
- : requiredFileSet.size === 0
1113
- ? 0
1114
- : [...requiredFileSet].filter(file => fileMatchesTargetAnchor(file, targetAnchors)).length / requiredFileSet.size;
1115
- if (targetAnchors.length > 0 && effectiveEntryPoints.length < originalEntryPoints.length) {
1116
- warnings.push(`context-quality-filtered:${originalEntryPoints.length - effectiveEntryPoints.length}`);
1117
- }
1118
- const provenanceFiles = [];
1119
- for (const file of [...requiredFileSet].sort()) {
1120
- const node = allNodes.find(candidate => candidate.filePath === file);
1121
- const language = this.getFileLanguage(file);
1122
- const layer = describeLayerEvidence(file, language);
1123
- provenanceFiles.push({
1124
- path: file,
1125
- layer: "codegraph",
1126
- score: 1,
1127
- evidence: [
1128
- ...(node ? [`matched symbol ${node.name}`] : []),
1129
- ...(graphEvidence.reasons.get(node?.id ?? "") ?? []),
1130
- ],
1131
- reason: `required as ${fileRoleByPath.get(file)?.role ?? 'target'} by CodeGraph evidence (${layer.evidence})`,
1132
- });
1133
- }
1134
- for (const candidate of rankedOptionalFiles) {
1135
- const layer = describeLayerEvidence(candidate.path, this.getFileLanguage(candidate.path));
1136
- provenanceFiles.push({
1137
- path: candidate.path,
1138
- layer: "codegraph",
1139
- score: candidate.score,
1140
- evidence: candidate.evidence,
1141
- reason: `selected as ${fileRoleByPath.get(candidate.path)?.role ?? 'dependency'} (${layer.evidence})`,
1142
- });
1143
- }
1144
- const knowledgeScore = knowledgeHits.length === 0
1145
- ? 0
1146
- : knowledgeHits.reduce((total, hit) => total + (typeof hit.score === "number" ? hit.score : 0.5), 0) / knowledgeHits.length;
1147
- const memoryScore = memoryHits.length === 0
1148
- ? 0
1149
- : memoryHits.reduce((total, hit) => total + (typeof hit.score === "number" ? hit.score : 0), 0) / memoryHits.length;
1150
- const provenance = {
1151
- layers: [
1152
- {
1153
- layer: "codegraph",
1154
- score: requiredFileSet.size > 0 ? 1 : 0,
1155
- evidence: [`${requiredFileSet.size} required and ${optionalFileSet.size} optional repository files retained`],
1156
- },
1157
- {
1158
- layer: "knowledge",
1159
- score: knowledgeScore,
1160
- evidence: knowledgeHits.slice(0, 5).map((hit) => `${hit.source ?? "knowledge"}: ${hit.title || "untitled"}`),
1161
- },
1162
- {
1163
- layer: "memory",
1164
- score: memoryScore,
1165
- evidence: memoryHits.slice(0, 5).map((hit) => String(hit.entry?.content ?? "").slice(0, 120)),
1166
- },
1167
- ],
1168
- files: provenanceFiles,
1169
- };
1170
- const result = {
1171
- taskType,
1172
- requiredFiles: {
1173
- files: [...requiredFileSet],
1174
- symbols: keySymbols.filter(s => s.relevance === "direct").map(s => s.name).slice(0, 30),
1175
- reasoning: isNonCodeQuery
1176
- ? [...cgReasoning, ...knowledgeReasoning, ...memoryReasoning]
1177
- : cgReasoning,
1178
- },
1179
- optionalFiles: {
1180
- files: [...optionalFileSet],
1181
- symbols: keySymbols.filter(s => s.relevance === "indirect").map(s => s.name).slice(0, 30),
1182
- reasoning: isNonCodeQuery ? [] : [
1183
- ...rankedOptionalFiles.map(candidate => ({
1184
- source: "dependency_trace",
1185
- fileGroup: candidate.path,
1186
- explanation: candidate.evidence.join("; "),
1187
- })),
1188
- ],
1189
- },
1190
- keySymbols: keySymbols
1191
- .sort((a, b) => {
1192
- // Direct before indirect
1193
- if (a.relevance === "direct" && b.relevance !== "direct")
1194
- return -1;
1195
- if (a.relevance !== "direct" && b.relevance === "direct")
1196
- return 1;
1197
- // Functions/classes before constants/variables
1198
- const kindScore = (k) => k === "function" || k === "method" || k === "class" ? 0 :
1199
- k === "component" || k === "interface" || k === "type" ? 1 :
1200
- k === "variable" || k === "constant" || k === "enum" ? 2 : 3;
1201
- return kindScore(a.kind) - kindScore(b.kind);
1202
- })
1203
- .slice(0, 20),
1204
- graphSummary: {
1205
- roots: [...rootIds],
1206
- traversalDepth: ctx.stats?.traversalDepth ?? 2,
1207
- relatedNodeCount: allNodes.length,
1208
- },
1209
- riskHints,
1210
- nextActions,
1211
- reasoning: [...cgReasoning, ...knowledgeReasoning, ...memoryReasoning],
1212
- confidence: calculateConfidence({
1213
- cgConfidence: ctx.stats?.confidence,
1214
- knowledgeHitCount: knowledgeHits.length,
1215
- memoryHitCount: memoryHits.length,
1216
- nodeCount: allNodes.length,
1217
- intentConfidence: intent.confidence,
1218
- hasEdges: Array.isArray(ctx.edges) && (ctx.edges.length > 0),
1219
- hasExactMatches: keySymbols.some(s => s.relevance === "direct"),
1220
- targetPrecision: rootPrecision,
1221
- }),
1222
- tokenCount: compressed.tokenCount,
1223
- warnings: warnings.length > 0 ? warnings : undefined,
1224
- provenance,
69
+ } : {}),
70
+ confidence: codeConfidence(selected),
71
+ tokenCount: selected.reduce((sum, candidate) => sum + (candidate.estimatedTokens ?? 0), 0),
72
+ ...(output.gaps.length > 0 || output.rejectedRefs.length > 0
73
+ ? { warnings: reasonCodes(output.gaps.map(item => item.reason), output.rejectedRefs.map(item => item.reason)) }
74
+ : {}),
1225
75
  };
1226
- result.taskEnvelope = buildTaskContextEnvelope({
1227
- query,
1228
- result,
1229
- projectRoot: this.cg.getProjectRoot?.() ?? process.cwd(),
1230
- sessionId: options?.sessionId,
1231
- executionId: options?.executionId,
1232
- requestId: options?.requestId,
1233
- host: options?.host,
1234
- command: options?.command,
1235
- turnId: options?.turnId,
1236
- intentArtifact: options?.intentArtifact,
1237
- queryPlan: options?.queryPlan,
1238
- tokenBudget: maxTokens,
1239
- targetAnchors,
1240
- taskPlan: retrievalTaskPlan,
1241
- graphAmbiguities: graphSelection.ambiguities,
1242
- codeBudget: {
1243
- maxFiles: codeBudget.maxFiles,
1244
- graphFileLimit: codeBudget.graphFileLimit,
1245
- selectedGraphFiles: codeBudget.selectedGraphFiles,
1246
- },
1247
- fileRoles: fileRoleByPath,
1248
- fileFreshness: fileFreshnessByPath,
1249
- memoryHits,
1250
- knowledgeHits,
1251
- rejectedCodeRelations: edgeQuality.rejected.length,
1252
- knowledgeRejected: (knowledgeResult.rejectedLowScore ?? 0)
1253
- + (knowledgeResult.rejectedIncompatibleSources ?? 0)
1254
- + Object.values((knowledgeResult
1255
- .retrievalDiagnostics?.qualityRejected) ?? {}).reduce((total, count) => total + count, 0),
1256
- });
1257
- try {
1258
- recordTaskEnvelopeLedger(result.taskEnvelope);
1259
- }
1260
- catch {
1261
- result.warnings = [...(result.warnings ?? []), 'retrieval-ledger-degraded'];
1262
- }
1263
- if (options?.requestId && result.taskEnvelope.memory.entries.length > 0) {
1264
- try {
1265
- await this.memory.recordContextEnvelopeSelection?.({
1266
- entries: result.taskEnvelope.memory.entries,
1267
- sessionId: options.sessionId,
1268
- executionId: options.executionId,
1269
- requestId: options.requestId,
1270
- query,
1271
- contextReceipt: result.taskEnvelope.receipt.contextHash,
1272
- });
1273
- }
1274
- catch {
1275
- result.warnings = [...(result.warnings ?? []), 'memory-attribution-degraded'];
1276
- }
1277
- }
1278
- if (warnings.length === 0 && !options?.requestId) {
1279
- this.queryCache.set(cacheKey, result);
1280
- }
1281
- timing["0_total"] = Date.now() - t0;
1282
- emitSemanticControlLog({
1283
- event: 'codegraph.selection.completed',
1284
- identity: {
1285
- ...logIdentity,
1286
- contextReceipt: result.taskEnvelope?.receipt.contextHash,
1287
- },
1288
- level: warnings.length > 0 ? 'warn' : 'info',
1289
- timestamp: Date.now(),
1290
- data: {
1291
- selectedFiles: result.taskEnvelope?.code.files.length ?? 0,
1292
- selectedSymbols: result.taskEnvelope?.code.symbols.length ?? 0,
1293
- rejectedRelations: result.taskEnvelope?.quality.code.rejected ?? 0,
1294
- ambiguityCount: result.taskEnvelope?.code.ambiguities?.length ?? 0,
1295
- timing,
1296
- warnings: warnings.map(value => value.split(':')[0]),
1297
- },
1298
- });
1299
- result._timing = timing;
1300
- return result;
1301
76
  }
1302
- async buildTaskContextLegacy(query, options) {
1303
- const warnings = this.scheduleSymbolVectorBuild();
1304
- const stats = this.cg.getStats();
1305
- const adaptive = getAdaptiveParams(stats.fileCount ?? 500);
1306
- const maxTokens = options?.maxTokens ?? 8000;
1307
- const depth = options?.mode === "deep" ? 3 : Math.max(2, adaptive.depth);
1308
- const intent = parseIntent(query);
1309
- const taskType = intent.intent;
1310
- // L1 dict + agent terms merge
1311
- const l1Query = expandQuery(query);
1312
- const combinedQuery = mergeQueries(query, l1Query, options?.expandedTerms);
1313
- // CodeGraph semantic search + graph traversal
1314
- const cgResult = await this.cg.findRelevantContext(combinedQuery, {
1315
- searchLimit: 10,
1316
- traversalDepth: depth,
1317
- maxNodes: depth === 3 ? 30 : 15,
1318
- });
1319
- await this.applyLegacySymbolVectorRecall(cgResult, query, warnings);
1320
- this.clearRecoveredSymbolWarning(warnings);
1321
- const fileFreshnessByPath = new Map();
1322
- if (this.indexer?.ensureFilesCurrent) {
1323
- const candidatePaths = [...new Set(Array.from(cgResult.nodes.values()).map(node => node.filePath))];
1324
- const freshness = await this.indexer.ensureFilesCurrent(candidatePaths, { refresh: true, timeoutMs: 5_000 });
1325
- for (const file of freshness.files)
1326
- fileFreshnessByPath.set(file.path, file);
1327
- const rejectedPaths = new Set([...freshness.missing, ...freshness.stale]);
1328
- for (const [id, node] of cgResult.nodes) {
1329
- if (rejectedPaths.has(node.filePath))
1330
- cgResult.nodes.delete(id);
1331
- }
1332
- cgResult.roots = cgResult.roots.filter(id => cgResult.nodes.has(id));
1333
- if (freshness.missing.length > 0)
1334
- warnings.push(`codegraph-missing-files:${freshness.missing.length}`);
1335
- if (freshness.stale.length > 0)
1336
- warnings.push(`codegraph-stale-degraded:${freshness.stale.length}`);
1337
- if (rejectedPaths.size > 0 || freshness.refreshed.length > 0) {
1338
- this.queryCache.invalidatePaths(new Set([...rejectedPaths, ...freshness.refreshed]));
1339
- invalidateContextPrefetchCache(this.cg.getProjectRoot?.() ?? process.cwd());
1340
- }
1341
- }
1342
- const edgeQuality = sanitizeCodeGraphEdges(this.cg, cgResult.edges ?? []);
1343
- if (edgeQuality.rejected.length > 0) {
1344
- warnings.push(`codegraph-relations-rejected:${edgeQuality.rejected.length}`);
1345
- }
1346
- // Build full node list for classification
1347
- const allNodes = Array.from(cgResult.nodes.values());
1348
- const rootIds = new Set(cgResult.roots);
1349
- // Get outgoing edges to classify direct vs indirect dependencies
1350
- const directDeps = new Set();
1351
- for (const rootId of rootIds) {
1352
- const edges = sanitizeCodeGraphEdges(this.cg, this.cg.getOutgoingEdges(rootId)).accepted;
1353
- for (const e of edges)
1354
- directDeps.add(e.target);
1355
- }
1356
- const graphEvidence = buildGraphEvidence(allNodes.filter(node => node.id && node.filePath), rootIds, edgeQuality.accepted, this.cg);
1357
- // Classify files into required (root matches + direct deps) and evidence-ranked optional files.
1358
- const requiredFileSet = new Set();
1359
- const requiredSymbols = [];
1360
- const optionalFileSet = new Set();
1361
- const optionalSymbols = [];
1362
- const optionalCandidates = new Map();
1363
- const queryTerms = query.split(/\s+/).map(term => term.toLowerCase()).filter(term => term.length >= 3);
1364
- for (const node of allNodes) {
1365
- const fp = node.filePath;
1366
- if (rootIds.has(node.id) || directDeps.has(node.id)) {
1367
- requiredFileSet.add(fp);
1368
- requiredSymbols.push(node.name);
1369
- }
1370
- else {
1371
- const distance = graphEvidence.distances.get(node.id);
1372
- const text = `${node.name} ${node.signature ?? ""} ${node.filePath}`.toLowerCase();
1373
- const lexicalMatches = queryTerms.filter(term => text.includes(term));
1374
- const evidence = [
1375
- ...(graphEvidence.reasons.get(node.id) ?? []),
1376
- ...(lexicalMatches.length > 0 ? [`matched query term${lexicalMatches.length > 1 ? "s" : ""}: ${lexicalMatches.join(", ")}`] : []),
1377
- ];
1378
- if (evidence.length === 0)
1379
- continue;
1380
- const score = Math.min(1, (distance === undefined ? 0 : Math.max(0.35, 0.8 - distance * 0.15)) + Math.min(0.3, lexicalMatches.length * 0.1));
1381
- const existing = optionalCandidates.get(fp);
1382
- if (!existing || score > existing.score) {
1383
- optionalCandidates.set(fp, {
1384
- path: fp,
1385
- score,
1386
- evidence,
1387
- symbols: [],
1388
- snippet: "",
1389
- distance,
1390
- });
1391
- }
1392
- }
1393
- }
1394
- const targetAnchors = extractTargetAnchors(query);
1395
- const retrievalTaskPlan = buildRetrievalTaskPlan({
77
+ async buildTaskContext(queryText, options = {}) {
78
+ const planned = options.queryPlan?.code;
79
+ const codeQueries = planned
80
+ ? planned.requirement === 'prohibited' ? [] : [planned.typedQuery]
81
+ : [directCodeQuery(queryText, options)];
82
+ const budget = options.maxTokens ?? 8_000;
83
+ const outputs = await Promise.all(codeQueries.map(query => this.runtime.search({
1396
84
  query,
1397
- parsedIntent: intent,
1398
- targetAnchors,
1399
- activeSkill: options?.activeSkill,
1400
- command: options?.command,
1401
- });
1402
- const roleTargets = targetAnchors.length > 0
1403
- ? [...requiredFileSet].filter(file => fileMatchesTargetAnchor(file, targetAnchors))
1404
- : allNodes.filter(node => rootIds.has(node.id)).map(node => node.filePath);
1405
- const fileRoleByPath = new Map();
1406
- const roleCandidates = this.indexer
1407
- ? assembleContextFileRoles({
1408
- codegraph: this.cg,
1409
- indexer: this.indexer,
1410
- targetFiles: [...new Set(roleTargets)],
1411
- taskType,
1412
- })
1413
- : [];
1414
- for (const candidate of roleCandidates) {
1415
- if (this.indexer?.getFileFreshness) {
1416
- fileFreshnessByPath.set(candidate.path, this.indexer.getFileFreshness(candidate.path));
1417
- }
1418
- fileRoleByPath.set(candidate.path, candidate);
1419
- if (candidate.role === 'target') {
1420
- requiredFileSet.add(candidate.path);
1421
- continue;
1422
- }
1423
- const existing = optionalCandidates.get(candidate.path);
1424
- if (!existing || candidate.score > existing.score) {
1425
- optionalCandidates.set(candidate.path, {
1426
- path: candidate.path,
1427
- score: candidate.score,
1428
- evidence: candidate.evidence,
1429
- symbols: [],
1430
- snippet: '',
1431
- distance: candidate.role === 'caller' ? 1 : undefined,
1432
- });
1433
- }
1434
- }
1435
- let rankedOptionalFiles = rankOptionalContextFiles([...optionalCandidates.values()], taskType);
1436
- for (const candidate of rankedOptionalFiles)
1437
- optionalFileSet.add(candidate.path);
1438
- if (this.indexer?.getFileFreshness) {
1439
- for (const path of [...requiredFileSet, ...optionalFileSet]) {
1440
- const freshness = fileFreshnessByPath.get(path) ?? this.indexer.getFileFreshness(path);
1441
- fileFreshnessByPath.set(path, freshness);
1442
- if (freshness.current)
1443
- continue;
1444
- requiredFileSet.delete(path);
1445
- optionalFileSet.delete(path);
1446
- fileRoleByPath.delete(path);
1447
- }
1448
- rankedOptionalFiles = rankedOptionalFiles.filter(candidate => optionalFileSet.has(candidate.path));
1449
- }
1450
- for (const node of allNodes) {
1451
- if (optionalFileSet.has(node.filePath))
1452
- optionalSymbols.push(node.name);
1453
- }
1454
- // Sort required files: roots first, then alphabetically
1455
- const requiredFilesList = [...requiredFileSet].sort((a, b) => {
1456
- const aIsRoot = allNodes.some(n => n.filePath === a && rootIds.has(n.id));
1457
- const bIsRoot = allNodes.some(n => n.filePath === b && rootIds.has(n.id));
1458
- if (aIsRoot && !bIsRoot)
1459
- return -1;
1460
- if (!aIsRoot && bIsRoot)
1461
- return 1;
1462
- return a.localeCompare(b);
1463
- });
1464
- const optionalFilesList = [...optionalFileSet].sort();
1465
- // Build reasoning for file groups
1466
- const requiredReasoning = requiredFilesList.slice(0, 10).map(file => {
1467
- const node = allNodes.find(n => n.filePath === file);
1468
- if (!node)
1469
- return null;
1470
- const isRoot = rootIds.has(node.id);
1471
- return {
1472
- source: (isRoot ? "graph_root" : "dependency_trace"),
1473
- fileGroup: file,
1474
- explanation: isRoot
1475
- ? `Matched query "${query}" via semantic search on symbol "${node.name}"`
1476
- : `Dependency of a root match, relevant for context`,
1477
- };
1478
- }).filter(Boolean);
1479
- const optionalReasoning = rankedOptionalFiles.map(candidate => {
1480
- return {
1481
- source: "dependency_trace",
1482
- fileGroup: candidate.path,
1483
- explanation: candidate.evidence.join("; "),
1484
- };
1485
- });
1486
- // Key symbols: root symbols as "direct", non-root as "indirect"
1487
- const keySymbols = allNodes
1488
- .filter(n => requiredFileSet.has(n.filePath) || optionalFileSet.has(n.filePath))
1489
- .map(n => ({
1490
- name: n.name,
1491
- file: n.filePath,
1492
- kind: n.kind,
1493
- relevance: (rootIds.has(n.id) ? "direct" : "indirect"),
85
+ projectId: projectId(this.dependencies.cg),
86
+ contextHash: options.queryPlan?.planHash ?? 'diagnostic-code-search',
87
+ tokenBudget: Math.max(256, Math.floor(budget / codeQueries.length)),
88
+ now: Date.now(),
89
+ })));
90
+ const candidates = dedupeCandidates(outputs.flatMap(output => output.candidates));
91
+ const selected = packCandidates(candidates, budget);
92
+ const required = selected.filter(candidate => candidate.strength === 'strong');
93
+ const optional = selected.filter(candidate => candidate.strength !== 'strong');
94
+ const paths = [...new Set(selected.map(candidate => String(candidate.payload.path ?? '')))].filter(Boolean);
95
+ const riskHints = generateRiskHints(paths);
96
+ const warnings = reasonCodes(outputs.flatMap(output => output.gaps.map(item => item.reason)), outputs.flatMap(output => output.rejectedRefs.map(item => item.reason)));
97
+ const reasoning = selected.map(candidate => ({
98
+ source: candidate.strength === 'weak' ? 'fuzzy_search' : 'dependency_trace',
99
+ fileGroup: String(candidate.payload.path ?? candidate.evidence.sourceId),
100
+ explanation: `${candidate.strength} code evidence from ${candidate.contributingLegs.map(leg => leg.leg).join(', ')}`,
1494
101
  }));
1495
- // Graph summary
1496
- const graphSummary = {
1497
- roots: cgResult.roots,
1498
- traversalDepth: depth,
1499
- relatedNodeCount: allNodes.length,
1500
- };
1501
- // Generate risk hints
1502
- const riskHints = generateRiskHints(allNodes.filter(node => node.kind !== "file" && node.kind !== "import"), graphEvidence);
1503
- // Generate next actions
1504
- const nextActions = generateNextActions(taskType, keySymbols.slice(0, 5));
1505
- // Knowledge hits
1506
- let knowledgeReasoning = [];
1507
- let knowledgeHits = [];
1508
- try {
1509
- const kResults = await this.searchKnowledgeWithStatus(combinedQuery, this.getKnowledgeSearchOptions(combinedQuery, 5, options, retrievalTaskPlan, maxTokens));
1510
- if (kResults.warnings.length > 0)
1511
- warnings.push(...kResults.warnings);
1512
- knowledgeHits = kResults.entries;
1513
- knowledgeReasoning = kResults.entries.map(k => ({
1514
- source: "knowledge",
1515
- fileGroup: k.source ?? "knowledge",
1516
- explanation: `Knowledge entry "${k.title || k.source || "source"}" matched query${typeof k.score === "number" ? ` with score ${k.score.toFixed(2)}` : ""}`,
1517
- }));
1518
- }
1519
- catch {
1520
- // knowledge may be empty
1521
- }
1522
- // Memory context
1523
- let memoryReasoning = [];
1524
- let memoryHits = [];
1525
- try {
1526
- memoryHits = await this.memory.search(query, {
1527
- limit: 5,
1528
- purpose: taskType,
1529
- scope: this.cg.getProjectRoot?.() ?? process.cwd(),
1530
- action: retrievalTaskPlan.action,
1531
- requiredPolicyFacets: retrievalTaskPlan.requiredPolicyFacets,
1532
- recordAccess: false,
1533
- });
1534
- memoryReasoning = memoryHits.map((hit) => ({
1535
- source: 'memory',
1536
- fileGroup: hit.entry?.title ?? 'project memory',
1537
- explanation: `Selected applicable memory ${hit.entry?.id ?? 'unknown'} with score ${hit.score ?? 0}`,
1538
- }));
1539
- }
1540
- catch {
1541
- // memory may be empty
1542
- }
1543
- // Compress: prioritize required files, then optional
1544
- const allFileSnippets = [];
1545
- for (const file of requiredFilesList) {
1546
- const nodesInFile = allNodes.filter(n => n.filePath === file);
1547
- allFileSnippets.push({
1548
- path: file,
1549
- snippet: nodesInFile.map(n => `// ${n.kind}: ${n.name}${n.signature ? ` ${n.signature}` : ""}`).join("\n"),
1550
- symbols: nodesInFile.map(n => n.name),
1551
- score: 1.0,
1552
- });
1553
- }
1554
- for (const file of optionalFilesList) {
1555
- const nodesInFile = allNodes.filter(n => n.filePath === file);
1556
- allFileSnippets.push({
1557
- path: file,
1558
- snippet: nodesInFile.map(n => `// ${n.kind}: ${n.name}${n.signature ? ` ${n.signature}` : ""}`).join("\n"),
1559
- symbols: nodesInFile.map(n => n.name),
1560
- score: 0.5,
1561
- });
1562
- }
1563
- const compressed = compressToTokenLimit(allFileSnippets, Math.floor(maxTokens * 0.45));
1564
- if (compressed.overBudget)
1565
- warnings.push("context-compressor-budget-exceeded");
1566
- const requiredPaths = new Set(compressed.files.filter(f => requiredFileSet.has(f.path)).map(f => f.path));
1567
- const optionalPaths = new Set(compressed.files.filter(f => optionalFileSet.has(f.path)).map(f => f.path));
102
+ const taskType = taskTypeFromProjection(options);
1568
103
  const result = {
1569
104
  taskType,
1570
- requiredFiles: {
1571
- files: [...requiredPaths],
1572
- symbols: requiredSymbols.filter((_, i) => i < 30),
1573
- reasoning: requiredReasoning,
1574
- },
1575
- optionalFiles: {
1576
- files: [...optionalPaths],
1577
- symbols: optionalSymbols.filter((_, i) => i < 30),
1578
- reasoning: optionalReasoning,
105
+ selectedEvidence: selected,
106
+ requiredFiles: group(required, reasoning),
107
+ optionalFiles: group(optional, reasoning),
108
+ keySymbols: selected.flatMap(candidate => {
109
+ const symbol = typeof candidate.payload.symbol === 'string' ? candidate.payload.symbol : undefined;
110
+ const path = typeof candidate.payload.path === 'string' ? candidate.payload.path : undefined;
111
+ const kind = typeof candidate.payload.kind === 'string' ? candidate.payload.kind : 'unknown';
112
+ return symbol && path ? [{
113
+ name: symbol, file: path, kind,
114
+ relevance: candidate.strength === 'strong' ? 'direct' : 'indirect',
115
+ }] : [];
116
+ }),
117
+ graphSummary: {
118
+ roots: required.map(candidate => candidate.evidence.sourceId),
119
+ traversalDepth: Math.max(0, ...codeQueries.map(query => query.maxDepth)),
120
+ relatedNodeCount: selected.length,
1579
121
  },
1580
- keySymbols: keySymbols.slice(0, 20),
1581
- graphSummary,
1582
122
  riskHints,
1583
- nextActions,
1584
- reasoning: [...requiredReasoning, ...optionalReasoning, ...knowledgeReasoning, ...memoryReasoning],
1585
- confidence: intent.confidence,
1586
- tokenCount: compressed.tokenCount,
1587
- warnings: warnings.length > 0 ? warnings : undefined,
123
+ nextActions: selected.slice(0, 1).flatMap(candidate => {
124
+ const symbol = typeof candidate.payload.symbol === 'string' ? candidate.payload.symbol : undefined;
125
+ return symbol ? [{ kind: 'graph_query', label: `Explore ${symbol}`, payload: symbol }] : [];
126
+ }),
127
+ reasoning,
128
+ confidence: codeConfidence(selected),
129
+ tokenCount: selected.reduce((sum, candidate) => sum + (candidate.estimatedTokens ?? 0), 0),
130
+ ...(warnings.length > 0 ? { warnings } : {}),
1588
131
  provenance: {
1589
- layers: [
1590
- {
1591
- layer: "codegraph",
1592
- score: requiredFileSet.size > 0 ? 1 : 0,
1593
- evidence: [`${requiredFileSet.size} required and ${optionalFileSet.size} optional repository files retained`],
1594
- },
1595
- {
1596
- layer: "knowledge",
1597
- score: knowledgeReasoning.length > 0 ? 0.5 : 0,
1598
- evidence: knowledgeReasoning.map(reason => reason.explanation),
1599
- },
1600
- {
1601
- layer: "memory",
1602
- score: 0,
1603
- evidence: memoryReasoning.map(reason => reason.explanation),
1604
- },
1605
- ],
1606
- files: [
1607
- ...requiredFilesList.map(file => ({
1608
- path: file,
1609
- layer: "codegraph",
1610
- score: 1,
1611
- evidence: ["matched target or direct dependency"],
1612
- reason: "required by CodeGraph evidence",
1613
- })),
1614
- ...rankedOptionalFiles.map(candidate => ({
1615
- path: candidate.path,
1616
- layer: "codegraph",
1617
- score: candidate.score,
1618
- evidence: candidate.evidence,
1619
- reason: "optional file survived graph/query evidence",
1620
- })),
1621
- ],
132
+ layers: [{
133
+ layer: 'codegraph', score: codeConfidence(selected),
134
+ evidence: [`${required.length} strong, ${optional.filter(item => item.strength === 'supporting').length} supporting, ${optional.filter(item => item.strength === 'weak').length} weak candidates`],
135
+ }],
136
+ files: selected.map(candidate => ({
137
+ path: String(candidate.payload.path ?? candidate.evidence.sourceId),
138
+ layer: 'codegraph', score: strengthScore(candidate.strength),
139
+ evidence: candidate.contributingLegs.map(leg => leg.leg),
140
+ reason: `${candidate.strength} typed code evidence`,
141
+ })),
1622
142
  },
1623
143
  };
1624
- result.taskEnvelope = buildTaskContextEnvelope({
1625
- query,
1626
- result,
1627
- projectRoot: this.cg.getProjectRoot?.() ?? process.cwd(),
1628
- sessionId: options?.sessionId,
1629
- executionId: options?.executionId,
1630
- requestId: options?.requestId,
1631
- host: options?.host,
1632
- command: options?.command,
1633
- turnId: options?.turnId,
1634
- intentArtifact: options?.intentArtifact,
1635
- queryPlan: options?.queryPlan,
1636
- tokenBudget: maxTokens,
1637
- targetAnchors,
1638
- fileRoles: fileRoleByPath,
1639
- fileFreshness: fileFreshnessByPath,
1640
- memoryHits,
1641
- knowledgeHits,
1642
- rejectedCodeRelations: edgeQuality.rejected.length,
1643
- taskPlan: retrievalTaskPlan,
1644
- });
1645
144
  return result;
1646
145
  }
1647
- scheduleSymbolVectorBuild() {
1648
- const warnings = [];
1649
- try {
1650
- if (this.symbolEmbedder) {
1651
- void this.symbolEmbedder.buildAllIfNeeded().catch(() => {
1652
- // Status is retained by SymbolEmbedder and surfaced on the next request.
1653
- });
1654
- }
1655
- }
1656
- catch {
1657
- this.addSymbolVectorWarning(warnings);
1658
- }
1659
- this.isSymbolVectorReady(warnings);
1660
- return warnings;
1661
- }
1662
- async applyTaskSymbolVectorRecall(ctx, query, warnings) {
1663
- const embedder = this.embedder;
1664
- const symbolStore = this.symbolStore;
1665
- if (!embedder || !symbolStore || !this.isSymbolVectorReady(warnings)) {
1666
- return;
1667
- }
1668
- try {
1669
- const combinedQuery = mergeQueries(query, expandQuery(query));
1670
- const queryEmbedding = await embedder.embed(combinedQuery);
1671
- const vectorResults = symbolStore.search(queryEmbedding, 50);
1672
- if (vectorResults.length === 0)
1673
- return;
1674
- const currentNodes = new Map(ctx.nodes.map((node) => [node.id, node]));
1675
- const fused = rrfFusion(currentNodes, vectorResults, this.cg);
1676
- ctx.nodes = [...fused.values()];
1677
- ctx.entryPoints = [...fused.values()].slice(0, 3);
1678
- }
1679
- catch {
1680
- this.addSymbolVectorWarning(warnings);
1681
- }
1682
- }
1683
- async applyLegacySymbolVectorRecall(result, query, warnings) {
1684
- const vectorContext = {
1685
- nodes: [...result.nodes.values()],
1686
- entryPoints: result.roots
1687
- .map((id) => result.nodes.get(id))
1688
- .filter(Boolean),
1689
- };
1690
- await this.applyTaskSymbolVectorRecall(vectorContext, query, warnings);
1691
- result.nodes = new Map(vectorContext.nodes.map((node) => [node.id, node]));
1692
- result.roots = vectorContext.entryPoints.map((node) => node.id);
1693
- }
1694
- isSymbolVectorReady(warnings) {
1695
- try {
1696
- const ready = this.embedder?.getStatus() === "ready"
1697
- && this.symbolEmbedder?.getStatus().state === "ready";
1698
- if (!ready)
1699
- this.addSymbolVectorWarning(warnings);
1700
- return ready;
1701
- }
1702
- catch {
1703
- this.addSymbolVectorWarning(warnings);
1704
- return false;
1705
- }
1706
- }
1707
- addSymbolVectorWarning(warnings) {
1708
- if (!warnings.includes(SYMBOL_VECTOR_WARNING)) {
1709
- warnings.push(SYMBOL_VECTOR_WARNING);
1710
- }
1711
- }
1712
- clearRecoveredSymbolWarning(warnings) {
1713
- const currentStatus = [];
1714
- if (!this.isSymbolVectorReady(currentStatus))
1715
- return;
1716
- const index = warnings.indexOf(SYMBOL_VECTOR_WARNING);
1717
- if (index >= 0)
1718
- warnings.splice(index, 1);
1719
- }
1720
- getStatus() {
1721
- const stats = this.cg.getStats();
1722
- return {
1723
- totalFiles: stats.fileCount,
1724
- totalSymbols: stats.nodeCount,
1725
- lastIndexTime: stats.lastUpdated,
1726
- indexing: this.cg.isIndexing(),
1727
- };
146
+ }
147
+ function directCodeQuery(query, options) {
148
+ return {
149
+ query: query.slice(0, 2_000),
150
+ targetAnchors: options.relevantFiles ?? [],
151
+ symbols: [],
152
+ relationKinds: ['dependency'],
153
+ maxDepth: options.mode === 'deep' ? 3 : 1,
154
+ };
155
+ }
156
+ function taskTypeFromProjection(options) {
157
+ const intent = options.intentArtifact?.intent;
158
+ return intent === 'debug' || intent === 'feature' || intent === 'refactor'
159
+ || intent === 'understand' || intent === 'review' ? intent : 'unknown';
160
+ }
161
+ function group(candidates, reasoning) {
162
+ const files = [...new Set(candidates.map(candidate => String(candidate.payload.path ?? '')))].filter(Boolean);
163
+ const symbols = [...new Set(candidates.map(candidate => String(candidate.payload.symbol ?? '')).filter(Boolean))];
164
+ return { files, symbols, reasoning: reasoning.filter(item => files.includes(item.fileGroup)) };
165
+ }
166
+ function packCandidates(candidates, tokenBudget) {
167
+ const allocation = allocateContextFileBudget({
168
+ requiredPaths: candidates
169
+ .filter(candidate => candidate.strength === 'strong')
170
+ .map(candidate => String(candidate.payload.path ?? candidate.evidence.sourceId)),
171
+ candidates: candidates.map(candidate => ({
172
+ path: String(candidate.payload.path ?? candidate.evidence.sourceId),
173
+ score: strengthScore(candidate.strength),
174
+ source: candidate.contributingLegs.some(leg => /graph/iu.test(leg.leg)) ? 'graph' : 'structural',
175
+ hop: candidate.contributingLegs.some(leg => /graph/iu.test(leg.leg)) ? 1 : undefined,
176
+ })),
177
+ tokenBudget,
178
+ });
179
+ const sorted = [...candidates].sort((left, right) => strengthScore(right.strength) - strengthScore(left.strength)
180
+ || left.rank - right.rank
181
+ || left.stableRef.localeCompare(right.stableRef));
182
+ const result = [];
183
+ let used = 0;
184
+ for (const candidate of sorted) {
185
+ const path = String(candidate.payload.path ?? candidate.evidence.sourceId);
186
+ if (!allocation.selectedPaths.has(path))
187
+ continue;
188
+ const cost = Math.max(1, candidate.estimatedTokens ?? 128);
189
+ if (used + cost > tokenBudget)
190
+ continue;
191
+ result.push(candidate);
192
+ used += cost;
1728
193
  }
194
+ return result;
1729
195
  }
1730
- function getGroupFilePaths(group) {
1731
- if (Array.isArray(group))
1732
- return group;
1733
- return group?.files ?? [];
196
+ function dedupeCandidates(candidates) {
197
+ return [...new Map(candidates.map(candidate => [candidate.stableRef, candidate])).values()];
198
+ }
199
+ function toSnippet(candidate) {
200
+ const symbol = String(candidate.payload.symbol ?? 'symbol');
201
+ const kind = String(candidate.payload.kind ?? 'unknown');
202
+ return {
203
+ path: String(candidate.payload.path ?? candidate.evidence.sourceId),
204
+ symbols: [symbol],
205
+ snippet: `// ${kind}: ${symbol}`,
206
+ score: strengthScore(candidate.strength),
207
+ };
208
+ }
209
+ function codeConfidence(candidates) {
210
+ if (candidates.some(candidate => candidate.strength === 'strong'))
211
+ return 0.9;
212
+ if (candidates.some(candidate => candidate.strength === 'supporting'))
213
+ return 0.65;
214
+ if (candidates.length > 0)
215
+ return 0.35;
216
+ return 0;
217
+ }
218
+ function strengthScore(value) {
219
+ return value === 'strong' ? 1 : value === 'supporting' ? 0.65 : 0.3;
220
+ }
221
+ function projectId(codegraph) {
222
+ return `project:${codegraph.getProjectRoot?.() ?? process.cwd()}`;
223
+ }
224
+ function generateRiskHints(paths) {
225
+ const rules = [
226
+ { pattern: /(?:^|\/)(?:main|index|app|entry|bootstrap)\.[^/]+$/iu, level: 'high', category: 'main_flow' },
227
+ { pattern: /(?:config|settings?|env|constants?)/iu, level: 'medium', category: 'config' },
228
+ { pattern: /(?:store|state|provider|redux)/iu, level: 'high', category: 'global_state' },
229
+ { pattern: /(?:api|route|controller|handler|service|middleware)/iu, level: 'medium', category: 'api_contract' },
230
+ ];
231
+ return rules.flatMap(rule => {
232
+ const affectedFiles = paths.filter(path => rule.pattern.test(path));
233
+ return affectedFiles.length > 0 ? [{
234
+ level: rule.level, category: rule.category,
235
+ description: `${rule.category.replaceAll('_', ' ')} code evidence`, affectedFiles,
236
+ }] : [];
237
+ });
1734
238
  }
1735
- function countValues(values) {
1736
- const counts = {};
1737
- for (const value of values)
1738
- counts[value] = (counts[value] ?? 0) + 1;
1739
- return counts;
239
+ function reasonCodes(...groups) {
240
+ return [...new Set(groups.flat().map(value => value.slice(0, 160)))];
1740
241
  }
1741
242
  //# sourceMappingURL=context-engine.js.map