@duckcodeailabs/dql-core 1.6.17 → 1.6.19

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 (55) hide show
  1. package/dist/apps/app-document.d.ts +2 -1
  2. package/dist/apps/app-document.d.ts.map +1 -1
  3. package/dist/apps/app-document.js +18 -5
  4. package/dist/apps/app-document.js.map +1 -1
  5. package/dist/apps/dashboard-document.d.ts +19 -0
  6. package/dist/apps/dashboard-document.d.ts.map +1 -1
  7. package/dist/apps/dashboard-document.js +72 -0
  8. package/dist/apps/dashboard-document.js.map +1 -1
  9. package/dist/apps/index.d.ts +1 -1
  10. package/dist/apps/index.d.ts.map +1 -1
  11. package/dist/ast/nodes.d.ts +40 -1
  12. package/dist/ast/nodes.d.ts.map +1 -1
  13. package/dist/ast/nodes.js +1 -0
  14. package/dist/ast/nodes.js.map +1 -1
  15. package/dist/ast/printer.d.ts.map +1 -1
  16. package/dist/ast/printer.js +40 -0
  17. package/dist/ast/printer.js.map +1 -1
  18. package/dist/formatter/formatter.d.ts.map +1 -1
  19. package/dist/formatter/formatter.js +66 -0
  20. package/dist/formatter/formatter.js.map +1 -1
  21. package/dist/lineage/builder.d.ts +35 -0
  22. package/dist/lineage/builder.d.ts.map +1 -1
  23. package/dist/lineage/builder.js +96 -8
  24. package/dist/lineage/builder.js.map +1 -1
  25. package/dist/lineage/index.d.ts +1 -1
  26. package/dist/lineage/index.d.ts.map +1 -1
  27. package/dist/lineage/index.js.map +1 -1
  28. package/dist/lineage/query.d.ts +30 -1
  29. package/dist/lineage/query.d.ts.map +1 -1
  30. package/dist/lineage/query.js +248 -1
  31. package/dist/lineage/query.js.map +1 -1
  32. package/dist/manifest/builder.d.ts +6 -1
  33. package/dist/manifest/builder.d.ts.map +1 -1
  34. package/dist/manifest/builder.js +359 -13
  35. package/dist/manifest/builder.js.map +1 -1
  36. package/dist/manifest/types.d.ts +52 -0
  37. package/dist/manifest/types.d.ts.map +1 -1
  38. package/dist/parser/parser.d.ts +4 -0
  39. package/dist/parser/parser.d.ts.map +1 -1
  40. package/dist/parser/parser.js +262 -20
  41. package/dist/parser/parser.js.map +1 -1
  42. package/dist/semantic/analyzer.d.ts.map +1 -1
  43. package/dist/semantic/analyzer.js +4 -0
  44. package/dist/semantic/analyzer.js.map +1 -1
  45. package/dist/semantic/providers/dbt-provider.js +130 -4
  46. package/dist/semantic/providers/dbt-provider.js.map +1 -1
  47. package/dist/semantic/providers/registry.js +68 -4
  48. package/dist/semantic/providers/registry.js.map +1 -1
  49. package/dist/semantic/providers/snowflake-provider.d.ts.map +1 -1
  50. package/dist/semantic/providers/snowflake-provider.js +60 -8
  51. package/dist/semantic/providers/snowflake-provider.js.map +1 -1
  52. package/dist/semantic/semantic-layer.d.ts.map +1 -1
  53. package/dist/semantic/semantic-layer.js +11 -20
  54. package/dist/semantic/semantic-layer.js.map +1 -1
  55. package/package.json +1 -1
