@devflow-tools/context-engine 0.16.26 → 0.16.28

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/dist/context-engine.d.ts.map +1 -1
  2. package/dist/context-engine.js +193 -23
  3. package/dist/context-engine.js.map +1 -1
  4. package/dist/context-file-roles.d.ts.map +1 -1
  5. package/dist/context-file-roles.js +16 -4
  6. package/dist/context-file-roles.js.map +1 -1
  7. package/dist/create-engines.d.ts.map +1 -1
  8. package/dist/create-engines.js +10 -8
  9. package/dist/create-engines.js.map +1 -1
  10. package/dist/graph-adapter.d.ts.map +1 -1
  11. package/dist/graph-adapter.js +21 -10
  12. package/dist/graph-adapter.js.map +1 -1
  13. package/dist/graph-edge-quality.d.ts +7 -0
  14. package/dist/graph-edge-quality.d.ts.map +1 -1
  15. package/dist/graph-edge-quality.js +52 -1
  16. package/dist/graph-edge-quality.js.map +1 -1
  17. package/dist/index.d.ts +2 -0
  18. package/dist/index.d.ts.map +1 -1
  19. package/dist/index.js +1 -0
  20. package/dist/index.js.map +1 -1
  21. package/dist/indexer-adapter.d.ts +23 -0
  22. package/dist/indexer-adapter.d.ts.map +1 -1
  23. package/dist/indexer-adapter.js +94 -0
  24. package/dist/indexer-adapter.js.map +1 -1
  25. package/dist/intent-parser.js +3 -3
  26. package/dist/intent-parser.js.map +1 -1
  27. package/dist/query-cache.d.ts +2 -0
  28. package/dist/query-cache.d.ts.map +1 -1
  29. package/dist/query-cache.js +27 -1
  30. package/dist/query-cache.js.map +1 -1
  31. package/dist/retrieval-task-plan.d.ts +19 -0
  32. package/dist/retrieval-task-plan.d.ts.map +1 -0
  33. package/dist/retrieval-task-plan.js +160 -0
  34. package/dist/retrieval-task-plan.js.map +1 -0
  35. package/dist/task-retrieval-orchestrator.d.ts +10 -0
  36. package/dist/task-retrieval-orchestrator.d.ts.map +1 -1
  37. package/dist/task-retrieval-orchestrator.js +23 -2
  38. package/dist/task-retrieval-orchestrator.js.map +1 -1
  39. package/package.json +8 -8
