executable-stories-formatters 1.20.0 → 1.21.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/cli.js CHANGED
@@ -1,8 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/cli.ts
4
- import * as fs19 from "fs";
5
- import * as path25 from "path";
4
+ import * as fs16 from "fs";
5
+ import * as path22 from "path";
6
6
  import { parseArgs as parseArgs4 } from "util";
7
7
  import { canonicalizeRun as canonicalizeRun7 } from "executable-stories-core/converters/acl/canonicalize";
8
8
  import {
@@ -127,7 +127,6 @@ Useful commands (all take \`raw-run.json\`):
127
127
  - \`executable-stories check <raw-run.json>\` \u2014 failure summary with non-zero exit (agent backpressure)
128
128
  - \`executable-stories list <raw-run.json>\` \u2014 scenario listing
129
129
  - \`executable-stories compare <baseline> <current>\` \u2014 behavioural diff between runs
130
- - \`executable-stories dev\` \u2014 live docs site (scaffold once with \`init-astro --install\`)
131
130
 
132
131
  <!-- generated by executable-stories; edit or delete freely \u2014 it will not be rewritten -->
133
132
  `;
@@ -626,7 +625,7 @@ function createTestRailProvider(config, auth, deps) {
626
625
  if (response.status === 429 && attempt < MAX_ATTEMPTS - 1) {
627
626
  const retryAfter = Number(response.headers.get("retry-after") ?? "1");
628
627
  deps.logger.warn(`TestRail rate limit hit, retrying in ${retryAfter}s`);
629
- await new Promise((resolve13) => setTimeout(resolve13, Math.max(1, retryAfter) * 1e3));
628
+ await new Promise((resolve12) => setTimeout(resolve12, Math.max(1, retryAfter) * 1e3));
630
629
  continue;
631
630
  }
632
631
  const text2 = await response.text();
@@ -1181,14 +1180,10 @@ var COMPLETION_SUBCOMMANDS = [
1181
1180
  ["triage", "Discovery worklist: failing scenarios, regressions first"],
1182
1181
  ["validate", "Validate a JSON file against the schema"],
1183
1182
  ["doctor", "Diagnose the run JSON: location, schema version, contents"],
1184
- ["dev", "Run the Astro docs site dev server"],
1185
- ["init-astro", "Scaffold a thin Astro docs site"],
1186
- ["new", "Scaffold a docs page from a template"],
1187
1183
  ["check-links", "Scan docs for broken links"],
1188
1184
  ["push", "Send a run to a cloud ingest endpoint (any framework's output)"],
1189
1185
  ["coverage", "Compare stories against a test-management system (read-only)"],
1190
1186
  ["sync", "Push cases, executions, and evidence to TestRail or Xray"],
1191
- ["import-openapi", "Generate API doc pages from an OpenAPI spec"],
1192
1187
  ["publish-confluence", "Publish an ADF JSON file to Confluence"],
1193
1188
  ["publish-jira", "Publish an ADF JSON file to a Jira issue"],
1194
1189
  ["deploy", "Record deployments, show status, detect drift"],
@@ -2009,9 +2004,9 @@ function renderDiffHunk(file, hunk, anchoredStart, anchoredCount) {
2009
2004
  const sign = line.kind === "add" ? "+" : line.kind === "del" ? "-" : " ";
2010
2005
  return `<tr class="${cls}"><td class="diff-ln">${o}</td><td class="diff-ln">${n}</td><td class="diff-sign">${sign}</td><td class="diff-code">${escapeHtml(line.text)}</td></tr>`;
2011
2006
  });
2012
- const path26 = file.newPath ?? file.oldPath ?? "";
2007
+ const path23 = file.newPath ?? file.oldPath ?? "";
2013
2008
  return `<div class="diff-hunk">
2014
- <div class="diff-file-header"><code>${escapeHtml(path26)}</code> <span class="subtle">@@ -${hunk.oldStart} +${hunk.newStart} @@ ${escapeHtml(hunk.header)}</span></div>
2009
+ <div class="diff-file-header"><code>${escapeHtml(path23)}</code> <span class="subtle">@@ -${hunk.oldStart} +${hunk.newStart} @@ ${escapeHtml(hunk.header)}</span></div>
2015
2010
  <table class="diff-table"><tbody>${rows.join("")}</tbody></table>
2016
2011
  </div>`;
2017
2012
  }
@@ -2304,7 +2299,8 @@ function renderClaim(lines, claim) {
2304
2299
  lines.push("");
2305
2300
  lines.push(`- File: \`${claim.sourceFile}:${claim.sourceLine}\``);
2306
2301
  if (claim.changeType !== "unknown") {
2307
- lines.push(`- Change: \`${claim.changeType}\``);
2302
+ const inferred = claim.changeTypeConfidence === void 0 ? "" : ` _(inferred, jev ${claim.changeTypeConfidence.toFixed(2)})_`;
2303
+ lines.push(`- Change: \`${claim.changeType}\`${inferred}`);
2308
2304
  }
2309
2305
  const tickets = claim.testCase.story.tickets ?? [];
2310
2306
  if (tickets.length > 0) {
@@ -2572,6 +2568,7 @@ function toJsonClaim(claim) {
2572
2568
  status: claim.status,
2573
2569
  audience: claim.audience,
2574
2570
  changeType: claim.changeType,
2571
+ ...claim.changeTypeConfidence === void 0 ? {} : { changeTypeConfidence: claim.changeTypeConfidence },
2575
2572
  strength: claim.strength,
2576
2573
  strengthReasons: claim.strengthReasons,
2577
2574
  coversFiles: claim.coversFiles
@@ -2597,6 +2594,50 @@ function buildReviewJson(review) {
2597
2594
  };
2598
2595
  }
2599
2596
 
2597
+ // src/jev.ts
2598
+ var JEV_ENDPOINT = "https://api.typesafe.ai/v1/systemone";
2599
+ var JEV_MODEL = "jev-latest";
2600
+ function createJevClient(options) {
2601
+ const {
2602
+ apiKey,
2603
+ model = JEV_MODEL,
2604
+ endpoint = JEV_ENDPOINT,
2605
+ fetch: fetch2 = globalThis.fetch,
2606
+ timeoutMs = 1e4
2607
+ } = options;
2608
+ return {
2609
+ model,
2610
+ // One attempt per question set; add backoff on 429/529 if a loop hits rate limits.
2611
+ async ask(state, questions) {
2612
+ const response = await fetch2(endpoint, {
2613
+ method: "POST",
2614
+ headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json" },
2615
+ body: JSON.stringify({ model, state, questions }),
2616
+ signal: AbortSignal.timeout(timeoutMs)
2617
+ });
2618
+ if (!response.ok) throw new Error(`Jev ${response.status}: ${(await response.text()).slice(0, 200)}`);
2619
+ const body = await response.json();
2620
+ if (!body.answers) throw new Error("Jev response has no answers");
2621
+ return body.answers;
2622
+ }
2623
+ };
2624
+ }
2625
+ function jevFromEnv(env = process.env) {
2626
+ const apiKey = env.JEV_API_KEY;
2627
+ if (!apiKey) return void 0;
2628
+ return createJevClient({
2629
+ apiKey,
2630
+ ...env.JEV_MODEL ? { model: env.JEV_MODEL } : {},
2631
+ ...env.JEV_ENDPOINT ? { endpoint: env.JEV_ENDPOINT } : {}
2632
+ });
2633
+ }
2634
+ function asChoice(answer) {
2635
+ return answer?.type === "choice" ? answer : void 0;
2636
+ }
2637
+ function asNoul(answer) {
2638
+ return answer?.type === "noul" ? answer.noul : void 0;
2639
+ }
2640
+
2600
2641
  // src/goal.ts
2601
2642
  var ACTIVE = ["passed", "failed"];
2602
2643
  function buildGoal(args, _deps = {}) {
@@ -2642,9 +2683,41 @@ function buildGoal(args, _deps = {}) {
2642
2683
  requirements,
2643
2684
  regressions,
2644
2685
  regressionsEnforced: Boolean(baseline && args.enforceNoRegressions),
2645
- ratchet: { enforced: Boolean(baseline && args.enforceRatchet), violations }
2686
+ ratchet: { enforced: Boolean(baseline && args.enforceRatchet), violations, advisories: [] }
2646
2687
  };
2647
2688
  }
2689
+ var GOAL_WEAKENED_MIN_PROBABILITY = 0.75;
2690
+ var stepText = (tc) => tc.story.steps.map((s) => `${s.keyword} ${s.text}`);
2691
+ async function enrichGoal(report, args, jev) {
2692
+ const { baseline } = args;
2693
+ if (!baseline || !report.ratchet.enforced) return report;
2694
+ const current = new Map(args.run.testCases.map((tc) => [tc.id, tc]));
2695
+ const flagged = new Set(report.ratchet.violations.map((v) => v.id));
2696
+ const rewritten = baseline.testCases.flatMap((base) => {
2697
+ const now = current.get(base.id);
2698
+ if (!now || flagged.has(base.id)) return [];
2699
+ const before = stepText(base);
2700
+ const after = stepText(now);
2701
+ return before.join("\n") === after.join("\n") ? [] : [{ base, before, after }];
2702
+ });
2703
+ const advisories = (await Promise.all(
2704
+ rewritten.map(async ({ base, before, after }) => {
2705
+ const answers = await jev.ask(
2706
+ { baseline: before, now: after },
2707
+ {
2708
+ weakened: {
2709
+ type: "noul",
2710
+ instructions: "Does the NOW scenario check less than the BASELINE scenario: fewer or weaker assertions, looser expectations, or a removed check?"
2711
+ }
2712
+ }
2713
+ );
2714
+ const p = asNoul(answers.weakened);
2715
+ if (p === void 0 || p < GOAL_WEAKENED_MIN_PROBABILITY) return [];
2716
+ return [{ id: base.id, title: base.story.scenario, kind: "weakened", detail: `jev ${p.toFixed(2)}: steps rewritten, checks less` }];
2717
+ })
2718
+ )).flat();
2719
+ return { ...report, ratchet: { ...report.ratchet, advisories } };
2720
+ }
2648
2721
  function evaluate(selector, matched) {
2649
2722
  const passed = matched.filter((tc) => tc.status === "passed").length;
2650
2723
  const failing = matched.filter((tc) => tc.status !== "passed").map((tc) => tc.story.scenario);
@@ -2686,6 +2759,9 @@ function renderGoal(report, format) {
2686
2759
  lines.push(` ${v.kind}: ${v.title} (${v.detail})`);
2687
2760
  }
2688
2761
  }
2762
+ for (const v of report.ratchet.advisories) {
2763
+ lines.push(` advisory ${v.kind}: ${v.title} (${v.detail})`);
2764
+ }
2689
2765
  }
2690
2766
  return lines.join("\n");
2691
2767
  }
@@ -2768,193 +2844,8 @@ function updateHistory(args) {
2768
2844
  };
2769
2845
  }
2770
2846
 
2771
- // src/import-openapi.ts
2772
- import * as fs6 from "fs";
2773
- import * as path7 from "path";
2774
- import { parse as parseYamlString } from "yaml";
2775
- var HTTP_METHODS = ["get", "post", "put", "patch", "delete", "options", "head"];
2776
- function parseYaml2(raw, specPath) {
2777
- try {
2778
- return parseYamlString(raw);
2779
- } catch (err) {
2780
- throw new Error(`Could not parse YAML spec ${specPath}: ${err.message}`, {
2781
- cause: err
2782
- });
2783
- }
2784
- }
2785
- function parseSpec(specPath) {
2786
- if (!fs6.existsSync(specPath)) throw new Error(`Spec not found: ${specPath}`);
2787
- const raw = fs6.readFileSync(specPath, "utf8");
2788
- const ext = path7.extname(specPath).toLowerCase();
2789
- if (ext === ".json") return JSON.parse(raw);
2790
- if (ext === ".yaml" || ext === ".yml") return parseYaml2(raw, specPath);
2791
- try {
2792
- return JSON.parse(raw);
2793
- } catch {
2794
- return parseYaml2(raw, specPath);
2795
- }
2796
- }
2797
- function extractEndpoints(spec) {
2798
- const paths = spec.paths ?? {};
2799
- const endpoints = [];
2800
- for (const [route, item] of Object.entries(paths)) {
2801
- if (!item || typeof item !== "object") continue;
2802
- for (const method of HTTP_METHODS) {
2803
- const op = item[method];
2804
- if (!op || typeof op !== "object") continue;
2805
- const tags = Array.isArray(op.tags) && op.tags.length > 0 ? op.tags : ["API"];
2806
- endpoints.push({
2807
- method: method.toUpperCase(),
2808
- path: route,
2809
- operationId: typeof op.operationId === "string" ? op.operationId : void 0,
2810
- summary: typeof op.summary === "string" && op.summary || typeof op.description === "string" && op.description || "",
2811
- tag: String(tags[0])
2812
- });
2813
- }
2814
- }
2815
- return endpoints;
2816
- }
2817
- function loadScenarios(runFile) {
2818
- if (!runFile) return [];
2819
- if (!fs6.existsSync(runFile)) throw new Error(`Run file not found: ${runFile}`);
2820
- const report = JSON.parse(fs6.readFileSync(runFile, "utf8"));
2821
- return (report.features ?? []).flatMap((f) => f.scenarios ?? []);
2822
- }
2823
- function endpointRefs(endpoint) {
2824
- const refs = [
2825
- endpoint.operationId,
2826
- `${endpoint.method} ${endpoint.path}`,
2827
- endpoint.path
2828
- ].filter((r) => Boolean(r));
2829
- return refs;
2830
- }
2831
- function scenarioMatchesEndpoint(scenario, refs) {
2832
- const tags = scenario.tags ?? [];
2833
- return refs.some(
2834
- (ref) => scenario.id === ref || scenario.title === ref || tags.includes(ref)
2835
- );
2836
- }
2837
- function computeCoverage(endpoints, scenarios) {
2838
- return endpoints.map((endpoint) => {
2839
- const refs = endpointRefs(endpoint);
2840
- const matched = scenarios.filter((s) => scenarioMatchesEndpoint(s, refs));
2841
- let status;
2842
- if (matched.length === 0) status = "uncovered";
2843
- else if (matched.some((s) => s.status === "failed")) status = "failing";
2844
- else status = "covered";
2845
- return {
2846
- endpoint,
2847
- status,
2848
- stories: matched.map((s) => ({
2849
- id: s.id ?? s.title ?? "",
2850
- title: s.title ?? s.id ?? "story",
2851
- status: s.status ?? "passed"
2852
- }))
2853
- };
2854
- });
2855
- }
2856
- function slug(input) {
2857
- return input.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "api";
2858
- }
2859
- function yamlQuote(value) {
2860
- return value.replace(/'/g, "''");
2861
- }
2862
- function coverageSummary(rows) {
2863
- return {
2864
- total: rows.length,
2865
- covered: rows.filter((r) => r.status === "covered").length,
2866
- failing: rows.filter((r) => r.status === "failing").length,
2867
- uncovered: rows.filter((r) => r.status === "uncovered").length
2868
- };
2869
- }
2870
- function renderTagPage(tag, rows, hasRun) {
2871
- const endpoints = rows.map((r) => ({
2872
- method: r.endpoint.method,
2873
- path: r.endpoint.path,
2874
- summary: r.endpoint.summary || "",
2875
- status: r.status,
2876
- stories: r.stories
2877
- }));
2878
- const summary = coverageSummary(rows);
2879
- return `---
2880
- title: 'API \u2014 ${yamlQuote(tag)}'
2881
- description: 'Endpoints for ${yamlQuote(tag)}, linked to the stories that exercise them.'
2882
- ---
2883
-
2884
- import ApiOperations from 'executable-stories-astro/components/ApiOperations.astro';
2885
-
2886
- <ApiOperations
2887
- tag={${JSON.stringify(tag)}}
2888
- hasRun={${hasRun}}
2889
- summary={${JSON.stringify(summary)}}
2890
- endpoints={${JSON.stringify(endpoints)}}
2891
- />
2892
- `;
2893
- }
2894
- function renderIndex(groups, hasRun, totals2) {
2895
- const rows = [...groups.entries()].map(([tag, eps]) => {
2896
- const covered = eps.filter((e) => e.status === "covered").length;
2897
- const cov = hasRun ? ` | ${covered}/${eps.length} covered` : "";
2898
- return `- [${tag}](./${slug(tag)}/) \u2014 ${eps.length} endpoint(s)${cov}`;
2899
- }).join("\n");
2900
- const coverageNote = hasRun ? `
2901
- **${totals2.coveredCount} of ${totals2.endpointCount} endpoints** are covered by a passing story.` + (totals2.uncoveredCount > 0 ? ` \u26A0 ${totals2.uncoveredCount} endpoint(s) have no verifying test.` : "") : "\nRe-run with `--run <story-report.json>` to show per-endpoint test coverage.";
2902
- return `---
2903
- title: 'API reference'
2904
- description: 'API endpoints generated from OpenAPI, linked to verifying stories.'
2905
- ---
2906
-
2907
- ${coverageNote}
2908
-
2909
- ${rows}
2910
- `;
2911
- }
2912
- async function importOpenApi(options) {
2913
- const spec = parseSpec(options.specPath);
2914
- const endpoints = extractEndpoints(spec);
2915
- if (endpoints.length === 0) {
2916
- throw new Error(`No endpoints found in ${options.specPath} (expected an OpenAPI "paths" object).`);
2917
- }
2918
- const scenarios = loadScenarios(options.runFile);
2919
- const hasRun = Boolean(options.runFile);
2920
- const coverage = computeCoverage(endpoints, scenarios);
2921
- const groups = /* @__PURE__ */ new Map();
2922
- for (const item of coverage) {
2923
- const list = groups.get(item.endpoint.tag) ?? [];
2924
- list.push(item);
2925
- groups.set(item.endpoint.tag, list);
2926
- }
2927
- const outputDir = options.outputDir ?? path7.join("src", "content", "docs", "api");
2928
- if (fs6.existsSync(outputDir) && !options.force) {
2929
- const entries = fs6.readdirSync(outputDir);
2930
- if (entries.length > 0) {
2931
- throw new Error(`Output directory "${outputDir}" is not empty. Use --force to overwrite.`);
2932
- }
2933
- }
2934
- fs6.mkdirSync(outputDir, { recursive: true });
2935
- const coveredCount = coverage.filter((c) => c.status === "covered").length;
2936
- const uncoveredCount = coverage.filter((c) => c.status === "uncovered").length;
2937
- fs6.writeFileSync(
2938
- path7.join(outputDir, "index.mdx"),
2939
- renderIndex(groups, hasRun, { endpointCount: endpoints.length, coveredCount, uncoveredCount }),
2940
- "utf8"
2941
- );
2942
- for (const [tag, rows] of groups) {
2943
- const dir = path7.join(outputDir, slug(tag));
2944
- fs6.mkdirSync(dir, { recursive: true });
2945
- fs6.writeFileSync(path7.join(dir, "index.mdx"), renderTagPage(tag, rows, hasRun), "utf8");
2946
- }
2947
- return {
2948
- outputDir,
2949
- pageCount: groups.size + 1,
2950
- endpointCount: endpoints.length,
2951
- coveredCount,
2952
- uncoveredCount
2953
- };
2954
- }
2955
-
2956
2847
  // src/index.ts
2957
- import * as path17 from "path";
2848
+ import * as path16 from "path";
2958
2849
  import * as fsPromises3 from "fs/promises";
2959
2850
  import { canonicalizeRun as canonicalizeRun2 } from "executable-stories-core/converters/acl/canonicalize";
2960
2851
 
@@ -3979,8 +3870,8 @@ import { assertNever } from "executable-stories-core/utils/assert-never";
3979
3870
  import { behaviourFingerprint as behaviourFingerprint2, behaviourSimilarity as behaviourSimilarity2 } from "executable-stories-core/converters/acl/ids";
3980
3871
 
3981
3872
  // src/sync/lockfile.ts
3982
- import * as fs7 from "fs";
3983
- import * as path8 from "path";
3873
+ import * as fs6 from "fs";
3874
+ import * as path7 from "path";
3984
3875
  import { createHash } from "crypto";
3985
3876
  var DEFAULT_LOCKFILE_PATH = ".executable-stories/sync.lock.json";
3986
3877
  var LOCKFILE_VERSION = 1;
@@ -5562,14 +5453,14 @@ function selectTestCases(args, deps) {
5562
5453
  }
5563
5454
 
5564
5455
  // src/watch.ts
5565
- import * as fs12 from "fs";
5566
- import * as path15 from "path";
5456
+ import * as fs11 from "fs";
5457
+ import * as path14 from "path";
5567
5458
  import { canonicalizeRun } from "executable-stories-core/converters/acl/canonicalize";
5568
5459
  import { synthesizeStories } from "executable-stories-core/converters/synthesize";
5569
5460
 
5570
5461
  // src/report-generator.ts
5571
- import * as path14 from "path";
5572
- import * as fs11 from "fs";
5462
+ import * as path13 from "path";
5463
+ import * as fs10 from "fs";
5573
5464
  import * as fsPromises2 from "fs/promises";
5574
5465
  import { toStoryReportWithIndex } from "executable-stories-core/converters/story-report";
5575
5466
 
@@ -6599,9 +6490,9 @@ var MAX_FUZZ = 2;
6599
6490
  var normalize = (line) => line.trim();
6600
6491
  var HUNK_HEADER = /^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@ ?(.*)$/;
6601
6492
  function stripPathPrefix(raw) {
6602
- const path26 = raw.split(" ")[0].trim();
6603
- if (path26 === "/dev/null") return void 0;
6604
- return path26.replace(/^[ab]\//, "");
6493
+ const path23 = raw.split(" ")[0].trim();
6494
+ if (path23 === "/dev/null") return void 0;
6495
+ return path23.replace(/^[ab]\//, "");
6605
6496
  }
6606
6497
  function parseUnifiedDiff(patch) {
6607
6498
  const files = [];
@@ -6681,7 +6572,7 @@ function createAnchor(args) {
6681
6572
  };
6682
6573
  }
6683
6574
  function changedRunCandidates(anchor, file, fileIndex) {
6684
- const path26 = file.newPath ?? file.oldPath ?? "";
6575
+ const path23 = file.newPath ?? file.oldPath ?? "";
6685
6576
  const out = [];
6686
6577
  file.hunks.forEach((hunk, hunkIndex) => {
6687
6578
  outer: for (let i = 0; i + anchor.changed.length <= hunk.lines.length; i++) {
@@ -6692,7 +6583,7 @@ function changedRunCandidates(anchor, file, fileIndex) {
6692
6583
  continue outer;
6693
6584
  }
6694
6585
  }
6695
- out.push({ fileIndex, file: path26, hunkIndex, lineIndex: i, lines: hunk.lines });
6586
+ out.push({ fileIndex, file: path23, hunkIndex, lineIndex: i, lines: hunk.lines });
6696
6587
  }
6697
6588
  });
6698
6589
  return out;
@@ -6789,18 +6680,18 @@ function deriveChangeType(tags) {
6789
6680
  }
6790
6681
  return "unknown";
6791
6682
  }
6792
- function extensionOf(path26) {
6793
- const base = path26.split("/").pop() ?? path26;
6683
+ function extensionOf(path23) {
6684
+ const base = path23.split("/").pop() ?? path23;
6794
6685
  const dot = base.lastIndexOf(".");
6795
6686
  return dot === -1 ? "" : base.slice(dot + 1).toLowerCase();
6796
6687
  }
6797
- function isTestFile(path26) {
6798
- return TEST_INFIX.test(path26);
6688
+ function isTestFile(path23) {
6689
+ return TEST_INFIX.test(path23);
6799
6690
  }
6800
- function isReviewableSource(path26) {
6801
- if (isTestFile(path26)) return false;
6802
- if (path26.endsWith(".d.ts")) return false;
6803
- return CODE_EXTENSIONS.has(extensionOf(path26));
6691
+ function isReviewableSource(path23) {
6692
+ if (isTestFile(path23)) return false;
6693
+ if (path23.endsWith(".d.ts")) return false;
6694
+ return CODE_EXTENSIONS.has(extensionOf(path23));
6804
6695
  }
6805
6696
  function testBaseKey(testFile) {
6806
6697
  return testFile.replace(TEST_INFIX, "");
@@ -6910,7 +6801,7 @@ function toClaim(testCase, changedSourcePaths) {
6910
6801
  const { strength, reasons } = gradeEvidence(testCase, audience);
6911
6802
  const key = testBaseKey(testCase.sourceFile);
6912
6803
  const coversFiles = changedSourcePaths.filter(
6913
- (path26) => sourceBaseKey(path26) === key
6804
+ (path23) => sourceBaseKey(path23) === key
6914
6805
  );
6915
6806
  return {
6916
6807
  id: testCase.id,
@@ -7011,6 +6902,61 @@ function buildReview(run, context = { changedFiles: [] }) {
7011
6902
  codeDiffs: (context.codeDiffs ?? []).map((d) => buildCodeDiff(d, run, context))
7012
6903
  };
7013
6904
  }
6905
+ var REVIEW_CHANGE_TYPE_MIN_CONFIDENCE = 0.6;
6906
+ var PATCH_EXCERPT_CHARS = 4e3;
6907
+ var CHANGE_TYPE_CRITERIA = {
6908
+ feature: "new user-visible behaviour or capability",
6909
+ bugfix: "corrects behaviour that was wrong",
6910
+ refactor: "restructures code without changing behaviour",
6911
+ perf: "same behaviour, faster or cheaper",
6912
+ deps: "dependency, toolchain, or lockfile change"
6913
+ };
6914
+ async function enrichReview(review, jev) {
6915
+ const untagged = review.claims.filter((c) => c.changeType === "unknown" && c.coversFiles.length > 0);
6916
+ if (untagged.length === 0) return review;
6917
+ const hunksByPath = /* @__PURE__ */ new Map();
6918
+ for (const group of review.context.codeDiffs ?? []) {
6919
+ for (const file of parseUnifiedDiff(group.patch)) {
6920
+ const path23 = file.newPath ?? file.oldPath;
6921
+ if (!path23) continue;
6922
+ const text2 = file.hunks.flatMap((h) => h.lines.map((l) => (l.kind === "add" ? "+" : l.kind === "del" ? "-" : " ") + l.text));
6923
+ hunksByPath.set(path23, [...hunksByPath.get(path23) ?? [], ...text2]);
6924
+ }
6925
+ }
6926
+ const inferred = /* @__PURE__ */ new Map();
6927
+ await Promise.all(
6928
+ untagged.map(async (claim) => {
6929
+ const patch = claim.coversFiles.flatMap((f) => hunksByPath.get(f) ?? []).join("\n").slice(0, PATCH_EXCERPT_CHARS);
6930
+ const answers = await jev.ask(
6931
+ {
6932
+ scenario: claim.scenario,
6933
+ steps: claim.testCase.story.steps.map((s) => `${s.keyword} ${s.text}`),
6934
+ ...claim.intent ? { intent: claim.intent } : {},
6935
+ changedFiles: claim.coversFiles,
6936
+ ...patch ? { patch } : {}
6937
+ },
6938
+ {
6939
+ changeType: {
6940
+ type: "choice",
6941
+ instructions: "What kind of change does this scenario prove?",
6942
+ criteria: CHANGE_TYPE_CRITERIA
6943
+ }
6944
+ }
6945
+ );
6946
+ const answer = asChoice(answers.changeType);
6947
+ if (!answer || answer.confidence < REVIEW_CHANGE_TYPE_MIN_CONFIDENCE) return;
6948
+ const changeType = answer.choice;
6949
+ if (VALID_CHANGE_TYPES.has(changeType)) inferred.set(claim.id, { changeType, confidence: answer.confidence });
6950
+ })
6951
+ );
6952
+ return {
6953
+ ...review,
6954
+ claims: review.claims.map((claim) => {
6955
+ const hit = inferred.get(claim.id);
6956
+ return hit ? { ...claim, changeType: hit.changeType, changeTypeConfidence: hit.confidence } : claim;
6957
+ })
6958
+ };
6959
+ }
7014
6960
  function codeDiffDiagnostics(review) {
7015
6961
  const issues = [];
7016
6962
  for (const evidence of review.codeDiffs) {
@@ -7082,14 +7028,14 @@ var TraceabilityMatrixFormatter = class {
7082
7028
  lines.push("");
7083
7029
  lines.push(`Status: ${renderRequirementStatus(req.status)}`);
7084
7030
  if (req.covers.length > 0) {
7085
- lines.push(`Covers: ${req.covers.map((path26) => `\`${path26}\``).join(", ")}`);
7031
+ lines.push(`Covers: ${req.covers.map((path23) => `\`${path23}\``).join(", ")}`);
7086
7032
  }
7087
7033
  lines.push("");
7088
7034
  lines.push("| Status | Scenario | Source | Covers |");
7089
7035
  lines.push("| --- | --- | --- | --- |");
7090
7036
  for (const scenario of req.scenarios) {
7091
7037
  const source = `${scenario.sourceFile}:${scenario.sourceLine}`;
7092
- const covers = scenario.covers.length > 0 ? scenario.covers.map((path26) => `\`${path26}\``).join(", ") : "";
7038
+ const covers = scenario.covers.length > 0 ? scenario.covers.map((path23) => `\`${path23}\``).join(", ") : "";
7093
7039
  lines.push(`| ${scenario.status} | ${escapePipe2(scenario.title)} | \`${source}\` | ${covers} |`);
7094
7040
  }
7095
7041
  lines.push("");
@@ -7863,8 +7809,8 @@ function extractDocAttachments(step) {
7863
7809
  }
7864
7810
  return attachments;
7865
7811
  }
7866
- function guessMediaType(path26) {
7867
- const lower = path26.toLowerCase();
7812
+ function guessMediaType(path23) {
7813
+ const lower = path23.toLowerCase();
7868
7814
  if (lower.endsWith(".png")) return "image/png";
7869
7815
  if (lower.endsWith(".jpg") || lower.endsWith(".jpeg")) return "image/jpeg";
7870
7816
  if (lower.endsWith(".gif")) return "image/gif";
@@ -8005,11 +7951,11 @@ var CucumberHtmlFormatter = class {
8005
7951
  for (const envelope of envelopes) {
8006
7952
  const accepted = htmlStream.write(envelope);
8007
7953
  if (!accepted) {
8008
- await new Promise((resolve13) => htmlStream.once("drain", resolve13));
7954
+ await new Promise((resolve12) => htmlStream.once("drain", resolve12));
8009
7955
  }
8010
7956
  }
8011
- await new Promise((resolve13, reject) => {
8012
- collector.on("finish", resolve13);
7957
+ await new Promise((resolve12, reject) => {
7958
+ collector.on("finish", resolve12);
8013
7959
  collector.on("error", reject);
8014
7960
  htmlStream.end();
8015
7961
  });
@@ -8018,7 +7964,7 @@ var CucumberHtmlFormatter = class {
8018
7964
  };
8019
7965
 
8020
7966
  // src/colocated-index.ts
8021
- import path9 from "path";
7967
+ import path8 from "path";
8022
7968
  import { REPORT_FAVICON_LINK as REPORT_FAVICON_LINK3 } from "executable-stories-core/utils/report-favicon";
8023
7969
  function escapeHtml3(value) {
8024
7970
  return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
@@ -8039,7 +7985,7 @@ function buildIndexEntries(run, reportsBySourceFile, indexDir) {
8039
7985
  const key = bucket(tc.status);
8040
7986
  if (key) counts[key] += 1;
8041
7987
  }
8042
- const href = path9.relative(indexDir, reportPath).split(path9.sep).join("/");
7988
+ const href = path8.relative(indexDir, reportPath).split(path8.sep).join("/");
8043
7989
  entries.push({ href, sourceFile, counts });
8044
7990
  }
8045
7991
  return entries.sort((a, b) => {
@@ -8113,7 +8059,7 @@ ${REPORT_FAVICON_LINK3}
8113
8059
  }
8114
8060
 
8115
8061
  // src/file-reports.ts
8116
- import path10 from "path";
8062
+ import path9 from "path";
8117
8063
  import * as fsPromises from "fs/promises";
8118
8064
  var BY_FILE_DIR = "by-file";
8119
8065
  function reportSourceFile(report) {
@@ -8132,14 +8078,14 @@ function toPosix(p) {
8132
8078
  return p.replace(/\\/g, "/");
8133
8079
  }
8134
8080
  function byFileDirFor(outputDir) {
8135
- return path10.posix.join(outputDir.replace(/\\/g, "/"), BY_FILE_DIR);
8081
+ return path9.posix.join(outputDir.replace(/\\/g, "/"), BY_FILE_DIR);
8136
8082
  }
8137
8083
  function readFileReports(dir, deps) {
8138
8084
  const names = deps.listDir(dir);
8139
8085
  if (!names) return [];
8140
8086
  const reports = [];
8141
8087
  for (const name of names.filter((n) => n.endsWith(".json")).sort()) {
8142
- const filePath = path10.posix.join(dir, name);
8088
+ const filePath = path9.posix.join(dir, name);
8143
8089
  try {
8144
8090
  const parsed = JSON.parse(deps.readFile(filePath));
8145
8091
  if (Array.isArray(parsed?.testCases) && reportSourceFile(parsed)) {
@@ -8204,7 +8150,7 @@ async function updateFileReports(args, deps) {
8204
8150
  deps
8205
8151
  );
8206
8152
  await deps.writeFile(
8207
- path10.posix.join(dir, reportFileNameFor(sourceFile)),
8153
+ path9.posix.join(dir, reportFileNameFor(sourceFile)),
8208
8154
  `${JSON.stringify(report, null, 2)}
8209
8155
  `
8210
8156
  );
@@ -8233,7 +8179,7 @@ async function removeReportsFor(args, deps) {
8233
8179
  async function removeOtherReportsFor(args, deps) {
8234
8180
  for (const name of deps.listDir(args.dir) ?? []) {
8235
8181
  if (!name.endsWith(".json") || name === args.keep) continue;
8236
- const filePath = path10.posix.join(args.dir, name);
8182
+ const filePath = path9.posix.join(args.dir, name);
8237
8183
  try {
8238
8184
  const other = JSON.parse(deps.readFile(filePath));
8239
8185
  if (reportSourceFile(other) !== args.sourceFile) continue;
@@ -8270,14 +8216,14 @@ function warnAboutUnreported(args, deps) {
8270
8216
  );
8271
8217
  }
8272
8218
  function prunable(args, deps) {
8273
- const resolve13 = (sourceFile) => path10.resolve(args.projectRoot, sourceFile);
8219
+ const resolve12 = (sourceFile) => path9.resolve(args.projectRoot, sourceFile);
8274
8220
  const canResolveSources = args.ran.some(
8275
- (sourceFile) => deps.fileExists(resolve13(sourceFile))
8221
+ (sourceFile) => deps.fileExists(resolve12(sourceFile))
8276
8222
  );
8277
8223
  if (!canResolveSources) return [];
8278
8224
  const ran = new Set(args.ran);
8279
8225
  return args.stored.filter(
8280
- (sourceFile) => !ran.has(sourceFile) && !deps.fileExists(resolve13(sourceFile))
8226
+ (sourceFile) => !ran.has(sourceFile) && !deps.fileExists(resolve12(sourceFile))
8281
8227
  );
8282
8228
  }
8283
8229
 
@@ -8797,25 +8743,25 @@ function groupBy3(items, keyFn) {
8797
8743
  }
8798
8744
 
8799
8745
  // src/formatters/astro-assets.ts
8800
- import * as fs9 from "fs";
8801
- import * as path12 from "path";
8802
-
8803
- // src/bundler/copy-asset.ts
8804
8746
  import * as fs8 from "fs";
8805
8747
  import * as path11 from "path";
8748
+
8749
+ // src/bundler/copy-asset.ts
8750
+ import * as fs7 from "fs";
8751
+ import * as path10 from "path";
8806
8752
  import * as crypto from "crypto";
8807
8753
  function copyAsset(sourcePath, assetsDir) {
8808
- if (!fs8.existsSync(assetsDir)) {
8809
- fs8.mkdirSync(assetsDir, { recursive: true });
8754
+ if (!fs7.existsSync(assetsDir)) {
8755
+ fs7.mkdirSync(assetsDir, { recursive: true });
8810
8756
  }
8811
- const content = fs8.readFileSync(sourcePath);
8757
+ const content = fs7.readFileSync(sourcePath);
8812
8758
  const hash = crypto.createHash("sha256").update(content).digest("hex").slice(0, 8);
8813
- const ext = path11.extname(sourcePath);
8814
- const baseName = sanitize(path11.basename(sourcePath, ext));
8759
+ const ext = path10.extname(sourcePath);
8760
+ const baseName = sanitize(path10.basename(sourcePath, ext));
8815
8761
  const destName = `${baseName}-${hash}${ext}`;
8816
- const destPath = path11.join(assetsDir, destName);
8817
- if (!fs8.existsSync(destPath)) {
8818
- fs8.copyFileSync(sourcePath, destPath);
8762
+ const destPath = path10.join(assetsDir, destName);
8763
+ if (!fs7.existsSync(destPath)) {
8764
+ fs7.copyFileSync(sourcePath, destPath);
8819
8765
  }
8820
8766
  return `assets/${destName}`;
8821
8767
  }
@@ -8831,7 +8777,7 @@ function isRemoteRef(src) {
8831
8777
  }
8832
8778
  function isAbsoluteRef(src) {
8833
8779
  const trimmed = src.trim();
8834
- return path12.posix.isAbsolute(trimmed) || path12.win32.isAbsolute(trimmed);
8780
+ return path11.posix.isAbsolute(trimmed) || path11.win32.isAbsolute(trimmed);
8835
8781
  }
8836
8782
  function isRelativeLocalPath(src) {
8837
8783
  return !isRemoteRef(src) && !isAbsoluteRef(src);
@@ -8921,8 +8867,8 @@ function copyMarkdownAssets(options) {
8921
8867
  const pathMap = /* @__PURE__ */ new Map();
8922
8868
  const missing = [];
8923
8869
  for (const ref of refs) {
8924
- const absPath = isAbsoluteRef(ref) ? ref : path12.resolve(markdownDir, ref);
8925
- if (!fs9.existsSync(absPath)) {
8870
+ const absPath = isAbsoluteRef(ref) ? ref : path11.resolve(markdownDir, ref);
8871
+ if (!fs8.existsSync(absPath)) {
8926
8872
  if (isAbsoluteRef(ref)) continue;
8927
8873
  if (!allowMissing) {
8928
8874
  throw new Error(`Asset not found: ${absPath}`);
@@ -8944,8 +8890,8 @@ function copyMarkdownAssets(options) {
8944
8890
  }
8945
8891
 
8946
8892
  // src/bundler/bundle-assets.ts
8947
- import * as fs10 from "fs";
8948
- import * as path13 from "path";
8893
+ import * as fs9 from "fs";
8894
+ import * as path12 from "path";
8949
8895
 
8950
8896
  // src/bundler/scan-html-assets.ts
8951
8897
  function scanHtmlAssets(html) {
@@ -8977,15 +8923,15 @@ function isLocalAssetRef(ref) {
8977
8923
 
8978
8924
  // src/bundler/bundle-assets.ts
8979
8925
  function bundleAssets(htmlPath, options = {}) {
8980
- const htmlDir = path13.dirname(htmlPath);
8981
- const assetsDir = path13.join(htmlDir, "assets");
8982
- let html = fs10.readFileSync(htmlPath, "utf8");
8926
+ const htmlDir = path12.dirname(htmlPath);
8927
+ const assetsDir = path12.join(htmlDir, "assets");
8928
+ let html = fs9.readFileSync(htmlPath, "utf8");
8983
8929
  const refs = scanHtmlAssets(html);
8984
8930
  let copiedCount = 0;
8985
8931
  const missing = [];
8986
8932
  for (const ref of refs) {
8987
- const absolutePath = path13.resolve(htmlDir, ref);
8988
- if (!fs10.existsSync(absolutePath)) {
8933
+ const absolutePath = path12.resolve(htmlDir, ref);
8934
+ if (!fs9.existsSync(absolutePath)) {
8989
8935
  missing.push(ref);
8990
8936
  continue;
8991
8937
  }
@@ -8999,7 +8945,7 @@ function bundleAssets(htmlPath, options = {}) {
8999
8945
  `Missing asset${missing.length > 1 ? "s" : ""}: ${missing.join(", ")}`
9000
8946
  );
9001
8947
  }
9002
- fs10.writeFileSync(htmlPath, html, "utf8");
8948
+ fs9.writeFileSync(htmlPath, html, "utf8");
9003
8949
  return {
9004
8950
  copiedCount,
9005
8951
  missingCount: missing.length,
@@ -9098,11 +9044,11 @@ function computeOutputPath(sourceFile, format, mode, colocatedStyle, baseOutputD
9098
9044
  const ext = FORMAT_EXTENSIONS[format];
9099
9045
  const effectiveName = outputName + (outputNameSuffix ?? "");
9100
9046
  if (mode === "aggregated") {
9101
- return toPosix2(path14.join(baseOutputDir, joinNameAndExt(effectiveName, ext)));
9047
+ return toPosix2(path13.join(baseOutputDir, joinNameAndExt(effectiveName, ext)));
9102
9048
  }
9103
9049
  const normalizedSource = toPosix2(sourceFile);
9104
- const dirOfSource = path14.posix.dirname(normalizedSource);
9105
- let baseName = path14.posix.basename(normalizedSource);
9050
+ const dirOfSource = path13.posix.dirname(normalizedSource);
9051
+ let baseName = path13.posix.basename(normalizedSource);
9106
9052
  for (const testExt of TEST_EXTENSIONS) {
9107
9053
  if (baseName.endsWith(testExt)) {
9108
9054
  baseName = baseName.slice(0, -testExt.length);
@@ -9111,12 +9057,12 @@ function computeOutputPath(sourceFile, format, mode, colocatedStyle, baseOutputD
9111
9057
  }
9112
9058
  const fileName = `${baseName}.${effectiveName}${ext}`;
9113
9059
  if (colocatedStyle === "adjacent") {
9114
- return toPosix2(path14.posix.join(dirOfSource, fileName));
9060
+ return toPosix2(path13.posix.join(dirOfSource, fileName));
9115
9061
  }
9116
9062
  if (colocatedStyle === "flat") {
9117
- return toPosix2(path14.posix.join(baseOutputDir, `${cleanTestStem(normalizedSource)}${ext}`));
9063
+ return toPosix2(path13.posix.join(baseOutputDir, `${cleanTestStem(normalizedSource)}${ext}`));
9118
9064
  }
9119
- return toPosix2(path14.posix.join(baseOutputDir, dirOfSource, fileName));
9065
+ return toPosix2(path13.posix.join(baseOutputDir, dirOfSource, fileName));
9120
9066
  }
9121
9067
  function groupTestCasesByOutput(testCases, format, options, logger, outputNameSuffix) {
9122
9068
  const groups = /* @__PURE__ */ new Map();
@@ -9202,15 +9148,15 @@ var ReportGenerator = class {
9202
9148
  this.deps = {
9203
9149
  logger: deps?.logger ?? console,
9204
9150
  writeFile: deps?.writeFile ?? ((p, c) => fsPromises2.writeFile(p, c, "utf8")),
9205
- readFile: deps?.readFile ?? ((p) => fs11.readFileSync(p, "utf8")),
9151
+ readFile: deps?.readFile ?? ((p) => fs10.readFileSync(p, "utf8")),
9206
9152
  listDir: deps?.listDir ?? ((dir) => {
9207
9153
  try {
9208
- return fs11.readdirSync(dir);
9154
+ return fs10.readdirSync(dir);
9209
9155
  } catch {
9210
9156
  return void 0;
9211
9157
  }
9212
9158
  }),
9213
- fileExists: deps?.fileExists ?? ((p) => fs11.existsSync(p)),
9159
+ fileExists: deps?.fileExists ?? ((p) => fs10.existsSync(p)),
9214
9160
  removeFile: deps?.removeFile ?? ((p) => fsPromises2.rm(p, { force: true }))
9215
9161
  };
9216
9162
  }
@@ -9396,12 +9342,12 @@ var ReportGenerator = class {
9396
9342
  }
9397
9343
  }
9398
9344
  await this.bundleMarkdownAssets(results.get("markdown"), (markdownDir) => ({
9399
- assetsDir: path14.join(markdownDir, "assets"),
9345
+ assetsDir: path13.join(markdownDir, "assets"),
9400
9346
  assetsBaseUrl: "assets"
9401
9347
  }));
9402
9348
  await this.bundleMarkdownAssets(results.get("astro-markdown"), () => ({
9403
9349
  // assetsDir is resolved from CWD (same as outputDir), not relative to outputDir
9404
- assetsDir: path14.resolve(this.options.astro.assetsDir),
9350
+ assetsDir: path13.resolve(this.options.astro.assetsDir),
9405
9351
  assetsBaseUrl: this.options.astro.assetsBaseUrl
9406
9352
  }));
9407
9353
  }
@@ -9416,7 +9362,7 @@ var ReportGenerator = class {
9416
9362
  if (!markdownPaths) return;
9417
9363
  for (const markdownPath of markdownPaths) {
9418
9364
  const markdown = await fsPromises2.readFile(markdownPath, "utf8");
9419
- const markdownDir = path14.dirname(markdownPath);
9365
+ const markdownDir = path13.dirname(markdownPath);
9420
9366
  const result = copyMarkdownAssets({
9421
9367
  markdown,
9422
9368
  markdownDir,
@@ -9460,16 +9406,16 @@ var ReportGenerator = class {
9460
9406
  bySourceFile.set(sourceFile, outputPath);
9461
9407
  }
9462
9408
  if (bySourceFile.size === 0) return void 0;
9463
- const indexPath = toPosix2(path14.join(this.options.outputDir, "index.html"));
9409
+ const indexPath = toPosix2(path13.join(this.options.outputDir, "index.html"));
9464
9410
  if (htmlPaths.some((p) => toPosix2(p) === indexPath)) {
9465
9411
  this.deps.logger.warn?.(
9466
9412
  `Skipping colocated index: a report already occupies ${indexPath}.`
9467
9413
  );
9468
9414
  return void 0;
9469
9415
  }
9470
- const entries = buildIndexEntries(run, bySourceFile, path14.dirname(indexPath));
9416
+ const entries = buildIndexEntries(run, bySourceFile, path13.dirname(indexPath));
9471
9417
  const html = renderColocatedIndex(entries, this.options.html.title);
9472
- await fsPromises2.mkdir(path14.dirname(indexPath), { recursive: true });
9418
+ await fsPromises2.mkdir(path13.dirname(indexPath), { recursive: true });
9473
9419
  await this.deps.writeFile(indexPath, html);
9474
9420
  return indexPath;
9475
9421
  }
@@ -9488,10 +9434,10 @@ var ReportGenerator = class {
9488
9434
  if (groups.size === 0 && this.options.output.mode === "aggregated") {
9489
9435
  const ext = FORMAT_EXTENSIONS[format];
9490
9436
  const effectiveName = this.options.outputName + (outputNameSuffix ?? "");
9491
- const outputPath = toPosix2(path14.join(this.options.outputDir, joinNameAndExt(effectiveName, ext)));
9437
+ const outputPath = toPosix2(path13.join(this.options.outputDir, joinNameAndExt(effectiveName, ext)));
9492
9438
  const content = await this.formatContent(run, format, outputPath);
9493
9439
  if (content === "" && SKIP_WHEN_EMPTY.has(format)) return [];
9494
- const dir = path14.dirname(outputPath);
9440
+ const dir = path13.dirname(outputPath);
9495
9441
  await fsPromises2.mkdir(dir, { recursive: true });
9496
9442
  await this.deps.writeFile(outputPath, content);
9497
9443
  return [outputPath];
@@ -9504,7 +9450,7 @@ var ReportGenerator = class {
9504
9450
  };
9505
9451
  const content = await this.formatContent(groupRun, format, outputPath);
9506
9452
  if (content === "" && SKIP_WHEN_EMPTY.has(format)) continue;
9507
- const dir = path14.dirname(outputPath);
9453
+ const dir = path13.dirname(outputPath);
9508
9454
  await fsPromises2.mkdir(dir, { recursive: true });
9509
9455
  await this.deps.writeFile(outputPath, content);
9510
9456
  writtenPaths.push(outputPath);
@@ -9701,8 +9647,8 @@ function toRun(data, inputType, synthesize) {
9701
9647
  return canonicalizeRun(raw);
9702
9648
  }
9703
9649
  async function regenerateRun(options, deps = {}) {
9704
- const read = deps.readFile ?? ((filePath) => fs12.readFileSync(filePath, "utf8"));
9705
- const data = JSON.parse(read(path15.resolve(options.input)));
9650
+ const read = deps.readFile ?? ((filePath) => fs11.readFileSync(filePath, "utf8"));
9651
+ const data = JSON.parse(read(path14.resolve(options.input)));
9706
9652
  const run = toRun(data, options.inputType ?? "raw", options.synthesize !== false);
9707
9653
  const generator = new ReportGenerator({
9708
9654
  formats: options.formats,
@@ -9718,7 +9664,7 @@ async function regenerateArtifacts(options, deps = {}) {
9718
9664
  function startWatch(options, deps = {}) {
9719
9665
  const log = deps.log ?? ((message) => console.log(message));
9720
9666
  const regenerate = deps.regenerate ?? ((input) => regenerateArtifacts({ ...options, input }, deps));
9721
- const watchFn = deps.watch ?? ((filePath, listener) => fs12.watch(filePath, listener));
9667
+ const watchFn = deps.watch ?? ((filePath, listener) => fs11.watch(filePath, listener));
9722
9668
  const debounceMs = options.debounceMs ?? 150;
9723
9669
  let timer;
9724
9670
  let running = false;
@@ -9747,7 +9693,7 @@ function startWatch(options, deps = {}) {
9747
9693
  timer = setTimeout(() => void run(), debounceMs);
9748
9694
  };
9749
9695
  trigger();
9750
- const watcher = watchFn(path15.resolve(options.input), trigger);
9696
+ const watcher = watchFn(path14.resolve(options.input), trigger);
9751
9697
  return {
9752
9698
  close: () => {
9753
9699
  if (timer) clearTimeout(timer);
@@ -9760,7 +9706,7 @@ function startWatch(options, deps = {}) {
9760
9706
  import { advanceState, initialRunState } from "executable-stories-core";
9761
9707
 
9762
9708
  // src/runs-lifecycle.ts
9763
- import path16 from "path";
9709
+ import path15 from "path";
9764
9710
  function relativeAge(thenMs, nowMs) {
9765
9711
  const deltaMs = Math.max(0, nowMs - thenMs);
9766
9712
  const minutes = Math.floor(deltaMs / 6e4);
@@ -9776,7 +9722,7 @@ function readFileReports2(dir, deps) {
9776
9722
  const files = [];
9777
9723
  const unreadable = [];
9778
9724
  for (const name of names) {
9779
- const filePath = path16.posix.join(dir, name);
9725
+ const filePath = path15.posix.join(dir, name);
9780
9726
  try {
9781
9727
  const run = JSON.parse(deps.readFile(filePath));
9782
9728
  const testCases = run.testCases ?? [];
@@ -9837,7 +9783,7 @@ function runsStatus(args, deps) {
9837
9783
  function runsReset(args, deps) {
9838
9784
  const directory = byFileDirFor(args.outputDir);
9839
9785
  const names = (deps.listDir(directory) ?? []).filter((n) => n.endsWith(".json"));
9840
- for (const name of names) deps.removeFile(path16.posix.join(directory, name));
9786
+ for (const name of names) deps.removeFile(path15.posix.join(directory, name));
9841
9787
  const text2 = names.length === 0 ? `Nothing to reset: no per-file reports in ${directory}.` : `Removed ${names.length} per-file report${names.length === 1 ? "" : "s"} from ${directory}.
9842
9788
  Run your full test suite to write them again.`;
9843
9789
  return { directory, removed: names.length, text: text2 };
@@ -10692,8 +10638,8 @@ function toRegExp(pattern) {
10692
10638
  const tail = body.endsWith("/") ? ".*" : "(/.*)?";
10693
10639
  return new RegExp(`^${floating ? "(.*/)?" : ""}${globbed}${tail}$`);
10694
10640
  }
10695
- function ownersFor(rules, path26) {
10696
- const clean = path26.replace(/^\.\//, "").replace(/^\//, "");
10641
+ function ownersFor(rules, path23) {
10642
+ const clean = path23.replace(/^\.\//, "").replace(/^\//, "");
10697
10643
  let owners = [];
10698
10644
  for (const rule of rules) {
10699
10645
  if (toRegExp(rule.pattern).test(clean)) owners = rule.owners;
@@ -10708,8 +10654,8 @@ function buildTriage(args, _deps = {}) {
10708
10654
  if (!codeowners) return [];
10709
10655
  const paths = tc.story.covers?.length ? tc.story.covers : [tc.sourceFile];
10710
10656
  const found = /* @__PURE__ */ new Set();
10711
- for (const path26 of paths) {
10712
- for (const owner of ownersFor(codeowners, path26)) found.add(owner);
10657
+ for (const path23 of paths) {
10658
+ for (const owner of ownersFor(codeowners, path23)) found.add(owner);
10713
10659
  }
10714
10660
  return [...found];
10715
10661
  };
@@ -10748,6 +10694,55 @@ function buildTriage(args, _deps = {}) {
10748
10694
  items
10749
10695
  };
10750
10696
  }
10697
+ var TRIAGE_SUGGEST_MIN_PROBABILITY = 0.5;
10698
+ var FAILURE_KIND_CRITERIA = {
10699
+ product: "the application code under test behaves wrongly",
10700
+ test: "the scenario, assertion, fixture, or test data is wrong or stale",
10701
+ infra: "environment, network, timeout, resource, or flaky-timing failure"
10702
+ };
10703
+ async function enrichTriage(report, testCases, jev, codeowners) {
10704
+ const candidates = new Set(testCases.flatMap((tc) => tc.story.covers ?? []));
10705
+ for (const rule of codeowners ?? []) candidates.add(rule.pattern);
10706
+ const criteria = Object.fromEntries([...candidates].map((path23) => [path23, null]));
10707
+ const byId = new Map(testCases.map((tc) => [tc.id, tc]));
10708
+ const items = await Promise.all(
10709
+ report.items.map(async (item) => {
10710
+ if (item.covers.length > 0) return item;
10711
+ const tc = byId.get(item.id);
10712
+ const answers = await jev.ask(
10713
+ {
10714
+ scenario: item.scenario,
10715
+ steps: tc?.story.steps.map((s) => `${s.keyword} ${s.text}`) ?? [],
10716
+ testFile: item.location,
10717
+ error: item.errorMessage ?? null
10718
+ },
10719
+ {
10720
+ kind: {
10721
+ type: "choice",
10722
+ instructions: "What is most likely broken, given this failing scenario and its error?",
10723
+ criteria: FAILURE_KIND_CRITERIA
10724
+ },
10725
+ ...candidates.size > 0 ? {
10726
+ covers: {
10727
+ type: "choice",
10728
+ instructions: "Which of these product paths does the fix for this failure most likely land in?",
10729
+ criteria
10730
+ }
10731
+ } : {}
10732
+ }
10733
+ );
10734
+ const kind = asChoice(answers.kind);
10735
+ const covers = asChoice(answers.covers);
10736
+ const probability = covers ? covers.probabilities[covers.choice] ?? 0 : 0;
10737
+ return {
10738
+ ...item,
10739
+ ...kind && kind.choice in FAILURE_KIND_CRITERIA ? { failureKind: { kind: kind.choice, confidence: kind.confidence } } : {},
10740
+ ...covers && probability >= TRIAGE_SUGGEST_MIN_PROBABILITY ? { suggestedCovers: { path: covers.choice, probability } } : {}
10741
+ };
10742
+ })
10743
+ );
10744
+ return { ...report, items };
10745
+ }
10751
10746
  function renderTriage(report, format, options = {}) {
10752
10747
  if (format === "json") return JSON.stringify(report, null, 2);
10753
10748
  if (report.items.length === 0) {
@@ -10765,9 +10760,14 @@ function renderTriage(report, format, options = {}) {
10765
10760
  }
10766
10761
  if (item.covers.length > 0) {
10767
10762
  lines.push(` fix: ${item.covers.join(", ")}`);
10763
+ } else if (item.suggestedCovers) {
10764
+ lines.push(` fix: ${item.suggestedCovers.path}? (jev ${item.suggestedCovers.probability.toFixed(2)}, no covers declared)`);
10768
10765
  } else {
10769
10766
  lines.push(" fix: (no covers declared \u2014 add `covers` to route this to code)");
10770
10767
  }
10768
+ if (item.failureKind) {
10769
+ lines.push(` kind: ${item.failureKind.kind} (jev ${item.failureKind.confidence.toFixed(2)})`);
10770
+ }
10771
10771
  if (item.tickets.length > 0) {
10772
10772
  lines.push(` ticket: ${item.tickets.join(", ")}`);
10773
10773
  }
@@ -10971,7 +10971,7 @@ async function generateRunComparison(args) {
10971
10971
  await fsPromises3.mkdir(outputDir, { recursive: true });
10972
10972
  for (const format of args.formats) {
10973
10973
  const ext = format === "html" ? ".html" : format === "changelog" ? ".changelog.md" : ".md";
10974
- const outputPath = toPosix2(path17.join(outputDir, `${outputName}${ext}`));
10974
+ const outputPath = toPosix2(path16.join(outputDir, `${outputName}${ext}`));
10975
10975
  const content = format === "html" ? new RunDiffHtmlFormatter({ title: args.title }).format(diff) : format === "changelog" ? new RunDiffChangelogFormatter().format(diff) : new RunDiffMarkdownFormatter({ title: args.title }).format(diff);
10976
10976
  await fsPromises3.writeFile(outputPath, content, "utf8");
10977
10977
  files.push(outputPath);
@@ -10979,119 +10979,9 @@ async function generateRunComparison(args) {
10979
10979
  return { files, diff };
10980
10980
  }
10981
10981
 
10982
- // src/init-astro.ts
10983
- import { spawnSync } from "child_process";
10984
- import * as fs13 from "fs";
10985
- import * as path18 from "path";
10986
- import { fileURLToPath } from "url";
10987
- var __dirname = path18.dirname(fileURLToPath(import.meta.url));
10988
- function detectPackageManager(cwd = process.cwd()) {
10989
- if (fs13.existsSync(path18.join(cwd, "pnpm-lock.yaml"))) return "pnpm";
10990
- if (fs13.existsSync(path18.join(cwd, "yarn.lock"))) return "yarn";
10991
- if (fs13.existsSync(path18.join(cwd, "bun.lock")) || fs13.existsSync(path18.join(cwd, "bun.lockb"))) {
10992
- return "bun";
10993
- }
10994
- return "npm";
10995
- }
10996
- function runPackageManager(pm, pmArgs, cwd) {
10997
- const result = spawnSync(pm, pmArgs, {
10998
- cwd,
10999
- stdio: "inherit",
11000
- shell: process.platform === "win32"
11001
- });
11002
- return result.status;
11003
- }
11004
- function installScaffoldDependencies(targetDir, pm) {
11005
- return runPackageManager(pm, ["install"], targetDir) === 0;
11006
- }
11007
- function runDocsDev(siteDir) {
11008
- if (!isScaffoldedAstroSite(siteDir)) {
11009
- return { kind: "not-scaffolded" };
11010
- }
11011
- const pm = detectPackageManager();
11012
- if (!fs13.existsSync(path18.join(siteDir, "node_modules"))) {
11013
- console.log(`Installing docs site dependencies with ${pm}\u2026`);
11014
- if (!installScaffoldDependencies(siteDir, pm)) {
11015
- return { kind: "install-failed", pm };
11016
- }
11017
- }
11018
- console.log("Tip: run your tests in watch mode in another terminal \u2014 the site hot-reloads on every run.");
11019
- return { kind: "dev-exited", status: runPackageManager(pm, ["run", "dev"], siteDir) };
11020
- }
11021
- var SCAFFOLD_MARKER = "executable-stories.config.mjs";
11022
- function isScaffoldedAstroSite(dir) {
11023
- return fs13.existsSync(path18.join(dir, SCAFFOLD_MARKER));
11024
- }
11025
- function initAstro(options = {}) {
11026
- const targetDir = options.targetDir ?? "./story-docs";
11027
- const force = options.force ?? false;
11028
- const update = options.update ?? false;
11029
- const templateDir = path18.resolve(__dirname, "..", "templates", "astro-thin");
11030
- if (!fs13.existsSync(templateDir)) {
11031
- throw new Error(
11032
- `Template directory not found at ${templateDir}. Ensure the package is installed correctly.`
11033
- );
11034
- }
11035
- if (update) {
11036
- return updateScaffoldDeps(templateDir, targetDir);
11037
- }
11038
- if (fs13.existsSync(targetDir)) {
11039
- const entries = fs13.readdirSync(targetDir);
11040
- if (entries.length > 0 && !force) {
11041
- throw new Error(
11042
- `Directory "${targetDir}" already exists and is not empty. Use --force to overlay the template (existing files are kept; same-path template files are overwritten), or --update to refresh framework files only.`
11043
- );
11044
- }
11045
- }
11046
- copyDirRecursive(templateDir, targetDir);
11047
- return { targetDir };
11048
- }
11049
- function updateScaffoldDeps(templateDir, targetDir) {
11050
- if (!isScaffoldedAstroSite(targetDir)) {
11051
- throw new Error(
11052
- `"${targetDir}" does not look like a scaffolded docs site. Run init-astro (without --update) first.`
11053
- );
11054
- }
11055
- mergeDependencies(templateDir, targetDir);
11056
- return { targetDir };
11057
- }
11058
- function mergeDependencies(templateDir, targetDir) {
11059
- const tmplPkgPath = path18.join(templateDir, "package.json");
11060
- const userPkgPath = path18.join(targetDir, "package.json");
11061
- if (!fs13.existsSync(tmplPkgPath) || !fs13.existsSync(userPkgPath)) return;
11062
- const tmpl = JSON.parse(fs13.readFileSync(tmplPkgPath, "utf8"));
11063
- const user = JSON.parse(fs13.readFileSync(userPkgPath, "utf8"));
11064
- user.dependencies = user.dependencies ?? {};
11065
- let changed = false;
11066
- for (const [name, version] of Object.entries(tmpl.dependencies ?? {})) {
11067
- if (!(name in user.dependencies)) {
11068
- user.dependencies[name] = version;
11069
- changed = true;
11070
- }
11071
- }
11072
- if (changed) {
11073
- fs13.writeFileSync(userPkgPath, `${JSON.stringify(user, null, 2)}
11074
- `, "utf8");
11075
- }
11076
- }
11077
- function copyDirRecursive(src, dest) {
11078
- fs13.mkdirSync(dest, { recursive: true });
11079
- const entries = fs13.readdirSync(src, { withFileTypes: true });
11080
- for (const entry of entries) {
11081
- const srcPath = path18.join(src, entry.name);
11082
- const destName = entry.name === "gitignore" ? ".gitignore" : entry.name;
11083
- const destPath = path18.join(dest, destName);
11084
- if (entry.isDirectory()) {
11085
- copyDirRecursive(srcPath, destPath);
11086
- } else {
11087
- fs13.copyFileSync(srcPath, destPath);
11088
- }
11089
- }
11090
- }
11091
-
11092
10982
  // src/open-report.ts
11093
10983
  import { spawn } from "child_process";
11094
- import path19 from "path";
10984
+ import path17 from "path";
11095
10985
  function openCommand(platform) {
11096
10986
  if (platform === "darwin") return { command: "open", args: [] };
11097
10987
  if (platform === "win32") return { command: "cmd", args: ["/c", "start", ""] };
@@ -11107,7 +10997,7 @@ function openInBrowser(file, platform = process.platform) {
11107
10997
  }
11108
10998
  const { command, args } = openCommand(platform);
11109
10999
  try {
11110
- const child = spawn(command, [...args, path19.resolve(file)], { stdio: "ignore", detached: true });
11000
+ const child = spawn(command, [...args, path17.resolve(file)], { stdio: "ignore", detached: true });
11111
11001
  child.on("error", (err) => {
11112
11002
  console.error(`--open: could not open ${file}: ${err.message}`);
11113
11003
  });
@@ -11147,8 +11037,8 @@ function expandPreset(preset, explicitFormats, userSetFormat) {
11147
11037
 
11148
11038
  // src/push.ts
11149
11039
  import { execFileSync } from "child_process";
11150
- import * as fs14 from "fs";
11151
- import * as path20 from "path";
11040
+ import * as fs12 from "fs";
11041
+ import * as path18 from "path";
11152
11042
  import { parseArgs } from "util";
11153
11043
  import { canonicalizeRun as canonicalizeRun4 } from "executable-stories-core/converters/acl/canonicalize";
11154
11044
  import { toStoryReport as toStoryReport7 } from "executable-stories-core/converters/story-report";
@@ -11209,16 +11099,16 @@ the job summary, and the run id is written to GITHUB_OUTPUT as ingest-run-id.
11209
11099
  Exit codes: 0 pushed, 1 push rejected/failed, 4 usage error, 5 gate blocked.`;
11210
11100
  function defaultDeps() {
11211
11101
  return {
11212
- readFile: (filePath) => fs14.readFileSync(filePath, "utf8"),
11102
+ readFile: (filePath) => fs12.readFileSync(filePath, "utf8"),
11213
11103
  listDir: (dirPath) => {
11214
11104
  try {
11215
- return fs14.readdirSync(dirPath);
11105
+ return fs12.readdirSync(dirPath);
11216
11106
  } catch {
11217
11107
  return void 0;
11218
11108
  }
11219
11109
  },
11220
- appendFile: (filePath, text2) => fs14.appendFileSync(filePath, text2),
11221
- writeFile: (filePath, text2) => fs14.writeFileSync(filePath, text2, "utf8"),
11110
+ appendFile: (filePath, text2) => fs12.appendFileSync(filePath, text2),
11111
+ writeFile: (filePath, text2) => fs12.writeFileSync(filePath, text2, "utf8"),
11222
11112
  fetchFn: fetch,
11223
11113
  git: (args) => {
11224
11114
  try {
@@ -11249,7 +11139,7 @@ function readInput(inputPath, deps) {
11249
11139
  const entries = deps.listDir(inputPath);
11250
11140
  if (!entries) return deps.readFile(inputPath);
11251
11141
  const results = entries.filter((name) => name.endsWith("-result.json")).sort().map((name) => {
11252
- const file = path20.join(inputPath, name);
11142
+ const file = path18.join(inputPath, name);
11253
11143
  try {
11254
11144
  return JSON.parse(deps.readFile(file));
11255
11145
  } catch (err) {
@@ -11732,13 +11622,16 @@ Release gate: clear for ${commit}`);
11732
11622
  }
11733
11623
 
11734
11624
  // src/share.ts
11735
- import * as fs15 from "fs";
11736
- import * as path21 from "path";
11625
+ import * as fs13 from "fs";
11626
+ import * as path19 from "path";
11737
11627
  import { parseArgs as parseArgs2 } from "util";
11738
11628
  import { canonicalizeRun as canonicalizeRun5 } from "executable-stories-core/converters/acl/canonicalize";
11739
- import { collectReportAssets, rewriteReportAssets } from "executable-stories-core/report-assets";
11740
11629
  import { toStoryReport as toStoryReport8 } from "executable-stories-core/converters/story-report";
11741
11630
  import { synthesizeStories as synthesizeStories3 } from "executable-stories-core/converters/synthesize";
11631
+ import {
11632
+ collectReportAssets,
11633
+ rewriteReportAssets
11634
+ } from "executable-stories-core/report-assets";
11742
11635
  var EXIT_SUCCESS2 = 0;
11743
11636
  var EXIT_SHARE_FAILED = 1;
11744
11637
  var EXIT_USAGE2 = 4;
@@ -11767,11 +11660,11 @@ Options:
11767
11660
  Exit codes: 0 shared, 1 rejected or failed, 4 usage error.`;
11768
11661
  function defaultDeps2() {
11769
11662
  return {
11770
- readFile: (filePath) => fs15.readFileSync(filePath, "utf8"),
11771
- readBinary: (filePath) => new Uint8Array(fs15.readFileSync(filePath)),
11663
+ readFile: (filePath) => fs13.readFileSync(filePath, "utf8"),
11664
+ readBinary: (filePath) => new Uint8Array(fs13.readFileSync(filePath)),
11772
11665
  fileSize: (filePath) => {
11773
11666
  try {
11774
- const stat = fs15.statSync(filePath);
11667
+ const stat = fs13.statSync(filePath);
11775
11668
  return stat.isFile() ? stat.size : void 0;
11776
11669
  } catch {
11777
11670
  return void 0;
@@ -11779,7 +11672,7 @@ function defaultDeps2() {
11779
11672
  },
11780
11673
  listDir: (dirPath) => {
11781
11674
  try {
11782
- return fs15.readdirSync(dirPath);
11675
+ return fs13.readdirSync(dirPath);
11783
11676
  } catch {
11784
11677
  return void 0;
11785
11678
  }
@@ -11807,11 +11700,11 @@ var CONTENT_TYPES = {
11807
11700
  ".zip": "application/zip"
11808
11701
  };
11809
11702
  function contentTypeFor(filePath) {
11810
- return CONTENT_TYPES[path21.extname(filePath).toLowerCase()] ?? "application/octet-stream";
11703
+ return CONTENT_TYPES[path19.extname(filePath).toLowerCase()] ?? "application/octet-stream";
11811
11704
  }
11812
11705
  function resolveReport(inputPath, deps) {
11813
11706
  const entries = deps.listDir(inputPath);
11814
- const candidates = entries === void 0 ? [inputPath] : candidateNames(entries).map((name) => path21.join(inputPath, name));
11707
+ const candidates = entries === void 0 ? [inputPath] : candidateNames(entries).map((name) => path19.join(inputPath, name));
11815
11708
  if (candidates.length === 0) {
11816
11709
  throw new Error(
11817
11710
  "no report in it. Generate one first: executable-stories format <run.json> --format story-report-json --output-dir <dir> --output-name index"
@@ -11843,13 +11736,20 @@ function loadReport(filePath, deps) {
11843
11736
  const text2 = deps.readFile(filePath);
11844
11737
  if (filePath.endsWith(".html")) {
11845
11738
  const match = HTML_REPORT_DATA.exec(text2);
11846
- if (!match?.[1]) throw new Error(`${filePath} is an HTML page with no report embedded in it`);
11847
- return asStoryReport(JSON.parse(match[1]), filePath);
11739
+ if (!match?.[1])
11740
+ throw new Error(
11741
+ `${filePath} is an HTML page with no report embedded in it`
11742
+ );
11743
+ return asStoryReport(
11744
+ JSON.parse(match[1]),
11745
+ filePath
11746
+ );
11848
11747
  }
11849
11748
  return asStoryReport(JSON.parse(text2), filePath);
11850
11749
  }
11851
11750
  function asStoryReport(data, filePath) {
11852
- if (typeof data.schemaVersion === "string") return data;
11751
+ if (typeof data.schemaVersion === "string")
11752
+ return data;
11853
11753
  try {
11854
11754
  return toStoryReport8(canonicalizeRun5(synthesizeStories3(data)));
11855
11755
  } catch (err) {
@@ -11860,12 +11760,12 @@ function asStoryReport(data, filePath) {
11860
11760
  }
11861
11761
  }
11862
11762
  function keyFor(assetPath, reportDir, taken) {
11863
- const resolved = path21.resolve(reportDir, assetPath);
11864
- const relative3 = path21.relative(reportDir, resolved);
11865
- const inside = relative3 !== "" && !relative3.startsWith("..") && !path21.isAbsolute(relative3);
11866
- const base = inside ? relative3.split(path21.sep).join("/") : `assets/${path21.basename(resolved)}`;
11763
+ const resolved = path19.resolve(reportDir, assetPath);
11764
+ const relative3 = path19.relative(reportDir, resolved);
11765
+ const inside = relative3 !== "" && !relative3.startsWith("..") && !path19.isAbsolute(relative3);
11766
+ const base = inside ? relative3.split(path19.sep).join("/") : `assets/${path19.basename(resolved)}`;
11867
11767
  let key = base;
11868
- const ext = path21.extname(base);
11768
+ const ext = path19.extname(base);
11869
11769
  const stem = base.slice(0, base.length - ext.length);
11870
11770
  for (let n = 2; taken.has(key); n++) key = `${stem}-${n}${ext}`;
11871
11771
  taken.add(key);
@@ -11877,7 +11777,7 @@ function planAssets(report, reportDir, deps) {
11877
11777
  const keyByPath = /* @__PURE__ */ new Map();
11878
11778
  const taken = /* @__PURE__ */ new Set();
11879
11779
  for (const assetPath of collectReportAssets(report)) {
11880
- const localPath = path21.resolve(reportDir, assetPath);
11780
+ const localPath = path19.resolve(reportDir, assetPath);
11881
11781
  const bytes = deps.fileSize(localPath);
11882
11782
  const key = keyFor(assetPath, reportDir, taken);
11883
11783
  keyByPath.set(assetPath, key);
@@ -11885,7 +11785,12 @@ function planAssets(report, reportDir, deps) {
11885
11785
  missing.push(assetPath);
11886
11786
  continue;
11887
11787
  }
11888
- assets.push({ path: key, localPath, contentType: contentTypeFor(key), bytes });
11788
+ assets.push({
11789
+ path: key,
11790
+ localPath,
11791
+ contentType: contentTypeFor(key),
11792
+ bytes
11793
+ });
11889
11794
  }
11890
11795
  return { assets, missing, keyByPath };
11891
11796
  }
@@ -11900,6 +11805,10 @@ async function describeFailure(response) {
11900
11805
  if (error?.type === "SHARE_TOO_LARGE") {
11901
11806
  return `the report and its assets are over the ${error.maxBytes ?? 0} byte limit for a share.`;
11902
11807
  }
11808
+ if (error?.type === "STORAGE_LIMIT") {
11809
+ const mb = (bytes) => `${Math.round(bytes / 1024 / 1024)} MB`;
11810
+ return `stored evidence is at the plan's ${mb(error.limit ?? 0)} (${mb(error.used ?? 0)} used). Delete a share or an attachment in your cloud settings, or upgrade.`;
11811
+ }
11903
11812
  if (error?.message) return error.message;
11904
11813
  if (error?.type) return error.type;
11905
11814
  } catch {
@@ -11934,7 +11843,9 @@ async function runShare(rawArgs, depsOverride = {}) {
11934
11843
  }
11935
11844
  const inputPath = parsed.positionals[0];
11936
11845
  if (!inputPath) {
11937
- deps.error("share needs a report: executable-stories share <reports-dir|report.html|report.json>");
11846
+ deps.error(
11847
+ "share needs a report: executable-stories share <reports-dir|report.html|report.json>"
11848
+ );
11938
11849
  deps.error(HELP2);
11939
11850
  return EXIT_USAGE2;
11940
11851
  }
@@ -11948,7 +11859,9 @@ async function runShare(rawArgs, depsOverride = {}) {
11948
11859
  const expiresRaw = parsed.values["expires-days"];
11949
11860
  const expiresInDays = expiresRaw === void 0 ? 30 : Number(expiresRaw);
11950
11861
  if (!Number.isInteger(expiresInDays) || expiresInDays < 0) {
11951
- deps.error(`--expires-days takes a whole number of days (0 never expires), not "${expiresRaw}".`);
11862
+ deps.error(
11863
+ `--expires-days takes a whole number of days (0 never expires), not "${expiresRaw}".`
11864
+ );
11952
11865
  return EXIT_USAGE2;
11953
11866
  }
11954
11867
  let reportPath;
@@ -11956,16 +11869,23 @@ async function runShare(rawArgs, depsOverride = {}) {
11956
11869
  try {
11957
11870
  ({ path: reportPath, report } = resolveReport(inputPath, deps));
11958
11871
  } catch (err) {
11959
- deps.error(`Could not read ${inputPath}: ${err instanceof Error ? err.message : String(err)}`);
11872
+ deps.error(
11873
+ `Could not read ${inputPath}: ${err instanceof Error ? err.message : String(err)}`
11874
+ );
11960
11875
  return EXIT_USAGE2;
11961
11876
  }
11962
- const reportDir = path21.dirname(reportPath);
11877
+ const reportDir = path19.dirname(reportPath);
11963
11878
  const { assets, missing, keyByPath } = planAssets(report, reportDir, deps);
11964
11879
  for (const assetPath of missing) {
11965
- deps.error(`Warning: ${assetPath} is missing, so it will not be in the share.`);
11880
+ deps.error(
11881
+ `Warning: ${assetPath} is missing, so it will not be in the share.`
11882
+ );
11966
11883
  }
11967
11884
  const shareReport = {
11968
- ...rewriteReportAssets(report, (assetPath) => keyByPath.get(assetPath) ?? assetPath),
11885
+ ...rewriteReportAssets(
11886
+ report,
11887
+ (assetPath) => keyByPath.get(assetPath) ?? assetPath
11888
+ ),
11969
11889
  projectRoot: ""
11970
11890
  };
11971
11891
  const emails = (parsed.values.emails ?? "").split(",").map((email) => email.trim()).filter(Boolean);
@@ -11974,7 +11894,10 @@ async function runShare(rawArgs, depsOverride = {}) {
11974
11894
  try {
11975
11895
  const response = await deps.fetchFn(new URL("/api/v1/shares", baseUrl), {
11976
11896
  method: "POST",
11977
- headers: { "Content-Type": "application/json", Authorization: `Bearer ${key}` },
11897
+ headers: {
11898
+ "Content-Type": "application/json",
11899
+ Authorization: `Bearer ${key}`
11900
+ },
11978
11901
  body: JSON.stringify({
11979
11902
  title: parsed.values.title,
11980
11903
  report: shareReport,
@@ -11994,21 +11917,32 @@ async function runShare(rawArgs, depsOverride = {}) {
11994
11917
  }
11995
11918
  created = await response.json();
11996
11919
  } catch (err) {
11997
- deps.error(`Could not reach ${baseUrl}: ${err instanceof Error ? err.message : String(err)}`);
11920
+ deps.error(
11921
+ `Could not reach ${baseUrl}: ${err instanceof Error ? err.message : String(err)}`
11922
+ );
11998
11923
  return EXIT_SHARE_FAILED;
11999
11924
  }
12000
- const localByKey = new Map(assets.map((asset) => [asset.path, asset.localPath]));
11925
+ const localByKey = new Map(
11926
+ assets.map((asset) => [asset.path, asset.localPath])
11927
+ );
12001
11928
  for (const upload of created.uploads ?? []) {
12002
11929
  const filePath = localByKey.get(upload.path);
12003
11930
  if (filePath === void 0) {
12004
- deps.error(`Share asked for a file this report did not offer: ${upload.path}`);
11931
+ deps.error(
11932
+ `Share asked for a file this report did not offer: ${upload.path}`
11933
+ );
12005
11934
  return EXIT_SHARE_FAILED;
12006
11935
  }
12007
11936
  try {
12008
11937
  const response = await deps.fetchFn(upload.url, {
12009
11938
  method: "PUT",
12010
- headers: { "Content-Type": contentTypeFor(upload.path), ...upload.headers },
12011
- body: new Blob([deps.readBinary(filePath)], { type: contentTypeFor(upload.path) })
11939
+ headers: {
11940
+ "Content-Type": contentTypeFor(upload.path),
11941
+ ...upload.headers
11942
+ },
11943
+ body: new Blob([deps.readBinary(filePath)], {
11944
+ type: contentTypeFor(upload.path)
11945
+ })
12012
11946
  });
12013
11947
  if (!response.ok) {
12014
11948
  deps.error(`Upload of ${upload.path} failed: HTTP ${response.status}`);
@@ -12022,12 +11956,17 @@ async function runShare(rawArgs, depsOverride = {}) {
12022
11956
  }
12023
11957
  }
12024
11958
  try {
12025
- const response = await deps.fetchFn(new URL(`/api/v1/shares/${created.id}/complete`, baseUrl), {
12026
- method: "POST",
12027
- headers: { Authorization: `Bearer ${key}` }
12028
- });
11959
+ const response = await deps.fetchFn(
11960
+ new URL(`/api/v1/shares/${created.id}/complete`, baseUrl),
11961
+ {
11962
+ method: "POST",
11963
+ headers: { Authorization: `Bearer ${key}` }
11964
+ }
11965
+ );
12029
11966
  if (!response.ok) {
12030
- deps.error(`Share could not be published: ${await describeFailure(response)}`);
11967
+ deps.error(
11968
+ `Share could not be published: ${await describeFailure(response)}`
11969
+ );
12031
11970
  return EXIT_SHARE_FAILED;
12032
11971
  }
12033
11972
  } catch (err) {
@@ -12036,27 +11975,37 @@ async function runShare(rawArgs, depsOverride = {}) {
12036
11975
  );
12037
11976
  return EXIT_SHARE_FAILED;
12038
11977
  }
11978
+ const notStored = created.notStored ?? [];
12039
11979
  if (parsed.values.json) {
12040
- deps.log(JSON.stringify({ id: created.id, url: created.url, assets: assets.length }, null, 2));
11980
+ deps.log(
11981
+ JSON.stringify(
11982
+ { id: created.id, url: created.url, assets: assets.length, notStored },
11983
+ null,
11984
+ 2
11985
+ )
11986
+ );
12041
11987
  return EXIT_SUCCESS2;
12042
11988
  }
12043
11989
  const withAssets = assets.length === 1 ? "1 asset" : `${assets.length} assets`;
12044
- deps.log(`Shared ${path21.basename(reportPath)} (${withAssets}):`);
11990
+ deps.log(`Shared ${path19.basename(reportPath)} (${withAssets}):`);
12045
11991
  deps.log(` ${created.url}`);
12046
11992
  deps.log(
12047
11993
  emails.length > 0 ? ` Only ${emails.join(", ")} can open it, after signing in.` : " Anyone with the link can open it."
12048
11994
  );
11995
+ for (const skipped of notStored) {
11996
+ deps.log(` Not stored: ${skipped.path} (${skipped.reason})`);
11997
+ }
12049
11998
  return EXIT_SUCCESS2;
12050
11999
  }
12051
12000
 
12052
12001
  // src/run-file.ts
12053
- import fs16 from "fs";
12054
- import path22 from "path";
12002
+ import fs14 from "fs";
12003
+ import path20 from "path";
12055
12004
  var DEFAULT_RUN_FILES = [".executable-stories/raw-run.json", "reports/raw-run.json"];
12056
12005
  var SUPPORTED_RAW_RUN_SCHEMA = 1;
12057
12006
  function findDefaultRunFile(cwd = process.cwd()) {
12058
12007
  for (const candidate of DEFAULT_RUN_FILES) {
12059
- if (fs16.existsSync(path22.resolve(cwd, candidate))) return candidate;
12008
+ if (fs14.existsSync(path20.resolve(cwd, candidate))) return candidate;
12060
12009
  }
12061
12010
  return void 0;
12062
12011
  }
@@ -12082,8 +12031,8 @@ function diagnoseRunFile(file, cwd = process.cwd()) {
12082
12031
  });
12083
12032
  return { checks, healthy: false };
12084
12033
  }
12085
- const abs = path22.resolve(cwd, resolved);
12086
- if (!fs16.existsSync(abs)) {
12034
+ const abs = path20.resolve(cwd, resolved);
12035
+ if (!fs14.existsSync(abs)) {
12087
12036
  checks.push({
12088
12037
  label: "run file",
12089
12038
  status: "fail",
@@ -12092,7 +12041,7 @@ function diagnoseRunFile(file, cwd = process.cwd()) {
12092
12041
  });
12093
12042
  return { checks, healthy: false };
12094
12043
  }
12095
- const stat = fs16.statSync(abs);
12044
+ const stat = fs14.statSync(abs);
12096
12045
  checks.push({
12097
12046
  label: "run file",
12098
12047
  status: "ok",
@@ -12100,7 +12049,7 @@ function diagnoseRunFile(file, cwd = process.cwd()) {
12100
12049
  });
12101
12050
  let parsed;
12102
12051
  try {
12103
- parsed = JSON.parse(fs16.readFileSync(abs, "utf8"));
12052
+ parsed = JSON.parse(fs14.readFileSync(abs, "utf8"));
12104
12053
  } catch (err) {
12105
12054
  checks.push({
12106
12055
  label: "json",
@@ -12187,246 +12136,6 @@ function formatDoctorReport(report) {
12187
12136
  return lines.join("\n");
12188
12137
  }
12189
12138
 
12190
- // src/scaffold-doc.ts
12191
- import * as fs17 from "fs";
12192
- import * as path23 from "path";
12193
- var TEMPLATES = [
12194
- "adr",
12195
- "runbook",
12196
- "decision-log",
12197
- "incident",
12198
- "scenario-note"
12199
- ];
12200
- function slugify2(input) {
12201
- return input.toLowerCase().trim().replace(/['"]/g, "").replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "untitled";
12202
- }
12203
- function isoDate(today) {
12204
- return today.toISOString().slice(0, 10);
12205
- }
12206
- function nextSeq(dir) {
12207
- let max = 0;
12208
- try {
12209
- for (const entry of fs17.readdirSync(dir)) {
12210
- const match = /^(\d{1,4})-/.exec(entry);
12211
- if (match) max = Math.max(max, Number.parseInt(match[1], 10));
12212
- }
12213
- } catch {
12214
- }
12215
- return String(max + 1).padStart(4, "0");
12216
- }
12217
- var COMPONENTS = "../../../components";
12218
- var TEMPLATE_SPECS = {
12219
- adr: {
12220
- subdir: "adr",
12221
- filename: (slug2, ctx) => `${ctx.seq}-${slug2}`,
12222
- content: (ctx) => `---
12223
- title: 'ADR ${ctx.seq} \u2014 ${ctx.name}'
12224
- description: '${ctx.name}'
12225
- # Link the stories that prove this decision. The badge under the title turns
12226
- # red the moment any of them fail, so this record can't drift from the code.
12227
- verifiedBy: []
12228
- ---
12229
-
12230
- ## Status
12231
-
12232
- **Proposed** \u2014 proposed \xB7 accepted \xB7 superseded \xB7 deprecated
12233
-
12234
- ## Context
12235
-
12236
- _What problem are we solving? What constraints and forces apply?_
12237
-
12238
- ## Decision
12239
-
12240
- _What did we decide to do?_
12241
-
12242
- ## Consequences
12243
-
12244
- _What becomes easier, and what becomes harder, as a result?_
12245
-
12246
- ## Verified by
12247
-
12248
- Add the story ids or tags that exercise this decision to \`verifiedBy\` in the
12249
- frontmatter above. Until you do, the badge reads **Unverified** \u2014 by design.
12250
- `
12251
- },
12252
- runbook: {
12253
- subdir: "runbooks",
12254
- filename: (slug2) => slug2,
12255
- content: (ctx) => `---
12256
- title: 'Runbook \u2014 ${ctx.name}'
12257
- description: 'Operational runbook for ${ctx.name}'
12258
- ---
12259
-
12260
- import Checklist from '${COMPONENTS}/Checklist.astro';
12261
- import VerifiedStep from '${COMPONENTS}/VerifiedStep.astro';
12262
-
12263
- _When to use this runbook, prerequisites, and who to contact._
12264
-
12265
- ## Steps
12266
-
12267
- Each step linked with \`story=\` shows a live green check when its test passed in
12268
- the last run \u2014 so this runbook is trustworthy, not aspirational.
12269
-
12270
- <Checklist>
12271
- <VerifiedStep story="">Describe the first action, and link the story that verifies it.</VerifiedStep>
12272
- <VerifiedStep>A manual step with no automated check.</VerifiedStep>
12273
- </Checklist>
12274
-
12275
- ## Rollback
12276
-
12277
- _How to safely undo if something goes wrong._
12278
- `
12279
- },
12280
- "decision-log": {
12281
- subdir: "decisions",
12282
- filename: (slug2) => slug2,
12283
- content: (ctx) => `---
12284
- title: 'Decision log \u2014 ${ctx.name}'
12285
- description: 'Running log of decisions for ${ctx.name}'
12286
- ---
12287
-
12288
- A lightweight running log. For weightier decisions, scaffold a full ADR with
12289
- \`executable-stories new adr\`.
12290
-
12291
- | Date | Decision | Owner | Verified by |
12292
- | ---- | -------- | ----- | ----------- |
12293
- | ${ctx.isoDate} | _What was decided_ | _Who_ | _story id or tag_ |
12294
- `
12295
- },
12296
- incident: {
12297
- subdir: "incidents",
12298
- filename: (slug2, ctx) => `${ctx.isoDate}-${slug2}`,
12299
- content: (ctx) => `---
12300
- title: 'Incident \u2014 ${ctx.name}'
12301
- description: 'Post-mortem for ${ctx.name}'
12302
- # Link the regression story added to stop this recurring.
12303
- verifiedBy: []
12304
- ---
12305
-
12306
- ## Summary
12307
-
12308
- _What happened, who was affected, and for how long._
12309
-
12310
- ## Timeline
12311
-
12312
- | Time | Event |
12313
- | ---- | ----- |
12314
- | ${ctx.isoDate} | Detected |
12315
-
12316
- ## Root cause
12317
-
12318
- _The underlying cause, not just the trigger._
12319
-
12320
- ## Resolution
12321
-
12322
- _How it was fixed._
12323
-
12324
- ## Action items
12325
-
12326
- - [ ] Add a regression story and link it in \`verifiedBy\` so a silent recurrence
12327
- becomes a failing badge.
12328
- `
12329
- },
12330
- "scenario-note": {
12331
- subdir: "notes",
12332
- filename: (_slug, ctx) => ctx.scenarioId ?? ctx.slug,
12333
- content: (ctx) => `---
12334
- title: 'Business context \u2014 ${ctx.name}'
12335
- description: 'Stakeholder context for ${ctx.name}'
12336
- scenarioId: ${ctx.scenarioId}
12337
- # Link this note back to the scenario it explains so the badge and explorer stay aligned.
12338
- verifiedBy: [${ctx.scenarioId}]
12339
- ---
12340
-
12341
- This page is hand-written commentary for a generated scenario. The generated
12342
- stories render live from the run JSON, so this page is never overwritten.
12343
-
12344
- ## Why this behavior matters
12345
-
12346
- _Describe the business rule, policy, customer promise, or operational nuance._
12347
-
12348
- ## Caveats
12349
-
12350
- - _What readers should know when this scenario passes_
12351
- - _Any assumptions, exclusions, or follow-up links_
12352
- `
12353
- }
12354
- };
12355
- function isTemplateName(value) {
12356
- return TEMPLATES.includes(value);
12357
- }
12358
- function scaffoldDoc(options) {
12359
- const { template } = options;
12360
- if (!isTemplateName(template)) {
12361
- throw new Error(
12362
- `Unknown template "${template}". Available: ${TEMPLATES.join(", ")}.`
12363
- );
12364
- }
12365
- const spec = TEMPLATE_SPECS[template];
12366
- const baseDir = options.baseDir ?? path23.join("src", "content", "docs");
12367
- const today = options.today ?? /* @__PURE__ */ new Date();
12368
- const name = (options.name ?? "").trim() || defaultName(template);
12369
- const slug2 = slugify2(name);
12370
- const scenarioId = normalizeScenarioId(options.scenarioId);
12371
- const dir = path23.join(baseDir, spec.subdir);
12372
- if (template === "scenario-note" && !scenarioId) {
12373
- throw new Error(`Template "scenario-note" requires --scenario-id.`);
12374
- }
12375
- const ctx = {
12376
- name,
12377
- slug: slug2,
12378
- scenarioId,
12379
- isoDate: isoDate(today),
12380
- seq: nextSeq(dir)
12381
- };
12382
- const filename = `${spec.filename(slug2, ctx)}.mdx`;
12383
- const filePath = path23.join(dir, filename);
12384
- if (fs17.existsSync(filePath) && !options.force) {
12385
- throw new Error(
12386
- `File "${filePath}" already exists. Use --force to overwrite.`
12387
- );
12388
- }
12389
- fs17.mkdirSync(dir, { recursive: true });
12390
- fs17.writeFileSync(filePath, spec.content(ctx), "utf8");
12391
- return { template, path: filePath, title: titleFor2(template, ctx) };
12392
- }
12393
- function defaultName(template) {
12394
- switch (template) {
12395
- case "adr":
12396
- return "Untitled decision";
12397
- case "runbook":
12398
- return "Untitled runbook";
12399
- case "decision-log":
12400
- return "Decisions";
12401
- case "incident":
12402
- return "Untitled incident";
12403
- case "scenario-note":
12404
- return "Untitled scenario note";
12405
- }
12406
- }
12407
- function titleFor2(template, ctx) {
12408
- switch (template) {
12409
- case "adr":
12410
- return `ADR ${ctx.seq} \u2014 ${ctx.name}`;
12411
- case "runbook":
12412
- return `Runbook \u2014 ${ctx.name}`;
12413
- case "decision-log":
12414
- return `Decision log \u2014 ${ctx.name}`;
12415
- case "incident":
12416
- return `Incident \u2014 ${ctx.name}`;
12417
- case "scenario-note":
12418
- return `Business context \u2014 ${ctx.name}`;
12419
- }
12420
- }
12421
- function normalizeScenarioId(input) {
12422
- const value = input?.trim();
12423
- if (!value) return void 0;
12424
- if (value.includes("/") || value.includes("\\")) {
12425
- throw new Error(`scenarioId must not contain path separators.`);
12426
- }
12427
- return value;
12428
- }
12429
-
12430
12139
  // src/summary-line.ts
12431
12140
  function summaryLine(counts, files, durationMs, options = {}) {
12432
12141
  const { passed, failed, skipped, pending } = counts;
@@ -12456,8 +12165,8 @@ function summaryLine(counts, files, durationMs, options = {}) {
12456
12165
  }
12457
12166
 
12458
12167
  // src/sync/run.ts
12459
- import * as fs18 from "fs";
12460
- import * as path24 from "path";
12168
+ import * as fs15 from "fs";
12169
+ import * as path21 from "path";
12461
12170
  import { parseArgs as parseArgs3 } from "util";
12462
12171
  import { canonicalizeRun as canonicalizeRun6 } from "executable-stories-core/converters/acl/canonicalize";
12463
12172
  import { synthesizeStories as synthesizeStories4 } from "executable-stories-core/converters/synthesize";
@@ -12512,11 +12221,11 @@ pull request that created the case.
12512
12221
  Exit codes: 0 applied (or planned), 1 some writes failed, 4 usage error.`;
12513
12222
  function defaultDeps3() {
12514
12223
  return {
12515
- readFile: (filePath) => fs18.readFileSync(filePath, "utf8"),
12516
- fileExists: (filePath) => fs18.existsSync(filePath),
12224
+ readFile: (filePath) => fs15.readFileSync(filePath, "utf8"),
12225
+ fileExists: (filePath) => fs15.existsSync(filePath),
12517
12226
  writeFile: (filePath, contents) => {
12518
- fs18.mkdirSync(path24.dirname(path24.resolve(filePath)), { recursive: true });
12519
- fs18.writeFileSync(filePath, contents, "utf8");
12227
+ fs15.mkdirSync(path21.dirname(path21.resolve(filePath)), { recursive: true });
12228
+ fs15.writeFileSync(filePath, contents, "utf8");
12520
12229
  },
12521
12230
  fetchFn: globalThis.fetch,
12522
12231
  env: process.env,
@@ -12697,8 +12406,8 @@ async function runSyncCommand(mode, rawArgs, depsOverride = {}) {
12697
12406
  deps.error(`Could not read from ${providerName}: ${err instanceof Error ? err.message : String(err)}`);
12698
12407
  return EXIT_FAILED;
12699
12408
  }
12700
- const jsonPath = path24.join(outputDir, `sync-coverage.${providerName}.json`);
12701
- const markdownPath = path24.join(outputDir, `sync-coverage.${providerName}.md`);
12409
+ const jsonPath = path21.join(outputDir, `sync-coverage.${providerName}.json`);
12410
+ const markdownPath = path21.join(outputDir, `sync-coverage.${providerName}.md`);
12702
12411
  deps.writeFile(jsonPath, `${JSON.stringify(buildCoverageJson(analysis), null, 2)}
12703
12412
  `);
12704
12413
  deps.writeFile(markdownPath, `${renderCoverageMarkdown(analysis)}
@@ -13767,17 +13476,17 @@ function validateRawRun(data) {
13767
13476
  return { valid: true, errors: [] };
13768
13477
  }
13769
13478
  const errors = (validate.errors ?? []).map((err) => {
13770
- const path26 = err.instancePath || "/";
13479
+ const path23 = err.instancePath || "/";
13771
13480
  const message = err.message ?? "unknown error";
13772
13481
  if (err.keyword === "additionalProperties") {
13773
13482
  const extra = err.params.additionalProperty;
13774
- return `${path26}: ${message} \u2014 '${extra}'`;
13483
+ return `${path23}: ${message} \u2014 '${extra}'`;
13775
13484
  }
13776
13485
  if (err.keyword === "enum") {
13777
13486
  const allowed = err.params.allowedValues;
13778
- return `${path26}: ${message} \u2014 allowed: ${JSON.stringify(allowed)}`;
13487
+ return `${path23}: ${message} \u2014 allowed: ${JSON.stringify(allowed)}`;
13779
13488
  }
13780
- return `${path26}: ${message}`;
13489
+ return `${path23}: ${message}`;
13781
13490
  });
13782
13491
  return { valid: false, errors };
13783
13492
  }
@@ -13792,7 +13501,7 @@ import { toStoryReport as toStoryReport9 } from "executable-stories-core/convert
13792
13501
  function buildGhAttachCommand(args) {
13793
13502
  const assets = collectReportAssets2(toStoryReport9(args.run));
13794
13503
  if (assets.length === 0) return void 0;
13795
- const attachments = assets.map((path26) => `--attach '${path26}'`).join(" \\\n ");
13504
+ const attachments = assets.map((path23) => `--attach '${path23}'`).join(" \\\n ");
13796
13505
  return `gh pr comment ${args.pr ?? "<number>"} --body-file ${args.bodyFile} \\
13797
13506
  ${attachments}`;
13798
13507
  }
@@ -13833,15 +13542,11 @@ USAGE
13833
13542
  executable-stories triage <file|directory> [--baseline <path|auto>] [--triage-format text|json] [--by-owner]
13834
13543
  executable-stories validate <file>
13835
13544
  executable-stories validate --stdin
13836
- executable-stories dev [directory]
13837
- executable-stories init-astro [directory] [--install] [--force] [--update]
13838
- executable-stories new <template> "<name>" [options]
13839
13545
  executable-stories check-links <dir> [options]
13840
13546
  executable-stories push <run.json|results.xml|allure-results/> [--format <fmt>] [--title <text>] [--env <name>] [--description <text|@file.md>] [--gate] [--force]
13841
13547
  executable-stories share <reports-dir|report.json> [--emails <a@b,c@d>] [--expires-days <n>]
13842
13548
  executable-stories coverage <testrail|xray> <run.json> [options]
13843
13549
  executable-stories sync <testrail|xray> <run.json> [--apply] [options]
13844
- executable-stories import-openapi <spec> [options]
13845
13550
  executable-stories publish-confluence <file.adf.json> [options]
13846
13551
  executable-stories publish-jira <file.adf.json> [options]
13847
13552
  executable-stories deploy record <file> --env <env> [--tag <tag>] [options]
@@ -13863,27 +13568,24 @@ SUBCOMMANDS
13863
13568
  doctor Diagnose the run JSON: where it is, whether it parses, schema version vs this CLI, what it contains
13864
13569
  runs Inspect or clear the accumulated run state: "runs status", "runs reset"
13865
13570
  completion Output a shell completion script (bash, zsh, fish)
13866
- init-astro Scaffold a thin Astro docs site (Starlight + executable-stories-astro; live stories at /stories)
13867
- new Scaffold a docs page from a template (adr, runbook, decision-log, incident, scenario-note)
13868
13571
  check-links Scan docs for broken internal/external links (CI-friendly exit code)
13869
13572
  push Send a run to a cloud ingest endpoint: StoryReport, raw run, JUnit XML, Playwright JSON or allure-results
13870
13573
  share Publish a report (with its screenshots and video) and print a link to it
13871
13574
  coverage Compare your stories against a test-management system (read-only)
13872
13575
  sync Push cases, executions, and evidence to TestRail or Xray (dry run by default)
13873
- import-openapi Generate API doc pages from an OpenAPI spec, linked to verifying stories
13874
13576
  publish-confluence Publish an ADF JSON file to a Confluence page via REST API
13875
13577
  publish-jira Publish an ADF JSON file to a Jira issue (as comment or description)
13876
13578
  deploy Record deployments, show environment status, detect drift
13877
13579
 
13878
13580
  OPTIONS
13879
13581
  --format <formats> Comma-separated formats: html, markdown, release-manifest, traceability-matrix, traceability-csv, junit, cucumber-json, cucumber-messages, cucumber-html, astro-markdown, confluence, story-report-json, scenario-index-json, behavior-manifest-json, agent-text, span-graph, or custom names from config (default: html)
13880
- astro-markdown Starlight-flavored Markdown (single aggregated page; for a live site use "init-astro" + "astro dev")
13582
+ astro-markdown Starlight-flavored Markdown (single aggregated page)
13881
13583
  confluence Atlassian Document Format (ADF) JSON for Confluence / Jira
13882
13584
  behavior-manifest-json Agent-readable behavior manifest and debugger warnings
13883
13585
  agent-text Full run as flat token-lean plain text for pasting into an LLM
13884
13586
  span-graph Architecture the run exercised, from its OTel spans, as mermaid.
13885
13587
  Writes nothing when the run carries no spans
13886
- html Standalone interactive HTML report, rendered via executable-stories-react (same component tree as the Astro site)
13588
+ html Standalone interactive HTML report, rendered via executable-stories-react
13887
13589
  cucumber-html Official Cucumber HTML report
13888
13590
  markdown Markdown documentation
13889
13591
  junit JUnit XML
@@ -14007,6 +13709,14 @@ TRIAGE
14007
13709
  with no covers are flagged. --triage-format json emits the work queue. triage
14008
13710
  always exits 0 \u2014 it reports work, it does not gate.
14009
13711
 
13712
+ JEV (optional)
13713
+ With JEV_API_KEY set, triage, goal, and review ask Jev (TypeSafe AI) the
13714
+ bounded questions their rules leave blank: triage suggests a covers path and
13715
+ a failure kind (product/test/infra) for unrouted failures; goal flags
13716
+ rewritten scenarios that check less as advisories; review infers change-type
13717
+ for claims with no change:* tag. Every answer carries its probability and
13718
+ none of them changes an exit code. JEV_MODEL / JEV_ENDPOINT override defaults.
13719
+
14010
13720
  COMPARE
14011
13721
  compare supports --format html,markdown,changelog
14012
13722
  changelog writes a release-notes-style behavior changelog (<output-name>.changelog.md)
@@ -14030,13 +13740,6 @@ DEPLOY
14030
13740
  executable-stories deploy diff <env-a> <env-b> [--ledger <path>]
14031
13741
  Show scenario drift between two environments (what's in one but not the other).
14032
13742
 
14033
- INIT-ASTRO
14034
- executable-stories dev [directory] Run the live docs site (default: ./story-docs); installs its deps on first use
14035
- executable-stories init-astro [directory] Scaffold into directory (default: ./story-docs)
14036
- --install Also run the package manager install (detected from your lockfile) so the site is ready to \`dev\`
14037
- --force Write into a non-empty directory (overlays template files)
14038
- --update Refresh framework files only (keeps your content + config)
14039
-
14040
13743
  PUBLISH-CONFLUENCE
14041
13744
  executable-stories publish-confluence <file.adf.json> [options]
14042
13745
  --page-id <id> Update an existing page (alternative to --space-id)
@@ -14183,19 +13886,9 @@ async function parseCliArgs(argv) {
14183
13886
  process.exit(EXIT_SUCCESS4);
14184
13887
  }
14185
13888
  const subcommand = args[0];
14186
- if (subcommand !== "format" && subcommand !== "watch" && subcommand !== "compare" && subcommand !== "gate-release" && subcommand !== "deploy" && subcommand !== "review" && subcommand !== "list" && subcommand !== "check" && subcommand !== "check-explainers" && subcommand !== "goal" && subcommand !== "triage" && subcommand !== "validate" && subcommand !== "doctor" && subcommand !== "completion" && subcommand !== "dev" && subcommand !== "init-astro" && subcommand !== "new" && subcommand !== "check-links" && subcommand !== "push" && subcommand !== "share" && subcommand !== "import-openapi" && subcommand !== "publish-confluence" && subcommand !== "publish-jira" && subcommand !== "sync" && subcommand !== "coverage" && subcommand !== "runs") {
14187
- if (subcommand === "serve" || subcommand === "build-docs") {
14188
- console.error(
14189
- `The "${subcommand}" subcommand was removed. Living docs are now an Astro site, rendered live from the run JSON (no Markdown generation step):
14190
- 1. executable-stories init-astro --install (one-time scaffold)
14191
- 2. run your tests in watch mode in one terminal
14192
- 3. run \`executable-stories dev\` in another \u2014 it hot-reloads the docs.
14193
- See: https://github.com/jagreehal/executable-stories (executable-stories-astro).`
14194
- );
14195
- process.exit(EXIT_USAGE4);
14196
- }
13889
+ if (subcommand !== "format" && subcommand !== "watch" && subcommand !== "compare" && subcommand !== "gate-release" && subcommand !== "deploy" && subcommand !== "review" && subcommand !== "list" && subcommand !== "check" && subcommand !== "check-explainers" && subcommand !== "goal" && subcommand !== "triage" && subcommand !== "validate" && subcommand !== "doctor" && subcommand !== "completion" && subcommand !== "check-links" && subcommand !== "push" && subcommand !== "share" && subcommand !== "publish-confluence" && subcommand !== "publish-jira" && subcommand !== "sync" && subcommand !== "coverage" && subcommand !== "runs") {
14197
13890
  console.error(
14198
- `Unknown subcommand: "${subcommand}". Use "format", "watch", "compare", "gate-release", "deploy", "review", "list", "check", "check-explainers", "goal", "triage", "validate", "doctor", "completion", "dev", "init-astro", "new", "check-links", "push", "share", "sync", "coverage", "import-openapi", "publish-confluence", or "publish-jira".`
13891
+ `Unknown subcommand: "${subcommand}". Use "format", "watch", "compare", "gate-release", "deploy", "review", "list", "check", "check-explainers", "goal", "triage", "validate", "doctor", "completion", "check-links", "push", "share", "sync", "coverage", "publish-confluence", or "publish-jira".`
14199
13892
  );
14200
13893
  process.exit(EXIT_USAGE4);
14201
13894
  }
@@ -14218,15 +13911,15 @@ See: https://github.com/jagreehal/executable-stories (executable-stories-astro).
14218
13911
  const outputDirFlag = args.indexOf("--output-dir");
14219
13912
  const outputDir = outputDirFlag >= 0 ? args[outputDirFlag + 1] : void 0;
14220
13913
  const deps = {
14221
- readFile: (filePath) => fs19.readFileSync(filePath, "utf8"),
13914
+ readFile: (filePath) => fs16.readFileSync(filePath, "utf8"),
14222
13915
  listDir: (dir) => {
14223
13916
  try {
14224
- return fs19.readdirSync(dir);
13917
+ return fs16.readdirSync(dir);
14225
13918
  } catch {
14226
13919
  return void 0;
14227
13920
  }
14228
13921
  },
14229
- removeFile: (filePath) => fs19.rmSync(filePath, { force: true }),
13922
+ removeFile: (filePath) => fs16.rmSync(filePath, { force: true }),
14230
13923
  logger: console
14231
13924
  };
14232
13925
  if (action === "status") {
@@ -14258,88 +13951,6 @@ See: https://github.com/jagreehal/executable-stories (executable-stories-astro).
14258
13951
  if (subcommand === "deploy") {
14259
13952
  process.exit(await runDeploy(args.slice(1)));
14260
13953
  }
14261
- if (subcommand === "dev") {
14262
- const devArgs = args.slice(1);
14263
- const siteDir = devArgs.find((a) => !a.startsWith("--")) ?? "./story-docs";
14264
- const dev = runDocsDev(siteDir);
14265
- if (dev.kind === "not-scaffolded") {
14266
- console.error(
14267
- `No docs site found at ${siteDir}. Create one (scaffold + install) with:
14268
- npx executable-stories init-astro --install`
14269
- );
14270
- process.exit(EXIT_USAGE4);
14271
- }
14272
- if (dev.kind === "install-failed") {
14273
- console.error(
14274
- `"${dev.pm} install" failed in ${siteDir} \u2014 run it manually, then retry.`
14275
- );
14276
- process.exit(EXIT_GENERATION);
14277
- }
14278
- process.exit(dev.status ?? EXIT_GENERATION);
14279
- }
14280
- if (subcommand === "init-astro") {
14281
- const initArgs = args.slice(1);
14282
- const targetDir = initArgs.find((a) => !a.startsWith("--")) ?? "./story-docs";
14283
- const force = initArgs.includes("--force");
14284
- const update = initArgs.includes("--update");
14285
- const install = initArgs.includes("--install");
14286
- try {
14287
- const result = initAstro({ targetDir, force, update });
14288
- if (update) {
14289
- console.log(
14290
- `Updated ${result.targetDir} (content + config left untouched)`
14291
- );
14292
- console.log(
14293
- " Framework updates come via: pnpm update executable-stories-astro"
14294
- );
14295
- process.exit(EXIT_SUCCESS4);
14296
- }
14297
- console.log(`Scaffolded Astro docs site at ${result.targetDir}`);
14298
- const pm = detectPackageManager();
14299
- if (install) {
14300
- console.log(`Installing dependencies with ${pm}\u2026`);
14301
- if (!installScaffoldDependencies(result.targetDir, pm)) {
14302
- console.error(
14303
- `Scaffold complete, but "${pm} install" failed in ${result.targetDir} \u2014 run it manually, then \`${pm} run dev\`.`
14304
- );
14305
- process.exit(EXIT_GENERATION);
14306
- }
14307
- }
14308
- console.log("");
14309
- console.log("Next steps:");
14310
- let step = 1;
14311
- if (!install) {
14312
- console.log(` ${step++}. cd ${result.targetDir} && ${pm} install`);
14313
- }
14314
- console.log(
14315
- ` ${step++}. In your TEST project, add the StoryReporter with a rawRunPath, e.g.`
14316
- );
14317
- console.log(
14318
- " StoryReporter({ rawRunPath: 'reports/raw-run.json' })"
14319
- );
14320
- console.log(
14321
- ` ${step++}. Run your tests in watch mode (terminal 1): ${pm} test --watch`
14322
- );
14323
- console.log(
14324
- ` ${step++}. Run the docs dev server (terminal 2): npx executable-stories dev`
14325
- );
14326
- console.log(
14327
- " Editing tests hot-reloads the Stories pages \u2014 nothing is written to disk."
14328
- );
14329
- console.log("");
14330
- console.log(
14331
- "Everything is configured in one file: executable-stories.config.mjs"
14332
- );
14333
- console.log(
14334
- " \u2014 sources, scenario selection (include/exclude), grouping (groupBy), docs, and theme."
14335
- );
14336
- process.exit(EXIT_SUCCESS4);
14337
- } catch (err) {
14338
- console.error(`Error: ${err.message}`);
14339
- process.exit(EXIT_USAGE4);
14340
- }
14341
- }
14342
- if (subcommand === "new") process.exit(runNew(args.slice(1)));
14343
13954
  if (subcommand === "check-links")
14344
13955
  process.exit(await runCheckLinks(args.slice(1)));
14345
13956
  if (subcommand === "push") process.exit(await runPush(args.slice(1)));
@@ -14348,8 +13959,6 @@ See: https://github.com/jagreehal/executable-stories (executable-stories-astro).
14348
13959
  process.exit(await runSyncCommand("sync", args.slice(1)));
14349
13960
  if (subcommand === "coverage")
14350
13961
  process.exit(await runSyncCommand("coverage", args.slice(1)));
14351
- if (subcommand === "import-openapi")
14352
- process.exit(await runImportOpenApi(args.slice(1)));
14353
13962
  const { values, positionals, tokens } = parseArgs4({
14354
13963
  args: args.slice(1),
14355
13964
  options: CLI_OPTIONS,
@@ -14467,7 +14076,7 @@ See: https://github.com/jagreehal/executable-stories (executable-stories-astro).
14467
14076
  const requestedFormats = preset.formats;
14468
14077
  if (requestedFormats.includes("astro")) {
14469
14078
  console.warn(
14470
- "\u26A0 The 'astro' format was renamed to 'astro-markdown' \u2014 it emits Starlight Markdown, not the\n executable-stories-astro live integration. '--format astro' still works but will be removed in a\n future major; use 'astro-markdown', or scaffold a live site with `init-astro` + `astro dev`."
14079
+ "\u26A0 The 'astro' format was renamed to 'astro-markdown'. '--format astro' still works but will be\n removed in a future major; use 'astro-markdown'."
14471
14080
  );
14472
14081
  }
14473
14082
  const allRequestedFormats = requestedFormats.map(
@@ -14719,30 +14328,30 @@ async function readInput2(args) {
14719
14328
  if (args.stdin) {
14720
14329
  return readStdin();
14721
14330
  }
14722
- const filePath = path25.resolve(args.inputFile);
14723
- if (!fs19.existsSync(filePath)) {
14331
+ const filePath = path22.resolve(args.inputFile);
14332
+ if (!fs16.existsSync(filePath)) {
14724
14333
  console.error(`Error: File not found: ${filePath}`);
14725
14334
  process.exit(EXIT_USAGE4);
14726
14335
  }
14727
- return fs19.readFileSync(filePath, "utf8");
14336
+ return fs16.readFileSync(filePath, "utf8");
14728
14337
  }
14729
14338
  function validateReportsDirectory(args) {
14730
14339
  const input = args.inputFile;
14731
14340
  if (!input || args.stdin) return void 0;
14732
- const resolved = path25.resolve(input);
14733
- if (!fs19.existsSync(resolved) || !fs19.statSync(resolved).isDirectory())
14341
+ const resolved = path22.resolve(input);
14342
+ if (!fs16.existsSync(resolved) || !fs16.statSync(resolved).isDirectory())
14734
14343
  return void 0;
14735
- const names = fs19.readdirSync(resolved).filter((n) => n.endsWith(".json")).sort();
14344
+ const names = fs16.readdirSync(resolved).filter((n) => n.endsWith(".json")).sort();
14736
14345
  if (names.length === 0) {
14737
14346
  console.error(`Error: no per-file reports (*.json) in ${input}.`);
14738
14347
  return EXIT_USAGE4;
14739
14348
  }
14740
14349
  const problems = [];
14741
14350
  for (const name of names) {
14742
- const filePath = path25.join(resolved, name);
14351
+ const filePath = path22.join(resolved, name);
14743
14352
  try {
14744
14353
  const run = JSON.parse(
14745
- fs19.readFileSync(filePath, "utf8")
14354
+ fs16.readFileSync(filePath, "utf8")
14746
14355
  );
14747
14356
  const result = validateCanonicalRun2(run);
14748
14357
  if (!result.valid) {
@@ -14766,16 +14375,16 @@ function readAggregateInput(args) {
14766
14375
  const input = args.inputFile;
14767
14376
  if (!input || args.stdin) return void 0;
14768
14377
  if (args.subcommand === "validate") return void 0;
14769
- const resolved = path25.resolve(input);
14770
- if (!fs19.existsSync(resolved) || !fs19.statSync(resolved).isDirectory())
14378
+ const resolved = path22.resolve(input);
14379
+ if (!fs16.existsSync(resolved) || !fs16.statSync(resolved).isDirectory())
14771
14380
  return void 0;
14772
14381
  const result = aggregateReports(
14773
14382
  { dir: resolved.replace(/\\/g, "/") },
14774
14383
  {
14775
- readFile: (p) => fs19.readFileSync(p, "utf8"),
14384
+ readFile: (p) => fs16.readFileSync(p, "utf8"),
14776
14385
  listDir: (dir) => {
14777
14386
  try {
14778
- return fs19.readdirSync(dir);
14387
+ return fs16.readdirSync(dir);
14779
14388
  } catch {
14780
14389
  return void 0;
14781
14390
  }
@@ -14797,19 +14406,19 @@ async function readRunInput(args) {
14797
14406
  return normalizeRunFromText(await readInput2(args), args).run;
14798
14407
  }
14799
14408
  function readFileInput(filePath) {
14800
- const resolved = path25.resolve(filePath);
14801
- if (!fs19.existsSync(resolved)) {
14409
+ const resolved = path22.resolve(filePath);
14410
+ if (!fs16.existsSync(resolved)) {
14802
14411
  console.error(`Error: File not found: ${resolved}`);
14803
14412
  process.exit(EXIT_USAGE4);
14804
14413
  }
14805
- return fs19.readFileSync(resolved, "utf8");
14414
+ return fs16.readFileSync(resolved, "utf8");
14806
14415
  }
14807
14416
  function readStdin() {
14808
- return new Promise((resolve13, reject) => {
14417
+ return new Promise((resolve12, reject) => {
14809
14418
  const chunks = [];
14810
14419
  process.stdin.setEncoding("utf8");
14811
14420
  process.stdin.on("data", (chunk) => chunks.push(chunk));
14812
- process.stdin.on("end", () => resolve13(chunks.join("")));
14421
+ process.stdin.on("end", () => resolve12(chunks.join("")));
14813
14422
  process.stdin.on("error", reject);
14814
14423
  });
14815
14424
  }
@@ -14957,16 +14566,16 @@ function tryNormalizeRunFromText(text2, args) {
14957
14566
  }
14958
14567
  }
14959
14568
  function listBaselineCandidates(currentFile, args) {
14960
- const baselineDir = path25.resolve(
14961
- args.baselineDir ?? path25.dirname(currentFile)
14569
+ const baselineDir = path22.resolve(
14570
+ args.baselineDir ?? path22.dirname(currentFile)
14962
14571
  );
14963
- const currentResolved = path25.resolve(currentFile);
14964
- if (!fs19.existsSync(baselineDir)) {
14572
+ const currentResolved = path22.resolve(currentFile);
14573
+ if (!fs16.existsSync(baselineDir)) {
14965
14574
  console.error(`Error: baseline directory not found: ${baselineDir}`);
14966
14575
  process.exit(EXIT_USAGE4);
14967
14576
  }
14968
- const entries = fs19.readdirSync(baselineDir, { withFileTypes: true });
14969
- return entries.filter((entry) => entry.isFile()).map((entry) => path25.join(baselineDir, entry.name)).filter((candidate) => path25.resolve(candidate) !== currentResolved).filter(
14577
+ const entries = fs16.readdirSync(baselineDir, { withFileTypes: true });
14578
+ return entries.filter((entry) => entry.isFile()).map((entry) => path22.join(baselineDir, entry.name)).filter((candidate) => path22.resolve(candidate) !== currentResolved).filter(
14970
14579
  (candidate) => args.inputType === "ndjson" ? candidate.endsWith(".ndjson") : candidate.endsWith(".json")
14971
14580
  );
14972
14581
  }
@@ -14975,7 +14584,7 @@ function resolveBaselineAuto(currentFile, currentRun, args) {
14975
14584
  const comparable = [];
14976
14585
  for (const candidate of candidates) {
14977
14586
  const run = tryNormalizeRunFromText(
14978
- fs19.readFileSync(candidate, "utf8"),
14587
+ fs16.readFileSync(candidate, "utf8"),
14979
14588
  args
14980
14589
  );
14981
14590
  if (run) {
@@ -14984,7 +14593,7 @@ function resolveBaselineAuto(currentFile, currentRun, args) {
14984
14593
  }
14985
14594
  if (comparable.length === 0) {
14986
14595
  console.error(
14987
- `Error: no compatible baseline files found in ${path25.resolve(args.baselineDir ?? path25.dirname(currentFile))}.`
14596
+ `Error: no compatible baseline files found in ${path22.resolve(args.baselineDir ?? path22.dirname(currentFile))}.`
14988
14597
  );
14989
14598
  process.exit(EXIT_USAGE4);
14990
14599
  }
@@ -15155,7 +14764,8 @@ async function runReview(ctx) {
15155
14764
  const { args } = ctx;
15156
14765
  const run = applySelection(await readRunInput(args), args);
15157
14766
  const context = loadReviewContext(args);
15158
- const review = buildReview(run, context);
14767
+ const built = buildReview(run, context);
14768
+ const review = await withJev(built, (jev) => enrichReview(built, jev));
15159
14769
  try {
15160
14770
  const files = writeReviewReport(review, args);
15161
14771
  for (const f of files) {
@@ -15239,7 +14849,7 @@ async function runCheckExplainers(ctx) {
15239
14849
  );
15240
14850
  process.exit(EXIT_USAGE4);
15241
14851
  }
15242
- if (!fs19.existsSync(args.explainersDir) || !fs19.statSync(args.explainersDir).isDirectory()) {
14852
+ if (!fs16.existsSync(args.explainersDir) || !fs16.statSync(args.explainersDir).isDirectory()) {
15243
14853
  console.error(
15244
14854
  `Error: --explainers-dir "${args.explainersDir}" is not a directory.`
15245
14855
  );
@@ -15253,30 +14863,39 @@ async function runCheckExplainers(ctx) {
15253
14863
  }
15254
14864
  process.exit(EXIT_SUCCESS4);
15255
14865
  }
14866
+ async function withJev(report, enrich) {
14867
+ const jev = jevFromEnv();
14868
+ if (!jev) return report;
14869
+ try {
14870
+ return await enrich(jev);
14871
+ } catch (err) {
14872
+ console.error(`Jev unavailable, deterministic output only: ${err instanceof Error ? err.message : String(err)}`);
14873
+ return report;
14874
+ }
14875
+ }
15256
14876
  async function runGoal(ctx) {
15257
14877
  const { args } = ctx;
15258
14878
  const run = applySelection(await readRunInput(args), args);
15259
14879
  const baseline = resolveBaselineRun(args, run);
15260
- const report = buildGoal(
15261
- {
15262
- run,
15263
- baseline,
15264
- requireTags: args.requireTags,
15265
- requireTickets: args.requireTickets,
15266
- requireScenarios: args.requireScenarios,
15267
- enforceNoRegressions: args.noRegressions,
15268
- enforceRatchet: !args.noRatchet,
15269
- format: args.goalFormat
15270
- },
15271
- {}
15272
- );
14880
+ const goalArgs = {
14881
+ run,
14882
+ baseline,
14883
+ requireTags: args.requireTags,
14884
+ requireTickets: args.requireTickets,
14885
+ requireScenarios: args.requireScenarios,
14886
+ enforceNoRegressions: args.noRegressions,
14887
+ enforceRatchet: !args.noRatchet,
14888
+ format: args.goalFormat
14889
+ };
14890
+ const built = buildGoal(goalArgs, {});
14891
+ const report = await withJev(built, (jev) => enrichGoal(built, goalArgs, jev));
15273
14892
  console.log(renderGoal(report, args.goalFormat));
15274
14893
  process.exit(report.met ? EXIT_SUCCESS4 : EXIT_AGENT_GATE);
15275
14894
  }
15276
14895
  function readCodeowners() {
15277
14896
  for (const candidate of ["CODEOWNERS", ".github/CODEOWNERS", "docs/CODEOWNERS"]) {
15278
- if (fs19.existsSync(candidate)) {
15279
- return parseCodeowners(fs19.readFileSync(candidate, "utf8"));
14897
+ if (fs16.existsSync(candidate)) {
14898
+ return parseCodeowners(fs16.readFileSync(candidate, "utf8"));
15280
14899
  }
15281
14900
  }
15282
14901
  console.error(
@@ -15289,7 +14908,7 @@ async function runTriage(ctx) {
15289
14908
  const run = applySelection(await readRunInput(args), args);
15290
14909
  const baseline = resolveBaselineStatusMap(args, run);
15291
14910
  const codeowners = args.byOwner ? readCodeowners() : void 0;
15292
- const report = buildTriage(
14911
+ const built = buildTriage(
15293
14912
  {
15294
14913
  testCases: run.testCases,
15295
14914
  baseline,
@@ -15298,6 +14917,7 @@ async function runTriage(ctx) {
15298
14917
  },
15299
14918
  {}
15300
14919
  );
14920
+ const report = await withJev(built, (jev) => enrichTriage(built, run.testCases, jev, codeowners));
15301
14921
  console.log(renderTriage(report, args.triageFormat, { byOwner: args.byOwner }));
15302
14922
  process.exit(EXIT_SUCCESS4);
15303
14923
  }
@@ -15396,9 +15016,9 @@ async function runFormatOrValidate(ctx) {
15396
15016
  process.exit(EXIT_SCHEMA_VALIDATION);
15397
15017
  }
15398
15018
  if (args.emitCanonical) {
15399
- const outPath = path25.resolve(args.emitCanonical);
15400
- fs19.mkdirSync(path25.dirname(outPath), { recursive: true });
15401
- fs19.writeFileSync(outPath, JSON.stringify(run, null, 2), "utf8");
15019
+ const outPath = path22.resolve(args.emitCanonical);
15020
+ fs16.mkdirSync(path22.dirname(outPath), { recursive: true });
15021
+ fs16.writeFileSync(outPath, JSON.stringify(run, null, 2), "utf8");
15402
15022
  }
15403
15023
  try {
15404
15024
  const history = runHistoryPipeline(run, args);
@@ -15462,9 +15082,9 @@ ${msg}`);
15462
15082
  }
15463
15083
  const run = data;
15464
15084
  if (args.emitCanonical) {
15465
- const outPath = path25.resolve(args.emitCanonical);
15466
- fs19.mkdirSync(path25.dirname(outPath), { recursive: true });
15467
- fs19.writeFileSync(outPath, JSON.stringify(run, null, 2), "utf8");
15085
+ const outPath = path22.resolve(args.emitCanonical);
15086
+ fs16.mkdirSync(path22.dirname(outPath), { recursive: true });
15087
+ fs16.writeFileSync(outPath, JSON.stringify(run, null, 2), "utf8");
15468
15088
  }
15469
15089
  try {
15470
15090
  const history = runHistoryPipeline(run, args);
@@ -15523,9 +15143,9 @@ ${msg}`);
15523
15143
  process.exit(EXIT_CANONICAL_VALIDATION);
15524
15144
  }
15525
15145
  if (args.emitCanonical) {
15526
- const outPath = path25.resolve(args.emitCanonical);
15527
- fs19.mkdirSync(path25.dirname(outPath), { recursive: true });
15528
- fs19.writeFileSync(outPath, JSON.stringify(canonical, null, 2), "utf8");
15146
+ const outPath = path22.resolve(args.emitCanonical);
15147
+ fs16.mkdirSync(path22.dirname(outPath), { recursive: true });
15148
+ fs16.writeFileSync(outPath, JSON.stringify(canonical, null, 2), "utf8");
15529
15149
  }
15530
15150
  try {
15531
15151
  const history = runHistoryPipeline(canonical, args);
@@ -15560,9 +15180,9 @@ function runCustomFormatters(run, customRequested, formatters, args) {
15560
15180
  const ext = formatter.fileExtension ?? formatName;
15561
15181
  const baseName = args.outputName ?? "report";
15562
15182
  const filename = args.outputNameTimestamp ? `${baseName}-${Math.floor(run.startedAtMs / 1e3)}.${ext}` : `${baseName}.${ext}`;
15563
- const filepath = path25.join(outputDir, filename);
15564
- fs19.mkdirSync(outputDir, { recursive: true });
15565
- fs19.writeFileSync(filepath, content, "utf8");
15183
+ const filepath = path22.join(outputDir, filename);
15184
+ fs16.mkdirSync(outputDir, { recursive: true });
15185
+ fs16.writeFileSync(filepath, content, "utf8");
15566
15186
  console.log(`Generated: ${filepath}`);
15567
15187
  } catch (err) {
15568
15188
  console.error(
@@ -15616,13 +15236,13 @@ async function dispatchNotifications(run, args) {
15616
15236
  }
15617
15237
  function runHistoryPipeline(run, args) {
15618
15238
  if (!args.historyFile) return void 0;
15619
- const historyPath = path25.resolve(args.historyFile);
15239
+ const historyPath = path22.resolve(args.historyFile);
15620
15240
  const store = loadHistory(
15621
15241
  { filePath: historyPath },
15622
15242
  {
15623
15243
  readFile: (p) => {
15624
15244
  try {
15625
- return fs19.readFileSync(p, "utf8");
15245
+ return fs16.readFileSync(p, "utf8");
15626
15246
  } catch {
15627
15247
  return void 0;
15628
15248
  }
@@ -15635,12 +15255,12 @@ function runHistoryPipeline(run, args) {
15635
15255
  run,
15636
15256
  maxRuns: args.maxHistoryRuns
15637
15257
  });
15638
- const dir = path25.dirname(historyPath);
15639
- fs19.mkdirSync(dir, { recursive: true });
15258
+ const dir = path22.dirname(historyPath);
15259
+ fs16.mkdirSync(dir, { recursive: true });
15640
15260
  saveHistory(
15641
15261
  { filePath: historyPath, store: updated },
15642
15262
  {
15643
- writeFile: (p, content) => fs19.writeFileSync(p, content, "utf8")
15263
+ writeFile: (p, content) => fs16.writeFileSync(p, content, "utf8")
15644
15264
  }
15645
15265
  );
15646
15266
  let metricsCount = 0;
@@ -15706,7 +15326,7 @@ async function generateReports(run, args, historyStore, _droppedMissingStory = 0
15706
15326
  files.push(...paths);
15707
15327
  (EXECUTION_ONLY_FORMATS.has(format) ? executionFiles : documentationFiles).push(...paths);
15708
15328
  }
15709
- const createdArtifactsReadme = writeArtifactsReadme(args.outputDir);
15329
+ writeArtifactsReadme(args.outputDir);
15710
15330
  const documented = generator.renderedRun ?? run;
15711
15331
  const wroteDocumentation = args.formats.some(
15712
15332
  (f) => !EXECUTION_ONLY_FORMATS.has(f)
@@ -15740,7 +15360,6 @@ async function generateReports(run, args, historyStore, _droppedMissingStory = 0
15740
15360
  return {
15741
15361
  files,
15742
15362
  counts,
15743
- createdArtifactsReadme,
15744
15363
  // A run that traced itself can draw the architecture it exercised, and
15745
15364
  // nobody discovers a format they have never seen named.
15746
15365
  spansUnused: !args.formats.includes("span-graph") && !args.htmlArchitecture && documented.testCases.some((tc) => (tc.story.otelSpans?.length ?? 0) > 0),
@@ -15885,13 +15504,13 @@ function writeReviewReport(review, args) {
15885
15504
  const outputDir = args.outputDir ?? "reports";
15886
15505
  const baseName = args.outputName ?? "evidence-review";
15887
15506
  const suffix = args.outputNameTimestamp ? `-${Math.floor(review.run.startedAtMs / 1e3)}` : "";
15888
- fs19.mkdirSync(outputDir, { recursive: true });
15889
- const mdPath = path25.join(outputDir, `${baseName}${suffix}.md`);
15890
- const htmlPath = path25.join(outputDir, `${baseName}${suffix}.html`);
15891
- const jsonPath = path25.join(outputDir, `${baseName}${suffix}.review.json`);
15892
- fs19.writeFileSync(mdPath, markdown, "utf8");
15893
- fs19.writeFileSync(htmlPath, html, "utf8");
15894
- fs19.writeFileSync(
15507
+ fs16.mkdirSync(outputDir, { recursive: true });
15508
+ const mdPath = path22.join(outputDir, `${baseName}${suffix}.md`);
15509
+ const htmlPath = path22.join(outputDir, `${baseName}${suffix}.html`);
15510
+ const jsonPath = path22.join(outputDir, `${baseName}${suffix}.review.json`);
15511
+ fs16.writeFileSync(mdPath, markdown, "utf8");
15512
+ fs16.writeFileSync(htmlPath, html, "utf8");
15513
+ fs16.writeFileSync(
15895
15514
  jsonPath,
15896
15515
  `${JSON.stringify(buildReviewJson(review), null, 2)}
15897
15516
  `,
@@ -15987,11 +15606,6 @@ function printResult(result, args, startMs, droppedMissingStory = 0) {
15987
15606
  "Tip: this run carries OTel spans, so it can draw the architecture it exercised: --format span-graph (a file), --html-architecture (a section in the HTML report)"
15988
15607
  );
15989
15608
  }
15990
- if (result.createdArtifactsReadme && !isScaffoldedAstroSite(".") && !isScaffoldedAstroSite("./story-docs")) {
15991
- console.error(
15992
- "Tip: for a live docs site (stories, explainers, freshness): npx executable-stories init-astro --install"
15993
- );
15994
- }
15995
15609
  }
15996
15610
  if (args.open) {
15997
15611
  openInBrowser(pickOpenTarget(result.files));
@@ -16000,9 +15614,9 @@ function printResult(result, args, startMs, droppedMissingStory = 0) {
16000
15614
  function printCompareResult(result, args, startMs) {
16001
15615
  const durationMs = Date.now() - startMs;
16002
15616
  if (result.prSummary && args.prSummaryFile) {
16003
- const outputPath = path25.resolve(args.prSummaryFile);
16004
- fs19.mkdirSync(path25.dirname(outputPath), { recursive: true });
16005
- fs19.writeFileSync(outputPath, result.prSummary, "utf8");
15617
+ const outputPath = path22.resolve(args.prSummaryFile);
15618
+ fs16.mkdirSync(path22.dirname(outputPath), { recursive: true });
15619
+ fs16.writeFileSync(outputPath, result.prSummary, "utf8");
16006
15620
  }
16007
15621
  if (args.jsonSummary) {
16008
15622
  console.log(
@@ -16036,13 +15650,13 @@ function printCompareResult(result, args, startMs) {
16036
15650
  }
16037
15651
  }
16038
15652
  function loadReleasePolicy(policyPath) {
16039
- const resolved = path25.resolve(policyPath);
16040
- if (!fs19.existsSync(resolved)) {
15653
+ const resolved = path22.resolve(policyPath);
15654
+ if (!fs16.existsSync(resolved)) {
16041
15655
  console.error(`Error: release policy file not found: ${resolved}`);
16042
15656
  process.exit(EXIT_USAGE4);
16043
15657
  }
16044
15658
  try {
16045
- const raw = JSON.parse(fs19.readFileSync(resolved, "utf8"));
15659
+ const raw = JSON.parse(fs16.readFileSync(resolved, "utf8"));
16046
15660
  return {
16047
15661
  allowedOmissions: Array.isArray(raw.allowedOmissions) ? raw.allowedOmissions : [],
16048
15662
  allowedRegressions: Array.isArray(raw.allowedRegressions) ? raw.allowedRegressions : [],
@@ -16148,7 +15762,7 @@ Generate an API token at https://id.atlassian.com/manage-profile/security/api-to
16148
15762
  );
16149
15763
  process.exit(EXIT_USAGE4);
16150
15764
  }
16151
- if (!fs19.existsSync(inputFile)) {
15765
+ if (!fs16.existsSync(inputFile)) {
16152
15766
  console.error(`Error: file not found: ${inputFile}`);
16153
15767
  process.exit(EXIT_USAGE4);
16154
15768
  }
@@ -16176,7 +15790,7 @@ Generate an API token at https://id.atlassian.com/manage-profile/security/api-to
16176
15790
  console.error("Error: --title is required when creating a new page");
16177
15791
  process.exit(EXIT_USAGE4);
16178
15792
  }
16179
- const adf = fs19.readFileSync(path25.resolve(inputFile), "utf8");
15793
+ const adf = fs16.readFileSync(path22.resolve(inputFile), "utf8");
16180
15794
  if (dryRun) {
16181
15795
  console.log(
16182
15796
  JSON.stringify(
@@ -16257,7 +15871,7 @@ Generate an API token at https://id.atlassian.com/manage-profile/security/api-to
16257
15871
  );
16258
15872
  process.exit(EXIT_USAGE4);
16259
15873
  }
16260
- if (!fs19.existsSync(inputFile)) {
15874
+ if (!fs16.existsSync(inputFile)) {
16261
15875
  console.error(`Error: file not found: ${inputFile}`);
16262
15876
  process.exit(EXIT_USAGE4);
16263
15877
  }
@@ -16284,7 +15898,7 @@ Generate an API token at https://id.atlassian.com/manage-profile/security/api-to
16284
15898
  process.exit(EXIT_USAGE4);
16285
15899
  }
16286
15900
  const mode = modeRaw;
16287
- const adf = fs19.readFileSync(path25.resolve(inputFile), "utf8");
15901
+ const adf = fs16.readFileSync(path22.resolve(inputFile), "utf8");
16288
15902
  if (dryRun) {
16289
15903
  console.log(
16290
15904
  JSON.stringify(
@@ -16325,46 +15939,6 @@ Generate an API token at https://id.atlassian.com/manage-profile/security/api-to
16325
15939
  process.exit(EXIT_GENERATION);
16326
15940
  }
16327
15941
  }
16328
- function runNew(rawArgs) {
16329
- const { values, positionals } = parseArgs4({
16330
- args: rawArgs,
16331
- options: {
16332
- dir: { type: "string" },
16333
- force: { type: "boolean", default: false },
16334
- "scenario-id": { type: "string" }
16335
- },
16336
- allowPositionals: true,
16337
- strict: true
16338
- });
16339
- const template = positionals[0];
16340
- const name = positionals.slice(1).join(" ");
16341
- if (!template) {
16342
- console.error(
16343
- `Usage: executable-stories new <template> "<name>" [--dir <docs-dir>] [--scenario-id <id>] [--force]`
16344
- );
16345
- console.error(`Templates: ${TEMPLATES.join(", ")}`);
16346
- return EXIT_USAGE4;
16347
- }
16348
- try {
16349
- const result = scaffoldDoc({
16350
- template,
16351
- name,
16352
- scenarioId: values["scenario-id"],
16353
- baseDir: values.dir,
16354
- force: values.force
16355
- });
16356
- console.log(`Created ${result.template}: ${result.path}`);
16357
- console.log(` Title: ${result.title}`);
16358
- console.log("");
16359
- console.log(
16360
- "Next: fill in the content and link verifying stories in `verifiedBy`."
16361
- );
16362
- return EXIT_SUCCESS4;
16363
- } catch (err) {
16364
- console.error(`Error: ${err.message}`);
16365
- return EXIT_USAGE4;
16366
- }
16367
- }
16368
15942
  async function runCheckLinks(rawArgs) {
16369
15943
  const { values, positionals } = parseArgs4({
16370
15944
  args: rawArgs,
@@ -16394,48 +15968,6 @@ async function runCheckLinks(rawArgs) {
16394
15968
  return EXIT_USAGE4;
16395
15969
  }
16396
15970
  }
16397
- async function runImportOpenApi(rawArgs) {
16398
- const { values, positionals } = parseArgs4({
16399
- args: rawArgs,
16400
- options: {
16401
- "output-dir": { type: "string" },
16402
- run: { type: "string" },
16403
- force: { type: "boolean", default: false }
16404
- },
16405
- allowPositionals: true,
16406
- strict: true
16407
- });
16408
- const spec = positionals[0];
16409
- if (!spec) {
16410
- console.error(
16411
- `Usage: executable-stories import-openapi <spec.json|yaml> [--output-dir <dir>] [--run <story-report.json>] [--force]`
16412
- );
16413
- return EXIT_USAGE4;
16414
- }
16415
- try {
16416
- const result = await importOpenApi({
16417
- specPath: spec,
16418
- outputDir: values["output-dir"],
16419
- runFile: values.run,
16420
- force: values.force
16421
- });
16422
- console.log(
16423
- `Generated ${result.pageCount} API page(s) at ${result.outputDir}`
16424
- );
16425
- console.log(
16426
- ` Covered endpoints: ${result.coveredCount} / ${result.endpointCount}`
16427
- );
16428
- if (result.uncoveredCount > 0) {
16429
- console.log(
16430
- ` \u26A0 ${result.uncoveredCount} endpoint(s) have no verifying story`
16431
- );
16432
- }
16433
- return EXIT_SUCCESS4;
16434
- } catch (err) {
16435
- console.error(`Error: ${err.message}`);
16436
- return EXIT_USAGE4;
16437
- }
16438
- }
16439
15971
  async function runDeploy(rawArgs) {
16440
15972
  const mode = rawArgs[0];
16441
15973
  if (!mode || !["record", "status", "diff"].includes(mode)) {