infernoflow 0.32.5 → 0.32.7
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.
|
@@ -102,7 +102,9 @@ export async function checkCommand(args) {
|
|
|
102
102
|
console.log(JSON.stringify({ ok: false, errors, warnings }, null, 2));
|
|
103
103
|
process.exit(1);
|
|
104
104
|
}
|
|
105
|
-
|
|
105
|
+
// Support both bare array format (written by scan) and { capabilities: [...] } object format
|
|
106
|
+
const regArray = Array.isArray(registry) ? registry : (registry.capabilities || []);
|
|
107
|
+
const registryIds = new Set(regArray.map(c => c?.id).filter(Boolean));
|
|
106
108
|
|
|
107
109
|
const missingInRegistry = caps.filter(c => !registryIds.has(c));
|
|
108
110
|
if (missingInRegistry.length > 0) {
|
|
@@ -127,9 +129,12 @@ export async function checkCommand(args) {
|
|
|
127
129
|
if (!jsonOut) ok(`${scenarioFiles.length} scenario file(s) found`);
|
|
128
130
|
|
|
129
131
|
if (uncovered.length > 0 && requireCoverage) {
|
|
132
|
+
// Scenario coverage is advisory — missing coverage is a warning, not a hard error.
|
|
133
|
+
// This allows `infernoflow_apply` to add new capabilities without immediately
|
|
134
|
+
// requiring scenarios (which can't exist for brand-new capabilities yet).
|
|
130
135
|
uncovered.forEach(c => {
|
|
131
|
-
if (!jsonOut)
|
|
132
|
-
|
|
136
|
+
if (!jsonOut) warn(`"${c}" has no scenario coverage`, `Add to capabilitiesCovered in a scenario file`);
|
|
137
|
+
warnings.push(`"${c}" uncovered`);
|
|
133
138
|
});
|
|
134
139
|
} else if (!jsonOut) {
|
|
135
140
|
ok(`All capabilities covered by scenarios`);
|
|
@@ -283,11 +283,13 @@ export async function explainCommand(rawArgs) {
|
|
|
283
283
|
const dryRun = args.includes("--dry-run");
|
|
284
284
|
const jsonMode = args.includes("--json");
|
|
285
285
|
|
|
286
|
-
const
|
|
286
|
+
const arg = args.find(a => !a.startsWith("--"));
|
|
287
287
|
|
|
288
|
-
if (!
|
|
289
|
-
console.error(red("✗ Usage: infernoflow explain <capability-id> [--dry-run] [--json]"));
|
|
290
|
-
console.error(gray("
|
|
288
|
+
if (!arg) {
|
|
289
|
+
console.error(red("✗ Usage: infernoflow explain <capability-id|file-path> [--dry-run] [--json]"));
|
|
290
|
+
console.error(gray(" Examples:"));
|
|
291
|
+
console.error(gray(" infernoflow explain CreateTask"));
|
|
292
|
+
console.error(gray(" infernoflow explain src/components/TaskComposer.jsx"));
|
|
291
293
|
process.exit(1);
|
|
292
294
|
}
|
|
293
295
|
|
|
@@ -299,75 +301,138 @@ export async function explainCommand(rawArgs) {
|
|
|
299
301
|
const rawCaps = loadJson(path.join(infernoDir, "capabilities.json"));
|
|
300
302
|
if (rawCaps) allCaps = Array.isArray(rawCaps) ? rawCaps : (rawCaps.capabilities || []);
|
|
301
303
|
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
console.error(red(`✗ Capability "${capId}" not found in capabilities.json`));
|
|
305
|
-
console.error(gray(" Run: infernoflow stability — to list all capability IDs"));
|
|
306
|
-
process.exit(1);
|
|
307
|
-
}
|
|
304
|
+
// Detect if arg is a file path and resolve to capability IDs
|
|
305
|
+
const isFilePath = arg.includes("/") || arg.includes("\\") || /\.\w+$/.test(arg);
|
|
308
306
|
|
|
309
|
-
|
|
310
|
-
const scanData = loadJson(path.join(infernoDir, "scan.json"));
|
|
311
|
-
const graph = loadJson(path.join(infernoDir, "graph.json"));
|
|
312
|
-
const scanEntry = scanData?.capabilities?.find(c => c.id === capId);
|
|
307
|
+
let capIdsToExplain = [];
|
|
313
308
|
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
309
|
+
if (isFilePath) {
|
|
310
|
+
// Look up which capabilities are mapped to this file via scan.json
|
|
311
|
+
const scanData = loadJson(path.join(infernoDir, "scan.json"));
|
|
312
|
+
const scanCaps = scanData?.capabilities || [];
|
|
313
|
+
const relTarget = path.relative(cwd, path.resolve(cwd, arg));
|
|
318
314
|
|
|
319
|
-
|
|
320
|
-
|
|
315
|
+
for (const entry of scanCaps) {
|
|
316
|
+
const sourceFiles = entry.codeAnalysis?.sourceFiles || [];
|
|
317
|
+
const matched = sourceFiles.some(f =>
|
|
318
|
+
f === relTarget || f.endsWith(relTarget) || relTarget.endsWith(f)
|
|
319
|
+
);
|
|
320
|
+
if (matched) capIdsToExplain.push(entry.id);
|
|
321
|
+
}
|
|
321
322
|
|
|
322
|
-
|
|
323
|
-
|
|
323
|
+
if (capIdsToExplain.length === 0) {
|
|
324
|
+
// scan.json may not exist yet — try capability-map.json
|
|
325
|
+
const capMap = loadJson(path.join(infernoDir, "capability-map.json"));
|
|
326
|
+
if (capMap) {
|
|
327
|
+
const relNorm = relTarget.replace(/\\/g, "/");
|
|
328
|
+
for (const [capId, files] of Object.entries(capMap)) {
|
|
329
|
+
const fileList = Array.isArray(files) ? files : [files];
|
|
330
|
+
if (fileList.some(f => f === relNorm || f.endsWith(relNorm) || relNorm.endsWith(f))) {
|
|
331
|
+
capIdsToExplain.push(capId);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
}
|
|
324
336
|
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
337
|
+
if (capIdsToExplain.length === 0) {
|
|
338
|
+
console.error(red(`✗ No capabilities found mapped to "${arg}"`));
|
|
339
|
+
console.error(gray(" Run: infernoflow scan — to build the source-file → capability map"));
|
|
340
|
+
console.error(gray(" Then retry: infernoflow explain " + arg));
|
|
341
|
+
process.exit(1);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
if (!jsonMode) {
|
|
345
|
+
console.log(gray(`\n infernoflow explain → ${bold(arg)}`));
|
|
346
|
+
console.log(gray(` Found ${capIdsToExplain.length} mapped ${capIdsToExplain.length > 1 ? "capabilities" : "capability"}: ${capIdsToExplain.join(", ")}`));
|
|
347
|
+
console.log();
|
|
348
|
+
}
|
|
349
|
+
} else {
|
|
350
|
+
capIdsToExplain = [arg];
|
|
334
351
|
}
|
|
335
352
|
|
|
336
|
-
//
|
|
337
|
-
|
|
338
|
-
|
|
353
|
+
// Explain each capability
|
|
354
|
+
const jsonResults = [];
|
|
355
|
+
for (const capId of capIdsToExplain) {
|
|
356
|
+
const cap = allCaps.find(c => c.id === capId);
|
|
357
|
+
if (!cap) {
|
|
358
|
+
if (!jsonMode) {
|
|
359
|
+
console.error(red(`✗ Capability "${capId}" not found in capabilities.json`));
|
|
360
|
+
console.error(gray(" Run: infernoflow stability — to list all capability IDs"));
|
|
361
|
+
}
|
|
362
|
+
continue;
|
|
363
|
+
}
|
|
339
364
|
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
365
|
+
// Load scan + graph for this cap
|
|
366
|
+
const scanData = loadJson(path.join(infernoDir, "scan.json"));
|
|
367
|
+
const graph = loadJson(path.join(infernoDir, "graph.json"));
|
|
368
|
+
const scanEntry = scanData?.capabilities?.find(c => c.id === capId);
|
|
369
|
+
|
|
370
|
+
// Git history
|
|
371
|
+
const files = scanEntry?.codeAnalysis?.sourceFiles || [];
|
|
372
|
+
const firstCommit = getFirstCommit(files[0], cwd);
|
|
373
|
+
const recentHistory = getRecentHistory(files[0], cwd);
|
|
374
|
+
|
|
375
|
+
// Scenarios
|
|
376
|
+
const scenarios = findScenarios(capId, infernoDir);
|
|
377
|
+
|
|
378
|
+
// Build prompt
|
|
379
|
+
const prompt = buildPrompt(capId, cap, scanEntry, graph, allCaps, scenarios, firstCommit, recentHistory);
|
|
380
|
+
|
|
381
|
+
if (dryRun && !jsonMode) {
|
|
382
|
+
console.log(gray(` ── ${bold(capId)} ──────────────────────────────────────────────────────`));
|
|
383
|
+
printExplain(capId, cap, "", null, true);
|
|
384
|
+
if (capIdsToExplain.length === 1) {
|
|
385
|
+
console.log(bold(" Prompt that would be sent to AI:"));
|
|
386
|
+
console.log();
|
|
387
|
+
console.log(prompt.split("\n").map(l => " " + l).join("\n"));
|
|
346
388
|
}
|
|
347
|
-
|
|
348
|
-
|
|
389
|
+
console.log();
|
|
390
|
+
continue;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
// Call AI
|
|
394
|
+
let narrative = null;
|
|
395
|
+
let provider = null;
|
|
396
|
+
|
|
397
|
+
if (!dryRun) {
|
|
398
|
+
try {
|
|
399
|
+
const result = await callAI(prompt, cwd);
|
|
400
|
+
if (result?.text) {
|
|
401
|
+
narrative = result.text.trim();
|
|
402
|
+
provider = result.provider;
|
|
403
|
+
}
|
|
404
|
+
} catch {}
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
if (!narrative) {
|
|
408
|
+
narrative = buildFallback(capId, cap, scanEntry, graph, allCaps, scenarios);
|
|
409
|
+
provider = null;
|
|
410
|
+
}
|
|
349
411
|
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
412
|
+
if (jsonMode) {
|
|
413
|
+
jsonResults.push({
|
|
414
|
+
capId,
|
|
415
|
+
name: cap.name || cap.title,
|
|
416
|
+
stability: stability(cap),
|
|
417
|
+
narrative,
|
|
418
|
+
provider: provider || "fallback",
|
|
419
|
+
sourceFiles: files,
|
|
420
|
+
scenarios: scenarios.map(s => s.scenarioId || s.description),
|
|
421
|
+
firstCommit,
|
|
422
|
+
});
|
|
423
|
+
} else {
|
|
424
|
+
if (!isFilePath) {
|
|
425
|
+
console.log(gray(`\n infernoflow explain → ${bold(capId)}`));
|
|
426
|
+
console.log(gray(" ──────────────────────────────────────────────────────────────"));
|
|
427
|
+
}
|
|
428
|
+
printExplain(capId, cap, narrative, provider, false);
|
|
429
|
+
}
|
|
354
430
|
}
|
|
355
431
|
|
|
356
432
|
if (jsonMode) {
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
name: cap.name || cap.title,
|
|
360
|
-
stability: stability(cap),
|
|
361
|
-
narrative,
|
|
362
|
-
provider: provider || "fallback",
|
|
363
|
-
sourceFiles: files,
|
|
364
|
-
scenarios: scenarios.map(s => s.scenarioId || s.description),
|
|
365
|
-
firstCommit,
|
|
366
|
-
}, null, 2));
|
|
433
|
+
const out = capIdsToExplain.length === 1 ? jsonResults[0] : jsonResults;
|
|
434
|
+
console.log(JSON.stringify(out, null, 2));
|
|
367
435
|
return;
|
|
368
436
|
}
|
|
369
437
|
|
|
370
|
-
console.log(gray(`\n infernoflow explain → ${bold(capId)}`));
|
|
371
|
-
console.log(gray(" ──────────────────────────────────────────────────────────────"));
|
|
372
|
-
printExplain(capId, cap, narrative, provider, false);
|
|
373
438
|
}
|
|
@@ -462,12 +462,18 @@ export async function scanCommand(rawArgs) {
|
|
|
462
462
|
process.exit(1);
|
|
463
463
|
}
|
|
464
464
|
let capabilities;
|
|
465
|
+
let capsFileIsObject = false; // track original format so we can write it back correctly
|
|
466
|
+
let capsFileWrapper = null;
|
|
465
467
|
try { capabilities = JSON.parse(fs.readFileSync(capsPath, "utf8")); }
|
|
466
468
|
catch (e) { console.error(red("✗ Failed to parse capabilities.json: " + e.message)); process.exit(1); }
|
|
467
469
|
|
|
468
470
|
if (!Array.isArray(capabilities)) {
|
|
469
471
|
// handle object format { capabilities: [...] }
|
|
470
|
-
if (capabilities.capabilities)
|
|
472
|
+
if (capabilities.capabilities) {
|
|
473
|
+
capsFileIsObject = true;
|
|
474
|
+
capsFileWrapper = capabilities; // preserve other top-level keys
|
|
475
|
+
capabilities = capabilities.capabilities;
|
|
476
|
+
}
|
|
471
477
|
else { console.error(red("✗ Unexpected capabilities.json format.")); process.exit(1); }
|
|
472
478
|
}
|
|
473
479
|
|
|
@@ -572,7 +578,11 @@ export async function scanCommand(rawArgs) {
|
|
|
572
578
|
});
|
|
573
579
|
|
|
574
580
|
if (changed > 0) {
|
|
575
|
-
|
|
581
|
+
// Preserve the original file format: object wrapper or bare array
|
|
582
|
+
const toWrite = capsFileIsObject
|
|
583
|
+
? { ...capsFileWrapper, capabilities: updatedCaps }
|
|
584
|
+
: updatedCaps;
|
|
585
|
+
fs.writeFileSync(capsPath, JSON.stringify(toWrite, null, 2));
|
|
576
586
|
console.log(gray(` Updated ${changed} capability entries in capabilities.json`));
|
|
577
587
|
}
|
|
578
588
|
|