@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,28 @@
1
+ import { createHash } from 'node:crypto';
2
+
3
+ export function createKinetixRenderProjection({ world, products }) {
4
+ if (typeof world?.deriveTransforms !== 'function' || !Array.isArray(products?.presentation?.records)
5
+ || !Array.isArray(products?.binding?.records)) {
6
+ throw new Error('KINETIX_RENDER_PROJECTION_INVALID');
7
+ }
8
+ const bindingDigest = createHash('sha256').update(JSON.stringify(products.binding)).digest('hex');
9
+ return Object.freeze({
10
+ project: () => {
11
+ const snapshot = world.snapshot();
12
+ const transforms = [...world.deriveTransforms()].map(([handle, transform]) => ({ handle, transform }));
13
+ return products.presentation.records.map((record) => {
14
+ const sourceId = record.source?.kind === 'entity' ? record.source.id : null;
15
+ const handle = sourceId === null ? null : world.directHandle(sourceId);
16
+ const projected = handle === null ? null : transforms.find((entry) => entry.handle.index === handle.index)?.transform ?? null;
17
+ return {
18
+ kind: record.kind,
19
+ source: structuredClone(record.source),
20
+ component: structuredClone(record.component ?? null),
21
+ transform: structuredClone(projected),
22
+ worldTick: snapshot.tick,
23
+ bindingDigest,
24
+ };
25
+ });
26
+ },
27
+ });
28
+ }
@@ -0,0 +1,168 @@
1
+ import { assertKinetixRuntimeIdentity } from './kinetix-runtime-contract.mjs';
2
+ import { kinetixSessionConfigDigest } from './kinetix-session-config.mjs';
3
+ import { sha256Hex } from './shared/sha256.mjs';
4
+
5
+ const encoder = new TextEncoder();
6
+ const decoder = new TextDecoder('utf-8', { fatal: true });
7
+
8
+ function fail(code) {
9
+ throw new Error(code);
10
+ }
11
+
12
+ function isPlainObject(value) {
13
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
14
+ }
15
+
16
+ function tagged(value) {
17
+ if (typeof value === 'bigint') return { $kinetix: 'bigint', value: value.toString() };
18
+ if (value instanceof Map) {
19
+ const entries = [...value.entries()].map(([key, entry]) => [tagged(key), tagged(entry)]);
20
+ entries.sort((left, right) => JSON.stringify(left[0]).localeCompare(JSON.stringify(right[0])));
21
+ return { $kinetix: 'map', entries };
22
+ }
23
+ if (Array.isArray(value)) return value.map(tagged);
24
+ if (isPlainObject(value)) {
25
+ return Object.fromEntries(Object.keys(value).sort().map((key) => [key, tagged(value[key])]));
26
+ }
27
+ if (value === null || typeof value === 'string' || typeof value === 'boolean'
28
+ || (typeof value === 'number' && Number.isFinite(value))) return value;
29
+ fail('KINETIX_RUNTIME_CHECKPOINT_INVALID');
30
+ }
31
+
32
+ function untagged(value) {
33
+ if (Array.isArray(value)) return value.map(untagged);
34
+ if (isPlainObject(value)) {
35
+ if (value.$kinetix === 'bigint' && typeof value.value === 'string') return BigInt(value.value);
36
+ if (value.$kinetix === 'map' && Array.isArray(value.entries)) {
37
+ return new Map(value.entries.map(([key, entry]) => [untagged(key), untagged(entry)]));
38
+ }
39
+ return Object.fromEntries(Object.entries(value).map(([key, entry]) => [key, untagged(entry)]));
40
+ }
41
+ return value;
42
+ }
43
+
44
+ export function encodeRuntimeCheckpoint(value) {
45
+ return encoder.encode(JSON.stringify(tagged(value)));
46
+ }
47
+
48
+ export function decodeRuntimeCheckpoint(bytes) {
49
+ if (!ArrayBuffer.isView(bytes) || bytes.BYTES_PER_ELEMENT !== 1) {
50
+ fail('KINETIX_RUNTIME_CHECKPOINT_INVALID');
51
+ }
52
+ const normalized = Uint8Array.from(bytes);
53
+ try {
54
+ const decoded = untagged(JSON.parse(decoder.decode(normalized)));
55
+ const canonical = encodeRuntimeCheckpoint(decoded);
56
+ if (canonical.length !== normalized.length || canonical.some((byte, index) => byte !== normalized[index])) {
57
+ fail('KINETIX_RUNTIME_CHECKPOINT_INVALID');
58
+ }
59
+ return decoded;
60
+ } catch (error) {
61
+ if (error instanceof Error && error.message === 'KINETIX_RUNTIME_CHECKPOINT_INVALID') throw error;
62
+ fail('KINETIX_RUNTIME_CHECKPOINT_INVALID');
63
+ }
64
+ }
65
+
66
+ export function createInstalledRuntimeAdapter({
67
+ identity,
68
+ sessionConfigBytes,
69
+ captureState,
70
+ restoreState,
71
+ frameOf,
72
+ step,
73
+ checksumBytes = encodeRuntimeCheckpoint,
74
+ capturedStateIsDetached = false,
75
+ inspectState = (state) => state,
76
+ }) {
77
+ assertKinetixRuntimeIdentity(identity);
78
+ const checkpoints = new WeakMap();
79
+
80
+ function makeCheckpoint(state) {
81
+ const copy = capturedStateIsDetached ? state : structuredClone(state);
82
+ const frame = frameOf(copy);
83
+ if (!Number.isSafeInteger(frame) || frame < 0) fail('KINETIX_RUNTIME_FRAME_MISMATCH');
84
+ const checkpoint = Object.freeze({ frame });
85
+ checkpoints.set(checkpoint, { frame, state: copy, serialized: null });
86
+ return checkpoint;
87
+ }
88
+
89
+ function data(checkpoint) {
90
+ const selected = isPlainObject(checkpoint) ? checkpoints.get(checkpoint) : undefined;
91
+ if (!selected) fail('KINETIX_RUNTIME_CHECKPOINT_INVALID');
92
+ return selected;
93
+ }
94
+
95
+ let currentCheckpoint = makeCheckpoint(captureState());
96
+ const runtime = {
97
+ identity: Object.freeze(structuredClone(identity)),
98
+ sessionConfigDigest: kinetixSessionConfigDigest(sessionConfigBytes),
99
+ get currentFrame() {
100
+ return data(currentCheckpoint).frame;
101
+ },
102
+ sessionConfigBytes() {
103
+ return sessionConfigBytes.slice();
104
+ },
105
+ capture() {
106
+ return makeCheckpoint(data(currentCheckpoint).state);
107
+ },
108
+ advance(requests) {
109
+ if (!Array.isArray(requests)) fail('KINETIX_RUNTIME_BATCH_INVALID');
110
+ let expected = this.currentFrame + 1;
111
+ for (const request of requests) {
112
+ if (!isPlainObject(request) || request.frame !== expected
113
+ || !Array.isArray(request.inputs) || !Array.isArray(request.commands)
114
+ || request.commands.length !== 0 || typeof request.checksum !== 'boolean') {
115
+ fail('KINETIX_RUNTIME_FRAME_MISMATCH');
116
+ }
117
+ expected += 1;
118
+ }
119
+ const starting = data(currentCheckpoint);
120
+ const results = [];
121
+ try {
122
+ for (const request of requests) {
123
+ step(request.inputs);
124
+ const state = captureState();
125
+ if (frameOf(state) !== request.frame) fail('KINETIX_RUNTIME_FRAME_MISMATCH');
126
+ currentCheckpoint = makeCheckpoint(state);
127
+ const result = { frame: request.frame, checkpoint: currentCheckpoint };
128
+ if (request.checksum) result.checksum = this.checksum(currentCheckpoint);
129
+ results.push(result);
130
+ }
131
+ return results;
132
+ } catch (error) {
133
+ restoreState(structuredClone(starting.state));
134
+ currentCheckpoint = makeCheckpoint(starting.state);
135
+ throw error;
136
+ }
137
+ },
138
+ restore(checkpoint) {
139
+ const selected = data(checkpoint);
140
+ restoreState(structuredClone(selected.state));
141
+ currentCheckpoint = makeCheckpoint(captureState());
142
+ if (this.currentFrame !== selected.frame) fail('KINETIX_RUNTIME_FRAME_MISMATCH');
143
+ },
144
+ inspect(checkpoint) {
145
+ return structuredClone(inspectState(data(checkpoint).state));
146
+ },
147
+ serializeCheckpoint(checkpoint) {
148
+ const selected = data(checkpoint);
149
+ selected.serialized ??= encodeRuntimeCheckpoint(selected.state);
150
+ return selected.serialized.slice();
151
+ },
152
+ hydrateCheckpoint(frame, bytes) {
153
+ const state = decodeRuntimeCheckpoint(bytes);
154
+ if (frameOf(state) !== frame) fail('KINETIX_RUNTIME_FRAME_MISMATCH');
155
+ return makeCheckpoint(state);
156
+ },
157
+ checksum(checkpoint) {
158
+ return sha256Hex(checksumBytes(data(checkpoint).state));
159
+ },
160
+ wireChecksum(checkpoint) {
161
+ return sha256Hex(checksumBytes(data(checkpoint).state));
162
+ },
163
+ release(checkpoint) {
164
+ if (checkpoint !== currentCheckpoint) checkpoints.delete(checkpoint);
165
+ },
166
+ };
167
+ return Object.freeze(runtime);
168
+ }
@@ -0,0 +1,54 @@
1
+ export const kinetixRuntimeAbiVersion = 1;
2
+
3
+ const runtimeMethods = [
4
+ 'sessionConfigBytes',
5
+ 'capture',
6
+ 'advance',
7
+ 'restore',
8
+ 'inspect',
9
+ 'serializeCheckpoint',
10
+ 'hydrateCheckpoint',
11
+ 'checksum',
12
+ 'wireChecksum',
13
+ 'release',
14
+ ];
15
+
16
+ const identityStringFields = [
17
+ 'inputSchemaId',
18
+ 'stateSchemaId',
19
+ 'deterministicVersion',
20
+ 'engineIdentityHash',
21
+ ];
22
+
23
+ const supportedTickRates = [10, 20, 30, 60];
24
+
25
+ export function assertKinetixRuntime(runtime) {
26
+ if (
27
+ runtime === null
28
+ || typeof runtime !== 'object'
29
+ || !Number.isSafeInteger(runtime.currentFrame)
30
+ || runtime.currentFrame < 0
31
+ || typeof runtime.sessionConfigDigest !== 'string'
32
+ || runtime.sessionConfigDigest.length !== 64
33
+ || runtimeMethods.some((name) => typeof runtime[name] !== 'function')
34
+ ) {
35
+ throw new Error('KINETIX_RUNTIME_INVALID');
36
+ }
37
+ assertKinetixRuntimeIdentity(runtime.identity);
38
+ return runtime;
39
+ }
40
+
41
+ export function assertKinetixRuntimeIdentity(identity) {
42
+ if (
43
+ identity === null
44
+ || typeof identity !== 'object'
45
+ || identity.abiVersion !== kinetixRuntimeAbiVersion
46
+ || !supportedTickRates.includes(identity.tickRate)
47
+ || identityStringFields.some(
48
+ (key) => typeof identity[key] !== 'string' || identity[key].length === 0,
49
+ )
50
+ ) {
51
+ throw new Error('KINETIX_RUNTIME_IDENTITY_INVALID');
52
+ }
53
+ return identity;
54
+ }
@@ -0,0 +1,170 @@
1
+ import { sha256Hex } from './shared/sha256.mjs';
2
+
3
+ const encoder = new TextEncoder();
4
+ const decoder = new TextDecoder('utf-8', { fatal: true });
5
+ const magic = encoder.encode('KSCFG001');
6
+ const digestDomain = encoder.encode('rundot.kinetix.session-config.v1\0');
7
+ const schemaKeys = new Set(['id', 'version', 'maxBytes', 'fields']);
8
+ const fieldKeys = new Set(['type', 'min', 'max', 'maxBytes', 'values', 'required']);
9
+
10
+ function fail(code) {
11
+ throw new Error(code);
12
+ }
13
+
14
+ function isPlainObject(value) {
15
+ return typeof value === 'object' && value !== null && !Array.isArray(value)
16
+ && Object.getPrototypeOf(value) === Object.prototype;
17
+ }
18
+
19
+ function concatBytes(chunks) {
20
+ const output = new Uint8Array(chunks.reduce((length, chunk) => length + chunk.length, 0));
21
+ let offset = 0;
22
+ for (const chunk of chunks) {
23
+ output.set(chunk, offset);
24
+ offset += chunk.length;
25
+ }
26
+ return output;
27
+ }
28
+
29
+ function u32(value) {
30
+ const bytes = new Uint8Array(4);
31
+ new DataView(bytes.buffer).setUint32(0, value, true);
32
+ return bytes;
33
+ }
34
+
35
+ function readU32(bytes, offset) {
36
+ if (offset + 4 > bytes.length) fail('KINETIX_SESSION_CONFIG_INVALID');
37
+ return new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength).getUint32(offset, true);
38
+ }
39
+
40
+ function canonicalize(value) {
41
+ if (Array.isArray(value)) return value.map(canonicalize);
42
+ if (isPlainObject(value)) {
43
+ return Object.fromEntries(Object.keys(value).sort().map((key) => [key, canonicalize(value[key])]));
44
+ }
45
+ return value;
46
+ }
47
+
48
+ export function normalizeKinetixSessionConfigSchema(schema) {
49
+ if (!isPlainObject(schema)
50
+ || Object.keys(schema).some((key) => !schemaKeys.has(key))
51
+ || typeof schema.id !== 'string'
52
+ || schema.id.length === 0
53
+ || !Number.isSafeInteger(schema.version)
54
+ || schema.version < 1
55
+ || !Number.isSafeInteger(schema.maxBytes)
56
+ || schema.maxBytes < 32
57
+ || !isPlainObject(schema.fields)) {
58
+ fail('KINETIX_SESSION_SCHEMA_INVALID');
59
+ }
60
+ const fields = {};
61
+ for (const name of Object.keys(schema.fields).sort()) {
62
+ const descriptor = schema.fields[name];
63
+ if (!/^[A-Za-z][A-Za-z0-9_]*$/u.test(name)
64
+ || !isPlainObject(descriptor)
65
+ || Object.keys(descriptor).some((key) => !fieldKeys.has(key))
66
+ || !['string', 'integer', 'boolean', 'enum'].includes(descriptor.type)
67
+ || (descriptor.required !== undefined && typeof descriptor.required !== 'boolean')) {
68
+ fail('KINETIX_SESSION_SCHEMA_INVALID');
69
+ }
70
+ if (descriptor.type === 'string'
71
+ && (!Number.isSafeInteger(descriptor.maxBytes) || descriptor.maxBytes < 0)) {
72
+ fail('KINETIX_SESSION_SCHEMA_INVALID');
73
+ }
74
+ if (descriptor.type === 'integer'
75
+ && (!Number.isSafeInteger(descriptor.min) || !Number.isSafeInteger(descriptor.max)
76
+ || descriptor.min > descriptor.max)) {
77
+ fail('KINETIX_SESSION_SCHEMA_INVALID');
78
+ }
79
+ if (descriptor.type === 'enum'
80
+ && (!Array.isArray(descriptor.values) || descriptor.values.length === 0
81
+ || descriptor.values.some((entry) => typeof entry !== 'string')
82
+ || new Set(descriptor.values).size !== descriptor.values.length
83
+ || [...descriptor.values].sort().some((entry, index) => entry !== descriptor.values[index]))) {
84
+ fail('KINETIX_SESSION_SCHEMA_INVALID');
85
+ }
86
+ fields[name] = canonicalize(descriptor);
87
+ }
88
+ return Object.freeze({ id: schema.id, version: schema.version, maxBytes: schema.maxBytes, fields: Object.freeze(fields) });
89
+ }
90
+
91
+ function validateValue(schema, value) {
92
+ if (!isPlainObject(value)) fail('KINETIX_SESSION_CONFIG_VALUE_INVALID');
93
+ if (Object.keys(value).some((key) => !Object.hasOwn(schema.fields, key))) {
94
+ fail('KINETIX_SESSION_CONFIG_FIELD_UNKNOWN');
95
+ }
96
+ const output = {};
97
+ for (const [name, descriptor] of Object.entries(schema.fields)) {
98
+ const entry = value[name];
99
+ if (entry === undefined && descriptor.required === false) continue;
100
+ let valid = true;
101
+ if (descriptor.type === 'string') valid = typeof entry === 'string' && encoder.encode(entry).length <= descriptor.maxBytes;
102
+ if (descriptor.type === 'integer') valid = Number.isSafeInteger(entry) && entry >= descriptor.min && entry <= descriptor.max;
103
+ if (descriptor.type === 'boolean') valid = typeof entry === 'boolean';
104
+ if (descriptor.type === 'enum') valid = typeof entry === 'string' && descriptor.values.includes(entry);
105
+ if (!valid) {
106
+ if (name === 'playerCount' || name === 'humanCount') fail('KINETIX_SESSION_CONFIG_PLAYER_COUNT_INVALID');
107
+ fail('KINETIX_SESSION_CONFIG_FIELD_INVALID');
108
+ }
109
+ output[name] = entry;
110
+ }
111
+ if (Number.isSafeInteger(output.playerCount)) {
112
+ if (output.playerCount < 1
113
+ || (Number.isSafeInteger(output.humanCount) && (output.humanCount < 0 || output.humanCount > output.playerCount))) {
114
+ fail('KINETIX_SESSION_CONFIG_PLAYER_COUNT_INVALID');
115
+ }
116
+ }
117
+ return canonicalize(output);
118
+ }
119
+
120
+ export function encodeKinetixSessionConfig(inputSchema, value) {
121
+ const schema = normalizeKinetixSessionConfigSchema(inputSchema);
122
+ const canonicalValue = validateValue(schema, value);
123
+ const schemaId = encoder.encode(schema.id);
124
+ const payload = encoder.encode(JSON.stringify(canonicalValue));
125
+ const bytes = concatBytes([magic, u32(schemaId.length), schemaId, u32(schema.version), u32(payload.length), payload]);
126
+ if (bytes.length > schema.maxBytes) fail('KINETIX_SESSION_CONFIG_SIZE_LIMIT');
127
+ return bytes;
128
+ }
129
+
130
+ export function decodeKinetixSessionConfig(inputSchema, bytes) {
131
+ const schema = normalizeKinetixSessionConfigSchema(inputSchema);
132
+ if (!(bytes instanceof Uint8Array) || bytes.length > schema.maxBytes || bytes.length < magic.length + 12
133
+ || magic.some((byte, index) => bytes[index] !== byte)) {
134
+ fail('KINETIX_SESSION_CONFIG_INVALID');
135
+ }
136
+ let offset = magic.length;
137
+ const schemaIdLength = readU32(bytes, offset);
138
+ offset += 4;
139
+ if (offset + schemaIdLength + 8 > bytes.length) fail('KINETIX_SESSION_CONFIG_INVALID');
140
+ let schemaId;
141
+ try {
142
+ schemaId = decoder.decode(bytes.subarray(offset, offset + schemaIdLength));
143
+ } catch {
144
+ fail('KINETIX_SESSION_CONFIG_INVALID');
145
+ }
146
+ offset += schemaIdLength;
147
+ const version = readU32(bytes, offset);
148
+ offset += 4;
149
+ if (schemaId !== schema.id || version !== schema.version) fail('KINETIX_SESSION_CONFIG_SCHEMA_MISMATCH');
150
+ const payloadLength = readU32(bytes, offset);
151
+ offset += 4;
152
+ if (offset + payloadLength !== bytes.length) fail('KINETIX_SESSION_CONFIG_INVALID');
153
+ let value;
154
+ try {
155
+ value = JSON.parse(decoder.decode(bytes.subarray(offset)));
156
+ } catch {
157
+ fail('KINETIX_SESSION_CONFIG_INVALID');
158
+ }
159
+ const validated = validateValue(schema, value);
160
+ const canonical = encodeKinetixSessionConfig(schema, validated);
161
+ if (canonical.length !== bytes.length || canonical.some((byte, index) => byte !== bytes[index])) {
162
+ fail('KINETIX_SESSION_CONFIG_NONCANONICAL');
163
+ }
164
+ return structuredClone(validated);
165
+ }
166
+
167
+ export function kinetixSessionConfigDigest(bytes) {
168
+ if (!(bytes instanceof Uint8Array)) fail('KINETIX_SESSION_CONFIG_INVALID');
169
+ return sha256Hex(concatBytes([digestDomain, bytes]));
170
+ }
@@ -0,0 +1,24 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { readFileSync, readdirSync, statSync } from 'node:fs';
3
+ import { join, relative, sep } from 'node:path';
4
+
5
+ export function kinetixCanonicalSourceDigest(sourceRoot) {
6
+ const hash = createHash('sha256');
7
+ function visit(directory) {
8
+ for (const name of readdirSync(directory).sort()) {
9
+ const path = join(directory, name);
10
+ const relativePath = relative(sourceRoot, path).split(sep).join('/');
11
+ const stats = statSync(path);
12
+ if (stats.isDirectory()) {
13
+ if (name !== '.git' && name !== 'node_modules' && name !== 'dist' && name !== 'build') visit(path);
14
+ } else if (!relativePath.endsWith('.product.generated.json')) {
15
+ hash.update(relativePath);
16
+ hash.update('\0');
17
+ hash.update(readFileSync(path));
18
+ hash.update('\0');
19
+ }
20
+ }
21
+ }
22
+ visit(sourceRoot);
23
+ return hash.digest('hex');
24
+ }