Exception catalogue
Each card describes one L1 invariant kind: what it means, what to do when it fires, and a link to the dashboard sheet that surfaces the underlying matview. The dashboards open in App2 (the self-hosted renderer) so the link narrows to the kind on initial load.
-
drift Sub-ledger drift
For every CurrentStoredBalance where `Account.Scope = Internal` and `¬IsParent(Account)`, `Drift(Account, BusinessDay)` SHOULD equal 0.
Action. Diff the day's transactions for `account_id` against the stored balance — the gap is missing or duplicated postings on that account-day. Re-load the source feed for the account-day and refresh matviews.
Open dashboard sheet → -
ledger_drift Parent-account roll-up drift
For every CurrentStoredBalance where `Account.Scope = Internal` and `IsParent(Account)`, `LedgerDrift(Account, BusinessDay)` SHOULD equal 0.
Action. Sum the child accounts of `account_id` on `business_day_start` and compare to the parent's stored balance. The gap is a child posting that didn't propagate to the parent — usually a missing FK in the feed. Fix the parent link upstream, re-load, refresh.
Open dashboard sheet → -
overdraft Non-negative balance
For every CurrentStoredBalance, `money` SHOULD be ≥ 0.
Action. Trace `account_id`'s posting sequence on `business_day_end` to find the over-debit — usually a missing inbound credit or an over-issued debit. Reconcile against the source system and post a correction.
Open dashboard sheet → -
limit_breach Per-direction flow cap {: #5-per-direction-flow-cap}
For every CurrentStoredBalance where `Limits` is set, for every `(Rail, limit, direction)` in `Limits`, for every child Account whose `Parent = this account`, when `direction = Outbound` `OutboundFlow(child, rail, businessDay)` SHOULD be ≤ `limit`; when `direction = Inbound` `InboundFlow(child, rail, businessDay)` SHOULD be ≤ `limit`.
Action. Either the LimitSchedule cap is too low (raise it after confirming the day's volume is legitimate) or an upstream control failed. For Outbound breaches, audit the feed for over-sent volume — downstream beneficiaries may be undercredited until reconciled. For Inbound breaches, flag the source for review per the AML / KYC policy that motivated the cap (structuring, unexpected deposits, counterparty source diligence).
Open dashboard sheet → -
expected_eod_balance_breach Expected EOD
For every CurrentStoredBalance where `expected_eod_balance` is set, `money` SHOULD equal `expected_eod_balance`.
Action. Compare `stored_balance` against `expected_eod_balance` for the gap size — typically a delayed posting that should have landed before EOD. Verify the source-system posting time and re-time the posting if needed.
Open dashboard sheet → -
stuck_pending Per-rail pending aging
For every Rail with `max_pending_age` set, every Transaction on that rail SHOULD transition `Pending → Posted` before `posting + max_pending_age`.
Action. Either re-poke the source-system integration to transition the transaction, or raise the rail's `max_pending_age` if the cap is too aggressive for normal volume. Escalate to ops when `age_seconds` is in days rather than hours.
Open dashboard sheet → -
stuck_unbundled Per-rail unbundled aging
For every Rail with `max_unbundled_age` set, every Posted leg on that rail SHOULD be picked up by an AggregatingRail (`bundle_id` set) before `posting + max_unbundled_age`.
Action. Verify the AggregatingRail's `bundles_activity` still names this rail — the bundler may be silently mis-configured. For high `age_seconds`, run the bundle process manually and investigate why the regular cycle missed it.
Open dashboard sheet → -
chain_parent_disagreement Two-template chain Parent disagreement (AB.2.3) {: #chain-parent-disagreement}
For every two-template chain (chain.children resolves to a TransferTemplate), every leg_rail firing of one child Transfer SHOULD agree on which parent firing it descends from (first-firing-wins per gap doc §3).
Action. Identify which leg_rails of the child template wrote the conflicting `parent_transfer_id` values — usually the bug is upstream of the matview (in the ETL adapter that assigns `parent_transfer_id` from a stale reference). Compare the two parents by drilling into their respective transfer_ids on the Transactions sheet; one should be the correct first-firing parent and the other a contamination from an unrelated cycle. Fix the ETL's parent-resolution logic and re-run the affected child Transfer.
Open dashboard sheet → -
xor_group_violation Multi-mode template variant XOR violation (AB.3.3) {: #xor-group-violation}
For every TransferTemplate that declares `leg_rail_xor_groups`, for every group in that template, exactly ONE member of the group SHOULD fire per Transfer (the variants compete; the runtime picks one per cycle).
Action. Identify which template firing this is, then look at the upstream variant trigger. For missed firings (count=0): the template fired but no variant trigger ran — usually a routing bug where the runtime resolved to no variant. For overlaps (count≥2): two variant triggers both fired — usually a re-post that didn't suppress the original, or a race condition between two variant selection paths. Drill from the row to the Transactions sheet to see every leg of the Transfer, including the non-variant legs that DID fire (so you know which template firing the violation belongs to).
Open dashboard sheet → -
fan_in_disagreement Fan-in chain parent-set mismatch (AB.4.7) {: #fan-in-disagreement}
For every chain child entry declaring `fan_in: true` (AB.6 per-child shape), every child Transfer's contributing parent set SHOULD match the entry's `expected_parent_count` (when set), or have cardinality ≥2 (when unset).
Action. Identify which batch this is via the `business_day` + `child_template_name`. For 'missing': the upstream parent rail should have fired N times but one (or more) firings never landed — look at the parent rail's firing log for the batch period and find the missing date(s). For 'extra': a parent firing claimed membership in this batch it shouldn't have — could be a stale `parent_transfer_id` metadata value (ETL pulled from a wrong source) or a duplicate re-post. For 'orphan': only one parent contributed — either the chain shouldn't be `fan_in` (it's actually 1:1) OR the operator should set `expected_parent_count` so the matview can flag the missing siblings.
Open dashboard sheet → -
multi_xor_violation Chain XOR alternation violation (AB.6.5)
For every multi-children chain (≥2 children) — after stripping per-child `fan_in` entries — every parent firing SHOULD have exactly ONE non-fan_in child fire under it. Zero fires (none of the declared XOR alternatives followed the parent) and overlap (≥2 fired) both surface here.
Action. For 'missed': inspect the parent firing's `metadata` for clues about which alternative the operator intended. Either a child contribution failed to post (ETL bug on the child rail) or the L2 declaration over-specifies the children list (the intended alternative was never on the operator's menu). For 'overlap': two children fired for one parent — either the XOR contract is wrong (the children aren't truly alternates and the L2 should split into separate chain rows) or the seed-emit / ETL double-fired on a single firing. The drill from Today's Exceptions on a row navigates to Transactions filtered to the parent's `transfer_id` — eyeball the child legs to see which alternatives landed.
Open dashboard sheet → -
supersession_audit Supersession Audit
Action. Diagnostic only — supersession is expected for normal corrections. Investigate when `entry_count` is unusually high for the row's age, or when the rewrite reason is missing. Diff the entries to see what actually changed across versions.
Open dashboard sheet →