@duckcodeailabs/dql-agent 1.13.5 → 1.14.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.
Files changed (158) hide show
  1. package/dist/agent-run-engine.d.ts +79 -4
  2. package/dist/agent-run-engine.d.ts.map +1 -1
  3. package/dist/agent-run-engine.js +457 -39
  4. package/dist/agent-run-engine.js.map +1 -1
  5. package/dist/agent-run-gates.d.ts.map +1 -1
  6. package/dist/agent-run-gates.js +13 -0
  7. package/dist/agent-run-gates.js.map +1 -1
  8. package/dist/agent-run-store.d.ts.map +1 -1
  9. package/dist/agent-run-store.js +42 -25
  10. package/dist/agent-run-store.js.map +1 -1
  11. package/dist/agentic/analyst-loop.d.ts +91 -0
  12. package/dist/agentic/analyst-loop.d.ts.map +1 -0
  13. package/dist/agentic/analyst-loop.js +311 -0
  14. package/dist/agentic/analyst-loop.js.map +1 -0
  15. package/dist/agentic/assumptions.d.ts +62 -0
  16. package/dist/agentic/assumptions.d.ts.map +1 -0
  17. package/dist/agentic/assumptions.js +73 -0
  18. package/dist/agentic/assumptions.js.map +1 -0
  19. package/dist/agentic/business-explanation.d.ts +56 -0
  20. package/dist/agentic/business-explanation.d.ts.map +1 -0
  21. package/dist/agentic/business-explanation.js +130 -0
  22. package/dist/agentic/business-explanation.js.map +1 -0
  23. package/dist/agentic/identifier-ledger.d.ts +74 -0
  24. package/dist/agentic/identifier-ledger.d.ts.map +1 -0
  25. package/dist/agentic/identifier-ledger.js +151 -0
  26. package/dist/agentic/identifier-ledger.js.map +1 -0
  27. package/dist/agentic/ledger-tools.d.ts +68 -0
  28. package/dist/agentic/ledger-tools.d.ts.map +1 -0
  29. package/dist/agentic/ledger-tools.js +198 -0
  30. package/dist/agentic/ledger-tools.js.map +1 -0
  31. package/dist/agentic/orchestrator-policy.d.ts +60 -0
  32. package/dist/agentic/orchestrator-policy.d.ts.map +1 -0
  33. package/dist/agentic/orchestrator-policy.js +73 -0
  34. package/dist/agentic/orchestrator-policy.js.map +1 -0
  35. package/dist/agentic/orchestrator.d.ts +54 -0
  36. package/dist/agentic/orchestrator.d.ts.map +1 -0
  37. package/dist/agentic/orchestrator.js +67 -0
  38. package/dist/agentic/orchestrator.js.map +1 -0
  39. package/dist/agentic/preview-tool.d.ts +36 -0
  40. package/dist/agentic/preview-tool.d.ts.map +1 -0
  41. package/dist/agentic/preview-tool.js +71 -0
  42. package/dist/agentic/preview-tool.js.map +1 -0
  43. package/dist/agentic/rerank.d.ts +50 -0
  44. package/dist/agentic/rerank.d.ts.map +1 -0
  45. package/dist/agentic/rerank.js +140 -0
  46. package/dist/agentic/rerank.js.map +1 -0
  47. package/dist/agentic/research/hypothesis.d.ts +110 -0
  48. package/dist/agentic/research/hypothesis.d.ts.map +1 -0
  49. package/dist/agentic/research/hypothesis.js +125 -0
  50. package/dist/agentic/research/hypothesis.js.map +1 -0
  51. package/dist/agentic/research/synthesis.d.ts +32 -0
  52. package/dist/agentic/research/synthesis.d.ts.map +1 -0
  53. package/dist/agentic/research/synthesis.js +94 -0
  54. package/dist/agentic/research/synthesis.js.map +1 -0
  55. package/dist/agentic/research-agent.d.ts +54 -0
  56. package/dist/agentic/research-agent.d.ts.map +1 -0
  57. package/dist/agentic/research-agent.js +176 -0
  58. package/dist/agentic/research-agent.js.map +1 -0
  59. package/dist/agentic/sql-authorization.d.ts +131 -0
  60. package/dist/agentic/sql-authorization.d.ts.map +1 -0
  61. package/dist/agentic/sql-authorization.js +232 -0
  62. package/dist/agentic/sql-authorization.js.map +1 -0
  63. package/dist/agentic/tool-loop.d.ts +29 -0
  64. package/dist/agentic/tool-loop.d.ts.map +1 -1
  65. package/dist/agentic/tool-loop.js +129 -52
  66. package/dist/agentic/tool-loop.js.map +1 -1
  67. package/dist/agentic/toolset.d.ts.map +1 -1
  68. package/dist/agentic/toolset.js +155 -0
  69. package/dist/agentic/toolset.js.map +1 -1
  70. package/dist/agentic/turn-plan.d.ts +35 -0
  71. package/dist/agentic/turn-plan.d.ts.map +1 -0
  72. package/dist/agentic/turn-plan.js +108 -0
  73. package/dist/agentic/turn-plan.js.map +1 -0
  74. package/dist/agentic/value-probe.d.ts +89 -0
  75. package/dist/agentic/value-probe.d.ts.map +1 -0
  76. package/dist/agentic/value-probe.js +304 -0
  77. package/dist/agentic/value-probe.js.map +1 -0
  78. package/dist/analytical-orchestration.d.ts +336 -0
  79. package/dist/analytical-orchestration.d.ts.map +1 -0
  80. package/dist/analytical-orchestration.js +631 -0
  81. package/dist/analytical-orchestration.js.map +1 -0
  82. package/dist/analytical-request-policy.d.ts +26 -0
  83. package/dist/analytical-request-policy.d.ts.map +1 -0
  84. package/dist/analytical-request-policy.js +82 -0
  85. package/dist/analytical-request-policy.js.map +1 -0
  86. package/dist/analytical-result-facts.d.ts +14 -1
  87. package/dist/analytical-result-facts.d.ts.map +1 -1
  88. package/dist/analytical-result-facts.js +76 -1
  89. package/dist/analytical-result-facts.js.map +1 -1
  90. package/dist/answer-loop.d.ts +61 -16
  91. package/dist/answer-loop.d.ts.map +1 -1
  92. package/dist/answer-loop.js +256 -36
  93. package/dist/answer-loop.js.map +1 -1
  94. package/dist/cascade/budgets.d.ts.map +1 -1
  95. package/dist/cascade/budgets.js +8 -1
  96. package/dist/cascade/budgets.js.map +1 -1
  97. package/dist/cascade/packer.d.ts +88 -0
  98. package/dist/cascade/packer.d.ts.map +1 -0
  99. package/dist/cascade/packer.js +106 -0
  100. package/dist/cascade/packer.js.map +1 -0
  101. package/dist/conversation/result-ops.d.ts +18 -3
  102. package/dist/conversation/result-ops.d.ts.map +1 -1
  103. package/dist/conversation/result-ops.js +77 -5
  104. package/dist/conversation/result-ops.js.map +1 -1
  105. package/dist/conversation/rolling-summary.d.ts.map +1 -1
  106. package/dist/conversation/rolling-summary.js +7 -2
  107. package/dist/conversation/rolling-summary.js.map +1 -1
  108. package/dist/conversation/session-store.d.ts +3 -0
  109. package/dist/conversation/session-store.d.ts.map +1 -1
  110. package/dist/conversation/session-store.js +8 -4
  111. package/dist/conversation/session-store.js.map +1 -1
  112. package/dist/conversation/snapshot.d.ts.map +1 -1
  113. package/dist/conversation/snapshot.js +13 -1
  114. package/dist/conversation/snapshot.js.map +1 -1
  115. package/dist/conversation/turn-trust.d.ts.map +1 -1
  116. package/dist/conversation/turn-trust.js +6 -0
  117. package/dist/conversation/turn-trust.js.map +1 -1
  118. package/dist/hints/store.d.ts.map +1 -1
  119. package/dist/hints/store.js +15 -5
  120. package/dist/hints/store.js.map +1 -1
  121. package/dist/index.d.ts +27 -5
  122. package/dist/index.d.ts.map +1 -1
  123. package/dist/index.js +28 -4
  124. package/dist/index.js.map +1 -1
  125. package/dist/intent-controller.d.ts +8 -0
  126. package/dist/intent-controller.d.ts.map +1 -1
  127. package/dist/intent-controller.js +65 -0
  128. package/dist/intent-controller.js.map +1 -1
  129. package/dist/meaning-resolution.d.ts +7 -0
  130. package/dist/meaning-resolution.d.ts.map +1 -1
  131. package/dist/meaning-resolution.js.map +1 -1
  132. package/dist/metadata/block-fit.d.ts +18 -0
  133. package/dist/metadata/block-fit.d.ts.map +1 -1
  134. package/dist/metadata/block-fit.js +41 -1
  135. package/dist/metadata/block-fit.js.map +1 -1
  136. package/dist/metadata/catalog.d.ts +44 -23
  137. package/dist/metadata/catalog.d.ts.map +1 -1
  138. package/dist/metadata/catalog.js +363 -53
  139. package/dist/metadata/catalog.js.map +1 -1
  140. package/dist/metadata/meaning-evidence.d.ts.map +1 -1
  141. package/dist/metadata/meaning-evidence.js +101 -8
  142. package/dist/metadata/meaning-evidence.js.map +1 -1
  143. package/dist/metadata/metric-match.d.ts +32 -0
  144. package/dist/metadata/metric-match.d.ts.map +1 -1
  145. package/dist/metadata/metric-match.js +120 -9
  146. package/dist/metadata/metric-match.js.map +1 -1
  147. package/dist/research-loop.d.ts +33 -2
  148. package/dist/research-loop.d.ts.map +1 -1
  149. package/dist/research-loop.js +44 -4
  150. package/dist/research-loop.js.map +1 -1
  151. package/dist/resolved-analytical-plan.d.ts.map +1 -1
  152. package/dist/resolved-analytical-plan.js +92 -5
  153. package/dist/resolved-analytical-plan.js.map +1 -1
  154. package/dist/router.d.ts +36 -16
  155. package/dist/router.d.ts.map +1 -1
  156. package/dist/router.js +604 -25
  157. package/dist/router.js.map +1 -1
  158. package/package.json +4 -4
