koota-kit 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,25 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.3.0](https://github.com/jbcom/koota-kit/compare/v0.2.1...v0.3.0) (2026-10-06)
4
+
5
+
6
+ ### Features
7
+
8
+ * add named RNG substreams, seed derivation, and master seeds ([a120cb7](https://github.com/jbcom/koota-kit/commit/a120cb7209eac1d16b964c6f3af2d97061cfcfc3))
9
+
10
+
11
+ ### Bug Fixes
12
+
13
+ * type EntropySource so Web Crypto implementations satisfy it ([7b8d3cb](https://github.com/jbcom/koota-kit/commit/7b8d3cb3ee8a5a994b1578f3000270d40b5295f9))
14
+
15
+ ## [0.2.1](https://github.com/jbcom/koota-kit/compare/v0.2.0...v0.2.1) (2026-10-06)
16
+
17
+
18
+ ### Bug Fixes
19
+
20
+ * sync the lockfile with the docs sourcey range ([eab4ae1](https://github.com/jbcom/koota-kit/commit/eab4ae1df3194cc1157b2a59ea0b04c3bc5345eb))
21
+ * sync the lockfile with the docs sourcey range ([be276e5](https://github.com/jbcom/koota-kit/commit/be276e52f7d58621fd3730a5762b8c4eccbc5cbb))
22
+
3
23
  ## [0.2.0](https://github.com/jbcom/koota-kit/compare/v0.1.1...v0.2.0) (2026-08-24)
4
24
 
5
25
 
package/README.md CHANGED
@@ -106,15 +106,20 @@ stream in query iteration order. Entity IDs contain process-global world and
106
106
  generation bits. Derive a local stream from a stable domain key instead:
107
107
 
108
108
  ```ts
109
- import { createRng, nextU32 } from "koota-kit/rng";
109
+ import { nextU32, substream } from "koota-kit/rng";
110
+ import { deriveSeed } from "koota-kit/seed";
110
111
 
111
- const tileRng = createRng({
112
- gen: `${String(sim.seeds.gen)}/tile:12,8`,
113
- events: 0,
114
- });
115
- const terrainVariant = nextU32(tileRng.gen);
112
+ const tileSeed = deriveSeed(sim.seeds.gen, "tile", 12, 8);
113
+ const terrainVariant = nextU32(substream(tileSeed, "terrain"));
116
114
  ```
117
115
 
116
+ `deriveSeed` hashes length-prefixed parts into a 128-bit hex seed, so seed
117
+ chains such as master seed → run → level never collide by accident.
118
+ `substream(seed, name)` gives each generator its own stream; adding a new
119
+ named substream never shifts an existing one. `createMasterSeed()` makes a
120
+ 128-bit root seed from `crypto.getRandomValues`, and accepts an injected
121
+ entropy source for tests.
122
+
118
123
  ### Object-valued traits need factories
119
124
 
120
125
  Koota's structure-of-arrays layout can alias a bare object literal across
@@ -137,9 +142,12 @@ the system responsible for acting on the event should drain.
137
142
  ## Persistence boundary
138
143
 
139
144
  `snapshotWorld` stores the clock and both RNG states. It intentionally does not
140
- serialize entities or traits: those are application data with application-
141
- specific schemas and migrations. `restoreWorldHeader` validates a parsed save
142
- before changing the handle and restores atomically.
145
+ serialize immutable seeds, entities, or traits: persist `sim.seeds` beside the
146
+ header so a fresh handle can retain stable derived randomness, and keep entity
147
+ data in application-specific schemas and migrations. Stage a fresh handle when
148
+ loading so old scratch caches and event logs cannot leak into the restored run.
149
+ `restoreWorldHeader` validates a parsed save before changing that staged handle
150
+ and restores atomically.
143
151
 
144
152
  Persist a version beside the header and your ECS state so the application can
145
153
  migrate its own schema.
@@ -151,7 +159,8 @@ The root package exports the full API. Focused entry points are also available:
151
159
  | Entry point | Purpose |
152
160
  | --- | --- |
153
161
  | `koota-kit/world` | World lifecycle, clock, Koota facade, world snapshots |
154
- | `koota-kit/rng` | Seeded streams, bounded draws, snapshots |
162
+ | `koota-kit/rng` | Seeded streams, named substreams, bounded draws, snapshots |
163
+ | `koota-kit/seed` | Seed derivation and 128-bit master seeds |
155
164
  | `koota-kit/traits` | Safe trait declaration |
156
165
  | `koota-kit/eventLog` | World-scoped publish/drain logs |
157
166
 
@@ -159,9 +168,17 @@ See the [API reference](./docs/API.md) for signatures and validation behavior,
159
168
  and [architecture notes](./docs/ARCHITECTURE.md) for module boundaries and
160
169
  invariants.
161
170
 
171
+ For task-oriented usage, read the Sourcey guides on
172
+ [deterministic randomness](https://jonbogaty.com/koota-kit/determinism/),
173
+ [save headers](https://jonbogaty.com/koota-kit/persistence/), and
174
+ [event ownership](https://jonbogaty.com/koota-kit/event-logs/).
175
+
162
176
  ## Errors and edge cases
163
177
 
164
- - Seeds must be strings or finite numbers.
178
+ - Seeds must be strings or finite numbers. Substream names must be non-empty
179
+ strings.
180
+ - `createMasterSeed` throws when no entropy source exists or the source leaves
181
+ the buffer all zero.
165
182
  - `advanceClock` accepts finite, non-negative `dt`; `0` is a valid paused tick.
166
183
  - `nextInt` requires safe-integer bounds with `maxExclusive > minInclusive` and
167
184
  a range no larger than `2**32`.
@@ -1,16 +1,18 @@
1
1
  "use strict";
2
2
  // koota-kit — thin koota ECS conventions layer.
3
3
  //
4
- // Four modules, also importable as subpaths:
4
+ // Five modules, also importable as subpaths:
5
5
  // ./world — WorldHandle lifecycle (world + dual-stream rng + clock + scratch)
6
- // ./rng — dual-layer deterministic seedrandom PRNG with byte-exact
7
- // snapshot/restore
6
+ // ./rng — dual-layer deterministic seedrandom PRNG and named substreams
7
+ // with byte-exact snapshot/restore
8
+ // ./seed — length-prefixed SHA-256 seed derivation and 128-bit master
9
+ // seeds (leaf module, zero runtime dependencies)
8
10
  // ./traits — defineTrait AoS-aliasing guard (leaf module, zero sibling
9
11
  // imports)
10
12
  // ./eventLog — scratch-backed publish/drain event logs with a peek seam for
11
13
  // observers (harnesses, tests, HUDs)
12
14
  Object.defineProperty(exports, "__esModule", { value: true });
13
- exports.defineTrait = exports.defineEventLog = exports.snapshotStream = exports.snapshotLayers = exports.restoreStream = exports.restoreLayers = exports.nextU32 = exports.nextInt = exports.nextFloat = exports.createRng = exports.chance = exports.trait = exports.snapshotWorld = exports.restoreWorldHeader = exports.relation = exports.destroySimWorld = exports.createSimWorld = exports.createActions = exports.advanceClock = void 0;
15
+ exports.defineTrait = exports.defineEventLog = exports.MASTER_SEED_BYTES = exports.isMasterSeed = exports.deriveSeed = exports.createMasterSeed = exports.substream = exports.snapshotSubstreams = exports.snapshotStream = exports.snapshotLayers = exports.restoreSubstreams = exports.restoreStream = exports.restoreLayers = exports.nextU32 = exports.nextInt = exports.nextFloat = exports.getSubstream = exports.createSubstreams = exports.createRng = exports.chance = exports.trait = exports.snapshotWorld = exports.restoreWorldHeader = exports.relation = exports.destroySimWorld = exports.createSimWorld = exports.createActions = exports.advanceClock = void 0;
14
16
  var world_js_1 = require("./world.cjs");
15
17
  Object.defineProperty(exports, "advanceClock", { enumerable: true, get: function () { return world_js_1.advanceClock; } });
16
18
  Object.defineProperty(exports, "createActions", { enumerable: true, get: function () { return world_js_1.createActions; } });
@@ -23,13 +25,23 @@ Object.defineProperty(exports, "trait", { enumerable: true, get: function () { r
23
25
  var rng_js_1 = require("./rng.cjs");
24
26
  Object.defineProperty(exports, "chance", { enumerable: true, get: function () { return rng_js_1.chance; } });
25
27
  Object.defineProperty(exports, "createRng", { enumerable: true, get: function () { return rng_js_1.createRng; } });
28
+ Object.defineProperty(exports, "createSubstreams", { enumerable: true, get: function () { return rng_js_1.createSubstreams; } });
29
+ Object.defineProperty(exports, "getSubstream", { enumerable: true, get: function () { return rng_js_1.getSubstream; } });
26
30
  Object.defineProperty(exports, "nextFloat", { enumerable: true, get: function () { return rng_js_1.nextFloat; } });
27
31
  Object.defineProperty(exports, "nextInt", { enumerable: true, get: function () { return rng_js_1.nextInt; } });
28
32
  Object.defineProperty(exports, "nextU32", { enumerable: true, get: function () { return rng_js_1.nextU32; } });
29
33
  Object.defineProperty(exports, "restoreLayers", { enumerable: true, get: function () { return rng_js_1.restoreLayers; } });
30
34
  Object.defineProperty(exports, "restoreStream", { enumerable: true, get: function () { return rng_js_1.restoreStream; } });
35
+ Object.defineProperty(exports, "restoreSubstreams", { enumerable: true, get: function () { return rng_js_1.restoreSubstreams; } });
31
36
  Object.defineProperty(exports, "snapshotLayers", { enumerable: true, get: function () { return rng_js_1.snapshotLayers; } });
32
37
  Object.defineProperty(exports, "snapshotStream", { enumerable: true, get: function () { return rng_js_1.snapshotStream; } });
38
+ Object.defineProperty(exports, "snapshotSubstreams", { enumerable: true, get: function () { return rng_js_1.snapshotSubstreams; } });
39
+ Object.defineProperty(exports, "substream", { enumerable: true, get: function () { return rng_js_1.substream; } });
40
+ var seed_js_1 = require("./seed.cjs");
41
+ Object.defineProperty(exports, "createMasterSeed", { enumerable: true, get: function () { return seed_js_1.createMasterSeed; } });
42
+ Object.defineProperty(exports, "deriveSeed", { enumerable: true, get: function () { return seed_js_1.deriveSeed; } });
43
+ Object.defineProperty(exports, "isMasterSeed", { enumerable: true, get: function () { return seed_js_1.isMasterSeed; } });
44
+ Object.defineProperty(exports, "MASTER_SEED_BYTES", { enumerable: true, get: function () { return seed_js_1.MASTER_SEED_BYTES; } });
33
45
  var eventLog_js_1 = require("./eventLog.cjs");
34
46
  Object.defineProperty(exports, "defineEventLog", { enumerable: true, get: function () { return eventLog_js_1.defineEventLog; } });
35
47
  var index_js_1 = require("./traits/index.cjs");
@@ -1,7 +1,9 @@
1
1
  export { advanceClock, createActions, createSimWorld, destroySimWorld, relation, restoreWorldHeader, snapshotWorld, trait, } from "./world.cjs";
2
2
  export type { Entity, World, WorldHandle, WorldSnapshot } from "./world.cjs";
3
- export { chance, createRng, nextFloat, nextInt, nextU32, restoreLayers, restoreStream, snapshotLayers, snapshotStream, } from "./rng.cjs";
4
- export type { RngLayers, RngLayersSnapshot, RngSeeds, RngStream, RngStreamSnapshot, } from "./rng.cjs";
3
+ export { chance, createRng, createSubstreams, getSubstream, nextFloat, nextInt, nextU32, restoreLayers, restoreStream, restoreSubstreams, snapshotLayers, snapshotStream, snapshotSubstreams, substream, } from "./rng.cjs";
4
+ export type { RngLayers, RngLayersSnapshot, RngSeeds, RngStream, RngStreamSnapshot, Substreams, SubstreamsSnapshot, } from "./rng.cjs";
5
+ export { createMasterSeed, deriveSeed, isMasterSeed, MASTER_SEED_BYTES } from "./seed.cjs";
6
+ export type { EntropySource, SeedPart } from "./seed.cjs";
5
7
  export { defineEventLog } from "./eventLog.cjs";
6
8
  export type { EventLog } from "./eventLog.cjs";
7
9
  export { defineTrait } from "./traits/index.cjs";
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAWA,OAAO,EACL,YAAY,EACZ,aAAa,EACb,cAAc,EACd,eAAe,EACf,QAAQ,EACR,kBAAkB,EAClB,aAAa,EACb,KAAK,GACN,MAAM,YAAY,CAAC;AACpB,YAAY,EAAE,MAAM,EAAE,KAAK,EAAE,WAAW,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAE5E,OAAO,EACL,MAAM,EACN,SAAS,EACT,SAAS,EACT,OAAO,EACP,OAAO,EACP,aAAa,EACb,aAAa,EACb,cAAc,EACd,cAAc,GACf,MAAM,UAAU,CAAC;AAClB,YAAY,EACV,SAAS,EACT,iBAAiB,EACjB,QAAQ,EACR,SAAS,EACT,iBAAiB,GAClB,MAAM,UAAU,CAAC;AAElB,OAAO,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AAC/C,YAAY,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AAE9C,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAChD,YAAY,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAaA,OAAO,EACL,YAAY,EACZ,aAAa,EACb,cAAc,EACd,eAAe,EACf,QAAQ,EACR,kBAAkB,EAClB,aAAa,EACb,KAAK,GACN,MAAM,YAAY,CAAC;AACpB,YAAY,EAAE,MAAM,EAAE,KAAK,EAAE,WAAW,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAE5E,OAAO,EACL,MAAM,EACN,SAAS,EACT,gBAAgB,EAChB,YAAY,EACZ,SAAS,EACT,OAAO,EACP,OAAO,EACP,aAAa,EACb,aAAa,EACb,iBAAiB,EACjB,cAAc,EACd,cAAc,EACd,kBAAkB,EAClB,SAAS,GACV,MAAM,UAAU,CAAC;AAClB,YAAY,EACV,SAAS,EACT,iBAAiB,EACjB,QAAQ,EACR,SAAS,EACT,iBAAiB,EACjB,UAAU,EACV,kBAAkB,GACnB,MAAM,UAAU,CAAC;AAElB,OAAO,EAAE,gBAAgB,EAAE,UAAU,EAAE,YAAY,EAAE,iBAAiB,EAAE,MAAM,WAAW,CAAC;AAC1F,YAAY,EAAE,aAAa,EAAE,QAAQ,EAAE,MAAM,WAAW,CAAC;AAEzD,OAAO,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AAC/C,YAAY,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AAE9C,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAChD,YAAY,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC"}
package/dist/cjs/rng.cjs CHANGED
@@ -32,7 +32,13 @@ exports.snapshotStream = snapshotStream;
32
32
  exports.snapshotLayers = snapshotLayers;
33
33
  exports.restoreStream = restoreStream;
34
34
  exports.restoreLayers = restoreLayers;
35
+ exports.substream = substream;
36
+ exports.createSubstreams = createSubstreams;
37
+ exports.getSubstream = getSubstream;
38
+ exports.snapshotSubstreams = snapshotSubstreams;
39
+ exports.restoreSubstreams = restoreSubstreams;
35
40
  const seedrandom_1 = __importDefault(require("seedrandom"));
41
+ const seed_js_1 = require("./seed.cjs");
36
42
  function assertSeed(seed, name) {
37
43
  if (typeof seed !== "string" && typeof seed !== "number") {
38
44
  throw new TypeError(`createRng: seeds.${name} must be a string or finite number.`);
@@ -166,3 +172,77 @@ function restoreLayers(snap) {
166
172
  }
167
173
  return { gen: restoreStream(snap.gen), events: restoreStream(snap.events) };
168
174
  }
175
+ function assertSubstreamName(name, api) {
176
+ if (typeof name !== "string" || name.length === 0) {
177
+ throw new TypeError(`${api}: name must be a non-empty string.`);
178
+ }
179
+ }
180
+ function assertSubstreamSeed(seed, api) {
181
+ if (typeof seed !== "string" && !(typeof seed === "number" && Number.isFinite(seed))) {
182
+ throw new TypeError(`${api}: seed must be a string or finite number.`);
183
+ }
184
+ }
185
+ /**
186
+ * Create the stream named `name` under `seed`: a fresh stream seeded by
187
+ * `deriveSeed(seed, name)`. Creating or consuming any other substream never
188
+ * shifts this one.
189
+ */
190
+ function substream(seed, name) {
191
+ assertSubstreamName(name, "substream");
192
+ return makeStream((0, seed_js_1.deriveSeed)(seed, name));
193
+ }
194
+ /** Create an empty, lazily populated set of named substreams under `seed`. */
195
+ function createSubstreams(seed) {
196
+ assertSubstreamSeed(seed, "createSubstreams");
197
+ return { seed, streams: new Map() };
198
+ }
199
+ /**
200
+ * Return the set's stream for `name`, creating it from the set's seed on first
201
+ * use. The same name always returns the same live stream instance.
202
+ */
203
+ function getSubstream(substreams, name) {
204
+ assertSubstreamName(name, "getSubstream");
205
+ const existing = substreams.streams.get(name);
206
+ if (existing)
207
+ return existing;
208
+ const stream = substream(substreams.seed, name);
209
+ substreams.streams.set(name, stream);
210
+ return stream;
211
+ }
212
+ /**
213
+ * Capture the set's seed plus every stream created so far, with names in
214
+ * sorted order. JSON-serializable and detached from later draws.
215
+ */
216
+ function snapshotSubstreams(substreams) {
217
+ // Object.fromEntries defines own data properties, so a stream named
218
+ // "__proto__" is captured instead of hitting the prototype setter.
219
+ const streams = Object.fromEntries([...substreams.streams.keys()]
220
+ // UTF-16 code-unit order, never localeCompare: the order must not vary
221
+ // by runtime locale. Branch-free because names in a Map are unique.
222
+ .sort((left, right) => Number(left > right) - Number(left < right))
223
+ .map((name) => [name, snapshotStream(substreams.streams.get(name))]));
224
+ return { seed: substreams.seed, streams };
225
+ }
226
+ /**
227
+ * Rebuild a substream set from `snapshotSubstreams` output. Validates the
228
+ * complete snapshot before building anything; streams that were never created
229
+ * restore lazily from the seed on first `getSubstream`.
230
+ */
231
+ function restoreSubstreams(snapshot) {
232
+ if (typeof snapshot !== "object" || snapshot === null) {
233
+ throw new TypeError("restoreSubstreams: snapshot must contain a seed and streams.");
234
+ }
235
+ assertSubstreamSeed(snapshot.seed, "restoreSubstreams");
236
+ const entries = snapshot.streams;
237
+ if (typeof entries !== "object" || entries === null || Array.isArray(entries)) {
238
+ throw new TypeError("restoreSubstreams: streams must be an object keyed by stream name.");
239
+ }
240
+ const restored = new Map();
241
+ for (const [name, stream] of Object.entries(entries)) {
242
+ if (name.length === 0) {
243
+ throw new TypeError("restoreSubstreams: stream names must be non-empty strings.");
244
+ }
245
+ restored.set(name, restoreStream(stream));
246
+ }
247
+ return { seed: snapshot.seed, streams: restored };
248
+ }
@@ -82,5 +82,39 @@ export declare function restoreStream(snap: RngStreamSnapshot): RngStream;
82
82
  * `restoreStream` — applied independently to `gen` and `events`.
83
83
  */
84
84
  export declare function restoreLayers(snap: RngLayersSnapshot): RngLayers;
85
+ export type Substreams = {
86
+ /** The seed every member stream is derived from. */
87
+ readonly seed: string | number;
88
+ /** Streams created so far, keyed by name. Create members with `getSubstream`. */
89
+ readonly streams: ReadonlyMap<string, RngStream>;
90
+ };
91
+ export type SubstreamsSnapshot = {
92
+ seed: string | number;
93
+ streams: Record<string, RngStreamSnapshot>;
94
+ };
95
+ /**
96
+ * Create the stream named `name` under `seed`: a fresh stream seeded by
97
+ * `deriveSeed(seed, name)`. Creating or consuming any other substream never
98
+ * shifts this one.
99
+ */
100
+ export declare function substream(seed: string | number, name: string): RngStream;
101
+ /** Create an empty, lazily populated set of named substreams under `seed`. */
102
+ export declare function createSubstreams(seed: string | number): Substreams;
103
+ /**
104
+ * Return the set's stream for `name`, creating it from the set's seed on first
105
+ * use. The same name always returns the same live stream instance.
106
+ */
107
+ export declare function getSubstream(substreams: Substreams, name: string): RngStream;
108
+ /**
109
+ * Capture the set's seed plus every stream created so far, with names in
110
+ * sorted order. JSON-serializable and detached from later draws.
111
+ */
112
+ export declare function snapshotSubstreams(substreams: Substreams): SubstreamsSnapshot;
113
+ /**
114
+ * Rebuild a substream set from `snapshotSubstreams` output. Validates the
115
+ * complete snapshot before building anything; streams that were never created
116
+ * restore lazily from the seed on first `getSubstream`.
117
+ */
118
+ export declare function restoreSubstreams(snapshot: SubstreamsSnapshot): Substreams;
85
119
  export {};
86
120
  //# sourceMappingURL=rng.d.cts.map
@@ -1 +1 @@
1
- {"version":3,"file":"rng.d.ts","sourceRoot":"","sources":["../../src/rng.ts"],"names":[],"mappings":"AAqBA,OAAO,UAAU,MAAM,YAAY,CAAC;AAEpC,KAAK,KAAK,GAAG,UAAU,CAAC,KAAK,CAAC,IAAI,CAAC;AACnC,KAAK,QAAQ,GAAG,UAAU,CAAC,YAAY,CAAC,KAAK,CAAC,CAAC;AAE/C,MAAM,MAAM,SAAS,GAAG;IACtB,IAAI,EAAE,QAAQ,CAAC;CAChB,CAAC;AAEF,MAAM,MAAM,SAAS,GAAG;IACtB,GAAG,EAAE,SAAS,CAAC;IACf,MAAM,EAAE,SAAS,CAAC;CACnB,CAAC;AAEF,MAAM,MAAM,QAAQ,GAAG;IACrB,uEAAuE;IACvE,QAAQ,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAAC;IAC9B,8FAA8F;IAC9F,QAAQ,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAAC;CAClC,CAAC;AAgBF;;;;;GAKG;AACH,wBAAgB,SAAS,CAAC,KAAK,EAAE,QAAQ,GAAG,SAAS,CAUpD;AAED;;;;;;GAMG;AACH,wBAAgB,OAAO,CAAC,MAAM,EAAE,SAAS,GAAG,MAAM,CAEjD;AAED;;;;GAIG;AACH,wBAAgB,SAAS,CAAC,MAAM,EAAE,SAAS,GAAG,MAAM,CAEnD;AAED;;;;;GAKG;AACH,wBAAgB,OAAO,CAAC,MAAM,EAAE,SAAS,EAAE,YAAY,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,GAAG,MAAM,CAc7F;AAED;;;;;;GAMG;AACH,wBAAgB,MAAM,CAAC,MAAM,EAAE,SAAS,EAAE,CAAC,EAAE,MAAM,GAAG,OAAO,CAK5D;AAED,MAAM,MAAM,iBAAiB,GAAG;IAAE,KAAK,EAAE,KAAK,CAAA;CAAE,CAAC;AACjD,MAAM,MAAM,iBAAiB,GAAG;IAAE,GAAG,EAAE,iBAAiB,CAAC;IAAC,MAAM,EAAE,iBAAiB,CAAA;CAAE,CAAC;AAEtF;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,MAAM,EAAE,SAAS,GAAG,iBAAiB,CAGnE;AAED;;;GAGG;AACH,wBAAgB,cAAc,CAAC,GAAG,EAAE,SAAS,GAAG,iBAAiB,CAEhE;AAED;;;;;;GAMG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,iBAAiB,GAAG,SAAS,CAqBhE;AAED;;;GAGG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,iBAAiB,GAAG,SAAS,CAKhE"}
1
+ {"version":3,"file":"rng.d.ts","sourceRoot":"","sources":["../../src/rng.ts"],"names":[],"mappings":"AAqBA,OAAO,UAAU,MAAM,YAAY,CAAC;AAGpC,KAAK,KAAK,GAAG,UAAU,CAAC,KAAK,CAAC,IAAI,CAAC;AACnC,KAAK,QAAQ,GAAG,UAAU,CAAC,YAAY,CAAC,KAAK,CAAC,CAAC;AAE/C,MAAM,MAAM,SAAS,GAAG;IACtB,IAAI,EAAE,QAAQ,CAAC;CAChB,CAAC;AAEF,MAAM,MAAM,SAAS,GAAG;IACtB,GAAG,EAAE,SAAS,CAAC;IACf,MAAM,EAAE,SAAS,CAAC;CACnB,CAAC;AAEF,MAAM,MAAM,QAAQ,GAAG;IACrB,uEAAuE;IACvE,QAAQ,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAAC;IAC9B,8FAA8F;IAC9F,QAAQ,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAAC;CAClC,CAAC;AAgBF;;;;;GAKG;AACH,wBAAgB,SAAS,CAAC,KAAK,EAAE,QAAQ,GAAG,SAAS,CAUpD;AAED;;;;;;GAMG;AACH,wBAAgB,OAAO,CAAC,MAAM,EAAE,SAAS,GAAG,MAAM,CAEjD;AAED;;;;GAIG;AACH,wBAAgB,SAAS,CAAC,MAAM,EAAE,SAAS,GAAG,MAAM,CAEnD;AAED;;;;;GAKG;AACH,wBAAgB,OAAO,CAAC,MAAM,EAAE,SAAS,EAAE,YAAY,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,GAAG,MAAM,CAc7F;AAED;;;;;;GAMG;AACH,wBAAgB,MAAM,CAAC,MAAM,EAAE,SAAS,EAAE,CAAC,EAAE,MAAM,GAAG,OAAO,CAK5D;AAED,MAAM,MAAM,iBAAiB,GAAG;IAAE,KAAK,EAAE,KAAK,CAAA;CAAE,CAAC;AACjD,MAAM,MAAM,iBAAiB,GAAG;IAAE,GAAG,EAAE,iBAAiB,CAAC;IAAC,MAAM,EAAE,iBAAiB,CAAA;CAAE,CAAC;AAEtF;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,MAAM,EAAE,SAAS,GAAG,iBAAiB,CAGnE;AAED;;;GAGG;AACH,wBAAgB,cAAc,CAAC,GAAG,EAAE,SAAS,GAAG,iBAAiB,CAEhE;AAED;;;;;;GAMG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,iBAAiB,GAAG,SAAS,CAqBhE;AAED;;;GAGG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,iBAAiB,GAAG,SAAS,CAKhE;AAUD,MAAM,MAAM,UAAU,GAAG;IACvB,oDAAoD;IACpD,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAAC;IAC/B,iFAAiF;IACjF,QAAQ,CAAC,OAAO,EAAE,WAAW,CAAC,MAAM,EAAE,SAAS,CAAC,CAAC;CAClD,CAAC;AAEF,MAAM,MAAM,kBAAkB,GAAG;IAC/B,IAAI,EAAE,MAAM,GAAG,MAAM,CAAC;IACtB,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,iBAAiB,CAAC,CAAC;CAC5C,CAAC;AAcF;;;;GAIG;AACH,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,SAAS,CAGxE;AAED,8EAA8E;AAC9E,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,UAAU,CAGlE;AAED;;;GAGG;AACH,wBAAgB,YAAY,CAAC,UAAU,EAAE,UAAU,EAAE,IAAI,EAAE,MAAM,GAAG,SAAS,CAO5E;AAED;;;GAGG;AACH,wBAAgB,kBAAkB,CAAC,UAAU,EAAE,UAAU,GAAG,kBAAkB,CAW7E;AAED;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,kBAAkB,GAAG,UAAU,CAiB1E"}
@@ -0,0 +1,188 @@
1
+ "use strict";
2
+ // Seed derivation and master seeds.
3
+ //
4
+ // A seeded game usually has a chain of seeds: a buried master seed for the
5
+ // profile, a run seed derived from it, a level seed derived from that, and
6
+ // one stream per generator. Building those keys by string concatenation is
7
+ // ambiguous ("1" + "23" === "12" + "3"), so `deriveSeed` hashes length-prefixed
8
+ // parts instead. Leaf module: no runtime dependency, so it is identical in
9
+ // browsers and Node and in both the ESM and CommonJS builds.
10
+ Object.defineProperty(exports, "__esModule", { value: true });
11
+ exports.MASTER_SEED_BYTES = void 0;
12
+ exports.deriveSeed = deriveSeed;
13
+ exports.createMasterSeed = createMasterSeed;
14
+ exports.isMasterSeed = isMasterSeed;
15
+ /** Byte length of a master seed (128 bits). */
16
+ exports.MASTER_SEED_BYTES = 16;
17
+ // Changing the domain tag or the encoding changes every derived seed in every
18
+ // consumer's saves. Treat both as a frozen public contract.
19
+ const DERIVE_DOMAIN = "koota-kit:deriveSeed:v1";
20
+ const HEX_SEED = /^[0-9a-f]{32}$/;
21
+ function canonicalPart(value, label, api) {
22
+ if (typeof value === "string")
23
+ return value;
24
+ if (typeof value === "number" && Number.isFinite(value))
25
+ return String(value);
26
+ throw new TypeError(`${api}: ${label} must be a string or finite number.`);
27
+ }
28
+ /** UTF-8 encode, mapping lone surrogates to U+FFFD exactly like TextEncoder. */
29
+ function utf8(text) {
30
+ const bytes = [];
31
+ // String iteration yields whole code points; an unpaired surrogate arrives
32
+ // alone as its own code unit, so it is the only surrogate value seen here.
33
+ for (const char of text) {
34
+ let code = char.codePointAt(0);
35
+ if (code >= 0xd800 && code <= 0xdfff)
36
+ code = 0xfffd;
37
+ if (code < 0x80) {
38
+ bytes.push(code);
39
+ }
40
+ else if (code < 0x800) {
41
+ bytes.push(0xc0 | (code >> 6), 0x80 | (code & 0x3f));
42
+ }
43
+ else if (code < 0x10000) {
44
+ bytes.push(0xe0 | (code >> 12), 0x80 | ((code >> 6) & 0x3f), 0x80 | (code & 0x3f));
45
+ }
46
+ else {
47
+ bytes.push(0xf0 | (code >> 18), 0x80 | ((code >> 12) & 0x3f), 0x80 | ((code >> 6) & 0x3f), 0x80 | (code & 0x3f));
48
+ }
49
+ }
50
+ return bytes;
51
+ }
52
+ const K = [
53
+ 0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5,
54
+ 0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174,
55
+ 0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da,
56
+ 0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967,
57
+ 0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85,
58
+ 0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070,
59
+ 0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3,
60
+ 0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2,
61
+ ];
62
+ const rotr = (value, bits) => (value >>> bits) | (value << (32 - bits));
63
+ /** FIPS 180-4 SHA-256 of `message`, as eight big-endian 32-bit words. */
64
+ function sha256(message) {
65
+ const bitLength = message.length * 8;
66
+ const zeroPadding = (55 - (message.length % 64) + 64) % 64;
67
+ // Seed inputs are far below 2**32 bits, so the high length word is zero.
68
+ const padded = [
69
+ ...message,
70
+ 0x80,
71
+ ...new Array(zeroPadding).fill(0),
72
+ 0,
73
+ 0,
74
+ 0,
75
+ 0,
76
+ bitLength >>> 24,
77
+ (bitLength >>> 16) & 0xff,
78
+ (bitLength >>> 8) & 0xff,
79
+ bitLength & 0xff,
80
+ ];
81
+ const hash = new Uint32Array([
82
+ 0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a, 0x510e527f, 0x9b05688c, 0x1f83d9ab, 0x5be0cd19,
83
+ ]);
84
+ const w = new Uint32Array(64);
85
+ for (let offset = 0; offset < padded.length; offset += 64) {
86
+ for (let t = 0; t < 16; t += 1) {
87
+ const i = offset + t * 4;
88
+ w[t] =
89
+ (padded[i] << 24) |
90
+ (padded[i + 1] << 16) |
91
+ (padded[i + 2] << 8) |
92
+ padded[i + 3];
93
+ }
94
+ for (let t = 16; t < 64; t += 1) {
95
+ const w15 = w[t - 15];
96
+ const w2 = w[t - 2];
97
+ const s0 = rotr(w15, 7) ^ rotr(w15, 18) ^ (w15 >>> 3);
98
+ const s1 = rotr(w2, 17) ^ rotr(w2, 19) ^ (w2 >>> 10);
99
+ w[t] = (w[t - 16] + s0 + w[t - 7] + s1) >>> 0;
100
+ }
101
+ let a = hash[0];
102
+ let b = hash[1];
103
+ let c = hash[2];
104
+ let d = hash[3];
105
+ let e = hash[4];
106
+ let f = hash[5];
107
+ let g = hash[6];
108
+ let h = hash[7];
109
+ for (let t = 0; t < 64; t += 1) {
110
+ const t1 = (h +
111
+ (rotr(e, 6) ^ rotr(e, 11) ^ rotr(e, 25)) +
112
+ ((e & f) ^ (~e & g)) +
113
+ K[t] +
114
+ w[t]) >>>
115
+ 0;
116
+ const t2 = ((rotr(a, 2) ^ rotr(a, 13) ^ rotr(a, 22)) + ((a & b) ^ (a & c) ^ (b & c))) >>> 0;
117
+ h = g;
118
+ g = f;
119
+ f = e;
120
+ e = (d + t1) >>> 0;
121
+ d = c;
122
+ c = b;
123
+ b = a;
124
+ a = (t1 + t2) >>> 0;
125
+ }
126
+ hash[0] = (hash[0] + a) >>> 0;
127
+ hash[1] = (hash[1] + b) >>> 0;
128
+ hash[2] = (hash[2] + c) >>> 0;
129
+ hash[3] = (hash[3] + d) >>> 0;
130
+ hash[4] = (hash[4] + e) >>> 0;
131
+ hash[5] = (hash[5] + f) >>> 0;
132
+ hash[6] = (hash[6] + g) >>> 0;
133
+ hash[7] = (hash[7] + h) >>> 0;
134
+ }
135
+ return hash;
136
+ }
137
+ const toHex = (bytes) => Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("");
138
+ /**
139
+ * Derive a child seed from `parent` and ordered `parts`. Returns the first 128
140
+ * bits of SHA-256 over a domain tag plus each value as a 4-byte big-endian
141
+ * UTF-8 length and its UTF-8 text, as 32 lowercase hex characters. Numbers
142
+ * canonicalize to their string form, matching `createRng`.
143
+ */
144
+ function deriveSeed(parent, ...parts) {
145
+ const values = [canonicalPart(parent, "parent", "deriveSeed")];
146
+ parts.forEach((part, index) => {
147
+ values.push(canonicalPart(part, `parts[${index}]`, "deriveSeed"));
148
+ });
149
+ const chunks = [utf8(DERIVE_DOMAIN)];
150
+ for (const value of values) {
151
+ const encoded = utf8(value);
152
+ const length = encoded.length;
153
+ const prefix = [
154
+ (length >>> 24) & 0xff,
155
+ (length >>> 16) & 0xff,
156
+ (length >>> 8) & 0xff,
157
+ length & 0xff,
158
+ ];
159
+ chunks.push(prefix, encoded);
160
+ }
161
+ const digest = sha256(chunks.flat());
162
+ return Array.from(digest.subarray(0, 4), (word) => word.toString(16).padStart(8, "0")).join("");
163
+ }
164
+ /**
165
+ * Create a 128-bit master seed from `entropy` (default `globalThis.crypto`)
166
+ * as 32 lowercase hex characters. Fails closed when no source exists, when it
167
+ * returns anything but the requested 16-byte buffer, or when the buffer is
168
+ * still all zero (an unfilled stub, not entropy).
169
+ */
170
+ function createMasterSeed(entropy) {
171
+ const source = entropy ?? globalThis.crypto;
172
+ if (!source || typeof source.getRandomValues !== "function") {
173
+ throw new TypeError("createMasterSeed: no entropy source with getRandomValues is available.");
174
+ }
175
+ const requested = new Uint8Array(exports.MASTER_SEED_BYTES);
176
+ const bytes = source.getRandomValues(requested);
177
+ if (bytes !== requested) {
178
+ throw new TypeError("createMasterSeed: the entropy source must fill the requested 16-byte array.");
179
+ }
180
+ if (bytes.every((byte) => byte === 0)) {
181
+ throw new RangeError("createMasterSeed: the entropy source returned all zero bytes.");
182
+ }
183
+ return toHex(bytes);
184
+ }
185
+ /** True for exactly 32 lowercase hex characters: the shape of a master or derived seed. */
186
+ function isMasterSeed(value) {
187
+ return typeof value === "string" && HEX_SEED.test(value);
188
+ }
@@ -0,0 +1,25 @@
1
+ /** A value that can seed a stream or be mixed into a derived seed. */
2
+ export type SeedPart = string | number;
3
+ /** The subset of Web Crypto that `createMasterSeed` needs. Inject one in tests. */
4
+ export type EntropySource = {
5
+ getRandomValues(array: Uint8Array<ArrayBuffer>): Uint8Array<ArrayBuffer>;
6
+ };
7
+ /** Byte length of a master seed (128 bits). */
8
+ export declare const MASTER_SEED_BYTES = 16;
9
+ /**
10
+ * Derive a child seed from `parent` and ordered `parts`. Returns the first 128
11
+ * bits of SHA-256 over a domain tag plus each value as a 4-byte big-endian
12
+ * UTF-8 length and its UTF-8 text, as 32 lowercase hex characters. Numbers
13
+ * canonicalize to their string form, matching `createRng`.
14
+ */
15
+ export declare function deriveSeed(parent: SeedPart, ...parts: SeedPart[]): string;
16
+ /**
17
+ * Create a 128-bit master seed from `entropy` (default `globalThis.crypto`)
18
+ * as 32 lowercase hex characters. Fails closed when no source exists, when it
19
+ * returns anything but the requested 16-byte buffer, or when the buffer is
20
+ * still all zero (an unfilled stub, not entropy).
21
+ */
22
+ export declare function createMasterSeed(entropy?: EntropySource): string;
23
+ /** True for exactly 32 lowercase hex characters: the shape of a master or derived seed. */
24
+ export declare function isMasterSeed(value: unknown): value is string;
25
+ //# sourceMappingURL=seed.d.cts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"seed.d.ts","sourceRoot":"","sources":["../../src/seed.ts"],"names":[],"mappings":"AASA,sEAAsE;AACtE,MAAM,MAAM,QAAQ,GAAG,MAAM,GAAG,MAAM,CAAC;AAEvC,mFAAmF;AAGnF,MAAM,MAAM,aAAa,GAAG;IAC1B,eAAe,CAAC,KAAK,EAAE,UAAU,CAAC,WAAW,CAAC,GAAG,UAAU,CAAC,WAAW,CAAC,CAAC;CAC1E,CAAC;AAEF,+CAA+C;AAC/C,eAAO,MAAM,iBAAiB,KAAK,CAAC;AAoIpC;;;;;GAKG;AACH,wBAAgB,UAAU,CAAC,MAAM,EAAE,QAAQ,EAAE,GAAG,KAAK,EAAE,QAAQ,EAAE,GAAG,MAAM,CAmBzE;AAED;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,CAAC,EAAE,aAAa,GAAG,MAAM,CAgBhE;AAED,2FAA2F;AAC3F,wBAAgB,YAAY,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,MAAM,CAE5D"}
@@ -1,7 +1,9 @@
1
1
  export { advanceClock, createActions, createSimWorld, destroySimWorld, relation, restoreWorldHeader, snapshotWorld, trait, } from "./world.js";
2
2
  export type { Entity, World, WorldHandle, WorldSnapshot } from "./world.js";
3
- export { chance, createRng, nextFloat, nextInt, nextU32, restoreLayers, restoreStream, snapshotLayers, snapshotStream, } from "./rng.js";
4
- export type { RngLayers, RngLayersSnapshot, RngSeeds, RngStream, RngStreamSnapshot, } from "./rng.js";
3
+ export { chance, createRng, createSubstreams, getSubstream, nextFloat, nextInt, nextU32, restoreLayers, restoreStream, restoreSubstreams, snapshotLayers, snapshotStream, snapshotSubstreams, substream, } from "./rng.js";
4
+ export type { RngLayers, RngLayersSnapshot, RngSeeds, RngStream, RngStreamSnapshot, Substreams, SubstreamsSnapshot, } from "./rng.js";
5
+ export { createMasterSeed, deriveSeed, isMasterSeed, MASTER_SEED_BYTES } from "./seed.js";
6
+ export type { EntropySource, SeedPart } from "./seed.js";
5
7
  export { defineEventLog } from "./eventLog.js";
6
8
  export type { EventLog } from "./eventLog.js";
7
9
  export { defineTrait } from "./traits/index.js";
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAWA,OAAO,EACL,YAAY,EACZ,aAAa,EACb,cAAc,EACd,eAAe,EACf,QAAQ,EACR,kBAAkB,EAClB,aAAa,EACb,KAAK,GACN,MAAM,YAAY,CAAC;AACpB,YAAY,EAAE,MAAM,EAAE,KAAK,EAAE,WAAW,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAE5E,OAAO,EACL,MAAM,EACN,SAAS,EACT,SAAS,EACT,OAAO,EACP,OAAO,EACP,aAAa,EACb,aAAa,EACb,cAAc,EACd,cAAc,GACf,MAAM,UAAU,CAAC;AAClB,YAAY,EACV,SAAS,EACT,iBAAiB,EACjB,QAAQ,EACR,SAAS,EACT,iBAAiB,GAClB,MAAM,UAAU,CAAC;AAElB,OAAO,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AAC/C,YAAY,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AAE9C,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAChD,YAAY,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAaA,OAAO,EACL,YAAY,EACZ,aAAa,EACb,cAAc,EACd,eAAe,EACf,QAAQ,EACR,kBAAkB,EAClB,aAAa,EACb,KAAK,GACN,MAAM,YAAY,CAAC;AACpB,YAAY,EAAE,MAAM,EAAE,KAAK,EAAE,WAAW,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAE5E,OAAO,EACL,MAAM,EACN,SAAS,EACT,gBAAgB,EAChB,YAAY,EACZ,SAAS,EACT,OAAO,EACP,OAAO,EACP,aAAa,EACb,aAAa,EACb,iBAAiB,EACjB,cAAc,EACd,cAAc,EACd,kBAAkB,EAClB,SAAS,GACV,MAAM,UAAU,CAAC;AAClB,YAAY,EACV,SAAS,EACT,iBAAiB,EACjB,QAAQ,EACR,SAAS,EACT,iBAAiB,EACjB,UAAU,EACV,kBAAkB,GACnB,MAAM,UAAU,CAAC;AAElB,OAAO,EAAE,gBAAgB,EAAE,UAAU,EAAE,YAAY,EAAE,iBAAiB,EAAE,MAAM,WAAW,CAAC;AAC1F,YAAY,EAAE,aAAa,EAAE,QAAQ,EAAE,MAAM,WAAW,CAAC;AAEzD,OAAO,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AAC/C,YAAY,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AAE9C,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAChD,YAAY,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC"}
package/dist/esm/index.js CHANGED
@@ -1,14 +1,17 @@
1
1
  // koota-kit — thin koota ECS conventions layer.
2
2
  //
3
- // Four modules, also importable as subpaths:
3
+ // Five modules, also importable as subpaths:
4
4
  // ./world — WorldHandle lifecycle (world + dual-stream rng + clock + scratch)
5
- // ./rng — dual-layer deterministic seedrandom PRNG with byte-exact
6
- // snapshot/restore
5
+ // ./rng — dual-layer deterministic seedrandom PRNG and named substreams
6
+ // with byte-exact snapshot/restore
7
+ // ./seed — length-prefixed SHA-256 seed derivation and 128-bit master
8
+ // seeds (leaf module, zero runtime dependencies)
7
9
  // ./traits — defineTrait AoS-aliasing guard (leaf module, zero sibling
8
10
  // imports)
9
11
  // ./eventLog — scratch-backed publish/drain event logs with a peek seam for
10
12
  // observers (harnesses, tests, HUDs)
11
13
  export { advanceClock, createActions, createSimWorld, destroySimWorld, relation, restoreWorldHeader, snapshotWorld, trait, } from "./world.js";
12
- export { chance, createRng, nextFloat, nextInt, nextU32, restoreLayers, restoreStream, snapshotLayers, snapshotStream, } from "./rng.js";
14
+ export { chance, createRng, createSubstreams, getSubstream, nextFloat, nextInt, nextU32, restoreLayers, restoreStream, restoreSubstreams, snapshotLayers, snapshotStream, snapshotSubstreams, substream, } from "./rng.js";
15
+ export { createMasterSeed, deriveSeed, isMasterSeed, MASTER_SEED_BYTES } from "./seed.js";
13
16
  export { defineEventLog } from "./eventLog.js";
14
17
  export { defineTrait } from "./traits/index.js";
package/dist/esm/rng.d.ts CHANGED
@@ -82,5 +82,39 @@ export declare function restoreStream(snap: RngStreamSnapshot): RngStream;
82
82
  * `restoreStream` — applied independently to `gen` and `events`.
83
83
  */
84
84
  export declare function restoreLayers(snap: RngLayersSnapshot): RngLayers;
85
+ export type Substreams = {
86
+ /** The seed every member stream is derived from. */
87
+ readonly seed: string | number;
88
+ /** Streams created so far, keyed by name. Create members with `getSubstream`. */
89
+ readonly streams: ReadonlyMap<string, RngStream>;
90
+ };
91
+ export type SubstreamsSnapshot = {
92
+ seed: string | number;
93
+ streams: Record<string, RngStreamSnapshot>;
94
+ };
95
+ /**
96
+ * Create the stream named `name` under `seed`: a fresh stream seeded by
97
+ * `deriveSeed(seed, name)`. Creating or consuming any other substream never
98
+ * shifts this one.
99
+ */
100
+ export declare function substream(seed: string | number, name: string): RngStream;
101
+ /** Create an empty, lazily populated set of named substreams under `seed`. */
102
+ export declare function createSubstreams(seed: string | number): Substreams;
103
+ /**
104
+ * Return the set's stream for `name`, creating it from the set's seed on first
105
+ * use. The same name always returns the same live stream instance.
106
+ */
107
+ export declare function getSubstream(substreams: Substreams, name: string): RngStream;
108
+ /**
109
+ * Capture the set's seed plus every stream created so far, with names in
110
+ * sorted order. JSON-serializable and detached from later draws.
111
+ */
112
+ export declare function snapshotSubstreams(substreams: Substreams): SubstreamsSnapshot;
113
+ /**
114
+ * Rebuild a substream set from `snapshotSubstreams` output. Validates the
115
+ * complete snapshot before building anything; streams that were never created
116
+ * restore lazily from the seed on first `getSubstream`.
117
+ */
118
+ export declare function restoreSubstreams(snapshot: SubstreamsSnapshot): Substreams;
85
119
  export {};
86
120
  //# sourceMappingURL=rng.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"rng.d.ts","sourceRoot":"","sources":["../../src/rng.ts"],"names":[],"mappings":"AAqBA,OAAO,UAAU,MAAM,YAAY,CAAC;AAEpC,KAAK,KAAK,GAAG,UAAU,CAAC,KAAK,CAAC,IAAI,CAAC;AACnC,KAAK,QAAQ,GAAG,UAAU,CAAC,YAAY,CAAC,KAAK,CAAC,CAAC;AAE/C,MAAM,MAAM,SAAS,GAAG;IACtB,IAAI,EAAE,QAAQ,CAAC;CAChB,CAAC;AAEF,MAAM,MAAM,SAAS,GAAG;IACtB,GAAG,EAAE,SAAS,CAAC;IACf,MAAM,EAAE,SAAS,CAAC;CACnB,CAAC;AAEF,MAAM,MAAM,QAAQ,GAAG;IACrB,uEAAuE;IACvE,QAAQ,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAAC;IAC9B,8FAA8F;IAC9F,QAAQ,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAAC;CAClC,CAAC;AAgBF;;;;;GAKG;AACH,wBAAgB,SAAS,CAAC,KAAK,EAAE,QAAQ,GAAG,SAAS,CAUpD;AAED;;;;;;GAMG;AACH,wBAAgB,OAAO,CAAC,MAAM,EAAE,SAAS,GAAG,MAAM,CAEjD;AAED;;;;GAIG;AACH,wBAAgB,SAAS,CAAC,MAAM,EAAE,SAAS,GAAG,MAAM,CAEnD;AAED;;;;;GAKG;AACH,wBAAgB,OAAO,CAAC,MAAM,EAAE,SAAS,EAAE,YAAY,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,GAAG,MAAM,CAc7F;AAED;;;;;;GAMG;AACH,wBAAgB,MAAM,CAAC,MAAM,EAAE,SAAS,EAAE,CAAC,EAAE,MAAM,GAAG,OAAO,CAK5D;AAED,MAAM,MAAM,iBAAiB,GAAG;IAAE,KAAK,EAAE,KAAK,CAAA;CAAE,CAAC;AACjD,MAAM,MAAM,iBAAiB,GAAG;IAAE,GAAG,EAAE,iBAAiB,CAAC;IAAC,MAAM,EAAE,iBAAiB,CAAA;CAAE,CAAC;AAEtF;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,MAAM,EAAE,SAAS,GAAG,iBAAiB,CAGnE;AAED;;;GAGG;AACH,wBAAgB,cAAc,CAAC,GAAG,EAAE,SAAS,GAAG,iBAAiB,CAEhE;AAED;;;;;;GAMG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,iBAAiB,GAAG,SAAS,CAqBhE;AAED;;;GAGG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,iBAAiB,GAAG,SAAS,CAKhE"}
1
+ {"version":3,"file":"rng.d.ts","sourceRoot":"","sources":["../../src/rng.ts"],"names":[],"mappings":"AAqBA,OAAO,UAAU,MAAM,YAAY,CAAC;AAGpC,KAAK,KAAK,GAAG,UAAU,CAAC,KAAK,CAAC,IAAI,CAAC;AACnC,KAAK,QAAQ,GAAG,UAAU,CAAC,YAAY,CAAC,KAAK,CAAC,CAAC;AAE/C,MAAM,MAAM,SAAS,GAAG;IACtB,IAAI,EAAE,QAAQ,CAAC;CAChB,CAAC;AAEF,MAAM,MAAM,SAAS,GAAG;IACtB,GAAG,EAAE,SAAS,CAAC;IACf,MAAM,EAAE,SAAS,CAAC;CACnB,CAAC;AAEF,MAAM,MAAM,QAAQ,GAAG;IACrB,uEAAuE;IACvE,QAAQ,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAAC;IAC9B,8FAA8F;IAC9F,QAAQ,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAAC;CAClC,CAAC;AAgBF;;;;;GAKG;AACH,wBAAgB,SAAS,CAAC,KAAK,EAAE,QAAQ,GAAG,SAAS,CAUpD;AAED;;;;;;GAMG;AACH,wBAAgB,OAAO,CAAC,MAAM,EAAE,SAAS,GAAG,MAAM,CAEjD;AAED;;;;GAIG;AACH,wBAAgB,SAAS,CAAC,MAAM,EAAE,SAAS,GAAG,MAAM,CAEnD;AAED;;;;;GAKG;AACH,wBAAgB,OAAO,CAAC,MAAM,EAAE,SAAS,EAAE,YAAY,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,GAAG,MAAM,CAc7F;AAED;;;;;;GAMG;AACH,wBAAgB,MAAM,CAAC,MAAM,EAAE,SAAS,EAAE,CAAC,EAAE,MAAM,GAAG,OAAO,CAK5D;AAED,MAAM,MAAM,iBAAiB,GAAG;IAAE,KAAK,EAAE,KAAK,CAAA;CAAE,CAAC;AACjD,MAAM,MAAM,iBAAiB,GAAG;IAAE,GAAG,EAAE,iBAAiB,CAAC;IAAC,MAAM,EAAE,iBAAiB,CAAA;CAAE,CAAC;AAEtF;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,MAAM,EAAE,SAAS,GAAG,iBAAiB,CAGnE;AAED;;;GAGG;AACH,wBAAgB,cAAc,CAAC,GAAG,EAAE,SAAS,GAAG,iBAAiB,CAEhE;AAED;;;;;;GAMG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,iBAAiB,GAAG,SAAS,CAqBhE;AAED;;;GAGG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,iBAAiB,GAAG,SAAS,CAKhE;AAUD,MAAM,MAAM,UAAU,GAAG;IACvB,oDAAoD;IACpD,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAAC;IAC/B,iFAAiF;IACjF,QAAQ,CAAC,OAAO,EAAE,WAAW,CAAC,MAAM,EAAE,SAAS,CAAC,CAAC;CAClD,CAAC;AAEF,MAAM,MAAM,kBAAkB,GAAG;IAC/B,IAAI,EAAE,MAAM,GAAG,MAAM,CAAC;IACtB,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,iBAAiB,CAAC,CAAC;CAC5C,CAAC;AAcF;;;;GAIG;AACH,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,SAAS,CAGxE;AAED,8EAA8E;AAC9E,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,UAAU,CAGlE;AAED;;;GAGG;AACH,wBAAgB,YAAY,CAAC,UAAU,EAAE,UAAU,EAAE,IAAI,EAAE,MAAM,GAAG,SAAS,CAO5E;AAED;;;GAGG;AACH,wBAAgB,kBAAkB,CAAC,UAAU,EAAE,UAAU,GAAG,kBAAkB,CAW7E;AAED;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,kBAAkB,GAAG,UAAU,CAiB1E"}
package/dist/esm/rng.js CHANGED
@@ -19,6 +19,7 @@
19
19
  // Forbidden in sim code: `Math.random()`. Use `nextFloat(rng.gen)` or
20
20
  // `nextFloat(rng.events)` depending on the layer.
21
21
  import seedrandom from "seedrandom";
22
+ import { deriveSeed } from "./seed.js";
22
23
  function assertSeed(seed, name) {
23
24
  if (typeof seed !== "string" && typeof seed !== "number") {
24
25
  throw new TypeError(`createRng: seeds.${name} must be a string or finite number.`);
@@ -152,3 +153,77 @@ export function restoreLayers(snap) {
152
153
  }
153
154
  return { gen: restoreStream(snap.gen), events: restoreStream(snap.events) };
154
155
  }
156
+ function assertSubstreamName(name, api) {
157
+ if (typeof name !== "string" || name.length === 0) {
158
+ throw new TypeError(`${api}: name must be a non-empty string.`);
159
+ }
160
+ }
161
+ function assertSubstreamSeed(seed, api) {
162
+ if (typeof seed !== "string" && !(typeof seed === "number" && Number.isFinite(seed))) {
163
+ throw new TypeError(`${api}: seed must be a string or finite number.`);
164
+ }
165
+ }
166
+ /**
167
+ * Create the stream named `name` under `seed`: a fresh stream seeded by
168
+ * `deriveSeed(seed, name)`. Creating or consuming any other substream never
169
+ * shifts this one.
170
+ */
171
+ export function substream(seed, name) {
172
+ assertSubstreamName(name, "substream");
173
+ return makeStream(deriveSeed(seed, name));
174
+ }
175
+ /** Create an empty, lazily populated set of named substreams under `seed`. */
176
+ export function createSubstreams(seed) {
177
+ assertSubstreamSeed(seed, "createSubstreams");
178
+ return { seed, streams: new Map() };
179
+ }
180
+ /**
181
+ * Return the set's stream for `name`, creating it from the set's seed on first
182
+ * use. The same name always returns the same live stream instance.
183
+ */
184
+ export function getSubstream(substreams, name) {
185
+ assertSubstreamName(name, "getSubstream");
186
+ const existing = substreams.streams.get(name);
187
+ if (existing)
188
+ return existing;
189
+ const stream = substream(substreams.seed, name);
190
+ substreams.streams.set(name, stream);
191
+ return stream;
192
+ }
193
+ /**
194
+ * Capture the set's seed plus every stream created so far, with names in
195
+ * sorted order. JSON-serializable and detached from later draws.
196
+ */
197
+ export function snapshotSubstreams(substreams) {
198
+ // Object.fromEntries defines own data properties, so a stream named
199
+ // "__proto__" is captured instead of hitting the prototype setter.
200
+ const streams = Object.fromEntries([...substreams.streams.keys()]
201
+ // UTF-16 code-unit order, never localeCompare: the order must not vary
202
+ // by runtime locale. Branch-free because names in a Map are unique.
203
+ .sort((left, right) => Number(left > right) - Number(left < right))
204
+ .map((name) => [name, snapshotStream(substreams.streams.get(name))]));
205
+ return { seed: substreams.seed, streams };
206
+ }
207
+ /**
208
+ * Rebuild a substream set from `snapshotSubstreams` output. Validates the
209
+ * complete snapshot before building anything; streams that were never created
210
+ * restore lazily from the seed on first `getSubstream`.
211
+ */
212
+ export function restoreSubstreams(snapshot) {
213
+ if (typeof snapshot !== "object" || snapshot === null) {
214
+ throw new TypeError("restoreSubstreams: snapshot must contain a seed and streams.");
215
+ }
216
+ assertSubstreamSeed(snapshot.seed, "restoreSubstreams");
217
+ const entries = snapshot.streams;
218
+ if (typeof entries !== "object" || entries === null || Array.isArray(entries)) {
219
+ throw new TypeError("restoreSubstreams: streams must be an object keyed by stream name.");
220
+ }
221
+ const restored = new Map();
222
+ for (const [name, stream] of Object.entries(entries)) {
223
+ if (name.length === 0) {
224
+ throw new TypeError("restoreSubstreams: stream names must be non-empty strings.");
225
+ }
226
+ restored.set(name, restoreStream(stream));
227
+ }
228
+ return { seed: snapshot.seed, streams: restored };
229
+ }
@@ -0,0 +1,25 @@
1
+ /** A value that can seed a stream or be mixed into a derived seed. */
2
+ export type SeedPart = string | number;
3
+ /** The subset of Web Crypto that `createMasterSeed` needs. Inject one in tests. */
4
+ export type EntropySource = {
5
+ getRandomValues(array: Uint8Array<ArrayBuffer>): Uint8Array<ArrayBuffer>;
6
+ };
7
+ /** Byte length of a master seed (128 bits). */
8
+ export declare const MASTER_SEED_BYTES = 16;
9
+ /**
10
+ * Derive a child seed from `parent` and ordered `parts`. Returns the first 128
11
+ * bits of SHA-256 over a domain tag plus each value as a 4-byte big-endian
12
+ * UTF-8 length and its UTF-8 text, as 32 lowercase hex characters. Numbers
13
+ * canonicalize to their string form, matching `createRng`.
14
+ */
15
+ export declare function deriveSeed(parent: SeedPart, ...parts: SeedPart[]): string;
16
+ /**
17
+ * Create a 128-bit master seed from `entropy` (default `globalThis.crypto`)
18
+ * as 32 lowercase hex characters. Fails closed when no source exists, when it
19
+ * returns anything but the requested 16-byte buffer, or when the buffer is
20
+ * still all zero (an unfilled stub, not entropy).
21
+ */
22
+ export declare function createMasterSeed(entropy?: EntropySource): string;
23
+ /** True for exactly 32 lowercase hex characters: the shape of a master or derived seed. */
24
+ export declare function isMasterSeed(value: unknown): value is string;
25
+ //# sourceMappingURL=seed.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"seed.d.ts","sourceRoot":"","sources":["../../src/seed.ts"],"names":[],"mappings":"AASA,sEAAsE;AACtE,MAAM,MAAM,QAAQ,GAAG,MAAM,GAAG,MAAM,CAAC;AAEvC,mFAAmF;AAGnF,MAAM,MAAM,aAAa,GAAG;IAC1B,eAAe,CAAC,KAAK,EAAE,UAAU,CAAC,WAAW,CAAC,GAAG,UAAU,CAAC,WAAW,CAAC,CAAC;CAC1E,CAAC;AAEF,+CAA+C;AAC/C,eAAO,MAAM,iBAAiB,KAAK,CAAC;AAoIpC;;;;;GAKG;AACH,wBAAgB,UAAU,CAAC,MAAM,EAAE,QAAQ,EAAE,GAAG,KAAK,EAAE,QAAQ,EAAE,GAAG,MAAM,CAmBzE;AAED;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,CAAC,EAAE,aAAa,GAAG,MAAM,CAgBhE;AAED,2FAA2F;AAC3F,wBAAgB,YAAY,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,MAAM,CAE5D"}
@@ -0,0 +1,182 @@
1
+ // Seed derivation and master seeds.
2
+ //
3
+ // A seeded game usually has a chain of seeds: a buried master seed for the
4
+ // profile, a run seed derived from it, a level seed derived from that, and
5
+ // one stream per generator. Building those keys by string concatenation is
6
+ // ambiguous ("1" + "23" === "12" + "3"), so `deriveSeed` hashes length-prefixed
7
+ // parts instead. Leaf module: no runtime dependency, so it is identical in
8
+ // browsers and Node and in both the ESM and CommonJS builds.
9
+ /** Byte length of a master seed (128 bits). */
10
+ export const MASTER_SEED_BYTES = 16;
11
+ // Changing the domain tag or the encoding changes every derived seed in every
12
+ // consumer's saves. Treat both as a frozen public contract.
13
+ const DERIVE_DOMAIN = "koota-kit:deriveSeed:v1";
14
+ const HEX_SEED = /^[0-9a-f]{32}$/;
15
+ function canonicalPart(value, label, api) {
16
+ if (typeof value === "string")
17
+ return value;
18
+ if (typeof value === "number" && Number.isFinite(value))
19
+ return String(value);
20
+ throw new TypeError(`${api}: ${label} must be a string or finite number.`);
21
+ }
22
+ /** UTF-8 encode, mapping lone surrogates to U+FFFD exactly like TextEncoder. */
23
+ function utf8(text) {
24
+ const bytes = [];
25
+ // String iteration yields whole code points; an unpaired surrogate arrives
26
+ // alone as its own code unit, so it is the only surrogate value seen here.
27
+ for (const char of text) {
28
+ let code = char.codePointAt(0);
29
+ if (code >= 0xd800 && code <= 0xdfff)
30
+ code = 0xfffd;
31
+ if (code < 0x80) {
32
+ bytes.push(code);
33
+ }
34
+ else if (code < 0x800) {
35
+ bytes.push(0xc0 | (code >> 6), 0x80 | (code & 0x3f));
36
+ }
37
+ else if (code < 0x10000) {
38
+ bytes.push(0xe0 | (code >> 12), 0x80 | ((code >> 6) & 0x3f), 0x80 | (code & 0x3f));
39
+ }
40
+ else {
41
+ bytes.push(0xf0 | (code >> 18), 0x80 | ((code >> 12) & 0x3f), 0x80 | ((code >> 6) & 0x3f), 0x80 | (code & 0x3f));
42
+ }
43
+ }
44
+ return bytes;
45
+ }
46
+ const K = [
47
+ 0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5,
48
+ 0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174,
49
+ 0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da,
50
+ 0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967,
51
+ 0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85,
52
+ 0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070,
53
+ 0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3,
54
+ 0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2,
55
+ ];
56
+ const rotr = (value, bits) => (value >>> bits) | (value << (32 - bits));
57
+ /** FIPS 180-4 SHA-256 of `message`, as eight big-endian 32-bit words. */
58
+ function sha256(message) {
59
+ const bitLength = message.length * 8;
60
+ const zeroPadding = (55 - (message.length % 64) + 64) % 64;
61
+ // Seed inputs are far below 2**32 bits, so the high length word is zero.
62
+ const padded = [
63
+ ...message,
64
+ 0x80,
65
+ ...new Array(zeroPadding).fill(0),
66
+ 0,
67
+ 0,
68
+ 0,
69
+ 0,
70
+ bitLength >>> 24,
71
+ (bitLength >>> 16) & 0xff,
72
+ (bitLength >>> 8) & 0xff,
73
+ bitLength & 0xff,
74
+ ];
75
+ const hash = new Uint32Array([
76
+ 0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a, 0x510e527f, 0x9b05688c, 0x1f83d9ab, 0x5be0cd19,
77
+ ]);
78
+ const w = new Uint32Array(64);
79
+ for (let offset = 0; offset < padded.length; offset += 64) {
80
+ for (let t = 0; t < 16; t += 1) {
81
+ const i = offset + t * 4;
82
+ w[t] =
83
+ (padded[i] << 24) |
84
+ (padded[i + 1] << 16) |
85
+ (padded[i + 2] << 8) |
86
+ padded[i + 3];
87
+ }
88
+ for (let t = 16; t < 64; t += 1) {
89
+ const w15 = w[t - 15];
90
+ const w2 = w[t - 2];
91
+ const s0 = rotr(w15, 7) ^ rotr(w15, 18) ^ (w15 >>> 3);
92
+ const s1 = rotr(w2, 17) ^ rotr(w2, 19) ^ (w2 >>> 10);
93
+ w[t] = (w[t - 16] + s0 + w[t - 7] + s1) >>> 0;
94
+ }
95
+ let a = hash[0];
96
+ let b = hash[1];
97
+ let c = hash[2];
98
+ let d = hash[3];
99
+ let e = hash[4];
100
+ let f = hash[5];
101
+ let g = hash[6];
102
+ let h = hash[7];
103
+ for (let t = 0; t < 64; t += 1) {
104
+ const t1 = (h +
105
+ (rotr(e, 6) ^ rotr(e, 11) ^ rotr(e, 25)) +
106
+ ((e & f) ^ (~e & g)) +
107
+ K[t] +
108
+ w[t]) >>>
109
+ 0;
110
+ const t2 = ((rotr(a, 2) ^ rotr(a, 13) ^ rotr(a, 22)) + ((a & b) ^ (a & c) ^ (b & c))) >>> 0;
111
+ h = g;
112
+ g = f;
113
+ f = e;
114
+ e = (d + t1) >>> 0;
115
+ d = c;
116
+ c = b;
117
+ b = a;
118
+ a = (t1 + t2) >>> 0;
119
+ }
120
+ hash[0] = (hash[0] + a) >>> 0;
121
+ hash[1] = (hash[1] + b) >>> 0;
122
+ hash[2] = (hash[2] + c) >>> 0;
123
+ hash[3] = (hash[3] + d) >>> 0;
124
+ hash[4] = (hash[4] + e) >>> 0;
125
+ hash[5] = (hash[5] + f) >>> 0;
126
+ hash[6] = (hash[6] + g) >>> 0;
127
+ hash[7] = (hash[7] + h) >>> 0;
128
+ }
129
+ return hash;
130
+ }
131
+ const toHex = (bytes) => Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("");
132
+ /**
133
+ * Derive a child seed from `parent` and ordered `parts`. Returns the first 128
134
+ * bits of SHA-256 over a domain tag plus each value as a 4-byte big-endian
135
+ * UTF-8 length and its UTF-8 text, as 32 lowercase hex characters. Numbers
136
+ * canonicalize to their string form, matching `createRng`.
137
+ */
138
+ export function deriveSeed(parent, ...parts) {
139
+ const values = [canonicalPart(parent, "parent", "deriveSeed")];
140
+ parts.forEach((part, index) => {
141
+ values.push(canonicalPart(part, `parts[${index}]`, "deriveSeed"));
142
+ });
143
+ const chunks = [utf8(DERIVE_DOMAIN)];
144
+ for (const value of values) {
145
+ const encoded = utf8(value);
146
+ const length = encoded.length;
147
+ const prefix = [
148
+ (length >>> 24) & 0xff,
149
+ (length >>> 16) & 0xff,
150
+ (length >>> 8) & 0xff,
151
+ length & 0xff,
152
+ ];
153
+ chunks.push(prefix, encoded);
154
+ }
155
+ const digest = sha256(chunks.flat());
156
+ return Array.from(digest.subarray(0, 4), (word) => word.toString(16).padStart(8, "0")).join("");
157
+ }
158
+ /**
159
+ * Create a 128-bit master seed from `entropy` (default `globalThis.crypto`)
160
+ * as 32 lowercase hex characters. Fails closed when no source exists, when it
161
+ * returns anything but the requested 16-byte buffer, or when the buffer is
162
+ * still all zero (an unfilled stub, not entropy).
163
+ */
164
+ export function createMasterSeed(entropy) {
165
+ const source = entropy ?? globalThis.crypto;
166
+ if (!source || typeof source.getRandomValues !== "function") {
167
+ throw new TypeError("createMasterSeed: no entropy source with getRandomValues is available.");
168
+ }
169
+ const requested = new Uint8Array(MASTER_SEED_BYTES);
170
+ const bytes = source.getRandomValues(requested);
171
+ if (bytes !== requested) {
172
+ throw new TypeError("createMasterSeed: the entropy source must fill the requested 16-byte array.");
173
+ }
174
+ if (bytes.every((byte) => byte === 0)) {
175
+ throw new RangeError("createMasterSeed: the entropy source returned all zero bytes.");
176
+ }
177
+ return toHex(bytes);
178
+ }
179
+ /** True for exactly 32 lowercase hex characters: the shape of a master or derived seed. */
180
+ export function isMasterSeed(value) {
181
+ return typeof value === "string" && HEX_SEED.test(value);
182
+ }
package/docs/API.md CHANGED
@@ -43,9 +43,11 @@ function restoreWorldHeader(handle: WorldHandle, snapshot: WorldSnapshot): void;
43
43
  ```
44
44
 
45
45
  Snapshots and restores both RNG streams plus the simulation clock. The plain
46
- object can be round-tripped through JSON. Entity and trait data are outside
47
- this package's persistence boundary. Restore validates the complete replacement
48
- before mutating the handle.
46
+ object can be round-tripped through JSON. It does not include `WorldHandle.seeds`:
47
+ persist the immutable seeds separately and use them when creating a fresh
48
+ handle for loading. Entity and trait data are also outside this package's
49
+ persistence boundary. Restore validates the complete replacement before
50
+ mutating the handle.
49
51
 
50
52
  ### Koota exports
51
53
 
@@ -80,6 +82,72 @@ function restoreLayers(snapshot: RngLayersSnapshot): RngLayers;
80
82
  - Restore functions reject malformed ARC4 state and clone accepted state so a
81
83
  snapshot can seed multiple independent replays.
82
84
 
85
+ ### Named substreams
86
+
87
+ ```ts
88
+ type Substreams = {
89
+ readonly seed: string | number;
90
+ readonly streams: ReadonlyMap<string, RngStream>;
91
+ };
92
+ type SubstreamsSnapshot = {
93
+ seed: string | number;
94
+ streams: Record<string, RngStreamSnapshot>;
95
+ };
96
+
97
+ function substream(seed: string | number, name: string): RngStream;
98
+ function createSubstreams(seed: string | number): Substreams;
99
+ function getSubstream(substreams: Substreams, name: string): RngStream;
100
+ function snapshotSubstreams(substreams: Substreams): SubstreamsSnapshot;
101
+ function restoreSubstreams(snapshot: SubstreamsSnapshot): Substreams;
102
+ ```
103
+
104
+ - `substream(seed, name)` returns a fresh stream seeded by
105
+ `deriveSeed(seed, name)`. Its sequence depends only on `seed` and `name`, so
106
+ adding, removing, reordering, or consuming another substream never shifts it.
107
+ - `name` must be a non-empty string. Invalid input throws before anything is
108
+ allocated.
109
+ - `createSubstreams` holds one lazily created stream per name, so generators
110
+ that share a set keep drawing where they left off. `getSubstream` returns the
111
+ same stream instance for the same name.
112
+ - `snapshotSubstreams` captures the seed plus every stream created so far, with
113
+ names in sorted order. `restoreSubstreams` validates the whole snapshot
114
+ before building anything and reproduces every later draw byte-exact. Streams
115
+ that were never created restore lazily from the seed.
116
+
117
+ ## `seed`
118
+
119
+ ```ts
120
+ type SeedPart = string | number;
121
+ type EntropySource = {
122
+ getRandomValues(array: Uint8Array<ArrayBuffer>): Uint8Array<ArrayBuffer>;
123
+ }; // `globalThis.crypto` satisfies it, with or without DOM typings
124
+
125
+ const MASTER_SEED_BYTES = 16;
126
+
127
+ function deriveSeed(parent: SeedPart, ...parts: SeedPart[]): string;
128
+ function createMasterSeed(entropy?: EntropySource): string;
129
+ function isMasterSeed(value: unknown): value is string;
130
+ ```
131
+
132
+ - `deriveSeed` hashes the parent seed and every part with SHA-256 and returns
133
+ the first 128 bits as 32 lowercase hex characters. The result is a valid
134
+ seed for `createRng`, `createSimWorld`, `substream`, and further
135
+ `deriveSeed` calls.
136
+ - Every value is length-prefixed before hashing, so `("a", "bc")` and
137
+ `("ab", "c")` derive different seeds. Order matters.
138
+ - Numbers canonicalize to their string form, matching `createRng`, so
139
+ `deriveSeed(7, "x") === deriveSeed("7", "x")`. Non-finite numbers and
140
+ non-string, non-number values throw.
141
+ - The output is stable across platforms and package versions. A change to the
142
+ derivation would be a breaking change.
143
+ - `createMasterSeed` fills 16 bytes from `entropy` (default
144
+ `globalThis.crypto`) and returns them as 32 lowercase hex characters. It
145
+ throws when no entropy source exists, when the source returns something
146
+ other than the requested 16-byte array, or when it returns all zero bytes,
147
+ which signals an unfilled buffer rather than real entropy.
148
+ - `isMasterSeed` accepts exactly 32 lowercase hex characters. Use it to
149
+ validate a persisted seed before deriving from it.
150
+
83
151
  ## `traits`
84
152
 
85
153
  ```ts
@@ -8,7 +8,8 @@ queries, and actions directly.
8
8
 
9
9
  ```text
10
10
  traits ───────────────> koota
11
- rng ──────────────────> seedrandom
11
+ seed ─────────────────> (leaf: built-in SHA-256)
12
+ rng ──────────────────> seedrandom + seed
12
13
  world ────────────────> koota + rng
13
14
  eventLog ─────────────> WorldHandle scratch
14
15
  index ────────────────> public re-exports only
@@ -19,7 +20,15 @@ keeps trait declarations out of world/scenario evaluation cycles and preserves
19
20
  the reference identity Koota uses to identify traits.
20
21
 
21
22
  `rng` has no Koota dependency. Its serialized contract is the seedrandom ARC4
22
- state plus a two-stream container.
23
+ state plus a two-stream container, or a seed plus named-substream container.
24
+
25
+ `seed` is a leaf module with no runtime dependency at all. `deriveSeed` uses a
26
+ small built-in synchronous SHA-256 so it behaves identically in browsers and
27
+ Node, in both the ESM and the CommonJS build (Web Crypto's digest is async, and
28
+ the common hash packages are ESM-only). The test suite checks it against
29
+ `node:crypto` and a frozen golden vector. Its byte encoding (a
30
+ domain tag, then each value as a 4-byte big-endian UTF-8 length plus its UTF-8
31
+ text) is a stable contract: changing it would change every derived world.
23
32
 
24
33
  `world` owns allocation, teardown, time, and facade-owned persistence. It does
25
34
  not own application entity serialization.
@@ -36,6 +45,8 @@ lifetime. It does not use a process-global queue.
36
45
  5. Object-valued structure-of-arrays fields use factories.
37
46
  6. Event logs have one consuming owner; observers use `peek`.
38
47
  7. World teardown clears facade-owned scratch state and releases the Koota ID.
48
+ 8. A named substream's sequence depends only on its seed and name; creating or
49
+ consuming another substream never shifts it.
39
50
 
40
51
  ## Performance choices
41
52
 
@@ -52,6 +63,7 @@ lifetime. It does not use a process-global queue.
52
63
  - No entity/trait serializer: applications need versioned domain schemas.
53
64
  - No scheduler or system runner: Koota's queries/actions remain the execution
54
65
  model.
55
- - No automatic derived-key hashing: the application owns stable domain keys.
66
+ - No implicit derived keys: `deriveSeed` hashes only the parts the application
67
+ passes, so the application still owns stable domain keys.
56
68
  - No multi-consumer queue semantics: publish separate logs when two systems
57
69
  must independently consume the same fact.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "koota-kit",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "Deterministic Koota simulation conventions: world lifecycle, dual RNG streams, safe object traits, and world-scoped event logs.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -43,6 +43,16 @@
43
43
  "default": "./dist/cjs/rng.cjs"
44
44
  }
45
45
  },
46
+ "./seed": {
47
+ "import": {
48
+ "types": "./dist/esm/seed.d.ts",
49
+ "default": "./dist/esm/seed.js"
50
+ },
51
+ "require": {
52
+ "types": "./dist/cjs/seed.d.cts",
53
+ "default": "./dist/cjs/seed.cjs"
54
+ }
55
+ },
46
56
  "./traits": {
47
57
  "import": {
48
58
  "types": "./dist/esm/traits/index.d.ts",
@@ -147,6 +157,9 @@
147
157
  "rng": [
148
158
  "./dist/esm/rng.d.ts"
149
159
  ],
160
+ "seed": [
161
+ "./dist/esm/seed.d.ts"
162
+ ],
150
163
  "traits": [
151
164
  "./dist/esm/traits/index.d.ts"
152
165
  ],
package/docs/changelog.md DELETED
@@ -1,13 +0,0 @@
1
- ---
2
- title: Changelog
3
- description: Release history maintained by Release Please from Conventional Commits.
4
- ---
5
-
6
- Release Please maintains the canonical
7
- [CHANGELOG.md](https://github.com/jbcom/koota-kit/blob/main/CHANGELOG.md),
8
- version metadata, tags, and GitHub Releases. Published releases use npm
9
- provenance and the project supports the latest `0.x` line.
10
-
11
- For planned changes, use a Conventional Commit type such as `fix:`, `feat:`,
12
- `docs:`, `refactor:`, `test:`, or `chore:`. Breaking public API changes need a
13
- `!` or `BREAKING CHANGE:` footer.