executable-stories-formatters 0.11.0 → 0.11.2

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/cli.js CHANGED
@@ -2,8 +2,8 @@
2
2
 
3
3
  // src/cli.ts
4
4
  import { parseArgs } from "util";
5
- import * as fs13 from "fs";
6
- import * as path14 from "path";
5
+ import * as fs14 from "fs";
6
+ import * as path15 from "path";
7
7
 
8
8
  // src/validation/schema-validator.ts
9
9
  import Ajv from "ajv/dist/2020.js";
@@ -558,17 +558,17 @@ function validateRawRun(data) {
558
558
  return { valid: true, errors: [] };
559
559
  }
560
560
  const errors = (validate.errors ?? []).map((err) => {
561
- const path15 = err.instancePath || "/";
561
+ const path16 = err.instancePath || "/";
562
562
  const message = err.message ?? "unknown error";
563
563
  if (err.keyword === "additionalProperties") {
564
564
  const extra = err.params.additionalProperty;
565
- return `${path15}: ${message} \u2014 '${extra}'`;
565
+ return `${path16}: ${message} \u2014 '${extra}'`;
566
566
  }
567
567
  if (err.keyword === "enum") {
568
568
  const allowed = err.params.allowedValues;
569
- return `${path15}: ${message} \u2014 allowed: ${JSON.stringify(allowed)}`;
569
+ return `${path16}: ${message} \u2014 allowed: ${JSON.stringify(allowed)}`;
570
570
  }
571
- return `${path15}: ${message}`;
571
+ return `${path16}: ${message}`;
572
572
  });
573
573
  return { valid: false, errors };
574
574
  }
@@ -1043,7 +1043,7 @@ ${result.errors.join("\n")}`);
1043
1043
 
1044
1044
  // src/index.ts
1045
1045
  import "fs";
1046
- import * as path8 from "path";
1046
+ import * as path9 from "path";
1047
1047
  import * as fsPromises from "fs/promises";
1048
1048
 
1049
1049
  // src/converters/acl/lines.ts
@@ -1631,23 +1631,23 @@ function buildFeature(relSourceFile, group) {
1631
1631
  function ensureUniqueFeatureIds(features) {
1632
1632
  const seen = /* @__PURE__ */ new Map();
1633
1633
  for (const f of features) {
1634
- const count = seen.get(f.id) ?? 0;
1635
- if (count > 0) f.id = `${f.id}-${count + 1}`;
1636
- seen.set(f.id, count + 1);
1634
+ const count2 = seen.get(f.id) ?? 0;
1635
+ if (count2 > 0) f.id = `${f.id}-${count2 + 1}`;
1636
+ seen.set(f.id, count2 + 1);
1637
1637
  }
1638
1638
  }
1639
1639
  function ensureUniqueScenarioIds(feature) {
1640
1640
  const seen = /* @__PURE__ */ new Map();
1641
1641
  for (const s of feature.scenarios) {
1642
- const count = seen.get(s.id) ?? 0;
1643
- if (count > 0) {
1644
- const newId = `${s.id}-${count + 1}`;
1642
+ const count2 = seen.get(s.id) ?? 0;
1643
+ if (count2 > 0) {
1644
+ const newId = `${s.id}-${count2 + 1}`;
1645
1645
  for (const step of s.steps) {
1646
1646
  step.id = step.id.replace(s.id, newId);
1647
1647
  }
1648
1648
  s.id = newId;
1649
1649
  }
1650
- seen.set(s.id, count + 1);
1650
+ seen.set(s.id, count2 + 1);
1651
1651
  }
1652
1652
  }
1653
1653
  function toStoryReport(run) {
@@ -16145,6 +16145,88 @@ function groupBy6(items, keyFn) {
16145
16145
  return map;
16146
16146
  }
16147
16147
 
16148
+ // src/formatters/release-manifest.ts
16149
+ import { createHash as createHash2 } from "crypto";
16150
+ var ReleaseManifestFormatter = class {
16151
+ format(run) {
16152
+ const manifest = toReleaseManifest(run);
16153
+ const lines = [];
16154
+ lines.push("# Release Manifest");
16155
+ lines.push("");
16156
+ lines.push(`Generated: ${manifest.generatedAt}`);
16157
+ lines.push(`Run: ${manifest.run.startedAt} to ${manifest.run.finishedAt}`);
16158
+ if (manifest.run.branch) lines.push(`Branch: ${manifest.run.branch}`);
16159
+ if (manifest.run.gitSha) lines.push(`Commit: ${manifest.run.gitSha}`);
16160
+ lines.push(`Tested-together hash: \`${manifest.testedTogetherHash}\``);
16161
+ lines.push("");
16162
+ lines.push("| Scenarios | Passed | Failed | Skipped | Pending |");
16163
+ lines.push("| ---: | ---: | ---: | ---: | ---: |");
16164
+ lines.push(`| ${manifest.run.total} | ${manifest.run.passed} | ${manifest.run.failed} | ${manifest.run.skipped} | ${manifest.run.pending} |`);
16165
+ lines.push("");
16166
+ lines.push("## Scenarios");
16167
+ lines.push("");
16168
+ lines.push("| Status | Scenario | Source | Tags |");
16169
+ lines.push("| --- | --- | --- | --- |");
16170
+ for (const scenario of manifest.scenarios) {
16171
+ const source = `${scenario.sourceFile}:${scenario.sourceLine}`;
16172
+ const tags = scenario.tags.length > 0 ? scenario.tags.map((tag) => `\`${tag}\``).join(", ") : "";
16173
+ lines.push(`| ${renderStatus(scenario.status)} | ${escapePipe(scenario.title)} | \`${source}\` | ${tags} |`);
16174
+ }
16175
+ return lines.join("\n");
16176
+ }
16177
+ };
16178
+ function toReleaseManifest(run) {
16179
+ const scenarios = [...run.testCases].sort((a, b) => a.id.localeCompare(b.id)).map((tc) => ({
16180
+ id: tc.id,
16181
+ title: tc.story.scenario,
16182
+ status: tc.status,
16183
+ sourceFile: tc.sourceFile,
16184
+ sourceLine: tc.sourceLine,
16185
+ tags: tc.tags
16186
+ }));
16187
+ const fingerprint = scenarios.map((scenario) => `${scenario.id}:${scenario.status}`).join("\n");
16188
+ return {
16189
+ schemaVersion: "1.0",
16190
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
16191
+ run: {
16192
+ startedAt: new Date(run.startedAtMs).toISOString(),
16193
+ finishedAt: new Date(run.finishedAtMs).toISOString(),
16194
+ gitSha: run.gitSha,
16195
+ branch: getBranch(run),
16196
+ total: run.testCases.length,
16197
+ passed: count(run.testCases, "passed"),
16198
+ failed: count(run.testCases, "failed"),
16199
+ skipped: count(run.testCases, "skipped"),
16200
+ pending: count(run.testCases, "pending")
16201
+ },
16202
+ testedTogetherHash: createHash2("sha256").update(fingerprint).digest("hex"),
16203
+ scenarios
16204
+ };
16205
+ }
16206
+ function getBranch(run) {
16207
+ return run.ci?.branch;
16208
+ }
16209
+ function count(testCases, status) {
16210
+ return testCases.filter((tc) => tc.status === status).length;
16211
+ }
16212
+ function renderStatus(status) {
16213
+ switch (status) {
16214
+ case "passed":
16215
+ return "passed";
16216
+ case "failed":
16217
+ return "failed";
16218
+ case "skipped":
16219
+ return "skipped";
16220
+ case "pending":
16221
+ return "pending";
16222
+ default:
16223
+ return status;
16224
+ }
16225
+ }
16226
+ function escapePipe(value) {
16227
+ return value.replace(/\|/g, "\\|");
16228
+ }
16229
+
16148
16230
  // src/formatters/cucumber-messages/synthesize-feature.ts
