@devflow-tools/context-engine 0.17.0 → 0.17.2

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 (47) hide show
  1. package/dist/artifact-inventory.d.ts +37 -0
  2. package/dist/artifact-inventory.d.ts.map +1 -0
  3. package/dist/artifact-inventory.js +211 -0
  4. package/dist/artifact-inventory.js.map +1 -0
  5. package/dist/channel-query-planner.d.ts +9 -0
  6. package/dist/channel-query-planner.d.ts.map +1 -0
  7. package/dist/channel-query-planner.js +92 -0
  8. package/dist/channel-query-planner.js.map +1 -0
  9. package/dist/context-engine.d.ts.map +1 -1
  10. package/dist/context-engine.js +163 -12
  11. package/dist/context-engine.js.map +1 -1
  12. package/dist/context-file-roles.d.ts +2 -1
  13. package/dist/context-file-roles.d.ts.map +1 -1
  14. package/dist/context-file-roles.js +6 -0
  15. package/dist/context-file-roles.js.map +1 -1
  16. package/dist/create-engines.d.ts.map +1 -1
  17. package/dist/create-engines.js +17 -5
  18. package/dist/create-engines.js.map +1 -1
  19. package/dist/graph-evidence-selector.d.ts.map +1 -1
  20. package/dist/graph-evidence-selector.js +7 -1
  21. package/dist/graph-evidence-selector.js.map +1 -1
  22. package/dist/index.d.ts +3 -0
  23. package/dist/index.d.ts.map +1 -1
  24. package/dist/index.js +2 -0
  25. package/dist/index.js.map +1 -1
  26. package/dist/indexer-adapter.d.ts.map +1 -1
  27. package/dist/indexer-adapter.js +7 -1
  28. package/dist/indexer-adapter.js.map +1 -1
  29. package/dist/intent-parser.d.ts +5 -2
  30. package/dist/intent-parser.d.ts.map +1 -1
  31. package/dist/intent-parser.js +4 -4
  32. package/dist/intent-parser.js.map +1 -1
  33. package/dist/retrieval-handlers.d.ts +5 -0
  34. package/dist/retrieval-handlers.d.ts.map +1 -1
  35. package/dist/retrieval-handlers.js +70 -2
  36. package/dist/retrieval-handlers.js.map +1 -1
  37. package/dist/retrieval-task-plan.d.ts +2 -1
  38. package/dist/retrieval-task-plan.d.ts.map +1 -1
  39. package/dist/retrieval-task-plan.js +12 -11
  40. package/dist/retrieval-task-plan.js.map +1 -1
  41. package/dist/task-retrieval-orchestrator.d.ts +5 -1
  42. package/dist/task-retrieval-orchestrator.d.ts.map +1 -1
  43. package/dist/task-retrieval-orchestrator.js +58 -12
  44. package/dist/task-retrieval-orchestrator.js.map +1 -1
  45. package/dist/types.d.ts +4 -1
  46. package/dist/types.d.ts.map +1 -1
  47. package/package.json +7 -7
@@ -9,10 +9,12 @@ import { rankOptionalContextFiles, } from "./ranker.js";
9
9
  import { describeLayerEvidence, detectLanguageFromPath, } from "./layer-rules.js";
10
10
  import { reachableCodeGraphNodeIds, sanitizeCodeGraphEdges, } from './graph-edge-quality.js';
11
11
  import { assembleContextFileRoles } from './context-file-roles.js';
12
+ import { buildArtifactInventory } from './artifact-inventory.js';
12
13
  import { buildTaskContextEnvelope } from './task-retrieval-orchestrator.js';
13
14
  import { recordTaskEnvelopeLedger } from './retrieval-ledger.js';
14
15
  import { selectGraphEvidence } from './graph-evidence-selector.js';
15
16
  import { allocateContextFileBudget } from './context-budget-allocator.js';
17
+ import { emitSemanticControlLog } from '@devflow-tools/telemetry';
16
18
  const CG = CGModule.default ?? CGModule;
17
19
  const SYMBOL_VECTOR_WARNING = "Symbol vector search is not ready; using CodeGraph/BM25 results. Run `devflow doctor` for details.";
18
20
  // ── RRF (Reciprocal Rank Fusion) ─────────────────────────────────
