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/README.md +0 -37
- package/dist/cli.js +438 -965
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +201 -2
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +78 -6
- package/dist/index.d.ts +78 -6
- package/dist/index.js +196 -2
- package/dist/index.js.map +1 -1
- package/package.json +2 -3
- package/templates/astro-thin/astro.config.mjs +0 -46
- package/templates/astro-thin/executable-stories.config.mjs +0 -76
- package/templates/astro-thin/gitignore +0 -3
- package/templates/astro-thin/package.json +0 -20
- package/templates/astro-thin/reports/sample-run.json +0 -214
- package/templates/astro-thin/src/content/docs/404.md +0 -20
- package/templates/astro-thin/src/content/docs/guides/writing-docs.mdx +0 -58
- package/templates/astro-thin/src/content/docs/index.mdx +0 -51
- package/templates/astro-thin/src/content.config.ts +0 -33
- package/templates/astro-thin/src/styles/stories.css +0 -32
- package/templates/astro-thin/tsconfig.json +0 -5
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
|
|
5
|
-
import * as
|
|
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((
|
|
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
|
|
2007
|
+
const path23 = file.newPath ?? file.oldPath ?? "";
|
|
2013
2008
|
return `<div class="diff-hunk">
|
|
2014
|
-
<div class="diff-file-header"><code>${escapeHtml(
|
|
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
|
-
|
|
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
|
|
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
|
|
3983
|
-
import * as
|
|
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
|
|
5566
|
-
import * as
|
|
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
|
|
5572
|
-
import * as
|
|
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
|
|
6603
|
-
if (
|
|
6604
|
-
return
|
|
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
|
|
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:
|
|
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(
|
|
6793
|
-
const base =
|
|
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(
|
|
6798
|
-
return TEST_INFIX.test(
|
|
6688
|
+
function isTestFile(path23) {
|
|
6689
|
+
return TEST_INFIX.test(path23);
|
|
6799
6690
|
}
|
|
6800
|
-
function isReviewableSource(
|
|
6801
|
-
if (isTestFile(
|
|
6802
|
-
if (
|
|
6803
|
-
return CODE_EXTENSIONS.has(extensionOf(
|
|
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
|
-
(
|
|
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((
|
|
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((
|
|
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(
|
|
7867
|
-
const lower =
|
|
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((
|
|
7954
|
+
await new Promise((resolve12) => htmlStream.once("drain", resolve12));
|
|
8009
7955
|
}
|
|
8010
7956
|
}
|
|
8011
|
-
await new Promise((
|
|
8012
|
-
collector.on("finish",
|
|
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
|
|
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, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
@@ -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 =
|
|
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
|
|
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
|
|
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 =
|
|
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
|
-
|
|
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 =
|
|
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
|
|
8219
|
+
const resolve12 = (sourceFile) => path9.resolve(args.projectRoot, sourceFile);
|
|
8274
8220
|
const canResolveSources = args.ran.some(
|
|
8275
|
-
(sourceFile) => deps.fileExists(
|
|
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(
|
|
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 (!
|
|
8809
|
-
|
|
8754
|
+
if (!fs7.existsSync(assetsDir)) {
|
|
8755
|
+
fs7.mkdirSync(assetsDir, { recursive: true });
|
|
8810
8756
|
}
|
|
8811
|
-
const content =
|
|
8757
|
+
const content = fs7.readFileSync(sourcePath);
|
|
8812
8758
|
const hash = crypto.createHash("sha256").update(content).digest("hex").slice(0, 8);
|
|
8813
|
-
const ext =
|
|
8814
|
-
const baseName = sanitize(
|
|
8759
|
+
const ext = path10.extname(sourcePath);
|
|
8760
|
+
const baseName = sanitize(path10.basename(sourcePath, ext));
|
|
8815
8761
|
const destName = `${baseName}-${hash}${ext}`;
|
|
8816
|
-
const destPath =
|
|
8817
|
-
if (!
|
|
8818
|
-
|
|
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
|
|
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 :
|
|
8925
|
-
if (!
|
|
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
|
|
8948
|
-
import * as
|
|
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 =
|
|
8981
|
-
const assetsDir =
|
|
8982
|
-
let html =
|
|
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 =
|
|
8988
|
-
if (!
|
|
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
|
-
|
|
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(
|
|
9047
|
+
return toPosix2(path13.join(baseOutputDir, joinNameAndExt(effectiveName, ext)));
|
|
9102
9048
|
}
|
|
9103
9049
|
const normalizedSource = toPosix2(sourceFile);
|
|
9104
|
-
const dirOfSource =
|
|
9105
|
-
let baseName =
|
|
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(
|
|
9060
|
+
return toPosix2(path13.posix.join(dirOfSource, fileName));
|
|
9115
9061
|
}
|
|
9116
9062
|
if (colocatedStyle === "flat") {
|
|
9117
|
-
return toPosix2(
|
|
9063
|
+
return toPosix2(path13.posix.join(baseOutputDir, `${cleanTestStem(normalizedSource)}${ext}`));
|
|
9118
9064
|
}
|
|
9119
|
-
return toPosix2(
|
|
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) =>
|
|
9151
|
+
readFile: deps?.readFile ?? ((p) => fs10.readFileSync(p, "utf8")),
|
|
9206
9152
|
listDir: deps?.listDir ?? ((dir) => {
|
|
9207
9153
|
try {
|
|
9208
|
-
return
|
|
9154
|
+
return fs10.readdirSync(dir);
|
|
9209
9155
|
} catch {
|
|
9210
9156
|
return void 0;
|
|
9211
9157
|
}
|
|
9212
9158
|
}),
|
|
9213
|
-
fileExists: deps?.fileExists ?? ((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:
|
|
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:
|
|
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 =
|
|
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(
|
|
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,
|
|
9416
|
+
const entries = buildIndexEntries(run, bySourceFile, path13.dirname(indexPath));
|
|
9471
9417
|
const html = renderColocatedIndex(entries, this.options.html.title);
|
|
9472
|
-
await fsPromises2.mkdir(
|
|
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(
|
|
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 =
|
|
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 =
|
|
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) =>
|
|
9705
|
-
const data = JSON.parse(read(
|
|
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) =>
|
|
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(
|
|
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
|
|
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 =
|
|
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(
|
|
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,
|
|
10696
|
-
const clean =
|
|
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
|
|
10712
|
-
for (const owner of ownersFor(codeowners,
|
|
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(
|
|
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
|
|
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,
|
|
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
|
|
11151
|
-
import * as
|
|
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) =>
|
|
11102
|
+
readFile: (filePath) => fs12.readFileSync(filePath, "utf8"),
|
|
11213
11103
|
listDir: (dirPath) => {
|
|
11214
11104
|
try {
|
|
11215
|
-
return
|
|
11105
|
+
return fs12.readdirSync(dirPath);
|
|
11216
11106
|
} catch {
|
|
11217
11107
|
return void 0;
|
|
11218
11108
|
}
|
|
11219
11109
|
},
|
|
11220
|
-
appendFile: (filePath, text2) =>
|
|
11221
|
-
writeFile: (filePath, text2) =>
|
|
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 =
|
|
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
|
|
11736
|
-
import * as
|
|
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) =>
|
|
11771
|
-
readBinary: (filePath) => new Uint8Array(
|
|
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 =
|
|
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
|
|
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[
|
|
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) =>
|
|
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 =
|
|
11864
|
-
const relative3 =
|
|
11865
|
-
const inside = relative3 !== "" && !relative3.startsWith("..") && !
|
|
11866
|
-
const base = inside ? relative3.split(
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 ${
|
|
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
|
|
12054
|
-
import
|
|
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 (
|
|
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 =
|
|
12086
|
-
if (!
|
|
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 =
|
|
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(
|
|
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
|
|
12460
|
-
import * as
|
|
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) =>
|
|
12516
|
-
fileExists: (filePath) =>
|
|
12165
|
+
readFile: (filePath) => fs15.readFileSync(filePath, "utf8"),
|
|
12166
|
+
fileExists: (filePath) => fs15.existsSync(filePath),
|
|
12517
12167
|
writeFile: (filePath, contents) => {
|
|
12518
|
-
|
|
12519
|
-
|
|
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 =
|
|
12701
|
-
const markdownPath =
|
|
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
|
|
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 `${
|
|
13424
|
+
return `${path23}: ${message} \u2014 '${extra}'`;
|
|
13775
13425
|
}
|
|
13776
13426
|
if (err.keyword === "enum") {
|
|
13777
13427
|
const allowed = err.params.allowedValues;
|
|
13778
|
-
return `${
|
|
13428
|
+
return `${path23}: ${message} \u2014 allowed: ${JSON.stringify(allowed)}`;
|
|
13779
13429
|
}
|
|
13780
|
-
return `${
|
|
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((
|
|
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
|
|
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
|
|
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 !== "
|
|
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", "
|
|
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) =>
|
|
13855
|
+
readFile: (filePath) => fs16.readFileSync(filePath, "utf8"),
|
|
14222
13856
|
listDir: (dir) => {
|
|
14223
13857
|
try {
|
|
14224
|
-
return
|
|
13858
|
+
return fs16.readdirSync(dir);
|
|
14225
13859
|
} catch {
|
|
14226
13860
|
return void 0;
|
|
14227
13861
|
}
|
|
14228
13862
|
},
|
|
14229
|
-
removeFile: (filePath) =>
|
|
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'
|
|
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 =
|
|
14723
|
-
if (!
|
|
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
|
|
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 =
|
|
14733
|
-
if (!
|
|
14282
|
+
const resolved = path22.resolve(input);
|
|
14283
|
+
if (!fs16.existsSync(resolved) || !fs16.statSync(resolved).isDirectory())
|
|
14734
14284
|
return void 0;
|
|
14735
|
-
const names =
|
|
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 =
|
|
14292
|
+
const filePath = path22.join(resolved, name);
|
|
14743
14293
|
try {
|
|
14744
14294
|
const run = JSON.parse(
|
|
14745
|
-
|
|
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 =
|
|
14770
|
-
if (!
|
|
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) =>
|
|
14325
|
+
readFile: (p) => fs16.readFileSync(p, "utf8"),
|
|
14776
14326
|
listDir: (dir) => {
|
|
14777
14327
|
try {
|
|
14778
|
-
return
|
|
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 =
|
|
14801
|
-
if (!
|
|
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
|
|
14355
|
+
return fs16.readFileSync(resolved, "utf8");
|
|
14806
14356
|
}
|
|
14807
14357
|
function readStdin() {
|
|
14808
|
-
return new Promise((
|
|
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", () =>
|
|
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 =
|
|
14961
|
-
args.baselineDir ??
|
|
14510
|
+
const baselineDir = path22.resolve(
|
|
14511
|
+
args.baselineDir ?? path22.dirname(currentFile)
|
|
14962
14512
|
);
|
|
14963
|
-
const currentResolved =
|
|
14964
|
-
if (!
|
|
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 =
|
|
14969
|
-
return entries.filter((entry) => entry.isFile()).map((entry) =>
|
|
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
|
-
|
|
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 ${
|
|
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
|
|
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 (!
|
|
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
|
|
15261
|
-
|
|
15262
|
-
|
|
15263
|
-
|
|
15264
|
-
|
|
15265
|
-
|
|
15266
|
-
|
|
15267
|
-
|
|
15268
|
-
|
|
15269
|
-
|
|
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 (
|
|
15279
|
-
return parseCodeowners(
|
|
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
|
|
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 =
|
|
15400
|
-
|
|
15401
|
-
|
|
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 =
|
|
15466
|
-
|
|
15467
|
-
|
|
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 =
|
|
15527
|
-
|
|
15528
|
-
|
|
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 =
|
|
15564
|
-
|
|
15565
|
-
|
|
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 =
|
|
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
|
|
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 =
|
|
15639
|
-
|
|
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) =>
|
|
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
|
-
|
|
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
|
-
|
|
15889
|
-
const mdPath =
|
|
15890
|
-
const htmlPath =
|
|
15891
|
-
const jsonPath =
|
|
15892
|
-
|
|
15893
|
-
|
|
15894
|
-
|
|
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 =
|
|
16004
|
-
|
|
16005
|
-
|
|
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 =
|
|
16040
|
-
if (!
|
|
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(
|
|
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 (!
|
|
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 =
|
|
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 (!
|
|
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 =
|
|
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)) {
|