@devflow-tools/context-engine 0.9.0 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,15 @@
1
+ import type { LLMClient } from '@devflow-tools/sdk';
2
+ export interface RerankCandidate {
3
+ id: string;
4
+ name: string;
5
+ snippet: string;
6
+ file?: string;
7
+ score?: number;
8
+ }
9
+ export declare function setRerankLLMClient(client: LLMClient): void;
10
+ export declare function isRerankAvailable(): boolean;
11
+ export declare function claudeRerank(query: string, candidates: RerankCandidate[], options?: {
12
+ topK?: number;
13
+ model?: string;
14
+ }): Promise<RerankCandidate[]>;
15
+ //# sourceMappingURL=claude-rerank.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"claude-rerank.d.ts","sourceRoot":"","sources":["../src/claude-rerank.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAC;AAKpD,MAAM,WAAW,eAAe;IAC9B,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AASD,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,SAAS,GAAG,IAAI,CAE1D;AAED,wBAAgB,iBAAiB,IAAI,OAAO,CAE3C;AAwCD,wBAAsB,YAAY,CAChC,KAAK,EAAE,MAAM,EACb,UAAU,EAAE,eAAe,EAAE,EAC7B,OAAO,CAAC,EAAE;IAAE,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,GAC1C,OAAO,CAAC,eAAe,EAAE,CAAC,CAsC5B"}
@@ -0,0 +1,93 @@
1
+ const TIMEOUT_MS = 2000;
2
+ const CACHE_TTL = 5 * 60 * 1000;
3
+ const RERANK_SYSTEM_PROMPT = `You are a code search relevance ranker. Given a user query and
4
+ a list of code search results, return the indices of the most relevant results
5
+ in order of relevance. Only include results that are genuinely relevant to the query.
6
+ Respond with ONLY a JSON array of indices, like [3, 0, 7, 1]. Do not explain.`;
7
+ let llmClient = null;
8
+ export function setRerankLLMClient(client) {
9
+ llmClient = client;
10
+ }
11
+ export function isRerankAvailable() {
12
+ return llmClient?.isAvailable ?? false;
13
+ }
14
+ // LRU cache
15
+ const cache = new Map();
16
+ function cacheGet(query) {
17
+ const key = query.toLowerCase().trim();
18
+ const entry = cache.get(key);
19
+ if (entry && Date.now() - entry.ts < CACHE_TTL)
20
+ return entry.indices;
21
+ cache.delete(key);
22
+ return null;
23
+ }
24
+ // Concurrency semaphore
25
+ let activeCalls = 0;
26
+ const MAX_CONCURRENT = 2;
27
+ async function acquireSemaphore() {
28
+ const start = Date.now();
29
+ while (activeCalls >= MAX_CONCURRENT) {
30
+ if (Date.now() - start > 5000)
31
+ return false;
32
+ await new Promise(r => setTimeout(r, 100));
33
+ }
34
+ activeCalls++;
35
+ return true;
36
+ }
37
+ function releaseSemaphore() { activeCalls = Math.max(0, activeCalls - 1); }
38
+ // Multi-pattern response parser
39
+ function parseIndices(text, maxIndex) {
40
+ const patterns = [
41
+ t => { try {
42
+ const v = JSON.parse(t);
43
+ if (Array.isArray(v) && v.every(x => typeof x === 'number'))
44
+ return v;
45
+ }
46
+ catch { } return null; },
47
+ t => { const m = t.match(/```(?:json)?\s*([\s\S]*?)```/); return m ? parseIndices(m[1], maxIndex) : null; },
48
+ t => { const m = t.match(/\[([0-9,\s]+)\]/); return m ? m[1].split(',').map(s => parseInt(s.trim())).filter(n => !isNaN(n) && n >= 0 && n < maxIndex) : null; },
49
+ ];
50
+ for (const p of patterns) {
51
+ const result = p(text);
52
+ if (result !== null && result.length > 0)
53
+ return result;
54
+ }
55
+ return null;
56
+ }
57
+ export async function claudeRerank(query, candidates, options) {
58
+ const topK = options?.topK ?? 10;
59
+ if (candidates.length <= topK)
60
+ return candidates;
61
+ const cached = cacheGet(query);
62
+ if (cached)
63
+ return cached.map(i => candidates[i]).filter(Boolean).slice(0, topK);
64
+ if (!llmClient?.isAvailable) {
65
+ return candidates.slice(0, topK);
66
+ }
67
+ if (!(await acquireSemaphore())) {
68
+ return candidates.slice(0, topK);
69
+ }
70
+ try {
71
+ const candidatesJson = candidates.map((c, i) => ({
72
+ index: i, id: c.id, name: c.name,
73
+ snippet: c.snippet.slice(0, 150),
74
+ file: c.file ?? '',
75
+ }));
76
+ const response = await llmClient.chat({
77
+ systemPrompt: RERANK_SYSTEM_PROMPT,
78
+ messages: [{ role: 'user', content: `Query: ${query}\n\nCandidates:\n${JSON.stringify(candidatesJson)}` }],
79
+ maxTokens: 256,
80
+ });
81
+ const indices = parseIndices(response.content.trim(), candidates.length);
82
+ if (indices) {
83
+ cache.set(query.toLowerCase().trim(), { indices, ts: Date.now() });
84
+ return indices.map(i => candidates[i]).filter(Boolean).slice(0, topK);
85
+ }
86
+ }
87
+ catch { /* silent fallback */ }
88
+ finally {
89
+ releaseSemaphore();
90
+ }
91
+ return candidates.slice(0, topK);
92
+ }
93
+ //# sourceMappingURL=claude-rerank.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"claude-rerank.js","sourceRoot":"","sources":["../src/claude-rerank.ts"],"names":[],"mappings":"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"}
@@ -1,29 +1,37 @@
1
1
  import type { ContextRequest, ContextResult, IndexStatus, IDataProvider, TaskContextResult } from "@devflow-tools/sdk";
