@duckcodeailabs/dql-agent 1.6.34 → 1.7.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 (95) 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 +36 -3
  13. package/dist/app-builder.d.ts.map +1 -1
  14. package/dist/app-builder.js +285 -22
  15. package/dist/app-builder.js.map +1 -1
  16. package/dist/cascade/budgets.d.ts.map +1 -1
  17. package/dist/cascade/budgets.js +8 -3
  18. package/dist/cascade/budgets.js.map +1 -1
  19. package/dist/cascade/route-policy.d.ts.map +1 -1
  20. package/dist/cascade/route-policy.js +5 -0
  21. package/dist/cascade/route-policy.js.map +1 -1
  22. package/dist/dashboard-story.d.ts +36 -0
  23. package/dist/dashboard-story.d.ts.map +1 -0
  24. package/dist/dashboard-story.js +140 -0
  25. package/dist/dashboard-story.js.map +1 -0
  26. package/dist/domain-context.d.ts +30 -0
  27. package/dist/domain-context.d.ts.map +1 -0
  28. package/dist/domain-context.js +76 -0
  29. package/dist/domain-context.js.map +1 -0
  30. package/dist/hints/git-store.d.ts +39 -3
  31. package/dist/hints/git-store.d.ts.map +1 -1
  32. package/dist/hints/git-store.js +172 -2
  33. package/dist/hints/git-store.js.map +1 -1
  34. package/dist/hints/types.d.ts +41 -0
  35. package/dist/hints/types.d.ts.map +1 -1
  36. package/dist/hints/types.js +1 -0
  37. package/dist/hints/types.js.map +1 -1
  38. package/dist/index.d.ts +10 -6
  39. package/dist/index.d.ts.map +1 -1
  40. package/dist/index.js +39 -3
  41. package/dist/index.js.map +1 -1
  42. package/dist/kg/build.d.ts.map +1 -1
  43. package/dist/kg/build.js +220 -4
  44. package/dist/kg/build.js.map +1 -1
  45. package/dist/kg/sqlite-fts.d.ts.map +1 -1
  46. package/dist/kg/sqlite-fts.js +8 -3
  47. package/dist/kg/sqlite-fts.js.map +1 -1
  48. package/dist/kg/types.d.ts +9 -2
  49. package/dist/kg/types.d.ts.map +1 -1
  50. package/dist/metadata/analysis-planner.d.ts.map +1 -1
  51. package/dist/metadata/analysis-planner.js +134 -18
  52. package/dist/metadata/analysis-planner.js.map +1 -1
  53. package/dist/metadata/analytical-policy.d.ts +51 -0
  54. package/dist/metadata/analytical-policy.d.ts.map +1 -0
  55. package/dist/metadata/analytical-policy.js +309 -0
  56. package/dist/metadata/analytical-policy.js.map +1 -0
  57. package/dist/metadata/block-fit.js +37 -4
  58. package/dist/metadata/block-fit.js.map +1 -1
  59. package/dist/metadata/catalog.d.ts +49 -1
  60. package/dist/metadata/catalog.d.ts.map +1 -1
  61. package/dist/metadata/catalog.js +365 -54
  62. package/dist/metadata/catalog.js.map +1 -1
  63. package/dist/metadata/dbt-first-safety.d.ts +19 -0
  64. package/dist/metadata/dbt-first-safety.d.ts.map +1 -0
  65. package/dist/metadata/dbt-first-safety.js +17 -0
  66. package/dist/metadata/dbt-first-safety.js.map +1 -0
  67. package/dist/metadata/sql-context-validation.d.ts.map +1 -1
  68. package/dist/metadata/sql-context-validation.js +42 -2
  69. package/dist/metadata/sql-context-validation.js.map +1 -1
  70. package/dist/metadata/sql-grounding.d.ts.map +1 -1
  71. package/dist/metadata/sql-grounding.js +25 -2
  72. package/dist/metadata/sql-grounding.js.map +1 -1
  73. package/dist/metadata/sql-retrieval.d.ts +2 -0
  74. package/dist/metadata/sql-retrieval.d.ts.map +1 -1
  75. package/dist/metadata/sql-retrieval.js +5 -1
  76. package/dist/metadata/sql-retrieval.js.map +1 -1
  77. package/dist/propose/build-from-prompt.d.ts +34 -0
  78. package/dist/propose/build-from-prompt.d.ts.map +1 -1
  79. package/dist/propose/build-from-prompt.js +199 -19
  80. package/dist/propose/build-from-prompt.js.map +1 -1
  81. package/dist/semantic-bridge/compose.d.ts.map +1 -1
  82. package/dist/semantic-bridge/compose.js +169 -5
  83. package/dist/semantic-bridge/compose.js.map +1 -1
  84. package/dist/skills/loader.d.ts +19 -0
  85. package/dist/skills/loader.d.ts.map +1 -1
  86. package/dist/skills/loader.js +107 -12
  87. package/dist/skills/loader.js.map +1 -1
  88. package/dist/synthesize.d.ts.map +1 -1
  89. package/dist/synthesize.js +152 -16
  90. package/dist/synthesize.js.map +1 -1
  91. package/dist/tools/registry.d.ts +1 -1
  92. package/dist/tools/registry.d.ts.map +1 -1
  93. package/dist/tools/registry.js +42 -0
  94. package/dist/tools/registry.js.map +1 -1
  95. 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,20 +224,32 @@ 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 areaObjects = filterMetadataObjectsByDomainContext(catalog.listObjects({ objectTypes: ['model_area'], limit: 500 }), request.domainContext);
