executable-stories-formatters 0.7.14 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -3,7 +3,7 @@
3
3
  // src/cli.ts
4
4
  import { parseArgs } from "util";
5
5
  import * as fs8 from "fs";
6
- import * as path8 from "path";
6
+ import * as path9 from "path";
7
7
 
8
8
  // src/validation/schema-validator.ts
9
9
  import Ajv from "ajv/dist/2020.js";
@@ -492,17 +492,17 @@ function validateRawRun(data) {
492
492
  return { valid: true, errors: [] };
493
493
  }
494
494
  const errors = (validate.errors ?? []).map((err) => {
495
- const path9 = err.instancePath || "/";
495
+ const path10 = err.instancePath || "/";
496
496
  const message = err.message ?? "unknown error";
497
497
  if (err.keyword === "additionalProperties") {
498
498
  const extra = err.params.additionalProperty;
499
- return `${path9}: ${message} \u2014 '${extra}'`;
499
+ return `${path10}: ${message} \u2014 '${extra}'`;
500
500
  }
501
501
  if (err.keyword === "enum") {
502
502
  const allowed = err.params.allowedValues;
503
- return `${path9}: ${message} \u2014 allowed: ${JSON.stringify(allowed)}`;
503
+ return `${path10}: ${message} \u2014 allowed: ${JSON.stringify(allowed)}`;
504
504
  }
505
- return `${path9}: ${message}`;
505
+ return `${path10}: ${message}`;
506
506
  });
507
507
  return { valid: false, errors };
508
508
  }
@@ -976,7 +976,7 @@ ${result.errors.join("\n")}`);
976
976
 
977
977
  // src/index.ts
978
978
  import "fs";
979
- import * as path6 from "path";
979
+ import * as path7 from "path";
980
980
  import * as fsPromises from "fs/promises";
981
981
 
982
982
  // src/converters/acl/lines.ts
@@ -1355,9 +1355,298 @@ ${doc.markdown}`,
1355
1355
  }
1356
1356
  };
1357
1357
 