@@ -1 +1 @@
1
- {"version":3,"file":"builder.d.ts","sourceRoot":"","sources":["../../src/manifest/builder.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAsBH,OAAO,KAAK,EACV,WAAW,EAcZ,MAAM,YAAY,CAAC;AAcpB;;;;;;;;;;;;GAYG;AACH,MAAM,WAAW,gBAAgB;IAC/B,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;IACnB,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;IACnB,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;CACpB;AAED,MAAM,WAAW,oBAAoB;IACnC,4DAA4D;IAC5D,WAAW,EAAE,MAAM,CAAC;IACpB,6BAA6B;IAC7B,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,2CAA2C;IAC3C,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,+EAA+E;IAC/E,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B;;;;;OAKG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB;;;OAGG;IACH,qBAAqB,CAAC,EAAE,MAAM,CAAC;IAC/B;;;OAGG;IACH,gBAAgB,CAAC,EAAE,gBAAgB,CAAC;IACpC,oDAAoD;IACpD,cAAc,CAAC,EAAE,MAAM,EAAE,CAAC;IAC1B,sDAAsD;IACtD,iBAAiB,CAAC,EAAE,MAAM,EAAE,CAAC;CAC9B;AAED;;;;;;;;GAQG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,oBAAoB,GAAG,MAAM,EAAE,CAmDzE;AAED,wBAAgB,aAAa,CAAC,OAAO,EAAE,oBAAoB,GAAG,WAAW,CAuHxE;AAID,UAAU,aAAa;IACrB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,aAAa,CAAC,EAAE;QAAE,QAAQ,CAAC,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,WAAW,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IAC3E,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,uEAAuE;IACvE,SAAS,CAAC,EAAE,gBAAgB,CAAC;IAC7B;;;OAGG;IACH,GAAG,CAAC,EAAE;QACJ,0EAA0E;QAC1E,UAAU,CAAC,EAAE,MAAM,CAAC;QACpB,6FAA6F;QAC7F,YAAY,CAAC,EAAE,MAAM,CAAC;KACvB,CAAC;IACF,iGAAiG;IACjG,OAAO,CAAC,EAAE;QACR,yEAAyE;QACzE,YAAY,CAAC,EAAE,MAAM,CAAC;KACvB,CAAC;CACH;AAED;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,WAAW,EAAE,MAAM,GAAG,aAAa,CAQpE;AAED;;;;;GAKG;AACH,wBAAgB,sBAAsB,CACpC,WAAW,EAAE,MAAM,EACnB,QAAQ,CAAC,EAAE,MAAM,GAChB,MAAM,GAAG,IAAI,CAgBf;AAED,wBAAgB,0BAA0B,CACxC,WAAW,EAAE,MAAM,EACnB,QAAQ,CAAC,EAAE,MAAM,EACjB,MAAM,GAAE,aAA8C,GACrD,MAAM,GAAG,IAAI,CAWf"}
1
+ {"version":3,"file":"builder.d.ts","sourceRoot":"","sources":["../../src/manifest/builder.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAuBH,OAAO,KAAK,EACV,WAAW,EAeZ,MAAM,YAAY,CAAC;AAcpB;;;;;;;;;;;;GAYG;AACH,MAAM,WAAW,gBAAgB;IAC/B,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;IACnB,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;IACnB,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;CACpB;AAED,MAAM,WAAW,oBAAoB;IACnC,4DAA4D;IAC5D,WAAW,EAAE,MAAM,CAAC;IACpB,6BAA6B;IAC7B,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,2CAA2C;IAC3C,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,+EAA+E;IAC/E,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B;;;;;OAKG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB;;;OAGG;IACH,qBAAqB,CAAC,EAAE,MAAM,CAAC;IAC/B;;;OAGG;IACH,gBAAgB,CAAC,EAAE,gBAAgB,CAAC;IACpC,oDAAoD;IACpD,cAAc,CAAC,EAAE,MAAM,EAAE,CAAC;IAC1B,sDAAsD;IACtD,iBAAiB,CAAC,EAAE,MAAM,EAAE,CAAC;CAC9B;AAED;;;;;;;;GAQG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,oBAAoB,GAAG,MAAM,EAAE,CAmDzE;AAED,wBAAgB,aAAa,CAAC,OAAO,EAAE,oBAAoB,GAAG,WAAW,CA8HxE;AAID,UAAU,aAAa;IACrB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,aAAa,CAAC,EAAE;QAAE,QAAQ,CAAC,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,WAAW,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IAC3E,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,uEAAuE;IACvE,SAAS,CAAC,EAAE,gBAAgB,CAAC;IAC7B;;;OAGG;IACH,GAAG,CAAC,EAAE;QACJ,0EAA0E;QAC1E,UAAU,CAAC,EAAE,MAAM,CAAC;QACpB,6FAA6F;QAC7F,YAAY,CAAC,EAAE,MAAM,CAAC;KACvB,CAAC;IACF,iGAAiG;IACjG,OAAO,CAAC,EAAE;QACR,yEAAyE;QACzE,YAAY,CAAC,EAAE,MAAM,CAAC;KACvB,CAAC;CACH;AAED;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,WAAW,EAAE,MAAM,GAAG,aAAa,CAQpE;AAED;;;;;GAKG;AACH,wBAAgB,sBAAsB,CACpC,WAAW,EAAE,MAAM,EACnB,QAAQ,CAAC,EAAE,MAAM,GAChB,MAAM,GAAG,IAAI,CAgBf;AAED;;;;GAIG;AACH,wBAAgB,0BAA0B,CACxC,WAAW,EAAE,MAAM,EACnB,QAAQ,CAAC,EAAE,MAAM,EACjB,YAAY,CAAC,EAAE,aAAa,GAC3B,MAAM,GAAG,IAAI,CAef"}
@@ -36,17 +36,17 @@ export function collectInputFiles(options) {
36
36
  if (datalexManifestPath && existsSync(datalexManifestPath)) {
37
37
  files.add(datalexManifestPath);
38
38
  }
39
- const blockDirs = ['blocks', 'dashboards', 'workbooks', ...(options.extraBlockDirs ?? [])];
39
+ const blockDirs = ['blocks', 'domains', 'dashboards', 'workbooks', ...(options.extraBlockDirs ?? [])];
40
40
  for (const dir of blockDirs) {
41
41
  for (const f of scanFilesRecursive(join(projectRoot, dir), ['.dql']))
42
42
  files.add(f);
43
43
  }
44
- const businessViewDirs = ['blocks', 'business-views', 'dashboards', 'workbooks', ...(options.extraBlockDirs ?? [])];
44
+ const businessViewDirs = ['blocks', 'business-views', 'domains', 'dashboards', 'workbooks', ...(options.extraBlockDirs ?? [])];
45
45
  for (const dir of businessViewDirs) {
46
46
  for (const f of scanFilesRecursive(join(projectRoot, dir), ['.dql']))
47
47
  files.add(f);
48
48
  }
49
- const termDirs = ['terms', 'blocks', 'business-views', 'dashboards', 'workbooks', ...(options.extraBlockDirs ?? [])];
49
+ const termDirs = ['terms', 'blocks', 'business-views', 'domains', 'dashboards', 'workbooks', ...(options.extraBlockDirs ?? [])];
50
50
  for (const dir of termDirs) {
51
51
  for (const f of scanFilesRecursive(join(projectRoot, dir), ['.dql']))
52
52
  files.add(f);
@@ -92,14 +92,17 @@ export function buildManifest(options) {
92
92
  message,
93
93
  });
94
94
  }
95
+ // Scan first-class business domains
96
+ const domainDirs = ['domains', 'blocks', 'terms', 'business-views', ...(options.extraBlockDirs ?? [])];
97
+ const domains = scanDomains(projectRoot, domainDirs, diagnostics);
95
98
  // Scan blocks
96
- const blockDirs = ['blocks', 'dashboards', 'workbooks', ...(options.extraBlockDirs ?? [])];
99
+ const blockDirs = ['blocks', 'domains', 'dashboards', 'workbooks', ...(options.extraBlockDirs ?? [])];
97
100
  const blocks = scanBlocks(projectRoot, blockDirs, diagnostics, datalexRegistry);
98
101
  // Scan business composition views
99
- const businessViewDirs = ['blocks', 'business-views', 'dashboards', 'workbooks', ...(options.extraBlockDirs ?? [])];
102
+ const businessViewDirs = ['blocks', 'business-views', 'domains', 'dashboards', 'workbooks', ...(options.extraBlockDirs ?? [])];
100
103
  const businessViews = scanBusinessViews(projectRoot, businessViewDirs, diagnostics);
101
104
  // Scan business glossary terms
102
- const termDirs = ['terms', 'blocks', 'business-views', 'dashboards', 'workbooks', ...(options.extraBlockDirs ?? [])];
105
+ const termDirs = ['terms', 'blocks', 'business-views', 'domains', 'dashboards', 'workbooks', ...(options.extraBlockDirs ?? [])];
103
106
  const terms = scanTerms(projectRoot, termDirs, diagnostics);
104
107
  // Scan notebooks
105
108
  const notebookDirs = ['notebooks', 'blocks', 'dashboards', 'workbooks', ...(options.extraNotebookDirs ?? [])];
@@ -115,6 +118,7 @@ export function buildManifest(options) {
115
118
  if (!terms[name])
116
119
  terms[name] = term;
117
120
  }
121
+ validateDomains(domains, blocks, businessViews, terms, diagnostics);
118
122
  validateBusinessViews(businessViews, blocks, diagnostics);
119
123
  validateTermRefs(terms, blocks, businessViews, diagnostics);
120
124
  // Load semantic layer
@@ -154,13 +158,14 @@ export function buildManifest(options) {
154
158
  // because dashboard refs are resolved against the block path → name map.
155
159
  const { apps, dashboards } = scanAppsAndDashboards(projectRoot, blocks, diagnostics);
156
160
  // Build lineage
157
- const lineage = buildManifestLineage(blocks, metrics, dimensions, notebooks, dbtImport, apps, dashboards, businessViews, terms);
161
+ const lineage = buildManifestLineage(domains, blocks, metrics, dimensions, notebooks, dbtImport, apps, dashboards, businessViews, terms);
158
162
  return {
159
163
  manifestVersion: 2,
160
164
  dqlVersion,
161
165
  generatedAt: new Date().toISOString(),
162
166
  project: projectName,
163
167
  projectRoot,
168
+ domains,
164
169
  blocks,
165
170
  businessViews,
166
171
  terms,
@@ -215,13 +220,23 @@ export function resolveDbtManifestPath(projectRoot, explicit) {
215
220
  return fallback;
216
221
  return null;
217
222
  }
218
- export function resolveDataLexManifestPath(projectRoot, explicit, config = loadProjectConfig(projectRoot)) {
223
+ /**
224
+ * Resolve the absolute path to the configured DataLex manifest.
225
+ * Honors explicit CLI flag first, then `datalex.manifestPath` from config,
226
+ * then `<projectRoot>/datalex-manifest.json`.
227
+ */
228
+ export function resolveDataLexManifestPath(projectRoot, explicit, loadedConfig) {
219
229
  if (explicit) {
220
- return isAbsPath(explicit) ? explicit : join(projectRoot, explicit);
230
+ const abs = isAbsPath(explicit) ? explicit : join(projectRoot, explicit);
231
+ return existsSync(abs) ? abs : abs;
221
232
  }
222
- const configured = config.datalex?.manifestPath;
223
- if (configured) {
224
- return isAbsPath(configured) ? configured : join(projectRoot, configured);
233
+ const config = loadedConfig ?? loadProjectConfig(projectRoot);
234
+ if (config.datalex?.manifestPath) {
235
+ const rel = config.datalex.manifestPath;
236
+ const abs = isAbsPath(rel) ? rel : join(projectRoot, rel);
237
+ if (existsSync(abs))
238
+ return abs;
239
+ return abs;
225
240
  }
226
241
  const fallback = join(projectRoot, 'datalex-manifest.json');
227
242
  if (existsSync(fallback))
@@ -258,6 +273,52 @@ function scanFilesRecursive(dir, extensions) {
258
273
  return results;
259
274
  }
260
275
  // ---- Block Scanning ----
276
+ function scanDomains(projectRoot, dirs, diagnostics) {
277
+ const domains = {};
278
+ const seenFiles = new Set();
279
+ for (const dir of dirs) {
280
+ const dirPath = join(projectRoot, dir);
281
+ const files = scanFilesRecursive(dirPath, ['.dql']);
282
+ for (const filePath of files) {
283
+ if (seenFiles.has(filePath))
284
+ continue;
285
+ seenFiles.add(filePath);
286
+ const relPath = relative(projectRoot, filePath);
287
+ try {
288
+ const source = readFileSync(filePath, 'utf-8');
289
+ if (!/(^|\n)\s*domain\s+"/.test(source))
290
+ continue;
291
+ const parser = new Parser(source, relPath);
292
+ const ast = parser.parse();
293
+ for (const stmt of ast.statements) {
294
+ const domain = stmt;
295
+ if (domain.kind !== 'DomainDecl')
296
+ continue;
297
+ if (domains[domain.name]) {
298
+ diagnostics?.push({
299
+ kind: 'resolve',
300
+ filePath: relPath,
301
+ severity: 'error',
302
+ message: `duplicate domain "${domain.name}" also declared in ${domains[domain.name].filePath}`,
303
+ });
304
+ continue;
305
+ }
306
+ domains[domain.name] = domainDeclToManifestDomain(domain, relPath);
307
+ }
308
+ }
309
+ catch (err) {
310
+ const msg = err instanceof Error ? err.message : String(err);
311
+ diagnostics?.push({
312
+ kind: 'parse',
313
+ filePath: relPath,
314
+ severity: 'error',
315
+ message: `Failed to parse domain file: ${msg}`,
316
+ });
317
+ }
318
+ }
319
+ }
320
+ return domains;
321
+ }
261
322
  function scanBlocks(projectRoot, dirs, diagnostics, datalexRegistry) {
262
323
  const blocks = {};
263
324
  for (const dir of dirs) {
@@ -394,6 +455,181 @@ function scanTerms(projectRoot, dirs, diagnostics) {
394
455
  }
395
456
  return terms;
396
457
  }
458
+ function validateDomains(domains, blocks, views, terms, diagnostics) {
459
+ const resolveDomain = createManifestDomainResolver(domains);
460
+ const declaredDomains = new Set(Object.values(domains).map((domain) => domain.name));
461
+ const usedDomains = new Map();
462
+ const addUse = (domain, label) => {
463
+ if (!domain)
464
+ return;
465
+ const resolvedDomain = resolveDomain(domain);
466
+ const list = usedDomains.get(resolvedDomain) ?? [];
467
+ list.push(label);
468
+ usedDomains.set(resolvedDomain, list);
469
+ };
470
+ for (const block of Object.values(blocks)) {
471
+ addUse(block.domain, `block "${block.name}"`);
472
+ if (!block.domain) {
473
+ diagnostics.push({
474
+ kind: 'resolve',
475
+ filePath: block.filePath,
476
+ severity: 'warning',
477
+ message: `block "${block.name}" is missing a domain. Add a domain field or place it under domains/<domain>/blocks/.`,
478
+ });
479
+ }
480
+ }
481
+ for (const view of Object.values(views))
482
+ addUse(view.domain, `business_view "${view.name}"`);
483
+ for (const term of Object.values(terms))
484
+ addUse(term.domain, `term "${term.name}"`);
485
+ for (const block of Object.values(blocks)) {
486
+ if (!block.domain)
487
+ continue;
488
+ const blockDomain = resolveDomain(block.domain);
489
+ for (const ref of block.refDependencies ?? []) {
490
+ const dependency = blocks[ref];
491
+ const dependencyDomain = dependency?.domain ? resolveDomain(dependency.domain) : undefined;
492
+ if (!dependencyDomain || dependencyDomain === blockDomain)
493
+ continue;
494
+ diagnostics.push({
495
+ kind: 'resolve',
496
+ filePath: block.filePath,
497
+ severity: 'warning',
498
+ message: `block "${block.name}" in domain "${blockDomain}" depends on block "${dependency.name}" in domain "${dependencyDomain}". Mark this as a bridge pattern or document the cross-domain dependency.`,
499
+ });
500
+ }
501
+ }
502
+ for (const view of Object.values(views)) {
503
+ if (!view.domain)
504
+ continue;
505
+ const viewDomain = resolveDomain(view.domain);
506
+ for (const ref of view.blockRefs ?? []) {
507
+ const block = blocks[ref];
508
+ const blockDomain = block?.domain ? resolveDomain(block.domain) : undefined;
509
+ if (!blockDomain || blockDomain === viewDomain)
510
+ continue;
511
+ diagnostics.push({
512
+ kind: 'resolve',
513
+ filePath: view.filePath,
514
+ severity: 'warning',
515
+ message: `business_view "${view.name}" in domain "${viewDomain}" includes block "${block.name}" from domain "${blockDomain}". Review the cross-domain dependency.`,
516
+ });
517
+ }
518
+ for (const ref of view.businessViewRefs ?? []) {
519
+ const dependency = views[ref];
520
+ const dependencyDomain = dependency?.domain ? resolveDomain(dependency.domain) : undefined;
521
+ if (!dependencyDomain || dependencyDomain === viewDomain)
522
+ continue;
523
+ diagnostics.push({
524
+ kind: 'resolve',
525
+ filePath: view.filePath,
526
+ severity: 'warning',
527
+ message: `business_view "${view.name}" in domain "${viewDomain}" includes business_view "${dependency.name}" from domain "${dependencyDomain}". Review the cross-domain dependency.`,
528
+ });
529
+ }
530
+ }
531
+ for (const [domain, users] of usedDomains) {
532
+ if (!declaredDomains.has(domain)) {
533
+ diagnostics.push({
534
+ kind: 'resolve',
535
+ severity: 'warning',
536
+ message: `domain "${domain}" is used by ${users.slice(0, 3).join(', ')}${users.length > 3 ? ` and ${users.length - 3} more` : ''} but has no first-class domain declaration.`,
537
+ });
538
+ }
539
+ }
540
+ for (const domain of Object.values(domains)) {
541
+ if (!domain.owner) {
542
+ diagnostics.push({
543
+ kind: 'resolve',
544
+ filePath: domain.filePath,
545
+ severity: 'warning',
546
+ message: `domain "${domain.name}" is missing owner metadata.`,
547
+ });
548
+ }
549
+ if (!domain.reviewCadence) {
550
+ diagnostics.push({
551
+ kind: 'resolve',
552
+ filePath: domain.filePath,
553
+ severity: 'warning',
554
+ message: `domain "${domain.name}" is missing reviewCadence metadata.`,
555
+ });
556
+ }
557
+ else {
558
+ const cadenceDays = reviewCadenceDays(domain.reviewCadence);
559
+ if (cadenceDays === null) {
560
+ diagnostics.push({
561
+ kind: 'resolve',
562
+ filePath: domain.filePath,
563
+ severity: 'warning',
564
+ message: `domain "${domain.name}" has unrecognized reviewCadence "${domain.reviewCadence}". Use daily, weekly, biweekly, monthly, quarterly, semiannual, or annual.`,
565
+ });
566
+ }
567
+ else if (cadenceDays > 180) {
568
+ diagnostics.push({
569
+ kind: 'resolve',
570
+ filePath: domain.filePath,
571
+ severity: 'warning',
572
+ message: `domain "${domain.name}" reviewCadence "${domain.reviewCadence}" is stale for enterprise use. Prefer quarterly or more frequent review for active domains.`,
573
+ });
574
+ }
575
+ }
576
+ if (!usedDomains.has(domain.name)) {
577
+ diagnostics.push({
578
+ kind: 'resolve',
579
+ filePath: domain.filePath,
580
+ severity: 'warning',
581
+ message: `domain "${domain.name}" has no terms, blocks, or business views yet.`,
582
+ });
583
+ }
584
+ }
585
+ }
586
+ function createManifestDomainResolver(domains) {
587
+ const aliases = new Map();
588
+ for (const domain of Object.values(domains)) {
589
+ aliases.set(domain.name, domain.name);
590
+ aliases.set(domainAliasKey(domain.name), domain.name);
591
+ const folderAlias = domain.filePath?.replace(/\\/g, '/').match(/^domains\/([^/]+)\//)?.[1];
592
+ if (folderAlias)
593
+ aliases.set(domainAliasKey(folderAlias), domain.name);
594
+ }
595
+ return (domain) => aliases.get(domain) ?? aliases.get(domainAliasKey(domain)) ?? domain;
596
+ }
597
+ function domainAliasKey(value) {
598
+ return value.trim().toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '');
599
+ }
600
+ function reviewCadenceDays(value) {
601
+ const normalized = value.trim().toLowerCase().replace(/[\s_-]+/g, '');
602
+ if (!normalized)
603
+ return null;
604
+ if (normalized === 'daily' || normalized === 'day')
605
+ return 1;
606
+ if (normalized === 'weekly' || normalized === 'week')
607
+ return 7;
608
+ if (normalized === 'biweekly' || normalized === 'fortnightly')
609
+ return 14;
610
+ if (normalized === 'monthly' || normalized === 'month')
611
+ return 30;
612
+ if (normalized === 'quarterly' || normalized === 'quarter')
613
+ return 90;
614
+ if (normalized === 'semiannual' || normalized === 'semiannually' || normalized === 'halfyearly')
615
+ return 180;
616
+ if (normalized === 'annual' || normalized === 'annually' || normalized === 'yearly')
617
+ return 365;
618
+ const everyMatch = normalized.match(/^every(\d+)(day|days|week|weeks|month|months)$/);
619
+ if (!everyMatch)
620
+ return null;
621
+ const amount = Number(everyMatch[1]);
622
+ if (!Number.isFinite(amount) || amount <= 0)
623
+ return null;
624
+ const unit = everyMatch[2];
625
+ if (unit.startsWith('day'))
626
+ return amount;
627
+ if (unit.startsWith('week'))
628
+ return amount * 7;
629
+ if (unit.startsWith('month'))
630
+ return amount * 30;
631
+ return null;
632
+ }
397
633
  function validateBusinessViews(views, blocks, diagnostics) {
398
634
  const blockNames = new Set(Object.keys(blocks));
399
635
  const viewNames = new Set(Object.keys(views));
@@ -1251,7 +1487,7 @@ function appDocumentToManifest(app, filePath, dashboardIds) {
1251
1487
  };
1252
1488
  }
1253
1489
  // ---- Lineage Builder ----
1254
- function buildManifestLineage(blocks, metrics, dimensions, notebooks, dbtImport, apps, appDashboards, businessViews, terms) {
1490
+ function buildManifestLineage(manifestDomains, blocks, metrics, dimensions, notebooks, dbtImport, apps, appDashboards, businessViews, terms) {
1255
1491
  // Convert to lineage builder input format
1256
1492
  const lineageBlocks = Object.values(blocks).map((b) => ({
1257
1493
  name: b.name,
@@ -1265,7 +1501,33 @@ function buildManifestLineage(blocks, metrics, dimensions, notebooks, dbtImport,
1265
1501
  dimensionsRef: b.dimensionsRef,
1266
1502
  chartType: b.chartType,
1267
1503
  filePath: b.filePath,
1504
+ description: b.description,
1505
+ businessOutcome: b.businessOutcome,
1506
+ reviewCadence: b.reviewCadence,
1507
+ tests: b.tests,
1268
1508
  termRefs: b.termRefs,
1509
+ pattern: b.pattern,
1510
+ grain: b.grain,
1511
+ entities: b.entities,
1512
+ declaredOutputs: b.declaredOutputs,
1513
+ dimensions: b.dimensions,
1514
+ allowedFilters: b.allowedFilters,
1515
+ parameterPolicy: b.parameterPolicy,
1516
+ filterBindings: b.filterBindings,
1517
+ sourceSystems: b.sourceSystems,
1518
+ replacementFor: b.replacementFor,
1519
+ }));
1520
+ const lineageDomains = Object.values(manifestDomains ?? {}).map((domain) => ({
1521
+ name: domain.name,
1522
+ owner: domain.owner,
1523
+ businessOwner: domain.businessOwner,
1524
+ boundedContext: domain.boundedContext,
1525
+ filePath: domain.filePath,
1526
+ sourceSystems: domain.sourceSystems,
1527
+ primaryTerms: domain.primaryTerms,
1528
+ reviewCadence: domain.reviewCadence,
1529
+ businessOutcome: domain.businessOutcome,
1530
+ tags: domain.tags,
1269
1531
  }));
1270
1532
  const lineageTerms = Object.values(terms ?? {}).map((term) => ({
1271
1533
  name: term.name,
@@ -1278,6 +1540,7 @@ function buildManifestLineage(blocks, metrics, dimensions, notebooks, dbtImport,
1278
1540
  identifiers: term.identifiers,
1279
1541
  synonyms: term.synonyms,
1280
1542
  businessOutcome: term.businessOutcome,
1543
+ reviewCadence: term.reviewCadence,
1281
1544
  }));
1282
1545
  const lineageMetrics = Object.values(metrics).map((m) => ({
1283
1546
  name: m.name,
@@ -1297,6 +1560,7 @@ function buildManifestLineage(blocks, metrics, dimensions, notebooks, dbtImport,
1297
1560
  filePath: view.filePath,
1298
1561
  description: view.description,
1299
1562
  businessOutcome: view.businessOutcome,
1563
+ reviewCadence: view.reviewCadence,
1300
1564
  blockRefs: view.blockRefs,
1301
1565
  businessViewRefs: view.businessViewRefs,
1302
1566
  termRefs: view.termRefs,
@@ -1403,6 +1667,7 @@ function buildManifestLineage(blocks, metrics, dimensions, notebooks, dbtImport,
1403
1667
  apps: lineageApps,
1404
1668
  businessViews: lineageBusinessViews,
1405
1669
  terms: lineageTerms,
1670
+ domains: lineageDomains,
1406
1671
  });
1407
1672
  // Serialize to manifest format
1408
1673
  const graphJson = graph.toJSON();
@@ -1480,12 +1745,33 @@ function blockDeclToManifestBlock(block, filePath) {
1480
1745
  ...((block.dimensionsRef ?? []).map((d) => `@dim(${d})`)),
1481
1746
  ],
1482
1747
  chartType: extractVisualizationChart(block),
1748
+ displayHints: extractDisplayHints(block),
1483
1749
  metricRef: block.metricRef,
1484
1750
  metricsRef: block.metricsRef,
1485
1751
  tests,
1486
1752
  tags,
1487
1753
  description,
1488
1754
  termRefs: Array.isArray(block.termRefs) ? block.termRefs : undefined,
1755
+ pattern: typeof block.pattern === 'string' ? block.pattern : undefined,
1756
+ grain: typeof block.grain === 'string' ? block.grain : undefined,
1757
+ entities: Array.isArray(block.entities) ? block.entities : undefined,
1758
+ declaredOutputs: Array.isArray(block.outputs) ? block.outputs : undefined,
1759
+ dimensions: Array.isArray(block.dimensions) ? block.dimensions : undefined,
1760
+ allowedFilters: Array.isArray(block.allowedFilters) ? block.allowedFilters : undefined,
1761
+ parameterPolicy: Array.isArray(block.parameterPolicy)
1762
+ ? block.parameterPolicy.map((entry) => ({
1763
+ name: entry.name,
1764
+ policy: entry.policy,
1765
+ }))
1766
+ : undefined,
1767
+ filterBindings: Array.isArray(block.filterBindings)
1768
+ ? block.filterBindings.map((entry) => ({
1769
+ filter: entry.filter,
1770
+ binding: entry.binding,
1771
+ }))
1772
+ : undefined,
1773
+ sourceSystems: Array.isArray(block.sourceSystems) ? block.sourceSystems : undefined,
1774
+ replacementFor: Array.isArray(block.replacementFor) ? block.replacementFor : undefined,
1489
1775
  unresolvedTermRefs: [],
1490
1776
  llmContext: agent.llmContext,
1491
1777
  examples: agent.examples,
@@ -1509,6 +1795,21 @@ function blockDeclToManifestBlock(block, filePath) {
1509
1795
  : undefined,
1510
1796
  };
1511
1797
  }
1798
+ function domainDeclToManifestDomain(domain, filePath) {
1799
+ return {
1800
+ name: domain.name,
1801
+ filePath,
1802
+ owner: typeof domain.owner === 'string' ? domain.owner : undefined,
1803
+ businessOwner: typeof domain.businessOwner === 'string' ? domain.businessOwner : undefined,
1804
+ boundedContext: typeof domain.boundedContext === 'string' ? domain.boundedContext : undefined,
1805
+ sourceSystems: Array.isArray(domain.sourceSystems) ? domain.sourceSystems : undefined,
1806
+ primaryTerms: Array.isArray(domain.primaryTerms) ? domain.primaryTerms : undefined,
1807
+ reviewCadence: typeof domain.reviewCadence === 'string' ? domain.reviewCadence : undefined,
1808
+ tags: Array.isArray(domain.tags) ? domain.tags : undefined,
1809
+ businessOutcome: typeof domain.businessOutcome === 'string' ? domain.businessOutcome : undefined,
1810
+ description: typeof domain.description === 'string' ? domain.description : undefined,
1811
+ };
1812
+ }
1512
1813
  function termDeclToManifestTerm(term, filePath) {
1513
1814
  return {
1514
1815
  name: term.name,
@@ -1606,6 +1907,51 @@ function extractVisualizationChart(block) {
1606
1907
  }
1607
1908
  return undefined;
1608
1909
  }
1910
+ function extractDisplayHints(block) {
1911
+ const chart = extractVisualizationChart(block);
1912
+ const fieldHints = {};
1913
+ if (block.visualization) {
1914
+ for (const prop of block.visualization.properties ?? []) {
1915
+ const name = String(prop.name ?? prop.key ?? '');
1916
+ if (!['x', 'y', 'color', 'time', 'label', 'value', 'rank'].includes(name))
1917
+ continue;
1918
+ const value = literalOrIdentifier(prop.value);
1919
+ if (value)
1920
+ fieldHints[name] = value;
1921
+ }
1922
+ }
1923
+ if (!chart && Object.keys(fieldHints).length === 0)
1924
+ return undefined;
1925
+ const allowed = new Set();
1926
+ if (chart)
1927
+ allowed.add(chart);
1928
+ allowed.add('table');
1929
+ if (chart === 'bar' || chart === 'grouped_bar' || chart === 'stacked_bar') {
1930
+ allowed.add('line');
1931
+ }
1932
+ if (chart === 'line' || chart === 'area') {
1933
+ allowed.add('bar');
1934
+ }
1935
+ return {
1936
+ ...(chart ? { defaultVisualization: chart } : {}),
1937
+ allowedVisualizations: Array.from(allowed),
1938
+ ...(Object.keys(fieldHints).length > 0 ? { fieldHints } : {}),
1939
+ source: 'block_visualization',
1940
+ };
1941
+ }
1942
+ function literalOrIdentifier(value) {
1943
+ if (!value)
1944
+ return undefined;
1945
+ if (value.kind === 'StringLiteral' || value.kind === 'Literal')
1946
+ return String(value.value);
1947
+ if (value.kind === 'Identifier')
1948
+ return String(value.name);
1949
+ if (typeof value.name === 'string')
1950
+ return value.name;
1951
+ if (typeof value.value === 'string' || typeof value.value === 'number')
1952
+ return String(value.value);
1953
+ return undefined;
1954
+ }
1609
1955
  function extractTags(block) {
1610
1956
  if (Array.isArray(block.tags))
1611
1957
  return block.tags;