@validation-os/dashboard 0.7.1 → 0.9.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.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import * as react from 'react';
2
2
  import { ReactNode } from 'react';
3
- import { Collection, AnyRecord, Relation } from '@validation-os/core';
3
+ import { Collection, AnyRecord, Result, Rung, MagnitudeBand, Relation, BarLine } from '@validation-os/core';
4
4
  import { PortfolioProgress, StageExperimentInput, MoveKind, NextMoveInput, BeliefStage, JourneyEvent, NextMove, JourneyEventKind, ConfSign, StageKey, Progress, MoverKind, TrajectoryPoint } from '@validation-os/core/derivation';
5
5
  export { StageKey } from '@validation-os/core/derivation';
6
6
 
@@ -243,7 +243,9 @@ interface GroupBucket {
243
243
  * risk band by severity, everything else first-appearance.
244
244
  */
245
245
  declare function groupRecords(records: AnyRecord[], axis: GroupByAxis): GroupBucket[];
246
- /** Case-insensitive substring filter over Title + Description. */
246
+ /** Case-insensitive substring filter over Title + Description + Source. Source
247
+ * (a reading's generator — person / dataset / cohort) is searchable so a
248
+ * teammate can pull every reading from one source by typing its name. */
247
249
  declare function filterRecords(records: AnyRecord[], query: string): AnyRecord[];
248
250
  /** A stable sort by one key; numbers compare numerically, everything else by
249
251
  * locale string. Missing values sort last regardless of direction. */
@@ -384,6 +386,60 @@ interface SidebarNavProps {
384
386
  */
385
387
  declare function SidebarNav({ route, onNavigate, counts, needsHuman, registers, }: SidebarNavProps): react.JSX.Element;
386
388
 
389
+ /**
390
+ * The "Connect Claude Code" command composer (OPS-1349). A person signed into
391
+ * the dashboard mints a personal token and gets back ONE ready-to-paste command
392
+ * that points their own Claude Code at this hosted register through the
393
+ * `remote-api` connector. Everything environmental — the API URL, the env-var
394
+ * name — is baked in here; the person supplies only their minted token.
395
+ *
396
+ * Pure and deterministic so it can be unit-tested exactly (prior art:
397
+ * `route.ts`). The config keys it writes (`api_base_url`, `token_env`) are the
398
+ * `remote-api` connector's contract, fixed by the spec — so this does not
399
+ * depend on the connector doc or the setup wizard shipping first.
400
+ */
401
+ /** The env var the `remote-api` connector reads the bearer token from. */
402
+ declare const DEFAULT_TOKEN_ENV = "VALIDATION_OS_TOKEN";
403
+ interface ConnectCommandInput {
404
+ /** The personal bearer token the deployment minted for the signed-in user. */
405
+ token: string;
406
+ /** The hosted API root, baked in by the deployment. */
407
+ apiBaseUrl: string;
408
+ /** Override the token env-var name; defaults to {@link DEFAULT_TOKEN_ENV}. */
409
+ tokenEnv?: string;
410
+ }
411
+ /**
412
+ * Build the paste command: export the token into its env var, then write a
413
+ * `remote-api` `validation-os.config.yaml` at the workspace root. Running it
414
+ * leaves the workspace ready for `/setup-validation-os` to confirm, or for the
415
+ * skills to use directly.
416
+ */
417
+ declare function composeConnectCommand(input: ConnectCommandInput): string;
418
+
419
+ /**
420
+ * The "Connect Claude Code" page (OPS-1349). A person already signed into the
421
+ * dashboard mints a personal token and gets one ready-to-paste command that
422
+ * points their own Claude Code at this hosted register.
423
+ *
424
+ * Auth-vendor-free by construction: token minting is INJECTED (`mintToken`), so
425
+ * this package never imports Clerk — the deployment wires a mint function that
426
+ * creates the signed-in user's personal API key and hands back the token
427
+ * (mirrors how the API's `authenticate` is injected). The command itself is the
428
+ * pure `composeConnectCommand`; this component is only the chrome around it.
429
+ */
430
+ interface ConnectClaudeCodeProps {
431
+ /** The hosted API root, baked into the emitted command. */
432
+ apiBaseUrl: string;
433
+ /** Override the token env-var name written into the config. */
434
+ tokenEnv?: string;
435
+ /**
436
+ * Mint a personal bearer token for the signed-in user. Injected by the
437
+ * deployment (e.g. create a Clerk API key whose subject is the user's ID).
438
+ */
439
+ mintToken: () => Promise<string>;
440
+ }
441
+ declare function ConnectClaudeCode({ apiBaseUrl, tokenEnv, mintToken, }: ConnectClaudeCodeProps): react.JSX.Element;
442
+
387
443
  interface RecordPageProps {
388
444
  /** The record being drilled into (`#record/<id>`). */
389
445
  recordId: string;
@@ -574,6 +630,75 @@ interface MovePresentation {
574
630
  /** The front-door copy + step-in modality for one act. */
575
631
  declare function movePresentation(kind: MoveKind): MovePresentation;
576
632
 
633
+ /**
634
+ * The per-belief cycles view-model (OPS-1347) — the validation loop's rounds,
635
+ * not just the belief's flat dated event log (`journey.ts`) or the
636
+ * strongest-push-first attribution list (`understanding.ts`). Grounded
637
+ * directly in the registry model (`registry-schema.md`):
638
+ *
639
+ * - a **cycle** is one round of the loop — an Experiment designed against
640
+ * this belief, the (dated) Readings it produced, and the per-belief
641
+ * bar-line verdict it settles at closure. That is literally
642
+ * "experiment → readings → re-score", the round the operator asked to
643
+ * see, and it is the only unit the data model pre-registers as a trial:
644
+ * the bar line's `We're right if` / `We're wrong if` are written *before*
645
+ * any Reading exists, so its verdict is a real round boundary, not one we
646
+ * invent;
647
+ * - readings with **no** Experiment (bare/found — a Market-rung commitment
648
+ * or desk research dropped in directly) carry no pre-registered bar, so
649
+ * they are not a "round" in that strict sense. They still move the
650
+ * number, so they collect into one closing, kind-`"direct"` entry rather
651
+ * than disappearing from the picture.
652
+ *
653
+ * Ordered chronologically (oldest first) — the point of this view is watching
654
+ * the belief move round by round, which `understanding.ts`'s magnitude-first
655
+ * ranking deliberately does not show.
656
+ *
657
+ * No new maths: each round's push on Confidence is read off
658
+ * `confidenceAttribution`'s per-experiment mover — the same decomposition
659
+ * `understanding.ts` shows, just regrouped by time instead of by size, so a
660
+ * cycle's push and the drawer's push always agree. Computed fresh on read,
661
+ * out of the OPS-1251 on-write recompute.
662
+ */
663
+
664
+ /** The round's key: the experiment id, or `"direct"` for the bare-reading bucket. */
665
+ declare const DIRECT_CYCLE_KEY = "direct";
666
+ /** One reading's place inside a round, reduced to what the timeline draws. */
667
+ interface CycleReadingView {
668
+ id: string;
669
+ date: string | null;
670
+ result: Result | null;
671
+ }
672
+ /** One round of the loop. */
673
+ interface CycleView {
674
+ key: string;
675
+ kind: "experiment" | "direct";
676
+ /** The experiment's title; null for the direct bucket. */
677
+ title: string | null;
678
+ /** The experiment's lifecycle status; null for the direct bucket. */
679
+ status: string | null;
680
+ /** The round's anchor date: the experiment's own `Date`, or (falling back)
681
+ * its earliest reading's date. Null only when neither is known. */
682
+ date: string | null;
683
+ /** This belief's bar-line verdict, once judged; null pre-closure and always
684
+ * null for the direct bucket (it carries no bar line). */
685
+ barVerdict: Result | null;
686
+ /** The round's readings, oldest first. */
687
+ readings: CycleReadingView[];
688
+ /** Signed push on Confidence this round contributed (sums, across every
689
+ * round, to the belief's Confidence). */
690
+ contribution: number;
691
+ /** |contribution| — how hard this round moved the number. */
692
+ magnitude: number;
693
+ }
694
+ /**
695
+ * Build one belief's cycles from its readings and the Experiments register.
696
+ * Both arrays are the raw records (Title-cased fields) — the same shape
697
+ * `understanding.ts`/`journey.ts` take — so a caller loading the registers
698
+ * once can hand them straight through.
699
+ */
700
+ declare function buildCycles(assumptionId: string, readings: AnyRecord[], experiments: AnyRecord[]): CycleView[];
701
+
577
702
  /** A journey event with its front-door copy attached. */
578
703
  interface JourneyEventView extends JourneyEvent {
579
704
  /** Plain-language label the story renders. */
@@ -587,6 +712,9 @@ interface JourneyView {
587
712
  stage: BeliefStage;
588
713
  /** The story: the belief's life, oldest first, `now` last. */
589
714
  events: JourneyEventView[];
715
+ /** The loop's rounds, oldest first (OPS-1347) — the same history the story
716
+ * tells, regrouped by Experiment run instead of by dated event. */
717
+ cycles: CycleView[];
590
718
  /** The ranked next move for this belief; null once it is resolved. */
591
719
  nextMove: NextMove | null;
592
720
  /** Killed (Invalidated) / moot / null — the drill-in's terminal state. */
@@ -626,7 +754,9 @@ declare function buildJourney(assumptionId: string, records: NextMoveRecords, no
626
754
  * rail is pure status: nothing to act on there;
627
755
  * - it **expands into the chronological story** through the record page's
628
756
  * existing "Why?" reveal idiom — the dated event log (bet → score →
629
- * experiment → readings → confidence-cross → now), ending in the ranked
757
+ * experiment → readings → confidence-cross → now), the same history
758
+ * regrouped into **rounds** (OPS-1347 — one card per Experiment run, plus
759
+ * any bare/direct evidence, oldest first), and ending in the ranked
630
760
  * next-move card (OPS-1292).
631
761
  *
632
762
  * **Step-in is story-only** (OPS-1297): the next-move card and the per-event
@@ -839,6 +969,10 @@ interface FieldEditor {
839
969
  options?: readonly string[];
840
970
  /** An empty input clears the field to `null` rather than "". */
841
971
  nullable?: boolean;
972
+ /** For `number`: the inclusive range a hand-scored value must sit in
973
+ * (e.g. Impact 0–100, `registry-schema.md`). Omitted = unbounded. */
974
+ min?: number;
975
+ max?: number;
842
976
  }
843
977
  /** The fields a register lets you edit, in render order. */
844
978
  declare function editableFields(register: Collection): FieldEditor[];
@@ -865,15 +999,21 @@ declare function hasEdits(register: Collection, original: AnyRecord, draft: Draf
865
999
  * surfaces editing one register should never drift into two different forms.
866
1000
  * Derived numbers are never here — they are computed on write (OPS-1251).
867
1001
  */
868
- declare function EditFields({ register, draft, onField, }: {
1002
+ declare function EditFields({ register, draft, errors, onField, }: {
869
1003
  register: Collection;
870
1004
  draft: Draft;
1005
+ /** Per-field validation messages (keyed by field), e.g. Impact out of
1006
+ * 0–100 — `edit.ts`'s `draftErrors`. Absent/empty = every field is valid. */
1007
+ errors?: Record<string, string>;
871
1008
  onField: (key: string, value: string) => void;
872
1009
  }): react.JSX.Element;
873
1010
  /** One editable field, as the control its `kind` calls for. */
874
- declare function FieldInput({ field, value, onChange, }: {
1011
+ declare function FieldInput({ field, value, error, onChange, }: {
875
1012
  field: FieldEditor;
876
1013
  value: string | undefined;
1014
+ /** A validation message shown under the control, e.g. "Impact must be at
1015
+ * most 100." — never blocks typing, only the Save the caller gates on it. */
1016
+ error?: string;
877
1017
  onChange: (value: string) => void;
878
1018
  }): react.JSX.Element;
879
1019
 
@@ -902,6 +1042,9 @@ interface RegisterTableProps {
902
1042
  onRowClick?: (id: string) => void;
903
1043
  /** The id of the currently-open record, highlighted in the list. */
904
1044
  selectedId?: string | null;
1045
+ /** Assumption id → title, so a reading row's belief chips read as titles
1046
+ * rather than ids (OPS-1305). Omitted → chips fall back to the bare ids. */
1047
+ assumptionTitles?: Map<string, string>;
905
1048
  }
906
1049
  /**
907
1050
  * A list table for one register — a row per record, the register's key fields
@@ -911,7 +1054,139 @@ interface RegisterTableProps {
911
1054
  * the treatment stays testable at the columns seam and this component stays a
912
1055
  * dumb renderer. Presentational: the caller supplies the rows.
913
1056
  */
914
- declare function RegisterTable({ register, records, onRowClick, selectedId, }: RegisterTableProps): react.JSX.Element;
1057
+ declare function RegisterTable({ register, records, onRowClick, selectedId, assumptionTitles, }: RegisterTableProps): react.JSX.Element;
1058
+
1059
+ /**
1060
+ * The record-page view-model (OPS-1286) — the pure join behind the canonical
1061
+ * full record page. Given a record and the related registers, it computes the
1062
+ * header lane/queue pills (all derived), the leading-score meters per register,
1063
+ * the genuine human-input free-text remainder, and the backlink panels grouped
1064
+ * by relation (each row carrying a glance-readable score chip, an empty relation
1065
+ * kept as a "none yet" panel rather than dropped). DOM-free and unit-tested at
1066
+ * this seam; `RecordPage` renders what it returns and the understanding layer
1067
+ * supplies the "Why?" attribution unchanged.
1068
+ */
1069
+
1070
+ /** The registers a record page reads to resolve its relations. All optional —
1071
+ * a panel whose register is absent simply resolves to "none yet". */
1072
+ interface RelatedSet {
1073
+ assumptions?: AnyRecord[];
1074
+ experiments?: AnyRecord[];
1075
+ readings?: AnyRecord[];
1076
+ decisions?: AnyRecord[];
1077
+ glossary?: AnyRecord[];
1078
+ }
1079
+ interface RecordPageOptions {
1080
+ /** ISO date "now" for the Overdue pill; omitted → nothing reads overdue. */
1081
+ asOf?: string;
1082
+ }
1083
+ /** A header pill: a short derived label toned by meaning. */
1084
+ interface Pill {
1085
+ label: string;
1086
+ tone: Tone;
1087
+ }
1088
+ /**
1089
+ * The derived lane/queue pills for a record header (story 2). Every register
1090
+ * leads with its Status pill; assumptions add Moot / Kill lane / Testing,
1091
+ * experiments add Concluded / Overdue, decisions mark Standing. All derived —
1092
+ * none is a stored field.
1093
+ */
1094
+ declare function headerPills(register: Collection, record: AnyRecord, related?: RelatedSet, options?: RecordPageOptions): Pill[];
1095
+ /** A leading-score meter. `bar`/`signed` carry a number in a min…max domain;
1096
+ * `pill` carries a categorical value shown as a toned pill. */
1097
+ interface Meter {
1098
+ key: string;
1099
+ label: string;
1100
+ kind: "bar" | "signed" | "pill";
1101
+ /** Number (bar/signed) or category (pill); null when the record has no value. */
1102
+ value: number | string | null;
1103
+ tone: Tone;
1104
+ /** For bar/signed: the domain the fill maps onto. */
1105
+ min?: number;
1106
+ max?: number;
1107
+ /** True when a "Why?" attribution is available (Confidence only). */
1108
+ hasWhy?: boolean;
1109
+ }
1110
+ /**
1111
+ * The register's leading scores as meters (story 4). Reads only what the
1112
+ * migrated schema actually stores — a derived number becomes a bar/signed meter,
1113
+ * a categorical leading field becomes a pill; nothing is invented where the
1114
+ * schema carries no number. Confidence is flagged `hasWhy` — the understanding
1115
+ * layer decomposes it.
1116
+ */
1117
+ declare function leadingMeters(register: Collection, record: AnyRecord): Meter[];
1118
+ /** The genuine human-input free-text remainder — clearly separated from what
1119
+ * the system computed. Auto-linked against the glossary at render time. */
1120
+ interface HumanText {
1121
+ key: string;
1122
+ label: string;
1123
+ text: string;
1124
+ }
1125
+ /** The human-input fields a register carries text for (empty ones dropped). */
1126
+ declare function humanInputFields(register: Collection, record: AnyRecord): HumanText[];
1127
+ /** One belief a reading grades, prepared for the reading detail's verdict list
1128
+ * (OPS-1305). Modelled on the experiment bar-line view: the assumption resolved
1129
+ * to a title + navigable id, plus this belief's own Rung / Result / derived
1130
+ * Strength / magnitude band and the grading justification. */
1131
+ interface BeliefVerdict {
1132
+ assumptionId: string;
1133
+ /** The belief's title if it's in the loaded set, else its bare id. */
1134
+ title: string;
1135
+ /** True when the assumption resolved — drives whether the title links. */
1136
+ linked: boolean;
1137
+ rung: Rung | null;
1138
+ result: Result | null;
1139
+ /** Derived per-belief strength (signed −100…100). */
1140
+ strength: number | null;
1141
+ magnitudeBand: MagnitudeBand | null;
1142
+ justification: string;
1143
+ }
1144
+ /** The per-belief verdicts a reading carries, in stored order — the reading
1145
+ * detail's answer to "what did this artifact say about each belief?". Pure:
1146
+ * resolves each belief-score against the loaded assumptions for its title. */
1147
+ declare function readingBeliefVerdicts(reading: AnyRecord, assumptions?: AnyRecord[]): BeliefVerdict[];
1148
+ /** A glance-readable score chip for a linked record (story 9). */
1149
+ interface ScoreChip {
1150
+ label: string;
1151
+ value: string;
1152
+ tone: Tone;
1153
+ }
1154
+ interface BacklinkItem {
1155
+ id: string;
1156
+ register: Collection;
1157
+ title: string;
1158
+ chip: ScoreChip;
1159
+ }
1160
+ /** A relation panel — the inbound/outbound edges of one relation, grouped and
1161
+ * labelled. Kept even when empty (story 10). */
1162
+ interface RelationPanel {
1163
+ id: string;
1164
+ label: string;
1165
+ register: Collection;
1166
+ items: BacklinkItem[];
1167
+ }
1168
+ /** The linked record's headline score, glance-readable (story 9). */
1169
+ declare function scoreChip(register: Collection, record: AnyRecord): ScoreChip;
1170
+ /** The backlink panels for a record, grouped by relation, each row carrying a
1171
+ * score chip. Empty relations are kept (items: []) so a missing connection reads
1172
+ * as "none yet", not an absent section (story 8/10). */
1173
+ declare function backlinkPanels(register: Collection, record: AnyRecord, related?: RelatedSet): RelationPanel[];
1174
+ type RecordTabId = "overview" | "evidence" | "connections" | "history";
1175
+ interface RecordPageModel {
1176
+ register: Collection;
1177
+ title: string;
1178
+ pills: Pill[];
1179
+ meters: Meter[];
1180
+ humanText: HumanText[];
1181
+ panels: RelationPanel[];
1182
+ /** The tabs this record shows, in order (story 3 + 11). */
1183
+ tabs: RecordTabId[];
1184
+ /** True when the belief journey drill-in can mount here (story 13). */
1185
+ hasJourney: boolean;
1186
+ }
1187
+ /** Assemble the whole record-page model. Pure: same record + related always
1188
+ * gives the same page. */
1189
+ declare function buildRecordPage(register: Collection, record: AnyRecord, related?: RelatedSet, options?: RecordPageOptions): RecordPageModel;
915
1190
 
916
1191
  interface RecordDrawerProps {
917
1192
  register: Collection;
@@ -931,6 +1206,17 @@ interface RecordDrawerProps {
931
1206
  /** Open this record's canonical full page (story 12). When set, the header
932
1207
  * shows a "Full page" link; the drawer stays the quick read/edit peek. */
933
1208
  onOpenFull?: () => void;
1209
+ /**
1210
+ * Open *any* linked record's full page (OPS-1345) — the same navigation the
1211
+ * canonical record page's Connections tab uses, reused here so a relation
1212
+ * field or a bar line's assumption is a click, not inert text. Falls back
1213
+ * to plain (unclickable) titles when omitted.
1214
+ */
1215
+ onOpenRecord?: (id: string) => void;
1216
+ /** The other registers' rows, loaded so a relation field / bar line can
1217
+ * resolve to a title instead of a raw id (OPS-1345). Omitted relations
1218
+ * simply fall back to showing the id. */
1219
+ related?: RelatedSet;
934
1220
  /** Extra content below the fields in read mode — e.g. the relation editor. */
935
1221
  children?: ReactNode;
936
1222
  }
@@ -945,7 +1231,7 @@ interface RecordDrawerProps {
945
1231
  * the relation editor (`children`). Chrome is shared via `DrawerShell`; styled
946
1232
  * with the package's own token sheet, no host Tailwind.
947
1233
  */
948
- declare function RecordDrawer({ register, record, loading, error, open, onClose, basePath, onChanged, onOpenFull, children, }: RecordDrawerProps): react.JSX.Element;
1234
+ declare function RecordDrawer({ register, record, loading, error, open, onClose, basePath, onChanged, onOpenFull, onOpenRecord, related, children, }: RecordDrawerProps): react.JSX.Element;
949
1235
 
950
1236
  interface RecordFormProps {
951
1237
  register: Collection;
@@ -1003,7 +1289,10 @@ declare function UnderstandingPanel({ assumption, basePath, }: {
1003
1289
  * also moves the number, and the Confidence-over-time trajectory.
1004
1290
  *
1005
1291
  * The record → derivation-input mapping is `@validation-os/core`'s shared
1006
- * `toReadingInput`, so a reading is read here exactly as it is server-side.
1292
+ * `readingBeliefInputs`, which fans a reading row out into one input per belief;
1293
+ * we keep this belief's inputs, so a reading is read here exactly as it is
1294
+ * server-side. Archived experiments never surface here (OPS-1305) — the "Why?"
1295
+ * only ever shows a live plan or direct evidence.
1007
1296
  */
1008
1297
 
1009
1298
  /** An experiment testing this assumption: how hard it moves Confidence, and
@@ -1277,6 +1566,14 @@ interface ColumnDef {
1277
1566
  }
1278
1567
  /** The columns to render for a register. */
1279
1568
  declare function columnsFor(register: Collection): ColumnDef[];
1569
+ /** A one-line, length-capped preview of a reading's free-text `body` (its
1570
+ * quote), for the readings table. Collapses whitespace and ellipsises; a
1571
+ * missing/empty body reads as the empty string (formatted to an em dash). */
1572
+ declare function bodyPreview(value: unknown, max?: number): string;
1573
+ /** The belief chips for a reading row — the assumptions it grades, resolved to
1574
+ * titles when a lookup is given, else their bare ids. Disambiguates readings
1575
+ * that share a Title by showing which belief(s) each one actually scored. */
1576
+ declare function readingAssumptionChips(record: AnyRecord, titleById?: Map<string, string>): string[];
1280
1577
  /** Read a column's raw value from a record (accessor, else `record[key]`). */
1281
1578
  declare function cellValue(column: ColumnDef, record: AnyRecord): unknown;
1282
1579
  /** Format any stored value for display; empty/missing reads as an em dash. */
@@ -1284,6 +1581,17 @@ declare function formatValue(value: unknown): string;
1284
1581
  /** The record's headline for a row/drawer: Title, else Name, else its id. */
1285
1582
  declare function primaryLabel(record: AnyRecord): string;
1286
1583
 
1584
+ /** Render Markdown `text` into the package's prose styling. Empty → nothing. */
1585
+ declare function Markdown({ text }: {
1586
+ text: string;
1587
+ }): react.JSX.Element | null;
1588
+
1589
+ declare function BeliefVerdicts({ reading, assumptions, onOpenRecord, }: {
1590
+ reading: AnyRecord;
1591
+ assumptions: AnyRecord[];
1592
+ onOpenRecord?: (id: string) => void;
1593
+ }): react.JSX.Element;
1594
+
1287
1595
  /**
1288
1596
  * The editable-field spec for the "new record" form, per register — kept as
1289
1597
  * pure data + functions (like `columns.ts`) so the create form is unit-testable
@@ -1418,115 +1726,62 @@ interface GlossaryTextProps {
1418
1726
  declare function GlossaryText({ text, terms, selfId, onOpenTerm, }: GlossaryTextProps): react.JSX.Element;
1419
1727
 
1420
1728
  /**
1421
- * The record-page view-model (OPS-1286) — the pure join behind the canonical
1422
- * full record page. Given a record and the related registers, it computes the
1423
- * header lane/queue pills (all derived), the leading-score meters per register,
1424
- * the genuine human-input free-text remainder, and the backlink panels grouped
1425
- * by relation (each row carrying a glance-readable score chip, an empty relation
1426
- * kept as a "none yet" panel rather than dropped). DOM-free and unit-tested at
1427
- * this seam; `RecordPage` renders what it returns and the understanding layer
1428
- * supplies the "Why?" attribution unchanged.
1729
+ * The record drawer's generic field list (OPS-1345) — the pure view-model
1730
+ * behind the "everything else" rows a record carries beyond its meters/
1731
+ * human-text/panels. `RecordDrawer` iterates a record's own keys (skipping
1732
+ * provenance/meta), and this module decides how each one reads: a relation
1733
+ * id/id-list (`Depends on`, `Readings`, `Assumption`…) resolves to the linked
1734
+ * record's title, `Owner`/`Agreed by` (stored as dashboard-user objects,
1735
+ * `{id, name}` `connectors/nosql-schema.md`) reads as name(s) only, and
1736
+ * `barLines` (the embedded per-belief pre-registration on an experiment)
1737
+ * reads as structured rows with a linked assumption title — never the raw
1738
+ * stored JSON or an internal id, per this project's "explain visually, hide
1739
+ * complexity" bias. Everything else still formats through `columns.ts`.
1740
+ * DOM-free and unit-tested at this seam; `record-drawer.tsx` renders what it
1741
+ * returns.
1429
1742
  */
1430
1743
 
1431
- /** The registers a record page reads to resolve its relations. All optional —
1432
- * a panel whose register is absent simply resolves to "none yet". */
1433
- interface RelatedSet {
1434
- assumptions?: AnyRecord[];
1435
- experiments?: AnyRecord[];
1436
- readings?: AnyRecord[];
1437
- decisions?: AnyRecord[];
1438
- glossary?: AnyRecord[];
1439
- }
1440
- interface RecordPageOptions {
1441
- /** ISO date "now" for the Overdue pill; omitted → nothing reads overdue. */
1442
- asOf?: string;
1443
- }
1444
- /** A header pill: a short derived label toned by meaning. */
1445
- interface Pill {
1446
- label: string;
1447
- tone: Tone;
1448
- }
1449
- /**
1450
- * The derived lane/queue pills for a record header (story 2). Every register
1451
- * leads with its Status pill; assumptions add Moot / Kill lane / Testing,
1452
- * experiments add Concluded / Overdue, decisions mark Standing. All derived —
1453
- * none is a stored field.
1454
- */
1455
- declare function headerPills(register: Collection, record: AnyRecord, related?: RelatedSet, options?: RecordPageOptions): Pill[];
1456
- /** A leading-score meter. `bar`/`signed` carry a number in a min…max domain;
1457
- * `pill` carries a categorical value shown as a toned pill. */
1458
- interface Meter {
1459
- key: string;
1460
- label: string;
1461
- kind: "bar" | "signed" | "pill";
1462
- /** Number (bar/signed) or category (pill); null when the record has no value. */
1463
- value: number | string | null;
1464
- tone: Tone;
1465
- /** For bar/signed: the domain the fill maps onto. */
1466
- min?: number;
1467
- max?: number;
1468
- /** True when a "Why?" attribution is available (Confidence only). */
1469
- hasWhy?: boolean;
1470
- }
1471
- /**
1472
- * The register's leading scores as meters (story 4). Reads only what the
1473
- * migrated schema actually stores — a derived number becomes a bar/signed meter,
1474
- * a categorical leading field becomes a pill; nothing is invented where the
1475
- * schema carries no number. Confidence is flagged `hasWhy` — the understanding
1476
- * layer decomposes it.
1477
- */
1478
- declare function leadingMeters(register: Collection, record: AnyRecord): Meter[];
1479
- /** The genuine human-input free-text remainder — clearly separated from what
1480
- * the system computed. Auto-linked against the glossary at render time. */
1481
- interface HumanText {
1482
- key: string;
1483
- label: string;
1484
- text: string;
1485
- }
1486
- /** The human-input fields a register carries text for (empty ones dropped). */
1487
- declare function humanInputFields(register: Collection, record: AnyRecord): HumanText[];
1488
- /** A glance-readable score chip for a linked record (story 9). */
1489
- interface ScoreChip {
1490
- label: string;
1491
- value: string;
1492
- tone: Tone;
1493
- }
1494
- interface BacklinkItem {
1744
+ type DetailRowKind = "text" | "relation" | "owner" | "bar-lines";
1745
+ /** One resolved relation target a title to show, an id to navigate to. */
1746
+ interface DetailRelationItem {
1495
1747
  id: string;
1496
1748
  register: Collection;
1497
1749
  title: string;
1498
- chip: ScoreChip;
1499
1750
  }
1500
- /** A relation panel the inbound/outbound edges of one relation, grouped and
1501
- * labelled. Kept even when empty (story 10). */
1502
- interface RelationPanel {
1503
- id: string;
1504
- label: string;
1505
- register: Collection;
1506
- items: BacklinkItem[];
1751
+ /** One bar line, human-readable: the belief it tests resolved to a title. */
1752
+ interface ResolvedBarLine {
1753
+ rightIf: string;
1754
+ wrongIf: string | null;
1755
+ plannedRung: string;
1756
+ barVerdict: string | null;
1757
+ assumption: DetailRelationItem | null;
1507
1758
  }
1508
- /** The linked record's headline score, glance-readable (story 9). */
1509
- declare function scoreChip(register: Collection, record: AnyRecord): ScoreChip;
1510
- /** The backlink panels for a record, grouped by relation, each row carrying a
1511
- * score chip. Empty relations are kept (items: []) so a missing connection reads
1512
- * as "none yet", not an absent section (story 8/10). */
1513
- declare function backlinkPanels(register: Collection, record: AnyRecord, related?: RelatedSet): RelationPanel[];
1514
- type RecordTabId = "overview" | "evidence" | "connections" | "history";
1515
- interface RecordPageModel {
1516
- register: Collection;
1517
- title: string;
1518
- pills: Pill[];
1519
- meters: Meter[];
1520
- humanText: HumanText[];
1521
- panels: RelationPanel[];
1522
- /** The tabs this record shows, in order (story 3 + 11). */
1523
- tabs: RecordTabId[];
1524
- /** True when the belief journey drill-in can mount here (story 13). */
1525
- hasJourney: boolean;
1526
- }
1527
- /** Assemble the whole record-page model. Pure: same record + related always
1528
- * gives the same page. */
1529
- declare function buildRecordPage(register: Collection, record: AnyRecord, related?: RelatedSet, options?: RecordPageOptions): RecordPageModel;
1759
+ interface DetailRow {
1760
+ key: string;
1761
+ label: string;
1762
+ kind: DetailRowKind;
1763
+ /** `kind: "text"` the formatted display string. */
1764
+ text?: string;
1765
+ /** `kind: "relation"` the resolved link targets, in stored order. */
1766
+ items?: DetailRelationItem[];
1767
+ /** `kind: "owner"` — the dashboard-user name(s), never the id/object. */
1768
+ names?: string[];
1769
+ /** `kind: "bar-lines"` — the embedded pre-registration, resolved. */
1770
+ bars?: ResolvedBarLine[];
1771
+ }
1772
+ /** The dashboard-user name(s) off an `Owner`/`Agreed by` value — the stored
1773
+ * shape is `{id, name}[]`; a bare string is tolerated as a fallback. */
1774
+ declare function ownerNames(value: unknown): string[];
1775
+ /** Bar lines resolved against the loaded assumptions shared by the drawer's
1776
+ * generic list and the record page's Evidence tab so the two never drift. */
1777
+ declare function resolveBarLines(bars: BarLine[], related: RelatedSet): ResolvedBarLine[];
1778
+ /**
1779
+ * The record's own fields, beyond its meta/derived tuple, as rows a drawer
1780
+ * can render directly relation fields resolved to a title + navigate
1781
+ * target, `Owner`/`Agreed by` to plain names, `barLines` to structured rows.
1782
+ * Everything else still formats through `formatValue` (spec stories 1–3).
1783
+ */
1784
+ declare function detailRows(register: Collection, record: AnyRecord, related?: RelatedSet): DetailRow[];
1530
1785
 
1531
1786
  /** Plain-language labels — the register is a surface a non-technical
1532
1787
  * teammate meets, so no jargon and no code-y plurals. */
@@ -1548,4 +1803,4 @@ declare const REGISTER_GROUPS: {
1548
1803
  registers: Collection[];
1549
1804
  }[];
1550
1805
 
1551
- export { type BacklinkItem, BeliefJourney, type BeliefJourneyProps, CONFLICT_MESSAGE, type CellKind, type ColdStart, type ColumnDef, ConfidenceCell, type Counts, type DashboardConfig, type Draft, EditBeliefForm, type EditBeliefFormProps, EditFields, type ExperimentView, FIRST_RUN_LINE, type FieldEditor, FieldInput, type FieldKind, type FormField, type GlossaryTerm, GlossaryText, type GlossaryTextProps, type GroupBucket, type GroupByAxis, type HumanText, type JourneyColdState, type JourneyEventView, type JourneyView, type LinkArgs, type LinkChoice, type LinkifyNode, type LinkifyOptions, type Meter, type MeterKind, type MovePresentation, type NeedsHumanByRegister, type NeedsHumanCounts, type NeighbourChip, type NestedGroup, type NextColdStart, type NextMoveRecords, NextMoveSurface, type NextMoveSurfaceProps, type OtherMover, type Pill, type PipelineColdStart, type PipelineRow, PipelineSurface, type PipelineView, REGISTER_GROUPS, REGISTER_ICON, REGISTER_LABEL, REGISTER_ORDER, REGISTER_SINGULAR, REGISTER_SUBTITLE, RISK_CRIT, RISK_WARN, RecordDrawer, type RecordDrawerProps, RecordForm, type RecordFormProps, RecordPage, type RecordPageModel, type RecordPageProps, type RecordTabId, RegisterBrowser, type RegisterBrowserProps, type RegisterContext, RegisterCounts, type RegisterCountsProps, RegisterTable, type RegisterTableProps, type RelatedSet, RelationEditor, type RelationEditorProps, type RelationPanel, type ResolvedRow, type RiskBand, RiskBar, type Route, type SaveResult, type SavedView, type ScoreChip, ScoreImpactForm, type ScoreImpactFormProps, type ShapedRegister, SidebarNav, type SidebarNavProps, type SortSpec, Sparkline, type StageMeterInput, type StageMeterView, StatTile, StatusPill, type StepInForm, type StoryStepIn, SurfacePlaceholder, type SurfacePlaceholderProps, type TabDef, type TermPreview, type Tone, type Understanding, UnderstandingPanel, type UseCountsResult, type UseCreateResult, type UseLinkResult, type UseListResult, type UseNeedsHumanResult, type UseRecordResult, type UseSavedViewsResult, type UseUpdateResult, ValidationOSDashboard, type ValidationOSDashboardProps, type ViewDescriptor, WriteDecisionForm, type WriteDecisionFormProps, backlinkPanels, buildJourney, buildPatch, buildPipeline, buildRecordPage, buildUnderstanding, cellValue, coldStartFor, columnsFor, confidenceTone, configureRiskBands, defaultTabId, dontConfuseWith, draftFrom, editableFields, emptyDraft, eventStepIn, eventTone, filterRecords, formFieldsFor, formatCount, formatRoute, formatSigned, formatValue, groupByAxesFor, groupRecords, hasEdits, headerPills, humanInputFields, interpretSave, journeyColdState, leadingMeters, linkChoicesFrom, linkify, missingRequired, movePresentation, needsHumanCounts, nestReadingsByPlan, parseRoute, primaryLabel, riskBand, riskFraction, riskLevel, scoreChip, shapeRegister, sortRecords, sparklinePath, stageMeters, statusTone, tabsFor, toCreatePayload, toGlossaryTerms, toNextMoveInput, toStageExperimentInput, useCounts, useCreate, useLink, useList, useNeedsHuman, useRecord, useSavedViews, useUpdate, weekOverWeekDelta };
1806
+ export { type BacklinkItem, BeliefJourney, type BeliefJourneyProps, type BeliefVerdict, BeliefVerdicts, CONFLICT_MESSAGE, type CellKind, type ColdStart, type ColumnDef, ConfidenceCell, ConnectClaudeCode, type ConnectClaudeCodeProps, type ConnectCommandInput, type Counts, type CycleReadingView, type CycleView, DEFAULT_TOKEN_ENV, DIRECT_CYCLE_KEY, type DashboardConfig, type DetailRelationItem, type DetailRow, type DetailRowKind, type Draft, EditBeliefForm, type EditBeliefFormProps, EditFields, type ExperimentView, FIRST_RUN_LINE, type FieldEditor, FieldInput, type FieldKind, type FormField, type GlossaryTerm, GlossaryText, type GlossaryTextProps, type GroupBucket, type GroupByAxis, type HumanText, type JourneyColdState, type JourneyEventView, type JourneyView, type LinkArgs, type LinkChoice, type LinkifyNode, type LinkifyOptions, Markdown, type Meter, type MeterKind, type MovePresentation, type NeedsHumanByRegister, type NeedsHumanCounts, type NeighbourChip, type NestedGroup, type NextColdStart, type NextMoveRecords, NextMoveSurface, type NextMoveSurfaceProps, type OtherMover, type Pill, type PipelineColdStart, type PipelineRow, PipelineSurface, type PipelineView, REGISTER_GROUPS, REGISTER_ICON, REGISTER_LABEL, REGISTER_ORDER, REGISTER_SINGULAR, REGISTER_SUBTITLE, RISK_CRIT, RISK_WARN, RecordDrawer, type RecordDrawerProps, RecordForm, type RecordFormProps, RecordPage, type RecordPageModel, type RecordPageProps, type RecordTabId, RegisterBrowser, type RegisterBrowserProps, type RegisterContext, RegisterCounts, type RegisterCountsProps, RegisterTable, type RegisterTableProps, type RelatedSet, RelationEditor, type RelationEditorProps, type RelationPanel, type ResolvedBarLine, type ResolvedRow, type RiskBand, RiskBar, type Route, type SaveResult, type SavedView, type ScoreChip, ScoreImpactForm, type ScoreImpactFormProps, type ShapedRegister, SidebarNav, type SidebarNavProps, type SortSpec, Sparkline, type StageMeterInput, type StageMeterView, StatTile, StatusPill, type StepInForm, type StoryStepIn, SurfacePlaceholder, type SurfacePlaceholderProps, type TabDef, type TermPreview, type Tone, type Understanding, UnderstandingPanel, type UseCountsResult, type UseCreateResult, type UseLinkResult, type UseListResult, type UseNeedsHumanResult, type UseRecordResult, type UseSavedViewsResult, type UseUpdateResult, ValidationOSDashboard, type ValidationOSDashboardProps, type ViewDescriptor, WriteDecisionForm, type WriteDecisionFormProps, backlinkPanels, bodyPreview, buildCycles, buildJourney, buildPatch, buildPipeline, buildRecordPage, buildUnderstanding, cellValue, coldStartFor, columnsFor, composeConnectCommand, confidenceTone, configureRiskBands, defaultTabId, detailRows, dontConfuseWith, draftFrom, editableFields, emptyDraft, eventStepIn, eventTone, filterRecords, formFieldsFor, formatCount, formatRoute, formatSigned, formatValue, groupByAxesFor, groupRecords, hasEdits, headerPills, humanInputFields, interpretSave, journeyColdState, leadingMeters, linkChoicesFrom, linkify, missingRequired, movePresentation, needsHumanCounts, nestReadingsByPlan, ownerNames, parseRoute, primaryLabel, readingAssumptionChips, readingBeliefVerdicts, resolveBarLines, riskBand, riskFraction, riskLevel, scoreChip, shapeRegister, sortRecords, sparklinePath, stageMeters, statusTone, tabsFor, toCreatePayload, toGlossaryTerms, toNextMoveInput, toStageExperimentInput, useCounts, useCreate, useLink, useList, useNeedsHuman, useRecord, useSavedViews, useUpdate, weekOverWeekDelta };