1358
+ // src/converters/story-report.ts
1359
+ import { posix as path2 } from "path";
1360
+
1361
+ // src/types/story-report.ts
1362
+ var STORY_REPORT_SCHEMA_VERSION = "1.0";
1363
+
1364
+ // src/converters/story-report.ts
1365
+ function reportSlug(text2) {
1366
+ return text2.toLowerCase().replace(/[/\\.]+/g, "-").replace(/[^\w\s-]/g, "").replace(/[\s_]+/g, "-").replace(/-+/g, "-").replace(/^-+|-+$/g, "");
1367
+ }
1368
+ function toRelativeSourceFile(sourceFile, projectRoot) {
1369
+ if (!sourceFile) return sourceFile;
1370
+ const normalized = sourceFile.split(path2.sep).join("/");
1371
+ const root = projectRoot.split(path2.sep).join("/").replace(/\/$/, "");
1372
+ if (root && normalized.startsWith(root + "/")) return normalized.slice(root.length + 1);
1373
+ return normalized;
1374
+ }
1375
+ function fileBasenameTitle(sourceFile) {
1376
+ const base = sourceFile.split("/").pop() ?? sourceFile;
1377
+ return base.replace(/\.(story\.)?(test|spec)\.[tj]sx?$/, "").replace(/\.[tj]sx?$/, "");
1378
+ }
1379
+ function emptySummary() {
1380
+ return { total: 0, passed: 0, failed: 0, skipped: 0, pending: 0, durationMs: 0 };
1381
+ }
1382
+ function addToSummary(summary, status, durationMs) {
1383
+ summary.total += 1;
1384
+ summary[status] += 1;
1385
+ summary.durationMs += durationMs;
1386
+ }
1387
+ function isKeyword(value) {
1388
+ return value === "Given" || value === "When" || value === "Then" || value === "And" || value === "But";
1389
+ }
1390
+ function copyDocEntries(entries) {
1391
+ if (!entries || entries.length === 0) return [];
1392
+ return entries.map(copyDocEntry);
1393
+ }
1394
+ function copyDocEntry(entry) {
1395
+ const children = entry.children ? { children: copyDocEntries(entry.children) } : {};
1396
+ switch (entry.kind) {
1397
+ case "note":
1398
+ return { kind: "note", text: entry.text, phase: entry.phase, ...children };
1399
+ case "tag":
1400
+ return { kind: "tag", names: [...entry.names], phase: entry.phase, ...children };
1401
+ case "kv":
1402
+ return { kind: "kv", label: entry.label, value: entry.value, phase: entry.phase, ...children };
1403
+ case "code":
1404
+ return {
1405
+ kind: "code",
1406
+ label: entry.label,
1407
+ content: entry.content,
1408
+ ...entry.lang ? { lang: entry.lang } : {},
1409
+ phase: entry.phase,
1410
+ ...children
1411
+ };
1412
+ case "table":
1413
+ return {
1414
+ kind: "table",
1415
+ label: entry.label,
1416
+ columns: [...entry.columns],
1417
+ rows: entry.rows.map((r) => [...r]),
1418
+ phase: entry.phase,
1419
+ ...children
1420
+ };
1421
+ case "link":
1422
+ return { kind: "link", label: entry.label, url: entry.url, phase: entry.phase, ...children };
1423
+ case "section":
1424
+ return { kind: "section", title: entry.title, markdown: entry.markdown, phase: entry.phase, ...children };
1425
+ case "mermaid":
1426
+ return {
1427
+ kind: "mermaid",
1428
+ code: entry.code,
1429
+ ...entry.title ? { title: entry.title } : {},
1430
+ phase: entry.phase,
1431
+ ...children
1432
+ };
1433
+ case "screenshot":
1434
+ return {
1435
+ kind: "screenshot",
1436
+ path: entry.path,
1437
+ ...entry.alt ? { alt: entry.alt } : {},
1438
+ phase: entry.phase,
1439
+ ...children
1440
+ };
1441
+ case "custom":
1442
+ return {
1443
+ kind: "custom",
1444
+ type: entry.type,
1445
+ data: entry.data,
1446
+ phase: entry.phase,
1447
+ ...children
1448
+ };
1449
+ }
1450
+ }
1451
+ function buildStep(args) {
1452
+ const step = {
1453
+ id: `${args.scenarioId}--step-${args.index}`,
1454
+ index: args.index,
1455
+ keyword: args.keyword,
1456
+ text: args.text,
1457
+ status: args.status,
1458
+ durationMs: args.durationMs,
1459
+ docEntries: args.docEntries
1460
+ };
1461
+ if (args.errorMessage !== void 0) step.errorMessage = args.errorMessage;
1462
+ if (args.mode !== void 0) step.mode = args.mode;
1463
+ return step;
1464
+ }
1465
+ function buildSteps(scenarioId, tc) {
1466
+ const declared = tc.story.steps ?? [];
1467
+ const results = tc.stepResults;
1468
+ const max = Math.max(declared.length, results.length);
1469
+ const steps = [];
1470
+ for (let i = 0; i < max; i++) {
1471
+ const decl = declared[i];
1472
+ const res = results[i];
1473
+ const keywordSource = decl?.keyword;
1474
+ const keyword = keywordSource && isKeyword(keywordSource) ? keywordSource : "Given";
1475
+ const text2 = decl?.text ?? "";
1476
+ const status = res?.status ?? "pending";
1477
+ const durationMs = res?.durationMs ?? decl?.durationMs ?? 0;
1478
+ const docEntries = copyDocEntries(decl?.docs);
1479
+ steps.push(buildStep({
1480
+ scenarioId,
1481
+ index: i,
1482
+ keyword,
1483
+ text: text2,
1484
+ status,
1485
+ durationMs,
1486
+ ...res?.errorMessage !== void 0 ? { errorMessage: res.errorMessage } : {},
1487
+ ...decl?.mode !== void 0 ? { mode: decl.mode } : {},
1488
+ docEntries
1489
+ }));
1490
+ }
1491
+ return steps;
1492
+ }
1493
+ function buildAttachments(tc) {
1494
+ return tc.attachments.map((a) => ({
1495
+ name: a.name,
1496
+ mediaType: a.mediaType,
1497
+ body: a.body,
1498
+ contentEncoding: a.contentEncoding
1499
+ }));
1500
+ }
1501
+ function buildScenario(tc, featureId) {
1502
+ const titleRaw = tc.story.scenario?.trim() || "(untitled scenario)";
1503
+ const id = `${featureId}--${reportSlug(titleRaw) || `case-${tc.id}`}`;
1504
+ const steps = buildSteps(id, tc);
1505
+ const scenario = {
1506
+ id,
1507
+ title: titleRaw,
1508
+ status: tc.status,
1509
+ durationMs: tc.durationMs,
1510
+ tags: [...tc.tags],
1511
+ retry: tc.retry,
1512
+ retries: tc.retries,
1513
+ docEntries: copyDocEntries(tc.story.docs),
1514
+ steps,
1515
+ attachments: buildAttachments(tc)
1516
+ };
1517
+ if (tc.sourceLine && tc.sourceLine > 0) scenario.sourceLine = tc.sourceLine;
1518
+ if (tc.errorMessage !== void 0) scenario.errorMessage = tc.errorMessage;
1519
+ if (tc.errorStack !== void 0) scenario.errorStack = tc.errorStack;
1520
+ const tickets = tc.story.tickets;
1521
+ if (tickets && tickets.length > 0) {
1522
+ scenario.tickets = tickets.map((t) => t.url ? { id: t.id, url: t.url } : { id: t.id });
1523
+ }
1524
+ return scenario;
1525
+ }
1526
+ function deriveFeatureTitle(group, relSourceFile) {
1527
+ for (const tc of group) {
1528
+ const head = tc.titlePath?.[0];
1529
+ if (head && head.trim()) return head.trim();
1530
+ }
1531
+ return fileBasenameTitle(relSourceFile);
1532
+ }
1533
+ function compareScenarios(a, b) {
1534
+ const aLine = a.sourceLine ?? Number.POSITIVE_INFINITY;
1535
+ const bLine = b.sourceLine ?? Number.POSITIVE_INFINITY;
1536
+ if (aLine !== bLine) return aLine - bLine;
1537
+ return a.title.localeCompare(b.title);
1538
+ }
1539
+ function buildFeature(relSourceFile, group) {
1540
+ const id = `feature-${reportSlug(relSourceFile.replace(/\.[^.]+$/, "")) || "untitled"}`;
1541
+ const title = deriveFeatureTitle(group, relSourceFile);
1542
+ const summary = emptySummary();
1543
+ const scenarios = [];
1544
+ for (const tc of group) {
1545
+ const scenario = buildScenario(tc, id);
1546
+ scenarios.push(scenario);
1547
+ addToSummary(summary, scenario.status, scenario.durationMs);
1548
+ }
1549
+ scenarios.sort(compareScenarios);
1550
+ return { id, title, sourceFile: relSourceFile, summary, scenarios };
1551
+ }
1552
+ function ensureUniqueFeatureIds(features) {
1553
+ const seen = /* @__PURE__ */ new Map();
1554
+ for (const f of features) {
1555
+ const count = seen.get(f.id) ?? 0;
1556
+ if (count > 0) f.id = `${f.id}-${count + 1}`;
1557
+ seen.set(f.id, count + 1);
1558
+ }
1559
+ }
1560
+ function ensureUniqueScenarioIds(feature) {
1561
+ const seen = /* @__PURE__ */ new Map();
1562
+ for (const s of feature.scenarios) {
1563
+ const count = seen.get(s.id) ?? 0;
1564
+ if (count > 0) {
1565
+ const newId = `${s.id}-${count + 1}`;
1566
+ for (const step of s.steps) {
1567
+ step.id = step.id.replace(s.id, newId);
1568
+ }
1569
+ s.id = newId;
1570
+ }
1571
+ seen.set(s.id, count + 1);
1572
+ }
1573
+ }
1574
+ function toStoryReport(run) {
1575
+ const groups = /* @__PURE__ */ new Map();
1576
+ for (const tc of run.testCases) {
1577
+ const rel = toRelativeSourceFile(tc.sourceFile, run.projectRoot);
1578
+ const existing = groups.get(rel);
1579
+ if (existing) existing.push(tc);
1580
+ else groups.set(rel, [tc]);
1581
+ }
1582
+ const features = [];
1583
+ for (const [rel, group] of groups) {
1584
+ features.push(buildFeature(rel, group));
1585
+ }
1586
+ features.sort((a, b) => a.title.localeCompare(b.title));
1587
+ ensureUniqueFeatureIds(features);
1588
+ for (const f of features) ensureUniqueScenarioIds(f);
1589
+ const summary = emptySummary();
1590
+ for (const f of features) {
1591
+ summary.total += f.summary.total;
1592
+ summary.passed += f.summary.passed;
1593
+ summary.failed += f.summary.failed;
1594
+ summary.skipped += f.summary.skipped;
1595
+ summary.pending += f.summary.pending;
1596
+ summary.durationMs += f.summary.durationMs;
1597
+ }
1598
+ const report = {
1599
+ schemaVersion: STORY_REPORT_SCHEMA_VERSION,
1600
+ runId: run.runId,
1601
+ startedAtMs: run.startedAtMs,
1602
+ finishedAtMs: run.finishedAtMs,
1603
+ durationMs: run.durationMs,
1604
+ projectRoot: run.projectRoot,
1605
+ summary,
1606
+ features
1607
+ };
1608
+ if (run.packageVersion) report.packageVersion = run.packageVersion;
1609
+ if (run.gitSha) report.gitSha = run.gitSha;
1610
+ if (run.ci) {
1611
+ const ci = { name: run.ci.name };
1612
+ if (run.ci.url) ci.url = run.ci.url;
1613
+ if (run.ci.buildNumber) ci.buildNumber = run.ci.buildNumber;
1614
+ if (run.ci.branch) ci.branch = run.ci.branch;
1615
+ if (run.ci.commitSha) ci.commitSha = run.ci.commitSha;
1616
+ if (run.ci.prNumber) ci.prNumber = run.ci.prNumber;
1617
+ report.ci = ci;
1618
+ }
1619
+ if (run.coverage) {
1620
+ const cov = {};
1621
+ if (run.coverage.linesPct !== void 0) cov.linesPct = run.coverage.linesPct;
1622
+ if (run.coverage.branchesPct !== void 0) cov.branchesPct = run.coverage.branchesPct;
1623
+ if (run.coverage.functionsPct !== void 0) cov.functionsPct = run.coverage.functionsPct;
1624
+ if (run.coverage.statementsPct !== void 0) cov.statementsPct = run.coverage.statementsPct;
1625
+ report.coverage = cov;
1626
+ }
1627
+ return report;
1628
+ }
1629
+
1630
+ // src/formatters/story-report-json.ts
1631
+ var StoryReportJsonFormatter = class {
1632
+ options;
1633
+ constructor(options = {}) {
1634
+ this.options = {
1635
+ pretty: options.pretty ?? true
1636
+ };
1637
+ }
1638
+ toReport(run) {
1639
+ return toStoryReport(run);
1640
+ }
1641
+ format(run) {
1642
+ const report = toStoryReport(run);
1643
+ return this.options.pretty ? JSON.stringify(report, null, 2) : JSON.stringify(report);
1644
+ }
1645
+ };
1646
+
1358
1647
  // src/formatters/html/renderers/index.ts
1359
1648
  import * as fs2 from "fs";
1360
- import * as path2 from "path";
1649
+ import * as path3 from "path";
1361
1650
 
1362
1651
  // src/formatters/html/template.ts