@@ -314,11 +316,18 @@ export class ContextEngine {
314
316
  const languages = Object.keys(stats.filesByLanguage ?? {});
315
317
  return {
316
318
  limit,
319
+ retrievalIntent: 'automatic_context',
317
320
  budgetTokens: Math.max(0, Math.floor(totalBudget * 0.25)),
318
321
  activeSkill: options.activeSkill,
319
322
  relevantFiles: options.relevantFiles,
320
323
  action: taskPlan?.action,
321
324
  knowledgeNeeds: taskPlan?.knowledgeNeeds,
325
+ sessionId: options.sessionId,
326
+ executionId: options.executionId,
327
+ requestId: options.requestId,
328
+ turnId: options.turnId,
329
+ intentArtifactHash: options.intentArtifact?.artifactHash,
330
+ queryPlanHash: options.queryPlan?.planHash,
322
331
  projectSignals: {
323
332
  frameworks,
324
333
  languages,
@@ -624,15 +633,70 @@ export class ContextEngine {
624
633
  }
625
634
  const intent = parseIntent(query);
626
635
  const taskType = intent.intent;
627
- const combinedQuery = mergeQueries(query, expandQuery(query), options?.expandedTerms);
628
- const targetAnchors = extractTargetAnchors(query);
636
+ const combinedQuery = options?.queryPlan?.code[0]?.query
637
+ ?? mergeQueries(query, expandQuery(query), options?.expandedTerms);
638
+ const explicitTargetAnchors = options?.intentArtifact?.targetAnchors ?? extractTargetAnchors(query);
639
+ const initialTaskPlan = buildRetrievalTaskPlan({
640
+ query,
641
+ parsedIntent: intent,
642
+ targetAnchors: explicitTargetAnchors,
643
+ activeSkill: options?.activeSkill,
644
+ command: options?.command,
645
+ authoritativeArtifact: options?.intentArtifact,
646
+ });
647
+ const projectRoot = this.cg.getProjectRoot?.() ?? process.cwd();
648
+ const logIdentity = {
649
+ projectRoot,
650
+ sessionId: options?.sessionId,
651
+ turnId: options?.turnId,
652
+ requestId: options?.requestId,
653
+ executionId: options?.executionId,
654
+ intentArtifactHash: options?.intentArtifact?.artifactHash,
655
+ queryPlanHash: options?.queryPlan?.planHash,
656
+ };
657
+ let artifactInventory = this.indexer
658
+ ? buildArtifactInventory({
659
+ codegraph: this.cg,
660
+ indexer: this.indexer,
661
+ query: combinedQuery,
662
+ explicitTargetAnchors,
663
+ action: initialTaskPlan.action,
664
+ identity: logIdentity,
665
+ })
666
+ : { entries: [], operationTargets: [], missingExplicitTargets: [], ambiguous: false };
667
+ const staleInventoryTargets = artifactInventory.operationTargets
668
+ .filter(entry => entry.freshness.exists && !entry.freshness.current)
669
+ .map(entry => entry.path);
670
+ if (this.indexer && staleInventoryTargets.length > 0) {
671
+ await this.indexer.ensureFilesCurrent(staleInventoryTargets, { refresh: true, timeoutMs: 5_000 });
672
+ artifactInventory = buildArtifactInventory({
673
+ codegraph: this.cg,
674
+ indexer: this.indexer,
675
+ query: combinedQuery,
676
+ explicitTargetAnchors,
677
+ action: initialTaskPlan.action,
678
+ identity: logIdentity,
679
+ });
680
+ }
681
+ const targetAnchors = [...new Set([
682
+ ...explicitTargetAnchors,
683
+ ...artifactInventory.operationTargets
684
+ .filter(entry => entry.freshness.current)
685
+ .map(entry => entry.path),
686
+ ])];
629
687
  const retrievalTaskPlan = buildRetrievalTaskPlan({
630
688
  query,
631
689
  parsedIntent: intent,
632
690
  targetAnchors,
633
691
  activeSkill: options?.activeSkill,
634
692
  command: options?.command,
693
+ authoritativeArtifact: options?.intentArtifact,
635
694
  });
695
+ if (artifactInventory.missingExplicitTargets.length > 0) {
696
+ warnings.push(`explicit-target-missing:${artifactInventory.missingExplicitTargets.length}`);
697
+ }
698
+ if (artifactInventory.ambiguous)
699
+ warnings.push('artifact-target-ambiguous');
636
700
  const timing = {};
637
701
  const t0 = Date.now();
638
702
  const [cgResult, knowledgeResult, memoryHits] = await Promise.all([
@@ -647,7 +711,10 @@ export class ContextEngine {
647
711
  }),
648
712
  new Promise((_, reject) => setTimeout(() => reject(new Error("buildContext timeout")), 15000)),
649
713
  ]).catch((err) => {
650
- console.error("[devflow] cg.buildContext failed, falling back:", err.message);
714
+ emitSemanticControlLog({
715
+ event: 'context.build.degraded', identity: logIdentity, level: 'warn', timestamp: Date.now(),
716
+ data: { stage: 'codegraph_context', reason: err.message, fallback: 'manual_pipeline' },
717
+ });
651
718
  return null;
652
719
  })
653
720
  : Promise.resolve(null),
@@ -655,7 +722,10 @@ export class ContextEngine {
655
722
  this.searchKnowledgeWithStatus(combinedQuery, this.getKnowledgeSearchOptions(combinedQuery, 5, options, retrievalTaskPlan, maxTokens)),
656
723
  new Promise((_, reject) => setTimeout(() => reject(new Error("knowledge timeout")), 5000)),
657
724
  ]).catch((err) => {
658
- console.error("[devflow] knowledge.search timeout:", err.message);
725
+ emitSemanticControlLog({
726
+ event: 'knowledge.search.degraded', identity: logIdentity, level: 'warn', timestamp: Date.now(),
727
+ data: { reason: err.message, fallback: 'knowledge_empty' },
728
+ });
659
729
  return { entries: [], warnings: [`knowledge-search-failed: ${err.message}`] };
660
730
  }),
661
731
  typeof this.memory.search === "function"
@@ -670,18 +740,23 @@ export class ContextEngine {
670
740
  }),
