koota-kit 0.7.1 → 0.8.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 CHANGED
@@ -1,5 +1,15 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.8.0 (2026-10-14)
4
+
5
+ <!-- Release notes generated using configuration in .github/release.yml at main -->
6
+
7
+ ## What's Changed
8
+ * feat(spatial): add renderer-neutral clearance queries by @jbdevprimary in https://github.com/jbcom/koota-kit/pull/55
9
+
10
+
11
+ **Full Changelog**: https://github.com/jbcom/koota-kit/compare/v0.7.1...v0.8.0
12
+
3
13
  ## 0.7.1 (2026-10-14)
4
14
 
5
15
  <!-- Release notes generated using configuration in .github/release.yml at main -->
package/README.md CHANGED
@@ -265,6 +265,7 @@ are also available:
265
265
  | `koota-kit/traits` | Safe trait declaration |
266
266
  | `koota-kit/schedule` | Fixed-step system schedule with a step cap |
267
267
  | `koota-kit/eventLog` | World-scoped publish/drain logs |
268
+ | `koota-kit/spatial` | Renderer-neutral point-to-open-surface clearance queries |
268
269
  | `koota-kit/react` | `SimWorldProvider` and `useSimWorld` (optional React peer) |
269
270
 
270
271
  See the [API reference](./docs/API.md) for signatures and validation behavior,
@@ -285,6 +286,9 @@ For task-oriented usage, read the Sourcey guides on
285
286
  - `advanceClock` accepts finite, non-negative `dt`; `0` is a valid paused tick.
286
287
  - `createSchedule` needs a finite `step > 0` and a safe-integer `maxSteps >= 1`;
287
288
  a negative, `NaN` or infinite `frameDt` counts as `0`.
289
+ - `queryPointClearance` accepts only finite coordinates and a shared Euclidean
290
+ frame/unit. It proves distance to declared open surfaces, never containment
291
+ or solid nonpenetration.
288
292
  - `nextInt` requires safe-integer bounds with `maxExclusive > minInclusive` and
289
293
  a range no larger than `2**32`.
290
294
  - `chance` accepts only finite probabilities in `[0, 1]`.
@@ -13,7 +13,7 @@
13
13
  // ./eventLog — scratch-backed publish/drain event logs with a peek seam for
14
14
  // observers (harnesses, tests, HUDs)
15
15
  Object.defineProperty(exports, "__esModule", { value: true });
16
- exports.CoordinationRegistration = exports.CoordinationBinding = exports.bindCoordination = exports.defineTrait = exports.defineEventLog = exports.createSchedule = exports.MASTER_SEED_BYTES = exports.isMasterSeed = exports.deriveSeed = exports.createMasterSeed = exports.substream = exports.snapshotSubstreams = exports.snapshotStream = exports.snapshotLayers = exports.shuffle = exports.restoreSubstreams = exports.restoreStream = exports.restoreLayers = exports.pick = exports.nextU32 = exports.nextInt = exports.nextFloat = exports.getSubstream = exports.createSubstreams = exports.createRng = exports.chance = exports.trait = exports.snapshotWorld = exports.restoreWorldHeader = exports.relation = exports.destroySimWorld = exports.createSimWorld = exports.createActions = exports.advanceClock = void 0;
16
+ exports.CoordinationRegistration = exports.CoordinationBinding = exports.bindCoordination = exports.defineTrait = exports.queryPointClearance = exports.defineEventLog = exports.createSchedule = exports.MASTER_SEED_BYTES = exports.isMasterSeed = exports.deriveSeed = exports.createMasterSeed = exports.substream = exports.snapshotSubstreams = exports.snapshotStream = exports.snapshotLayers = exports.shuffle = exports.restoreSubstreams = exports.restoreStream = exports.restoreLayers = exports.pick = exports.nextU32 = exports.nextInt = exports.nextFloat = exports.getSubstream = exports.createSubstreams = exports.createRng = exports.chance = exports.trait = exports.snapshotWorld = exports.restoreWorldHeader = exports.relation = exports.destroySimWorld = exports.createSimWorld = exports.createActions = exports.advanceClock = void 0;
17
17
  var world_js_1 = require("./world.cjs");
18
18
  Object.defineProperty(exports, "advanceClock", { enumerable: true, get: function () { return world_js_1.advanceClock; } });
19
19
  Object.defineProperty(exports, "createActions", { enumerable: true, get: function () { return world_js_1.createActions; } });
@@ -49,6 +49,8 @@ var schedule_js_1 = require("./schedule.cjs");
49
49
  Object.defineProperty(exports, "createSchedule", { enumerable: true, get: function () { return schedule_js_1.createSchedule; } });
50
50
  var eventLog_js_1 = require("./eventLog.cjs");
51
51
  Object.defineProperty(exports, "defineEventLog", { enumerable: true, get: function () { return eventLog_js_1.defineEventLog; } });
52
+ var spatial_js_1 = require("./spatial.cjs");
53
+ Object.defineProperty(exports, "queryPointClearance", { enumerable: true, get: function () { return spatial_js_1.queryPointClearance; } });
52
54
  var index_js_1 = require("./traits/index.cjs");
53
55
  Object.defineProperty(exports, "defineTrait", { enumerable: true, get: function () { return index_js_1.defineTrait; } });
54
56
  var coordination_js_1 = require("./coordination.cjs");
@@ -8,6 +8,8 @@ export { createSchedule } from "./schedule.cjs";
8
8
  export type { Schedule, ScheduleOptions, System } from "./schedule.cjs";
9
9
  export { defineEventLog } from "./eventLog.cjs";
10
10
  export type { EventLog } from "./eventLog.cjs";
11
+ export { queryPointClearance } from "./spatial.cjs";
12
+ export type { SpatialAabb, SpatialClearanceQuery, SpatialClearanceResult, SpatialFrame, SpatialIdentity, SpatialNearestSurfaceOracle, SpatialNearestWitness, SpatialOracleReply, SpatialPartRecord, SpatialPoint, SpatialProvider, SpatialQueryExactness, SpatialQueryReason, SpatialQueryStatus, SpatialRecordStamp, SpatialRegistrationIdentity, SpatialRevision, SpatialTopology, SpatialTopologySelection, } from "./spatial.cjs";
11
13
  export { defineTrait } from "./traits/index.cjs";
12
14
  export type { SafeSchema } from "./traits/index.cjs";
