@validation-os/dashboard 0.15.4 → 0.15.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -9,14 +9,14 @@ var REGISTER_LABEL = {
9
9
  // One container-agnostic lens now Goal is unified into Experiment (OPS-1299 /
10
10
  // OPS-1287 story 22): all plans browse in one place under this label.
11
11
  experiments: "Evidence plans",
12
- readings: "Readings",
12
+ readings: "Evidence",
13
13
  decisions: "Decisions",
14
14
  glossary: "Glossary"
15
15
  };
16
16
  var REGISTER_SINGULAR = {
17
17
  assumptions: "Assumption",
18
18
  experiments: "Evidence plan",
19
- readings: "Reading",
19
+ readings: "Evidence",
20
20
  decisions: "Decision",
21
21
  glossary: "Glossary term"
22
22
  };
@@ -37,7 +37,7 @@ var REGISTER_SUBTITLE = {
37
37
  var REGISTER_ICON = {
38
38
  assumptions: "\u25CE",
39
39
  experiments: "\u2697",
40
- readings: "\u25A4",
40
+ readings: "\u2726",
41
41
  decisions: "\xA7",
42
42
  glossary: "A"
43
43
  };
@@ -50,7 +50,7 @@ var REGISTER_GROUPS = [
50
50
  var WORKFLOW_NAV = [
51
51
  { route: "assumptions", label: "Assumptions", icon: "\u25CE", isDefault: true },
52
52
  { route: "experiments", label: "Experiments", icon: "\u2697" },
53
- { route: "readings", label: "Readings", icon: "\u25A4" }
53
+ { route: "readings", label: "Evidence", icon: "\u2726" }
54
54
  ];
55
55
 
56
56
  // src/assumption-detail.tsx
@@ -218,6 +218,47 @@ function buildEvidenceComposition(assumption, readings) {
218
218
  totalContribution: Math.round((total + Number.EPSILON) * 100) / 100
219
219
  };
220
220
  }
221
+ function readingContributions(assumption, readings) {
222
+ const id = str(assumption.id) ?? "";
223
+ const inputs = [];
224
+ for (const r of readings) {
225
+ const belief2 = readingBeliefFor(r, id);
226
+ if (!belief2) continue;
227
+ const result = str(belief2.Result) ?? "Inconclusive";
228
+ if (result === "Inconclusive") continue;
229
+ const rung = str(r.Rung) ?? "Talk";
230
+ inputs.push({
231
+ id: str(r.id) ?? "",
232
+ source: str(r.Source) ?? null,
233
+ rung,
234
+ result,
235
+ representativeness: Number(r.Representativeness) || 1,
236
+ credibility: Number(r.Credibility) || 1,
237
+ date: str(r.Date),
238
+ magnitudeBand: r.magnitudeBand,
239
+ experimentId: str(r.experimentId)
240
+ });
241
+ }
242
+ const winners = scoreAndDedupe(inputs);
243
+ const winnerIds = new Set(winners.map((w) => w.input.id));
244
+ const rungsPresent = new Set(winners.map((x) => x.input.rung));
245
+ let den = 0;
246
+ for (const rung of rungsPresent) den += w0ForRung(rung);
247
+ for (const x of winners) den += x.weight;
248
+ return inputs.map((input) => {
249
+ const w = winners.find((x) => x.input.id === input.id);
250
+ const used = winnerIds.has(input.id);
251
+ const strength = w ? w.strength : (input.result === "Validated" ? 1 : input.result === "Invalidated" ? -1 : 0) * (RUNG_CAPS[input.rung] ?? 0);
252
+ const weight = w ? w.weight : 0;
253
+ return {
254
+ id: input.id,
255
+ contribution: w && den > 0 ? Math.round(w.weight * w.strength / den * 100) / 100 : 0,
256
+ weight,
257
+ strength,
258
+ used
259
+ };
260
+ });
261
+ }
221
262
 
222
263
  // src/confidence-explainer.ts
223
264
  import { RUNG_ANCHOR, W0_BY_RUNG } from "@validation-os/core/derivation";
@@ -1004,10 +1045,26 @@ function readingBeliefVerdicts(reading, assumptions = []) {
1004
1045
  linked: hit != null,
1005
1046
  result: b.Result ?? null,
1006
1047
  strength,
1007
- justification: typeof b["Grading justification"] === "string" ? b["Grading justification"] : ""
1048
+ justification: typeof b["Grading justification"] === "string" ? b["Grading justification"] : "",
1049
+ excerpt: typeof b.excerpt === "string" && b.excerpt !== "" ? b.excerpt : snippetFromBody(String(reading.body ?? ""), b.assumptionId)
1008
1050
  };
1009
1051
  });
1010
1052
  }
1053
+ function snippetFromBody(body, cue) {
1054
+ if (!body) return "";
1055
+ const quoteMatch = body.match(/## Quote\n+([\s\S]*?)(?=\n## |\n##$|$)/i);
1056
+ if (quoteMatch) {
1057
+ const q = quoteMatch[1].trim();
1058
+ return q.length > 220 ? q.slice(0, 217).trim() + "\u2026" : q;
1059
+ }
1060
+ const sentences = body.split(/(?<=[.!?])\s+/);
1061
+ const cueLower = cue.toLowerCase();
1062
+ for (const s of sentences) {
1063
+ if (s.toLowerCase().includes(cueLower)) return s.trim();
1064
+ }
1065
+ const first = sentences[0]?.trim() ?? "";
1066
+ return first.length > 220 ? first.slice(0, 217).trim() + "\u2026" : first;
1067
+ }
1011
1068
  function readingBeliefSummary(reading, assumptions = []) {
1012
1069
  const verdicts = readingBeliefVerdicts(reading, assumptions);
1013
1070
  let validated = 0;
@@ -1554,64 +1611,118 @@ function AssumptionDetail({
1554
1611
  );
1555
1612
  })
1556
1613
  ] }) : null,
