recon_gen.common.spine.rng

Deterministic RNG factory for ViolationGenerator impls.

Every generator that makes a non-trivial choice — anomaly’s 20 quiet baseline pairs (which sender/recipient accounts, which amounts within a quiet range), money_trail’s chain of N hops (parent-linked transfer IDs), AS.4’s cross-account vector state (opening balances across many accounts) — needs randomness. Naive random.X() calls would break byte-identical replay across runs.

The convention this module locks in (matching the rest of the codebase — build_full_seed_sql(base_seed=…), random.Random(seed=…)):

  1. Each concrete ViolationGenerator that makes a random choice carries an rng: random.Random field on the dataclass.

  2. Each Invariant.scenario_for(…) accepts a seed: int | None = None kwarg; None resolves to SCENARIO_BASE_SEED (this module’s default).

  3. The generator constructs its RNG via scenario_rng(seed) — never calls random.Random() directly.

The tests/unit/test_typing_smells.py::determinism AST lint already rejects module-level random.X() calls in seed modules; this convention extends it to spine generators by funneling every construction site through one factory.

Composing multiple generators in one scenario: each gets its OWN rng, seeded from a derived value so the order of generator construction doesn’t affect the rng stream a downstream generator sees. The recommended derivation is seed + i where i is the generator’s position in the scenario. Locking the precedent here so AS.2 + AT don’t have to re-litigate.

Module Attributes

SCENARIO_BASE_SEED

Default seed for generators when the caller doesn't pin one.

Functions

scenario_rng([seed])

Build a seeded random.Random for a ViolationGenerator.

recon_gen.common.spine.rng.SCENARIO_BASE_SEED: Final[int] = 23053819629

Default seed for generators when the caller doesn’t pin one. Hex-spelled “spine seed” — uncommitted to a specific calendar date so swapping locked anchors doesn’t drag this with it.

recon_gen.common.spine.rng.scenario_rng(seed=None)[source]

Build a seeded random.Random for a ViolationGenerator.

seed=None uses SCENARIO_BASE_SEED — every scenario_for call site that doesn’t pin its own seed lands at the same starting point, so the locked-seed agreement (tests/data/test_locked_seeds.py shape) keeps holding.

Return type:

Random

Parameters:

seed (int | None)