@validation-os/dashboard 0.15.1 → 0.15.3

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
@@ -219,6 +219,75 @@ function buildEvidenceComposition(assumption, readings) {
219
219
  };
220
220
  }
221
221
 
222
+ // src/confidence-explainer.ts
223
+ import { RUNG_ANCHOR, W0_BY_RUNG } from "@validation-os/core/derivation";
224
+ var RUNG_INFO = {
225
+ Talk: {
226
+ label: "Talk",
227
+ description: "Opinions, pitch reactions, anecdotes. Quick to get but slow to move the needle \u2014 needs ~10 distinct sources to approach the cap."
228
+ },
229
+ "Desk research": {
230
+ label: "Desk research",
231
+ description: "Published sources, competitor analysis, market reports. Authoritative \u2014 2 strong sources nearly saturate this rung."
232
+ },
233
+ "Signed up": {
234
+ label: "Signed up (Consumer)",
235
+ description: "Consumers signing up for the product. A do-rung \u2014 20 signups bring this to ~75% of its cap."
236
+ },
237
+ "Observed usage": {
238
+ label: "Observed usage (Consumer)",
239
+ description: "Prototype usage, analytics, telemetry. A do-rung \u2014 20 observed users bring this to ~75% of its cap."
240
+ },
241
+ "Signed intent": {
242
+ label: "Signed intent (Commercial)",
243
+ description: "LOIs, signed letters of intent from businesses. A do-rung \u2014 20 signed intents bring this to ~75% of its cap."
244
+ },
245
+ "Paying users": {
246
+ label: "Paying users (Commercial)",
247
+ description: "Closed commitments \u2014 revenue. The strongest do-rung \u2014 20 paying users bring this to ~75% of its cap."
248
+ }
249
+ };
250
+ function buildConfidenceExplainer(assumption, readings) {
251
+ const comp = buildEvidenceComposition(assumption, readings);
252
+ const lens = str(assumption.Lens) ?? "";
253
+ const lensRungs = new Set(comp.rungs.map((r) => r.rung));
254
+ const allRungs = [
255
+ "Talk",
256
+ "Desk research",
257
+ "Signed up",
258
+ "Observed usage",
259
+ "Signed intent",
260
+ "Paying users"
261
+ ];
262
+ const rungs = allRungs.map((rung) => {
263
+ const e = comp.rungs.find((r) => r.rung === rung);
264
+ const info = RUNG_INFO[rung] ?? { label: rung, description: "" };
265
+ return {
266
+ rung,
267
+ w0: W0_BY_RUNG[rung] ?? 100,
268
+ anchors: RUNG_ANCHOR[rung] ?? { Low: 0, Typical: 0, High: 0 },
269
+ contribution: e?.contribution ?? 0,
270
+ count: e?.count ?? 0,
271
+ inLens: lensRungs.has(rung),
272
+ label: info.label,
273
+ description: info.description
274
+ };
275
+ });
276
+ const confidence2 = assumption.derived?.confidence ?? 0;
277
+ const totalContribution = comp.totalContribution;
278
+ const rungsWithEvidence = rungs.filter((r) => r.count > 0);
279
+ const w0Sum = rungsWithEvidence.reduce((s, r) => s + r.w0, 0);
280
+ const summary = rungsWithEvidence.length === 0 ? "No concluded evidence yet. Confidence is 0 \u2014 the bet is open. Any Validated or Invalidated evidence at any rung will start moving this number." : rungsWithEvidence.length === 1 ? `All evidence is at the ${rungsWithEvidence[0].rung} rung (${rungsWithEvidence[0].count} source${rungsWithEvidence[0].count === 1 ? "" : "s"}). ${rungsWithEvidence[0].description}` : `Evidence spans ${rungsWithEvidence.length} rungs. The strongest push comes from ${rungs.reduce((a, b) => Math.abs(b.contribution) > Math.abs(a.contribution) ? b : a).rung}.`;
281
+ return {
282
+ confidence: confidence2,
283
+ formula: "Confidence = \u03A3(weight \xD7 strength) / (\u03A3 W0[rungs with evidence] + \u03A3 weights)",
284
+ rungs,
285
+ totalContribution,
286
+ denominator: w0Sum,
287
+ summary
288
+ };
289
+ }
290
+
222
291
  // src/markdown.tsx