1557
- linkedReadings.length > 0 ? /* @__PURE__ */ jsxs4("div", { className: "vos-card vos-detail-section", children: [
1558
- /* @__PURE__ */ jsxs4("div", { className: "vos-detail-section-label", children: [
1559
- "Evidence \xB7 ",
1560
- linkedReadings.length,
1561
- " reading",
1562
- linkedReadings.length === 1 ? "" : "s"
1563
- ] }),
1564
- linkedReadings.map((r) => {
1565
- const id = String(r.id ?? "");
1566
- const belief2 = readingBeliefFor(r, assumptionId);
1567
- if (!belief2) return null;
1568
- const result = String(belief2.Result ?? "Inconclusive");
1569
- const justification = String(belief2["Grading justification"] ?? "");
1570
- const rung = String(r.Rung ?? "");
1571
- const source = String(r.Source ?? "");
1572
- const expId = r.experimentId ? String(r.experimentId) : null;
1573
- return /* @__PURE__ */ jsxs4(
1574
- "button",
1575
- {
1576
- type: "button",
1577
- className: `vos-linked-row vos-linked-reading vos-verdict-${verdictTone(result)}`,
1578
- onClick: () => onNavigate({ name: "reading", id }),
1579
- children: [
1580
- /* @__PURE__ */ jsxs4("div", { className: "vos-reading-head", children: [
1581
- /* @__PURE__ */ jsx4("span", { className: "vos-reading-date vos-num", children: String(r.Date ?? "") }),
1582
- /* @__PURE__ */ jsx4("span", { className: "vos-reading-title", children: String(r.Title ?? id) }),
1583
- /* @__PURE__ */ jsx4("span", { className: `vos-pill vos-pill-${verdictTone(result)}`, children: result }),
1584
- /* @__PURE__ */ jsx4("span", { className: "vos-rung-tag", children: rung })
1585
- ] }),
1586
- justification ? /* @__PURE__ */ jsxs4("div", { className: `vos-belief-rationale vos-verdict-border-${verdictTone(result)}`, children: [
1587
- /* @__PURE__ */ jsx4("span", { className: "vos-belief-rationale-label", children: "grading rationale:" }),
1588
- justification
1589
- ] }) : null,
1590
- /* @__PURE__ */ jsxs4("div", { className: "vos-reading-source", children: [
1591
- source,
1592
- expId ? /* @__PURE__ */ jsxs4(Fragment3, { children: [
1593
- " \xB7 from ",
1594
- /* @__PURE__ */ jsx4(
1595
- "span",
1596
- {
1597
- className: "vos-link",
1598
- onClick: (ev) => {
1599
- ev.stopPropagation();
1600
- onNavigate({ name: "experiment", id: expId });
1601
- },
1602
- children: expId
1603
- }
1604
- )
1605
- ] }) : null
1606
- ] })
1607
- ]
1608
- },
1609
- id
1610
- );
1611
- })
1612
- ] }) : null
1614
+ /* @__PURE__ */ jsx4(
1615
+ EvidenceList,
1616
+ {
1617
+ assumptionId,
1618
+ readings: readings.records ?? [],
1619
+ onNavigate
1620
+ }
1621
+ )
1622
+ ] });
1623
+ }
1624
+ function EvidenceList({
1625
+ assumptionId,
1626
+ readings,
1627
+ onNavigate
1628
+ }) {
1629
+ const linkedReadings = readings.filter((r) => readingBeliefs(r).some((b) => b.assumptionId === assumptionId)).sort((a, b) => String(b.Date ?? "").localeCompare(String(a.Date ?? "")));
1630
+ const contribById = useMemo(() => {
1631
+ const rec = readings.find((a) => String(a.id) === assumptionId);
1632
+ if (!rec) return /* @__PURE__ */ new Map();
1633
+ const rows = readingContributions(rec, readings);
1634
+ return new Map(rows.map((r) => [r.id, r]));
1635
+ }, [readings, assumptionId]);
1636
+ if (linkedReadings.length === 0) return null;
1637
+ return /* @__PURE__ */ jsxs4("div", { className: "vos-card vos-detail-section", children: [
1638
+ /* @__PURE__ */ jsxs4("div", { className: "vos-detail-section-label", children: [
1639
+ "Evidence \xB7 ",
1640
+ linkedReadings.length,
1641
+ " piece",
1642
+ linkedReadings.length === 1 ? "" : "s",
1643
+ " of evidence"
1644
+ ] }),
1645
+ linkedReadings.map((r) => {
1646
+ const id = String(r.id ?? "");
1647
+ const belief2 = readingBeliefFor(r, assumptionId);
1648
+ if (!belief2) return null;
1649
+ const result = String(belief2.Result ?? "Inconclusive");
1650
+ const justification = String(belief2["Grading justification"] ?? "");
1651
+ const excerpt = typeof belief2.excerpt === "string" && belief2.excerpt !== "" ? belief2.excerpt : snippetFromBody2(String(r.body ?? ""), assumptionId);
1652
+ const rung = String(r.Rung ?? "");
1653
+ const source = String(r.Source ?? "");
1654
+ const expId = r.experimentId ? String(r.experimentId) : null;
1655
+ const c = contribById.get(id);
1656
+ return /* @__PURE__ */ jsxs4(
1657
+ "button",
1658
+ {
1659
+ type: "button",
1660
+ className: `vos-evidence-row vos-verdict-${verdictTone(result)}`,
1661
+ onClick: () => onNavigate({ name: "reading", id }),
1662
+ children: [
1663
+ /* @__PURE__ */ jsxs4("div", { className: "vos-evidence-row-head", children: [
1664
+ /* @__PURE__ */ jsx4("span", { className: "vos-evidence-date vos-num", children: String(r.Date ?? "") }),
1665
+ /* @__PURE__ */ jsx4("span", { className: "vos-evidence-title", children: String(r.Title ?? id) }),
1666
+ /* @__PURE__ */ jsx4("span", { className: `vos-pill vos-pill-${verdictTone(result)}`, children: result }),
1667
+ /* @__PURE__ */ jsx4("span", { className: "vos-rung-tag", children: rung }),
1668
+ c && c.used ? /* @__PURE__ */ jsxs4("span", { className: `vos-evidence-score vos-num vos-text-${contributionTone(c.contribution)}`, children: [
1669
+ formatSigned(c.contribution),
1670
+ " confidence"
1671
+ ] }) : null
1672
+ ] }),
1673
+ excerpt ? /* @__PURE__ */ jsxs4("div", { className: "vos-evidence-excerpt", children: [
1674
+ "\u201C",
1675
+ excerpt,
1676
+ "\u201D"
1677
+ ] }) : null,
1678
+ justification ? /* @__PURE__ */ jsxs4("div", { className: `vos-belief-rationale vos-verdict-border-${verdictTone(result)}`, children: [
1679
+ /* @__PURE__ */ jsx4("span", { className: "vos-belief-rationale-label", children: "grading rationale:" }),
1680
+ justification
1681
+ ] }) : null,
1682
+ /* @__PURE__ */ jsxs4("div", { className: "vos-evidence-source", children: [
1683
+ /* @__PURE__ */ jsx4("span", { className: "vos-evidence-source-link", title: source, children: source }),
1684
+ expId ? /* @__PURE__ */ jsxs4(Fragment3, { children: [
1685
+ " \xB7 from experiment ",
1686
+ /* @__PURE__ */ jsx4(
1687
+ "span",
1688
+ {
1689
+ className: "vos-link",
1690
+ onClick: (ev) => {
1691
+ ev.stopPropagation();
1692
+ onNavigate({ name: "experiment", id: expId });
1693
+ },
1694
+ children: expId
1695
+ }
1696
+ )
1697
+ ] }) : null
1698
+ ] })
1699
+ ]
1700
+ },
1701
+ id
1702
+ );
1703
+ })
1613
1704
  ] });
1614
1705
  }
