@duckcodeailabs/dql-agent 1.6.34 → 1.7.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.
Files changed (90) hide show
  1. package/dist/agent-run-engine.d.ts +14 -1
  2. package/dist/agent-run-engine.d.ts.map +1 -1
  3. package/dist/agent-run-engine.js +123 -3
  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 +6 -0
  7. package/dist/agent-run-gates.js.map +1 -1
  8. package/dist/answer-loop.d.ts +57 -4
  9. package/dist/answer-loop.d.ts.map +1 -1
  10. package/dist/answer-loop.js +435 -44
  11. package/dist/answer-loop.js.map +1 -1
  12. package/dist/app-builder.d.ts.map +1 -1
  13. package/dist/app-builder.js +18 -10
  14. package/dist/app-builder.js.map +1 -1
  15. package/dist/cascade/budgets.d.ts.map +1 -1
  16. package/dist/cascade/budgets.js +8 -3
  17. package/dist/cascade/budgets.js.map +1 -1
  18. package/dist/cascade/route-policy.d.ts.map +1 -1
  19. package/dist/cascade/route-policy.js +5 -0
  20. package/dist/cascade/route-policy.js.map +1 -1
  21. package/dist/domain-context.d.ts +30 -0
  22. package/dist/domain-context.d.ts.map +1 -0
  23. package/dist/domain-context.js +68 -0
  24. package/dist/domain-context.js.map +1 -0
  25. package/dist/hints/git-store.d.ts +39 -3
  26. package/dist/hints/git-store.d.ts.map +1 -1
  27. package/dist/hints/git-store.js +172 -2
  28. package/dist/hints/git-store.js.map +1 -1
  29. package/dist/hints/types.d.ts +41 -0
  30. package/dist/hints/types.d.ts.map +1 -1
  31. package/dist/hints/types.js +1 -0
  32. package/dist/hints/types.js.map +1 -1
  33. package/dist/index.d.ts +7 -4
  34. package/dist/index.d.ts.map +1 -1
  35. package/dist/index.js +37 -2
  36. package/dist/index.js.map +1 -1
  37. package/dist/kg/build.d.ts.map +1 -1
  38. package/dist/kg/build.js +196 -4
  39. package/dist/kg/build.js.map +1 -1
  40. package/dist/kg/sqlite-fts.d.ts.map +1 -1
  41. package/dist/kg/sqlite-fts.js +8 -3
  42. package/dist/kg/sqlite-fts.js.map +1 -1
  43. package/dist/kg/types.d.ts +9 -2
  44. package/dist/kg/types.d.ts.map +1 -1
  45. package/dist/metadata/analysis-planner.d.ts.map +1 -1
  46. package/dist/metadata/analysis-planner.js +134 -18
  47. package/dist/metadata/analysis-planner.js.map +1 -1
  48. package/dist/metadata/analytical-policy.d.ts +51 -0
  49. package/dist/metadata/analytical-policy.d.ts.map +1 -0
  50. package/dist/metadata/analytical-policy.js +309 -0
  51. package/dist/metadata/analytical-policy.js.map +1 -0
  52. package/dist/metadata/block-fit.js +37 -4
  53. package/dist/metadata/block-fit.js.map +1 -1
  54. package/dist/metadata/catalog.d.ts +46 -1
  55. package/dist/metadata/catalog.d.ts.map +1 -1
  56. package/dist/metadata/catalog.js +313 -51
  57. package/dist/metadata/catalog.js.map +1 -1
  58. package/dist/metadata/dbt-first-safety.d.ts +19 -0
  59. package/dist/metadata/dbt-first-safety.d.ts.map +1 -0
  60. package/dist/metadata/dbt-first-safety.js +17 -0
  61. package/dist/metadata/dbt-first-safety.js.map +1 -0
  62. package/dist/metadata/sql-context-validation.d.ts.map +1 -1
  63. package/dist/metadata/sql-context-validation.js +42 -2
  64. package/dist/metadata/sql-context-validation.js.map +1 -1
  65. package/dist/metadata/sql-grounding.d.ts.map +1 -1
  66. package/dist/metadata/sql-grounding.js +25 -2
  67. package/dist/metadata/sql-grounding.js.map +1 -1
  68. package/dist/metadata/sql-retrieval.d.ts +2 -0
  69. package/dist/metadata/sql-retrieval.d.ts.map +1 -1
  70. package/dist/metadata/sql-retrieval.js +5 -1
  71. package/dist/metadata/sql-retrieval.js.map +1 -1
  72. package/dist/propose/build-from-prompt.d.ts +34 -0
  73. package/dist/propose/build-from-prompt.d.ts.map +1 -1
  74. package/dist/propose/build-from-prompt.js +199 -19
  75. package/dist/propose/build-from-prompt.js.map +1 -1
  76. package/dist/semantic-bridge/compose.d.ts.map +1 -1
  77. package/dist/semantic-bridge/compose.js +169 -5
  78. package/dist/semantic-bridge/compose.js.map +1 -1
  79. package/dist/skills/loader.d.ts +10 -0
  80. package/dist/skills/loader.d.ts.map +1 -1
  81. package/dist/skills/loader.js +81 -10
  82. package/dist/skills/loader.js.map +1 -1
  83. package/dist/synthesize.d.ts.map +1 -1
  84. package/dist/synthesize.js +152 -16
  85. package/dist/synthesize.js.map +1 -1
  86. package/dist/tools/registry.d.ts +1 -1
  87. package/dist/tools/registry.d.ts.map +1 -1
  88. package/dist/tools/registry.js +42 -0
  89. package/dist/tools/registry.js.map +1 -1
  90. package/package.json +4 -4