13
15
  export { bindCoordination, CoordinationBinding, CoordinationRegistration, } from "./coordination.cjs";
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAcA,OAAO,EACL,YAAY,EACZ,aAAa,EACb,cAAc,EACd,eAAe,EACf,QAAQ,EACR,kBAAkB,EAClB,aAAa,EACb,KAAK,GACN,MAAM,YAAY,CAAC;AACpB,YAAY,EAAE,MAAM,EAAE,KAAK,EAAE,WAAW,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAE5E,OAAO,EACL,MAAM,EACN,SAAS,EACT,gBAAgB,EAChB,YAAY,EACZ,SAAS,EACT,OAAO,EACP,OAAO,EACP,IAAI,EACJ,aAAa,EACb,aAAa,EACb,iBAAiB,EACjB,OAAO,EACP,cAAc,EACd,cAAc,EACd,kBAAkB,EAClB,SAAS,GACV,MAAM,UAAU,CAAC;AAClB,YAAY,EACV,SAAS,EACT,iBAAiB,EACjB,QAAQ,EACR,SAAS,EACT,iBAAiB,EACjB,UAAU,EACV,kBAAkB,GACnB,MAAM,UAAU,CAAC;AAElB,OAAO,EAAE,gBAAgB,EAAE,UAAU,EAAE,YAAY,EAAE,iBAAiB,EAAE,MAAM,WAAW,CAAC;AAC1F,YAAY,EAAE,aAAa,EAAE,QAAQ,EAAE,MAAM,WAAW,CAAC;AAEzD,OAAO,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AAC/C,YAAY,EAAE,QAAQ,EAAE,eAAe,EAAE,MAAM,EAAE,MAAM,eAAe,CAAC;AAEvE,OAAO,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AAC/C,YAAY,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AAE9C,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAChD,YAAY,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AAEpD,OAAO,EACL,gBAAgB,EAChB,mBAAmB,EACnB,wBAAwB,GACzB,MAAM,mBAAmB,CAAC;AAC3B,YAAY,EACV,iBAAiB,EACjB,kBAAkB,EAClB,YAAY,EACZ,sBAAsB,EACtB,iBAAiB,EACjB,oBAAoB,EACpB,oBAAoB,EACpB,kBAAkB,EAClB,iBAAiB,GAClB,MAAM,mBAAmB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAcA,OAAO,EACL,YAAY,EACZ,aAAa,EACb,cAAc,EACd,eAAe,EACf,QAAQ,EACR,kBAAkB,EAClB,aAAa,EACb,KAAK,GACN,MAAM,YAAY,CAAC;AACpB,YAAY,EAAE,MAAM,EAAE,KAAK,EAAE,WAAW,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAE5E,OAAO,EACL,MAAM,EACN,SAAS,EACT,gBAAgB,EAChB,YAAY,EACZ,SAAS,EACT,OAAO,EACP,OAAO,EACP,IAAI,EACJ,aAAa,EACb,aAAa,EACb,iBAAiB,EACjB,OAAO,EACP,cAAc,EACd,cAAc,EACd,kBAAkB,EAClB,SAAS,GACV,MAAM,UAAU,CAAC;AAClB,YAAY,EACV,SAAS,EACT,iBAAiB,EACjB,QAAQ,EACR,SAAS,EACT,iBAAiB,EACjB,UAAU,EACV,kBAAkB,GACnB,MAAM,UAAU,CAAC;AAElB,OAAO,EAAE,gBAAgB,EAAE,UAAU,EAAE,YAAY,EAAE,iBAAiB,EAAE,MAAM,WAAW,CAAC;AAC1F,YAAY,EAAE,aAAa,EAAE,QAAQ,EAAE,MAAM,WAAW,CAAC;AAEzD,OAAO,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AAC/C,YAAY,EAAE,QAAQ,EAAE,eAAe,EAAE,MAAM,EAAE,MAAM,eAAe,CAAC;AAEvE,OAAO,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AAC/C,YAAY,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AAE9C,OAAO,EAAE,mBAAmB,EAAE,MAAM,cAAc,CAAC;AACnD,YAAY,EACV,WAAW,EACX,qBAAqB,EACrB,sBAAsB,EACtB,YAAY,EACZ,eAAe,EACf,2BAA2B,EAC3B,qBAAqB,EACrB,kBAAkB,EAClB,iBAAiB,EACjB,YAAY,EACZ,eAAe,EACf,qBAAqB,EACrB,kBAAkB,EAClB,kBAAkB,EAClB,kBAAkB,EAClB,2BAA2B,EAC3B,eAAe,EACf,eAAe,EACf,wBAAwB,GACzB,MAAM,cAAc,CAAC;AAEtB,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAChD,YAAY,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AAEpD,OAAO,EACL,gBAAgB,EAChB,mBAAmB,EACnB,wBAAwB,GACzB,MAAM,mBAAmB,CAAC;AAC3B,YAAY,EACV,iBAAiB,EACjB,kBAAkB,EAClB,YAAY,EACZ,sBAAsB,EACtB,iBAAiB,EACjB,oBAAoB,EACpB,oBAAoB,EACpB,kBAAkB,EAClB,iBAAiB,GAClB,MAAM,mBAAmB,CAAC"}
@@ -0,0 +1,371 @@
1
+ "use strict";
2
+ /**
3
+ * Renderer-neutral point-to-surface clearance queries.
4
+ *
5
+ * Producers own their buffers, transforms, and exact geometry implementations.
6
+ * This module only validates a completed provider snapshot, filters certified
7
+ * AABBs, and calls an injected oracle in one declared common metric.
8
+ */
9
+ Object.defineProperty(exports, "__esModule", { value: true });
10
+ exports.queryPointClearance = queryPointClearance;
11
+ const finitePoint = (value, label) => {
12
+ if (typeof value !== "object" || value === null)
13
+ throw new TypeError(`spatial: ${label} must be a point.`);
14
+ const point = value;
15
+ for (const axis of ["x", "y", "z"]) {
16
+ if (!Number.isFinite(point[axis]))
17
+ throw new RangeError(`spatial: ${label}.${axis} must be finite.`);
18
+ }
19
+ return Object.freeze({ x: point.x, y: point.y, z: point.z });
20
+ };
21
+ const nonEmpty = (value, label) => {
22
+ if (typeof value !== "string" || value.length === 0)
23
+ throw new TypeError(`spatial: ${label} must be a non-empty string.`);
24
+ return value;
25
+ };
26
+ const nonNegativeRevision = (value, label) => {
27
+ if (!Number.isSafeInteger(value) || value < 0) {
28
+ throw new RangeError(`spatial: ${label} must be a non-negative safe integer.`);
29
+ }
30
+ return value;
31
+ };
32
+ function copyFrame(value, label) {
33
+ if (typeof value !== "object" || value === null)
34
+ throw new TypeError(`spatial: ${label} must be a frame.`);
35
+ const frame = value;
36
+ if (frame.metric !== "euclidean")
37
+ throw new TypeError(`spatial: ${label}.metric must be euclidean.`);
38
+ return Object.freeze({
39
+ id: nonEmpty(frame.id, `${label}.id`),
40
+ unit: nonEmpty(frame.unit, `${label}.unit`),
41
+ metric: "euclidean",
42
+ });
43
+ }
44
+ function sameFrame(left, right) {
45
+ return left.id === right.id && left.unit === right.unit && left.metric === right.metric;
46
+ }
47
+ function copyIdentity(value, label) {
48
+ if (typeof value !== "object" || value === null)
49
+ throw new TypeError(`spatial: ${label} must be an identity.`);
50
+ const identity = value;
51
+ if (typeof identity.registration !== "object" || identity.registration === null) {
52
+ throw new TypeError(`spatial: ${label}.registration must be an identity.`);
53
+ }
54
+ return Object.freeze({
55
+ sourceId: nonEmpty(identity.sourceId, `${label}.sourceId`),
56
+ instanceId: nonEmpty(identity.instanceId, `${label}.instanceId`),
57
+ partId: nonEmpty(identity.partId, `${label}.partId`),
58
+ registration: Object.freeze({
59
+ id: nonEmpty(identity.registration.id, `${label}.registration.id`),
60
+ generation: nonNegativeRevision(identity.registration.generation, `${label}.registration.generation`),
61
+ }),
62
+ });
63
+ }
64
+ function sameIdentity(left, right) {
65
+ return (left.sourceId === right.sourceId &&
66
+ left.instanceId === right.instanceId &&
67
+ left.partId === right.partId &&
68
+ left.registration.id === right.registration.id &&
69
+ left.registration.generation === right.registration.generation);
70
+ }
71
+ function identityKey(identity) {
72
+ return JSON.stringify([
73
+ identity.sourceId,
74
+ identity.instanceId,
75
+ identity.partId,
76
+ identity.registration.id,
77
+ identity.registration.generation,
78
+ ]);
79
+ }
80
+ function compareIdentity(left, right) {
81
+ const leftKey = identityKey(left);
82
+ const rightKey = identityKey(right);
83
+ return Number(leftKey > rightKey) - Number(leftKey < rightKey);
84
+ }
85
+ function copyRevisions(value, label) {
86
+ if (typeof value !== "object" || value === null)
87
+ throw new TypeError(`spatial: ${label} must be revisions.`);
88
+ const revisions = value;
89
+ return Object.freeze({
90
+ geometry: nonNegativeRevision(revisions.geometry, `${label}.geometry`),
91
+ topology: nonNegativeRevision(revisions.topology, `${label}.topology`),
92
+ pose: nonNegativeRevision(revisions.pose, `${label}.pose`),
93
+ transform: nonNegativeRevision(revisions.transform, `${label}.transform`),
94
+ });
95
+ }
96
+ function sameRevisions(left, right) {
97
+ return (left.geometry === right.geometry &&
98
+ left.topology === right.topology &&
99
+ left.pose === right.pose &&
100
+ left.transform === right.transform);
101
+ }
102
+ function copyBounds(value, label) {
103
+ if (typeof value !== "object" || value === null)
104
+ throw new TypeError(`spatial: ${label} must be an AABB.`);
105
+ const bounds = value;
106
+ const min = finitePoint(bounds.min, `${label}.min`);
107
+ const max = finitePoint(bounds.max, `${label}.max`);
108
+ for (const axis of ["x", "y", "z"]) {
109
+ if (min[axis] > max[axis])
110
+ throw new RangeError(`spatial: ${label}.min.${axis} must not exceed max.${axis}.`);
111
+ }
112
+ return Object.freeze({ min, max });
113
+ }
114
+ function copyTopology(value, label) {
115
+ if (typeof value !== "object" || value === null)
116
+ throw new TypeError(`spatial: ${label} must be topology.`);
117
+ const topology = value;
118
+ if (topology.enclosesSurface !== true)
119
+ throw new TypeError(`spatial: ${label}.enclosesSurface must certify true.`);
120
+ return Object.freeze({
121
+ id: nonEmpty(topology.id, `${label}.id`),
122
+ bounds: copyBounds(topology.bounds, `${label}.bounds`),
123
+ enclosesSurface: true,
124
+ });
125
+ }
126
+ function copyRecord(value, label) {
127
+ if (typeof value !== "object" || value === null)
128
+ throw new TypeError(`spatial: ${label} must be a part record.`);
129
+ const record = value;
130
+ if (typeof record.topology !== "object" || record.topology === null)
131
+ throw new TypeError(`spatial: ${label}.topology must be an object.`);
132
+ if (!Array.isArray(record.capabilities))
133
+ throw new TypeError(`spatial: ${label}.capabilities must be an array.`);
134
+ const capabilities = Array.from(record.capabilities);
135
+ if (capabilities.some((capability) => capability !== "nearest-surface") ||
136
+ new Set(capabilities).size !== capabilities.length) {
137
+ throw new TypeError(`spatial: ${label}.capabilities may only contain nearest-surface.`);
138
+ }
139
+ return Object.freeze({
140
+ identity: copyIdentity(record.identity, `${label}.identity`),
141
+ frame: copyFrame(record.frame, `${label}.frame`),
142
+ revisions: copyRevisions(record.revisions, `${label}.revisions`),
143
+ topology: Object.freeze({
144
+ full: copyTopology(record.topology.full, `${label}.topology.full`),
145
+ visible: copyTopology(record.topology.visible, `${label}.topology.visible`),
146
+ }),
147
+ capabilities: Object.freeze(capabilities),
148
+ });
149
+ }
150
+ function makeStamp(record, selection) {
151
+ return Object.freeze({
152
+ identity: record.identity,
153
+ frame: record.frame,
154
+ topology: Object.freeze({ selection, id: record.topology[selection].id }),
155
+ revisions: record.revisions,
156
+ });
157
+ }
158
+ function sameStamp(left, right) {
159
+ return (sameIdentity(left.identity, right.identity) &&
160
+ sameFrame(left.frame, right.frame) &&
161
+ left.topology.selection === right.topology.selection &&
162
+ left.topology.id === right.topology.id &&
163
+ sameRevisions(left.revisions, right.revisions));
164
+ }
165
+ function copyStamp(value, label) {
166
+ if (typeof value !== "object" || value === null)
167
+ throw new TypeError(`spatial: ${label} must be a record stamp.`);
168
+ const stamp = value;
169
+ if (typeof stamp.topology !== "object" ||
170
+ stamp.topology === null ||
171
+ (stamp.topology.selection !== "full" && stamp.topology.selection !== "visible")) {
172
+ throw new TypeError(`spatial: ${label}.topology.selection must be full or visible.`);
173
+ }
174
+ return Object.freeze({
175
+ identity: copyIdentity(stamp.identity, `${label}.identity`),
176
+ frame: copyFrame(stamp.frame, `${label}.frame`),
177
+ topology: Object.freeze({
178
+ selection: stamp.topology.selection,
179
+ id: nonEmpty(stamp.topology.id, `${label}.topology.id`),
180
+ }),
181
+ revisions: copyRevisions(stamp.revisions, `${label}.revisions`),
182
+ });
183
+ }
184
+ function distanceToBounds(point, bounds) {
185
+ const delta = ["x", "y", "z"].map((axis) => Math.max(bounds.min[axis] - point[axis], 0, point[axis] - bounds.max[axis]));
186
+ return Math.hypot(...delta);
187
+ }
188
+ function pointDistance(left, right) {
189
+ return Math.hypot(left.x - right.x, left.y - right.y, left.z - right.z);
190
+ }
191
+ function pointIsInside(point, bounds) {
192
+ return ["x", "y", "z"].every((axis) => point[axis] >= bounds.min[axis] && point[axis] <= bounds.max[axis]);
193
+ }
194
+ function sameDistance(left, right) {
195
+ return Math.abs(left - right) <= Math.max(1e-9, Math.max(Math.abs(left), Math.abs(right)) / 1e9);
196
+ }
197
+ function result(status, reason, exactness, topology, checked, exclusions, witnesses) {
198
+ return Object.freeze({
199
+ status,
200
+ reason,
201
+ exactness,
202
+ topology,
203
+ checked: Object.freeze([...checked]),
204
+ exclusions: Object.freeze([...exclusions]),
205
+ witnesses: Object.freeze([...witnesses]),
206
+ });
207
+ }
208
+ /**
209
+ * Decide a point-to-open-surface clearance predicate in the given common
210
+ * Euclidean frame. It never proves containment or solid nonpenetration.
211
+ *
212
+ * Status precedence is stale, then violated, then unsupported/unknown, then
213
+ * satisfied. An exact close witness therefore proves a violation even if a
214
+ * different selected provider cannot answer.
215
+ */
216
+ function queryPointClearance(provider, oracle, query) {
217
+ if (typeof provider !== "object" || provider === null || typeof provider.snapshot !== "function")
218
+ throw new TypeError("spatial: provider must expose snapshot().");
219
+ if (typeof oracle !== "object" || oracle === null || typeof oracle.nearest !== "function")
220
+ throw new TypeError("spatial: oracle must expose nearest().");
221
+ if (typeof query !== "object" || query === null)
222
+ throw new TypeError("spatial: query must be an object.");
223
+ const point = finitePoint(query.point, "query.point");
224
+ const frame = copyFrame(query.frame, "query.frame");
225
+ const minimumDistance = query.minimumDistance;
226
+ const searchDistance = query.searchDistance;
227
+ if (!Number.isFinite(minimumDistance) || minimumDistance < 0)
228
+ throw new RangeError("spatial: query.minimumDistance must be finite and non-negative.");
229
+ if (!Number.isFinite(searchDistance) || searchDistance < minimumDistance)
230
+ throw new RangeError("spatial: query.searchDistance must be finite and at least minimumDistance.");
231
+ const topology = query.topology ?? "full";
232
+ if (topology !== "full" && topology !== "visible")
233
+ throw new TypeError("spatial: query.topology must be full or visible.");
234
+ if (query.exclusions !== undefined && !Array.isArray(query.exclusions))
235
+ throw new TypeError("spatial: query.exclusions must be an array.");
236
+ if (query.expected !== undefined && !Array.isArray(query.expected))
237
+ throw new TypeError("spatial: query.expected must be an array.");
238
+ const exclusions = Array.from(query.exclusions ?? [], (value, index) => copyIdentity(value, `query.exclusions[${index}]`)).sort(compareIdentity);
239
+ const exclusionKeys = new Set(exclusions.map(identityKey));
240
+ if (exclusionKeys.size !== exclusions.length)
241
+ throw new TypeError("spatial: query.exclusions must not contain duplicates.");
242
+ const expected = Array.from(query.expected ?? [], (value, index) => copyStamp(value, `query.expected[${index}]`));
243
+ const expectedKeys = new Set(expected.map((stamp) => identityKey(stamp.identity)));
244
+ if (expectedKeys.size !== expected.length)
245
+ throw new TypeError("spatial: query.expected must not contain duplicate identities.");
246
+ const snapshot = provider.snapshot();
247
+ if (!Array.isArray(snapshot))
248
+ throw new TypeError("spatial: provider.snapshot() must return an array.");
249
+ let records;
250
+ try {
251
+ records = Array.from(snapshot, (record, index) => copyRecord(record, `provider.snapshot()[${index}]`));
252
+ }
253
+ catch {
254
+ return result("unsupported", "provider-contract-invalid", "none", topology, [], exclusions, []);
255
+ }
256
+ const recordKeys = new Set(records.map((record) => identityKey(record.identity)));
257
+ if (recordKeys.size !== records.length) {
258
+ return result("unsupported", "provider-duplicate-record", "none", topology, [], exclusions, []);
259
+ }
260
+ records.sort((left, right) => compareIdentity(left.identity, right.identity));
261
+ const allStamps = records.map((record) => makeStamp(record, topology));
262
+ if (expected.length > 0) {
263
+ const stampsByIdentity = new Map(allStamps.map((stamp) => [identityKey(stamp.identity), stamp]));
264
+ if (expected.some((stamp) => {
265
+ const current = stampsByIdentity.get(identityKey(stamp.identity));
266
+ return !current || !sameStamp(current, stamp);
267
+ })) {
268
+ return result("stale", "revision-mismatch", "none", topology, allStamps, exclusions, []);
269
+ }
270
+ }
271
+ const selected = records.filter((record) => !exclusionKeys.has(identityKey(record.identity)));
272
+ const stamps = selected.map((record) => makeStamp(record, topology));
273
+ let unresolved;
274
+ const prepared = [];
275
+ for (const record of selected) {
276
+ if (!sameFrame(record.frame, frame)) {
277
+ unresolved = "provider-contract-invalid";
278
+ continue;
279
+ }
280
+ const stamp = makeStamp(record, topology);
281
+ prepared.push(Object.freeze({
282
+ record,
283
+ stamp,
284
+ boundsDistance: distanceToBounds(point, record.topology[topology].bounds),
285
+ }));
286
+ }
287
+ const candidates = prepared.filter(({ boundsDistance }) => boundsDistance < minimumDistance);
288
+ const witnesses = [];
289
+ for (const candidate of candidates) {
290
+ if (!candidate.record.capabilities.includes("nearest-surface")) {
291
+ if (unresolved !== "provider-contract-invalid")
292
+ unresolved = "nearest-surface-unsupported";
293
+ continue;
294
+ }
295
+ const reply = oracle.nearest(Object.freeze({
296
+ record: candidate.record,
297
+ topology,
298
+ point,
299
+ maximumDistance: searchDistance,
300
+ }));
301
+ if (reply === undefined) {
302
+ if (unresolved !== "provider-contract-invalid")
303
+ unresolved = "nearest-surface-unsupported";
304
+ continue;
305
+ }
306
+ if (typeof reply !== "object" ||
307
+ reply === null ||
308
+ (reply.kind !== "nearest" && reply.kind !== "no-hit" && reply.kind !== "unknown")) {
309
+ unresolved = "provider-contract-invalid";
310
+ continue;
311
+ }
312
+ let consumed;
313
+ try {
314
+ consumed = copyStamp(reply.kind === "nearest" ? reply.witness?.consumed : reply.consumed, "oracle.consumed");
315
+ }
316
+ catch {
317
+ unresolved = "provider-contract-invalid";
318
+ continue;
319
+ }
320
+ if (!sameStamp(consumed, candidate.stamp)) {
321
+ return result("stale", "oracle-stale", "none", topology, stamps, exclusions, witnesses);
322
+ }
323
+ if (reply.kind === "unknown") {
324
+ if (typeof reply.reason !== "string" || reply.reason.length === 0)
325
+ unresolved = "provider-contract-invalid";
326
+ else
327
+ unresolved ?? (unresolved = "oracle-unknown");
328
+ continue;
329
+ }
330
+ if (reply.kind === "no-hit")
331
+ continue;
332
+ let witness;
333
+ try {
334
+ const witnessPoint = finitePoint(reply.witness.point, "oracle.witness.point");
335
+ const distance = reply.witness.distance;
336
+ const actualDistance = pointDistance(point, witnessPoint);
337
+ if (!Number.isFinite(distance) ||
338
+ distance < 0 ||
339
+ distance > searchDistance ||
340
+ !Number.isFinite(actualDistance) ||
341
+ actualDistance > searchDistance ||
342
+ !sameDistance(distance, actualDistance) ||
343
+ !pointIsInside(witnessPoint, candidate.record.topology[topology].bounds))
344
+ throw new RangeError();
345
+ if (!sameFrame(copyFrame(reply.witness.frame, "oracle.witness.frame"), frame))
346
+ throw new TypeError();
347
+ witness = Object.freeze({
348
+ triangleId: nonEmpty(reply.witness.triangleId, "oracle.witness.triangleId"),
349
+ point: witnessPoint,
350
+ distance: actualDistance,
351
+ frame,
352
+ consumed: candidate.stamp,
353
+ });
354
+ }
355
+ catch {
356
+ unresolved = "provider-contract-invalid";
357
+ continue;
358
+ }
359
+ witnesses.push(witness);
360
+ }
361
+ const violation = witnesses.find((witness) => witness.distance < minimumDistance);
362
+ if (violation)
363
+ return result("violated", "clearance-violated", "exact", topology, stamps, exclusions, witnesses);
364
+ if (unresolved === "provider-contract-invalid")
365
+ return result("unsupported", "provider-contract-invalid", "none", topology, stamps, exclusions, witnesses);
366
+ if (unresolved === "nearest-surface-unsupported")
367
+ return result("unsupported", "nearest-surface-unsupported", "none", topology, stamps, exclusions, witnesses);
368
+ if (unresolved === "oracle-unknown")
369
+ return result("unknown", "oracle-unknown", "none", topology, stamps, exclusions, witnesses);
370
+ return result("satisfied", "clearance-satisfied", candidates.length > 0 && candidates.length === selected.length ? "exact" : "conservative", topology, stamps, exclusions, witnesses);
371
+ }
@@ -0,0 +1,130 @@
1
+ /**
2
+ * Renderer-neutral point-to-surface clearance queries.
3
+ *
4
+ * Producers own their buffers, transforms, and exact geometry implementations.
5
+ * This module only validates a completed provider snapshot, filters certified
6
+ * AABBs, and calls an injected oracle in one declared common metric.
7
+ */
8
+ export type SpatialPoint = Readonly<{
9
+ x: number;
10
+ y: number;
11
+ z: number;
12
+ }>;
13
+ export type SpatialAabb = Readonly<{
14
+ min: SpatialPoint;
15
+ max: SpatialPoint;
16
+ }>;
17
+ export type SpatialFrame = Readonly<{
18
+ id: string;
19
+ unit: string;
20
+ metric: "euclidean";
21
+ }>;
22
+ /** A provider-local registration identity. It is deliberately not a coordination token. */
23
+ export type SpatialRegistrationIdentity = Readonly<{
24
+ id: string;
25
+ generation: number;
26
+ }>;
27
+ export type SpatialIdentity = Readonly<{
28
+ sourceId: string;
29
+ instanceId: string;
30
+ partId: string;
31
+ registration: SpatialRegistrationIdentity;
32
+ }>;
33
+ /** Independent completed producer epochs; none is a coordination publication revision. */
34
+ export type SpatialRevision = Readonly<{
35
+ geometry: number;
36
+ topology: number;
37
+ pose: number;
38
+ transform: number;
39
+ }>;
40
+ export type SpatialTopology = Readonly<{
41
+ id: string;
42
+ /** The provider certifies that this AABB encloses the topology it supplies to the oracle. */
43
+ bounds: SpatialAabb;
44
+ enclosesSurface: true;
45
+ }>;
46
+ export type SpatialPartRecord = Readonly<{
47
+ identity: SpatialIdentity;
48
+ frame: SpatialFrame;
49
+ revisions: SpatialRevision;
50
+ /** `full` remains the default. `visible` is only selected explicitly by a query. */
51
+ topology: Readonly<{
52
+ full: SpatialTopology;
53
+ visible: SpatialTopology;
54
+ }>;
55
+ capabilities: readonly "nearest-surface"[];
56
+ }>;
57
+ export type SpatialTopologySelection = "full" | "visible";
58
+ export type SpatialRecordStamp = Readonly<{
59
+ identity: SpatialIdentity;
60
+ frame: SpatialFrame;
61
+ topology: Readonly<{
62
+ selection: SpatialTopologySelection;
63
+ id: string;
64
+ }>;
65
+ revisions: SpatialRevision;
66
+ }>;
67
+ export type SpatialNearestWitness = Readonly<{
68
+ triangleId: string;
69
+ point: SpatialPoint;
70
+ distance: number;
71
+ frame: SpatialFrame;
72
+ consumed: SpatialRecordStamp;
73
+ }>;
74
+ export type SpatialOracleReply = Readonly<{
75
+ kind: "nearest";
76
+ witness: SpatialNearestWitness;
77
+ }> | Readonly<{
78
+ kind: "no-hit";
79
+ consumed: SpatialRecordStamp;
80
+ }> | Readonly<{
81
+ kind: "unknown";
82
+ reason: string;
83
+ consumed: SpatialRecordStamp;
84
+ }>;
85
+ export type SpatialNearestSurfaceOracle = Readonly<{
86
+ nearest: (request: Readonly<{
87
+ record: SpatialPartRecord;
88
+ topology: SpatialTopologySelection;
89
+ point: SpatialPoint;
90
+ maximumDistance: number;
91
+ }>) => SpatialOracleReply | undefined;
92
+ }>;
93
+ /** Provider snapshots must be completed, immutable records for one query call. */
94
+ export type SpatialProvider = Readonly<{
95
+ snapshot: () => readonly SpatialPartRecord[];
96
+ }>;
97
+ export type SpatialClearanceQuery = Readonly<{
98
+ point: SpatialPoint;
99
+ frame: SpatialFrame;
100
+ /** The predicate: every selected open surface must be at least this far away. */
101
+ minimumDistance: number;
102
+ /** Finite oracle search bound; it must be at least `minimumDistance`. */
103
+ searchDistance: number;
104
+ topology?: SpatialTopologySelection;
105
+ exclusions?: readonly SpatialIdentity[];
106
+ /** A mismatch is stale, before an oracle result can be trusted. */
107
+ expected?: readonly SpatialRecordStamp[];
108
+ }>;
109
+ export type SpatialQueryStatus = "satisfied" | "violated" | "unknown" | "unsupported" | "stale";
110
+ export type SpatialQueryExactness = "exact" | "conservative" | "none";
111
+ export type SpatialQueryReason = "clearance-satisfied" | "clearance-violated" | "oracle-unknown" | "nearest-surface-unsupported" | "provider-contract-invalid" | "provider-duplicate-record" | "revision-mismatch" | "oracle-stale";
112
+ export type SpatialClearanceResult = Readonly<{
113
+ status: SpatialQueryStatus;
114
+ reason: SpatialQueryReason;
115
+ exactness: SpatialQueryExactness;
116
+ topology: SpatialTopologySelection;
117
+ checked: readonly SpatialRecordStamp[];
118
+ exclusions: readonly SpatialIdentity[];
119
+ witnesses: readonly SpatialNearestWitness[];
120
+ }>;
121
+ /**
122
+ * Decide a point-to-open-surface clearance predicate in the given common
123
+ * Euclidean frame. It never proves containment or solid nonpenetration.
124
+ *
125
+ * Status precedence is stale, then violated, then unsupported/unknown, then
126
+ * satisfied. An exact close witness therefore proves a violation even if a
127
+ * different selected provider cannot answer.
128
+ */
129
+ export declare function queryPointClearance(provider: SpatialProvider, oracle: SpatialNearestSurfaceOracle, query: SpatialClearanceQuery): SpatialClearanceResult;
130
+ //# sourceMappingURL=spatial.d.cts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"spatial.d.ts","sourceRoot":"","sources":["../../src/spatial.ts"],"names":[],"mappings":"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"}
@@ -8,6 +8,8 @@ export { createSchedule } from "./schedule.js";
8
8
  export type { Schedule, ScheduleOptions, System } from "./schedule.js";