16149
16231
  function extractFeatureName(testCases, uri) {
16150
16232
  for (const tc of testCases) {
@@ -16211,7 +16293,7 @@ function synthesizeFeature(uri, testCases) {
16211
16293
  }
16212
16294
 
16213
16295
  // src/utils/cucumber-messages.ts
16214
- import { createHash as createHash2 } from "crypto";
16296
+ import { createHash as createHash3 } from "crypto";
16215
16297
  function msToTimestamp(ms) {
16216
16298
  const seconds = Math.floor(ms / 1e3);
16217
16299
  const nanos = Math.round(ms % 1e3 * 1e6);
@@ -16277,7 +16359,7 @@ function statusToCucumberStatus(status) {
16277
16359
  }
16278
16360
  function deterministicId(kind, salt, ...parts) {
16279
16361
  const input = [salt, kind, ...parts].join("::");
16280
- return createHash2("sha1").update(input).digest("hex").slice(0, 36);
16362
+ return createHash3("sha1").update(input).digest("hex").slice(0, 36);
16281
16363
  }
16282
16364
 
16283
16365
  // src/formatters/cucumber-messages/build-gherkin-document.ts
@@ -16765,8 +16847,8 @@ function extractDocAttachments(step) {
16765
16847
  }
16766
16848
  return attachments;
16767
16849
  }
16768
- function guessMediaType(path15) {
16769
- const lower = path15.toLowerCase();
16850
+ function guessMediaType(path16) {
16851
+ const lower = path16.toLowerCase();
16770
16852
  if (lower.endsWith(".png")) return "image/png";
16771
16853
  if (lower.endsWith(".jpg") || lower.endsWith(".jpeg")) return "image/jpeg";
16772
16854
  if (lower.endsWith(".gif")) return "image/gif";
@@ -16907,11 +16989,11 @@ var CucumberHtmlFormatter = class {
16907
16989
  for (const envelope of envelopes) {
16908
16990
  const accepted = htmlStream.write(envelope);
16909
16991
  if (!accepted) {
16910
- await new Promise((resolve11) => htmlStream.once("drain", resolve11));
16992
+ await new Promise((resolve12) => htmlStream.once("drain", resolve12));
16911
16993
  }
16912
16994
  }
16913
- await new Promise((resolve11, reject) => {
16914
- collector.on("finish", resolve11);
16995
+ await new Promise((resolve12, reject) => {
16996
+ collector.on("finish", resolve12);
16915
16997
  collector.on("error", reject);
16916
16998
  htmlStream.end();
16917
16999
  });
@@ -17005,15 +17087,15 @@ function createPrCommentSummary(diff, maxScenarios = 10) {
17005
17087
  function getCommit(run) {
17006
17088
  return run.ci?.commitSha ?? run.gitSha;
17007
17089
  }
17008
- function getBranch(run) {
17090
+ function getBranch2(run) {
17009
17091
  return run.ci?.branch;
17010
17092
  }
17011
17093
  function pickAutoBaseline(currentRun, candidates) {
17012
- const currentBranch = getBranch(currentRun);
17094
+ const currentBranch = getBranch2(currentRun);
17013
17095
  const currentCommit = getCommit(currentRun);
17014
17096
  return [...candidates].sort((a, b) => {
17015
- const aSameBranch = Boolean(currentBranch && getBranch(a.run) && currentBranch === getBranch(a.run));
17016
- const bSameBranch = Boolean(currentBranch && getBranch(b.run) && currentBranch === getBranch(b.run));
17097
+ const aSameBranch = Boolean(currentBranch && getBranch2(a.run) && currentBranch === getBranch2(a.run));
17098
+ const bSameBranch = Boolean(currentBranch && getBranch2(b.run) && currentBranch === getBranch2(b.run));
17017
17099
  if (aSameBranch !== bSameBranch) {
17018
17100
  return Number(bSameBranch) - Number(aSameBranch);
17019
17101
  }
@@ -18005,8 +18087,8 @@ ${body}`;
18005
18087
  }
18006
18088
  buildFrontmatter(run) {
18007
18089
  const badge = _AstroFormatter.computeBadge(run.testCases);
18008
- const count = run.testCases.length;
18009
- const description = `${count} scenario${count !== 1 ? "s" : ""} \u2014 ${badge.text.toLowerCase()}`;
18090
+ const count2 = run.testCases.length;
18091
+ const description = `${count2} scenario${count2 !== 1 ? "s" : ""} \u2014 ${badge.text.toLowerCase()}`;
18010
18092
  const lines = [
18011
18093
  "---",
18012
18094
  `title: ${yamlScalar(this.deriveTitle(run))}`,
@@ -19988,18 +20070,18 @@ function deriveChangeType(tags) {
19988
20070
  }
19989
20071
  return "unknown";
19990
20072
  }
19991
- function extensionOf(path15) {
19992
- const base = path15.split("/").pop() ?? path15;
20073
+ function extensionOf(path16) {
20074
+ const base = path16.split("/").pop() ?? path16;
19993
20075
  const dot = base.lastIndexOf(".");
19994
20076
  return dot === -1 ? "" : base.slice(dot + 1).toLowerCase();
19995
20077
  }
19996
- function isTestFile(path15) {
19997
- return TEST_INFIX.test(path15);
20078
+ function isTestFile(path16) {
20079
+ return TEST_INFIX.test(path16);
19998
20080
  }
19999
- function isReviewableSource(path15) {
20000
- if (isTestFile(path15)) return false;
20001
- if (path15.endsWith(".d.ts")) return false;
20002
- return CODE_EXTENSIONS.has(extensionOf(path15));
20081
+ function isReviewableSource(path16) {
20082
+ if (isTestFile(path16)) return false;
20083
+ if (path16.endsWith(".d.ts")) return false;
20084
+ return CODE_EXTENSIONS.has(extensionOf(path16));
20003
20085
  }
20004
20086
  function testBaseKey(testFile) {
20005
20087
  return testFile.replace(TEST_INFIX, "");
@@ -20103,7 +20185,7 @@ function toClaim(testCase, changedSourcePaths) {
20103
20185
  const { strength, reasons } = gradeEvidence(testCase, audience);
20104
20186
  const key = testBaseKey(testCase.sourceFile);
20105
20187
  const coversFiles = changedSourcePaths.filter(
20106
- (path15) => sourceBaseKey(path15) === key
20188
+ (path16) => sourceBaseKey(path16) === key
20107
20189
  );
20108
20190
  return {
20109
20191
  id: testCase.id,
@@ -20633,11 +20715,116 @@ applyTheme(getEffectiveTheme());` : "";
20633
20715
  }
20634
20716
  };
20635
20717
 
20718
+ // src/deploy/ledger.ts
20719
+ import * as fs7 from "fs";
20720
+ import * as path8 from "path";
20721
+ function createEmptyLedger() {
20722
+ return {
20723
+ deployments: [],
20724
+ schemaVersion: 1
20725
+ };
20726
+ }
20727
+ function loadLedger(ledgerPath) {
20728
+ const resolved = path8.resolve(ledgerPath);
20729
+ if (!fs7.existsSync(resolved)) {
20730
+ return createEmptyLedger();
20731
+ }
20732
+ try {
20733
+ const raw = JSON.parse(fs7.readFileSync(resolved, "utf8"));
20734
+ if (raw.schemaVersion !== 1) {
20735
+ throw new Error(`Unsupported ledger schemaVersion: ${raw.schemaVersion}`);
20736
+ }
20737
+ return raw;
20738
+ } catch (err) {
20739
+ const msg = err instanceof Error ? err.message : String(err);
20740
+ throw new Error(`Failed to load deployment ledger at ${resolved}: ${msg}`);
20741
+ }
20742
+ }
20743
+ function saveLedger(ledger, ledgerPath) {
20744
+ const resolved = path8.resolve(ledgerPath);
20745
+ const dir = path8.dirname(resolved);
20746
+ fs7.mkdirSync(dir, { recursive: true });
20747
+ fs7.writeFileSync(resolved, JSON.stringify(ledger, null, 2), "utf8");
20748
+ }
20749
+ function getLatestDeployment(ledger, environment) {
20750
+ return [...ledger.deployments].reverse().find((d) => d.environment === environment);
20751
+ }
20752
+
20753
+ // src/deploy/index.ts
20754
+ function recordDeployment(args) {
20755
+ const ledger = loadLedger(args.ledgerPath);
20756
+ const previous = getLatestDeployment(ledger, args.environment);
20757
+ const entry = {
20758
+ environment: args.environment,
20759
+ tag: args.tag,
20760
+ sha: args.run.gitSha,
20761
+ runFile: args.runFilePath,
20762
+ scenarioIds: args.run.testCases.map((tc) => tc.id),
20763
+ scenarioStatuses: Object.fromEntries(args.run.testCases.map((tc) => [tc.id, tc.status])),
20764
+ timestamp: new Date(args.run.finishedAtMs).toISOString(),
20765
+ summary: countStatuses(args.run)
20766
+ };
20767
+ ledger.deployments.push(entry);
20768
+ if (previous) {
20769
+ const previousIds = new Set(previous.scenarioIds);
20770
+ const added = entry.scenarioIds.filter((id) => !previousIds.has(id)).length;
20771
+ const removed = previous.scenarioIds.filter((id) => !entry.scenarioIds.includes(id)).length;
20772
+ if (added > 0 || removed > 0) {
20773
+ }
20774
+ }
20775
+ saveLedger(ledger, args.ledgerPath);
20776
+ return { entry, ledgerPath: args.ledgerPath };
20777
+ }
20778
+ function getDeploymentStatus(ledgerPath) {
20779
+ const ledger = loadLedger(ledgerPath);
20780
+ const environments = {};
20781
+ for (const entry of ledger.deployments) {
20782
+ environments[entry.environment] = {
20783
+ latest: entry,
20784
+ previousDeployment: environments[entry.environment]?.latest
20785
+ };
20786
+ }
20787
+ return { environments, ledgerPath };
20788
+ }
20789
+ function getEnvironmentDrift(ledgerPath, envA, envB) {
20790
+ const ledger = loadLedger(ledgerPath);
20791
+ const aEntry = getLatestDeployment(ledger, envA);
20792
+ const bEntry = getLatestDeployment(ledger, envB);
20793
+ if (!aEntry) {
20794
+ throw new Error(`No deployment found for environment "${envA}"`);
20795
+ }
20796
+ if (!bEntry) {
20797
+ throw new Error(`No deployment found for environment "${envB}"`);
20798
+ }
20799
+ const aIds = new Set(aEntry.scenarioIds);
20800
+ const bIds = new Set(bEntry.scenarioIds);
20801
+ const onlyInA = aEntry.scenarioIds.filter((id) => !bIds.has(id));
20802
+ const onlyInB = bEntry.scenarioIds.filter((id) => !aIds.has(id));
20803
+ const inBoth = aEntry.scenarioIds.filter((id) => bIds.has(id));
20804
+ const statusChanged = inBoth.map((id) => ({
20805
+ id,
20806
+ statusA: aEntry.scenarioStatuses?.[id] ?? "unknown",
20807
+ statusB: bEntry.scenarioStatuses?.[id] ?? "unknown"
20808
+ })).filter((item) => item.statusA !== item.statusB);
20809
+ return { environmentA: envA, environmentB: envB, onlyInA, onlyInB, inBoth, statusChanged, aEntry, bEntry };
20810
+ }
20811
+ function countStatuses(run) {
20812
+ const summary = { total: 0, passed: 0, failed: 0, skipped: 0, pending: 0 };
20813
+ for (const tc of run.testCases) {
20814
+ summary.total++;
20815
+ if (tc.status in summary) {
20816
+ summary[tc.status]++;
20817
+ }
20818
+ }
20819
+ return summary;
20820
+ }
20821
+
20636
20822
  // src/index.ts
20637
20823
  var FORMAT_EXTENSIONS = {
20638
20824
  astro: ".md",
20639
20825
  "behavior-manifest-json": ".behavior-manifest.json",
20640
20826
  markdown: ".md",
20827
+ "release-manifest": ".release-manifest.md",
20641
20828
  html: ".html",
20642
20829
  "cucumber-html": ".cucumber.html",
20643
20830
  junit: ".junit.xml",
@@ -20676,11 +20863,11 @@ function computeOutputPath(sourceFile, format, mode, colocatedStyle, baseOutputD
20676
20863
  const ext = FORMAT_EXTENSIONS[format];
20677
20864
  const effectiveName = outputName + (outputNameSuffix ?? "");
20678
20865
  if (mode === "aggregated") {
20679
- return toPosix(path8.join(baseOutputDir, joinNameAndExt(effectiveName, ext)));
20866
+ return toPosix(path9.join(baseOutputDir, joinNameAndExt(effectiveName, ext)));
20680
20867
  }
20681
20868
  const normalizedSource = toPosix(sourceFile);
20682
- const dirOfSource = path8.posix.dirname(normalizedSource);
20683
- let baseName = path8.posix.basename(normalizedSource);
20869
+ const dirOfSource = path9.posix.dirname(normalizedSource);
20870
+ let baseName = path9.posix.basename(normalizedSource);
20684
20871
  for (const testExt of TEST_EXTENSIONS) {
20685
20872
  if (baseName.endsWith(testExt)) {
20686
20873
  baseName = baseName.slice(0, -testExt.length);
@@ -20689,12 +20876,12 @@ function computeOutputPath(sourceFile, format, mode, colocatedStyle, baseOutputD
20689
20876
  }
20690
20877
  const fileName = `${baseName}.${effectiveName}${ext}`;
20691
20878
  if (colocatedStyle === "adjacent") {
20692
- return toPosix(path8.posix.join(dirOfSource, fileName));
20879
+ return toPosix(path9.posix.join(dirOfSource, fileName));
20693
20880
  }
20694
20881
  if (colocatedStyle === "flat") {
20695
- return toPosix(path8.posix.join(baseOutputDir, `${cleanTestStem(normalizedSource)}${ext}`));
20882
+ return toPosix(path9.posix.join(baseOutputDir, `${cleanTestStem(normalizedSource)}${ext}`));
20696
20883
  }
20697
- return toPosix(path8.posix.join(baseOutputDir, dirOfSource, fileName));
20884
+ return toPosix(path9.posix.join(baseOutputDir, dirOfSource, fileName));
20698
20885
  }
20699
20886
  function groupTestCasesByOutput(testCases, format, options, logger, outputNameSuffix) {
20700
20887
  const groups = /* @__PURE__ */ new Map();
@@ -20901,8 +21088,8 @@ var ReportGenerator = class {
20901
21088
  if (astroPaths) {
20902
21089
  for (const mdPath of astroPaths) {
20903
21090
  const content = await fsPromises.readFile(mdPath, "utf8");
20904
- const mdDir = path8.dirname(mdPath);
20905
- const assetsDir = path8.resolve(this.options.astro.assetsDir);
21091
+ const mdDir = path9.dirname(mdPath);
21092
+ const assetsDir = path9.resolve(this.options.astro.assetsDir);
20906
21093
  const result = copyMarkdownAssets({
20907
21094
  markdown: content,
20908
21095
  markdownDir: mdDir,
@@ -20933,9 +21120,9 @@ var ReportGenerator = class {
20933
21120
  if (groups.size === 0 && this.options.output.mode === "aggregated") {
20934
21121
  const ext = FORMAT_EXTENSIONS[format];
20935
21122
  const effectiveName = this.options.outputName + (outputNameSuffix ?? "");
20936
- const outputPath = toPosix(path8.join(this.options.outputDir, joinNameAndExt(effectiveName, ext)));
21123
+ const outputPath = toPosix(path9.join(this.options.outputDir, joinNameAndExt(effectiveName, ext)));
20937
21124
  const content = await this.formatContent(run, format);
20938
- const dir = path8.dirname(outputPath);
21125
+ const dir = path9.dirname(outputPath);
20939
21126
  await fsPromises.mkdir(dir, { recursive: true });
20940
21127
  await this.deps.writeFile(outputPath, content);
20941
21128
  return [outputPath];
@@ -20947,7 +21134,7 @@ var ReportGenerator = class {
20947
21134
  testCases
20948
21135
  };
20949
21136
  const content = await this.formatContent(groupRun, format);
20950
- const dir = path8.dirname(outputPath);
21137
+ const dir = path9.dirname(outputPath);
20951
21138
  await fsPromises.mkdir(dir, { recursive: true });
20952
21139
  await this.deps.writeFile(outputPath, content);
20953
21140
  writtenPaths.push(outputPath);
@@ -21056,6 +21243,10 @@ var ReportGenerator = class {
21056
21243
  });
21057
21244
  return formatter.format(run);
21058
21245
  }
21246
+ case "release-manifest": {
21247
+ const formatter = new ReleaseManifestFormatter();
21248
+ return formatter.format(run);
21249
+ }
21059
21250
  case "story-report-json": {
21060
21251
  const formatter = new StoryReportJsonFormatter({
21061
21252
  pretty: this.options.storyReportJson.pretty
@@ -21087,7 +21278,7 @@ async function generateRunComparison(args) {
21087
21278
  await fsPromises.mkdir(outputDir, { recursive: true });
21088
21279
  for (const format of args.formats) {
21089
21280
  const ext = format === "html" ? ".html" : ".md";
21090
- const outputPath = toPosix(path8.join(outputDir, `${outputName}${ext}`));
21281
+ const outputPath = toPosix(path9.join(outputDir, `${outputName}${ext}`));
21091
21282
  const content = format === "html" ? new RunDiffHtmlFormatter({ title: args.title }).format(diff) : new RunDiffMarkdownFormatter({ title: args.title }).format(diff);
21092
21283
  await fsPromises.writeFile(outputPath, content, "utf8");
21093
21284
  files.push(outputPath);
@@ -21096,18 +21287,18 @@ async function generateRunComparison(args) {
21096
21287
  }
21097
21288
 
21098
21289
  // src/init-astro.ts
21099
- import * as fs8 from "fs";
21100
- import * as path9 from "path";
21290
+ import * as fs9 from "fs";
21291
+ import * as path10 from "path";
21101
21292
  import { fileURLToPath } from "url";
21102
- var __dirname = path9.dirname(fileURLToPath(import.meta.url));
21293
+ var __dirname = path10.dirname(fileURLToPath(import.meta.url));
21103
21294
  var FRAMEWORK_DIRS = ["src/components", "src/lib", "src/styles", "src/pages"];
21104
21295
  var FRAMEWORK_FILES = ["tsconfig.json"];
21105
21296
  function initAstro(options = {}) {
21106
21297
  const targetDir = options.targetDir ?? "./story-docs";
21107
21298
  const force = options.force ?? false;
21108
21299
  const update = options.update ?? false;
21109
- const templateDir = path9.resolve(__dirname, "..", "templates", "astro-starlight");
21110
- if (!fs8.existsSync(templateDir)) {
21300
+ const templateDir = path10.resolve(__dirname, "..", "templates", "astro-starlight");
21301
+ if (!fs9.existsSync(templateDir)) {
21111
21302
  throw new Error(
21112
21303
  `Template directory not found at ${templateDir}. Ensure the package is installed correctly.`
21113
21304
  );
@@ -21115,8 +21306,8 @@ function initAstro(options = {}) {
21115
21306
  if (update) {
21116
21307
  return updateFrameworkFiles(templateDir, targetDir);
21117
21308
  }
21118
- if (fs8.existsSync(targetDir)) {
21119
- const entries = fs8.readdirSync(targetDir);
21309
+ if (fs9.existsSync(targetDir)) {
21310
+ const entries = fs9.readdirSync(targetDir);
21120
21311
  if (entries.length > 0 && !force) {
21121
21312
  throw new Error(
21122
21313
  `Directory "${targetDir}" already exists and is not empty. Use --force to overwrite, or --update to refresh framework files only.`
@@ -21127,32 +21318,32 @@ function initAstro(options = {}) {
21127
21318
  return { targetDir };
21128
21319
  }
21129
21320
  function updateFrameworkFiles(templateDir, targetDir) {
21130
- if (!fs8.existsSync(targetDir) || !fs8.existsSync(path9.join(targetDir, "astro.config.mjs"))) {
21321
+ if (!fs9.existsSync(targetDir) || !fs9.existsSync(path10.join(targetDir, "astro.config.mjs"))) {
21131
21322
  throw new Error(
21132
21323
  `"${targetDir}" does not look like a scaffolded docs site. Run init-astro (without --update) first.`
21133
21324
  );
21134
21325
  }
21135
21326
  const updated = [];
21136
21327
  for (const dir of FRAMEWORK_DIRS) {
21137
- const src = path9.join(templateDir, dir);
21138
- if (!fs8.existsSync(src)) continue;
21139
- copyDirRecursive(src, path9.join(targetDir, dir), (rel) => updated.push(path9.join(dir, rel)));
21328
+ const src = path10.join(templateDir, dir);
21329
+ if (!fs9.existsSync(src)) continue;
21330
+ copyDirRecursive(src, path10.join(targetDir, dir), (rel) => updated.push(path10.join(dir, rel)));
21140
21331
  }
21141
21332
  for (const file of FRAMEWORK_FILES) {
21142
- const src = path9.join(templateDir, file);
21143
- if (!fs8.existsSync(src)) continue;
21144
- fs8.copyFileSync(src, path9.join(targetDir, file));
21333
+ const src = path10.join(templateDir, file);
21334
+ if (!fs9.existsSync(src)) continue;
21335
+ fs9.copyFileSync(src, path10.join(targetDir, file));
21145
21336
  updated.push(file);
21146
21337
  }
21147
21338
  if (mergeDependencies(templateDir, targetDir)) updated.push("package.json (deps)");
21148
21339
  return { targetDir, updatedFiles: updated };
21149
21340
  }
21150
21341
  function mergeDependencies(templateDir, targetDir) {
21151
- const tmplPkgPath = path9.join(templateDir, "package.json");
21152
- const userPkgPath = path9.join(targetDir, "package.json");
21153
- if (!fs8.existsSync(tmplPkgPath) || !fs8.existsSync(userPkgPath)) return false;
21154
- const tmpl = JSON.parse(fs8.readFileSync(tmplPkgPath, "utf8"));
21155
- const user = JSON.parse(fs8.readFileSync(userPkgPath, "utf8"));
21342
+ const tmplPkgPath = path10.join(templateDir, "package.json");
21343
+ const userPkgPath = path10.join(targetDir, "package.json");
21344
+ if (!fs9.existsSync(tmplPkgPath) || !fs9.existsSync(userPkgPath)) return false;
21345
+ const tmpl = JSON.parse(fs9.readFileSync(tmplPkgPath, "utf8"));
21346
+ const user = JSON.parse(fs9.readFileSync(userPkgPath, "utf8"));
21156
21347
  user.dependencies = user.dependencies ?? {};
21157
21348
  let changed = false;
21158
21349
  for (const [name, version] of Object.entries(tmpl.dependencies ?? {})) {
@@ -21162,29 +21353,29 @@ function mergeDependencies(templateDir, targetDir) {
21162
21353
  }
21163
21354
  }
21164
21355
  if (changed) {
21165
- fs8.writeFileSync(userPkgPath, `${JSON.stringify(user, null, 2)}
21356
+ fs9.writeFileSync(userPkgPath, `${JSON.stringify(user, null, 2)}
21166
21357
  `, "utf8");
21167
21358
  }
21168
21359
  return changed;
21169
21360
  }
21170
21361
  function copyDirRecursive(src, dest, onFile, baseSrc = src) {
21171
- fs8.mkdirSync(dest, { recursive: true });
21172
- const entries = fs8.readdirSync(src, { withFileTypes: true });
21362
+ fs9.mkdirSync(dest, { recursive: true });
21363
+ const entries = fs9.readdirSync(src, { withFileTypes: true });
21173
21364
  for (const entry of entries) {
21174
- const srcPath = path9.join(src, entry.name);
21175
- const destPath = path9.join(dest, entry.name);
21365
+ const srcPath = path10.join(src, entry.name);
21366
+ const destPath = path10.join(dest, entry.name);
21176
21367
  if (entry.isDirectory()) {
21177
21368
  copyDirRecursive(srcPath, destPath, onFile, baseSrc);
21178
21369
  } else {
21179
- fs8.copyFileSync(srcPath, destPath);
21180
- onFile?.(path9.relative(baseSrc, srcPath));
21370
+ fs9.copyFileSync(srcPath, destPath);
21371
+ onFile?.(path10.relative(baseSrc, srcPath));
21181
21372
  }
21182
21373
  }
21183
21374
  }
21184
21375
 
21185
21376
  // src/scaffold-doc.ts
21186
- import * as fs9 from "fs";
21187
- import * as path10 from "path";
21377
+ import * as fs10 from "fs";
21378
+ import * as path11 from "path";
21188
21379
  var TEMPLATES = ["adr", "runbook", "decision-log", "incident"];
21189
21380
  function slugify3(input) {
21190
21381
  return input.toLowerCase().trim().replace(/['"]/g, "").replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "untitled";
@@ -21195,7 +21386,7 @@ function isoDate(today) {
21195
21386
  function nextSeq(dir) {
21196
21387
  let max = 0;
21197
21388
  try {
21198
- for (const entry of fs9.readdirSync(dir)) {
21389
+ for (const entry of fs10.readdirSync(dir)) {
21199
21390
  const match = /^(\d{1,4})-/.exec(entry);
21200
21391
  if (match) max = Math.max(max, Number.parseInt(match[1], 10));
21201
21392
  }
@@ -21328,11 +21519,11 @@ function scaffoldDoc(options) {
21328
21519
  );
21329
21520
  }
21330
21521
  const spec = TEMPLATE_SPECS[template];
21331
- const baseDir = options.baseDir ?? path10.join("src", "content", "docs");
21522
+ const baseDir = options.baseDir ?? path11.join("src", "content", "docs");
21332
21523
  const today = options.today ?? /* @__PURE__ */ new Date();
21333
21524
  const name = (options.name ?? "").trim() || defaultName(template);
21334
21525
  const slug2 = slugify3(name);
21335
- const dir = path10.join(baseDir, spec.subdir);
21526
+ const dir = path11.join(baseDir, spec.subdir);
21336
21527
  const ctx = {
21337
21528
  name,
21338
21529
  slug: slug2,
@@ -21340,14 +21531,14 @@ function scaffoldDoc(options) {
21340
21531
  seq: nextSeq(dir)
21341
21532
  };
21342
21533
  const filename = `${spec.filename(slug2, ctx)}.mdx`;
21343
- const filePath = path10.join(dir, filename);
21344
- if (fs9.existsSync(filePath) && !options.force) {
21534
+ const filePath = path11.join(dir, filename);
21535
+ if (fs10.existsSync(filePath) && !options.force) {
21345
21536
  throw new Error(
21346
21537
  `File "${filePath}" already exists. Use --force to overwrite.`
21347
21538
  );
21348
21539
  }
21349
- fs9.mkdirSync(dir, { recursive: true });
21350
- fs9.writeFileSync(filePath, spec.content(ctx), "utf8");
21540
+ fs10.mkdirSync(dir, { recursive: true });
21541
+ fs10.writeFileSync(filePath, spec.content(ctx), "utf8");
21351
21542
  return { template, path: filePath, title: titleFor2(template, ctx) };
21352
21543
  }
21353
21544
  function defaultName(template) {
@@ -21376,8 +21567,8 @@ function titleFor2(template, ctx) {
21376
21567
  }
21377
21568
 
21378
21569
  // src/check-links.ts
21379
- import * as fs10 from "fs";
21380
- import * as path11 from "path";
21570
+ import * as fs11 from "fs";
21571
+ import * as path12 from "path";
21381
21572
  function stripCode(markdown) {
21382
21573
  let out = markdown.replace(/^[ \t]*(`{3,}|~{3,})[^\n]*\n[\s\S]*?^[ \t]*\1\s*$/gm, "");
21383
21574
  out = out.replace(/(`+)(?:(?!\1).)+\1/g, "");
@@ -21407,27 +21598,27 @@ function classifyLink(link2) {
21407
21598
  function resolutionCandidates(fromFile, link2) {
21408
21599
  const withoutAnchor = link2.split("#")[0];
21409
21600
  if (!withoutAnchor) return [];
21410
- const base = path11.resolve(path11.dirname(fromFile), withoutAnchor);
21601
+ const base = path12.resolve(path12.dirname(fromFile), withoutAnchor);
21411
21602
  const candidates = [base];
21412
- if (!path11.extname(base)) {
21603
+ if (!path12.extname(base)) {
21413
21604
  candidates.push(`${base}.md`, `${base}.mdx`);
21414
- candidates.push(path11.join(base, "index.md"), path11.join(base, "index.mdx"));
21605
+ candidates.push(path12.join(base, "index.md"), path12.join(base, "index.mdx"));
21415
21606
  }
21416
21607
  return candidates;
21417
21608
  }
21418
21609
  function resolvesOnDisk(fromFile, link2) {
21419
21610
  return resolutionCandidates(fromFile, link2).some(
21420
- (candidate) => fs10.existsSync(candidate) && fs10.statSync(candidate).isFile()
21611
+ (candidate) => fs11.existsSync(candidate) && fs11.statSync(candidate).isFile()
21421
21612
  );
21422
21613
  }
21423
21614
  function collectDocFiles(target) {
21424
- const stat = fs10.statSync(target);
21615
+ const stat = fs11.statSync(target);
21425
21616
  if (stat.isFile()) return [target];
21426
21617
  const out = [];
21427
21618
  const walk = (dir) => {
21428
- for (const entry of fs10.readdirSync(dir, { withFileTypes: true })) {
21619
+ for (const entry of fs11.readdirSync(dir, { withFileTypes: true })) {
21429
21620
  if (entry.name.startsWith(".") || entry.name === "node_modules") continue;
21430
- const full = path11.join(dir, entry.name);
21621
+ const full = path12.join(dir, entry.name);
21431
21622
  if (entry.isDirectory()) walk(full);
21432
21623
  else if (/\.mdx?$/.test(entry.name)) out.push(full);
21433
21624
  }
@@ -21458,7 +21649,7 @@ async function isExternalAlive(url, timeoutMs) {
21458
21649
  }
21459
21650
  async function checkLinks(options) {
21460
21651
  const { target, checkExternal = false, externalTimeoutMs = 8e3 } = options;
21461
- if (!fs10.existsSync(target)) {
21652
+ if (!fs11.existsSync(target)) {
21462
21653
  throw new Error(`Path not found: ${target}`);
21463
21654
  }
21464
21655
  const files = collectDocFiles(target);
@@ -21468,7 +21659,7 @@ async function checkLinks(options) {
21468
21659
  let skipped = 0;
21469
21660
  const externalCache = /* @__PURE__ */ new Map();
21470
21661
  for (const file of files) {
21471
- const content = fs10.readFileSync(file, "utf8");
21662
+ const content = fs11.readFileSync(file, "utf8");
21472
21663
  for (const link2 of extractLinks(content)) {
21473
21664
  const kind = classifyLink(link2);
21474
21665
  if (kind === "anchor" || kind === "mail" || kind === "root") {
@@ -21524,8 +21715,8 @@ function formatLinkReport(report) {
21524
21715
  }
21525
21716
 
21526
21717
  // src/import-openapi.ts
21527
- import * as fs11 from "fs";
21528
- import * as path12 from "path";
21718
+ import * as fs12 from "fs";
21719
+ import * as path13 from "path";
21529
21720
  import { parse as parseYamlString } from "yaml";
21530
21721
  var HTTP_METHODS = ["get", "post", "put", "patch", "delete", "options", "head"];
21531
21722
  function parseYaml(raw, specPath) {
@@ -21538,9 +21729,9 @@ function parseYaml(raw, specPath) {
21538
21729
  }
21539
21730
  }
21540
21731
  function parseSpec(specPath) {
21541
- if (!fs11.existsSync(specPath)) throw new Error(`Spec not found: ${specPath}`);
21542
- const raw = fs11.readFileSync(specPath, "utf8");
21543
- const ext = path12.extname(specPath).toLowerCase();
21732
+ if (!fs12.existsSync(specPath)) throw new Error(`Spec not found: ${specPath}`);
21733
+ const raw = fs12.readFileSync(specPath, "utf8");
21734
+ const ext = path13.extname(specPath).toLowerCase();
21544
21735
  if (ext === ".json") return JSON.parse(raw);
21545
21736
  if (ext === ".yaml" || ext === ".yml") return parseYaml(raw, specPath);
21546
21737
  try {
@@ -21571,8 +21762,8 @@ function extractEndpoints(spec) {
21571
21762
  }
21572
21763
  function loadScenarios(runFile) {
21573
21764
  if (!runFile) return [];
21574
- if (!fs11.existsSync(runFile)) throw new Error(`Run file not found: ${runFile}`);
21575
- const report = JSON.parse(fs11.readFileSync(runFile, "utf8"));
21765
+ if (!fs12.existsSync(runFile)) throw new Error(`Run file not found: ${runFile}`);
21766
+ const report = JSON.parse(fs12.readFileSync(runFile, "utf8"));
21576
21767
  return (report.features ?? []).flatMap((f) => f.scenarios ?? []);
21577
21768
  }
21578
21769
  function endpointRefs(endpoint) {
@@ -21679,25 +21870,25 @@ async function importOpenApi(options) {
21679
21870
  list.push(item);
21680
21871
  groups.set(item.endpoint.tag, list);
21681
21872
  }
21682
- const outputDir = options.outputDir ?? path12.join("src", "content", "docs", "api");
21683
- if (fs11.existsSync(outputDir) && !options.force) {
21684
- const entries = fs11.readdirSync(outputDir);
21873
+ const outputDir = options.outputDir ?? path13.join("src", "content", "docs", "api");
21874
+ if (fs12.existsSync(outputDir) && !options.force) {
21875
+ const entries = fs12.readdirSync(outputDir);
21685
21876
  if (entries.length > 0) {
21686
21877
  throw new Error(`Output directory "${outputDir}" is not empty. Use --force to overwrite.`);
21687
21878
  }
21688
21879
  }
21689
- fs11.mkdirSync(outputDir, { recursive: true });
21880
+ fs12.mkdirSync(outputDir, { recursive: true });
21690
21881
  const coveredCount = coverage.filter((c) => c.status === "covered").length;
21691
21882
  const uncoveredCount = coverage.filter((c) => c.status === "uncovered").length;
21692
- fs11.writeFileSync(
21693
- path12.join(outputDir, "index.mdx"),
21883
+ fs12.writeFileSync(
21884
+ path13.join(outputDir, "index.mdx"),
21694
21885
  renderIndex(groups, hasRun, { endpointCount: endpoints.length, coveredCount, uncoveredCount }),
21695
21886
  "utf8"
21696
21887
  );
21697
21888
  for (const [tag, rows] of groups) {
21698
- const dir = path12.join(outputDir, slug(tag));
21699
- fs11.mkdirSync(dir, { recursive: true });
21700
- fs11.writeFileSync(path12.join(dir, "index.mdx"), renderTagPage(tag, rows, hasRun), "utf8");
21889
+ const dir = path13.join(outputDir, slug(tag));
21890
+ fs12.mkdirSync(dir, { recursive: true });
21891
+ fs12.writeFileSync(path13.join(dir, "index.mdx"), renderTagPage(tag, rows, hasRun), "utf8");
21701
21892
  }
21702
21893
  return {
21703
21894
  outputDir,
@@ -21709,8 +21900,8 @@ async function importOpenApi(options) {
21709
21900
  }
21710
21901
 
21711
21902
  // src/build-docs.ts
21712
- import * as fs12 from "fs";
21713
- import * as path13 from "path";
21903
+ import * as fs13 from "fs";
21904
+ import * as path14 from "path";
21714
21905
  var BuildDocsError = class extends Error {
21715
21906
  constructor(message, kind) {
21716
21907
  super(message);
@@ -21720,22 +21911,22 @@ var BuildDocsError = class extends Error {
21720
21911
  };
21721
21912
  var isRemote = (p) => /^(?:https?:|data:)/i.test(p);
21722
21913
  function bundleExplorerAssets(reportPath, assetsDir, baseUrl = "/stories/assets") {
21723
- if (!fs12.existsSync(reportPath)) return 0;
21724
- const report = JSON.parse(fs12.readFileSync(reportPath, "utf8"));
21914
+ if (!fs13.existsSync(reportPath)) return 0;
21915
+ const report = JSON.parse(fs13.readFileSync(reportPath, "utf8"));
21725
21916
  let copied = 0;
21726
21917
  const bundle = (value) => {
21727
- const rel = copyAsset(path13.resolve(value), assetsDir);
21918
+ const rel = copyAsset(path14.resolve(value), assetsDir);
21728
21919
  copied++;
21729
- return `${baseUrl}/${path13.basename(rel)}`;
21920
+ return `${baseUrl}/${path14.basename(rel)}`;
21730
21921
  };
21731
21922
  const visit = (entries) => {
21732
21923
  for (const entry of entries ?? []) {
21733
21924
  const e = entry;
21734
21925
  if (e.kind === "screenshot" || e.kind === "video") {
21735
- if (typeof e.path === "string" && !isRemote(e.path) && fs12.existsSync(e.path)) {
21926
+ if (typeof e.path === "string" && !isRemote(e.path) && fs13.existsSync(e.path)) {
21736
21927
  e.path = bundle(e.path);
21737
21928
  }
21738
- if (typeof e.poster === "string" && !isRemote(e.poster) && fs12.existsSync(e.poster)) {
21929
+ if (typeof e.poster === "string" && !isRemote(e.poster) && fs13.existsSync(e.poster)) {
21739
21930
  e.poster = bundle(e.poster);
21740
21931
  }
21741
21932
  }
@@ -21748,25 +21939,25 @@ function bundleExplorerAssets(reportPath, assetsDir, baseUrl = "/stories/assets"
21748
21939
  }
21749
21940
  }
21750
21941
  if (copied > 0) {
21751
- fs12.writeFileSync(reportPath, JSON.stringify(report, null, 2), "utf8");
21942
+ fs13.writeFileSync(reportPath, JSON.stringify(report, null, 2), "utf8");
21752
21943
  }
21753
21944
  return copied;
21754
21945
  }
21755
21946
  function clearGeneratedPages(dir) {
21756
- if (!fs12.existsSync(dir)) return;
21757
- for (const entry of fs12.readdirSync(dir, { withFileTypes: true })) {
21758
- const full = path13.join(dir, entry.name);
21947
+ if (!fs13.existsSync(dir)) return;
21948
+ for (const entry of fs13.readdirSync(dir, { withFileTypes: true })) {
21949
+ const full = path14.join(dir, entry.name);
21759
21950
  if (entry.isDirectory()) {
21760
21951
  clearGeneratedPages(full);
21761
- if (fs12.readdirSync(full).length === 0) fs12.rmdirSync(full);
21952
+ if (fs13.readdirSync(full).length === 0) fs13.rmdirSync(full);
21762
21953
  } else if (/\.mdx?$/.test(entry.name)) {
21763
- fs12.rmSync(full);
21954
+ fs13.rmSync(full);
21764
21955
  }
21765
21956
  }
21766
21957
  }
21767
21958
  function loadCanonicalRun(rawRunPath, synthesize) {
21768
21959
  try {
21769
- const data = JSON.parse(fs12.readFileSync(path13.resolve(rawRunPath), "utf8"));
21960
+ const data = JSON.parse(fs13.readFileSync(path14.resolve(rawRunPath), "utf8"));
21770
21961
  if (data.schemaVersion !== 1) {
21771
21962
  throw new BuildDocsError(`Unsupported schemaVersion ${data.schemaVersion}. Supported: 1.`, "schema");
21772
21963
  }
@@ -21789,12 +21980,12 @@ ${schemaResult.errors.map((e) => ` ${e}`).join("\n")}`,
21789
21980
  }
21790
21981
  }
21791
21982
  async function buildDocs(options) {
21792
- const siteDir = path13.resolve(options.siteDir);
21793
- const storiesPublicDir = path13.join(siteDir, "public", "stories");
21794
- const assetsDir = path13.join(storiesPublicDir, "assets");
21795
- const storyPagesDir = path13.join(siteDir, "src", "content", "docs", "stories");
21796
- const apiDir = path13.join(siteDir, "src", "content", "docs", "api");
21797
- const reportPath = path13.join(storiesPublicDir, "story-report.json");
21983
+ const siteDir = path14.resolve(options.siteDir);
21984
+ const storiesPublicDir = path14.join(siteDir, "public", "stories");
21985
+ const assetsDir = path14.join(storiesPublicDir, "assets");
21986
+ const storyPagesDir = path14.join(siteDir, "src", "content", "docs", "stories");
21987
+ const apiDir = path14.join(siteDir, "src", "content", "docs", "api");
21988
+ const reportPath = path14.join(storiesPublicDir, "story-report.json");
21798
21989
  const canonical = loadCanonicalRun(options.rawRunPath, options.synthesizeStories ?? true);
21799
21990
  try {
21800
21991
  await new ReportGenerator({
@@ -21815,7 +22006,7 @@ async function buildDocs(options) {
21815
22006
  let apiPages = 0;
21816
22007
  if (options.openapiPath) {
21817
22008
  const res = await importOpenApi({
21818
- specPath: path13.resolve(options.openapiPath),
22009
+ specPath: path14.resolve(options.openapiPath),
21819
22010
  outputDir: apiDir,
21820
22011
  runFile: reportPath,
21821
22012
  force: true
@@ -21830,11 +22021,11 @@ async function buildDocs(options) {
21830
22021
  }
21831
22022
 
21832
22023
  // src/config.ts
21833
- import { existsSync as existsSync11 } from "fs";
21834
- import { resolve as resolve9 } from "path";
22024
+ import { existsSync as existsSync12 } from "fs";
22025
+ import { resolve as resolve10 } from "path";
21835
22026
  async function loadConfig(configPath) {
21836
- const resolved = configPath ? resolve9(configPath) : resolve9(process.cwd(), "executable-stories.config.js");
21837
- if (!existsSync11(resolved)) return {};
22027
+ const resolved = configPath ? resolve10(configPath) : resolve10(process.cwd(), "executable-stories.config.js");
22028
+ if (!existsSync12(resolved)) return {};
21838
22029
  const mod = await import(resolved);
21839
22030
  const config = mod.default;
21840
22031
  if (!config || typeof config !== "object" || Array.isArray(config)) {
@@ -21853,6 +22044,7 @@ var EXIT_GENERATION = 3;
21853
22044
  var EXIT_USAGE = 4;
21854
22045
  var EXIT_COMPARE_GATE = 5;
21855
22046
  var EXIT_REVIEW_GATE = 5;
22047
+ var EXIT_RELEASE_GATE = 6;
21856
22048
  var HELP_TEXT = `
21857
22049
  executable-stories \u2014 Generate reports from test results JSON.
21858
22050
 
@@ -21860,6 +22052,7 @@ USAGE
21860
22052
  executable-stories format <file> [options]
21861
22053
  executable-stories format --stdin [options]
21862
22054
  executable-stories compare <baseline-file> <current-file> [options]
22055
+ executable-stories gate-release <dev-run.json> <rc-run.json> [options]
21863
22056
  executable-stories review <file> --changed-files <path> [options]
21864
22057
  executable-stories list <file> [options]
21865
22058
  executable-stories validate <file>
@@ -21870,11 +22063,15 @@ USAGE
21870
22063
  executable-stories import-openapi <spec> [options]
21871
22064
  executable-stories publish-confluence <file.adf.json> [options]
21872
22065
  executable-stories publish-jira <file.adf.json> [options]
22066
+ executable-stories deploy record <file> --env <env> [--tag <tag>] [options]
22067
+ executable-stories deploy status [options]
22068
+ executable-stories deploy diff <env-a> <env-b> [options]
21873
22069
 
21874
22070
  SUBCOMMANDS
21875
22071
  format Read raw test results and generate reports
21876
22072
  watch Regenerate reports whenever the raw-run file changes (live agent index)
21877
22073
  compare Compare two runs and generate a diff report
22074
+ gate-release Verify a release candidate against the dev test baseline (RC gate)
21878
22075
  review Generate an Evidence Review of AI-authored changes (correlate a run to the diff)
21879
22076
  list List scenarios from a test run (text table or JSON)
21880
22077
  validate Validate a JSON file against the schema (no output generated)
@@ -21884,9 +22081,10 @@ SUBCOMMANDS
21884
22081
  import-openapi Generate API doc pages from an OpenAPI spec, linked to verifying stories
21885
22082
  publish-confluence Publish an ADF JSON file to a Confluence page via REST API
21886
22083
  publish-jira Publish an ADF JSON file to a Jira issue (as comment or description)
22084
+ deploy Record deployments, show environment status, detect drift
21887
22085
 
21888
22086
  OPTIONS
21889
- --format <formats> Comma-separated formats: html, markdown, junit, cucumber-json, cucumber-messages, cucumber-html, astro, confluence, story-report-json, scenario-index-json, behavior-manifest-json, or custom names from config (default: html)
22087
+ --format <formats> Comma-separated formats: html, markdown, release-manifest, junit, cucumber-json, cucumber-messages, cucumber-html, astro, confluence, story-report-json, scenario-index-json, behavior-manifest-json, or custom names from config (default: html)
21890
22088
  astro Themed Markdown (for Astro docs sites with matching CSS)
21891
22089
  confluence Atlassian Document Format (ADF) JSON for Confluence / Jira
21892
22090
  behavior-manifest-json Agent-readable behavior manifest and debugger warnings
@@ -21930,7 +22128,10 @@ OPTIONS
21930
22128
  --pr-summary-file <path> Write the PR-friendly markdown summary to a file
21931
22129
  --fail-on-regression Exit non-zero when any regression is detected in compare
21932
22130
  --fail-on-added-failures Exit non-zero when newly added scenarios are failing
22131
+ --fail-on-removal Exit non-zero when scenarios are removed from the baseline
22132
+ --fail-on-new Exit non-zero when new scenarios appear that weren't in the baseline
21933
22133
  --max-regressions <n> Exit non-zero when regressions exceed threshold
22134
+ --release-policy <path> (gate-release) Path to JSON policy file with allowed exceptions
21934
22135
  --changed-files <path> (review) Changed files: JSON (ChangedFile[] or {changedFiles,baseRef,headRef}) or "git diff --name-status" text
21935
22136
  --base-ref <ref> (review) Base ref label shown in the report (informational)
21936
22137
  --head-ref <ref> (review) Head ref label shown in the report (informational)
@@ -21949,6 +22150,24 @@ COMPARE
21949
22150
  compare supports --format html,markdown
21950
22151
  compare uses the same --input-type for both baseline and current files
21951
22152
 
22153
+ GATE-RELEASE
22154
+ gate-release compares a dev environment test run (baseline) against a
22155
+ release candidate test run to verify the RC matches what was tested in dev.
22156
+ By default, fails if scenarios are omitted or regressed.
22157
+ --fail-on-regression and --fail-on-removal are enabled by default.
22158
+ Supports --release-policy for exception lists.
22159
+
22160
+ DEPLOY
22161
+ executable-stories deploy record <file> --env <env> [--tag <tag>]
22162
+ Record a deployment of a test run to an environment (e.g. dev, staging, prod).
22163
+ The deployment ledger is at .executable-stories/deployments.json by default.
22164
+
22165
+ executable-stories deploy status [--ledger <path>]
22166
+ Show the latest deployment for each environment.
22167
+
22168
+ executable-stories deploy diff <env-a> <env-b> [--ledger <path>]
22169
+ Show scenario drift between two environments (what's in one but not the other).
22170
+
21952
22171
  INIT-ASTRO
21953
22172
  executable-stories init-astro [directory] Scaffold into directory (default: ./story-docs)
21954
22173
  --force Overwrite existing directory
@@ -22000,6 +22219,7 @@ EXIT CODES
22000
22219
  3 Formatter/generation failure
22001
22220
  4 Bad arguments / usage error
22002
22221
  5 Compare gate failed
22222
+ 6 Release gate failed
22003
22223
  `.trim();
22004
22224
  async function parseCliArgs(argv) {
22005
22225
  const args = argv.slice(2);
@@ -22008,9 +22228,9 @@ async function parseCliArgs(argv) {
22008
22228
  process.exit(EXIT_SUCCESS);
22009
22229
  }
22010
22230
  const subcommand = args[0];
22011
- if (subcommand !== "format" && subcommand !== "watch" && subcommand !== "compare" && subcommand !== "review" && subcommand !== "list" && subcommand !== "validate" && subcommand !== "init-astro" && subcommand !== "build-docs" && subcommand !== "new" && subcommand !== "check-links" && subcommand !== "import-openapi" && subcommand !== "publish-confluence" && subcommand !== "publish-jira") {
22231
+ if (subcommand !== "format" && subcommand !== "watch" && subcommand !== "compare" && subcommand !== "gate-release" && subcommand !== "deploy" && subcommand !== "review" && subcommand !== "list" && subcommand !== "validate" && subcommand !== "init-astro" && subcommand !== "build-docs" && subcommand !== "new" && subcommand !== "check-links" && subcommand !== "import-openapi" && subcommand !== "publish-confluence" && subcommand !== "publish-jira") {
22012
22232
  console.error(
22013
- `Unknown subcommand: "${subcommand}". Use "format", "watch", "compare", "review", "list", "validate", "init-astro", "build-docs", "new", "check-links", "import-openapi", "publish-confluence", or "publish-jira".`
22233
+ `Unknown subcommand: "${subcommand}". Use "format", "watch", "compare", "gate-release", "deploy", "review", "list", "validate", "init-astro", "build-docs", "new", "check-links", "import-openapi", "publish-confluence", or "publish-jira".`
22014
22234
  );
22015
22235
  process.exit(EXIT_USAGE);
22016
22236
  }
@@ -22022,6 +22242,9 @@ async function parseCliArgs(argv) {
22022
22242
  await runPublishJira(args.slice(1));
22023
22243
  process.exit(EXIT_SUCCESS);
22024
22244
  }
22245
+ if (subcommand === "deploy") {
22246
+ process.exit(await runDeploy(args.slice(1)));
22247
+ }
22025
22248
  if (subcommand === "init-astro") {
22026
22249
  const initArgs = args.slice(1);
22027
22250
  const targetDir = initArgs.find((a) => !a.startsWith("--")) ?? "./story-docs";
@@ -22115,7 +22338,10 @@ async function parseCliArgs(argv) {
22115
22338
  "pr-summary-file": { type: "string" },
22116
22339
  "fail-on-regression": { type: "boolean", default: false },
22117
22340
  "fail-on-added-failures": { type: "boolean", default: false },
22341
+ "fail-on-removal": { type: "boolean", default: false },
22342
+ "fail-on-new": { type: "boolean", default: false },
22118
22343
  "max-regressions": { type: "string" },
22344
+ "release-policy": { type: "string" },
22119
22345
  "changed-files": { type: "string" },
22120
22346
  "base-ref": { type: "string" },
22121
22347
  "head-ref": { type: "string" },
@@ -22134,20 +22360,21 @@ async function parseCliArgs(argv) {
22134
22360
  const useStdin = values.stdin;
22135
22361
  const baselineValue = values.baseline;
22136
22362
  const baselineMode = baselineValue === "auto" ? "auto" : "explicit";
22137
- const inputFile = subcommand === "compare" ? void 0 : positionals[0];
22138
- const baselineFile = subcommand === "compare" ? baselineMode === "auto" ? baselineValue && baselineValue !== "auto" ? baselineValue : positionals.length > 1 ? positionals[0] : void 0 : baselineValue && baselineValue !== "auto" ? baselineValue : positionals.length > 1 ? positionals[0] : void 0 : void 0;
22139
- const currentFile = subcommand === "compare" ? positionals.length > 1 ? positionals[1] : positionals[0] : void 0;
22140
- if (subcommand === "compare") {
22363
+ const isCompareLike = subcommand === "compare" || subcommand === "gate-release";
22364
+ const inputFile = isCompareLike ? void 0 : positionals[0];
22365
+ const baselineFile = isCompareLike ? baselineMode === "auto" ? baselineValue && baselineValue !== "auto" ? baselineValue : positionals.length > 1 ? positionals[0] : void 0 : baselineValue && baselineValue !== "auto" ? baselineValue : positionals.length > 1 ? positionals[0] : void 0 : void 0;
22366
+ const currentFile = isCompareLike ? positionals.length > 1 ? positionals[1] : positionals[0] : void 0;
22367
+ if (isCompareLike) {
22141
22368
  if (useStdin) {
22142
- console.error("Error: compare does not support --stdin. Pass baseline and current files.");
22369
+ console.error(`Error: ${subcommand} does not support --stdin. Pass baseline and current files.`);
22143
22370
  process.exit(EXIT_USAGE);
22144
22371
  }
22145
22372
  if (!currentFile) {
22146
- console.error("Error: compare requires <current-file>, and either <baseline-file> or --baseline auto.");
22373
+ console.error(`Error: ${subcommand} requires <current-file>, and either <baseline-file> or --baseline auto.`);
22147
22374
  process.exit(EXIT_USAGE);
22148
22375
  }
22149
22376
  if (baselineMode === "explicit" && !baselineFile) {
22150
- console.error("Error: compare requires <baseline-file> and <current-file>, or use --baseline auto.");
22377
+ console.error(`Error: ${subcommand} requires <baseline-file> and <current-file>, or use --baseline auto.`);
22151
22378
  process.exit(EXIT_USAGE);
22152
22379
  }
22153
22380
  } else if (!useStdin && !inputFile) {
@@ -22161,7 +22388,7 @@ async function parseCliArgs(argv) {
22161
22388
  }
22162
22389
  const pluginConfig = await loadConfig(values["config"]);
22163
22390
  const customFormatterNames = new Set(Object.keys(pluginConfig.formatters ?? {}));
22164
- const builtInFormats = /* @__PURE__ */ new Set(["astro", "behavior-manifest-json", "confluence", "html", "markdown", "junit", "cucumber-json", "cucumber-messages", "cucumber-html", "scenario-index-json", "story-report-json"]);
22391
+ const builtInFormats = /* @__PURE__ */ new Set(["astro", "behavior-manifest-json", "confluence", "html", "markdown", "release-manifest", "junit", "cucumber-json", "cucumber-messages", "cucumber-html", "scenario-index-json", "story-report-json"]);
22165
22392
  const formatStr = values.format;
22166
22393
  const allRequestedFormats = formatStr.split(",").map((f) => f.trim());
22167
22394
  const builtInRequested = allRequestedFormats.filter((f) => builtInFormats.has(f));
@@ -22169,7 +22396,7 @@ async function parseCliArgs(argv) {
22169
22396
  const unknownFormats = allRequestedFormats.filter((f) => !builtInFormats.has(f) && !customFormatterNames.has(f));
22170
22397
  if (unknownFormats.length > 0) {
22171
22398
  const knownCustom = customFormatterNames.size > 0 ? `, ${[...customFormatterNames].join(", ")}` : "";
22172
- console.error(`Error: Unknown format(s): ${unknownFormats.join(", ")}. Valid built-in: astro, behavior-manifest-json, confluence, html, markdown, junit, cucumber-json, cucumber-messages, cucumber-html, scenario-index-json, story-report-json${knownCustom}.`);
22399
+ console.error(`Error: Unknown format(s): ${unknownFormats.join(", ")}. Valid built-in: astro, behavior-manifest-json, confluence, html, markdown, release-manifest, junit, cucumber-json, cucumber-messages, cucumber-html, scenario-index-json, story-report-json${knownCustom}.`);
22173
22400
  process.exit(EXIT_USAGE);
22174
22401
  }
22175
22402
  const formats = builtInRequested;
@@ -22307,7 +22534,10 @@ async function parseCliArgs(argv) {
22307
22534
  prSummaryFile: values["pr-summary-file"],
22308
22535
  failOnRegression: values["fail-on-regression"],
22309
22536
  failOnAddedFailures: values["fail-on-added-failures"],
22537
+ failOnRemoval: values["fail-on-removal"],
22538
+ failOnNew: values["fail-on-new"],
22310
22539
  maxRegressions,
22540
+ releasePolicy: values["release-policy"],
22311
22541
  changedFilesPath: values["changed-files"],
22312
22542
  baseRef: values["base-ref"],
22313
22543
  headRef: values["head-ref"],
@@ -22321,27 +22551,27 @@ async function readInput(args) {
22321
22551
  if (args.stdin) {
22322
22552
  return readStdin();
22323
22553
  }
22324
- const filePath = path14.resolve(args.inputFile);
22325
- if (!fs13.existsSync(filePath)) {
22554
+ const filePath = path15.resolve(args.inputFile);
22555
+ if (!fs14.existsSync(filePath)) {
22326
22556
  console.error(`Error: File not found: ${filePath}`);
22327
22557
  process.exit(EXIT_USAGE);
22328
22558
  }
22329
- return fs13.readFileSync(filePath, "utf8");
22559
+ return fs14.readFileSync(filePath, "utf8");
22330
22560
  }
22331
22561
  function readFileInput(filePath) {
22332
- const resolved = path14.resolve(filePath);
22333
- if (!fs13.existsSync(resolved)) {
22562
+ const resolved = path15.resolve(filePath);
22563
+ if (!fs14.existsSync(resolved)) {
22334
22564
  console.error(`Error: File not found: ${resolved}`);
22335
22565
  process.exit(EXIT_USAGE);
22336
22566
  }
22337
- return fs13.readFileSync(resolved, "utf8");
22567
+ return fs14.readFileSync(resolved, "utf8");
22338
22568
  }
22339
22569
  function readStdin() {
22340
- return new Promise((resolve11, reject) => {
22570
+ return new Promise((resolve12, reject) => {
22341
22571
  const chunks = [];
22342
22572
  process.stdin.setEncoding("utf8");
22343
22573
  process.stdin.on("data", (chunk) => chunks.push(chunk));
22344
- process.stdin.on("end", () => resolve11(chunks.join("")));
22574
+ process.stdin.on("end", () => resolve12(chunks.join("")));
22345
22575
  process.stdin.on("error", reject);
22346
22576
  });
22347
22577
  }
@@ -22467,14 +22697,14 @@ function tryNormalizeRunFromText(text2, args) {
22467
22697
  }
22468
22698
  }
22469
22699
  function listBaselineCandidates(currentFile, args) {
22470
- const baselineDir = path14.resolve(args.baselineDir ?? path14.dirname(currentFile));
22471
- const currentResolved = path14.resolve(currentFile);
22472
- if (!fs13.existsSync(baselineDir)) {
22700
+ const baselineDir = path15.resolve(args.baselineDir ?? path15.dirname(currentFile));
22701
+ const currentResolved = path15.resolve(currentFile);
22702
+ if (!fs14.existsSync(baselineDir)) {
22473
22703
  console.error(`Error: baseline directory not found: ${baselineDir}`);
22474
22704
  process.exit(EXIT_USAGE);
22475
22705
  }
22476
- const entries = fs13.readdirSync(baselineDir, { withFileTypes: true });
22477
- return entries.filter((entry) => entry.isFile()).map((entry) => path14.join(baselineDir, entry.name)).filter((candidate) => path14.resolve(candidate) !== currentResolved).filter(
22706
+ const entries = fs14.readdirSync(baselineDir, { withFileTypes: true });
22707
+ return entries.filter((entry) => entry.isFile()).map((entry) => path15.join(baselineDir, entry.name)).filter((candidate) => path15.resolve(candidate) !== currentResolved).filter(
22478
22708
  (candidate) => args.inputType === "ndjson" ? candidate.endsWith(".ndjson") : candidate.endsWith(".json")
22479
22709
  );
22480
22710
  }
@@ -22482,14 +22712,14 @@ function resolveBaselineAuto(currentFile, currentRun, args) {
22482
22712
  const candidates = listBaselineCandidates(currentFile, args);
22483
22713
  const comparable = [];
22484
22714
  for (const candidate of candidates) {
22485
- const run = tryNormalizeRunFromText(fs13.readFileSync(candidate, "utf8"), args);
22715
+ const run = tryNormalizeRunFromText(fs14.readFileSync(candidate, "utf8"), args);
22486
22716
  if (run) {
22487
22717
  comparable.push({ file: candidate, run });
22488
22718
  }
22489
22719
  }
22490
22720
  if (comparable.length === 0) {
22491
22721
  console.error(
22492
- `Error: no compatible baseline files found in ${path14.resolve(args.baselineDir ?? path14.dirname(currentFile))}.`
22722
+ `Error: no compatible baseline files found in ${path15.resolve(args.baselineDir ?? path15.dirname(currentFile))}.`
22493
22723
  );
22494
22724
  process.exit(EXIT_USAGE);
22495
22725
  }
@@ -22526,6 +22756,50 @@ async function main() {
22526
22756
  process.exit(EXIT_GENERATION);
22527
22757
  }
22528
22758
  }
22759
+ if (args.subcommand === "gate-release") {
22760
+ const gatedArgs = {
22761
+ ...args,
22762
+ failOnRegression: true,
22763
+ failOnRemoval: true
22764
+ // failOnNew is opt-in via --fail-on-new flag
22765
+ };
22766
+ let policy;
22767
+ if (args.releasePolicy) {
22768
+ policy = loadReleasePolicy(args.releasePolicy);
22769
+ }
22770
+ const currentText = readFileInput(gatedArgs.currentFile);
22771
+ const current = applySelection(normalizeRunFromText(currentText, gatedArgs).run, gatedArgs);
22772
+ const baselineFile = gatedArgs.baselineMode === "auto" ? resolveBaselineAuto(gatedArgs.currentFile, current, gatedArgs) : gatedArgs.baselineFile;
22773
+ const baselineText = readFileInput(baselineFile);
22774
+ const baseline = applySelection(normalizeRunFromText(baselineText, gatedArgs).run, gatedArgs);
22775
+ try {
22776
+ const result = await generateCompareReports(baseline, current, baselineFile, gatedArgs);
22777
+ const effectiveResult = policy ? applyReleasePolicy(result, policy) : result;
22778
+ printCompareResult(effectiveResult, gatedArgs, startMs);
22779
+ const gateFailures = evaluateCompareGate(effectiveResult, gatedArgs);
22780
+ if (gateFailures.length > 0) {
22781
+ for (const failure of gateFailures) {
22782
+ console.error(`Release gate failed: ${failure}`);
22783
+ }
22784
+ if (policy) {
22785
+ console.error(`Release policy: ${args.releasePolicy}`);
22786
+ if (policy.allowedOmissions && policy.allowedOmissions.length > 0) {
22787
+ console.error(` Allowed omissions: ${policy.allowedOmissions.join(", ")}`);
22788
+ }
22789
+ if (policy.allowedRegressions && policy.allowedRegressions.length > 0) {
22790
+ console.error(` Allowed regressions: ${policy.allowedRegressions.join(", ")}`);
22791
+ }
22792
+ }
22793
+ process.exit(EXIT_RELEASE_GATE);
22794
+ }
22795
+ console.error("Release gate passed: RC matches dev baseline.");
22796
+ process.exit(EXIT_SUCCESS);
22797
+ } catch (err) {
22798
+ const msg = err instanceof Error ? err.message : String(err);
22799
+ console.error(`Release gate check failed: ${msg}`);
22800
+ process.exit(EXIT_GENERATION);
22801
+ }
22802
+ }
22529
22803
  if (args.subcommand === "review") {
22530
22804
  const text3 = await readInput(args);
22531
22805
  const run = applySelection(normalizeRunFromText(text3, args).run, args);
@@ -22627,9 +22901,9 @@ async function main() {
22627
22901
  process.exit(EXIT_SCHEMA_VALIDATION);
22628
22902
  }
22629
22903
  if (args.emitCanonical) {
22630
- const outPath = path14.resolve(args.emitCanonical);
22631
- fs13.mkdirSync(path14.dirname(outPath), { recursive: true });
22632
- fs13.writeFileSync(outPath, JSON.stringify(run, null, 2), "utf8");
22904
+ const outPath = path15.resolve(args.emitCanonical);
22905
+ fs14.mkdirSync(path15.dirname(outPath), { recursive: true });
22906
+ fs14.writeFileSync(outPath, JSON.stringify(run, null, 2), "utf8");
22633
22907
  }
22634
22908
  try {
22635
22909
  const result = await generateReports(run, args);
@@ -22686,9 +22960,9 @@ ${msg}`);
22686
22960
  }
22687
22961
  const run = data;
22688
22962
  if (args.emitCanonical) {
22689
- const outPath = path14.resolve(args.emitCanonical);
22690
- fs13.mkdirSync(path14.dirname(outPath), { recursive: true });
22691
- fs13.writeFileSync(outPath, JSON.stringify(run, null, 2), "utf8");
22963
+ const outPath = path15.resolve(args.emitCanonical);
22964
+ fs14.mkdirSync(path15.dirname(outPath), { recursive: true });
22965
+ fs14.writeFileSync(outPath, JSON.stringify(run, null, 2), "utf8");
22692
22966
  }
22693
22967
  try {
22694
22968
  const result = await generateReports(run, args);
@@ -22744,9 +23018,9 @@ ${msg}`);
22744
23018
  process.exit(EXIT_CANONICAL_VALIDATION);
22745
23019
  }
22746
23020
  if (args.emitCanonical) {
22747
- const outPath = path14.resolve(args.emitCanonical);
22748
- fs13.mkdirSync(path14.dirname(outPath), { recursive: true });
22749
- fs13.writeFileSync(outPath, JSON.stringify(canonical, null, 2), "utf8");
23021
+ const outPath = path15.resolve(args.emitCanonical);
23022
+ fs14.mkdirSync(path15.dirname(outPath), { recursive: true });
23023
+ fs14.writeFileSync(outPath, JSON.stringify(canonical, null, 2), "utf8");
22750
23024
  }
22751
23025
  try {
22752
23026
  const result = await generateReports(canonical, args, droppedMissingStory);
@@ -22771,9 +23045,9 @@ function runCustomFormatters(run, customRequested, formatters, args) {
22771
23045
  const ext = formatter.fileExtension ?? formatName;
22772
23046
  const baseName = args.outputName ?? "report";
22773
23047
  const filename = args.outputNameTimestamp ? `${baseName}-${Math.floor(run.startedAtMs / 1e3)}.${ext}` : `${baseName}.${ext}`;
22774
- const filepath = path14.join(outputDir, filename);
22775
- fs13.mkdirSync(outputDir, { recursive: true });
22776
- fs13.writeFileSync(filepath, content, "utf8");
23048
+ const filepath = path15.join(outputDir, filename);
23049
+ fs14.mkdirSync(outputDir, { recursive: true });
23050
+ fs14.writeFileSync(filepath, content, "utf8");
22777
23051
  console.log(`Generated: ${filepath}`);
22778
23052
  } catch (err) {
22779
23053
  console.error(`Error running custom formatter "${formatName}": ${err instanceof Error ? err.message : String(err)}`);
@@ -22823,13 +23097,13 @@ async function dispatchNotifications(run, args) {
22823
23097
  }
22824
23098
  function runHistoryPipeline(run, args) {
22825
23099
  if (!args.historyFile) return;
22826
- const historyPath = path14.resolve(args.historyFile);
23100
+ const historyPath = path15.resolve(args.historyFile);
22827
23101
  const store = loadHistory(
22828
23102
  { filePath: historyPath },
22829
23103
  {
22830
23104
  readFile: (p) => {
22831
23105
  try {
22832
- return fs13.readFileSync(p, "utf8");
23106
+ return fs14.readFileSync(p, "utf8");
22833
23107
  } catch {
22834
23108
  return void 0;
22835
23109
  }
@@ -22842,11 +23116,11 @@ function runHistoryPipeline(run, args) {
22842
23116
  run,
22843
23117
  maxRuns: args.maxHistoryRuns
22844
23118
  });
22845
- const dir = path14.dirname(historyPath);
22846
- fs13.mkdirSync(dir, { recursive: true });
23119
+ const dir = path15.dirname(historyPath);
23120
+ fs14.mkdirSync(dir, { recursive: true });
22847
23121
  saveHistory(
22848
23122
  { filePath: historyPath, store: updated },
22849
- { writeFile: (p, content) => fs13.writeFileSync(p, content, "utf8") }
23123
+ { writeFile: (p, content) => fs14.writeFileSync(p, content, "utf8") }
22850
23124
  );
22851
23125
  let metricsCount = 0;
22852
23126
  for (const testId of Object.keys(updated.tests)) {
@@ -22923,6 +23197,7 @@ async function generateCompareReports(baseline, current, baselineFile, args) {
22923
23197
  (scenario) => scenario.kind === "added" && scenario.current?.status === "failed"
22924
23198
  ).length,
22925
23199
  summary: result.diff.summary,
23200
+ scenarios: result.diff.scenarios,
22926
23201
  prSummary: args.prSummary || args.prSummaryFile ? createPrCommentSummary(result.diff) : void 0
22927
23202
  };
22928
23203
  }
@@ -22993,11 +23268,11 @@ function writeReviewReport(review, args) {
22993
23268
  const outputDir = args.outputDir ?? "reports";
22994
23269
  const baseName = args.outputName ?? "evidence-review";
22995
23270
  const suffix = args.outputNameTimestamp ? `-${Math.floor(review.run.startedAtMs / 1e3)}` : "";
22996
- fs13.mkdirSync(outputDir, { recursive: true });
22997
- const mdPath = path14.join(outputDir, `${baseName}${suffix}.md`);
22998
- const htmlPath = path14.join(outputDir, `${baseName}${suffix}.html`);
22999
- fs13.writeFileSync(mdPath, markdown, "utf8");
23000
- fs13.writeFileSync(htmlPath, html, "utf8");
23271
+ fs14.mkdirSync(outputDir, { recursive: true });
23272
+ const mdPath = path15.join(outputDir, `${baseName}${suffix}.md`);
23273
+ const htmlPath = path15.join(outputDir, `${baseName}${suffix}.html`);
23274
+ fs14.writeFileSync(mdPath, markdown, "utf8");
23275
+ fs14.writeFileSync(htmlPath, html, "utf8");
23001
23276
  return [mdPath, htmlPath];
23002
23277
  }
23003
23278
  function evaluateReviewGate(review, args) {
@@ -23043,9 +23318,9 @@ function printResult(result, args, startMs, droppedMissingStory = 0) {
23043
23318
  function printCompareResult(result, args, startMs) {
23044
23319
  const durationMs = Date.now() - startMs;
23045
23320
  if (result.prSummary && args.prSummaryFile) {
23046
- const outputPath = path14.resolve(args.prSummaryFile);
23047
- fs13.mkdirSync(path14.dirname(outputPath), { recursive: true });
23048
- fs13.writeFileSync(outputPath, result.prSummary, "utf8");
23321
+ const outputPath = path15.resolve(args.prSummaryFile);
23322
+ fs14.mkdirSync(path15.dirname(outputPath), { recursive: true });
23323
+ fs14.writeFileSync(outputPath, result.prSummary, "utf8");
23049
23324
  }
23050
23325
  if (args.jsonSummary) {
23051
23326
  console.log(
@@ -23073,6 +23348,44 @@ function printCompareResult(result, args, startMs) {
23073
23348
  console.log(result.prSummary);
23074
23349
  }
23075
23350
  }
23351
+ function loadReleasePolicy(policyPath) {
23352
+ const resolved = path15.resolve(policyPath);
23353
+ if (!fs14.existsSync(resolved)) {
23354
+ console.error(`Error: release policy file not found: ${resolved}`);
23355
+ process.exit(EXIT_USAGE);
23356
+ }
23357
+ try {
23358
+ const raw = JSON.parse(fs14.readFileSync(resolved, "utf8"));
23359
+ return {
23360
+ allowedOmissions: Array.isArray(raw.allowedOmissions) ? raw.allowedOmissions : [],
23361
+ allowedRegressions: Array.isArray(raw.allowedRegressions) ? raw.allowedRegressions : [],
23362
+ allowNewScenarios: Boolean(raw.allowNewScenarios)
23363
+ };
23364
+ } catch (err) {
23365
+ const msg = err instanceof Error ? err.message : String(err);
23366
+ console.error(`Error reading release policy: ${msg}`);
23367
+ process.exit(EXIT_USAGE);
23368
+ }
23369
+ }
23370
+ function applyReleasePolicy(result, policy) {
23371
+ const allowedOmissionSet = new Set(policy.allowedOmissions ?? []);
23372
+ const allowedRegressionSet = new Set(policy.allowedRegressions ?? []);
23373
+ const adjustedOmissions = result.scenarios.filter(
23374
+ (scenario) => scenario.kind === "removed" && !allowedOmissionSet.has(scenario.id)
23375
+ ).length;
23376
+ const adjustedRegressions = result.scenarios.filter(
23377
+ (scenario) => scenario.kind === "regressed" && !allowedRegressionSet.has(scenario.id)
23378
+ ).length;
23379
+ const adjustedSummary = {
23380
+ ...result.summary,
23381
+ removed: adjustedOmissions,
23382
+ regressed: adjustedRegressions
23383
+ };
23384
+ return {
23385
+ ...result,
23386
+ summary: adjustedSummary
23387
+ };
23388
+ }
23076
23389
  function evaluateCompareGate(result, args) {
23077
23390
  const failures = [];
23078
23391
  if (args.failOnRegression && result.summary.regressed > 0) {
@@ -23085,6 +23398,16 @@ function evaluateCompareGate(result, args) {
23085
23398
  `new failing scenarios detected (${result.addedFailures}) with --fail-on-added-failures`
23086
23399
  );
23087
23400
  }
23401
+ if (args.failOnRemoval && result.summary.removed > 0) {
23402
+ failures.push(
23403
+ `removed scenarios detected (${result.summary.removed}) with --fail-on-removal`
23404
+ );
23405
+ }
23406
+ if (args.failOnNew && result.summary.added > 0) {
23407
+ failures.push(
23408
+ `new scenarios detected (${result.summary.added}) with --fail-on-new`
23409
+ );
23410
+ }
23088
23411
  if (args.maxRegressions !== void 0 && result.summary.regressed > args.maxRegressions) {
23089
23412
  failures.push(
23090
23413
  `regressions ${result.summary.regressed} exceed --max-regressions ${args.maxRegressions}`
@@ -23136,7 +23459,7 @@ Generate an API token at https://id.atlassian.com/manage-profile/security/api-to
23136
23459
  console.error("Error: missing ADF file argument. Run with --help for usage.");
23137
23460
  process.exit(EXIT_USAGE);
23138
23461
  }
23139
- if (!fs13.existsSync(inputFile)) {
23462
+ if (!fs14.existsSync(inputFile)) {
23140
23463
  console.error(`Error: file not found: ${inputFile}`);
23141
23464
  process.exit(EXIT_USAGE);
23142
23465
  }
@@ -23164,7 +23487,7 @@ Generate an API token at https://id.atlassian.com/manage-profile/security/api-to
23164
23487
  console.error("Error: --title is required when creating a new page");
23165
23488
  process.exit(EXIT_USAGE);
23166
23489
  }
23167
- const adf = fs13.readFileSync(path14.resolve(inputFile), "utf8");
23490
+ const adf = fs14.readFileSync(path15.resolve(inputFile), "utf8");
23168
23491
  if (dryRun) {
23169
23492
  console.log(
23170
23493
  JSON.stringify(
@@ -23243,7 +23566,7 @@ Generate an API token at https://id.atlassian.com/manage-profile/security/api-to
23243
23566
  console.error("Error: missing ADF file argument. Run with --help for usage.");
23244
23567
  process.exit(EXIT_USAGE);
23245
23568
  }
23246
- if (!fs13.existsSync(inputFile)) {
23569
+ if (!fs14.existsSync(inputFile)) {
23247
23570
  console.error(`Error: file not found: ${inputFile}`);
23248
23571
  process.exit(EXIT_USAGE);
23249
23572
  }
@@ -23270,7 +23593,7 @@ Generate an API token at https://id.atlassian.com/manage-profile/security/api-to
23270
23593
  process.exit(EXIT_USAGE);
23271
23594
  }
23272
23595
  const mode = modeRaw;
23273
- const adf = fs13.readFileSync(path14.resolve(inputFile), "utf8");
23596
+ const adf = fs14.readFileSync(path15.resolve(inputFile), "utf8");
23274
23597
  if (dryRun) {
23275
23598
  console.log(
23276
23599
  JSON.stringify(
@@ -23432,7 +23755,7 @@ async function runBuildDocs(rawArgs) {
23432
23755
  if (result.apiPages > 0) {
23433
23756
  console.log(` \u2022 API pages \u2192 src/content/docs/api (${result.apiPages})`);
23434
23757
  }
23435
- const rel = path14.relative(process.cwd(), result.siteDir) || ".";
23758
+ const rel = path15.relative(process.cwd(), result.siteDir) || ".";
23436
23759
  console.log(`
23437
23760
  Preview: cd ${rel} && npm run dev`);
23438
23761
  return EXIT_SUCCESS;
@@ -23447,6 +23770,212 @@ Preview: cd ${rel} && npm run dev`);
23447
23770
  return EXIT_USAGE;
23448
23771
  }
23449
23772
  }
23773
+ async function runDeploy(rawArgs) {
23774
+ const mode = rawArgs[0];
23775
+ if (!mode || !["record", "status", "diff"].includes(mode)) {
23776
+ console.error("Usage: executable-stories deploy <record|status|diff> [options]");
23777
+ console.error(" deploy record <file> --env <env> [--tag <tag>] [--ledger <path>]");
23778
+ console.error(" deploy status [--ledger <path>] [--json]");
23779
+ console.error(" deploy diff <env-a> <env-b> [--ledger <path>] [--json]");
23780
+ return EXIT_USAGE;
23781
+ }
23782
+ const { values, positionals } = parseArgs({
23783
+ args: rawArgs.slice(1),
23784
+ options: {
23785
+ env: { type: "string" },
23786
+ tag: { type: "string" },
23787
+ ledger: { type: "string", default: ".executable-stories/deployments.json" },
23788
+ json: { type: "boolean", default: false },
23789
+ help: { type: "boolean", default: false }
23790
+ },
23791
+ allowPositionals: true,
23792
+ strict: true
23793
+ });
23794
+ if (values.help) {
23795
+ console.log(`executable-stories deploy \u2014 Track deployments across environments.
23796
+
23797
+ USAGE
23798
+ executable-stories deploy record <file> --env <env> [--tag <tag>] [--ledger <path>]
23799
+ executable-stories deploy status [--ledger <path>] [--json]
23800
+ executable-stories deploy diff <env-a> <env-b> [--ledger <path>] [--json]
23801
+
23802
+ OPTIONS
23803
+ --env <env> Environment name (e.g. dev, staging, production)
23804
+ --tag <tag> Optional Git tag for this deployment (e.g. v1.2.3)
23805
+ --ledger <path> Path to deployment ledger JSON (default: .executable-stories/deployments.json)
23806
+ --json Output as JSON instead of text`);
23807
+ return EXIT_SUCCESS;
23808
+ }
23809
+ const ledgerPath = values.ledger;
23810
+ if (mode === "record") {
23811
+ const inputFile = positionals[0];
23812
+ if (!inputFile) {
23813
+ console.error("Error: deploy record requires an input file.");
23814
+ return EXIT_USAGE;
23815
+ }
23816
+ const env = values.env;
23817
+ if (!env) {
23818
+ console.error("Error: deploy record requires --env <environment>.");
23819
+ return EXIT_USAGE;
23820
+ }
23821
+ const text2 = readFileInput(inputFile);
23822
+ const { run } = normalizeRunFromText(text2, {
23823
+ ...createDefaultCliArgs(),
23824
+ inputType: "raw",
23825
+ inputFile
23826
+ });
23827
+ const applied = applySelection(run, createDefaultCliArgs());
23828
+ const result = recordDeployment({
23829
+ run: applied,
23830
+ environment: env,
23831
+ tag: values.tag,
23832
+ ledgerPath,
23833
+ runFilePath: inputFile
23834
+ });
23835
+ console.error(
23836
+ `Recorded deployment to "${result.entry.environment}" at ${result.entry.timestamp}`
23837
+ );
23838
+ console.error(
23839
+ ` Scenarios: ${result.entry.summary.total} (${result.entry.summary.passed} passed, ${result.entry.summary.failed} failed)`
23840
+ );
23841
+ if (result.entry.tag) {
23842
+ console.error(` Tag: ${result.entry.tag}`);
23843
+ }
23844
+ console.error(` Ledger: ${result.ledgerPath}`);
23845
+ return EXIT_SUCCESS;
23846
+ }
23847
+ if (mode === "status") {
23848
+ const status = getDeploymentStatus(ledgerPath);
23849
+ const envs = Object.keys(status.environments);
23850
+ if (envs.length === 0) {
23851
+ console.error("No deployments recorded yet.");
23852
+ return EXIT_SUCCESS;
23853
+ }
23854
+ if (values.json) {
23855
+ console.log(JSON.stringify(status, null, 2));
23856
+ } else {
23857
+ for (const envName of envs) {
23858
+ const env = status.environments[envName];
23859
+ if (!env) continue;
23860
+ const e = env.latest;
23861
+ console.log(`${envName}:`);
23862
+ console.log(` Deployed: ${e.timestamp}`);
23863
+ console.log(` SHA: ${e.sha ?? "unknown"}`);
23864
+ console.log(` Tag: ${e.tag ?? "none"}`);
23865
+ console.log(` Scenarios: ${e.summary.total} (${e.summary.passed} passed, ${e.summary.failed} failed, ${e.summary.skipped} skipped, ${e.summary.pending} pending)`);
23866
+ if (env.previousDeployment) {
23867
+ const prev = env.previousDeployment;
23868
+ const added = e.scenarioIds.filter((id) => !new Set(prev.scenarioIds).has(id)).length;
23869
+ const removed = prev.scenarioIds.filter((id) => !new Set(e.scenarioIds).has(id)).length;
23870
+ if (added > 0 || removed > 0) {
23871
+ console.log(` Drift from previous: +${added} added, -${removed} removed`);
23872
+ }
23873
+ }
23874
+ console.log();
23875
+ }
23876
+ console.log(`Ledger: ${ledgerPath}`);
23877
+ }
23878
+ return EXIT_SUCCESS;
23879
+ }
23880
+ if (mode === "diff") {
23881
+ const envA = positionals[0];
23882
+ const envB = positionals[1];
23883
+ if (!envA || !envB) {
23884
+ console.error("Error: deploy diff requires two environment names.");
23885
+ return EXIT_USAGE;
23886
+ }
23887
+ try {
23888
+ const drift = getEnvironmentDrift(ledgerPath, envA, envB);
23889
+ if (values.json) {
23890
+ console.log(JSON.stringify(drift, null, 2));
23891
+ } else {
23892
+ console.log(`Environment drift: ${envA} \u2194 ${envB}`);
23893
+ console.log(` ${envA}: ${drift.aEntry.summary.total} scenarios (${drift.aEntry.timestamp})`);
23894
+ console.log(` ${envB}: ${drift.bEntry.summary.total} scenarios (${drift.bEntry.timestamp})`);
23895
+ console.log(` In both: ${drift.inBoth.length}`);
23896
+ console.log(` Only in ${envA}: ${drift.onlyInA.length}`);
23897
+ console.log(` Only in ${envB}: ${drift.onlyInB.length}`);
23898
+ console.log(` Status changed: ${drift.statusChanged.length}`);
23899
+ if (drift.onlyInA.length > 0) {
23900
+ console.log(`
23901
+ Only in ${envA}:`);
23902
+ for (const id of drift.onlyInA.slice(0, 20)) {
23903
+ console.log(` - ${id}`);
23904
+ }
23905
+ if (drift.onlyInA.length > 20) {
23906
+ console.log(` ... and ${drift.onlyInA.length - 20} more`);
23907
+ }
23908
+ }
23909
+ if (drift.onlyInB.length > 0) {
23910
+ console.log(`
23911
+ Only in ${envB}:`);
23912
+ for (const id of drift.onlyInB.slice(0, 20)) {
23913
+ console.log(` - ${id}`);
23914
+ }
23915
+ if (drift.onlyInB.length > 20) {
23916
+ console.log(` ... and ${drift.onlyInB.length - 20} more`);
23917
+ }
23918
+ }
23919
+ if (drift.statusChanged.length > 0) {
23920
+ console.log("\n Status changed:");
23921
+ for (const item of drift.statusChanged.slice(0, 20)) {
23922
+ console.log(` - ${item.id}: ${item.statusA} -> ${item.statusB}`);
23923
+ }
23924
+ if (drift.statusChanged.length > 20) {
23925
+ console.log(` ... and ${drift.statusChanged.length - 20} more`);
23926
+ }
23927
+ }
23928
+ }
23929
+ } catch (err) {
23930
+ console.error(`Error: ${err.message}`);
23931
+ return EXIT_USAGE;
23932
+ }
23933
+ return EXIT_SUCCESS;
23934
+ }
23935
+ return EXIT_USAGE;
23936
+ }
23937
+ function createDefaultCliArgs() {
23938
+ return {
23939
+ subcommand: "format",
23940
+ stdin: false,
23941
+ formats: [],
23942
+ inputType: "raw",
23943
+ outputDir: "reports",
23944
+ outputName: "index",
23945
+ outputNameTimestamp: false,
23946
+ sortTestCases: "none",
23947
+ include: [],
23948
+ exclude: [],
23949
+ includeTags: [],
23950
+ excludeTags: [],
23951
+ synthesizeStories: true,
23952
+ htmlTitle: "Test Results",
23953
+ htmlTheme: "default",
23954
+ htmlNoSyntaxHighlighting: false,
23955
+ htmlNoMermaid: false,
23956
+ htmlNoMarkdown: false,
23957
+ htmlNoToc: false,
23958
+ htmlThemePicker: false,
23959
+ jsonSummary: false,
23960
+ listFormat: "text",
23961
+ notify: "never",
23962
+ maxFailedTests: 5,
23963
+ maxHistoryRuns: 10,
23964
+ webhookUrls: [],
23965
+ webhookHeaders: {},
23966
+ webhookMethod: "POST",
23967
+ webhookHmacHeader: "X-Signature",
23968
+ webhookHmacTimestamp: false,
23969
+ assetMode: "none",
23970
+ allowMissingAssets: false,
23971
+ prSummary: false,
23972
+ failOnRegression: false,
23973
+ failOnAddedFailures: false,
23974
+ failOnRemoval: false,
23975
+ failOnNew: false,
23976
+ baselineMode: "explicit"
23977
+ };
23978
+ }
23450
23979
  main().catch((err) => {
23451
23980
  console.error(err);
23452
23981
  process.exit(EXIT_USAGE);