@validation-os/dashboard 0.15.5 → 0.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.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 { 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,15 @@ 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 clearedThreshold = riskThreshold != null ? risk <= riskThreshold : null;
1514
1522
  const nextMove2 = nextMoveFor(record, experiments.records ?? []);
1515
1523
  const linkedExperiments = liveExperiments(experiments.records ?? []).filter((e) => {
1516
1524
  const ids = Array.isArray(e.barLineAssumptionIds) ? e.barLineAssumptionIds : [];
@@ -1538,7 +1546,8 @@ function AssumptionDetail({
1538
1546
  /* @__PURE__ */ jsxs4("div", { className: "vos-detail-head", children: [
1539
1547
  /* @__PURE__ */ jsx4("span", { className: "vos-detail-id vos-num", children: assumptionId }),
1540
1548
  /* @__PURE__ */ jsx4("span", { className: "vos-detail-tag", children: lens }),
1541
- /* @__PURE__ */ jsx4("span", { className: "vos-detail-tag", children: stage })
1549
+ /* @__PURE__ */ jsx4("span", { className: "vos-detail-tag", children: stage }),
1550
+ /* @__PURE__ */ jsx4("span", { className: "vos-detail-tag vos-detail-tag-qt", children: questionType })
1542
1551
  ] }),
1543
1552
  /* @__PURE__ */ jsx4("div", { className: "vos-detail-title", children: statement }),
1544
1553
  /* @__PURE__ */ jsxs4("div", { className: "vos-card vos-next-move", children: [
@@ -1551,6 +1560,24 @@ function AssumptionDetail({
1551
1560
  /* @__PURE__ */ jsx4(ScoreCard, { label: "Confidence", value: formatSigned(confidence2) }),
1552
1561
  /* @__PURE__ */ jsx4(ScoreCard, { label: "Framed", value: `${Math.round(framed)}%` })
1553
1562
  ] }),
1563
+ riskThreshold != null ? /* @__PURE__ */ jsxs4("div", { className: "vos-card vos-threshold-indicator", children: [
1564
+ /* @__PURE__ */ jsx4("div", { className: "vos-threshold-label", children: "Stage threshold" }),
1565
+ /* @__PURE__ */ jsxs4("div", { className: "vos-threshold-row", children: [
1566
+ /* @__PURE__ */ jsxs4("span", { className: "vos-num", children: [
1567
+ "Risk ",
1568
+ Math.round(risk)
1569
+ ] }),
1570
+ /* @__PURE__ */ jsx4("span", { className: "vos-threshold-bar vos-num", children: riskThreshold }),
1571
+ /* @__PURE__ */ jsx4(
1572
+ "span",
1573
+ {
1574
+ className: `vos-threshold-status ${clearedThreshold ? "vos-threshold-cleared" : "vos-threshold-needs"}`,
1575
+ children: clearedThreshold ? "Cleared for this stage" : "Needs evidence"
1576
+ }
1577
+ )
1578
+ ] }),
1579
+ /* @__PURE__ */ jsx4("div", { className: "vos-threshold-hint", children: clearedThreshold ? `Risk at or below the ${stage} threshold \u2014 de-prioritized.` : `Risk above the ${stage} threshold \u2014 testing-priority.` })
1580
+ ] }) : null,
1554
1581
  /* @__PURE__ */ jsx4(EvidenceCompositionView, { assumption: record, readings: readings.records ?? [] }),
1555
1582
  /* @__PURE__ */ jsx4(ConfidenceExplainerView, { assumption: record, readings: readings.records ?? [] }),
1556
1583
  statement ? /* @__PURE__ */ jsxs4("div", { className: "vos-card vos-detail-section", children: [
@@ -1615,6 +1642,7 @@ function AssumptionDetail({
1615
1642
  EvidenceList,
1616
1643
  {
1617
1644
  assumptionId,
1645
+ questionType,
1618
1646
  readings: readings.records ?? [],
1619
1647
  onNavigate
1620
1648
  }
@@ -1623,6 +1651,7 @@ function AssumptionDetail({
1623
1651
  }
1624
1652
  function EvidenceList({
1625
1653
  assumptionId,
1654
+ questionType,
1626
1655
  readings,
1627
1656
  onNavigate
1628
1657
  }) {
@@ -1633,7 +1662,81 @@ function EvidenceList({
1633
1662
  const rows = readingContributions(rec, readings);
1634
1663
  return new Map(rows.map((r) => [r.id, r]));
1635
1664
  }, [readings, assumptionId]);
1665
+ const qt = QUESTION_TYPES.find((q) => q === questionType);
1666
+ const { probative, flagged } = useMemo(() => {
1667
+ const prob = [];
1668
+ const flag = [];
1669
+ for (const r of linkedReadings) {
1670
+ const rung = String(r.Rung ?? "");
1671
+ if (qt && RUNGS.includes(rung) && isNonEvidence(qt, rung)) {
1672
+ flag.push(r);
1673
+ } else {
1674
+ prob.push(r);
1675
+ }
1676
+ }
1677
+ return { probative: prob, flagged: flag };
1678
+ }, [linkedReadings, qt]);
1636
1679
  if (linkedReadings.length === 0) return null;
1680
+ const renderRow = (r, flagged2) => {
1681
+ const id = String(r.id ?? "");
1682
+ const belief2 = readingBeliefFor(r, assumptionId);
1683
+ if (!belief2) return null;
1684
+ const result = String(belief2.Result ?? "Inconclusive");
1685
+ const justification = String(belief2["Grading justification"] ?? "");
1686
+ const excerpt = typeof belief2.excerpt === "string" && belief2.excerpt !== "" ? belief2.excerpt : snippetFromBody2(String(r.body ?? ""), assumptionId);
1687
+ const rung = String(r.Rung ?? "");
1688
+ const source = String(r.Source ?? "");
1689
+ const expId = r.experimentId ? String(r.experimentId) : null;
1690
+ const c = contribById.get(id);
1691
+ return /* @__PURE__ */ jsxs4(
1692
+ "button",
1693
+ {
1694
+ type: "button",
1695
+ className: `vos-evidence-row vos-verdict-${verdictTone(result)}${flagged2 ? " vos-evidence-row-flagged" : ""}`,
1696
+ onClick: () => onNavigate({ name: "reading", id }),
1697
+ children: [
1698
+ /* @__PURE__ */ jsxs4("div", { className: "vos-evidence-row-head", children: [
1699
+ /* @__PURE__ */ jsx4("span", { className: "vos-evidence-date vos-num", children: String(r.Date ?? "") }),
1700
+ /* @__PURE__ */ jsx4("span", { className: "vos-evidence-title", children: String(r.Title ?? id) }),
1701
+ /* @__PURE__ */ jsx4("span", { className: `vos-pill vos-pill-${verdictTone(result)}`, children: result }),
1702
+ /* @__PURE__ */ jsx4("span", { className: "vos-rung-tag", children: rung }),
1703
+ 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,
1704
+ c && c.used ? /* @__PURE__ */ jsxs4("span", { className: `vos-evidence-score vos-num vos-text-${contributionTone(c.contribution)}`, children: [
1705
+ formatSigned(c.contribution),
1706
+ " confidence"
1707
+ ] }) : null
1708
+ ] }),
1709
+ excerpt ? /* @__PURE__ */ jsxs4("div", { className: "vos-evidence-excerpt", children: [
1710
+ "\u201C",
1711
+ excerpt,
1712
+ "\u201D"
1713
+ ] }) : null,
1714
+ justification ? /* @__PURE__ */ jsxs4("div", { className: `vos-belief-rationale vos-verdict-border-${verdictTone(result)}`, children: [
1715
+ /* @__PURE__ */ jsx4("span", { className: "vos-belief-rationale-label", children: "grading rationale:" }),
1716
+ justification
1717
+ ] }) : null,
1718
+ /* @__PURE__ */ jsxs4("div", { className: "vos-evidence-source", children: [
1719
+ /* @__PURE__ */ jsx4("span", { className: "vos-evidence-source-link", title: source, children: source }),
1720
+ expId ? /* @__PURE__ */ jsxs4(Fragment3, { children: [
1721
+ " \xB7 from experiment ",
1722
+ /* @__PURE__ */ jsx4(
1723
+ "span",
1724
+ {
1725
+ className: "vos-link",
1726
+ onClick: (ev) => {
1727
+ ev.stopPropagation();
1728
+ onNavigate({ name: "experiment", id: expId });
1729
+ },
1730
+ children: expId
1731
+ }
1732
+ )
1733
+ ] }) : null
1734
+ ] })
1735
+ ]
1736
+ },
1737
+ id
1738
+ );
1739
+ };
1637
1740
  return /* @__PURE__ */ jsxs4("div", { className: "vos-card vos-detail-section", children: [
1638
1741
  /* @__PURE__ */ jsxs4("div", { className: "vos-detail-section-label", children: [
1639
1742
  "Evidence \xB7 ",
@@ -1642,65 +1745,20 @@ function EvidenceList({
1642
1745
  linkedReadings.length === 1 ? "" : "s",
1643
1746
  " of evidence"
1644
1747
  ] }),
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
- })
1748
+ probative.length > 0 ? /* @__PURE__ */ jsxs4("div", { className: "vos-evidence-group vos-evidence-group-probative", children: [
1749
+ /* @__PURE__ */ jsxs4("div", { className: "vos-evidence-group-label", children: [
1750
+ "Probative evidence \xB7 ",
1751
+ probative.length
1752
+ ] }),
1753
+ probative.map((r) => renderRow(r, false))
1754
+ ] }) : null,
1755
+ flagged.length > 0 ? /* @__PURE__ */ jsxs4("div", { className: "vos-evidence-group vos-evidence-group-flagged", children: [
1756
+ /* @__PURE__ */ jsxs4("div", { className: "vos-evidence-group-label", children: [
1757
+ "Flagged as non-evidence for this question type \xB7 ",
1758
+ flagged.length
1759
+ ] }),
1760
+ flagged.map((r) => renderRow(r, true))
1761
+ ] }) : null
1704
1762
  ] });
1705
1763
  }
1706
1764
  function contributionTone(v) {
@@ -1708,6 +1766,14 @@ function contributionTone(v) {
1708
1766
  if (v < 0) return "crit";
1709
1767
  return "neutral";
1710
1768
  }
1769
+ var RUNGS = [
1770
+ "Talk",
1771
+ "Desk research",
1772
+ "Signed up",
1773
+ "Observed usage",
1774
+ "Signed intent",
1775
+ "Paying users"
1776
+ ];
1711
1777
  function snippetFromBody2(body, cue) {
1712
1778
  if (!body) return "";
1713
1779
  const quoteMatch = body.match(/## Quote\n+([\s\S]*?)(?=\n## |\n##$|$)/i);
@@ -1917,7 +1983,7 @@ function EvidenceCompositionView({
1917
1983
  }
1918
1984
 
1919
1985
  // src/assumptions-surface.tsx
1920
- import { useMemo as useMemo2 } from "react";
1986
+ import { useMemo as useMemo2, useState as useState3 } from "react";
1921
1987
 
1922
1988
  // src/next-move.ts
1923
1989
  import {
@@ -2068,6 +2134,114 @@ import {
2068
2134
  assumptionCompleteness,
2069
2135
  readingBeliefInputs
2070
2136
  } from "@validation-os/core";
2137
+ import { riskThresholdForStage as riskThresholdForStage2 } from "@validation-os/core/derivation";
2138
+
2139
+ // src/stage-grid-model.ts
2140
+ var STAGE_ORDER = [
2141
+ "Discovery",
2142
+ "Validation",
2143
+ "Scale",
2144
+ "Maturity"
2145
+ ];
2146
+ var STAGE_GLOSS = {
2147
+ Discovery: "Problem-solution fit \u2014 will they engage, care, disclose?",
2148
+ Validation: "Product-market fit \u2014 will they pay, sign, stay?",
2149
+ Scale: "Growth \u2014 can we acquire efficiently, does CAC<LTV hold at volume?",
2150
+ Maturity: "Defense \u2014 will incumbents respond, will regulators accept?"
2151
+ };
2152
+ var NO_LENS = "\u2014";
2153
+ var NO_STAGE = "\u2014";
2154
+ function stageOf(record) {
2155
+ const v = str(record.Stage);
2156
+ if (!v) return null;
2157
+ return STAGE_ORDER.includes(v) ? v : null;
2158
+ }
2159
+ function rankByRisk(records) {
2160
+ return [...records].map((r) => ({ r, risk: derivedNum(r, "risk") ?? 0 })).sort(
2161
+ (a, b) => a.risk !== b.risk ? b.risk - a.risk : a.r.id.localeCompare(b.r.id)
2162
+ ).map((x) => x.r);
2163
+ }
2164
+ function buildStageGrid(assumptions) {
2165
+ const lensOrder = [];
2166
+ const seenLens = /* @__PURE__ */ new Set();
2167
+ const pushLens = (lens) => {
2168
+ if (!seenLens.has(lens)) {
2169
+ seenLens.add(lens);
2170
+ lensOrder.push(lens);
2171
+ }
2172
+ };
2173
+ const buckets = /* @__PURE__ */ new Map();
2174
+ const ensureLens = (lens) => {
2175
+ let m = buckets.get(lens);
2176
+ if (!m) {
2177
+ m = /* @__PURE__ */ new Map();
2178
+ buckets.set(lens, m);
2179
+ }
2180
+ return m;
2181
+ };
2182
+ let hasNoLens = false;
2183
+ let hasNoStage = false;
2184
+ for (const a of assumptions) {
2185
+ const lens = str(a.Lens) ?? NO_LENS;
2186
+ const stage = stageOf(a) ?? NO_STAGE;
2187
+ if (lens === NO_LENS) hasNoLens = true;
2188
+ if (stage === NO_STAGE) hasNoStage = true;
2189
+ pushLens(lens);
2190
+ const byStage = ensureLens(lens);
2191
+ let list = byStage.get(stage);
2192
+ if (!list) {
2193
+ list = [];
2194
+ byStage.set(stage, list);
2195
+ }
2196
+ list.push(a);
2197
+ }
2198
+ if (hasNoLens) {
2199
+ lensOrder.splice(lensOrder.indexOf(NO_LENS), 1);
2200
+ lensOrder.push(NO_LENS);
2201
+ } else {
2202
+ const idx = lensOrder.indexOf(NO_LENS);
2203
+ if (idx >= 0) lensOrder.splice(idx, 1);
2204
+ }
2205
+ const stageOrder = [...STAGE_ORDER];
2206
+ if (hasNoStage) stageOrder.push(NO_STAGE);
2207
+ const cells = [];
2208
+ let maxCellCount = 0;
2209
+ let total = 0;
2210
+ for (const lens of lensOrder) {
2211
+ const byStage = buckets.get(lens) ?? /* @__PURE__ */ new Map();
2212
+ for (const stage of stageOrder) {
2213
+ const records = byStage.get(stage) ?? [];
2214
+ const ranked = rankByRisk(records);
2215
+ const count = ranked.length;
2216
+ maxCellCount = Math.max(maxCellCount, count);
2217
+ total += count;
2218
+ cells.push({
2219
+ lens,
2220
+ stage,
2221
+ count,
2222
+ assumptions: ranked,
2223
+ // density filled in a second pass once maxCellCount is known.
2224
+ density: 0
2225
+ });
2226
+ }
2227
+ }
2228
+ const norm2 = maxCellCount > 0 ? 1 / maxCellCount : 0;
2229
+ for (const cell of cells) {
2230
+ cell.density = cell.count * norm2;
2231
+ }
2232
+ return {
2233
+ lenses: lensOrder,
2234
+ stages: [...STAGE_ORDER],
2235
+ cells,
2236
+ maxCellCount,
2237
+ total
2238
+ };
2239
+ }
2240
+ function cellAt(view, lens, stage) {
2241
+ return view.cells.find((c) => c.lens === lens && c.stage === stage) ?? null;
2242
+ }
2243
+
2244
+ // src/pipeline.ts
2071
2245
  import {
2072
2246
  beliefRisk,
2073
2247
  beliefTestMeters,
@@ -2156,6 +2330,10 @@ function buildPipeline(assumptions, experiments) {
2156
2330
  };
2157
2331
  const framed = assumptionCompleteness(a);
2158
2332
  const stage = deriveBeliefStage({ framed, confidence: d.confidence, test });
2333
+ const questionType = str3(a["Question Type"]);
2334
+ const stageName = str3(a.Stage);
2335
+ const stageKey = stageName && STAGE_ORDER.includes(stageName) ? stageName : null;
2336
+ const riskThreshold = stageKey ? riskThresholdForStage2(stageKey) : null;
2159
2337
  rows.push({
2160
2338
  id: a.id,
2161
2339
  statement: str3(a.Title),
@@ -2168,7 +2346,11 @@ function buildPipeline(assumptions, experiments) {
2168
2346
  framed: stage.framed,
2169
2347
  planned: stage.planned,
2170
2348
  tested: stage.tested,
2171
- nextMove: nextMove(stage.stage, stage.killZone)
2349
+ nextMove: nextMove(stage.stage, stage.killZone),
2350
+ questionType,
2351
+ stage: stageName,
2352
+ riskThreshold,
2353
+ clearedThreshold: riskThreshold != null ? d.risk <= riskThreshold : null
2172
2354
  });
2173
2355
  }
2174
2356
  rows.sort(
@@ -2188,8 +2370,13 @@ function weekOverWeekDelta(assumptions, readings, now) {
2188
2370
  });
2189
2371
  if (!anyDated) return null;
2190
2372
  const cutoff = now.getTime() - 7 * 24 * 60 * 60 * 1e3;
2373
+ const assumptionsById = new Map(
2374
+ assumptions.map((a) => [String(a.id), a])
2375
+ );
2191
2376
  const byAssumption = /* @__PURE__ */ new Map();
2192
- for (const input of readings.flatMap(readingBeliefInputs)) {
2377
+ for (const input of readings.flatMap(
2378
+ (r) => readingBeliefInputs(r, assumptionsById)
2379
+ )) {
2193
2380
  if (!input.assumptionId) continue;
2194
2381
  const arr = byAssumption.get(input.assumptionId);
2195
2382
  if (arr) arr.push(input);
@@ -2408,111 +2595,6 @@ function stageMeters(input) {
2408
2595
  ];
2409
2596
  }
2410
2597
 
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
2598
  // src/assumptions-surface.tsx
2517
2599
  import { Fragment as Fragment4, jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
2518
2600
  function AssumptionsSurface({
@@ -2779,7 +2861,17 @@ function PipelineRowView({ row, onOpen }) {
2779
2861
  /* @__PURE__ */ jsxs5("span", { className: "vos-num", children: [
2780
2862
  "conf ",
2781
2863
  formatSigned(row.confidence)
2782
- ] })
2864
+ ] }),
2865
+ row.questionType ? /* @__PURE__ */ jsx5("span", { className: "vos-pipe-tag vos-pipe-tag-qt", children: row.questionType }) : null,
2866
+ row.stage ? /* @__PURE__ */ jsx5("span", { className: "vos-pipe-tag vos-pipe-tag-stage", children: row.stage }) : null,
2867
+ row.riskThreshold != null ? /* @__PURE__ */ jsx5(
2868
+ "span",
2869
+ {
2870
+ className: `vos-pipe-tag vos-pipe-tag-thresh ${row.clearedThreshold ? "vos-pipe-tag-cleared" : "vos-pipe-tag-needs"}`,
2871
+ title: `Stage threshold ${row.riskThreshold} \u2014 ${row.clearedThreshold ? "cleared for this stage" : "needs evidence"}`,
2872
+ children: row.clearedThreshold ? "cleared" : `bar ${row.riskThreshold}`
2873
+ }
2874
+ ) : null
2783
2875
  ] })
2784
2876
  ] }),
2785
2877
  /* @__PURE__ */ jsx5("div", { className: "vos-pipe-prog-2seg", "aria-label": "Evidence progress (Framed + Known)", children: /* @__PURE__ */ jsx5(Meter2Seg, { framed: row.framed, knownPct, knownSign }) }),
@@ -2808,6 +2900,7 @@ function NextMovesSection({
2808
2900
  recs,
2809
2901
  onOpenAssumption
2810
2902
  }) {
2903
+ const [openRec, setOpenRec] = useState3(null);
2811
2904
  return /* @__PURE__ */ jsxs5("div", { className: "vos-next-moves", children: [
2812
2905
  /* @__PURE__ */ jsx5("div", { className: "vos-next-moves-head", children: "Next moves" }),
2813
2906
  /* @__PURE__ */ jsxs5("div", { className: "vos-next-moves-cols", children: [
@@ -2847,38 +2940,118 @@ function NextMovesSection({
2847
2940
  ] }),
2848
2941
  /* @__PURE__ */ jsxs5("div", { className: "vos-card vos-next-moves-col", children: [
2849
2942
  /* @__PURE__ */ jsx5("div", { className: "vos-next-moves-col-label", children: "Proposed experiments \xB7 one per lens" }),
2850
- 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("div", { className: "vos-next-moves-rec", children: [
2851
- /* @__PURE__ */ jsxs5("div", { className: "vos-next-moves-rec-head", children: [
2852
- /* @__PURE__ */ jsx5(
2853
- "span",
2854
- {
2855
- className: "vos-next-moves-lens",
2856
- style: { background: `${rec.lensColour}20`, color: rec.lensColour, borderColor: `${rec.lensColour}40` },
2857
- children: rec.lens
2858
- }
2859
- ),
2860
- /* @__PURE__ */ jsx5("span", { className: "vos-next-moves-rec-type", children: rec.type }),
2861
- /* @__PURE__ */ jsxs5("span", { className: "vos-next-moves-rec-risk vos-num", children: [
2862
- Math.round(rec.maxRisk),
2863
- " risk"
2943
+ 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(
2944
+ "button",
2945
+ {
2946
+ type: "button",
2947
+ className: "vos-next-moves-rec vos-next-moves-rec-compact",
2948
+ onClick: () => setOpenRec(rec),
2949
+ children: [
2950
+ /* @__PURE__ */ jsxs5("div", { className: "vos-next-moves-rec-head", children: [
2951
+ /* @__PURE__ */ jsx5(
2952
+ "span",
2953
+ {
2954
+ className: "vos-next-moves-lens",
2955
+ style: { background: `${rec.lensColour}20`, color: rec.lensColour, borderColor: `${rec.lensColour}40` },
2956
+ children: rec.lens
2957
+ }
2958
+ ),
2959
+ /* @__PURE__ */ jsx5("span", { className: "vos-next-moves-rec-type", children: rec.type }),
2960
+ /* @__PURE__ */ jsxs5("span", { className: "vos-next-moves-rec-risk vos-num", children: [
2961
+ Math.round(rec.maxRisk),
2962
+ " risk"
2963
+ ] })
2964
+ ] }),
2965
+ /* @__PURE__ */ jsx5("div", { className: "vos-next-moves-rec-title", children: rec.title }),
2966
+ /* @__PURE__ */ jsx5("span", { className: "vos-next-moves-rec-open", "aria-hidden": "true", children: "\u2192" })
2967
+ ]
2968
+ },
2969
+ rec.id
2970
+ ))
2971
+ ] })
2972
+ ] }),
2973
+ openRec ? /* @__PURE__ */ jsx5(
2974
+ RecommendedExperimentDrawer,
2975
+ {
2976
+ rec: openRec,
2977
+ onOpenAssumption,
2978
+ onClose: () => setOpenRec(null)
2979
+ }
2980
+ ) : null
2981
+ ] });
2982
+ }
2983
+ function RecommendedExperimentDrawer({
2984
+ rec,
2985
+ onOpenAssumption,
2986
+ onClose
2987
+ }) {
2988
+ return /* @__PURE__ */ jsxs5(Fragment4, { children: [
2989
+ /* @__PURE__ */ jsx5("div", { className: "vos-scrim", onClick: onClose, "aria-hidden": "true" }),
2990
+ /* @__PURE__ */ jsxs5(
2991
+ "aside",
2992
+ {
2993
+ className: "vos-drawer vos-rec-drawer",
2994
+ role: "dialog",
2995
+ "aria-modal": "true",
2996
+ "aria-label": `Proposed experiment: ${rec.title}`,
2997
+ children: [
2998
+ /* @__PURE__ */ jsxs5("div", { className: "vos-drawer-header", children: [
2999
+ /* @__PURE__ */ jsxs5("div", { children: [
3000
+ /* @__PURE__ */ jsx5("div", { className: "vos-drawer-eyebrow", children: "Proposed experiment" }),
3001
+ /* @__PURE__ */ jsx5("h2", { className: "vos-drawer-title", children: rec.title })
3002
+ ] }),
3003
+ /* @__PURE__ */ jsxs5("div", { style: { marginLeft: "auto", display: "flex", gap: 8, alignItems: "center" }, children: [
3004
+ /* @__PURE__ */ jsx5(
3005
+ "span",
3006
+ {
3007
+ className: "vos-next-moves-lens",
3008
+ style: { background: `${rec.lensColour}20`, color: rec.lensColour, borderColor: `${rec.lensColour}40` },
3009
+ children: rec.lens
3010
+ }
3011
+ ),
3012
+ /* @__PURE__ */ jsx5("span", { className: "vos-next-moves-rec-type", children: rec.type }),
3013
+ /* @__PURE__ */ jsxs5("span", { className: "vos-next-moves-rec-risk vos-num", children: [
3014
+ Math.round(rec.maxRisk),
3015
+ " risk"
3016
+ ] }),
3017
+ /* @__PURE__ */ jsx5("button", { type: "button", className: "vos-btn vos-btn-sm", onClick: onClose, "aria-label": "Close", children: "\u2715" })
2864
3018
  ] })
2865
3019
  ] }),
2866
- /* @__PURE__ */ jsx5("div", { className: "vos-next-moves-rec-title", children: rec.title }),
2867
- /* @__PURE__ */ jsx5("div", { className: "vos-next-moves-rec-rationale", children: rec.rationale }),
2868
- /* @__PURE__ */ jsx5("div", { className: "vos-next-moves-rec-chips", children: rec.assumptionIds.map((id) => /* @__PURE__ */ jsx5(
2869
- "button",
2870
- {
2871
- type: "button",
2872
- className: "vos-next-moves-rec-chip",
2873
- onClick: () => onOpenAssumption(id),
2874
- children: id
2875
- },
2876
- id
2877
- )) }),
2878
- /* @__PURE__ */ jsx5("div", { className: "vos-next-moves-rec-actions", children: /* @__PURE__ */ jsx5("button", { type: "button", className: "vos-btn vos-btn-sm", children: "Accept & create experiment" }) })
2879
- ] }, rec.id))
2880
- ] })
2881
- ] })
3020
+ /* @__PURE__ */ jsxs5("div", { className: "vos-drawer-body", children: [
3021
+ /* @__PURE__ */ jsxs5("div", { className: "vos-rec-drawer-section", children: [
3022
+ /* @__PURE__ */ jsx5("div", { className: "vos-drawer-eyebrow", children: "Why this test" }),
3023
+ /* @__PURE__ */ jsx5("p", { className: "vos-rec-drawer-rationale", children: rec.rationale })
3024
+ ] }),
3025
+ /* @__PURE__ */ jsxs5("div", { className: "vos-rec-drawer-section", children: [
3026
+ /* @__PURE__ */ jsx5("div", { className: "vos-drawer-eyebrow", children: "Bar" }),
3027
+ /* @__PURE__ */ jsxs5("div", { className: "vos-rec-drawer-bar", children: [
3028
+ /* @__PURE__ */ jsx5("strong", { children: "Right if:" }),
3029
+ " ",
3030
+ rec.barPreview
3031
+ ] })
3032
+ ] }),
3033
+ /* @__PURE__ */ jsxs5("div", { className: "vos-rec-drawer-section", children: [
3034
+ /* @__PURE__ */ jsx5("div", { className: "vos-drawer-eyebrow", children: "Assumptions" }),
3035
+ /* @__PURE__ */ jsx5("div", { className: "vos-next-moves-rec-chips", children: rec.assumptionIds.map((id) => /* @__PURE__ */ jsx5(
3036
+ "button",
3037
+ {
3038
+ type: "button",
3039
+ className: "vos-next-moves-rec-chip",
3040
+ onClick: () => onOpenAssumption(id),
3041
+ children: id
3042
+ },
3043
+ id
3044
+ )) })
3045
+ ] }),
3046
+ /* @__PURE__ */ jsxs5("div", { className: "vos-rec-drawer-section", children: [
3047
+ /* @__PURE__ */ jsx5("div", { className: "vos-drawer-eyebrow", children: "Protocol" }),
3048
+ /* @__PURE__ */ jsx5("div", { className: "vos-rec-drawer-body", children: /* @__PURE__ */ jsx5(EvidenceBody, { text: rec.body }) })
3049
+ ] })
3050
+ ] }),
3051
+ /* @__PURE__ */ jsx5("div", { className: "vos-drawer-footer", children: /* @__PURE__ */ jsx5("button", { type: "button", className: "vos-btn vos-btn-sm vos-btn-accent", children: "Accept & create experiment" }) })
3052
+ ]
3053
+ }
3054
+ )
2882
3055
  ] });
2883
3056
  }
2884
3057
  function riskToneClass(risk) {
@@ -3733,9 +3906,9 @@ function barVerdictFor(exp, assumptionId) {
3733
3906
  const line = bars.find((b) => b.assumptionId === assumptionId);
3734
3907
  return line?.barVerdict ?? null;
3735
3908
  }
3736
- function buildCycles(assumptionId, readings, experiments) {
3909
+ function buildCycles(assumptionId, readings, experiments, assumptionsById) {
3737
3910
  const mine = readings.filter((r) => readingGrades(r, assumptionId));
3738
- const inputs = readings.flatMap(readingBeliefInputs2).filter((i) => i.assumptionId === assumptionId);
3911
+ const inputs = readings.flatMap((r) => readingBeliefInputs2(r, assumptionsById)).filter((i) => i.assumptionId === assumptionId);
3739
3912
  const { movers } = confidenceAttribution(inputs);
3740
3913
  const moverByKey = new Map(movers.map((m) => [m.key, m]));
3741
3914
  const byExperiment = /* @__PURE__ */ new Map();
@@ -3859,7 +4032,10 @@ function buildJourney(assumptionId, records, now) {
3859
4032
  const test = beliefTestMeters2(live.map(toStageExperimentInput)).get(assumptionId) ?? emptyTestMeter();
3860
4033
  const framed = assumptionCompleteness2(belief2);
3861
4034
  const stage = deriveBeliefStage2({ framed, confidence: confidence2, test });
3862
- const myReadingInputs = records.readings.flatMap(readingBeliefInputs3).filter((i) => i.assumptionId === assumptionId);
4035
+ const assumptionsById = new Map(
4036
+ records.assumptions.map((a) => [String(a.id), a])
4037
+ );
4038
+ const myReadingInputs = records.readings.flatMap((r) => readingBeliefInputs3(r, assumptionsById)).filter((i) => i.assumptionId === assumptionId);
3863
4039
  const myExperiments = live.filter((e) => testsAssumption(e, assumptionId)).map((e) => ({ id: e.id, date: str4(e.Date) || str4(e.createdAt) || null }));
3864
4040
  const events = assembleJourney({
3865
4041
  belief: {
@@ -3873,7 +4049,12 @@ function buildJourney(assumptionId, records, now) {
3873
4049
  const nextMove2 = rankNextMoves(toNextMoveInput(records)).find(
3874
4050
  (m) => m.assumptionId === assumptionId
3875
4051
  ) ?? null;
3876
- const cycles = buildCycles(assumptionId, records.readings, records.experiments);
4052
+ const cycles = buildCycles(
4053
+ assumptionId,
4054
+ records.readings,
4055
+ records.experiments,
4056
+ assumptionsById
4057
+ );
3877
4058
  return {
3878
4059
  id: assumptionId,
3879
4060
  statement: str4(belief2.Title),
@@ -4462,7 +4643,8 @@ import {
4462
4643
  isConcluded as isConcluded2
4463
4644
  } from "@validation-os/core/derivation";
4464
4645
  function buildUnderstanding(assumption, readings, experiments) {
4465
- const inputs = readings.flatMap(readingBeliefInputs4).filter((i) => i.assumptionId === assumption.id);
4646
+ const assumptionsById = /* @__PURE__ */ new Map([[String(assumption.id), assumption]]);
4647
+ const inputs = readings.flatMap((r) => readingBeliefInputs4(r, assumptionsById)).filter((i) => i.assumptionId === assumption.id);
4466
4648
  const { confidence: confidence2, movers } = confidenceAttribution2(inputs);
4467
4649
  const experimentsById = new Map(experiments.map((e) => [e.id, e]));
4468
4650
  const moverByExperiment = new Map(
@@ -7739,7 +7921,7 @@ function RegisterCounts({
7739
7921
  import { useMemo as useMemo10, useState as useState17 } from "react";
7740
7922
  import { rankNextMoves as rankNextMoves2 } from "@validation-os/core/derivation";
7741
7923
  import { Fragment as Fragment12, jsx as jsx30, jsxs as jsxs30 } from "react/jsx-runtime";
7742
- var STAGES = ["Framed", "Planned", "Tested", "Known"];
7924
+ var STAGES2 = ["Framed", "Planned", "Tested", "Known"];
7743
7925
  var MOVE_STAGE = {
7744
7926
  "score-impact": 0,
7745
7927
  "design-experiment": 1,
@@ -8007,7 +8189,7 @@ function WhyPanel({ top, ranked }) {
8007
8189
  }
8008
8190
  function StageStepper({ move }) {
8009
8191
  const at = MOVE_STAGE[move];
8010
- return /* @__PURE__ */ jsx30("div", { className: "vos-stepper", "aria-label": `Stage: ${STAGES[at]}`, children: STAGES.map((label, i) => /* @__PURE__ */ jsx30(
8192
+ return /* @__PURE__ */ jsx30("div", { className: "vos-stepper", "aria-label": `Stage: ${STAGES2[at]}`, children: STAGES2.map((label, i) => /* @__PURE__ */ jsx30(
8011
8193
  "span",
8012
8194
  {
8013
8195
  className: `vos-step${i === at ? " vos-step-at" : ""}${i < at ? " vos-step-done" : ""}`,