infernoflow 0.32.6 → 0.32.8

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`);
@@ -343,7 +343,7 @@ export async function explainCommand(rawArgs) {
343
343
 
344
344
  if (!jsonMode) {
345
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(", ")}`));
346
+ console.log(gray(` Found ${capIdsToExplain.length} mapped ${capIdsToExplain.length > 1 ? "capabilities" : "capability"}: ${capIdsToExplain.join(", ")}`));
347
347
  console.log();
348
348
  }
349
349
  } else {
@@ -48,7 +48,8 @@ function buildGraph(scanCaps, allCaps) {
48
48
  const reverse = {};
49
49
 
50
50
  // Build function → capId index
51
- const funcIndex = {}; // functionName capId
51
+ // Use Map (not plain object) to avoid collisions with inherited properties like toString, constructor, etc.
52
+ const funcIndex = new Map(); // functionName → capId
52
53
  for (const entry of scanCaps) {
53
54
  const capFull = allCaps.find(c => c.id === entry.id) || {};
54
55
  nodes[entry.id] = {
@@ -66,8 +67,8 @@ function buildGraph(scanCaps, allCaps) {
66
67
 
67
68
  for (const fn of (entry.codeAnalysis?.functions || [])) {
68
69
  const bare = fn.replace(/\(\)$/, "");
69
- funcIndex[bare] = entry.id;
70
- funcIndex[bare.toLowerCase()] = entry.id;
70
+ funcIndex.set(bare, entry.id);
71
+ funcIndex.set(bare.toLowerCase(), entry.id);
71
72
  }
72
73
  }
73
74
 
@@ -75,7 +76,7 @@ function buildGraph(scanCaps, allCaps) {
75
76
  for (const [capId, node] of Object.entries(nodes)) {
76
77
  for (const call of node.calls) {
77
78
  const bare = call.replace(/\(\)$/, "");
78
- const target = funcIndex[bare] || funcIndex[bare.toLowerCase()];
79
+ const target = funcIndex.get(bare) || funcIndex.get(bare.toLowerCase());
79
80
  if (target && target !== capId) {
80
81
  edges[capId].add(target);
81
82
  reverse[target].add(capId);
@@ -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.6",
3
+ "version": "0.32.8",
4
4
  "description": "The forge for liquid code - keep capabilities, contracts, and docs in sync.",
5
5
  "type": "module",
6
6
  "bin": {