223
292
  import { useState } from "react";
224
293
  import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
@@ -1426,6 +1495,7 @@ function AssumptionDetail({
1426
1495
  /* @__PURE__ */ jsx4(ScoreCard, { label: "Framed", value: `${Math.round(framed)}%` })
1427
1496
  ] }),
1428
1497
  /* @__PURE__ */ jsx4(EvidenceCompositionView, { assumption: record, readings: readings.records ?? [] }),
1498
+ /* @__PURE__ */ jsx4(ConfidenceExplainerView, { assumption: record, readings: readings.records ?? [] }),
1429
1499
  statement ? /* @__PURE__ */ jsxs4("div", { className: "vos-card vos-detail-section", children: [
1430
1500
  /* @__PURE__ */ jsx4("div", { className: "vos-detail-section-label", children: "Body" }),
1431
1501
  /* @__PURE__ */ jsx4(GlossaryText, { text: statement, terms: glossaryTerms, selfId: assumptionId })
@@ -1513,11 +1583,10 @@ function AssumptionDetail({
1513
1583
  /* @__PURE__ */ jsx4("span", { className: `vos-pill vos-pill-${verdictTone(result)}`, children: result }),
1514
1584
  /* @__PURE__ */ jsx4("span", { className: "vos-rung-tag", children: rung })
1515
1585
  ] }),
1516
- /* @__PURE__ */ jsxs4("div", { className: `vos-reading-excerpt vos-verdict-border-${verdictTone(result)}`, children: [
1517
- '"',
1518
- justification,
1519
- '"'
1520
- ] }),
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,
1521
1590
  /* @__PURE__ */ jsxs4("div", { className: "vos-reading-source", children: [
1522
1591
  source,
1523
1592
  expId ? /* @__PURE__ */ jsxs4(Fragment3, { children: [
@@ -1631,6 +1700,72 @@ function nextMoveFor(assumption, experiments) {
1631
1700
  if (framed) return "Design an experiment";
1632
1701
  return "Frame the belief";
1633
1702
  }
1703
+ function ConfidenceExplainerView({
1704
+ assumption,
1705
+ readings
1706
+ }) {
1707
+ const view = useMemo(
1708
+ () => buildConfidenceExplainer(assumption, readings),
1709
+ [assumption, readings]
1710
+ );
1711
+ return /* @__PURE__ */ jsxs4("details", { className: "vos-card vos-detail-section vos-explainer", children: [
1712
+ /* @__PURE__ */ jsxs4("summary", { className: "vos-explainer-summary", children: [
1713
+ /* @__PURE__ */ jsx4("span", { className: "vos-detail-section-label", children: "How Confidence is calculated" }),
1714
+ /* @__PURE__ */ jsx4("span", { className: "vos-explainer-conf vos-num", children: formatSigned(view.confidence) })
1715
+ ] }),
1716
+ /* @__PURE__ */ jsxs4("div", { className: "vos-explainer-body", children: [
1717
+ /* @__PURE__ */ jsx4("p", { className: "vos-explainer-formula", children: view.formula }),
1718
+ /* @__PURE__ */ jsx4("p", { className: "vos-explainer-summary-text", children: view.summary }),
1719
+ /* @__PURE__ */ jsxs4("div", { className: "vos-explainer-rungs", children: [
1720
+ /* @__PURE__ */ jsxs4("div", { className: "vos-explainer-rungs-head", children: [
1721
+ /* @__PURE__ */ jsx4("span", { children: "Rung" }),
1722
+ /* @__PURE__ */ jsx4("span", { children: "W0 (prior)" }),
1723
+ /* @__PURE__ */ jsx4("span", { children: "Anchors (L/T/H)" }),
1724
+ /* @__PURE__ */ jsx4("span", { children: "Evidence" }),
1725
+ /* @__PURE__ */ jsx4("span", { children: "Contribution" })
1726
+ ] }),
1727
+ view.rungs.map((r) => /* @__PURE__ */ jsxs4(
1728
+ "div",
1729
+ {
1730
+ className: `vos-explainer-rung ${!r.inLens ? "is-not-lens" : ""} ${r.count > 0 ? "has-evidence" : ""}`,
1731
+ children: [
1732
+ /* @__PURE__ */ jsx4("span", { className: "vos-explainer-rung-name", title: r.description, children: r.label }),
1733
+ /* @__PURE__ */ jsx4("span", { className: "vos-explainer-rung-w0 vos-num", children: r.w0 }),
1734
+ /* @__PURE__ */ jsxs4("span", { className: "vos-explainer-rung-anchors vos-num", children: [
1735
+ r.anchors.Low,
1736
+ "/",
1737
+ r.anchors.Typical,
1738
+ "/",
1739
+ r.anchors.High
1740
+ ] }),
1741
+ /* @__PURE__ */ jsx4("span", { className: "vos-explainer-rung-count vos-num", children: r.count > 0 ? `${r.count} source${r.count === 1 ? "" : "s"}` : "\u2014" }),
1742
+ /* @__PURE__ */ jsx4("span", { className: `vos-explainer-rung-contrib vos-num ${r.contribution > 0 ? "vos-text-good" : r.contribution < 0 ? "vos-text-crit" : ""}`, children: r.count > 0 ? formatSigned(r.contribution) : "\u2014" })
1743
+ ]
1744
+ },
1745
+ r.rung
1746
+ ))
1747
+ ] }),
1748
+ /* @__PURE__ */ jsxs4("div", { className: "vos-explainer-foot", children: [
1749
+ /* @__PURE__ */ jsxs4("p", { children: [
1750
+ /* @__PURE__ */ jsx4("strong", { children: "W0" }),
1751
+ " = the prior weight for each rung \u2014 how many distinct sources it takes to approach that rung's cap. Desk research has a low W0 (2 \u2014 one authoritative source nearly saturates it); Talk has a higher W0 (6.5 \u2014 needs ~10 sources); do-rungs have high W0s (327 \u2014 needs ~20 sources to reach 75% of the cap)."
1752
+ ] }),
1753
+ /* @__PURE__ */ jsxs4("p", { children: [
1754
+ /* @__PURE__ */ jsx4("strong", { children: "Strength" }),
1755
+ " = the rung's anchor \xD7 the sign of the result (Validated = +, Invalidated = \u2212, Inconclusive = 0). The anchor is the band (Low/Typical/High) the evidence lands at."
1756
+ ] }),
1757
+ /* @__PURE__ */ jsxs4("p", { children: [
1758
+ /* @__PURE__ */ jsx4("strong", { children: "Weight" }),
1759
+ " = |Strength| \xD7 source quality \xD7 commitment factor (1.0 for evidence linked to an experiment, 0.85 for found evidence)."
1760
+ ] }),
1761
+ /* @__PURE__ */ jsxs4("p", { children: [
1762
+ /* @__PURE__ */ jsx4("strong", { children: "Contribution" }),
1763
+ " = (Weight \xD7 Strength) / denominator. The sum of all contributions equals Confidence. Evidence at a higher rung always outweighs evidence at a lower rung \u2014 the rung ladder dominates."
1764
+ ] })
1765
+ ] })
1766
+ ] })
1767
+ ] });
1768
+ }
1634
1769
  function EvidenceCompositionView({
1635
1770
  assumption,
1636
1771
  readings
@@ -1964,6 +2099,38 @@ function weekOverWeekDelta(assumptions, readings, now) {
1964
2099
  }
1965
2100
 
1966
2101
  // src/recommended-experiments.ts
2102
+ var MAX_RECOMMENDED = 2;
2103
+ var MAX_CLUSTER_SIZE = 3;
2104
+ var MAX_NEEDS_FRAMING = 2;
2105
+ function buildNeedsFraming(assumptions) {
2106
+ const live = assumptions.filter((a) => {
2107
+ const status = str(a.Status);
2108
+ const moot = a.moot === true;
2109
+ return !moot && (status === "Live" || status === "Draft");
2110
+ });
2111
+ const items = live.map((a) => {
2112
+ const id = str(a.id) ?? "";
2113
+ const completeness = derivedNum(a, "completeness") ?? 0;
2114
+ const risk = derivedNum(a, "risk") ?? 0;
2115
+ const lens = str(a.Lens) ?? "\u2014";
2116
+ const stage = str(a.Stage) ?? "\u2014";
2117
+ const title = str(a.Title) ?? id;
2118
+ const hint = framingHint(a, completeness);
2119
+ return { id, title, risk, completeness, lens, stage, hint };
2120
+ }).filter((a) => a.completeness < 100).sort((a, b) => b.risk - a.risk).slice(0, MAX_NEEDS_FRAMING);
2121
+ return items;
2122
+ }
2123
+ function framingHint(a, completeness) {
2124
+ const hasScoring = Boolean(str(a["Scoring justification"]));
2125
+ const hasDescription = Boolean(str(a.Description));
2126
+ if (completeness < 50) {
2127
+ return hasDescription ? "The belief is written but incomplete \u2014 fill in the scoring justification and any missing framing fields." : "The belief needs a description \u2014 what exactly is being claimed, in falsifiable terms.";
2128
+ }
2129
+ if (!hasScoring) {
2130
+ return "Add a scoring justification \u2014 why is the Impact seed scored as it is?";
2131
+ }
2132
+ return "Nearly framed \u2014 check the 5 Whys and the metric for truth are complete.";
2133
+ }
1967
2134
  var LENS_TO_TYPE = {
1968
2135
  Consumer: "Observation",
1969
2136
  Commercial: "Test"
@@ -1992,16 +2159,22 @@ function buildRecommendedExperiments(assumptions, experiments) {
1992
2159
  if (bucket) bucket.push(a);
1993
2160
  else clusters.set(key, [a]);
1994
2161
  }
2162
+ const clusterEntries = [...clusters.entries()].map(([key, cluster]) => {
2163
+ const ranked = cluster.sort((a, b) => (derivedNum(b, "risk") ?? 0) - (derivedNum(a, "risk") ?? 0)).slice(0, MAX_CLUSTER_SIZE);
2164
+ const maxRisk = Math.max(...ranked.map((a) => derivedNum(a, "risk") ?? 0), 0);
2165
+ return { key, cluster: ranked, maxRisk };
2166
+ }).sort((a, b) => b.maxRisk - a.maxRisk).slice(0, MAX_RECOMMENDED);
1995
2167
  const recs = [];
1996
- for (const [key, cluster] of clusters) {
1997
- const [lens, stage] = key.split("\xD7");
1998
- const risks = cluster.map((a) => derivedNum(a, "risk") ?? 0);
1999
- const maxRisk = Math.max(...risks, 0);
2168
+ for (const { key, cluster, maxRisk } of clusterEntries) {
2169
+ const lens = key.split("\xD7")[0] ?? "\u2014";
2170
+ const stage = key.split("\xD7")[1] ?? "\u2014";
2000
2171
  const assumptionIds = cluster.map((a) => str(a.id) ?? "").filter(Boolean).sort();
2001
2172
  const type = LENS_TO_TYPE[lens ?? ""] ?? "Desk research";
2173
+ const titles = cluster.map((a) => str(a.Title) ?? str(a.id) ?? "");
2002
2174
  const title = cluster.length === 1 ? `Test ${assumptionIds[0]}` : `Test ${cluster.length} ${lens} \xB7 ${stage} beliefs`;
2003
2175
  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.`;
2004
2176
  const barPreview = `The riskiest belief moves out of the kill zone, or stays in it.`;
2177
+ const body = generateExperimentBody(type, lens, stage, cluster, maxRisk);
2005
2178
  recs.push({
2006
2179
  id: assumptionIds.join("+"),
2007
2180
  type,
@@ -2009,7 +2182,8 @@ function buildRecommendedExperiments(assumptions, experiments) {
2009
2182
  assumptionIds,
2010
2183
  maxRisk,
2011
2184
  rationale,
2012
- barPreview
2185
+ barPreview,
2186
+ body
2013
2187
  });
2014
2188
  }
2015
2189
  recs.sort(
@@ -2017,6 +2191,29 @@ function buildRecommendedExperiments(assumptions, experiments) {
2017
2191
  );
2018
2192
  return recs;
2019
2193
  }
2194
+ function generateExperimentBody(type, lens, stage, cluster, maxRisk) {
2195
+ const beliefs = cluster.map((a) => `- **${str(a.id)}**: ${str(a.Title) ?? str(a.id) ?? ""}`).join("\n");
2196
+ const rung = type === "Observation" ? "Observed usage" : type === "Test" ? "Signed intent" : "Desk research";
2197
+ return `## What this tests
2198
+
2199
+ This ${type.toLowerCase()} addresses ${cluster.length} ${lens} \xB7 ${stage} belief${cluster.length === 1 ? "" : "s"} at ${Math.round(maxRisk)} risk \u2014 the riskiest untested cluster on the board.
2200
+
2201
+ ${beliefs}
2202
+
2203
+ ## How to run it
2204
+
2205
+ ${type === "Observation" ? `Set up a prototype or analytics instrument that captures real user behaviour. Watch how ${cluster.length === 1 ? "this belief plays out" : "these beliefs play out"} in actual usage \u2014 not what people say they'd do, but what they actually do. Log each observation as evidence at the **${rung}** rung.` : type === "Test" ? `Reach out to potential ${lens === "Consumer" ? "customers" : "businesses"} and seek a signed commitment \u2014 a letter of intent, a pre-order, a pilot agreement. The commitment is the evidence; it lands at the **${rung}** rung.` : `Research published sources, competitor behaviour, and market data. Find external evidence that bears on ${cluster.length === 1 ? "this belief" : "these beliefs"}. Log each source as evidence at the **Desk research** rung.`}
2206
+
2207
+ ## Questions to answer
2208
+
2209
+ ${cluster.map((a, i) => `${i + 1}. Does ${str(a.Title) ?? str(a.id) ?? ""} hold \u2014 or does the evidence break it?`).join("\n")}
2210
+
2211
+ ## Pre-registered bars
2212
+
2213
+ - **Right if:** the riskiest belief moves out of the kill zone (Confidence > 0).
2214
+ - **Wrong if:** the evidence invalidates the riskiest belief (Confidence enters the kill zone).
2215
+ `;
2216
+ }
2020
2217
 
2021
2218
  // src/stage-meters.ts
2022
2219
  function clamp(pct2) {
@@ -2255,6 +2452,10 @@ function GridPane({
2255
2452
  () => buildRecommendedExperiments(assumptions, experiments),
2256
2453
  [assumptions, experiments]
2257
2454
  );
2455
+ const needsFraming = useMemo2(
2456
+ () => buildNeedsFraming(assumptions),
2457
+ [assumptions]
2458
+ );
2258
2459
  const cold = coldStartFor({
2259
2460
  assumptions,
2260
2461
  experiments,
@@ -2322,9 +2523,10 @@ function GridPane({
2322
2523
  ] }) }),
2323
2524
  /* @__PURE__ */ jsx5("p", { className: "vos-hint vos-stage-grid-foot", children: "The densest cell per row is where that part of the business is. Click a cell to drill into its assumptions, ranked by Risk; a single-assumption cell opens the detail directly." })
2324
2525
  ] }),
2325
- recs.length > 0 ? /* @__PURE__ */ jsx5(
2326
- RecommendedExperimentsSection,
2526
+ needsFraming.length > 0 || recs.length > 0 ? /* @__PURE__ */ jsx5(
2527
+ NextMovesSection,
2327
2528
  {
2529
+ needsFraming,
2328
2530
  recs,
2329
2531
  onOpenAssumption: (id) => onNavigate({ name: "assumption", id })
2330
2532
  }
@@ -2461,37 +2663,84 @@ function Meter2Seg({
2461
2663
  ] })
2462
2664
  ] });
2463
2665
  }
2464
- function RecommendedExperimentsSection({
2666
+ function NextMovesSection({
2667
+ needsFraming,
2465
2668
  recs,
2466
2669
  onOpenAssumption
2467
2670
  }) {
2468
- return /* @__PURE__ */ jsxs5("div", { className: "vos-card vos-pipe-recs", children: [
2469
- /* @__PURE__ */ jsx5("div", { className: "vos-pipe-recs-head", children: "Recommended experiments \xB7 grouped by riskiest assumptions" }),
2470
- /* @__PURE__ */ jsx5("div", { className: "vos-pipe-recs-list", children: recs.map((rec) => /* @__PURE__ */ jsxs5("div", { className: "vos-pipe-rec", children: [
2471
- /* @__PURE__ */ jsxs5("div", { className: "vos-pipe-rec-head", children: [
2472
- /* @__PURE__ */ jsx5("span", { className: "vos-pipe-rec-type", children: rec.type }),
2473
- /* @__PURE__ */ jsx5("span", { className: "vos-pipe-rec-title", children: rec.title }),
2474
- /* @__PURE__ */ jsx5("button", { type: "button", className: "vos-btn vos-btn-sm vos-btn-accent", children: "Accept" })
2671
+ return /* @__PURE__ */ jsxs5("div", { className: "vos-next-moves", children: [
2672
+ /* @__PURE__ */ jsx5("div", { className: "vos-next-moves-head", children: "Next moves" }),
2673
+ /* @__PURE__ */ jsxs5("div", { className: "vos-next-moves-cols", children: [
2674
+ /* @__PURE__ */ jsxs5("div", { className: "vos-card vos-next-moves-col", children: [
2675
+ /* @__PURE__ */ jsxs5("div", { className: "vos-next-moves-col-label", children: [
2676
+ "Needs framing \xB7 top ",
2677
+ needsFraming.length
2678
+ ] }),
2679
+ needsFraming.length === 0 ? /* @__PURE__ */ jsx5("div", { className: "vos-muted vos-next-moves-empty", children: "Every belief is fully framed." }) : needsFraming.map((a) => /* @__PURE__ */ jsxs5(
2680
+ "button",
2681
+ {
2682
+ type: "button",
2683
+ className: "vos-next-moves-item",
2684
+ onClick: () => onOpenAssumption(a.id),
2685
+ children: [
2686
+ /* @__PURE__ */ jsxs5("div", { className: "vos-next-moves-item-head", children: [
2687
+ /* @__PURE__ */ jsx5("span", { className: "vos-next-moves-item-id vos-num", children: a.id }),
2688
+ /* @__PURE__ */ jsxs5("span", { className: `vos-next-moves-item-risk vos-num vos-text-${riskToneClass(a.risk)}`, children: [
2689
+ Math.round(a.risk),
2690
+ " risk"
2691
+ ] })
2692
+ ] }),
2693
+ /* @__PURE__ */ jsx5("div", { className: "vos-next-moves-item-title", children: a.title }),
2694
+ /* @__PURE__ */ jsx5("div", { className: "vos-next-moves-item-hint", children: a.hint })
2695
+ ]
2696
+ },
2697
+ a.id
2698
+ ))
2475
2699
  ] }),
2476
- /* @__PURE__ */ jsx5("div", { className: "vos-pipe-rec-chips", children: rec.assumptionIds.map((id) => /* @__PURE__ */ jsx5(
2477
- "button",
2478
- {
2479
- type: "button",
2480
- className: "vos-pipe-rec-chip",
2481
- onClick: () => onOpenAssumption(id),
2482
- children: id
2483
- },
2484
- id
2485
- )) }),
2486
- /* @__PURE__ */ jsx5("div", { className: "vos-pipe-rec-rationale", children: rec.rationale }),
2487
- /* @__PURE__ */ jsxs5("div", { className: "vos-pipe-rec-bar", children: [
2488
- /* @__PURE__ */ jsx5("strong", { children: "Right if:" }),
2489
- " ",
2490
- rec.barPreview
2700
+ /* @__PURE__ */ jsxs5("div", { className: "vos-card vos-next-moves-col", children: [
2701
+ /* @__PURE__ */ jsxs5("div", { className: "vos-next-moves-col-label", children: [
2702
+ "Proposed experiments \xB7 top ",
2703
+ recs.length
2704
+ ] }),
2705
+ 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: [
2706
+ /* @__PURE__ */ jsxs5("summary", { className: "vos-pipe-rec-head", children: [
2707
+ /* @__PURE__ */ jsx5("span", { className: "vos-pipe-rec-type", children: rec.type }),
2708
+ /* @__PURE__ */ jsx5("span", { className: "vos-pipe-rec-title", children: rec.title }),
2709
+ /* @__PURE__ */ jsxs5("span", { className: "vos-pipe-rec-risk vos-num", children: [
2710
+ Math.round(rec.maxRisk),
2711
+ " risk"
2712
+ ] })
2713
+ ] }),
2714
+ /* @__PURE__ */ jsxs5("div", { className: "vos-pipe-rec-body-inner", children: [
2715
+ /* @__PURE__ */ jsx5("div", { className: "vos-pipe-rec-chips", children: rec.assumptionIds.map((id) => /* @__PURE__ */ jsx5(
2716
+ "button",
2717
+ {
2718
+ type: "button",
2719
+ className: "vos-pipe-rec-chip",
2720
+ onClick: () => onOpenAssumption(id),
2721
+ children: id
2722
+ },
2723
+ id
2724
+ )) }),
2725
+ /* @__PURE__ */ jsx5("div", { className: "vos-pipe-rec-rationale", children: rec.rationale }),
2726
+ /* @__PURE__ */ jsxs5("div", { className: "vos-pipe-rec-bar", children: [
2727
+ /* @__PURE__ */ jsx5("strong", { children: "Right if:" }),
2728
+ " ",
2729
+ rec.barPreview
2730
+ ] }),
2731
+ /* @__PURE__ */ jsx5("div", { className: "vos-pipe-rec-body", children: /* @__PURE__ */ jsx5(EvidenceBody, { text: rec.body }) }),
2732
+ /* @__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" }) })
2733
+ ] })
2734
+ ] }, rec.id))
2491
2735
  ] })
2492
- ] }, rec.id)) })
2736
+ ] })
2493
2737
  ] });
2494
2738
  }
2739
+ function riskToneClass(risk) {
2740
+ if (risk >= 70) return "crit";
2741
+ if (risk >= 40) return "warn";
2742
+ return "good";
2743
+ }
2495
2744
  function Breadcrumb2({
2496
2745
  trail
2497
2746
  }) {
@@ -2715,20 +2964,18 @@ function ExperimentDetail({
2715
2964
  ] }),
2716
2965
  beliefs.length > 0 ? /* @__PURE__ */ jsx7("div", { className: "vos-reading-quotes", children: beliefs.map((b) => {
2717
2966
  const justification = String(b["Grading justification"] ?? "");
2718
- const excerpt = justification || truncate(String(r.body ?? ""), 120);
2719
- if (!excerpt) return null;
2967
+ if (!justification) return null;
2720
2968
  return /* @__PURE__ */ jsxs7(
2721
2969
  "div",
2722
2970
  {
2723
- className: `vos-reading-quote vos-verdict-border-${verdictTone2(b.Result)}`,
2971
+ className: `vos-reading-rationale vos-verdict-border-${verdictTone2(b.Result)}`,
2724
2972
  children: [
2725
2973
  /* @__PURE__ */ jsxs7("span", { className: "vos-reading-quote-id vos-num", children: [
2726
2974
  b.assumptionId,
2727
2975
  " \xB7"
2728
2976
  ] }),
2729
- '"',
2730
- excerpt,
2731
- '"'
2977
+ /* @__PURE__ */ jsx7("span", { className: "vos-reading-rationale-label", children: "grading rationale:" }),
2978
+ justification
2732
2979
  ]
2733
2980
  },
2734
2981
  b.assumptionId
@@ -2827,10 +3074,6 @@ function ExperimentDetail({
2827
3074
  function pct(n, total) {
2828
3075
  return total === 0 ? 0 : n / total * 100;
2829
3076
  }
2830
- function truncate(s, max) {
2831
- if (s.length <= max) return s;
2832
- return s.slice(0, max - 1) + "\u2026";
2833
- }
2834
3077
  function verdictTone2(verdict) {
2835
3078
  if (verdict === "Validated") return "good";
2836
3079
  if (verdict === "Invalidated") return "crit";
@@ -2982,7 +3225,6 @@ function ReadingDetail({
2982
3225
  const bl = experiment && Array.isArray(experiment.barLines) ? experiment.barLines.find((x) => x?.assumptionId === b.assumptionId) : null;
2983
3226
  const result = String(b.Result ?? "Inconclusive");
2984
3227
  const justification = String(b["Grading justification"] ?? "");
2985
- const excerpt = justification || truncate2(body, 120);
2986
3228
  return /* @__PURE__ */ jsxs9("div", { className: `vos-belief-card vos-verdict-border-${verdictTone3(result)}`, children: [
2987
3229
  /* @__PURE__ */ jsxs9("div", { className: "vos-belief-head", children: [
2988
3230
  /* @__PURE__ */ jsx9(
@@ -2998,10 +3240,9 @@ function ReadingDetail({
2998
3240
  /* @__PURE__ */ jsx9("span", { className: `vos-pill vos-pill-${verdictTone3(result)}`, children: result }),
2999
3241
  /* @__PURE__ */ jsx9("span", { className: "vos-rung-tag", children: rung })
3000
3242
  ] }),
3001
- excerpt ? /* @__PURE__ */ jsxs9("div", { className: `vos-belief-excerpt vos-verdict-border-${verdictTone3(result)}`, children: [
3002
- '"',
3003
- excerpt,
3004
- '"'
3243
+ justification ? /* @__PURE__ */ jsxs9("div", { className: `vos-belief-rationale vos-verdict-border-${verdictTone3(result)}`, children: [
3244
+ /* @__PURE__ */ jsx9("span", { className: "vos-belief-rationale-label", children: "grading rationale:" }),
3245
+ justification
3005
3246
  ] }) : null,
3006
3247
  bl ? /* @__PURE__ */ jsxs9("div", { className: "vos-belief-bar", children: [
3007
3248
  /* @__PURE__ */ jsx9("div", { className: "vos-belief-bar-label", children: "Pre-registered bar" }),
@@ -3049,10 +3290,6 @@ function verdictTone3(verdict) {
3049
3290
  if (verdict === "Invalidated") return "crit";
3050
3291
  return "neutral";
3051
3292
  }
3052
- function truncate2(s, max) {
3053
- if (s.length <= max) return s;
3054
- return s.slice(0, max - 1) + "\u2026";
3055
- }
3056
3293
 
3057
3294
  // src/readings-surface.tsx
3058
3295
  import { jsx as jsx10, jsxs as jsxs10 } from "react/jsx-runtime";