1363
1652
  var JS_THEME = `
@@ -1708,11 +1997,53 @@ function initKeyboardShortcuts() {
1708
1997
  });
1709
1998
  }
1710
1999
 
1711
- // Collapse/expand functionality
2000
+ // Collapse/expand functionality (persisted in localStorage)
2001
+ var COLLAPSE_KEY = 'es-collapsed-ids';
2002
+
2003
+ function loadCollapseState() {
2004
+ try {
2005
+ var raw = localStorage.getItem(COLLAPSE_KEY);
2006
+ return raw ? new Set(JSON.parse(raw)) : new Set();
2007
+ } catch (e) {
2008
+ return new Set();
2009
+ }
2010
+ }
2011
+
2012
+ function saveCollapseState(set) {
2013
+ try {
2014
+ localStorage.setItem(COLLAPSE_KEY, JSON.stringify(Array.from(set)));
2015
+ } catch (e) { /* quota or disabled */ }
2016
+ }
2017
+
2018
+ function persistCollapse(container) {
2019
+ if (!container || !container.id) return;
2020
+ var state = loadCollapseState();
2021
+ if (container.classList.contains('collapsed')) {
2022
+ state.add(container.id);
2023
+ } else {
2024
+ state.delete(container.id);
2025
+ }
2026
+ saveCollapseState(state);
2027
+ }
2028
+
1712
2029
  function toggleCollapse(header, container) {
1713
2030
  container?.classList.toggle('collapsed');
1714
2031
  const isCollapsed = container?.classList.contains('collapsed');
1715
2032
  header.setAttribute('aria-expanded', !isCollapsed);
2033
+ persistCollapse(container);
2034
+ }
2035
+
2036
+ function restoreCollapseState() {
2037
+ var state = loadCollapseState();
2038
+ if (state.size === 0) return;
2039
+ state.forEach(function(id) {
2040
+ var el = document.getElementById(id);
2041
+ if (!el) return;
2042
+ if (!el.classList.contains('feature') && !el.classList.contains('scenario')) return;
2043
+ el.classList.add('collapsed');
2044
+ var header = el.querySelector('.feature-header, .scenario-header');
2045
+ if (header) header.setAttribute('aria-expanded', 'false');
2046
+ });
1716
2047
  }
1717
2048
 
1718
2049
  function initCollapse() {
@@ -1759,14 +2090,20 @@ function expandAll() {
1759
2090
  const header = el.querySelector('.feature-header, .scenario-header, .trace-view-header');
1760
2091
  header?.setAttribute('aria-expanded', 'true');
1761
2092
  });
2093
+ saveCollapseState(new Set());
1762
2094
  }
1763
2095
 
1764
2096
  function collapseAll() {
2097
+ var ids = new Set();
1765
2098
  document.querySelectorAll('.feature, .scenario, .trace-view').forEach(el => {
1766
2099
  el.classList.add('collapsed');
1767
2100
  const header = el.querySelector('.feature-header, .scenario-header, .trace-view-header');
1768
2101
  header?.setAttribute('aria-expanded', 'false');
2102
+ if (el.id && (el.classList.contains('feature') || el.classList.contains('scenario'))) {
2103
+ ids.add(el.id);
2104
+ }
1769
2105
  });
2106
+ saveCollapseState(ids);
1770
2107
  }
1771
2108
 
1772
2109
  // Detail level toggle
@@ -1893,6 +2230,68 @@ function copyScenarioAsMarkdown(scenarioId) {
1893
2230
  });
1894
2231
  }
1895
2232
 
2233
+ // Copy scenario as Claude-ready prompt (failure investigation context)
2234
+ function copyScenarioAsPrompt(scenarioId) {
2235
+ var scenario = document.getElementById(scenarioId);
2236
+ if (!scenario) return;
2237
+
2238
+ var feature = scenario.closest('.feature');
2239
+ var featureTitle = feature ? (feature.querySelector('.feature-title') || {}).textContent || '' : '';
2240
+ var title = (scenario.querySelector('.scenario-name') || {}).textContent || '';
2241
+ var statusEl = scenario.querySelector('.status-icon');
2242
+ var status = statusEl && statusEl.classList.contains('status-passed') ? 'passed' :
2243
+ statusEl && statusEl.classList.contains('status-failed') ? 'failed' :
2244
+ statusEl && statusEl.classList.contains('status-skipped') ? 'skipped' : 'pending';
2245
+ var sourceLink = scenario.querySelector('.source-link');
2246
+ var source = sourceLink ? sourceLink.textContent || '' : '';
2247
+ var tags = Array.from(scenario.querySelectorAll('.scenario-meta .tag')).map(function(t) { return t.textContent.trim(); });
2248
+ var steps = scenario.querySelectorAll('.step, .step.continuation');
2249
+
2250
+ var lines = [];
2251
+ lines.push('I need help investigating a failing executable-story scenario.');
2252
+ lines.push('');
2253
+ if (featureTitle.trim()) lines.push('Feature: ' + featureTitle.trim());
2254
+ lines.push('Scenario: ' + title.trim());
2255
+ lines.push('Status: ' + status);
2256
+ if (source.trim()) lines.push('Source: ' + source.trim());
2257
+ if (tags.length > 0) lines.push('Tags: ' + tags.join(', '));
2258
+ lines.push('');
2259
+ lines.push('Steps:');
2260
+ steps.forEach(function(step) {
2261
+ var keyword = step.getAttribute('data-keyword') || '';
2262
+ var text = step.getAttribute('data-text') || '';
2263
+ var stepStatusEl = step.querySelector('.step-status');
2264
+ var marker = ' ';
2265
+ if (stepStatusEl) {
2266
+ if (stepStatusEl.classList.contains('status-failed')) marker = 'x ';
2267
+ else if (stepStatusEl.classList.contains('status-passed')) marker = '+ ';
2268
+ else if (stepStatusEl.classList.contains('status-skipped')) marker = '- ';
2269
+ }
2270
+ lines.push(marker + keyword + ' ' + text);
2271
+ });
2272
+
2273
+ var errorBox = scenario.querySelector('.error-message');
2274
+ if (errorBox) {
2275
+ lines.push('');
2276
+ lines.push('Error:');
2277
+ lines.push((errorBox.textContent || '').trim());
2278
+ }
2279
+ var stackBox = scenario.querySelector('.error-stack');
2280
+ if (stackBox) {
2281
+ lines.push('');
2282
+ lines.push('Stack:');
2283
+ lines.push((stackBox.textContent || '').trim());
2284
+ }
2285
+
2286
+ lines.push('');
2287
+ lines.push('Please read the source file, identify the root cause, and propose a fix.');
2288
+
2289
+ var text = lines.join('\\n');
2290
+ navigator.clipboard.writeText(text).then(function() {
2291
+ showCopyToast(scenario);
2292
+ });
2293
+ }
2294
+
1896
2295
  // Hash scroll on load
1897
2296
  function initHashScroll() {
1898
2297
  if (!location.hash) return;
@@ -2062,6 +2461,7 @@ function generateScript(options) {
2062
2461
  initCalls.push("initStatusFilter();");
2063
2462
  initCalls.push("initKeyboardShortcuts();");
2064
2463
  initCalls.push("initCollapse();");
2464
+ initCalls.push("restoreCollapseState();");
2065
2465
  initCalls.push("initDetailLevel();");
2066
2466
  initCalls.push("applyAllFilters();");
2067
2467
  initCalls.push("initHashScroll();");
@@ -2166,6 +2566,80 @@ function escapeHtml(str) {
2166
2566
  return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#039;");
2167
2567
  }
2168
2568
 
2569
+ // src/theme/tokens.ts
2570
+ var ES_THEME_TOKENS_CSS = `
2571
+ :root,
2572
+ [data-theme="light"] {
2573
+ --es-color-bg: #ffffff;
2574
+ --es-color-fg: #111827;
2575
+ --es-color-muted: #6b7280;
2576
+ --es-color-border: #e5e7eb;
2577
+ --es-color-surface: #f9fafb;
2578
+ --es-color-link: #2563eb;
2579
+ --es-color-passed: #16a34a;
2580
+ --es-color-failed: #dc2626;
2581
+ --es-color-skipped: #9ca3af;
2582
+ --es-color-pending: #d97706;
2583
+ --es-color-passed-bg: #f0fdf4;
2584
+ --es-color-failed-bg: #fef2f2;
2585
+ --es-color-skipped-bg: #f3f4f6;
2586
+ --es-color-pending-bg: #fffbeb;
2587
+ --es-font-body: -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif;
2588
+ --es-font-mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, monospace;
2589
+ --es-size-base: 1rem;
2590
+ --es-size-sm: 0.875rem;
2591
+ --es-size-xs: 0.75rem;
2592
+ --es-size-h1: 1.875rem;
2593
+ --es-size-h2: 1.5rem;
2594
+ --es-size-h3: 1.25rem;
2595
+ --es-space-1: 0.25rem;
2596
+ --es-space-2: 0.5rem;
2597
+ --es-space-3: 0.75rem;
2598
+ --es-space-4: 1rem;
2599
+ --es-space-6: 1.5rem;
2600
+ --es-space-8: 2rem;
2601
+ --es-radius: 0.5rem;
2602
+ --es-line: 1.6;
2603
+ --es-measure: 72ch;
2604
+ }
2605
+
2606
+ @media (prefers-color-scheme: dark) {
2607
+ :root {
2608
+ --es-color-bg: #0b0f17;
2609
+ --es-color-fg: #e5e7eb;
2610
+ --es-color-muted: #9ca3af;
2611
+ --es-color-border: #1f2937;
2612
+ --es-color-surface: #111827;
2613
+ --es-color-link: #60a5fa;
2614
+ --es-color-passed: #4ade80;
2615
+ --es-color-failed: #f87171;
2616
+ --es-color-skipped: #6b7280;
2617
+ --es-color-pending: #fbbf24;
2618
+ --es-color-passed-bg: rgba(74, 222, 128, 0.08);
2619
+ --es-color-failed-bg: rgba(248, 113, 113, 0.08);
2620
+ --es-color-skipped-bg: rgba(107, 114, 128, 0.08);
2621
+ --es-color-pending-bg: rgba(251, 191, 36, 0.08);
2622
+ }
2623
+ }
2624
+
2625
+ [data-theme="dark"] {
2626
+ --es-color-bg: #0b0f17;
2627
+ --es-color-fg: #e5e7eb;
2628
+ --es-color-muted: #9ca3af;
2629
+ --es-color-border: #1f2937;
2630
+ --es-color-surface: #111827;
2631
+ --es-color-link: #60a5fa;
2632
+ --es-color-passed: #4ade80;
2633
+ --es-color-failed: #f87171;
2634
+ --es-color-skipped: #6b7280;
2635
+ --es-color-pending: #fbbf24;
2636
+ --es-color-passed-bg: rgba(74, 222, 128, 0.08);
2637
+ --es-color-failed-bg: rgba(248, 113, 113, 0.08);
2638
+ --es-color-skipped-bg: rgba(107, 114, 128, 0.08);
2639
+ --es-color-pending-bg: rgba(251, 191, 36, 0.08);
2640
+ }
2641
+ `.trim();
2642
+
2169
2643
  // src/formatters/html/styles.ts
2170
2644
  var CSS_STYLES = `
