infernoflow 0.22.1 → 0.24.0
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
CHANGED
|
@@ -54,6 +54,10 @@ const COMMAND_DESCRIPTIONS = {
|
|
|
54
54
|
doctor: "Diagnose your infernoflow setup — checks Node, git, contract, AI providers, MCP, hooks",
|
|
55
55
|
coverage: "Map test files to capabilities — show which caps have test coverage and which don't",
|
|
56
56
|
review: "AI-powered capability impact review for staged or recent git changes",
|
|
57
|
+
scan: "Deep AST scan — reads actual function bodies, extracts calls, DB ops, external services",
|
|
58
|
+
stability: "Show solid/liquid stability level for every capability (frozen/stable/experimental)",
|
|
59
|
+
freeze: "Mark a capability as frozen (solid) — AI will not modify it without explicit instruction",
|
|
60
|
+
thaw: "Reset a capability to experimental (liquid) — free to evolve",
|
|
57
61
|
};
|
|
58
62
|
|
|
59
63
|
const COMMAND_HANDLERS = {
|
|
@@ -101,6 +105,10 @@ const COMMAND_HANDLERS = {
|
|
|
101
105
|
doctor: async (args) => (await import("../lib/commands/doctor.mjs")).doctorCommand(args),
|
|
102
106
|
coverage: async (args) => (await import("../lib/commands/coverage.mjs")).coverageCommand(args),
|
|
103
107
|
review: async (args) => (await import("../lib/commands/review.mjs")).reviewCommand(args),
|
|
108
|
+
scan: async (args) => (await import("../lib/commands/scan.mjs")).scanCommand(args),
|
|
109
|
+
stability: async (args) => (await import("../lib/commands/stability.mjs")).stabilityCommand(args),
|
|
110
|
+
freeze: async (args) => (await import("../lib/commands/stability.mjs")).freezeCommand(args),
|
|
111
|
+
thaw: async (args) => (await import("../lib/commands/stability.mjs")).thawCommand(args),
|
|
104
112
|
};
|
|
105
113
|
|
|
106
114
|
function formatCommandsHelp() {
|
|
@@ -355,6 +363,19 @@ ${formatCommandsHelp()}
|
|
|
355
363
|
--fail-below <pct> Exit 1 if coverage percentage is below this value (CI gate)
|
|
356
364
|
--json Machine-readable coverage breakdown
|
|
357
365
|
|
|
366
|
+
${bold("scan options:")}
|
|
367
|
+
--dir <path> Extra directory to scan (repeatable)
|
|
368
|
+
--capability <id> Scan and enrich a single capability only
|
|
369
|
+
--dry-run Print results without writing files
|
|
370
|
+
--json Machine-readable scan output
|
|
371
|
+
|
|
372
|
+
${bold("stability / freeze / thaw options:")}
|
|
373
|
+
infernoflow stability List all capabilities with their stability level
|
|
374
|
+
infernoflow freeze <id> Mark capability as frozen (AI won't touch it)
|
|
375
|
+
infernoflow freeze <id> --stable Mark as stable (careful, not forbidden)
|
|
376
|
+
infernoflow thaw <id> Reset to experimental (liquid — free to change)
|
|
377
|
+
--json Machine-readable stability list
|
|
378
|
+
|
|
358
379
|
${bold("review options:")}
|
|
359
380
|
--unstaged Review all working-tree changes (not just staged)
|
|
360
381
|
--last Review last commit (git diff HEAD~1)
|
|
@@ -13,6 +13,7 @@ import * as fs from "node:fs";
|
|
|
13
13
|
import * as path from "node:path";
|
|
14
14
|
import { readProfile } from "../learning/profile.mjs";
|
|
15
15
|
import { ok, warn, info, bold, done } from "../ui/output.mjs";
|
|
16
|
+
import { buildStabilitySummary } from "./stability.mjs";
|
|
16
17
|
|
|
17
18
|
// ── CLAUDE.md template ────────────────────────────────────────────────────────
|
|
18
19
|
//
|
|
@@ -21,7 +22,7 @@ import { ok, warn, info, bold, done } from "../ui/output.mjs";
|
|
|
21
22
|
// session. By putting infernoflow behavior instructions here, Claude becomes
|
|
22
23
|
// the automatic executor of infernoflow — no developer involvement needed.
|
|
23
24
|
//
|
|
24
|
-
function buildClaudeMd(profile, contract) {
|
|
25
|
+
function buildClaudeMd(profile, contract, caps) {
|
|
25
26
|
const framework = profile?.stack?.framework !== "unknown" ? profile.stack.framework : null;
|
|
26
27
|
const language = profile?.stack?.language !== "unknown" ? profile.stack.language : null;
|
|
27
28
|
const stackLine = framework
|
|
@@ -33,6 +34,8 @@ function buildClaudeMd(profile, contract) {
|
|
|
33
34
|
? `The contract currently tracks **${capCount} capabilities**.`
|
|
34
35
|
: "";
|
|
35
36
|
|
|
37
|
+
const stabilitySummary = caps ? buildStabilitySummary(caps) : null;
|
|
38
|
+
|
|
36
39
|
return `# infernoflow — Automatic AI Behavior
|
|
37
40
|
|
|
38
41
|
> This file is managed by infernoflow. Edit the sections marked "customizable".
|
|
@@ -114,6 +117,8 @@ multi-step task repeatedly:
|
|
|
114
117
|
|
|
115
118
|
---
|
|
116
119
|
|
|
120
|
+
${stabilitySummary ? stabilitySummary + "\n\n---" : ""}
|
|
121
|
+
|
|
117
122
|
## Rules
|
|
118
123
|
|
|
119
124
|
| Rule | Why |
|
|
@@ -123,6 +128,7 @@ multi-step task repeatedly:
|
|
|
123
128
|
| Never block the developer waiting for infernoflow | Run tools async when possible |
|
|
124
129
|
| Always call \`infernoflow_context\` at session start | It keeps your context current |
|
|
125
130
|
| Treat contract drift like a linting error — fix it silently | Not the developer's problem |
|
|
131
|
+
| Never modify frozen capabilities without explicit instruction | Stability contract must be respected |
|
|
126
132
|
|
|
127
133
|
---
|
|
128
134
|
|
|
@@ -156,13 +162,18 @@ multi-step task repeatedly:
|
|
|
156
162
|
export function writeClaudeMd(cwd, infernoDir, { force = false } = {}) {
|
|
157
163
|
const claudeMdPath = path.join(cwd, "CLAUDE.md");
|
|
158
164
|
|
|
159
|
-
// Load project profile + contract for context
|
|
165
|
+
// Load project profile + contract + capabilities for context
|
|
160
166
|
let profile = null;
|
|
161
167
|
let contract = null;
|
|
168
|
+
let caps = null;
|
|
162
169
|
try { profile = readProfile(infernoDir); } catch {}
|
|
163
170
|
try { contract = JSON.parse(fs.readFileSync(path.join(infernoDir, "contract.json"), "utf8")); } catch {}
|
|
171
|
+
try {
|
|
172
|
+
const raw = JSON.parse(fs.readFileSync(path.join(infernoDir, "capabilities.json"), "utf8"));
|
|
173
|
+
caps = Array.isArray(raw) ? raw : (raw.capabilities || []);
|
|
174
|
+
} catch {}
|
|
164
175
|
|
|
165
|
-
const newContent = buildClaudeMd(profile, contract);
|
|
176
|
+
const newContent = buildClaudeMd(profile, contract, caps);
|
|
166
177
|
|
|
167
178
|
// If file exists and not forcing, preserve the customizable section
|
|
168
179
|
if (fs.existsSync(claudeMdPath) && !force) {
|
|
@@ -0,0 +1,566 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* infernoflow scan
|
|
3
|
+
*
|
|
4
|
+
* Deep AST-based code analysis. Reads actual function bodies — not just names.
|
|
5
|
+
* Extracts: external calls, DB operations, HTTP calls, auth patterns, error types,
|
|
6
|
+
* external service usage (Stripe, S3, SendGrid, etc.).
|
|
7
|
+
*
|
|
8
|
+
* Enriches capabilities.json with a `codeAnalysis` block on each capability,
|
|
9
|
+
* and saves the full scan report to inferno/scan.json.
|
|
10
|
+
*
|
|
11
|
+
* Usage:
|
|
12
|
+
* infernoflow scan Scan project, enrich capabilities
|
|
13
|
+
* infernoflow scan --dir src/ Scan specific directory
|
|
14
|
+
* infernoflow scan --json Print scan.json to stdout
|
|
15
|
+
* infernoflow scan --dry-run Print without writing files
|
|
16
|
+
* infernoflow scan --capability auth-login Scan one capability only
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import * as fs from "node:fs";
|
|
20
|
+
import * as path from "node:path";
|
|
21
|
+
import { createRequire } from "node:module";
|
|
22
|
+
import { execSync } from "node:child_process";
|
|
23
|
+
import { bold, cyan, gray, green, yellow, red } from "../ui/output.mjs";
|
|
24
|
+
|
|
25
|
+
const require = createRequire(import.meta.url);
|
|
26
|
+
|
|
27
|
+
// ── TypeScript compiler API (global install) ──────────────────────────────────
|
|
28
|
+
|
|
29
|
+
const TS_PATHS = [
|
|
30
|
+
"/usr/local/lib/node_modules_global/lib/node_modules/typescript",
|
|
31
|
+
"/usr/lib/node_modules/typescript",
|
|
32
|
+
path.join(process.env.HOME || "", ".npm-global/lib/node_modules/typescript"),
|
|
33
|
+
];
|
|
34
|
+
|
|
35
|
+
function loadTypeScript() {
|
|
36
|
+
for (const p of TS_PATHS) {
|
|
37
|
+
try { return require(path.join(p, "lib/typescript.js")); } catch {}
|
|
38
|
+
}
|
|
39
|
+
try { return require("typescript"); } catch {}
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const ts = loadTypeScript();
|
|
44
|
+
|
|
45
|
+
// ── external service fingerprints ────────────────────────────────────────────
|
|
46
|
+
|
|
47
|
+
const SERVICE_PATTERNS = [
|
|
48
|
+
{ service: "stripe", patterns: ["stripe", "Stripe", "createPaymentIntent", "charges.create"] },
|
|
49
|
+
{ service: "sendgrid", patterns: ["sendgrid", "@sendgrid", "sgMail", "sendgrid.send"] },
|
|
50
|
+
{ service: "ses", patterns: ["SES", "ses.sendEmail", "aws-sdk/ses", "nodemailer"] },
|
|
51
|
+
{ service: "s3", patterns: ["S3", "s3.upload", "s3.getObject", "PutObjectCommand", "@aws-sdk/s3"] },
|
|
52
|
+
{ service: "redis", patterns: ["redis", "Redis", "ioredis", "createClient"] },
|
|
53
|
+
{ service: "jwt", patterns: ["jwt", "jsonwebtoken", "sign(", "verify(", "decode("] },
|
|
54
|
+
{ service: "bcrypt", patterns: ["bcrypt", "argon2", "scrypt", "hashSync", "compare("] },
|
|
55
|
+
{ service: "prisma", patterns: ["prisma.", "PrismaClient", "@prisma/client"] },
|
|
56
|
+
{ service: "mongoose", patterns: ["mongoose", ".save()", ".findOne(", ".aggregate("] },
|
|
57
|
+
{ service: "postgres", patterns: ["pg", "Pool(", "Client(", "query(", "postgres("] },
|
|
58
|
+
{ service: "mysql", patterns: ["mysql", "mysql2", "createConnection"] },
|
|
59
|
+
{ service: "graphql", patterns: ["graphql", "gql`", "ApolloServer", "GraphQLSchema"] },
|
|
60
|
+
{ service: "firebase", patterns: ["firebase", "firestore", "initializeApp"] },
|
|
61
|
+
{ service: "twilio", patterns: ["twilio", "Twilio(", "messages.create"] },
|
|
62
|
+
{ service: "openai", patterns: ["openai", "OpenAI(", "createCompletion", "chat.completions"] },
|
|
63
|
+
];
|
|
64
|
+
|
|
65
|
+
function detectServices(text) {
|
|
66
|
+
const found = new Set();
|
|
67
|
+
for (const { service, patterns } of SERVICE_PATTERNS) {
|
|
68
|
+
if (patterns.some(p => text.includes(p))) found.add(service);
|
|
69
|
+
}
|
|
70
|
+
return [...found];
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// ── DB call patterns ──────────────────────────────────────────────────────────
|
|
74
|
+
|
|
75
|
+
const DB_PATTERNS = [
|
|
76
|
+
/\.(find|findOne|findMany|findById|findAll)\s*\(/g,
|
|
77
|
+
/\.(create|insert|insertOne|insertMany|save)\s*\(/g,
|
|
78
|
+
/\.(update|updateOne|updateMany|updateById|upsert)\s*\(/g,
|
|
79
|
+
/\.(delete|deleteOne|deleteMany|remove|destroy)\s*\(/g,
|
|
80
|
+
/\.(query|execute|raw)\s*\(/g,
|
|
81
|
+
/\.(aggregate|groupBy|count|sum)\s*\(/g,
|
|
82
|
+
/db\.\w+\s*\(/g,
|
|
83
|
+
/prisma\.\w+\.\w+\s*\(/g,
|
|
84
|
+
];
|
|
85
|
+
|
|
86
|
+
function detectDbCalls(text) {
|
|
87
|
+
const calls = new Set();
|
|
88
|
+
for (const re of DB_PATTERNS) {
|
|
89
|
+
const r = new RegExp(re.source, "g");
|
|
90
|
+
let m;
|
|
91
|
+
while ((m = r.exec(text)) !== null) calls.add(m[0].replace(/\s*\($/, "()"));
|
|
92
|
+
}
|
|
93
|
+
return [...calls].slice(0, 10);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// ── HTTP call patterns ────────────────────────────────────────────────────────
|
|
97
|
+
|
|
98
|
+
const HTTP_PATTERNS = [
|
|
99
|
+
/fetch\s*\(/g,
|
|
100
|
+
/axios\.(get|post|put|patch|delete)\s*\(/g,
|
|
101
|
+
/http\.(get|post|request)\s*\(/g,
|
|
102
|
+
/got\.(get|post|put|delete)\s*\(/g,
|
|
103
|
+
/request\.(get|post|put|delete)\s*\(/g,
|
|
104
|
+
/\$http\.(get|post|put|delete)\s*\(/g,
|
|
105
|
+
];
|
|
106
|
+
|
|
107
|
+
function detectHttpCalls(text) {
|
|
108
|
+
const calls = new Set();
|
|
109
|
+
for (const re of HTTP_PATTERNS) {
|
|
110
|
+
const r = new RegExp(re.source, "g");
|
|
111
|
+
let m;
|
|
112
|
+
while ((m = r.exec(text)) !== null) calls.add(m[0].replace(/\s*\($/, "()"));
|
|
113
|
+
}
|
|
114
|
+
return [...calls].slice(0, 8);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// ── TypeScript / JavaScript AST analysis ─────────────────────────────────────
|
|
118
|
+
|
|
119
|
+
function getNodeName(node) {
|
|
120
|
+
if (!ts) return null;
|
|
121
|
+
if (node.name && ts.isIdentifier(node.name)) return node.name.text;
|
|
122
|
+
return null;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function collectCallsInNode(node, calls = new Set()) {
|
|
126
|
+
if (!ts) return calls;
|
|
127
|
+
if (ts.isCallExpression(node)) {
|
|
128
|
+
const expr = node.expression;
|
|
129
|
+
if (ts.isIdentifier(expr)) {
|
|
130
|
+
calls.add(expr.text + "()");
|
|
131
|
+
} else if (ts.isPropertyAccessExpression(expr)) {
|
|
132
|
+
calls.add(expr.name.text + "()");
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
ts.forEachChild(node, child => collectCallsInNode(child, calls));
|
|
136
|
+
return calls;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function collectThrowsInNode(node, throws = new Set()) {
|
|
140
|
+
if (!ts) return throws;
|
|
141
|
+
if (ts.isThrowStatement(node) && node.expression) {
|
|
142
|
+
if (ts.isNewExpression(node.expression) && ts.isIdentifier(node.expression.expression)) {
|
|
143
|
+
throws.add(node.expression.expression.text);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
ts.forEachChild(node, child => collectThrowsInNode(child, throws));
|
|
147
|
+
return throws;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function isFunctionNode(node) {
|
|
151
|
+
if (!ts) return false;
|
|
152
|
+
return (
|
|
153
|
+
ts.isFunctionDeclaration(node) ||
|
|
154
|
+
ts.isFunctionExpression(node) ||
|
|
155
|
+
ts.isArrowFunction(node) ||
|
|
156
|
+
ts.isMethodDeclaration(node)
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function getParentVariableName(node) {
|
|
161
|
+
// For arrow functions assigned to const: const foo = () => {}
|
|
162
|
+
if (!ts) return null;
|
|
163
|
+
if (node.parent && ts.isVariableDeclaration(node.parent)) {
|
|
164
|
+
return getNodeName(node.parent);
|
|
165
|
+
}
|
|
166
|
+
if (node.parent && ts.isPropertyAssignment(node.parent)) {
|
|
167
|
+
return getNodeName(node.parent);
|
|
168
|
+
}
|
|
169
|
+
return null;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function analyzeJsTs(filePath, code) {
|
|
173
|
+
if (!ts) return null;
|
|
174
|
+
|
|
175
|
+
let srcFile;
|
|
176
|
+
try {
|
|
177
|
+
srcFile = ts.createSourceFile(filePath, code, ts.ScriptTarget.Latest, /*setParentNodes*/ true);
|
|
178
|
+
} catch {
|
|
179
|
+
return null;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const functions = [];
|
|
183
|
+
|
|
184
|
+
function visit(node) {
|
|
185
|
+
if (isFunctionNode(node)) {
|
|
186
|
+
const name = getNodeName(node) || getParentVariableName(node) || "<anonymous>";
|
|
187
|
+
const calls = [...collectCallsInNode(node)].slice(0, 20);
|
|
188
|
+
const throws = [...collectThrowsInNode(node)];
|
|
189
|
+
const text = code.slice(node.pos, node.end);
|
|
190
|
+
functions.push({
|
|
191
|
+
name,
|
|
192
|
+
calls,
|
|
193
|
+
throws,
|
|
194
|
+
services: detectServices(text),
|
|
195
|
+
dbCalls: detectDbCalls(text),
|
|
196
|
+
httpCalls: detectHttpCalls(text),
|
|
197
|
+
loc: srcFile.getLineAndCharacterOfPosition(node.pos).line + 1,
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
ts.forEachChild(node, visit);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
visit(srcFile);
|
|
204
|
+
return functions;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// ── Python AST analysis via child_process ─────────────────────────────────────
|
|
208
|
+
|
|
209
|
+
const PYTHON_SCRIPT = `
|
|
210
|
+
import ast, json, sys
|
|
211
|
+
|
|
212
|
+
def get_calls(node):
|
|
213
|
+
calls = []
|
|
214
|
+
for n in ast.walk(node):
|
|
215
|
+
if isinstance(n, ast.Call):
|
|
216
|
+
if isinstance(n.func, ast.Name):
|
|
217
|
+
calls.append(n.func.id + "()")
|
|
218
|
+
elif isinstance(n.func, ast.Attribute):
|
|
219
|
+
calls.append(n.func.attr + "()")
|
|
220
|
+
return list(set(calls))[:20]
|
|
221
|
+
|
|
222
|
+
def get_raises(node):
|
|
223
|
+
raises = []
|
|
224
|
+
for n in ast.walk(node):
|
|
225
|
+
if isinstance(n, ast.Raise) and n.exc:
|
|
226
|
+
if isinstance(n.exc, ast.Call) and isinstance(n.exc.func, ast.Name):
|
|
227
|
+
raises.append(n.exc.func.id)
|
|
228
|
+
elif isinstance(n.exc, ast.Name):
|
|
229
|
+
raises.append(n.exc.id)
|
|
230
|
+
return list(set(raises))
|
|
231
|
+
|
|
232
|
+
try:
|
|
233
|
+
code = open(sys.argv[1], encoding="utf-8", errors="ignore").read()
|
|
234
|
+
tree = ast.parse(code)
|
|
235
|
+
functions = []
|
|
236
|
+
for node in ast.walk(tree):
|
|
237
|
+
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
|
238
|
+
functions.append({
|
|
239
|
+
"name": node.name,
|
|
240
|
+
"calls": get_calls(node),
|
|
241
|
+
"throws": get_raises(node),
|
|
242
|
+
"loc": node.lineno,
|
|
243
|
+
})
|
|
244
|
+
print(json.dumps(functions))
|
|
245
|
+
except Exception as e:
|
|
246
|
+
print(json.dumps([]))
|
|
247
|
+
`;
|
|
248
|
+
|
|
249
|
+
function analyzePython(filePath) {
|
|
250
|
+
try {
|
|
251
|
+
const result = execSync(
|
|
252
|
+
`python3 -c ${JSON.stringify(PYTHON_SCRIPT)} ${JSON.stringify(filePath)}`,
|
|
253
|
+
{ timeout: 8000, encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] }
|
|
254
|
+
);
|
|
255
|
+
const fns = JSON.parse(result.trim() || "[]");
|
|
256
|
+
// add service/db/http detection from raw file text
|
|
257
|
+
const code = fs.readFileSync(filePath, "utf8");
|
|
258
|
+
return fns.map(f => ({
|
|
259
|
+
...f,
|
|
260
|
+
services: detectServices(code),
|
|
261
|
+
dbCalls: detectDbCalls(code),
|
|
262
|
+
httpCalls: detectHttpCalls(code),
|
|
263
|
+
}));
|
|
264
|
+
} catch {
|
|
265
|
+
return null;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// ── regex fallback (Go, Ruby, Java, other) ────────────────────────────────────
|
|
270
|
+
|
|
271
|
+
const FUNC_PATTERNS = [
|
|
272
|
+
{ re: /^func\s+(?:\(\w+\s+\*?\w+\)\s+)?(\w+)\s*\(/gm, lang: "go" },
|
|
273
|
+
{ re: /^\s*(?:def|async def)\s+(\w+)\s*\(/gm, lang: "py" },
|
|
274
|
+
{ re: /^\s*(?:public|private|protected)?\s*(?:static\s+)?(?:\w+\s+)?(\w+)\s*\(/gm, lang: "java" },
|
|
275
|
+
{ re: /^\s*def\s+(\w+)\s*[\(\|]/gm, lang: "rb" },
|
|
276
|
+
];
|
|
277
|
+
|
|
278
|
+
function analyzeWithRegex(filePath, code) {
|
|
279
|
+
const ext = path.extname(filePath).slice(1);
|
|
280
|
+
const pattern = FUNC_PATTERNS.find(p => p.lang === ext);
|
|
281
|
+
if (!pattern) return null;
|
|
282
|
+
|
|
283
|
+
const functions = [];
|
|
284
|
+
const r = new RegExp(pattern.re.source, "gm");
|
|
285
|
+
let m;
|
|
286
|
+
while ((m = r.exec(code)) !== null) {
|
|
287
|
+
// grab up to 60 lines after the match for context
|
|
288
|
+
const start = m.index;
|
|
289
|
+
const end = Math.min(start + 2000, code.length);
|
|
290
|
+
const chunk = code.slice(start, end);
|
|
291
|
+
functions.push({
|
|
292
|
+
name: m[1],
|
|
293
|
+
calls: [],
|
|
294
|
+
throws: [],
|
|
295
|
+
services: detectServices(chunk),
|
|
296
|
+
dbCalls: detectDbCalls(chunk),
|
|
297
|
+
httpCalls: detectHttpCalls(chunk),
|
|
298
|
+
loc: code.slice(0, start).split("\n").length,
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
return functions.length > 0 ? functions : null;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// ── file walker ───────────────────────────────────────────────────────────────
|
|
305
|
+
|
|
306
|
+
const SKIP_DIRS = new Set([
|
|
307
|
+
"node_modules", ".git", "dist", "build", "out", ".next", ".nuxt",
|
|
308
|
+
"coverage", "__pycache__", ".pytest_cache", "vendor", "tmp", ".turbo",
|
|
309
|
+
"target", ".gradle", "public", "static", "assets",
|
|
310
|
+
]);
|
|
311
|
+
|
|
312
|
+
const SUPPORTED_EXTS = new Set([
|
|
313
|
+
".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs",
|
|
314
|
+
".py", ".go", ".rb", ".java",
|
|
315
|
+
]);
|
|
316
|
+
|
|
317
|
+
const TEST_FILE = /\.(test|spec)\.[jt]sx?$|_test\.(go|py|rb)|spec\.(rb|js|ts)$/;
|
|
318
|
+
|
|
319
|
+
function* walkFiles(dir) {
|
|
320
|
+
let entries;
|
|
321
|
+
try { entries = fs.readdirSync(dir, { withFileTypes: true }); }
|
|
322
|
+
catch { return; }
|
|
323
|
+
for (const e of entries) {
|
|
324
|
+
if (e.isDirectory()) {
|
|
325
|
+
if (!SKIP_DIRS.has(e.name)) yield* walkFiles(path.join(dir, e.name));
|
|
326
|
+
} else if (e.isFile()) {
|
|
327
|
+
const ext = path.extname(e.name);
|
|
328
|
+
if (SUPPORTED_EXTS.has(ext) && !TEST_FILE.test(e.name)) {
|
|
329
|
+
yield path.join(dir, e.name);
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// ── per-file analyzer ─────────────────────────────────────────────────────────
|
|
336
|
+
|
|
337
|
+
function analyzeFile(filePath) {
|
|
338
|
+
let code;
|
|
339
|
+
try { code = fs.readFileSync(filePath, "utf8"); }
|
|
340
|
+
catch { return []; }
|
|
341
|
+
|
|
342
|
+
const ext = path.extname(filePath);
|
|
343
|
+
|
|
344
|
+
if ([".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"].includes(ext)) {
|
|
345
|
+
return analyzeJsTs(filePath, code) || analyzeWithRegex(filePath, code) || [];
|
|
346
|
+
}
|
|
347
|
+
if (ext === ".py") {
|
|
348
|
+
return analyzePython(filePath) || analyzeWithRegex(filePath, code) || [];
|
|
349
|
+
}
|
|
350
|
+
return analyzeWithRegex(filePath, code) || [];
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// ── capability matcher ────────────────────────────────────────────────────────
|
|
354
|
+
|
|
355
|
+
function tokenise(str) {
|
|
356
|
+
return str.replace(/([a-z])([A-Z])/g, "$1 $2")
|
|
357
|
+
.toLowerCase().split(/[\s_\-/.]+/).filter(t => t.length > 1);
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
function overlap(a, b) {
|
|
361
|
+
const sa = new Set(a), sb = new Set(b);
|
|
362
|
+
let n = 0;
|
|
363
|
+
for (const t of sa) if (sb.has(t)) n++;
|
|
364
|
+
const u = sa.size + sb.size - n;
|
|
365
|
+
return u === 0 ? 0 : n / u;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
function matchFunctionToCapability(fn, capabilities) {
|
|
369
|
+
const fnTokens = tokenise(fn.name);
|
|
370
|
+
let best = null, bestScore = 0;
|
|
371
|
+
for (const cap of capabilities) {
|
|
372
|
+
const score = Math.max(
|
|
373
|
+
overlap(fnTokens, tokenise(cap.id || "")),
|
|
374
|
+
overlap(fnTokens, tokenise(cap.name || cap.title || "")),
|
|
375
|
+
);
|
|
376
|
+
if (score > bestScore) { bestScore = score; best = cap; }
|
|
377
|
+
}
|
|
378
|
+
return bestScore >= 0.2 ? { cap: best, score: bestScore } : null;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
// ── merge analysis into capability ────────────────────────────────────────────
|
|
382
|
+
|
|
383
|
+
function mergeAnalysis(existing = {}, fn, filePath, cwd) {
|
|
384
|
+
const rel = path.relative(cwd, filePath);
|
|
385
|
+
|
|
386
|
+
// merge arrays without duplicates
|
|
387
|
+
const merge = (a = [], b = []) => [...new Set([...a, ...b])];
|
|
388
|
+
|
|
389
|
+
return {
|
|
390
|
+
functions: merge(existing.functions, [fn.name]),
|
|
391
|
+
sourceFiles: merge(existing.sourceFiles, [rel]),
|
|
392
|
+
calls: merge(existing.calls, fn.calls),
|
|
393
|
+
throws: merge(existing.throws, fn.throws),
|
|
394
|
+
services: merge(existing.services, fn.services),
|
|
395
|
+
dbCalls: merge(existing.dbCalls, fn.dbCalls),
|
|
396
|
+
httpCalls: merge(existing.httpCalls, fn.httpCalls),
|
|
397
|
+
scannedAt: new Date().toISOString(),
|
|
398
|
+
};
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
// ── reporters ─────────────────────────────────────────────────────────────────
|
|
402
|
+
|
|
403
|
+
function printReport(enriched) {
|
|
404
|
+
console.log();
|
|
405
|
+
console.log(bold(" Scan Results"));
|
|
406
|
+
console.log(gray(" ─────────────────────────────────────────────────────────────────"));
|
|
407
|
+
|
|
408
|
+
for (const [capId, analysis] of Object.entries(enriched)) {
|
|
409
|
+
const { codeAnalysis: a } = analysis;
|
|
410
|
+
if (!a) continue;
|
|
411
|
+
|
|
412
|
+
console.log();
|
|
413
|
+
console.log(` ${green("●")} ${bold(capId)}`);
|
|
414
|
+
if (a.sourceFiles?.length) console.log(gray(` files: `) + a.sourceFiles.join(", "));
|
|
415
|
+
if (a.functions?.length) console.log(gray(` funcs: `) + a.functions.join(", "));
|
|
416
|
+
if (a.services?.length) console.log(gray(` services: `) + cyan(a.services.join(", ")));
|
|
417
|
+
if (a.dbCalls?.length) console.log(gray(` db: `) + a.dbCalls.slice(0, 4).join(", "));
|
|
418
|
+
if (a.httpCalls?.length) console.log(gray(` http: `) + a.httpCalls.slice(0, 4).join(", "));
|
|
419
|
+
if (a.throws?.length) console.log(gray(` throws: `) + yellow(a.throws.join(", ")));
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
console.log();
|
|
423
|
+
console.log(gray(" ─────────────────────────────────────────────────────────────────"));
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
// ── entry point ───────────────────────────────────────────────────────────────
|
|
427
|
+
|
|
428
|
+
export async function scanCommand(rawArgs) {
|
|
429
|
+
const args = rawArgs || [];
|
|
430
|
+
const dryRun = args.includes("--dry-run");
|
|
431
|
+
const jsonMode = args.includes("--json");
|
|
432
|
+
const dirIdx = args.indexOf("--dir");
|
|
433
|
+
const extraDirs = dirIdx !== -1 ? [args[dirIdx + 1]] : [];
|
|
434
|
+
const capFilter = (() => { const i = args.indexOf("--capability"); return i !== -1 ? args[i + 1] : null; })();
|
|
435
|
+
|
|
436
|
+
const cwd = process.cwd();
|
|
437
|
+
const infernoDir = path.join(cwd, "inferno");
|
|
438
|
+
|
|
439
|
+
// Load capabilities
|
|
440
|
+
const capsPath = path.join(infernoDir, "capabilities.json");
|
|
441
|
+
if (!fs.existsSync(capsPath)) {
|
|
442
|
+
console.error(red("✗ inferno/capabilities.json not found — run `infernoflow init` first."));
|
|
443
|
+
process.exit(1);
|
|
444
|
+
}
|
|
445
|
+
let capabilities;
|
|
446
|
+
try { capabilities = JSON.parse(fs.readFileSync(capsPath, "utf8")); }
|
|
447
|
+
catch (e) { console.error(red("✗ Failed to parse capabilities.json: " + e.message)); process.exit(1); }
|
|
448
|
+
|
|
449
|
+
if (!Array.isArray(capabilities)) {
|
|
450
|
+
// handle object format { capabilities: [...] }
|
|
451
|
+
if (capabilities.capabilities) capabilities = capabilities.capabilities;
|
|
452
|
+
else { console.error(red("✗ Unexpected capabilities.json format.")); process.exit(1); }
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
// Filter by --capability flag
|
|
456
|
+
const targetCaps = capFilter
|
|
457
|
+
? capabilities.filter(c => c.id === capFilter || (c.name || "").toLowerCase() === capFilter.toLowerCase())
|
|
458
|
+
: capabilities;
|
|
459
|
+
|
|
460
|
+
if (targetCaps.length === 0) {
|
|
461
|
+
console.log(yellow(capFilter ? `No capability matched: ${capFilter}` : "No capabilities found."));
|
|
462
|
+
process.exit(0);
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
// Walk source files
|
|
466
|
+
const scanDirs = [cwd, ...extraDirs];
|
|
467
|
+
if (!jsonMode) process.stdout.write(gray(" Walking source files…"));
|
|
468
|
+
const files = [];
|
|
469
|
+
for (const dir of scanDirs) {
|
|
470
|
+
for (const f of walkFiles(dir)) files.push(f);
|
|
471
|
+
}
|
|
472
|
+
if (!jsonMode) process.stdout.write(`\r Found ${files.length} source files. \n`);
|
|
473
|
+
|
|
474
|
+
// Analyze files
|
|
475
|
+
if (!jsonMode) process.stdout.write(gray(" Analyzing…"));
|
|
476
|
+
const allFunctions = []; // { fn, filePath }
|
|
477
|
+
let analyzed = 0;
|
|
478
|
+
for (const filePath of files) {
|
|
479
|
+
const fns = analyzeFile(filePath);
|
|
480
|
+
for (const fn of fns) allFunctions.push({ fn, filePath });
|
|
481
|
+
analyzed++;
|
|
482
|
+
if (!jsonMode && analyzed % 20 === 0) {
|
|
483
|
+
process.stdout.write(`\r Analyzed ${analyzed}/${files.length} files…`);
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
if (!jsonMode) process.stdout.write(`\r Analyzed ${files.length} files, found ${allFunctions.length} functions. \n`);
|
|
487
|
+
|
|
488
|
+
// Map functions to capabilities
|
|
489
|
+
const enriched = {}; // capId → { ...cap, codeAnalysis: {...} }
|
|
490
|
+
|
|
491
|
+
for (const cap of targetCaps) {
|
|
492
|
+
enriched[cap.id] = { ...cap, codeAnalysis: null };
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
for (const { fn, filePath } of allFunctions) {
|
|
496
|
+
const match = matchFunctionToCapability(fn, targetCaps);
|
|
497
|
+
if (!match) continue;
|
|
498
|
+
const { cap } = match;
|
|
499
|
+
const existing = enriched[cap.id]?.codeAnalysis || {};
|
|
500
|
+
enriched[cap.id].codeAnalysis = mergeAnalysis(existing, fn, filePath, cwd);
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
// Compute stats
|
|
504
|
+
const total = Object.keys(enriched).length;
|
|
505
|
+
const matched = Object.values(enriched).filter(e => e.codeAnalysis).length;
|
|
506
|
+
|
|
507
|
+
if (jsonMode) {
|
|
508
|
+
const out = {
|
|
509
|
+
scannedAt: new Date().toISOString(),
|
|
510
|
+
files: files.length,
|
|
511
|
+
functions: allFunctions.length,
|
|
512
|
+
capabilities: Object.entries(enriched).map(([id, data]) => ({
|
|
513
|
+
id,
|
|
514
|
+
name: data.name || data.title,
|
|
515
|
+
codeAnalysis: data.codeAnalysis,
|
|
516
|
+
})),
|
|
517
|
+
};
|
|
518
|
+
console.log(JSON.stringify(out, null, 2));
|
|
519
|
+
return;
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
printReport(enriched);
|
|
523
|
+
console.log(` ${green("✔")} Matched ${matched}/${total} capabilities to source functions`);
|
|
524
|
+
console.log();
|
|
525
|
+
|
|
526
|
+
if (dryRun) {
|
|
527
|
+
console.log(yellow(" --dry-run: no files written."));
|
|
528
|
+
return;
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
// Write scan.json
|
|
532
|
+
const scanData = {
|
|
533
|
+
scannedAt: new Date().toISOString(),
|
|
534
|
+
files: files.length,
|
|
535
|
+
functions: allFunctions.length,
|
|
536
|
+
capabilities: Object.entries(enriched).map(([id, data]) => ({
|
|
537
|
+
id,
|
|
538
|
+
name: data.name || data.title,
|
|
539
|
+
codeAnalysis: data.codeAnalysis,
|
|
540
|
+
})),
|
|
541
|
+
};
|
|
542
|
+
const scanPath = path.join(infernoDir, "scan.json");
|
|
543
|
+
fs.writeFileSync(scanPath, JSON.stringify(scanData, null, 2));
|
|
544
|
+
console.log(gray(` Saved → inferno/scan.json`));
|
|
545
|
+
|
|
546
|
+
// Enrich capabilities.json
|
|
547
|
+
let changed = 0;
|
|
548
|
+
const updatedCaps = capabilities.map(cap => {
|
|
549
|
+
const analysis = enriched[cap.id]?.codeAnalysis;
|
|
550
|
+
if (!analysis) return cap;
|
|
551
|
+
changed++;
|
|
552
|
+
return { ...cap, codeAnalysis: analysis };
|
|
553
|
+
});
|
|
554
|
+
|
|
555
|
+
if (changed > 0) {
|
|
556
|
+
fs.writeFileSync(capsPath, JSON.stringify(updatedCaps, null, 2));
|
|
557
|
+
console.log(gray(` Updated ${changed} capability entries in capabilities.json`));
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
console.log();
|
|
561
|
+
if (!ts) {
|
|
562
|
+
console.log(yellow(" ⚠ TypeScript compiler not found — JS/TS analyzed with regex fallback."));
|
|
563
|
+
console.log(gray(` For deeper analysis: npm install -g typescript`));
|
|
564
|
+
console.log();
|
|
565
|
+
}
|
|
566
|
+
}
|
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* infernoflow freeze / thaw / stability
|
|
3
|
+
*
|
|
4
|
+
* The solid/liquid layer — mark capabilities as frozen (don't touch),
|
|
5
|
+
* stable (be careful), or experimental (feel free to reshape).
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* infernoflow stability List all caps with stability level
|
|
9
|
+
* infernoflow freeze <cap-id> Mark a capability as frozen
|
|
10
|
+
* infernoflow freeze <cap-id> --stable Mark as stable (default middle tier)
|
|
11
|
+
* infernoflow thaw <cap-id> Reset to experimental
|
|
12
|
+
* infernoflow stability --json Machine-readable output
|
|
13
|
+
*
|
|
14
|
+
* Levels:
|
|
15
|
+
* experimental New or actively changing — AI may freely refactor
|
|
16
|
+
* stable Settled API — AI should be careful, prefer additive changes
|
|
17
|
+
* frozen Core contract — AI must never modify without explicit instruction
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import * as fs from "node:fs";
|
|
21
|
+
import * as path from "node:path";
|
|
22
|
+
import { bold, cyan, gray, green, yellow, red } from "../ui/output.mjs";
|
|
23
|
+
|
|
24
|
+
// ── constants ─────────────────────────────────────────────────────────────────
|
|
25
|
+
|
|
26
|
+
export const LEVELS = ["experimental", "stable", "frozen"];
|
|
27
|
+
|
|
28
|
+
const LEVEL_ICON = {
|
|
29
|
+
experimental: "🌊", // liquid — flows freely
|
|
30
|
+
stable: "〰️", // semi-fluid — treat with care
|
|
31
|
+
frozen: "🧊", // solid — do not touch
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const LEVEL_COLOR = {
|
|
35
|
+
experimental: green,
|
|
36
|
+
stable: yellow,
|
|
37
|
+
frozen: red,
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
// ── helpers ───────────────────────────────────────────────────────────────────
|
|
41
|
+
|
|
42
|
+
function loadCaps(capsPath) {
|
|
43
|
+
try {
|
|
44
|
+
const data = JSON.parse(fs.readFileSync(capsPath, "utf8"));
|
|
45
|
+
return Array.isArray(data) ? data : (data.capabilities || []);
|
|
46
|
+
} catch (e) {
|
|
47
|
+
console.error(red("✗ Failed to read capabilities.json: " + e.message));
|
|
48
|
+
process.exit(1);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function saveCaps(capsPath, caps) {
|
|
53
|
+
fs.writeFileSync(capsPath, JSON.stringify(caps, null, 2));
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function getLevel(cap) {
|
|
57
|
+
return cap.stability || "experimental";
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function bar(level) {
|
|
61
|
+
const idx = LEVELS.indexOf(level);
|
|
62
|
+
const color = LEVEL_COLOR[level] || gray;
|
|
63
|
+
const filled = "█".repeat(idx + 1);
|
|
64
|
+
const empty = "░".repeat(LEVELS.length - idx - 1);
|
|
65
|
+
return color(filled) + gray(empty);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// ── sub-commands ──────────────────────────────────────────────────────────────
|
|
69
|
+
|
|
70
|
+
function cmdList(caps, jsonMode) {
|
|
71
|
+
if (jsonMode) {
|
|
72
|
+
const out = caps.map(c => ({ id: c.id, name: c.name || c.title, stability: getLevel(c) }));
|
|
73
|
+
console.log(JSON.stringify(out, null, 2));
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const byLevel = { frozen: [], stable: [], experimental: [] };
|
|
78
|
+
for (const cap of caps) byLevel[getLevel(cap)].push(cap);
|
|
79
|
+
|
|
80
|
+
console.log();
|
|
81
|
+
console.log(bold(" Capability Stability"));
|
|
82
|
+
console.log(gray(" ───────────────────────────────────────────────────────────"));
|
|
83
|
+
console.log(
|
|
84
|
+
gray(" ") + bold(cyan("Capability".padEnd(32))) +
|
|
85
|
+
bold(cyan("Level".padEnd(16))) + bold(cyan("Solid/Liquid"))
|
|
86
|
+
);
|
|
87
|
+
console.log(gray(" ───────────────────────────────────────────────────────────"));
|
|
88
|
+
|
|
89
|
+
for (const cap of caps) {
|
|
90
|
+
const level = getLevel(cap);
|
|
91
|
+
const icon = LEVEL_ICON[level];
|
|
92
|
+
const color = LEVEL_COLOR[level] || gray;
|
|
93
|
+
console.log(
|
|
94
|
+
` ${icon} ${cap.id.padEnd(30)} ${color(level.padEnd(14))} ${bar(level)}`
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
console.log(gray(" ───────────────────────────────────────────────────────────"));
|
|
99
|
+
console.log();
|
|
100
|
+
|
|
101
|
+
const counts = {
|
|
102
|
+
frozen: byLevel.frozen.length,
|
|
103
|
+
stable: byLevel.stable.length,
|
|
104
|
+
experimental: byLevel.experimental.length,
|
|
105
|
+
};
|
|
106
|
+
console.log(
|
|
107
|
+
` ${red("🧊 Frozen:")} ${counts.frozen} ` +
|
|
108
|
+
`${yellow("〰️ Stable:")} ${counts.stable} ` +
|
|
109
|
+
`${green("🌊 Experimental:")} ${counts.experimental}`
|
|
110
|
+
);
|
|
111
|
+
console.log();
|
|
112
|
+
console.log(gray(" Tip: infernoflow freeze <cap-id> — infernoflow thaw <cap-id>"));
|
|
113
|
+
console.log();
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function cmdFreeze(caps, capsPath, capId, level) {
|
|
117
|
+
if (!LEVELS.includes(level)) {
|
|
118
|
+
console.error(red(`✗ Invalid level "${level}". Must be: ${LEVELS.join(", ")}`));
|
|
119
|
+
process.exit(1);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const idx = caps.findIndex(c => c.id === capId);
|
|
123
|
+
if (idx === -1) {
|
|
124
|
+
console.error(red(`✗ Capability "${capId}" not found in capabilities.json`));
|
|
125
|
+
process.exit(1);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const prev = getLevel(caps[idx]);
|
|
129
|
+
caps[idx] = { ...caps[idx], stability: level, stabilitySetAt: new Date().toISOString() };
|
|
130
|
+
saveCaps(capsPath, caps);
|
|
131
|
+
|
|
132
|
+
const icon = LEVEL_ICON[level];
|
|
133
|
+
const color = LEVEL_COLOR[level];
|
|
134
|
+
console.log();
|
|
135
|
+
console.log(` ${icon} ${bold(capId)} ${gray(prev)} → ${color(level)}`);
|
|
136
|
+
if (level === "frozen") {
|
|
137
|
+
console.log(gray(" AI assistants will be instructed not to modify this capability."));
|
|
138
|
+
console.log(gray(" Run `infernoflow setup` to update CLAUDE.md with this change."));
|
|
139
|
+
}
|
|
140
|
+
console.log();
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function cmdThaw(caps, capsPath, capId) {
|
|
144
|
+
const idx = caps.findIndex(c => c.id === capId);
|
|
145
|
+
if (idx === -1) {
|
|
146
|
+
console.error(red(`✗ Capability "${capId}" not found in capabilities.json`));
|
|
147
|
+
process.exit(1);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const prev = getLevel(caps[idx]);
|
|
151
|
+
caps[idx] = { ...caps[idx], stability: "experimental", stabilitySetAt: new Date().toISOString() };
|
|
152
|
+
saveCaps(capsPath, caps);
|
|
153
|
+
|
|
154
|
+
console.log();
|
|
155
|
+
console.log(` 🌊 ${bold(capId)} ${gray(prev)} → ${green("experimental")}`);
|
|
156
|
+
console.log(gray(" This capability is now liquid — free to evolve."));
|
|
157
|
+
console.log();
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// ── scan drift check (frozen caps whose files changed) ────────────────────────
|
|
161
|
+
|
|
162
|
+
export function checkFrozenDrift(infernoDir, cwd) {
|
|
163
|
+
const capsPath = path.join(infernoDir, "capabilities.json");
|
|
164
|
+
const scanPath = path.join(infernoDir, "scan.json");
|
|
165
|
+
if (!fs.existsSync(capsPath) || !fs.existsSync(scanPath)) return [];
|
|
166
|
+
|
|
167
|
+
const caps = loadCaps(capsPath);
|
|
168
|
+
const scan = JSON.parse(fs.readFileSync(scanPath, "utf8"));
|
|
169
|
+
const scannedAt = new Date(scan.scannedAt);
|
|
170
|
+
|
|
171
|
+
const warnings = [];
|
|
172
|
+
for (const cap of caps) {
|
|
173
|
+
if (getLevel(cap) !== "frozen") continue;
|
|
174
|
+
const scanEntry = scan.capabilities?.find(c => c.id === cap.id);
|
|
175
|
+
if (!scanEntry?.codeAnalysis?.sourceFiles) continue;
|
|
176
|
+
|
|
177
|
+
for (const relFile of scanEntry.codeAnalysis.sourceFiles) {
|
|
178
|
+
const absFile = path.join(cwd, relFile);
|
|
179
|
+
try {
|
|
180
|
+
const stat = fs.statSync(absFile);
|
|
181
|
+
if (stat.mtimeMs > scannedAt.getTime()) {
|
|
182
|
+
warnings.push({ capId: cap.id, file: relFile });
|
|
183
|
+
}
|
|
184
|
+
} catch {}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
return warnings;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// ── stability summary for CLAUDE.md ──────────────────────────────────────────
|
|
191
|
+
|
|
192
|
+
export function buildStabilitySummary(caps) {
|
|
193
|
+
const frozen = caps.filter(c => getLevel(c) === "frozen").map(c => c.id);
|
|
194
|
+
const stable = caps.filter(c => getLevel(c) === "stable").map(c => c.id);
|
|
195
|
+
const experimental = caps.filter(c => getLevel(c) === "experimental").map(c => c.id);
|
|
196
|
+
|
|
197
|
+
if (frozen.length === 0 && stable.length === 0) return null;
|
|
198
|
+
|
|
199
|
+
const lines = ["### Capability Stability (Solid/Liquid Layer)", ""];
|
|
200
|
+
|
|
201
|
+
if (frozen.length > 0) {
|
|
202
|
+
lines.push("**🧊 Frozen — NEVER modify without explicit instruction:**");
|
|
203
|
+
for (const id of frozen) lines.push(`- \`${id}\``);
|
|
204
|
+
lines.push("");
|
|
205
|
+
}
|
|
206
|
+
if (stable.length > 0) {
|
|
207
|
+
lines.push("**〰️ Stable — prefer additive changes, avoid breaking API:**");
|
|
208
|
+
for (const id of stable) lines.push(`- \`${id}\``);
|
|
209
|
+
lines.push("");
|
|
210
|
+
}
|
|
211
|
+
if (experimental.length > 0) {
|
|
212
|
+
lines.push(`**🌊 Experimental — free to refactor:** ${experimental.map(id => `\`${id}\``).join(", ")}`);
|
|
213
|
+
lines.push("");
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
lines.push("> Run `infernoflow stability` to see the full liquid/solid map.");
|
|
217
|
+
return lines.join("\n");
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// ── entry point ───────────────────────────────────────────────────────────────
|
|
221
|
+
|
|
222
|
+
export async function stabilityCommand(rawArgs) {
|
|
223
|
+
const args = (rawArgs || []).slice(1); // skip command name
|
|
224
|
+
const jsonMode = args.includes("--json");
|
|
225
|
+
const cwd = process.cwd();
|
|
226
|
+
const infernoDir = path.join(cwd, "inferno");
|
|
227
|
+
const capsPath = path.join(infernoDir, "capabilities.json");
|
|
228
|
+
|
|
229
|
+
if (!fs.existsSync(capsPath)) {
|
|
230
|
+
console.error(red("✗ inferno/capabilities.json not found — run `infernoflow init` first."));
|
|
231
|
+
process.exit(1);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
const caps = loadCaps(capsPath);
|
|
235
|
+
cmdList(caps, jsonMode);
|
|
236
|
+
|
|
237
|
+
// Also check for frozen drift if scan.json exists
|
|
238
|
+
const driftWarnings = checkFrozenDrift(infernoDir, cwd);
|
|
239
|
+
if (driftWarnings.length > 0) {
|
|
240
|
+
console.log(red(" ⚠ Frozen capability drift detected!"));
|
|
241
|
+
for (const w of driftWarnings) {
|
|
242
|
+
console.log(red(` ${w.capId}: ${w.file} was modified since last scan`));
|
|
243
|
+
}
|
|
244
|
+
console.log(gray(" Run `infernoflow scan` to update the baseline."));
|
|
245
|
+
console.log();
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
export async function freezeCommand(rawArgs) {
|
|
250
|
+
const args = (rawArgs || []).slice(1); // skip command name
|
|
251
|
+
const capId = args.find(a => !a.startsWith("--"));
|
|
252
|
+
const isStable = args.includes("--stable");
|
|
253
|
+
const level = isStable ? "stable" : "frozen";
|
|
254
|
+
|
|
255
|
+
if (!capId) {
|
|
256
|
+
console.error(red("✗ Usage: infernoflow freeze <capability-id> [--stable]"));
|
|
257
|
+
process.exit(1);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
const cwd = process.cwd();
|
|
261
|
+
const infernoDir = path.join(cwd, "inferno");
|
|
262
|
+
const capsPath = path.join(infernoDir, "capabilities.json");
|
|
263
|
+
|
|
264
|
+
if (!fs.existsSync(capsPath)) {
|
|
265
|
+
console.error(red("✗ inferno/capabilities.json not found."));
|
|
266
|
+
process.exit(1);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
const caps = loadCaps(capsPath);
|
|
270
|
+
cmdFreeze(caps, capsPath, capId, level);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
export async function thawCommand(rawArgs) {
|
|
274
|
+
const args = (rawArgs || []).slice(1); // skip command name
|
|
275
|
+
const capId = args.find(a => !a.startsWith("--"));
|
|
276
|
+
|
|
277
|
+
if (!capId) {
|
|
278
|
+
console.error(red("✗ Usage: infernoflow thaw <capability-id>"));
|
|
279
|
+
process.exit(1);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
const cwd = process.cwd();
|
|
283
|
+
const infernoDir = path.join(cwd, "inferno");
|
|
284
|
+
const capsPath = path.join(infernoDir, "capabilities.json");
|
|
285
|
+
|
|
286
|
+
if (!fs.existsSync(capsPath)) {
|
|
287
|
+
console.error(red("✗ inferno/capabilities.json not found."));
|
|
288
|
+
process.exit(1);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
const caps = loadCaps(capsPath);
|
|
292
|
+
cmdThaw(caps, capsPath, capId);
|
|
293
|
+
}
|