@validation-os/dashboard 0.2.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 +292 -11
- package/dist/index.js +1218 -84
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import * as react from 'react';
|
|
2
|
-
import {
|
|
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
|
-
*
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
93
|
+
* The "new record" form for one register (spec user story 13). Editable own-
|
|
94
|
+
* fields only — derived 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
|
-
|
|
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-
|
|
79
|
-
*
|
|
80
|
-
*
|
|
81
|
-
*
|
|
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 };
|