@yesprasad/fluent-graph 0.1.0 → 0.2.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.
@@ -0,0 +1,141 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.traverseBlastRadius = traverseBlastRadius;
4
+ /**
5
+ * Build a map from node name → all node IDs with that name.
6
+ * Used to treat same-name siblings (sys_dictionary, sys_db_object, etc.
7
+ * representing the same table) as a single logical entity.
8
+ */
9
+ function buildNameIndex(graph) {
10
+ const index = new Map();
11
+ for (const node of graph.nodes) {
12
+ const ids = index.get(node.name) ?? [];
13
+ ids.push(node.id);
14
+ index.set(node.name, ids);
15
+ }
16
+ return index;
17
+ }
18
+ /**
19
+ * BFS reverse-traversal over the dependency graph.
20
+ *
21
+ * KEY DESIGN DECISIONS:
22
+ *
23
+ * 1. A logical "entity" is identified by node NAME, not by node ID.
24
+ * Multiple physical nodes can share a name (e.g., a table and all its
25
+ * field/documentation records are all named "x_my_table"). The BFS
26
+ * treats them as one logical node — once any physical node for a name
27
+ * is discovered, ALL siblings for that name are immediately marked
28
+ * visited so they never appear again.
29
+ *
30
+ * 2. The initial target is seeded with ALL physical nodes sharing the
31
+ * target name (plus the synthetic "platform:tableName" ID). This
32
+ * ensures incoming edges are found regardless of which physical node
33
+ * the edge was recorded against.
34
+ *
35
+ * 3. Edge matching uses BOTH:
36
+ * - edge.to === currentNodeId (exact physical node)
37
+ * - edge.targetTable === currentNodeName (canonical name — reliable
38
+ * for platform nodes and catches mis-targeted edges)
39
+ *
40
+ * 4. ALL edge types are traversed. Display filtering is the caller's job.
41
+ *
42
+ * @param graph The loaded LineageGraph.
43
+ * @param targetTableName The table name the user asked about.
44
+ * @param maxDepth Maximum hop depth. Silently clamped to 25.
45
+ */
46
+ function traverseBlastRadius(graph, targetTableName, maxDepth) {
47
+ const depth = Math.min(maxDepth, 25);
48
+ const nameIndex = buildNameIndex(graph);
49
+ // All physical nodes for the target name (may be many — sys_dictionary,
50
+ // sys_db_object, sys_documentation, etc. all named the same thing).
51
+ const targetPhysicalNodes = graph.nodes.filter(n => n.name === targetTableName);
52
+ // Prefer a non-child / primary node as the canonical representative.
53
+ // We can't use the registry here (pure function), so heuristic: prefer
54
+ // any node where the table name itself matches the target name (for custom
55
+ // tables this is the sys_db_object entry); otherwise first found.
56
+ const target = targetPhysicalNodes[0] ?? null;
57
+ // Pre-visit the platform shadow ID and ALL physical nodes for the target.
58
+ const visited = new Set();
59
+ const platformId = `platform:${targetTableName}`;
60
+ visited.add(platformId);
61
+ for (const n of targetPhysicalNodes)
62
+ visited.add(n.id);
63
+ // Seed the frontier with one entry per physical target node + platform shadow.
64
+ // This ensures we catch edges recorded against any of the physical nodes.
65
+ const seedIds = new Set([platformId, ...targetPhysicalNodes.map(n => n.id)]);
66
+ let frontier = [...seedIds].map(id => ({
67
+ nodeId: id,
68
+ nodeName: targetTableName,
69
+ hop: 0,
70
+ path: [targetTableName],
71
+ }));
72
+ const results = [];
73
+ // Track which logical names we have already emitted in results (for dedup).
74
+ const emittedNames = new Set([targetTableName]);
75
+ let maxHopReached = 0;
76
+ let cyclesPrevented = 0;
77
+ while (frontier.length > 0) {
78
+ const nextFrontier = [];
79
+ for (const entry of frontier) {
80
+ if (entry.hop >= depth)
81
+ continue;
82
+ // Reverse-edge scan: all edges whose destination is the current node.
83
+ const incomingEdges = graph.edges.filter(edge => edge.to === entry.nodeId ||
84
+ (entry.nodeName.length > 0 && edge.targetTable === entry.nodeName));
85
+ for (const edge of incomingEdges) {
86
+ const sourceNode = graph.nodes.find(n => n.id === edge.from);
87
+ if (!sourceNode)
88
+ continue;
89
+ if (visited.has(sourceNode.id)) {
90
+ cyclesPrevented++;
91
+ continue;
92
+ }
93
+ // Mark this physical node visited.
94
+ visited.add(sourceNode.id);
95
+ // Mark ALL sibling physical nodes (same name) visited immediately
96
+ // so they never surface as separate hop results.
97
+ const siblings = nameIndex.get(sourceNode.name) ?? [];
98
+ for (const sibId of siblings) {
99
+ if (!visited.has(sibId))
100
+ visited.add(sibId);
101
+ }
102
+ const newHop = entry.hop + 1;
103
+ const newPath = [...entry.path, sourceNode.name];
104
+ // Only emit one result per logical name (the first physical node found).
105
+ if (!emittedNames.has(sourceNode.name)) {
106
+ emittedNames.add(sourceNode.name);
107
+ results.push({
108
+ node: sourceNode,
109
+ hop: newHop,
110
+ path: newPath,
111
+ viaEdge: edge,
112
+ });
113
+ }
114
+ // Continue BFS from this logical entity, using the canonical name.
115
+ // Also seed entries for all sibling physical IDs so their incoming
116
+ // edges are explored too.
117
+ for (const sibId of siblings) {
118
+ nextFrontier.push({
119
+ nodeId: sibId,
120
+ nodeName: sourceNode.name,
121
+ hop: newHop,
122
+ path: newPath,
123
+ });
124
+ }
125
+ if (newHop > maxHopReached) {
126
+ maxHopReached = newHop;
127
+ }
128
+ }
129
+ }
130
+ frontier = nextFrontier;
131
+ }
132
+ // Sort by hop ASC, then name ASC for stable output.
133
+ results.sort((a, b) => a.hop - b.hop || a.node.name.localeCompare(b.node.name));
134
+ return {
135
+ target,
136
+ targetName: targetTableName,
137
+ hops: results,
138
+ maxHopReached,
139
+ cyclesPrevented,
140
+ };
141
+ }
@@ -42,11 +42,17 @@ const fs = __importStar(require("fs"));
42
42
  const path = __importStar(require("path"));
