executable-stories-formatters 0.12.0 → 0.13.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/index.js CHANGED
@@ -526,14 +526,14 @@ var CucumberJsonFormatter = class {
526
526
  duration: 0
527
527
  };
528
528
  }
529
- const statusMap = {
529
+ const statusMap2 = {
530
530
  passed: "passed",
531
531
  failed: "failed",
532
532
  skipped: "skipped",
533
533
  pending: "pending"
534
534
  };
535
535
  const stepResult = {
536
- status: statusMap[result.status] ?? "undefined",
536
+ status: statusMap2[result.status] ?? "undefined",
537
537
  // Duration in nanoseconds (Cucumber uses nanoseconds)
538
538
  duration: result.durationMs * 1e6
539
539
  };
@@ -15717,6 +15717,132 @@ function escapePipe(value) {
15717
15717
  return value.replace(/\|/g, "\\|");
15718
15718
  }
15719
15719
 
15720
+ // src/formatters/traceability-matrix.ts
15721
+ var TraceabilityMatrixFormatter = class {
15722
+ format(run) {
15723
+ const matrix = toTraceabilityMatrix(run);
15724
+ const lines = [];
15725
+ lines.push("# Traceability Matrix");
15726
+ lines.push("");
15727
+ lines.push(`Generated: ${matrix.generatedAt}`);
15728
+ lines.push(`Run: ${matrix.run.startedAt} to ${matrix.run.finishedAt}`);
15729
+ if (matrix.run.branch) lines.push(`Branch: ${matrix.run.branch}`);
15730
+ if (matrix.run.gitSha) lines.push(`Commit: ${matrix.run.gitSha}`);
15731
+ lines.push("");
15732
+ lines.push("| Requirements | Verified | Failing | Scenarios | Untraced |");
15733
+ lines.push("| ---: | ---: | ---: | ---: | ---: |");
15734
+ lines.push(
15735
+ `| ${matrix.summary.requirements} | ${matrix.summary.requirementsVerified} | ${matrix.summary.requirementsFailing} | ${matrix.summary.scenarios} | ${matrix.summary.untracedScenarios} |`
15736
+ );
15737
+ lines.push("");
15738
+ for (const req of matrix.requirements) {
15739
+ const heading2 = req.url ? `[${req.ticket}](${req.url})` : req.ticket;
15740
+ lines.push(`## ${heading2}`);
15741
+ lines.push("");
15742
+ lines.push(`Status: ${renderRequirementStatus(req.status)}`);
15743
+ if (req.covers.length > 0) {
15744
+ lines.push(`Covers: ${req.covers.map((path12) => `\`${path12}\``).join(", ")}`);
15745
+ }
15746
+ lines.push("");
15747
+ lines.push("| Status | Scenario | Source | Covers |");
15748
+ lines.push("| --- | --- | --- | --- |");
15749
+ for (const scenario of req.scenarios) {
15750
+ const source = `${scenario.sourceFile}:${scenario.sourceLine}`;
15751
+ const covers = scenario.covers.length > 0 ? scenario.covers.map((path12) => `\`${path12}\``).join(", ") : "";
15752
+ lines.push(`| ${scenario.status} | ${escapePipe2(scenario.title)} | \`${source}\` | ${covers} |`);
15753
+ }
15754
+ lines.push("");
15755
+ }
15756
+ if (matrix.untraced.length > 0) {
15757
+ lines.push("## Untraced scenarios");
15758
+ lines.push("");
15759
+ lines.push("Behavior with no requirement link. Add a `ticket` to each so it appears against a requirement.");
15760
+ lines.push("");
15761
+ lines.push("| Status | Scenario | Source |");
15762
+ lines.push("| --- | --- | --- |");
15763
+ for (const scenario of matrix.untraced) {
15764
+ const source = `${scenario.sourceFile}:${scenario.sourceLine}`;
15765
+ lines.push(`| ${scenario.status} | ${escapePipe2(scenario.title)} | \`${source}\` |`);
15766
+ }
15767
+ lines.push("");
15768
+ }
15769
+ return lines.join("\n").trimEnd();
15770
+ }
15771
+ };
15772
+ function toTraceabilityMatrix(run) {
15773
+ const sorted = [...run.testCases].sort((a, b) => a.id.localeCompare(b.id));
15774
+ const byTicket = /* @__PURE__ */ new Map();
15775
+ const untraced = [];
15776
+ for (const tc of sorted) {
15777
+ const tickets = tc.story.tickets ?? [];
15778
+ if (tickets.length === 0) {
15779
+ untraced.push({
15780
+ id: tc.id,
15781
+ title: tc.story.scenario,
15782
+ status: tc.status,
15783
+ sourceFile: tc.sourceFile,
15784
+ sourceLine: tc.sourceLine
15785
+ });
15786
+ continue;
15787
+ }
15788
+ for (const ticket of tickets) {
15789
+ const entry = byTicket.get(ticket.id) ?? { url: ticket.url, cases: [] };
15790
+ if (!entry.url && ticket.url) entry.url = ticket.url;
15791
+ entry.cases.push(tc);
15792
+ byTicket.set(ticket.id, entry);
15793
+ }
15794
+ }
15795
+ const requirements = [...byTicket.entries()].sort(([a], [b]) => a.localeCompare(b)).map(([ticket, entry]) => {
15796
+ const scenarios = entry.cases.map((tc) => ({
15797
+ id: tc.id,
15798
+ title: tc.story.scenario,
15799
+ status: tc.status,
15800
+ sourceFile: tc.sourceFile,
15801
+ sourceLine: tc.sourceLine,
15802
+ covers: tc.story.covers ?? []
15803
+ }));
15804
+ const covers = [...new Set(scenarios.flatMap((s) => s.covers))].sort();
15805
+ return { ticket, url: entry.url, status: requirementStatus(entry.cases), scenarios, covers };
15806
+ });
15807
+ return {
15808
+ schemaVersion: "1.0",
15809
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
15810
+ run: {
15811
+ startedAt: new Date(run.startedAtMs).toISOString(),
15812
+ finishedAt: new Date(run.finishedAtMs).toISOString(),
15813
+ gitSha: run.gitSha,
15814
+ branch: run.ci?.branch
15815
+ },
15816
+ summary: {
15817
+ requirements: requirements.length,
15818
+ requirementsVerified: requirements.filter((r) => r.status === "verified").length,
15819
+ requirementsFailing: requirements.filter((r) => r.status === "failing").length,
15820
+ scenarios: run.testCases.length,
15821
+ untracedScenarios: untraced.length
15822
+ },
15823
+ requirements,
15824
+ untraced
15825
+ };
15826
+ }
15827
+ function requirementStatus(cases) {
15828
+ if (cases.some((tc) => tc.status === "failed")) return "failing";
15829
+ if (cases.some((tc) => tc.status === "passed")) return "verified";
15830
+ return "incomplete";
15831
+ }
15832
+ function renderRequirementStatus(status) {
15833
+ switch (status) {
15834
+ case "verified":
15835
+ return "verified (all scenarios passed)";
15836
+ case "failing":
15837
+ return "failing (a scenario failed)";
15838
+ default:
15839
+ return "incomplete (no scenario passed yet)";
15840
+ }
15841
+ }
15842
+ function escapePipe2(value) {
15843
+ return value.replace(/\|/g, "\\|");
15844
+ }
15845
+
15720
15846
  // src/formatters/cucumber-messages/synthesize-feature.ts