228
+ const focusedArea = resolveFocusedModelArea(request.domainContext, request.question, areaObjects);
229
+ const effectiveDomainContext = focusedArea && request.domainContext
230
+ ? { ...request.domainContext, modelAreaId: focusedArea.id }
231
+ : request.domainContext;
232
+ const scopeObjects = (rows) => filterMetadataObjectsByDomainContext(rows, effectiveDomainContext);
233
+ // Skill guidance is injected only through selectContextPackSkills below.
234
+ // Leaving it in generic FTS results would bypass status/domain/area/exclusion
235
+ // eligibility simply because a word in its body matched the question.
236
+ const retrievalObjects = (rows) => scopeObjects(rows).filter((row) => row.objectType !== 'skill');
237
+ const searchRows = retrievalObjects(mergeObjects(searchQueries.flatMap((query) => catalog.searchObjects({
215
238
  query,
216
239
  objectTypes: request.objectTypes,
217
240
  limit: Math.max(request.limit ?? 80, 20),
218
- })));
241
+ }))));
219
242
  const schemaShapeCandidates = schemaShapeCandidateObjects(catalog, questionPlan, request, mergeObjects([...runtimeObjects, ...runtimeValueObjects]));
220
243
  const schemaShapeObjects = schemaShapeCandidates.map((candidate) => candidate.object);
221
- const exact = request.focusObjectKey ? catalog.getObject(request.focusObjectKey) : null;
244
+ const exactCandidate = request.focusObjectKey ? catalog.getObject(request.focusObjectKey) : null;
245
+ const exact = exactCandidate && retrievalObjects([exactCandidate]).length > 0 ? exactCandidate : null;
222
246
  const ranked = rankMetadataObjects({
223
- rows: mergeObjects(exact
224
- ? [exact, ...followUpSourceObjects, ...followUpObjects, ...searchRows, ...schemaShapeObjects, ...runtimeObjects, ...runtimeValueObjects, ...selectedObjects, ...priorSeedObjects]
225
- : [...followUpSourceObjects, ...followUpObjects, ...searchRows, ...schemaShapeObjects, ...runtimeObjects, ...runtimeValueObjects, ...selectedObjects, ...priorSeedObjects]),
247
+ rows: retrievalObjects(mergeObjects(exact
248
+ ? [exact, ...(focusedArea ? [focusedArea.object] : []), ...followUpSourceObjects, ...followUpObjects, ...searchRows, ...schemaShapeObjects, ...runtimeObjects, ...runtimeValueObjects, ...selectedObjects, ...priorSeedObjects]
249
+ : [...(focusedArea ? [focusedArea.object] : []), ...followUpSourceObjects, ...followUpObjects, ...searchRows, ...schemaShapeObjects, ...runtimeObjects, ...runtimeValueObjects, ...selectedObjects, ...priorSeedObjects])),
226
250
  question: searchQueries.join(' '),
227
251
  questionPlan,
252
+ modelAreaId: focusedArea?.id,
228
253
  limit: request.limit ?? 80,
229
254
  });
230
255
  // Advisory 'contextual' carry: the prior turn's block competes on rank (it is