43
43
  const chalk_1 = __importDefault(require("chalk"));
44
44
  const cli_table3_1 = __importDefault(require("cli-table3"));
45
+ const registry_1 = require("../../sdk/registry");
46
+ const detector_1 = require("../../sdk/detector");
47
+ const traversal_1 = require("../../blast/traversal");
45
48
  exports.blastCommand = new commander_1.Command('blast')
46
49
  .description('Analyze blast radius for a target table')
47
50
  .argument('<target>', 'Target table name (e.g., incident, x_bank_account)')
48
51
  .option('-g, --graph <path>', 'Path to graph JSON file', 'fluent-graph.json')
49
52
  .option('-f, --format <type>', 'Output format: table, json, csv', 'table')
53
+ .option('--depth <n>', 'Maximum traversal depth', '10')
54
+ .option('--show-paths', 'Show the full dependency chain for each transitive artifact')
55
+ .option('--direct-only', 'Show only hop-1 (direct) impacts')
50
56
  .option('--include-deleted', 'Include deleted artifacts in the analysis')
51
57
  .option('--no-banner', 'Skip banner (inherited)')
52
58
  .action(async (targetTableName, options) => {
@@ -54,22 +60,27 @@ exports.blastCommand = new commander_1.Command('blast')
54
60
  const graphFilePath = path.isAbsolute(options.graph)
55
61
  ? options.graph
56
62
  : path.join(process.cwd(), options.graph);
63
+ // Load registry for type name derivation (best-effort; falls back gracefully)
64
+ const registry = new registry_1.ArtifactTypeRegistry();
65
+ const sdkDetection = await (0, detector_1.detectSDK)(process.cwd());
66
+ if (sdkDetection.found && sdkDetection.sdkPath) {
67
+ registry.load(sdkDetection.sdkPath);
68
+ }
57
69
  if (!fs.existsSync(graphFilePath)) {
58
70
  console.error(chalk_1.default.red(`\n❌ Error: Graph file not found at ${graphFilePath}`));
59
71
  process.exit(1);
60
72
  }
61
73
  const lineageGraph = JSON.parse(fs.readFileSync(graphFilePath, 'utf8'));
62
- // STEP 1: Determine target node (real node or shadow for platform)
74
+ // ── STEP 1: Validate target exists or is referenced ─────────────────────
63
75
  const targetNode = lineageGraph.nodes.find(node => node.name === targetTableName);
64
76
  const targetNodeId = targetNode ? targetNode.id : `platform:${targetTableName}`;
65
77
  const isBaseTable = !targetNode;
66
- // STEP 2: Check if target is referenced at all
67
78
  const isTargetReferenced = lineageGraph.edges.some(edge => edge.to === targetNodeId || edge.targetTable === targetTableName);
68
79
  if (!targetNode && !isTargetReferenced) {
69
80
  console.log(chalk_1.default.red(`\n❌ Target "${targetTableName}" not found or referenced`));
70
81
  console.log(chalk_1.default.yellow('\n💡 Available custom tables:'));
71
82
  const customTables = lineageGraph.nodes
72
- .filter(node => node.table === 'sys_db_object' && node.action !== 'DELETE')
83
+ .filter(node => registry.getPluginName(node.table) === 'TablePlugin' && !registry.isChild(node.table) && node.action !== 'DELETE')
73
84
  .map(node => node.name)
74
85
  .sort();
75
86
  customTables.forEach(tableName => console.log(chalk_1.default.gray(' • ') + chalk_1.default.white(tableName)));
@@ -80,7 +91,7 @@ exports.blastCommand = new commander_1.Command('blast')
80
91
  platformTables.forEach(tableName => console.log(chalk_1.default.gray(' • ') + chalk_1.default.magenta(tableName)));
81
92
  process.exit(1);
82
93
  }
83
- // STEP 3: Display header
94
+ // ── STEP 2: Display header ───────────────────────────────────────────────
84
95
  console.log(chalk_1.default.yellow.bold('\n🔥 BLAST RADIUS ANALYSIS'));
85
96
  console.log(chalk_1.default.gray('═'.repeat(80)));
86
97
  console.log(chalk_1.default.gray('Target: ') + chalk_1.default.white(targetTableName) + (isBaseTable ? chalk_1.default.yellow(' [Base Table]') : ''));
@@ -88,111 +99,137 @@ exports.blastCommand = new commander_1.Command('blast')
88
99
  console.log(chalk_1.default.gray('File: ') + chalk_1.default.gray(targetNode.source.file));
89
100
  }
90
101
  console.log(chalk_1.default.gray('═'.repeat(80)));
91
- // STEP 4: Collect impacted artifacts from edges
92
- const impactedArtifacts = lineageGraph.edges
93
- .filter(edge => edge.to === targetNodeId || edge.targetTable === targetTableName)
94
- .map(edge => {
95
- const sourceNode = lineageGraph.nodes.find(node => node.id === edge.from);
96
- if (!sourceNode || (!options.includeDeleted && sourceNode.action === 'DELETE'))
97
- return null;
98
- // Friendly type mapping
99
- let artifactType = 'unknown';
100
- switch (sourceNode.table) {
101
- case 'sys_script_client':
102
- artifactType = 'client_script';
103
- break;
104
- case 'sys_script':
105
- artifactType = 'business_rule';
106
- break;
107
- case 'sys_security_acl':
108
- artifactType = 'acl';
109
- break;
110
- case 'sys_ui_action':
111
- artifactType = 'ui_action';
112
- break;
113
- case 'sys_script_include':
114
- artifactType = 'script_include';
115
- break;
116
- case 'sys_dictionary':
117
- artifactType = 'field';
118
- break; // For schema/FK
119
- default: artifactType = sourceNode.table.replace('sys_', '').replace('_', ' ');
120
- }
121
- // Reason based on edge type
122
- const impactReason = edge.type === 'schema_relationship'
123
- ? `FK reference via ${edge.via || 'field'}`
124
- : `${sourceNode.attributes?.type || sourceNode.attributes?.when || edge.via || 'attachment'}`;
125
- return {
126
- artifact: sourceNode.name,
127
- type: artifactType,
128
- file: sourceNode.source.file,
129
- reason: impactReason,
130
- state: sourceNode.action
131
- };
132
- })
133
- .filter((artifact) => artifact !== null);
134
- if (impactedArtifacts.length === 0) {
102
+ // ── STEP 3: BFS traversal ────────────────────────────────────────────────
103
+ const maxDepth = options.directOnly ? 1 : Math.max(1, parseInt(options.depth, 10) || 10);
104
+ const traversal = (0, traversal_1.traverseBlastRadius)(lineageGraph, targetTableName, maxDepth);
105
+ // ── STEP 4: Apply display filter ─────────────────────────────────────────
106
+ // Show a node if it has a source file OR is a non-deleted local node.
107
+ // Suppress SDK-managed child records that have no user-authored source.
108
+ const isDisplayable = (tn) => {
109
+ if (!options.includeDeleted && tn.node.action === 'DELETE')
110
+ return false;
111
+ if (tn.node.source.file)
112
+ return true;
113
+ // Suppress isChild records with no source file (intermediate composition nodes)
114
+ if (registry.isChild(tn.node.table))
115
+ return false;
116
+ return true;
117
+ };
118
+ const displayed = traversal.hops.filter(isDisplayable);
119
+ const directImpact = displayed.filter(tn => tn.hop === 1);
120
+ const transitiveImpact = displayed.filter(tn => tn.hop > 1);
121
+ if (displayed.length === 0) {
135
122
  console.log(chalk_1.default.green('\n✅ Zero dependencies detected in local project.\n'));
136
123
  return;
137
124
  }
138
- // STEP 5: Output based on format
125
+ // ── STEP 5: Output ───────────────────────────────────────────────────────
139
126
  switch (options.format) {
140
- case 'json':
141
- console.log(JSON.stringify(impactedArtifacts, null, 2));
127
+ case 'json': {
128
+ const output = displayed.map(tn => ({
129
+ artifact: tn.node.name,
130
+ type: registry.getDisplayGroupName(tn.node.table).toLowerCase(),
131
+ file: tn.node.source.file,
132
+ hop: tn.hop,
133
+ path: tn.path,
134
+ via: tn.viaEdge.via ?? tn.viaEdge.type,
135
+ edgeType: tn.viaEdge.type,
136
+ state: tn.node.action,
137
+ }));
138
+ console.log(JSON.stringify(output, null, 2));
142
139
  break;
143
- case 'csv':
144
- console.log('Artifact,Type,File,Reason,State');
145
- impactedArtifacts.forEach(impactedArtifact => {
146
- console.log(`"${impactedArtifact.artifact}","${impactedArtifact.type}","${impactedArtifact.file}","${impactedArtifact.reason}","${impactedArtifact.state}"`);
140
+ }
141
+ case 'csv': {
142
+ console.log('Artifact,Type,File,Hop,Via,State,Path');
143
+ displayed.forEach(tn => {
144
+ const via = tn.viaEdge.via ?? tn.viaEdge.type;
145
+ const pathStr = tn.path.join(' > ');
146
+ console.log(`"${tn.node.name}","${registry.getDisplayGroupName(tn.node.table).toLowerCase()}","${tn.node.source.file}",${tn.hop},"${via}","${tn.node.action}","${pathStr}"`);
147
147
  });
148
148
  break;
149
+ }
149
150
  case 'table':
150
- default:
151
- const table = new cli_table3_1.default({
152
- head: [
151
+ default: {
152
+ // ── Direct Impact section ────────────────────────────────────────────
153
+ if (directImpact.length > 0) {
154
+ console.log(chalk_1.default.bold('\n⚡ DIRECT IMPACT') + chalk_1.default.gray(' (Hop 1)'));
155
+ const directTable = new cli_table3_1.default({
156
+ head: [
157
+ chalk_1.default.cyan('Artifact'),
158
+ chalk_1.default.cyan('Type'),
159
+ chalk_1.default.cyan('File'),
160
+ chalk_1.default.cyan('Via'),
161
+ chalk_1.default.cyan('State'),
162
+ ],
163
+ wordWrap: true,
164
+ style: { head: [], border: ['gray'] },
165
+ });
166
+ directImpact.forEach(tn => {
167
+ const via = formatVia(tn);
168
+ const stateColor = tn.node.action === 'DELETE' ? chalk_1.default.red : chalk_1.default.green;
169
+ directTable.push([
170
+ chalk_1.default.white(tn.node.name),
171
+ chalk_1.default.gray(registry.getDisplayGroupName(tn.node.table).toLowerCase()),
172
+ chalk_1.default.gray(tn.node.source.file || '—'),
173
+ chalk_1.default.gray(via),
174
+ stateColor(tn.node.action === 'DELETE' ? 'DELETE' : 'ACTIVE'),
175
+ ]);
176
+ });
177
+ console.log(directTable.toString());
178
+ }
179
+ // ── Transitive Impact section ────────────────────────────────────────
180
+ if (transitiveImpact.length > 0 && !options.directOnly) {
181
+ console.log(chalk_1.default.bold('\n🌊 TRANSITIVE IMPACT') + chalk_1.default.gray(' (Hop 2+)'));
182
+ const transitiveHead = [
153
183
  chalk_1.default.cyan('Artifact'),
154
184
  chalk_1.default.cyan('Type'),
155
185
  chalk_1.default.cyan('File'),
156
- chalk_1.default.cyan('Reason'),
157
- chalk_1.default.cyan('State')
158
- ],
159
- wordWrap: true,
160
- style: { head: [], border: ['gray'] }
161
- });
162
- impactedArtifacts.forEach(impactedArtifact => {
163
- const typeIcon = impactedArtifact.type === 'field' ? '🔗'
164
- : impactedArtifact.type === 'client_script' ? '⚡'
165
- : impactedArtifact.type === 'business_rule' ? '🔧'
166
- : impactedArtifact.type === 'acl' ? '🔒'
167
- : '';
168
- const stateColor = impactedArtifact.state === 'DELETE' ? chalk_1.default.red : chalk_1.default.green;
169
- table.push([
170
- chalk_1.default.white(impactedArtifact.artifact),
171
- `${typeIcon} ${chalk_1.default.gray(impactedArtifact.type)}`,
172
- chalk_1.default.gray(impactedArtifact.file),
173
- impactedArtifact.reason,
174
- stateColor(impactedArtifact.state)
175
- ]);
176
- });
177
- console.log('\n' + table.toString());
178
- // STEP 6: Impact Summary
186
+ chalk_1.default.cyan('Hop'),
187
+ chalk_1.default.cyan('State'),
188
+ ];
189
+ if (options.showPaths) {
190
+ transitiveHead.push(chalk_1.default.cyan('Path'));
191
+ }
192
+ const transitiveTable = new cli_table3_1.default({
193
+ head: transitiveHead,
194
+ wordWrap: true,
195
+ style: { head: [], border: ['gray'] },
196
+ });
197
+ transitiveImpact.forEach(tn => {
198
+ const stateColor = tn.node.action === 'DELETE' ? chalk_1.default.red : chalk_1.default.green;
199
+ const row = [
200
+ chalk_1.default.white(tn.node.name),
201
+ chalk_1.default.gray(registry.getDisplayGroupName(tn.node.table).toLowerCase()),
202
+ chalk_1.default.gray(tn.node.source.file || '—'),
203
+ chalk_1.default.yellow(String(tn.hop)),
204
+ stateColor(tn.node.action === 'DELETE' ? 'DELETE' : 'ACTIVE'),
205
+ ];
206
+ if (options.showPaths) {
207
+ row.push(chalk_1.default.gray(tn.path.join(' ')));
208
+ }
209
+ transitiveTable.push(row);
210
+ });
211
+ console.log(transitiveTable.toString());
212
+ }
213
+ // ── Impact Summary ───────────────────────────────────────────────────
179
214
  console.log(chalk_1.default.yellow('\n📊 IMPACT SUMMARY'));
180
- const riskLevel = impactedArtifacts.length < 3 ? 'LOW'
181
- : impactedArtifacts.length < 8 ? 'MEDIUM'
182
- : 'HIGH';
215
+ const totalCount = displayed.length;
216
+ const riskLevel = totalCount < 3 ? 'LOW' : totalCount < 8 ? 'MEDIUM' : 'HIGH';
183
217
  const riskColor = riskLevel === 'LOW' ? chalk_1.default.green
184
218
  : riskLevel === 'MEDIUM' ? chalk_1.default.yellow
185
219
  : chalk_1.default.red;
186
- console.log(chalk_1.default.gray('Total Impacted: ') + chalk_1.default.cyan(impactedArtifacts.length));
187
- console.log(chalk_1.default.gray('Risk Level: ') + riskColor.bold(riskLevel));
220
+ console.log(chalk_1.default.gray('Total Impacted: ') + chalk_1.default.cyan(totalCount) +
221
+ chalk_1.default.gray(` (${directImpact.length} direct, ${transitiveImpact.length} transitive)`));
222
+ console.log(chalk_1.default.gray('Max Depth Reached: ') + chalk_1.default.cyan(traversal.maxHopReached));
223
+ console.log(chalk_1.default.gray('Risk Level: ') + riskColor.bold(riskLevel));
188
224
  console.log(chalk_1.default.gray('─'.repeat(80)) + '\n');
189
225
  if (riskLevel === 'HIGH') {
190
- console.log(chalk_1.default.yellow('⚠️ High impact - Review carefully'));
226
+ console.log(chalk_1.default.yellow('⚠️ High impact review carefully before deploying'));
191
227
  }
192
228
  else if (riskLevel === 'MEDIUM') {
193
229
  console.log(chalk_1.default.yellow('💡 Moderate impact'));
194
230
  }
195
231
  break;
232
+ }
196
233
  }
197
234
  }
198
235
  catch (error) {
@@ -200,3 +237,11 @@ exports.blastCommand = new commander_1.Command('blast')
200
237
  process.exit(1);
201
238
  }
202
239
  });
240
+ /** Format the "Via" column label from a traversal node's edge. */
241
+ function formatVia(tn) {
242
+ const edge = tn.viaEdge;
243
+ if (edge.type === 'schema_relationship') {
244
+ return `${edge.via ?? 'ref'} (ref field)`;
245
+ }
246
+ return edge.via ?? edge.type;
247
+ }
@@ -13,85 +13,6 @@ const loader_1 = require("../../sdk/loader");
13
13
  const extractor_1 = require("../../extractor");
14
14
  const writer_1 = require("../../output/writer");
15
15
  const display_1 = require("../utils/display");
16
- // /**
17
- // * The Analyze Command: Orchestrates the transformation of a ServiceNow Fluent
18
- // * project into a formal directed graph.
19
- // */
20
- // export const extractCommand = new Command('analyze')
21
- // .description('Analyze and Extract dependency graph from current project')
22
- // .option('-o, --output <path>', 'Output file path', 'fluent-graph.json')
23
- // .option('-q, --quiet', 'Suppress console output')
24
- // .option('--no-display', 'Skip visual display')
25
- // .option('--no-banner', 'Skip banner (inherited)')
26
- // .action(async (options) => {
27
- // try {
28
- // const cwd = process.cwd();
29
- // let spinner: any;
30
- // // Step 1: Detect ServiceNow SDK Environment
31
- // if (!options.quiet) {
32
- // spinner = ora('Detecting ServiceNow SDK...').start();
33
- // }
34
- // const sdk = await detectSDK(cwd);
35
- // if (!sdk.found) {
36
- // if (spinner) spinner.fail('SDK not found');
37
- // console.error(chalk.red('\n❌ Error:'), sdk.error);
38
- // console.log(chalk.yellow('\n💡 Make sure you\'re in a ServiceNow Fluent project directory'));
39
- // process.exit(1);
40
- // }
41
- // if (spinner) {
42
- // spinner.succeed(`✅ SDK detected at ${chalk.gray(sdk.sdkPath)}`);
43
- // }
44
- // // Step 2: Dynamically Load Workspace SDK
45
- // if (!options.quiet) {
46
- // spinner = ora('Loading workspace SDK...').start();
47
- // }
48
- // const { sdkapi } = loadWorkspaceSDK(sdk.sdkPath!);
49
- // if (spinner) {
50
- // spinner.succeed('Workspace SDK loaded');
51
- // }
52
- // // Step 3: Instantiate the Project Proxy
53
- // if (!options.quiet) {
54
- // spinner = ora('Loading project configuration...').start();
55
- // }
56
- // const project = new sdkapi.Project({ rootDir: cwd });
57
- // if (spinner) {
58
- // spinner.succeed('Project loaded');
59
- // }
60
- // // Step 4: Extract Lineage Graph
61
- // // We pass the full project object, allowing the extractor to
62
- // // access nodes, edges, and metadata (projectRoot, scope, etc.)
63
- // if (!options.quiet) {
64
- // spinner = ora('Building dependency graph...').start();
65
- // }
66
- // const graph = await extractLineageGraph(project);
67
- // if (spinner) {
68
- // spinner.succeed(`Graph built: ${chalk.cyan(graph.nodes.length)} nodes, ${chalk.cyan(graph.edges.length)} edges`);
69
- // }
70
- // // Step 5: Persist the Graph (The "Iterable" result)
71
- // const outputPath = path.isAbsolute(options.output)
72
- // ? options.output
73
- // : path.join(cwd, options.output);
74
- // writeGraph(graph, outputPath);
75
- // if (!options.quiet) {
76
- // console.log(chalk.green('\n✅ Analysis complete'));
77
- // console.log(chalk.gray(' Location: ') + chalk.white(outputPath));
78
- // console.log(chalk.gray(' Complexity: ') + chalk.cyan((graph.edges.length / graph.nodes.length).toFixed(2)) + chalk.gray(' edges/node'));
79
- // console.log(chalk.gray(' Generated at: ') + chalk.gray(new Date().toLocaleTimeString()));
80
- // }
81
- // // Step 6: Visual Lineage Map
82
- // // "Artifact-Agnostic" display logic
83
- // if (options.display && !options.quiet) {
84
- // displayLineageMap(graph);
85
- // }
86
- // } catch (err: any) {
87
- // console.error(chalk.red('\n❌ Error:'), err.message);
88
- // if (process.env.DEBUG) {
89
- // console.error(chalk.gray('\nStack trace:'));
90
- // console.error(err.stack);
91
- // }
92
- // process.exit(1);
93
- // }
94
- // });
95
16
  /**
96
17
  * The Extract Command: Orchestrates the transformation of a ServiceNow Fluent
97
18
  * project into a formal directed graph.
@@ -141,7 +62,7 @@ exports.extractCommand = new commander_1.Command('analyze')
141
62
  if (!options.quiet) {
142
63
  spinner = (0, ora_1.default)('Building dependency graph...').start();
143
64
  }
144
- const lineageGraph = await (0, extractor_1.extractLineageGraph)(projectInstance);
65
+ const lineageGraph = await (0, extractor_1.extractLineageGraph)(projectInstance, workspaceSDK.registry);
145
66
  if (spinner) {
146
67
  spinner.succeed(`Graph built: ${chalk_1.default.cyan(lineageGraph.nodes.length)} nodes, ${chalk_1.default.cyan(lineageGraph.edges.length)} edges`);
147
68
  }
@@ -158,7 +79,7 @@ exports.extractCommand = new commander_1.Command('analyze')
158
79
  }
159
80
  // Step 6: Visual Lineage Map
160
81
  if (options.display && !options.quiet) {
161
- (0, display_1.displayLineageMap)(lineageGraph);
82
+ (0, display_1.displayLineageMap)(lineageGraph, workspaceSDK.registry);
162
83
  }
163
84
  }
164
85
  catch (error) {
@@ -1,2 +1 @@
1
- #!/usr/bin/env node
2
1
  export {};
package/dist/cli/index.js CHANGED
@@ -1,33 +1,8 @@
1
- #!/usr/bin/env node
2
1
  "use strict";
3
- // import { Command } from 'commander';
4
- // import chalk from 'chalk';
5
- // import { extractCommand } from './commands/extract';
6
- // import { blastCommand } from './commands/blast';
7
- // import { displayBanner } from './utils/banner';
8
2
  var __importDefault = (this && this.__importDefault) || function (mod) {
9
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
10
4
  };
11
5
  Object.defineProperty(exports, "__esModule", { value: true });
12
- // const program = new Command();
13
- // // Show banner on first run (unless --no-banner flag)
14
- // const showBanner = !process.argv.includes('--no-banner') && !process.argv.includes('--quiet');
15
- // if (showBanner) {
16
- // displayBanner();
17
- // }
18
- // program
19
- // .name('fluent-graph')
20
- // .description('Dependency/ Lineage graph extraction & Blast-radius/ impact analysis for ServiceNow Fluent Artifacts')
21
- // .version('1.0.0')
22
- // .option('--no-banner', 'Suppress ASCII banner');
23
- // // Add commands
24
- // program.addCommand(extractCommand);
25
- // program.addCommand(blastCommand);
26
- // // Default behavior: show help if no command provided
27
- // if (process.argv.length === 2) {
28
- // program.help();
29
- // }
30
- // program.parse(process.argv);
31
6
  const commander_1 = require("commander");
32
7
  const path_1 = __importDefault(require("path"));
33
8
  const fs_1 = __importDefault(require("fs"));
@@ -16,10 +16,10 @@ function displayBanner() {
16
16
  ╚═╝ ╚══════╝ ╚═════╝ ╚══════╝╚═╝ ╚═══╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═╝╚═╝ ╚═╝ ╚═╝
17
17
  `;
18
18
  console.log(chalk_1.default.cyan(banner));
19
- console.log(chalk_1.default.gray(' Dependency/ Lineage graph extraction & Blast-radius/ impact analysis for ServiceNow Fluent Artifacts'));
20
- console.log(chalk_1.default.gray(' ─────────────────────────────────────────────────────────────────────────────\n'));
19
+ console.log(chalk_1.default.gray(' Dependency visibility and impact analysis for ServiceNow Fluent SDK projects'));
20
+ console.log(chalk_1.default.gray(' ────────────────────────────────────────────────────────────────────────────\n'));
21
21
  }
22
22
  function displayVersion() {
23
23
  console.log(chalk_1.default.cyan.bold('Fluent Graph') + chalk_1.default.gray(' v0.1.0'));
24
- console.log(chalk_1.default.gray('Dependency visibility for ServiceNow Fluent projects\n'));
24
+ console.log(chalk_1.default.gray('Dependency visibility and impact analysis for ServiceNow Fluent SDK projects\n'));
25
25
  }
@@ -1,2 +1,3 @@
1
1
  import { LineageGraph } from '../../types/graphs';
2
- export declare function displayLineageMap(graph: LineageGraph): void;
2
+ import { ArtifactTypeRegistry } from '../../sdk/registry';
3
+ export declare function displayLineageMap(graph: LineageGraph, registry: ArtifactTypeRegistry): void;