L1 Invariants¶
The L1 SPEC declares a small set of SHOULD-constraints that any
healthy ledger feed must satisfy. The L1 library
(common.l2.emit_schema) materializes each constraint as a
prefixed PostgreSQL view; rows in any of these views ARE the
constraint violations. Healthy = empty.
This page is the authoritative reference for what each
spec_example_* view returns, the SHOULD-constraint motivation, and
what scripts/m2_6_verify.py asserts against the canonical
demo seed.
How the views are layered¶
base tables
├── spec_example_transactions
└── spec_example_daily_balances
↓
Current* matviews (M.1.5 — max-Entry-per-logical-key projection)
├── spec_example_current_transactions
└── spec_example_current_daily_balances
↓
Helper matviews (computed-balance derivation)
├── spec_example_computed_subledger_balance
└── spec_example_computed_ledger_balance
↓
L1 invariant matviews (the SHOULD-constraint surfaces)
├── spec_example_drift
├── spec_example_ledger_drift
├── spec_example_overdraft
├── spec_example_expected_eod_balance_breach
├── spec_example_limit_breach
├── spec_example_stuck_pending (M.2b.8)
└── spec_example_stuck_unbundled (M.2b.9)
↓
Dashboard-shape matviews (UI convenience)
├── spec_example_daily_statement_summary
└── spec_example_todays_exceptions (UNION over the 5 baseline L1s)
13 matviews total. Refresh contract: every batch insert into the
base tables MUST be followed by refresh_matviews_sql(instance)
to recompute every dependent matview in dependency order. The
refresh is deterministic — leaves first, helpers second, L1
invariants third, dashboard-shape last.
The seven L1 SHOULD-constraints¶
1. spec_example_drift — Sub-ledger drift¶
For every CurrentStoredBalance where
Account.Scope = Internaland¬IsParent(Account),Drift(Account, BusinessDay)SHOULD equal 0.
Each leaf-account day where the stored balance disagrees with the cumulative net of every Posted Money record posted to that account through the BusinessDay's end. The disagreement is the drift; a non-zero value signals the feed diverged from the underlying ledger.
Columns: account_id, account_name, account_role,
account_parent_role, business_day_start, business_day_end,
stored_balance, computed_balance, drift.
2. spec_example_ledger_drift — Parent-account roll-up drift¶
For every CurrentStoredBalance where
Account.Scope = InternalandIsParent(Account),LedgerDrift(Account, BusinessDay)SHOULD equal 0.
Each parent-account day where the stored balance disagrees with the sum of its child accounts' stored balances. Surfaces a child posting that didn't roll up correctly to its parent.
Columns: same as _drift minus account_parent_role
(parents ARE the parents).
3. spec_example_overdraft — Non-negative balance¶
For every CurrentStoredBalance,
moneySHOULD be ≥ 0.
Each internal account day where the stored balance is negative. External counterparties are excluded by construction (the asymmetry is intentional: banks may legitimately overdraft us; we MUST NOT overdraft them).
Columns: account_id, account_name, account_role,
account_parent_role, business_day_start, business_day_end,
stored_balance.
4. spec_example_expected_eod_balance_breach — Expected EOD¶
For every CurrentStoredBalance where
expected_eod_balanceis set,moneySHOULD equalexpected_eod_balance.
Each account day where the stored EOD balance differs from the L2-declared expected EOD balance. Surfaces accounts that didn't clean up to their expected zero / target by end-of-day.
Columns: account_id, account_name, account_role,
business_day_start, business_day_end, stored_balance,
expected_eod_balance, variance.
5. spec_example_limit_breach — Outbound flow cap¶
For every CurrentStoredBalance where
Limitsis set, for every(TransferType, limit)inLimits, for every child Account whoseParent = this account,OutboundFlow(child, type, businessDay)SHOULD be ≤limit.
Per-(account, day, transfer_type) cells where cumulative
outbound debit exceeded the cap. Caps come from the L2 instance's
LimitSchedules and are inlined into the view as CASE branches at
schema-emit time.
Columns: account_id, account_name, account_role,
account_parent_role, business_day, transfer_type,
outbound_total, cap.
6. spec_example_stuck_pending — Per-rail pending aging (M.2b.8)¶
For every Rail with
max_pending_ageset, every Transaction on that rail SHOULD transitionPending → Postedbeforeposting + max_pending_age.
Transactions stuck in status='Pending' past their rail's
configured cap. Caps come from the per-Rail max_pending_age
field and are inlined as CASE branches keyed on rail_name. Rails
without an aging watch contribute no branch and are excluded.
Columns: transaction_id, account_id, account_name,
account_role, account_parent_role, transfer_id,
transfer_type, rail_name, amount_money, amount_direction,
posting, max_pending_age_seconds, age_seconds.
7. spec_example_stuck_unbundled — Per-rail unbundled aging (M.2b.9)¶
For every Rail with
max_unbundled_ageset, every Posted leg on that rail SHOULD be picked up by an AggregatingRail (bundle_idset) beforeposting + max_unbundled_age.
Posted transactions whose bundle_id IS NULL past their rail's
cap. Per validator R8, max_unbundled_age is only meaningful on
rails appearing in some AggregatingRail's bundles_activity.
Columns: same shape as _stuck_pending with
max_unbundled_age_seconds instead of max_pending_age_seconds.
Diagnostic surface — Supersession Audit¶
spec_example_supersession_* is not a SHOULD-constraint — it's a
diagnostic view that surfaces logical keys with multiple entry
versions (the audit trail for TechnicalCorrection /
BundleAssignment / Inflight rewrites). Reads from BASE tables
(not Current) since Current hides superseded entries by
construction. See M.2b.12 dashboard for the visualization.
Refresh + extend contracts¶
- Refresh:
refresh_matviews_sql(instance)returns a single SQL string with 26 statements (13 REFRESH + 13 ANALYZE) in dependency order. Caller splits on;and executes per-statement (psycopg2'scursor.executecan't run multiple statements separated by;reliably). - Per-instance hot-path indexes: every L1 invariant matview
ships indexes on the dashboard's filter dropdowns
(
account_id,rail_name,business_day, etc) so the deployed dashboard's per-visual SELECTs hit indexed lookups. - Adding a new SHOULD-constraint: declare the underlying L1
primitive in the SPEC, add the matview to
common.l2.schema._L1_INVARIANT_VIEWS_TEMPLATE, register it in_L1_INVARIANT_VIEWS_DROPS_TEMPLATE+refresh_matviews_sql, and write a_render_<name>_caseshelper if it needs L2 data inlined at schema-emit time (mirror of_render_pending_age_cases). Then surface it via a new dataset - sheet on the L1 dashboard.
See also¶
- Schema v6 — Data Feed Contract — the column contract for the two base tables.
- L1 Reconciliation Dashboard — the analyst's view of these invariants.
- Customization Handbook — L2-fed pattern — how to point the dashboard at your own L2 instance.