@validation-os/dashboard 0.15.2 → 0.15.4

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,45 @@ 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 = 3;
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);
2121
+ const byLens = /* @__PURE__ */ new Map();
2122
+ for (const item of items) {
2123
+ const existing = byLens.get(item.lens);
2124
+ if (!existing || item.risk > existing.risk) {
2125
+ byLens.set(item.lens, item);
2126
+ }
2127
+ }
2128
+ return [...byLens.values()].sort((a, b) => b.risk - a.risk).slice(0, MAX_NEEDS_FRAMING);
2129
+ }
2130
+ function framingHint(a, completeness) {
2131
+ const hasScoring = Boolean(str(a["Scoring justification"]));
2132
+ const hasDescription = Boolean(str(a.Description));
2133
+ if (completeness < 50) {
2134
+ 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.";
2135
+ }
2136
+ if (!hasScoring) {
2137
+ return "Add a scoring justification \u2014 why is the Impact seed scored as it is?";
2138
+ }
2139
+ return "Nearly framed \u2014 check the 5 Whys and the metric for truth are complete.";
2140
+ }
1967
2141
  var LENS_TO_TYPE = {
1968
2142
  Consumer: "Observation",
1969
2143
  Commercial: "Test"
@@ -1992,16 +2166,22 @@ function buildRecommendedExperiments(assumptions, experiments) {
1992
2166
  if (bucket) bucket.push(a);
1993
2167
  else clusters.set(key, [a]);
1994
2168
  }
2169
+ const clusterEntries = [...clusters.entries()].map(([key, cluster]) => {
2170
+ const ranked = cluster.sort((a, b) => (derivedNum(b, "risk") ?? 0) - (derivedNum(a, "risk") ?? 0)).slice(0, MAX_CLUSTER_SIZE);
2171
+ const maxRisk = Math.max(...ranked.map((a) => derivedNum(a, "risk") ?? 0), 0);
2172
+ return { key, cluster: ranked, maxRisk };
2173
+ }).sort((a, b) => b.maxRisk - a.maxRisk).slice(0, MAX_RECOMMENDED);
1995
2174
  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);
