@yesprasad/fluent-graph 0.2.2 → 0.2.3

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/cli/index.js CHANGED
@@ -1,49 +1,32 @@
1
- "use strict";
2
- var __importDefault = (this && this.__importDefault) || function (mod) {
3
- return (mod && mod.__esModule) ? mod : { "default": mod };
4
- };
5
- Object.defineProperty(exports, "__esModule", { value: true });
6
- const commander_1 = require("commander");
7
- const path_1 = __importDefault(require("path"));
8
- const fs_1 = __importDefault(require("fs"));
9
- const extract_1 = require("./commands/extract");
10
- const blast_1 = require("./commands/blast");
11
- const banner_1 = require("./utils/banner");
12
- // ===== 1. Load package.json dynamically =====
13
- const packageJsonPath = path_1.default.join(__dirname, '../../package.json');
14
- const packageJsonContent = fs_1.default.readFileSync(packageJsonPath, 'utf-8');
15
- const packageJson = JSON.parse(packageJsonContent);
16
- // ===== 2. Initialize CLI program =====
17
- const program = new commander_1.Command();
18
- // Show banner on first run (unless --no-banner / --quiet, or JSON output requested).
19
- // JSON consumers (agents, scripts) need stdout to be pure JSON, so the banner is
20
- // suppressed whenever `--format json` (or `-f json`) is present.
21
- const wantsJsonOutput = (() => {
22
- const argv = process.argv;
23
- for (let i = 0; i < argv.length; i++) {
24
- const a = argv[i];
25
- if (a === '--format' || a === '-f')
26
- return argv[i + 1] === 'json';
27
- if (a === '--format=json' || a === '-f=json')
28
- return true;
29
- }
30
- return false;
31
- })();
32
- const showBanner = !process.argv.includes('--no-banner') && !process.argv.includes('--quiet') && !wantsJsonOutput;
33
- if (showBanner) {
34
- (0, banner_1.displayBanner)();
35
- }
36
- // Use dynamic name, description, version
37
- program
38
- .name(packageJson.name)
39
- .description(packageJson.description)
40
- .version(packageJson.version)
41
- .option('--no-banner', 'Suppress ASCII banner');
42
- // ===== 3. Register commands =====
43
- program.addCommand(extract_1.extractCommand);
44
- program.addCommand(blast_1.blastCommand);
45
- // ===== 4. Default: show help if no command =====
46
- if (process.argv.length === 2) {
47
- program.help();
48
- }
49
- program.parse(process.argv);
1
+ "use strict";var ue=Object.create;var H=Object.defineProperty;var me=Object.getOwnPropertyDescriptor;var he=Object.getOwnPropertyNames;var ye=Object.getPrototypeOf,be=Object.prototype.hasOwnProperty;var Se=(o,e,t,r)=>{if(e&&typeof e=="object"||typeof e=="function")for(let n of he(e))!be.call(o,n)&&n!==t&&H(o,n,{get:()=>e[n],enumerable:!(r=me(e,n))||r.enumerable});return o};var E=(o,e,t)=>(t=o!=null?ue(ye(o)):{},Se(e||!o||!o.__esModule?H(t,"default",{value:o,enumerable:!0}):t,o));var pe=require("commander"),ge=E(require("path")),fe=E(require("fs"));var ae=require("commander"),M=E(require("path")),w=E(require("chalk")),R=E(require("ora"));var F=E(require("path")),$=E(require("fs")),P=async o=>{let e=F.default.join(o,"node_modules");if(!$.default.existsSync(e))return{found:!1,error:"node_modules not found. Run npm install first."};let t=F.default.join(e,"@servicenow","sdk");return $.default.existsSync(t)?{found:!0,sdkPath:t}:{found:!1,error:"@servicenow/sdk not found. This is not a ServiceNow Fluent project."}};var J=require("module"),U=E(require("path"));var _=require("module"),q=E(require("path")),I=class{constructor(){this.tables=new Map}load(e){try{let r=(0,_.createRequire)(q.default.join(e,"package.json"))("@servicenow/sdk-build-plugins");for(let[,n]of Object.entries(r)){let i=n;if(!i?.config?.name)continue;let p=i.config.name,h=Object.keys(i.config.records||{}).filter(s=>s!=="*");if(h.length!==0){for(let s of h)this.register(s,p);try{let s=i.getRelationships();s&&typeof s=="object"&&this.parseRelationships(s,p)}catch{}}}}catch{}}register(e,t){this.tables.has(e)||this.tables.set(e,{table:e,pluginName:t,isChild:!1,children:[]})}parseRelationships(e,t){for(let[r,n]of Object.entries(e))this.register(r,t),n&&typeof n=="object"&&this.processChildren(n,t,r)}processChildren(e,t,r){for(let[n,i]of Object.entries(e)){if(n==="via"||n==="descendant"||n==="relationships"||n===r)continue;let p=n;this.register(p,t);let h=this.tables.get(p);h.isChild||(h.isChild=!0,h.parentTable=r,i&&typeof i=="object"&&typeof i.via=="string"&&(h.parentVia=i.via));let s=this.tables.get(r);s&&!s.children.includes(p)&&s.children.push(p),i&&typeof i=="object"&&i.relationships&&typeof i.relationships=="object"&&this.processChildren(i.relationships,t,p)}}get(e){return this.tables.get(e)}getPluginName(e){return this.tables.get(e)?.pluginName??"Record"}getDisplayGroupName(e){let t=this.tables.get(e);return t?t.pluginName.replace(/Plugin$/,"").replace(/([a-z])([A-Z])/g,"$1 $2").replace(/^./,r=>r.toUpperCase()).trim():"Other"}isChild(e){return this.tables.get(e)?.isChild??!1}getParentTable(e){return this.tables.get(e)?.parentTable}getChildren(e){return this.tables.get(e)?.children??[]}getTablesByPlugin(e){return[...this.tables.values()].filter(t=>t.pluginName===e).map(t=>t.table)}getAllPluginNames(){return[...new Set([...this.tables.values()].map(e=>e.pluginName))].sort()}getAllTables(){return[...this.tables.keys()]}isKnown(e){return this.tables.has(e)}};function B(o){let e=(0,J.createRequire)(U.default.join(o,"package.json")),t=new I;return t.load(o),{buildCore:e("@servicenow/sdk-build-core"),sdkapi:e("@servicenow/sdk-api"),registry:t}}var z=E(require("path"));function ve(o){let e={};for(let[t,r]of Object.entries(o))if(r)try{let n=typeof r.getValue=="function"?r.getValue():r;(typeof n!="object"||n===null||Array.isArray(n))&&(e[t]=n)}catch{continue}return e}function X(o,e){let t=[];for(let r of o.query()){let n=r.setProperties||{},i=r.getId()?.getValue()?.toString()||"",p=r.getTable()?.toString()||"",h=n.name?.getValue?.()?.toString()||n.label?.getValue?.()?.toString()||n.sys_name?.getValue?.()?.toString()||i,s=r.getOriginalFilePath()?.toString()||"",y=s?z.default.relative(e,s):"";t.push({id:i,table:p,action:r.getAction(),name:h,attributes:ve(n),source:{file:y,type:r.getOriginalSource()?.getKindName?.()||"SourceFile"}})}return t}function Y(o,e,t){let r=[];for(let n of o.query()){let i=n.getId()?.getValue()?.toString(),p=n.setProperties||{},h=n.getTable()?.toString(),s=p.internal_type?.getValue?.()?.toString(),y=p.reference?.getValue?.()?.toString(),S=p.element?.getValue?.()?.toString();if(s==="reference"&&y){let f=e.find(l=>l.name===y);r.push({from:i,to:f?.id||`platform:${y}`,via:S||"reference",type:"schema_relationship"})}let N=p.table?.getValue?.()?.toString()||p.collection?.getValue?.()?.toString();if(N){let f=e.find(l=>l.name===N);r.push({from:i,to:f?.id||`platform:${N}`,via:p.table?"table":"collection",type:"dependency",targetTable:N})}if(t.getPluginName(h)==="AclPlugin"){let f=p.name?.getValue?.()?.toString();if(f&&f.includes(".")){let[l]=f.split("."),u=e.find(m=>m.name===l);r.push({from:i,to:u?.id||`platform:${l}`,via:"name",type:"dependency",targetTable:l})}}for(let[f,l]of Object.entries(p))if(Array.isArray(l))l.forEach(u=>{if(u&&u.constructor?.name==="RecordId"){let m=u.getValue?.()?.toString(),v=u.getTable?.()?.toString();m&&t.getPluginName(v)!=="TablePlugin"&&r.push({from:i,to:m,via:f,type:"dependency",targetTable:v})}});else if(l&&l.constructor?.name==="RecordId"){let u=l.getValue?.()?.toString(),m=l.getTable?.()?.toString();u&&t.getPluginName(m)!=="TablePlugin"&&r.push({from:i,to:u,via:f,type:"dependency",targetTable:m})}else if(l&&l.constructor?.name==="Record"){let u=l.getId?.()?.getValue?.()?.toString();if(u){let m=e.find(v=>v.id===u);m&&t.getPluginName(m.table)!=="TablePlugin"&&r.push({from:i,to:u,via:f,type:"dependency",targetTable:m.table})}}}return r}function Z(o){let e=[];for(let t of o.query()){let r=t.getId()?.getValue()?.toString(),n=t.getRelated?.()||[];for(let i of n){let p=i.getId()?.getValue()?.toString();p&&r&&e.push({from:r,to:p,type:"composition"})}}return e}function Q(o,e,t,r){let n=o.getConfig()||{},i=o.getPackage()||{};return{projectRoot:o.getRootDir()||process.cwd(),scopeId:n.scopeId||"unknown",scopeName:n.scope||"Global",recordCount:e,referenceCount:t,compositionCount:r,generatedAt:new Date().toISOString(),sdkVersion:i.dependencies?.["@servicenow/sdk"]||i.devDependencies?.["@servicenow/sdk"]||"unknown"}}async function ee(o,e){let t=await o.getRecords(),r=o.getRootDir(),n=X(t,r),i=Y(t,n,e),p=Z(t),h=[...i,...p];return{metadata:Q(o,t.query().length,i.length,p.length),nodes:n,edges:h}}var C=E(require("fs")),te=E(require("path"));function oe(o,e){let t=JSON.stringify(o,null,2),r=te.default.dirname(e);C.default.existsSync(r)||C.default.mkdirSync(r,{recursive:!0}),C.default.writeFileSync(e,t,"utf-8")}var re=E(require("path")),d=E(require("chalk")),L=E(require("cli-table3"));function ne(o,e){console.log(d.default.yellow.bold(`
2
+ \u{1F4CA} LINEAGE MAP`)),console.log(d.default.gray("\u2550".repeat(80)));let t=new Map;for(let s of o.nodes){if(s.action==="DELETE")continue;let y=e.getDisplayGroupName(s.table);t.has(y)||t.set(y,[]),t.get(y).push(s)}for(let[s,y]of t){let S=y.filter(f=>!e.isChild(f.table)&&f.source.file);if(S.length===0)continue;if(console.log(d.default.cyan.bold(`
3
+ ${s}`)),S.some(f=>e.getPluginName(f.table)==="BusinessRulePlugin")){let f=new L.default({head:[d.default.cyan("Name"),d.default.cyan("When"),d.default.cyan("Table"),d.default.cyan("On"),d.default.cyan("Order"),d.default.cyan("Status"),d.default.cyan("File")],colWidths:[32,8,28,16,7,8,28],wordWrap:!0,style:{head:[],border:["gray"]}});S.forEach(l=>{let u=l.action==="DELETE"?d.default.red("DELETE"):d.default.green("ACTIVE"),m=l.attributes.when||"",g=o.edges.find(c=>c.from===l.id&&c.type==="dependency"&&c.targetTable)?.targetTable||"",b=[];l.attributes.action_insert&&b.push("Insert"),l.attributes.action_update&&b.push("Update"),l.attributes.action_delete&&b.push("Delete"),l.attributes.action_query&&b.push("Query"),f.push([d.default.white(l.name),d.default.yellow(m),d.default.magenta(g),b.length?b.join(", "):d.default.gray("\u2014"),String(l.attributes.order??""),u,d.default.gray(re.default.basename(l.source.file))])}),console.log(f.toString())}else{let f=new L.default({head:[d.default.cyan("Name"),d.default.cyan("Status"),d.default.cyan("Source File")],wordWrap:!0,style:{head:[],border:["gray"]}});S.forEach(l=>{let u=l.action==="DELETE"?d.default.red("DELETE"):d.default.green("ACTIVE");f.push([d.default.white(l.name),u,d.default.gray(l.source.file)])}),console.log(f.toString())}}let r=o.edges.filter(s=>s.type==="schema_relationship");if(r.length>0){console.log(d.default.cyan.bold(`
4
+ \u{1F517} SCHEMA RELATIONSHIPS (Reference Fields)`));let s=new L.default({head:[d.default.cyan("Field"),d.default.cyan("From Table"),d.default.cyan("Target Table")],wordWrap:!0,style:{head:[],border:["gray"]}});r.forEach(y=>{let S=o.nodes.find(u=>u.id===y.from),N=S?.attributes?.element||y.via||"unknown",f=S?.attributes?.name||"unknown",l=y.to.startsWith("platform:")?y.to.split(":")[1]:o.nodes.find(u=>u.id===y.to)?.name||y.to;s.push([d.default.white(N),d.default.gray(f),d.default.magenta(l)])}),console.log(s.toString())}console.log(d.default.cyan.bold(`
5
+ \u{1F9E0} PROJECT INSIGHTS`));let n=o.edges.filter(s=>s.to.startsWith("platform:")).length,i=o.edges.filter(s=>!s.to.startsWith("platform:")).length,p=o.nodes.filter(s=>s.action!=="DELETE"&&s.source.file&&!s.id.startsWith("platform:")&&!e.isChild(s.table));console.log(` \u2022 Total User Artifacts: ${d.default.cyan(p.length)}`);let h=p.reduce((s,y)=>{let S=e.getDisplayGroupName(y.table);return s[S]=(s[S]||0)+1,s},{});console.log(` \u2022 User Artifacts Breakdown: ${JSON.stringify(h,null,2)}`),console.log(` \u2022 Platform Dependencies: ${d.default.magenta(n)}`),console.log(` \u2022 Internal References: ${d.default.white(i)}`),console.log(` \u2022 Total Artifacts: ${d.default.cyan(o.nodes.length)}`),console.log(` \u2022 Total Relationships: ${d.default.cyan(o.edges.length)}`),console.log(d.default.gray("\u2550".repeat(80))+`
6
+ `)}var se=new ae.Command("analyze").description("Analyze and extract dependency graph from the current project").option("-o, --output <path>","Output file path","fluent-graph.json").option("-f, --format <type>","Output format: table, json","table").option("-q, --quiet","Suppress console output").option("--no-display","Skip visual display").option("--no-banner","Skip banner (inherited)").action(async o=>{let e=o.format==="json",t;try{let r=process.cwd();!o.quiet&&!e&&(t=(0,R.default)("Detecting ServiceNow SDK...").start());let n=await P(r);n.found||(t&&t.fail("SDK not found"),e?console.log(JSON.stringify({error:n.error,code:"SDK_NOT_FOUND"})):(console.error(w.default.red(`
7
+ \u274C Error:`),n.error),console.log(w.default.yellow(`
8
+ \u{1F4A1} Make sure you're in a ServiceNow Fluent project directory`))),process.exit(1)),t&&t.succeed(`\u2705 SDK detected at ${w.default.gray(n.sdkPath)}`),!o.quiet&&!e&&(t=(0,R.default)("Loading workspace SDK...").start());let i=B(n.sdkPath);t&&t.succeed("Workspace SDK loaded"),!o.quiet&&!e&&(t=(0,R.default)("Loading project configuration...").start());let p=new i.sdkapi.Project({rootDir:r});t&&t.succeed("Project loaded"),!o.quiet&&!e&&(t=(0,R.default)("Building dependency graph...").start());let h;if(e){let S=process.stdout.write.bind(process.stdout);process.stdout.write=process.stderr.write.bind(process.stderr),h=()=>{process.stdout.write=S}}let s=await ee(p,i.registry);h?.(),t&&t.succeed(`Graph built: ${w.default.cyan(s.nodes.length)} nodes, ${w.default.cyan(s.edges.length)} edges`);let y=M.default.isAbsolute(o.output)?o.output:M.default.join(r,o.output);oe(s,y),e?console.log(JSON.stringify(s,null,2)):(o.quiet||(console.log(w.default.green(`
9
+ \u2705 Analysis complete`)),console.log(w.default.gray(" Location: ")+w.default.white(y)),console.log(w.default.gray(" Complexity: ")+w.default.cyan((s.edges.length/s.nodes.length).toFixed(2))+w.default.gray(" edges/node")),console.log(w.default.gray(" Generated at: ")+w.default.gray(new Date().toLocaleTimeString()))),o.display&&!o.quiet&&ne(s,i.registry))}catch(r){if(e){let n=r instanceof Error?r.message:"Unexpected error";console.log(JSON.stringify({error:n,code:"EXTRACTION_FAILED"}))}else r instanceof Error?(console.error(w.default.red(`
10
+ \u274C Error:`),r.message),process.env.DEBUG&&(console.error(w.default.gray(`
11
+ Stack trace:`)),console.error(r.stack))):(console.error(w.default.red(`
12
+ \u274C Unknown error occurred`)),console.error(r));process.exit(1)}});var ce=require("commander"),O=E(require("fs")),G=E(require("path")),a=E(require("chalk")),V=E(require("cli-table3"));function Ee(o){let e=new Map;for(let t of o.nodes){let r=e.get(t.name)??[];r.push(t.id),e.set(t.name,r)}return e}function ie(o,e,t){let r=Math.min(t,25),n=Ee(o),i=o.nodes.filter(m=>m.name===e),p=i[0]??null,h=new Set,s=`platform:${e}`;h.add(s);for(let m of i)h.add(m.id);let S=[...new Set([s,...i.map(m=>m.id)])].map(m=>({nodeId:m,nodeName:e,hop:0,path:[e]})),N=[],f=new Set([e]),l=0,u=0;for(;S.length>0;){let m=[];for(let v of S){if(v.hop>=r)continue;let g=o.edges.filter(b=>b.to===v.nodeId||v.nodeName.length>0&&b.targetTable===v.nodeName);for(let b of g){let c=o.nodes.find(k=>k.id===b.from);if(!c)continue;if(h.has(c.id)){u++;continue}h.add(c.id);let A=n.get(c.name)??[];for(let k of A)h.has(k)||h.add(k);let T=v.hop+1,D=[...v.path,c.name];f.has(c.name)||(f.add(c.name),N.push({node:c,hop:T,path:D,viaEdge:b}));for(let k of A)m.push({nodeId:k,nodeName:c.name,hop:T,path:D});T>l&&(l=T)}}S=m}return N.sort((m,v)=>m.hop-v.hop||m.node.name.localeCompare(v.node.name)),{target:p,targetName:e,hops:N,maxHopReached:l,cyclesPrevented:u}}var le=new ce.Command("blast").description("Analyze blast radius for a target table").argument("<target>","Target table name (e.g., incident, x_bank_account)").option("-g, --graph <path>","Path to graph JSON file","fluent-graph.json").option("-f, --format <type>","Output format: table, json, csv","table").option("--depth <n>","Maximum traversal depth","10").option("--show-paths","Show the full dependency chain for each transitive artifact").option("--direct-only","Show only hop-1 (direct) impacts").option("--include-deleted","Include deleted artifacts in the analysis").option("--no-banner","Skip banner (inherited)").action(async(o,e)=>{let t=e.format==="json";try{let r=G.isAbsolute(e.graph)?e.graph:G.join(process.cwd(),e.graph),n=new I,i=await P(process.cwd());i.found&&i.sdkPath&&n.load(i.sdkPath),O.existsSync(r)||(t?console.log(JSON.stringify({error:`Graph file not found at ${r}`,code:"GRAPH_NOT_FOUND"})):console.error(a.default.red(`
13
+ \u274C Error: Graph file not found at ${r}`)),process.exit(1));let p=JSON.parse(O.readFileSync(r,"utf8")),h=p.nodes.find(g=>g.name===o),s=h?h.id:`platform:${o}`,y=!h,S=p.edges.some(g=>g.to===s||g.targetTable===o);if(!h&&!S){let g=p.nodes.filter(c=>n.getPluginName(c.table)==="TablePlugin"&&!n.isChild(c.table)&&c.action!=="DELETE").map(c=>c.name).sort(),b=[...new Set(p.edges.filter(c=>c.to.startsWith("platform:")).map(c=>c.to.split(":")[1]))].sort();t?console.log(JSON.stringify({error:`Target "${o}" not found or referenced`,code:"TARGET_NOT_FOUND",availableCustomTables:g,availablePlatformTables:b})):(console.log(a.default.red(`
14
+ \u274C Target "${o}" not found or referenced`)),console.log(a.default.yellow(`
15
+ \u{1F4A1} Available custom tables:`)),g.forEach(c=>console.log(a.default.gray(" \u2022 ")+a.default.white(c))),console.log(a.default.yellow(`
16
+ \u{1F4A1} Referenced platform tables:`)),b.forEach(c=>console.log(a.default.gray(" \u2022 ")+a.default.magenta(c)))),process.exit(1)}t||(console.log(a.default.yellow.bold(`
17
+ \u{1F525} BLAST RADIUS ANALYSIS`)),console.log(a.default.gray("\u2550".repeat(80))),console.log(a.default.gray("Target: ")+a.default.white(o)+(y?a.default.yellow(" [Base Table]"):"")),y||console.log(a.default.gray("File: ")+a.default.gray(h.source.file)),console.log(a.default.gray("\u2550".repeat(80))));let N=e.directOnly?1:Math.max(1,parseInt(e.depth,10)||10),f=ie(p,o,N),l=g=>!e.includeDeleted&&g.node.action==="DELETE"?!1:g.node.source.file?!0:!n.isChild(g.node.table),u=f.hops.filter(l),m=u.filter(g=>g.hop===1),v=u.filter(g=>g.hop>1);if(u.length===0){console.log(t?JSON.stringify({target:o,isBaseTable:y,totalImpacted:0,direct:0,transitive:0,maxHopReached:0,cyclesPrevented:f.cyclesPrevented,riskLevel:"LOW",impacts:[]},null,2):a.default.green(`
18
+ \u2705 Zero dependencies detected in local project.
19
+ `));return}switch(e.format){case"json":{let g=u.length,b=g<3?"LOW":g<8?"MEDIUM":"HIGH";console.log(JSON.stringify({target:o,isBaseTable:y,totalImpacted:g,direct:m.length,transitive:v.length,maxHopReached:f.maxHopReached,cyclesPrevented:f.cyclesPrevented,riskLevel:b,impacts:u.map(c=>({artifact:c.node.name,type:n.getDisplayGroupName(c.node.table).toLowerCase(),file:c.node.source.file,hop:c.hop,path:c.path,via:c.viaEdge.via??c.viaEdge.type,edgeType:c.viaEdge.type,state:c.node.action}))},null,2));break}case"csv":{console.log("Artifact,Type,File,Hop,Via,State,Path"),u.forEach(g=>{let b=g.viaEdge.via??g.viaEdge.type,c=g.path.join(" > ");console.log(`"${g.node.name}","${n.getDisplayGroupName(g.node.table).toLowerCase()}","${g.node.source.file}",${g.hop},"${b}","${g.node.action}","${c}"`)});break}default:{if(m.length>0){console.log(a.default.bold(`
20
+ \u26A1 DIRECT IMPACT`)+a.default.gray(" (Hop 1)"));let A=new V.default({head:[a.default.cyan("Artifact"),a.default.cyan("Type"),a.default.cyan("File"),a.default.cyan("Via"),a.default.cyan("State")],wordWrap:!0,style:{head:[],border:["gray"]}});m.forEach(T=>{let D=we(T),k=T.node.action==="DELETE"?a.default.red:a.default.green;A.push([a.default.white(T.node.name),a.default.gray(n.getDisplayGroupName(T.node.table).toLowerCase()),a.default.gray(T.node.source.file||"\u2014"),a.default.gray(D),k(T.node.action==="DELETE"?"DELETE":"ACTIVE")])}),console.log(A.toString())}if(v.length>0&&!e.directOnly){console.log(a.default.bold(`
21
+ \u{1F30A} TRANSITIVE IMPACT`)+a.default.gray(" (Hop 2+)"));let A=[a.default.cyan("Artifact"),a.default.cyan("Type"),a.default.cyan("File"),a.default.cyan("Hop"),a.default.cyan("State")];e.showPaths&&A.push(a.default.cyan("Path"));let T=new V.default({head:A,wordWrap:!0,style:{head:[],border:["gray"]}});v.forEach(D=>{let k=D.node.action==="DELETE"?a.default.red:a.default.green,K=[a.default.white(D.node.name),a.default.gray(n.getDisplayGroupName(D.node.table).toLowerCase()),a.default.gray(D.node.source.file||"\u2014"),a.default.yellow(String(D.hop)),k(D.node.action==="DELETE"?"DELETE":"ACTIVE")];e.showPaths&&K.push(a.default.gray(D.path.join(" \u2192 "))),T.push(K)}),console.log(T.toString())}console.log(a.default.yellow(`
22
+ \u{1F4CA} IMPACT SUMMARY`));let g=u.length,b=g<3?"LOW":g<8?"MEDIUM":"HIGH",c=b==="LOW"?a.default.green:b==="MEDIUM"?a.default.yellow:a.default.red;console.log(a.default.gray("Total Impacted: ")+a.default.cyan(g)+a.default.gray(` (${m.length} direct, ${v.length} transitive)`)),console.log(a.default.gray("Max Depth Reached: ")+a.default.cyan(f.maxHopReached)),console.log(a.default.gray("Risk Level: ")+c.bold(b)),console.log(a.default.gray("\u2500".repeat(80))+`
23
+ `),b==="HIGH"?console.log(a.default.yellow("\u26A0\uFE0F High impact \u2014 review carefully before deploying")):b==="MEDIUM"&&console.log(a.default.yellow("\u{1F4A1} Moderate impact"));break}}}catch(r){t?console.log(JSON.stringify({error:r.message??"Unexpected error",code:"UNEXPECTED_ERROR"})):console.error(a.default.red(`
24
+ \u274C Error:`),r.message),process.exit(1)}});function we(o){let e=o.viaEdge;return e.type==="schema_relationship"?`${e.via??"ref"} (ref field)`:e.via??e.type}var j=E(require("chalk"));function de(){console.log(j.default.cyan(`
25
+ \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2557 \u2588\u2588\u2557 \u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2588\u2557 \u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2557 \u2588\u2588\u2557
26
+ \u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D\u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2551\u255A\u2550\u2550\u2588\u2588\u2554\u2550\u2550\u255D\u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D \u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2551 \u2588\u2588\u2551
27
+ \u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2554\u2588\u2588\u2557 \u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2551\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2551
28
+ \u2588\u2588\u2554\u2550\u2550\u255D \u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u255D \u2588\u2588\u2551\u255A\u2588\u2588\u2557\u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2551
29
+ \u2588\u2588\u2551 \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u255A\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2551 \u255A\u2588\u2588\u2588\u2588\u2551 \u2588\u2588\u2551 \u255A\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551
30
+ \u255A\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u2550\u255D\u255A\u2550\u255D \u255A\u2550\u2550\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u255D\u255A\u2550\u255D \u255A\u2550\u255D\u255A\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u255D
31
+ `)),console.log(j.default.gray(" Dependency visibility and impact analysis for ServiceNow Fluent SDK projects")),console.log(j.default.gray(` \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
32
+ `))}var Te=ge.default.join(__dirname,"../../package.json"),Ne=fe.default.readFileSync(Te,"utf-8"),W=JSON.parse(Ne),x=new pe.Command,De=(()=>{let o=process.argv;for(let e=0;e<o.length;e++){let t=o[e];if(t==="--format"||t==="-f")return o[e+1]==="json";if(t==="--format=json"||t==="-f=json")return!0}return!1})(),ke=!process.argv.includes("--no-banner")&&!process.argv.includes("--quiet")&&!De;ke&&de();x.name(W.name).description(W.description).version(W.version).option("--no-banner","Suppress ASCII banner");x.addCommand(se);x.addCommand(le);process.argv.length===2&&x.help();x.parse(process.argv);
package/dist/index.d.ts CHANGED
@@ -1,3 +1,117 @@
1
- export * from './extractor';
2
- export * from './types/graphs';
3
- export * from './blast/traversal';
1
+ interface ArtifactNode {
2
+ id: string;
3
+ table: string;
4
+ name: string;
5
+ label?: string;
6
+ action: 'INSERT_OR_UPDATE' | 'DELETE';
7
+ attributes: Record<string, any>;
8
+ source: {
9
+ file: string;
10
+ type: string;
11
+ };
12
+ }
13
+ interface ArtifactEdge {
14
+ from: string;
15
+ to: string;
16
+ via?: string;
17
+ type: 'reference' | 'composition' | 'dependency' | 'inheritance' | 'schema_relationship';
18
+ targetTable?: string;
19
+ childTable?: string;
20
+ }
21
+ interface GraphMetadata {
22
+ projectRoot: string;
23
+ scopeId: string;
24
+ scopeName: string;
25
+ recordCount: number;
26
+ referenceCount: number;
27
+ compositionCount: number;
28
+ generatedAt: string;
29
+ sdkVersion: string;
30
+ }
31
+ interface LineageGraph {
32
+ nodes: ArtifactNode[];
33
+ edges: ArtifactEdge[];
34
+ metadata: GraphMetadata;
35
+ }
36
+
37
+ interface TableInfo {
38
+ table: string;
39
+ pluginName: string;
40
+ isChild: boolean;
41
+ parentTable?: string;
42
+ parentVia?: string;
43
+ children: string[];
44
+ }
45
+ declare class ArtifactTypeRegistry {
46
+ private tables;
47
+ load(sdkPath: string): void;
48
+ private register;
49
+ /**
50
+ * Parse the top-level relationships map:
51
+ * { parentTable: { childTable: { via, descendant, relationships: { grandchild: ... } } } }
52
+ */
53
+ private parseRelationships;
54
+ private processChildren;
55
+ get(table: string): TableInfo | undefined;
56
+ getPluginName(table: string): string;
57
+ getDisplayGroupName(table: string): string;
58
+ isChild(table: string): boolean;
59
+ getParentTable(table: string): string | undefined;
60
+ getChildren(table: string): string[];
61
+ getTablesByPlugin(pluginName: string): string[];
62
+ getAllPluginNames(): string[];
63
+ getAllTables(): string[];
64
+ isKnown(table: string): boolean;
65
+ }
66
+
67
+ declare function extractLineageGraph(project: any, registry: ArtifactTypeRegistry): Promise<LineageGraph>;
68
+
69
+ interface TraversalNode {
70
+ node: ArtifactNode;
71
+ hop: number;
72
+ /** Chain of node names from target → this node (inclusive). */
73
+ path: string[];
74
+ /** The edge that caused this node to be discovered. */
75
+ viaEdge: ArtifactEdge;
76
+ }
77
+ interface TraversalResult {
78
+ /** The representative local graph node for the target (non-child preferred), or null if platform. */
79
+ target: ArtifactNode | null;
80
+ targetName: string;
81
+ /** All discovered nodes across all hops, sorted by hop ASC then name ASC. */
82
+ hops: TraversalNode[];
83
+ maxHopReached: number;
84
+ /** Times a visited node was skipped (cycle prevention + same-name deduplication). */
85
+ cyclesPrevented: number;
86
+ }
87
+ /**
88
+ * BFS reverse-traversal over the dependency graph.
89
+ *
90
+ * KEY DESIGN DECISIONS:
91
+ *
92
+ * 1. A logical "entity" is identified by node NAME, not by node ID.
93
+ * Multiple physical nodes can share a name (e.g., a table and all its
94
+ * field/documentation records are all named "x_my_table"). The BFS
95
+ * treats them as one logical node — once any physical node for a name
96
+ * is discovered, ALL siblings for that name are immediately marked
97
+ * visited so they never appear again.
98
+ *
99
+ * 2. The initial target is seeded with ALL physical nodes sharing the
100
+ * target name (plus the synthetic "platform:tableName" ID). This
101
+ * ensures incoming edges are found regardless of which physical node
102
+ * the edge was recorded against.
103
+ *
104
+ * 3. Edge matching uses BOTH:
105
+ * - edge.to === currentNodeId (exact physical node)
106
+ * - edge.targetTable === currentNodeName (canonical name — reliable
107
+ * for platform nodes and catches mis-targeted edges)
108
+ *
109
+ * 4. ALL edge types are traversed. Display filtering is the caller's job.
110
+ *
111
+ * @param graph The loaded LineageGraph.
112
+ * @param targetTableName The table name the user asked about.
113
+ * @param maxDepth Maximum hop depth. Silently clamped to 25.
114
+ */
115
+ declare function traverseBlastRadius(graph: LineageGraph, targetTableName: string, maxDepth: number): TraversalResult;
116
+
117
+ export { type ArtifactEdge, type ArtifactNode, type GraphMetadata, type LineageGraph, type TraversalNode, type TraversalResult, extractLineageGraph, traverseBlastRadius };
package/dist/index.js CHANGED
@@ -1,19 +1 @@
1
- "use strict";
2
- var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
- if (k2 === undefined) k2 = k;
4
- var desc = Object.getOwnPropertyDescriptor(m, k);
5
- if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
- desc = { enumerable: true, get: function() { return m[k]; } };
7
- }
8
- Object.defineProperty(o, k2, desc);
9
- }) : (function(o, m, k, k2) {
10
- if (k2 === undefined) k2 = k;
11
- o[k2] = m[k];
12
- }));
13
- var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
- for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
- };
16
- Object.defineProperty(exports, "__esModule", { value: true });
17
- __exportStar(require("./extractor"), exports);
18
- __exportStar(require("./types/graphs"), exports);
19
- __exportStar(require("./blast/traversal"), exports);
1
+ "use strict";var k=Object.create;var A=Object.defineProperty;var G=Object.getOwnPropertyDescriptor;var F=Object.getOwnPropertyNames;var M=Object.getPrototypeOf,L=Object.prototype.hasOwnProperty;var D=(t,e)=>{for(var n in e)A(t,n,{get:e[n],enumerable:!0})},x=(t,e,n,o)=>{if(e&&typeof e=="object"||typeof e=="function")for(let r of F(e))!L.call(t,r)&&r!==n&&A(t,r,{get:()=>e[r],enumerable:!(o=G(e,r))||o.enumerable});return t};var O=(t,e,n)=>(n=t!=null?k(M(t)):{},x(e||!t||!t.__esModule?A(n,"default",{value:t,enumerable:!0}):n,t)),q=t=>x(A({},"__esModule",{value:!0}),t);var z={};D(z,{extractLineageGraph:()=>$,traverseBlastRadius:()=>_});module.exports=q(z);var R=O(require("path"));function C(t){let e={};for(let[n,o]of Object.entries(t))if(o)try{let r=typeof o.getValue=="function"?o.getValue():o;(typeof r!="object"||r===null||Array.isArray(r))&&(e[n]=r)}catch{continue}return e}function V(t,e){let n=[];for(let o of t.query()){let r=o.setProperties||{},i=o.getId()?.getValue()?.toString()||"",c=o.getTable()?.toString()||"",f=r.name?.getValue?.()?.toString()||r.label?.getValue?.()?.toString()||r.sys_name?.getValue?.()?.toString()||i,m=o.getOriginalFilePath()?.toString()||"",y=m?R.default.relative(e,m):"";n.push({id:i,table:c,action:o.getAction(),name:f,attributes:C(r),source:{file:y,type:o.getOriginalSource()?.getKindName?.()||"SourceFile"}})}return n}function E(t,e,n){let o=[];for(let r of t.query()){let i=r.getId()?.getValue()?.toString(),c=r.setProperties||{},f=r.getTable()?.toString(),m=c.internal_type?.getValue?.()?.toString(),y=c.reference?.getValue?.()?.toString(),b=c.element?.getValue?.()?.toString();if(m==="reference"&&y){let g=e.find(s=>s.name===y);o.push({from:i,to:g?.id||`platform:${y}`,via:b||"reference",type:"schema_relationship"})}let u=c.table?.getValue?.()?.toString()||c.collection?.getValue?.()?.toString();if(u){let g=e.find(s=>s.name===u);o.push({from:i,to:g?.id||`platform:${u}`,via:c.table?"table":"collection",type:"dependency",targetTable:u})}if(n.getPluginName(f)==="AclPlugin"){let g=c.name?.getValue?.()?.toString();if(g&&g.includes(".")){let[s]=g.split("."),d=e.find(a=>a.name===s);o.push({from:i,to:d?.id||`platform:${s}`,via:"name",type:"dependency",targetTable:s})}}for(let[g,s]of Object.entries(c))if(Array.isArray(s))s.forEach(d=>{if(d&&d.constructor?.name==="RecordId"){let a=d.getValue?.()?.toString(),p=d.getTable?.()?.toString();a&&n.getPluginName(p)!=="TablePlugin"&&o.push({from:i,to:a,via:g,type:"dependency",targetTable:p})}});else if(s&&s.constructor?.name==="RecordId"){let d=s.getValue?.()?.toString(),a=s.getTable?.()?.toString();d&&n.getPluginName(a)!=="TablePlugin"&&o.push({from:i,to:d,via:g,type:"dependency",targetTable:a})}else if(s&&s.constructor?.name==="Record"){let d=s.getId?.()?.getValue?.()?.toString();if(d){let a=e.find(p=>p.id===d);a&&n.getPluginName(a.table)!=="TablePlugin"&&o.push({from:i,to:d,via:g,type:"dependency",targetTable:a.table})}}}return o}function T(t){let e=[];for(let n of t.query()){let o=n.getId()?.getValue()?.toString(),r=n.getRelated?.()||[];for(let i of r){let c=i.getId()?.getValue()?.toString();c&&o&&e.push({from:o,to:c,type:"composition"})}}return e}function P(t,e,n,o){let r=t.getConfig()||{},i=t.getPackage()||{};return{projectRoot:t.getRootDir()||process.cwd(),scopeId:r.scopeId||"unknown",scopeName:r.scope||"Global",recordCount:e,referenceCount:n,compositionCount:o,generatedAt:new Date().toISOString(),sdkVersion:i.dependencies?.["@servicenow/sdk"]||i.devDependencies?.["@servicenow/sdk"]||"unknown"}}async function $(t,e){let n=await t.getRecords(),o=t.getRootDir(),r=V(n,o),i=E(n,r,e),c=T(n),f=[...i,...c];return{metadata:P(t,n.query().length,i.length,c.length),nodes:r,edges:f}}function H(t){let e=new Map;for(let n of t.nodes){let o=e.get(n.name)??[];o.push(n.id),e.set(n.name,o)}return e}function _(t,e,n){let o=Math.min(n,25),r=H(t),i=t.nodes.filter(a=>a.name===e),c=i[0]??null,f=new Set,m=`platform:${e}`;f.add(m);for(let a of i)f.add(a.id);let b=[...new Set([m,...i.map(a=>a.id)])].map(a=>({nodeId:a,nodeName:e,hop:0,path:[e]})),u=[],g=new Set([e]),s=0,d=0;for(;b.length>0;){let a=[];for(let p of b){if(p.hop>=o)continue;let w=t.edges.filter(N=>N.to===p.nodeId||p.nodeName.length>0&&N.targetTable===p.nodeName);for(let N of w){let l=t.nodes.find(h=>h.id===N.from);if(!l)continue;if(f.has(l.id)){d++;continue}f.add(l.id);let I=r.get(l.name)??[];for(let h of I)f.has(h)||f.add(h);let S=p.hop+1,v=[...p.path,l.name];g.has(l.name)||(g.add(l.name),u.push({node:l,hop:S,path:v,viaEdge:N}));for(let h of I)a.push({nodeId:h,nodeName:l.name,hop:S,path:v});S>s&&(s=S)}}b=a}return u.sort((a,p)=>a.hop-p.hop||a.node.name.localeCompare(p.node.name)),{target:c,targetName:e,hops:u,maxHopReached:s,cyclesPrevented:d}}0&&(module.exports={extractLineageGraph,traverseBlastRadius});
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yesprasad/fluent-graph",
3
- "version": "0.2.2",
3
+ "version": "0.2.3",
4
4
  "description": "Dependency visibility and impact analysis for ServiceNow Fluent SDK projects",
