arcane-cli 0.34.2 → 0.35.1

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/dist/index.js CHANGED
@@ -6,7 +6,7 @@ const require = __createRequire(import.meta.url);
6
6
  import { Command } from "commander";
7
7
  import { createRequire as createRequire2 } from "module";
8
8
  import { fileURLToPath } from "url";
9
- import { dirname as dirname4, join as join14 } from "path";
9
+ import { dirname as dirname5, join as join17 } from "path";
10
10
 
11
11
  // src/commands/init.ts
12
12
  import { select as select2, confirm, input as input2 } from "@inquirer/prompts";
@@ -758,7 +758,7 @@ var WORDMARK = [
758
758
  var INDENT = " ";
759
759
  var LOCKUP_WIDTH = INDENT.length + Math.max(...WORDMARK.map((l) => l.length));
760
760
  function sleep(ms) {
761
- return new Promise((resolve3) => setTimeout(resolve3, ms));
761
+ return new Promise((resolve4) => setTimeout(resolve4, ms));
762
762
  }
763
763
  function isAnimatable() {
764
764
  return !!process.stdout.isTTY && process.env["TERM"] !== "dumb" && process.env["CI"] !== "true" && process.env["ARCANE_DISABLE_ANIMATION"] !== "true" && (process.stdout.columns ?? 80) >= LOCKUP_WIDTH;
@@ -2030,7 +2030,7 @@ function buildGitEnv() {
2030
2030
  function runGit(cwd, args, options = {}) {
2031
2031
  const commandClass = options.commandClass ?? classifyGitCommand(args);
2032
2032
  const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUTS_MS[commandClass] ?? DEFAULT_TIMEOUTS_MS.write;
2033
- return new Promise((resolve3, reject) => {
2033
+ return new Promise((resolve4, reject) => {
2034
2034
  const child = execFile(
2035
2035
  "git",
2036
2036
  args,
@@ -2064,7 +2064,7 @@ function runGit(cwd, args, options = {}) {
2064
2064
  reject(error);
2065
2065
  return;
2066
2066
  }
2067
- resolve3({ stdout, stderr });
2067
+ resolve4({ stdout, stderr });
2068
2068
  }
2069
2069
  );
2070
2070
  child.stdin?.end();
@@ -4388,8 +4388,8 @@ async function checkVSCodeExtension(id, label) {
4388
4388
  ];
4389
4389
  for (const dir of extDirs) {
4390
4390
  try {
4391
- const { readdir: readdir7 } = await import("fs/promises");
4392
- const entries = await readdir7(dir);
4391
+ const { readdir: readdir8 } = await import("fs/promises");
4392
+ const entries = await readdir8(dir);
4393
4393
  const prefix = id.toLowerCase() + "-";
4394
4394
  const found = entries.some(
4395
4395
  (e) => e.toLowerCase() === id.toLowerCase() || e.toLowerCase().startsWith(prefix)
@@ -5097,11 +5097,676 @@ Name did not match ("${typed.trim()}" \u2260 "${repoName}"). Nothing changed.`);
5097
5097
  );
5098
5098
  }
5099
5099
 
5100
+ // src/commands/report.ts
5101
+ import { access as access5 } from "fs/promises";
5102
+ import { join as join16 } from "path";
5103
+
5104
+ // src/modules/show-report/generate.ts
5105
+ import { mkdir as mkdir7, readdir as readdir7, readFile as readFile13, writeFile as writeFile10 } from "fs/promises";
5106
+ import { basename as basename3, dirname as dirname4, isAbsolute, join as join15, relative as relative3, resolve as resolve3 } from "path";
5107
+
5108
+ // src/modules/show-report/model.ts
5109
+ import { readFile as readFile12 } from "fs/promises";
5110
+ import { join as join14 } from "path";
5111
+
5112
+ // src/modules/show-report/plan-parser.ts
5113
+ function parseFrontmatter(content) {
5114
+ const match = /^---\r?\n([\s\S]*?)\r?\n---/.exec(content);
5115
+ if (!match) return {};
5116
+ const fields = {};
5117
+ for (const line of match[1].split(/\r?\n/)) {
5118
+ const kv = /^([a-zA-Z][\w-]*):\s*(.*)$/.exec(line);
5119
+ if (kv) fields[kv[1]] = kv[2].trim();
5120
+ }
5121
+ return fields;
5122
+ }
5123
+ var EPIC_START = /^- \[([ x])\] \*\*([A-Z]+-\d+) — /gm;
5124
+ var EPIC_TITLE = /^- \[[ x]\] \*\*[A-Z]+-\d+ — ([\s\S]+?)\.\*\*/;
5125
+ var HEADING_LINE = /^(#{2,3}) (.+)$/gm;
5126
+ var REPORT_LINE = /\*\*Report:\*\*\s*([\s\S]*?)\s*·\s*category:\s*(\S+)\s*·\s*glyph:\s*(\S+)/;
5127
+ var TITLE_OVERRIDE_LINE = /^\s*title:\s*(.+)$/m;
5128
+ var PR_LINK = /\[PR #(\d+)\]\((https:\/\/github\.com\/[^)\s]+\/pull\/\d+)\)/g;
5129
+ function parseEpics(content) {
5130
+ const markers = [];
5131
+ EPIC_START.lastIndex = 0;
5132
+ let epicMatch;
5133
+ while ((epicMatch = EPIC_START.exec(content)) !== null) {
5134
+ markers.push({ index: epicMatch.index, type: "epic", id: epicMatch[2], done: epicMatch[1] === "x" });
5135
+ }
5136
+ HEADING_LINE.lastIndex = 0;
5137
+ let headingMatch;
5138
+ while ((headingMatch = HEADING_LINE.exec(content)) !== null) {
5139
+ if (headingMatch[1] === "##") {
5140
+ markers.push({ index: headingMatch.index, type: "h2" });
5141
+ } else {
5142
+ markers.push({ index: headingMatch.index, type: "h3", waveTitle: headingMatch[2].trim() });
5143
+ }
5144
+ }
5145
+ markers.sort((a, b) => a.index - b.index);
5146
+ const epics = [];
5147
+ let currentWave = null;
5148
+ for (let i = 0; i < markers.length; i += 1) {
5149
+ const marker = markers[i];
5150
+ if (marker.type === "h2") {
5151
+ currentWave = null;
5152
+ continue;
5153
+ }
5154
+ if (marker.type === "h3") {
5155
+ currentWave = marker.waveTitle;
5156
+ continue;
5157
+ }
5158
+ const blockEnd = i + 1 < markers.length ? markers[i + 1].index : content.length;
5159
+ const block = content.slice(marker.index, blockEnd);
5160
+ const titleMatch = EPIC_TITLE.exec(block);
5161
+ const reportMatch = REPORT_LINE.exec(block);
5162
+ const titleOverrideMatch = TITLE_OVERRIDE_LINE.exec(block);
5163
+ const prLinks = [...new Set([...block.matchAll(PR_LINK)].map((m) => m[2]))];
5164
+ epics.push({
5165
+ id: marker.id,
5166
+ // A title that still fails to match (e.g. a malformed bullet with no
5167
+ // closing `.**` at all) falls back to the raw id rather than throwing --
5168
+ // an epic missing from the report is far more visible, and therefore
5169
+ // more likely to get fixed, than a crashed report generator.
5170
+ title: titleMatch ? titleMatch[1].replace(/\s+/g, " ").trim() : marker.id,
5171
+ done: marker.done,
5172
+ wave: currentWave,
5173
+ report: reportMatch ? {
5174
+ description: reportMatch[1].replace(/\s+/g, " ").trim(),
5175
+ category: reportMatch[2],
5176
+ glyph: reportMatch[3],
5177
+ titleOverride: titleOverrideMatch ? titleOverrideMatch[1].trim() : void 0
5178
+ } : null,
5179
+ prLinks,
5180
+ block
5181
+ });
5182
+ }
5183
+ return epics;
5184
+ }
5185
+ function extractSection(content, headingPrefix) {
5186
+ const lines = content.split(/\r?\n/);
5187
+ const startIndex = lines.findIndex((line) => line.startsWith(headingPrefix));
5188
+ if (startIndex === -1) return null;
5189
+ let endIndex = lines.length;
5190
+ for (let i = startIndex + 1; i < lines.length; i += 1) {
5191
+ if (lines[i].startsWith("## ")) {
5192
+ endIndex = i;
5193
+ break;
5194
+ }
5195
+ }
5196
+ return lines.slice(startIndex + 1, endIndex).join("\n");
5197
+ }
5198
+ function parseParkedSection(content) {
5199
+ const body = extractSection(content, "## Parked \u2014 Needs Operator");
5200
+ if (body === null) return [];
5201
+ const tableRows = [...body.matchAll(/^\|(?!---)([^|]+)\|([^|]+)\|([^|]+)\|\s*$/gm)];
5202
+ if (tableRows.length > 1) {
5203
+ return tableRows.slice(1).map((m) => ({
5204
+ title: stripMarkdown(m[1].trim()),
5205
+ reason: stripMarkdown(m[3].trim())
5206
+ }));
5207
+ }
5208
+ const bulletItems = [...body.matchAll(/^- \*\*(.+?)\*\*\s*(?:\([^)]*\))?\s*—\s*(.+)$/gm)];
5209
+ return bulletItems.map((m) => ({
5210
+ title: stripMarkdown(m[1].trim()),
5211
+ reason: stripMarkdown(m[2].trim())
5212
+ }));
5213
+ }
5214
+ function computeCoverageOutcome(content) {
5215
+ const body = extractSection(content, "## Coverage Map");
5216
+ if (body === null) return void 0;
5217
+ const rows = [...body.matchAll(/^\|(.+)\|\s*$/gm)].filter((m) => !/^-+\s*\|/.test(m[1].trim()));
5218
+ if (rows.length < 2) return void 0;
5219
+ const dataRows = rows.slice(1);
5220
+ const parkedCount = dataRows.filter((m) => /\*\*Park(?:ed)?\*\*/i.test(m[1])).length;
5221
+ const total = dataRows.length;
5222
+ const dispositioned = total - parkedCount;
5223
+ return `${total} of ${total} items dispositioned \u2014 ${dispositioned} routed to an epic, ${parkedCount} parked`;
5224
+ }
5225
+ function stripMarkdown(text) {
5226
+ return text.replace(/\[([^\]]+)\]\([^)]+\)/g, "$1").replace(/\*\*/g, "").replace(/`/g, "").trim();
5227
+ }
5228
+
5229
+ // src/modules/show-report/queue-parser.ts
5230
+ var LINK = /\[[^\]]+\]\((https?:\/\/[^)\s]+)\)/;
5231
+ function parseOperatorQueue(content) {
5232
+ const sections = content.split(/^## /m).slice(1);
5233
+ const entries = [];
5234
+ for (const section of sections) {
5235
+ const headingMatch = /^(Q-\d+)\s*—\s*(.+?)\r?\n/.exec(section);
5236
+ if (!headingMatch) continue;
5237
+ const id = headingMatch[1];
5238
+ const title = headingMatch[2].trim();
5239
+ const statusMatch = /^-\s*\*\*Status:\*\*\s*\[([ x])\]/m.exec(section);
5240
+ const done = statusMatch ? statusMatch[1] === "x" : false;
5241
+ const whyMatch = /^-\s*\*\*Why:\*\*\s*([\s\S]*?)(?=\r?\n-\s*\*\*|\r?\n\r?\n|$)/m.exec(section);
5242
+ const why = whyMatch ? whyMatch[1].replace(/\s+/g, " ").trim() : null;
5243
+ const linkMatch = LINK.exec(section);
5244
+ entries.push({ id, title, done, why, href: linkMatch ? linkMatch[1] : void 0 });
5245
+ }
5246
+ return entries;
5247
+ }
5248
+
5249
+ // src/modules/show-report/ledger-parser.ts
5250
+ function parseVerificationLedgerSection(content, programNameWords) {
5251
+ const sections = content.split(/^## /m).slice(1);
5252
+ const rows = [];
5253
+ for (const section of sections) {
5254
+ const headingLine = section.slice(0, section.indexOf("\n"));
5255
+ if (!headingLine.toLowerCase().includes(programNameWords.toLowerCase())) continue;
5256
+ const tableRows = [...section.matchAll(/^\|([^|]+)\|([^|]+)\|([^|]+)\|([^|]+)\|\s*$/gm)];
5257
+ for (const m of tableRows.slice(1)) {
5258
+ const claimCell = m[1].trim();
5259
+ const resultCell = m[3].trim().toLowerCase();
5260
+ const correctionCell = m[4].trim();
5261
+ if (claimCell === "---" || claimCell.startsWith("---")) continue;
5262
+ if (resultCell !== "confirmed" && resultCell !== "corrected" && resultCell !== "unverifiable") continue;
5263
+ rows.push({
5264
+ claim: stripMarkdown2(claimCell),
5265
+ result: resultCell,
5266
+ correction: correctionCell === "\u2014" ? null : stripMarkdown2(correctionCell)
5267
+ });
5268
+ }
5269
+ }
5270
+ return rows;
5271
+ }
5272
+ function stripMarkdown2(text) {
5273
+ return text.replace(/\[([^\]]+)\]\([^)]+\)/g, "$1").replace(/\*\*/g, "").replace(/`/g, "").trim();
5274
+ }
5275
+
5276
+ // src/modules/show-report/decisions-parser.ts
5277
+ function parseAdrs(decisionsContent) {
5278
+ const headingPattern = /^## ARC-(\d{3})\b/gm;
5279
+ const positions = [];
5280
+ let match;
5281
+ while ((match = headingPattern.exec(decisionsContent)) !== null) {
5282
+ positions.push({ id: `ARC-${match[1]}`, index: match.index });
5283
+ }
5284
+ return positions.map(({ id, index }, i) => {
5285
+ const end = i + 1 < positions.length ? positions[i + 1].index : decisionsContent.length;
5286
+ const section = decisionsContent.slice(index, end);
5287
+ const dateMatch = /^\*\*Date:\*\*\s*(\d{4}-\d{2}-\d{2})/m.exec(section);
5288
+ const statusMatch = /^\*\*Status:\*\*\s*(\S+)/m.exec(section);
5289
+ return { id, date: dateMatch ? dateMatch[1] : null, status: statusMatch ? statusMatch[1] : null };
5290
+ });
5291
+ }
5292
+ function countAcceptedAdrsInWindow(adrs, fromDate, toDate) {
5293
+ return adrs.filter(
5294
+ (adr) => adr.status === "Accepted" && adr.date !== null && adr.date >= fromDate && adr.date <= toDate
5295
+ ).length;
5296
+ }
5297
+
5298
+ // src/modules/show-report/sources.ts
5299
+ async function runGitTextOrNull(cwd, args) {
5300
+ try {
5301
+ const { stdout } = await runGit(cwd, args, { commandClass: "read" });
5302
+ return stdout.trim();
5303
+ } catch {
5304
+ return null;
5305
+ }
5306
+ }
5307
+ async function isShallowRepository(cwd) {
5308
+ const out = await runGitTextOrNull(cwd, ["rev-parse", "--is-shallow-repository"]);
5309
+ return out === "true";
5310
+ }
5311
+ async function getVersionAtRef(cwd, ref) {
5312
+ const content = await runGitTextOrNull(cwd, ["show", `${ref}:package.json`]);
5313
+ if (content === null) return null;
5314
+ try {
5315
+ const parsed = JSON.parse(content);
5316
+ const version = parsed.version;
5317
+ return typeof version === "string" ? version : null;
5318
+ } catch {
5319
+ return null;
5320
+ }
5321
+ }
5322
+ async function getCloseCommit(cwd, completedDate) {
5323
+ if (!completedDate) {
5324
+ const head = await runGitTextOrNull(cwd, ["rev-parse", "HEAD"]);
5325
+ return head === null || head === "" ? null : head;
5326
+ }
5327
+ const log = await runGitTextOrNull(cwd, ["log", "--format=%H%x09%cs", "HEAD"]);
5328
+ if (log === null || log === "") return null;
5329
+ for (const line of log.split("\n")) {
5330
+ const [sha, landed] = line.split(" ");
5331
+ if (sha && landed && landed <= completedDate) return sha;
5332
+ }
5333
+ return null;
5334
+ }
5335
+ async function getCast(cwd, fromRef, toRef) {
5336
+ const ETX = "";
5337
+ const log = await runGitTextOrNull(cwd, [
5338
+ "log",
5339
+ "--no-merges",
5340
+ `--format=%B${ETX}`,
5341
+ `${fromRef}..${toRef}`
5342
+ ]);
5343
+ const cast = /* @__PURE__ */ new Map();
5344
+ if (log === null || log === "") return cast;
5345
+ for (const body of log.split(ETX)) {
5346
+ const personaMatch = /^Persona:\s*(.+)$/m.exec(body);
5347
+ const agentMatch = /^Agent:\s*(.+)$/m.exec(body);
5348
+ const name = personaMatch?.[1]?.trim() || agentMatch?.[1]?.trim();
5349
+ if (!name) continue;
5350
+ cast.set(name, (cast.get(name) ?? 0) + 1);
5351
+ }
5352
+ return cast;
5353
+ }
5354
+
5355
+ // src/modules/show-report/model.ts
5356
+ async function readOptional(path4) {
5357
+ try {
5358
+ return await readFile12(path4, "utf8");
5359
+ } catch {
5360
+ return null;
5361
+ }
5362
+ }
5363
+ function extractSha(baselineField) {
5364
+ return baselineField.trim().split(/\s+/)[0];
5365
+ }
5366
+ function epicToRow(epic) {
5367
+ const category = epic.report?.category ?? "process";
5368
+ const row = {
5369
+ id: epic.id,
5370
+ title: epic.report?.titleOverride ?? epic.title,
5371
+ description: epic.report?.description ?? null,
5372
+ descriptionState: epic.report ? "authored" : "unwritten",
5373
+ category,
5374
+ // Last-cited PR link is closest, in reading order, to the epic's actual
5375
+ // completion note (earlier links in a long Done note are often an ADR's
5376
+ // own PR or an intermediate fix, not the epic's own shipping PR).
5377
+ href: epic.prLinks.length > 0 ? epic.prLinks[epic.prLinks.length - 1] : void 0,
5378
+ refs: epic.prLinks
5379
+ };
5380
+ if (epic.report?.glyph) row.glyph = epic.report.glyph;
5381
+ return row;
5382
+ }
5383
+ function buildSections(epics) {
5384
+ const waveOrder = [];
5385
+ const byWave = /* @__PURE__ */ new Map();
5386
+ for (const epic of epics) {
5387
+ const wave = epic.wave ?? "Epics";
5388
+ if (!byWave.has(wave)) {
5389
+ byWave.set(wave, []);
5390
+ waveOrder.push(wave);
5391
+ }
5392
+ byWave.get(wave).push(epic);
5393
+ }
5394
+ return waveOrder.map((wave, i) => ({
5395
+ id: `wave-${i + 1}`,
5396
+ title: wave,
5397
+ rows: byWave.get(wave).map(epicToRow)
5398
+ }));
5399
+ }
5400
+ function buildStats(epics, versionSpan, adrsAccepted) {
5401
+ const done = epics.filter((e) => e.done).length;
5402
+ const allPrs = new Set(epics.flatMap((e) => e.prLinks));
5403
+ const stats = [
5404
+ { id: "epics", value: `${done}/${epics.length}`, label: "epics shipped", derived: true },
5405
+ { id: "prs", value: allPrs.size, label: "pull requests", derived: true }
5406
+ ];
5407
+ if (versionSpan) {
5408
+ stats.push({
5409
+ id: "version-span",
5410
+ value: `${versionSpan.from} \u2192 ${versionSpan.to}`,
5411
+ label: "version span",
5412
+ derived: true
5413
+ });
5414
+ }
5415
+ if (adrsAccepted !== null) {
5416
+ stats.push({ id: "adrs", value: adrsAccepted, label: "ADRs accepted", derived: true });
5417
+ }
5418
+ return stats;
5419
+ }
5420
+ function buildNeedsYou(queueEntries) {
5421
+ return queueEntries.filter((e) => !e.done).map((e) => {
5422
+ const entry = { id: e.id, title: e.title, reason: e.why ?? e.title };
5423
+ if (e.href) entry.href = e.href;
5424
+ return entry;
5425
+ });
5426
+ }
5427
+ function buildCorrections(ledgerRows, programName) {
5428
+ if (ledgerRows.length === 0) return void 0;
5429
+ const corrected = ledgerRows.filter((r) => r.result === "corrected");
5430
+ const unverifiable = ledgerRows.filter((r) => r.result === "unverifiable").length;
5431
+ const highlights = corrected.slice(0, 3).map((row, i) => ({
5432
+ id: `correction-${i + 1}`,
5433
+ title: row.claim.length > 80 ? `${row.claim.slice(0, 77)}...` : row.claim,
5434
+ description: row.correction,
5435
+ descriptionState: row.correction ? "authored" : "unwritten",
5436
+ category: "fix",
5437
+ refs: []
5438
+ }));
5439
+ return {
5440
+ checked: ledgerRows.length,
5441
+ corrected: corrected.length,
5442
+ unverifiable,
5443
+ scopeNote: `${programName} corrections logged in docs/verification-ledger.md`,
5444
+ highlights
5445
+ };
5446
+ }
5447
+ async function buildShowReportModel(options) {
5448
+ const planAbsPath = join14(options.rootDir, options.planRelPath);
5449
+ const queueAbsPath = join14(options.rootDir, options.queueRelPath);
5450
+ const ledgerRelPath = "docs/verification-ledger.md";
5451
+ const ledgerAbsPath = join14(options.rootDir, ledgerRelPath);
5452
+ const decisionsAbsPath = join14(options.rootDir, "DECISIONS.md");
5453
+ const [planContent, queueContent, ledgerContent, decisionsContent] = await Promise.all([
5454
+ readFile12(planAbsPath, "utf8"),
5455
+ readOptional(queueAbsPath),
5456
+ readOptional(ledgerAbsPath),
5457
+ readOptional(decisionsAbsPath)
5458
+ ]);
5459
+ const frontmatter = parseFrontmatter(planContent);
5460
+ const epics = parseEpics(planContent);
5461
+ const parked = parseParkedSection(planContent);
5462
+ const outcome = computeCoverageOutcome(planContent);
5463
+ const queueEntries = queueContent ? parseOperatorQueue(queueContent) : [];
5464
+ const ledgerRows = ledgerContent ? parseVerificationLedgerSection(ledgerContent, options.ledgerProgramName) : [];
5465
+ const adrsAccepted = decisionsContent && frontmatter.created ? countAcceptedAdrsInWindow(
5466
+ parseAdrs(decisionsContent),
5467
+ frontmatter.created,
5468
+ frontmatter.completed ?? options.compiledAt.slice(0, 10)
5469
+ ) : null;
5470
+ let versionSpan;
5471
+ const cast = /* @__PURE__ */ new Map();
5472
+ if (frontmatter.baseline) {
5473
+ const baselineSha = extractSha(frontmatter.baseline);
5474
+ const closeSha = await getCloseCommit(options.rootDir, frontmatter.completed);
5475
+ if (closeSha) {
5476
+ const [from, to, castMap] = await Promise.all([
5477
+ getVersionAtRef(options.rootDir, baselineSha),
5478
+ getVersionAtRef(options.rootDir, closeSha),
5479
+ getCast(options.rootDir, baselineSha, closeSha)
5480
+ ]);
5481
+ if (from && to) versionSpan = { from, to };
5482
+ for (const [name, count] of castMap) cast.set(name, count);
5483
+ }
5484
+ }
5485
+ const done = epics.filter((e) => e.done).length;
5486
+ const title = frontmatter.title ?? options.slug;
5487
+ const corrections = buildCorrections(ledgerRows, options.ledgerProgramName);
5488
+ return {
5489
+ schemaVersion: 1,
5490
+ program: {
5491
+ id: options.slug,
5492
+ slug: options.slug,
5493
+ title,
5494
+ status: frontmatter.status ?? "unknown",
5495
+ baseline: frontmatter.baseline ?? "unknown",
5496
+ started: frontmatter.created ?? "unknown",
5497
+ ...frontmatter.completed ? { completed: frontmatter.completed } : {},
5498
+ ...versionSpan ? { versionSpan } : {}
5499
+ },
5500
+ masthead: {
5501
+ eyebrow: "Show Report",
5502
+ title,
5503
+ dek: `${frontmatter.status ?? "unknown"} \u2014 ${done} of ${epics.length} epics shipped`
5504
+ },
5505
+ stats: buildStats(epics, versionSpan, adrsAccepted),
5506
+ ...outcome ? { outcome } : {},
5507
+ needsYou: buildNeedsYou(queueEntries),
5508
+ sections: buildSections(epics),
5509
+ ...corrections ? { corrections } : {},
5510
+ parked,
5511
+ cast: [...cast.entries()].map(([name, commits]) => ({ name, commits, source: "commit-trailer" })),
5512
+ colophon: {
5513
+ sources: [options.planRelPath, options.queueRelPath, ledgerRelPath],
5514
+ compiledAt: options.compiledAt,
5515
+ templateVersion: options.templateVersion
5516
+ }
5517
+ };
5518
+ }
5519
+
5520
+ // src/modules/show-report/render.ts
5521
+ import Mustache from "mustache";
5522
+ var CATEGORY_LABELS = {
5523
+ spell: "New Spell",
5524
+ feature: "New Feature",
5525
+ governance: "Governance",
5526
+ decision: "Decision (ADR)",
5527
+ fix: "Bug Fix",
5528
+ process: "Process",
5529
+ docs: "Docs",
5530
+ platform: "Platform"
5531
+ };
5532
+ var CATEGORY_ORDER = [
5533
+ "spell",
5534
+ "feature",
5535
+ "governance",
5536
+ "decision",
5537
+ "fix",
5538
+ "process",
5539
+ "docs",
5540
+ "platform"
5541
+ ];
5542
+ function escapeHtml(text) {
5543
+ return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
5544
+ }
5545
+ function inlineMarkupToHtml(text) {
5546
+ const escaped = escapeHtml(text);
5547
+ const withLinks = escaped.replace(
5548
+ /\[([^\]]+)\]\((https?:\/\/[^)\s]+)\)/g,
5549
+ '<a href="$2">$1</a>'
5550
+ );
5551
+ return withLinks.replace(/`([^`]+)`/g, "<code>$1</code>");
5552
+ }
5553
+ function toRowView(row) {
5554
+ return {
5555
+ ...row,
5556
+ categoryLabel: CATEGORY_LABELS[row.category] ?? row.category,
5557
+ descriptionHtml: row.description === null ? null : inlineMarkupToHtml(row.description),
5558
+ isUnwritten: row.descriptionState === "unwritten",
5559
+ hasHref: typeof row.href === "string" && row.href.length > 0,
5560
+ hasGlyph: typeof row.glyph === "string" && row.glyph.length > 0
5561
+ };
5562
+ }
5563
+ function buildShowReportView(model) {
5564
+ const corrections = model.corrections ? { ...model.corrections, highlights: model.corrections.highlights.map(toRowView) } : void 0;
5565
+ return {
5566
+ ...model,
5567
+ sections: model.sections.map((section) => ({
5568
+ ...section,
5569
+ hasNote: typeof section.note === "string" && section.note.length > 0,
5570
+ rows: section.rows.map(toRowView)
5571
+ })),
5572
+ corrections,
5573
+ hasCorrections: corrections !== void 0,
5574
+ hasParked: model.parked.length > 0,
5575
+ hasCast: model.cast.length > 0,
5576
+ hasOutcome: typeof model.outcome === "string" && model.outcome.length > 0,
5577
+ hasVersionSpan: model.program.versionSpan !== void 0,
5578
+ needsYouCount: model.needsYou.length,
5579
+ compiledAtDate: model.colophon.compiledAt.slice(0, 10),
5580
+ legend: CATEGORY_ORDER.map((key) => ({ key, label: CATEGORY_LABELS[key] })),
5581
+ sourcesJoined: model.colophon.sources.join(", ")
5582
+ };
5583
+ }
5584
+ function renderShowReport(model, templateHtml) {
5585
+ return Mustache.render(templateHtml, buildShowReportView(model), void 0, {
5586
+ escape: (value) => escapeHtml(String(value))
5587
+ });
5588
+ }
5589
+
5590
+ // src/modules/show-report/generate.ts
5591
+ var TEMPLATE_VERSION = "v0-interim";
5592
+ var NO_EPICS_REASON = "no parseable `- [x] **ID \u2014 Title.**` epic bullets";
5593
+ function programNameFromTitle(title) {
5594
+ return title.split(" \u2014 ")[0].trim();
5595
+ }
5596
+ function deterministicCompiledAt(frontmatter) {
5597
+ return frontmatter.completed ?? frontmatter.activated ?? frontmatter.created ?? "unknown";
5598
+ }
5599
+ function toPosix(path4) {
5600
+ return path4.replace(/\\/g, "/");
5601
+ }
5602
+ async function programFromPlan(rootDir, slug, planRelPath) {
5603
+ let planContent;
5604
+ try {
5605
+ planContent = await readFile13(join15(rootDir, planRelPath), "utf8");
5606
+ } catch {
5607
+ return { skipped: { slug, reason: `no PLAN.md at ${planRelPath}` } };
5608
+ }
5609
+ if (parseEpics(planContent).length === 0) {
5610
+ return { skipped: { slug, reason: NO_EPICS_REASON } };
5611
+ }
5612
+ const frontmatter = parseFrontmatter(planContent);
5613
+ return {
5614
+ program: {
5615
+ slug,
5616
+ planRelPath,
5617
+ queueRelPath: toPosix(join15(dirname4(planRelPath), "OPERATOR-QUEUE.md")),
5618
+ ledgerProgramName: programNameFromTitle(frontmatter.title ?? slug)
5619
+ }
5620
+ };
5621
+ }
5622
+ async function discoverPrograms(rootDir) {
5623
+ const plansDir = join15(rootDir, "docs", "plans");
5624
+ let entries;
5625
+ try {
5626
+ entries = (await readdir7(plansDir, { withFileTypes: true })).filter((e) => e.isDirectory());
5627
+ } catch (error) {
5628
+ if (error.code === "ENOENT") return { programs: [], skipped: [] };
5629
+ throw error;
5630
+ }
5631
+ const programs = [];
5632
+ const skipped = [];
5633
+ for (const entry of entries.sort((a, b) => a.name.localeCompare(b.name))) {
5634
+ const result = await programFromPlan(rootDir, entry.name, `docs/plans/${entry.name}/PLAN.md`);
5635
+ if (result.program) programs.push(result.program);
5636
+ if (result.skipped) skipped.push(result.skipped);
5637
+ }
5638
+ return { programs, skipped };
5639
+ }
5640
+ async function programFromPlanPath(rootDir, planPath) {
5641
+ const planAbs = isAbsolute(planPath) ? planPath : resolve3(rootDir, planPath);
5642
+ const planRelPath = toPosix(relative3(rootDir, planAbs));
5643
+ return programFromPlan(rootDir, basename3(dirname4(planAbs)), planRelPath);
5644
+ }
5645
+ function normalizeLineEndings(content) {
5646
+ return content.replace(/\r\n?/g, "\n");
5647
+ }
5648
+ async function readOptionalFile(path4) {
5649
+ try {
5650
+ return await readFile13(path4, "utf8");
5651
+ } catch (error) {
5652
+ if (error.code === "ENOENT") return null;
5653
+ throw error;
5654
+ }
5655
+ }
5656
+ async function generateReport(rootDir, program2, templateHtml) {
5657
+ const planContent = await readFile13(join15(rootDir, program2.planRelPath), "utf8");
5658
+ const frontmatter = parseFrontmatter(planContent);
5659
+ const model = await buildShowReportModel({
5660
+ rootDir,
5661
+ slug: program2.slug,
5662
+ planRelPath: program2.planRelPath,
5663
+ queueRelPath: program2.queueRelPath,
5664
+ ledgerProgramName: program2.ledgerProgramName,
5665
+ compiledAt: deterministicCompiledAt(frontmatter),
5666
+ templateVersion: TEMPLATE_VERSION
5667
+ });
5668
+ return {
5669
+ json: `${JSON.stringify(model, null, 2)}
5670
+ `,
5671
+ html: renderShowReport(model, templateHtml)
5672
+ };
5673
+ }
5674
+ async function runReportGeneration(options) {
5675
+ const templateHtml = await readFile13(options.templatePath, "utf8");
5676
+ const drifted = [];
5677
+ const repaired = [];
5678
+ const unwritten = [];
5679
+ for (const program2 of options.programs) {
5680
+ const generated = await generateReport(options.rootDir, program2, templateHtml);
5681
+ const missing = JSON.parse(generated.json).sections.flatMap((section) => section.rows).filter((row) => row.descriptionState === "unwritten").map((row) => row.id);
5682
+ if (missing.length > 0) unwritten.push({ slug: program2.slug, ids: missing });
5683
+ const outAbs = options.outDir ? isAbsolute(options.outDir) ? options.outDir : resolve3(options.rootDir, options.outDir) : resolve3(options.rootDir, dirname4(program2.planRelPath));
5684
+ const outRel = toPosix(relative3(options.rootDir, outAbs));
5685
+ const outputs = [
5686
+ [`${outRel}/show-report.json`, generated.json],
5687
+ [`${outRel}/show-report.html`, generated.html]
5688
+ ];
5689
+ for (const [relPath, expected] of outputs) {
5690
+ const actual = await readOptionalFile(join15(options.rootDir, relPath));
5691
+ if (actual === null || normalizeLineEndings(actual) !== normalizeLineEndings(expected)) {
5692
+ drifted.push(`${relPath} does not match a regeneration from its sources.`);
5693
+ if (options.mode === "write") {
5694
+ await mkdir7(outAbs, { recursive: true });
5695
+ await writeFile10(join15(options.rootDir, relPath), expected, "utf8");
5696
+ repaired.push(relPath);
5697
+ }
5698
+ }
5699
+ }
5700
+ }
5701
+ return { drifted, repaired, unwritten };
5702
+ }
5703
+
5704
+ // src/commands/report.ts
5705
+ async function runReport(targetDir, options = {}, assetsDir) {
5706
+ const templatePath = join16(assetsDir, "report", "show-report.template.html");
5707
+ try {
5708
+ await access5(templatePath);
5709
+ } catch {
5710
+ console.error(
5711
+ `\u2716 arcane: report template not found at ${templatePath} -- this arcane-cli build is missing dist/assets/report/. Reinstall the package (or run \`npm run build\` in a source checkout).`
5712
+ );
5713
+ process.exitCode = 1;
5714
+ return;
5715
+ }
5716
+ if (options.refresh) {
5717
+ console.log(
5718
+ "--refresh: external-data snapshotting (gh PR titles/merge dates) is not implemented yet; regenerating from local sources."
5719
+ );
5720
+ }
5721
+ if (await isShallowRepository(targetDir)) {
5722
+ console.warn(
5723
+ "\u26A0 arcane: shallow clone -- the report's version span and cast are derived from git history and will be omitted. Run `git fetch --unshallow` for a complete report."
5724
+ );
5725
+ }
5726
+ let programs;
5727
+ let skipped;
5728
+ if (options.plan) {
5729
+ const result2 = await programFromPlanPath(targetDir, options.plan);
5730
+ programs = result2.program ? [result2.program] : [];
5731
+ skipped = result2.skipped ? [result2.skipped] : [];
5732
+ } else {
5733
+ const discovery = await discoverPrograms(targetDir);
5734
+ programs = discovery.programs;
5735
+ skipped = discovery.skipped;
5736
+ }
5737
+ for (const s of skipped) console.log(`Skipped ${s.slug} -- ${s.reason}.`);
5738
+ if (programs.length === 0) {
5739
+ console.error(
5740
+ "\u2716 arcane: no program to report on -- expected docs/plans/<slug>/PLAN.md with `- [x] **ID \u2014 Title.**` epic bullets, or pass --plan <path>."
5741
+ );
5742
+ process.exitCode = 1;
5743
+ return;
5744
+ }
5745
+ const result = await runReportGeneration({
5746
+ rootDir: targetDir,
5747
+ templatePath,
5748
+ mode: "write",
5749
+ programs,
5750
+ outDir: options.out
5751
+ });
5752
+ for (const u of result.unwritten) {
5753
+ console.warn(
5754
+ `\u26A0 arcane: ${u.slug} has ${u.ids.length} epic(s) with no \`**Report:**\` line in PLAN.md -- they render as "unwritten": ${u.ids.join(", ")}.`
5755
+ );
5756
+ }
5757
+ if (result.repaired.length === 0) {
5758
+ console.log(`Show report: ${programs.length} program(s) already up to date.`);
5759
+ return;
5760
+ }
5761
+ console.log(`Show report: wrote ${result.repaired.length} file(s).`);
5762
+ for (const path4 of result.repaired) console.log(` ${path4}`);
5763
+ }
5764
+
5100
5765
  // src/index.ts
5101
5766
  import { realpathSync } from "fs";
5102
5767
  var require3 = createRequire2(import.meta.url);
5103
5768
  var pkg = require3("../package.json");
5104
- var ASSETS_DIR = process.env["ARCANE_ASSETS_DIR"] ?? join14(dirname4(fileURLToPath(import.meta.url)), "assets");
5769
+ var ASSETS_DIR = process.env["ARCANE_ASSETS_DIR"] ?? join17(dirname5(fileURLToPath(import.meta.url)), "assets");
5105
5770
  var program = new Command();
5106
5771
  program.name("spell").description("Arcane framework CLI \u2014 scaffold and manage governance files").version(pkg.version).action(async () => {
5107
5772
  if (program.args.length > 0) {
@@ -5179,6 +5844,14 @@ program.command("ward").description("Scan the repo for leaked identifiers -- thi
5179
5844
  ).option("--gate", "Exit non-zero if any leak is found (for CI)").action(async (opts) => {
5180
5845
  await runWardCli(process.cwd(), opts);
5181
5846
  });
5847
+ program.command("report").description(
5848
+ "Generate a program's Show Report (show-report.json + .html) from docs/plans/<slug>/PLAN.md, OPERATOR-QUEUE.md and the verification ledger -- offline, no network (ARC-042)"
5849
+ ).option("--plan <path>", "Report on one PLAN.md instead of auto-discovering docs/plans/*/PLAN.md").option("--out <dir>", "Write show-report.{json,html} into this directory instead of next to the plan").option(
5850
+ "--refresh",
5851
+ "Accepted for forward compatibility; external-data snapshotting is not implemented yet, so this regenerates from local sources"
5852
+ ).action(async (opts) => {
5853
+ await runReport(process.cwd(), opts, ASSETS_DIR);
5854
+ });
5182
5855
  var agentsCommand = program.command("agents").description("Manage the portable agent system (init, sync, list)");
5183
5856
  agentsCommand.command("init").description(
5184
5857
  "Interactive agent roster setup \u2014 choose profile, naming strategy, and install definitions"