9
9
  export { defineEventLog } from "./eventLog.js";
10
10
  export type { EventLog } from "./eventLog.js";
11
+ export { queryPointClearance } from "./spatial.js";
12
+ export type { SpatialAabb, SpatialClearanceQuery, SpatialClearanceResult, SpatialFrame, SpatialIdentity, SpatialNearestSurfaceOracle, SpatialNearestWitness, SpatialOracleReply, SpatialPartRecord, SpatialPoint, SpatialProvider, SpatialQueryExactness, SpatialQueryReason, SpatialQueryStatus, SpatialRecordStamp, SpatialRegistrationIdentity, SpatialRevision, SpatialTopology, SpatialTopologySelection, } from "./spatial.js";
11
13
  export { defineTrait } from "./traits/index.js";
12
14
  export type { SafeSchema } from "./traits/index.js";
13
15
  export { bindCoordination, CoordinationBinding, CoordinationRegistration, } from "./coordination.js";
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAcA,OAAO,EACL,YAAY,EACZ,aAAa,EACb,cAAc,EACd,eAAe,EACf,QAAQ,EACR,kBAAkB,EAClB,aAAa,EACb,KAAK,GACN,MAAM,YAAY,CAAC;AACpB,YAAY,EAAE,MAAM,EAAE,KAAK,EAAE,WAAW,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAE5E,OAAO,EACL,MAAM,EACN,SAAS,EACT,gBAAgB,EAChB,YAAY,EACZ,SAAS,EACT,OAAO,EACP,OAAO,EACP,IAAI,EACJ,aAAa,EACb,aAAa,EACb,iBAAiB,EACjB,OAAO,EACP,cAAc,EACd,cAAc,EACd,kBAAkB,EAClB,SAAS,GACV,MAAM,UAAU,CAAC;AAClB,YAAY,EACV,SAAS,EACT,iBAAiB,EACjB,QAAQ,EACR,SAAS,EACT,iBAAiB,EACjB,UAAU,EACV,kBAAkB,GACnB,MAAM,UAAU,CAAC;AAElB,OAAO,EAAE,gBAAgB,EAAE,UAAU,EAAE,YAAY,EAAE,iBAAiB,EAAE,MAAM,WAAW,CAAC;AAC1F,YAAY,EAAE,aAAa,EAAE,QAAQ,EAAE,MAAM,WAAW,CAAC;AAEzD,OAAO,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AAC/C,YAAY,EAAE,QAAQ,EAAE,eAAe,EAAE,MAAM,EAAE,MAAM,eAAe,CAAC;AAEvE,OAAO,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AAC/C,YAAY,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AAE9C,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAChD,YAAY,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AAEpD,OAAO,EACL,gBAAgB,EAChB,mBAAmB,EACnB,wBAAwB,GACzB,MAAM,mBAAmB,CAAC;AAC3B,YAAY,EACV,iBAAiB,EACjB,kBAAkB,EAClB,YAAY,EACZ,sBAAsB,EACtB,iBAAiB,EACjB,oBAAoB,EACpB,oBAAoB,EACpB,kBAAkB,EAClB,iBAAiB,GAClB,MAAM,mBAAmB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAcA,OAAO,EACL,YAAY,EACZ,aAAa,EACb,cAAc,EACd,eAAe,EACf,QAAQ,EACR,kBAAkB,EAClB,aAAa,EACb,KAAK,GACN,MAAM,YAAY,CAAC;AACpB,YAAY,EAAE,MAAM,EAAE,KAAK,EAAE,WAAW,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAE5E,OAAO,EACL,MAAM,EACN,SAAS,EACT,gBAAgB,EAChB,YAAY,EACZ,SAAS,EACT,OAAO,EACP,OAAO,EACP,IAAI,EACJ,aAAa,EACb,aAAa,EACb,iBAAiB,EACjB,OAAO,EACP,cAAc,EACd,cAAc,EACd,kBAAkB,EAClB,SAAS,GACV,MAAM,UAAU,CAAC;AAClB,YAAY,EACV,SAAS,EACT,iBAAiB,EACjB,QAAQ,EACR,SAAS,EACT,iBAAiB,EACjB,UAAU,EACV,kBAAkB,GACnB,MAAM,UAAU,CAAC;AAElB,OAAO,EAAE,gBAAgB,EAAE,UAAU,EAAE,YAAY,EAAE,iBAAiB,EAAE,MAAM,WAAW,CAAC;AAC1F,YAAY,EAAE,aAAa,EAAE,QAAQ,EAAE,MAAM,WAAW,CAAC;AAEzD,OAAO,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AAC/C,YAAY,EAAE,QAAQ,EAAE,eAAe,EAAE,MAAM,EAAE,MAAM,eAAe,CAAC;AAEvE,OAAO,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AAC/C,YAAY,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AAE9C,OAAO,EAAE,mBAAmB,EAAE,MAAM,cAAc,CAAC;AACnD,YAAY,EACV,WAAW,EACX,qBAAqB,EACrB,sBAAsB,EACtB,YAAY,EACZ,eAAe,EACf,2BAA2B,EAC3B,qBAAqB,EACrB,kBAAkB,EAClB,iBAAiB,EACjB,YAAY,EACZ,eAAe,EACf,qBAAqB,EACrB,kBAAkB,EAClB,kBAAkB,EAClB,kBAAkB,EAClB,2BAA2B,EAC3B,eAAe,EACf,eAAe,EACf,wBAAwB,GACzB,MAAM,cAAc,CAAC;AAEtB,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAChD,YAAY,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AAEpD,OAAO,EACL,gBAAgB,EAChB,mBAAmB,EACnB,wBAAwB,GACzB,MAAM,mBAAmB,CAAC;AAC3B,YAAY,EACV,iBAAiB,EACjB,kBAAkB,EAClB,YAAY,EACZ,sBAAsB,EACtB,iBAAiB,EACjB,oBAAoB,EACpB,oBAAoB,EACpB,kBAAkB,EAClB,iBAAiB,GAClB,MAAM,mBAAmB,CAAC"}
package/dist/esm/index.js CHANGED
@@ -16,5 +16,6 @@ export { chance, createRng, createSubstreams, getSubstream, nextFloat, nextInt,
16
16
  export { createMasterSeed, deriveSeed, isMasterSeed, MASTER_SEED_BYTES } from "./seed.js";
17
17
  export { createSchedule } from "./schedule.js";
18
18
  export { defineEventLog } from "./eventLog.js";
19
+ export { queryPointClearance } from "./spatial.js";
19
20
  export { defineTrait } from "./traits/index.js";
20
21
  export { bindCoordination, CoordinationBinding, CoordinationRegistration, } from "./coordination.js";
@@ -0,0 +1,130 @@
1
+ /**
2
+ * Renderer-neutral point-to-surface clearance queries.
3
+ *
4
+ * Producers own their buffers, transforms, and exact geometry implementations.
5
+ * This module only validates a completed provider snapshot, filters certified
6
+ * AABBs, and calls an injected oracle in one declared common metric.
7
+ */
8
+ export type SpatialPoint = Readonly<{
9
+ x: number;
10
+ y: number;
11
+ z: number;
12
+ }>;
13
+ export type SpatialAabb = Readonly<{
14
+ min: SpatialPoint;
15
+ max: SpatialPoint;
16
+ }>;
17
+ export type SpatialFrame = Readonly<{
18
+ id: string;
19
+ unit: string;
20
+ metric: "euclidean";
21
+ }>;
22
+ /** A provider-local registration identity. It is deliberately not a coordination token. */
23
+ export type SpatialRegistrationIdentity = Readonly<{
24
+ id: string;
25
+ generation: number;
26
+ }>;
27
+ export type SpatialIdentity = Readonly<{
28
+ sourceId: string;
29
+ instanceId: string;
30
+ partId: string;
31
+ registration: SpatialRegistrationIdentity;
32
+ }>;
33
+ /** Independent completed producer epochs; none is a coordination publication revision. */
34
+ export type SpatialRevision = Readonly<{
35
+ geometry: number;
36
+ topology: number;
37
+ pose: number;
38
+ transform: number;
39
+ }>;
40
+ export type SpatialTopology = Readonly<{
41
+ id: string;
42
+ /** The provider certifies that this AABB encloses the topology it supplies to the oracle. */
43
+ bounds: SpatialAabb;
44
+ enclosesSurface: true;
45
+ }>;
46
+ export type SpatialPartRecord = Readonly<{
47
+ identity: SpatialIdentity;
48
+ frame: SpatialFrame;
49
+ revisions: SpatialRevision;
50
+ /** `full` remains the default. `visible` is only selected explicitly by a query. */
51
+ topology: Readonly<{
52
+ full: SpatialTopology;
53
+ visible: SpatialTopology;
54
+ }>;
55
+ capabilities: readonly "nearest-surface"[];
56
+ }>;
57
+ export type SpatialTopologySelection = "full" | "visible";
58
+ export type SpatialRecordStamp = Readonly<{
59
+ identity: SpatialIdentity;
60
+ frame: SpatialFrame;
61
+ topology: Readonly<{
62
+ selection: SpatialTopologySelection;
63
+ id: string;
64
+ }>;
65
+ revisions: SpatialRevision;
66
+ }>;
67
+ export type SpatialNearestWitness = Readonly<{
68
+ triangleId: string;
69
+ point: SpatialPoint;
70
+ distance: number;
71
+ frame: SpatialFrame;
72
+ consumed: SpatialRecordStamp;
73
+ }>;
74
+ export type SpatialOracleReply = Readonly<{
75
+ kind: "nearest";
76
+ witness: SpatialNearestWitness;
77
+ }> | Readonly<{
78
+ kind: "no-hit";
79
+ consumed: SpatialRecordStamp;
80
+ }> | Readonly<{
81
+ kind: "unknown";
82
+ reason: string;
83
+ consumed: SpatialRecordStamp;
84
+ }>;
85
+ export type SpatialNearestSurfaceOracle = Readonly<{
86
+ nearest: (request: Readonly<{
87
+ record: SpatialPartRecord;
88
+ topology: SpatialTopologySelection;
89
+ point: SpatialPoint;
90
+ maximumDistance: number;
91
+ }>) => SpatialOracleReply | undefined;
92
+ }>;
93
+ /** Provider snapshots must be completed, immutable records for one query call. */
94
+ export type SpatialProvider = Readonly<{
95
+ snapshot: () => readonly SpatialPartRecord[];
96
+ }>;
97
+ export type SpatialClearanceQuery = Readonly<{
98
+ point: SpatialPoint;
99
+ frame: SpatialFrame;
100
+ /** The predicate: every selected open surface must be at least this far away. */
101
+ minimumDistance: number;
102
+ /** Finite oracle search bound; it must be at least `minimumDistance`. */
103
+ searchDistance: number;
104
+ topology?: SpatialTopologySelection;
105
+ exclusions?: readonly SpatialIdentity[];
106
+ /** A mismatch is stale, before an oracle result can be trusted. */
107
+ expected?: readonly SpatialRecordStamp[];
108
+ }>;
109
+ export type SpatialQueryStatus = "satisfied" | "violated" | "unknown" | "unsupported" | "stale";
110
+ export type SpatialQueryExactness = "exact" | "conservative" | "none";
111
+ export type SpatialQueryReason = "clearance-satisfied" | "clearance-violated" | "oracle-unknown" | "nearest-surface-unsupported" | "provider-contract-invalid" | "provider-duplicate-record" | "revision-mismatch" | "oracle-stale";
112
+ export type SpatialClearanceResult = Readonly<{
113
+ status: SpatialQueryStatus;
114
+ reason: SpatialQueryReason;
115
+ exactness: SpatialQueryExactness;
116
+ topology: SpatialTopologySelection;
117
+ checked: readonly SpatialRecordStamp[];
118
+ exclusions: readonly SpatialIdentity[];
119
+ witnesses: readonly SpatialNearestWitness[];
120
+ }>;
121
+ /**
122
+ * Decide a point-to-open-surface clearance predicate in the given common
123
+ * Euclidean frame. It never proves containment or solid nonpenetration.
124
+ *
125
+ * Status precedence is stale, then violated, then unsupported/unknown, then
126
+ * satisfied. An exact close witness therefore proves a violation even if a
127
+ * different selected provider cannot answer.
128
+ */
129
+ export declare function queryPointClearance(provider: SpatialProvider, oracle: SpatialNearestSurfaceOracle, query: SpatialClearanceQuery): SpatialClearanceResult;
130
+ //# sourceMappingURL=spatial.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"spatial.d.ts","sourceRoot":"","sources":["../../src/spatial.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,368 @@
1
+ /**
2
+ * Renderer-neutral point-to-surface clearance queries.
3
+ *
4
+ * Producers own their buffers, transforms, and exact geometry implementations.
5
+ * This module only validates a completed provider snapshot, filters certified
6
+ * AABBs, and calls an injected oracle in one declared common metric.
7
+ */
8
+ const finitePoint = (value, label) => {
9
+ if (typeof value !== "object" || value === null)
10
+ throw new TypeError(`spatial: ${label} must be a point.`);
11
+ const point = value;
12
+ for (const axis of ["x", "y", "z"]) {
13
+ if (!Number.isFinite(point[axis]))
14
+ throw new RangeError(`spatial: ${label}.${axis} must be finite.`);
15
+ }
16
+ return Object.freeze({ x: point.x, y: point.y, z: point.z });
17
+ };
18
+ const nonEmpty = (value, label) => {
19
+ if (typeof value !== "string" || value.length === 0)
20
+ throw new TypeError(`spatial: ${label} must be a non-empty string.`);
21
+ return value;
22
+ };
23
+ const nonNegativeRevision = (value, label) => {
24
+ if (!Number.isSafeInteger(value) || value < 0) {
25
+ throw new RangeError(`spatial: ${label} must be a non-negative safe integer.`);
26
+ }
27
+ return value;
28
+ };
29
+ function copyFrame(value, label) {
30
+ if (typeof value !== "object" || value === null)
31
+ throw new TypeError(`spatial: ${label} must be a frame.`);
32
+ const frame = value;
33
+ if (frame.metric !== "euclidean")
34
+ throw new TypeError(`spatial: ${label}.metric must be euclidean.`);
35
+ return Object.freeze({
36
+ id: nonEmpty(frame.id, `${label}.id`),
37
+ unit: nonEmpty(frame.unit, `${label}.unit`),
38
+ metric: "euclidean",
39
+ });
40
+ }
41
+ function sameFrame(left, right) {
42
+ return left.id === right.id && left.unit === right.unit && left.metric === right.metric;
43
+ }
44
+ function copyIdentity(value, label) {
45
+ if (typeof value !== "object" || value === null)
46
+ throw new TypeError(`spatial: ${label} must be an identity.`);
47
+ const identity = value;
48
+ if (typeof identity.registration !== "object" || identity.registration === null) {
49
+ throw new TypeError(`spatial: ${label}.registration must be an identity.`);
50
+ }
51
+ return Object.freeze({
52
+ sourceId: nonEmpty(identity.sourceId, `${label}.sourceId`),
53
+ instanceId: nonEmpty(identity.instanceId, `${label}.instanceId`),
54
+ partId: nonEmpty(identity.partId, `${label}.partId`),
55
+ registration: Object.freeze({
56
+ id: nonEmpty(identity.registration.id, `${label}.registration.id`),
57
+ generation: nonNegativeRevision(identity.registration.generation, `${label}.registration.generation`),
58
+ }),
59
+ });
60
+ }
61
+ function sameIdentity(left, right) {
62
+ return (left.sourceId === right.sourceId &&
63
+ left.instanceId === right.instanceId &&
64
+ left.partId === right.partId &&
65
+ left.registration.id === right.registration.id &&
66
+ left.registration.generation === right.registration.generation);
67
+ }
68
+ function identityKey(identity) {
69
+ return JSON.stringify([
70
+ identity.sourceId,
71
+ identity.instanceId,
72
+ identity.partId,
73
+ identity.registration.id,
74
+ identity.registration.generation,
75
+ ]);
76
+ }
77
+ function compareIdentity(left, right) {
78
+ const leftKey = identityKey(left);
79
+ const rightKey = identityKey(right);
80
+ return Number(leftKey > rightKey) - Number(leftKey < rightKey);
81
+ }
82
+ function copyRevisions(value, label) {
83
+ if (typeof value !== "object" || value === null)
84
+ throw new TypeError(`spatial: ${label} must be revisions.`);
85
+ const revisions = value;
86
+ return Object.freeze({
87
+ geometry: nonNegativeRevision(revisions.geometry, `${label}.geometry`),
88
+ topology: nonNegativeRevision(revisions.topology, `${label}.topology`),
89
+ pose: nonNegativeRevision(revisions.pose, `${label}.pose`),
90
+ transform: nonNegativeRevision(revisions.transform, `${label}.transform`),
91
+ });
92
+ }
93
+ function sameRevisions(left, right) {
94
+ return (left.geometry === right.geometry &&
95
+ left.topology === right.topology &&
96
+ left.pose === right.pose &&
97
+ left.transform === right.transform);
98
+ }
99
+ function copyBounds(value, label) {
100
+ if (typeof value !== "object" || value === null)
101
+ throw new TypeError(`spatial: ${label} must be an AABB.`);
102
+ const bounds = value;
103
+ const min = finitePoint(bounds.min, `${label}.min`);
104
+ const max = finitePoint(bounds.max, `${label}.max`);
105
+ for (const axis of ["x", "y", "z"]) {
106
+ if (min[axis] > max[axis])
107
+ throw new RangeError(`spatial: ${label}.min.${axis} must not exceed max.${axis}.`);
108
+ }
109
+ return Object.freeze({ min, max });
110
+ }
111
+ function copyTopology(value, label) {
112
+ if (typeof value !== "object" || value === null)
113
+ throw new TypeError(`spatial: ${label} must be topology.`);
114
+ const topology = value;
115
+ if (topology.enclosesSurface !== true)
116
+ throw new TypeError(`spatial: ${label}.enclosesSurface must certify true.`);
117
+ return Object.freeze({
118
+ id: nonEmpty(topology.id, `${label}.id`),
119
+ bounds: copyBounds(topology.bounds, `${label}.bounds`),
120
+ enclosesSurface: true,
121
+ });
122
+ }
123
+ function copyRecord(value, label) {
124
+ if (typeof value !== "object" || value === null)
125
+ throw new TypeError(`spatial: ${label} must be a part record.`);
126
+ const record = value;
127
+ if (typeof record.topology !== "object" || record.topology === null)
128
+ throw new TypeError(`spatial: ${label}.topology must be an object.`);
129
+ if (!Array.isArray(record.capabilities))
130
+ throw new TypeError(`spatial: ${label}.capabilities must be an array.`);
131
+ const capabilities = Array.from(record.capabilities);
132
+ if (capabilities.some((capability) => capability !== "nearest-surface") ||
133
+ new Set(capabilities).size !== capabilities.length) {
134
+ throw new TypeError(`spatial: ${label}.capabilities may only contain nearest-surface.`);
135
+ }
136
+ return Object.freeze({
137
+ identity: copyIdentity(record.identity, `${label}.identity`),
138
+ frame: copyFrame(record.frame, `${label}.frame`),
139
+ revisions: copyRevisions(record.revisions, `${label}.revisions`),
140
+ topology: Object.freeze({
141
+ full: copyTopology(record.topology.full, `${label}.topology.full`),
142
+ visible: copyTopology(record.topology.visible, `${label}.topology.visible`),
143
+ }),
144
+ capabilities: Object.freeze(capabilities),
145
+ });
146
+ }
147
+ function makeStamp(record, selection) {
148
+ return Object.freeze({
149
+ identity: record.identity,
150
+ frame: record.frame,
151
+ topology: Object.freeze({ selection, id: record.topology[selection].id }),
152
+ revisions: record.revisions,
153
+ });
154
+ }
155
+ function sameStamp(left, right) {
156
+ return (sameIdentity(left.identity, right.identity) &&
157
+ sameFrame(left.frame, right.frame) &&
158
+ left.topology.selection === right.topology.selection &&
159
+ left.topology.id === right.topology.id &&
160
+ sameRevisions(left.revisions, right.revisions));
161
+ }
162
+ function copyStamp(value, label) {
163
+ if (typeof value !== "object" || value === null)
164
+ throw new TypeError(`spatial: ${label} must be a record stamp.`);
165
+ const stamp = value;
166
+ if (typeof stamp.topology !== "object" ||
167
+ stamp.topology === null ||
168
+ (stamp.topology.selection !== "full" && stamp.topology.selection !== "visible")) {
169
+ throw new TypeError(`spatial: ${label}.topology.selection must be full or visible.`);
170
+ }
171
+ return Object.freeze({
172
+ identity: copyIdentity(stamp.identity, `${label}.identity`),
173
+ frame: copyFrame(stamp.frame, `${label}.frame`),
174
+ topology: Object.freeze({
175
+ selection: stamp.topology.selection,
176
+ id: nonEmpty(stamp.topology.id, `${label}.topology.id`),
177
+ }),
178
+ revisions: copyRevisions(stamp.revisions, `${label}.revisions`),
179
+ });
180
+ }
181
+ function distanceToBounds(point, bounds) {
182
+ const delta = ["x", "y", "z"].map((axis) => Math.max(bounds.min[axis] - point[axis], 0, point[axis] - bounds.max[axis]));
183
+ return Math.hypot(...delta);
184
+ }
185
+ function pointDistance(left, right) {
186
+ return Math.hypot(left.x - right.x, left.y - right.y, left.z - right.z);
187
+ }
188
+ function pointIsInside(point, bounds) {
189
+ return ["x", "y", "z"].every((axis) => point[axis] >= bounds.min[axis] && point[axis] <= bounds.max[axis]);
190
+ }
191
+ function sameDistance(left, right) {
192
+ return Math.abs(left - right) <= Math.max(1e-9, Math.max(Math.abs(left), Math.abs(right)) / 1e9);
193
+ }
194
+ function result(status, reason, exactness, topology, checked, exclusions, witnesses) {
195
+ return Object.freeze({
196
+ status,
197
+ reason,
198
+ exactness,
199
+ topology,
200
+ checked: Object.freeze([...checked]),
201
+ exclusions: Object.freeze([...exclusions]),
202
+ witnesses: Object.freeze([...witnesses]),
203
+ });
204
+ }
205
+ /**
206
+ * Decide a point-to-open-surface clearance predicate in the given common
207
+ * Euclidean frame. It never proves containment or solid nonpenetration.
208
+ *
209
+ * Status precedence is stale, then violated, then unsupported/unknown, then
210
+ * satisfied. An exact close witness therefore proves a violation even if a
211
+ * different selected provider cannot answer.
212
+ */
213
+ export function queryPointClearance(provider, oracle, query) {
214
+ if (typeof provider !== "object" || provider === null || typeof provider.snapshot !== "function")
215
+ throw new TypeError("spatial: provider must expose snapshot().");
216
+ if (typeof oracle !== "object" || oracle === null || typeof oracle.nearest !== "function")
217
+ throw new TypeError("spatial: oracle must expose nearest().");
218
+ if (typeof query !== "object" || query === null)
219
+ throw new TypeError("spatial: query must be an object.");
220
+ const point = finitePoint(query.point, "query.point");
221
+ const frame = copyFrame(query.frame, "query.frame");
222
+ const minimumDistance = query.minimumDistance;
223
+ const searchDistance = query.searchDistance;
224
+ if (!Number.isFinite(minimumDistance) || minimumDistance < 0)
225
+ throw new RangeError("spatial: query.minimumDistance must be finite and non-negative.");
226
+ if (!Number.isFinite(searchDistance) || searchDistance < minimumDistance)
227
+ throw new RangeError("spatial: query.searchDistance must be finite and at least minimumDistance.");
228
+ const topology = query.topology ?? "full";
229
+ if (topology !== "full" && topology !== "visible")
230
+ throw new TypeError("spatial: query.topology must be full or visible.");
231
+ if (query.exclusions !== undefined && !Array.isArray(query.exclusions))
232
+ throw new TypeError("spatial: query.exclusions must be an array.");
233
+ if (query.expected !== undefined && !Array.isArray(query.expected))
234
+ throw new TypeError("spatial: query.expected must be an array.");
235
+ const exclusions = Array.from(query.exclusions ?? [], (value, index) => copyIdentity(value, `query.exclusions[${index}]`)).sort(compareIdentity);
236
+ const exclusionKeys = new Set(exclusions.map(identityKey));
237
+ if (exclusionKeys.size !== exclusions.length)
238
+ throw new TypeError("spatial: query.exclusions must not contain duplicates.");
239
+ const expected = Array.from(query.expected ?? [], (value, index) => copyStamp(value, `query.expected[${index}]`));
240
+ const expectedKeys = new Set(expected.map((stamp) => identityKey(stamp.identity)));
241
+ if (expectedKeys.size !== expected.length)
242
+ throw new TypeError("spatial: query.expected must not contain duplicate identities.");
243
+ const snapshot = provider.snapshot();
244
+ if (!Array.isArray(snapshot))
245
+ throw new TypeError("spatial: provider.snapshot() must return an array.");
246
+ let records;
247
+ try {
248
+ records = Array.from(snapshot, (record, index) => copyRecord(record, `provider.snapshot()[${index}]`));
249
+ }
250
+ catch {
251
+ return result("unsupported", "provider-contract-invalid", "none", topology, [], exclusions, []);
252
+ }
253
+ const recordKeys = new Set(records.map((record) => identityKey(record.identity)));
254
+ if (recordKeys.size !== records.length) {
255
+ return result("unsupported", "provider-duplicate-record", "none", topology, [], exclusions, []);
256
+ }
257
+ records.sort((left, right) => compareIdentity(left.identity, right.identity));
258
+ const allStamps = records.map((record) => makeStamp(record, topology));
259
+ if (expected.length > 0) {
260
+ const stampsByIdentity = new Map(allStamps.map((stamp) => [identityKey(stamp.identity), stamp]));
261
+ if (expected.some((stamp) => {
262
+ const current = stampsByIdentity.get(identityKey(stamp.identity));
263
+ return !current || !sameStamp(current, stamp);
264
+ })) {
265
+ return result("stale", "revision-mismatch", "none", topology, allStamps, exclusions, []);
266
+ }
267
+ }
268
+ const selected = records.filter((record) => !exclusionKeys.has(identityKey(record.identity)));
269
+ const stamps = selected.map((record) => makeStamp(record, topology));
270
+ let unresolved;
271
+ const prepared = [];
272
+ for (const record of selected) {
273
+ if (!sameFrame(record.frame, frame)) {
274
+ unresolved = "provider-contract-invalid";
275
+ continue;
276
+ }
277
+ const stamp = makeStamp(record, topology);
278
+ prepared.push(Object.freeze({
279
+ record,
280
+ stamp,
281
+ boundsDistance: distanceToBounds(point, record.topology[topology].bounds),
282
+ }));
283
+ }
284
+ const candidates = prepared.filter(({ boundsDistance }) => boundsDistance < minimumDistance);
285
+ const witnesses = [];
286
+ for (const candidate of candidates) {
287
+ if (!candidate.record.capabilities.includes("nearest-surface")) {
288
+ if (unresolved !== "provider-contract-invalid")
289
+ unresolved = "nearest-surface-unsupported";
290
+ continue;
291
+ }
292
+ const reply = oracle.nearest(Object.freeze({
293
+ record: candidate.record,
294
+ topology,
295
+ point,
296
+ maximumDistance: searchDistance,
297
+ }));
298
+ if (reply === undefined) {
299
+ if (unresolved !== "provider-contract-invalid")
300
+ unresolved = "nearest-surface-unsupported";
301
+ continue;
302
+ }
303
+ if (typeof reply !== "object" ||
304
+ reply === null ||
305
+ (reply.kind !== "nearest" && reply.kind !== "no-hit" && reply.kind !== "unknown")) {
306
+ unresolved = "provider-contract-invalid";
307
+ continue;
308
+ }
309
+ let consumed;
310
+ try {
311
+ consumed = copyStamp(reply.kind === "nearest" ? reply.witness?.consumed : reply.consumed, "oracle.consumed");
312
+ }
313
+ catch {
314
+ unresolved = "provider-contract-invalid";
315
+ continue;
316
+ }
317
+ if (!sameStamp(consumed, candidate.stamp)) {
318
+ return result("stale", "oracle-stale", "none", topology, stamps, exclusions, witnesses);
319
+ }
320
+ if (reply.kind === "unknown") {
321
+ if (typeof reply.reason !== "string" || reply.reason.length === 0)
322
+ unresolved = "provider-contract-invalid";
323
+ else
324
+ unresolved ?? (unresolved = "oracle-unknown");
325
+ continue;
326
+ }
327
+ if (reply.kind === "no-hit")
328
+ continue;
329
+ let witness;
330
+ try {
331
+ const witnessPoint = finitePoint(reply.witness.point, "oracle.witness.point");
332
+ const distance = reply.witness.distance;
333
+ const actualDistance = pointDistance(point, witnessPoint);
334
+ if (!Number.isFinite(distance) ||
335
+ distance < 0 ||
336
+ distance > searchDistance ||
337
+ !Number.isFinite(actualDistance) ||
338
+ actualDistance > searchDistance ||
339
+ !sameDistance(distance, actualDistance) ||
340
+ !pointIsInside(witnessPoint, candidate.record.topology[topology].bounds))
341
+ throw new RangeError();
342
+ if (!sameFrame(copyFrame(reply.witness.frame, "oracle.witness.frame"), frame))
343
+ throw new TypeError();
344
+ witness = Object.freeze({
345
+ triangleId: nonEmpty(reply.witness.triangleId, "oracle.witness.triangleId"),
346
+ point: witnessPoint,
347
+ distance: actualDistance,
348
+ frame,
349
+ consumed: candidate.stamp,
350
+ });
351
+ }
352
+ catch {
353
+ unresolved = "provider-contract-invalid";
354
+ continue;
355
+ }
356
+ witnesses.push(witness);
357
+ }
358
+ const violation = witnesses.find((witness) => witness.distance < minimumDistance);
359
+ if (violation)
360
+ return result("violated", "clearance-violated", "exact", topology, stamps, exclusions, witnesses);
361
+ if (unresolved === "provider-contract-invalid")
362
+ return result("unsupported", "provider-contract-invalid", "none", topology, stamps, exclusions, witnesses);
363
+ if (unresolved === "nearest-surface-unsupported")
364
+ return result("unsupported", "nearest-surface-unsupported", "none", topology, stamps, exclusions, witnesses);
365
+ if (unresolved === "oracle-unknown")
366
+ return result("unknown", "oracle-unknown", "none", topology, stamps, exclusions, witnesses);
367
+ return result("satisfied", "clearance-satisfied", candidates.length > 0 && candidates.length === selected.length ? "exact" : "conservative", topology, stamps, exclusions, witnesses);
368
+ }
package/docs/API.md CHANGED
@@ -5,6 +5,45 @@ bindings, which are only available from `koota-kit/react` so the root entry
5
5
  never loads React. The module-specific import paths shown below can reduce
6
6
  coupling in larger codebases.
7
7
 
8
+ ## `spatial`
9
+
10
+ Import from `koota-kit/spatial` or the root:
11
+
12
+ ```ts
13
+ function queryPointClearance(
14
+ provider: SpatialProvider,
15
+ oracle: SpatialNearestSurfaceOracle,
16
+ query: SpatialClearanceQuery,
17
+ ): SpatialClearanceResult;
18
+ ```
19
+
20
+ `queryPointClearance` decides one explicit predicate: the query point is at
21
+ least `minimumDistance` from every selected **open surface**. The default
22
+ selection is each part's `full` topology; callers must explicitly request
23
+ `visible` topology. A provider record carries stable source, instance, part,
24
+ and provider-local registration identity; a common Euclidean frame and unit;
25
+ independent geometry, topology, pose, and transform revisions; and certified
26
+ enclosing AABBs for both topology selections.
27
+
28
+ The core performs deterministic AABB broadphase only. It invokes the supplied
29
+ nearest-surface oracle for overlapping candidates and validates that its
30
+ witness is in the selected certified bound, in the same frame/unit, and was
31
+ computed from the exact consumed identity/topology/revision tuple. Results
32
+ include those checked tuples, ordered witnesses, exclusions, one of
33
+ `satisfied`, `violated`, `unknown`, `unsupported`, or `stale`, and an
34
+ exactness label. An AABB rejection can conservatively satisfy the threshold;
35
+ an oracle witness or a stamped bounded `no-hit` is exact. Satisfaction that
36
+ combines AABB and oracle evidence is conservative. A stale expected/provider
37
+ tuple wins before the result can be trusted; otherwise an exact violating
38
+ witness wins over an unavailable or foreign-frame peer. Sparse provider
39
+ snapshots and capability lists fail closed. Validated query thresholds are captured before any
40
+ provider or oracle callback and cannot change during evaluation.
41
+
42
+ There is no geometry kernel, BVH, renderer, model, physics, Koota, or React
43
+ dependency in this entry. It does not prove containment or solid
44
+ nonpenetration: an open-surface nearest query has no watertightness/thickness
45
+ semantics. A future solid query needs an injected containment-capable oracle.
46
+
8
47
  ## `world`
9
48
 
10
49
  ### `createSimWorld(seeds)`
@@ -13,6 +13,7 @@ rng ──────────────────> seedrandom + seed
13
13
  world ────────────────> koota + rng
14
14
  schedule ─────────────> world (advanceClock) + WorldHandle scratch
15
15
  eventLog ─────────────> WorldHandle scratch
16
+ spatial ──────────────> (leaf: caller-provided provider + exact oracle)
16
17
  coordination ─────────> koota (borrowed World, no sibling runtime dependency)
17
18
  react ────────────────> koota/react + react + world (type only)
18
19
  index ────────────────> public re-exports only (never react)
@@ -45,6 +46,20 @@ than carrying it, so one huge dt cannot cause a catch-up storm.
45
46
  `eventLog` uses the handle's scratch map so event lifetime matches world
46
47
  lifetime. It does not use a process-global queue.
47
48
 
49
+ `spatial` is a renderer-neutral leaf entry. Providers publish immutable,
50
+ completed records with caller-owned source/instance/part/registration identity,
51
+ common frame/unit, separate geometry/topology/pose/transform revisions, full
52
+ and visible topology identities, and certified enclosing AABBs. The module
53
+ sorts records deterministically, does only conservative AABB filtering, and
54
+ delegates exact nearest-surface work to an injected oracle. It retains neither
55
+ provider records nor buffers, so producer-owned buffers cannot be aliased by a
56
+ core cache. Full topology is the default; a visible selection is explicit.
57
+ Sparse provider slots and capability lists fail closed; copied query thresholds stay fixed through
58
+ callbacks. Foreign-frame peers remain unresolved while compatible candidates
59
+ are evaluated, preserving exact violation precedence. Satisfaction involving
60
+ any AABB-only evidence remains conservative.
61
+ Point clearance establishes distance to open surfaces only. It deliberately
62
+ does not infer solid containment or nonpenetration from triangle proximity.
48
63
  `coordination` accepts an existing raw Koota `World` and attaches only its
49
64
  own ownership and registration traits. Its immutable snapshot is a completed
50
65
  metadata projection, not a second World or ECS serializer. The World retains
@@ -90,9 +105,12 @@ it reaches `react` or `koota/react`.
90
105
  delivered at any frame rate runs the same steps in the same order, as long
91
106
  as no single tick owes more than `maxSteps` (beyond that, the excess is
92
107
  dropped by design, so a one-second frame and sixty 1/60 s frames differ).
93
- 11. A coordination snapshot publishes only fully validated completed metadata;
108
+ 11. Spatial query results are stamped with the provider's full identity,
109
+ selected topology, frame/unit, and independent completed revisions. A
110
+ mismatched expected or oracle-consumed tuple is stale, never exact.
111
+ 12. A coordination snapshot publishes only fully validated completed metadata;
94
112
  failed batch preparation leaves the prior projection visible.
95
- 12. Coordination disposal never destroys or resets a caller-owned World,
113
+ 13. Coordination disposal never destroys or resets a caller-owned World,
96
114
  entity, caller trait, provider, or geometry buffer.
97
115
 
98
116
  ## Performance choices
@@ -102,6 +120,10 @@ it reaches `react` or `koota/react`.
102
120
  through JSON.
103
121
  - Event logs allocate on first push, leave scratch untouched for empty reads,
104
122
  truncate in place on drain, and expose a non-allocating `size` operation.
123
+ - Spatial expected stamps use a per-query map keyed by the complete encoded
124
+ identity tuple. Matching all expectations takes linear lookup work after
125
+ preparing the snapshot, before any exclusions; no map is retained between
126
+ queries. Queries without expected stamps do not allocate this map.
105
127
  - Coordination snapshots are rebuilt only after a completed registration,
106
128
  publication, removal, or explicit prune. Equal completed revisions retain
107
129
  the prior snapshot identity and do not notify subscribers.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "koota-kit",
3
- "version": "0.7.1",
3
+ "version": "0.8.0",
4
4
  "description": "Deterministic Koota simulation conventions: world lifecycle, dual RNG streams, safe object traits, and world-scoped event logs.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -83,14 +83,14 @@
83
83
  "default": "./dist/cjs/eventLog.cjs"
84
84
  }
