@duckcodeailabs/dql-cli 0.8.9 → 0.8.11

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-Cp34wXvX.js"></script>
10
+ <script type="module" crossorigin src="/assets/index-Cxj__xjY.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
  <link rel="stylesheet" crossorigin href="/assets/index-BZV40eAE.css">
@@ -7,15 +7,15 @@
7
7
  * Usage:
8
8
  * dql lineage [path] Show full lineage graph summary
9
9
  * dql lineage <name> [path] Show upstream/downstream for a block, table, or metric
10
- * dql lineage --search <term> Search all nodes by name
11
- * dql lineage --focus <name> [--depth N] Focused subgraph centered on a node
12
- * dql lineage --dashboard <name> Show what feeds a dashboard/notebook
13
- * dql lineage --dbt Show the dbt model DAG portion
14
10
  * dql lineage --table <name> [path] Show lineage for a specific source table
15
11
  * dql lineage --metric <name> [path] Show lineage for a specific metric
16
12
  * dql lineage --domain <name> [path] Show lineage within a domain
17
13
  * dql lineage --impact <name> [path] Impact analysis: what breaks if this node changes?
18
14
  * dql lineage --trust-chain <from> <to> Show trust chain between two blocks
15
+ * dql lineage --search <term> [path] Search lineage nodes by name
16
+ * dql lineage --focus <name> [path] Show a focused lineage subgraph
17
+ * dql lineage --dashboard <name> [path] Show lineage for a dashboard/notebook
18
+ * dql lineage --dbt [path] Show the dbt portion of lineage
19
19
  * dql lineage --export [path] Export lineage as JSON
20
20
  * dql lineage --no-manifest Force live scan (skip dql-manifest.json)
21
21
  */
