@modernrelay/orbit-core 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/dist/chunk-Z7FOEASL.js +141 -0
- package/dist/chunk-Z7FOEASL.js.map +1 -0
- package/dist/clusters-ExqnvobT.d.ts +123 -0
- package/dist/engine.d.ts +1 -0
- package/dist/engine.js +3 -0
- package/dist/engine.js.map +1 -0
- package/dist/index-BPjuELfY.d.ts +1198 -0
- package/dist/index.d.ts +2995 -0
- package/dist/index.js +11938 -0
- package/dist/index.js.map +1 -0
- package/dist/testing.d.ts +163 -0
- package/dist/testing.js +384 -0
- package/dist/testing.js.map +1 -0
- package/package.json +45 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 ModernRelay
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
// src/types.ts
|
|
2
|
+
var DIAGNOSTIC_SAMPLE_CAP = 10;
|
|
3
|
+
|
|
4
|
+
// src/clusters.ts
|
|
5
|
+
var clusterProbe = {
|
|
6
|
+
derivations: 0,
|
|
7
|
+
memberVisits: 0
|
|
8
|
+
};
|
|
9
|
+
function resetClusterProbe() {
|
|
10
|
+
clusterProbe.derivations = 0;
|
|
11
|
+
clusterProbe.memberVisits = 0;
|
|
12
|
+
}
|
|
13
|
+
var DEFAULT_LAYOUT_SEED = 1592594996;
|
|
14
|
+
var DEFAULT_CLUSTER_CENTER_RADIUS = 1024;
|
|
15
|
+
function hash32(key, seed) {
|
|
16
|
+
let h = (2166136261 ^ (seed | 0)) >>> 0;
|
|
17
|
+
for (let i = 0; i < key.length; i++) {
|
|
18
|
+
h = (h ^ key.charCodeAt(i)) >>> 0;
|
|
19
|
+
h = Math.imul(h, 16777619) >>> 0;
|
|
20
|
+
}
|
|
21
|
+
h = (h ^ h >>> 16) >>> 0;
|
|
22
|
+
h = Math.imul(h, 2246822507) >>> 0;
|
|
23
|
+
h = (h ^ h >>> 13) >>> 0;
|
|
24
|
+
h = Math.imul(h, 3266489909) >>> 0;
|
|
25
|
+
return (h ^ h >>> 16) >>> 0;
|
|
26
|
+
}
|
|
27
|
+
var UNIT = 23283064365386963e-26;
|
|
28
|
+
function radicalInverse(index, base) {
|
|
29
|
+
let result = 0;
|
|
30
|
+
let denominator = 1;
|
|
31
|
+
let n = index;
|
|
32
|
+
while (n > 0) {
|
|
33
|
+
denominator *= base;
|
|
34
|
+
result += n % base / denominator;
|
|
35
|
+
n = Math.floor(n / base);
|
|
36
|
+
}
|
|
37
|
+
return result;
|
|
38
|
+
}
|
|
39
|
+
function frac(v) {
|
|
40
|
+
return v - Math.floor(v);
|
|
41
|
+
}
|
|
42
|
+
function generateClusterCenters(keys, seed = DEFAULT_LAYOUT_SEED, radius = DEFAULT_CLUSTER_CENTER_RADIUS) {
|
|
43
|
+
const out = new Float32Array(2 * keys.length);
|
|
44
|
+
for (let i = 0; i < keys.length; i++) {
|
|
45
|
+
const key = keys[i];
|
|
46
|
+
const u = frac(radicalInverse(i + 1, 2) + hash32(key, seed) * UNIT);
|
|
47
|
+
const v = frac(radicalInverse(i + 1, 3) + hash32(key, seed ^ 2654435769 | 0) * UNIT);
|
|
48
|
+
out[2 * i] = radius * (2 * u - 1);
|
|
49
|
+
out[2 * i + 1] = radius * (2 * v - 1);
|
|
50
|
+
}
|
|
51
|
+
return out;
|
|
52
|
+
}
|
|
53
|
+
function resolveClusterCenters(keys, explicit, seed = DEFAULT_LAYOUT_SEED, radius = DEFAULT_CLUSTER_CENTER_RADIUS) {
|
|
54
|
+
const out = generateClusterCenters(keys, seed, radius);
|
|
55
|
+
if (explicit === void 0 || explicit.size === 0) return out;
|
|
56
|
+
for (let i = 0; i < keys.length; i++) {
|
|
57
|
+
const pair = explicit.get(keys[i]);
|
|
58
|
+
if (pair === void 0) continue;
|
|
59
|
+
const x = pair[0];
|
|
60
|
+
const y = pair[1];
|
|
61
|
+
if (!Number.isFinite(x) || !Number.isFinite(y)) continue;
|
|
62
|
+
out[2 * i] = x;
|
|
63
|
+
out[2 * i + 1] = y;
|
|
64
|
+
}
|
|
65
|
+
return out;
|
|
66
|
+
}
|
|
67
|
+
function deriveClusters(nodes, by, sceneCount = nodes.length) {
|
|
68
|
+
clusterProbe.derivations++;
|
|
69
|
+
const keys = [];
|
|
70
|
+
const ordinalByKey = /* @__PURE__ */ new Map();
|
|
71
|
+
const membersByKey = /* @__PURE__ */ new Map();
|
|
72
|
+
const slotOrdinals = new Float32Array(Math.max(sceneCount, nodes.length)).fill(NaN);
|
|
73
|
+
let errorCount = 0;
|
|
74
|
+
const errorSamples = [];
|
|
75
|
+
for (let i = 0; i < nodes.length; i++) {
|
|
76
|
+
clusterProbe.memberVisits++;
|
|
77
|
+
const node = nodes[i];
|
|
78
|
+
let raw;
|
|
79
|
+
try {
|
|
80
|
+
raw = by(node);
|
|
81
|
+
} catch {
|
|
82
|
+
errorCount++;
|
|
83
|
+
if (errorSamples.length < DIAGNOSTIC_SAMPLE_CAP) errorSamples.push(node.id);
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
if (typeof raw !== "string") continue;
|
|
87
|
+
let ordinal = ordinalByKey.get(raw);
|
|
88
|
+
if (ordinal === void 0) {
|
|
89
|
+
ordinal = keys.length;
|
|
90
|
+
keys.push(raw);
|
|
91
|
+
ordinalByKey.set(raw, ordinal);
|
|
92
|
+
membersByKey.set(raw, []);
|
|
93
|
+
}
|
|
94
|
+
membersByKey.get(raw).push(node.id);
|
|
95
|
+
slotOrdinals[i] = ordinal;
|
|
96
|
+
}
|
|
97
|
+
return {
|
|
98
|
+
keys,
|
|
99
|
+
ordinalByKey,
|
|
100
|
+
membersByKey,
|
|
101
|
+
slotOrdinals,
|
|
102
|
+
diagnostic: errorCount === 0 ? null : {
|
|
103
|
+
code: "accessor-error",
|
|
104
|
+
severity: "warning",
|
|
105
|
+
count: errorCount,
|
|
106
|
+
sampleIds: errorSamples,
|
|
107
|
+
message: "clusters.by threw; the affected nodes derived as unclustered (\xA716.3/\xA78)"
|
|
108
|
+
}
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
function clusterCentroids(slotOrdinals, positions, clusterCount, fallback) {
|
|
112
|
+
const out = new Float32Array(2 * clusterCount);
|
|
113
|
+
const counts = new Float64Array(clusterCount);
|
|
114
|
+
const slots = Math.min(slotOrdinals.length, Math.floor(positions.length / 2));
|
|
115
|
+
for (let i = 0; i < slots; i++) {
|
|
116
|
+
const ordinal = slotOrdinals[i];
|
|
117
|
+
if (Number.isNaN(ordinal)) continue;
|
|
118
|
+
clusterProbe.memberVisits++;
|
|
119
|
+
const x = positions[2 * i];
|
|
120
|
+
const y = positions[2 * i + 1];
|
|
121
|
+
if (Number.isNaN(x) || Number.isNaN(y)) continue;
|
|
122
|
+
out[2 * ordinal] = out[2 * ordinal] + x;
|
|
123
|
+
out[2 * ordinal + 1] = out[2 * ordinal + 1] + y;
|
|
124
|
+
counts[ordinal] = counts[ordinal] + 1;
|
|
125
|
+
}
|
|
126
|
+
for (let c = 0; c < clusterCount; c++) {
|
|
127
|
+
const n = counts[c];
|
|
128
|
+
if (n === 0) {
|
|
129
|
+
out[2 * c] = fallback[2 * c] ?? 0;
|
|
130
|
+
out[2 * c + 1] = fallback[2 * c + 1] ?? 0;
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
out[2 * c] = out[2 * c] / n;
|
|
134
|
+
out[2 * c + 1] = out[2 * c + 1] / n;
|
|
135
|
+
}
|
|
136
|
+
return out;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export { DEFAULT_CLUSTER_CENTER_RADIUS, DEFAULT_LAYOUT_SEED, DIAGNOSTIC_SAMPLE_CAP, clusterCentroids, clusterProbe, deriveClusters, generateClusterCenters, resetClusterProbe, resolveClusterCenters };
|
|
140
|
+
//# sourceMappingURL=chunk-Z7FOEASL.js.map
|
|
141
|
+
//# sourceMappingURL=chunk-Z7FOEASL.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/types.ts","../src/clusters.ts"],"names":[],"mappings":"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","file":"chunk-Z7FOEASL.js","sourcesContent":["/**\n * orbit-core public data model (spec §5, v0.1 subset).\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 (§4).\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 (§16.14: stored, round-tripped, compared canonically, 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 (layout 'fixed' honors these; §10). */\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 (§5). 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 (§4, §5). */\nexport interface GraphSnapshot<N = Record<string, unknown>, E = Record<string, unknown>> {\n /** Identity of the dataset; changing it clears all per-dataset state (§5). */\n datasetKey: string;\n /** Caller-owned revision; same {datasetKey, sourceRevision} replays are idempotent (§5). */\n sourceRevision: number | string;\n nodes: readonly GraphNode<N>[];\n edges: readonly GraphEdge<E>[];\n}\n\n// ---------------------------------------------------------------------------\n// Diagnostics (§5.1 subset). 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 (§9.1). */\n | 'filter-error'\n /** Async metric column rejected (misaligned/duplicate/unknown ids; §12). */\n | 'metric-column-error'\n /** A channel reprojected repeatedly with identical outputs (§8 dev). */\n | 'accessor-churn'\n /** Image atlas resolve/decoding failures, cadence-batched (§8). */\n | 'image-resolve-failed'\n | 'source-revision-reused'\n /** A host config lane was rejected at the boundary (§5.1/§16.3 — e.g. a\n * groups array whose containment is cyclic or multiply parented); the\n * previous config stays live. */\n | 'config-error'\n /** §16.14: 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 per §15 (the chain continues). */\n | 'listener-error'\n /** showLabelsFor exceeded tracked-label capacity; omissions counted (§14). */\n | 'label-overload'\n /** A same-id row from an earlier overlay won in admission order (§7.5). */\n | 'overlay-node-shadowed'\n /** A service call was aborted/discarded before admission (§9.2; info). */\n | 'service-aborted'\n /** A service call failed (§9.2; error). */\n | 'service-error'\n | 'context-lost'\n | 'operation-rejected'\n /** Adapter-defined codes are namespaced (spec §6.5 routing note). */\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 (§5, §7 — v0.1 subset of the four-way taxonomy).\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 (§9). 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 (v0.5, §9.2). */\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 §5.1 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 (§5.1, §16.2). */\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 (§7). Public\n// payloads never expose engine indices (§7.4); 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 (§7.3); 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 * §16.3 stage-3 synthetic suffix (S12). 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 (§7.4) — consumers\n * resolve slots through the discriminated ScenePointRef/SceneLinkRef\n * helpers instead.\n */\n groups?: SceneGroups;\n}\n\n/** §16.3 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 * §16.3 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, R-6.1-14) — 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/** §7.4 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/** §7.4 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 (§6.1/§8 subset): 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 * §10 force tunables under stable, engine-neutral names — orbit maps them onto\n * the active engine's parameters through atomic config-only commits (§13), 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 */\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 (§6): one call carries data + config +\n// controlled state and publishes exactly one store revision and at most one\n// engine commit.\n// ---------------------------------------------------------------------------\n\nexport interface GraphHostUpdate<N = Record<string, unknown>, E = Record<string, unknown>> {\n data?: GraphSnapshot<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 /** §12 async metric columns, joined once with revision-gated admission. */\n metrics?: readonly MetricColumn[];\n /**\n * §8 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 (§13 capability policy).\n */\n /** §8 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 /** §16.12 instanced arrowheads (capability-gated; inert when unsupported). */\n edgeArrows?: boolean;\n /** §16.14 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 /** §16.13 runtime toggles — atomic config-only commits, no reprojection. */\n showLinks?: boolean;\n layout?: LayoutKind;\n simulation?: SimulationConfig;\n /** Controlled selection (uncontrolled when never provided; §6.4 subset). */\n selection?: readonly NodeId[];\n theme?: ThemeInput;\n /** DOM label lane configuration (§14; strategy 'dom' only in v0.4). */\n labels?: LabelConfig<N>;\n /** §15.1 accessibility runtime options. */\n accessibility?: AccessibilityConfig<N>;\n /** §9.2 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 /** §9.1 soft filter: mask (hide/dim) with ZERO relayout; null clears. */\n filter?: FilterSpec<N, E> | null;\n /** §16.6 crossfilter dimensions (declarative; brushes live on the session). */\n crossfilter?: readonly DimensionSpec<N>[];\n /** §16.3 manual groups; null clears (D2). Config-error with groupBy. */\n groups?: readonly GroupSpec[] | null;\n /** §16.3 derived grouping; null clears (D2). Config-error with groups. */\n groupBy?: GroupBySpec<N> | null;\n /** §16.3 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 /** §16.3 persistent pins (independent of transient drag pinning); null\n * clears (D2). Departed ids prune through ownership. */\n pinnedNodeIds?: readonly NodeId[] | null;\n /** §16.3 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// §11/§12 scales & metrics (v0.8 subset). Scales are plain descriptors and\n// compare by CANONICAL STRUCTURAL VALUE — equal inline literals never\n// reproject (§8). 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 (§12). */\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 * §12/I1 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// §8 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}\n\nexport type ThemeInput =\n | (Partial<GraphTheme> & { base?: 'light' | 'dark' })\n | GraphTheme;\n\n// ---------------------------------------------------------------------------\n// §9.1 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// §16.6 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; §8 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 §16.6 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 §8 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// §16.6 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// §9.2 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// §16.5 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 (Omnigraph B.7). 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 (§16.5 result contract). */\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 (§9.2 sequencing rule). */\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 (§9.2). */\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 * §16.2 path resolver seam (S12-T08). `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 §9.2 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// §7.5 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 (§7.5). */\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 (§7.5). */\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 emitted only then; §7.5). */\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/** §14 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 * §16.3 cluster-label LOD ceiling. At or BELOW this zoom the active\n * `clusters` spec's 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 (§14 showLabelsFor). */\n showFor?: readonly NodeId[];\n /** Label text; default attrs.label ?? id. Rendered as a TEXT NODE (§14). */\n getText?: (node: GraphNode<N>) => string;\n /** Ranking weight; default nodeSize result order, else degree. */\n getWeight?: (node: GraphNode<N>) => number;\n}\n\n/** §15.1 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 (§15.1).\n */\n reducedMotion?: boolean;\n}\n\n/** One positioned label emitted to the overlay lane per scheduler tick (§14). */\nexport interface LabelPlacement {\n /** Node id — or, for `kind: 'cluster'`, the §16.3 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 * §16.3 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 (R-16.3-18/21). 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; §6.3/§6.4 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 (§13.1). */\n | 'lost'\n /** Context restored; the full-scene replay commit is in flight (§13.1). */\n | 'recovering'\n | 'destroyed'\n | 'error';\n\n/**\n * Namespaced selection (§16.2). Namespaces are independent: node-set algebra\n * never mutates edge selection. `groupIds` is reserved (populated from S12).\n */\nexport interface SelectionState {\n nodeIds: readonly NodeId[];\n edgeIds: readonly EdgeId[];\n groupIds: readonly string[];\n}\n\n// ---------------------------------------------------------------------------\n// §16.3 semantic exploration (S12): groups, groupBy, meta-edges, paths.\n// ---------------------------------------------------------------------------\n\n/** §16.3 manual group definition. Flat and disjoint: membership may not\n * nest, overlap, duplicate, self-reference, or name unknown ids — violations\n * are §5.1 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/** §16.3 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 * §16.3 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 (R-16.3-17/19); 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 (R-13-39). */\n strength?: number;\n /** Explicit force centers per key, in SPACE coordinates. Keys omitted here\n * generate deterministically from the ordered keys + layout seed\n * (R-16.3-20 — see `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 §7.1 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/** §16.3 rerouted member edge on a collapsed group (stage 3), or a grouped\n * parallel-edge bundle (§16.3 R-24). 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/** §16.2 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 (§16.3 pin slice; drag-pinning writes here). */\n pins: ReadonlyMap<NodeId, readonly [number, number]>;\n /** §16.3 PERSISTENT pins (S12-T09): 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 (§9.2); null = full scope. */\n scope: SubgraphSpec | null;\n /** Soft-mask visibility counts (§9.1): RENDERED SCENE entities with zero\n * hide-failures — the §16.3 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 (§7.4). 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 (§16.6): at most one playing dimension. */\n timeline: { playingKey: string | null };\n /** §16.14 history kernel depths (S9-T20; full walk semantics in S15). */\n history: { undoDepth: number; redoDepth: number };\n /** Node ids with an expansion in flight (§9.2 loading affordance). */\n pendingExpansions: ReadonlySet<NodeId>;\n /**\n * §16.3 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 §14 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 (§7.5). */\n overlayIds: readonly string[];\n /** §16.3 resolved groups (manual or groupBy-derived); [] when ungrouped.\n * Path highlight is deliberately NOT here: session-local, never\n * serialized (§16.2). */\n groups: readonly ResolvedGroup[];\n /** Last completed search (§16.5): feeds <GraphSearch> and the §15.1\n * navigator's search-results section. Cleared on datasetKey change. */\n search: { query: string; results: readonly SearchResult[] } | null;\n viewport: ViewportState | null;\n /** §14/§16.1: live force-simulation activity — true after a commit with\n * restart or resumeSimulation(); false on settle or pauseSimulation(). */\n simulationRunning: boolean;\n /** §8 resolved theme tokens (S10): 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 (§7.4/§15): 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 (§15).\n// ---------------------------------------------------------------------------\n\nexport interface GraphListenerControl {\n preventDefault(): void;\n}\n\nexport interface NodeEventPayload<N = Record<string, unknown>> {\n node: GraphNode<N>;\n}\n\nexport interface GraphEventMap<N = Record<string, unknown>, E = Record<string, unknown>> {\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 /** §16.14: 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 /** §16.14 aggregate restore intent: fired ONCE per restore/history\n * transaction touching any §6.4 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 /** §16.3/§7.4 (R-16.3-12): 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 /** §16.3/§7.4: 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 /** §6.4 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, R-16.3-16). 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 /** §6.4 persistent-pin slice change (S12-T09), 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 /** §16.3 effective-set reporting seam (S12-T03): 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 §6.4 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 * §16.3 stage-4 non-collapsing clusters (S12-T06) — pure derivation, no\n * engine, no DOM, no instance.\n *\n * Contract summary (spec §16.3 / §7.6 stage 4, R-16.3-17..21):\n * - Clusters are a categorical PARTITION of the current PHYSICAL scene, never\n * a rewrite: `deriveClusters` returns membership only. It synthesizes no\n * super-nodes and no meta-edges, so node and edge counts are identical\n * before and after a cluster spec lands (R-16.3-17/19) — clusters coexist\n * with the stage-3 group rewrite by construction.\n * - `null` (and any non-string) from `by` means UNCLUSTERED: the slot carries\n * NaN in {@link ClusterDerivation.slotOrdinals} (the engine contract's\n * \"not in any cluster\" value, mirroring the NaN-position convention).\n * - Missing force centers generate deterministically from the ORDERED cluster\n * keys plus the layout seed (R-16.3-20). {@link generateClusterCenters} uses\n * only IEEE-754 correctly-rounded operations (+ - * / and integer ops) — no\n * transcendentals — so the same ordered keys and seed produce BIT-IDENTICAL\n * centers on any conforming engine, and a different seed produces different\n * ones.\n * - Overlay anchoring must never scan members per frame (R-16.3-21):\n * {@link clusterCentroids} is the ONLY member-scanning anchor path and runs\n * at settle (from the single permitted §7.1 readback) or at commit under a\n * fixed layout. {@link clusterProbe} counts every member visit so tests can\n * pin \"zero per-frame member iterations\".\n */\n\nimport type { GraphDiagnostic, GraphNode, NodeId } from './types';\nimport { DIAGNOSTIC_SAMPLE_CAP } from './types';\n\n// ---------------------------------------------------------------------------\n// Instrumentation (§18 test seam, re-exported from /testing).\n// ---------------------------------------------------------------------------\n\n/**\n * Cluster-work counters. `memberVisits` counts EVERY per-member iteration the\n * cluster lane performs (derivation scans and centroid scans — the only two\n * O(members) passes); `derivations` counts stage-4 recomputations. Both are\n * unconditional O(1) increments so the instrumented and production paths are\n * identical code. Tests snapshot, act, and compare: a per-frame overlay tick\n * must add ZERO member visits (R-16.3-21) and a stage-5 soft-mask change must\n * add ZERO derivations.\n */\nexport const clusterProbe = {\n derivations: 0,\n memberVisits: 0,\n};\n\n/** Reset both counters (per-test isolation). */\nexport function resetClusterProbe(): void {\n clusterProbe.derivations = 0;\n clusterProbe.memberVisits = 0;\n}\n\n// ---------------------------------------------------------------------------\n// Deterministic force-center generation (R-16.3-20).\n// ---------------------------------------------------------------------------\n\n/**\n * The layout seed generated cluster centers key on.\n *\n * FINDING (S12-T06 investigation, recorded here because it is a contract\n * decision): the core owns NO numeric layout seed in v0.10. The only\n * layout-seeding knob on the §13 config contract is\n * `EngineConfigUpdate.seedRadius` (the ring radius unknown/NaN positions seed\n * onto) and `instance.ts` never sets it — every adapter defaults it itself\n * (`CosmosEngine` → `spaceSize / 4`). Spec §10's `LayoutSpec.seed` (the\n * `static` layout's reproducibility seed) is not implemented in v0.10 either.\n * So this constant is the single named place the instance keys generated\n * centers on until §10's normalized layout object lands and can supply\n * `LayoutSpec.seed` here.\n */\nexport const DEFAULT_LAYOUT_SEED = 0x5eed_1234;\n\n/**\n * Radius of the square region generated centers spread over, in SPACE\n * coordinates. Mirrors the role of `EngineConfigUpdate.seedRadius` (see\n * {@link DEFAULT_LAYOUT_SEED}) and matches `CosmosEngine`'s own default\n * (`spaceSize / 4` = 4096 / 4).\n */\nexport const DEFAULT_CLUSTER_CENTER_RADIUS = 1024;\n\n/** FNV-1a over the key mixed with the seed, finished with murmur3's fmix32. */\nfunction hash32(key: string, seed: number): number {\n let h = (0x811c9dc5 ^ (seed | 0)) >>> 0;\n for (let i = 0; i < key.length; i++) {\n h = (h ^ key.charCodeAt(i)) >>> 0;\n h = Math.imul(h, 0x01000193) >>> 0;\n }\n h = (h ^ (h >>> 16)) >>> 0;\n h = Math.imul(h, 0x85ebca6b) >>> 0;\n h = (h ^ (h >>> 13)) >>> 0;\n h = Math.imul(h, 0xc2b2ae35) >>> 0;\n return (h ^ (h >>> 16)) >>> 0;\n}\n\n/** 2^-32 — exact in binary64, so `hash * UNIT` is exact. */\nconst UNIT = 2.3283064365386963e-10;\n\n/** van der Corput radical inverse — integer ops plus one division per digit,\n * all correctly rounded, so the result is reproducible bit-for-bit. */\nfunction radicalInverse(index: number, base: number): number {\n let result = 0;\n let denominator = 1;\n let n = index;\n while (n > 0) {\n denominator *= base;\n result += (n % base) / denominator;\n n = Math.floor(n / base);\n }\n return result;\n}\n\nfunction frac(v: number): number {\n return v - Math.floor(v);\n}\n\n/**\n * Deterministic force centers for ORDERED cluster keys (R-16.3-20): a\n * low-discrepancy (Halton base 2/3) spread offset per key by a seeded hash,\n * mapped into `[-radius, radius]²`.\n *\n * Pure and total: same ordered keys + same seed ⇒ bit-identical output (only\n * IEEE-754 correctly-rounded arithmetic is used — no `Math.sin/cos`, whose\n * results are implementation-defined); a different seed ⇒ different output;\n * reordering or renaming keys ⇒ different output (both the ordinal and the\n * key text feed the placement).\n */\nexport function generateClusterCenters(\n keys: readonly string[],\n seed: number = DEFAULT_LAYOUT_SEED,\n radius: number = DEFAULT_CLUSTER_CENTER_RADIUS,\n): Float32Array {\n const out = new Float32Array(2 * keys.length);\n for (let i = 0; i < keys.length; i++) {\n const key = keys[i]!;\n const u = frac(radicalInverse(i + 1, 2) + hash32(key, seed) * UNIT);\n const v = frac(radicalInverse(i + 1, 3) + hash32(key, (seed ^ 0x9e3779b9) | 0) * UNIT);\n out[2 * i] = radius * (2 * u - 1);\n out[2 * i + 1] = radius * (2 * v - 1);\n }\n return out;\n}\n\n/**\n * Force centers for ordered keys with caller-supplied entries winning:\n * ONLY the missing keys generate (R-16.3-20). A non-finite explicit pair is\n * treated as missing (D4 boundary hygiene — a NaN center would poison the\n * engine's force field).\n */\nexport function resolveClusterCenters(\n keys: readonly string[],\n explicit: ReadonlyMap<string, readonly [number, number]> | undefined,\n seed: number = DEFAULT_LAYOUT_SEED,\n radius: number = DEFAULT_CLUSTER_CENTER_RADIUS,\n): Float32Array {\n const out = generateClusterCenters(keys, seed, radius);\n if (explicit === undefined || explicit.size === 0) return out;\n for (let i = 0; i < keys.length; i++) {\n const pair = explicit.get(keys[i]!);\n if (pair === undefined) continue;\n const x = pair[0];\n const y = pair[1];\n if (!Number.isFinite(x) || !Number.isFinite(y)) continue;\n out[2 * i] = x;\n out[2 * i + 1] = y;\n }\n return out;\n}\n\n// ---------------------------------------------------------------------------\n// Derivation.\n// ---------------------------------------------------------------------------\n\nexport interface ClusterDerivation {\n /** Distinct keys in FIRST-ENCOUNTER order over the physical scene (the §11\n * categorical-domain convention, shared with groupBy). Ordinal i ↔ keys[i]. */\n keys: readonly string[];\n /** key → ordinal (the reverse of `keys`). */\n ordinalByKey: ReadonlyMap<string, number>;\n /** key → member PHYSICAL node ids, scene order. Public ids only. */\n membersByKey: ReadonlyMap<string, readonly NodeId[]>;\n /**\n * PHYSICAL slot → cluster ordinal, NaN = unclustered. Length is the caller's\n * `sceneCount`, so synthetic suffix slots (§16.3 super-nodes/meta-edges) are\n * always NaN: aggregates are never members of a cluster. This IS the engine\n * `config.cluster.pointClusters` payload — no copy at the sink.\n */\n slotOrdinals: Float32Array;\n /** ONE aggregated 'accessor-error' warning when `by` threw (affected nodes\n * derive as unclustered — never silent loss, I3), else null. */\n diagnostic: GraphDiagnostic | null;\n}\n\n/**\n * §16.3 stage-4 derivation over the PHYSICAL prefix of the current scene.\n *\n * `nodes` are the physical rows (post-group-rewrite when a rewrite is live)\n * aligned to scene slots `0..nodes.length-1`; `sceneCount` is the FULL scene\n * point count so the returned `slotOrdinals` covers the synthetic suffix too.\n * Preserves everything and synthesizes nothing (R-16.3-17/19).\n */\nexport function deriveClusters<N>(\n nodes: readonly GraphNode<N>[],\n by: (node: GraphNode<N>) => string | null,\n sceneCount: number = nodes.length,\n): ClusterDerivation {\n clusterProbe.derivations++;\n const keys: string[] = [];\n const ordinalByKey = new Map<string, number>();\n const membersByKey = new Map<string, NodeId[]>();\n const slotOrdinals = new Float32Array(Math.max(sceneCount, nodes.length)).fill(NaN);\n let errorCount = 0;\n const errorSamples: string[] = [];\n\n for (let i = 0; i < nodes.length; i++) {\n clusterProbe.memberVisits++;\n const node = nodes[i]!;\n let raw: string | null;\n try {\n raw = by(node);\n } catch {\n errorCount++;\n if (errorSamples.length < DIAGNOSTIC_SAMPLE_CAP) errorSamples.push(node.id);\n continue;\n }\n if (typeof raw !== 'string') continue; // null / non-string ⇒ unclustered\n let ordinal = ordinalByKey.get(raw);\n if (ordinal === undefined) {\n ordinal = keys.length;\n keys.push(raw);\n ordinalByKey.set(raw, ordinal);\n membersByKey.set(raw, []);\n }\n membersByKey.get(raw)!.push(node.id);\n slotOrdinals[i] = ordinal;\n }\n\n return {\n keys,\n ordinalByKey,\n membersByKey,\n slotOrdinals,\n diagnostic:\n errorCount === 0\n ? null\n : {\n code: 'accessor-error',\n severity: 'warning',\n count: errorCount,\n sampleIds: errorSamples,\n message: 'clusters.by threw; the affected nodes derived as unclustered (§16.3/§8)',\n },\n };\n}\n\n/**\n * Centroids of every cluster from a slot-aligned position buffer — the ONLY\n * member-scanning anchor path (R-16.3-21). Called at settle (over the single\n * permitted §7.1 readback) and at commit under a FIXED layout; never per\n * frame. Slots with unknown (NaN) positions are skipped; a cluster with no\n * placeable member keeps its `fallback` entry (its force center), so a label\n * never jumps to the origin.\n */\nexport function clusterCentroids(\n slotOrdinals: Float32Array,\n positions: Float32Array,\n clusterCount: number,\n fallback: Float32Array,\n): Float32Array {\n const out = new Float32Array(2 * clusterCount);\n const counts = new Float64Array(clusterCount);\n const slots = Math.min(slotOrdinals.length, Math.floor(positions.length / 2));\n for (let i = 0; i < slots; i++) {\n const ordinal = slotOrdinals[i]!;\n if (Number.isNaN(ordinal)) continue;\n clusterProbe.memberVisits++;\n const x = positions[2 * i]!;\n const y = positions[2 * i + 1]!;\n if (Number.isNaN(x) || Number.isNaN(y)) continue;\n out[2 * ordinal] = out[2 * ordinal]! + x;\n out[2 * ordinal + 1] = out[2 * ordinal + 1]! + y;\n counts[ordinal] = counts[ordinal]! + 1;\n }\n for (let c = 0; c < clusterCount; c++) {\n const n = counts[c]!;\n if (n === 0) {\n out[2 * c] = fallback[2 * c] ?? 0;\n out[2 * c + 1] = fallback[2 * c + 1] ?? 0;\n continue;\n }\n out[2 * c] = out[2 * c]! / n;\n out[2 * c + 1] = out[2 * c + 1]! / n;\n }\n return out;\n}\n"]}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { N as NodeId, a as GraphDiagnostic, b as GraphNode } from './index-BPjuELfY.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* §16.3 stage-4 non-collapsing clusters (S12-T06) — pure derivation, no
|
|
5
|
+
* engine, no DOM, no instance.
|
|
6
|
+
*
|
|
7
|
+
* Contract summary (spec §16.3 / §7.6 stage 4, R-16.3-17..21):
|
|
8
|
+
* - Clusters are a categorical PARTITION of the current PHYSICAL scene, never
|
|
9
|
+
* a rewrite: `deriveClusters` returns membership only. It synthesizes no
|
|
10
|
+
* super-nodes and no meta-edges, so node and edge counts are identical
|
|
11
|
+
* before and after a cluster spec lands (R-16.3-17/19) — clusters coexist
|
|
12
|
+
* with the stage-3 group rewrite by construction.
|
|
13
|
+
* - `null` (and any non-string) from `by` means UNCLUSTERED: the slot carries
|
|
14
|
+
* NaN in {@link ClusterDerivation.slotOrdinals} (the engine contract's
|
|
15
|
+
* "not in any cluster" value, mirroring the NaN-position convention).
|
|
16
|
+
* - Missing force centers generate deterministically from the ORDERED cluster
|
|
17
|
+
* keys plus the layout seed (R-16.3-20). {@link generateClusterCenters} uses
|
|
18
|
+
* only IEEE-754 correctly-rounded operations (+ - * / and integer ops) — no
|
|
19
|
+
* transcendentals — so the same ordered keys and seed produce BIT-IDENTICAL
|
|
20
|
+
* centers on any conforming engine, and a different seed produces different
|
|
21
|
+
* ones.
|
|
22
|
+
* - Overlay anchoring must never scan members per frame (R-16.3-21):
|
|
23
|
+
* {@link clusterCentroids} is the ONLY member-scanning anchor path and runs
|
|
24
|
+
* at settle (from the single permitted §7.1 readback) or at commit under a
|
|
25
|
+
* fixed layout. {@link clusterProbe} counts every member visit so tests can
|
|
26
|
+
* pin "zero per-frame member iterations".
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Cluster-work counters. `memberVisits` counts EVERY per-member iteration the
|
|
31
|
+
* cluster lane performs (derivation scans and centroid scans — the only two
|
|
32
|
+
* O(members) passes); `derivations` counts stage-4 recomputations. Both are
|
|
33
|
+
* unconditional O(1) increments so the instrumented and production paths are
|
|
34
|
+
* identical code. Tests snapshot, act, and compare: a per-frame overlay tick
|
|
35
|
+
* must add ZERO member visits (R-16.3-21) and a stage-5 soft-mask change must
|
|
36
|
+
* add ZERO derivations.
|
|
37
|
+
*/
|
|
38
|
+
declare const clusterProbe: {
|
|
39
|
+
derivations: number;
|
|
40
|
+
memberVisits: number;
|
|
41
|
+
};
|
|
42
|
+
/** Reset both counters (per-test isolation). */
|
|
43
|
+
declare function resetClusterProbe(): void;
|
|
44
|
+
/**
|
|
45
|
+
* The layout seed generated cluster centers key on.
|
|
46
|
+
*
|
|
47
|
+
* FINDING (S12-T06 investigation, recorded here because it is a contract
|
|
48
|
+
* decision): the core owns NO numeric layout seed in v0.10. The only
|
|
49
|
+
* layout-seeding knob on the §13 config contract is
|
|
50
|
+
* `EngineConfigUpdate.seedRadius` (the ring radius unknown/NaN positions seed
|
|
51
|
+
* onto) and `instance.ts` never sets it — every adapter defaults it itself
|
|
52
|
+
* (`CosmosEngine` → `spaceSize / 4`). Spec §10's `LayoutSpec.seed` (the
|
|
53
|
+
* `static` layout's reproducibility seed) is not implemented in v0.10 either.
|
|
54
|
+
* So this constant is the single named place the instance keys generated
|
|
55
|
+
* centers on until §10's normalized layout object lands and can supply
|
|
56
|
+
* `LayoutSpec.seed` here.
|
|
57
|
+
*/
|
|
58
|
+
declare const DEFAULT_LAYOUT_SEED = 1592594996;
|
|
59
|
+
/**
|
|
60
|
+
* Radius of the square region generated centers spread over, in SPACE
|
|
61
|
+
* coordinates. Mirrors the role of `EngineConfigUpdate.seedRadius` (see
|
|
62
|
+
* {@link DEFAULT_LAYOUT_SEED}) and matches `CosmosEngine`'s own default
|
|
63
|
+
* (`spaceSize / 4` = 4096 / 4).
|
|
64
|
+
*/
|
|
65
|
+
declare const DEFAULT_CLUSTER_CENTER_RADIUS = 1024;
|
|
66
|
+
/**
|
|
67
|
+
* Deterministic force centers for ORDERED cluster keys (R-16.3-20): a
|
|
68
|
+
* low-discrepancy (Halton base 2/3) spread offset per key by a seeded hash,
|
|
69
|
+
* mapped into `[-radius, radius]²`.
|
|
70
|
+
*
|
|
71
|
+
* Pure and total: same ordered keys + same seed ⇒ bit-identical output (only
|
|
72
|
+
* IEEE-754 correctly-rounded arithmetic is used — no `Math.sin/cos`, whose
|
|
73
|
+
* results are implementation-defined); a different seed ⇒ different output;
|
|
74
|
+
* reordering or renaming keys ⇒ different output (both the ordinal and the
|
|
75
|
+
* key text feed the placement).
|
|
76
|
+
*/
|
|
77
|
+
declare function generateClusterCenters(keys: readonly string[], seed?: number, radius?: number): Float32Array;
|
|
78
|
+
/**
|
|
79
|
+
* Force centers for ordered keys with caller-supplied entries winning:
|
|
80
|
+
* ONLY the missing keys generate (R-16.3-20). A non-finite explicit pair is
|
|
81
|
+
* treated as missing (D4 boundary hygiene — a NaN center would poison the
|
|
82
|
+
* engine's force field).
|
|
83
|
+
*/
|
|
84
|
+
declare function resolveClusterCenters(keys: readonly string[], explicit: ReadonlyMap<string, readonly [number, number]> | undefined, seed?: number, radius?: number): Float32Array;
|
|
85
|
+
interface ClusterDerivation {
|
|
86
|
+
/** Distinct keys in FIRST-ENCOUNTER order over the physical scene (the §11
|
|
87
|
+
* categorical-domain convention, shared with groupBy). Ordinal i ↔ keys[i]. */
|
|
88
|
+
keys: readonly string[];
|
|
89
|
+
/** key → ordinal (the reverse of `keys`). */
|
|
90
|
+
ordinalByKey: ReadonlyMap<string, number>;
|
|
91
|
+
/** key → member PHYSICAL node ids, scene order. Public ids only. */
|
|
92
|
+
membersByKey: ReadonlyMap<string, readonly NodeId[]>;
|
|
93
|
+
/**
|
|
94
|
+
* PHYSICAL slot → cluster ordinal, NaN = unclustered. Length is the caller's
|
|
95
|
+
* `sceneCount`, so synthetic suffix slots (§16.3 super-nodes/meta-edges) are
|
|
96
|
+
* always NaN: aggregates are never members of a cluster. This IS the engine
|
|
97
|
+
* `config.cluster.pointClusters` payload — no copy at the sink.
|
|
98
|
+
*/
|
|
99
|
+
slotOrdinals: Float32Array;
|
|
100
|
+
/** ONE aggregated 'accessor-error' warning when `by` threw (affected nodes
|
|
101
|
+
* derive as unclustered — never silent loss, I3), else null. */
|
|
102
|
+
diagnostic: GraphDiagnostic | null;
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* §16.3 stage-4 derivation over the PHYSICAL prefix of the current scene.
|
|
106
|
+
*
|
|
107
|
+
* `nodes` are the physical rows (post-group-rewrite when a rewrite is live)
|
|
108
|
+
* aligned to scene slots `0..nodes.length-1`; `sceneCount` is the FULL scene
|
|
109
|
+
* point count so the returned `slotOrdinals` covers the synthetic suffix too.
|
|
110
|
+
* Preserves everything and synthesizes nothing (R-16.3-17/19).
|
|
111
|
+
*/
|
|
112
|
+
declare function deriveClusters<N>(nodes: readonly GraphNode<N>[], by: (node: GraphNode<N>) => string | null, sceneCount?: number): ClusterDerivation;
|
|
113
|
+
/**
|
|
114
|
+
* Centroids of every cluster from a slot-aligned position buffer — the ONLY
|
|
115
|
+
* member-scanning anchor path (R-16.3-21). Called at settle (over the single
|
|
116
|
+
* permitted §7.1 readback) and at commit under a FIXED layout; never per
|
|
117
|
+
* frame. Slots with unknown (NaN) positions are skipped; a cluster with no
|
|
118
|
+
* placeable member keeps its `fallback` entry (its force center), so a label
|
|
119
|
+
* never jumps to the origin.
|
|
120
|
+
*/
|
|
121
|
+
declare function clusterCentroids(slotOrdinals: Float32Array, positions: Float32Array, clusterCount: number, fallback: Float32Array): Float32Array;
|
|
122
|
+
|
|
123
|
+
export { type ClusterDerivation as C, DEFAULT_CLUSTER_CENTER_RADIUS as D, DEFAULT_LAYOUT_SEED as a, clusterProbe as b, clusterCentroids as c, deriveClusters as d, resetClusterProbe as e, generateClusterCenters as g, resolveClusterCenters as r };
|
package/dist/engine.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { a2 as EngineBufferChannel, a5 as EngineCapabilities, a4 as EngineCommit, aC as EngineConfigUpdate, aD as EngineContextEvent, aB as EngineDiagnostic, E as EngineFactory, az as EngineHostEvents, aA as FitViewOptions, a3 as GraphEngine } from './index-BPjuELfY.js';
|
package/dist/engine.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":[],"names":[],"mappings":"","file":"engine.js"}
|