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.
- package/dist/bin/infernoflow.mjs +84 -255
- package/dist/lib/adopters/angular.mjs +1 -128
- package/dist/lib/adopters/css.mjs +1 -111
- package/dist/lib/adopters/react.mjs +1 -104
- package/dist/lib/ai/ideDetection.mjs +1 -31
- package/dist/lib/ai/localProvider.mjs +1 -88
- package/dist/lib/ai/providerRouter.mjs +2 -295
- package/dist/lib/commands/adopt.mjs +20 -869
- package/dist/lib/commands/adoptWizard.mjs +9 -320
- package/dist/lib/commands/agent.mjs +5 -191
- package/dist/lib/commands/ai.mjs +2 -407
- package/dist/lib/commands/audit.mjs +13 -300
- package/dist/lib/commands/changelog.mjs +26 -594
- package/dist/lib/commands/check.mjs +3 -184
- package/dist/lib/commands/ci.mjs +3 -208
- package/dist/lib/commands/claudeMd.mjs +25 -130
- package/dist/lib/commands/cloud.mjs +5 -521
- package/dist/lib/commands/context.mjs +31 -287
- package/dist/lib/commands/coverage.mjs +2 -282
- package/dist/lib/commands/dashboard.mjs +123 -635
- package/dist/lib/commands/demo.mjs +8 -465
- package/dist/lib/commands/diff.mjs +5 -274
- package/dist/lib/commands/docGate.mjs +2 -81
- package/dist/lib/commands/doctor.mjs +3 -321
- package/dist/lib/commands/explain.mjs +8 -438
- package/dist/lib/commands/export.mjs +10 -239
- package/dist/lib/commands/generateSkills.mjs +38 -163
- package/dist/lib/commands/graph.mjs +203 -320
- package/dist/lib/commands/health.mjs +2 -309
- package/dist/lib/commands/impact.mjs +2 -325
- package/dist/lib/commands/implement.mjs +7 -103
- package/dist/lib/commands/init.mjs +23 -475
- package/dist/lib/commands/installCursorHooks.mjs +1 -36
- package/dist/lib/commands/installVsCodeCopilotHooks.mjs +1 -37
- package/dist/lib/commands/link.mjs +2 -342
- package/dist/lib/commands/monorepo.mjs +4 -428
- package/dist/lib/commands/notify.mjs +4 -258
- package/dist/lib/commands/onboard.mjs +4 -296
- package/dist/lib/commands/prComment.mjs +2 -361
- package/dist/lib/commands/prImpact.mjs +2 -157
- package/dist/lib/commands/publish.mjs +15 -316
- package/dist/lib/commands/report.mjs +28 -272
- package/dist/lib/commands/review.mjs +9 -223
- package/dist/lib/commands/run.mjs +8 -336
- package/dist/lib/commands/scaffold.mjs +54 -419
- package/dist/lib/commands/scan.mjs +5 -558
- package/dist/lib/commands/scout.mjs +2 -291
- package/dist/lib/commands/setup.mjs +5 -310
- package/dist/lib/commands/share.mjs +13 -196
- package/dist/lib/commands/snapshot.mjs +3 -383
- package/dist/lib/commands/stability.mjs +2 -293
- package/dist/lib/commands/status.mjs +4 -172
- package/dist/lib/commands/suggest.mjs +21 -563
- package/dist/lib/commands/syncAuto.mjs +1 -96
- package/dist/lib/commands/synthesize.mjs +10 -228
- package/dist/lib/commands/teamSync.mjs +2 -388
- package/dist/lib/commands/test.mjs +6 -363
- package/dist/lib/commands/version.mjs +2 -282
- package/dist/lib/commands/vibe.mjs +7 -357
- package/dist/lib/commands/watch.mjs +4 -203
- package/dist/lib/commands/why.mjs +4 -358
- package/dist/lib/cursorHooksInstall.mjs +1 -60
- package/dist/lib/draftToolingInstall.mjs +7 -68
- package/dist/lib/git/detect-drift.mjs +4 -208
- package/dist/lib/learning/adapt.mjs +6 -101
- package/dist/lib/learning/observe.mjs +1 -119
- package/dist/lib/learning/patternDetector.mjs +1 -298
- package/dist/lib/learning/profile.mjs +2 -279
- package/dist/lib/learning/skillSynthesizer.mjs +24 -145
- package/dist/lib/templates/index.mjs +1 -131
- package/dist/lib/ui/errors.mjs +1 -142
- package/dist/lib/ui/output.mjs +6 -72
- package/dist/lib/ui/prompts.mjs +6 -147
- package/dist/lib/vsCodeCopilotHooksInstall.mjs +1 -42
- package/dist/templates/cursor/inferno-mcp-server.mjs +29 -0
- package/dist/templates/github-app/GITHUB_APP.md +67 -0
- package/dist/templates/github-app/app-manifest.json +20 -0
- package/package.json +1 -1
|
@@ -1,309 +1,2 @@
|
|
|
1
|
-
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
* Computes a weighted 0–100 health score for the capability contract.
|
|
5
|
-
* Breaks it down across five dimensions so you know exactly where to improve.
|
|
6
|
-
*
|
|
7
|
-
* Dimensions:
|
|
8
|
-
* Coverage % of capabilities that have descriptions (weight 25)
|
|
9
|
-
* Documentation % with at least one scenario/test (weight 20)
|
|
10
|
-
* Freshness How recently the contract was updated (weight 20)
|
|
11
|
-
* Completeness Version field, owner, tags present (weight 15)
|
|
12
|
-
* Drift risk Open issues from `infernoflow check` (weight 20)
|
|
13
|
-
*
|
|
14
|
-
* Usage:
|
|
15
|
-
* infernoflow health Print score + breakdown
|
|
16
|
-
* infernoflow health --json Machine-readable
|
|
17
|
-
* infernoflow health --fail-below 70 Exit 1 if score < 70 (CI gate)
|
|
18
|
-
* infernoflow health --watch Re-run every 30s (for terminals)
|
|
19
|
-
*/
|
|
20
|
-
|
|
21
|
-
import * as fs from "node:fs";
|
|
22
|
-
import * as path from "node:path";
|
|
23
|
-
import { execSync } from "node:child_process";
|
|
24
|
-
import { done, warn, info, bold, cyan, gray, green, yellow, red } from "../ui/output.mjs";
|
|
25
|
-
|
|
26
|
-
const WEIGHTS = {
|
|
27
|
-
coverage: 25,
|
|
28
|
-
documentation: 20,
|
|
29
|
-
freshness: 20,
|
|
30
|
-
completeness: 15,
|
|
31
|
-
drift: 20,
|
|
32
|
-
};
|
|
33
|
-
|
|
34
|
-
// ── Readers ───────────────────────────────────────────────────────────────────
|
|
35
|
-
|
|
36
|
-
function readContract(infernoDir) {
|
|
37
|
-
for (const f of ["contract.json", "capabilities.json"]) {
|
|
38
|
-
const p = path.join(infernoDir, f);
|
|
39
|
-
if (!fs.existsSync(p)) continue;
|
|
40
|
-
try { return JSON.parse(fs.readFileSync(p, "utf8")); } catch {}
|
|
41
|
-
}
|
|
42
|
-
return null;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
function readScenarios(infernoDir) {
|
|
46
|
-
const scenDir = path.join(infernoDir, "scenarios");
|
|
47
|
-
if (!fs.existsSync(scenDir)) return [];
|
|
48
|
-
return fs.readdirSync(scenDir)
|
|
49
|
-
.filter(f => f.endsWith(".json"))
|
|
50
|
-
.flatMap(f => {
|
|
51
|
-
try {
|
|
52
|
-
const data = JSON.parse(fs.readFileSync(path.join(scenDir, f), "utf8"));
|
|
53
|
-
return data.capability ? [data.capability] : (data.capabilities || []);
|
|
54
|
-
} catch { return []; }
|
|
55
|
-
});
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
function runCheck(cwd) {
|
|
59
|
-
try {
|
|
60
|
-
const out = execSync("npx infernoflow check --json", {
|
|
61
|
-
cwd, encoding: "utf8", timeout: 15_000, stdio: ["ignore", "pipe", "pipe"],
|
|
62
|
-
});
|
|
63
|
-
return JSON.parse(out);
|
|
64
|
-
} catch (err) {
|
|
65
|
-
try { return JSON.parse(err.stdout || "{}"); } catch { return {}; }
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
function lastModifiedDaysAgo(infernoDir) {
|
|
70
|
-
const files = ["contract.json", "capabilities.json"]
|
|
71
|
-
.map(f => path.join(infernoDir, f))
|
|
72
|
-
.filter(fs.existsSync);
|
|
73
|
-
if (!files.length) return 999;
|
|
74
|
-
const mtime = Math.max(...files.map(f => fs.statSync(f).mtimeMs));
|
|
75
|
-
return (Date.now() - mtime) / (1000 * 60 * 60 * 24);
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
// ── Scorers ───────────────────────────────────────────────────────────────────
|
|
79
|
-
|
|
80
|
-
function scoreCoverage(caps) {
|
|
81
|
-
if (!caps.length) return { score: 0, detail: "No capabilities" };
|
|
82
|
-
const withDesc = caps.filter(c => c.description && c.description.length > 5).length;
|
|
83
|
-
const pct = Math.round((withDesc / caps.length) * 100);
|
|
84
|
-
return {
|
|
85
|
-
score: pct,
|
|
86
|
-
detail: `${withDesc}/${caps.length} capabilities have descriptions`,
|
|
87
|
-
pct,
|
|
88
|
-
};
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
function scoreDocumentation(caps, scenarioCaps) {
|
|
92
|
-
if (!caps.length) return { score: 0, detail: "No capabilities" };
|
|
93
|
-
const scenSet = new Set(scenarioCaps.map(s => String(s).toLowerCase()));
|
|
94
|
-
const withScen = caps.filter(c => scenSet.has(c.id.toLowerCase())).length;
|
|
95
|
-
const pct = Math.round((withScen / caps.length) * 100);
|
|
96
|
-
return {
|
|
97
|
-
score: Math.min(100, pct + (scenSet.size === 0 ? 0 : 10)), // bonus for having any scenarios
|
|
98
|
-
detail: `${withScen}/${caps.length} capabilities have test scenarios`,
|
|
99
|
-
pct,
|
|
100
|
-
};
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
function scoreFreshness(daysAgo) {
|
|
104
|
-
let score;
|
|
105
|
-
let label;
|
|
106
|
-
if (daysAgo <= 1) { score = 100; label = "updated today"; }
|
|
107
|
-
else if (daysAgo <= 3) { score = 95; label = `updated ${Math.round(daysAgo)}d ago`; }
|
|
108
|
-
else if (daysAgo <= 7) { score = 85; label = `updated ${Math.round(daysAgo)}d ago`; }
|
|
109
|
-
else if (daysAgo <= 14) { score = 70; label = `updated ${Math.round(daysAgo)}d ago`; }
|
|
110
|
-
else if (daysAgo <= 30) { score = 50; label = `updated ${Math.round(daysAgo)}d ago`; }
|
|
111
|
-
else if (daysAgo <= 60) { score = 30; label = `updated ${Math.round(daysAgo)}d ago — stale`; }
|
|
112
|
-
else { score = 10; label = `not updated in ${Math.round(daysAgo)}d — very stale`; }
|
|
113
|
-
return { score, detail: label };
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
function scoreCompleteness(caps, contract) {
|
|
117
|
-
if (!caps.length) return { score: 0, detail: "No capabilities" };
|
|
118
|
-
const hasVersion = !!(contract?.version || contract?.contractVersion);
|
|
119
|
-
const withTags = caps.filter(c => c.tags?.length).length;
|
|
120
|
-
const withOwner = caps.filter(c => c.owner).length;
|
|
121
|
-
const withSince = caps.filter(c => c.since).length;
|
|
122
|
-
|
|
123
|
-
const tagPct = Math.round((withTags / caps.length) * 100);
|
|
124
|
-
const ownerPct = Math.round((withOwner / caps.length) * 100);
|
|
125
|
-
const sincePct = Math.round((withSince / caps.length) * 100);
|
|
126
|
-
|
|
127
|
-
const score = Math.round(
|
|
128
|
-
(hasVersion ? 20 : 0) +
|
|
129
|
-
tagPct * 0.3 +
|
|
130
|
-
ownerPct * 0.3 +
|
|
131
|
-
sincePct * 0.2
|
|
132
|
-
);
|
|
133
|
-
|
|
134
|
-
return {
|
|
135
|
-
score: Math.min(100, score),
|
|
136
|
-
detail: `version: ${hasVersion ? "✓" : "✗"}, tags: ${tagPct}%, owner: ${ownerPct}%, since: ${sincePct}%`,
|
|
137
|
-
};
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
function scoreDrift(checkResult) {
|
|
141
|
-
const issues = checkResult?.issues || [];
|
|
142
|
-
const warnings = issues.filter(i => (i.severity || i.level || "error") === "warning").length;
|
|
143
|
-
const errors = issues.filter(i => (i.severity || i.level || "error") === "error").length;
|
|
144
|
-
const status = checkResult?.status;
|
|
145
|
-
|
|
146
|
-
if (status === "ok" || (!errors && !warnings)) return { score: 100, detail: "No issues found" };
|
|
147
|
-
|
|
148
|
-
const score = Math.max(0, 100 - (errors * 20) - (warnings * 8));
|
|
149
|
-
return {
|
|
150
|
-
score,
|
|
151
|
-
detail: `${errors} error${errors !== 1 ? "s" : ""}, ${warnings} warning${warnings !== 1 ? "s" : ""}`,
|
|
152
|
-
};
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
// ── Aggregate ─────────────────────────────────────────────────────────────────
|
|
156
|
-
|
|
157
|
-
function computeHealth(infernoDir, cwd) {
|
|
158
|
-
const contract = readContract(infernoDir);
|
|
159
|
-
const caps = (contract?.capabilities || []).map(c =>
|
|
160
|
-
typeof c === "string" ? { id: c, description: "", tags: [], owner: "", since: "" } : c
|
|
161
|
-
);
|
|
162
|
-
const scenarioCaps = readScenarios(infernoDir);
|
|
163
|
-
const daysAgo = lastModifiedDaysAgo(infernoDir);
|
|
164
|
-
const checkResult = runCheck(cwd);
|
|
165
|
-
|
|
166
|
-
const dimensions = {
|
|
167
|
-
coverage: scoreCoverage(caps),
|
|
168
|
-
documentation: scoreDocumentation(caps, scenarioCaps),
|
|
169
|
-
freshness: scoreFreshness(daysAgo),
|
|
170
|
-
completeness: scoreCompleteness(caps, contract),
|
|
171
|
-
drift: scoreDrift(checkResult),
|
|
172
|
-
};
|
|
173
|
-
|
|
174
|
-
const totalScore = Math.round(
|
|
175
|
-
Object.entries(dimensions).reduce((sum, [key, dim]) => {
|
|
176
|
-
return sum + (dim.score * WEIGHTS[key]) / 100;
|
|
177
|
-
}, 0)
|
|
178
|
-
);
|
|
179
|
-
|
|
180
|
-
return { totalScore, dimensions, caps, daysAgo, checkResult };
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
// ── Renderer ──────────────────────────────────────────────────────────────────
|
|
184
|
-
|
|
185
|
-
function scoreColor(score) {
|
|
186
|
-
if (score >= 80) return green;
|
|
187
|
-
if (score >= 60) return yellow;
|
|
188
|
-
return red;
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
function scoreGrade(score) {
|
|
192
|
-
if (score >= 90) return "A";
|
|
193
|
-
if (score >= 80) return "B";
|
|
194
|
-
if (score >= 70) return "C";
|
|
195
|
-
if (score >= 60) return "D";
|
|
196
|
-
return "F";
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
function barOf(score, width = 30) {
|
|
200
|
-
const filled = Math.round((score / 100) * width);
|
|
201
|
-
const empty = width - filled;
|
|
202
|
-
return "█".repeat(filled) + "░".repeat(empty);
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
function printReport(totalScore, dimensions) {
|
|
206
|
-
const col = scoreColor(totalScore);
|
|
207
|
-
const grade = scoreGrade(totalScore);
|
|
208
|
-
|
|
209
|
-
console.log();
|
|
210
|
-
console.log(` ${bold("🔥 infernoflow health score")}`);
|
|
211
|
-
console.log();
|
|
212
|
-
console.log(` ${col(bold(String(totalScore)))} / 100 ${col(bold(grade))} ${col(barOf(totalScore))}`);
|
|
213
|
-
console.log();
|
|
214
|
-
|
|
215
|
-
const dimNames = {
|
|
216
|
-
coverage: "Coverage ",
|
|
217
|
-
documentation: "Docs/Tests ",
|
|
218
|
-
freshness: "Freshness ",
|
|
219
|
-
completeness: "Completeness ",
|
|
220
|
-
drift: "Drift risk ",
|
|
221
|
-
};
|
|
222
|
-
|
|
223
|
-
for (const [key, dim] of Object.entries(dimensions)) {
|
|
224
|
-
const w = WEIGHTS[key];
|
|
225
|
-
const sc = dim.score;
|
|
226
|
-
const c = scoreColor(sc);
|
|
227
|
-
const bar = barOf(sc, 20);
|
|
228
|
-
const weighted = Math.round((sc * w) / 100);
|
|
229
|
-
console.log(
|
|
230
|
-
` ${bold(dimNames[key])} ${c(String(sc).padStart(3))} ${c(bar)} ${gray(`×${w}% = ${weighted}pts ${dim.detail}`)}`
|
|
231
|
-
);
|
|
232
|
-
}
|
|
233
|
-
console.log();
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
function printTips(totalScore, dimensions) {
|
|
237
|
-
const tips = [];
|
|
238
|
-
|
|
239
|
-
if (dimensions.coverage.score < 70)
|
|
240
|
-
tips.push("Add descriptions to your capabilities in contract.json");
|
|
241
|
-
if (dimensions.documentation.score < 60)
|
|
242
|
-
tips.push("Create scenario files in inferno/scenarios/ for each capability");
|
|
243
|
-
if (dimensions.freshness.score < 70)
|
|
244
|
-
tips.push("Run `infernoflow suggest` to sync recent changes to the contract");
|
|
245
|
-
if (dimensions.completeness.score < 60)
|
|
246
|
-
tips.push("Add version, tags, owner, and since fields to your capabilities");
|
|
247
|
-
if (dimensions.drift.score < 80)
|
|
248
|
-
tips.push("Run `infernoflow check` and fix the reported issues");
|
|
249
|
-
|
|
250
|
-
if (tips.length) {
|
|
251
|
-
console.log(` ${bold("Tips to improve:")}`);
|
|
252
|
-
tips.forEach(t => console.log(` ${yellow("·")} ${t}`));
|
|
253
|
-
console.log();
|
|
254
|
-
}
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
// ── Entry ─────────────────────────────────────────────────────────────────────
|
|
258
|
-
|
|
259
|
-
export async function healthCommand(rawArgs) {
|
|
260
|
-
const args = rawArgs.slice(1);
|
|
261
|
-
const jsonMode = args.includes("--json");
|
|
262
|
-
const watchMode = args.includes("--watch");
|
|
263
|
-
const cwd = process.cwd();
|
|
264
|
-
const infernoDir = path.join(cwd, "inferno");
|
|
265
|
-
|
|
266
|
-
if (!fs.existsSync(infernoDir)) {
|
|
267
|
-
const msg = "inferno/ not found. Run: infernoflow init";
|
|
268
|
-
if (jsonMode) console.log(JSON.stringify({ ok: false, error: msg }));
|
|
269
|
-
else warn(msg);
|
|
270
|
-
process.exit(1);
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
const failBelowIdx = args.indexOf("--fail-below");
|
|
274
|
-
const failBelow = failBelowIdx !== -1 ? parseInt(args[failBelowIdx + 1], 10) : null;
|
|
275
|
-
|
|
276
|
-
const intervalIdx = args.indexOf("--interval");
|
|
277
|
-
const intervalSecs = intervalIdx !== -1 ? parseInt(args[intervalIdx + 1], 10) : 30;
|
|
278
|
-
|
|
279
|
-
const runOnce = () => {
|
|
280
|
-
const { totalScore, dimensions } = computeHealth(infernoDir, cwd);
|
|
281
|
-
|
|
282
|
-
if (jsonMode) {
|
|
283
|
-
const dimFlat = Object.fromEntries(
|
|
284
|
-
Object.entries(dimensions).map(([k, v]) => [k, { score: v.score, detail: v.detail, weight: WEIGHTS[k] }])
|
|
285
|
-
);
|
|
286
|
-
console.log(JSON.stringify({ ok: true, score: totalScore, grade: scoreGrade(totalScore), dimensions: dimFlat }));
|
|
287
|
-
} else {
|
|
288
|
-
if (watchMode) process.stdout.write("\x1Bc"); // clear screen
|
|
289
|
-
printReport(totalScore, dimensions);
|
|
290
|
-
if (!watchMode) printTips(totalScore, dimensions);
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
if (failBelow !== null && totalScore < failBelow) {
|
|
294
|
-
if (!jsonMode) console.error(red(` ✗ Score ${totalScore} is below threshold ${failBelow} — failing.\n`));
|
|
295
|
-
process.exit(1);
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
return totalScore;
|
|
299
|
-
};
|
|
300
|
-
|
|
301
|
-
if (watchMode) {
|
|
302
|
-
if (!jsonMode) info(`Watching health score every ${intervalSecs}s — press Ctrl+C to stop`);
|
|
303
|
-
runOnce();
|
|
304
|
-
setInterval(runOnce, intervalSecs * 1000);
|
|
305
|
-
await new Promise(() => {});
|
|
306
|
-
} else {
|
|
307
|
-
runOnce();
|
|
308
|
-
}
|
|
309
|
-
}
|
|
1
|
+
import*as d from"node:fs";import*as h from"node:path";import{execSync as x}from"node:child_process";import{warn as C,info as O,bold as p,gray as N,green as D,yellow as S,red as b}from"../ui/output.mjs";const m={coverage:25,documentation:20,freshness:20,completeness:15,drift:20};function k(e){for(const n of["contract.json","capabilities.json"]){const t=h.join(e,n);if(d.existsSync(t))try{return JSON.parse(d.readFileSync(t,"utf8"))}catch{}}return null}function F(e){const n=h.join(e,"scenarios");return d.existsSync(n)?d.readdirSync(n).filter(t=>t.endsWith(".json")).flatMap(t=>{try{const o=JSON.parse(d.readFileSync(h.join(n,t),"utf8"));return o.capability?[o.capability]:o.capabilities||[]}catch{return[]}}):[]}function I(e){try{const n=x("npx infernoflow check --json",{cwd:e,encoding:"utf8",timeout:15e3,stdio:["ignore","pipe","pipe"]});return JSON.parse(n)}catch(n){try{return JSON.parse(n.stdout||"{}")}catch{return{}}}}function J(e){const n=["contract.json","capabilities.json"].map(o=>h.join(e,o)).filter(d.existsSync);if(!n.length)return 999;const t=Math.max(...n.map(o=>d.statSync(o).mtimeMs));return(Date.now()-t)/(1e3*60*60*24)}function T(e){if(!e.length)return{score:0,detail:"No capabilities"};const n=e.filter(o=>o.description&&o.description.length>5).length,t=Math.round(n/e.length*100);return{score:t,detail:`${n}/${e.length} capabilities have descriptions`,pct:t}}function B(e,n){if(!e.length)return{score:0,detail:"No capabilities"};const t=new Set(n.map(s=>String(s).toLowerCase())),o=e.filter(s=>t.has(s.id.toLowerCase())).length,l=Math.round(o/e.length*100);return{score:Math.min(100,l+(t.size===0?0:10)),detail:`${o}/${e.length} capabilities have test scenarios`,pct:l}}function P(e){let n,t;return e<=1?(n=100,t="updated today"):e<=3?(n=95,t=`updated ${Math.round(e)}d ago`):e<=7?(n=85,t=`updated ${Math.round(e)}d ago`):e<=14?(n=70,t=`updated ${Math.round(e)}d ago`):e<=30?(n=50,t=`updated ${Math.round(e)}d ago`):e<=60?(n=30,t=`updated ${Math.round(e)}d ago \u2014 stale`):(n=10,t=`not updated in ${Math.round(e)}d \u2014 very stale`),{score:n,detail:t}}function R(e,n){if(!e.length)return{score:0,detail:"No capabilities"};const t=!!(n?.version||n?.contractVersion),o=e.filter(a=>a.tags?.length).length,l=e.filter(a=>a.owner).length,s=e.filter(a=>a.since).length,r=Math.round(o/e.length*100),f=Math.round(l/e.length*100),u=Math.round(s/e.length*100),c=Math.round((t?20:0)+r*.3+f*.3+u*.2);return{score:Math.min(100,c),detail:`version: ${t?"\u2713":"\u2717"}, tags: ${r}%, owner: ${f}%, since: ${u}%`}}function E(e){const n=e?.issues||[],t=n.filter(r=>(r.severity||r.level||"error")==="warning").length,o=n.filter(r=>(r.severity||r.level||"error")==="error").length;return e?.status==="ok"||!o&&!t?{score:100,detail:"No issues found"}:{score:Math.max(0,100-o*20-t*8),detail:`${o} error${o!==1?"s":""}, ${t} warning${t!==1?"s":""}`}}function W(e,n){const t=k(e),o=(t?.capabilities||[]).map(c=>typeof c=="string"?{id:c,description:"",tags:[],owner:"",since:""}:c),l=F(e),s=J(e),r=I(n),f={coverage:T(o),documentation:B(o,l),freshness:P(s),completeness:R(o,t),drift:E(r)};return{totalScore:Math.round(Object.entries(f).reduce((c,[a,i])=>c+i.score*m[a]/100,0)),dimensions:f,caps:o,daysAgo:s,checkResult:r}}function y(e){return e>=80?D:e>=60?S:b}function M(e){return e>=90?"A":e>=80?"B":e>=70?"C":e>=60?"D":"F"}function v(e,n=30){const t=Math.round(e/100*n),o=n-t;return"\u2588".repeat(t)+"\u2591".repeat(o)}function G(e,n){const t=y(e),o=M(e);console.log(),console.log(` ${p("\u{1F525} infernoflow health score")}`),console.log(),console.log(` ${t(p(String(e)))} / 100 ${t(p(o))} ${t(v(e))}`),console.log();const l={coverage:"Coverage ",documentation:"Docs/Tests ",freshness:"Freshness ",completeness:"Completeness ",drift:"Drift risk "};for(const[s,r]of Object.entries(n)){const f=m[s],u=r.score,c=y(u),a=v(u,20),i=Math.round(u*f/100);console.log(` ${p(l[s])} ${c(String(u).padStart(3))} ${c(a)} ${N(`\xD7${f}% = ${i}pts ${r.detail}`)}`)}console.log()}function H(e,n){const t=[];n.coverage.score<70&&t.push("Add descriptions to your capabilities in contract.json"),n.documentation.score<60&&t.push("Create scenario files in inferno/scenarios/ for each capability"),n.freshness.score<70&&t.push("Run `infernoflow suggest` to sync recent changes to the contract"),n.completeness.score<60&&t.push("Add version, tags, owner, and since fields to your capabilities"),n.drift.score<80&&t.push("Run `infernoflow check` and fix the reported issues"),t.length&&(console.log(` ${p("Tips to improve:")}`),t.forEach(o=>console.log(` ${S("\xB7")} ${o}`)),console.log())}async function q(e){const n=e.slice(1),t=n.includes("--json"),o=n.includes("--watch"),l=process.cwd(),s=h.join(l,"inferno");if(!d.existsSync(s)){const i="inferno/ not found. Run: infernoflow init";t?console.log(JSON.stringify({ok:!1,error:i})):C(i),process.exit(1)}const r=n.indexOf("--fail-below"),f=r!==-1?parseInt(n[r+1],10):null,u=n.indexOf("--interval"),c=u!==-1?parseInt(n[u+1],10):30,a=()=>{const{totalScore:i,dimensions:g}=W(s,l);if(t){const j=Object.fromEntries(Object.entries(g).map(([w,$])=>[w,{score:$.score,detail:$.detail,weight:m[w]}]));console.log(JSON.stringify({ok:!0,score:i,grade:M(i),dimensions:j}))}else o&&process.stdout.write("\x1Bc"),G(i,g),o||H(i,g);return f!==null&&i<f&&(t||console.error(b(` \u2717 Score ${i} is below threshold ${f} \u2014 failing.
|
|
2
|
+
`)),process.exit(1)),i};o?(t||O(`Watching health score every ${c}s \u2014 press Ctrl+C to stop`),a(),setInterval(a,c*1e3),await new Promise(()=>{})):a()}export{q as healthCommand};
|
|
@@ -1,325 +1,2 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
*
|
|
4
|
-
* Before you touch a capability — see the blast radius.
|
|
5
|
-
*
|
|
6
|
-
* Given a capability ID, answers:
|
|
7
|
-
* • Which caps directly depend on this one?
|
|
8
|
-
* • Which caps transitively depend on it?
|
|
9
|
-
* • What scenarios would be affected?
|
|
10
|
-
* • What is the overall risk level? (low / medium / high / critical)
|
|
11
|
-
* • Are any frozen/stable caps in the blast zone?
|
|
12
|
-
*
|
|
13
|
-
* Pure graph traversal — no AI needed. Fast and deterministic.
|
|
14
|
-
*
|
|
15
|
-
* Usage:
|
|
16
|
-
* infernoflow impact auth-login
|
|
17
|
-
* infernoflow impact auth-login --depth 5
|
|
18
|
-
* infernoflow impact auth-login --json
|
|
19
|
-
* infernoflow impact auth-login --check Exit 1 if risk is HIGH or CRITICAL
|
|
20
|
-
*/
|
|
21
|
-
|
|
22
|
-
import * as fs from "node:fs";
|
|
23
|
-
import * as path from "node:path";
|
|
24
|
-
import { bold, cyan, gray, green, yellow, red } from "../ui/output.mjs";
|
|
25
|
-
|
|
26
|
-
// ── helpers ───────────────────────────────────────────────────────────────────
|
|
27
|
-
|
|
28
|
-
function loadJson(p) {
|
|
29
|
-
try { return JSON.parse(fs.readFileSync(p, "utf8")); }
|
|
30
|
-
catch { return null; }
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
const LEVEL_ICON = { frozen: "🧊", stable: "〰️ ", experimental: "🌊" };
|
|
34
|
-
const LEVEL_COLOR = { frozen: red, stable: yellow, experimental: green };
|
|
35
|
-
|
|
36
|
-
function stability(cap) {
|
|
37
|
-
return cap?.stability || "experimental";
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
// ── blast radius (BFS on reverse graph) ──────────────────────────────────────
|
|
41
|
-
|
|
42
|
-
/**
|
|
43
|
-
* Walk the reverse dependency graph (dependents) starting from capId.
|
|
44
|
-
* Returns: { direct: Set<string>, transitive: Set<string> }
|
|
45
|
-
*/
|
|
46
|
-
function blastRadius(capId, dependents, maxDepth = 10) {
|
|
47
|
-
const direct = new Set(dependents[capId] || []);
|
|
48
|
-
const transitive = new Set();
|
|
49
|
-
const queue = [...direct].map(id => ({ id, depth: 1 }));
|
|
50
|
-
const visited = new Set([capId, ...direct]);
|
|
51
|
-
|
|
52
|
-
while (queue.length > 0) {
|
|
53
|
-
const { id, depth } = queue.shift();
|
|
54
|
-
if (depth >= maxDepth) continue;
|
|
55
|
-
for (const dep of (dependents[id] || [])) {
|
|
56
|
-
if (!visited.has(dep)) {
|
|
57
|
-
visited.add(dep);
|
|
58
|
-
transitive.add(dep);
|
|
59
|
-
queue.push({ id: dep, depth: depth + 1 });
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
return { direct, transitive };
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
// ── scenario finder ───────────────────────────────────────────────────────────
|
|
68
|
-
|
|
69
|
-
function loadScenarios(infernoDir) {
|
|
70
|
-
const scenariosDir = path.join(infernoDir, "scenarios");
|
|
71
|
-
if (!fs.existsSync(scenariosDir)) return [];
|
|
72
|
-
|
|
73
|
-
const scenarios = [];
|
|
74
|
-
for (const f of fs.readdirSync(scenariosDir)) {
|
|
75
|
-
if (!f.endsWith(".json")) continue;
|
|
76
|
-
try {
|
|
77
|
-
const s = JSON.parse(fs.readFileSync(path.join(scenariosDir, f), "utf8"));
|
|
78
|
-
scenarios.push(s);
|
|
79
|
-
} catch {}
|
|
80
|
-
}
|
|
81
|
-
return scenarios;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
function scenariosForCap(capId, scenarios) {
|
|
85
|
-
return scenarios.filter(s => {
|
|
86
|
-
const covered = s.capabilitiesCovered || s.capabilities || [];
|
|
87
|
-
return covered.some(c => c.toLowerCase() === capId.toLowerCase());
|
|
88
|
-
});
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
// ── risk calculator ───────────────────────────────────────────────────────────
|
|
92
|
-
|
|
93
|
-
/**
|
|
94
|
-
* Risk levels:
|
|
95
|
-
* critical — the target cap itself is frozen AND has dependents
|
|
96
|
-
* high — a frozen cap is in the blast zone
|
|
97
|
-
* medium — a stable cap is in the blast zone
|
|
98
|
-
* low — all dependents are experimental
|
|
99
|
-
*/
|
|
100
|
-
function computeRisk(targetCap, allInBlast, allCaps) {
|
|
101
|
-
const targetLevel = stability(targetCap);
|
|
102
|
-
|
|
103
|
-
if (targetLevel === "frozen" && allInBlast.size > 0) {
|
|
104
|
-
return "critical";
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
for (const id of allInBlast) {
|
|
108
|
-
const cap = allCaps.find(c => c.id === id);
|
|
109
|
-
if (stability(cap) === "frozen") return "high";
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
for (const id of allInBlast) {
|
|
113
|
-
const cap = allCaps.find(c => c.id === id);
|
|
114
|
-
if (stability(cap) === "stable") return "medium";
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
return "low";
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
const RISK_COLOR = {
|
|
121
|
-
critical: red,
|
|
122
|
-
high: red,
|
|
123
|
-
medium: yellow,
|
|
124
|
-
low: green,
|
|
125
|
-
};
|
|
126
|
-
|
|
127
|
-
const RISK_ICON = {
|
|
128
|
-
critical: "🔴",
|
|
129
|
-
high: "🔴",
|
|
130
|
-
medium: "🟡",
|
|
131
|
-
low: "🟢",
|
|
132
|
-
};
|
|
133
|
-
|
|
134
|
-
const RISK_ADVICE = {
|
|
135
|
-
critical: "This capability is FROZEN — any change is high-risk. Requires explicit approval.",
|
|
136
|
-
high: "A frozen capability depends on this — test thoroughly before merging.",
|
|
137
|
-
medium: "A stable capability is in the blast zone — prefer additive changes only.",
|
|
138
|
-
low: "All dependents are experimental — safe to iterate freely.",
|
|
139
|
-
};
|
|
140
|
-
|
|
141
|
-
// ── printer ───────────────────────────────────────────────────────────────────
|
|
142
|
-
|
|
143
|
-
function printImpact({ capId, targetCap, direct, transitive, affectedScenarios, risk, allCaps, deps }) {
|
|
144
|
-
const targetLevel = stability(targetCap);
|
|
145
|
-
const targetIcon = LEVEL_ICON[targetLevel] || "🌊";
|
|
146
|
-
const targetColor = LEVEL_COLOR[targetLevel] || green;
|
|
147
|
-
const riskColor = RISK_COLOR[risk];
|
|
148
|
-
const riskIcon = RISK_ICON[risk];
|
|
149
|
-
|
|
150
|
-
console.log();
|
|
151
|
-
console.log(bold(` ${targetIcon} ${targetColor(capId)}`));
|
|
152
|
-
if (targetCap?.name || targetCap?.title) {
|
|
153
|
-
console.log(gray(` ${targetCap.name || targetCap.title}`));
|
|
154
|
-
}
|
|
155
|
-
console.log(gray(` stability: `) + targetColor(targetLevel));
|
|
156
|
-
console.log();
|
|
157
|
-
|
|
158
|
-
// What this cap calls (downstream)
|
|
159
|
-
if (deps.length > 0) {
|
|
160
|
-
console.log(gray(" This cap calls:"));
|
|
161
|
-
for (const dep of deps) {
|
|
162
|
-
const d = allCaps.find(c => c.id === dep);
|
|
163
|
-
const icon = LEVEL_ICON[stability(d)] || "🌊";
|
|
164
|
-
const color = LEVEL_COLOR[stability(d)] || green;
|
|
165
|
-
console.log(` ${icon} ${color(dep)}`);
|
|
166
|
-
}
|
|
167
|
-
console.log();
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
// Blast radius
|
|
171
|
-
if (direct.size === 0) {
|
|
172
|
-
console.log(gray(" No capabilities depend on this one."));
|
|
173
|
-
console.log(gray(" ✔ Safe to change freely."));
|
|
174
|
-
console.log();
|
|
175
|
-
} else {
|
|
176
|
-
console.log(bold(" Direct dependents (will be immediately affected):"));
|
|
177
|
-
for (const id of direct) {
|
|
178
|
-
const cap = allCaps.find(c => c.id === id);
|
|
179
|
-
const level = stability(cap);
|
|
180
|
-
const icon = LEVEL_ICON[level] || "🌊";
|
|
181
|
-
const color = LEVEL_COLOR[level] || green;
|
|
182
|
-
console.log(` ${icon} ${color(id)}`);
|
|
183
|
-
}
|
|
184
|
-
console.log();
|
|
185
|
-
|
|
186
|
-
if (transitive.size > 0) {
|
|
187
|
-
console.log(bold(" Transitive dependents (indirectly affected):"));
|
|
188
|
-
for (const id of transitive) {
|
|
189
|
-
const cap = allCaps.find(c => c.id === id);
|
|
190
|
-
const level = stability(cap);
|
|
191
|
-
const icon = LEVEL_ICON[level] || "🌊";
|
|
192
|
-
const color = LEVEL_COLOR[level] || green;
|
|
193
|
-
console.log(` ${icon} ${color(id)}`);
|
|
194
|
-
}
|
|
195
|
-
console.log();
|
|
196
|
-
}
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
// Affected scenarios
|
|
200
|
-
if (affectedScenarios.length > 0) {
|
|
201
|
-
console.log(bold(" Scenarios at risk:"));
|
|
202
|
-
for (const s of affectedScenarios) {
|
|
203
|
-
console.log(` ${yellow("⚠")} ${s.scenarioId || s.description || "(unnamed)"}`);
|
|
204
|
-
if (s.description) console.log(gray(` ${s.description}`));
|
|
205
|
-
}
|
|
206
|
-
console.log();
|
|
207
|
-
} else if (direct.size > 0) {
|
|
208
|
-
console.log(gray(" No scenarios cover the affected capabilities."));
|
|
209
|
-
console.log(gray(" Consider adding scenarios before making this change."));
|
|
210
|
-
console.log();
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
// Risk banner
|
|
214
|
-
console.log(` ${riskIcon} Risk level: ${bold(riskColor(risk.toUpperCase()))}`);
|
|
215
|
-
console.log(` ${gray(RISK_ADVICE[risk])}`);
|
|
216
|
-
console.log();
|
|
217
|
-
|
|
218
|
-
// Summary numbers
|
|
219
|
-
const total = direct.size + transitive.size;
|
|
220
|
-
if (total > 0) {
|
|
221
|
-
console.log(gray(
|
|
222
|
-
` ── ${direct.size} direct · ${transitive.size} transitive · ${total} total affected · ${affectedScenarios.length} scenario(s) at risk`
|
|
223
|
-
));
|
|
224
|
-
console.log();
|
|
225
|
-
}
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
// ── entry point ───────────────────────────────────────────────────────────────
|
|
229
|
-
|
|
230
|
-
export async function impactCommand(rawArgs) {
|
|
231
|
-
const args = (rawArgs || []).slice(1); // skip command name
|
|
232
|
-
const jsonMode = args.includes("--json");
|
|
233
|
-
const checkMode = args.includes("--check");
|
|
234
|
-
const depthIdx = args.indexOf("--depth");
|
|
235
|
-
const maxDepth = depthIdx !== -1 ? parseInt(args[depthIdx + 1], 10) || 10 : 10;
|
|
236
|
-
|
|
237
|
-
const capId = args.find((a, i) => !a.startsWith("--") && (depthIdx === -1 || i !== depthIdx + 1));
|
|
238
|
-
|
|
239
|
-
if (!capId) {
|
|
240
|
-
console.error(red("✗ Usage: infernoflow impact <capability-id> [--depth N] [--json] [--check]"));
|
|
241
|
-
console.error(gray(" Example: infernoflow impact user-auth"));
|
|
242
|
-
process.exit(1);
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
const cwd = process.cwd();
|
|
246
|
-
const infernoDir = path.join(cwd, "inferno");
|
|
247
|
-
|
|
248
|
-
// Load graph
|
|
249
|
-
const graph = loadJson(path.join(infernoDir, "graph.json"));
|
|
250
|
-
if (!graph) {
|
|
251
|
-
console.error(red("✗ inferno/graph.json not found — run `infernoflow graph` first."));
|
|
252
|
-
process.exit(1);
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
// Load capabilities
|
|
256
|
-
let allCaps = [];
|
|
257
|
-
const rawCaps = loadJson(path.join(infernoDir, "capabilities.json"));
|
|
258
|
-
if (rawCaps) allCaps = Array.isArray(rawCaps) ? rawCaps : (rawCaps.capabilities || []);
|
|
259
|
-
|
|
260
|
-
// Validate cap exists
|
|
261
|
-
const targetCap = allCaps.find(c => c.id === capId);
|
|
262
|
-
if (!targetCap) {
|
|
263
|
-
console.error(red(`✗ Capability "${capId}" not found in capabilities.json`));
|
|
264
|
-
console.error(gray(" Run: infernoflow stability — to list all capability IDs"));
|
|
265
|
-
process.exit(1);
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
// Blast radius
|
|
269
|
-
const { direct, transitive } = blastRadius(capId, graph.dependents || {}, maxDepth);
|
|
270
|
-
const allInBlast = new Set([...direct, ...transitive]);
|
|
271
|
-
|
|
272
|
-
// Dependencies (what this cap calls)
|
|
273
|
-
const deps = graph.deps?.[capId] || [];
|
|
274
|
-
|
|
275
|
-
// Scenarios
|
|
276
|
-
const scenarios = loadScenarios(infernoDir);
|
|
277
|
-
const targetScenarios = scenariosForCap(capId, scenarios);
|
|
278
|
-
// Collect scenarios for all affected caps too
|
|
279
|
-
const affectedScenarioSet = new Map();
|
|
280
|
-
for (const s of targetScenarios) {
|
|
281
|
-
affectedScenarioSet.set(s.scenarioId || s.description, s);
|
|
282
|
-
}
|
|
283
|
-
for (const id of allInBlast) {
|
|
284
|
-
for (const s of scenariosForCap(id, scenarios)) {
|
|
285
|
-
affectedScenarioSet.set(s.scenarioId || s.description, s);
|
|
286
|
-
}
|
|
287
|
-
}
|
|
288
|
-
const affectedScenarios = [...affectedScenarioSet.values()];
|
|
289
|
-
|
|
290
|
-
// Risk
|
|
291
|
-
const risk = computeRisk(targetCap, allInBlast, allCaps);
|
|
292
|
-
|
|
293
|
-
// JSON mode
|
|
294
|
-
if (jsonMode) {
|
|
295
|
-
const out = {
|
|
296
|
-
capId,
|
|
297
|
-
name: targetCap.name || targetCap.title,
|
|
298
|
-
stability: stability(targetCap),
|
|
299
|
-
risk,
|
|
300
|
-
direct: [...direct],
|
|
301
|
-
transitive: [...transitive],
|
|
302
|
-
deps,
|
|
303
|
-
affectedScenarios: affectedScenarios.map(s => s.scenarioId || s.description),
|
|
304
|
-
summary: {
|
|
305
|
-
directCount: direct.size,
|
|
306
|
-
transitiveCount: transitive.size,
|
|
307
|
-
totalAffected: allInBlast.size,
|
|
308
|
-
scenariosAtRisk: affectedScenarios.length,
|
|
309
|
-
},
|
|
310
|
-
};
|
|
311
|
-
console.log(JSON.stringify(out, null, 2));
|
|
312
|
-
if (checkMode && (risk === "high" || risk === "critical")) process.exit(1);
|
|
313
|
-
return;
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
console.log(gray(`\n infernoflow impact → ${bold(capId)}`));
|
|
317
|
-
console.log(gray(" ──────────────────────────────────────────────────────────────"));
|
|
318
|
-
|
|
319
|
-
printImpact({ capId, targetCap, direct, transitive, affectedScenarios, risk, allCaps, deps });
|
|
320
|
-
|
|
321
|
-
if (checkMode && (risk === "high" || risk === "critical")) {
|
|
322
|
-
console.log(red(" ✗ --check failed: risk level is " + risk.toUpperCase()));
|
|
323
|
-
process.exit(1);
|
|
324
|
-
}
|
|
325
|
-
}
|
|
1
|
+
import*as R from"node:fs";import*as k from"node:path";import{bold as C,gray as l,green as j,yellow as A,red as x}from"../ui/output.mjs";function E(t){try{return JSON.parse(R.readFileSync(t,"utf8"))}catch{return null}}const L={frozen:"\u{1F9CA}",stable:"\u3030\uFE0F ",experimental:"\u{1F30A}"},O={frozen:x,stable:A,experimental:j};function h(t){return t?.stability||"experimental"}function _(t,o,i=10){const c=new Set(o[t]||[]),n=new Set,r=[...c].map(u=>({id:u,depth:1})),e=new Set([t,...c]);for(;r.length>0;){const{id:u,depth:f}=r.shift();if(!(f>=i))for(const d of o[u]||[])e.has(d)||(e.add(d),n.add(d),r.push({id:d,depth:f+1}))}return{direct:c,transitive:n}}function F(t){const o=k.join(t,"scenarios");if(!R.existsSync(o))return[];const i=[];for(const c of R.readdirSync(o))if(c.endsWith(".json"))try{const n=JSON.parse(R.readFileSync(k.join(o,c),"utf8"));i.push(n)}catch{}return i}function D(t,o){return o.filter(i=>(i.capabilitiesCovered||i.capabilities||[]).some(n=>n.toLowerCase()===t.toLowerCase()))}function J(t,o,i){if(h(t)==="frozen"&&o.size>0)return"critical";for(const n of o){const r=i.find(e=>e.id===n);if(h(r)==="frozen")return"high"}for(const n of o){const r=i.find(e=>e.id===n);if(h(r)==="stable")return"medium"}return"low"}const K={critical:x,high:x,medium:A,low:j},M={critical:"\u{1F534}",high:"\u{1F534}",medium:"\u{1F7E1}",low:"\u{1F7E2}"},T={critical:"This capability is FROZEN \u2014 any change is high-risk. Requires explicit approval.",high:"A frozen capability depends on this \u2014 test thoroughly before merging.",medium:"A stable capability is in the blast zone \u2014 prefer additive changes only.",low:"All dependents are experimental \u2014 safe to iterate freely."};function U({capId:t,targetCap:o,direct:i,transitive:c,affectedScenarios:n,risk:r,allCaps:e,deps:u}){const f=h(o),d=L[f]||"\u{1F30A}",w=O[f]||j,I=K[r],m=M[r];if(console.log(),console.log(C(` ${d} ${w(t)}`)),(o?.name||o?.title)&&console.log(l(` ${o.name||o.title}`)),console.log(l(" stability: ")+w(f)),console.log(),u.length>0){console.log(l(" This cap calls:"));for(const s of u){const p=e.find(S=>S.id===s),g=L[h(p)]||"\u{1F30A}",y=O[h(p)]||j;console.log(` ${g} ${y(s)}`)}console.log()}if(i.size===0)console.log(l(" No capabilities depend on this one.")),console.log(l(" \u2714 Safe to change freely.")),console.log();else{console.log(C(" Direct dependents (will be immediately affected):"));for(const s of i){const p=e.find(v=>v.id===s),g=h(p),y=L[g]||"\u{1F30A}",S=O[g]||j;console.log(` ${y} ${S(s)}`)}if(console.log(),c.size>0){console.log(C(" Transitive dependents (indirectly affected):"));for(const s of c){const p=e.find(v=>v.id===s),g=h(p),y=L[g]||"\u{1F30A}",S=O[g]||j;console.log(` ${y} ${S(s)}`)}console.log()}}if(n.length>0){console.log(C(" Scenarios at risk:"));for(const s of n)console.log(` ${A("\u26A0")} ${s.scenarioId||s.description||"(unnamed)"}`),s.description&&console.log(l(` ${s.description}`));console.log()}else i.size>0&&(console.log(l(" No scenarios cover the affected capabilities.")),console.log(l(" Consider adding scenarios before making this change.")),console.log());console.log(` ${m} Risk level: ${C(I(r.toUpperCase()))}`),console.log(` ${l(T[r])}`),console.log();const $=i.size+c.size;$>0&&(console.log(l(` \u2500\u2500 ${i.size} direct \xB7 ${c.size} transitive \xB7 ${$} total affected \xB7 ${n.length} scenario(s) at risk`)),console.log())}async function W(t){const o=(t||[]).slice(1),i=o.includes("--json"),c=o.includes("--check"),n=o.indexOf("--depth"),r=n!==-1&&parseInt(o[n+1],10)||10,e=o.find((a,b)=>!a.startsWith("--")&&(n===-1||b!==n+1));e||(console.error(x("\u2717 Usage: infernoflow impact <capability-id> [--depth N] [--json] [--check]")),console.error(l(" Example: infernoflow impact user-auth")),process.exit(1));const u=process.cwd(),f=k.join(u,"inferno"),d=E(k.join(f,"graph.json"));d||(console.error(x("\u2717 inferno/graph.json not found \u2014 run `infernoflow graph` first.")),process.exit(1));let w=[];const I=E(k.join(f,"capabilities.json"));I&&(w=Array.isArray(I)?I:I.capabilities||[]);const m=w.find(a=>a.id===e);m||(console.error(x(`\u2717 Capability "${e}" not found in capabilities.json`)),console.error(l(" Run: infernoflow stability \u2014 to list all capability IDs")),process.exit(1));const{direct:$,transitive:s}=_(e,d.dependents||{},r),p=new Set([...$,...s]),g=d.deps?.[e]||[],y=F(f),S=D(e,y),v=new Map;for(const a of S)v.set(a.scenarioId||a.description,a);for(const a of p)for(const b of D(a,y))v.set(b.scenarioId||b.description,b);const N=[...v.values()],z=J(m,p,w);if(i){const a={capId:e,name:m.name||m.title,stability:h(m),risk:z,direct:[...$],transitive:[...s],deps:g,affectedScenarios:N.map(b=>b.scenarioId||b.description),summary:{directCount:$.size,transitiveCount:s.size,totalAffected:p.size,scenariosAtRisk:N.length}};console.log(JSON.stringify(a,null,2)),c&&(z==="high"||z==="critical")&&process.exit(1);return}console.log(l(`
|
|
2
|
+
infernoflow impact \u2192 ${C(e)}`)),console.log(l(" \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")),U({capId:e,targetCap:m,direct:$,transitive:s,affectedScenarios:N,risk:z,allCaps:w,deps:g}),c&&(z==="high"||z==="critical")&&(console.log(x(" \u2717 --check failed: risk level is "+z.toUpperCase())),process.exit(1))}export{W as impactCommand};
|