5
5
  "bin": {
6
6
  "fluent-graph": "./bin/fluent-graph.js"
@@ -12,7 +12,8 @@
12
12
  "bin"
13
13
  ],
14
14
  "scripts": {
15
- "build": "tsc && chmod +x bin/fluent-graph.js",
15
+ "build": "tsup && chmod +x bin/fluent-graph.js",
16
+ "typecheck": "tsc --noEmit",
16
17
  "dev": "tsx src/cli/index.ts",
17
18
  "prepublishOnly": "npm run build"
18
19
  },
@@ -40,17 +41,16 @@
40
41
  "chalk": "^4.1.2",
41
42
  "cli-table3": "^0.6.5",
42
43
  "commander": "^12.0.0",
44
+ "login": "^0.8.0",
43
45
  "ora": "^5.4.1"
44
46
  },
45
47
  "devDependencies": {
46
48
  "@types/node": "^20.19.39",
49
+ "tsup": "^8.5.1",
47
50
  "tsx": "^4.0.0",
48
51
  "typescript": "^5.0.0"
49
52
  },
50
53
  "publishConfig": {
51
54
  "access": "public"
52
- },
53
- "peerDependencies": {
54
- "@servicenow/sdk": "4.5.0"
55
55
  }
56
56
  }
@@ -1,48 +0,0 @@
1
- import type { LineageGraph, ArtifactNode, ArtifactEdge } from '../types/graphs';
2
- export interface TraversalNode {
3
- node: ArtifactNode;
4
- hop: number;
5
- /** Chain of node names from target → this node (inclusive). */
6
- path: string[];
7
- /** The edge that caused this node to be discovered. */
8
- viaEdge: ArtifactEdge;
9
- }
10
- export interface TraversalResult {
11
- /** The representative local graph node for the target (non-child preferred), or null if platform. */
12
- target: ArtifactNode | null;
13
- targetName: string;
14
- /** All discovered nodes across all hops, sorted by hop ASC then name ASC. */
15
- hops: TraversalNode[];
16
- maxHopReached: number;
17
- /** Times a visited node was skipped (cycle prevention + same-name deduplication). */
18
- cyclesPrevented: number;
19
- }
20
- /**
21
- * BFS reverse-traversal over the dependency graph.
22
- *
23
- * KEY DESIGN DECISIONS:
24
- *
25
- * 1. A logical "entity" is identified by node NAME, not by node ID.
26
- * Multiple physical nodes can share a name (e.g., a table and all its
27
- * field/documentation records are all named "x_my_table"). The BFS
28
- * treats them as one logical node — once any physical node for a name
29
- * is discovered, ALL siblings for that name are immediately marked
30
- * visited so they never appear again.
31
- *
32
- * 2. The initial target is seeded with ALL physical nodes sharing the
33
- * target name (plus the synthetic "platform:tableName" ID). This
34
- * ensures incoming edges are found regardless of which physical node
35
- * the edge was recorded against.
36
- *
37
- * 3. Edge matching uses BOTH:
38
- * - edge.to === currentNodeId (exact physical node)
39
- * - edge.targetTable === currentNodeName (canonical name — reliable
40
- * for platform nodes and catches mis-targeted edges)
41
- *
42
- * 4. ALL edge types are traversed. Display filtering is the caller's job.
43
- *
44
- * @param graph The loaded LineageGraph.
45
- * @param targetTableName The table name the user asked about.
46
- * @param maxDepth Maximum hop depth. Silently clamped to 25.
47
- */
48
- export declare function traverseBlastRadius(graph: LineageGraph, targetTableName: string, maxDepth: number): TraversalResult;
@@ -1,141 +0,0 @@
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
- }
@@ -1,2 +0,0 @@
1
- import { Command } from 'commander';
2
- export declare const blastCommand: Command;