@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,1017 @@
1
+ import { spawnSync } from 'node:child_process';
2
+ import { createHash } from 'node:crypto';
3
+ import { mkdtempSync, readdirSync, rmSync, statSync } from 'node:fs';
4
+ import { tmpdir } from 'node:os';
5
+ import { join, relative, sep } from 'node:path';
6
+ import ts from 'typescript';
7
+ import { kinetixCanonicalSourceDigest } from './kinetix-source-snapshot.mjs';
8
+
9
+ import {
10
+ installedSystemFamiliesForProfile,
11
+ kinetixRuntimeContractForProfile,
12
+ kinetixInstalledSourceSignatures,
13
+ } from './kinetix-installed-systems.mjs';
14
+
15
+ const authoritativeDirectoryNames = new Set(['engine', 'sim', 'simulation']);
16
+ const assignmentOperators = new Set([
17
+ ts.SyntaxKind.EqualsToken,
18
+ ts.SyntaxKind.PlusEqualsToken,
19
+ ts.SyntaxKind.MinusEqualsToken,
20
+ ts.SyntaxKind.AsteriskEqualsToken,
21
+ ts.SyntaxKind.SlashEqualsToken,
22
+ ]);
23
+
24
+ function git(projectRoot, args, encoding = 'utf8') {
25
+ return spawnSync('git', ['-C', projectRoot, ...args], {
26
+ encoding,
27
+ maxBuffer: 1 << 29,
28
+ });
29
+ }
30
+
31
+ function diagnostic(code, file, line, column, message, details = {}) {
32
+ return { code, file, line, column, message, ...details };
33
+ }
34
+
35
+ function sourceFilesUnder(root) {
36
+ const files = [];
37
+ function visit(directory) {
38
+ for (const name of readdirSync(directory).sort()) {
39
+ const path = join(directory, name);
40
+ const stats = statSync(path);
41
+ if (stats.isDirectory()) {
42
+ if (name !== 'node_modules' && name !== '.git' && name !== 'dist' && name !== 'build') {
43
+ visit(path);
44
+ }
45
+ } else if (/\.(?:ts|tsx)$/u.test(name) && !/\.(?:test|spec)\.(?:ts|tsx)$/u.test(name)) {
46
+ files.push(path);
47
+ }
48
+ }
49
+ }
50
+ visit(root);
51
+ return files;
52
+ }
53
+
54
+ function isAuthoritativeFile(snapshotRoot, filePath) {
55
+ const segments = relative(snapshotRoot, filePath).split(sep);
56
+ return segments.some((segment) => authoritativeDirectoryNames.has(segment));
57
+ }
58
+
59
+ function containsStateMutation(node) {
60
+ let found = false;
61
+ function visit(current) {
62
+ if (found || current === undefined) return;
63
+ if (Array.isArray(current)) {
64
+ current.forEach(visit);
65
+ return;
66
+ }
67
+ if (typeof current.kind !== 'number') return;
68
+ if (ts.isBinaryExpression(current) && assignmentOperators.has(current.operatorToken.kind)
69
+ && (ts.isPropertyAccessExpression(current.left) || ts.isElementAccessExpression(current.left))) {
70
+ found = true;
71
+ return;
72
+ }
73
+ ts.forEachChild(current, visit);
74
+ }
75
+ visit(node);
76
+ return found;
77
+ }
78
+
79
+ function sourceOwnsSerializableState(sourceFile) {
80
+ let serializes = false;
81
+ let ticks = false;
82
+ function visit(node) {
83
+ if (ts.isMethodDeclaration(node) && ts.isIdentifier(node.name)) {
84
+ if (node.name.text === 'serialize') serializes = true;
85
+ if (node.name.text === 'tick') ticks = true;
86
+ }
87
+ if (ts.isFunctionDeclaration(node) && node.name?.text === 'step') ticks = true;
88
+ if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name) && node.name.text === 'step') ticks = true;
89
+ if (!serializes || !ticks) ts.forEachChild(node, visit);
90
+ }
91
+ visit(sourceFile);
92
+ return serializes || ticks;
93
+ }
94
+
95
+ function isNonAuthoritativeMember(member) {
96
+ return member === 'hydrate'
97
+ || member === 'serialize'
98
+ || member === 'snapshot'
99
+ || member === 'testHelpers'
100
+ || member?.startsWith('render') === true
101
+ || member?.startsWith('captureRender') === true;
102
+ }
103
+
104
+ function descendantCallNames(node) {
105
+ const names = new Set();
106
+ function visit(current) {
107
+ if (ts.isCallExpression(current)) {
108
+ if (ts.isIdentifier(current.expression)) names.add(current.expression.text);
109
+ if (ts.isPropertyAccessExpression(current.expression)) names.add(current.expression.name.text);
110
+ }
111
+ ts.forEachChild(current, visit);
112
+ }
113
+ visit(node);
114
+ return names;
115
+ }
116
+
117
+ // C2: this predicate must stay free of game symbols. A state-authority boundary is
118
+ // recognized structurally — a branch that mutates nothing but the authority fields
119
+ // themselves is engine plumbing, not gameplay behavior — never by naming a game's
120
+ // authority-advance function.
121
+ function isStateAuthorityBoundary(node, sourceFile) {
122
+ const authorityFields = new Set(['authoritativeState', 'stateAuthorityEnabled']);
123
+ const roots = mutatedFieldNames(node.thenStatement, sourceFile).map((field) => (
124
+ field.split(/[.\[]/u, 1)[0]
125
+ ));
126
+ return roots.length > 0 && roots.every((root) => authorityFields.has(root));
127
+ }
128
+
129
+ function assignedElementRoots(node) {
130
+ const roots = new Set();
131
+ function visit(current) {
132
+ if (ts.isBinaryExpression(current) && assignmentOperators.has(current.operatorToken.kind)
133
+ && ts.isElementAccessExpression(current.left)) {
134
+ let expression = current.left.expression;
135
+ while (ts.isPropertyAccessExpression(expression) || ts.isElementAccessExpression(expression)) {
136
+ expression = expression.expression;
137
+ }
138
+ if (ts.isIdentifier(expression)) roots.add(expression.text);
139
+ }
140
+ ts.forEachChild(current, visit);
141
+ }
142
+ visit(node);
143
+ return roots;
144
+ }
145
+
146
+ function assignedIdentifiers(node) {
147
+ const identifiers = new Set();
148
+ function visit(current) {
149
+ if (ts.isBinaryExpression(current) && assignmentOperators.has(current.operatorToken.kind)
150
+ && ts.isIdentifier(current.left)) identifiers.add(current.left.text);
151
+ ts.forEachChild(current, visit);
152
+ }
153
+ visit(node);
154
+ return identifiers;
155
+ }
156
+
157
+ function mutatedFieldNames(node, sourceFile) {
158
+ const fields = new Set();
159
+ function visit(current) {
160
+ if (ts.isBinaryExpression(current) && assignmentOperators.has(current.operatorToken.kind)
161
+ && (ts.isPropertyAccessExpression(current.left) || ts.isElementAccessExpression(current.left))) {
162
+ fields.add(current.left.getText(sourceFile).replace(/^this\./u, ''));
163
+ }
164
+ ts.forEachChild(current, visit);
165
+ }
166
+ visit(node);
167
+ return [...fields].sort();
168
+ }
169
+
170
+ function matchedFixedConditionalFamily(node) {
171
+ const calls = descendantCallNames(node);
172
+ const elementRoots = assignedElementRoots(node);
173
+ const identifiers = assignedIdentifiers(node);
174
+ if (calls.has('stepVehicle')
175
+ && ['yawTurns', 'speed', 'drifting', 'carVX', 'carVZ'].every((name) => elementRoots.has(name))) {
176
+ return 'vehicle-drive-body-build';
177
+ }
178
+ if (calls.has('stepLap')
179
+ && ['lap', 'cellsVisited', 'finishFrame', 'lapProj'].every((name) => elementRoots.has(name))) {
180
+ return 'vehicle-lap-finish';
181
+ }
182
+ if (calls.has('round')
183
+ && ['carX', 'carZ', 'carVX', 'carVZ'].every((name) => elementRoots.has(name))) {
184
+ return 'vehicle-physics-step';
185
+ }
186
+ const transitionWrites = ['phase', 'phaseStartFrame', 'lobbyDeadlineFrame'];
187
+ if (transitionWrites.some((name) => identifiers.has(name))
188
+ && (calls.has('snapToGrid') || calls.has('deriveLobbySpawns') || identifiers.has('phase'))) {
189
+ return 'match-phase-transition';
190
+ }
191
+ return null;
192
+ }
193
+
194
+ function sourceSemanticFacts(sourceFile) {
195
+ const identifiers = new Set();
196
+ const calls = new Set();
197
+ function visit(node) {
198
+ if (ts.isIdentifier(node)) identifiers.add(node.text);
199
+ if (ts.isCallExpression(node)) {
200
+ if (ts.isIdentifier(node.expression)) calls.add(node.expression.text);
201
+ if (ts.isPropertyAccessExpression(node.expression)) calls.add(node.expression.name.text);
202
+ }
203
+ ts.forEachChild(node, visit);
204
+ }
205
+ visit(sourceFile);
206
+ return { identifiers, calls };
207
+ }
208
+
209
+ function matchedF64SourceFamily(sourceFile) {
210
+ const { identifiers, calls } = sourceSemanticFacts(sourceFile);
211
+ const hasIdentifiers = (...names) => names.every((name) => identifiers.has(name));
212
+ if (calls.has('processGateCollisions') && calls.has('consumeBombAndDetonateAt')) {
213
+ return ['phase-spawn-score-policy', ['processGateCollisions', 'consumeBombAndDetonateAt']];
214
+ }
215
+ if (hasIdentifiers('_bombsEarned', '_livesEarned')) {
216
+ return ['consumable-charge-economy', ['_bombsEarned', '_livesEarned']];
217
+ }
218
+ if (hasIdentifiers('_peak', '_decaying', '_decayElapsed')) {
219
+ return ['score-multiplier-decay', ['_peak', '_decaying', '_decayElapsed']];
220
+ }
221
+ if (hasIdentifiers('dodgeCooldown', 'dodgeTimer', 'dodgeDir')) {
222
+ return ['staged-axis-runner', ['dodgeCooldown', 'dodgeTimer', 'dodgeDir']];
223
+ }
224
+ if (hasIdentifiers('pullRadiusPx', 'pullStrength', 'ingestCount') && calls.has('releaseSpawns')) {
225
+ return ['gravity-well-state-machine', ['pullRadiusPx', 'pullStrength', 'releaseSpawns']];
226
+ }
227
+ if (hasIdentifiers('bodySegments', 'trail', 'heading', 'onBodyBulletHit')) {
228
+ return ['segmented-follow-chain', ['bodySegments', 'trail', 'onBodyBulletHit']];
229
+ }
230
+ if (hasIdentifiers('arcSign', 'LIFETIME', 'curveStrength')) {
231
+ return ['spawn-pellet-growth', ['arcSign', 'LIFETIME', 'curveStrength']];
232
+ }
233
+ if (calls.has('gaussian') && hasIdentifiers('rerollTimer', 'REROLL_SECONDS')) {
234
+ return ['bounded-random-steering', ['gaussian', 'rerollTimer', 'REROLL_SECONDS']];
235
+ }
236
+ if (hasIdentifiers('rerollTimer', 'REROLL_SECONDS') && calls.has('rng')) {
237
+ return ['boundary-crossing-oscillator', ['rng', 'rerollTimer', 'REROLL_SECONDS']];
238
+ }
239
+ if (hasIdentifiers('ACCEL_START', 'ACCEL_END', 'TOP_SPEED') && calls.has('hypot')) {
240
+ return ['target-seeking-drift', ['ACCEL_START', 'ACCEL_END', 'TOP_SPEED']];
241
+ }
242
+ if (hasIdentifiers('_fireCooldowns', '_bullets') && calls.has('releaseBullet')) {
243
+ return ['projectile-lifecycle-collision', ['_fireCooldowns', '_bullets', 'releaseBullet']];
244
+ }
245
+ if (hasIdentifiers('shield', 'heading', 'movementVector') && calls.has('atan2')) {
246
+ return ['axis-drive-fire-control', ['shield', 'heading', 'movementVector']];
247
+ }
248
+ if (hasIdentifiers('_sparks', 'fadeAge') && calls.has('releaseSpark')) {
249
+ return ['pickup-drift-lifecycle', ['_sparks', 'fadeAge', 'releaseSpark']];
250
+ }
251
+ return null;
252
+ }
253
+
254
+ const f64FamilyMutationRoots = new Map([
255
+ ['phase-spawn-score-policy', new Set([
256
+ '_deathAnimTimer', '_duration', '_status', 'b', 'ctx', 'e', 'gateSpawnTimer',
257
+ 'pickup', 'previousShipPositions', 'sparkMagnetSurgeMultiplier', 'spawnSuppressionTimer',
258
+ ])],
259
+ ['consumable-charge-economy', new Set(['_bombs', '_bombsEarned', '_lives', '_livesEarned'])],
260
+ ['score-multiplier-decay', new Set(['_decaying', '_peak', '_value'])],
261
+ ['boundary-crossing-oscillator', new Set(['entity', 'rerollTimer'])],
262
+ ['staged-axis-runner', new Set(['dodgeCooldown', 'dodgeDir', 'dodgeTimer', 'entity'])],
263
+ ['target-seeking-drift', new Set(['entity'])],
264
+ ['gravity-well-state-machine', new Set(['bulletsAbsorbed', 'e', 'entity', 'ingestCount', 'phase'])],
265
+ ['segmented-follow-chain', new Set(['entity', 'heading', 's', 'seg', 'trail'])],
266
+ ['spawn-pellet-growth', new Set(['entity'])],
267
+ ['bounded-random-steering', new Set(['entity', 'rerollTimer'])],
268
+ ['projectile-lifecycle-collision', new Set(['_bullets', '_fireCooldowns', 'b', 'head', 'impact'])],
269
+ ['axis-drive-fire-control', new Set(['heading', 'position', 'shield'])],
270
+ ['pickup-drift-lifecycle', new Set(['_sparks', 's', 'spark'])],
271
+ ]);
272
+
273
+ function f64FamilyMatchesBranch(sourceFamily, node, sourceFile) {
274
+ if (sourceFamily === null) return false;
275
+ const allowedRoots = f64FamilyMutationRoots.get(sourceFamily[0]);
276
+ if (allowedRoots === undefined) return false;
277
+ const roots = mutatedFieldNames(node.thenStatement, sourceFile).map((field) => (
278
+ field.split(/[.\[]/u, 1)[0]
279
+ ));
280
+ return roots.length > 0 && roots.every((root) => allowedRoots.has(root));
281
+ }
282
+
283
+ function classifyUnmatched(snapshotRoot, files, targetProfile, classifyInstalledBranches) {
284
+ const program = ts.createProgram(files, {
285
+ allowJs: false,
286
+ jsx: ts.JsxEmit.ReactJSX,
287
+ module: ts.ModuleKind.ESNext,
288
+ moduleResolution: ts.ModuleResolutionKind.Bundler,
289
+ noEmit: true,
290
+ skipLibCheck: true,
291
+ target: ts.ScriptTarget.ES2022,
292
+ });
293
+ const unmatched = [];
294
+ const coverage = [];
295
+ for (const sourceFile of program.getSourceFiles()) {
296
+ if (sourceFile.isDeclarationFile || !sourceFile.fileName.startsWith(snapshotRoot)
297
+ || !isAuthoritativeFile(snapshotRoot, sourceFile.fileName)
298
+ || !sourceOwnsSerializableState(sourceFile)) {
299
+ continue;
300
+ }
301
+ const f64SourceFamily = targetProfile === 'deterministic-f64' && classifyInstalledBranches
302
+ ? matchedF64SourceFamily(sourceFile)
303
+ : null;
304
+ function visit(node, enclosingMember = null) {
305
+ if (node === undefined) return;
306
+ if (Array.isArray(node)) {
307
+ node.forEach((child) => visit(child, enclosingMember));
308
+ return;
309
+ }
310
+ if (typeof node.kind !== 'number') return;
311
+ const member = (ts.isMethodDeclaration(node) || ts.isPropertyDeclaration(node))
312
+ && (ts.isIdentifier(node.name) || ts.isStringLiteral(node.name))
313
+ ? node.name.text
314
+ : enclosingMember;
315
+ if (ts.isIfStatement(node) && containsStateMutation(node.thenStatement)
316
+ && !isNonAuthoritativeMember(member)
317
+ && !isStateAuthorityBoundary(node, sourceFile)) {
318
+ const { line, character } = sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile));
319
+ const matchedSystemId = targetProfile === 'fixed-1000-3d'
320
+ ? matchedFixedConditionalFamily(node)
321
+ : f64FamilyMatchesBranch(f64SourceFamily, node, sourceFile)
322
+ ? f64SourceFamily[0]
323
+ : null;
324
+ if (matchedSystemId !== null) {
325
+ const installedFamily = installedSystemFamiliesForProfile(targetProfile)
326
+ .find((family) => family.id === matchedSystemId);
327
+ coverage.push({
328
+ source: {
329
+ file: relative(snapshotRoot, sourceFile.fileName).split(sep).join('/'),
330
+ line: line + 1,
331
+ column: character + 1,
332
+ branch: true,
333
+ },
334
+ systemId: matchedSystemId,
335
+ phase: installedFamily.phase,
336
+ order: installedFamily.order,
337
+ match: targetProfile === 'deterministic-f64'
338
+ ? { kind: 'semantic-source-shape', evidence: f64SourceFamily[1] }
339
+ : { kind: 'conditional-family' },
340
+ });
341
+ ts.forEachChild(node, (child) => visit(child, member));
342
+ return;
343
+ }
344
+ unmatched.push(diagnostic(
345
+ 'KINETIX_SOURCE_BEHAVIOR_UNSUPPORTED',
346
+ relative(snapshotRoot, sourceFile.fileName).split(sep).join('/'),
347
+ line + 1,
348
+ character + 1,
349
+ 'Authoritative conditional state mutation does not match an installed system family.',
350
+ {
351
+ enclosingMember: member ?? '<module>',
352
+ mutatedFields: mutatedFieldNames(node.thenStatement, sourceFile),
353
+ descendantCalls: [...descendantCallNames(node)].sort(),
354
+ },
355
+ ));
356
+ }
357
+ ts.forEachChild(node, (child) => visit(child, member));
358
+ }
359
+ visit(sourceFile);
360
+ }
361
+ unmatched.sort((left, right) => left.file.localeCompare(right.file)
362
+ || left.line - right.line || left.column - right.column);
363
+ coverage.sort((left, right) => left.source.file.localeCompare(right.source.file)
364
+ || left.source.line - right.source.line || left.source.column - right.source.column);
365
+ return { unmatched, coverage };
366
+ }
367
+
368
+ function calledName(expression, sourceFile) {
369
+ if (ts.isIdentifier(expression)) return { call: expression.text, receiver: null };
370
+ if (ts.isPropertyAccessExpression(expression)) {
371
+ return { call: expression.name.text, receiver: expression.expression.getText(sourceFile) };
372
+ }
373
+ return { call: null, receiver: null };
374
+ }
375
+
376
+ function collectCalls(snapshotRoot, files) {
377
+ const program = ts.createProgram(files, {
378
+ allowJs: false,
379
+ jsx: ts.JsxEmit.ReactJSX,
380
+ module: ts.ModuleKind.ESNext,
381
+ moduleResolution: ts.ModuleResolutionKind.Bundler,
382
+ noEmit: true,
383
+ skipLibCheck: true,
384
+ target: ts.ScriptTarget.ES2022,
385
+ });
386
+ const calls = [];
387
+ for (const sourceFile of program.getSourceFiles()) {
388
+ if (sourceFile.isDeclarationFile || !sourceFile.fileName.startsWith(snapshotRoot)
389
+ || !isAuthoritativeFile(snapshotRoot, sourceFile.fileName)) continue;
390
+ function visit(node) {
391
+ if (node === undefined) return;
392
+ if (Array.isArray(node)) {
393
+ node.forEach(visit);
394
+ return;
395
+ }
396
+ if (typeof node.kind !== 'number') return;
397
+ if (ts.isCallExpression(node)) {
398
+ const name = calledName(node.expression, sourceFile);
399
+ const { line, character } = sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile));
400
+ calls.push({
401
+ ...name,
402
+ arguments: node.arguments.map((argument) => ts.isStringLiteral(argument) ? argument.text : null),
403
+ source: {
404
+ file: relative(snapshotRoot, sourceFile.fileName).split(sep).join('/'),
405
+ line: line + 1,
406
+ column: character + 1,
407
+ },
408
+ });
409
+ }
410
+ ts.forEachChild(node, visit);
411
+ }
412
+ visit(sourceFile);
413
+ }
414
+ return calls;
415
+ }
416
+
417
+ function matchSignature(calls, signature) {
418
+ const matches = calls.filter((call) => call.call === signature.call
419
+ && (signature.receiver === undefined || call.receiver === signature.receiver
420
+ || call.receiver?.endsWith(`.${signature.receiver}`))
421
+ && (signature.argument === undefined || call.arguments.includes(signature.argument)));
422
+ return matches[signature.occurrence ?? 0] ?? null;
423
+ }
424
+
425
+ function semanticCoverageFor(snapshotRoot, files, targetProfile) {
426
+ const calls = collectCalls(snapshotRoot, files);
427
+ return installedSystemFamiliesForProfile(targetProfile).flatMap((installedFamily) => {
428
+ const signatures = kinetixInstalledSourceSignatures[installedFamily.id] ?? [];
429
+ const matched = signatures.map((signature) => matchSignature(calls, signature)).find(Boolean);
430
+ return matched ? [{
431
+ source: matched.source,
432
+ systemId: installedFamily.id,
433
+ phase: installedFamily.phase,
434
+ order: installedFamily.order,
435
+ }] : [];
436
+ });
437
+ }
438
+
439
+ function sourceDefiningFunction(program, functionName) {
440
+ for (const sourceFile of program.getSourceFiles()) {
441
+ if (sourceFile.isDeclarationFile) continue;
442
+ let found = false;
443
+ function visit(node) {
444
+ if (ts.isFunctionDeclaration(node) && node.name?.text === functionName) found = true;
445
+ if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name)
446
+ && node.name.text === functionName
447
+ && (ts.isArrowFunction(node.initializer) || ts.isFunctionExpression(node.initializer))) found = true;
448
+ if (!found) ts.forEachChild(node, visit);
449
+ }
450
+ visit(sourceFile);
451
+ if (found) return sourceFile;
452
+ }
453
+ throw new Error(`KINETIX_INSTALLED_SIGNATURE_MISSING ${functionName}`);
454
+ }
455
+
456
+ function variableInitializer(sourceFile, name) {
457
+ let initializer = null;
458
+ function visit(node) {
459
+ if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name) && node.name.text === name) {
460
+ initializer = node.initializer ?? null;
461
+ return;
462
+ }
463
+ if (initializer === null) ts.forEachChild(node, visit);
464
+ }
465
+ visit(sourceFile);
466
+ if (initializer === null) throw new Error(`KINETIX_INSTALLED_PARAMETER_MISSING ${name}`);
467
+ return initializer;
468
+ }
469
+
470
+ function staticValue(expression, checker, locals = new Map(), stack = new Set()) {
471
+ if (ts.isParenthesizedExpression(expression) || ts.isAsExpression(expression)
472
+ || ts.isTypeAssertionExpression(expression) || ts.isNonNullExpression(expression)) {
473
+ return staticValue(expression.expression, checker, locals, stack);
474
+ }
475
+ if (ts.isNumericLiteral(expression)) return Number(expression.text.replaceAll('_', ''));
476
+ if (ts.isStringLiteral(expression) || ts.isNoSubstitutionTemplateLiteral(expression)) return expression.text;
477
+ if (expression.kind === ts.SyntaxKind.TrueKeyword) return true;
478
+ if (expression.kind === ts.SyntaxKind.FalseKeyword) return false;
479
+ if (expression.kind === ts.SyntaxKind.NullKeyword) return null;
480
+ if (ts.isPrefixUnaryExpression(expression)) {
481
+ const value = staticValue(expression.operand, checker, locals, stack);
482
+ if (expression.operator === ts.SyntaxKind.MinusToken) return -value;
483
+ if (expression.operator === ts.SyntaxKind.PlusToken) return +value;
484
+ throw new Error('KINETIX_STATIC_EXPRESSION_UNSUPPORTED prefix');
485
+ }
486
+ if (ts.isIdentifier(expression)) {
487
+ if (locals.has(expression.text)) return locals.get(expression.text);
488
+ let symbol = checker.getSymbolAtLocation(expression);
489
+ if (symbol !== undefined && (symbol.flags & ts.SymbolFlags.Alias) !== 0) symbol = checker.getAliasedSymbol(symbol);
490
+ const declaration = symbol?.valueDeclaration ?? symbol?.declarations?.find((candidate) => ts.isVariableDeclaration(candidate));
491
+ if (!ts.isVariableDeclaration(declaration) || declaration.initializer === undefined) {
492
+ throw new Error(`KINETIX_STATIC_IDENTIFIER_UNRESOLVED ${expression.text}`);
493
+ }
494
+ if (stack.has(declaration)) throw new Error(`KINETIX_STATIC_EXPRESSION_CYCLE ${expression.text}`);
495
+ stack.add(declaration);
496
+ const value = staticValue(declaration.initializer, checker, locals, stack);
497
+ stack.delete(declaration);
498
+ return value;
499
+ }
500
+ if (ts.isArrayLiteralExpression(expression)) {
501
+ return expression.elements.map((element) => staticValue(element, checker, locals, stack));
502
+ }
503
+ if (ts.isObjectLiteralExpression(expression)) {
504
+ const value = {};
505
+ for (const property of expression.properties) {
506
+ if (!ts.isPropertyAssignment(property)) throw new Error('KINETIX_STATIC_OBJECT_PROPERTY_UNSUPPORTED');
507
+ const name = ts.isIdentifier(property.name) || ts.isStringLiteral(property.name)
508
+ || ts.isNumericLiteral(property.name)
509
+ ? property.name.text
510
+ : String(staticValue(property.name.expression, checker, locals, stack));
511
+ value[name] = staticValue(property.initializer, checker, locals, stack);
512
+ }
513
+ return value;
514
+ }
515
+ if (ts.isPropertyAccessExpression(expression)) {
516
+ const object = staticValue(expression.expression, checker, locals, stack);
517
+ return object?.[expression.name.text];
518
+ }
519
+ if (ts.isElementAccessExpression(expression)) {
520
+ const object = staticValue(expression.expression, checker, locals, stack);
521
+ const key = staticValue(expression.argumentExpression, checker, locals, stack);
522
+ return object?.[key];
523
+ }
524
+ if (ts.isBinaryExpression(expression)) {
525
+ const left = staticValue(expression.left, checker, locals, stack);
526
+ if (expression.operatorToken.kind === ts.SyntaxKind.QuestionQuestionToken) {
527
+ return left ?? staticValue(expression.right, checker, locals, stack);
528
+ }
529
+ const right = staticValue(expression.right, checker, locals, stack);
530
+ const operations = new Map([
531
+ [ts.SyntaxKind.PlusToken, () => left + right],
532
+ [ts.SyntaxKind.MinusToken, () => left - right],
533
+ [ts.SyntaxKind.AsteriskToken, () => left * right],
534
+ [ts.SyntaxKind.SlashToken, () => left / right],
535
+ [ts.SyntaxKind.PercentToken, () => left % right],
536
+ [ts.SyntaxKind.LessThanToken, () => left < right],
537
+ [ts.SyntaxKind.GreaterThanToken, () => left > right],
538
+ [ts.SyntaxKind.EqualsEqualsEqualsToken, () => left === right],
539
+ ]);
540
+ const operation = operations.get(expression.operatorToken.kind);
541
+ if (operation === undefined) throw new Error(`KINETIX_STATIC_BINARY_UNSUPPORTED ${expression.operatorToken.kind}`);
542
+ return operation();
543
+ }
544
+ if (ts.isCallExpression(expression)) {
545
+ const arguments_ = expression.arguments.map((argument) => staticValue(argument, checker, locals, stack));
546
+ if (ts.isPropertyAccessExpression(expression.expression)
547
+ && ts.isIdentifier(expression.expression.expression)
548
+ && expression.expression.expression.text === 'Math') {
549
+ const operation = Math[expression.expression.name.text];
550
+ if (!['trunc', 'round', 'floor', 'ceil', 'min', 'max'].includes(expression.expression.name.text)) {
551
+ throw new Error(`KINETIX_STATIC_MATH_UNSUPPORTED ${expression.expression.name.text}`);
552
+ }
553
+ return operation(...arguments_);
554
+ }
555
+ let symbol = checker.getSymbolAtLocation(expression.expression);
556
+ if (symbol !== undefined && (symbol.flags & ts.SymbolFlags.Alias) !== 0) symbol = checker.getAliasedSymbol(symbol);
557
+ const declaration = symbol?.valueDeclaration;
558
+ const callable = ts.isFunctionDeclaration(declaration)
559
+ ? declaration
560
+ : ts.isVariableDeclaration(declaration)
561
+ && (ts.isArrowFunction(declaration.initializer) || ts.isFunctionExpression(declaration.initializer))
562
+ ? declaration.initializer
563
+ : null;
564
+ if (callable === null || callable.body === undefined) throw new Error('KINETIX_STATIC_CALL_UNRESOLVED');
565
+ const callLocals = new Map(locals);
566
+ callable.parameters.forEach((parameter, index) => {
567
+ if (!ts.isIdentifier(parameter.name)) throw new Error('KINETIX_STATIC_PARAMETER_UNSUPPORTED');
568
+ callLocals.set(parameter.name.text, arguments_[index]);
569
+ });
570
+ if (!ts.isBlock(callable.body)) return staticValue(callable.body, checker, callLocals, stack);
571
+ const returned = callable.body.statements.find((statement) => ts.isReturnStatement(statement));
572
+ if (!ts.isReturnStatement(returned) || returned.expression === undefined) {
573
+ throw new Error('KINETIX_STATIC_FUNCTION_BODY_UNSUPPORTED');
574
+ }
575
+ return staticValue(returned.expression, checker, callLocals, stack);
576
+ }
577
+ throw new Error(`KINETIX_STATIC_EXPRESSION_UNSUPPORTED ${expression.kind}`);
578
+ }
579
+
580
+ function constantValue(sourceFile, name, checker) {
581
+ return staticValue(variableInitializer(sourceFile, name), checker);
582
+ }
583
+
584
+ function encodeGridCells(cells) {
585
+ const bytes = Buffer.alloc(cells.length * 3);
586
+ cells.forEach((cell, index) => {
587
+ bytes[index * 3] = cell.gridX + 128;
588
+ bytes[index * 3 + 1] = cell.gridZ + 128;
589
+ bytes[index * 3 + 2] = (cell.typeId << 2) | cell.orientation;
590
+ });
591
+ return bytes.toString('base64url');
592
+ }
593
+
594
+ function encodeFixedProfileBinary(pack, cells) {
595
+ const chunks = [Buffer.from('KFXDATA1')];
596
+ const i64 = (value) => {
597
+ const bytes = Buffer.alloc(8);
598
+ bytes.writeBigInt64LE(BigInt(value));
599
+ chunks.push(bytes);
600
+ };
601
+ const string = (value) => {
602
+ const bytes = Buffer.from(value);
603
+ const length = Buffer.alloc(4);
604
+ length.writeUInt32LE(bytes.length);
605
+ chunks.push(length, bytes);
606
+ };
607
+ const array = (values) => {
608
+ const length = Buffer.alloc(4);
609
+ length.writeUInt32LE(values.length);
610
+ chunks.push(length);
611
+ values.forEach(i64);
612
+ };
613
+ string(pack.grid.encoded);
614
+ [
615
+ pack.scale, pack.slotCount, pack.humanCount,
616
+ pack.initialPhase === 'lobby' ? 0 : pack.initialPhase === 'countdown' ? 1 : 2,
617
+ pack.grid.cellRaw, pack.grid.gridScale, pack.grid.finishTypeId,
618
+ pack.components.arcadeDriftVehicle.maxSpeed,
619
+ pack.components.arcadeDriftVehicle.accelRate,
620
+ pack.components.arcadeDriftVehicle.brakeRate,
621
+ pack.components.arcadeDriftVehicle.maxReverse,
622
+ pack.components.arcadeDriftVehicle.turnPerTick,
623
+ pack.components.arcadeDriftVehicle.gripMin,
624
+ pack.components.arcadeDriftVehicle.slipSpeed,
625
+ pack.components.arcadeDriftVehicle.tractionSlow,
626
+ pack.components.arcadeDriftVehicle.tractionFast,
627
+ pack.components.arcadeDriftVehicle.driftSteer,
628
+ pack.components.arcadeDriftVehicle.stopDeadband,
629
+ pack.components.gridTrackCollider.wallHalfThick,
630
+ pack.components.gridTrackCollider.wallLateral,
631
+ pack.components.gridTrackCollider.wallY,
632
+ pack.components.gridTrackCollider.wallHalfHeightRaw,
633
+ pack.components.gridTrackCollider.halfPiFixed,
634
+ pack.components.gridTrackCollider.twoPiFixed,
635
+ pack.components.gridTrackCollider.outerArcSegments,
636
+ pack.components.gridTrackCollider.innerArcSegments,
637
+ pack.components.gridTrackCollider.layer,
638
+ pack.components.gridTrackCollider.mask,
639
+ pack.components.arenaBounds.halfUnits,
640
+ pack.components.arenaBounds.wallThick,
641
+ pack.components.arenaBounds.wallHalfHeight,
642
+ pack.components.arenaBounds.wallY,
643
+ pack.components.arenaBounds.centerX,
644
+ pack.components.arenaBounds.centerZ,
645
+ pack.components.scatterSpawn.cols,
646
+ pack.components.scatterSpawn.spacing,
647
+ pack.components.gridSpawn.lateral,
648
+ pack.components.gridSpawn.rowBack,
649
+ pack.components.gridSpawn.minSlots,
650
+ pack.components.gridSpawn.maxSlots,
651
+ pack.components.waypointRingDriver.lookahead,
652
+ pack.components.waypointRingDriver.steerGain,
653
+ pack.components.waypointRingDriver.sharpTurnThrottle,
654
+ pack.components.lapProgress.totalLaps,
655
+ pack.components.lapProgress.maxRaceFrames,
656
+ pack.components.lapProgress.noTeleport,
657
+ pack.components.lapProgress.maxRequiredBits,
658
+ pack.components.matchPhaseMachine.lobbyInitialFrames,
659
+ pack.components.matchPhaseMachine.lobbyExtendFrames,
660
+ pack.components.matchPhaseMachine.lobbyMaxFrames,
661
+ pack.components.matchPhaseMachine.countdownFrames,
662
+ pack.components.matchPhaseMachine.resultsHoldFrames,
663
+ pack.components.matchPhaseMachine.maxSlotsAutoStart,
664
+ pack.components.physicsWriteback.bodyRadius,
665
+ pack.components.physicsWriteback.bodyY,
666
+ pack.components.physicsWriteback.layer,
667
+ pack.components.physicsWriteback.mask,
668
+ ].forEach(i64);
669
+ const cellCount = Buffer.alloc(4);
670
+ cellCount.writeUInt32LE(cells.length);
671
+ chunks.push(cellCount);
672
+ cells.forEach((cell) => [cell.gridX, cell.gridZ, cell.typeId, cell.orientation].forEach(i64));
673
+ array(pack.grid.cornerTypeIds);
674
+ array(pack.grid.passThroughTypeIds);
675
+ string(pack.components.gridTrackCollider.bodyIdPrefix);
676
+ string(pack.components.physicsWriteback.bodyIdPrefix);
677
+ const bodyIds = pack.components.arenaBounds.bodyIds;
678
+ const bodyIdCount = Buffer.alloc(4);
679
+ bodyIdCount.writeUInt32LE(bodyIds.length);
680
+ chunks.push(bodyIdCount);
681
+ bodyIds.forEach(string);
682
+ return Buffer.concat(chunks).toString('base64');
683
+ }
684
+
685
+ function extractFixedProfileData(snapshotRoot, files) {
686
+ const program = ts.createProgram(files, {
687
+ jsx: ts.JsxEmit.ReactJSX,
688
+ module: ts.ModuleKind.ESNext,
689
+ moduleResolution: ts.ModuleResolutionKind.Bundler,
690
+ noEmit: true,
691
+ skipLibCheck: true,
692
+ target: ts.ScriptTarget.ES2022,
693
+ });
694
+ const checker = program.getTypeChecker();
695
+ const stepSource = sourceDefiningFunction(program, 'step');
696
+ const vehicle = sourceDefiningFunction(program, 'stepVehicle');
697
+ const track = sourceDefiningFunction(program, 'cookWalls');
698
+ const arena = sourceDefiningFunction(program, 'cookArena');
699
+ const scatter = sourceDefiningFunction(program, 'deriveLobbySpawns');
700
+ const spawn = sourceDefiningFunction(program, 'deriveSpawns');
701
+ const bot = sourceDefiningFunction(program, 'botDrive');
702
+ const lap = sourceDefiningFunction(program, 'stepLap');
703
+ const phases = new Map();
704
+ for (const name of [
705
+ 'LOBBY_INITIAL_FRAMES', 'LOBBY_EXTEND_FRAMES', 'LOBBY_MAX_FRAMES',
706
+ 'COUNTDOWN_FRAMES', 'RESULTS_HOLD_FRAMES',
707
+ ]) {
708
+ const symbol = checker.resolveName(name, stepSource, ts.SymbolFlags.Value, false);
709
+ const declaration = symbol?.flags & ts.SymbolFlags.Alias ? checker.getAliasedSymbol(symbol).valueDeclaration : symbol?.valueDeclaration;
710
+ if (!ts.isVariableDeclaration(declaration) || declaration.initializer === undefined) {
711
+ throw new Error(`KINETIX_INSTALLED_PARAMETER_MISSING ${name}`);
712
+ }
713
+ phases.set(name, staticValue(declaration.initializer, checker));
714
+ }
715
+ const one = constantValue(sourceDefiningFunction(program, 'fsin'), 'ONE', checker);
716
+ const pack = {
717
+ packVersion: 'rundot.kinetix-installed-profile-data.v1',
718
+ numericProfile: 'fixed-1000',
719
+ scale: one,
720
+ tickRate: 30,
721
+ grid: {
722
+ codec: 'cell-3byte-base64url',
723
+ cellRaw: constantValue(track, 'CELL_RAW', checker),
724
+ gridScale: constantValue(track, 'S', checker),
725
+ cornerTypeIds: [1],
726
+ passThroughTypeIds: [2],
727
+ finishTypeId: constantValue(spawn, 'FINISH_TYPE_ID', checker),
728
+ },
729
+ components: {
730
+ arcadeDriftVehicle: {
731
+ maxSpeed: constantValue(vehicle, 'MAX_SPEED', checker),
732
+ accelRate: constantValue(vehicle, 'ACCEL_RATE', checker),
733
+ brakeRate: constantValue(vehicle, 'BRAKE_RATE', checker),
734
+ maxReverse: constantValue(vehicle, 'MAX_REVERSE', checker),
735
+ turnPerTick: constantValue(vehicle, 'TURN_PER_TICK', checker),
736
+ gripMin: constantValue(vehicle, 'GRIP_MIN', checker),
737
+ slipSpeed: constantValue(vehicle, 'SLIP_SPEED', checker),
738
+ tractionSlow: constantValue(vehicle, 'TRACTION_SLOW', checker),
739
+ tractionFast: constantValue(vehicle, 'TRACTION_FAST', checker),
740
+ driftSteer: constantValue(vehicle, 'DRIFT_STEER', checker),
741
+ stopDeadband: 2,
742
+ },
743
+ gridTrackCollider: {
744
+ wallHalfThick: constantValue(track, 'WALL_HALF_THICK', checker),
745
+ wallLateral: constantValue(track, 'WALL_X', checker),
746
+ wallY: constantValue(track, 'WALL_Y', checker),
747
+ wallHalfHeightRaw: 1500,
748
+ halfPiFixed: constantValue(track, 'HALF_PI', checker),
749
+ twoPiFixed: constantValue(track, 'TWO_PI', checker),
750
+ outerArcSegments: constantValue(track, 'OUTER_SEG', checker),
751
+ innerArcSegments: constantValue(track, 'INNER_SEG', checker),
752
+ bodyIdPrefix: 'w', layer: 1, mask: 0xffff,
753
+ },
754
+ arenaBounds: {
755
+ halfUnits: constantValue(arena, 'ARENA_HALF', checker),
756
+ wallThick: constantValue(arena, 'WALL_THICK_F', checker),
757
+ wallHalfHeight: constantValue(arena, 'WALL_HALF_H_F', checker),
758
+ wallY: constantValue(arena, 'WALL_Y_F', checker),
759
+ centerX: constantValue(arena, 'ARENA_CX', checker),
760
+ centerZ: constantValue(arena, 'ARENA_CZ', checker),
761
+ bodyIds: ['a-n', 'a-s', 'a-w', 'a-e'], layer: 1, mask: 0xffff,
762
+ },
763
+ scatterSpawn: {
764
+ cols: constantValue(scatter, 'cols', checker),
765
+ spacing: constantValue(scatter, 'spacing', checker),
766
+ },
767
+ gridSpawn: {
768
+ lateral: constantValue(spawn, 'LATERAL', checker),
769
+ rowBack: constantValue(spawn, 'ROW_BACK', checker), minSlots: 1, maxSlots: 8,
770
+ },
771
+ waypointRingDriver: {
772
+ lookahead: constantValue(bot, 'LOOKAHEAD', checker),
773
+ steerGain: constantValue(bot, 'STEER_GAIN', checker),
774
+ sharpTurnThrottle: constantValue(bot, 'SHARP_TURN_THROTTLE', checker),
775
+ },
776
+ lapProgress: {
777
+ totalLaps: constantValue(lap, 'TOTAL_LAPS', checker),
778
+ maxRaceFrames: constantValue(lap, 'MAX_RACE_FRAMES', checker),
779
+ noTeleport: constantValue(lap, 'NO_TELEPORT', checker),
780
+ maxRequiredBits: constantValue(lap, 'MAX_REQUIRED_BITS', checker),
781
+ },
782
+ matchPhaseMachine: {
783
+ lobbyInitialFrames: phases.get('LOBBY_INITIAL_FRAMES'),
784
+ lobbyExtendFrames: phases.get('LOBBY_EXTEND_FRAMES'),
785
+ lobbyMaxFrames: phases.get('LOBBY_MAX_FRAMES'),
786
+ countdownFrames: phases.get('COUNTDOWN_FRAMES'),
787
+ resultsHoldFrames: phases.get('RESULTS_HOLD_FRAMES'),
788
+ maxSlotsAutoStart: constantValue(stepSource, 'MAX_PLAYERS', checker),
789
+ },
790
+ physicsWriteback: {
791
+ bodyRadius: constantValue(stepSource, 'CAR_RADIUS', checker),
792
+ bodyY: constantValue(stepSource, 'CAR_Y', checker),
793
+ bodyIdPrefix: 'car', layer: 1, mask: 0xffff, gravityY: 0,
794
+ },
795
+ },
796
+ };
797
+ return pack;
798
+ }
799
+
800
+ function extractF64ProfileData(snapshotRoot, files) {
801
+ const program = ts.createProgram(files, {
802
+ jsx: ts.JsxEmit.ReactJSX,
803
+ module: ts.ModuleKind.ESNext,
804
+ moduleResolution: ts.ModuleResolutionKind.Bundler,
805
+ noEmit: true,
806
+ skipLibCheck: true,
807
+ target: ts.ScriptTarget.ES2022,
808
+ });
809
+ const checker = program.getTypeChecker();
810
+ const parameters = [];
811
+ for (const sourceFile of program.getSourceFiles()) {
812
+ if (sourceFile.isDeclarationFile || !sourceFile.fileName.startsWith(snapshotRoot)
813
+ || !isAuthoritativeFile(snapshotRoot, sourceFile.fileName)) continue;
814
+ for (const statement of sourceFile.statements) {
815
+ if (!ts.isVariableStatement(statement)) continue;
816
+ for (const declaration of statement.declarationList.declarations) {
817
+ if (!ts.isIdentifier(declaration.name) || declaration.initializer === undefined) continue;
818
+ try {
819
+ const value = staticValue(declaration.initializer, checker);
820
+ JSON.stringify(value);
821
+ if (/(?:track|room|human|player|seed|initial.*mode)/iu.test(declaration.name.text)) continue;
822
+ parameters.push({
823
+ source: relative(snapshotRoot, sourceFile.fileName).split(sep).join('/'),
824
+ name: declaration.name.text,
825
+ value,
826
+ });
827
+ } catch {
828
+ // Runtime-only expressions are deliberately absent from data products.
829
+ }
830
+ }
831
+ }
832
+ }
833
+ parameters.sort((left, right) => left.source.localeCompare(right.source)
834
+ || left.name.localeCompare(right.name));
835
+ const parameterBytes = Buffer.from(JSON.stringify(parameters));
836
+ return {
837
+ packVersion: 'rundot.kinetix-installed-profile-data.v1',
838
+ numericProfile: 'deterministic-f64',
839
+ sourceParameterDigest: createHash('sha256').update(parameterBytes).digest('hex'),
840
+ parameters,
841
+ };
842
+ }
843
+
844
+ function envelopeFor(targetProfile, expectedCommit, archiveSha256, semanticCoverage, installedProfileData) {
845
+ const profile = targetProfile === 'deterministic-f64'
846
+ ? { id: 'deterministic-f64-2d', abi: 'deterministic-f64-2d.v1' }
847
+ : { id: 'fixed-1000-3d', abi: 'fixed-1000-3d.v1' };
848
+ return {
849
+ version: 'rundot.kinetix-envelope.v1',
850
+ compilerProvenance: {
851
+ compilerVersion: 'rundot.kinetix-project-compiler.v1',
852
+ targetProfile,
853
+ sourceSnapshot: { commit: expectedCommit, archiveSha256 },
854
+ },
855
+ semanticCoverage: semanticCoverage.map((entry) => ({
856
+ source: {
857
+ file: entry.source.file,
858
+ line: entry.source.line,
859
+ column: entry.source.column,
860
+ },
861
+ systemId: entry.systemId,
862
+ phase: entry.phase,
863
+ order: entry.order,
864
+ })),
865
+ sceneId: `compiled-${archiveSha256.slice(0, 16)}`,
866
+ sceneDependencies: [],
867
+ profiles: [profile],
868
+ assets: [],
869
+ budgets: {
870
+ maxDirectEntities: 1,
871
+ maxPrefabs: 1,
872
+ maxLocalEntitiesPerPrefab: 1,
873
+ maxComponents: 3,
874
+ maxLiveRuntimeEntities: 1,
875
+ maxSimulationRecords: 1,
876
+ maxPresentationRecords: 1,
877
+ maxBindingRecords: 1,
878
+ maxCommandsPerTick: 1,
879
+ },
880
+ prefabs: [],
881
+ entities: [{
882
+ id: 'runtime-root',
883
+ profile: profile.id,
884
+ lifecycle: 'static',
885
+ section: 0,
886
+ components: [
887
+ {
888
+ id: 'state',
889
+ type: 'KinetixRuntimeState',
890
+ schemaVersion: 1,
891
+ domain: 'simulation',
892
+ properties: { numericProfile: targetProfile, installedProfileData },
893
+ },
894
+ {
895
+ id: 'render-record',
896
+ type: 'KinetixRenderRecord',
897
+ schemaVersion: 1,
898
+ domain: 'presentation',
899
+ properties: { primitive: 'point', sourceSemanticDigest: archiveSha256 },
900
+ },
901
+ {
902
+ id: 'render-binding',
903
+ type: 'KinetixRenderBinding',
904
+ schemaVersion: 1,
905
+ domain: 'binding',
906
+ properties: {
907
+ source: { $kinetixRef: 'component', entityId: 'runtime-root', componentId: 'state' },
908
+ target: { $kinetixRef: 'component', entityId: 'runtime-root', componentId: 'render-record' },
909
+ },
910
+ },
911
+ ],
912
+ }],
913
+ };
914
+ }
915
+
916
+ function treeFingerprint(projectRoot) {
917
+ const status = git(projectRoot, ['status', '--porcelain=v1', '--untracked-files=all']);
918
+ const diff = git(projectRoot, ['diff', '--binary']);
919
+ if (status.status !== 0 || diff.status !== 0) {
920
+ throw new Error('KINETIX_SOURCE_TREE_UNREADABLE');
921
+ }
922
+ return createHash('sha256').update(status.stdout).update('\0').update(diff.stdout).digest('hex');
923
+ }
924
+
925
+ export function compileKinetixProject({
926
+ projectRoot,
927
+ expectedCommit,
928
+ targetProfile,
929
+ classifyInstalledBranches = true,
930
+ }) {
931
+ if (typeof projectRoot !== 'string' || typeof expectedCommit !== 'string' || typeof targetProfile !== 'string') {
932
+ throw new Error('KINETIX_PROJECT_OPTIONS_INVALID');
933
+ }
934
+ const beforeFingerprint = treeFingerprint(projectRoot);
935
+ const archive = git(projectRoot, [
936
+ '-c', 'filter.lfs.required=false',
937
+ '-c', 'filter.lfs.process=',
938
+ '-c', 'filter.lfs.smudge=',
939
+ 'archive', '--format=tar', expectedCommit,
940
+ ], null);
941
+ if (archive.status !== 0) {
942
+ return {
943
+ diagnostics: [diagnostic('KINETIX_SOURCE_COMMIT_MISSING', '', 1, 1, `Git commit ${expectedCommit} is unavailable.`)],
944
+ unmatchedBehaviors: [],
945
+ sourceSnapshot: { commit: expectedCommit, archiveSha256: '' },
946
+ semanticCoverage: [],
947
+ runtimeContract: null,
948
+ envelope: null,
949
+ };
950
+ }
951
+ const snapshotRoot = mkdtempSync(join(tmpdir(), 'kinetix-project-'));
952
+ try {
953
+ const extracted = spawnSync('tar', ['-xf', '-', '-C', snapshotRoot], {
954
+ input: archive.stdout,
955
+ encoding: null,
956
+ maxBuffer: 1 << 29,
957
+ });
958
+ if (extracted.status !== 0) {
959
+ throw new Error('KINETIX_SOURCE_ARCHIVE_INVALID');
960
+ }
961
+ const result = compileTrustedKinetixSource({
962
+ sourceRoot: snapshotRoot,
963
+ expectedCommit,
964
+ targetProfile,
965
+ classifyInstalledBranches,
966
+ });
967
+ if (treeFingerprint(projectRoot) !== beforeFingerprint) {
968
+ throw new Error('KINETIX_SOURCE_TREE_MUTATED');
969
+ }
970
+ return result;
971
+ } finally {
972
+ rmSync(snapshotRoot, { recursive: true, force: true });
973
+ }
974
+ }
975
+
976
+ export function compileTrustedKinetixSource({
977
+ sourceRoot,
978
+ expectedCommit,
979
+ targetProfile,
980
+ classifyInstalledBranches = true,
981
+ }) {
982
+ if (typeof sourceRoot !== 'string'
983
+ || typeof expectedCommit !== 'string'
984
+ || typeof targetProfile !== 'string') {
985
+ throw new Error('KINETIX_TRUSTED_SOURCE_OPTIONS_INVALID');
986
+ }
987
+ const files = sourceFilesUnder(sourceRoot);
988
+ const sourceDigest = kinetixCanonicalSourceDigest(sourceRoot);
989
+ const classified = classifyUnmatched(
990
+ sourceRoot,
991
+ files,
992
+ targetProfile,
993
+ classifyInstalledBranches,
994
+ );
995
+ const unmatchedBehaviors = classified.unmatched;
996
+ const semanticCoverage = [
997
+ ...semanticCoverageFor(sourceRoot, files, targetProfile),
998
+ ...classified.coverage,
999
+ ];
1000
+ const installedProfileData = unmatchedBehaviors.length === 0
1001
+ ? targetProfile === 'fixed-1000-3d'
1002
+ ? extractFixedProfileData(sourceRoot, files)
1003
+ : extractF64ProfileData(sourceRoot, files)
1004
+ : null;
1005
+ return {
1006
+ diagnostics: [...unmatchedBehaviors],
1007
+ unmatchedBehaviors,
1008
+ sourceSnapshot: { commit: expectedCommit, archiveSha256: sourceDigest },
1009
+ semanticCoverage,
1010
+ runtimeContract: unmatchedBehaviors.length === 0
1011
+ ? kinetixRuntimeContractForProfile(targetProfile)
1012
+ : null,
1013
+ envelope: unmatchedBehaviors.length === 0
1014
+ ? envelopeFor(targetProfile, expectedCommit, sourceDigest, semanticCoverage, installedProfileData)
1015
+ : null,
1016
+ };
1017
+ }