@series-inc/rundot-kinetix 0.0.0-bootstrap.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.
Files changed (58) hide show
  1. package/README.md +77 -0
  2. package/authoring.d.ts +30 -0
  3. package/index.d.ts +221 -0
  4. package/native/component_runtime.cpp +341 -0
  5. package/native/component_runtime.hpp +112 -0
  6. package/native/deterministic_math.cpp +594 -0
  7. package/native/deterministic_math.hpp +21 -0
  8. package/native/kinetix-f64-native-profile.cpp +406 -0
  9. package/native/kinetix-f64-native-profile.hpp +5 -0
  10. package/native/kinetix-fixed1000-native-profile.cpp +777 -0
  11. package/native/kinetix-fixed1000-native-profile.hpp +58 -0
  12. package/native/kinetix-fixed1000-physics.cpp +887 -0
  13. package/native/kinetix-fixed1000-physics.hpp +28 -0
  14. package/native/kinetix-installed-f64-renderer.cpp +344 -0
  15. package/native/kinetix-installed-f64-renderer.hpp +35 -0
  16. package/native/kinetix-installed-f64-runtime.cpp +1085 -0
  17. package/native/kinetix-installed-f64-runtime.hpp +141 -0
  18. package/native/kinetix-installed-fixed1000-data.hpp +77 -0
  19. package/native/kinetix-native-main.cpp +37 -0
  20. package/native/kinetix-native-runtime.cpp +20 -0
  21. package/native/kinetix-native-runtime.hpp +25 -0
  22. package/native/kinetix-render-projection.cpp +20 -0
  23. package/native/kinetix-render-projection.hpp +14 -0
  24. package/package.json +65 -0
  25. package/runtime.d.ts +76 -0
  26. package/scripts/build-native.mjs +67 -0
  27. package/scripts/emit-product-digests.mjs +33 -0
  28. package/scripts/preflight.mjs +76 -0
  29. package/src/index.mjs +57 -0
  30. package/src/kinetix-authoring.mjs +69 -0
  31. package/src/kinetix-baker.mjs +587 -0
  32. package/src/kinetix-deterministic-math.mjs +1044 -0
  33. package/src/kinetix-fixed1000-runtime-adapter.mjs +33 -0
  34. package/src/kinetix-fixed1000-runtime.mjs +954 -0
  35. package/src/kinetix-installed-f64-reference.mjs +157 -0
  36. package/src/kinetix-installed-f64-render-frames.mjs +53 -0
  37. package/src/kinetix-installed-f64-renderer.mjs +240 -0
  38. package/src/kinetix-installed-f64-runtime-adapter.mjs +68 -0
  39. package/src/kinetix-installed-f64-runtime.mjs +607 -0
  40. package/src/kinetix-installed-mechanics.mjs +377 -0
  41. package/src/kinetix-installed-systems.mjs +181 -0
  42. package/src/kinetix-native-product.mjs +72 -0
  43. package/src/kinetix-product-contract.mjs +121 -0
  44. package/src/kinetix-project-compiler.mjs +1017 -0
  45. package/src/kinetix-render-projection.mjs +28 -0
  46. package/src/kinetix-runtime-adapter-utils.mjs +168 -0
  47. package/src/kinetix-runtime-contract.mjs +54 -0
  48. package/src/kinetix-session-config.mjs +170 -0
  49. package/src/kinetix-source-snapshot.mjs +24 -0
  50. package/src/kinetix-survival-runtime-adapter.mjs +305 -0
  51. package/src/kinetix-survival-runtime.generated.mjs +1 -0
  52. package/src/kinetix-world-kernel.mjs +580 -0
  53. package/src/kinetix-world-runtime.mjs +171 -0
  54. package/src/runtime.mjs +14 -0
  55. package/src/shared/f64-bits.mjs +14 -0
  56. package/src/shared/kinetix-envelope-v1.mjs +589 -0
  57. package/src/shared/render-records-v1.mjs +168 -0
  58. package/src/shared/sha256.mjs +73 -0
