executable-stories-react 0.10.0 → 0.11.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
@@ -377,23 +377,11 @@ function useReport() {
377
377
  return ctx.report;
378
378
  }
379
379
 
380
- // ../executable-stories-core/dist/utils/duration.js
381
- function formatDuration(ms) {
382
- if (ms < 1e3) {
383
- return `${Math.round(ms)} ms`;
384
- }
385
- if (ms < 6e4) {
386
- return `${(ms / 1e3).toFixed(2)} s`;
387
- }
388
- const minutes = Math.floor(ms / 6e4);
389
- const seconds = Math.floor(ms % 6e4 / 1e3);
390
- if (minutes < 60) {
391
- return seconds > 0 ? `${minutes}m ${seconds}s` : `${minutes}m`;
392
- }
393
- const hours = Math.floor(minutes / 60);
394
- const remainingMinutes = minutes % 60;
395
- return `${hours}h ${remainingMinutes}m`;
396
- }
380
+ // src/components/ReportScenario.tsx
381
+ var import_duration3 = require("executable-stories-core/utils/duration");
382
+
383
+ // src/components/ReportSteps.tsx
384
+ var import_duration = require("executable-stories-core/utils/duration");
397
385
 
398
386
  // src/components/doc/DocNote.tsx
399
387
  var import_jsx_runtime3 = require("react/jsx-runtime");
@@ -482,36 +470,29 @@ function DocLink({ entry }) {
482
470
  );
483
471
  }
484
472
 
485
- // src/components/doc/DocSection.tsx
473
+ // src/lib/markdown.ts
486
474
  var import_marked = require("marked");
487
475
 
