@duckcodeailabs/dql-cli 0.6.0 → 0.7.1
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/assets/dql-notebook/assets/index-BwgX4Mvs.js +510 -0
- package/dist/assets/dql-notebook/index.html +1 -1
- package/dist/commands/compile.d.ts +15 -0
- package/dist/commands/compile.d.ts.map +1 -0
- package/dist/commands/compile.js +125 -0
- package/dist/commands/compile.js.map +1 -0
- package/dist/commands/lineage.d.ts +5 -2
- package/dist/commands/lineage.d.ts.map +1 -1
- package/dist/commands/lineage.js +291 -49
- package/dist/commands/lineage.js.map +1 -1
- package/dist/index.js +6 -1
- package/dist/index.js.map +1 -1
- package/dist/package.json +7 -7
- package/package.json +7 -7
- package/dist/assets/dql-notebook/assets/index-B_X7pyPz.js +0 -510
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
|
8
8
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
|
9
9
|
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet" />
|
|
10
|
-
<script type="module" crossorigin src="/assets/index-
|
|
10
|
+
<script type="module" crossorigin src="/assets/index-BwgX4Mvs.js"></script>
|
|
11
11
|
<link rel="modulepreload" crossorigin href="/assets/react-CRB3T2We.js">
|
|
12
12
|
<link rel="modulepreload" crossorigin href="/assets/codemirror-CCrEt63p.js">
|
|
13
13
|
</head>
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `dql compile` — Generate the DQL project manifest.
|
|
3
|
+
*
|
|
4
|
+
* Scans all blocks, notebooks, and semantic layer definitions,
|
|
5
|
+
* resolves dependencies, builds lineage, and writes dql-manifest.json.
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* dql compile [path] Compile project at path (default: .)
|
|
9
|
+
* dql compile --dbt-manifest <path> Import dbt manifest.json as upstream
|
|
10
|
+
* dql compile --out-dir <dir> Write manifest to a specific directory
|
|
11
|
+
* dql compile --format json Output manifest to stdout (no file write)
|
|
12
|
+
*/
|
|
13
|
+
import type { CLIFlags } from '../args.js';
|
|
14
|
+
export declare function runCompile(pathArg: string | null, rest: string[], flags: CLIFlags): Promise<void>;
|
|
15
|
+
//# sourceMappingURL=compile.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"compile.d.ts","sourceRoot":"","sources":["../../src/commands/compile.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAKH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAE3C,wBAAsB,UAAU,CAC9B,OAAO,EAAE,MAAM,GAAG,IAAI,EACtB,IAAI,EAAE,MAAM,EAAE,EACd,KAAK,EAAE,QAAQ,GACd,OAAO,CAAC,IAAI,CAAC,CA4Hf"}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `dql compile` — Generate the DQL project manifest.
|
|
3
|
+
*
|
|
4
|
+
* Scans all blocks, notebooks, and semantic layer definitions,
|
|
5
|
+
* resolves dependencies, builds lineage, and writes dql-manifest.json.
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* dql compile [path] Compile project at path (default: .)
|
|
9
|
+
* dql compile --dbt-manifest <path> Import dbt manifest.json as upstream
|
|
10
|
+
* dql compile --out-dir <dir> Write manifest to a specific directory
|
|
11
|
+
* dql compile --format json Output manifest to stdout (no file write)
|
|
12
|
+
*/
|
|
13
|
+
import { writeFileSync, existsSync, readFileSync } from 'node:fs';
|
|
14
|
+
import { join, resolve } from 'node:path';
|
|
15
|
+
import { buildManifest } from '@duckcodeailabs/dql-core';
|
|
16
|
+
export async function runCompile(pathArg, rest, flags) {
|
|
17
|
+
// Collect all args (pathArg might be a flag if no path was given)
|
|
18
|
+
const allArgs = [...(pathArg ? [pathArg] : []), ...rest];
|
|
19
|
+
// Parse --dbt-manifest flag
|
|
20
|
+
let dbtManifestPath;
|
|
21
|
+
const dbtIdx = allArgs.indexOf('--dbt-manifest');
|
|
22
|
+
if (dbtIdx >= 0 && allArgs[dbtIdx + 1]) {
|
|
23
|
+
dbtManifestPath = resolve(allArgs[dbtIdx + 1]);
|
|
24
|
+
if (!existsSync(dbtManifestPath)) {
|
|
25
|
+
console.error(`dbt manifest not found: ${dbtManifestPath}`);
|
|
26
|
+
process.exitCode = 1;
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
// Determine project root — first non-flag positional arg, or cwd
|
|
31
|
+
const pathCandidates = allArgs.filter((a) => !a.startsWith('-'));
|
|
32
|
+
// Remove the dbt manifest path from candidates (only if --dbt-manifest was found)
|
|
33
|
+
const filteredCandidates = dbtIdx >= 0
|
|
34
|
+
? pathCandidates.filter((c) => c !== allArgs[dbtIdx + 1])
|
|
35
|
+
: pathCandidates;
|
|
36
|
+
const projectRoot = resolve(filteredCandidates[0] ?? '.');
|
|
37
|
+
if (!existsSync(join(projectRoot, 'dql.config.json'))) {
|
|
38
|
+
console.error('No DQL project found (missing dql.config.json). Run from a project root or pass a project path.');
|
|
39
|
+
process.exitCode = 1;
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
// Read DQL version from CLI package.json
|
|
43
|
+
let dqlVersion = '0.6.0';
|
|
44
|
+
try {
|
|
45
|
+
const pkgPath = join(import.meta.dirname ?? __dirname, '..', '..', 'package.json');
|
|
46
|
+
if (existsSync(pkgPath)) {
|
|
47
|
+
dqlVersion = JSON.parse(readFileSync(pkgPath, 'utf-8')).version ?? dqlVersion;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
catch { /* use default */ }
|
|
51
|
+
// Build manifest
|
|
52
|
+
const startTime = Date.now();
|
|
53
|
+
let manifest;
|
|
54
|
+
try {
|
|
55
|
+
manifest = buildManifest({
|
|
56
|
+
projectRoot,
|
|
57
|
+
dqlVersion,
|
|
58
|
+
dbtManifestPath,
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
catch (err) {
|
|
62
|
+
console.error(`Failed to compile project: ${err instanceof Error ? err.message : String(err)}`);
|
|
63
|
+
process.exitCode = 1;
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
const elapsed = Date.now() - startTime;
|
|
67
|
+
// JSON mode: output to stdout
|
|
68
|
+
if (flags.format === 'json') {
|
|
69
|
+
console.log(JSON.stringify(manifest, null, 2));
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
// Write manifest file
|
|
73
|
+
const outDir = flags.outDir ? resolve(flags.outDir) : projectRoot;
|
|
74
|
+
const manifestPath = join(outDir, 'dql-manifest.json');
|
|
75
|
+
writeFileSync(manifestPath, JSON.stringify(manifest, null, 2));
|
|
76
|
+
// Print summary
|
|
77
|
+
const blockCount = Object.keys(manifest.blocks).length;
|
|
78
|
+
const notebookCount = Object.keys(manifest.notebooks).length;
|
|
79
|
+
const metricCount = Object.keys(manifest.metrics).length;
|
|
80
|
+
const dimensionCount = Object.keys(manifest.dimensions).length;
|
|
81
|
+
const sourceCount = Object.keys(manifest.sources).length;
|
|
82
|
+
const nodeCount = manifest.lineage.nodes.length;
|
|
83
|
+
const edgeCount = manifest.lineage.edges.length;
|
|
84
|
+
const domainCount = manifest.lineage.domains.length;
|
|
85
|
+
console.log(`\n DQL Compile — ${manifest.project}`);
|
|
86
|
+
console.log(' ' + '='.repeat(50));
|
|
87
|
+
console.log(`\n Scanned:`);
|
|
88
|
+
console.log(` ${blockCount} block(s)`);
|
|
89
|
+
console.log(` ${notebookCount} notebook(s)`);
|
|
90
|
+
console.log(` ${metricCount} metric(s)`);
|
|
91
|
+
console.log(` ${dimensionCount} dimension(s)`);
|
|
92
|
+
console.log(` ${sourceCount} source table(s)`);
|
|
93
|
+
console.log(`\n Lineage:`);
|
|
94
|
+
console.log(` ${nodeCount} nodes, ${edgeCount} edges, ${domainCount} domain(s)`);
|
|
95
|
+
if (manifest.lineage.crossDomainFlows.length > 0) {
|
|
96
|
+
console.log(` ${manifest.lineage.crossDomainFlows.length} cross-domain flow(s)`);
|
|
97
|
+
}
|
|
98
|
+
if (manifest.dbtImport) {
|
|
99
|
+
console.log(`\n dbt Import:`);
|
|
100
|
+
console.log(` ${manifest.dbtImport.modelsImported} model(s), ${manifest.dbtImport.sourcesImported} source(s)`);
|
|
101
|
+
console.log(` from: ${manifest.dbtImport.manifestPath}`);
|
|
102
|
+
}
|
|
103
|
+
console.log(`\n Manifest written to: ${manifestPath}`);
|
|
104
|
+
console.log(` Compiled in ${elapsed}ms\n`);
|
|
105
|
+
if (flags.verbose) {
|
|
106
|
+
// Show block details
|
|
107
|
+
console.log(' Blocks:');
|
|
108
|
+
for (const block of Object.values(manifest.blocks)) {
|
|
109
|
+
const deps = block.allDependencies;
|
|
110
|
+
const meta = [block.domain, block.owner].filter(Boolean).join(', ');
|
|
111
|
+
console.log(` ${block.name}${meta ? ` (${meta})` : ''}`);
|
|
112
|
+
if (deps.length > 0) {
|
|
113
|
+
console.log(` depends on: ${deps.join(', ')}`);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
if (Object.keys(manifest.notebooks).length > 0) {
|
|
117
|
+
console.log('\n Notebooks:');
|
|
118
|
+
for (const nb of Object.values(manifest.notebooks)) {
|
|
119
|
+
console.log(` ${nb.title} (${nb.cells.length} cells) — ${nb.filePath}`);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
console.log('');
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
//# sourceMappingURL=compile.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"compile.js","sourceRoot":"","sources":["../../src/commands/compile.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,EAAE,aAAa,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAClE,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAC1C,OAAO,EAAE,aAAa,EAAoB,MAAM,0BAA0B,CAAC;AAG3E,MAAM,CAAC,KAAK,UAAU,UAAU,CAC9B,OAAsB,EACtB,IAAc,EACd,KAAe;IAEf,kEAAkE;IAClE,MAAM,OAAO,GAAG,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,GAAG,IAAI,CAAC,CAAC;IAEzD,4BAA4B;IAC5B,IAAI,eAAmC,CAAC;IACxC,MAAM,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,gBAAgB,CAAC,CAAC;IACjD,IAAI,MAAM,IAAI,CAAC,IAAI,OAAO,CAAC,MAAM,GAAG,CAAC,CAAC,EAAE,CAAC;QACvC,eAAe,GAAG,OAAO,CAAC,OAAO,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC;QAC/C,IAAI,CAAC,UAAU,CAAC,eAAe,CAAC,EAAE,CAAC;YACjC,OAAO,CAAC,KAAK,CAAC,2BAA2B,eAAe,EAAE,CAAC,CAAC;YAC5D,OAAO,CAAC,QAAQ,GAAG,CAAC,CAAC;YACrB,OAAO;QACT,CAAC;IACH,CAAC;IAED,iEAAiE;IACjE,MAAM,cAAc,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC,CAAC;IACjE,kFAAkF;IAClF,MAAM,kBAAkB,GAAG,MAAM,IAAI,CAAC;QACpC,CAAC,CAAC,cAAc,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,KAAK,OAAO,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;QACzD,CAAC,CAAC,cAAc,CAAC;IACnB,MAAM,WAAW,GAAG,OAAO,CAAC,kBAAkB,CAAC,CAAC,CAAC,IAAI,GAAG,CAAC,CAAC;IAE1D,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC,WAAW,EAAE,iBAAiB,CAAC,CAAC,EAAE,CAAC;QACtD,OAAO,CAAC,KAAK,CAAC,iGAAiG,CAAC,CAAC;QACjH,OAAO,CAAC,QAAQ,GAAG,CAAC,CAAC;QACrB,OAAO;IACT,CAAC;IAED,yCAAyC;IACzC,IAAI,UAAU,GAAG,OAAO,CAAC;IACzB,IAAI,CAAC;QACH,MAAM,OAAO,GAAG,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,OAAO,IAAI,SAAS,EAAE,IAAI,EAAE,IAAI,EAAE,cAAc,CAAC,CAAC;QACnF,IAAI,UAAU,CAAC,OAAO,CAAC,EAAE,CAAC;YACxB,UAAU,GAAG,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC,CAAC,OAAO,IAAI,UAAU,CAAC;QAChF,CAAC;IACH,CAAC;IAAC,MAAM,CAAC,CAAC,iBAAiB,CAAC,CAAC;IAE7B,iBAAiB;IACjB,MAAM,SAAS,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;IAC7B,IAAI,QAAqB,CAAC;IAE1B,IAAI,CAAC;QACH,QAAQ,GAAG,aAAa,CAAC;YACvB,WAAW;YACX,UAAU;YACV,eAAe;SAChB,CAAC,CAAC;IACL,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,OAAO,CAAC,KAAK,CAAC,8BAA8B,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;QAChG,OAAO,CAAC,QAAQ,GAAG,CAAC,CAAC;QACrB,OAAO;IACT,CAAC;IAED,MAAM,OAAO,GAAG,IAAI,CAAC,GAAG,EAAE,GAAG,SAAS,CAAC;IAEvC,8BAA8B;IAC9B,IAAI,KAAK,CAAC,MAAM,KAAK,MAAM,EAAE,CAAC;QAC5B,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,SAAS,CAAC,QAAQ,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC;QAC/C,OAAO;IACT,CAAC;IAED,sBAAsB;IACtB,MAAM,MAAM,GAAG,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,OAAO,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,WAAW,CAAC;IAClE,MAAM,YAAY,GAAG,IAAI,CAAC,MAAM,EAAE,mBAAmB,CAAC,CAAC;IACvD,aAAa,CAAC,YAAY,EAAE,IAAI,CAAC,SAAS,CAAC,QAAQ,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC;IAE/D,gBAAgB;IAChB,MAAM,UAAU,GAAG,MAAM,CAAC,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,MAAM,CAAC;IACvD,MAAM,aAAa,GAAG,MAAM,CAAC,IAAI,CAAC,QAAQ,CAAC,SAAS,CAAC,CAAC,MAAM,CAAC;IAC7D,MAAM,WAAW,GAAG,MAAM,CAAC,IAAI,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC,MAAM,CAAC;IACzD,MAAM,cAAc,GAAG,MAAM,CAAC,IAAI,CAAC,QAAQ,CAAC,UAAU,CAAC,CAAC,MAAM,CAAC;IAC/D,MAAM,WAAW,GAAG,MAAM,CAAC,IAAI,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC,MAAM,CAAC;IACzD,MAAM,SAAS,GAAG,QAAQ,CAAC,OAAO,CAAC,KAAK,CAAC,MAAM,CAAC;IAChD,MAAM,SAAS,GAAG,QAAQ,CAAC,OAAO,CAAC,KAAK,CAAC,MAAM,CAAC;IAChD,MAAM,WAAW,GAAG,QAAQ,CAAC,OAAO,CAAC,OAAO,CAAC,MAAM,CAAC;IAEpD,OAAO,CAAC,GAAG,CAAC,qBAAqB,QAAQ,CAAC,OAAO,EAAE,CAAC,CAAC;IACrD,OAAO,CAAC,GAAG,CAAC,IAAI,GAAG,GAAG,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC,CAAC;IACnC,OAAO,CAAC,GAAG,CAAC,cAAc,CAAC,CAAC;IAC5B,OAAO,CAAC,GAAG,CAAC,OAAO,UAAU,WAAW,CAAC,CAAC;IAC1C,OAAO,CAAC,GAAG,CAAC,OAAO,aAAa,cAAc,CAAC,CAAC;IAChD,OAAO,CAAC,GAAG,CAAC,OAAO,WAAW,YAAY,CAAC,CAAC;IAC5C,OAAO,CAAC,GAAG,CAAC,OAAO,cAAc,eAAe,CAAC,CAAC;IAClD,OAAO,CAAC,GAAG,CAAC,OAAO,WAAW,kBAAkB,CAAC,CAAC;IAElD,OAAO,CAAC,GAAG,CAAC,cAAc,CAAC,CAAC;IAC5B,OAAO,CAAC,GAAG,CAAC,OAAO,SAAS,WAAW,SAAS,WAAW,WAAW,YAAY,CAAC,CAAC;IAEpF,IAAI,QAAQ,CAAC,OAAO,CAAC,gBAAgB,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACjD,OAAO,CAAC,GAAG,CAAC,OAAO,QAAQ,CAAC,OAAO,CAAC,gBAAgB,CAAC,MAAM,uBAAuB,CAAC,CAAC;IACtF,CAAC;IAED,IAAI,QAAQ,CAAC,SAAS,EAAE,CAAC;QACvB,OAAO,CAAC,GAAG,CAAC,iBAAiB,CAAC,CAAC;QAC/B,OAAO,CAAC,GAAG,CAAC,OAAO,QAAQ,CAAC,SAAS,CAAC,cAAc,cAAc,QAAQ,CAAC,SAAS,CAAC,eAAe,YAAY,CAAC,CAAC;QAClH,OAAO,CAAC,GAAG,CAAC,aAAa,QAAQ,CAAC,SAAS,CAAC,YAAY,EAAE,CAAC,CAAC;IAC9D,CAAC;IAED,OAAO,CAAC,GAAG,CAAC,4BAA4B,YAAY,EAAE,CAAC,CAAC;IACxD,OAAO,CAAC,GAAG,CAAC,iBAAiB,OAAO,MAAM,CAAC,CAAC;IAE5C,IAAI,KAAK,CAAC,OAAO,EAAE,CAAC;QAClB,qBAAqB;QACrB,OAAO,CAAC,GAAG,CAAC,WAAW,CAAC,CAAC;QACzB,KAAK,MAAM,KAAK,IAAI,MAAM,CAAC,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC,EAAE,CAAC;YACnD,MAAM,IAAI,GAAG,KAAK,CAAC,eAAe,CAAC;YACnC,MAAM,IAAI,GAAG,CAAC,KAAK,CAAC,MAAM,EAAE,KAAK,CAAC,KAAK,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;YACpE,OAAO,CAAC,GAAG,CAAC,OAAO,KAAK,CAAC,IAAI,GAAG,IAAI,CAAC,CAAC,CAAC,KAAK,IAAI,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;YAC5D,IAAI,IAAI,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;gBACpB,OAAO,CAAC,GAAG,CAAC,qBAAqB,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;YACtD,CAAC;QACH,CAAC;QAED,IAAI,MAAM,CAAC,IAAI,CAAC,QAAQ,CAAC,SAAS,CAAC,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YAC/C,OAAO,CAAC,GAAG,CAAC,gBAAgB,CAAC,CAAC;YAC9B,KAAK,MAAM,EAAE,IAAI,MAAM,CAAC,MAAM,CAAC,QAAQ,CAAC,SAAS,CAAC,EAAE,CAAC;gBACnD,OAAO,CAAC,GAAG,CAAC,OAAO,EAAE,CAAC,KAAK,KAAK,EAAE,CAAC,KAAK,CAAC,MAAM,aAAa,EAAE,CAAC,QAAQ,EAAE,CAAC,CAAC;YAC7E,CAAC;QACH,CAAC;QAED,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;IAClB,CAAC;AACH,CAAC"}
|
|
@@ -6,11 +6,14 @@
|
|
|
6
6
|
*
|
|
7
7
|
* Usage:
|
|
8
8
|
* dql lineage [path] Show full lineage graph summary
|
|
9
|
-
* dql lineage <
|
|
9
|
+
* dql lineage <name> [path] Show upstream/downstream for a block, table, or metric
|
|
10
|
+
* dql lineage --table <name> [path] Show lineage for a specific source table
|
|
11
|
+
* dql lineage --metric <name> [path] Show lineage for a specific metric
|
|
10
12
|
* dql lineage --domain <name> [path] Show lineage within a domain
|
|
11
|
-
* dql lineage --impact <
|
|
13
|
+
* dql lineage --impact <name> [path] Impact analysis: what breaks if this node changes?
|
|
12
14
|
* dql lineage --trust-chain <from> <to> Show trust chain between two blocks
|
|
13
15
|
* dql lineage --export [path] Export lineage as JSON
|
|
16
|
+
* dql lineage --no-manifest Force live scan (skip dql-manifest.json)
|
|
14
17
|
*/
|
|
15
18
|
import type { CLIFlags } from '../args.js';
|
|
16
19
|
export declare function runLineage(blockNameOrPath: string | null, rest: string[], flags: CLIFlags): Promise<void>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"lineage.d.ts","sourceRoot":"","sources":["../../src/commands/lineage.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"lineage.d.ts","sourceRoot":"","sources":["../../src/commands/lineage.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAkBH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAE3C,wBAAsB,UAAU,CAC9B,eAAe,EAAE,MAAM,GAAG,IAAI,EAC9B,IAAI,EAAE,MAAM,EAAE,EACd,KAAK,EAAE,QAAQ,GACd,OAAO,CAAC,IAAI,CAAC,CA4Ff"}
|
package/dist/commands/lineage.js
CHANGED
|
@@ -6,19 +6,29 @@
|
|
|
6
6
|
*
|
|
7
7
|
* Usage:
|
|
8
8
|
* dql lineage [path] Show full lineage graph summary
|
|
9
|
-
* dql lineage <
|
|
9
|
+
* dql lineage <name> [path] Show upstream/downstream for a block, table, or metric
|
|
10
|
+
* dql lineage --table <name> [path] Show lineage for a specific source table
|
|
11
|
+
* dql lineage --metric <name> [path] Show lineage for a specific metric
|
|
10
12
|
* dql lineage --domain <name> [path] Show lineage within a domain
|
|
11
|
-
* dql lineage --impact <
|
|
13
|
+
* dql lineage --impact <name> [path] Impact analysis: what breaks if this node changes?
|
|
12
14
|
* dql lineage --trust-chain <from> <to> Show trust chain between two blocks
|
|
13
15
|
* dql lineage --export [path] Export lineage as JSON
|
|
16
|
+
* dql lineage --no-manifest Force live scan (skip dql-manifest.json)
|
|
14
17
|
*/
|
|
15
18
|
import { readdirSync, readFileSync, existsSync } from 'node:fs';
|
|
16
19
|
import { join, extname, resolve } from 'node:path';
|
|
17
|
-
import { Parser, loadSemanticLayerFromDir, buildLineageGraph, analyzeImpact, buildTrustChain, detectDomainFlows, getDomainTrustOverview, } from '@duckcodeailabs/dql-core';
|
|
20
|
+
import { Parser, loadSemanticLayerFromDir, buildLineageGraph, analyzeImpact, buildTrustChain, detectDomainFlows, getDomainTrustOverview, LineageGraph, } from '@duckcodeailabs/dql-core';
|
|
18
21
|
export async function runLineage(blockNameOrPath, rest, flags) {
|
|
22
|
+
// Collect all args and separate flags from positional args
|
|
23
|
+
const allArgs = [...(blockNameOrPath ? [blockNameOrPath] : []), ...rest];
|
|
24
|
+
const noManifest = allArgs.includes('--no-manifest');
|
|
25
|
+
const positionalArgs = allArgs.filter((a) => !a.startsWith('-'));
|
|
26
|
+
const flagArgs = allArgs.filter((a) => a.startsWith('-'));
|
|
27
|
+
// Re-derive blockNameOrPath from the first non-flag positional arg
|
|
28
|
+
const effectiveBlockArg = positionalArgs[0] ?? null;
|
|
19
29
|
// Determine project root — check for dql.config.json
|
|
20
|
-
const candidateRoot = resolve(
|
|
21
|
-
??
|
|
30
|
+
const candidateRoot = resolve(positionalArgs.find((r) => existsSync(join(resolve(r), 'dql.config.json')))
|
|
31
|
+
?? '.');
|
|
22
32
|
const projectRoot = existsSync(join(candidateRoot, 'dql.config.json'))
|
|
23
33
|
? candidateRoot
|
|
24
34
|
: resolve('.');
|
|
@@ -27,39 +37,83 @@ export async function runLineage(blockNameOrPath, rest, flags) {
|
|
|
27
37
|
process.exitCode = 1;
|
|
28
38
|
return;
|
|
29
39
|
}
|
|
30
|
-
// Build the lineage graph
|
|
31
|
-
const graph =
|
|
40
|
+
// Build the lineage graph — prefer manifest if available
|
|
41
|
+
const graph = noManifest
|
|
42
|
+
? buildProjectLineage(projectRoot)
|
|
43
|
+
: loadFromManifestOrScan(projectRoot);
|
|
32
44
|
if (graph.nodeCount === 0) {
|
|
33
45
|
console.log('\n No lineage data found.');
|
|
34
|
-
console.log('
|
|
46
|
+
console.log(' Run `dql compile` to generate the project manifest, or add blocks in blocks/.\n');
|
|
35
47
|
return;
|
|
36
48
|
}
|
|
37
49
|
// Route to the right subcommand based on flags
|
|
38
|
-
if (flags.format === 'json' ||
|
|
50
|
+
if (flags.format === 'json' || allArgs.includes('--export')) {
|
|
39
51
|
console.log(JSON.stringify(graph.toJSON(), null, 2));
|
|
40
52
|
return;
|
|
41
53
|
}
|
|
42
|
-
// --impact <
|
|
43
|
-
const impactIdx =
|
|
44
|
-
if (impactIdx >= 0 &&
|
|
45
|
-
|
|
54
|
+
// --impact <name>
|
|
55
|
+
const impactIdx = allArgs.indexOf('--impact');
|
|
56
|
+
if (impactIdx >= 0 && allArgs[impactIdx + 1]) {
|
|
57
|
+
const nodeId = resolveNodeId(graph, allArgs[impactIdx + 1]);
|
|
58
|
+
if (!nodeId) {
|
|
59
|
+
console.error(`"${allArgs[impactIdx + 1]}" not found in lineage graph.`);
|
|
60
|
+
process.exitCode = 1;
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
return printImpactAnalysis(graph, nodeId, flags);
|
|
46
64
|
}
|
|
47
65
|
// --trust-chain <from> <to>
|
|
48
|
-
const trustIdx =
|
|
49
|
-
if (trustIdx >= 0 &&
|
|
50
|
-
return printTrustChain(graph,
|
|
66
|
+
const trustIdx = allArgs.indexOf('--trust-chain');
|
|
67
|
+
if (trustIdx >= 0 && allArgs[trustIdx + 1] && allArgs[trustIdx + 2]) {
|
|
68
|
+
return printTrustChain(graph, allArgs[trustIdx + 1], allArgs[trustIdx + 2], flags);
|
|
51
69
|
}
|
|
52
70
|
// --domain <name>
|
|
53
71
|
if (flags.domain) {
|
|
54
72
|
return printDomainLineage(graph, flags.domain, flags);
|
|
55
73
|
}
|
|
56
|
-
//
|
|
57
|
-
|
|
58
|
-
|
|
74
|
+
// --table <name>
|
|
75
|
+
const tableIdx = allArgs.indexOf('--table');
|
|
76
|
+
if (tableIdx >= 0 && allArgs[tableIdx + 1]) {
|
|
77
|
+
return printNodeLineage(graph, `table:${allArgs[tableIdx + 1]}`, flags);
|
|
78
|
+
}
|
|
79
|
+
// --metric <name>
|
|
80
|
+
const metricIdx = allArgs.indexOf('--metric');
|
|
81
|
+
if (metricIdx >= 0 && allArgs[metricIdx + 1]) {
|
|
82
|
+
return printNodeLineage(graph, `metric:${allArgs[metricIdx + 1]}`, flags);
|
|
83
|
+
}
|
|
84
|
+
// Specific node — smart lookup: try block, then table, then metric
|
|
85
|
+
if (effectiveBlockArg && resolve(effectiveBlockArg) !== projectRoot) {
|
|
86
|
+
const nodeId = resolveNodeId(graph, effectiveBlockArg);
|
|
87
|
+
if (!nodeId) {
|
|
88
|
+
console.error(`"${effectiveBlockArg}" not found in lineage graph.`);
|
|
89
|
+
console.error(' Hint: use --table <name> or --metric <name> for explicit lookup.');
|
|
90
|
+
process.exitCode = 1;
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
return printNodeLineage(graph, nodeId, flags);
|
|
59
94
|
}
|
|
60
95
|
// Default: show full summary
|
|
61
96
|
return printSummary(graph, flags);
|
|
62
97
|
}
|
|
98
|
+
/** Load lineage from dql-manifest.json if available, otherwise scan live. */
|
|
99
|
+
function loadFromManifestOrScan(projectRoot) {
|
|
100
|
+
const manifestPath = join(projectRoot, 'dql-manifest.json');
|
|
101
|
+
if (existsSync(manifestPath)) {
|
|
102
|
+
try {
|
|
103
|
+
const manifest = JSON.parse(readFileSync(manifestPath, 'utf-8'));
|
|
104
|
+
if (manifest.lineage?.nodes && manifest.lineage?.edges) {
|
|
105
|
+
return LineageGraph.fromJSON({
|
|
106
|
+
nodes: manifest.lineage.nodes,
|
|
107
|
+
edges: manifest.lineage.edges,
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
catch {
|
|
112
|
+
// Fall through to live scan
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
return buildProjectLineage(projectRoot);
|
|
116
|
+
}
|
|
63
117
|
/** Discover all blocks and semantic layer definitions and build the lineage graph. */
|
|
64
118
|
function buildProjectLineage(projectRoot) {
|
|
65
119
|
const blocks = [];
|
|
@@ -158,18 +212,117 @@ function printSummary(graph, _flags) {
|
|
|
158
212
|
const nodes = graph.getAllNodes();
|
|
159
213
|
const edges = graph.getAllEdges();
|
|
160
214
|
const domains = graph.getDomains();
|
|
215
|
+
const sourceTables = graph.getNodesByType('source_table');
|
|
216
|
+
const blocks = graph.getNodesByType('block');
|
|
217
|
+
const metrics = graph.getNodesByType('metric');
|
|
218
|
+
const dimensions = graph.getNodesByType('dimension');
|
|
219
|
+
const charts = graph.getNodesByType('chart');
|
|
161
220
|
console.log('\n DQL Lineage Summary');
|
|
162
|
-
console.log(' ' + '='.repeat(
|
|
163
|
-
//
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
221
|
+
console.log(' ' + '='.repeat(50));
|
|
222
|
+
// Overview counts
|
|
223
|
+
console.log(`\n ${nodes.length} nodes, ${edges.length} edges, ${domains.length} domain(s)`);
|
|
224
|
+
// Source Tables
|
|
225
|
+
if (sourceTables.length > 0) {
|
|
226
|
+
console.log(`\n Source Tables (${sourceTables.length}):`);
|
|
227
|
+
for (const t of sourceTables.sort((a, b) => a.name.localeCompare(b.name))) {
|
|
228
|
+
const downstream = graph.getOutgoingEdges(t.id);
|
|
229
|
+
const targets = [...new Set(downstream.map((e) => graph.getNode(e.target)?.name).filter(Boolean))];
|
|
230
|
+
const arrow = targets.length > 0 ? ` -> ${targets.join(', ')}` : '';
|
|
231
|
+
console.log(` ${t.name}${arrow}`);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
// Blocks
|
|
235
|
+
if (blocks.length > 0) {
|
|
236
|
+
console.log(`\n Blocks (${blocks.length}):`);
|
|
237
|
+
for (const b of blocks.sort((a, b) => a.name.localeCompare(b.name))) {
|
|
238
|
+
const parts = [];
|
|
239
|
+
if (b.domain)
|
|
240
|
+
parts.push(`domain: ${b.domain}`);
|
|
241
|
+
if (b.owner)
|
|
242
|
+
parts.push(`owner: ${b.owner}`);
|
|
243
|
+
if (b.status)
|
|
244
|
+
parts.push(b.status);
|
|
245
|
+
const meta = parts.length > 0 ? ` (${parts.join(', ')})` : '';
|
|
246
|
+
console.log(` ${b.name}${meta}`);
|
|
247
|
+
// Show what this block reads from (upstream) — deduplicate by node id
|
|
248
|
+
const incoming = graph.getIncomingEdges(b.id);
|
|
249
|
+
const upstreamNames = [...new Set(incoming
|
|
250
|
+
.map((e) => graph.getNode(e.source))
|
|
251
|
+
.filter((n) => n !== undefined && n.type !== 'domain')
|
|
252
|
+
.map((n) => n.name))];
|
|
253
|
+
if (upstreamNames.length > 0) {
|
|
254
|
+
console.log(` reads from: ${upstreamNames.join(', ')}`);
|
|
255
|
+
}
|
|
256
|
+
// Show what this block feeds into (downstream) — deduplicate by node id
|
|
257
|
+
const outgoing = graph.getOutgoingEdges(b.id);
|
|
258
|
+
const downstreamNames = [...new Set(outgoing
|
|
259
|
+
.map((e) => graph.getNode(e.target))
|
|
260
|
+
.filter((n) => n !== undefined && n.type !== 'domain')
|
|
261
|
+
.map((n) => n.name))];
|
|
262
|
+
if (downstreamNames.length > 0) {
|
|
263
|
+
console.log(` feeds into: ${downstreamNames.join(', ')}`);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
167
266
|
}
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
console.log(
|
|
267
|
+
// Metrics
|
|
268
|
+
if (metrics.length > 0) {
|
|
269
|
+
console.log(`\n Metrics (${metrics.length}):`);
|
|
270
|
+
for (const m of metrics.sort((a, b) => a.name.localeCompare(b.name))) {
|
|
271
|
+
const incoming = graph.getIncomingEdges(m.id);
|
|
272
|
+
const sources = incoming.map((e) => graph.getNode(e.source)?.name).filter(Boolean);
|
|
273
|
+
const from = sources.length > 0 ? ` <- ${sources.join(', ')}` : '';
|
|
274
|
+
const domain = m.domain ? ` (${m.domain})` : '';
|
|
275
|
+
console.log(` ${m.name}${domain}${from}`);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
// Dimensions
|
|
279
|
+
if (dimensions.length > 0) {
|
|
280
|
+
console.log(`\n Dimensions (${dimensions.length}):`);
|
|
281
|
+
for (const d of dimensions.sort((a, b) => a.name.localeCompare(b.name))) {
|
|
282
|
+
console.log(` ${d.name}`);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
// Charts
|
|
286
|
+
if (charts.length > 0) {
|
|
287
|
+
console.log(`\n Charts (${charts.length}):`);
|
|
288
|
+
for (const c of charts.sort((a, b) => a.name.localeCompare(b.name))) {
|
|
289
|
+
const incoming = graph.getIncomingEdges(c.id);
|
|
290
|
+
const sources = incoming.map((e) => graph.getNode(e.source)?.name).filter(Boolean);
|
|
291
|
+
const from = sources.length > 0 ? ` <- ${sources.join(', ')}` : '';
|
|
292
|
+
console.log(` ${c.name}${from}`);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
// Shared Table Correlation — blocks reading the same tables
|
|
296
|
+
const tableReaders = new Map();
|
|
297
|
+
for (const t of sourceTables) {
|
|
298
|
+
const downstream = graph.getOutgoingEdges(t.id)
|
|
299
|
+
.map((e) => graph.getNode(e.target))
|
|
300
|
+
.filter((n) => n !== undefined && n.type === 'block');
|
|
301
|
+
if (downstream.length >= 2) {
|
|
302
|
+
tableReaders.set(t.name, downstream.map((n) => n.name));
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
if (tableReaders.size > 0) {
|
|
306
|
+
console.log(`\n Shared Tables (${tableReaders.size}):`);
|
|
307
|
+
for (const [table, readers] of [...tableReaders.entries()].sort()) {
|
|
308
|
+
console.log(` ${table} <- ${readers.join(', ')}`);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
// Data Flow DAG
|
|
312
|
+
console.log('\n Data Flow:');
|
|
313
|
+
console.log(' ' + '-'.repeat(50));
|
|
314
|
+
// Find root nodes (no incoming edges, excluding domain nodes)
|
|
315
|
+
const roots = nodes.filter((n) => n.type !== 'domain' && (graph.getIncomingEdges(n.id).length === 0));
|
|
316
|
+
const printed = new Set();
|
|
317
|
+
for (const root of roots.sort((a, b) => a.name.localeCompare(b.name))) {
|
|
318
|
+
printDAGNode(graph, root, 0, printed);
|
|
319
|
+
}
|
|
320
|
+
// Print any remaining nodes not reachable from roots (cycles or disconnected)
|
|
321
|
+
for (const node of nodes) {
|
|
322
|
+
if (!printed.has(node.id) && node.type !== 'domain') {
|
|
323
|
+
printDAGNode(graph, node, 0, printed);
|
|
324
|
+
}
|
|
171
325
|
}
|
|
172
|
-
console.log(`\n Edges: ${edges.length}`);
|
|
173
326
|
// Cross-domain flows
|
|
174
327
|
const flows = detectDomainFlows(graph);
|
|
175
328
|
if (flows.length > 0) {
|
|
@@ -191,38 +344,127 @@ function printSummary(graph, _flags) {
|
|
|
191
344
|
}
|
|
192
345
|
console.log('');
|
|
193
346
|
}
|
|
194
|
-
|
|
195
|
-
|
|
347
|
+
/** Recursively print DAG tree from a node. */
|
|
348
|
+
function printDAGNode(graph, node, depth, printed) {
|
|
349
|
+
if (printed.has(node.id))
|
|
350
|
+
return;
|
|
351
|
+
printed.add(node.id);
|
|
352
|
+
const indent = ' ' + ' '.repeat(depth);
|
|
353
|
+
const prefix = depth === 0 ? '' : '└── ';
|
|
354
|
+
const typeLabel = node.type === 'source_table' ? 'table' : node.type;
|
|
355
|
+
const badge = node.status === 'certified' ? ' ✓' : '';
|
|
356
|
+
const domain = node.domain ? ` [${node.domain}]` : '';
|
|
357
|
+
console.log(`${indent}${prefix}${typeLabel}:${node.name}${domain}${badge}`);
|
|
358
|
+
// Get non-domain downstream
|
|
359
|
+
const outgoing = graph.getOutgoingEdges(node.id);
|
|
360
|
+
const children = outgoing
|
|
361
|
+
.map((e) => graph.getNode(e.target))
|
|
362
|
+
.filter((n) => n !== undefined && n.type !== 'domain' && !printed.has(n.id));
|
|
363
|
+
for (const child of children) {
|
|
364
|
+
printDAGNode(graph, child, depth + 1, printed);
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
/**
|
|
368
|
+
* Resolve a user-provided name to a node ID by trying multiple type prefixes.
|
|
369
|
+
* Priority: block > source_table > metric > dimension > chart > exact match.
|
|
370
|
+
*/
|
|
371
|
+
function resolveNodeId(graph, name) {
|
|
372
|
+
// If the name already contains a colon, try it as-is
|
|
373
|
+
if (name.includes(':') && graph.getNode(name))
|
|
374
|
+
return name;
|
|
375
|
+
// Try common type prefixes in priority order
|
|
376
|
+
const prefixes = ['block', 'table', 'metric', 'dimension', 'chart', 'domain'];
|
|
377
|
+
for (const prefix of prefixes) {
|
|
378
|
+
const id = `${prefix}:${name}`;
|
|
379
|
+
if (graph.getNode(id))
|
|
380
|
+
return id;
|
|
381
|
+
}
|
|
382
|
+
// Fuzzy match: search all nodes for a name match
|
|
383
|
+
for (const node of graph.getAllNodes()) {
|
|
384
|
+
if (node.name === name)
|
|
385
|
+
return node.id;
|
|
386
|
+
}
|
|
387
|
+
return null;
|
|
388
|
+
}
|
|
389
|
+
function printNodeLineage(graph, nodeId, _flags) {
|
|
196
390
|
const node = graph.getNode(nodeId);
|
|
197
391
|
if (!node) {
|
|
198
|
-
console.error(`
|
|
392
|
+
console.error(`Node "${nodeId}" not found in lineage graph.`);
|
|
199
393
|
process.exitCode = 1;
|
|
200
394
|
return;
|
|
201
395
|
}
|
|
396
|
+
const typeLabel = node.type === 'source_table' ? 'Table' : node.type.charAt(0).toUpperCase() + node.type.slice(1);
|
|
202
397
|
const ancestors = graph.ancestors(nodeId);
|
|
203
398
|
const descendants = graph.descendants(nodeId);
|
|
204
|
-
console.log(`\n Lineage
|
|
205
|
-
console.log(' ' + '='.repeat(
|
|
399
|
+
console.log(`\n ${typeLabel} Lineage: ${node.name}`);
|
|
400
|
+
console.log(' ' + '='.repeat(50));
|
|
401
|
+
// Metadata
|
|
402
|
+
const meta = [];
|
|
403
|
+
if (node.type !== 'source_table')
|
|
404
|
+
meta.push(`Type: ${node.type}`);
|
|
206
405
|
if (node.domain)
|
|
207
|
-
|
|
406
|
+
meta.push(`Domain: ${node.domain}`);
|
|
208
407
|
if (node.status)
|
|
209
|
-
|
|
408
|
+
meta.push(`Status: ${node.status}`);
|
|
210
409
|
if (node.owner)
|
|
211
|
-
|
|
212
|
-
if (
|
|
213
|
-
|
|
214
|
-
|
|
410
|
+
meta.push(`Owner: ${node.owner}`);
|
|
411
|
+
if (meta.length > 0) {
|
|
412
|
+
for (const m of meta)
|
|
413
|
+
console.log(` ${m}`);
|
|
414
|
+
}
|
|
415
|
+
// Direct connections (immediate neighbors)
|
|
416
|
+
const inEdges = graph.getIncomingEdges(nodeId);
|
|
417
|
+
const outEdges = graph.getOutgoingEdges(nodeId);
|
|
418
|
+
const directUpstream = [...new Set(inEdges
|
|
419
|
+
.map((e) => graph.getNode(e.source))
|
|
420
|
+
.filter((n) => n !== undefined && n.type !== 'domain'))];
|
|
421
|
+
const directDownstream = [...new Set(outEdges
|
|
422
|
+
.map((e) => graph.getNode(e.target))
|
|
423
|
+
.filter((n) => n !== undefined && n.type !== 'domain'))];
|
|
424
|
+
if (directUpstream.length > 0) {
|
|
425
|
+
console.log(`\n Direct Upstream (${directUpstream.length}):`);
|
|
426
|
+
for (const n of directUpstream) {
|
|
427
|
+
const badge = n.status === 'certified' ? ' [certified]' : '';
|
|
428
|
+
const typePfx = n.type === 'source_table' ? 'table' : n.type;
|
|
429
|
+
console.log(` ${typePfx}:${n.name}${badge}${n.domain ? ` (${n.domain})` : ''}`);
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
if (directDownstream.length > 0) {
|
|
433
|
+
console.log(`\n Direct Downstream (${directDownstream.length}):`);
|
|
434
|
+
for (const n of directDownstream) {
|
|
435
|
+
const badge = n.status === 'certified' ? ' [certified]' : '';
|
|
436
|
+
const typePfx = n.type === 'source_table' ? 'table' : n.type;
|
|
437
|
+
console.log(` ${typePfx}:${n.name}${badge}${n.domain ? ` (${n.domain})` : ''}`);
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
// Full transitive upstream/downstream (if different from direct)
|
|
441
|
+
const transitiveUp = ancestors.filter((n) => n.type !== 'domain');
|
|
442
|
+
const transitiveDown = descendants.filter((n) => n.type !== 'domain');
|
|
443
|
+
if (transitiveUp.length > directUpstream.length) {
|
|
444
|
+
console.log(`\n All Upstream (${transitiveUp.length}):`);
|
|
445
|
+
for (const a of transitiveUp) {
|
|
215
446
|
const badge = a.status === 'certified' ? ' [certified]' : '';
|
|
216
|
-
|
|
447
|
+
const typePfx = a.type === 'source_table' ? 'table' : a.type;
|
|
448
|
+
const direct = directUpstream.some((d) => d.id === a.id) ? ' *' : '';
|
|
449
|
+
console.log(` ${typePfx}:${a.name}${badge}${a.domain ? ` (${a.domain})` : ''}${direct}`);
|
|
217
450
|
}
|
|
218
451
|
}
|
|
219
|
-
if (
|
|
220
|
-
console.log(`\n Downstream (${
|
|
221
|
-
for (const d of
|
|
452
|
+
if (transitiveDown.length > directDownstream.length) {
|
|
453
|
+
console.log(`\n All Downstream (${transitiveDown.length}):`);
|
|
454
|
+
for (const d of transitiveDown) {
|
|
222
455
|
const badge = d.status === 'certified' ? ' [certified]' : '';
|
|
223
|
-
|
|
456
|
+
const typePfx = d.type === 'source_table' ? 'table' : d.type;
|
|
457
|
+
const direct = directDownstream.some((dd) => dd.id === d.id) ? ' *' : '';
|
|
458
|
+
console.log(` ${typePfx}:${d.name}${badge}${d.domain ? ` (${d.domain})` : ''}${direct}`);
|
|
224
459
|
}
|
|
225
460
|
}
|
|
461
|
+
// Data flow tree from this node
|
|
462
|
+
if (transitiveDown.length > 0) {
|
|
463
|
+
console.log('\n Data Flow:');
|
|
464
|
+
console.log(' ' + '-'.repeat(50));
|
|
465
|
+
const printed = new Set();
|
|
466
|
+
printDAGNode(graph, node, 0, printed);
|
|
467
|
+
}
|
|
226
468
|
console.log('');
|
|
227
469
|
}
|
|
228
470
|
function printDomainLineage(graph, domain, _flags) {
|
|
@@ -255,15 +497,15 @@ function printDomainLineage(graph, domain, _flags) {
|
|
|
255
497
|
}
|
|
256
498
|
console.log('');
|
|
257
499
|
}
|
|
258
|
-
function printImpactAnalysis(graph,
|
|
259
|
-
const
|
|
260
|
-
if (!
|
|
261
|
-
console.error(`
|
|
500
|
+
function printImpactAnalysis(graph, nodeId, _flags) {
|
|
501
|
+
const node = graph.getNode(nodeId);
|
|
502
|
+
if (!node) {
|
|
503
|
+
console.error(`"${nodeId}" not found.`);
|
|
262
504
|
process.exitCode = 1;
|
|
263
505
|
return;
|
|
264
506
|
}
|
|
265
507
|
const impact = analyzeImpact(graph, nodeId);
|
|
266
|
-
console.log(`\n Impact Analysis: ${
|
|
508
|
+
console.log(`\n Impact Analysis: ${node.name}`);
|
|
267
509
|
console.log(' ' + '='.repeat(40));
|
|
268
510
|
console.log(`\n Total downstream affected: ${impact.totalAffected}`);
|
|
269
511
|
if (impact.domainImpacts.length > 0) {
|