2
- import type { MemoryEngine } from "@devflow-tools/memory-engine";
2
+ import type { MemoryEngine, EmbeddingProvider } from "@devflow-tools/memory-engine";
3
3
  import type { KnowledgeEngine } from "@devflow-tools/knowledge-engine";
4
4
  import type { GraphAdapter } from "./graph-adapter.js";
5
+ import type { IndexerAdapter } from "./indexer-adapter.js";
5
6
  import type { CodeGraph } from "@colbymchenry/codegraph";
6
7
  export interface ContextEngineDeps {
7
8
  cg: CodeGraph;
8
9
  data: IDataProvider;
9
10
  graph: GraphAdapter;
11
+ indexer: IndexerAdapter;
10
12
  memory: MemoryEngine;
11
13
  knowledge: KnowledgeEngine;
14
+ embedder?: EmbeddingProvider;
12
15
  }
13
16
  export declare class ContextEngine {
14
17
  private cg;
15
18
  private data;
16
19
  private graph;
20
+ private indexer;
17
21
  private memory;
18
22
  private knowledge;
23
+ private embedder?;
19
24
  constructor(deps: ContextEngineDeps);
25
+ private queryCache;
20
26
  index(_projectRoot: string): Promise<void>;
21
27
  build(request: ContextRequest): Promise<ContextResult>;
22
28
  private logAccuracyQuery;
23
29
  buildTaskContext(query: string, options?: {
24
30
  maxTokens?: number;
25
31
  mode?: "fast" | "deep";
32
+ sessionId?: string;
26
33
  }): Promise<TaskContextResult>;
34
+ private buildTaskContextLegacy;
27
35
  getStatus(): IndexStatus;
28
36
  }
29
37
  //# sourceMappingURL=context-engine.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"context-engine.d.ts","sourceRoot":"","sources":["../src/context-engine.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,cAAc,EACd,aAAa,EAEb,WAAW,EACX,aAAa,EACb,iBAAiB,EAMlB,MAAM,oBAAoB,CAAC;AAC5B,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,8BAA8B,CAAC;AACjE,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,iCAAiC,CAAC;AACvE,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AACvD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yBAAyB,CAAC;AAyHzD,MAAM,WAAW,iBAAiB;IAChC,EAAE,EAAE,SAAS,CAAC;IACd,IAAI,EAAE,aAAa,CAAC;IACpB,KAAK,EAAE,YAAY,CAAC;IACpB,MAAM,EAAE,YAAY,CAAC;IACrB,SAAS,EAAE,eAAe,CAAC;CAC5B;AAED,qBAAa,aAAa;IACxB,OAAO,CAAC,EAAE,CAAY;IACtB,OAAO,CAAC,IAAI,CAAgB;IAC5B,OAAO,CAAC,KAAK,CAAe;IAC5B,OAAO,CAAC,MAAM,CAAe;IAC7B,OAAO,CAAC,SAAS,CAAkB;gBAEvB,IAAI,EAAE,iBAAiB;IAQ7B,KAAK,CAAC,YAAY,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAI1C,KAAK,CAAC,OAAO,EAAE,cAAc,GAAG,OAAO,CAAC,aAAa,CAAC;YA2N9C,gBAAgB;IAexB,gBAAgB,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE;QAAE,SAAS,CAAC,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,GAAG,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,iBAAiB,CAAC;IAkL3H,SAAS,IAAI,WAAW;CASzB"}
