infernoflow 0.32.7 → 0.32.9

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.
Files changed (78) hide show
  1. package/dist/bin/infernoflow.mjs +84 -255
  2. package/dist/lib/adopters/angular.mjs +1 -128
  3. package/dist/lib/adopters/css.mjs +1 -111
  4. package/dist/lib/adopters/react.mjs +1 -104
  5. package/dist/lib/ai/ideDetection.mjs +1 -31
  6. package/dist/lib/ai/localProvider.mjs +1 -88
  7. package/dist/lib/ai/providerRouter.mjs +2 -295
  8. package/dist/lib/commands/adopt.mjs +20 -869
  9. package/dist/lib/commands/adoptWizard.mjs +9 -320
  10. package/dist/lib/commands/agent.mjs +5 -191
  11. package/dist/lib/commands/ai.mjs +2 -407
  12. package/dist/lib/commands/audit.mjs +13 -300
  13. package/dist/lib/commands/changelog.mjs +26 -594
  14. package/dist/lib/commands/check.mjs +3 -184
  15. package/dist/lib/commands/ci.mjs +3 -208
  16. package/dist/lib/commands/claudeMd.mjs +25 -130
  17. package/dist/lib/commands/cloud.mjs +5 -521
  18. package/dist/lib/commands/context.mjs +31 -287
  19. package/dist/lib/commands/coverage.mjs +2 -282
  20. package/dist/lib/commands/dashboard.mjs +123 -635
  21. package/dist/lib/commands/demo.mjs +8 -465
  22. package/dist/lib/commands/diff.mjs +5 -274
  23. package/dist/lib/commands/docGate.mjs +2 -81
  24. package/dist/lib/commands/doctor.mjs +3 -321
  25. package/dist/lib/commands/explain.mjs +8 -438
  26. package/dist/lib/commands/export.mjs +10 -239
  27. package/dist/lib/commands/generateSkills.mjs +38 -163
  28. package/dist/lib/commands/graph.mjs +203 -320
  29. package/dist/lib/commands/health.mjs +2 -309
  30. package/dist/lib/commands/impact.mjs +2 -325
  31. package/dist/lib/commands/implement.mjs +7 -103
  32. package/dist/lib/commands/init.mjs +23 -475
  33. package/dist/lib/commands/installCursorHooks.mjs +1 -36
  34. package/dist/lib/commands/installVsCodeCopilotHooks.mjs +1 -37
  35. package/dist/lib/commands/link.mjs +2 -342
  36. package/dist/lib/commands/monorepo.mjs +4 -428
  37. package/dist/lib/commands/notify.mjs +4 -258
  38. package/dist/lib/commands/onboard.mjs +4 -296
  39. package/dist/lib/commands/prComment.mjs +2 -361
  40. package/dist/lib/commands/prImpact.mjs +2 -157
  41. package/dist/lib/commands/publish.mjs +15 -316
  42. package/dist/lib/commands/report.mjs +28 -272
  43. package/dist/lib/commands/review.mjs +9 -223
  44. package/dist/lib/commands/run.mjs +8 -336
  45. package/dist/lib/commands/scaffold.mjs +54 -419
  46. package/dist/lib/commands/scan.mjs +5 -558
  47. package/dist/lib/commands/scout.mjs +2 -291
  48. package/dist/lib/commands/setup.mjs +5 -310
  49. package/dist/lib/commands/share.mjs +13 -196
  50. package/dist/lib/commands/snapshot.mjs +3 -383
  51. package/dist/lib/commands/stability.mjs +2 -293
  52. package/dist/lib/commands/status.mjs +4 -172
  53. package/dist/lib/commands/suggest.mjs +21 -563
  54. package/dist/lib/commands/syncAuto.mjs +1 -96
  55. package/dist/lib/commands/synthesize.mjs +10 -228
  56. package/dist/lib/commands/teamSync.mjs +2 -388
  57. package/dist/lib/commands/test.mjs +6 -363
  58. package/dist/lib/commands/version.mjs +2 -282
  59. package/dist/lib/commands/vibe.mjs +7 -357
  60. package/dist/lib/commands/watch.mjs +4 -203
  61. package/dist/lib/commands/why.mjs +4 -358
  62. package/dist/lib/cursorHooksInstall.mjs +1 -60
  63. package/dist/lib/draftToolingInstall.mjs +7 -68
  64. package/dist/lib/git/detect-drift.mjs +4 -208
  65. package/dist/lib/learning/adapt.mjs +6 -101
  66. package/dist/lib/learning/observe.mjs +1 -119
  67. package/dist/lib/learning/patternDetector.mjs +1 -298
  68. package/dist/lib/learning/profile.mjs +2 -279
  69. package/dist/lib/learning/skillSynthesizer.mjs +24 -145
  70. package/dist/lib/templates/index.mjs +1 -131
  71. package/dist/lib/ui/errors.mjs +1 -142
  72. package/dist/lib/ui/output.mjs +6 -72
  73. package/dist/lib/ui/prompts.mjs +6 -147
  74. package/dist/lib/vsCodeCopilotHooksInstall.mjs +1 -42
  75. package/dist/templates/cursor/inferno-mcp-server.mjs +29 -0
  76. package/dist/templates/github-app/GITHUB_APP.md +67 -0
  77. package/dist/templates/github-app/app-manifest.json +20 -0
  78. package/package.json +1 -1
