@modernrelay/orbit-core 0.14.0 → 0.16.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.
@@ -1,5 +1,16 @@
1
1
  // src/types.ts
2
2
  var DIAGNOSTIC_SAMPLE_CAP = 10;
3
+ var SIMULATION_PRESETS = Object.freeze({
4
+ calm: Object.freeze({ repulsion: 1.4, gravity: 0.15, friction: 0.6, decay: 1e3 }),
5
+ spread: Object.freeze({ repulsion: 2, gravity: 0.1, friction: 0.6, decay: 1400 }),
6
+ tight: Object.freeze({ repulsion: 0.8, gravity: 0.3, friction: 0.55, decay: 1200 }),
7
+ lively: Object.freeze({ repulsion: 1, gravity: 0.25, friction: 0.85, decay: 5e3 })
8
+ });
9
+ function resolveSimulation(input) {
10
+ if (input === void 0) return SIMULATION_PRESETS.calm;
11
+ if (typeof input === "string") return SIMULATION_PRESETS[input] ?? SIMULATION_PRESETS.calm;
12
+ return input;
13
+ }
3
14
 
4
15
  // src/clusters.ts
5
16
  var clusterProbe = {
@@ -266,6 +277,7 @@ function acceptColumnar(snapshot) {
266
277
  const nodeRows = snapshot.nodes.length;
267
278
  const edgeRows = snapshot.edges.length;
268
279
  const duplicateNode = { count: 0, samples: [] };
280
+ const invalidEdge = { count: 0, samples: [] };
269
281
  const duplicateEdge = { count: 0, samples: [] };
270
282
  const selfLoop = { count: 0, samples: [] };
271
283
  const invalidNode = { count: 0, samples: [] };
@@ -297,18 +309,21 @@ function acceptColumnar(snapshot) {
297
309
  }
298
310
  }
299
311
  const edgeCanonical = canonicalizeDictionary(edgeIds.dictionary);
312
+ const nulEdgeDict = new Uint8Array(edgeIds.dictionary.length);
313
+ for (let d = 0; d < edgeIds.dictionary.length; d++) {
314
+ if (edgeIds.dictionary[d].includes("\0")) nulEdgeDict[d] = 1;
315
+ }
300
316
  const keepEdges = new Uint8Array(edgeRows);
301
317
  const seenEdgeByCanonical = new Uint8Array(edgeIds.dictionary.length);
302
318
  const { source, target } = snapshot.edges;
303
319
  const linksOut = new Uint32Array(edgeRows * 2);
304
320
  let acceptedEdgeCount = 0;
305
321
  for (let e = 0; e < edgeRows; e++) {
306
- const canonical = edgeCanonical[edgeIds.codes[e]];
307
- if (seenEdgeByCanonical[canonical] !== 0) {
308
- record(duplicateEdge, edgeIds.dictionary[edgeIds.codes[e]]);
322
+ if (nulEdgeDict[edgeIds.codes[e]] !== 0) {
323
+ record(invalidEdge, `[${e}]`);
309
324
  continue;
310
325
  }
311
- seenEdgeByCanonical[canonical] = 1;
326
+ const canonical = edgeCanonical[edgeIds.codes[e]];
312
327
  const s = nodeAcceptedIndex[source[e]];
313
328
  const t = nodeAcceptedIndex[target[e]];
314
329
  if (s === -1 || t === -1) {
@@ -318,6 +333,11 @@ function acceptColumnar(snapshot) {
318
333
  );
319
334
  continue;
320
335
  }
336
+ if (seenEdgeByCanonical[canonical] !== 0) {
337
+ record(duplicateEdge, edgeIds.dictionary[edgeIds.codes[e]]);
338
+ continue;
339
+ }
340
+ seenEdgeByCanonical[canonical] = 1;
321
341
  if (s === t) {
322
342
  record(selfLoop, nodeIds.dictionary[nodeIds.codes[source[e]]]);
323
343
  }
@@ -341,6 +361,13 @@ function acceptColumnar(snapshot) {
341
361
  duplicateNode,
342
362
  `${duplicateNode.count} duplicate node id(s) dropped (first occurrence wins)`
343
363
  );
364
+ pushDiagnostic(
365
+ diagnostics,
366
+ "invalid-edge",
367
+ "error",
368
+ invalidEdge,
369
+ `${invalidEdge.count} edge row(s) dropped: missing or non-string source/target, or NUL-containing explicit id`
370
+ );
344
371
  pushDiagnostic(
345
372
  diagnostics,
346
373
  "dangling-edge-endpoint",
@@ -373,6 +400,6 @@ function acceptColumnar(snapshot) {
373
400
  };
374
401
  }
375
402
 
376
- export { DEFAULT_CLUSTER_CENTER_RADIUS, DEFAULT_LAYOUT_SEED, DIAGNOSTIC_SAMPLE_CAP, EnvelopeSequencer, RequestLedger, acceptColumnar, clusterCentroids, clusterProbe, collectTransfers, decodeStringTable, deriveClusters, encodeStringTable, generateClusterCenters, isWellFormedEnvelope, judgeEpoch, resetClusterProbe, resolveClusterCenters };
377
- //# sourceMappingURL=chunk-KRCG2JFN.js.map
378
- //# sourceMappingURL=chunk-KRCG2JFN.js.map
403
+ export { DEFAULT_CLUSTER_CENTER_RADIUS, DEFAULT_LAYOUT_SEED, DIAGNOSTIC_SAMPLE_CAP, EnvelopeSequencer, RequestLedger, SIMULATION_PRESETS, acceptColumnar, clusterCentroids, clusterProbe, collectTransfers, decodeStringTable, deriveClusters, encodeStringTable, generateClusterCenters, isWellFormedEnvelope, judgeEpoch, resetClusterProbe, resolveClusterCenters, resolveSimulation };
404
+ //# sourceMappingURL=chunk-DCY66QZS.js.map
405
+ //# sourceMappingURL=chunk-DCY66QZS.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/types.ts","../src/clusters.ts","../src/workerProtocol.ts","../src/columnarValidate.ts"],"names":[],"mappings":"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","file":"chunk-DCY66QZS.js","sourcesContent":["/**\n * orbit-core public data model.\n *\n * The public model is object-based, id-keyed, and generic over caller attribute\n * types. A `GraphSnapshot` is the declarative source of truth; the core keeps a\n * derived index model and drives the engine imperatively.\n */\n\nimport type { GraphError } from './errors';\n\nexport type NodeId = string;\nexport type EdgeId = string;\n\n/** Plain JSON value — the shape `dataRef` and other verbatim host payloads\n * must fit. Values are stored, round-tripped, compared canonically, and NEVER\n * interpreted. */\nexport type JsonValue =\n | null\n | boolean\n | number\n | string\n | JsonValue[]\n | { [key: string]: JsonValue };\n\nexport interface GraphNode<N = Record<string, unknown>> {\n id: NodeId;\n attrs?: N;\n /** Optional fixed/persisted position honored by the fixed layout. */\n x?: number;\n y?: number;\n}\n\nexport interface GraphEdge<E = Record<string, unknown>> {\n /**\n * Optional stable id. When absent, the core synthesizes a deterministic id\n * `${escapedSource}→${escapedTarget}#${k}` where `\\\\`, `→`, and `#` are\n * backslash-escaped inside endpoint ids, and k disambiguates parallel edges\n * in first-occurrence order. Simple endpoint ids retain the familiar\n * `${source}→${target}#${k}` form.\n */\n id?: EdgeId;\n source: NodeId;\n target: NodeId;\n attrs?: E;\n}\n\n/** Versioned snapshot — the declarative source of truth. */\nexport interface GraphSnapshot<N = Record<string, unknown>, E = Record<string, unknown>> {\n /** Identity of the dataset; changing it clears all per-dataset state. */\n datasetKey: string;\n /** Caller-owned revision; same {datasetKey, sourceRevision} replays are idempotent. */\n sourceRevision: number | string;\n nodes: readonly GraphNode<N>[];\n edges: readonly GraphEdge<E>[];\n}\n\n// ---------------------------------------------------------------------------\n// Diagnostics. Batched: one diagnostic per code per validation\n// pass with a count and capped samples — O(categories), never O(bad rows).\n// ---------------------------------------------------------------------------\n\nexport type DiagnosticSeverity = 'info' | 'warning' | 'error';\n\nexport type DiagnosticCode =\n | 'duplicate-node-id'\n | 'duplicate-edge-id'\n | 'dangling-edge-endpoint'\n | 'invalid-node'\n | 'invalid-edge'\n | 'self-loop-retained'\n /** A filter predicate threw or an expr referenced bad data; aggregated. */\n | 'filter-error'\n /** Async metric column rejected due to misalignment, duplicates, or unknown ids. */\n | 'metric-column-error'\n /** A channel reprojected repeatedly with identical outputs. */\n | 'accessor-churn'\n /** Image atlas resolve/decoding failures, cadence-batched. */\n | 'image-resolve-failed'\n | 'source-revision-reused'\n /** columnar lane: invalid structure (length mismatch, detached\n * buffer, out-of-range dictionary or endpoint index) — the WHOLE snapshot\n * is rejected before derivation; the previous accepted scene stays. */\n | 'invalid-columnar-snapshot'\n /** The worker lane could not boot — columnar acceptance runs\n * on the main lane instead (info under execution:'auto', error under\n * 'worker'). One-shot per instance. */\n | 'worker-unavailable'\n /** A host config lane was rejected at the boundary — e.g. a\n * groups array whose containment is cyclic or multiply parented); the\n * previous config stays live. */\n | 'config-error'\n /** a setViewState payload failed structural validation or carries\n * a version newer than this library; NOTHING was applied. */\n | 'invalid-view-state'\n | 'engine-error'\n | 'accessor-error'\n /** A user event listener threw; isolated so the listener chain continues. */\n | 'listener-error'\n /** showLabelsFor exceeded tracked-label capacity; omissions counted. */\n | 'label-overload'\n /** A same-id row from an earlier overlay won in admission order. */\n | 'overlay-node-shadowed'\n /** A service call was aborted/discarded before admission. */\n | 'service-aborted'\n /** A service call failed. */\n | 'service-error'\n | 'context-lost'\n | 'operation-rejected'\n /** Adapter-defined codes are namespaced. */\n | `engine:${string}`;\n\nexport const DIAGNOSTIC_SAMPLE_CAP = 10;\n\nexport interface GraphDiagnostic {\n code: DiagnosticCode;\n severity: DiagnosticSeverity;\n /** Total occurrences in the pass this diagnostic summarizes. */\n count: number;\n /** At most DIAGNOSTIC_SAMPLE_CAP offending ids. */\n sampleIds: readonly string[];\n message: string;\n}\n\n// ---------------------------------------------------------------------------\n// Revisions.\n// ---------------------------------------------------------------------------\n\nexport interface Revisions {\n /** Last accepted caller sourceRevision (null before first accept). */\n source: number | string | null;\n /** Monotonic counter advanced on every accepted model change. */\n model: number;\n /** Filtering/subgraph scope revision. Advances with every accepted\n * model change AND on every hard-scope (subgraph) change; a SCOPE-ONLY\n * change advances `scope` and `render` but NOT `model` — the first genuine\n * scope/model split. */\n scope: number;\n /** Monotonic counter advanced on every desired-render publication. */\n render: number;\n /** Highest render revision the engine has visibly applied (null pre-mount). */\n appliedRender: number | null;\n}\n\n// ---------------------------------------------------------------------------\n// Accepted graph — output of validation, input to the reconciler.\n// ---------------------------------------------------------------------------\n\nexport interface AcceptedEdge<E = Record<string, unknown>> extends GraphEdge<E> {\n id: EdgeId;\n}\n\nexport interface AcceptedGraph<N = Record<string, unknown>, E = Record<string, unknown>> {\n datasetKey: string;\n sourceRevision: number | string;\n /** Deduplicated (first-wins), in accepted-base order. */\n nodes: readonly GraphNode<N>[];\n /** Dangling endpoints dropped; ids present (synthesized when needed). */\n edges: readonly AcceptedEdge<E>[];\n /** id → position in `nodes` (accepted-base order). */\n nodeIndex: ReadonlyMap<NodeId, number>;\n diagnostics: readonly GraphDiagnostic[];\n}\n\n// ---------------------------------------------------------------------------\n// RenderScene — compact typed scene the reconciler publishes. Public\n// payloads never expose engine indices; this type is internal-ish but\n// exported for FakeEngine-based testing.\n// ---------------------------------------------------------------------------\n\nexport interface RenderScene {\n count: number;\n linkCount: number;\n /** engine index → node id. */\n idByIndex: readonly NodeId[];\n /** node id → engine index. */\n indexById: ReadonlyMap<NodeId, number>;\n /** engine link index → edge id. */\n edgeIdByIndex: readonly EdgeId[];\n /**\n * 2*count floats. NaN pairs mean \"no known position\" — the engine seeds\n * them; known positions come from the position cache.\n */\n positions: Float32Array;\n /** 2*linkCount uint32 endpoint indices into the point set. */\n links: Uint32Array;\n /**\n * Synthetic suffix. Present iff the scene was rewritten\n * by collapsed groups: point slots >= physicalPointCount are super-nodes\n * and link slots >= physicalLinkCount are meta-edges (synthetics are always\n * a contiguous suffix). For those slots, idByIndex/edgeIdByIndex hold\n * INTERNAL scene keys that never escape public payloads — consumers\n * resolve slots through the discriminated ScenePointRef/SceneLinkRef\n * helpers instead.\n */\n groups?: SceneGroups;\n}\n\n/** compact synthetic-suffix descriptor attached to a rewritten scene. */\nexport interface SceneGroups {\n physicalPointCount: number;\n physicalLinkCount: number;\n /** Aligned to point slots physicalPointCount..count-1. */\n superNodes: readonly ResolvedGroup[];\n /** Aligned to link slots physicalLinkCount..linkCount-1. */\n metaEdges: readonly MetaEdge[];\n /**\n * node folds: representatives that are REAL nodes, so they carry no\n * synthetic slot and never appear in `superNodes`. A folded anchor keeps\n * its physical row (and its own caller-driven styling) — this\n * list only reports how many descendants it currently stands for, for\n * badge rendering. Empty when nothing is folded.\n */\n folds: readonly SceneFold[];\n}\n\n/** One drawn fold anchor and the descendant count it currently hides. */\nexport interface SceneFold {\n anchorId: NodeId;\n hiddenCount: number;\n}\n\n/** discriminated point ref: a physical node id or a resolved group\n * public namespaces only, never internal scene keys. */\nexport type ScenePointRef =\n | { kind: 'node'; id: NodeId }\n | { kind: 'group'; group: ResolvedGroup };\n\n/** discriminated link ref: a physical edge id or a meta-edge record. */\nexport type SceneLinkRef =\n | { kind: 'edge'; id: EdgeId }\n | { kind: 'meta-edge'; metaEdge: MetaEdge };\n\n// ---------------------------------------------------------------------------\n// Styling accessors: constant or function of the typed node.\n// Descriptor (FieldAccessor) forms arrive in later slices.\n// ---------------------------------------------------------------------------\n\nexport type Accessor<T, V> = V | ((item: T) => V);\n\nexport type LayoutKind = 'force' | 'fixed';\n\n/**\n * force tunables under stable, engine-neutral names — orbit maps them onto\n * the active engine's parameters through atomic config-only commits, so\n * a value here never resets positions or restarts the layout.\n *\n * Every field is optional and OMISSION MEANS \"leave the engine's default\n * alone\" — it is never written as an explicit value. The defaults quoted below\n * are cosmos 3.3.0's (`defaultConfigValues`), listed so a host knows what it is\n * overriding; an engine without a given force ignores that field.\n *\n * NOT here: `spaceSize` is a construction option on the adapter, not a runtime\n * tunable (cosmos documents that large values crash some devices, and the\n * seeding ring is derived from it).\n */\nexport interface SimulationConfig {\n /** Pull toward the layout centre. Default 0.25. */\n gravity?: number;\n /** How hard every node pushes every other away — the spread. Default 1. */\n repulsion?: number;\n /** Velocity retained per tick: lower settles sooner, higher keeps drifting.\n * Default 0.85. */\n friction?: number;\n /** Rest length of an edge spring. Default 10. */\n linkDistance?: number;\n /** Edge spring stiffness. Default 1. */\n linkSpring?: number;\n /**\n * Cool-down coefficient — how fast the run loses energy and comes to rest.\n * SMALLER cools slower (a longer, more thorough settle); larger snaps to a\n * stop. Default 5000.\n */\n decay?: number;\n /**\n * Overlap resolution: above 0, nodes push apart when their circles\n * intersect. Default 0 (OFF) — the reason dense clusters render as solid\n * blobs until you turn it on.\n */\n collision?: number;\n /** Collision circle radius. Default: derived from the point size. */\n collisionRadius?: number;\n /** Extra spacing added around each collision circle. Default 0. */\n collisionPadding?: number;\n /**\n * Barnes-Hut opening angle θ for the many-body approximation: larger is\n * coarser and faster, smaller is more exact and slower. Default 1.15.\n * @deprecated Ignored on cosmos >= 3.4 (grid-based repulsion replaced\n * Barnes-Hut; the engine emits `engine:repulsion-theta-deprecated` once).\n * Retained for engines with a Barnes-Hut many-body force.\n */\n repulsionTheta?: number;\n /** Attraction toward the scene's centre of mass. Default 0 (OFF). */\n center?: number;\n /** How strongly nodes shy away from the cursor. Default 2. */\n repulsionFromMouse?: number;\n}\n\n/**\n * Named simulation presets, measured on the reference protocol (800-node\n * clustered graph, max node displacement sampled at 500ms): the times below\n * are seconds until visible stillness (< 1.5 space units/s sustained).\n *\n * - `calm` — damped and settles in ~5s; the DEFAULT when no `simulation`\n * value is given. The engine's own defaults (`lively`) keep visible motion\n * alive for tens of seconds, which reads as jitter on first load.\n * - `spread` — airier inter-cluster spacing, ~7s to stillness.\n * - `tight` — compact clusters, ~6s to stillness.\n * - `lively` — the engine's own defaults: ambient continuous motion.\n */\nexport type SimulationPreset = 'calm' | 'spread' | 'tight' | 'lively';\n\n/** The `simulation` input surface: a full config or a named preset. */\nexport type SimulationInput = SimulationConfig | SimulationPreset;\n\nexport const SIMULATION_PRESETS: Readonly<Record<SimulationPreset, Readonly<SimulationConfig>>> =\n Object.freeze({\n calm: Object.freeze({ repulsion: 1.4, gravity: 0.15, friction: 0.6, decay: 1000 }),\n spread: Object.freeze({ repulsion: 2, gravity: 0.1, friction: 0.6, decay: 1400 }),\n tight: Object.freeze({ repulsion: 0.8, gravity: 0.3, friction: 0.55, decay: 1200 }),\n lively: Object.freeze({ repulsion: 1, gravity: 0.25, friction: 0.85, decay: 5000 }),\n });\n\n/** Resolve a `simulation` input to a concrete config. Omitted input resolves\n * to the `calm` preset — the measured-good default. Preset strings resolve to\n * frozen singletons, so identity comparison stays meaningful. */\nexport function resolveSimulation(input: SimulationInput | undefined): SimulationConfig {\n if (input === undefined) return SIMULATION_PRESETS.calm;\n if (typeof input === 'string') return SIMULATION_PRESETS[input] ?? SIMULATION_PRESETS.calm;\n return input;\n}\n\n// ---------------------------------------------------------------------------\n// Host update — the atomic boundary: one call carries data + config +\n// controlled state and publishes exactly one store revision and at most one\n// engine commit.\n// ---------------------------------------------------------------------------\n\n// ---------------------------------------------------------------------------\n// columnar snapshots — the supported large-data lane:\n// transferable typed columns and PRE-INDEXED endpoints (source/target are\n// node indices, not ids). Spec-verbatim shapes.\n// ---------------------------------------------------------------------------\n\nexport type ColumnChange = {\n /** Unchanged revision permits index/cache reuse (assertion, not a hint). */\n revision?: string | number;\n /** Half-open, sorted, disjoint — MUST exhaust every changed row. */\n dirtyRanges?: readonly { start: number; end: number }[];\n};\n\n/** Dictionary-encoded strings. `nulls`: one byte per row, nonzero = null. */\nexport type StringColumn = ColumnChange & {\n kind: 'string';\n dictionary: readonly string[];\n codes: Uint32Array;\n nulls?: Uint8Array;\n};\n\nexport type Column =\n | (ColumnChange & { kind: 'f64'; data: Float64Array; nulls?: Uint8Array })\n | (ColumnChange & { kind: 'i32'; data: Int32Array; nulls?: Uint8Array })\n | (ColumnChange & { kind: 'u32'; data: Uint32Array; nulls?: Uint8Array })\n | (ColumnChange & { kind: 'bool'; data: Uint8Array; nulls?: Uint8Array })\n | StringColumn;\n\n/** Supported large-data lane: transferable columns and pre-indexed endpoints. */\nexport interface ColumnarGraphSnapshot<N = Record<string, unknown>, E = Record<string, unknown>> {\n kind: 'columnar';\n datasetKey: string;\n sourceRevision: string | number;\n /** Default 'borrowed'. 'transfer' detaches the supplied ArrayBuffers ONLY\n * after structural validation AND admission succeed; the\n * snapshot object is then single-use. */\n bufferOwnership?: 'borrowed' | 'transfer';\n nodes: {\n ids: StringColumn;\n columns: Readonly<Record<string, Column>>;\n length: number;\n /** Compile-time witness only; never materialized. */\n readonly __attrs?: N;\n };\n edges: {\n ids: StringColumn;\n source: Uint32Array;\n target: Uint32Array;\n endpointRevision?: string | number;\n endpointDirtyRanges?: readonly { start: number; end: number }[];\n columns: Readonly<Record<string, Column>>;\n length: number;\n readonly __attrs?: E;\n };\n}\n\nexport type GraphSnapshotInput<N = Record<string, unknown>, E = Record<string, unknown>> =\n | GraphSnapshot<N, E>\n | ColumnarGraphSnapshot<N, E>;\n\nexport interface GraphHostUpdate<N = Record<string, unknown>, E = Record<string, unknown>> {\n data?: GraphSnapshotInput<N, E>;\n nodeColor?: Accessor<GraphNode<N>, string> | Scale<string, N>;\n nodeSize?: Accessor<GraphNode<N>, number> | Scale<number, N>;\n linkColor?: Accessor<AcceptedEdge<E>, string>;\n linkWidth?: Accessor<AcceptedEdge<E>, number>;\n /** async metric columns, joined once with revision-gated admission. */\n metrics?: readonly MetricColumn[];\n /**\n * image sprites: synchronous, string-valued ref accessor (URL/blob\n * ref/cache key — opaque to orbit). Refs feed the image-atlas pipeline when\n * the engine declares `pointImages`; otherwise refs are retained and the\n * placeholder shape renders.\n */\n /** image refs; `null` CLEARS the accessor and evicts the atlas back to\n * placeholders (D2 explicit reset — omission stays \"no change\"). */\n nodeImage?: ((node: GraphNode<N>) => string | null) | null;\n /** instanced arrowheads (capability-gated; inert when unsupported). */\n edgeArrows?: boolean;\n /** durable source coordinate for view states — stored VERBATIM,\n * never interpreted; serialized by getViewState and canonically compared\n * on setViewState. Stash-only lane: no publish, no commit. Omission means\n * no change (there is no clear form in v1 — set `{}` for emptiness). */\n dataRef?: JsonValue;\n /** runtime toggles — atomic config-only commits, no reprojection. */\n showLinks?: boolean;\n /** emphasis-ring toggle (default TRUE — the ring predates its name: it\n * has followed hover since v0.1). False clears the engine ring once and\n * suppresses every driver (hover, focusNode, emphasizeNode). */\n emphasisRing?: boolean;\n layout?: LayoutKind;\n simulation?: SimulationInput;\n /** Controlled selection (uncontrolled when never provided; subset). */\n selection?: readonly NodeId[];\n theme?: ThemeInput;\n /** DOM label lane configuration; strategy 'dom' only in v0.4. */\n labels?: LabelConfig<N>;\n /** accessibility runtime options. */\n accessibility?: AccessibilityConfig<N>;\n /** hard scope: feed ONLY the resolved subset through the reconciler;\n * null restores full scope. Positions come from the cache; reflow default\n * true restarts the layout around the remainder. */\n subgraph?: SubgraphSpec | null;\n /** soft filter: mask (hide/dim) with ZERO relayout; null clears. */\n filter?: FilterSpec<N, E> | null;\n /** crossfilter dimensions (declarative; brushes live on the session). */\n crossfilter?: readonly DimensionSpec<N>[];\n /** manual groups; null clears (D2). Config-error with groupBy. */\n groups?: readonly GroupSpec[] | null;\n /** derived grouping; null clears (D2). Config-error with groups. */\n groupBy?: GroupBySpec<N> | null;\n /** stage-4 non-collapsing layout clusters; null clears (D2). Clusters\n * COEXIST with groups — they preserve every node and edge. */\n clusters?: ClusterSpec<N> | null;\n /** persistent pins (independent of transient drag pinning); null\n * clears (D2). Departed ids prune through ownership. */\n pinnedNodeIds?: readonly NodeId[] | null;\n /** parallel-edge grouping toggle: same-pair edges collapse into one\n * count-weighted meta-edge. */\n parallelEdgeGrouping?: boolean;\n // NOTE (D7): `searchIndex` is a CONSTRUCTION option (spec host\n // construction options — read once; changing it requires a keyed remount).\n // It is deliberately NOT a host-update lane; a runtime attempt is ignored\n // with a one-shot 'operation-rejected' warning diagnostic.\n}\n\n// ---------------------------------------------------------------------------\n// Scales and metrics. Scales are plain descriptors and\n// compare by CANONICAL STRUCTURAL VALUE — equal inline literals never\n// reproject. The categorical `by` accepts a field name (addressing\n// attrs[field], 'id' for the entity id — the FilterExpr convention) or a\n// function compared by reference; FieldAccessor descriptors arrive with the\n// columnar lane.\n// ---------------------------------------------------------------------------\n\n/** Built-in synchronous metrics plus caller-supplied async column names. */\nexport type MetricName = 'degree' | 'inDegree' | 'outDegree' | (string & {});\n\nexport interface DomainPolicy {\n /** Domain population. Default 'dataset' (frozen per dataset revision\n * masking/isolation never change what a color means). */\n scope?: 'dataset' | 'hard-scope' | 'visible';\n /** Streaming behavior. Default 'freeze-per-revision'; 'expand' permits\n * monotonic growth as batches arrive. */\n streaming?: 'freeze-per-revision' | 'expand';\n}\n\nexport type Scale<T, N = Record<string, unknown>> =\n | {\n kind: 'sequential';\n metric: MetricName;\n range: readonly [T, T];\n domain?: readonly [number, number] | DomainPolicy;\n }\n | {\n kind: 'categorical';\n by: string | ((node: GraphNode<N>) => string | null);\n palette?: readonly T[];\n /** Fixed category order → stable colors and stable legend rows,\n * including empty categories; out-of-domain values hash stably. */\n domain?: readonly string[];\n domainPolicy?: DomainPolicy;\n }\n | {\n kind: 'diverging';\n metric: MetricName;\n mid: number;\n range: readonly [T, T, T];\n };\n\n/** Async metric column joined against the accepted model. */\nexport interface MetricColumn {\n metric: string;\n /** 'ids' joins by the ids array; 'index' is accepted-base positional. */\n align: 'ids' | 'index';\n values: readonly (number | null)[];\n ids?: readonly NodeId[];\n /**\n * Issue-time stamp: the `getRevisions().model` value CURRENT WHEN\n * THE UPDATE CARRYING THIS COLUMN WAS BUILT. Capture it before starting an\n * async computation and deliver it with the result — admission rejects the\n * column (info diagnostic) when the model has moved since, so stale async\n * work can never join a newer roster. Columns delivered atomically with\n * their matching `data` in one update stamp the revision current at build\n * time (the pre-update revision): the transaction is atomic, so that stamp\n * uniquely names the roster the columns were derived from.\n */\n forModelRevision: number;\n}\n\n// ---------------------------------------------------------------------------\n// theme tokens. The `theme` prop accepts a full GraphTheme, a partial over\n// a named base, or the v0.1 `{background}` shorthand (kept compatible).\n// ---------------------------------------------------------------------------\n\nexport interface GraphTheme {\n background: string;\n nodeDefault: string;\n edgeDefault: string;\n labelFg: string;\n accent: string;\n mutedAlpha: number;\n /** emphasis-ring color (pointer hover, `focusNode`, `emphasizeNode`).\n * Distinct from `accent` on purpose: accent is the SELECTION highlight, and\n * an emphasized node must not read as selected. */\n emphasisRing: string;\n}\n\nexport type ThemeInput =\n | (Partial<GraphTheme> & { base?: 'light' | 'dark' })\n | GraphTheme;\n\n// ---------------------------------------------------------------------------\n// soft filtering — mask, never reflow. `field` addresses `attrs[field]`\n// ('id' addresses the entity id). Serializable exprs compare by canonical\n// structural value (identity churn with equal structure never re-evaluates);\n// function predicates compare by reference and re-evaluate O(n) on change.\n// ---------------------------------------------------------------------------\n\nexport type FilterMode = 'hide' | 'dim';\n\nexport type FilterValue = string | number | boolean | null;\n\nexport type FilterExpr =\n | { op: 'eq' | 'neq'; field: string; value: FilterValue }\n | { op: 'in'; field: string; values: readonly FilterValue[] }\n | {\n op: 'range';\n field: string;\n min?: number;\n max?: number;\n /** Default true. */\n includeMin?: boolean;\n /** Default true. */\n includeMax?: boolean;\n }\n | { op: 'is-null'; field: string }\n | { op: 'not'; expr: FilterExpr }\n | { op: 'and' | 'or'; exprs: readonly FilterExpr[] };\n\nexport interface FilterSpec<N = Record<string, unknown>, E = Record<string, unknown>> {\n nodes?: FilterExpr | ((node: GraphNode<N>) => boolean);\n edges?: FilterExpr | ((edge: AcceptedEdge<E>) => boolean);\n /** 'hide' removes from view (alpha 0 + picking); 'dim' mutes. Default 'hide'. */\n mode?: FilterMode;\n}\n\n// ---------------------------------------------------------------------------\n// crossfilter (v0.7 subset: node dimensions, typed-column backend).\n// ---------------------------------------------------------------------------\n\nexport type DimensionKind = 'numeric' | 'temporal' | 'categorical';\n\nexport interface DimensionSpec<N = Record<string, unknown>> {\n /** Stable dimension key (brushes rebase by this key across data updates). */\n key: string;\n kind: DimensionKind;\n /** Raw value accessor; hygiene applies (non-finite → excluded from bins).\n * Temporal accepts epoch-ms numbers, ISO strings, or 'YYYY-MM-DD'. */\n get: (node: GraphNode<N>) => unknown;\n /** Histogram bin count for numeric/temporal (default 24). */\n bins?: number;\n}\n\n/** Numeric/temporal brush (coordinates in the dimension's units — epoch ms\n * for temporal), or categorical EXCLUSIONS, or null = no brush. */\nexport type BrushState =\n | { min: number; max: number }\n | { excluded: readonly string[] }\n | null;\n\nexport interface HistogramBin {\n x0: number;\n x1: number;\n /** Rows in this bin regardless of any mask. */\n total: number;\n /** Rows in this bin passing every OTHER dimension's brush + the filter\n * prop's node mask (the joint \"filtered\" second layer). */\n filtered: number;\n}\n\nexport interface CategoryBin {\n key: string;\n total: number;\n filtered: number;\n excluded: boolean;\n}\n\nexport interface DimensionSummary {\n key: string;\n kind: DimensionKind;\n /** Numeric/temporal domain (finite rows only); undefined when empty. */\n domain?: { min: number; max: number };\n bins: readonly HistogramBin[];\n categories: readonly CategoryBin[];\n /** Rows excluded by hygiene (non-finite / unparseable). */\n excludedRows: number;\n}\n\nexport interface CrossfilterSession {\n /** Monotonic from 0; advances exactly once per observable selection change. */\n readonly selectionRevision: number;\n /** Latest-call-wins coalescing per dimension; resolves once observable. */\n setBrush(key: string, brush: BrushState): Promise<void>;\n getBrush(key: string): BrushState;\n summarize(key: string): DimensionSummary;\n /** Fires once per observable selection/summary change. */\n subscribe(cb: () => void): () => void;\n}\n\n// ---------------------------------------------------------------------------\n// timeline playback (headless controller; v0.7).\n// ---------------------------------------------------------------------------\n\nexport interface TimelinePlayback {\n /** 'sliding' plays a fixed window; 'cumulative' grows from the domain start. */\n mode: 'sliding' | 'cumulative';\n /** Window width in dimension units (sliding; default domain/10). */\n window?: number;\n /** Tick interval in ms (default 100). */\n tickMs?: number;\n /** Fraction of the domain traversed per tick (default 0.01). */\n step?: number;\n loop?: boolean;\n}\n\n// ---------------------------------------------------------------------------\n// hard scope + expansion services.\n// ---------------------------------------------------------------------------\n\nexport interface SubgraphSpec {\n seedIds: readonly NodeId[];\n /** Expand N hops from the seeds via the expansion service (default 0). */\n hops?: number;\n /** Restart the layout around the subset (default true). */\n reflow?: boolean;\n}\n\n// ---------------------------------------------------------------------------\n// search. The default service is client-side, field-scoped over the\n// declared searchIndex (id-only when absent — it never guesses attr names);\n// custom services plug in server-side search. Search NEVER\n// changes scope/filters or fetches graph data.\n// ---------------------------------------------------------------------------\n\nexport interface SearchResult<N = Record<string, unknown>> {\n id: string;\n score?: number;\n label?: string;\n node?: GraphNode<N>;\n}\n\n/** Why an activated result could not be focused. */\nexport type SearchUnavailableReason = 'not-loaded' | 'out-of-scope' | 'filtered';\n\nexport type SearchActivation =\n | { status: 'focused'; id: NodeId }\n | { status: 'unavailable'; reason: SearchUnavailableReason; result: SearchResult };\n\n/** Context every async service call receives. */\nexport interface RequestContext {\n datasetKey: string;\n sourceRevision: number | string | null;\n modelRevision: number;\n scopeRevision: number;\n requestId: string;\n /** Abort is an optimization; admission is the correctness gate. */\n signal: AbortSignal;\n}\n\nexport type RevisionDimension = 'source' | 'model' | 'scope';\n\n/** A service declares exactly the revision dimensions it consumes. */\nexport interface RevisionAwareService {\n readonly revisionDependencies: readonly RevisionDimension[];\n}\n\nexport interface ExpansionBatch<N = Record<string, unknown>, E = Record<string, unknown>> {\n nodes?: readonly GraphNode<N>[];\n edges?: readonly GraphEdge<E>[];\n}\n\nexport type ExpansionResponse<N = Record<string, unknown>, E = Record<string, unknown>> =\n | (ExpansionBatch<N, E> & { provenance?: unknown })\n | { batches: AsyncIterable<ExpansionBatch<N, E>>; provenance?: unknown };\n\n/**\n * path resolver seam. `find` resolves the node/edge id path\n * between two loaded nodes or null when unreachable (null is a RESULT, not\n * an error). Extends the revision-aware contract: abort is advisory,\n * revision admission at delivery is authoritative.\n */\nexport interface PathService extends RevisionAwareService {\n find(\n sourceId: NodeId,\n targetId: NodeId,\n options: PathOptions,\n ctx: RequestContext,\n ): Promise<PathResult | null>;\n}\n\nexport interface ExpansionService<N = Record<string, unknown>, E = Record<string, unknown>>\n extends RevisionAwareService {\n neighbors(\n seedIds: readonly NodeId[],\n hops: number,\n ctx: RequestContext,\n ): Promise<ExpansionResponse<N, E>>;\n}\n\n// ---------------------------------------------------------------------------\n// revisioned ingestion — bounded, cancellable sessions serialized\n// through the instance-local acceptance queue.\n// ---------------------------------------------------------------------------\n\nexport interface BeginIngestOptions {\n /** 'replace' commits a new source coordinate atomically; 'overlay' advances\n * only modelRevision and may be progressive. */\n purpose: 'replace' | 'overlay';\n datasetKey: string;\n /** Required for 'replace': the source coordinate the commit establishes. */\n sourceRevision?: number | string;\n /** CAS precondition: the model revision current when the session begins\n * (zero on an empty instance). Mismatch rejects with 'stale-revision'. */\n baseModelRevision: number;\n /** Overlays only (replace is always atomic). Default true. */\n atomic?: boolean;\n /** Caller-supplied stable overlay id; generated when omitted. */\n overlayId?: string;\n /** Progressive overlays: flush no later than this while running (default 50). */\n maxFlushLatencyMs?: number;\n /** Byte backpressure budget. Progressive receipts await drainage past this;\n * atomic sessions terminally reject an append that would exceed it because\n * atomic staging cannot drain before commit. */\n maxPendingBytes?: number;\n}\n\nexport interface IngestBatch<N = Record<string, unknown>, E = Record<string, unknown>> {\n /** Consecutive, strictly monotonic from zero. */\n sequence: number;\n /** Idempotency key: an admitted {sequence, batchId} replay returns its\n * original receipt; same sequence + different batchId rejects. */\n batchId: string;\n nodes?: readonly GraphNode<N>[];\n edges?: readonly GraphEdge<E>[];\n /** Caller-declared payload size; estimated when omitted. */\n bytes?: number;\n}\n\nexport interface AppendReceipt {\n sequence: number;\n batchId: string;\n admittedNodes: number;\n admittedEdges: number;\n /** Present once the flush containing this batch became public (progressive\n * overlays resolve only then, so exact replays return complete receipts). */\n publishedModelRevision?: number;\n /** Bytes admitted but not yet flushed (the backpressure signal). */\n pendingBytes: number;\n}\n\nexport interface IngestCommitReceipt {\n overlayId?: string;\n modelRevision: number;\n sourceRevision?: number | string;\n admittedNodes: number;\n admittedEdges: number;\n /** Dangling edges dropped at commit; diagnostics are emitted only then. */\n danglingEdges: number;\n}\n\nexport type IngestSessionState = 'open' | 'committing' | 'committed' | 'aborted';\n\nexport interface IngestSession<N = Record<string, unknown>, E = Record<string, unknown>> {\n readonly state: IngestSessionState;\n readonly overlayId: string | undefined;\n append(batch: IngestBatch<N, E>): Promise<AppendReceipt>;\n commit(): Promise<IngestCommitReceipt>;\n abort(reason?: unknown): Promise<void>;\n}\n\n/** label lane configuration (zoom-LOD, ranking, forced ids). */\nexport interface LabelConfig<N = Record<string, unknown>> {\n enabled?: boolean;\n /** Labels appear only at/above this zoom (LOD threshold). Default 1. */\n minZoom?: number;\n /**\n * cluster-label LOD ceiling. At or BELOW this zoom the active\n * cluster labels render and NODE labels are suppressed; above it\n * cluster labels stop and node-label LOD (`minZoom`) takes over. Absent ⇒\n * no LOD hand-off: cluster labels (when a spec is active) and node labels\n * coexist, each on its own gate.\n */\n maxZoom?: number;\n /** Ranked-candidate cap k (viewport-culled). Default 64, policy max 1024. */\n maxVisible?: number;\n /** Ids that claim capacity FIRST, bypassing ranking. */\n showFor?: readonly NodeId[];\n /** Label text; default attrs.label ?? id. Rendered as a TEXT NODE. */\n getText?: (node: GraphNode<N>) => string;\n /** Ranking weight; default nodeSize result order, else degree. */\n getWeight?: (node: GraphNode<N>) => number;\n}\n\n/** accessibility runtime options. */\nexport interface AccessibilityConfig<N = Record<string, unknown>> {\n /** Canvas aria-label. Default 'Graph visualization'. */\n label?: string;\n description?: string;\n /** Max items per navigator relationship page. Default 50. */\n navigatorWindow?: number;\n /** Gate live-region announcements (default true). */\n announcements?: boolean;\n /** Text name for a node in the navigator/live region; default label/id. */\n getAccessibleLabel?: (node: GraphNode<N>) => string;\n /**\n * Reduced-motion override: true forces reduced, false forces full motion,\n * undefined follows the host binding's media-query detection.\n */\n reducedMotion?: boolean;\n}\n\n/** One positioned label emitted to the overlay lane per scheduler tick. */\nexport interface LabelPlacement {\n /** Node id — or, for `kind: 'cluster'`, the CLUSTER KEY. */\n id: NodeId;\n text: string;\n /** Screen coordinates (CSS px, container-relative). */\n x: number;\n y: number;\n forced: boolean;\n /**\n * placement kind. 'node' (default) anchors to the node's cached\n * position; 'cluster' anchors to the cluster's force center while the\n * simulation is hot and to its settled centroid afterwards, and selects its\n * MEMBER node ids when activated. Ids are drawn from\n * different namespaces, so consumers must key on `(kind, id)`.\n */\n kind?: 'node' | 'cluster';\n}\n\n// ---------------------------------------------------------------------------\n// Store state (vanilla Zustand subset).\n// ---------------------------------------------------------------------------\n\nexport interface ViewportState {\n x: number;\n y: number;\n zoom: number;\n}\n\nexport type InstanceStatus =\n | 'idle'\n | 'mounting'\n | 'ready'\n /** WebGL context lost; engine frozen, CPU model stays live. */\n | 'lost'\n /** Context restored; the full-scene replay commit is in flight. */\n | 'recovering'\n | 'destroyed'\n | 'error';\n\n/**\n * Namespaced selection. Namespaces are independent: node-set algebra\n * never mutates edge selection. `groupIds` is populated by group operations.\n */\nexport interface SelectionState {\n nodeIds: readonly NodeId[];\n edgeIds: readonly EdgeId[];\n groupIds: readonly string[];\n}\n\n// ---------------------------------------------------------------------------\n// Semantic exploration: groups, groupBy, meta-edges, paths.\n// ---------------------------------------------------------------------------\n\n/** manual group definition. Flat and disjoint: membership may not\n * nest, overlap, duplicate, self-reference, or name unknown ids — violations\n * are config-error diagnostics BEFORE any scene rewrite. */\nexport interface GroupSpec {\n /** Public group id — its own namespace, never colliding with node ids. */\n id: string;\n memberIds: readonly NodeId[];\n label?: string;\n /** Collapsed groups rewrite to super-nodes with meta-edges (stage 3). */\n collapsed?: boolean;\n color?: string;\n}\n\n/** derived grouping: one group per distinct accessor key (null =\n * ungrouped). Membership is derived and READ-ONLY; collapsed defaults false\n * so adding groupBy alone changes no rendering. */\nexport interface GroupBySpec<N = Record<string, unknown>> {\n by: (node: GraphNode<N>) => string | null;\n /** Hysteresis semantic zoom: crossing below collapseBelow collapses all\n * derived groups; crossing above expandAbove expands only groups\n * intersecting the viewport; between the thresholds the band holds.\n * expandAbove must be strictly greater than collapseBelow. */\n semanticZoom?: { collapseBelow: number; expandAbove: number };\n}\n\n/**\n * stage-4 non-collapsing layout clusters: a categorical `by` accessor\n * partitions the PHYSICAL scene (`null` ⇒ unclustered) into force-clustered,\n * centroid-labelled sets. Clusters preserve every node and edge and therefore\n * NEVER synthesize super-nodes or meta-edges; they coexist with\n * groups and re-derive over the post-group-rewrite physical scene.\n */\nexport interface ClusterSpec<N = Record<string, unknown>> {\n /** Membership accessor, compared by function REFERENCE (a new inline lambda\n * re-derives — the groupBy convention). */\n by: (node: GraphNode<N>) => string | null;\n /** Cluster-force strength handed to the engine. Inert (with ONE loud\n * degradation diagnostic) on engines that do not declare `clusterForce`;\n * membership, labels, and centroids still work. */\n strength?: number;\n /** Explicit force centers per key, in SPACE coordinates. Keys omitted here\n * generate deterministically from the ordered keys + layout seed; see\n * `resolveClusterCenters`. */\n centers?: ReadonlyMap<string, readonly [number, number]>;\n}\n\n/** Resolved cluster surface for overlays/selection (public ids only). */\nexport interface ResolvedCluster {\n /** The categorical key — also the cluster label's text and overlay id. */\n key: string;\n /** Member PHYSICAL node ids in scene order. */\n memberIds: readonly NodeId[];\n /** The force center labels anchor to while the simulation is HOT. */\n forceCenter: readonly [number, number];\n /** Settled centroid from the last permitted readback (or the commit\n * under a fixed layout); null until one has landed. */\n centroid: readonly [number, number] | null;\n}\n\n/** Resolved group surface for events/selection/store (public namespace). */\nexport interface ResolvedGroup {\n id: string;\n label?: string;\n memberIds: readonly NodeId[];\n collapsed: boolean;\n /** True for groupBy-derived groups (membership read-only). */\n derived: boolean;\n color?: string;\n}\n\n/** rerouted member edge on a collapsed group (stage 3), or a grouped\n * parallel-edge bundle. Count is the badge datum. */\nexport interface MetaEdge {\n id: string;\n /** Node id OR group id endpoint (public namespaces). */\n source: string;\n target: string;\n /** Underlying (rerouted / collapsed-parallel) edge count. */\n count: number;\n}\n\n/** path query options (PathService). */\nexport interface PathOptions {\n /** Edge-direction rule for traversal. Default 'outgoing'. */\n direction?: 'outgoing' | 'incoming' | 'either';\n}\n\n/** A resolved path: node ids in order plus the edge ids walked. */\nexport interface PathResult {\n nodeIds: readonly NodeId[];\n edgeIds: readonly EdgeId[];\n}\n\nexport interface GraphStoreState {\n status: InstanceStatus;\n revisions: Revisions;\n nodeCount: number;\n edgeCount: number;\n selection: SelectionState;\n hover: { nodeId: NodeId | null; edgeId: EdgeId | null };\n /** id → pinned space position. */\n pins: ReadonlyMap<NodeId, readonly [number, number]>;\n /** PERSISTENT pins: ids held at their CURRENT position via\n * engine.setPinnedIndices. Independent lifecycle from the transient\n * drag-pin `pins` slice — the engine receives the UNION; releasing a drag\n * pin on a persistently-pinned node leaves it pinned. No position payload\n * in v0.10: a persistent pin freezes the node wherever it currently is. */\n pinnedNodeIds: ReadonlySet<NodeId>;\n hiddenNodeIds: ReadonlySet<NodeId>;\n /** Active hard scope; null = full scope. */\n scope: SubgraphSpec | null;\n /** Soft-mask visibility counts: RENDERED SCENE entities with zero\n * hide-failures — the synthetic suffix INCLUDED, so a collapsed\n * group contributes its one drawn super-node. Equals nodeCount/edgeCount\n * when nothing masks, scopes, or groups.\n *\n * NOT the same question as `getVisibleNodeIds()`, which lists PUBLIC\n * physical ids only. Pair a count with that list via\n * `getVisibleNodeIds().length`; use `visible` for \"how much is on screen\". */\n visible: { nodes: number; edges: number };\n /** Timeline playback state: at most one playing dimension. */\n timeline: { playingKey: string | null };\n /** history kernel depths. */\n history: { undoDepth: number; redoDepth: number };\n /** Node ids with an expansion in flight. */\n pendingExpansions: ReadonlySet<NodeId>;\n /**\n * node folds: anchor id → how many members it stands for. Empty when\n * nothing is folded.\n *\n * Published so folds are OBSERVABLE. A fold changes neither an anchor's id\n * nor its label text, so the label lane — which re-renders content only\n * when the candidate SET changes — would otherwise never re-render a badge\n * that depends on fold state. Subscribing to this slice is how a host keeps\n * fold-derived chrome (badges, affordances) in step.\n */\n folds: ReadonlyMap<NodeId, number>;\n /** Committed overlay ids for the current dataset. */\n overlayIds: readonly string[];\n /** resolved groups (manual or groupBy-derived); [] when ungrouped.\n * Path highlight is deliberately NOT here: session-local, never\n * serialized. */\n groups: readonly ResolvedGroup[];\n /** Last completed search: feeds <GraphSearch> and the\n * navigator's search-results section. Cleared on datasetKey change. */\n search: { query: string; results: readonly SearchResult[] } | null;\n viewport: ViewportState | null;\n /** Live force-simulation activity — true after a commit with\n * restart or resumeSimulation; false on settle or pauseSimulation. */\n simulationRunning: boolean;\n /** Resolved theme tokens: the merged GraphTheme currently driving\n * engine config, projection fallbacks, and mask dim alpha. Published on\n * change; defaults to the dark base. */\n theme: GraphTheme;\n diagnostics: readonly GraphDiagnostic[];\n}\n\n// ---------------------------------------------------------------------------\n// Typed events: payloads carry caller objects, never indices.\n// Listener chains run synchronously in registration order; the second\n// argument's preventDefault cancels ONLY the built-in follow-up action\n// (e.g. click-selection), never other listeners.\n// ---------------------------------------------------------------------------\n\nexport interface GraphListenerControl {\n preventDefault(): void;\n}\n\nexport interface NodeEventPayload<N = Record<string, unknown>> {\n node: GraphNode<N>;\n}\n\n// ---------------------------------------------------------------------------\n// telemetry + degradation ladder. Spec-verbatim shapes.\n// ---------------------------------------------------------------------------\n\n/** engine buffer channels. Canonical home (engine/index.ts re-exports\n * the engine seam imports from types, never the reverse). */\nexport type EngineBufferChannel =\n | 'pointPosition'\n | 'link'\n | 'pointColor'\n | 'pointSize'\n | 'linkColor'\n | 'linkWidth';\n\n/** performance snapshot — NEVER carries raw attrs or ids. */\nexport interface GraphPerfSnapshot {\n at: number;\n nodeCount: number;\n edgeCount: number;\n visibleNodeCount: number;\n visibleEdgeCount: number;\n /** Estimated bytes of CPU-side typed storage the instance holds (scene\n * buffers, base color caches, crossfilter columns, metric columns, mask\n * lanes). An estimate, not an audit — documented components only. */\n estimatedCpuBytes: number;\n /** Estimated bytes of engine-side channel storage (positions + the four\n * style channels at current scene sizes). Absent pre-scene. */\n estimatedGpuBytes?: number;\n queueDepth: number;\n modelRevision: number;\n scopeRevision: number;\n renderRevision: number;\n /** null while detached; may lag in mount/recovery. */\n appliedRenderRevision: number | null;\n lastCommitMs?: {\n kind: 'model' | 'scope' | 'config' | 'mask' | 'recovery';\n validate: number;\n derive: number;\n project: number;\n upload: number;\n firstDraw?: number;\n };\n activeDegradations: readonly DegradeStep[];\n execution: 'main' | 'worker';\n rangeUpdates: readonly EngineBufferChannel[];\n /** pressure-sampler mirror: EWMA of per-window mean frame\n * deltas, dropped-frame count, and idle wakeups since the last sample.\n * Zero idle wakeups is the healthy reading under the gated activity clock. */\n pressure: {\n frameEwmaMs: number;\n droppedFrames: number;\n idleWakeups: number;\n };\n}\n\n/** `limits` — construction-time thresholds for the ladder (construction-only:\n * read once; a runtime change warns and is ignored). */\nexport interface ScaleLimits {\n /** Default 100_000. */\n domLabelNodes: number;\n /** Default 250_000. */\n pickingLinks: number;\n /** Default 500_000. */\n histogramBatchNodes: number;\n /** Per-step engage/disengage band as a fraction. Default 0.10. */\n hysteresis: number;\n /** Minimum time a step holds its state. Default 1_000. */\n minimumDwellMs: number;\n /** Resource steps in engagement order. `uniform-link-style` participates\n * ONLY when explicitly listed — it can erase data-encoded styling, so\n * omission means resource admission rejects instead. */\n resourceDegradationOrder: readonly ResourceDegradeStep[];\n}\n\nexport type ResourceDegradeStep = 'disable-transitions' | 'defer-images' | 'uniform-link-style';\n\nexport type DegradeStep =\n | 'cap-dom-labels'\n | 'defer-link-picking'\n | 'batch-histograms'\n | ResourceDegradeStep;\n\nexport interface DegradeEvent {\n step: DegradeStep;\n engaged: boolean;\n reason: 'count' | 'resource-estimate' | 'frame-pressure' | 'input-pressure';\n visible: { nodes: number; edges: number };\n}\n\nexport interface GraphEventMap<N = Record<string, unknown>, E = Record<string, unknown>> {\n /** throttled telemetry sample — never per frame. */\n perfSample: GraphPerfSnapshot;\n /** Ladder step engagement/disengagement. This is a notification pattern,\n * not a controlled state lane. */\n degrade: DegradeEvent;\n nodeClick: NodeEventPayload<N> & { metaKey?: boolean };\n backgroundClick: Record<string, never>;\n nodeHover: { node: GraphNode<N> | null };\n edgeClick: { edge: AcceptedEdge<E> };\n edgeHover: { edge: AcceptedEdge<E> | null };\n nodeDragStart: NodeEventPayload<N>;\n /** Fired on drag release with the final space position; the built-in\n * follow-up pins the node there (preventDefault cancels the pin). */\n nodeDragEnd: NodeEventPayload<N> & { x: number; y: number };\n /** a setViewState dataRef mismatch — fired INSTEAD of applying.\n * Restoration proceeds only when the caller re-invokes with the opt-in. */\n viewStateMismatch: { stored: JsonValue | undefined; current: JsonValue | undefined };\n /** aggregate restore intent: fired ONCE per restore/history\n * transaction touching any controlled slice or serialized styling\n * never fanned out per lane. The host reflects every participating prop in\n * one commit; the transaction commits when the reflected values match, and\n * times out / diverges / supersedes as typed results otherwise. `next` is\n * the full target view state. */\n viewStateRestore: {\n transactionId: string;\n source: 'setViewState' | 'undo' | 'redo';\n next: unknown;\n };\n /** Right-click / long-press; built-in follow-up opens <GraphContextMenu>. */\n contextMenu: {\n target: { kind: 'node'; node: GraphNode<N> } | { kind: 'background' };\n /** Container-relative CSS px. */\n screen: readonly [number, number];\n };\n viewportChange: ViewportState;\n selectionChange: SelectionState;\n /** A super-node hit carries the resolved GROUP\n * never a GraphNode, never an internal scene key. Built-in follow-up\n * selects the group id into SelectionState.groupIds (preventDefault\n * cancels it, mirroring nodeClick). */\n groupClick: { group: ResolvedGroup; metaKey?: boolean };\n /** A meta-edge hit carries the MetaEdge record (public\n * endpoint ids + the underlying count badge datum). No built-in follow-up. */\n metaEdgeClick: { metaEdge: MetaEdge };\n /** groups slice change: op results (uncontrolled), op intents\n * (controlled — the host reflects the array back through the `groups`\n * prop), and groupBy re-derivations (notification; groupBy is always\n * instance-derived). Host `groups` prop writes and manual\n * model-drift re-resolutions are store-only and do NOT fire this. */\n groupsChange: { groups: readonly ResolvedGroup[] };\n /** persistent-pin slice change, the groups-latch mirror:\n * op results (uncontrolled) and op INTENTS (controlled — the host\n * reflects the array back through the `pinnedNodeIds` prop). Host prop\n * writes and model-drift prunes are store-only and do NOT fire this. */\n pinnedChange: { pinnedNodeIds: readonly NodeId[] };\n /** effective-set reporting seam: retractExpansion fires this\n * with the NEXT effective set as a SubgraphSpec whenever a collapse\n * changed what is displayed. v0.10 keeps `subgraph` UNCONTROLLED-ONLY, so\n * this is a notification today; a future controlled subgraph mode turns\n * it into the intent without changing the payload shape. */\n subgraphChange: { subgraph: SubgraphSpec };\n ready: Record<string, never>;\n error: { error: Error; detail?: GraphError };\n simulationEnd: Record<string, never>;\n}\n\nexport type GraphEventName = keyof GraphEventMap;\n","/**\n * stage-4 non-collapsing clusters — pure derivation, no\n * engine, no DOM, no instance.\n *\n * Contract summary:\n * - Clusters are a categorical PARTITION of the current PHYSICAL scene, never\n * a rewrite: `deriveClusters` returns membership only. It synthesizes no\n * super-nodes and no meta-edges, so node and edge counts are identical\n * before and after a cluster spec lands — clusters coexist\n * with the stage-3 group rewrite by construction.\n * - `null` (and any non-string) from `by` means UNCLUSTERED: the slot carries\n * NaN in {@link ClusterDerivation.slotOrdinals} (the engine contract's\n * \"not in any cluster\" value, mirroring the NaN-position convention).\n * - Missing force centers generate deterministically from the ORDERED cluster\n * keys plus the layout seed. {@link generateClusterCenters} uses\n * only IEEE-754 correctly-rounded operations (+ - * / and integer ops) — no\n * transcendentals — so the same ordered keys and seed produce BIT-IDENTICAL\n * centers on any conforming engine, and a different seed produces different\n * ones.\n * - Overlay anchoring must never scan members per frame:\n * {@link clusterCentroids} is the ONLY member-scanning anchor path and runs\n * at settle (from the single permitted readback) or at commit under a\n * fixed layout. {@link clusterProbe} counts every member visit so tests can\n * pin \"zero per-frame member iterations\".\n */\n\nimport type { GraphDiagnostic, GraphNode, NodeId } from './types';\nimport { DIAGNOSTIC_SAMPLE_CAP } from './types';\n\n// ---------------------------------------------------------------------------\n// Instrumentation.\n// ---------------------------------------------------------------------------\n\n/**\n * Cluster-work counters. `memberVisits` counts EVERY per-member iteration the\n * cluster lane performs (derivation scans and centroid scans — the only two\n * O(members) passes); `derivations` counts stage-4 recomputations. Both are\n * unconditional O(1) increments so the instrumented and production paths are\n * identical code. Tests snapshot, act, and compare: a per-frame overlay tick\n * must add ZERO member visits and a stage-5 soft-mask change must\n * add ZERO derivations.\n */\nexport const clusterProbe = {\n derivations: 0,\n memberVisits: 0,\n};\n\n/** Reset both counters (per-test isolation). */\nexport function resetClusterProbe(): void {\n clusterProbe.derivations = 0;\n clusterProbe.memberVisits = 0;\n}\n\n// ---------------------------------------------------------------------------\n// Deterministic force-center generation.\n// ---------------------------------------------------------------------------\n\n/**\n * The layout seed generated cluster centers key on.\n *\n * FINDING (investigation, recorded here because it is a contract\n * decision): the core owns NO numeric layout seed in v0.10. The only\n * layout-seeding knob on the config contract is\n * `EngineConfigUpdate.seedRadius` (the ring radius unknown/NaN positions seed\n * onto) and `instance.ts` never sets it — every adapter defaults it itself\n * (`CosmosEngine` → `spaceSize / 4`). `LayoutSpec.seed` (the\n * `static` layout's reproducibility seed) is not implemented in v0.10 either.\n * So this constant is the single named place the instance keys generated\n * centers on until normalized layout object lands and can supply\n * `LayoutSpec.seed` here.\n */\nexport const DEFAULT_LAYOUT_SEED = 0x5eed_1234;\n\n/**\n * Radius of the square region generated centers spread over, in SPACE\n * coordinates. Mirrors the role of `EngineConfigUpdate.seedRadius` (see\n * {@link DEFAULT_LAYOUT_SEED}) and matches `CosmosEngine`'s own default\n * (`spaceSize / 4` = 4096 / 4).\n */\nexport const DEFAULT_CLUSTER_CENTER_RADIUS = 1024;\n\n/** FNV-1a over the key mixed with the seed, finished with murmur3's fmix32. */\nfunction hash32(key: string, seed: number): number {\n let h = (0x811c9dc5 ^ (seed | 0)) >>> 0;\n for (let i = 0; i < key.length; i++) {\n h = (h ^ key.charCodeAt(i)) >>> 0;\n h = Math.imul(h, 0x01000193) >>> 0;\n }\n h = (h ^ (h >>> 16)) >>> 0;\n h = Math.imul(h, 0x85ebca6b) >>> 0;\n h = (h ^ (h >>> 13)) >>> 0;\n h = Math.imul(h, 0xc2b2ae35) >>> 0;\n return (h ^ (h >>> 16)) >>> 0;\n}\n\n/** 2^-32 — exact in binary64, so `hash * UNIT` is exact. */\nconst UNIT = 2.3283064365386963e-10;\n\n/** van der Corput radical inverse — integer ops plus one division per digit,\n * all correctly rounded, so the result is reproducible bit-for-bit. */\nfunction radicalInverse(index: number, base: number): number {\n let result = 0;\n let denominator = 1;\n let n = index;\n while (n > 0) {\n denominator *= base;\n result += (n % base) / denominator;\n n = Math.floor(n / base);\n }\n return result;\n}\n\nfunction frac(v: number): number {\n return v - Math.floor(v);\n}\n\n/**\n * Deterministic force centers for ORDERED cluster keys: a\n * low-discrepancy (Halton base 2/3) spread offset per key by a seeded hash,\n * mapped into `[-radius, radius]²`.\n *\n * Pure and total: same ordered keys + same seed ⇒ bit-identical output (only\n * IEEE-754 correctly-rounded arithmetic is used — no `Math.sin/cos`, whose\n * results are implementation-defined); a different seed ⇒ different output;\n * reordering or renaming keys ⇒ different output (both the ordinal and the\n * key text feed the placement).\n */\nexport function generateClusterCenters(\n keys: readonly string[],\n seed: number = DEFAULT_LAYOUT_SEED,\n radius: number = DEFAULT_CLUSTER_CENTER_RADIUS,\n): Float32Array {\n const out = new Float32Array(2 * keys.length);\n for (let i = 0; i < keys.length; i++) {\n const key = keys[i]!;\n const u = frac(radicalInverse(i + 1, 2) + hash32(key, seed) * UNIT);\n const v = frac(radicalInverse(i + 1, 3) + hash32(key, (seed ^ 0x9e3779b9) | 0) * UNIT);\n out[2 * i] = radius * (2 * u - 1);\n out[2 * i + 1] = radius * (2 * v - 1);\n }\n return out;\n}\n\n/**\n * Force centers for ordered keys with caller-supplied entries winning:\n * ONLY the missing keys generate. A non-finite explicit pair is\n * treated as missing (D4 boundary hygiene — a NaN center would poison the\n * engine's force field).\n */\nexport function resolveClusterCenters(\n keys: readonly string[],\n explicit: ReadonlyMap<string, readonly [number, number]> | undefined,\n seed: number = DEFAULT_LAYOUT_SEED,\n radius: number = DEFAULT_CLUSTER_CENTER_RADIUS,\n): Float32Array {\n const out = generateClusterCenters(keys, seed, radius);\n if (explicit === undefined || explicit.size === 0) return out;\n for (let i = 0; i < keys.length; i++) {\n const pair = explicit.get(keys[i]!);\n if (pair === undefined) continue;\n const x = pair[0];\n const y = pair[1];\n if (!Number.isFinite(x) || !Number.isFinite(y)) continue;\n out[2 * i] = x;\n out[2 * i + 1] = y;\n }\n return out;\n}\n\n// ---------------------------------------------------------------------------\n// Derivation.\n// ---------------------------------------------------------------------------\n\nexport interface ClusterDerivation {\n /** Distinct keys in FIRST-ENCOUNTER order over the physical scene (the\n * categorical-domain convention, shared with groupBy). Ordinal i ↔ keys[i]. */\n keys: readonly string[];\n /** key → ordinal (the reverse of `keys`). */\n ordinalByKey: ReadonlyMap<string, number>;\n /** key → member PHYSICAL node ids, scene order. Public ids only. */\n membersByKey: ReadonlyMap<string, readonly NodeId[]>;\n /**\n * PHYSICAL slot → cluster ordinal, NaN = unclustered. Length is the caller's\n * `sceneCount`, so synthetic suffix slots are\n * always NaN: aggregates are never members of a cluster. This IS the engine\n * `config.cluster.pointClusters` payload — no copy at the sink.\n */\n slotOrdinals: Float32Array;\n /** ONE aggregated 'accessor-error' warning when `by` threw (affected nodes\n * derive as unclustered — never silent loss, I3), else null. */\n diagnostic: GraphDiagnostic | null;\n}\n\n/**\n * stage-4 derivation over the PHYSICAL prefix of the current scene.\n *\n * `nodes` are the physical rows (post-group-rewrite when a rewrite is live)\n * aligned to scene slots `0..nodes.length-1`; `sceneCount` is the FULL scene\n * point count so the returned `slotOrdinals` covers the synthetic suffix too.\n * Preserves everything and synthesizes nothing.\n */\nexport function deriveClusters<N>(\n nodes: readonly GraphNode<N>[],\n by: (node: GraphNode<N>) => string | null,\n sceneCount: number = nodes.length,\n): ClusterDerivation {\n clusterProbe.derivations++;\n const keys: string[] = [];\n const ordinalByKey = new Map<string, number>();\n const membersByKey = new Map<string, NodeId[]>();\n const slotOrdinals = new Float32Array(Math.max(sceneCount, nodes.length)).fill(NaN);\n let errorCount = 0;\n const errorSamples: string[] = [];\n\n for (let i = 0; i < nodes.length; i++) {\n clusterProbe.memberVisits++;\n const node = nodes[i]!;\n let raw: string | null;\n try {\n raw = by(node);\n } catch {\n errorCount++;\n if (errorSamples.length < DIAGNOSTIC_SAMPLE_CAP) errorSamples.push(node.id);\n continue;\n }\n if (typeof raw !== 'string') continue; // null / non-string ⇒ unclustered\n let ordinal = ordinalByKey.get(raw);\n if (ordinal === undefined) {\n ordinal = keys.length;\n keys.push(raw);\n ordinalByKey.set(raw, ordinal);\n membersByKey.set(raw, []);\n }\n membersByKey.get(raw)!.push(node.id);\n slotOrdinals[i] = ordinal;\n }\n\n return {\n keys,\n ordinalByKey,\n membersByKey,\n slotOrdinals,\n diagnostic:\n errorCount === 0\n ? null\n : {\n code: 'accessor-error',\n severity: 'warning',\n count: errorCount,\n sampleIds: errorSamples,\n message: 'clusters.by threw; the affected nodes derived as unclustered',\n },\n };\n}\n\n/**\n * Centroids of every cluster from a slot-aligned position buffer — the ONLY\n * member-scanning anchor path. Called at settle (over the single\n * permitted readback) and at commit under a FIXED layout; never per\n * frame. Slots with unknown (NaN) positions are skipped; a cluster with no\n * placeable member keeps its `fallback` entry (its force center), so a label\n * never jumps to the origin.\n */\nexport function clusterCentroids(\n slotOrdinals: Float32Array,\n positions: Float32Array,\n clusterCount: number,\n fallback: Float32Array,\n): Float32Array {\n const out = new Float32Array(2 * clusterCount);\n const counts = new Float64Array(clusterCount);\n const slots = Math.min(slotOrdinals.length, Math.floor(positions.length / 2));\n for (let i = 0; i < slots; i++) {\n const ordinal = slotOrdinals[i]!;\n if (Number.isNaN(ordinal)) continue;\n clusterProbe.memberVisits++;\n const x = positions[2 * i]!;\n const y = positions[2 * i + 1]!;\n if (Number.isNaN(x) || Number.isNaN(y)) continue;\n out[2 * ordinal] = out[2 * ordinal]! + x;\n out[2 * ordinal + 1] = out[2 * ordinal + 1]! + y;\n counts[ordinal] = counts[ordinal]! + 1;\n }\n for (let c = 0; c < clusterCount; c++) {\n const n = counts[c]!;\n if (n === 0) {\n out[2 * c] = fallback[2 * c] ?? 0;\n out[2 * c + 1] = fallback[2 * c + 1] ?? 0;\n continue;\n }\n out[2 * c] = out[2 * c]! / n;\n out[2 * c + 1] = out[2 * c + 1]! / n;\n }\n return out;\n}\n","/**\n * Worker-lane wire protocol: envelope codec, epoch guards,\n * consolidated transfer lists, and request-class bookkeeping\n * (guaranteed-vs-throttled). Pure functions and one small class; no Worker\n * construction here — the runtime that OWNS a thread imports this, and the\n * parity suite drives the same codec in-process (the codec is the unit\n * under test; a live thread adds scheduling, not semantics).\n *\n * Contract invariants (each pinned by test):\n * - `msgId` is monotonic PER DIRECTION; a reply names the request it answers\n * via `inReplyTo`.\n * - Every envelope carries the model acceptance `epoch` it derived from\n * (I1 discipline: references do not cross threads — epochs replace them).\n * Results for a superseded epoch are dropped AT THE BOUNDARY, before the\n * acceptance queue ever sees them.\n * - Transfers are CONSOLIDATED: one ArrayBuffer per channel per message,\n * deduplicated (two views over one buffer transfer once).\n * - Request classes: 'guaranteed' requests all complete (structural\n * derivation); 'throttled' requests coalesce LATEST-WINS per lane key\n * (styling reprojection) — superseding a pending throttled request aborts\n * the old one.\n */\n\nexport type WorkerEntity = 'nodes' | 'edges' | 'scene';\n\nexport interface WorkerEnvelope {\n msgId: number;\n /** The request this envelope answers (results/errors only). */\n inReplyTo?: number;\n /** Model acceptance epoch the payload derives from. */\n epoch: number;\n entity: WorkerEntity;\n op: string;\n payload: unknown;\n}\n\n/** One direction of the channel: monotonic ids + epoch stamping. */\nexport class EnvelopeSequencer {\n private nextId = 1;\n\n make(\n epoch: number,\n entity: WorkerEntity,\n op: string,\n payload: unknown,\n inReplyTo?: number,\n ): WorkerEnvelope {\n const envelope: WorkerEnvelope = { msgId: this.nextId, epoch, entity, op, payload };\n this.nextId += 1;\n if (inReplyTo !== undefined) envelope.inReplyTo = inReplyTo;\n return envelope;\n }\n}\n\n/**\n * Boundary guard: does an arriving envelope still apply? Stale epochs are\n * dropped silently (superseded work is EXPECTED under latest-wins, not an\n * error); a FUTURE epoch is a protocol violation (the other side cannot\n * know an epoch this side has not yet issued).\n */\nexport type EpochVerdict = 'accept' | 'stale' | 'protocol-violation';\n\nexport function judgeEpoch(envelope: WorkerEnvelope, currentEpoch: number): EpochVerdict {\n if (envelope.epoch === currentEpoch) return 'accept';\n if (envelope.epoch < currentEpoch) return 'stale';\n return 'protocol-violation';\n}\n\n/**\n * Consolidated transfer list: every DISTINCT underlying ArrayBuffer behind\n * the given views, in first-seen order. Two views over one buffer yield one\n * entry (transferring twice throws in every engine). SharedArrayBuffer is\n * excluded by construction — the D3 contract never requires shared memory.\n */\nexport function collectTransfers(views: readonly ArrayBufferView[]): ArrayBuffer[] {\n const seen = new Set<ArrayBuffer>();\n const out: ArrayBuffer[] = [];\n for (const view of views) {\n const buffer = view.buffer;\n if (!(buffer instanceof ArrayBuffer)) continue; // SAB stays shared\n if (seen.has(buffer)) continue;\n seen.add(buffer);\n out.push(buffer);\n }\n return out;\n}\n\n/** Request classes (Mosaic split): 'guaranteed' all complete; 'throttled'\n * coalesces latest-wins per lane. */\nexport type RequestClass = 'guaranteed' | 'throttled';\n\ninterface PendingRequest {\n envelope: WorkerEnvelope;\n klass: RequestClass;\n /** Lane key for throttled coalescing (e.g. 'project:pointColor'). */\n lane: string;\n controller: AbortController;\n}\n\n/**\n * Main-side request ledger. Owns AbortControllers and the latest-wins rule;\n * transport (postMessage or the in-process double) is injected by the\n * caller, so the ledger is testable without a thread.\n */\nexport class RequestLedger {\n private readonly pending = new Map<number, PendingRequest>();\n\n /** Register an outbound request. A throttled request SUPERSEDES any\n * pending request on the same lane: the old one is aborted and forgotten\n * (its eventual reply will be dropped as unmatched). Returns the signal\n * the transport should honor. */\n track(envelope: WorkerEnvelope, klass: RequestClass, lane: string): AbortSignal {\n if (klass === 'throttled') {\n for (const [id, entry] of this.pending) {\n if (entry.klass === 'throttled' && entry.lane === lane) {\n entry.controller.abort();\n this.pending.delete(id);\n }\n }\n }\n const controller = new AbortController();\n this.pending.set(envelope.msgId, { envelope, klass, lane, controller });\n return controller.signal;\n }\n\n /** Match an arriving reply to its request. Returns the original request\n * envelope, or null when the request was superseded/aborted (drop the\n * reply — it answers work nobody wants anymore). */\n settle(reply: WorkerEnvelope): WorkerEnvelope | null {\n if (reply.inReplyTo === undefined) return null;\n const entry = this.pending.get(reply.inReplyTo);\n if (entry === undefined) return null;\n this.pending.delete(reply.inReplyTo);\n if (entry.controller.signal.aborted) return null;\n return entry.envelope;\n }\n\n /** Abort EVERYTHING (epoch advance / detach / dataset swap). */\n abortAll(): number {\n let aborted = 0;\n for (const entry of this.pending.values()) {\n entry.controller.abort();\n aborted += 1;\n }\n this.pending.clear();\n return aborted;\n }\n\n pendingCount(): number {\n return this.pending.size;\n }\n}\n\n/**\n * UTF-8 string tables — dictionaries cross the boundary as TRANSFERABLES,\n * never as structured-clone string arrays (cloning 1M strings serializes on\n * the SENDING thread — the exact main-thread tax this lane exists to\n * remove; the mapbox pattern). Layout: byte offsets (Uint32Array, length\n * n+1) + concatenated UTF-8 bytes.\n */\nexport interface EncodedStringTable {\n offsets: Uint32Array;\n bytes: Uint8Array;\n}\n\nexport function encodeStringTable(strings: readonly string[]): EncodedStringTable {\n const encoder = new TextEncoder();\n const chunks: Uint8Array[] = new Array(strings.length);\n const offsets = new Uint32Array(strings.length + 1);\n let total = 0;\n for (let i = 0; i < strings.length; i++) {\n const chunk = encoder.encode(strings[i]!);\n chunks[i] = chunk;\n total += chunk.length;\n offsets[i + 1] = total;\n }\n const bytes = new Uint8Array(total);\n for (let i = 0; i < strings.length; i++) bytes.set(chunks[i]!, offsets[i]!);\n return { offsets, bytes };\n}\n\nexport function decodeStringTable(table: EncodedStringTable): string[] {\n const decoder = new TextDecoder();\n const n = table.offsets.length - 1;\n const out: string[] = new Array(n);\n for (let i = 0; i < n; i++) {\n out[i] = decoder.decode(table.bytes.subarray(table.offsets[i]!, table.offsets[i + 1]!));\n }\n return out;\n}\n\n/**\n * Structural envelope check for the RECEIVING side — a malformed message is\n * a protocol violation, never an exception path (the worker boundary is a\n * trust boundary within one page, but versions can skew during upgrades).\n */\nexport function isWellFormedEnvelope(value: unknown): value is WorkerEnvelope {\n if (value === null || typeof value !== 'object') return false;\n const e = value as Partial<WorkerEnvelope>;\n return (\n typeof e.msgId === 'number' &&\n Number.isInteger(e.msgId) &&\n e.msgId > 0 &&\n typeof e.epoch === 'number' &&\n Number.isInteger(e.epoch) &&\n e.epoch >= 0 &&\n (e.entity === 'nodes' || e.entity === 'edges' || e.entity === 'scene') &&\n typeof e.op === 'string' &&\n e.op.length > 0 &&\n (e.inReplyTo === undefined || (Number.isInteger(e.inReplyTo) && (e.inReplyTo as number) > 0))\n );\n}\n","/**\n * Columnar-native acceptance rules — the column-oriented twin of validate.ts,\n * built to run INSIDE the worker over typed\n * columns without materializing a single row object.\n *\n * Semantics mirror the object lane EXACTLY (the equivalence oracle pins\n * rosters AND diagnostics, message strings included):\n * - duplicate node ids drop, first occurrence wins ('duplicate-node-id',\n * warning) — and edges addressing a dropped duplicate ROW remap to the\n * surviving occurrence, because the object lane resolves endpoints by ID\n * STRING, which survives.\n * - duplicate edge ids drop, first wins ('duplicate-edge-id', warning).\n * - self-loops are RETAINED with 'self-loop-retained' (info).\n * - invalid-node / invalid-edge rows with NUL-reserved ids drop here;\n * dangling edges can then arise when an endpoint names a dropped invalid\n * node. Other structural corruption cannot occur because ids come from a\n * structurally validated dictionary column and endpoints are in-bounds\n * indices by prior validation (validateColumnarStructure).\n *\n * Duplicates hide in TWO encodings: two rows sharing a code, and two\n * DISTINCT dictionary entries holding equal strings. Both are handled by\n * canonicalizing the dictionary first (O(dictionary)), then scanning rows\n * with an integer seen-set (O(rows)) — no per-row string work.\n *\n * Worker-safe by construction: pure over typed arrays; no DOM, no instance\n * state, no Date/random.\n */\n\nimport { DIAGNOSTIC_SAMPLE_CAP } from './types';\nimport type { ColumnarGraphSnapshot, GraphDiagnostic, StringColumn } from './types';\n\nexport interface ColumnarAcceptance {\n /** 1 = the ORIGINAL row survives into the accepted roster. */\n keepNodes: Uint8Array;\n keepEdges: Uint8Array;\n acceptedNodeCount: number;\n acceptedEdgeCount: number;\n /** ORIGINAL node row → accepted index of its SURVIVING id (a dropped\n * duplicate row points at the first occurrence's accepted index). */\n nodeAcceptedIndex: Int32Array;\n /** Resolved links (2 × acceptedEdgeCount), endpoints in ACCEPTED node\n * indices, remapped through surviving occurrences. */\n links: Uint32Array;\n /** Batched, object-lane-identical diagnostics (codes, counts, capped\n * samples, message strings). */\n diagnostics: GraphDiagnostic[];\n}\n\n/** dictionary index → canonical (first) dictionary index for equal strings. */\nfunction canonicalizeDictionary(dictionary: readonly string[]): Uint32Array {\n const canonical = new Uint32Array(dictionary.length);\n const firstByString = new Map<string, number>();\n for (let d = 0; d < dictionary.length; d++) {\n const existing = firstByString.get(dictionary[d]!);\n if (existing === undefined) {\n firstByString.set(dictionary[d]!, d);\n canonical[d] = d;\n } else {\n canonical[d] = existing;\n }\n }\n return canonical;\n}\n\ninterface Tally {\n count: number;\n samples: string[];\n}\n\nfunction record(tally: Tally, sample: string): void {\n tally.count++;\n if (tally.samples.length < DIAGNOSTIC_SAMPLE_CAP) tally.samples.push(sample);\n}\n\nfunction pushDiagnostic(\n out: GraphDiagnostic[],\n code: GraphDiagnostic['code'],\n severity: GraphDiagnostic['severity'],\n tally: Tally,\n message: string,\n): void {\n if (tally.count === 0) return;\n out.push({ code, severity, count: tally.count, sampleIds: tally.samples, message });\n}\n\n/**\n * Run the acceptance rules over a STRUCTURALLY VALID columnar snapshot\n * (validateColumnarStructure returned no issues — lengths and bounds are\n * trusted here).\n */\nexport function acceptColumnar(\n snapshot: ColumnarGraphSnapshot<unknown, unknown>,\n): ColumnarAcceptance {\n const nodeIds: StringColumn = snapshot.nodes.ids;\n const edgeIds: StringColumn = snapshot.edges.ids;\n const nodeRows = snapshot.nodes.length;\n const edgeRows = snapshot.edges.length;\n\n const duplicateNode: Tally = { count: 0, samples: [] };\n const invalidEdge: Tally = { count: 0, samples: [] };\n const duplicateEdge: Tally = { count: 0, samples: [] };\n const selfLoop: Tally = { count: 0, samples: [] };\n const invalidNode: Tally = { count: 0, samples: [] };\n const danglingEdge: Tally = { count: 0, samples: [] };\n\n // --- Nodes: first occurrence per canonical id wins. -----------------------\n const nodeCanonical = canonicalizeDictionary(nodeIds.dictionary);\n // As in validate.ts, NUL-containing ids collide with\n // the internal scene-key codec — their ROWS drop as invalid-node. One\n // O(dictionary) scan marks the offending entries.\n const nulDict = new Uint8Array(nodeIds.dictionary.length);\n for (let d = 0; d < nodeIds.dictionary.length; d++) {\n if (nodeIds.dictionary[d]!.includes('\\u0000')) nulDict[d] = 1;\n }\n const keepNodes = new Uint8Array(nodeRows);\n const nodeAcceptedIndex = new Int32Array(nodeRows).fill(-1);\n // canonical dictionary index → accepted index of the surviving row\n // (-1 = unseen). Sized to the dictionary, integer-indexed — O(1) per row.\n const acceptedByCanonical = new Int32Array(nodeIds.dictionary.length).fill(-1);\n let acceptedNodeCount = 0;\n for (let i = 0; i < nodeRows; i++) {\n if (nulDict[nodeIds.codes[i]!] !== 0) {\n record(invalidNode, `[${i}]`);\n continue; // nodeAcceptedIndex stays -1: edges to this row will drop\n }\n const canonical = nodeCanonical[nodeIds.codes[i]!]!;\n const survivor = acceptedByCanonical[canonical]!;\n if (survivor === -1) {\n acceptedByCanonical[canonical] = acceptedNodeCount;\n nodeAcceptedIndex[i] = acceptedNodeCount;\n keepNodes[i] = 1;\n acceptedNodeCount += 1;\n } else {\n // Dropped duplicate ROW — its id string survives at the first\n // occurrence, so edges addressing this row remap there.\n nodeAcceptedIndex[i] = survivor;\n record(duplicateNode, nodeIds.dictionary[nodeIds.codes[i]!]!);\n }\n }\n\n // --- Edges: first occurrence per canonical edge id wins; self-loops kept. -\n const edgeCanonical = canonicalizeDictionary(edgeIds.dictionary);\n const nulEdgeDict = new Uint8Array(edgeIds.dictionary.length);\n for (let d = 0; d < edgeIds.dictionary.length; d++) {\n if (edgeIds.dictionary[d]!.includes('\\u0000')) nulEdgeDict[d] = 1;\n }\n const keepEdges = new Uint8Array(edgeRows);\n const seenEdgeByCanonical = new Uint8Array(edgeIds.dictionary.length);\n const { source, target } = snapshot.edges;\n const linksOut = new Uint32Array(edgeRows * 2); // trimmed after the scan\n let acceptedEdgeCount = 0;\n for (let e = 0; e < edgeRows; e++) {\n if (nulEdgeDict[edgeIds.codes[e]!] !== 0) {\n record(invalidEdge, `[${e}]`);\n continue;\n }\n const canonical = edgeCanonical[edgeIds.codes[e]!]!;\n const s = nodeAcceptedIndex[source[e]!]!;\n const t = nodeAcceptedIndex[target[e]!]!;\n if (s === -1 || t === -1) {\n // Object-lane mirror: an endpoint whose row was invalid records the\n // ENDPOINT ID STRING, exactly like validate.ts's dangling tally.\n record(\n danglingEdge,\n nodeIds.dictionary[nodeIds.codes[(s === -1 ? source : target)[e]!]!]!,\n );\n continue;\n }\n // Object-lane order is endpoint admission BEFORE final-id dedupe: a\n // dangling record never occupies its edge id, so a later endpoint-valid\n // record with the same id remains eligible. NUL-invalid node rows make\n // that distinction observable in the otherwise structurally sound\n // columnar lane.\n if (seenEdgeByCanonical[canonical] !== 0) {\n record(duplicateEdge, edgeIds.dictionary[edgeIds.codes[e]!]!);\n continue;\n }\n seenEdgeByCanonical[canonical] = 1;\n if (s === t) {\n // Same ACCEPTED node = same id string (the object lane compares\n // source/target strings) — retained, reported.\n record(selfLoop, nodeIds.dictionary[nodeIds.codes[source[e]!]!]!);\n }\n keepEdges[e] = 1;\n linksOut[acceptedEdgeCount * 2] = s;\n linksOut[acceptedEdgeCount * 2 + 1] = t;\n acceptedEdgeCount += 1;\n }\n\n // Message strings MATCH validate.ts verbatim — the parity oracle compares\n // diagnostics whole.\n const diagnostics: GraphDiagnostic[] = [];\n pushDiagnostic(\n diagnostics,\n 'invalid-node',\n 'error',\n invalidNode,\n `${invalidNode.count} node row(s) dropped: missing, non-string, or NUL-containing id`,\n );\n pushDiagnostic(\n diagnostics,\n 'duplicate-node-id',\n 'warning',\n duplicateNode,\n `${duplicateNode.count} duplicate node id(s) dropped (first occurrence wins)`,\n );\n pushDiagnostic(\n diagnostics,\n 'invalid-edge',\n 'error',\n invalidEdge,\n `${invalidEdge.count} edge row(s) dropped: missing or non-string source/target, or NUL-containing explicit id`,\n );\n pushDiagnostic(\n diagnostics,\n 'dangling-edge-endpoint',\n 'warning',\n danglingEdge,\n `${danglingEdge.count} edge(s) dropped: endpoint not in accepted node set`,\n );\n pushDiagnostic(\n diagnostics,\n 'duplicate-edge-id',\n 'warning',\n duplicateEdge,\n `${duplicateEdge.count} duplicate edge id(s) dropped (first occurrence wins)`,\n );\n pushDiagnostic(\n diagnostics,\n 'self-loop-retained',\n 'info',\n selfLoop,\n `${selfLoop.count} self-loop edge(s) retained`,\n );\n\n return {\n keepNodes,\n keepEdges,\n acceptedNodeCount,\n acceptedEdgeCount,\n nodeAcceptedIndex,\n links: linksOut.subarray(0, acceptedEdgeCount * 2).slice(),\n diagnostics,\n };\n}\n"]}
package/dist/engine.d.ts CHANGED
@@ -1 +1 @@
1
- export { aM as BufferPatch, a4 as EngineBufferChannel, a7 as EngineCapabilities, a6 as EngineCommit, aN as EngineConfigUpdate, aO as EngineContextEvent, aL as EngineDiagnostic, E as EngineFactory, aJ as EngineHostEvents, aK as FitViewOptions, a5 as GraphEngine } from './index-CPRTWpGA.js';
1
+ export { aQ as BufferPatch, a4 as EngineBufferChannel, a7 as EngineCapabilities, a6 as EngineCommit, aR as EngineConfigUpdate, aS as EngineContextEvent, aP as EngineDiagnostic, E as EngineFactory, aN as EngineHostEvents, aO as FitViewOptions, a5 as GraphEngine } from './index-BoX8Q5jn.js';
@@ -334,6 +334,26 @@ interface SimulationConfig {
334
334
  /** How strongly nodes shy away from the cursor. Default 2. */
335
335
  repulsionFromMouse?: number;
336
336
  }
337
+ /**
338
+ * Named simulation presets, measured on the reference protocol (800-node
339
+ * clustered graph, max node displacement sampled at 500ms): the times below
340
+ * are seconds until visible stillness (< 1.5 space units/s sustained).
341
+ *
342
+ * - `calm` — damped and settles in ~5s; the DEFAULT when no `simulation`
343
+ * value is given. The engine's own defaults (`lively`) keep visible motion
344
+ * alive for tens of seconds, which reads as jitter on first load.
345
+ * - `spread` — airier inter-cluster spacing, ~7s to stillness.
346
+ * - `tight` — compact clusters, ~6s to stillness.
347
+ * - `lively` — the engine's own defaults: ambient continuous motion.
348
+ */
349
+ type SimulationPreset = 'calm' | 'spread' | 'tight' | 'lively';
350
+ /** The `simulation` input surface: a full config or a named preset. */
351
+ type SimulationInput = SimulationConfig | SimulationPreset;
352
+ declare const SIMULATION_PRESETS: Readonly<Record<SimulationPreset, Readonly<SimulationConfig>>>;
353
+ /** Resolve a `simulation` input to a concrete config. Omitted input resolves
354
+ * to the `calm` preset — the measured-good default. Preset strings resolve to
355
+ * frozen singletons, so identity comparison stays meaningful. */
356
+ declare function resolveSimulation(input: SimulationInput | undefined): SimulationConfig;
337
357
  type ColumnChange = {
338
358
  /** Unchanged revision permits index/cache reuse (assertion, not a hint). */
339
359
  revision?: string | number;
@@ -429,7 +449,7 @@ interface GraphHostUpdate<N = Record<string, unknown>, E = Record<string, unknow
429
449
  * suppresses every driver (hover, focusNode, emphasizeNode). */
430
450
  emphasisRing?: boolean;
431
451
  layout?: LayoutKind;
432
- simulation?: SimulationConfig;
452
+ simulation?: SimulationInput;
433
453
  /** Controlled selection (uncontrolled when never provided; subset). */
434
454
  selection?: readonly NodeId[];
435
455
  theme?: ThemeInput;
@@ -1353,6 +1373,13 @@ interface EngineHostEvents {
1353
1373
  interface FitViewOptions {
1354
1374
  durationMs?: number;
1355
1375
  padding?: number;
1376
+ /**
1377
+ * Upper bound on the zoom the fit may land at. Small scenes otherwise fit
1378
+ * until nodes balloon (measured: a 60-node graph fits at zoom 4.3). When
1379
+ * the natural fit zoom exceeds the bound, the engine centers the scene at
1380
+ * `maxZoom` instead. Omitted = unclamped.
1381
+ */
1382
+ maxZoom?: number;
1356
1383
  }
1357
1384
  interface GraphEngine {
1358
1385
  readonly capabilities: EngineCapabilities;
@@ -1392,4 +1419,4 @@ interface GraphEngine {
1392
1419
  /** Engine factory the host passes in; called once per instance mount. */
1393
1420
  type EngineFactory = () => GraphEngine;
1394
1421
 
1395
- export { type FilterExpr as $, type AcceptedGraph as A, type BeginIngestOptions as B, type PathOptions as C, type PathResult as D, type EngineFactory as E, type SearchActivation as F, type GraphSnapshot as G, type CrossfilterSession as H, type IngestSession as I, type JsonValue as J, type Scale as K, type LabelPlacement as L, type MetaEdge as M, type NodeId as N, type MetricName as O, type PathService as P, type Revisions as Q, type ResolvedGroup as R, type SceneFold as S, type TimelinePlayback as T, type AccessibilityConfig as U, type ViewportState as V, type ThemeInput as W, type LabelConfig as X, type IngestBatch as Y, type RevisionDimension as Z, type FilterSpec as _, type GraphDiagnostic as a, type DimensionSpec as a0, type BrushState as a1, type DimensionSummary as a2, type DomainPolicy as a3, type EngineBufferChannel as a4, type GraphEngine as a5, type EngineCommit as a6, type EngineCapabilities as a7, type MetricColumn as a8, type ColumnarGraphSnapshot as a9, OrbitOperationError as aA, type ResourceAdmissionReport as aB, type ResourceDegradeStep as aC, type SearchUnavailableReason as aD, type SimulationConfig as aE, type StringColumn as aF, graphErrorToError as aG, isFatalGraphError as aH, resourceLimitFatal as aI, type EngineHostEvents as aJ, type FitViewOptions as aK, type EngineDiagnostic as aL, type BufferPatch as aM, type EngineConfigUpdate as aN, type EngineContextEvent as aO, type GraphSnapshotInput as aa, type AppendReceipt as ab, type CategoryBin as ac, type ClusterSpec as ad, type Column as ae, type ColumnChange as af, DIAGNOSTIC_SAMPLE_CAP as ag, type DegradeEvent as ah, type DegradeStep as ai, type DiagnosticCode as aj, type DiagnosticSeverity as ak, type DimensionKind as al, type ErrorPhase as am, type ExpansionBatch as an, type ExpansionResponse as ao, type FilterMode as ap, type FilterValue as aq, type GraphEdge as ar, type GraphError as as, type GraphOperationError as at, type HistogramBin as au, type IngestCommitReceipt as av, type IngestSessionState as aw, type InstanceStatus as ax, type LayoutKind as ay, type NodeEventPayload 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 ScaleLimits as q, type GraphTheme as r, type GraphStoreState as s, type GraphHostUpdate as t, type GraphEventName as u, type GraphEventMap as v, type GraphListenerControl as w, type EdgeId as x, type GraphPerfSnapshot as y, type ResolvedCluster as z };
1422
+ export { type FilterExpr as $, type AcceptedGraph as A, type BeginIngestOptions as B, type PathOptions as C, type PathResult as D, type EngineFactory as E, type SearchActivation as F, type GraphSnapshot as G, type CrossfilterSession as H, type IngestSession as I, type JsonValue as J, type Scale as K, type LabelPlacement as L, type MetaEdge as M, type NodeId as N, type MetricName as O, type PathService as P, type Revisions as Q, type ResolvedGroup as R, type SceneFold as S, type TimelinePlayback as T, type AccessibilityConfig as U, type ViewportState as V, type ThemeInput as W, type LabelConfig as X, type IngestBatch as Y, type RevisionDimension as Z, type FilterSpec as _, type GraphDiagnostic as a, type DimensionSpec as a0, type BrushState as a1, type DimensionSummary as a2, type DomainPolicy as a3, type EngineBufferChannel as a4, type GraphEngine as a5, type EngineCommit as a6, type EngineCapabilities as a7, type MetricColumn as a8, type ColumnarGraphSnapshot as a9, OrbitOperationError as aA, type ResourceAdmissionReport as aB, type ResourceDegradeStep as aC, SIMULATION_PRESETS as aD, type SearchUnavailableReason as aE, type SimulationConfig as aF, type SimulationInput as aG, type SimulationPreset as aH, type StringColumn as aI, graphErrorToError as aJ, isFatalGraphError as aK, resolveSimulation as aL, resourceLimitFatal as aM, type EngineHostEvents as aN, type FitViewOptions as aO, type EngineDiagnostic as aP, type BufferPatch as aQ, type EngineConfigUpdate as aR, type EngineContextEvent as aS, type GraphSnapshotInput as aa, type AppendReceipt as ab, type CategoryBin as ac, type ClusterSpec as ad, type Column as ae, type ColumnChange as af, DIAGNOSTIC_SAMPLE_CAP as ag, type DegradeEvent as ah, type DegradeStep as ai, type DiagnosticCode as aj, type DiagnosticSeverity as ak, type DimensionKind as al, type ErrorPhase as am, type ExpansionBatch as an, type ExpansionResponse as ao, type FilterMode as ap, type FilterValue as aq, type GraphEdge as ar, type GraphError as as, type GraphOperationError as at, type HistogramBin as au, type IngestCommitReceipt as av, type IngestSessionState as aw, type InstanceStatus as ax, type LayoutKind as ay, type NodeEventPayload 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 ScaleLimits as q, type GraphTheme as r, type GraphStoreState as s, type GraphHostUpdate as t, type GraphEventName as u, type GraphEventMap as v, type GraphListenerControl as w, type EdgeId as x, type GraphPerfSnapshot as y, type ResolvedCluster as z };
package/dist/index.d.ts CHANGED
@@ -1,7 +1,7 @@
1
- import { G as GraphSnapshot, A as AcceptedGraph, R as ResolvedGroup, a as GraphDiagnostic, b as GraphNode, c as AcceptedEdge, N as NodeId, M as MetaEdge, S as SceneFold, d as GroupSpec, e as GroupBySpec, f as SceneGroups, g as RenderScene, h as SceneLinkRef, i as ScenePointRef, j as Accessor, k as RevisionAwareService, l as RequestContext, m as SearchResult, V as ViewportState, n as SelectionState, o as SubgraphSpec, J as JsonValue, E as EngineFactory, p as ExpansionService, P as PathService, q as ScaleLimits, r as GraphTheme, s as GraphStoreState, L as LabelPlacement, t as GraphHostUpdate, B as BeginIngestOptions, I as IngestSession, u as GraphEventName, v as GraphEventMap, w as GraphListenerControl, x as EdgeId, y as GraphPerfSnapshot, z as ResolvedCluster, C as PathOptions, D as PathResult, F as SearchActivation, H as CrossfilterSession, T as TimelinePlayback, K as Scale, O as MetricName, Q as Revisions, U as AccessibilityConfig, W as ThemeInput, X as LabelConfig, Y as IngestBatch, Z as RevisionDimension, _ as FilterSpec, $ as FilterExpr, a0 as DimensionSpec, a1 as BrushState, a2 as DimensionSummary, a3 as DomainPolicy, a4 as EngineBufferChannel, a5 as GraphEngine, a6 as EngineCommit, a7 as EngineCapabilities, a8 as MetricColumn, a9 as ColumnarGraphSnapshot, aa as GraphSnapshotInput } from './index-CPRTWpGA.js';
2
- export { ab as AppendReceipt, ac as CategoryBin, ad as ClusterSpec, ae as Column, af as ColumnChange, ag as DIAGNOSTIC_SAMPLE_CAP, ah as DegradeEvent, ai as DegradeStep, aj as DiagnosticCode, ak as DiagnosticSeverity, al as DimensionKind, am as ErrorPhase, an as ExpansionBatch, ao as ExpansionResponse, ap as FilterMode, aq as FilterValue, ar as GraphEdge, as as GraphError, at as GraphOperationError, au as HistogramBin, av as IngestCommitReceipt, aw as IngestSessionState, ax as InstanceStatus, ay as LayoutKind, az as NodeEventPayload, aA as OrbitOperationError, aB as ResourceAdmissionReport, aC as ResourceDegradeStep, aD as SearchUnavailableReason, aE as SimulationConfig, aF as StringColumn, aG as graphErrorToError, aH as isFatalGraphError, aI as resourceLimitFatal } from './index-CPRTWpGA.js';
3
- import { W as WorkerFactoryOption } from './lane-_KPQ1deI.js';
4
- export { C as ClusterDerivation, D as DEFAULT_CLUSTER_CENTER_RADIUS, a as DEFAULT_LAYOUT_SEED, E as EncodedStringTable, R as RequestClass, b as WorkerEntity, c as WorkerEnvelope, d as WorkerTransport, e as clusterCentroids, f as collectTransfers, g as decodeStringTable, h as deriveClusters, i as encodeStringTable, j as generateClusterCenters, k as judgeEpoch, r as resolveClusterCenters } from './lane-_KPQ1deI.js';
1
+ import { G as GraphSnapshot, A as AcceptedGraph, R as ResolvedGroup, a as GraphDiagnostic, b as GraphNode, c as AcceptedEdge, N as NodeId, M as MetaEdge, S as SceneFold, d as GroupSpec, e as GroupBySpec, f as SceneGroups, g as RenderScene, h as SceneLinkRef, i as ScenePointRef, j as Accessor, k as RevisionAwareService, l as RequestContext, m as SearchResult, V as ViewportState, n as SelectionState, o as SubgraphSpec, J as JsonValue, E as EngineFactory, p as ExpansionService, P as PathService, q as ScaleLimits, r as GraphTheme, s as GraphStoreState, L as LabelPlacement, t as GraphHostUpdate, B as BeginIngestOptions, I as IngestSession, u as GraphEventName, v as GraphEventMap, w as GraphListenerControl, x as EdgeId, y as GraphPerfSnapshot, z as ResolvedCluster, C as PathOptions, D as PathResult, F as SearchActivation, H as CrossfilterSession, T as TimelinePlayback, K as Scale, O as MetricName, Q as Revisions, U as AccessibilityConfig, W as ThemeInput, X as LabelConfig, Y as IngestBatch, Z as RevisionDimension, _ as FilterSpec, $ as FilterExpr, a0 as DimensionSpec, a1 as BrushState, a2 as DimensionSummary, a3 as DomainPolicy, a4 as EngineBufferChannel, a5 as GraphEngine, a6 as EngineCommit, a7 as EngineCapabilities, a8 as MetricColumn, a9 as ColumnarGraphSnapshot, aa as GraphSnapshotInput } from './index-BoX8Q5jn.js';
2
+ export { ab as AppendReceipt, ac as CategoryBin, ad as ClusterSpec, ae as Column, af as ColumnChange, ag as DIAGNOSTIC_SAMPLE_CAP, ah as DegradeEvent, ai as DegradeStep, aj as DiagnosticCode, ak as DiagnosticSeverity, al as DimensionKind, am as ErrorPhase, an as ExpansionBatch, ao as ExpansionResponse, ap as FilterMode, aq as FilterValue, ar as GraphEdge, as as GraphError, at as GraphOperationError, au as HistogramBin, av as IngestCommitReceipt, aw as IngestSessionState, ax as InstanceStatus, ay as LayoutKind, az as NodeEventPayload, aA as OrbitOperationError, aB as ResourceAdmissionReport, aC as ResourceDegradeStep, aD as SIMULATION_PRESETS, aE as SearchUnavailableReason, aF as SimulationConfig, aG as SimulationInput, aH as SimulationPreset, aI as StringColumn, aJ as graphErrorToError, aK as isFatalGraphError, aL as resolveSimulation, aM as resourceLimitFatal } from './index-BoX8Q5jn.js';
3
+ import { W as WorkerFactoryOption } from './lane-DKwMcUe3.js';
4
+ export { C as ClusterDerivation, D as DEFAULT_CLUSTER_CENTER_RADIUS, a as DEFAULT_LAYOUT_SEED, E as EncodedStringTable, R as RequestClass, b as WorkerEntity, c as WorkerEnvelope, d as WorkerTransport, e as clusterCentroids, f as collectTransfers, g as decodeStringTable, h as deriveClusters, i as encodeStringTable, j as generateClusterCenters, k as judgeEpoch, r as resolveClusterCenters } from './lane-DKwMcUe3.js';
5
5
  import { StoreApi } from 'zustand/vanilla';
6
6
 
7
7
  /**
@@ -603,23 +603,31 @@ type ViewBrushState = {
603
603
  interface ViewLayoutSpec {
604
604
  kind: 'force' | 'fixed';
605
605
  }
606
- /** Scale subset that is data by construction (no functions). */
607
- type SerializableScale = {
608
- kind: 'sequential' | 'diverging';
606
+ type SerializableScaleFor<T extends string | number> = {
607
+ kind: 'sequential';
608
+ metric: string;
609
+ range: readonly [T, T];
610
+ domain?: readonly [number, number];
611
+ } | {
612
+ kind: 'diverging';
609
613
  metric: string;
610
- range: readonly string[] | readonly number[];
611
- domain?: readonly number[];
612
- mid?: number;
614
+ range: readonly [T, T, T];
615
+ mid: number;
613
616
  } | {
614
617
  kind: 'categorical';
615
618
  /** Field-descriptor form only — a function `by` is omitted upstream. */
616
619
  by: string;
617
- palette?: readonly string[] | readonly number[];
620
+ palette?: readonly T[];
618
621
  domain?: readonly string[];
619
622
  };
623
+ /** Scale subset that is data by construction (no functions). `T` is bound
624
+ * by the styling channel: CSS strings for nodeColor, finite numbers for
625
+ * nodeSize. Tuple arity mirrors the runtime Scale<T> contract exactly. The
626
+ * conditional preserves homogeneous value collections when `T` is a union. */
627
+ type SerializableScale<T extends string | number = string | number> = T extends unknown ? SerializableScaleFor<T> : never;
620
628
  interface ViewStyling {
621
- nodeColor?: SerializableScale;
622
- nodeSize?: SerializableScale;
629
+ nodeColor?: SerializableScale<string>;
630
+ nodeSize?: SerializableScale<number>;
623
631
  showLinks?: boolean;
624
632
  edgeArrows?: boolean;
625
633
  /** Named themes only; a custom GraphTheme object is omitted upstream. */
@@ -777,6 +785,27 @@ interface CreateGraphInstanceOptions<N = Record<string, unknown>, E = Record<str
777
785
  engine: EngineFactory;
778
786
  /** Fit the camera once when the first data-bearing commit reaches a fresh engine. Default true. */
779
787
  fitViewOnFirstData?: boolean;
788
+ /**
789
+ * Upper bound on the zoom any internally issued fit may land at (first-data
790
+ * fit, settle follow, public fitView). Small graphs otherwise fit until
791
+ * nodes balloon — measured: a 60-node graph fits at zoom 4.3. Default 1.5;
792
+ * null disables the clamp.
793
+ */
794
+ fitViewMaxZoom?: number | null;
795
+ /**
796
+ * Camera behavior while the FIRST force-layout settle runs. The fit at
797
+ * first data frames the seed ring; the simulation then contracts the graph
798
+ * to a fraction of that frame (measured 5-17% viewport fill across the
799
+ * parameter space), so without a follow-up the graph reads as a distant
800
+ * blob.
801
+ * - 'follow' (default): periodic animated refits ride the engine frame
802
+ * fan-out while the first settle runs (no extra timers or rAF), with a
803
+ * final fit at quiescence. Any user camera input cancels the follow.
804
+ * - 'once': a single animated fit at first quiescence.
805
+ * - false: v0.15 behavior (first-data fit only).
806
+ * Only force layouts follow; the fixed layout keeps the single fit.
807
+ */
808
+ fitViewOnSettle?: 'follow' | 'once' | false;
780
809
  /** revision-aware services (expansion + search). */
781
810
  services?: GraphServices<N, E>;
782
811
  /**
@@ -3174,9 +3203,11 @@ declare function renderSvg(scene: SvgScene, opts?: RenderSvgOptions): string;
3174
3203
  * STRING, which survives.
3175
3204
  * - duplicate edge ids drop, first wins ('duplicate-edge-id', warning).
3176
3205
  * - self-loops are RETAINED with 'self-loop-retained' (info).
3177
- * - invalid-node / invalid-edge / dangling-edge cannot occur here: ids come
3178
- * from a structurally validated dictionary column and endpoints are
3179
- * in-bounds indices by prior validation (validateColumnarStructure).
3206
+ * - invalid-node / invalid-edge rows with NUL-reserved ids drop here;
3207
+ * dangling edges can then arise when an endpoint names a dropped invalid
3208
+ * node. Other structural corruption cannot occur because ids come from a
3209
+ * structurally validated dictionary column and endpoints are in-bounds
3210
+ * indices by prior validation (validateColumnarStructure).
3180
3211
  *
3181
3212
  * Duplicates hide in TWO encodings: two rows sharing a code, and two
3182
3213
  * DISTINCT dictionary entries holding equal strings. Both are handled by