1
+ {"version":3,"file":"context-engine.d.ts","sourceRoot":"","sources":["../src/context-engine.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,cAAc,EACd,aAAa,EAEb,WAAW,EACX,aAAa,EACb,iBAAiB,EAMlB,MAAM,oBAAoB,CAAC;AAC5B,OAAO,KAAK,EAAE,YAAY,EAAE,iBAAiB,EAAE,MAAM,8BAA8B,CAAC;AACpF,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,iCAAiC,CAAC;AACvE,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AACvD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAC3D,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yBAAyB,CAAC;AAqLzD,MAAM,WAAW,iBAAiB;IAChC,EAAE,EAAE,SAAS,CAAC;IACd,IAAI,EAAE,aAAa,CAAC;IACpB,KAAK,EAAE,YAAY,CAAC;IACpB,OAAO,EAAE,cAAc,CAAC;IACxB,MAAM,EAAE,YAAY,CAAC;IACrB,SAAS,EAAE,eAAe,CAAC;IAC3B,QAAQ,CAAC,EAAE,iBAAiB,CAAC;CAC9B;AAED,qBAAa,aAAa;IACxB,OAAO,CAAC,EAAE,CAAY;IACtB,OAAO,CAAC,IAAI,CAAgB;IAC5B,OAAO,CAAC,KAAK,CAAe;IAC5B,OAAO,CAAC,OAAO,CAAiB;IAChC,OAAO,CAAC,MAAM,CAAe;IAC7B,OAAO,CAAC,SAAS,CAAkB;IACnC,OAAO,CAAC,QAAQ,CAAC,CAAoB;gBAEzB,IAAI,EAAE,iBAAiB;IAUnC,OAAO,CAAC,UAAU,CAAkD;IAE9D,KAAK,CAAC,YAAY,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAI1C,KAAK,CAAC,OAAO,EAAE,cAAc,GAAG,OAAO,CAAC,aAAa,CAAC;YAqP9C,gBAAgB;IAexB,gBAAgB,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE;QAAE,SAAS,CAAC,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;QAAC,SAAS,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,iBAAiB,CAAC;YAoOjI,sBAAsB;IAyLpC,SAAS,IAAI,WAAW;CASzB"}
@@ -2,6 +2,8 @@ import CGModule from "@colbymchenry/codegraph";
2
2
  import { parseIntent, expandQuery } from "./intent-parser.js";
3
3
  import { fuzzySearch, shouldFallback } from "./fuzzy-fallback.js";
4
4
  import { compressToTokenLimit } from "./compressor.js";
5
+ import { claudeRerank } from "./claude-rerank.js";
6
+ import { QueryCache } from "./query-cache.js";
5
7
  const CG = CGModule.default ?? CGModule;
6
8
  /**
7
9
  * Merge the original query with L1 dict expansion and Agent-provided English terms.
@@ -19,6 +21,45 @@ function mergeQueries(original, l1, agentTerms) {
19
21
  }
20
22
  return [...terms].join(" ");
21
23
  }
24
+ function sigmoid(x, midpoint = 0.5, steepness = 6) {
25
+ return 1 / (1 + Math.exp(-steepness * (x - midpoint)));
26
+ }
27
+ function calculateConfidence(input) {
28
+ // CodeGraph score: based on provided confidence, node count, and structural signals
29
+ let cgScore = typeof input.cgConfidence === "number" ? input.cgConfidence : 0;
30
+ if (cgScore === 0 && input.nodeCount > 0) {
31
+ // Derive from node count when explicit confidence unavailable
32
+ cgScore = sigmoid(Math.min(input.nodeCount, 30) / 30, 0.3, 8);
33
+ }
34
+ // Bonus for structural richness
35
+ if (input.hasEdges)
36
+ cgScore = Math.min(1, cgScore + 0.1);
37
+ if (input.hasExactMatches)
38
+ cgScore = Math.min(1, cgScore + 0.15);
39
+ // Knowledge score: diminishing returns after 5 hits
40
+ const knowledgeScore = sigmoid(input.knowledgeHitCount / 10, 0.3, 5);
41
+ // Memory score: diminishing returns
42
+ const memoryScore = sigmoid(input.memoryHitCount / 10, 0.25, 5);
43
+ // Node count score
44
+ const nodeScore = sigmoid(input.nodeCount / 30, 0.3, 5);
45
+ // Intent confidence
46
+ const intentScore = input.intentConfidence;
47
+ const weighted = cgScore * 0.35 +
48
+ knowledgeScore * 0.25 +
49
+ memoryScore * 0.2 +
50
+ nodeScore * 0.1 +
51
+ intentScore * 0.1;
52
+ let confidence = weighted;
53
+ // Diversity bonus: all five signals present
54
+ const activeSignals = [cgScore > 0, input.knowledgeHitCount > 0, input.memoryHitCount > 0, input.nodeCount > 0, input.intentConfidence > 0].filter(Boolean).length;
55
+ if (activeSignals >= 4)
56
+ confidence += 0.05;
57
+ // Penalty: very low node count with no other signals
58
+ if (input.nodeCount < 3 && input.knowledgeHitCount === 0 && input.memoryHitCount === 0) {
59
+ confidence *= 0.5;
60
+ }
61
+ return Math.round(Math.min(1, Math.max(0, confidence)) * 100) / 100;
62
+ }
22
63
  function getAdaptiveParams(fileCount) {
23
64
  if (fileCount <= 200)
24
65
  return { depth: 1, searchLimit: 5, maxNodes: 10 };
@@ -103,17 +144,22 @@ export class ContextEngine {
103
144
  cg;
104
145
  data;
105
146
  graph;
147
+ indexer;
106
148
  memory;
107
149
  knowledge;
150
+ embedder;
108
151
  constructor(deps) {
109
152
  this.cg = deps.cg;
110
153
  this.data = deps.data;
111
154
  this.graph = deps.graph;
155
+ this.indexer = deps.indexer;
112
156
  this.memory = deps.memory;
113
157
  this.knowledge = deps.knowledge;
158
+ this.embedder = deps.embedder;
114
159
  }
160
+ queryCache = new QueryCache(60_000, 100);
115
161
  async index(_projectRoot) {
116
- await this.cg.indexAll();
162
+ await this.indexer.reconcile();
117
163
  }
118
164
  async build(request) {
119
165
  const maxTokens = request.maxTokens ?? 8000;
@@ -147,37 +193,66 @@ export class ContextEngine {
147
193
  }
148
194
  }
149
195
  }
150
- // 2. Knowledge search (DevFlow unique)
151
- let knowledgeHits = [];
152
- try {
153
- const kResults = await this.knowledge.search(request.query, { limit: 10 });
154
- knowledgeHits = kResults.map((k) => ({
155
- title: k.title,
156
- content: k.content,
157
- source: k.source,
158
- }));
159
- }
160
- catch {
161
- // Knowledge engine may not have data yet
162
- }
163
- // 3. Memory context (DevFlow unique)
164
- let memoryContext;
165
- try {
166
- const mem = await this.memory.getAll();
167
- memoryContext = {
168
- project: mem.project,
169
- preferences: mem.user.preferences,
170
- };
196
+ // Embedding reranking (if available)
197
+ if (this.embedder?.isReady()) {
198
+ try {
199
+ const nodesList = Array.from(cgResult.nodes.values());
200
+ const nodeTexts = nodesList.map(n => `${n.name} ${n.signature ?? ''} ${n.kind} ${n.filePath}`);
201
+ const queryEmbedding = await this.embedder.embed(combinedQuery);
202
+ const nodeEmbeddings = await this.embedder.embedBatch(nodeTexts);
203
+ const scoredNodes = nodesList.map((node, i) => {
204
+ const nodeEmb = nodeEmbeddings[i];
205
+ let dot = 0, normA = 0, normB = 0;
206
+ for (let j = 0; j < queryEmbedding.length; j++) {
207
+ dot += queryEmbedding[j] * nodeEmb[j];
208
+ normA += queryEmbedding[j] * queryEmbedding[j];
209
+ normB += nodeEmb[j] * nodeEmb[j];
210
+ }
211
+ const score = dot / (Math.sqrt(normA) * Math.sqrt(normB));
212
+ return { node, score };
213
+ });
214
+ scoredNodes.sort((a, b) => b.score - a.score);
215
+ cgResult.nodes = new Map();
216
+ const topK = Math.min(scoredNodes.length, request.mode === "deep" ? 30 : adaptive.maxNodes);
217
+ for (const { node } of scoredNodes.slice(0, topK)) {
218
+ cgResult.nodes.set(node.id, node);
219
+ }
220
+ cgResult.roots = scoredNodes.slice(0, 3).map(s => s.node.id);
221
+ }
222
+ catch { /* fall back to BM25 ranking */ }
171
223
  }