@@ -1 +1 @@
1
- {"version":3,"file":"lineage.d.ts","sourceRoot":"","sources":["../../src/commands/lineage.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAoBH,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,CAsHf"}
1
+ {"version":3,"file":"lineage.d.ts","sourceRoot":"","sources":["../../src/commands/lineage.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAoBH,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,CAiHf"}
@@ -7,21 +7,21 @@
7
7
  * Usage:
8
8
  * dql lineage [path] Show full lineage graph summary
9
9
  * dql lineage <name> [path] Show upstream/downstream for a block, table, or metric
10
- * dql lineage --search <term> Search all nodes by name
11
- * dql lineage --focus <name> [--depth N] Focused subgraph centered on a node
12
- * dql lineage --dashboard <name> Show what feeds a dashboard/notebook
13
- * dql lineage --dbt Show the dbt model DAG portion
14
10
  * dql lineage --table <name> [path] Show lineage for a specific source table
15
11
  * dql lineage --metric <name> [path] Show lineage for a specific metric
16
12
  * dql lineage --domain <name> [path] Show lineage within a domain
17
13
  * dql lineage --impact <name> [path] Impact analysis: what breaks if this node changes?
18
14
  * dql lineage --trust-chain <from> <to> Show trust chain between two blocks
15
+ * dql lineage --search <term> [path] Search lineage nodes by name
16
+ * dql lineage --focus <name> [path] Show a focused lineage subgraph
17
+ * dql lineage --dashboard <name> [path] Show lineage for a dashboard/notebook
18
+ * dql lineage --dbt [path] Show the dbt portion of lineage
19
19
  * dql lineage --export [path] Export lineage as JSON
20
20
  * dql lineage --no-manifest Force live scan (skip dql-manifest.json)
21
21
  */
22
22
  import { readdirSync, readFileSync, existsSync } from 'node:fs';
23
23
  import { join, extname, resolve } from 'node:path';
24
- import { Parser, loadSemanticLayerFromDir, buildLineageGraph, analyzeImpact, buildTrustChain, detectDomainFlows, getDomainTrustOverview, queryLineage, searchNodes, LineageGraph, } from '@duckcodeailabs/dql-core';
24
+ import { Parser, loadSemanticLayerFromDir, buildLineageGraph, buildManifest, analyzeImpact, buildTrustChain, detectDomainFlows, getDomainTrustOverview, LineageGraph, queryLineage, } from '@duckcodeailabs/dql-core';
25
25
  export async function runLineage(blockNameOrPath, rest, flags) {
26
26
  // Collect all args and separate flags from positional args
27
27
  const allArgs = [...(blockNameOrPath ? [blockNameOrPath] : []), ...rest];
@@ -55,28 +55,6 @@ export async function runLineage(blockNameOrPath, rest, flags) {
55
55
  console.log(JSON.stringify(graph.toJSON(), null, 2));
56
56
  return;
57
57
  }
58
- // --search <term>
59
- const searchIdx = allArgs.indexOf('--search');
60
- if (searchIdx >= 0 && allArgs[searchIdx + 1]) {
61
- return printSearch(graph, allArgs[searchIdx + 1]);
62
- }
63
- // --focus <name> [--depth N]
64
- const focusIdx = allArgs.indexOf('--focus');
65
- if (focusIdx >= 0 && allArgs[focusIdx + 1]) {
66
- const depthIdx = allArgs.indexOf('--depth');
67
- const depth = depthIdx >= 0 && allArgs[depthIdx + 1] ? Number(allArgs[depthIdx + 1]) : undefined;
68
- return printFocused(graph, allArgs[focusIdx + 1], depth);
69
- }
70
- // --dashboard <name>
71
- const dashIdx = allArgs.indexOf('--dashboard');
72
- if (dashIdx >= 0 && allArgs[dashIdx + 1]) {
73
- const nodeId = resolveNodeId(graph, allArgs[dashIdx + 1]) ?? `dashboard:${allArgs[dashIdx + 1]}`;
74
- return printNodeLineage(graph, nodeId, flags);
75
- }
76
- // --dbt (show dbt model DAG)
77
- if (allArgs.includes('--dbt')) {
78
- return printDbtDAG(graph);
79
- }
80
58
  // --impact <name>
81
59
  const impactIdx = allArgs.indexOf('--impact');
82
60
  if (impactIdx >= 0 && allArgs[impactIdx + 1]) {
@@ -93,6 +71,23 @@ export async function runLineage(blockNameOrPath, rest, flags) {
93
71
  if (trustIdx >= 0 && allArgs[trustIdx + 1] && allArgs[trustIdx + 2]) {
94
72
  return printTrustChain(graph, allArgs[trustIdx + 1], allArgs[trustIdx + 2], flags);
95
73
  }
74
+ const searchIdx = allArgs.indexOf('--search');
75
+ if (searchIdx >= 0 && allArgs[searchIdx + 1]) {
76
+ return printSearchResults(graph, allArgs[searchIdx + 1]);
77
+ }
78
+ const focusIdx = allArgs.indexOf('--focus');
79
+ if (focusIdx >= 0 && allArgs[focusIdx + 1]) {
80
+ return printFocusedLineage(graph, allArgs[focusIdx + 1]);
81
+ }
82
+ const dashboardIdx = allArgs.indexOf('--dashboard');
83
+ if (dashboardIdx >= 0 && allArgs[dashboardIdx + 1]) {
84
+ return printFocusedLineage(graph, `dashboard:${allArgs[dashboardIdx + 1]}`);
85
+ }
86
+ if (allArgs.includes('--dbt')) {
87
+ const result = queryLineage(graph, { types: ['dbt_model', 'dbt_source'] });
88
+ console.log(JSON.stringify(result.graph, null, 2));
89
+ return;
90
+ }
96
91
  // --domain <name>
97
92
  if (flags.domain) {
98
93
  return printDomainLineage(graph, flags.domain, flags);
@@ -142,69 +137,81 @@ function loadFromManifestOrScan(projectRoot) {
142
137
  }
143
138
  /** Discover all blocks and semantic layer definitions and build the lineage graph. */
144
139
  function buildProjectLineage(projectRoot) {
145
- const blocks = [];
146
- const metrics = [];
147
- const dimensions = [];
148
- // Scan .dql files
149
- const dirs = ['blocks', 'dashboards', 'workbooks'];
150
- for (const dir of dirs) {
151
- const dirPath = join(projectRoot, dir);
152
- if (!existsSync(dirPath))
153
- continue;
154
- for (const entry of readdirSync(dirPath, { withFileTypes: true })) {
155
- if (!entry.isFile() || extname(entry.name) !== '.dql')
140
+ const dbtManifestPath = resolveDbtManifestPath(projectRoot);
141
+ try {
142
+ const manifest = buildManifest({ projectRoot, dbtManifestPath });
143
+ return LineageGraph.fromJSON({
144
+ nodes: manifest.lineage.nodes,
145
+ edges: manifest.lineage.edges,
146
+ });
147
+ }
148
+ catch {
149
+ const blocks = [];
150
+ const metrics = [];
151
+ const dimensions = [];
152
+ const dirs = ['blocks', 'dashboards', 'workbooks'];
153
+ for (const dir of dirs) {
154
+ const dirPath = join(projectRoot, dir);
155
+ if (!existsSync(dirPath))
156
156
  continue;
157
- const filePath = join(dirPath, entry.name);
157
+ for (const entry of readdirSync(dirPath, { withFileTypes: true })) {
158
+ if (!entry.isFile() || extname(entry.name) !== '.dql')
159
+ continue;
160
+ const filePath = join(dirPath, entry.name);
161
+ try {
162
+ const source = readFileSync(filePath, 'utf-8');
163
+ const parser = new Parser(source, `${dir}/${entry.name}`);
164
+ const ast = parser.parse();
165
+ for (const stmt of ast.statements) {
166
+ const block = stmt;
167
+ if (block.kind !== 'BlockDecl')
168
+ continue;
169
+ blocks.push({
170
+ name: block.name,
171
+ sql: block.query?.rawSQL ?? '',
172
+ domain: extractBlockProperty(block, 'domain'),
173
+ owner: extractBlockProperty(block, 'owner'),
174
+ status: extractBlockProperty(block, 'status'),
175
+ blockType: block.blockType,
176
+ metricRef: block.metricRef,
177
+ chartType: extractVisualizationChart(block),
178
+ });
179
+ }
180
+ }
181
+ catch {
182
+ // Skip unparseable files
183
+ }
184
+ }
185
+ }
186
+ const semanticDir = join(projectRoot, 'semantic-layer');
187
+ if (existsSync(semanticDir)) {
158
188
  try {
159
- const source = readFileSync(filePath, 'utf-8');
160
- const parser = new Parser(source, `${dir}/${entry.name}`);
161
- const ast = parser.parse();
162
- for (const stmt of ast.statements) {
163
- const block = stmt;
164
- if (block.kind !== 'BlockDecl')
165
- continue;
166
- blocks.push({
167
- name: block.name,
168
- sql: block.query?.rawSQL ?? '',
169
- domain: extractBlockProperty(block, 'domain'),
170
- owner: extractBlockProperty(block, 'owner'),
171
- status: extractBlockProperty(block, 'status'),
172
- blockType: block.blockType,
173
- metricRef: block.metricRef,
174
- chartType: extractVisualizationChart(block),
189
+ const layer = loadSemanticLayerFromDir(semanticDir);
190
+ for (const metric of layer.listMetrics()) {
191
+ metrics.push({
192
+ name: metric.name,
193
+ table: metric.table,
194
+ domain: metric.domain,
195
+ type: metric.type,
196
+ });
197
+ }
198
+ for (const dim of layer.listDimensions()) {
199
+ dimensions.push({
200
+ name: dim.name,
201
+ table: dim.table,
175
202
  });
176
203
  }
177
204
  }
178
205
  catch {
179
- // Skip unparseable files
180
- }
181
- }
182
- }
183
- // Load semantic layer
184
- const semanticDir = join(projectRoot, 'semantic-layer');
185
- if (existsSync(semanticDir)) {
186
- try {
187
- const layer = loadSemanticLayerFromDir(semanticDir);
188
- for (const metric of layer.listMetrics()) {
189
- metrics.push({
190
- name: metric.name,
191
- table: metric.table,
192
- domain: metric.domain,
193
- type: metric.type,
194
- });
206
+ // Non-fatal
195
207
  }
196
- for (const dim of layer.listDimensions()) {
197
- dimensions.push({
198
- name: dim.name,
199
- table: dim.table,
200
- });
201
- }
202
- }
203
- catch {
204
- // Non-fatal
205
208
  }
209
+ return buildLineageGraph(blocks, metrics, dimensions);
206
210
  }
207
- return buildLineageGraph(blocks, metrics, dimensions);
211
+ }
212
+ function resolveDbtManifestPath(projectRoot) {
213
+ const candidate = join(projectRoot, 'target', 'manifest.json');
214
+ return existsSync(candidate) ? candidate : undefined;
208
215
  }
209
216
  /** Extract a property value from a block — checks direct AST fields first, then properties array. */
210
217
  function extractBlockProperty(block, propName) {
@@ -239,23 +246,14 @@ function printSummary(graph, _flags) {
239
246
  const edges = graph.getAllEdges();
240
247
  const domains = graph.getDomains();
241
248
  const sourceTables = graph.getNodesByType('source_table');
242
- const dbtModels = graph.getNodesByType('dbt_model');
243
- const dbtSources = graph.getNodesByType('dbt_source');
244
249
  const blocks = graph.getNodesByType('block');
245
250
  const metrics = graph.getNodesByType('metric');
246
251
  const dimensions = graph.getNodesByType('dimension');
247
252
  const charts = graph.getNodesByType('chart');
248
- const dashboards = graph.getNodesByType('dashboard');
249
253
  console.log('\n DQL Lineage Summary');
250
254
  console.log(' ' + '='.repeat(50));
251
255
  // Overview counts
252
256
  console.log(`\n ${nodes.length} nodes, ${edges.length} edges, ${domains.length} domain(s)`);
253
- if (dbtModels.length > 0 || dbtSources.length > 0) {
254
- console.log(` dbt: ${dbtSources.length} source(s), ${dbtModels.length} model(s)`);
255
- }
256
- if (dashboards.length > 0) {
257
- console.log(` ${dashboards.length} dashboard(s)`);
258
- }
259
257
  // Source Tables
260
258
  if (sourceTables.length > 0) {
261
259
  console.log(`\n Source Tables (${sourceTables.length}):`);
@@ -386,10 +384,7 @@ function printDAGNode(graph, node, depth, printed) {
386
384
  printed.add(node.id);
387
385
  const indent = ' ' + ' '.repeat(depth);
388
386
  const prefix = depth === 0 ? '' : '└── ';
389
- const typeLabel = node.type === 'source_table' ? 'table'
390
- : node.type === 'dbt_model' ? 'dbt_model'
391
- : node.type === 'dbt_source' ? 'dbt_source'
392
- : node.type;
387
+ const typeLabel = node.type === 'source_table' ? 'table' : node.type;
393
388
  const badge = node.status === 'certified' ? ' ✓' : '';
394
389
  const domain = node.domain ? ` [${node.domain}]` : '';
395
390
  console.log(`${indent}${prefix}${typeLabel}:${node.name}${domain}${badge}`);
@@ -411,7 +406,7 @@ function resolveNodeId(graph, name) {
411
406
  if (name.includes(':') && graph.getNode(name))
412
407
  return name;
413
408
  // Try common type prefixes in priority order
414
- const prefixes = ['block', 'table', 'dbt_model', 'dbt_source', 'metric', 'dimension', 'chart', 'dashboard', 'domain'];
409
+ const prefixes = ['block', 'dashboard', 'dbt_model', 'dbt_source', 'table', 'metric', 'dimension', 'chart', 'domain'];
415
410
  for (const prefix of prefixes) {
416
411
  const id = `${prefix}:${name}`;
417
412
  if (graph.getNode(id))
@@ -424,6 +419,40 @@ function resolveNodeId(graph, name) {
424
419
  }
425
420
  return null;
426
421
  }
422
+ function printSearchResults(graph, term) {
423
+ const result = queryLineage(graph, { search: term });
424
+ console.log(`\n Search: ${term}`);
425
+ console.log(' ' + '='.repeat(50));
426
+ if (!result.matches?.length) {
427
+ console.log(' No lineage nodes matched.\n');
428
+ return;
429
+ }
430
+ for (const match of result.matches) {
431
+ const node = match.node;
432
+ console.log(` ${node.id}${node.domain ? ` [${node.domain}]` : ''}`);
433
+ }
434
+ console.log('');
435
+ }
436
+ function printFocusedLineage(graph, focus) {
437
+ const result = queryLineage(graph, { focus });
438
+ if (!result.focalNode) {
439
+ console.error(`"${focus}" not found in lineage graph.`);
440
+ process.exitCode = 1;
441
+ return;
442
+ }
443
+ console.log(`\n Focused Lineage: ${result.focalNode.name}`);
444
+ console.log(' ' + '='.repeat(50));
445
+ for (const node of result.graph.nodes.sort((a, b) => a.id.localeCompare(b.id))) {
446
+ console.log(` ${node.id}${node.domain ? ` [${node.domain}]` : ''}`);
447
+ }
448
+ if (result.graph.edges.length > 0) {
449
+ console.log('\n Edges:');
450
+ for (const edge of result.graph.edges) {
451
+ console.log(` ${edge.source} -${edge.type}-> ${edge.target}`);
452
+ }
453
+ }
454
+ console.log('');
455
+ }
427
456
  function printNodeLineage(graph, nodeId, _flags) {
428
457
  const node = graph.getNode(nodeId);
429
458
  if (!node) {
@@ -431,17 +460,14 @@ function printNodeLineage(graph, nodeId, _flags) {
431
460
  process.exitCode = 1;
432
461
  return;
433
462
  }
434
- const typeLabel = node.type === 'source_table' ? 'Table'
435
- : node.type === 'dbt_model' ? 'dbt Model'
436
- : node.type === 'dbt_source' ? 'dbt Source'
437
- : node.type.charAt(0).toUpperCase() + node.type.slice(1);
463
+ const typeLabel = node.type === 'source_table' ? 'Table' : node.type.charAt(0).toUpperCase() + node.type.slice(1);
438
464
  const ancestors = graph.ancestors(nodeId);
439
465
  const descendants = graph.descendants(nodeId);
440
466
  console.log(`\n ${typeLabel} Lineage: ${node.name}`);
441
467
  console.log(' ' + '='.repeat(50));
442
468
  // Metadata
443
469
  const meta = [];
444
- if (!['source_table', 'dbt_model', 'dbt_source'].includes(node.type))
470
+ if (node.type !== 'source_table')
445
471
  meta.push(`Type: ${node.type}`);
446
472
  if (node.domain)
447
473
  meta.push(`Domain: ${node.domain}`);
@@ -567,152 +593,6 @@ function printImpactAnalysis(graph, nodeId, _flags) {
567
593
  }
568
594
  console.log('');
569
595
  }
570
- function printSearch(graph, term) {
571
- const matches = searchNodes(graph, term, 30);
572
- if (matches.length === 0) {
573
- console.log(`\n No nodes found matching "${term}".\n`);
574
- return;
575
- }
576
- console.log(`\n Search Results for "${term}" (${matches.length} match${matches.length === 1 ? '' : 'es'}):`);
577
- console.log(' ' + '='.repeat(50));
578
- for (const { node, score } of matches) {
579
- const typeLabel = nodeTypeLabel(node.type);
580
- const domain = node.domain ? ` [${node.domain}]` : '';
581
- const status = node.status ? ` (${node.status})` : '';
582
- console.log(` ${typeLabel} ${node.name}${domain}${status}`);
583
- }
584
- console.log('\n Use `dql lineage --focus <name>` to see focused lineage for a specific node.\n');
585
- }
586
- function printFocused(graph, name, depth) {
587
- const result = queryLineage(graph, {
588
- focus: name,
589
- upstreamDepth: depth,
590
- downstreamDepth: depth,
591
- });
592
- if (!result.focalNode) {
593
- console.error(`"${name}" not found in lineage graph.`);
594
- process.exitCode = 1;
595
- return;
596
- }
597
- const focal = result.focalNode;
598
- const subNodes = result.graph.nodes;
599
- const subEdges = result.graph.edges;
600
- const typeLabel = nodeTypeLabel(focal.type);
601
- console.log(`\n Focused Lineage: ${focal.name} (${typeLabel})`);
602
- console.log(' ' + '='.repeat(50));
603
- if (depth !== undefined)
604
- console.log(` Depth: ${depth} hop(s)`);
605
- console.log(` Subgraph: ${subNodes.length} nodes, ${subEdges.length} edges`);
606
- // Separate upstream vs downstream relative to focal
607
- const focusGraph = LineageGraph.fromJSON(result.graph);
608
- const upstream = focusGraph.ancestors(focal.id).filter((n) => n.type !== 'domain');
609
- const downstream = focusGraph.descendants(focal.id).filter((n) => n.type !== 'domain');
610
- if (upstream.length > 0) {
611
- console.log(`\n Upstream (${upstream.length}):`);
612
- for (const n of upstream) {
613
- const label = nodeTypeLabel(n.type);
614
- const badge = n.status === 'certified' ? ' [certified]' : '';
615
- console.log(` ${label} ${n.name}${badge}${n.domain ? ` [${n.domain}]` : ''}`);
616
- }
617
- }
618
- console.log(`\n >>> ${typeLabel} ${focal.name} ${focal.status ? `(${focal.status})` : ''} <<<`);
619
- if (downstream.length > 0) {
620
- console.log(`\n Downstream (${downstream.length}):`);
621
- for (const n of downstream) {
622
- const label = nodeTypeLabel(n.type);
623
- const badge = n.status === 'certified' ? ' [certified]' : '';
624
- console.log(` ${label} ${n.name}${badge}${n.domain ? ` [${n.domain}]` : ''}`);
625
- }
626
- }
627
- // Show data flow tree
628
- console.log('\n Data Flow:');
629
- console.log(' ' + '-'.repeat(50));
630
- const printed = new Set();
631
- const roots = subNodes.filter((n) => n.type !== 'domain' && focusGraph.getIncomingEdges(n.id).length === 0);
632
- for (const root of roots.sort((a, b) => a.name.localeCompare(b.name))) {
633
- const rootNode = focusGraph.getNode(root.id);
634
- if (rootNode)
635
- printDAGNode(focusGraph, rootNode, 0, printed);
636
- }
637
- console.log('');
638
- }
639
- function printDbtDAG(graph) {
640
- const dbtModels = graph.getNodesByType('dbt_model');
641
- const dbtSources = graph.getNodesByType('dbt_source');
642
- if (dbtModels.length === 0 && dbtSources.length === 0) {
643
- console.log('\n No dbt models found in lineage.');
644
- console.log(' Run `dql compile --dbt-manifest target/manifest.json` to import dbt lineage.\n');
645
- return;
646
- }
647
- console.log('\n dbt Model DAG');
648
- console.log(' ' + '='.repeat(50));
649
- console.log(`\n ${dbtSources.length} source(s), ${dbtModels.length} model(s)`);
650
- if (dbtSources.length > 0) {
651
- console.log(`\n Sources:`);
652
- for (const src of dbtSources.sort((a, b) => a.name.localeCompare(b.name))) {
653
- const downstream = graph.getOutgoingEdges(src.id)
654
- .map((e) => graph.getNode(e.target))
655
- .filter((n) => n !== undefined)
656
- .map((n) => n.name);
657
- const arrow = downstream.length > 0 ? ` -> ${downstream.join(', ')}` : '';
658
- console.log(` SRC ${src.name}${arrow}`);
659
- }
660
- }
661
- if (dbtModels.length > 0) {
662
- console.log(`\n Models:`);
663
- for (const model of dbtModels.sort((a, b) => a.name.localeCompare(b.name))) {
664
- const upstream = graph.getIncomingEdges(model.id)
665
- .filter((e) => e.type === 'depends_on')
666
- .map((e) => graph.getNode(e.source))
667
- .filter((n) => n !== undefined)
668
- .map((n) => n.name);
669
- const downstream = graph.getOutgoingEdges(model.id)
670
- .map((e) => graph.getNode(e.target))
671
- .filter((n) => n !== undefined)
672
- .map((n) => `${nodeTypeLabel(n.type)} ${n.name}`);
673
- const from = upstream.length > 0 ? ` <- ${upstream.join(', ')}` : '';
674
- console.log(` DBT ${model.name}${from}`);
675
- if (downstream.length > 0) {
676
- console.log(` -> ${downstream.join(', ')}`);
677
- }
678
- // Show columns if available
679
- if (model.columns && model.columns.length > 0) {
680
- const colNames = model.columns.slice(0, 8).map((c) => c.name);
681
- const more = model.columns.length > 8 ? ` +${model.columns.length - 8} more` : '';
682
- console.log(` columns: ${colNames.join(', ')}${more}`);
683
- }
684
- }
685
- }
686
- // Show data flow tree
687
- console.log('\n dbt Data Flow:');
688
- console.log(' ' + '-'.repeat(50));
689
- const printed = new Set();
690
- for (const src of dbtSources.sort((a, b) => a.name.localeCompare(b.name))) {
691
- printDAGNode(graph, src, 0, printed);
692
- }
693
- // Print models not reachable from sources
694
- for (const model of dbtModels) {
695
- if (!printed.has(model.id)) {
696
- printDAGNode(graph, model, 0, printed);
697
- }
698
- }
699
- console.log('');
700
- }
701
- /** Short type label for display */
702
- function nodeTypeLabel(type) {
703
- switch (type) {
704
- case 'source_table': return 'TBL ';
705
- case 'dbt_model': return 'DBT ';
706
- case 'dbt_source': return 'SRC ';
707
- case 'block': return 'BLK ';
708
- case 'metric': return 'MET ';
709
- case 'dimension': return 'DIM ';
710
- case 'chart': return 'CHT ';
711
- case 'dashboard': return 'DASH';
712
- case 'domain': return 'DOM ';
713
- default: return type.toUpperCase().padEnd(4);
714
- }
715
- }
716
596
  function printTrustChain(graph, fromBlock, toBlock, _flags) {
717
597
  const fromId = `block:${fromBlock}`;
718
598
  const toId = `block:${toBlock}`;