infernoflow 0.32.5 → 0.32.6

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.
@@ -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 capability${capIdsToExplain.length > 1 ? "ies" : "y"}: ${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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "infernoflow",
3
- "version": "0.32.5",
3
+ "version": "0.32.6",
4
4
  "description": "The forge for liquid code - keep capabilities, contracts, and docs in sync.",
5
5
  "type": "module",
6
6
  "bin": {