@validation-os/dashboard 0.15.6 → 0.16.1

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
@@ -55,6 +55,11 @@ var WORKFLOW_NAV = [
55
55
 
56
56
  // src/assumption-detail.tsx
57
57
  import { useMemo } from "react";
58
+ import {
59
+ QUESTION_TYPES,
60
+ STAGES
61
+ } from "@validation-os/core";
62
+ import { confidenceFloorForStage, isNonEvidence, riskThresholdForStage } from "@validation-os/core/derivation";
58
63
 
59
64
  // src/breadcrumb.tsx
60
65
  import { jsx, jsxs } from "react/jsx-runtime";
@@ -77,7 +82,7 @@ function Breadcrumb({ trail, onNavigate }) {
77
82
  }
78
83
 
79
84
  // src/evidence-composition.ts
80
- import { scoreAndDedupe, w0ForRung } from "@validation-os/core/derivation";
85
+ import { RUNG_ANCHOR, scoreAndDedupe, w0ForRung } from "@validation-os/core/derivation";
81
86
 
82
87
  // src/derived-views.ts
83
88
  var KILL_ZONE = -50;
@@ -156,20 +161,13 @@ var ALL_RUNGS = [
156
161
  "Signed intent",
157
162
  "Paying users"
158
163
  ];
159
- var RUNG_CAPS = {
160
- Talk: 6,
161
- "Desk research": 15,
162
- "Signed up": 50,
163
- "Observed usage": 50,
164
- "Signed intent": 50,
165
- "Paying users": 50
166
- };
167
164
  function rungsForLens(lens) {
168
165
  return LENS_RUNGS[lens] ?? ALL_RUNGS;
169
166
  }
170
167
  function buildEvidenceComposition(assumption, readings) {
171
168
  const id = str(assumption.id) ?? "";
172
169
  const lens = str(assumption.Lens) ?? "";
170
+ const questionType = str(assumption["Question Type"]) ?? "Existence";
173
171
  const ladder = rungsForLens(lens);
174
172
  const inputs = [];
175
173
  for (const r of readings) {
@@ -183,6 +181,7 @@ function buildEvidenceComposition(assumption, readings) {
183
181
  source: str(r.Source) ?? null,
184
182
  rung,
185
183
  result,
184
+ questionType,
186
185
  representativeness: Number(r.Representativeness) || 1,
187
186
  credibility: Number(r.Credibility) || 1,
188
187
  date: str(r.Date),
@@ -208,7 +207,9 @@ function buildEvidenceComposition(assumption, readings) {
208
207
  return {
209
208
  rung,
210
209
  contribution: e ? Math.round((e.contribution + Number.EPSILON) * 100) / 100 : 0,
211
- cap: RUNG_CAPS[rung] ?? 50,
210
+ // DEV-5890: cap is the rung's High anchor in the assumption's
211
+ // question-type sub-ladder — the ceiling (0 for non-evidence rungs).
212
+ cap: RUNG_ANCHOR[questionType]?.[rung]?.High ?? 0,
212
213
  count: e?.count ?? 0
213
214
  };
214
215
  });
@@ -220,6 +221,7 @@ function buildEvidenceComposition(assumption, readings) {
220
221
  }
221
222
  function readingContributions(assumption, readings) {
222
223
  const id = str(assumption.id) ?? "";
224
+ const questionType = str(assumption["Question Type"]) ?? "Existence";
223
225
  const inputs = [];
224
226
  for (const r of readings) {
225
227
  const belief2 = readingBeliefFor(r, id);
@@ -232,6 +234,7 @@ function readingContributions(assumption, readings) {
232
234
  source: str(r.Source) ?? null,
233
235
  rung,
234
236
  result,
237
+ questionType,
235
238
  representativeness: Number(r.Representativeness) || 1,
236
239
  credibility: Number(r.Credibility) || 1,
237
240
  date: str(r.Date),
@@ -248,7 +251,7 @@ function readingContributions(assumption, readings) {
248
251
  return inputs.map((input) => {
249
252
  const w = winners.find((x) => x.input.id === input.id);
250
253
  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);
254
+ const strength = w ? w.strength : (input.result === "Validated" ? 1 : input.result === "Invalidated" ? -1 : 0) * (RUNG_ANCHOR[questionType]?.[input.rung]?.Typical ?? 0);
252
255
  const weight = w ? w.weight : 0;
253
256
  return {
254
257
  id: input.id,
@@ -261,7 +264,7 @@ function readingContributions(assumption, readings) {
261
264
  }
262
265
 
263
266
  // src/confidence-explainer.ts
264
- import { RUNG_ANCHOR, W0_BY_RUNG } from "@validation-os/core/derivation";
267
+ import { RUNG_ANCHOR as RUNG_ANCHOR2, W0_BY_RUNG } from "@validation-os/core/derivation";
265
268
  var RUNG_INFO = {
266
269
  Talk: {
267
270
  label: "Talk",
@@ -277,7 +280,7 @@ var RUNG_INFO = {
277
280
  },
278
281
  "Observed usage": {
279
282
  label: "Observed usage (Consumer)",
280
- description: "Prototype usage, analytics, telemetry. A do-rung \u2014 20 observed users bring this to ~75% of its cap."
283
+ description: "Usage sessions, analytics, telemetry, A/B tests. A do-rung \u2014 20 observed users bring this to ~75% of its cap."
281
284
  },
282
285
  "Signed intent": {
283
286
  label: "Signed intent (Commercial)",
@@ -292,6 +295,7 @@ function buildConfidenceExplainer(assumption, readings) {
292
295
  const comp = buildEvidenceComposition(assumption, readings);
293
296
  const lens = str(assumption.Lens) ?? "";
294
297
  const lensRungs = new Set(comp.rungs.map((r) => r.rung));
298
+ const questionType = str(assumption["Question Type"]) ?? "Existence";
295
299
  const allRungs = [
296
300
  "Talk",
297
301
  "Desk research",
@@ -306,7 +310,7 @@ function buildConfidenceExplainer(assumption, readings) {
306
310
  return {
307
311
  rung,
308
312
  w0: W0_BY_RUNG[rung] ?? 100,
309
- anchors: RUNG_ANCHOR[rung] ?? { Low: 0, Typical: 0, High: 0 },
313
+ anchors: RUNG_ANCHOR2[questionType]?.[rung] ?? { Low: 0, Typical: 0, High: 0 },
310
314
  contribution: e?.contribution ?? 0,
311
315
  count: e?.count ?? 0,
312
316
  inLens: lensRungs.has(rung),
@@ -1506,11 +1510,16 @@ function AssumptionDetail({
1506
1510
  const page = buildRecordPage("assumptions", record, related);
1507
1511
  const lens = String(record.Lens ?? "\u2014");
1508
1512
  const stage = String(record.Stage ?? "\u2014");
1513
+ const questionType = String(record["Question Type"] ?? "\u2014");
1509
1514
  const derived = record.derived ?? {};
1510
1515
  const confidence2 = derived.confidence ?? 0;
1511
1516
  const risk = derived.risk ?? 0;
1512
1517
  const impact = derived.derivedImpact ?? 0;
1513
1518
  const framed = derived.completeness ?? 0;
1519
+ const stageKey = STAGES.find((s) => s === stage);
1520
+ const riskThreshold = stageKey ? riskThresholdForStage(stageKey) : null;
1521
+ const confFloor = stageKey ? confidenceFloorForStage(stageKey) : null;
1522
+ const clearedThreshold = riskThreshold != null && confFloor != null ? risk <= riskThreshold && confidence2 >= confFloor : null;
1514
1523
  const nextMove2 = nextMoveFor(record, experiments.records ?? []);
1515
1524
  const linkedExperiments = liveExperiments(experiments.records ?? []).filter((e) => {
1516
1525
  const ids = Array.isArray(e.barLineAssumptionIds) ? e.barLineAssumptionIds : [];
@@ -1538,7 +1547,8 @@ function AssumptionDetail({
1538
1547
  /* @__PURE__ */ jsxs4("div", { className: "vos-detail-head", children: [
1539
1548
  /* @__PURE__ */ jsx4("span", { className: "vos-detail-id vos-num", children: assumptionId }),
1540
1549
  /* @__PURE__ */ jsx4("span", { className: "vos-detail-tag", children: lens }),
1541
- /* @__PURE__ */ jsx4("span", { className: "vos-detail-tag", children: stage })
1550
+ /* @__PURE__ */ jsx4("span", { className: "vos-detail-tag", children: stage }),
1551
+ /* @__PURE__ */ jsx4("span", { className: "vos-detail-tag vos-detail-tag-qt", children: questionType })
1542
1552
  ] }),
1543
1553
  /* @__PURE__ */ jsx4("div", { className: "vos-detail-title", children: statement }),
1544
1554
  /* @__PURE__ */ jsxs4("div", { className: "vos-card vos-next-move", children: [
@@ -1551,6 +1561,17 @@ function AssumptionDetail({
1551
1561
  /* @__PURE__ */ jsx4(ScoreCard, { label: "Confidence", value: formatSigned(confidence2) }),
1552
1562
  /* @__PURE__ */ jsx4(ScoreCard, { label: "Framed", value: `${Math.round(framed)}%` })
1553
1563
  ] }),
1564
+ riskThreshold != null ? /* @__PURE__ */ jsx4(
1565
+ ThresholdBar,
1566
+ {
1567
+ risk,
1568
+ threshold: riskThreshold,
1569
+ confidence: confidence2,
1570
+ confFloor: confFloor ?? 0,
1571
+ stage,
1572
+ cleared: clearedThreshold ?? false
1573
+ }
1574
+ ) : null,
1554
1575
  /* @__PURE__ */ jsx4(EvidenceCompositionView, { assumption: record, readings: readings.records ?? [] }),
1555
1576
  /* @__PURE__ */ jsx4(ConfidenceExplainerView, { assumption: record, readings: readings.records ?? [] }),
1556
1577
  statement ? /* @__PURE__ */ jsxs4("div", { className: "vos-card vos-detail-section", children: [
@@ -1615,6 +1636,7 @@ function AssumptionDetail({
1615
1636
  EvidenceList,
1616
1637
  {
1617
1638
  assumptionId,
1639
+ questionType,
1618
1640
  readings: readings.records ?? [],
1619
1641
  onNavigate
1620
1642
  }
@@ -1623,6 +1645,7 @@ function AssumptionDetail({
1623
1645
  }
1624
1646
  function EvidenceList({
1625
1647
  assumptionId,
1648
+ questionType,
1626
1649
  readings,
1627
1650
  onNavigate
1628
1651
  }) {
@@ -1633,7 +1656,81 @@ function EvidenceList({
1633
1656
  const rows = readingContributions(rec, readings);
1634
1657
  return new Map(rows.map((r) => [r.id, r]));
1635
1658
  }, [readings, assumptionId]);
1659
+ const qt = QUESTION_TYPES.find((q) => q === questionType);
1660
+ const { probative, flagged } = useMemo(() => {
1661
+ const prob = [];
1662
+ const flag = [];
1663
+ for (const r of linkedReadings) {
1664
+ const rung = String(r.Rung ?? "");
1665
+ if (qt && RUNGS.includes(rung) && isNonEvidence(qt, rung)) {
1666
+ flag.push(r);
1667
+ } else {
1668
+ prob.push(r);
1669
+ }
1670
+ }
1671
+ return { probative: prob, flagged: flag };
1672
+ }, [linkedReadings, qt]);
1636
1673
  if (linkedReadings.length === 0) return null;
1674
+ const renderRow = (r, flagged2) => {
1675
+ const id = String(r.id ?? "");
1676
+ const belief2 = readingBeliefFor(r, assumptionId);
1677
+ if (!belief2) return null;
1678
+ const result = String(belief2.Result ?? "Inconclusive");
1679
+ const justification = String(belief2["Grading justification"] ?? "");
1680
+ const excerpt = typeof belief2.excerpt === "string" && belief2.excerpt !== "" ? belief2.excerpt : snippetFromBody2(String(r.body ?? ""), assumptionId);
1681
+ const rung = String(r.Rung ?? "");
1682
+ const source = String(r.Source ?? "");
1683
+ const expId = r.experimentId ? String(r.experimentId) : null;
1684
+ const c = contribById.get(id);
1685
+ return /* @__PURE__ */ jsxs4(
1686
+ "button",
1687
+ {
1688
+ type: "button",
1689
+ className: `vos-evidence-row vos-verdict-${verdictTone(result)}${flagged2 ? " vos-evidence-row-flagged" : ""}`,
1690
+ onClick: () => onNavigate({ name: "reading", id }),
1691
+ children: [
1692
+ /* @__PURE__ */ jsxs4("div", { className: "vos-evidence-row-head", children: [
1693
+ /* @__PURE__ */ jsx4("span", { className: "vos-evidence-date vos-num", children: String(r.Date ?? "") }),
1694
+ /* @__PURE__ */ jsx4("span", { className: "vos-evidence-title", children: String(r.Title ?? id) }),
1695
+ /* @__PURE__ */ jsx4("span", { className: `vos-pill vos-pill-${verdictTone(result)}`, children: result }),
1696
+ /* @__PURE__ */ jsx4("span", { className: "vos-rung-tag", children: rung }),
1697
+ flagged2 ? /* @__PURE__ */ jsx4("span", { className: "vos-evidence-flag", title: "This rung is non-evidence for this assumption's question type \u2014 reclassify the assumption or drop the reading.", children: "non-evidence" }) : null,
1698
+ c && c.used ? /* @__PURE__ */ jsxs4("span", { className: `vos-evidence-score vos-num vos-text-${contributionTone(c.contribution)}`, children: [
1699
+ formatSigned(c.contribution),
1700
+ " confidence"
1701
+ ] }) : null
1702
+ ] }),
1703
+ excerpt ? /* @__PURE__ */ jsxs4("div", { className: "vos-evidence-excerpt", children: [
1704
+ "\u201C",
1705
+ excerpt,
1706
+ "\u201D"
1707
+ ] }) : null,
1708
+ justification ? /* @__PURE__ */ jsxs4("div", { className: `vos-belief-rationale vos-verdict-border-${verdictTone(result)}`, children: [
1709
+ /* @__PURE__ */ jsx4("span", { className: "vos-belief-rationale-label", children: "grading rationale:" }),
1710
+ justification
1711
+ ] }) : null,
1712
+ /* @__PURE__ */ jsxs4("div", { className: "vos-evidence-source", children: [
1713
+ /* @__PURE__ */ jsx4("span", { className: "vos-evidence-source-link", title: source, children: source }),
1714
+ expId ? /* @__PURE__ */ jsxs4(Fragment3, { children: [
1715
+ " \xB7 from experiment ",
1716
+ /* @__PURE__ */ jsx4(
1717
+ "span",
1718
+ {
1719
+ className: "vos-link",
1720
+ onClick: (ev) => {
1721
+ ev.stopPropagation();
1722
+ onNavigate({ name: "experiment", id: expId });
1723
+ },
1724
+ children: expId
1725
+ }
1726
+ )
1727
+ ] }) : null
1728
+ ] })
1729
+ ]
1730
+ },
1731
+ id
1732
+ );
1733
+ };
1637
1734
  return /* @__PURE__ */ jsxs4("div", { className: "vos-card vos-detail-section", children: [
1638
1735
  /* @__PURE__ */ jsxs4("div", { className: "vos-detail-section-label", children: [
1639
1736
  "Evidence \xB7 ",
@@ -1642,65 +1739,20 @@ function EvidenceList({
1642
1739
  linkedReadings.length === 1 ? "" : "s",
1643
1740
  " of evidence"
1644
1741
  ] }),
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
- })
1742
+ probative.length > 0 ? /* @__PURE__ */ jsxs4("div", { className: "vos-evidence-group vos-evidence-group-probative", children: [
1743
+ /* @__PURE__ */ jsxs4("div", { className: "vos-evidence-group-label", children: [
1744
+ "Probative evidence \xB7 ",
1745
+ probative.length
1746
+ ] }),
1747
+ probative.map((r) => renderRow(r, false))
1748
+ ] }) : null,
1749
+ flagged.length > 0 ? /* @__PURE__ */ jsxs4("div", { className: "vos-evidence-group vos-evidence-group-flagged", children: [
1750
+ /* @__PURE__ */ jsxs4("div", { className: "vos-evidence-group-label", children: [
1751
+ "Flagged as non-evidence for this question type \xB7 ",
1752
+ flagged.length
1753
+ ] }),
1754
+ flagged.map((r) => renderRow(r, true))
1755
+ ] }) : null
1704
1756
  ] });
1705
1757
  }
1706
1758
  function contributionTone(v) {
@@ -1708,6 +1760,14 @@ function contributionTone(v) {
1708
1760
  if (v < 0) return "crit";
1709
1761
  return "neutral";
1710
1762
  }
1763
+ var RUNGS = [
1764
+ "Talk",
1765
+ "Desk research",
1766
+ "Signed up",
1767
+ "Observed usage",
1768
+ "Signed intent",
1769
+ "Paying users"
1770
+ ];
1711
1771
  function snippetFromBody2(body, cue) {
1712
1772
  if (!body) return "";
1713
1773
  const quoteMatch = body.match(/## Quote\n+([\s\S]*?)(?=\n## |\n##$|$)/i);
@@ -1734,6 +1794,88 @@ function ScoreCard({
1734
1794
  /* @__PURE__ */ jsx4("div", { className: "vos-score-label", children: label })
1735
1795
  ] });
1736
1796
  }
1797
+ function ThresholdBar({
1798
+ risk,
1799
+ threshold,
1800
+ confidence: confidence2,
1801
+ confFloor,
1802
+ stage,
1803
+ cleared
1804
+ }) {
1805
+ const riskTrackMax = Math.max(risk, threshold) * 1.25;
1806
+ const riskPct = Math.min(100, risk / riskTrackMax * 100);
1807
+ const thresholdPct = Math.min(100, threshold / riskTrackMax * 100);
1808
+ const confPct = Math.min(100, Math.abs(confidence2));
1809
+ const confFloorPct = Math.min(100, confFloor);
1810
+ const riskBelow = risk <= threshold;
1811
+ const confAbove = confidence2 >= confFloor;
1812
+ const tone = cleared ? "good" : !riskBelow && !confAbove ? "crit" : "warn";
1813
+ return /* @__PURE__ */ jsxs4("div", { className: "vos-card vos-threshold-bar-card", children: [
1814
+ /* @__PURE__ */ jsxs4("div", { className: "vos-threshold-bar-header", children: [
1815
+ /* @__PURE__ */ jsxs4("span", { className: "vos-threshold-bar-title", children: [
1816
+ stage,
1817
+ " evidence bar",
1818
+ /* @__PURE__ */ jsx4("span", { className: "vos-threshold-bar-stage-tag", children: stage })
1819
+ ] }),
1820
+ /* @__PURE__ */ jsx4("span", { className: `vos-threshold-bar-status vos-threshold-bar-${tone}`, children: cleared ? "Cleared" : "Needs evidence" })
1821
+ ] }),
1822
+ /* @__PURE__ */ jsxs4("div", { className: "vos-threshold-bar-section", children: [
1823
+ /* @__PURE__ */ jsxs4("div", { className: "vos-threshold-bar-section-label", children: [
1824
+ /* @__PURE__ */ jsx4("span", { children: "Risk" }),
1825
+ /* @__PURE__ */ jsx4("span", { className: "vos-num vos-threshold-bar-section-val", children: Math.round(risk) }),
1826
+ /* @__PURE__ */ jsxs4("span", { className: "vos-threshold-bar-section-target", children: [
1827
+ "bar \u2264 ",
1828
+ threshold
1829
+ ] })
1830
+ ] }),
1831
+ /* @__PURE__ */ jsxs4("div", { className: "vos-threshold-bar-track", children: [
1832
+ /* @__PURE__ */ jsx4(
1833
+ "div",
1834
+ {
1835
+ className: `vos-threshold-bar-fill vos-fill-${riskBelow ? "good" : tone}`,
1836
+ style: { width: `${riskPct}%` }
1837
+ }
1838
+ ),
1839
+ /* @__PURE__ */ jsx4(
1840
+ "div",
1841
+ {
1842
+ className: "vos-threshold-bar-marker",
1843
+ style: { left: `${thresholdPct}%` },
1844
+ title: `Risk threshold: ${threshold}`
1845
+ }
1846
+ )
1847
+ ] })
1848
+ ] }),
1849
+ /* @__PURE__ */ jsxs4("div", { className: "vos-threshold-bar-section", children: [
1850
+ /* @__PURE__ */ jsxs4("div", { className: "vos-threshold-bar-section-label", children: [
1851
+ /* @__PURE__ */ jsx4("span", { children: "Confidence" }),
1852
+ /* @__PURE__ */ jsx4("span", { className: "vos-num vos-threshold-bar-section-val", children: formatSigned(confidence2) }),
1853
+ /* @__PURE__ */ jsxs4("span", { className: "vos-threshold-bar-section-target", children: [
1854
+ "floor \u2265 ",
1855
+ confFloor
1856
+ ] })
1857
+ ] }),
1858
+ /* @__PURE__ */ jsxs4("div", { className: "vos-threshold-bar-track vos-threshold-bar-track-conf", children: [
1859
+ /* @__PURE__ */ jsx4(
1860
+ "div",
1861
+ {
1862
+ className: `vos-threshold-bar-fill vos-fill-${confAbove ? "good" : tone}`,
1863
+ style: { width: `${confPct}%` }
1864
+ }
1865
+ ),
1866
+ /* @__PURE__ */ jsx4(
1867
+ "div",
1868
+ {
1869
+ className: "vos-threshold-bar-marker vos-threshold-bar-marker-conf",
1870
+ style: { left: `${confFloorPct}%` },
1871
+ title: `Confidence floor: ${confFloor}`
1872
+ }
1873
+ )
1874
+ ] })
1875
+ ] }),
1876
+ /* @__PURE__ */ jsx4("div", { className: "vos-threshold-bar-hint", children: cleared ? `Risk ${Math.round(risk)} \u2264 ${threshold} and Confidence ${formatSigned(confidence2)} \u2265 ${confFloor} \u2014 cleared for ${stage}.` : `Risk ${Math.round(risk)} ${riskBelow ? "\u2264" : ">"} ${threshold}, Confidence ${formatSigned(confidence2)} ${confAbove ? "\u2265" : "<"} ${confFloor} \u2014 needs ${!riskBelow ? "lower Risk" : ""}${!riskBelow && !confAbove ? " + " : ""}${!confAbove ? "higher Confidence" : ""}.` })
1877
+ ] });
1878
+ }
1737
1879
  function riskTone(risk) {
1738
1880
  if (risk >= 70) return "crit";
1739
1881
  if (risk >= 40) return "warn";
@@ -2068,6 +2210,114 @@ import {
2068
2210
  assumptionCompleteness,
2069
2211
  readingBeliefInputs
2070
2212
  } from "@validation-os/core";
2213
+ import { confidenceFloorForStage as confidenceFloorForStage2, riskThresholdForStage as riskThresholdForStage2 } from "@validation-os/core/derivation";
2214
+
2215
+ // src/stage-grid-model.ts
2216
+ var STAGE_ORDER = [
2217
+ "Discovery",
2218
+ "Validation",
2219
+ "Scale",
2220
+ "Maturity"
2221
+ ];
2222
+ var STAGE_GLOSS = {
2223
+ Discovery: "Problem-solution fit \u2014 will they engage, care, disclose?",
2224
+ Validation: "Product-market fit \u2014 will they pay, sign, stay?",
2225
+ Scale: "Growth \u2014 can we acquire efficiently, does CAC<LTV hold at volume?",
2226
+ Maturity: "Defense \u2014 will incumbents respond, will regulators accept?"
2227
+ };
2228
+ var NO_LENS = "\u2014";
2229
+ var NO_STAGE = "\u2014";
2230
+ function stageOf(record) {
2231
+ const v = str(record.Stage);
2232
+ if (!v) return null;
2233
+ return STAGE_ORDER.includes(v) ? v : null;
2234
+ }
2235
+ function rankByRisk(records) {
2236
+ return [...records].map((r) => ({ r, risk: derivedNum(r, "risk") ?? 0 })).sort(
2237
+ (a, b) => a.risk !== b.risk ? b.risk - a.risk : a.r.id.localeCompare(b.r.id)
2238
+ ).map((x) => x.r);
2239
+ }
2240
+ function buildStageGrid(assumptions) {
2241
+ const lensOrder = [];
2242
+ const seenLens = /* @__PURE__ */ new Set();
2243
+ const pushLens = (lens) => {
2244
+ if (!seenLens.has(lens)) {
2245
+ seenLens.add(lens);
2246
+ lensOrder.push(lens);
2247
+ }
2248
+ };
2249
+ const buckets = /* @__PURE__ */ new Map();
2250
+ const ensureLens = (lens) => {
2251
+ let m = buckets.get(lens);
2252
+ if (!m) {
2253
+ m = /* @__PURE__ */ new Map();
2254
+ buckets.set(lens, m);
2255
+ }
2256
+ return m;
2257
+ };
2258
+ let hasNoLens = false;
2259
+ let hasNoStage = false;
2260
+ for (const a of assumptions) {
2261
+ const lens = str(a.Lens) ?? NO_LENS;
2262
+ const stage = stageOf(a) ?? NO_STAGE;
2263
+ if (lens === NO_LENS) hasNoLens = true;
2264
+ if (stage === NO_STAGE) hasNoStage = true;
2265
+ pushLens(lens);
2266
+ const byStage = ensureLens(lens);
2267
+ let list = byStage.get(stage);
2268
+ if (!list) {
2269
+ list = [];
2270
+ byStage.set(stage, list);
2271
+ }
2272
+ list.push(a);
2273
+ }
2274
+ if (hasNoLens) {
2275
+ lensOrder.splice(lensOrder.indexOf(NO_LENS), 1);
2276
+ lensOrder.push(NO_LENS);
2277
+ } else {
2278
+ const idx = lensOrder.indexOf(NO_LENS);
2279
+ if (idx >= 0) lensOrder.splice(idx, 1);
2280
+ }
2281
+ const stageOrder = [...STAGE_ORDER];
2282
+ if (hasNoStage) stageOrder.push(NO_STAGE);
2283
+ const cells = [];
2284
+ let maxCellCount = 0;
2285
+ let total = 0;
2286
+ for (const lens of lensOrder) {
2287
+ const byStage = buckets.get(lens) ?? /* @__PURE__ */ new Map();
2288
+ for (const stage of stageOrder) {
2289
+ const records = byStage.get(stage) ?? [];
2290
+ const ranked = rankByRisk(records);
2291
+ const count = ranked.length;
2292
+ maxCellCount = Math.max(maxCellCount, count);
2293
+ total += count;
2294
+ cells.push({
2295
+ lens,
2296
+ stage,
2297
+ count,
2298
+ assumptions: ranked,
2299
+ // density filled in a second pass once maxCellCount is known.
2300
+ density: 0
2301
+ });
2302
+ }
2303
+ }
2304
+ const norm2 = maxCellCount > 0 ? 1 / maxCellCount : 0;
2305
+ for (const cell of cells) {
2306
+ cell.density = cell.count * norm2;
2307
+ }
2308
+ return {
2309
+ lenses: lensOrder,
2310
+ stages: [...STAGE_ORDER],
2311
+ cells,
2312
+ maxCellCount,
2313
+ total
2314
+ };
2315
+ }
2316
+ function cellAt(view, lens, stage) {
2317
+ return view.cells.find((c) => c.lens === lens && c.stage === stage) ?? null;
2318
+ }
2319
+
2320
+ // src/pipeline.ts
2071
2321
  import {
2072
2322
  beliefRisk,
2073
2323
  beliefTestMeters,
@@ -2156,6 +2406,11 @@ function buildPipeline(assumptions, experiments) {
2156
2406
  };
2157
2407
  const framed = assumptionCompleteness(a);
2158
2408
  const stage = deriveBeliefStage({ framed, confidence: d.confidence, test });
2409
+ const questionType = str3(a["Question Type"]);
2410
+ const stageName = str3(a.Stage);
2411
+ const stageKey = stageName && STAGE_ORDER.includes(stageName) ? stageName : null;
2412
+ const riskThreshold = stageKey ? riskThresholdForStage2(stageKey) : null;
2413
+ const confFloor = stageKey ? confidenceFloorForStage2(stageKey) : null;
2159
2414
  rows.push({
2160
2415
  id: a.id,
2161
2416
  statement: str3(a.Title),
@@ -2168,7 +2423,11 @@ function buildPipeline(assumptions, experiments) {
2168
2423
  framed: stage.framed,
2169
2424
  planned: stage.planned,
2170
2425
  tested: stage.tested,
2171
- nextMove: nextMove(stage.stage, stage.killZone)
2426
+ nextMove: nextMove(stage.stage, stage.killZone),
2427
+ questionType,
2428
+ stage: stageName,
2429
+ riskThreshold,
2430
+ clearedThreshold: riskThreshold != null ? d.risk <= riskThreshold && (confFloor == null || stage.confidence >= confFloor) : null
2172
2431
  });
2173
2432
  }
2174
2433
  rows.sort(
@@ -2188,8 +2447,13 @@ function weekOverWeekDelta(assumptions, readings, now) {
2188
2447
  });
2189
2448
  if (!anyDated) return null;
2190
2449
  const cutoff = now.getTime() - 7 * 24 * 60 * 60 * 1e3;
2450
+ const assumptionsById = new Map(
2451
+ assumptions.map((a) => [String(a.id), a])
2452
+ );
2191
2453
  const byAssumption = /* @__PURE__ */ new Map();
2192
- for (const input of readings.flatMap(readingBeliefInputs)) {
2454
+ for (const input of readings.flatMap(
2455
+ (r) => readingBeliefInputs(r, assumptionsById)
2456
+ )) {
2193
2457
  if (!input.assumptionId) continue;
2194
2458
  const arr = byAssumption.get(input.assumptionId);
2195
2459
  if (arr) arr.push(input);
@@ -2408,111 +2672,6 @@ function stageMeters(input) {
2408
2672
  ];
2409
2673
  }
2410
2674
 
2411
- // src/stage-grid-model.ts
2412
- var STAGE_ORDER = [
2413
- "Discovery",
2414
- "Validation",
2415
- "Scale",
2416
- "Maturity"
2417
- ];
2418
- var STAGE_GLOSS = {
2419
- Discovery: "Problem-solution fit \u2014 will they engage, care, disclose?",
2420
- Validation: "Product-market fit \u2014 will they pay, sign, stay?",
2421
- Scale: "Growth \u2014 can we acquire efficiently, does CAC<LTV hold at volume?",
2422
- Maturity: "Defense \u2014 will incumbents respond, will regulators accept?"
2423
- };
2424
- var NO_LENS = "\u2014";
2425
- var NO_STAGE = "\u2014";
2426
- function stageOf(record) {
2427
- const v = str(record.Stage);
2428
- if (!v) return null;
2429
- return STAGE_ORDER.includes(v) ? v : null;
2430
- }
2431
- function rankByRisk(records) {
2432
- return [...records].map((r) => ({ r, risk: derivedNum(r, "risk") ?? 0 })).sort(
2433
- (a, b) => a.risk !== b.risk ? b.risk - a.risk : a.r.id.localeCompare(b.r.id)
2434
- ).map((x) => x.r);
2435
- }
2436
- function buildStageGrid(assumptions) {
2437
- const lensOrder = [];
2438
- const seenLens = /* @__PURE__ */ new Set();
2439
- const pushLens = (lens) => {
2440
- if (!seenLens.has(lens)) {
2441
- seenLens.add(lens);
2442
- lensOrder.push(lens);
2443
- }
2444
- };
2445
- const buckets = /* @__PURE__ */ new Map();
2446
- const ensureLens = (lens) => {
2447
- let m = buckets.get(lens);
2448
- if (!m) {
2449
- m = /* @__PURE__ */ new Map();
2450
- buckets.set(lens, m);
2451
- }
2452
- return m;
2453
- };
2454
- let hasNoLens = false;
2455
- let hasNoStage = false;
2456
- for (const a of assumptions) {
2457
- const lens = str(a.Lens) ?? NO_LENS;
2458
- const stage = stageOf(a) ?? NO_STAGE;
2459
- if (lens === NO_LENS) hasNoLens = true;
2460
- if (stage === NO_STAGE) hasNoStage = true;
2461
- pushLens(lens);
2462
- const byStage = ensureLens(lens);
2463
- let list = byStage.get(stage);
2464
- if (!list) {
2465
- list = [];
2466
- byStage.set(stage, list);
2467
- }
2468
- list.push(a);
2469
- }
2470
- if (hasNoLens) {
2471
- lensOrder.splice(lensOrder.indexOf(NO_LENS), 1);
2472
- lensOrder.push(NO_LENS);
2473
- } else {
2474
- const idx = lensOrder.indexOf(NO_LENS);
2475
- if (idx >= 0) lensOrder.splice(idx, 1);
2476
- }
2477
- const stageOrder = [...STAGE_ORDER];
2478
- if (hasNoStage) stageOrder.push(NO_STAGE);
2479
- const cells = [];
2480
- let maxCellCount = 0;
2481
- let total = 0;
2482
- for (const lens of lensOrder) {
2483
- const byStage = buckets.get(lens) ?? /* @__PURE__ */ new Map();
2484
- for (const stage of stageOrder) {
2485
- const records = byStage.get(stage) ?? [];
2486
- const ranked = rankByRisk(records);
2487
- const count = ranked.length;
2488
- maxCellCount = Math.max(maxCellCount, count);
2489
- total += count;
2490
- cells.push({
2491
- lens,
2492
- stage,
2493
- count,
2494
- assumptions: ranked,
2495
- // density filled in a second pass once maxCellCount is known.
2496
- density: 0
2497
- });
2498
- }
2499
- }
2500
- const norm2 = maxCellCount > 0 ? 1 / maxCellCount : 0;
2501
- for (const cell of cells) {
2502
- cell.density = cell.count * norm2;
2503
- }
2504
- return {
2505
- lenses: lensOrder,
2506
- stages: [...STAGE_ORDER],
2507
- cells,
2508
- maxCellCount,
2509
- total
2510
- };
2511
- }
2512
- function cellAt(view, lens, stage) {
2513
- return view.cells.find((c) => c.lens === lens && c.stage === stage) ?? null;
2514
- }
2515
-
2516
2675
  // src/assumptions-surface.tsx
2517
2676
  import { Fragment as Fragment4, jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
2518
2677
  function AssumptionsSurface({
@@ -2779,7 +2938,17 @@ function PipelineRowView({ row, onOpen }) {
2779
2938
  /* @__PURE__ */ jsxs5("span", { className: "vos-num", children: [
2780
2939
  "conf ",
2781
2940
  formatSigned(row.confidence)
2782
- ] })
2941
+ ] }),
2942
+ row.questionType ? /* @__PURE__ */ jsx5("span", { className: "vos-pipe-tag vos-pipe-tag-qt", children: row.questionType }) : null,
2943
+ row.stage ? /* @__PURE__ */ jsx5("span", { className: "vos-pipe-tag vos-pipe-tag-stage", children: row.stage }) : null,
2944
+ row.riskThreshold != null ? /* @__PURE__ */ jsx5(
2945
+ "span",
2946
+ {
2947
+ className: `vos-pipe-tag vos-pipe-tag-thresh ${row.clearedThreshold ? "vos-pipe-tag-cleared" : "vos-pipe-tag-needs"}`,
2948
+ title: `Stage threshold ${row.riskThreshold} \u2014 ${row.clearedThreshold ? "cleared for this stage" : "needs evidence"}`,
2949
+ children: row.clearedThreshold ? "cleared" : `bar ${row.riskThreshold}`
2950
+ }
2951
+ ) : null
2783
2952
  ] })
2784
2953
  ] }),
2785
2954
  /* @__PURE__ */ jsx5("div", { className: "vos-pipe-prog-2seg", "aria-label": "Evidence progress (Framed + Known)", children: /* @__PURE__ */ jsx5(Meter2Seg, { framed: row.framed, knownPct, knownSign }) }),
@@ -3814,9 +3983,9 @@ function barVerdictFor(exp, assumptionId) {
3814
3983
  const line = bars.find((b) => b.assumptionId === assumptionId);
3815
3984
  return line?.barVerdict ?? null;
3816
3985
  }
3817
- function buildCycles(assumptionId, readings, experiments) {
3986
+ function buildCycles(assumptionId, readings, experiments, assumptionsById) {
3818
3987
  const mine = readings.filter((r) => readingGrades(r, assumptionId));
3819
- const inputs = readings.flatMap(readingBeliefInputs2).filter((i) => i.assumptionId === assumptionId);
3988
+ const inputs = readings.flatMap((r) => readingBeliefInputs2(r, assumptionsById)).filter((i) => i.assumptionId === assumptionId);
3820
3989
  const { movers } = confidenceAttribution(inputs);
3821
3990
  const moverByKey = new Map(movers.map((m) => [m.key, m]));
3822
3991
  const byExperiment = /* @__PURE__ */ new Map();
@@ -3940,7 +4109,10 @@ function buildJourney(assumptionId, records, now) {
3940
4109
  const test = beliefTestMeters2(live.map(toStageExperimentInput)).get(assumptionId) ?? emptyTestMeter();
3941
4110
  const framed = assumptionCompleteness2(belief2);
3942
4111
  const stage = deriveBeliefStage2({ framed, confidence: confidence2, test });
3943
- const myReadingInputs = records.readings.flatMap(readingBeliefInputs3).filter((i) => i.assumptionId === assumptionId);
4112
+ const assumptionsById = new Map(
4113
+ records.assumptions.map((a) => [String(a.id), a])
4114
+ );
4115
+ const myReadingInputs = records.readings.flatMap((r) => readingBeliefInputs3(r, assumptionsById)).filter((i) => i.assumptionId === assumptionId);
3944
4116
  const myExperiments = live.filter((e) => testsAssumption(e, assumptionId)).map((e) => ({ id: e.id, date: str4(e.Date) || str4(e.createdAt) || null }));
3945
4117
  const events = assembleJourney({
3946
4118
  belief: {
@@ -3954,7 +4126,12 @@ function buildJourney(assumptionId, records, now) {
3954
4126
  const nextMove2 = rankNextMoves(toNextMoveInput(records)).find(
3955
4127
  (m) => m.assumptionId === assumptionId
3956
4128
  ) ?? null;
3957
- const cycles = buildCycles(assumptionId, records.readings, records.experiments);
4129
+ const cycles = buildCycles(
4130
+ assumptionId,
4131
+ records.readings,
4132
+ records.experiments,
4133
+ assumptionsById
4134
+ );
3958
4135
  return {
3959
4136
  id: assumptionId,
3960
4137
  statement: str4(belief2.Title),
@@ -4543,7 +4720,8 @@ import {
4543
4720
  isConcluded as isConcluded2
4544
4721
  } from "@validation-os/core/derivation";
4545
4722
  function buildUnderstanding(assumption, readings, experiments) {
4546
- const inputs = readings.flatMap(readingBeliefInputs4).filter((i) => i.assumptionId === assumption.id);
4723
+ const assumptionsById = /* @__PURE__ */ new Map([[String(assumption.id), assumption]]);
4724
+ const inputs = readings.flatMap((r) => readingBeliefInputs4(r, assumptionsById)).filter((i) => i.assumptionId === assumption.id);
4547
4725
  const { confidence: confidence2, movers } = confidenceAttribution2(inputs);
4548
4726
  const experimentsById = new Map(experiments.map((e) => [e.id, e]));
4549
4727
  const moverByExperiment = new Map(
@@ -7820,7 +7998,7 @@ function RegisterCounts({
7820
7998
  import { useMemo as useMemo10, useState as useState17 } from "react";
7821
7999
  import { rankNextMoves as rankNextMoves2 } from "@validation-os/core/derivation";
7822
8000
  import { Fragment as Fragment12, jsx as jsx30, jsxs as jsxs30 } from "react/jsx-runtime";
7823
- var STAGES = ["Framed", "Planned", "Tested", "Known"];
8001
+ var STAGES2 = ["Framed", "Planned", "Tested", "Known"];
7824
8002
  var MOVE_STAGE = {
7825
8003
  "score-impact": 0,
7826
8004
  "design-experiment": 1,
@@ -8088,7 +8266,7 @@ function WhyPanel({ top, ranked }) {
8088
8266
  }
8089
8267
  function StageStepper({ move }) {
8090
8268
  const at = MOVE_STAGE[move];
8091
- return /* @__PURE__ */ jsx30("div", { className: "vos-stepper", "aria-label": `Stage: ${STAGES[at]}`, children: STAGES.map((label, i) => /* @__PURE__ */ jsx30(
8269
+ return /* @__PURE__ */ jsx30("div", { className: "vos-stepper", "aria-label": `Stage: ${STAGES2[at]}`, children: STAGES2.map((label, i) => /* @__PURE__ */ jsx30(
8092
8270
  "span",
8093
8271
  {
8094
8272
  className: `vos-step${i === at ? " vos-step-at" : ""}${i < at ? " vos-step-done" : ""}`,