@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.
@@ -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-B_X7pyPz.js"></script>
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":"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"}
@@ -6,11 +6,14 @@
6
6
  *
7
7
  * Usage:
8
8
  * dql lineage [path] Show full lineage graph summary
9
- * dql lineage <block> [path] Show upstream/downstream for a block
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 <block> [path] Impact analysis: what breaks if this changes?
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;;;;;;;;;;;;;GAaG;AAiBH,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,CAwDf"}
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"}
@@ -6,19 +6,29 @@
6
6
  *
7
7
  * Usage:
8
8
  * dql lineage [path] Show full lineage graph summary
9
- * dql lineage <block> [path] Show upstream/downstream for a block
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 <block> [path] Impact analysis: what breaks if this changes?
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(rest.find((r) => !r.startsWith('-') && existsSync(join(resolve(r), 'dql.config.json')))
21
- ?? (blockNameOrPath && existsSync(join(resolve(blockNameOrPath), 'dql.config.json')) ? blockNameOrPath : '.'));
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 = buildProjectLineage(projectRoot);
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(' Add blocks in blocks/ and semantic definitions in semantic-layer/ to see lineage.\n');
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' || rest.includes('--export')) {
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 <block>
43
- const impactIdx = rest.indexOf('--impact');
44
- if (impactIdx >= 0 && rest[impactIdx + 1]) {
45
- return printImpactAnalysis(graph, rest[impactIdx + 1], flags);
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 = rest.indexOf('--trust-chain');
49
- if (trustIdx >= 0 && rest[trustIdx + 1] && rest[trustIdx + 2]) {
50
- return printTrustChain(graph, rest[trustIdx + 1], rest[trustIdx + 2], flags);
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
- // Specific block — treat first arg as a block name if it's not the project root
57
- if (blockNameOrPath && resolve(blockNameOrPath) !== projectRoot) {
58
- return printBlockLineage(graph, blockNameOrPath, flags);
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(40));
163
- // Node counts by type
164
- const typeCount = {};
165
- for (const node of nodes) {
166
- typeCount[node.type] = (typeCount[node.type] ?? 0) + 1;
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
- console.log('\n Nodes:');
169
- for (const [type, count] of Object.entries(typeCount).sort()) {
170
- console.log(` ${type}: ${count}`);
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
- function printBlockLineage(graph, blockName, _flags) {
195
- const nodeId = `block:${blockName}`;
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(`Block "${blockName}" not found in lineage graph.`);
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 for: ${blockName}`);
205
- console.log(' ' + '='.repeat(40));
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
- console.log(` Domain: ${node.domain}`);
406
+ meta.push(`Domain: ${node.domain}`);
208
407
  if (node.status)
209
- console.log(` Status: ${node.status}`);
408
+ meta.push(`Status: ${node.status}`);
210
409
  if (node.owner)
211
- console.log(` Owner: ${node.owner}`);
212
- if (ancestors.length > 0) {
213
- console.log(`\n Upstream (${ancestors.length}):`);
214
- for (const a of ancestors) {
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
- console.log(` ${a.type}:${a.name}${badge}${a.domain ? ` (${a.domain})` : ''}`);
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 (descendants.length > 0) {
220
- console.log(`\n Downstream (${descendants.length}):`);
221
- for (const d of descendants) {
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
- console.log(` ${d.type}:${d.name}${badge}${d.domain ? ` (${d.domain})` : ''}`);
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, blockName, _flags) {
259
- const nodeId = `block:${blockName}`;
260
- if (!graph.getNode(nodeId)) {
261
- console.error(`Block "${blockName}" not found.`);
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: ${blockName}`);
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) {