Makes seeded output identical on every device: one PRNG, date keys, cutovers, golden vectors. Use when code shuffles, replaces Math.random, picks daily content or replays a seed or share code. Not for streaks (dates-streaks-and-statistics).
Installs into .claude/skills of the current project.
Are you the author of Seeded Determinism And Golden Vectors?
Add the live security badge to your README. It updates with every re-scan.
[](https://www.skillsdirectory.com/skills/zakariaf-seeded-determinism-and-golden-vectors-reactnative-skills)
---
name: seeded-determinism-and-golden-vectors
description: "Makes seeded output identical on every device: one PRNG, date keys, cutovers, golden vectors. Use when code shuffles, replaces Math.random, picks daily content or replays a seed or share code. Not for streaks (dates-streaks-and-statistics)."
---
# Seeded determinism and golden vectors
Some output must be a pure function of a key: the daily content every user compares, a generated
layout, a shuffle a share code reproduces, a replay. This skill makes that output identical on every
device, in every time zone and on next year's build, with no server: one seeded PRNG the app owns,
civil-date keys, content regenerated from `(key, version)` instead of stored, shipped generators frozen
behind versioned cutovers, and golden vectors computed by an independent oracle that two checkers and
the test suite verify.
## Rules that must hold
1. **The key is an argument; seeded code never reads a clock.** A generator takes its `DateKey`, id or
sequence number as a parameter; nothing it reaches calls `Date.now`, `new Date`, `performance.now`
or imports the clock service. Why: a clock-reading generator cannot be pinned, replayed from a bug
report or run for tomorrow in a test.
2. **A day is a civil-date key `'YYYY-MM-DD'`, computed with integers, from one day definition.** Keys
come from `clock.today()` at the edge and go through `src/core/dates/date-key.ts`; never an instant,
never the UTC day next to a local-day comparison. Why: an instant makes every read zone-dependent,
and two day definitions disagree for hours every day.
3. **One entropy source: `src/core/rng/sfc32.ts`, threaded as state.** No `Math.random`, `crypto`,
`expo-crypto`, `uuid` or PRNG package on a seeded path; functions take a state and return
`{ value, state }`. Why: a library PRNG promises no frozen sequence across its versions, and only
exact integer operations (all sfc32 uses) give the same bits in V8 and in Hermes on iOS and Android.
4. **A seed is a hash plus a frozen salt, mixed by `seedRng` before any draw.** `seedRng(dailySeed(key,
SALT))` or `seedRng(hashSeed(text) ^ SALT)`; never `dailySeed(...) % n`. One salt per feature and
mode, unique and recorded. Why: FNV seeds of neighbouring days differ by a small multiple of the FNV
prime, so an unmixed seed walks the catalog in a visible step; shared salts make two features move
together.
5. **Seeded paths, and every file they import, use only exactly specified operations.** `+ - * / %`,
bitwise, `Math.sqrt/imul/floor/round/abs/min/max/PI`; no `**`, no other `Math.*`, no `Intl`,
`toLocale*` or `localeCompare`; floats frozen to integers before they are stored or fingerprinted.
Why: ECMA-262 leaves `sin`, `exp`, `pow` and `**` implementation-approximated (Hermes uses each
platform's libm) and Hermes takes locale data from iOS and Android separately.
6. **Draw order is part of the output.** Each generator version documents its draws; inputs are sorted
by a stable key (never `localeCompare`, never unordered SQL); the core modules never change. Why:
one extra or reordered draw rewrites every output after it.
7. **Regenerate, never store.** Persist the key, the `generatorVersion` that ran and the user's outcome;
regenerate the content with `generate(key, storedVersion)`. Why: a stored copy is a second source of
truth that the next version contradicts, and history becomes un-migratable.
8. **A shipped generator is frozen; a change is a new version.** A date-keyed generator serves it from
a future cutover date; a text-keyed one (a share code) carries the version in the key, and callers
pass it. Append the version to the code table and the contract, keep every old version callable,
stamp the version in the output; a committed cutover, salt or generator never moves or disappears
(removing a whole generator takes a migration, the developer's approval and a `"retired"` entry).
Salt, catalog, count and draw-order changes are all versions. Why: once users saw a key's output,
that output is what the key means.
9. **Every generator is pinned by golden vectors.** A committed `<feature>.vectors.json` of `(key,
version) → fingerprint + summary` rows, asserted exactly by a Jest test, covering every version; for
date-keyed generators also both sides of each cutover, a year-end/new-year pair and a leap day. Why:
a fingerprint catches any change and a summary line shows a reviewer what changed.
10. **Expected values come from an independent oracle.** The oracle is written from the spec in another
language or style (BigInt TypeScript, Python), imports nothing from `src/`, directly or through a
helper, and carries its own copy of the data. Why: values generated by the implementation under
test enshrine its bugs.
11. **Rows are filled by a reviewed hand-run script; committed rows are frozen; CI verifies and never
blesses.** The update script fills only missing rows and refuses to change a filled one; the checker
fails any committed row that changed or vanished, however it was refilled; no CI job, hook or gate
script runs the script. Why: a refilled row erases the record of what users saw, and a gate that
rewrites what it checks verifies nothing.
12. **The core is a compatibility contract pinned by literals.** `sfc32.ts`, `seeded-draws.ts`,
`fingerprint.ts`, `date-key.ts` and `daily-seed.ts` keep the golden values their tests pin as
literals computed by the oracle. Why: a change there changes every seeded output on every installed
copy, and a golden computed by the module under test can never fail.
13. **Generators are pure and repeatable.** A generator loads in plain Node (no React Native, Expo,
stores or services), is synchronous, keeps no module state, and returns identical output for the
same `(key, version)` whatever ran before. Why: purity is what lets tests, oracles and checkers run
it headlessly, and hidden state makes the second call differ from the first.
## Workflow
1. **Decide what is seeded.** List every output a user may compare with another user or with their own
past (daily content, shuffles, share codes, generated layouts). Read
[references/determinism-policy.md](references/determinism-policy.md) for what seeded code may use
and why.
2. **Copy the core** (once per app) from `templates/src/core/`: `rng/sfc32.ts`, `rng/seeded-draws.ts`,
`rng/fingerprint.ts`, `dates/date-key.ts`, `dates/daily-seed.ts`, `dates/generator-versions.ts`, each
with its test. Never edit them; read [references/seeded-rng-and-seeds.md](references/seeded-rng-and-seeds.md)
and [references/civil-date-keys.md](references/civil-date-keys.md) for the API, the seeding and the
goldens. If an app already has a `date-key.ts` from dates-streaks-and-statistics, keep one copy.
3. **Write the generator** from `templates/src/features/__FEATURE__/__FEATURE__-generator.ts` (replace
`__FEATURE__` kebab, `__FEATURE_PASCAL__`, `__FEATURE_CONST__`): pick a new random salt, freeze the
catalog, document the draw order, export `generate(key, version?)` and `summarize(output)`. Take
"today" from `clock.today()` in the caller (dates-streaks-and-statistics owns today). The template
salt `0x2f6b4c1d` (795560989 in the contract), the `item-01 … item-12` catalog in the generator and
the oracle, the generator test's `/^item-\d{2}$/` id check and the vectors rows are template values:
replace them all before the first release (step 6 refills the rows). A generator keyed by text (a
share code) imitates `examples/starter-pack-share-code/`: `"keyKind": "text"`, every version
`from: null`, and callers always pass the version the code carries.
4. **Register it** in `contracts/seeded-generators.json` (template; keys in
[references/checkers.md](references/checkers.md)): module, exports, salt in decimal, versions,
vectors file, outcome file, regenerate command. An app with no generator keeps `"generators": []`.
5. **Write the oracle** from `templates/scripts/oracles/__FEATURE__-oracle.ts`: reimplement the
generator from its spec with BigInt or another language, no import from `src/`, its own copy of the
catalog and salt. Read [references/golden-vectors.md](references/golden-vectors.md).
6. **Pin the vectors.** The template rows belong to the template salt and catalog. Before the
generator first ships, delete each row's `fingerprint` and `summary` (keep `key`, `version` and
`note`) and set the contract's `salt` to the new salt in decimal. Add rows `{ "key", "version",
"note" }` (first key, a historical key, year end and new year, leap day), run
`node scripts/oracles/update-<feature>-vectors.ts` from the repo root, and read the diff. A
committed row is never refilled: `check-golden-vectors.mjs` fails it (rule `vectors-frozen`; the
template's seven rows are the one exception). Keep the vectors test and the oracle test from the
templates.
7. **Store outcomes, not content.** Persist `(key, generatorVersion, outcome)` as
`examples/daily-focus-outcomes/` shows: the repository port and row mapping in
`src/data/<feature>/`, the replay in `src/features/<feature>/`. persistence-and-migrations owns the
table. When the table exists, point the contract's `outcome.file` at the file that declares the
stored row. Read [references/versioning-and-regeneration.md](references/versioning-and-regeneration.md).
8. **To change a shipped generator**, follow "Shipping a new version, step by step" in
[references/versioning-and-regeneration.md](references/versioning-and-regeneration.md) and imitate
`examples/daily-focus-cutover/`: new version, future cutover, new oracle branch, new rows only.
`check-golden-vectors.mjs` fails a new cutover that starts today or earlier, and any edit of what
is committed (git `HEAD`): a moved or deleted cutover, a changed salt, a removed generator, a
changed or deleted vectors row. In CI pass the base branch: `--committed-ref origin/main`.
9. **Test:** `npx jest src/core src/features/<feature> scripts/oracles --ci --selectProjects unit`
and `npm run typecheck`.
10. **Run the checks:** `node ${CLAUDE_SKILL_DIR}/scripts/check-determinism.mjs .` and
`node ${CLAUDE_SKILL_DIR}/scripts/check-golden-vectors.mjs .` (in a workspaces repo, once per app,
with the app folder as the root); fix every `FAIL` line (file, rule, fix) and rerun until both
print `RESULT: PASS`.
11. **Commit** vectors and contract changes with a `Gate-Change:` trailer (git-commits-and-reporting)
naming the generator, the version and why; a changed row is never committed.
## Definition of done
- [ ] Every seeded output has a generator that takes its key as an argument and reads no clock,
ambient randomness or locale data; `clock.today()` is read once at the edge.
- [ ] The core files are the templates, unedited, with their tests pinning the oracle's goldens.
- [ ] No template value is left before the first release: own salt (in code, oracle and contract),
own catalog, own id check in the generator test, rows refilled by the oracle (the checker's
`template values still in use` note no longer prints).
- [ ] Each generator is registered with a unique frozen salt, its versions table, vectors file, outcome
file (or `null`) and regenerate command.
- [ ] Each vectors file was filled by the oracle (not the generator) and covers every version (date
keys: both sides of each cutover, a year-end/new-year pair, a leap day); no committed row changed
or vanished (`vectors-frozen`; without git the checker prints a `frozen checks:` note instead).
- [ ] Stored outcomes carry the key and `generatorVersion`, never the generated content.
- [ ] No CI job, hook or gate script runs a regenerate command.
- [ ] The core, generator, vectors and oracle tests pass; `npm run typecheck` is clean.
- [ ] `node ${CLAUDE_SKILL_DIR}/scripts/check-golden-vectors.mjs .` prints `RESULT: PASS` (allowed
while the outcome table does not exist yet:
`SKIP src/data/<feature>/<feature>-outcome-repository.ts [outcome-version] not yet created (persistence-and-migrations creates it)`;
an app without generators prints `NOT APPLICABLE: contracts/seeded-generators.json declares no seeded generators`).
- [ ] `node ${CLAUDE_SKILL_DIR}/scripts/check-determinism.mjs .` prints `RESULT: PASS`
## Anti-patterns
- **`new Date().toISOString().slice(0, 10)` inside a generator.** The UTC day, read from a clock: not
reproducible and wrong for hours every day. Take the key from `clock.today()` as an argument.
- **`CATALOG[dailySeed(key, SALT) % CATALOG.length]`.** Neighbouring days step through the catalog by a
fixed amount. `drawOne(CATALOG, seedRng(dailySeed(key, SALT)))`.
- **`Math.random()` for a harmless tie-break.** One call and no two devices agree. Draw it from the
threaded state.
- **Sorting with `localeCompare` before drawing.** iOS, Android and Jest order differently. Compare ids
with `<`.
- **Fixing a typo in a shipped catalog.** Every past key that drew it changes. Ship version 2 with a
future cutover.
- **Storing the generated list next to the outcome.** Two sources of truth. Store the version and
regenerate.
- **Pasting the generator's output into the vectors file, or `const GOLDEN = hashSeed(...)` in a test.**
The expected value then agrees with every bug. Compute it with the oracle.
- **Deleting a shipped row's values and re-running the update script to make a red test green.** That
rewrites history users saw. Restore the version that made the row. Refilling is only for rows no
released build has served, such as the template rows after you set your own salt.
- **`npm run vectors:update` (or `pnpm vectors:update`, or a script that calls it) in `verify`, CI, an
EAS Workflow or a hook (`simple-git-hooks`, `lint-staged`, husky).** The gate passes by
construction. Run it by hand and review the diff.
- **Moving a committed cutover to "fix" a date, or editing a committed salt in code and contract
together.** Both still agree, so only the committed contract shows the rewrite. Append a version with
a future date instead.
- **Deleting or renaming a shipped generator's entry to get past a red check.** Stored outcomes and
share codes still point at it. A rename is compared with the committed entry; a removal needs a
migration, the developer's approval and a `"retired"` entry.
- **A share-code generator that defaults to its newest version.** A version 1 code then opens as a
version 2 pack. Pass the version the code carries.
- **A helper in `src/utils/` that a generator imports and that reads `Date.now()` or calls `Math.exp`.**
`check-determinism.mjs` follows the generator's imports into it and names the chain; a package from
`node_modules` (a library `shuffle`) is caught only by the runtime trap. Pass the value in, and use
integer tables instead of inexact maths.
## Files in this skill
| File | What it is | Read/run when |
|---|---|---|
| [references/determinism-policy.md](references/determinism-policy.md) | Seeded paths, allowed operations and the spec/Hermes evidence, clocks, randomness, locale, ordering, floats, iOS/Android/Jest table, who enforces what, pitfalls | Workflow step 1, and when a lint or scan rule fires |
| [references/seeded-rng-and-seeds.md](references/seeded-rng-and-seeds.md) | sfc32 and its API, hash + salt + mix, the unmixed-seed trap in numbers, salts, threading, seeded draws, per-index hashing, the UI thread, the goldens and the Python oracle that computed them | Workflow steps 2 and 3 |
| [references/civil-date-keys.md](references/civil-date-keys.md) | DateKey, its integer arithmetic, one day definition, routing by key, the daily seed, pinned facts and their oracle, device behaviour | Workflow steps 2 and 3 |
| [references/golden-vectors.md](references/golden-vectors.md) | The vectors file, canonical JSON v1, coverage, the independent oracle, the fill-only update script, replacing the template rows before the first release, CI verifies, reading a diff, labels over metrics, a Python generator oracle | Workflow steps 5 and 6 |
| [references/versioning-and-regeneration.md](references/versioning-and-regeneration.md) | What to store and where (the outcome layout), replay by version, what counts as a change, the cutover procedure, choosing the date under store releases and OTA, text keys and share codes, retiring a generator | Workflow steps 7 and 8 |
| [references/checkers.md](references/checkers.md) | Both checkers' rules, the contract format, the committed baseline (`--committed-ref` in CI, renames, `"retired"`), layout options, multi-package use, expected SKIP/NOT APPLICABLE lines and notes, troubleshooting | Workflow steps 4 and 10 |
| `templates/src/core/rng/` | `sfc32.ts`, `seeded-draws.ts`, `fingerprint.ts` and their tests (synced, canonical) | Workflow step 2 |
| `templates/src/core/dates/` | `date-key.ts`, `daily-seed.ts`, `generator-versions.ts` and their tests (synced, canonical) | Workflow step 2 |
| `templates/src/features/__FEATURE__/` | The generator scaffold, its test, its vectors file and the vectors test | Workflow steps 3 and 6 |
| `templates/scripts/oracles/` | The BigInt oracle, its test (CI checks the committed rows) and the fill-only update script | Workflow steps 5 and 6 |
| `templates/contracts/seeded-generators.json` | The generator register both checkers read | Workflow step 4 |
| `examples/daily-focus-cutover/` | The Tasks app's daily-focus generator after a version 2 cutover: generator, vectors, oracle, contract | Workflow step 8 |
| `examples/daily-focus-outcomes/` | Storing `(key, generatorVersion, outcome)`: the repository port, the row schema and mapping (`src/data/`), and the replay that regenerates what the user saw (`src/features/`) | Workflow step 7 |
| `examples/starter-pack-share-code/` | A text-keyed generator (share codes `v2:7KQ2`): versions without cutover dates, explicit version on every call, a Python oracle with verify and `--fill` modes, its vectors | Workflow step 3, when the key is not a date |
| [examples/reminder-jitter.ts](examples/reminder-jitter.ts) | Stateless per-id randomness with `hashU32` (reminder offsets) | When each item needs its own stable random value |
| `scripts/check-determinism.mjs` | Runs the core against an independent implementation and the goldens; scans seeded paths and every local file they import | Workflow step 10, and at the end |
| `scripts/check-golden-vectors.mjs` | Runs every registered generator against its vectors with ambient APIs and inexact maths trapped; checks the register, what is committed (generators, salts, cutovers, vectors rows), coverage, oracle, test, outcome, no auto-bless | Workflow step 10, and at the end |
| `scripts/lib/determinism-spec.mjs` | The checkers' BigInt/Date.UTC implementation of the core, the golden table and the Math allow-list | When a golden, a core rule or the allow-list changes |
| `scripts/lib/module-graph.mjs` | The checkers' import reader: what a file loads (static, `import()`, `require()`, type-only), every local file a set of files reaches, and the enclosing workspaces repo of an app folder | When a checker follows imports |
| `scripts/lib/app-modules.mjs` | Shared helper (synced; do not edit here): imports the app's TypeScript for the runtime checks | Never by hand |
| `scripts/lib/source-scan.mjs` | Shared helper (synced; do not edit here): the source scanner (masking, imports, worklet directive) | Never by hand |
| `scripts/check-lib.mjs` | Shared script library (synced; do not edit here) | Never by hand |
| `scripts/selftest.mjs` | Proves both checkers pass the templates and catch every planted bug | After changing a checker, template or example |
| `assets/shared.json` | Declares the synced files (check-lib, helpers, the canonical core) | When adding a shared file |
| `assets/template-placeholders.json` | The placeholder values the template verification uses | When adding a placeholder |
| `tests/bases/app/` | The minimal app the fixtures lay the templates on | When the fixture base changes |
| `tests/fixtures/` | Overlay suites: `good/`, one `bad-*` per rule, `pass-*` and `error-*` cases for both checkers | When adding a checker rule |
## Related skills
- `dates-streaks-and-statistics` - reading today, local midnight, DST, streaks and statistics over date keys.
- `architecture-and-boundaries` - the `ClockPort` and where pure core code lives.
- `persistence-and-migrations` - the outcome table and retiring a generator version.
- `golden-and-snapshot-tests` - data snapshots and pixel goldens; `run-goldens-rebaseline` re-blesses those.
- `unit-and-component-tests` - the Jest projects, fast-check and the pinned `TZ=UTC`.
- `typescript-and-lint-rules` - the `DETERMINISTIC` and `PURE` lint blocks.
- `value-objects-money-and-units` - integers instead of floats for stored values.
- `i18n-strings-and-catalogs` - `Intl` formatting at the edge, never on a seeded path.
- `git-commits-and-reporting` - the `Gate-Change:` trailer for vectors and contract changes.
- `ota-updates` - runtime versions and rollouts that decide when a cutover is safe.
- `game-rules-engine`, `level-generation-and-solvers`, `realtime-game-loop` - seeded games built on this core.