@validation-os/dashboard 0.7.1 → 0.8.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
@@ -506,11 +506,12 @@ var area = (key, label) => ({
506
506
  label,
507
507
  kind: "textarea"
508
508
  });
509
- var num2 = (key, label) => ({
509
+ var num2 = (key, label, range = {}) => ({
510
510
  key,
511
511
  label,
512
512
  kind: "number",
513
- nullable: true
513
+ nullable: true,
514
+ ...range
514
515
  });
515
516
  var sel = (key, label, options, nullable = false) => ({ key, label, kind: "select", options, nullable });
516
517
  var READING_RUNGS = [
@@ -528,7 +529,11 @@ var EDITORS = {
528
529
  assumptions: [
529
530
  t("Title", "Assumption"),
530
531
  area("Description", "Description"),
531
- num2("Impact", "Impact"),
532
+ // The only hand-scored number in the registry (`registry-schema.md`) — a
533
+ // 0–100 severity-if-false seed. Every other number (Confidence, Derived
534
+ // Impact, Risk, Strength, Source quality, Completeness %) is computed on
535
+ // write and deliberately absent from this list.
536
+ num2("Impact", "Impact", { min: 0, max: 100 }),
532
537
  sel("Status", "Status", ["Draft", "Live", "Invalidated"]),
533
538
  t("Lens", "Lens"),
534
539
  area("Scoring justification", "Scoring justification")
@@ -601,6 +606,28 @@ function coerce(field, raw) {
601
606
  function norm(value) {
602
607
  return value === void 0 || value === "" ? null : value;
603
608
  }
609
+ function fieldError(field, raw) {
610
+ if (field.kind !== "number") return null;
611
+ const str5 = String(raw ?? "").trim();
612
+ if (str5 === "") return null;
613
+ const n = Number(str5);
614
+ if (Number.isNaN(n)) return `${field.label} must be a number.`;
615
+ if (field.min !== void 0 && n < field.min) {
616
+ return `${field.label} must be at least ${field.min}.`;
617
+ }
618
+ if (field.max !== void 0 && n > field.max) {
619
+ return `${field.label} must be at most ${field.max}.`;
620
+ }
621
+ return null;
622
+ }
623
+ function draftErrors(register, draft) {
624
+ const errors = {};
625
+ for (const field of editableFields(register)) {
626
+ const message = fieldError(field, draft[field.key] ?? "");
627
+ if (message) errors[field.key] = message;
628
+ }
629
+ return errors;
630
+ }
604
631
  function buildPatch(register, original, draft) {
605
632
  const patch = { version: original.version };
606
633
  for (const field of editableFields(register)) {
@@ -1113,6 +1140,154 @@ function derivedLabel(key) {
1113
1140
  return DERIVED_LABEL[key] ?? fieldLabel(key);
1114
1141
  }
1115
1142
 
1143
+ // src/detail-fields.ts
1144
+ var META_FIELDS = /* @__PURE__ */ new Set([
1145
+ "id",
1146
+ "version",
1147
+ "createdAt",
1148
+ "updatedAt",
1149
+ "derived"
1150
+ ]);
1151
+ var SUPPRESSED_FIELDS = /* @__PURE__ */ new Set(["barLineAssumptionIds"]);
1152
+ var RELATION_TARGET = {
1153
+ dependsOnIds: "assumptions",
1154
+ enablesIds: "assumptions",
1155
+ contradictsIds: "assumptions",
1156
+ readingIds: "readings",
1157
+ assumptionId: "assumptions",
1158
+ experimentId: "experiments",
1159
+ basedOnIds: "assumptions",
1160
+ resolvesIds: "assumptions"
1161
+ };
1162
+ var OWNER_FIELDS = /* @__PURE__ */ new Set(["Owner", "Agreed by"]);
1163
+ function relatedList(related, register) {
1164
+ return related[register] ?? [];
1165
+ }
1166
+ function resolveItem(related, register, id) {
1167
+ const hit = relatedList(related, register).find((r) => r.id === id);
1168
+ return { id, register, title: hit ? primaryLabel(hit) : id };
1169
+ }
1170
+ function idsOf(value) {
1171
+ if (Array.isArray(value)) {
1172
+ return value.filter((v) => typeof v === "string");
1173
+ }
1174
+ return typeof value === "string" && value ? [value] : [];
1175
+ }
1176
+ function ownerNames(value) {
1177
+ if (!Array.isArray(value)) return [];
1178
+ return value.map((v) => {
1179
+ if (v && typeof v === "object") {
1180
+ const name = v.name;
1181
+ if (typeof name === "string" && name) return name;
1182
+ const id = v.id;
1183
+ return typeof id === "string" ? id : "\u2014";
1184
+ }
1185
+ return typeof v === "string" ? v : "\u2014";
1186
+ });
1187
+ }
1188
+ function resolveBarLines(bars, related) {
1189
+ return bars.map((b) => ({
1190
+ rightIf: b.rightIf ?? "",
1191
+ wrongIf: b.wrongIf ?? null,
1192
+ plannedRung: b.plannedRung ?? "",
1193
+ barVerdict: b.barVerdict ?? null,
1194
+ assumption: b.assumptionId ? resolveItem(related, "assumptions", b.assumptionId) : null
1195
+ }));
1196
+ }
1197
+ function detailRows(register, record, related = {}) {
1198
+ const keys = Object.keys(record).filter(
1199
+ (k) => !META_FIELDS.has(k) && !SUPPRESSED_FIELDS.has(k)
1200
+ );
1201
+ return keys.map((key) => {
1202
+ const value = record[key];
1203
+ const label = fieldLabel(key);
1204
+ const target = RELATION_TARGET[key];
1205
+ if (target) {
1206
+ const items = idsOf(value).map((id) => resolveItem(related, target, id));
1207
+ return { key, label, kind: "relation", items };
1208
+ }
1209
+ if (OWNER_FIELDS.has(key)) {
1210
+ return { key, label, kind: "owner", names: ownerNames(value) };
1211
+ }
1212
+ if (key === "barLines" && Array.isArray(value)) {
1213
+ return {
1214
+ key,
1215
+ label,
1216
+ kind: "bar-lines",
1217
+ bars: resolveBarLines(value, related)
1218
+ };
1219
+ }
1220
+ return { key, label, kind: "text", text: formatValue(value) };
1221
+ });
1222
+ }
1223
+
1224
+ // src/edit-fields.tsx
1225
+ import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
1226
+ function EditFields({
1227
+ register,
1228
+ draft,
1229
+ errors,
1230
+ onField
1231
+ }) {
1232
+ return /* @__PURE__ */ jsx2("div", { className: "vos-field-stack", children: editableFields(register).map((field) => /* @__PURE__ */ jsx2(
1233
+ FieldInput,
1234
+ {
1235
+ field,
1236
+ value: draft[field.key],
1237
+ error: errors?.[field.key],
1238
+ onChange: (v) => onField(field.key, v)
1239
+ },
1240
+ field.key
1241
+ )) });
1242
+ }
1243
+ function FieldInput({
1244
+ field,
1245
+ value,
1246
+ error,
1247
+ onChange
1248
+ }) {
1249
+ const id = `field-${field.key.replace(/\s+/g, "-")}`;
1250
+ return /* @__PURE__ */ jsxs2("div", { className: "vos-field", children: [
1251
+ /* @__PURE__ */ jsx2("label", { htmlFor: id, children: field.label }),
1252
+ field.kind === "textarea" ? /* @__PURE__ */ jsx2(
1253
+ "textarea",
1254
+ {
1255
+ id,
1256
+ value: String(value ?? ""),
1257
+ onChange: (e) => onChange(e.target.value),
1258
+ rows: 3,
1259
+ className: "vos-input",
1260
+ "aria-invalid": error ? true : void 0
1261
+ }
1262
+ ) : field.kind === "select" ? /* @__PURE__ */ jsxs2(
1263
+ "select",
1264
+ {
1265
+ id,
1266
+ value: String(value ?? ""),
1267
+ onChange: (e) => onChange(e.target.value),
1268
+ className: "vos-input",
1269
+ children: [
1270
+ field.nullable ? /* @__PURE__ */ jsx2("option", { value: "", children: "\u2014" }) : null,
1271
+ field.options?.map((opt) => /* @__PURE__ */ jsx2("option", { value: opt, children: opt }, opt))
1272
+ ]
1273
+ }
1274
+ ) : /* @__PURE__ */ jsx2(
1275
+ "input",
1276
+ {
1277
+ id,
1278
+ type: field.kind === "number" ? "number" : "text",
1279
+ min: field.kind === "number" ? field.min : void 0,
1280
+ max: field.kind === "number" ? field.max : void 0,
1281
+ value: String(value ?? ""),
1282
+ onChange: (e) => onChange(e.target.value),
1283
+ className: "vos-input",
1284
+ "aria-invalid": error ? true : void 0
1285
+ }
1286
+ ),
1287
+ error ? /* @__PURE__ */ jsx2("p", { role: "alert", className: "vos-field-error", children: error }) : null
1288
+ ] });
1289
+ }
1290
+
1116
1291
  // src/glossary-text.tsx
1117
1292
  import { Fragment as Fragment2 } from "react";
1118
1293
 
@@ -1193,7 +1368,7 @@ function toGlossaryTerms(records) {
1193
1368
  }
1194
1369
 
1195
1370
  // src/glossary-text.tsx
1196
- import { Fragment as Fragment3, jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
1371
+ import { Fragment as Fragment3, jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
1197
1372
  var PILL_CLASS = {
1198
1373
  good: "vos-pill vos-pill-good",
1199
1374
  warn: "vos-pill vos-pill-warn",
@@ -1208,8 +1383,8 @@ function GlossaryText({
1208
1383
  onOpenTerm
1209
1384
  }) {
1210
1385
  const nodes = linkify(text, terms, { selfId });
1211
- return /* @__PURE__ */ jsx2(Fragment3, { children: nodes.map(
1212
- (node, i) => node.kind === "text" ? /* @__PURE__ */ jsx2(Fragment2, { children: node.text }, i) : /* @__PURE__ */ jsx2(
1386
+ return /* @__PURE__ */ jsx3(Fragment3, { children: nodes.map(
1387
+ (node, i) => node.kind === "text" ? /* @__PURE__ */ jsx3(Fragment2, { children: node.text }, i) : /* @__PURE__ */ jsx3(
1213
1388
  TermLink,
1214
1389
  {
1215
1390
  text: node.text,
@@ -1225,8 +1400,8 @@ function TermLink({
1225
1400
  term,
1226
1401
  onOpenTerm
1227
1402
  }) {
1228
- return /* @__PURE__ */ jsxs2("span", { className: "vos-gloss", children: [
1229
- /* @__PURE__ */ jsx2(
1403
+ return /* @__PURE__ */ jsxs3("span", { className: "vos-gloss", children: [
1404
+ /* @__PURE__ */ jsx3(
1230
1405
  "button",
1231
1406
  {
1232
1407
  type: "button",
@@ -1235,15 +1410,15 @@ function TermLink({
1235
1410
  children: text
1236
1411
  }
1237
1412
  ),
1238
- /* @__PURE__ */ jsxs2("span", { className: "vos-gloss-pop", role: "tooltip", children: [
1239
- /* @__PURE__ */ jsxs2("span", { className: "vos-gloss-pop-head", children: [
1240
- /* @__PURE__ */ jsx2("b", { children: term.title }),
1241
- /* @__PURE__ */ jsx2("span", { className: PILL_CLASS[statusTone(term.status)], children: term.status })
1413
+ /* @__PURE__ */ jsxs3("span", { className: "vos-gloss-pop", role: "tooltip", children: [
1414
+ /* @__PURE__ */ jsxs3("span", { className: "vos-gloss-pop-head", children: [
1415
+ /* @__PURE__ */ jsx3("b", { children: term.title }),
1416
+ /* @__PURE__ */ jsx3("span", { className: PILL_CLASS[statusTone(term.status)], children: term.status })
1242
1417
  ] }),
1243
- /* @__PURE__ */ jsx2("span", { className: "vos-gloss-pop-def", children: term.definition || "\u2014" }),
1244
- term.dontConfuseWith.length ? /* @__PURE__ */ jsxs2("span", { className: "vos-gloss-pop-neighbours", children: [
1245
- /* @__PURE__ */ jsx2("span", { className: "vos-gloss-pop-label", children: "Don't confuse with" }),
1246
- term.dontConfuseWith.map((n) => /* @__PURE__ */ jsx2(
1418
+ /* @__PURE__ */ jsx3("span", { className: "vos-gloss-pop-def", children: term.definition || "\u2014" }),
1419
+ term.dontConfuseWith.length ? /* @__PURE__ */ jsxs3("span", { className: "vos-gloss-pop-neighbours", children: [
1420
+ /* @__PURE__ */ jsx3("span", { className: "vos-gloss-pop-label", children: "Don't confuse with" }),
1421
+ term.dontConfuseWith.map((n) => /* @__PURE__ */ jsx3(
1247
1422
  "button",
1248
1423
  {
1249
1424
  type: "button",
@@ -1261,7 +1436,7 @@ function TermLink({
1261
1436
  // src/journey.ts
1262
1437
  import {
1263
1438
  assumptionCompleteness as assumptionCompleteness2,
1264
- toReadingInput as toReadingInput2
1439
+ toReadingInput as toReadingInput3
1265
1440
  } from "@validation-os/core";
1266
1441
  import {
1267
1442
  assembleJourney,
@@ -1302,6 +1477,97 @@ function isTesting(a, experiments) {
1302
1477
  );
1303
1478
  }
1304
1479
 
1480
+ // src/cycles.ts
1481
+ import {
1482
+ toReadingInput as toReadingInput2
1483
+ } from "@validation-os/core";
1484
+ import { confidenceAttribution } from "@validation-os/core/derivation";
1485
+ var DIRECT_CYCLE_KEY = "direct";
1486
+ function readingDate(r) {
1487
+ return str3(r.Date);
1488
+ }
1489
+ function toCycleReading(r) {
1490
+ return {
1491
+ id: r.id,
1492
+ date: readingDate(r),
1493
+ result: r.Result ?? null
1494
+ };
1495
+ }
1496
+ function sortByDate(readings) {
1497
+ return [...readings].sort((a, b) => {
1498
+ if (a.date === b.date) return 0;
1499
+ if (a.date === null) return 1;
1500
+ if (b.date === null) return -1;
1501
+ return a.date < b.date ? -1 : 1;
1502
+ });
1503
+ }
1504
+ function barVerdictFor(exp, assumptionId) {
1505
+ const bars = exp.barLines ?? [];
1506
+ const line = bars.find((b) => b.assumptionId === assumptionId);
1507
+ return line?.barVerdict ?? null;
1508
+ }
1509
+ function buildCycles(assumptionId, readings, experiments) {
1510
+ const mine = readings.filter((r) => r.assumptionId === assumptionId);
1511
+ const { movers } = confidenceAttribution(mine.map(toReadingInput2));
1512
+ const moverByKey = new Map(movers.map((m) => [m.key, m]));
1513
+ const byExperiment = /* @__PURE__ */ new Map();
1514
+ const direct = [];
1515
+ for (const r of mine) {
1516
+ const expId = str3(r.experimentId);
1517
+ if (expId) {
1518
+ const bucket = byExperiment.get(expId) ?? [];
1519
+ bucket.push(r);
1520
+ byExperiment.set(expId, bucket);
1521
+ } else {
1522
+ direct.push(r);
1523
+ }
1524
+ }
1525
+ const experimentsById = new Map(experiments.map((e) => [e.id, e]));
1526
+ const experimentIds = /* @__PURE__ */ new Set([
1527
+ ...experiments.filter((e) => testsAssumption(e, assumptionId)).map((e) => e.id),
1528
+ ...byExperiment.keys()
1529
+ ]);
1530
+ const cycles = [...experimentIds].map((id) => {
1531
+ const exp = experimentsById.get(id);
1532
+ const readingViews = sortByDate((byExperiment.get(id) ?? []).map(toCycleReading));
1533
+ const mover = moverByKey.get(id);
1534
+ const date = (exp ? str3(exp.Date) : null) ?? readingViews[0]?.date ?? null;
1535
+ return {
1536
+ key: id,
1537
+ kind: "experiment",
1538
+ title: exp ? str3(exp.Title) : null,
1539
+ status: exp ? str3(exp.Status) : null,
1540
+ date,
1541
+ barVerdict: exp ? barVerdictFor(exp, assumptionId) : null,
1542
+ readings: readingViews,
1543
+ contribution: mover?.contribution ?? 0,
1544
+ magnitude: mover?.magnitude ?? 0
1545
+ };
1546
+ });
1547
+ if (direct.length > 0) {
1548
+ const readingViews = sortByDate(direct.map(toCycleReading));
1549
+ const mover = moverByKey.get(DIRECT_CYCLE_KEY);
1550
+ cycles.push({
1551
+ key: DIRECT_CYCLE_KEY,
1552
+ kind: "direct",
1553
+ title: null,
1554
+ status: null,
1555
+ date: readingViews[0]?.date ?? null,
1556
+ barVerdict: null,
1557
+ readings: readingViews,
1558
+ contribution: mover?.contribution ?? 0,
1559
+ magnitude: mover?.magnitude ?? 0
1560
+ });
1561
+ }
1562
+ cycles.sort((a, b) => {
1563
+ if (a.date === b.date) return a.key.localeCompare(b.key);
1564
+ if (a.date === null) return 1;
1565
+ if (b.date === null) return -1;
1566
+ return a.date < b.date ? -1 : 1;
1567
+ });
1568
+ return cycles;
1569
+ }
1570
+
1305
1571
  // src/journey.ts
1306
1572
  function str4(v) {
1307
1573
  return typeof v === "string" ? v : "";
@@ -1368,18 +1634,20 @@ function buildJourney(assumptionId, records, now) {
1368
1634
  createdAt: str4(belief2.createdAt) || null,
1369
1635
  impactScored: belief2.Impact != null
1370
1636
  },
1371
- readings: myReadings.map(toReadingInput2),
1637
+ readings: myReadings.map(toReadingInput3),
1372
1638
  experiments: myExperiments,
1373
1639
  now
1374
1640
  }).map((event) => ({ ...event, label: labelFor(event) }));
1375
1641
  const nextMove2 = rankNextMoves(toNextMoveInput(records)).find(
1376
1642
  (m) => m.assumptionId === assumptionId
1377
1643
  ) ?? null;
1644
+ const cycles = buildCycles(assumptionId, records.readings, records.experiments);
1378
1645
  return {
1379
1646
  id: assumptionId,
1380
1647
  statement: str4(belief2.Title),
1381
1648
  stage,
1382
1649
  events,
1650
+ cycles,
1383
1651
  nextMove: nextMove2,
1384
1652
  resolved: resolvedKind(belief2)
1385
1653
  };
@@ -1393,7 +1661,7 @@ import { useState as useState3 } from "react";
1393
1661
 
1394
1662
  // src/drawer-shell.tsx
1395
1663
  import { useEffect as useEffect2, useRef } from "react";
1396
- import { Fragment as Fragment4, jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
1664
+ import { Fragment as Fragment4, jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
1397
1665
  function DrawerShell({
1398
1666
  open,
1399
1667
  onClose,
@@ -1411,8 +1679,8 @@ function DrawerShell({
1411
1679
  return () => document.removeEventListener("keydown", onKey);
1412
1680
  }, [open, onClose]);
1413
1681
  if (!open) return null;
1414
- return /* @__PURE__ */ jsxs3(Fragment4, { children: [
1415
- /* @__PURE__ */ jsx3(
1682
+ return /* @__PURE__ */ jsxs4(Fragment4, { children: [
1683
+ /* @__PURE__ */ jsx4(
1416
1684
  "button",
1417
1685
  {
1418
1686
  type: "button",
@@ -1421,7 +1689,7 @@ function DrawerShell({
1421
1689
  className: "vos-scrim"
1422
1690
  }
1423
1691
  ),
1424
- /* @__PURE__ */ jsx3(
1692
+ /* @__PURE__ */ jsx4(
1425
1693
  "aside",
1426
1694
  {
1427
1695
  ref: panelRef,
@@ -1436,65 +1704,6 @@ function DrawerShell({
1436
1704
  ] });
1437
1705
  }
1438
1706
 
1439
- // src/edit-fields.tsx
1440
- import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
1441
- function EditFields({
1442
- register,
1443
- draft,
1444
- onField
1445
- }) {
1446
- return /* @__PURE__ */ jsx4("div", { className: "vos-field-stack", children: editableFields(register).map((field) => /* @__PURE__ */ jsx4(
1447
- FieldInput,
1448
- {
1449
- field,
1450
- value: draft[field.key],
1451
- onChange: (v) => onField(field.key, v)
1452
- },
1453
- field.key
1454
- )) });
1455
- }
1456
- function FieldInput({
1457
- field,
1458
- value,
1459
- onChange
1460
- }) {
1461
- const id = `field-${field.key.replace(/\s+/g, "-")}`;
1462
- return /* @__PURE__ */ jsxs4("div", { className: "vos-field", children: [
1463
- /* @__PURE__ */ jsx4("label", { htmlFor: id, children: field.label }),
1464
- field.kind === "textarea" ? /* @__PURE__ */ jsx4(
1465
- "textarea",
1466
- {
1467
- id,
1468
- value: String(value ?? ""),
1469
- onChange: (e) => onChange(e.target.value),
1470
- rows: 3,
1471
- className: "vos-input"
1472
- }
1473
- ) : field.kind === "select" ? /* @__PURE__ */ jsxs4(
1474
- "select",
1475
- {
1476
- id,
1477
- value: String(value ?? ""),
1478
- onChange: (e) => onChange(e.target.value),
1479
- className: "vos-input",
1480
- children: [
1481
- field.nullable ? /* @__PURE__ */ jsx4("option", { value: "", children: "\u2014" }) : null,
1482
- field.options?.map((opt) => /* @__PURE__ */ jsx4("option", { value: opt, children: opt }, opt))
1483
- ]
1484
- }
1485
- ) : /* @__PURE__ */ jsx4(
1486
- "input",
1487
- {
1488
- id,
1489
- type: field.kind === "number" ? "number" : "text",
1490
- value: String(value ?? ""),
1491
- onChange: (e) => onChange(e.target.value),
1492
- className: "vos-input"
1493
- }
1494
- )
1495
- ] });
1496
- }
1497
-
1498
1707
  // src/field-styles.ts
1499
1708
  var FIELD_LABEL_CLASS = "vos-field-label";
1500
1709
  var FIELD_CONTROL_CLASS = "vos-input";
@@ -2012,18 +2221,18 @@ function StatTile({
2012
2221
 
2013
2222
  // src/understanding.ts
2014
2223
  import {
2015
- toReadingInput as toReadingInput3
2224
+ toReadingInput as toReadingInput4
2016
2225
  } from "@validation-os/core";
2017
2226
  import {
2018
- confidenceAttribution,
2227
+ confidenceAttribution as confidenceAttribution2,
2019
2228
  confidenceTrajectory,
2020
2229
  experimentProgress,
2021
2230
  isConcluded as isConcluded2
2022
2231
  } from "@validation-os/core/derivation";
2023
2232
  function buildUnderstanding(assumption, readings, experiments) {
2024
2233
  const mine = readings.filter((r) => r.assumptionId === assumption.id);
2025
- const inputs = mine.map(toReadingInput3);
2026
- const { confidence: confidence2, movers } = confidenceAttribution(inputs);
2234
+ const inputs = mine.map(toReadingInput4);
2235
+ const { confidence: confidence2, movers } = confidenceAttribution2(inputs);
2027
2236
  const experimentsById = new Map(experiments.map((e) => [e.id, e]));
2028
2237
  const moverByExperiment = new Map(
2029
2238
  movers.filter((m) => m.kind === "experiment").map((m) => [m.experimentId, m])
@@ -2268,6 +2477,10 @@ function BeliefJourney({
2268
2477
  `${event.kind}-${event.refId ?? i}`
2269
2478
  )) })
2270
2479
  ] }),
2480
+ journey.cycles.length > 0 ? /* @__PURE__ */ jsxs8("section", { children: [
2481
+ /* @__PURE__ */ jsx8("div", { className: "vos-why-section-title", children: "Round by round" }),
2482
+ /* @__PURE__ */ jsx8(CycleTimeline, { cycles: journey.cycles })
2483
+ ] }) : null,
2271
2484
  /* @__PURE__ */ jsxs8("section", { children: [
2272
2485
  /* @__PURE__ */ jsx8("div", { className: "vos-why-section-title", children: "What moved the number" }),
2273
2486
  /* @__PURE__ */ jsx8(UnderstandingPanel, { assumption, basePath })
@@ -2413,6 +2626,62 @@ function EventRow({
2413
2626
  ) : null
2414
2627
  ] });
2415
2628
  }
2629
+ function readingDotTone(result) {
2630
+ if (result === "Validated") return "good";
2631
+ if (result === "Invalidated") return "crit";
2632
+ return "neutral";
2633
+ }
2634
+ var BAR_VERDICT_PILL = {
2635
+ Validated: "vos-pill vos-pill-good",
2636
+ Invalidated: "vos-pill vos-pill-crit",
2637
+ Inconclusive: "vos-pill vos-pill-neutral"
2638
+ };
2639
+ function CycleTimeline({ cycles }) {
2640
+ const maxMagnitude = Math.max(...cycles.map((c) => c.magnitude), 0.01);
2641
+ return /* @__PURE__ */ jsx8("ol", { className: "vos-cyc-list", children: cycles.map((cycle, i) => /* @__PURE__ */ jsx8(CycleCard, { cycle, n: i + 1, max: maxMagnitude }, cycle.key)) });
2642
+ }
2643
+ function CycleCard({
2644
+ cycle,
2645
+ n,
2646
+ max
2647
+ }) {
2648
+ const up = cycle.contribution >= 0;
2649
+ const moving = cycle.magnitude > 0;
2650
+ const width = Math.round(cycle.magnitude / max * 50);
2651
+ const fill = up ? { left: "50%", width: `${width}%`, background: "var(--vos-good)" } : { right: "50%", width: `${width}%`, background: "var(--vos-crit)" };
2652
+ return /* @__PURE__ */ jsxs8("li", { className: "vos-cyc-card", children: [
2653
+ /* @__PURE__ */ jsxs8("div", { className: "vos-cyc-head", children: [
2654
+ /* @__PURE__ */ jsxs8("span", { className: "vos-cyc-num", children: [
2655
+ "Round ",
2656
+ n
2657
+ ] }),
2658
+ cycle.date ? /* @__PURE__ */ jsx8("span", { className: "vos-cyc-date", children: cycle.date }) : null
2659
+ ] }),
2660
+ /* @__PURE__ */ jsx8("div", { className: "vos-cyc-title", children: cycle.kind === "direct" ? "Direct evidence" : cycle.title ?? "Untitled experiment" }),
2661
+ cycle.readings.length > 0 ? /* @__PURE__ */ jsx8(
2662
+ "div",
2663
+ {
2664
+ className: "vos-cyc-dots",
2665
+ role: "img",
2666
+ "aria-label": `${cycle.readings.length} reading${cycle.readings.length === 1 ? "" : "s"}`,
2667
+ children: cycle.readings.map((r) => /* @__PURE__ */ jsx8(
2668
+ "span",
2669
+ {
2670
+ className: `vos-jny-dot ${DOT_CLASS[readingDotTone(r.result)]}`
2671
+ },
2672
+ r.id
2673
+ ))
2674
+ }
2675
+ ) : /* @__PURE__ */ jsx8("span", { className: "vos-hint", children: "No readings yet" }),
2676
+ /* @__PURE__ */ jsxs8("div", { className: "vos-cyc-foot", children: [
2677
+ cycle.barVerdict ? /* @__PURE__ */ jsx8("span", { className: BAR_VERDICT_PILL[cycle.barVerdict], children: cycle.barVerdict }) : cycle.kind === "experiment" ? /* @__PURE__ */ jsx8("span", { className: "vos-hint", children: cycle.status === "Closed" ? "No bar line" : "Bar pending" }) : null,
2678
+ moving ? /* @__PURE__ */ jsxs8("span", { className: "vos-cyc-push", children: [
2679
+ /* @__PURE__ */ jsx8("span", { className: "vos-track vos-signed vos-cyc-track", children: /* @__PURE__ */ jsx8("i", { style: fill }) }),
2680
+ /* @__PURE__ */ jsx8("span", { className: up ? "vos-text-good" : "vos-text-crit", children: formatSigned(cycle.contribution) })
2681
+ ] }) : null
2682
+ ] })
2683
+ ] });
2684
+ }
2416
2685
  function NextMoveCard({
2417
2686
  move,
2418
2687
  resolved,
@@ -3073,9 +3342,62 @@ function RecordPage({
3073
3342
  lists.experiments.refresh();
3074
3343
  lists.readings.refresh();
3075
3344
  lists.decisions.refresh();
3345
+ lists.glossary.refresh();
3076
3346
  };
3077
3347
  const terms = toGlossaryTerms(related.glossary ?? []);
3078
3348
  const openTerm = (id) => onNavigate({ name: "record", id });
3349
+ const [editing, setEditing] = useState5(false);
3350
+ const [draft, setDraft] = useState5({});
3351
+ const [baseline, setBaseline] = useState5(null);
3352
+ const { save, saving, conflict, error: saveError, reset } = useUpdate(
3353
+ register ?? backRegister,
3354
+ basePath
3355
+ );
3356
+ function startEditing() {
3357
+ if (!record || !register) return;
3358
+ setBaseline(record);
3359
+ setDraft(draftFrom(register, record));
3360
+ reset();
3361
+ setEditing(true);
3362
+ }
3363
+ function cancelEditing() {
3364
+ setEditing(false);
3365
+ reset();
3366
+ }
3367
+ async function onSaveEdit() {
3368
+ if (!record || !register || !baseline) return;
3369
+ if (Object.keys(draftErrors(register, draft)).length > 0) return;
3370
+ const patch = buildPatch(register, baseline, draft);
3371
+ patch.version = record.version;
3372
+ if (Object.keys(patch).length <= 1) {
3373
+ setEditing(false);
3374
+ return;
3375
+ }
3376
+ const result = await save(record.id, patch);
3377
+ if (result.ok) {
3378
+ setEditing(false);
3379
+ refreshAll();
3380
+ }
3381
+ }
3382
+ function reloadLatest() {
3383
+ reset();
3384
+ refreshAll();
3385
+ }
3386
+ const setField = (key, value) => setDraft((d) => ({ ...d, [key]: value }));
3387
+ const editErrors = editing && register ? draftErrors(register, draft) : {};
3388
+ const edit = {
3389
+ editing,
3390
+ draft,
3391
+ errors: editErrors,
3392
+ saving,
3393
+ conflict,
3394
+ saveError,
3395
+ onEdit: startEditing,
3396
+ onCancel: cancelEditing,
3397
+ onSave: onSaveEdit,
3398
+ onReload: reloadLatest,
3399
+ onField: setField
3400
+ };
3079
3401
  return /* @__PURE__ */ jsxs9("div", { children: [
3080
3402
  /* @__PURE__ */ jsxs9("nav", { className: "vos-crumbs", "aria-label": "Breadcrumb", children: [
3081
3403
  /* @__PURE__ */ jsx9(
@@ -3108,7 +3430,8 @@ function RecordPage({
3108
3430
  basePath,
3109
3431
  journey,
3110
3432
  onJourneyChanged: refreshAll,
3111
- onNavigate
3433
+ onNavigate,
3434
+ edit
3112
3435
  }
3113
3436
  )
3114
3437
  ] });
@@ -3126,11 +3449,13 @@ function RecordBody({
3126
3449
  basePath,
3127
3450
  journey,
3128
3451
  onJourneyChanged,
3129
- onNavigate
3452
+ onNavigate,
3453
+ edit
3130
3454
  }) {
3131
3455
  const page = buildRecordPage(register, record, related, { asOf });
3132
3456
  const activeTab = page.tabs.includes(tab) ? tab : "overview";
3133
3457
  const description = typeof record.Description === "string" ? record.Description : "";
3458
+ const hasErrors = Object.keys(edit.errors).length > 0;
3134
3459
  return /* @__PURE__ */ jsxs9(Fragment8, { children: [
3135
3460
  /* @__PURE__ */ jsxs9("div", { className: "vos-head vos-record-head", children: [
3136
3461
  /* @__PURE__ */ jsxs9("div", { children: [
@@ -3142,7 +3467,16 @@ function RecordBody({
3142
3467
  /* @__PURE__ */ jsxs9("span", { className: "vos-verbadge", children: [
3143
3468
  "v",
3144
3469
  formatValue(record.version)
3145
- ] })
3470
+ ] }),
3471
+ !edit.editing ? /* @__PURE__ */ jsx9(
3472
+ "button",
3473
+ {
3474
+ type: "button",
3475
+ onClick: edit.onEdit,
3476
+ className: "vos-btn vos-btn-ghost vos-btn-sm",
3477
+ children: "Edit"
3478
+ }
3479
+ ) : null
3146
3480
  ] }),
3147
3481
  /* @__PURE__ */ jsx9("div", { className: "vos-tabs", role: "tablist", "aria-label": "Record sections", children: page.tabs.map((t2) => /* @__PURE__ */ jsx9(
3148
3482
  "button",
@@ -3166,36 +3500,74 @@ function RecordBody({
3166
3500
  },
3167
3501
  m.key
3168
3502
  )) }),
3169
- description ? /* @__PURE__ */ jsxs9("section", { className: "vos-record-prose", children: [
3170
- /* @__PURE__ */ jsx9("h3", { className: "vos-section-title", children: "Description" }),
3171
- /* @__PURE__ */ jsx9("p", { children: /* @__PURE__ */ jsx9(
3172
- GlossaryText,
3503
+ edit.editing ? /* @__PURE__ */ jsxs9("section", { className: "vos-record-editor", children: [
3504
+ /* @__PURE__ */ jsx9("h3", { className: "vos-section-title", children: "Edit" }),
3505
+ edit.conflict ? /* @__PURE__ */ jsx9(ConflictBanner, { message: edit.conflict, onReload: edit.onReload }) : null,
3506
+ edit.saveError ? /* @__PURE__ */ jsx9("p", { role: "alert", className: "vos-error", children: edit.saveError }) : null,
3507
+ /* @__PURE__ */ jsx9(
3508
+ EditFields,
3173
3509
  {
3174
- text: description,
3175
- terms,
3176
- selfId: register === "glossary" ? record.id : void 0,
3177
- onOpenTerm
3510
+ register,
3511
+ draft: edit.draft,
3512
+ errors: edit.errors,
3513
+ onField: edit.onField
3178
3514
  }
3179
- ) })
3180
- ] }) : null,
3181
- page.humanText.length ? /* @__PURE__ */ jsxs9("section", { className: "vos-record-prose", children: [
3182
- /* @__PURE__ */ jsxs9("h3", { className: "vos-section-title", children: [
3183
- "From a human ",
3184
- /* @__PURE__ */ jsx9("span", { className: "vos-hint", children: "\u2014 not computed" })
3185
- ] }),
3186
- page.humanText.map((h) => /* @__PURE__ */ jsxs9("div", { className: "vos-human-field", children: [
3187
- /* @__PURE__ */ jsx9("div", { className: "vos-detail-k", children: h.label }),
3515
+ ),
3516
+ /* @__PURE__ */ jsxs9("footer", { className: "vos-drawer-footer", children: [
3517
+ /* @__PURE__ */ jsx9(
3518
+ "button",
3519
+ {
3520
+ type: "button",
3521
+ onClick: edit.onCancel,
3522
+ disabled: edit.saving,
3523
+ className: "vos-btn vos-btn-ghost vos-btn-sm",
3524
+ children: "Cancel"
3525
+ }
3526
+ ),
3527
+ /* @__PURE__ */ jsx9(
3528
+ "button",
3529
+ {
3530
+ type: "button",
3531
+ onClick: edit.onSave,
3532
+ disabled: edit.saving || hasErrors,
3533
+ title: hasErrors ? "Fix the highlighted field before saving" : void 0,
3534
+ className: "vos-btn vos-btn-sm",
3535
+ children: edit.saving ? "Saving\u2026" : "Save"
3536
+ }
3537
+ )
3538
+ ] })
3539
+ ] }) : /* @__PURE__ */ jsxs9(Fragment8, { children: [
3540
+ description ? /* @__PURE__ */ jsxs9("section", { className: "vos-record-prose", children: [
3541
+ /* @__PURE__ */ jsx9("h3", { className: "vos-section-title", children: "Description" }),
3188
3542
  /* @__PURE__ */ jsx9("p", { children: /* @__PURE__ */ jsx9(
3189
3543
  GlossaryText,
3190
3544
  {
3191
- text: h.text,
3545
+ text: description,
3192
3546
  terms,
3193
3547
  selfId: register === "glossary" ? record.id : void 0,
3194
3548
  onOpenTerm
3195
3549
  }
3196
3550
  ) })
3197
- ] }, h.key))
3198
- ] }) : null
3551
+ ] }) : null,
3552
+ page.humanText.length ? /* @__PURE__ */ jsxs9("section", { className: "vos-record-prose", children: [
3553
+ /* @__PURE__ */ jsxs9("h3", { className: "vos-section-title", children: [
3554
+ "From a human ",
3555
+ /* @__PURE__ */ jsx9("span", { className: "vos-hint", children: "\u2014 not computed" })
3556
+ ] }),
3557
+ page.humanText.map((h) => /* @__PURE__ */ jsxs9("div", { className: "vos-human-field", children: [
3558
+ /* @__PURE__ */ jsx9("div", { className: "vos-detail-k", children: h.label }),
3559
+ /* @__PURE__ */ jsx9("p", { children: /* @__PURE__ */ jsx9(
3560
+ GlossaryText,
3561
+ {
3562
+ text: h.text,
3563
+ terms,
3564
+ selfId: register === "glossary" ? record.id : void 0,
3565
+ onOpenTerm
3566
+ }
3567
+ ) })
3568
+ ] }, h.key))
3569
+ ] }) : null
3570
+ ] })
3199
3571
  ] }) : null,
3200
3572
  activeTab === "evidence" ? /* @__PURE__ */ jsx9(
3201
3573
  EvidenceTab,
@@ -3244,6 +3616,15 @@ function RecordBody({
3244
3616
  function PillView({ pill }) {
3245
3617
  return /* @__PURE__ */ jsx9("span", { className: PILL_CLASS3[pill.tone], children: pill.label });
3246
3618
  }
3619
+ function ConflictBanner({
3620
+ message,
3621
+ onReload
3622
+ }) {
3623
+ return /* @__PURE__ */ jsxs9("div", { role: "alert", className: "vos-banner vos-banner-warn", children: [
3624
+ /* @__PURE__ */ jsx9("div", { className: "vos-banner-body", children: /* @__PURE__ */ jsx9("span", { children: message }) }),
3625
+ /* @__PURE__ */ jsx9("button", { type: "button", onClick: onReload, children: "Review the latest" })
3626
+ ] });
3627
+ }
3247
3628
  function MeterView({
3248
3629
  meter,
3249
3630
  assumption,
@@ -3333,14 +3714,26 @@ function EvidenceTab({
3333
3714
  if (register === "assumptions") {
3334
3715
  return /* @__PURE__ */ jsx9("section", { className: "vos-why-panel", children: /* @__PURE__ */ jsx9(UnderstandingPanel, { assumption: record, basePath }) });
3335
3716
  }
3336
- const bars = record.barLines ?? [];
3717
+ const bars = resolveBarLines(
3718
+ record.barLines ?? [],
3719
+ related
3720
+ );
3337
3721
  const mine = (related.readings ?? []).filter((r) => r.experimentId === record.id);
3338
3722
  const nested = nestReadingsByPlan(mine, [record]);
3339
3723
  return /* @__PURE__ */ jsxs9("div", { className: "vos-record-cols", children: [
3340
3724
  /* @__PURE__ */ jsxs9("section", { children: [
3341
3725
  /* @__PURE__ */ jsx9("h3", { className: "vos-section-title", children: "Bar lines" }),
3342
3726
  bars.length === 0 ? /* @__PURE__ */ jsx9("p", { className: "vos-hint", children: "No pre-registered bars." }) : /* @__PURE__ */ jsx9("ul", { className: "vos-bars", children: bars.map((b, i) => /* @__PURE__ */ jsxs9("li", { className: "vos-bar-line", children: [
3343
- /* @__PURE__ */ jsx9("span", { className: "vos-bar-if", children: b.rightIf ?? "\u2014" }),
3727
+ /* @__PURE__ */ jsx9("span", { className: "vos-bar-if", children: b.rightIf || "\u2014" }),
3728
+ b.assumption ? /* @__PURE__ */ jsx9(
3729
+ "button",
3730
+ {
3731
+ type: "button",
3732
+ className: "vos-inline-link",
3733
+ onClick: () => onOpenRecord(b.assumption.id),
3734
+ children: b.assumption.title
3735
+ }
3736
+ ) : null,
3344
3737
  /* @__PURE__ */ jsx9(
3345
3738
  "span",
3346
3739
  {
@@ -3444,7 +3837,6 @@ function Cell({
3444
3837
  // src/record-drawer.tsx
3445
3838
  import { useEffect as useEffect3, useState as useState6 } from "react";
3446
3839
  import { Fragment as Fragment9, jsx as jsx11, jsxs as jsxs11 } from "react/jsx-runtime";
3447
- var META_FIELDS = /* @__PURE__ */ new Set(["id", "version", "createdAt", "updatedAt", "derived"]);
3448
3840
  var DERIVED_SUB = {
3449
3841
  confidence: "Signed average of concluded readings",
3450
3842
  risk: "Impact \xD7 (1 \u2212 Confidence\u207A/100)",
@@ -3463,6 +3855,8 @@ function RecordDrawer({
3463
3855
  basePath,
3464
3856
  onChanged,
3465
3857
  onOpenFull,
3858
+ onOpenRecord,
3859
+ related,
3466
3860
  children
3467
3861
  }) {
3468
3862
  const [editing, setEditing] = useState6(false);
@@ -3481,7 +3875,7 @@ function RecordDrawer({
3481
3875
  reset();
3482
3876
  }, [recordId, reset]);
3483
3877
  const derived = record && record.derived && typeof record.derived === "object" ? record.derived : null;
3484
- const fields = record ? Object.keys(record).filter((k) => !META_FIELDS.has(k)) : [];
3878
+ const rows = record ? detailRows(register, record, related ?? {}) : [];
3485
3879
  function startEditing() {
3486
3880
  if (!record) return;
3487
3881
  setBaseline(record);
@@ -3495,6 +3889,7 @@ function RecordDrawer({
3495
3889
  }
3496
3890
  async function onSave() {
3497
3891
  if (!record || !baseline) return;
3892
+ if (Object.keys(draftErrors(register, draft)).length > 0) return;
3498
3893
  const patch = buildPatch(register, baseline, draft);
3499
3894
  patch.version = record.version;
3500
3895
  if (Object.keys(patch).length <= 1) {
@@ -3512,6 +3907,8 @@ function RecordDrawer({
3512
3907
  onChanged?.();
3513
3908
  }
3514
3909
  const setField = (key, value) => setDraft((d) => ({ ...d, [key]: value }));
3910
+ const errors = editing ? draftErrors(register, draft) : {};
3911
+ const hasErrors = Object.keys(errors).length > 0;
3515
3912
  return /* @__PURE__ */ jsxs11(
3516
3913
  DrawerShell,
3517
3914
  {
@@ -3578,19 +3975,24 @@ function RecordDrawer({
3578
3975
  ] }),
3579
3976
  "confidence" in derived && why ? /* @__PURE__ */ jsx11("div", { className: "vos-why-panel", children: /* @__PURE__ */ jsx11(UnderstandingPanel, { assumption: record, basePath }) }) : null
3580
3977
  ] }) : null,
3581
- conflict ? /* @__PURE__ */ jsx11(ConflictBanner, { message: conflict, onReload: reloadLatest }) : null,
3978
+ conflict ? /* @__PURE__ */ jsx11(ConflictBanner2, { message: conflict, onReload: reloadLatest }) : null,
3582
3979
  saveError ? /* @__PURE__ */ jsx11("p", { role: "alert", className: "vos-banner vos-banner-crit", children: saveError }) : null,
3583
3980
  editing ? /* @__PURE__ */ jsx11(
3584
3981
  EditFields,
3585
3982
  {
3586
3983
  register,
3587
3984
  draft,
3985
+ errors,
3588
3986
  onField: setField
3589
3987
  }
3590
- ) : /* @__PURE__ */ jsx11("div", { className: "vos-detail-list", children: fields.map((key) => /* @__PURE__ */ jsxs11("div", { className: "vos-detail-row", children: [
3591
- /* @__PURE__ */ jsx11("span", { className: "vos-detail-k", children: fieldLabel(key) }),
3592
- /* @__PURE__ */ jsx11("span", { className: "vos-detail-v", children: formatValue(record[key]) })
3593
- ] }, key)) })
3988
+ ) : /* @__PURE__ */ jsx11("div", { className: "vos-detail-list", children: rows.map((row) => /* @__PURE__ */ jsx11(
3989
+ DetailRowView,
3990
+ {
3991
+ row,
3992
+ onOpenRecord
3993
+ },
3994
+ row.key
3995
+ )) })
3594
3996
  ] }) }),
3595
3997
  record && !loading && !error && !editing ? children : null,
3596
3998
  record && editing ? /* @__PURE__ */ jsxs11("footer", { className: "vos-drawer-footer", children: [
@@ -3609,7 +4011,8 @@ function RecordDrawer({
3609
4011
  {
3610
4012
  type: "button",
3611
4013
  onClick: onSave,
3612
- disabled: saving,
4014
+ disabled: saving || hasErrors,
4015
+ title: hasErrors ? "Fix the highlighted field before saving" : void 0,
3613
4016
  className: "vos-btn vos-btn-sm",
3614
4017
  children: saving ? "Saving\u2026" : "Save"
3615
4018
  }
@@ -3623,6 +4026,56 @@ function RecordDrawer({
3623
4026
  }
3624
4027
  );
3625
4028
  }
4029
+ function DetailRowView({
4030
+ row,
4031
+ onOpenRecord
4032
+ }) {
4033
+ return /* @__PURE__ */ jsxs11("div", { className: "vos-detail-row", children: [
4034
+ /* @__PURE__ */ jsx11("span", { className: "vos-detail-k", children: row.label }),
4035
+ /* @__PURE__ */ jsx11("span", { className: "vos-detail-v", children: row.kind === "relation" ? row.items && row.items.length ? /* @__PURE__ */ jsx11(RelationLinks, { items: row.items, onOpenRecord }) : "\u2014" : row.kind === "owner" ? row.names && row.names.length ? row.names.join(", ") : "\u2014" : row.kind === "bar-lines" ? row.bars && row.bars.length ? /* @__PURE__ */ jsx11("ul", { className: "vos-bars", children: row.bars.map((b, i) => /* @__PURE__ */ jsxs11("li", { className: "vos-bar-line", children: [
4036
+ /* @__PURE__ */ jsx11("span", { className: "vos-bar-if", children: b.rightIf || "\u2014" }),
4037
+ b.assumption ? /* @__PURE__ */ jsx11(
4038
+ InlineLink,
4039
+ {
4040
+ id: b.assumption.id,
4041
+ title: b.assumption.title,
4042
+ onOpenRecord
4043
+ }
4044
+ ) : null,
4045
+ /* @__PURE__ */ jsx11(
4046
+ "span",
4047
+ {
4048
+ className: b.barVerdict ? "vos-pill vos-pill-good" : "vos-pill vos-pill-neutral",
4049
+ children: b.barVerdict ?? "open"
4050
+ }
4051
+ )
4052
+ ] }, i)) }) : "\u2014" : row.text })
4053
+ ] });
4054
+ }
4055
+ function RelationLinks({
4056
+ items,
4057
+ onOpenRecord
4058
+ }) {
4059
+ return /* @__PURE__ */ jsx11("span", { className: "vos-detail-links", children: items.map((item, i) => /* @__PURE__ */ jsxs11("span", { children: [
4060
+ i > 0 ? ", " : null,
4061
+ /* @__PURE__ */ jsx11(InlineLink, { id: item.id, title: item.title, onOpenRecord })
4062
+ ] }, item.id)) });
4063
+ }
4064
+ function InlineLink({
4065
+ id,
4066
+ title,
4067
+ onOpenRecord
4068
+ }) {
4069
+ return onOpenRecord ? /* @__PURE__ */ jsx11(
4070
+ "button",
4071
+ {
4072
+ type: "button",
4073
+ className: "vos-inline-link",
4074
+ onClick: () => onOpenRecord(id),
4075
+ children: title
4076
+ }
4077
+ ) : /* @__PURE__ */ jsx11("span", { children: title });
4078
+ }
3626
4079
  function DerivedCell({
3627
4080
  field,
3628
4081
  value,
@@ -3658,7 +4111,7 @@ function DerivedCell({
3658
4111
  DERIVED_SUB[field] ? /* @__PURE__ */ jsx11("div", { className: "vos-dcell-sub", children: DERIVED_SUB[field] }) : null
3659
4112
  ] });
3660
4113
  }
3661
- function ConflictBanner({
4114
+ function ConflictBanner2({
3662
4115
  message,
3663
4116
  onReload
3664
4117
  }) {
@@ -4045,7 +4498,7 @@ function contextNeeds(register) {
4045
4498
  return {
4046
4499
  experiments: register === "assumptions" || register === "readings",
4047
4500
  readings: register === "assumptions",
4048
- assumptions: register === "decisions"
4501
+ assumptions: register === "decisions" || register === "experiments" || register === "readings"
4049
4502
  };
4050
4503
  }
4051
4504
  function RegisterBrowser({
@@ -4098,6 +4551,12 @@ function RegisterBrowser({
4098
4551
  const n = humanCount[tab.id] ?? 0;
4099
4552
  return n > 0 ? n : null;
4100
4553
  };
4554
+ const related = {
4555
+ assumptions: ctx.assumptions,
4556
+ experiments: ctx.experiments,
4557
+ readings: ctx.readings,
4558
+ decisions: ctx.decisions
4559
+ };
4101
4560
  const patch = (p) => setDescriptor((d) => ({ ...d, ...p }));
4102
4561
  const saveCurrentView = () => {
4103
4562
  if (typeof window === "undefined") return;
@@ -4225,6 +4684,8 @@ function RegisterBrowser({
4225
4684
  onClose: () => setOpenId(null),
4226
4685
  basePath,
4227
4686
  onOpenFull: onOpenRecord && openId ? () => onOpenRecord(openId) : void 0,
4687
+ onOpenRecord,
4688
+ related,
4228
4689
  onChanged: () => {
4229
4690
  refreshRecord();
4230
4691
  refreshList();
@@ -4919,24 +5380,124 @@ function ValidationOSDashboard({ config = {} }) {
4919
5380
  ] });
4920
5381
  }
4921
5382
 
4922
- // src/surface-placeholder.tsx
5383
+ // src/connect.ts
5384
+ var DEFAULT_TOKEN_ENV = "VALIDATION_OS_TOKEN";
5385
+ var SHELL_UNSAFE = /['"\\\s]/;
5386
+ function composeConnectCommand(input) {
5387
+ const tokenEnv = input.tokenEnv ?? DEFAULT_TOKEN_ENV;
5388
+ const guarded = [
5389
+ ["Token", input.token],
5390
+ ["API base URL", input.apiBaseUrl]
5391
+ ];
5392
+ for (const [label, value] of guarded) {
5393
+ if (SHELL_UNSAFE.test(value)) {
5394
+ throw new Error(`${label} contains characters that can't be safely pasted.`);
5395
+ }
5396
+ }
5397
+ return [
5398
+ `export ${tokenEnv}='${input.token}'`,
5399
+ `cat > validation-os.config.yaml <<'EOF'`,
5400
+ `connector: remote-api`,
5401
+ `remote_api:`,
5402
+ ` api_base_url: ${input.apiBaseUrl}`,
5403
+ ` token_env: ${tokenEnv}`,
5404
+ `EOF`
5405
+ ].join("\n");
5406
+ }
5407
+
5408
+ // src/connect-claude-code.tsx
5409
+ import { useState as useState14 } from "react";
4923
5410
  import { jsx as jsx18, jsxs as jsxs18 } from "react/jsx-runtime";
5411
+ function ConnectClaudeCode({
5412
+ apiBaseUrl,
5413
+ tokenEnv,
5414
+ mintToken
5415
+ }) {
5416
+ const [state, setState] = useState14({ phase: "idle" });
5417
+ const [copied, setCopied] = useState14(false);
5418
+ async function generate() {
5419
+ setState({ phase: "minting" });
5420
+ setCopied(false);
5421
+ try {
5422
+ const token = await mintToken();
5423
+ const command = composeConnectCommand({ token, apiBaseUrl, tokenEnv });
5424
+ setState({ phase: "ready", command });
5425
+ } catch (e) {
5426
+ setState({
5427
+ phase: "error",
5428
+ message: e instanceof Error ? e.message : "Couldn't generate your connection command."
5429
+ });
5430
+ }
5431
+ }
5432
+ async function copy(command) {
5433
+ try {
5434
+ await navigator.clipboard.writeText(command);
5435
+ setCopied(true);
5436
+ } catch {
5437
+ }
5438
+ }
5439
+ return /* @__PURE__ */ jsxs18("div", { children: [
5440
+ /* @__PURE__ */ jsx18("div", { className: "vos-head", children: /* @__PURE__ */ jsxs18("div", { children: [
5441
+ /* @__PURE__ */ jsx18("h1", { children: "Connect Claude Code" }),
5442
+ /* @__PURE__ */ jsx18("p", { children: "Run the validation skills against this register from your own Claude Code \u2014 no repo, no keys to hunt down." })
5443
+ ] }) }),
5444
+ /* @__PURE__ */ jsxs18("ol", { className: "vos-hint", style: { lineHeight: 1.8 }, children: [
5445
+ /* @__PURE__ */ jsx18("li", { children: "Generate your personal connection command below." }),
5446
+ /* @__PURE__ */ jsx18("li", { children: "Paste it into a terminal in the workspace you'll run the skills in." }),
5447
+ /* @__PURE__ */ jsxs18("li", { children: [
5448
+ "The command carries a token tied to ",
5449
+ /* @__PURE__ */ jsx18("strong", { children: "you" }),
5450
+ " \u2014 anything you write lands under your name. Don't share it."
5451
+ ] })
5452
+ ] }),
5453
+ state.phase !== "ready" ? /* @__PURE__ */ jsx18(
5454
+ "button",
5455
+ {
5456
+ type: "button",
5457
+ className: "vos-btn",
5458
+ onClick: generate,
5459
+ disabled: state.phase === "minting",
5460
+ children: state.phase === "minting" ? "Generating\u2026" : "Generate connection command"
5461
+ }
5462
+ ) : null,
5463
+ state.phase === "error" ? /* @__PURE__ */ jsx18("p", { className: "vos-hint", role: "alert", children: state.message }) : null,
5464
+ state.phase === "ready" ? /* @__PURE__ */ jsxs18("div", { children: [
5465
+ /* @__PURE__ */ jsx18("pre", { className: "vos-code", "aria-label": "Connection command", children: state.command }),
5466
+ /* @__PURE__ */ jsxs18("div", { style: { display: "flex", gap: 8 }, children: [
5467
+ /* @__PURE__ */ jsx18(
5468
+ "button",
5469
+ {
5470
+ type: "button",
5471
+ className: "vos-btn",
5472
+ onClick: () => copy(state.command),
5473
+ children: copied ? "Copied" : "Copy command"
5474
+ }
5475
+ ),
5476
+ /* @__PURE__ */ jsx18("button", { type: "button", className: "vos-btn-ghost", onClick: generate, children: "Regenerate" })
5477
+ ] }),
5478
+ /* @__PURE__ */ jsx18("p", { className: "vos-hint", style: { marginTop: 12 }, children: "Generating again revokes nothing on its own \u2014 remove old keys from your account settings when you rotate." })
5479
+ ] }) : null
5480
+ ] });
5481
+ }
5482
+
5483
+ // src/surface-placeholder.tsx
5484
+ import { jsx as jsx19, jsxs as jsxs19 } from "react/jsx-runtime";
4924
5485
  function SurfacePlaceholder({
4925
5486
  title,
4926
5487
  subtitle,
4927
5488
  detail
4928
5489
  }) {
4929
- return /* @__PURE__ */ jsxs18("div", { children: [
4930
- /* @__PURE__ */ jsx18("div", { className: "vos-head", children: /* @__PURE__ */ jsxs18("div", { children: [
4931
- /* @__PURE__ */ jsx18("h1", { children: title }),
4932
- /* @__PURE__ */ jsx18("p", { children: subtitle })
5490
+ return /* @__PURE__ */ jsxs19("div", { children: [
5491
+ /* @__PURE__ */ jsx19("div", { className: "vos-head", children: /* @__PURE__ */ jsxs19("div", { children: [
5492
+ /* @__PURE__ */ jsx19("h1", { children: title }),
5493
+ /* @__PURE__ */ jsx19("p", { children: subtitle })
4933
5494
  ] }) }),
4934
- /* @__PURE__ */ jsx18("div", { className: "vos-empty", children: detail })
5495
+ /* @__PURE__ */ jsx19("div", { className: "vos-empty", children: detail })
4935
5496
  ] });
4936
5497
  }
4937
5498
 
4938
5499
  // src/register-counts.tsx
4939
- import { jsx as jsx19, jsxs as jsxs19 } from "react/jsx-runtime";
5500
+ import { jsx as jsx20, jsxs as jsxs20 } from "react/jsx-runtime";
4940
5501
  function RegisterCounts({
4941
5502
  counts,
4942
5503
  caption,
@@ -4946,8 +5507,8 @@ function RegisterCounts({
4946
5507
  const registers = REGISTER_ORDER.filter(
4947
5508
  (r) => counts[r] !== void 0
4948
5509
  );
4949
- return /* @__PURE__ */ jsxs19("section", { "aria-label": "Register counts", children: [
4950
- /* @__PURE__ */ jsx19("div", { className: "vos-tile-grid", children: registers.map((register) => /* @__PURE__ */ jsx19(
5510
+ return /* @__PURE__ */ jsxs20("section", { "aria-label": "Register counts", children: [
5511
+ /* @__PURE__ */ jsx20("div", { className: "vos-tile-grid", children: registers.map((register) => /* @__PURE__ */ jsx20(
4951
5512
  StatTile,
4952
5513
  {
4953
5514
  label: REGISTER_LABEL[register],
@@ -4957,13 +5518,16 @@ function RegisterCounts({
4957
5518
  },
4958
5519
  register
4959
5520
  )) }),
4960
- caption ? /* @__PURE__ */ jsx19("p", { className: "vos-hint", style: { marginTop: 16 }, children: caption }) : null
5521
+ caption ? /* @__PURE__ */ jsx20("p", { className: "vos-hint", style: { marginTop: 16 }, children: caption }) : null
4961
5522
  ] });
4962
5523
  }
4963
5524
  export {
4964
5525
  BeliefJourney,
4965
5526
  CONFLICT_MESSAGE,
4966
5527
  ConfidenceCell,
5528
+ ConnectClaudeCode,
5529
+ DEFAULT_TOKEN_ENV,
5530
+ DIRECT_CYCLE_KEY,
4967
5531
  EditBeliefForm,
4968
5532
  EditFields,
4969
5533
  FIRST_RUN_LINE,
@@ -4997,6 +5561,7 @@ export {
4997
5561
  ValidationOSDashboard,
4998
5562
  WriteDecisionForm,
4999
5563
  backlinkPanels,
5564
+ buildCycles,
5000
5565
  buildJourney,
5001
5566
  buildPatch,
5002
5567
  buildPipeline,
@@ -5005,9 +5570,11 @@ export {
5005
5570
  cellValue,
5006
5571
  coldStartFor,
5007
5572
  columnsFor,
5573
+ composeConnectCommand,
5008
5574
  confidenceTone,
5009
5575
  configureRiskBands,
5010
5576
  defaultTabId,
5577
+ detailRows,
5011
5578
  dontConfuseWith,
5012
5579
  draftFrom,
5013
5580
  editableFields,
@@ -5034,8 +5601,10 @@ export {
5034
5601
  movePresentation,
5035
5602
  needsHumanCounts,
5036
5603
  nestReadingsByPlan,
5604
+ ownerNames,
5037
5605
  parseRoute,
5038
5606
  primaryLabel,
5607
+ resolveBarLines,
5039
5608
  riskBand,
5040
5609
  riskFraction,
5041
5610
  riskLevel,