@duckcodeailabs/dql-core 1.6.0 → 1.6.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. package/README.md +7 -5
  2. package/dist/ast/index.d.ts +1 -1
  3. package/dist/ast/index.d.ts.map +1 -1
  4. package/dist/ast/index.js.map +1 -1
  5. package/dist/ast/nodes.d.ts +51 -1
  6. package/dist/ast/nodes.d.ts.map +1 -1
  7. package/dist/ast/nodes.js +3 -0
  8. package/dist/ast/nodes.js.map +1 -1
  9. package/dist/ast/printer.d.ts.map +1 -1
  10. package/dist/ast/printer.js +62 -0
  11. package/dist/ast/printer.js.map +1 -1
  12. package/dist/ast/visitor.d.ts +5 -1
  13. package/dist/ast/visitor.d.ts.map +1 -1
  14. package/dist/ast/visitor.js +16 -0
  15. package/dist/ast/visitor.js.map +1 -1
  16. package/dist/formatter/formatter.d.ts.map +1 -1
  17. package/dist/formatter/formatter.js +122 -0
  18. package/dist/formatter/formatter.js.map +1 -1
  19. package/dist/lexer/token.d.ts +5 -0
  20. package/dist/lexer/token.d.ts.map +1 -1
  21. package/dist/lexer/token.js +10 -0
  22. package/dist/lexer/token.js.map +1 -1
  23. package/dist/lineage/builder.d.ts +33 -0
  24. package/dist/lineage/builder.d.ts.map +1 -1
  25. package/dist/lineage/builder.js +108 -11
  26. package/dist/lineage/builder.js.map +1 -1
  27. package/dist/lineage/index.d.ts +2 -2
  28. package/dist/lineage/index.d.ts.map +1 -1
  29. package/dist/lineage/index.js +1 -1
  30. package/dist/lineage/index.js.map +1 -1
  31. package/dist/lineage/lineage-graph.d.ts +2 -2
  32. package/dist/lineage/lineage-graph.d.ts.map +1 -1
  33. package/dist/lineage/lineage-graph.js +2 -0
  34. package/dist/lineage/lineage-graph.js.map +1 -1
  35. package/dist/lineage/query.d.ts +64 -0
  36. package/dist/lineage/query.d.ts.map +1 -1
  37. package/dist/lineage/query.js +260 -0
  38. package/dist/lineage/query.js.map +1 -1
  39. package/dist/manifest/builder.d.ts +8 -0
  40. package/dist/manifest/builder.d.ts.map +1 -1
  41. package/dist/manifest/builder.js +481 -73
  42. package/dist/manifest/builder.js.map +1 -1
  43. package/dist/manifest/index.d.ts +2 -2
  44. package/dist/manifest/index.d.ts.map +1 -1
  45. package/dist/manifest/index.js +1 -1
  46. package/dist/manifest/index.js.map +1 -1
  47. package/dist/manifest/types.d.ts +63 -0
  48. package/dist/manifest/types.d.ts.map +1 -1
  49. package/dist/parser/parser.d.ts +4 -0
  50. package/dist/parser/parser.d.ts.map +1 -1
  51. package/dist/parser/parser.js +320 -3
  52. package/dist/parser/parser.js.map +1 -1
  53. package/dist/semantic/analyzer.d.ts +2 -0
  54. package/dist/semantic/analyzer.d.ts.map +1 -1
  55. package/dist/semantic/analyzer.js +28 -0
  56. package/dist/semantic/analyzer.js.map +1 -1
  57. package/dist/semantic/semantic-layer.d.ts.map +1 -1
  58. package/dist/semantic/semantic-layer.js +24 -2
  59. package/dist/semantic/semantic-layer.js.map +1 -1
  60. package/package.json +2 -2
@@ -8,11 +8,13 @@
8
8
  import { readdirSync, readFileSync, existsSync } from 'node:fs';
9
9
  import { join, extname, relative } from 'node:path';
10
10
  import { Parser } from '../parser/index.js';
11
+ import { analyze } from '../semantic/index.js';
11
12
  import { extractTablesFromSql } from '../lineage/sql-parser.js';
12
13
  import { extractColumnLineage } from '../lineage/column-lineage.js';
13
14
  import { buildLineageGraph } from '../lineage/builder.js';
14
15
  import { detectDomainFlows, getDomainTrustOverview } from '../lineage/domain-lineage.js';
15
16
  import { loadSemanticLayerFromDir } from '../semantic/index.js';
17
+ import { DataLexContractRegistry } from '../contracts/index.js';
16
18
  import { loadAppDocument, findAppDocuments, findDashboardsForApp, loadDashboardDocument, extractDashboardBlockRefs, appFolderRelPath, } from '../apps/index.js';
