executable-stories-formatters 1.20.0 → 1.21.0

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,8 +11622,8 @@ 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
11629
  import { collectReportAssets, rewriteReportAssets } from "executable-stories-core/report-assets";
@@ -11767,11 +11657,11 @@ Options:
11767
11657
  Exit codes: 0 shared, 1 rejected or failed, 4 usage error.`;
11768
11658
  function defaultDeps2() {
11769
11659
  return {
11770
- readFile: (filePath) => fs15.readFileSync(filePath, "utf8"),
11771
- readBinary: (filePath) => new Uint8Array(fs15.readFileSync(filePath)),
11660
+ readFile: (filePath) => fs13.readFileSync(filePath, "utf8"),
11661
+ readBinary: (filePath) => new Uint8Array(fs13.readFileSync(filePath)),
11772
11662
  fileSize: (filePath) => {
11773
11663
  try {
11774
- const stat = fs15.statSync(filePath);
11664
+ const stat = fs13.statSync(filePath);
11775
11665
  return stat.isFile() ? stat.size : void 0;
11776
11666
  } catch {
11777
11667
  return void 0;
@@ -11779,7 +11669,7 @@ function defaultDeps2() {
11779
11669
  },
11780
11670
  listDir: (dirPath) => {
11781
11671
  try {
11782
- return fs15.readdirSync(dirPath);
11672
+ return fs13.readdirSync(dirPath);
11783
11673
  } catch {
11784
11674
  return void 0;
11785
11675
  }
@@ -11807,11 +11697,11 @@ var CONTENT_TYPES = {
11807
11697
  ".zip": "application/zip"
11808
11698
  };
11809
11699
  function contentTypeFor(filePath) {
11810
- return CONTENT_TYPES[path21.extname(filePath).toLowerCase()] ?? "application/octet-stream";
11700
+ return CONTENT_TYPES[path19.extname(filePath).toLowerCase()] ?? "application/octet-stream";
11811
11701
  }
11812
11702
  function resolveReport(inputPath, deps) {
11813
11703
  const entries = deps.listDir(inputPath);
11814
- const candidates = entries === void 0 ? [inputPath] : candidateNames(entries).map((name) => path21.join(inputPath, name));
11704
+ const candidates = entries === void 0 ? [inputPath] : candidateNames(entries).map((name) => path19.join(inputPath, name));
11815
11705
  if (candidates.length === 0) {
11816
11706
  throw new Error(
11817
11707
  "no report in it. Generate one first: executable-stories format <run.json> --format story-report-json --output-dir <dir> --output-name index"
@@ -11860,12 +11750,12 @@ function asStoryReport(data, filePath) {
11860
11750
  }
11861
11751
  }
11862
11752
  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)}`;
11753
+ const resolved = path19.resolve(reportDir, assetPath);
11754
+ const relative3 = path19.relative(reportDir, resolved);
11755
+ const inside = relative3 !== "" && !relative3.startsWith("..") && !path19.isAbsolute(relative3);
11756
+ const base = inside ? relative3.split(path19.sep).join("/") : `assets/${path19.basename(resolved)}`;
11867
11757
  let key = base;
11868
- const ext = path21.extname(base);
11758
+ const ext = path19.extname(base);
11869
11759
  const stem = base.slice(0, base.length - ext.length);
11870
11760
  for (let n = 2; taken.has(key); n++) key = `${stem}-${n}${ext}`;
11871
11761
  taken.add(key);
