diff --git a/.github/workflows/ts-core-tests.yml b/.github/workflows/ts-core-tests.yml new file mode 100644 index 0000000..d417977 --- /dev/null +++ b/.github/workflows/ts-core-tests.yml @@ -0,0 +1,48 @@ +name: ts_core tests + +on: + push: + branches: [main] + paths: ['ts_core/**'] + pull_request: + branches: [main] + paths: ['ts_core/**'] + +jobs: + test: + runs-on: ubuntu-latest + defaults: + run: + working-directory: ts_core + + steps: + - uses: actions/checkout@v4 + + - uses: pnpm/action-setup@v4 + with: + version: 10 + + - uses: actions/setup-node@v4 + with: + node-version: '24' + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Lint + run: pnpm lint + + - name: Run tests with the 100% coverage gate + run: pnpm coverage + + # The tag ships a prebuilt dist/ because Vite does not transpile + # dependencies. A committed dist/ that does not match src/ would ship + # stale code to every consumer with no error anywhere, so rebuild and + # fail if the result differs from what is checked in. + - name: dist/ is in sync with src/ + run: | + pnpm build + if ! git diff --exit-code -- dist; then + echo "::error::dist/ is stale — run 'pnpm build' in ts_core and commit the result." + exit 1 + fi diff --git a/ts_core/.gitignore b/ts_core/.gitignore new file mode 100644 index 0000000..668a70b --- /dev/null +++ b/ts_core/.gitignore @@ -0,0 +1,9 @@ +# dist/ is deliberately NOT ignored here: consumers pull this package as a git +# dependency and Vite does not transpile dependencies, so the build must be +# committed in the tag. See README.md. The repo root ignores `dist/` globally, +# hence the negation below. +node_modules/ +coverage/ +*.tsbuildinfo +!dist/ +!dist/** diff --git a/ts_core/README.md b/ts_core/README.md new file mode 100644 index 0000000..6b76b82 --- /dev/null +++ b/ts_core/README.md @@ -0,0 +1,77 @@ +# `@kuhyx/ts-core` + +Framework-free TypeScript utilities shared across kuhy's games and web apps. + +Deliberately **no React and no CSS** — those live in +[`@kuhyx/web-ui`](../web_ui). A game that needs a seeded RNG should not have +to install a UI package to get one; that coupling is what makes people vendor +their own copy instead, which is how these ended up duplicated four ways. + +## Install + +**pnpm only.** npm cannot install a subdirectory of a git repo — it ignores +the `&path:` key, looks for `package.json` at the repo root and fails with +ENOENT. Every consumer of this package must be on pnpm. + +```jsonc +// package.json +"dependencies": { + "@kuhyx/ts-core": "github:kuhyx/utils#ts_core-v0.1.0&path:/ts_core" +} +``` + +## What it ships + +| Export | Notes | +| --- | --- | +| `Clock`, `createRealClock`, `realClock`, `createManualClock`, `ManualClock` | Injectable time source, so real-time mechanics are driven by a scripted clock in tests instead of racing the wall clock. | +| `Rng`, `createRng`, `nextFloat`, `nextInt`, `nextChance`, `pick` | Seeded mulberry32, free-function style over mutable state. | +| `SeededRng`, `createSeededRng` | The same core, closure style. | + +### Why the RNG has two interfaces + +The repos that had grown their own copy did not agree on shape: konbini-67 +used mutable state plus free functions (`nextFloat(rng)`); sims3-clone and +europe-county-map returned an object of closures (`rng.next()` / `rng.float()`). + +Both ship here rather than being reconciled into one, because **the sequence +is the contract**. These repos have seeded tests and "same seed → identical +world" guarantees, so changing how many times a helper advances the generator +silently re-rolls every saved world. A shared package that did that would be +worse than the duplication it replaced. + +The generator core is identical in both, so a given seed produces the same +float sequence whichever interface you use — asserted in the tests. + +`int` is inclusive of both bounds, matching sims3-clone and europe-county-map. +konbini-67's local helper took a single *exclusive* max; it maps to +`nextInt(rng, 0, max - 1)`. + +### The golden-sequence test + +`rng.test.ts` pins the first five floats for seed `12345`, captured from the +**pre-extraction** konbini-67 implementation. If that test fails, the shared +core has stopped reproducing the sequence every consumer's saved seeds depend +on. Regenerating those constants to make a failure go away defeats the entire +point of the test — fix the code instead. + +### `hotline3d` is deliberately not a consumer + +`hotline3d/hotline-kernel/src/core/rng.ts` looks like the same generator but +uses `%` and `Math.trunc` where mulberry32 uses `>>>`/`>>`. Measured, it +produces a **different sequence** — so adopting this package would change its +generated output. Left alone on purpose. + +## Commands + +```bash +pnpm install +pnpm test # vitest +pnpm coverage # 100% branches/functions/lines/statements, enforced +pnpm lint # tsc --noEmit + eslint (type-aware, strict) +pnpm build # tsc -> dist/ (ESM + .d.ts), committed to the tag +``` + +`dist/` is committed, like `web_ui`'s: Vite does not transpile dependencies, +so a consumer would otherwise need per-repo bundler config. CI fails if the +committed `dist/` does not match a fresh build. diff --git a/ts_core/dist/clock.d.ts b/ts_core/dist/clock.d.ts new file mode 100644 index 0000000..e552bfc --- /dev/null +++ b/ts_core/dist/clock.d.ts @@ -0,0 +1,33 @@ +/** + * Injectable time source. + * + * All elapsed-time reads flow through this interface instead of calling + * `performance.now()`/`Date.now()` directly (banned by lint everywhere except + * this file), so real-time mechanics can be driven by a scripted clock in + * tests rather than racing the wall clock. + */ +export interface Clock { + /** Current time in milliseconds. Not comparable across different clocks. */ + now: () => number; +} +/** A {@link Clock} backed by the browser's monotonic performance timer. */ +export declare const createRealClock: () => Clock; +/** + * The one real clock instance UI code should pass around. + * + * A single shared instance (rather than calling {@link createRealClock} per + * component) keeps its identity stable across renders, which matters for + * hooks that take it as an effect dependency -- a fresh object each render + * re-fires the effect every time. + */ +export declare const realClock: Clock; +/** A {@link Clock} test double whose time only moves when told to. */ +export interface ManualClock extends Clock { + /** Moves the clock forward by the given number of milliseconds. */ + advance: (deltaMs: number) => void; + /** Jumps the clock to an absolute time in milliseconds. */ + set: (timeMs: number) => void; +} +/** Creates a {@link ManualClock} starting at `startMs` (default 0). */ +export declare const createManualClock: (startMs?: number) => ManualClock; +//# sourceMappingURL=clock.d.ts.map \ No newline at end of file diff --git a/ts_core/dist/clock.d.ts.map b/ts_core/dist/clock.d.ts.map new file mode 100644 index 0000000..839dbc4 --- /dev/null +++ b/ts_core/dist/clock.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"clock.d.ts","sourceRoot":"","sources":["../src/clock.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,MAAM,WAAW,KAAK;IACpB,4EAA4E;IAC5E,GAAG,EAAE,MAAM,MAAM,CAAA;CAClB;AAED,2EAA2E;AAC3E,eAAO,MAAM,eAAe,QAAO,KAEjC,CAAA;AAEF;;;;;;;GAOG;AACH,eAAO,MAAM,SAAS,EAAE,KAAyB,CAAA;AAEjD,sEAAsE;AACtE,MAAM,WAAW,WAAY,SAAQ,KAAK;IACxC,mEAAmE;IACnE,OAAO,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAA;IAElC,2DAA2D;IAC3D,GAAG,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,IAAI,CAAA;CAC9B;AAED,uEAAuE;AACvE,eAAO,MAAM,iBAAiB,GAAI,gBAAW,KAAG,WAW/C,CAAA"} \ No newline at end of file diff --git a/ts_core/dist/clock.js b/ts_core/dist/clock.js new file mode 100644 index 0000000..954a4ec --- /dev/null +++ b/ts_core/dist/clock.js @@ -0,0 +1,27 @@ +/** A {@link Clock} backed by the browser's monotonic performance timer. */ +export const createRealClock = () => ({ + now: () => performance.now(), +}); +/** + * The one real clock instance UI code should pass around. + * + * A single shared instance (rather than calling {@link createRealClock} per + * component) keeps its identity stable across renders, which matters for + * hooks that take it as an effect dependency -- a fresh object each render + * re-fires the effect every time. + */ +export const realClock = createRealClock(); +/** Creates a {@link ManualClock} starting at `startMs` (default 0). */ +export const createManualClock = (startMs = 0) => { + let current = startMs; + return { + now: () => current, + advance: (deltaMs) => { + current += deltaMs; + }, + set: (timeMs) => { + current = timeMs; + }, + }; +}; +//# sourceMappingURL=clock.js.map \ No newline at end of file diff --git a/ts_core/dist/clock.js.map b/ts_core/dist/clock.js.map new file mode 100644 index 0000000..ba08390 --- /dev/null +++ b/ts_core/dist/clock.js.map @@ -0,0 +1 @@ +{"version":3,"file":"clock.js","sourceRoot":"","sources":["../src/clock.ts"],"names":[],"mappings":"AAaA,2EAA2E;AAC3E,MAAM,CAAC,MAAM,eAAe,GAAG,GAAU,EAAE,CAAC,CAAC;IAC3C,GAAG,EAAE,GAAW,EAAE,CAAC,WAAW,CAAC,GAAG,EAAE;CACrC,CAAC,CAAA;AAEF;;;;;;;GAOG;AACH,MAAM,CAAC,MAAM,SAAS,GAAU,eAAe,EAAE,CAAA;AAWjD,uEAAuE;AACvE,MAAM,CAAC,MAAM,iBAAiB,GAAG,CAAC,OAAO,GAAG,CAAC,EAAe,EAAE;IAC5D,IAAI,OAAO,GAAG,OAAO,CAAA;IACrB,OAAO;QACL,GAAG,EAAE,GAAW,EAAE,CAAC,OAAO;QAC1B,OAAO,EAAE,CAAC,OAAe,EAAQ,EAAE;YACjC,OAAO,IAAI,OAAO,CAAA;QACpB,CAAC;QACD,GAAG,EAAE,CAAC,MAAc,EAAQ,EAAE;YAC5B,OAAO,GAAG,MAAM,CAAA;QAClB,CAAC;KACF,CAAA;AACH,CAAC,CAAA"} \ No newline at end of file diff --git a/ts_core/dist/index.d.ts b/ts_core/dist/index.d.ts new file mode 100644 index 0000000..3b36514 --- /dev/null +++ b/ts_core/dist/index.d.ts @@ -0,0 +1,11 @@ +/** + * Framework-free TypeScript utilities shared across kuhy's games and web apps. + * + * Deliberately no React and no CSS: those live in `@kuhyx/web-ui`. A game + * that needs a seeded RNG should not have to install a UI package to get one + * -- that is the coupling that makes people vendor their own copy instead, + * which is how these ended up duplicated four ways to begin with. + */ +export { createManualClock, createRealClock, realClock, type Clock, type ManualClock, } from './clock.ts'; +export { createRng, createSeededRng, nextChance, nextFloat, nextInt, pick, type Rng, type SeededRng, } from './rng.ts'; +//# sourceMappingURL=index.d.ts.map \ No newline at end of file diff --git a/ts_core/dist/index.d.ts.map b/ts_core/dist/index.d.ts.map new file mode 100644 index 0000000..f06cc7b --- /dev/null +++ b/ts_core/dist/index.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,OAAO,EACL,iBAAiB,EACjB,eAAe,EACf,SAAS,EACT,KAAK,KAAK,EACV,KAAK,WAAW,GACjB,MAAM,YAAY,CAAA;AACnB,OAAO,EACL,SAAS,EACT,eAAe,EACf,UAAU,EACV,SAAS,EACT,OAAO,EACP,IAAI,EACJ,KAAK,GAAG,EACR,KAAK,SAAS,GACf,MAAM,UAAU,CAAA"} \ No newline at end of file diff --git a/ts_core/dist/index.js b/ts_core/dist/index.js new file mode 100644 index 0000000..6c31f43 --- /dev/null +++ b/ts_core/dist/index.js @@ -0,0 +1,11 @@ +/** + * Framework-free TypeScript utilities shared across kuhy's games and web apps. + * + * Deliberately no React and no CSS: those live in `@kuhyx/web-ui`. A game + * that needs a seeded RNG should not have to install a UI package to get one + * -- that is the coupling that makes people vendor their own copy instead, + * which is how these ended up duplicated four ways to begin with. + */ +export { createManualClock, createRealClock, realClock, } from "./clock.js"; +export { createRng, createSeededRng, nextChance, nextFloat, nextInt, pick, } from "./rng.js"; +//# sourceMappingURL=index.js.map \ No newline at end of file diff --git a/ts_core/dist/index.js.map b/ts_core/dist/index.js.map new file mode 100644 index 0000000..f8480a1 --- /dev/null +++ b/ts_core/dist/index.js.map @@ -0,0 +1 @@ +{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,OAAO,EACL,iBAAiB,EACjB,eAAe,EACf,SAAS,GAGV,MAAM,YAAY,CAAA;AACnB,OAAO,EACL,SAAS,EACT,eAAe,EACf,UAAU,EACV,SAAS,EACT,OAAO,EACP,IAAI,GAGL,MAAM,UAAU,CAAA"} \ No newline at end of file diff --git a/ts_core/dist/rng.d.ts b/ts_core/dist/rng.d.ts new file mode 100644 index 0000000..c9b1015 --- /dev/null +++ b/ts_core/dist/rng.d.ts @@ -0,0 +1,74 @@ +/** + * Seeded, deterministic PRNG (mulberry32). + * + * A given seed must always reproduce the exact same world, so consumers ban + * `Math.random` by lint outside this file. + * + * ## Why there are two interfaces + * + * The four repos that had grown their own copy did not agree on shape: + * konbini-67 used mutable state plus free functions (`nextFloat(rng)`), + * while sims3-clone and europe-county-map returned an object of closures + * (`rng.next()` / `rng.float()`). Both are exported here rather than + * reconciled into one, because the *sequence* is the contract: these repos + * have seeded tests and "same seed -> identical world" guarantees, and + * changing how many times a helper advances the generator silently changes + * every generated world. A shared package that quietly re-rolled saved + * worlds would be worse than the duplication it replaced. + * + * The generator core is identical in both, so a given seed produces the same + * float sequence whichever interface you use -- verified in the tests. + * + * Note `int` is inclusive of both bounds, matching sims3-clone and + * europe-county-map. konbini-67's local helper took a single *exclusive* + * max; it maps to `int(rng, 0, max - 1)`. + */ +/** Mutable state for a mulberry32 generator. */ +export interface Rng { + /** Internal 32-bit state. Advanced by every draw. */ + s: number; +} +/** Creates mutable generator state from a seed. */ +export declare const createRng: (seed: number) => Rng; +/** + * Deterministic float in [0, 1). Advances the generator. + * + * This is the mulberry32 core; every other helper here is defined in terms + * of it, so they all consume exactly one step per call. + */ +export declare const nextFloat: (rng: Rng) => number; +/** Deterministic integer in [minInclusive, maxInclusive]. One step. */ +export declare const nextInt: (rng: Rng, minInclusive: number, maxInclusive: number) => number; +/** True with the given probability in [0, 1]. One step. */ +export declare const nextChance: (rng: Rng, probability: number) => boolean; +/** + * Deterministic pick from a non-empty tuple. One step. + * + * Iterates rather than indexing: under `noUncheckedIndexedAccess` an index + * read is `T | undefined`, and the undefined arm is unreachable here, so it + * could never be covered by a test. + */ +export declare const pick: (rng: Rng, items: readonly [T, ...T[]]) => T; +/** + * The closure-style interface, for consumers that pass an `Rng` object + * around rather than threading mutable state through free functions. + */ +export interface SeededRng { + /** Uniform float in [0, 1). */ + float: () => number; + /** Uniform integer in [minInclusive, maxInclusive]. */ + int: (minInclusive: number, maxInclusive: number) => number; + /** Uniform element of a non-empty array. */ + pick: (items: readonly T[]) => T; + /** True with the given probability in [0, 1]. */ + chance: (probability: number) => boolean; +} +/** + * Creates a {@link SeededRng} over the same mulberry32 core. + * + * `pick` throws on an empty array rather than returning `undefined`: the + * callers all treat their input as non-empty, and a silent `undefined` + * propagates into generated data where it is far harder to trace. + */ +export declare const createSeededRng: (seed: number) => SeededRng; +//# sourceMappingURL=rng.d.ts.map \ No newline at end of file diff --git a/ts_core/dist/rng.d.ts.map b/ts_core/dist/rng.d.ts.map new file mode 100644 index 0000000..b0c2d38 --- /dev/null +++ b/ts_core/dist/rng.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"rng.d.ts","sourceRoot":"","sources":["../src/rng.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAEH,gDAAgD;AAChD,MAAM,WAAW,GAAG;IAClB,qDAAqD;IACrD,CAAC,EAAE,MAAM,CAAA;CACV;AAED,mDAAmD;AACnD,eAAO,MAAM,SAAS,GAAI,MAAM,MAAM,KAAG,GAA0B,CAAA;AAEnE;;;;;GAKG;AACH,eAAO,MAAM,SAAS,GAAI,KAAK,GAAG,KAAG,MAMpC,CAAA;AAED,uEAAuE;AACvE,eAAO,MAAM,OAAO,GAClB,KAAK,GAAG,EACR,cAAc,MAAM,EACpB,cAAc,MAAM,KACnB,MAC4E,CAAA;AAE/E,2DAA2D;AAC3D,eAAO,MAAM,UAAU,GAAI,KAAK,GAAG,EAAE,aAAa,MAAM,KAAG,OAC7B,CAAA;AAE9B;;;;;;GAMG;AACH,eAAO,MAAM,IAAI,GAAI,CAAC,EAAE,KAAK,GAAG,EAAE,OAAO,SAAS,CAAC,CAAC,EAAE,GAAG,CAAC,EAAE,CAAC,KAAG,CAW/D,CAAA;AAED;;;GAGG;AACH,MAAM,WAAW,SAAS;IACxB,+BAA+B;IAC/B,KAAK,EAAE,MAAM,MAAM,CAAA;IACnB,uDAAuD;IACvD,GAAG,EAAE,CAAC,YAAY,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,KAAK,MAAM,CAAA;IAC3D,4CAA4C;IAC5C,IAAI,EAAE,CAAC,CAAC,EAAE,KAAK,EAAE,SAAS,CAAC,EAAE,KAAK,CAAC,CAAA;IACnC,iDAAiD;IACjD,MAAM,EAAE,CAAC,WAAW,EAAE,MAAM,KAAK,OAAO,CAAA;CACzC;AAED;;;;;;GAMG;AACH,eAAO,MAAM,eAAe,GAAI,MAAM,MAAM,KAAG,SAgB9C,CAAA"} \ No newline at end of file diff --git a/ts_core/dist/rng.js b/ts_core/dist/rng.js new file mode 100644 index 0000000..6aa0d80 --- /dev/null +++ b/ts_core/dist/rng.js @@ -0,0 +1,87 @@ +/** + * Seeded, deterministic PRNG (mulberry32). + * + * A given seed must always reproduce the exact same world, so consumers ban + * `Math.random` by lint outside this file. + * + * ## Why there are two interfaces + * + * The four repos that had grown their own copy did not agree on shape: + * konbini-67 used mutable state plus free functions (`nextFloat(rng)`), + * while sims3-clone and europe-county-map returned an object of closures + * (`rng.next()` / `rng.float()`). Both are exported here rather than + * reconciled into one, because the *sequence* is the contract: these repos + * have seeded tests and "same seed -> identical world" guarantees, and + * changing how many times a helper advances the generator silently changes + * every generated world. A shared package that quietly re-rolled saved + * worlds would be worse than the duplication it replaced. + * + * The generator core is identical in both, so a given seed produces the same + * float sequence whichever interface you use -- verified in the tests. + * + * Note `int` is inclusive of both bounds, matching sims3-clone and + * europe-county-map. konbini-67's local helper took a single *exclusive* + * max; it maps to `int(rng, 0, max - 1)`. + */ +/** Creates mutable generator state from a seed. */ +export const createRng = (seed) => ({ s: seed >>> 0 }); +/** + * Deterministic float in [0, 1). Advances the generator. + * + * This is the mulberry32 core; every other helper here is defined in terms + * of it, so they all consume exactly one step per call. + */ +export const nextFloat = (rng) => { + rng.s = (rng.s + 0x6d_2b_79_f5) >>> 0; + let t = rng.s; + t = Math.imul(t ^ (t >>> 15), t | 1); + t ^= t + Math.imul(t ^ (t >>> 7), t | 61); + return ((t ^ (t >>> 14)) >>> 0) / 4_294_967_296; +}; +/** Deterministic integer in [minInclusive, maxInclusive]. One step. */ +export const nextInt = (rng, minInclusive, maxInclusive) => minInclusive + Math.floor(nextFloat(rng) * (maxInclusive - minInclusive + 1)); +/** True with the given probability in [0, 1]. One step. */ +export const nextChance = (rng, probability) => nextFloat(rng) < probability; +/** + * Deterministic pick from a non-empty tuple. One step. + * + * Iterates rather than indexing: under `noUncheckedIndexedAccess` an index + * read is `T | undefined`, and the undefined arm is unreachable here, so it + * could never be covered by a test. + */ +export const pick = (rng, items) => { + const target = nextInt(rng, 0, items.length - 1); + let out = items[0]; + let seen = 0; + for (const item of items) { + if (seen === target) { + out = item; + } + seen += 1; + } + return out; +}; +/** + * Creates a {@link SeededRng} over the same mulberry32 core. + * + * `pick` throws on an empty array rather than returning `undefined`: the + * callers all treat their input as non-empty, and a silent `undefined` + * propagates into generated data where it is far harder to trace. + */ +export const createSeededRng = (seed) => { + const state = createRng(seed); + const float = () => nextFloat(state); + return { + float, + int: (minInclusive, maxInclusive) => nextInt(state, minInclusive, maxInclusive), + pick: (items) => { + const chosen = items[Math.floor(float() * items.length)]; + if (chosen === undefined) { + throw new Error('pick() on an empty array'); + } + return chosen; + }, + chance: (probability) => float() < probability, + }; +}; +//# sourceMappingURL=rng.js.map \ No newline at end of file diff --git a/ts_core/dist/rng.js.map b/ts_core/dist/rng.js.map new file mode 100644 index 0000000..d176a26 --- /dev/null +++ b/ts_core/dist/rng.js.map @@ -0,0 +1 @@ +{"version":3,"file":"rng.js","sourceRoot":"","sources":["../src/rng.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAQH,mDAAmD;AACnD,MAAM,CAAC,MAAM,SAAS,GAAG,CAAC,IAAY,EAAO,EAAE,CAAC,CAAC,EAAE,CAAC,EAAE,IAAI,KAAK,CAAC,EAAE,CAAC,CAAA;AAEnE;;;;;GAKG;AACH,MAAM,CAAC,MAAM,SAAS,GAAG,CAAC,GAAQ,EAAU,EAAE;IAC5C,GAAG,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,GAAG,aAAa,CAAC,KAAK,CAAC,CAAA;IACrC,IAAI,CAAC,GAAG,GAAG,CAAC,CAAC,CAAA;IACb,CAAC,GAAG,IAAI,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,CAAC,EAAE,CAAC,GAAG,CAAC,CAAC,CAAA;IACpC,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,GAAG,EAAE,CAAC,CAAA;IACzC,OAAO,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,GAAG,aAAa,CAAA;AACjD,CAAC,CAAA;AAED,uEAAuE;AACvE,MAAM,CAAC,MAAM,OAAO,GAAG,CACrB,GAAQ,EACR,YAAoB,EACpB,YAAoB,EACZ,EAAE,CACV,YAAY,GAAG,IAAI,CAAC,KAAK,CAAC,SAAS,CAAC,GAAG,CAAC,GAAG,CAAC,YAAY,GAAG,YAAY,GAAG,CAAC,CAAC,CAAC,CAAA;AAE/E,2DAA2D;AAC3D,MAAM,CAAC,MAAM,UAAU,GAAG,CAAC,GAAQ,EAAE,WAAmB,EAAW,EAAE,CACnE,SAAS,CAAC,GAAG,CAAC,GAAG,WAAW,CAAA;AAE9B;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,IAAI,GAAG,CAAI,GAAQ,EAAE,KAA2B,EAAK,EAAE;IAClE,MAAM,MAAM,GAAG,OAAO,CAAC,GAAG,EAAE,CAAC,EAAE,KAAK,CAAC,MAAM,GAAG,CAAC,CAAC,CAAA;IAChD,IAAI,GAAG,GAAM,KAAK,CAAC,CAAC,CAAC,CAAA;IACrB,IAAI,IAAI,GAAG,CAAC,CAAA;IACZ,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACzB,IAAI,IAAI,KAAK,MAAM,EAAE,CAAC;YACpB,GAAG,GAAG,IAAI,CAAA;QACZ,CAAC;QACD,IAAI,IAAI,CAAC,CAAA;IACX,CAAC;IACD,OAAO,GAAG,CAAA;AACZ,CAAC,CAAA;AAiBD;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,eAAe,GAAG,CAAC,IAAY,EAAa,EAAE;IACzD,MAAM,KAAK,GAAG,SAAS,CAAC,IAAI,CAAC,CAAA;IAC7B,MAAM,KAAK,GAAG,GAAW,EAAE,CAAC,SAAS,CAAC,KAAK,CAAC,CAAA;IAC5C,OAAO;QACL,KAAK;QACL,GAAG,EAAE,CAAC,YAAoB,EAAE,YAAoB,EAAU,EAAE,CAC1D,OAAO,CAAC,KAAK,EAAE,YAAY,EAAE,YAAY,CAAC;QAC5C,IAAI,EAAE,CAAI,KAAmB,EAAK,EAAE;YAClC,MAAM,MAAM,GAAG,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,KAAK,EAAE,GAAG,KAAK,CAAC,MAAM,CAAC,CAAC,CAAA;YACxD,IAAI,MAAM,KAAK,SAAS,EAAE,CAAC;gBACzB,MAAM,IAAI,KAAK,CAAC,0BAA0B,CAAC,CAAA;YAC7C,CAAC;YACD,OAAO,MAAM,CAAA;QACf,CAAC;QACD,MAAM,EAAE,CAAC,WAAmB,EAAW,EAAE,CAAC,KAAK,EAAE,GAAG,WAAW;KAChE,CAAA;AACH,CAAC,CAAA"} \ No newline at end of file diff --git a/ts_core/eslint.config.js b/ts_core/eslint.config.js new file mode 100644 index 0000000..be5b2e0 --- /dev/null +++ b/ts_core/eslint.config.js @@ -0,0 +1,28 @@ +// Type-aware flat config, mirroring web_ui's so the two shared TS packages +// hold an identical bar. +import js from "@eslint/js"; +import tseslint from "typescript-eslint"; + +export default tseslint.config( + { ignores: ["dist", "coverage", "eslint.config.js"] }, + { linterOptions: { reportUnusedDisableDirectives: "error" } }, + + js.configs.recommended, + ...tseslint.configs.strictTypeChecked, + ...tseslint.configs.stylisticTypeChecked, + + { + files: ["**/*.ts"], + languageOptions: { + parserOptions: { + project: ["./tsconfig.lint.json"], + tsconfigRootDir: import.meta.dirname, + }, + }, + rules: { + // Every exported symbol crosses a package boundary, so an explicit + // return type is documentation, not ceremony. + "@typescript-eslint/explicit-function-return-type": "error", + }, + }, +); diff --git a/ts_core/package.json b/ts_core/package.json new file mode 100644 index 0000000..766c8c8 --- /dev/null +++ b/ts_core/package.json @@ -0,0 +1,39 @@ +{ + "name": "@kuhyx/ts-core", + "version": "0.1.0", + "private": false, + "type": "module", + "description": "Framework-free TypeScript utilities shared across kuhy's games and web apps: an injectable Clock and a seeded mulberry32 RNG. Deliberately no React and no CSS -- see @kuhyx/web-ui for the UI layer.", + "license": "MIT", + "engines": { + "node": ">=24", + "pnpm": ">=10" + }, + "packageManager": "pnpm@10.13.1", + "files": [ + "dist" + ], + "main": "./dist/index.js", + "types": "./dist/index.d.ts", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "default": "./dist/index.js" + } + }, + "scripts": { + "build": "tsc -b", + "lint": "tsc --noEmit -p tsconfig.lint.json && eslint .", + "test": "vitest run", + "coverage": "vitest run --coverage" + }, + "devDependencies": { + "@eslint/js": "^10.0.1", + "@types/node": "^26.1.1", + "@vitest/coverage-v8": "^4.1.10", + "eslint": "^10.7.0", + "typescript": "^6.0.3", + "typescript-eslint": "^8.64.0", + "vitest": "^4.1.10" + } +} diff --git a/ts_core/pnpm-lock.yaml b/ts_core/pnpm-lock.yaml new file mode 100644 index 0000000..b5d5591 --- /dev/null +++ b/ts_core/pnpm-lock.yaml @@ -0,0 +1,1603 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + devDependencies: + '@eslint/js': + specifier: ^10.0.1 + version: 10.0.1(eslint@10.8.1) + '@types/node': + specifier: ^26.1.1 + version: 26.2.0 + '@vitest/coverage-v8': + specifier: ^4.1.10 + version: 4.1.10(vitest@4.1.10) + eslint: + specifier: ^10.7.0 + version: 10.8.1 + typescript: + specifier: ^6.0.3 + version: 6.0.3 + typescript-eslint: + specifier: ^8.64.0 + version: 8.67.0(eslint@10.8.1)(typescript@6.0.3) + vitest: + specifier: ^4.1.10 + version: 4.1.10(@types/node@26.2.0)(@vitest/coverage-v8@4.1.10)(vite@8.2.1(@types/node@26.2.0)) + +packages: + + '@babel/helper-string-parser@7.29.7': + resolution: {integrity: sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-identifier@7.29.7': + resolution: {integrity: sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==} + engines: {node: '>=6.9.0'} + + '@babel/parser@7.29.8': + resolution: {integrity: sha512-E8lTAYNB1KW+FH+VGJuZM1ioAx2E6oVlvQFRrf5P8ZZmsiJXYAD9vTFV7yyEURNzgh1dFqMZuO6tUwcARbqFCA==} + engines: {node: '>=6.0.0'} + hasBin: true + + '@babel/types@7.29.8': + resolution: {integrity: sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==} + engines: {node: '>=6.9.0'} + + '@bcoe/v8-coverage@1.0.2': + resolution: {integrity: sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==} + engines: {node: '>=18'} + + '@eslint-community/eslint-utils@4.10.1': + resolution: {integrity: sha512-cuadcxVFE8sDK6iWJbs8Sn0av2Nrh2QSGQhVlBW9AaAHqHwjWsZHT8LJ4hFGPh7ASBV2deFdM7H/DPjulmh8rg==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 + + '@eslint-community/regexpp@4.12.2': + resolution: {integrity: sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==} + engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} + + '@eslint/config-array@0.23.5': + resolution: {integrity: sha512-Y3kKLvC1dvTOT+oGlqNQ1XLqK6D1HU2YXPc52NmAlJZbMMWDzGYXMiPRJ8TYD39muD/OTjlZmNJ4ib7dvSrMBA==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + '@eslint/config-helpers@0.7.0': + resolution: {integrity: sha512-DObd/KKUsU+FaFv4PLxSRenpXfQWmPXXP3pPZ6/K1PCrMu2vQpMDMuQe/BqYeoLcz8ro0bVDF1RxOJgfVEdhUw==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + '@eslint/core@1.2.1': + resolution: {integrity: sha512-MwcE1P+AZ4C6DWlpin/OmOA54mmIZ/+xZuJiQd4SyB29oAJjN30UW9wkKNptW2ctp4cEsvhlLY/CsQ1uoHDloQ==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + '@eslint/js@10.0.1': + resolution: {integrity: sha512-zeR9k5pd4gxjZ0abRoIaxdc7I3nDktoXZk2qOv9gCNWx3mVwEn32VRhyLaRsDiJjTs0xq/T8mfPtyuXu7GWBcA==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + peerDependencies: + eslint: ^10.0.0 + peerDependenciesMeta: + eslint: + optional: true + + '@eslint/object-schema@3.0.5': + resolution: {integrity: sha512-vqTaUEgxzm+YDSdElad6PiRoX4t8VGDjCtt05zn4nU810UIx/uNEV7/lZJ6KwFThKZOzOxzXy48da+No7HZaMw==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + '@eslint/plugin-kit@0.7.2': + resolution: {integrity: sha512-+CNAzxglkrpNf/kKywqQfk74QjtceuOE7Qm+AF8miRvPF/wmmK5+OJOgVh3AVTT3RP2mH3+FOaxlE5v72owk0A==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + '@humanfs/core@0.19.2': + resolution: {integrity: sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==} + engines: {node: '>=18.18.0'} + + '@humanfs/node@0.16.8': + resolution: {integrity: sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==} + engines: {node: '>=18.18.0'} + + '@humanfs/types@0.15.0': + resolution: {integrity: sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==} + engines: {node: '>=18.18.0'} + + '@humanwhocodes/module-importer@1.0.1': + resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} + engines: {node: '>=12.22'} + + '@humanwhocodes/retry@0.4.3': + resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==} + engines: {node: '>=18.18'} + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + + '@jridgewell/sourcemap-codec@1.5.5': + resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + + '@jridgewell/trace-mapping@0.3.31': + resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + + '@oxc-project/types@0.144.0': + resolution: {integrity: sha512-nuhZIOLuI6TFQ32I/WnUx+SCPY7SdSKwgnFHydAuoS1+Z4BRcaP+RRJmGzl9lw+0OFF7UmaESf7KQRXaNLHypg==} + + '@rolldown/binding-android-arm64@1.2.4': + resolution: {integrity: sha512-jHC2cnyKz5xU2fhECtFl8OZ83cYNt13GZQD+0uMJ/X3o+ijmd56okHhTUwxVSHPx1IRVIJEZ1/1pPzeLCU6XKA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [android] + + '@rolldown/binding-darwin-arm64@1.2.4': + resolution: {integrity: sha512-Dc5mPD8F5F/FS8i01syd7FTF6yB2fVthH/TRkjwJkzUK6EpoxHtqvZQP5Zwq80/5z19TWYHIg1KOHboCgVx/aQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [darwin] + + '@rolldown/binding-darwin-x64@1.2.4': + resolution: {integrity: sha512-fpDm4oBo6SqLvWUYCmFhdde3U9KH2fRNNMeAnAPAIwxRL345xutL0EtEUcuoxsoazdJGv/MuDBQHlCDrtbvqOg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [darwin] + + '@rolldown/binding-freebsd-x64@1.2.4': + resolution: {integrity: sha512-rSJoreDE/HoIzoaib6MTp5jQtCTdMHKIvItAKT/ImS6Y6Ww76oUaeMyp4Vc/fAgd/ehji068IxetHXAnqUwN9A==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [freebsd] + + '@rolldown/binding-linux-arm-gnueabihf@1.2.4': + resolution: {integrity: sha512-/jm8OGHgn7oGaJu3i/qZI9spUGcJ+y/lk43ttQ/iO1tOd9NissG6o97bighBCiL+BKRngmcDuR6ikfwYdJmVuQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@rolldown/binding-linux-arm64-gnu@1.2.4': + resolution: {integrity: sha512-tIP06BeD9EqvECBrPZ+sqdPlYrT+aYaAiu1wYziVx5elRK/ftm33JxVDy2bXGbr6J0CrtirCkR87/X5a2euEng==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + + '@rolldown/binding-linux-arm64-musl@1.2.4': + resolution: {integrity: sha512-Ql1Q0EQqVThvn9VAVlwNzsUvbSFtCMGjLpRRi4pk5i7NZZ4n5ISiLMjHYtus4VQ2PvkSw24zyaCVsiS+sXPj1w==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + + '@rolldown/binding-linux-ppc64-gnu@1.2.4': + resolution: {integrity: sha512-GjbjXD4XXfN19D0LZNbmiCBUoDiRACsYHr0yaIbbn8aFsXjHZifcYqu/W5Er5X2X990WjHXFrxarn5chzItorQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ppc64] + os: [linux] + + '@rolldown/binding-linux-s390x-gnu@1.2.4': + resolution: {integrity: sha512-p5WR0NOwaRmJ/B1b6IjEFLLivwEsf3PrdBIhRbhTCQisbo2SvHHpG4ELB/+FgQNnB88LTOF86upmJmbvZdQ2lw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [s390x] + os: [linux] + + '@rolldown/binding-linux-x64-gnu@1.2.4': + resolution: {integrity: sha512-4/GyVjmhR+Tc6HLJvwc1sOhPqAZtySiSMesOZyX6JQ5XBxoTDEMKQzvo07NIK6nTon/SivlZqvhzvuVBNQhObQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + + '@rolldown/binding-linux-x64-musl@1.2.4': + resolution: {integrity: sha512-l9eeLsCNvPpmSXUej0etw/J1eqV0Jj1D5G/xG6YTijmE6dkv6E2QezgWbTfQk63v952DPqrjOCoiqxq7Bw0YUQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + + '@rolldown/binding-openharmony-arm64@1.2.4': + resolution: {integrity: sha512-e0F355MSTMm3+UOqtV3L24gFUp2N5m1f8L/7d56deik6va+AXdrt9F8LbzGpeWGWRbZEDq4m8NVnJDeBtf9DZg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [openharmony] + + '@rolldown/binding-win32-arm64-msvc@1.2.4': + resolution: {integrity: sha512-AWLi0uBRYh6QlE7OKhiz+phZC0qwtij2QZmhmOdsLdFn64m7oMpooE9ICE3lhm9xMb4SpDo2WbHcxX1iFLFtqw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [win32] + + '@rolldown/binding-win32-x64-msvc@1.2.4': + resolution: {integrity: sha512-UwSDJOg3dqCAejWdxclJjCsh3Qq4vLYMDxmyHqo1btz3stK2VqgwNd3mm5tuIwzSlGIQ/1H9Hr+Zn09mrezNqQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [win32] + + '@rolldown/pluginutils@1.0.1': + resolution: {integrity: sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==} + + '@standard-schema/spec@1.1.0': + resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} + + '@types/chai@5.2.3': + resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} + + '@types/deep-eql@4.0.2': + resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} + + '@types/esrecurse@4.3.1': + resolution: {integrity: sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==} + + '@types/estree@1.0.9': + resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==} + + '@types/json-schema@7.0.15': + resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} + + '@types/node@26.2.0': + resolution: {integrity: sha512-5IviulTZeRNp2vAJ514cc/HUlY5nZ9fCbq9DMyC52BrhFZACo3nI0R7qBxhQmo/d27NFe96ur/b7Wwxklda+kg==} + + '@typescript-eslint/eslint-plugin@8.67.0': + resolution: {integrity: sha512-Un7Heoyj65NREbKAyIrFxeM143NZpExWmy1Nep4DLeQOeLlTeumPjoNKnBrU5D5moWXbPJgRa5Uwcdu0faVNGQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + '@typescript-eslint/parser': ^8.67.0 + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/parser@8.67.0': + resolution: {integrity: sha512-fUBfTuuEulWqX6V8+O3PtScV01tzYYRUDTAirHFKoRAt7nOzoGiPt0M/bB47wWNy0coOOcgEwAMUtBpykMxl6w==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/project-service@8.67.0': + resolution: {integrity: sha512-cvE8c7ulYeXN9fYuszhCeCsbzyVEXuhrRCybnBre7TUmqb5nRmBfQAwCj0O3WJFDeyAZt4VYv51vMCC9LHSdYw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/scope-manager@8.67.0': + resolution: {integrity: sha512-EgvsleTwS4E+WzzSvem8fAUubLwatMNF1B5hHSLQxcvs7q2dtRhGyujHwLJSYlG41niJ7GP24Aha2+0mb1b2kg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/tsconfig-utils@8.67.0': + resolution: {integrity: sha512-vV+LUSv5njUWsknE71fqKTlXUva+R76SaeORd6Zojcunk/6DvKFXONU3BrAs2H49mbygUXt6gbYunzwqNwlhdg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/type-utils@8.67.0': + resolution: {integrity: sha512-aVWDXbRmdXO9siTfX4ditQI1T9+zVcNazT48EJCD0v40/9RIFoUgZ05CmGEq9H2gixRpjUn/iplwvlcvutJW/Q==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/types@8.67.0': + resolution: {integrity: sha512-sBtgslww8nsMYUjhdPBiSyUqSzT8uR6g93A2QXnQC8+cGdjz0CyaOdqHDRJb1AtORbZCNUJBBeFA/tNR2uQmww==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/typescript-estree@8.67.0': + resolution: {integrity: sha512-EKQBCE9yNlRJYm7jdTW5AhDacDUmSwQb0FAJAmK2EKYrNXIsa2vxcSZx6PvJ/dEdI6lS+Y9W+EXckLj0iPFGcw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/utils@8.67.0': + resolution: {integrity: sha512-U9D1FdwEWBwok3hxxSdhclMb0twvt9QnjIQ0VfQ1AiX2epnpSgv2ubVDsayOFyY8K6FX+AQ7E0FKWVG3iKsj1A==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/visitor-keys@8.67.0': + resolution: {integrity: sha512-fkv8dHRDqfGtTHuJeebdrQ7cX6Ad4WAS00rgHh9UGvMycF1mjBfsxry1XsLIFhWZ6Judlh6UdzK+TYlbpCXgnA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@vitest/coverage-v8@4.1.10': + resolution: {integrity: sha512-IM49HmthevbgAO4anp1hwtoT9wYe59w0LR00gr+eagHE+ZJ5lK4sLPeO0ubgoJcwLk6dehU3R24N+FbEEKDc8g==} + peerDependencies: + '@vitest/browser': 4.1.10 + vitest: 4.1.10 + peerDependenciesMeta: + '@vitest/browser': + optional: true + + '@vitest/expect@4.1.10': + resolution: {integrity: sha512-YsCn+qAk1GWjQOWFEsEcL2gNQ0zmVmQu3T03qP6UyjhtmdtwtbuI+DASn/7iQB3HGTXkdBwGddzxPlmiql5vlA==} + + '@vitest/mocker@4.1.10': + resolution: {integrity: sha512-v0xaezt+DKEmKfaxg133ldzADrwLGd7Ze1MfQQTYfvs8OqZIwbxyxaYURivwV7sWy5fqn3rH5uOrSp07bp44Ow==} + peerDependencies: + msw: ^2.4.9 + vite: ^6.0.0 || ^7.0.0 || ^8.0.0 + peerDependenciesMeta: + msw: + optional: true + vite: + optional: true + + '@vitest/pretty-format@4.1.10': + resolution: {integrity: sha512-W1HsjSH4MXQ9YfmmhLAoIYf1HRfekQCGngeIgcei6MP5QQGWUe0gkopdZQaVCFO+JDJMrAJGwa5pRpNpvy4P8Q==} + + '@vitest/runner@4.1.10': + resolution: {integrity: sha512-IKI6kpIH+LmpROplyLwBBaCfMgOZOMsygVa6BARD6ahA04VRuJSa6OaVG7kRvSEMD870Vd91rSSw0eegtWyLGg==} + + '@vitest/snapshot@4.1.10': + resolution: {integrity: sha512-xRkfOT1qpTAi/Ti4Y1LtfRc3kEuqxGw59eN2jN9pRWMtS/XDevekhcFSqvQqjUNGksfjMJu3Y+oJ+4Ypn2OaJw==} + + '@vitest/spy@4.1.10': + resolution: {integrity: sha512-PLf/Ugvoq5wO/b4rwYCR1h2PSIdXz7wnkQFMiUpLdtM7l6pqVFcQIBEHyT1+l+cj7mNwAfZHzqXqDyjvOuwbDw==} + + '@vitest/utils@4.1.10': + resolution: {integrity: sha512-fy9am/HWxbaGt/Sawrp90vt6Y6jQwf1RX77cz3uwoJwJVMli/e1IEwRPnMNJ7vKfPTwo0diXifkpPvwH9v7nGA==} + + acorn-jsx@5.3.2: + resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} + peerDependencies: + acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 + + acorn@8.18.0: + resolution: {integrity: sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ==} + engines: {node: '>=0.4.0'} + hasBin: true + + ajv@6.15.0: + resolution: {integrity: sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==} + + assertion-error@2.0.1: + resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} + engines: {node: '>=12'} + + ast-v8-to-istanbul@1.0.5: + resolution: {integrity: sha512-UPAgKJFSEGMWSDr3LX4tqnAb4f7KGT8O40Tyx8wbYmmZ/yn58lNCm8h3svs3eXgiGd5AXxz8NDOvXWvicq+rJA==} + + balanced-match@4.0.4: + resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==} + engines: {node: 18 || 20 || >=22} + + brace-expansion@5.0.9: + resolution: {integrity: sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==} + engines: {node: 20 || >=22} + + chai@6.2.2: + resolution: {integrity: sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==} + engines: {node: '>=18'} + + convert-source-map@2.0.0: + resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + + cross-spawn@7.0.6: + resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} + engines: {node: '>= 8'} + + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + deep-is@0.1.4: + resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} + + detect-libc@2.1.2: + resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} + engines: {node: '>=8'} + + es-module-lexer@2.3.1: + resolution: {integrity: sha512-shc1dbU90Yl/xq1QrC7QRtfcwURZuVRfPhZbDoldJ1cn1gzDvBaBWlv0eFolj5+0znnPJz5TXLxsN77X/12KTA==} + + escape-string-regexp@4.0.0: + resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} + engines: {node: '>=10'} + + eslint-scope@9.1.2: + resolution: {integrity: sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + eslint-visitor-keys@3.4.3: + resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + eslint-visitor-keys@5.0.1: + resolution: {integrity: sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + eslint@10.8.1: + resolution: {integrity: sha512-wqA7W2jbsC/BnV9Iv1UZpKVFkO1AdNoSmYW8NWG4HNOBbkAMvIqDZ27pI2f07dqn583NcIC44ckjAcOXDL1QbQ==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + hasBin: true + peerDependencies: + jiti: '*' + peerDependenciesMeta: + jiti: + optional: true + + espree@11.2.0: + resolution: {integrity: sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + esquery@1.7.0: + resolution: {integrity: sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==} + engines: {node: '>=0.10'} + + esrecurse@4.3.0: + resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} + engines: {node: '>=4.0'} + + estraverse@5.3.0: + resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} + engines: {node: '>=4.0'} + + estree-walker@3.0.3: + resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} + + esutils@2.0.3: + resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} + engines: {node: '>=0.10.0'} + + expect-type@1.4.0: + resolution: {integrity: sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==} + engines: {node: '>=12.0.0'} + + fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + + fast-json-stable-stringify@2.1.0: + resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} + + fast-levenshtein@2.0.6: + resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + + file-entry-cache@8.0.0: + resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} + engines: {node: '>=16.0.0'} + + find-up@5.0.0: + resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} + engines: {node: '>=10'} + + flat-cache@4.0.1: + resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==} + engines: {node: '>=16'} + + flatted@3.4.4: + resolution: {integrity: sha512-5+ybhBZANEJxaH3X5evAFatUxLfEHSr7n6kYJ+1Qd0mUqr4eu9gIf6GDbWHf8RJijHrjjO8G+la14SlL2SeS1Q==} + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + glob-parent@6.0.2: + resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} + engines: {node: '>=10.13.0'} + + has-flag@4.0.0: + resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} + engines: {node: '>=8'} + + html-escaper@2.0.2: + resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==} + + ignore@5.3.2: + resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} + engines: {node: '>= 4'} + + ignore@7.0.6: + resolution: {integrity: sha512-BAg6QkE8W+TuQLrrw0Ugr7HegXduRuuj8/ti2kSOc+jz1dmx8/WNcjr6XGnq5YpDWxFwwaavqD0+jIUOKelTsw==} + engines: {node: '>= 4'} + + imurmurhash@0.1.4: + resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} + engines: {node: '>=0.8.19'} + + is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + + is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + + isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + + istanbul-lib-coverage@3.2.2: + resolution: {integrity: sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==} + engines: {node: '>=8'} + + istanbul-lib-report@3.0.1: + resolution: {integrity: sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==} + engines: {node: '>=10'} + + istanbul-reports@3.2.0: + resolution: {integrity: sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==} + engines: {node: '>=8'} + + js-tokens@10.0.0: + resolution: {integrity: sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q==} + + json-buffer@3.0.1: + resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} + + json-schema-traverse@0.4.1: + resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} + + json-stable-stringify-without-jsonify@1.0.1: + resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} + + keyv@4.5.4: + resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} + + levn@0.4.1: + resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} + engines: {node: '>= 0.8.0'} + + lightningcss-android-arm64@1.33.0: + resolution: {integrity: sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [android] + + lightningcss-darwin-arm64@1.33.0: + resolution: {integrity: sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [darwin] + + lightningcss-darwin-x64@1.33.0: + resolution: {integrity: sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [darwin] + + lightningcss-freebsd-x64@1.33.0: + resolution: {integrity: sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [freebsd] + + lightningcss-linux-arm-gnueabihf@1.33.0: + resolution: {integrity: sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm] + os: [linux] + + lightningcss-linux-arm64-gnu@1.33.0: + resolution: {integrity: sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + + lightningcss-linux-arm64-musl@1.33.0: + resolution: {integrity: sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + + lightningcss-linux-x64-gnu@1.33.0: + resolution: {integrity: sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + + lightningcss-linux-x64-musl@1.33.0: + resolution: {integrity: sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + + lightningcss-win32-arm64-msvc@1.33.0: + resolution: {integrity: sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [win32] + + lightningcss-win32-x64-msvc@1.33.0: + resolution: {integrity: sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [win32] + + lightningcss@1.33.0: + resolution: {integrity: sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==} + engines: {node: '>= 12.0.0'} + + locate-path@6.0.0: + resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} + engines: {node: '>=10'} + + magic-string@0.30.21: + resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + + magicast@0.5.4: + resolution: {integrity: sha512-llBEhWm1SacoRwgHUoQJYtwp4PBLF4faQi5TCpIGyGs9n4y5+juI0tDgyKIfpqxckRHaHzouUEph3THklWh03w==} + + make-dir@4.0.0: + resolution: {integrity: sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==} + engines: {node: '>=10'} + + minimatch@10.2.6: + resolution: {integrity: sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==} + engines: {node: 18 || 20 || >=22} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + nanoid@3.3.18: + resolution: {integrity: sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + natural-compare@1.4.0: + resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} + + obug@2.1.4: + resolution: {integrity: sha512-4a+OsYv9UktOJKE+l1A4OufDgdRF9PifWj+tJnHURo/P+WOxpG4GzUFL9qCalmWauao6ogiG+QvnCovwPoyAWA==} + engines: {node: '>=12.20.0'} + + optionator@0.9.4: + resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} + engines: {node: '>= 0.8.0'} + + p-limit@3.1.0: + resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} + engines: {node: '>=10'} + + p-locate@5.0.0: + resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} + engines: {node: '>=10'} + + path-exists@4.0.0: + resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} + engines: {node: '>=8'} + + path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + + pathe@2.0.3: + resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@4.0.5: + resolution: {integrity: sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==} + engines: {node: '>=12'} + + postcss@8.5.26: + resolution: {integrity: sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ==} + engines: {node: ^10 || ^12 || >=14} + + prelude-ls@1.2.1: + resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} + engines: {node: '>= 0.8.0'} + + punycode@2.3.1: + resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} + engines: {node: '>=6'} + + rolldown@1.2.4: + resolution: {integrity: sha512-rSr7irW0K7QRWzjdJXqZowkcRdDtjRduh43rBltnVKd0VFq839l1lJoDvGJb6gl7+4rTTCrPWu+YfujUL8Ug7w==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + + semver@7.8.5: + resolution: {integrity: sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==} + engines: {node: '>=10'} + hasBin: true + + shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + + shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} + + siginfo@2.0.0: + resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + stackback@0.0.2: + resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + + std-env@4.2.0: + resolution: {integrity: sha512-oCUKSupKTHX53EyjDtuZQ64pjLJ6yYCtpmEw0goYxtjG9KpbRe8KAsl2tBUGU9DyMcJ0RwJ8GqJAFzMXcXW1Rw==} + + supports-color@7.2.0: + resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} + engines: {node: '>=8'} + + tinybench@2.9.0: + resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} + + tinyexec@1.3.0: + resolution: {integrity: sha512-QKAl9m8gWWGHV8jZcPeym6j+XULi6tOf1mT83WYJ4Lk2ytW/uwAWkrP0uFsdoYMdueVJ0qs26wZ+23xeB4ibNQ==} + engines: {node: '>=18'} + + tinyglobby@0.2.17: + resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==} + engines: {node: '>=12.0.0'} + + tinyrainbow@3.1.1: + resolution: {integrity: sha512-yau8yJdTt989Mm0Bd/236QnzEiPf2xLLTqUZRUJOo/3CB078LSwzei343DgtJVmfJKJE3TMINY1u42SQsP6mXw==} + engines: {node: '>=14.0.0'} + + ts-api-utils@2.5.0: + resolution: {integrity: sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==} + engines: {node: '>=18.12'} + peerDependencies: + typescript: '>=4.8.4' + + type-check@0.4.0: + resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} + engines: {node: '>= 0.8.0'} + + typescript-eslint@8.67.0: + resolution: {integrity: sha512-S2udFs8tCKEKffuJ4TB1idGUZiXdCPGi3IPBGWXarbLQ5UPXORV8QEVzJ4gCRduURMb5EkpNCdjbk0eDIuI8Yg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + + typescript@6.0.3: + resolution: {integrity: sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==} + engines: {node: '>=14.17'} + hasBin: true + + undici-types@8.3.0: + resolution: {integrity: sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==} + + uri-js@4.4.1: + resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + + vite@8.2.1: + resolution: {integrity: sha512-EU/eS7BH3XROHh2YnBefjM6DBKA6ZeMZEYQbj7NLWg5wHYlhB8B/Mayd5XsgWq+NFYccDOTemRpdETWR6Ka/lw==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + peerDependencies: + '@types/node': ^20.19.0 || >=22.12.0 + '@vitejs/devtools': ^0.4.0 + esbuild: ^0.27.0 || ^0.28.0 + jiti: '>=1.21.0' + less: ^4.0.0 + sass: ^1.70.0 + sass-embedded: ^1.70.0 + stylus: '>=0.54.8' + sugarss: ^5.0.0 + terser: ^5.16.0 + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + '@types/node': + optional: true + '@vitejs/devtools': + optional: true + esbuild: + optional: true + jiti: + optional: true + less: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + tsx: + optional: true + yaml: + optional: true + + vitest@4.1.10: + resolution: {integrity: sha512-R9jUTe5S4Qb0HCd4TNqpC7oGcrMssMRGXLW80ubjWsW9VH5GF8y1Y0SFLY9AbqSk6nt0PnOx4H4WNJYZ13GUPw==} + engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0} + hasBin: true + peerDependencies: + '@edge-runtime/vm': '*' + '@opentelemetry/api': ^1.9.0 + '@types/node': ^20.0.0 || ^22.0.0 || >=24.0.0 + '@vitest/browser-playwright': 4.1.10 + '@vitest/browser-preview': 4.1.10 + '@vitest/browser-webdriverio': 4.1.10 + '@vitest/coverage-istanbul': 4.1.10 + '@vitest/coverage-v8': 4.1.10 + '@vitest/ui': 4.1.10 + happy-dom: '*' + jsdom: '*' + vite: ^6.0.0 || ^7.0.0 || ^8.0.0 + peerDependenciesMeta: + '@edge-runtime/vm': + optional: true + '@opentelemetry/api': + optional: true + '@types/node': + optional: true + '@vitest/browser-playwright': + optional: true + '@vitest/browser-preview': + optional: true + '@vitest/browser-webdriverio': + optional: true + '@vitest/coverage-istanbul': + optional: true + '@vitest/coverage-v8': + optional: true + '@vitest/ui': + optional: true + happy-dom: + optional: true + jsdom: + optional: true + + which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + + why-is-node-running@2.3.0: + resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} + engines: {node: '>=8'} + hasBin: true + + word-wrap@1.2.5: + resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} + engines: {node: '>=0.10.0'} + + yocto-queue@0.1.0: + resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} + engines: {node: '>=10'} + +snapshots: + + '@babel/helper-string-parser@7.29.7': {} + + '@babel/helper-validator-identifier@7.29.7': {} + + '@babel/parser@7.29.8': + dependencies: + '@babel/types': 7.29.8 + + '@babel/types@7.29.8': + dependencies: + '@babel/helper-string-parser': 7.29.7 + '@babel/helper-validator-identifier': 7.29.7 + + '@bcoe/v8-coverage@1.0.2': {} + + '@eslint-community/eslint-utils@4.10.1(eslint@10.8.1)': + dependencies: + eslint: 10.8.1 + eslint-visitor-keys: 3.4.3 + + '@eslint-community/regexpp@4.12.2': {} + + '@eslint/config-array@0.23.5': + dependencies: + '@eslint/object-schema': 3.0.5 + debug: 4.4.3 + minimatch: 10.2.6 + transitivePeerDependencies: + - supports-color + + '@eslint/config-helpers@0.7.0': + dependencies: + '@eslint/core': 1.2.1 + + '@eslint/core@1.2.1': + dependencies: + '@types/json-schema': 7.0.15 + + '@eslint/js@10.0.1(eslint@10.8.1)': + optionalDependencies: + eslint: 10.8.1 + + '@eslint/object-schema@3.0.5': {} + + '@eslint/plugin-kit@0.7.2': + dependencies: + '@eslint/core': 1.2.1 + levn: 0.4.1 + + '@humanfs/core@0.19.2': + dependencies: + '@humanfs/types': 0.15.0 + + '@humanfs/node@0.16.8': + dependencies: + '@humanfs/core': 0.19.2 + '@humanfs/types': 0.15.0 + '@humanwhocodes/retry': 0.4.3 + + '@humanfs/types@0.15.0': {} + + '@humanwhocodes/module-importer@1.0.1': {} + + '@humanwhocodes/retry@0.4.3': {} + + '@jridgewell/resolve-uri@3.1.2': {} + + '@jridgewell/sourcemap-codec@1.5.5': {} + + '@jridgewell/trace-mapping@0.3.31': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.5 + + '@oxc-project/types@0.144.0': {} + + '@rolldown/binding-android-arm64@1.2.4': + optional: true + + '@rolldown/binding-darwin-arm64@1.2.4': + optional: true + + '@rolldown/binding-darwin-x64@1.2.4': + optional: true + + '@rolldown/binding-freebsd-x64@1.2.4': + optional: true + + '@rolldown/binding-linux-arm-gnueabihf@1.2.4': + optional: true + + '@rolldown/binding-linux-arm64-gnu@1.2.4': + optional: true + + '@rolldown/binding-linux-arm64-musl@1.2.4': + optional: true + + '@rolldown/binding-linux-ppc64-gnu@1.2.4': + optional: true + + '@rolldown/binding-linux-s390x-gnu@1.2.4': + optional: true + + '@rolldown/binding-linux-x64-gnu@1.2.4': + optional: true + + '@rolldown/binding-linux-x64-musl@1.2.4': + optional: true + + '@rolldown/binding-openharmony-arm64@1.2.4': + optional: true + + '@rolldown/binding-win32-arm64-msvc@1.2.4': + optional: true + + '@rolldown/binding-win32-x64-msvc@1.2.4': + optional: true + + '@rolldown/pluginutils@1.0.1': {} + + '@standard-schema/spec@1.1.0': {} + + '@types/chai@5.2.3': + dependencies: + '@types/deep-eql': 4.0.2 + assertion-error: 2.0.1 + + '@types/deep-eql@4.0.2': {} + + '@types/esrecurse@4.3.1': {} + + '@types/estree@1.0.9': {} + + '@types/json-schema@7.0.15': {} + + '@types/node@26.2.0': + dependencies: + undici-types: 8.3.0 + + '@typescript-eslint/eslint-plugin@8.67.0(@typescript-eslint/parser@8.67.0(eslint@10.8.1)(typescript@6.0.3))(eslint@10.8.1)(typescript@6.0.3)': + dependencies: + '@eslint-community/regexpp': 4.12.2 + '@typescript-eslint/parser': 8.67.0(eslint@10.8.1)(typescript@6.0.3) + '@typescript-eslint/scope-manager': 8.67.0 + '@typescript-eslint/type-utils': 8.67.0(eslint@10.8.1)(typescript@6.0.3) + '@typescript-eslint/utils': 8.67.0(eslint@10.8.1)(typescript@6.0.3) + '@typescript-eslint/visitor-keys': 8.67.0 + eslint: 10.8.1 + ignore: 7.0.6 + natural-compare: 1.4.0 + ts-api-utils: 2.5.0(typescript@6.0.3) + typescript: 6.0.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/parser@8.67.0(eslint@10.8.1)(typescript@6.0.3)': + dependencies: + '@typescript-eslint/scope-manager': 8.67.0 + '@typescript-eslint/types': 8.67.0 + '@typescript-eslint/typescript-estree': 8.67.0(typescript@6.0.3) + '@typescript-eslint/visitor-keys': 8.67.0 + debug: 4.4.3 + eslint: 10.8.1 + typescript: 6.0.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/project-service@8.67.0(typescript@6.0.3)': + dependencies: + '@typescript-eslint/tsconfig-utils': 8.67.0(typescript@6.0.3) + '@typescript-eslint/types': 8.67.0 + debug: 4.4.3 + typescript: 6.0.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/scope-manager@8.67.0': + dependencies: + '@typescript-eslint/types': 8.67.0 + '@typescript-eslint/visitor-keys': 8.67.0 + + '@typescript-eslint/tsconfig-utils@8.67.0(typescript@6.0.3)': + dependencies: + typescript: 6.0.3 + + '@typescript-eslint/type-utils@8.67.0(eslint@10.8.1)(typescript@6.0.3)': + dependencies: + '@typescript-eslint/types': 8.67.0 + '@typescript-eslint/typescript-estree': 8.67.0(typescript@6.0.3) + '@typescript-eslint/utils': 8.67.0(eslint@10.8.1)(typescript@6.0.3) + debug: 4.4.3 + eslint: 10.8.1 + ts-api-utils: 2.5.0(typescript@6.0.3) + typescript: 6.0.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/types@8.67.0': {} + + '@typescript-eslint/typescript-estree@8.67.0(typescript@6.0.3)': + dependencies: + '@typescript-eslint/project-service': 8.67.0(typescript@6.0.3) + '@typescript-eslint/tsconfig-utils': 8.67.0(typescript@6.0.3) + '@typescript-eslint/types': 8.67.0 + '@typescript-eslint/visitor-keys': 8.67.0 + debug: 4.4.3 + minimatch: 10.2.6 + semver: 7.8.5 + tinyglobby: 0.2.17 + ts-api-utils: 2.5.0(typescript@6.0.3) + typescript: 6.0.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/utils@8.67.0(eslint@10.8.1)(typescript@6.0.3)': + dependencies: + '@eslint-community/eslint-utils': 4.10.1(eslint@10.8.1) + '@typescript-eslint/scope-manager': 8.67.0 + '@typescript-eslint/types': 8.67.0 + '@typescript-eslint/typescript-estree': 8.67.0(typescript@6.0.3) + eslint: 10.8.1 + typescript: 6.0.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/visitor-keys@8.67.0': + dependencies: + '@typescript-eslint/types': 8.67.0 + eslint-visitor-keys: 5.0.1 + + '@vitest/coverage-v8@4.1.10(vitest@4.1.10)': + dependencies: + '@bcoe/v8-coverage': 1.0.2 + '@vitest/utils': 4.1.10 + ast-v8-to-istanbul: 1.0.5 + istanbul-lib-coverage: 3.2.2 + istanbul-lib-report: 3.0.1 + istanbul-reports: 3.2.0 + magicast: 0.5.4 + obug: 2.1.4 + std-env: 4.2.0 + tinyrainbow: 3.1.1 + vitest: 4.1.10(@types/node@26.2.0)(@vitest/coverage-v8@4.1.10)(vite@8.2.1(@types/node@26.2.0)) + + '@vitest/expect@4.1.10': + dependencies: + '@standard-schema/spec': 1.1.0 + '@types/chai': 5.2.3 + '@vitest/spy': 4.1.10 + '@vitest/utils': 4.1.10 + chai: 6.2.2 + tinyrainbow: 3.1.1 + + '@vitest/mocker@4.1.10(vite@8.2.1(@types/node@26.2.0))': + dependencies: + '@vitest/spy': 4.1.10 + estree-walker: 3.0.3 + magic-string: 0.30.21 + optionalDependencies: + vite: 8.2.1(@types/node@26.2.0) + + '@vitest/pretty-format@4.1.10': + dependencies: + tinyrainbow: 3.1.1 + + '@vitest/runner@4.1.10': + dependencies: + '@vitest/utils': 4.1.10 + pathe: 2.0.3 + + '@vitest/snapshot@4.1.10': + dependencies: + '@vitest/pretty-format': 4.1.10 + '@vitest/utils': 4.1.10 + magic-string: 0.30.21 + pathe: 2.0.3 + + '@vitest/spy@4.1.10': {} + + '@vitest/utils@4.1.10': + dependencies: + '@vitest/pretty-format': 4.1.10 + convert-source-map: 2.0.0 + tinyrainbow: 3.1.1 + + acorn-jsx@5.3.2(acorn@8.18.0): + dependencies: + acorn: 8.18.0 + + acorn@8.18.0: {} + + ajv@6.15.0: + dependencies: + fast-deep-equal: 3.1.3 + fast-json-stable-stringify: 2.1.0 + json-schema-traverse: 0.4.1 + uri-js: 4.4.1 + + assertion-error@2.0.1: {} + + ast-v8-to-istanbul@1.0.5: + dependencies: + '@jridgewell/trace-mapping': 0.3.31 + estree-walker: 3.0.3 + js-tokens: 10.0.0 + + balanced-match@4.0.4: {} + + brace-expansion@5.0.9: + dependencies: + balanced-match: 4.0.4 + + chai@6.2.2: {} + + convert-source-map@2.0.0: {} + + cross-spawn@7.0.6: + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + + debug@4.4.3: + dependencies: + ms: 2.1.3 + + deep-is@0.1.4: {} + + detect-libc@2.1.2: {} + + es-module-lexer@2.3.1: {} + + escape-string-regexp@4.0.0: {} + + eslint-scope@9.1.2: + dependencies: + '@types/esrecurse': 4.3.1 + '@types/estree': 1.0.9 + esrecurse: 4.3.0 + estraverse: 5.3.0 + + eslint-visitor-keys@3.4.3: {} + + eslint-visitor-keys@5.0.1: {} + + eslint@10.8.1: + dependencies: + '@eslint-community/eslint-utils': 4.10.1(eslint@10.8.1) + '@eslint-community/regexpp': 4.12.2 + '@eslint/config-array': 0.23.5 + '@eslint/config-helpers': 0.7.0 + '@eslint/core': 1.2.1 + '@eslint/plugin-kit': 0.7.2 + '@humanfs/node': 0.16.8 + '@humanwhocodes/module-importer': 1.0.1 + '@humanwhocodes/retry': 0.4.3 + '@types/estree': 1.0.9 + ajv: 6.15.0 + cross-spawn: 7.0.6 + debug: 4.4.3 + escape-string-regexp: 4.0.0 + eslint-scope: 9.1.2 + eslint-visitor-keys: 5.0.1 + espree: 11.2.0 + esquery: 1.7.0 + esutils: 2.0.3 + fast-deep-equal: 3.1.3 + file-entry-cache: 8.0.0 + find-up: 5.0.0 + glob-parent: 6.0.2 + ignore: 5.3.2 + imurmurhash: 0.1.4 + is-glob: 4.0.3 + json-stable-stringify-without-jsonify: 1.0.1 + minimatch: 10.2.6 + natural-compare: 1.4.0 + optionator: 0.9.4 + transitivePeerDependencies: + - supports-color + + espree@11.2.0: + dependencies: + acorn: 8.18.0 + acorn-jsx: 5.3.2(acorn@8.18.0) + eslint-visitor-keys: 5.0.1 + + esquery@1.7.0: + dependencies: + estraverse: 5.3.0 + + esrecurse@4.3.0: + dependencies: + estraverse: 5.3.0 + + estraverse@5.3.0: {} + + estree-walker@3.0.3: + dependencies: + '@types/estree': 1.0.9 + + esutils@2.0.3: {} + + expect-type@1.4.0: {} + + fast-deep-equal@3.1.3: {} + + fast-json-stable-stringify@2.1.0: {} + + fast-levenshtein@2.0.6: {} + + fdir@6.5.0(picomatch@4.0.5): + optionalDependencies: + picomatch: 4.0.5 + + file-entry-cache@8.0.0: + dependencies: + flat-cache: 4.0.1 + + find-up@5.0.0: + dependencies: + locate-path: 6.0.0 + path-exists: 4.0.0 + + flat-cache@4.0.1: + dependencies: + flatted: 3.4.4 + keyv: 4.5.4 + + flatted@3.4.4: {} + + fsevents@2.3.3: + optional: true + + glob-parent@6.0.2: + dependencies: + is-glob: 4.0.3 + + has-flag@4.0.0: {} + + html-escaper@2.0.2: {} + + ignore@5.3.2: {} + + ignore@7.0.6: {} + + imurmurhash@0.1.4: {} + + is-extglob@2.1.1: {} + + is-glob@4.0.3: + dependencies: + is-extglob: 2.1.1 + + isexe@2.0.0: {} + + istanbul-lib-coverage@3.2.2: {} + + istanbul-lib-report@3.0.1: + dependencies: + istanbul-lib-coverage: 3.2.2 + make-dir: 4.0.0 + supports-color: 7.2.0 + + istanbul-reports@3.2.0: + dependencies: + html-escaper: 2.0.2 + istanbul-lib-report: 3.0.1 + + js-tokens@10.0.0: {} + + json-buffer@3.0.1: {} + + json-schema-traverse@0.4.1: {} + + json-stable-stringify-without-jsonify@1.0.1: {} + + keyv@4.5.4: + dependencies: + json-buffer: 3.0.1 + + levn@0.4.1: + dependencies: + prelude-ls: 1.2.1 + type-check: 0.4.0 + + lightningcss-android-arm64@1.33.0: + optional: true + + lightningcss-darwin-arm64@1.33.0: + optional: true + + lightningcss-darwin-x64@1.33.0: + optional: true + + lightningcss-freebsd-x64@1.33.0: + optional: true + + lightningcss-linux-arm-gnueabihf@1.33.0: + optional: true + + lightningcss-linux-arm64-gnu@1.33.0: + optional: true + + lightningcss-linux-arm64-musl@1.33.0: + optional: true + + lightningcss-linux-x64-gnu@1.33.0: + optional: true + + lightningcss-linux-x64-musl@1.33.0: + optional: true + + lightningcss-win32-arm64-msvc@1.33.0: + optional: true + + lightningcss-win32-x64-msvc@1.33.0: + optional: true + + lightningcss@1.33.0: + dependencies: + detect-libc: 2.1.2 + optionalDependencies: + lightningcss-android-arm64: 1.33.0 + lightningcss-darwin-arm64: 1.33.0 + lightningcss-darwin-x64: 1.33.0 + lightningcss-freebsd-x64: 1.33.0 + lightningcss-linux-arm-gnueabihf: 1.33.0 + lightningcss-linux-arm64-gnu: 1.33.0 + lightningcss-linux-arm64-musl: 1.33.0 + lightningcss-linux-x64-gnu: 1.33.0 + lightningcss-linux-x64-musl: 1.33.0 + lightningcss-win32-arm64-msvc: 1.33.0 + lightningcss-win32-x64-msvc: 1.33.0 + + locate-path@6.0.0: + dependencies: + p-locate: 5.0.0 + + magic-string@0.30.21: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + + magicast@0.5.4: + dependencies: + '@babel/parser': 7.29.8 + '@babel/types': 7.29.8 + source-map-js: 1.2.1 + + make-dir@4.0.0: + dependencies: + semver: 7.8.5 + + minimatch@10.2.6: + dependencies: + brace-expansion: 5.0.9 + + ms@2.1.3: {} + + nanoid@3.3.18: {} + + natural-compare@1.4.0: {} + + obug@2.1.4: {} + + optionator@0.9.4: + dependencies: + deep-is: 0.1.4 + fast-levenshtein: 2.0.6 + levn: 0.4.1 + prelude-ls: 1.2.1 + type-check: 0.4.0 + word-wrap: 1.2.5 + + p-limit@3.1.0: + dependencies: + yocto-queue: 0.1.0 + + p-locate@5.0.0: + dependencies: + p-limit: 3.1.0 + + path-exists@4.0.0: {} + + path-key@3.1.1: {} + + pathe@2.0.3: {} + + picocolors@1.1.1: {} + + picomatch@4.0.5: {} + + postcss@8.5.26: + dependencies: + nanoid: 3.3.18 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + prelude-ls@1.2.1: {} + + punycode@2.3.1: {} + + rolldown@1.2.4: + dependencies: + '@oxc-project/types': 0.144.0 + '@rolldown/pluginutils': 1.0.1 + optionalDependencies: + '@rolldown/binding-android-arm64': 1.2.4 + '@rolldown/binding-darwin-arm64': 1.2.4 + '@rolldown/binding-darwin-x64': 1.2.4 + '@rolldown/binding-freebsd-x64': 1.2.4 + '@rolldown/binding-linux-arm-gnueabihf': 1.2.4 + '@rolldown/binding-linux-arm64-gnu': 1.2.4 + '@rolldown/binding-linux-arm64-musl': 1.2.4 + '@rolldown/binding-linux-ppc64-gnu': 1.2.4 + '@rolldown/binding-linux-s390x-gnu': 1.2.4 + '@rolldown/binding-linux-x64-gnu': 1.2.4 + '@rolldown/binding-linux-x64-musl': 1.2.4 + '@rolldown/binding-openharmony-arm64': 1.2.4 + '@rolldown/binding-win32-arm64-msvc': 1.2.4 + '@rolldown/binding-win32-x64-msvc': 1.2.4 + + semver@7.8.5: {} + + shebang-command@2.0.0: + dependencies: + shebang-regex: 3.0.0 + + shebang-regex@3.0.0: {} + + siginfo@2.0.0: {} + + source-map-js@1.2.1: {} + + stackback@0.0.2: {} + + std-env@4.2.0: {} + + supports-color@7.2.0: + dependencies: + has-flag: 4.0.0 + + tinybench@2.9.0: {} + + tinyexec@1.3.0: {} + + tinyglobby@0.2.17: + dependencies: + fdir: 6.5.0(picomatch@4.0.5) + picomatch: 4.0.5 + + tinyrainbow@3.1.1: {} + + ts-api-utils@2.5.0(typescript@6.0.3): + dependencies: + typescript: 6.0.3 + + type-check@0.4.0: + dependencies: + prelude-ls: 1.2.1 + + typescript-eslint@8.67.0(eslint@10.8.1)(typescript@6.0.3): + dependencies: + '@typescript-eslint/eslint-plugin': 8.67.0(@typescript-eslint/parser@8.67.0(eslint@10.8.1)(typescript@6.0.3))(eslint@10.8.1)(typescript@6.0.3) + '@typescript-eslint/parser': 8.67.0(eslint@10.8.1)(typescript@6.0.3) + '@typescript-eslint/typescript-estree': 8.67.0(typescript@6.0.3) + '@typescript-eslint/utils': 8.67.0(eslint@10.8.1)(typescript@6.0.3) + eslint: 10.8.1 + typescript: 6.0.3 + transitivePeerDependencies: + - supports-color + + typescript@6.0.3: {} + + undici-types@8.3.0: {} + + uri-js@4.4.1: + dependencies: + punycode: 2.3.1 + + vite@8.2.1(@types/node@26.2.0): + dependencies: + lightningcss: 1.33.0 + picomatch: 4.0.5 + postcss: 8.5.26 + rolldown: 1.2.4 + tinyglobby: 0.2.17 + optionalDependencies: + '@types/node': 26.2.0 + fsevents: 2.3.3 + + vitest@4.1.10(@types/node@26.2.0)(@vitest/coverage-v8@4.1.10)(vite@8.2.1(@types/node@26.2.0)): + dependencies: + '@vitest/expect': 4.1.10 + '@vitest/mocker': 4.1.10(vite@8.2.1(@types/node@26.2.0)) + '@vitest/pretty-format': 4.1.10 + '@vitest/runner': 4.1.10 + '@vitest/snapshot': 4.1.10 + '@vitest/spy': 4.1.10 + '@vitest/utils': 4.1.10 + es-module-lexer: 2.3.1 + expect-type: 1.4.0 + magic-string: 0.30.21 + obug: 2.1.4 + pathe: 2.0.3 + picomatch: 4.0.5 + std-env: 4.2.0 + tinybench: 2.9.0 + tinyexec: 1.3.0 + tinyglobby: 0.2.17 + tinyrainbow: 3.1.1 + vite: 8.2.1(@types/node@26.2.0) + why-is-node-running: 2.3.0 + optionalDependencies: + '@types/node': 26.2.0 + '@vitest/coverage-v8': 4.1.10(vitest@4.1.10) + transitivePeerDependencies: + - msw + + which@2.0.2: + dependencies: + isexe: 2.0.0 + + why-is-node-running@2.3.0: + dependencies: + siginfo: 2.0.0 + stackback: 0.0.2 + + word-wrap@1.2.5: {} + + yocto-queue@0.1.0: {} diff --git a/ts_core/src/clock.test.ts b/ts_core/src/clock.test.ts new file mode 100644 index 0000000..3835c66 --- /dev/null +++ b/ts_core/src/clock.test.ts @@ -0,0 +1,80 @@ +import { describe, expect, it, vi } from 'vitest' + +import { + createManualClock, + createRealClock, + realClock, + type Clock, +} from './clock.ts' + +describe('createRealClock', () => { + it('reads from performance.now()', () => { + const spy = vi.spyOn(performance, 'now').mockReturnValue(1234) + expect(createRealClock().now()).toBe(1234) + spy.mockRestore() + }) + + it('advances as real time passes', () => { + const clock = createRealClock() + const first = clock.now() + expect(clock.now()).toBeGreaterThanOrEqual(first) + }) +}) + +describe('realClock', () => { + it('is a stable shared instance', () => { + // The whole reason this export exists: a fresh object per render would + // re-fire every effect that takes the clock as a dependency. + expect(realClock).toBe(realClock) + expect(realClock).not.toBe(createRealClock()) + }) + + it('satisfies the Clock interface', () => { + const asClock: Clock = realClock + expect(typeof asClock.now()).toBe('number') + }) +}) + +describe('createManualClock', () => { + it('starts at 0 by default', () => { + expect(createManualClock().now()).toBe(0) + }) + + it('starts at the given time', () => { + expect(createManualClock(500).now()).toBe(500) + }) + + it('only moves when told to', () => { + const clock = createManualClock(100) + expect(clock.now()).toBe(100) + expect(clock.now()).toBe(100) + clock.advance(50) + expect(clock.now()).toBe(150) + }) + + it('accumulates repeated advances', () => { + const clock = createManualClock() + clock.advance(10) + clock.advance(15) + expect(clock.now()).toBe(25) + }) + + it('moves backwards on a negative advance', () => { + const clock = createManualClock(100) + clock.advance(-40) + expect(clock.now()).toBe(60) + }) + + it('jumps to an absolute time with set()', () => { + const clock = createManualClock(100) + clock.set(7) + expect(clock.now()).toBe(7) + }) + + it('keeps two instances independent', () => { + const a = createManualClock() + const b = createManualClock() + a.advance(99) + expect(b.now()).toBe(0) + }) +}) diff --git a/ts_core/src/clock.ts b/ts_core/src/clock.ts new file mode 100644 index 0000000..66ffe51 --- /dev/null +++ b/ts_core/src/clock.ts @@ -0,0 +1,50 @@ +/** + * Injectable time source. + * + * All elapsed-time reads flow through this interface instead of calling + * `performance.now()`/`Date.now()` directly (banned by lint everywhere except + * this file), so real-time mechanics can be driven by a scripted clock in + * tests rather than racing the wall clock. + */ +export interface Clock { + /** Current time in milliseconds. Not comparable across different clocks. */ + now: () => number +} + +/** A {@link Clock} backed by the browser's monotonic performance timer. */ +export const createRealClock = (): Clock => ({ + now: (): number => performance.now(), +}) + +/** + * The one real clock instance UI code should pass around. + * + * A single shared instance (rather than calling {@link createRealClock} per + * component) keeps its identity stable across renders, which matters for + * hooks that take it as an effect dependency -- a fresh object each render + * re-fires the effect every time. + */ +export const realClock: Clock = createRealClock() + +/** A {@link Clock} test double whose time only moves when told to. */ +export interface ManualClock extends Clock { + /** Moves the clock forward by the given number of milliseconds. */ + advance: (deltaMs: number) => void + + /** Jumps the clock to an absolute time in milliseconds. */ + set: (timeMs: number) => void +} + +/** Creates a {@link ManualClock} starting at `startMs` (default 0). */ +export const createManualClock = (startMs = 0): ManualClock => { + let current = startMs + return { + now: (): number => current, + advance: (deltaMs: number): void => { + current += deltaMs + }, + set: (timeMs: number): void => { + current = timeMs + }, + } +} diff --git a/ts_core/src/index.ts b/ts_core/src/index.ts new file mode 100644 index 0000000..2af2421 --- /dev/null +++ b/ts_core/src/index.ts @@ -0,0 +1,25 @@ +/** + * Framework-free TypeScript utilities shared across kuhy's games and web apps. + * + * Deliberately no React and no CSS: those live in `@kuhyx/web-ui`. A game + * that needs a seeded RNG should not have to install a UI package to get one + * -- that is the coupling that makes people vendor their own copy instead, + * which is how these ended up duplicated four ways to begin with. + */ +export { + createManualClock, + createRealClock, + realClock, + type Clock, + type ManualClock, +} from './clock.ts' +export { + createRng, + createSeededRng, + nextChance, + nextFloat, + nextInt, + pick, + type Rng, + type SeededRng, +} from './rng.ts' diff --git a/ts_core/src/rng.test.ts b/ts_core/src/rng.test.ts new file mode 100644 index 0000000..8a667c7 --- /dev/null +++ b/ts_core/src/rng.test.ts @@ -0,0 +1,157 @@ +import { describe, expect, it } from 'vitest' + +import { + createRng, + createSeededRng, + nextChance, + nextFloat, + nextInt, + pick, +} from './rng.ts' + +/** + * The golden sequence for seed 12345, captured from the *existing* konbini-67 + * and sims3-clone implementations before extraction. + * + * This is the load-bearing test in the package. These repos guarantee "same + * seed -> identical world"; if the shared core ever stops reproducing this + * exact sequence, every saved seed in every consumer silently generates a + * different world. Regenerating these numbers to make a failure go away + * would defeat the entire point of the test. + */ +const GOLDEN_12345 = [ + 0.979_728_267_760_947_3, 0.306_752_264_499_664_3, 0.484_205_421_525_985, + 0.817_934_412_509_203, 0.509_428_369_347_006_1, +] + +describe('mulberry32 core', () => { + it('reproduces the pre-extraction sequence for a known seed', () => { + const rng = createRng(12_345) + const seen = Array.from({ length: 5 }, () => nextFloat(rng)) + expect(seen).toEqual(GOLDEN_12345) + }) + + it('gives both interfaces the same sequence', () => { + const free = createRng(999) + const closure = createSeededRng(999) + const a = Array.from({ length: 8 }, () => nextFloat(free)) + const b = Array.from({ length: 8 }, () => closure.float()) + expect(a).toEqual(b) + }) + + it('is reproducible: same seed, same draws', () => { + const a = createRng(7) + const b = createRng(7) + expect(nextFloat(a)).toBe(nextFloat(b)) + }) + + it('differs between seeds', () => { + expect(nextFloat(createRng(1))).not.toBe(nextFloat(createRng(2))) + }) + + it('stays within [0, 1) across many draws', () => { + const rng = createRng(42) + for (let i = 0; i < 1000; i += 1) { + const value = nextFloat(rng) + expect(value).toBeGreaterThanOrEqual(0) + expect(value).toBeLessThan(1) + } + }) + + it('handles a seed of 0 and a negative seed', () => { + expect(nextFloat(createRng(0))).toBeGreaterThanOrEqual(0) + expect(nextFloat(createRng(-1))).toBeGreaterThanOrEqual(0) + }) +}) + +describe('nextInt', () => { + it('is inclusive of both bounds', () => { + const rng = createRng(3) + const seen = new Set() + for (let i = 0; i < 500; i += 1) { + seen.add(nextInt(rng, 1, 3)) + } + expect([...seen].sort()).toEqual([1, 2, 3]) + }) + + it('returns the only value when min equals max', () => { + expect(nextInt(createRng(5), 4, 4)).toBe(4) + }) + + it('consumes exactly one step, like a raw float draw', () => { + const a = createRng(11) + const b = createRng(11) + nextInt(a, 0, 9) + nextFloat(b) + expect(nextFloat(a)).toBe(nextFloat(b)) + }) +}) + +describe('nextChance', () => { + it('is always true at probability 1 and never at 0', () => { + const rng = createRng(8) + expect(nextChance(rng, 1)).toBe(true) + expect(nextChance(rng, 0)).toBe(false) + }) + + it('lands near the requested probability over many draws', () => { + const rng = createRng(2024) + let hits = 0 + for (let i = 0; i < 4000; i += 1) { + if (nextChance(rng, 0.25)) { + hits += 1 + } + } + expect(hits / 4000).toBeGreaterThan(0.2) + expect(hits / 4000).toBeLessThan(0.3) + }) +}) + +describe('pick', () => { + it('returns the single element of a one-item tuple', () => { + expect(pick(createRng(1), ['only'])).toBe('only') + }) + + it('eventually returns every element', () => { + const rng = createRng(4) + const seen = new Set() + for (let i = 0; i < 500; i += 1) { + seen.add(pick(rng, ['a', 'b', 'c'])) + } + expect([...seen].sort()).toEqual(['a', 'b', 'c']) + }) + + it('is deterministic for a given seed', () => { + const items: readonly [string, ...string[]] = ['w', 'x', 'y', 'z'] + expect(pick(createRng(77), items)).toBe(pick(createRng(77), items)) + }) +}) + +describe('createSeededRng', () => { + it('picks uniformly and deterministically', () => { + expect(createSeededRng(5).pick(['p', 'q', 'r'])).toBe( + createSeededRng(5).pick(['p', 'q', 'r']), + ) + }) + + it('throws rather than returning undefined on an empty array', () => { + expect(() => createSeededRng(1).pick([])).toThrow('pick() on an empty array') + }) + + it('matches europe-county-map: pick indexes by floor(float * len)', () => { + // The old local implementation indexed directly off the float rather than + // going through int(). Same step count, and algebraically identical for + // 0..len-1 -- pinned here so a future "simplification" to int() cannot + // silently shift every pick by an off-by-one. + const probe = createRng(31) + const expectedIndex = Math.floor(nextFloat(probe) * 5) + const items = ['a', 'b', 'c', 'd', 'e'] + expect(createSeededRng(31).pick(items)).toBe(items[expectedIndex]) + }) + + it('exposes int and chance over the same stream', () => { + const rng = createSeededRng(64) + expect(rng.int(10, 10)).toBe(10) + expect(typeof rng.chance(0.5)).toBe('boolean') + }) +}) diff --git a/ts_core/src/rng.ts b/ts_core/src/rng.ts new file mode 100644 index 0000000..4338d2e --- /dev/null +++ b/ts_core/src/rng.ts @@ -0,0 +1,120 @@ +/** + * Seeded, deterministic PRNG (mulberry32). + * + * A given seed must always reproduce the exact same world, so consumers ban + * `Math.random` by lint outside this file. + * + * ## Why there are two interfaces + * + * The four repos that had grown their own copy did not agree on shape: + * konbini-67 used mutable state plus free functions (`nextFloat(rng)`), + * while sims3-clone and europe-county-map returned an object of closures + * (`rng.next()` / `rng.float()`). Both are exported here rather than + * reconciled into one, because the *sequence* is the contract: these repos + * have seeded tests and "same seed -> identical world" guarantees, and + * changing how many times a helper advances the generator silently changes + * every generated world. A shared package that quietly re-rolled saved + * worlds would be worse than the duplication it replaced. + * + * The generator core is identical in both, so a given seed produces the same + * float sequence whichever interface you use -- verified in the tests. + * + * Note `int` is inclusive of both bounds, matching sims3-clone and + * europe-county-map. konbini-67's local helper took a single *exclusive* + * max; it maps to `int(rng, 0, max - 1)`. + */ + +/** Mutable state for a mulberry32 generator. */ +export interface Rng { + /** Internal 32-bit state. Advanced by every draw. */ + s: number +} + +/** Creates mutable generator state from a seed. */ +export const createRng = (seed: number): Rng => ({ s: seed >>> 0 }) + +/** + * Deterministic float in [0, 1). Advances the generator. + * + * This is the mulberry32 core; every other helper here is defined in terms + * of it, so they all consume exactly one step per call. + */ +export const nextFloat = (rng: Rng): number => { + rng.s = (rng.s + 0x6d_2b_79_f5) >>> 0 + let t = rng.s + t = Math.imul(t ^ (t >>> 15), t | 1) + t ^= t + Math.imul(t ^ (t >>> 7), t | 61) + return ((t ^ (t >>> 14)) >>> 0) / 4_294_967_296 +} + +/** Deterministic integer in [minInclusive, maxInclusive]. One step. */ +export const nextInt = ( + rng: Rng, + minInclusive: number, + maxInclusive: number, +): number => + minInclusive + Math.floor(nextFloat(rng) * (maxInclusive - minInclusive + 1)) + +/** True with the given probability in [0, 1]. One step. */ +export const nextChance = (rng: Rng, probability: number): boolean => + nextFloat(rng) < probability + +/** + * Deterministic pick from a non-empty tuple. One step. + * + * Iterates rather than indexing: under `noUncheckedIndexedAccess` an index + * read is `T | undefined`, and the undefined arm is unreachable here, so it + * could never be covered by a test. + */ +export const pick = (rng: Rng, items: readonly [T, ...T[]]): T => { + const target = nextInt(rng, 0, items.length - 1) + let out: T = items[0] + let seen = 0 + for (const item of items) { + if (seen === target) { + out = item + } + seen += 1 + } + return out +} + +/** + * The closure-style interface, for consumers that pass an `Rng` object + * around rather than threading mutable state through free functions. + */ +export interface SeededRng { + /** Uniform float in [0, 1). */ + float: () => number + /** Uniform integer in [minInclusive, maxInclusive]. */ + int: (minInclusive: number, maxInclusive: number) => number + /** Uniform element of a non-empty array. */ + pick: (items: readonly T[]) => T + /** True with the given probability in [0, 1]. */ + chance: (probability: number) => boolean +} + +/** + * Creates a {@link SeededRng} over the same mulberry32 core. + * + * `pick` throws on an empty array rather than returning `undefined`: the + * callers all treat their input as non-empty, and a silent `undefined` + * propagates into generated data where it is far harder to trace. + */ +export const createSeededRng = (seed: number): SeededRng => { + const state = createRng(seed) + const float = (): number => nextFloat(state) + return { + float, + int: (minInclusive: number, maxInclusive: number): number => + nextInt(state, minInclusive, maxInclusive), + pick: (items: readonly T[]): T => { + const chosen = items[Math.floor(float() * items.length)] + if (chosen === undefined) { + throw new Error('pick() on an empty array') + } + return chosen + }, + chance: (probability: number): boolean => float() < probability, + } +} diff --git a/ts_core/tsconfig.json b/ts_core/tsconfig.json new file mode 100644 index 0000000..12dc55e --- /dev/null +++ b/ts_core/tsconfig.json @@ -0,0 +1,42 @@ +{ + "compilerOptions": { + "target": "ES2023", + // DOM is here only for `performance.now()` in clock.ts -- the package + // ships no DOM *code*. Without it the build fails while the lint config + // (which adds @types/node) passes, so the two must not be trusted to + // agree by accident. + "lib": ["ESNext", "DOM"], + "module": "ESNext", + "moduleResolution": "bundler", + + /* Strictness: everything on, matching the consumers so a file that + compiles here compiles there. */ + "strict": true, + "noUncheckedIndexedAccess": true, + "noImplicitOverride": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "exactOptionalPropertyTypes": true, + "useUnknownInCatchVariables": true, + "verbatimModuleSyntax": true, + "isolatedModules": true, + "erasableSyntaxOnly": true, + + /* Ships a prebuilt dist/ (ESM + .d.ts) because consumers use Vite, which + does not transpile dependencies. */ + "rewriteRelativeImportExtensions": true, + "allowImportingTsExtensions": true, + "declaration": true, + "declarationMap": true, + "sourceMap": true, + "outDir": "dist", + "rootDir": "src", + + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true + }, + "include": ["src"], + "exclude": ["src/**/*.test.ts"] +} diff --git a/ts_core/tsconfig.lint.json b/ts_core/tsconfig.lint.json new file mode 100644 index 0000000..965f8fe --- /dev/null +++ b/ts_core/tsconfig.lint.json @@ -0,0 +1,13 @@ +{ + // tsconfig.json excludes tests so they never land in the published dist/. + // Type-aware linting still needs them in a project, so this config widens + // the include for eslint and `tsc --noEmit` only. It emits nothing. + "extends": "./tsconfig.json", + "compilerOptions": { + "noEmit": true, + "rootDir": ".", + "types": ["vitest/globals", "node"] + }, + "include": ["src", "*.config.ts"], + "exclude": [] +} diff --git a/ts_core/vitest.config.ts b/ts_core/vitest.config.ts new file mode 100644 index 0000000..0b3a882 --- /dev/null +++ b/ts_core/vitest.config.ts @@ -0,0 +1,19 @@ +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + test: { + coverage: { + exclude: ["src/index.ts", "src/**/*.d.ts"], + include: ["src/**/*.ts"], + provider: "v8", + // Same bar as design_system and web_ui: a shared layer several repos + // depend on does not get to be the least-tested code in the tree. + thresholds: { branches: 100, functions: 100, lines: 100, statements: 100 }, + }, + // No jsdom: this package is framework-free. `performance.now()` exists in + // plain node, which is the only browser-ish API it touches. + environment: "node", + globals: true, + include: ["src/**/*.test.ts"], + }, +});