2175
+ for (const { key, cluster, maxRisk } of clusterEntries) {
2176
+ const lens = key.split("\xD7")[0] ?? "\u2014";
2177
+ const stage = key.split("\xD7")[1] ?? "\u2014";
2000
2178
  const assumptionIds = cluster.map((a) => str(a.id) ?? "").filter(Boolean).sort();
2001
2179
  const type = LENS_TO_TYPE[lens ?? ""] ?? "Desk research";
2180
+ const titles = cluster.map((a) => str(a.Title) ?? str(a.id) ?? "");
2002
2181
  const title = cluster.length === 1 ? `Test ${assumptionIds[0]}` : `Test ${cluster.length} ${lens} \xB7 ${stage} beliefs`;
2003
2182
  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
2183
  const barPreview = `The riskiest belief moves out of the kill zone, or stays in it.`;
2184
+ const body = generateExperimentBody(type, lens, stage, cluster, maxRisk);
2005
2185
  recs.push({
2006
2186
  id: assumptionIds.join("+"),
2007
2187
  type,
@@ -2009,7 +2189,8 @@ function buildRecommendedExperiments(assumptions, experiments) {
2009
2189
  assumptionIds,
2010
2190
  maxRisk,
2011
2191
  rationale,
2012
- barPreview
2192
+ barPreview,
2193
+ body
2013
2194
  });
2014
2195
  }
2015
2196
  recs.sort(
@@ -2017,6 +2198,29 @@ function buildRecommendedExperiments(assumptions, experiments) {
2017
2198
  );
2018
2199
  return recs;
2019
2200
  }
2201
+ function generateExperimentBody(type, lens, stage, cluster, maxRisk) {
2202
+ const beliefs = cluster.map((a) => `- **${str(a.id)}**: ${str(a.Title) ?? str(a.id) ?? ""}`).join("\n");
2203
+ const rung = type === "Observation" ? "Observed usage" : type === "Test" ? "Signed intent" : "Desk research";
2204
+ return `## What this tests
2205
+
2206
+ 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.
2207
+
2208
+ ${beliefs}
2209
+
2210
+ ## How to run it
2211
+
2212
+ ${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.`}
2213
+
2214
+ ## Questions to answer
2215
+
2216
+ ${cluster.map((a, i) => `${i + 1}. Does ${str(a.Title) ?? str(a.id) ?? ""} hold \u2014 or does the evidence break it?`).join("\n")}
2217
+
2218
+ ## Pre-registered bars
2219
+
2220
+ - **Right if:** the riskiest belief moves out of the kill zone (Confidence > 0).
2221
+ - **Wrong if:** the evidence invalidates the riskiest belief (Confidence enters the kill zone).
2222
+ `;
2223
+ }
2020
2224
 
2021
2225
  // src/stage-meters.ts
2022
2226
  function clamp(pct2) {
@@ -2255,6 +2459,10 @@ function GridPane({
2255
2459
  () => buildRecommendedExperiments(assumptions, experiments),
2256
2460
  [assumptions, experiments]
2257
2461
  );
2462
+ const needsFraming = useMemo2(
2463
+ () => buildNeedsFraming(assumptions),
2464
+ [assumptions]
2465
+ );
2258
2466
  const cold = coldStartFor({
2259
2467
  assumptions,
2260
2468
  experiments,
@@ -2322,9 +2530,10 @@ function GridPane({
2322
2530
  ] }) }),
2323
2531
  /* @__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
2532
  ] }),
2325
- recs.length > 0 ? /* @__PURE__ */ jsx5(
2326
- RecommendedExperimentsSection,
2533
+ needsFraming.length > 0 || recs.length > 0 ? /* @__PURE__ */ jsx5(
2534
+ NextMovesSection,
2327
2535
  {
2536
+ needsFraming,
2328
2537
  recs,
2329
2538
  onOpenAssumption: (id) => onNavigate({ name: "assumption", id })
2330
2539
  }
@@ -2461,37 +2670,84 @@ function Meter2Seg({
2461
2670
  ] })
2462
2671
  ] });
2463
2672
  }
2464
- function RecommendedExperimentsSection({
2673
+ function NextMovesSection({
2674
+ needsFraming,
2465
2675
  recs,
2466
2676
  onOpenAssumption
2467
2677
  }) {
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" })
2678
+ return /* @__PURE__ */ jsxs5("div", { className: "vos-next-moves", children: [
2679
+ /* @__PURE__ */ jsx5("div", { className: "vos-next-moves-head", children: "Next moves" }),
2680
+ /* @__PURE__ */ jsxs5("div", { className: "vos-next-moves-cols", children: [
2681
+ /* @__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
+ ] }),
2686
+ 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
+ "button",
2688
+ {
2689
+ type: "button",
2690
+ className: "vos-next-moves-item",
2691
+ onClick: () => onOpenAssumption(a.id),
2692
+ children: [
2693
+ /* @__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 }),
2695
+ /* @__PURE__ */ jsxs5("span", { className: `vos-next-moves-item-risk vos-num vos-text-${riskToneClass(a.risk)}`, children: [
2696
+ Math.round(a.risk),
2697
+ " risk"
2698
+ ] })
2699
+ ] }),
2700
+ /* @__PURE__ */ jsx5("div", { className: "vos-next-moves-item-title", children: a.title }),
2701
+ /* @__PURE__ */ jsx5("div", { className: "vos-next-moves-item-hint", children: a.hint })
2702
+ ]
2703
+ },
2704
+ a.id
2705
+ ))
2475
2706
  ] }),
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
2707
+ /* @__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"
2719
+ ] })
2720
+ ] }),
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
2737
+ ] }),
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))
2491
2742
  ] })
2492
- ] }, rec.id)) })
2743
+ ] })
2493
2744
  ] });
2494
2745
  }
2746
+ function riskToneClass(risk) {
2747
+ if (risk >= 70) return "crit";
2748
+ if (risk >= 40) return "warn";
2749
+ return "good";
2750
+ }
2495
2751
  function Breadcrumb2({
2496
2752
  trail
2497
2753
  }) {
@@ -2510,15 +2766,14 @@ function ConfidenceDonut({
2510
2766
  value,
2511
2767
  size = 56
2512
2768
  }) {
2513
- const r = size / 2 - 5;
2769
+ const stroke = size > 60 ? 6 : 4;
2770
+ const r = (size - stroke) / 2;
2514
2771
  const cx = size / 2;
2515
2772
  const cy = size / 2;
2773
+ const circumference = 2 * Math.PI * r;
2516
2774
  const pct2 = Math.max(0, Math.min(100, value)) / 100;
2775
+ const dash = pct2 * circumference;
2517
2776
  const color = value >= 67 ? "var(--vos-good)" : value >= 33 ? "var(--vos-warn)" : "var(--vos-crit)";
2518
- const endAngle = -Math.PI / 2 + pct2 * 2 * Math.PI;
2519
- const x2 = cx + r * Math.cos(endAngle);
2520
- const y2 = cy + r * Math.sin(endAngle);
2521
- const largeArc = pct2 > 0.5 ? 1 : 0;
2522
2777
  return /* @__PURE__ */ jsxs6("div", { className: "vos-donut", style: { width: size, height: size }, children: [
2523
2778
  /* @__PURE__ */ jsxs6("svg", { width: size, height: size, "aria-hidden": "true", children: [
2524
2779
  /* @__PURE__ */ jsx6(
@@ -2529,21 +2784,33 @@ function ConfidenceDonut({
2529
2784
  r,
2530
2785
  fill: "none",
2531
2786
  stroke: "var(--vos-surface-2)",
2532
- strokeWidth: 4
2787
+ strokeWidth: stroke
2533
2788
  }
2534
2789
  ),
2535
2790
  value > 0 ? /* @__PURE__ */ jsx6(
2536
- "path",
2791
+ "circle",
2537
2792
  {
2538
- d: `M ${cx} ${cy} L ${cx} ${cy - r} A ${r} ${r} 0 ${largeArc} 1 ${x2} ${y2} Z`,
2539
- fill: color,
2540
- opacity: 0.25,
2793
+ cx,
2794
+ cy,
2795
+ r,
2796
+ fill: "none",
2541
2797
  stroke: color,
2542
- strokeWidth: 1.5
2798
+ strokeWidth: stroke,
2799
+ strokeDasharray: `${dash} ${circumference - dash}`,
2800
+ strokeDashoffset: 0,
2801
+ strokeLinecap: "round",
2802
+ transform: `rotate(-90 ${cx} ${cy})`
2543
2803
  }
2544
2804
  ) : null
2545
2805
  ] }),
2546
- /* @__PURE__ */ jsx6("span", { className: "vos-donut-num vos-num", style: { fontSize: size > 60 ? 18 : 14 }, children: Math.round(value) })
2806
+ /* @__PURE__ */ jsx6(
2807
+ "span",
2808
+ {
2809
+ className: "vos-donut-num vos-num",
2810
+ style: { fontSize: size > 60 ? 20 : 14 },
2811
+ children: Math.round(value)
2812
+ }
2813
+ )
2547
2814
  ] });
2548
2815
  }
2549
2816
 
@@ -2713,37 +2980,10 @@ function ExperimentDetail({
2713
2980
  }
2714
2981
  )
2715
2982
  ] }),
2716
- beliefs.length > 0 ? /* @__PURE__ */ jsx7("div", { className: "vos-reading-quotes", children: beliefs.map((b) => {
2717
- const justification = String(b["Grading justification"] ?? "");
2718
- const excerpt = justification || truncate(String(r.body ?? ""), 120);
2719
- if (!excerpt) return null;
2720
- return /* @__PURE__ */ jsxs7(
2721
- "div",
2722
- {
2723
- className: `vos-reading-quote vos-verdict-border-${verdictTone2(b.Result)}`,
2724
- children: [
2725
- /* @__PURE__ */ jsxs7("span", { className: "vos-reading-quote-id vos-num", children: [
2726
- b.assumptionId,
2727
- " \xB7"
2728
- ] }),
2729
- '"',
2730
- excerpt,
2731
- '"'
2732
- ]
2733
- },
2734
- b.assumptionId
2735
- );
2736
- }) }) : null,
2737
- /* @__PURE__ */ jsxs7("div", { className: "vos-reading-beliefs-label", children: [
2738
- "ADDRESSES ",
2739
- beliefs.length,
2740
- " BELIEF",
2741
- beliefs.length === 1 ? "" : "S",
2742
- ":"
2743
- ] }),
2744
2983
  beliefs.map((b) => {
2745
2984
  const bl = barLines.find((x) => x.assumptionId === b.assumptionId);
2746
2985
  const a = (assumptions.records ?? []).find((x) => String(x.id) === b.assumptionId);
2986
+ const justification = String(b["Grading justification"] ?? "");
2747
2987
  return /* @__PURE__ */ jsxs7("div", { className: `vos-belief-card vos-verdict-border-${verdictTone2(b.Result)}`, children: [
2748
2988
  /* @__PURE__ */ jsxs7("div", { className: "vos-belief-head", children: [
2749
2989
  /* @__PURE__ */ jsx7(
@@ -2759,7 +2999,12 @@ function ExperimentDetail({
2759
2999
  /* @__PURE__ */ jsx7("span", { className: `vos-pill vos-pill-${verdictTone2(b.Result)}`, children: b.Result }),
2760
3000
  /* @__PURE__ */ jsx7("span", { className: "vos-rung-tag", children: String(r.Rung ?? "") })
2761
3001
  ] }),
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,
2762
3006
  bl ? /* @__PURE__ */ jsxs7("div", { className: "vos-belief-bar", children: [
3007
+ /* @__PURE__ */ jsx7("div", { className: "vos-belief-bar-label", children: "Pre-registered bar" }),
2763
3008
  /* @__PURE__ */ jsxs7("div", { children: [
2764
3009
  /* @__PURE__ */ jsx7("strong", { children: "Right if:" }),
2765
3010
  " ",
@@ -2774,8 +3019,7 @@ function ExperimentDetail({
2774
3019
  "Bar verdict: ",
2775
3020
  /* @__PURE__ */ jsx7("strong", { children: bl.barVerdict })
2776
3021
  ] }) : null
2777
- ] }) : null,
2778
- /* @__PURE__ */ jsx7("div", { className: "vos-belief-why", children: String(b["Grading justification"] ?? "") })
3022
+ ] }) : null
2779
3023
  ] }, b.assumptionId);
2780
3024
  })
2781
3025
  ] })
@@ -2827,10 +3071,6 @@ function ExperimentDetail({
2827
3071
  function pct(n, total) {
2828
3072
  return total === 0 ? 0 : n / total * 100;
2829
3073
  }
2830
- function truncate(s, max) {
2831
- if (s.length <= max) return s;
2832
- return s.slice(0, max - 1) + "\u2026";
2833
- }
2834
3074
  function verdictTone2(verdict) {
2835
3075
  if (verdict === "Validated") return "good";
2836
3076
  if (verdict === "Invalidated") return "crit";
@@ -2982,7 +3222,6 @@ function ReadingDetail({
2982
3222
  const bl = experiment && Array.isArray(experiment.barLines) ? experiment.barLines.find((x) => x?.assumptionId === b.assumptionId) : null;
2983
3223
  const result = String(b.Result ?? "Inconclusive");
2984
3224
  const justification = String(b["Grading justification"] ?? "");
2985
- const excerpt = justification || truncate2(body, 120);
2986
3225
  return /* @__PURE__ */ jsxs9("div", { className: `vos-belief-card vos-verdict-border-${verdictTone3(result)}`, children: [
2987
3226
  /* @__PURE__ */ jsxs9("div", { className: "vos-belief-head", children: [
2988
3227
  /* @__PURE__ */ jsx9(
@@ -2998,11 +3237,11 @@ function ReadingDetail({
2998
3237
  /* @__PURE__ */ jsx9("span", { className: `vos-pill vos-pill-${verdictTone3(result)}`, children: result }),
2999
3238
  /* @__PURE__ */ jsx9("span", { className: "vos-rung-tag", children: rung })
3000
3239
  ] }),
3001
- excerpt ? /* @__PURE__ */ jsxs9("div", { className: `vos-belief-excerpt vos-verdict-border-${verdictTone3(result)}`, children: [
3002
- '"',
3003
- excerpt,
3004
- '"'
3240
+ justification ? /* @__PURE__ */ jsxs9("div", { className: `vos-belief-rationale vos-verdict-border-${verdictTone3(result)}`, children: [
3241
+ /* @__PURE__ */ jsx9("span", { className: "vos-belief-rationale-label", children: "grading rationale:" }),
3242
+ justification
3005
3243
  ] }) : null,
3244
+ body ? /* @__PURE__ */ jsx9("div", { className: "vos-belief-context-link vos-muted", children: "see context above for the full quote" }) : null,
3006
3245
  bl ? /* @__PURE__ */ jsxs9("div", { className: "vos-belief-bar", children: [
3007
3246
  /* @__PURE__ */ jsx9("div", { className: "vos-belief-bar-label", children: "Pre-registered bar" }),
3008
3247
  /* @__PURE__ */ jsxs9("div", { children: [
@@ -3049,10 +3288,6 @@ function verdictTone3(verdict) {
3049
3288
  if (verdict === "Invalidated") return "crit";
3050
3289
  return "neutral";
3051
3290
  }
3052
- function truncate2(s, max) {
3053
- if (s.length <= max) return s;
3054
- return s.slice(0, max - 1) + "\u2026";
3055
- }
3056
3291
 
3057
3292
  // src/readings-surface.tsx
3058
3293
  import { jsx as jsx10, jsxs as jsxs10 } from "react/jsx-runtime";
@@ -6800,15 +7035,6 @@ function ValidationOSDashboard({ config = {} }) {
6800
7035
  brandName
6801
7036
  ] }),
6802
7037
  /* @__PURE__ */ jsxs25("div", { className: "vos-topbar", children: [
6803
- backendLabel ? /* @__PURE__ */ jsxs25("div", { className: "vos-backend", children: [
6804
- /* @__PURE__ */ jsx25("span", { className: "vos-live-dot" }),
6805
- " Backend: ",
6806
- /* @__PURE__ */ jsx25("b", { children: backendLabel })
6807
- ] }) : null,
6808
- agentLabel ? /* @__PURE__ */ jsxs25("span", { className: "vos-hint", children: [
6809
- "Agent: ",
6810
- /* @__PURE__ */ jsx25("b", { style: { color: "var(--vos-text)" }, children: agentLabel })
6811
- ] }) : null,
6812
7038
  /* @__PURE__ */ jsx25("div", { className: "vos-spacer" }),
6813
7039
  /* @__PURE__ */ jsx25("button", { type: "button", className: "vos-iconbtn", onClick: toggleTheme, children: "\u25D0 Theme" }),
6814
7040
  user?.name ? /* @__PURE__ */ jsxs25("div", { className: "vos-user", children: [