recon_gen.common.spine.overdraft
Overdraft family — concrete Invariant + ViolationGenerator impls.
OverdraftInvariant fires when an internal account’s stored balance
goes negative. The matview is a one-line filter on
<prefix>_current_daily_balances — no leg arithmetic, no parent
dependency, no role join. Structurally the simplest L1 invariant after
drift.
The AU.0 spike (tests/unit/test_au0_overdraft_full_spine.py) caught
a real finding: an overdraft planted on a LEAF internal account ALSO
trips DriftInvariant. Mechanism — drift’s matview filter is
parent_role IS NOT NULL AND stored ≠ Σ posted legs. The
overdraft plant satisfies both (the leaf has a parent_role; the plant
emits stored=−magnitude with ZERO transactions, so Σ legs = 0 ≠
−magnitude). The edge falls out of overlapping base-table predicates
between two independent matview SELECTs — it’s not drift-specific
exotica.
So AU.1’s INVARIANT_GENERATOR_EDGES entry for OverdraftGenerator is
(OverdraftInvariant, DriftInvariant): two edges, same shape as
drift’s (DriftInvariant, LedgerDriftInvariant).
What this module deliberately does NOT carry:
An rng field on OverdraftGenerator. Overdraft’s emission is fully determined by construction params (one balance row, magnitude scalar); no randomization surface. Drift accepts rng for structural uniformity across the spine; overdraft has no use for it. AT’s anomaly generator will actually use the RNG.
A stateful day-by-day fold. Overdraft is a single-row witness; no carried state across days; the AccountSimulation AS.3 base class is for invariants with running balance.
Cross-account composition (AS.4’s LedgerSimulation). Overdraft is per-account; AU.2’s composition test wires it into a LedgerSimulation alongside DriftGenerator for the spine-scales-past-one-invariant gate.
Classes
|
Emit a daily_balances row whose money is below zero by magnitude. |
|
Non-negative-stored-balance detector. |
- class recon_gen.common.spine.overdraft.OverdraftGenerator(account_id, account_role, account_parent_role, anchor_day, magnitude, prefix='spec_example')[source]
Bases:
objectEmit a daily_balances row whose money is below zero by magnitude. NO transactions — overdraft’s matview reads current_daily_balances directly; only the balance row is needed.
Per the AP.2 convention: magnitude=0.0 means the perturbation is OFF; the emitted row has money=0, which is NOT < 0, so overdraft does NOT fire. The non-violating shape is the same generator with the knob off.
AU.0 finding: on a LEAF internal account (account_parent_role != None), this emission ALSO trips DriftInvariant because drift’s matview filter parent_role IS NOT NULL AND stored ≠ Σ legs is satisfied (no transactions emitted ⇒ Σ legs = 0 ≠ −magnitude). The registry records the two-edge entry.
- Parameters:
account_id (str)
account_role (str)
account_parent_role (str | None)
anchor_day (date)
magnitude (float)
prefix (str)
- account_id: str
- account_parent_role: str | None
- account_role: str
- property also_trips_drift: RuleViolation | None
drift fires on the same account/day when the planted account is a LEAF (account_parent_role is set). Returns None when the planted account is NOT a leaf (drift’s parent_role IS NOT NULL filter excludes it).
Magnitude sign: drift = stored − Σ legs = −magnitude − 0 = −magnitude.
- Type:
The empirical AU.0 edge
- anchor_day: date
- property claimed_accounts: frozenset[str]
The single account_id this plant overdrafts. AV.5.
- emit(conn, *, scenario_id=None)[source]
- Return type:
None- Parameters:
conn (SyncConnection)
scenario_id (str | None)
- property intended: RuleViolation
- magnitude: float
- prefix: str = 'spec_example'
- class recon_gen.common.spine.overdraft.OverdraftInvariant(prefix='spec_example')[source]
Bases:
objectNon-negative-stored-balance detector. Persona-blind — the matview SQL is WHERE money < 0 on every internal account, no role join. scenario_for(role) filters the L2 by role only; ANY scope=internal account qualifies (no parent_role requirement that drift carries).
- Parameters:
prefix (str)
- name: ClassVar[str] = 'overdraft'
- prefix: str = 'spec_example'
Prefix of the deployed L2 instance’s matviews. Same default + per-call override pattern drift uses.
- scenario_for(role, *, magnitude=5.0, instance=None, account_id=None)[source]
Resolve a role against the shape; return a generator that manufactures a stored-balance overdraft on the first internal account with that role.
magnitude is caller-facing (“how far below zero the planted stored is” — positive). magnitude=0.0 plants stored=0 which is NOT < 0, so overdraft does NOT fire — AP.2’s non-violating convention promoted to overdraft.
Raises ValueError if the L2 has no internal account with the requested role. Smart-constructor discipline matching drift’s: the invariant owns shape resolution, fails loud at the request site, never silently emits inert rows.
instance=None loads the bundled spec_example — production callers (deploy-time, e2e fixtures) thread the real L2.
AY.4.c — account_id overrides the default synthetic ID. The plant adapter (AY.4.c.3) threads OLD OverdraftPlant.account_id through this kwarg so N overdraft plants on the same role produce N distinct generators (the default f”acct-overdraft-{role}” derivation would collide). Existing test callers can pass nothing → preserves the synthetic default byte-stable.
- Return type:
- Parameters:
role (str)
magnitude (float)
instance (L2Instance | None)
account_id (str | None)