@@ -22,6 +22,7 @@ import { retrieveScopedHints } from '../hints/retrieval.js';
22
22
  import { buildFtsMatch, sanitizeFtsQuery } from '../memory/fts-query.js';
23
23
  import { envEmbeddingProvider } from '../embeddings/provider.js';
24
24
  import { matchExampleParaphrase } from './example-match.js';
25
+ import { loadSkills, selectRelevantSkills } from '../skills/loader.js';
25
26
  const require = createRequire(import.meta.url);
26
27
  let databaseCtor = null;
27
28
  function loadDatabase() {
@@ -96,7 +97,8 @@ export async function ensureMetadataCatalogFresh(projectRoot, options = {}) {
96
97
  ? options.semanticLayer ?? undefined
97
98
  : loadAgentSemanticLayer(projectRoot);
98
99
  const manifest = options.manifest ?? loadAgentManifest(projectRoot);
99
- const snapshot = buildMetadataSnapshot(projectRoot, manifest, semanticLayer);
100
+ const skills = options.skills ?? loadSkills(projectRoot).skills;
101
+ const snapshot = buildMetadataSnapshot(projectRoot, manifest, semanticLayer, skills);
100
102
  const catalog = openMetadataCatalog(projectRoot);
101
103
  try {
102
104
  const existing = catalog.state('fingerprint');
@@ -150,7 +152,18 @@ export function upsertMetadataSnapshot(projectRoot, snapshot) {
150
152
  }
151
153
  }
152
154
  export async function buildLocalContextPack(projectRoot, request) {
153
- await ensureMetadataCatalogFresh(projectRoot);
155
+ let prepared = false;
156
+ if (request.preparedMetadataFingerprint) {
157
+ const catalog = openMetadataCatalog(projectRoot);
158
+ try {
159
+ prepared = catalog.state('fingerprint') === request.preparedMetadataFingerprint;
160
+ }
161
+ finally {
162
+ catalog.close();
163
+ }
164
+ }
165
+ if (!prepared)
166
+ await ensureMetadataCatalogFresh(projectRoot);
154
167
  const catalog = openMetadataCatalog(projectRoot);
155
168
  try {
156
169
  const mode = request.mode ?? 'question';
@@ -163,7 +176,7 @@ export async function buildLocalContextPack(projectRoot, request) {
163
176
  // candidates only; a SHIFT ignores it. A metadata fingerprint mismatch
164
177
  // always disqualifies reuse.
165
178
  const reusePolicy = request.reusePolicy ?? 'seed';
166
- const priorPack = reusePolicy !== 'off'
179
+ const priorPack = !request.domainContext && reusePolicy !== 'off'
167
180
  && request.priorContextPackId
168
181
  && request.conversationTopicRelation
169
182
  && request.conversationTopicRelation !== 'shift'
@@ -211,18 +224,24 @@ export async function buildLocalContextPack(projectRoot, request) {
211
224
  buildFollowUpSearchQuery(request.question, followUp),
212
225
  ...questionPlan.searchQueries,
213
226
  ]);
214
- const searchRows = mergeObjects(searchQueries.flatMap((query) => catalog.searchObjects({
227
+ const scopeObjects = (rows) => filterMetadataObjectsByDomainContext(rows, request.domainContext);
228
+ // Skill guidance is injected only through selectContextPackSkills below.
229
+ // Leaving it in generic FTS results would bypass status/domain/area/exclusion
230
+ // eligibility simply because a word in its body matched the question.
231
+ const retrievalObjects = (rows) => scopeObjects(rows).filter((row) => row.objectType !== 'skill');
232
+ const searchRows = retrievalObjects(mergeObjects(searchQueries.flatMap((query) => catalog.searchObjects({
215
233
  query,
216
234
  objectTypes: request.objectTypes,
217
235
  limit: Math.max(request.limit ?? 80, 20),
218
- })));
236
+ }))));
219
237
  const schemaShapeCandidates = schemaShapeCandidateObjects(catalog, questionPlan, request, mergeObjects([...runtimeObjects, ...runtimeValueObjects]));
220
238
  const schemaShapeObjects = schemaShapeCandidates.map((candidate) => candidate.object);
221
- const exact = request.focusObjectKey ? catalog.getObject(request.focusObjectKey) : null;
239
+ const exactCandidate = request.focusObjectKey ? catalog.getObject(request.focusObjectKey) : null;
240
+ const exact = exactCandidate && retrievalObjects([exactCandidate]).length > 0 ? exactCandidate : null;
222
241
  const ranked = rankMetadataObjects({
223
- rows: mergeObjects(exact
242
+ rows: retrievalObjects(mergeObjects(exact
224
243
  ? [exact, ...followUpSourceObjects, ...followUpObjects, ...searchRows, ...schemaShapeObjects, ...runtimeObjects, ...runtimeValueObjects, ...selectedObjects, ...priorSeedObjects]
225
- : [...followUpSourceObjects, ...followUpObjects, ...searchRows, ...schemaShapeObjects, ...runtimeObjects, ...runtimeValueObjects, ...selectedObjects, ...priorSeedObjects]),
244
+ : [...followUpSourceObjects, ...followUpObjects, ...searchRows, ...schemaShapeObjects, ...runtimeObjects, ...runtimeValueObjects, ...selectedObjects, ...priorSeedObjects])),
226
245
  question: searchQueries.join(' '),
227
246
  questionPlan,
228
247
  limit: request.limit ?? 80,
@@ -236,9 +255,9 @@ export async function buildLocalContextPack(projectRoot, request) {
236
255
  const focusObjectKey = request.focusObjectKey ?? selected[0]?.objectKey ?? null;
237
256
  const edgeWalk = catalog.edgesForKeys(selected.map((row) => row.objectKey), 3);
238
257
  const edgeObjectKeys = Array.from(new Set(edgeWalk.flatMap((edge) => [edge.fromKey, edge.toKey])));
239
- const graphObjects = catalog.getObjectsByKeys(edgeObjectKeys);
258
+ const graphObjects = retrievalObjects(catalog.getObjectsByKeys(edgeObjectKeys));
240
259
  const rankedObjects = rankMetadataObjects({
241
- rows: mergeObjects([...followUpSourceObjects, ...followUpObjects, ...selected, ...graphObjects, ...schemaShapeObjects, ...runtimeObjects, ...runtimeValueObjects, ...selectedObjects]),
260
+ rows: retrievalObjects(mergeObjects([...followUpSourceObjects, ...followUpObjects, ...selected, ...graphObjects, ...schemaShapeObjects, ...runtimeObjects, ...runtimeValueObjects, ...selectedObjects])),
242
261
  question: searchQueries.join(' '),
243
262
  questionPlan,
244
263
  limit: request.limit ?? 120,
@@ -248,17 +267,23 @@ export async function buildLocalContextPack(projectRoot, request) {
248
267
  // entire relation set ("send everything, let the agent decide"). Only when the
249
268
  // whole catalog fits comfortably in context — otherwise we keep ranked selection.
250
269
  const fullCatalogObjects = request.strictness === 'exploratory'
251
- ? collectFullCatalogObjects(catalog)
270
+ ? retrievalObjects(collectFullCatalogObjects(catalog) ?? [])
252
271
  : undefined;
253
272
  const usedFullCatalog = Boolean(fullCatalogObjects);
273
+ // Skills are selected from the catalog snapshot with the same hard domain,
274
+ // area, status, and exclusion gates as the loader. They are not left to FTS
275
+ // chance or re-read from disk after the snapshot has been fingerprinted.
276
+ const selectedSkills = selectContextPackSkills(catalog.listObjects({ objectTypes: ['skill'], limit: 500 }), request.question, request.domainContext);
277
+ const selectedSkillObjects = selectedSkills.map((item) => item.object);
254
278
  const objects = fullCatalogObjects
255
- ? mergeObjects([...rankedObjects, ...sqlParentObjects, ...fullCatalogObjects])
256
- : mergeObjects([...rankedObjects, ...sqlParentObjects]);
279
+ ? mergeObjects([...rankedObjects, ...sqlParentObjects, ...fullCatalogObjects, ...selectedSkillObjects])
280
+ : mergeObjects([...rankedObjects, ...sqlParentObjects, ...selectedSkillObjects]);
257
281
  const objectKeys = objects.map((row) => row.objectKey);
282
+ const allowedObjectKeys = new Set(objectKeys);
258
283
  const contextEdges = mergeMetadataEdges([
259
284
  ...edgeWalk,
260
285
  ...catalog.edgesForKeys(objectKeys, 2),
261
- ]);
286
+ ]).filter((edge) => allowedObjectKeys.has(edge.fromKey) && allowedObjectKeys.has(edge.toKey));
262
287
  const queryRuns = catalog.queryRunsForObjectKeys(objectKeys, 20);
263
288
  const diagnostics = catalog.diagnostics();
264
289
  const warnings = buildWarnings(diagnostics, objects);
@@ -281,7 +306,7 @@ export async function buildLocalContextPack(projectRoot, request) {
281
306
  const selectedJoinPaths = buildSelectedJoinPaths(allowedSqlContext, contextEdges);
282
307
  const evidenceRoles = buildEvidenceRoles(objects, queryRuns);
283
308
  const reranked = rankMetadataObjects({
284
- rows: mergeObjects([...searchRows, ...schemaShapeObjects, ...objects]),
309
+ rows: retrievalObjects(mergeObjects([...searchRows, ...schemaShapeObjects, ...objects])),
285
310
  question: searchQueries.join(' '),
286
311
  questionPlan,
287
312
  limit: request.limit ?? 120,
@@ -323,6 +348,7 @@ export async function buildLocalContextPack(projectRoot, request) {
323
348
  trustLabel,
324
349
  trustLabelInfo: metadataTrustLabelInfo(trustLabel),
325
350
  objects,
351
+ skills: selectedSkills.map((item) => item.skill),
326
352
  edges: contextEdges,
327
353
  queryRuns,
328
354
  citations,
@@ -416,7 +442,7 @@ export function latestRuntimeSchemaSnapshotForProject(projectRoot) {
416
442
  catalog.close();
417
443
  }
418
444
  }
419
- export function buildMetadataSnapshot(projectRoot, manifest, semanticLayer) {
445
+ export function buildMetadataSnapshot(projectRoot, manifest, semanticLayer, skills = loadSkills(projectRoot).skills) {
420
446
  const manifestGraph = buildKGFromManifest(manifest);
421
447
  const semanticGraph = buildKGFromSemanticLayer(semanticLayer);
422
448
  const objects = new Map();
@@ -441,11 +467,16 @@ export function buildMetadataSnapshot(projectRoot, manifest, semanticLayer) {
441
467
  }
442
468
  }
443
469
  addManifestBlockDetails(manifest, objects);
470
+ addSkillObjects(skills, objects, edges);
444
471
  addDbtDagObjects(manifest, objects, edges, diagnostics);
445
472
  addRawDbtManifestCatalogObjects(projectRoot, manifest, objects, edges, diagnostics);
446
473
  addBlockDependencyEdges(manifest, edges);
447
474
  addBlockOutputLineageObjects(manifest, objects, edges);
448
- addDataLexManifestObjects(projectRoot, manifest, objects, edges, diagnostics);
475
+ // Manifest v3 is the unified DQL runtime. DataLex is migration input only;
476
+ // retaining it here would reintroduce a second analytical source of truth.
477
+ if (manifest.manifestVersion !== 3) {
478
+ addDataLexManifestObjects(projectRoot, manifest, objects, edges, diagnostics);
479
+ }
449
480
  const nodeKeyMap = new Map();
450
481
  for (const node of [...manifestGraph.nodes, ...semanticGraph.nodes]) {
451
482
  nodeKeyMap.set(node.nodeId, objectKeyFromKGNode(node));
@@ -1207,6 +1238,7 @@ function objectFromKGNode(node) {
1207
1238
  dimensions: node.dimensions ?? [],
1208
1239
  allowedFilters: node.allowedFilters ?? [],
1209
1240
  parameterPolicy: node.parameterPolicy ?? [],
1241
+ parameters: node.parameters ?? [],
1210
1242
  filterBindings: node.filterBindings ?? [],
1211
1243
  sourceSystems: node.sourceSystems ?? [],
1212
1244
  replacementFor: node.replacementFor ?? [],
@@ -1286,6 +1318,71 @@ function normalizeEdge(edge, fromKey, toKey) {
1286
1318
  payload: { kgSource: edge.src, kgTarget: edge.dst },
1287
1319
  };
1288
1320
  }
1321
+ /**
1322
+ * Skills are source-owned context, not a prompt-only side channel. Persist their
1323
+ * parsed fields in the catalog snapshot so selection, provenance, and
1324
+ * fingerprinting all observe the same immutable project state.
1325
+ */
1326
+ function addSkillObjects(skills, objects, edges) {
1327
+ for (const skill of skills) {
1328
+ const identity = skill.qualifiedId ?? skill.id;
1329
+ const objectKey = `skill:${identity}`;
1330
+ const domains = uniqueNonBlank([skill.domain, ...(skill.domains ?? [])]);
1331
+ const object = {
1332
+ objectKey,
1333
+ objectType: 'skill',
1334
+ name: skill.id,
1335
+ fullName: skill.qualifiedId ?? skill.id,
1336
+ domain: skill.domain,
1337
+ owner: skill.owner,
1338
+ status: skill.status ?? 'active',
1339
+ description: skill.description ?? compactSkillDescription(skill.body),
1340
+ sourcePath: skill.sourcePath,
1341
+ sourceSystem: 'DQL domain skill',
1342
+ payload: compactObject({
1343
+ skillId: skill.id,
1344
+ localId: skill.localId ?? skill.id,
1345
+ qualifiedId: skill.qualifiedId,
1346
+ scope: skill.scope,
1347
+ user: skill.user,
1348
+ domains,
1349
+ modelAreaRefs: skill.modelAreaRefs ?? [],
1350
+ kind: skill.kind,
1351
+ triggers: skill.triggers ?? [],
1352
+ exclusions: skill.exclusions ?? [],
1353
+ preferredMetrics: skill.preferredMetrics,
1354
+ preferredBlocks: skill.preferredBlocks,
1355
+ preferredDimensions: skill.preferredDimensions ?? [],
1356
+ requiredFilters: skill.requiredFilters ?? [],
1357
+ clarifyWhen: skill.clarifyWhen ?? [],
1358
+ examples: skill.examples ?? [],
1359
+ sourceRefs: skill.sourceRefs ?? [],
1360
+ vocabulary: skill.vocabulary,
1361
+ body: skill.body,
1362
+ isStarter: skill.isStarter,
1363
+ provenance: 'DQL domain skill',
1364
+ }),
1365
+ };
1366
+ objects.set(objectKey, object);
1367
+ for (const domain of domains) {
1368
+ const edge = {
1369
+ edgeType: 'contains',
1370
+ fromKey: `domain:${domain}`,
1371
+ toKey: objectKey,
1372
+ confidence: 1,
1373
+ payload: { provenance: 'DQL domain skill' },
1374
+ };
1375
+ edges.set(`${edge.edgeType}\u0000${edge.fromKey}\u0000${edge.toKey}`, edge);
1376
+ }
1377
+ }
1378
+ }
1379
+ function compactSkillDescription(body) {
1380
+ const normalized = body.replace(/\s+/g, ' ').trim();
1381
+ return normalized ? normalized.slice(0, 480) : undefined;
1382
+ }
1383
+ function uniqueNonBlank(values) {
1384
+ return Array.from(new Set(values.map((value) => value?.trim()).filter((value) => Boolean(value))));
1385
+ }
1289
1386
  function addDbtDagObjects(manifest, objects, edges, diagnostics) {
1290
1387
  const models = manifest.dbtImport?.dbtDag?.models ?? [];
1291
1388
  for (const model of models) {
@@ -1378,6 +1475,18 @@ function addRawDbtManifestCatalogObjects(projectRoot, manifest, objects, edges,
1378
1475
  const catalogColumns = loadRawDbtCatalogColumns(join(dirname(manifestPath), 'catalog.json'));
1379
1476
  const uniqueColumnsByModel = extractDbtUniqueColumns(raw.nodes ?? {});
1380
1477
  const entries = [];
1478
+ // PERF-001: at enterprise scale, keep columns in their model payload and
1479
+ // load full node detail lazily from the immutable dbt artifact cache. A
1480
+ // separate FTS row + graph edge for every column makes a 300k-column project
1481
+ // consume gigabytes without improving the bounded model-first route.
1482
+ const rawEntries = [...Object.values(raw.nodes ?? {}), ...Object.values(raw.sources ?? {})];
1483
+ const totalColumnCount = rawEntries.reduce((sum, node) => {
1484
+ const columns = node.columns;
1485
+ return sum + (columns && typeof columns === 'object' && !Array.isArray(columns)
1486
+ ? Object.keys(columns).length
1487
+ : 0);
1488
+ }, 0);
1489
+ const includeColumnObjects = totalColumnCount <= 50_000;
1381
1490
  for (const [uniqueId, node] of Object.entries(raw.nodes ?? {})) {
1382
1491
  if (node.resource_type !== 'model')
1383
1492
  continue;
@@ -1403,6 +1512,7 @@ function addRawDbtManifestCatalogObjects(projectRoot, manifest, objects, edges,
1403
1512
  ...entry,
1404
1513
  objects,
1405
1514
  edges,
1515
+ includeColumnObjects,
1406
1516
  });
1407
1517
  }
1408
1518
  for (const [uniqueId, source] of Object.entries(raw.sources ?? {})) {
@@ -1427,6 +1537,7 @@ function addRawDbtManifestCatalogObjects(projectRoot, manifest, objects, edges,
1427
1537
  ...entry,
1428
1538
  objects,
1429
1539
  edges,
1540
+ includeColumnObjects,
1430
1541
  });
1431
1542
  }
1432
1543
  const relationLookup = buildRawDbtRelationLookup(entries);
@@ -1470,36 +1581,37 @@ function addRawDbtCatalogObject(input) {
1470
1581
  uniqueColumns: input.uniqueColumns && input.uniqueColumns.length > 0 ? input.uniqueColumns : undefined,
1471
1582
  }),
1472
1583
  }));
1473
- for (const column of columns) {
1474
- const columnKey = `dbt:column:${input.name}.${column.name}`;
1475
- const existingColumn = input.objects.get(columnKey);
1476
- input.objects.set(columnKey, mergeObject(existingColumn, {
1477
- objectKey: columnKey,
1478
- objectType: 'dbt_column',
1479
- name: column.name,
1480
- fullName: `${input.name}.${column.name}`,
1481
- description: column.description,
1482
- status: existingColumn?.status ?? 'dbt_catalog',
1483
- sourcePath: stringValue(input.node.original_file_path) ?? stringValue(input.node.path),
1484
- sourceSystem: existingColumn?.sourceSystem ?? 'dbt manifest.json catalog',
1485
- payload: compactObject({
1486
- ...(existingColumn?.payload ?? {}),
1487
- model: input.name,
1488
- uniqueId: input.uniqueId,
1489
- type: column.type,
1490
- relation,
1491
- columnCompleteness: 'partial',
1492
- catalogOnly: existingColumn ? undefined : true,
1493
- }),
1494
- }));
1495
- putEdge(input.edges, {
1496
- edgeType: 'contains',
1497
- fromKey: input.objectKey,
1498
- toKey: columnKey,
1499
- confidence: 1,
1500
- payload: { source: 'raw dbt manifest column catalog' },
1501
- });
1502
- }
1584
+ if (input.includeColumnObjects)
1585
+ for (const column of columns) {
1586
+ const columnKey = `dbt:column:${input.name}.${column.name}`;
1587
+ const existingColumn = input.objects.get(columnKey);
1588
+ input.objects.set(columnKey, mergeObject(existingColumn, {
1589
+ objectKey: columnKey,
1590
+ objectType: 'dbt_column',
1591
+ name: column.name,
1592
+ fullName: `${input.name}.${column.name}`,
1593
+ description: column.description,
1594
+ status: existingColumn?.status ?? 'dbt_catalog',
1595
+ sourcePath: stringValue(input.node.original_file_path) ?? stringValue(input.node.path),
1596
+ sourceSystem: existingColumn?.sourceSystem ?? 'dbt manifest.json catalog',
1597
+ payload: compactObject({
1598
+ ...(existingColumn?.payload ?? {}),
1599
+ model: input.name,
1600
+ uniqueId: input.uniqueId,
1601
+ type: column.type,
1602
+ relation,
1603
+ columnCompleteness: 'partial',
1604
+ catalogOnly: existingColumn ? undefined : true,
1605
+ }),
1606
+ }));
1607
+ putEdge(input.edges, {
1608
+ edgeType: 'contains',
1609
+ fromKey: input.objectKey,
1610
+ toKey: columnKey,
1611
+ confidence: 1,
1612
+ payload: { source: 'raw dbt manifest column catalog' },
1613
+ });
1614
+ }
1503
1615
  for (const dep of rawDbtDependsOn(input.node)) {
1504
1616
  putEdge(input.edges, {
1505
1617
  edgeType: 'depends_on',
@@ -1720,6 +1832,7 @@ function addManifestBlockDetails(manifest, objects) {
1720
1832
  blockType: block.blockType,
1721
1833
  tests: block.tests,
1722
1834
  parameterPolicy: block.parameterPolicy,
1835
+ parameters: block.parameters,
1723
1836
  filterBindings: block.filterBindings,
1724
1837
  sqlFingerprints: buildBlockSqlFingerprints(block.sql),
1725
1838
  businessFingerprint: buildBlockBusinessFingerprint({
@@ -2295,9 +2408,34 @@ async function planContextPackRoute(input) {
2295
2408
  const exactByApplicability = [...applicabilityByKey.values()]
2296
2409
  .filter((item) => item.kind === 'exact_answer' || item.kind === 'safe_parameterized')
2297
2410
  .sort((a, b) => b.score - a.score)[0];
2411
+ // Analytical questions intentionally keep certified applicability conservative:
2412
+ // a lexical/content match alone must not terminate the cascade. A certified
2413
+ // block whose declared/inferred output contract fully covers the requested
2414
+ // answer shape is different, however — that is a proven Tier 1 answer even
2415
+ // when the planner also knows how to generate SQL. Promote only exact/trim-safe
2416
+ // contract fits here; near matches remain context for Tier 2.
2417
+ const exactByContract = input.objects
2418
+ .filter((object) => object.objectType === 'dql_block'
2419
+ && isCertifiedMetadataObject(object)
2420
+ && hasCompatibleMetadataRankingDirection(input.request.question, object))
2421
+ .map((object) => ({
2422
+ object,
2423
+ fit: evaluateCertifiedBlockFit({
2424
+ question: input.request.question,
2425
+ plan: input.questionPlan,
2426
+ block: object,
2427
+ exactExampleMatch: hasExactExampleQuestion(input.request.question, object),
2428
+ definitionLookup: intent === 'definition_lookup',
2429
+ }),
2430
+ applicabilityScore: applicabilityByKey.get(object.objectKey)?.score ?? 0,
2431
+ }))
2432
+ .filter(({ fit }) => (fit.kind === 'exact' || fit.kind === 'trim_safe')
2433
+ && (fit.confidence === 'high' || fit.confidence === 'medium'))
2434
+ .sort((a, b) => (b.fit.confidence === 'high' ? 1 : 0) - (a.fit.confidence === 'high' ? 1 : 0)
2435
+ || b.applicabilityScore - a.applicabilityScore)[0]?.object;
2298
2436
  let exact = exactByApplicability
2299
2437
  ? input.objects.find((object) => object.objectKey === exactByApplicability.objectKey)
2300
- : findExactCertifiedObject(input.request.question, intent, input.objects);
2438
+ : findExactCertifiedObject(input.request.question, intent, input.objects) ?? exactByContract;
2301
2439
  const contextApplicability = [...applicabilityByKey.values()]
2302
2440
  .filter((item) => item.kind === 'context_only')
2303
2441
  .sort((a, b) => b.score - a.score)[0];
@@ -2316,6 +2454,13 @@ async function planContextPackRoute(input) {
2316
2454
  paraphraseExampleMatch = true;
2317
2455
  }
2318
2456
  }
2457
+ // Keep the strongest context-only certified candidate attached to the route
2458
+ // so the contract and grain gates can explain exactly why it is demoted.
2459
+ // This does not promote a loose match: both gates still have to pass before
2460
+ // Tier 1 is possible.
2461
+ if (!exact && contextApplicability) {
2462
+ exact = input.objects.find((object) => object.objectKey === contextApplicability.objectKey);
2463
+ }
2319
2464
  const certifiedApplicability = exact
2320
2465
  ? applicabilityByKey.get(exact.objectKey) ?? certifiedApplicabilityForObject(exact, input.questionPlan)
2321
2466
  : undefined;
@@ -2382,6 +2527,7 @@ async function planContextPackRoute(input) {
2382
2527
  || intent === 'definition_lookup'
2383
2528
  || exactExampleMatch
2384
2529
  || paraphraseExampleMatch
2530
+ || (blockFit && certifiedFitAllowsTier1(blockFit) && hasCompatibleMetadataRankingDirection(input.request.question, exact))
2385
2531
  || (intent === 'ad_hoc_ranking' && objectNameInQuestion(input.request.question, exact)))) {
2386
2532
  return {
2387
2533
  route: 'certified',
@@ -2407,7 +2553,7 @@ async function planContextPackRoute(input) {
2407
2553
  route: 'generated_sql',
2408
2554
  intent: isGeneratedMetadataIntent(intent) ? intent : 'ad_hoc_ranking',
2409
2555
  reason: `Certified block "${exact.name}" is close but answers a different grain than the question, so it is context only and SQL is generated for the requested grain.`,
2410
- routeReason: grainGate.reason,
2556
+ routeReason: `${grainGate.reason}; routed to Tier 2 generated SQL`,
2411
2557
  grainGate: grainGateInfo,
2412
2558
  blockFit,
2413
2559
  trustLabel: input.trustLabel === 'certified' ? 'mixed' : input.trustLabel,
@@ -2425,7 +2571,7 @@ async function planContextPackRoute(input) {
2425
2571
  route: 'generated_sql',
2426
2572
  intent: isGeneratedMetadataIntent(intent) ? intent : 'ad_hoc_ranking',
2427
2573
  reason: `Certified block "${exact.name}" is relevant but does not satisfy the requested answer shape, so it is context only and SQL is generated for the requested result.`,
2428
- routeReason: blockFit.reasons.join('; '),
2574
+ routeReason: `${blockFit.reasons.join('; ')}; routed to Tier 2 generated SQL`,
2429
2575
  grainGate: grainGateInfo,
2430
2576
  blockFit,
2431
2577
  trustLabel: input.trustLabel === 'certified' ? 'mixed' : input.trustLabel,
@@ -2778,7 +2924,8 @@ function hasCompatibleMetadataRankingDirection(question, object) {
2778
2924
  const objectDirection = rankingDirection([
2779
2925
  object.name,
2780
2926
  object.description ?? '',
2781
- JSON.stringify(object.payload ?? {}),
2927
+ Array.isArray(object.payload?.tags) ? object.payload.tags.join(' ') : '',
2928
+ typeof object.payload?.sql === 'string' ? object.payload.sql : '',
2782
2929
  ].join(' '));
2783
2930
  if (!objectDirection)
2784
2931
  return true;
@@ -4756,6 +4903,121 @@ function reasonForObject(row) {
4756
4903
  return 'Published consumption context';
4757
4904
  return 'Relevant project metadata';
4758
4905
  }
4906
+ function filterMetadataObjectsByDomainContext(rows, context) {
4907
+ if (!context?.activeDomain)
4908
+ return rows;
4909
+ const domains = new Set([
4910
+ context.activeDomain,
4911
+ ...context.ancestors,
4912
+ ...context.allowedImports.map((item) => item.providerDomain),
4913
+ ]);
4914
+ return rows.filter((row) => !row.domain || domains.has(row.domain));
4915
+ }
4916
+ function selectContextPackSkills(objects, question, context) {
4917
+ const byIdentity = new Map();
4918
+ const parsed = objects.flatMap((object) => {
4919
+ const skill = skillFromMetadataObject(object);
4920
+ if (!skill)
4921
+ return [];
4922
+ const identity = skill.qualifiedId ?? skill.id;
4923
+ byIdentity.set(identity, object);
4924
+ return [skill];
4925
+ });
4926
+ const domains = context?.activeDomain
4927
+ ? [context.activeDomain, ...context.ancestors, ...context.allowedImports.map((item) => item.providerDomain)]
4928
+ : [];
4929
+ const selected = selectRelevantSkills(parsed, question, {
4930
+ domains,
4931
+ modelAreaIds: context?.modelAreaId ? [context.modelAreaId] : [],
4932
+ });
4933
+ return selected.flatMap((skill) => {
4934
+ const object = byIdentity.get(skill.qualifiedId ?? skill.id);
4935
+ return object ? [{ object, skill: localContextSkillFromParsed(skill, object) }] : [];
4936
+ });
4937
+ }
4938
+ function skillFromMetadataObject(object) {
4939
+ if (object.objectType !== 'skill')
4940
+ return null;
4941
+ const payload = object.payload ?? {};
4942
+ const scope = payload.scope === 'personal' ? 'personal' : 'project';
4943
+ const status = payload.status === 'draft' || payload.status === 'deprecated' || payload.status === 'active'
4944
+ ? payload.status
4945
+ : object.status === 'draft' || object.status === 'deprecated' || object.status === 'active'
4946
+ ? object.status
4947
+ : 'active';
4948
+ return {
4949
+ id: stringPayload(payload.skillId) ?? object.name,
4950
+ localId: stringPayload(payload.localId) ?? object.name,
4951
+ qualifiedId: stringPayload(payload.qualifiedId) ?? object.fullName,
4952
+ scope,
4953
+ user: stringPayload(payload.user),
4954
+ domain: object.domain,
4955
+ domains: stringArrayPayload(payload.domains),
4956
+ modelAreaRefs: stringArrayPayload(payload.modelAreaRefs),
4957
+ kind: skillKindPayload(payload.kind),
4958
+ status,
4959
+ owner: object.owner,
4960
+ triggers: stringArrayPayload(payload.triggers),
4961
+ exclusions: stringArrayPayload(payload.exclusions),
4962
+ description: object.description,
4963
+ preferredMetrics: stringArrayPayload(payload.preferredMetrics),
4964
+ preferredBlocks: stringArrayPayload(payload.preferredBlocks),
4965
+ preferredDimensions: stringArrayPayload(payload.preferredDimensions),
4966
+ requiredFilters: stringArrayPayload(payload.requiredFilters),
4967
+ clarifyWhen: stringArrayPayload(payload.clarifyWhen),
4968
+ examples: stringArrayPayload(payload.examples),
4969
+ sourceRefs: stringArrayPayload(payload.sourceRefs),
4970
+ vocabulary: stringRecordPayload(payload.vocabulary),
4971
+ body: stringPayload(payload.body) ?? '',
4972
+ sourcePath: object.sourcePath ?? '',
4973
+ isStarter: payload.isStarter === true,
4974
+ };
4975
+ }
4976
+ function localContextSkillFromParsed(skill, object) {
4977
+ const guidance = skill.body.slice(0, 4_000);
4978
+ return {
4979
+ objectKey: object.objectKey,
4980
+ id: skill.id,
4981
+ qualifiedId: skill.qualifiedId,
4982
+ domain: skill.domain,
4983
+ domains: [...(skill.domains ?? [])],
4984
+ modelAreaRefs: [...(skill.modelAreaRefs ?? [])],
4985
+ kind: skill.kind,
4986
+ status: skill.status,
4987
+ owner: skill.owner,
4988
+ description: skill.description,
4989
+ triggers: [...(skill.triggers ?? [])],
4990
+ exclusions: [...(skill.exclusions ?? [])],
4991
+ preferredMetrics: [...skill.preferredMetrics],
4992
+ preferredBlocks: [...skill.preferredBlocks],
4993
+ preferredDimensions: [...(skill.preferredDimensions ?? [])],
4994
+ requiredFilters: [...(skill.requiredFilters ?? [])],
4995
+ clarifyWhen: [...(skill.clarifyWhen ?? [])],
4996
+ vocabulary: { ...skill.vocabulary },
4997
+ guidance,
4998
+ guidanceTruncated: guidance.length < skill.body.length,
4999
+ sourceRefs: [...(skill.sourceRefs ?? [])],
5000
+ provenance: stringPayload(object.payload?.provenance) ?? object.sourceSystem ?? 'DQL domain skill',
5001
+ sourcePath: object.sourcePath,
5002
+ };
5003
+ }
5004
+ function stringPayload(value) {
5005
+ return typeof value === 'string' && value.trim() ? value : undefined;
5006
+ }
5007
+ function stringArrayPayload(value) {
5008
+ return Array.isArray(value) ? value.filter((item) => typeof item === 'string' && Boolean(item.trim())) : [];
5009
+ }
5010
+ function stringRecordPayload(value) {
5011
+ if (!value || typeof value !== 'object' || Array.isArray(value))
5012
+ return {};
5013
+ return Object.fromEntries(Object.entries(value).filter((entry) => typeof entry[1] === 'string'));
5014
+ }
5015
+ function skillKindPayload(value) {
5016
+ return value === 'domain_reference' || value === 'metric_policy' || value === 'glossary'
5017
+ || value === 'analysis_pattern' || value === 'sql_policy' || value === 'custom'
5018
+ ? value
5019
+ : undefined;
5020
+ }
4759
5021
  function mergeObjects(rows) {
4760
5022
  const byKey = new Map();
4761
5023
  for (const row of rows) {