488
- // ../executable-stories-core/dist/utils/url.js
489
- function safeUrl(value) {
490
- if (!value) return void 0;
491
- const trimmed = value.trim();
492
- const scheme = /^([a-z][a-z0-9+.-]*):/i.exec(trimmed);
493
- if (scheme && !/^https?$/i.test(scheme[1] ?? "")) return void 0;
494
- return trimmed;
476
+ // src/lib/url.ts
477
+ var import_url = require("executable-stories-core/utils/url");
478
+
479
+ // src/lib/markdown.ts
480
+ function safeMarkdownHtml(markdown) {
481
+ const raw = import_marked.marked.parse(markdown, { async: false });
482
+ return raw.replace(/<script\b[^>]*>[\s\S]*?<\/script>/gi, "").replace(/<style\b[^>]*>[\s\S]*?<\/style>/gi, "").replace(/<\/?(?:iframe|object|embed|form|base)\b[^>]*>/gi, "").replace(/\son[a-z]+\s*=\s*"[^"]*"/gi, "").replace(/\son[a-z]+\s*=\s*'[^']*'/gi, "").replace(/\son[a-z]+\s*=\s*[^\s>]+/gi, "").replace(/(href|src)\s*=\s*"([^"]*)"/gi, (_m, attr, val) => `${attr}="${neutralizeUrl(val)}"`).replace(/(href|src)\s*=\s*'([^']*)'/gi, (_m, attr, val) => `${attr}='${neutralizeUrl(val)}'`);
495
483
  }
496
- function safeImageUrl(value) {
497
- if (!value) return void 0;
498
- const trimmed = value.trim();
499
- if (/^data:image\//i.test(trimmed)) return trimmed;
500
- return safeUrl(trimmed);
484
+ function dedent(text) {
485
+ const lines = text.replace(/\t/g, " ").split("\n");
486
+ const indents = lines.filter((line) => line.trim().length > 0).map((line) => line.length - line.trimStart().length);
487
+ const shortest = indents.length > 0 ? Math.min(...indents) : 0;
488
+ return lines.map((line) => line.slice(shortest)).join("\n").trim();
501
489
  }
502
- function isLocalFsPath(value) {
503
- return /^(?:[/\\]|[A-Za-z]:[/\\])/.test(value);
490
+ function neutralizeUrl(value) {
491
+ return (0, import_url.safeUrl)(value) ?? "#";
504
492
  }
505
493
 
506
494
  // src/components/doc/DocSection.tsx
507
495
  var import_jsx_runtime9 = require("react/jsx-runtime");
508
- function neutralizeUrl(value) {
509
- return safeUrl(value) ?? "#";
510
- }
511
- function safeMarkdownHtml(markdown) {
512
- const raw = import_marked.marked.parse(markdown, { async: false });
513
- return raw.replace(/<script\b[^>]*>[\s\S]*?<\/script>/gi, "").replace(/<style\b[^>]*>[\s\S]*?<\/style>/gi, "").replace(/<\/?(?:iframe|object|embed|form|base)\b[^>]*>/gi, "").replace(/\son[a-z]+\s*=\s*"[^"]*"/gi, "").replace(/\son[a-z]+\s*=\s*'[^']*'/gi, "").replace(/\son[a-z]+\s*=\s*[^\s>]+/gi, "").replace(/(href|src)\s*=\s*"([^"]*)"/gi, (_m, attr, val) => `${attr}="${neutralizeUrl(val)}"`).replace(/(href|src)\s*=\s*'([^']*)'/gi, (_m, attr, val) => `${attr}='${neutralizeUrl(val)}'`);
514
- }
515
496
  function DocSection({ entry }) {
516
497
  const renderers = useBuiltinRenderers();
517
498
  if (renderers.section) {
@@ -557,7 +538,7 @@ function DocMermaid({ entry }) {
557
538
  // src/components/doc/DocScreenshot.tsx
558
539
  var import_jsx_runtime11 = require("react/jsx-runtime");
559
540
  function DocScreenshot({ entry }) {
560
- const src = isLocalFsPath(entry.path) ? void 0 : safeImageUrl(entry.path);
541
+ const src = (0, import_url.isLocalFsPath)(entry.path) ? void 0 : (0, import_url.safeImageUrl)(entry.path);
561
542
  if (!src) {
562
543
  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: [
563
544
  /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("div", { className: "font-medium", children: "Screenshot unavailable" }),
@@ -582,15 +563,15 @@ function DocScreenshot({ entry }) {
582
563
  // src/components/doc/DocVideo.tsx
583
564
  var import_jsx_runtime12 = require("react/jsx-runtime");
584
565
  function DocVideo({ entry }) {
585
- if (isLocalFsPath(entry.path)) {
566
+ if ((0, import_url.isLocalFsPath)(entry.path)) {
586
567
  return /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("figure", { className: "my-3 rounded-md border border-dashed border-border p-3 text-sm text-muted-foreground", children: [
587
568
  /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("div", { className: "font-medium", children: "Video unavailable" }),
588
569
  /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("div", { className: "mt-1 break-all font-mono text-xs", children: entry.path }),
589
570
  entry.caption ? /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("figcaption", { className: "mt-1.5 text-xs", children: entry.caption }) : null
590
571
  ] });
591
572
  }
592
- const src = safeUrl(entry.path);
593
- const poster = safeUrl(entry.poster);
573
+ const src = (0, import_url.safeUrl)(entry.path);
574
+ const poster = (0, import_url.safeUrl)(entry.poster);
594
575
  return /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("figure", { className: "my-3", children: [
595
576
  /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
596
577
  "video",
@@ -611,7 +592,7 @@ var import_jsx_runtime13 = require("react/jsx-runtime");
611
592
  function DocHtml({ entry }) {
612
593
  const height = typeof entry.height === "number" ? `${entry.height}px` : entry.height ?? "400px";
613
594
  const title = entry.title ?? "Embedded HTML";
614
- const safeHref = safeUrl(entry.url ?? entry.path);
595
+ const safeHref = (0, import_url.safeUrl)(entry.url ?? entry.path);
615
596
  const frameProps = entry.content !== void 0 ? { srcDoc: entry.content } : { src: safeHref ?? "" };
616
597
  return /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)("figure", { className: "my-3 overflow-hidden rounded-md border border-border", children: [
617
598
  /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)("figcaption", { className: "flex items-center justify-between border-b border-border bg-muted/40 px-3 py-1.5 text-xs font-medium text-muted-foreground", children: [
@@ -778,7 +759,7 @@ function ReportStepItem({ step }) {
778
759
  }
779
760
  ),
780
761
  /* @__PURE__ */ (0, import_jsx_runtime19.jsx)("span", { className: "flex-1 text-foreground", children: highlightStepParams(step.text) }),
781
- step.durationMs > 0 ? /* @__PURE__ */ (0, import_jsx_runtime19.jsx)("span", { className: "shrink-0 whitespace-nowrap font-mono text-[0.6875rem] text-muted-foreground", children: formatDuration(step.durationMs) }) : null
762
+ step.durationMs > 0 ? /* @__PURE__ */ (0, import_jsx_runtime19.jsx)("span", { className: "shrink-0 whitespace-nowrap font-mono text-[0.6875rem] text-muted-foreground", children: (0, import_duration.formatDuration)(step.durationMs) }) : null
782
763
  ] }),
783
764
  step.errorMessage ? /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(
784
765
  "pre",
@@ -794,125 +775,10 @@ function ReportStepItem({ step }) {
794
775
  );
795
776
  }
796
777
 
797
- // ../executable-stories-core/dist/state-diff.js
798
- var isPlainObject = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
799
- function deepEqual(a, b) {
800
- if (Object.is(a, b)) return true;
801
- if (Array.isArray(a) && Array.isArray(b)) {
802
- return a.length === b.length && a.every((v, i) => deepEqual(v, b[i]));
803
- }
804
- if (isPlainObject(a) && isPlainObject(b)) {
805
- const ak = Object.keys(a);
806
- const bk = Object.keys(b);
807
- return ak.length === bk.length && ak.every((k) => k in b && deepEqual(a[k], b[k]));
808
- }
809
- return false;
810
- }
811
- var joinKey = (path, key) => path === "" ? key : `${path}.${key}`;
812
- function walk(path, before, after, out) {
813
- if (Object.is(before, after)) return;
814
- if (Array.isArray(before) && Array.isArray(after)) {
815
- const shared = Math.min(before.length, after.length);
816
- for (let i = 0; i < shared; i++) walk(`${path}[${i}]`, before[i], after[i], out);
817
- for (let i = shared; i < after.length; i++) out.push({ path: `${path}[${i}]`, kind: "added", after: after[i] });
818
- for (let i = shared; i < before.length; i++) out.push({ path: `${path}[${i}]`, kind: "removed", before: before[i] });
819
- return;
820
- }
821
- if (isPlainObject(before) && isPlainObject(after)) {
822
- for (const key of Object.keys(after)) {
823
- const p = joinKey(path, key);
824
- if (key in before) walk(p, before[key], after[key], out);
825
- else out.push({ path: p, kind: "added", after: after[key] });
826
- }
827
- for (const key of Object.keys(before)) {
828
- if (!(key in after)) out.push({ path: joinKey(path, key), kind: "removed", before: before[key] });
829
- }
830
- return;
831
- }
832
- if (!deepEqual(before, after)) out.push({ path, kind: "changed", before, after });
833
- }
834
- function diffStateValues(before, after) {
835
- const out = [];
836
- walk("", before, after, out);
837
- return out;
838
- }
839
- function formatStateValue(value, maxLen = 60) {
840
- const s = value === void 0 ? "undefined" : JSON.stringify(value);
841
- return s.length > maxLen ? `${s.slice(0, maxLen - 1)}\u2026` : s;
842
- }
843
- function summarizeStateChanges(changes) {
844
- return changes.map((c) => {
845
- const at = c.path === "" ? "(root)" : c.path;
846
- switch (c.kind) {
847
- case "added":
848
- return `+ ${at}: ${formatStateValue(c.after)}`;
849
- case "removed":
850
- return `\u2212 ${at}`;
851
- case "changed":
852
- return `${at}: ${formatStateValue(c.before)} \u2192 ${formatStateValue(c.after)}`;
853
- }
854
- });
855
- }
856
-
857
- // ../executable-stories-core/dist/storyboard.js
858
- function findScreenshot(entries) {
859
- for (const entry of entries) {
860
- if (entry.kind === "screenshot") return entry;
861
- if (entry.children) {
862
- const nested = findScreenshot(entry.children);
863
- if (nested) return nested;
864
- }
865
- }
866
- return void 0;
867
- }
868
- function findStates(entries, out = []) {
869
- for (const entry of entries) {
870
- if (entry.kind === "state") out.push(entry);
871
- if (entry.children) findStates(entry.children, out);
872
- }
873
- return out;
874
- }
875
- var laneOf = (label) => label ?? "";
876
- function extractStoryboardFrames(scenario) {
877
- const frames = [];
878
- const laneOrder = [];
879
- const previous = /* @__PURE__ */ new Map();
880
- for (const step of scenario.steps) {
881
- const shot = findScreenshot(step.docEntries);
882
- const stateDocs = findStates(step.docEntries);
883
- if (!shot && stateDocs.length === 0) continue;
884
- const byLane = /* @__PURE__ */ new Map();
885
- for (const doc of stateDocs) {
886
- const lane = laneOf(doc.label);
887
- if (!byLane.has(lane) && !laneOrder.includes(lane)) laneOrder.push(lane);
888
- byLane.set(lane, doc);
889
- }
890
- const states = [];
891
- for (const lane of laneOrder) {
892
- const doc = byLane.get(lane);
893
- if (!doc) continue;
894
- const card = {
895
- ...doc.label !== void 0 && { label: doc.label },
896
- value: doc.value
897
- };
898
- if (previous.has(lane)) card.changes = diffStateValues(previous.get(lane), doc.value);
899
- previous.set(lane, doc.value);
900
- states.push(card);
901
- }
902
- frames.push({
903
- stepId: step.id,
904
- stepIndex: step.index,
905
- keyword: step.keyword,
906
- text: step.text,
907
- status: step.status,
908
- ...shot && { screenshot: { path: shot.path, ...shot.alt !== void 0 && { alt: shot.alt } } },
909
- states
910
- });
911
- }
912
- return frames;
913
- }
914
-
915
778
  // src/components/ReportStoryboard.tsx
779
+ var import_storyboard = require("executable-stories-core/storyboard");
780
+ var import_state_diff = require("executable-stories-core/state-diff");
781
+ var import_url6 = require("executable-stories-core/utils/url");
916
782
  var import_jsx_runtime20 = require("react/jsx-runtime");
917
783
  var FRAME_GLYPH = {
918
784
  passed: "\u2713",
@@ -927,7 +793,7 @@ var FRAME_GLYPH_COLOR = {
927
793
  pending: "text-pend"
928
794
  };
929
795
  function ReportStoryboard({ scenario }) {
930
- const frames = extractStoryboardFrames(scenario);
796
+ const frames = (0, import_storyboard.extractStoryboardFrames)(scenario);
931
797
  if (frames.length < 2) return null;
932
798
  return /* @__PURE__ */ (0, import_jsx_runtime20.jsxs)("section", { "data-slot": "storyboard", "aria-label": "Storyboard", className: "mb-3", children: [
933
799
  /* @__PURE__ */ (0, import_jsx_runtime20.jsx)("h4", { className: "mb-1.5 text-xs font-semibold tracking-wide text-muted-foreground uppercase", children: "Storyboard" }),
@@ -961,7 +827,7 @@ function StoryboardFrameItem({ frame }) {
961
827
  }
962
828
  function FrameScreenshot({ frame, label }) {
963
829
  const path = frame.screenshot?.path ?? "";
964
- const src = isLocalFsPath(path) ? void 0 : safeImageUrl(path);
830
+ const src = (0, import_url6.isLocalFsPath)(path) ? void 0 : (0, import_url6.safeImageUrl)(path);
965
831
  return /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(
966
832
  "a",
967
833
  {
@@ -1007,7 +873,7 @@ function StateCardView({ card }) {
1007
873
  "rounded-sm px-1.5 py-0.5 font-mono text-[0.625rem] leading-snug break-all",
1008
874
  CHANGE_TONE[change.kind]
1009
875
  ),
1010
- children: summarizeStateChanges([change])[0]
876
+ children: (0, import_state_diff.summarizeStateChanges)([change])[0]
1011
877
  },
1012
878
  i
1013
879
  )) }) : /* @__PURE__ */ (0, import_jsx_runtime20.jsx)("p", { className: "mt-1.5 text-[0.625rem] text-muted-foreground", children: "No changes" })),
@@ -1067,6 +933,9 @@ function ReportAttachments({ attachments }) {
1067
933
  );
1068
934
  }
1069
935
 
936
+ // src/components/ReportTrace.tsx
937
+ var import_duration2 = require("executable-stories-core/utils/duration");
938
+
1070
939
  // src/lib/trace.ts
1071
940
  var VALID = /* @__PURE__ */ new Set(["ok", "error", "unset"]);
1072
941
  var safeStatus = (s) => VALID.has(s) ? s : "unset";
@@ -1119,15 +988,15 @@ function toTraceRows(spans) {
1119
988
  roots.sort((a, b) => a.span.startTimeMs - b.span.startTimeMs);
1120
989
  const rows = [];
1121
990
  const seen = /* @__PURE__ */ new Set();
1122
- const walk2 = (node, depth) => {
991
+ const walk = (node, depth) => {
1123
992
  if (seen.has(node.span.spanId)) return;
1124
993
  seen.add(node.span.spanId);
1125
994
  rows.push({ ...node.span, depth });
1126
- for (const child of node.children) walk2(child, depth + 1);
995
+ for (const child of node.children) walk(child, depth + 1);
1127
996
  };
1128
- for (const root of roots) walk2(root, 0);
997
+ for (const root of roots) walk(root, 0);
1129
998
  for (const node of byId.values()) {
1130
- if (!seen.has(node.span.spanId)) walk2(node, 0);
999
+ if (!seen.has(node.span.spanId)) walk(node, 0);
1131
1000
  }
1132
1001
  return rows;
1133
1002
  }
@@ -1161,7 +1030,7 @@ function ReportTrace({ spans }) {
1161
1030
  " span",
1162
1031
  rows.length === 1 ? "" : "s",
1163
1032
  ", ",
1164
- formatDuration(totalMs),
1033
+ (0, import_duration2.formatDuration)(totalMs),
1165
1034
  ")"
1166
1035
  ] }),
1167
1036
  /* @__PURE__ */ (0, import_jsx_runtime22.jsx)("ul", { className: "flex flex-col gap-0.5 px-3 pb-3 font-mono text-[0.6875rem]", children: rows.map((row, i) => {
@@ -1184,7 +1053,7 @@ function ReportTrace({ spans }) {
1184
1053
  style: { left: `${leftPct}%`, width: `${widthPct}%` }
1185
1054
  }
1186
1055
  ) }),
1187
- /* @__PURE__ */ (0, import_jsx_runtime22.jsx)("span", { className: "w-16 shrink-0 text-right text-muted-foreground", children: formatDuration(row.durationMs) })
1056
+ /* @__PURE__ */ (0, import_jsx_runtime22.jsx)("span", { className: "w-16 shrink-0 text-right text-muted-foreground", children: (0, import_duration2.formatDuration)(row.durationMs) })
1188
1057
  ] }, `${row.spanId}-${i}`);
1189
1058
  }) })
1190
1059
  ] });
@@ -1295,6 +1164,7 @@ function useCollapse() {
1295
1164
 
1296
1165
  // src/interactive/scenario-actions.tsx
1297
1166
  var import_react7 = require("react");
1167
+ var import_scenario_markdown = require("executable-stories-core/utils/scenario-markdown");
1298
1168
  var ScenarioActionsContext = (0, import_react7.createContext)(null);
1299
1169
  var ScenarioActionsProvider = ScenarioActionsContext.Provider;
1300
1170
  function useScenarioActions() {
@@ -1440,7 +1310,7 @@ function ReportScenario({ scenario, hideTitle = false }) {
1440
1310
  ] }),
1441
1311
  /* @__PURE__ */ (0, import_jsx_runtime26.jsxs)("div", { className: "flex shrink-0 items-center gap-2", children: [
1442
1312
  /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(ScenarioRunHistory, { scenarioId: scenario.id }),
1443
- scenario.durationMs > 0 ? /* @__PURE__ */ (0, import_jsx_runtime26.jsx)("span", { className: "whitespace-nowrap font-mono text-[0.6875rem] text-muted-foreground", children: formatDuration(scenario.durationMs) }) : null,
1313
+ scenario.durationMs > 0 ? /* @__PURE__ */ (0, import_jsx_runtime26.jsx)("span", { className: "whitespace-nowrap font-mono text-[0.6875rem] text-muted-foreground", children: (0, import_duration3.formatDuration)(scenario.durationMs) }) : null,
1444
1314
  meta.showPill ? /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(Badge, { variant: meta.badgeVariant, "aria-label": `Status: ${label}`, children: label }) : /* @__PURE__ */ (0, import_jsx_runtime26.jsx)("span", { className: "sr-only", children: `Status: ${label}` }),
1445
1315
  actions ? /* @__PURE__ */ (0, import_jsx_runtime26.jsxs)(DropdownMenu, { children: [
1446
1316
  /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(
@@ -1499,8 +1369,35 @@ function ReportScenarioList({ feature }) {
1499
1369
  return /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(import_jsx_runtime27.Fragment, { children: feature.scenarios.map((scenario) => /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(ReportScenario, { scenario }, scenario.id)) });
1500
1370
  }
1501
1371
 
1502
- // src/components/ReportFeature.tsx
1372
+ // src/components/ReportFeatureIntro.tsx
1503
1373
  var import_jsx_runtime28 = require("react/jsx-runtime");
1374
+ function ReportFeatureIntro({ feature }) {
1375
+ const hasNarrative = Boolean(feature.narrative?.trim());
1376
+ const hasGlossary = Boolean(feature.glossary?.length);
1377
+ if (!hasNarrative && !hasGlossary) return null;
1378
+ return /* @__PURE__ */ (0, import_jsx_runtime28.jsxs)("div", { className: "flex flex-col gap-3 border-l-2 border-border pl-4 text-sm", children: [
1379
+ hasNarrative ? /* @__PURE__ */ (0, import_jsx_runtime28.jsx)(
1380
+ "div",
1381
+ {
1382
+ className: "es-doc-prose prose prose-sm max-w-none text-muted-foreground",
1383
+ dangerouslySetInnerHTML: { __html: safeMarkdownHtml(dedent(feature.narrative)) }
1384
+ }
1385
+ ) : null,
1386
+ hasGlossary ? /* @__PURE__ */ (0, import_jsx_runtime28.jsx)("dl", { className: "grid gap-1.5", "aria-label": "Glossary", children: feature.glossary.map((entry) => /* @__PURE__ */ (0, import_jsx_runtime28.jsxs)("div", { className: "flex flex-wrap gap-x-2 text-xs", children: [
1387
+ /* @__PURE__ */ (0, import_jsx_runtime28.jsx)("dt", { className: "font-medium text-foreground", children: entry.term }),
1388
+ /* @__PURE__ */ (0, import_jsx_runtime28.jsx)("dd", { className: "flex-1 text-muted-foreground", children: entry.definition })
1389
+ ] }, entry.term)) }) : null
1390
+ ] });
1391
+ }
1392
+ function featureKindLabel(kind) {
1393
+ if (kind === "ability") return "Ability";
1394
+ if (kind === "business-need") return "Business need";
1395
+ if (kind === "feature") return "Feature";
1396
+ return null;
1397
+ }
1398
+
1399
+ // src/components/ReportFeature.tsx
1400
+ var import_jsx_runtime29 = require("react/jsx-runtime");
1504
1401
  function ReportFeature({ feature }) {
1505
1402
  const titleId = `${feature.id}-title`;
1506
1403
  const bodyId = `${feature.id}-body`;
@@ -1508,10 +1405,11 @@ function ReportFeature({ feature }) {
1508
1405
  const skipped = s.skipped + s.pending;
1509
1406
  const collapse = useCollapse();
1510
1407
  const collapsed = collapse?.isCollapsed(feature.id) ?? false;
1408
+ const kindLabel = featureKindLabel(feature.kind);
1511
1409
  return (
1512
1410
  // Suite = a section heading + a divider, NOT another bordered card. Drops a
1513
1411
  // layer of nesting so the scenario cards below read as the primary unit.
1514
- /* @__PURE__ */ (0, import_jsx_runtime28.jsxs)(
1412
+ /* @__PURE__ */ (0, import_jsx_runtime29.jsxs)(
1515
1413
  "section",
1516
1414
  {
1517
1415
  id: feature.id,
@@ -1519,9 +1417,9 @@ function ReportFeature({ feature }) {
1519
1417
  "aria-labelledby": titleId,
1520
1418
  className: "flex flex-col gap-3",
1521
1419
  children: [
1522
- /* @__PURE__ */ (0, import_jsx_runtime28.jsxs)("div", { className: "flex items-end justify-between gap-4 border-b border-border pb-2", children: [
1523
- /* @__PURE__ */ (0, import_jsx_runtime28.jsxs)("div", { className: "min-w-0", children: [
1524
- /* @__PURE__ */ (0, import_jsx_runtime28.jsx)("h2", { id: titleId, className: "text-base font-semibold tracking-tight text-foreground", children: collapse ? /* @__PURE__ */ (0, import_jsx_runtime28.jsxs)(
1420
+ /* @__PURE__ */ (0, import_jsx_runtime29.jsxs)("div", { className: "flex items-end justify-between gap-4 border-b border-border pb-2", children: [
1421
+ /* @__PURE__ */ (0, import_jsx_runtime29.jsxs)("div", { className: "min-w-0", children: [
1422
+ /* @__PURE__ */ (0, import_jsx_runtime29.jsx)("h2", { id: titleId, className: "text-base font-semibold tracking-tight text-foreground", children: collapse ? /* @__PURE__ */ (0, import_jsx_runtime29.jsxs)(
1525
1423
  "button",
1526
1424
  {
1527
1425
  type: "button",
@@ -1530,31 +1428,34 @@ function ReportFeature({ feature }) {
1530
1428
  "aria-controls": bodyId,
1531
1429
  className: "flex w-full cursor-pointer items-center gap-2 text-left hover:text-foreground/80",
1532
1430
  children: [
1533
- /* @__PURE__ */ (0, import_jsx_runtime28.jsx)("span", { "aria-hidden": true, className: cn("shrink-0 text-xs text-muted-foreground transition-transform", !collapsed && "rotate-90"), children: "\u25B8" }),
1534
- /* @__PURE__ */ (0, import_jsx_runtime28.jsx)("span", { className: "truncate", children: feature.title })
1431
+ /* @__PURE__ */ (0, import_jsx_runtime29.jsx)("span", { "aria-hidden": true, className: cn("shrink-0 text-xs text-muted-foreground transition-transform", !collapsed && "rotate-90"), children: "\u25B8" }),
1432
+ /* @__PURE__ */ (0, import_jsx_runtime29.jsx)("span", { className: "truncate", children: feature.title })
1535
1433
  ]
1536
1434
  }
1537
1435
  ) : feature.title }),
1538
- /* @__PURE__ */ (0, import_jsx_runtime28.jsx)("p", { className: cn("mt-0.5 truncate font-mono text-xs text-muted-foreground", collapse && "pl-5"), children: feature.sourceFile })
1436
+ /* @__PURE__ */ (0, import_jsx_runtime29.jsxs)("p", { className: cn("mt-0.5 flex items-center gap-2 text-xs text-muted-foreground", collapse && "pl-5"), children: [
1437
+ kindLabel ? /* @__PURE__ */ (0, import_jsx_runtime29.jsx)("span", { className: "shrink-0 rounded-sm bg-muted px-1.5 py-0.5 font-medium text-foreground", children: kindLabel }) : null,
1438
+ /* @__PURE__ */ (0, import_jsx_runtime29.jsx)("span", { className: "truncate font-mono", children: feature.sourceFile })
1439
+ ] })
1539
1440
  ] }),
1540
- /* @__PURE__ */ (0, import_jsx_runtime28.jsxs)(
1441
+ /* @__PURE__ */ (0, import_jsx_runtime29.jsxs)(
1541
1442
  "p",
1542
1443
  {
1543
1444
  className: "flex shrink-0 items-center gap-1.5 text-xs text-muted-foreground",
1544
1445
  "aria-label": `${s.passed} passed, ${s.failed} failed, ${skipped} skipped`,
1545
1446
  children: [
1546
- /* @__PURE__ */ (0, import_jsx_runtime28.jsxs)("span", { children: [
1447
+ /* @__PURE__ */ (0, import_jsx_runtime29.jsxs)("span", { children: [
1547
1448
  s.passed,
1548
1449
  " passed"
1549
1450
  ] }),
1550
- /* @__PURE__ */ (0, import_jsx_runtime28.jsx)("span", { "aria-hidden": true, children: "\xB7" }),
1551
- /* @__PURE__ */ (0, import_jsx_runtime28.jsxs)("span", { className: cn(s.failed > 0 && "font-medium text-fail"), children: [
1451
+ /* @__PURE__ */ (0, import_jsx_runtime29.jsx)("span", { "aria-hidden": true, children: "\xB7" }),
1452
+ /* @__PURE__ */ (0, import_jsx_runtime29.jsxs)("span", { className: cn(s.failed > 0 && "font-medium text-fail"), children: [
1552
1453
  s.failed,
1553
1454
  " failed"
1554
1455
  ] }),
1555
- skipped > 0 ? /* @__PURE__ */ (0, import_jsx_runtime28.jsxs)(import_jsx_runtime28.Fragment, { children: [
1556
- /* @__PURE__ */ (0, import_jsx_runtime28.jsx)("span", { "aria-hidden": true, children: "\xB7" }),
1557
- /* @__PURE__ */ (0, import_jsx_runtime28.jsxs)("span", { children: [
1456
+ skipped > 0 ? /* @__PURE__ */ (0, import_jsx_runtime29.jsxs)(import_jsx_runtime29.Fragment, { children: [
1457
+ /* @__PURE__ */ (0, import_jsx_runtime29.jsx)("span", { "aria-hidden": true, children: "\xB7" }),
1458
+ /* @__PURE__ */ (0, import_jsx_runtime29.jsxs)("span", { children: [
1558
1459
  skipped,
1559
1460
  " skipped"
1560
1461
  ] })
@@ -1563,7 +1464,10 @@ function ReportFeature({ feature }) {
1563
1464
  }
1564
1465
  )
1565
1466
  ] }),
1566
- /* @__PURE__ */ (0, import_jsx_runtime28.jsx)("div", { id: bodyId, hidden: collapsed, className: "flex flex-col gap-3", children: /* @__PURE__ */ (0, import_jsx_runtime28.jsx)(ReportScenarioList, { feature }) })
1467
+ /* @__PURE__ */ (0, import_jsx_runtime29.jsxs)("div", { id: bodyId, hidden: collapsed, className: "flex flex-col gap-3", children: [
1468
+ /* @__PURE__ */ (0, import_jsx_runtime29.jsx)(ReportFeatureIntro, { feature }),
1469
+ /* @__PURE__ */ (0, import_jsx_runtime29.jsx)(ReportScenarioList, { feature })
1470
+ ] })
1567
1471
  ]
1568
1472
  }
1569
1473
  )
@@ -1571,17 +1475,17 @@ function ReportFeature({ feature }) {
1571
1475
  }
1572
1476
 
1573
1477
  // src/components/ReportFeatureList.tsx
1574
- var import_jsx_runtime29 = require("react/jsx-runtime");
1478
+ var import_jsx_runtime30 = require("react/jsx-runtime");
1575
1479
  function ReportFeatureList() {
1576
1480
  const report = useReport();
1577
- return /* @__PURE__ */ (0, import_jsx_runtime29.jsx)("div", { className: "flex flex-col gap-8", children: report.features.map((feature) => /* @__PURE__ */ (0, import_jsx_runtime29.jsx)(ReportFeature, { feature }, feature.id)) });
1481
+ return /* @__PURE__ */ (0, import_jsx_runtime30.jsx)("div", { className: "flex flex-col gap-8", children: report.features.map((feature) => /* @__PURE__ */ (0, import_jsx_runtime30.jsx)(ReportFeature, { feature }, feature.id)) });
1578
1482
  }
1579
1483
 
1580
1484
  // src/components/ui/empty.tsx
1581
1485
  var import_class_variance_authority2 = require("class-variance-authority");
1582
- var import_jsx_runtime30 = require("react/jsx-runtime");
1486
+ var import_jsx_runtime31 = require("react/jsx-runtime");
1583
1487
  function Empty({ className, ...props }) {
1584
- return /* @__PURE__ */ (0, import_jsx_runtime30.jsx)(
1488
+ return /* @__PURE__ */ (0, import_jsx_runtime31.jsx)(
1585
1489
  "div",
1586
1490
  {
1587
1491
  "data-slot": "empty",
@@ -1594,7 +1498,7 @@ function Empty({ className, ...props }) {
1594
1498
  );
1595
1499
  }
1596
1500
  function EmptyHeader({ className, ...props }) {
1597
- return /* @__PURE__ */ (0, import_jsx_runtime30.jsx)(
1501
+ return /* @__PURE__ */ (0, import_jsx_runtime31.jsx)(
1598
1502
  "div",
1599
1503
  {
1600
1504
  "data-slot": "empty-header",
@@ -1621,7 +1525,7 @@ var emptyMediaVariants = (0, import_class_variance_authority2.cva)(
1621
1525
  }
1622
1526
  );
1623
1527
  function EmptyTitle({ className, ...props }) {
1624
- return /* @__PURE__ */ (0, import_jsx_runtime30.jsx)(
1528
+ return /* @__PURE__ */ (0, import_jsx_runtime31.jsx)(
1625
1529
  "div",
1626
1530
  {
1627
1531
  "data-slot": "empty-title",
@@ -1632,16 +1536,16 @@ function EmptyTitle({ className, ...props }) {
1632
1536
  }
1633
1537
 
1634
1538
  // src/components/ReportEmpty.tsx
1635
- var import_jsx_runtime31 = require("react/jsx-runtime");
1539
+ var import_jsx_runtime32 = require("react/jsx-runtime");
1636
1540
  function ReportEmpty({ message }) {
1637
- return /* @__PURE__ */ (0, import_jsx_runtime31.jsx)(Empty, { "aria-live": "polite", className: "border", children: /* @__PURE__ */ (0, import_jsx_runtime31.jsx)(EmptyHeader, { children: /* @__PURE__ */ (0, import_jsx_runtime31.jsx)(EmptyTitle, { children: message ?? "No scenarios in this report." }) }) });
1541
+ return /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(Empty, { "aria-live": "polite", className: "border", children: /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(EmptyHeader, { children: /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(EmptyTitle, { children: message ?? "No scenarios in this report." }) }) });
1638
1542
  }
1639
1543
 
1640
1544
  // src/components/ReportSummary.tsx
1641
- var import_jsx_runtime32 = require("react/jsx-runtime");
1545
+ var import_jsx_runtime33 = require("react/jsx-runtime");
1642
1546
  function ReportSummary({ className }) {
1643
1547
  const report = useReport();
1644
- return /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(
1548
+ return /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(
1645
1549
  ReportSummaryView,
1646
1550
  {
1647
1551
  summary: report.summary,
@@ -1667,7 +1571,7 @@ function ReportSummaryView({ summary, className, ariaLabel }) {
1667
1571
  if (summary.pending > 0) {
1668
1572
  cards.push({ status: "pending", label: "Pending", value: summary.pending });
1669
1573
  }
1670
- return /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(
1574
+ return /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(
1671
1575
  "dl",
1672
1576
  {
1673
1577
  className: cn(
@@ -1680,17 +1584,17 @@ function ReportSummaryView({ summary, className, ariaLabel }) {
1680
1584
  "aria-label": ariaLabel,
1681
1585
  children: cards.map((c) => {
1682
1586
  const tone = TONE[c.status];
1683
- return /* @__PURE__ */ (0, import_jsx_runtime32.jsxs)(
1587
+ return /* @__PURE__ */ (0, import_jsx_runtime33.jsxs)(
1684
1588
  "div",
1685
1589
  {
1686
1590
  "data-status": c.status,
1687
1591
  className: "rounded-lg border bg-card px-4 py-3",
1688
1592
  children: [
1689
- /* @__PURE__ */ (0, import_jsx_runtime32.jsxs)("dt", { className: "flex items-center gap-1.5 font-mono text-[11px] font-medium uppercase tracking-wider text-muted-foreground", children: [
1690
- /* @__PURE__ */ (0, import_jsx_runtime32.jsx)("span", { className: cn("size-1.5 shrink-0 rounded-full", tone.dot), "aria-hidden": "true" }),
1593
+ /* @__PURE__ */ (0, import_jsx_runtime33.jsxs)("dt", { className: "flex items-center gap-1.5 font-mono text-[11px] font-medium uppercase tracking-wider text-muted-foreground", children: [
1594
+ /* @__PURE__ */ (0, import_jsx_runtime33.jsx)("span", { className: cn("size-1.5 shrink-0 rounded-full", tone.dot), "aria-hidden": "true" }),
1691
1595
  c.label
1692
1596
  ] }),
1693
- /* @__PURE__ */ (0, import_jsx_runtime32.jsx)("dd", { className: cn("mt-2 text-3xl font-semibold leading-none tracking-tight tabular-nums", tone.num), children: c.value })
1597
+ /* @__PURE__ */ (0, import_jsx_runtime33.jsx)("dd", { className: cn("mt-2 text-3xl font-semibold leading-none tracking-tight tabular-nums", tone.num), children: c.value })
1694
1598
  ]
1695
1599
  },
1696
1600
  c.status
@@ -1700,6 +1604,9 @@ function ReportSummaryView({ summary, className, ariaLabel }) {
1700
1604
  );
1701
1605
  }
1702
1606
 
1607
+ // src/components/ReportMeta.tsx
1608
+ var import_duration4 = require("executable-stories-core/utils/duration");
1609
+
1703
1610
  // src/lib/provenance.ts
1704
1611
  function reportLastRunMs(report) {
1705
1612
  if (report.finishedAtMs && report.finishedAtMs > 0) return report.finishedAtMs;
@@ -1750,7 +1657,7 @@ function prUrl(ci) {
1750
1657
  }
1751
1658
 
1752
1659
  // src/components/ReportMeta.tsx
1753
- var import_jsx_runtime33 = require("react/jsx-runtime");
1660
+ var import_jsx_runtime34 = require("react/jsx-runtime");
1754
1661
  function ReportMeta() {
1755
1662
  const report = useReport();
1756
1663
  const items = [];
@@ -1759,7 +1666,7 @@ function ReportMeta() {
1759
1666
  items.push({ label: "Started", value: new Date(report.startedAtMs).toLocaleString() });
1760
1667
  }
1761
1668
  if (report.durationMs) {
1762
- items.push({ label: "Duration", value: formatDuration(report.durationMs) });
1669
+ items.push({ label: "Duration", value: (0, import_duration4.formatDuration)(report.durationMs) });
1763
1670
  }
1764
1671
  if (report.packageVersion) {
1765
1672
  items.push({ label: "Version", value: report.packageVersion });
@@ -1773,13 +1680,13 @@ function ReportMeta() {
1773
1680
  const short = sha.slice(0, 8);
1774
1681
  items.push({
1775
1682
  label: "Commit",
1776
- value: url ? /* @__PURE__ */ (0, import_jsx_runtime33.jsx)("a", { href: url, target: "_blank", rel: "noreferrer noopener", className: linkClass, children: short }) : short
1683
+ value: url ? /* @__PURE__ */ (0, import_jsx_runtime34.jsx)("a", { href: url, target: "_blank", rel: "noreferrer noopener", className: linkClass, children: short }) : short
1777
1684
  });
1778
1685
  }
1779
1686
  if (report.ci) {
1780
1687
  items.push({
1781
1688
  label: "CI",
1782
- value: report.ci.url ? /* @__PURE__ */ (0, import_jsx_runtime33.jsxs)("a", { href: report.ci.url, target: "_blank", rel: "noreferrer noopener", className: linkClass, children: [
1689
+ value: report.ci.url ? /* @__PURE__ */ (0, import_jsx_runtime34.jsxs)("a", { href: report.ci.url, target: "_blank", rel: "noreferrer noopener", className: linkClass, children: [
1783
1690
  ciDisplayName(report.ci.name),
1784
1691
  report.ci.buildNumber ? ` #${report.ci.buildNumber}` : ""
1785
1692
  ] }) : `${ciDisplayName(report.ci.name)}${report.ci.buildNumber ? ` #${report.ci.buildNumber}` : ""}`
@@ -1790,7 +1697,7 @@ function ReportMeta() {
1790
1697
  const label = `#${report.ci.prNumber}`;
1791
1698
  items.push({
1792
1699
  label: "PR",
1793
- value: url ? /* @__PURE__ */ (0, import_jsx_runtime33.jsx)("a", { href: url, target: "_blank", rel: "noreferrer noopener", className: linkClass, children: label }) : label
1700
+ value: url ? /* @__PURE__ */ (0, import_jsx_runtime34.jsx)("a", { href: url, target: "_blank", rel: "noreferrer noopener", className: linkClass, children: label }) : label
1794
1701
  });
1795
1702
  }
1796
1703
  if (items.length === 0) return null;
@@ -1798,48 +1705,48 @@ function ReportMeta() {
1798
1705
  // Run metadata is useful but secondary to "what failed" — tuck it into a
1799
1706
  // collapsed disclosure so it doesn't push the result down. Native <details>
1800
1707
  // so it works with no JS (the SSR/no-hydration report still toggles).
1801
- /* @__PURE__ */ (0, import_jsx_runtime33.jsxs)("details", { "data-es-meta-card": true, className: "es-run-details group rounded-lg border border-border bg-card text-xs", children: [
1802
- /* @__PURE__ */ (0, import_jsx_runtime33.jsxs)("summary", { className: "flex cursor-pointer list-none select-none items-center gap-1.5 px-4 py-2 font-medium text-muted-foreground [&::-webkit-details-marker]:hidden", children: [
1803
- /* @__PURE__ */ (0, import_jsx_runtime33.jsx)("span", { "aria-hidden": true, className: "text-xs transition-transform group-open:rotate-90", children: "\u25B8" }),
1708
+ /* @__PURE__ */ (0, import_jsx_runtime34.jsxs)("details", { "data-es-meta-card": true, className: "es-run-details group rounded-lg border border-border bg-card text-xs", children: [
1709
+ /* @__PURE__ */ (0, import_jsx_runtime34.jsxs)("summary", { className: "flex cursor-pointer list-none select-none items-center gap-1.5 px-4 py-2 font-medium text-muted-foreground [&::-webkit-details-marker]:hidden", children: [
1710
+ /* @__PURE__ */ (0, import_jsx_runtime34.jsx)("span", { "aria-hidden": true, className: "text-xs transition-transform group-open:rotate-90", children: "\u25B8" }),
1804
1711
  "Run details"
1805
1712
  ] }),
1806
- /* @__PURE__ */ (0, import_jsx_runtime33.jsx)("dl", { className: "flex flex-wrap gap-x-5 gap-y-2 border-t border-border px-4 py-3 text-muted-foreground", "aria-label": "Run metadata", children: items.map((item) => /* @__PURE__ */ (0, import_jsx_runtime33.jsxs)("div", { className: "flex items-baseline gap-1.5", children: [
1807
- /* @__PURE__ */ (0, import_jsx_runtime33.jsx)("dt", { className: "font-medium", children: item.label }),
1808
- /* @__PURE__ */ (0, import_jsx_runtime33.jsx)("dd", { className: "font-mono text-foreground", children: item.value })
1713
+ /* @__PURE__ */ (0, import_jsx_runtime34.jsx)("dl", { className: "flex flex-wrap gap-x-5 gap-y-2 border-t border-border px-4 py-3 text-muted-foreground", "aria-label": "Run metadata", children: items.map((item) => /* @__PURE__ */ (0, import_jsx_runtime34.jsxs)("div", { className: "flex items-baseline gap-1.5", children: [
1714
+ /* @__PURE__ */ (0, import_jsx_runtime34.jsx)("dt", { className: "font-medium", children: item.label }),
1715
+ /* @__PURE__ */ (0, import_jsx_runtime34.jsx)("dd", { className: "font-mono text-foreground", children: item.value })
1809
1716
  ] }, item.label)) })
1810
1717
  ] })
1811
1718
  );
1812
1719
  }
1813
1720
 
1814
1721
  // src/components/ReportSchemaError.tsx
1815
- var import_jsx_runtime34 = require("react/jsx-runtime");
1722
+ var import_jsx_runtime35 = require("react/jsx-runtime");
1816
1723
  function ReportSchemaError({ error }) {
1817
- return /* @__PURE__ */ (0, import_jsx_runtime34.jsxs)("section", { className: "es-schema-error", role: "alert", "aria-live": "assertive", children: [
1818
- /* @__PURE__ */ (0, import_jsx_runtime34.jsx)("p", { children: /* @__PURE__ */ (0, import_jsx_runtime34.jsx)("strong", { children: "Report could not be displayed." }) }),
1819
- /* @__PURE__ */ (0, import_jsx_runtime34.jsx)("p", { children: error.message }),
1820
- error.code === "SCHEMA_VERSION_MISMATCH" ? /* @__PURE__ */ (0, import_jsx_runtime34.jsxs)("p", { children: [
1724
+ return /* @__PURE__ */ (0, import_jsx_runtime35.jsxs)("section", { className: "es-schema-error", role: "alert", "aria-live": "assertive", children: [
1725
+ /* @__PURE__ */ (0, import_jsx_runtime35.jsx)("p", { children: /* @__PURE__ */ (0, import_jsx_runtime35.jsx)("strong", { children: "Report could not be displayed." }) }),
1726
+ /* @__PURE__ */ (0, import_jsx_runtime35.jsx)("p", { children: error.message }),
1727
+ error.code === "SCHEMA_VERSION_MISMATCH" ? /* @__PURE__ */ (0, import_jsx_runtime35.jsxs)("p", { children: [
1821
1728
  "The report bundle is newer than this version of ",
1822
- /* @__PURE__ */ (0, import_jsx_runtime34.jsx)("code", { children: "executable-stories-react" }),
1729
+ /* @__PURE__ */ (0, import_jsx_runtime35.jsx)("code", { children: "executable-stories-react" }),
1823
1730
  ". Upgrade the package, or regenerate the report with an older formatters CLI."
1824
1731
  ] }) : null,
1825
- error.issues && error.issues.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime34.jsxs)("details", { children: [
1826
- /* @__PURE__ */ (0, import_jsx_runtime34.jsxs)("summary", { children: [
1732
+ error.issues && error.issues.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime35.jsxs)("details", { children: [
1733
+ /* @__PURE__ */ (0, import_jsx_runtime35.jsxs)("summary", { children: [
1827
1734
  error.issues.length,
1828
1735
  " validation issue",
1829
1736
  error.issues.length === 1 ? "" : "s"
1830
1737
  ] }),
1831
- /* @__PURE__ */ (0, import_jsx_runtime34.jsx)("pre", { children: error.issues.slice(0, 20).map((i) => `${i.path}: ${i.message}`).join("\n") })
1738
+ /* @__PURE__ */ (0, import_jsx_runtime35.jsx)("pre", { children: error.issues.slice(0, 20).map((i) => `${i.path}: ${i.message}`).join("\n") })
1832
1739
  ] }) : null
1833
1740
  ] });
1834
1741
  }
1835
1742
 
1836
1743
  // src/components/ReportShell.tsx
1837
- var import_jsx_runtime35 = require("react/jsx-runtime");
1744
+ var import_jsx_runtime36 = require("react/jsx-runtime");
1838
1745
  function ReportTitleBlock({ title }) {
1839
- return /* @__PURE__ */ (0, import_jsx_runtime35.jsxs)(import_jsx_runtime35.Fragment, { children: [
1840
- /* @__PURE__ */ (0, import_jsx_runtime35.jsx)("h1", { children: title ?? "Story Report" }),
1841
- /* @__PURE__ */ (0, import_jsx_runtime35.jsx)(ReportSummary, {}),
1842
- /* @__PURE__ */ (0, import_jsx_runtime35.jsx)(ReportMeta, {})
1746
+ return /* @__PURE__ */ (0, import_jsx_runtime36.jsxs)(import_jsx_runtime36.Fragment, { children: [
1747
+ /* @__PURE__ */ (0, import_jsx_runtime36.jsx)("h1", { children: title ?? "Story Report" }),
1748
+ /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(ReportSummary, {}),
1749
+ /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(ReportMeta, {})
1843
1750
  ] });
1844
1751
  }
1845
1752
  function ReportErrorShell({
@@ -1848,28 +1755,28 @@ function ReportErrorShell({
1848
1755
  title,
1849
1756
  dataTheme
1850
1757
  }) {
1851
- return /* @__PURE__ */ (0, import_jsx_runtime35.jsx)("main", { className: cn("es-report", className), "aria-label": title ?? "Test report", "data-theme": dataTheme, children: /* @__PURE__ */ (0, import_jsx_runtime35.jsx)(ReportSchemaError, { error }) });
1758
+ return /* @__PURE__ */ (0, import_jsx_runtime36.jsx)("main", { className: cn("es-report", className), "aria-label": title ?? "Test report", "data-theme": dataTheme, children: /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(ReportSchemaError, { error }) });
1852
1759
  }
1853
1760
 
1854
1761
  // src/components/Report.tsx
1855
- var import_jsx_runtime36 = require("react/jsx-runtime");
1762
+ var import_jsx_runtime37 = require("react/jsx-runtime");
1856
1763
  function Report(props) {
1857
1764
  const { report, customRenderers, renderers, className, title, dataTheme } = props;
1858
1765
  const result = unwrapReport(report);
1859
1766
  if (!result.ok) {
1860
- return /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(ReportErrorShell, { error: result.error, className, title, dataTheme });
1767
+ return /* @__PURE__ */ (0, import_jsx_runtime37.jsx)(ReportErrorShell, { error: result.error, className, title, dataTheme });
1861
1768
  }
1862
1769
  const data = result.data;
1863
1770
  const hasContent = data.features.length > 0;
1864
- return /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(ReportRoot, { report: data, customRenderers, renderers, children: /* @__PURE__ */ (0, import_jsx_runtime36.jsxs)("main", { className: cn("es-report", className), "aria-label": title ?? "Test report", "data-theme": dataTheme, children: [
1865
- /* @__PURE__ */ (0, import_jsx_runtime36.jsx)("header", { className: "es-report-header", children: /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(ReportTitleBlock, { title }) }),
1866
- hasContent ? /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(ReportFeatureList, {}) : /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(ReportEmpty, {})
1771
+ return /* @__PURE__ */ (0, import_jsx_runtime37.jsx)(ReportRoot, { report: data, customRenderers, renderers, children: /* @__PURE__ */ (0, import_jsx_runtime37.jsxs)("main", { className: cn("es-report", className), "aria-label": title ?? "Test report", "data-theme": dataTheme, children: [
1772
+ /* @__PURE__ */ (0, import_jsx_runtime37.jsx)("header", { className: "es-report-header", children: /* @__PURE__ */ (0, import_jsx_runtime37.jsx)(ReportTitleBlock, { title }) }),
1773
+ hasContent ? /* @__PURE__ */ (0, import_jsx_runtime37.jsx)(ReportFeatureList, {}) : /* @__PURE__ */ (0, import_jsx_runtime37.jsx)(ReportEmpty, {})
1867
1774
  ] }) });
1868
1775
  }
1869
1776
 
1870
1777
  // src/components/doc/MermaidView.tsx
1871
1778
  var import_react8 = require("react");
1872
- var import_jsx_runtime37 = require("react/jsx-runtime");
1779
+ var import_jsx_runtime38 = require("react/jsx-runtime");
1873
1780
  function prefersDark(el) {
1874
1781
  const themed = el?.closest("[data-theme]");
1875
1782
  if (themed?.dataset.theme === "dark") return true;
@@ -1923,9 +1830,9 @@ function MermaidView({ entry, load }) {
1923
1830
  };
1924
1831
  }, [entry.code, load]);
1925
1832
  if (svg) {
1926
- return /* @__PURE__ */ (0, import_jsx_runtime37.jsxs)("figure", { ref, className: "my-3", children: [
1927
- entry.title ? /* @__PURE__ */ (0, import_jsx_runtime37.jsx)("figcaption", { className: "mb-1.5 text-xs font-medium text-muted-foreground", children: entry.title }) : null,
1928
- /* @__PURE__ */ (0, import_jsx_runtime37.jsx)(
1833
+ return /* @__PURE__ */ (0, import_jsx_runtime38.jsxs)("figure", { ref, className: "my-3", children: [
1834
+ entry.title ? /* @__PURE__ */ (0, import_jsx_runtime38.jsx)("figcaption", { className: "mb-1.5 text-xs font-medium text-muted-foreground", children: entry.title }) : null,
1835
+ /* @__PURE__ */ (0, import_jsx_runtime38.jsx)(
1929
1836
  "div",
1930
1837
  {
1931
1838
  role: "img",
@@ -1936,8 +1843,8 @@ function MermaidView({ entry, load }) {
1936
1843
  )
1937
1844
  ] });
1938
1845
  }
1939
- return /* @__PURE__ */ (0, import_jsx_runtime37.jsxs)(import_jsx_runtime37.Fragment, { children: [
1940
- error ? /* @__PURE__ */ (0, import_jsx_runtime37.jsxs)(
1846
+ return /* @__PURE__ */ (0, import_jsx_runtime38.jsxs)(import_jsx_runtime38.Fragment, { children: [
1847
+ error ? /* @__PURE__ */ (0, import_jsx_runtime38.jsxs)(
1941
1848
  "p",
1942
1849
  {
1943
1850
  role: "alert",
@@ -1949,19 +1856,19 @@ function MermaidView({ entry, load }) {
1949
1856
  ]
1950
1857
  }
1951
1858
  ) : null,
1952
- /* @__PURE__ */ (0, import_jsx_runtime37.jsx)(MermaidSource, { entry, ref })
1859
+ /* @__PURE__ */ (0, import_jsx_runtime38.jsx)(MermaidSource, { entry, ref })
1953
1860
  ] });
1954
1861
  }
1955
1862
 
1956
1863
  // src/components/doc/MermaidDiagram.tsx
1957
- var import_jsx_runtime38 = require("react/jsx-runtime");
1864
+ var import_jsx_runtime39 = require("react/jsx-runtime");
1958
1865
  var mermaidLoader;
1959
1866
  function loadMermaid() {
1960
1867
  mermaidLoader ??= import("mermaid").then((m) => m.default);
1961
1868
  return mermaidLoader;
1962
1869
  }
1963
1870
  function MermaidDiagram({ entry, load = loadMermaid }) {
1964
- return /* @__PURE__ */ (0, import_jsx_runtime38.jsx)(MermaidView, { entry, load });
1871
+ return /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(MermaidView, { entry, load });
1965
1872
  }
1966
1873
 
1967
1874
  // src/components/ScenarioExplorer.tsx
@@ -1971,12 +1878,12 @@ var import_react9 = require("react");
1971
1878
  var React4 = require("react");
1972
1879
  var import_cmdk = require("cmdk");
1973
1880
  var import_lucide_react3 = require("lucide-react");
1974
- var import_jsx_runtime39 = require("react/jsx-runtime");
1881
+ var import_jsx_runtime40 = require("react/jsx-runtime");
1975
1882
  function Command({
1976
1883
  className,
1977
1884
  ...props
1978
1885
  }) {
1979
- return /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(
1886
+ return /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(
1980
1887
  import_cmdk.Command,
1981
1888
  {
1982
1889
  "data-slot": "command",
@@ -1992,14 +1899,14 @@ function CommandInput({
1992
1899
  className,
1993
1900
  ...props
1994
1901
  }) {
1995
- return /* @__PURE__ */ (0, import_jsx_runtime39.jsxs)(
1902
+ return /* @__PURE__ */ (0, import_jsx_runtime40.jsxs)(
1996
1903
  "div",
1997
1904
  {
1998
1905
  "data-slot": "command-input-wrapper",
1999
1906
  className: "flex h-9 items-center gap-2 border-b px-3",
2000
1907
  children: [
2001
- /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(import_lucide_react3.SearchIcon, { className: "size-4 shrink-0 opacity-50" }),
2002
- /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(
1908
+ /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(import_lucide_react3.SearchIcon, { className: "size-4 shrink-0 opacity-50" }),
1909
+ /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(
2003
1910
  import_cmdk.Command.Input,
2004
1911
  {
2005
1912
  "data-slot": "command-input",
@@ -2018,7 +1925,7 @@ function CommandList({
2018
1925
  className,
2019
1926
  ...props
2020
1927
  }) {
2021
- return /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(
1928
+ return /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(
2022
1929
  import_cmdk.Command.List,
2023
1930
  {
2024
1931
  "data-slot": "command-list",
@@ -2033,7 +1940,7 @@ function CommandList({
2033
1940
  function CommandEmpty({
2034
1941
  ...props
2035
1942
  }) {
2036
- return /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(
1943
+ return /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(
2037
1944
  import_cmdk.Command.Empty,
2038
1945
  {
2039
1946
  "data-slot": "command-empty",
@@ -2046,7 +1953,7 @@ function CommandGroup({
2046
1953
  className,
2047
1954
  ...props
2048
1955
  }) {
2049
- return /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(
1956
+ return /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(
2050
1957
  import_cmdk.Command.Group,
2051
1958
  {
2052
1959
  "data-slot": "command-group",
@@ -2062,7 +1969,7 @@ function CommandItem({
2062
1969
  className,
2063
1970
  ...props
2064
1971
  }) {
2065
- return /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(
1972
+ return /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(
2066
1973
  import_cmdk.Command.Item,
2067
1974
  {
2068
1975
  "data-slot": "command-item",
@@ -2076,7 +1983,7 @@ function CommandItem({
2076
1983
  }
2077
1984
 
2078
1985
  // src/components/ScenarioExplorer.tsx
2079
- var import_jsx_runtime40 = require("react/jsx-runtime");
1986
+ var import_jsx_runtime41 = require("react/jsx-runtime");
2080
1987
  var STATUS_FILTERS = [
2081
1988
  { value: "all", label: "All" },
2082
1989
  { value: "passed", label: "Passed" },
@@ -2087,22 +1994,22 @@ var STATUS_FILTERS = [
2087
1994
  function ScenarioExplorer({ scenarios, summary }) {
2088
1995
  const [status, setStatus] = (0, import_react9.useState)("all");
2089
1996
  const visible = status === "all" ? scenarios : scenarios.filter((s) => s.status === status);
2090
- return /* @__PURE__ */ (0, import_jsx_runtime40.jsxs)("div", { className: "font-sans text-foreground", children: [
2091
- summary ? /* @__PURE__ */ (0, import_jsx_runtime40.jsxs)("p", { className: "mb-3 text-sm text-muted-foreground", children: [
1997
+ return /* @__PURE__ */ (0, import_jsx_runtime41.jsxs)("div", { className: "font-sans text-foreground", children: [
1998
+ summary ? /* @__PURE__ */ (0, import_jsx_runtime41.jsxs)("p", { className: "mb-3 text-sm text-muted-foreground", children: [
2092
1999
  summary.total,
2093
2000
  " scenarios \xB7 ",
2094
- /* @__PURE__ */ (0, import_jsx_runtime40.jsxs)("span", { className: "text-pass", children: [
2001
+ /* @__PURE__ */ (0, import_jsx_runtime41.jsxs)("span", { className: "text-pass", children: [
2095
2002
  summary.passed,
2096
2003
  " passed"
2097
2004
  ] }),
2098
2005
  " \xB7",
2099
2006
  " ",
2100
- /* @__PURE__ */ (0, import_jsx_runtime40.jsxs)("span", { className: "text-fail", children: [
2007
+ /* @__PURE__ */ (0, import_jsx_runtime41.jsxs)("span", { className: "text-fail", children: [
2101
2008
  summary.failed,
2102
2009
  " failed"
2103
2010
  ] })
2104
2011
  ] }) : null,
2105
- /* @__PURE__ */ (0, import_jsx_runtime40.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_runtime40.jsx)(
2012
+ /* @__PURE__ */ (0, import_jsx_runtime41.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_runtime41.jsx)(
2106
2013
  "button",
2107
2014
  {
2108
2015
  type: "button",
@@ -2117,26 +2024,26 @@ function ScenarioExplorer({ scenarios, summary }) {
2117
2024
  },
2118
2025
  f.value
2119
2026
  )) }),
2120
- /* @__PURE__ */ (0, import_jsx_runtime40.jsxs)(
2027
+ /* @__PURE__ */ (0, import_jsx_runtime41.jsxs)(
2121
2028
  Command,
2122
2029
  {
2123
2030
  className: "rounded-lg border border-border bg-card",
2124
2031
  filter: (value, search) => value.toLowerCase().includes(search.toLowerCase()) ? 1 : 0,
2125
2032
  children: [
2126
- /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(CommandInput, { placeholder: "Search scenarios\u2026" }),
2127
- /* @__PURE__ */ (0, import_jsx_runtime40.jsxs)(CommandList, { className: "max-h-none", children: [
2128
- /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(CommandEmpty, { children: "No scenarios match your filters." }),
2129
- /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(CommandGroup, { children: visible.map((s) => /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(
2033
+ /* @__PURE__ */ (0, import_jsx_runtime41.jsx)(CommandInput, { placeholder: "Search scenarios\u2026" }),
2034
+ /* @__PURE__ */ (0, import_jsx_runtime41.jsxs)(CommandList, { className: "max-h-none", children: [
2035
+ /* @__PURE__ */ (0, import_jsx_runtime41.jsx)(CommandEmpty, { children: "No scenarios match your filters." }),
2036
+ /* @__PURE__ */ (0, import_jsx_runtime41.jsx)(CommandGroup, { children: visible.map((s) => /* @__PURE__ */ (0, import_jsx_runtime41.jsx)(
2130
2037
  CommandItem,
2131
2038
  {
2132
2039
  value: `${s.title} ${s.feature} ${s.tags.join(" ")}`,
2133
2040
  asChild: true,
2134
2041
  className: "cursor-pointer",
2135
- children: /* @__PURE__ */ (0, import_jsx_runtime40.jsxs)("a", { href: s.href, className: "flex items-center gap-3", children: [
2136
- /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(Badge, { variant: s.status, children: s.status }),
2137
- /* @__PURE__ */ (0, import_jsx_runtime40.jsx)("span", { className: "min-w-0 flex-1 truncate font-medium", children: s.title }),
2138
- s.tags.slice(0, 3).map((t) => /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(Badge, { variant: "tag", className: "hidden sm:inline-flex", children: t }, t)),
2139
- /* @__PURE__ */ (0, import_jsx_runtime40.jsx)("code", { className: "hidden font-mono text-xs text-muted-foreground md:inline", children: s.feature })
2042
+ children: /* @__PURE__ */ (0, import_jsx_runtime41.jsxs)("a", { href: s.href, className: "flex items-center gap-3", children: [
2043
+ /* @__PURE__ */ (0, import_jsx_runtime41.jsx)(Badge, { variant: s.status, children: s.status }),
2044
+ /* @__PURE__ */ (0, import_jsx_runtime41.jsx)("span", { className: "min-w-0 flex-1 truncate font-medium", children: s.title }),
2045
+ s.tags.slice(0, 3).map((t) => /* @__PURE__ */ (0, import_jsx_runtime41.jsx)(Badge, { variant: "tag", className: "hidden sm:inline-flex", children: t }, t)),
2046
+ /* @__PURE__ */ (0, import_jsx_runtime41.jsx)("code", { className: "hidden font-mono text-xs text-muted-foreground md:inline", children: s.feature })
2140
2047
  ] })
2141
2048
  },
2142
2049
  s.id
@@ -2283,7 +2190,7 @@ var story_report_v1_default = {
2283
2190
  title: {
2284
2191
  type: "string",
2285
2192
  minLength: 1,
2286
- description: "Display title. Derived from describe block when present, otherwise the file basename."
2193
+ description: "Display title. Comes from story.feature(...) when declared, otherwise the describe block or file basename."
2287
2194
  },
2288
2195
  sourceFile: {
2289
2196
  type: "string",
@@ -2299,6 +2206,26 @@ var story_report_v1_default = {
2299
2206
  $ref: "#/$defs/Scenario"
2300
2207
  },
2301
2208
  description: "Scenarios in this feature, in declaration order."
2209
+ },
2210
+ kind: {
2211
+ type: "string",
2212
+ enum: [
2213
+ "feature",
2214
+ "ability",
2215
+ "business-need"
2216
+ ],
2217
+ description: "How the feature was declared. Absent when the title was derived rather than declared."
2218
+ },
2219
+ narrative: {
2220
+ type: "string",
2221
+ description: "Markdown explaining why the feature exists and who it serves."
2222
+ },
2223
+ glossary: {
2224
+ type: "array",
2225
+ items: {
2226
+ $ref: "#/$defs/GlossaryTerm"
2227
+ },
2228
+ description: "Terms this feature defines, linked wherever they appear in scenario text."
2302
2229
  }
2303
2230
  },
2304
2231
  required: [
@@ -3110,6 +3037,25 @@ var story_report_v1_default = {
3110
3037
  }
3111
3038
  },
3112
3039
  additionalProperties: false
3040
+ },
3041
+ GlossaryTerm: {
3042
+ type: "object",
3043
+ description: "One glossary entry: a term the scenarios use and what it means.",
3044
+ properties: {
3045
+ term: {
3046
+ type: "string",
3047
+ minLength: 1
3048
+ },
3049
+ definition: {
3050
+ type: "string",
3051
+ minLength: 1
3052
+ }
3053
+ },
3054
+ required: [
3055
+ "term",
3056
+ "definition"
3057
+ ],
3058
+ additionalProperties: false
3113
3059
  }
3114
3060
  }
3115
3061
  };