executable-stories-formatters 0.12.0 → 0.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +20 -0
- package/dist/adapters.d.cts +1 -1
- package/dist/adapters.d.ts +1 -1
- package/dist/cli.js +991 -111
- package/dist/cli.js.map +1 -1
- package/dist/{index-mrT6-JSt.d.cts → index-CXrzCk9p.d.cts} +1 -1
- package/dist/{index-mrT6-JSt.d.ts → index-CXrzCk9p.d.ts} +1 -1
- package/dist/index.cjs +466 -54
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +252 -4
- package/dist/index.d.ts +252 -4
- package/dist/index.js +458 -54
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/schemas/raw-run.schema.json +9 -0
package/dist/cli.js
CHANGED
|
@@ -191,6 +191,11 @@ var raw_run_schema_default = {
|
|
|
191
191
|
},
|
|
192
192
|
description: "Ticket/issue references. Each item is either a string ID or an object with id and optional url."
|
|
193
193
|
},
|
|
194
|
+
covers: {
|
|
195
|
+
type: "array",
|
|
196
|
+
items: { type: "string" },
|
|
197
|
+
description: "Product-code paths/globs this scenario exercises (project-root-relative). Used to map code changes to at-risk scenarios."
|
|
198
|
+
},
|
|
194
199
|
meta: {
|
|
195
200
|
type: "object",
|
|
196
201
|
description: "User-defined metadata for this story."
|
|
@@ -512,6 +517,10 @@ var raw_run_schema_default = {
|
|
|
512
517
|
minimum: 0,
|
|
513
518
|
description: "Step index (0-based)."
|
|
514
519
|
},
|
|
520
|
+
stepId: {
|
|
521
|
+
type: "string",
|
|
522
|
+
description: "Stable step ID when the framework provides one (correlates with StoryStep.id)."
|
|
523
|
+
},
|
|
515
524
|
title: {
|
|
516
525
|
type: "string",
|
|
517
526
|
description: "Step title/description."
|
|
@@ -1228,7 +1237,7 @@ var CucumberJsonFormatter = class {
|
|
|
1228
1237
|
/**
|
|
1229
1238
|
* Build a single step.
|
|
1230
1239
|
*/
|
|
1231
|
-
buildStep(step, result,
|
|
1240
|
+
buildStep(step, result, line2, index, attachments, isLastStep, hasFailedStep) {
|
|
1232
1241
|
const keyword = this.options.keywordSpacing ? `${step.keyword} ` : step.keyword;
|
|
1233
1242
|
const stepResult = this.buildStepResult(result);
|
|
1234
1243
|
const embeddings = this.buildEmbeddings(attachments, result, isLastStep, hasFailedStep);
|
|
@@ -1236,7 +1245,7 @@ var CucumberJsonFormatter = class {
|
|
|
1236
1245
|
embeddings.push(...screenshotEmbeddings);
|
|
1237
1246
|
const jsonStep = {
|
|
1238
1247
|
keyword,
|
|
1239
|
-
line,
|
|
1248
|
+
line: line2,
|
|
1240
1249
|
name: step.text,
|
|
1241
1250
|
result: stepResult
|
|
1242
1251
|
};
|
|
@@ -1290,14 +1299,14 @@ var CucumberJsonFormatter = class {
|
|
|
1290
1299
|
duration: 0
|
|
1291
1300
|
};
|
|
1292
1301
|
}
|
|
1293
|
-
const
|
|
1302
|
+
const statusMap2 = {
|
|
1294
1303
|
passed: "passed",
|
|
1295
1304
|
failed: "failed",
|
|
1296
1305
|
skipped: "skipped",
|
|
1297
1306
|
pending: "pending"
|
|
1298
1307
|
};
|
|
1299
1308
|
const stepResult = {
|
|
1300
|
-
status:
|
|
1309
|
+
status: statusMap2[result.status] ?? "undefined",
|
|
1301
1310
|
// Duration in nanoseconds (Cucumber uses nanoseconds)
|
|
1302
1311
|
duration: result.durationMs * 1e6
|
|
1303
1312
|
};
|
|
@@ -15783,8 +15792,8 @@ var JUnitFormatter = class {
|
|
|
15783
15792
|
case "section": {
|
|
15784
15793
|
const lines = [];
|
|
15785
15794
|
lines.push(`${indent}${entry.title}:`);
|
|
15786
|
-
for (const
|
|
15787
|
-
lines.push(`${indent} ${
|
|
15795
|
+
for (const line2 of entry.markdown.split("\n")) {
|
|
15796
|
+
lines.push(`${indent} ${line2}`);
|
|
15788
15797
|
}
|
|
15789
15798
|
return lines.join("\n");
|
|
15790
15799
|
}
|
|
@@ -15793,8 +15802,8 @@ var JUnitFormatter = class {
|
|
|
15793
15802
|
if (entry.title) {
|
|
15794
15803
|
lines.push(`${indent}${entry.title}:`);
|
|
15795
15804
|
}
|
|
15796
|
-
for (const
|
|
15797
|
-
lines.push(`${indent} ${
|
|
15805
|
+
for (const line2 of entry.code.split("\n")) {
|
|
15806
|
+
lines.push(`${indent} ${line2}`);
|
|
15798
15807
|
}
|
|
15799
15808
|
return lines.join("\n");
|
|
15800
15809
|
}
|
|
@@ -15806,8 +15815,8 @@ var JUnitFormatter = class {
|
|
|
15806
15815
|
const dataStr = JSON.stringify(entry.data, null, 2);
|
|
15807
15816
|
const lines = [];
|
|
15808
15817
|
lines.push(`${indent}[${entry.type}]:`);
|
|
15809
|
-
for (const
|
|
15810
|
-
lines.push(`${indent} ${
|
|
15818
|
+
for (const line2 of dataStr.split("\n")) {
|
|
15819
|
+
lines.push(`${indent} ${line2}`);
|
|
15811
15820
|
}
|
|
15812
15821
|
return lines.join("\n");
|
|
15813
15822
|
}
|
|
@@ -15853,7 +15862,9 @@ var MarkdownFormatter = class {
|
|
|
15853
15862
|
ticketUrlTemplate: options.ticketUrlTemplate,
|
|
15854
15863
|
traceUrlTemplate: options.traceUrlTemplate,
|
|
15855
15864
|
includeSourceLinks: options.includeSourceLinks ?? true,
|
|
15856
|
-
customRenderers: options.customRenderers
|
|
15865
|
+
customRenderers: options.customRenderers,
|
|
15866
|
+
scenarioAnchor: options.scenarioAnchor,
|
|
15867
|
+
scenarioBadge: options.scenarioBadge
|
|
15857
15868
|
};
|
|
15858
15869
|
}
|
|
15859
15870
|
/**
|
|
@@ -16040,6 +16051,11 @@ var MarkdownFormatter = class {
|
|
|
16040
16051
|
* Render a single scenario.
|
|
16041
16052
|
*/
|
|
16042
16053
|
renderScenario(lines, tc) {
|
|
16054
|
+
const anchorId = this.options.scenarioAnchor?.(tc);
|
|
16055
|
+
if (anchorId) {
|
|
16056
|
+
lines.push(`<a id="${anchorId}"></a>`);
|
|
16057
|
+
lines.push("");
|
|
16058
|
+
}
|
|
16043
16059
|
if (this.options.customRenderers?.renderScenarioHeader) {
|
|
16044
16060
|
const custom = this.options.customRenderers.renderScenarioHeader(tc);
|
|
16045
16061
|
if (custom !== null) {
|
|
@@ -16055,6 +16071,10 @@ var MarkdownFormatter = class {
|
|
|
16055
16071
|
icon = this.getStatusIcon(tc.status) + " ";
|
|
16056
16072
|
}
|
|
16057
16073
|
lines.push(`${headingPrefix} ${icon}${tc.story.scenario}`);
|
|
16074
|
+
const badge2 = this.options.scenarioBadge?.(tc);
|
|
16075
|
+
if (badge2) {
|
|
16076
|
+
lines.push(badge2);
|
|
16077
|
+
}
|
|
16058
16078
|
if (this.options.includeSourceLinks && this.options.permalinkBaseUrl && tc.sourceFile !== "unknown") {
|
|
16059
16079
|
const permalink = this.buildPermalink(tc);
|
|
16060
16080
|
lines.push(`Source: [${tc.sourceFile}](${permalink})`);
|
|
@@ -16129,8 +16149,8 @@ var MarkdownFormatter = class {
|
|
|
16129
16149
|
buildPermalink(tc) {
|
|
16130
16150
|
const base = this.options.permalinkBaseUrl.replace(/\/$/, "");
|
|
16131
16151
|
const file = tc.sourceFile;
|
|
16132
|
-
const
|
|
16133
|
-
return `${base}/${file}${
|
|
16152
|
+
const line2 = tc.sourceLine > 0 ? `#L${tc.sourceLine}` : "";
|
|
16153
|
+
return `${base}/${file}${line2}`;
|
|
16134
16154
|
}
|
|
16135
16155
|
/**
|
|
16136
16156
|
* Render a step.
|
|
@@ -16198,8 +16218,8 @@ var MarkdownFormatter = class {
|
|
|
16198
16218
|
lines.push(`${indent}`);
|
|
16199
16219
|
}
|
|
16200
16220
|
lines.push(`${indent}\`\`\`${entry.lang ?? ""}`);
|
|
16201
|
-
for (const
|
|
16202
|
-
lines.push(`${indent}${
|
|
16221
|
+
for (const line2 of (entry.content ?? "").split("\n")) {
|
|
16222
|
+
lines.push(`${indent}${line2}`);
|
|
16203
16223
|
}
|
|
16204
16224
|
lines.push(`${indent}\`\`\``);
|
|
16205
16225
|
lines.push(`${indent}`);
|
|
@@ -16222,8 +16242,8 @@ var MarkdownFormatter = class {
|
|
|
16222
16242
|
case "section":
|
|
16223
16243
|
lines.push(`${indent}**${entry.title}**`);
|
|
16224
16244
|
lines.push(`${indent}`);
|
|
16225
|
-
for (const
|
|
16226
|
-
lines.push(`${indent}${
|
|
16245
|
+
for (const line2 of (entry.markdown ?? "").split("\n")) {
|
|
16246
|
+
lines.push(`${indent}${line2}`);
|
|
16227
16247
|
}
|
|
16228
16248
|
lines.push(`${indent}`);
|
|
16229
16249
|
break;
|
|
@@ -16232,8 +16252,8 @@ var MarkdownFormatter = class {
|
|
|
16232
16252
|
lines.push(`${indent}**${entry.title}**`);
|
|
16233
16253
|
}
|
|
16234
16254
|
lines.push(`${indent}\`\`\`mermaid`);
|
|
16235
|
-
for (const
|
|
16236
|
-
lines.push(`${indent}${
|
|
16255
|
+
for (const line2 of (entry.code ?? "").split("\n")) {
|
|
16256
|
+
lines.push(`${indent}${line2}`);
|
|
16237
16257
|
}
|
|
16238
16258
|
lines.push(`${indent}\`\`\``);
|
|
16239
16259
|
break;
|
|
@@ -16263,8 +16283,8 @@ var MarkdownFormatter = class {
|
|
|
16263
16283
|
lines.push(`${indent}<summary>${htmlLabel}</summary>`);
|
|
16264
16284
|
lines.push(`${indent}`);
|
|
16265
16285
|
lines.push(`${indent}\`\`\`html`);
|
|
16266
|
-
for (const
|
|
16267
|
-
lines.push(`${indent}${
|
|
16286
|
+
for (const line2 of (entry.content ?? "").split("\n")) {
|
|
16287
|
+
lines.push(`${indent}${line2}`);
|
|
16268
16288
|
}
|
|
16269
16289
|
lines.push(`${indent}\`\`\``);
|
|
16270
16290
|
lines.push(`${indent}`);
|
|
@@ -16286,8 +16306,8 @@ var MarkdownFormatter = class {
|
|
|
16286
16306
|
lines.push(`${indent}**[${entry.type}]**`);
|
|
16287
16307
|
lines.push(`${indent}`);
|
|
16288
16308
|
lines.push(`${indent}\`\`\`json`);
|
|
16289
|
-
for (const
|
|
16290
|
-
lines.push(`${indent}${
|
|
16309
|
+
for (const line2 of JSON.stringify(entry.data ?? null, null, 2).split("\n")) {
|
|
16310
|
+
lines.push(`${indent}${line2}`);
|
|
16291
16311
|
}
|
|
16292
16312
|
lines.push(`${indent}\`\`\``);
|
|
16293
16313
|
lines.push(`${indent}`);
|
|
@@ -16446,6 +16466,132 @@ function escapePipe(value) {
|
|
|
16446
16466
|
return value.replace(/\|/g, "\\|");
|
|
16447
16467
|
}
|
|
16448
16468
|
|
|
16469
|
+
// src/formatters/traceability-matrix.ts
|
|
16470
|
+
var TraceabilityMatrixFormatter = class {
|
|
16471
|
+
format(run) {
|
|
16472
|
+
const matrix = toTraceabilityMatrix(run);
|
|
16473
|
+
const lines = [];
|
|
16474
|
+
lines.push("# Traceability Matrix");
|
|
16475
|
+
lines.push("");
|
|
16476
|
+
lines.push(`Generated: ${matrix.generatedAt}`);
|
|
16477
|
+
lines.push(`Run: ${matrix.run.startedAt} to ${matrix.run.finishedAt}`);
|
|
16478
|
+
if (matrix.run.branch) lines.push(`Branch: ${matrix.run.branch}`);
|
|
16479
|
+
if (matrix.run.gitSha) lines.push(`Commit: ${matrix.run.gitSha}`);
|
|
16480
|
+
lines.push("");
|
|
16481
|
+
lines.push("| Requirements | Verified | Failing | Scenarios | Untraced |");
|
|
16482
|
+
lines.push("| ---: | ---: | ---: | ---: | ---: |");
|
|
16483
|
+
lines.push(
|
|
16484
|
+
`| ${matrix.summary.requirements} | ${matrix.summary.requirementsVerified} | ${matrix.summary.requirementsFailing} | ${matrix.summary.scenarios} | ${matrix.summary.untracedScenarios} |`
|
|
16485
|
+
);
|
|
16486
|
+
lines.push("");
|
|
16487
|
+
for (const req of matrix.requirements) {
|
|
16488
|
+
const heading2 = req.url ? `[${req.ticket}](${req.url})` : req.ticket;
|
|
16489
|
+
lines.push(`## ${heading2}`);
|
|
16490
|
+
lines.push("");
|
|
16491
|
+
lines.push(`Status: ${renderRequirementStatus(req.status)}`);
|
|
16492
|
+
if (req.covers.length > 0) {
|
|
16493
|
+
lines.push(`Covers: ${req.covers.map((path16) => `\`${path16}\``).join(", ")}`);
|
|
16494
|
+
}
|
|
16495
|
+
lines.push("");
|
|
16496
|
+
lines.push("| Status | Scenario | Source | Covers |");
|
|
16497
|
+
lines.push("| --- | --- | --- | --- |");
|
|
16498
|
+
for (const scenario of req.scenarios) {
|
|
16499
|
+
const source = `${scenario.sourceFile}:${scenario.sourceLine}`;
|
|
16500
|
+
const covers = scenario.covers.length > 0 ? scenario.covers.map((path16) => `\`${path16}\``).join(", ") : "";
|
|
16501
|
+
lines.push(`| ${scenario.status} | ${escapePipe2(scenario.title)} | \`${source}\` | ${covers} |`);
|
|
16502
|
+
}
|
|
16503
|
+
lines.push("");
|
|
16504
|
+
}
|
|
16505
|
+
if (matrix.untraced.length > 0) {
|
|
16506
|
+
lines.push("## Untraced scenarios");
|
|
16507
|
+
lines.push("");
|
|
16508
|
+
lines.push("Behavior with no requirement link. Add a `ticket` to each so it appears against a requirement.");
|
|
16509
|
+
lines.push("");
|
|
16510
|
+
lines.push("| Status | Scenario | Source |");
|
|
16511
|
+
lines.push("| --- | --- | --- |");
|
|
16512
|
+
for (const scenario of matrix.untraced) {
|
|
16513
|
+
const source = `${scenario.sourceFile}:${scenario.sourceLine}`;
|
|
16514
|
+
lines.push(`| ${scenario.status} | ${escapePipe2(scenario.title)} | \`${source}\` |`);
|
|
16515
|
+
}
|
|
16516
|
+
lines.push("");
|
|
16517
|
+
}
|
|
16518
|
+
return lines.join("\n").trimEnd();
|
|
16519
|
+
}
|
|
16520
|
+
};
|
|
16521
|
+
function toTraceabilityMatrix(run) {
|
|
16522
|
+
const sorted = [...run.testCases].sort((a, b) => a.id.localeCompare(b.id));
|
|
16523
|
+
const byTicket = /* @__PURE__ */ new Map();
|
|
16524
|
+
const untraced = [];
|
|
16525
|
+
for (const tc of sorted) {
|
|
16526
|
+
const tickets = tc.story.tickets ?? [];
|
|
16527
|
+
if (tickets.length === 0) {
|
|
16528
|
+
untraced.push({
|
|
16529
|
+
id: tc.id,
|
|
16530
|
+
title: tc.story.scenario,
|
|
16531
|
+
status: tc.status,
|
|
16532
|
+
sourceFile: tc.sourceFile,
|
|
16533
|
+
sourceLine: tc.sourceLine
|
|
16534
|
+
});
|
|
16535
|
+
continue;
|
|
16536
|
+
}
|
|
16537
|
+
for (const ticket of tickets) {
|
|
16538
|
+
const entry = byTicket.get(ticket.id) ?? { url: ticket.url, cases: [] };
|
|
16539
|
+
if (!entry.url && ticket.url) entry.url = ticket.url;
|
|
16540
|
+
entry.cases.push(tc);
|
|
16541
|
+
byTicket.set(ticket.id, entry);
|
|
16542
|
+
}
|
|
16543
|
+
}
|
|
16544
|
+
const requirements = [...byTicket.entries()].sort(([a], [b]) => a.localeCompare(b)).map(([ticket, entry]) => {
|
|
16545
|
+
const scenarios = entry.cases.map((tc) => ({
|
|
16546
|
+
id: tc.id,
|
|
16547
|
+
title: tc.story.scenario,
|
|
16548
|
+
status: tc.status,
|
|
16549
|
+
sourceFile: tc.sourceFile,
|
|
16550
|
+
sourceLine: tc.sourceLine,
|
|
16551
|
+
covers: tc.story.covers ?? []
|
|
16552
|
+
}));
|
|
16553
|
+
const covers = [...new Set(scenarios.flatMap((s) => s.covers))].sort();
|
|
16554
|
+
return { ticket, url: entry.url, status: requirementStatus(entry.cases), scenarios, covers };
|
|
16555
|
+
});
|
|
16556
|
+
return {
|
|
16557
|
+
schemaVersion: "1.0",
|
|
16558
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
16559
|
+
run: {
|
|
16560
|
+
startedAt: new Date(run.startedAtMs).toISOString(),
|
|
16561
|
+
finishedAt: new Date(run.finishedAtMs).toISOString(),
|
|
16562
|
+
gitSha: run.gitSha,
|
|
16563
|
+
branch: run.ci?.branch
|
|
16564
|
+
},
|
|
16565
|
+
summary: {
|
|
16566
|
+
requirements: requirements.length,
|
|
16567
|
+
requirementsVerified: requirements.filter((r) => r.status === "verified").length,
|
|
16568
|
+
requirementsFailing: requirements.filter((r) => r.status === "failing").length,
|
|
16569
|
+
scenarios: run.testCases.length,
|
|
16570
|
+
untracedScenarios: untraced.length
|
|
16571
|
+
},
|
|
16572
|
+
requirements,
|
|
16573
|
+
untraced
|
|
16574
|
+
};
|
|
16575
|
+
}
|
|
16576
|
+
function requirementStatus(cases) {
|
|
16577
|
+
if (cases.some((tc) => tc.status === "failed")) return "failing";
|
|
16578
|
+
if (cases.some((tc) => tc.status === "passed")) return "verified";
|
|
16579
|
+
return "incomplete";
|
|
16580
|
+
}
|
|
16581
|
+
function renderRequirementStatus(status) {
|
|
16582
|
+
switch (status) {
|
|
16583
|
+
case "verified":
|
|
16584
|
+
return "verified (all scenarios passed)";
|
|
16585
|
+
case "failing":
|
|
16586
|
+
return "failing (a scenario failed)";
|
|
16587
|
+
default:
|
|
16588
|
+
return "incomplete (no scenario passed yet)";
|
|
16589
|
+
}
|
|
16590
|
+
}
|
|
16591
|
+
function escapePipe2(value) {
|
|
16592
|
+
return value.replace(/\|/g, "\\|");
|
|
16593
|
+
}
|
|
16594
|
+
|
|
16449
16595
|
// src/formatters/cucumber-messages/synthesize-feature.ts
|
|
16450
16596
|
function extractFeatureName(testCases, uri) {
|
|
16451
16597
|
for (const tc of testCases) {
|
|
@@ -16672,60 +16818,60 @@ function buildStepArguments(step, stepLine) {
|
|
|
16672
16818
|
}
|
|
16673
16819
|
return {};
|
|
16674
16820
|
}
|
|
16675
|
-
function docEntryToDocString(doc,
|
|
16821
|
+
function docEntryToDocString(doc, line2) {
|
|
16676
16822
|
switch (doc.kind) {
|
|
16677
16823
|
case "code":
|
|
16678
16824
|
return {
|
|
16679
|
-
location: { line },
|
|
16825
|
+
location: { line: line2 },
|
|
16680
16826
|
mediaType: doc.lang,
|
|
16681
16827
|
content: doc.content,
|
|
16682
16828
|
delimiter: '"""'
|
|
16683
16829
|
};
|
|
16684
16830
|
case "note":
|
|
16685
16831
|
return {
|
|
16686
|
-
location: { line },
|
|
16832
|
+
location: { line: line2 },
|
|
16687
16833
|
mediaType: "text/plain",
|
|
16688
16834
|
content: doc.text,
|
|
16689
16835
|
delimiter: '"""'
|
|
16690
16836
|
};
|
|
16691
16837
|
case "section":
|
|
16692
16838
|
return {
|
|
16693
|
-
location: { line },
|
|
16839
|
+
location: { line: line2 },
|
|
16694
16840
|
mediaType: "text/markdown",
|
|
16695
16841
|
content: doc.markdown,
|
|
16696
16842
|
delimiter: '"""'
|
|
16697
16843
|
};
|
|
16698
16844
|
case "mermaid":
|
|
16699
16845
|
return {
|
|
16700
|
-
location: { line },
|
|
16846
|
+
location: { line: line2 },
|
|
16701
16847
|
mediaType: "text/x-mermaid",
|
|
16702
16848
|
content: doc.code,
|
|
16703
16849
|
delimiter: '"""'
|
|
16704
16850
|
};
|
|
16705
16851
|
case "kv":
|
|
16706
16852
|
return {
|
|
16707
|
-
location: { line },
|
|
16853
|
+
location: { line: line2 },
|
|
16708
16854
|
mediaType: "text/plain",
|
|
16709
16855
|
content: `${doc.label}: ${typeof doc.value === "string" ? doc.value : JSON.stringify(doc.value)}`,
|
|
16710
16856
|
delimiter: '"""'
|
|
16711
16857
|
};
|
|
16712
16858
|
case "link":
|
|
16713
16859
|
return {
|
|
16714
|
-
location: { line },
|
|
16860
|
+
location: { line: line2 },
|
|
16715
16861
|
mediaType: "text/markdown",
|
|
16716
16862
|
content: `[${doc.label}](${doc.url})`,
|
|
16717
16863
|
delimiter: '"""'
|
|
16718
16864
|
};
|
|
16719
16865
|
case "custom":
|
|
16720
16866
|
return {
|
|
16721
|
-
location: { line },
|
|
16867
|
+
location: { line: line2 },
|
|
16722
16868
|
mediaType: "application/json",
|
|
16723
16869
|
content: JSON.stringify(doc.data, null, 2),
|
|
16724
16870
|
delimiter: '"""'
|
|
16725
16871
|
};
|
|
16726
16872
|
case "tag":
|
|
16727
16873
|
return {
|
|
16728
|
-
location: { line },
|
|
16874
|
+
location: { line: line2 },
|
|
16729
16875
|
mediaType: "text/plain",
|
|
16730
16876
|
content: doc.names.map((n) => `@${n}`).join(" "),
|
|
16731
16877
|
delimiter: '"""'
|
|
@@ -16735,18 +16881,18 @@ function docEntryToDocString(doc, line) {
|
|
|
16735
16881
|
return void 0;
|
|
16736
16882
|
}
|
|
16737
16883
|
}
|
|
16738
|
-
function buildDataTable(table2,
|
|
16884
|
+
function buildDataTable(table2, line2) {
|
|
16739
16885
|
const rows = [];
|
|
16740
16886
|
rows.push({
|
|
16741
|
-
location: { line },
|
|
16887
|
+
location: { line: line2 },
|
|
16742
16888
|
cells: table2.columns.map((col) => ({
|
|
16743
|
-
location: { line },
|
|
16889
|
+
location: { line: line2 },
|
|
16744
16890
|
value: col
|
|
16745
16891
|
})),
|
|
16746
16892
|
id: ""
|
|
16747
16893
|
});
|
|
16748
16894
|
for (let r = 0; r < table2.rows.length; r++) {
|
|
16749
|
-
const rowLine =
|
|
16895
|
+
const rowLine = line2 + 1 + r;
|
|
16750
16896
|
rows.push({
|
|
16751
16897
|
location: { line: rowLine },
|
|
16752
16898
|
cells: table2.rows[r].map((cell) => ({
|
|
@@ -16757,7 +16903,7 @@ function buildDataTable(table2, line) {
|
|
|
16757
16903
|
});
|
|
16758
16904
|
}
|
|
16759
16905
|
return {
|
|
16760
|
-
location: { line },
|
|
16906
|
+
location: { line: line2 },
|
|
16761
16907
|
rows
|
|
16762
16908
|
};
|
|
16763
16909
|
}
|
|
@@ -18309,17 +18455,17 @@ ${body}`;
|
|
|
18309
18455
|
return humanizeSourceFile([...sourceFiles][0]) || this.title;
|
|
18310
18456
|
}
|
|
18311
18457
|
buildFrontmatter(run) {
|
|
18312
|
-
const
|
|
18458
|
+
const badge2 = _AstroFormatter.computeBadge(run.testCases);
|
|
18313
18459
|
const count2 = run.testCases.length;
|
|
18314
|
-
const description = `${count2} scenario${count2 !== 1 ? "s" : ""} \u2014 ${
|
|
18460
|
+
const description = `${count2} scenario${count2 !== 1 ? "s" : ""} \u2014 ${badge2.text.toLowerCase()}`;
|
|
18315
18461
|
const lines = [
|
|
18316
18462
|
"---",
|
|
18317
18463
|
`title: ${yamlScalar(this.deriveTitle(run))}`,
|
|
18318
18464
|
`description: ${description}`,
|
|
18319
18465
|
"sidebar:",
|
|
18320
18466
|
" badge:",
|
|
18321
|
-
` text: ${
|
|
18322
|
-
` variant: ${
|
|
18467
|
+
` text: ${badge2.text}`,
|
|
18468
|
+
` variant: ${badge2.variant}`,
|
|
18323
18469
|
"---"
|
|
18324
18470
|
];
|
|
18325
18471
|
return lines.join("\n");
|
|
@@ -18775,12 +18921,16 @@ function groupBy7(items, keyFn) {
|
|
|
18775
18921
|
import * as fs5 from "fs";
|
|
18776
18922
|
import * as path6 from "path";
|
|
18777
18923
|
var SKIP_PREFIXES = ["http://", "https://", "data:", "#"];
|
|
18778
|
-
function
|
|
18924
|
+
function isRemoteRef(src) {
|
|
18779
18925
|
const trimmed = src.trim();
|
|
18780
|
-
|
|
18781
|
-
|
|
18782
|
-
|
|
18783
|
-
|
|
18926
|
+
return SKIP_PREFIXES.some((prefix) => trimmed.startsWith(prefix));
|
|
18927
|
+
}
|
|
18928
|
+
function isAbsoluteRef(src) {
|
|
18929
|
+
const trimmed = src.trim();
|
|
18930
|
+
return path6.posix.isAbsolute(trimmed) || path6.win32.isAbsolute(trimmed);
|
|
18931
|
+
}
|
|
18932
|
+
function isRelativeLocalPath(src) {
|
|
18933
|
+
return !isRemoteRef(src) && !isAbsoluteRef(src);
|
|
18784
18934
|
}
|
|
18785
18935
|
function stripCodeContent(markdown) {
|
|
18786
18936
|
let result = markdown.replace(/^[ \t]*(`{3,}|~{3,})[^\n]*\n[\s\S]*?^[ \t]*\1\s*$/gm, "");
|
|
@@ -18796,21 +18946,21 @@ function scanMarkdownAssets(markdown) {
|
|
|
18796
18946
|
let match;
|
|
18797
18947
|
while ((match = mdImageRe.exec(stripped)) !== null) {
|
|
18798
18948
|
const src = match[1].trim();
|
|
18799
|
-
if (
|
|
18949
|
+
if (!isRemoteRef(src)) {
|
|
18800
18950
|
found.add(src);
|
|
18801
18951
|
}
|
|
18802
18952
|
}
|
|
18803
18953
|
const htmlSrcRe = /<(?:img|source|video)[^>]+\bsrc=["']([^"']+)["'][^>]*>/gi;
|
|
18804
18954
|
while ((match = htmlSrcRe.exec(stripped)) !== null) {
|
|
18805
18955
|
const src = match[1].trim();
|
|
18806
|
-
if (
|
|
18956
|
+
if (!isRemoteRef(src)) {
|
|
18807
18957
|
found.add(src);
|
|
18808
18958
|
}
|
|
18809
18959
|
}
|
|
18810
18960
|
const posterRe = /<video[^>]+\bposter=["']([^"']+)["'][^>]*>/gi;
|
|
18811
18961
|
while ((match = posterRe.exec(stripped)) !== null) {
|
|
18812
18962
|
const src = match[1].trim();
|
|
18813
|
-
if (
|
|
18963
|
+
if (!isRemoteRef(src)) {
|
|
18814
18964
|
found.add(src);
|
|
18815
18965
|
}
|
|
18816
18966
|
}
|
|
@@ -18836,48 +18986,21 @@ function isCode(segment) {
|
|
|
18836
18986
|
const trimmed = segment.trimStart();
|
|
18837
18987
|
return trimmed.startsWith("`") || trimmed.startsWith("~") || trimmed.startsWith("<pre") || trimmed.startsWith("<code");
|
|
18838
18988
|
}
|
|
18989
|
+
function resolveRewrite(trimmed, assetsBaseUrl, pathMap) {
|
|
18990
|
+
if (isRemoteRef(trimmed)) return null;
|
|
18991
|
+
if (pathMap) {
|
|
18992
|
+
const mapped = pathMap.get(trimmed);
|
|
18993
|
+
return mapped === void 0 ? null : `${assetsBaseUrl}/${mapped}`;
|
|
18994
|
+
}
|
|
18995
|
+
if (!isRelativeLocalPath(trimmed)) return null;
|
|
18996
|
+
return `${assetsBaseUrl}/${trimmed}`;
|
|
18997
|
+
}
|
|
18839
18998
|
function rewriteProseSegment(prose, assetsBaseUrl, pathMap) {
|
|
18840
|
-
|
|
18841
|
-
|
|
18842
|
-
|
|
18843
|
-
|
|
18844
|
-
|
|
18845
|
-
if (!isLocalPath(trimmed)) return full;
|
|
18846
|
-
if (pathMap) {
|
|
18847
|
-
const mapped = pathMap.get(trimmed);
|
|
18848
|
-
if (mapped === void 0) return full;
|
|
18849
|
-
return `${pre}${assetsBaseUrl}/${mapped}${post}`;
|
|
18850
|
-
}
|
|
18851
|
-
return `${pre}${assetsBaseUrl}/${trimmed}${post}`;
|
|
18852
|
-
}
|
|
18853
|
-
);
|
|
18854
|
-
result = result.replace(
|
|
18855
|
-
/(<(?:img|source|video)[^>]+\bsrc=["'])([^"']+)(["'][^>]*>)/gi,
|
|
18856
|
-
(full, pre, src, post) => {
|
|
18857
|
-
const trimmed = src.trim();
|
|
18858
|
-
if (!isLocalPath(trimmed)) return full;
|
|
18859
|
-
if (pathMap) {
|
|
18860
|
-
const mapped = pathMap.get(trimmed);
|
|
18861
|
-
if (mapped === void 0) return full;
|
|
18862
|
-
return `${pre}${assetsBaseUrl}/${mapped}${post}`;
|
|
18863
|
-
}
|
|
18864
|
-
return `${pre}${assetsBaseUrl}/${trimmed}${post}`;
|
|
18865
|
-
}
|
|
18866
|
-
);
|
|
18867
|
-
result = result.replace(
|
|
18868
|
-
/(<video[^>]+\bposter=["'])([^"']+)(["'][^>]*>)/gi,
|
|
18869
|
-
(full, pre, src, post) => {
|
|
18870
|
-
const trimmed = src.trim();
|
|
18871
|
-
if (!isLocalPath(trimmed)) return full;
|
|
18872
|
-
if (pathMap) {
|
|
18873
|
-
const mapped = pathMap.get(trimmed);
|
|
18874
|
-
if (mapped === void 0) return full;
|
|
18875
|
-
return `${pre}${assetsBaseUrl}/${mapped}${post}`;
|
|
18876
|
-
}
|
|
18877
|
-
return `${pre}${assetsBaseUrl}/${trimmed}${post}`;
|
|
18878
|
-
}
|
|
18879
|
-
);
|
|
18880
|
-
return result;
|
|
18999
|
+
const rewrite = (full, pre, src, post) => {
|
|
19000
|
+
const target = resolveRewrite(src.trim(), assetsBaseUrl, pathMap);
|
|
19001
|
+
return target === null ? full : `${pre}${target}${post}`;
|
|
19002
|
+
};
|
|
19003
|
+
return prose.replace(/(!\[[^\]]*\]\()([^)"'\s]+)((?:\s+["'][^"']*["'])?\s*\))/g, rewrite).replace(/(<(?:img|source|video)[^>]+\bsrc=["'])([^"']+)(["'][^>]*>)/gi, rewrite).replace(/(<video[^>]+\bposter=["'])([^"']+)(["'][^>]*>)/gi, rewrite);
|
|
18881
19004
|
}
|
|
18882
19005
|
function rewriteAssetPaths(markdown, assetsBaseUrl, pathMap) {
|
|
18883
19006
|
return splitByCode(markdown).map((seg) => isCode(seg) ? seg : rewriteProseSegment(seg, assetsBaseUrl, pathMap)).join("");
|
|
@@ -18894,8 +19017,9 @@ function copyMarkdownAssets(options) {
|
|
|
18894
19017
|
const pathMap = /* @__PURE__ */ new Map();
|
|
18895
19018
|
const missing = [];
|
|
18896
19019
|
for (const ref of refs) {
|
|
18897
|
-
const absPath = path6.resolve(markdownDir, ref);
|
|
19020
|
+
const absPath = isAbsoluteRef(ref) ? ref : path6.resolve(markdownDir, ref);
|
|
18898
19021
|
if (!fs5.existsSync(absPath)) {
|
|
19022
|
+
if (isAbsoluteRef(ref)) continue;
|
|
18899
19023
|
if (!allowMissing) {
|
|
18900
19024
|
throw new Error(`Asset not found: ${absPath}`);
|
|
18901
19025
|
}
|
|
@@ -18977,6 +19101,47 @@ function startWatch(options, deps = {}) {
|
|
|
18977
19101
|
};
|
|
18978
19102
|
}
|
|
18979
19103
|
|
|
19104
|
+
// src/behavior-diff.ts
|
|
19105
|
+
function classifyStatusChange(baseline, current) {
|
|
19106
|
+
if (baseline === void 0) return "added";
|
|
19107
|
+
if (current === void 0) return "removed";
|
|
19108
|
+
if (baseline === current) return "unchanged";
|
|
19109
|
+
if (baseline === "passed" && current === "failed") return "regressed";
|
|
19110
|
+
if (baseline === "failed" && current === "passed") return "fixed";
|
|
19111
|
+
return "changed";
|
|
19112
|
+
}
|
|
19113
|
+
function scenarioMap(report) {
|
|
19114
|
+
const map = /* @__PURE__ */ new Map();
|
|
19115
|
+
for (const feature of report.features) {
|
|
19116
|
+
for (const scenario of feature.scenarios) {
|
|
19117
|
+
map.set(scenario.id, { scenario, sourceFile: feature.sourceFile });
|
|
19118
|
+
}
|
|
19119
|
+
}
|
|
19120
|
+
return map;
|
|
19121
|
+
}
|
|
19122
|
+
function diffStoryReports(baseline, current) {
|
|
19123
|
+
const base = scenarioMap(baseline);
|
|
19124
|
+
const curr = scenarioMap(current);
|
|
19125
|
+
const ids = [.../* @__PURE__ */ new Set([...base.keys(), ...curr.keys()])];
|
|
19126
|
+
const scenarios = ids.map((id) => {
|
|
19127
|
+
const b = base.get(id);
|
|
19128
|
+
const c = curr.get(id);
|
|
19129
|
+
const kind = classifyStatusChange(b?.scenario.status, c?.scenario.status);
|
|
19130
|
+
const meta = c ?? b;
|
|
19131
|
+
return {
|
|
19132
|
+
id,
|
|
19133
|
+
title: meta.scenario.title,
|
|
19134
|
+
sourceFile: meta.sourceFile,
|
|
19135
|
+
kind,
|
|
19136
|
+
baselineStatus: b?.scenario.status,
|
|
19137
|
+
currentStatus: c?.scenario.status
|
|
19138
|
+
};
|
|
19139
|
+
});
|
|
19140
|
+
const summary = { added: 0, removed: 0, regressed: 0, fixed: 0, changed: 0, unchanged: 0 };
|
|
19141
|
+
for (const s of scenarios) summary[s.kind] += 1;
|
|
19142
|
+
return { schemaVersion: "1.0", summary, scenarios };
|
|
19143
|
+
}
|
|
19144
|
+
|
|
18980
19145
|
// src/publishers/confluence.ts
|
|
18981
19146
|
function parseAdf(adf) {
|
|
18982
19147
|
let parsed;
|
|
@@ -19209,7 +19374,7 @@ async function updateDescription(issueKey, base, adf, headers, fetchFn) {
|
|
|
19209
19374
|
// src/converters/ndjson-parser.ts
|
|
19210
19375
|
function parseNdjson(ndjson) {
|
|
19211
19376
|
const lines = ndjson.trim().split("\n").filter(Boolean);
|
|
19212
|
-
const envelopes = lines.map((
|
|
19377
|
+
const envelopes = lines.map((line2) => JSON.parse(line2));
|
|
19213
19378
|
return parseEnvelopes(envelopes);
|
|
19214
19379
|
}
|
|
19215
19380
|
function parseEnvelopes(envelopes) {
|
|
@@ -20262,6 +20427,280 @@ function collectDocKinds(testCase) {
|
|
|
20262
20427
|
return [...kinds].sort();
|
|
20263
20428
|
}
|
|
20264
20429
|
|
|
20430
|
+
// src/scenario-failure.ts
|
|
20431
|
+
function failingScenarioMessage(tc) {
|
|
20432
|
+
const failingStep = tc.stepResults.find((s) => s.status === "failed" && s.errorMessage);
|
|
20433
|
+
return failingStep?.errorMessage ?? tc.errorMessage;
|
|
20434
|
+
}
|
|
20435
|
+
|
|
20436
|
+
// src/check.ts
|
|
20437
|
+
var ICON_PASS = "\u2713";
|
|
20438
|
+
var ICON_FAIL = "\u2717";
|
|
20439
|
+
var ICON_SKIP = "\u2298";
|
|
20440
|
+
var ICON_PENDING = "\u23F3";
|
|
20441
|
+
var ICON_WARN = "\u26A0";
|
|
20442
|
+
function buildCheck(args, _deps = {}) {
|
|
20443
|
+
const { testCases, baseline } = args;
|
|
20444
|
+
const summary = {
|
|
20445
|
+
total: testCases.length,
|
|
20446
|
+
passed: testCases.filter((tc) => tc.status === "passed").length,
|
|
20447
|
+
failed: testCases.filter((tc) => tc.status === "failed").length,
|
|
20448
|
+
skipped: testCases.filter((tc) => tc.status === "skipped").length,
|
|
20449
|
+
pending: testCases.filter((tc) => tc.status === "pending").length
|
|
20450
|
+
};
|
|
20451
|
+
let regressed = 0;
|
|
20452
|
+
let fixed = 0;
|
|
20453
|
+
if (baseline) {
|
|
20454
|
+
for (const tc of testCases) {
|
|
20455
|
+
const before = baseline.get(tc.id);
|
|
20456
|
+
if (before === "passed" && tc.status === "failed") regressed += 1;
|
|
20457
|
+
if (before === "failed" && tc.status === "passed") fixed += 1;
|
|
20458
|
+
}
|
|
20459
|
+
}
|
|
20460
|
+
const failures = testCases.filter((tc) => tc.status === "failed").map((tc) => toFailure(tc, baseline)).sort((a, b) => {
|
|
20461
|
+
if (a.regressed !== b.regressed) return a.regressed ? -1 : 1;
|
|
20462
|
+
return a.location.localeCompare(b.location);
|
|
20463
|
+
});
|
|
20464
|
+
return {
|
|
20465
|
+
summary,
|
|
20466
|
+
failures,
|
|
20467
|
+
regressed,
|
|
20468
|
+
fixed,
|
|
20469
|
+
comparedToBaseline: baseline !== void 0
|
|
20470
|
+
};
|
|
20471
|
+
}
|
|
20472
|
+
function toFailure(tc, baseline) {
|
|
20473
|
+
const failedIndexes = new Set(
|
|
20474
|
+
tc.stepResults.filter((s) => s.status === "failed").map((s) => s.index)
|
|
20475
|
+
);
|
|
20476
|
+
const steps = tc.story.steps.map((step, index) => ({
|
|
20477
|
+
keyword: step.keyword,
|
|
20478
|
+
text: step.text,
|
|
20479
|
+
failed: failedIndexes.has(index)
|
|
20480
|
+
}));
|
|
20481
|
+
return {
|
|
20482
|
+
id: tc.id,
|
|
20483
|
+
scenario: tc.story.scenario,
|
|
20484
|
+
location: `${tc.sourceFile}:${tc.sourceLine}`,
|
|
20485
|
+
steps,
|
|
20486
|
+
errorMessage: failingScenarioMessage(tc),
|
|
20487
|
+
covers: tc.story.covers ?? [],
|
|
20488
|
+
tickets: (tc.story.tickets ?? []).map((t) => t.id),
|
|
20489
|
+
regressed: baseline?.get(tc.id) === "passed"
|
|
20490
|
+
};
|
|
20491
|
+
}
|
|
20492
|
+
function renderCheck(report, format) {
|
|
20493
|
+
return format === "json" ? JSON.stringify(report, null, 2) : renderCheckText(report);
|
|
20494
|
+
}
|
|
20495
|
+
function renderCheckText(report) {
|
|
20496
|
+
const { summary, failures } = report;
|
|
20497
|
+
const headlineParts = [`${ICON_PASS} ${summary.passed} passed`];
|
|
20498
|
+
if (summary.failed > 0) headlineParts.push(`${ICON_FAIL} ${summary.failed} failed`);
|
|
20499
|
+
if (summary.skipped > 0) headlineParts.push(`${ICON_SKIP} ${summary.skipped} skipped`);
|
|
20500
|
+
if (summary.pending > 0) headlineParts.push(`${ICON_PENDING} ${summary.pending} pending`);
|
|
20501
|
+
const headline = `${headlineParts.join(" ")} (${summary.total} scenarios)`;
|
|
20502
|
+
if (failures.length === 0) {
|
|
20503
|
+
const lines2 = [headline];
|
|
20504
|
+
if (report.comparedToBaseline && report.fixed > 0) {
|
|
20505
|
+
lines2.push(`${ICON_PASS} ${report.fixed} fixed since baseline.`);
|
|
20506
|
+
}
|
|
20507
|
+
lines2.push("All scenarios green.");
|
|
20508
|
+
return lines2.join("\n");
|
|
20509
|
+
}
|
|
20510
|
+
const lines = [headline, ""];
|
|
20511
|
+
for (const f of failures) {
|
|
20512
|
+
lines.push(`${ICON_FAIL} ${f.scenario}${f.regressed ? " (regressed)" : ""}`);
|
|
20513
|
+
lines.push(` ${f.location}`);
|
|
20514
|
+
for (const step of f.steps) {
|
|
20515
|
+
const marker = step.failed ? ` ${ICON_FAIL} ` : " ";
|
|
20516
|
+
lines.push(`${marker}${step.keyword} ${step.text}`);
|
|
20517
|
+
}
|
|
20518
|
+
if (f.errorMessage) {
|
|
20519
|
+
const firstLine = f.errorMessage.split("\n")[0];
|
|
20520
|
+
lines.push(` \u2192 ${firstLine}`);
|
|
20521
|
+
}
|
|
20522
|
+
if (f.covers.length > 0) {
|
|
20523
|
+
lines.push(` covers: ${f.covers.join(", ")}`);
|
|
20524
|
+
}
|
|
20525
|
+
if (f.tickets.length > 0) {
|
|
20526
|
+
lines.push(` ticket: ${f.tickets.join(", ")}`);
|
|
20527
|
+
}
|
|
20528
|
+
lines.push("");
|
|
20529
|
+
}
|
|
20530
|
+
if (report.comparedToBaseline) {
|
|
20531
|
+
if (report.regressed > 0) {
|
|
20532
|
+
lines.push(`${ICON_WARN} ${report.regressed} regressed since baseline (was passing).`);
|
|
20533
|
+
}
|
|
20534
|
+
if (report.fixed > 0) {
|
|
20535
|
+
lines.push(`${ICON_PASS} ${report.fixed} fixed since baseline.`);
|
|
20536
|
+
}
|
|
20537
|
+
if (report.regressed === 0 && report.fixed === 0) {
|
|
20538
|
+
lines.push("No status changes vs. baseline.");
|
|
20539
|
+
}
|
|
20540
|
+
}
|
|
20541
|
+
return lines.join("\n").trimEnd();
|
|
20542
|
+
}
|
|
20543
|
+
|
|
20544
|
+
// src/goal.ts
|
|
20545
|
+
var ACTIVE = ["passed", "failed"];
|
|
20546
|
+
function buildGoal(args, _deps = {}) {
|
|
20547
|
+
const { run, baseline } = args;
|
|
20548
|
+
const cases = run.testCases;
|
|
20549
|
+
const selectors = [
|
|
20550
|
+
...args.requireTags.map((tag) => ({ label: `tag:${tag}`, match: (tc) => tc.tags.includes(tag) })),
|
|
20551
|
+
...args.requireTickets.map((id) => ({ label: `ticket:${id}`, match: (tc) => (tc.story.tickets ?? []).some((t) => t.id === id) })),
|
|
20552
|
+
...args.requireScenarios.map((sel) => ({ label: `scenario:${sel}`, match: (tc) => tc.id === sel || tc.story.scenario === sel }))
|
|
20553
|
+
];
|
|
20554
|
+
const requirements = selectors.length === 0 ? [evaluate("all scenarios", cases)] : selectors.map((s) => evaluate(s.label, cases.filter(s.match)));
|
|
20555
|
+
const regressions = [];
|
|
20556
|
+
if (baseline && args.enforceNoRegressions) {
|
|
20557
|
+
const before = statusMap(baseline);
|
|
20558
|
+
for (const tc of cases) {
|
|
20559
|
+
if (before.get(tc.id) === "passed" && tc.status === "failed") {
|
|
20560
|
+
regressions.push({ id: tc.id, title: tc.story.scenario });
|
|
20561
|
+
}
|
|
20562
|
+
}
|
|
20563
|
+
}
|
|
20564
|
+
const violations = [];
|
|
20565
|
+
if (baseline && args.enforceRatchet) {
|
|
20566
|
+
const current = new Map(cases.map((tc) => [tc.id, tc]));
|
|
20567
|
+
for (const base of baseline.testCases) {
|
|
20568
|
+
const now = current.get(base.id);
|
|
20569
|
+
if (!now) {
|
|
20570
|
+
violations.push({ id: base.id, title: base.story.scenario, kind: "removed", detail: "scenario no longer present" });
|
|
20571
|
+
continue;
|
|
20572
|
+
}
|
|
20573
|
+
if (ACTIVE.includes(base.status) && (now.status === "skipped" || now.status === "pending")) {
|
|
20574
|
+
violations.push({ id: base.id, title: base.story.scenario, kind: "disabled", detail: `${base.status} -> ${now.status}` });
|
|
20575
|
+
}
|
|
20576
|
+
const baseSteps = base.story.steps.length;
|
|
20577
|
+
const nowSteps = now.story.steps.length;
|
|
20578
|
+
if (nowSteps < baseSteps) {
|
|
20579
|
+
violations.push({ id: base.id, title: base.story.scenario, kind: "weakened", detail: `${baseSteps} steps -> ${nowSteps} steps` });
|
|
20580
|
+
}
|
|
20581
|
+
}
|
|
20582
|
+
}
|
|
20583
|
+
const met = requirements.every((r) => r.met) && regressions.length === 0 && violations.length === 0;
|
|
20584
|
+
return {
|
|
20585
|
+
met,
|
|
20586
|
+
requirements,
|
|
20587
|
+
regressions,
|
|
20588
|
+
regressionsEnforced: Boolean(baseline && args.enforceNoRegressions),
|
|
20589
|
+
ratchet: { enforced: Boolean(baseline && args.enforceRatchet), violations }
|
|
20590
|
+
};
|
|
20591
|
+
}
|
|
20592
|
+
function evaluate(selector, matched) {
|
|
20593
|
+
const passed = matched.filter((tc) => tc.status === "passed").length;
|
|
20594
|
+
const failing = matched.filter((tc) => tc.status !== "passed").map((tc) => tc.story.scenario);
|
|
20595
|
+
return {
|
|
20596
|
+
selector,
|
|
20597
|
+
matched: matched.length,
|
|
20598
|
+
passed,
|
|
20599
|
+
failing,
|
|
20600
|
+
met: matched.length > 0 && failing.length === 0
|
|
20601
|
+
};
|
|
20602
|
+
}
|
|
20603
|
+
function statusMap(run) {
|
|
20604
|
+
return new Map(run.testCases.map((tc) => [tc.id, tc.status]));
|
|
20605
|
+
}
|
|
20606
|
+
function renderGoal(report, format) {
|
|
20607
|
+
if (format === "json") return JSON.stringify(report, null, 2);
|
|
20608
|
+
const lines = [`GOAL: ${report.met ? "met" : "not met"}`];
|
|
20609
|
+
for (const req of report.requirements) {
|
|
20610
|
+
if (req.matched === 0) {
|
|
20611
|
+
lines.push(` ${req.selector}: no matching scenario (no proof)`);
|
|
20612
|
+
continue;
|
|
20613
|
+
}
|
|
20614
|
+
const tail = req.failing.length > 0 ? ` (${req.failing.length} failing)` : "";
|
|
20615
|
+
lines.push(` ${req.selector}: ${req.passed}/${req.matched} scenarios pass${tail}`);
|
|
20616
|
+
}
|
|
20617
|
+
if (report.regressionsEnforced) {
|
|
20618
|
+
if (report.regressions.length === 0) {
|
|
20619
|
+
lines.push(" regressions: 0");
|
|
20620
|
+
} else {
|
|
20621
|
+
lines.push(` regressions: ${report.regressions.length} (${report.regressions.map((r) => r.title).join(", ")})`);
|
|
20622
|
+
}
|
|
20623
|
+
}
|
|
20624
|
+
if (report.ratchet.enforced) {
|
|
20625
|
+
if (report.ratchet.violations.length === 0) {
|
|
20626
|
+
lines.push(" ratchet: clean (0 scenarios removed/weakened)");
|
|
20627
|
+
} else {
|
|
20628
|
+
lines.push(` ratchet: ${report.ratchet.violations.length} removed/weakened`);
|
|
20629
|
+
for (const v of report.ratchet.violations) {
|
|
20630
|
+
lines.push(` ${v.kind}: ${v.title} (${v.detail})`);
|
|
20631
|
+
}
|
|
20632
|
+
}
|
|
20633
|
+
}
|
|
20634
|
+
return lines.join("\n");
|
|
20635
|
+
}
|
|
20636
|
+
|
|
20637
|
+
// src/triage.ts
|
|
20638
|
+
function buildTriage(args, _deps = {}) {
|
|
20639
|
+
const { testCases, baseline } = args;
|
|
20640
|
+
const failing = testCases.filter((tc) => tc.status === "failed");
|
|
20641
|
+
const ranked = failing.map((tc) => {
|
|
20642
|
+
const regressed = baseline?.get(tc.id) === "passed";
|
|
20643
|
+
return {
|
|
20644
|
+
tc,
|
|
20645
|
+
regressed,
|
|
20646
|
+
covers: tc.story.covers ?? []
|
|
20647
|
+
};
|
|
20648
|
+
}).sort((a, b) => {
|
|
20649
|
+
if (a.regressed !== b.regressed) return a.regressed ? -1 : 1;
|
|
20650
|
+
const la = `${a.tc.sourceFile}:${a.tc.sourceLine}`;
|
|
20651
|
+
const lb = `${b.tc.sourceFile}:${b.tc.sourceLine}`;
|
|
20652
|
+
return la.localeCompare(lb);
|
|
20653
|
+
});
|
|
20654
|
+
const items = ranked.map((entry, index) => ({
|
|
20655
|
+
rank: index + 1,
|
|
20656
|
+
id: entry.tc.id,
|
|
20657
|
+
scenario: entry.tc.story.scenario,
|
|
20658
|
+
status: entry.tc.status,
|
|
20659
|
+
location: `${entry.tc.sourceFile}:${entry.tc.sourceLine}`,
|
|
20660
|
+
covers: entry.covers,
|
|
20661
|
+
tickets: (entry.tc.story.tickets ?? []).map((t) => t.id),
|
|
20662
|
+
errorMessage: failingScenarioMessage(entry.tc),
|
|
20663
|
+
regressed: entry.regressed,
|
|
20664
|
+
reason: entry.regressed ? "regression" : "failing"
|
|
20665
|
+
}));
|
|
20666
|
+
return {
|
|
20667
|
+
total: testCases.length,
|
|
20668
|
+
failing: failing.length,
|
|
20669
|
+
regressions: items.filter((i) => i.regressed).length,
|
|
20670
|
+
needsCovers: items.filter((i) => i.covers.length === 0).length,
|
|
20671
|
+
items
|
|
20672
|
+
};
|
|
20673
|
+
}
|
|
20674
|
+
function renderTriage(report, format) {
|
|
20675
|
+
if (format === "json") return JSON.stringify(report, null, 2);
|
|
20676
|
+
if (report.items.length === 0) {
|
|
20677
|
+
return "Nothing to triage. No failing scenarios.";
|
|
20678
|
+
}
|
|
20679
|
+
const header = report.regressions > 0 ? `${report.items.length} items to triage (${report.regressions} regression${report.regressions === 1 ? "" : "s"})` : `${report.items.length} items to triage`;
|
|
20680
|
+
const lines = [header, ""];
|
|
20681
|
+
for (const item of report.items) {
|
|
20682
|
+
const tag = item.regressed ? "[regression] " : "";
|
|
20683
|
+
lines.push(`${item.rank}. ${tag}${item.scenario}`);
|
|
20684
|
+
lines.push(` ${item.location}`);
|
|
20685
|
+
if (item.errorMessage) {
|
|
20686
|
+
lines.push(` \u2192 ${item.errorMessage.split("\n")[0]}`);
|
|
20687
|
+
}
|
|
20688
|
+
if (item.covers.length > 0) {
|
|
20689
|
+
lines.push(` fix: ${item.covers.join(", ")}`);
|
|
20690
|
+
} else {
|
|
20691
|
+
lines.push(" fix: (no covers declared \u2014 add `covers` to route this to code)");
|
|
20692
|
+
}
|
|
20693
|
+
if (item.tickets.length > 0) {
|
|
20694
|
+
lines.push(` ticket: ${item.tickets.join(", ")}`);
|
|
20695
|
+
}
|
|
20696
|
+
lines.push("");
|
|
20697
|
+
}
|
|
20698
|
+
if (report.needsCovers > 0) {
|
|
20699
|
+
lines.push(`${report.needsCovers} failing scenario(s) have no covers and can't be routed to code automatically.`);
|
|
20700
|
+
}
|
|
20701
|
+
return lines.join("\n").trimEnd();
|
|
20702
|
+
}
|
|
20703
|
+
|
|
20265
20704
|
// src/review/conventions.ts
|
|
20266
20705
|
var CHANGE_TAG_PREFIX = "change:";
|
|
20267
20706
|
var AUDIENCE_TAG_PREFIX = "audience:";
|
|
@@ -21063,6 +21502,7 @@ var FORMAT_EXTENSIONS = {
|
|
|
21063
21502
|
"behavior-manifest-json": ".behavior-manifest.json",
|
|
21064
21503
|
markdown: ".md",
|
|
21065
21504
|
"release-manifest": ".release-manifest.md",
|
|
21505
|
+
"traceability-matrix": ".traceability-matrix.md",
|
|
21066
21506
|
html: ".html",
|
|
21067
21507
|
"cucumber-html": ".cucumber.html",
|
|
21068
21508
|
junit: ".junit.xml",
|
|
@@ -21285,7 +21725,9 @@ var ReportGenerator = class {
|
|
|
21285
21725
|
permalinkBaseUrl: options.astro?.markdown?.permalinkBaseUrl,
|
|
21286
21726
|
ticketUrlTemplate: options.astro?.markdown?.ticketUrlTemplate,
|
|
21287
21727
|
traceUrlTemplate: options.astro?.markdown?.traceUrlTemplate,
|
|
21288
|
-
customRenderers: options.astro?.markdown?.customRenderers
|
|
21728
|
+
customRenderers: options.astro?.markdown?.customRenderers,
|
|
21729
|
+
scenarioAnchor: options.astro?.markdown?.scenarioAnchor,
|
|
21730
|
+
scenarioBadge: options.astro?.markdown?.scenarioBadge
|
|
21289
21731
|
}
|
|
21290
21732
|
},
|
|
21291
21733
|
assetMode: options.assetMode ?? "none",
|
|
@@ -21489,6 +21931,10 @@ var ReportGenerator = class {
|
|
|
21489
21931
|
const formatter = new ReleaseManifestFormatter();
|
|
21490
21932
|
return formatter.format(run);
|
|
21491
21933
|
}
|
|
21934
|
+
case "traceability-matrix": {
|
|
21935
|
+
const formatter = new TraceabilityMatrixFormatter();
|
|
21936
|
+
return formatter.format(run);
|
|
21937
|
+
}
|
|
21492
21938
|
case "story-report-json": {
|
|
21493
21939
|
const formatter = new StoryReportJsonFormatter({
|
|
21494
21940
|
pretty: this.options.storyReportJson.pretty
|
|
@@ -22144,6 +22590,189 @@ async function importOpenApi(options) {
|
|
|
22144
22590
|
// src/build-docs.ts
|
|
22145
22591
|
import * as fs13 from "fs";
|
|
22146
22592
|
import * as path14 from "path";
|
|
22593
|
+
|
|
22594
|
+
// src/scenario-links.ts
|
|
22595
|
+
function scenarioAnchor(title) {
|
|
22596
|
+
return `scenario-${slugify(title)}`;
|
|
22597
|
+
}
|
|
22598
|
+
function buildScenarioLinks(report, options = {}) {
|
|
22599
|
+
const audienceSplit = options.audienceSplit ?? false;
|
|
22600
|
+
const baseUrl = (options.baseUrl ?? "/stories").replace(/\/$/, "");
|
|
22601
|
+
const scenarios = {};
|
|
22602
|
+
for (const feature of report.features) {
|
|
22603
|
+
const stem = slugify(cleanTestStem(feature.sourceFile));
|
|
22604
|
+
for (const scenario of feature.scenarios) {
|
|
22605
|
+
const audience = deriveAudience(feature.sourceFile, scenario.tags);
|
|
22606
|
+
const url = audienceSplit ? `${baseUrl}/${audience}/${stem}/` : `${baseUrl}/${stem}/`;
|
|
22607
|
+
const anchor = scenarioAnchor(scenario.title);
|
|
22608
|
+
scenarios[scenario.id] = {
|
|
22609
|
+
id: scenario.id,
|
|
22610
|
+
title: scenario.title,
|
|
22611
|
+
audience,
|
|
22612
|
+
status: scenario.status,
|
|
22613
|
+
sourceFile: feature.sourceFile,
|
|
22614
|
+
url,
|
|
22615
|
+
anchor,
|
|
22616
|
+
deepLink: `${url}#${anchor}`
|
|
22617
|
+
};
|
|
22618
|
+
}
|
|
22619
|
+
}
|
|
22620
|
+
return { schemaVersion: "1.0", runId: report.runId, baseUrl, scenarios };
|
|
22621
|
+
}
|
|
22622
|
+
|
|
22623
|
+
// src/changes-page.ts
|
|
22624
|
+
var GROUPS = [
|
|
22625
|
+
{ kind: "regressed", heading: "Regressed", icon: "\u26A0\uFE0F" },
|
|
22626
|
+
{ kind: "removed", heading: "Removed", icon: "\u{1F5D1}\uFE0F" },
|
|
22627
|
+
{ kind: "added", heading: "Added", icon: "\u2728" },
|
|
22628
|
+
{ kind: "fixed", heading: "Fixed", icon: "\u2705" },
|
|
22629
|
+
{ kind: "changed", heading: "Changed", icon: "\u{1F501}" }
|
|
22630
|
+
];
|
|
22631
|
+
function yamlScalar2(value) {
|
|
22632
|
+
if (/[:#[\]{}&*!|>'"%@`]|^[\s-]|\s$/.test(value)) {
|
|
22633
|
+
return `'${value.replace(/'/g, "''")}'`;
|
|
22634
|
+
}
|
|
22635
|
+
return value;
|
|
22636
|
+
}
|
|
22637
|
+
function badge(summary) {
|
|
22638
|
+
if (summary.regressed > 0 || summary.removed > 0) return { text: "Regressed", variant: "danger" };
|
|
22639
|
+
if (summary.added > 0 || summary.fixed > 0 || summary.changed > 0)
|
|
22640
|
+
return { text: "Updated", variant: "tip" };
|
|
22641
|
+
return { text: "No changes", variant: "note" };
|
|
22642
|
+
}
|
|
22643
|
+
function line(entry, links) {
|
|
22644
|
+
const link2 = links.scenarios[entry.id];
|
|
22645
|
+
const label = link2 ? `[${entry.title}](${link2.deepLink})` : entry.title;
|
|
22646
|
+
const transition = entry.baselineStatus && entry.currentStatus && entry.baselineStatus !== entry.currentStatus ? ` \u2014 \`${entry.baselineStatus}\` \u2192 \`${entry.currentStatus}\`` : "";
|
|
22647
|
+
return `- ${label} \`${entry.sourceFile}\`${transition}`;
|
|
22648
|
+
}
|
|
22649
|
+
function renderChangesPage(diff, links) {
|
|
22650
|
+
const b = badge(diff.summary);
|
|
22651
|
+
const { added, removed, regressed, fixed, changed } = diff.summary;
|
|
22652
|
+
const totalChanged = added + removed + regressed + fixed + changed;
|
|
22653
|
+
const frontmatter = [
|
|
22654
|
+
"---",
|
|
22655
|
+
"title: What's changed",
|
|
22656
|
+
`description: ${yamlScalar2(
|
|
22657
|
+
totalChanged === 0 ? "No behavioural changes since the baseline" : `${totalChanged} scenario${totalChanged !== 1 ? "s" : ""} changed since the baseline`
|
|
22658
|
+
)}`,
|
|
22659
|
+
"sidebar:",
|
|
22660
|
+
" order: 0",
|
|
22661
|
+
" badge:",
|
|
22662
|
+
` text: ${b.text}`,
|
|
22663
|
+
` variant: ${b.variant}`,
|
|
22664
|
+
"---"
|
|
22665
|
+
].join("\n");
|
|
22666
|
+
const body = [];
|
|
22667
|
+
if (totalChanged === 0) {
|
|
22668
|
+
body.push("No behavioural changes since the baseline run. \u2705");
|
|
22669
|
+
} else {
|
|
22670
|
+
body.push(
|
|
22671
|
+
`**${regressed}** regressed \xB7 **${removed}** removed \xB7 **${added}** added \xB7 **${fixed}** fixed \xB7 **${changed}** changed`,
|
|
22672
|
+
""
|
|
22673
|
+
);
|
|
22674
|
+
for (const group of GROUPS) {
|
|
22675
|
+
const entries = diff.scenarios.filter((s) => s.kind === group.kind);
|
|
22676
|
+
if (entries.length === 0) continue;
|
|
22677
|
+
body.push(`## ${group.icon} ${group.heading} (${entries.length})`, "");
|
|
22678
|
+
for (const entry of entries) body.push(line(entry, links));
|
|
22679
|
+
body.push("");
|
|
22680
|
+
}
|
|
22681
|
+
}
|
|
22682
|
+
return `${frontmatter}
|
|
22683
|
+
|
|
22684
|
+
${body.join("\n")}
|
|
22685
|
+
`;
|
|
22686
|
+
}
|
|
22687
|
+
|
|
22688
|
+
// src/overview-page.ts
|
|
22689
|
+
var AUDIENCE_CARDS = [
|
|
22690
|
+
{
|
|
22691
|
+
key: "engineer",
|
|
22692
|
+
label: "Engineer",
|
|
22693
|
+
icon: "\u{1F527}",
|
|
22694
|
+
blurb: "Unit & integration behaviour \u2014 how the system works under the hood."
|
|
22695
|
+
},
|
|
22696
|
+
{
|
|
22697
|
+
key: "stakeholder",
|
|
22698
|
+
label: "Stakeholder",
|
|
22699
|
+
icon: "\u{1F3AC}",
|
|
22700
|
+
blurb: "End-to-end journeys \u2014 what the product does, with video and traces."
|
|
22701
|
+
}
|
|
22702
|
+
];
|
|
22703
|
+
var STATUS_ICON = {
|
|
22704
|
+
passed: "\u2705",
|
|
22705
|
+
failed: "\u274C",
|
|
22706
|
+
skipped: "\u23ED\uFE0F",
|
|
22707
|
+
pending: "\u{1F6A7}"
|
|
22708
|
+
};
|
|
22709
|
+
function yamlScalar3(value) {
|
|
22710
|
+
if (/[:#[\]{}&*!|>'"%@`]|^[\s-]|\s$/.test(value)) {
|
|
22711
|
+
return `'${value.replace(/'/g, "''")}'`;
|
|
22712
|
+
}
|
|
22713
|
+
return value;
|
|
22714
|
+
}
|
|
22715
|
+
function renderOverviewPage(links) {
|
|
22716
|
+
const all = Object.values(links.scenarios);
|
|
22717
|
+
const total = all.length;
|
|
22718
|
+
const passed = all.filter((s) => s.status === "passed").length;
|
|
22719
|
+
const failed = all.filter((s) => s.status === "failed").length;
|
|
22720
|
+
const frontmatter = [
|
|
22721
|
+
"---",
|
|
22722
|
+
"title: Stories",
|
|
22723
|
+
`description: ${yamlScalar3(
|
|
22724
|
+
`${total} scenario${total !== 1 ? "s" : ""} \u2014 ${passed} passed, ${failed} failed`
|
|
22725
|
+
)}`,
|
|
22726
|
+
"sidebar:",
|
|
22727
|
+
" order: 1",
|
|
22728
|
+
"---"
|
|
22729
|
+
].join("\n");
|
|
22730
|
+
const body = [
|
|
22731
|
+
`**${total}** scenarios \xB7 **${passed}** passed \xB7 **${failed}** failed`,
|
|
22732
|
+
""
|
|
22733
|
+
];
|
|
22734
|
+
for (const card of AUDIENCE_CARDS) {
|
|
22735
|
+
const scenarios = all.filter((s) => s.audience === card.key);
|
|
22736
|
+
if (scenarios.length === 0) continue;
|
|
22737
|
+
const cardPassed = scenarios.filter((s) => s.status === "passed").length;
|
|
22738
|
+
const cardFailed = scenarios.filter((s) => s.status === "failed").length;
|
|
22739
|
+
const counts = cardFailed > 0 ? `${cardPassed} passed, ${cardFailed} failed` : `${cardPassed} passed`;
|
|
22740
|
+
body.push(`## ${card.icon} ${card.label} (${scenarios.length} \u2014 ${counts})`, "");
|
|
22741
|
+
body.push(`${card.blurb}`, "");
|
|
22742
|
+
for (const s of scenariosSorted(scenarios)) {
|
|
22743
|
+
body.push(`- ${STATUS_ICON[s.status] ?? "\u2022"} [${s.title}](${s.deepLink})`);
|
|
22744
|
+
}
|
|
22745
|
+
body.push("");
|
|
22746
|
+
}
|
|
22747
|
+
return `${frontmatter}
|
|
22748
|
+
|
|
22749
|
+
${body.join("\n")}
|
|
22750
|
+
`;
|
|
22751
|
+
}
|
|
22752
|
+
function scenariosSorted(scenarios) {
|
|
22753
|
+
return [...scenarios].sort((a, b) => {
|
|
22754
|
+
const aFail = a.status === "failed" ? 0 : 1;
|
|
22755
|
+
const bFail = b.status === "failed" ? 0 : 1;
|
|
22756
|
+
if (aFail !== bFail) return aFail - bFail;
|
|
22757
|
+
return a.title.localeCompare(b.title);
|
|
22758
|
+
});
|
|
22759
|
+
}
|
|
22760
|
+
|
|
22761
|
+
// src/build-docs.ts
|
|
22762
|
+
var AUDIENCES = ["engineer", "stakeholder"];
|
|
22763
|
+
function partitionByAudience(run) {
|
|
22764
|
+
const buckets = {
|
|
22765
|
+
engineer: [],
|
|
22766
|
+
stakeholder: []
|
|
22767
|
+
};
|
|
22768
|
+
for (const tc of run.testCases) {
|
|
22769
|
+
buckets[deriveAudience(tc.sourceFile, tc.tags)].push(tc);
|
|
22770
|
+
}
|
|
22771
|
+
return {
|
|
22772
|
+
engineer: { ...run, testCases: buckets.engineer },
|
|
22773
|
+
stakeholder: { ...run, testCases: buckets.stakeholder }
|
|
22774
|
+
};
|
|
22775
|
+
}
|
|
22147
22776
|
var BuildDocsError = class extends Error {
|
|
22148
22777
|
constructor(message, kind) {
|
|
22149
22778
|
super(message);
|
|
@@ -22185,6 +22814,40 @@ function bundleExplorerAssets(reportPath, assetsDir, baseUrl = "/stories/assets"
|
|
|
22185
22814
|
}
|
|
22186
22815
|
return copied;
|
|
22187
22816
|
}
|
|
22817
|
+
var CHANGE_BADGE = {
|
|
22818
|
+
added: "\u{1F195} **New** _since last run_",
|
|
22819
|
+
fixed: "\u2705 **Fixed** _since last run_",
|
|
22820
|
+
regressed: "\u26A0\uFE0F **Regressed** _since last run_"
|
|
22821
|
+
};
|
|
22822
|
+
function changeBadgeLookup(diff) {
|
|
22823
|
+
if (!diff) return void 0;
|
|
22824
|
+
const byKey = /* @__PURE__ */ new Map();
|
|
22825
|
+
for (const s of diff.scenarios) {
|
|
22826
|
+
const badge2 = CHANGE_BADGE[s.kind];
|
|
22827
|
+
if (badge2) byKey.set(`${s.sourceFile}\0${s.title}`, badge2);
|
|
22828
|
+
}
|
|
22829
|
+
if (byKey.size === 0) return void 0;
|
|
22830
|
+
return (tc) => byKey.get(`${tc.sourceFile}\0${tc.story.scenario}`);
|
|
22831
|
+
}
|
|
22832
|
+
function readStoryReport(reportPath) {
|
|
22833
|
+
if (!fs13.existsSync(reportPath)) return null;
|
|
22834
|
+
try {
|
|
22835
|
+
return JSON.parse(fs13.readFileSync(reportPath, "utf8"));
|
|
22836
|
+
} catch {
|
|
22837
|
+
return null;
|
|
22838
|
+
}
|
|
22839
|
+
}
|
|
22840
|
+
function writeScenarioLinks(reportPath, outDir, options = {}) {
|
|
22841
|
+
const report = readStoryReport(reportPath);
|
|
22842
|
+
if (!report) return null;
|
|
22843
|
+
const index = buildScenarioLinks(report, { audienceSplit: options.audienceSplit });
|
|
22844
|
+
fs13.writeFileSync(
|
|
22845
|
+
path14.join(outDir, "scenario-links.json"),
|
|
22846
|
+
JSON.stringify(index, null, 2),
|
|
22847
|
+
"utf8"
|
|
22848
|
+
);
|
|
22849
|
+
return index;
|
|
22850
|
+
}
|
|
22188
22851
|
function clearGeneratedPages(dir) {
|
|
22189
22852
|
if (!fs13.existsSync(dir)) return;
|
|
22190
22853
|
for (const entry of fs13.readdirSync(dir, { withFileTypes: true })) {
|
|
@@ -22235,16 +22898,72 @@ async function buildDocs(options) {
|
|
|
22235
22898
|
outputDir: storiesPublicDir,
|
|
22236
22899
|
outputName: "story-report"
|
|
22237
22900
|
}).generate(canonical);
|
|
22901
|
+
let diff;
|
|
22902
|
+
if (options.baselinePath) {
|
|
22903
|
+
const baselineResolved = path14.resolve(options.baselinePath);
|
|
22904
|
+
const baseline = readStoryReport(baselineResolved);
|
|
22905
|
+
if (!baseline) {
|
|
22906
|
+
throw new BuildDocsError(
|
|
22907
|
+
`Baseline story-report not found or unreadable: ${baselineResolved}`,
|
|
22908
|
+
"input"
|
|
22909
|
+
);
|
|
22910
|
+
}
|
|
22911
|
+
const current = readStoryReport(reportPath);
|
|
22912
|
+
if (current) diff = diffStoryReports(baseline, current);
|
|
22913
|
+
}
|
|
22914
|
+
const scenarioBadge = changeBadgeLookup(diff);
|
|
22238
22915
|
clearGeneratedPages(storyPagesDir);
|
|
22239
|
-
|
|
22916
|
+
const genPages = (run, outDir) => new ReportGenerator({
|
|
22240
22917
|
formats: ["astro"],
|
|
22241
|
-
outputDir:
|
|
22918
|
+
outputDir: outDir,
|
|
22242
22919
|
outputName: "index",
|
|
22243
22920
|
output: { mode: "colocated", colocatedStyle: "flat" },
|
|
22244
22921
|
assetMode: "copy",
|
|
22245
|
-
astro: {
|
|
22246
|
-
|
|
22922
|
+
astro: {
|
|
22923
|
+
assetsDir,
|
|
22924
|
+
assetsBaseUrl: "/stories/assets",
|
|
22925
|
+
markdown: {
|
|
22926
|
+
// Emit the same anchor scenario-links.json points at, so fragments resolve.
|
|
22927
|
+
scenarioAnchor: (tc) => scenarioAnchor(tc.story.scenario),
|
|
22928
|
+
scenarioBadge
|
|
22929
|
+
}
|
|
22930
|
+
}
|
|
22931
|
+
}).generate(run);
|
|
22932
|
+
const audiences = { engineer: 0, stakeholder: 0 };
|
|
22933
|
+
if (options.audienceSplit ?? false) {
|
|
22934
|
+
const partitioned = partitionByAudience(canonical);
|
|
22935
|
+
for (const audience of AUDIENCES) {
|
|
22936
|
+
const sub = partitioned[audience];
|
|
22937
|
+
audiences[audience] = sub.testCases.length;
|
|
22938
|
+
if (sub.testCases.length === 0) continue;
|
|
22939
|
+
await genPages(sub, path14.join(storyPagesDir, audience));
|
|
22940
|
+
}
|
|
22941
|
+
} else {
|
|
22942
|
+
await genPages(canonical, storyPagesDir);
|
|
22943
|
+
}
|
|
22247
22944
|
const bundledAssets = bundleExplorerAssets(reportPath, assetsDir);
|
|
22945
|
+
const linksIndex = writeScenarioLinks(reportPath, storiesPublicDir, {
|
|
22946
|
+
audienceSplit: options.audienceSplit ?? false
|
|
22947
|
+
});
|
|
22948
|
+
const scenarioLinks = linksIndex ? Object.keys(linksIndex.scenarios).length : 0;
|
|
22949
|
+
if (linksIndex) {
|
|
22950
|
+
fs13.writeFileSync(
|
|
22951
|
+
path14.join(storyPagesDir, "index.md"),
|
|
22952
|
+
renderOverviewPage(linksIndex),
|
|
22953
|
+
"utf8"
|
|
22954
|
+
);
|
|
22955
|
+
}
|
|
22956
|
+
const changesJsonPath = path14.join(storiesPublicDir, "changes.json");
|
|
22957
|
+
const changesMdPath = path14.join(storyPagesDir, "changes.md");
|
|
22958
|
+
let changes;
|
|
22959
|
+
if (diff && linksIndex) {
|
|
22960
|
+
fs13.writeFileSync(changesJsonPath, JSON.stringify(diff, null, 2), "utf8");
|
|
22961
|
+
fs13.writeFileSync(changesMdPath, renderChangesPage(diff, linksIndex), "utf8");
|
|
22962
|
+
changes = diff.summary;
|
|
22963
|
+
} else {
|
|
22964
|
+
fs13.rmSync(changesJsonPath, { force: true });
|
|
22965
|
+
fs13.rmSync(changesMdPath, { force: true });
|
|
22966
|
+
}
|
|
22248
22967
|
let apiPages = 0;
|
|
22249
22968
|
if (options.openapiPath) {
|
|
22250
22969
|
const res = await importOpenApi({
|
|
@@ -22255,7 +22974,7 @@ async function buildDocs(options) {
|
|
|
22255
22974
|
});
|
|
22256
22975
|
apiPages = res.pageCount;
|
|
22257
22976
|
}
|
|
22258
|
-
return { siteDir, bundledAssets, apiPages };
|
|
22977
|
+
return { siteDir, bundledAssets, apiPages, audiences, scenarioLinks, changes };
|
|
22259
22978
|
} catch (err) {
|
|
22260
22979
|
if (err instanceof BuildDocsError) throw err;
|
|
22261
22980
|
throw new BuildDocsError(`Generation failed: ${err.message}`, "generation");
|
|
@@ -22286,6 +23005,7 @@ var EXIT_GENERATION = 3;
|
|
|
22286
23005
|
var EXIT_USAGE = 4;
|
|
22287
23006
|
var EXIT_COMPARE_GATE = 5;
|
|
22288
23007
|
var EXIT_REVIEW_GATE = 5;
|
|
23008
|
+
var EXIT_AGENT_GATE = 5;
|
|
22289
23009
|
var EXIT_RELEASE_GATE = 6;
|
|
22290
23010
|
var HELP_TEXT = `
|
|
22291
23011
|
executable-stories \u2014 Generate reports from test results JSON.
|
|
@@ -22297,6 +23017,9 @@ USAGE
|
|
|
22297
23017
|
executable-stories gate-release <dev-run.json> <rc-run.json> [options]
|
|
22298
23018
|
executable-stories review <file> --changed-files <path> [options]
|
|
22299
23019
|
executable-stories list <file> [options]
|
|
23020
|
+
executable-stories check <file> [--baseline <path|auto>] [--check-format text|json] [--no-fail]
|
|
23021
|
+
executable-stories goal <file> [--require-tags <csv>] [--require-tickets <csv>] [--require-scenarios <csv>] [--baseline <path|auto>] [--no-regressions] [--goal-format text|json]
|
|
23022
|
+
executable-stories triage <file> [--baseline <path|auto>] [--triage-format text|json]
|
|
22300
23023
|
executable-stories validate <file>
|
|
22301
23024
|
executable-stories validate --stdin
|
|
22302
23025
|
executable-stories init-astro [directory]
|
|
@@ -22316,6 +23039,9 @@ SUBCOMMANDS
|
|
|
22316
23039
|
gate-release Verify a release candidate against the dev test baseline (RC gate)
|
|
22317
23040
|
review Generate an Evidence Review of AI-authored changes (correlate a run to the diff)
|
|
22318
23041
|
list List scenarios from a test run (text table or JSON)
|
|
23042
|
+
check Backpressure summary: compress passing, expand failing (GWT + error + covers); non-zero exit on failures
|
|
23043
|
+
goal Behavioral definition-of-done for agent loops: required scenarios pass, no regressions, no weakened scenarios (exit 0 = met, 5 = not)
|
|
23044
|
+
triage Discovery worklist for agent loops: failing scenarios, regressions first, each with the code it covers
|
|
22319
23045
|
validate Validate a JSON file against the schema (no output generated)
|
|
22320
23046
|
init-astro Scaffold an Astro docs site for story output (Starlight with themed CSS)
|
|
22321
23047
|
new Scaffold a docs page from a template (adr, runbook, decision-log, incident)
|
|
@@ -22326,7 +23052,7 @@ SUBCOMMANDS
|
|
|
22326
23052
|
deploy Record deployments, show environment status, detect drift
|
|
22327
23053
|
|
|
22328
23054
|
OPTIONS
|
|
22329
|
-
--format <formats> Comma-separated formats: html, markdown, release-manifest, junit, cucumber-json, cucumber-messages, cucumber-html, astro, confluence, story-report-json, scenario-index-json, behavior-manifest-json, or custom names from config (default: html)
|
|
23055
|
+
--format <formats> Comma-separated formats: html, markdown, release-manifest, traceability-matrix, junit, cucumber-json, cucumber-messages, cucumber-html, astro, confluence, story-report-json, scenario-index-json, behavior-manifest-json, or custom names from config (default: html)
|
|
22330
23056
|
astro Themed Markdown (for Astro docs sites with matching CSS)
|
|
22331
23057
|
confluence Atlassian Document Format (ADF) JSON for Confluence / Jira
|
|
22332
23058
|
behavior-manifest-json Agent-readable behavior manifest and debugger warnings
|
|
@@ -22338,6 +23064,7 @@ OPTIONS
|
|
|
22338
23064
|
cucumber-messages Raw NDJSON (Cucumber Messages)
|
|
22339
23065
|
story-report-json StoryReport v1 JSON (consumed by executable-stories-react and other UI renderers)
|
|
22340
23066
|
scenario-index-json Storybook-like scenario index for agents and explorers
|
|
23067
|
+
traceability-matrix Requirement-first matrix (ticket -> scenarios -> covered code -> status)
|
|
22341
23068
|
--config <path> Path to executable-stories.config.js (default: ./executable-stories.config.js)
|
|
22342
23069
|
--input-type <type> Input type: raw, canonical, or ndjson (default: raw)
|
|
22343
23070
|
--output-dir <dir> Output directory (default: reports)
|
|
@@ -22364,6 +23091,15 @@ OPTIONS
|
|
|
22364
23091
|
--stdin Read JSON from stdin instead of file
|
|
22365
23092
|
--list-format <format> list output format: text (default), json, csv, markdown-table
|
|
22366
23093
|
--json-summary Deprecated alias for --list-format json
|
|
23094
|
+
--check-format <format> check output format: text (default) or json
|
|
23095
|
+
--no-fail (check) Report only \u2014 always exit 0 even when scenarios failed
|
|
23096
|
+
--require-tags <csv> (goal) Every scenario carrying any of these tags must pass
|
|
23097
|
+
--require-tickets <csv> (goal) Every scenario carrying any of these tickets must pass
|
|
23098
|
+
--require-scenarios <csv> (goal) These scenarios (by id or exact title) must pass
|
|
23099
|
+
--no-regressions (goal) Not met if any scenario regressed vs --baseline
|
|
23100
|
+
--no-ratchet (goal) Disable the removed/weakened-scenario guard (on by default with --baseline)
|
|
23101
|
+
--goal-format <format> goal output format: text (default) or json
|
|
23102
|
+
--triage-format <format> triage output format: text (default) or json
|
|
22367
23103
|
--baseline <path|auto> Compare baseline file, or auto-pick a prior run for compare
|
|
22368
23104
|
--baseline-dir <dir> Directory to scan when --baseline auto is used
|
|
22369
23105
|
--pr-summary Print a PR-friendly markdown summary after compare
|
|
@@ -22388,6 +23124,31 @@ LIST
|
|
|
22388
23124
|
list supports --include-tags, --exclude-tags for filtering
|
|
22389
23125
|
list supports --input-type and --stdin
|
|
22390
23126
|
|
|
23127
|
+
CHECK
|
|
23128
|
+
check is the inner-loop "backpressure" view for coding agents: run it after tests.
|
|
23129
|
+
Passing scenarios collapse to a single count line; each failing scenario expands
|
|
23130
|
+
to its Given/When/Then steps, the step that broke, the error, and the product
|
|
23131
|
+
code it covers \u2014 so the agent gets an actionable signal, not a wall of green.
|
|
23132
|
+
check exits 5 when any scenario failed (so the agent loop pushes back); pass
|
|
23133
|
+
--no-fail to report only. --baseline <path|auto> adds "N regressed / N fixed"
|
|
23134
|
+
since the prior run. --check-format json emits the structured report.
|
|
23135
|
+
|
|
23136
|
+
GOAL
|
|
23137
|
+
goal is the behavioral stopping condition for an agent loop (the /goal pattern).
|
|
23138
|
+
It is "met" when the required scenarios pass, nothing regressed (with
|
|
23139
|
+
--no-regressions), and no scenario was removed, disabled, or had steps deleted
|
|
23140
|
+
versus --baseline (the ratchet, on by default when a baseline is given). Declare
|
|
23141
|
+
the target with --require-tags / --require-tickets / --require-scenarios; with
|
|
23142
|
+
none given, the goal is "every scenario passes". Exit 0 means met, 5 means not
|
|
23143
|
+
yet, so a loop can run until the verdict flips. --goal-format json for machines.
|
|
23144
|
+
|
|
23145
|
+
TRIAGE
|
|
23146
|
+
triage is the discovery-phase worklist for a loop. It lists failing scenarios,
|
|
23147
|
+
regressions first (with --baseline), each with the product code it covers, the
|
|
23148
|
+
error, and its tickets, so the loop can route each fix to a sub-agent. Failures
|
|
23149
|
+
with no covers are flagged. --triage-format json emits the work queue. triage
|
|
23150
|
+
always exits 0 \u2014 it reports work, it does not gate.
|
|
23151
|
+
|
|
22391
23152
|
COMPARE
|
|
22392
23153
|
compare supports --format html,markdown
|
|
22393
23154
|
compare uses the same --input-type for both baseline and current files
|
|
@@ -22460,9 +23221,16 @@ EXIT CODES
|
|
|
22460
23221
|
2 Canonical validation failure
|
|
22461
23222
|
3 Formatter/generation failure
|
|
22462
23223
|
4 Bad arguments / usage error
|
|
22463
|
-
5 Compare gate failed
|
|
23224
|
+
5 Compare / review / check gate failed
|
|
22464
23225
|
6 Release gate failed
|
|
22465
23226
|
`.trim();
|
|
23227
|
+
function parseTextJsonFormat(flag, value) {
|
|
23228
|
+
if (value !== "text" && value !== "json") {
|
|
23229
|
+
console.error(`Error: ${flag} must be "text" or "json", got "${value}".`);
|
|
23230
|
+
process.exit(EXIT_USAGE);
|
|
23231
|
+
}
|
|
23232
|
+
return value;
|
|
23233
|
+
}
|
|
22466
23234
|
async function parseCliArgs(argv) {
|
|
22467
23235
|
const args = argv.slice(2);
|
|
22468
23236
|
if (args.length === 0 || args.includes("--help") || args.includes("-h")) {
|
|
@@ -22470,9 +23238,9 @@ async function parseCliArgs(argv) {
|
|
|
22470
23238
|
process.exit(EXIT_SUCCESS);
|
|
22471
23239
|
}
|
|
22472
23240
|
const subcommand = args[0];
|
|
22473
|
-
if (subcommand !== "format" && subcommand !== "watch" && subcommand !== "compare" && subcommand !== "gate-release" && subcommand !== "deploy" && subcommand !== "review" && subcommand !== "list" && subcommand !== "validate" && subcommand !== "init-astro" && subcommand !== "build-docs" && subcommand !== "new" && subcommand !== "check-links" && subcommand !== "import-openapi" && subcommand !== "publish-confluence" && subcommand !== "publish-jira") {
|
|
23241
|
+
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") {
|
|
22474
23242
|
console.error(
|
|
22475
|
-
`Unknown subcommand: "${subcommand}". Use "format", "watch", "compare", "gate-release", "deploy", "review", "list", "validate", "init-astro", "build-docs", "new", "check-links", "import-openapi", "publish-confluence", or "publish-jira".`
|
|
23243
|
+
`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".`
|
|
22476
23244
|
);
|
|
22477
23245
|
process.exit(EXIT_USAGE);
|
|
22478
23246
|
}
|
|
@@ -22560,6 +23328,15 @@ async function parseCliArgs(argv) {
|
|
|
22560
23328
|
stdin: { type: "boolean", default: false },
|
|
22561
23329
|
"json-summary": { type: "boolean", default: false },
|
|
22562
23330
|
"list-format": { type: "string", default: "text" },
|
|
23331
|
+
"check-format": { type: "string", default: "text" },
|
|
23332
|
+
"no-fail": { type: "boolean", default: false },
|
|
23333
|
+
"require-tags": { type: "string" },
|
|
23334
|
+
"require-tickets": { type: "string" },
|
|
23335
|
+
"require-scenarios": { type: "string" },
|
|
23336
|
+
"no-regressions": { type: "boolean", default: false },
|
|
23337
|
+
"no-ratchet": { type: "boolean", default: false },
|
|
23338
|
+
"goal-format": { type: "string", default: "text" },
|
|
23339
|
+
"triage-format": { type: "string", default: "text" },
|
|
22563
23340
|
"emit-canonical": { type: "string" },
|
|
22564
23341
|
"slack-webhook": { type: "string" },
|
|
22565
23342
|
"teams-webhook": { type: "string" },
|
|
@@ -22630,7 +23407,7 @@ async function parseCliArgs(argv) {
|
|
|
22630
23407
|
}
|
|
22631
23408
|
const pluginConfig = await loadConfig(values["config"]);
|
|
22632
23409
|
const customFormatterNames = new Set(Object.keys(pluginConfig.formatters ?? {}));
|
|
22633
|
-
const builtInFormats = /* @__PURE__ */ new Set(["astro", "behavior-manifest-json", "confluence", "html", "markdown", "release-manifest", "junit", "cucumber-json", "cucumber-messages", "cucumber-html", "scenario-index-json", "story-report-json"]);
|
|
23410
|
+
const builtInFormats = /* @__PURE__ */ new Set(["astro", "behavior-manifest-json", "confluence", "html", "markdown", "release-manifest", "traceability-matrix", "junit", "cucumber-json", "cucumber-messages", "cucumber-html", "scenario-index-json", "story-report-json"]);
|
|
22634
23411
|
const formatStr = values.format;
|
|
22635
23412
|
const allRequestedFormats = formatStr.split(",").map((f) => f.trim());
|
|
22636
23413
|
const builtInRequested = allRequestedFormats.filter((f) => builtInFormats.has(f));
|
|
@@ -22638,7 +23415,7 @@ async function parseCliArgs(argv) {
|
|
|
22638
23415
|
const unknownFormats = allRequestedFormats.filter((f) => !builtInFormats.has(f) && !customFormatterNames.has(f));
|
|
22639
23416
|
if (unknownFormats.length > 0) {
|
|
22640
23417
|
const knownCustom = customFormatterNames.size > 0 ? `, ${[...customFormatterNames].join(", ")}` : "";
|
|
22641
|
-
console.error(`Error: Unknown format(s): ${unknownFormats.join(", ")}. Valid built-in: astro, behavior-manifest-json, confluence, html, markdown, release-manifest, junit, cucumber-json, cucumber-messages, cucumber-html, scenario-index-json, story-report-json${knownCustom}.`);
|
|
23418
|
+
console.error(`Error: Unknown format(s): ${unknownFormats.join(", ")}. Valid built-in: astro, behavior-manifest-json, confluence, html, markdown, release-manifest, traceability-matrix, junit, cucumber-json, cucumber-messages, cucumber-html, scenario-index-json, story-report-json${knownCustom}.`);
|
|
22642
23419
|
process.exit(EXIT_USAGE);
|
|
22643
23420
|
}
|
|
22644
23421
|
const formats = builtInRequested;
|
|
@@ -22726,10 +23503,14 @@ async function parseCliArgs(argv) {
|
|
|
22726
23503
|
console.error(`Error: --min-evidence must be "weak", "moderate", or "strong", got "${minEvidenceRaw}".`);
|
|
22727
23504
|
process.exit(EXIT_USAGE);
|
|
22728
23505
|
}
|
|
23506
|
+
const checkFormat = parseTextJsonFormat("--check-format", values["check-format"]);
|
|
23507
|
+
const goalFormat = parseTextJsonFormat("--goal-format", values["goal-format"]);
|
|
23508
|
+
const triageFormat = parseTextJsonFormat("--triage-format", values["triage-format"]);
|
|
22729
23509
|
const cliArgs = {
|
|
22730
23510
|
subcommand,
|
|
22731
23511
|
inputFile,
|
|
22732
23512
|
baselineFile,
|
|
23513
|
+
baselineArg: baselineValue,
|
|
22733
23514
|
currentFile,
|
|
22734
23515
|
baselineMode,
|
|
22735
23516
|
baselineDir: values["baseline-dir"],
|
|
@@ -22756,6 +23537,15 @@ async function parseCliArgs(argv) {
|
|
|
22756
23537
|
htmlThemePicker: values["html-theme-picker"],
|
|
22757
23538
|
jsonSummary: values["json-summary"],
|
|
22758
23539
|
listFormat: values["list-format"],
|
|
23540
|
+
checkFormat,
|
|
23541
|
+
noFail: values["no-fail"],
|
|
23542
|
+
requireTags: parseGlobs(values["require-tags"]),
|
|
23543
|
+
requireTickets: parseGlobs(values["require-tickets"]),
|
|
23544
|
+
requireScenarios: parseGlobs(values["require-scenarios"]),
|
|
23545
|
+
noRegressions: values["no-regressions"],
|
|
23546
|
+
noRatchet: values["no-ratchet"],
|
|
23547
|
+
goalFormat,
|
|
23548
|
+
triageFormat,
|
|
22759
23549
|
emitCanonical: values["emit-canonical"],
|
|
22760
23550
|
slackWebhook,
|
|
22761
23551
|
teamsWebhook,
|
|
@@ -22972,6 +23762,24 @@ function resolveBaselineAuto(currentFile, currentRun, args) {
|
|
|
22972
23762
|
}
|
|
22973
23763
|
return picked.file;
|
|
22974
23764
|
}
|
|
23765
|
+
function resolveBaselineRun(args, currentRun) {
|
|
23766
|
+
if (!args.baselineArg) return void 0;
|
|
23767
|
+
let baselineFile;
|
|
23768
|
+
if (args.baselineArg === "auto") {
|
|
23769
|
+
if (!args.inputFile) {
|
|
23770
|
+
console.error("Error: --baseline auto requires a current input file (not --stdin).");
|
|
23771
|
+
process.exit(EXIT_USAGE);
|
|
23772
|
+
}
|
|
23773
|
+
baselineFile = resolveBaselineAuto(args.inputFile, currentRun, args);
|
|
23774
|
+
} else {
|
|
23775
|
+
baselineFile = args.baselineArg;
|
|
23776
|
+
}
|
|
23777
|
+
return applySelection(normalizeRunFromText(readFileInput(baselineFile), args).run, args);
|
|
23778
|
+
}
|
|
23779
|
+
function resolveBaselineStatusMap(args, currentRun) {
|
|
23780
|
+
const baselineRun = resolveBaselineRun(args, currentRun);
|
|
23781
|
+
return baselineRun ? new Map(baselineRun.testCases.map((tc) => [tc.id, tc.status])) : void 0;
|
|
23782
|
+
}
|
|
22975
23783
|
async function main() {
|
|
22976
23784
|
const { args, pluginConfig, customRequested } = await parseCliArgs(process.argv);
|
|
22977
23785
|
const startMs = Date.now();
|
|
@@ -23082,6 +23890,51 @@ async function main() {
|
|
|
23082
23890
|
console.log(output);
|
|
23083
23891
|
process.exit(EXIT_SUCCESS);
|
|
23084
23892
|
}
|
|
23893
|
+
if (args.subcommand === "check") {
|
|
23894
|
+
const text3 = await readInput(args);
|
|
23895
|
+
const run = applySelection(normalizeRunFromText(text3, args).run, args);
|
|
23896
|
+
const baseline = resolveBaselineStatusMap(args, run);
|
|
23897
|
+
const report = buildCheck(
|
|
23898
|
+
{ testCases: run.testCases, baseline, format: args.checkFormat },
|
|
23899
|
+
{}
|
|
23900
|
+
);
|
|
23901
|
+
console.log(renderCheck(report, args.checkFormat));
|
|
23902
|
+
if (report.summary.failed > 0 && !args.noFail) {
|
|
23903
|
+
process.exit(EXIT_AGENT_GATE);
|
|
23904
|
+
}
|
|
23905
|
+
process.exit(EXIT_SUCCESS);
|
|
23906
|
+
}
|
|
23907
|
+
if (args.subcommand === "goal") {
|
|
23908
|
+
const text3 = await readInput(args);
|
|
23909
|
+
const run = applySelection(normalizeRunFromText(text3, args).run, args);
|
|
23910
|
+
const baseline = resolveBaselineRun(args, run);
|
|
23911
|
+
const report = buildGoal(
|
|
23912
|
+
{
|
|
23913
|
+
run,
|
|
23914
|
+
baseline,
|
|
23915
|
+
requireTags: args.requireTags,
|
|
23916
|
+
requireTickets: args.requireTickets,
|
|
23917
|
+
requireScenarios: args.requireScenarios,
|
|
23918
|
+
enforceNoRegressions: args.noRegressions,
|
|
23919
|
+
enforceRatchet: !args.noRatchet,
|
|
23920
|
+
format: args.goalFormat
|
|
23921
|
+
},
|
|
23922
|
+
{}
|
|
23923
|
+
);
|
|
23924
|
+
console.log(renderGoal(report, args.goalFormat));
|
|
23925
|
+
process.exit(report.met ? EXIT_SUCCESS : EXIT_AGENT_GATE);
|
|
23926
|
+
}
|
|
23927
|
+
if (args.subcommand === "triage") {
|
|
23928
|
+
const text3 = await readInput(args);
|
|
23929
|
+
const run = applySelection(normalizeRunFromText(text3, args).run, args);
|
|
23930
|
+
const baseline = resolveBaselineStatusMap(args, run);
|
|
23931
|
+
const report = buildTriage(
|
|
23932
|
+
{ testCases: run.testCases, baseline, format: args.triageFormat },
|
|
23933
|
+
{}
|
|
23934
|
+
);
|
|
23935
|
+
console.log(renderTriage(report, args.triageFormat));
|
|
23936
|
+
process.exit(EXIT_SUCCESS);
|
|
23937
|
+
}
|
|
23085
23938
|
if (args.subcommand === "watch") {
|
|
23086
23939
|
if (!args.inputFile) {
|
|
23087
23940
|
console.error("Error: watch requires an input file (the raw-run JSON the framework writes).");
|
|
@@ -23460,9 +24313,9 @@ function mapStatus(status) {
|
|
|
23460
24313
|
function parseNameStatus(text2) {
|
|
23461
24314
|
const files = [];
|
|
23462
24315
|
for (const raw of text2.split("\n")) {
|
|
23463
|
-
const
|
|
23464
|
-
if (!
|
|
23465
|
-
const cols =
|
|
24316
|
+
const line2 = raw.trim();
|
|
24317
|
+
if (!line2) continue;
|
|
24318
|
+
const cols = line2.includes(" ") ? line2.split(" ") : line2.split(/\s+/);
|
|
23466
24319
|
const status = cols[0];
|
|
23467
24320
|
if (!status) continue;
|
|
23468
24321
|
const filePath = /^[RC]/i.test(status) && cols.length >= 3 ? cols[cols.length - 1] : cols[1];
|
|
@@ -23969,7 +24822,9 @@ async function runBuildDocs(rawArgs) {
|
|
|
23969
24822
|
options: {
|
|
23970
24823
|
"site-dir": { type: "string" },
|
|
23971
24824
|
openapi: { type: "string" },
|
|
23972
|
-
"no-synthesize-stories": { type: "boolean", default: false }
|
|
24825
|
+
"no-synthesize-stories": { type: "boolean", default: false },
|
|
24826
|
+
"audience-split": { type: "boolean", default: false },
|
|
24827
|
+
baseline: { type: "string" }
|
|
23973
24828
|
},
|
|
23974
24829
|
allowPositionals: true,
|
|
23975
24830
|
strict: true
|
|
@@ -23977,26 +24832,42 @@ async function runBuildDocs(rawArgs) {
|
|
|
23977
24832
|
const rawRunPath = positionals[0];
|
|
23978
24833
|
if (!rawRunPath) {
|
|
23979
24834
|
console.error(
|
|
23980
|
-
`Usage: executable-stories build-docs <raw-run.json> [--site-dir <dir>] [--openapi <spec>]`
|
|
24835
|
+
`Usage: executable-stories build-docs <raw-run.json> [--site-dir <dir>] [--openapi <spec>] [--baseline <prev-story-report.json>] [--audience-split]`
|
|
23981
24836
|
);
|
|
23982
24837
|
return EXIT_USAGE;
|
|
23983
24838
|
}
|
|
24839
|
+
const audienceSplit = values["audience-split"];
|
|
23984
24840
|
try {
|
|
23985
24841
|
const result = await buildDocs({
|
|
23986
24842
|
rawRunPath,
|
|
23987
24843
|
siteDir: values["site-dir"] ?? ".",
|
|
23988
24844
|
openapiPath: values.openapi,
|
|
23989
|
-
synthesizeStories: !values["no-synthesize-stories"]
|
|
24845
|
+
synthesizeStories: !values["no-synthesize-stories"],
|
|
24846
|
+
audienceSplit,
|
|
24847
|
+
baselinePath: values.baseline
|
|
23990
24848
|
});
|
|
23991
24849
|
console.log(`\u2713 Living docs generated in ${result.siteDir}`);
|
|
23992
24850
|
console.log(` \u2022 Explorer data \u2192 public/stories/story-report.json`);
|
|
23993
|
-
console.log(` \u2022
|
|
24851
|
+
console.log(` \u2022 Deep links \u2192 public/stories/scenario-links.json (${result.scenarioLinks})`);
|
|
24852
|
+
if (audienceSplit) {
|
|
24853
|
+
console.log(
|
|
24854
|
+
` \u2022 Story pages \u2192 src/content/docs/stories/{engineer,stakeholder} (engineer: ${result.audiences.engineer}, stakeholder: ${result.audiences.stakeholder})`
|
|
24855
|
+
);
|
|
24856
|
+
} else {
|
|
24857
|
+
console.log(` \u2022 Story pages \u2192 src/content/docs/stories`);
|
|
24858
|
+
}
|
|
23994
24859
|
if (result.bundledAssets > 0) {
|
|
23995
24860
|
console.log(` \u2022 Bundled assets \u2192 public/stories/assets (${result.bundledAssets})`);
|
|
23996
24861
|
}
|
|
23997
24862
|
if (result.apiPages > 0) {
|
|
23998
24863
|
console.log(` \u2022 API pages \u2192 src/content/docs/api (${result.apiPages})`);
|
|
23999
24864
|
}
|
|
24865
|
+
if (result.changes) {
|
|
24866
|
+
const c = result.changes;
|
|
24867
|
+
console.log(
|
|
24868
|
+
` \u2022 What's changed \u2192 src/content/docs/stories/changes.md (+${c.added} added, ${c.regressed} regressed, ${c.fixed} fixed, ${c.removed} removed)`
|
|
24869
|
+
);
|
|
24870
|
+
}
|
|
24000
24871
|
const rel = path15.relative(process.cwd(), result.siteDir) || ".";
|
|
24001
24872
|
console.log(`
|
|
24002
24873
|
Preview: cd ${rel} && npm run dev`);
|
|
@@ -24200,6 +25071,15 @@ function createDefaultCliArgs() {
|
|
|
24200
25071
|
htmlThemePicker: false,
|
|
24201
25072
|
jsonSummary: false,
|
|
24202
25073
|
listFormat: "text",
|
|
25074
|
+
checkFormat: "text",
|
|
25075
|
+
noFail: false,
|
|
25076
|
+
requireTags: [],
|
|
25077
|
+
requireTickets: [],
|
|
25078
|
+
requireScenarios: [],
|
|
25079
|
+
noRegressions: false,
|
|
25080
|
+
noRatchet: false,
|
|
25081
|
+
goalFormat: "text",
|
|
25082
|
+
triageFormat: "text",
|
|
24203
25083
|
notify: "never",
|
|
24204
25084
|
maxFailedTests: 5,
|
|
24205
25085
|
maxHistoryRuns: 10,
|