recon_gen.common.spine.account_simulation
AccountSimulation — the AP.2 stateful-fold pattern, productionized.
A leaf internal account stepped forward day by day as a pure fold:
State -> (flows, State’). Each day’s emitted stored balance IS the
running State' (Σ recorded legs so far). Concrete
ViolationGenerator impls compose AccountSimulation when they need
multi-day choices; the simple DriftGenerator (AS.2, single-day)
stays unchanged.
What AP.2 proved + this module locks in:
The fold is PURE (_fold() returns per-day emissions; emit(conn) writes them). Separating the two lets the SAME fold drive an all-at-once write OR a day-by-day write with detect-between (the violation_trajectory carries the violation set as state).
Non-violating = the same simulation with
perturbations=(). Conformance is flow/state AGREEMENT, not the absence of activity (AP.2 finding Q2).Propagation is governed by which side you break, predictable from the detector SQL: state-snapshot blip is LOCAL; unrecorded-flow PROPAGATES forward. Pinned by AP.2 + reusable here.
Generators STACK: pass
perturbations=(p1, p2, ...); each adds its own violation to the carried set; existing ones persist.
AS.4 generalizes the State from a scalar balance to a vector dict[account_id, balance] (cross-account legs net to zero across accounts). AS.3 is the scalar foundation.
Classes
|
A leaf internal account stepped forward day by day. |
|
The materialized result of one folded step: the legs to write and the stored balance that IS the running |
|
One day's intended activity — signed leg amounts. |
|
How a single day's step deviates from the clean fold. |
- class recon_gen.common.spine.account_simulation.AccountSimulation(plans, perturbations=<factory>, account_id='acct-sim', account_role='CustomerSubledger', parent_role='CustomerLedger', opening_balance=0.0, prefix='spec_example', rng=<factory>, emit_legs=True)[source]
Bases:
objectA leaf internal account stepped forward day by day.
Authoring shape mirrors the AP.2 spike:
plansdeclare per-day flows,perturbationsdeclare the AP.2 perturbation knobs. The fold is PURE (_fold()is side-effect free);run(conn)writes the rows;violation_trajectory(inv, conn)carries the violation set as state day by day.Per the AS.1 RNG convention: every concrete generator that composes AccountSimulation passes an
rng(seeded via scenario_rng); AS.3 itself doesn’t randomize anything (the legs are author-declared), but the field carries forward for compose- time choices in AT’s anomaly/money_trail.- Parameters:
plans (list[DayPlan])
perturbations (list[Perturbation])
account_id (str)
account_role (str)
parent_role (str)
opening_balance (float)
prefix (str)
rng (Random)
emit_legs (bool)
- account_id: str = 'acct-sim'
- account_role: str = 'CustomerSubledger'
- emit(conn, *, scenario_id=None)[source]
Write the full fold to the connection in one pass.
AV.5:
scenario_idkwarg tags each emitted row’s metadata column for ScenarioContext cleanup attribution.None(the default) preserves the pre-AV.5 untagged emit shape.- Return type:
None- Parameters:
conn (SyncConnection)
scenario_id (str | None)
- emit_legs: bool = True
AS.4 — when False, the fold still computes per-day stored via Σ legs, but does NOT insert leg rows into _transactions. Right for parent-style ledger accounts that have no direct postings of their own — pure aggregators of child balances. Default True keeps every existing leaf-style use site unchanged. (Parents with mixed direct postings + child rollups also work; AO.L fixed _computed_ledger_balance to sum direct postings cumulatively across days, so the matview no longer relies on direct_totals=0 for clean drift.)
- opening_balance: float = 0.0
- parent_role: str = 'CustomerLedger'
- perturbations: list[Perturbation]
- prefix: str = 'spec_example'
- rng: Random
- violation_trajectory(invariant, conn)[source]
Run the fold day by day, refresh + detect after each day, return the per-day violation set. The carried-state shape from AP.2: each snapshot is the active violations as the institution reaches that day. The delta between consecutive snapshots IS each step’s effect (opened / closed / inert).
- class recon_gen.common.spine.account_simulation.DayEmission(day, legs, stored)[source]
Bases:
objectThe materialized result of one folded step: the legs to write and the stored balance that IS the running
State'.runiterates these;violation_trajectorywrites one at a time + refreshes + detects between.- Parameters:
day (date)
legs (tuple[tuple[str, float], ...])
stored (float)
- day: date
- legs: tuple[tuple[str, float], ...]
- stored: float
- class recon_gen.common.spine.account_simulation.DayPlan(day, legs)[source]
Bases:
objectOne day’s intended activity — signed leg amounts. A clean fold sets
State'.balance = State.balance + Σ legsand stores it.- Parameters:
day (date)
legs (tuple[float, ...])
- day: date
- legs: tuple[float, ...]
- class recon_gen.common.spine.account_simulation.Perturbation(kind='none', day_index=0, amount=0.0, correct_day_index=None)[source]
Bases:
objectHow a single day’s step deviates from the clean fold. The knob AP.2 surfaced — one shape, three kinds + a correction.
"none": clean — never emits anything on this day (no-op)."state_blip": corrupt ONLY the stored snapshot onday_indexbyamount(the running balance stays clean → drift is LOCAL to that day)."unrecorded_leg": emit an extra leg onday_indexthat is NOT folded into stored (flow/state DISAGREEMENT → drift PROPAGATES forward; computed is cumulative, stored stayed on the clean fold). Optionalcorrect_day_indexbooks the leg into stored on a later day (the AN.1 supersession shape — closes the forward propagation; the historical breach remains)."recorded_leg": emit an EXTRA real leg that DOES fold into state — a different-but-consistent history (conforming).
- Parameters:
kind (Literal['none', 'state_blip', 'unrecorded_leg', 'recorded_leg'])
day_index (int)
amount (float)
correct_day_index (int | None)
- amount: float = 0.0
- correct_day_index: int | None = None
- day_index: int = 0
- kind: Literal['none', 'state_blip', 'unrecorded_leg', 'recorded_leg'] = 'none'