2171
2645
  /* ============================================================================
@@ -2173,6 +2647,14 @@ var CSS_STYLES = `
2173
2647
  ============================================================================ */
2174
2648
  @import url('https://fonts.googleapis.com/css2?family=IBM+Plex+Sans:wght@400;500;600;700&family=IBM+Plex+Mono:wght@400;500&display=swap');
2175
2649
 
2650
+ /* ============================================================================
2651
+ executable-stories canonical tokens (--es-*).
2652
+ Shared with executable-stories-react. Override on :root or any ancestor of
2653
+ the report to re-color both the standalone HTML and the React component.
2654
+ ============================================================================ */
2655
+ ${ES_THEME_TOKENS_CSS}
2656
+
2657
+
2176
2658
  /* ============================================================================
2177
2659
  CSS Custom Properties - Light Mode (Default)
2178
2660
  Cucumber-branded shadcn/ui base theme
@@ -4061,6 +4543,33 @@ body {
4061
4543
  color: var(--primary);
4062
4544
  }
4063
4545
 
4546
+ .copy-prompt-btn {
4547
+ display: inline-flex;
4548
+ align-items: center;
4549
+ justify-content: center;
4550
+ width: 1.5rem;
4551
+ height: 1.5rem;
4552
+ border: none;
4553
+ background: none;
4554
+ color: var(--muted-foreground);
4555
+ cursor: pointer;
4556
+ opacity: 0.6;
4557
+ transition: opacity 0.15s ease, transform 0.15s ease;
4558
+ font-size: 0.95rem;
4559
+ padding: 0;
4560
+ flex-shrink: 0;
4561
+ }
4562
+
4563
+ .scenario-header:hover .copy-prompt-btn,
4564
+ .copy-prompt-btn:focus-visible {
4565
+ opacity: 1;
4566
+ }
4567
+
4568
+ .copy-prompt-btn:hover {
4569
+ color: var(--primary);
4570
+ transform: scale(1.15);
4571
+ }
4572
+
4064
4573
  /* ============================================================================
4065
4574
  Keyboard Navigation
4066
4575
  ============================================================================ */
@@ -4298,6 +4807,82 @@ a.toc-title:hover {
4298
4807
  outline-offset: 2px;
4299
4808
  }
4300
4809
 