17
19
  /**
18
20
  * Enumerate every file whose contents could change the manifest output.
@@ -30,11 +32,25 @@ export function collectInputFiles(options) {
30
32
  if (existsSync(configPath))
31
33
  files.add(configPath);
32
34
  const config = loadProjectConfig(projectRoot);
35
+ const datalexManifestPath = resolveDataLexManifestPath(projectRoot, options.datalexManifestPath, config);
36
+ if (datalexManifestPath && existsSync(datalexManifestPath)) {
37
+ files.add(datalexManifestPath);
38
+ }
33
39
  const blockDirs = ['blocks', 'dashboards', 'workbooks', ...(options.extraBlockDirs ?? [])];
34
40
  for (const dir of blockDirs) {
35
41
  for (const f of scanFilesRecursive(join(projectRoot, dir), ['.dql']))
36
42
  files.add(f);
37
43
  }
44
+ const businessViewDirs = ['blocks', 'business-views', 'dashboards', 'workbooks', ...(options.extraBlockDirs ?? [])];
45
+ for (const dir of businessViewDirs) {
46
+ for (const f of scanFilesRecursive(join(projectRoot, dir), ['.dql']))
47
+ files.add(f);
48
+ }
49
+ const termDirs = ['terms', 'blocks', 'business-views', 'dashboards', 'workbooks', ...(options.extraBlockDirs ?? [])];
50
+ for (const dir of termDirs) {
51
+ for (const f of scanFilesRecursive(join(projectRoot, dir), ['.dql']))
52
+ files.add(f);
53
+ }
38
54
  const notebookDirs = ['notebooks', 'blocks', 'dashboards', 'workbooks', ...(options.extraNotebookDirs ?? [])];
39
55
  for (const dir of notebookDirs) {
40
56
  for (const f of scanFilesRecursive(join(projectRoot, dir), ['.dqlnb']))
@@ -64,18 +80,38 @@ export function buildManifest(options) {
64
80
  // Load project config
65
81
  const config = loadProjectConfig(projectRoot);
66
82
  const projectName = config.project ?? 'dql-project';
83
+ const datalexManifestPath = resolveDataLexManifestPath(projectRoot, options.datalexManifestPath, config);
84
+ const datalexRegistry = datalexManifestPath
85
+ ? new DataLexContractRegistry({ manifestPath: datalexManifestPath })
86
+ : undefined;
87
+ for (const message of datalexRegistry?.loadDiagnostics() ?? []) {
88
+ diagnostics.push({
89
+ kind: 'config',
90
+ filePath: datalexManifestPath ? relative(projectRoot, datalexManifestPath) : undefined,
91
+ severity: 'warning',
92
+ message,
93
+ });
94
+ }
67
95
  // Scan blocks
68
96
  const blockDirs = ['blocks', 'dashboards', 'workbooks', ...(options.extraBlockDirs ?? [])];
69
- const blocks = scanBlocks(projectRoot, blockDirs, diagnostics);
97
+ const blocks = scanBlocks(projectRoot, blockDirs, diagnostics, datalexRegistry);
98
+ // Scan business composition views
99
+ const businessViewDirs = ['blocks', 'business-views', 'dashboards', 'workbooks', ...(options.extraBlockDirs ?? [])];
100
+ const businessViews = scanBusinessViews(projectRoot, businessViewDirs, diagnostics);
101
+ // Scan business glossary terms
102
+ const termDirs = ['terms', 'blocks', 'business-views', 'dashboards', 'workbooks', ...(options.extraBlockDirs ?? [])];
103
+ const terms = scanTerms(projectRoot, termDirs, diagnostics);
70
104
  // Scan notebooks
71
105
  const notebookDirs = ['notebooks', 'blocks', 'dashboards', 'workbooks', ...(options.extraNotebookDirs ?? [])];
72
- const notebooks = scanNotebooks(projectRoot, notebookDirs, diagnostics);
106
+ const notebooks = scanNotebooks(projectRoot, notebookDirs, diagnostics, datalexRegistry);
73
107
  // Extract blocks declared inside notebook DQL cells
74
- const notebookBlocks = extractNotebookBlocks(notebooks, projectRoot);
108
+ const notebookBlocks = extractNotebookBlocks(notebooks);
75
109
  for (const [name, block] of Object.entries(notebookBlocks)) {
76
110
  if (!blocks[name])
77
111
  blocks[name] = block;
78
112
  }
113
+ validateBusinessViews(businessViews, blocks, diagnostics);
114
+ validateTermRefs(terms, blocks, businessViews, diagnostics);
79
115
  // Load semantic layer
80
116
  const semanticDir = resolveSemanticPath(projectRoot, config);
81
117
  const { metrics, dimensions } = loadSemanticDefinitions(projectRoot, semanticDir);
@@ -113,7 +149,7 @@ export function buildManifest(options) {
113
149
  // because dashboard refs are resolved against the block path → name map.
114
150
  const { apps, dashboards } = scanAppsAndDashboards(projectRoot, blocks, diagnostics);
115
151
  // Build lineage
116
- const lineage = buildManifestLineage(blocks, metrics, dimensions, notebooks, dbtImport, apps, dashboards);
152
+ const lineage = buildManifestLineage(blocks, metrics, dimensions, notebooks, dbtImport, apps, dashboards, businessViews, terms);
117
153
  return {
118
154
  manifestVersion: 2,
119
155
  dqlVersion,
@@ -121,6 +157,8 @@ export function buildManifest(options) {
121
157
  project: projectName,
122
158
  projectRoot,
123
159
  blocks,
160
+ businessViews,
161
+ terms,
124
162
  notebooks,
125
163
  metrics,
126
164
  dimensions,
@@ -172,6 +210,19 @@ export function resolveDbtManifestPath(projectRoot, explicit) {
172
210
  return fallback;
173
211
  return null;
174
212
  }
213
+ export function resolveDataLexManifestPath(projectRoot, explicit, config = loadProjectConfig(projectRoot)) {
214
+ if (explicit) {
215
+ return isAbsPath(explicit) ? explicit : join(projectRoot, explicit);
216
+ }
217
+ const configured = config.datalex?.manifestPath;
218
+ if (configured) {
219
+ return isAbsPath(configured) ? configured : join(projectRoot, configured);
220
+ }
221
+ const fallback = join(projectRoot, 'datalex-manifest.json');
222
+ if (existsSync(fallback))
223
+ return fallback;
224
+ return null;
225
+ }
175
226
  function isAbsPath(p) {
176
227
  return p.startsWith('/') || /^[A-Za-z]:[\\/]/.test(p);
177
228
  }
@@ -202,7 +253,7 @@ function scanFilesRecursive(dir, extensions) {
202
253
  return results;
203
254
  }
204
255
  // ---- Block Scanning ----
205
- function scanBlocks(projectRoot, dirs, diagnostics) {
256
+ function scanBlocks(projectRoot, dirs, diagnostics, datalexRegistry) {
206
257
  const blocks = {};
207
258
  for (const dir of dirs) {
208
259
  const dirPath = join(projectRoot, dir);
@@ -213,63 +264,12 @@ function scanBlocks(projectRoot, dirs, diagnostics) {
213
264
  const source = readFileSync(filePath, 'utf-8');
214
265
  const parser = new Parser(source, relPath);
215
266
  const ast = parser.parse();
267
+ collectContractDiagnostics(ast, relPath, diagnostics, datalexRegistry);
216
268
  for (const stmt of ast.statements) {
217
269
  const block = stmt;
218
270
  if (block.kind !== 'BlockDecl')
219
271
  continue;
220
- const sql = block.query?.rawSQL ?? '';
221
- const parseResult = extractTablesFromSql(sql);
222
- const columnLineage = sql ? extractColumnLineage(sql) : null;
223
- const domain = extractProp(block, 'domain');
224
- const owner = extractProp(block, 'owner');
225
- const description = extractProp(block, 'description');
226
- const tags = extractTags(block);
227
- const tests = extractTests(block);
228
- const agent = extractAgentMetadata(block);
229
- blocks[block.name] = {
230
- name: block.name,
231
- filePath: relPath,
232
- domain,
233
- owner,
234
- status: extractProp(block, 'status'),
235
- blockType: block.blockType,
236
- sql,
237
- rawTableRefs: parseResult.tables,
238
- tableDependencies: parseResult.tables.map(normalizeTableName),
239
- refDependencies: parseResult.refs,
240
- metricRefs: parseResult.metricRefs,
241
- dimensionRefs: parseResult.dimensionRefs,
242
- allDependencies: [
243
- ...parseResult.refs,
244
- ...parseResult.tables.map(normalizeTableName),
245
- ...parseResult.metricRefs.map((m) => `@metric(${m})`),
246
- ...parseResult.dimensionRefs.map((d) => `@dim(${d})`),
247
- ],
248
- chartType: extractVisualizationChart(block),
249
- metricRef: block.metricRef,
250
- tests,
251
- tags,
252
- description,
253
- llmContext: agent.llmContext,
254
- examples: agent.examples,
255
- invariants: agent.invariants,
256
- businessOutcome: agent.businessOutcome,
257
- businessOwner: agent.businessOwner,
258
- decisionUse: agent.decisionUse,
259
- reviewCadence: agent.reviewCadence,
260
- businessRules: agent.businessRules,
261
- caveats: agent.caveats,
262
- datalexContract: typeof block.datalexContract === 'string' ? block.datalexContract : undefined,
263
- outputs: columnLineage?.parsed && columnLineage.columns.length > 0
264
- ? columnLineage.columns.map((c) => ({
265
- name: c.name,
266
- isAggregate: c.isAggregate,
267
- aggregateFn: c.aggregateFn,
268
- sources: c.sources,
269
- unresolved: c.unresolved,
270
- }))
271
- : undefined,
272
- };
272
+ blocks[block.name] = blockDeclToManifestBlock(block, relPath);
273
273
  }
274
274
  }
275
275
  catch (err) {
@@ -287,8 +287,216 @@ function scanBlocks(projectRoot, dirs, diagnostics) {
287
287
  }
288
288
  return blocks;
289
289
  }
290
+ // ---- Business View Scanning ----
291
+ function scanBusinessViews(projectRoot, dirs, diagnostics) {
292
+ const views = {};
293
+ const seenFiles = new Set();
294
+ for (const dir of dirs) {
295
+ const dirPath = join(projectRoot, dir);
296
+ const files = scanFilesRecursive(dirPath, ['.dql']);
297
+ for (const filePath of files) {
298
+ if (seenFiles.has(filePath))
299
+ continue;
300
+ seenFiles.add(filePath);
301
+ const relPath = relative(projectRoot, filePath);
302
+ try {
303
+ const source = readFileSync(filePath, 'utf-8');
304
+ if (!source.includes('business_view'))
305
+ continue;
306
+ const parser = new Parser(source, relPath);
307
+ const ast = parser.parse();
308
+ for (const stmt of ast.statements) {
309
+ const view = stmt;
310
+ if (view.kind !== 'BusinessViewDecl')
311
+ continue;
312
+ if (views[view.name]) {
313
+ diagnostics?.push({
314
+ kind: 'resolve',
315
+ filePath: relPath,
316
+ severity: 'error',
317
+ message: `duplicate business_view "${view.name}" also declared in ${views[view.name].filePath}`,
318
+ });
319
+ continue;
320
+ }
321
+ views[view.name] = businessViewDeclToManifestBusinessView(view, relPath);
322
+ }
323
+ }
324
+ catch (err) {
325
+ const msg = err instanceof Error ? err.message : String(err);
326
+ diagnostics?.push({
327
+ kind: 'parse',
328
+ filePath: relPath,
329
+ severity: 'error',
330
+ message: `Failed to parse business view file: ${msg}`,
331
+ });
332
+ }
333
+ }
334
+ }
335
+ return views;
336
+ }
337
+ // ---- Business Term Scanning ----
338
+ function scanTerms(projectRoot, dirs, diagnostics) {
339
+ const terms = {};
340
+ const seenFiles = new Set();
341
+ for (const dir of dirs) {
342
+ const dirPath = join(projectRoot, dir);
343
+ const files = scanFilesRecursive(dirPath, ['.dql']);
344
+ for (const filePath of files) {
345
+ if (seenFiles.has(filePath))
346
+ continue;
347
+ seenFiles.add(filePath);
348
+ const relPath = relative(projectRoot, filePath);
349
+ try {
350
+ const source = readFileSync(filePath, 'utf-8');
351
+ if (!source.includes('term'))
352
+ continue;
353
+ const parser = new Parser(source, relPath);
354
+ const ast = parser.parse();
355
+ for (const stmt of ast.statements) {
356
+ const term = stmt;
357
+ if (term.kind !== 'TermDecl')
358
+ continue;
359
+ if (terms[term.name]) {
360
+ diagnostics?.push({
361
+ kind: 'resolve',
362
+ filePath: relPath,
363
+ severity: 'error',
364
+ message: `duplicate term "${term.name}" also declared in ${terms[term.name].filePath}`,
365
+ });
366
+ continue;
367
+ }
368
+ terms[term.name] = termDeclToManifestTerm(term, relPath);
369
+ }
370
+ }
371
+ catch (err) {
372
+ const msg = err instanceof Error ? err.message : String(err);
373
+ diagnostics?.push({
374
+ kind: 'parse',
375
+ filePath: relPath,
376
+ severity: 'error',
377
+ message: `Failed to parse term file: ${msg}`,
378
+ });
379
+ }
380
+ }
381
+ }
382
+ return terms;
383
+ }
384
+ function validateBusinessViews(views, blocks, diagnostics) {
385
+ const blockNames = new Set(Object.keys(blocks));
386
+ const viewNames = new Set(Object.keys(views));
387
+ for (const view of Object.values(views)) {
388
+ view.unresolvedBlockRefs = view.blockRefs.filter((ref) => !blockNames.has(ref));
389
+ view.blockRefs = view.blockRefs.filter((ref) => blockNames.has(ref));
390
+ view.unresolvedBusinessViewRefs = view.businessViewRefs.filter((ref) => !viewNames.has(ref));
391
+ view.businessViewRefs = view.businessViewRefs.filter((ref) => viewNames.has(ref));
392
+ if (view.unresolvedBlockRefs.length > 0) {
393
+ diagnostics.push({
394
+ kind: 'resolve',
395
+ filePath: view.filePath,
396
+ severity: 'error',
397
+ message: `business_view "${view.name}" has unresolved block refs: ${view.unresolvedBlockRefs.join(', ')}`,
398
+ });
399
+ }
400
+ if (view.unresolvedBusinessViewRefs.length > 0) {
401
+ diagnostics.push({
402
+ kind: 'resolve',
403
+ filePath: view.filePath,
404
+ severity: 'error',
405
+ message: `business_view "${view.name}" has unresolved business_view refs: ${view.unresolvedBusinessViewRefs.join(', ')}`,
406
+ });
407
+ }
408
+ }
409
+ detectBusinessViewCycles(views, diagnostics);
410
+ }
411
+ function validateTermRefs(terms, blocks, views, diagnostics) {
412
+ const termNames = new Set(Object.keys(terms));
413
+ for (const block of Object.values(blocks)) {
414
+ const declared = block.termRefs ?? [];
415
+ block.unresolvedTermRefs = declared.filter((ref) => !termNames.has(ref));
416
+ block.termRefs = declared.filter((ref) => termNames.has(ref));
417
+ if (block.unresolvedTermRefs.length > 0) {
418
+ diagnostics.push({
419
+ kind: 'resolve',
420
+ filePath: block.filePath,
421
+ severity: 'error',
422
+ message: `block "${block.name}" has unresolved term refs: ${block.unresolvedTermRefs.join(', ')}`,
423
+ });
424
+ }
425
+ }
426
+ for (const view of Object.values(views)) {
427
+ const declared = view.declaredTermRefs ?? [];
428
+ view.unresolvedTermRefs = declared.filter((ref) => !termNames.has(ref));
429
+ view.declaredTermRefs = declared.filter((ref) => termNames.has(ref));
430
+ if (view.unresolvedTermRefs.length > 0) {
431
+ diagnostics.push({
432
+ kind: 'resolve',
433
+ filePath: view.filePath,
434
+ severity: 'error',
435
+ message: `business_view "${view.name}" has unresolved term refs: ${view.unresolvedTermRefs.join(', ')}`,
436
+ });
437
+ }
438
+ }
439
+ const collectTermsForView = (viewName, seen = new Set()) => {
440
+ const view = views[viewName];
441
+ if (!view || seen.has(viewName))
442
+ return [];
443
+ seen.add(viewName);
444
+ const refs = new Set(view.declaredTermRefs ?? []);
445
+ for (const blockRef of view.blockRefs) {
446
+ for (const termRef of blocks[blockRef]?.termRefs ?? [])
447
+ refs.add(termRef);
448
+ }
449
+ for (const viewRef of view.businessViewRefs) {
450
+ for (const termRef of collectTermsForView(viewRef, seen))
451
+ refs.add(termRef);
452
+ }
453
+ return [...refs];
454
+ };
455
+ for (const view of Object.values(views)) {
456
+ const declaredSet = new Set(view.declaredTermRefs ?? []);
457
+ const allRefs = collectTermsForView(view.name);
458
+ view.termRefs = allRefs;
459
+ view.inheritedTermRefs = allRefs.filter((ref) => !declaredSet.has(ref));
460
+ }
461
+ }
462
+ function detectBusinessViewCycles(views, diagnostics) {
463
+ const visiting = new Set();
464
+ const visited = new Set();
465
+ const stack = [];
466
+ const reported = new Set();
467
+ const visit = (name) => {
468
+ if (visited.has(name))
469
+ return;
470
+ if (visiting.has(name)) {
471
+ const cycleStart = stack.indexOf(name);
472
+ const cycle = [...stack.slice(cycleStart), name];
473
+ const key = cycle.join(' -> ');
474
+ if (!reported.has(key)) {
475
+ reported.add(key);
476
+ diagnostics.push({
477
+ kind: 'resolve',
478
+ filePath: views[name]?.filePath,
479
+ severity: 'error',
480
+ message: `business_view cycle detected: ${cycle.join(' -> ')}`,
481
+ });
482
+ }
483
+ return;
484
+ }
485
+ visiting.add(name);
486
+ stack.push(name);
487
+ for (const ref of views[name]?.businessViewRefs ?? []) {
488
+ visit(ref);
489
+ }
490
+ stack.pop();
491
+ visiting.delete(name);
492
+ visited.add(name);
493
+ };
494
+ for (const name of Object.keys(views)) {
495
+ visit(name);
496
+ }
497
+ }
290
498
  // ---- Notebook Scanning ----
291
- function scanNotebooks(projectRoot, dirs, diagnostics) {
499
+ function scanNotebooks(projectRoot, dirs, diagnostics, datalexRegistry) {
292
500
  const notebooks = {};
293
501
  const seen = new Set();
294
502
  for (const dir of dirs) {
@@ -319,6 +527,7 @@ function scanNotebooks(projectRoot, dirs, diagnostics) {
319
527
  try {
320
528
  const parser = new Parser(source, `${relPath}:${cell.id}`);
321
529
  const ast = parser.parse();
530
+ collectContractDiagnostics(ast, relPath, diagnostics, datalexRegistry);
322
531
  for (const stmt of ast.statements) {
323
532
  if (stmt.kind === 'BlockDecl') {
324
533
  blockName = stmt.name;
@@ -368,7 +577,7 @@ function scanNotebooks(projectRoot, dirs, diagnostics) {
368
577
  return notebooks;
369
578
  }
370
579
  /** Extract blocks declared inside notebook DQL cells into the blocks map. */
