@validation-os/dashboard 0.4.0 → 0.6.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,7 +1,334 @@
1
1
  import * as react from 'react';
2
2
  import { ReactNode } from 'react';
3
3
  import { Collection, AnyRecord, Relation } from '@validation-os/core';
4
- import { Progress, MoverKind, TrajectoryPoint } from '@validation-os/core/derivation';
4
+ import { PortfolioProgress, StageExperimentInput, MoveKind, NextMoveInput, JourneyEvent, BeliefStage, NextMove, Progress, MoverKind, TrajectoryPoint } from '@validation-os/core/derivation';
5
+ export { StageKey } from '@validation-os/core/derivation';
6
+
7
+ /**
8
+ * Everything the instance passes — config only, never secrets. The API base
9
+ * path points at where the Clerk-gated `@validation-os/api` routes are mounted;
10
+ * the backend label (from the connector config) shows in the topbar; branding
11
+ * and the signed-in user are optional cosmetics. Auth is the instance's Clerk
12
+ * provider that this renders within — no credential reaches the package.
13
+ */
14
+ interface DashboardConfig {
15
+ /** Where the API is mounted (default `/api`). */
16
+ basePath?: string;
17
+ /** Topbar backend indicator, e.g. "Firestore · doshi-crm". */
18
+ backendLabel?: string;
19
+ /** Optional product branding for the sidebar. */
20
+ branding?: {
21
+ /** Product name shown next to the mark (default "Validation-OS"). */
22
+ name?: string;
23
+ /** One or two letters for the square mark (default "V"). */
24
+ initials?: string;
25
+ /** A logo image for the square mark; overrides `initials` when set. */
26
+ logoUrl?: string;
27
+ };
28
+ /** Optional agent label shown in the topbar, e.g. "Claude Code". */
29
+ agentLabel?: string;
30
+ /** The signed-in user, for the topbar avatar + name. */
31
+ user?: {
32
+ name?: string;
33
+ caption?: string;
34
+ };
35
+ /** Restrict/reorder the registers shown; defaults to all, in order. */
36
+ registers?: Collection[];
37
+ }
38
+ interface ValidationOSDashboardProps {
39
+ config?: DashboardConfig;
40
+ }
41
+ /**
42
+ * The entire styled dashboard as one mountable app (spec OPS-1280): the frame —
43
+ * sidebar composing the workflow + register nav with live counts, topbar with
44
+ * the backend indicator and user — and the surfaces it routes between across
45
+ * the three altitudes (front door → pipeline → per-belief drill-in) plus the
46
+ * kept register tables. Navigation is owned here, not the host router: the
47
+ * active route lives in client state, synced to the URL hash (OPS-1298), so the
48
+ * instance mounts this at one route and wires no routing. Styled by the
49
+ * package's own token sheet — the instance imports `styles.css` once and builds
50
+ * no UI.
51
+ *
52
+ * The front-door (`#next`) and pipeline (`#pipeline`) surfaces are now live;
53
+ * OPS-1282's record page (`#record/<id>`) still fills its pane as it ships.
54
+ * Records is the browse-everything / manual-override surface — the register
55
+ * browser, kept from the original scheme.
56
+ */
57
+ declare function ValidationOSDashboard({ config }: ValidationOSDashboardProps): react.JSX.Element;
58
+
59
+ /**
60
+ * Pure presentation logic for the dashboard's visual primitives — status→tone,
61
+ * risk→fraction/level, the sparkline path, and count/number formatting. Kept
62
+ * free of React and the DOM so the mapping that drives every pill, bar and
63
+ * sparkline is unit-tested at this seam (spec story 13), and the components stay
64
+ * thin wrappers over it.
65
+ */
66
+ /** A pill/fill tone — maps onto the `--vos-good/warn/crit/accent` tokens. */
67
+ type Tone = "good" | "warn" | "crit" | "accent" | "neutral";
68
+ /**
69
+ * A record's Status → a pill tone. Live/concluded read good, testing/running
70
+ * read warn, invalidated/rejected read crit; everything else (Proposed, and any
71
+ * status we don't recognise) stays neutral rather than guessing a colour. Case-
72
+ * and whitespace-insensitive.
73
+ */
74
+ declare function statusTone(status: string | null | undefined): Tone;
75
+ /**
76
+ * Risk band thresholds — Critical ≥ 70, High 40–69, Watch < 40 (OPS-1287). A
77
+ * belief is critical at ≥ `RISK_CRIT`, watch/high at ≥ `RISK_WARN`. These are a
78
+ * prioritisation setting (a config knob), not a record property; overriding them
79
+ * is `configureRiskBands`. Kept dashboard-wide so the flat-table risk cell, the
80
+ * group-by risk-band axis, and the hero meter all band a number the same way.
81
+ */
82
+ declare let RISK_CRIT: number;
83
+ declare let RISK_WARN: number;
84
+ /** The three fixed risk bands, strongest first (OPS-1287 story 19). */
85
+ type RiskBand = "Critical" | "High" | "Watch";
86
+ /**
87
+ * Override the two risk-band thresholds (the config knob). Left as a setter over
88
+ * module state rather than threaded through every call, so an instance can retune
89
+ * the bands once at startup and every risk rendering follows.
90
+ */
91
+ declare function configureRiskBands(crit: number, warn: number): void;
92
+ /** Risk (0–100) → its band label. Grouping uses this; sort stays continuous. */
93
+ declare function riskBand(risk: number): RiskBand;
94
+ /** Risk (0–100) → a tone for the bar fill and the number. */
95
+ declare function riskLevel(risk: number): Tone;
96
+ /** Risk (0–100) → the bar's fill fraction (0–1), clamped. */
97
+ declare function riskFraction(risk: number): number;
98
+ /** Confidence (signed) → the tone for its number: negative reads crit. */
99
+ declare function confidenceTone(confidence: number): Tone;
100
+ /** A signed number rendered with an explicit sign: `+6`, `-3`, `0`. */
101
+ declare function formatSigned(n: number): string;
102
+ /** A count for the nav/tiles, thousands-separated. */
103
+ declare function formatCount(n: number): string;
104
+ /**
105
+ * An SVG polyline `d` for a sparkline over `values`, fit to a `width`×`height`
106
+ * box with a 2px inset. The vertical domain defaults to the data's own range
107
+ * (with 0 always included, so a signed series keeps its baseline meaningful);
108
+ * pass `min`/`max` to pin it — e.g. −100…100 for Confidence. Returns "" for
109
+ * fewer than two points (nothing to draw).
110
+ */
111
+ declare function sparklinePath(values: number[], width: number, height: number, min?: number, max?: number): string;
112
+
113
+ /** A colored status pill — tone from the status, the label shown verbatim. */
114
+ declare function StatusPill({ status }: {
115
+ status: string | null | undefined;
116
+ }): react.JSX.Element;
117
+ /**
118
+ * A risk bar (0–100) plus its number, both toned by threshold: a longer, redder
119
+ * bar means a riskier belief the eye should land on first (spec story 5).
120
+ */
121
+ declare function RiskBar({ risk }: {
122
+ risk: number;
123
+ }): react.JSX.Element;
124
+ /**
125
+ * A signed confidence reading: a tiny sparkline of its trajectory when a
126
+ * history is available, always the +/- number (negative reads crit) — so a row
127
+ * shows both where a belief stands and, when known, which way it is trending
128
+ * (spec story 6). With no history the number stands alone; the list endpoint
129
+ * carries no per-row series, so the in-row sparkline appears wherever a caller
130
+ * does have one (e.g. the drawer trajectory uses `Sparkline` directly).
131
+ */
132
+ declare function ConfidenceCell({ confidence, history, }: {
133
+ confidence: number;
134
+ history?: number[];
135
+ }): react.JSX.Element;
136
+ /**
137
+ * A signed sparkline over `values`. A zero baseline is drawn whenever the domain
138
+ * spans it, so a line dipping below zero reads as a belief losing ground. The
139
+ * last point gets a dot. `fill` shades the area under the line. Pure geometry
140
+ * comes from `sparklinePath`/`sparklineY`.
141
+ */
142
+ declare function Sparkline({ values, width, height, min, max, tone, fill, ariaLabel, }: {
143
+ values: number[];
144
+ width?: number;
145
+ height?: number;
146
+ min?: number;
147
+ max?: number;
148
+ tone?: Tone;
149
+ fill?: boolean;
150
+ ariaLabel?: string;
151
+ }): react.JSX.Element | null;
152
+ /**
153
+ * A glanceable stat tile — a label, a big number, and an optional sub-caption.
154
+ * Becomes a button when `onClick` is given (counts that double as nav). Pure
155
+ * presentation; the caller supplies the value.
156
+ */
157
+ declare function StatTile({ label, value, sub, onClick, active, }: {
158
+ label: string;
159
+ value: number;
160
+ sub?: ReactNode;
161
+ onClick?: () => void;
162
+ active?: boolean;
163
+ }): react.JSX.Element;
164
+
165
+ /**
166
+ * The list-surface view-model (OPS-1287) — the pure shaped-query layer above the
167
+ * flat `RegisterTable`. Given a register's records (plus, for the cross-register
168
+ * views, the other registers), it computes the canonical derived-view tabs, the
169
+ * group-by boards, the filtered/sorted rows, the readings-under-plan nesting, and
170
+ * the needs-a-human counts. DOM-free and unit-tested at this seam exactly like
171
+ * `columns.ts`; `RegisterBrowser` renders what it returns.
172
+ *
173
+ * The tabs are **derived views, never stored** — each is a predicate over the
174
+ * records (and, where the ontology's derived view needs it, the other
175
+ * registers). Membership is recomputed on every read, so renames, new readings,
176
+ * and status changes are always reflected. Saved views are the same shaped query
177
+ * under a user name, kept as a separate list from these canonical tabs.
178
+ */
179
+
180
+ /** A group-by axis. Only assumptions expose the full set; see `groupByAxesFor`. */
181
+ type GroupByAxis = "Lens" | "Theme" | "Risk band" | "Status" | "Owner";
182
+ interface SortSpec {
183
+ /** A record field or a `derived.*` key (risk / confidence / strength …). */
184
+ key: string;
185
+ dir: "asc" | "desc";
186
+ }
187
+ /** The query that shapes a register view — all optional, all recomputable. */
188
+ interface ViewDescriptor {
189
+ /** Which canonical tab; defaults to the register's default tab. */
190
+ tabId?: string;
191
+ /** Group rows by an axis, or null for a flat list. */
192
+ groupBy?: GroupByAxis | null;
193
+ /** Sort override; null falls back to the tab's default sort. */
194
+ sort?: SortSpec | null;
195
+ /** Free-text filter over Title + Description, case-insensitive. */
196
+ query?: string;
197
+ }
198
+ /** A saved view is a named shaped query, kept apart from the canonical tabs. */
199
+ interface SavedView extends ViewDescriptor {
200
+ name: string;
201
+ }
202
+ /**
203
+ * The other registers a cross-register tab reads (Testing/Proven/In-tension),
204
+ * plus the reference date overdue is measured against. Everything optional: a
205
+ * tab whose context is absent simply matches nothing rather than throwing.
206
+ */
207
+ interface RegisterContext {
208
+ assumptions?: AnyRecord[];
209
+ experiments?: AnyRecord[];
210
+ readings?: AnyRecord[];
211
+ decisions?: AnyRecord[];
212
+ /** ISO date "now" for the Overdue view; omitted → nothing reads overdue. */
213
+ asOf?: string;
214
+ }
215
+ /** A tab as the caller sees it — id + label, and whether it flags a human. */
216
+ interface TabDef {
217
+ id: string;
218
+ label: string;
219
+ /** The register lands here first. */
220
+ isDefault?: boolean;
221
+ /** A state that needs a human — surfaced as a nav count badge too (story 20). */
222
+ needsHuman?: boolean;
223
+ }
224
+ /** The canonical derived-view tabs for a register, in display order. */
225
+ declare function tabsFor(register: Collection): TabDef[];
226
+ /** The default tab id for a register (the curated front door, story 15). */
227
+ declare function defaultTabId(register: Collection): string;
228
+ /** The group-by axes a register offers. Assumptions get the full set (story 18);
229
+ * the others expose Status, the one axis every register shares. */
230
+ declare function groupByAxesFor(register: Collection): GroupByAxis[];
231
+ interface GroupBucket {
232
+ /** Stable bucket key. */
233
+ key: string;
234
+ /** Plain-language bucket heading. */
235
+ label: string;
236
+ records: AnyRecord[];
237
+ }
238
+ /**
239
+ * Group records by an axis. Risk band uses the three fixed bands in
240
+ * strongest-first order; multi-value axes (Theme, Owner) place a record in every
241
+ * bucket it belongs to, with an explicit empty-value bucket. Single-value axes
242
+ * (Lens, Status) bucket once, empty → an "—" bucket. Bucket order is stable:
243
+ * risk band by severity, everything else first-appearance.
244
+ */
245
+ declare function groupRecords(records: AnyRecord[], axis: GroupByAxis): GroupBucket[];
246
+ /** Case-insensitive substring filter over Title + Description. */
247
+ declare function filterRecords(records: AnyRecord[], query: string): AnyRecord[];
248
+ /** A stable sort by one key; numbers compare numerically, everything else by
249
+ * locale string. Missing values sort last regardless of direction. */
250
+ declare function sortRecords(records: AnyRecord[], sort: SortSpec): AnyRecord[];
251
+ interface NestedGroup {
252
+ /** The experiment id, or null for bare/found readings. */
253
+ experimentId: string | null;
254
+ /** The plan's title, or a plain label for the bare bucket. */
255
+ label: string;
256
+ readings: AnyRecord[];
257
+ }
258
+ /** Nest readings under their originating evidence plan (story 21). Bare/found
259
+ * readings (no `experimentId`) collect in a trailing "No plan" group. Plans keep
260
+ * the order they first appear in `experiments`. */
261
+ declare function nestReadingsByPlan(readings: AnyRecord[], experiments: AnyRecord[]): NestedGroup[];
262
+ interface ShapedRegister {
263
+ tabs: TabDef[];
264
+ activeTabId: string;
265
+ groupByAxes: GroupByAxis[];
266
+ activeGroupBy: GroupByAxis | null;
267
+ sort: SortSpec | null;
268
+ query: string;
269
+ /** Rows after tab predicate + filter + sort (the flat list). */
270
+ rows: AnyRecord[];
271
+ /** Group buckets when an axis is active, else null. */
272
+ groups: GroupBucket[] | null;
273
+ /** Readings-under-plan nesting when the active tab nests, else null. */
274
+ nested: NestedGroup[] | null;
275
+ }
276
+ /**
277
+ * Shape a register into its tabs, active rows, groups and nesting from a view
278
+ * descriptor. Pure: the same records + descriptor always give the same view. The
279
+ * descriptor round-trips — a saved view's tab/group/sort/query reproduce here.
280
+ */
281
+ declare function shapeRegister(register: Collection, records: AnyRecord[], descriptor?: ViewDescriptor, ctx?: RegisterContext): ShapedRegister;
282
+ /** The needs-a-human counts that surface as nav badges (story 20). */
283
+ interface NeedsHumanCounts {
284
+ /** Live beliefs in the kill zone awaiting a human verdict. */
285
+ killLane: number;
286
+ /** Running plans past their deadline. */
287
+ overdue: number;
288
+ /** Standing decisions resting on a belief that turned against them. */
289
+ inTension: number;
290
+ }
291
+ /** Count the needs-a-human states across the registers, reusing the same tab
292
+ * predicates so a badge and its tab never disagree. */
293
+ declare function needsHumanCounts(ctx: RegisterContext): NeedsHumanCounts;
294
+
295
+ /**
296
+ * The dashboard's client-owned navigation state, across the three workflow
297
+ * altitudes plus the kept register tables (OPS-1298). One `<ValidationOSDashboard/>`
298
+ * mounts at a single host route and drives everything off the URL hash — there
299
+ * is no second entry point (OPS-1280).
300
+ *
301
+ * - `next` — the front door ("what's my next move"); the default landing.
302
+ * - `pipeline` — the step-back portfolio pipeline.
303
+ * - `records` — one register's browse table (the manual-override surface,
304
+ * kept from the original scheme).
305
+ * - `record` — the per-belief drill-in (full record page).
306
+ */
307
+ type Route = {
308
+ name: "next";
309
+ } | {
310
+ name: "pipeline";
311
+ } | {
312
+ name: "records";
313
+ register: Collection;
314
+ } | {
315
+ name: "record";
316
+ id: string;
317
+ };
318
+ /**
319
+ * Parse a URL hash into a Route. The empty hash and anything unrecognised fall
320
+ * back to the front door. A bare register name (`#assumptions`) stays
321
+ * backward-compatible with the original `#<register>` scheme, so it resolves to
322
+ * that register's Records table; `registers` is the set the instance allows, so
323
+ * an unknown or disallowed register name falls through to the default.
324
+ */
325
+ declare function parseRoute(hash: string, registers: Collection[]): Route;
326
+ /**
327
+ * The hash fragment (no leading `#`) for a Route — the inverse of `parseRoute`.
328
+ * `records` serialises to the bare register name to keep deep links stable with
329
+ * the original scheme.
330
+ */
331
+ declare function formatRoute(route: Route): string;
5
332
 