@@ -1,3 +1,4 @@
1
+ import { applyRerank } from '../agentic/rerank.js';
1
2
  /**
2
3
  * Project-local metadata catalog for OSS agentic analytics.
3
4
  *
@@ -27,6 +28,7 @@ import { projectEmbeddingProvider } from '../embeddings/project-embeddings.js';
27
28
  import { matchExampleParaphrase } from './example-match.js';
28
29
  import { loadSkills, selectRelevantSkills } from '../skills/loader.js';
29
30
  import { buildMeaningEvidencePackage, } from './meaning-evidence.js';
31
+ import { fuseContextCandidates, retrieveContextLanes } from '../analytical-orchestration.js';
30
32
  import { hasGeneratedAppSourceOrigin, isExplicitlyReusableAppSource, } from '../app-source-policy.js';
31
33
  export { applyContextPackCompatibility, toAgentRetrievalEvidence } from './meaning-evidence.js';
32
34
  const require = createRequire(import.meta.url);
@@ -105,8 +107,20 @@ const DEFAULT_VECTOR_PROVIDER = new HashedTokenEmbeddingProvider();
105
107
  export function defaultMetadataPath(projectRoot) {
106
108
  return join(projectRoot, '.dql', 'cache', 'metadata.sqlite');
107
109
  }
108
- export function metadataSnapshotPath(projectRoot, fingerprint) {
109
- return join(projectRoot, '.dql', 'cache', 'snapshots', `${METADATA_INDEX_VERSION}-${fingerprint}.sqlite`);
110
+ /**
111
+ * Snapshots are immutable and addressed by content fingerprint, so
112
+ * `exportSnapshot` refuses to overwrite an existing file. The embedded VECTORS
113
+ * are part of what a snapshot contains but are NOT part of the content
114
+ * fingerprint, so two different encodings of identical content would otherwise
115
+ * collide on one path — and switching `ai.embeddings` would silently keep
116
+ * serving the hashed vectors forever. Qualify the filename with the provider so
117
+ * they are the distinct artifacts they actually are.
118
+ */
119
+ export function metadataSnapshotPath(projectRoot, fingerprint, vectorProviderId) {
120
+ const suffix = vectorProviderId && !vectorProviderId.startsWith('hashed-token')
121
+ ? `-${vectorProviderId.replace(/[^a-z0-9]+/gi, '_').slice(0, 48)}`
122
+ : '';
123
+ return join(projectRoot, '.dql', 'cache', 'snapshots', `${METADATA_INDEX_VERSION}-${fingerprint}${suffix}.sqlite`);
110
124
  }
