koota-kit 0.8.6 → 0.8.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,408 @@
1
+ import { createHash } from "node:crypto";
2
+ import { assertMesh, sourceFaceIds } from "./fixture.mjs";
3
+ import {
4
+ indexedBounds,
5
+ nearestOpenSurface,
6
+ NumericalOracleRangeError,
7
+ worldPositions,
8
+ } from "./open-surface.mjs";
9
+
10
+ export const PROOF_FRAME = Object.freeze({
11
+ id: "caller-proof-world",
12
+ unit: "metre",
13
+ metric: "euclidean",
14
+ });
15
+ const freeze = Object.freeze;
16
+ const tuple = (r) => JSON.stringify([r.geometry, r.pose, r.transform]);
17
+ const stampKey = (r, s) =>
18
+ JSON.stringify([
19
+ r.identity.sourceId,
20
+ r.identity.instanceId,
21
+ r.identity.partId,
22
+ r.identity.registration.id,
23
+ r.identity.registration.generation,
24
+ [r.frame.id, r.frame.unit, r.frame.metric],
25
+ s,
26
+ r.topology[s].id,
27
+ [r.revisions.geometry, r.revisions.topology, r.revisions.pose, r.revisions.transform],
28
+ ]);
29
+ function hashArrays(...values) {
30
+ const hash = createHash("sha256");
31
+ for (const v of values)
32
+ hash.update(
33
+ ArrayBuffer.isView(v) ? Buffer.from(v.buffer, v.byteOffset, v.byteLength) : JSON.stringify(v),
34
+ );
35
+ return hash.digest("hex");
36
+ }
37
+ function sameIndexBytes(left, right) {
38
+ if (left.length !== right.length || left.byteLength !== right.byteLength) return false;
39
+ const a = new Uint8Array(left.buffer, left.byteOffset, left.byteLength);
40
+ const b = new Uint8Array(right.buffer, right.byteOffset, right.byteLength);
41
+ for (let index = 0; index < a.length; index += 1) if (a[index] !== b[index]) return false;
42
+ return true;
43
+ }
44
+
45
+ /** Test-only caller resource ledger; coordination snapshots are the sole commit boundary. */
46
+ export function createProofCohort(coordination, specifications) {
47
+ const models = new Map(),
48
+ views = new WeakMap();
49
+ let busy = false,
50
+ disposalStarted = false;
51
+ function current(m) {
52
+ return (
53
+ coordination.getRegistration(m.id) === m.token && coordination.getEntity(m.id) === m.entity
54
+ );
55
+ }
56
+ function selectedVersion(m, s) {
57
+ const e = s.entries.find((e) => e.id === m.id);
58
+ return e?.completed ? m.versions.get(tuple(e.completed.revisions)) : undefined;
59
+ }
60
+ function collect() {
61
+ if (busy) return;
62
+ const s = coordination.getSnapshot();
63
+ for (const m of models.values()) {
64
+ const active = current(m) ? selectedVersion(m, s) : undefined;
65
+ for (const [id, v] of m.versions) if (v !== active && v.leases === 0) m.versions.delete(id);
66
+ }
67
+ }
68
+ function view(s) {
69
+ function invalid() {
70
+ const cached = views.get(s);
71
+ if (cached?.valid === false) return cached;
72
+ const result = freeze({ coordination: s, records: freeze([]), valid: false });
73
+ views.set(s, result);
74
+ return result;
75
+ }
76
+ if ([...models.values()].some((m) => !m.removed && !current(m))) return invalid();
77
+ // Cached metadata does not lease buffers; verify retained versions before using it.
78
+ if (
79
+ [...models.values()].some(
80
+ (m) =>
81
+ !m.removed &&
82
+ (!s.entries.find((entry) => entry.id === m.id)?.completed || !selectedVersion(m, s)),
83
+ )
84
+ )
85
+ return invalid();
86
+ const cached = views.get(s);
87
+ if (cached?.valid === true) return cached;
88
+ const records = [];
89
+ for (const m of models.values()) {
90
+ if (m.removed) continue;
91
+ const v = selectedVersion(m, s);
92
+ if (s.entries.find((entry) => entry.id === m.id)?.completed && !v) {
93
+ return invalid();
94
+ }
95
+ if (v) records.push(...v.parts.map((p) => p.record));
96
+ }
97
+ records.sort(
98
+ (a, b) =>
99
+ Number(stampKey(a, "full") > stampKey(b, "full")) -
100
+ Number(stampKey(a, "full") < stampKey(b, "full")),
101
+ );
102
+ const result = freeze({ coordination: s, records: freeze(records), valid: true });
103
+ views.set(s, result);
104
+ return result;
105
+ }
106
+ function prepare(m, u) {
107
+ if (!current(m)) throw new Error("proof: stale owned registration lifetime");
108
+ const revisions = freeze({ ...u.revisions }),
109
+ previous = selectedVersion(m, coordination.getSnapshot());
110
+ for (const epoch of ["geometry", "topology", "pose", "transform"]) {
111
+ if (
112
+ !Number.isSafeInteger(revisions[epoch]) ||
113
+ revisions[epoch] < 0 ||
114
+ (previous && revisions[epoch] < previous.revisions[epoch])
115
+ )
116
+ throw new Error(`proof: invalid or regressing ${epoch} epoch`);
117
+ }
118
+ if (
119
+ previous &&
120
+ revisions.topology !== previous.revisions.topology &&
121
+ revisions.geometry === previous.revisions.geometry
122
+ )
123
+ throw new Error("proof: topology replacement must advance geometry epoch");
124
+ const frame = freeze({ ...(u.frame ?? m.frame ?? PROOF_FRAME) });
125
+ if (
126
+ typeof frame.id !== "string" ||
127
+ frame.id.length === 0 ||
128
+ typeof frame.unit !== "string" ||
129
+ frame.unit.length === 0 ||
130
+ frame.metric !== "euclidean"
131
+ )
132
+ throw new Error("proof: invalid declared frame");
133
+ const partIds = new Set();
134
+ const parts = u.parts.map((p) => {
135
+ if (!p.id || partIds.has(p.id) || !p.fullTopologyId || !p.visibleTopologyId)
136
+ throw new Error("proof: invalid or duplicate part/topology identity");
137
+ partIds.add(p.id);
138
+ assertMesh(p.positions, p.full, p.positions.length / 3);
139
+ assertMesh(p.positions, p.visible, p.positions.length / 3);
140
+ const full = new Uint32Array(p.full),
141
+ visible = new Uint32Array(p.visible),
142
+ sourcePositions = new Float32Array(p.positions);
143
+ assertMesh(sourcePositions, full, sourcePositions.length / 3);
144
+ assertMesh(sourcePositions, visible, sourcePositions.length / 3);
145
+ const previousPart = previous?.parts.find((part) => part.record.identity.partId === p.id);
146
+ // Reuse only a committed same-lifetime part after validating fresh copies.
147
+ const canReuse =
148
+ previousPart &&
149
+ previous.revisions.topology === revisions.topology &&
150
+ previousPart.record.identity.sourceId === m.sourceId &&
151
+ previousPart.record.identity.instanceId === m.instanceId &&
152
+ previousPart.record.identity.registration.generation === m.providerGeneration &&
153
+ previousPart.record.topology.full.id === p.fullTopologyId &&
154
+ previousPart.record.topology.visible.id === p.visibleTopologyId &&
155
+ sameIndexBytes(previousPart.full, full) &&
156
+ sameIndexBytes(previousPart.visible, visible);
157
+ const visibleFaces = canReuse ? previousPart.visibleFaces : sourceFaceIds(full, visible),
158
+ positions = worldPositions(sourcePositions, u.toWorld);
159
+ const identity = freeze({
160
+ sourceId: m.sourceId,
161
+ instanceId: m.instanceId,
162
+ partId: p.id,
163
+ registration: freeze({ id: m.id, generation: m.providerGeneration }),
164
+ });
165
+ const record = freeze({
166
+ identity,
167
+ frame,
168
+ revisions,
169
+ topology: freeze({
170
+ full: freeze({
171
+ id: p.fullTopologyId,
172
+ bounds: indexedBounds(positions, full),
173
+ enclosesSurface: true,
174
+ }),
175
+ visible: freeze({
176
+ id: p.visibleTopologyId,
177
+ bounds: indexedBounds(positions, visible),
178
+ enclosesSurface: true,
179
+ }),
180
+ }),
181
+ capabilities: freeze(p.nearestSurface === false ? [] : ["nearest-surface"]),
182
+ });
183
+ return {
184
+ record,
185
+ sourcePositions,
186
+ positions,
187
+ full,
188
+ visible,
189
+ visibleFaces,
190
+ provenance: p.provenance ?? null,
191
+ };
192
+ });
193
+ const topologyHash = hashArrays(
194
+ ...parts.flatMap((p) => [
195
+ p.record.identity,
196
+ p.record.topology.full.id,
197
+ p.record.topology.visible.id,
198
+ p.full,
199
+ p.visible,
200
+ ]),
201
+ );
202
+ if (
203
+ previous &&
204
+ previous.topologyHash !== topologyHash &&
205
+ revisions.geometry === previous.revisions.geometry
206
+ )
207
+ throw new Error("proof: topology bytes/identity replacement must advance geometry epoch");
208
+ const fingerprint = hashArrays(
209
+ revisions,
210
+ [...u.toWorld],
211
+ ...parts.flatMap((p) => [p.record, p.provenance, p.sourcePositions, p.full, p.visible]),
212
+ );
213
+ const existing = m.versions.get(tuple(revisions));
214
+ if (existing) {
215
+ if (existing.fingerprint !== fingerprint)
216
+ throw new Error("proof: changed content under the same completed tuple");
217
+ return existing;
218
+ }
219
+ return { revisions, parts, topologyHash, fingerprint, leases: 0 };
220
+ }
221
+ const specificationIds = new Set();
222
+ for (const specification of specifications) {
223
+ for (const field of ["id", "sourceId", "instanceId"])
224
+ if (typeof specification[field] !== "string" || specification[field].length === 0)
225
+ throw new Error("proof: invalid model identity");
226
+ if (
227
+ !Number.isSafeInteger(specification.providerGeneration) ||
228
+ specification.providerGeneration < 0
229
+ )
230
+ throw new Error("proof: invalid provider generation");
231
+ if (specificationIds.has(specification.id)) throw new Error("proof: duplicate model identity");
232
+ if (coordination.getRegistration(specification.id))
233
+ throw new Error("proof: pre-existing caller registration");
234
+ specificationIds.add(specification.id);
235
+ }
236
+ for (const specification of specifications) {
237
+ if (models.has(specification.id)) throw new Error("proof: duplicate model identity");
238
+ const m = { ...specification, token: undefined, versions: new Map(), removed: false };
239
+ models.set(m.id, m);
240
+ const previousToken = coordination.getRegistration(m.id);
241
+ try {
242
+ m.token = coordination.register(m.entity, { id: m.id, capabilities: ["open-surface"] });
243
+ } catch (error) {
244
+ if (
245
+ coordination.getEntity(m.id) === m.entity &&
246
+ coordination.getRegistration(m.id) !== previousToken
247
+ )
248
+ m.token = coordination.getRegistration(m.id);
249
+ const errors = [error];
250
+ for (const owned of models.values())
251
+ if (owned.token && coordination.getRegistration(owned.id) === owned.token) {
252
+ try {
253
+ coordination.unregister(owned.token);
254
+ } catch (e) {
255
+ errors.push(e);
256
+ }
257
+ }
258
+ throw new AggregateError(
259
+ errors,
260
+ "proof: construction failed; owned token recovery attempted",
261
+ );
262
+ }
263
+ }
264
+ function publish(updates) {
265
+ if (busy) throw new Error("proof: publication/disposal reentry");
266
+ if (disposalStarted) throw new Error("proof: disposed cohort");
267
+ busy = true;
268
+ try {
269
+ const seen = new Set();
270
+ const prepared = updates.map((u) => {
271
+ const m = models.get(u.id);
272
+ if (!m || seen.has(u.id)) throw new Error("proof: unknown or duplicate model update");
273
+ seen.add(u.id);
274
+ return { m, v: prepare(m, u) };
275
+ });
276
+ // Native trait callbacks select OLD; committed subscribers select NEW, from this same ledger.
277
+ for (const { m, v } of prepared) m.versions.set(tuple(v.revisions), v);
278
+ coordination.publishBatch(
279
+ prepared.map(({ m, v }) => ({
280
+ token: m.token,
281
+ completed: {
282
+ revisions: {
283
+ geometry: v.revisions.geometry,
284
+ pose: v.revisions.pose,
285
+ transform: v.revisions.transform,
286
+ },
287
+ },
288
+ })),
289
+ );
290
+ return view(coordination.getSnapshot());
291
+ } finally {
292
+ busy = false;
293
+ collect();
294
+ }
295
+ }
296
+ function capture(snapshot = coordination.getSnapshot()) {
297
+ if (disposalStarted) throw new Error("proof: disposed cohort cannot acquire fresh captures");
298
+ const selected = view(snapshot),
299
+ versions = [],
300
+ partsByStamp = new Map(),
301
+ diagnostics = [];
302
+ if (selected.valid)
303
+ for (const m of models.values()) {
304
+ if (m.removed) continue;
305
+ const v = selectedVersion(m, snapshot);
306
+ if (!v) continue;
307
+ v.leases += 1;
308
+ versions.push(v);
309
+ for (const p of v.parts)
310
+ for (const s of ["full", "visible"]) partsByStamp.set(stampKey(p.record, s), p);
311
+ }
312
+ let released = false;
313
+ const assertLease = () => {
314
+ if (released) throw new Error("proof: released capture");
315
+ };
316
+ return freeze({
317
+ snapshot: selected,
318
+ provider: freeze({
319
+ snapshot() {
320
+ assertLease();
321
+ return selected.valid ? selected.records : [freeze({})];
322
+ },
323
+ }),
324
+ oracle: freeze({
325
+ nearest({ record, topology, point, maximumDistance }) {
326
+ assertLease();
327
+ const p = partsByStamp.get(stampKey(record, topology));
328
+ if (!p) return undefined;
329
+ const consumed = freeze({
330
+ identity: record.identity,
331
+ frame: record.frame,
332
+ topology: freeze({ selection: topology, id: record.topology[topology].id }),
333
+ revisions: record.revisions,
334
+ });
335
+ let hit;
336
+ try {
337
+ hit = nearestOpenSurface(
338
+ p.positions,
339
+ p[topology],
340
+ point,
341
+ topology === "visible" ? p.visibleFaces : undefined,
342
+ );
343
+ } catch (error) {
344
+ if (error instanceof NumericalOracleRangeError)
345
+ return freeze({ kind: "unknown", reason: "numerical-oracle-range", consumed });
346
+ throw error;
347
+ }
348
+ if (!hit || hit.distance > maximumDistance) return freeze({ kind: "no-hit", consumed });
349
+ diagnostics.push(freeze({ identity: record.identity, ...hit, consumed }));
350
+ return freeze({
351
+ kind: "nearest",
352
+ witness: freeze({
353
+ triangleId: `${record.identity.partId}/full-face/${hit.face}`,
354
+ point: hit.point,
355
+ distance: hit.distance,
356
+ frame: record.frame,
357
+ consumed,
358
+ }),
359
+ });
360
+ },
361
+ }),
362
+ getDiagnostics: () => freeze([...diagnostics]),
363
+ release() {
364
+ if (released) return;
365
+ released = true;
366
+ for (const v of versions) v.leases -= 1;
367
+ partsByStamp.clear();
368
+ collect();
369
+ },
370
+ });
371
+ }
372
+ function dispose() {
373
+ if (busy) throw new Error("proof: publication/disposal reentry");
374
+ busy = true;
375
+ disposalStarted = true;
376
+ const errors = [];
377
+ try {
378
+ for (const m of models.values()) {
379
+ if (m.removed) continue;
380
+ if (coordination.getRegistration(m.id) !== m.token) {
381
+ m.removed = true;
382
+ continue;
383
+ }
384
+ try {
385
+ coordination.unregister(m.token);
386
+ } catch (e) {
387
+ errors.push(e);
388
+ }
389
+ m.removed = coordination.getRegistration(m.id) !== m.token;
390
+ }
391
+ } finally {
392
+ busy = false;
393
+ collect();
394
+ }
395
+ if (errors.length)
396
+ throw new AggregateError(
397
+ errors,
398
+ "proof: disposal errors; failed owned cleanup remains retryable",
399
+ );
400
+ }
401
+ return freeze({
402
+ publish,
403
+ capture,
404
+ dispose,
405
+ getSnapshot: () => view(coordination.getSnapshot()),
406
+ subscribe: (listener) => coordination.subscribe(() => listener()),
407
+ });
408
+ }
@@ -0,0 +1,15 @@
1
+ {
2
+ "name": "koota-caller-proof-consumer",
3
+ "private": true,
4
+ "type": "module",
5
+ "dependencies": {
6
+ "jsdom": "30.1.2",
7
+ "koota": "0.6.6",
8
+ "koota-kit": "0.8.6",
9
+ "react": "19.3.0",
10
+ "react-dom": "19.3.0",
11
+ "strata-procedural": "0.1.0",
12
+ "yuka": "0.7.8",
13
+ "yuka-kit": "1.1.0"
14
+ }
15
+ }
@@ -0,0 +1,122 @@
1
+ import { createHash } from "node:crypto";
2
+ import { readFile } from "node:fs/promises";
3
+ import { basename, join } from "node:path";
4
+
5
+ const types = {
6
+ Float32Array: [Float32Array, 4, "getFloat32"],
7
+ Uint32Array: [Uint32Array, 4, "getUint32"],
8
+ Uint16Array: [Uint16Array, 2, "getUint16"],
9
+ Uint8Array: [Uint8Array, 1, "getUint8"],
10
+ };
11
+
12
+ /** Decode independently of host endianness and retain no aliases into file bytes. */
13
+ export function decodeArray(bytes, descriptor) {
14
+ const type = types[descriptor.type];
15
+ if (!type || !Number.isSafeInteger(descriptor.length) || descriptor.length < 0) {
16
+ throw new TypeError("fixture: invalid array descriptor");
17
+ }
18
+ const [Constructor, width, getter] = type;
19
+ if (
20
+ bytes.byteLength !== descriptor.byteLength ||
21
+ bytes.byteLength !== descriptor.length * width
22
+ ) {
23
+ throw new Error("fixture: array byte length mismatch");
24
+ }
25
+ const hash = createHash("sha256").update(bytes).digest("hex");
26
+ if (hash !== descriptor.sha256) throw new Error("fixture: SHA256 mismatch");
27
+ const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
28
+ const array = new Constructor(descriptor.length);
29
+ for (let index = 0; index < array.length; index += 1) {
30
+ array[index] = view[getter](index * width, true);
31
+ if (!Number.isFinite(array[index])) throw new Error("fixture: nonfinite array value");
32
+ }
33
+ return array;
34
+ }
35
+
36
+ export function assertMesh(positions, indices, vertexCount) {
37
+ if (positions.length !== vertexCount * 3 || indices.length % 3 !== 0) {
38
+ throw new Error("fixture: malformed mesh layout");
39
+ }
40
+ for (const index of indices) {
41
+ if (!Number.isInteger(index) || index < 0 || index >= vertexCount) {
42
+ throw new Error("fixture: index out of range");
43
+ }
44
+ }
45
+ }
46
+
47
+ /** Preserve authored full-source face ordinals even for render-visible selection. */
48
+ export function sourceFaceIds(full, visible) {
49
+ const faces = new Map();
50
+ for (let offset = 0; offset < full.length; offset += 3) {
51
+ const key = `${full[offset]},${full[offset + 1]},${full[offset + 2]}`;
52
+ if (faces.has(key)) throw new Error("fixture: duplicate full source face");
53
+ faces.set(key, offset / 3);
54
+ }
55
+ const selected = new Uint32Array(visible.length / 3);
56
+ for (let offset = 0; offset < visible.length; offset += 3) {
57
+ const id = faces.get(`${visible[offset]},${visible[offset + 1]},${visible[offset + 2]}`);
58
+ if (id === undefined) throw new Error("fixture: visible face absent from full source");
59
+ selected[offset / 3] = id;
60
+ }
61
+ return selected;
62
+ }
63
+
64
+ /** Fixture transport only: this does not evaluate poses or claim a live HK adapter. */
65
+ export async function loadFigureFixture(directory) {
66
+ const manifestBytes = await readFile(join(directory, "fixture.json"));
67
+ const manifest = JSON.parse(manifestBytes.toString("utf8"));
68
+ if (
69
+ manifest.format !== 1 ||
70
+ manifest.unit !== "metre" ||
71
+ manifest.byteOrder !== "little-endian"
72
+ ) {
73
+ throw new Error("fixture: unsupported format, unit or byte order");
74
+ }
75
+ const inputs = [
76
+ { file: "fixture.json", sha256: createHash("sha256").update(manifestBytes).digest("hex") },
77
+ ];
78
+ async function load(descriptor) {
79
+ if (basename(descriptor.file) !== descriptor.file) throw new Error("fixture: unsafe file name");
80
+ const array = decodeArray(await readFile(join(directory, descriptor.file)), descriptor);
81
+ inputs.push({ file: descriptor.file, sha256: descriptor.sha256 });
82
+ return array;
83
+ }
84
+ const parts = [];
85
+ for (const part of manifest.parts) {
86
+ const rest = await load(part.arrays.rest);
87
+ const restNormals = await load(part.arrays.restNormals);
88
+ const full = await load(part.arrays.fullIndex);
89
+ const visible = await load(part.arrays.index);
90
+ assertMesh(rest, full, part.vertexCount);
91
+ assertMesh(rest, visible, part.vertexCount);
92
+ if (full.length / 3 !== part.fullTriangleCount || visible.length / 3 !== part.triangleCount) {
93
+ throw new Error("fixture: declared topology count mismatch");
94
+ }
95
+ if (restNormals.length !== rest.length) throw new Error("fixture: malformed normals");
96
+ const visibleFaceIds = sourceFaceIds(full, visible);
97
+ const poses = {};
98
+ for (const pose of manifest.poses) {
99
+ if (pose.grounding !== "already applied once to every part; do not apply again") {
100
+ throw new Error("fixture: unknown grounding contract");
101
+ }
102
+ const source = pose.parts.find((candidate) => candidate.id === part.id);
103
+ if (!source) throw new Error("fixture: pose part missing");
104
+ const positions = await load(source.positions);
105
+ const normals = await load(source.normals);
106
+ assertMesh(positions, full, part.vertexCount);
107
+ if (normals.length !== positions.length) throw new Error("fixture: malformed posed normals");
108
+ poses[pose.name] = { positions, normals };
109
+ }
110
+ parts.push({
111
+ id: part.id,
112
+ geometryHash: part.geometryHash,
113
+ rest,
114
+ restNormals,
115
+ full,
116
+ visible,
117
+ visibleFaceIds,
118
+ poses,
119
+ });
120
+ }
121
+ return { manifest, parts, inputs };
122
+ }