@@ -236,11 +261,12 @@ export async function buildLocalContextPack(projectRoot, request) {
236
261
  const focusObjectKey = request.focusObjectKey ?? selected[0]?.objectKey ?? null;
237
262
  const edgeWalk = catalog.edgesForKeys(selected.map((row) => row.objectKey), 3);
238
263
  const edgeObjectKeys = Array.from(new Set(edgeWalk.flatMap((edge) => [edge.fromKey, edge.toKey])));
239
- const graphObjects = catalog.getObjectsByKeys(edgeObjectKeys);
264
+ const graphObjects = retrievalObjects(catalog.getObjectsByKeys(edgeObjectKeys));
240
265
  const rankedObjects = rankMetadataObjects({
241
- rows: mergeObjects([...followUpSourceObjects, ...followUpObjects, ...selected, ...graphObjects, ...schemaShapeObjects, ...runtimeObjects, ...runtimeValueObjects, ...selectedObjects]),
266
+ rows: retrievalObjects(mergeObjects([...followUpSourceObjects, ...followUpObjects, ...selected, ...graphObjects, ...schemaShapeObjects, ...runtimeObjects, ...runtimeValueObjects, ...selectedObjects])),
242
267
  question: searchQueries.join(' '),
243
268
  questionPlan,
269
+ modelAreaId: focusedArea?.id,
244
270
  limit: request.limit ?? 120,
245
271
  }).selected;
246
272
  const sqlParentObjects = sqlParentObjectsForSelectedColumns(rankedObjects, mergeObjects([...graphObjects, ...runtimeObjects]), questionPlan);
@@ -248,17 +274,23 @@ export async function buildLocalContextPack(projectRoot, request) {
248
274
  // entire relation set ("send everything, let the agent decide"). Only when the
249
275
  // whole catalog fits comfortably in context — otherwise we keep ranked selection.
250
276
  const fullCatalogObjects = request.strictness === 'exploratory'
251
- ? collectFullCatalogObjects(catalog)
277
+ ? retrievalObjects(collectFullCatalogObjects(catalog) ?? [])
252
278
  : undefined;
253
279
  const usedFullCatalog = Boolean(fullCatalogObjects);
280
+ // Skills are selected from the catalog snapshot with the same hard domain,
281
+ // area, status, and exclusion gates as the loader. They are not left to FTS
282
+ // chance or re-read from disk after the snapshot has been fingerprinted.
283
+ const selectedSkills = selectContextPackSkills(catalog.listObjects({ objectTypes: ['skill'], limit: 500 }), request.question, effectiveDomainContext);
284
+ const selectedSkillObjects = selectedSkills.map((item) => item.object);
254
285
  const objects = fullCatalogObjects
255
- ? mergeObjects([...rankedObjects, ...sqlParentObjects, ...fullCatalogObjects])
256
- : mergeObjects([...rankedObjects, ...sqlParentObjects]);
286
+ ? mergeObjects([...rankedObjects, ...sqlParentObjects, ...fullCatalogObjects, ...selectedSkillObjects])
287
+ : mergeObjects([...rankedObjects, ...sqlParentObjects, ...selectedSkillObjects]);
257
288
  const objectKeys = objects.map((row) => row.objectKey);
289
+ const allowedObjectKeys = new Set(objectKeys);
258
290
  const contextEdges = mergeMetadataEdges([
259
291
  ...edgeWalk,
260
292
  ...catalog.edgesForKeys(objectKeys, 2),
261
- ]);
293
+ ]).filter((edge) => allowedObjectKeys.has(edge.fromKey) && allowedObjectKeys.has(edge.toKey));
262
294
  const queryRuns = catalog.queryRunsForObjectKeys(objectKeys, 20);
263
295
  const diagnostics = catalog.diagnostics();
264
296
  const warnings = buildWarnings(diagnostics, objects);