671
741
  new Promise((_, reject) => setTimeout(() => reject(new Error("memory timeout")), 5000)),
672
742
  ]).catch((err) => {
673
- console.error("[devflow] memory.search timeout:", err.message);
743
+ emitSemanticControlLog({
744
+ event: 'memory.search.completed', identity: logIdentity, level: 'warn', timestamp: Date.now(),
745
+ data: { status: 'degraded', reason: err.message, selected: 0, fallback: 'memory_empty' },
746
+ });
674
747
  return [];
675
748
  })
676
749
  : Promise.resolve([]),
677
750
  ]);
678
751
  timing["1_promise_all"] = Date.now() - t0;
679
- console.error("[devflow:timing] Promise.all:", timing["1_promise_all"], "ms");
680
752
  const knowledgeHits = knowledgeResult.entries;
681
753
  if (knowledgeResult.warnings.length > 0)
682
754
  warnings.push(...knowledgeResult.warnings);
683
755
  if (!cgResult) {
684
- console.error("[devflow] buildContext failed, falling back to manual pipeline");
756
+ emitSemanticControlLog({
757
+ event: 'context.build.degraded', identity: logIdentity, level: 'warn', timestamp: Date.now(),
758
+ data: { stage: 'codegraph_context', reason: 'context_unavailable', fallback: 'manual_pipeline' },
759
+ });
685
760
  const result = await this.buildTaskContextLegacy(query, options);
686
761
  result._devflow = { fallback: "buildContext-unavailable" };
687
762
  return result;
@@ -693,7 +768,10 @@ export class ContextEngine {
693
768
  ctx = JSON.parse(cgResult);
694
769
  }
695
770
  catch {
696
- console.error("[devflow] buildContext returned unparseable string, falling back");
771
+ emitSemanticControlLog({
772
+ event: 'context.build.degraded', identity: logIdentity, level: 'warn', timestamp: Date.now(),
773
+ data: { stage: 'codegraph_parse', reason: 'unparseable_context', fallback: 'manual_pipeline' },
774
+ });
697
775
  return this.buildTaskContextLegacy(query, options);
698
776
  }
699
777
  }
@@ -702,6 +780,20 @@ export class ContextEngine {
702
780
  }
