executable-stories-react 0.2.0 → 0.3.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.cjs CHANGED
@@ -64,9 +64,18 @@ __export(src_exports, {
64
64
  ReportTrace: () => ReportTrace,
65
65
  STORY_REPORT_SCHEMA_MAJOR: () => STORY_REPORT_SCHEMA_MAJOR,
66
66
  ScenarioExplorer: () => ScenarioExplorer,
67
+ ScenarioRunHistory: () => ScenarioRunHistory,
68
+ ciDisplayName: () => ciDisplayName,
69
+ commitUrl: () => commitUrl,
70
+ currentStreak: () => currentStreak,
71
+ describeRunHistory: () => describeRunHistory,
67
72
  err: () => err,
73
+ formatRelativeAge: () => formatRelativeAge,
74
+ isReportStale: () => isReportStale,
68
75
  ok: () => ok,
69
76
  parseStoryReport: () => parseStoryReport,
77
+ prUrl: () => prUrl,
78
+ reportLastRunMs: () => reportLastRunMs,
70
79
  storyReportSchema: () => storyReportSchema,
71
80
  useBuiltinRenderers: () => useBuiltinRenderers,
72
81
  useCustomRenderers: () => useCustomRenderers,
@@ -124,11 +133,23 @@ function useReport() {
124
133
  return ctx.report;
125
134
  }
126
135
 
127
- // src/components/ReportScenario.tsx
128
- var import_duration3 = require("executable-stories-core/utils/duration");
129
-
130
- // src/components/ReportSteps.tsx
131
- var import_duration = require("executable-stories-core/utils/duration");
136
+ // ../executable-stories-core/dist/utils/duration.js
137
+ function formatDuration(ms) {
138
+ if (ms < 1e3) {
139
+ return `${Math.round(ms)} ms`;
140
+ }
141
+ if (ms < 6e4) {
142
+ return `${(ms / 1e3).toFixed(2)} s`;
143
+ }
144
+ const minutes = Math.floor(ms / 6e4);
145
+ const seconds = Math.floor(ms % 6e4 / 1e3);
146
+ if (minutes < 60) {
147
+ return seconds > 0 ? `${minutes}m ${seconds}s` : `${minutes}m`;
148
+ }
149
+ const hours = Math.floor(minutes / 60);
150
+ const remainingMinutes = minutes % 60;
151
+ return `${hours}h ${remainingMinutes}m`;
152
+ }
132
153
 
133
154
  // src/components/doc/DocNote.tsx
134
155
  var import_jsx_runtime2 = require("react/jsx-runtime");
@@ -282,6 +303,15 @@ function safeUrl(value) {
282
303
  if (scheme && !/^https?$/i.test(scheme[1] ?? "")) return void 0;
283
304
  return trimmed;
284
305
  }
306
+ function safeImageUrl(value) {
307
+ if (!value) return void 0;
308
+ const trimmed = value.trim();
309
+ if (/^data:image\//i.test(trimmed)) return trimmed;
310
+ return safeUrl(trimmed);
311
+ }
312
+ function isLocalFsPath(value) {
313
+ return /^(?:[/\\]|[A-Za-z]:[/\\])/.test(value);
314
+ }
285
315
 
286
316
  // src/components/doc/DocSection.tsx
287
317
  var import_jsx_runtime8 = require("react/jsx-runtime");
@@ -337,11 +367,19 @@ function DocMermaid({ entry }) {
337
367
  // src/components/doc/DocScreenshot.tsx
338
368
  var import_jsx_runtime10 = require("react/jsx-runtime");
339
369
  function DocScreenshot({ entry }) {
370
+ const src = isLocalFsPath(entry.path) ? void 0 : safeImageUrl(entry.path);
371
+ if (!src) {
372
+ return /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("figure", { className: "my-3 rounded-md border border-dashed border-border p-3 text-sm text-muted-foreground", children: [
373
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("div", { className: "font-medium", children: "Screenshot unavailable" }),
374
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("div", { className: "mt-1 break-all font-mono text-xs", children: entry.path }),
375
+ entry.alt ? /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("figcaption", { className: "mt-1.5 text-xs", children: entry.alt }) : null
376
+ ] });
377
+ }
340
378
  return /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("figure", { className: "my-3", children: [
341
379
  /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
342
380
  "img",
343
381
  {
344
- src: entry.path,
382
+ src,
345
383
  alt: entry.alt ?? "",
346
384
  loading: "lazy",
347
385
  className: "max-w-full rounded-md border border-border"
@@ -354,6 +392,13 @@ function DocScreenshot({ entry }) {
354
392
  // src/components/doc/DocVideo.tsx
355
393
  var import_jsx_runtime11 = require("react/jsx-runtime");
356
394
  function DocVideo({ entry }) {
395
+ if (isLocalFsPath(entry.path)) {
396
+ return /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("figure", { className: "my-3 rounded-md border border-dashed border-border p-3 text-sm text-muted-foreground", children: [
397
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("div", { className: "font-medium", children: "Video unavailable" }),
398
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("div", { className: "mt-1 break-all font-mono text-xs", children: entry.path }),
399
+ entry.caption ? /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("figcaption", { className: "mt-1.5 text-xs", children: entry.caption }) : null
400
+ ] });
401
+ }
357
402
  const src = safeUrl(entry.path);
358
403
  const poster = safeUrl(entry.poster);
359
404
  return /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("figure", { className: "my-3", children: [
@@ -529,7 +574,7 @@ function ReportStepItem({ step }) {
529
574
  }
530
575
  ),
531
576
  /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("span", { className: "flex-1 text-foreground", children: highlightStepParams(step.text) }),
532
- step.durationMs > 0 ? /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("span", { className: "shrink-0 whitespace-nowrap font-mono text-[0.6875rem] text-muted-foreground", children: (0, import_duration.formatDuration)(step.durationMs) }) : null
577
+ step.durationMs > 0 ? /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("span", { className: "shrink-0 whitespace-nowrap font-mono text-[0.6875rem] text-muted-foreground", children: formatDuration(step.durationMs) }) : null
533
578
  ] }),
534
579
  step.errorMessage ? /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(
535
580
  "pre",
@@ -587,9 +632,6 @@ function ReportAttachments({ attachments }) {
587
632
  );
588
633
  }
589
634
 
590
- // src/components/ReportTrace.tsx
591
- var import_duration2 = require("executable-stories-core/utils/duration");
592
-
593
635
  // src/lib/trace.ts
594
636
  var VALID = /* @__PURE__ */ new Set(["ok", "error", "unset"]);
595
637
  var safeStatus = (s) => VALID.has(s) ? s : "unset";
@@ -684,7 +726,7 @@ function ReportTrace({ spans }) {
684
726
  " span",
685
727
  rows.length === 1 ? "" : "s",
686
728
  ", ",
687
- (0, import_duration2.formatDuration)(totalMs),
729
+ formatDuration(totalMs),
688
730
  ")"
689
731
  ] }),
690
732
  /* @__PURE__ */ (0, import_jsx_runtime19.jsx)("ul", { className: "flex flex-col gap-0.5 px-3 pb-3 font-mono text-[0.6875rem]", children: rows.map((row, i) => {
@@ -707,33 +749,128 @@ function ReportTrace({ spans }) {
707
749
  style: { left: `${leftPct}%`, width: `${widthPct}%` }
708
750
  }
709
751
  ) }),
710
- /* @__PURE__ */ (0, import_jsx_runtime19.jsx)("span", { className: "w-16 shrink-0 text-right text-muted-foreground", children: (0, import_duration2.formatDuration)(row.durationMs) })
752
+ /* @__PURE__ */ (0, import_jsx_runtime19.jsx)("span", { className: "w-16 shrink-0 text-right text-muted-foreground", children: formatDuration(row.durationMs) })
711
753
  ] }, `${row.spanId}-${i}`);
712
754
  }) })
713
755
  ] });
714
756
  }
715
757
 
716
- // src/interactive/collapse-context.ts
758
+ // src/interactive/scenario-history-context.ts
717
759
  var import_react5 = require("react");