@@ -281,9 +313,10 @@ export async function buildLocalContextPack(projectRoot, request) {
281
313
  const selectedJoinPaths = buildSelectedJoinPaths(allowedSqlContext, contextEdges);
282
314
  const evidenceRoles = buildEvidenceRoles(objects, queryRuns);
283
315
  const reranked = rankMetadataObjects({
284
- rows: mergeObjects([...searchRows, ...schemaShapeObjects, ...objects]),
316
+ rows: retrievalObjects(mergeObjects([...searchRows, ...schemaShapeObjects, ...objects])),
285
317
  question: searchQueries.join(' '),
286
318
  questionPlan,
319
+ modelAreaId: focusedArea?.id,
287
320
  limit: request.limit ?? 120,
288
321
  });
289
322
  const conflicts = buildCandidateConflicts(reranked.ranked);
@@ -323,6 +356,7 @@ export async function buildLocalContextPack(projectRoot, request) {
323
356
  trustLabel,
324
357
  trustLabelInfo: metadataTrustLabelInfo(trustLabel),
325
358
  objects,
359
+ skills: selectedSkills.map((item) => item.skill),
326
360
  edges: contextEdges,
327
361
  queryRuns,
328
362
  citations,
@@ -337,6 +371,8 @@ export async function buildLocalContextPack(projectRoot, request) {
337
371
  hintConflicts: hintResult.conflicts,
338
372
  retrievalDiagnostics: {
339
373
  strategy: usedFullCatalog ? 'full_catalog' : 'sqlite_fts',
374
+ focusedModelAreaId: focusedArea?.id,
375
+ modelAreaSource: focusedArea?.source,
340
376
  selectedObjects: objects.length,
341
377
  selectedEvidence: reranked.ranked.slice(0, 20).map((item) => ({
342
378
  objectKey: item.row.objectKey,
@@ -416,7 +452,7 @@ export function latestRuntimeSchemaSnapshotForProject(projectRoot) {
416
452
  catalog.close();
417
453
  }
418
454
  }
419
- export function buildMetadataSnapshot(projectRoot, manifest, semanticLayer) {
455
+ export function buildMetadataSnapshot(projectRoot, manifest, semanticLayer, skills = loadSkills(projectRoot).skills) {
420
456
  const manifestGraph = buildKGFromManifest(manifest);
421
457
  const semanticGraph = buildKGFromSemanticLayer(semanticLayer);
422
458
  const objects = new Map();
@@ -441,11 +477,16 @@ export function buildMetadataSnapshot(projectRoot, manifest, semanticLayer) {
441
477
  }
442
478
  }
443
479
  addManifestBlockDetails(manifest, objects);
480
+ addSkillObjects(skills, objects, edges);
444
481
  addDbtDagObjects(manifest, objects, edges, diagnostics);
445
482
  addRawDbtManifestCatalogObjects(projectRoot, manifest, objects, edges, diagnostics);
446
483
  addBlockDependencyEdges(manifest, edges);
447
484
  addBlockOutputLineageObjects(manifest, objects, edges);
448
- addDataLexManifestObjects(projectRoot, manifest, objects, edges, diagnostics);
485
+ // Manifest v3 is the unified DQL runtime. DataLex is migration input only;
486
+ // retaining it here would reintroduce a second analytical source of truth.
487
+ if (manifest.manifestVersion !== 3) {
488
+ addDataLexManifestObjects(projectRoot, manifest, objects, edges, diagnostics);
489
+ }
449
490
  const nodeKeyMap = new Map();
450
491
  for (const node of [...manifestGraph.nodes, ...semanticGraph.nodes]) {
451
492
  nodeKeyMap.set(node.nodeId, objectKeyFromKGNode(node));
@@ -1187,6 +1228,7 @@ function manifestDiagnosticToMetadataDiagnostic(diagnostic) {
1187
1228
  }
1188
1229
  function objectFromKGNode(node) {
1189
1230
  const payload = {
1231
+ ...(node.payload ?? {}),
1190
1232
  kgNodeId: node.nodeId,
1191
1233
  tags: node.tags ?? [],
1192
1234
  examples: node.examples ?? [],
@@ -1207,6 +1249,7 @@ function objectFromKGNode(node) {
1207
1249
  dimensions: node.dimensions ?? [],
1208
1250
  allowedFilters: node.allowedFilters ?? [],
1209
1251
  parameterPolicy: node.parameterPolicy ?? [],
1252
+ parameters: node.parameters ?? [],
1210
1253
  filterBindings: node.filterBindings ?? [],
1211
1254
  sourceSystems: node.sourceSystems ?? [],
1212
1255
  replacementFor: node.replacementFor ?? [],
@@ -1286,6 +1329,71 @@ function normalizeEdge(edge, fromKey, toKey) {
1286
1329
  payload: { kgSource: edge.src, kgTarget: edge.dst },
1287
1330
  };
1288
1331
  }
1332
+ /**
1333
+ * Skills are source-owned context, not a prompt-only side channel. Persist their
1334
+ * parsed fields in the catalog snapshot so selection, provenance, and
1335
+ * fingerprinting all observe the same immutable project state.
1336
+ */
1337
+ function addSkillObjects(skills, objects, edges) {
1338
+ for (const skill of skills) {
1339
+ const identity = skill.qualifiedId ?? skill.id;
1340
+ const objectKey = `skill:${identity}`;
1341
+ const domains = uniqueNonBlank([skill.domain, ...(skill.domains ?? [])]);
1342
+ const object = {
1343
+ objectKey,
1344
+ objectType: 'skill',
1345
+ name: skill.id,
1346
+ fullName: skill.qualifiedId ?? skill.id,
1347
+ domain: skill.domain,
1348
+ owner: skill.owner,
1349
+ status: skill.status ?? 'active',
1350
+ description: skill.description ?? compactSkillDescription(skill.body),
1351
+ sourcePath: skill.sourcePath,
1352
+ sourceSystem: 'DQL domain skill',
1353
+ payload: compactObject({
1354
+ skillId: skill.id,
1355
+ localId: skill.localId ?? skill.id,
1356
+ qualifiedId: skill.qualifiedId,
1357
+ scope: skill.scope,
1358
+ user: skill.user,
1359
+ domains,
1360
+ modelAreaRefs: skill.modelAreaRefs ?? [],
1361
+ kind: skill.kind,
1362
+ triggers: skill.triggers ?? [],
1363
+ exclusions: skill.exclusions ?? [],
1364
+ preferredMetrics: skill.preferredMetrics,
1365
+ preferredBlocks: skill.preferredBlocks,
1366
+ preferredDimensions: skill.preferredDimensions ?? [],
1367
+ requiredFilters: skill.requiredFilters ?? [],
1368
+ clarifyWhen: skill.clarifyWhen ?? [],
1369
+ examples: skill.examples ?? [],
1370
+ sourceRefs: skill.sourceRefs ?? [],
1371
+ vocabulary: skill.vocabulary,
1372
+ body: skill.body,
1373
+ isStarter: skill.isStarter,
1374
+ provenance: 'DQL domain skill',
1375
+ }),
1376
+ };
1377
+ objects.set(objectKey, object);
1378
+ for (const domain of domains) {
1379
+ const edge = {
1380
+ edgeType: 'contains',
1381
+ fromKey: `domain:${domain}`,
1382
+ toKey: objectKey,
1383
+ confidence: 1,
1384
+ payload: { provenance: 'DQL domain skill' },
1385
+ };
1386
+ edges.set(`${edge.edgeType}\u0000${edge.fromKey}\u0000${edge.toKey}`, edge);
1387
+ }
1388
+ }
1389
+ }
1390
+ function compactSkillDescription(body) {
1391
+ const normalized = body.replace(/\s+/g, ' ').trim();
1392
+ return normalized ? normalized.slice(0, 480) : undefined;
1393
+ }
1394
+ function uniqueNonBlank(values) {
1395
+ return Array.from(new Set(values.map((value) => value?.trim()).filter((value) => Boolean(value))));
1396
+ }
1289
1397
  function addDbtDagObjects(manifest, objects, edges, diagnostics) {
1290
1398
  const models = manifest.dbtImport?.dbtDag?.models ?? [];
1291
1399
  for (const model of models) {
@@ -1378,6 +1486,18 @@ function addRawDbtManifestCatalogObjects(projectRoot, manifest, objects, edges,
1378
1486
  const catalogColumns = loadRawDbtCatalogColumns(join(dirname(manifestPath), 'catalog.json'));
1379
1487
  const uniqueColumnsByModel = extractDbtUniqueColumns(raw.nodes ?? {});
1380
1488
  const entries = [];
1489
+ // PERF-001: at enterprise scale, keep columns in their model payload and
1490
+ // load full node detail lazily from the immutable dbt artifact cache. A
1491
+ // separate FTS row + graph edge for every column makes a 300k-column project
1492
+ // consume gigabytes without improving the bounded model-first route.
1493
+ const rawEntries = [...Object.values(raw.nodes ?? {}), ...Object.values(raw.sources ?? {})];
1494
+ const totalColumnCount = rawEntries.reduce((sum, node) => {
1495
+ const columns = node.columns;
1496
+ return sum + (columns && typeof columns === 'object' && !Array.isArray(columns)
1497
+ ? Object.keys(columns).length
1498
+ : 0);
1499
+ }, 0);
1500
+ const includeColumnObjects = totalColumnCount <= 50_000;
1381
1501
  for (const [uniqueId, node] of Object.entries(raw.nodes ?? {})) {
1382
1502
  if (node.resource_type !== 'model')
1383
1503
  continue;
@@ -1403,6 +1523,7 @@ function addRawDbtManifestCatalogObjects(projectRoot, manifest, objects, edges,
1403
1523
  ...entry,
1404
1524
  objects,
1405
1525
  edges,
1526
+ includeColumnObjects,
1406
1527
  });
1407
1528
  }
1408
1529
  for (const [uniqueId, source] of Object.entries(raw.sources ?? {})) {
@@ -1427,6 +1548,7 @@ function addRawDbtManifestCatalogObjects(projectRoot, manifest, objects, edges,
1427
1548
  ...entry,
1428
1549
  objects,
1429
1550
  edges,
1551
+ includeColumnObjects,
1430
1552
  });
1431
1553
  }
1432
1554
  const relationLookup = buildRawDbtRelationLookup(entries);
@@ -1470,36 +1592,37 @@ function addRawDbtCatalogObject(input) {
1470
1592
  uniqueColumns: input.uniqueColumns && input.uniqueColumns.length > 0 ? input.uniqueColumns : undefined,
1471
1593
  }),
1472
1594
  }));
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
- }
1595
+ if (input.includeColumnObjects)
1596
+ for (const column of columns) {
1597
+ const columnKey = `dbt:column:${input.name}.${column.name}`;
1598
+ const existingColumn = input.objects.get(columnKey);
1599
+ input.objects.set(columnKey, mergeObject(existingColumn, {
1600
+ objectKey: columnKey,
1601
+ objectType: 'dbt_column',
1602
+ name: column.name,
1603
+ fullName: `${input.name}.${column.name}`,
1604
+ description: column.description,
1605
+ status: existingColumn?.status ?? 'dbt_catalog',
1606
+ sourcePath: stringValue(input.node.original_file_path) ?? stringValue(input.node.path),
1607
+ sourceSystem: existingColumn?.sourceSystem ?? 'dbt manifest.json catalog',
1608
+ payload: compactObject({
1609
+ ...(existingColumn?.payload ?? {}),
1610
+ model: input.name,
1611
+ uniqueId: input.uniqueId,
1612
+ type: column.type,
1613
+ relation,
1614
+ columnCompleteness: 'partial',
1615
+ catalogOnly: existingColumn ? undefined : true,
1616
+ }),
1617
+ }));
1618
+ putEdge(input.edges, {
1619
+ edgeType: 'contains',
1620
+ fromKey: input.objectKey,
1621
+ toKey: columnKey,
1622
+ confidence: 1,
1623
+ payload: { source: 'raw dbt manifest column catalog' },
1624
+ });
1625
+ }
1503
1626
  for (const dep of rawDbtDependsOn(input.node)) {
1504
1627
  putEdge(input.edges, {
1505
1628
  edgeType: 'depends_on',
@@ -1720,6 +1843,7 @@ function addManifestBlockDetails(manifest, objects) {
1720
1843
  blockType: block.blockType,
1721
1844
  tests: block.tests,
1722
1845
  parameterPolicy: block.parameterPolicy,
1846
+ parameters: block.parameters,
1723
1847
  filterBindings: block.filterBindings,
1724
1848
  sqlFingerprints: buildBlockSqlFingerprints(block.sql),
1725
1849
  businessFingerprint: buildBlockBusinessFingerprint({
@@ -2295,9 +2419,34 @@ async function planContextPackRoute(input) {
2295
2419
  const exactByApplicability = [...applicabilityByKey.values()]
2296
2420
  .filter((item) => item.kind === 'exact_answer' || item.kind === 'safe_parameterized')
2297
2421
  .sort((a, b) => b.score - a.score)[0];
2422
+ // Analytical questions intentionally keep certified applicability conservative:
2423
+ // a lexical/content match alone must not terminate the cascade. A certified
2424
+ // block whose declared/inferred output contract fully covers the requested
2425
+ // answer shape is different, however — that is a proven Tier 1 answer even
2426
+ // when the planner also knows how to generate SQL. Promote only exact/trim-safe
2427
+ // contract fits here; near matches remain context for Tier 2.
2428
+ const exactByContract = input.objects
2429
+ .filter((object) => object.objectType === 'dql_block'
2430
+ && isCertifiedMetadataObject(object)
2431
+ && hasCompatibleMetadataRankingDirection(input.request.question, object))
2432
+ .map((object) => ({
2433
+ object,
2434
+ fit: evaluateCertifiedBlockFit({
2435
+ question: input.request.question,
2436
+ plan: input.questionPlan,
2437
+ block: object,
2438
+ exactExampleMatch: hasExactExampleQuestion(input.request.question, object),
2439
+ definitionLookup: intent === 'definition_lookup',
2440
+ }),
2441
+ applicabilityScore: applicabilityByKey.get(object.objectKey)?.score ?? 0,
2442
+ }))
2443
+ .filter(({ fit }) => (fit.kind === 'exact' || fit.kind === 'trim_safe')
2444
+ && (fit.confidence === 'high' || fit.confidence === 'medium'))
2445
+ .sort((a, b) => (b.fit.confidence === 'high' ? 1 : 0) - (a.fit.confidence === 'high' ? 1 : 0)
2446
+ || b.applicabilityScore - a.applicabilityScore)[0]?.object;
2298
2447
  let exact = exactByApplicability
2299
2448
  ? input.objects.find((object) => object.objectKey === exactByApplicability.objectKey)
2300
- : findExactCertifiedObject(input.request.question, intent, input.objects);
2449
+ : findExactCertifiedObject(input.request.question, intent, input.objects) ?? exactByContract;
2301
2450
  const contextApplicability = [...applicabilityByKey.values()]
2302
2451
  .filter((item) => item.kind === 'context_only')
2303
2452
  .sort((a, b) => b.score - a.score)[0];
@@ -2316,6 +2465,13 @@ async function planContextPackRoute(input) {
2316
2465
  paraphraseExampleMatch = true;
2317
2466
  }
2318
2467
  }
2468
+ // Keep the strongest context-only certified candidate attached to the route
2469
+ // so the contract and grain gates can explain exactly why it is demoted.
2470
+ // This does not promote a loose match: both gates still have to pass before
2471
+ // Tier 1 is possible.
2472
+ if (!exact && contextApplicability) {
2473
+ exact = input.objects.find((object) => object.objectKey === contextApplicability.objectKey);
2474
+ }
2319
2475
  const certifiedApplicability = exact
2320
2476
  ? applicabilityByKey.get(exact.objectKey) ?? certifiedApplicabilityForObject(exact, input.questionPlan)
2321
2477
  : undefined;
@@ -2382,6 +2538,7 @@ async function planContextPackRoute(input) {
2382
2538
  || intent === 'definition_lookup'
2383
2539
  || exactExampleMatch
2384
2540
  || paraphraseExampleMatch
2541
+ || (blockFit && certifiedFitAllowsTier1(blockFit) && hasCompatibleMetadataRankingDirection(input.request.question, exact))
2385
2542
  || (intent === 'ad_hoc_ranking' && objectNameInQuestion(input.request.question, exact)))) {
2386
2543
  return {
2387
2544
  route: 'certified',
@@ -2407,7 +2564,7 @@ async function planContextPackRoute(input) {
2407
2564
  route: 'generated_sql',
2408
2565
  intent: isGeneratedMetadataIntent(intent) ? intent : 'ad_hoc_ranking',
2409
2566
  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,
2567
+ routeReason: `${grainGate.reason}; routed to Tier 2 generated SQL`,
2411
2568
  grainGate: grainGateInfo,
2412
2569
  blockFit,
2413
2570
  trustLabel: input.trustLabel === 'certified' ? 'mixed' : input.trustLabel,
@@ -2425,7 +2582,7 @@ async function planContextPackRoute(input) {
2425
2582
  route: 'generated_sql',
2426
2583
  intent: isGeneratedMetadataIntent(intent) ? intent : 'ad_hoc_ranking',
2427
2584
  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('; '),
2585
+ routeReason: `${blockFit.reasons.join('; ')}; routed to Tier 2 generated SQL`,
2429
2586
  grainGate: grainGateInfo,
2430
2587
  blockFit,
2431
2588
  trustLabel: input.trustLabel === 'certified' ? 'mixed' : input.trustLabel,
@@ -2778,7 +2935,8 @@ function hasCompatibleMetadataRankingDirection(question, object) {
2778
2935
  const objectDirection = rankingDirection([
2779
2936
  object.name,
2780
2937
  object.description ?? '',
2781
- JSON.stringify(object.payload ?? {}),
2938
+ Array.isArray(object.payload?.tags) ? object.payload.tags.join(' ') : '',
2939
+ typeof object.payload?.sql === 'string' ? object.payload.sql : '',
2782
2940
  ].join(' '));
2783
2941
  if (!objectDirection)
2784
2942
  return true;
@@ -4614,12 +4772,13 @@ function rankMetadataObjects(args) {
4614
4772
  const scored = mergeObjects(args.rows).map((row) => {
4615
4773
  const baseScore = scoreMetadataObject(row, terms);
4616
4774
  const planScore = args.questionPlan ? scoreMetadataObjectWithAnalysisPlan(row, args.questionPlan) : { score: 0, reasons: [] };
4617
- const score = Number((baseScore + planScore.score).toFixed(3));
4775
+ const areaScore = modelAreaAffinityScore(row, args.modelAreaId);
4776
+ const score = Number((baseScore + planScore.score + areaScore).toFixed(3));
4618
4777
  return {
4619
4778
  row,
4620
4779
  rank: 0,
4621
4780
  score,
4622
- reason: selectionReason(row, score, planScore.reasons),
4781
+ reason: selectionReason(row, score, areaScore > 0 ? [...planScore.reasons, 'focused Model Area match'] : planScore.reasons),
4623
4782
  priorityTier: priorityTier(row),
4624
4783
  };
4625
4784
  });
@@ -4646,6 +4805,17 @@ function rankMetadataObjects(args) {
4646
4805
  })),
4647
4806
  };
4648
4807
  }
4808
+ function modelAreaAffinityScore(row, modelAreaId) {
4809
+ if (!modelAreaId)
4810
+ return 0;
4811
+ const qualifiedId = stringValue(row.payload?.qualifiedId);
4812
+ const areaId = stringValue(row.payload?.areaId);
4813
+ if (row.objectType === 'model_area' && qualifiedId === modelAreaId)
4814
+ return 48;
4815
+ if (areaId === modelAreaId)
4816
+ return 30;
4817
+ return 0;
4818
+ }
4649
4819
  function selectRankedMetadataObjects(ranked, limit) {
4650
4820
  if (ranked.length <= limit)
4651
4821
  return ranked;
@@ -4756,6 +4926,147 @@ function reasonForObject(row) {
4756
4926
  return 'Published consumption context';
4757
4927
  return 'Relevant project metadata';
4758
4928
  }
4929
+ function resolveFocusedModelArea(context, question, areaObjects) {
4930
+ if (!context?.activeDomain)
4931
+ return undefined;
4932
+ const candidates = areaObjects.filter((object) => object.objectType === 'model_area' && object.domain === context.activeDomain);
4933
+ if (context.modelAreaId) {
4934
+ const object = candidates.find((candidate) => stringValue(candidate.payload?.qualifiedId) === context.modelAreaId);
4935
+ return object ? { id: context.modelAreaId, object, source: 'explicit' } : undefined;
4936
+ }
4937
+ const terms = tokenize(question);
4938
+ if (terms.length === 0)
4939
+ return undefined;
4940
+ const ranked = candidates.map((object) => {
4941
+ const searchable = [
4942
+ object.name,
4943
+ object.description ?? '',
4944
+ ...metadataStringArray(object.payload?.intentExamples),
4945
+ ].join(' ');
4946
+ return { object, score: scoreText(searchable, terms) };
4947
+ }).sort((a, b) => b.score - a.score || a.object.name.localeCompare(b.object.name));
4948
+ const first = ranked[0];
4949
+ const second = ranked[1];
4950
+ if (!first || first.score < 1 || (second && first.score === second.score))
4951
+ return undefined;
4952
+ const id = stringValue(first.object.payload?.qualifiedId);
4953
+ return id ? { id, object: first.object, source: 'inferred' } : undefined;
4954
+ }
4955
+ function filterMetadataObjectsByDomainContext(rows, context) {
4956
+ if (!context?.activeDomain)
4957
+ return rows;
4958
+ const domains = new Set([
4959
+ context.activeDomain,
4960
+ ...context.ancestors,
4961
+ ...context.allowedImports.map((item) => item.providerDomain),
4962
+ ]);
4963
+ return rows.filter((row) => !row.domain || domains.has(row.domain));
4964
+ }
4965
+ function selectContextPackSkills(objects, question, context) {
4966
+ const byIdentity = new Map();
4967
+ const parsed = objects.flatMap((object) => {
4968
+ const skill = skillFromMetadataObject(object);
4969
+ if (!skill)
4970
+ return [];
4971
+ const identity = skill.qualifiedId ?? skill.id;
4972
+ byIdentity.set(identity, object);
4973
+ return [skill];
4974
+ });
4975
+ const domains = context?.activeDomain
4976
+ ? [context.activeDomain, ...context.ancestors, ...context.allowedImports.map((item) => item.providerDomain)]
4977
+ : [];
4978
+ const selected = selectRelevantSkills(parsed, question, {
4979
+ domains,
4980
+ modelAreaIds: context?.modelAreaId ? [context.modelAreaId] : [],
4981
+ });
4982
+ return selected.flatMap((skill) => {
4983
+ const object = byIdentity.get(skill.qualifiedId ?? skill.id);
4984
+ return object ? [{ object, skill: localContextSkillFromParsed(skill, object) }] : [];
4985
+ });
4986
+ }
4987
+ function skillFromMetadataObject(object) {
4988
+ if (object.objectType !== 'skill')
4989
+ return null;
4990
+ const payload = object.payload ?? {};
4991
+ const scope = payload.scope === 'personal' ? 'personal' : 'project';
4992
+ const status = payload.status === 'draft' || payload.status === 'deprecated' || payload.status === 'active'
4993
+ ? payload.status
4994
+ : object.status === 'draft' || object.status === 'deprecated' || object.status === 'active'
4995
+ ? object.status
4996
+ : 'active';
4997
+ return {
4998
+ id: stringPayload(payload.skillId) ?? object.name,
4999
+ localId: stringPayload(payload.localId) ?? object.name,
5000
+ qualifiedId: stringPayload(payload.qualifiedId) ?? object.fullName,
5001
+ scope,
5002
+ user: stringPayload(payload.user),
5003
+ domain: object.domain,
5004
+ domains: stringArrayPayload(payload.domains),
5005
+ modelAreaRefs: stringArrayPayload(payload.modelAreaRefs),
5006
+ kind: skillKindPayload(payload.kind),
5007
+ status,
5008
+ owner: object.owner,
5009
+ triggers: stringArrayPayload(payload.triggers),
5010
+ exclusions: stringArrayPayload(payload.exclusions),
5011
+ description: object.description,
5012
+ preferredMetrics: stringArrayPayload(payload.preferredMetrics),
5013
+ preferredBlocks: stringArrayPayload(payload.preferredBlocks),
5014
+ preferredDimensions: stringArrayPayload(payload.preferredDimensions),
5015
+ requiredFilters: stringArrayPayload(payload.requiredFilters),
5016
+ clarifyWhen: stringArrayPayload(payload.clarifyWhen),
5017
+ examples: stringArrayPayload(payload.examples),
5018
+ sourceRefs: stringArrayPayload(payload.sourceRefs),
5019
+ vocabulary: stringRecordPayload(payload.vocabulary),
5020
+ body: stringPayload(payload.body) ?? '',
5021
+ sourcePath: object.sourcePath ?? '',
5022
+ isStarter: payload.isStarter === true,
5023
+ };
5024
+ }
5025
+ function localContextSkillFromParsed(skill, object) {
5026
+ const guidance = skill.body.slice(0, 4_000);
5027
+ return {
5028
+ objectKey: object.objectKey,
5029
+ id: skill.id,
5030
+ qualifiedId: skill.qualifiedId,
5031
+ domain: skill.domain,
5032
+ domains: [...(skill.domains ?? [])],
5033
+ modelAreaRefs: [...(skill.modelAreaRefs ?? [])],
5034
+ kind: skill.kind,
5035
+ status: skill.status,
5036
+ owner: skill.owner,
5037
+ description: skill.description,
5038
+ triggers: [...(skill.triggers ?? [])],
5039
+ exclusions: [...(skill.exclusions ?? [])],
5040
+ preferredMetrics: [...skill.preferredMetrics],
5041
+ preferredBlocks: [...skill.preferredBlocks],
5042
+ preferredDimensions: [...(skill.preferredDimensions ?? [])],
5043
+ requiredFilters: [...(skill.requiredFilters ?? [])],
5044
+ clarifyWhen: [...(skill.clarifyWhen ?? [])],
5045
+ vocabulary: { ...skill.vocabulary },
5046
+ guidance,
5047
+ guidanceTruncated: guidance.length < skill.body.length,
5048
+ sourceRefs: [...(skill.sourceRefs ?? [])],
5049
+ provenance: stringPayload(object.payload?.provenance) ?? object.sourceSystem ?? 'DQL domain skill',
5050
+ sourcePath: object.sourcePath,
5051
+ };
5052
+ }
5053
+ function stringPayload(value) {
5054
+ return typeof value === 'string' && value.trim() ? value : undefined;
5055
+ }
5056
+ function stringArrayPayload(value) {
5057
+ return Array.isArray(value) ? value.filter((item) => typeof item === 'string' && Boolean(item.trim())) : [];
5058
+ }
5059
+ function stringRecordPayload(value) {
5060
+ if (!value || typeof value !== 'object' || Array.isArray(value))
5061
+ return {};
5062
+ return Object.fromEntries(Object.entries(value).filter((entry) => typeof entry[1] === 'string'));
5063
+ }
5064
+ function skillKindPayload(value) {
5065
+ return value === 'domain_reference' || value === 'metric_policy' || value === 'glossary'
5066
+ || value === 'analysis_pattern' || value === 'sql_policy' || value === 'custom'
5067
+ ? value
5068
+ : undefined;
5069
+ }
4759
5070
  function mergeObjects(rows) {
4760
5071
  const byKey = new Map();
4761
5072
  for (const row of rows) {