703
781
  ctx.nodes = Array.isArray(ctx.nodes) ? ctx.nodes : [];
704
782
  ctx.entryPoints = Array.isArray(ctx.entryPoints) ? ctx.entryPoints : [];
783
+ const inventoryNodeIds = new Set(ctx.nodes.map((node) => String(node.id ?? '')));
784
+ for (const target of artifactInventory.operationTargets.filter(entry => entry.freshness.current)) {
785
+ const nodes = this.cg.getNodesInFile(target.path)
786
+ .filter(node => node.kind !== 'file' && node.kind !== 'import');
787
+ for (const node of nodes) {
788
+ if (!inventoryNodeIds.has(node.id)) {
789
+ ctx.nodes.push(node);
790
+ inventoryNodeIds.add(node.id);
791
+ }
792
+ }
793
+ const root = nodes.find(node => target.symbols.includes(node.name)) ?? nodes[0];
794
+ if (root && !ctx.entryPoints.some((entry) => entry.id === root.id))
795
+ ctx.entryPoints.unshift(root);
796
+ }
705
797
  await this.applyTaskSymbolVectorRecall(ctx, combinedQuery, warnings);
706
798
  this.clearRecoveredSymbolWarning(warnings);
707
799
  // Fallback: direct searchNodes for query terms that buildContext may have missed
@@ -734,7 +826,6 @@ export class ContextEngine {
734
826
  }
735
827
  }
736
828
  }
737
- console.error("[devflow:timing] Fallback searchNodes loop:", Date.now() - tFallback, "ms");
738
829
  timing["2_fallback_loop"] = Date.now() - tFallback;
739
830
  const fileFreshnessByPath = new Map();
