recon_gen.common.spine.expected_eod

Expected-EOD-balance family — Invariant + ViolationGenerator.

ExpectedEodBalanceInvariant fires when a daily_balances row has a non-null expected_eod_balance AND money ≠ expected_eod_balance. The matview is a one-line variance check on <prefix>_current_daily_balances; like overdraft, no leg arithmetic, no parent dependency, no role join.

Per AU.0/AU.2 lessons (audit §5 “AU.2 result”):

  • Many-to-many edges are universal. A plant on a LEAF internal account satisfies drift’s matview filter (parent_role IS NOT NULL AND stored Σ legs: leaf has parent_role; emission has zero transactions ⇒ Σ legs = 0; planted money is expected + variance, so drift = stored − 0 = expected + variance ≠ 0). So (ExpectedEodBalanceInvariant, DriftInvariant) is the registered edge tuple — same shape as overdraft’s two-edge entry.

  • Lone parent plants are single-edge. A plant on a parent role (e.g. CustomerLedger) trips ONLY this invariant; _computed_ledger_balance requires children to exist (the EXISTS gate). ledger_drift fires only in COMPOSITION scenarios where another generator supplies the children.

What this module deliberately does NOT carry: an rng field on the generator (deterministic single-row plant; same as overdraft). The helpers stay module-private for now — AU.3 will hoist once the third balance-only invariant lands and the duplication becomes painful.

Classes

ExpectedEodBalanceGenerator(account_id, ...)

Emit a daily_balances row whose money is the configured expected ± variance, with expected_eod_balance = expected.

ExpectedEodBalanceInvariant([prefix])

Expected-EOD-balance detector.

class recon_gen.common.spine.expected_eod.ExpectedEodBalanceGenerator(account_id, account_role, account_parent_role, anchor_day, expected, variance, prefix='spec_example')[source]

Bases: object

Emit a daily_balances row whose money is the configured expected ± variance, with expected_eod_balance = expected. NO transactions — the variance matview reads daily_balances directly.

variance=0.0 ⇒ money == expected ⇒ no variance row materializes. Non-violating shape per the AP.2 convention.

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 ⇒ Σ legs = 0; planted stored = expected + variance ≠ 0). Registry records the two-edge entry.

Parameters:
  • account_id (str)

  • account_role (str)

  • account_parent_role (str | None)

  • anchor_day (date)

  • expected (float)

  • variance (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). Drift magnitude = stored − Σ legs = (expected + variance) − 0 = expected + variance.

Returns None when the planted account is NOT a leaf — drift’s matview filter excludes parent-role rows.

Type:

The empirical AU.0-style edge

anchor_day: date
property claimed_accounts: frozenset[str]

The single account_id this plant carries an EOD target for. AV.5.

emit(conn, *, scenario_id=None)[source]
Return type:

None

Parameters:
  • conn (SyncConnection)

  • scenario_id (str | None)

expected: float
property intended: RuleViolation
prefix: str = 'spec_example'
variance: float
class recon_gen.common.spine.expected_eod.ExpectedEodBalanceInvariant(prefix='spec_example')[source]

Bases: object

Expected-EOD-balance detector. Persona-blind — the matview SQL filters only on the per-row expected_eod_balance column being set and not matching money. scenario_for(role) accepts ANY internal account with the requested role (no leaf/parent filter).

Parameters:

prefix (str)

detect(conn)[source]
Return type:

set[Violation]

Parameters:

conn (SyncConnection)

name: ClassVar[str] = 'expected_eod_balance_breach'
prefix: str = 'spec_example'
scenario_for(role, *, expected=100.0, variance=5.0, instance=None, account_id=None)[source]

Resolve a role; return a generator that plants money = expected + variance with the per-row expected_eod_balance set, so the variance row materializes.

variance=0.0 is the non-violating shape (stored == expected ⇒ the matview row is filtered out). Same AP.2 convention as overdraft / drift.

Raises ValueError if the L2 has no internal account with the requested role.

AY.4.c — account_id overrides the default synthetic ID. The plant adapter (AY.4.c.3) threads OLD ExpectedEodBalancePlant.account_id through this kwarg so N plants on the same role produce N distinct generators (the default f”acct-eod-{role}” derivation would collide). Existing test callers can pass nothing → preserves the synthetic default byte-stable.

Return type:

ExpectedEodBalanceGenerator

Parameters:
  • role (str)

  • expected (float)

  • variance (float)

  • instance (L2Instance | None)

  • account_id (str | None)