@duckcodeailabs/dql-agent 1.6.18 → 1.6.20

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.
@@ -10,8 +10,9 @@ import { createHash } from 'node:crypto';
10
10
  import { mkdirSync, readFileSync } from 'node:fs';
11
11
  import { createRequire } from 'node:module';
12
12
  import { dirname, join } from 'node:path';
13
- import { buildManifest, loadProjectConfig, resolveDbtManifestPath, resolveSemanticLayerWithDiagnostics, } from '@duckcodeailabs/dql-core';
13
+ import { buildManifest, loadProjectConfig, parseContractRef, resolveDataLexManifestPath, resolveDbtManifestPath, resolveSemanticLayerWithDiagnostics, } from '@duckcodeailabs/dql-core';
14
14
  import { buildKGFromManifest, buildKGFromSemanticLayer } from '../kg/build.js';
15
+ import { buildBlockBusinessFingerprint, buildBlockSqlFingerprints } from './block-fingerprints.js';
15
16
  import { buildAnalysisQuestionPlan, certifiedApplicabilityForObject, scoreAllowedSqlRelationWithAnalysisPlan, scoreMetadataObjectWithAnalysisPlan, sortAllowedSqlContextForAnalysisPlan, } from './analysis-planner.js';
16
17
  import { extractSimpleSelectShape, sourceSqlShapeColumns } from './sql-shape.js';
17
18
  const require = createRequire(import.meta.url);
