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
- const registryIds = new Set((registry.capabilities || []).map(c => c?.id).filter(Boolean));
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) fail(`"${c}" has no scenario coverage`, `Add to capabilitiesCovered in a scenario file`);
132
- errors.push(`"${c}" uncovered`);
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 capId = args.find(a => !a.startsWith("--"));
286
+ const arg = args.find(a => !a.startsWith("--"));
287
287
 
288
- if (!capId) {
289
- console.error(red("✗ Usage: infernoflow explain <capability-id> [--dry-run] [--json]"));
290
- console.error(gray(" Example: infernoflow explain user-auth"));
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
- const cap = allCaps.find(c => c.id === capId);
303
- if (!cap) {
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
- // Load scan + graph
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
- // Git history
315
- const files = scanEntry?.codeAnalysis?.sourceFiles || [];
316
- const firstCommit = getFirstCommit(files[0], cwd);
317
- const recentHistory = getRecentHistory(files[0], cwd);
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
- // Scenarios
320
- const scenarios = findScenarios(capId, infernoDir);
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
- // Build prompt
323
- const prompt = buildPrompt(capId, cap, scanEntry, graph, allCaps, scenarios, firstCommit, recentHistory);
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
- if (dryRun && !jsonMode) {
326
- console.log(gray(`\n infernoflow explain → ${bold(capId)}`));
327
- console.log(gray(" ──────────────────────────────────────────────────────────────"));
328
- printExplain(capId, cap, "", null, true);
329
- console.log(bold(" Prompt that would be sent to AI:"));
330
- console.log();
331
- console.log(prompt.split("\n").map(l => " " + l).join("\n"));
332
- console.log();
333
- return;
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
- // Call AI
337
- let narrative = null;
338
- let provider = null;
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
- if (!dryRun) {
341
- try {
342
- const result = await callAI(prompt, cwd);
343
- if (result?.text) {
344
- narrative = result.text.trim();
345
- provider = result.provider;
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
- } catch {}
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
- // Fallback if no AI
351
- if (!narrative) {
352
- narrative = buildFallback(capId, cap, scanEntry, graph, allCaps, scenarios);
353
- provider = null;
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
- console.log(JSON.stringify({
358
- capId,
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) capabilities = 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
- fs.writeFileSync(capsPath, JSON.stringify(updatedCaps, null, 2));
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
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "infernoflow",
3
- "version": "0.32.5",
3
+ "version": "0.32.7",
4
4
  "description": "The forge for liquid code - keep capabilities, contracts, and docs in sync.",
5
5
  "type": "module",
6
6
  "bin": {