4810
+ /* ============================================================================
4811
+ Mobile responsive refinements
4812
+ ============================================================================ */
4813
+ @media (max-width: 640px) {
4814
+ .container {
4815
+ padding: 0.875rem;
4816
+ }
4817
+
4818
+ .header {
4819
+ flex-direction: column;
4820
+ align-items: stretch;
4821
+ gap: 0.75rem;
4822
+ }
4823
+
4824
+ .header-actions {
4825
+ flex-wrap: wrap;
4826
+ gap: 0.5rem;
4827
+ }
4828
+
4829
+ .search-input {
4830
+ width: 100%;
4831
+ flex: 1 1 100%;
4832
+ min-width: 0;
4833
+ }
4834
+
4835
+ .header h1 {
4836
+ font-size: 1.25rem;
4837
+ }
4838
+
4839
+ .scenario-header,
4840
+ .feature-header {
4841
+ flex-wrap: wrap;
4842
+ gap: 0.5rem;
4843
+ }
4844
+
4845
+ .scenario-meta {
4846
+ flex-wrap: wrap;
4847
+ }
4848
+
4849
+ .scenario-actions {
4850
+ flex-wrap: wrap;
4851
+ }
4852
+
4853
+ /* Always-visible action buttons on touch (no hover) */
4854
+ .copy-scenario-btn,
4855
+ .copy-prompt-btn,
4856
+ .permalink-anchor {
4857
+ opacity: 1 !important;
4858
+ }
4859
+
4860
+ .summary-card {
4861
+ padding: 0.75rem 0.875rem;
4862
+ }
4863
+
4864
+ .summary-card .value {
4865
+ font-size: 1.5rem;
4866
+ }
4867
+
4868
+ .tag-bar {
4869
+ overflow-x: auto;
4870
+ -webkit-overflow-scrolling: touch;
4871
+ }
4872
+
4873
+ .shortcuts-overlay {
4874
+ padding: 1rem;
4875
+ }
4876
+ }
4877
+
4878
+ @media (hover: none) and (pointer: coarse) {
4879
+ .copy-scenario-btn,
4880
+ .copy-prompt-btn,
4881
+ .permalink-anchor {
4882
+ opacity: 1;
4883
+ }
4884
+ }
4885
+
4301
4886
  `;
4302
4887
 
4303
4888
  // src/formatters/html/themes/default.ts
@@ -14016,6 +14601,7 @@ function renderScenario(args, deps) {
14016
14601
  </div>
14017
14602
  <div class="scenario-actions">
14018
14603
  <button class="copy-scenario-btn" onclick="copyScenarioAsMarkdown('scenario-${tc.id}')" aria-label="Copy scenario as markdown" title="Copy as Markdown"><svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg></button>
14604
+ ${tc.status === "failed" ? `<button class="copy-prompt-btn" onclick="copyScenarioAsPrompt('scenario-${tc.id}')" aria-label="Copy as Claude prompt" title="Copy as AI prompt">\u2728</button>` : ""}
14019
14605
  <button class="permalink-anchor" onclick="copyPermalink('scenario-${tc.id}')" aria-label="Copy link to scenario" title="Copy link">#</button>
14020
14606
  <span class="scenario-duration">${duration}</span>
14021
14607
  </div>
@@ -14415,7 +15001,7 @@ var SCREENSHOT_MIME_BY_EXT = {
14415
15001
  };
14416
15002
  function readScreenshotAsDataUri(filePath) {
14417
15003
  try {
14418
- const ext = path2.extname(filePath).slice(1).toLowerCase();
15004
+ const ext = path3.extname(filePath).slice(1).toLowerCase();
14419
15005
  const mime = SCREENSHOT_MIME_BY_EXT[ext];
14420
15006
  if (!mime) return void 0;
14421
15007
  if (!fs2.existsSync(filePath)) return void 0;
@@ -15892,8 +16478,8 @@ function extractDocAttachments(step) {
15892
16478
  }
15893
16479
  return attachments;
15894
16480
  }
15895
- function guessMediaType(path9) {
15896
- const lower = path9.toLowerCase();
16481
+ function guessMediaType(path10) {
16482
+ const lower = path10.toLowerCase();
15897
16483
  if (lower.endsWith(".png")) return "image/png";
15898
16484
  if (lower.endsWith(".jpg") || lower.endsWith(".jpeg")) return "image/jpeg";
15899
16485
  if (lower.endsWith(".gif")) return "image/gif";
@@ -16968,7 +17554,7 @@ function selectTestCases(args, deps) {
16968
17554
 
16969
17555
  // src/bundler/bundle-assets.ts
16970
17556
  import * as fs4 from "fs";
16971
- import * as path4 from "path";
17557
+ import * as path5 from "path";
16972
17558
 
16973
17559
  // src/bundler/scan-html-assets.ts
16974
17560
  function scanHtmlAssets(html) {
@@ -16999,7 +17585,7 @@ function isLocalAssetRef(ref) {
16999
17585
 
17000
17586
  // src/bundler/copy-asset.ts
17001
17587
  import * as fs3 from "fs";
17002
- import * as path3 from "path";
17588
+ import * as path4 from "path";
17003
17589
  import * as crypto from "crypto";
17004
17590
  function copyAsset(sourcePath, assetsDir) {
17005
17591
  if (!fs3.existsSync(assetsDir)) {
@@ -17007,10 +17593,10 @@ function copyAsset(sourcePath, assetsDir) {
17007
17593
  }
17008
17594
  const content = fs3.readFileSync(sourcePath);
17009
17595
  const hash = crypto.createHash("sha256").update(content).digest("hex").slice(0, 8);
17010
- const ext = path3.extname(sourcePath);
17011
- const baseName = sanitize(path3.basename(sourcePath, ext));
17596
+ const ext = path4.extname(sourcePath);
17597
+ const baseName = sanitize(path4.basename(sourcePath, ext));
17012
17598
  const destName = `${baseName}-${hash}${ext}`;
17013
- const destPath = path3.join(assetsDir, destName);
17599
+ const destPath = path4.join(assetsDir, destName);
17014
17600
  if (!fs3.existsSync(destPath)) {
17015
17601
  fs3.copyFileSync(sourcePath, destPath);
17016
17602
  }
@@ -17022,14 +17608,14 @@ function sanitize(name) {
17022
17608
 
17023
17609
  // src/bundler/bundle-assets.ts
17024
17610
  function bundleAssets(htmlPath, options = {}) {
17025
- const htmlDir = path4.dirname(htmlPath);
17026
- const assetsDir = path4.join(htmlDir, "assets");
17611
+ const htmlDir = path5.dirname(htmlPath);
17612
+ const assetsDir = path5.join(htmlDir, "assets");
17027
17613
  let html = fs4.readFileSync(htmlPath, "utf8");
17028
17614
  const refs = scanHtmlAssets(html);
17029
17615
  let copiedCount = 0;
17030
17616
  const missing = [];
17031
17617
  for (const ref of refs) {
17032
- const absolutePath = path4.resolve(htmlDir, ref);
17618
+ const absolutePath = path5.resolve(htmlDir, ref);
17033
17619
  if (!fs4.existsSync(absolutePath)) {
17034
17620
  missing.push(ref);
17035
17621
  continue;
@@ -17527,14 +18113,14 @@ function groupBy7(items, keyFn) {
17527
18113
 
17528
18114
  // src/formatters/astro-assets.ts
17529
18115
  import * as fs5 from "fs";
17530
- import * as path5 from "path";
18116
+ import * as path6 from "path";
17531
18117
  var SKIP_PREFIXES = ["http://", "https://", "data:", "#"];
17532
18118
  function isLocalPath(src) {
17533
18119
  const trimmed = src.trim();
17534
18120
  if (SKIP_PREFIXES.some((prefix) => trimmed.startsWith(prefix))) {
17535
18121
  return false;
17536
18122
  }
17537
- return !path5.posix.isAbsolute(trimmed) && !path5.win32.isAbsolute(trimmed);
18123
+ return !path6.posix.isAbsolute(trimmed) && !path6.win32.isAbsolute(trimmed);
17538
18124
  }
17539
18125
  function stripCodeContent(markdown) {
17540
18126
  let result = markdown.replace(/^[ \t]*(`{3,}|~{3,})[^\n]*\n[\s\S]*?^[ \t]*\1\s*$/gm, "");