718
- var CollapseContext = (0, import_react5.createContext)(null);
760
+ var ScenarioHistoryContext = (0, import_react5.createContext)(null);
761
+ var ScenarioHistoryProvider = ScenarioHistoryContext.Provider;
762
+ function useScenarioHistory() {
763
+ return (0, import_react5.useContext)(ScenarioHistoryContext);
764
+ }
765
+
766
+ // src/lib/run-history.ts
767
+ function currentStreak(entries) {
768
+ const last = entries[entries.length - 1];
769
+ if (!last) return void 0;
770
+ let count = 0;
771
+ for (let i = entries.length - 1; i >= 0 && entries[i].status === last.status; i--) count++;
772
+ return { status: last.status, count };
773
+ }
774
+ var STREAK_VERB = {
775
+ passed: "Passing",
776
+ failed: "Failing",
777
+ skipped: "Skipped",
778
+ pending: "Pending"
779
+ };
780
+ function describeRunHistory(entries) {
781
+ const passed = entries.filter((e) => e.status === "passed").length;
782
+ const base = `${passed}/${entries.length} runs passed`;
783
+ const streak = currentStreak(entries);
784
+ if (!streak || entries.length < 2) return base;
785
+ return `${base} \xB7 ${STREAK_VERB[streak.status]} for the last ${streak.count === 1 ? "run" : `${streak.count} runs`}`;
786
+ }
787
+ var MIN_FLAKINESS_SAMPLES = 3;
788
+ function flakinessOf(entries) {
789
+ const countable = entries.filter((e) => e.status === "passed" || e.status === "failed");
790
+ if (countable.length < MIN_FLAKINESS_SAMPLES) return "stable";
791
+ let transitions = 0;
792
+ for (let i = 1; i < countable.length; i++) {
793
+ if (countable[i].status !== countable[i - 1].status) transitions++;
794
+ }
795
+ const transitionScore = transitions / (countable.length - 1);
796
+ const failureRate = countable.filter((e) => e.status === "failed").length / countable.length;
797
+ if (transitionScore > 0.5 || transitionScore > 0.3 && failureRate > 0.2) return "flaky";
798
+ if (transitionScore > 0.2 || failureRate > 0.3) return "unstable";
799
+ return "stable";
800
+ }
801
+
802
+ // src/components/ScenarioRunHistory.tsx
803
+ var import_jsx_runtime20 = require("react/jsx-runtime");
804
+ var MAX_DOTS = 10;
805
+ var DOT_COLOR = {
806
+ passed: "bg-pass",
807
+ failed: "bg-fail",
808
+ skipped: "bg-skip",
809
+ pending: "bg-pend"
810
+ };
811
+ function dotTitle(e) {
812
+ const parts = [new Date(e.timestamp).toLocaleString(), e.status];
813
+ if (e.commitSha) parts.push(e.commitSha.slice(0, 8));
814
+ return parts.join(" \xB7 ");
815
+ }
816
+ function ScenarioRunHistory({ scenarioId }) {
817
+ const history = useScenarioHistory();
818
+ const entries = history?.[scenarioId];
819
+ if (!entries || entries.length < 2) return null;
820
+ const recent = entries.slice(-MAX_DOTS);
821
+ const flaky = flakinessOf(recent) === "flaky";
822
+ const summary = flaky ? `${describeRunHistory(recent)} \xB7 Flaky` : describeRunHistory(recent);
823
+ return /* @__PURE__ */ (0, import_jsx_runtime20.jsxs)(import_jsx_runtime20.Fragment, { children: [
824
+ flaky ? /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(Badge, { variant: "pending", "aria-label": "Flaky: status flips between recent runs", title: summary, children: "Flaky" }) : null,
825
+ /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(
826
+ "span",
827
+ {
828
+ role: "img",
829
+ "aria-label": `Run history: ${summary}`,
830
+ title: summary,
831
+ className: "flex shrink-0 items-center gap-[3px]",
832
+ children: recent.map((e, i) => /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(
833
+ "span",
834
+ {
835
+ "aria-hidden": true,
836
+ title: dotTitle(e),
837
+ className: cn(
838
+ "inline-block size-1.5 rounded-full",
839
+ DOT_COLOR[e.status],
840
+ // The newest run (rightmost dot) is the one the card's status
841
+ // badge already describes; ring it so the timeline reads as
842
+ // "…leading up to now".
843
+ i === recent.length - 1 && "ring-1 ring-border ring-offset-1 ring-offset-card"
844
+ )
845
+ },
846
+ `${e.timestamp}-${i}`
847
+ ))
848
+ }
849
+ )
850
+ ] });
851
+ }
852
+
853
+ // src/interactive/collapse-context.ts
854
+ var import_react6 = require("react");
855
+ var CollapseContext = (0, import_react6.createContext)(null);
719
856
  var CollapseProvider = CollapseContext.Provider;
720
857
  function useCollapse() {
721
- return (0, import_react5.useContext)(CollapseContext);
858
+ return (0, import_react6.useContext)(CollapseContext);
722
859
  }
723
860
 
724
861
  // src/interactive/scenario-actions.tsx
725
- var import_react6 = require("react");
726
- var ScenarioActionsContext = (0, import_react6.createContext)(null);
862
+ var import_react7 = require("react");
863
+ var ScenarioActionsContext = (0, import_react7.createContext)(null);
727
864
  var ScenarioActionsProvider = ScenarioActionsContext.Provider;
728
865
  function useScenarioActions() {
729
- return (0, import_react6.useContext)(ScenarioActionsContext);
866
+ return (0, import_react7.useContext)(ScenarioActionsContext);
730
867
  }
731
868
 
732
869
  // src/components/ui/card.tsx
733
870
  var React2 = require("react");