@@ -0,0 +1,589 @@
1
+ import { createHash } from 'node:crypto';
2
+
3
+ export const kinetixEnvelopeVersion = 'rundot.kinetix-envelope.v1';
4
+ export const kinetixDomains = Object.freeze(['simulation', 'presentation', 'binding']);
5
+ export const kinetixReferenceKinds = Object.freeze([
6
+ 'entity',
7
+ 'component',
8
+ 'prefab',
9
+ 'prefab-local-entity',
10
+ 'prefab-local-component',
11
+ 'asset',
12
+ 'scene',
13
+ ]);
14
+
15
+ const identifierPattern = /^[A-Za-z][A-Za-z0-9._-]*$/u;
16
+ const sha256Pattern = /^[0-9a-f]{64}$/u;
17
+ const prohibitedKeys = Object.freeze([
18
+ { fragment: 'script', code: 'KINETIX_PROHIBITED_SCRIPT' },
19
+ { fragment: 'bytecode', code: 'KINETIX_PROHIBITED_BYTECODE' },
20
+ { fragment: 'wasm', code: 'KINETIX_PROHIBITED_WASM' },
21
+ { fragment: 'shader', code: 'KINETIX_PROHIBITED_SHADER_SOURCE' },
22
+ ]);
23
+ const prohibitedContent = Object.freeze([
24
+ { code: 'KINETIX_PROHIBITED_SCRIPT', signatures: ['eval(', 'new Function', 'import(', 'require(', '<script', 'javascript:'] },
25
+ { code: 'KINETIX_PROHIBITED_BYTECODE', signatures: ['\u007fELF', 'MZ'] },
26
+ { code: 'KINETIX_PROHIBITED_WASM', signatures: ['\u0000asm'] },
27
+ { code: 'KINETIX_PROHIBITED_SHADER_SOURCE', signatures: ['gl_Position', 'gl_FragColor', '#version ', '[[stage_in]]', 'kernel void'] },
28
+ ]);
29
+ const budgetKeys = Object.freeze([
30
+ 'maxDirectEntities',
31
+ 'maxPrefabs',
32
+ 'maxLocalEntitiesPerPrefab',
33
+ 'maxComponents',
34
+ 'maxLiveRuntimeEntities',
35
+ 'maxSimulationRecords',
36
+ 'maxPresentationRecords',
37
+ 'maxBindingRecords',
38
+ 'maxCommandsPerTick',
39
+ ]);
40
+
41
+ function isPlainObject(value) {
42
+ if (typeof value !== 'object' || value === null || Array.isArray(value)) {
43
+ return false;
44
+ }
45
+ const prototype = Object.getPrototypeOf(value);
46
+ return prototype === Object.prototype || prototype === null;
47
+ }
48
+
49
+ function addDiagnostic(diagnostics, code, path) {
50
+ if (!diagnostics.some((entry) => entry.code === code && entry.path === path)) {
51
+ diagnostics.push({ code, path });
52
+ }
53
+ }
54
+
55
+ function validateIdentifier(value, path, diagnostics, code = 'KINETIX_INVALID_ID') {
56
+ if (typeof value !== 'string' || !identifierPattern.test(value)) {
57
+ addDiagnostic(diagnostics, code, path);
58
+ return false;
59
+ }
60
+ return true;
61
+ }
62
+
63
+ function validateUniqueIds(values, path, diagnostics, validateEntry) {
64
+ if (!Array.isArray(values)) {
65
+ addDiagnostic(diagnostics, 'KINETIX_INVALID_STRUCTURE', path);
66
+ return new Map();
67
+ }
68
+ const byId = new Map();
69
+ values.forEach((value, index) => {
70
+ const entryPath = `${path}[${index}]`;
71
+ if (!isPlainObject(value)) {
72
+ addDiagnostic(diagnostics, 'KINETIX_INVALID_STRUCTURE', entryPath);
73
+ return;
74
+ }
75
+ if (validateIdentifier(value.id, `${entryPath}.id`, diagnostics)) {
76
+ if (byId.has(value.id)) {
77
+ addDiagnostic(diagnostics, 'KINETIX_DUPLICATE_ID', `${entryPath}.id`);
78
+ } else {
79
+ byId.set(value.id, { value, index, path: entryPath });
80
+ }
81
+ }
82
+ validateEntry(value, entryPath, index);
83
+ });
84
+ return byId;
85
+ }
86
+
87
+ function scanSafety(value, path, diagnostics, seen) {
88
+ const valueType = typeof value;
89
+ if (value === null || valueType === 'boolean' || valueType === 'string') {
90
+ if (valueType === 'string') {
91
+ for (const rule of prohibitedContent) {
92
+ if (rule.signatures.some((signature) => value.includes(signature))) {
93
+ addDiagnostic(diagnostics, rule.code, path);
94
+ }
95
+ }
96
+ }
97
+ return;
98
+ }
99
+ if (valueType === 'number') {
100
+ if (!Number.isFinite(value)) {
101
+ addDiagnostic(diagnostics, 'KINETIX_UNSAFE_VALUE', path);
102
+ }
103
+ return;
104
+ }
105
+ if (valueType !== 'object') {
106
+ addDiagnostic(diagnostics, 'KINETIX_UNSAFE_VALUE', path);
107
+ return;
108
+ }
109
+ if (seen.has(value)) {
110
+ addDiagnostic(diagnostics, 'KINETIX_UNSAFE_VALUE', path);
111
+ return;
112
+ }
113
+ seen.add(value);
114
+ if (Array.isArray(value)) {
115
+ value.forEach((entry, index) => scanSafety(entry, `${path}[${index}]`, diagnostics, seen));
116
+ return;
117
+ }
118
+ if (!isPlainObject(value)) {
119
+ addDiagnostic(diagnostics, 'KINETIX_UNSAFE_VALUE', path);
120
+ return;
121
+ }
122
+ for (const [key, entry] of Object.entries(value)) {
123
+ const entryPath = `${path}.${key}`;
124
+ if (key === '__proto__' || key === 'prototype' || key === 'constructor') {
125
+ addDiagnostic(diagnostics, 'KINETIX_UNSAFE_KEY', entryPath);
126
+ }
127
+ const lowerKey = key.toLowerCase();
128
+ for (const rule of prohibitedKeys) {
129
+ if (lowerKey.includes(rule.fragment)) {
130
+ addDiagnostic(diagnostics, rule.code, entryPath);
131
+ }
132
+ }
133
+ scanSafety(entry, entryPath, diagnostics, seen);
134
+ }
135
+ }
136
+
137
+ function validateExactKeys(value, allowedKeys, path, diagnostics) {
138
+ for (const key of Object.keys(value)) {
139
+ if (!allowedKeys.includes(key)) {
140
+ addDiagnostic(diagnostics, 'KINETIX_REFERENCE_INVALID', `${path}.${key}`);
141
+ }
142
+ }
143
+ for (const key of allowedKeys) {
144
+ if (key !== 'prefabId' && !Object.hasOwn(value, key)) {
145
+ addDiagnostic(diagnostics, 'KINETIX_REFERENCE_INVALID', `${path}.${key}`);
146
+ }
147
+ }
148
+ }
149
+
150
+ function validateAllowedKeys(value, allowedKeys, path, diagnostics) {
151
+ for (const key of Object.keys(value)) {
152
+ if (!allowedKeys.includes(key)) {
153
+ addDiagnostic(diagnostics, 'KINETIX_UNKNOWN_FIELD', `${path}.${key}`);
154
+ }
155
+ }
156
+ }
157
+
158
+ function validateReference(reference, path, context, indexes, diagnostics) {
159
+ const kind = reference.$kinetixRef;
160
+ if (!kinetixReferenceKinds.includes(kind)) {
161
+ addDiagnostic(diagnostics, 'KINETIX_REFERENCE_INVALID', `${path}.$kinetixRef`);
162
+ return;
163
+ }
164
+ const shapes = {
165
+ entity: ['$kinetixRef', 'id'],
166
+ component: ['$kinetixRef', 'entityId', 'componentId'],
167
+ prefab: ['$kinetixRef', 'id'],
168
+ 'prefab-local-entity': ['$kinetixRef', 'id', 'prefabId'],
169
+ 'prefab-local-component': ['$kinetixRef', 'entityId', 'componentId', 'prefabId'],
170
+ asset: ['$kinetixRef', 'id'],
171
+ scene: ['$kinetixRef', 'id'],
172
+ };
173
+ validateExactKeys(reference, shapes[kind], path, diagnostics);
174
+
175
+ if (kind === 'entity' || kind === 'component') {
176
+ if (context.prefabId !== null) {
177
+ addDiagnostic(diagnostics, 'KINETIX_REFERENCE_SCOPE', path);
178
+ return;
179
+ }
180
+ const entityId = kind === 'entity' ? reference.id : reference.entityId;
181
+ const entityEntry = indexes.entities.get(entityId);
182
+ const entityPath = kind === 'entity' ? `${path}.id` : `${path}.entityId`;
183
+ if (!entityEntry) {
184
+ addDiagnostic(diagnostics, 'KINETIX_REFERENCE_NOT_FOUND', entityPath);
185
+ return;
186
+ }
187
+ if (context.section !== null && entityEntry.value.section !== context.section && entityEntry.value.section !== 0) {
188
+ addDiagnostic(diagnostics, 'KINETIX_REFERENCE_SECTION', entityPath);
189
+ }
190
+ if (kind === 'component') {
191
+ const componentExists = Array.isArray(entityEntry.value.components)
192
+ && entityEntry.value.components.some((component) => component?.id === reference.componentId);
193
+ if (!componentExists) {
194
+ addDiagnostic(diagnostics, 'KINETIX_REFERENCE_NOT_FOUND', `${path}.componentId`);
195
+ }
196
+ }
197
+ return;
198
+ }
199
+
200
+ if (kind === 'prefab') {
201
+ if (!indexes.prefabs.has(reference.id)) {
202
+ addDiagnostic(diagnostics, 'KINETIX_REFERENCE_NOT_FOUND', `${path}.id`);
203
+ }
204
+ return;
205
+ }
206
+
207
+ if (kind === 'asset') {
208
+ if (!indexes.assets.has(reference.id)) {
209
+ addDiagnostic(diagnostics, 'KINETIX_REFERENCE_NOT_FOUND', `${path}.id`);
210
+ }
211
+ return;
212
+ }
213
+
214
+ if (kind === 'scene') {
215
+ if (reference.id !== indexes.sceneId && !indexes.scenes.has(reference.id)) {
216
+ addDiagnostic(diagnostics, 'KINETIX_REFERENCE_NOT_FOUND', `${path}.id`);
217
+ }
218
+ return;
219
+ }
220
+
221
+ if (kind === 'prefab-local-entity' && context.prefabId === null) {
222
+ addDiagnostic(diagnostics, 'KINETIX_REFERENCE_SCOPE', path);
223
+ return;
224
+ }
225
+
226
+ const explicitPrefabId = reference.prefabId;
227
+ if (context.prefabId !== null && explicitPrefabId !== undefined && explicitPrefabId !== context.prefabId) {
228
+ addDiagnostic(diagnostics, 'KINETIX_REFERENCE_SCOPE', `${path}.prefabId`);
229
+ return;
230
+ }
231
+ const prefabId = explicitPrefabId ?? context.prefabId;
232
+ if (prefabId === null || prefabId === undefined) {
233
+ addDiagnostic(diagnostics, 'KINETIX_REFERENCE_SCOPE', `${path}.prefabId`);
234
+ return;
235
+ }
236
+ const prefabEntry = indexes.prefabs.get(prefabId);
237
+ if (!prefabEntry) {
238
+ addDiagnostic(diagnostics, 'KINETIX_REFERENCE_NOT_FOUND', `${path}.prefabId`);
239
+ return;
240
+ }
241
+ const localEntityId = kind === 'prefab-local-entity' ? reference.id : reference.entityId;
242
+ const localEntity = Array.isArray(prefabEntry.value.entities)
243
+ ? prefabEntry.value.entities.find((entity) => entity?.id === localEntityId)
244
+ : null;
245
+ if (!localEntity) {
246
+ const entityPath = kind === 'prefab-local-entity' ? `${path}.id` : `${path}.entityId`;
247
+ addDiagnostic(diagnostics, 'KINETIX_REFERENCE_NOT_FOUND', entityPath);
248
+ return;
249
+ }
250
+ if (kind === 'prefab-local-component') {
251
+ const componentExists = Array.isArray(localEntity.components)
252
+ && localEntity.components.some((component) => component?.id === reference.componentId);
253
+ if (!componentExists) {
254
+ addDiagnostic(diagnostics, 'KINETIX_REFERENCE_NOT_FOUND', `${path}.componentId`);
255
+ }
256
+ }
257
+ }
258
+
259
+ function scanReferences(value, path, context, indexes, diagnostics) {
260
+ if (Array.isArray(value)) {
261
+ value.forEach((entry, index) => scanReferences(entry, `${path}[${index}]`, context, indexes, diagnostics));
262
+ return;
263
+ }
264
+ if (!isPlainObject(value)) {
265
+ return;
266
+ }
267
+ if (Object.hasOwn(value, '$kinetixRef')) {
268
+ validateReference(value, path, context, indexes, diagnostics);
269
+ return;
270
+ }
271
+ for (const [key, entry] of Object.entries(value)) {
272
+ scanReferences(entry, `${path}.${key}`, context, indexes, diagnostics);
273
+ }
274
+ }
275
+
276
+ function validateComponent(component, path, diagnostics) {
277
+ if (!isPlainObject(component)) {
278
+ addDiagnostic(diagnostics, 'KINETIX_INVALID_COMPONENT', path);
279
+ return;
280
+ }
281
+ validateAllowedKeys(component, ['id', 'type', 'schemaVersion', 'domain', 'properties'], path, diagnostics);
282
+ validateIdentifier(component.id, `${path}.id`, diagnostics);
283
+ validateIdentifier(component.type, `${path}.type`, diagnostics, 'KINETIX_INVALID_COMPONENT');
284
+ if (!Number.isSafeInteger(component.schemaVersion) || component.schemaVersion < 1) {
285
+ addDiagnostic(diagnostics, 'KINETIX_INVALID_SCHEMA_VERSION', `${path}.schemaVersion`);
286
+ }
287
+ if (component.domain === 'ui') {
288
+ addDiagnostic(diagnostics, 'KINETIX_DOMAIN_RESERVED', `${path}.domain`);
289
+ } else if (!kinetixDomains.includes(component.domain)) {
290
+ addDiagnostic(diagnostics, 'KINETIX_DOMAIN_UNKNOWN', `${path}.domain`);
291
+ }
292
+ if (!isPlainObject(component.properties)) {
293
+ addDiagnostic(diagnostics, 'KINETIX_INVALID_COMPONENT', `${path}.properties`);
294
+ }
295
+ }
296
+
297
+ function validateComponents(components, path, diagnostics) {
298
+ return validateUniqueIds(components, path, diagnostics, (component, componentPath) => {
299
+ validateComponent(component, componentPath, diagnostics);
300
+ });
301
+ }
302
+
303
+ function validateHierarchy(entries, path, parentKind, diagnostics) {
304
+ if (!Array.isArray(entries)) {
305
+ return;
306
+ }
307
+ const byId = new Map(entries.filter(isPlainObject).map((entry, index) => [entry.id, { entry, index }]));
308
+ const states = new Map();
309
+ function visit(id, stack) {
310
+ const state = states.get(id);
311
+ if (state === 'done') {
312
+ return;
313
+ }
314
+ if (state === 'visiting') {
315
+ return;
316
+ }
317
+ states.set(id, 'visiting');
318
+ stack.push(id);
319
+ const current = byId.get(id);
320
+ const parent = current?.entry.parent;
321
+ if (isPlainObject(parent) && parent.$kinetixRef === parentKind) {
322
+ const parentId = parentKind === 'entity' ? parent.id : parent.id;
323
+ if (states.get(parentId) === 'visiting') {
324
+ const firstCycleId = stack[stack.indexOf(parentId)];
325
+ addDiagnostic(diagnostics, 'KINETIX_HIERARCHY_CYCLE', `${path}[${byId.get(firstCycleId).index}].parent`);
326
+ } else if (byId.has(parentId)) {
327
+ visit(parentId, stack);
328
+ }
329
+ }
330
+ stack.pop();
331
+ states.set(id, 'done');
332
+ }
333
+ for (const id of byId.keys()) {
334
+ visit(id, []);
335
+ }
336
+ }
337
+
338
+ function canonicalize(value) {
339
+ if (Array.isArray(value)) {
340
+ return value.map(canonicalize);
341
+ }
342
+ if (isPlainObject(value)) {
343
+ return Object.fromEntries(
344
+ Object.keys(value)
345
+ .sort()
346
+ .map((key) => [key, canonicalize(value[key])]),
347
+ );
348
+ }
349
+ return value;
350
+ }
351
+
352
+ export function validateKinetixEnvelope(envelope) {
353
+ const diagnostics = [];
354
+ scanSafety(envelope, '$', diagnostics, new Set());
355
+ if (!isPlainObject(envelope)) {
356
+ addDiagnostic(diagnostics, 'KINETIX_INVALID_STRUCTURE', '$');
357
+ return { valid: false, diagnostics };
358
+ }
359
+ if (envelope.version !== kinetixEnvelopeVersion) {
360
+ addDiagnostic(diagnostics, 'KINETIX_VERSION_UNSUPPORTED', '$.version');
361
+ }
362
+ if (Array.isArray(envelope.payloads)) {
363
+ envelope.payloads.forEach((payload, index) => {
364
+ if (payload?.name === 'ui') {
365
+ addDiagnostic(diagnostics, 'KINETIX_DOMAIN_RESERVED', `$.payloads[${index}].name`);
366
+ }
367
+ });
368
+ }
369
+ validateAllowedKeys(
370
+ envelope,
371
+ ['version', 'sceneId', 'sceneDependencies', 'profiles', 'assets', 'budgets', 'prefabs', 'entities', 'compilerProvenance', 'semanticCoverage'],
372
+ '$',
373
+ diagnostics,
374
+ );
375
+ validateIdentifier(envelope.sceneId, '$.sceneId', diagnostics);
376
+
377
+ if (envelope.compilerProvenance !== undefined) {
378
+ const provenance = envelope.compilerProvenance;
379
+ if (!isPlainObject(provenance)) {
380
+ addDiagnostic(diagnostics, 'KINETIX_COMPILER_PROVENANCE_INVALID', '$.compilerProvenance');
381
+ } else {
382
+ validateAllowedKeys(provenance, ['compilerVersion', 'targetProfile', 'sourceSnapshot'], '$.compilerProvenance', diagnostics);
383
+ if (typeof provenance.compilerVersion !== 'string' || provenance.compilerVersion.length === 0
384
+ || typeof provenance.targetProfile !== 'string' || provenance.targetProfile.length === 0
385
+ || !isPlainObject(provenance.sourceSnapshot)) {
386
+ addDiagnostic(diagnostics, 'KINETIX_COMPILER_PROVENANCE_INVALID', '$.compilerProvenance');
387
+ } else {
388
+ validateAllowedKeys(provenance.sourceSnapshot, ['commit', 'archiveSha256'], '$.compilerProvenance.sourceSnapshot', diagnostics);
389
+ if (!/^(?:[0-9a-f]{40}|[0-9a-f]{64})$/u.test(provenance.sourceSnapshot.commit ?? '')
390
+ || !sha256Pattern.test(provenance.sourceSnapshot.archiveSha256 ?? '')) {
391
+ addDiagnostic(diagnostics, 'KINETIX_COMPILER_PROVENANCE_INVALID', '$.compilerProvenance.sourceSnapshot');
392
+ }
393
+ }
394
+ }
395
+ }
396
+
397
+ if (envelope.semanticCoverage !== undefined) {
398
+ if (!Array.isArray(envelope.semanticCoverage)) {
399
+ addDiagnostic(diagnostics, 'KINETIX_SEMANTIC_COVERAGE_INVALID', '$.semanticCoverage');
400
+ } else {
401
+ envelope.semanticCoverage.forEach((entry, index) => {
402
+ const path = `$.semanticCoverage[${index}]`;
403
+ if (!isPlainObject(entry) || !isPlainObject(entry.source)) {
404
+ addDiagnostic(diagnostics, 'KINETIX_SEMANTIC_COVERAGE_INVALID', path);
405
+ return;
406
+ }
407
+ validateAllowedKeys(entry, ['source', 'systemId', 'phase', 'order'], path, diagnostics);
408
+ validateAllowedKeys(entry.source, ['file', 'line', 'column'], `${path}.source`, diagnostics);
409
+ if (typeof entry.source.file !== 'string' || entry.source.file.length === 0
410
+ || !Number.isSafeInteger(entry.source.line) || entry.source.line < 1
411
+ || !Number.isSafeInteger(entry.source.column) || entry.source.column < 1
412
+ || !identifierPattern.test(entry.systemId ?? '')
413
+ || !['initialization', 'fixed-simulation', 'post-simulation', 'presentation-projection', 'presentation'].includes(entry.phase)
414
+ || !Number.isSafeInteger(entry.order) || entry.order < 0) {
415
+ addDiagnostic(diagnostics, 'KINETIX_SEMANTIC_COVERAGE_INVALID', path);
416
+ }
417
+ });
418
+ }
419
+ }
420
+
421
+ const scenes = validateUniqueIds(envelope.sceneDependencies, '$.sceneDependencies', diagnostics, (scene, path) => {
422
+ validateAllowedKeys(scene, ['id', 'sha256'], path, diagnostics);
423
+ if (!sha256Pattern.test(scene.sha256 ?? '')) {
424
+ addDiagnostic(diagnostics, 'KINETIX_INVALID_SCENE_DEPENDENCY', `${path}.sha256`);
425
+ }
426
+ });
427
+ const profiles = validateUniqueIds(envelope.profiles, '$.profiles', diagnostics, (profile, path) => {
428
+ validateAllowedKeys(profile, ['id', 'abi'], path, diagnostics);
429
+ if (typeof profile.abi !== 'string' || profile.abi.length === 0) {
430
+ addDiagnostic(diagnostics, 'KINETIX_INVALID_PROFILE', `${path}.abi`);
431
+ }
432
+ });
433
+ const assets = validateUniqueIds(envelope.assets, '$.assets', diagnostics, (asset, path) => {
434
+ validateAllowedKeys(asset, ['id', 'kind', 'sha256', 'sourceSha256', 'byteLength'], path, diagnostics);
435
+ if (typeof asset.kind !== 'string' || asset.kind.length === 0
436
+ || !sha256Pattern.test(asset.sha256 ?? '')
437
+ || !sha256Pattern.test(asset.sourceSha256 ?? '')
438
+ || !Number.isSafeInteger(asset.byteLength)
439
+ || asset.byteLength < 0) {
440
+ addDiagnostic(diagnostics, 'KINETIX_INVALID_ASSET', path);
441
+ }
442
+ });
443
+
444
+ if (!isPlainObject(envelope.budgets)) {
445
+ addDiagnostic(diagnostics, 'KINETIX_INVALID_BUDGET', '$.budgets');
446
+ } else {
447
+ validateAllowedKeys(envelope.budgets, budgetKeys, '$.budgets', diagnostics);
448
+ for (const key of budgetKeys) {
449
+ const value = envelope.budgets[key];
450
+ const minimum = key === 'maxCommandsPerTick' ? 1 : 0;
451
+ if (!Number.isSafeInteger(value) || value < minimum) {
452
+ addDiagnostic(diagnostics, 'KINETIX_INVALID_BUDGET', `$.budgets.${key}`);
453
+ }
454
+ }
455
+ }
456
+
457
+ const prefabs = validateUniqueIds(envelope.prefabs, '$.prefabs', diagnostics, (prefab, prefabPath) => {
458
+ validateAllowedKeys(prefab, ['id', 'profile', 'poolBudget', 'rootLocalEntityId', 'entities'], prefabPath, diagnostics);
459
+ if (!profiles.has(prefab.profile)) {
460
+ addDiagnostic(diagnostics, 'KINETIX_UNKNOWN_PROFILE', `${prefabPath}.profile`);
461
+ }
462
+ if (!Number.isSafeInteger(prefab.poolBudget) || prefab.poolBudget < 1) {
463
+ addDiagnostic(diagnostics, 'KINETIX_INVALID_BUDGET', `${prefabPath}.poolBudget`);
464
+ }
465
+ const localEntities = validateUniqueIds(prefab.entities, `${prefabPath}.entities`, diagnostics, (entity, entityPath) => {
466
+ validateAllowedKeys(entity, ['id', 'enabled', 'parent', 'components'], entityPath, diagnostics);
467
+ if (Object.hasOwn(entity, 'enabled') && typeof entity.enabled !== 'boolean') {
468
+ addDiagnostic(diagnostics, 'KINETIX_INVALID_ENABLED', `${entityPath}.enabled`);
469
+ }
470
+ validateComponents(entity.components, `${entityPath}.components`, diagnostics);
471
+ });
472
+ if (!localEntities.has(prefab.rootLocalEntityId)) {
473
+ addDiagnostic(diagnostics, 'KINETIX_PREFAB_ROOT_MISSING', `${prefabPath}.rootLocalEntityId`);
474
+ }
475
+ validateHierarchy(prefab.entities, `${prefabPath}.entities`, 'prefab-local-entity', diagnostics);
476
+ });
477
+
478
+ const entities = validateUniqueIds(envelope.entities, '$.entities', diagnostics, (entity, entityPath) => {
479
+ validateAllowedKeys(entity, ['id', 'profile', 'lifecycle', 'section', 'parent', 'enabled', 'components'], entityPath, diagnostics);
480
+ if (!profiles.has(entity.profile)) {
481
+ addDiagnostic(diagnostics, 'KINETIX_UNKNOWN_PROFILE', `${entityPath}.profile`);
482
+ }
483
+ if (entity.lifecycle !== 'static' && entity.lifecycle !== 'slot') {
484
+ addDiagnostic(diagnostics, 'KINETIX_INVALID_LIFECYCLE', `${entityPath}.lifecycle`);
485
+ }
486
+ if (!Number.isSafeInteger(entity.section) || entity.section < 0) {
487
+ addDiagnostic(diagnostics, 'KINETIX_INVALID_SECTION', `${entityPath}.section`);
488
+ }
489
+ if (Object.hasOwn(entity, 'enabled') && typeof entity.enabled !== 'boolean') {
490
+ addDiagnostic(diagnostics, 'KINETIX_INVALID_ENABLED', `${entityPath}.enabled`);
491
+ }
492
+ validateComponents(entity.components, `${entityPath}.components`, diagnostics);
493
+ });
494
+
495
+ const indexes = { assets, entities, prefabs, profiles, scenes, sceneId: envelope.sceneId };
496
+ if (Array.isArray(envelope.prefabs)) {
497
+ envelope.prefabs.forEach((prefab, prefabIndex) => {
498
+ if (!isPlainObject(prefab) || !Array.isArray(prefab.entities)) {
499
+ return;
500
+ }
501
+ prefab.entities.forEach((entity, entityIndex) => {
502
+ if (!isPlainObject(entity)) {
503
+ return;
504
+ }
505
+ const entityPath = `$.prefabs[${prefabIndex}].entities[${entityIndex}]`;
506
+ if (entity.parent !== undefined && entity.parent !== null) {
507
+ if (!isPlainObject(entity.parent) || entity.parent.$kinetixRef !== 'prefab-local-entity') {
508
+ addDiagnostic(diagnostics, 'KINETIX_REFERENCE_INVALID', `${entityPath}.parent`);
509
+ } else {
510
+ validateReference(entity.parent, `${entityPath}.parent`, { prefabId: prefab.id, section: null }, indexes, diagnostics);
511
+ }
512
+ }
513
+ scanReferences(entity.components, `${entityPath}.components`, { prefabId: prefab.id, section: null }, indexes, diagnostics);
514
+ });
515
+ });
516
+ }
517
+ if (Array.isArray(envelope.entities)) {
518
+ envelope.entities.forEach((entity, entityIndex) => {
519
+ if (!isPlainObject(entity)) {
520
+ return;
521
+ }
522
+ const entityPath = `$.entities[${entityIndex}]`;
523
+ if (entity.parent !== undefined && entity.parent !== null) {
524
+ if (!isPlainObject(entity.parent) || entity.parent.$kinetixRef !== 'entity') {
525
+ addDiagnostic(diagnostics, 'KINETIX_REFERENCE_INVALID', `${entityPath}.parent`);
526
+ } else {
527
+ validateReference(entity.parent, `${entityPath}.parent`, { prefabId: null, section: entity.section }, indexes, diagnostics);
528
+ const parentEntry = entities.get(entity.parent.id);
529
+ if (parentEntry && parentEntry.value.section !== entity.section) {
530
+ addDiagnostic(diagnostics, 'KINETIX_REFERENCE_SECTION', `${entityPath}.parent.id`);
531
+ }
532
+ }
533
+ }
534
+ scanReferences(entity.components, `${entityPath}.components`, { prefabId: null, section: entity.section }, indexes, diagnostics);
535
+ });
536
+ validateHierarchy(envelope.entities, '$.entities', 'entity', diagnostics);
537
+ }
538
+
539
+ if (isPlainObject(envelope.budgets)) {
540
+ const directCount = Array.isArray(envelope.entities) ? envelope.entities.length : 0;
541
+ const prefabCount = Array.isArray(envelope.prefabs) ? envelope.prefabs.length : 0;
542
+ if (directCount > envelope.budgets.maxDirectEntities) {
543
+ addDiagnostic(diagnostics, 'KINETIX_BUDGET_EXCEEDED', '$.entities');
544
+ }
545
+ if (prefabCount > envelope.budgets.maxPrefabs) {
546
+ addDiagnostic(diagnostics, 'KINETIX_BUDGET_EXCEEDED', '$.prefabs');
547
+ }
548
+ let componentCount = 0;
549
+ let maxLocalCount = 0;
550
+ let pooledRuntimeCount = 0;
551
+ for (const prefab of Array.isArray(envelope.prefabs) ? envelope.prefabs : []) {
552
+ const localCount = Array.isArray(prefab?.entities) ? prefab.entities.length : 0;
553
+ maxLocalCount = Math.max(maxLocalCount, localCount);
554
+ if (Number.isSafeInteger(prefab?.poolBudget)) {
555
+ pooledRuntimeCount += prefab.poolBudget * localCount;
556
+ }
557
+ for (const entity of Array.isArray(prefab?.entities) ? prefab.entities : []) {
558
+ componentCount += Array.isArray(entity?.components) ? entity.components.length : 0;
559
+ }
560
+ }
561
+ for (const entity of Array.isArray(envelope.entities) ? envelope.entities : []) {
562
+ componentCount += Array.isArray(entity?.components) ? entity.components.length : 0;
563
+ }
564
+ if (maxLocalCount > envelope.budgets.maxLocalEntitiesPerPrefab) {
565
+ const prefabIndex = envelope.prefabs.findIndex((prefab) => prefab.entities.length === maxLocalCount);
566
+ addDiagnostic(diagnostics, 'KINETIX_BUDGET_EXCEEDED', `$.prefabs[${prefabIndex}].entities`);
567
+ }
568
+ if (componentCount > envelope.budgets.maxComponents) {
569
+ addDiagnostic(diagnostics, 'KINETIX_BUDGET_EXCEEDED', '$.budgets.maxComponents');
570
+ }
571
+ if (directCount + pooledRuntimeCount > envelope.budgets.maxLiveRuntimeEntities) {
572
+ addDiagnostic(diagnostics, 'KINETIX_BUDGET_EXCEEDED', '$.budgets.maxLiveRuntimeEntities');
573
+ }
574
+ }
575
+
576
+ return { valid: diagnostics.length === 0, diagnostics };
577
+ }
578
+
579
+ export function stableKinetixEnvelopeBytes(envelope) {
580
+ const result = validateKinetixEnvelope(envelope);
581
+ if (!result.valid) {
582
+ throw new Error(result.diagnostics[0].code);
583
+ }
584
+ return Buffer.from(JSON.stringify(canonicalize(envelope)));
585
+ }
586
+
587
+ export function kinetixEnvelopeDigest(envelope) {
588
+ return createHash('sha256').update(stableKinetixEnvelopeBytes(envelope)).digest('hex');
589
+ }