@duckcodeailabs/dql-core 1.6.1 → 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 (56) hide show
  1. package/dist/ast/index.d.ts +1 -1
  2. package/dist/ast/index.d.ts.map +1 -1
  3. package/dist/ast/index.js.map +1 -1
  4. package/dist/ast/nodes.d.ts +49 -1
  5. package/dist/ast/nodes.d.ts.map +1 -1
  6. package/dist/ast/nodes.js +3 -0
  7. package/dist/ast/nodes.js.map +1 -1
  8. package/dist/ast/printer.d.ts.map +1 -1
  9. package/dist/ast/printer.js +56 -0
  10. package/dist/ast/printer.js.map +1 -1
  11. package/dist/ast/visitor.d.ts +5 -1
  12. package/dist/ast/visitor.d.ts.map +1 -1
  13. package/dist/ast/visitor.js +16 -0
  14. package/dist/ast/visitor.js.map +1 -1
  15. package/dist/formatter/formatter.d.ts.map +1 -1
  16. package/dist/formatter/formatter.js +111 -0
  17. package/dist/formatter/formatter.js.map +1 -1
  18. package/dist/lexer/token.d.ts +4 -0
  19. package/dist/lexer/token.d.ts.map +1 -1
  20. package/dist/lexer/token.js +8 -0
  21. package/dist/lexer/token.js.map +1 -1
  22. package/dist/lineage/builder.d.ts +29 -0
  23. package/dist/lineage/builder.d.ts.map +1 -1
  24. package/dist/lineage/builder.js +88 -4
  25. package/dist/lineage/builder.js.map +1 -1
  26. package/dist/lineage/index.d.ts +2 -2
  27. package/dist/lineage/index.d.ts.map +1 -1
  28. package/dist/lineage/index.js +1 -1
  29. package/dist/lineage/index.js.map +1 -1
  30. package/dist/lineage/lineage-graph.d.ts +2 -2
  31. package/dist/lineage/lineage-graph.d.ts.map +1 -1
  32. package/dist/lineage/lineage-graph.js +2 -0
  33. package/dist/lineage/lineage-graph.js.map +1 -1
  34. package/dist/lineage/query.d.ts +64 -0
  35. package/dist/lineage/query.d.ts.map +1 -1
  36. package/dist/lineage/query.js +260 -0
  37. package/dist/lineage/query.js.map +1 -1
  38. package/dist/manifest/builder.d.ts +8 -0
  39. package/dist/manifest/builder.d.ts.map +1 -1
  40. package/dist/manifest/builder.js +479 -77
  41. package/dist/manifest/builder.js.map +1 -1
  42. package/dist/manifest/index.d.ts +2 -2
  43. package/dist/manifest/index.d.ts.map +1 -1
  44. package/dist/manifest/index.js +1 -1
  45. package/dist/manifest/index.js.map +1 -1
  46. package/dist/manifest/types.d.ts +59 -0
  47. package/dist/manifest/types.d.ts.map +1 -1
  48. package/dist/parser/parser.d.ts +4 -0
  49. package/dist/parser/parser.d.ts.map +1 -1
  50. package/dist/parser/parser.js +308 -3
  51. package/dist/parser/parser.js.map +1 -1
  52. package/dist/semantic/analyzer.d.ts +2 -0
  53. package/dist/semantic/analyzer.d.ts.map +1 -1
  54. package/dist/semantic/analyzer.js +28 -0
  55. package/dist/semantic/analyzer.js.map +1 -1
  56. package/package.json +1 -1