111
125
  function readActiveMetadataSnapshotPointer(projectRoot) {
112
126
  const pointerPath = join(projectRoot, '.dql', 'cache', 'active-snapshot.json');
@@ -143,7 +157,7 @@ export function currentMetadataFingerprint(projectRoot) {
143
157
  }
144
158
  }
145
159
  function activateMetadataSnapshot(projectRoot, fingerprint, catalog) {
146
- const snapshotPath = metadataSnapshotPath(projectRoot, fingerprint);
160
+ const snapshotPath = metadataSnapshotPath(projectRoot, fingerprint, catalog.state('vector_provider') ?? undefined);
147
161
  catalog.exportSnapshot(snapshotPath);
148
162
  const cacheDir = join(projectRoot, '.dql', 'cache');
149
163
  const pointerPath = join(cacheDir, 'active-snapshot.json');
@@ -314,15 +328,22 @@ export async function awaitVectorIndexUpgrade(projectRoot) {
314
328
  * runs or if it fails. Best-effort by design: an unreachable Ollama must not
315
329
  * break indexing or answering.
316
330
  */
317
- export async function upgradeVectorIndexForProject(projectRoot, provider) {
331
+ /** How long a cold re-embed may hold up indexing before we ship the hashed index. */
332
+ const VECTOR_UPGRADE_BUDGET_MS = 8_000;
333
+ /**
334
+ * Re-embed an ALREADY-OPEN catalog. Split out from `upgradeVectorIndexForProject`
335
+ * so `ensureMetadataCatalogFresh` can upgrade the same catalog handle it is about
336
+ * to export — the vectors have to be in place BEFORE `activateMetadataSnapshot`,
337
+ * because retrieval reads the exported snapshot, not this working database.
338
+ */
339
+ async function upgradeVectorIndexOnCatalog(catalog, provider) {
318
340
  if (provider.id.startsWith('hashed-token')) {
319
341
  return { upgraded: false, providerId: provider.id, reason: 'project uses the offline hashed embedder' };
320
342
  }
321
- const catalog = openMetadataCatalog(projectRoot);
343
+ if (catalog.state('vector_provider') === provider.id) {
344
+ return { upgraded: false, providerId: provider.id, reason: 'index already embedded with this provider' };
345
+ }
322
346
  try {
323
- if (catalog.state('vector_provider') === provider.id) {
324
- return { upgraded: false, providerId: provider.id, reason: 'index already embedded with this provider' };
325
- }
326
347
  await catalog.rebuildVectorIndex(provider);
327
348
  return { upgraded: true, providerId: provider.id };
328
349
  }
@@ -333,6 +354,22 @@ export async function upgradeVectorIndexForProject(projectRoot, provider) {
333
354
  reason: error instanceof Error ? error.message : String(error),
334
355
  };
335
356
  }
357
+ }
358
+ export async function upgradeVectorIndexForProject(projectRoot, provider) {
359
+ const catalog = openMetadataCatalog(projectRoot);
360
+ try {
361
+ const result = await upgradeVectorIndexOnCatalog(catalog, provider);
362
+ // Re-export so the ACTIVE snapshot carries the new vectors. Without this the
363
+ // upgrade only ever touched the mutable working database, which the answer
364
+ // path never reads — so `POST /api/settings/embeddings/reindex` reported
365
+ // success while retrieval kept serving the hashed index.
366
+ if (result.upgraded) {
367
+ const fingerprint = catalog.state('fingerprint');
368
+ if (fingerprint)
369
+ activateMetadataSnapshot(projectRoot, fingerprint, catalog);
370
+ }
371
+ return result;
372
+ }
336
373
  finally {
337
374
  catalog.close();
338
375
  }
@@ -348,7 +385,30 @@ export async function ensureMetadataCatalogFresh(projectRoot, options = {}) {
348
385
  try {
349
386
  const existing = catalog.state('fingerprint');
350
387
  const compatibleIndex = catalog.state('index_version') === METADATA_INDEX_VERSION;
388
+ // The snapshot write embeds with the sync hashed provider (better-sqlite3
389
+ // transactions cannot await), so a configured Ollama/OpenAI embedder can only
390
+ // be applied afterwards — and it must land BEFORE the export, since retrieval
391
+ // reads the exported snapshot. `searchVectorObjects` returns ZERO candidates
392
+ // when the requested provider id does not match the indexed one, so skipping
393
+ // this makes configuring a real embedder strictly worse than not configuring
394
+ // one: an empty vector lane instead of a weak one.
395
+ const vectorProvider = options.embeddingProvider ?? projectEmbeddingProvider(projectRoot);
396
+ const upgradeVectors = async () => {
397
+ const upgrade = upgradeVectorIndexOnCatalog(catalog, vectorProvider);
398
+ pendingVectorUpgrades.set(projectRoot, upgrade.then(() => undefined, () => undefined));
399
+ // Never let a cold or unreachable embedder block indexing: the hashed index
400
+ // is already in place, so timing out simply ships it and the next refresh
401
+ // retries.
402
+ await Promise.race([
403
+ upgrade,
404
+ new Promise((resolve) => setTimeout(resolve, VECTOR_UPGRADE_BUDGET_MS)),
405
+ ]);
406
+ };
351
407
  if (!options.force && compatibleIndex && existing === snapshot.fingerprint) {
408
+ // The project content is unchanged but the CONFIGURED EMBEDDER may not be.
409
+ // Without this, switching `ai.embeddings` never takes effect until an
410
+ // unrelated dbt change happens to move the fingerprint.
411
+ await upgradeVectors();
352
412
  const snapshotPath = activateMetadataSnapshot(projectRoot, snapshot.fingerprint, catalog);
353
413
  return {
354
414
  path: defaultMetadataPath(projectRoot),
@@ -369,6 +429,7 @@ export async function ensureMetadataCatalogFresh(projectRoot, options = {}) {
369
429
  else {
370
430
  catalog.rebuildIncremental(snapshot);
371
431
  }
432
+ await upgradeVectors();
372
433
  const snapshotPath = activateMetadataSnapshot(projectRoot, snapshot.fingerprint, catalog);
373
434
  return {
374
435
  path: defaultMetadataPath(projectRoot),
@@ -417,57 +478,148 @@ export async function retrieveMetadataSnapshotCandidates(catalog, input) {
417
478
  && (!input.objectTypes?.length || input.objectTypes.includes(object.objectType))
418
479
  && (domains.length === 0 || !object.domain || domains.includes(object.domain)));
419
480
  const explicitIdentities = explicitMetadataIdentities(input.question);
420
- const exactObjects = explicitIdentities.flatMap((identity) => {
421
- const resolution = catalog.resolveIdentity(identity);
422
- return resolution.status === 'resolved' && resolution.object && isEligible(resolution.object)
423
- ? [resolution.object]
424
- : [];
425
- });
426
481
  const queries = uniqueMetadataSearchQueries(input.searchQueries?.length
427
482
  ? input.searchQueries
428
483
  : [input.question]);
429
- const lexicalObjects = mergeObjects(queries.flatMap((query) => catalog.searchObjects({
430
- query,
431
- objectTypes: input.objectTypes,
432
- domains: domains.length > 0 ? domains : undefined,
433
- limit,
434
- }))).filter(isEligible).slice(0, limit);
435
- const vector = await catalog.searchVectorObjects({
436
- query: input.question,
437
- objectTypes: input.objectTypes,
438
- domains: domains.length > 0 ? domains : undefined,
439
- limit: Math.min(limit, 24),
440
- // Fall back to the PROJECT's configured embedder rather than the hashed
441
- // default, so a project that turned on a real one actually queries with it.
442
- // searchVectorObjects still refuses (with a reason) if the index was built
443
- // by a different provider, which keeps a half-migrated index from silently
444
- // returning nonsense.
445
- provider: input.embeddingProvider
446
- ?? (input.projectRoot ? projectEmbeddingProvider(input.projectRoot) : undefined),
447
- });
448
- const vectorObjects = vector.candidates.filter(isEligible);
484
+ // Context lanes are genuinely independent and bounded. Their fan-out is
485
+ // deliberately before graph expansion: graph admission depends on the
486
+ // successful seed lanes, while an embedding failure must not block lexical
487
+ // or exact retrieval (CTX-007/PERF-003).
488
+ let vector;
489
+ let exactObjects = [];
490
+ let lexicalObjects = [];
491
+ let vectorObjects = [];
492
+ const laneCards = await retrieveContextLanes({
493
+ exact: async () => {
494
+ exactObjects = explicitIdentities.flatMap((identity) => {
495
+ const resolution = catalog.resolveIdentity(identity);
496
+ return resolution.status === 'resolved' && resolution.object && isEligible(resolution.object)
497
+ ? [resolution.object]
498
+ : [];
499
+ });
500
+ return exactObjects.map((object) => ({
501
+ id: object.objectKey,
502
+ lane: 'exact',
503
+ relevance: 1,
504
+ summary: object.name,
505
+ }));
506
+ },
507
+ lexical: async () => {
508
+ lexicalObjects = mergeObjects(queries.flatMap((query) => catalog.searchObjects({
509
+ query,
510
+ objectTypes: input.objectTypes,
511
+ domains: domains.length > 0 ? domains : undefined,
512
+ limit,
513
+ }))).filter(isEligible).slice(0, limit);
514
+ return lexicalObjects.map((object) => ({
515
+ id: object.objectKey,
516
+ lane: 'lexical',
517
+ relevance: object.score ?? 0,
518
+ summary: object.name,
519
+ }));
520
+ },
521
+ vector: async () => {
522
+ vector = await catalog.searchVectorObjects({
523
+ query: input.question,
524
+ objectTypes: input.objectTypes,
525
+ domains: domains.length > 0 ? domains : undefined,
526
+ limit: Math.min(limit, 24),
527
+ provider: input.embeddingProvider
528
+ ?? (input.projectRoot ? projectEmbeddingProvider(input.projectRoot) : undefined),
529
+ });
530
+ vectorObjects = vector.candidates.filter(isEligible);
531
+ return vectorObjects.map((object) => ({
532
+ id: object.objectKey,
533
+ lane: 'vector',
534
+ relevance: object.score ?? 0,
535
+ summary: object.name,
536
+ }));
537
+ },
538
+ }, Math.min(limit * 2, 100));
449
539
  const seedKeys = mergeObjects([...exactObjects, ...lexicalObjects.slice(0, 12), ...vectorObjects.slice(0, 12)])
450
540
  .map((object) => object.objectKey);
451
- const graphEdges = catalog.edgesForKeys(seedKeys, 1);
452
- const graphObjects = catalog.getObjectsByKeys(Array.from(new Set(graphEdges.flatMap((edge) => [edge.fromKey, edge.toKey])))).filter((object) => isEligible(object) && !seedKeys.includes(object.objectKey)).slice(0, Math.min(limit, 24));
541
+ const graphStartedAt = Date.now();
542
+ let graphError;
543
+ let graphEdges = [];
544
+ try {
545
+ graphEdges = catalog.edgesForKeys(seedKeys, 1);
546
+ }
547
+ catch (error) {
548
+ graphError = error instanceof Error ? error.message : String(error);
549
+ }
550
+ let graphObjects = [];
551
+ if (!graphError) {
552
+ try {
553
+ graphObjects = catalog.getObjectsByKeys(Array.from(new Set(graphEdges.flatMap((edge) => [edge.fromKey, edge.toKey])))).filter((object) => isEligible(object) && !seedKeys.includes(object.objectKey)).slice(0, Math.min(limit, 24));
554
+ }
555
+ catch (error) {
556
+ // Graph expansion is advisory. Preserve exact/lexical/vector candidates
557
+ // and expose the typed graph failure in the lane diagnostics.
558
+ graphError = error instanceof Error ? error.message : String(error);
559
+ graphObjects = [];
560
+ }
561
+ }
453
562
  const lanes = [
454
563
  retrievalLane('exact', exactObjects, (object) => explicitIdentities.includes(object.objectKey)
455
564
  ? 'exact object-key reference'
456
565
  : 'resolved qualified/native/alias reference'),
457
566
  retrievalLane('lexical', lexicalObjects, () => 'BM25/lexical candidate from the immutable snapshot'),
458
567
  {
459
- ...retrievalLane('vector', vectorObjects, () => `independent vector candidate from ${vector.providerId}`),
460
- provider: vector.providerId,
461
- unavailableReason: vector.unavailableReason,
568
+ ...retrievalLane('vector', vectorObjects, () => `independent vector candidate from ${vector?.providerId ?? 'unavailable'}`),
569
+ ...(vector?.providerId ? { provider: vector.providerId } : {}),
570
+ ...(vector?.unavailableReason ? { unavailableReason: vector.unavailableReason } : {}),
462
571
  },
463
572
  retrievalLane('graph', graphObjects, () => 'one-hop neighbor of an eligible exact/lexical/vector seed'),
464
573
  ];
465
- const selected = mergeObjects([...exactObjects, ...lexicalObjects, ...vectorObjects, ...graphObjects])
466
- .slice(0, limit * 2);
574
+ const allLaneCards = {
575
+ exact: exactObjects.map((object) => ({
576
+ id: object.objectKey,
577
+ lane: 'exact',
578
+ relevance: 1,
579
+ summary: object.name,
580
+ })),
581
+ lexical: lexicalObjects.map((object) => ({
582
+ id: object.objectKey,
583
+ lane: 'lexical',
584
+ relevance: object.score ?? 0,
585
+ summary: object.name,
586
+ })),
587
+ vector: vectorObjects.map((object) => ({
588
+ id: object.objectKey,
589
+ lane: 'vector',
590
+ relevance: object.score ?? 0,
591
+ summary: object.name,
592
+ })),
593
+ graph: graphObjects.map((object) => ({
594
+ id: object.objectKey,
595
+ lane: 'graph',
596
+ relevance: object.score ?? 0,
597
+ summary: object.name,
598
+ })),
599
+ };
600
+ const fused = fuseContextCandidates(allLaneCards, Math.min(limit * 2, 100));
601
+ const objectsByKey = new Map(mergeObjects([...exactObjects, ...lexicalObjects, ...vectorObjects, ...graphObjects])
602
+ .map((object) => [object.objectKey, object]));
603
+ const selected = fused.candidates
604
+ .map((candidate) => objectsByKey.get(candidate.id))
605
+ .filter((object) => Boolean(object));
467
606
  return {
468
607
  snapshotId: catalog.state('fingerprint') ?? 'metadata-unavailable',
469
608
  selected,
470
609
  lanes,
610
+ fusion: {
611
+ selectedKeys: fused.diagnostics.selectedIds,
612
+ truncated: fused.diagnostics.truncated,
613
+ lanes: {
614
+ ...laneCards.diagnostics.lanes,
615
+ graph: {
616
+ returned: graphObjects.length,
617
+ durationMs: Date.now() - graphStartedAt,
618
+ status: graphError ? 'error' : graphObjects.length > 0 ? 'ok' : 'empty',
619
+ ...(graphError ? { error: graphError } : {}),
620
+ },
621
+ },
622
+ },
471
623
  };
472
624
  }
473
625
  function retrievalLane(lane, objects, reason) {
@@ -614,7 +766,17 @@ export async function buildLocalContextPack(projectRoot, request) {
614
766
  const runtimeValueObjects = runtimeValueMatchObjects(runtimeCatalog.searchRuntimeValues(metadataValueSearchTerms(request.question, questionPlan, followUp), 32));
615
767
  const selectedObjects = selectedContextObjects(request.selectedContext);
616
768
  const followUpObjects = followUpContextObjects(followUp);
617
- const followUpSourceObjects = catalog.getObjectsByKeys(followUpSourceObjectKeys(followUp));
769
+ let graphError;
770
+ let followUpSourceObjects = [];
771
+ try {
772
+ followUpSourceObjects = catalog.getObjectsByKeys(followUpSourceObjectKeys(followUp));
773
+ }
774
+ catch (error) {
775
+ // A stale follow-up source reference is advisory context. Keep the
776
+ // successful exact/lexical/vector lanes and surface a typed graph lane
777
+ // diagnostic instead of failing the entire context pack (CTX-007).
778
+ graphError = error instanceof Error ? error.message : String(error);
779
+ }
618
780
  const areaObjects = filterMetadataObjectsByDomainContext(catalog.listAllObjects({ objectTypes: ['model_area'] }), request.domainContext);
619
781
  const focusedArea = resolveFocusedModelArea(request.domainContext, request.question, areaObjects);
620
782
  const effectiveDomainContext = focusedArea && request.domainContext
@@ -655,9 +817,20 @@ export async function buildLocalContextPack(projectRoot, request) {
655
817
  ? ranked.selected
656
818
  : mergeObjects([...followUpSourceObjects, ...followUpObjects, ...ranked.selected]);
657
819
  const focusObjectKey = request.focusObjectKey ?? selected[0]?.objectKey ?? null;
658
- const edgeWalk = catalog.edgesForKeys(selected.map((row) => row.objectKey), 3);
659
- const edgeObjectKeys = Array.from(new Set(edgeWalk.flatMap((edge) => [edge.fromKey, edge.toKey])));
660
- const graphObjects = retrievalObjects(catalog.getObjectsByKeys(edgeObjectKeys));
820
+ let edgeWalk = [];
821
+ let graphObjects = [];
822
+ try {
823
+ edgeWalk = catalog.edgesForKeys(selected.map((row) => row.objectKey), 3);
824
+ const edgeObjectKeys = Array.from(new Set(edgeWalk.flatMap((edge) => [edge.fromKey, edge.toKey])));
825
+ graphObjects = retrievalObjects(catalog.getObjectsByKeys(edgeObjectKeys));
826
+ }
827
+ catch (error) {
828
+ // The graph is an enrichment lane. A stale/corrupt graph must not discard
829
+ // successful snapshot, runtime, or semantic candidates (CTX-007).
830
+ graphError = error instanceof Error ? error.message : String(error);
831
+ edgeWalk = [];
832
+ graphObjects = [];
833
+ }
661
834
  const rankedObjects = rankMetadataObjects({
662
835
  rows: retrievalObjects(mergeObjects([...followUpSourceObjects, ...followUpObjects, ...selected, ...graphObjects, ...schemaShapeObjects, ...runtimeObjects, ...runtimeValueObjects, ...selectedObjects])),
663
836
  question: searchQueries.join(' '),
@@ -665,7 +838,27 @@ export async function buildLocalContextPack(projectRoot, request) {
665
838
  modelAreaId: focusedArea?.id,
666
839
  limit: request.limit ?? 120,
667
840
  }).selected;
668
- const sqlParentObjects = sqlParentObjectsForSelectedColumns(rankedObjects, mergeObjects([...graphObjects, ...runtimeObjects]), questionPlan);
841
+ // Reorder by what the QUESTION needs. RRF and the additive score rank by
842
+ // overlap, distance, and type weight — none of which read the question — so
843
+ // the object that answers it can sit behind ten near-synonyms and be cut by
844
+ // a top-N that never considered the ask. Advisory: the reranker may only
845
+ // reorder ids retrieval already returned, and any failure leaves this list
846
+ // exactly as it was.
847
+ const rerankedObjects = request.rerankCandidates
848
+ ? applyRerank(rankedObjects, (object) => object.objectKey, await request.rerankCandidates(searchQueries.join(' '), rankedObjects.slice(0, 40).map((object) => ({
849
+ id: object.objectKey,
850
+ summary: [
851
+ object.name,
852
+ object.objectType,
853
+ object.description?.slice(0, 160),
854
+ ].filter(Boolean).join(' — '),
855
+ }))).catch(() => undefined))
856
+ : rankedObjects;
857
+ if (process.env.DQL_ORCHESTRATOR_TRACE && request.rerankCandidates) {
858
+ const moved = rerankedObjects.findIndex((object, position) => object.objectKey !== rankedObjects[position]?.objectKey);
859
+ console.warn(`[dql] rerank: ${moved < 0 ? 'no change' : `reordered from position ${moved}`} over ${Math.min(40, rankedObjects.length)} candidates`);
860
+ }
861
+ const sqlParentObjects = sqlParentObjectsForSelectedColumns(rerankedObjects, mergeObjects([...graphObjects, ...runtimeObjects]), questionPlan);
669
862
  // Deep-research over a SMALL catalog: skip top-k pruning and hand the model the
670
863
  // entire relation set ("send everything, let the agent decide"). Only when the
671
864
  // whole catalog fits comfortably in context — otherwise we keep ranked selection.
@@ -689,17 +882,27 @@ export async function buildLocalContextPack(projectRoot, request) {
689
882
  limit: 8,
690
883
  }).selected;
691
884
  const objects = fullCatalogObjects
692
- ? mergeObjects([...rankedObjects, ...sqlParentObjects, ...fullCatalogObjects, ...selectedSkillObjects, ...routeObjects])
693
- : mergeObjects([...rankedObjects, ...sqlParentObjects, ...selectedSkillObjects, ...routeObjects]);
885
+ ? mergeObjects([...rerankedObjects, ...sqlParentObjects, ...fullCatalogObjects, ...selectedSkillObjects, ...routeObjects])
886
+ : mergeObjects([...rerankedObjects, ...sqlParentObjects, ...selectedSkillObjects, ...routeObjects]);
694
887
  const objectKeys = objects.map((row) => row.objectKey);
695
888
  const allowedObjectKeys = new Set(objectKeys);
696
- const contextEdges = mergeMetadataEdges([
697
- ...edgeWalk,
698
- ...catalog.edgesForKeys(objectKeys, 2),
699
- ]).filter((edge) => allowedObjectKeys.has(edge.fromKey) && allowedObjectKeys.has(edge.toKey));
889
+ let contextEdges = mergeMetadataEdges(edgeWalk)
890
+ .filter((edge) => allowedObjectKeys.has(edge.fromKey) && allowedObjectKeys.has(edge.toKey));
891
+ try {
892
+ contextEdges = mergeMetadataEdges([
893
+ ...contextEdges,
894
+ ...catalog.edgesForKeys(objectKeys, 2),
895
+ ]).filter((edge) => allowedObjectKeys.has(edge.fromKey) && allowedObjectKeys.has(edge.toKey));
896
+ }
897
+ catch (error) {
898
+ graphError ??= error instanceof Error ? error.message : String(error);
899
+ }
700
900
  const queryRuns = runtimeCatalog.queryRunsForObjectKeys(objectKeys, 20);
701
901
  const diagnostics = catalog.diagnostics();
702
- const warnings = buildWarnings(diagnostics, objects);
902
+ const warnings = [
903
+ ...buildWarnings(diagnostics, objects),
904
+ ...(graphError ? [`Graph/context enrichment lane unavailable: ${graphError}`] : []),
905
+ ];
703
906
  const trustLabel = deriveTrust(objects);
704
907
  const citations = buildCitations(objects, contextEdges);
705
908
  const evidenceSummaries = buildEvidenceSummaries(objects, contextEdges, queryRuns, diagnostics);
@@ -796,6 +999,29 @@ export async function buildLocalContextPack(projectRoot, request) {
796
999
  retrievalDiagnostics: {
797
1000
  strategy: usedFullCatalog ? 'full_catalog' : 'sqlite_fts',
798
1001
  lanes: snapshotRetrieval.lanes,
1002
+ ...(snapshotRetrieval.fusion ? {
1003
+ fusion: {
1004
+ ...snapshotRetrieval.fusion,
1005
+ ...(graphError ? {
1006
+ lanes: {
1007
+ ...(snapshotRetrieval.fusion.lanes ?? {}),
1008
+ graph: {
1009
+ ...(snapshotRetrieval.fusion.lanes?.graph ?? { returned: 0 }),
1010
+ status: 'error',
1011
+ error: graphError,
1012
+ },
1013
+ },
1014
+ } : {}),
1015
+ },
1016
+ } : graphError ? {
1017
+ fusion: {
1018
+ selectedKeys: selected.map((object) => object.objectKey),
1019
+ truncated: false,
1020
+ lanes: {
1021
+ graph: { returned: 0, status: 'error', error: graphError },
1022
+ },
1023
+ },
1024
+ } : {}),
799
1025
  focusedModelAreaId: focusedArea?.id,
800
1026
  modelAreaSource: focusedArea?.source,
801
1027
  selectedObjects: objects.length,
@@ -1325,6 +1551,19 @@ function addManifestKnowledgeGraph(manifest, objects, edges) {
1325
1551
  const displayName = knowledgeDisplayName(item.kind, item.localId, itemPayload);
1326
1552
  const objectKey = knowledgeMetadataKey(item.kind, displayName, item.id, item.source.system);
1327
1553
  keyByGraphId.set(item.id, objectKey);
1554
+ // The registry emits an UNRESOLVED ALIAS beside the entity-qualified node
1555
+ // for the same semantic member: `semantic::metric.jaffle_shop.revenue`
1556
+ // (note the empty entity segment) beside `semantic:order_item:revenue`.
1557
+ // The alias carries no aggregation, grain, or prose — its description is
1558
+ // the raw dbt unique_id — so it can never be proven compatible, yet it
1559
+ // competes for bounded retrieval and clarification slots with the node that
1560
+ // can. 18 of 37 metrics in the commerce fixture were these.
1561
+ //
1562
+ // Skipping it is a no-op for meaning: it holds nothing the qualified node
1563
+ // does not. The graph-id mapping above is still recorded so edges keep
1564
+ // resolving. Anything carrying real payload is indexed as before.
1565
+ if (isUnresolvedSemanticAlias(item.kind, item.id, itemPayload))
1566
+ continue;
1328
1567
  const description = stringValue(itemPayload?.description)
1329
1568
  ?? stringValue(itemPayload?.businessContext)
1330
1569
  ?? itemAliases?.find((alias) => alias !== item.localId);
@@ -1410,6 +1649,32 @@ function addMetadataEdge(edges, edgeType, fromKey, toKey, payload) {
1410
1649
  if (!edges.has(key))
1411
1650
  edges.set(key, { edgeType, fromKey, toKey, confidence: 1, payload: compactObject(payload) });
1412
1651
  }
1652
+ /**
1653
+ * True for the registry's payload-less alias of a semantic member.
1654
+ *
1655
+ * A qualified semantic identity is `semantic:<entity>:<member>`. When the
1656
+ * owning entity could not be resolved the registry still emits the node with an
1657
+ * EMPTY entity segment — `semantic::metric.jaffle_shop.revenue` — carrying only
1658
+ * the source's native id. Requiring an entity segment is the same
1659
+ * entity-qualified identity rule the semantic catalog is built on.
1660
+ *
1661
+ * Anything holding real meaning (an aggregation, a label, an expression, or
1662
+ * LLM context) is NOT an alias and is kept, even unqualified.
1663
+ */
1664
+ function isUnresolvedSemanticAlias(kind, qualifiedId, payload) {
1665
+ if (!['metric', 'measure', 'dimension'].includes(kind))
1666
+ return false;
1667
+ if (!/^semantic::/.test(qualifiedId))
1668
+ return false;
1669
+ const record = payload ?? {};
1670
+ return !record.aggregation
1671
+ && !record.llmContext
1672
+ && !record.label
1673
+ && !record.expression
1674
+ && !record.formula
1675
+ && !record.dimensions
1676
+ && !record.entities;
1677
+ }
1413
1678
  function knowledgeMetadataKey(kind, localId, qualifiedId, sourceSystem) {
1414
1679
  switch (kind) {
1415
1680
  case 'block': return `dql:block:${localId}`;
@@ -6637,11 +6902,56 @@ const VECTOR_EXCLUDED_OBJECT_TYPES = new Set([
6637
6902
  'skill',
6638
6903
  'runtime_value',
6639
6904
  ]);
6905
+ /**
6906
+ * A DOCUMENTED column earns a vector, even though columns are excluded as a class.
6907
+ *
6908
+ * Excluding every column left a question phrased in column vocabulary ("which
6909
+ * field holds the customer's tier?") with no semantic recall path at all — the
6910
+ * lexical lane is the only thing that could ever find it, and that is exactly
6911
+ * the case where the user's words and the schema's words differ.
6912
+ *
6913
+ * The exclusion existed for volume, and that concern is real: columns outnumber
6914
+ * models by 10-50x and `searchVectorObjects` is an O(n) scan in JS. A DESCRIPTION
6915
+ * is the natural bound — it means someone documented the column on purpose, which
6916
+ * is both a quality signal and a small fraction of any real warehouse. An
6917
+ * undocumented column has nothing to embed but its name, which the lexical lane
6918
+ * already matches better than a vector would.
6919
+ */
6920
+ function isDocumentedColumnObject(object) {
6921
+ return COLUMN_OBJECT_TYPES.has(object.objectType)
6922
+ && object.objectType !== 'runtime_value'
6923
+ && (object.description?.trim().length ?? 0) > 0;
6924
+ }
6925
+ /**
6926
+ * A skill is written vocabulary — the words a team uses for a task — which is
6927
+ * exactly what a vector is for. Excluding the type wholesale meant a question
6928
+ * phrased in the team's language could only ever be found by keyword. Bounded
6929
+ * the same way as columns: it must actually be documented, so an empty skill
6930
+ * stub does not earn an embedding.
6931
+ */
6932
+ function isDocumentedSkillObject(object) {
6933
+ return object.objectType === 'skill' && (object.description?.trim().length ?? 0) > 0;
6934
+ }
6640
6935
  function isVectorIndexObject(object) {
6936
+ if (isDocumentedColumnObject(object))
6937
+ return true;
6938
+ if (isDocumentedSkillObject(object))
6939
+ return true;
6641
6940
  return !VECTOR_EXCLUDED_OBJECT_TYPES.has(object.objectType);
6642
6941
  }
6643
6942
  function metadataVectorText(object) {
6943
+ // A bare column name is a naked token: `status` on its own is equally close to
6944
+ // every status-ish thing in the warehouse. Lead with the parent-qualified name
6945
+ // and the type so the vector carries WHICH table's status this is.
6946
+ const columnPrefix = isDocumentedColumnObject(object)
6947
+ ? [
6948
+ `${object.fullName ?? object.name} column`,
6949
+ stringValue((object.payload ?? {}).type) ?? '',
6950
+ stringValue((object.payload ?? {}).relation) ?? '',
6951
+ ].filter(Boolean).join(' ')
6952
+ : '';
6644
6953
  return [
6954
+ columnPrefix,
6645
6955
  object.name,
6646
6956
  object.fullName ?? '',
6647
6957
  object.description ?? '',