15721
15847
  function extractFeatureName(testCases, uri) {
15722
15848
  for (const tc of testCases) {
@@ -20464,6 +20590,280 @@ function collectDocKinds(testCase) {
20464
20590
  return [...kinds].sort();
20465
20591
  }
20466
20592
 
20593
+ // src/scenario-failure.ts
20594
+ function failingScenarioMessage(tc) {
20595
+ const failingStep = tc.stepResults.find((s) => s.status === "failed" && s.errorMessage);
20596
+ return failingStep?.errorMessage ?? tc.errorMessage;
20597
+ }
20598
+
20599
+ // src/check.ts
20600
+ var ICON_PASS = "\u2713";
20601
+ var ICON_FAIL = "\u2717";
20602
+ var ICON_SKIP = "\u2298";
20603
+ var ICON_PENDING = "\u23F3";
20604
+ var ICON_WARN = "\u26A0";
20605
+ function buildCheck(args, _deps = {}) {
20606
+ const { testCases, baseline } = args;
20607
+ const summary = {
20608
+ total: testCases.length,
20609
+ passed: testCases.filter((tc) => tc.status === "passed").length,
20610
+ failed: testCases.filter((tc) => tc.status === "failed").length,
20611
+ skipped: testCases.filter((tc) => tc.status === "skipped").length,
20612
+ pending: testCases.filter((tc) => tc.status === "pending").length
20613
+ };
20614
+ let regressed = 0;
20615
+ let fixed = 0;
20616
+ if (baseline) {
20617
+ for (const tc of testCases) {
20618
+ const before = baseline.get(tc.id);
20619
+ if (before === "passed" && tc.status === "failed") regressed += 1;
20620
+ if (before === "failed" && tc.status === "passed") fixed += 1;
20621
+ }
20622
+ }
20623
+ const failures = testCases.filter((tc) => tc.status === "failed").map((tc) => toFailure(tc, baseline)).sort((a, b) => {
20624
+ if (a.regressed !== b.regressed) return a.regressed ? -1 : 1;
20625
+ return a.location.localeCompare(b.location);
20626
+ });
20627
+ return {
20628
+ summary,
20629
+ failures,
20630
+ regressed,
20631
+ fixed,
20632
+ comparedToBaseline: baseline !== void 0
20633
+ };
20634
+ }
20635
+ function toFailure(tc, baseline) {
20636
+ const failedIndexes = new Set(
20637
+ tc.stepResults.filter((s) => s.status === "failed").map((s) => s.index)
20638
+ );
20639
+ const steps = tc.story.steps.map((step, index) => ({
20640
+ keyword: step.keyword,
20641
+ text: step.text,
20642
+ failed: failedIndexes.has(index)
20643
+ }));
20644
+ return {
20645
+ id: tc.id,
20646
+ scenario: tc.story.scenario,
20647
+ location: `${tc.sourceFile}:${tc.sourceLine}`,
20648
+ steps,
20649
+ errorMessage: failingScenarioMessage(tc),
20650
+ covers: tc.story.covers ?? [],
20651
+ tickets: (tc.story.tickets ?? []).map((t) => t.id),
20652
+ regressed: baseline?.get(tc.id) === "passed"
20653
+ };
20654
+ }
20655
+ function renderCheck(report, format) {
20656
+ return format === "json" ? JSON.stringify(report, null, 2) : renderCheckText(report);
20657
+ }
20658
+ function renderCheckText(report) {
20659
+ const { summary, failures } = report;
20660
+ const headlineParts = [`${ICON_PASS} ${summary.passed} passed`];
20661
+ if (summary.failed > 0) headlineParts.push(`${ICON_FAIL} ${summary.failed} failed`);
20662
+ if (summary.skipped > 0) headlineParts.push(`${ICON_SKIP} ${summary.skipped} skipped`);
20663
+ if (summary.pending > 0) headlineParts.push(`${ICON_PENDING} ${summary.pending} pending`);
20664
+ const headline = `${headlineParts.join(" ")} (${summary.total} scenarios)`;
20665
+ if (failures.length === 0) {
20666
+ const lines2 = [headline];
20667
+ if (report.comparedToBaseline && report.fixed > 0) {
20668
+ lines2.push(`${ICON_PASS} ${report.fixed} fixed since baseline.`);
20669
+ }
20670
+ lines2.push("All scenarios green.");
20671
+ return lines2.join("\n");
20672
+ }
20673
+ const lines = [headline, ""];
20674
+ for (const f of failures) {
20675
+ lines.push(`${ICON_FAIL} ${f.scenario}${f.regressed ? " (regressed)" : ""}`);
20676
+ lines.push(` ${f.location}`);
20677
+ for (const step of f.steps) {
20678
+ const marker = step.failed ? ` ${ICON_FAIL} ` : " ";
20679
+ lines.push(`${marker}${step.keyword} ${step.text}`);
20680
+ }
20681
+ if (f.errorMessage) {
20682
+ const firstLine = f.errorMessage.split("\n")[0];
20683
+ lines.push(` \u2192 ${firstLine}`);
20684
+ }
20685
+ if (f.covers.length > 0) {
20686
+ lines.push(` covers: ${f.covers.join(", ")}`);
20687
+ }
20688
+ if (f.tickets.length > 0) {
20689
+ lines.push(` ticket: ${f.tickets.join(", ")}`);
20690
+ }
20691
+ lines.push("");
20692
+ }
20693
+ if (report.comparedToBaseline) {
20694
+ if (report.regressed > 0) {
20695
+ lines.push(`${ICON_WARN} ${report.regressed} regressed since baseline (was passing).`);
20696
+ }
20697
+ if (report.fixed > 0) {
20698
+ lines.push(`${ICON_PASS} ${report.fixed} fixed since baseline.`);
20699
+ }
20700
+ if (report.regressed === 0 && report.fixed === 0) {
20701
+ lines.push("No status changes vs. baseline.");
20702
+ }
20703
+ }
20704
+ return lines.join("\n").trimEnd();
20705
+ }
20706
+
20707
+ // src/goal.ts
20708
+ var ACTIVE = ["passed", "failed"];
20709
+ function buildGoal(args, _deps = {}) {
20710
+ const { run, baseline } = args;
20711
+ const cases = run.testCases;
20712
+ const selectors = [
20713
+ ...args.requireTags.map((tag) => ({ label: `tag:${tag}`, match: (tc) => tc.tags.includes(tag) })),
20714
+ ...args.requireTickets.map((id) => ({ label: `ticket:${id}`, match: (tc) => (tc.story.tickets ?? []).some((t) => t.id === id) })),
20715
+ ...args.requireScenarios.map((sel) => ({ label: `scenario:${sel}`, match: (tc) => tc.id === sel || tc.story.scenario === sel }))
20716
+ ];
20717
+ const requirements = selectors.length === 0 ? [evaluate("all scenarios", cases)] : selectors.map((s) => evaluate(s.label, cases.filter(s.match)));
20718
+ const regressions = [];
20719
+ if (baseline && args.enforceNoRegressions) {
20720
+ const before = statusMap(baseline);
20721
+ for (const tc of cases) {
20722
+ if (before.get(tc.id) === "passed" && tc.status === "failed") {
20723
+ regressions.push({ id: tc.id, title: tc.story.scenario });
20724
+ }
20725
+ }
20726
+ }
20727
+ const violations = [];
20728
+ if (baseline && args.enforceRatchet) {
20729
+ const current = new Map(cases.map((tc) => [tc.id, tc]));
20730
+ for (const base of baseline.testCases) {
20731
+ const now = current.get(base.id);
20732
+ if (!now) {
20733
+ violations.push({ id: base.id, title: base.story.scenario, kind: "removed", detail: "scenario no longer present" });
20734
+ continue;
20735
+ }
20736
+ if (ACTIVE.includes(base.status) && (now.status === "skipped" || now.status === "pending")) {
20737
+ violations.push({ id: base.id, title: base.story.scenario, kind: "disabled", detail: `${base.status} -> ${now.status}` });
20738
+ }
20739
+ const baseSteps = base.story.steps.length;
20740
+ const nowSteps = now.story.steps.length;
20741
+ if (nowSteps < baseSteps) {
20742
+ violations.push({ id: base.id, title: base.story.scenario, kind: "weakened", detail: `${baseSteps} steps -> ${nowSteps} steps` });
20743
+ }
20744
+ }
20745
+ }
20746
+ const met = requirements.every((r) => r.met) && regressions.length === 0 && violations.length === 0;
20747
+ return {
20748
+ met,
20749
+ requirements,
20750
+ regressions,
20751
+ regressionsEnforced: Boolean(baseline && args.enforceNoRegressions),
20752
+ ratchet: { enforced: Boolean(baseline && args.enforceRatchet), violations }
20753
+ };
20754
+ }
20755
+ function evaluate(selector, matched) {
20756
+ const passed = matched.filter((tc) => tc.status === "passed").length;
20757
+ const failing = matched.filter((tc) => tc.status !== "passed").map((tc) => tc.story.scenario);
20758
+ return {
20759
+ selector,
20760
+ matched: matched.length,
20761
+ passed,
20762
+ failing,
20763
+ met: matched.length > 0 && failing.length === 0
20764
+ };
20765
+ }
20766
+ function statusMap(run) {
20767
+ return new Map(run.testCases.map((tc) => [tc.id, tc.status]));
20768
+ }
20769
+ function renderGoal(report, format) {
20770
+ if (format === "json") return JSON.stringify(report, null, 2);
20771
+ const lines = [`GOAL: ${report.met ? "met" : "not met"}`];
20772
+ for (const req of report.requirements) {
20773
+ if (req.matched === 0) {
20774
+ lines.push(` ${req.selector}: no matching scenario (no proof)`);
20775
+ continue;
20776
+ }
20777
+ const tail = req.failing.length > 0 ? ` (${req.failing.length} failing)` : "";
20778
+ lines.push(` ${req.selector}: ${req.passed}/${req.matched} scenarios pass${tail}`);
20779
+ }
20780
+ if (report.regressionsEnforced) {
20781
+ if (report.regressions.length === 0) {
20782
+ lines.push(" regressions: 0");
20783
+ } else {
20784
+ lines.push(` regressions: ${report.regressions.length} (${report.regressions.map((r) => r.title).join(", ")})`);
20785
+ }
20786
+ }
20787
+ if (report.ratchet.enforced) {
20788
+ if (report.ratchet.violations.length === 0) {
20789
+ lines.push(" ratchet: clean (0 scenarios removed/weakened)");
20790
+ } else {
20791
+ lines.push(` ratchet: ${report.ratchet.violations.length} removed/weakened`);
20792
+ for (const v of report.ratchet.violations) {
20793
+ lines.push(` ${v.kind}: ${v.title} (${v.detail})`);
20794
+ }
20795
+ }
20796
+ }
20797
+ return lines.join("\n");
20798
+ }
20799
+
20800
+ // src/triage.ts
20801
+ function buildTriage(args, _deps = {}) {
20802
+ const { testCases, baseline } = args;
20803
+ const failing = testCases.filter((tc) => tc.status === "failed");
20804
+ const ranked = failing.map((tc) => {
20805
+ const regressed = baseline?.get(tc.id) === "passed";
20806
+ return {
20807
+ tc,
20808
+ regressed,
20809
+ covers: tc.story.covers ?? []
20810
+ };
20811
+ }).sort((a, b) => {
20812
+ if (a.regressed !== b.regressed) return a.regressed ? -1 : 1;
20813
+ const la = `${a.tc.sourceFile}:${a.tc.sourceLine}`;
20814
+ const lb = `${b.tc.sourceFile}:${b.tc.sourceLine}`;
20815
+ return la.localeCompare(lb);
20816
+ });
20817
+ const items = ranked.map((entry, index) => ({
20818
+ rank: index + 1,
20819
+ id: entry.tc.id,
20820
+ scenario: entry.tc.story.scenario,
20821
+ status: entry.tc.status,
20822
+ location: `${entry.tc.sourceFile}:${entry.tc.sourceLine}`,
20823
+ covers: entry.covers,
20824
+ tickets: (entry.tc.story.tickets ?? []).map((t) => t.id),
20825
+ errorMessage: failingScenarioMessage(entry.tc),
20826
+ regressed: entry.regressed,
20827
+ reason: entry.regressed ? "regression" : "failing"
20828
+ }));
20829
+ return {
20830
+ total: testCases.length,
20831
+ failing: failing.length,
20832
+ regressions: items.filter((i) => i.regressed).length,
20833
+ needsCovers: items.filter((i) => i.covers.length === 0).length,
20834
+ items
20835
+ };
20836
+ }
20837
+ function renderTriage(report, format) {
20838
+ if (format === "json") return JSON.stringify(report, null, 2);
20839
+ if (report.items.length === 0) {
20840
+ return "Nothing to triage. No failing scenarios.";
20841
+ }
20842
+ const header = report.regressions > 0 ? `${report.items.length} items to triage (${report.regressions} regression${report.regressions === 1 ? "" : "s"})` : `${report.items.length} items to triage`;
20843
+ const lines = [header, ""];
20844
+ for (const item of report.items) {
20845
+ const tag = item.regressed ? "[regression] " : "";
20846
+ lines.push(`${item.rank}. ${tag}${item.scenario}`);
20847
+ lines.push(` ${item.location}`);
20848
+ if (item.errorMessage) {
20849
+ lines.push(` \u2192 ${item.errorMessage.split("\n")[0]}`);
20850
+ }
20851
+ if (item.covers.length > 0) {
20852
+ lines.push(` fix: ${item.covers.join(", ")}`);
20853
+ } else {
20854
+ lines.push(" fix: (no covers declared \u2014 add `covers` to route this to code)");
20855
+ }
20856
+ if (item.tickets.length > 0) {
20857
+ lines.push(` ticket: ${item.tickets.join(", ")}`);
20858
+ }
20859
+ lines.push("");
20860
+ }
20861
+ if (report.needsCovers > 0) {
20862
+ lines.push(`${report.needsCovers} failing scenario(s) have no covers and can't be routed to code automatically.`);
20863
+ }
20864
+ return lines.join("\n").trimEnd();
20865
+ }
20866
+
20467
20867
  // src/review/conventions.ts
20468
20868
  var CHANGE_TAG_PREFIX = "change:";
20469
20869
  var AUDIENCE_TAG_PREFIX = "audience:";
@@ -21265,6 +21665,7 @@ var FORMAT_EXTENSIONS = {
21265
21665
  "behavior-manifest-json": ".behavior-manifest.json",
21266
21666
  markdown: ".md",
21267
21667
  "release-manifest": ".release-manifest.md",
21668
+ "traceability-matrix": ".traceability-matrix.md",
21268
21669
  html: ".html",
21269
21670
  "cucumber-html": ".cucumber.html",
21270
21671
  junit: ".junit.xml",
@@ -21691,6 +22092,10 @@ var ReportGenerator = class {
21691
22092
  const formatter = new ReleaseManifestFormatter();
21692
22093
  return formatter.format(run);
21693
22094
  }
22095
+ case "traceability-matrix": {
22096
+ const formatter = new TraceabilityMatrixFormatter();
22097
+ return formatter.format(run);
22098
+ }
21694
22099
  case "story-report-json": {
21695
22100
  const formatter = new StoryReportJsonFormatter({
21696
22101
  pretty: this.options.storyReportJson.pretty
@@ -21770,12 +22175,16 @@ export {
21770
22175
  STORY_REPORT_SCHEMA_VERSION,
21771
22176
  ScenarioIndexJsonFormatter,
21772
22177
  StoryReportJsonFormatter,
22178
+ TraceabilityMatrixFormatter,
21773
22179
  adaptJestRun,
21774
22180
  adaptPlaywrightRun,
21775
22181
  adaptVitestRun,
21776
22182
  assertValidRun,
22183
+ buildCheck,
22184
+ buildGoal,
21777
22185
  buildHtmlDocEntry,
21778
22186
  buildReview,
22187
+ buildTriage,
21779
22188
  bundleAssets,
21780
22189
  calculateFlakiness,
21781
22190
  calculateStability,
@@ -21825,6 +22234,9 @@ export {
21825
22234
  readPackageVersion,
21826
22235
  recordDeployment,
21827
22236
  regenerateArtifacts,
22237
+ renderCheck,
22238
+ renderGoal,
22239
+ renderTriage,
21828
22240
  resolveAttachment,
21829
22241
  resolveAttachments,
21830
22242
  resolveTheme,
@@ -21846,6 +22258,7 @@ export {
21846
22258
  toReleaseManifest,
21847
22259
  toScenarioIndex,
21848
22260
  toStoryReport,
22261
+ toTraceabilityMatrix,
21849
22262
  tryGetActiveOtelContext,
21850
22263
  updateHistory,
21851
22264
  validateCanonicalRun