@@ -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,67 +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
- dimensionsRef: block.dimensionsRef,
243
- allDependencies: [
244
- ...parseResult.refs,
245
- ...parseResult.tables.map(normalizeTableName),
246
- ...parseResult.metricRefs.map((m) => `@metric(${m})`),
247
- ...parseResult.dimensionRefs.map((d) => `@dim(${d})`),
248
- ...((block.metricsRef ?? (block.metricRef ? [block.metricRef] : [])).map((m) => `@metric(${m})`)),
249
- ...((block.dimensionsRef ?? []).map((d) => `@dim(${d})`)),
250
- ],
251
- chartType: extractVisualizationChart(block),
252
- metricRef: block.metricRef,
253
- metricsRef: block.metricsRef,
254
- tests,
255
- tags,
256
- description,
257
- llmContext: agent.llmContext,
258
- examples: agent.examples,
259
- invariants: agent.invariants,
260
- businessOutcome: agent.businessOutcome,
261
- businessOwner: agent.businessOwner,
262
- decisionUse: agent.decisionUse,
263
- reviewCadence: agent.reviewCadence,
264
- businessRules: agent.businessRules,
265
- caveats: agent.caveats,
266
- datalexContract: typeof block.datalexContract === 'string' ? block.datalexContract : undefined,
267
- outputs: columnLineage?.parsed && columnLineage.columns.length > 0
268
- ? columnLineage.columns.map((c) => ({
269
- name: c.name,
270
- isAggregate: c.isAggregate,
271
- aggregateFn: c.aggregateFn,
272
- sources: c.sources,
273
- unresolved: c.unresolved,
274
- }))
275
- : undefined,
276
- };
272
+ blocks[block.name] = blockDeclToManifestBlock(block, relPath);
277
273
  }
278
274
  }
279
275
  catch (err) {
@@ -291,8 +287,216 @@ function scanBlocks(projectRoot, dirs, diagnostics) {
291
287
  }
292
288
  return blocks;
293
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
+ }
294
498
  // ---- Notebook Scanning ----