@@ -17628,7 +18214,7 @@ function copyMarkdownAssets(options) {
17628
18214
  const pathMap = /* @__PURE__ */ new Map();
17629
18215
  const missing = [];
17630
18216
  for (const ref of refs) {
17631
- const absPath = path5.resolve(markdownDir, ref);
18217
+ const absPath = path6.resolve(markdownDir, ref);
17632
18218
  if (!fs5.existsSync(absPath)) {
17633
18219
  if (!allowMissing) {
17634
18220
  throw new Error(`Asset not found: ${absPath}`);
@@ -18905,7 +19491,8 @@ var FORMAT_EXTENSIONS = {
18905
19491
  junit: ".junit.xml",
18906
19492
  "cucumber-json": ".cucumber.json",
18907
19493
  "cucumber-messages": ".ndjson",
18908
- confluence: ".adf.json"
19494
+ confluence: ".adf.json",
19495
+ "story-report-json": ".story-report.json"
18909
19496
  };
18910
19497
  var TEST_EXTENSIONS = [
18911
19498
  ".test.ts",
@@ -18932,11 +19519,11 @@ function computeOutputPath(sourceFile, format, mode, colocatedStyle, baseOutputD
18932
19519
  const ext = FORMAT_EXTENSIONS[format];
18933
19520
  const effectiveName = outputName + (outputNameSuffix ?? "");
18934
19521
  if (mode === "aggregated") {
18935
- return toPosix(path6.join(baseOutputDir, `${effectiveName}${ext}`));
19522
+ return toPosix(path7.join(baseOutputDir, `${effectiveName}${ext}`));
18936
19523
  }
18937
19524
  const normalizedSource = toPosix(sourceFile);
18938
- const dirOfSource = path6.posix.dirname(normalizedSource);
18939
- let baseName = path6.posix.basename(normalizedSource);
19525
+ const dirOfSource = path7.posix.dirname(normalizedSource);
19526
+ let baseName = path7.posix.basename(normalizedSource);
18940
19527
  for (const testExt of TEST_EXTENSIONS) {
18941
19528
  if (baseName.endsWith(testExt)) {
18942
19529
  baseName = baseName.slice(0, -testExt.length);
@@ -18945,9 +19532,9 @@ function computeOutputPath(sourceFile, format, mode, colocatedStyle, baseOutputD
18945
19532
  }
18946
19533
  const fileName = `${baseName}.${effectiveName}${ext}`;
18947
19534
  if (colocatedStyle === "adjacent") {
18948
- return toPosix(path6.posix.join(dirOfSource, fileName));
19535
+ return toPosix(path7.posix.join(dirOfSource, fileName));
18949
19536
  }
18950
- return toPosix(path6.posix.join(baseOutputDir, dirOfSource, fileName));
19537
+ return toPosix(path7.posix.join(baseOutputDir, dirOfSource, fileName));
18951
19538
  }
18952
19539
  function groupTestCasesByOutput(testCases, format, options, logger, outputNameSuffix) {
18953
19540
  const groups = /* @__PURE__ */ new Map();
@@ -19016,7 +19603,7 @@ var ReportGenerator = class {
19016
19603
  exclude: options.exclude ?? [],
19017
19604
  includeTags: options.includeTags ?? [],
19018
19605
  excludeTags: options.excludeTags ?? [],
19019
- formats: options.formats ?? ["cucumber-json"],
19606
+ formats: options.formats ?? ["html"],
19020
19607
  outputDir: options.outputDir ?? "reports",
19021
19608
  outputName: options.outputName ?? "index",
19022
19609
  outputNameTimestamp: options.outputNameTimestamp ?? false,
@@ -19030,6 +19617,9 @@ var ReportGenerator = class {
19030
19617
  cucumberJson: {
19031
19618
  pretty: options.cucumberJson?.pretty ?? false
19032
19619
  },
19620
+ storyReportJson: {
19621
+ pretty: options.storyReportJson?.pretty ?? true
19622
+ },
19033
19623
  cucumberMessages: {
19034
19624
  uriStrategy: options.cucumberMessages?.uriStrategy ?? "sourceFile",
19035
19625
  includeSynthetics: options.cucumberMessages?.includeSynthetics ?? true,
@@ -19145,8 +19735,8 @@ var ReportGenerator = class {
19145
19735
  if (astroPaths) {
19146
19736
  for (const mdPath of astroPaths) {
19147
19737
  const content = await fsPromises.readFile(mdPath, "utf8");
19148
- const mdDir = path6.dirname(mdPath);
19149
- const assetsDir = path6.resolve(this.options.astro.assetsDir);
19738
+ const mdDir = path7.dirname(mdPath);
19739
+ const assetsDir = path7.resolve(this.options.astro.assetsDir);
19150
19740
  const result = copyMarkdownAssets({
19151
19741
  markdown: content,
19152
19742
  markdownDir: mdDir,
@@ -19177,9 +19767,9 @@ var ReportGenerator = class {
19177
19767
  if (groups.size === 0 && this.options.output.mode === "aggregated") {
19178
19768
  const ext = FORMAT_EXTENSIONS[format];
19179
19769
  const effectiveName = this.options.outputName + (outputNameSuffix ?? "");
19180
- const outputPath = toPosix(path6.join(this.options.outputDir, `${effectiveName}${ext}`));
19770
+ const outputPath = toPosix(path7.join(this.options.outputDir, `${effectiveName}${ext}`));
19181
19771
  const content = await this.formatContent(run, format);
19182
- const dir = path6.dirname(outputPath);
19772
+ const dir = path7.dirname(outputPath);
19183
19773
  await fsPromises.mkdir(dir, { recursive: true });
19184
19774
  await this.deps.writeFile(outputPath, content);
19185
19775
  return [outputPath];
@@ -19191,7 +19781,7 @@ var ReportGenerator = class {
19191
19781
  testCases
19192
19782
  };
19193
19783
  const content = await this.formatContent(groupRun, format);
19194
- const dir = path6.dirname(outputPath);
19784
+ const dir = path7.dirname(outputPath);
19195
19785
  await fsPromises.mkdir(dir, { recursive: true });
19196
19786
  await this.deps.writeFile(outputPath, content);
19197
19787
  writtenPaths.push(outputPath);
@@ -19298,6 +19888,12 @@ var ReportGenerator = class {
19298
19888
  });
19299
19889
  return formatter.format(run);
19300
19890
  }
19891
+ case "story-report-json": {
19892
+ const formatter = new StoryReportJsonFormatter({
19893
+ pretty: this.options.storyReportJson.pretty
19894
+ });
19895
+ return formatter.format(run);
19896
+ }
19301
19897
  default:
19302
19898
  throw new Error(`Unknown format: ${format}`);
19303
19899
  }
@@ -19311,7 +19907,7 @@ async function generateRunComparison(args) {
19311
19907
  await fsPromises.mkdir(outputDir, { recursive: true });
19312
19908
  for (const format of args.formats) {
19313
19909
  const ext = format === "html" ? ".html" : ".md";
19314
- const outputPath = toPosix(path6.join(outputDir, `${outputName}${ext}`));
19910
+ const outputPath = toPosix(path7.join(outputDir, `${outputName}${ext}`));
19315
19911
  const content = format === "html" ? new RunDiffHtmlFormatter({ title: args.title }).format(diff) : new RunDiffMarkdownFormatter({ title: args.title }).format(diff);
19316
19912
  await fsPromises.writeFile(outputPath, content, "utf8");
19317
19913
  files.push(outputPath);
@@ -19321,9 +19917,9 @@ async function generateRunComparison(args) {
19321
19917
 
19322
19918
  // src/init-astro.ts
19323
19919
  import * as fs7 from "fs";
19324
- import * as path7 from "path";
19920
+ import * as path8 from "path";
19325
19921
  import { fileURLToPath } from "url";
19326
- var __dirname = path7.dirname(fileURLToPath(import.meta.url));
19922
+ var __dirname = path8.dirname(fileURLToPath(import.meta.url));
19327
19923
  function initAstro(options = {}) {
19328
19924
  const targetDir = options.targetDir ?? "./story-docs";
19329
19925
  const force = options.force ?? false;
@@ -19335,7 +19931,7 @@ function initAstro(options = {}) {
19335
19931
  );
19336
19932
  }
19337
19933
  }
19338
- const templateDir = path7.resolve(__dirname, "..", "templates", "astro-starlight");
19934
+ const templateDir = path8.resolve(__dirname, "..", "templates", "astro-starlight");
19339
19935
  if (!fs7.existsSync(templateDir)) {
19340
19936
  throw new Error(
19341
19937
  `Template directory not found at ${templateDir}. Ensure the package is installed correctly.`
@@ -19348,8 +19944,8 @@ function copyDirRecursive(src, dest) {
19348
19944
  fs7.mkdirSync(dest, { recursive: true });
19349
19945
  const entries = fs7.readdirSync(src, { withFileTypes: true });
19350
19946
  for (const entry of entries) {
19351
- const srcPath = path7.join(src, entry.name);
19352
- const destPath = path7.join(dest, entry.name);
19947
+ const srcPath = path8.join(src, entry.name);
19948
+ const destPath = path8.join(dest, entry.name);
19353
19949
  if (entry.isDirectory()) {
19354
19950
  copyDirRecursive(srcPath, destPath);
19355
19951
  } else {
@@ -19380,6 +19976,7 @@ var EXIT_SCHEMA_VALIDATION = 1;
19380
19976
  var EXIT_CANONICAL_VALIDATION = 2;
19381
19977
  var EXIT_GENERATION = 3;
19382
19978
  var EXIT_USAGE = 4;
19979
+ var EXIT_COMPARE_GATE = 5;
19383
19980
  var HELP_TEXT = `
19384
19981
  executable-stories \u2014 Generate reports from test results JSON.
19385
19982
 
@@ -19404,7 +20001,7 @@ SUBCOMMANDS
19404
20001
  publish-jira Publish an ADF JSON file to a Jira issue (as comment or description)
19405
20002
 
19406
20003
  OPTIONS
19407
- --format <formats> Comma-separated formats: html, markdown, junit, cucumber-json, cucumber-messages, cucumber-html, astro, confluence, or custom names from config (default: html)
20004
+ --format <formats> Comma-separated formats: html, markdown, junit, cucumber-json, cucumber-messages, cucumber-html, astro, confluence, story-report-json, or custom names from config (default: html)
19408
20005
  astro Themed Markdown (for Astro docs sites with matching CSS)
19409
20006
  confluence Atlassian Document Format (ADF) JSON for Confluence / Jira
19410
20007
  html Custom HTML report (accessible, dark mode, mermaid)
@@ -19413,6 +20010,7 @@ OPTIONS
19413
20010
  junit JUnit XML
19414
20011
  cucumber-json Cucumber JSON
19415
20012
  cucumber-messages Raw NDJSON (Cucumber Messages)
20013
+ story-report-json StoryReport v1 JSON (consumed by executable-stories-react and other UI renderers)
19416
20014
  --config <path> Path to executable-stories.config.js (default: ./executable-stories.config.js)
19417
20015
  --input-type <type> Input type: raw, canonical, or ndjson (default: raw)
19418
20016
  --output-dir <dir> Output directory (default: reports)
@@ -19443,6 +20041,9 @@ OPTIONS
19443
20041
  --baseline-dir <dir> Directory to scan when --baseline auto is used
19444
20042
  --pr-summary Print a PR-friendly markdown summary after compare
19445
20043
  --pr-summary-file <path> Write the PR-friendly markdown summary to a file
20044
+ --fail-on-regression Exit non-zero when any regression is detected in compare
20045
+ --fail-on-added-failures Exit non-zero when newly added scenarios are failing
20046
+ --max-regressions <n> Exit non-zero when regressions exceed threshold
19446
20047
  --emit-canonical <path> Write canonical JSON to given path
19447
20048
  --help Show this help message
19448
20049
 
@@ -19506,6 +20107,7 @@ EXIT CODES
19506
20107
  2 Canonical validation failure
19507
20108
  3 Formatter/generation failure
19508
20109
  4 Bad arguments / usage error
20110
+ 5 Compare gate failed
19509
20111
  `.trim();
19510
20112
  async function parseCliArgs(argv) {
19511
20113
  const args = argv.slice(2);
@@ -19607,6 +20209,9 @@ async function parseCliArgs(argv) {
19607
20209
  "allow-missing-assets": { type: "boolean", default: false },
19608
20210
  "pr-summary": { type: "boolean", default: false },
19609
20211
  "pr-summary-file": { type: "string" },
20212
+ "fail-on-regression": { type: "boolean", default: false },
20213
+ "fail-on-added-failures": { type: "boolean", default: false },
20214
+ "max-regressions": { type: "string" },
19610
20215
  "config": { type: "string" },
19611
20216
  help: { type: "boolean", default: false }
19612
20217
  },
@@ -19647,7 +20252,7 @@ async function parseCliArgs(argv) {
19647
20252
  }
19648
20253
  const pluginConfig = await loadConfig(values["config"]);
19649
20254
  const customFormatterNames = new Set(Object.keys(pluginConfig.formatters ?? {}));
19650
- const builtInFormats = /* @__PURE__ */ new Set(["astro", "confluence", "html", "markdown", "junit", "cucumber-json", "cucumber-messages", "cucumber-html"]);
20255
+ const builtInFormats = /* @__PURE__ */ new Set(["astro", "confluence", "html", "markdown", "junit", "cucumber-json", "cucumber-messages", "cucumber-html", "story-report-json"]);
19651
20256
  const formatStr = values.format;
19652
20257
  const allRequestedFormats = formatStr.split(",").map((f) => f.trim());
19653
20258
  const builtInRequested = allRequestedFormats.filter((f) => builtInFormats.has(f));
@@ -19655,7 +20260,7 @@ async function parseCliArgs(argv) {
19655
20260
  const unknownFormats = allRequestedFormats.filter((f) => !builtInFormats.has(f) && !customFormatterNames.has(f));
19656
20261
  if (unknownFormats.length > 0) {
19657
20262
  const knownCustom = customFormatterNames.size > 0 ? `, ${[...customFormatterNames].join(", ")}` : "";
19658
- console.error(`Error: Unknown format(s): ${unknownFormats.join(", ")}. Valid built-in: astro, confluence, html, markdown, junit, cucumber-json, cucumber-messages, cucumber-html${knownCustom}.`);
20263
+ console.error(`Error: Unknown format(s): ${unknownFormats.join(", ")}. Valid built-in: astro, confluence, html, markdown, junit, cucumber-json, cucumber-messages, cucumber-html, story-report-json${knownCustom}.`);
19659
20264
  process.exit(EXIT_USAGE);
19660
20265
  }
19661
20266
  const formats = builtInRequested;
@@ -19714,6 +20319,12 @@ async function parseCliArgs(argv) {
19714
20319
  console.error(`Error: --max-history-runs must be a positive integer, got "${maxHistoryRunsStr}".`);
19715
20320
  process.exit(EXIT_USAGE);
19716
20321
  }
20322
+ const maxRegressionsStr = values["max-regressions"];
20323
+ const maxRegressions = maxRegressionsStr !== void 0 ? parseInt(maxRegressionsStr, 10) : void 0;
20324
+ if (maxRegressionsStr !== void 0 && (isNaN(maxRegressions) || maxRegressions < 0)) {
20325
+ console.error(`Error: --max-regressions must be a non-negative integer, got "${maxRegressionsStr}".`);
20326
+ process.exit(EXIT_USAGE);
20327
+ }
19717
20328
  const sortTestCasesRaw = values["sort-test-cases"];
19718
20329
  const validSortModes = /* @__PURE__ */ new Set(["id", "source", "none"]);
19719
20330
  if (!validSortModes.has(sortTestCasesRaw)) {
@@ -19774,6 +20385,9 @@ async function parseCliArgs(argv) {
19774
20385
  allowMissingAssets: values["allow-missing-assets"],
19775
20386
  prSummary: values["pr-summary"],
19776
20387
  prSummaryFile: values["pr-summary-file"],
20388
+ failOnRegression: values["fail-on-regression"],
20389
+ failOnAddedFailures: values["fail-on-added-failures"],
20390
+ maxRegressions,
19777
20391
  config: values["config"]
19778
20392
  };
19779
20393
  return { args: cliArgs, pluginConfig, customRequested };
@@ -19782,7 +20396,7 @@ async function readInput(args) {
19782
20396
  if (args.stdin) {
19783
20397
  return readStdin();
19784
20398
  }
19785
- const filePath = path8.resolve(args.inputFile);
20399
+ const filePath = path9.resolve(args.inputFile);
19786
20400
  if (!fs8.existsSync(filePath)) {
19787
20401
  console.error(`Error: File not found: ${filePath}`);
19788
20402
  process.exit(EXIT_USAGE);
@@ -19790,7 +20404,7 @@ async function readInput(args) {
19790
20404
  return fs8.readFileSync(filePath, "utf8");
19791
20405
  }
19792
20406
  function readFileInput(filePath) {
19793
- const resolved = path8.resolve(filePath);
20407
+ const resolved = path9.resolve(filePath);
19794
20408
  if (!fs8.existsSync(resolved)) {
19795
20409
  console.error(`Error: File not found: ${resolved}`);
19796
20410
  process.exit(EXIT_USAGE);
@@ -19928,14 +20542,14 @@ function tryNormalizeRunFromText(text2, args) {
19928
20542
  }
19929
20543
  }
19930
20544
  function listBaselineCandidates(currentFile, args) {
19931
- const baselineDir = path8.resolve(args.baselineDir ?? path8.dirname(currentFile));
19932
- const currentResolved = path8.resolve(currentFile);
20545
+ const baselineDir = path9.resolve(args.baselineDir ?? path9.dirname(currentFile));
20546
+ const currentResolved = path9.resolve(currentFile);
19933
20547
  if (!fs8.existsSync(baselineDir)) {
19934
20548
  console.error(`Error: baseline directory not found: ${baselineDir}`);
19935
20549
  process.exit(EXIT_USAGE);
19936
20550
  }
19937
20551
  const entries = fs8.readdirSync(baselineDir, { withFileTypes: true });
19938
- return entries.filter((entry) => entry.isFile()).map((entry) => path8.join(baselineDir, entry.name)).filter((candidate) => path8.resolve(candidate) !== currentResolved).filter(
20552
+ return entries.filter((entry) => entry.isFile()).map((entry) => path9.join(baselineDir, entry.name)).filter((candidate) => path9.resolve(candidate) !== currentResolved).filter(
19939
20553
  (candidate) => args.inputType === "ndjson" ? candidate.endsWith(".ndjson") : candidate.endsWith(".json")
19940
20554
  );
19941
20555
  }
@@ -19950,7 +20564,7 @@ function resolveBaselineAuto(currentFile, currentRun, args) {
19950
20564
  }
19951
20565
  if (comparable.length === 0) {
19952
20566
  console.error(
19953
- `Error: no compatible baseline files found in ${path8.resolve(args.baselineDir ?? path8.dirname(currentFile))}.`
20567
+ `Error: no compatible baseline files found in ${path9.resolve(args.baselineDir ?? path9.dirname(currentFile))}.`
19954
20568
  );
19955
20569
  process.exit(EXIT_USAGE);
19956
20570
  }
@@ -19973,6 +20587,13 @@ async function main() {
19973
20587
  try {
19974
20588
  const result = await generateCompareReports(baseline, current, baselineFile, args);
19975
20589
  printCompareResult(result, args, startMs);
20590
+ const gateFailures = evaluateCompareGate(result, args);
20591
+ if (gateFailures.length > 0) {
20592
+ for (const failure of gateFailures) {
20593
+ console.error(`Compare gate failed: ${failure}`);
20594
+ }
20595
+ process.exit(EXIT_COMPARE_GATE);
20596
+ }
19976
20597
  process.exit(EXIT_SUCCESS);
19977
20598
  } catch (err) {
19978
20599
  const msg = err instanceof Error ? err.message : String(err);
@@ -20039,8 +20660,8 @@ async function main() {
20039
20660
  process.exit(EXIT_SCHEMA_VALIDATION);
20040
20661
  }
20041
20662
  if (args.emitCanonical) {
20042
- const outPath = path8.resolve(args.emitCanonical);
20043
- fs8.mkdirSync(path8.dirname(outPath), { recursive: true });
20663
+ const outPath = path9.resolve(args.emitCanonical);
20664
+ fs8.mkdirSync(path9.dirname(outPath), { recursive: true });
20044
20665
  fs8.writeFileSync(outPath, JSON.stringify(run, null, 2), "utf8");
20045
20666
  }
20046
20667
  try {
@@ -20098,8 +20719,8 @@ ${msg}`);
20098
20719
  }
20099
20720
  const run = data;
20100
20721
  if (args.emitCanonical) {
20101
- const outPath = path8.resolve(args.emitCanonical);
20102
- fs8.mkdirSync(path8.dirname(outPath), { recursive: true });
20722
+ const outPath = path9.resolve(args.emitCanonical);
20723
+ fs8.mkdirSync(path9.dirname(outPath), { recursive: true });
20103
20724
  fs8.writeFileSync(outPath, JSON.stringify(run, null, 2), "utf8");
20104
20725
  }
20105
20726
  try {
@@ -20156,8 +20777,8 @@ ${msg}`);
20156
20777
  process.exit(EXIT_CANONICAL_VALIDATION);
20157
20778
  }
20158
20779
  if (args.emitCanonical) {
20159
- const outPath = path8.resolve(args.emitCanonical);
20160
- fs8.mkdirSync(path8.dirname(outPath), { recursive: true });
20780
+ const outPath = path9.resolve(args.emitCanonical);
20781
+ fs8.mkdirSync(path9.dirname(outPath), { recursive: true });
20161
20782
  fs8.writeFileSync(outPath, JSON.stringify(canonical, null, 2), "utf8");
20162
20783
  }
20163
20784
  try {
@@ -20183,7 +20804,7 @@ function runCustomFormatters(run, customRequested, formatters, args) {
20183
20804
  const ext = formatter.fileExtension ?? formatName;
20184
20805
  const baseName = args.outputName ?? "report";
20185
20806
  const filename = args.outputNameTimestamp ? `${baseName}-${Math.floor(run.startedAtMs / 1e3)}.${ext}` : `${baseName}.${ext}`;
20186
- const filepath = path8.join(outputDir, filename);
20807
+ const filepath = path9.join(outputDir, filename);
20187
20808
  fs8.mkdirSync(outputDir, { recursive: true });
20188
20809
  fs8.writeFileSync(filepath, content, "utf8");
20189
20810
  console.log(`Generated: ${filepath}`);
@@ -20235,7 +20856,7 @@ async function dispatchNotifications(run, args) {
20235
20856
  }
20236
20857
  function runHistoryPipeline(run, args) {
20237
20858
  if (!args.historyFile) return;
20238
- const historyPath = path8.resolve(args.historyFile);
20859
+ const historyPath = path9.resolve(args.historyFile);
20239
20860
  const store = loadHistory(
20240
20861
  { filePath: historyPath },
20241
20862
  {
@@ -20254,7 +20875,7 @@ function runHistoryPipeline(run, args) {
20254
20875
  run,
20255
20876
  maxRuns: args.maxHistoryRuns
20256
20877
  });
20257
- const dir = path8.dirname(historyPath);
20878
+ const dir = path9.dirname(historyPath);
20258
20879
  fs8.mkdirSync(dir, { recursive: true });
20259
20880
  saveHistory(
20260
20881
  { filePath: historyPath, store: updated },
@@ -20331,6 +20952,9 @@ async function generateCompareReports(baseline, current, baselineFile, args) {
20331
20952
  return {
20332
20953
  files: result.files,
20333
20954
  baselineFile,
20955
+ addedFailures: result.diff.scenarios.filter(
20956
+ (scenario) => scenario.kind === "added" && scenario.current?.status === "failed"
20957
+ ).length,
20334
20958
  summary: result.diff.summary,
20335
20959
  prSummary: args.prSummary || args.prSummaryFile ? createPrCommentSummary(result.diff) : void 0
20336
20960
  };
@@ -20356,8 +20980,8 @@ function printResult(result, args, startMs, droppedMissingStory = 0) {
20356
20980
  function printCompareResult(result, args, startMs) {
20357
20981
  const durationMs = Date.now() - startMs;
20358
20982
  if (result.prSummary && args.prSummaryFile) {
20359
- const outputPath = path8.resolve(args.prSummaryFile);
20360
- fs8.mkdirSync(path8.dirname(outputPath), { recursive: true });
20983
+ const outputPath = path9.resolve(args.prSummaryFile);
20984
+ fs8.mkdirSync(path9.dirname(outputPath), { recursive: true });
20361
20985
  fs8.writeFileSync(outputPath, result.prSummary, "utf8");
20362
20986
  }
20363
20987
  if (args.jsonSummary) {
@@ -20366,6 +20990,7 @@ function printCompareResult(result, args, startMs) {
20366
20990
  {
20367
20991
  files: result.files,
20368
20992
  baselineFile: result.baselineFile,
20993
+ addedFailures: result.addedFailures,
20369
20994
  diff: result.summary,
20370
20995
  prSummary: result.prSummary,
20371
20996
  durationMs
@@ -20385,6 +21010,25 @@ function printCompareResult(result, args, startMs) {
20385
21010
  console.log(result.prSummary);
20386
21011
  }
20387
21012
  }
21013
+ function evaluateCompareGate(result, args) {
21014
+ const failures = [];
21015
+ if (args.failOnRegression && result.summary.regressed > 0) {
21016
+ failures.push(
21017
+ `regressions detected (${result.summary.regressed}) with --fail-on-regression`
21018
+ );
21019
+ }
21020
+ if (args.failOnAddedFailures && result.addedFailures > 0) {
21021
+ failures.push(
21022
+ `new failing scenarios detected (${result.addedFailures}) with --fail-on-added-failures`
21023
+ );
21024
+ }
21025
+ if (args.maxRegressions !== void 0 && result.summary.regressed > args.maxRegressions) {
21026
+ failures.push(
21027
+ `regressions ${result.summary.regressed} exceed --max-regressions ${args.maxRegressions}`
21028
+ );
21029
+ }
21030
+ return failures;
21031
+ }
20388
21032
  async function runPublishConfluence(rawArgs) {
20389
21033
  const { values, positionals } = parseArgs({
20390
21034
  args: rawArgs,
@@ -20457,7 +21101,7 @@ Generate an API token at https://id.atlassian.com/manage-profile/security/api-to
20457
21101
  console.error("Error: --title is required when creating a new page");
20458
21102
  process.exit(EXIT_USAGE);
20459
21103
  }
20460
- const adf = fs8.readFileSync(path8.resolve(inputFile), "utf8");
21104
+ const adf = fs8.readFileSync(path9.resolve(inputFile), "utf8");
20461
21105
  if (dryRun) {
20462
21106
  console.log(
20463
21107
  JSON.stringify(
@@ -20563,7 +21207,7 @@ Generate an API token at https://id.atlassian.com/manage-profile/security/api-to
20563
21207
  process.exit(EXIT_USAGE);
20564
21208
  }
20565
21209
  const mode = modeRaw;
20566
- const adf = fs8.readFileSync(path8.resolve(inputFile), "utf8");
21210
+ const adf = fs8.readFileSync(path9.resolve(inputFile), "utf8");
20567
21211
  if (dryRun) {
20568
21212
  console.log(
20569
21213
  JSON.stringify(