@@ -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,EAOd,MAAM,oBAAoB,CAAC;AAC5B,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,8BAA8B,CAAC;AACtE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AACxD,OAAO,KAAK,EAAE,eAAe,EAA0B,MAAM,iCAAiC,CAAC;AAC/F,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;AAOzD,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,0BAA0B,CAAC;AAClE,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAU3D,OAAO,KAAK,EAKV,kBAAkB,EAClB,+BAA+B,EAChC,MAAM,YAAY,CAAC;AAsSpB,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,aAAa,CAAC;IACtB,SAAS,EAAE,eAAe,CAAC;IAC3B,QAAQ,CAAC,EAAE,iBAAiB,CAAC;IAC7B,WAAW,CAAC,EAAE,iBAAiB,CAAC;IAChC,cAAc,CAAC,EAAE,cAAc,CAAC;CACjC;AAwBD,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,CAAgB;IAC9B,OAAO,CAAC,SAAS,CAAkB;IACnC,OAAO,CAAC,QAAQ,CAAC,CAAoB;IACrC,OAAO,CAAC,WAAW,CAAC,CAAoB;IACxC,OAAO,CAAC,cAAc,CAAC,CAAiB;gBAE5B,IAAI,EAAE,iBAAiB;IAYnC,KAAK,IAAI,IAAI;IAKb,OAAO,CAAC,UAAU,CAAkD;IAE9D,gCAAgC,CAAC,KAAK,EAAE;QAC5C,SAAS,EAAE,MAAM,EAAE,CAAC;QACpB,SAAS,CAAC,EAAE,MAAM,CAAC;QACnB,WAAW,CAAC,EAAE,MAAM,CAAC;QACrB,SAAS,CAAC,EAAE,MAAM,CAAC;QACnB,KAAK,EAAE,MAAM,CAAC;KACf,GAAG,OAAO,CAAC,IAAI,CAAC;IAIX,KAAK,CAAC,YAAY,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAIhD,OAAO,CAAC,yBAAyB;YAuBnB,yBAAyB;IAiCvC,OAAO,CAAC,eAAe;IAKjB,KAAK,CAAC,OAAO,EAAE,cAAc,GAAG,OAAO,CAAC,aAAa,CAAC;IAwQtD,gBAAgB,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,kBAAkB,GAAG,OAAO,CAAC,+BAA+B,CAAC;YAqe/F,sBAAsB;IAkSpC,OAAO,CAAC,yBAAyB;YAgBnB,2BAA2B;YA4B3B,6BAA6B;IAkB3C,OAAO,CAAC,mBAAmB;IAY3B,OAAO,CAAC,sBAAsB;IAM9B,OAAO,CAAC,2BAA2B;IAOnC,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,EAOd,MAAM,oBAAoB,CAAC;AAC5B,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,8BAA8B,CAAC;AACtE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AACxD,OAAO,KAAK,EAAE,eAAe,EAA0B,MAAM,iCAAiC,CAAC;AAC/F,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AACvD,OAAO,KAAK,EAAwB,cAAc,EAAE,MAAM,sBAAsB,CAAC;AACjF,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yBAAyB,CAAC;AAQzD,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,0BAA0B,CAAC;AAClE,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAU3D,OAAO,KAAK,EAKV,kBAAkB,EAClB,+BAA+B,EAChC,MAAM,YAAY,CAAC;AAsSpB,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,aAAa,CAAC;IACtB,SAAS,EAAE,eAAe,CAAC;IAC3B,QAAQ,CAAC,EAAE,iBAAiB,CAAC;IAC7B,WAAW,CAAC,EAAE,iBAAiB,CAAC;IAChC,cAAc,CAAC,EAAE,cAAc,CAAC;CACjC;AAoCD,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,CAAgB;IAC9B,OAAO,CAAC,SAAS,CAAkB;IACnC,OAAO,CAAC,QAAQ,CAAC,CAAoB;IACrC,OAAO,CAAC,WAAW,CAAC,CAAoB;IACxC,OAAO,CAAC,cAAc,CAAC,CAAiB;gBAE5B,IAAI,EAAE,iBAAiB;IAYnC,KAAK,IAAI,IAAI;IAKb,OAAO,CAAC,UAAU,CAAkD;IAE9D,gCAAgC,CAAC,KAAK,EAAE;QAC5C,SAAS,EAAE,MAAM,EAAE,CAAC;QACpB,SAAS,CAAC,EAAE,MAAM,CAAC;QACnB,WAAW,CAAC,EAAE,MAAM,CAAC;QACrB,SAAS,CAAC,EAAE,MAAM,CAAC;QACnB,KAAK,EAAE,MAAM,CAAC;KACf,GAAG,OAAO,CAAC,IAAI,CAAC;IAIX,KAAK,CAAC,YAAY,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAIhD,OAAO,CAAC,yBAAyB;YA4BnB,yBAAyB;IAiCvC,OAAO,CAAC,eAAe;IAKjB,KAAK,CAAC,OAAO,EAAE,cAAc,GAAG,OAAO,CAAC,aAAa,CAAC;IAmRtD,gBAAgB,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,kBAAkB,GAAG,OAAO,CAAC,+BAA+B,CAAC;YAwiB/F,sBAAsB;IAmWpC,OAAO,CAAC,yBAAyB;YAgBnB,2BAA2B;YA4B3B,6BAA6B;IAkB3C,OAAO,CAAC,mBAAmB;IAY3B,OAAO,CAAC,sBAAsB;IAM9B,OAAO,CAAC,2BAA2B;IAOnC,SAAS,IAAI,WAAW;CASzB"}
@@ -1,9 +1,10 @@
1
1
  import CGModule from "@colbymchenry/codegraph";
2
2
  import { parseIntent, expandQuery } from "./intent-parser.js";
