@validation-os/dashboard 0.3.0 → 0.5.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,5 +1,143 @@
1
1
  import * as react from 'react';
2
- import { Collection, AnyRecord } from '@validation-os/core';
2
+ import { ReactNode } from 'react';
3
+ import { Collection, AnyRecord, Relation } from '@validation-os/core';
4
+ import { Progress, MoverKind, TrajectoryPoint } from '@validation-os/core/derivation';
5
+
6
+ /**
7
+ * Everything the instance passes — config only, never secrets. The API base
8
+ * path points at where the Clerk-gated `@validation-os/api` routes are mounted;
9
+ * the backend label (from the connector config) shows in the topbar; branding
10
+ * and the signed-in user are optional cosmetics. Auth is the instance's Clerk
11
+ * provider that this renders within — no credential reaches the package.
12
+ */
13
+ interface DashboardConfig {
14
+ /** Where the API is mounted (default `/api`). */
15
+ basePath?: string;
16
+ /** Topbar backend indicator, e.g. "Firestore · doshi-crm". */
17
+ backendLabel?: string;
18
+ /** Optional product branding for the sidebar. */
19
+ branding?: {
20
+ /** Product name shown next to the mark (default "Validation-OS"). */
21
+ name?: string;
22
+ /** One or two letters for the square mark (default "V"). */
23
+ initials?: string;
24
+ /** A logo image for the square mark; overrides `initials` when set. */
25
+ logoUrl?: string;
26
+ };
27
+ /** Optional agent label shown in the topbar, e.g. "Claude Code". */
28
+ agentLabel?: string;
29
+ /** The signed-in user, for the topbar avatar + name. */
30
+ user?: {
31
+ name?: string;
32
+ caption?: string;
33
+ };
34
+ /** Restrict/reorder the registers shown; defaults to all, in order. */
35
+ registers?: Collection[];
36
+ }
37
+ interface ValidationOSDashboardProps {
38
+ config?: DashboardConfig;
39
+ }
40
+ /**
41
+ * The entire styled dashboard as one mountable app (spec OPS-1280): the frame —
42
+ * sidebar composing register nav + live counts, topbar with the backend
43
+ * indicator and user — and the register views (browse → drawer → understanding)
44
+ * it composes from the package's own bricks. Navigation is owned here, not the
45
+ * host router: the active register lives in client state (synced to the URL
46
+ * hash), so the instance mounts this at one route and wires no routing. Styled
47
+ * by the package's own token sheet — the instance imports `styles.css` once and
48
+ * builds no UI.
49
+ */
50
+ declare function ValidationOSDashboard({ config }: ValidationOSDashboardProps): react.JSX.Element;
51
+
52
+ /**
53
+ * Pure presentation logic for the dashboard's visual primitives — status→tone,
54
+ * risk→fraction/level, the sparkline path, and count/number formatting. Kept
55
+ * free of React and the DOM so the mapping that drives every pill, bar and
56
+ * sparkline is unit-tested at this seam (spec story 13), and the components stay
57
+ * thin wrappers over it.
58
+ */
59
+ /** A pill/fill tone — maps onto the `--vos-good/warn/crit/accent` tokens. */
60
+ type Tone = "good" | "warn" | "crit" | "accent" | "neutral";
61
+ /**
62
+ * A record's Status → a pill tone. Live/concluded read good, testing/running
63
+ * read warn, invalidated/rejected read crit; everything else (Proposed, and any
64
+ * status we don't recognise) stays neutral rather than guessing a colour. Case-
65
+ * and whitespace-insensitive.
66
+ */
67
+ declare function statusTone(status: string | null | undefined): Tone;
68
+ /** Risk thresholds — a belief is critical at ≥60, watch at ≥30 (prototype). */
69
+ declare const RISK_CRIT = 60;
70
+ declare const RISK_WARN = 30;
71
+ /** Risk (0–100) → a tone for the bar fill and the number. */
72
+ declare function riskLevel(risk: number): Tone;
73
+ /** Risk (0–100) → the bar's fill fraction (0–1), clamped. */
74
+ declare function riskFraction(risk: number): number;
75
+ /** Confidence (signed) → the tone for its number: negative reads crit. */
76
+ declare function confidenceTone(confidence: number): Tone;
77
+ /** A signed number rendered with an explicit sign: `+6`, `-3`, `0`. */
78
+ declare function formatSigned(n: number): string;
79
+ /** A count for the nav/tiles, thousands-separated. */
80
+ declare function formatCount(n: number): string;
81
+ /**
82
+ * An SVG polyline `d` for a sparkline over `values`, fit to a `width`×`height`
83
+ * box with a 2px inset. The vertical domain defaults to the data's own range
84
+ * (with 0 always included, so a signed series keeps its baseline meaningful);
85
+ * pass `min`/`max` to pin it — e.g. −100…100 for Confidence. Returns "" for
86
+ * fewer than two points (nothing to draw).
87
+ */
88
+ declare function sparklinePath(values: number[], width: number, height: number, min?: number, max?: number): string;
89
+
90
+ /** A colored status pill — tone from the status, the label shown verbatim. */
91
+ declare function StatusPill({ status }: {
92
+ status: string | null | undefined;
93
+ }): react.JSX.Element;
94
+ /**
95
+ * A risk bar (0–100) plus its number, both toned by threshold: a longer, redder
96
+ * bar means a riskier belief the eye should land on first (spec story 5).
97
+ */
98
+ declare function RiskBar({ risk }: {
99
+ risk: number;
100
+ }): react.JSX.Element;
101
+ /**
102
+ * A signed confidence reading: a tiny sparkline of its trajectory when a
103
+ * history is available, always the +/- number (negative reads crit) — so a row
104
+ * shows both where a belief stands and, when known, which way it is trending
105
+ * (spec story 6). With no history the number stands alone; the list endpoint
106
+ * carries no per-row series, so the in-row sparkline appears wherever a caller
107
+ * does have one (e.g. the drawer trajectory uses `Sparkline` directly).
108
+ */
109
+ declare function ConfidenceCell({ confidence, history, }: {
110
+ confidence: number;
111
+ history?: number[];
112
+ }): react.JSX.Element;
113
+ /**
114
+ * A signed sparkline over `values`. A zero baseline is drawn whenever the domain
115
+ * spans it, so a line dipping below zero reads as a belief losing ground. The
116
+ * last point gets a dot. `fill` shades the area under the line. Pure geometry
117
+ * comes from `sparklinePath`/`sparklineY`.
118
+ */
119
+ declare function Sparkline({ values, width, height, min, max, tone, fill, ariaLabel, }: {
120
+ values: number[];
121
+ width?: number;
122
+ height?: number;
123
+ min?: number;
124
+ max?: number;
125
+ tone?: Tone;
126
+ fill?: boolean;
127
+ ariaLabel?: string;
128
+ }): react.JSX.Element | null;
129
+ /**
130
+ * A glanceable stat tile — a label, a big number, and an optional sub-caption.
131
+ * Becomes a button when `onClick` is given (counts that double as nav). Pure
132
+ * presentation; the caller supplies the value.
133
+ */
134
+ declare function StatTile({ label, value, sub, onClick, active, }: {
135
+ label: string;
136
+ value: number;
137
+ sub?: ReactNode;
138
+ onClick?: () => void;
139
+ active?: boolean;
140
+ }): react.JSX.Element;
3
141
 
