@validation-os/dashboard 0.3.0 → 0.4.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,7 @@
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';
3
5
 
4
6
  type Counts = Partial<Record<Collection, number>>;
5
7
  interface UseCountsResult {
@@ -60,14 +62,123 @@ interface RecordDrawerProps {
60
62
  /** Whether the drawer is open (a row is selected). */
61
63
  open: boolean;
62
64
  onClose: () => void;
65
+ /** API base path (default `/api`). */
66
+ basePath?: string;
67
+ /** Re-fetch the record + its list — called after a save, and the re-fetch
68
+ * path offered when a concurrent edit is detected. */
69
+ onChanged?: () => void;
70
+ /** Extra content below the fields in read mode — e.g. the relation editor. */
71
+ children?: ReactNode;
72
+ }
73
+ /**
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`.
82
+ */
83
+ declare function RecordDrawer({ register, record, loading, error, open, onClose, basePath, onChanged, children, }: RecordDrawerProps): react.JSX.Element;
84
+
85
+ interface RecordFormProps {
86
+ register: Collection;
87
+ basePath?: string;
88
+ /** Called with the new record's id after a successful create. */
89
+ onCreated: (id: string) => void;
90
+ onCancel: () => void;
63
91
  }
64
92
  /**
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.
93
+ * The "new record" form for one register (spec user story 13). Editable own-
94
+ * fields onlyderived numbers are computed server-side and marked
95
+ * computed-not-editable elsewhere; relations are wired by linking after the
96
+ * record exists. Presence-gap fields (5 Whys, etc.) appear as first-class
97
+ * textareas. On submit it POSTs through the API, which recomputes on write.
98
+ */
99
+ declare function RecordForm({ register, basePath, onCreated, onCancel, }: RecordFormProps): react.JSX.Element;
100
+
101
+ interface RelationEditorProps {
102
+ /** The register of the open record initiating the link. */
103
+ register: Collection;
104
+ /** The open record's id (the `from` end). */
105
+ recordId: string;
106
+ basePath?: string;
107
+ /** Called after a relation is wired, so the drawer can re-fetch. */
108
+ onLinked: () => void;
109
+ }
110
+ /**
111
+ * Wire a relation from the open record (spec user story 14). Pick an edge, then
112
+ * a target record from the register that edge points at; linking sets both ends
113
+ * server-side and recomputes derived fields. Registers with no outbound edges
114
+ * (glossary, people) render nothing.
115
+ */
116
+ declare function RelationEditor({ register, recordId, basePath, onLinked, }: RelationEditorProps): react.JSX.Element | null;
117
+
118
+ /**
119
+ * The understanding layer behind the Confidence "Why?" (OPS-1276): which
120
+ * 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.
126
+ */
127
+ declare function UnderstandingPanel({ assumption, basePath, }: {
128
+ assumption: AnyRecord;
129
+ basePath?: string;
130
+ }): react.JSX.Element;
131
+
132
+ /**
133
+ * The understanding layer's data join (OPS-1276). Pure: given an assumption
134
+ * and the readings + experiments registers, it produces everything the Reveal
135
+ * shows — the experiments testing the belief (each with how hard it moves
136
+ * Confidence and how close it is to concluding), the goal/direct evidence that
137
+ * also moves the number, and the Confidence-over-time trajectory.
138
+ *
139
+ * The record → derivation-input mapping is `@validation-os/core`'s shared
140
+ * `toReadingInput`, so a reading is read here exactly as it is server-side.
69
141
  */
70
- declare function RecordDrawer({ register, record, loading, error, open, onClose, }: RecordDrawerProps): react.JSX.Element | null;
142
+
143
+ /** An experiment testing this assumption: how hard it moves Confidence, and
144
+ * how close it is to concluding. `contribution` is 0 for a running experiment
145
+ * that has not produced a concluded reading yet — it still shows, so its
146
+ * progress-to-conclusion is visible. */
147
+ interface ExperimentView {
148
+ experimentId: string;
149
+ title: string | null;
150
+ status: string | null;
151
+ /** Signed push on Confidence; 0 until a concluded reading lands. */
152
+ contribution: number;
153
+ magnitude: number;
154
+ /** Concluded readings this experiment has produced for the belief. */
155
+ readingCount: number;
156
+ progress: Progress | null;
157
+ /** Concluded/closed — reads as done rather than in-flight. */
158
+ done: boolean;
159
+ }
160
+ /** Goal-rung or direct evidence that moves Confidence but is not an experiment. */
161
+ interface OtherMover {
162
+ key: string;
163
+ kind: Exclude<MoverKind, "experiment">;
164
+ goalId: string | null;
165
+ contribution: number;
166
+ magnitude: number;
167
+ readingCount: number;
168
+ }
169
+ interface Understanding {
170
+ /** The same Confidence the derived box shows. */
171
+ confidence: number;
172
+ /** Experiments testing this belief, ranked by how hard they push. */
173
+ experiments: ExperimentView[];
174
+ /** Goal/direct evidence that also moves the number. */
175
+ otherMovers: OtherMover[];
176
+ /** Confidence over time; empty when no concluded reading is dated. */
177
+ trajectory: TrajectoryPoint[];
178
+ /** Concluded readings feeding the number, across all sources. */
179
+ readingCount: number;
180
+ }
181
+ declare function buildUnderstanding(assumption: AnyRecord, readings: AnyRecord[], experiments: AnyRecord[]): Understanding;
71
182
 
72
183
  interface RegisterBrowserProps {
73
184
  register: Collection;
@@ -75,10 +186,14 @@ interface RegisterBrowserProps {
75
186
  basePath?: string;
76
187
  }
77
188
  /**
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.
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.
82
197
  */
83
198
  declare function RegisterBrowser({ register, basePath }: RegisterBrowserProps): react.JSX.Element;
84
199
 
@@ -94,12 +209,122 @@ interface UseRecordResult {
94
209
  record: AnyRecord | null;
95
210
  loading: boolean;
96
211
  error: string | null;
212
+ /** Re-fetch the record — the re-fetch path after a save or edit conflict. */
213
+ refresh: () => void;
97
214
  }
98
215
  /**
99
216
  * Fetch one record by id. `id` may be null (nothing open) — the hook stays
100
217
  * idle until an id is supplied, so it drives a drawer that opens on row click.
218
+ * `refresh` re-fetches: the edit flow calls it after a save, and it's the
219
+ * re-fetch path offered when a write hits a concurrent-edit conflict.
101
220
  */
102
221
  declare function useRecord(register: Collection, id: string | null, basePath?: string): UseRecordResult;
222
+ /** The outcome of a save: the recomputed record, or a rejection with a
223
+ * plain-language message (a concurrent-edit conflict, or another failure). */
224
+ type SaveResult = {
225
+ ok: true;
226
+ record: AnyRecord;
227
+ } | {
228
+ ok: false;
229
+ conflict: boolean;
230
+ message: string;
231
+ };
232
+ /**
233
+ * Map an update response (status + parsed body) to a `SaveResult`. Pure, so
234
+ * the 409/error/ok branching is unit-testable without a DOM. A 409 is a
235
+ * concurrent-edit conflict, surfaced in the API's plain-language copy (or the
236
+ * `CONFLICT_MESSAGE` fallback) — never version jargon (spec user story 12).
237
+ */
238
+ declare function interpretSave(status: number, body: unknown): SaveResult;
239
+ interface UseUpdateResult {
240
+ /** PATCH `{ version, ...patch }`; resolves to the recomputed record or a
241
+ * rejection. Never throws — callers branch on `result.ok`. */
242
+ save: (id: string, patch: Record<string, unknown>) => Promise<SaveResult>;
243
+ saving: boolean;
244
+ /** Plain-language conflict prompt when a concurrent edit was detected. */
245
+ conflict: string | null;
246
+ /** Plain-language message for any other save failure. */
247
+ error: string | null;
248
+ /** Clear conflict/error state (e.g. when re-entering edit mode). */
249
+ reset: () => void;
250
+ }
251
+ /**
252
+ * Version-guarded update over the API write route. A stale version comes back
253
+ * as a 409, which we surface as a gentle, jargon-free `conflict` message (spec
254
+ * user story 12) — the API sends the copy, and `CONFLICT_MESSAGE` is the
255
+ * fallback. Derived values are recomputed server-side, so the returned record
256
+ * carries the authoritative numbers, never anything the client computed.
257
+ */
258
+ declare function useUpdate(register: Collection, basePath?: string): UseUpdateResult;
259
+
260
+ interface UseCreateResult {
261
+ create: (data: Record<string, unknown>) => Promise<AnyRecord>;
262
+ saving: boolean;
263
+ error: string | null;
264
+ }
265
+ /** Create a record in `register`; the server stamps derived fields on write. */
266
+ declare function useCreate(register: Collection, basePath?: string): UseCreateResult;
267
+ interface LinkArgs {
268
+ relation: Relation;
269
+ from: {
270
+ register: Collection;
271
+ id: string;
272
+ };
273
+ to: {
274
+ register: Collection;
275
+ id: string;
276
+ };
277
+ }
278
+ interface UseLinkResult {
279
+ link: (args: LinkArgs) => Promise<void>;
280
+ linking: boolean;
281
+ error: string | null;
282
+ }
283
+ /** Wire a relation; the API sets both ends and recomputes derived fields. */
284
+ declare function useLink(basePath?: string): UseLinkResult;
285
+
286
+ /**
287
+ * The pure edit-logic seam — which fields a register lets you edit, how a
288
+ * form draft maps back to a version-guarded patch, and the plain-language
289
+ * conflict copy. Kept as pure data/functions (no DOM) so the edit behaviour
290
+ * is unit-testable exactly like `columns.ts`; the drawer component consumes it.
291
+ *
292
+ * Derived numbers (Confidence, Risk, Derived Impact, Strength) are never
293
+ * editable — they are computed server-side on write (spec user story 4/11),
294
+ * so they never appear here. Relation links are set through `link`, a separate
295
+ * story, so relation-id fields aren't editable inputs either.
296
+ */
297
+
298
+ /** The plain-language conflict prompt — never version jargon (user story 12).
299
+ * The API returns its own copy on a 409; this is the client-side fallback. */
300
+ declare const CONFLICT_MESSAGE: string;
301
+ type FieldKind = "text" | "textarea" | "number" | "select";
302
+ interface FieldEditor {
303
+ /** The record key this editor writes. */
304
+ key: string;
305
+ /** Plain-language label shown beside the input. */
306
+ label: string;
307
+ kind: FieldKind;
308
+ /** For `select`: the allowed options (a leading blank = "unset"). */
309
+ options?: readonly string[];
310
+ /** An empty input clears the field to `null` rather than "". */
311
+ nullable?: boolean;
312
+ }
313
+ /** The fields a register lets you edit, in render order. */
314
+ declare function editableFields(register: Collection): FieldEditor[];
315
+ /** A form draft is a plain string map keyed by field — what the inputs hold. */
316
+ type Draft = Record<string, string>;
317
+ /** Build the initial form draft from a record: every editable field becomes a
318
+ * string input value, and missing values become empty inputs. */
319
+ declare function draftFrom(register: Collection, record: AnyRecord): Draft;
320
+ /**
321
+ * A version-guarded patch from a draft: the loaded `version` plus only the
322
+ * fields whose value actually changed (so an untouched save doesn't churn
323
+ * unrelated fields). The API rejects a stale version with a 409.
324
+ */
325
+ declare function buildPatch(register: Collection, original: AnyRecord, draft: Draft): Record<string, unknown>;
326
+ /** Whether a draft differs from the record it was loaded from. */
327
+ declare function hasEdits(register: Collection, original: AnyRecord, draft: Draft): boolean;
103
328
 
104
329
  /**
105
330
  * Per-register table columns and value formatting — the presentational shape
@@ -129,10 +354,66 @@ declare function formatValue(value: unknown): string;
129
354
  /** The record's headline for a row/drawer: Title, else Name, else its id. */
130
355
  declare function primaryLabel(record: AnyRecord): string;
131
356
 
357
+ /**
358
+ * The editable-field spec for the "new record" form, per register — kept as
359
+ * pure data + functions (like `columns.ts`) so the create form is unit-testable
360
+ * without a DOM. Only a record's own scalar fields appear here: derived numbers
361
+ * are computed server-side (never typed), and relations are wired separately by
362
+ * linking (see `relations.ts` / the relation editor), so id/relation fields are
363
+ * deliberately absent. Vocabularies mirror `core`'s type unions.
364
+ */
365
+
366
+ interface FormField {
367
+ /** Record field key (what the create payload uses). */
368
+ key: string;
369
+ /** Plain-language label a non-technical teammate reads. */
370
+ label: string;
371
+ kind: FieldKind;
372
+ /** Options for a `select`. */
373
+ options?: readonly string[];
374
+ /** A `select`/`text` value that should be stored as a number. */
375
+ coerce?: "number";
376
+ required?: boolean;
377
+ placeholder?: string;
378
+ }
379
+ /** The editable fields for a register's create form, in display order. */
380
+ declare function formFieldsFor(register: Collection): FormField[];
381
+ /** A blank draft: every field keyed to an empty string (form-friendly). */
382
+ declare function emptyDraft(register: Collection): Record<string, string>;
383
+ /** Labels of required fields the draft has left blank (submit-gating). */
384
+ declare function missingRequired(register: Collection, draft: Record<string, string>): string[];
385
+ /**
386
+ * Turn a string-keyed form draft into a create payload: numbers coerced, blank
387
+ * optional values dropped so the record stores only what was filled. Derived
388
+ * and relation fields are never here — the server computes the former and
389
+ * linking wires the latter.
390
+ */
391
+ declare function toCreatePayload(register: Collection, draft: Record<string, string>): Record<string, unknown>;
392
+
393
+ /**
394
+ * Which relations a record of a given register can initiate — the menu the
395
+ * relation editor offers. Derived from `core`'s single `RELATIONS` table (so
396
+ * the two never drift) and turned into plain-language choices, each naming the
397
+ * register whose records are valid link targets.
398
+ */
399
+
400
+ interface LinkChoice {
401
+ relation: Relation;
402
+ /** Plain-language name of the edge, from the initiating record's side. */
403
+ label: string;
404
+ /** The register to pick a target record from. */
405
+ targetRegister: Collection;
406
+ }
407
+ /** The relations a record of `register` can initiate, in table order. */
408
+ declare function linkChoicesFrom(register: Collection): LinkChoice[];
409
+
132
410
  /** Plain-language labels — the register is a surface a non-technical
133
411
  * teammate meets, so no jargon and no code-y plurals. */
134
412
  declare const REGISTER_LABEL: Record<Collection, string>;
413
+ /** Singular labels — for "New {thing}" affordances (never a naive de-pluralise
414
+ * that would read "New Peopl" / "New Glossary"). */
415
+ declare const REGISTER_SINGULAR: Record<Collection, string>;
135
416
  /** The order tiles read left-to-right, top-to-bottom. */
136
417
  declare const REGISTER_ORDER: Collection[];
137
418
 
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 };
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 };