85
85
  },
86
- "./coordination": {
86
+ "./spatial": {
87
87
  "import": {
88
- "types": "./dist/esm/coordination.d.ts",
89
- "default": "./dist/esm/coordination.js"
88
+ "types": "./dist/esm/spatial.d.ts",
89
+ "default": "./dist/esm/spatial.js"
90
90
  },
91
91
  "require": {
92
- "types": "./dist/cjs/coordination.d.cts",
93
- "default": "./dist/cjs/coordination.cjs"
92
+ "types": "./dist/cjs/spatial.d.cts",
93
+ "default": "./dist/cjs/spatial.cjs"
94
94
  }
95
95
  },
96
96
  "./react": {
@@ -103,7 +103,17 @@
103
103
  "default": "./dist/cjs/react.cjs"
104
104
  }
105
105
  },
106
- "./package.json": "./package.json"
106
+ "./package.json": "./package.json",
107
+ "./coordination": {
108
+ "import": {
109
+ "types": "./dist/esm/coordination.d.ts",
110
+ "default": "./dist/esm/coordination.js"
111
+ },
112
+ "require": {
113
+ "types": "./dist/cjs/coordination.d.cts",
114
+ "default": "./dist/cjs/coordination.cjs"
115
+ }
116
+ }
107
117
  },
108
118
  "files": [
109
119
  "CHANGELOG.md",
@@ -215,14 +225,17 @@
215
225
  "eventLog": [
216
226
  "./dist/esm/eventLog.d.ts"
217
227
  ],
218
- "coordination": [
219
- "./dist/esm/coordination.d.ts"
228
+ "spatial": [
229
+ "./dist/esm/spatial.d.ts"
220
230
  ],
221
231
  "react": [
222
232
  "./dist/esm/react.d.ts"
223
233
  ],
224
234
  "*": [
225
235
  "./dist/esm/index.d.ts"
236
+ ],
237
+ "coordination": [
238
+ "./dist/esm/coordination.d.ts"
226
239
  ]
227
240
  }
228
241
  },