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

OverdraftGenerator(account_id, account_role, ...)

Emit a daily_balances row whose money is below zero by magnitude.

OverdraftInvariant([prefix])

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: object

Emit 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: object

Non-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)

detect(conn)[source]
Return type:

set[Violation]

Parameters:

conn (SyncConnection)

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:

OverdraftGenerator

Parameters:
  • role (str)

  • magnitude (float)

  • instance (L2Instance | None)

  • account_id (str | None)