executable-stories-react 0.2.1 → 0.4.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");
@@ -312,7 +333,7 @@ function DocSection({ entry }) {
312
333
  /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
313
334
  "div",
314
335
  {
315
- className: "es-doc-prose text-muted-foreground",
336
+ className: "es-doc-prose prose prose-sm max-w-none",
316
337
  dangerouslySetInnerHTML: { __html: html }
317
338
  }
318
339
  )
@@ -553,7 +574,7 @@ function ReportStepItem({ step }) {
553
574
  }
554
575
  ),
555
576
  /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("span", { className: "flex-1 text-foreground", children: highlightStepParams(step.text) }),
556
- 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
557
578
  ] }),
558
579
  step.errorMessage ? /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(
559
580
  "pre",
@@ -611,9 +632,6 @@ function ReportAttachments({ attachments }) {
611
632
  );
612
633
  }
613
634
 
614
- // src/components/ReportTrace.tsx
615
- var import_duration2 = require("executable-stories-core/utils/duration");
616
-
617
635
  // src/lib/trace.ts
618
636
  var VALID = /* @__PURE__ */ new Set(["ok", "error", "unset"]);
619
637
  var safeStatus = (s) => VALID.has(s) ? s : "unset";
@@ -708,7 +726,7 @@ function ReportTrace({ spans }) {
708
726
  " span",
709
727
  rows.length === 1 ? "" : "s",
710
728
  ", ",
711
- (0, import_duration2.formatDuration)(totalMs),
729
+ formatDuration(totalMs),
712
730
  ")"
713
731
  ] }),
714
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) => {
@@ -731,33 +749,128 @@ function ReportTrace({ spans }) {
731
749
  style: { left: `${leftPct}%`, width: `${widthPct}%` }
732
750
  }
733
751
  ) }),
