@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.
- package/dist/apps/app-document.d.ts +2 -1
- package/dist/apps/app-document.d.ts.map +1 -1
- package/dist/apps/app-document.js +18 -5
- package/dist/apps/app-document.js.map +1 -1
- package/dist/apps/dashboard-document.d.ts +19 -0
- package/dist/apps/dashboard-document.d.ts.map +1 -1
- package/dist/apps/dashboard-document.js +72 -0
- package/dist/apps/dashboard-document.js.map +1 -1
- package/dist/apps/index.d.ts +1 -1
- package/dist/apps/index.d.ts.map +1 -1
- package/dist/ast/nodes.d.ts +40 -1
- package/dist/ast/nodes.d.ts.map +1 -1
- package/dist/ast/nodes.js +1 -0
- package/dist/ast/nodes.js.map +1 -1
- package/dist/ast/printer.d.ts.map +1 -1
- package/dist/ast/printer.js +40 -0
- package/dist/ast/printer.js.map +1 -1
- package/dist/formatter/formatter.d.ts.map +1 -1
- package/dist/formatter/formatter.js +66 -0
- package/dist/formatter/formatter.js.map +1 -1
- package/dist/lineage/builder.d.ts +35 -0
- package/dist/lineage/builder.d.ts.map +1 -1
- package/dist/lineage/builder.js +96 -8
- package/dist/lineage/builder.js.map +1 -1
- package/dist/lineage/index.d.ts +1 -1
- package/dist/lineage/index.d.ts.map +1 -1
- package/dist/lineage/index.js.map +1 -1
- package/dist/lineage/query.d.ts +30 -1
- package/dist/lineage/query.d.ts.map +1 -1
- package/dist/lineage/query.js +248 -1
- package/dist/lineage/query.js.map +1 -1
- package/dist/manifest/builder.d.ts +6 -1
- package/dist/manifest/builder.d.ts.map +1 -1
- package/dist/manifest/builder.js +359 -13
- package/dist/manifest/builder.js.map +1 -1
- package/dist/manifest/types.d.ts +52 -0
- package/dist/manifest/types.d.ts.map +1 -1
- package/dist/parser/parser.d.ts +4 -0
- package/dist/parser/parser.d.ts.map +1 -1
- package/dist/parser/parser.js +262 -20
- package/dist/parser/parser.js.map +1 -1
- package/dist/semantic/analyzer.d.ts.map +1 -1
- package/dist/semantic/analyzer.js +4 -0
- package/dist/semantic/analyzer.js.map +1 -1
- package/dist/semantic/providers/dbt-provider.js +130 -4
- package/dist/semantic/providers/dbt-provider.js.map +1 -1
- package/dist/semantic/providers/registry.js +68 -4
- package/dist/semantic/providers/registry.js.map +1 -1
- package/dist/semantic/providers/snowflake-provider.d.ts.map +1 -1
- package/dist/semantic/providers/snowflake-provider.js +60 -8
- package/dist/semantic/providers/snowflake-provider.js.map +1 -1
- package/dist/semantic/semantic-layer.d.ts.map +1 -1
- package/dist/semantic/semantic-layer.js +11 -20
- package/dist/semantic/semantic-layer.js.map +1 -1
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"builder.d.ts","sourceRoot":"","sources":["../../src/manifest/builder.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;
|
|
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"}
|
package/dist/manifest/builder.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
230
|
+
const abs = isAbsPath(explicit) ? explicit : join(projectRoot, explicit);
|
|
231
|
+
return existsSync(abs) ? abs : abs;
|
|
221
232
|
}
|
|
222
|
-
const
|
|
223
|
-
if (
|
|
224
|
-
|
|
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;
|