371
- function extractNotebookBlocks(notebooks, projectRoot) {
580
+ function extractNotebookBlocks(notebooks) {
372
581
  const blocks = {};
373
582
  for (const [nbPath, nb] of Object.entries(notebooks)) {
374
583
  for (const cell of nb.cells) {
@@ -376,17 +585,26 @@ function extractNotebookBlocks(notebooks, projectRoot) {
376
585
  continue;
377
586
  // Only add if not already found as a standalone block file
378
587
  if (!blocks[cell.blockName]) {
379
- const parseResult = extractTablesFromSql(cell.source);
380
- blocks[cell.blockName] = {
381
- name: cell.blockName,
382
- filePath: `${nbPath}#${cell.id}`,
383
- sql: cell.source,
384
- rawTableRefs: parseResult.tables,
385
- tableDependencies: parseResult.tables.map(normalizeTableName),
386
- refDependencies: parseResult.refs,
387
- allDependencies: [...parseResult.refs, ...parseResult.tables.map(normalizeTableName)],
388
- tests: [],
389
- };
588
+ try {
589
+ const ast = new Parser(cell.source, `${nbPath}:${cell.id}`).parse();
590
+ const block = ast.statements.find((stmt) => stmt.kind === 'BlockDecl' && stmt.name === cell.blockName);
591
+ if (block) {
592
+ blocks[cell.blockName] = blockDeclToManifestBlock(block, `${nbPath}#${cell.id}`);
593
+ }
594
+ }
595
+ catch {
596
+ const parseResult = extractTablesFromSql(cell.source);
597
+ blocks[cell.blockName] = {
598
+ name: cell.blockName,
599
+ filePath: `${nbPath}#${cell.id}`,
600
+ sql: '',
601
+ rawTableRefs: parseResult.tables,
602
+ tableDependencies: parseResult.tables.map(normalizeTableName),
603
+ refDependencies: parseResult.refs,
604
+ allDependencies: [...parseResult.refs, ...parseResult.tables.map(normalizeTableName)],
605
+ tests: [],
606
+ };
607
+ }
390
608
  }
391
609
  }
392
610
  }
@@ -956,7 +1174,7 @@ function appDocumentToManifest(app, filePath, dashboardIds) {
956
1174
  };
957
1175
  }
958
1176
  // ---- Lineage Builder ----
959
- function buildManifestLineage(blocks, metrics, dimensions, notebooks, dbtImport, apps, appDashboards) {
1177
+ function buildManifestLineage(blocks, metrics, dimensions, notebooks, dbtImport, apps, appDashboards, businessViews, terms) {
960
1178
  // Convert to lineage builder input format
961
1179
  const lineageBlocks = Object.values(blocks).map((b) => ({
962
1180
  name: b.name,
@@ -966,8 +1184,23 @@ function buildManifestLineage(blocks, metrics, dimensions, notebooks, dbtImport,
966
1184
  status: b.status,
967
1185
  blockType: b.blockType,
968
1186
  metricRef: b.metricRef,
1187
+ metricsRef: b.metricsRef,
1188
+ dimensionsRef: b.dimensionsRef,
969
1189
  chartType: b.chartType,
970
1190
  filePath: b.filePath,
1191
+ termRefs: b.termRefs,
1192
+ }));
1193
+ const lineageTerms = Object.values(terms ?? {}).map((term) => ({
1194
+ name: term.name,
1195
+ domain: term.domain,
1196
+ owner: term.owner,
1197
+ status: term.status,
1198
+ termType: term.termType,
1199
+ filePath: term.filePath,
1200
+ description: term.description,
1201
+ identifiers: term.identifiers,
1202
+ synonyms: term.synonyms,
1203
+ businessOutcome: term.businessOutcome,
971
1204
  }));
972
1205
  const lineageMetrics = Object.values(metrics).map((m) => ({
973
1206
  name: m.name,
@@ -979,6 +1212,19 @@ function buildManifestLineage(blocks, metrics, dimensions, notebooks, dbtImport,
979
1212
  name: d.name,
980
1213
  table: d.table,
981
1214
  }));
1215
+ const lineageBusinessViews = Object.values(businessViews ?? {}).map((view) => ({
1216
+ name: view.name,
1217
+ domain: view.domain,
1218
+ owner: view.owner,
1219
+ status: view.status,
1220
+ filePath: view.filePath,
1221
+ description: view.description,
1222
+ businessOutcome: view.businessOutcome,
1223
+ blockRefs: view.blockRefs,
1224
+ businessViewRefs: view.businessViewRefs,
1225
+ termRefs: view.termRefs,
1226
+ declaredTermRefs: view.declaredTermRefs,
1227
+ }));
982
1228
  // Pre-build a map of table name → block names that query that table.
983
1229
  // Used to resolve "which blocks feed into a notebook" when a notebook
984
1230
  // references a table directly rather than via ref().
@@ -1078,6 +1324,8 @@ function buildManifestLineage(blocks, metrics, dimensions, notebooks, dbtImport,
1078
1324
  dbtModels,
1079
1325
  dashboards: [...dashboards, ...appDashboardInputs],
1080
1326
  apps: lineageApps,
1327
+ businessViews: lineageBusinessViews,
1328
+ terms: lineageTerms,
1081
1329
  });
1082
1330
  // Serialize to manifest format
1083
1331
  const graphJson = graph.toJSON();
@@ -1101,7 +1349,7 @@ function buildManifestLineage(blocks, metrics, dimensions, notebooks, dbtImport,
1101
1349
  domain: n.domain,
1102
1350
  owner: n.owner,
1103
1351
  status: n.status,
1104
- filePath: blocks[n.name]?.filePath ?? n.metadata?.filePath,
1352
+ filePath: blocks[n.name]?.filePath ?? businessViews?.[n.name]?.filePath ?? terms?.[n.name]?.filePath ?? n.metadata?.filePath,
1105
1353
  columns: n.columns,
1106
1354
  metadata: n.metadata,
1107
1355
  })),
@@ -1122,6 +1370,138 @@ function buildManifestLineage(blocks, metrics, dimensions, notebooks, dbtImport,
1122
1370
  };
1123
1371
  }
1124
1372
  // ---- AST Helpers ----
1373
+ function blockDeclToManifestBlock(block, filePath) {
1374
+ const sql = block.query?.rawSQL ?? '';
1375
+ const parseResult = extractTablesFromSql(sql);
1376
+ const columnLineage = sql ? extractColumnLineage(sql) : null;
1377
+ const domain = extractProp(block, 'domain');
1378
+ const owner = extractProp(block, 'owner');
1379
+ const description = extractProp(block, 'description');
1380
+ const tags = extractTags(block);
1381
+ const tests = extractTests(block);
1382
+ const agent = extractAgentMetadata(block);
1383
+ return {
1384
+ name: block.name,
1385
+ filePath,
1386
+ domain,
1387
+ owner,
1388
+ status: extractProp(block, 'status'),
1389
+ blockType: block.blockType,
1390
+ sql,
1391
+ rawTableRefs: parseResult.tables,
1392
+ tableDependencies: parseResult.tables.map(normalizeTableName),
1393
+ refDependencies: parseResult.refs,
1394
+ metricRefs: parseResult.metricRefs,
1395
+ dimensionRefs: parseResult.dimensionRefs,
1396
+ dimensionsRef: block.dimensionsRef,
1397
+ allDependencies: [
1398
+ ...parseResult.refs,
1399
+ ...parseResult.tables.map(normalizeTableName),
1400
+ ...parseResult.metricRefs.map((m) => `@metric(${m})`),
1401
+ ...parseResult.dimensionRefs.map((d) => `@dim(${d})`),
1402
+ ...((block.metricsRef ?? (block.metricRef ? [block.metricRef] : [])).map((m) => `@metric(${m})`)),
1403
+ ...((block.dimensionsRef ?? []).map((d) => `@dim(${d})`)),
1404
+ ],
1405
+ chartType: extractVisualizationChart(block),
1406
+ metricRef: block.metricRef,
1407
+ metricsRef: block.metricsRef,
1408
+ tests,
1409
+ tags,
1410
+ description,
1411
+ termRefs: Array.isArray(block.termRefs) ? block.termRefs : undefined,
1412
+ unresolvedTermRefs: [],
1413
+ llmContext: agent.llmContext,
1414
+ examples: agent.examples,
1415
+ invariants: agent.invariants,
1416
+ businessOutcome: agent.businessOutcome,
1417
+ businessOwner: agent.businessOwner,
1418
+ decisionUse: agent.decisionUse,
1419
+ reviewCadence: agent.reviewCadence,
1420
+ businessRules: agent.businessRules,
1421
+ caveats: agent.caveats,
1422
+ datalexContract: typeof block.datalexContract === 'string' ? block.datalexContract : undefined,
1423
+ outputs: columnLineage?.parsed && columnLineage.columns.length > 0
1424
+ ? columnLineage.columns.map((c) => ({
1425
+ name: c.name,
1426
+ isAggregate: c.isAggregate,
1427
+ aggregateFn: c.aggregateFn,
1428
+ sources: c.sources,
1429
+ unresolved: c.unresolved,
1430
+ }))
1431
+ : undefined,
1432
+ };
1433
+ }
1434
+ function termDeclToManifestTerm(term, filePath) {
1435
+ return {
1436
+ name: term.name,
1437
+ filePath,
1438
+ domain: extractProp(term, 'domain'),
1439
+ owner: extractProp(term, 'owner'),
1440
+ status: extractProp(term, 'status'),
1441
+ termType: typeof term.termType === 'string' ? term.termType : undefined,
1442
+ tags: extractTags(term),
1443
+ description: extractProp(term, 'description'),
1444
+ identifiers: Array.isArray(term.identifiers) ? term.identifiers : undefined,
1445
+ synonyms: Array.isArray(term.synonyms) ? term.synonyms : undefined,
1446
+ businessOutcome: typeof term.businessOutcome === 'string' ? term.businessOutcome : undefined,
1447
+ businessOwner: typeof term.businessOwner === 'string' ? term.businessOwner : undefined,
1448
+ decisionUse: typeof term.decisionUse === 'string' ? term.decisionUse : undefined,
1449
+ reviewCadence: typeof term.reviewCadence === 'string' ? term.reviewCadence : undefined,
1450
+ businessRules: Array.isArray(term.businessRules) ? term.businessRules : undefined,
1451
+ caveats: Array.isArray(term.caveats) ? term.caveats : undefined,
1452
+ };
1453
+ }
1454
+ function businessViewDeclToManifestBusinessView(view, filePath) {
1455
+ const blockRefs = [];
1456
+ const businessViewRefs = [];
1457
+ for (const ref of view.includes ?? []) {
1458
+ if (ref.refType === 'block')
1459
+ blockRefs.push(ref.name);
1460
+ if (ref.refType === 'business_view')
1461
+ businessViewRefs.push(ref.name);
1462
+ }
1463
+ return {
1464
+ name: view.name,
1465
+ filePath,
1466
+ domain: extractProp(view, 'domain'),
1467
+ owner: extractProp(view, 'owner'),
1468
+ status: extractProp(view, 'status'),
1469
+ tags: extractTags(view),
1470
+ description: extractProp(view, 'description'),
1471
+ businessOutcome: typeof view.businessOutcome === 'string' ? view.businessOutcome : undefined,
1472
+ businessOwner: typeof view.businessOwner === 'string' ? view.businessOwner : undefined,
1473
+ decisionUse: typeof view.decisionUse === 'string' ? view.decisionUse : undefined,
1474
+ reviewCadence: typeof view.reviewCadence === 'string' ? view.reviewCadence : undefined,
1475
+ businessRules: Array.isArray(view.businessRules) ? view.businessRules : undefined,
1476
+ caveats: Array.isArray(view.caveats) ? view.caveats : undefined,
1477
+ blockRefs,
1478
+ businessViewRefs,
1479
+ termRefs: Array.isArray(view.termRefs) ? view.termRefs : [],
1480
+ declaredTermRefs: Array.isArray(view.termRefs) ? view.termRefs : [],
1481
+ inheritedTermRefs: [],
1482
+ unresolvedTermRefs: [],
1483
+ unresolvedBlockRefs: [],
1484
+ unresolvedBusinessViewRefs: [],
1485
+ };
1486
+ }
1487
+ function collectContractDiagnostics(ast, relPath, diagnostics, datalexRegistry) {
1488
+ if (!diagnostics)
1489
+ return;
1490
+ const hasContractRef = ast.statements.some((stmt) => stmt.kind === 'BlockDecl' && typeof stmt.datalexContract === 'string' && stmt.datalexContract.trim() !== '');
1491
+ if (!hasContractRef)
1492
+ return;
1493
+ const semanticDiagnostics = analyze(ast, { datalexRegistry });
1494
+ for (const diag of semanticDiagnostics) {
1495
+ if (!diag.message.includes('datalex_contract') && !diag.message.includes('DataLex manifest'))
1496
+ continue;
1497
+ diagnostics.push({
1498
+ kind: 'semantic',
1499
+ filePath: relPath,
1500
+ severity: diag.severity === 'error' ? 'error' : 'warning',
1501
+ message: diag.message,
1502
+ });
1503
+ }
1504
+ }
1125
1505
  function extractProp(block, propName) {
1126
1506
  if (block[propName] !== undefined && block[propName] !== null) {
1127
1507
  return String(block[propName]);
@@ -1139,6 +1519,9 @@ function extractVisualizationChart(block) {
1139
1519
  if (!block.visualization)
1140
1520
  return undefined;
1141
1521
  for (const prop of block.visualization.properties ?? []) {
1522
+ if (prop.name === 'chart' && prop.value?.kind === 'StringLiteral') {
1523
+ return String(prop.value.value);
1524
+ }
1142
1525
  if (prop.key === 'chart' && prop.value?.kind === 'Literal') {
1143
1526
  return String(prop.value.value);
1144
1527
  }
@@ -1179,6 +1562,14 @@ function extractAgentMetadata(block) {
1179
1562
  };
1180
1563
  }
1181
1564
  function extractTests(block) {
1565
+ if (Array.isArray(block.tests)) {
1566
+ return block.tests.map((a) => {
1567
+ if (typeof a === 'string')
1568
+ return a;
1569
+ const expected = formatManifestTestExpected(a.expected);
1570
+ return `${a.field ?? ''} ${a.operator ?? ''} ${expected}`.trim();
1571
+ });
1572
+ }
1182
1573
  if (!block.tests?.assertions)
1183
1574
  return [];
1184
1575
  return block.tests.assertions.map((a) => {
@@ -1187,6 +1578,23 @@ function extractTests(block) {
1187
1578
  return `${a.field ?? ''} ${a.operator ?? ''} ${a.expected ?? ''}`.trim();
1188
1579
  });
1189
1580
  }
1581
+ function formatManifestTestExpected(node) {
1582
+ if (!node || typeof node !== 'object')
1583
+ return String(node ?? '');
1584
+ switch (node.kind) {
1585
+ case 'StringLiteral':
1586
+ return JSON.stringify(node.value);
1587
+ case 'NumberLiteral':
1588
+ case 'BooleanLiteral':
1589
+ return String(node.value);
1590
+ case 'Identifier':
1591
+ return node.name;
1592
+ case 'ArrayLiteral':
1593
+ return `[${(node.elements ?? []).map(formatManifestTestExpected).join(', ')}]`;
1594
+ default:
1595
+ return String(node.value ?? '');
1596
+ }
1597
+ }
1190
1598
  /**
1191
1599
  * Normalize DuckDB reader function calls to plain table names.
1192
1600
  * e.g., "read_csv_auto('./data/revenue.csv')" → "revenue"