@@ -294,6 +295,7 @@ export function buildMetadataSnapshot(projectRoot, manifest, semanticLayer) {
294
295
  addDbtDagObjects(manifest, objects, edges, diagnostics);
295
296
  addRawDbtManifestCatalogObjects(projectRoot, manifest, objects, edges, diagnostics);
296
297
  addBlockDependencyEdges(manifest, edges);
298
+ addDataLexManifestObjects(projectRoot, manifest, objects, edges, diagnostics);
297
299
  const nodeKeyMap = new Map();
298
300
  for (const node of [...manifestGraph.nodes, ...semanticGraph.nodes]) {
299
301
  nodeKeyMap.set(node.nodeId, objectKeyFromKGNode(node));
@@ -423,6 +425,23 @@ export class MetadataCatalog {
423
425
  created_at TEXT NOT NULL
424
426
  );
425
427
  CREATE INDEX IF NOT EXISTS idx_metadata_diagnostics_severity ON metadata_diagnostics(severity);
428
+
429
+ CREATE TABLE IF NOT EXISTS metadata_source_fingerprints (
430
+ source_path TEXT PRIMARY KEY,
431
+ fingerprint TEXT NOT NULL,
432
+ object_count INTEGER NOT NULL,
433
+ updated_at TEXT NOT NULL
434
+ );
435
+
436
+ CREATE TABLE IF NOT EXISTS metadata_domain_shards (
437
+ domain TEXT PRIMARY KEY,
438
+ object_count INTEGER NOT NULL,
439
+ block_count INTEGER NOT NULL,
440
+ certified_block_count INTEGER NOT NULL,
441
+ semantic_metric_count INTEGER NOT NULL,
442
+ dbt_object_count INTEGER NOT NULL,
443
+ updated_at TEXT NOT NULL
444
+ );
426
445
  `);
427
446
  }
428
447
  rebuild(snapshot) {
@@ -450,11 +469,26 @@ export class MetadataCatalog {
450
469
  const setState = this.db.prepare(`
451
470
  INSERT OR REPLACE INTO metadata_state (key, value) VALUES (?, ?)
452
471
  `);
472
+ const insertSourceFingerprint = this.db.prepare(`
473
+ INSERT OR REPLACE INTO metadata_source_fingerprints (
474
+ source_path, fingerprint, object_count, updated_at
475
+ ) VALUES (?, ?, ?, ?)
476
+ `);
477
+ const insertDomainShard = this.db.prepare(`
478
+ INSERT OR REPLACE INTO metadata_domain_shards (
479
+ domain, object_count, block_count, certified_block_count,
480
+ semantic_metric_count, dbt_object_count, updated_at
481
+ ) VALUES (?, ?, ?, ?, ?, ?, ?)
482
+ `);
483
+ const sourceFingerprints = buildSourceFingerprints(snapshot.objects, now);
484
+ const domainShards = buildDomainShards(snapshot.objects, now);
453
485
  const txn = this.db.transaction(() => {
454
486
  this.db.prepare('DELETE FROM metadata_edges').run();
455
487
  this.db.prepare('DELETE FROM metadata_fts').run();
456
488
  this.db.prepare('DELETE FROM metadata_objects').run();
457
489
  this.db.prepare('DELETE FROM metadata_diagnostics').run();
490
+ this.db.prepare('DELETE FROM metadata_source_fingerprints').run();
491
+ this.db.prepare('DELETE FROM metadata_domain_shards').run();
458
492
  for (const object of snapshot.objects) {
459
493
  const payload = object.payload ?? {};
460
494
  insertObject.run(object.objectKey, object.objectType, object.name, object.fullName ?? null, object.domain ?? null, object.owner ?? null, object.status ?? null, object.description ?? null, object.sourcePath ?? null, object.sourceSystem ?? null, JSON.stringify(payload), object.updatedAt ?? now);
@@ -466,11 +500,19 @@ export class MetadataCatalog {
466
500
  for (const diagnostic of snapshot.diagnostics) {
467
501
  insertDiagnostic.run(diagnosticId(diagnostic), diagnostic.kind, diagnostic.severity, diagnostic.message, diagnostic.objectKey ?? null, diagnostic.filePath ?? null, now);
468
502
  }
503
+ for (const item of sourceFingerprints) {
504
+ insertSourceFingerprint.run(item.sourcePath, item.fingerprint, item.objectCount, item.updatedAt);
505
+ }
506
+ for (const item of domainShards) {
507
+ insertDomainShard.run(item.domain, item.objectCount, item.blockCount, item.certifiedBlockCount, item.semanticMetricCount, item.dbtObjectCount, item.updatedAt);
508
+ }
469
509
  setState.run('built_at', now);
470
510
  setState.run('fingerprint', snapshot.fingerprint);
471
511
  setState.run('project_root', snapshot.projectRoot);
472
512
  setState.run('object_count', String(snapshot.objects.length));
473
513
  setState.run('edge_count', String(snapshot.edges.length));
514
+ setState.run('source_fingerprint_count', String(sourceFingerprints.length));
515
+ setState.run('domain_shard_count', String(domainShards.length));
474
516
  setState.run('diagnostics_json', JSON.stringify(snapshot.diagnostics));
475
517
  setState.run('manifest_generated_at', snapshot.manifest.generatedAt);
476
518
  });
@@ -696,6 +738,38 @@ export class MetadataCatalog {
696
738
  filePath: row.file_path ?? undefined,
697
739
  }));
698
740
  }
741
+ sourceFingerprints(limit = 500) {
742
+ const rows = this.db.prepare(`
743
+ SELECT source_path, fingerprint, object_count, updated_at
744
+ FROM metadata_source_fingerprints
745
+ ORDER BY object_count DESC, source_path
746
+ LIMIT ?
747
+ `).all(limit);
748
+ return rows.map((row) => ({
749
+ sourcePath: row.source_path,
750
+ fingerprint: row.fingerprint,
751
+ objectCount: row.object_count,
752
+ updatedAt: row.updated_at,
753
+ }));
754
+ }
755
+ domainShards(limit = 100) {
756
+ const rows = this.db.prepare(`
757
+ SELECT domain, object_count, block_count, certified_block_count,
758
+ semantic_metric_count, dbt_object_count, updated_at
759
+ FROM metadata_domain_shards
760
+ ORDER BY object_count DESC, domain
761
+ LIMIT ?
762
+ `).all(limit);
763
+ return rows.map((row) => ({
764
+ domain: row.domain,
765
+ objectCount: row.object_count,
766
+ blockCount: row.block_count,
767
+ certifiedBlockCount: row.certified_block_count,
768
+ semanticMetricCount: row.semantic_metric_count,
769
+ dbtObjectCount: row.dbt_object_count,
770
+ updatedAt: row.updated_at,
771
+ }));
772
+ }
699
773
  state(key) {
700
774
  const row = this.db.prepare('SELECT value FROM metadata_state WHERE key = ?').get(key);
701
775
  return row?.value ?? null;
@@ -754,6 +828,21 @@ function objectFromKGNode(node) {
754
828
  businessOwner: node.businessOwner,
755
829
  decisionUse: node.decisionUse,
756
830
  reviewCadence: node.reviewCadence,
831
+ pattern: node.pattern,
832
+ grain: node.grain,
833
+ entities: node.entities ?? [],
834
+ declaredOutputs: node.declaredOutputs ?? [],
835
+ dimensions: node.dimensions ?? [],
836
+ allowedFilters: node.allowedFilters ?? [],
837
+ parameterPolicy: node.parameterPolicy ?? [],
838
+ filterBindings: node.filterBindings ?? [],
839
+ sourceSystems: node.sourceSystems ?? [],
840
+ replacementFor: node.replacementFor ?? [],
841
+ sqlFingerprints: node.sqlFingerprints,
842
+ businessFingerprint: node.businessFingerprint,
843
+ datalexContract: node.datalexContract,
844
+ boundedContext: node.boundedContext,
845
+ primaryTerms: node.primaryTerms ?? [],
757
846
  businessRules: node.businessRules ?? [],
758
847
  caveats: node.caveats ?? [],
759
848
  llmContext: node.llmContext,
@@ -1056,14 +1145,206 @@ function addManifestBlockDetails(manifest, objects) {
1056
1145
  refDependencies: block.refDependencies,
1057
1146
  metricRefs: block.metricRefs,
1058
1147
  dimensionRefs: block.dimensionRefs,
1148
+ dimensions: block.dimensions,
1059
1149
  chartType: block.chartType,
1060
1150
  blockType: block.blockType,
1061
1151
  tests: block.tests,
1152
+ parameterPolicy: block.parameterPolicy,
1153
+ filterBindings: block.filterBindings,
1154
+ sqlFingerprints: buildBlockSqlFingerprints(block.sql),
1155
+ businessFingerprint: buildBlockBusinessFingerprint({
1156
+ name: block.name,
1157
+ domain: block.domain,
1158
+ pattern: block.pattern,
1159
+ grain: block.grain,
1160
+ entities: block.entities,
1161
+ terms: block.termRefs,
1162
+ outputs: block.declaredOutputs,
1163
+ dimensions: block.dimensions,
1164
+ filters: block.allowedFilters,
1165
+ sources: [...(block.tableDependencies ?? []), ...(block.rawTableRefs ?? [])],
1166
+ sourceSystems: block.sourceSystems,
1167
+ }),
1062
1168
  draftMetadata: block.draftMetadata,
1063
1169
  }),
1064
1170
  }));
1065
1171
  }
1066
1172
  }
1173
+ function addDataLexManifestObjects(projectRoot, manifest, objects, edges, diagnostics) {
1174
+ const manifestPath = resolveDataLexManifestPath(projectRoot);
1175
+ if (!manifestPath)
1176
+ return;
1177
+ let raw;
1178
+ try {
1179
+ raw = JSON.parse(readFileSync(manifestPath, 'utf-8'));
1180
+ }
1181
+ catch (error) {
1182
+ diagnostics.push({
1183
+ kind: 'datalex',
1184
+ severity: 'warning',
1185
+ message: `Could not read DataLex manifest context from ${manifestPath}: ${error instanceof Error ? error.message : String(error)}`,
1186
+ filePath: manifestPath,
1187
+ });
1188
+ return;
1189
+ }
1190
+ if (!Array.isArray(raw.domains)) {
1191
+ diagnostics.push({
1192
+ kind: 'datalex',
1193
+ severity: 'warning',
1194
+ message: `DataLex manifest at ${manifestPath} does not contain a domains array.`,
1195
+ filePath: manifestPath,
1196
+ });
1197
+ return;
1198
+ }
1199
+ const latestContractKeyById = new Map();
1200
+ const contractKeyByVersionedRef = new Map();
1201
+ for (const domain of raw.domains) {
1202
+ if (!domain?.name)
1203
+ continue;
1204
+ const domainKey = `datalex:domain:${domain.name}`;
1205
+ objects.set(domainKey, mergeObject(objects.get(domainKey), {
1206
+ objectKey: domainKey,
1207
+ objectType: 'datalex_domain',
1208
+ name: domain.name,
1209
+ fullName: domain.name,
1210
+ domain: domain.name,
1211
+ owner: domain.owners?.[0],
1212
+ status: 'contract_evidence',
1213
+ description: domain.description,
1214
+ sourcePath: manifestPath,
1215
+ sourceSystem: 'DataLex manifest',
1216
+ payload: compactObject({
1217
+ project: raw.project?.name,
1218
+ owners: domain.owners ?? [],
1219
+ tags: [],
1220
+ generatedAt: raw.generatedAt,
1221
+ }),
1222
+ }));
1223
+ for (const term of domain.glossary ?? []) {
1224
+ if (!term?.term)
1225
+ continue;
1226
+ const termKey = `datalex:term:${domain.name}.${term.term}`;
1227
+ objects.set(termKey, mergeObject(objects.get(termKey), {
1228
+ objectKey: termKey,
1229
+ objectType: 'datalex_term',
1230
+ name: term.term,
1231
+ fullName: `${domain.name}.${term.term}`,
1232
+ domain: domain.name,
1233
+ status: 'contract_evidence',
1234
+ description: term.definition,
1235
+ sourcePath: manifestPath,
1236
+ sourceSystem: 'DataLex manifest',
1237
+ payload: compactObject({
1238
+ tags: term.tags ?? [],
1239
+ relatedFields: term.related_fields ?? [],
1240
+ }),
1241
+ }));
1242
+ putEdge(edges, {
1243
+ edgeType: 'contains',
1244
+ fromKey: domainKey,
1245
+ toKey: termKey,
1246
+ confidence: 1,
1247
+ payload: { source: 'datalex manifest glossary' },
1248
+ });
1249
+ }
1250
+ for (const entity of domain.entities ?? []) {
1251
+ if (!entity?.name)
1252
+ continue;
1253
+ const entityKey = `datalex:entity:${domain.name}.${entity.name}`;
1254
+ objects.set(entityKey, mergeObject(objects.get(entityKey), {
1255
+ objectKey: entityKey,
1256
+ objectType: 'datalex_entity',
1257
+ name: entity.name,
1258
+ fullName: `${domain.name}.${entity.name}`,
1259
+ domain: domain.name,
1260
+ status: 'contract_evidence',
1261
+ description: entity.description,
1262
+ sourcePath: manifestPath,
1263
+ sourceSystem: 'DataLex manifest',
1264
+ payload: compactObject({
1265
+ tags: entity.tags ?? [],
1266
+ binding: entity.binding,
1267
+ fields: (entity.fields ?? []).slice(0, 100).map((field) => compactObject({
1268
+ name: field.name,
1269
+ type: field.type,
1270
+ description: field.description,
1271
+ primaryKey: field.primary_key,
1272
+ classification: field.classification,
1273
+ tags: field.tags ?? [],
1274
+ })),
1275
+ }),
1276
+ }));
1277
+ putEdge(edges, {
1278
+ edgeType: 'contains',
1279
+ fromKey: domainKey,
1280
+ toKey: entityKey,
1281
+ confidence: 1,
1282
+ payload: { source: 'datalex manifest entity' },
1283
+ });
1284
+ for (const contract of entity.contracts ?? []) {
1285
+ if (!contract?.id)
1286
+ continue;
1287
+ const version = Number(contract.version);
1288
+ if (!Number.isFinite(version))
1289
+ continue;
1290
+ const contractKey = `datalex:contract:${contract.id}@${version}`;
1291
+ contractKeyByVersionedRef.set(`${contract.id}@${version}`, contractKey);
1292
+ const latest = latestContractKeyById.get(contract.id);
1293
+ if (!latest || Number(latest.split('@').at(-1) ?? 0) < version) {
1294
+ latestContractKeyById.set(contract.id, contractKey);
1295
+ }
1296
+ objects.set(contractKey, mergeObject(objects.get(contractKey), {
1297
+ objectKey: contractKey,
1298
+ objectType: 'datalex_contract',
1299
+ name: contract.name || contract.id,
1300
+ fullName: `${contract.id}@${contract.version}`,
1301
+ domain: domain.name,
1302
+ owner: contract.owner ?? domain.owners?.[0],
1303
+ status: 'contract_evidence',
1304
+ description: contract.description,
1305
+ sourcePath: manifestPath,
1306
+ sourceSystem: 'DataLex manifest',
1307
+ payload: compactObject({
1308
+ contractId: contract.id,
1309
+ version,
1310
+ entity: entity.name,
1311
+ tags: contract.tags ?? [],
1312
+ signature: contract.signature,
1313
+ }),
1314
+ }));
1315
+ putEdge(edges, {
1316
+ edgeType: 'contains',
1317
+ fromKey: entityKey,
1318
+ toKey: contractKey,
1319
+ confidence: 1,
1320
+ payload: { source: 'datalex manifest contract' },
1321
+ });
1322
+ }
1323
+ }
1324
+ }
1325
+ for (const block of Object.values(manifest.blocks ?? {})) {
1326
+ if (!block.datalexContract)
1327
+ continue;
1328
+ const parsed = parseContractRef(block.datalexContract);
1329
+ if (!parsed.ok || !parsed.id)
1330
+ continue;
1331
+ const contractKey = parsed.version
1332
+ ? contractKeyByVersionedRef.get(`${parsed.id}@${parsed.version}`)
1333
+ : latestContractKeyById.get(parsed.id);
1334
+ if (!contractKey)
1335
+ continue;
1336
+ putEdge(edges, {
1337
+ edgeType: 'resolves_contract',
1338
+ fromKey: `dql:block:${block.name}`,
1339
+ toKey: contractKey,
1340
+ confidence: 1,
1341
+ payload: {
1342
+ source: 'dql datalex_contract',
1343
+ reference: block.datalexContract,
1344
+ },
1345
+ });
1346
+ }
1347
+ }
1067
1348
  function addBlockDependencyEdges(manifest, edges) {
1068
1349
  const dbtLookup = buildDbtModelLookup(manifest);
1069
1350
  for (const block of Object.values(manifest.blocks ?? {})) {
@@ -2376,6 +2657,48 @@ function mergeObject(a, b) {
2376
2657
  payload: compactObject({ ...(a.payload ?? {}), ...(b.payload ?? {}) }),
2377
2658
  };
2378
2659
  }
2660
+ function buildSourceFingerprints(objects, updatedAt) {
2661
+ const bySource = new Map();
2662
+ for (const object of objects) {
2663
+ const sourcePath = object.sourcePath ?? object.sourceSystem;
2664
+ if (!sourcePath)
2665
+ continue;
2666
+ const list = bySource.get(sourcePath) ?? [];
2667
+ list.push(object);
2668
+ bySource.set(sourcePath, list);
2669
+ }
2670
+ return Array.from(bySource.entries()).map(([sourcePath, rows]) => ({
2671
+ sourcePath,
2672
+ fingerprint: sha256(stableStringify(rows.map((row) => ({
2673
+ objectKey: row.objectKey,
2674
+ objectType: row.objectType,
2675
+ name: row.name,
2676
+ domain: row.domain,
2677
+ status: row.status,
2678
+ payload: row.payload,
2679
+ })))),
2680
+ objectCount: rows.length,
2681
+ updatedAt,
2682
+ })).sort((a, b) => a.sourcePath.localeCompare(b.sourcePath));
2683
+ }
2684
+ function buildDomainShards(objects, updatedAt) {
2685
+ const byDomain = new Map();
2686
+ for (const object of objects) {
2687
+ const domain = object.domain?.trim() || '_uncategorized';
2688
+ const list = byDomain.get(domain) ?? [];
2689
+ list.push(object);
2690
+ byDomain.set(domain, list);
2691
+ }
2692
+ return Array.from(byDomain.entries()).map(([domain, rows]) => ({
2693
+ domain,
2694
+ objectCount: rows.length,
2695
+ blockCount: rows.filter((row) => row.objectType === 'dql_block').length,
2696
+ certifiedBlockCount: rows.filter((row) => row.objectType === 'dql_block' && row.status === 'certified').length,
2697
+ semanticMetricCount: rows.filter((row) => row.objectType === 'semantic_metric').length,
2698
+ dbtObjectCount: rows.filter((row) => row.objectType === 'dbt_model' || row.objectType === 'dbt_source' || row.objectType === 'dbt_column').length,
2699
+ updatedAt,
2700
+ })).sort((a, b) => a.domain.localeCompare(b.domain));
2701
+ }
2379
2702
  function fingerprintSnapshot(snapshot) {
2380
2703
  return sha256(stableStringify({
2381
2704
  projectRoot: snapshot.projectRoot,