295
- function scanNotebooks(projectRoot, dirs, diagnostics) {
499
+ function scanNotebooks(projectRoot, dirs, diagnostics, datalexRegistry) {
296
500
  const notebooks = {};
297
501
  const seen = new Set();
298
502
  for (const dir of dirs) {
@@ -323,6 +527,7 @@ function scanNotebooks(projectRoot, dirs, diagnostics) {
323
527
  try {
324
528
  const parser = new Parser(source, `${relPath}:${cell.id}`);
325
529
  const ast = parser.parse();
530
+ collectContractDiagnostics(ast, relPath, diagnostics, datalexRegistry);
326
531
  for (const stmt of ast.statements) {
327
532
  if (stmt.kind === 'BlockDecl') {
328
533
  blockName = stmt.name;
@@ -372,7 +577,7 @@ function scanNotebooks(projectRoot, dirs, diagnostics) {
372
577
  return notebooks;
373
578
  }
374
579
  /** Extract blocks declared inside notebook DQL cells into the blocks map. */
375
- function extractNotebookBlocks(notebooks, projectRoot) {
580
+ function extractNotebookBlocks(notebooks) {
376
581
  const blocks = {};
377
582
  for (const [nbPath, nb] of Object.entries(notebooks)) {
378
583
  for (const cell of nb.cells) {
@@ -380,17 +585,26 @@ function extractNotebookBlocks(notebooks, projectRoot) {
380
585
  continue;
381
586
  // Only add if not already found as a standalone block file
382
587
  if (!blocks[cell.blockName]) {
383
- const parseResult = extractTablesFromSql(cell.source);
384
- blocks[cell.blockName] = {
385
- name: cell.blockName,
386
- filePath: `${nbPath}#${cell.id}`,
387
- sql: cell.source,
388
- rawTableRefs: parseResult.tables,
389
- tableDependencies: parseResult.tables.map(normalizeTableName),
390
- refDependencies: parseResult.refs,
391
- allDependencies: [...parseResult.refs, ...parseResult.tables.map(normalizeTableName)],
392
- tests: [],
393
- };
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
+ }
394
608
  }
395
609
  }
396
610
  }
@@ -960,7 +1174,7 @@ function appDocumentToManifest(app, filePath, dashboardIds) {
960
1174
  };
961
1175
  }
962
1176
  // ---- Lineage Builder ----
963
- function buildManifestLineage(blocks, metrics, dimensions, notebooks, dbtImport, apps, appDashboards) {
1177
+ function buildManifestLineage(blocks, metrics, dimensions, notebooks, dbtImport, apps, appDashboards, businessViews, terms) {
964
1178
  // Convert to lineage builder input format
965
1179
  const lineageBlocks = Object.values(blocks).map((b) => ({
966
1180
  name: b.name,
@@ -974,6 +1188,19 @@ function buildManifestLineage(blocks, metrics, dimensions, notebooks, dbtImport,
974
1188
  dimensionsRef: b.dimensionsRef,
975
1189
  chartType: b.chartType,
976
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,
977
1204
  }));
978
1205
  const lineageMetrics = Object.values(metrics).map((m) => ({
979
1206
  name: m.name,
@@ -985,6 +1212,19 @@ function buildManifestLineage(blocks, metrics, dimensions, notebooks, dbtImport,
985
1212
  name: d.name,
986
1213
  table: d.table,
987
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
+ }));
988
1228
  // Pre-build a map of table name → block names that query that table.
989
1229
  // Used to resolve "which blocks feed into a notebook" when a notebook
990
1230
  // references a table directly rather than via ref().
@@ -1084,6 +1324,8 @@ function buildManifestLineage(blocks, metrics, dimensions, notebooks, dbtImport,
1084
1324
  dbtModels,
1085
1325
  dashboards: [...dashboards, ...appDashboardInputs],
1086
1326
  apps: lineageApps,
1327
+ businessViews: lineageBusinessViews,
1328
+ terms: lineageTerms,
1087
1329
  });
1088
1330
  // Serialize to manifest format
1089
1331
  const graphJson = graph.toJSON();
@@ -1107,7 +1349,7 @@ function buildManifestLineage(blocks, metrics, dimensions, notebooks, dbtImport,
1107
1349
  domain: n.domain,
1108
1350
  owner: n.owner,
1109
1351
  status: n.status,
1110
- 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,
1111
1353
  columns: n.columns,
1112
1354
  metadata: n.metadata,
1113
1355
  })),
@@ -1128,6 +1370,138 @@ function buildManifestLineage(blocks, metrics, dimensions, notebooks, dbtImport,
1128
1370
  };
1129
1371
  }
1130
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
+ }
1131
1505
  function extractProp(block, propName) {
1132
1506
  if (block[propName] !== undefined && block[propName] !== null) {
1133
1507
  return String(block[propName]);
@@ -1145,6 +1519,9 @@ function extractVisualizationChart(block) {
1145
1519
  if (!block.visualization)
1146
1520
  return undefined;
1147
1521
  for (const prop of block.visualization.properties ?? []) {
1522
+ if (prop.name === 'chart' && prop.value?.kind === 'StringLiteral') {
1523
+ return String(prop.value.value);
1524
+ }
1148
1525
  if (prop.key === 'chart' && prop.value?.kind === 'Literal') {
1149
1526
  return String(prop.value.value);
1150
1527
  }
@@ -1185,6 +1562,14 @@ function extractAgentMetadata(block) {
1185
1562
  };
1186
1563
  }
1187
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
+ }
1188
1573
  if (!block.tests?.assertions)
1189
1574
  return [];
1190
1575
  return block.tests.assertions.map((a) => {
@@ -1193,6 +1578,23 @@ function extractTests(block) {
1193
1578
  return `${a.field ?? ''} ${a.operator ?? ''} ${a.expected ?? ''}`.trim();
1194
1579
  });
1195
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
+ }
1196
1598
  /**
1197
1599
  * Normalize DuckDB reader function calls to plain table names.
1198
1600
  * e.g., "read_csv_auto('./data/revenue.csv')" → "revenue"