4
142
  type Counts = Partial<Record<Collection, number>>;
5
143
  interface UseCountsResult {
@@ -15,24 +153,43 @@ interface UseCountsResult {
15
153
  */
16
154
  declare function useCounts(basePath?: string): UseCountsResult;
17
155
 
156
+ interface RegisterNavProps {
157
+ /** The active register, highlighted with a clear active state (spec story 7). */
158
+ active: Collection;
159
+ /** Called when a register is chosen. */
160
+ onSelect: (register: Collection) => void;
161
+ /** Live per-register counts; a middle dot shows until they load. */
162
+ counts?: Counts | null;
163
+ /** Restrict/reorder the registers shown; defaults to all, grouped. */
164
+ registers?: Collection[];
165
+ }
166
+ /**
167
+ * The sidebar register nav — every register with its live count and an active
168
+ * state, grouped into the register set and reference data (spec story 7). A
169
+ * presentational brick: the caller owns which register is active and supplies
170
+ * the counts. The assembled `<ValidationOSDashboard />` composes this; it's also
171
+ * exported so anyone building their own surface can reuse it (the second level
172
+ * of entry). Styled with the package's own token sheet — no host Tailwind.
173
+ */
174
+ declare function RegisterNav({ active, onSelect, counts, registers, }: RegisterNavProps): react.JSX.Element;
175
+
18
176
  interface RegisterCountsProps {
19
177
  counts: Counts;
20
178
  /** Optional caption under the tiles (e.g. the backend the numbers came from). */
21
179
  caption?: string;
22
- /**
23
- * Optional link target per register. When supplied, each tile becomes a link
24
- * (a plain anchor works with any router), so counts double as navigation
25
- * into the browse tables.
26
- */
27
- hrefFor?: (register: Collection) => string;
180
+ /** Optional click handler per register — makes each tile a nav button. */
181
+ onSelect?: (register: Collection) => void;
182
+ /** The active register, highlighted when tiles are nav. */
183
+ active?: Collection | null;
28
184
  }
29
185
  /**
30
186
  * A glanceable grid of stat tiles — one per register, the register's row count
31
- * as the hero number. Presentational: the caller reads the counts (server-side,
32
- * through the adapter) and hands them in. Styled with Tailwind utility classes;
33
- * the host app provides Tailwind.
187
+ * as the hero number. A brick kept for a counts landing; the assembled app
188
+ * surfaces the same counts in the sidebar nav. Presentational: the caller reads
189
+ * the counts (server-side, through the adapter) and hands them in. Rendered in
190
+ * the package's own token sheet — no host Tailwind.
34
191
  */
35
- declare function RegisterCounts({ counts, caption, hrefFor }: RegisterCountsProps): react.JSX.Element;
192
+ declare function RegisterCounts({ counts, caption, onSelect, active, }: RegisterCountsProps): react.JSX.Element;
36
193
 
37
194
  interface RegisterTableProps {
38
195
  register: Collection;
@@ -44,9 +201,11 @@ interface RegisterTableProps {
44
201
  }
45
202
  /**
46
203
  * A list table for one register — a row per record, the register's key fields
47
- * as columns (assumptions show Impact, Confidence and Risk). Presentational:
48
- * the caller supplies the rows. Clicking a row opens the read-only drawer.
49
- * Styled with Tailwind utilities the host app provides.
204
+ * as columns. Assumptions read their state at a glance: a colored Status pill, a
205
+ * signed Confidence, and a threshold-toned Risk bar (spec stories 4–6). Which
206
+ * cells render as pills/bars/sparklines is declared on the column (`kind`), so
207
+ * the treatment stays testable at the columns seam and this component stays a
208
+ * dumb renderer. Presentational: the caller supplies the rows.
50
209
  */
51
210
  declare function RegisterTable({ register, records, onRowClick, selectedId, }: RegisterTableProps): react.JSX.Element;
52
211
 
@@ -60,27 +219,144 @@ interface RecordDrawerProps {
60
219
  /** Whether the drawer is open (a row is selected). */
61
220
  open: boolean;
62
221
  onClose: () => void;
222
+ /** API base path (default `/api`). */
223
+ basePath?: string;
224
+ /** Re-fetch the record + its list — called after a save, and the re-fetch
225
+ * path offered when a concurrent edit is detected. */
226
+ onChanged?: () => void;
227
+ /** Extra content below the fields in read mode — e.g. the relation editor. */
228
+ children?: ReactNode;
229
+ }
230
+ /**
231
+ * A record drawer that reads and edits, and wires relations. The derived
232
+ * numbers lead as a visually distinct "computed — not editable" hero (spec
233
+ * stories 4/10): a bordered box, each number big and mono, Confidence toned by
234
+ * sign and Risk by threshold, with a "Why?" reveal opening the understanding
235
+ * layer in the same accent/pill language (story 11). Editing recomputes those
236
+ * numbers server-side on save; a concurrent edit surfaces as a gentle, jargon-
237
+ * free prompt with a re-fetch path (story 12). In read mode the drawer hosts
238
+ * the relation editor (`children`). Chrome is shared via `DrawerShell`; styled
239
+ * with the package's own token sheet, no host Tailwind.
240
+ */
241
+ declare function RecordDrawer({ register, record, loading, error, open, onClose, basePath, onChanged, children, }: RecordDrawerProps): react.JSX.Element;
242
+
243
+ interface RecordFormProps {
244
+ register: Collection;
245
+ basePath?: string;
246
+ /** Called with the new record's id after a successful create. */
247
+ onCreated: (id: string) => void;
248
+ onCancel: () => void;
63
249
  }
64
250
  /**
65
- * A read-only drawer for one record. Any derived numbers lead as the hero,
66
- * explicitly marked computed not editable (spec user story 4), so a reader
67
- * trusts they follow the formulas; the record's own fields follow. This slice
68
- * is read-only; editing lands in a later slice.
251
+ * The "new record" form for one register (spec user story 13). Editable own-
252
+ * fields onlyderived numbers are computed server-side and marked
253
+ * computed-not-editable elsewhere; relations are wired by linking after the
254
+ * record exists. Presence-gap fields (5 Whys, etc.) appear as first-class
255
+ * textareas. On submit it POSTs through the API, which recomputes on write.
256
+ */
257
+ declare function RecordForm({ register, basePath, onCreated, onCancel, }: RecordFormProps): react.JSX.Element;
258
+
259
+ interface RelationEditorProps {
260
+ /** The register of the open record initiating the link. */
261
+ register: Collection;
262
+ /** The open record's id (the `from` end). */
263
+ recordId: string;
264
+ basePath?: string;
265
+ /** Called after a relation is wired, so the drawer can re-fetch. */
266
+ onLinked: () => void;
267
+ }
268
+ /**
269
+ * Wire a relation from the open record (spec user story 14). Pick an edge, then
270
+ * a target record from the register that edge points at; linking sets both ends
271
+ * server-side and recomputes derived fields. Registers with no outbound edges
272
+ * (glossary, people) render nothing.
273
+ */
274
+ declare function RelationEditor({ register, recordId, basePath, onLinked, }: RelationEditorProps): react.JSX.Element | null;
275
+
276
+ /**
277
+ * The understanding layer behind the Confidence "Why?" (OPS-1276): which
278
+ * experiments move the number (ranked by push) and how close each running
279
+ * experiment is to concluding, the goal/direct evidence that also moves it, and
280
+ * Confidence over time. It lazy-loads the readings + experiments registers (it
281
+ * only mounts when the Reveal is open), then derives everything through the
282
+ * shared derivation module. Restyled into the drawer's accent/pill language
283
+ * (spec story 11): signed push tracks and a signed trajectory sparkline, in the
284
+ * package's own token sheet — no host Tailwind.
69
285
  */
70
- declare function RecordDrawer({ register, record, loading, error, open, onClose, }: RecordDrawerProps): react.JSX.Element | null;
286
+ declare function UnderstandingPanel({ assumption, basePath, }: {
287
+ assumption: AnyRecord;
288
+ basePath?: string;
289
+ }): react.JSX.Element;
290
+
291
+ /**
292
+ * The understanding layer's data join (OPS-1276). Pure: given an assumption
293
+ * and the readings + experiments registers, it produces everything the Reveal
294
+ * shows — the experiments testing the belief (each with how hard it moves
295
+ * Confidence and how close it is to concluding), the goal/direct evidence that
296
+ * also moves the number, and the Confidence-over-time trajectory.
297
+ *
298
+ * The record → derivation-input mapping is `@validation-os/core`'s shared
299
+ * `toReadingInput`, so a reading is read here exactly as it is server-side.
300
+ */
301
+
302
+ /** An experiment testing this assumption: how hard it moves Confidence, and
303
+ * how close it is to concluding. `contribution` is 0 for a running experiment
304
+ * that has not produced a concluded reading yet — it still shows, so its
305
+ * progress-to-conclusion is visible. */
306
+ interface ExperimentView {
307
+ experimentId: string;
308
+ title: string | null;
309
+ status: string | null;
310
+ /** Signed push on Confidence; 0 until a concluded reading lands. */
311
+ contribution: number;
312
+ magnitude: number;
313
+ /** Concluded readings this experiment has produced for the belief. */
314
+ readingCount: number;
315
+ progress: Progress | null;
316
+ /** Concluded/closed — reads as done rather than in-flight. */
317
+ done: boolean;
318
+ }
319
+ /** Goal-rung or direct evidence that moves Confidence but is not an experiment. */
320
+ interface OtherMover {
321
+ key: string;
322
+ kind: Exclude<MoverKind, "experiment">;
323
+ goalId: string | null;
324
+ contribution: number;
325
+ magnitude: number;
326
+ readingCount: number;
327
+ }
328
+ interface Understanding {
329
+ /** The same Confidence the derived box shows. */
330
+ confidence: number;
331
+ /** Experiments testing this belief, ranked by how hard they push. */
332
+ experiments: ExperimentView[];
333
+ /** Goal/direct evidence that also moves the number. */
334
+ otherMovers: OtherMover[];
335
+ /** Confidence over time; empty when no concluded reading is dated. */
336
+ trajectory: TrajectoryPoint[];
337
+ /** Concluded readings feeding the number, across all sources. */
338
+ readingCount: number;
339
+ }
340
+ declare function buildUnderstanding(assumption: AnyRecord, readings: AnyRecord[], experiments: AnyRecord[]): Understanding;
71
341
 
72
342
  interface RegisterBrowserProps {
73
343
  register: Collection;
74
344
  /** API base path (default `/api`). */
75
345
  basePath?: string;
346
+ /** A one-line description under the register title (spec story 7/9). */
347
+ subtitle?: string;
76
348
  }
77
349
  /**
78
- * The browse-and-open surface for one register: a list table that opens a
79
- * read-only drawer on row click. Reads over HTTP through the Clerk-gated API
80
- * read routes (list + get), so the browser never touches Firestore directly.
81
- * The thin host app renders this with a `register` that's the whole page.
350
+ * The browse-create-edit surface for one register: a list table that opens a
351
+ * record drawer on row click, a "New" button that opens the create form, and a
352
+ * relation editor in the drawer for wiring links. All reads and writes go over
353
+ * HTTP through the Clerk-gated API (which recomputes derived fields on write),
354
+ * so the browser never touches Firestore directly. After an edit saves, both
355
+ * the drawer's record and the list re-fetch so recomputed derived numbers show
356
+ * everywhere. The thin host app renders this with a `register` — that's the
357
+ * whole page.
82
358
  */
83
- declare function RegisterBrowser({ register, basePath }: RegisterBrowserProps): react.JSX.Element;
359
+ declare function RegisterBrowser({ register, basePath, subtitle, }: RegisterBrowserProps): react.JSX.Element;
84
360
 
85
361
  interface UseListResult {
86
362
  records: AnyRecord[] | null;
@@ -94,12 +370,122 @@ interface UseRecordResult {
94
370
  record: AnyRecord | null;
95
371
  loading: boolean;
96
372
  error: string | null;
373
+ /** Re-fetch the record — the re-fetch path after a save or edit conflict. */
374
+ refresh: () => void;
97
375
  }
98
376
  /**
99
377
  * Fetch one record by id. `id` may be null (nothing open) — the hook stays
100
378
  * idle until an id is supplied, so it drives a drawer that opens on row click.
379
+ * `refresh` re-fetches: the edit flow calls it after a save, and it's the
380
+ * re-fetch path offered when a write hits a concurrent-edit conflict.
101
381
  */
102
382
  declare function useRecord(register: Collection, id: string | null, basePath?: string): UseRecordResult;
383
+ /** The outcome of a save: the recomputed record, or a rejection with a
384
+ * plain-language message (a concurrent-edit conflict, or another failure). */
385
+ type SaveResult = {
386
+ ok: true;
387
+ record: AnyRecord;
388
+ } | {
389
+ ok: false;
390
+ conflict: boolean;
391
+ message: string;
392
+ };
393
+ /**
394
+ * Map an update response (status + parsed body) to a `SaveResult`. Pure, so
395
+ * the 409/error/ok branching is unit-testable without a DOM. A 409 is a
396
+ * concurrent-edit conflict, surfaced in the API's plain-language copy (or the
397
+ * `CONFLICT_MESSAGE` fallback) — never version jargon (spec user story 12).
398
+ */
399
+ declare function interpretSave(status: number, body: unknown): SaveResult;
400
+ interface UseUpdateResult {
401
+ /** PATCH `{ version, ...patch }`; resolves to the recomputed record or a
402
+ * rejection. Never throws — callers branch on `result.ok`. */
403
+ save: (id: string, patch: Record<string, unknown>) => Promise<SaveResult>;
404
+ saving: boolean;
405
+ /** Plain-language conflict prompt when a concurrent edit was detected. */
406
+ conflict: string | null;
407
+ /** Plain-language message for any other save failure. */
408
+ error: string | null;
409
+ /** Clear conflict/error state (e.g. when re-entering edit mode). */
410
+ reset: () => void;
411
+ }
412
+ /**
413
+ * Version-guarded update over the API write route. A stale version comes back
414
+ * as a 409, which we surface as a gentle, jargon-free `conflict` message (spec
415
+ * user story 12) — the API sends the copy, and `CONFLICT_MESSAGE` is the
416
+ * fallback. Derived values are recomputed server-side, so the returned record
417
+ * carries the authoritative numbers, never anything the client computed.
418
+ */
419
+ declare function useUpdate(register: Collection, basePath?: string): UseUpdateResult;
420
+
421
+ interface UseCreateResult {
422
+ create: (data: Record<string, unknown>) => Promise<AnyRecord>;
423
+ saving: boolean;
424
+ error: string | null;
425
+ }
426
+ /** Create a record in `register`; the server stamps derived fields on write. */
427
+ declare function useCreate(register: Collection, basePath?: string): UseCreateResult;
428
+ interface LinkArgs {
429
+ relation: Relation;
430
+ from: {
431
+ register: Collection;
432
+ id: string;
433
+ };
434
+ to: {
435
+ register: Collection;
436
+ id: string;
437
+ };
438
+ }
439
+ interface UseLinkResult {
440
+ link: (args: LinkArgs) => Promise<void>;
441
+ linking: boolean;
442
+ error: string | null;
443
+ }
444
+ /** Wire a relation; the API sets both ends and recomputes derived fields. */
445
+ declare function useLink(basePath?: string): UseLinkResult;
446
+
447
+ /**
448
+ * The pure edit-logic seam — which fields a register lets you edit, how a
449
+ * form draft maps back to a version-guarded patch, and the plain-language
450
+ * conflict copy. Kept as pure data/functions (no DOM) so the edit behaviour
451
+ * is unit-testable exactly like `columns.ts`; the drawer component consumes it.
452
+ *
453
+ * Derived numbers (Confidence, Risk, Derived Impact, Strength) are never
454
+ * editable — they are computed server-side on write (spec user story 4/11),
455
+ * so they never appear here. Relation links are set through `link`, a separate
456
+ * story, so relation-id fields aren't editable inputs either.
457
+ */
458
+
459
+ /** The plain-language conflict prompt — never version jargon (user story 12).
460
+ * The API returns its own copy on a 409; this is the client-side fallback. */
461
+ declare const CONFLICT_MESSAGE: string;
462
+ type FieldKind = "text" | "textarea" | "number" | "select";
463
+ interface FieldEditor {
464
+ /** The record key this editor writes. */
465
+ key: string;
466
+ /** Plain-language label shown beside the input. */
467
+ label: string;
468
+ kind: FieldKind;
469
+ /** For `select`: the allowed options (a leading blank = "unset"). */
470
+ options?: readonly string[];
471
+ /** An empty input clears the field to `null` rather than "". */
472
+ nullable?: boolean;
473
+ }
474
+ /** The fields a register lets you edit, in render order. */
475
+ declare function editableFields(register: Collection): FieldEditor[];
476
+ /** A form draft is a plain string map keyed by field — what the inputs hold. */
477
+ type Draft = Record<string, string>;
478
+ /** Build the initial form draft from a record: every editable field becomes a
479
+ * string input value, and missing values become empty inputs. */
480
+ declare function draftFrom(register: Collection, record: AnyRecord): Draft;
481
+ /**
482
+ * A version-guarded patch from a draft: the loaded `version` plus only the
483
+ * fields whose value actually changed (so an untouched save doesn't churn
484
+ * unrelated fields). The API rejects a stale version with a 409.
485
+ */
486
+ declare function buildPatch(register: Collection, original: AnyRecord, draft: Draft): Record<string, unknown>;
487
+ /** Whether a draft differs from the record it was loaded from. */
488
+ declare function hasEdits(register: Collection, original: AnyRecord, draft: Draft): boolean;
103
489
 
104
490
  /**
105
491
  * Per-register table columns and value formatting — the presentational shape
@@ -108,6 +494,14 @@ declare function useRecord(register: Collection, id: string | null, basePath?: s
108
494
  * headers are plain language and derived numbers are shown, never hidden.
109
495
  */
110
496
 
497
+ /**
498
+ * How a cell renders. `text` is plain formatted text; `status` is a colored
499
+ * pill; `risk` is a threshold-toned bar + number; `confidence` is a signed
500
+ * number (with a sparkline when a trajectory is available). Keeping this on the
501
+ * column — not branching by key in the table — is what makes the visual
502
+ * treatment declarative and testable at this seam (spec story 13).
503
+ */
504
+ type CellKind = "text" | "status" | "risk" | "confidence";
111
505
  interface ColumnDef {
112
506
  /** Stable key; also the default field read from the record. */
113
507
  key: string;
@@ -119,6 +513,8 @@ interface ColumnDef {
119
513
  align?: "left" | "right";
120
514
  /** Marks a column whose value is computed, never hand-typed (spec story 4). */
121
515
  derived?: boolean;
516
+ /** How the cell renders; defaults to `text`. */
517
+ kind?: CellKind;
122
518
  }
123
519
  /** The columns to render for a register. */
124
520
  declare function columnsFor(register: Collection): ColumnDef[];
@@ -129,10 +525,76 @@ declare function formatValue(value: unknown): string;
129
525
  /** The record's headline for a row/drawer: Title, else Name, else its id. */
130
526
  declare function primaryLabel(record: AnyRecord): string;
131
527
 
528
+ /**
529
+ * The editable-field spec for the "new record" form, per register — kept as
530
+ * pure data + functions (like `columns.ts`) so the create form is unit-testable
531
+ * without a DOM. Only a record's own scalar fields appear here: derived numbers
532
+ * are computed server-side (never typed), and relations are wired separately by
533
+ * linking (see `relations.ts` / the relation editor), so id/relation fields are
534
+ * deliberately absent. Vocabularies mirror `core`'s type unions.
535
+ */
536
+
537
+ interface FormField {
538
+ /** Record field key (what the create payload uses). */
539
+ key: string;
540
+ /** Plain-language label a non-technical teammate reads. */
541
+ label: string;
542
+ kind: FieldKind;
543
+ /** Options for a `select`. */
544
+ options?: readonly string[];
545
+ /** A `select`/`text` value that should be stored as a number. */
546
+ coerce?: "number";
547
+ required?: boolean;
548
+ placeholder?: string;
549
+ }
550
+ /** The editable fields for a register's create form, in display order. */
551
+ declare function formFieldsFor(register: Collection): FormField[];
552
+ /** A blank draft: every field keyed to an empty string (form-friendly). */
553
+ declare function emptyDraft(register: Collection): Record<string, string>;
554
+ /** Labels of required fields the draft has left blank (submit-gating). */
555
+ declare function missingRequired(register: Collection, draft: Record<string, string>): string[];
556
+ /**
557
+ * Turn a string-keyed form draft into a create payload: numbers coerced, blank
558
+ * optional values dropped so the record stores only what was filled. Derived
559
+ * and relation fields are never here — the server computes the former and
560
+ * linking wires the latter.
561
+ */
562
+ declare function toCreatePayload(register: Collection, draft: Record<string, string>): Record<string, unknown>;
563
+
564
+ /**
565
+ * Which relations a record of a given register can initiate — the menu the
566
+ * relation editor offers. Derived from `core`'s single `RELATIONS` table (so
567
+ * the two never drift) and turned into plain-language choices, each naming the
568
+ * register whose records are valid link targets.
569
+ */
570
+
571
+ interface LinkChoice {
572
+ relation: Relation;
573
+ /** Plain-language name of the edge, from the initiating record's side. */
574
+ label: string;
575
+ /** The register to pick a target record from. */
576
+ targetRegister: Collection;
577
+ }
578
+ /** The relations a record of `register` can initiate, in table order. */
579
+ declare function linkChoicesFrom(register: Collection): LinkChoice[];
580
+
132
581
  /** Plain-language labels — the register is a surface a non-technical
133
582
  * teammate meets, so no jargon and no code-y plurals. */
134
583
  declare const REGISTER_LABEL: Record<Collection, string>;
584
+ /** Singular labels — for "New {thing}" affordances (never a naive de-pluralise
585
+ * that would read "New Peopl" / "New Glossary"). */
586
+ declare const REGISTER_SINGULAR: Record<Collection, string>;
135
587
  /** The order tiles read left-to-right, top-to-bottom. */
136
588
  declare const REGISTER_ORDER: Collection[];
589
+ /** A one-line description shown under each register's title (spec story 9). */
590
+ declare const REGISTER_SUBTITLE: Record<Collection, string>;
591
+ /** A single-glyph icon per register for the sidebar nav (matches the prototype
592
+ * feel; a glyph, not an icon font, so nothing external is pulled in). */
593
+ declare const REGISTER_ICON: Record<Collection, string>;
594
+ /** The sidebar groups the registers into the register set and reference data. */
595
+ declare const REGISTER_GROUPS: {
596
+ label: string;
597
+ registers: Collection[];
598
+ }[];
137
599
 
138
- export { type ColumnDef, type Counts, REGISTER_LABEL, REGISTER_ORDER, RecordDrawer, type RecordDrawerProps, RegisterBrowser, type RegisterBrowserProps, RegisterCounts, type RegisterCountsProps, RegisterTable, type RegisterTableProps, type UseCountsResult, type UseListResult, type UseRecordResult, cellValue, columnsFor, formatValue, primaryLabel, useCounts, useList, useRecord };
600
+ export { CONFLICT_MESSAGE, type CellKind, type ColumnDef, ConfidenceCell, type Counts, type DashboardConfig, type Draft, type ExperimentView, type FieldEditor, type FieldKind, type FormField, type LinkArgs, type LinkChoice, type OtherMover, REGISTER_GROUPS, REGISTER_ICON, REGISTER_LABEL, REGISTER_ORDER, REGISTER_SINGULAR, REGISTER_SUBTITLE, RISK_CRIT, RISK_WARN, RecordDrawer, type RecordDrawerProps, RecordForm, type RecordFormProps, RegisterBrowser, type RegisterBrowserProps, RegisterCounts, type RegisterCountsProps, RegisterNav, type RegisterNavProps, RegisterTable, type RegisterTableProps, RelationEditor, type RelationEditorProps, RiskBar, type SaveResult, Sparkline, StatTile, StatusPill, type Tone, type Understanding, UnderstandingPanel, type UseCountsResult, type UseCreateResult, type UseLinkResult, type UseListResult, type UseRecordResult, type UseUpdateResult, ValidationOSDashboard, type ValidationOSDashboardProps, buildPatch, buildUnderstanding, cellValue, columnsFor, confidenceTone, draftFrom, editableFields, emptyDraft, formFieldsFor, formatCount, formatSigned, formatValue, hasEdits, interpretSave, linkChoicesFrom, missingRequired, primaryLabel, riskFraction, riskLevel, sparklinePath, statusTone, toCreatePayload, useCounts, useCreate, useLink, useList, useRecord, useUpdate };