734
- var import_jsx_runtime20 = require("react/jsx-runtime");
871
+ var import_jsx_runtime21 = require("react/jsx-runtime");
735
872
  function Card({ className, ...props }) {
736
- return /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(
873
+ return /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(
737
874
  "div",
738
875
  {
739
876
  "data-slot": "card",
@@ -746,7 +883,7 @@ function Card({ className, ...props }) {
746
883
  );
747
884
  }
748
885
  function CardHeader({ className, ...props }) {
749
- return /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(
886
+ return /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(
750
887
  "div",
751
888
  {
752
889
  "data-slot": "card-header",
@@ -759,7 +896,7 @@ function CardHeader({ className, ...props }) {
759
896
  );
760
897
  }
761
898
  function CardContent({ className, ...props }) {
762
- return /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(
899
+ return /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(
763
900
  "div",
764
901
  {
765
902
  "data-slot": "card-content",
@@ -770,7 +907,7 @@ function CardContent({ className, ...props }) {
770
907
  }
771
908
 
772
909
  // src/components/ReportScenario.tsx
773
- var import_jsx_runtime21 = require("react/jsx-runtime");
910
+ var import_jsx_runtime22 = require("react/jsx-runtime");
774
911
  var STATUS_LABEL = {
775
912
  passed: "Passed",
776
913
  failed: "Failed",
@@ -798,12 +935,12 @@ var STATUS_BADGE = {
798
935
  function ReportScenario({ scenario, hideTitle = false }) {
799
936
  const titleId = `${scenario.id}-title`;
800
937
  const bodyId = `${scenario.id}-body`;
801
- const label = STATUS_LABEL[scenario.status];
938
+ const label = scenario.planned ? "Planned" : STATUS_LABEL[scenario.status];
802
939
  const collapse = useCollapse();
803
940
  const collapsible = collapse !== null && !hideTitle;
804
941
  const collapsed = collapsible ? collapse.isCollapsed(scenario.id) : false;
805
942
  const actions = useScenarioActions();
806
- return /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)(
943
+ return /* @__PURE__ */ (0, import_jsx_runtime22.jsxs)(
807
944
  Card,
808
945
  {
809
946
  id: scenario.id,
@@ -811,10 +948,10 @@ function ReportScenario({ scenario, hideTitle = false }) {
811
948
  ...hideTitle ? { "aria-label": `${scenario.title} \u2014 ${label}` } : { "aria-labelledby": titleId },
812
949
  className: "gap-0 py-0",
813
950
  children: [
814
- /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)(CardHeader, { className: "px-4 py-3", children: [
815
- /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)("div", { className: cn("flex items-start gap-4", hideTitle ? "justify-end" : "justify-between"), children: [
816
- hideTitle ? null : /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)("h3", { id: titleId, className: "flex items-center gap-2 text-sm font-medium text-foreground", children: [
817
- collapsible ? /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(
951
+ /* @__PURE__ */ (0, import_jsx_runtime22.jsxs)(CardHeader, { className: "px-4 py-3", children: [
952
+ /* @__PURE__ */ (0, import_jsx_runtime22.jsxs)("div", { className: cn("flex items-start gap-4", hideTitle ? "justify-end" : "justify-between"), children: [
953
+ hideTitle ? null : /* @__PURE__ */ (0, import_jsx_runtime22.jsxs)("h3", { id: titleId, className: "flex items-center gap-2 text-sm font-medium text-foreground", children: [
954
+ collapsible ? /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(
818
955
  "button",
819
956
  {
820
957
  type: "button",
@@ -823,17 +960,18 @@ function ReportScenario({ scenario, hideTitle = false }) {
823
960
  "aria-controls": bodyId,
824
961
  "aria-label": `Toggle ${scenario.title}`,
825
962
  className: "shrink-0 cursor-pointer text-muted-foreground hover:text-foreground",
826
- children: /* @__PURE__ */ (0, import_jsx_runtime21.jsx)("span", { "aria-hidden": true, className: cn("inline-block text-xs transition-transform", !collapsed && "rotate-90"), children: "\u25B8" })
963
+ children: /* @__PURE__ */ (0, import_jsx_runtime22.jsx)("span", { "aria-hidden": true, className: cn("inline-block text-xs transition-transform", !collapsed && "rotate-90"), children: "\u25B8" })
827
964
  }
828
965
  ) : null,
829
- /* @__PURE__ */ (0, import_jsx_runtime21.jsx)("span", { "aria-hidden": true, className: cn("text-sm leading-none", STATUS_GLYPH_COLOR[scenario.status]), children: STATUS_GLYPH[scenario.status] }),
830
- /* @__PURE__ */ (0, import_jsx_runtime21.jsx)("span", { children: scenario.title })
966
+ /* @__PURE__ */ (0, import_jsx_runtime22.jsx)("span", { "aria-hidden": true, className: cn("text-sm leading-none", STATUS_GLYPH_COLOR[scenario.status]), children: STATUS_GLYPH[scenario.status] }),
967
+ /* @__PURE__ */ (0, import_jsx_runtime22.jsx)("span", { children: scenario.title })
831
968
  ] }),
832
- /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)("div", { className: "flex shrink-0 items-center gap-2", children: [
833
- scenario.durationMs > 0 ? /* @__PURE__ */ (0, import_jsx_runtime21.jsx)("span", { className: "whitespace-nowrap font-mono text-[0.6875rem] text-muted-foreground", children: (0, import_duration3.formatDuration)(scenario.durationMs) }) : null,
834
- /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(Badge, { variant: STATUS_BADGE[scenario.status], "aria-label": `Status: ${label}`, children: label }),
835
- actions ? /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)("div", { className: "flex items-center gap-0.5", children: [
836
- /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(
969
+ /* @__PURE__ */ (0, import_jsx_runtime22.jsxs)("div", { className: "flex shrink-0 items-center gap-2", children: [
970
+ /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(ScenarioRunHistory, { scenarioId: scenario.id }),
971
+ scenario.durationMs > 0 ? /* @__PURE__ */ (0, import_jsx_runtime22.jsx)("span", { className: "whitespace-nowrap font-mono text-[0.6875rem] text-muted-foreground", children: formatDuration(scenario.durationMs) }) : null,
972
+ /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(Badge, { variant: STATUS_BADGE[scenario.status], "aria-label": `Status: ${label}`, children: label }),
973
+ actions ? /* @__PURE__ */ (0, import_jsx_runtime22.jsxs)("div", { className: "flex items-center gap-0.5", children: [
974
+ /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(
837
975
  "button",
838
976
  {
839
977
  type: "button",
@@ -844,7 +982,7 @@ function ReportScenario({ scenario, hideTitle = false }) {
844
982
  children: "link"
845
983
  }
846
984
  ),
847
- /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(
985
+ /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(
848
986
  "button",
849
987
  {
850
988
  type: "button",
@@ -855,7 +993,7 @@ function ReportScenario({ scenario, hideTitle = false }) {
855
993
  children: "md"
856
994
  }
857
995
  ),
858
- /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(
996
+ /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(
859
997
  "button",
860
998
  {
861
999
  type: "button",
@@ -869,13 +1007,13 @@ function ReportScenario({ scenario, hideTitle = false }) {
869
1007
  ] }) : null
870
1008
  ] })
871
1009
  ] }),
872
- scenario.tags.length > 0 || (scenario.tickets?.length ?? 0) > 0 ? /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)("ul", { "aria-label": "Tags and tickets", className: "flex flex-wrap gap-1.5", children: [
873
- scenario.tags.map((t) => /* @__PURE__ */ (0, import_jsx_runtime21.jsx)("li", { children: /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(Badge, { variant: "tag", children: t }) }, `tag-${t}`)),
874
- (scenario.tickets ?? []).map((ticket) => /* @__PURE__ */ (0, import_jsx_runtime21.jsx)("li", { children: ticket.url ? /* @__PURE__ */ (0, import_jsx_runtime21.jsx)("a", { href: ticket.url, target: "_blank", rel: "noreferrer noopener", children: /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(Badge, { variant: "outline", children: ticket.id }) }) : /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(Badge, { variant: "outline", children: ticket.id }) }, `ticket-${ticket.id}`))
1010
+ scenario.tags.length > 0 || (scenario.tickets?.length ?? 0) > 0 ? /* @__PURE__ */ (0, import_jsx_runtime22.jsxs)("ul", { "aria-label": "Tags and tickets", className: "flex flex-wrap gap-1.5", children: [
1011
+ scenario.tags.map((t) => /* @__PURE__ */ (0, import_jsx_runtime22.jsx)("li", { children: /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(Badge, { variant: "tag", children: t }) }, `tag-${t}`)),
1012
+ (scenario.tickets ?? []).map((ticket) => /* @__PURE__ */ (0, import_jsx_runtime22.jsx)("li", { children: ticket.url ? /* @__PURE__ */ (0, import_jsx_runtime22.jsx)("a", { href: ticket.url, target: "_blank", rel: "noreferrer noopener", children: /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(Badge, { variant: "outline", children: ticket.id }) }) : /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(Badge, { variant: "outline", children: ticket.id }) }, `ticket-${ticket.id}`))
875
1013
  ] }) : null
876
1014
  ] }),
877
- /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)(CardContent, { id: bodyId, hidden: collapsed, className: "px-4 pt-0 pb-4", children: [
878
- scenario.errorMessage ? /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(
1015
+ /* @__PURE__ */ (0, import_jsx_runtime22.jsxs)(CardContent, { id: bodyId, hidden: collapsed, className: "px-4 pt-0 pb-4", children: [
1016
+ scenario.errorMessage ? /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(
879
1017
  "pre",
880
1018
  {
881
1019
  role: "alert",
@@ -883,10 +1021,10 @@ function ReportScenario({ scenario, hideTitle = false }) {
883
1021
  children: scenario.errorMessage
884
1022
  }
885
1023
  ) : null,
886
- /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(ReportSteps, { scenario }),
887
- scenario.docEntries.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime21.jsx)("div", { "data-es-docs": true, className: "mt-3", children: /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(ReportDocEntries, { entries: scenario.docEntries }) }) : null,
888
- /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(ReportAttachments, { attachments: scenario.attachments }),
889
- /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(ReportTrace, { spans: scenario.otelSpans })
1024
+ /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(ReportSteps, { scenario }),
1025
+ scenario.docEntries.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime22.jsx)("div", { "data-es-docs": true, className: "mt-3", children: /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(ReportDocEntries, { entries: scenario.docEntries }) }) : null,
1026
+ /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(ReportAttachments, { attachments: scenario.attachments }),
1027
+ /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(ReportTrace, { spans: scenario.otelSpans })
890
1028
  ] })
891
1029
  ]
892
1030
  }
@@ -894,13 +1032,13 @@ function ReportScenario({ scenario, hideTitle = false }) {
894
1032
  }
895
1033
 
896
1034
  // src/components/ReportScenarioList.tsx
897
- var import_jsx_runtime22 = require("react/jsx-runtime");
1035
+ var import_jsx_runtime23 = require("react/jsx-runtime");
898
1036
  function ReportScenarioList({ feature }) {
899
- return /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(import_jsx_runtime22.Fragment, { children: feature.scenarios.map((scenario) => /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(ReportScenario, { scenario }, scenario.id)) });
1037
+ return /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(import_jsx_runtime23.Fragment, { children: feature.scenarios.map((scenario) => /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(ReportScenario, { scenario }, scenario.id)) });
900
1038
  }
901
1039
 
902
1040
  // src/components/ReportFeature.tsx
903
- var import_jsx_runtime23 = require("react/jsx-runtime");
1041
+ var import_jsx_runtime24 = require("react/jsx-runtime");
904
1042
  function ReportFeature({ feature }) {
905
1043
  const titleId = `${feature.id}-title`;
906
1044
  const bodyId = `${feature.id}-body`;
@@ -908,7 +1046,7 @@ function ReportFeature({ feature }) {
908
1046
  const skipped = s.skipped + s.pending;
909
1047
  const collapse = useCollapse();
910
1048
  const collapsed = collapse?.isCollapsed(feature.id) ?? false;
911
- return /* @__PURE__ */ (0, import_jsx_runtime23.jsxs)(
1049
+ return /* @__PURE__ */ (0, import_jsx_runtime24.jsxs)(
912
1050
  "section",
913
1051
  {
914
1052
  id: feature.id,
@@ -916,9 +1054,9 @@ function ReportFeature({ feature }) {
916
1054
  "aria-labelledby": titleId,
917
1055
  className: "overflow-hidden rounded-xl border border-border bg-card shadow-sm",
918
1056
  children: [
919
- /* @__PURE__ */ (0, import_jsx_runtime23.jsxs)("div", { className: "flex items-center justify-between gap-4 border-b border-border bg-muted/40 px-4 py-3", children: [
920
- /* @__PURE__ */ (0, import_jsx_runtime23.jsxs)("div", { className: "flex min-w-0 items-center gap-2", children: [
921
- collapse ? /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(
1057
+ /* @__PURE__ */ (0, import_jsx_runtime24.jsxs)("div", { className: "flex items-center justify-between gap-4 border-b border-border bg-muted/40 px-4 py-3", children: [
1058
+ /* @__PURE__ */ (0, import_jsx_runtime24.jsxs)("div", { className: "flex min-w-0 items-center gap-2", children: [
1059
+ collapse ? /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(
922
1060
  "button",
923
1061
  {
924
1062
  type: "button",
@@ -927,30 +1065,30 @@ function ReportFeature({ feature }) {
927
1065
  "aria-controls": bodyId,
928
1066
  "aria-label": `Toggle ${feature.title}`,
929
1067
  className: "shrink-0 cursor-pointer text-muted-foreground hover:text-foreground",
930
- children: /* @__PURE__ */ (0, import_jsx_runtime23.jsx)("span", { "aria-hidden": true, className: cn("inline-block text-xs transition-transform", !collapsed && "rotate-90"), children: "\u25B8" })
1068
+ children: /* @__PURE__ */ (0, import_jsx_runtime24.jsx)("span", { "aria-hidden": true, className: cn("inline-block text-xs transition-transform", !collapsed && "rotate-90"), children: "\u25B8" })
931
1069
  }
932
1070
  ) : null,
933
- /* @__PURE__ */ (0, import_jsx_runtime23.jsxs)("div", { className: "min-w-0", children: [
934
- /* @__PURE__ */ (0, import_jsx_runtime23.jsx)("h2", { id: titleId, className: "text-sm font-semibold tracking-tight text-foreground", children: feature.title }),
935
- /* @__PURE__ */ (0, import_jsx_runtime23.jsx)("p", { className: "mt-0.5 truncate font-mono text-xs text-muted-foreground", children: feature.sourceFile })
1071
+ /* @__PURE__ */ (0, import_jsx_runtime24.jsxs)("div", { className: "min-w-0", children: [
1072
+ /* @__PURE__ */ (0, import_jsx_runtime24.jsx)("h2", { id: titleId, className: "text-sm font-semibold tracking-tight text-foreground", children: feature.title }),
1073
+ /* @__PURE__ */ (0, import_jsx_runtime24.jsx)("p", { className: "mt-0.5 truncate font-mono text-xs text-muted-foreground", children: feature.sourceFile })
936
1074
  ] })
937
1075
  ] }),
938
- /* @__PURE__ */ (0, import_jsx_runtime23.jsxs)(
1076
+ /* @__PURE__ */ (0, import_jsx_runtime24.jsxs)(
939
1077
  "div",
940
1078
  {
941
1079
  role: "img",
942
1080
  className: "flex shrink-0 items-center gap-2.5 font-mono text-xs font-medium",
943
1081
  "aria-label": `${s.passed} passed, ${s.failed} failed, ${skipped} skipped`,
944
1082
  children: [
945
- /* @__PURE__ */ (0, import_jsx_runtime23.jsxs)("span", { "aria-hidden": true, className: "text-pass", children: [
1083
+ /* @__PURE__ */ (0, import_jsx_runtime24.jsxs)("span", { "aria-hidden": true, className: "text-pass", children: [
946
1084
  "\u2713",
947
1085
  s.passed
948
1086
  ] }),
949
- /* @__PURE__ */ (0, import_jsx_runtime23.jsxs)("span", { "aria-hidden": true, className: "text-fail", children: [
1087
+ /* @__PURE__ */ (0, import_jsx_runtime24.jsxs)("span", { "aria-hidden": true, className: "text-fail", children: [
950
1088
  "\u2717",
951
1089
  s.failed
952
1090
  ] }),
953
- /* @__PURE__ */ (0, import_jsx_runtime23.jsxs)("span", { "aria-hidden": true, className: "text-skip", children: [
1091
+ /* @__PURE__ */ (0, import_jsx_runtime24.jsxs)("span", { "aria-hidden": true, className: "text-skip", children: [
954
1092
  "\u25CB",
955
1093
  skipped
956
1094
  ] })
@@ -958,24 +1096,24 @@ function ReportFeature({ feature }) {
958
1096
  }
959
1097
  )
960
1098
  ] }),
961
- /* @__PURE__ */ (0, import_jsx_runtime23.jsx)("div", { id: bodyId, hidden: collapsed, className: "flex flex-col gap-2 p-3", children: /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(ReportScenarioList, { feature }) })
1099
+ /* @__PURE__ */ (0, import_jsx_runtime24.jsx)("div", { id: bodyId, hidden: collapsed, className: "flex flex-col gap-2 p-3", children: /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(ReportScenarioList, { feature }) })
962
1100
  ]
963
1101
  }
964
1102
  );
965
1103
  }
966
1104
 
967
1105
  // src/components/ReportFeatureList.tsx
968
- var import_jsx_runtime24 = require("react/jsx-runtime");
1106
+ var import_jsx_runtime25 = require("react/jsx-runtime");
969
1107
  function ReportFeatureList() {
970
1108
  const report = useReport();
971
- return /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(import_jsx_runtime24.Fragment, { children: report.features.map((feature) => /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(ReportFeature, { feature }, feature.id)) });
1109
+ return /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(import_jsx_runtime25.Fragment, { children: report.features.map((feature) => /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(ReportFeature, { feature }, feature.id)) });
972
1110
  }
973
1111
 
974
1112
  // src/components/ui/empty.tsx
975
1113
  var import_class_variance_authority2 = require("class-variance-authority");
976
- var import_jsx_runtime25 = require("react/jsx-runtime");
1114
+ var import_jsx_runtime26 = require("react/jsx-runtime");
977
1115
  function Empty({ className, ...props }) {
978
- return /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(
1116
+ return /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(
979
1117
  "div",
980
1118
  {
981
1119
  "data-slot": "empty",
@@ -988,7 +1126,7 @@ function Empty({ className, ...props }) {
988
1126
  );
989
1127
  }
990
1128
  function EmptyHeader({ className, ...props }) {
991
- return /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(
1129
+ return /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(
992
1130
  "div",
993
1131
  {
994
1132
  "data-slot": "empty-header",
@@ -1015,7 +1153,7 @@ var emptyMediaVariants = (0, import_class_variance_authority2.cva)(
1015
1153
  }
1016
1154
  );
1017
1155
  function EmptyTitle({ className, ...props }) {
1018
- return /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(
1156
+ return /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(
1019
1157
  "div",
1020
1158
  {
1021
1159
  "data-slot": "empty-title",
@@ -1026,16 +1164,16 @@ function EmptyTitle({ className, ...props }) {
1026
1164
  }
1027
1165
 
1028
1166
  // src/components/ReportEmpty.tsx
1029
- var import_jsx_runtime26 = require("react/jsx-runtime");
1167
+ var import_jsx_runtime27 = require("react/jsx-runtime");
1030
1168
  function ReportEmpty({ message }) {
1031
- return /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(Empty, { "aria-live": "polite", className: "border", children: /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(EmptyHeader, { children: /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(EmptyTitle, { children: message ?? "No scenarios in this report." }) }) });
1169
+ return /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(Empty, { "aria-live": "polite", className: "border", children: /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(EmptyHeader, { children: /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(EmptyTitle, { children: message ?? "No scenarios in this report." }) }) });
1032
1170
  }
1033
1171
 
1034
1172
  // src/components/ReportSummary.tsx
1035
- var import_jsx_runtime27 = require("react/jsx-runtime");
1173
+ var import_jsx_runtime28 = require("react/jsx-runtime");
1036
1174
  function ReportSummary({ className }) {
1037
1175
  const report = useReport();
1038
- return /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(
1176
+ return /* @__PURE__ */ (0, import_jsx_runtime28.jsx)(
1039
1177
  ReportSummaryView,
1040
1178
  {
1041
1179
  summary: report.summary,
@@ -1045,32 +1183,32 @@ function ReportSummary({ className }) {
1045
1183
  );
1046
1184
  }
1047
1185
  function ReportSummaryView({ summary, className, ariaLabel }) {
1048
- return /* @__PURE__ */ (0, import_jsx_runtime27.jsxs)("p", { className: cn("text-sm text-muted-foreground", className), "aria-label": ariaLabel, children: [
1049
- /* @__PURE__ */ (0, import_jsx_runtime27.jsxs)("span", { children: [
1050
- /* @__PURE__ */ (0, import_jsx_runtime27.jsx)("strong", { className: "font-semibold text-foreground", children: summary.total }),
1186
+ return /* @__PURE__ */ (0, import_jsx_runtime28.jsxs)("p", { className: cn("text-sm text-muted-foreground", className), "aria-label": ariaLabel, children: [
1187
+ /* @__PURE__ */ (0, import_jsx_runtime28.jsxs)("span", { children: [
1188
+ /* @__PURE__ */ (0, import_jsx_runtime28.jsx)("strong", { className: "font-semibold text-foreground", children: summary.total }),
1051
1189
  " scenario",
1052
1190
  summary.total === 1 ? "" : "s"
1053
1191
  ] }),
1054
1192
  " \xB7 ",
1055
- /* @__PURE__ */ (0, import_jsx_runtime27.jsxs)("span", { "data-status": "passed", className: "text-pass", children: [
1193
+ /* @__PURE__ */ (0, import_jsx_runtime28.jsxs)("span", { "data-status": "passed", className: "text-pass", children: [
1056
1194
  summary.passed,
1057
1195
  " passed"
1058
1196
  ] }),
1059
1197
  " \xB7 ",
1060
- /* @__PURE__ */ (0, import_jsx_runtime27.jsxs)("span", { "data-status": "failed", className: "text-fail", children: [
1198
+ /* @__PURE__ */ (0, import_jsx_runtime28.jsxs)("span", { "data-status": "failed", className: "text-fail", children: [
1061
1199
  summary.failed,
1062
1200
  " failed"
1063
1201
  ] }),
1064
- summary.skipped > 0 ? /* @__PURE__ */ (0, import_jsx_runtime27.jsxs)(import_jsx_runtime27.Fragment, { children: [
1202
+ summary.skipped > 0 ? /* @__PURE__ */ (0, import_jsx_runtime28.jsxs)(import_jsx_runtime28.Fragment, { children: [
1065
1203
  " \xB7 ",
1066
- /* @__PURE__ */ (0, import_jsx_runtime27.jsxs)("span", { "data-status": "skipped", className: "text-skip", children: [
1204
+ /* @__PURE__ */ (0, import_jsx_runtime28.jsxs)("span", { "data-status": "skipped", className: "text-skip", children: [
1067
1205
  summary.skipped,
1068
1206
  " skipped"
1069
1207
  ] })
1070
1208
  ] }) : null,
1071
- summary.pending > 0 ? /* @__PURE__ */ (0, import_jsx_runtime27.jsxs)(import_jsx_runtime27.Fragment, { children: [
1209
+ summary.pending > 0 ? /* @__PURE__ */ (0, import_jsx_runtime28.jsxs)(import_jsx_runtime28.Fragment, { children: [
1072
1210
  " \xB7 ",
1073
- /* @__PURE__ */ (0, import_jsx_runtime27.jsxs)("span", { "data-status": "pending", className: "text-pend", children: [
1211
+ /* @__PURE__ */ (0, import_jsx_runtime28.jsxs)("span", { "data-status": "pending", className: "text-pend", children: [
1074
1212
  summary.pending,
1075
1213
  " pending"
1076
1214
  ] })
@@ -1078,69 +1216,135 @@ function ReportSummaryView({ summary, className, ariaLabel }) {
1078
1216
  ] });
1079
1217
  }
1080
1218
 
1219
+ // src/lib/provenance.ts
1220
+ function reportLastRunMs(report) {
1221
+ if (report.finishedAtMs && report.finishedAtMs > 0) return report.finishedAtMs;
1222
+ if (report.startedAtMs && report.startedAtMs > 0) return report.startedAtMs;
1223
+ return void 0;
1224
+ }
1225
+ function formatRelativeAge(thenMs, nowMs) {
1226
+ const deltaMs = Math.max(0, nowMs - thenMs);
1227
+ const minutes = Math.floor(deltaMs / 6e4);
1228
+ if (minutes < 1) return "just now";
1229
+ if (minutes < 60) return `${minutes} minute${minutes === 1 ? "" : "s"} ago`;
1230
+ const hours = Math.floor(minutes / 60);
1231
+ if (hours < 24) return `${hours} hour${hours === 1 ? "" : "s"} ago`;
1232
+ const days = Math.floor(hours / 24);
1233
+ return `${days} day${days === 1 ? "" : "s"} ago`;
1234
+ }
1235
+ function isReportStale(lastRunMs, staleAfterDays, nowMs) {
1236
+ if (lastRunMs === void 0 || staleAfterDays <= 0) return false;
1237
+ return nowMs - lastRunMs >= staleAfterDays * 864e5;
1238
+ }
1239
+ var CI_DISPLAY_NAMES = {
1240
+ github: "GitHub Actions",
1241
+ gitlab: "GitLab CI",
1242
+ circleci: "CircleCI",
1243
+ jenkins: "Jenkins",
1244
+ azure: "Azure DevOps",
1245
+ buildkite: "Buildkite",
1246
+ travis: "Travis CI",
1247
+ ci: "CI"
1248
+ };
1249
+ function ciDisplayName(name) {
1250
+ return CI_DISPLAY_NAMES[name] ?? name;
1251
+ }
1252
+ function commitUrl(ci, sha) {
1253
+ const commitSha = ci?.commitSha ?? sha;
1254
+ if (!ci?.url || !commitSha) return void 0;
1255
+ const github = ci.url.match(/^(https?:\/\/[^/]+\/[^/]+\/[^/]+)\/actions\/runs\//);
1256
+ if (github) return `${github[1]}/commit/${commitSha}`;
1257
+ const gitlab = ci.url.match(/^(https?:\/\/.+)\/-\/pipelines\//);
1258
+ if (gitlab) return `${gitlab[1]}/-/commit/${commitSha}`;
1259
+ return void 0;
1260
+ }
1261
+ function prUrl(ci) {
1262
+ if (!ci?.url || !ci.prNumber) return void 0;
1263
+ const github = ci.url.match(/^(https?:\/\/[^/]+\/[^/]+\/[^/]+)\/actions\/runs\//);
1264
+ if (github) return `${github[1]}/pull/${ci.prNumber}`;
1265
+ return void 0;
1266
+ }
1267
+
1081
1268
  // src/components/ReportMeta.tsx
1082
- var import_duration4 = require("executable-stories-core/utils/duration");
1083
- var import_jsx_runtime28 = require("react/jsx-runtime");
1269
+ var import_jsx_runtime29 = require("react/jsx-runtime");
1084
1270
  function ReportMeta() {
1085
1271
  const report = useReport();
1086
1272
  const items = [];
1273
+ const linkClass = "text-primary underline underline-offset-2";
1087
1274
  if (report.startedAtMs) {
1088
1275
  items.push({ label: "Started", value: new Date(report.startedAtMs).toLocaleString() });
1089
1276
  }
1090
1277
  if (report.durationMs) {
1091
- items.push({ label: "Duration", value: (0, import_duration4.formatDuration)(report.durationMs) });
1278
+ items.push({ label: "Duration", value: formatDuration(report.durationMs) });
1092
1279
  }
1093
1280
  if (report.packageVersion) {
1094
1281
  items.push({ label: "Version", value: report.packageVersion });
1095
1282
  }
1096
- if (report.gitSha) {
1097
- items.push({ label: "Commit", value: report.gitSha.slice(0, 8) });
1283
+ if (report.ci?.branch) {
1284
+ items.push({ label: "Branch", value: report.ci.branch });
1285
+ }
1286
+ const sha = report.ci?.commitSha ?? report.gitSha;
1287
+ if (sha) {
1288
+ const url = commitUrl(report.ci, report.gitSha);
1289
+ const short = sha.slice(0, 8);
1290
+ items.push({
1291
+ label: "Commit",
1292
+ value: url ? /* @__PURE__ */ (0, import_jsx_runtime29.jsx)("a", { href: url, target: "_blank", rel: "noreferrer noopener", className: linkClass, children: short }) : short
1293
+ });
1098
1294
  }
1099
1295
  if (report.ci) {
1100
1296
  items.push({
1101
1297
  label: "CI",
1102
- value: report.ci.url ? /* @__PURE__ */ (0, import_jsx_runtime28.jsxs)("a", { href: report.ci.url, target: "_blank", rel: "noreferrer noopener", className: "text-primary underline underline-offset-2", children: [
1103
- report.ci.name,
1298
+ value: report.ci.url ? /* @__PURE__ */ (0, import_jsx_runtime29.jsxs)("a", { href: report.ci.url, target: "_blank", rel: "noreferrer noopener", className: linkClass, children: [
1299
+ ciDisplayName(report.ci.name),
1104
1300
  report.ci.buildNumber ? ` #${report.ci.buildNumber}` : ""
1105
- ] }) : `${report.ci.name}${report.ci.buildNumber ? ` #${report.ci.buildNumber}` : ""}`
1301
+ ] }) : `${ciDisplayName(report.ci.name)}${report.ci.buildNumber ? ` #${report.ci.buildNumber}` : ""}`
1302
+ });
1303
+ }
1304
+ if (report.ci?.prNumber) {
1305
+ const url = prUrl(report.ci);
1306
+ const label = `#${report.ci.prNumber}`;
1307
+ items.push({
1308
+ label: "PR",
1309
+ value: url ? /* @__PURE__ */ (0, import_jsx_runtime29.jsx)("a", { href: url, target: "_blank", rel: "noreferrer noopener", className: linkClass, children: label }) : label
1106
1310
  });
1107
1311
  }
1108
1312
  if (items.length === 0) return null;
1109
- return /* @__PURE__ */ (0, import_jsx_runtime28.jsx)("dl", { "aria-label": "Run metadata", className: "flex flex-wrap gap-x-6 gap-y-1 text-xs text-muted-foreground", children: items.map((item) => /* @__PURE__ */ (0, import_jsx_runtime28.jsxs)("div", { className: "flex gap-1.5", children: [
1110
- /* @__PURE__ */ (0, import_jsx_runtime28.jsx)("dt", { className: "font-medium", children: item.label }),
1111
- /* @__PURE__ */ (0, import_jsx_runtime28.jsx)("dd", { className: "font-mono text-foreground", children: item.value })
1313
+ return /* @__PURE__ */ (0, import_jsx_runtime29.jsx)("dl", { "aria-label": "Run metadata", className: "flex flex-wrap gap-x-6 gap-y-1 text-xs text-muted-foreground", children: items.map((item) => /* @__PURE__ */ (0, import_jsx_runtime29.jsxs)("div", { className: "flex gap-1.5", children: [
1314
+ /* @__PURE__ */ (0, import_jsx_runtime29.jsx)("dt", { className: "font-medium", children: item.label }),
1315
+ /* @__PURE__ */ (0, import_jsx_runtime29.jsx)("dd", { className: "font-mono text-foreground", children: item.value })
1112
1316
  ] }, item.label)) });
1113
1317
  }
1114
1318
 
1115
1319
  // src/components/ReportSchemaError.tsx
1116
- var import_jsx_runtime29 = require("react/jsx-runtime");
1320
+ var import_jsx_runtime30 = require("react/jsx-runtime");
1117
1321
  function ReportSchemaError({ error }) {
1118
- return /* @__PURE__ */ (0, import_jsx_runtime29.jsxs)("section", { className: "es-schema-error", role: "alert", "aria-live": "assertive", children: [
1119
- /* @__PURE__ */ (0, import_jsx_runtime29.jsx)("p", { children: /* @__PURE__ */ (0, import_jsx_runtime29.jsx)("strong", { children: "Report could not be displayed." }) }),
1120
- /* @__PURE__ */ (0, import_jsx_runtime29.jsx)("p", { children: error.message }),
1121
- error.code === "SCHEMA_VERSION_MISMATCH" ? /* @__PURE__ */ (0, import_jsx_runtime29.jsxs)("p", { children: [
1322
+ return /* @__PURE__ */ (0, import_jsx_runtime30.jsxs)("section", { className: "es-schema-error", role: "alert", "aria-live": "assertive", children: [
1323
+ /* @__PURE__ */ (0, import_jsx_runtime30.jsx)("p", { children: /* @__PURE__ */ (0, import_jsx_runtime30.jsx)("strong", { children: "Report could not be displayed." }) }),
1324
+ /* @__PURE__ */ (0, import_jsx_runtime30.jsx)("p", { children: error.message }),
1325
+ error.code === "SCHEMA_VERSION_MISMATCH" ? /* @__PURE__ */ (0, import_jsx_runtime30.jsxs)("p", { children: [
1122
1326
  "The report bundle is newer than this version of ",
1123
- /* @__PURE__ */ (0, import_jsx_runtime29.jsx)("code", { children: "executable-stories-react" }),
1327
+ /* @__PURE__ */ (0, import_jsx_runtime30.jsx)("code", { children: "executable-stories-react" }),
1124
1328
  ". Upgrade the package, or regenerate the report with an older formatters CLI."
1125
1329
  ] }) : null,
1126
- error.issues && error.issues.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime29.jsxs)("details", { children: [
1127
- /* @__PURE__ */ (0, import_jsx_runtime29.jsxs)("summary", { children: [
1330
+ error.issues && error.issues.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime30.jsxs)("details", { children: [
1331
+ /* @__PURE__ */ (0, import_jsx_runtime30.jsxs)("summary", { children: [
1128
1332
  error.issues.length,
1129
1333
  " validation issue",
1130
1334
  error.issues.length === 1 ? "" : "s"
1131
1335
  ] }),
1132
- /* @__PURE__ */ (0, import_jsx_runtime29.jsx)("pre", { children: error.issues.slice(0, 20).map((i) => `${i.path}: ${i.message}`).join("\n") })
1336
+ /* @__PURE__ */ (0, import_jsx_runtime30.jsx)("pre", { children: error.issues.slice(0, 20).map((i) => `${i.path}: ${i.message}`).join("\n") })
1133
1337
  ] }) : null
1134
1338
  ] });
1135
1339
  }
1136
1340
 
1137
1341
  // src/components/ReportShell.tsx
1138
- var import_jsx_runtime30 = require("react/jsx-runtime");
1342
+ var import_jsx_runtime31 = require("react/jsx-runtime");
1139
1343
  function ReportTitleBlock({ title }) {
1140
- return /* @__PURE__ */ (0, import_jsx_runtime30.jsxs)(import_jsx_runtime30.Fragment, { children: [
1141
- /* @__PURE__ */ (0, import_jsx_runtime30.jsx)("h1", { children: title ?? "Story Report" }),
1142
- /* @__PURE__ */ (0, import_jsx_runtime30.jsx)(ReportSummary, {}),
1143
- /* @__PURE__ */ (0, import_jsx_runtime30.jsx)(ReportMeta, {})
1344
+ return /* @__PURE__ */ (0, import_jsx_runtime31.jsxs)(import_jsx_runtime31.Fragment, { children: [
1345
+ /* @__PURE__ */ (0, import_jsx_runtime31.jsx)("h1", { children: title ?? "Story Report" }),
1346
+ /* @__PURE__ */ (0, import_jsx_runtime31.jsx)(ReportSummary, {}),
1347
+ /* @__PURE__ */ (0, import_jsx_runtime31.jsx)(ReportMeta, {})
1144
1348
  ] });
1145
1349
  }
1146
1350
  function ReportErrorShell({
@@ -1149,28 +1353,28 @@ function ReportErrorShell({
1149
1353
  title,
1150
1354
  dataTheme
1151
1355
  }) {
1152
- return /* @__PURE__ */ (0, import_jsx_runtime30.jsx)("main", { className: cn("es-report", className), "aria-label": title ?? "Test report", "data-theme": dataTheme, children: /* @__PURE__ */ (0, import_jsx_runtime30.jsx)(ReportSchemaError, { error }) });
1356
+ return /* @__PURE__ */ (0, import_jsx_runtime31.jsx)("main", { className: cn("es-report", className), "aria-label": title ?? "Test report", "data-theme": dataTheme, children: /* @__PURE__ */ (0, import_jsx_runtime31.jsx)(ReportSchemaError, { error }) });
1153
1357
  }
1154
1358
 
1155
1359
  // src/components/Report.tsx
1156
- var import_jsx_runtime31 = require("react/jsx-runtime");
1360
+ var import_jsx_runtime32 = require("react/jsx-runtime");
1157
1361
  function Report(props) {
1158
1362
  const { report, customRenderers, renderers, className, title, dataTheme } = props;
1159
1363
  const result = unwrapReport(report);
1160
1364
  if (!result.ok) {
1161
- return /* @__PURE__ */ (0, import_jsx_runtime31.jsx)(ReportErrorShell, { error: result.error, className, title, dataTheme });
1365
+ return /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(ReportErrorShell, { error: result.error, className, title, dataTheme });
1162
1366
  }
1163
1367
  const data = result.data;
1164
1368
  const hasContent = data.features.length > 0;
1165
- return /* @__PURE__ */ (0, import_jsx_runtime31.jsx)(ReportRoot, { report: data, customRenderers, renderers, children: /* @__PURE__ */ (0, import_jsx_runtime31.jsxs)("main", { className: cn("es-report", className), "aria-label": title ?? "Test report", "data-theme": dataTheme, children: [
1166
- /* @__PURE__ */ (0, import_jsx_runtime31.jsx)("header", { className: "es-report-header", children: /* @__PURE__ */ (0, import_jsx_runtime31.jsx)(ReportTitleBlock, { title }) }),
1167
- hasContent ? /* @__PURE__ */ (0, import_jsx_runtime31.jsx)(ReportFeatureList, {}) : /* @__PURE__ */ (0, import_jsx_runtime31.jsx)(ReportEmpty, {})
1369
+ return /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(ReportRoot, { report: data, customRenderers, renderers, children: /* @__PURE__ */ (0, import_jsx_runtime32.jsxs)("main", { className: cn("es-report", className), "aria-label": title ?? "Test report", "data-theme": dataTheme, children: [
1370
+ /* @__PURE__ */ (0, import_jsx_runtime32.jsx)("header", { className: "es-report-header", children: /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(ReportTitleBlock, { title }) }),
1371
+ hasContent ? /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(ReportFeatureList, {}) : /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(ReportEmpty, {})
1168
1372
  ] }) });
1169
1373
  }
1170
1374
 
1171
1375
  // src/components/doc/MermaidView.tsx
1172
- var import_react7 = require("react");
1173
- var import_jsx_runtime32 = require("react/jsx-runtime");
1376
+ var import_react8 = require("react");
1377
+ var import_jsx_runtime33 = require("react/jsx-runtime");
1174
1378
  function prefersDark(el) {
1175
1379
  const themed = el?.closest("[data-theme]");
1176
1380
  if (themed?.dataset.theme === "dark") return true;
@@ -1178,9 +1382,9 @@ function prefersDark(el) {
1178
1382
  return typeof window !== "undefined" && !!window.matchMedia?.("(prefers-color-scheme: dark)").matches;
1179
1383
  }
1180
1384
  function MermaidView({ entry, load }) {
1181
- const ref = (0, import_react7.useRef)(null);
1182
- const [svg, setSvg] = (0, import_react7.useState)(null);
1183
- (0, import_react7.useEffect)(() => {
1385
+ const ref = (0, import_react8.useRef)(null);
1386
+ const [svg, setSvg] = (0, import_react8.useState)(null);
1387
+ (0, import_react8.useEffect)(() => {
1184
1388
  let cancelled = false;
1185
1389
  const draw = async () => {
1186
1390
  try {
@@ -1207,9 +1411,9 @@ function MermaidView({ entry, load }) {
1207
1411
  };
1208
1412
  }, [entry.code, load]);
1209
1413
  if (svg) {
1210
- return /* @__PURE__ */ (0, import_jsx_runtime32.jsxs)("figure", { ref, className: "my-3", children: [
1211
- entry.title ? /* @__PURE__ */ (0, import_jsx_runtime32.jsx)("figcaption", { className: "mb-1.5 text-xs font-medium text-muted-foreground", children: entry.title }) : null,
1212
- /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(
1414
+ return /* @__PURE__ */ (0, import_jsx_runtime33.jsxs)("figure", { ref, className: "my-3", children: [
1415
+ entry.title ? /* @__PURE__ */ (0, import_jsx_runtime33.jsx)("figcaption", { className: "mb-1.5 text-xs font-medium text-muted-foreground", children: entry.title }) : null,
1416
+ /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(
1213
1417
  "div",
1214
1418
  {
1215
1419
  role: "img",
@@ -1220,33 +1424,33 @@ function MermaidView({ entry, load }) {
1220
1424
  )
1221
1425
  ] });
1222
1426
  }
1223
- return /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(MermaidSource, { entry, ref });
1427
+ return /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(MermaidSource, { entry, ref });
1224
1428
  }
1225
1429
 
1226
1430
  // src/components/doc/MermaidDiagram.tsx
1227
- var import_jsx_runtime33 = require("react/jsx-runtime");
1431
+ var import_jsx_runtime34 = require("react/jsx-runtime");
1228
1432
  var mermaidLoader;
1229
1433
  function loadMermaid() {
1230
1434
  mermaidLoader ??= import("mermaid").then((m) => m.default);
1231
1435
  return mermaidLoader;
1232
1436
  }
1233
1437
  function MermaidDiagram({ entry, load = loadMermaid }) {
1234
- return /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(MermaidView, { entry, load });
1438
+ return /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(MermaidView, { entry, load });
1235
1439
  }
1236
1440
 
1237
1441
  // src/components/ScenarioExplorer.tsx
1238
- var import_react8 = require("react");
1442
+ var import_react9 = require("react");
1239
1443
 
1240
1444
  // src/components/ui/command.tsx
1241
1445
  var React3 = require("react");
1242
1446
  var import_cmdk = require("cmdk");
1243
1447
  var import_lucide_react = require("lucide-react");
1244
- var import_jsx_runtime34 = require("react/jsx-runtime");
1448
+ var import_jsx_runtime35 = require("react/jsx-runtime");
1245
1449
  function Command({
1246
1450
  className,
1247
1451
  ...props
1248
1452
  }) {
1249
- return /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(
1453
+ return /* @__PURE__ */ (0, import_jsx_runtime35.jsx)(
1250
1454
  import_cmdk.Command,
1251
1455
  {
1252
1456
  "data-slot": "command",
@@ -1262,14 +1466,14 @@ function CommandInput({
1262
1466
  className,
1263
1467
  ...props
1264
1468
  }) {
1265
- return /* @__PURE__ */ (0, import_jsx_runtime34.jsxs)(
1469
+ return /* @__PURE__ */ (0, import_jsx_runtime35.jsxs)(
1266
1470
  "div",
1267
1471
  {
1268
1472
  "data-slot": "command-input-wrapper",
1269
1473
  className: "flex h-9 items-center gap-2 border-b px-3",
1270
1474
  children: [
1271
- /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(import_lucide_react.SearchIcon, { className: "size-4 shrink-0 opacity-50" }),
1272
- /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(
1475
+ /* @__PURE__ */ (0, import_jsx_runtime35.jsx)(import_lucide_react.SearchIcon, { className: "size-4 shrink-0 opacity-50" }),
1476
+ /* @__PURE__ */ (0, import_jsx_runtime35.jsx)(
1273
1477
  import_cmdk.Command.Input,
1274
1478
  {
1275
1479
  "data-slot": "command-input",
@@ -1288,7 +1492,7 @@ function CommandList({
1288
1492
  className,
1289
1493
  ...props
1290
1494
  }) {
1291
- return /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(
1495
+ return /* @__PURE__ */ (0, import_jsx_runtime35.jsx)(
1292
1496
  import_cmdk.Command.List,
1293
1497
  {
1294
1498
  "data-slot": "command-list",
@@ -1303,7 +1507,7 @@ function CommandList({
1303
1507
  function CommandEmpty({
1304
1508
  ...props
1305
1509
  }) {
1306
- return /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(
1510
+ return /* @__PURE__ */ (0, import_jsx_runtime35.jsx)(
1307
1511
  import_cmdk.Command.Empty,
1308
1512
  {
1309
1513
  "data-slot": "command-empty",
@@ -1316,7 +1520,7 @@ function CommandGroup({
1316
1520
  className,
1317
1521
  ...props
1318
1522
  }) {
1319
- return /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(
1523
+ return /* @__PURE__ */ (0, import_jsx_runtime35.jsx)(
1320
1524
  import_cmdk.Command.Group,
1321
1525
  {
1322
1526
  "data-slot": "command-group",
@@ -1332,7 +1536,7 @@ function CommandItem({
1332
1536
  className,
1333
1537
  ...props
1334
1538
  }) {
1335
- return /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(
1539
+ return /* @__PURE__ */ (0, import_jsx_runtime35.jsx)(
1336
1540
  import_cmdk.Command.Item,
1337
1541
  {
1338
1542
  "data-slot": "command-item",
@@ -1346,7 +1550,7 @@ function CommandItem({
1346
1550
  }
1347
1551
 
1348
1552
  // src/components/ScenarioExplorer.tsx
1349
- var import_jsx_runtime35 = require("react/jsx-runtime");
1553
+ var import_jsx_runtime36 = require("react/jsx-runtime");
1350
1554
  var STATUS_FILTERS = [
1351
1555
  { value: "all", label: "All" },
1352
1556
  { value: "passed", label: "Passed" },
@@ -1355,24 +1559,24 @@ var STATUS_FILTERS = [
1355
1559
  { value: "pending", label: "Pending" }
1356
1560
  ];
1357
1561
  function ScenarioExplorer({ scenarios, summary }) {
1358
- const [status, setStatus] = (0, import_react8.useState)("all");
1562
+ const [status, setStatus] = (0, import_react9.useState)("all");
1359
1563
  const visible = status === "all" ? scenarios : scenarios.filter((s) => s.status === status);
1360
- return /* @__PURE__ */ (0, import_jsx_runtime35.jsxs)("div", { className: "font-sans text-foreground", children: [
1361
- summary ? /* @__PURE__ */ (0, import_jsx_runtime35.jsxs)("p", { className: "mb-3 text-sm text-muted-foreground", children: [
1564
+ return /* @__PURE__ */ (0, import_jsx_runtime36.jsxs)("div", { className: "font-sans text-foreground", children: [
1565
+ summary ? /* @__PURE__ */ (0, import_jsx_runtime36.jsxs)("p", { className: "mb-3 text-sm text-muted-foreground", children: [
1362
1566
  summary.total,
1363
1567
  " scenarios \xB7 ",
1364
- /* @__PURE__ */ (0, import_jsx_runtime35.jsxs)("span", { className: "text-pass", children: [
1568
+ /* @__PURE__ */ (0, import_jsx_runtime36.jsxs)("span", { className: "text-pass", children: [
1365
1569
  summary.passed,
1366
1570
  " passed"
1367
1571
  ] }),
1368
1572
  " \xB7",
1369
1573
  " ",
1370
- /* @__PURE__ */ (0, import_jsx_runtime35.jsxs)("span", { className: "text-fail", children: [
1574
+ /* @__PURE__ */ (0, import_jsx_runtime36.jsxs)("span", { className: "text-fail", children: [
1371
1575
  summary.failed,
1372
1576
  " failed"
1373
1577
  ] })
1374
1578
  ] }) : null,
1375
- /* @__PURE__ */ (0, import_jsx_runtime35.jsx)("div", { className: "mb-2 flex flex-wrap gap-1.5", role: "group", "aria-label": "Filter by status", children: STATUS_FILTERS.map((f) => /* @__PURE__ */ (0, import_jsx_runtime35.jsx)(
1579
+ /* @__PURE__ */ (0, import_jsx_runtime36.jsx)("div", { className: "mb-2 flex flex-wrap gap-1.5", role: "group", "aria-label": "Filter by status", children: STATUS_FILTERS.map((f) => /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(
1376
1580
  "button",
1377
1581
  {
1378
1582
  type: "button",
@@ -1387,26 +1591,26 @@ function ScenarioExplorer({ scenarios, summary }) {
1387
1591
  },
1388
1592
  f.value
1389
1593
  )) }),
1390
- /* @__PURE__ */ (0, import_jsx_runtime35.jsxs)(
1594
+ /* @__PURE__ */ (0, import_jsx_runtime36.jsxs)(
1391
1595
  Command,
1392
1596
  {
1393
1597
  className: "rounded-lg border border-border bg-card",
1394
1598
  filter: (value, search) => value.toLowerCase().includes(search.toLowerCase()) ? 1 : 0,
1395
1599
  children: [
1396
- /* @__PURE__ */ (0, import_jsx_runtime35.jsx)(CommandInput, { placeholder: "Search scenarios\u2026" }),
1397
- /* @__PURE__ */ (0, import_jsx_runtime35.jsxs)(CommandList, { className: "max-h-none", children: [
1398
- /* @__PURE__ */ (0, import_jsx_runtime35.jsx)(CommandEmpty, { children: "No scenarios match your filters." }),
1399
- /* @__PURE__ */ (0, import_jsx_runtime35.jsx)(CommandGroup, { children: visible.map((s) => /* @__PURE__ */ (0, import_jsx_runtime35.jsx)(
1600
+ /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(CommandInput, { placeholder: "Search scenarios\u2026" }),
1601
+ /* @__PURE__ */ (0, import_jsx_runtime36.jsxs)(CommandList, { className: "max-h-none", children: [
1602
+ /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(CommandEmpty, { children: "No scenarios match your filters." }),
1603
+ /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(CommandGroup, { children: visible.map((s) => /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(
1400
1604
  CommandItem,
1401
1605
  {
1402
1606
  value: `${s.title} ${s.feature} ${s.tags.join(" ")}`,
1403
1607
  asChild: true,
1404
1608
  className: "cursor-pointer",
1405
- children: /* @__PURE__ */ (0, import_jsx_runtime35.jsxs)("a", { href: s.href, className: "flex items-center gap-3", children: [
1406
- /* @__PURE__ */ (0, import_jsx_runtime35.jsx)(Badge, { variant: s.status, children: s.status }),
1407
- /* @__PURE__ */ (0, import_jsx_runtime35.jsx)("span", { className: "min-w-0 flex-1 truncate font-medium", children: s.title }),
1408
- s.tags.slice(0, 3).map((t) => /* @__PURE__ */ (0, import_jsx_runtime35.jsx)(Badge, { variant: "tag", className: "hidden sm:inline-flex", children: t }, t)),
1409
- /* @__PURE__ */ (0, import_jsx_runtime35.jsx)("code", { className: "hidden font-mono text-xs text-muted-foreground md:inline", children: s.feature })
1609
+ children: /* @__PURE__ */ (0, import_jsx_runtime36.jsxs)("a", { href: s.href, className: "flex items-center gap-3", children: [
1610
+ /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(Badge, { variant: s.status, children: s.status }),
1611
+ /* @__PURE__ */ (0, import_jsx_runtime36.jsx)("span", { className: "min-w-0 flex-1 truncate font-medium", children: s.title }),
1612
+ s.tags.slice(0, 3).map((t) => /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(Badge, { variant: "tag", className: "hidden sm:inline-flex", children: t }, t)),
1613
+ /* @__PURE__ */ (0, import_jsx_runtime36.jsx)("code", { className: "hidden font-mono text-xs text-muted-foreground md:inline", children: s.feature })
1410
1614
  ] })
1411
1615
  },
1412
1616
  s.id
@@ -1596,6 +1800,10 @@ var story_report_v1_default = {
1596
1800
  status: {
1597
1801
  $ref: "#/$defs/TestStatus"
1598
1802
  },
1803
+ planned: {
1804
+ type: "boolean",
1805
+ description: "True for scenarios authored with the framework's todo modifier (e.g. it.todo): specified behavior with no implementation yet. Canonical status stays 'pending'; this flags why."
1806
+ },
1599
1807
  durationMs: {
1600
1808
  type: "number",
1601
1809
  minimum: 0
@@ -2428,9 +2636,18 @@ function parseStoryReport(input) {
2428
2636
  ReportTrace,
2429
2637
  STORY_REPORT_SCHEMA_MAJOR,
2430
2638
  ScenarioExplorer,
2639
+ ScenarioRunHistory,
2640
+ ciDisplayName,
2641
+ commitUrl,
2642
+ currentStreak,
2643
+ describeRunHistory,
2431
2644
  err,
2645
+ formatRelativeAge,
2646
+ isReportStale,
2432
2647
  ok,
2433
2648
  parseStoryReport,
2649
+ prUrl,
2650
+ reportLastRunMs,
2434
2651
  storyReportSchema,
2435
2652
  useBuiltinRenderers,
2436
2653
  useCustomRenderers,