utils/ts_core
Krzysztof kuhy Rudnicki 0e78c8b752 ts_core + web_ui: latest stable, pnpm 11, and a quarantine-aware gate
Finishes the last two of the eleven manifests in this repo. Exact-pins every
devDependency and takes three majors -- @testing-library/jest-dom 6 -> 7,
jsdom 29 -> 30, pnpm 10.13.1 -> 11.24.0 -- with lint and coverage green in
both (ts_core 42/42, web_ui 102/102, 100% branches).

**TypeScript stays at 6.0.3, and this is the campaign's first real
unsatisfiable conflict.** TS 7.0.2 is latest, but typescript-eslint 8.68.0 --
itself the latest release -- declares `typescript: ">=4.8.4 <6.1.0"` and throws
"does not support TS 7.0" as eslint loads its config. There is no 9.x. Taking
TS 7 would mean dropping every type-aware lint rule from both packages, so the
compiler waits for the linter. Recorded as a `transitive:` allowlist entry,
which is a predicate rather than a date: the gate re-checks the blocker each
run and the entry clears itself when upstream ships support. First use of the
allowlist, and the loud banner now prints on every run here.

**pnpm 11 also made the gate wrong, and the gate was fixed rather than pnpm.**
pnpm 11 quarantines packages published inside a 24h window -- a supply-chain
measure against the period when a compromised publish is live -- and refused
@testing-library/react 16.3.3, published 18h earlier. Reporting a repo as
behind a version its package manager will not install is a finding nobody can
act on: the hook fails, and so does the fix it asks for. `quarantine.py` now
narrows npm's "latest" to the newest release old enough to install, bounded
above by `dist-tags.latest` so a maintenance-line release never outranks the
tag. It fetches the FULL packument (7MB for react, 16MB for typescript) and so
runs only after a finding already exists -- a clean run pays nothing, which a
test asserts by making the fetch raise.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CRykabMwPgDzFHcARoqpaK
2026-08-28 13:50:25 +02:00
..
dist ts_core: a framework-free TypeScript utility package 2026-08-15 15:42:41 +02:00
src ts_core: a framework-free TypeScript utility package 2026-08-15 15:42:41 +02:00
.gitignore ts_core: a framework-free TypeScript utility package 2026-08-15 15:42:41 +02:00
eslint.config.js ts_core: a framework-free TypeScript utility package 2026-08-15 15:42:41 +02:00
package.json ts_core + web_ui: latest stable, pnpm 11, and a quarantine-aware gate 2026-08-28 13:50:25 +02:00
pnpm-lock.yaml ts_core + web_ui: latest stable, pnpm 11, and a quarantine-aware gate 2026-08-28 13:50:25 +02:00
README.md ts_core: a framework-free TypeScript utility package 2026-08-15 15:42:41 +02:00
tsconfig.json ts_core: a framework-free TypeScript utility package 2026-08-15 15:42:41 +02:00
tsconfig.lint.json ts_core: a framework-free TypeScript utility package 2026-08-15 15:42:41 +02:00
vitest.config.ts ts_core: a framework-free TypeScript utility package 2026-08-15 15:42:41 +02:00

@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. 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.

// 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

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.