executable-stories-formatters 1.20.0 → 1.21.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +0 -37
- package/dist/cli.js +519 -987
- 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 +3 -4
- 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,13 +11622,16 @@ 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
|
-
import { collectReportAssets, rewriteReportAssets } from "executable-stories-core/report-assets";
|
|
11740
11629
|
import { toStoryReport as toStoryReport8 } from "executable-stories-core/converters/story-report";
|
|
11741
11630
|
import { synthesizeStories as synthesizeStories3 } from "executable-stories-core/converters/synthesize";
|
|
11631
|
+
import {
|
|
11632
|
+
collectReportAssets,
|
|
11633
|
+
rewriteReportAssets
|
|
11634
|
+
} from "executable-stories-core/report-assets";
|
|
11742
11635
|
var EXIT_SUCCESS2 = 0;
|
|
11743
11636
|
var EXIT_SHARE_FAILED = 1;
|
|
11744
11637
|
var EXIT_USAGE2 = 4;
|
|
@@ -11767,11 +11660,11 @@ Options:
|
|
|
11767
11660
|
Exit codes: 0 shared, 1 rejected or failed, 4 usage error.`;
|
|
11768
11661
|
function defaultDeps2() {
|
|
11769
11662
|
return {
|
|
11770
|
-
readFile: (filePath) =>
|
|
11771
|
-
readBinary: (filePath) => new Uint8Array(
|
|
11663
|
+
readFile: (filePath) => fs13.readFileSync(filePath, "utf8"),
|
|
11664
|
+
readBinary: (filePath) => new Uint8Array(fs13.readFileSync(filePath)),
|
|
11772
11665
|
fileSize: (filePath) => {
|
|
11773
11666
|
try {
|
|
11774
|
-
const stat =
|
|
11667
|
+
const stat = fs13.statSync(filePath);
|
|
11775
11668
|
return stat.isFile() ? stat.size : void 0;
|
|
11776
11669
|
} catch {
|
|
11777
11670
|
return void 0;
|
|
@@ -11779,7 +11672,7 @@ function defaultDeps2() {
|
|
|
11779
11672
|
},
|
|
11780
11673
|
listDir: (dirPath) => {
|
|
11781
11674
|
try {
|
|
11782
|
-
return
|
|
11675
|
+
return fs13.readdirSync(dirPath);
|
|
11783
11676
|
} catch {
|
|
11784
11677
|
return void 0;
|
|
11785
11678
|
}
|
|
@@ -11807,11 +11700,11 @@ var CONTENT_TYPES = {
|
|
|
11807
11700
|
".zip": "application/zip"
|
|
11808
11701
|
};
|
|
11809
11702
|
function contentTypeFor(filePath) {
|
|
11810
|
-
return CONTENT_TYPES[
|
|
11703
|
+
return CONTENT_TYPES[path19.extname(filePath).toLowerCase()] ?? "application/octet-stream";
|
|
11811
11704
|
}
|
|
11812
11705
|
function resolveReport(inputPath, deps) {
|
|
11813
11706
|
const entries = deps.listDir(inputPath);
|
|
11814
|
-
const candidates = entries === void 0 ? [inputPath] : candidateNames(entries).map((name) =>
|
|
11707
|
+
const candidates = entries === void 0 ? [inputPath] : candidateNames(entries).map((name) => path19.join(inputPath, name));
|
|
11815
11708
|
if (candidates.length === 0) {
|
|
11816
11709
|
throw new Error(
|
|
11817
11710
|
"no report in it. Generate one first: executable-stories format <run.json> --format story-report-json --output-dir <dir> --output-name index"
|
|
@@ -11843,13 +11736,20 @@ function loadReport(filePath, deps) {
|
|
|
11843
11736
|
const text2 = deps.readFile(filePath);
|
|
11844
11737
|
if (filePath.endsWith(".html")) {
|
|
11845
11738
|
const match = HTML_REPORT_DATA.exec(text2);
|
|
11846
|
-
if (!match?.[1])
|
|
11847
|
-
|
|
11739
|
+
if (!match?.[1])
|
|
11740
|
+
throw new Error(
|
|
11741
|
+
`${filePath} is an HTML page with no report embedded in it`
|
|
11742
|
+
);
|
|
11743
|
+
return asStoryReport(
|
|
11744
|
+
JSON.parse(match[1]),
|
|
11745
|
+
filePath
|
|
11746
|
+
);
|
|
11848
11747
|
}
|
|
11849
11748
|
return asStoryReport(JSON.parse(text2), filePath);
|
|
11850
11749
|
}
|
|
11851
11750
|
function asStoryReport(data, filePath) {
|
|
11852
|
-
if (typeof data.schemaVersion === "string")
|
|
11751
|
+
if (typeof data.schemaVersion === "string")
|
|
11752
|
+
return data;
|
|
11853
11753
|
try {
|
|
11854
11754
|
return toStoryReport8(canonicalizeRun5(synthesizeStories3(data)));
|
|
11855
11755
|
} catch (err) {
|
|
@@ -11860,12 +11760,12 @@ function asStoryReport(data, filePath) {
|
|
|
11860
11760
|
}
|
|
11861
11761
|
}
|
|
11862
11762
|
function keyFor(assetPath, reportDir, taken) {
|
|
11863
|
-
const resolved =
|
|
11864
|
-
const relative3 =
|
|
11865
|
-
const inside = relative3 !== "" && !relative3.startsWith("..") && !
|
|
11866
|
-
const base = inside ? relative3.split(
|
|
11763
|
+
const resolved = path19.resolve(reportDir, assetPath);
|
|
11764
|
+
const relative3 = path19.relative(reportDir, resolved);
|
|
11765
|
+
const inside = relative3 !== "" && !relative3.startsWith("..") && !path19.isAbsolute(relative3);
|
|
11766
|
+
const base = inside ? relative3.split(path19.sep).join("/") : `assets/${path19.basename(resolved)}`;
|
|
11867
11767
|
let key = base;
|
|
11868
|
-
const ext =
|
|
11768
|
+
const ext = path19.extname(base);
|
|
11869
11769
|
const stem = base.slice(0, base.length - ext.length);
|
|
11870
11770
|
for (let n = 2; taken.has(key); n++) key = `${stem}-${n}${ext}`;
|
|
11871
11771
|
taken.add(key);
|
|
@@ -11877,7 +11777,7 @@ function planAssets(report, reportDir, deps) {
|
|
|
11877
11777
|
const keyByPath = /* @__PURE__ */ new Map();
|
|
11878
11778
|
const taken = /* @__PURE__ */ new Set();
|
|
11879
11779
|
for (const assetPath of collectReportAssets(report)) {
|
|
11880
|
-
const localPath =
|
|
11780
|
+
const localPath = path19.resolve(reportDir, assetPath);
|
|
11881
11781
|
const bytes = deps.fileSize(localPath);
|
|
11882
11782
|
const key = keyFor(assetPath, reportDir, taken);
|
|
11883
11783
|
keyByPath.set(assetPath, key);
|
|
@@ -11885,7 +11785,12 @@ function planAssets(report, reportDir, deps) {
|
|
|
11885
11785
|
missing.push(assetPath);
|
|
11886
11786
|
continue;
|
|
11887
11787
|
}
|
|
11888
|
-
assets.push({
|
|
11788
|
+
assets.push({
|
|
11789
|
+
path: key,
|
|
11790
|
+
localPath,
|
|
11791
|
+
contentType: contentTypeFor(key),
|
|
11792
|
+
bytes
|
|
11793
|
+
});
|
|
11889
11794
|
}
|
|
11890
11795
|
return { assets, missing, keyByPath };
|
|
11891
11796
|
}
|
|
@@ -11900,6 +11805,10 @@ async function describeFailure(response) {
|
|
|
11900
11805
|
if (error?.type === "SHARE_TOO_LARGE") {
|
|
11901
11806
|
return `the report and its assets are over the ${error.maxBytes ?? 0} byte limit for a share.`;
|
|
11902
11807
|
}
|
|
11808
|
+
if (error?.type === "STORAGE_LIMIT") {
|
|
11809
|
+
const mb = (bytes) => `${Math.round(bytes / 1024 / 1024)} MB`;
|
|
11810
|
+
return `stored evidence is at the plan's ${mb(error.limit ?? 0)} (${mb(error.used ?? 0)} used). Delete a share or an attachment in your cloud settings, or upgrade.`;
|
|
11811
|
+
}
|
|
11903
11812
|
if (error?.message) return error.message;
|
|
11904
11813
|
if (error?.type) return error.type;
|
|
11905
11814
|
} catch {
|
|
@@ -11934,7 +11843,9 @@ async function runShare(rawArgs, depsOverride = {}) {
|
|
|
11934
11843
|
}
|
|
11935
11844
|
const inputPath = parsed.positionals[0];
|
|
11936
11845
|
if (!inputPath) {
|
|
11937
|
-
deps.error(
|
|
11846
|
+
deps.error(
|
|
11847
|
+
"share needs a report: executable-stories share <reports-dir|report.html|report.json>"
|
|
11848
|
+
);
|
|
11938
11849
|
deps.error(HELP2);
|
|
11939
11850
|
return EXIT_USAGE2;
|
|
11940
11851
|
}
|
|
@@ -11948,7 +11859,9 @@ async function runShare(rawArgs, depsOverride = {}) {
|
|
|
11948
11859
|
const expiresRaw = parsed.values["expires-days"];
|
|
11949
11860
|
const expiresInDays = expiresRaw === void 0 ? 30 : Number(expiresRaw);
|
|
11950
11861
|
if (!Number.isInteger(expiresInDays) || expiresInDays < 0) {
|
|
11951
|
-
deps.error(
|
|
11862
|
+
deps.error(
|
|
11863
|
+
`--expires-days takes a whole number of days (0 never expires), not "${expiresRaw}".`
|
|
11864
|
+
);
|
|
11952
11865
|
return EXIT_USAGE2;
|
|
11953
11866
|
}
|
|
11954
11867
|
let reportPath;
|
|
@@ -11956,16 +11869,23 @@ async function runShare(rawArgs, depsOverride = {}) {
|
|
|
11956
11869
|
try {
|
|
11957
11870
|
({ path: reportPath, report } = resolveReport(inputPath, deps));
|
|
11958
11871
|
} catch (err) {
|
|
11959
|
-
deps.error(
|
|
11872
|
+
deps.error(
|
|
11873
|
+
`Could not read ${inputPath}: ${err instanceof Error ? err.message : String(err)}`
|
|
11874
|
+
);
|
|
11960
11875
|
return EXIT_USAGE2;
|
|
11961
11876
|
}
|
|
11962
|
-
const reportDir =
|
|
11877
|
+
const reportDir = path19.dirname(reportPath);
|
|
11963
11878
|
const { assets, missing, keyByPath } = planAssets(report, reportDir, deps);
|
|
11964
11879
|
for (const assetPath of missing) {
|
|
11965
|
-
deps.error(
|
|
11880
|
+
deps.error(
|
|
11881
|
+
`Warning: ${assetPath} is missing, so it will not be in the share.`
|
|
11882
|
+
);
|
|
11966
11883
|
}
|
|
11967
11884
|
const shareReport = {
|
|
11968
|
-
...rewriteReportAssets(
|
|
11885
|
+
...rewriteReportAssets(
|
|
11886
|
+
report,
|
|
11887
|
+
(assetPath) => keyByPath.get(assetPath) ?? assetPath
|
|
11888
|
+
),
|
|
11969
11889
|
projectRoot: ""
|
|
11970
11890
|
};
|
|
11971
11891
|
const emails = (parsed.values.emails ?? "").split(",").map((email) => email.trim()).filter(Boolean);
|
|
@@ -11974,7 +11894,10 @@ async function runShare(rawArgs, depsOverride = {}) {
|
|
|
11974
11894
|
try {
|
|
11975
11895
|
const response = await deps.fetchFn(new URL("/api/v1/shares", baseUrl), {
|
|
11976
11896
|
method: "POST",
|
|
11977
|
-
headers: {
|
|
11897
|
+
headers: {
|
|
11898
|
+
"Content-Type": "application/json",
|
|
11899
|
+
Authorization: `Bearer ${key}`
|
|
11900
|
+
},
|
|
11978
11901
|
body: JSON.stringify({
|
|
11979
11902
|
title: parsed.values.title,
|
|
11980
11903
|
report: shareReport,
|
|
@@ -11994,21 +11917,32 @@ async function runShare(rawArgs, depsOverride = {}) {
|
|
|
11994
11917
|
}
|
|
11995
11918
|
created = await response.json();
|
|
11996
11919
|
} catch (err) {
|
|
11997
|
-
deps.error(
|
|
11920
|
+
deps.error(
|
|
11921
|
+
`Could not reach ${baseUrl}: ${err instanceof Error ? err.message : String(err)}`
|
|
11922
|
+
);
|
|
11998
11923
|
return EXIT_SHARE_FAILED;
|
|
11999
11924
|
}
|
|
12000
|
-
const localByKey = new Map(
|
|
11925
|
+
const localByKey = new Map(
|
|
11926
|
+
assets.map((asset) => [asset.path, asset.localPath])
|
|
11927
|
+
);
|
|
12001
11928
|
for (const upload of created.uploads ?? []) {
|
|
12002
11929
|
const filePath = localByKey.get(upload.path);
|
|
12003
11930
|
if (filePath === void 0) {
|
|
12004
|
-
deps.error(
|
|
11931
|
+
deps.error(
|
|
11932
|
+
`Share asked for a file this report did not offer: ${upload.path}`
|
|
11933
|
+
);
|
|
12005
11934
|
return EXIT_SHARE_FAILED;
|
|
12006
11935
|
}
|
|
12007
11936
|
try {
|
|
12008
11937
|
const response = await deps.fetchFn(upload.url, {
|
|
12009
11938
|
method: "PUT",
|
|
12010
|
-
headers: {
|
|
12011
|
-
|
|
11939
|
+
headers: {
|
|
11940
|
+
"Content-Type": contentTypeFor(upload.path),
|
|
11941
|
+
...upload.headers
|
|
11942
|
+
},
|
|
11943
|
+
body: new Blob([deps.readBinary(filePath)], {
|
|
11944
|
+
type: contentTypeFor(upload.path)
|
|
11945
|
+
})
|
|
12012
11946
|
});
|
|
12013
11947
|
if (!response.ok) {
|
|
12014
11948
|
deps.error(`Upload of ${upload.path} failed: HTTP ${response.status}`);
|
|
@@ -12022,12 +11956,17 @@ async function runShare(rawArgs, depsOverride = {}) {
|
|
|
12022
11956
|
}
|
|
12023
11957
|
}
|
|
12024
11958
|
try {
|
|
12025
|
-
const response = await deps.fetchFn(
|
|
12026
|
-
|
|
12027
|
-
|
|
12028
|
-
|
|
11959
|
+
const response = await deps.fetchFn(
|
|
11960
|
+
new URL(`/api/v1/shares/${created.id}/complete`, baseUrl),
|
|
11961
|
+
{
|
|
11962
|
+
method: "POST",
|
|
11963
|
+
headers: { Authorization: `Bearer ${key}` }
|
|
11964
|
+
}
|
|
11965
|
+
);
|
|
12029
11966
|
if (!response.ok) {
|
|
12030
|
-
deps.error(
|
|
11967
|
+
deps.error(
|
|
11968
|
+
`Share could not be published: ${await describeFailure(response)}`
|
|
11969
|
+
);
|
|
12031
11970
|
return EXIT_SHARE_FAILED;
|
|
12032
11971
|
}
|
|
12033
11972
|
} catch (err) {
|
|
@@ -12036,27 +11975,37 @@ async function runShare(rawArgs, depsOverride = {}) {
|
|
|
12036
11975
|
);
|
|
12037
11976
|
return EXIT_SHARE_FAILED;
|
|
12038
11977
|
}
|
|
11978
|
+
const notStored = created.notStored ?? [];
|
|
12039
11979
|
if (parsed.values.json) {
|
|
12040
|
-
deps.log(
|
|
11980
|
+
deps.log(
|
|
11981
|
+
JSON.stringify(
|
|
11982
|
+
{ id: created.id, url: created.url, assets: assets.length, notStored },
|
|
11983
|
+
null,
|
|
11984
|
+
2
|
|
11985
|
+
)
|
|
11986
|
+
);
|
|
12041
11987
|
return EXIT_SUCCESS2;
|
|
12042
11988
|
}
|
|
12043
11989
|
const withAssets = assets.length === 1 ? "1 asset" : `${assets.length} assets`;
|
|
12044
|
-
deps.log(`Shared ${
|
|
11990
|
+
deps.log(`Shared ${path19.basename(reportPath)} (${withAssets}):`);
|
|
12045
11991
|
deps.log(` ${created.url}`);
|
|
12046
11992
|
deps.log(
|
|
12047
11993
|
emails.length > 0 ? ` Only ${emails.join(", ")} can open it, after signing in.` : " Anyone with the link can open it."
|
|
12048
11994
|
);
|
|
11995
|
+
for (const skipped of notStored) {
|
|
11996
|
+
deps.log(` Not stored: ${skipped.path} (${skipped.reason})`);
|
|
11997
|
+
}
|
|
12049
11998
|
return EXIT_SUCCESS2;
|
|
12050
11999
|
}
|
|
12051
12000
|
|
|
12052
12001
|
// src/run-file.ts
|
|
12053
|
-
import
|
|
12054
|
-
import
|
|
12002
|
+
import fs14 from "fs";
|
|
12003
|
+
import path20 from "path";
|
|
12055
12004
|
var DEFAULT_RUN_FILES = [".executable-stories/raw-run.json", "reports/raw-run.json"];
|
|
12056
12005
|
var SUPPORTED_RAW_RUN_SCHEMA = 1;
|
|
12057
12006
|
function findDefaultRunFile(cwd = process.cwd()) {
|
|
12058
12007
|
for (const candidate of DEFAULT_RUN_FILES) {
|
|
12059
|
-
if (
|
|
12008
|
+
if (fs14.existsSync(path20.resolve(cwd, candidate))) return candidate;
|
|
12060
12009
|
}
|
|
12061
12010
|
return void 0;
|
|
12062
12011
|
}
|
|
@@ -12082,8 +12031,8 @@ function diagnoseRunFile(file, cwd = process.cwd()) {
|
|
|
12082
12031
|
});
|
|
12083
12032
|
return { checks, healthy: false };
|
|
12084
12033
|
}
|
|
12085
|
-
const abs =
|
|
12086
|
-
if (!
|
|
12034
|
+
const abs = path20.resolve(cwd, resolved);
|
|
12035
|
+
if (!fs14.existsSync(abs)) {
|
|
12087
12036
|
checks.push({
|
|
12088
12037
|
label: "run file",
|
|
12089
12038
|
status: "fail",
|
|
@@ -12092,7 +12041,7 @@ function diagnoseRunFile(file, cwd = process.cwd()) {
|
|
|
12092
12041
|
});
|
|
12093
12042
|
return { checks, healthy: false };
|
|
12094
12043
|
}
|
|
12095
|
-
const stat =
|
|
12044
|
+
const stat = fs14.statSync(abs);
|
|
12096
12045
|
checks.push({
|
|
12097
12046
|
label: "run file",
|
|
12098
12047
|
status: "ok",
|
|
@@ -12100,7 +12049,7 @@ function diagnoseRunFile(file, cwd = process.cwd()) {
|
|
|
12100
12049
|
});
|
|
12101
12050
|
let parsed;
|
|
12102
12051
|
try {
|
|
12103
|
-
parsed = JSON.parse(
|
|
12052
|
+
parsed = JSON.parse(fs14.readFileSync(abs, "utf8"));
|
|
12104
12053
|
} catch (err) {
|
|
12105
12054
|
checks.push({
|
|
12106
12055
|
label: "json",
|
|
@@ -12187,246 +12136,6 @@ function formatDoctorReport(report) {
|
|
|
12187
12136
|
return lines.join("\n");
|
|
12188
12137
|
}
|
|
12189
12138
|
|
|
12190
|
-
// src/scaffold-doc.ts
|
|
12191
|
-
import * as fs17 from "fs";
|
|
12192
|
-
import * as path23 from "path";
|
|
12193
|
-
var TEMPLATES = [
|
|
12194
|
-
"adr",
|
|
12195
|
-
"runbook",
|
|
12196
|
-
"decision-log",
|
|
12197
|
-
"incident",
|
|
12198
|
-
"scenario-note"
|
|
12199
|
-
];
|
|
12200
|
-
function slugify2(input) {
|
|
12201
|
-
return input.toLowerCase().trim().replace(/['"]/g, "").replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "untitled";
|
|
12202
|
-
}
|
|
12203
|
-
function isoDate(today) {
|
|
12204
|
-
return today.toISOString().slice(0, 10);
|
|
12205
|
-
}
|
|
12206
|
-
function nextSeq(dir) {
|
|
12207
|
-
let max = 0;
|
|
12208
|
-
try {
|
|
12209
|
-
for (const entry of fs17.readdirSync(dir)) {
|
|
12210
|
-
const match = /^(\d{1,4})-/.exec(entry);
|
|
12211
|
-
if (match) max = Math.max(max, Number.parseInt(match[1], 10));
|
|
12212
|
-
}
|
|
12213
|
-
} catch {
|
|
12214
|
-
}
|
|
12215
|
-
return String(max + 1).padStart(4, "0");
|
|
12216
|
-
}
|
|
12217
|
-
var COMPONENTS = "../../../components";
|
|
12218
|
-
var TEMPLATE_SPECS = {
|
|
12219
|
-
adr: {
|
|
12220
|
-
subdir: "adr",
|
|
12221
|
-
filename: (slug2, ctx) => `${ctx.seq}-${slug2}`,
|
|
12222
|
-
content: (ctx) => `---
|
|
12223
|
-
title: 'ADR ${ctx.seq} \u2014 ${ctx.name}'
|
|
12224
|
-
description: '${ctx.name}'
|
|
12225
|
-
# Link the stories that prove this decision. The badge under the title turns
|
|
12226
|
-
# red the moment any of them fail, so this record can't drift from the code.
|
|
12227
|
-
verifiedBy: []
|
|
12228
|
-
---
|
|
12229
|
-
|
|
12230
|
-
## Status
|
|
12231
|
-
|
|
12232
|
-
**Proposed** \u2014 proposed \xB7 accepted \xB7 superseded \xB7 deprecated
|
|
12233
|
-
|
|
12234
|
-
## Context
|
|
12235
|
-
|
|
12236
|
-
_What problem are we solving? What constraints and forces apply?_
|
|
12237
|
-
|
|
12238
|
-
## Decision
|
|
12239
|
-
|
|
12240
|
-
_What did we decide to do?_
|
|
12241
|
-
|
|
12242
|
-
## Consequences
|
|
12243
|
-
|
|
12244
|
-
_What becomes easier, and what becomes harder, as a result?_
|
|
12245
|
-
|
|
12246
|
-
## Verified by
|
|
12247
|
-
|
|
12248
|
-
Add the story ids or tags that exercise this decision to \`verifiedBy\` in the
|
|
12249
|
-
frontmatter above. Until you do, the badge reads **Unverified** \u2014 by design.
|
|
12250
|
-
`
|
|
12251
|
-
},
|
|
12252
|
-
runbook: {
|
|
12253
|
-
subdir: "runbooks",
|
|
12254
|
-
filename: (slug2) => slug2,
|
|
12255
|
-
content: (ctx) => `---
|
|
12256
|
-
title: 'Runbook \u2014 ${ctx.name}'
|
|
12257
|
-
description: 'Operational runbook for ${ctx.name}'
|
|
12258
|
-
---
|
|
12259
|
-
|
|
12260
|
-
import Checklist from '${COMPONENTS}/Checklist.astro';
|
|
12261
|
-
import VerifiedStep from '${COMPONENTS}/VerifiedStep.astro';
|
|
12262
|
-
|
|
12263
|
-
_When to use this runbook, prerequisites, and who to contact._
|
|
12264
|
-
|
|
12265
|
-
## Steps
|
|
12266
|
-
|
|
12267
|
-
Each step linked with \`story=\` shows a live green check when its test passed in
|
|
12268
|
-
the last run \u2014 so this runbook is trustworthy, not aspirational.
|
|
12269
|
-
|
|
12270
|
-
<Checklist>
|
|
12271
|
-
<VerifiedStep story="">Describe the first action, and link the story that verifies it.</VerifiedStep>
|
|
12272
|
-
<VerifiedStep>A manual step with no automated check.</VerifiedStep>
|
|
12273
|
-
</Checklist>
|
|
12274
|
-
|
|
12275
|
-
## Rollback
|
|
12276
|
-
|
|
12277
|
-
_How to safely undo if something goes wrong._
|
|
12278
|
-
`
|
|
12279
|
-
},
|
|
12280
|
-
"decision-log": {
|
|
12281
|
-
subdir: "decisions",
|
|
12282
|
-
filename: (slug2) => slug2,
|
|
12283
|
-
content: (ctx) => `---
|
|
12284
|
-
title: 'Decision log \u2014 ${ctx.name}'
|
|
12285
|
-
description: 'Running log of decisions for ${ctx.name}'
|
|
12286
|
-
---
|
|
12287
|
-
|
|
12288
|
-
A lightweight running log. For weightier decisions, scaffold a full ADR with
|
|
12289
|
-
\`executable-stories new adr\`.
|
|
12290
|
-
|
|
12291
|
-
| Date | Decision | Owner | Verified by |
|
|
12292
|
-
| ---- | -------- | ----- | ----------- |
|
|
12293
|
-
| ${ctx.isoDate} | _What was decided_ | _Who_ | _story id or tag_ |
|
|
12294
|
-
`
|
|
12295
|
-
},
|
|
12296
|
-
incident: {
|
|
12297
|
-
subdir: "incidents",
|
|
12298
|
-
filename: (slug2, ctx) => `${ctx.isoDate}-${slug2}`,
|
|
12299
|
-
content: (ctx) => `---
|
|
12300
|
-
title: 'Incident \u2014 ${ctx.name}'
|
|
12301
|
-
description: 'Post-mortem for ${ctx.name}'
|
|
12302
|
-
# Link the regression story added to stop this recurring.
|
|
12303
|
-
verifiedBy: []
|
|
12304
|
-
---
|
|
12305
|
-
|
|
12306
|
-
## Summary
|
|
12307
|
-
|
|
12308
|
-
_What happened, who was affected, and for how long._
|
|
12309
|
-
|
|
12310
|
-
## Timeline
|
|
12311
|
-
|
|
12312
|
-
| Time | Event |
|
|
12313
|
-
| ---- | ----- |
|
|
12314
|
-
| ${ctx.isoDate} | Detected |
|
|
12315
|
-
|
|
12316
|
-
## Root cause
|
|
12317
|
-
|
|
12318
|
-
_The underlying cause, not just the trigger._
|
|
12319
|
-
|
|
12320
|
-
## Resolution
|
|
12321
|
-
|
|
12322
|
-
_How it was fixed._
|
|
12323
|
-
|
|
12324
|
-
## Action items
|
|
12325
|
-
|
|
12326
|
-
- [ ] Add a regression story and link it in \`verifiedBy\` so a silent recurrence
|
|
12327
|
-
becomes a failing badge.
|
|
12328
|
-
`
|
|
12329
|
-
},
|
|
12330
|
-
"scenario-note": {
|
|
12331
|
-
subdir: "notes",
|
|
12332
|
-
filename: (_slug, ctx) => ctx.scenarioId ?? ctx.slug,
|
|
12333
|
-
content: (ctx) => `---
|
|
12334
|
-
title: 'Business context \u2014 ${ctx.name}'
|
|
12335
|
-
description: 'Stakeholder context for ${ctx.name}'
|
|
12336
|
-
scenarioId: ${ctx.scenarioId}
|
|
12337
|
-
# Link this note back to the scenario it explains so the badge and explorer stay aligned.
|
|
12338
|
-
verifiedBy: [${ctx.scenarioId}]
|
|
12339
|
-
---
|
|
12340
|
-
|
|
12341
|
-
This page is hand-written commentary for a generated scenario. The generated
|
|
12342
|
-
stories render live from the run JSON, so this page is never overwritten.
|
|
12343
|
-
|
|
12344
|
-
## Why this behavior matters
|
|
12345
|
-
|
|
12346
|
-
_Describe the business rule, policy, customer promise, or operational nuance._
|
|
12347
|
-
|
|
12348
|
-
## Caveats
|
|
12349
|
-
|
|
12350
|
-
- _What readers should know when this scenario passes_
|
|
12351
|
-
- _Any assumptions, exclusions, or follow-up links_
|
|
12352
|
-
`
|
|
12353
|
-
}
|
|
12354
|
-
};
|
|
12355
|
-
function isTemplateName(value) {
|
|
12356
|
-
return TEMPLATES.includes(value);
|
|
12357
|
-
}
|
|
12358
|
-
function scaffoldDoc(options) {
|
|
12359
|
-
const { template } = options;
|
|
12360
|
-
if (!isTemplateName(template)) {
|
|
12361
|
-
throw new Error(
|
|
12362
|
-
`Unknown template "${template}". Available: ${TEMPLATES.join(", ")}.`
|
|
12363
|
-
);
|
|
12364
|
-
}
|
|
12365
|
-
const spec = TEMPLATE_SPECS[template];
|
|
12366
|
-
const baseDir = options.baseDir ?? path23.join("src", "content", "docs");
|
|
12367
|
-
const today = options.today ?? /* @__PURE__ */ new Date();
|
|
12368
|
-
const name = (options.name ?? "").trim() || defaultName(template);
|
|
12369
|
-
const slug2 = slugify2(name);
|
|
12370
|
-
const scenarioId = normalizeScenarioId(options.scenarioId);
|
|
12371
|
-
const dir = path23.join(baseDir, spec.subdir);
|
|
12372
|
-
if (template === "scenario-note" && !scenarioId) {
|
|
12373
|
-
throw new Error(`Template "scenario-note" requires --scenario-id.`);
|
|
12374
|
-
}
|
|
12375
|
-
const ctx = {
|
|
12376
|
-
name,
|
|
12377
|
-
slug: slug2,
|
|
12378
|
-
scenarioId,
|
|
12379
|
-
isoDate: isoDate(today),
|
|
12380
|
-
seq: nextSeq(dir)
|
|
12381
|
-
};
|
|
12382
|
-
const filename = `${spec.filename(slug2, ctx)}.mdx`;
|
|
12383
|
-
const filePath = path23.join(dir, filename);
|
|
12384
|
-
if (fs17.existsSync(filePath) && !options.force) {
|
|
12385
|
-
throw new Error(
|
|
12386
|
-
`File "${filePath}" already exists. Use --force to overwrite.`
|
|
12387
|
-
);
|
|
12388
|
-
}
|
|
12389
|
-
fs17.mkdirSync(dir, { recursive: true });
|
|
12390
|
-
fs17.writeFileSync(filePath, spec.content(ctx), "utf8");
|
|
12391
|
-
return { template, path: filePath, title: titleFor2(template, ctx) };
|
|
12392
|
-
}
|
|
12393
|
-
function defaultName(template) {
|
|
12394
|
-
switch (template) {
|
|
12395
|
-
case "adr":
|
|
12396
|
-
return "Untitled decision";
|
|
12397
|
-
case "runbook":
|
|
12398
|
-
return "Untitled runbook";
|
|
12399
|
-
case "decision-log":
|
|
12400
|
-
return "Decisions";
|
|
12401
|
-
case "incident":
|
|
12402
|
-
return "Untitled incident";
|
|
12403
|
-
case "scenario-note":
|
|
12404
|
-
return "Untitled scenario note";
|
|
12405
|
-
}
|
|
12406
|
-
}
|
|
12407
|
-
function titleFor2(template, ctx) {
|
|
12408
|
-
switch (template) {
|
|
12409
|
-
case "adr":
|
|
12410
|
-
return `ADR ${ctx.seq} \u2014 ${ctx.name}`;
|
|
12411
|
-
case "runbook":
|
|
12412
|
-
return `Runbook \u2014 ${ctx.name}`;
|
|
12413
|
-
case "decision-log":
|
|
12414
|
-
return `Decision log \u2014 ${ctx.name}`;
|
|
12415
|
-
case "incident":
|
|
12416
|
-
return `Incident \u2014 ${ctx.name}`;
|
|
12417
|
-
case "scenario-note":
|
|
12418
|
-
return `Business context \u2014 ${ctx.name}`;
|
|
12419
|
-
}
|
|
12420
|
-
}
|
|
12421
|
-
function normalizeScenarioId(input) {
|
|
12422
|
-
const value = input?.trim();
|
|
12423
|
-
if (!value) return void 0;
|
|
12424
|
-
if (value.includes("/") || value.includes("\\")) {
|
|
12425
|
-
throw new Error(`scenarioId must not contain path separators.`);
|
|
12426
|
-
}
|
|
12427
|
-
return value;
|
|
12428
|
-
}
|
|
12429
|
-
|
|
12430
12139
|
// src/summary-line.ts
|
|
12431
12140
|
function summaryLine(counts, files, durationMs, options = {}) {
|
|
12432
12141
|
const { passed, failed, skipped, pending } = counts;
|
|
@@ -12456,8 +12165,8 @@ function summaryLine(counts, files, durationMs, options = {}) {
|
|
|
12456
12165
|
}
|
|
12457
12166
|
|
|
12458
12167
|
// src/sync/run.ts
|
|
12459
|
-
import * as
|
|
12460
|
-
import * as
|
|
12168
|
+
import * as fs15 from "fs";
|
|
12169
|
+
import * as path21 from "path";
|
|
12461
12170
|
import { parseArgs as parseArgs3 } from "util";
|
|
12462
12171
|
import { canonicalizeRun as canonicalizeRun6 } from "executable-stories-core/converters/acl/canonicalize";
|
|
12463
12172
|
import { synthesizeStories as synthesizeStories4 } from "executable-stories-core/converters/synthesize";
|
|
@@ -12512,11 +12221,11 @@ pull request that created the case.
|
|
|
12512
12221
|
Exit codes: 0 applied (or planned), 1 some writes failed, 4 usage error.`;
|
|
12513
12222
|
function defaultDeps3() {
|
|
12514
12223
|
return {
|
|
12515
|
-
readFile: (filePath) =>
|
|
12516
|
-
fileExists: (filePath) =>
|
|
12224
|
+
readFile: (filePath) => fs15.readFileSync(filePath, "utf8"),
|
|
12225
|
+
fileExists: (filePath) => fs15.existsSync(filePath),
|
|
12517
12226
|
writeFile: (filePath, contents) => {
|
|
12518
|
-
|
|
12519
|
-
|
|
12227
|
+
fs15.mkdirSync(path21.dirname(path21.resolve(filePath)), { recursive: true });
|
|
12228
|
+
fs15.writeFileSync(filePath, contents, "utf8");
|
|
12520
12229
|
},
|
|
12521
12230
|
fetchFn: globalThis.fetch,
|
|
12522
12231
|
env: process.env,
|
|
@@ -12697,8 +12406,8 @@ async function runSyncCommand(mode, rawArgs, depsOverride = {}) {
|
|
|
12697
12406
|
deps.error(`Could not read from ${providerName}: ${err instanceof Error ? err.message : String(err)}`);
|
|
12698
12407
|
return EXIT_FAILED;
|
|
12699
12408
|
}
|
|
12700
|
-
const jsonPath =
|
|
12701
|
-
const markdownPath =
|
|
12409
|
+
const jsonPath = path21.join(outputDir, `sync-coverage.${providerName}.json`);
|
|
12410
|
+
const markdownPath = path21.join(outputDir, `sync-coverage.${providerName}.md`);
|
|
12702
12411
|
deps.writeFile(jsonPath, `${JSON.stringify(buildCoverageJson(analysis), null, 2)}
|
|
12703
12412
|
`);
|
|
12704
12413
|
deps.writeFile(markdownPath, `${renderCoverageMarkdown(analysis)}
|
|
@@ -13767,17 +13476,17 @@ function validateRawRun(data) {
|
|
|
13767
13476
|
return { valid: true, errors: [] };
|
|
13768
13477
|
}
|
|
13769
13478
|
const errors = (validate.errors ?? []).map((err) => {
|
|
13770
|
-
const
|
|
13479
|
+
const path23 = err.instancePath || "/";
|
|
13771
13480
|
const message = err.message ?? "unknown error";
|
|
13772
13481
|
if (err.keyword === "additionalProperties") {
|
|
13773
13482
|
const extra = err.params.additionalProperty;
|
|
13774
|
-
return `${
|
|
13483
|
+
return `${path23}: ${message} \u2014 '${extra}'`;
|
|
13775
13484
|
}
|
|
13776
13485
|
if (err.keyword === "enum") {
|
|
13777
13486
|
const allowed = err.params.allowedValues;
|
|
13778
|
-
return `${
|
|
13487
|
+
return `${path23}: ${message} \u2014 allowed: ${JSON.stringify(allowed)}`;
|
|
13779
13488
|
}
|
|
13780
|
-
return `${
|
|
13489
|
+
return `${path23}: ${message}`;
|
|
13781
13490
|
});
|
|
13782
13491
|
return { valid: false, errors };
|
|
13783
13492
|
}
|
|
@@ -13792,7 +13501,7 @@ import { toStoryReport as toStoryReport9 } from "executable-stories-core/convert
|
|
|
13792
13501
|
function buildGhAttachCommand(args) {
|
|
13793
13502
|
const assets = collectReportAssets2(toStoryReport9(args.run));
|
|
13794
13503
|
if (assets.length === 0) return void 0;
|
|
13795
|
-
const attachments = assets.map((
|
|
13504
|
+
const attachments = assets.map((path23) => `--attach '${path23}'`).join(" \\\n ");
|
|
13796
13505
|
return `gh pr comment ${args.pr ?? "<number>"} --body-file ${args.bodyFile} \\
|
|
13797
13506
|
${attachments}`;
|
|
13798
13507
|
}
|
|
@@ -13833,15 +13542,11 @@ USAGE
|
|
|
13833
13542
|
executable-stories triage <file|directory> [--baseline <path|auto>] [--triage-format text|json] [--by-owner]
|
|
13834
13543
|
executable-stories validate <file>
|
|
13835
13544
|
executable-stories validate --stdin
|
|
13836
|
-
executable-stories dev [directory]
|
|
13837
|
-
executable-stories init-astro [directory] [--install] [--force] [--update]
|
|
13838
|
-
executable-stories new <template> "<name>" [options]
|
|
13839
13545
|
executable-stories check-links <dir> [options]
|
|
13840
13546
|
executable-stories push <run.json|results.xml|allure-results/> [--format <fmt>] [--title <text>] [--env <name>] [--description <text|@file.md>] [--gate] [--force]
|
|
13841
13547
|
executable-stories share <reports-dir|report.json> [--emails <a@b,c@d>] [--expires-days <n>]
|
|
13842
13548
|
executable-stories coverage <testrail|xray> <run.json> [options]
|
|
13843
13549
|
executable-stories sync <testrail|xray> <run.json> [--apply] [options]
|
|
13844
|
-
executable-stories import-openapi <spec> [options]
|
|
13845
13550
|
executable-stories publish-confluence <file.adf.json> [options]
|
|
13846
13551
|
executable-stories publish-jira <file.adf.json> [options]
|
|
13847
13552
|
executable-stories deploy record <file> --env <env> [--tag <tag>] [options]
|
|
@@ -13863,27 +13568,24 @@ SUBCOMMANDS
|
|
|
13863
13568
|
doctor Diagnose the run JSON: where it is, whether it parses, schema version vs this CLI, what it contains
|
|
13864
13569
|
runs Inspect or clear the accumulated run state: "runs status", "runs reset"
|
|
13865
13570
|
completion Output a shell completion script (bash, zsh, fish)
|
|
13866
|
-
init-astro Scaffold a thin Astro docs site (Starlight + executable-stories-astro; live stories at /stories)
|
|
13867
|
-
new Scaffold a docs page from a template (adr, runbook, decision-log, incident, scenario-note)
|
|
13868
13571
|
check-links Scan docs for broken internal/external links (CI-friendly exit code)
|
|
13869
13572
|
push Send a run to a cloud ingest endpoint: StoryReport, raw run, JUnit XML, Playwright JSON or allure-results
|
|
13870
13573
|
share Publish a report (with its screenshots and video) and print a link to it
|
|
13871
13574
|
coverage Compare your stories against a test-management system (read-only)
|
|
13872
13575
|
sync Push cases, executions, and evidence to TestRail or Xray (dry run by default)
|
|
13873
|
-
import-openapi Generate API doc pages from an OpenAPI spec, linked to verifying stories
|
|
13874
13576
|
publish-confluence Publish an ADF JSON file to a Confluence page via REST API
|
|
13875
13577
|
publish-jira Publish an ADF JSON file to a Jira issue (as comment or description)
|
|
13876
13578
|
deploy Record deployments, show environment status, detect drift
|
|
13877
13579
|
|
|
13878
13580
|
OPTIONS
|
|
13879
13581
|
--format <formats> Comma-separated formats: html, markdown, release-manifest, traceability-matrix, traceability-csv, junit, cucumber-json, cucumber-messages, cucumber-html, astro-markdown, confluence, story-report-json, scenario-index-json, behavior-manifest-json, agent-text, span-graph, or custom names from config (default: html)
|
|
13880
|
-
astro-markdown Starlight-flavored Markdown (single aggregated page
|
|
13582
|
+
astro-markdown Starlight-flavored Markdown (single aggregated page)
|
|
13881
13583
|
confluence Atlassian Document Format (ADF) JSON for Confluence / Jira
|
|
13882
13584
|
behavior-manifest-json Agent-readable behavior manifest and debugger warnings
|
|
13883
13585
|
agent-text Full run as flat token-lean plain text for pasting into an LLM
|
|
13884
13586
|
span-graph Architecture the run exercised, from its OTel spans, as mermaid.
|
|
13885
13587
|
Writes nothing when the run carries no spans
|
|
13886
|
-
html Standalone interactive HTML report, rendered via executable-stories-react
|
|
13588
|
+
html Standalone interactive HTML report, rendered via executable-stories-react
|
|
13887
13589
|
cucumber-html Official Cucumber HTML report
|
|
13888
13590
|
markdown Markdown documentation
|
|
13889
13591
|
junit JUnit XML
|
|
@@ -14007,6 +13709,14 @@ TRIAGE
|
|
|
14007
13709
|
with no covers are flagged. --triage-format json emits the work queue. triage
|
|
14008
13710
|
always exits 0 \u2014 it reports work, it does not gate.
|
|
14009
13711
|
|
|
13712
|
+
JEV (optional)
|
|
13713
|
+
With JEV_API_KEY set, triage, goal, and review ask Jev (TypeSafe AI) the
|
|
13714
|
+
bounded questions their rules leave blank: triage suggests a covers path and
|
|
13715
|
+
a failure kind (product/test/infra) for unrouted failures; goal flags
|
|
13716
|
+
rewritten scenarios that check less as advisories; review infers change-type
|
|
13717
|
+
for claims with no change:* tag. Every answer carries its probability and
|
|
13718
|
+
none of them changes an exit code. JEV_MODEL / JEV_ENDPOINT override defaults.
|
|
13719
|
+
|
|
14010
13720
|
COMPARE
|
|
14011
13721
|
compare supports --format html,markdown,changelog
|
|
14012
13722
|
changelog writes a release-notes-style behavior changelog (<output-name>.changelog.md)
|
|
@@ -14030,13 +13740,6 @@ DEPLOY
|
|
|
14030
13740
|
executable-stories deploy diff <env-a> <env-b> [--ledger <path>]
|
|
14031
13741
|
Show scenario drift between two environments (what's in one but not the other).
|
|
14032
13742
|
|
|
14033
|
-
INIT-ASTRO
|
|
14034
|
-
executable-stories dev [directory] Run the live docs site (default: ./story-docs); installs its deps on first use
|
|
14035
|
-
executable-stories init-astro [directory] Scaffold into directory (default: ./story-docs)
|
|
14036
|
-
--install Also run the package manager install (detected from your lockfile) so the site is ready to \`dev\`
|
|
14037
|
-
--force Write into a non-empty directory (overlays template files)
|
|
14038
|
-
--update Refresh framework files only (keeps your content + config)
|
|
14039
|
-
|
|
14040
13743
|
PUBLISH-CONFLUENCE
|
|
14041
13744
|
executable-stories publish-confluence <file.adf.json> [options]
|
|
14042
13745
|
--page-id <id> Update an existing page (alternative to --space-id)
|
|
@@ -14183,19 +13886,9 @@ async function parseCliArgs(argv) {
|
|
|
14183
13886
|
process.exit(EXIT_SUCCESS4);
|
|
14184
13887
|
}
|
|
14185
13888
|
const subcommand = args[0];
|
|
14186
|
-
if (subcommand !== "format" && subcommand !== "watch" && subcommand !== "compare" && subcommand !== "gate-release" && subcommand !== "deploy" && subcommand !== "review" && subcommand !== "list" && subcommand !== "check" && subcommand !== "check-explainers" && subcommand !== "goal" && subcommand !== "triage" && subcommand !== "validate" && subcommand !== "doctor" && subcommand !== "completion" && subcommand !== "
|
|
14187
|
-
if (subcommand === "serve" || subcommand === "build-docs") {
|
|
14188
|
-
console.error(
|
|
14189
|
-
`The "${subcommand}" subcommand was removed. Living docs are now an Astro site, rendered live from the run JSON (no Markdown generation step):
|
|
14190
|
-
1. executable-stories init-astro --install (one-time scaffold)
|
|
14191
|
-
2. run your tests in watch mode in one terminal
|
|
14192
|
-
3. run \`executable-stories dev\` in another \u2014 it hot-reloads the docs.
|
|
14193
|
-
See: https://github.com/jagreehal/executable-stories (executable-stories-astro).`
|
|
14194
|
-
);
|
|
14195
|
-
process.exit(EXIT_USAGE4);
|
|
14196
|
-
}
|
|
13889
|
+
if (subcommand !== "format" && subcommand !== "watch" && subcommand !== "compare" && subcommand !== "gate-release" && subcommand !== "deploy" && subcommand !== "review" && subcommand !== "list" && subcommand !== "check" && subcommand !== "check-explainers" && subcommand !== "goal" && subcommand !== "triage" && subcommand !== "validate" && subcommand !== "doctor" && subcommand !== "completion" && subcommand !== "check-links" && subcommand !== "push" && subcommand !== "share" && subcommand !== "publish-confluence" && subcommand !== "publish-jira" && subcommand !== "sync" && subcommand !== "coverage" && subcommand !== "runs") {
|
|
14197
13890
|
console.error(
|
|
14198
|
-
`Unknown subcommand: "${subcommand}". Use "format", "watch", "compare", "gate-release", "deploy", "review", "list", "check", "check-explainers", "goal", "triage", "validate", "doctor", "completion", "
|
|
13891
|
+
`Unknown subcommand: "${subcommand}". Use "format", "watch", "compare", "gate-release", "deploy", "review", "list", "check", "check-explainers", "goal", "triage", "validate", "doctor", "completion", "check-links", "push", "share", "sync", "coverage", "publish-confluence", or "publish-jira".`
|
|
14199
13892
|
);
|
|
14200
13893
|
process.exit(EXIT_USAGE4);
|
|
14201
13894
|
}
|
|
@@ -14218,15 +13911,15 @@ See: https://github.com/jagreehal/executable-stories (executable-stories-astro).
|
|
|
14218
13911
|
const outputDirFlag = args.indexOf("--output-dir");
|
|
14219
13912
|
const outputDir = outputDirFlag >= 0 ? args[outputDirFlag + 1] : void 0;
|
|
14220
13913
|
const deps = {
|
|
14221
|
-
readFile: (filePath) =>
|
|
13914
|
+
readFile: (filePath) => fs16.readFileSync(filePath, "utf8"),
|
|
14222
13915
|
listDir: (dir) => {
|
|
14223
13916
|
try {
|
|
14224
|
-
return
|
|
13917
|
+
return fs16.readdirSync(dir);
|
|
14225
13918
|
} catch {
|
|
14226
13919
|
return void 0;
|
|
14227
13920
|
}
|
|
14228
13921
|
},
|
|
14229
|
-
removeFile: (filePath) =>
|
|
13922
|
+
removeFile: (filePath) => fs16.rmSync(filePath, { force: true }),
|
|
14230
13923
|
logger: console
|
|
14231
13924
|
};
|
|
14232
13925
|
if (action === "status") {
|
|
@@ -14258,88 +13951,6 @@ See: https://github.com/jagreehal/executable-stories (executable-stories-astro).
|
|
|
14258
13951
|
if (subcommand === "deploy") {
|
|
14259
13952
|
process.exit(await runDeploy(args.slice(1)));
|
|
14260
13953
|
}
|
|
14261
|
-
if (subcommand === "dev") {
|
|
14262
|
-
const devArgs = args.slice(1);
|
|
14263
|
-
const siteDir = devArgs.find((a) => !a.startsWith("--")) ?? "./story-docs";
|
|
14264
|
-
const dev = runDocsDev(siteDir);
|
|
14265
|
-
if (dev.kind === "not-scaffolded") {
|
|
14266
|
-
console.error(
|
|
14267
|
-
`No docs site found at ${siteDir}. Create one (scaffold + install) with:
|
|
14268
|
-
npx executable-stories init-astro --install`
|
|
14269
|
-
);
|
|
14270
|
-
process.exit(EXIT_USAGE4);
|
|
14271
|
-
}
|
|
14272
|
-
if (dev.kind === "install-failed") {
|
|
14273
|
-
console.error(
|
|
14274
|
-
`"${dev.pm} install" failed in ${siteDir} \u2014 run it manually, then retry.`
|
|
14275
|
-
);
|
|
14276
|
-
process.exit(EXIT_GENERATION);
|
|
14277
|
-
}
|
|
14278
|
-
process.exit(dev.status ?? EXIT_GENERATION);
|
|
14279
|
-
}
|
|
14280
|
-
if (subcommand === "init-astro") {
|
|
14281
|
-
const initArgs = args.slice(1);
|
|
14282
|
-
const targetDir = initArgs.find((a) => !a.startsWith("--")) ?? "./story-docs";
|
|
14283
|
-
const force = initArgs.includes("--force");
|
|
14284
|
-
const update = initArgs.includes("--update");
|
|
14285
|
-
const install = initArgs.includes("--install");
|
|
14286
|
-
try {
|
|
14287
|
-
const result = initAstro({ targetDir, force, update });
|
|
14288
|
-
if (update) {
|
|
14289
|
-
console.log(
|
|
14290
|
-
`Updated ${result.targetDir} (content + config left untouched)`
|
|
14291
|
-
);
|
|
14292
|
-
console.log(
|
|
14293
|
-
" Framework updates come via: pnpm update executable-stories-astro"
|
|
14294
|
-
);
|
|
14295
|
-
process.exit(EXIT_SUCCESS4);
|
|
14296
|
-
}
|
|
14297
|
-
console.log(`Scaffolded Astro docs site at ${result.targetDir}`);
|
|
14298
|
-
const pm = detectPackageManager();
|
|
14299
|
-
if (install) {
|
|
14300
|
-
console.log(`Installing dependencies with ${pm}\u2026`);
|
|
14301
|
-
if (!installScaffoldDependencies(result.targetDir, pm)) {
|
|
14302
|
-
console.error(
|
|
14303
|
-
`Scaffold complete, but "${pm} install" failed in ${result.targetDir} \u2014 run it manually, then \`${pm} run dev\`.`
|
|
14304
|
-
);
|
|
14305
|
-
process.exit(EXIT_GENERATION);
|
|
14306
|
-
}
|
|
14307
|
-
}
|
|
14308
|
-
console.log("");
|
|
14309
|
-
console.log("Next steps:");
|
|
14310
|
-
let step = 1;
|
|
14311
|
-
if (!install) {
|
|
14312
|
-
console.log(` ${step++}. cd ${result.targetDir} && ${pm} install`);
|
|
14313
|
-
}
|
|
14314
|
-
console.log(
|
|
14315
|
-
` ${step++}. In your TEST project, add the StoryReporter with a rawRunPath, e.g.`
|
|
14316
|
-
);
|
|
14317
|
-
console.log(
|
|
14318
|
-
" StoryReporter({ rawRunPath: 'reports/raw-run.json' })"
|
|
14319
|
-
);
|
|
14320
|
-
console.log(
|
|
14321
|
-
` ${step++}. Run your tests in watch mode (terminal 1): ${pm} test --watch`
|
|
14322
|
-
);
|
|
14323
|
-
console.log(
|
|
14324
|
-
` ${step++}. Run the docs dev server (terminal 2): npx executable-stories dev`
|
|
14325
|
-
);
|
|
14326
|
-
console.log(
|
|
14327
|
-
" Editing tests hot-reloads the Stories pages \u2014 nothing is written to disk."
|
|
14328
|
-
);
|
|
14329
|
-
console.log("");
|
|
14330
|
-
console.log(
|
|
14331
|
-
"Everything is configured in one file: executable-stories.config.mjs"
|
|
14332
|
-
);
|
|
14333
|
-
console.log(
|
|
14334
|
-
" \u2014 sources, scenario selection (include/exclude), grouping (groupBy), docs, and theme."
|
|
14335
|
-
);
|
|
14336
|
-
process.exit(EXIT_SUCCESS4);
|
|
14337
|
-
} catch (err) {
|
|
14338
|
-
console.error(`Error: ${err.message}`);
|
|
14339
|
-
process.exit(EXIT_USAGE4);
|
|
14340
|
-
}
|
|
14341
|
-
}
|
|
14342
|
-
if (subcommand === "new") process.exit(runNew(args.slice(1)));
|
|
14343
13954
|
if (subcommand === "check-links")
|
|
14344
13955
|
process.exit(await runCheckLinks(args.slice(1)));
|
|
14345
13956
|
if (subcommand === "push") process.exit(await runPush(args.slice(1)));
|
|
@@ -14348,8 +13959,6 @@ See: https://github.com/jagreehal/executable-stories (executable-stories-astro).
|
|
|
14348
13959
|
process.exit(await runSyncCommand("sync", args.slice(1)));
|
|
14349
13960
|
if (subcommand === "coverage")
|
|
14350
13961
|
process.exit(await runSyncCommand("coverage", args.slice(1)));
|
|
14351
|
-
if (subcommand === "import-openapi")
|
|
14352
|
-
process.exit(await runImportOpenApi(args.slice(1)));
|
|
14353
13962
|
const { values, positionals, tokens } = parseArgs4({
|
|
14354
13963
|
args: args.slice(1),
|
|
14355
13964
|
options: CLI_OPTIONS,
|
|
@@ -14467,7 +14076,7 @@ See: https://github.com/jagreehal/executable-stories (executable-stories-astro).
|
|
|
14467
14076
|
const requestedFormats = preset.formats;
|
|
14468
14077
|
if (requestedFormats.includes("astro")) {
|
|
14469
14078
|
console.warn(
|
|
14470
|
-
"\u26A0 The 'astro' format was renamed to 'astro-markdown'
|
|
14079
|
+
"\u26A0 The 'astro' format was renamed to 'astro-markdown'. '--format astro' still works but will be\n removed in a future major; use 'astro-markdown'."
|
|
14471
14080
|
);
|
|
14472
14081
|
}
|
|
14473
14082
|
const allRequestedFormats = requestedFormats.map(
|
|
@@ -14719,30 +14328,30 @@ async function readInput2(args) {
|
|
|
14719
14328
|
if (args.stdin) {
|
|
14720
14329
|
return readStdin();
|
|
14721
14330
|
}
|
|
14722
|
-
const filePath =
|
|
14723
|
-
if (!
|
|
14331
|
+
const filePath = path22.resolve(args.inputFile);
|
|
14332
|
+
if (!fs16.existsSync(filePath)) {
|
|
14724
14333
|
console.error(`Error: File not found: ${filePath}`);
|
|
14725
14334
|
process.exit(EXIT_USAGE4);
|
|
14726
14335
|
}
|
|
14727
|
-
return
|
|
14336
|
+
return fs16.readFileSync(filePath, "utf8");
|
|
14728
14337
|
}
|
|
14729
14338
|
function validateReportsDirectory(args) {
|
|
14730
14339
|
const input = args.inputFile;
|
|
14731
14340
|
if (!input || args.stdin) return void 0;
|
|
14732
|
-
const resolved =
|
|
14733
|
-
if (!
|
|
14341
|
+
const resolved = path22.resolve(input);
|
|
14342
|
+
if (!fs16.existsSync(resolved) || !fs16.statSync(resolved).isDirectory())
|
|
14734
14343
|
return void 0;
|
|
14735
|
-
const names =
|
|
14344
|
+
const names = fs16.readdirSync(resolved).filter((n) => n.endsWith(".json")).sort();
|
|
14736
14345
|
if (names.length === 0) {
|
|
14737
14346
|
console.error(`Error: no per-file reports (*.json) in ${input}.`);
|
|
14738
14347
|
return EXIT_USAGE4;
|
|
14739
14348
|
}
|
|
14740
14349
|
const problems = [];
|
|
14741
14350
|
for (const name of names) {
|
|
14742
|
-
const filePath =
|
|
14351
|
+
const filePath = path22.join(resolved, name);
|
|
14743
14352
|
try {
|
|
14744
14353
|
const run = JSON.parse(
|
|
14745
|
-
|
|
14354
|
+
fs16.readFileSync(filePath, "utf8")
|
|
14746
14355
|
);
|
|
14747
14356
|
const result = validateCanonicalRun2(run);
|
|
14748
14357
|
if (!result.valid) {
|
|
@@ -14766,16 +14375,16 @@ function readAggregateInput(args) {
|
|
|
14766
14375
|
const input = args.inputFile;
|
|
14767
14376
|
if (!input || args.stdin) return void 0;
|
|
14768
14377
|
if (args.subcommand === "validate") return void 0;
|
|
14769
|
-
const resolved =
|
|
14770
|
-
if (!
|
|
14378
|
+
const resolved = path22.resolve(input);
|
|
14379
|
+
if (!fs16.existsSync(resolved) || !fs16.statSync(resolved).isDirectory())
|
|
14771
14380
|
return void 0;
|
|
14772
14381
|
const result = aggregateReports(
|
|
14773
14382
|
{ dir: resolved.replace(/\\/g, "/") },
|
|
14774
14383
|
{
|
|
14775
|
-
readFile: (p) =>
|
|
14384
|
+
readFile: (p) => fs16.readFileSync(p, "utf8"),
|
|
14776
14385
|
listDir: (dir) => {
|
|
14777
14386
|
try {
|
|
14778
|
-
return
|
|
14387
|
+
return fs16.readdirSync(dir);
|
|
14779
14388
|
} catch {
|
|
14780
14389
|
return void 0;
|
|
14781
14390
|
}
|
|
@@ -14797,19 +14406,19 @@ async function readRunInput(args) {
|
|
|
14797
14406
|
return normalizeRunFromText(await readInput2(args), args).run;
|
|
14798
14407
|
}
|
|
14799
14408
|
function readFileInput(filePath) {
|
|
14800
|
-
const resolved =
|
|
14801
|
-
if (!
|
|
14409
|
+
const resolved = path22.resolve(filePath);
|
|
14410
|
+
if (!fs16.existsSync(resolved)) {
|
|
14802
14411
|
console.error(`Error: File not found: ${resolved}`);
|
|
14803
14412
|
process.exit(EXIT_USAGE4);
|
|
14804
14413
|
}
|
|
14805
|
-
return
|
|
14414
|
+
return fs16.readFileSync(resolved, "utf8");
|
|
14806
14415
|
}
|
|
14807
14416
|
function readStdin() {
|
|
14808
|
-
return new Promise((
|
|
14417
|
+
return new Promise((resolve12, reject) => {
|
|
14809
14418
|
const chunks = [];
|
|
14810
14419
|
process.stdin.setEncoding("utf8");
|
|
14811
14420
|
process.stdin.on("data", (chunk) => chunks.push(chunk));
|
|
14812
|
-
process.stdin.on("end", () =>
|
|
14421
|
+
process.stdin.on("end", () => resolve12(chunks.join("")));
|
|
14813
14422
|
process.stdin.on("error", reject);
|
|
14814
14423
|
});
|
|
14815
14424
|
}
|
|
@@ -14957,16 +14566,16 @@ function tryNormalizeRunFromText(text2, args) {
|
|
|
14957
14566
|
}
|
|
14958
14567
|
}
|
|
14959
14568
|
function listBaselineCandidates(currentFile, args) {
|
|
14960
|
-
const baselineDir =
|
|
14961
|
-
args.baselineDir ??
|
|
14569
|
+
const baselineDir = path22.resolve(
|
|
14570
|
+
args.baselineDir ?? path22.dirname(currentFile)
|
|
14962
14571
|
);
|
|
14963
|
-
const currentResolved =
|
|
14964
|
-
if (!
|
|
14572
|
+
const currentResolved = path22.resolve(currentFile);
|
|
14573
|
+
if (!fs16.existsSync(baselineDir)) {
|
|
14965
14574
|
console.error(`Error: baseline directory not found: ${baselineDir}`);
|
|
14966
14575
|
process.exit(EXIT_USAGE4);
|
|
14967
14576
|
}
|
|
14968
|
-
const entries =
|
|
14969
|
-
return entries.filter((entry) => entry.isFile()).map((entry) =>
|
|
14577
|
+
const entries = fs16.readdirSync(baselineDir, { withFileTypes: true });
|
|
14578
|
+
return entries.filter((entry) => entry.isFile()).map((entry) => path22.join(baselineDir, entry.name)).filter((candidate) => path22.resolve(candidate) !== currentResolved).filter(
|
|
14970
14579
|
(candidate) => args.inputType === "ndjson" ? candidate.endsWith(".ndjson") : candidate.endsWith(".json")
|
|
14971
14580
|
);
|
|
14972
14581
|
}
|
|
@@ -14975,7 +14584,7 @@ function resolveBaselineAuto(currentFile, currentRun, args) {
|
|
|
14975
14584
|
const comparable = [];
|
|
14976
14585
|
for (const candidate of candidates) {
|
|
14977
14586
|
const run = tryNormalizeRunFromText(
|
|
14978
|
-
|
|
14587
|
+
fs16.readFileSync(candidate, "utf8"),
|
|
14979
14588
|
args
|
|
14980
14589
|
);
|
|
14981
14590
|
if (run) {
|
|
@@ -14984,7 +14593,7 @@ function resolveBaselineAuto(currentFile, currentRun, args) {
|
|
|
14984
14593
|
}
|
|
14985
14594
|
if (comparable.length === 0) {
|
|
14986
14595
|
console.error(
|
|
14987
|
-
`Error: no compatible baseline files found in ${
|
|
14596
|
+
`Error: no compatible baseline files found in ${path22.resolve(args.baselineDir ?? path22.dirname(currentFile))}.`
|
|
14988
14597
|
);
|
|
14989
14598
|
process.exit(EXIT_USAGE4);
|
|
14990
14599
|
}
|
|
@@ -15155,7 +14764,8 @@ async function runReview(ctx) {
|
|
|
15155
14764
|
const { args } = ctx;
|
|
15156
14765
|
const run = applySelection(await readRunInput(args), args);
|
|
15157
14766
|
const context = loadReviewContext(args);
|
|
15158
|
-
const
|
|
14767
|
+
const built = buildReview(run, context);
|
|
14768
|
+
const review = await withJev(built, (jev) => enrichReview(built, jev));
|
|
15159
14769
|
try {
|
|
15160
14770
|
const files = writeReviewReport(review, args);
|
|
15161
14771
|
for (const f of files) {
|
|
@@ -15239,7 +14849,7 @@ async function runCheckExplainers(ctx) {
|
|
|
15239
14849
|
);
|
|
15240
14850
|
process.exit(EXIT_USAGE4);
|
|
15241
14851
|
}
|
|
15242
|
-
if (!
|
|
14852
|
+
if (!fs16.existsSync(args.explainersDir) || !fs16.statSync(args.explainersDir).isDirectory()) {
|
|
15243
14853
|
console.error(
|
|
15244
14854
|
`Error: --explainers-dir "${args.explainersDir}" is not a directory.`
|
|
15245
14855
|
);
|
|
@@ -15253,30 +14863,39 @@ async function runCheckExplainers(ctx) {
|
|
|
15253
14863
|
}
|
|
15254
14864
|
process.exit(EXIT_SUCCESS4);
|
|
15255
14865
|
}
|
|
14866
|
+
async function withJev(report, enrich) {
|
|
14867
|
+
const jev = jevFromEnv();
|
|
14868
|
+
if (!jev) return report;
|
|
14869
|
+
try {
|
|
14870
|
+
return await enrich(jev);
|
|
14871
|
+
} catch (err) {
|
|
14872
|
+
console.error(`Jev unavailable, deterministic output only: ${err instanceof Error ? err.message : String(err)}`);
|
|
14873
|
+
return report;
|
|
14874
|
+
}
|
|
14875
|
+
}
|
|
15256
14876
|
async function runGoal(ctx) {
|
|
15257
14877
|
const { args } = ctx;
|
|
15258
14878
|
const run = applySelection(await readRunInput(args), args);
|
|
15259
14879
|
const baseline = resolveBaselineRun(args, run);
|
|
15260
|
-
const
|
|
15261
|
-
|
|
15262
|
-
|
|
15263
|
-
|
|
15264
|
-
|
|
15265
|
-
|
|
15266
|
-
|
|
15267
|
-
|
|
15268
|
-
|
|
15269
|
-
|
|
15270
|
-
|
|
15271
|
-
|
|
15272
|
-
);
|
|
14880
|
+
const goalArgs = {
|
|
14881
|
+
run,
|
|
14882
|
+
baseline,
|
|
14883
|
+
requireTags: args.requireTags,
|
|
14884
|
+
requireTickets: args.requireTickets,
|
|
14885
|
+
requireScenarios: args.requireScenarios,
|
|
14886
|
+
enforceNoRegressions: args.noRegressions,
|
|
14887
|
+
enforceRatchet: !args.noRatchet,
|
|
14888
|
+
format: args.goalFormat
|
|
14889
|
+
};
|
|
14890
|
+
const built = buildGoal(goalArgs, {});
|
|
14891
|
+
const report = await withJev(built, (jev) => enrichGoal(built, goalArgs, jev));
|
|
15273
14892
|
console.log(renderGoal(report, args.goalFormat));
|
|
15274
14893
|
process.exit(report.met ? EXIT_SUCCESS4 : EXIT_AGENT_GATE);
|
|
15275
14894
|
}
|
|
15276
14895
|
function readCodeowners() {
|
|
15277
14896
|
for (const candidate of ["CODEOWNERS", ".github/CODEOWNERS", "docs/CODEOWNERS"]) {
|
|
15278
|
-
if (
|
|
15279
|
-
return parseCodeowners(
|
|
14897
|
+
if (fs16.existsSync(candidate)) {
|
|
14898
|
+
return parseCodeowners(fs16.readFileSync(candidate, "utf8"));
|
|
15280
14899
|
}
|
|
15281
14900
|
}
|
|
15282
14901
|
console.error(
|
|
@@ -15289,7 +14908,7 @@ async function runTriage(ctx) {
|
|
|
15289
14908
|
const run = applySelection(await readRunInput(args), args);
|
|
15290
14909
|
const baseline = resolveBaselineStatusMap(args, run);
|
|
15291
14910
|
const codeowners = args.byOwner ? readCodeowners() : void 0;
|
|
15292
|
-
const
|
|
14911
|
+
const built = buildTriage(
|
|
15293
14912
|
{
|
|
15294
14913
|
testCases: run.testCases,
|
|
15295
14914
|
baseline,
|
|
@@ -15298,6 +14917,7 @@ async function runTriage(ctx) {
|
|
|
15298
14917
|
},
|
|
15299
14918
|
{}
|
|
15300
14919
|
);
|
|
14920
|
+
const report = await withJev(built, (jev) => enrichTriage(built, run.testCases, jev, codeowners));
|
|
15301
14921
|
console.log(renderTriage(report, args.triageFormat, { byOwner: args.byOwner }));
|
|
15302
14922
|
process.exit(EXIT_SUCCESS4);
|
|
15303
14923
|
}
|
|
@@ -15396,9 +15016,9 @@ async function runFormatOrValidate(ctx) {
|
|
|
15396
15016
|
process.exit(EXIT_SCHEMA_VALIDATION);
|
|
15397
15017
|
}
|
|
15398
15018
|
if (args.emitCanonical) {
|
|
15399
|
-
const outPath =
|
|
15400
|
-
|
|
15401
|
-
|
|
15019
|
+
const outPath = path22.resolve(args.emitCanonical);
|
|
15020
|
+
fs16.mkdirSync(path22.dirname(outPath), { recursive: true });
|
|
15021
|
+
fs16.writeFileSync(outPath, JSON.stringify(run, null, 2), "utf8");
|
|
15402
15022
|
}
|
|
15403
15023
|
try {
|
|
15404
15024
|
const history = runHistoryPipeline(run, args);
|
|
@@ -15462,9 +15082,9 @@ ${msg}`);
|
|
|
15462
15082
|
}
|
|
15463
15083
|
const run = data;
|
|
15464
15084
|
if (args.emitCanonical) {
|
|
15465
|
-
const outPath =
|
|
15466
|
-
|
|
15467
|
-
|
|
15085
|
+
const outPath = path22.resolve(args.emitCanonical);
|
|
15086
|
+
fs16.mkdirSync(path22.dirname(outPath), { recursive: true });
|
|
15087
|
+
fs16.writeFileSync(outPath, JSON.stringify(run, null, 2), "utf8");
|
|
15468
15088
|
}
|
|
15469
15089
|
try {
|
|
15470
15090
|
const history = runHistoryPipeline(run, args);
|
|
@@ -15523,9 +15143,9 @@ ${msg}`);
|
|
|
15523
15143
|
process.exit(EXIT_CANONICAL_VALIDATION);
|
|
15524
15144
|
}
|
|
15525
15145
|
if (args.emitCanonical) {
|
|
15526
|
-
const outPath =
|
|
15527
|
-
|
|
15528
|
-
|
|
15146
|
+
const outPath = path22.resolve(args.emitCanonical);
|
|
15147
|
+
fs16.mkdirSync(path22.dirname(outPath), { recursive: true });
|
|
15148
|
+
fs16.writeFileSync(outPath, JSON.stringify(canonical, null, 2), "utf8");
|
|
15529
15149
|
}
|
|
15530
15150
|
try {
|
|
15531
15151
|
const history = runHistoryPipeline(canonical, args);
|
|
@@ -15560,9 +15180,9 @@ function runCustomFormatters(run, customRequested, formatters, args) {
|
|
|
15560
15180
|
const ext = formatter.fileExtension ?? formatName;
|
|
15561
15181
|
const baseName = args.outputName ?? "report";
|
|
15562
15182
|
const filename = args.outputNameTimestamp ? `${baseName}-${Math.floor(run.startedAtMs / 1e3)}.${ext}` : `${baseName}.${ext}`;
|
|
15563
|
-
const filepath =
|
|
15564
|
-
|
|
15565
|
-
|
|
15183
|
+
const filepath = path22.join(outputDir, filename);
|
|
15184
|
+
fs16.mkdirSync(outputDir, { recursive: true });
|
|
15185
|
+
fs16.writeFileSync(filepath, content, "utf8");
|
|
15566
15186
|
console.log(`Generated: ${filepath}`);
|
|
15567
15187
|
} catch (err) {
|
|
15568
15188
|
console.error(
|
|
@@ -15616,13 +15236,13 @@ async function dispatchNotifications(run, args) {
|
|
|
15616
15236
|
}
|
|
15617
15237
|
function runHistoryPipeline(run, args) {
|
|
15618
15238
|
if (!args.historyFile) return void 0;
|
|
15619
|
-
const historyPath =
|
|
15239
|
+
const historyPath = path22.resolve(args.historyFile);
|
|
15620
15240
|
const store = loadHistory(
|
|
15621
15241
|
{ filePath: historyPath },
|
|
15622
15242
|
{
|
|
15623
15243
|
readFile: (p) => {
|
|
15624
15244
|
try {
|
|
15625
|
-
return
|
|
15245
|
+
return fs16.readFileSync(p, "utf8");
|
|
15626
15246
|
} catch {
|
|
15627
15247
|
return void 0;
|
|
15628
15248
|
}
|
|
@@ -15635,12 +15255,12 @@ function runHistoryPipeline(run, args) {
|
|
|
15635
15255
|
run,
|
|
15636
15256
|
maxRuns: args.maxHistoryRuns
|
|
15637
15257
|
});
|
|
15638
|
-
const dir =
|
|
15639
|
-
|
|
15258
|
+
const dir = path22.dirname(historyPath);
|
|
15259
|
+
fs16.mkdirSync(dir, { recursive: true });
|
|
15640
15260
|
saveHistory(
|
|
15641
15261
|
{ filePath: historyPath, store: updated },
|
|
15642
15262
|
{
|
|
15643
|
-
writeFile: (p, content) =>
|
|
15263
|
+
writeFile: (p, content) => fs16.writeFileSync(p, content, "utf8")
|
|
15644
15264
|
}
|
|
15645
15265
|
);
|
|
15646
15266
|
let metricsCount = 0;
|
|
@@ -15706,7 +15326,7 @@ async function generateReports(run, args, historyStore, _droppedMissingStory = 0
|
|
|
15706
15326
|
files.push(...paths);
|
|
15707
15327
|
(EXECUTION_ONLY_FORMATS.has(format) ? executionFiles : documentationFiles).push(...paths);
|
|
15708
15328
|
}
|
|
15709
|
-
|
|
15329
|
+
writeArtifactsReadme(args.outputDir);
|
|
15710
15330
|
const documented = generator.renderedRun ?? run;
|
|
15711
15331
|
const wroteDocumentation = args.formats.some(
|
|
15712
15332
|
(f) => !EXECUTION_ONLY_FORMATS.has(f)
|
|
@@ -15740,7 +15360,6 @@ async function generateReports(run, args, historyStore, _droppedMissingStory = 0
|
|
|
15740
15360
|
return {
|
|
15741
15361
|
files,
|
|
15742
15362
|
counts,
|
|
15743
|
-
createdArtifactsReadme,
|
|
15744
15363
|
// A run that traced itself can draw the architecture it exercised, and
|
|
15745
15364
|
// nobody discovers a format they have never seen named.
|
|
15746
15365
|
spansUnused: !args.formats.includes("span-graph") && !args.htmlArchitecture && documented.testCases.some((tc) => (tc.story.otelSpans?.length ?? 0) > 0),
|
|
@@ -15885,13 +15504,13 @@ function writeReviewReport(review, args) {
|
|
|
15885
15504
|
const outputDir = args.outputDir ?? "reports";
|
|
15886
15505
|
const baseName = args.outputName ?? "evidence-review";
|
|
15887
15506
|
const suffix = args.outputNameTimestamp ? `-${Math.floor(review.run.startedAtMs / 1e3)}` : "";
|
|
15888
|
-
|
|
15889
|
-
const mdPath =
|
|
15890
|
-
const htmlPath =
|
|
15891
|
-
const jsonPath =
|
|
15892
|
-
|
|
15893
|
-
|
|
15894
|
-
|
|
15507
|
+
fs16.mkdirSync(outputDir, { recursive: true });
|
|
15508
|
+
const mdPath = path22.join(outputDir, `${baseName}${suffix}.md`);
|
|
15509
|
+
const htmlPath = path22.join(outputDir, `${baseName}${suffix}.html`);
|
|
15510
|
+
const jsonPath = path22.join(outputDir, `${baseName}${suffix}.review.json`);
|
|
15511
|
+
fs16.writeFileSync(mdPath, markdown, "utf8");
|
|
15512
|
+
fs16.writeFileSync(htmlPath, html, "utf8");
|
|
15513
|
+
fs16.writeFileSync(
|
|
15895
15514
|
jsonPath,
|
|
15896
15515
|
`${JSON.stringify(buildReviewJson(review), null, 2)}
|
|
15897
15516
|
`,
|
|
@@ -15987,11 +15606,6 @@ function printResult(result, args, startMs, droppedMissingStory = 0) {
|
|
|
15987
15606
|
"Tip: this run carries OTel spans, so it can draw the architecture it exercised: --format span-graph (a file), --html-architecture (a section in the HTML report)"
|
|
15988
15607
|
);
|
|
15989
15608
|
}
|
|
15990
|
-
if (result.createdArtifactsReadme && !isScaffoldedAstroSite(".") && !isScaffoldedAstroSite("./story-docs")) {
|
|
15991
|
-
console.error(
|
|
15992
|
-
"Tip: for a live docs site (stories, explainers, freshness): npx executable-stories init-astro --install"
|
|
15993
|
-
);
|
|
15994
|
-
}
|
|
15995
15609
|
}
|
|
15996
15610
|
if (args.open) {
|
|
15997
15611
|
openInBrowser(pickOpenTarget(result.files));
|
|
@@ -16000,9 +15614,9 @@ function printResult(result, args, startMs, droppedMissingStory = 0) {
|
|
|
16000
15614
|
function printCompareResult(result, args, startMs) {
|
|
16001
15615
|
const durationMs = Date.now() - startMs;
|
|
16002
15616
|
if (result.prSummary && args.prSummaryFile) {
|
|
16003
|
-
const outputPath =
|
|
16004
|
-
|
|
16005
|
-
|
|
15617
|
+
const outputPath = path22.resolve(args.prSummaryFile);
|
|
15618
|
+
fs16.mkdirSync(path22.dirname(outputPath), { recursive: true });
|
|
15619
|
+
fs16.writeFileSync(outputPath, result.prSummary, "utf8");
|
|
16006
15620
|
}
|
|
16007
15621
|
if (args.jsonSummary) {
|
|
16008
15622
|
console.log(
|
|
@@ -16036,13 +15650,13 @@ function printCompareResult(result, args, startMs) {
|
|
|
16036
15650
|
}
|
|
16037
15651
|
}
|
|
16038
15652
|
function loadReleasePolicy(policyPath) {
|
|
16039
|
-
const resolved =
|
|
16040
|
-
if (!
|
|
15653
|
+
const resolved = path22.resolve(policyPath);
|
|
15654
|
+
if (!fs16.existsSync(resolved)) {
|
|
16041
15655
|
console.error(`Error: release policy file not found: ${resolved}`);
|
|
16042
15656
|
process.exit(EXIT_USAGE4);
|
|
16043
15657
|
}
|
|
16044
15658
|
try {
|
|
16045
|
-
const raw = JSON.parse(
|
|
15659
|
+
const raw = JSON.parse(fs16.readFileSync(resolved, "utf8"));
|
|
16046
15660
|
return {
|
|
16047
15661
|
allowedOmissions: Array.isArray(raw.allowedOmissions) ? raw.allowedOmissions : [],
|
|
16048
15662
|
allowedRegressions: Array.isArray(raw.allowedRegressions) ? raw.allowedRegressions : [],
|
|
@@ -16148,7 +15762,7 @@ Generate an API token at https://id.atlassian.com/manage-profile/security/api-to
|
|
|
16148
15762
|
);
|
|
16149
15763
|
process.exit(EXIT_USAGE4);
|
|
16150
15764
|
}
|
|
16151
|
-
if (!
|
|
15765
|
+
if (!fs16.existsSync(inputFile)) {
|
|
16152
15766
|
console.error(`Error: file not found: ${inputFile}`);
|
|
16153
15767
|
process.exit(EXIT_USAGE4);
|
|
16154
15768
|
}
|
|
@@ -16176,7 +15790,7 @@ Generate an API token at https://id.atlassian.com/manage-profile/security/api-to
|
|
|
16176
15790
|
console.error("Error: --title is required when creating a new page");
|
|
16177
15791
|
process.exit(EXIT_USAGE4);
|
|
16178
15792
|
}
|
|
16179
|
-
const adf =
|
|
15793
|
+
const adf = fs16.readFileSync(path22.resolve(inputFile), "utf8");
|
|
16180
15794
|
if (dryRun) {
|
|
16181
15795
|
console.log(
|
|
16182
15796
|
JSON.stringify(
|
|
@@ -16257,7 +15871,7 @@ Generate an API token at https://id.atlassian.com/manage-profile/security/api-to
|
|
|
16257
15871
|
);
|
|
16258
15872
|
process.exit(EXIT_USAGE4);
|
|
16259
15873
|
}
|
|
16260
|
-
if (!
|
|
15874
|
+
if (!fs16.existsSync(inputFile)) {
|
|
16261
15875
|
console.error(`Error: file not found: ${inputFile}`);
|
|
16262
15876
|
process.exit(EXIT_USAGE4);
|
|
16263
15877
|
}
|
|
@@ -16284,7 +15898,7 @@ Generate an API token at https://id.atlassian.com/manage-profile/security/api-to
|
|
|
16284
15898
|
process.exit(EXIT_USAGE4);
|
|
16285
15899
|
}
|
|
16286
15900
|
const mode = modeRaw;
|
|
16287
|
-
const adf =
|
|
15901
|
+
const adf = fs16.readFileSync(path22.resolve(inputFile), "utf8");
|
|
16288
15902
|
if (dryRun) {
|
|
16289
15903
|
console.log(
|
|
16290
15904
|
JSON.stringify(
|
|
@@ -16325,46 +15939,6 @@ Generate an API token at https://id.atlassian.com/manage-profile/security/api-to
|
|
|
16325
15939
|
process.exit(EXIT_GENERATION);
|
|
16326
15940
|
}
|
|
16327
15941
|
}
|
|
16328
|
-
function runNew(rawArgs) {
|
|
16329
|
-
const { values, positionals } = parseArgs4({
|
|
16330
|
-
args: rawArgs,
|
|
16331
|
-
options: {
|
|
16332
|
-
dir: { type: "string" },
|
|
16333
|
-
force: { type: "boolean", default: false },
|
|
16334
|
-
"scenario-id": { type: "string" }
|
|
16335
|
-
},
|
|
16336
|
-
allowPositionals: true,
|
|
16337
|
-
strict: true
|
|
16338
|
-
});
|
|
16339
|
-
const template = positionals[0];
|
|
16340
|
-
const name = positionals.slice(1).join(" ");
|
|
16341
|
-
if (!template) {
|
|
16342
|
-
console.error(
|
|
16343
|
-
`Usage: executable-stories new <template> "<name>" [--dir <docs-dir>] [--scenario-id <id>] [--force]`
|
|
16344
|
-
);
|
|
16345
|
-
console.error(`Templates: ${TEMPLATES.join(", ")}`);
|
|
16346
|
-
return EXIT_USAGE4;
|
|
16347
|
-
}
|
|
16348
|
-
try {
|
|
16349
|
-
const result = scaffoldDoc({
|
|
16350
|
-
template,
|
|
16351
|
-
name,
|
|
16352
|
-
scenarioId: values["scenario-id"],
|
|
16353
|
-
baseDir: values.dir,
|
|
16354
|
-
force: values.force
|
|
16355
|
-
});
|
|
16356
|
-
console.log(`Created ${result.template}: ${result.path}`);
|
|
16357
|
-
console.log(` Title: ${result.title}`);
|
|
16358
|
-
console.log("");
|
|
16359
|
-
console.log(
|
|
16360
|
-
"Next: fill in the content and link verifying stories in `verifiedBy`."
|
|
16361
|
-
);
|
|
16362
|
-
return EXIT_SUCCESS4;
|
|
16363
|
-
} catch (err) {
|
|
16364
|
-
console.error(`Error: ${err.message}`);
|
|
16365
|
-
return EXIT_USAGE4;
|
|
16366
|
-
}
|
|
16367
|
-
}
|
|
16368
15942
|
async function runCheckLinks(rawArgs) {
|
|
16369
15943
|
const { values, positionals } = parseArgs4({
|
|
16370
15944
|
args: rawArgs,
|
|
@@ -16394,48 +15968,6 @@ async function runCheckLinks(rawArgs) {
|
|
|
16394
15968
|
return EXIT_USAGE4;
|
|
16395
15969
|
}
|
|
16396
15970
|
}
|
|
16397
|
-
async function runImportOpenApi(rawArgs) {
|
|
16398
|
-
const { values, positionals } = parseArgs4({
|
|
16399
|
-
args: rawArgs,
|
|
16400
|
-
options: {
|
|
16401
|
-
"output-dir": { type: "string" },
|
|
16402
|
-
run: { type: "string" },
|
|
16403
|
-
force: { type: "boolean", default: false }
|
|
16404
|
-
},
|
|
16405
|
-
allowPositionals: true,
|
|
16406
|
-
strict: true
|
|
16407
|
-
});
|
|
16408
|
-
const spec = positionals[0];
|
|
16409
|
-
if (!spec) {
|
|
16410
|
-
console.error(
|
|
16411
|
-
`Usage: executable-stories import-openapi <spec.json|yaml> [--output-dir <dir>] [--run <story-report.json>] [--force]`
|
|
16412
|
-
);
|
|
16413
|
-
return EXIT_USAGE4;
|
|
16414
|
-
}
|
|
16415
|
-
try {
|
|
16416
|
-
const result = await importOpenApi({
|
|
16417
|
-
specPath: spec,
|
|
16418
|
-
outputDir: values["output-dir"],
|
|
16419
|
-
runFile: values.run,
|
|
16420
|
-
force: values.force
|
|
16421
|
-
});
|
|
16422
|
-
console.log(
|
|
16423
|
-
`Generated ${result.pageCount} API page(s) at ${result.outputDir}`
|
|
16424
|
-
);
|
|
16425
|
-
console.log(
|
|
16426
|
-
` Covered endpoints: ${result.coveredCount} / ${result.endpointCount}`
|
|
16427
|
-
);
|
|
16428
|
-
if (result.uncoveredCount > 0) {
|
|
16429
|
-
console.log(
|
|
16430
|
-
` \u26A0 ${result.uncoveredCount} endpoint(s) have no verifying story`
|
|
16431
|
-
);
|
|
16432
|
-
}
|
|
16433
|
-
return EXIT_SUCCESS4;
|
|
16434
|
-
} catch (err) {
|
|
16435
|
-
console.error(`Error: ${err.message}`);
|
|
16436
|
-
return EXIT_USAGE4;
|
|
16437
|
-
}
|
|
16438
|
-
}
|
|
16439
15971
|
async function runDeploy(rawArgs) {
|
|
16440
15972
|
const mode = rawArgs[0];
|
|
16441
15973
|
if (!mode || !["record", "status", "diff"].includes(mode)) {
|