infernoflow 0.10.6 → 0.10.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.
@@ -5,7 +5,7 @@ import { header, ok, fail, warn, info, done, section, nextSteps, bold, cyan, gra
5
5
 
6
6
  // ── Helpers ──────────────────────────────────────────────────────────────────
7
7
 
8
- function readJson(filePath) {
8
+ export function readJson(filePath) {
9
9
  try { return JSON.parse(fs.readFileSync(filePath, "utf8")); }
10
10
  catch { return null; }
11
11
  }
@@ -25,7 +25,7 @@ function toCapabilityId(str) {
25
25
  .join("");
26
26
  }
27
27
 
28
- function buildPrompt({ description, contract, capabilities, scenarios }) {
28
+ export function buildPrompt({ description, contract, capabilities, scenarios }) {
29
29
  const capsIds = contract.capabilities || [];
30
30
  const capsDetail = (capabilities?.capabilities || [])
31
31
  .map(c => ` - ${c.id}: ${c.title || c.id}`)
@@ -87,7 +87,7 @@ Rules:
87
87
  - Keep it minimal and accurate`;
88
88
  }
89
89
 
90
- function validateSuggestion(suggestion) {
90
+ export function validateSuggestion(suggestion) {
91
91
  const errors = [];
92
92
  if (!suggestion || typeof suggestion !== "object") {
93
93
  return ["AI response must be a JSON object."];
@@ -146,7 +146,7 @@ function validateSuggestion(suggestion) {
146
146
  return errors;
147
147
  }
148
148
 
149
- function detectSuggestionConflicts(contract, suggestion) {
149
+ export function detectSuggestionConflicts(contract, suggestion) {
150
150
  const issues = [];
151
151
  const existing = new Set(contract.capabilities || []);
152
152
  const newIds = new Set((suggestion.newCapabilities || []).map((c) => c.id));
@@ -170,7 +170,7 @@ function detectSuggestionConflicts(contract, suggestion) {
170
170
  return issues;
171
171
  }
172
172
 
173
- function applyChanges({ cwd, contract, capabilities, suggestion, version }) {
173
+ export function applyChanges({ cwd, contract, capabilities, suggestion, version, quiet = false }) {
174
174
  const infernoDir = path.join(cwd, "inferno");
175
175
  const contractPath = path.join(infernoDir, "contract.json");
176
176
  const capsPath = path.join(infernoDir, "capabilities.json");
@@ -195,7 +195,7 @@ function applyChanges({ cwd, contract, capabilities, suggestion, version }) {
195
195
  const nextVersion = Number(contract.policyVersion || 1) + 1;
196
196
  const contractUpdated = { ...contract, capabilities: updatedCaps, policyVersion: nextVersion };
197
197
  queueWrite(contractPath, JSON.stringify(contractUpdated, null, 2) + "\n");
198
- ok(`contract.json updated → policyVersion: v${nextVersion}`);
198
+ if (!quiet) ok(`contract.json updated → policyVersion: v${nextVersion}`);
199
199
  changed = true;
200
200
  }
201
201
 
@@ -209,7 +209,7 @@ function applyChanges({ cwd, contract, capabilities, suggestion, version }) {
209
209
  }
210
210
  }
211
211
  queueWrite(capsPath, JSON.stringify(reg, null, 2) + "\n");
212
- ok(`capabilities.json updated`);
212
+ if (!quiet) ok(`capabilities.json updated`);
213
213
  }
214
214
 
215
215
  // ── scenarios ─────────────────────────────────────────────────────────────
@@ -225,7 +225,7 @@ function applyChanges({ cwd, contract, capabilities, suggestion, version }) {
225
225
  steps: us.stepsToAdd || []
226
226
  };
227
227
  queueWrite(filePath, JSON.stringify(scenario, null, 2) + "\n");
228
- ok(`Created scenario: ${cyan(us.file)}`);
228
+ if (!quiet) ok(`Created scenario: ${cyan(us.file)}`);
229
229
  } else {
230
230
  scenario = readJson(filePath);
231
231
  const existingCaps = new Set(scenario.capabilitiesCovered || []);
@@ -233,7 +233,7 @@ function applyChanges({ cwd, contract, capabilities, suggestion, version }) {
233
233
  scenario.capabilitiesCovered = [...existingCaps];
234
234
  scenario.steps = [...(scenario.steps || []), ...(us.stepsToAdd || [])];
235
235
  queueWrite(filePath, JSON.stringify(scenario, null, 2) + "\n");
236
- ok(`Updated scenario: ${cyan(us.file)}`);
236
+ if (!quiet) ok(`Updated scenario: ${cyan(us.file)}`);
237
237
  }
238
238
  changed = true;
239
239
  }
@@ -244,7 +244,7 @@ function applyChanges({ cwd, contract, capabilities, suggestion, version }) {
244
244
  if (/##\s+Unreleased/i.test(txt)) {
245
245
  txt = txt.replace(/(##\s+Unreleased[^\n]*\n)/i, `$1\n${changelogEntry}\n`);
246
246
  queueWrite(changelogPath, txt);
247
- ok(`CHANGELOG.md updated`);
247
+ if (!quiet) ok(`CHANGELOG.md updated`);
248
248
  changed = true;
249
249
  }
250
250
  }
@@ -275,6 +275,37 @@ function applyChanges({ cwd, contract, capabilities, suggestion, version }) {
275
275
  return changed;
276
276
  }
277
277
 
278
+ export function parseSuggestionJson(rawInput) {
279
+ const clean = String(rawInput || "").trim().replace(/^```json?\n?/, "").replace(/\n?```$/, "");
280
+ return JSON.parse(clean);
281
+ }
282
+
283
+ export function loadSuggestContext(cwd) {
284
+ const infernoDir = path.join(cwd, "inferno");
285
+ const contractPath = path.join(infernoDir, "contract.json");
286
+ const capsPath = path.join(infernoDir, "capabilities.json");
287
+ const scenariosDir = path.join(infernoDir, "scenarios");
288
+
289
+ const contract = readJson(contractPath);
290
+ const capabilities = readJson(capsPath);
291
+ const scenarios = [];
292
+ if (fs.existsSync(scenariosDir)) {
293
+ for (const f of fs.readdirSync(scenariosDir).filter((name) => name.endsWith(".json"))) {
294
+ const s = readJson(path.join(scenariosDir, f));
295
+ if (s) scenarios.push({ ...s, _file: f });
296
+ }
297
+ }
298
+
299
+ let version = "0.1.0";
300
+ const pkgPath = path.join(cwd, "package.json");
301
+ if (fs.existsSync(pkgPath)) {
302
+ const pkg = readJson(pkgPath);
303
+ if (pkg?.version) version = pkg.version;
304
+ }
305
+
306
+ return { contract, capabilities, scenarios, version };
307
+ }
308
+
278
309
  // ── Main ─────────────────────────────────────────────────────────────────────
279
310
 
280
311
  export async function suggestCommand(args) {
@@ -375,8 +406,7 @@ export async function suggestCommand(args) {
375
406
  // ── Parse response ────────────────────────────────────────────────────────
376
407
  let suggestion;
377
408
  try {
378
- const clean = jsonInput.trim().replace(/^```json?\n?/, "").replace(/\n?```$/, "");
379
- suggestion = JSON.parse(clean);
409
+ suggestion = parseSuggestionJson(jsonInput);
380
410
  } catch {
381
411
  errorAndExit(
382
412
  "Could not parse the AI response as JSON",
@@ -0,0 +1,96 @@
1
+ import { execFileSync } from "node:child_process";
2
+ import * as path from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ import { header, section, ok, warn, yellow, gray } from "../ui/output.mjs";
5
+
6
+ const __filename = fileURLToPath(import.meta.url);
7
+ const __dirname = path.dirname(__filename);
8
+ const binPath = path.resolve(__dirname, "..", "..", "bin", "infernoflow.mjs");
9
+
10
+ function runCliJson(args) {
11
+ const out = execFileSync(process.execPath, [binPath, ...args], { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] });
12
+ return JSON.parse(out);
13
+ }
14
+
15
+ function tryRunCliJson(args) {
16
+ try {
17
+ return { ok: true, data: runCliJson(args) };
18
+ } catch (err) {
19
+ const stdout = err?.stdout?.toString?.() || "";
20
+ try {
21
+ return { ok: false, data: JSON.parse(stdout) };
22
+ } catch {
23
+ return { ok: false, data: { ok: false, errors: ["command_failed"] } };
24
+ }
25
+ }
26
+ }
27
+
28
+ export async function syncCommand(args = []) {
29
+ const auto = args.includes("--auto");
30
+ const asJson = args.includes("--json");
31
+ const dryRun = args.includes("--dry-run");
32
+
33
+ if (!auto) {
34
+ const payload = { ok: false, error: "missing_required_flag", hint: "Use: infernoflow sync --auto" };
35
+ if (asJson) {
36
+ console.log(JSON.stringify(payload, null, 2));
37
+ process.exit(1);
38
+ }
39
+ header("sync");
40
+ warn("missing --auto flag");
41
+ console.log(` ${yellow("→")} infernoflow sync --auto`);
42
+ console.log();
43
+ process.exit(1);
44
+ }
45
+
46
+ const impact = tryRunCliJson(["pr-impact", "--json"]);
47
+ const needsSync = !impact.data?.ok;
48
+ const confidence = impact.data?.confidence || "low";
49
+ const policyDecision = confidence === "high" ? "auto" : confidence === "medium" ? "ask" : "block";
50
+ const actions = needsSync
51
+ ? ["Generate inferno update proposal (suggest)", "Review changes", "Validate with check --json"]
52
+ : ["No inferno drift detected", "Validate with check --json"];
53
+
54
+ const check = tryRunCliJson(["check", "--json"]);
55
+ const payload = {
56
+ ok: impact.ok && check.ok && !!check.data?.ok,
57
+ mode: "auto-skeleton",
58
+ dryRun,
59
+ needsSync,
60
+ didApply: false,
61
+ confidence,
62
+ policyDecision,
63
+ actions,
64
+ prImpact: impact.data,
65
+ postCheck: check.data,
66
+ reasonCodes: [
67
+ ...(needsSync ? ["DRIFT_DETECTED"] : ["NO_DRIFT"]),
68
+ `POLICY_${policyDecision.toUpperCase()}`,
69
+ ...(policyDecision === "auto" ? ["AUTO_APPLY_DISABLED_IN_SKELETON"] : []),
70
+ ],
71
+ };
72
+
73
+ if (asJson) {
74
+ console.log(JSON.stringify(payload, null, 2));
75
+ process.exit(payload.ok ? 0 : 1);
76
+ }
77
+
78
+ header("sync --auto");
79
+ section("State");
80
+ if (needsSync) warn("Inferno drift detected");
81
+ else ok("No inferno drift detected");
82
+ ok(`Confidence: ${gray(confidence)}`);
83
+ ok(`Policy decision: ${gray(policyDecision)}`);
84
+ ok(`Apply mode: ${gray("skeleton (no file writes)")}`);
85
+ if (dryRun) ok("Dry run enabled");
86
+
87
+ section("Plan");
88
+ actions.forEach((a) => console.log(` ${yellow("→")} ${a}`));
89
+
90
+ section("Validation");
91
+ if (check.ok && check.data?.ok) ok("Post-check passed");
92
+ else warn("Post-check failed; see infernoflow check --json");
93
+ console.log();
94
+ process.exit(payload.ok ? 0 : 1);
95
+ }
96
+
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "infernoflow",
3
- "version": "0.10.6",
3
+ "version": "0.10.8",
4
4
  "description": "The forge for liquid code — keep capabilities, contracts, and docs in sync.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -16,7 +16,7 @@
16
16
  "README.md"
17
17
  ],
18
18
  "scripts": {
19
- "test": "node scripts/smoke.mjs && node scripts/json-smoke.mjs && node scripts/json-negative-smoke.mjs && node scripts/implement-smoke.mjs",
19
+ "test": "node scripts/smoke.mjs && node scripts/json-smoke.mjs && node scripts/json-negative-smoke.mjs && node scripts/implement-smoke.mjs && node scripts/adopt-smoke.mjs && node scripts/pr-impact-smoke.mjs && node scripts/sync-smoke.mjs && node scripts/run-smoke.mjs",
20
20
  "test:help": "node bin/infernoflow.mjs --help"
21
21
  },
22
22
  "keywords": [
@@ -0,0 +1,33 @@
1
+ name: infernoflow-check
2
+
3
+ on:
4
+ pull_request:
5
+ push:
6
+ branches: [main, master]
7
+
8
+ jobs:
9
+ inferno:
10
+ runs-on: ubuntu-latest
11
+ steps:
12
+ - name: Checkout
13
+ uses: actions/checkout@v4
14
+ with:
15
+ fetch-depth: 0
16
+
17
+ - name: Setup Node
18
+ uses: actions/setup-node@v4
19
+ with:
20
+ node-version: "20"
21
+
22
+ - name: Install dependencies
23
+ run: npm ci --ignore-scripts || npm install --ignore-scripts
24
+
25
+ - name: Inferno one-command run
26
+ run: npx infernoflow run "sync check" --json
27
+ env:
28
+ INFERNO_LOCAL_PROVIDER: ollama
29
+ INFERNO_LOCAL_ENDPOINT: http://127.0.0.1:11434/api/generate
30
+ INFERNO_LOCAL_MODEL: llama3.1:8b
31
+ BASE_SHA: ${{ github.event.pull_request.base.sha || github.event.before }}
32
+ HEAD_SHA: ${{ github.event.pull_request.head.sha || github.sha }}
33
+
@@ -0,0 +1,36 @@
1
+ #!/usr/bin/env node
2
+ import * as fs from "node:fs";
3
+ import * as path from "node:path";
4
+
5
+ const cwd = process.cwd();
6
+ const gitDir = path.join(cwd, ".git");
7
+ const hooksDir = path.join(gitDir, "hooks");
8
+
9
+ if (!fs.existsSync(gitDir)) {
10
+ console.error("[inferno hooks] .git not found. Run inside a git repository.");
11
+ process.exit(1);
12
+ }
13
+
14
+ fs.mkdirSync(hooksDir, { recursive: true });
15
+
16
+ const preCommit = `#!/bin/sh
17
+ echo "[inferno hooks] pre-commit: infernoflow run --dry-run"
18
+ npx infernoflow run "sync check" --dry-run
19
+ `;
20
+
21
+ const prePush = `#!/bin/sh
22
+ echo "[inferno hooks] pre-push: infernoflow run --json"
23
+ npx infernoflow run "sync check" --json
24
+ `;
25
+
26
+ const writeHook = (name, content) => {
27
+ const filePath = path.join(hooksDir, name);
28
+ fs.writeFileSync(filePath, content, "utf8");
29
+ fs.chmodSync(filePath, 0o755);
30
+ console.log(`[inferno hooks] installed ${name}`);
31
+ };
32
+
33
+ writeHook("pre-commit", preCommit);
34
+ writeHook("pre-push", prePush);
35
+ console.log("[inferno hooks] done");
36
+