recon_gen.common.spine.ledger_simulation
LedgerSimulation — vector-state composition of AccountSimulation`s plus the AT.3 `Transfer primitive for cross-account flow.
AS.4 landed the vector-of-scalar-folds shape: each AccountSimulation steps its own balance forward independently, the cross-boundary invariants (ledger_drift’s Σ child.money roll-up) emerge from the matview SQL. AT.3 adds the Transfer primitive — multi-leg events that share a transfer_id (and optionally chain via parent_transfer_id).
What lives here:
AccountSimulation composition — the AS.4 surface; emit runs each account’s fold, violation_trajectory carries the cross-account violation set as state day by day.
TransferLeg + Transfer (AT.3) — the cross-account flow primitive. Each Transfer is one event (one transfer_id); legs are per-account amounts that sum to zero for a fully-balanced Posted transfer (the conservation law). Single-leg or unbalanced transfers are representable — useful for external arrivals and Pending entries.
LedgerSimulation.transfers (AT.3) — emitted alongside (or instead of) account folds. Anomaly’s pair-shaped plant is “transfers only, no folds” (no balance rows, single-edge to anomaly per AT.0’s finding). Money_trail’s recursive chain is “transfers with parent_transfer_id linkage”.
What this deliberately does NOT do:
Auto-route transfer legs into per-account DayPlan.legs. The two flow shapes — AccountSimulation.plans (account-as-source-of-truth) vs Transfer.legs (transfer-as-source-of-truth) — are kept side-by-side. A scenario that wants both (transfers + balance rows matching the transfer-induced sums) composes both. The matview SQL is the consistency contract, not the in-process emit logic.
State is the vector of per-account scalar balances PLUS the set of emitted transfers; conservation laws apply per-transfer (legs sum to zero across accounts) but are not enforced at construction — the matview is the truth-source, the generator is honest.
Classes
|
A vector of `AccountSimulation`s sharing one connection, plus an optional list of cross-account `Transfer`s. |
|
A multi-leg money-movement event sharing one |
|
One leg of a Transfer — money in or out of one account. |
- class recon_gen.common.spine.ledger_simulation.LedgerSimulation(accounts=<factory>, transfers=<factory>, prefix='spec_example')[source]
Bases:
objectA vector of `AccountSimulation`s sharing one connection, plus an optional list of cross-account `Transfer`s.
Each per-account fold is independent (scalar); the LEDGER’s cross-account behavior emerges from the matview SQL — e.g., ledger_drift’s Σ child.money reads every account’s _current_daily_balances row. So vector state here is “many scalar folds emitted side by side”; the cross-boundary invariants pick up the structural property from the data.
transfers (AT.3) is the cross-account FLOW dimension — transfer-shaped emissions (multi-leg, shared transfer_id, optional parent_transfer_id for chains). Anomaly’s pair plant is “transfers only, no accounts” (no balance rows → single-edge to anomaly). Money_trail’s recursive chain is “transfers with parent linkage”. Drift-style scenarios continue to use the account-fold shape; a scenario that wants both composes both fields.
Per the AS.1 RNG convention: each composed AccountSimulation carries its own seeded rng. LedgerSimulation doesn’t override.
- Parameters:
accounts (list[AccountSimulation])
transfers (list[Transfer])
prefix (str)
- accounts: list[AccountSimulation]
- emit(conn, *, scenario_id=None)[source]
Write every account’s full fold AND every transfer’s legs. Commits to the caller — so a scenario can compose multiple LedgerSimulations against one connection and refresh once at the end (the AP.3 pattern).
AV.5:
scenario_idkwarg threads through to the per-row metadata tag ({"scenario_id": "..."}) when set;Nonepreserves the pre-AV.5 untagged emit (byte-identical).- Return type:
None- Parameters:
conn (SyncConnection)
scenario_id (str | None)
- prefix: str = 'spec_example'
Prefix for the <prefix>_transactions table when emitting transfers. For account-only ledgers this is unused (each AccountSimulation carries its own prefix). For transfer-only ledgers (anomaly’s shape) this is the source of truth.
- violation_trajectory(invariant, conn)[source]
Per-day violation snapshots, like AccountSimulation’s but emitting all accounts’ rows for day i BEFORE refresh+detect.
Assumes every AccountSimulation in the ledger has the SAME number of plans (one DayPlan per ledger-day). Heterogeneous timelines would need a different fold; AT.x extensions can add a sparser shape when there’s a use case.
- class recon_gen.common.spine.ledger_simulation.Transfer(day, transfer_id, rail_name, legs, status='Posted', parent_transfer_id=None, origin='etl', hour=12)[source]
Bases:
objectA multi-leg money-movement event sharing one
transfer_id.The double-entry invariant:
sum(leg.amount for leg in legs) == 0for a fully-balanced Posted transfer. NOT enforced at construction — Pending transfers commonly carry only one leg until they post; the matview SQL filters on Posted + matched legs, so an unbalanced transfer is harmless (it just doesn’t surface). The is_balanced helper exposes the check for callers that want it.parent_transfer_id chains transfers into trails — money_trail’s matview walks this recursively (each Posted multi-leg transfer becomes one edge per chain hop).
- Parameters:
day (date)
transfer_id (str)
rail_name (str)
legs (tuple[TransferLeg, ...])
status (Literal['Posted', 'Pending'])
parent_transfer_id (str | None)
origin (str)
hour (int)
- day: date
- hour: int = 12
Hour of day used for the
postingtimestamp on each leg. Defaults to noon so each leg lands inside the day’s balance window (00:00 ≤ posting < 24:00). Mirrors ts()’s convention.
- is_balanced()[source]
Trueiffsum(leg.amount) == 0— the double-entry conservation law. Used by callers (incl. tests) that want to verify a transfer is fully-resolved; the constructor doesn’t enforce it because Pending single-leg transfers are valid intermediate state.- Return type:
bool
- legs: tuple[TransferLeg, ...]
- origin: str = 'etl'
- parent_transfer_id: str | None = None
- rail_name: str
- status: Literal['Posted', 'Pending'] = 'Posted'
- transfer_id: str
- class recon_gen.common.spine.ledger_simulation.TransferLeg(account_id, amount, account_name, account_role, account_scope, account_parent_role=None)[source]
Bases:
objectOne leg of a Transfer — money in or out of one account.
amount > 0 = money IN (Credit), < 0 = money OUT (Debit) — the project sign convention. The denormalized account-side fields (name, role, scope, parent_role) MUST be on each leg because _transactions is the source-of-truth table for the matviews; the matview SQL doesn’t JOIN to a separate account dimension.
- Parameters:
account_id (str)
amount (float)
account_name (str)
account_role (str)
account_scope (Literal['internal', 'external'])
account_parent_role (str | None)
- account_id: str
- account_name: str
- account_parent_role: str | None = None
- account_role: str
- account_scope: Literal['internal', 'external']
- amount: float