@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.
- 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 +36 -3
- package/dist/app-builder.d.ts.map +1 -1
- package/dist/app-builder.js +285 -22
- 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/dashboard-story.d.ts +36 -0
- package/dist/dashboard-story.d.ts.map +1 -0
- package/dist/dashboard-story.js +140 -0
- package/dist/dashboard-story.js.map +1 -0
- package/dist/domain-context.d.ts +30 -0
- package/dist/domain-context.d.ts.map +1 -0
- package/dist/domain-context.js +76 -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 +10 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +39 -3
- package/dist/index.js.map +1 -1
- package/dist/kg/build.d.ts.map +1 -1
- package/dist/kg/build.js +220 -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 +49 -1
- package/dist/metadata/catalog.d.ts.map +1 -1
- package/dist/metadata/catalog.js +365 -54
- 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 +19 -0
- package/dist/skills/loader.d.ts.map +1 -1
- package/dist/skills/loader.js +107 -12
- 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,20 +224,32 @@ export async function buildLocalContextPack(projectRoot, request) {
|
|
|
211
224
|
buildFollowUpSearchQuery(request.question, followUp),
|
|
212
225
|
...questionPlan.searchQueries,
|
|
213
226
|
]);
|
|
214
|
-
const
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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) {
|