executable-stories-formatters 0.15.1 → 0.17.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +669 -301
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +511 -157
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +111 -6
- package/dist/index.d.ts +111 -6
- package/dist/index.js +504 -156
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
// src/cli.ts
|
|
4
4
|
import { parseArgs } from "util";
|
|
5
|
-
import * as
|
|
6
|
-
import * as
|
|
5
|
+
import * as fs17 from "fs";
|
|
6
|
+
import * as path18 from "path";
|
|
7
7
|
|
|
8
8
|
// src/validation/schema-validator.ts
|
|
9
9
|
import Ajv from "ajv/dist/2020.js";
|
|
@@ -588,17 +588,17 @@ function validateRawRun(data) {
|
|
|
588
588
|
return { valid: true, errors: [] };
|
|
589
589
|
}
|
|
590
590
|
const errors = (validate.errors ?? []).map((err) => {
|
|
591
|
-
const
|
|
591
|
+
const path19 = err.instancePath || "/";
|
|
592
592
|
const message = err.message ?? "unknown error";
|
|
593
593
|
if (err.keyword === "additionalProperties") {
|
|
594
594
|
const extra = err.params.additionalProperty;
|
|
595
|
-
return `${
|
|
595
|
+
return `${path19}: ${message} \u2014 '${extra}'`;
|
|
596
596
|
}
|
|
597
597
|
if (err.keyword === "enum") {
|
|
598
598
|
const allowed = err.params.allowedValues;
|
|
599
|
-
return `${
|
|
599
|
+
return `${path19}: ${message} \u2014 allowed: ${JSON.stringify(allowed)}`;
|
|
600
600
|
}
|
|
601
|
-
return `${
|
|
601
|
+
return `${path19}: ${message}`;
|
|
602
602
|
});
|
|
603
603
|
return { valid: false, errors };
|
|
604
604
|
}
|
|
@@ -702,6 +702,41 @@ function generateFeatureId(uri) {
|
|
|
702
702
|
function generateScenarioId(featureId, scenarioName) {
|
|
703
703
|
return `${featureId};${slugify(scenarioName)}`;
|
|
704
704
|
}
|
|
705
|
+
function normalizeText(text2) {
|
|
706
|
+
return text2.toLowerCase().replace(/[^\p{L}\p{N}\s]/gu, " ").replace(/\s+/g, " ").trim();
|
|
707
|
+
}
|
|
708
|
+
function tokenize(text2) {
|
|
709
|
+
const normalized = normalizeText(text2);
|
|
710
|
+
return normalized.length === 0 ? [] : normalized.split(" ");
|
|
711
|
+
}
|
|
712
|
+
function behaviourFingerprint(input) {
|
|
713
|
+
const steps = input.steps.map((step) => `${step.keyword.toLowerCase()}:${normalizeText(step.text)}`).join("\n");
|
|
714
|
+
const covers = [...input.covers ?? []].map((path19) => path19.trim()).filter(Boolean).sort().join(",");
|
|
715
|
+
if (steps.length === 0 && covers.length === 0) return "";
|
|
716
|
+
return createHash("sha1").update(`${steps}\0${covers}`).digest("hex").slice(0, 16);
|
|
717
|
+
}
|
|
718
|
+
function jaccard(a, b) {
|
|
719
|
+
if (a.size === 0 && b.size === 0) return 1;
|
|
720
|
+
if (a.size === 0 || b.size === 0) return 0;
|
|
721
|
+
let intersection = 0;
|
|
722
|
+
for (const value of a) if (b.has(value)) intersection += 1;
|
|
723
|
+
return intersection / (a.size + b.size - intersection);
|
|
724
|
+
}
|
|
725
|
+
function stepTokens(steps) {
|
|
726
|
+
const tokens = /* @__PURE__ */ new Set();
|
|
727
|
+
for (const step of steps) {
|
|
728
|
+
for (const token of tokenize(step.text)) tokens.add(token);
|
|
729
|
+
}
|
|
730
|
+
return tokens;
|
|
731
|
+
}
|
|
732
|
+
function behaviourSimilarity(a, b) {
|
|
733
|
+
const aSteps = stepTokens(a.steps);
|
|
734
|
+
const bSteps = stepTokens(b.steps);
|
|
735
|
+
if (aSteps.size === 0 || bSteps.size === 0) return 0;
|
|
736
|
+
const stepScore = jaccard(aSteps, bSteps);
|
|
737
|
+
const titleScore = jaccard(new Set(tokenize(a.scenario)), new Set(tokenize(b.scenario)));
|
|
738
|
+
return stepScore * 0.7 + titleScore * 0.3;
|
|
739
|
+
}
|
|
705
740
|
|
|
706
741
|
// src/converters/acl/steps.ts
|
|
707
742
|
function deriveStepResults(steps, scenarioStatus, error) {
|
|
@@ -1073,7 +1108,7 @@ ${result.errors.join("\n")}`);
|
|
|
1073
1108
|
|
|
1074
1109
|
// src/index.ts
|
|
1075
1110
|
import "fs";
|
|
1076
|
-
import * as
|
|
1111
|
+
import * as path10 from "path";
|
|
1077
1112
|
import * as fsPromises from "fs/promises";
|
|
1078
1113
|
|
|
1079
1114
|
// src/converters/acl/lines.ts
|
|
@@ -14977,12 +15012,12 @@ function highlightStepParams(text2, deps) {
|
|
|
14977
15012
|
var MIN_METRIC_SAMPLES = 5;
|
|
14978
15013
|
|
|
14979
15014
|
// src/formatters/html/renderers/scenario.ts
|
|
14980
|
-
function renderTicket(ticket, template,
|
|
15015
|
+
function renderTicket(ticket, template, escapeHtml5) {
|
|
14981
15016
|
const url = ticket.url ?? (template ? template.replace("{ticket}", ticket.id) : void 0);
|
|
14982
15017
|
if (url) {
|
|
14983
|
-
return `<a class="tag ticket-tag" href="${
|
|
15018
|
+
return `<a class="tag ticket-tag" href="${escapeHtml5(url)}" target="_blank" rel="noopener noreferrer">${escapeHtml5(ticket.id)}</a>`;
|
|
14984
15019
|
}
|
|
14985
|
-
return `<span class="tag ticket-tag">${
|
|
15020
|
+
return `<span class="tag ticket-tag">${escapeHtml5(ticket.id)}</span>`;
|
|
14986
15021
|
}
|
|
14987
15022
|
function renderScenario(args, deps) {
|
|
14988
15023
|
const { tc } = args;
|
|
@@ -15182,7 +15217,7 @@ function flattenTree(roots) {
|
|
|
15182
15217
|
}
|
|
15183
15218
|
return result;
|
|
15184
15219
|
}
|
|
15185
|
-
function buildTooltip(span,
|
|
15220
|
+
function buildTooltip(span, escapeHtml5) {
|
|
15186
15221
|
const parts = [];
|
|
15187
15222
|
parts.push(`${span.name} (${formatDuration(span.durationMs)})`);
|
|
15188
15223
|
if (span.statusMessage) {
|
|
@@ -15200,7 +15235,7 @@ function buildTooltip(span, escapeHtml4) {
|
|
|
15200
15235
|
if (text2.length > TOOLTIP_MAX_LENGTH) {
|
|
15201
15236
|
text2 = text2.slice(0, TOOLTIP_MAX_LENGTH - 3) + "...";
|
|
15202
15237
|
}
|
|
15203
|
-
return
|
|
15238
|
+
return escapeHtml5(text2);
|
|
15204
15239
|
}
|
|
15205
15240
|
function renderTraceView(args, deps) {
|
|
15206
15241
|
if (!args.spans || args.spans.length === 0) return "";
|
|
@@ -16495,14 +16530,14 @@ var TraceabilityMatrixFormatter = class {
|
|
|
16495
16530
|
lines.push("");
|
|
16496
16531
|
lines.push(`Status: ${renderRequirementStatus(req.status)}`);
|
|
16497
16532
|
if (req.covers.length > 0) {
|
|
16498
|
-
lines.push(`Covers: ${req.covers.map((
|
|
16533
|
+
lines.push(`Covers: ${req.covers.map((path19) => `\`${path19}\``).join(", ")}`);
|
|
16499
16534
|
}
|
|
16500
16535
|
lines.push("");
|
|
16501
16536
|
lines.push("| Status | Scenario | Source | Covers |");
|
|
16502
16537
|
lines.push("| --- | --- | --- | --- |");
|
|
16503
16538
|
for (const scenario of req.scenarios) {
|
|
16504
16539
|
const source = `${scenario.sourceFile}:${scenario.sourceLine}`;
|
|
16505
|
-
const covers = scenario.covers.length > 0 ? scenario.covers.map((
|
|
16540
|
+
const covers = scenario.covers.length > 0 ? scenario.covers.map((path19) => `\`${path19}\``).join(", ") : "";
|
|
16506
16541
|
lines.push(`| ${scenario.status} | ${escapePipe2(scenario.title)} | \`${source}\` | ${covers} |`);
|
|
16507
16542
|
}
|
|
16508
16543
|
lines.push("");
|
|
@@ -16604,8 +16639,8 @@ function extractFeatureName(testCases, uri) {
|
|
|
16604
16639
|
return tc.titlePath[0];
|
|
16605
16640
|
}
|
|
16606
16641
|
}
|
|
16607
|
-
const
|
|
16608
|
-
return
|
|
16642
|
+
const basename6 = uri.replace(/^.*[\\/]/, "").replace(/\.[^.]+$/, "");
|
|
16643
|
+
return basename6.replace(/[-_]+/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
|
|
16609
16644
|
}
|
|
16610
16645
|
function synthesizeFeature(uri, testCases) {
|
|
16611
16646
|
const featureName = extractFeatureName(testCases, uri);
|
|
@@ -17217,8 +17252,8 @@ function extractDocAttachments(step) {
|
|
|
17217
17252
|
}
|
|
17218
17253
|
return attachments;
|
|
17219
17254
|
}
|
|
17220
|
-
function guessMediaType(
|
|
17221
|
-
const lower =
|
|
17255
|
+
function guessMediaType(path19) {
|
|
17256
|
+
const lower = path19.toLowerCase();
|
|
17222
17257
|
if (lower.endsWith(".png")) return "image/png";
|
|
17223
17258
|
if (lower.endsWith(".jpg") || lower.endsWith(".jpeg")) return "image/jpeg";
|
|
17224
17259
|
if (lower.endsWith(".gif")) return "image/gif";
|
|
@@ -17359,11 +17394,11 @@ var CucumberHtmlFormatter = class {
|
|
|
17359
17394
|
for (const envelope of envelopes) {
|
|
17360
17395
|
const accepted = htmlStream.write(envelope);
|
|
17361
17396
|
if (!accepted) {
|
|
17362
|
-
await new Promise((
|
|
17397
|
+
await new Promise((resolve13) => htmlStream.once("drain", resolve13));
|
|
17363
17398
|
}
|
|
17364
17399
|
}
|
|
17365
|
-
await new Promise((
|
|
17366
|
-
collector.on("finish",
|
|
17400
|
+
await new Promise((resolve13, reject) => {
|
|
17401
|
+
collector.on("finish", resolve13);
|
|
17367
17402
|
collector.on("error", reject);
|
|
17368
17403
|
htmlStream.end();
|
|
17369
17404
|
});
|
|
@@ -17401,6 +17436,10 @@ function titleFor(kind) {
|
|
|
17401
17436
|
return "Added";
|
|
17402
17437
|
case "removed":
|
|
17403
17438
|
return "Removed";
|
|
17439
|
+
case "renamed":
|
|
17440
|
+
return "Renamed";
|
|
17441
|
+
case "moved":
|
|
17442
|
+
return "Moved";
|
|
17404
17443
|
case "changed":
|
|
17405
17444
|
return "Changed";
|
|
17406
17445
|
default:
|
|
@@ -17449,6 +17488,8 @@ function createPrCommentSummary(diff, maxScenarios = 10) {
|
|
|
17449
17488
|
addSection(lines, diff, "fixed", maxScenarios);
|
|
17450
17489
|
addSection(lines, diff, "added", maxScenarios);
|
|
17451
17490
|
addSection(lines, diff, "removed", maxScenarios);
|
|
17491
|
+
addSection(lines, diff, "renamed", maxScenarios);
|
|
17492
|
+
addSection(lines, diff, "moved", maxScenarios);
|
|
17452
17493
|
addSection(lines, diff, "changed", maxScenarios);
|
|
17453
17494
|
return lines.join("\n").trimEnd();
|
|
17454
17495
|
}
|
|
@@ -17525,8 +17566,10 @@ function sortDiffs(scenarios) {
|
|
|
17525
17566
|
fixed: 1,
|
|
17526
17567
|
added: 2,
|
|
17527
17568
|
removed: 3,
|
|
17528
|
-
|
|
17529
|
-
|
|
17569
|
+
renamed: 4,
|
|
17570
|
+
moved: 5,
|
|
17571
|
+
changed: 6,
|
|
17572
|
+
unchanged: 7
|
|
17530
17573
|
};
|
|
17531
17574
|
return [...scenarios].sort((a, b) => {
|
|
17532
17575
|
if (rank[a.kind] !== rank[b.kind]) {
|
|
@@ -17541,69 +17584,108 @@ function sortDiffs(scenarios) {
|
|
|
17541
17584
|
return a.scenario.localeCompare(b.scenario);
|
|
17542
17585
|
});
|
|
17543
17586
|
}
|
|
17587
|
+
var SIMILARITY_THRESHOLD = 0.75;
|
|
17588
|
+
function identityInput(tc) {
|
|
17589
|
+
return {
|
|
17590
|
+
scenario: tc.story.scenario,
|
|
17591
|
+
sourceFile: tc.sourceFile,
|
|
17592
|
+
steps: tc.story.steps,
|
|
17593
|
+
covers: tc.story.covers
|
|
17594
|
+
};
|
|
17595
|
+
}
|
|
17596
|
+
function changedFieldsOf(flags) {
|
|
17597
|
+
return Object.entries(flags).filter(([, changed]) => changed).map(([field]) => field);
|
|
17598
|
+
}
|
|
17599
|
+
function allChangedFlags(errorMessage) {
|
|
17600
|
+
return {
|
|
17601
|
+
status: true,
|
|
17602
|
+
steps: true,
|
|
17603
|
+
docs: true,
|
|
17604
|
+
tags: true,
|
|
17605
|
+
tickets: true,
|
|
17606
|
+
source: true,
|
|
17607
|
+
duration: true,
|
|
17608
|
+
attachments: true,
|
|
17609
|
+
error: Boolean(errorMessage),
|
|
17610
|
+
titlePath: true
|
|
17611
|
+
};
|
|
17612
|
+
}
|
|
17613
|
+
function matchIdentities(removed, added) {
|
|
17614
|
+
const pairs = [];
|
|
17615
|
+
const remainingRemoved = new Set(removed);
|
|
17616
|
+
const remainingAdded = new Set(added);
|
|
17617
|
+
const removedByFp = /* @__PURE__ */ new Map();
|
|
17618
|
+
const addedByFp = /* @__PURE__ */ new Map();
|
|
17619
|
+
for (const tc of remainingRemoved) {
|
|
17620
|
+
const fp = behaviourFingerprint(identityInput(tc));
|
|
17621
|
+
if (fp === "") continue;
|
|
17622
|
+
(removedByFp.get(fp) ?? removedByFp.set(fp, []).get(fp)).push(tc);
|
|
17623
|
+
}
|
|
17624
|
+
for (const tc of remainingAdded) {
|
|
17625
|
+
const fp = behaviourFingerprint(identityInput(tc));
|
|
17626
|
+
if (fp === "") continue;
|
|
17627
|
+
(addedByFp.get(fp) ?? addedByFp.set(fp, []).get(fp)).push(tc);
|
|
17628
|
+
}
|
|
17629
|
+
for (const [fp, removedGroup] of removedByFp) {
|
|
17630
|
+
const addedGroup = addedByFp.get(fp);
|
|
17631
|
+
if (removedGroup.length === 1 && addedGroup && addedGroup.length === 1) {
|
|
17632
|
+
pairs.push({ before: removedGroup[0], after: addedGroup[0], confidence: 1, matchedBy: "fingerprint" });
|
|
17633
|
+
remainingRemoved.delete(removedGroup[0]);
|
|
17634
|
+
remainingAdded.delete(addedGroup[0]);
|
|
17635
|
+
}
|
|
17636
|
+
}
|
|
17637
|
+
for (const before of [...remainingRemoved]) {
|
|
17638
|
+
let best;
|
|
17639
|
+
let bestScore = 0;
|
|
17640
|
+
let tied = false;
|
|
17641
|
+
for (const after of remainingAdded) {
|
|
17642
|
+
const score = behaviourSimilarity(identityInput(before), identityInput(after));
|
|
17643
|
+
if (score > bestScore) {
|
|
17644
|
+
bestScore = score;
|
|
17645
|
+
best = after;
|
|
17646
|
+
tied = false;
|
|
17647
|
+
} else if (score === bestScore) {
|
|
17648
|
+
tied = true;
|
|
17649
|
+
}
|
|
17650
|
+
}
|
|
17651
|
+
if (best && !tied && bestScore >= SIMILARITY_THRESHOLD) {
|
|
17652
|
+
pairs.push({ before, after: best, confidence: Math.round(bestScore * 100) / 100, matchedBy: "similarity" });
|
|
17653
|
+
remainingRemoved.delete(before);
|
|
17654
|
+
remainingAdded.delete(best);
|
|
17655
|
+
}
|
|
17656
|
+
}
|
|
17657
|
+
return {
|
|
17658
|
+
pairs,
|
|
17659
|
+
unmatchedRemoved: [...remainingRemoved],
|
|
17660
|
+
unmatchedAdded: [...remainingAdded]
|
|
17661
|
+
};
|
|
17662
|
+
}
|
|
17663
|
+
function identityKind(before, after) {
|
|
17664
|
+
const titleChanged = before.story.scenario !== after.story.scenario;
|
|
17665
|
+
const fileChanged = before.sourceFile !== after.sourceFile;
|
|
17666
|
+
return fileChanged && !titleChanged ? "moved" : "renamed";
|
|
17667
|
+
}
|
|
17544
17668
|
function diffRuns(baseline, current) {
|
|
17545
17669
|
const baselineById = new Map(baseline.testCases.map((tc) => [tc.id, tc]));
|
|
17546
17670
|
const currentById = new Map(current.testCases.map((tc) => [tc.id, tc]));
|
|
17547
17671
|
const ids = /* @__PURE__ */ new Set([...baselineById.keys(), ...currentById.keys()]);
|
|
17548
17672
|
const scenarios = [];
|
|
17673
|
+
const removedCases = [];
|
|
17674
|
+
const addedCases = [];
|
|
17549
17675
|
for (const id of ids) {
|
|
17550
17676
|
const before = baselineById.get(id);
|
|
17551
17677
|
const after = currentById.get(id);
|
|
17552
17678
|
if (!before && after) {
|
|
17553
|
-
|
|
17554
|
-
status: true,
|
|
17555
|
-
steps: true,
|
|
17556
|
-
docs: true,
|
|
17557
|
-
tags: true,
|
|
17558
|
-
tickets: true,
|
|
17559
|
-
source: true,
|
|
17560
|
-
duration: true,
|
|
17561
|
-
attachments: true,
|
|
17562
|
-
error: Boolean(after.errorMessage),
|
|
17563
|
-
titlePath: true
|
|
17564
|
-
};
|
|
17565
|
-
scenarios.push({
|
|
17566
|
-
kind: "added",
|
|
17567
|
-
id,
|
|
17568
|
-
scenario: after.story.scenario,
|
|
17569
|
-
sourceFile: after.sourceFile,
|
|
17570
|
-
sourceLine: after.sourceLine,
|
|
17571
|
-
current: toScenarioSnapshot(after),
|
|
17572
|
-
flags: flags2,
|
|
17573
|
-
changedFields: Object.entries(flags2).filter(([, changed]) => changed).map(([field]) => field)
|
|
17574
|
-
});
|
|
17679
|
+
addedCases.push(after);
|
|
17575
17680
|
continue;
|
|
17576
17681
|
}
|
|
17577
17682
|
if (before && !after) {
|
|
17578
|
-
|
|
17579
|
-
status: true,
|
|
17580
|
-
steps: true,
|
|
17581
|
-
docs: true,
|
|
17582
|
-
tags: true,
|
|
17583
|
-
tickets: true,
|
|
17584
|
-
source: true,
|
|
17585
|
-
duration: true,
|
|
17586
|
-
attachments: true,
|
|
17587
|
-
error: Boolean(before.errorMessage),
|
|
17588
|
-
titlePath: true
|
|
17589
|
-
};
|
|
17590
|
-
scenarios.push({
|
|
17591
|
-
kind: "removed",
|
|
17592
|
-
id,
|
|
17593
|
-
scenario: before.story.scenario,
|
|
17594
|
-
sourceFile: before.sourceFile,
|
|
17595
|
-
sourceLine: before.sourceLine,
|
|
17596
|
-
baseline: toScenarioSnapshot(before),
|
|
17597
|
-
flags: flags2,
|
|
17598
|
-
changedFields: Object.entries(flags2).filter(([, changed]) => changed).map(([field]) => field)
|
|
17599
|
-
});
|
|
17600
|
-
continue;
|
|
17601
|
-
}
|
|
17602
|
-
if (!before || !after) {
|
|
17683
|
+
removedCases.push(before);
|
|
17603
17684
|
continue;
|
|
17604
17685
|
}
|
|
17686
|
+
if (!before || !after) continue;
|
|
17605
17687
|
const flags = buildFlags(before, after);
|
|
17606
|
-
const changedFields =
|
|
17688
|
+
const changedFields = changedFieldsOf(flags);
|
|
17607
17689
|
const kind = getPrimaryKind(before, after, changedFields.length > 0);
|
|
17608
17690
|
scenarios.push({
|
|
17609
17691
|
kind,
|
|
@@ -17618,12 +17700,59 @@ function diffRuns(baseline, current) {
|
|
|
17618
17700
|
durationDeltaMs: after.durationMs - before.durationMs
|
|
17619
17701
|
});
|
|
17620
17702
|
}
|
|
17703
|
+
const { pairs, unmatchedRemoved, unmatchedAdded } = matchIdentities(removedCases, addedCases);
|
|
17704
|
+
for (const { before, after, confidence, matchedBy } of pairs) {
|
|
17705
|
+
const flags = buildFlags(before, after);
|
|
17706
|
+
scenarios.push({
|
|
17707
|
+
kind: identityKind(before, after),
|
|
17708
|
+
id: after.id,
|
|
17709
|
+
previousId: before.id,
|
|
17710
|
+
scenario: after.story.scenario,
|
|
17711
|
+
sourceFile: after.sourceFile,
|
|
17712
|
+
sourceLine: after.sourceLine,
|
|
17713
|
+
baseline: toScenarioSnapshot(before),
|
|
17714
|
+
current: toScenarioSnapshot(after),
|
|
17715
|
+
flags,
|
|
17716
|
+
changedFields: changedFieldsOf(flags),
|
|
17717
|
+
durationDeltaMs: after.durationMs - before.durationMs,
|
|
17718
|
+
matchConfidence: confidence,
|
|
17719
|
+
matchedBy
|
|
17720
|
+
});
|
|
17721
|
+
}
|
|
17722
|
+
for (const after of unmatchedAdded) {
|
|
17723
|
+
const flags = allChangedFlags(after.errorMessage);
|
|
17724
|
+
scenarios.push({
|
|
17725
|
+
kind: "added",
|
|
17726
|
+
id: after.id,
|
|
17727
|
+
scenario: after.story.scenario,
|
|
17728
|
+
sourceFile: after.sourceFile,
|
|
17729
|
+
sourceLine: after.sourceLine,
|
|
17730
|
+
current: toScenarioSnapshot(after),
|
|
17731
|
+
flags,
|
|
17732
|
+
changedFields: changedFieldsOf(flags)
|
|
17733
|
+
});
|
|
17734
|
+
}
|
|
17735
|
+
for (const before of unmatchedRemoved) {
|
|
17736
|
+
const flags = allChangedFlags(before.errorMessage);
|
|
17737
|
+
scenarios.push({
|
|
17738
|
+
kind: "removed",
|
|
17739
|
+
id: before.id,
|
|
17740
|
+
scenario: before.story.scenario,
|
|
17741
|
+
sourceFile: before.sourceFile,
|
|
17742
|
+
sourceLine: before.sourceLine,
|
|
17743
|
+
baseline: toScenarioSnapshot(before),
|
|
17744
|
+
flags,
|
|
17745
|
+
changedFields: changedFieldsOf(flags)
|
|
17746
|
+
});
|
|
17747
|
+
}
|
|
17621
17748
|
const sorted = sortDiffs(scenarios);
|
|
17622
17749
|
const summary = {
|
|
17623
17750
|
totalBaseline: baseline.testCases.length,
|
|
17624
17751
|
totalCurrent: current.testCases.length,
|
|
17625
17752
|
added: sorted.filter((s) => s.kind === "added").length,
|
|
17626
17753
|
removed: sorted.filter((s) => s.kind === "removed").length,
|
|
17754
|
+
renamed: sorted.filter((s) => s.kind === "renamed").length,
|
|
17755
|
+
moved: sorted.filter((s) => s.kind === "moved").length,
|
|
17627
17756
|
changed: sorted.filter((s) => s.kind === "changed").length,
|
|
17628
17757
|
regressed: sorted.filter((s) => s.kind === "regressed").length,
|
|
17629
17758
|
fixed: sorted.filter((s) => s.kind === "fixed").length,
|
|
@@ -17651,6 +17780,10 @@ function statusLabel(kind) {
|
|
|
17651
17780
|
return "Added";
|
|
17652
17781
|
case "removed":
|
|
17653
17782
|
return "Removed";
|
|
17783
|
+
case "renamed":
|
|
17784
|
+
return "Renamed";
|
|
17785
|
+
case "moved":
|
|
17786
|
+
return "Moved";
|
|
17654
17787
|
case "changed":
|
|
17655
17788
|
return "Changed";
|
|
17656
17789
|
default:
|
|
@@ -17976,6 +18109,8 @@ window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', fun
|
|
|
17976
18109
|
<div class="summary-card"><strong>${diff.summary.fixed}</strong><span>Fixed</span></div>
|
|
17977
18110
|
<div class="summary-card"><strong>${diff.summary.added}</strong><span>Added</span></div>
|
|
17978
18111
|
<div class="summary-card"><strong>${diff.summary.removed}</strong><span>Removed</span></div>
|
|
18112
|
+
<div class="summary-card"><strong>${diff.summary.renamed}</strong><span>Renamed</span></div>
|
|
18113
|
+
<div class="summary-card"><strong>${diff.summary.moved}</strong><span>Moved</span></div>
|
|
17979
18114
|
<div class="summary-card"><strong>${diff.summary.changed}</strong><span>Changed</span></div>
|
|
17980
18115
|
<div class="summary-card"><strong>${diff.summary.unchanged}</strong><span>Unchanged</span></div>
|
|
17981
18116
|
</section>
|
|
@@ -18036,6 +18171,10 @@ function formatStatus(kind) {
|
|
|
18036
18171
|
return "Added";
|
|
18037
18172
|
case "removed":
|
|
18038
18173
|
return "Removed";
|
|
18174
|
+
case "renamed":
|
|
18175
|
+
return "Renamed";
|
|
18176
|
+
case "moved":
|
|
18177
|
+
return "Moved";
|
|
18039
18178
|
case "changed":
|
|
18040
18179
|
return "Changed";
|
|
18041
18180
|
default:
|
|
@@ -18188,13 +18327,13 @@ var RunDiffMarkdownFormatter = class {
|
|
|
18188
18327
|
lines.push("No regressions or fixes detected. Remaining changes are neutral.");
|
|
18189
18328
|
}
|
|
18190
18329
|
lines.push("");
|
|
18191
|
-
lines.push("| Added | Removed | Regressed | Fixed | Changed | Unchanged |");
|
|
18192
|
-
lines.push("| ---: | ---: | ---: | ---: | ---: | ---: |");
|
|
18330
|
+
lines.push("| Added | Removed | Renamed | Moved | Regressed | Fixed | Changed | Unchanged |");
|
|
18331
|
+
lines.push("| ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: |");
|
|
18193
18332
|
lines.push(
|
|
18194
|
-
`| ${diff.summary.added} | ${diff.summary.removed} | ${diff.summary.regressed} | ${diff.summary.fixed} | ${diff.summary.changed} | ${diff.summary.unchanged} |`
|
|
18333
|
+
`| ${diff.summary.added} | ${diff.summary.removed} | ${diff.summary.renamed} | ${diff.summary.moved} | ${diff.summary.regressed} | ${diff.summary.fixed} | ${diff.summary.changed} | ${diff.summary.unchanged} |`
|
|
18195
18334
|
);
|
|
18196
18335
|
lines.push("");
|
|
18197
|
-
for (const kind of ["regressed", "fixed", "added", "removed", "changed"]) {
|
|
18336
|
+
for (const kind of ["regressed", "fixed", "added", "removed", "renamed", "moved", "changed"]) {
|
|
18198
18337
|
const scenarios = diff.scenarios.filter((scenario) => scenario.kind === kind);
|
|
18199
18338
|
if (scenarios.length === 0) continue;
|
|
18200
18339
|
lines.push(`## ${formatStatus(kind)} (${scenarios.length})`);
|
|
@@ -19053,7 +19192,7 @@ function toRun(data, inputType, synthesize) {
|
|
|
19053
19192
|
if (synthesize) raw = synthesizeStories(raw);
|
|
19054
19193
|
return canonicalizeRun(raw);
|
|
19055
19194
|
}
|
|
19056
|
-
async function
|
|
19195
|
+
async function regenerateRun(options, deps = {}) {
|
|
19057
19196
|
const read = deps.readFile ?? ((filePath) => fs6.readFileSync(filePath, "utf8"));
|
|
19058
19197
|
const data = JSON.parse(read(path7.resolve(options.input)));
|
|
19059
19198
|
const run = toRun(data, options.inputType ?? "raw", options.synthesize !== false);
|
|
@@ -19063,7 +19202,10 @@ async function regenerateArtifacts(options, deps = {}) {
|
|
|
19063
19202
|
outputName: options.outputName
|
|
19064
19203
|
});
|
|
19065
19204
|
const result = await generator.generate(run);
|
|
19066
|
-
return [...result.values()].flat();
|
|
19205
|
+
return { files: [...result.values()].flat(), run };
|
|
19206
|
+
}
|
|
19207
|
+
async function regenerateArtifacts(options, deps = {}) {
|
|
19208
|
+
return (await regenerateRun(options, deps)).files;
|
|
19067
19209
|
}
|
|
19068
19210
|
function startWatch(options, deps = {}) {
|
|
19069
19211
|
const log = deps.log ?? ((message) => console.log(message));
|
|
@@ -19106,6 +19248,203 @@ function startWatch(options, deps = {}) {
|
|
|
19106
19248
|
};
|
|
19107
19249
|
}
|
|
19108
19250
|
|
|
19251
|
+
// src/serve.ts
|
|
19252
|
+
import * as fs7 from "fs";
|
|
19253
|
+
import * as http from "http";
|
|
19254
|
+
import * as path8 from "path";
|
|
19255
|
+
function advanceState(prev, run) {
|
|
19256
|
+
if (prev.sessionBaseline === null) {
|
|
19257
|
+
return { sessionBaseline: run, previous: null, current: run, runCount: 1 };
|
|
19258
|
+
}
|
|
19259
|
+
return {
|
|
19260
|
+
sessionBaseline: prev.sessionBaseline,
|
|
19261
|
+
previous: prev.current,
|
|
19262
|
+
current: run,
|
|
19263
|
+
runCount: prev.runCount + 1
|
|
19264
|
+
};
|
|
19265
|
+
}
|
|
19266
|
+
function computeDeltas(state) {
|
|
19267
|
+
if (state.current === null || state.sessionBaseline === null || state.runCount <= 1) {
|
|
19268
|
+
return { session: null, iteration: null };
|
|
19269
|
+
}
|
|
19270
|
+
return {
|
|
19271
|
+
session: diffRuns(state.sessionBaseline, state.current),
|
|
19272
|
+
iteration: state.previous ? diffRuns(state.previous, state.current) : null
|
|
19273
|
+
};
|
|
19274
|
+
}
|
|
19275
|
+
function pluralize(n, word) {
|
|
19276
|
+
return `${n} ${word}${n === 1 ? "" : "s"}`;
|
|
19277
|
+
}
|
|
19278
|
+
function summarizeDiff(diff) {
|
|
19279
|
+
const s = diff.summary;
|
|
19280
|
+
const parts = [];
|
|
19281
|
+
if (s.fixed > 0) parts.push(`+${pluralize(s.fixed, "passing")}`);
|
|
19282
|
+
if (s.regressed > 0) parts.push(`${pluralize(s.regressed, "regressed")}`);
|
|
19283
|
+
if (s.added > 0) parts.push(`${pluralize(s.added, "new behaviour")}`);
|
|
19284
|
+
if (s.removed > 0) parts.push(`${pluralize(s.removed, "removed")}`);
|
|
19285
|
+
const moved = s.renamed + s.moved;
|
|
19286
|
+
if (moved > 0) parts.push(`${pluralize(moved, "renamed")}`);
|
|
19287
|
+
if (s.changed > 0) parts.push(`${pluralize(s.changed, "changed")}`);
|
|
19288
|
+
return parts.length > 0 ? parts.join(", ") : null;
|
|
19289
|
+
}
|
|
19290
|
+
function escapeHtml3(text2) {
|
|
19291
|
+
return text2.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
19292
|
+
}
|
|
19293
|
+
function renderDeltaStrip(state) {
|
|
19294
|
+
if (state.current === null) return "";
|
|
19295
|
+
const { session, iteration } = computeDeltas(state);
|
|
19296
|
+
if (session === null) {
|
|
19297
|
+
const label = `Run #${state.runCount} captured \u2014 baseline pinned. Watching for changes\u2026`;
|
|
19298
|
+
return `<div data-es-live="strip"><strong>Live</strong> \xB7 ${escapeHtml3(label)}</div>`;
|
|
19299
|
+
}
|
|
19300
|
+
const sessionLine = summarizeDiff(session) ?? "no change yet";
|
|
19301
|
+
let detail = "";
|
|
19302
|
+
if (iteration) {
|
|
19303
|
+
const iterationLine = summarizeDiff(iteration);
|
|
19304
|
+
if (iterationLine) detail = ` \xB7 <span data-es-live="iteration">this iteration: ${escapeHtml3(iterationLine)}</span>`;
|
|
19305
|
+
}
|
|
19306
|
+
return [
|
|
19307
|
+
`<div data-es-live="strip">`,
|
|
19308
|
+
`<strong>Live</strong> \xB7 run #${state.runCount} \xB7 `,
|
|
19309
|
+
`<span data-es-live="session">since you started: ${escapeHtml3(sessionLine)}</span>`,
|
|
19310
|
+
detail,
|
|
19311
|
+
`</div>`
|
|
19312
|
+
].join("");
|
|
19313
|
+
}
|
|
19314
|
+
var RELOAD_CLIENT = `<script data-es-live="client">
|
|
19315
|
+
(function () {
|
|
19316
|
+
try {
|
|
19317
|
+
var es = new EventSource("/__es_reload");
|
|
19318
|
+
es.onmessage = function (e) { if (e.data === "reload") location.reload(); };
|
|
19319
|
+
} catch (err) { /* SSE unavailable: stay static */ }
|
|
19320
|
+
})();
|
|
19321
|
+
</script>`;
|
|
19322
|
+
var STRIP_STYLE = `<style data-es-live="style">
|
|
19323
|
+
[data-es-live="strip"]{position:sticky;top:0;z-index:9999;font:14px/1.5 system-ui,sans-serif;
|
|
19324
|
+
padding:8px 16px;background:#0b1021;color:#e6e9f5;border-bottom:1px solid #2a3052}
|
|
19325
|
+
[data-es-live="strip"] strong{color:#7dd3fc}
|
|
19326
|
+
</style>`;
|
|
19327
|
+
function injectLiveBits(html, stripHtml) {
|
|
19328
|
+
let out = html;
|
|
19329
|
+
const bodyOpen = out.match(/<body[^>]*>/i);
|
|
19330
|
+
if (bodyOpen) {
|
|
19331
|
+
const at = bodyOpen.index + bodyOpen[0].length;
|
|
19332
|
+
out = out.slice(0, at) + stripHtml + out.slice(at);
|
|
19333
|
+
} else {
|
|
19334
|
+
out = stripHtml + out;
|
|
19335
|
+
}
|
|
19336
|
+
const tail = STRIP_STYLE + RELOAD_CLIENT;
|
|
19337
|
+
if (/<\/body>/i.test(out)) {
|
|
19338
|
+
out = out.replace(/<\/body>/i, tail + "</body>");
|
|
19339
|
+
} else {
|
|
19340
|
+
out += tail;
|
|
19341
|
+
}
|
|
19342
|
+
return out;
|
|
19343
|
+
}
|
|
19344
|
+
var CONTENT_TYPES = {
|
|
19345
|
+
".html": "text/html; charset=utf-8",
|
|
19346
|
+
".css": "text/css; charset=utf-8",
|
|
19347
|
+
".js": "text/javascript; charset=utf-8",
|
|
19348
|
+
".json": "application/json; charset=utf-8",
|
|
19349
|
+
".svg": "image/svg+xml",
|
|
19350
|
+
".png": "image/png"
|
|
19351
|
+
};
|
|
19352
|
+
function watchInputDir(filePath, listener) {
|
|
19353
|
+
const dir = path8.dirname(filePath);
|
|
19354
|
+
const base = path8.basename(filePath);
|
|
19355
|
+
fs7.mkdirSync(dir, { recursive: true });
|
|
19356
|
+
const watcher = fs7.watch(dir, (_event, changed) => {
|
|
19357
|
+
if (!changed || changed === base) listener();
|
|
19358
|
+
});
|
|
19359
|
+
return { close: () => watcher.close() };
|
|
19360
|
+
}
|
|
19361
|
+
function plainText(html) {
|
|
19362
|
+
return html.replace(/<[^>]+>/g, "").trim();
|
|
19363
|
+
}
|
|
19364
|
+
function startServe(options, deps = {}) {
|
|
19365
|
+
const log = deps.log ?? ((message) => console.log(message));
|
|
19366
|
+
const read = deps.readFile ?? ((filePath) => fs7.readFileSync(filePath, "utf8"));
|
|
19367
|
+
const port = options.port ?? 4321;
|
|
19368
|
+
const host = options.host ?? "127.0.0.1";
|
|
19369
|
+
let state = { sessionBaseline: null, previous: null, current: null, runCount: 0 };
|
|
19370
|
+
let htmlPath = null;
|
|
19371
|
+
let stripHtml = renderDeltaStrip(state);
|
|
19372
|
+
const clients = /* @__PURE__ */ new Set();
|
|
19373
|
+
const pushReload = () => {
|
|
19374
|
+
for (const res of clients) res.write("data: reload\n\n");
|
|
19375
|
+
};
|
|
19376
|
+
const handler = (req, res) => {
|
|
19377
|
+
const url = (req.url ?? "/").split("?")[0];
|
|
19378
|
+
if (url === "/__es_reload") {
|
|
19379
|
+
res.writeHead(200, {
|
|
19380
|
+
"Content-Type": "text/event-stream",
|
|
19381
|
+
"Cache-Control": "no-cache",
|
|
19382
|
+
Connection: "keep-alive"
|
|
19383
|
+
});
|
|
19384
|
+
res.write("retry: 1000\n\n");
|
|
19385
|
+
clients.add(res);
|
|
19386
|
+
req.on("close", () => clients.delete(res));
|
|
19387
|
+
return;
|
|
19388
|
+
}
|
|
19389
|
+
if (url === "/" || url === "/index.html") {
|
|
19390
|
+
const html = htmlPath ? read(htmlPath) : "<!doctype html><html><body><h1>executable-stories</h1></body></html>";
|
|
19391
|
+
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
19392
|
+
res.end(injectLiveBits(html, stripHtml));
|
|
19393
|
+
return;
|
|
19394
|
+
}
|
|
19395
|
+
const safe = path8.normalize(url).replace(/^(\.\.[/\\])+/, "");
|
|
19396
|
+
const filePath = path8.join(path8.resolve(options.outputDir), safe);
|
|
19397
|
+
if (filePath.startsWith(path8.resolve(options.outputDir)) && fs7.existsSync(filePath)) {
|
|
19398
|
+
const ext = path8.extname(filePath).toLowerCase();
|
|
19399
|
+
res.writeHead(200, { "Content-Type": CONTENT_TYPES[ext] ?? "application/octet-stream" });
|
|
19400
|
+
res.end(read(filePath));
|
|
19401
|
+
return;
|
|
19402
|
+
}
|
|
19403
|
+
res.writeHead(404, { "Content-Type": "text/plain" });
|
|
19404
|
+
res.end("Not found");
|
|
19405
|
+
};
|
|
19406
|
+
const server = deps.createServer ? deps.createServer(handler) : http.createServer(handler);
|
|
19407
|
+
const watchOptions = {
|
|
19408
|
+
input: options.input,
|
|
19409
|
+
outputDir: options.outputDir,
|
|
19410
|
+
outputName: options.outputName,
|
|
19411
|
+
formats: options.formats,
|
|
19412
|
+
inputType: options.inputType ?? "raw",
|
|
19413
|
+
synthesize: options.synthesize !== false,
|
|
19414
|
+
debounceMs: options.debounceMs
|
|
19415
|
+
};
|
|
19416
|
+
const watchHandle = startWatch(watchOptions, {
|
|
19417
|
+
readFile: read,
|
|
19418
|
+
watch: deps.watch ?? watchInputDir,
|
|
19419
|
+
log: () => {
|
|
19420
|
+
},
|
|
19421
|
+
// serve emits its own per-run line below
|
|
19422
|
+
regenerate: async (input) => {
|
|
19423
|
+
if (!fs7.existsSync(path8.resolve(input))) return [];
|
|
19424
|
+
const { files, run } = await regenerateRun({ ...watchOptions, input }, { readFile: read });
|
|
19425
|
+
htmlPath = files.find((f) => f.endsWith(".html")) ?? htmlPath;
|
|
19426
|
+
state = advanceState(state, run);
|
|
19427
|
+
stripHtml = renderDeltaStrip(state);
|
|
19428
|
+
log(`Run #${state.runCount}: ${plainText(stripHtml)}`);
|
|
19429
|
+
pushReload();
|
|
19430
|
+
return files;
|
|
19431
|
+
}
|
|
19432
|
+
});
|
|
19433
|
+
server.listen(port, host);
|
|
19434
|
+
const address = server.address();
|
|
19435
|
+
const boundPort = typeof address === "object" && address ? address.port : port;
|
|
19436
|
+
log(`Live docs: http://${host}:${boundPort} (Ctrl+C to stop)`);
|
|
19437
|
+
return {
|
|
19438
|
+
port: boundPort,
|
|
19439
|
+
close: () => {
|
|
19440
|
+
watchHandle.close();
|
|
19441
|
+
for (const res of clients) res.end();
|
|
19442
|
+
clients.clear();
|
|
19443
|
+
server.close();
|
|
19444
|
+
}
|
|
19445
|
+
};
|
|
19446
|
+
}
|
|
19447
|
+
|
|
19109
19448
|
// src/behavior-diff.ts
|
|
19110
19449
|
function classifyStatusChange(baseline, current) {
|
|
19111
19450
|
if (baseline === void 0) return "added";
|
|
@@ -19143,7 +19482,10 @@ function diffStoryReports(baseline, current) {
|
|
|
19143
19482
|
};
|
|
19144
19483
|
});
|
|
19145
19484
|
const summary = { added: 0, removed: 0, regressed: 0, fixed: 0, changed: 0, unchanged: 0 };
|
|
19146
|
-
for (const s of scenarios)
|
|
19485
|
+
for (const s of scenarios) {
|
|
19486
|
+
if (s.kind === "renamed" || s.kind === "moved") continue;
|
|
19487
|
+
summary[s.kind] += 1;
|
|
19488
|
+
}
|
|
19147
19489
|
return { schemaVersion: "1.0", summary, scenarios };
|
|
19148
19490
|
}
|
|
19149
19491
|
|
|
@@ -20752,18 +21094,18 @@ function deriveChangeType(tags) {
|
|
|
20752
21094
|
}
|
|
20753
21095
|
return "unknown";
|
|
20754
21096
|
}
|
|
20755
|
-
function extensionOf(
|
|
20756
|
-
const base =
|
|
21097
|
+
function extensionOf(path19) {
|
|
21098
|
+
const base = path19.split("/").pop() ?? path19;
|
|
20757
21099
|
const dot = base.lastIndexOf(".");
|
|
20758
21100
|
return dot === -1 ? "" : base.slice(dot + 1).toLowerCase();
|
|
20759
21101
|
}
|
|
20760
|
-
function isTestFile(
|
|
20761
|
-
return TEST_INFIX.test(
|
|
21102
|
+
function isTestFile(path19) {
|
|
21103
|
+
return TEST_INFIX.test(path19);
|
|
20762
21104
|
}
|
|
20763
|
-
function isReviewableSource(
|
|
20764
|
-
if (isTestFile(
|
|
20765
|
-
if (
|
|
20766
|
-
return CODE_EXTENSIONS.has(extensionOf(
|
|
21105
|
+
function isReviewableSource(path19) {
|
|
21106
|
+
if (isTestFile(path19)) return false;
|
|
21107
|
+
if (path19.endsWith(".d.ts")) return false;
|
|
21108
|
+
return CODE_EXTENSIONS.has(extensionOf(path19));
|
|
20767
21109
|
}
|
|
20768
21110
|
function testBaseKey(testFile) {
|
|
20769
21111
|
return testFile.replace(TEST_INFIX, "");
|
|
@@ -20867,7 +21209,7 @@ function toClaim(testCase, changedSourcePaths) {
|
|
|
20867
21209
|
const { strength, reasons } = gradeEvidence(testCase, audience);
|
|
20868
21210
|
const key = testBaseKey(testCase.sourceFile);
|
|
20869
21211
|
const coversFiles = changedSourcePaths.filter(
|
|
20870
|
-
(
|
|
21212
|
+
(path19) => sourceBaseKey(path19) === key
|
|
20871
21213
|
);
|
|
20872
21214
|
return {
|
|
20873
21215
|
id: testCase.id,
|
|
@@ -21116,7 +21458,7 @@ var ReviewMarkdownFormatter = class {
|
|
|
21116
21458
|
};
|
|
21117
21459
|
|
|
21118
21460
|
// src/formatters/review-html.ts
|
|
21119
|
-
function
|
|
21461
|
+
function escapeHtml4(value) {
|
|
21120
21462
|
return value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
21121
21463
|
}
|
|
21122
21464
|
var STRENGTH_LABEL = {
|
|
@@ -21138,22 +21480,22 @@ function statusIcon3(status) {
|
|
|
21138
21480
|
}
|
|
21139
21481
|
}
|
|
21140
21482
|
function formatStep3(step) {
|
|
21141
|
-
return `<li><strong>${
|
|
21483
|
+
return `<li><strong>${escapeHtml4(step.keyword)}</strong> ${escapeHtml4(step.text)}</li>`;
|
|
21142
21484
|
}
|
|
21143
21485
|
function inlineDoc(doc) {
|
|
21144
21486
|
switch (doc.kind) {
|
|
21145
21487
|
case "note":
|
|
21146
|
-
return
|
|
21488
|
+
return escapeHtml4(doc.text);
|
|
21147
21489
|
case "section":
|
|
21148
|
-
return `<strong>${
|
|
21490
|
+
return `<strong>${escapeHtml4(doc.title)}</strong>: ${escapeHtml4(doc.markdown)}`;
|
|
21149
21491
|
case "kv":
|
|
21150
|
-
return `${
|
|
21492
|
+
return `${escapeHtml4(doc.label)}: ${escapeHtml4(String(doc.value))}`;
|
|
21151
21493
|
case "code":
|
|
21152
|
-
return `${
|
|
21494
|
+
return `${escapeHtml4(doc.label)}: <code>${escapeHtml4(doc.content)}</code>`;
|
|
21153
21495
|
case "link":
|
|
21154
|
-
return `${
|
|
21496
|
+
return `${escapeHtml4(doc.label)}: ${escapeHtml4(doc.url)}`;
|
|
21155
21497
|
default:
|
|
21156
|
-
return
|
|
21498
|
+
return escapeHtml4(doc.kind);
|
|
21157
21499
|
}
|
|
21158
21500
|
}
|
|
21159
21501
|
function renderEvidenceArtifacts(testCase) {
|
|
@@ -21161,7 +21503,7 @@ function renderEvidenceArtifacts(testCase) {
|
|
|
21161
21503
|
for (const att of testCase.attachments) {
|
|
21162
21504
|
if (att.mediaType.startsWith("image/") && att.contentEncoding === "BASE64") {
|
|
21163
21505
|
parts.push(
|
|
21164
|
-
`<img class="shot" alt="${
|
|
21506
|
+
`<img class="shot" alt="${escapeHtml4(att.name)}" src="data:${escapeHtml4(att.mediaType)};base64,${att.body}" />`
|
|
21165
21507
|
);
|
|
21166
21508
|
}
|
|
21167
21509
|
}
|
|
@@ -21176,22 +21518,22 @@ function renderTicketPills(claim) {
|
|
|
21176
21518
|
const tickets = claim.testCase.story.tickets ?? [];
|
|
21177
21519
|
if (tickets.length === 0) return "";
|
|
21178
21520
|
return `<div class="ticket-row">${tickets.map((ticket) => {
|
|
21179
|
-
const label =
|
|
21521
|
+
const label = escapeHtml4(ticket.id);
|
|
21180
21522
|
if (ticket.url) {
|
|
21181
|
-
return `<a class="ticket-pill" href="${
|
|
21523
|
+
return `<a class="ticket-pill" href="${escapeHtml4(ticket.url)}" target="_blank" rel="noopener noreferrer">${label}</a>`;
|
|
21182
21524
|
}
|
|
21183
21525
|
return `<span class="ticket-pill">${label}</span>`;
|
|
21184
21526
|
}).join("")}</div>`;
|
|
21185
21527
|
}
|
|
21186
21528
|
function renderClaimCard(claim) {
|
|
21187
21529
|
const ticketSearch = (claim.testCase.story.tickets ?? []).map((ticket) => ticket.id).join(" ");
|
|
21188
|
-
const search =
|
|
21530
|
+
const search = escapeHtml4(
|
|
21189
21531
|
`${claim.scenario} ${claim.sourceFile} ${claim.changeType} ${claim.audience} ${claim.strength} ${ticketSearch}`
|
|
21190
21532
|
).toLowerCase();
|
|
21191
21533
|
const steps = claim.testCase.story.steps.length > 0 ? `<ul class="step-list">${claim.testCase.story.steps.map(formatStep3).join("")}</ul>` : "";
|
|
21192
|
-
const reasons = `<ul class="reasons">${claim.strengthReasons.map((r) => `<li>${
|
|
21193
|
-
const intent = claim.intent !== void 0 ? `<div class="intent"><span class="intent-label">Why</span> ${
|
|
21194
|
-
const covers = claim.coversFiles.length > 0 ? `<p class="covers">Covers ${claim.coversFiles.map((f) => `<code>${
|
|
21534
|
+
const reasons = `<ul class="reasons">${claim.strengthReasons.map((r) => `<li>${escapeHtml4(r)}</li>`).join("")}</ul>`;
|
|
21535
|
+
const intent = claim.intent !== void 0 ? `<div class="intent"><span class="intent-label">Why</span> ${escapeHtml4(claim.intent)}</div>` : "";
|
|
21536
|
+
const covers = claim.coversFiles.length > 0 ? `<p class="covers">Covers ${claim.coversFiles.map((f) => `<code>${escapeHtml4(f)}</code>`).join(", ")}</p>` : "";
|
|
21195
21537
|
const docs = (claim.testCase.story.docs ?? []).filter(
|
|
21196
21538
|
(d) => d.kind === "section" || d.kind === "note"
|
|
21197
21539
|
);
|
|
@@ -21201,9 +21543,9 @@ function renderClaimCard(claim) {
|
|
|
21201
21543
|
<header class="claim-header">
|
|
21202
21544
|
<div>
|
|
21203
21545
|
<span class="strength-badge strength-${claim.strength}">${STRENGTH_LABEL[claim.strength]}</span>
|
|
21204
|
-
${claim.changeType !== "unknown" ? `<span class="change-pill">${
|
|
21205
|
-
<h3>${statusIcon3(claim.status)} ${
|
|
21206
|
-
<p class="source">${
|
|
21546
|
+
${claim.changeType !== "unknown" ? `<span class="change-pill">${escapeHtml4(claim.changeType)}</span>` : ""}
|
|
21547
|
+
<h3>${statusIcon3(claim.status)} ${escapeHtml4(claim.scenario)}</h3>
|
|
21548
|
+
<p class="source">${escapeHtml4(`${claim.sourceFile}:${claim.sourceLine}`)}</p>
|
|
21207
21549
|
${renderTicketPills(claim)}
|
|
21208
21550
|
</div>
|
|
21209
21551
|
</header>
|
|
@@ -21218,18 +21560,18 @@ function renderClaimCard(claim) {
|
|
|
21218
21560
|
</article>`;
|
|
21219
21561
|
}
|
|
21220
21562
|
function renderChangedFileRow(file) {
|
|
21221
|
-
const claims = file.claims.length > 0 ? file.claims.map((c) => `${
|
|
21563
|
+
const claims = file.claims.length > 0 ? file.claims.map((c) => `${escapeHtml4(c.scenario)} <em>(${c.strength})</em>`).join(", ") : "\u2014";
|
|
21222
21564
|
return `<tr data-band="${file.band}">
|
|
21223
21565
|
<td><span class="band-dot band-${file.band}"></span></td>
|
|
21224
|
-
<td><code>${
|
|
21225
|
-
<td>${
|
|
21566
|
+
<td><code>${escapeHtml4(file.path)}</code></td>
|
|
21567
|
+
<td>${escapeHtml4(file.changeKind)}</td>
|
|
21226
21568
|
<td>${claims}</td>
|
|
21227
21569
|
</tr>`;
|
|
21228
21570
|
}
|
|
21229
21571
|
function renderAudienceSection2(title, claims) {
|
|
21230
21572
|
if (claims.length === 0) return "";
|
|
21231
21573
|
return `<section class="audience-section">
|
|
21232
|
-
<h2>${
|
|
21574
|
+
<h2>${escapeHtml4(title)} <span class="count">${claims.length}</span></h2>
|
|
21233
21575
|
<div class="claim-list">${claims.map(renderClaimCard).join("\n")}</div>
|
|
21234
21576
|
</section>`;
|
|
21235
21577
|
}
|
|
@@ -21319,13 +21661,13 @@ var ReviewHtmlFormatter = class {
|
|
|
21319
21661
|
const themeInitJs = this.darkMode ? `${JS_THEME_TOGGLE2}
|
|
21320
21662
|
applyTheme(getEffectiveTheme());` : "";
|
|
21321
21663
|
const themeAttr = this.darkMode ? ' data-theme="light"' : "";
|
|
21322
|
-
const refsLine = context.baseRef || context.headRef ? `<p class="subtle">Comparing ${
|
|
21664
|
+
const refsLine = context.baseRef || context.headRef ? `<p class="subtle">Comparing ${escapeHtml4(context.baseRef ?? "base")} \u2192 ${escapeHtml4(context.headRef ?? "head")}</p>` : "";
|
|
21323
21665
|
return `<!doctype html>
|
|
21324
21666
|
<html lang="en"${themeAttr}>
|
|
21325
21667
|
<head>
|
|
21326
21668
|
<meta charset="utf-8" />
|
|
21327
21669
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
21328
|
-
<title>${
|
|
21670
|
+
<title>${escapeHtml4(this.title)}</title>
|
|
21329
21671
|
<style>
|
|
21330
21672
|
${this.theme.css}
|
|
21331
21673
|
${REVIEW_CSS}
|
|
@@ -21335,7 +21677,7 @@ applyTheme(getEffectiveTheme());` : "";
|
|
|
21335
21677
|
<main>
|
|
21336
21678
|
<div class="hero-card card">
|
|
21337
21679
|
<div class="review-header">
|
|
21338
|
-
<h1>${
|
|
21680
|
+
<h1>${escapeHtml4(this.title)}</h1>
|
|
21339
21681
|
${themeToggleHtml}
|
|
21340
21682
|
</div>
|
|
21341
21683
|
${refsLine}
|
|
@@ -21350,7 +21692,7 @@ applyTheme(getEffectiveTheme());` : "";
|
|
|
21350
21692
|
</section>
|
|
21351
21693
|
<section class="card priority-banner">
|
|
21352
21694
|
<h2>Review priority</h2>
|
|
21353
|
-
<p class="subtle">${
|
|
21695
|
+
<p class="subtle">${escapeHtml4(priority)}</p>
|
|
21354
21696
|
</section>
|
|
21355
21697
|
${changedFilesPanel}
|
|
21356
21698
|
<section class="toolbar">
|
|
@@ -21398,8 +21740,8 @@ applyTheme(getEffectiveTheme());` : "";
|
|
|
21398
21740
|
};
|
|
21399
21741
|
|
|
21400
21742
|
// src/deploy/ledger.ts
|
|
21401
|
-
import * as
|
|
21402
|
-
import * as
|
|
21743
|
+
import * as fs8 from "fs";
|
|
21744
|
+
import * as path9 from "path";
|
|
21403
21745
|
function createEmptyLedger() {
|
|
21404
21746
|
return {
|
|
21405
21747
|
deployments: [],
|
|
@@ -21407,12 +21749,12 @@ function createEmptyLedger() {
|
|
|
21407
21749
|
};
|
|
21408
21750
|
}
|
|
21409
21751
|
function loadLedger(ledgerPath) {
|
|
21410
|
-
const resolved =
|
|
21411
|
-
if (!
|
|
21752
|
+
const resolved = path9.resolve(ledgerPath);
|
|
21753
|
+
if (!fs8.existsSync(resolved)) {
|
|
21412
21754
|
return createEmptyLedger();
|
|
21413
21755
|
}
|
|
21414
21756
|
try {
|
|
21415
|
-
const raw = JSON.parse(
|
|
21757
|
+
const raw = JSON.parse(fs8.readFileSync(resolved, "utf8"));
|
|
21416
21758
|
if (raw.schemaVersion !== 1) {
|
|
21417
21759
|
throw new Error(`Unsupported ledger schemaVersion: ${raw.schemaVersion}`);
|
|
21418
21760
|
}
|
|
@@ -21423,10 +21765,10 @@ function loadLedger(ledgerPath) {
|
|
|
21423
21765
|
}
|
|
21424
21766
|
}
|
|
21425
21767
|
function saveLedger(ledger, ledgerPath) {
|
|
21426
|
-
const resolved =
|
|
21427
|
-
const dir =
|
|
21428
|
-
|
|
21429
|
-
|
|
21768
|
+
const resolved = path9.resolve(ledgerPath);
|
|
21769
|
+
const dir = path9.dirname(resolved);
|
|
21770
|
+
fs8.mkdirSync(dir, { recursive: true });
|
|
21771
|
+
fs8.writeFileSync(resolved, JSON.stringify(ledger, null, 2), "utf8");
|
|
21430
21772
|
}
|
|
21431
21773
|
function getLatestDeployment(ledger, environment) {
|
|
21432
21774
|
return [...ledger.deployments].reverse().find((d) => d.environment === environment);
|
|
@@ -21546,11 +21888,11 @@ function computeOutputPath(sourceFile, format, mode, colocatedStyle, baseOutputD
|
|
|
21546
21888
|
const ext = FORMAT_EXTENSIONS[format];
|
|
21547
21889
|
const effectiveName = outputName + (outputNameSuffix ?? "");
|
|
21548
21890
|
if (mode === "aggregated") {
|
|
21549
|
-
return toPosix(
|
|
21891
|
+
return toPosix(path10.join(baseOutputDir, joinNameAndExt(effectiveName, ext)));
|
|
21550
21892
|
}
|
|
21551
21893
|
const normalizedSource = toPosix(sourceFile);
|
|
21552
|
-
const dirOfSource =
|
|
21553
|
-
let baseName =
|
|
21894
|
+
const dirOfSource = path10.posix.dirname(normalizedSource);
|
|
21895
|
+
let baseName = path10.posix.basename(normalizedSource);
|
|
21554
21896
|
for (const testExt of TEST_EXTENSIONS) {
|
|
21555
21897
|
if (baseName.endsWith(testExt)) {
|
|
21556
21898
|
baseName = baseName.slice(0, -testExt.length);
|
|
@@ -21559,12 +21901,12 @@ function computeOutputPath(sourceFile, format, mode, colocatedStyle, baseOutputD
|
|
|
21559
21901
|
}
|
|
21560
21902
|
const fileName = `${baseName}.${effectiveName}${ext}`;
|
|
21561
21903
|
if (colocatedStyle === "adjacent") {
|
|
21562
|
-
return toPosix(
|
|
21904
|
+
return toPosix(path10.posix.join(dirOfSource, fileName));
|
|
21563
21905
|
}
|
|
21564
21906
|
if (colocatedStyle === "flat") {
|
|
21565
|
-
return toPosix(
|
|
21907
|
+
return toPosix(path10.posix.join(baseOutputDir, `${cleanTestStem(normalizedSource)}${ext}`));
|
|
21566
21908
|
}
|
|
21567
|
-
return toPosix(
|
|
21909
|
+
return toPosix(path10.posix.join(baseOutputDir, dirOfSource, fileName));
|
|
21568
21910
|
}
|
|
21569
21911
|
function groupTestCasesByOutput(testCases, format, options, logger, outputNameSuffix) {
|
|
21570
21912
|
const groups = /* @__PURE__ */ new Map();
|
|
@@ -21777,8 +22119,8 @@ var ReportGenerator = class {
|
|
|
21777
22119
|
if (astroPaths) {
|
|
21778
22120
|
for (const mdPath of astroPaths) {
|
|
21779
22121
|
const content = await fsPromises.readFile(mdPath, "utf8");
|
|
21780
|
-
const mdDir =
|
|
21781
|
-
const assetsDir =
|
|
22122
|
+
const mdDir = path10.dirname(mdPath);
|
|
22123
|
+
const assetsDir = path10.resolve(this.options.astro.assetsDir);
|
|
21782
22124
|
const result = copyMarkdownAssets({
|
|
21783
22125
|
markdown: content,
|
|
21784
22126
|
markdownDir: mdDir,
|
|
@@ -21809,9 +22151,9 @@ var ReportGenerator = class {
|
|
|
21809
22151
|
if (groups.size === 0 && this.options.output.mode === "aggregated") {
|
|
21810
22152
|
const ext = FORMAT_EXTENSIONS[format];
|
|
21811
22153
|
const effectiveName = this.options.outputName + (outputNameSuffix ?? "");
|
|
21812
|
-
const outputPath = toPosix(
|
|
22154
|
+
const outputPath = toPosix(path10.join(this.options.outputDir, joinNameAndExt(effectiveName, ext)));
|
|
21813
22155
|
const content = await this.formatContent(run, format);
|
|
21814
|
-
const dir =
|
|
22156
|
+
const dir = path10.dirname(outputPath);
|
|
21815
22157
|
await fsPromises.mkdir(dir, { recursive: true });
|
|
21816
22158
|
await this.deps.writeFile(outputPath, content);
|
|
21817
22159
|
return [outputPath];
|
|
@@ -21823,7 +22165,7 @@ var ReportGenerator = class {
|
|
|
21823
22165
|
testCases
|
|
21824
22166
|
};
|
|
21825
22167
|
const content = await this.formatContent(groupRun, format);
|
|
21826
|
-
const dir =
|
|
22168
|
+
const dir = path10.dirname(outputPath);
|
|
21827
22169
|
await fsPromises.mkdir(dir, { recursive: true });
|
|
21828
22170
|
await this.deps.writeFile(outputPath, content);
|
|
21829
22171
|
writtenPaths.push(outputPath);
|
|
@@ -21972,7 +22314,7 @@ async function generateRunComparison(args) {
|
|
|
21972
22314
|
await fsPromises.mkdir(outputDir, { recursive: true });
|
|
21973
22315
|
for (const format of args.formats) {
|
|
21974
22316
|
const ext = format === "html" ? ".html" : ".md";
|
|
21975
|
-
const outputPath = toPosix(
|
|
22317
|
+
const outputPath = toPosix(path10.join(outputDir, `${outputName}${ext}`));
|
|
21976
22318
|
const content = format === "html" ? new RunDiffHtmlFormatter({ title: args.title }).format(diff) : new RunDiffMarkdownFormatter({ title: args.title }).format(diff);
|
|
21977
22319
|
await fsPromises.writeFile(outputPath, content, "utf8");
|
|
21978
22320
|
files.push(outputPath);
|
|
@@ -21981,21 +22323,21 @@ async function generateRunComparison(args) {
|
|
|
21981
22323
|
}
|
|
21982
22324
|
|
|
21983
22325
|
// src/init-astro.ts
|
|
21984
|
-
import * as
|
|
21985
|
-
import * as
|
|
22326
|
+
import * as fs10 from "fs";
|
|
22327
|
+
import * as path11 from "path";
|
|
21986
22328
|
import { fileURLToPath } from "url";
|
|
21987
|
-
var __dirname =
|
|
22329
|
+
var __dirname = path11.dirname(fileURLToPath(import.meta.url));
|
|
21988
22330
|
var FRAMEWORK_DIRS = ["src/components", "src/lib", "src/styles", "src/pages"];
|
|
21989
22331
|
var FRAMEWORK_FILES = ["tsconfig.json"];
|
|
21990
22332
|
function isScaffoldedAstroSite(dir) {
|
|
21991
|
-
return
|
|
22333
|
+
return fs10.existsSync(path11.join(dir, "astro.config.mjs"));
|
|
21992
22334
|
}
|
|
21993
22335
|
function initAstro(options = {}) {
|
|
21994
22336
|
const targetDir = options.targetDir ?? "./story-docs";
|
|
21995
22337
|
const force = options.force ?? false;
|
|
21996
22338
|
const update = options.update ?? false;
|
|
21997
|
-
const templateDir =
|
|
21998
|
-
if (!
|
|
22339
|
+
const templateDir = path11.resolve(__dirname, "..", "templates", "astro-starlight");
|
|
22340
|
+
if (!fs10.existsSync(templateDir)) {
|
|
21999
22341
|
throw new Error(
|
|
22000
22342
|
`Template directory not found at ${templateDir}. Ensure the package is installed correctly.`
|
|
22001
22343
|
);
|
|
@@ -22003,8 +22345,8 @@ function initAstro(options = {}) {
|
|
|
22003
22345
|
if (update) {
|
|
22004
22346
|
return updateFrameworkFiles(templateDir, targetDir);
|
|
22005
22347
|
}
|
|
22006
|
-
if (
|
|
22007
|
-
const entries =
|
|
22348
|
+
if (fs10.existsSync(targetDir)) {
|
|
22349
|
+
const entries = fs10.readdirSync(targetDir);
|
|
22008
22350
|
if (entries.length > 0 && !force) {
|
|
22009
22351
|
throw new Error(
|
|
22010
22352
|
`Directory "${targetDir}" already exists and is not empty. Use --force to overwrite, or --update to refresh framework files only.`
|
|
@@ -22022,25 +22364,25 @@ function updateFrameworkFiles(templateDir, targetDir) {
|
|
|
22022
22364
|
}
|
|
22023
22365
|
const updated = [];
|
|
22024
22366
|
for (const dir of FRAMEWORK_DIRS) {
|
|
22025
|
-
const src =
|
|
22026
|
-
if (!
|
|
22027
|
-
copyDirRecursive(src,
|
|
22367
|
+
const src = path11.join(templateDir, dir);
|
|
22368
|
+
if (!fs10.existsSync(src)) continue;
|
|
22369
|
+
copyDirRecursive(src, path11.join(targetDir, dir), (rel) => updated.push(path11.join(dir, rel)));
|
|
22028
22370
|
}
|
|
22029
22371
|
for (const file of FRAMEWORK_FILES) {
|
|
22030
|
-
const src =
|
|
22031
|
-
if (!
|
|
22032
|
-
|
|
22372
|
+
const src = path11.join(templateDir, file);
|
|
22373
|
+
if (!fs10.existsSync(src)) continue;
|
|
22374
|
+
fs10.copyFileSync(src, path11.join(targetDir, file));
|
|
22033
22375
|
updated.push(file);
|
|
22034
22376
|
}
|
|
22035
22377
|
if (mergeDependencies(templateDir, targetDir)) updated.push("package.json (deps)");
|
|
22036
22378
|
return { targetDir, updatedFiles: updated };
|
|
22037
22379
|
}
|
|
22038
22380
|
function mergeDependencies(templateDir, targetDir) {
|
|
22039
|
-
const tmplPkgPath =
|
|
22040
|
-
const userPkgPath =
|
|
22041
|
-
if (!
|
|
22042
|
-
const tmpl = JSON.parse(
|
|
22043
|
-
const user = JSON.parse(
|
|
22381
|
+
const tmplPkgPath = path11.join(templateDir, "package.json");
|
|
22382
|
+
const userPkgPath = path11.join(targetDir, "package.json");
|
|
22383
|
+
if (!fs10.existsSync(tmplPkgPath) || !fs10.existsSync(userPkgPath)) return false;
|
|
22384
|
+
const tmpl = JSON.parse(fs10.readFileSync(tmplPkgPath, "utf8"));
|
|
22385
|
+
const user = JSON.parse(fs10.readFileSync(userPkgPath, "utf8"));
|
|
22044
22386
|
user.dependencies = user.dependencies ?? {};
|
|
22045
22387
|
let changed = false;
|
|
22046
22388
|
for (const [name, version] of Object.entries(tmpl.dependencies ?? {})) {
|
|
@@ -22050,30 +22392,30 @@ function mergeDependencies(templateDir, targetDir) {
|
|
|
22050
22392
|
}
|
|
22051
22393
|
}
|
|
22052
22394
|
if (changed) {
|
|
22053
|
-
|
|
22395
|
+
fs10.writeFileSync(userPkgPath, `${JSON.stringify(user, null, 2)}
|
|
22054
22396
|
`, "utf8");
|
|
22055
22397
|
}
|
|
22056
22398
|
return changed;
|
|
22057
22399
|
}
|
|
22058
22400
|
function copyDirRecursive(src, dest, onFile, baseSrc = src) {
|
|
22059
|
-
|
|
22060
|
-
const entries =
|
|
22401
|
+
fs10.mkdirSync(dest, { recursive: true });
|
|
22402
|
+
const entries = fs10.readdirSync(src, { withFileTypes: true });
|
|
22061
22403
|
for (const entry of entries) {
|
|
22062
|
-
const srcPath =
|
|
22404
|
+
const srcPath = path11.join(src, entry.name);
|
|
22063
22405
|
const destName = entry.name === "gitignore" ? ".gitignore" : entry.name;
|
|
22064
|
-
const destPath =
|
|
22406
|
+
const destPath = path11.join(dest, destName);
|
|
22065
22407
|
if (entry.isDirectory()) {
|
|
22066
22408
|
copyDirRecursive(srcPath, destPath, onFile, baseSrc);
|
|
22067
22409
|
} else {
|
|
22068
|
-
|
|
22069
|
-
onFile?.(
|
|
22410
|
+
fs10.copyFileSync(srcPath, destPath);
|
|
22411
|
+
onFile?.(path11.relative(baseSrc, srcPath));
|
|
22070
22412
|
}
|
|
22071
22413
|
}
|
|
22072
22414
|
}
|
|
22073
22415
|
|
|
22074
22416
|
// src/scaffold-doc.ts
|
|
22075
|
-
import * as
|
|
22076
|
-
import * as
|
|
22417
|
+
import * as fs11 from "fs";
|
|
22418
|
+
import * as path12 from "path";
|
|
22077
22419
|
var TEMPLATES = [
|
|
22078
22420
|
"adr",
|
|
22079
22421
|
"runbook",
|
|
@@ -22090,7 +22432,7 @@ function isoDate(today) {
|
|
|
22090
22432
|
function nextSeq(dir) {
|
|
22091
22433
|
let max = 0;
|
|
22092
22434
|
try {
|
|
22093
|
-
for (const entry of
|
|
22435
|
+
for (const entry of fs11.readdirSync(dir)) {
|
|
22094
22436
|
const match = /^(\d{1,4})-/.exec(entry);
|
|
22095
22437
|
if (match) max = Math.max(max, Number.parseInt(match[1], 10));
|
|
22096
22438
|
}
|
|
@@ -22247,12 +22589,12 @@ function scaffoldDoc(options) {
|
|
|
22247
22589
|
);
|
|
22248
22590
|
}
|
|
22249
22591
|
const spec = TEMPLATE_SPECS[template];
|
|
22250
|
-
const baseDir = options.baseDir ??
|
|
22592
|
+
const baseDir = options.baseDir ?? path12.join("src", "content", "docs");
|
|
22251
22593
|
const today = options.today ?? /* @__PURE__ */ new Date();
|
|
22252
22594
|
const name = (options.name ?? "").trim() || defaultName(template);
|
|
22253
22595
|
const slug2 = slugify3(name);
|
|
22254
22596
|
const scenarioId = normalizeScenarioId(options.scenarioId);
|
|
22255
|
-
const dir =
|
|
22597
|
+
const dir = path12.join(baseDir, spec.subdir);
|
|
22256
22598
|
if (template === "scenario-note" && !scenarioId) {
|
|
22257
22599
|
throw new Error(`Template "scenario-note" requires --scenario-id.`);
|
|
22258
22600
|
}
|
|
@@ -22264,14 +22606,14 @@ function scaffoldDoc(options) {
|
|
|
22264
22606
|
seq: nextSeq(dir)
|
|
22265
22607
|
};
|
|
22266
22608
|
const filename = `${spec.filename(slug2, ctx)}.mdx`;
|
|
22267
|
-
const filePath =
|
|
22268
|
-
if (
|
|
22609
|
+
const filePath = path12.join(dir, filename);
|
|
22610
|
+
if (fs11.existsSync(filePath) && !options.force) {
|
|
22269
22611
|
throw new Error(
|
|
22270
22612
|
`File "${filePath}" already exists. Use --force to overwrite.`
|
|
22271
22613
|
);
|
|
22272
22614
|
}
|
|
22273
|
-
|
|
22274
|
-
|
|
22615
|
+
fs11.mkdirSync(dir, { recursive: true });
|
|
22616
|
+
fs11.writeFileSync(filePath, spec.content(ctx), "utf8");
|
|
22275
22617
|
return { template, path: filePath, title: titleFor2(template, ctx) };
|
|
22276
22618
|
}
|
|
22277
22619
|
function defaultName(template) {
|
|
@@ -22312,20 +22654,20 @@ function normalizeScenarioId(input) {
|
|
|
22312
22654
|
}
|
|
22313
22655
|
|
|
22314
22656
|
// src/check-links.ts
|
|
22315
|
-
import * as
|
|
22316
|
-
import * as
|
|
22657
|
+
import * as fs13 from "fs";
|
|
22658
|
+
import * as path14 from "path";
|
|
22317
22659
|
|
|
22318
22660
|
// src/utils/markdown-files.ts
|
|
22319
|
-
import * as
|
|
22320
|
-
import * as
|
|
22661
|
+
import * as fs12 from "fs";
|
|
22662
|
+
import * as path13 from "path";
|
|
22321
22663
|
function collectMarkdownFiles(target) {
|
|
22322
|
-
if (!
|
|
22323
|
-
if (
|
|
22664
|
+
if (!fs12.existsSync(target)) return [];
|
|
22665
|
+
if (fs12.statSync(target).isFile()) return [target];
|
|
22324
22666
|
const out = [];
|
|
22325
22667
|
const walk = (dir) => {
|
|
22326
|
-
for (const entry of
|
|
22668
|
+
for (const entry of fs12.readdirSync(dir, { withFileTypes: true })) {
|
|
22327
22669
|
if (entry.name.startsWith(".") || entry.name === "node_modules") continue;
|
|
22328
|
-
const full =
|
|
22670
|
+
const full = path13.join(dir, entry.name);
|
|
22329
22671
|
if (entry.isDirectory()) walk(full);
|
|
22330
22672
|
else if (/\.mdx?$/u.test(entry.name)) out.push(full);
|
|
22331
22673
|
}
|
|
@@ -22364,17 +22706,17 @@ function classifyLink(link2) {
|
|
|
22364
22706
|
function resolutionCandidates(fromFile, link2) {
|
|
22365
22707
|
const withoutAnchor = link2.split("#")[0];
|
|
22366
22708
|
if (!withoutAnchor) return [];
|
|
22367
|
-
const base =
|
|
22709
|
+
const base = path14.resolve(path14.dirname(fromFile), withoutAnchor);
|
|
22368
22710
|
const candidates = [base];
|
|
22369
|
-
if (!
|
|
22711
|
+
if (!path14.extname(base)) {
|
|
22370
22712
|
candidates.push(`${base}.md`, `${base}.mdx`);
|
|
22371
|
-
candidates.push(
|
|
22713
|
+
candidates.push(path14.join(base, "index.md"), path14.join(base, "index.mdx"));
|
|
22372
22714
|
}
|
|
22373
22715
|
return candidates;
|
|
22374
22716
|
}
|
|
22375
22717
|
function resolvesOnDisk(fromFile, link2) {
|
|
22376
22718
|
return resolutionCandidates(fromFile, link2).some(
|
|
22377
|
-
(candidate) =>
|
|
22719
|
+
(candidate) => fs13.existsSync(candidate) && fs13.statSync(candidate).isFile()
|
|
22378
22720
|
);
|
|
22379
22721
|
}
|
|
22380
22722
|
async function isExternalAlive(url, timeoutMs) {
|
|
@@ -22400,7 +22742,7 @@ async function isExternalAlive(url, timeoutMs) {
|
|
|
22400
22742
|
}
|
|
22401
22743
|
async function checkLinks(options) {
|
|
22402
22744
|
const { target, checkExternal = false, externalTimeoutMs = 8e3 } = options;
|
|
22403
|
-
if (!
|
|
22745
|
+
if (!fs13.existsSync(target)) {
|
|
22404
22746
|
throw new Error(`Path not found: ${target}`);
|
|
22405
22747
|
}
|
|
22406
22748
|
const files = collectMarkdownFiles(target);
|
|
@@ -22410,7 +22752,7 @@ async function checkLinks(options) {
|
|
|
22410
22752
|
let skipped = 0;
|
|
22411
22753
|
const externalCache = /* @__PURE__ */ new Map();
|
|
22412
22754
|
for (const file of files) {
|
|
22413
|
-
const content =
|
|
22755
|
+
const content = fs13.readFileSync(file, "utf8");
|
|
22414
22756
|
for (const link2 of extractLinks(content)) {
|
|
22415
22757
|
const kind = classifyLink(link2);
|
|
22416
22758
|
if (kind === "anchor" || kind === "mail" || kind === "root") {
|
|
@@ -22466,8 +22808,8 @@ function formatLinkReport(report) {
|
|
|
22466
22808
|
}
|
|
22467
22809
|
|
|
22468
22810
|
// src/import-openapi.ts
|
|
22469
|
-
import * as
|
|
22470
|
-
import * as
|
|
22811
|
+
import * as fs14 from "fs";
|
|
22812
|
+
import * as path15 from "path";
|
|
22471
22813
|
import { parse as parseYamlString } from "yaml";
|
|
22472
22814
|
var HTTP_METHODS = ["get", "post", "put", "patch", "delete", "options", "head"];
|
|
22473
22815
|
function parseYaml(raw, specPath) {
|
|
@@ -22480,9 +22822,9 @@ function parseYaml(raw, specPath) {
|
|
|
22480
22822
|
}
|
|
22481
22823
|
}
|
|
22482
22824
|
function parseSpec(specPath) {
|
|
22483
|
-
if (!
|
|
22484
|
-
const raw =
|
|
22485
|
-
const ext =
|
|
22825
|
+
if (!fs14.existsSync(specPath)) throw new Error(`Spec not found: ${specPath}`);
|
|
22826
|
+
const raw = fs14.readFileSync(specPath, "utf8");
|
|
22827
|
+
const ext = path15.extname(specPath).toLowerCase();
|
|
22486
22828
|
if (ext === ".json") return JSON.parse(raw);
|
|
22487
22829
|
if (ext === ".yaml" || ext === ".yml") return parseYaml(raw, specPath);
|
|
22488
22830
|
try {
|
|
@@ -22513,8 +22855,8 @@ function extractEndpoints(spec) {
|
|
|
22513
22855
|
}
|
|
22514
22856
|
function loadScenarios(runFile) {
|
|
22515
22857
|
if (!runFile) return [];
|
|
22516
|
-
if (!
|
|
22517
|
-
const report = JSON.parse(
|
|
22858
|
+
if (!fs14.existsSync(runFile)) throw new Error(`Run file not found: ${runFile}`);
|
|
22859
|
+
const report = JSON.parse(fs14.readFileSync(runFile, "utf8"));
|
|
22518
22860
|
return (report.features ?? []).flatMap((f) => f.scenarios ?? []);
|
|
22519
22861
|
}
|
|
22520
22862
|
function endpointRefs(endpoint) {
|
|
@@ -22621,25 +22963,25 @@ async function importOpenApi(options) {
|
|
|
22621
22963
|
list.push(item);
|
|
22622
22964
|
groups.set(item.endpoint.tag, list);
|
|
22623
22965
|
}
|
|
22624
|
-
const outputDir = options.outputDir ??
|
|
22625
|
-
if (
|
|
22626
|
-
const entries =
|
|
22966
|
+
const outputDir = options.outputDir ?? path15.join("src", "content", "docs", "api");
|
|
22967
|
+
if (fs14.existsSync(outputDir) && !options.force) {
|
|
22968
|
+
const entries = fs14.readdirSync(outputDir);
|
|
22627
22969
|
if (entries.length > 0) {
|
|
22628
22970
|
throw new Error(`Output directory "${outputDir}" is not empty. Use --force to overwrite.`);
|
|
22629
22971
|
}
|
|
22630
22972
|
}
|
|
22631
|
-
|
|
22973
|
+
fs14.mkdirSync(outputDir, { recursive: true });
|
|
22632
22974
|
const coveredCount = coverage.filter((c) => c.status === "covered").length;
|
|
22633
22975
|
const uncoveredCount = coverage.filter((c) => c.status === "uncovered").length;
|
|
22634
|
-
|
|
22635
|
-
|
|
22976
|
+
fs14.writeFileSync(
|
|
22977
|
+
path15.join(outputDir, "index.mdx"),
|
|
22636
22978
|
renderIndex(groups, hasRun, { endpointCount: endpoints.length, coveredCount, uncoveredCount }),
|
|
22637
22979
|
"utf8"
|
|
22638
22980
|
);
|
|
22639
22981
|
for (const [tag, rows] of groups) {
|
|
22640
|
-
const dir =
|
|
22641
|
-
|
|
22642
|
-
|
|
22982
|
+
const dir = path15.join(outputDir, slug(tag));
|
|
22983
|
+
fs14.mkdirSync(dir, { recursive: true });
|
|
22984
|
+
fs14.writeFileSync(path15.join(dir, "index.mdx"), renderTagPage(tag, rows, hasRun), "utf8");
|
|
22643
22985
|
}
|
|
22644
22986
|
return {
|
|
22645
22987
|
outputDir,
|
|
@@ -22651,8 +22993,8 @@ async function importOpenApi(options) {
|
|
|
22651
22993
|
}
|
|
22652
22994
|
|
|
22653
22995
|
// src/build-docs.ts
|
|
22654
|
-
import * as
|
|
22655
|
-
import * as
|
|
22996
|
+
import * as fs16 from "fs";
|
|
22997
|
+
import * as path17 from "path";
|
|
22656
22998
|
|
|
22657
22999
|
// src/scenario-links.ts
|
|
22658
23000
|
function scenarioAnchor(title) {
|
|
@@ -22749,8 +23091,8 @@ ${body.join("\n")}
|
|
|
22749
23091
|
}
|
|
22750
23092
|
|
|
22751
23093
|
// src/notes-index.ts
|
|
22752
|
-
import * as
|
|
22753
|
-
import * as
|
|
23094
|
+
import * as fs15 from "fs";
|
|
23095
|
+
import * as path16 from "path";
|
|
22754
23096
|
import { slug as githubSlug } from "github-slugger";
|
|
22755
23097
|
import { parse as parseYaml2 } from "yaml";
|
|
22756
23098
|
function buildScenarioNotesIndex(notesDir) {
|
|
@@ -22761,8 +23103,8 @@ function buildScenarioNotesIndex(notesDir) {
|
|
|
22761
23103
|
};
|
|
22762
23104
|
}
|
|
22763
23105
|
function writeNotesIndex(index, outPath) {
|
|
22764
|
-
|
|
22765
|
-
|
|
23106
|
+
fs15.mkdirSync(path16.dirname(outPath), { recursive: true });
|
|
23107
|
+
fs15.writeFileSync(outPath, JSON.stringify(index, null, 2), "utf8");
|
|
22766
23108
|
return index;
|
|
22767
23109
|
}
|
|
22768
23110
|
function notesByScenarioId(index) {
|
|
@@ -22779,10 +23121,10 @@ function noteLinkMarkdown(note) {
|
|
|
22779
23121
|
return `[Business context \u2192](${noteHref(note)})`;
|
|
22780
23122
|
}
|
|
22781
23123
|
function readScenarioNote(filePath, notesDir) {
|
|
22782
|
-
const relative5 =
|
|
23124
|
+
const relative5 = path16.relative(notesDir, filePath);
|
|
22783
23125
|
const stem = relative5.replace(/\.(?:md|mdx)$/u, "");
|
|
22784
|
-
const frontmatter = parseFrontmatter(
|
|
22785
|
-
const scenarioId = typeof frontmatter.scenarioId === "string" && frontmatter.scenarioId.trim().length > 0 ? frontmatter.scenarioId.trim() :
|
|
23126
|
+
const frontmatter = parseFrontmatter(fs15.readFileSync(filePath, "utf8"));
|
|
23127
|
+
const scenarioId = typeof frontmatter.scenarioId === "string" && frontmatter.scenarioId.trim().length > 0 ? frontmatter.scenarioId.trim() : path16.basename(stem);
|
|
22786
23128
|
const title = typeof frontmatter.title === "string" && frontmatter.title.trim().length > 0 ? frontmatter.title.trim() : `Business context \u2014 ${scenarioId}`;
|
|
22787
23129
|
return {
|
|
22788
23130
|
scenarioId,
|
|
@@ -22797,7 +23139,7 @@ function parseFrontmatter(source) {
|
|
|
22797
23139
|
return parsed && typeof parsed === "object" ? parsed : {};
|
|
22798
23140
|
}
|
|
22799
23141
|
function toRouteSlug(stem) {
|
|
22800
|
-
return stem.split(
|
|
23142
|
+
return stem.split(path16.sep).map((segment) => githubSlug(segment)).join("/").replace(/\/index$/u, "");
|
|
22801
23143
|
}
|
|
22802
23144
|
|
|
22803
23145
|
// src/overview-page.ts
|
|
@@ -22900,22 +23242,22 @@ var BuildDocsError = class extends Error {
|
|
|
22900
23242
|
};
|
|
22901
23243
|
var isRemote = (p) => /^(?:https?:|data:)/i.test(p);
|
|
22902
23244
|
function bundleExplorerAssets(reportPath, assetsDir, baseUrl = "/stories/assets") {
|
|
22903
|
-
if (!
|
|
22904
|
-
const report = JSON.parse(
|
|
23245
|
+
if (!fs16.existsSync(reportPath)) return 0;
|
|
23246
|
+
const report = JSON.parse(fs16.readFileSync(reportPath, "utf8"));
|
|
22905
23247
|
let copied = 0;
|
|
22906
23248
|
const bundle = (value) => {
|
|
22907
|
-
const rel = copyAsset(
|
|
23249
|
+
const rel = copyAsset(path17.resolve(value), assetsDir);
|
|
22908
23250
|
copied++;
|
|
22909
|
-
return `${baseUrl}/${
|
|
23251
|
+
return `${baseUrl}/${path17.basename(rel)}`;
|
|
22910
23252
|
};
|
|
22911
23253
|
const visit = (entries) => {
|
|
22912
23254
|
for (const entry of entries ?? []) {
|
|
22913
23255
|
const e = entry;
|
|
22914
23256
|
if (e.kind === "screenshot" || e.kind === "video" || e.kind === "html") {
|
|
22915
|
-
if (typeof e.path === "string" && !isRemote(e.path) &&
|
|
23257
|
+
if (typeof e.path === "string" && !isRemote(e.path) && fs16.existsSync(e.path)) {
|
|
22916
23258
|
e.path = bundle(e.path);
|
|
22917
23259
|
}
|
|
22918
|
-
if (typeof e.poster === "string" && !isRemote(e.poster) &&
|
|
23260
|
+
if (typeof e.poster === "string" && !isRemote(e.poster) && fs16.existsSync(e.poster)) {
|
|
22919
23261
|
e.poster = bundle(e.poster);
|
|
22920
23262
|
}
|
|
22921
23263
|
}
|
|
@@ -22928,7 +23270,7 @@ function bundleExplorerAssets(reportPath, assetsDir, baseUrl = "/stories/assets"
|
|
|
22928
23270
|
}
|
|
22929
23271
|
}
|
|
22930
23272
|
if (copied > 0) {
|
|
22931
|
-
|
|
23273
|
+
fs16.writeFileSync(reportPath, JSON.stringify(report, null, 2), "utf8");
|
|
22932
23274
|
}
|
|
22933
23275
|
return copied;
|
|
22934
23276
|
}
|
|
@@ -22951,9 +23293,9 @@ function changeBadgeLookup(diff) {
|
|
|
22951
23293
|
return (tc) => byKey.get(scenarioKey(tc.sourceFile, tc.story.scenario));
|
|
22952
23294
|
}
|
|
22953
23295
|
function readStoryReport(reportPath) {
|
|
22954
|
-
if (!
|
|
23296
|
+
if (!fs16.existsSync(reportPath)) return null;
|
|
22955
23297
|
try {
|
|
22956
|
-
return JSON.parse(
|
|
23298
|
+
return JSON.parse(fs16.readFileSync(reportPath, "utf8"));
|
|
22957
23299
|
} catch {
|
|
22958
23300
|
return null;
|
|
22959
23301
|
}
|
|
@@ -22978,28 +23320,28 @@ function writeScenarioLinks(reportPath, outDir, options = {}) {
|
|
|
22978
23320
|
const report = readStoryReport(reportPath);
|
|
22979
23321
|
if (!report) return null;
|
|
22980
23322
|
const index = buildScenarioLinks(report, { audienceSplit: options.audienceSplit });
|
|
22981
|
-
|
|
22982
|
-
|
|
23323
|
+
fs16.writeFileSync(
|
|
23324
|
+
path17.join(outDir, "scenario-links.json"),
|
|
22983
23325
|
JSON.stringify(index, null, 2),
|
|
22984
23326
|
"utf8"
|
|
22985
23327
|
);
|
|
22986
23328
|
return index;
|
|
22987
23329
|
}
|
|
22988
23330
|
function clearGeneratedPages(dir) {
|
|
22989
|
-
if (!
|
|
22990
|
-
for (const entry of
|
|
22991
|
-
const full =
|
|
23331
|
+
if (!fs16.existsSync(dir)) return;
|
|
23332
|
+
for (const entry of fs16.readdirSync(dir, { withFileTypes: true })) {
|
|
23333
|
+
const full = path17.join(dir, entry.name);
|
|
22992
23334
|
if (entry.isDirectory()) {
|
|
22993
23335
|
clearGeneratedPages(full);
|
|
22994
|
-
if (
|
|
23336
|
+
if (fs16.readdirSync(full).length === 0) fs16.rmdirSync(full);
|
|
22995
23337
|
} else if (/\.mdx?$/.test(entry.name)) {
|
|
22996
|
-
|
|
23338
|
+
fs16.rmSync(full);
|
|
22997
23339
|
}
|
|
22998
23340
|
}
|
|
22999
23341
|
}
|
|
23000
23342
|
function loadCanonicalRun(rawRunPath, synthesize) {
|
|
23001
23343
|
try {
|
|
23002
|
-
const data = JSON.parse(
|
|
23344
|
+
const data = JSON.parse(fs16.readFileSync(path17.resolve(rawRunPath), "utf8"));
|
|
23003
23345
|
if (data.schemaVersion !== 1) {
|
|
23004
23346
|
throw new BuildDocsError(`Unsupported schemaVersion ${data.schemaVersion}. Supported: 1.`, "schema");
|
|
23005
23347
|
}
|
|
@@ -23022,19 +23364,19 @@ ${schemaResult.errors.map((e) => ` ${e}`).join("\n")}`,
|
|
|
23022
23364
|
}
|
|
23023
23365
|
}
|
|
23024
23366
|
async function buildDocs(options) {
|
|
23025
|
-
const siteDir =
|
|
23367
|
+
const siteDir = path17.resolve(options.siteDir);
|
|
23026
23368
|
if (!isScaffoldedAstroSite(siteDir)) {
|
|
23027
23369
|
throw new BuildDocsError(
|
|
23028
23370
|
`"${siteDir}" is not a scaffolded Astro docs site (no astro.config.mjs). Run "executable-stories init-astro <dir>" first, then pass it with --site-dir <dir>.`,
|
|
23029
23371
|
"usage"
|
|
23030
23372
|
);
|
|
23031
23373
|
}
|
|
23032
|
-
const storiesPublicDir =
|
|
23033
|
-
const assetsDir =
|
|
23034
|
-
const storyPagesDir =
|
|
23035
|
-
const notesDir =
|
|
23036
|
-
const apiDir =
|
|
23037
|
-
const reportPath =
|
|
23374
|
+
const storiesPublicDir = path17.join(siteDir, "public", "stories");
|
|
23375
|
+
const assetsDir = path17.join(storiesPublicDir, "assets");
|
|
23376
|
+
const storyPagesDir = path17.join(siteDir, "src", "content", "docs", "stories");
|
|
23377
|
+
const notesDir = path17.join(siteDir, "src", "content", "docs", "notes");
|
|
23378
|
+
const apiDir = path17.join(siteDir, "src", "content", "docs", "api");
|
|
23379
|
+
const reportPath = path17.join(storiesPublicDir, "story-report.json");
|
|
23038
23380
|
const canonical = loadCanonicalRun(options.rawRunPath, options.synthesizeStories ?? true);
|
|
23039
23381
|
try {
|
|
23040
23382
|
await new ReportGenerator({
|
|
@@ -23045,7 +23387,7 @@ async function buildDocs(options) {
|
|
|
23045
23387
|
const currentReport = readStoryReport(reportPath);
|
|
23046
23388
|
let diff;
|
|
23047
23389
|
if (options.baselinePath) {
|
|
23048
|
-
const baselineResolved =
|
|
23390
|
+
const baselineResolved = path17.resolve(options.baselinePath);
|
|
23049
23391
|
const baseline = readStoryReport(baselineResolved);
|
|
23050
23392
|
if (!baseline) {
|
|
23051
23393
|
throw new BuildDocsError(
|
|
@@ -23083,7 +23425,7 @@ async function buildDocs(options) {
|
|
|
23083
23425
|
const sub = partitioned[audience];
|
|
23084
23426
|
audiences[audience] = sub.testCases.length;
|
|
23085
23427
|
if (sub.testCases.length === 0) continue;
|
|
23086
|
-
await genPages(sub,
|
|
23428
|
+
await genPages(sub, path17.join(storyPagesDir, audience));
|
|
23087
23429
|
}
|
|
23088
23430
|
} else {
|
|
23089
23431
|
await genPages(canonical, storyPagesDir);
|
|
@@ -23093,30 +23435,30 @@ async function buildDocs(options) {
|
|
|
23093
23435
|
audienceSplit: options.audienceSplit ?? false
|
|
23094
23436
|
});
|
|
23095
23437
|
const scenarioLinks = linksIndex ? Object.keys(linksIndex.scenarios).length : 0;
|
|
23096
|
-
writeNotesIndex(notesIndex,
|
|
23438
|
+
writeNotesIndex(notesIndex, path17.join(storiesPublicDir, "notes-index.json"));
|
|
23097
23439
|
const notesIndexed = notesIndex.notes.length;
|
|
23098
23440
|
if (linksIndex) {
|
|
23099
|
-
|
|
23100
|
-
|
|
23441
|
+
fs16.writeFileSync(
|
|
23442
|
+
path17.join(storyPagesDir, "index.md"),
|
|
23101
23443
|
renderOverviewPage(linksIndex, notesIndex),
|
|
23102
23444
|
"utf8"
|
|
23103
23445
|
);
|
|
23104
23446
|
}
|
|
23105
|
-
const changesJsonPath =
|
|
23106
|
-
const changesMdPath =
|
|
23447
|
+
const changesJsonPath = path17.join(storiesPublicDir, "changes.json");
|
|
23448
|
+
const changesMdPath = path17.join(storyPagesDir, "changes.md");
|
|
23107
23449
|
let changes;
|
|
23108
23450
|
if (diff && linksIndex) {
|
|
23109
|
-
|
|
23110
|
-
|
|
23451
|
+
fs16.writeFileSync(changesJsonPath, JSON.stringify(diff, null, 2), "utf8");
|
|
23452
|
+
fs16.writeFileSync(changesMdPath, renderChangesPage(diff, linksIndex), "utf8");
|
|
23111
23453
|
changes = diff.summary;
|
|
23112
23454
|
} else {
|
|
23113
|
-
|
|
23114
|
-
|
|
23455
|
+
fs16.rmSync(changesJsonPath, { force: true });
|
|
23456
|
+
fs16.rmSync(changesMdPath, { force: true });
|
|
23115
23457
|
}
|
|
23116
23458
|
let apiPages = 0;
|
|
23117
23459
|
if (options.openapiPath) {
|
|
23118
23460
|
const res = await importOpenApi({
|
|
23119
|
-
specPath:
|
|
23461
|
+
specPath: path17.resolve(options.openapiPath),
|
|
23120
23462
|
outputDir: apiDir,
|
|
23121
23463
|
runFile: reportPath,
|
|
23122
23464
|
force: true
|
|
@@ -23131,11 +23473,11 @@ async function buildDocs(options) {
|
|
|
23131
23473
|
}
|
|
23132
23474
|
|
|
23133
23475
|
// src/config.ts
|
|
23134
|
-
import { existsSync as
|
|
23135
|
-
import { resolve as
|
|
23476
|
+
import { existsSync as existsSync14 } from "fs";
|
|
23477
|
+
import { resolve as resolve11 } from "path";
|
|
23136
23478
|
async function loadConfig(configPath) {
|
|
23137
|
-
const resolved = configPath ?
|
|
23138
|
-
if (!
|
|
23479
|
+
const resolved = configPath ? resolve11(configPath) : resolve11(process.cwd(), "executable-stories.config.js");
|
|
23480
|
+
if (!existsSync14(resolved)) return {};
|
|
23139
23481
|
const mod = await import(resolved);
|
|
23140
23482
|
const config = mod.default;
|
|
23141
23483
|
if (!config || typeof config !== "object" || Array.isArray(config)) {
|
|
@@ -23163,6 +23505,7 @@ USAGE
|
|
|
23163
23505
|
executable-stories format <file> [options]
|
|
23164
23506
|
executable-stories format --stdin [options]
|
|
23165
23507
|
executable-stories watch <raw-run.json> [options]
|
|
23508
|
+
executable-stories serve <raw-run.json> [--port <n>] [--host <host>] [options]
|
|
23166
23509
|
executable-stories compare <baseline-file> <current-file> [options]
|
|
23167
23510
|
executable-stories gate-release <dev-run.json> <rc-run.json> [options]
|
|
23168
23511
|
executable-stories review <file> --changed-files <path> [options]
|
|
@@ -23186,6 +23529,7 @@ USAGE
|
|
|
23186
23529
|
SUBCOMMANDS
|
|
23187
23530
|
format Read raw test results and generate reports
|
|
23188
23531
|
watch Regenerate reports whenever the raw-run file changes (live agent index)
|
|
23532
|
+
serve Live docs URL: regenerate + browser reload + "what changed since you started" (for agent loops)
|
|
23189
23533
|
compare Compare two runs and generate a diff report
|
|
23190
23534
|
gate-release Verify a release candidate against the dev test baseline (RC gate)
|
|
23191
23535
|
review Generate an Evidence Review of AI-authored changes (correlate a run to the diff)
|
|
@@ -23404,9 +23748,9 @@ async function parseCliArgs(argv) {
|
|
|
23404
23748
|
process.exit(EXIT_SUCCESS);
|
|
23405
23749
|
}
|
|
23406
23750
|
const subcommand = args[0];
|
|
23407
|
-
if (subcommand !== "format" && subcommand !== "watch" && subcommand !== "compare" && subcommand !== "gate-release" && subcommand !== "deploy" && subcommand !== "review" && subcommand !== "list" && subcommand !== "check" && subcommand !== "goal" && subcommand !== "triage" && subcommand !== "validate" && subcommand !== "init-astro" && subcommand !== "build-docs" && subcommand !== "new" && subcommand !== "check-links" && subcommand !== "import-openapi" && subcommand !== "publish-confluence" && subcommand !== "publish-jira") {
|
|
23751
|
+
if (subcommand !== "format" && subcommand !== "watch" && subcommand !== "serve" && subcommand !== "compare" && subcommand !== "gate-release" && subcommand !== "deploy" && subcommand !== "review" && subcommand !== "list" && subcommand !== "check" && subcommand !== "goal" && subcommand !== "triage" && subcommand !== "validate" && subcommand !== "init-astro" && subcommand !== "build-docs" && subcommand !== "new" && subcommand !== "check-links" && subcommand !== "import-openapi" && subcommand !== "publish-confluence" && subcommand !== "publish-jira") {
|
|
23408
23752
|
console.error(
|
|
23409
|
-
`Unknown subcommand: "${subcommand}". Use "format", "watch", "compare", "gate-release", "deploy", "review", "list", "check", "goal", "triage", "validate", "init-astro", "build-docs", "new", "check-links", "import-openapi", "publish-confluence", or "publish-jira".`
|
|
23753
|
+
`Unknown subcommand: "${subcommand}". Use "format", "watch", "serve", "compare", "gate-release", "deploy", "review", "list", "check", "goal", "triage", "validate", "init-astro", "build-docs", "new", "check-links", "import-openapi", "publish-confluence", or "publish-jira".`
|
|
23410
23754
|
);
|
|
23411
23755
|
process.exit(EXIT_USAGE);
|
|
23412
23756
|
}
|
|
@@ -23524,6 +23868,8 @@ async function parseCliArgs(argv) {
|
|
|
23524
23868
|
"webhook-hmac-timestamp": { type: "boolean", default: false },
|
|
23525
23869
|
"asset-mode": { type: "string", default: "none" },
|
|
23526
23870
|
"allow-missing-assets": { type: "boolean", default: false },
|
|
23871
|
+
port: { type: "string" },
|
|
23872
|
+
host: { type: "string" },
|
|
23527
23873
|
"pr-summary": { type: "boolean", default: false },
|
|
23528
23874
|
"pr-summary-file": { type: "string" },
|
|
23529
23875
|
"fail-on-regression": { type: "boolean", default: false },
|
|
@@ -23746,7 +24092,9 @@ async function parseCliArgs(argv) {
|
|
|
23746
24092
|
headRef: values["head-ref"],
|
|
23747
24093
|
failOn: failOnRaw,
|
|
23748
24094
|
minEvidence: minEvidenceRaw,
|
|
23749
|
-
config: values["config"]
|
|
24095
|
+
config: values["config"],
|
|
24096
|
+
servePort: values["port"] ? Number.parseInt(values["port"], 10) : 4321,
|
|
24097
|
+
serveHost: values["host"] ?? "127.0.0.1"
|
|
23750
24098
|
};
|
|
23751
24099
|
return { args: cliArgs, pluginConfig, customRequested };
|
|
23752
24100
|
}
|
|
@@ -23754,27 +24102,27 @@ async function readInput(args) {
|
|
|
23754
24102
|
if (args.stdin) {
|
|
23755
24103
|
return readStdin();
|
|
23756
24104
|
}
|
|
23757
|
-
const filePath =
|
|
23758
|
-
if (!
|
|
24105
|
+
const filePath = path18.resolve(args.inputFile);
|
|
24106
|
+
if (!fs17.existsSync(filePath)) {
|
|
23759
24107
|
console.error(`Error: File not found: ${filePath}`);
|
|
23760
24108
|
process.exit(EXIT_USAGE);
|
|
23761
24109
|
}
|
|
23762
|
-
return
|
|
24110
|
+
return fs17.readFileSync(filePath, "utf8");
|
|
23763
24111
|
}
|
|
23764
24112
|
function readFileInput(filePath) {
|
|
23765
|
-
const resolved =
|
|
23766
|
-
if (!
|
|
24113
|
+
const resolved = path18.resolve(filePath);
|
|
24114
|
+
if (!fs17.existsSync(resolved)) {
|
|
23767
24115
|
console.error(`Error: File not found: ${resolved}`);
|
|
23768
24116
|
process.exit(EXIT_USAGE);
|
|
23769
24117
|
}
|
|
23770
|
-
return
|
|
24118
|
+
return fs17.readFileSync(resolved, "utf8");
|
|
23771
24119
|
}
|
|
23772
24120
|
function readStdin() {
|
|
23773
|
-
return new Promise((
|
|
24121
|
+
return new Promise((resolve13, reject) => {
|
|
23774
24122
|
const chunks = [];
|
|
23775
24123
|
process.stdin.setEncoding("utf8");
|
|
23776
24124
|
process.stdin.on("data", (chunk) => chunks.push(chunk));
|
|
23777
|
-
process.stdin.on("end", () =>
|
|
24125
|
+
process.stdin.on("end", () => resolve13(chunks.join("")));
|
|
23778
24126
|
process.stdin.on("error", reject);
|
|
23779
24127
|
});
|
|
23780
24128
|
}
|
|
@@ -23900,14 +24248,14 @@ function tryNormalizeRunFromText(text2, args) {
|
|
|
23900
24248
|
}
|
|
23901
24249
|
}
|
|
23902
24250
|
function listBaselineCandidates(currentFile, args) {
|
|
23903
|
-
const baselineDir =
|
|
23904
|
-
const currentResolved =
|
|
23905
|
-
if (!
|
|
24251
|
+
const baselineDir = path18.resolve(args.baselineDir ?? path18.dirname(currentFile));
|
|
24252
|
+
const currentResolved = path18.resolve(currentFile);
|
|
24253
|
+
if (!fs17.existsSync(baselineDir)) {
|
|
23906
24254
|
console.error(`Error: baseline directory not found: ${baselineDir}`);
|
|
23907
24255
|
process.exit(EXIT_USAGE);
|
|
23908
24256
|
}
|
|
23909
|
-
const entries =
|
|
23910
|
-
return entries.filter((entry) => entry.isFile()).map((entry) =>
|
|
24257
|
+
const entries = fs17.readdirSync(baselineDir, { withFileTypes: true });
|
|
24258
|
+
return entries.filter((entry) => entry.isFile()).map((entry) => path18.join(baselineDir, entry.name)).filter((candidate) => path18.resolve(candidate) !== currentResolved).filter(
|
|
23911
24259
|
(candidate) => args.inputType === "ndjson" ? candidate.endsWith(".ndjson") : candidate.endsWith(".json")
|
|
23912
24260
|
);
|
|
23913
24261
|
}
|
|
@@ -23915,14 +24263,14 @@ function resolveBaselineAuto(currentFile, currentRun, args) {
|
|
|
23915
24263
|
const candidates = listBaselineCandidates(currentFile, args);
|
|
23916
24264
|
const comparable = [];
|
|
23917
24265
|
for (const candidate of candidates) {
|
|
23918
|
-
const run = tryNormalizeRunFromText(
|
|
24266
|
+
const run = tryNormalizeRunFromText(fs17.readFileSync(candidate, "utf8"), args);
|
|
23919
24267
|
if (run) {
|
|
23920
24268
|
comparable.push({ file: candidate, run });
|
|
23921
24269
|
}
|
|
23922
24270
|
}
|
|
23923
24271
|
if (comparable.length === 0) {
|
|
23924
24272
|
console.error(
|
|
23925
|
-
`Error: no compatible baseline files found in ${
|
|
24273
|
+
`Error: no compatible baseline files found in ${path18.resolve(args.baselineDir ?? path18.dirname(currentFile))}.`
|
|
23926
24274
|
);
|
|
23927
24275
|
process.exit(EXIT_USAGE);
|
|
23928
24276
|
}
|
|
@@ -24124,6 +24472,24 @@ async function main() {
|
|
|
24124
24472
|
});
|
|
24125
24473
|
return;
|
|
24126
24474
|
}
|
|
24475
|
+
if (args.subcommand === "serve") {
|
|
24476
|
+
if (!args.inputFile) {
|
|
24477
|
+
console.error("Error: serve requires an input file (the raw-run JSON the framework writes).");
|
|
24478
|
+
process.exit(EXIT_USAGE);
|
|
24479
|
+
}
|
|
24480
|
+
const serveFormats = args.formats.includes("html") ? args.formats : [...args.formats, "html"];
|
|
24481
|
+
startServe({
|
|
24482
|
+
input: args.inputFile,
|
|
24483
|
+
outputDir: args.outputDir,
|
|
24484
|
+
outputName: args.outputName,
|
|
24485
|
+
formats: serveFormats,
|
|
24486
|
+
inputType: args.inputType === "canonical" ? "canonical" : "raw",
|
|
24487
|
+
synthesize: args.synthesizeStories,
|
|
24488
|
+
port: args.servePort,
|
|
24489
|
+
host: args.serveHost
|
|
24490
|
+
});
|
|
24491
|
+
return;
|
|
24492
|
+
}
|
|
24127
24493
|
const text2 = await readInput(args);
|
|
24128
24494
|
if (args.inputType === "ndjson") {
|
|
24129
24495
|
if (args.subcommand === "validate") {
|
|
@@ -24167,9 +24533,9 @@ async function main() {
|
|
|
24167
24533
|
process.exit(EXIT_SCHEMA_VALIDATION);
|
|
24168
24534
|
}
|
|
24169
24535
|
if (args.emitCanonical) {
|
|
24170
|
-
const outPath =
|
|
24171
|
-
|
|
24172
|
-
|
|
24536
|
+
const outPath = path18.resolve(args.emitCanonical);
|
|
24537
|
+
fs17.mkdirSync(path18.dirname(outPath), { recursive: true });
|
|
24538
|
+
fs17.writeFileSync(outPath, JSON.stringify(run, null, 2), "utf8");
|
|
24173
24539
|
}
|
|
24174
24540
|
try {
|
|
24175
24541
|
const result = await generateReports(run, args);
|
|
@@ -24226,9 +24592,9 @@ ${msg}`);
|
|
|
24226
24592
|
}
|
|
24227
24593
|
const run = data;
|
|
24228
24594
|
if (args.emitCanonical) {
|
|
24229
|
-
const outPath =
|
|
24230
|
-
|
|
24231
|
-
|
|
24595
|
+
const outPath = path18.resolve(args.emitCanonical);
|
|
24596
|
+
fs17.mkdirSync(path18.dirname(outPath), { recursive: true });
|
|
24597
|
+
fs17.writeFileSync(outPath, JSON.stringify(run, null, 2), "utf8");
|
|
24232
24598
|
}
|
|
24233
24599
|
try {
|
|
24234
24600
|
const result = await generateReports(run, args);
|
|
@@ -24284,9 +24650,9 @@ ${msg}`);
|
|
|
24284
24650
|
process.exit(EXIT_CANONICAL_VALIDATION);
|
|
24285
24651
|
}
|
|
24286
24652
|
if (args.emitCanonical) {
|
|
24287
|
-
const outPath =
|
|
24288
|
-
|
|
24289
|
-
|
|
24653
|
+
const outPath = path18.resolve(args.emitCanonical);
|
|
24654
|
+
fs17.mkdirSync(path18.dirname(outPath), { recursive: true });
|
|
24655
|
+
fs17.writeFileSync(outPath, JSON.stringify(canonical, null, 2), "utf8");
|
|
24290
24656
|
}
|
|
24291
24657
|
try {
|
|
24292
24658
|
const result = await generateReports(canonical, args, droppedMissingStory);
|
|
@@ -24311,9 +24677,9 @@ function runCustomFormatters(run, customRequested, formatters, args) {
|
|
|
24311
24677
|
const ext = formatter.fileExtension ?? formatName;
|
|
24312
24678
|
const baseName = args.outputName ?? "report";
|
|
24313
24679
|
const filename = args.outputNameTimestamp ? `${baseName}-${Math.floor(run.startedAtMs / 1e3)}.${ext}` : `${baseName}.${ext}`;
|
|
24314
|
-
const filepath =
|
|
24315
|
-
|
|
24316
|
-
|
|
24680
|
+
const filepath = path18.join(outputDir, filename);
|
|
24681
|
+
fs17.mkdirSync(outputDir, { recursive: true });
|
|
24682
|
+
fs17.writeFileSync(filepath, content, "utf8");
|
|
24317
24683
|
console.log(`Generated: ${filepath}`);
|
|
24318
24684
|
} catch (err) {
|
|
24319
24685
|
console.error(`Error running custom formatter "${formatName}": ${err instanceof Error ? err.message : String(err)}`);
|
|
@@ -24363,13 +24729,13 @@ async function dispatchNotifications(run, args) {
|
|
|
24363
24729
|
}
|
|
24364
24730
|
function runHistoryPipeline(run, args) {
|
|
24365
24731
|
if (!args.historyFile) return;
|
|
24366
|
-
const historyPath =
|
|
24732
|
+
const historyPath = path18.resolve(args.historyFile);
|
|
24367
24733
|
const store = loadHistory(
|
|
24368
24734
|
{ filePath: historyPath },
|
|
24369
24735
|
{
|
|
24370
24736
|
readFile: (p) => {
|
|
24371
24737
|
try {
|
|
24372
|
-
return
|
|
24738
|
+
return fs17.readFileSync(p, "utf8");
|
|
24373
24739
|
} catch {
|
|
24374
24740
|
return void 0;
|
|
24375
24741
|
}
|
|
@@ -24382,11 +24748,11 @@ function runHistoryPipeline(run, args) {
|
|
|
24382
24748
|
run,
|
|
24383
24749
|
maxRuns: args.maxHistoryRuns
|
|
24384
24750
|
});
|
|
24385
|
-
const dir =
|
|
24386
|
-
|
|
24751
|
+
const dir = path18.dirname(historyPath);
|
|
24752
|
+
fs17.mkdirSync(dir, { recursive: true });
|
|
24387
24753
|
saveHistory(
|
|
24388
24754
|
{ filePath: historyPath, store: updated },
|
|
24389
|
-
{ writeFile: (p, content) =>
|
|
24755
|
+
{ writeFile: (p, content) => fs17.writeFileSync(p, content, "utf8") }
|
|
24390
24756
|
);
|
|
24391
24757
|
let metricsCount = 0;
|
|
24392
24758
|
for (const testId of Object.keys(updated.tests)) {
|
|
@@ -24534,11 +24900,11 @@ function writeReviewReport(review, args) {
|
|
|
24534
24900
|
const outputDir = args.outputDir ?? "reports";
|
|
24535
24901
|
const baseName = args.outputName ?? "evidence-review";
|
|
24536
24902
|
const suffix = args.outputNameTimestamp ? `-${Math.floor(review.run.startedAtMs / 1e3)}` : "";
|
|
24537
|
-
|
|
24538
|
-
const mdPath =
|
|
24539
|
-
const htmlPath =
|
|
24540
|
-
|
|
24541
|
-
|
|
24903
|
+
fs17.mkdirSync(outputDir, { recursive: true });
|
|
24904
|
+
const mdPath = path18.join(outputDir, `${baseName}${suffix}.md`);
|
|
24905
|
+
const htmlPath = path18.join(outputDir, `${baseName}${suffix}.html`);
|
|
24906
|
+
fs17.writeFileSync(mdPath, markdown, "utf8");
|
|
24907
|
+
fs17.writeFileSync(htmlPath, html, "utf8");
|
|
24542
24908
|
return [mdPath, htmlPath];
|
|
24543
24909
|
}
|
|
24544
24910
|
function evaluateReviewGate(review, args) {
|
|
@@ -24584,9 +24950,9 @@ function printResult(result, args, startMs, droppedMissingStory = 0) {
|
|
|
24584
24950
|
function printCompareResult(result, args, startMs) {
|
|
24585
24951
|
const durationMs = Date.now() - startMs;
|
|
24586
24952
|
if (result.prSummary && args.prSummaryFile) {
|
|
24587
|
-
const outputPath =
|
|
24588
|
-
|
|
24589
|
-
|
|
24953
|
+
const outputPath = path18.resolve(args.prSummaryFile);
|
|
24954
|
+
fs17.mkdirSync(path18.dirname(outputPath), { recursive: true });
|
|
24955
|
+
fs17.writeFileSync(outputPath, result.prSummary, "utf8");
|
|
24590
24956
|
}
|
|
24591
24957
|
if (args.jsonSummary) {
|
|
24592
24958
|
console.log(
|
|
@@ -24615,13 +24981,13 @@ function printCompareResult(result, args, startMs) {
|
|
|
24615
24981
|
}
|
|
24616
24982
|
}
|
|
24617
24983
|
function loadReleasePolicy(policyPath) {
|
|
24618
|
-
const resolved =
|
|
24619
|
-
if (!
|
|
24984
|
+
const resolved = path18.resolve(policyPath);
|
|
24985
|
+
if (!fs17.existsSync(resolved)) {
|
|
24620
24986
|
console.error(`Error: release policy file not found: ${resolved}`);
|
|
24621
24987
|
process.exit(EXIT_USAGE);
|
|
24622
24988
|
}
|
|
24623
24989
|
try {
|
|
24624
|
-
const raw = JSON.parse(
|
|
24990
|
+
const raw = JSON.parse(fs17.readFileSync(resolved, "utf8"));
|
|
24625
24991
|
return {
|
|
24626
24992
|
allowedOmissions: Array.isArray(raw.allowedOmissions) ? raw.allowedOmissions : [],
|
|
24627
24993
|
allowedRegressions: Array.isArray(raw.allowedRegressions) ? raw.allowedRegressions : [],
|
|
@@ -24725,7 +25091,7 @@ Generate an API token at https://id.atlassian.com/manage-profile/security/api-to
|
|
|
24725
25091
|
console.error("Error: missing ADF file argument. Run with --help for usage.");
|
|
24726
25092
|
process.exit(EXIT_USAGE);
|
|
24727
25093
|
}
|
|
24728
|
-
if (!
|
|
25094
|
+
if (!fs17.existsSync(inputFile)) {
|
|
24729
25095
|
console.error(`Error: file not found: ${inputFile}`);
|
|
24730
25096
|
process.exit(EXIT_USAGE);
|
|
24731
25097
|
}
|
|
@@ -24753,7 +25119,7 @@ Generate an API token at https://id.atlassian.com/manage-profile/security/api-to
|
|
|
24753
25119
|
console.error("Error: --title is required when creating a new page");
|
|
24754
25120
|
process.exit(EXIT_USAGE);
|
|
24755
25121
|
}
|
|
24756
|
-
const adf =
|
|
25122
|
+
const adf = fs17.readFileSync(path18.resolve(inputFile), "utf8");
|
|
24757
25123
|
if (dryRun) {
|
|
24758
25124
|
console.log(
|
|
24759
25125
|
JSON.stringify(
|
|
@@ -24832,7 +25198,7 @@ Generate an API token at https://id.atlassian.com/manage-profile/security/api-to
|
|
|
24832
25198
|
console.error("Error: missing ADF file argument. Run with --help for usage.");
|
|
24833
25199
|
process.exit(EXIT_USAGE);
|
|
24834
25200
|
}
|
|
24835
|
-
if (!
|
|
25201
|
+
if (!fs17.existsSync(inputFile)) {
|
|
24836
25202
|
console.error(`Error: file not found: ${inputFile}`);
|
|
24837
25203
|
process.exit(EXIT_USAGE);
|
|
24838
25204
|
}
|
|
@@ -24859,7 +25225,7 @@ Generate an API token at https://id.atlassian.com/manage-profile/security/api-to
|
|
|
24859
25225
|
process.exit(EXIT_USAGE);
|
|
24860
25226
|
}
|
|
24861
25227
|
const mode = modeRaw;
|
|
24862
|
-
const adf =
|
|
25228
|
+
const adf = fs17.readFileSync(path18.resolve(inputFile), "utf8");
|
|
24863
25229
|
if (dryRun) {
|
|
24864
25230
|
console.log(
|
|
24865
25231
|
JSON.stringify(
|
|
@@ -25047,7 +25413,7 @@ async function runBuildDocs(rawArgs) {
|
|
|
25047
25413
|
` \u2022 What's changed \u2192 src/content/docs/stories/changes.md (+${c.added} added, ${c.regressed} regressed, ${c.fixed} fixed, ${c.removed} removed)`
|
|
25048
25414
|
);
|
|
25049
25415
|
}
|
|
25050
|
-
const rel =
|
|
25416
|
+
const rel = path18.relative(process.cwd(), result.siteDir) || ".";
|
|
25051
25417
|
console.log(`
|
|
25052
25418
|
Preview: cd ${rel} && npm run dev`);
|
|
25053
25419
|
return EXIT_SUCCESS;
|
|
@@ -25274,7 +25640,9 @@ function createDefaultCliArgs() {
|
|
|
25274
25640
|
failOnAddedFailures: false,
|
|
25275
25641
|
failOnRemoval: false,
|
|
25276
25642
|
failOnNew: false,
|
|
25277
|
-
baselineMode: "explicit"
|
|
25643
|
+
baselineMode: "explicit",
|
|
25644
|
+
servePort: 4321,
|
|
25645
|
+
serveHost: "127.0.0.1"
|
|
25278
25646
|
};
|
|
25279
25647
|
}
|
|
25280
25648
|
main().catch((err) => {
|