@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.
- package/dist/agent-run-engine.d.ts +14 -1
- package/dist/agent-run-engine.d.ts.map +1 -1
- package/dist/agent-run-engine.js +123 -3
- package/dist/agent-run-engine.js.map +1 -1
- package/dist/agent-run-gates.d.ts.map +1 -1
- package/dist/agent-run-gates.js +6 -0
- package/dist/agent-run-gates.js.map +1 -1
- package/dist/answer-loop.d.ts +57 -4
- package/dist/answer-loop.d.ts.map +1 -1
- package/dist/answer-loop.js +435 -44
- package/dist/answer-loop.js.map +1 -1
- package/dist/app-builder.d.ts.map +1 -1
- package/dist/app-builder.js +18 -10
- package/dist/app-builder.js.map +1 -1
- package/dist/cascade/budgets.d.ts.map +1 -1
- package/dist/cascade/budgets.js +8 -3
- package/dist/cascade/budgets.js.map +1 -1
- package/dist/cascade/route-policy.d.ts.map +1 -1
- package/dist/cascade/route-policy.js +5 -0
- package/dist/cascade/route-policy.js.map +1 -1
- package/dist/domain-context.d.ts +30 -0
- package/dist/domain-context.d.ts.map +1 -0
- package/dist/domain-context.js +68 -0
- package/dist/domain-context.js.map +1 -0
- package/dist/hints/git-store.d.ts +39 -3
- package/dist/hints/git-store.d.ts.map +1 -1
- package/dist/hints/git-store.js +172 -2
- package/dist/hints/git-store.js.map +1 -1
- package/dist/hints/types.d.ts +41 -0
- package/dist/hints/types.d.ts.map +1 -1
- package/dist/hints/types.js +1 -0
- package/dist/hints/types.js.map +1 -1
- package/dist/index.d.ts +7 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +37 -2
- package/dist/index.js.map +1 -1
- package/dist/kg/build.d.ts.map +1 -1
- package/dist/kg/build.js +196 -4
- package/dist/kg/build.js.map +1 -1
- package/dist/kg/sqlite-fts.d.ts.map +1 -1
- package/dist/kg/sqlite-fts.js +8 -3
- package/dist/kg/sqlite-fts.js.map +1 -1
- package/dist/kg/types.d.ts +9 -2
- package/dist/kg/types.d.ts.map +1 -1
- package/dist/metadata/analysis-planner.d.ts.map +1 -1
- package/dist/metadata/analysis-planner.js +134 -18
- package/dist/metadata/analysis-planner.js.map +1 -1
- package/dist/metadata/analytical-policy.d.ts +51 -0
- package/dist/metadata/analytical-policy.d.ts.map +1 -0
- package/dist/metadata/analytical-policy.js +309 -0
- package/dist/metadata/analytical-policy.js.map +1 -0
- package/dist/metadata/block-fit.js +37 -4
- package/dist/metadata/block-fit.js.map +1 -1
- package/dist/metadata/catalog.d.ts +46 -1
- package/dist/metadata/catalog.d.ts.map +1 -1
- package/dist/metadata/catalog.js +313 -51
- package/dist/metadata/catalog.js.map +1 -1
- package/dist/metadata/dbt-first-safety.d.ts +19 -0
- package/dist/metadata/dbt-first-safety.d.ts.map +1 -0
- package/dist/metadata/dbt-first-safety.js +17 -0
- package/dist/metadata/dbt-first-safety.js.map +1 -0
- package/dist/metadata/sql-context-validation.d.ts.map +1 -1
- package/dist/metadata/sql-context-validation.js +42 -2
- package/dist/metadata/sql-context-validation.js.map +1 -1
- package/dist/metadata/sql-grounding.d.ts.map +1 -1
- package/dist/metadata/sql-grounding.js +25 -2
- package/dist/metadata/sql-grounding.js.map +1 -1
- package/dist/metadata/sql-retrieval.d.ts +2 -0
- package/dist/metadata/sql-retrieval.d.ts.map +1 -1
- package/dist/metadata/sql-retrieval.js +5 -1
- package/dist/metadata/sql-retrieval.js.map +1 -1
- package/dist/propose/build-from-prompt.d.ts +34 -0
- package/dist/propose/build-from-prompt.d.ts.map +1 -1
- package/dist/propose/build-from-prompt.js +199 -19
- package/dist/propose/build-from-prompt.js.map +1 -1
- package/dist/semantic-bridge/compose.d.ts.map +1 -1
- package/dist/semantic-bridge/compose.js +169 -5
- package/dist/semantic-bridge/compose.js.map +1 -1
- package/dist/skills/loader.d.ts +10 -0
- package/dist/skills/loader.d.ts.map +1 -1
- package/dist/skills/loader.js +81 -10
- package/dist/skills/loader.js.map +1 -1
- package/dist/synthesize.d.ts.map +1 -1
- package/dist/synthesize.js +152 -16
- package/dist/synthesize.js.map +1 -1
- package/dist/tools/registry.d.ts +1 -1
- package/dist/tools/registry.d.ts.map +1 -1
- package/dist/tools/registry.js +42 -0
- package/dist/tools/registry.js.map +1 -1
- package/package.json +4 -4
package/dist/metadata/catalog.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
1474
|
-
const
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
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
|
-
|
|
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) {
|