@validation-os/dashboard 0.5.0 → 0.6.1
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 +740 -42
- package/dist/index.js +3544 -993
- package/dist/index.js.map +1 -1
- package/dist/styles.css +1065 -0
- package/package.json +2 -2
package/dist/index.d.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
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';
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
8
|
* Everything the instance passes — config only, never secrets. The API base
|
|
@@ -39,13 +40,19 @@ interface ValidationOSDashboardProps {
|
|
|
39
40
|
}
|
|
40
41
|
/**
|
|
41
42
|
* The entire styled dashboard as one mountable app (spec OPS-1280): the frame —
|
|
42
|
-
* sidebar composing register nav
|
|
43
|
-
* indicator and user — and the
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
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.
|
|
49
56
|
*/
|
|
50
57
|
declare function ValidationOSDashboard({ config }: ValidationOSDashboardProps): react.JSX.Element;
|
|
51
58
|
|
|
@@ -65,9 +72,25 @@ type Tone = "good" | "warn" | "crit" | "accent" | "neutral";
|
|
|
65
72
|
* and whitespace-insensitive.
|
|
66
73
|
*/
|
|
67
74
|
declare function statusTone(status: string | null | undefined): Tone;
|
|
68
|
-
/**
|
|
69
|
-
|
|
70
|
-
|
|
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;
|
|
71
94
|
/** Risk (0–100) → a tone for the bar fill and the number. */
|
|
72
95
|
declare function riskLevel(risk: number): Tone;
|
|
73
96
|
/** Risk (0–100) → the bar's fill fraction (0–1), clamped. */
|
|
@@ -139,6 +162,174 @@ declare function StatTile({ label, value, sub, onClick, active, }: {
|
|
|
139
162
|
active?: boolean;
|
|
140
163
|
}): react.JSX.Element;
|
|
141
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;
|
|
332
|
+
|
|
142
333
|
type Counts = Partial<Record<Collection, number>>;
|
|
143
334
|
interface UseCountsResult {
|
|
144
335
|
counts: Counts | null;
|
|
@@ -152,26 +343,261 @@ interface UseCountsResult {
|
|
|
152
343
|
* HTTP, so no backend credentials ever reach the browser.
|
|
153
344
|
*/
|
|
154
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;
|
|
155
361
|
|
|
156
|
-
interface
|
|
157
|
-
/** The active
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
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;
|
|
161
368
|
/** Live per-register counts; a middle dot shows until they load. */
|
|
162
369
|
counts?: Counts | null;
|
|
163
|
-
/**
|
|
164
|
-
|
|
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;
|
|
165
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;
|
|
166
512
|
/**
|
|
167
|
-
*
|
|
168
|
-
*
|
|
169
|
-
*
|
|
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.
|
|
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.
|
|
173
516
|
*/
|
|
174
|
-
declare function
|
|
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;
|
|
175
601
|
|
|
176
602
|
interface RegisterCountsProps {
|
|
177
603
|
counts: Counts;
|
|
@@ -224,6 +650,9 @@ interface RecordDrawerProps {
|
|
|
224
650
|
/** Re-fetch the record + its list — called after a save, and the re-fetch
|
|
225
651
|
* path offered when a concurrent edit is detected. */
|
|
226
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;
|
|
227
656
|
/** Extra content below the fields in read mode — e.g. the relation editor. */
|
|
228
657
|
children?: ReactNode;
|
|
229
658
|
}
|
|
@@ -238,7 +667,7 @@ interface RecordDrawerProps {
|
|
|
238
667
|
* the relation editor (`children`). Chrome is shared via `DrawerShell`; styled
|
|
239
668
|
* with the package's own token sheet, no host Tailwind.
|
|
240
669
|
*/
|
|
241
|
-
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;
|
|
242
671
|
|
|
243
672
|
interface RecordFormProps {
|
|
244
673
|
register: Collection;
|
|
@@ -316,11 +745,11 @@ interface ExperimentView {
|
|
|
316
745
|
/** Concluded/closed — reads as done rather than in-flight. */
|
|
317
746
|
done: boolean;
|
|
318
747
|
}
|
|
319
|
-
/**
|
|
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). */
|
|
320
750
|
interface OtherMover {
|
|
321
751
|
key: string;
|
|
322
752
|
kind: Exclude<MoverKind, "experiment">;
|
|
323
|
-
goalId: string | null;
|
|
324
753
|
contribution: number;
|
|
325
754
|
magnitude: number;
|
|
326
755
|
readingCount: number;
|
|
@@ -345,18 +774,90 @@ interface RegisterBrowserProps {
|
|
|
345
774
|
basePath?: string;
|
|
346
775
|
/** A one-line description under the register title (spec story 7/9). */
|
|
347
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;
|
|
348
824
|
}
|
|
349
825
|
/**
|
|
350
|
-
*
|
|
351
|
-
*
|
|
352
|
-
*
|
|
353
|
-
*
|
|
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.
|
|
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.
|
|
358
830
|
*/
|
|
359
|
-
declare function
|
|
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;
|
|
843
|
+
}
|
|
844
|
+
/**
|
|
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.
|
|
850
|
+
*/
|
|
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;
|
|
360
861
|
|
|
361
862
|
interface UseListResult {
|
|
362
863
|
records: AnyRecord[] | null;
|
|
@@ -364,8 +865,13 @@ interface UseListResult {
|
|
|
364
865
|
error: string | null;
|
|
365
866
|
refresh: () => void;
|
|
366
867
|
}
|
|
367
|
-
/**
|
|
368
|
-
|
|
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;
|
|
369
875
|
interface UseRecordResult {
|
|
370
876
|
record: AnyRecord | null;
|
|
371
877
|
loading: boolean;
|
|
@@ -578,11 +1084,202 @@ interface LinkChoice {
|
|
|
578
1084
|
/** The relations a record of `register` can initiate, in table order. */
|
|
579
1085
|
declare function linkChoicesFrom(register: Collection): LinkChoice[];
|
|
580
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
|
+
|
|
581
1278
|
/** Plain-language labels — the register is a surface a non-technical
|
|
582
1279
|
* teammate meets, so no jargon and no code-y plurals. */
|
|
583
1280
|
declare const REGISTER_LABEL: Record<Collection, string>;
|
|
584
1281
|
/** Singular labels — for "New {thing}" affordances (never a naive de-pluralise
|
|
585
|
-
* that would read "New
|
|
1282
|
+
* that would read "New Glossary"). */
|
|
586
1283
|
declare const REGISTER_SINGULAR: Record<Collection, string>;
|
|
587
1284
|
/** The order tiles read left-to-right, top-to-bottom. */
|
|
588
1285
|
declare const REGISTER_ORDER: Collection[];
|
|
@@ -591,10 +1288,11 @@ declare const REGISTER_SUBTITLE: Record<Collection, string>;
|
|
|
591
1288
|
/** A single-glyph icon per register for the sidebar nav (matches the prototype
|
|
592
1289
|
* feel; a glyph, not an icon font, so nothing external is pulled in). */
|
|
593
1290
|
declare const REGISTER_ICON: Record<Collection, string>;
|
|
594
|
-
/** The sidebar groups
|
|
1291
|
+
/** The sidebar groups — all five registers now sit in one set (the retired
|
|
1292
|
+
* `people` reference collection was the only "Reference" member). */
|
|
595
1293
|
declare const REGISTER_GROUPS: {
|
|
596
1294
|
label: string;
|
|
597
1295
|
registers: Collection[];
|
|
598
1296
|
}[];
|
|
599
1297
|
|
|
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,
|
|
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 };
|