3
+ import { buildRetrievalTaskPlan } from './retrieval-task-plan.js';
3
4
  import { fuzzySearch, shouldFallback } from "./fuzzy-fallback.js";
4
5
  import { compressToTokenLimit } from "./compressor.js";
5
6
  import { claudeRerank } from "./claude-rerank.js";
6
- import { QueryCache } from "./query-cache.js";
7
+ import { invalidateContextPrefetchCache, QueryCache } from "./query-cache.js";
7
8
  import { rankOptionalContextFiles, } from "./ranker.js";
8
9
  import { describeLayerEvidence, detectLanguageFromPath, } from "./layer-rules.js";
9
10
  import { reachableCodeGraphNodeIds, sanitizeCodeGraphEdges, } from './graph-edge-quality.js';
@@ -241,8 +242,21 @@ function extractTargetAnchors(query) {
241
242
  if (match[1])
242
243
  pathAnchors.add(match[1].replace(/^\.\//, '').replace(/\/+$/, '').toLowerCase());
243
244
  }
244
- if (pathAnchors.size > 0)
245
+ if (pathAnchors.size > 0) {
246
+ const directories = [...pathAnchors]
247
+ .filter(anchor => /\.[a-z0-9]+$/iu.test(anchor))
248
+ .map(anchor => anchor.split('/').slice(0, -1).join('/'))
249
+ .filter(Boolean);
250
+ const existingNames = new Set([...pathAnchors].map(anchor => anchor.split('/').at(-1)));
251
+ for (const match of query.matchAll(/\b([A-Za-z0-9_.-]+\.(?:[cm]?[jt]sx?|vue|svelte|json|ya?ml|md))\b/gu)) {
252
+ const fileName = match[1]?.toLowerCase();
253
+ if (!fileName || existingNames.has(fileName))
254
+ continue;
255
+ if (directories.length === 1)
256
+ pathAnchors.add(`${directories[0]}/${fileName}`);
257
+ }
245
258
  return [...pathAnchors];
259
+ }
246
260
  const anchors = new Set();
247
261
  for (const match of query.matchAll(/([a-zA-Z0-9]+(?:-[a-zA-Z0-9]+)+)\s+(?:component|module|file|组件|模块|文件)/gi)) {
248
262
  if (match[1])
@@ -289,7 +303,7 @@ export class ContextEngine {
289
303
  async index(_projectRoot) {
290
304
  await this.indexer.reconcile();
291
305
  }
292
- getKnowledgeSearchOptions(query, limit, options = {}) {
306
+ getKnowledgeSearchOptions(query, limit, options = {}, taskPlan, totalBudget = 8_000) {
293
307
  const stats = this.cg.getStats();
294
308
  const frameworks = typeof this.cg.getDetectedFrameworks === "function"
295
309
  ? this.cg.getDetectedFrameworks()
@@ -297,8 +311,11 @@ export class ContextEngine {
297
311
  const languages = Object.keys(stats.filesByLanguage ?? {});
298
312
  return {
299
313
  limit,
314
+ budgetTokens: Math.max(0, Math.floor(totalBudget * 0.25)),
300
315
  activeSkill: options.activeSkill,
301
316
  relevantFiles: options.relevantFiles,
317
+ action: taskPlan?.action,
318
+ knowledgeNeeds: taskPlan?.knowledgeNeeds,
302
319
  projectSignals: {
303
320
  frameworks,
304
321
  languages,
@@ -329,6 +346,12 @@ export class ContextEngine {
329
346
  const warnings = this.scheduleSymbolVectorBuild();
330
347
  const maxTokens = request.maxTokens ?? 8000;
331
348
  const intent = parseIntent(request.query);
349
+ const retrievalTaskPlan = buildRetrievalTaskPlan({
350
+ query: request.query,
351
+ parsedIntent: intent,
352
+ targetAnchors: request.relevantFiles,
353
+ activeSkill: request.activeSkill,
354
+ });
332
355
  const stats = this.cg.getStats();
333
356
  const adaptive = getAdaptiveParams(stats.fileCount ?? 500);
334
357
  const depth = request.mode === "deep" ? 3 : adaptive.depth;
@@ -418,10 +441,13 @@ export class ContextEngine {
418
441
  }
419
442
  // 2. Knowledge + Memory in parallel
420
443
  const [kResultsRaw, memResultsRaw] = await Promise.all([
421
- this.searchKnowledgeWithStatus(combinedQuery, this.getKnowledgeSearchOptions(combinedQuery, 10, { activeSkill: request.activeSkill, relevantFiles: request.relevantFiles })).catch(() => ({ entries: [], warnings: [] })),
444
+ this.searchKnowledgeWithStatus(combinedQuery, this.getKnowledgeSearchOptions(combinedQuery, 10, { activeSkill: request.activeSkill, relevantFiles: request.relevantFiles }, retrievalTaskPlan, maxTokens)).catch(() => ({ entries: [], warnings: [] })),
422
445
  this.memory.search(request.query, {
423
446
  limit: 5,
424
447
  purpose: intent.intent,
448
+ scope: this.cg.getProjectRoot?.() ?? process.cwd(),
449
+ action: retrievalTaskPlan.action,
450
+ requiredPolicyFacets: retrievalTaskPlan.requiredPolicyFacets,
425
451
  }).catch(() => []),
426
452
  ]);
427
453
  if (kResultsRaw.warnings.length > 0)
@@ -564,10 +590,29 @@ export class ContextEngine {
564
590
  const warnings = this.scheduleSymbolVectorBuild();
565
591
  const maxTokens = options?.maxTokens ?? 8000;
566
592
  const mode = options?.mode ?? "fast";
567
- const cacheKey = `ctx:${query}:${mode}:${options?.activeSkill ?? ""}:${(options?.relevantFiles ?? []).join(",")}`;
568
- const cached = warnings.length === 0 && !options?.requestId
593
+ const memoryRevision = this.memory.getRetrievalRevision?.() ?? 0;
594
+ const cacheKey = `ctx:${query}:${mode}:${options?.activeSkill ?? ""}:${(options?.relevantFiles ?? []).join(",")}:memory:${memoryRevision}`;
595
+ let cached = warnings.length === 0 && !options?.requestId
569
596
  ? this.queryCache.get(cacheKey)
570
597
  : undefined;
598
+ if (cached) {
599
+ const changed = typeof this.cg.getChangedFiles === 'function'
600
+ ? this.cg.getChangedFiles()
601
+ : {};
602
+ const changedPaths = [...(changed.added ?? []), ...(changed.modified ?? []), ...(changed.removed ?? [])];
603
+ const cachedPaths = [
604
+ ...getGroupFilePaths(cached.requiredFiles),
605
+ ...getGroupFilePaths(cached.optionalFiles),
606
+ ];
607
+ const freshness = this.indexer?.ensureFilesCurrent
608
+ ? await this.indexer.ensureFilesCurrent(cachedPaths, { refresh: false })
609
+ : null;
610
+ if (changedPaths.length > 0 || freshness?.files.some(file => !file.current)) {
611
+ this.queryCache.invalidatePaths(new Set([...changedPaths, ...(freshness?.stale ?? []), ...(freshness?.missing ?? [])]));
612
+ invalidateContextPrefetchCache(this.cg.getProjectRoot?.() ?? process.cwd());
613
+ cached = undefined;
614
+ }
615
+ }
571
616
  if (cached) {
572
617
  return {
573
618
  ...cached,
@@ -578,6 +623,13 @@ export class ContextEngine {
578
623
  const taskType = intent.intent;
579
624
  const combinedQuery = mergeQueries(query, expandQuery(query), options?.expandedTerms);
580
625
  const targetAnchors = extractTargetAnchors(query);
626
+ const retrievalTaskPlan = buildRetrievalTaskPlan({
627
+ query,
628
+ parsedIntent: intent,
629
+ targetAnchors,
630
+ activeSkill: options?.activeSkill,
631
+ command: options?.command,
632
+ });
581
633
  const timing = {};
582
634
  const t0 = Date.now();
583
635
  const [cgResult, knowledgeResult, memoryHits] = await Promise.all([
@@ -597,7 +649,7 @@ export class ContextEngine {
597
649
  })
598
650
  : Promise.resolve(null),
599
651
  Promise.race([
600
- this.searchKnowledgeWithStatus(combinedQuery, this.getKnowledgeSearchOptions(combinedQuery, 5, options)),
652
+ this.searchKnowledgeWithStatus(combinedQuery, this.getKnowledgeSearchOptions(combinedQuery, 5, options, retrievalTaskPlan, maxTokens)),
601
653
  new Promise((_, reject) => setTimeout(() => reject(new Error("knowledge timeout")), 5000)),
602
654
  ]).catch((err) => {
603
655
  console.error("[devflow] knowledge.search timeout:", err.message);
@@ -609,6 +661,9 @@ export class ContextEngine {
609
661
  limit: 5,
610
662
  purpose: taskType,
611
663
  recordAccess: false,
664
+ scope: this.cg.getProjectRoot?.() ?? process.cwd(),
665
+ action: retrievalTaskPlan.action,
666
+ requiredPolicyFacets: retrievalTaskPlan.requiredPolicyFacets,
612
667
  }),
613
668
  new Promise((_, reject) => setTimeout(() => reject(new Error("memory timeout")), 5000)),
614
669
  ]).catch((err) => {
@@ -678,6 +733,31 @@ export class ContextEngine {
678
733
  }
679
734
  console.error("[devflow:timing] Fallback searchNodes loop:", Date.now() - tFallback, "ms");
680
735
  timing["2_fallback_loop"] = Date.now() - tFallback;
736
+ const fileFreshnessByPath = new Map();
737
+ if (this.indexer?.ensureFilesCurrent) {
738
+ const candidatePaths = [...new Set([
739
+ ...ctx.nodes.map((node) => String(node.filePath ?? '')).filter(Boolean),
740
+ ...ctx.entryPoints.map((entry) => String(entry.filePath ?? '')).filter(Boolean),
741
+ ])];
742
+ const freshness = await this.indexer.ensureFilesCurrent(candidatePaths, { refresh: true, timeoutMs: 5_000 });
743
+ for (const file of freshness.files)
744
+ fileFreshnessByPath.set(file.path, file);
745
+ const rejectedPaths = new Set([...freshness.missing, ...freshness.stale]);
746
+ if (rejectedPaths.size > 0 || freshness.refreshed.length > 0) {
747
+ this.queryCache.invalidatePaths(new Set([...rejectedPaths, ...freshness.refreshed]));
748
+ invalidateContextPrefetchCache(this.cg.getProjectRoot?.() ?? process.cwd());
749
+ }
750
+ if (freshness.missing.length > 0)
751
+ warnings.push(`codegraph-missing-files:${freshness.missing.length}`);
752
+ if (freshness.stale.length > 0)
753
+ warnings.push(`codegraph-stale-degraded:${freshness.stale.length}`);
754
+ ctx.nodes = ctx.nodes.filter((node) => !rejectedPaths.has(node.filePath));
755
+ ctx.entryPoints = ctx.entryPoints.filter((entry) => !rejectedPaths.has(entry.filePath));
756
+ if (Array.isArray(ctx.edges) && rejectedPaths.size > 0) {
757
+ const retainedIds = new Set(ctx.nodes.map((node) => node.id));
758
+ ctx.edges = ctx.edges.filter((edge) => retainedIds.has(edge.source) && retainedIds.has(edge.target));
759
+ }
760
+ }
681
761
  const edgeQuality = sanitizeCodeGraphEdges(this.cg, Array.isArray(ctx.edges) ? ctx.edges : []);
682
762
  ctx.edges = edgeQuality.accepted;
683
763
  if (edgeQuality.rejected.length > 0) {
@@ -774,6 +854,9 @@ export class ContextEngine {
774
854
  })
775
855
  : [];
776
856
  for (const candidate of roleCandidates) {
857
+ if (this.indexer?.getFileFreshness) {
858
+ fileFreshnessByPath.set(candidate.path, this.indexer.getFileFreshness(candidate.path));
859
+ }
777
860
  fileRoleByPath.set(candidate.path, candidate);
778
861
  if (candidate.role === 'target') {
779
862
  requiredFileSet.add(candidate.path);
@@ -791,10 +874,21 @@ export class ContextEngine {
791
874
  });
792
875
  }
793
876
  }
794
- const rankedOptionalFiles = rankOptionalContextFiles([...optionalCandidates.values()], taskType);
795
- const rankedOptionalPaths = new Set(rankedOptionalFiles.map(candidate => candidate.path));
877
+ let rankedOptionalFiles = rankOptionalContextFiles([...optionalCandidates.values()], taskType);
796
878
  for (const candidate of rankedOptionalFiles)
797
879
  optionalFileSet.add(candidate.path);
880
+ if (this.indexer?.getFileFreshness) {
881
+ for (const path of [...requiredFileSet, ...optionalFileSet]) {
882
+ const freshness = fileFreshnessByPath.get(path) ?? this.indexer.getFileFreshness(path);
883
+ fileFreshnessByPath.set(path, freshness);
884
+ if (freshness.current)
885
+ continue;
886
+ requiredFileSet.delete(path);
887
+ optionalFileSet.delete(path);
888
+ fileRoleByPath.delete(path);
889
+ }
890
+ rankedOptionalFiles = rankedOptionalFiles.filter(candidate => optionalFileSet.has(candidate.path));
891
+ }
798
892
  const optionalCandidateByPath = new Map(rankedOptionalFiles.map(candidate => [candidate.path, candidate]));
799
893
  const retainedFiles = new Set([...requiredFileSet, ...optionalFileSet]);
800
894
  for (let index = keySymbols.length - 1; index >= 0; index--) {
@@ -845,7 +939,7 @@ export class ContextEngine {
845
939
  score: 0.5,
846
940
  });
847
941
  }
848
- const compressed = compressToTokenLimit(allFileSnippets, maxTokens);
942
+ const compressed = compressToTokenLimit(allFileSnippets, Math.floor(maxTokens * 0.45));
849
943
  if (compressed.overBudget)
850
944
  warnings.push("context-compressor-budget-exceeded");
851
945
  console.error("[devflow:timing] Snippet building + compress:", Date.now() - tSnippet, "ms");
@@ -979,7 +1073,9 @@ export class ContextEngine {
979
1073
  command: options?.command,
980
1074
  tokenBudget: maxTokens,
981
1075
  targetAnchors,
1076
+ taskPlan: retrievalTaskPlan,
982
1077
  fileRoles: fileRoleByPath,
1078
+ fileFreshness: fileFreshnessByPath,
983
1079
  memoryHits,
984
1080
  knowledgeHits,
985
1081
  rejectedCodeRelations: edgeQuality.rejected.length,
@@ -1029,6 +1125,27 @@ export class ContextEngine {
1029
1125
  });
1030
1126
  await this.applyLegacySymbolVectorRecall(cgResult, query, warnings);
1031
1127
  this.clearRecoveredSymbolWarning(warnings);
1128
+ const fileFreshnessByPath = new Map();
1129
+ if (this.indexer?.ensureFilesCurrent) {
1130
+ const candidatePaths = [...new Set(Array.from(cgResult.nodes.values()).map(node => node.filePath))];
1131
+ const freshness = await this.indexer.ensureFilesCurrent(candidatePaths, { refresh: true, timeoutMs: 5_000 });
1132
+ for (const file of freshness.files)
1133
+ fileFreshnessByPath.set(file.path, file);
1134
+ const rejectedPaths = new Set([...freshness.missing, ...freshness.stale]);
1135
+ for (const [id, node] of cgResult.nodes) {
1136
+ if (rejectedPaths.has(node.filePath))
1137
+ cgResult.nodes.delete(id);
1138
+ }
1139
+ cgResult.roots = cgResult.roots.filter(id => cgResult.nodes.has(id));
1140
+ if (freshness.missing.length > 0)
1141
+ warnings.push(`codegraph-missing-files:${freshness.missing.length}`);
1142
+ if (freshness.stale.length > 0)
1143
+ warnings.push(`codegraph-stale-degraded:${freshness.stale.length}`);
1144
+ if (rejectedPaths.size > 0 || freshness.refreshed.length > 0) {
1145
+ this.queryCache.invalidatePaths(new Set([...rejectedPaths, ...freshness.refreshed]));
1146
+ invalidateContextPrefetchCache(this.cg.getProjectRoot?.() ?? process.cwd());
1147
+ }
1148
+ }
1032
1149
  const edgeQuality = sanitizeCodeGraphEdges(this.cg, cgResult.edges ?? []);
1033
1150
  if (edgeQuality.rejected.length > 0) {
1034
1151
  warnings.push(`codegraph-relations-rejected:${edgeQuality.rejected.length}`);
@@ -1082,6 +1199,13 @@ export class ContextEngine {
1082
1199
  }
1083
1200
  }
1084
1201
  const targetAnchors = extractTargetAnchors(query);
1202
+ const retrievalTaskPlan = buildRetrievalTaskPlan({
1203
+ query,
1204
+ parsedIntent: intent,
1205
+ targetAnchors,
1206
+ activeSkill: options?.activeSkill,
1207
+ command: options?.command,
1208
+ });
1085
1209
  const roleTargets = targetAnchors.length > 0
1086
1210
  ? [...requiredFileSet].filter(file => fileMatchesTargetAnchor(file, targetAnchors))
1087
1211
  : allNodes.filter(node => rootIds.has(node.id)).map(node => node.filePath);
@@ -1095,6 +1219,9 @@ export class ContextEngine {
1095
1219
  })
1096
1220
  : [];
1097
1221
  for (const candidate of roleCandidates) {
1222
+ if (this.indexer?.getFileFreshness) {
1223
+ fileFreshnessByPath.set(candidate.path, this.indexer.getFileFreshness(candidate.path));
1224
+ }
1098
1225
  fileRoleByPath.set(candidate.path, candidate);
1099
1226
  if (candidate.role === 'target') {
1100
1227
  requiredFileSet.add(candidate.path);
@@ -1112,9 +1239,21 @@ export class ContextEngine {
1112
1239
  });
1113
1240
  }
1114
1241
  }
1115
- const rankedOptionalFiles = rankOptionalContextFiles([...optionalCandidates.values()], taskType);
1242
+ let rankedOptionalFiles = rankOptionalContextFiles([...optionalCandidates.values()], taskType);
1116
1243
  for (const candidate of rankedOptionalFiles)
1117
1244
  optionalFileSet.add(candidate.path);
1245
+ if (this.indexer?.getFileFreshness) {
1246
+ for (const path of [...requiredFileSet, ...optionalFileSet]) {
1247
+ const freshness = fileFreshnessByPath.get(path) ?? this.indexer.getFileFreshness(path);
1248
+ fileFreshnessByPath.set(path, freshness);
1249
+ if (freshness.current)
1250
+ continue;
1251
+ requiredFileSet.delete(path);
1252
+ optionalFileSet.delete(path);
1253
+ fileRoleByPath.delete(path);
1254
+ }
1255
+ rankedOptionalFiles = rankedOptionalFiles.filter(candidate => optionalFileSet.has(candidate.path));
1256
+ }
1118
1257
  for (const node of allNodes) {
1119
1258
  if (optionalFileSet.has(node.filePath))
1120
1259
  optionalSymbols.push(node.name);
@@ -1172,10 +1311,12 @@ export class ContextEngine {
1172
1311
  const nextActions = generateNextActions(taskType, keySymbols.slice(0, 5));
1173
1312
  // Knowledge hits
1174
1313
  let knowledgeReasoning = [];
1314
+ let knowledgeHits = [];
1175
1315
  try {
1176
- const kResults = await this.searchKnowledgeWithStatus(combinedQuery, this.getKnowledgeSearchOptions(combinedQuery, 5, options));
1316
+ const kResults = await this.searchKnowledgeWithStatus(combinedQuery, this.getKnowledgeSearchOptions(combinedQuery, 5, options, retrievalTaskPlan, maxTokens));
1177
1317
  if (kResults.warnings.length > 0)
1178
1318
  warnings.push(...kResults.warnings);
1319
+ knowledgeHits = kResults.entries;
1179
1320
  knowledgeReasoning = kResults.entries.map(k => ({
1180
1321
  source: "knowledge",
1181
1322
  fileGroup: k.source ?? "knowledge",
@@ -1187,16 +1328,21 @@ export class ContextEngine {
1187
1328
  }
1188
1329
  // Memory context
1189
1330
  let memoryReasoning = [];
1331
+ let memoryHits = [];
1190
1332
  try {
1191
- const mem = await this.memory.getAll();
1192
- const conventions = mem.project?.conventions ?? [];
1193
- if (Array.isArray(conventions) && conventions.length > 0) {
1194
- memoryReasoning = [{
1195
- source: "memory",
1196
- fileGroup: "project memory",
1197
- explanation: `${conventions.length} conventions available for context`,
1198
- }];
1199
- }
1333
+ memoryHits = await this.memory.search(query, {
1334
+ limit: 5,
1335
+ purpose: taskType,
1336
+ scope: this.cg.getProjectRoot?.() ?? process.cwd(),
1337
+ action: retrievalTaskPlan.action,
1338
+ requiredPolicyFacets: retrievalTaskPlan.requiredPolicyFacets,
1339
+ recordAccess: false,
1340
+ });
1341
+ memoryReasoning = memoryHits.map((hit) => ({
1342
+ source: 'memory',
1343
+ fileGroup: hit.entry?.title ?? 'project memory',
1344
+ explanation: `Selected applicable memory ${hit.entry?.id ?? 'unknown'} with score ${hit.score ?? 0}`,
1345
+ }));
1200
1346
  }
1201
1347
  catch {
1202
1348
  // memory may be empty
@@ -1221,12 +1367,12 @@ export class ContextEngine {
1221
1367
  score: 0.5,
1222
1368
  });
1223
1369
  }
1224
- const compressed = compressToTokenLimit(allFileSnippets, maxTokens);
1370
+ const compressed = compressToTokenLimit(allFileSnippets, Math.floor(maxTokens * 0.45));
1225
1371
  if (compressed.overBudget)
1226
1372
  warnings.push("context-compressor-budget-exceeded");
1227
1373
  const requiredPaths = new Set(compressed.files.filter(f => requiredFileSet.has(f.path)).map(f => f.path));
1228
1374
  const optionalPaths = new Set(compressed.files.filter(f => optionalFileSet.has(f.path)).map(f => f.path));
1229
- return {
1375
+ const result = {
1230
1376
  taskType,
1231
1377
  requiredFiles: {
1232
1378
  files: [...requiredPaths],
@@ -1282,6 +1428,25 @@ export class ContextEngine {
1282
1428
  ],
1283
1429
  },
1284
1430
  };
1431
+ result.taskEnvelope = buildTaskContextEnvelope({
1432
+ query,
1433
+ result,
1434
+ projectRoot: this.cg.getProjectRoot?.() ?? process.cwd(),
1435
+ sessionId: options?.sessionId,
1436
+ executionId: options?.executionId,
1437
+ requestId: options?.requestId,
1438
+ host: options?.host,
1439
+ command: options?.command,
1440
+ tokenBudget: maxTokens,
1441
+ targetAnchors,
1442
+ fileRoles: fileRoleByPath,
1443
+ fileFreshness: fileFreshnessByPath,
1444
+ memoryHits,
1445
+ knowledgeHits,
1446
+ rejectedCodeRelations: edgeQuality.rejected.length,
1447
+ taskPlan: retrievalTaskPlan,
1448
+ });
1449
+ return result;
1285
1450
  }
1286
1451
  scheduleSymbolVectorBuild() {
1287
1452
  const warnings = [];
@@ -1366,4 +1531,9 @@ export class ContextEngine {
1366
1531
  };
1367
1532
  }
1368
1533
  }
1534
+ function getGroupFilePaths(group) {
1535
+ if (Array.isArray(group))
1536
+ return group;
1537
+ return group?.files ?? [];
1538
+ }
1369
1539
  //# sourceMappingURL=context-engine.js.map