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; plantedmoneyisexpected + 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
|
Emit a daily_balances row whose |
|
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:
objectEmit a daily_balances row whose
moneyis the configuredexpected±variance, withexpected_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 ≠ Σ legsis 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:
objectExpected-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)
- 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 + variancewith the per-rowexpected_eod_balanceset, so the variance row materializes.variance=0.0is 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:
- Parameters:
role (str)
expected (float)
variance (float)
instance (L2Instance | None)
account_id (str | None)