1706
+ function contributionTone(v) {
1707
+ if (v > 0) return "good";
1708
+ if (v < 0) return "crit";
1709
+ return "neutral";
1710
+ }
1711
+ function snippetFromBody2(body, cue) {
1712
+ if (!body) return "";
1713
+ const quoteMatch = body.match(/## Quote\n+([\s\S]*?)(?=\n## |\n##$|$)/i);
1714
+ if (quoteMatch) {
1715
+ const q = quoteMatch[1].trim();
1716
+ return q.length > 220 ? q.slice(0, 217).trim() + "\u2026" : q;
1717
+ }
1718
+ const sentences = body.split(/(?<=[.!?])\s+/);
1719
+ const cueLower = cue.toLowerCase();
1720
+ for (const s of sentences) {
1721
+ if (s.toLowerCase().includes(cueLower)) return s.trim();
1722
+ }
1723
+ const first = sentences[0]?.trim() ?? "";
1724
+ return first.length > 220 ? first.slice(0, 217).trim() + "\u2026" : first;
1725
+ }
1615
1726
  function ScoreCard({
1616
1727
  label,
1617
1728
  value,
@@ -1781,16 +1892,24 @@ function EvidenceCompositionView({
1781
1892
  " (adds to Confidence)"
1782
1893
  ] }),
1783
1894
  comp.rungs.length === 0 ? /* @__PURE__ */ jsx4("div", { className: "vos-muted", children: "No lens set \u2014 evidence composition needs a lens." }) : comp.rungs.map((r) => {
1784
- const pct2 = r.cap > 0 ? Math.min(100, Math.abs(r.contribution) / r.cap * 100) : 0;
1895
+ const abs = Math.abs(r.contribution);
1896
+ const pct2 = r.cap > 0 ? Math.min(100, abs / r.cap * 100) : 0;
1785
1897
  const isEmpty = r.count === 0;
1898
+ const tone = r.contribution > 0 ? "good" : r.contribution < 0 ? "crit" : "accent";
1786
1899
  return /* @__PURE__ */ jsxs4("div", { className: "vos-comp-row", children: [
1787
1900
  /* @__PURE__ */ jsx4("span", { className: `vos-comp-rung ${isEmpty ? "is-empty" : ""}`, children: r.rung }),
1788
- /* @__PURE__ */ jsx4("div", { className: "vos-comp-bar", children: /* @__PURE__ */ jsx4("i", { className: "vos-comp-fill", style: { width: `${pct2}%`, opacity: isEmpty ? 0.15 : 1 } }) }),
1789
- /* @__PURE__ */ jsx4("span", { className: "vos-comp-val vos-num", children: isEmpty ? "\u2014" : `${formatSigned(r.contribution)}/${r.cap}` }),
1901
+ /* @__PURE__ */ jsx4("div", { className: "vos-comp-bar", children: /* @__PURE__ */ jsx4("i", { className: `vos-comp-fill vos-fill-${tone}`, style: { width: `${isEmpty ? 0 : Math.max(4, pct2)}%` } }) }),
1902
+ /* @__PURE__ */ jsx4("span", { className: "vos-comp-val vos-num", children: isEmpty ? "\u2014" : /* @__PURE__ */ jsxs4(Fragment3, { children: [
1903
+ /* @__PURE__ */ jsx4("span", { className: `vos-text-${tone}`, children: formatSigned(r.contribution) }),
1904
+ /* @__PURE__ */ jsxs4("span", { style: { color: "var(--vos-faint)" }, children: [
1905
+ " \xB7 cap ",
1906
+ r.cap
1907
+ ] })
1908
+ ] }) }),
1790
1909
  /* @__PURE__ */ jsxs4("span", { className: "vos-comp-count vos-num", children: [
1791
1910
  r.count,
1792
- " src",
1793
- r.count === 1 ? "" : "s"
1911
+ " ",
1912
+ r.count === 1 ? "source" : "sources"
1794
1913
  ] })
1795
1914
  ] }, r.rung);
1796
1915
  })
@@ -1798,7 +1917,7 @@ function EvidenceCompositionView({
1798
1917
  }
1799
1918
 
1800
1919
  // src/assumptions-surface.tsx
1801
- import { useMemo as useMemo2 } from "react";
1920
+ import { useMemo as useMemo2, useState as useState3 } from "react";
1802
1921
 
1803
1922
  // src/next-move.ts
1804
1923
  import {
@@ -2099,9 +2218,14 @@ function weekOverWeekDelta(assumptions, readings, now) {
2099
2218
  }
2100
2219
 
2101
2220
  // src/recommended-experiments.ts
2102
- var MAX_RECOMMENDED = 2;
2221
+ var MAX_RECOMMENDED = 3;
2103
2222
  var MAX_CLUSTER_SIZE = 3;
2104
2223
  var MAX_NEEDS_FRAMING = 3;
2224
+ var LENS_COLOUR = {
2225
+ Consumer: "#8b83f5",
2226
+ Commercial: "#38c793",
2227
+ Investor: "#e6a23c"
2228
+ };
2105
2229
  function buildNeedsFraming(assumptions) {
2106
2230
  const live = assumptions.filter((a) => {
2107
2231
  const status = str(a.Status);
@@ -2116,7 +2240,8 @@ function buildNeedsFraming(assumptions) {
2116
2240
  const stage = str(a.Stage) ?? "\u2014";
2117
2241
  const title = str(a.Title) ?? id;
2118
2242
  const hint = framingHint(a, completeness);
2119
- return { id, title, risk, completeness, lens, stage, hint };
2243
+ const lensColour = LENS_COLOUR[lens] ?? LENS_COLOUR["Investor"] ?? "#6b7484";
2244
+ return { id, title, risk, completeness, lens, stage, hint, lensColour };
2120
2245
  }).filter((a) => a.completeness < 100);
2121
2246
  const byLens = /* @__PURE__ */ new Map();
2122
2247
  for (const item of items) {
@@ -2166,22 +2291,28 @@ function buildRecommendedExperiments(assumptions, experiments) {
2166
2291
  if (bucket) bucket.push(a);
2167
2292
  else clusters.set(key, [a]);
2168
2293
  }
2169
- const clusterEntries = [...clusters.entries()].map(([key, cluster]) => {
2294
+ const rankedClusters = [...clusters.entries()].map(([key, cluster]) => {
2170
2295
  const ranked = cluster.sort((a, b) => (derivedNum(b, "risk") ?? 0) - (derivedNum(a, "risk") ?? 0)).slice(0, MAX_CLUSTER_SIZE);
2171
2296
  const maxRisk = Math.max(...ranked.map((a) => derivedNum(a, "risk") ?? 0), 0);
2172
2297
  return { key, cluster: ranked, maxRisk };
2173
- }).sort((a, b) => b.maxRisk - a.maxRisk).slice(0, MAX_RECOMMENDED);
2298
+ }).sort((a, b) => b.maxRisk - a.maxRisk);
2299
+ const byLens = /* @__PURE__ */ new Map();
2300
+ for (const entry of rankedClusters) {
2301
+ const lens = entry.key.split("\xD7")[0] ?? "\u2014";
2302
+ if (!byLens.has(lens)) byLens.set(lens, entry);
2303
+ }
2304
+ const clusterEntries = [...byLens.values()].sort((a, b) => b.maxRisk - a.maxRisk).slice(0, MAX_RECOMMENDED);
2174
2305
  const recs = [];
2175
2306
  for (const { key, cluster, maxRisk } of clusterEntries) {
2176
2307
  const lens = key.split("\xD7")[0] ?? "\u2014";
2177
2308
  const stage = key.split("\xD7")[1] ?? "\u2014";
2178
2309
  const assumptionIds = cluster.map((a) => str(a.id) ?? "").filter(Boolean).sort();
2179
2310
  const type = LENS_TO_TYPE[lens ?? ""] ?? "Desk research";
2180
- const titles = cluster.map((a) => str(a.Title) ?? str(a.id) ?? "");
2181
2311
  const title = cluster.length === 1 ? `Test ${assumptionIds[0]}` : `Test ${cluster.length} ${lens} \xB7 ${stage} beliefs`;
2182
2312
  const rationale = cluster.length === 1 ? `One belief (${assumptionIds[0]}) at ${Math.round(maxRisk)} risk with no live test. Designing an experiment here would buy down the most risk.` : `${cluster.length} beliefs share ${lens} \xB7 ${stage}, the riskiest at ${Math.round(maxRisk)} risk. One experiment can address them all.`;
2183
2313
  const barPreview = `The riskiest belief moves out of the kill zone, or stays in it.`;
2184
2314
  const body = generateExperimentBody(type, lens, stage, cluster, maxRisk);
2315
+ const lensColour = LENS_COLOUR[lens] ?? LENS_COLOUR["Investor"] ?? "#6b7484";
2185
2316
  recs.push({
2186
2317
  id: assumptionIds.join("+"),
2187
2318
  type,
@@ -2190,7 +2321,9 @@ function buildRecommendedExperiments(assumptions, experiments) {
2190
2321
  maxRisk,
2191
2322
  rationale,
2192
2323
  barPreview,
2193
- body
2324
+ body,
2325
+ lens,
2326
+ lensColour
2194
2327
  });
2195
2328
  }
2196
2329
  recs.sort(
@@ -2675,14 +2808,12 @@ function NextMovesSection({
2675
2808
  recs,
2676
2809
  onOpenAssumption
2677
2810
  }) {
2811
+ const [openRec, setOpenRec] = useState3(null);
2678
2812
  return /* @__PURE__ */ jsxs5("div", { className: "vos-next-moves", children: [
2679
2813
  /* @__PURE__ */ jsx5("div", { className: "vos-next-moves-head", children: "Next moves" }),
2680
2814
  /* @__PURE__ */ jsxs5("div", { className: "vos-next-moves-cols", children: [
2681
2815
  /* @__PURE__ */ jsxs5("div", { className: "vos-card vos-next-moves-col", children: [
2682
- /* @__PURE__ */ jsxs5("div", { className: "vos-next-moves-col-label", children: [
2683
- "Needs framing \xB7 top ",
2684
- needsFraming.length
2685
- ] }),
2816
+ /* @__PURE__ */ jsx5("div", { className: "vos-next-moves-col-label", children: "Needs framing \xB7 one per lens" }),
2686
2817
  needsFraming.length === 0 ? /* @__PURE__ */ jsx5("div", { className: "vos-muted vos-next-moves-empty", children: "Every belief is fully framed." }) : needsFraming.map((a) => /* @__PURE__ */ jsxs5(
2687
2818
  "button",
2688
2819
  {
@@ -2691,13 +2822,24 @@ function NextMovesSection({
2691
2822
  onClick: () => onOpenAssumption(a.id),
2692
2823
  children: [
2693
2824
  /* @__PURE__ */ jsxs5("div", { className: "vos-next-moves-item-head", children: [
2694
- /* @__PURE__ */ jsx5("span", { className: "vos-next-moves-item-id vos-num", children: a.id }),
2825
+ /* @__PURE__ */ jsx5(
2826
+ "span",
2827
+ {
2828
+ className: "vos-next-moves-lens",
2829
+ style: { background: `${a.lensColour}20`, color: a.lensColour, borderColor: `${a.lensColour}40` },
2830
+ children: a.lens
2831
+ }
2832
+ ),
2695
2833
  /* @__PURE__ */ jsxs5("span", { className: `vos-next-moves-item-risk vos-num vos-text-${riskToneClass(a.risk)}`, children: [
2696
2834
  Math.round(a.risk),
2697
2835
  " risk"
2698
2836
  ] })
2699
2837
  ] }),
2700
- /* @__PURE__ */ jsx5("div", { className: "vos-next-moves-item-title", children: a.title }),
2838
+ /* @__PURE__ */ jsxs5("div", { className: "vos-next-moves-item-title", children: [
2839
+ /* @__PURE__ */ jsx5("span", { className: "vos-next-moves-item-id vos-num", children: a.id }),
2840
+ " \xB7 ",
2841
+ a.title
2842
+ ] }),
2701
2843
  /* @__PURE__ */ jsx5("div", { className: "vos-next-moves-item-hint", children: a.hint })
2702
2844
  ]
2703
2845
  },
@@ -2705,42 +2847,119 @@ function NextMovesSection({
2705
2847
  ))
2706
2848
  ] }),
2707
2849
  /* @__PURE__ */ jsxs5("div", { className: "vos-card vos-next-moves-col", children: [
2708
- /* @__PURE__ */ jsxs5("div", { className: "vos-next-moves-col-label", children: [
2709
- "Proposed experiments \xB7 top ",
2710
- recs.length
2711
- ] }),
2712
- recs.length === 0 ? /* @__PURE__ */ jsx5("div", { className: "vos-muted vos-next-moves-empty", children: "Every risk has a live test." }) : recs.map((rec) => /* @__PURE__ */ jsxs5("details", { className: "vos-pipe-rec vos-next-moves-rec", children: [
2713
- /* @__PURE__ */ jsxs5("summary", { className: "vos-pipe-rec-head", children: [
2714
- /* @__PURE__ */ jsx5("span", { className: "vos-pipe-rec-type", children: rec.type }),
2715
- /* @__PURE__ */ jsx5("span", { className: "vos-pipe-rec-title", children: rec.title }),
2716
- /* @__PURE__ */ jsxs5("span", { className: "vos-pipe-rec-risk vos-num", children: [
2717
- Math.round(rec.maxRisk),
2718
- " risk"
2850
+ /* @__PURE__ */ jsx5("div", { className: "vos-next-moves-col-label", children: "Proposed experiments \xB7 one per lens" }),
2851
+ recs.length === 0 ? /* @__PURE__ */ jsx5("div", { className: "vos-muted vos-next-moves-empty", children: "Every risk has a live test." }) : recs.map((rec) => /* @__PURE__ */ jsxs5(
2852
+ "button",
2853
+ {
2854
+ type: "button",
2855
+ className: "vos-next-moves-rec vos-next-moves-rec-compact",
2856
+ onClick: () => setOpenRec(rec),
2857
+ children: [
2858
+ /* @__PURE__ */ jsxs5("div", { className: "vos-next-moves-rec-head", children: [
2859
+ /* @__PURE__ */ jsx5(
2860
+ "span",
2861
+ {
2862
+ className: "vos-next-moves-lens",
2863
+ style: { background: `${rec.lensColour}20`, color: rec.lensColour, borderColor: `${rec.lensColour}40` },
2864
+ children: rec.lens
2865
+ }
2866
+ ),
2867
+ /* @__PURE__ */ jsx5("span", { className: "vos-next-moves-rec-type", children: rec.type }),
2868
+ /* @__PURE__ */ jsxs5("span", { className: "vos-next-moves-rec-risk vos-num", children: [
2869
+ Math.round(rec.maxRisk),
2870
+ " risk"
2871
+ ] })
2872
+ ] }),
2873
+ /* @__PURE__ */ jsx5("div", { className: "vos-next-moves-rec-title", children: rec.title }),
2874
+ /* @__PURE__ */ jsx5("span", { className: "vos-next-moves-rec-open", "aria-hidden": "true", children: "\u2192" })
2875
+ ]
2876
+ },
2877
+ rec.id
2878
+ ))
2879
+ ] })
2880
+ ] }),
2881
+ openRec ? /* @__PURE__ */ jsx5(
2882
+ RecommendedExperimentDrawer,
2883
+ {
2884
+ rec: openRec,
2885
+ onOpenAssumption,
2886
+ onClose: () => setOpenRec(null)
2887
+ }
2888
+ ) : null
2889
+ ] });
2890
+ }
2891
+ function RecommendedExperimentDrawer({
2892
+ rec,
2893
+ onOpenAssumption,
2894
+ onClose
2895
+ }) {
2896
+ return /* @__PURE__ */ jsxs5(Fragment4, { children: [
2897
+ /* @__PURE__ */ jsx5("div", { className: "vos-scrim", onClick: onClose, "aria-hidden": "true" }),
2898
+ /* @__PURE__ */ jsxs5(
2899
+ "aside",
2900
+ {
2901
+ className: "vos-drawer vos-rec-drawer",
2902
+ role: "dialog",
2903
+ "aria-modal": "true",
2904
+ "aria-label": `Proposed experiment: ${rec.title}`,
2905
+ children: [
2906
+ /* @__PURE__ */ jsxs5("div", { className: "vos-drawer-header", children: [
2907
+ /* @__PURE__ */ jsxs5("div", { children: [
2908
+ /* @__PURE__ */ jsx5("div", { className: "vos-drawer-eyebrow", children: "Proposed experiment" }),
2909
+ /* @__PURE__ */ jsx5("h2", { className: "vos-drawer-title", children: rec.title })
2910
+ ] }),
2911
+ /* @__PURE__ */ jsxs5("div", { style: { marginLeft: "auto", display: "flex", gap: 8, alignItems: "center" }, children: [
2912
+ /* @__PURE__ */ jsx5(
2913
+ "span",
2914
+ {
2915
+ className: "vos-next-moves-lens",
2916
+ style: { background: `${rec.lensColour}20`, color: rec.lensColour, borderColor: `${rec.lensColour}40` },
2917
+ children: rec.lens
2918
+ }
2919
+ ),
2920
+ /* @__PURE__ */ jsx5("span", { className: "vos-next-moves-rec-type", children: rec.type }),
2921
+ /* @__PURE__ */ jsxs5("span", { className: "vos-next-moves-rec-risk vos-num", children: [
2922
+ Math.round(rec.maxRisk),
2923
+ " risk"
2924
+ ] }),
2925
+ /* @__PURE__ */ jsx5("button", { type: "button", className: "vos-btn vos-btn-sm", onClick: onClose, "aria-label": "Close", children: "\u2715" })
2719
2926
  ] })
2720
2927
  ] }),
2721
- /* @__PURE__ */ jsxs5("div", { className: "vos-pipe-rec-body-inner", children: [
2722
- /* @__PURE__ */ jsx5("div", { className: "vos-pipe-rec-chips", children: rec.assumptionIds.map((id) => /* @__PURE__ */ jsx5(
2723
- "button",
2724
- {
2725
- type: "button",
2726
- className: "vos-pipe-rec-chip",
2727
- onClick: () => onOpenAssumption(id),
2728
- children: id
2729
- },
2730
- id
2731
- )) }),
2732
- /* @__PURE__ */ jsx5("div", { className: "vos-pipe-rec-rationale", children: rec.rationale }),
2733
- /* @__PURE__ */ jsxs5("div", { className: "vos-pipe-rec-bar", children: [
2734
- /* @__PURE__ */ jsx5("strong", { children: "Right if:" }),
2735
- " ",
2736
- rec.barPreview
2928
+ /* @__PURE__ */ jsxs5("div", { className: "vos-drawer-body", children: [
2929
+ /* @__PURE__ */ jsxs5("div", { className: "vos-rec-drawer-section", children: [
2930
+ /* @__PURE__ */ jsx5("div", { className: "vos-drawer-eyebrow", children: "Why this test" }),
2931
+ /* @__PURE__ */ jsx5("p", { className: "vos-rec-drawer-rationale", children: rec.rationale })
2737
2932
  ] }),
2738
- /* @__PURE__ */ jsx5("div", { className: "vos-pipe-rec-body", children: /* @__PURE__ */ jsx5(EvidenceBody, { text: rec.body }) }),
2739
- /* @__PURE__ */ jsx5("div", { className: "vos-pipe-rec-actions", children: /* @__PURE__ */ jsx5("button", { type: "button", className: "vos-btn vos-btn-sm vos-btn-accent", children: "Accept & create experiment" }) })
2740
- ] })
2741
- ] }, rec.id))
2742
- ] })
2743
- ] })
2933
+ /* @__PURE__ */ jsxs5("div", { className: "vos-rec-drawer-section", children: [
2934
+ /* @__PURE__ */ jsx5("div", { className: "vos-drawer-eyebrow", children: "Bar" }),
2935
+ /* @__PURE__ */ jsxs5("div", { className: "vos-rec-drawer-bar", children: [
2936
+ /* @__PURE__ */ jsx5("strong", { children: "Right if:" }),
2937
+ " ",
2938
+ rec.barPreview
2939
+ ] })
2940
+ ] }),
2941
+ /* @__PURE__ */ jsxs5("div", { className: "vos-rec-drawer-section", children: [
2942
+ /* @__PURE__ */ jsx5("div", { className: "vos-drawer-eyebrow", children: "Assumptions" }),
2943
+ /* @__PURE__ */ jsx5("div", { className: "vos-next-moves-rec-chips", children: rec.assumptionIds.map((id) => /* @__PURE__ */ jsx5(
2944
+ "button",
2945
+ {
2946
+ type: "button",
2947
+ className: "vos-next-moves-rec-chip",
2948
+ onClick: () => onOpenAssumption(id),
2949
+ children: id
2950
+ },
2951
+ id
2952
+ )) })
2953
+ ] }),
2954
+ /* @__PURE__ */ jsxs5("div", { className: "vos-rec-drawer-section", children: [
2955
+ /* @__PURE__ */ jsx5("div", { className: "vos-drawer-eyebrow", children: "Protocol" }),
2956
+ /* @__PURE__ */ jsx5("div", { className: "vos-rec-drawer-body", children: /* @__PURE__ */ jsx5(EvidenceBody, { text: rec.body }) })
2957
+ ] })
2958
+ ] }),
2959
+ /* @__PURE__ */ jsx5("div", { className: "vos-drawer-footer", children: /* @__PURE__ */ jsx5("button", { type: "button", className: "vos-btn vos-btn-sm vos-btn-accent", children: "Accept & create experiment" }) })
2960
+ ]
2961
+ }
2962
+ )
2744
2963
  ] });
2745
2964
  }
2746
2965
  function riskToneClass(risk) {
@@ -2766,8 +2985,8 @@ function ConfidenceDonut({
2766
2985
  value,
2767
2986
  size = 56
2768
2987
  }) {
2769
- const stroke = size > 60 ? 6 : 4;
2770
- const r = (size - stroke) / 2;
2988
+ const stroke = size > 60 ? 4 : 3;
2989
+ const r = (size - stroke * 3) / 2;
2771
2990
  const cx = size / 2;
2772
2991
  const cy = size / 2;
2773
2992
  const circumference = 2 * Math.PI * r;
@@ -2783,7 +3002,7 @@ function ConfidenceDonut({
2783
3002
  cy,
2784
3003
  r,
2785
3004
  fill: "none",
2786
- stroke: "var(--vos-surface-2)",
3005
+ stroke: "var(--vos-border)",
2787
3006
  strokeWidth: stroke
2788
3007
  }
2789
3008
  ),
@@ -2807,7 +3026,7 @@ function ConfidenceDonut({
2807
3026
  "span",
2808
3027
  {
2809
3028
  className: "vos-donut-num vos-num",
2810
- style: { fontSize: size > 60 ? 20 : 14 },
3029
+ style: { fontSize: size > 60 ? 22 : 15 },
2811
3030
  children: Math.round(value)
2812
3031
  }
2813
3032
  )
@@ -2948,11 +3167,11 @@ function ExperimentDetail({
2948
3167
  /* @__PURE__ */ jsxs7("div", { className: "vos-detail-section-label", children: [
2949
3168
  "Evidence \xB7 ",
2950
3169
  expReadings.length,
2951
- " reading",
3170
+ " piece",
2952
3171
  expReadings.length === 1 ? "" : "s",
2953
3172
  " from this experiment"
2954
3173
  ] }),
2955
- expReadings.length === 0 ? /* @__PURE__ */ jsx7("div", { className: "vos-muted vos-empty", children: "No readings yet \u2014 experiment is running." }) : expReadings.map((r) => {
3174
+ expReadings.length === 0 ? /* @__PURE__ */ jsx7("div", { className: "vos-muted vos-empty", children: "No evidence yet \u2014 experiment is running." }) : expReadings.map((r) => {
2956
3175
  const beliefs = readingBeliefs(r);
2957
3176
  const rDate = String(r.Date ?? "");
2958
3177
  const rTitle = String(r.Title ?? r.id);
@@ -2999,10 +3218,20 @@ function ExperimentDetail({
2999
3218
  /* @__PURE__ */ jsx7("span", { className: `vos-pill vos-pill-${verdictTone2(b.Result)}`, children: b.Result }),
3000
3219
  /* @__PURE__ */ jsx7("span", { className: "vos-rung-tag", children: String(r.Rung ?? "") })
3001
3220
  ] }),
3002
- justification ? /* @__PURE__ */ jsxs7("div", { className: `vos-belief-rationale vos-verdict-border-${verdictTone2(b.Result)}`, children: [
3003
- /* @__PURE__ */ jsx7("span", { className: "vos-belief-rationale-label", children: "grading rationale:" }),
3004
- justification
3005
- ] }) : null,
3221
+ typeof b.excerpt === "string" && b.excerpt !== "" ? /* @__PURE__ */ jsxs7("div", { className: `vos-belief-excerpt vos-verdict-border-${verdictTone2(b.Result)}`, children: [
3222
+ "\u201C",
3223
+ b.excerpt,
3224
+ "\u201D"
3225
+ ] }) : justification ? /* @__PURE__ */ jsxs7(
3226
+ "div",
3227
+ {
3228
+ className: `vos-belief-rationale vos-verdict-border-${verdictTone2(b.Result)}`,
3229
+ children: [
3230
+ /* @__PURE__ */ jsx7("span", { className: "vos-belief-rationale-label", children: "grading rationale:" }),
3231
+ justification
3232
+ ]
3233
+ }
3234
+ ) : null,
3006
3235
  bl ? /* @__PURE__ */ jsxs7("div", { className: "vos-belief-bar", children: [
3007
3236
  /* @__PURE__ */ jsx7("div", { className: "vos-belief-bar-label", children: "Pre-registered bar" }),
3008
3237
  /* @__PURE__ */ jsxs7("div", { children: [
@@ -3032,7 +3261,7 @@ function ExperimentDetail({
3032
3261
  unstartedBars.length,
3033
3262
  " bar",
3034
3263
  unstartedBars.length === 1 ? "" : "s",
3035
- " with no readings"
3264
+ " with no evidence"
3036
3265
  ] }),
3037
3266
  unstartedBars.map((bl) => {
3038
3267
  const a = (assumptions.records ?? []).find((x) => String(x.id) === bl.assumptionId);
@@ -3049,7 +3278,7 @@ function ExperimentDetail({
3049
3278
  ),
3050
3279
  /* @__PURE__ */ jsx7("span", { className: "vos-belief-title", children: String(a?.Title ?? bl.assumptionId) }),
3051
3280
  /* @__PURE__ */ jsx7("span", { className: "vos-rung-tag", children: String(bl.plannedRung ?? "") }),
3052
- /* @__PURE__ */ jsx7("span", { className: "vos-muted", children: "\u25CC no reading" })
3281
+ /* @__PURE__ */ jsx7("span", { className: "vos-muted", children: "\u25CC no evidence" })
3053
3282
  ] }),
3054
3283
  /* @__PURE__ */ jsxs7("div", { className: "vos-belief-bar", children: [
3055
3284
  /* @__PURE__ */ jsxs7("div", { children: [
@@ -3168,15 +3397,15 @@ function ReadingDetail({
3168
3397
  );
3169
3398
  if (loading) {
3170
3399
  return /* @__PURE__ */ jsxs9("div", { children: [
3171
- /* @__PURE__ */ jsx9(Breadcrumb, { trail: [{ label: "Readings", route: { name: "readings" } }], onNavigate }),
3172
- /* @__PURE__ */ jsx9("p", { className: "vos-muted", children: "Loading reading\u2026" })
3400
+ /* @__PURE__ */ jsx9(Breadcrumb, { trail: [{ label: "Evidence", route: { name: "readings" } }], onNavigate }),
3401
+ /* @__PURE__ */ jsx9("p", { className: "vos-muted", children: "Loading evidence\u2026" })
3173
3402
  ] });
3174
3403
  }
3175
3404
  if (!reading) {
3176
3405
  return /* @__PURE__ */ jsxs9("div", { children: [
3177
- /* @__PURE__ */ jsx9(Breadcrumb, { trail: [{ label: "Readings", route: { name: "readings" } }], onNavigate }),
3406
+ /* @__PURE__ */ jsx9(Breadcrumb, { trail: [{ label: "Evidence", route: { name: "readings" } }], onNavigate }),
3178
3407
  /* @__PURE__ */ jsxs9("p", { className: "vos-error", children: [
3179
- "Reading not found: ",
3408
+ "Evidence not found: ",
3180
3409
  readingId
3181
3410
  ] })
3182
3411
  ] });
@@ -3193,7 +3422,7 @@ function ReadingDetail({
3193
3422
  Breadcrumb,
3194
3423
  {
3195
3424
  trail: [
3196
- { label: "Readings", route: { name: "readings" } },
3425
+ { label: "Evidence", route: { name: "readings" } },
3197
3426
  { label: readingId, route: { name: "reading", id: readingId } }
3198
3427
  ],
3199
3428
  onNavigate
@@ -3217,7 +3446,7 @@ function ReadingDetail({
3217
3446
  beliefs.length === 1 ? "" : "s",
3218
3447
  " \xB7 each with its own quote"
3219
3448
  ] }),
3220
- beliefs.length === 0 ? /* @__PURE__ */ jsx9("div", { className: "vos-muted", children: "No beliefs scored in this reading." }) : beliefs.map((b) => {
3449
+ beliefs.length === 0 ? /* @__PURE__ */ jsx9("div", { className: "vos-muted", children: "No beliefs scored in this evidence." }) : beliefs.map((b) => {
3221
3450
  const a = (assumptions.records ?? []).find((x) => String(x.id) === b.assumptionId);
3222
3451
  const bl = experiment && Array.isArray(experiment.barLines) ? experiment.barLines.find((x) => x?.assumptionId === b.assumptionId) : null;
3223
3452
  const result = String(b.Result ?? "Inconclusive");
@@ -3237,11 +3466,19 @@ function ReadingDetail({
3237
3466
  /* @__PURE__ */ jsx9("span", { className: `vos-pill vos-pill-${verdictTone3(result)}`, children: result }),
3238
3467
  /* @__PURE__ */ jsx9("span", { className: "vos-rung-tag", children: rung })
3239
3468
  ] }),
3469
+ typeof b.excerpt === "string" && b.excerpt !== "" ? /* @__PURE__ */ jsxs9("div", { className: `vos-belief-excerpt vos-verdict-border-${verdictTone3(result)}`, children: [
3470
+ "\u201C",
3471
+ b.excerpt,
3472
+ "\u201D"
3473
+ ] }) : body ? /* @__PURE__ */ jsxs9("div", { className: `vos-belief-excerpt vos-verdict-border-${verdictTone3(result)}`, children: [
3474
+ "\u201C",
3475
+ snippetFromReadingBody(body, b.assumptionId),
3476
+ "\u201D"
3477
+ ] }) : null,
3240
3478
  justification ? /* @__PURE__ */ jsxs9("div", { className: `vos-belief-rationale vos-verdict-border-${verdictTone3(result)}`, children: [
3241
3479
  /* @__PURE__ */ jsx9("span", { className: "vos-belief-rationale-label", children: "grading rationale:" }),
3242
3480
  justification
3243
3481
  ] }) : null,
3244
- body ? /* @__PURE__ */ jsx9("div", { className: "vos-belief-context-link vos-muted", children: "see context above for the full quote" }) : null,
3245
3482
  bl ? /* @__PURE__ */ jsxs9("div", { className: "vos-belief-bar", children: [
3246
3483
  /* @__PURE__ */ jsx9("div", { className: "vos-belief-bar-label", children: "Pre-registered bar" }),
3247
3484
  /* @__PURE__ */ jsxs9("div", { children: [
@@ -3288,6 +3525,21 @@ function verdictTone3(verdict) {
3288
3525
  if (verdict === "Invalidated") return "crit";
3289
3526
  return "neutral";
3290
3527
  }
3528
+ function snippetFromReadingBody(body, cue) {
3529
+ if (!body) return "";
3530
+ const quoteMatch = body.match(/## Quote\n+([\s\S]*?)(?=\n## |\n##$|$)/i);
3531
+ if (quoteMatch) {
3532
+ const q = quoteMatch[1].trim();
3533
+ return q.length > 220 ? q.slice(0, 217).trim() + "\u2026" : q;
3534
+ }
3535
+ const sentences = body.split(/(?<=[.!?])\s+/);
3536
+ const cueLower = cue.toLowerCase();
3537
+ for (const s of sentences) {
3538
+ if (s.toLowerCase().includes(cueLower)) return s.trim();
3539
+ }
3540
+ const first = sentences[0]?.trim() ?? "";
3541
+ return first.length > 220 ? first.slice(0, 217).trim() + "\u2026" : first;
3542
+ }
3291
3543
 
3292
3544
  // src/readings-surface.tsx
3293
3545
  import { jsx as jsx10, jsxs as jsxs10 } from "react/jsx-runtime";
@@ -5119,6 +5371,11 @@ function BeliefVerdicts({
5119
5371
  "Strength ",
5120
5372
  formatSigned(v.strength)
5121
5373
  ] }) }) : null,
5374
+ v.excerpt ? /* @__PURE__ */ jsxs17("p", { className: "vos-verdict-quote", children: [
5375
+ "\u201C",
5376
+ v.excerpt,
5377
+ "\u201D"
5378
+ ] }) : null,
5122
5379
  v.justification ? /* @__PURE__ */ jsx17("p", { className: "vos-verdict-why", children: v.justification }) : null
5123
5380
  ]
5124
5381
  },