infernoflow 0.10.23 → 0.10.26
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.
- package/CHANGELOG.md +13 -6
- package/dist/bin/infernoflow.mjs +135 -41
- package/dist/lib/adopters/angular.mjs +128 -1
- package/dist/lib/adopters/css.mjs +111 -1
- package/dist/lib/adopters/react.mjs +104 -1
- package/dist/lib/ai/ideDetection.mjs +31 -1
- package/dist/lib/ai/localProvider.mjs +88 -1
- package/dist/lib/ai/providerRouter.mjs +73 -1
- package/dist/lib/commands/adopt.mjs +869 -20
- package/dist/lib/commands/changelog.mjs +343 -21
- package/dist/lib/commands/check.mjs +179 -3
- package/dist/lib/commands/context.mjs +287 -31
- package/dist/lib/commands/diff.mjs +274 -5
- package/dist/lib/commands/docGate.mjs +81 -2
- package/dist/lib/commands/generateSkills.mjs +163 -38
- package/dist/lib/commands/implement.mjs +103 -7
- package/dist/lib/commands/init.mjs +394 -10
- package/dist/lib/commands/installCursorHooks.mjs +36 -1
- package/dist/lib/commands/installVsCodeCopilotHooks.mjs +37 -1
- package/dist/lib/commands/prImpact.mjs +157 -2
- package/dist/lib/commands/publish.mjs +293 -15
- package/dist/lib/commands/run.mjs +336 -8
- package/dist/lib/commands/setup.mjs +234 -4
- package/dist/lib/commands/status.mjs +172 -4
- package/dist/lib/commands/suggest.mjs +563 -21
- package/dist/lib/commands/syncAuto.mjs +96 -1
- package/dist/lib/cursorHooksInstall.mjs +60 -1
- package/dist/lib/draftToolingInstall.mjs +68 -7
- package/dist/lib/git/detect-drift.mjs +208 -4
- package/dist/lib/learning/adapt.mjs +101 -6
- package/dist/lib/learning/observe.mjs +114 -1
- package/dist/lib/learning/profile.mjs +212 -2
- package/dist/lib/ui/output.mjs +72 -6
- package/dist/lib/ui/prompts.mjs +147 -6
- package/dist/lib/vsCodeCopilotHooksInstall.mjs +42 -1
- package/package.json +47 -47
|
@@ -1,21 +1,343 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
1
|
+
/**
|
|
2
|
+
* infernoflow changelog update
|
|
3
|
+
*
|
|
4
|
+
* Reads git commits since the last tag, groups them by type (feat/fix/chore/…),
|
|
5
|
+
* drafts a clean ## Unreleased section, and writes it into CHANGELOG.md.
|
|
6
|
+
*
|
|
7
|
+
* Sub-commands:
|
|
8
|
+
* infernoflow changelog update # draft Unreleased from commits
|
|
9
|
+
* infernoflow changelog show # print current Unreleased block
|
|
10
|
+
* infernoflow changelog list # list commits since last tag
|
|
11
|
+
*
|
|
12
|
+
* Flags (update):
|
|
13
|
+
* --ref <tag|commit> Compare from a specific ref instead of last tag
|
|
14
|
+
* --dry-run Print what would be written without touching the file
|
|
15
|
+
* --append Append to existing ## Unreleased instead of replacing it
|
|
16
|
+
* --json Machine-readable output
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import * as fs from "node:fs";
|
|
20
|
+
import * as path from "node:path";
|
|
21
|
+
import { execSync } from "node:child_process";
|
|
22
|
+
import { header, ok, fail, warn, info, done, bold, cyan, gray, green, yellow } from "../ui/output.mjs";
|
|
23
|
+
|
|
24
|
+
// ── git helpers ──────────────────────────────────────────────────────────────
|
|
25
|
+
|
|
26
|
+
function capture(cmd, cwd) {
|
|
27
|
+
try {
|
|
28
|
+
return execSync(cmd, { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }).trim();
|
|
29
|
+
} catch {
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function lastTag(cwd) {
|
|
35
|
+
return capture("git describe --tags --abbrev=0", cwd);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function commitsSince(ref, cwd) {
|
|
39
|
+
// Returns array of { hash, subject, body }
|
|
40
|
+
// Use NUL-delimited output to avoid shell escaping issues with separators
|
|
41
|
+
const range = ref ? `${ref}..HEAD` : "";
|
|
42
|
+
const raw = capture(`git log ${range} --format=%H%x1f%s%x1f%b%x1e`, cwd);
|
|
43
|
+
if (!raw) return [];
|
|
44
|
+
|
|
45
|
+
return raw
|
|
46
|
+
.split("\x1e")
|
|
47
|
+
.map(s => s.trim())
|
|
48
|
+
.filter(Boolean)
|
|
49
|
+
.map(block => {
|
|
50
|
+
const parts = block.split("\x1f");
|
|
51
|
+
return {
|
|
52
|
+
hash: (parts[0] || "").trim().slice(0, 8),
|
|
53
|
+
subject: (parts[1] || "").trim(),
|
|
54
|
+
body: (parts[2] || "").trim(),
|
|
55
|
+
};
|
|
56
|
+
})
|
|
57
|
+
.filter(c => c.subject);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function refDate(ref, cwd) {
|
|
61
|
+
return capture(`git log -1 --format=%ci "${ref}"`, cwd)?.slice(0, 10) || null;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// ── commit classification ────────────────────────────────────────────────────
|
|
65
|
+
|
|
66
|
+
const TYPE_MAP = {
|
|
67
|
+
feat: "Added",
|
|
68
|
+
feature: "Added",
|
|
69
|
+
add: "Added",
|
|
70
|
+
fix: "Fixed",
|
|
71
|
+
bugfix: "Fixed",
|
|
72
|
+
hotfix: "Fixed",
|
|
73
|
+
perf: "Changed",
|
|
74
|
+
refactor: "Changed",
|
|
75
|
+
change: "Changed",
|
|
76
|
+
chore: "Changed",
|
|
77
|
+
docs: "Changed",
|
|
78
|
+
style: "Changed",
|
|
79
|
+
test: "Changed",
|
|
80
|
+
ci: "Changed",
|
|
81
|
+
remove: "Removed",
|
|
82
|
+
revert: "Removed",
|
|
83
|
+
deprecate:"Removed",
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
function classifyCommit(subject) {
|
|
87
|
+
// Conventional commits: "feat: ..." or "feat(scope): ..."
|
|
88
|
+
const match = subject.match(/^(\w+)(?:\([^)]+\))?[!]?:\s*(.+)/);
|
|
89
|
+
if (match) {
|
|
90
|
+
const type = match[1].toLowerCase();
|
|
91
|
+
return {
|
|
92
|
+
section: TYPE_MAP[type] || "Changed",
|
|
93
|
+
message: match[2].trim(),
|
|
94
|
+
breaking: subject.includes("!"),
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Heuristic: starts with a keyword
|
|
99
|
+
const lower = subject.toLowerCase();
|
|
100
|
+
for (const [keyword, section] of Object.entries(TYPE_MAP)) {
|
|
101
|
+
if (lower.startsWith(keyword + " ") || lower.startsWith(keyword + ":")) {
|
|
102
|
+
return { section, message: subject, breaking: false };
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return { section: "Changed", message: subject, breaking: false };
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function groupCommits(commits) {
|
|
110
|
+
const sections = { Added: [], Fixed: [], Changed: [], Removed: [], Breaking: [] };
|
|
111
|
+
|
|
112
|
+
for (const c of commits) {
|
|
113
|
+
const { section, message, breaking } = classifyCommit(c.subject);
|
|
114
|
+
if (breaking) sections.Breaking.push(message);
|
|
115
|
+
sections[section].push(message);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Remove duplicates (some commits end up in Breaking AND their category)
|
|
119
|
+
for (const key of Object.keys(sections)) {
|
|
120
|
+
sections[key] = [...new Set(sections[key])];
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return sections;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// ── markdown rendering ───────────────────────────────────────────────────────
|
|
127
|
+
|
|
128
|
+
function renderUnreleased(sections, ref) {
|
|
129
|
+
const lines = ["## Unreleased", ""];
|
|
130
|
+
|
|
131
|
+
if (ref) {
|
|
132
|
+
lines.push(`> Changes since ${ref}`, "");
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const ORDER = ["Breaking", "Added", "Fixed", "Changed", "Removed"];
|
|
136
|
+
let hasContent = false;
|
|
137
|
+
|
|
138
|
+
for (const heading of ORDER) {
|
|
139
|
+
const items = sections[heading];
|
|
140
|
+
if (!items || !items.length) continue;
|
|
141
|
+
hasContent = true;
|
|
142
|
+
lines.push(`### ${heading}`);
|
|
143
|
+
for (const item of items) {
|
|
144
|
+
lines.push(`- ${item}`);
|
|
145
|
+
}
|
|
146
|
+
lines.push("");
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (!hasContent) {
|
|
150
|
+
lines.push("- No significant changes", "");
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return lines.join("\n");
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// ── CHANGELOG file operations ────────────────────────────────────────────────
|
|
157
|
+
|
|
158
|
+
function readChangelog(changelogPath) {
|
|
159
|
+
if (!fs.existsSync(changelogPath)) return null;
|
|
160
|
+
return fs.readFileSync(changelogPath, "utf8");
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function extractUnreleased(text) {
|
|
164
|
+
// Returns the content of the ## Unreleased block, or null
|
|
165
|
+
const match = text.match(/^## Unreleased[\s\S]*?(?=\n## |\n---|\z)/im);
|
|
166
|
+
return match ? match[0].trim() : null;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function injectUnreleased(text, newBlock) {
|
|
170
|
+
// Replace existing ## Unreleased block
|
|
171
|
+
if (/^## Unreleased/im.test(text)) {
|
|
172
|
+
return text.replace(
|
|
173
|
+
/^## Unreleased[\s\S]*?(?=\n## |\n---)/im,
|
|
174
|
+
newBlock + "\n\n"
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// No existing Unreleased block — insert after the first # heading
|
|
179
|
+
if (/^# .+/im.test(text)) {
|
|
180
|
+
return text.replace(
|
|
181
|
+
/^(# .+\n)/im,
|
|
182
|
+
`$1\n${newBlock}\n\n`
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Prepend
|
|
187
|
+
return `${newBlock}\n\n${text}`;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function appendToUnreleased(text, newBlock) {
|
|
191
|
+
// Extract just the bullet lines from newBlock and append to existing Unreleased
|
|
192
|
+
const newLines = newBlock.split("\n").filter(l => l.startsWith("- ")).join("\n");
|
|
193
|
+
if (!newLines) return text;
|
|
194
|
+
|
|
195
|
+
if (/^## Unreleased/im.test(text)) {
|
|
196
|
+
// Find end of Unreleased and insert before next section
|
|
197
|
+
return text.replace(
|
|
198
|
+
/(^## Unreleased[\s\S]*?)(\n## )/im,
|
|
199
|
+
`$1\n${newLines}\n$2`
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
return injectUnreleased(text, newBlock);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// ── sub-commands ─────────────────────────────────────────────────────────────
|
|
207
|
+
|
|
208
|
+
function subcmdList(cwd, ref) {
|
|
209
|
+
const tag = ref || lastTag(cwd);
|
|
210
|
+
const commits = commitsSince(tag, cwd);
|
|
211
|
+
|
|
212
|
+
if (!commits.length) {
|
|
213
|
+
info(`No commits since ${tag || "beginning"}`);
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
console.log(`\n ${bold("Commits since")} ${cyan(tag || "beginning")} ${gray("(" + commits.length + ")")}\n`);
|
|
218
|
+
for (const c of commits) {
|
|
219
|
+
const { section } = classifyCommit(c.subject);
|
|
220
|
+
const color = section === "Added" ? green : section === "Fixed" ? yellow : gray;
|
|
221
|
+
console.log(` ${gray(c.hash)} ${color(c.subject)}`);
|
|
222
|
+
}
|
|
223
|
+
console.log();
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function subcmdShow(changelogPath) {
|
|
227
|
+
const text = readChangelog(changelogPath);
|
|
228
|
+
if (!text) {
|
|
229
|
+
fail("CHANGELOG.md not found");
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
const block = extractUnreleased(text);
|
|
233
|
+
if (!block) {
|
|
234
|
+
warn("No ## Unreleased section found in CHANGELOG.md");
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
console.log("\n" + block + "\n");
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
async function subcmdUpdate(cwd, changelogPath, opts) {
|
|
241
|
+
const { ref, dryRun, append, asJson } = opts;
|
|
242
|
+
|
|
243
|
+
const tag = ref || lastTag(cwd);
|
|
244
|
+
const commits = commitsSince(tag, cwd);
|
|
245
|
+
|
|
246
|
+
if (!commits.length) {
|
|
247
|
+
if (asJson) {
|
|
248
|
+
console.log(JSON.stringify({ ok: true, ref: tag, commits: 0, message: "No new commits" }));
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
warn(`No commits found since ${tag || "beginning of repo"}`);
|
|
252
|
+
console.log();
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
const sections = groupCommits(commits);
|
|
257
|
+
const newBlock = renderUnreleased(sections, tag);
|
|
258
|
+
|
|
259
|
+
if (asJson) {
|
|
260
|
+
console.log(JSON.stringify({
|
|
261
|
+
ok: true,
|
|
262
|
+
ref: tag,
|
|
263
|
+
commits: commits.length,
|
|
264
|
+
sections: {
|
|
265
|
+
breaking: sections.Breaking,
|
|
266
|
+
added: sections.Added,
|
|
267
|
+
fixed: sections.Fixed,
|
|
268
|
+
changed: sections.Changed,
|
|
269
|
+
removed: sections.Removed,
|
|
270
|
+
},
|
|
271
|
+
markdown: newBlock,
|
|
272
|
+
}, null, 2));
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// Print the drafted block
|
|
277
|
+
console.log();
|
|
278
|
+
console.log(gray(" ─── Drafted entry ─────────────────────────────────"));
|
|
279
|
+
newBlock.split("\n").forEach(l => console.log(" " + l));
|
|
280
|
+
console.log(gray(" ────────────────────────────────────────────────────"));
|
|
281
|
+
console.log();
|
|
282
|
+
info(`${commits.length} commit${commits.length > 1 ? "s" : ""} since ${cyan(tag || "beginning")}`);
|
|
283
|
+
|
|
284
|
+
if (dryRun) {
|
|
285
|
+
warn("Dry run — CHANGELOG.md not modified");
|
|
286
|
+
console.log();
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// Write to CHANGELOG.md
|
|
291
|
+
let text = readChangelog(changelogPath);
|
|
292
|
+
if (!text) {
|
|
293
|
+
// Create a fresh changelog
|
|
294
|
+
text = `# Changelog\n\n`;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
const updated = append ? appendToUnreleased(text, newBlock) : injectUnreleased(text, newBlock);
|
|
298
|
+
fs.writeFileSync(changelogPath, updated);
|
|
299
|
+
|
|
300
|
+
ok(`CHANGELOG.md updated ${gray("(" + (append ? "appended" : "replaced") + " ## Unreleased)")}`);
|
|
301
|
+
console.log();
|
|
302
|
+
done("Changelog drafted — review and edit before your next release");
|
|
303
|
+
console.log(` Run ${cyan("infernoflow publish")} when ready to cut the release\n`);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// ── main ─────────────────────────────────────────────────────────────────────
|
|
307
|
+
|
|
308
|
+
export async function changelogCommand(rawArgs) {
|
|
309
|
+
const args = rawArgs.slice(1); // drop "changelog"
|
|
310
|
+
|
|
311
|
+
// Sub-command: first non-flag arg
|
|
312
|
+
const sub = args.find(a => !a.startsWith("-")) || "update";
|
|
313
|
+
|
|
314
|
+
const dryRun = args.includes("--dry-run");
|
|
315
|
+
const append = args.includes("--append");
|
|
316
|
+
const asJson = args.includes("--json");
|
|
317
|
+
|
|
318
|
+
const refIdx = args.indexOf("--ref");
|
|
319
|
+
const ref = refIdx !== -1 ? args[refIdx + 1] : null;
|
|
320
|
+
|
|
321
|
+
const cwd = process.cwd();
|
|
322
|
+
const changelogPath = path.join(cwd, "CHANGELOG.md");
|
|
323
|
+
|
|
324
|
+
if (!asJson) header("changelog " + sub);
|
|
325
|
+
|
|
326
|
+
if (sub === "list") {
|
|
327
|
+
subcmdList(cwd, ref);
|
|
328
|
+
return;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
if (sub === "show") {
|
|
332
|
+
subcmdShow(changelogPath);
|
|
333
|
+
return;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
if (sub === "update") {
|
|
337
|
+
await subcmdUpdate(cwd, changelogPath, { ref, dryRun, append, asJson });
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
fail(`Unknown sub-command: ${sub}`, "Use: update | show | list");
|
|
342
|
+
process.exit(1);
|
|
343
|
+
}
|
|
@@ -1,3 +1,179 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import { header, ok, fail, warn, info, section, done, errorAndExit, cyan, bold, red, green, yellow, gray } from "../ui/output.mjs";
|
|
4
|
+
import { docGateCommand } from "./docGate.mjs";
|
|
5
|
+
|
|
6
|
+
function readJson(filePath, jsonOut = false) {
|
|
7
|
+
try { return JSON.parse(fs.readFileSync(filePath, "utf8")); }
|
|
8
|
+
catch (err) {
|
|
9
|
+
if (jsonOut) {
|
|
10
|
+
throw new Error(`Cannot parse ${path.basename(filePath)}`);
|
|
11
|
+
}
|
|
12
|
+
errorAndExit(
|
|
13
|
+
`Cannot parse ${path.basename(filePath)}`,
|
|
14
|
+
`Check JSON syntax in: ${filePath}`
|
|
15
|
+
);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function getScenarioFiles(scenariosDir) {
|
|
20
|
+
if (!fs.existsSync(scenariosDir)) return [];
|
|
21
|
+
return fs.readdirSync(scenariosDir)
|
|
22
|
+
.filter(f => f.endsWith(".json"))
|
|
23
|
+
.map(f => path.join(scenariosDir, f));
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function getCovered(scenarioFiles) {
|
|
27
|
+
const covered = new Set();
|
|
28
|
+
for (const f of scenarioFiles) {
|
|
29
|
+
try {
|
|
30
|
+
const s = JSON.parse(fs.readFileSync(f, "utf8"));
|
|
31
|
+
(s.capabilitiesCovered || []).forEach(c => covered.add(c));
|
|
32
|
+
} catch {}
|
|
33
|
+
}
|
|
34
|
+
return covered;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export async function checkCommand(args) {
|
|
38
|
+
const cwd = process.cwd();
|
|
39
|
+
const infernoDir = path.join(cwd, "inferno");
|
|
40
|
+
const skipDocGate = args.includes("--skip-doc-gate");
|
|
41
|
+
const jsonOut = args.includes("--json");
|
|
42
|
+
|
|
43
|
+
if (!jsonOut) header("check");
|
|
44
|
+
|
|
45
|
+
const errors = [];
|
|
46
|
+
const warnings = [];
|
|
47
|
+
|
|
48
|
+
// ── inferno/ exists ─────────────────────────────────────────────
|
|
49
|
+
if (!fs.existsSync(infernoDir)) {
|
|
50
|
+
if (jsonOut) { console.log(JSON.stringify({ ok: false, errors: ["inferno/ not found"] })); process.exit(1); }
|
|
51
|
+
errorAndExit("inferno/ not found", `Run: infernoflow init`);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// ── contract.json ───────────────────────────────────────────────
|
|
55
|
+
const contractPath = path.join(infernoDir, "contract.json");
|
|
56
|
+
const capsPath = path.join(infernoDir, "capabilities.json");
|
|
57
|
+
const scenariosDir = path.join(infernoDir, "scenarios");
|
|
58
|
+
const changelogPath = path.join(infernoDir, "CHANGELOG.md");
|
|
59
|
+
|
|
60
|
+
if (!jsonOut) section("Contract");
|
|
61
|
+
if (!fs.existsSync(contractPath)) {
|
|
62
|
+
fail("contract.json not found", "Run: infernoflow init");
|
|
63
|
+
errors.push("contract.json missing");
|
|
64
|
+
} else {
|
|
65
|
+
let contract;
|
|
66
|
+
try {
|
|
67
|
+
contract = readJson(contractPath, jsonOut);
|
|
68
|
+
} catch (err) {
|
|
69
|
+
errors.push(err.message);
|
|
70
|
+
if (!jsonOut) {
|
|
71
|
+
fail(err.message);
|
|
72
|
+
process.exit(1);
|
|
73
|
+
}
|
|
74
|
+
console.log(JSON.stringify({ ok: false, errors, warnings }, null, 2));
|
|
75
|
+
process.exit(1);
|
|
76
|
+
}
|
|
77
|
+
const caps = contract.capabilities || [];
|
|
78
|
+
|
|
79
|
+
if (!contract.policyId) { fail("policyId missing"); errors.push("policyId missing"); }
|
|
80
|
+
else if (!jsonOut) ok(`policyId: ${bold(contract.policyId)}`);
|
|
81
|
+
|
|
82
|
+
if (!Number.isInteger(contract.policyVersion)) { fail("policyVersion must be an integer"); errors.push("policyVersion invalid"); }
|
|
83
|
+
else if (!jsonOut) ok(`policyVersion: ${bold("v" + contract.policyVersion)}`);
|
|
84
|
+
|
|
85
|
+
if (caps.length === 0) { fail("capabilities array is empty"); errors.push("no capabilities"); }
|
|
86
|
+
else if (!jsonOut) ok(`${caps.length} capabilities declared`);
|
|
87
|
+
|
|
88
|
+
// ── capabilities.json ────────────────────────────────────────
|
|
89
|
+
if (!jsonOut) section("Capabilities Registry");
|
|
90
|
+
if (!fs.existsSync(capsPath)) {
|
|
91
|
+
fail("capabilities.json not found"); errors.push("capabilities.json missing");
|
|
92
|
+
} else {
|
|
93
|
+
let registry;
|
|
94
|
+
try {
|
|
95
|
+
registry = readJson(capsPath, jsonOut);
|
|
96
|
+
} catch (err) {
|
|
97
|
+
errors.push(err.message);
|
|
98
|
+
if (!jsonOut) {
|
|
99
|
+
fail(err.message);
|
|
100
|
+
process.exit(1);
|
|
101
|
+
}
|
|
102
|
+
console.log(JSON.stringify({ ok: false, errors, warnings }, null, 2));
|
|
103
|
+
process.exit(1);
|
|
104
|
+
}
|
|
105
|
+
const registryIds = new Set((registry.capabilities || []).map(c => c?.id).filter(Boolean));
|
|
106
|
+
|
|
107
|
+
const missingInRegistry = caps.filter(c => !registryIds.has(c));
|
|
108
|
+
if (missingInRegistry.length > 0) {
|
|
109
|
+
missingInRegistry.forEach(c => {
|
|
110
|
+
if (!jsonOut) fail(`"${c}" in contract but missing from capabilities.json`, `Add it to inferno/capabilities.json`);
|
|
111
|
+
errors.push(`"${c}" not registered`);
|
|
112
|
+
});
|
|
113
|
+
} else if (!jsonOut) {
|
|
114
|
+
ok(`All ${registryIds.size} capabilities registered`);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// ── scenarios ───────────────────────────────────────────────
|
|
118
|
+
if (!jsonOut) section("Scenarios");
|
|
119
|
+
const scenarioFiles = getScenarioFiles(scenariosDir);
|
|
120
|
+
if (scenarioFiles.length === 0) {
|
|
121
|
+
warn("No scenarios found"); warnings.push("no scenarios");
|
|
122
|
+
} else {
|
|
123
|
+
const covered = getCovered(scenarioFiles);
|
|
124
|
+
const requireCoverage = contract?.rules?.requireScenarioForEachCapability !== false;
|
|
125
|
+
const uncovered = caps.filter(c => !covered.has(c));
|
|
126
|
+
|
|
127
|
+
if (!jsonOut) ok(`${scenarioFiles.length} scenario file(s) found`);
|
|
128
|
+
|
|
129
|
+
if (uncovered.length > 0 && requireCoverage) {
|
|
130
|
+
uncovered.forEach(c => {
|
|
131
|
+
if (!jsonOut) fail(`"${c}" has no scenario coverage`, `Add to capabilitiesCovered in a scenario file`);
|
|
132
|
+
errors.push(`"${c}" uncovered`);
|
|
133
|
+
});
|
|
134
|
+
} else if (!jsonOut) {
|
|
135
|
+
ok(`All capabilities covered by scenarios`);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// ── CHANGELOG ────────────────────────────────────────────────────
|
|
142
|
+
if (!jsonOut) section("Changelog");
|
|
143
|
+
if (!fs.existsSync(changelogPath)) {
|
|
144
|
+
fail("inferno/CHANGELOG.md not found"); errors.push("CHANGELOG missing");
|
|
145
|
+
} else {
|
|
146
|
+
const txt = fs.readFileSync(changelogPath, "utf8");
|
|
147
|
+
if (!/##\s+Unreleased/i.test(txt)) {
|
|
148
|
+
fail("Missing '## Unreleased' section", "Add it to inferno/CHANGELOG.md");
|
|
149
|
+
errors.push("CHANGELOG missing Unreleased");
|
|
150
|
+
} else if (!jsonOut) {
|
|
151
|
+
ok("CHANGELOG.md has ## Unreleased section");
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// ── doc-gate ─────────────────────────────────────────────────────
|
|
156
|
+
if (!skipDocGate) {
|
|
157
|
+
if (!jsonOut) section("Doc Gate");
|
|
158
|
+
await docGateCommand({ silent: jsonOut, captureExit: true }).catch(() => {
|
|
159
|
+
errors.push("doc-gate failed");
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// ── Summary ──────────────────────────────────────────────────────
|
|
164
|
+
if (jsonOut) {
|
|
165
|
+
console.log(JSON.stringify({ ok: errors.length === 0, errors, warnings }, null, 2));
|
|
166
|
+
if (errors.length > 0) process.exit(1);
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
console.log();
|
|
171
|
+
if (errors.length > 0) {
|
|
172
|
+
console.log(" " + red(`✘ check failed — ${errors.length} error(s)\n`));
|
|
173
|
+
process.exit(1);
|
|
174
|
+
} else if (warnings.length > 0) {
|
|
175
|
+
console.log(" " + yellow(`⚠ check passed with ${warnings.length} warning(s)\n`));
|
|
176
|
+
} else {
|
|
177
|
+
done("check passed — everything is in sync");
|
|
178
|
+
}
|
|
179
|
+
}
|