koota-kit 0.8.7 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +20 -0
- package/README.md +4 -0
- package/docs/INTEGRATION_PROOF.md +158 -0
- package/examples/coordination-proof/browser-cohort.mjs +17 -0
- package/examples/coordination-proof/browser-observer.mjs +24 -0
- package/examples/coordination-proof/cohort-core.mjs +417 -0
- package/examples/coordination-proof/cohort.mjs +14 -0
- package/examples/coordination-proof/consumer-package.json +16 -0
- package/examples/coordination-proof/fixture.mjs +97 -0
- package/examples/coordination-proof/mesh-input.mjs +27 -0
- package/examples/coordination-proof/open-surface.mjs +195 -0
- package/examples/coordination-proof/prepare.mjs +82 -0
- package/examples/coordination-proof/react-inspector.mjs +19 -0
- package/examples/coordination-proof/react-observer.mjs +60 -0
- package/examples/coordination-proof/registry-import.mjs +3 -0
- package/examples/coordination-proof/run.mjs +390 -0
- package/package.json +2 -1
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { readFile } from "node:fs/promises";
|
|
3
|
+
import { basename, join } from "node:path";
|
|
4
|
+
|
|
5
|
+
import { assertMesh, sourceFaceIds } from "./mesh-input.mjs";
|
|
6
|
+
export { assertMesh, sourceFaceIds } from "./mesh-input.mjs";
|
|
7
|
+
|
|
8
|
+
const types = {
|
|
9
|
+
Float32Array: [Float32Array, 4, "getFloat32"],
|
|
10
|
+
Uint32Array: [Uint32Array, 4, "getUint32"],
|
|
11
|
+
Uint16Array: [Uint16Array, 2, "getUint16"],
|
|
12
|
+
Uint8Array: [Uint8Array, 1, "getUint8"],
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
/** Decode independently of host endianness and retain no aliases into file bytes. */
|
|
16
|
+
export function decodeArray(bytes, descriptor) {
|
|
17
|
+
const type = types[descriptor.type];
|
|
18
|
+
if (!type || !Number.isSafeInteger(descriptor.length) || descriptor.length < 0) {
|
|
19
|
+
throw new TypeError("fixture: invalid array descriptor");
|
|
20
|
+
}
|
|
21
|
+
const [Constructor, width, getter] = type;
|
|
22
|
+
if (
|
|
23
|
+
bytes.byteLength !== descriptor.byteLength ||
|
|
24
|
+
bytes.byteLength !== descriptor.length * width
|
|
25
|
+
) {
|
|
26
|
+
throw new Error("fixture: array byte length mismatch");
|
|
27
|
+
}
|
|
28
|
+
const hash = createHash("sha256").update(bytes).digest("hex");
|
|
29
|
+
if (hash !== descriptor.sha256) throw new Error("fixture: SHA256 mismatch");
|
|
30
|
+
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
31
|
+
const array = new Constructor(descriptor.length);
|
|
32
|
+
for (let index = 0; index < array.length; index += 1) {
|
|
33
|
+
array[index] = view[getter](index * width, true);
|
|
34
|
+
if (!Number.isFinite(array[index])) throw new Error("fixture: nonfinite array value");
|
|
35
|
+
}
|
|
36
|
+
return array;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Fixture transport only: this does not evaluate poses or claim a live HK adapter. */
|
|
40
|
+
export async function loadFigureFixture(directory) {
|
|
41
|
+
const manifestBytes = await readFile(join(directory, "fixture.json"));
|
|
42
|
+
const manifest = JSON.parse(manifestBytes.toString("utf8"));
|
|
43
|
+
if (
|
|
44
|
+
manifest.format !== 1 ||
|
|
45
|
+
manifest.unit !== "metre" ||
|
|
46
|
+
manifest.byteOrder !== "little-endian"
|
|
47
|
+
) {
|
|
48
|
+
throw new Error("fixture: unsupported format, unit or byte order");
|
|
49
|
+
}
|
|
50
|
+
const inputs = [
|
|
51
|
+
{ file: "fixture.json", sha256: createHash("sha256").update(manifestBytes).digest("hex") },
|
|
52
|
+
];
|
|
53
|
+
async function load(descriptor) {
|
|
54
|
+
if (basename(descriptor.file) !== descriptor.file) throw new Error("fixture: unsafe file name");
|
|
55
|
+
const array = decodeArray(await readFile(join(directory, descriptor.file)), descriptor);
|
|
56
|
+
inputs.push({ file: descriptor.file, sha256: descriptor.sha256 });
|
|
57
|
+
return array;
|
|
58
|
+
}
|
|
59
|
+
const parts = [];
|
|
60
|
+
for (const part of manifest.parts) {
|
|
61
|
+
const rest = await load(part.arrays.rest);
|
|
62
|
+
const restNormals = await load(part.arrays.restNormals);
|
|
63
|
+
const full = await load(part.arrays.fullIndex);
|
|
64
|
+
const visible = await load(part.arrays.index);
|
|
65
|
+
assertMesh(rest, full, part.vertexCount);
|
|
66
|
+
assertMesh(rest, visible, part.vertexCount);
|
|
67
|
+
if (full.length / 3 !== part.fullTriangleCount || visible.length / 3 !== part.triangleCount) {
|
|
68
|
+
throw new Error("fixture: declared topology count mismatch");
|
|
69
|
+
}
|
|
70
|
+
if (restNormals.length !== rest.length) throw new Error("fixture: malformed normals");
|
|
71
|
+
const visibleFaceIds = sourceFaceIds(full, visible);
|
|
72
|
+
const poses = {};
|
|
73
|
+
for (const pose of manifest.poses) {
|
|
74
|
+
if (pose.grounding !== "already applied once to every part; do not apply again") {
|
|
75
|
+
throw new Error("fixture: unknown grounding contract");
|
|
76
|
+
}
|
|
77
|
+
const source = pose.parts.find((candidate) => candidate.id === part.id);
|
|
78
|
+
if (!source) throw new Error("fixture: pose part missing");
|
|
79
|
+
const positions = await load(source.positions);
|
|
80
|
+
const normals = await load(source.normals);
|
|
81
|
+
assertMesh(positions, full, part.vertexCount);
|
|
82
|
+
if (normals.length !== positions.length) throw new Error("fixture: malformed posed normals");
|
|
83
|
+
poses[pose.name] = { positions, normals };
|
|
84
|
+
}
|
|
85
|
+
parts.push({
|
|
86
|
+
id: part.id,
|
|
87
|
+
geometryHash: part.geometryHash,
|
|
88
|
+
rest,
|
|
89
|
+
restNormals,
|
|
90
|
+
full,
|
|
91
|
+
visible,
|
|
92
|
+
visibleFaceIds,
|
|
93
|
+
poses,
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
return { manifest, parts, inputs };
|
|
97
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export function assertMesh(positions, indices, vertexCount) {
|
|
2
|
+
if (positions.length !== vertexCount * 3 || indices.length % 3 !== 0) {
|
|
3
|
+
throw new Error("fixture: malformed mesh layout");
|
|
4
|
+
}
|
|
5
|
+
for (const index of indices) {
|
|
6
|
+
if (!Number.isInteger(index) || index < 0 || index >= vertexCount) {
|
|
7
|
+
throw new Error("fixture: index out of range");
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/** Preserve authored full-source face ordinals even for render-visible selection. */
|
|
13
|
+
export function sourceFaceIds(full, visible) {
|
|
14
|
+
const faces = new Map();
|
|
15
|
+
for (let offset = 0; offset < full.length; offset += 3) {
|
|
16
|
+
const key = `${full[offset]},${full[offset + 1]},${full[offset + 2]}`;
|
|
17
|
+
if (faces.has(key)) throw new Error("fixture: duplicate full source face");
|
|
18
|
+
faces.set(key, offset / 3);
|
|
19
|
+
}
|
|
20
|
+
const selected = new Uint32Array(visible.length / 3);
|
|
21
|
+
for (let offset = 0; offset < visible.length; offset += 3) {
|
|
22
|
+
const id = faces.get(`${visible[offset]},${visible[offset + 1]},${visible[offset + 2]}`);
|
|
23
|
+
if (id === undefined) throw new Error("fixture: visible face absent from full source");
|
|
24
|
+
selected[offset / 3] = id;
|
|
25
|
+
}
|
|
26
|
+
return selected;
|
|
27
|
+
}
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
export class NumericalOracleRangeError extends RangeError {
|
|
2
|
+
constructor() {
|
|
3
|
+
super("proof: numerical oracle range exceeded");
|
|
4
|
+
this.name = "NumericalOracleRangeError";
|
|
5
|
+
}
|
|
6
|
+
}
|
|
7
|
+
const MIN_NORMAL = 2 ** -1022;
|
|
8
|
+
const checked = (value) => {
|
|
9
|
+
if (!Number.isFinite(value) || (value !== 0 && Math.abs(value) < MIN_NORMAL))
|
|
10
|
+
throw new NumericalOracleRangeError();
|
|
11
|
+
return value;
|
|
12
|
+
};
|
|
13
|
+
const sum = (...values) => values.reduce((total, value) => checked(total + value), 0);
|
|
14
|
+
const product = (left, right) => {
|
|
15
|
+
const value = checked(left * right);
|
|
16
|
+
if (value === 0 && left !== 0 && right !== 0) throw new NumericalOracleRangeError();
|
|
17
|
+
return value;
|
|
18
|
+
};
|
|
19
|
+
const quotient = (numerator, denominator) => {
|
|
20
|
+
if (denominator === 0) throw new NumericalOracleRangeError();
|
|
21
|
+
const value = checked(numerator / denominator);
|
|
22
|
+
if (value === 0 && numerator !== 0) throw new NumericalOracleRangeError();
|
|
23
|
+
return value;
|
|
24
|
+
};
|
|
25
|
+
const point = (x, y, z) => Object.freeze({ x: checked(x), y: checked(y), z: checked(z) });
|
|
26
|
+
const sub = (a, b) => [checked(a.x - b.x), checked(a.y - b.y), checked(a.z - b.z)];
|
|
27
|
+
const dot = (a, b) => sum(product(a[0], b[0]), product(a[1], b[1]), product(a[2], b[2]));
|
|
28
|
+
|
|
29
|
+
export const IDENTITY = Object.freeze([1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1]);
|
|
30
|
+
|
|
31
|
+
/** Column-major affine transformation, applied to every vertex before ranking. */
|
|
32
|
+
export function worldPositions(positions, matrix) {
|
|
33
|
+
if (
|
|
34
|
+
matrix.length !== 16 ||
|
|
35
|
+
matrix.some((value) => !Number.isFinite(value)) ||
|
|
36
|
+
matrix[3] !== 0 ||
|
|
37
|
+
matrix[7] !== 0 ||
|
|
38
|
+
matrix[11] !== 0 ||
|
|
39
|
+
matrix[15] !== 1
|
|
40
|
+
) {
|
|
41
|
+
throw new Error("proof: expected a finite column-major affine transform");
|
|
42
|
+
}
|
|
43
|
+
const output = new Float64Array(positions.length);
|
|
44
|
+
for (let i = 0; i < positions.length; i += 3) {
|
|
45
|
+
const [x, y, z] = positions.subarray(i, i + 3);
|
|
46
|
+
output[i] = sum(
|
|
47
|
+
product(matrix[0], x),
|
|
48
|
+
product(matrix[4], y),
|
|
49
|
+
product(matrix[8], z),
|
|
50
|
+
matrix[12],
|
|
51
|
+
);
|
|
52
|
+
output[i + 1] = sum(
|
|
53
|
+
product(matrix[1], x),
|
|
54
|
+
product(matrix[5], y),
|
|
55
|
+
product(matrix[9], z),
|
|
56
|
+
matrix[13],
|
|
57
|
+
);
|
|
58
|
+
output[i + 2] = sum(
|
|
59
|
+
product(matrix[2], x),
|
|
60
|
+
product(matrix[6], y),
|
|
61
|
+
product(matrix[10], z),
|
|
62
|
+
matrix[14],
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
if (output.some((value) => !Number.isFinite(value))) throw new Error("proof: transform overflow");
|
|
66
|
+
return output;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function indexedBounds(positions, indices) {
|
|
70
|
+
if (indices.length === 0) throw new Error("proof: empty topology has no certified bounds");
|
|
71
|
+
const min = [Infinity, Infinity, Infinity];
|
|
72
|
+
const max = [-Infinity, -Infinity, -Infinity];
|
|
73
|
+
for (const index of indices) {
|
|
74
|
+
for (let axis = 0; axis < 3; axis += 1) {
|
|
75
|
+
min[axis] = Math.min(min[axis], positions[index * 3 + axis]);
|
|
76
|
+
max[axis] = Math.max(max[axis], positions[index * 3 + axis]);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return Object.freeze({ min: point(...min), max: point(...max) });
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Bounded reference oracle only; deliberately exhaustive, with no cache or BVH.
|
|
83
|
+
function closestTriangle(p, a, b, c) {
|
|
84
|
+
const ab = sub(b, a),
|
|
85
|
+
ac = sub(c, a),
|
|
86
|
+
ap = sub(p, a);
|
|
87
|
+
const d1 = dot(ab, ap),
|
|
88
|
+
d2 = dot(ac, ap);
|
|
89
|
+
if (d1 <= 0 && d2 <= 0) return [1, 0, 0];
|
|
90
|
+
const bp = sub(p, b),
|
|
91
|
+
d3 = dot(ab, bp),
|
|
92
|
+
d4 = dot(ac, bp);
|
|
93
|
+
if (d3 >= 0 && d4 <= d3) return [0, 1, 0];
|
|
94
|
+
const vc = checked(product(d1, d4) - product(d3, d2));
|
|
95
|
+
if (vc <= 0 && d1 >= 0 && d3 <= 0) {
|
|
96
|
+
const v = quotient(d1, checked(d1 - d3));
|
|
97
|
+
return [checked(1 - v), v, 0];
|
|
98
|
+
}
|
|
99
|
+
const cp = sub(p, c),
|
|
100
|
+
d5 = dot(ab, cp),
|
|
101
|
+
d6 = dot(ac, cp);
|
|
102
|
+
if (d6 >= 0 && d5 <= d6) return [0, 0, 1];
|
|
103
|
+
const vb = checked(product(d5, d2) - product(d1, d6));
|
|
104
|
+
if (vb <= 0 && d2 >= 0 && d6 <= 0) {
|
|
105
|
+
const w = quotient(d2, checked(d2 - d6));
|
|
106
|
+
return [checked(1 - w), 0, w];
|
|
107
|
+
}
|
|
108
|
+
const va = checked(product(d3, d6) - product(d5, d4));
|
|
109
|
+
if (va <= 0 && d4 - d3 >= 0 && d5 - d6 >= 0) {
|
|
110
|
+
const numerator = checked(d4 - d3);
|
|
111
|
+
const w = quotient(numerator, sum(numerator, checked(d5 - d6)));
|
|
112
|
+
return [0, checked(1 - w), w];
|
|
113
|
+
}
|
|
114
|
+
const denominator = sum(va, vb, vc);
|
|
115
|
+
if (denominator <= 0) throw new NumericalOracleRangeError();
|
|
116
|
+
const inverse = quotient(1, denominator),
|
|
117
|
+
v = product(vb, inverse),
|
|
118
|
+
w = product(vc, inverse);
|
|
119
|
+
return [sum(1, -v, -w), v, w];
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export function nearestOpenSurface(positions, indices, query, sourceFaceIds) {
|
|
123
|
+
let best;
|
|
124
|
+
for (let offset = 0; offset < indices.length; offset += 3) {
|
|
125
|
+
const vertices = [0, 1, 2].map((corner) => {
|
|
126
|
+
const i = indices[offset + corner] * 3;
|
|
127
|
+
return point(positions[i], positions[i + 1], positions[i + 2]);
|
|
128
|
+
});
|
|
129
|
+
const [a, b, c] = vertices,
|
|
130
|
+
ab = sub(b, a),
|
|
131
|
+
ac = sub(c, a);
|
|
132
|
+
const cross = [
|
|
133
|
+
checked(product(ab[1], ac[2]) - product(ab[2], ac[1])),
|
|
134
|
+
checked(product(ab[2], ac[0]) - product(ab[0], ac[2])),
|
|
135
|
+
checked(product(ab[0], ac[1]) - product(ab[1], ac[0])),
|
|
136
|
+
];
|
|
137
|
+
const area = checked(Math.hypot(...cross));
|
|
138
|
+
let barycentric;
|
|
139
|
+
if (area === 0) {
|
|
140
|
+
const candidates = [
|
|
141
|
+
[0, 1],
|
|
142
|
+
[1, 2],
|
|
143
|
+
[2, 0],
|
|
144
|
+
].map(([start, end]) => {
|
|
145
|
+
const edge = sub(vertices[end], vertices[start]);
|
|
146
|
+
const length = dot(edge, edge);
|
|
147
|
+
const t =
|
|
148
|
+
length === 0
|
|
149
|
+
? 0
|
|
150
|
+
: Math.max(0, Math.min(1, quotient(dot(sub(query, vertices[start]), edge), length)));
|
|
151
|
+
const weights = [0, 0, 0];
|
|
152
|
+
weights[start] = checked(1 - t);
|
|
153
|
+
weights[end] = t;
|
|
154
|
+
const projected = point(
|
|
155
|
+
...["x", "y", "z"].map((axis) =>
|
|
156
|
+
sum(
|
|
157
|
+
vertices[start][axis],
|
|
158
|
+
product(t, checked(vertices[end][axis] - vertices[start][axis])),
|
|
159
|
+
),
|
|
160
|
+
),
|
|
161
|
+
);
|
|
162
|
+
return { weights, distance: checked(Math.hypot(...sub(query, projected))) };
|
|
163
|
+
});
|
|
164
|
+
candidates.sort((left, right) => left.distance - right.distance);
|
|
165
|
+
barycentric = candidates[0].weights;
|
|
166
|
+
} else {
|
|
167
|
+
// Gram-region classification squares the aspect ratio. Below this
|
|
168
|
+
// conservative roundoff margin, this bounded oracle cannot certify it.
|
|
169
|
+
const longestEdge = checked(
|
|
170
|
+
Math.max(Math.hypot(...ab), Math.hypot(...ac), Math.hypot(...sub(c, b))),
|
|
171
|
+
);
|
|
172
|
+
const quality = quotient(quotient(area, longestEdge), longestEdge);
|
|
173
|
+
if (quality <= Math.sqrt(64 * Number.EPSILON)) throw new NumericalOracleRangeError();
|
|
174
|
+
barycentric = closestTriangle(query, a, b, c);
|
|
175
|
+
}
|
|
176
|
+
const hit = point(
|
|
177
|
+
...["x", "y", "z"].map((axis) =>
|
|
178
|
+
sum(...vertices.map((vertex, corner) => product(vertex[axis], barycentric[corner]))),
|
|
179
|
+
),
|
|
180
|
+
);
|
|
181
|
+
const distance = checked(Math.hypot(...sub(query, hit)));
|
|
182
|
+
const face = sourceFaceIds?.[offset / 3] ?? offset / 3;
|
|
183
|
+
if (!best || distance < best.distance || (distance === best.distance && face < best.face)) {
|
|
184
|
+
best = Object.freeze({
|
|
185
|
+
face,
|
|
186
|
+
point: hit,
|
|
187
|
+
distance,
|
|
188
|
+
barycentric: Object.freeze(barycentric),
|
|
189
|
+
geometricNormal:
|
|
190
|
+
area === 0 ? undefined : point(...cross.map((value) => quotient(value, area))),
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
return best;
|
|
195
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { mkdir, writeFile } from "node:fs/promises";
|
|
3
|
+
import { dirname } from "node:path";
|
|
4
|
+
import { loadFigureFixture } from "./fixture.mjs";
|
|
5
|
+
import { IDENTITY, indexedBounds, nearestOpenSurface, worldPositions } from "./open-surface.mjs";
|
|
6
|
+
|
|
7
|
+
const directory = process.argv[2];
|
|
8
|
+
if (!directory)
|
|
9
|
+
throw new Error(
|
|
10
|
+
"Usage: node examples/coordination-proof/prepare.mjs <fixture-directory> [receipt.json]",
|
|
11
|
+
);
|
|
12
|
+
const figure = await loadFigureFixture(directory);
|
|
13
|
+
const report = {
|
|
14
|
+
scope: "copied-buffer/open-surface preparation; no live producer or rendered parity claim",
|
|
15
|
+
inputs: figure.inputs,
|
|
16
|
+
parts: [],
|
|
17
|
+
};
|
|
18
|
+
for (const part of figure.parts) {
|
|
19
|
+
assert.equal(part.poses.relaxed.positions.length, part.rest.length);
|
|
20
|
+
assert.notDeepEqual(part.poses.relaxed.positions, part.poses.overhead.positions);
|
|
21
|
+
const before = part.poses.relaxed.positions[0];
|
|
22
|
+
const copied = new Float32Array(part.poses.relaxed.positions);
|
|
23
|
+
copied[0] += 1;
|
|
24
|
+
assert.equal(part.poses.relaxed.positions[0], before);
|
|
25
|
+
const positions = worldPositions(part.poses.relaxed.positions, IDENTITY);
|
|
26
|
+
const bounds = indexedBounds(positions, part.full);
|
|
27
|
+
const face = 0;
|
|
28
|
+
const corners = [0, 1, 2].map((corner) => part.full[face * 3 + corner] * 3);
|
|
29
|
+
const centroid = Object.fromEntries(
|
|
30
|
+
["x", "y", "z"].map((axis, index) => [
|
|
31
|
+
axis,
|
|
32
|
+
corners.reduce((sum, offset) => sum + positions[offset + index], 0) / 3,
|
|
33
|
+
]),
|
|
34
|
+
);
|
|
35
|
+
const witness = nearestOpenSurface(positions, part.full, centroid);
|
|
36
|
+
assert.ok(witness.distance < 1e-12);
|
|
37
|
+
assert.ok(Math.abs(witness.barycentric.reduce((sum, value) => sum + value, 0) - 1) < 1e-12);
|
|
38
|
+
let hiddenWitness;
|
|
39
|
+
if (part.full.length !== part.visible.length) {
|
|
40
|
+
const visibleIds = new Set(part.visibleFaceIds);
|
|
41
|
+
const hiddenFace = Array.from({ length: part.full.length / 3 }, (_, face) => face).find(
|
|
42
|
+
(face) => !visibleIds.has(face),
|
|
43
|
+
);
|
|
44
|
+
const offsets = [0, 1, 2].map((corner) => part.full[hiddenFace * 3 + corner] * 3);
|
|
45
|
+
const point = Object.fromEntries(
|
|
46
|
+
["x", "y", "z"].map((axis, index) => [
|
|
47
|
+
axis,
|
|
48
|
+
offsets.reduce((sum, offset) => sum + positions[offset + index], 0) / 3,
|
|
49
|
+
]),
|
|
50
|
+
);
|
|
51
|
+
const full = nearestOpenSurface(positions, part.full, point);
|
|
52
|
+
const visible = nearestOpenSurface(positions, part.visible, point, part.visibleFaceIds);
|
|
53
|
+
assert.ok(full.distance < 1e-12);
|
|
54
|
+
assert.ok(visible.distance > 1e-12);
|
|
55
|
+
hiddenWitness = { hiddenFace, point, full, visible };
|
|
56
|
+
}
|
|
57
|
+
report.parts.push({
|
|
58
|
+
id: part.id,
|
|
59
|
+
fullTriangles: part.full.length / 3,
|
|
60
|
+
visibleTriangles: part.visible.length / 3,
|
|
61
|
+
hiddenTriangles: (part.full.length - part.visible.length) / 3,
|
|
62
|
+
bounds,
|
|
63
|
+
witness,
|
|
64
|
+
hiddenWitness,
|
|
65
|
+
grounding: "posed input bytes used unchanged; no additional lift",
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
assert.equal(report.parts[0].fullTriangles, 107024);
|
|
69
|
+
assert.equal(report.parts[0].visibleTriangles, 80112);
|
|
70
|
+
assert.equal(report.parts[1].fullTriangles, 66688);
|
|
71
|
+
assert.equal(report.parts[1].visibleTriangles, 66688);
|
|
72
|
+
if (process.argv[3]) {
|
|
73
|
+
await mkdir(dirname(process.argv[3]), { recursive: true });
|
|
74
|
+
await writeFile(process.argv[3], `${JSON.stringify(report, null, 2)}\n`);
|
|
75
|
+
}
|
|
76
|
+
console.log(
|
|
77
|
+
JSON.stringify(
|
|
78
|
+
{ scope: report.scope, inputCount: report.inputs.length, parts: report.parts },
|
|
79
|
+
null,
|
|
80
|
+
2,
|
|
81
|
+
),
|
|
82
|
+
);
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/** Shared raw-koota World inspector; rendering and unmount never own the World. */
|
|
2
|
+
export function createProofInspector({ React, useWorld }) {
|
|
3
|
+
return function Inspector({ cohort, world, onObservation }) {
|
|
4
|
+
if (useWorld() !== world) throw new Error("proof: inspector observed a different raw World");
|
|
5
|
+
const snapshot = React.useSyncExternalStore(
|
|
6
|
+
cohort.subscribe,
|
|
7
|
+
cohort.getSnapshot,
|
|
8
|
+
cohort.getSnapshot,
|
|
9
|
+
);
|
|
10
|
+
onObservation?.(snapshot);
|
|
11
|
+
return React.createElement(
|
|
12
|
+
"output",
|
|
13
|
+
null,
|
|
14
|
+
snapshot.coordination.entries
|
|
15
|
+
.map((entry) => `${entry.id}:${entry.completed?.revisions.pose ?? "pending"}`)
|
|
16
|
+
.join(","),
|
|
17
|
+
);
|
|
18
|
+
};
|
|
19
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { createProofInspector } from "./react-inspector.mjs";
|
|
3
|
+
|
|
4
|
+
/** A headless mounted observer; neither the component nor its unmount owns the World. */
|
|
5
|
+
export async function observeCommittedUpdate({ registryImport, cohort, world, publish }) {
|
|
6
|
+
const React = await registryImport("react");
|
|
7
|
+
const { createRoot } = await registryImport("react-dom/client");
|
|
8
|
+
const { WorldProvider, useWorld } = await registryImport("koota/react");
|
|
9
|
+
const { JSDOM } = await registryImport("jsdom");
|
|
10
|
+
const dom = new JSDOM("<!doctype html><html><body><div id='proof'></div></body></html>");
|
|
11
|
+
const descriptors = new Map();
|
|
12
|
+
for (const [name, value] of Object.entries({
|
|
13
|
+
window: dom.window,
|
|
14
|
+
document: dom.window.document,
|
|
15
|
+
IS_REACT_ACT_ENVIRONMENT: true,
|
|
16
|
+
})) {
|
|
17
|
+
descriptors.set(name, Object.getOwnPropertyDescriptor(globalThis, name));
|
|
18
|
+
Object.defineProperty(globalThis, name, { configurable: true, writable: true, value });
|
|
19
|
+
}
|
|
20
|
+
const observations = [];
|
|
21
|
+
const container = dom.window.document.getElementById("proof");
|
|
22
|
+
const root = createRoot(container);
|
|
23
|
+
const Inspector = createProofInspector({ React, useWorld });
|
|
24
|
+
const inspector = React.createElement(Inspector, {
|
|
25
|
+
cohort,
|
|
26
|
+
world,
|
|
27
|
+
onObservation: (snapshot) => observations.push(snapshot),
|
|
28
|
+
});
|
|
29
|
+
let unmounted = false;
|
|
30
|
+
try {
|
|
31
|
+
await React.act(async () =>
|
|
32
|
+
root.render(React.createElement(WorldProvider, { world }, inspector)),
|
|
33
|
+
);
|
|
34
|
+
const before = container.textContent;
|
|
35
|
+
const previous = cohort.getSnapshot();
|
|
36
|
+
await React.act(async () => publish());
|
|
37
|
+
assert.notEqual(cohort.getSnapshot(), previous);
|
|
38
|
+
assert.equal(observations.at(-1), cohort.getSnapshot());
|
|
39
|
+
const after = container.textContent;
|
|
40
|
+
assert.notEqual(after, before);
|
|
41
|
+
await React.act(async () => root.unmount());
|
|
42
|
+
unmounted = true;
|
|
43
|
+
assert.ok(world.isInitialized);
|
|
44
|
+
return {
|
|
45
|
+
before,
|
|
46
|
+
after,
|
|
47
|
+
renderCount: observations.length,
|
|
48
|
+
sameWorld: true,
|
|
49
|
+
unmountPreservedWorld: true,
|
|
50
|
+
};
|
|
51
|
+
} finally {
|
|
52
|
+
// Clean up an assertion failure before the normal unmount.
|
|
53
|
+
if (!unmounted) await React.act(async () => root.unmount());
|
|
54
|
+
dom.window.close();
|
|
55
|
+
for (const [name, descriptor] of descriptors) {
|
|
56
|
+
if (descriptor) Object.defineProperty(globalThis, name, descriptor);
|
|
57
|
+
else delete globalThis[name];
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|