@modernrelay/orbit-core 0.2.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.
@@ -0,0 +1,1198 @@
1
+ /**
2
+ * Typed error taxonomy (spec §6.5, v0.2 subset of call sites).
3
+ *
4
+ * Structural fatality rule: a *transient* context loss never mints a
5
+ * GraphError — it is a status transition ('lost') plus a 'context-lost'
6
+ * warning diagnostic, and the 'error' event does not fire. A GraphError with
7
+ * code 'context-lost' is constructed ONLY on terminal recovery failure, where
8
+ * it is always fatal. This keeps fatality structural rather than conditional.
9
+ */
10
+ /** Placeholder until resource admission control lands (plan S4-T06). */
11
+ interface ResourceAdmissionReport {
12
+ reason: string;
13
+ estimatedBytes?: number;
14
+ limitBytes?: number;
15
+ }
16
+ /** Instance-level faults, delivered via the 'error' event's `detail`. */
17
+ type GraphError = {
18
+ code: 'engine-unsupported';
19
+ detail: string;
20
+ } | {
21
+ code: 'resource-limit';
22
+ detail: ResourceAdmissionReport;
23
+ fatal: boolean;
24
+ }
25
+ /** Minted only on terminal context-recovery failure — always fatal. */
26
+ | {
27
+ code: 'context-lost';
28
+ } | {
29
+ code: 'service-failed';
30
+ service: 'search' | 'expansion' | 'metrics' | 'path' | 'crossfilter';
31
+ cause: unknown;
32
+ };
33
+ /**
34
+ * Operation-scoped failures (spec §6.5): rejected promises / thrown calls.
35
+ * All ten codes are typed now even where their subsystems are post-v0.2 —
36
+ * the union is the public contract, the wiring arrives with each subsystem.
37
+ */
38
+ type GraphOperationError = {
39
+ code: 'export-too-large';
40
+ elementCount: number;
41
+ limit: number;
42
+ } | {
43
+ code: 'export-materialization-too-large';
44
+ rowCount: number;
45
+ limit: number;
46
+ } | {
47
+ code: 'queue-overflow';
48
+ queuedBytes: number;
49
+ limit: number;
50
+ } | {
51
+ code: 'invalid-view-state';
52
+ detail: string;
53
+ } | {
54
+ code: 'invalid-ingest-sequence';
55
+ expected: number;
56
+ received: number;
57
+ conflict: boolean;
58
+ } | {
59
+ code: 'ingest-session-closed';
60
+ } | {
61
+ code: 'stale-revision';
62
+ expected: number;
63
+ actual: number;
64
+ } | {
65
+ code: 'overlay-id-conflict';
66
+ overlayId: string;
67
+ } | {
68
+ code: 'resource-limit';
69
+ detail: ResourceAdmissionReport;
70
+ } | {
71
+ code: 'aborted';
72
+ cause?: unknown;
73
+ };
74
+ /** Lifecycle phase an instance-level error occurred in. */
75
+ type ErrorPhase = 'mount' | 'running' | 'recovery';
76
+ /** Spec §6.5 fatality matrix as a pure decision function. */
77
+ declare function isFatalGraphError(error: GraphError, phase: ErrorPhase): boolean;
78
+ /**
79
+ * Spec §6.5 rule for a resource-limit producer deciding `fatal`: rejection is
80
+ * fatal before the first accepted scene or during terminal recovery; a later
81
+ * inadmissible declarative update keeps the previous scene (non-fatal).
82
+ */
83
+ declare function resourceLimitFatal(phase: ErrorPhase, hasAcceptedScene: boolean): boolean;
84
+ /** Error class carried by rejected/thrown operations; `.detail` is typed. */
85
+ declare class OrbitOperationError extends Error {
86
+ readonly name = "OrbitOperationError";
87
+ readonly detail: GraphOperationError;
88
+ constructor(detail: GraphOperationError, message?: string);
89
+ }
90
+ /** Human-readable Error for a GraphError (used as the 'error' event payload). */
91
+ declare function graphErrorToError(graphError: GraphError, cause?: Error): Error;
92
+
93
+ /**
94
+ * orbit-core public data model (spec §5, v0.1 subset).
95
+ *
96
+ * The public model is object-based, id-keyed, and generic over caller attribute
97
+ * types. A `GraphSnapshot` is the declarative source of truth; the core keeps a
98
+ * derived index model and drives the engine imperatively (§4).
99
+ */
100
+
101
+ type NodeId = string;
102
+ type EdgeId = string;
103
+ /** Plain JSON value — the shape `dataRef` and other verbatim host payloads
104
+ * must fit (§16.14: stored, round-tripped, compared canonically, NEVER
105
+ * interpreted). */
106
+ type JsonValue = null | boolean | number | string | JsonValue[] | {
107
+ [key: string]: JsonValue;
108
+ };
109
+ interface GraphNode<N = Record<string, unknown>> {
110
+ id: NodeId;
111
+ attrs?: N;
112
+ /** Optional fixed/persisted position (layout 'fixed' honors these; §10). */
113
+ x?: number;
114
+ y?: number;
115
+ }
116
+ interface GraphEdge<E = Record<string, unknown>> {
117
+ /**
118
+ * Optional stable id. When absent, the core synthesizes a deterministic id
119
+ * `${escapedSource}→${escapedTarget}#${k}` where `\\`, `→`, and `#` are
120
+ * backslash-escaped inside endpoint ids, and k disambiguates parallel edges
121
+ * in first-occurrence order (§5). Simple endpoint ids retain the familiar
122
+ * `${source}→${target}#${k}` form.
123
+ */
124
+ id?: EdgeId;
125
+ source: NodeId;
126
+ target: NodeId;
127
+ attrs?: E;
128
+ }
129
+ /** Versioned snapshot — the declarative source of truth (§4, §5). */
130
+ interface GraphSnapshot<N = Record<string, unknown>, E = Record<string, unknown>> {
131
+ /** Identity of the dataset; changing it clears all per-dataset state (§5). */
132
+ datasetKey: string;
133
+ /** Caller-owned revision; same {datasetKey, sourceRevision} replays are idempotent (§5). */
134
+ sourceRevision: number | string;
135
+ nodes: readonly GraphNode<N>[];
136
+ edges: readonly GraphEdge<E>[];
137
+ }
138
+ type DiagnosticSeverity = 'info' | 'warning' | 'error';
139
+ type DiagnosticCode = 'duplicate-node-id' | 'duplicate-edge-id' | 'dangling-edge-endpoint' | 'invalid-node' | 'invalid-edge' | 'self-loop-retained'
140
+ /** A filter predicate threw or an expr referenced bad data; aggregated (§9.1). */
141
+ | 'filter-error'
142
+ /** Async metric column rejected (misaligned/duplicate/unknown ids; §12). */
143
+ | 'metric-column-error'
144
+ /** A channel reprojected repeatedly with identical outputs (§8 dev). */
145
+ | 'accessor-churn'
146
+ /** Image atlas resolve/decoding failures, cadence-batched (§8). */
147
+ | 'image-resolve-failed' | 'source-revision-reused'
148
+ /** A host config lane was rejected at the boundary (§5.1/§16.3 — e.g. a
149
+ * groups array whose containment is cyclic or multiply parented); the
150
+ * previous config stays live. */
151
+ | 'config-error'
152
+ /** §16.14: a setViewState payload failed structural validation or carries
153
+ * a version newer than this library; NOTHING was applied. */
154
+ | 'invalid-view-state' | 'engine-error' | 'accessor-error'
155
+ /** A user event listener threw; isolated per §15 (the chain continues). */
156
+ | 'listener-error'
157
+ /** showLabelsFor exceeded tracked-label capacity; omissions counted (§14). */
158
+ | 'label-overload'
159
+ /** A same-id row from an earlier overlay won in admission order (§7.5). */
160
+ | 'overlay-node-shadowed'
161
+ /** A service call was aborted/discarded before admission (§9.2; info). */
162
+ | 'service-aborted'
163
+ /** A service call failed (§9.2; error). */
164
+ | 'service-error' | 'context-lost' | 'operation-rejected'
165
+ /** Adapter-defined codes are namespaced (spec §6.5 routing note). */
166
+ | `engine:${string}`;
167
+ declare const DIAGNOSTIC_SAMPLE_CAP = 10;
168
+ interface GraphDiagnostic {
169
+ code: DiagnosticCode;
170
+ severity: DiagnosticSeverity;
171
+ /** Total occurrences in the pass this diagnostic summarizes. */
172
+ count: number;
173
+ /** At most DIAGNOSTIC_SAMPLE_CAP offending ids. */
174
+ sampleIds: readonly string[];
175
+ message: string;
176
+ }
177
+ interface Revisions {
178
+ /** Last accepted caller sourceRevision (null before first accept). */
179
+ source: number | string | null;
180
+ /** Monotonic counter advanced on every accepted model change. */
181
+ model: number;
182
+ /** Filtering/subgraph scope revision (§9). Advances with every accepted
183
+ * model change AND on every hard-scope (subgraph) change; a SCOPE-ONLY
184
+ * change advances `scope` and `render` but NOT `model` — the first genuine
185
+ * scope/model split (v0.5, §9.2). */
186
+ scope: number;
187
+ /** Monotonic counter advanced on every desired-render publication. */
188
+ render: number;
189
+ /** Highest render revision the engine has visibly applied (null pre-mount). */
190
+ appliedRender: number | null;
191
+ }
192
+ interface AcceptedEdge<E = Record<string, unknown>> extends GraphEdge<E> {
193
+ id: EdgeId;
194
+ }
195
+ interface AcceptedGraph<N = Record<string, unknown>, E = Record<string, unknown>> {
196
+ datasetKey: string;
197
+ sourceRevision: number | string;
198
+ /** Deduplicated (first-wins), in accepted-base order (§5.1, §16.2). */
199
+ nodes: readonly GraphNode<N>[];
200
+ /** Dangling endpoints dropped; ids present (synthesized when needed). */
201
+ edges: readonly AcceptedEdge<E>[];
202
+ /** id → position in `nodes` (accepted-base order). */
203
+ nodeIndex: ReadonlyMap<NodeId, number>;
204
+ diagnostics: readonly GraphDiagnostic[];
205
+ }
206
+ interface RenderScene {
207
+ count: number;
208
+ linkCount: number;
209
+ /** engine index → node id. */
210
+ idByIndex: readonly NodeId[];
211
+ /** node id → engine index. */
212
+ indexById: ReadonlyMap<NodeId, number>;
213
+ /** engine link index → edge id. */
214
+ edgeIdByIndex: readonly EdgeId[];
215
+ /**
216
+ * 2*count floats. NaN pairs mean "no known position" — the engine seeds
217
+ * them (§7.3); known positions come from the position cache.
218
+ */
219
+ positions: Float32Array;
220
+ /** 2*linkCount uint32 endpoint indices into the point set. */
221
+ links: Uint32Array;
222
+ /**
223
+ * §16.3 stage-3 synthetic suffix (S12). Present iff the scene was rewritten
224
+ * by collapsed groups: point slots >= physicalPointCount are super-nodes
225
+ * and link slots >= physicalLinkCount are meta-edges (synthetics are always
226
+ * a contiguous suffix). For those slots, idByIndex/edgeIdByIndex hold
227
+ * INTERNAL scene keys that never escape public payloads (§7.4) — consumers
228
+ * resolve slots through the discriminated ScenePointRef/SceneLinkRef
229
+ * helpers instead.
230
+ */
231
+ groups?: SceneGroups;
232
+ }
233
+ /** §16.3 compact synthetic-suffix descriptor attached to a rewritten scene. */
234
+ interface SceneGroups {
235
+ physicalPointCount: number;
236
+ physicalLinkCount: number;
237
+ /** Aligned to point slots physicalPointCount..count-1. */
238
+ superNodes: readonly ResolvedGroup[];
239
+ /** Aligned to link slots physicalLinkCount..linkCount-1. */
240
+ metaEdges: readonly MetaEdge[];
241
+ /**
242
+ * §16.3 node folds: representatives that are REAL nodes, so they carry no
243
+ * synthetic slot and never appear in `superNodes`. A folded anchor keeps
244
+ * its physical row (and its own caller-driven styling, R-6.1-14) — this
245
+ * list only reports how many descendants it currently stands for, for
246
+ * badge rendering. Empty when nothing is folded.
247
+ */
248
+ folds: readonly SceneFold[];
249
+ }
250
+ /** One drawn fold anchor and the descendant count it currently hides. */
251
+ interface SceneFold {
252
+ anchorId: NodeId;
253
+ hiddenCount: number;
254
+ }
255
+ /** §7.4 discriminated point ref: a physical node id or a resolved group —
256
+ * public namespaces only, never internal scene keys. */
257
+ type ScenePointRef = {
258
+ kind: 'node';
259
+ id: NodeId;
260
+ } | {
261
+ kind: 'group';
262
+ group: ResolvedGroup;
263
+ };
264
+ /** §7.4 discriminated link ref: a physical edge id or a meta-edge record. */
265
+ type SceneLinkRef = {
266
+ kind: 'edge';
267
+ id: EdgeId;
268
+ } | {
269
+ kind: 'meta-edge';
270
+ metaEdge: MetaEdge;
271
+ };
272
+ type Accessor<T, V> = V | ((item: T) => V);
273
+ type LayoutKind = 'force' | 'fixed';
274
+ /**
275
+ * §10 force tunables under stable, engine-neutral names — orbit maps them onto
276
+ * the active engine's parameters through atomic config-only commits (§13), so
277
+ * a value here never resets positions or restarts the layout.
278
+ *
279
+ * Every field is optional and OMISSION MEANS "leave the engine's default
280
+ * alone" — it is never written as an explicit value. The defaults quoted below
281
+ * are cosmos 3.3.0's (`defaultConfigValues`), listed so a host knows what it is
282
+ * overriding; an engine without a given force ignores that field.
283
+ *
284
+ * NOT here: `spaceSize` is a construction option on the adapter, not a runtime
285
+ * tunable (cosmos documents that large values crash some devices, and the
286
+ * seeding ring is derived from it).
287
+ */
288
+ interface SimulationConfig {
289
+ /** Pull toward the layout centre. Default 0.25. */
290
+ gravity?: number;
291
+ /** How hard every node pushes every other away — the spread. Default 1. */
292
+ repulsion?: number;
293
+ /** Velocity retained per tick: lower settles sooner, higher keeps drifting.
294
+ * Default 0.85. */
295
+ friction?: number;
296
+ /** Rest length of an edge spring. Default 10. */
297
+ linkDistance?: number;
298
+ /** Edge spring stiffness. Default 1. */
299
+ linkSpring?: number;
300
+ /**
301
+ * Cool-down coefficient — how fast the run loses energy and comes to rest.
302
+ * SMALLER cools slower (a longer, more thorough settle); larger snaps to a
303
+ * stop. Default 5000.
304
+ */
305
+ decay?: number;
306
+ /**
307
+ * Overlap resolution: above 0, nodes push apart when their circles
308
+ * intersect. Default 0 (OFF) — the reason dense clusters render as solid
309
+ * blobs until you turn it on.
310
+ */
311
+ collision?: number;
312
+ /** Collision circle radius. Default: derived from the point size. */
313
+ collisionRadius?: number;
314
+ /** Extra spacing added around each collision circle. Default 0. */
315
+ collisionPadding?: number;
316
+ /**
317
+ * Barnes-Hut opening angle θ for the many-body approximation: larger is
318
+ * coarser and faster, smaller is more exact and slower. Default 1.15.
319
+ */
320
+ repulsionTheta?: number;
321
+ /** Attraction toward the scene's centre of mass. Default 0 (OFF). */
322
+ center?: number;
323
+ /** How strongly nodes shy away from the cursor. Default 2. */
324
+ repulsionFromMouse?: number;
325
+ }
326
+ interface GraphHostUpdate<N = Record<string, unknown>, E = Record<string, unknown>> {
327
+ data?: GraphSnapshot<N, E>;
328
+ nodeColor?: Accessor<GraphNode<N>, string> | Scale<string, N>;
329
+ nodeSize?: Accessor<GraphNode<N>, number> | Scale<number, N>;
330
+ linkColor?: Accessor<AcceptedEdge<E>, string>;
331
+ linkWidth?: Accessor<AcceptedEdge<E>, number>;
332
+ /** §12 async metric columns, joined once with revision-gated admission. */
333
+ metrics?: readonly MetricColumn[];
334
+ /**
335
+ * §8 image sprites: synchronous, string-valued ref accessor (URL/blob
336
+ * ref/cache key — opaque to orbit). Refs feed the image-atlas pipeline when
337
+ * the engine declares `pointImages`; otherwise refs are retained and the
338
+ * placeholder shape renders (§13 capability policy).
339
+ */
340
+ /** §8 image refs; `null` CLEARS the accessor and evicts the atlas back to
341
+ * placeholders (D2 explicit reset — omission stays "no change"). */
342
+ nodeImage?: ((node: GraphNode<N>) => string | null) | null;
343
+ /** §16.12 instanced arrowheads (capability-gated; inert when unsupported). */
344
+ edgeArrows?: boolean;
345
+ /** §16.14 durable source coordinate for view states — stored VERBATIM,
346
+ * never interpreted; serialized by getViewState and canonically compared
347
+ * on setViewState. Stash-only lane: no publish, no commit. Omission means
348
+ * no change (there is no clear form in v1 — set `{}` for emptiness). */
349
+ dataRef?: JsonValue;
350
+ /** §16.13 runtime toggles — atomic config-only commits, no reprojection. */
351
+ showLinks?: boolean;
352
+ layout?: LayoutKind;
353
+ simulation?: SimulationConfig;
354
+ /** Controlled selection (uncontrolled when never provided; §6.4 subset). */
355
+ selection?: readonly NodeId[];
356
+ theme?: ThemeInput;
357
+ /** DOM label lane configuration (§14; strategy 'dom' only in v0.4). */
358
+ labels?: LabelConfig<N>;
359
+ /** §15.1 accessibility runtime options. */
360
+ accessibility?: AccessibilityConfig<N>;
361
+ /** §9.2 hard scope: feed ONLY the resolved subset through the reconciler;
362
+ * null restores full scope. Positions come from the cache; reflow default
363
+ * true restarts the layout around the remainder. */
364
+ subgraph?: SubgraphSpec | null;
365
+ /** §9.1 soft filter: mask (hide/dim) with ZERO relayout; null clears. */
366
+ filter?: FilterSpec<N, E> | null;
367
+ /** §16.6 crossfilter dimensions (declarative; brushes live on the session). */
368
+ crossfilter?: readonly DimensionSpec<N>[];
369
+ /** §16.3 manual groups; null clears (D2). Config-error with groupBy. */
370
+ groups?: readonly GroupSpec[] | null;
371
+ /** §16.3 derived grouping; null clears (D2). Config-error with groups. */
372
+ groupBy?: GroupBySpec<N> | null;
373
+ /** §16.3 stage-4 non-collapsing layout clusters; null clears (D2). Clusters
374
+ * COEXIST with groups — they preserve every node and edge. */
375
+ clusters?: ClusterSpec<N> | null;
376
+ /** §16.3 persistent pins (independent of transient drag pinning); null
377
+ * clears (D2). Departed ids prune through ownership. */
378
+ pinnedNodeIds?: readonly NodeId[] | null;
379
+ /** §16.3 parallel-edge grouping toggle: same-pair edges collapse into one
380
+ * count-weighted meta-edge. */
381
+ parallelEdgeGrouping?: boolean;
382
+ }
383
+ /** Built-in synchronous metrics plus caller-supplied async column names. */
384
+ type MetricName = 'degree' | 'inDegree' | 'outDegree' | (string & {});
385
+ interface DomainPolicy {
386
+ /** Domain population. Default 'dataset' (frozen per dataset revision —
387
+ * masking/isolation never change what a color means). */
388
+ scope?: 'dataset' | 'hard-scope' | 'visible';
389
+ /** Streaming behavior. Default 'freeze-per-revision'; 'expand' permits
390
+ * monotonic growth as batches arrive. */
391
+ streaming?: 'freeze-per-revision' | 'expand';
392
+ }
393
+ type Scale<T, N = Record<string, unknown>> = {
394
+ kind: 'sequential';
395
+ metric: MetricName;
396
+ range: readonly [T, T];
397
+ domain?: readonly [number, number] | DomainPolicy;
398
+ } | {
399
+ kind: 'categorical';
400
+ by: string | ((node: GraphNode<N>) => string | null);
401
+ palette?: readonly T[];
402
+ /** Fixed category order → stable colors and stable legend rows,
403
+ * including empty categories; out-of-domain values hash stably. */
404
+ domain?: readonly string[];
405
+ domainPolicy?: DomainPolicy;
406
+ } | {
407
+ kind: 'diverging';
408
+ metric: MetricName;
409
+ mid: number;
410
+ range: readonly [T, T, T];
411
+ };
412
+ /** Async metric column joined against the accepted model (§12). */
413
+ interface MetricColumn {
414
+ metric: string;
415
+ /** 'ids' joins by the ids array; 'index' is accepted-base positional. */
416
+ align: 'ids' | 'index';
417
+ values: readonly (number | null)[];
418
+ ids?: readonly NodeId[];
419
+ /**
420
+ * §12/I1 issue-time stamp: the `getRevisions().model` value CURRENT WHEN
421
+ * THE UPDATE CARRYING THIS COLUMN WAS BUILT. Capture it before starting an
422
+ * async computation and deliver it with the result — admission rejects the
423
+ * column (info diagnostic) when the model has moved since, so stale async
424
+ * work can never join a newer roster. Columns delivered atomically with
425
+ * their matching `data` in one update stamp the revision current at build
426
+ * time (the pre-update revision): the transaction is atomic, so that stamp
427
+ * uniquely names the roster the columns were derived from.
428
+ */
429
+ forModelRevision: number;
430
+ }
431
+ interface GraphTheme {
432
+ background: string;
433
+ nodeDefault: string;
434
+ edgeDefault: string;
435
+ labelFg: string;
436
+ accent: string;
437
+ mutedAlpha: number;
438
+ }
439
+ type ThemeInput = (Partial<GraphTheme> & {
440
+ base?: 'light' | 'dark';
441
+ }) | GraphTheme;
442
+ type FilterMode = 'hide' | 'dim';
443
+ type FilterValue = string | number | boolean | null;
444
+ type FilterExpr = {
445
+ op: 'eq' | 'neq';
446
+ field: string;
447
+ value: FilterValue;
448
+ } | {
449
+ op: 'in';
450
+ field: string;
451
+ values: readonly FilterValue[];
452
+ } | {
453
+ op: 'range';
454
+ field: string;
455
+ min?: number;
456
+ max?: number;
457
+ /** Default true. */
458
+ includeMin?: boolean;
459
+ /** Default true. */
460
+ includeMax?: boolean;
461
+ } | {
462
+ op: 'is-null';
463
+ field: string;
464
+ } | {
465
+ op: 'not';
466
+ expr: FilterExpr;
467
+ } | {
468
+ op: 'and' | 'or';
469
+ exprs: readonly FilterExpr[];
470
+ };
471
+ interface FilterSpec<N = Record<string, unknown>, E = Record<string, unknown>> {
472
+ nodes?: FilterExpr | ((node: GraphNode<N>) => boolean);
473
+ edges?: FilterExpr | ((edge: AcceptedEdge<E>) => boolean);
474
+ /** 'hide' removes from view (alpha 0 + picking); 'dim' mutes. Default 'hide'. */
475
+ mode?: FilterMode;
476
+ }
477
+ type DimensionKind = 'numeric' | 'temporal' | 'categorical';
478
+ interface DimensionSpec<N = Record<string, unknown>> {
479
+ /** Stable dimension key (brushes rebase by this key across data updates). */
480
+ key: string;
481
+ kind: DimensionKind;
482
+ /** Raw value accessor; §8 hygiene applies (non-finite → excluded from bins).
483
+ * Temporal accepts epoch-ms numbers, ISO strings, or 'YYYY-MM-DD'. */
484
+ get: (node: GraphNode<N>) => unknown;
485
+ /** Histogram bin count for numeric/temporal (default 24). */
486
+ bins?: number;
487
+ }
488
+ /** Numeric/temporal brush (coordinates in the dimension's units — epoch ms
489
+ * for temporal), or categorical EXCLUSIONS, or null = no brush. */
490
+ type BrushState = {
491
+ min: number;
492
+ max: number;
493
+ } | {
494
+ excluded: readonly string[];
495
+ } | null;
496
+ interface HistogramBin {
497
+ x0: number;
498
+ x1: number;
499
+ /** Rows in this bin regardless of any mask. */
500
+ total: number;
501
+ /** Rows in this bin passing every OTHER dimension's brush + the filter
502
+ * prop's node mask (the §16.6 joint "filtered" second layer). */
503
+ filtered: number;
504
+ }
505
+ interface CategoryBin {
506
+ key: string;
507
+ total: number;
508
+ filtered: number;
509
+ excluded: boolean;
510
+ }
511
+ interface DimensionSummary {
512
+ key: string;
513
+ kind: DimensionKind;
514
+ /** Numeric/temporal domain (finite rows only); undefined when empty. */
515
+ domain?: {
516
+ min: number;
517
+ max: number;
518
+ };
519
+ bins: readonly HistogramBin[];
520
+ categories: readonly CategoryBin[];
521
+ /** Rows excluded by §8 hygiene (non-finite / unparseable). */
522
+ excludedRows: number;
523
+ }
524
+ interface CrossfilterSession {
525
+ /** Monotonic from 0; advances exactly once per observable selection change. */
526
+ readonly selectionRevision: number;
527
+ /** Latest-call-wins coalescing per dimension; resolves once observable. */
528
+ setBrush(key: string, brush: BrushState): Promise<void>;
529
+ getBrush(key: string): BrushState;
530
+ summarize(key: string): DimensionSummary;
531
+ /** Fires once per observable selection/summary change. */
532
+ subscribe(cb: () => void): () => void;
533
+ }
534
+ interface TimelinePlayback {
535
+ /** 'sliding' plays a fixed window; 'cumulative' grows from the domain start. */
536
+ mode: 'sliding' | 'cumulative';
537
+ /** Window width in dimension units (sliding; default domain/10). */
538
+ window?: number;
539
+ /** Tick interval in ms (default 100). */
540
+ tickMs?: number;
541
+ /** Fraction of the domain traversed per tick (default 0.01). */
542
+ step?: number;
543
+ loop?: boolean;
544
+ }
545
+ interface SubgraphSpec {
546
+ seedIds: readonly NodeId[];
547
+ /** Expand N hops from the seeds via the expansion service (default 0). */
548
+ hops?: number;
549
+ /** Restart the layout around the subset (default true). */
550
+ reflow?: boolean;
551
+ }
552
+ interface SearchResult<N = Record<string, unknown>> {
553
+ id: string;
554
+ score?: number;
555
+ label?: string;
556
+ node?: GraphNode<N>;
557
+ }
558
+ /** Why an activated result could not be focused (§16.5 result contract). */
559
+ type SearchUnavailableReason = 'not-loaded' | 'out-of-scope' | 'filtered';
560
+ type SearchActivation = {
561
+ status: 'focused';
562
+ id: NodeId;
563
+ } | {
564
+ status: 'unavailable';
565
+ reason: SearchUnavailableReason;
566
+ result: SearchResult;
567
+ };
568
+ /** Context every async service call receives (§9.2 sequencing rule). */
569
+ interface RequestContext {
570
+ datasetKey: string;
571
+ sourceRevision: number | string | null;
572
+ modelRevision: number;
573
+ scopeRevision: number;
574
+ requestId: string;
575
+ /** Abort is an optimization; admission is the correctness gate. */
576
+ signal: AbortSignal;
577
+ }
578
+ type RevisionDimension = 'source' | 'model' | 'scope';
579
+ /** A service declares exactly the revision dimensions it consumes (§9.2). */
580
+ interface RevisionAwareService {
581
+ readonly revisionDependencies: readonly RevisionDimension[];
582
+ }
583
+ interface ExpansionBatch<N = Record<string, unknown>, E = Record<string, unknown>> {
584
+ nodes?: readonly GraphNode<N>[];
585
+ edges?: readonly GraphEdge<E>[];
586
+ }
587
+ type ExpansionResponse<N = Record<string, unknown>, E = Record<string, unknown>> = (ExpansionBatch<N, E> & {
588
+ provenance?: unknown;
589
+ }) | {
590
+ batches: AsyncIterable<ExpansionBatch<N, E>>;
591
+ provenance?: unknown;
592
+ };
593
+ /**
594
+ * §16.2 path resolver seam (S12-T08). `find` resolves the node/edge id path
595
+ * between two loaded nodes or null when unreachable (null is a RESULT, not
596
+ * an error). Extends the §9.2 revision-aware contract: abort is advisory,
597
+ * revision admission at delivery is authoritative.
598
+ */
599
+ interface PathService extends RevisionAwareService {
600
+ find(sourceId: NodeId, targetId: NodeId, options: PathOptions, ctx: RequestContext): Promise<PathResult | null>;
601
+ }
602
+ interface ExpansionService<N = Record<string, unknown>, E = Record<string, unknown>> extends RevisionAwareService {
603
+ neighbors(seedIds: readonly NodeId[], hops: number, ctx: RequestContext): Promise<ExpansionResponse<N, E>>;
604
+ }
605
+ interface BeginIngestOptions {
606
+ /** 'replace' commits a new source coordinate atomically; 'overlay' advances
607
+ * only modelRevision and may be progressive (§7.5). */
608
+ purpose: 'replace' | 'overlay';
609
+ datasetKey: string;
610
+ /** Required for 'replace': the source coordinate the commit establishes. */
611
+ sourceRevision?: number | string;
612
+ /** CAS precondition: the model revision current when the session begins
613
+ * (zero on an empty instance). Mismatch rejects with 'stale-revision'. */
614
+ baseModelRevision: number;
615
+ /** Overlays only (replace is always atomic). Default true. */
616
+ atomic?: boolean;
617
+ /** Caller-supplied stable overlay id; generated when omitted. */
618
+ overlayId?: string;
619
+ /** Progressive overlays: flush no later than this while running (default 50). */
620
+ maxFlushLatencyMs?: number;
621
+ /** Byte backpressure budget. Progressive receipts await drainage past this;
622
+ * atomic sessions terminally reject an append that would exceed it because
623
+ * atomic staging cannot drain before commit. */
624
+ maxPendingBytes?: number;
625
+ }
626
+ interface IngestBatch<N = Record<string, unknown>, E = Record<string, unknown>> {
627
+ /** Consecutive, strictly monotonic from zero (§7.5). */
628
+ sequence: number;
629
+ /** Idempotency key: an admitted {sequence, batchId} replay returns its
630
+ * original receipt; same sequence + different batchId rejects. */
631
+ batchId: string;
632
+ nodes?: readonly GraphNode<N>[];
633
+ edges?: readonly GraphEdge<E>[];
634
+ /** Caller-declared payload size; estimated when omitted. */
635
+ bytes?: number;
636
+ }
637
+ interface AppendReceipt {
638
+ sequence: number;
639
+ batchId: string;
640
+ admittedNodes: number;
641
+ admittedEdges: number;
642
+ /** Present once the flush containing this batch became public (progressive
643
+ * overlays resolve only then, so exact replays return complete receipts). */
644
+ publishedModelRevision?: number;
645
+ /** Bytes admitted but not yet flushed (the backpressure signal). */
646
+ pendingBytes: number;
647
+ }
648
+ interface IngestCommitReceipt {
649
+ overlayId?: string;
650
+ modelRevision: number;
651
+ sourceRevision?: number | string;
652
+ admittedNodes: number;
653
+ admittedEdges: number;
654
+ /** Dangling edges dropped at commit (diagnostics emitted only then; §7.5). */
655
+ danglingEdges: number;
656
+ }
657
+ type IngestSessionState = 'open' | 'committing' | 'committed' | 'aborted';
658
+ interface IngestSession<N = Record<string, unknown>, E = Record<string, unknown>> {
659
+ readonly state: IngestSessionState;
660
+ readonly overlayId: string | undefined;
661
+ append(batch: IngestBatch<N, E>): Promise<AppendReceipt>;
662
+ commit(): Promise<IngestCommitReceipt>;
663
+ abort(reason?: unknown): Promise<void>;
664
+ }
665
+ /** §14 label lane configuration (zoom-LOD, ranking, forced ids). */
666
+ interface LabelConfig<N = Record<string, unknown>> {
667
+ enabled?: boolean;
668
+ /** Labels appear only at/above this zoom (LOD threshold). Default 1. */
669
+ minZoom?: number;
670
+ /**
671
+ * §16.3 cluster-label LOD ceiling. At or BELOW this zoom the active
672
+ * `clusters` spec's labels render and NODE labels are suppressed; above it
673
+ * cluster labels stop and node-label LOD (`minZoom`) takes over. Absent ⇒
674
+ * no LOD hand-off: cluster labels (when a spec is active) and node labels
675
+ * coexist, each on its own gate.
676
+ */
677
+ maxZoom?: number;
678
+ /** Ranked-candidate cap k (viewport-culled). Default 64, policy max 1024. */
679
+ maxVisible?: number;
680
+ /** Ids that claim capacity FIRST, bypassing ranking (§14 showLabelsFor). */
681
+ showFor?: readonly NodeId[];
682
+ /** Label text; default attrs.label ?? id. Rendered as a TEXT NODE (§14). */
683
+ getText?: (node: GraphNode<N>) => string;
684
+ /** Ranking weight; default nodeSize result order, else degree. */
685
+ getWeight?: (node: GraphNode<N>) => number;
686
+ }
687
+ /** §15.1 accessibility runtime options. */
688
+ interface AccessibilityConfig<N = Record<string, unknown>> {
689
+ /** Canvas aria-label. Default 'Graph visualization'. */
690
+ label?: string;
691
+ description?: string;
692
+ /** Max items per navigator relationship page. Default 50. */
693
+ navigatorWindow?: number;
694
+ /** Gate live-region announcements (default true). */
695
+ announcements?: boolean;
696
+ /** Text name for a node in the navigator/live region; default label/id. */
697
+ getAccessibleLabel?: (node: GraphNode<N>) => string;
698
+ /**
699
+ * Reduced-motion override: true forces reduced, false forces full motion,
700
+ * undefined follows the host binding's media-query detection (§15.1).
701
+ */
702
+ reducedMotion?: boolean;
703
+ }
704
+ /** One positioned label emitted to the overlay lane per scheduler tick (§14). */
705
+ interface LabelPlacement {
706
+ /** Node id — or, for `kind: 'cluster'`, the §16.3 CLUSTER KEY. */
707
+ id: NodeId;
708
+ text: string;
709
+ /** Screen coordinates (CSS px, container-relative). */
710
+ x: number;
711
+ y: number;
712
+ forced: boolean;
713
+ /**
714
+ * §16.3 placement kind. 'node' (default) anchors to the node's cached
715
+ * position; 'cluster' anchors to the cluster's force center while the
716
+ * simulation is hot and to its settled centroid afterwards, and selects its
717
+ * MEMBER node ids when activated (R-16.3-18/21). Ids are drawn from
718
+ * different namespaces, so consumers must key on `(kind, id)`.
719
+ */
720
+ kind?: 'node' | 'cluster';
721
+ }
722
+ interface ViewportState {
723
+ x: number;
724
+ y: number;
725
+ zoom: number;
726
+ }
727
+ type InstanceStatus = 'idle' | 'mounting' | 'ready'
728
+ /** WebGL context lost; engine frozen, CPU model stays live (§13.1). */
729
+ | 'lost'
730
+ /** Context restored; the full-scene replay commit is in flight (§13.1). */
731
+ | 'recovering' | 'destroyed' | 'error';
732
+ /**
733
+ * Namespaced selection (§16.2). Namespaces are independent: node-set algebra
734
+ * never mutates edge selection. `groupIds` is reserved (populated from S12).
735
+ */
736
+ interface SelectionState {
737
+ nodeIds: readonly NodeId[];
738
+ edgeIds: readonly EdgeId[];
739
+ groupIds: readonly string[];
740
+ }
741
+ /** §16.3 manual group definition. Flat and disjoint: membership may not
742
+ * nest, overlap, duplicate, self-reference, or name unknown ids — violations
743
+ * are §5.1 config-error diagnostics BEFORE any scene rewrite. */
744
+ interface GroupSpec {
745
+ /** Public group id — its own namespace, never colliding with node ids. */
746
+ id: string;
747
+ memberIds: readonly NodeId[];
748
+ label?: string;
749
+ /** Collapsed groups rewrite to super-nodes with meta-edges (stage 3). */
750
+ collapsed?: boolean;
751
+ color?: string;
752
+ }
753
+ /** §16.3 derived grouping: one group per distinct accessor key (null =
754
+ * ungrouped). Membership is derived and READ-ONLY; collapsed defaults false
755
+ * so adding groupBy alone changes no rendering. */
756
+ interface GroupBySpec<N = Record<string, unknown>> {
757
+ by: (node: GraphNode<N>) => string | null;
758
+ /** Hysteresis semantic zoom: crossing below collapseBelow collapses all
759
+ * derived groups; crossing above expandAbove expands only groups
760
+ * intersecting the viewport; between the thresholds the band holds.
761
+ * expandAbove must be strictly greater than collapseBelow. */
762
+ semanticZoom?: {
763
+ collapseBelow: number;
764
+ expandAbove: number;
765
+ };
766
+ }
767
+ /**
768
+ * §16.3 stage-4 non-collapsing layout clusters: a categorical `by` accessor
769
+ * partitions the PHYSICAL scene (`null` ⇒ unclustered) into force-clustered,
770
+ * centroid-labelled sets. Clusters preserve every node and edge and therefore
771
+ * NEVER synthesize super-nodes or meta-edges (R-16.3-17/19); they coexist with
772
+ * groups and re-derive over the post-group-rewrite physical scene.
773
+ */
774
+ interface ClusterSpec<N = Record<string, unknown>> {
775
+ /** Membership accessor, compared by function REFERENCE (a new inline lambda
776
+ * re-derives — the groupBy convention). */
777
+ by: (node: GraphNode<N>) => string | null;
778
+ /** Cluster-force strength handed to the engine. Inert (with ONE loud
779
+ * degradation diagnostic) on engines that do not declare `clusterForce`;
780
+ * membership, labels, and centroids still work (R-13-39). */
781
+ strength?: number;
782
+ /** Explicit force centers per key, in SPACE coordinates. Keys omitted here
783
+ * generate deterministically from the ordered keys + layout seed
784
+ * (R-16.3-20 — see `resolveClusterCenters`). */
785
+ centers?: ReadonlyMap<string, readonly [number, number]>;
786
+ }
787
+ /** Resolved cluster surface for overlays/selection (public ids only). */
788
+ interface ResolvedCluster {
789
+ /** The categorical key — also the cluster label's text and overlay id. */
790
+ key: string;
791
+ /** Member PHYSICAL node ids in scene order. */
792
+ memberIds: readonly NodeId[];
793
+ /** The force center labels anchor to while the simulation is HOT. */
794
+ forceCenter: readonly [number, number];
795
+ /** Settled centroid from the last permitted §7.1 readback (or the commit
796
+ * under a fixed layout); null until one has landed. */
797
+ centroid: readonly [number, number] | null;
798
+ }
799
+ /** Resolved group surface for events/selection/store (public namespace). */
800
+ interface ResolvedGroup {
801
+ id: string;
802
+ label?: string;
803
+ memberIds: readonly NodeId[];
804
+ collapsed: boolean;
805
+ /** True for groupBy-derived groups (membership read-only). */
806
+ derived: boolean;
807
+ color?: string;
808
+ }
809
+ /** §16.3 rerouted member edge on a collapsed group (stage 3), or a grouped
810
+ * parallel-edge bundle (§16.3 R-24). Count is the badge datum. */
811
+ interface MetaEdge {
812
+ id: string;
813
+ /** Node id OR group id endpoint (public namespaces). */
814
+ source: string;
815
+ target: string;
816
+ /** Underlying (rerouted / collapsed-parallel) edge count. */
817
+ count: number;
818
+ }
819
+ /** §16.2 path query options (PathService). */
820
+ interface PathOptions {
821
+ /** Edge-direction rule for traversal. Default 'outgoing'. */
822
+ direction?: 'outgoing' | 'incoming' | 'either';
823
+ }
824
+ /** A resolved path: node ids in order plus the edge ids walked. */
825
+ interface PathResult {
826
+ nodeIds: readonly NodeId[];
827
+ edgeIds: readonly EdgeId[];
828
+ }
829
+ interface GraphStoreState {
830
+ status: InstanceStatus;
831
+ revisions: Revisions;
832
+ nodeCount: number;
833
+ edgeCount: number;
834
+ selection: SelectionState;
835
+ hover: {
836
+ nodeId: NodeId | null;
837
+ edgeId: EdgeId | null;
838
+ };
839
+ /** id → pinned space position (§16.3 pin slice; drag-pinning writes here). */
840
+ pins: ReadonlyMap<NodeId, readonly [number, number]>;
841
+ /** §16.3 PERSISTENT pins (S12-T09): ids held at their CURRENT position via
842
+ * engine.setPinnedIndices. Independent lifecycle from the transient
843
+ * drag-pin `pins` slice — the engine receives the UNION; releasing a drag
844
+ * pin on a persistently-pinned node leaves it pinned. No position payload
845
+ * in v0.10: a persistent pin freezes the node wherever it currently is. */
846
+ pinnedNodeIds: ReadonlySet<NodeId>;
847
+ hiddenNodeIds: ReadonlySet<NodeId>;
848
+ /** Active hard scope (§9.2); null = full scope. */
849
+ scope: SubgraphSpec | null;
850
+ /** Soft-mask visibility counts (§9.1): RENDERED SCENE entities with zero
851
+ * hide-failures — the §16.3 synthetic suffix INCLUDED, so a collapsed
852
+ * group contributes its one drawn super-node. Equals nodeCount/edgeCount
853
+ * when nothing masks, scopes, or groups.
854
+ *
855
+ * NOT the same question as `getVisibleNodeIds()`, which lists PUBLIC
856
+ * physical ids only (§7.4). Pair a count with that list via
857
+ * `getVisibleNodeIds().length`; use `visible` for "how much is on screen". */
858
+ visible: {
859
+ nodes: number;
860
+ edges: number;
861
+ };
862
+ /** Timeline playback state (§16.6): at most one playing dimension. */
863
+ timeline: {
864
+ playingKey: string | null;
865
+ };
866
+ /** §16.14 history kernel depths (S9-T20; full walk semantics in S15). */
867
+ history: {
868
+ undoDepth: number;
869
+ redoDepth: number;
870
+ };
871
+ /** Node ids with an expansion in flight (§9.2 loading affordance). */
872
+ pendingExpansions: ReadonlySet<NodeId>;
873
+ /**
874
+ * §16.3 node folds: anchor id → how many members it stands for. Empty when
875
+ * nothing is folded.
876
+ *
877
+ * Published so folds are OBSERVABLE. A fold changes neither an anchor's id
878
+ * nor its label text, so the §14 label lane — which re-renders content only
879
+ * when the candidate SET changes — would otherwise never re-render a badge
880
+ * that depends on fold state. Subscribing to this slice is how a host keeps
881
+ * fold-derived chrome (badges, affordances) in step.
882
+ */
883
+ folds: ReadonlyMap<NodeId, number>;
884
+ /** Committed overlay ids for the current dataset (§7.5). */
885
+ overlayIds: readonly string[];
886
+ /** §16.3 resolved groups (manual or groupBy-derived); [] when ungrouped.
887
+ * Path highlight is deliberately NOT here: session-local, never
888
+ * serialized (§16.2). */
889
+ groups: readonly ResolvedGroup[];
890
+ /** Last completed search (§16.5): feeds <GraphSearch> and the §15.1
891
+ * navigator's search-results section. Cleared on datasetKey change. */
892
+ search: {
893
+ query: string;
894
+ results: readonly SearchResult[];
895
+ } | null;
896
+ viewport: ViewportState | null;
897
+ /** §14/§16.1: live force-simulation activity — true after a commit with
898
+ * restart or resumeSimulation(); false on settle or pauseSimulation(). */
899
+ simulationRunning: boolean;
900
+ /** §8 resolved theme tokens (S10): the merged GraphTheme currently driving
901
+ * engine config, projection fallbacks, and mask dim alpha. Published on
902
+ * change; defaults to the dark base. */
903
+ theme: GraphTheme;
904
+ diagnostics: readonly GraphDiagnostic[];
905
+ }
906
+ interface GraphListenerControl {
907
+ preventDefault(): void;
908
+ }
909
+ interface NodeEventPayload<N = Record<string, unknown>> {
910
+ node: GraphNode<N>;
911
+ }
912
+ interface GraphEventMap<N = Record<string, unknown>, E = Record<string, unknown>> {
913
+ nodeClick: NodeEventPayload<N> & {
914
+ metaKey?: boolean;
915
+ };
916
+ backgroundClick: Record<string, never>;
917
+ nodeHover: {
918
+ node: GraphNode<N> | null;
919
+ };
920
+ edgeClick: {
921
+ edge: AcceptedEdge<E>;
922
+ };
923
+ edgeHover: {
924
+ edge: AcceptedEdge<E> | null;
925
+ };
926
+ nodeDragStart: NodeEventPayload<N>;
927
+ /** Fired on drag release with the final space position; the built-in
928
+ * follow-up pins the node there (preventDefault cancels the pin). */
929
+ nodeDragEnd: NodeEventPayload<N> & {
930
+ x: number;
931
+ y: number;
932
+ };
933
+ /** §16.14: a setViewState dataRef mismatch — fired INSTEAD of applying.
934
+ * Restoration proceeds only when the caller re-invokes with the opt-in. */
935
+ viewStateMismatch: {
936
+ stored: JsonValue | undefined;
937
+ current: JsonValue | undefined;
938
+ };
939
+ /** §16.14 aggregate restore intent: fired ONCE per restore/history
940
+ * transaction touching any §6.4 controlled slice or serialized styling —
941
+ * never fanned out per lane. The host reflects every participating prop in
942
+ * one commit; the transaction commits when the reflected values match, and
943
+ * times out / diverges / supersedes as typed results otherwise. `next` is
944
+ * the full target view state. */
945
+ viewStateRestore: {
946
+ transactionId: string;
947
+ source: 'setViewState' | 'undo' | 'redo';
948
+ next: unknown;
949
+ };
950
+ /** Right-click / long-press; built-in follow-up opens <GraphContextMenu>. */
951
+ contextMenu: {
952
+ target: {
953
+ kind: 'node';
954
+ node: GraphNode<N>;
955
+ } | {
956
+ kind: 'background';
957
+ };
958
+ /** Container-relative CSS px. */
959
+ screen: readonly [number, number];
960
+ };
961
+ viewportChange: ViewportState;
962
+ selectionChange: SelectionState;
963
+ /** §16.3/§7.4 (R-16.3-12): a super-node hit carries the resolved GROUP —
964
+ * never a GraphNode, never an internal scene key. Built-in follow-up
965
+ * selects the group id into SelectionState.groupIds (preventDefault
966
+ * cancels it, mirroring nodeClick). */
967
+ groupClick: {
968
+ group: ResolvedGroup;
969
+ metaKey?: boolean;
970
+ };
971
+ /** §16.3/§7.4: a meta-edge hit carries the MetaEdge record (public
972
+ * endpoint ids + the underlying count badge datum). No built-in follow-up. */
973
+ metaEdgeClick: {
974
+ metaEdge: MetaEdge;
975
+ };
976
+ /** §6.4 groups slice change: op results (uncontrolled), op intents
977
+ * (controlled — the host reflects the array back through the `groups`
978
+ * prop), and groupBy re-derivations (notification; groupBy is always
979
+ * instance-derived, R-16.3-16). Host `groups` prop writes and manual
980
+ * model-drift re-resolutions are store-only and do NOT fire this. */
981
+ groupsChange: {
982
+ groups: readonly ResolvedGroup[];
983
+ };
984
+ /** §6.4 persistent-pin slice change (S12-T09), the groups-latch mirror:
985
+ * op results (uncontrolled) and op INTENTS (controlled — the host
986
+ * reflects the array back through the `pinnedNodeIds` prop). Host prop
987
+ * writes and model-drift prunes are store-only and do NOT fire this. */
988
+ pinnedChange: {
989
+ pinnedNodeIds: readonly NodeId[];
990
+ };
991
+ /** §16.3 effective-set reporting seam (S12-T03): retractExpansion fires this
992
+ * with the NEXT effective set as a SubgraphSpec whenever a collapse
993
+ * changed what is displayed. v0.10 keeps `subgraph` UNCONTROLLED-ONLY, so
994
+ * this is a notification today; a future controlled subgraph mode turns
995
+ * it into the §6.4 intent without changing the payload shape. */
996
+ subgraphChange: {
997
+ subgraph: SubgraphSpec;
998
+ };
999
+ ready: Record<string, never>;
1000
+ error: {
1001
+ error: Error;
1002
+ detail?: GraphError;
1003
+ };
1004
+ simulationEnd: Record<string, never>;
1005
+ }
1006
+ type GraphEventName = keyof GraphEventMap;
1007
+
1008
+ /**
1009
+ * GraphEngine adapter contract (spec §13, v0.1 subset).
1010
+ *
1011
+ * The core drives any engine exclusively through this interface; cosmos.gl is
1012
+ * the first implementation and lives in @modernrelay/orbit-engine-cosmos — the
1013
+ * only package allowed to import it. FakeEngine (core /testing) implements the
1014
+ * same contract headlessly.
1015
+ *
1016
+ * v0.1 subset decisions:
1017
+ * - Buffer updates are full-channel replaces (`rangeUpdates` reserved).
1018
+ * - One visibly atomic update per EngineCommit (§13): the adapter applies all
1019
+ * channels/config of a commit before the next drawn frame.
1020
+ * - Position readback (`getPositions`) is per-event only, never per-tick (§7.1/§17).
1021
+ */
1022
+
1023
+ type EngineBufferChannel = 'pointPosition' | 'link' | 'pointColor' | 'pointSize' | 'linkColor' | 'linkWidth';
1024
+ interface EngineCapabilities {
1025
+ /** Native edge hover/click picking. */
1026
+ linkPicking: boolean;
1027
+ /** Channels supporting ranged (partial) updates. Empty in v0.1 for cosmos. */
1028
+ rangeUpdates: readonly EngineBufferChannel[];
1029
+ /** O(k) tracked-subset position readback per frame (§13). */
1030
+ trackedPositions: boolean;
1031
+ /** Engine runs a live GPU force simulation. */
1032
+ simulation: boolean;
1033
+ /** Instanced directional arrowheads on links (§16.12). */
1034
+ edgeArrows?: boolean;
1035
+ /** Per-point image sprites via a texture atlas (§8). */
1036
+ pointImages?: boolean;
1037
+ /** §16.3 cluster force: per-point cluster assignment with optional
1038
+ * strength/centers (S12). Engines without it degrade loudly — membership,
1039
+ * labels, and centroids still work (R-13-39). */
1040
+ clusterForce?: boolean;
1041
+ }
1042
+ interface EngineConfigUpdate {
1043
+ /** §16.3 stage-4 cluster force (capability `clusterForce`; inert
1044
+ * otherwise). null clears. `pointClusters` maps point index → cluster
1045
+ * ordinal (aligned to `centers` pairs). */
1046
+ cluster?: {
1047
+ pointClusters: Float32Array;
1048
+ centers?: Float32Array;
1049
+ strength?: number;
1050
+ } | null;
1051
+ backgroundColor?: string;
1052
+ simulation?: SimulationConfig;
1053
+ /** Space-coordinate defaults used when seeding unknown (NaN) positions. */
1054
+ seedRadius?: number;
1055
+ /** §16.12 arrowheads on/off (capability edgeArrows; inert otherwise). */
1056
+ linkArrows?: boolean;
1057
+ /** §16.13 link visibility toggle — config-only, never a buffer rebuild. */
1058
+ renderLinks?: boolean;
1059
+ /** Engine-relevant §8 theme tokens beyond background. */
1060
+ defaultPointColor?: string;
1061
+ defaultLinkColor?: string;
1062
+ }
1063
+ /**
1064
+ * One atomic engine update. `revision` is the desired-render revision the
1065
+ * commit realizes; adapters must report it back via `appliedRevision` once
1066
+ * visible. Buffers are full-channel replaces sized to the current structure;
1067
+ * when `structure` is present it replaces point/link structure and MUST be
1068
+ * applied together with any buffers in the same commit.
1069
+ */
1070
+ interface EngineCommit {
1071
+ revision: number;
1072
+ structure?: {
1073
+ pointCount: number;
1074
+ /** 2*pointCount floats; NaN pairs = engine seeds the position. */
1075
+ positions: Float32Array;
1076
+ /** 2*linkCount uint32 endpoint indices. */
1077
+ links: Uint32Array;
1078
+ };
1079
+ buffers?: Partial<{
1080
+ /** 4*pointCount RGBA floats in [0,1]. */
1081
+ pointColor: Float32Array;
1082
+ /** pointCount floats (px). */
1083
+ pointSize: Float32Array;
1084
+ /** 4*linkCount RGBA floats in [0,1]. */
1085
+ linkColor: Float32Array;
1086
+ /** linkCount floats (px). */
1087
+ linkWidth: Float32Array;
1088
+ }>;
1089
+ config?: EngineConfigUpdate;
1090
+ /** §8 image-atlas resource updates (capability pointImages); applied
1091
+ * atomically with the same commit's buffers. */
1092
+ resources?: {
1093
+ imageAtlas?: {
1094
+ upserts?: readonly {
1095
+ slot: number;
1096
+ bitmap: ImageBitmap;
1097
+ }[];
1098
+ removeSlots?: readonly number[];
1099
+ };
1100
+ /** Per-point atlas slot indices (-1 = placeholder shape). */
1101
+ pointImageIndex?: Float32Array;
1102
+ };
1103
+ /** Restart/reheat the simulation after applying; false/absent = keep state. */
1104
+ restart?: {
1105
+ alpha: number;
1106
+ } | false;
1107
+ }
1108
+ /**
1109
+ * WebGL context lifecycle events (§13.1). `restored` means the adapter has a
1110
+ * fresh, empty, commit-ready GL machine in the same container — the core then
1111
+ * re-commits the scene. `failed` is terminal reinitialization failure.
1112
+ */
1113
+ type EngineContextEvent = {
1114
+ type: 'lost';
1115
+ } | {
1116
+ type: 'restored';
1117
+ } | {
1118
+ type: 'failed';
1119
+ error: Error;
1120
+ };
1121
+ /** Adapter observability channel; codes are namespaced `engine:*` (§6.5). */
1122
+ interface EngineDiagnostic {
1123
+ code: `engine:${string}`;
1124
+ severity: DiagnosticSeverity;
1125
+ message: string;
1126
+ }
1127
+ /** Events the engine reports up to the core (indices are engine-local; the
1128
+ * core maps them back to typed objects — §7.4). */
1129
+ interface EngineHostEvents {
1130
+ onPointClick?(index: number | null, modifiers?: {
1131
+ metaKey: boolean;
1132
+ shiftKey: boolean;
1133
+ }): void;
1134
+ onPointHover?(index: number | null): void;
1135
+ /** Native link picking (capability `linkPicking`); indices are link-local. */
1136
+ onLinkClick?(linkIndex: number): void;
1137
+ onLinkHover?(linkIndex: number | null): void;
1138
+ /** Native point drag (space coords). The core owns pin semantics (§16.3). */
1139
+ onDragStart?(index: number): void;
1140
+ onDragEnd?(index: number, x: number, y: number): void;
1141
+ /** Context-menu gesture (right-click / touch long-press); null = background. */
1142
+ onContextMenu?(index: number | null, screen: readonly [number, number]): void;
1143
+ /**
1144
+ * Overlay/activity clock (§13/§17): the adapter's per-frame callback for the
1145
+ * core scheduler's fan-out (DOM labels, tooltips). Under
1146
+ * `postDrawFrames:false` engines this is an activity clock — overlays may
1147
+ * lag the canvas by one sample; the adapter reports that degradation once.
1148
+ */
1149
+ onFrame?(timeMs: number): void;
1150
+ onViewportChange?(v: ViewportState): void;
1151
+ onSimulationEnd?(): void;
1152
+ onError?(error: Error): void;
1153
+ onContextEvent?(ev: EngineContextEvent): void;
1154
+ onDiagnostic?(d: EngineDiagnostic): void;
1155
+ }
1156
+ interface FitViewOptions {
1157
+ durationMs?: number;
1158
+ padding?: number;
1159
+ }
1160
+ interface GraphEngine {
1161
+ readonly capabilities: EngineCapabilities;
1162
+ /** Mount into a container. Resolve when the first frame can be produced. */
1163
+ mount(container: HTMLElement, events: EngineHostEvents): Promise<void>;
1164
+ /** Apply one atomic update (§13). Throws only on programmer error. */
1165
+ commit(update: EngineCommit): void;
1166
+ /** Highest commit revision that is visibly applied. */
1167
+ appliedRevision(): number | null;
1168
+ fitView(opts?: FitViewOptions): void;
1169
+ zoom(factor: number, durationMs?: number): void;
1170
+ setViewport(v: Partial<ViewportState>, opts?: {
1171
+ durationMs?: number;
1172
+ }): void;
1173
+ getViewport(): ViewportState | null;
1174
+ /** Optional: animate the camera to a point (used by focusNode). */
1175
+ zoomToIndex?(index: number, durationMs?: number): void;
1176
+ start(alpha?: number): void;
1177
+ pause(): void;
1178
+ setSelectedIndices(indices: readonly number[] | null): void;
1179
+ setFocusedIndex(index: number | null): void;
1180
+ /** Point indices inside a SCREEN-coordinate polygon (lasso/rectangle). */
1181
+ pointsInPolygon?(screenPolygon: readonly [number, number][]): number[];
1182
+ /** Point indices inside a SCREEN-coordinate rect [x0,y0,x1,y1] (label cull). */
1183
+ pointsInRect?(screenRect: readonly [number, number, number, number]): number[];
1184
+ /** Capture the current frame as an image (toolbar screenshot; §16.1). */
1185
+ captureScreenshot?(): Promise<Blob | null>;
1186
+ /** 1-hop neighbor point indices (engine adjacency, if available). */
1187
+ neighborIndices?(index: number): number[];
1188
+ screenToSpace?(p: readonly [number, number]): [number, number] | null;
1189
+ spaceToScreen?(p: readonly [number, number]): [number, number] | null;
1190
+ /** Replace the full pinned set; null/[] unpins all (idempotent). */
1191
+ setPinnedIndices?(indices: readonly number[] | null): void;
1192
+ getPositions(): Float32Array | null;
1193
+ destroy(): void;
1194
+ }
1195
+ /** Engine factory the host passes in; called once per instance mount. */
1196
+ type EngineFactory = () => GraphEngine;
1197
+
1198
+ export { type BrushState as $, type AcceptedGraph as A, type BeginIngestOptions as B, type SearchActivation as C, type CrossfilterSession as D, type EngineFactory as E, type Scale as F, type GraphSnapshot as G, type MetricName as H, type IngestSession as I, type JsonValue as J, type Revisions as K, type LabelPlacement as L, type MetaEdge as M, type NodeId as N, type AccessibilityConfig as O, type PathService as P, type ThemeInput as Q, type ResolvedGroup as R, type SceneFold as S, type TimelinePlayback as T, type LabelConfig as U, type ViewportState as V, type IngestBatch as W, type RevisionDimension as X, type FilterSpec as Y, type FilterExpr as Z, type DimensionSpec as _, type GraphDiagnostic as a, type DimensionSummary as a0, type DomainPolicy as a1, type EngineBufferChannel as a2, type GraphEngine as a3, type EngineCommit as a4, type EngineCapabilities as a5, type MetricColumn as a6, type AppendReceipt as a7, type CategoryBin as a8, type ClusterSpec as a9, type FitViewOptions as aA, type EngineDiagnostic as aB, type EngineConfigUpdate as aC, type EngineContextEvent as aD, DIAGNOSTIC_SAMPLE_CAP as aa, type DiagnosticCode as ab, type DiagnosticSeverity as ac, type DimensionKind as ad, type ErrorPhase as ae, type ExpansionBatch as af, type ExpansionResponse as ag, type FilterMode as ah, type FilterValue as ai, type GraphEdge as aj, type GraphError as ak, type GraphOperationError as al, type HistogramBin as am, type IngestCommitReceipt as an, type IngestSessionState as ao, type InstanceStatus as ap, type LayoutKind as aq, type NodeEventPayload as ar, OrbitOperationError as as, type ResourceAdmissionReport as at, type SearchUnavailableReason as au, type SimulationConfig as av, graphErrorToError as aw, isFatalGraphError as ax, resourceLimitFatal as ay, type EngineHostEvents as az, type GraphNode as b, type AcceptedEdge as c, type GroupSpec as d, type GroupBySpec as e, type SceneGroups as f, type RenderScene as g, type SceneLinkRef as h, type ScenePointRef as i, type Accessor as j, type RevisionAwareService as k, type RequestContext as l, type SearchResult as m, type SelectionState as n, type SubgraphSpec as o, type ExpansionService as p, type GraphTheme as q, type GraphStoreState as r, type GraphHostUpdate as s, type GraphEventName as t, type GraphEventMap as u, type GraphListenerControl as v, type EdgeId as w, type ResolvedCluster as x, type PathOptions as y, type PathResult as z };