@modernrelay/orbit-core 0.14.0 → 0.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/testing.js CHANGED
@@ -1,5 +1,5 @@
1
- import { EnvelopeSequencer, isWellFormedEnvelope, decodeStringTable, acceptColumnar, collectTransfers } from './chunk-KRCG2JFN.js';
2
- export { clusterProbe, resetClusterProbe } from './chunk-KRCG2JFN.js';
1
+ import { EnvelopeSequencer, isWellFormedEnvelope, decodeStringTable, acceptColumnar, collectTransfers } from './chunk-TDBIVBJ3.js';
2
+ export { clusterProbe, resetClusterProbe } from './chunk-TDBIVBJ3.js';
3
3
 
4
4
  // src/testing/FakeEngine.ts
5
5
  var DEFAULT_CAPABILITIES = {
@@ -67,6 +67,7 @@ function acceptColumnar(snapshot) {
67
67
  const nodeRows = snapshot.nodes.length;
68
68
  const edgeRows = snapshot.edges.length;
69
69
  const duplicateNode = { count: 0, samples: [] };
70
+ const invalidEdge = { count: 0, samples: [] };
70
71
  const duplicateEdge = { count: 0, samples: [] };
71
72
  const selfLoop = { count: 0, samples: [] };
72
73
  const invalidNode = { count: 0, samples: [] };
@@ -98,18 +99,21 @@ function acceptColumnar(snapshot) {
98
99
  }
99
100
  }
100
101
  const edgeCanonical = canonicalizeDictionary(edgeIds.dictionary);
102
+ const nulEdgeDict = new Uint8Array(edgeIds.dictionary.length);
103
+ for (let d = 0; d < edgeIds.dictionary.length; d++) {
104
+ if (edgeIds.dictionary[d].includes("\0")) nulEdgeDict[d] = 1;
105
+ }
101
106
  const keepEdges = new Uint8Array(edgeRows);
102
107
  const seenEdgeByCanonical = new Uint8Array(edgeIds.dictionary.length);
103
108
  const { source, target } = snapshot.edges;
104
109
  const linksOut = new Uint32Array(edgeRows * 2);
105
110
  let acceptedEdgeCount = 0;
106
111
  for (let e = 0; e < edgeRows; e++) {
107
- const canonical = edgeCanonical[edgeIds.codes[e]];
108
- if (seenEdgeByCanonical[canonical] !== 0) {
109
- record(duplicateEdge, edgeIds.dictionary[edgeIds.codes[e]]);
112
+ if (nulEdgeDict[edgeIds.codes[e]] !== 0) {
113
+ record(invalidEdge, `[${e}]`);
110
114
  continue;
111
115
  }
112
- seenEdgeByCanonical[canonical] = 1;
116
+ const canonical = edgeCanonical[edgeIds.codes[e]];
113
117
  const s = nodeAcceptedIndex[source[e]];
114
118
  const t = nodeAcceptedIndex[target[e]];
115
119
  if (s === -1 || t === -1) {
@@ -119,6 +123,11 @@ function acceptColumnar(snapshot) {
119
123
  );
120
124
  continue;
121
125
  }
126
+ if (seenEdgeByCanonical[canonical] !== 0) {
127
+ record(duplicateEdge, edgeIds.dictionary[edgeIds.codes[e]]);
128
+ continue;
129
+ }
130
+ seenEdgeByCanonical[canonical] = 1;
122
131
  if (s === t) {
123
132
  record(selfLoop, nodeIds.dictionary[nodeIds.codes[source[e]]]);
124
133
  }
@@ -142,6 +151,13 @@ function acceptColumnar(snapshot) {
142
151
  duplicateNode,
143
152
  `${duplicateNode.count} duplicate node id(s) dropped (first occurrence wins)`
144
153
  );
154
+ pushDiagnostic(
155
+ diagnostics,
156
+ "invalid-edge",
157
+ "error",
158
+ invalidEdge,
159
+ `${invalidEdge.count} edge row(s) dropped: missing or non-string source/target, or NUL-containing explicit id`
160
+ );
145
161
  pushDiagnostic(
146
162
  diagnostics,
147
163
  "dangling-edge-endpoint",
@@ -175,9 +191,9 @@ function acceptColumnar(snapshot) {
175
191
  }
176
192
 
177
193
  // src/worker/runtime.ts
178
- function handleWorkerRequest(request, sequencer2) {
194
+ function handleWorkerRequest(request, sequencer) {
179
195
  if (!isWellFormedEnvelope(request)) {
180
- const reply2 = sequencer2.make(0, "scene", "error", {
196
+ const reply2 = sequencer.make(0, "scene", "error", {
181
197
  message: "malformed envelope (protocol violation)"
182
198
  });
183
199
  return { reply: reply2, transfers: [] };
@@ -221,7 +237,7 @@ function handleWorkerRequest(request, sequencer2) {
221
237
  links: acceptance.links,
222
238
  diagnostics: acceptance.diagnostics
223
239
  };
224
- const reply2 = sequencer2.make(
240
+ const reply2 = sequencer.make(
225
241
  request.epoch,
226
242
  request.entity,
227
243
  "result",
@@ -238,7 +254,7 @@ function handleWorkerRequest(request, sequencer2) {
238
254
  ])
239
255
  };
240
256
  } catch (err) {
241
- const reply2 = sequencer2.make(
257
+ const reply2 = sequencer.make(
242
258
  request.epoch,
243
259
  request.entity,
244
260
  "error",
@@ -248,7 +264,7 @@ function handleWorkerRequest(request, sequencer2) {
248
264
  return { reply: reply2, transfers: [] };
249
265
  }
250
266
  }
251
- const reply = sequencer2.make(
267
+ const reply = sequencer.make(
252
268
  request.epoch,
253
269
  request.entity,
254
270
  "error",
@@ -259,11 +275,15 @@ function handleWorkerRequest(request, sequencer2) {
259
275
  }
260
276
 
261
277
  // src/worker/entry.ts
262
- var sequencer = new EnvelopeSequencer();
263
- var scope = self;
264
- scope.onmessage = (ev) => {
265
- const { reply, transfers } = handleWorkerRequest(ev.data, sequencer);
266
- scope.postMessage(reply, [...transfers]);
267
- };
278
+ function installWorkerEntry(scope) {
279
+ const sequencer = new EnvelopeSequencer();
280
+ scope.onmessage = (ev) => {
281
+ const { reply, transfers } = handleWorkerRequest(ev.data, sequencer);
282
+ scope.postMessage(reply, [...transfers]);
283
+ };
284
+ }
285
+
286
+ // src/worker/entry.js
287
+ installWorkerEntry(self);
268
288
  //# sourceMappingURL=entry.js.map
269
289
  //# sourceMappingURL=entry.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/workerProtocol.ts","../../src/types.ts","../../src/columnarValidate.ts","../../src/worker/runtime.ts","../../src/worker/entry.ts"],"names":["sequencer","reply"],"mappings":"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","file":"entry.js","sourcesContent":["/**\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 * 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// 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?: SimulationConfig;\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 * 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 / dangling-edge cannot occur here: ids come\n * from a structurally validated dictionary column and endpoints are\n * in-bounds 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 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 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 const canonical = edgeCanonical[edgeIds.codes[e]!]!;\n if (seenEdgeByCanonical[canonical] !== 0) {\n record(duplicateEdge, edgeIds.dictionary[edgeIds.codes[e]!]!);\n continue;\n }\n seenEdgeByCanonical[canonical] = 1;\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 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 '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","/**\n * Worker-side request handler, implemented as a PURE function over the\n * codec so the real thread entry (worker/entry.ts) and the in-process test\n * double drive IDENTICAL code (D2: one implementation, never duplicated).\n *\n * First cargo: `derive-columnar` — the acceptance rules over\n * transferred acceptance inputs. Dictionaries arrive as UTF-8 string tables\n * (transferables, decoded off-main); results leave as keep bitmaps, the\n * survivor remap, resolved links, and object-lane-identical diagnostics\n * every heavy field a transferable.\n *\n * No thread machinery here: input envelope in, reply envelope + transfer\n * list out. Unknown ops and malformed payloads reply with an 'error' op\n * (never throw — a worker that dies on one bad message kills every pending\n * request behind it).\n */\n\nimport { acceptColumnar } from '../columnarValidate';\nimport type { ColumnarGraphSnapshot, GraphDiagnostic } from '../types';\nimport {\n EnvelopeSequencer,\n collectTransfers,\n decodeStringTable,\n isWellFormedEnvelope,\n} from '../workerProtocol';\nimport type { EncodedStringTable, WorkerEnvelope } from '../workerProtocol';\n\n/** Wire payload for 'derive-columnar' — the acceptance inputs ONLY (attr\n * columns never cross; materialization stays main-side this slice). */\nexport interface DeriveColumnarRequest {\n nodeIdTable: EncodedStringTable;\n nodeIdCodes: Uint32Array;\n nodeCount: number;\n edgeIdTable: EncodedStringTable;\n edgeIdCodes: Uint32Array;\n edgeSource: Uint32Array;\n edgeTarget: Uint32Array;\n edgeCount: number;\n}\n\nexport interface DeriveColumnarResult {\n keepNodes: Uint8Array;\n keepEdges: Uint8Array;\n acceptedNodeCount: number;\n acceptedEdgeCount: number;\n nodeAcceptedIndex: Int32Array;\n links: Uint32Array;\n diagnostics: GraphDiagnostic[];\n}\n\nexport interface HandledReply {\n reply: WorkerEnvelope;\n transfers: ArrayBuffer[];\n}\n\n/** Handle ONE request envelope. The sequencer is the worker's own outbound\n * direction (per-direction monotonic ids). */\nexport function handleWorkerRequest(\n request: unknown,\n sequencer: EnvelopeSequencer,\n): HandledReply {\n if (!isWellFormedEnvelope(request)) {\n const reply = sequencer.make(0, 'scene', 'error', {\n message: 'malformed envelope (protocol violation)',\n });\n return { reply, transfers: [] };\n }\n\n if (request.op === 'derive-columnar') {\n try {\n const p = request.payload as DeriveColumnarRequest;\n // Rebuild the snapshot SHAPE acceptColumnar expects — same module the\n // main lane uses (D2), fed decoded-off-main dictionaries.\n const snapshot: ColumnarGraphSnapshot<unknown, unknown> = {\n kind: 'columnar',\n datasetKey: 'worker', // acceptance rules never read the coordinate\n sourceRevision: 0,\n nodes: {\n ids: {\n kind: 'string',\n dictionary: decodeStringTable(p.nodeIdTable),\n codes: p.nodeIdCodes,\n },\n columns: {},\n length: p.nodeCount,\n },\n edges: {\n ids: {\n kind: 'string',\n dictionary: decodeStringTable(p.edgeIdTable),\n codes: p.edgeIdCodes,\n },\n source: p.edgeSource,\n target: p.edgeTarget,\n columns: {},\n length: p.edgeCount,\n },\n };\n const acceptance = acceptColumnar(snapshot);\n const result: DeriveColumnarResult = {\n keepNodes: acceptance.keepNodes,\n keepEdges: acceptance.keepEdges,\n acceptedNodeCount: acceptance.acceptedNodeCount,\n acceptedEdgeCount: acceptance.acceptedEdgeCount,\n nodeAcceptedIndex: acceptance.nodeAcceptedIndex,\n links: acceptance.links,\n diagnostics: acceptance.diagnostics,\n };\n const reply = sequencer.make(\n request.epoch,\n request.entity,\n 'result',\n result,\n request.msgId,\n );\n return {\n reply,\n transfers: collectTransfers([\n result.keepNodes,\n result.keepEdges,\n result.nodeAcceptedIndex,\n result.links,\n ]),\n };\n } catch (err) {\n const reply = sequencer.make(\n request.epoch,\n request.entity,\n 'error',\n { message: err instanceof Error ? err.message : String(err) },\n request.msgId,\n );\n return { reply, transfers: [] };\n }\n }\n\n const reply = sequencer.make(\n request.epoch,\n request.entity,\n 'error',\n { message: `unknown op '${request.op}'` },\n request.msgId,\n );\n return { reply, transfers: [] };\n}\n","/**\n * Inline worker entry, built as its own asset inside\n * core (dist/worker/entry.js). All semantics live in runtime.ts (shared\n * with the in-process double — D2's one-implementation rule); this file is\n * ONLY the thread glue.\n */\n\nimport { EnvelopeSequencer } from '../workerProtocol';\nimport { handleWorkerRequest } from './runtime';\n\nconst sequencer = new EnvelopeSequencer();\nconst scope = self as unknown as {\n onmessage: ((ev: MessageEvent) => void) | null;\n postMessage: (message: unknown, transfer: Transferable[]) => void;\n};\n\nscope.onmessage = (ev: MessageEvent) => {\n const { reply, transfers } = handleWorkerRequest(ev.data, sequencer);\n scope.postMessage(reply, [...transfers]);\n};\n"]}
1
+ {"version":3,"sources":["../../src/workerProtocol.ts","../../src/types.ts","../../src/columnarValidate.ts","../../src/worker/runtime.ts","../../src/worker/entry.ts","../../src/worker/entry.js"],"names":["reply"],"mappings":"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","file":"entry.js","sourcesContent":["/**\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 * 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// 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?: SimulationConfig;\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 * 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","/**\n * Worker-side request handler, implemented as a PURE function over the\n * codec so the real thread entry (worker/entry.ts) and the in-process test\n * double drive IDENTICAL code (D2: one implementation, never duplicated).\n *\n * First cargo: `derive-columnar` — the acceptance rules over\n * transferred acceptance inputs. Dictionaries arrive as UTF-8 string tables\n * (transferables, decoded off-main); results leave as keep bitmaps, the\n * survivor remap, resolved links, and object-lane-identical diagnostics\n * every heavy field a transferable.\n *\n * No thread machinery here: input envelope in, reply envelope + transfer\n * list out. Unknown ops and malformed payloads reply with an 'error' op\n * (never throw — a worker that dies on one bad message kills every pending\n * request behind it).\n */\n\nimport { acceptColumnar } from '../columnarValidate';\nimport type { ColumnarGraphSnapshot, GraphDiagnostic } from '../types';\nimport {\n EnvelopeSequencer,\n collectTransfers,\n decodeStringTable,\n isWellFormedEnvelope,\n} from '../workerProtocol';\nimport type { EncodedStringTable, WorkerEnvelope } from '../workerProtocol';\n\n/** Wire payload for 'derive-columnar' — the acceptance inputs ONLY (attr\n * columns never cross; materialization stays main-side this slice). */\nexport interface DeriveColumnarRequest {\n nodeIdTable: EncodedStringTable;\n nodeIdCodes: Uint32Array;\n nodeCount: number;\n edgeIdTable: EncodedStringTable;\n edgeIdCodes: Uint32Array;\n edgeSource: Uint32Array;\n edgeTarget: Uint32Array;\n edgeCount: number;\n}\n\nexport interface DeriveColumnarResult {\n keepNodes: Uint8Array;\n keepEdges: Uint8Array;\n acceptedNodeCount: number;\n acceptedEdgeCount: number;\n nodeAcceptedIndex: Int32Array;\n links: Uint32Array;\n diagnostics: GraphDiagnostic[];\n}\n\nexport interface HandledReply {\n reply: WorkerEnvelope;\n transfers: ArrayBuffer[];\n}\n\n/** Handle ONE request envelope. The sequencer is the worker's own outbound\n * direction (per-direction monotonic ids). */\nexport function handleWorkerRequest(\n request: unknown,\n sequencer: EnvelopeSequencer,\n): HandledReply {\n if (!isWellFormedEnvelope(request)) {\n const reply = sequencer.make(0, 'scene', 'error', {\n message: 'malformed envelope (protocol violation)',\n });\n return { reply, transfers: [] };\n }\n\n if (request.op === 'derive-columnar') {\n try {\n const p = request.payload as DeriveColumnarRequest;\n // Rebuild the snapshot SHAPE acceptColumnar expects — same module the\n // main lane uses (D2), fed decoded-off-main dictionaries.\n const snapshot: ColumnarGraphSnapshot<unknown, unknown> = {\n kind: 'columnar',\n datasetKey: 'worker', // acceptance rules never read the coordinate\n sourceRevision: 0,\n nodes: {\n ids: {\n kind: 'string',\n dictionary: decodeStringTable(p.nodeIdTable),\n codes: p.nodeIdCodes,\n },\n columns: {},\n length: p.nodeCount,\n },\n edges: {\n ids: {\n kind: 'string',\n dictionary: decodeStringTable(p.edgeIdTable),\n codes: p.edgeIdCodes,\n },\n source: p.edgeSource,\n target: p.edgeTarget,\n columns: {},\n length: p.edgeCount,\n },\n };\n const acceptance = acceptColumnar(snapshot);\n const result: DeriveColumnarResult = {\n keepNodes: acceptance.keepNodes,\n keepEdges: acceptance.keepEdges,\n acceptedNodeCount: acceptance.acceptedNodeCount,\n acceptedEdgeCount: acceptance.acceptedEdgeCount,\n nodeAcceptedIndex: acceptance.nodeAcceptedIndex,\n links: acceptance.links,\n diagnostics: acceptance.diagnostics,\n };\n const reply = sequencer.make(\n request.epoch,\n request.entity,\n 'result',\n result,\n request.msgId,\n );\n return {\n reply,\n transfers: collectTransfers([\n result.keepNodes,\n result.keepEdges,\n result.nodeAcceptedIndex,\n result.links,\n ]),\n };\n } catch (err) {\n const reply = sequencer.make(\n request.epoch,\n request.entity,\n 'error',\n { message: err instanceof Error ? err.message : String(err) },\n request.msgId,\n );\n return { reply, transfers: [] };\n }\n }\n\n const reply = sequencer.make(\n request.epoch,\n request.entity,\n 'error',\n { message: `unknown op '${request.op}'` },\n request.msgId,\n );\n return { reply, transfers: [] };\n}\n","/**\n * Inline worker entry, built as its own asset inside\n * core (dist/worker/entry.js). All semantics live in runtime.ts (shared\n * with the in-process double — D2's one-implementation rule); this file is\n * ONLY the thread glue.\n */\n\nimport { EnvelopeSequencer } from '../workerProtocol';\nimport { handleWorkerRequest } from './runtime';\n\nexport interface WorkerEntryScope {\n onmessage: ((ev: MessageEvent) => void) | null;\n postMessage: (message: unknown, transfer: Transferable[]) => void;\n}\n\n/** Install the thread glue. The .js bootstrap calls this in both workspace\n * Vite builds and the self-contained published worker bundle. */\nexport function installWorkerEntry(scope: WorkerEntryScope): void {\n const sequencer = new EnvelopeSequencer();\n scope.onmessage = (ev: MessageEvent) => {\n const { reply, transfers } = handleWorkerRequest(ev.data, sequencer);\n scope.postMessage(reply, [...transfers]);\n };\n}\n","// Shared bootstrap for workspace Vite builds and the published worker asset.\n// The implementation stays in TypeScript so the real thread and test double\n// continue to share the same runtime/codec.\nimport { installWorkerEntry } from './entry.ts';\n\ninstallWorkerEntry(self);\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@modernrelay/orbit-core",
3
- "version": "0.14.0",
3
+ "version": "0.15.0",
4
4
  "description": "Headless graph-visualization core: typed declarative snapshots reconciled into atomic engine commits. No React, no DOM, no engine imports.",
5
5
  "license": "MIT",
6
6
  "repository": {