@devflow-tools/context-engine 0.17.8 → 0.18.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/dist/code-evidence-producer.d.ts +29 -0
- package/dist/code-evidence-producer.d.ts.map +1 -0
- package/dist/code-evidence-producer.js +522 -0
- package/dist/code-evidence-producer.js.map +1 -0
- package/dist/code-evidence-types.d.ts +26 -0
- package/dist/code-evidence-types.d.ts.map +1 -0
- package/dist/code-evidence-types.js +2 -0
- package/dist/code-evidence-types.js.map +1 -0
- package/dist/compressor.d.ts.map +1 -1
- package/dist/compressor.js +2 -3
- package/dist/compressor.js.map +1 -1
- package/dist/context-budget-allocator.d.ts +5 -1
- package/dist/context-budget-allocator.d.ts.map +1 -1
- package/dist/context-budget-allocator.js +25 -3
- package/dist/context-budget-allocator.js.map +1 -1
- package/dist/context-engine.d.ts +19 -43
- package/dist/context-engine.d.ts.map +1 -1
- package/dist/context-engine.js +203 -1702
- package/dist/context-engine.js.map +1 -1
- package/dist/create-engines.d.ts +15 -19
- package/dist/create-engines.d.ts.map +1 -1
- package/dist/create-engines.js +39 -106
- package/dist/create-engines.js.map +1 -1
- package/dist/index.d.ts +7 -22
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -15
- package/dist/index.js.map +1 -1
- package/dist/symbol-embedder.d.ts +1 -1
- package/dist/symbol-embedder.d.ts.map +1 -1
- package/dist/symbol-embedder.js +17 -12
- package/dist/symbol-embedder.js.map +1 -1
- package/dist/symbol-embedding-document.d.ts +4 -0
- package/dist/symbol-embedding-document.d.ts.map +1 -0
- package/dist/symbol-embedding-document.js +39 -0
- package/dist/symbol-embedding-document.js.map +1 -0
- package/dist/symbol-vector-store.d.ts +13 -1
- package/dist/symbol-vector-store.d.ts.map +1 -1
- package/dist/symbol-vector-store.js +45 -16
- package/dist/symbol-vector-store.js.map +1 -1
- package/dist/types.d.ts +1 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +12 -9
- package/dist/artifact-inventory.d.ts +0 -39
- package/dist/artifact-inventory.d.ts.map +0 -1
- package/dist/artifact-inventory.js +0 -224
- package/dist/artifact-inventory.js.map +0 -1
- package/dist/channel-query-planner.d.ts +0 -9
- package/dist/channel-query-planner.d.ts.map +0 -1
- package/dist/channel-query-planner.js +0 -92
- package/dist/channel-query-planner.js.map +0 -1
- package/dist/intent-parser.d.ts +0 -15
- package/dist/intent-parser.d.ts.map +0 -1
- package/dist/intent-parser.js +0 -388
- package/dist/intent-parser.js.map +0 -1
- package/dist/retrieval-gap-detector.d.ts +0 -10
- package/dist/retrieval-gap-detector.d.ts.map +0 -1
- package/dist/retrieval-gap-detector.js +0 -85
- package/dist/retrieval-gap-detector.js.map +0 -1
- package/dist/retrieval-handlers.d.ts +0 -73
- package/dist/retrieval-handlers.d.ts.map +0 -1
- package/dist/retrieval-handlers.js +0 -289
- package/dist/retrieval-handlers.js.map +0 -1
- package/dist/retrieval-ledger.d.ts +0 -3
- package/dist/retrieval-ledger.d.ts.map +0 -1
- package/dist/retrieval-ledger.js +0 -156
- package/dist/retrieval-ledger.js.map +0 -1
- package/dist/retrieval-session-controller.d.ts +0 -6
- package/dist/retrieval-session-controller.d.ts.map +0 -1
- package/dist/retrieval-session-controller.js +0 -372
- package/dist/retrieval-session-controller.js.map +0 -1
- package/dist/retrieval-task-plan.d.ts +0 -28
- package/dist/retrieval-task-plan.d.ts.map +0 -1
- package/dist/retrieval-task-plan.js +0 -232
- package/dist/retrieval-task-plan.js.map +0 -1
- package/dist/semantic-authority-merger.d.ts +0 -11
- package/dist/semantic-authority-merger.d.ts.map +0 -1
- package/dist/semantic-authority-merger.js +0 -176
- package/dist/semantic-authority-merger.js.map +0 -1
- package/dist/semantic-resolver.d.ts +0 -19
- package/dist/semantic-resolver.d.ts.map +0 -1
- package/dist/semantic-resolver.js +0 -49
- package/dist/semantic-resolver.js.map +0 -1
- package/dist/semantic-route-catalog.d.ts +0 -22
- package/dist/semantic-route-catalog.d.ts.map +0 -1
- package/dist/semantic-route-catalog.js +0 -53
- package/dist/semantic-route-catalog.js.map +0 -1
- package/dist/semantic-router.d.ts +0 -39
- package/dist/semantic-router.d.ts.map +0 -1
- package/dist/semantic-router.js +0 -153
- package/dist/semantic-router.js.map +0 -1
- package/dist/semantic-structural-evidence.d.ts +0 -20
- package/dist/semantic-structural-evidence.d.ts.map +0 -1
- package/dist/semantic-structural-evidence.js +0 -84
- package/dist/semantic-structural-evidence.js.map +0 -1
- package/dist/task-retrieval-orchestrator.d.ts +0 -46
- package/dist/task-retrieval-orchestrator.d.ts.map +0 -1
- package/dist/task-retrieval-orchestrator.js +0 -418
- package/dist/task-retrieval-orchestrator.js.map +0 -1
package/dist/context-engine.js
CHANGED
|
@@ -1,1741 +1,242 @@
|
|
|
1
|
-
import
|
|
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
|
-
*
|
|
55
|
-
*
|
|
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
|
-
|
|
281
|
-
data;
|
|
282
|
-
graph;
|
|
283
|
-
indexer;
|
|
284
|
-
memory;
|
|
285
|
-
knowledge;
|
|
286
|
-
embedder;
|
|
11
|
+
dependencies;
|
|
287
12
|
symbolStore;
|
|
288
|
-
|
|
289
|
-
constructor(
|
|
290
|
-
this.
|
|
291
|
-
this.
|
|
292
|
-
this.
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
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
|
-
|
|
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
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
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
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
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
|
-
|
|
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
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
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
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
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
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
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
|
|
1303
|
-
const
|
|
1304
|
-
const
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
const
|
|
1308
|
-
const
|
|
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
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
});
|
|
1402
|
-
const
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
const
|
|
1406
|
-
const
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
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
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
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
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
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
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
result.
|
|
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
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
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
|
|
1736
|
-
|
|
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
|