@series-inc/rundot-syncplay 5.26.0-beta.9 → 5.27.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/dist/physics3d.js CHANGED
@@ -281,6 +281,104 @@ export function applyDeterministicWorldEdit3D(world, edit) {
281
281
  : world.jointCache.filter((entry) => !removeJointIds.has(entry.jointId)),
282
282
  };
283
283
  }
284
+ /**
285
+ * Fast batch update of kinematic body transforms and velocities without
286
+ * allocating Set instances or re-sorting world bodies. Wakes touching dynamic
287
+ * bodies across active contact pairs. If linear velocity (vx, vy, vz) is omitted
288
+ * and dtTicks (> 0) is supplied, linear velocity is derived as (newPos - oldPos) / dtTicks.
289
+ */
290
+ export function setDeterministicKinematicPoses3D(world, poses, options = {}) {
291
+ if (poses.length === 0) {
292
+ return world;
293
+ }
294
+ const dt = options.dtTicks !== undefined && options.dtTicks > 0 ? options.dtTicks : undefined;
295
+ const poseById = new Map();
296
+ for (const pose of poses) {
297
+ if (!Number.isFinite(pose.x) || !Number.isFinite(pose.y) || !Number.isFinite(pose.z)) {
298
+ throw new Error(`setDeterministicKinematicPoses3D: body ${pose.id} position must be finite numbers`);
299
+ }
300
+ poseById.set(pose.id, pose);
301
+ }
302
+ const updatedIds = new Set();
303
+ const movedBodies = new Map();
304
+ const newBodies = world.bodies.map((body) => {
305
+ const pose = poseById.get(body.id);
306
+ if (pose === undefined) {
307
+ return body;
308
+ }
309
+ if (body.kind !== 'kinematic') {
310
+ throw new Error(`setDeterministicKinematicPoses3D: body ${body.id} is ${body.kind}, expected kinematic`);
311
+ }
312
+ updatedIds.add(body.id);
313
+ let vx = pose.vx;
314
+ let vy = pose.vy;
315
+ let vz = pose.vz;
316
+ if (vx === undefined || vy === undefined || vz === undefined) {
317
+ if (dt !== undefined) {
318
+ vx = vx ?? (pose.x - body.x) / dt;
319
+ vy = vy ?? (pose.y - body.y) / dt;
320
+ vz = vz ?? (pose.z - body.z) / dt;
321
+ }
322
+ else {
323
+ vx = vx ?? body.vx;
324
+ vy = vy ?? body.vy;
325
+ vz = vz ?? body.vz;
326
+ }
327
+ }
328
+ let angularVel = body.angularVel;
329
+ if (pose.angularVelX !== undefined || pose.angularVelY !== undefined || pose.angularVelZ !== undefined) {
330
+ angularVel = {
331
+ x: pose.angularVelX ?? body.angularVel?.x ?? 0,
332
+ y: pose.angularVelY ?? body.angularVel?.y ?? 0,
333
+ z: pose.angularVelZ ?? body.angularVel?.z ?? 0,
334
+ };
335
+ }
336
+ const updated = {
337
+ ...body,
338
+ x: pose.x,
339
+ y: pose.y,
340
+ z: pose.z,
341
+ vx,
342
+ vy,
343
+ vz,
344
+ rx: pose.rx ?? body.rx,
345
+ ry: pose.ry ?? body.ry,
346
+ rz: pose.rz ?? body.rz,
347
+ orientation: pose.orientation ?? body.orientation,
348
+ angularVel,
349
+ };
350
+ movedBodies.set(body.id, updated);
351
+ return updated;
352
+ });
353
+ for (const pose of poses) {
354
+ if (!updatedIds.has(pose.id)) {
355
+ throw new Error(`setDeterministicKinematicPoses3D: cannot update unknown kinematic body ${pose.id}`);
356
+ }
357
+ }
358
+ // Wake any dynamic bodies that share active pairs with updated kinematics
359
+ const wokenDynamicIds = new Set();
360
+ for (const pair of world.activePairMetadata) {
361
+ if (movedBodies.has(pair.a)) {
362
+ for (const member of contactIslandFor(world, pair.b)) {
363
+ wokenDynamicIds.add(member);
364
+ }
365
+ }
366
+ else if (movedBodies.has(pair.b)) {
367
+ for (const member of contactIslandFor(world, pair.a)) {
368
+ wokenDynamicIds.add(member);
369
+ }
370
+ }
371
+ }
372
+ const finalBodies = wokenDynamicIds.size === 0
373
+ ? newBodies
374
+ : newBodies.map((body) => (wokenDynamicIds.has(body.id) && body.kind === 'dynamic' && body.sleeping
375
+ ? { ...body, sleeping: false, lowVelFrames: 0 }
376
+ : body));
377
+ return {
378
+ ...world,
379
+ bodies: finalBodies,
380
+ };
381
+ }
284
382
  /**
285
383
  * Apply a deterministic impulse to a body (linear + angular via lever arm),
286
384
  * waking the body and its contact island. Returns a new canonical world.
@@ -3112,6 +3210,27 @@ function rayBodyDistance(body, query) {
3112
3210
  if (body.shape.type === 'hull') {
3113
3211
  return rayHullBodyDistance(body, body.shape, query);
3114
3212
  }
3213
+ if (body.shape.type === 'compound') {
3214
+ let closestDistance;
3215
+ for (let index = 0; index < body.shape.children.length; index += 1) {
3216
+ const child = body.shape.children[index];
3217
+ const childBody = {
3218
+ ...body,
3219
+ x: (body.x ?? 0) + (child.offset?.x ?? 0),
3220
+ y: (body.y ?? 0) + (child.offset?.y ?? 0),
3221
+ z: (body.z ?? 0) + (child.offset?.z ?? 0),
3222
+ shape: child.shape,
3223
+ rx: (body.rx ?? 0) + canonicalQuarterTurns(child.quarterTurns?.x ?? 0) / 4,
3224
+ ry: (body.ry ?? 0) + canonicalQuarterTurns(child.quarterTurns?.y ?? 0) / 4,
3225
+ rz: (body.rz ?? 0) + canonicalQuarterTurns(child.quarterTurns?.z ?? 0) / 4,
3226
+ };
3227
+ const childDistance = rayBodyDistance(childBody, query);
3228
+ if (childDistance !== undefined && (closestDistance === undefined || childDistance < closestDistance)) {
3229
+ closestDistance = childDistance;
3230
+ }
3231
+ }
3232
+ return closestDistance;
3233
+ }
3115
3234
  return rayAabbDistance(toBounds(body), query);
3116
3235
  }
3117
3236
  function rayHullBodyDistance(body, shape, query) {
@@ -3425,13 +3544,61 @@ function countCandidatePairs(bodies) {
3425
3544
  countCandidatePairsCalls += 1;
3426
3545
  return broadphaseCandidatePairs(bodies).length;
3427
3546
  }
3547
+ function compoundChildDetailedHit(body, query, distance) {
3548
+ if (body.shape.type !== 'compound') {
3549
+ return undefined;
3550
+ }
3551
+ let bestChildIndex;
3552
+ let bestNormal;
3553
+ let bestDistDiff = Infinity;
3554
+ for (let index = 0; index < body.shape.children.length; index += 1) {
3555
+ const child = body.shape.children[index];
3556
+ const childBody = {
3557
+ ...body,
3558
+ x: (body.x ?? 0) + (child.offset?.x ?? 0),
3559
+ y: (body.y ?? 0) + (child.offset?.y ?? 0),
3560
+ z: (body.z ?? 0) + (child.offset?.z ?? 0),
3561
+ shape: child.shape,
3562
+ rx: (body.rx ?? 0) + canonicalQuarterTurns(child.quarterTurns?.x ?? 0) / 4,
3563
+ ry: (body.ry ?? 0) + canonicalQuarterTurns(child.quarterTurns?.y ?? 0) / 4,
3564
+ rz: (body.rz ?? 0) + canonicalQuarterTurns(child.quarterTurns?.z ?? 0) / 4,
3565
+ };
3566
+ const childDist = 'dx' in query ? rayBodyDistance(childBody, query) : undefined;
3567
+ if (childDist !== undefined) {
3568
+ const diff = Math.abs(childDist - distance);
3569
+ if (diff < bestDistDiff) {
3570
+ bestDistDiff = diff;
3571
+ bestChildIndex = index;
3572
+ const center = quantizePoint({
3573
+ x: query.x + query.dx * childDist,
3574
+ y: query.y + query.dy * childDist,
3575
+ z: query.z + query.dz * childDist,
3576
+ });
3577
+ bestNormal = hitNormalForBody(childBody, center);
3578
+ }
3579
+ }
3580
+ }
3581
+ if (bestChildIndex !== undefined && bestNormal !== undefined) {
3582
+ return { childIndex: bestChildIndex, normal: bestNormal };
3583
+ }
3584
+ return undefined;
3585
+ }
3428
3586
  function detailedHitForBody(body, query, distance, castRadius) {
3429
3587
  const center = quantizePoint({
3430
3588
  x: query.x + query.dx * distance,
3431
3589
  y: query.y + query.dy * distance,
3432
3590
  z: query.z + query.dz * distance,
3433
3591
  });
3434
- const normal = hitNormalForBody(body, center);
3592
+ let normal;
3593
+ let childIndex;
3594
+ if (body.shape.type === 'compound') {
3595
+ const compoundHit = compoundChildDetailedHit(body, query, distance);
3596
+ childIndex = compoundHit?.childIndex;
3597
+ normal = compoundHit?.normal ?? hitNormalForBody(body, center);
3598
+ }
3599
+ else {
3600
+ normal = hitNormalForBody(body, center);
3601
+ }
3435
3602
  const point = quantizePoint({
3436
3603
  x: center.x - normal.x * castRadius,
3437
3604
  y: center.y - normal.y * castRadius,
@@ -3443,6 +3610,7 @@ function detailedHitForBody(body, query, distance, castRadius) {
3443
3610
  point,
3444
3611
  normal,
3445
3612
  penetration: distance === 0 ? penetrationAtPoint(body, center, castRadius) : 0,
3613
+ childIndex,
3446
3614
  };
3447
3615
  }
3448
3616
  function hitNormalForBody(body, point) {
@@ -3453,6 +3621,24 @@ function hitNormalForBody(body, point) {
3453
3621
  z: point.z - body.z,
3454
3622
  });
3455
3623
  }
3624
+ if (body.shape.type === 'capsule') {
3625
+ const halfHeight = body.shape.halfHeight;
3626
+ let localPoint = { x: point.x - body.x, y: point.y - body.y, z: point.z - body.z };
3627
+ if (body.orientation !== undefined) {
3628
+ localPoint = rotatePointByQuatInverse(body.orientation, localPoint);
3629
+ }
3630
+ const clampedY = Math.max(-halfHeight, Math.min(halfHeight, localPoint.y));
3631
+ const localDelta = {
3632
+ x: localPoint.x,
3633
+ y: localPoint.y - clampedY,
3634
+ z: localPoint.z,
3635
+ };
3636
+ const localNormal = normalizePoint(localDelta);
3637
+ if (body.orientation !== undefined) {
3638
+ return normalizePoint(rotatePointByQuat(body.orientation, localNormal));
3639
+ }
3640
+ return localNormal;
3641
+ }
3456
3642
  const bounds = toBounds(body);
3457
3643
  const candidates = [
3458
3644
  { normal: { x: -1, y: 0, z: 0 }, distance: Math.abs(point.x - bounds.minX), axis: 'x-' },
@@ -0,0 +1,47 @@
1
+ /**
2
+ * The runtime a deterministic room simulates: engine identity plus the
3
+ * canonical session config (the game's rules) it was created with.
4
+ *
5
+ * This is the ONE implementation. The build mints a pin with
6
+ * `createSyncplayRuntimePin`, the publish path validates it, and the room
7
+ * server validates it again on room create — all through
8
+ * `parseSyncplayRuntimeRoomMetadata` below. A second copy of these rules is how
9
+ * a pin could pass one gate and fail another.
10
+ */
11
+ export declare const MAX_SYNCPLAY_SESSION_CONFIG_BYTES: 65536;
12
+ export interface SyncplayRuntimeIdentity {
13
+ readonly abiVersion: 1;
14
+ readonly tickRate: 10 | 20 | 30 | 60;
15
+ readonly inputSchemaId: string;
16
+ readonly stateSchemaId: string;
17
+ readonly deterministicVersion: string;
18
+ readonly engineIdentityHash: string;
19
+ }
20
+ /** Wire shape: `sessionConfigBytes` is canonical base64. */
21
+ export interface SyncplayRuntimeRoomMetadata {
22
+ readonly runtimeIdentity: SyncplayRuntimeIdentity;
23
+ readonly sessionConfigBytes: string;
24
+ readonly sessionConfigDigest: string;
25
+ }
26
+ /** Parsed shape: `sessionConfigBytes` decoded. */
27
+ export interface ParsedSyncplayRuntimeRoomMetadata {
28
+ readonly runtimeIdentity: SyncplayRuntimeIdentity;
29
+ readonly sessionConfigBytes: Uint8Array;
30
+ readonly sessionConfigDigest: string;
31
+ }
32
+ /** A game's runtime config, as its simulation module exports it. */
33
+ export interface SyncplayRuntimeConfigInput {
34
+ readonly runtimeIdentity: SyncplayRuntimeIdentity;
35
+ readonly sessionConfigBytes: Uint8Array;
36
+ readonly sessionConfigDigest: string;
37
+ }
38
+ export declare function parseSyncplayRuntimeRoomMetadata(value: unknown): ParsedSyncplayRuntimeRoomMetadata;
39
+ /**
40
+ * Mint the wire-shaped runtime pin a build stamps into its rooms config.
41
+ *
42
+ * Validates through `parseSyncplayRuntimeRoomMetadata`, so a pin that mints is
43
+ * a pin that publishes and loads. Throws rather than returning a bad pin: the
44
+ * room server treats the pin as authoritative over player-supplied ticket
45
+ * metadata, so a wrong pin is worse than no pin.
46
+ */
47
+ export declare function createSyncplayRuntimePin(input: SyncplayRuntimeConfigInput): SyncplayRuntimeRoomMetadata;
@@ -0,0 +1,84 @@
1
+ /**
2
+ * The runtime a deterministic room simulates: engine identity plus the
3
+ * canonical session config (the game's rules) it was created with.
4
+ *
5
+ * This is the ONE implementation. The build mints a pin with
6
+ * `createSyncplayRuntimePin`, the publish path validates it, and the room
7
+ * server validates it again on room create — all through
8
+ * `parseSyncplayRuntimeRoomMetadata` below. A second copy of these rules is how
9
+ * a pin could pass one gate and fail another.
10
+ */
11
+ import { kinetixSessionConfigDigest, MAX_KINETIX_SESSION_CONFIG_BYTES, } from '@series-inc/rundot-syncplay/core/runtime';
12
+ export const MAX_SYNCPLAY_SESSION_CONFIG_BYTES = MAX_KINETIX_SESSION_CONFIG_BYTES;
13
+ export function parseSyncplayRuntimeRoomMetadata(value) {
14
+ if (!isRecord(value) || !hasExactKeys(value, ['runtimeIdentity', 'sessionConfigBytes', 'sessionConfigDigest'])) {
15
+ throw new Error('syncplay runtime metadata is missing or malformed');
16
+ }
17
+ const identity = value.runtimeIdentity;
18
+ if (!isRecord(identity)
19
+ || !hasExactKeys(identity, ['abiVersion', 'tickRate', 'inputSchemaId', 'stateSchemaId', 'deterministicVersion', 'engineIdentityHash'])
20
+ || identity.abiVersion !== 1
21
+ || ![10, 20, 30, 60].includes(identity.tickRate)
22
+ || !isNonEmptyString(identity.inputSchemaId)
23
+ || !isNonEmptyString(identity.stateSchemaId)
24
+ || !isNonEmptyString(identity.deterministicVersion)
25
+ || !isNonEmptyString(identity.engineIdentityHash)) {
26
+ throw new Error('syncplay runtime identity is malformed');
27
+ }
28
+ if (typeof value.sessionConfigBytes !== 'string' || !isCanonicalBase64(value.sessionConfigBytes)) {
29
+ throw new Error('syncplay session config bytes are not canonical base64');
30
+ }
31
+ const bytes = Uint8Array.from(Buffer.from(value.sessionConfigBytes, 'base64'));
32
+ if (bytes.byteLength < 1 || bytes.byteLength > MAX_SYNCPLAY_SESSION_CONFIG_BYTES) {
33
+ throw new Error('syncplay session config bytes are empty or oversized');
34
+ }
35
+ if (typeof value.sessionConfigDigest !== 'string'
36
+ || value.sessionConfigDigest !== kinetixSessionConfigDigest(bytes)) {
37
+ throw new Error('syncplay session config digest mismatch');
38
+ }
39
+ return {
40
+ runtimeIdentity: identity,
41
+ sessionConfigBytes: bytes,
42
+ sessionConfigDigest: value.sessionConfigDigest,
43
+ };
44
+ }
45
+ /**
46
+ * Mint the wire-shaped runtime pin a build stamps into its rooms config.
47
+ *
48
+ * Validates through `parseSyncplayRuntimeRoomMetadata`, so a pin that mints is
49
+ * a pin that publishes and loads. Throws rather than returning a bad pin: the
50
+ * room server treats the pin as authoritative over player-supplied ticket
51
+ * metadata, so a wrong pin is worse than no pin.
52
+ */
53
+ export function createSyncplayRuntimePin(input) {
54
+ // Check the caller's own keys before re-shaping: building a fresh object
55
+ // would silently drop a stray top-level key instead of rejecting it.
56
+ if (!isRecord(input)
57
+ || !hasExactKeys(input, ['runtimeIdentity', 'sessionConfigBytes', 'sessionConfigDigest'])) {
58
+ throw new Error('syncplay runtime config must carry exactly runtimeIdentity, sessionConfigBytes, sessionConfigDigest');
59
+ }
60
+ if (!(input.sessionConfigBytes instanceof Uint8Array)) {
61
+ throw new Error('syncplay runtime config sessionConfigBytes must be a Uint8Array');
62
+ }
63
+ const pin = {
64
+ runtimeIdentity: input.runtimeIdentity,
65
+ sessionConfigBytes: Buffer.from(input.sessionConfigBytes).toString('base64'),
66
+ sessionConfigDigest: input.sessionConfigDigest,
67
+ };
68
+ parseSyncplayRuntimeRoomMetadata(pin);
69
+ return pin;
70
+ }
71
+ function hasExactKeys(value, expected) {
72
+ return Object.keys(value).sort().join('\0') === [...expected].sort().join('\0');
73
+ }
74
+ function isNonEmptyString(value) {
75
+ return typeof value === 'string' && value.length > 0;
76
+ }
77
+ function isCanonicalBase64(value) {
78
+ if (value.length === 0 || value.length % 4 !== 0 || !/^[A-Za-z0-9+/]*={0,2}$/u.test(value))
79
+ return false;
80
+ return Buffer.from(value, 'base64').toString('base64') === value;
81
+ }
82
+ function isRecord(value) {
83
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
84
+ }
@@ -51,6 +51,30 @@ export declare function encodeDeterministicStateSnapshot(snapshot: Deterministic
51
51
  export declare function decodeDeterministicStateSnapshot(text: string): DeterministicStateSnapshotDecodeResult;
52
52
  export declare function bytesToBase64(bytes: Uint8Array): string;
53
53
  export declare function base64ToBytes(text: string): Uint8Array;
54
+ /**
55
+ * Fixed bytes the wire framing adds around a chunk: 225 B for the
56
+ * `snapshot-chunk` envelope plus 76 B for the SDK transport's outer
57
+ * {type,msgType,data} wrapper. Held at 512 B for headroom on transferId and
58
+ * checksum length changes.
59
+ */
60
+ export declare const SNAPSHOT_CHUNK_ENVELOPE_OVERHEAD_BYTES = 512;
61
+ /**
62
+ * Worst-case multiplicative growth of a chunk string on its way to the wire.
63
+ * `snapshotBytes` is JSON (`encodeDeterministicStateSnapshot` -> canonicalStringify),
64
+ * NOT base64, so it is quote-dense — and it is JSON-escaped TWICE: into the
65
+ * `snapshot-chunk` envelope, then again when the SDK transport stringifies the
66
+ * outer message (run-room-transport `sendRaw` -> RundotServerRoom
67
+ * {type,msgType,data} -> ServerWebSocket `JSON.stringify`). A `"` becomes `\"`
68
+ * becomes `\\\"`. Measured at exactly 4.001x for all-quote and all-backslash
69
+ * payloads; a realistic base64-dominated snapshot lands at ~1.001x.
70
+ *
71
+ * The room server's boot guard requires
72
+ * WS_MAX_MESSAGE_SIZE >= DEFAULT_SNAPSHOT_CHUNK_SIZE * this + the fixed overhead.
73
+ * A cap below that makes `ws` raise "Max payload size exceeded" and close the
74
+ * socket 1006, failing every snapshot transfer for that room — the 2026-08-25
75
+ * lattice incident. See .plans/syncplay-snapshot-cap-and-observability.md.
76
+ */
77
+ export declare const MAX_JSON_ESCAPE_EXPANSION = 4;
54
78
  export declare const DEFAULT_SNAPSHOT_CHUNK_SIZE = 262144;
55
79
  export declare const DEFAULT_MAX_SNAPSHOT_BYTES = 4194304;
56
80
  export interface CreateSnapshotTransferOptions {
@@ -155,6 +155,30 @@ export function base64ToBytes(text) {
155
155
  return bytes;
156
156
  }
157
157
  // ── transfer builder (raw bytes → frozen wire transfer shape) ──
158
+ /**
159
+ * Fixed bytes the wire framing adds around a chunk: 225 B for the
160
+ * `snapshot-chunk` envelope plus 76 B for the SDK transport's outer
161
+ * {type,msgType,data} wrapper. Held at 512 B for headroom on transferId and
162
+ * checksum length changes.
163
+ */
164
+ export const SNAPSHOT_CHUNK_ENVELOPE_OVERHEAD_BYTES = 512;
165
+ /**
166
+ * Worst-case multiplicative growth of a chunk string on its way to the wire.
167
+ * `snapshotBytes` is JSON (`encodeDeterministicStateSnapshot` -> canonicalStringify),
168
+ * NOT base64, so it is quote-dense — and it is JSON-escaped TWICE: into the
169
+ * `snapshot-chunk` envelope, then again when the SDK transport stringifies the
170
+ * outer message (run-room-transport `sendRaw` -> RundotServerRoom
171
+ * {type,msgType,data} -> ServerWebSocket `JSON.stringify`). A `"` becomes `\"`
172
+ * becomes `\\\"`. Measured at exactly 4.001x for all-quote and all-backslash
173
+ * payloads; a realistic base64-dominated snapshot lands at ~1.001x.
174
+ *
175
+ * The room server's boot guard requires
176
+ * WS_MAX_MESSAGE_SIZE >= DEFAULT_SNAPSHOT_CHUNK_SIZE * this + the fixed overhead.
177
+ * A cap below that makes `ws` raise "Max payload size exceeded" and close the
178
+ * socket 1006, failing every snapshot transfer for that room — the 2026-08-25
179
+ * lattice incident. See .plans/syncplay-snapshot-cap-and-observability.md.
180
+ */
181
+ export const MAX_JSON_ESCAPE_EXPANSION = 4;
158
182
  export const DEFAULT_SNAPSHOT_CHUNK_SIZE = 262_144;
159
183
  export const DEFAULT_MAX_SNAPSHOT_BYTES = 4_194_304;
160
184
  export function createSnapshotTransferFromBytes(snapshotBytes, options) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@series-inc/rundot-syncplay",
3
- "version": "5.26.0-beta.9",
3
+ "version": "5.27.0",
4
4
  "description": "Kinetix orchestration, rollback, replay, late-join, and deterministic networking for RUN.game",
5
5
  "repository": {
6
6
  "type": "git",
@@ -160,9 +160,10 @@
160
160
  "build": "node -e \"require('node:fs').rmSync('dist',{recursive:true,force:true})\" && tsc -p tsconfig.json",
161
161
  "test": "npm run test:core && npm run test:unit-control && npm run test:entity-registry",
162
162
  "test:module-promotion": "node --expose-gc --import tsx --test tests/module-promotion.test.ts tests/module-docs.test.ts tests/module-bundle-budget.test.ts scripts/prove-module-promotion.test.mjs",
163
- "test:core": "node --expose-gc --import tsx --test --test-concurrency=1 tests/canonical.test.ts tests/canonical-portability.test.ts tests/runtime-session.test.ts tests/kinetix-runtime-session.test.ts tests/runtime-support.test.ts tests/runtime-guards.test.ts tests/r3f-render-adapter.test.ts tests/snapshot-cost.test.ts tests/sdkSession.test.ts tests/input-authority.test.ts tests/session-wire.test.ts tests/session-snapshot.test.ts tests/authority-room.test.ts tests/networked-client-snapshot.test.ts tests/runner.test.ts tests/testing.test.ts tests/browser-session.test.ts tests/secret-config.test.ts tests/secret-protocol.test.ts tests/secret-authority.test.ts tests/secret-client.test.ts tests/late-join-checksum.test.ts tests/late-join-input-backfill.test.ts tests/replay-scoring.test.ts tests/bot-backfill.test.ts tests/latency-simulation.test.ts tests/production-input-budget.test.ts tests/time-sync.test.ts tests/match-log.test.ts tests/spectator.test.ts tests/synctest.test.ts tests/instant-replay.test.ts tests/session-promotion.test.ts tests/physics3d-broadphase-stats.test.ts tests/entrypoints.test.ts tests/export-surface.test.ts tests/module-seam.test.ts tests/module-contract-portability.test.ts tests/module-identity.test.ts tests/module-corpus.test.ts tests/component-model.test.ts tests/component-model-rollback.test.ts tests/component-model-integration.test.ts tests/actors-ai.test.ts tests/character-3d.test.ts tests/character-3d-certification.test.ts tests/combat-modules.test.ts tests/combat-modules-integration.test.ts tests/firearm.test.ts tests/gameplay-effects.test.ts tests/spatial-interaction.test.ts tests/stats-abilities.test.ts tests/inventory-equipment.test.ts tests/inventory-equipment-certification.test.ts tests/platformer-2d.test.ts tests/platformer-2d-certification.test.ts tests/module-promotion.test.ts tests/module-docs.test.ts tests/module-bundle-budget.test.ts tests/published-side-effects.test.ts tests/host-timing.test.ts tests/test-manifest.test.ts tests/coarse-region.test.ts tests/input-codec.test.ts tests/command-ingress.test.ts tests/session-reconfigure.test.ts tests/prediction-mode.test.ts tests/ephemeral-channel.test.ts tests/runner-surface.test.ts tests/runner-harness.test.ts tests/effects-adapter-bounds.test.ts tests/simulated-match-secrets.test.ts tests/run-glue-surface.test.mjs core/tests/core-boundary.test.mjs tests/math-core-parity.test.ts tests/math-profile.test.ts tests/movement3d-kcc-unit-contract.test.ts tests/movement3d-static-ground.test.ts",
163
+ "test:core": "node --expose-gc --import tsx --test --test-concurrency=1 tests/canonical.test.ts tests/canonical-portability.test.ts tests/runtime-session.test.ts tests/kinetix-runtime-session.test.ts tests/runtime-support.test.ts tests/runtime-guards.test.ts tests/r3f-render-adapter.test.ts tests/snapshot-cost.test.ts tests/sdkSession.test.ts tests/input-authority.test.ts tests/session-wire.test.ts tests/runtime-room-metadata.test.ts tests/session-snapshot.test.ts tests/authority-room.test.ts tests/networked-client-snapshot.test.ts tests/runner.test.ts tests/testing.test.ts tests/browser-session.test.ts tests/secret-config.test.ts tests/secret-protocol.test.ts tests/secret-authority.test.ts tests/secret-client.test.ts tests/late-join-checksum.test.ts tests/late-join-input-backfill.test.ts tests/replay-scoring.test.ts tests/bot-backfill.test.ts tests/latency-simulation.test.ts tests/production-input-budget.test.ts tests/time-sync.test.ts tests/match-log.test.ts tests/spectator.test.ts tests/synctest.test.ts tests/instant-replay.test.ts tests/session-promotion.test.ts tests/physics3d-broadphase-stats.test.ts tests/entrypoints.test.ts tests/export-surface.test.ts tests/module-seam.test.ts tests/module-contract-portability.test.ts tests/module-identity.test.ts tests/module-corpus.test.ts tests/component-model.test.ts tests/component-model-rollback.test.ts tests/component-model-integration.test.ts tests/actors-ai.test.ts tests/character-3d.test.ts tests/character-3d-certification.test.ts tests/combat-modules.test.ts tests/combat-modules-integration.test.ts tests/firearm.test.ts tests/gameplay-effects.test.ts tests/spatial-interaction.test.ts tests/stats-abilities.test.ts tests/inventory-equipment.test.ts tests/inventory-equipment-certification.test.ts tests/platformer-2d.test.ts tests/platformer-2d-certification.test.ts tests/module-promotion.test.ts tests/module-docs.test.ts tests/module-bundle-budget.test.ts tests/published-side-effects.test.ts tests/host-timing.test.ts tests/test-manifest.test.ts tests/coarse-region.test.ts tests/input-codec.test.ts tests/command-ingress.test.ts tests/session-reconfigure.test.ts tests/prediction-mode.test.ts tests/ephemeral-channel.test.ts tests/runner-surface.test.ts tests/runner-harness.test.ts tests/effects-adapter-bounds.test.ts tests/simulated-match-secrets.test.ts tests/run-glue-surface.test.mjs core/tests/core-boundary.test.mjs tests/math-core-parity.test.ts tests/math-profile.test.ts tests/movement3d-kcc-unit-contract.test.ts tests/movement3d-static-ground.test.ts",
164
+ "test:sdk-contract": "node --import tsx --test tests/sdk-runtime-contract.test.ts",
164
165
  "test:soak": "tsx --test tests/runtime-soak.test.ts",
165
- "test:extended": "tsx --test tests/kcc-vehicle-characterization.test.ts tests/animation.test.ts tests/asset-cooking.test.ts tests/backend-proof-checksum-vectors.test.ts tests/bot-documents.test.ts tests/bot-primitives.test.ts tests/collider-cooking.test.ts tests/collision.test.ts tests/command-timeline.test.ts tests/config-bundle.test.ts tests/cosmetic-physics3d.test.ts tests/deterministic-primitives.test.ts tests/ecs-lite.test.ts tests/effects-adapter.test.ts tests/engine-runtime.test.ts tests/errors.test.ts tests/events.test.ts tests/input-button.test.ts tests/load-128-players.test.ts tests/math.test.ts tests/movement.test.ts tests/movement3d.test.ts tests/multiclient-authority.test.ts tests/navigation.test.ts tests/noise.test.ts tests/physics-cert.test.ts tests/physics2d.test.ts tests/physics3d-ccd.test.ts tests/physics3d-coverage.test.ts tests/physics3d-destruction-cert.test.ts tests/physics3d-geometry-limits.test.ts tests/physics3d-heightfield.test.ts tests/physics3d-hull.test.ts tests/physics3d-joints.test.ts tests/physics3d-kinematic-mesh.test.ts tests/physics3d-mass.test.ts tests/physics3d-materials.test.ts tests/physics3d-mesh-onesided.test.ts tests/physics3d-query-index.test.ts tests/physics3d-recipes.test.ts tests/physics3d-rotation-convention.test.ts tests/physics3d-shared.test.ts tests/physics3d-sleeping-manifold-reuse.test.ts tests/physics3d-solver.test.ts tests/physics3d-static-memoization.test.ts tests/physics3d-static-overlap-memo.test.ts tests/physics3d-cylinder-hull.test.ts tests/physics3d-stacking.test.ts tests/physics3d-toi.test.ts tests/physics3d-vehicle.test.ts tests/physics3d-voxel.test.ts tests/physics3d-world-edit.test.ts tests/physics3d.test.ts tests/physics-vector-generator.test.ts tests/random.test.ts tests/rollback-history.test.ts tests/schema-artifacts.test.ts tests/schema-codegen.test.ts tests/schema-dsl-binary.test.ts tests/schema.test.ts tests/signals.test.ts tests/sparse-set.test.ts tests/static-checker.test.ts tests/system-lifecycle.test.ts tests/wire-canonical-sentinels.test.ts tests/ws-frame.test.ts",
166
+ "test:extended": "tsx --test tests/kcc-vehicle-characterization.test.ts tests/animation.test.ts tests/asset-cooking.test.ts tests/backend-proof-checksum-vectors.test.ts tests/bot-documents.test.ts tests/bot-primitives.test.ts tests/collider-cooking.test.ts tests/collision.test.ts tests/command-timeline.test.ts tests/config-bundle.test.ts tests/cosmetic-physics3d.test.ts tests/deterministic-primitives.test.ts tests/ecs-lite.test.ts tests/effects-adapter.test.ts tests/engine-runtime.test.ts tests/errors.test.ts tests/events.test.ts tests/input-button.test.ts tests/load-128-players.test.ts tests/math.test.ts tests/movement.test.ts tests/movement3d.test.ts tests/multiclient-authority.test.ts tests/navigation.test.ts tests/noise.test.ts tests/physics-cert.test.ts tests/physics2d.test.ts tests/physics3d-ccd.test.ts tests/physics3d-coverage.test.ts tests/physics3d-destruction-cert.test.ts tests/physics3d-geometry-limits.test.ts tests/physics3d-heightfield.test.ts tests/physics3d-hull.test.ts tests/physics3d-joints.test.ts tests/physics3d-kinematic-mesh.test.ts tests/physics3d-kinematic-poses.test.ts tests/physics3d-mass.test.ts tests/physics3d-materials.test.ts tests/physics3d-mesh-onesided.test.ts tests/physics3d-queries-capsule-childindex.test.ts tests/physics3d-query-index.test.ts tests/physics3d-recipes.test.ts tests/physics3d-rotation-convention.test.ts tests/physics3d-shared.test.ts tests/physics3d-sleeping-manifold-reuse.test.ts tests/physics3d-solver.test.ts tests/physics3d-static-memoization.test.ts tests/physics3d-static-overlap-memo.test.ts tests/physics3d-cylinder-hull.test.ts tests/physics3d-stacking.test.ts tests/physics3d-toi.test.ts tests/physics3d-vehicle.test.ts tests/physics3d-voxel-dda.test.ts tests/physics3d-voxel.test.ts tests/physics3d-world-edit.test.ts tests/physics3d.test.ts tests/physics-vector-generator.test.ts tests/random.test.ts tests/rollback-history.test.ts tests/schema-artifacts.test.ts tests/schema-codegen.test.ts tests/schema-dsl-binary.test.ts tests/schema.test.ts tests/signals.test.ts tests/sparse-set.test.ts tests/static-checker.test.ts tests/system-lifecycle.test.ts tests/wire-canonical-sentinels.test.ts tests/ws-frame.test.ts",
166
167
  "test:ci": "npm run build && npm run test",
167
168
  "build:native": "node core/scripts/build-native.mjs",
168
169
  "build:mobile-native": "node core/scripts/build-mobile-native.mjs",