@@ -11877,7 +11767,7 @@ function planAssets(report, reportDir, deps) {
11877
11767
  const keyByPath = /* @__PURE__ */ new Map();
11878
11768
  const taken = /* @__PURE__ */ new Set();
11879
11769
  for (const assetPath of collectReportAssets(report)) {
11880
- const localPath = path21.resolve(reportDir, assetPath);
11770
+ const localPath = path19.resolve(reportDir, assetPath);
11881
11771
  const bytes = deps.fileSize(localPath);
11882
11772
  const key = keyFor(assetPath, reportDir, taken);
11883
11773
  keyByPath.set(assetPath, key);
@@ -11959,7 +11849,7 @@ async function runShare(rawArgs, depsOverride = {}) {
11959
11849
  deps.error(`Could not read ${inputPath}: ${err instanceof Error ? err.message : String(err)}`);
11960
11850
  return EXIT_USAGE2;
11961
11851
  }
11962
- const reportDir = path21.dirname(reportPath);
11852
+ const reportDir = path19.dirname(reportPath);
11963
11853
  const { assets, missing, keyByPath } = planAssets(report, reportDir, deps);
11964
11854
  for (const assetPath of missing) {
11965
11855
  deps.error(`Warning: ${assetPath} is missing, so it will not be in the share.`);
@@ -12041,7 +11931,7 @@ async function runShare(rawArgs, depsOverride = {}) {
12041
11931
  return EXIT_SUCCESS2;
12042
11932
  }
12043
11933
  const withAssets = assets.length === 1 ? "1 asset" : `${assets.length} assets`;
12044
- deps.log(`Shared ${path21.basename(reportPath)} (${withAssets}):`);
11934
+ deps.log(`Shared ${path19.basename(reportPath)} (${withAssets}):`);
12045
11935
  deps.log(` ${created.url}`);
12046
11936
  deps.log(
12047
11937
  emails.length > 0 ? ` Only ${emails.join(", ")} can open it, after signing in.` : " Anyone with the link can open it."
@@ -12050,13 +11940,13 @@ async function runShare(rawArgs, depsOverride = {}) {
12050
11940
  }
12051
11941
 
12052
11942
  // src/run-file.ts
12053
- import fs16 from "fs";
12054
- import path22 from "path";
11943
+ import fs14 from "fs";
11944
+ import path20 from "path";
12055
11945
  var DEFAULT_RUN_FILES = [".executable-stories/raw-run.json", "reports/raw-run.json"];
12056
11946
  var SUPPORTED_RAW_RUN_SCHEMA = 1;
12057
11947
  function findDefaultRunFile(cwd = process.cwd()) {
12058
11948
  for (const candidate of DEFAULT_RUN_FILES) {
12059
- if (fs16.existsSync(path22.resolve(cwd, candidate))) return candidate;
11949
+ if (fs14.existsSync(path20.resolve(cwd, candidate))) return candidate;
12060
11950
  }
12061
11951
  return void 0;
12062
11952
  }
@@ -12082,8 +11972,8 @@ function diagnoseRunFile(file, cwd = process.cwd()) {
12082
11972
  });
12083
11973
  return { checks, healthy: false };
12084
11974
  }
12085
- const abs = path22.resolve(cwd, resolved);
12086
- if (!fs16.existsSync(abs)) {
11975
+ const abs = path20.resolve(cwd, resolved);
11976
+ if (!fs14.existsSync(abs)) {
12087
11977
  checks.push({
12088
11978
  label: "run file",
12089
11979
  status: "fail",
@@ -12092,7 +11982,7 @@ function diagnoseRunFile(file, cwd = process.cwd()) {
12092
11982
  });
12093
11983
  return { checks, healthy: false };
12094
11984
  }
12095
- const stat = fs16.statSync(abs);
11985
+ const stat = fs14.statSync(abs);
12096
11986
  checks.push({
12097
11987
  label: "run file",
12098
11988
  status: "ok",
@@ -12100,7 +11990,7 @@ function diagnoseRunFile(file, cwd = process.cwd()) {
12100
11990
  });
12101
11991
  let parsed;
12102
11992
  try {
12103
- parsed = JSON.parse(fs16.readFileSync(abs, "utf8"));
11993
+ parsed = JSON.parse(fs14.readFileSync(abs, "utf8"));
12104
11994
  } catch (err) {
12105
11995
  checks.push({
12106
11996
  label: "json",
@@ -12187,246 +12077,6 @@ function formatDoctorReport(report) {
12187
12077
  return lines.join("\n");
12188
12078
  }
12189
12079
 
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
12080
  // src/summary-line.ts
12431
12081
  function summaryLine(counts, files, durationMs, options = {}) {
12432
12082
  const { passed, failed, skipped, pending } = counts;
@@ -12456,8 +12106,8 @@ function summaryLine(counts, files, durationMs, options = {}) {
12456
12106
  }
12457
12107
 
12458
12108
  // src/sync/run.ts
12459
- import * as fs18 from "fs";
12460
- import * as path24 from "path";
12109
+ import * as fs15 from "fs";
12110
+ import * as path21 from "path";
12461
12111
  import { parseArgs as parseArgs3 } from "util";
12462
12112
  import { canonicalizeRun as canonicalizeRun6 } from "executable-stories-core/converters/acl/canonicalize";
12463
12113
  import { synthesizeStories as synthesizeStories4 } from "executable-stories-core/converters/synthesize";
@@ -12512,11 +12162,11 @@ pull request that created the case.
12512
12162
  Exit codes: 0 applied (or planned), 1 some writes failed, 4 usage error.`;
12513
12163
  function defaultDeps3() {
12514
12164
  return {
12515
- readFile: (filePath) => fs18.readFileSync(filePath, "utf8"),
12516
- fileExists: (filePath) => fs18.existsSync(filePath),
12165
+ readFile: (filePath) => fs15.readFileSync(filePath, "utf8"),
12166
+ fileExists: (filePath) => fs15.existsSync(filePath),
12517
12167
  writeFile: (filePath, contents) => {
12518
- fs18.mkdirSync(path24.dirname(path24.resolve(filePath)), { recursive: true });
12519
- fs18.writeFileSync(filePath, contents, "utf8");
12168
+ fs15.mkdirSync(path21.dirname(path21.resolve(filePath)), { recursive: true });
12169
+ fs15.writeFileSync(filePath, contents, "utf8");
12520
12170
  },
12521
12171
  fetchFn: globalThis.fetch,
12522
12172
  env: process.env,
@@ -12697,8 +12347,8 @@ async function runSyncCommand(mode, rawArgs, depsOverride = {}) {
12697
12347
  deps.error(`Could not read from ${providerName}: ${err instanceof Error ? err.message : String(err)}`);
12698
12348
  return EXIT_FAILED;
12699
12349
  }
12700
- const jsonPath = path24.join(outputDir, `sync-coverage.${providerName}.json`);
12701
- const markdownPath = path24.join(outputDir, `sync-coverage.${providerName}.md`);
12350
+ const jsonPath = path21.join(outputDir, `sync-coverage.${providerName}.json`);
12351
+ const markdownPath = path21.join(outputDir, `sync-coverage.${providerName}.md`);
12702
12352
  deps.writeFile(jsonPath, `${JSON.stringify(buildCoverageJson(analysis), null, 2)}
12703
12353
  `);
12704
12354
  deps.writeFile(markdownPath, `${renderCoverageMarkdown(analysis)}
@@ -13767,17 +13417,17 @@ function validateRawRun(data) {
13767
13417
  return { valid: true, errors: [] };
13768
13418
  }
13769
13419
  const errors = (validate.errors ?? []).map((err) => {
13770
- const path26 = err.instancePath || "/";
13420
+ const path23 = err.instancePath || "/";
13771
13421
  const message = err.message ?? "unknown error";
13772
13422
  if (err.keyword === "additionalProperties") {
13773
13423
  const extra = err.params.additionalProperty;
13774
- return `${path26}: ${message} \u2014 '${extra}'`;
13424
+ return `${path23}: ${message} \u2014 '${extra}'`;
13775
13425
  }
13776
13426
  if (err.keyword === "enum") {
13777
13427
  const allowed = err.params.allowedValues;
13778
- return `${path26}: ${message} \u2014 allowed: ${JSON.stringify(allowed)}`;
13428
+ return `${path23}: ${message} \u2014 allowed: ${JSON.stringify(allowed)}`;
13779
13429
  }
13780
- return `${path26}: ${message}`;
13430
+ return `${path23}: ${message}`;
13781
13431
  });
13782
13432
  return { valid: false, errors };
13783
13433
  }
@@ -13792,7 +13442,7 @@ import { toStoryReport as toStoryReport9 } from "executable-stories-core/convert
13792
13442
  function buildGhAttachCommand(args) {
13793
13443
  const assets = collectReportAssets2(toStoryReport9(args.run));
13794
13444
  if (assets.length === 0) return void 0;
13795
- const attachments = assets.map((path26) => `--attach '${path26}'`).join(" \\\n ");
13445
+ const attachments = assets.map((path23) => `--attach '${path23}'`).join(" \\\n ");
13796
13446
  return `gh pr comment ${args.pr ?? "<number>"} --body-file ${args.bodyFile} \\
13797
13447
  ${attachments}`;
13798
13448
  }
@@ -13833,15 +13483,11 @@ USAGE
13833
13483
  executable-stories triage <file|directory> [--baseline <path|auto>] [--triage-format text|json] [--by-owner]
13834
13484
  executable-stories validate <file>
13835
13485
  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
13486
  executable-stories check-links <dir> [options]
13840
13487
  executable-stories push <run.json|results.xml|allure-results/> [--format <fmt>] [--title <text>] [--env <name>] [--description <text|@file.md>] [--gate] [--force]
13841
13488
  executable-stories share <reports-dir|report.json> [--emails <a@b,c@d>] [--expires-days <n>]
13842
13489
  executable-stories coverage <testrail|xray> <run.json> [options]
13843
13490
  executable-stories sync <testrail|xray> <run.json> [--apply] [options]
13844
- executable-stories import-openapi <spec> [options]
13845
13491
  executable-stories publish-confluence <file.adf.json> [options]
13846
13492
  executable-stories publish-jira <file.adf.json> [options]
13847
13493
  executable-stories deploy record <file> --env <env> [--tag <tag>] [options]
@@ -13863,27 +13509,24 @@ SUBCOMMANDS
13863
13509
  doctor Diagnose the run JSON: where it is, whether it parses, schema version vs this CLI, what it contains
13864
13510
  runs Inspect or clear the accumulated run state: "runs status", "runs reset"
13865
13511
  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
13512
  check-links Scan docs for broken internal/external links (CI-friendly exit code)
13869
13513
  push Send a run to a cloud ingest endpoint: StoryReport, raw run, JUnit XML, Playwright JSON or allure-results
13870
13514
  share Publish a report (with its screenshots and video) and print a link to it
13871
13515
  coverage Compare your stories against a test-management system (read-only)
13872
13516
  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
13517
  publish-confluence Publish an ADF JSON file to a Confluence page via REST API
13875
13518
  publish-jira Publish an ADF JSON file to a Jira issue (as comment or description)
13876
13519
  deploy Record deployments, show environment status, detect drift
13877
13520
 
13878
13521
  OPTIONS
13879
13522
  --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")
13523
+ astro-markdown Starlight-flavored Markdown (single aggregated page)
13881
13524
  confluence Atlassian Document Format (ADF) JSON for Confluence / Jira
13882
13525
  behavior-manifest-json Agent-readable behavior manifest and debugger warnings
13883
13526
  agent-text Full run as flat token-lean plain text for pasting into an LLM
13884
13527
  span-graph Architecture the run exercised, from its OTel spans, as mermaid.
13885
13528
  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)
13529
+ html Standalone interactive HTML report, rendered via executable-stories-react
13887
13530
  cucumber-html Official Cucumber HTML report
13888
13531
  markdown Markdown documentation
13889
13532
  junit JUnit XML
@@ -14007,6 +13650,14 @@ TRIAGE
14007
13650
  with no covers are flagged. --triage-format json emits the work queue. triage
14008
13651
  always exits 0 \u2014 it reports work, it does not gate.
14009
13652
 
13653
+ JEV (optional)
13654
+ With JEV_API_KEY set, triage, goal, and review ask Jev (TypeSafe AI) the
13655
+ bounded questions their rules leave blank: triage suggests a covers path and
13656
+ a failure kind (product/test/infra) for unrouted failures; goal flags
13657
+ rewritten scenarios that check less as advisories; review infers change-type
13658
+ for claims with no change:* tag. Every answer carries its probability and
13659
+ none of them changes an exit code. JEV_MODEL / JEV_ENDPOINT override defaults.
13660
+
14010
13661
  COMPARE
14011
13662
  compare supports --format html,markdown,changelog
14012
13663
  changelog writes a release-notes-style behavior changelog (<output-name>.changelog.md)
@@ -14030,13 +13681,6 @@ DEPLOY
14030
13681
  executable-stories deploy diff <env-a> <env-b> [--ledger <path>]
14031
13682
  Show scenario drift between two environments (what's in one but not the other).
14032
13683
 
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
13684
  PUBLISH-CONFLUENCE
14041
13685
  executable-stories publish-confluence <file.adf.json> [options]
14042
13686
  --page-id <id> Update an existing page (alternative to --space-id)
@@ -14183,19 +13827,9 @@ async function parseCliArgs(argv) {
14183
13827
  process.exit(EXIT_SUCCESS4);
14184
13828
  }
14185
13829
  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
- }
13830
+ 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
13831
  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".`
13832
+ `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
13833
  );
14200
13834
  process.exit(EXIT_USAGE4);
14201
13835
  }
@@ -14218,15 +13852,15 @@ See: https://github.com/jagreehal/executable-stories (executable-stories-astro).
14218
13852
  const outputDirFlag = args.indexOf("--output-dir");
14219
13853
  const outputDir = outputDirFlag >= 0 ? args[outputDirFlag + 1] : void 0;
14220
13854
  const deps = {
14221
- readFile: (filePath) => fs19.readFileSync(filePath, "utf8"),
13855
+ readFile: (filePath) => fs16.readFileSync(filePath, "utf8"),
14222
13856
  listDir: (dir) => {
14223
13857
  try {
14224
- return fs19.readdirSync(dir);
13858
+ return fs16.readdirSync(dir);
14225
13859
  } catch {
14226
13860
  return void 0;
14227
13861
  }
14228
13862
  },
14229
- removeFile: (filePath) => fs19.rmSync(filePath, { force: true }),
13863
+ removeFile: (filePath) => fs16.rmSync(filePath, { force: true }),
14230
13864
  logger: console
14231
13865
  };
14232
13866
  if (action === "status") {
@@ -14258,88 +13892,6 @@ See: https://github.com/jagreehal/executable-stories (executable-stories-astro).
14258
13892
  if (subcommand === "deploy") {
14259
13893
  process.exit(await runDeploy(args.slice(1)));
14260
13894
  }
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
13895
  if (subcommand === "check-links")
14344
13896
  process.exit(await runCheckLinks(args.slice(1)));
14345
13897
  if (subcommand === "push") process.exit(await runPush(args.slice(1)));
@@ -14348,8 +13900,6 @@ See: https://github.com/jagreehal/executable-stories (executable-stories-astro).
14348
13900
  process.exit(await runSyncCommand("sync", args.slice(1)));
14349
13901
  if (subcommand === "coverage")
14350
13902
  process.exit(await runSyncCommand("coverage", args.slice(1)));
14351
- if (subcommand === "import-openapi")
14352
- process.exit(await runImportOpenApi(args.slice(1)));
14353
13903
  const { values, positionals, tokens } = parseArgs4({
14354
13904
  args: args.slice(1),
14355
13905
  options: CLI_OPTIONS,
@@ -14467,7 +14017,7 @@ See: https://github.com/jagreehal/executable-stories (executable-stories-astro).
14467
14017
  const requestedFormats = preset.formats;
14468
14018
  if (requestedFormats.includes("astro")) {
14469
14019
  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`."
14020
+ "\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
14021
  );
14472
14022
  }
14473
14023
  const allRequestedFormats = requestedFormats.map(
@@ -14719,30 +14269,30 @@ async function readInput2(args) {
14719
14269
  if (args.stdin) {
14720
14270
  return readStdin();
14721
14271
  }
14722
- const filePath = path25.resolve(args.inputFile);
14723
- if (!fs19.existsSync(filePath)) {
14272
+ const filePath = path22.resolve(args.inputFile);
14273
+ if (!fs16.existsSync(filePath)) {
14724
14274
  console.error(`Error: File not found: ${filePath}`);
14725
14275
  process.exit(EXIT_USAGE4);
14726
14276
  }
14727
- return fs19.readFileSync(filePath, "utf8");
14277
+ return fs16.readFileSync(filePath, "utf8");
14728
14278
  }
14729
14279
  function validateReportsDirectory(args) {
14730
14280
  const input = args.inputFile;
14731
14281
  if (!input || args.stdin) return void 0;
14732
- const resolved = path25.resolve(input);
14733
- if (!fs19.existsSync(resolved) || !fs19.statSync(resolved).isDirectory())
14282
+ const resolved = path22.resolve(input);
14283
+ if (!fs16.existsSync(resolved) || !fs16.statSync(resolved).isDirectory())
14734
14284
  return void 0;
14735
- const names = fs19.readdirSync(resolved).filter((n) => n.endsWith(".json")).sort();
14285
+ const names = fs16.readdirSync(resolved).filter((n) => n.endsWith(".json")).sort();
14736
14286
  if (names.length === 0) {
14737
14287
  console.error(`Error: no per-file reports (*.json) in ${input}.`);
14738
14288
  return EXIT_USAGE4;
14739
14289
  }
14740
14290
  const problems = [];
14741
14291
  for (const name of names) {
14742
- const filePath = path25.join(resolved, name);
14292
+ const filePath = path22.join(resolved, name);
14743
14293
  try {
14744
14294
  const run = JSON.parse(
14745
- fs19.readFileSync(filePath, "utf8")
14295
+ fs16.readFileSync(filePath, "utf8")
14746
14296
  );
14747
14297
  const result = validateCanonicalRun2(run);
14748
14298
  if (!result.valid) {
@@ -14766,16 +14316,16 @@ function readAggregateInput(args) {
14766
14316
  const input = args.inputFile;
14767
14317
  if (!input || args.stdin) return void 0;
14768
14318
  if (args.subcommand === "validate") return void 0;
14769
- const resolved = path25.resolve(input);
14770
- if (!fs19.existsSync(resolved) || !fs19.statSync(resolved).isDirectory())
14319
+ const resolved = path22.resolve(input);
14320
+ if (!fs16.existsSync(resolved) || !fs16.statSync(resolved).isDirectory())
14771
14321
  return void 0;
14772
14322
  const result = aggregateReports(
14773
14323
  { dir: resolved.replace(/\\/g, "/") },
14774
14324
  {
14775
- readFile: (p) => fs19.readFileSync(p, "utf8"),
14325
+ readFile: (p) => fs16.readFileSync(p, "utf8"),
14776
14326
  listDir: (dir) => {
14777
14327
  try {
14778
- return fs19.readdirSync(dir);
14328
+ return fs16.readdirSync(dir);
14779
14329
  } catch {
14780
14330
  return void 0;
14781
14331
  }
@@ -14797,19 +14347,19 @@ async function readRunInput(args) {
14797
14347
  return normalizeRunFromText(await readInput2(args), args).run;
14798
14348
  }
14799
14349
  function readFileInput(filePath) {
14800
- const resolved = path25.resolve(filePath);
14801
- if (!fs19.existsSync(resolved)) {
14350
+ const resolved = path22.resolve(filePath);
14351
+ if (!fs16.existsSync(resolved)) {
14802
14352
  console.error(`Error: File not found: ${resolved}`);
14803
14353
  process.exit(EXIT_USAGE4);
14804
14354
  }
14805
- return fs19.readFileSync(resolved, "utf8");
14355
+ return fs16.readFileSync(resolved, "utf8");
14806
14356
  }
14807
14357
  function readStdin() {
14808
- return new Promise((resolve13, reject) => {
14358
+ return new Promise((resolve12, reject) => {
14809
14359
  const chunks = [];
14810
14360
  process.stdin.setEncoding("utf8");
14811
14361
  process.stdin.on("data", (chunk) => chunks.push(chunk));
14812
- process.stdin.on("end", () => resolve13(chunks.join("")));
14362
+ process.stdin.on("end", () => resolve12(chunks.join("")));
14813
14363
  process.stdin.on("error", reject);
14814
14364
  });
14815
14365
  }
@@ -14957,16 +14507,16 @@ function tryNormalizeRunFromText(text2, args) {
14957
14507
  }
14958
14508
  }
14959
14509
  function listBaselineCandidates(currentFile, args) {
14960
- const baselineDir = path25.resolve(
14961
- args.baselineDir ?? path25.dirname(currentFile)
14510
+ const baselineDir = path22.resolve(
14511
+ args.baselineDir ?? path22.dirname(currentFile)
14962
14512
  );
14963
- const currentResolved = path25.resolve(currentFile);
14964
- if (!fs19.existsSync(baselineDir)) {
14513
+ const currentResolved = path22.resolve(currentFile);
14514
+ if (!fs16.existsSync(baselineDir)) {
14965
14515
  console.error(`Error: baseline directory not found: ${baselineDir}`);
14966
14516
  process.exit(EXIT_USAGE4);
14967
14517
  }
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(
14518
+ const entries = fs16.readdirSync(baselineDir, { withFileTypes: true });
14519
+ return entries.filter((entry) => entry.isFile()).map((entry) => path22.join(baselineDir, entry.name)).filter((candidate) => path22.resolve(candidate) !== currentResolved).filter(
14970
14520
  (candidate) => args.inputType === "ndjson" ? candidate.endsWith(".ndjson") : candidate.endsWith(".json")
14971
14521
  );
14972
14522
  }
@@ -14975,7 +14525,7 @@ function resolveBaselineAuto(currentFile, currentRun, args) {
14975
14525
  const comparable = [];
14976
14526
  for (const candidate of candidates) {
14977
14527
  const run = tryNormalizeRunFromText(
14978
- fs19.readFileSync(candidate, "utf8"),
14528
+ fs16.readFileSync(candidate, "utf8"),
14979
14529
  args
14980
14530
  );
14981
14531
  if (run) {
@@ -14984,7 +14534,7 @@ function resolveBaselineAuto(currentFile, currentRun, args) {
14984
14534
  }
14985
14535
  if (comparable.length === 0) {
14986
14536
  console.error(
14987
- `Error: no compatible baseline files found in ${path25.resolve(args.baselineDir ?? path25.dirname(currentFile))}.`
14537
+ `Error: no compatible baseline files found in ${path22.resolve(args.baselineDir ?? path22.dirname(currentFile))}.`
14988
14538
  );
14989
14539
  process.exit(EXIT_USAGE4);
14990
14540
  }
@@ -15155,7 +14705,8 @@ async function runReview(ctx) {
15155
14705
  const { args } = ctx;
15156
14706
  const run = applySelection(await readRunInput(args), args);
15157
14707
  const context = loadReviewContext(args);
15158
- const review = buildReview(run, context);
14708
+ const built = buildReview(run, context);
14709
+ const review = await withJev(built, (jev) => enrichReview(built, jev));
15159
14710
  try {
15160
14711
  const files = writeReviewReport(review, args);
15161
14712
  for (const f of files) {
@@ -15239,7 +14790,7 @@ async function runCheckExplainers(ctx) {
15239
14790
  );
15240
14791
  process.exit(EXIT_USAGE4);
15241
14792
  }
15242
- if (!fs19.existsSync(args.explainersDir) || !fs19.statSync(args.explainersDir).isDirectory()) {
14793
+ if (!fs16.existsSync(args.explainersDir) || !fs16.statSync(args.explainersDir).isDirectory()) {
15243
14794
  console.error(
15244
14795
  `Error: --explainers-dir "${args.explainersDir}" is not a directory.`
15245
14796
  );
@@ -15253,30 +14804,39 @@ async function runCheckExplainers(ctx) {
15253
14804
  }
15254
14805
  process.exit(EXIT_SUCCESS4);
15255
14806
  }
14807
+ async function withJev(report, enrich) {
14808
+ const jev = jevFromEnv();
14809
+ if (!jev) return report;
14810
+ try {
14811
+ return await enrich(jev);
14812
+ } catch (err) {
14813
+ console.error(`Jev unavailable, deterministic output only: ${err instanceof Error ? err.message : String(err)}`);
14814
+ return report;
14815
+ }
14816
+ }
15256
14817
  async function runGoal(ctx) {
15257
14818
  const { args } = ctx;
15258
14819
  const run = applySelection(await readRunInput(args), args);
15259
14820
  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
- );
14821
+ const goalArgs = {
14822
+ run,
14823
+ baseline,
14824
+ requireTags: args.requireTags,
14825
+ requireTickets: args.requireTickets,
14826
+ requireScenarios: args.requireScenarios,
14827
+ enforceNoRegressions: args.noRegressions,
14828
+ enforceRatchet: !args.noRatchet,
14829
+ format: args.goalFormat
14830
+ };
14831
+ const built = buildGoal(goalArgs, {});
14832
+ const report = await withJev(built, (jev) => enrichGoal(built, goalArgs, jev));
15273
14833
  console.log(renderGoal(report, args.goalFormat));
15274
14834
  process.exit(report.met ? EXIT_SUCCESS4 : EXIT_AGENT_GATE);
15275
14835
  }
15276
14836
  function readCodeowners() {
15277
14837
  for (const candidate of ["CODEOWNERS", ".github/CODEOWNERS", "docs/CODEOWNERS"]) {
15278
- if (fs19.existsSync(candidate)) {
15279
- return parseCodeowners(fs19.readFileSync(candidate, "utf8"));
14838
+ if (fs16.existsSync(candidate)) {
14839
+ return parseCodeowners(fs16.readFileSync(candidate, "utf8"));
15280
14840
  }
15281
14841
  }
15282
14842
  console.error(
@@ -15289,7 +14849,7 @@ async function runTriage(ctx) {
15289
14849
  const run = applySelection(await readRunInput(args), args);
15290
14850
  const baseline = resolveBaselineStatusMap(args, run);
15291
14851
  const codeowners = args.byOwner ? readCodeowners() : void 0;
15292
- const report = buildTriage(
14852
+ const built = buildTriage(
15293
14853
  {
15294
14854
  testCases: run.testCases,
15295
14855
  baseline,
@@ -15298,6 +14858,7 @@ async function runTriage(ctx) {
15298
14858
  },
15299
14859
  {}
15300
14860
  );
14861
+ const report = await withJev(built, (jev) => enrichTriage(built, run.testCases, jev, codeowners));
15301
14862
  console.log(renderTriage(report, args.triageFormat, { byOwner: args.byOwner }));
15302
14863
  process.exit(EXIT_SUCCESS4);
15303
14864
  }
@@ -15396,9 +14957,9 @@ async function runFormatOrValidate(ctx) {
15396
14957
  process.exit(EXIT_SCHEMA_VALIDATION);
15397
14958
  }
15398
14959
  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");
14960
+ const outPath = path22.resolve(args.emitCanonical);
14961
+ fs16.mkdirSync(path22.dirname(outPath), { recursive: true });
14962
+ fs16.writeFileSync(outPath, JSON.stringify(run, null, 2), "utf8");
15402
14963
  }
15403
14964
  try {
15404
14965
  const history = runHistoryPipeline(run, args);
@@ -15462,9 +15023,9 @@ ${msg}`);
15462
15023
  }
15463
15024
  const run = data;
15464
15025
  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");
15026
+ const outPath = path22.resolve(args.emitCanonical);
15027
+ fs16.mkdirSync(path22.dirname(outPath), { recursive: true });
15028
+ fs16.writeFileSync(outPath, JSON.stringify(run, null, 2), "utf8");
15468
15029
  }
15469
15030
  try {
15470
15031
  const history = runHistoryPipeline(run, args);
@@ -15523,9 +15084,9 @@ ${msg}`);
15523
15084
  process.exit(EXIT_CANONICAL_VALIDATION);
15524
15085
  }
15525
15086
  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");
15087
+ const outPath = path22.resolve(args.emitCanonical);
15088
+ fs16.mkdirSync(path22.dirname(outPath), { recursive: true });
15089
+ fs16.writeFileSync(outPath, JSON.stringify(canonical, null, 2), "utf8");
15529
15090
  }
15530
15091
  try {
15531
15092
  const history = runHistoryPipeline(canonical, args);
@@ -15560,9 +15121,9 @@ function runCustomFormatters(run, customRequested, formatters, args) {
15560
15121
  const ext = formatter.fileExtension ?? formatName;
15561
15122
  const baseName = args.outputName ?? "report";
15562
15123
  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");
15124
+ const filepath = path22.join(outputDir, filename);
15125
+ fs16.mkdirSync(outputDir, { recursive: true });
15126
+ fs16.writeFileSync(filepath, content, "utf8");
15566
15127
  console.log(`Generated: ${filepath}`);
15567
15128
  } catch (err) {
15568
15129
  console.error(
@@ -15616,13 +15177,13 @@ async function dispatchNotifications(run, args) {
15616
15177
  }
15617
15178
  function runHistoryPipeline(run, args) {
15618
15179
  if (!args.historyFile) return void 0;
15619
- const historyPath = path25.resolve(args.historyFile);
15180
+ const historyPath = path22.resolve(args.historyFile);
15620
15181
  const store = loadHistory(
15621
15182
  { filePath: historyPath },
15622
15183
  {
15623
15184
  readFile: (p) => {
15624
15185
  try {
15625
- return fs19.readFileSync(p, "utf8");
15186
+ return fs16.readFileSync(p, "utf8");
15626
15187
  } catch {
15627
15188
  return void 0;
15628
15189
  }
@@ -15635,12 +15196,12 @@ function runHistoryPipeline(run, args) {
15635
15196
  run,
15636
15197
  maxRuns: args.maxHistoryRuns
15637
15198
  });
15638
- const dir = path25.dirname(historyPath);
15639
- fs19.mkdirSync(dir, { recursive: true });
15199
+ const dir = path22.dirname(historyPath);
15200
+ fs16.mkdirSync(dir, { recursive: true });
15640
15201
  saveHistory(
15641
15202
  { filePath: historyPath, store: updated },
15642
15203
  {
15643
- writeFile: (p, content) => fs19.writeFileSync(p, content, "utf8")
15204
+ writeFile: (p, content) => fs16.writeFileSync(p, content, "utf8")
15644
15205
  }
15645
15206
  );
15646
15207
  let metricsCount = 0;
@@ -15706,7 +15267,7 @@ async function generateReports(run, args, historyStore, _droppedMissingStory = 0
15706
15267
  files.push(...paths);
15707
15268
  (EXECUTION_ONLY_FORMATS.has(format) ? executionFiles : documentationFiles).push(...paths);
15708
15269
  }
15709
- const createdArtifactsReadme = writeArtifactsReadme(args.outputDir);
15270
+ writeArtifactsReadme(args.outputDir);
15710
15271
  const documented = generator.renderedRun ?? run;
15711
15272
  const wroteDocumentation = args.formats.some(
15712
15273
  (f) => !EXECUTION_ONLY_FORMATS.has(f)
@@ -15740,7 +15301,6 @@ async function generateReports(run, args, historyStore, _droppedMissingStory = 0
15740
15301
  return {
15741
15302
  files,
15742
15303
  counts,
15743
- createdArtifactsReadme,
15744
15304
  // A run that traced itself can draw the architecture it exercised, and
15745
15305
  // nobody discovers a format they have never seen named.
15746
15306
  spansUnused: !args.formats.includes("span-graph") && !args.htmlArchitecture && documented.testCases.some((tc) => (tc.story.otelSpans?.length ?? 0) > 0),
@@ -15885,13 +15445,13 @@ function writeReviewReport(review, args) {
15885
15445
  const outputDir = args.outputDir ?? "reports";
15886
15446
  const baseName = args.outputName ?? "evidence-review";
15887
15447
  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(
15448
+ fs16.mkdirSync(outputDir, { recursive: true });
15449
+ const mdPath = path22.join(outputDir, `${baseName}${suffix}.md`);
15450
+ const htmlPath = path22.join(outputDir, `${baseName}${suffix}.html`);
15451
+ const jsonPath = path22.join(outputDir, `${baseName}${suffix}.review.json`);
15452
+ fs16.writeFileSync(mdPath, markdown, "utf8");
15453
+ fs16.writeFileSync(htmlPath, html, "utf8");
15454
+ fs16.writeFileSync(
15895
15455
  jsonPath,
15896
15456
  `${JSON.stringify(buildReviewJson(review), null, 2)}
15897
15457
  `,
@@ -15987,11 +15547,6 @@ function printResult(result, args, startMs, droppedMissingStory = 0) {
15987
15547
  "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
15548
  );
15989
15549
  }
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
15550
  }
15996
15551
  if (args.open) {
15997
15552
  openInBrowser(pickOpenTarget(result.files));
@@ -16000,9 +15555,9 @@ function printResult(result, args, startMs, droppedMissingStory = 0) {
16000
15555
  function printCompareResult(result, args, startMs) {
16001
15556
  const durationMs = Date.now() - startMs;
16002
15557
  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");
15558
+ const outputPath = path22.resolve(args.prSummaryFile);
15559
+ fs16.mkdirSync(path22.dirname(outputPath), { recursive: true });
15560
+ fs16.writeFileSync(outputPath, result.prSummary, "utf8");
16006
15561
  }
16007
15562
  if (args.jsonSummary) {
16008
15563
  console.log(
@@ -16036,13 +15591,13 @@ function printCompareResult(result, args, startMs) {
16036
15591
  }
16037
15592
  }
16038
15593
  function loadReleasePolicy(policyPath) {
16039
- const resolved = path25.resolve(policyPath);
16040
- if (!fs19.existsSync(resolved)) {
15594
+ const resolved = path22.resolve(policyPath);
15595
+ if (!fs16.existsSync(resolved)) {
16041
15596
  console.error(`Error: release policy file not found: ${resolved}`);
16042
15597
  process.exit(EXIT_USAGE4);
16043
15598
  }
16044
15599
  try {
16045
- const raw = JSON.parse(fs19.readFileSync(resolved, "utf8"));
15600
+ const raw = JSON.parse(fs16.readFileSync(resolved, "utf8"));
16046
15601
  return {
16047
15602
  allowedOmissions: Array.isArray(raw.allowedOmissions) ? raw.allowedOmissions : [],
16048
15603
  allowedRegressions: Array.isArray(raw.allowedRegressions) ? raw.allowedRegressions : [],
@@ -16148,7 +15703,7 @@ Generate an API token at https://id.atlassian.com/manage-profile/security/api-to
16148
15703
  );
16149
15704
  process.exit(EXIT_USAGE4);
16150
15705
  }
16151
- if (!fs19.existsSync(inputFile)) {
15706
+ if (!fs16.existsSync(inputFile)) {
16152
15707
  console.error(`Error: file not found: ${inputFile}`);
16153
15708
  process.exit(EXIT_USAGE4);
16154
15709
  }
@@ -16176,7 +15731,7 @@ Generate an API token at https://id.atlassian.com/manage-profile/security/api-to
16176
15731
  console.error("Error: --title is required when creating a new page");
16177
15732
  process.exit(EXIT_USAGE4);
16178
15733
  }
16179
- const adf = fs19.readFileSync(path25.resolve(inputFile), "utf8");
15734
+ const adf = fs16.readFileSync(path22.resolve(inputFile), "utf8");
16180
15735
  if (dryRun) {
16181
15736
  console.log(
16182
15737
  JSON.stringify(
@@ -16257,7 +15812,7 @@ Generate an API token at https://id.atlassian.com/manage-profile/security/api-to
16257
15812
  );
16258
15813
  process.exit(EXIT_USAGE4);
16259
15814
  }
16260
- if (!fs19.existsSync(inputFile)) {
15815
+ if (!fs16.existsSync(inputFile)) {
16261
15816
  console.error(`Error: file not found: ${inputFile}`);
16262
15817
  process.exit(EXIT_USAGE4);
16263
15818
  }
@@ -16284,7 +15839,7 @@ Generate an API token at https://id.atlassian.com/manage-profile/security/api-to
16284
15839
  process.exit(EXIT_USAGE4);
16285
15840
  }
16286
15841
  const mode = modeRaw;
16287
- const adf = fs19.readFileSync(path25.resolve(inputFile), "utf8");
15842
+ const adf = fs16.readFileSync(path22.resolve(inputFile), "utf8");
16288
15843
  if (dryRun) {
16289
15844
  console.log(
16290
15845
  JSON.stringify(
@@ -16325,46 +15880,6 @@ Generate an API token at https://id.atlassian.com/manage-profile/security/api-to
16325
15880
  process.exit(EXIT_GENERATION);
16326
15881
  }
16327
15882
  }
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
15883
  async function runCheckLinks(rawArgs) {
16369
15884
  const { values, positionals } = parseArgs4({
16370
15885
  args: rawArgs,
@@ -16394,48 +15909,6 @@ async function runCheckLinks(rawArgs) {
16394
15909
  return EXIT_USAGE4;
16395
15910
  }
16396
15911
  }
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
15912
  async function runDeploy(rawArgs) {
16440
15913
  const mode = rawArgs[0];
16441
15914
  if (!mode || !["record", "status", "diff"].includes(mode)) {