@modernrelay/orbit-core 0.2.0 → 0.13.5
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/chunk-GUXIITKK.js +89 -0
- package/dist/chunk-GUXIITKK.js.map +1 -0
- package/dist/chunk-NIJX5NVJ.js +212 -0
- package/dist/chunk-NIJX5NVJ.js.map +1 -0
- package/dist/{chunk-Z7FOEASL.js → chunk-XAR262L3.js} +4 -5
- package/dist/chunk-XAR262L3.js.map +1 -0
- package/dist/engine.d.ts +1 -1
- package/dist/{index-BPjuELfY.d.ts → index-CFZMson3.d.ts} +201 -4
- package/dist/index.d.ts +364 -4
- package/dist/index.js +1690 -44
- package/dist/index.js.map +1 -1
- package/dist/{clusters-ExqnvobT.d.ts → lane-CHHLuxgq.d.ts} +95 -2
- package/dist/testing.d.ts +54 -3
- package/dist/testing.js +115 -7
- package/dist/testing.js.map +1 -1
- package/dist/worker/entry.d.ts +2 -0
- package/dist/worker/entry.js +12 -0
- package/dist/worker/entry.js.map +1 -0
- package/package.json +1 -1
- package/dist/chunk-Z7FOEASL.js.map +0 -1
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { isWellFormedEnvelope, decodeStringTable, acceptColumnar, collectTransfers } from './chunk-NIJX5NVJ.js';
|
|
2
|
+
|
|
3
|
+
// src/worker/runtime.ts
|
|
4
|
+
function handleWorkerRequest(request, sequencer) {
|
|
5
|
+
if (!isWellFormedEnvelope(request)) {
|
|
6
|
+
const reply2 = sequencer.make(0, "scene", "error", {
|
|
7
|
+
message: "malformed envelope (protocol violation)"
|
|
8
|
+
});
|
|
9
|
+
return { reply: reply2, transfers: [] };
|
|
10
|
+
}
|
|
11
|
+
if (request.op === "derive-columnar") {
|
|
12
|
+
try {
|
|
13
|
+
const p = request.payload;
|
|
14
|
+
const snapshot = {
|
|
15
|
+
kind: "columnar",
|
|
16
|
+
datasetKey: "worker",
|
|
17
|
+
// acceptance rules never read the coordinate
|
|
18
|
+
sourceRevision: 0,
|
|
19
|
+
nodes: {
|
|
20
|
+
ids: {
|
|
21
|
+
kind: "string",
|
|
22
|
+
dictionary: decodeStringTable(p.nodeIdTable),
|
|
23
|
+
codes: p.nodeIdCodes
|
|
24
|
+
},
|
|
25
|
+
columns: {},
|
|
26
|
+
length: p.nodeCount
|
|
27
|
+
},
|
|
28
|
+
edges: {
|
|
29
|
+
ids: {
|
|
30
|
+
kind: "string",
|
|
31
|
+
dictionary: decodeStringTable(p.edgeIdTable),
|
|
32
|
+
codes: p.edgeIdCodes
|
|
33
|
+
},
|
|
34
|
+
source: p.edgeSource,
|
|
35
|
+
target: p.edgeTarget,
|
|
36
|
+
columns: {},
|
|
37
|
+
length: p.edgeCount
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
const acceptance = acceptColumnar(snapshot);
|
|
41
|
+
const result = {
|
|
42
|
+
keepNodes: acceptance.keepNodes,
|
|
43
|
+
keepEdges: acceptance.keepEdges,
|
|
44
|
+
acceptedNodeCount: acceptance.acceptedNodeCount,
|
|
45
|
+
acceptedEdgeCount: acceptance.acceptedEdgeCount,
|
|
46
|
+
nodeAcceptedIndex: acceptance.nodeAcceptedIndex,
|
|
47
|
+
links: acceptance.links,
|
|
48
|
+
diagnostics: acceptance.diagnostics
|
|
49
|
+
};
|
|
50
|
+
const reply2 = sequencer.make(
|
|
51
|
+
request.epoch,
|
|
52
|
+
request.entity,
|
|
53
|
+
"result",
|
|
54
|
+
result,
|
|
55
|
+
request.msgId
|
|
56
|
+
);
|
|
57
|
+
return {
|
|
58
|
+
reply: reply2,
|
|
59
|
+
transfers: collectTransfers([
|
|
60
|
+
result.keepNodes,
|
|
61
|
+
result.keepEdges,
|
|
62
|
+
result.nodeAcceptedIndex,
|
|
63
|
+
result.links
|
|
64
|
+
])
|
|
65
|
+
};
|
|
66
|
+
} catch (err) {
|
|
67
|
+
const reply2 = sequencer.make(
|
|
68
|
+
request.epoch,
|
|
69
|
+
request.entity,
|
|
70
|
+
"error",
|
|
71
|
+
{ message: err instanceof Error ? err.message : String(err) },
|
|
72
|
+
request.msgId
|
|
73
|
+
);
|
|
74
|
+
return { reply: reply2, transfers: [] };
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
const reply = sequencer.make(
|
|
78
|
+
request.epoch,
|
|
79
|
+
request.entity,
|
|
80
|
+
"error",
|
|
81
|
+
{ message: `unknown op '${request.op}'` },
|
|
82
|
+
request.msgId
|
|
83
|
+
);
|
|
84
|
+
return { reply, transfers: [] };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export { handleWorkerRequest };
|
|
88
|
+
//# sourceMappingURL=chunk-GUXIITKK.js.map
|
|
89
|
+
//# sourceMappingURL=chunk-GUXIITKK.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/worker/runtime.ts"],"names":["reply"],"mappings":";;;AAyDO,SAAS,mBAAA,CACd,SACA,SAAA,EACc;AACd,EAAA,IAAI,CAAC,oBAAA,CAAqB,OAAO,CAAA,EAAG;AAClC,IAAA,MAAMA,MAAAA,GAAQ,SAAA,CAAU,IAAA,CAAK,CAAA,EAAG,SAAS,OAAA,EAAS;AAAA,MAChD,OAAA,EAAS;AAAA,KACV,CAAA;AACD,IAAA,OAAO,EAAE,KAAA,EAAAA,MAAAA,EAAO,SAAA,EAAW,EAAC,EAAE;AAAA,EAChC;AAEA,EAAA,IAAI,OAAA,CAAQ,OAAO,iBAAA,EAAmB;AACpC,IAAA,IAAI;AACF,MAAA,MAAM,IAAI,OAAA,CAAQ,OAAA;AAGlB,MAAA,MAAM,QAAA,GAAoD;AAAA,QACxD,IAAA,EAAM,UAAA;AAAA,QACN,UAAA,EAAY,QAAA;AAAA;AAAA,QACZ,cAAA,EAAgB,CAAA;AAAA,QAChB,KAAA,EAAO;AAAA,UACL,GAAA,EAAK;AAAA,YACH,IAAA,EAAM,QAAA;AAAA,YACN,UAAA,EAAY,iBAAA,CAAkB,CAAA,CAAE,WAAW,CAAA;AAAA,YAC3C,OAAO,CAAA,CAAE;AAAA,WACX;AAAA,UACA,SAAS,EAAC;AAAA,UACV,QAAQ,CAAA,CAAE;AAAA,SACZ;AAAA,QACA,KAAA,EAAO;AAAA,UACL,GAAA,EAAK;AAAA,YACH,IAAA,EAAM,QAAA;AAAA,YACN,UAAA,EAAY,iBAAA,CAAkB,CAAA,CAAE,WAAW,CAAA;AAAA,YAC3C,OAAO,CAAA,CAAE;AAAA,WACX;AAAA,UACA,QAAQ,CAAA,CAAE,UAAA;AAAA,UACV,QAAQ,CAAA,CAAE,UAAA;AAAA,UACV,SAAS,EAAC;AAAA,UACV,QAAQ,CAAA,CAAE;AAAA;AACZ,OACF;AACA,MAAA,MAAM,UAAA,GAAa,eAAe,QAAQ,CAAA;AAC1C,MAAA,MAAM,MAAA,GAA+B;AAAA,QACnC,WAAW,UAAA,CAAW,SAAA;AAAA,QACtB,WAAW,UAAA,CAAW,SAAA;AAAA,QACtB,mBAAmB,UAAA,CAAW,iBAAA;AAAA,QAC9B,mBAAmB,UAAA,CAAW,iBAAA;AAAA,QAC9B,mBAAmB,UAAA,CAAW,iBAAA;AAAA,QAC9B,OAAO,UAAA,CAAW,KAAA;AAAA,QAClB,aAAa,UAAA,CAAW;AAAA,OAC1B;AACA,MAAA,MAAMA,SAAQ,SAAA,CAAU,IAAA;AAAA,QACtB,OAAA,CAAQ,KAAA;AAAA,QACR,OAAA,CAAQ,MAAA;AAAA,QACR,QAAA;AAAA,QACA,MAAA;AAAA,QACA,OAAA,CAAQ;AAAA,OACV;AACA,MAAA,OAAO;AAAA,QACL,KAAA,EAAAA,MAAAA;AAAA,QACA,WAAW,gBAAA,CAAiB;AAAA,UAC1B,MAAA,CAAO,SAAA;AAAA,UACP,MAAA,CAAO,SAAA;AAAA,UACP,MAAA,CAAO,iBAAA;AAAA,UACP,MAAA,CAAO;AAAA,SACR;AAAA,OACH;AAAA,IACF,SAAS,GAAA,EAAK;AACZ,MAAA,MAAMA,SAAQ,SAAA,CAAU,IAAA;AAAA,QACtB,OAAA,CAAQ,KAAA;AAAA,QACR,OAAA,CAAQ,MAAA;AAAA,QACR,OAAA;AAAA,QACA,EAAE,SAAS,GAAA,YAAe,KAAA,GAAQ,IAAI,OAAA,GAAU,MAAA,CAAO,GAAG,CAAA,EAAE;AAAA,QAC5D,OAAA,CAAQ;AAAA,OACV;AACA,MAAA,OAAO,EAAE,KAAA,EAAAA,MAAAA,EAAO,SAAA,EAAW,EAAC,EAAE;AAAA,IAChC;AAAA,EACF;AAEA,EAAA,MAAM,QAAQ,SAAA,CAAU,IAAA;AAAA,IACtB,OAAA,CAAQ,KAAA;AAAA,IACR,OAAA,CAAQ,MAAA;AAAA,IACR,OAAA;AAAA,IACA,EAAE,OAAA,EAAS,CAAA,YAAA,EAAe,OAAA,CAAQ,EAAE,CAAA,CAAA,CAAA,EAAI;AAAA,IACxC,OAAA,CAAQ;AAAA,GACV;AACA,EAAA,OAAO,EAAE,KAAA,EAAO,SAAA,EAAW,EAAC,EAAE;AAChC","file":"chunk-GUXIITKK.js","sourcesContent":["/**\n * ADR-006 — the worker-side request handler, as a PURE function over the D3\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 (PR-F): `derive-columnar` — the §5.1 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"]}
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
// src/types.ts
|
|
2
|
+
var DIAGNOSTIC_SAMPLE_CAP = 10;
|
|
3
|
+
|
|
4
|
+
// src/workerProtocol.ts
|
|
5
|
+
var EnvelopeSequencer = class {
|
|
6
|
+
nextId = 1;
|
|
7
|
+
make(epoch, entity, op, payload, inReplyTo) {
|
|
8
|
+
const envelope = { msgId: this.nextId, epoch, entity, op, payload };
|
|
9
|
+
this.nextId += 1;
|
|
10
|
+
if (inReplyTo !== void 0) envelope.inReplyTo = inReplyTo;
|
|
11
|
+
return envelope;
|
|
12
|
+
}
|
|
13
|
+
};
|
|
14
|
+
function judgeEpoch(envelope, currentEpoch) {
|
|
15
|
+
if (envelope.epoch === currentEpoch) return "accept";
|
|
16
|
+
if (envelope.epoch < currentEpoch) return "stale";
|
|
17
|
+
return "protocol-violation";
|
|
18
|
+
}
|
|
19
|
+
function collectTransfers(views) {
|
|
20
|
+
const seen = /* @__PURE__ */ new Set();
|
|
21
|
+
const out = [];
|
|
22
|
+
for (const view of views) {
|
|
23
|
+
const buffer = view.buffer;
|
|
24
|
+
if (!(buffer instanceof ArrayBuffer)) continue;
|
|
25
|
+
if (seen.has(buffer)) continue;
|
|
26
|
+
seen.add(buffer);
|
|
27
|
+
out.push(buffer);
|
|
28
|
+
}
|
|
29
|
+
return out;
|
|
30
|
+
}
|
|
31
|
+
var RequestLedger = class {
|
|
32
|
+
pending = /* @__PURE__ */ new Map();
|
|
33
|
+
/** Register an outbound request. A throttled request SUPERSEDES any
|
|
34
|
+
* pending request on the same lane: the old one is aborted and forgotten
|
|
35
|
+
* (its eventual reply will be dropped as unmatched). Returns the signal
|
|
36
|
+
* the transport should honor. */
|
|
37
|
+
track(envelope, klass, lane) {
|
|
38
|
+
if (klass === "throttled") {
|
|
39
|
+
for (const [id, entry] of this.pending) {
|
|
40
|
+
if (entry.klass === "throttled" && entry.lane === lane) {
|
|
41
|
+
entry.controller.abort();
|
|
42
|
+
this.pending.delete(id);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
const controller = new AbortController();
|
|
47
|
+
this.pending.set(envelope.msgId, { envelope, klass, lane, controller });
|
|
48
|
+
return controller.signal;
|
|
49
|
+
}
|
|
50
|
+
/** Match an arriving reply to its request. Returns the original request
|
|
51
|
+
* envelope, or null when the request was superseded/aborted (drop the
|
|
52
|
+
* reply — it answers work nobody wants anymore). */
|
|
53
|
+
settle(reply) {
|
|
54
|
+
if (reply.inReplyTo === void 0) return null;
|
|
55
|
+
const entry = this.pending.get(reply.inReplyTo);
|
|
56
|
+
if (entry === void 0) return null;
|
|
57
|
+
this.pending.delete(reply.inReplyTo);
|
|
58
|
+
if (entry.controller.signal.aborted) return null;
|
|
59
|
+
return entry.envelope;
|
|
60
|
+
}
|
|
61
|
+
/** Abort EVERYTHING (epoch advance / detach / dataset swap). */
|
|
62
|
+
abortAll() {
|
|
63
|
+
let aborted = 0;
|
|
64
|
+
for (const entry of this.pending.values()) {
|
|
65
|
+
entry.controller.abort();
|
|
66
|
+
aborted += 1;
|
|
67
|
+
}
|
|
68
|
+
this.pending.clear();
|
|
69
|
+
return aborted;
|
|
70
|
+
}
|
|
71
|
+
pendingCount() {
|
|
72
|
+
return this.pending.size;
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
function encodeStringTable(strings) {
|
|
76
|
+
const encoder = new TextEncoder();
|
|
77
|
+
const chunks = new Array(strings.length);
|
|
78
|
+
const offsets = new Uint32Array(strings.length + 1);
|
|
79
|
+
let total = 0;
|
|
80
|
+
for (let i = 0; i < strings.length; i++) {
|
|
81
|
+
const chunk = encoder.encode(strings[i]);
|
|
82
|
+
chunks[i] = chunk;
|
|
83
|
+
total += chunk.length;
|
|
84
|
+
offsets[i + 1] = total;
|
|
85
|
+
}
|
|
86
|
+
const bytes = new Uint8Array(total);
|
|
87
|
+
for (let i = 0; i < strings.length; i++) bytes.set(chunks[i], offsets[i]);
|
|
88
|
+
return { offsets, bytes };
|
|
89
|
+
}
|
|
90
|
+
function decodeStringTable(table) {
|
|
91
|
+
const decoder = new TextDecoder();
|
|
92
|
+
const n = table.offsets.length - 1;
|
|
93
|
+
const out = new Array(n);
|
|
94
|
+
for (let i = 0; i < n; i++) {
|
|
95
|
+
out[i] = decoder.decode(table.bytes.subarray(table.offsets[i], table.offsets[i + 1]));
|
|
96
|
+
}
|
|
97
|
+
return out;
|
|
98
|
+
}
|
|
99
|
+
function isWellFormedEnvelope(value) {
|
|
100
|
+
if (value === null || typeof value !== "object") return false;
|
|
101
|
+
const e = value;
|
|
102
|
+
return typeof e.msgId === "number" && Number.isInteger(e.msgId) && e.msgId > 0 && typeof e.epoch === "number" && Number.isInteger(e.epoch) && e.epoch >= 0 && (e.entity === "nodes" || e.entity === "edges" || e.entity === "scene") && typeof e.op === "string" && e.op.length > 0 && (e.inReplyTo === void 0 || Number.isInteger(e.inReplyTo) && e.inReplyTo > 0);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// src/columnarValidate.ts
|
|
106
|
+
function canonicalizeDictionary(dictionary) {
|
|
107
|
+
const canonical = new Uint32Array(dictionary.length);
|
|
108
|
+
const firstByString = /* @__PURE__ */ new Map();
|
|
109
|
+
for (let d = 0; d < dictionary.length; d++) {
|
|
110
|
+
const existing = firstByString.get(dictionary[d]);
|
|
111
|
+
if (existing === void 0) {
|
|
112
|
+
firstByString.set(dictionary[d], d);
|
|
113
|
+
canonical[d] = d;
|
|
114
|
+
} else {
|
|
115
|
+
canonical[d] = existing;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return canonical;
|
|
119
|
+
}
|
|
120
|
+
function record(tally, sample) {
|
|
121
|
+
tally.count++;
|
|
122
|
+
if (tally.samples.length < DIAGNOSTIC_SAMPLE_CAP) tally.samples.push(sample);
|
|
123
|
+
}
|
|
124
|
+
function pushDiagnostic(out, code, severity, tally, message) {
|
|
125
|
+
if (tally.count === 0) return;
|
|
126
|
+
out.push({ code, severity, count: tally.count, sampleIds: tally.samples, message });
|
|
127
|
+
}
|
|
128
|
+
function acceptColumnar(snapshot) {
|
|
129
|
+
const nodeIds = snapshot.nodes.ids;
|
|
130
|
+
const edgeIds = snapshot.edges.ids;
|
|
131
|
+
const nodeRows = snapshot.nodes.length;
|
|
132
|
+
const edgeRows = snapshot.edges.length;
|
|
133
|
+
const duplicateNode = { count: 0, samples: [] };
|
|
134
|
+
const duplicateEdge = { count: 0, samples: [] };
|
|
135
|
+
const selfLoop = { count: 0, samples: [] };
|
|
136
|
+
const nodeCanonical = canonicalizeDictionary(nodeIds.dictionary);
|
|
137
|
+
const keepNodes = new Uint8Array(nodeRows);
|
|
138
|
+
const nodeAcceptedIndex = new Int32Array(nodeRows).fill(-1);
|
|
139
|
+
const acceptedByCanonical = new Int32Array(nodeIds.dictionary.length).fill(-1);
|
|
140
|
+
let acceptedNodeCount = 0;
|
|
141
|
+
for (let i = 0; i < nodeRows; i++) {
|
|
142
|
+
const canonical = nodeCanonical[nodeIds.codes[i]];
|
|
143
|
+
const survivor = acceptedByCanonical[canonical];
|
|
144
|
+
if (survivor === -1) {
|
|
145
|
+
acceptedByCanonical[canonical] = acceptedNodeCount;
|
|
146
|
+
nodeAcceptedIndex[i] = acceptedNodeCount;
|
|
147
|
+
keepNodes[i] = 1;
|
|
148
|
+
acceptedNodeCount += 1;
|
|
149
|
+
} else {
|
|
150
|
+
nodeAcceptedIndex[i] = survivor;
|
|
151
|
+
record(duplicateNode, nodeIds.dictionary[nodeIds.codes[i]]);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
const edgeCanonical = canonicalizeDictionary(edgeIds.dictionary);
|
|
155
|
+
const keepEdges = new Uint8Array(edgeRows);
|
|
156
|
+
const seenEdgeByCanonical = new Uint8Array(edgeIds.dictionary.length);
|
|
157
|
+
const { source, target } = snapshot.edges;
|
|
158
|
+
const linksOut = new Uint32Array(edgeRows * 2);
|
|
159
|
+
let acceptedEdgeCount = 0;
|
|
160
|
+
for (let e = 0; e < edgeRows; e++) {
|
|
161
|
+
const canonical = edgeCanonical[edgeIds.codes[e]];
|
|
162
|
+
if (seenEdgeByCanonical[canonical] !== 0) {
|
|
163
|
+
record(duplicateEdge, edgeIds.dictionary[edgeIds.codes[e]]);
|
|
164
|
+
continue;
|
|
165
|
+
}
|
|
166
|
+
seenEdgeByCanonical[canonical] = 1;
|
|
167
|
+
const s = nodeAcceptedIndex[source[e]];
|
|
168
|
+
const t = nodeAcceptedIndex[target[e]];
|
|
169
|
+
if (s === t) {
|
|
170
|
+
record(selfLoop, nodeIds.dictionary[nodeIds.codes[source[e]]]);
|
|
171
|
+
}
|
|
172
|
+
keepEdges[e] = 1;
|
|
173
|
+
linksOut[acceptedEdgeCount * 2] = s;
|
|
174
|
+
linksOut[acceptedEdgeCount * 2 + 1] = t;
|
|
175
|
+
acceptedEdgeCount += 1;
|
|
176
|
+
}
|
|
177
|
+
const diagnostics = [];
|
|
178
|
+
pushDiagnostic(
|
|
179
|
+
diagnostics,
|
|
180
|
+
"duplicate-node-id",
|
|
181
|
+
"warning",
|
|
182
|
+
duplicateNode,
|
|
183
|
+
`${duplicateNode.count} duplicate node id(s) dropped (first occurrence wins)`
|
|
184
|
+
);
|
|
185
|
+
pushDiagnostic(
|
|
186
|
+
diagnostics,
|
|
187
|
+
"duplicate-edge-id",
|
|
188
|
+
"warning",
|
|
189
|
+
duplicateEdge,
|
|
190
|
+
`${duplicateEdge.count} duplicate edge id(s) dropped (first occurrence wins)`
|
|
191
|
+
);
|
|
192
|
+
pushDiagnostic(
|
|
193
|
+
diagnostics,
|
|
194
|
+
"self-loop-retained",
|
|
195
|
+
"info",
|
|
196
|
+
selfLoop,
|
|
197
|
+
`${selfLoop.count} self-loop edge(s) retained`
|
|
198
|
+
);
|
|
199
|
+
return {
|
|
200
|
+
keepNodes,
|
|
201
|
+
keepEdges,
|
|
202
|
+
acceptedNodeCount,
|
|
203
|
+
acceptedEdgeCount,
|
|
204
|
+
nodeAcceptedIndex,
|
|
205
|
+
links: linksOut.subarray(0, acceptedEdgeCount * 2).slice(),
|
|
206
|
+
diagnostics
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
export { DIAGNOSTIC_SAMPLE_CAP, EnvelopeSequencer, RequestLedger, acceptColumnar, collectTransfers, decodeStringTable, encodeStringTable, isWellFormedEnvelope, judgeEpoch };
|
|
211
|
+
//# sourceMappingURL=chunk-NIJX5NVJ.js.map
|
|
212
|
+
//# sourceMappingURL=chunk-NIJX5NVJ.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/types.ts","../src/workerProtocol.ts","../src/columnarValidate.ts"],"names":[],"mappings":"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","file":"chunk-NIJX5NVJ.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 /** §5.1 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 /** ADR-006 D5: 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 (§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 * @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 (§6): 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// §5 columnar snapshots (S13-T01, ADR-006) — 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 (ADR-006 D4); 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 /** §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 /** §13 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; §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 /** §13 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// §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\n// ---------------------------------------------------------------------------\n// §17 telemetry + degradation ladder (S13-T07/T08). Spec-verbatim shapes.\n// ---------------------------------------------------------------------------\n\n/** §13 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/** §17 performance snapshot — NEVER carries raw attrs or ids (§17). */\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 /** §17 pressure-sampler mirror (S13-T07): EWMA of per-window mean frame\n * deltas, dropped-frame count, and idle wakeups since the last sample —\n * 0 idle wakeups is the healthy reading under the ADR-005 gated clock. */\n pressure: {\n frameEwmaMs: number;\n droppedFrames: number;\n idleWakeups: number;\n };\n}\n\n/** §17/§6.1 `limits` — construction-time thresholds for the ladder (D7:\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 (§17). */\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 /** §17 throttled telemetry sample — never per frame (S13-T07). */\n perfSample: GraphPerfSnapshot;\n /** §17 ladder step engagement/disengagement (S13-T08). Notification\n * pattern, not §6.4-controlled. */\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 /** §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 * ADR-006 D3 — the worker-lane wire contract: 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 * §5.1 columnar-NATIVE acceptance rules (PR-F wave 1, ADR-006) — the column\n * -domain twin of validate.ts, 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\n // --- Nodes: first occurrence per canonical id wins. -----------------------\n const nodeCanonical = canonicalizeDictionary(nodeIds.dictionary);\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 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 === 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 'duplicate-node-id',\n 'warning',\n duplicateNode,\n `${duplicateNode.count} duplicate node id(s) dropped (first occurrence wins)`,\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"]}
|
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
var DIAGNOSTIC_SAMPLE_CAP = 10;
|
|
1
|
+
import { DIAGNOSTIC_SAMPLE_CAP } from './chunk-NIJX5NVJ.js';
|
|
3
2
|
|
|
4
3
|
// src/clusters.ts
|
|
5
4
|
var clusterProbe = {
|
|
@@ -136,6 +135,6 @@ function clusterCentroids(slotOrdinals, positions, clusterCount, fallback) {
|
|
|
136
135
|
return out;
|
|
137
136
|
}
|
|
138
137
|
|
|
139
|
-
export { DEFAULT_CLUSTER_CENTER_RADIUS, DEFAULT_LAYOUT_SEED,
|
|
140
|
-
//# sourceMappingURL=chunk-
|
|
141
|
-
//# sourceMappingURL=chunk-
|
|
138
|
+
export { DEFAULT_CLUSTER_CENTER_RADIUS, DEFAULT_LAYOUT_SEED, clusterCentroids, clusterProbe, deriveClusters, generateClusterCenters, resetClusterProbe, resolveClusterCenters };
|
|
139
|
+
//# sourceMappingURL=chunk-XAR262L3.js.map
|
|
140
|
+
//# sourceMappingURL=chunk-XAR262L3.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/clusters.ts"],"names":[],"mappings":"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","file":"chunk-XAR262L3.js","sourcesContent":["/**\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"]}
|
package/dist/engine.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export {
|
|
1
|
+
export { aM as BufferPatch, aa as EngineBufferChannel, E as EngineCapabilities, a as EngineCommit, aN as EngineConfigUpdate, aO as EngineContextEvent, c as EngineDiagnostic, t as EngineFactory, b as EngineHostEvents, F as FitViewOptions, G as GraphEngine } from './index-CFZMson3.js';
|