734
- /* @__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) })
735
753
  ] }, `${row.spanId}-${i}`);
736
754
  }) })
737
755
  ] });
738
756
  }
739
757
 
740
- // src/interactive/collapse-context.ts
758
+ // src/interactive/scenario-history-context.ts
741
759
  var import_react5 = require("react");
742
- 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);
743
856
  var CollapseProvider = CollapseContext.Provider;
744
857
  function useCollapse() {
745
- return (0, import_react5.useContext)(CollapseContext);
858
+ return (0, import_react6.useContext)(CollapseContext);
746
859
  }
747
860
 
748
861
  // src/interactive/scenario-actions.tsx
749
- var import_react6 = require("react");
750
- var ScenarioActionsContext = (0, import_react6.createContext)(null);
862
+ var import_react7 = require("react");
863
+ var ScenarioActionsContext = (0, import_react7.createContext)(null);
751
864
  var ScenarioActionsProvider = ScenarioActionsContext.Provider;
752
865
  function useScenarioActions() {
753
- return (0, import_react6.useContext)(ScenarioActionsContext);
866
+ return (0, import_react7.useContext)(ScenarioActionsContext);
754
867
  }
755
868
 
756
869
  // src/components/ui/card.tsx
757
870
  var React2 = require("react");
758
- var import_jsx_runtime20 = require("react/jsx-runtime");
871
+ var import_jsx_runtime21 = require("react/jsx-runtime");
759
872
  function Card({ className, ...props }) {
760
- return /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(
873
+ return /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(
761
874
  "div",
762
875
  {
763
876
  "data-slot": "card",
@@ -770,7 +883,7 @@ function Card({ className, ...props }) {
770
883
  );
771
884
  }
772
885
  function CardHeader({ className, ...props }) {
773
- return /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(
886
+ return /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(
774
887
  "div",
775
888
  {
776
889
  "data-slot": "card-header",
@@ -783,7 +896,7 @@ function CardHeader({ className, ...props }) {
783
896
  );
784
897
  }
785
898
  function CardContent({ className, ...props }) {
786
- return /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(
899
+ return /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(
787
900
  "div",
788
901
  {
789
902
  "data-slot": "card-content",
@@ -794,7 +907,7 @@ function CardContent({ className, ...props }) {
794
907
  }
795
908
 
796
909
  // src/components/ReportScenario.tsx
797
- var import_jsx_runtime21 = require("react/jsx-runtime");
910
+ var import_jsx_runtime22 = require("react/jsx-runtime");
798
911
  var STATUS_LABEL = {
799
912
  passed: "Passed",
800
913
  failed: "Failed",
@@ -822,12 +935,12 @@ var STATUS_BADGE = {
822
935
  function ReportScenario({ scenario, hideTitle = false }) {
823
936
  const titleId = `${scenario.id}-title`;
824
937
  const bodyId = `${scenario.id}-body`;
825
- const label = STATUS_LABEL[scenario.status];
938
+ const label = scenario.planned ? "Planned" : STATUS_LABEL[scenario.status];
826
939
  const collapse = useCollapse();
827
940
  const collapsible = collapse !== null && !hideTitle;
828
941
  const collapsed = collapsible ? collapse.isCollapsed(scenario.id) : false;
829
942
  const actions = useScenarioActions();
830
- return /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)(
943
+ return /* @__PURE__ */ (0, import_jsx_runtime22.jsxs)(
831
944
  Card,
832
945
  {
833
946
  id: scenario.id,
@@ -835,10 +948,10 @@ function ReportScenario({ scenario, hideTitle = false }) {
835
948
  ...hideTitle ? { "aria-label": `${scenario.title} \u2014 ${label}` } : { "aria-labelledby": titleId },
836
949
  className: "gap-0 py-0",
837
950
  children: [
838
- /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)(CardHeader, { className: "px-4 py-3", children: [
839
- /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)("div", { className: cn("flex items-start gap-4", hideTitle ? "justify-end" : "justify-between"), children: [
840
- hideTitle ? null : /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)("h3", { id: titleId, className: "flex items-center gap-2 text-sm font-medium text-foreground", children: [
841
- 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)(
842
955
  "button",
843
956
  {
844
957
  type: "button",
@@ -847,17 +960,18 @@ function ReportScenario({ scenario, hideTitle = false }) {
847
960
  "aria-controls": bodyId,
848
961
  "aria-label": `Toggle ${scenario.title}`,
849
962
  className: "shrink-0 cursor-pointer text-muted-foreground hover:text-foreground",
850
- 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" })
851
964
  }
852
965
  ) : null,
853
- /* @__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] }),
854
- /* @__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 })
855
968
  ] }),
856
- /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)("div", { className: "flex shrink-0 items-center gap-2", children: [
857
- 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,
858
- /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(Badge, { variant: STATUS_BADGE[scenario.status], "aria-label": `Status: ${label}`, children: label }),
859
- actions ? /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)("div", { className: "flex items-center gap-0.5", children: [
860
- /* @__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)(
861
975
  "button",
862
976
  {
863
977
  type: "button",
@@ -868,7 +982,7 @@ function ReportScenario({ scenario, hideTitle = false }) {
868
982
  children: "link"
869
983
  }
870
984
  ),
871
- /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(
985
+ /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(
872
986
  "button",
873
987
  {
874
988
  type: "button",
@@ -879,7 +993,7 @@ function ReportScenario({ scenario, hideTitle = false }) {
879
993
  children: "md"
880
994
  }
881
995
  ),
882
- /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(
996
+ /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(
883
997
  "button",
884
998
  {
885
999
  type: "button",
@@ -893,13 +1007,13 @@ function ReportScenario({ scenario, hideTitle = false }) {
893
1007
  ] }) : null
894
1008
  ] })
895
1009
  ] }),
896
- 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: [
897
- 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}`)),
898
- (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}`))
899
1013
  ] }) : null
900
1014
  ] }),
901
- /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)(CardContent, { id: bodyId, hidden: collapsed, className: "px-4 pt-0 pb-4", children: [
902
- 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)(
903
1017
  "pre",
904
1018
  {
905
1019
  role: "alert",
@@ -907,10 +1021,10 @@ function ReportScenario({ scenario, hideTitle = false }) {
907
1021
  children: scenario.errorMessage
908
1022
  }
909
1023
  ) : null,
910
- /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(ReportSteps, { scenario }),
911
- 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,
912
- /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(ReportAttachments, { attachments: scenario.attachments }),
913
- /* @__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 })
914
1028
  ] })
915
1029
  ]
916
1030
  }
@@ -918,13 +1032,13 @@ function ReportScenario({ scenario, hideTitle = false }) {
918
1032
  }
919
1033
 
920
1034
  // src/components/ReportScenarioList.tsx
921
- var import_jsx_runtime22 = require("react/jsx-runtime");
1035
+ var import_jsx_runtime23 = require("react/jsx-runtime");
922
1036
  function ReportScenarioList({ feature }) {
923
- 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)) });
924
1038
  }
925
1039
 
926
1040
  // src/components/ReportFeature.tsx
927
- var import_jsx_runtime23 = require("react/jsx-runtime");
1041
+ var import_jsx_runtime24 = require("react/jsx-runtime");
928
1042
  function ReportFeature({ feature }) {
929
1043
  const titleId = `${feature.id}-title`;
930
1044
  const bodyId = `${feature.id}-body`;
@@ -932,7 +1046,7 @@ function ReportFeature({ feature }) {
932
1046
  const skipped = s.skipped + s.pending;
933
1047
  const collapse = useCollapse();
934
1048
  const collapsed = collapse?.isCollapsed(feature.id) ?? false;
935
- return /* @__PURE__ */ (0, import_jsx_runtime23.jsxs)(
1049
+ return /* @__PURE__ */ (0, import_jsx_runtime24.jsxs)(
936
1050
  "section",
937
1051
  {
938
1052
  id: feature.id,
@@ -940,9 +1054,9 @@ function ReportFeature({ feature }) {
940
1054
  "aria-labelledby": titleId,
941
1055
  className: "overflow-hidden rounded-xl border border-border bg-card shadow-sm",
942
1056
  children: [
943
- /* @__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: [
944
- /* @__PURE__ */ (0, import_jsx_runtime23.jsxs)("div", { className: "flex min-w-0 items-center gap-2", children: [
945
- 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)(
946
1060
  "button",
947
1061
  {
948
1062
  type: "button",
@@ -951,30 +1065,30 @@ function ReportFeature({ feature }) {
951
1065
  "aria-controls": bodyId,
952
1066
  "aria-label": `Toggle ${feature.title}`,
953
1067
  className: "shrink-0 cursor-pointer text-muted-foreground hover:text-foreground",
954
- 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" })
955
1069
  }
956
1070
  ) : null,
957
- /* @__PURE__ */ (0, import_jsx_runtime23.jsxs)("div", { className: "min-w-0", children: [
958
- /* @__PURE__ */ (0, import_jsx_runtime23.jsx)("h2", { id: titleId, className: "text-sm font-semibold tracking-tight text-foreground", children: feature.title }),
959
- /* @__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 })
960
1074
  ] })
961
1075
  ] }),
962
- /* @__PURE__ */ (0, import_jsx_runtime23.jsxs)(
1076
+ /* @__PURE__ */ (0, import_jsx_runtime24.jsxs)(
963
1077
  "div",
964
1078
  {
965
1079
  role: "img",
966
1080
  className: "flex shrink-0 items-center gap-2.5 font-mono text-xs font-medium",
967
1081
  "aria-label": `${s.passed} passed, ${s.failed} failed, ${skipped} skipped`,
968
1082
  children: [
969
- /* @__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: [
970
1084
  "\u2713",
971
1085
  s.passed
972
1086
  ] }),
973
- /* @__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: [
974
1088
  "\u2717",
975
1089
  s.failed
976
1090
  ] }),
977
- /* @__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: [
978
1092
  "\u25CB",
979
1093
  skipped
980
1094
  ] })
@@ -982,24 +1096,24 @@ function ReportFeature({ feature }) {
982
1096
  }
983
1097
  )
984
1098
  ] }),
985
- /* @__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 }) })
986
1100
  ]
987
1101
  }
988
1102
  );
989
1103
  }
990
1104
 
991
1105
  // src/components/ReportFeatureList.tsx
992
- var import_jsx_runtime24 = require("react/jsx-runtime");
1106
+ var import_jsx_runtime25 = require("react/jsx-runtime");
993
1107
  function ReportFeatureList() {
994
1108
  const report = useReport();
995
- 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)) });
996
1110
  }
997
1111
 
998
1112
  // src/components/ui/empty.tsx
999
1113
  var import_class_variance_authority2 = require("class-variance-authority");
1000
- var import_jsx_runtime25 = require("react/jsx-runtime");
1114
+ var import_jsx_runtime26 = require("react/jsx-runtime");
1001
1115
  function Empty({ className, ...props }) {
1002
- return /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(
1116
+ return /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(
1003
1117
  "div",
1004
1118
  {
1005
1119
  "data-slot": "empty",
@@ -1012,7 +1126,7 @@ function Empty({ className, ...props }) {
1012
1126
  );
1013
1127
  }
1014
1128
  function EmptyHeader({ className, ...props }) {
1015
- return /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(
1129
+ return /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(
1016
1130
  "div",
1017
1131
  {
1018
1132
  "data-slot": "empty-header",
@@ -1039,7 +1153,7 @@ var emptyMediaVariants = (0, import_class_variance_authority2.cva)(
1039
1153
  }
1040
1154
  );
1041
1155
  function EmptyTitle({ className, ...props }) {
1042
- return /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(
1156
+ return /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(
1043
1157
  "div",
1044
1158
  {
1045
1159
  "data-slot": "empty-title",
@@ -1050,16 +1164,16 @@ function EmptyTitle({ className, ...props }) {
1050
1164
  }
1051
1165
 
1052
1166
  // src/components/ReportEmpty.tsx
1053
- var import_jsx_runtime26 = require("react/jsx-runtime");
1167
+ var import_jsx_runtime27 = require("react/jsx-runtime");
1054
1168
  function ReportEmpty({ message }) {
1055
- 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." }) }) });
1056
1170
  }
1057
1171
 
1058
1172
  // src/components/ReportSummary.tsx
1059
- var import_jsx_runtime27 = require("react/jsx-runtime");
1173
+ var import_jsx_runtime28 = require("react/jsx-runtime");
1060
1174
  function ReportSummary({ className }) {
1061
1175
  const report = useReport();
1062
- return /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(
1176
+ return /* @__PURE__ */ (0, import_jsx_runtime28.jsx)(
1063
1177
  ReportSummaryView,
1064
1178
  {
1065
1179
  summary: report.summary,
@@ -1069,32 +1183,32 @@ function ReportSummary({ className }) {
1069
1183
  );
1070
1184
  }
1071
1185
  function ReportSummaryView({ summary, className, ariaLabel }) {
1072
- return /* @__PURE__ */ (0, import_jsx_runtime27.jsxs)("p", { className: cn("text-sm text-muted-foreground", className), "aria-label": ariaLabel, children: [
1073
- /* @__PURE__ */ (0, import_jsx_runtime27.jsxs)("span", { children: [
1074
- /* @__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 }),
1075
1189
  " scenario",
1076
1190
  summary.total === 1 ? "" : "s"
1077
1191
  ] }),
1078
1192
  " \xB7 ",
1079
- /* @__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: [
1080
1194
  summary.passed,
1081
1195
  " passed"
1082
1196
  ] }),
1083
1197
  " \xB7 ",
1084
- /* @__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: [
1085
1199
  summary.failed,
1086
1200
  " failed"
1087
1201
  ] }),
1088
- 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: [
1089
1203
  " \xB7 ",
1090
- /* @__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: [
1091
1205
  summary.skipped,
1092
1206
  " skipped"
1093
1207
  ] })
1094
1208
  ] }) : null,
1095
- 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: [
1096
1210
  " \xB7 ",
1097
- /* @__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: [
1098
1212
  summary.pending,
1099
1213
  " pending"
1100
1214
  ] })
@@ -1102,69 +1216,135 @@ function ReportSummaryView({ summary, className, ariaLabel }) {
1102
1216
  ] });
1103
1217
  }
1104
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
+
1105
1268
  // src/components/ReportMeta.tsx
1106
- var import_duration4 = require("executable-stories-core/utils/duration");
1107
- var import_jsx_runtime28 = require("react/jsx-runtime");
1269
+ var import_jsx_runtime29 = require("react/jsx-runtime");
1108
1270
  function ReportMeta() {
1109
1271
  const report = useReport();
1110
1272
  const items = [];
1273
+ const linkClass = "text-primary underline underline-offset-2";
1111
1274
  if (report.startedAtMs) {
1112
1275
  items.push({ label: "Started", value: new Date(report.startedAtMs).toLocaleString() });
1113
1276
  }
1114
1277
  if (report.durationMs) {
1115
- items.push({ label: "Duration", value: (0, import_duration4.formatDuration)(report.durationMs) });
1278
+ items.push({ label: "Duration", value: formatDuration(report.durationMs) });
1116
1279
  }
1117
1280
  if (report.packageVersion) {
1118
1281
  items.push({ label: "Version", value: report.packageVersion });
1119
1282
  }
1120
- if (report.gitSha) {
1121
- 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
+ });
1122
1294
  }
1123
1295
  if (report.ci) {
1124
1296
  items.push({
1125
1297
  label: "CI",
1126
- 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: [
1127
- 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),
1128
1300
  report.ci.buildNumber ? ` #${report.ci.buildNumber}` : ""
1129
- ] }) : `${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
1130
1310
  });
1131
1311
  }
1132
1312
  if (items.length === 0) return null;
1133
- 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: [
1134
- /* @__PURE__ */ (0, import_jsx_runtime28.jsx)("dt", { className: "font-medium", children: item.label }),
1135
- /* @__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 })
1136
1316
  ] }, item.label)) });
1137
1317
  }
1138
1318
 
1139
1319
  // src/components/ReportSchemaError.tsx
1140
- var import_jsx_runtime29 = require("react/jsx-runtime");
1320
+ var import_jsx_runtime30 = require("react/jsx-runtime");
1141
1321
  function ReportSchemaError({ error }) {
1142
- return /* @__PURE__ */ (0, import_jsx_runtime29.jsxs)("section", { className: "es-schema-error", role: "alert", "aria-live": "assertive", children: [
1143
- /* @__PURE__ */ (0, import_jsx_runtime29.jsx)("p", { children: /* @__PURE__ */ (0, import_jsx_runtime29.jsx)("strong", { children: "Report could not be displayed." }) }),
1144
- /* @__PURE__ */ (0, import_jsx_runtime29.jsx)("p", { children: error.message }),
1145
- 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: [
1146
1326
  "The report bundle is newer than this version of ",
1147
- /* @__PURE__ */ (0, import_jsx_runtime29.jsx)("code", { children: "executable-stories-react" }),
1327
+ /* @__PURE__ */ (0, import_jsx_runtime30.jsx)("code", { children: "executable-stories-react" }),
1148
1328
  ". Upgrade the package, or regenerate the report with an older formatters CLI."
1149
1329
  ] }) : null,
1150
- error.issues && error.issues.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime29.jsxs)("details", { children: [
1151
- /* @__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: [
1152
1332
  error.issues.length,
1153
1333
  " validation issue",
1154
1334
  error.issues.length === 1 ? "" : "s"
1155
1335
  ] }),
1156
- /* @__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") })
1157
1337
  ] }) : null
1158
1338
  ] });
1159
1339
  }
1160
1340
 
1161
1341
  // src/components/ReportShell.tsx
1162
- var import_jsx_runtime30 = require("react/jsx-runtime");
1342
+ var import_jsx_runtime31 = require("react/jsx-runtime");
1163
1343
  function ReportTitleBlock({ title }) {
1164
- return /* @__PURE__ */ (0, import_jsx_runtime30.jsxs)(import_jsx_runtime30.Fragment, { children: [
1165
- /* @__PURE__ */ (0, import_jsx_runtime30.jsx)("h1", { children: title ?? "Story Report" }),
1166
- /* @__PURE__ */ (0, import_jsx_runtime30.jsx)(ReportSummary, {}),
1167
- /* @__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, {})
1168
1348
  ] });
1169
1349
  }
1170
1350
  function ReportErrorShell({
@@ -1173,28 +1353,28 @@ function ReportErrorShell({
1173
1353
  title,
1174
1354
  dataTheme
1175
1355
  }) {
1176
- 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 }) });
1177
1357
  }
1178
1358
 
1179
1359
  // src/components/Report.tsx
1180
- var import_jsx_runtime31 = require("react/jsx-runtime");
1360
+ var import_jsx_runtime32 = require("react/jsx-runtime");
1181
1361
  function Report(props) {
1182
1362
  const { report, customRenderers, renderers, className, title, dataTheme } = props;
1183
1363
  const result = unwrapReport(report);
1184
1364
  if (!result.ok) {
1185
- 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 });
1186
1366
  }
1187
1367
  const data = result.data;
1188
1368
  const hasContent = data.features.length > 0;
1189
- 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: [
1190
- /* @__PURE__ */ (0, import_jsx_runtime31.jsx)("header", { className: "es-report-header", children: /* @__PURE__ */ (0, import_jsx_runtime31.jsx)(ReportTitleBlock, { title }) }),
1191
- 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, {})
1192
1372
  ] }) });
1193
1373
  }
1194
1374
 
1195
1375
  // src/components/doc/MermaidView.tsx
1196
- var import_react7 = require("react");
1197
- var import_jsx_runtime32 = require("react/jsx-runtime");
1376
+ var import_react8 = require("react");
1377
+ var import_jsx_runtime33 = require("react/jsx-runtime");
1198
1378
  function prefersDark(el) {
1199
1379
  const themed = el?.closest("[data-theme]");
1200
1380
  if (themed?.dataset.theme === "dark") return true;
@@ -1202,9 +1382,9 @@ function prefersDark(el) {
1202
1382
  return typeof window !== "undefined" && !!window.matchMedia?.("(prefers-color-scheme: dark)").matches;
1203
1383
  }
1204
1384
  function MermaidView({ entry, load }) {
1205
- const ref = (0, import_react7.useRef)(null);
1206
- const [svg, setSvg] = (0, import_react7.useState)(null);
1207
- (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)(() => {
1208
1388
  let cancelled = false;
1209
1389
  const draw = async () => {
1210
1390
  try {
@@ -1231,9 +1411,9 @@ function MermaidView({ entry, load }) {
1231
1411
  };
1232
1412
  }, [entry.code, load]);
1233
1413
  if (svg) {
1234
- return /* @__PURE__ */ (0, import_jsx_runtime32.jsxs)("figure", { ref, className: "my-3", children: [
1235
- entry.title ? /* @__PURE__ */ (0, import_jsx_runtime32.jsx)("figcaption", { className: "mb-1.5 text-xs font-medium text-muted-foreground", children: entry.title }) : null,
1236
- /* @__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)(
1237
1417
  "div",
1238
1418
  {
1239
1419
  role: "img",
@@ -1244,33 +1424,33 @@ function MermaidView({ entry, load }) {
1244
1424
  )
1245
1425
  ] });
1246
1426
  }
1247
- return /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(MermaidSource, { entry, ref });
1427
+ return /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(MermaidSource, { entry, ref });
1248
1428
  }
1249
1429
 
1250
1430
  // src/components/doc/MermaidDiagram.tsx
1251
- var import_jsx_runtime33 = require("react/jsx-runtime");
1431
+ var import_jsx_runtime34 = require("react/jsx-runtime");
1252
1432
  var mermaidLoader;
1253
1433
  function loadMermaid() {
1254
1434
  mermaidLoader ??= import("mermaid").then((m) => m.default);
1255
1435
  return mermaidLoader;
1256
1436
  }
1257
1437
  function MermaidDiagram({ entry, load = loadMermaid }) {
1258
- return /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(MermaidView, { entry, load });
1438
+ return /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(MermaidView, { entry, load });
1259
1439
  }
1260
1440
 
1261
1441
  // src/components/ScenarioExplorer.tsx
1262
- var import_react8 = require("react");
1442
+ var import_react9 = require("react");
1263
1443
 
1264
1444
  // src/components/ui/command.tsx
1265
1445
  var React3 = require("react");
1266
1446
  var import_cmdk = require("cmdk");
1267
1447
  var import_lucide_react = require("lucide-react");
1268
- var import_jsx_runtime34 = require("react/jsx-runtime");
1448
+ var import_jsx_runtime35 = require("react/jsx-runtime");
1269
1449
  function Command({
1270
1450
  className,
1271
1451
  ...props
1272
1452
  }) {
1273
- return /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(
1453
+ return /* @__PURE__ */ (0, import_jsx_runtime35.jsx)(
1274
1454
  import_cmdk.Command,
1275
1455
  {
1276
1456
  "data-slot": "command",
@@ -1286,14 +1466,14 @@ function CommandInput({
1286
1466
  className,
1287
1467
  ...props
1288
1468
  }) {
1289
- return /* @__PURE__ */ (0, import_jsx_runtime34.jsxs)(
1469
+ return /* @__PURE__ */ (0, import_jsx_runtime35.jsxs)(
1290
1470
  "div",
1291
1471
  {
1292
1472
  "data-slot": "command-input-wrapper",
1293
1473
  className: "flex h-9 items-center gap-2 border-b px-3",
1294
1474
  children: [
1295
- /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(import_lucide_react.SearchIcon, { className: "size-4 shrink-0 opacity-50" }),
1296
- /* @__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)(
1297
1477
  import_cmdk.Command.Input,
1298
1478
  {
1299
1479
  "data-slot": "command-input",
@@ -1312,7 +1492,7 @@ function CommandList({
1312
1492
  className,
1313
1493
  ...props
1314
1494
  }) {
1315
- return /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(
1495
+ return /* @__PURE__ */ (0, import_jsx_runtime35.jsx)(
1316
1496
  import_cmdk.Command.List,
1317
1497
  {
1318
1498
  "data-slot": "command-list",
@@ -1327,7 +1507,7 @@ function CommandList({
1327
1507
  function CommandEmpty({
1328
1508
  ...props
1329
1509
  }) {
1330
- return /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(
1510
+ return /* @__PURE__ */ (0, import_jsx_runtime35.jsx)(
1331
1511
  import_cmdk.Command.Empty,
1332
1512
  {
1333
1513
  "data-slot": "command-empty",
@@ -1340,7 +1520,7 @@ function CommandGroup({
1340
1520
  className,
1341
1521
  ...props
1342
1522
  }) {
1343
- return /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(
1523
+ return /* @__PURE__ */ (0, import_jsx_runtime35.jsx)(
1344
1524
  import_cmdk.Command.Group,
1345
1525
  {
1346
1526
  "data-slot": "command-group",
@@ -1356,7 +1536,7 @@ function CommandItem({
1356
1536
  className,
1357
1537
  ...props
1358
1538
  }) {
1359
- return /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(
1539
+ return /* @__PURE__ */ (0, import_jsx_runtime35.jsx)(
1360
1540
  import_cmdk.Command.Item,
1361
1541
  {
1362
1542
  "data-slot": "command-item",
@@ -1370,7 +1550,7 @@ function CommandItem({
1370
1550
  }
1371
1551
 
1372
1552
  // src/components/ScenarioExplorer.tsx
1373
- var import_jsx_runtime35 = require("react/jsx-runtime");
1553
+ var import_jsx_runtime36 = require("react/jsx-runtime");
1374
1554
  var STATUS_FILTERS = [
1375
1555
  { value: "all", label: "All" },
1376
1556
  { value: "passed", label: "Passed" },
@@ -1379,24 +1559,24 @@ var STATUS_FILTERS = [
1379
1559
  { value: "pending", label: "Pending" }
1380
1560
  ];
1381
1561
  function ScenarioExplorer({ scenarios, summary }) {
1382
- const [status, setStatus] = (0, import_react8.useState)("all");
1562
+ const [status, setStatus] = (0, import_react9.useState)("all");
1383
1563
  const visible = status === "all" ? scenarios : scenarios.filter((s) => s.status === status);
1384
- return /* @__PURE__ */ (0, import_jsx_runtime35.jsxs)("div", { className: "font-sans text-foreground", children: [
1385
- 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: [
1386
1566
  summary.total,
1387
1567
  " scenarios \xB7 ",
1388
- /* @__PURE__ */ (0, import_jsx_runtime35.jsxs)("span", { className: "text-pass", children: [
1568
+ /* @__PURE__ */ (0, import_jsx_runtime36.jsxs)("span", { className: "text-pass", children: [
1389
1569
  summary.passed,
1390
1570
  " passed"
1391
1571
  ] }),
1392
1572
  " \xB7",
1393
1573
  " ",
1394
- /* @__PURE__ */ (0, import_jsx_runtime35.jsxs)("span", { className: "text-fail", children: [
1574
+ /* @__PURE__ */ (0, import_jsx_runtime36.jsxs)("span", { className: "text-fail", children: [
1395
1575
  summary.failed,
1396
1576
  " failed"
1397
1577
  ] })
1398
1578
  ] }) : null,
1399
- /* @__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)(
1400
1580
  "button",
1401
1581
  {
1402
1582
  type: "button",
@@ -1411,26 +1591,26 @@ function ScenarioExplorer({ scenarios, summary }) {
1411
1591
  },
1412
1592
  f.value
1413
1593
  )) }),
1414
- /* @__PURE__ */ (0, import_jsx_runtime35.jsxs)(
1594
+ /* @__PURE__ */ (0, import_jsx_runtime36.jsxs)(
1415
1595
  Command,
1416
1596
  {
1417
1597
  className: "rounded-lg border border-border bg-card",
1418
1598
  filter: (value, search) => value.toLowerCase().includes(search.toLowerCase()) ? 1 : 0,
1419
1599
  children: [
1420
- /* @__PURE__ */ (0, import_jsx_runtime35.jsx)(CommandInput, { placeholder: "Search scenarios\u2026" }),
1421
- /* @__PURE__ */ (0, import_jsx_runtime35.jsxs)(CommandList, { className: "max-h-none", children: [
1422
- /* @__PURE__ */ (0, import_jsx_runtime35.jsx)(CommandEmpty, { children: "No scenarios match your filters." }),
1423
- /* @__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)(
1424
1604
  CommandItem,
1425
1605
  {
1426
1606
  value: `${s.title} ${s.feature} ${s.tags.join(" ")}`,
1427
1607
  asChild: true,
1428
1608
  className: "cursor-pointer",
1429
- children: /* @__PURE__ */ (0, import_jsx_runtime35.jsxs)("a", { href: s.href, className: "flex items-center gap-3", children: [
1430
- /* @__PURE__ */ (0, import_jsx_runtime35.jsx)(Badge, { variant: s.status, children: s.status }),
1431
- /* @__PURE__ */ (0, import_jsx_runtime35.jsx)("span", { className: "min-w-0 flex-1 truncate font-medium", children: s.title }),
1432
- s.tags.slice(0, 3).map((t) => /* @__PURE__ */ (0, import_jsx_runtime35.jsx)(Badge, { variant: "tag", className: "hidden sm:inline-flex", children: t }, t)),
1433
- /* @__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 })
1434
1614
  ] })
1435
1615
  },
1436
1616
  s.id
@@ -1620,6 +1800,10 @@ var story_report_v1_default = {
1620
1800
  status: {
1621
1801
  $ref: "#/$defs/TestStatus"
1622
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
+ },
1623
1807
  durationMs: {
1624
1808
  type: "number",
1625
1809
  minimum: 0
@@ -2452,9 +2636,18 @@ function parseStoryReport(input) {
2452
2636
  ReportTrace,
2453
2637
  STORY_REPORT_SCHEMA_MAJOR,
2454
2638
  ScenarioExplorer,
2639
+ ScenarioRunHistory,
2640
+ ciDisplayName,
2641
+ commitUrl,
2642
+ currentStreak,
2643
+ describeRunHistory,
2455
2644
  err,
2645
+ formatRelativeAge,
2646
+ isReportStale,
2456
2647
  ok,
2457
2648
  parseStoryReport,
2649
+ prUrl,
2650
+ reportLastRunMs,
2458
2651
  storyReportSchema,
2459
2652
  useBuiltinRenderers,
2460
2653
  useCustomRenderers,