740
831
  if (this.indexer?.ensureFilesCurrent) {
@@ -765,6 +856,22 @@ export class ContextEngine {
765
856
  ctx.edges = edgeQuality.accepted;
766
857
  if (edgeQuality.rejected.length > 0) {
767
858
  warnings.push(`codegraph-relations-rejected:${edgeQuality.rejected.length}`);
859
+ emitSemanticControlLog({
860
+ event: 'codegraph.edge.rejected',
861
+ identity: logIdentity,
862
+ level: 'warn',
863
+ timestamp: Date.now(),
864
+ data: {
865
+ rejectedCount: edgeQuality.rejected.length,
866
+ reasons: countValues(edgeQuality.rejected.map(edge => edge.reason)),
867
+ examples: edgeQuality.rejected.slice(0, 8).map(edge => ({
868
+ kind: edge.kind,
869
+ sourceFile: edge.sourceFile,
870
+ targetFile: edge.targetFile,
871
+ reason: edge.reason,
872
+ })),
873
+ },
874
+ });
768
875
  }
769
876
  const tSnippet = Date.now();
770
877
  let allNodes = ctx.nodes ?? [];
@@ -813,6 +920,10 @@ export class ContextEngine {
813
920
  const optionalCandidates = new Map();
814
921
  const graphOptionalPaths = new Set();
815
922
  const queryTerms = combinedQuery.split(/\s+/).map(term => term.toLowerCase()).filter(term => term.length >= 3);
923
+ for (const target of artifactInventory.operationTargets) {
924
+ if (target.freshness.current)
925
+ requiredFileSet.add(target.path);
926
+ }
816
927
  for (const node of allNodes) {
817
928
  // Skip file wrappers and import edges — they're not actionable symbols
818
929
  if (node.kind === "file" || node.kind === "import")
@@ -867,6 +978,16 @@ export class ContextEngine {
867
978
  }
868
979
  }
869
980
  const fileRoleByPath = new Map();
981
+ for (const artifact of artifactInventory.entries.filter(entry => entry.freshness.current)) {
982
+ fileRoleByPath.set(artifact.path, {
983
+ path: artifact.path,
984
+ role: artifact.artifactRole === 'operation_target' ? 'target' : 'dependency',
985
+ artifactRole: artifact.artifactRole,
986
+ score: artifact.score,
987
+ evidence: artifact.evidence,
988
+ });
989
+ fileFreshnessByPath.set(artifact.path, artifact.freshness);
990
+ }
870
991
  const roleCandidates = this.indexer
871
992
  ? assembleContextFileRoles({
872
993
  codegraph: this.cg,
@@ -879,7 +1000,10 @@ export class ContextEngine {
879
1000
  if (this.indexer?.getFileFreshness) {
880
1001
  fileFreshnessByPath.set(candidate.path, this.indexer.getFileFreshness(candidate.path));
881
1002
  }
882
- fileRoleByPath.set(candidate.path, candidate);
1003
+ const inventoryRole = fileRoleByPath.get(candidate.path);
1004
+ fileRoleByPath.set(candidate.path, inventoryRole && inventoryRole.score >= candidate.score
1005
+ ? inventoryRole
1006
+ : candidate);
883
1007
  if (candidate.role === 'target') {
884
1008
  requiredFileSet.add(candidate.path);
885
1009
  continue;
@@ -979,7 +1103,6 @@ export class ContextEngine {
979
1103
  const compressed = compressToTokenLimit(allFileSnippets, Math.floor(maxTokens * 0.45));
980
1104
  if (compressed.overBudget)
981
1105
  warnings.push("context-compressor-budget-exceeded");
982
- console.error("[devflow:timing] Snippet building + compress:", Date.now() - tSnippet, "ms");
983
1106
  timing["3_snippet_compress"] = Date.now() - tSnippet;
984
1107
  const riskHints = generateRiskHints(allNodes.filter(node => node.kind !== "file" && node.kind !== "import"), graphEvidence);
985
1108
  const nextActions = generateNextActions(taskType, keySymbols.slice(0, 5));
@@ -1108,6 +1231,9 @@ export class ContextEngine {
1108
1231
  requestId: options?.requestId,
1109
1232
  host: options?.host,
1110
1233
  command: options?.command,
1234
+ turnId: options?.turnId,
1235
+ intentArtifact: options?.intentArtifact,
1236
+ queryPlan: options?.queryPlan,
1111
1237
  tokenBudget: maxTokens,
1112
1238
  targetAnchors,
1113
1239
  taskPlan: retrievalTaskPlan,
@@ -1152,7 +1278,23 @@ export class ContextEngine {
1152
1278
  this.queryCache.set(cacheKey, result);
1153
1279
  }
1154
1280
  timing["0_total"] = Date.now() - t0;
1155
- console.error("[devflow:timing] buildTaskContext TOTAL:", timing["0_total"], "ms");
1281
+ emitSemanticControlLog({
1282
+ event: 'codegraph.selection.completed',
1283
+ identity: {
1284
+ ...logIdentity,
1285
+ contextReceipt: result.taskEnvelope?.receipt.contextHash,
1286
+ },
1287
+ level: warnings.length > 0 ? 'warn' : 'info',
1288
+ timestamp: Date.now(),
1289
+ data: {
1290
+ selectedFiles: result.taskEnvelope?.code.files.length ?? 0,
1291
+ selectedSymbols: result.taskEnvelope?.code.symbols.length ?? 0,
1292
+ rejectedRelations: result.taskEnvelope?.quality.code.rejected ?? 0,
1293
+ ambiguityCount: result.taskEnvelope?.code.ambiguities?.length ?? 0,
1294
+ timing,
1295
+ warnings: warnings.map(value => value.split(':')[0]),
1296
+ },
1297
+ });
1156
1298
  result._timing = timing;
1157
1299
  return result;
1158
1300
  }
@@ -1487,6 +1629,9 @@ export class ContextEngine {
1487
1629
  requestId: options?.requestId,
1488
1630
  host: options?.host,
1489
1631
  command: options?.command,
1632
+ turnId: options?.turnId,
1633
+ intentArtifact: options?.intentArtifact,
1634
+ queryPlan: options?.queryPlan,
1490
1635
  tokenBudget: maxTokens,
1491
1636
  targetAnchors,
1492
1637
  fileRoles: fileRoleByPath,
@@ -1586,4 +1731,10 @@ function getGroupFilePaths(group) {
1586
1731
  return group;
1587
1732
  return group?.files ?? [];
1588
1733
  }
1734
+ function countValues(values) {
1735
+ const counts = {};
1736
+ for (const value of values)
1737
+ counts[value] = (counts[value] ?? 0) + 1;
1738
+ return counts;
1739
+ }
1589
1740
  //# sourceMappingURL=context-engine.js.map