@@ -1,337 +1,220 @@
1
- /**
2
- * infernoflow graph
3
- *
4
- * Builds a capability dependency graph from scan.json.
5
- * Shows which capabilities call which — so changing one reveals its downstream impact.
6
- *
7
- * Usage:
8
- * infernoflow graph Print full dependency tree
9
- * infernoflow graph --cap auth-login Show deps for one capability (up + down)
10
- * infernoflow graph --json Machine-readable graph.json to stdout
11
- * infernoflow graph --check Warn if frozen/stable caps have new dependents
12
- */
13
-
14
- import * as fs from "node:fs";
15
- import * as path from "node:path";
16
- import { bold, cyan, gray, green, yellow, red } from "../ui/output.mjs";
17
-
18
- // ── helpers ───────────────────────────────────────────────────────────────────
19
-
20
- function loadJson(p) {
21
- try { return JSON.parse(fs.readFileSync(p, "utf8")); }
22
- catch { return null; }
23
- }
24
-
25
- function getLevel(cap) {
26
- return cap?.stability || "experimental";
27
- }
28
-
29
- const LEVEL_ICON = { frozen: "🧊", stable: "〰️ ", experimental: "🌊" };
30
- const LEVEL_COLOR = { frozen: red, stable: yellow, experimental: green };
31
-
32
- // ── graph builder ─────────────────────────────────────────────────────────────
33
-
34
- /**
35
- * Build edges: capA → capB if any function in capA calls a function in capB.
36
- *
37
- * Strategy:
38
- * 1. Build a function-name → capId index from scan data
39
- * 2. For each cap, check its calls[] against the index
40
- * 3. If a call matches a function in another cap → edge
41
- */
42
- function buildGraph(scanCaps, allCaps) {
43
- // capId { id, name, stability, functions[], calls[], services[], dbCalls[], httpCalls[] }
44
- const nodes = {};
45
- // capId → Set<capId> (edges: this cap calls that cap)
46
- const edges = {};
47
- // capId → Set<capId> (reverse: this cap is called by those caps)
48
- const reverse = {};
49
-
50
- // Build function capId index
51
- const funcIndex = {}; // functionName → capId
52
- for (const entry of scanCaps) {
53
- const capFull = allCaps.find(c => c.id === entry.id) || {};
54
- nodes[entry.id] = {
55
- id: entry.id,
56
- name: entry.name || capFull.name || capFull.title || entry.id,
57
- stability: capFull.stability || "experimental",
58
- functions: entry.codeAnalysis?.functions || [],
59
- calls: entry.codeAnalysis?.calls || [],
60
- services: entry.codeAnalysis?.services || [],
61
- dbCalls: entry.codeAnalysis?.dbCalls || [],
62
- httpCalls: entry.codeAnalysis?.httpCalls || [],
63
- };
64
- edges[entry.id] = new Set();
65
- reverse[entry.id] = new Set();
66
-
67
- for (const fn of (entry.codeAnalysis?.functions || [])) {
68
- const bare = fn.replace(/\(\)$/, "");
69
- funcIndex[bare] = entry.id;
70
- funcIndex[bare.toLowerCase()] = entry.id;
71
- }
72
- }
73
-
74
- // Build edges from calls[]
75
- for (const [capId, node] of Object.entries(nodes)) {
76
- for (const call of node.calls) {
77
- const bare = call.replace(/\(\)$/, "");
78
- const target = funcIndex[bare] || funcIndex[bare.toLowerCase()];
79
- if (target && target !== capId) {
80
- edges[capId].add(target);
81
- reverse[target].add(capId);
1
+ import*as E from"node:fs";import*as x from"node:path";import{bold as u,cyan as z,gray as c,green as A,yellow as $,red as k}from"../ui/output.mjs";function S(t){try{return JSON.parse(E.readFileSync(t,"utf8"))}catch{return null}}function B(t){return t?.stability||"experimental"}const w={frozen:"\u{1F9CA}",stable:"\u3030\uFE0F ",experimental:"\u{1F30A}"},C={frozen:k,stable:$,experimental:A};function T(t,i){const l={},a={},d={},n=new Map;for(const e of t){const r=i.find(o=>o.id===e.id)||{};l[e.id]={id:e.id,name:e.name||r.name||r.title||e.id,stability:r.stability||"experimental",functions:e.codeAnalysis?.functions||[],calls:e.codeAnalysis?.calls||[],services:e.codeAnalysis?.services||[],dbCalls:e.codeAnalysis?.dbCalls||[],httpCalls:e.codeAnalysis?.httpCalls||[]},a[e.id]=new Set,d[e.id]=new Set;for(const o of e.codeAnalysis?.functions||[]){const s=o.replace(/\(\)$/,"");n.set(s,e.id),n.set(s.toLowerCase(),e.id)}}for(const[e,r]of Object.entries(l))for(const o of r.calls){const s=o.replace(/\(\)$/,""),g=n.get(s)||n.get(s.toLowerCase());g&&g!==e&&(a[e].add(g),d[g].add(e))}const f={},p={};for(const e of Object.keys(l))f[e]=[...a[e]],p[e]=[...d[e]];return{nodes:l,edges:f,reverse:p}}function O(t){const{nodes:i,edges:l,reverse:a}=t,d=Object.keys(i).sort();console.log(),console.log(u(" Capability Dependency Graph")),console.log(c(" \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")),console.log();let n=!1;for(const p of d){const e=i[p],r=l[p]||[],o=a[p]||[],s=w[e.stability]||"\u{1F30A}",g=C[e.stability]||A;if(!(r.length===0&&o.length===0)){if(n=!0,console.log(` ${s} ${u(g(p))}`),r.length>0){console.log(c(" calls \u2192"));for(const h of r){const y=i[h],j=w[y?.stability]||"\u{1F30A}";console.log(c(` ${j} ${h}`))}}if(o.length>0){console.log(c(" called by \u2190"));for(const h of o){const y=w[i[h]?.stability]||"\u{1F30A}";console.log(c(` ${y} ${h}`))}}console.log()}}n||(console.log(c(" No inter-capability dependencies detected.")),console.log(c(" Run `infernoflow scan` first to populate call data.")),console.log());const f=Object.values(t.edges).reduce((p,e)=>p+e.length,0);console.log(c(" \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")),console.log(c(` ${d.length} capabilities \xB7 ${f} dependency edge(s)`)),console.log()}function W(t,i){const{nodes:l,edges:a,reverse:d}=i,n=l[t];n||(console.error(k(`\u2717 Capability "${t}" not found in graph.`)),process.exit(1));const f=w[n.stability]||"\u{1F30A}",p=C[n.stability]||A;console.log(),console.log(u(` ${f} ${p(t)}`)+c(` (${n.stability})`)),n.services?.length&&console.log(c(" external: ")+z(n.services.join(", "))),console.log();const e=a[t]||[],r=d[t]||[];if(e.length>0){console.log(u(" Calls (downstream dependencies):"));for(const o of e){const s=l[o],g=C[s?.stability]||A,h=w[s?.stability]||"\u{1F30A}";console.log(` ${h} ${g(o)}`+c(s?.services?.length?` [${s.services.join(", ")}]`:""))}console.log()}else console.log(c(" No downstream dependencies.")),console.log();if(r.length>0){console.log(u(" Called by (upstream dependents):"));for(const o of r){const s=l[o],g=C[s?.stability]||A,h=w[s?.stability]||"\u{1F30A}";console.log(` ${h} ${g(o)}`)}console.log()}else console.log(c(" No capabilities call this one.")),console.log();if((n.stability==="frozen"||n.stability==="stable")&&r.length>0){const o=n.stability==="frozen"?k:$;console.log(o(` \u26A0 This capability is ${n.stability}. Changing it may break:`));for(const s of r)console.log(o(` \u2022 ${s}`));console.log()}}function H(t,i){const l=[];if(!t||!i)return l;for(const[a,d]of Object.entries(i.nodes)){if(d.stability==="experimental")continue;const n=new Set(t.reverse?.[a]||[]),p=[...new Set(i.reverse[a]||[])].filter(e=>!n.has(e));if(p.length>0&&l.push({type:"new-dependents",capId:a,stability:d.stability,detail:`${p.join(", ")} now depend on this`}),d.stability==="frozen"){const e=new Set(t.edges?.[a]||[]),r=new Set(i.edges[a]||[]),o=[...r].filter(g=>!e.has(g)),s=[...e].filter(g=>!r.has(g));(o.length>0||s.length>0)&&l.push({type:"frozen-internals-changed",capId:a,stability:d.stability,detail:[o.length?`added calls: ${o.join(", ")}`:"",s.length?`removed calls: ${s.join(", ")}`:""].filter(Boolean).join("; ")})}}return l}function M(t){const i=t.nodes||{},l=t.deps||{},a=Object.keys(i),d=[];for(const[f,p]of Object.entries(l))for(const e of p)d.push({from:f,to:e});return`<!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width,initial-scale=1">
6
+ <title>infernoflow \u2014 Capability Graph</title>
7
+ <style>
8
+ *{box-sizing:border-box;margin:0;padding:0}
9
+ body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;background:#0f1117;color:#e2e8f0;height:100vh;display:flex;flex-direction:column}
10
+ header{padding:14px 20px;border-bottom:1px solid #1e2535;display:flex;align-items:center;gap:12px;background:#0b0e18}
11
+ header h1{font-size:14px;font-weight:700;color:#f1f5f9;letter-spacing:.02em}
12
+ header span{font-size:12px;color:#64748b}
13
+ #stats{margin-left:auto;font-size:11px;color:#475569}
14
+ #canvas{flex:1;position:relative;overflow:hidden}
15
+ svg{width:100%;height:100%}
16
+ .node{cursor:pointer}
17
+ .node rect{rx:8;ry:8;transition:filter .15s}
18
+ .node rect:hover{filter:brightness(1.2)}
19
+ .node text{font-size:11px;font-weight:600;pointer-events:none;text-anchor:middle;dominant-baseline:middle}
20
+ .edge{fill:none;stroke:#334155;stroke-width:1.5;transition:stroke .15s,stroke-width .15s}
21
+ .edge.active{stroke:#f97316;stroke-width:2.5}
22
+ .edge.dim{stroke:#1e2535;stroke-width:1}
23
+ .tooltip{position:absolute;background:#1e2535;border:1px solid #334155;border-radius:10px;padding:12px 16px;font-size:12px;pointer-events:none;opacity:0;transition:opacity .15s;box-shadow:0 8px 24px rgba(0,0,0,.5);max-width:220px;color:#e2e8f0;z-index:10}
24
+ .tooltip strong{display:block;margin-bottom:6px;font-size:13px;color:#f1f5f9}
25
+ .tag{display:inline-block;background:#0f1117;border:1px solid #334155;border-radius:4px;padding:2px 7px;margin:2px 2px 2px 0;font-size:10px;color:#94a3b8}
26
+ .tag.calls{border-color:#6366f1;color:#a5b4fc}
27
+ .tag.callers{border-color:#10b981;color:#6ee7b7}
28
+ #legend{position:absolute;bottom:16px;left:20px;display:flex;gap:14px;font-size:11px;color:#64748b}
29
+ .ldot{width:10px;height:10px;border-radius:3px;display:inline-block;margin-right:5px;vertical-align:middle}
30
+ </style>
31
+ </head>
32
+ <body>
33
+ <header>
34
+ <span style="font-size:18px">\u{1F525}</span>
35
+ <h1>infernoflow \u2014 Capability Graph</h1>
36
+ <span id="project-name"></span>
37
+ <span id="stats"></span>
38
+ </header>
39
+ <div id="canvas">
40
+ <svg id="svg">
41
+ <defs>
42
+ <marker id="arr" markerWidth="8" markerHeight="8" refX="6" refY="3" orient="auto"><path d="M0,0 L0,6 L8,3 z" fill="#334155"/></marker>
43
+ <marker id="arr-hi" markerWidth="8" markerHeight="8" refX="6" refY="3" orient="auto"><path d="M0,0 L0,6 L8,3 z" fill="#f97316"/></marker>
44
+ </defs>
45
+ </svg>
46
+ <div class="tooltip" id="tip"></div>
47
+ <div id="legend">
48
+ <span><span class="ldot" style="background:#6366f180;border:2px solid #6366f1"></span>Has dependencies</span>
49
+ <span><span class="ldot" style="background:#10b98180;border:2px solid #10b981"></span>View layer</span>
50
+ <span><span class="ldot" style="background:#33415580;border:2px solid #475569"></span>Independent</span>
51
+ </div>
52
+ </div>
53
+ <script>
54
+ const DATA = ${JSON.stringify({nodes:a,edges:d})};
55
+ const NW=120, NH=34;
56
+
57
+ // Separate connected vs isolated nodes
58
+ const connectedSet = new Set([...DATA.edges.map(e=>e.from),...DATA.edges.map(e=>e.to)]);
59
+ const connected = DATA.nodes.filter(id=>connectedSet.has(id));
60
+ const isolated = DATA.nodes.filter(id=>!connectedSet.has(id));
61
+
62
+ // Layout connected nodes using basic force-inspired placement
63
+ const W = ()=>document.getElementById("canvas").offsetWidth || 900;
64
+ const H = ()=>document.getElementById("canvas").offsetHeight || 600;
65
+
66
+ function layout() {
67
+ const w=W(), h=H();
68
+ const pos = {};
69
+ // Find sources (no incoming edges)
70
+ const hasIncoming = new Set(DATA.edges.map(e=>e.to));
71
+ const sources = connected.filter(id=>!hasIncoming.has(id));
72
+ const layers = [];
73
+ const assigned = new Set();
74
+
75
+ // BFS layering
76
+ let layer = sources.filter(id=>connected.includes(id));
77
+ while (layer.length > 0) {
78
+ layers.push(layer);
79
+ layer.forEach(id=>assigned.add(id));
80
+ const next = [];
81
+ for (const id of layer) {
82
+ for (const {from,to} of DATA.edges) {
83
+ if (from===id && !assigned.has(to)) { next.push(to); assigned.add(to); }
82
84
  }
83
85
  }
86
+ layer = [...new Set(next)];
84
87
  }
85
-
86
- // Serialise Sets to arrays
87
- const serialisedEdges = {};
88
- const serialisedReverse = {};
89
- for (const id of Object.keys(nodes)) {
90
- serialisedEdges[id] = [...edges[id]];
91
- serialisedReverse[id] = [...reverse[id]];
92
- }
93
-
94
- return { nodes, edges: serialisedEdges, reverse: serialisedReverse };
88
+ // Any connected nodes not yet assigned
89
+ connected.filter(id=>!assigned.has(id)).forEach(id=>layers[layers.length-1||0]?.push(id)||layers.push([id]));
90
+
91
+ const layerH = Math.min(140, (h-180) / Math.max(layers.length,1));
92
+ layers.forEach((layer,li) => {
93
+ const y = 60 + li*layerH;
94
+ const totalW = layer.length*(NW+24)-24;
95
+ const startX = (w-totalW)/2;
96
+ layer.forEach((id,i)=>{
97
+ pos[id] = { x: startX+i*(NW+24), y };
98
+ });
99
+ });
100
+
101
+ // Isolated nodes in a grid at bottom
102
+ const cols = Math.max(1, Math.floor((w-40)/(NW+8)));
103
+ isolated.forEach((id,i)=>{
104
+ const col=i%cols, row=Math.floor(i/cols);
105
+ pos[id]={ x:20+col*(NW+8), y:h-120+row*40 };
106
+ });
107
+
108
+ return pos;
95
109
  }
96
110
 
97
- // ── terminal reporters ────────────────────────────────────────────────────────
98
-
99
- function printFullGraph(graph) {
100
- const { nodes, edges, reverse } = graph;
101
- const ids = Object.keys(nodes).sort();
111
+ function render() {
112
+ const svg = document.getElementById("svg");
113
+ svg.innerHTML = \`<defs>
114
+ <marker id="arr" markerWidth="8" markerHeight="8" refX="6" refY="3" orient="auto"><path d="M0,0 L0,6 L8,3 z" fill="#334155"/></marker>
115
+ <marker id="arr-hi" markerWidth="8" markerHeight="8" refX="6" refY="3" orient="auto"><path d="M0,0 L0,6 L8,3 z" fill="#f97316"/></marker>
116
+ </defs>\`;
102
117
 
103
- console.log();
104
- console.log(bold(" Capability Dependency Graph"));
105
- console.log(gray(" ────────────────────────────────────────────────────────────"));
106
- console.log();
118
+ const pos = layout();
107
119
 
108
- let hasDeps = false;
109
- for (const id of ids) {
110
- const node = nodes[id];
111
- const deps = edges[id] || [];
112
- const callers = reverse[id] || [];
113
- const icon = LEVEL_ICON[node.stability] || "🌊";
114
- const color = LEVEL_COLOR[node.stability] || green;
115
-
116
- if (deps.length === 0 && callers.length === 0) continue;
117
- hasDeps = true;
118
-
119
- console.log(` ${icon} ${bold(color(id))}`);
120
-
121
- if (deps.length > 0) {
122
- console.log(gray(" calls →"));
123
- for (const dep of deps) {
124
- const depNode = nodes[dep];
125
- const depIcon = LEVEL_ICON[depNode?.stability] || "🌊";
126
- console.log(gray(` ${depIcon} ${dep}`));
127
- }
128
- }
129
- if (callers.length > 0) {
130
- console.log(gray(" called by ←"));
131
- for (const caller of callers) {
132
- const callerIcon = LEVEL_ICON[nodes[caller]?.stability] || "🌊";
133
- console.log(gray(` ${callerIcon} ${caller}`));
134
- }
135
- }
136
- console.log();
137
- }
138
-
139
- if (!hasDeps) {
140
- console.log(gray(" No inter-capability dependencies detected."));
141
- console.log(gray(" Run `infernoflow scan` first to populate call data."));
142
- console.log();
120
+ function nodeColor(id) {
121
+ if (!connectedSet.has(id)) return {fill:"#33415540",stroke:"#475569",text:"#94a3b8"};
122
+ if (id.startsWith("View")) return {fill:"#10b98120",stroke:"#10b981",text:"#6ee7b7"};
123
+ return {fill:"#6366f120",stroke:"#6366f1",text:"#a5b4fc"};
143
124
  }
144
125
 
145
- // Summary stats
146
- const totalEdges = Object.values(graph.edges).reduce((n, arr) => n + arr.length, 0);
147
- console.log(gray(` ────────────────────────────────────────────────────────────`));
148
- console.log(gray(` ${ids.length} capabilities · ${totalEdges} dependency edge(s)`));
149
- console.log();
150
- }
151
-
152
- function printCapGraph(capId, graph) {
153
- const { nodes, edges, reverse } = graph;
154
- const node = nodes[capId];
155
- if (!node) {
156
- console.error(red(`✗ Capability "${capId}" not found in graph.`));
157
- process.exit(1);
158
- }
159
-
160
- const icon = LEVEL_ICON[node.stability] || "🌊";
161
- const color = LEVEL_COLOR[node.stability] || green;
162
-
163
- console.log();
164
- console.log(bold(` ${icon} ${color(capId)}`) + gray(` (${node.stability})`));
165
- if (node.services?.length) console.log(gray(` external: `) + cyan(node.services.join(", ")));
166
- console.log();
167
-
168
- const deps = edges[capId] || [];
169
- const callers = reverse[capId] || [];
170
-
171
- if (deps.length > 0) {
172
- console.log(bold(" Calls (downstream dependencies):"));
173
- for (const dep of deps) {
174
- const d = nodes[dep];
175
- const dColor = LEVEL_COLOR[d?.stability] || green;
176
- const dIcon = LEVEL_ICON[d?.stability] || "🌊";
177
- console.log(` ${dIcon} ${dColor(dep)}` + gray(d?.services?.length ? ` [${d.services.join(", ")}]` : ""));
126
+ // Draw edges first
127
+ DATA.edges.forEach(({from,to})=>{
128
+ const f=pos[from], t=pos[to];
129
+ if (!f||!t) return;
130
+ const fx=f.x+NW/2, fy=f.y+NH/2, tx=t.x+NW/2, ty=t.y+NH/2;
131
+ const dx=tx-fx, dy=ty-fy, len=Math.sqrt(dx*dx+dy*dy)||1;
132
+ const sx=fx+(dx/len)*(NW/2+4), sy=fy+(dy/len)*(NH/2+4);
133
+ const ex=tx-(dx/len)*(NW/2+8), ey=ty-(dy/len)*(NH/2+8);
134
+ const mx=(sx+ex)/2-dy*0.18, my=(sy+ey)/2+dx*0.18;
135
+ const p=document.createElementNS("http://www.w3.org/2000/svg","path");
136
+ p.setAttribute("d",\`M\${sx},\${sy} Q\${mx},\${my} \${ex},\${ey}\`);
137
+ p.setAttribute("class","edge");
138
+ p.setAttribute("marker-end","url(#arr)");
139
+ p.dataset.from=from; p.dataset.to=to;
140
+ svg.appendChild(p);
141
+ });
142
+
143
+ // Draw nodes
144
+ DATA.nodes.forEach(id=>{
145
+ const p=pos[id]; if(!p) return;
146
+ const c=nodeColor(id);
147
+ const isSmall=!connectedSet.has(id);
148
+ const w=isSmall?NW:NW, h=isSmall?26:NH;
149
+
150
+ const g=document.createElementNS("http://www.w3.org/2000/svg","g");
151
+ g.setAttribute("class","node");
152
+ g.setAttribute("transform",\`translate(\${p.x},\${p.y})\`);
153
+
154
+ const rect=document.createElementNS("http://www.w3.org/2000/svg","rect");
155
+ rect.setAttribute("width",w); rect.setAttribute("height",h);
156
+ rect.setAttribute("rx",isSmall?5:8);
157
+ rect.setAttribute("fill",c.fill);
158
+ rect.setAttribute("stroke",c.stroke);
159
+ rect.setAttribute("stroke-width",isSmall?"1.5":"2");
160
+
161
+ const txt=document.createElementNS("http://www.w3.org/2000/svg","text");
162
+ txt.setAttribute("x",w/2); txt.setAttribute("y",h/2);
163
+ txt.setAttribute("fill",c.text);
164
+ txt.setAttribute("font-size",isSmall?"9":"11");
165
+ txt.setAttribute("font-weight","600");
166
+ txt.setAttribute("text-anchor","middle");
167
+ txt.setAttribute("dominant-baseline","middle");
168
+ // Shorten long names for display
169
+ let label=id;
170
+ if(label.length>15) {
171
+ label=label.replace(/([A-Z])/g,(m,c,o)=>o>0?' '+m:m);
172
+ if(label.length>18) label=label.replace('Complete Pending On','CPO ').replace('Advance Repeat','AdvRpt ');
173
+ if(label.length>18) label=id.replace(/([A-Z][a-z]+)/g,w=>w.slice(0,3)).slice(0,16);
178
174
  }
179
- console.log();
180
- } else {
181
- console.log(gray(" No downstream dependencies."));
182
- console.log();
183
- }
175
+ txt.textContent=label;
184
176
 
185
- if (callers.length > 0) {
186
- console.log(bold(" Called by (upstream dependents):"));
187
- for (const caller of callers) {
188
- const c = nodes[caller];
189
- const cColor = LEVEL_COLOR[c?.stability] || green;
190
- const cIcon = LEVEL_ICON[c?.stability] || "🌊";
191
- console.log(` ${cIcon} ${cColor(caller)}`);
192
- }
193
- console.log();
194
- } else {
195
- console.log(gray(" No capabilities call this one."));
196
- console.log();
197
- }
177
+ g.appendChild(rect); g.appendChild(txt);
178
+ g.addEventListener("mouseenter",e=>showTip(id,e));
179
+ g.addEventListener("mouseleave",hideTip);
180
+ svg.appendChild(g);
181
+ });
198
182
 
199
- // Impact warning for frozen/stable
200
- if ((node.stability === "frozen" || node.stability === "stable") && callers.length > 0) {
201
- const color2 = node.stability === "frozen" ? red : yellow;
202
- console.log(color2(` ⚠ This capability is ${node.stability}. Changing it may break:`));
203
- for (const caller of callers) console.log(color2(` • ${caller}`));
204
- console.log();
205
- }
183
+ document.getElementById("stats").textContent = \`\${DATA.nodes.length} capabilities \xB7 \${DATA.edges.length} edges\`;
206
184
  }
207
185
 
208
- // ── breaking change checker ───────────────────────────────────────────────────
209
-
210
- /**
211
- * Compare previous graph.json with new graph to detect:
212
- * - frozen/stable caps that have gained new callers (more dependents = higher risk)
213
- * - frozen caps that have new outgoing deps (their internals changed)
214
- */
215
- function checkBreakingChanges(prevGraph, newGraph) {
216
- const warnings = [];
217
- if (!prevGraph || !newGraph) return warnings;
218
-
219
- for (const [capId, node] of Object.entries(newGraph.nodes)) {
220
- if (node.stability === "experimental") continue;
221
-
222
- const prevCallers = new Set(prevGraph.reverse?.[capId] || []);
223
- const newCallers = new Set(newGraph.reverse[capId] || []);
224
- const addedCallers = [...newCallers].filter(c => !prevCallers.has(c));
225
-
226
- if (addedCallers.length > 0) {
227
- warnings.push({
228
- type: "new-dependents",
229
- capId,
230
- stability: node.stability,
231
- detail: `${addedCallers.join(", ")} now depend on this`,
232
- });
233
- }
234
-
235
- if (node.stability === "frozen") {
236
- const prevDeps = new Set(prevGraph.edges?.[capId] || []);
237
- const newDeps = new Set(newGraph.edges[capId] || []);
238
- const addedDeps = [...newDeps].filter(d => !prevDeps.has(d));
239
- const removedDeps = [...prevDeps].filter(d => !newDeps.has(d));
240
-
241
- if (addedDeps.length > 0 || removedDeps.length > 0) {
242
- warnings.push({
243
- type: "frozen-internals-changed",
244
- capId,
245
- stability: node.stability,
246
- detail: [
247
- addedDeps.length ? `added calls: ${addedDeps.join(", ")}` : "",
248
- removedDeps.length ? `removed calls: ${removedDeps.join(", ")}` : "",
249
- ].filter(Boolean).join("; "),
250
- });
251
- }
252
- }
253
- }
254
-
255
- return warnings;
186
+ function showTip(id, evt) {
187
+ const calls = DATA.edges.filter(e=>e.from===id).map(e=>e.to);
188
+ const callers = DATA.edges.filter(e=>e.to===id).map(e=>e.from);
189
+ let html = \`<strong>\u{1F30A} \${id}</strong>\`;
190
+ if (calls.length) html += \`<div style="margin-top:6px">calls \u2192<br>\${calls.map(c=>\`<span class="tag calls">\${c}</span>\`).join('')}</div>\`;
191
+ if (callers.length) html += \`<div style="margin-top:6px">\u2190 called by<br>\${callers.map(c=>\`<span class="tag callers">\${c}</span>\`).join('')}</div>\`;
192
+ if (!calls.length && !callers.length) html += \`<div style="margin-top:6px;color:#475569;font-size:11px">No inter-capability dependencies</div>\`;
193
+ const tip=document.getElementById("tip");
194
+ tip.innerHTML=html; tip.style.opacity="1";
195
+ const canvas=document.getElementById("canvas").getBoundingClientRect();
196
+ let tx=evt.clientX-canvas.left+16, ty=evt.clientY-canvas.top-60;
197
+ if(tx+220>canvas.width) tx=evt.clientX-canvas.left-236;
198
+ tip.style.left=Math.max(0,tx)+"px"; tip.style.top=Math.max(0,ty)+"px";
199
+
200
+ // Highlight edges
201
+ document.querySelectorAll(".edge").forEach(p=>{
202
+ const active=p.dataset.from===id||p.dataset.to===id;
203
+ p.setAttribute("class",active?"edge active":"edge dim");
204
+ p.setAttribute("marker-end",active?"url(#arr-hi)":"url(#arr)");
205
+ });
256
206
  }
257
-
258
- // ── entry point ───────────────────────────────────────────────────────────────
259
-
260
- export async function graphCommand(rawArgs) {
261
- const args = (rawArgs || []).slice(1); // skip command name
262
- const jsonMode = args.includes("--json");
263
- const checkMode = args.includes("--check");
264
- const capIdx = args.indexOf("--cap");
265
- const capFilter = capIdx !== -1 ? args[capIdx + 1] : null;
266
-
267
- const cwd = process.cwd();
268
- const infernoDir = path.join(cwd, "inferno");
269
- const scanPath = path.join(infernoDir, "scan.json");
270
- const graphPath = path.join(infernoDir, "graph.json");
271
- const capsPath = path.join(infernoDir, "capabilities.json");
272
-
273
- // Load scan data
274
- const scan = loadJson(scanPath);
275
- if (!scan) {
276
- console.error(red("✗ inferno/scan.json not found — run `infernoflow scan` first."));
277
- process.exit(1);
278
- }
279
-
280
- // Load capabilities (for stability info)
281
- let allCaps = [];
282
- const rawCaps = loadJson(capsPath);
283
- if (rawCaps) allCaps = Array.isArray(rawCaps) ? rawCaps : (rawCaps.capabilities || []);
284
-
285
- // Build graph
286
- const scanCaps = scan.capabilities || [];
287
- const graph = buildGraph(scanCaps, allCaps);
288
-
289
- // Check for breaking changes vs saved graph
290
- const prevGraph = loadJson(graphPath);
291
- const breakingWarnings = checkMode || true ? checkBreakingChanges(prevGraph, graph) : [];
292
-
293
- // Save graph.json
294
- const graphData = {
295
- builtAt: new Date().toISOString(),
296
- capabilities: Object.keys(graph.nodes).length,
297
- edges: Object.values(graph.edges).reduce((n, arr) => n + arr.length, 0),
298
- nodes: graph.nodes,
299
- deps: graph.edges,
300
- dependents: graph.reverse,
301
- };
302
-
303
- if (!jsonMode) {
304
- fs.writeFileSync(graphPath, JSON.stringify(graphData, null, 2));
305
- }
306
-
307
- // Output
308
- if (jsonMode) {
309
- console.log(JSON.stringify(graphData, null, 2));
310
- return;
311
- }
312
-
313
- if (capFilter) {
314
- printCapGraph(capFilter, graph);
315
- } else {
316
- printFullGraph(graph);
317
- }
318
-
319
- // Breaking change warnings
320
- if (breakingWarnings.length > 0) {
321
- console.log(yellow(" ⚠ Dependency changes detected:"));
322
- for (const w of breakingWarnings) {
323
- const icon = w.stability === "frozen" ? red("🧊") : yellow("〰️ ");
324
- console.log(` ${icon} ${bold(w.capId)} — ${w.detail}`);
325
- }
326
- console.log();
327
- if (checkMode) process.exit(1);
328
- }
329
-
330
- if (!jsonMode) console.log(gray(` Graph saved → inferno/graph.json`));
207
+ function hideTip(){
208
+ document.getElementById("tip").style.opacity="0";
209
+ document.querySelectorAll(".edge").forEach(p=>{
210
+ p.setAttribute("class","edge");
211
+ p.setAttribute("marker-end","url(#arr)");
212
+ });
331
213
  }
332
214
 
333
- // ── exported utility for other commands ──────────────────────────────────────
334
-
335
- export function loadGraph(infernoDir) {
336
- return loadJson(path.join(infernoDir, "graph.json"));
337
- }
215
+ render();
216
+ window.addEventListener("resize", render);
217
+ </script>
218
+ </body>
219
+ </html>`}async function F(t){const i=(t||[]).slice(1),l=i.includes("--json"),a=i.includes("--check"),d=i.includes("--html"),n=i.indexOf("--cap"),f=n!==-1?i[n+1]:null,p=process.cwd(),e=x.join(p,"inferno"),r=x.join(e,"scan.json"),o=x.join(e,"graph.json"),s=x.join(e,"capabilities.json"),g=S(r);g||(console.error(k("\u2717 inferno/scan.json not found \u2014 run `infernoflow scan` first.")),process.exit(1));let h=[];const y=S(s);y&&(h=Array.isArray(y)?y:y.capabilities||[]);const j=g.capabilities||[],m=T(j,h),L=S(o),D=H(L,m),v={builtAt:new Date().toISOString(),capabilities:Object.keys(m.nodes).length,edges:Object.values(m.edges).reduce((b,N)=>b+N.length,0),nodes:m.nodes,deps:m.edges,dependents:m.reverse};if(l||E.writeFileSync(o,JSON.stringify(v,null,2)),d){const b=x.join(e,"graph.html");E.writeFileSync(b,M(v)),console.log(c(`
220
+ infernoflow graph \u2192 HTML`)),console.log(c(" \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")),console.log(` ${u("graph.html")} written \u2192 ${z("inferno/graph.html")}`),console.log(c(` ${v.capabilities} capabilities \xB7 ${v.edges} dependency edge(s)`)),console.log(),console.log(c(" Open in browser: ")+z("inferno/graph.html")),console.log();return}if(l){console.log(JSON.stringify(v,null,2));return}if(f?W(f,m):O(m),D.length>0){console.log($(" \u26A0 Dependency changes detected:"));for(const b of D){const N=b.stability==="frozen"?k("\u{1F9CA}"):$("\u3030\uFE0F ");console.log(` ${N} ${u(b.capId)} \u2014 ${b.detail}`)}console.log(),a&&process.exit(1)}l||console.log(c(" Graph saved \u2192 inferno/graph.json"))}function X(t){return S(x.join(t,"graph.json"))}export{M as buildGraphHtml,F as graphCommand,X as loadGraph};