172
- catch {
173
- // Memory may be empty
224
+ // Claude rerank (second pass, if available)
225
+ const allNodesList = Array.from(cgResult.nodes.values());
226
+ if (allNodesList.length > 30) {
227
+ try {
228
+ const reranked = await claudeRerank(combinedQuery, allNodesList.map(n => ({ id: n.id, name: n.name, snippet: n.signature ?? '', file: n.filePath })), { topK: 30 });
229
+ cgResult.nodes = new Map(reranked.map(r => [r.id, allNodesList.find(n => n.id === r.id)]));
230
+ cgResult.roots = reranked.slice(0, 3).map(r => r.id);
231
+ }
232
+ catch { /* keep embedding/bm25 ranking */ }
174
233
  }
234
+ // 2. Knowledge + Memory in parallel
235
+ const [kResultsRaw, memResultsRaw] = await Promise.all([
236
+ this.knowledge.search(request.query, { limit: 10 }).catch(() => []),
237
+ this.memory.search(request.query, { limit: 5 }).catch(() => []),
238
+ ]);
239
+ const knowledgeHits = kResultsRaw.map((k) => ({
240
+ title: k.title,
241
+ content: k.content,
242
+ source: k.source,
243
+ }));
244
+ const memoryContext = memResultsRaw.length > 0 ? {
245
+ relevant: memResultsRaw.map((r) => ({
246
+ content: r.entry.content,
247
+ category: r.entry.category,
248
+ score: r.score,
249
+ })),
250
+ } : undefined;
175
251
  // 4. Build file snippets, layer by quality
176
252
  const MIN_CORE_SCORE = 0.7;
177
253
  const MIN_WEAK_SCORE = 0.4;
178
254
  const L1_TOKEN_RATIO = 0.6;
179
255
  const L2_TOKEN_RATIO = 0.3;
180
- const EXTERNAL_MAX_RATIO = 0.1;
181
256
  const ESTIMATED_TOKENS_PER_CHAR = 0.25;
182
257
  function estimateTokens(text) {
183
258
  return Math.ceil(text.length * ESTIMATED_TOKENS_PER_CHAR);
@@ -241,7 +316,6 @@ export class ContextEngine {
241
316
  let remaining = maxTokens;
242
317
  const l1Cap = Math.floor(maxTokens * L1_TOKEN_RATIO);
243
318
  const l2Cap = Math.floor(maxTokens * L2_TOKEN_RATIO);
244
- const l3Cap = Math.floor(maxTokens * EXTERNAL_MAX_RATIO);
245
319
  // L1: by score descending
246
320
  let l1Used = 0;
247
321
  for (const f of l1Files.sort((a, b) => (b.score ?? 0) - (a.score ?? 0))) {
@@ -264,18 +338,6 @@ export class ContextEngine {
264
338
  remaining -= tokens;
265
339
  l2Used += tokens;
266
340
  }
267
- // L3: fill remaining (max 10% budget)
268
- let l3Used = 0;
269
- for (const f of l3Files) {
270
- const tokens = Math.min(estimateTokens(f.snippet), 200);
271
- if (remaining < 100)
272
- break;
273
- if (l3Used + tokens > l3Cap)
274
- continue;
275
- allFiles.push(f);
276
- remaining -= tokens;
277
- l3Used += tokens;
278
- }
279
341
  // fallback: if nothing made it in, push top L1
280
342
  if (allFiles.length === 0 && l1Files.length > 0) {
281
343
  allFiles.push(l1Files.sort((a, b) => (b.score ?? 0) - (a.score ?? 0))[0]);
@@ -310,19 +372,20 @@ export class ContextEngine {
310
372
  file: n.filePath,
311
373
  kind: n.kind,
312
374
  }));
313
- this.logAccuracyQuery(queryId, 'codegraph', request.query, topKResults).catch(() => { });
375
+ this.logAccuracyQuery(queryId, 'codegraph', request.query, topKResults, request.sessionId).catch(() => { });
314
376
  return {
315
377
  files: allFiles,
378
+ knowledgeFiles: l3Files.length > 0 ? l3Files : undefined,
316
379
  graph: graphOutput,
317
380
  memory: memoryContext,
318
381
  confidence: intent.confidence,
319
382
  tokenCount,
320
383
  };
321
384
  }
322
- async logAccuracyQuery(id, engine, query, topKResults) {
385
+ async logAccuracyQuery(id, engine, query, topKResults, sessionId) {
323
386
  try {
324
387
  const http = await import('node:http');
325
- const body = JSON.stringify({ id, sessionId: 'unknown', engine, query, topKResults });
388
+ const body = JSON.stringify({ id, sessionId: sessionId ?? 'unknown', engine, query, topKResults });
326
389
  const req = http.request({
327
390
  hostname: '127.0.0.1', port: 13337, path: '/api/telemetry/accuracy/query',
328
391
  method: 'POST', headers: { 'Content-Type': 'application/json', 'X-API-Key': 'dev-key-000000', 'Content-Length': Buffer.byteLength(body) },
@@ -335,6 +398,212 @@ export class ContextEngine {
335
398
  catch { /* best-effort logging */ }
336
399
  }
337
400
  async buildTaskContext(query, options) {
401
+ const maxTokens = options?.maxTokens ?? 8000;
402
+ const mode = options?.mode ?? "fast";
403
+ const cacheKey = `ctx:${query}:${mode}`;
404
+ const cached = this.queryCache.get(cacheKey);
405
+ if (cached)
406
+ return cached;
407
+ const intent = parseIntent(query);
408
+ const taskType = intent.intent;
409
+ const timing = {};
410
+ const t0 = Date.now();
411
+ const [cgResult, knowledgeHits, memoryHits] = await Promise.all([
412
+ Promise.race([
413
+ this.cg.buildContext(query, {
414
+ searchLimit: mode === "deep" ? 10 : 5,
415
+ traversalDepth: mode === "deep" ? 3 : 2,
416
+ maxNodes: mode === "deep" ? 50 : 20,
417
+ includeCode: false,
418
+ format: "json",
419
+ }),
420
+ new Promise((_, reject) => setTimeout(() => reject(new Error("buildContext timeout")), 15000)),
421
+ ]).catch((err) => {
422
+ console.error("[devflow] cg.buildContext failed, falling back:", err.message);
423
+ return null;
424
+ }),
425
+ Promise.race([
426
+ this.knowledge.search(query, { limit: 5 }),
427
+ new Promise((_, reject) => setTimeout(() => reject(new Error("knowledge timeout")), 5000)),
428
+ ]).catch((err) => {
429
+ console.error("[devflow] knowledge.search timeout:", err.message);
430
+ return [];
431
+ }),
432
+ Promise.race([
433
+ this.memory.search(query, { limit: 5 }),
434
+ new Promise((_, reject) => setTimeout(() => reject(new Error("memory timeout")), 5000)),
435
+ ]).catch((err) => {
436
+ console.error("[devflow] memory.search timeout:", err.message);
437
+ return [];
438
+ }),
439
+ ]);
440
+ timing["1_promise_all"] = Date.now() - t0;
441
+ console.error("[devflow:timing] Promise.all:", timing["1_promise_all"], "ms");
442
+ if (!cgResult) {
443
+ console.error("[devflow] buildContext failed, falling back to manual pipeline");
444
+ const result = await this.buildTaskContextLegacy(query, options);
445
+ result._devflow = { fallback: "buildContext-unavailable" };
446
+ return result;
447
+ }
448
+ // buildContext returns JSON string when format="json" — parse it
449
+ let ctx;
450
+ if (typeof cgResult === "string") {
451
+ try {
452
+ ctx = JSON.parse(cgResult);
453
+ }
454
+ catch {
455
+ console.error("[devflow] buildContext returned unparseable string, falling back");
456
+ return this.buildTaskContextLegacy(query, options);
457
+ }
458
+ }
459
+ else {
460
+ ctx = cgResult;
461
+ }
462
+ // Fallback: direct searchNodes for query terms that buildContext may have missed
463
+ // (e.g. hyphenated terms like "post-tool-use" get tokenized and lost in FTS noise)
464
+ const tFallback = Date.now();
465
+ const existingFiles = new Set((ctx.nodes ?? []).map((n) => n.filePath));
466
+ const existingIds = new Set((ctx.nodes ?? []).map((n) => n.id));
467
+ const queryTerms = query.split(/\s+/).filter(w => w.length > 2).sort((a, b) => b.length - a.length).slice(0, 3);
468
+ for (const term of queryTerms) {
469
+ const hits = this.cg.searchNodes(term, { limit: 3 });
470
+ for (const hit of hits) {
471
+ if (!existingFiles.has(hit.node.filePath)) {
472
+ const fileNodes = this.cg.getNodesInFile(hit.node.filePath);
473
+ let firstSymbol = null;
474
+ for (const fn of fileNodes) {
475
+ if (fn.kind === "file" || fn.kind === "import" || existingIds.has(fn.id))
476
+ continue;
477
+ ctx.nodes.push(fn);
478
+ existingIds.add(fn.id);
479
+ if (!firstSymbol)
480
+ firstSymbol = fn;
481
+ }
482
+ existingFiles.add(hit.node.filePath);
483
+ // Use a real symbol as entry point, not the file wrapper
484
+ if (firstSymbol) {
485
+ ctx.entryPoints.push(firstSymbol);
486
+ }
487
+ }
488
+ }
489
+ }
490
+ console.error("[devflow:timing] Fallback searchNodes loop:", Date.now() - tFallback, "ms");
491
+ timing["2_fallback_loop"] = Date.now() - tFallback;
492
+ const tSnippet = Date.now();
493
+ const allNodes = ctx.nodes ?? [];
494
+ const rootIds = new Set((ctx.entryPoints ?? []).map((ep) => ep.id));
495
+ const requiredFileSet = new Set();
496
+ const optionalFileSet = new Set();
497
+ const keySymbols = [];
498
+ const entryFileSet = new Set(ctx.entryPoints?.map((ep) => ep.filePath) ?? []);
499
+ for (const node of allNodes) {
500
+ const isDirect = rootIds.has(node.id) || entryFileSet.has(node.filePath);
501
+ (isDirect ? requiredFileSet : optionalFileSet).add(node.filePath);
502
+ keySymbols.push({
503
+ name: node.name,
504
+ file: node.filePath,
505
+ kind: node.kind,
506
+ relevance: isDirect ? "direct" : "indirect",
507
+ });
508
+ }
509
+ const cgReasoning = [{
510
+ source: "graph_root",
511
+ fileGroup: "codegraph-buildContext",
512
+ explanation: ctx.summary
513
+ ? `CodeGraph: ${ctx.summary}`
514
+ : `CodeGraph matched ${allNodes.length} nodes across ${requiredFileSet.size + optionalFileSet.size} files`,
515
+ }];
516
+ const knowledgeReasoning = knowledgeHits.map((k) => ({
517
+ source: "knowledge",
518
+ fileGroup: k.source ?? "knowledge",
519
+ explanation: `Knowledge entry "${k.title}" matched query`,
520
+ }));
521
+ const memoryReasoning = memoryHits.map((m) => ({
522
+ source: "memory",
523
+ fileGroup: m.entry?.category ?? "memory",
524
+ explanation: `Memory entry matched: ${String(m.entry?.content ?? "").slice(0, 100)}`,
525
+ }));
526
+ const allFileSnippets = [];
527
+ for (const fp of [...requiredFileSet].sort()) {
528
+ const nodesInFile = allNodes.filter(n => n.filePath === fp);
529
+ allFileSnippets.push({
530
+ path: fp,
531
+ snippet: nodesInFile.map(n => `// ${n.kind}: ${n.name}${n.signature ? ` ${n.signature}` : ""}`).join("\n"),
532
+ symbols: nodesInFile.map(n => n.name),
533
+ score: 1.0,
534
+ });
535
+ }
536
+ for (const fp of [...optionalFileSet].sort()) {
537
+ const nodesInFile = allNodes.filter(n => n.filePath === fp);
538
+ allFileSnippets.push({
539
+ path: fp,
540
+ snippet: nodesInFile.map(n => `// ${n.kind}: ${n.name}${n.signature ? ` ${n.signature}` : ""}`).join("\n"),
541
+ symbols: nodesInFile.map(n => n.name),
542
+ score: 0.5,
543
+ });
544
+ }
545
+ const compressed = compressToTokenLimit(allFileSnippets, maxTokens);
546
+ console.error("[devflow:timing] Snippet building + compress:", Date.now() - tSnippet, "ms");
547
+ timing["3_snippet_compress"] = Date.now() - tSnippet;
548
+ const riskHints = generateRiskHints(allNodes);
549
+ const nextActions = generateNextActions(taskType, keySymbols.slice(0, 5));
550
+ const result = {
551
+ taskType,
552
+ requiredFiles: {
553
+ files: [...requiredFileSet],
554
+ symbols: keySymbols.filter(s => s.relevance === "direct").map(s => s.name).slice(0, 30),
555
+ reasoning: cgReasoning,
556
+ },
557
+ optionalFiles: {
558
+ files: [...optionalFileSet],
559
+ symbols: keySymbols.filter(s => s.relevance === "indirect").map(s => s.name).slice(0, 30),
560
+ reasoning: [...knowledgeReasoning, ...memoryReasoning],
561
+ },
562
+ keySymbols: keySymbols
563
+ .sort((a, b) => {
564
+ // Direct before indirect
565
+ if (a.relevance === "direct" && b.relevance !== "direct")
566
+ return -1;
567
+ if (a.relevance !== "direct" && b.relevance === "direct")
568
+ return 1;
569
+ // Functions/classes before constants/variables
570
+ const kindScore = (k) => k === "function" || k === "method" || k === "class" ? 0 :
571
+ k === "component" || k === "interface" || k === "type" ? 1 :
572
+ k === "variable" || k === "constant" || k === "enum" ? 2 : 3;
573
+ return kindScore(a.kind) - kindScore(b.kind);
574
+ })
575
+ .slice(0, 20),
576
+ graphSummary: {
577
+ roots: [...rootIds],
578
+ traversalDepth: ctx.stats?.traversalDepth ?? 2,
579
+ relatedNodeCount: allNodes.length,
580
+ },
581
+ riskHints,
582
+ nextActions,
583
+ reasoning: [...cgReasoning, ...knowledgeReasoning, ...memoryReasoning],
584
+ confidence: calculateConfidence({
585
+ cgConfidence: ctx.stats?.confidence,
586
+ knowledgeHitCount: knowledgeHits.length,
587
+ memoryHitCount: memoryHits.length,
588
+ nodeCount: allNodes.length,
589
+ intentConfidence: intent.confidence,
590
+ hasEdges: Array.isArray(ctx.edges) && (ctx.edges.length > 0),
591
+ hasExactMatches: keySymbols.some(s => s.relevance === "direct"),
592
+ }),
593
+ tokenCount: compressed.tokenCount,
594
+ };
595
+ this.queryCache.set(cacheKey, result);
596
+ timing["0_total"] = Date.now() - t0;
597
+ console.error("[devflow:timing] buildTaskContext TOTAL:", timing["0_total"], "ms");
598
+ result._timing = timing;
599
+ const queryId = `acc:${Date.now()}:${Math.random().toString(36).slice(2, 9)}`;
600
+ const topKResults = keySymbols.slice(0, 10).map(s => ({
601
+ id: s.name, name: s.name, file: s.file, kind: s.kind,
602
+ }));
603
+ this.logAccuracyQuery(queryId, "codegraph", query, topKResults, options?.sessionId).catch(() => { });
604
+ return result;
605
+ }
606
+ async buildTaskContextLegacy(query, options) {
338
607
  const stats = this.cg.getStats();
339
608
  const adaptive = getAdaptiveParams(stats.fileCount ?? 500);
340
609
  const maxTokens = options?.maxTokens ?? 8000;
@@ -418,6 +687,12 @@ export class ContextEngine {
418
687
  kind: n.kind,
419
688
  relevance: (rootIds.has(n.id) ? "direct" : "indirect"),
420
689
  }));
690
+ // Accuracy logging
691
+ const queryId = `acc:${Date.now()}:${Math.random().toString(36).slice(2, 9)}`;
692
+ const topKResults = keySymbols.slice(0, 10).map(s => ({
693
+ id: s.name, name: s.name, file: s.file, kind: s.kind,
694
+ }));
695
+ this.logAccuracyQuery(queryId, 'codegraph', query, topKResults, options?.sessionId).catch(() => { });
421
696
  // Graph summary
422
697
  const graphSummary = {
423
698
  roots: cgResult.roots,