6
333
  type Counts = Partial<Record<Collection, number>>;
7
334
  interface UseCountsResult {
@@ -16,25 +343,279 @@ interface UseCountsResult {
16
343
  * HTTP, so no backend credentials ever reach the browser.
17
344
  */
18
345
  declare function useCounts(basePath?: string): UseCountsResult;
346
+ /** The needs-a-human counts mapped to the register that owns each badge — kill
347
+ * lane → assumptions, overdue → experiments, in-tension → decisions. */
348
+ type NeedsHumanByRegister = Partial<Record<Collection, number>>;
349
+ interface UseNeedsHumanResult {
350
+ counts: NeedsHumanCounts;
351
+ byRegister: NeedsHumanByRegister;
352
+ }
353
+ /**
354
+ * Client hook: load the assumptions / experiments / decisions registers and
355
+ * derive the needs-a-human counts (kill lane, overdue, in tension) so the nav
356
+ * can carry a persistent badge on the register that needs a human (story 20).
357
+ * Reuses the same `needsHumanCounts` view-model the tabs and their badges use, so
358
+ * a nav badge and its tab never disagree.
359
+ */
360
+ declare function useNeedsHuman(basePath?: string): UseNeedsHumanResult;
361
+
362
+ interface SidebarNavProps {
363
+ /** The active route — drives which item is highlighted. A `record` route
364
+ * highlights nothing (the drill-in has no nav item; OPS-1298). */
365
+ route: Route;
366
+ /** Called when a nav item is chosen, with the route it selects. */
367
+ onNavigate: (route: Route) => void;
368
+ /** Live per-register counts; a middle dot shows until they load. */
369
+ counts?: Counts | null;
370
+ /** Per-register needs-a-human counts — a persistent alert badge on the
371
+ * register that needs attention (kill lane, overdue plans, tensions; story 20). */
372
+ needsHuman?: Partial<Record<Collection, number>>;
373
+ /** The registers shown under Records, in order. */
374
+ registers: Collection[];
375
+ }
376
+ /**
377
+ * The sidebar nav across the two groups (OPS-1298): a **Workflow** group — Next
378
+ * move (the default landing, badged `home`) and Pipeline — above the **Records**
379
+ * group of register tables (kept, not subsumed; the browse-everything /
380
+ * manual-override surface). A presentational brick: the caller owns the active
381
+ * route and supplies the counts. The assembled `<ValidationOSDashboard/>`
382
+ * composes this; it's also exported for anyone building their own surface.
383
+ * Styled with the package's own token sheet — no host Tailwind.
384
+ */
385
+ declare function SidebarNav({ route, onNavigate, counts, needsHuman, registers, }: SidebarNavProps): react.JSX.Element;
386
+
387
+ interface RecordPageProps {
388
+ /** The record being drilled into (`#record/<id>`). */
389
+ recordId: string;
390
+ /** Navigate elsewhere — the breadcrumb, backlinks and glossary links use it. */
391
+ onNavigate: (route: Route) => void;
392
+ /** Where the "Records" breadcrumb returns to before the record's own register
393
+ * is known (resolved once the record loads). */
394
+ backRegister: Collection;
395
+ /** API base path (default `/api`). */
396
+ basePath?: string;
397
+ }
398
+ /**
399
+ * The canonical full record page (OPS-1286) — promoted from the enriched drawer
400
+ * to the one place a record lives, reachable identically from a table, a
401
+ * backlink or a search (story 12). It loads the registers once (the related set
402
+ * every panel reads), resolves which register the id belongs to, and renders the
403
+ * pure `buildRecordPage` model: a status + derived lane/queue header, leading
404
+ * scores as meters with a "Why?" attribution (reusing the understanding layer),
405
+ * the genuine human-input free text (glossary auto-linked), backlink panels
406
+ * grouped by relation with score chips, and a history/audit view. A belief's
407
+ * page also hosts the per-belief journey (built by the OPS-1289 map).
408
+ */
409
+ declare function RecordPage({ recordId, onNavigate, backRegister, basePath, }: RecordPageProps): react.JSX.Element;
410
+
411
+ interface SurfacePlaceholderProps {
412
+ /** The surface's title, shown as the pane heading. */
413
+ title: string;
414
+ /** A one-line description under the title. */
415
+ subtitle: string;
416
+ /** What fills this pane, and which build ships it. */
417
+ detail: ReactNode;
418
+ }
419
+ /**
420
+ * A pane the navigation shell reserves for a surface built in its own step
421
+ * (OPS-1298: "the shell can land first; each surface fills its pane as it
422
+ * ships"). It renders the surface's heading and a labelled placeholder so the
423
+ * route, nav slot, and title are real and reachable while the surface itself is
424
+ * still to come. Each later build swaps this out for the real component.
425
+ */
426
+ declare function SurfacePlaceholder({ title, subtitle, detail, }: SurfacePlaceholderProps): react.JSX.Element;
427
+
428
+ /**
429
+ * The step-back portfolio pipeline (OPS-1300) — the middle altitude of the
430
+ * workflow dashboard. One row per live belief, sorted riskiest first, each
431
+ * carrying the four loop meters (Framed → Planned → Tested → Known) as a
432
+ * connected track, with a stage-aware next move on hover. Above them, the
433
+ * single burn-up headline: "% of risk bought down". Resolved beliefs are set
434
+ * apart behind a disclosure; the raw Impact shows only as a faint bar.
435
+ *
436
+ * It lazy-loads the assumption, experiment and reading registers and derives
437
+ * everything through the pure `pipeline` view-model — no number is computed
438
+ * here (spec: explain from inputs). Clicking a belief, its next move, or its
439
+ * Journey link routes to that belief's record page (OPS-1298), the review
440
+ * surface where step-in happens.
441
+ */
442
+ declare function PipelineSurface({ basePath, onNavigate, }: {
443
+ basePath?: string;
444
+ onNavigate: (route: Route) => void;
445
+ }): react.JSX.Element;
446
+
447
+ /**
448
+ * The portfolio pipeline's view-model (OPS-1300) — pure, no React, no I/O, so
449
+ * the whole "where does everything stand" mapping is unit-tested at this seam
450
+ * (like `understanding.ts` for the drawer). It joins the assumption, reading
451
+ * and experiment registers into: one row per live belief carrying the four
452
+ * loop meters (Framed → Planned → Tested → Known) and its stage-aware next
453
+ * move, the resolved beliefs set apart, and the portfolio burn-up headline.
454
+ *
455
+ * Every number is derived through `@validation-os/core` — the stored per-belief
456
+ * derived numbers, `assumptionCompleteness` for Framed, and `portfolioProgress`
457
+ * for the cross-belief roll-up — so the pipeline reconciles with the drawer and
458
+ * the agent, and computes fresh on read.
459
+ */
460
+
461
+ /** One live belief's row on the board. */
462
+ interface PipelineRow {
463
+ id: string;
464
+ statement: string;
465
+ /** Derived Impact — shown only as a faint bar (machinery, not the move). */
466
+ impact: number;
467
+ risk: number;
468
+ /** crit / warn / good — the severity stripe, risk number and bar tone. */
469
+ riskTone: Extract<Tone, "crit" | "warn" | "good">;
470
+ confidence: number;
471
+ /** Sign bucket for the confidence chip / Known gauge direction. */
472
+ confSign: "pos" | "neg" | "zero";
473
+ /** Conf ≤ −50 — the Known meter flips to a red re-test flag. */
474
+ killZone: boolean;
475
+ /** Meter 1 — framing completeness, 0–100. */
476
+ framed: number;
477
+ /** Meter 2 — a test with a bar line naming this belief has been designed. */
478
+ planned: boolean;
479
+ /** Meter 3 — pre-registered bars settled / total, across all experiments. */
480
+ tested: {
481
+ settled: number;
482
+ total: number;
483
+ };
484
+ /** The stage-aware verb the front door offers (navigates to the record). */
485
+ nextMove: string;
486
+ }
487
+ /** A belief taken off the board — killed (Invalidated) or made moot. */
488
+ interface ResolvedRow {
489
+ id: string;
490
+ statement: string;
491
+ kind: "killed" | "moot";
492
+ /** Risk retired by resolving it. */
493
+ retired: number;
494
+ }
495
+ interface PipelineView {
496
+ /** Live beliefs, riskiest first. */
497
+ rows: PipelineRow[];
498
+ /** Resolved beliefs, set apart. */
499
+ resolved: ResolvedRow[];
500
+ /** The burn-up headline: identified / retired / live / percent. */
501
+ progress: PortfolioProgress;
502
+ /** Σ risk retired across the resolved set — the disclosure's summary. */
503
+ resolvedRetired: number;
504
+ }
505
+ /**
506
+ * Reduce an experiment record to the shape the shared meter logic reads: each
507
+ * bar line naming a belief (with whether it has settled) plus the convenience
508
+ * projection. Shared with the journey view-model so the board and a belief's
509
+ * rail derive test state one way.
510
+ */
511
+ declare function toStageExperimentInput(exp: AnyRecord): StageExperimentInput;
512
+ /**
513
+ * Build the whole board from the three registers. Pass the full assumption
514
+ * register (resolved rows included) — `portfolioProgress` needs the whole set
515
+ * or the burn-up denominator understates.
516
+ */
517
+ declare function buildPipeline(assumptions: AnyRecord[], experiments: AnyRecord[]): PipelineView;
518
+ /**
519
+ * The headline's "this week" delta, in percentage points, or null when it
520
+ * can't be told honestly. It time-travels through the readings' own dates:
521
+ * recompute every belief's Confidence (and thus Risk) from only the readings
522
+ * dated on or before a cutoff, roll the portfolio up at "now" and at a week
523
+ * ago, and report the change. Structure and resolved-status are held at their
524
+ * current value (the system keeps no history of those), so this is the
525
+ * evidence-driven movement, not a full snapshot diff — hence "this week" reads
526
+ * as "what landing evidence moved", which is the honest claim.
527
+ *
528
+ * Returns null when no reading carries a parseable date (nothing to travel
529
+ * through) — the surface then simply omits the delta rather than inventing one.
530
+ */
531
+ declare function weekOverWeekDelta(assumptions: AnyRecord[], readings: AnyRecord[], now: Date): number | null;
532
+
533
+ /**
534
+ * Front-door presentation logic for the next-move ranking (build OPS-1304).
535
+ * Two pure seams, kept free of React/DOM so they're unit-tested like
536
+ * `columns.ts` / `primitives.ts`:
537
+ *
538
+ * - `toNextMoveInput` maps the fetched register records onto the shape
539
+ * `packages/core`'s `rankNextMoves` consumes (the ranking rule itself lives
540
+ * once in core — `ontology.yaml → derived_views.next_move`, OPS-1292);
541
+ * - `movePresentation` maps each act to its front-door copy and whether it's a
542
+ * human step-in form or an agent-run act shown for review (OPS-1291/1294).
543
+ */
544
+
545
+ /** The four registers the ranking reads, as fetched from the API. */
546
+ interface NextMoveRecords {
547
+ assumptions: AnyRecord[];
548
+ experiments: AnyRecord[];
549
+ readings: AnyRecord[];
550
+ decisions: AnyRecord[];
551
+ }
552
+ /**
553
+ * Fold the fetched records into `rankNextMoves`' input: the derived Risk /
554
+ * Confidence come straight off each assumption (the server keeps them current,
555
+ * OPS-1251 — never recomputed here), and concluded readings are counted per
556
+ * belief so the stage logic can tell "no evidence yet" from "evidence in".
557
+ */
558
+ declare function toNextMoveInput(records: NextMoveRecords): NextMoveInput;
559
+ /** Which step-in form an act opens, or null for an agent-run act. */
560
+ type StepInForm = "score-impact" | "write-decision";
561
+ interface MovePresentation {
562
+ /** Imperative CTA on the hero's act button. */
563
+ cta: string;
564
+ /** Short label for the "On deck" act pill. */
565
+ pill: string;
566
+ /**
567
+ * A human step-in form (the dashboard is a review surface, OPS-1294) vs an
568
+ * agent-run act the front door only visualises for review.
569
+ */
570
+ steppable: boolean;
571
+ /** The form the act opens when steppable; null for agent-run acts. */
572
+ form: StepInForm | null;
573
+ }
574
+ /** The front-door copy + step-in modality for one act. */
575
+ declare function movePresentation(kind: MoveKind): MovePresentation;
576
+
577
+ /** A journey event with its front-door copy attached. */
578
+ interface JourneyEventView extends JourneyEvent {
579
+ /** Plain-language label the story renders. */
580
+ label: string;
581
+ }
582
+ interface JourneyView {
583
+ id: string;
584
+ /** The belief statement — the drill-in's headline. */
585
+ statement: string;
586
+ /** The rail: where the belief sits at rest, with its four meters. */
587
+ stage: BeliefStage;
588
+ /** The story: the belief's life, oldest first, `now` last. */
589
+ events: JourneyEventView[];
590
+ /** The ranked next move for this belief; null once it is resolved. */
591
+ nextMove: NextMove | null;
592
+ /** Killed (Invalidated) / moot / null — the drill-in's terminal state. */
593
+ resolved: "killed" | "moot" | null;
594
+ }
595
+ /**
596
+ * Build one belief's journey from the four registers. Returns null when the
597
+ * belief id isn't in the assumptions register. `now` is passed in (an ISO date)
598
+ * so the view-model stays pure — the surface supplies it.
599
+ */
600
+ declare function buildJourney(assumptionId: string, records: NextMoveRecords, now: string): JourneyView | null;
19
601
 
20
602
  interface RegisterCountsProps {
21
603
  counts: Counts;
22
604
  /** Optional caption under the tiles (e.g. the backend the numbers came from). */
23
605
  caption?: string;
24
- /**
25
- * Optional link target per register. When supplied, each tile becomes a link
26
- * (a plain anchor works with any router), so counts double as navigation
27
- * into the browse tables.
28
- */
29
- hrefFor?: (register: Collection) => string;
606
+ /** Optional click handler per register — makes each tile a nav button. */
607
+ onSelect?: (register: Collection) => void;
608
+ /** The active register, highlighted when tiles are nav. */
609
+ active?: Collection | null;
30
610
  }
31
611
  /**
32
612
  * A glanceable grid of stat tiles — one per register, the register's row count
33
- * as the hero number. Presentational: the caller reads the counts (server-side,
34
- * through the adapter) and hands them in. Styled with Tailwind utility classes;
35
- * the host app provides Tailwind.
613
+ * as the hero number. A brick kept for a counts landing; the assembled app
614
+ * surfaces the same counts in the sidebar nav. Presentational: the caller reads
615
+ * the counts (server-side, through the adapter) and hands them in. Rendered in
616
+ * the package's own token sheet — no host Tailwind.
36
617
  */
37
- declare function RegisterCounts({ counts, caption, hrefFor }: RegisterCountsProps): react.JSX.Element;
618
+ declare function RegisterCounts({ counts, caption, onSelect, active, }: RegisterCountsProps): react.JSX.Element;
38
619
 
39
620
  interface RegisterTableProps {
40
621
  register: Collection;
@@ -46,9 +627,11 @@ interface RegisterTableProps {
46
627
  }
47
628
  /**
48
629
  * A list table for one register — a row per record, the register's key fields
49
- * as columns (assumptions show Impact, Confidence and Risk). Presentational:
50
- * the caller supplies the rows. Clicking a row opens the read-only drawer.
51
- * Styled with Tailwind utilities the host app provides.
630
+ * as columns. Assumptions read their state at a glance: a colored Status pill, a
631
+ * signed Confidence, and a threshold-toned Risk bar (spec stories 4–6). Which
632
+ * cells render as pills/bars/sparklines is declared on the column (`kind`), so
633
+ * the treatment stays testable at the columns seam and this component stays a
634
+ * dumb renderer. Presentational: the caller supplies the rows.
52
635
  */
53
636
  declare function RegisterTable({ register, records, onRowClick, selectedId, }: RegisterTableProps): react.JSX.Element;
54
637
 
@@ -67,20 +650,24 @@ interface RecordDrawerProps {
67
650
  /** Re-fetch the record + its list — called after a save, and the re-fetch
68
651
  * path offered when a concurrent edit is detected. */
69
652
  onChanged?: () => void;
653
+ /** Open this record's canonical full page (story 12). When set, the header
654
+ * shows a "Full page" link; the drawer stays the quick read/edit peek. */
655
+ onOpenFull?: () => void;
70
656
  /** Extra content below the fields in read mode — e.g. the relation editor. */
71
657
  children?: ReactNode;
72
658
  }
73
659
  /**
74
- * A record drawer that reads and edits, and wires relations. Derived numbers
75
- * always lead as the hero, explicitly marked computed and never editable (spec
76
- * user story 4) a "Why?" affordance on Confidence explains how the number was
77
- * earned. Editing recomputes those numbers server-side on save (story 11); a
78
- * concurrent edit surfaces as a gentle, jargon-free prompt with a re-fetch path
79
- * (story 12). In read mode the drawer also hosts the relation editor (`children`,
80
- * story 14). The slide-over chrome is shared with the create drawer via
81
- * `DrawerShell`.
660
+ * A record drawer that reads and edits, and wires relations. The derived
661
+ * numbers lead as a visually distinct "computed not editable" hero (spec
662
+ * stories 4/10): a bordered box, each number big and mono, Confidence toned by
663
+ * sign and Risk by threshold, with a "Why?" reveal opening the understanding
664
+ * layer in the same accent/pill language (story 11). Editing recomputes those
665
+ * numbers server-side on save; a concurrent edit surfaces as a gentle, jargon-
666
+ * free prompt with a re-fetch path (story 12). In read mode the drawer hosts
667
+ * the relation editor (`children`). Chrome is shared via `DrawerShell`; styled
668
+ * with the package's own token sheet, no host Tailwind.
82
669
  */
83
- declare function RecordDrawer({ register, record, loading, error, open, onClose, basePath, onChanged, children, }: RecordDrawerProps): react.JSX.Element;
670
+ declare function RecordDrawer({ register, record, loading, error, open, onClose, basePath, onChanged, onOpenFull, children, }: RecordDrawerProps): react.JSX.Element;
84
671
 
85
672
  interface RecordFormProps {
86
673
  register: Collection;
@@ -118,11 +705,12 @@ declare function RelationEditor({ register, recordId, basePath, onLinked, }: Rel
118
705
  /**
119
706
  * The understanding layer behind the Confidence "Why?" (OPS-1276): which
120
707
  * experiments move the number (ranked by push) and how close each running
121
- * experiment is to concluding, the goal/direct evidence that also moves it,
122
- * and Confidence over time. It lazy-loads the readings + experiments registers
123
- * (it only mounts when the Reveal is open), then derives everything through the
124
- * shared derivation module. The derived box stays the hero; this is the tucked-
125
- * away detail.
708
+ * experiment is to concluding, the goal/direct evidence that also moves it, and
709
+ * Confidence over time. It lazy-loads the readings + experiments registers (it
710
+ * only mounts when the Reveal is open), then derives everything through the
711
+ * shared derivation module. Restyled into the drawer's accent/pill language
712
+ * (spec story 11): signed push tracks and a signed trajectory sparkline, in the
713
+ * package's own token sheet — no host Tailwind.
126
714
  */
127
715
  declare function UnderstandingPanel({ assumption, basePath, }: {
128
716
  assumption: AnyRecord;
@@ -157,11 +745,11 @@ interface ExperimentView {
157
745
  /** Concluded/closed — reads as done rather than in-flight. */
158
746
  done: boolean;
159
747
  }
160
- /** Goal-rung or direct evidence that moves Confidence but is not an experiment. */
748
+ /** Direct evidence that moves Confidence but is not tied to an experiment
749
+ * (a bare/found reading, or a Market-rung reading with no plan). */
161
750
  interface OtherMover {
162
751
  key: string;
163
752
  kind: Exclude<MoverKind, "experiment">;
164
- goalId: string | null;
165
753
  contribution: number;
166
754
  magnitude: number;
167
755
  readingCount: number;
@@ -184,18 +772,92 @@ interface RegisterBrowserProps {
184
772
  register: Collection;
185
773
  /** API base path (default `/api`). */
186
774
  basePath?: string;
775
+ /** A one-line description under the register title (spec story 7/9). */
776
+ subtitle?: string;
777
+ /** Open a record's canonical full page (story 12). When set, the drawer
778
+ * offers a "Full page" link; reads/edits still happen in the peek drawer. */
779
+ onOpenRecord?: (id: string) => void;
780
+ }
781
+ /**
782
+ * The browse-create-edit surface for one register. Above the flat table sits the
783
+ * list-surface (OPS-1287): canonical derived-view tabs (a curated default first),
784
+ * a group-by board (assumptions by Lens / Theme / Risk band / Status / Owner),
785
+ * free-text search and sort, readings nested under their evidence plan, and a
786
+ * count badge on the states that need a human. Everything visible is computed by
787
+ * the pure `shapeRegister` view-model; this component just renders it and owns
788
+ * the descriptor state.
789
+ *
790
+ * The write surface is unchanged: a row opens the read/edit/relations drawer, a
791
+ * "New" button opens the create form, and all reads/writes go over the Clerk-
792
+ * gated API (which recomputes derived fields on write). The canonical full record
793
+ * page is reachable via the drawer's "Full page" link when `onOpenRecord` is set.
794
+ */
795
+ declare function RegisterBrowser({ register, basePath, subtitle, onOpenRecord, }: RegisterBrowserProps): react.JSX.Element;
796
+
797
+ /**
798
+ * The front door — "what's my next move" (design OPS-1295, build OPS-1304): the
799
+ * map's headline surface. One belief, one act, all machinery behind one "Why
800
+ * this?" (progressive disclosure — the hero stays clean). The single riskiest
801
+ * unresolved belief leads (Model A, OPS-1291); a kill-lane belief (Confidence ≤
802
+ * −50) raises a crit banner above the hero; three runners-up sit "On deck"; and
803
+ * a quiet pick-list lets you act on any belief the ranking wouldn't pick (manual
804
+ * override). Step-in adapts to the act (OPS-1294): human acts open a form here,
805
+ * agent-run acts point at the record for review.
806
+ *
807
+ * The ranking itself lives once in `packages/core` (OPS-1292); this surface only
808
+ * fetches the registers, folds them into that function, and renders the result.
809
+ */
810
+ interface NextMoveSurfaceProps {
811
+ basePath?: string;
812
+ /** Navigate across the shell (belief → record, override → records). */
813
+ onNavigate: (route: Route) => void;
814
+ }
815
+ declare function NextMoveSurface({ basePath, onNavigate }: NextMoveSurfaceProps): react.JSX.Element;
816
+
817
+ interface ScoreImpactFormProps {
818
+ /** The assumption being weighted (its version guards the write). */
819
+ assumption: AnyRecord;
820
+ basePath?: string;
821
+ /** Called after a successful save. */
822
+ onDone: () => void;
823
+ onCancel: () => void;
824
+ }
825
+ /**
826
+ * Score a belief's Impact — a real input (a slider tied to a number), not a bare
827
+ * cell edit (OPS-1294). Impact is the one hand-scored number Risk propagates
828
+ * from, so scoring it is what lets an unweighted belief take its place in the
829
+ * ranking. The optional justification records *why* that weight.
830
+ */
831
+ declare function ScoreImpactForm({ assumption, basePath, onDone, onCancel, }: ScoreImpactFormProps): react.JSX.Element;
832
+ interface WriteDecisionFormProps {
833
+ /** The belief the decision rests on or resolves. */
834
+ assumption: AnyRecord;
835
+ basePath?: string;
836
+ /**
837
+ * A kill-lane decision (Confidence ≤ −50) defaults to *resolving* (retiring)
838
+ * the belief; an ordinary decide defaults to resting *on* it.
839
+ */
840
+ kill?: boolean;
841
+ onDone: () => void;
842
+ onCancel: () => void;
187
843
  }
188
844
  /**
189
- * The browse-create-edit surface for one register: a list table that opens a
190
- * record drawer on row click, a "New" button that opens the create form, and a
191
- * relation editor in the drawer for wiring links. All reads and writes go over
192
- * HTTP through the Clerk-gated API (which recomputes derived fields on write),
193
- * so the browser never touches Firestore directly. After an edit saves, both
194
- * the drawer's record and the list re-fetch so recomputed derived numbers show
195
- * everywhere. The thin host app renders this with a `register` — that's the
196
- * whole page.
845
+ * Write a decision against a belief (OPS-1294) create the Decision record and
846
+ * wire it to the belief in one step, honouring the method's `based on` vs
847
+ * `resolves` split: a decision that *rests on* a belief keeps the question open
848
+ * (rationale); one that *resolves* it retires the question without a test
849
+ * (Impact 0, it goes moot). The kill lane defaults to resolving.
197
850
  */
198
- declare function RegisterBrowser({ register, basePath }: RegisterBrowserProps): react.JSX.Element;
851
+ declare function WriteDecisionForm({ assumption, basePath, kill, onDone, onCancel, }: WriteDecisionFormProps): react.JSX.Element;
852
+
853
+ interface UseSavedViewsResult {
854
+ views: SavedView[];
855
+ /** Save (or overwrite by name) the current shaped query under a name. */
856
+ save: (name: string, descriptor: ViewDescriptor) => void;
857
+ /** Drop a saved view by name. */
858
+ remove: (name: string) => void;
859
+ }
860
+ declare function useSavedViews(register: Collection): UseSavedViewsResult;
199
861
 
200
862
  interface UseListResult {
201
863
  records: AnyRecord[] | null;
@@ -203,8 +865,13 @@ interface UseListResult {
203
865
  error: string | null;
204
866
  refresh: () => void;
205
867
  }
206
- /** Fetch every row of a register. */
207
- declare function useList(register: Collection, basePath?: string): UseListResult;
868
+ /**
869
+ * Fetch every row of a register. `enabled` (default true) gates the fetch: pass
870
+ * false to keep the hook idle — the list-surface loads a *context* register
871
+ * (e.g. experiments, for the assumptions "Testing" view) only when the current
872
+ * register's tabs actually need it, without breaking the rules-of-hooks.
873
+ */
874
+ declare function useList(register: Collection, basePath?: string, enabled?: boolean): UseListResult;
208
875
  interface UseRecordResult {
209
876
  record: AnyRecord | null;
210
877
  loading: boolean;
@@ -333,6 +1000,14 @@ declare function hasEdits(register: Collection, original: AnyRecord, draft: Draf
333
1000
  * headers are plain language and derived numbers are shown, never hidden.
334
1001
  */
335
1002
 
1003
+ /**
1004
+ * How a cell renders. `text` is plain formatted text; `status` is a colored
1005
+ * pill; `risk` is a threshold-toned bar + number; `confidence` is a signed
1006
+ * number (with a sparkline when a trajectory is available). Keeping this on the
1007
+ * column — not branching by key in the table — is what makes the visual
1008
+ * treatment declarative and testable at this seam (spec story 13).
1009
+ */
1010
+ type CellKind = "text" | "status" | "risk" | "confidence";
336
1011
  interface ColumnDef {
337
1012
  /** Stable key; also the default field read from the record. */
338
1013
  key: string;
@@ -344,6 +1019,8 @@ interface ColumnDef {
344
1019
  align?: "left" | "right";
345
1020
  /** Marks a column whose value is computed, never hand-typed (spec story 4). */
346
1021
  derived?: boolean;
1022
+ /** How the cell renders; defaults to `text`. */
1023
+ kind?: CellKind;
347
1024
  }
348
1025
  /** The columns to render for a register. */
349
1026
  declare function columnsFor(register: Collection): ColumnDef[];
@@ -407,13 +1084,215 @@ interface LinkChoice {
407
1084
  /** The relations a record of `register` can initiate, in table order. */
408
1085
  declare function linkChoicesFrom(register: Collection): LinkChoice[];
409
1086
 
1087
+ /**
1088
+ * Glossary auto-linking (OPS-1285) — the pure render-time pass that turns
1089
+ * glossary Titles appearing in prose into links, computed fresh on every render
1090
+ * so renames / retirements / status changes are always reflected with nothing
1091
+ * stored. Kept DOM-free at this seam and unit-tested exactly like
1092
+ * `link-choices.ts`; the `GlossaryText` component renders what it returns.
1093
+ *
1094
+ * The four forks the OPS-1285 prototype resolved are the rules here:
1095
+ * - match on **Title only** (no alias list), case-insensitive, word-boundary,
1096
+ * **longest title wins** (Derived Impact over Impact), **every** occurrence;
1097
+ * - **Active + Provisional** terms link, **Superseded never**, and never a
1098
+ * term inside its own record;
1099
+ * - **no false-positive machinery** — a generic word that is a term links
1100
+ * harmlessly; the simplicity is the decision.
1101
+ */
1102
+
1103
+ /** A glossary term reduced to what linking needs — the normalised input. */
1104
+ interface GlossaryTerm {
1105
+ id: string;
1106
+ title: string;
1107
+ /** "Active" | "Provisional" | "Superseded". */
1108
+ status: string;
1109
+ definition: string;
1110
+ howItDiffers: string;
1111
+ }
1112
+ /** A "don't confuse with" neighbour — a chip that is itself a link. */
1113
+ interface NeighbourChip {
1114
+ id: string;
1115
+ title: string;
1116
+ }
1117
+ /** The definition-preview payload a link node carries (the hover popover). */
1118
+ interface TermPreview {
1119
+ id: string;
1120
+ title: string;
1121
+ status: string;
1122
+ definition: string;
1123
+ /** Neighbours named in this term's "How it differs" (story 29/30). */
1124
+ dontConfuseWith: NeighbourChip[];
1125
+ }
1126
+ /** One piece of linkified prose: literal text, or a link to a term. */
1127
+ type LinkifyNode = {
1128
+ kind: "text";
1129
+ text: string;
1130
+ } | {
1131
+ kind: "link";
1132
+ text: string;
1133
+ term: TermPreview;
1134
+ };
1135
+ interface LinkifyOptions {
1136
+ /** The id of the record the prose belongs to — its own term never links. */
1137
+ selfId?: string;
1138
+ }
1139
+ /**
1140
+ * The terms named in a term's "How it differs" prose — the neighbour chips.
1141
+ * Excludes the term itself and any non-linkable (Superseded) neighbour, and is
1142
+ * de-duplicated in first-appearance order.
1143
+ */
1144
+ declare function dontConfuseWith(term: GlossaryTerm, terms: GlossaryTerm[]): NeighbourChip[];
1145
+ /**
1146
+ * Linkify `text` against the glossary. Returns an ordered list of text and link
1147
+ * nodes; a link node carries the term's definition-preview payload. Pure and
1148
+ * render-time — nothing is stored.
1149
+ */
1150
+ declare function linkify(text: string, terms: GlossaryTerm[], options?: LinkifyOptions): LinkifyNode[];
1151
+ /** Normalise glossary records into the linking input, dropping blank-title
1152
+ * rows (they can never match). Tolerates missing optional fields. */
1153
+ declare function toGlossaryTerms(records: AnyRecord[]): GlossaryTerm[];
1154
+
1155
+ interface GlossaryTextProps {
1156
+ /** The prose to linkify — body text or a short-text field (story 27). */
1157
+ text: string;
1158
+ /** The live glossary, already normalised (see `toGlossaryTerms`). */
1159
+ terms: GlossaryTerm[];
1160
+ /** The record this prose belongs to — its own term never self-links. */
1161
+ selfId?: string;
1162
+ /** Open a term's record (from a link or a neighbour chip; story 30). */
1163
+ onOpenTerm?: (id: string) => void;
1164
+ }
1165
+ declare function GlossaryText({ text, terms, selfId, onOpenTerm, }: GlossaryTextProps): react.JSX.Element;
1166
+
1167
+ /**
1168
+ * The record-page view-model (OPS-1286) — the pure join behind the canonical
1169
+ * full record page. Given a record and the related registers, it computes the
1170
+ * header lane/queue pills (all derived), the leading-score meters per register,
1171
+ * the genuine human-input free-text remainder, and the backlink panels grouped
1172
+ * by relation (each row carrying a glance-readable score chip, an empty relation
1173
+ * kept as a "none yet" panel rather than dropped). DOM-free and unit-tested at
1174
+ * this seam; `RecordPage` renders what it returns and the understanding layer
1175
+ * supplies the "Why?" attribution unchanged.
1176
+ */
1177
+
1178
+ /** The registers a record page reads to resolve its relations. All optional —
1179
+ * a panel whose register is absent simply resolves to "none yet". */
1180
+ interface RelatedSet {
1181
+ assumptions?: AnyRecord[];
1182
+ experiments?: AnyRecord[];
1183
+ readings?: AnyRecord[];
1184
+ decisions?: AnyRecord[];
1185
+ glossary?: AnyRecord[];
1186
+ }
1187
+ interface RecordPageOptions {
1188
+ /** ISO date "now" for the Overdue pill; omitted → nothing reads overdue. */
1189
+ asOf?: string;
1190
+ }
1191
+ /** A header pill: a short derived label toned by meaning. */
1192
+ interface Pill {
1193
+ label: string;
1194
+ tone: Tone;
1195
+ }
1196
+ /**
1197
+ * The derived lane/queue pills for a record header (story 2). Every register
1198
+ * leads with its Status pill; assumptions add Moot / Kill lane / Testing,
1199
+ * experiments add Concluded / Overdue, decisions mark Standing. All derived —
1200
+ * none is a stored field.
1201
+ */
1202
+ declare function headerPills(register: Collection, record: AnyRecord, related?: RelatedSet, options?: RecordPageOptions): Pill[];
1203
+ /** A leading-score meter. `bar`/`signed` carry a number in a min…max domain;
1204
+ * `pill` carries a categorical value shown as a toned pill. */
1205
+ interface Meter {
1206
+ key: string;
1207
+ label: string;
1208
+ kind: "bar" | "signed" | "pill";
1209
+ /** Number (bar/signed) or category (pill); null when the record has no value. */
1210
+ value: number | string | null;
1211
+ tone: Tone;
1212
+ /** For bar/signed: the domain the fill maps onto. */
1213
+ min?: number;
1214
+ max?: number;
1215
+ /** True when a "Why?" attribution is available (Confidence only). */
1216
+ hasWhy?: boolean;
1217
+ }
1218
+ /**
1219
+ * The register's leading scores as meters (story 4). Reads only what the
1220
+ * migrated schema actually stores — a derived number becomes a bar/signed meter,
1221
+ * a categorical leading field becomes a pill; nothing is invented where the
1222
+ * schema carries no number. Confidence is flagged `hasWhy` — the understanding
1223
+ * layer decomposes it.
1224
+ */
1225
+ declare function leadingMeters(register: Collection, record: AnyRecord): Meter[];
1226
+ /** The genuine human-input free-text remainder — clearly separated from what
1227
+ * the system computed. Auto-linked against the glossary at render time. */
1228
+ interface HumanText {
1229
+ key: string;
1230
+ label: string;
1231
+ text: string;
1232
+ }
1233
+ /** The human-input fields a register carries text for (empty ones dropped). */
1234
+ declare function humanInputFields(register: Collection, record: AnyRecord): HumanText[];
1235
+ /** A glance-readable score chip for a linked record (story 9). */
1236
+ interface ScoreChip {
1237
+ label: string;
1238
+ value: string;
1239
+ tone: Tone;
1240
+ }
1241
+ interface BacklinkItem {
1242
+ id: string;
1243
+ register: Collection;
1244
+ title: string;
1245
+ chip: ScoreChip;
1246
+ }
1247
+ /** A relation panel — the inbound/outbound edges of one relation, grouped and
1248
+ * labelled. Kept even when empty (story 10). */
1249
+ interface RelationPanel {
1250
+ id: string;
1251
+ label: string;
1252
+ register: Collection;
1253
+ items: BacklinkItem[];
1254
+ }
1255
+ /** The linked record's headline score, glance-readable (story 9). */
1256
+ declare function scoreChip(register: Collection, record: AnyRecord): ScoreChip;
1257
+ /** The backlink panels for a record, grouped by relation, each row carrying a
1258
+ * score chip. Empty relations are kept (items: []) so a missing connection reads
1259
+ * as "none yet", not an absent section (story 8/10). */
1260
+ declare function backlinkPanels(register: Collection, record: AnyRecord, related?: RelatedSet): RelationPanel[];
1261
+ type RecordTabId = "overview" | "evidence" | "connections" | "history";
1262
+ interface RecordPageModel {
1263
+ register: Collection;
1264
+ title: string;
1265
+ pills: Pill[];
1266
+ meters: Meter[];
1267
+ humanText: HumanText[];
1268
+ panels: RelationPanel[];
1269
+ /** The tabs this record shows, in order (story 3 + 11). */
1270
+ tabs: RecordTabId[];
1271
+ /** True when the belief journey drill-in can mount here (story 13). */
1272
+ hasJourney: boolean;
1273
+ }
1274
+ /** Assemble the whole record-page model. Pure: same record + related always
1275
+ * gives the same page. */
1276
+ declare function buildRecordPage(register: Collection, record: AnyRecord, related?: RelatedSet, options?: RecordPageOptions): RecordPageModel;
1277
+
410
1278
  /** Plain-language labels — the register is a surface a non-technical
411
1279
  * teammate meets, so no jargon and no code-y plurals. */
412
1280
  declare const REGISTER_LABEL: Record<Collection, string>;
413
1281
  /** Singular labels — for "New {thing}" affordances (never a naive de-pluralise
414
- * that would read "New Peopl" / "New Glossary"). */
1282
+ * that would read "New Glossary"). */
415
1283
  declare const REGISTER_SINGULAR: Record<Collection, string>;
416
1284
  /** The order tiles read left-to-right, top-to-bottom. */
417
1285
  declare const REGISTER_ORDER: Collection[];
1286
+ /** A one-line description shown under each register's title (spec story 9). */
1287
+ declare const REGISTER_SUBTITLE: Record<Collection, string>;
1288
+ /** A single-glyph icon per register for the sidebar nav (matches the prototype
1289
+ * feel; a glyph, not an icon font, so nothing external is pulled in). */
1290
+ declare const REGISTER_ICON: Record<Collection, string>;
1291
+ /** The sidebar groups — all five registers now sit in one set (the retired
1292
+ * `people` reference collection was the only "Reference" member). */
1293
+ declare const REGISTER_GROUPS: {
1294
+ label: string;
1295
+ registers: Collection[];
1296
+ }[];
418
1297
 
419
- export { CONFLICT_MESSAGE, type ColumnDef, type Counts, type Draft, type ExperimentView, type FieldEditor, type FieldKind, type FormField, type LinkArgs, type LinkChoice, type OtherMover, REGISTER_LABEL, REGISTER_ORDER, REGISTER_SINGULAR, RecordDrawer, type RecordDrawerProps, RecordForm, type RecordFormProps, RegisterBrowser, type RegisterBrowserProps, RegisterCounts, type RegisterCountsProps, RegisterTable, type RegisterTableProps, RelationEditor, type RelationEditorProps, type SaveResult, type Understanding, UnderstandingPanel, type UseCountsResult, type UseCreateResult, type UseLinkResult, type UseListResult, type UseRecordResult, type UseUpdateResult, buildPatch, buildUnderstanding, cellValue, columnsFor, draftFrom, editableFields, emptyDraft, formFieldsFor, formatValue, hasEdits, interpretSave, linkChoicesFrom, missingRequired, primaryLabel, toCreatePayload, useCounts, useCreate, useLink, useList, useRecord, useUpdate };
1298
+ export { type BacklinkItem, CONFLICT_MESSAGE, type CellKind, type ColumnDef, ConfidenceCell, type Counts, type DashboardConfig, type Draft, type ExperimentView, type FieldEditor, type FieldKind, type FormField, type GlossaryTerm, GlossaryText, type GlossaryTextProps, type GroupBucket, type GroupByAxis, type HumanText, type JourneyEventView, type JourneyView, type LinkArgs, type LinkChoice, type LinkifyNode, type LinkifyOptions, type Meter, type MovePresentation, type NeedsHumanByRegister, type NeedsHumanCounts, type NeighbourChip, type NestedGroup, type NextMoveRecords, NextMoveSurface, type NextMoveSurfaceProps, type OtherMover, type Pill, 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, StatTile, StatusPill, type StepInForm, 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, columnsFor, confidenceTone, configureRiskBands, defaultTabId, dontConfuseWith, draftFrom, editableFields, emptyDraft, filterRecords, formFieldsFor, formatCount, formatRoute, formatSigned, formatValue, groupByAxesFor, groupRecords, hasEdits, headerPills, humanInputFields, interpretSave, leadingMeters, linkChoicesFrom, linkify, missingRequired, movePresentation, needsHumanCounts, nestReadingsByPlan, parseRoute, primaryLabel, riskBand, riskFraction, riskLevel, scoreChip, shapeRegister, sortRecords, sparklinePath, statusTone, tabsFor, toCreatePayload, toGlossaryTerms, toNextMoveInput, toStageExperimentInput, useCounts, useCreate, useLink, useList, useNeedsHuman, useRecord, useSavedViews, useUpdate, weekOverWeekDelta };