Unify SQLite balance_assertions with Beancount Balance directives #27

Open
opened 2026-06-06 11:03:44 +00:00 by padreug · 0 comments
Owner

The dual-system problem

Libra currently has two parallel balance-assertion systems that don't know about each other:

  1. Libra's SQLite balance_assertions table (migrations.py:166): records assertions with a date, account, expected balance (sats + fiat), tolerance, and status. Checked by tasks.py:74 (daily reconciliation watchdog), reported via views_api.py:3126+ (reconciliation endpoints).
  2. Beancount's native Balance directive (beancount/core/data.py:177-203, confirmed against refs/repos/libra/beancount): "asserts that the declared account should have a known number of units of a particular currency at the beginning of its date … these assertions act as checkpoints." Validated by bean-check at parse time. Failures show up in Fava's UI as red flags.

These are two reconciliation mechanisms that can independently pass or fail. A Libra balance_assertion failure surfaces in the daily reconciliation report; a Beancount Balance directive failure surfaces in Fava and in bean-check output. There is no code path that knows about both, so a failure in one is invisible to the other. "All green" in the reconciliation watchdog can coexist with a red Beancount Balance failure that nobody on the Libra side sees.

Options

A. Unify upward (write Libra assertions as Beancount Balance directives)

When POST /api/v1/assertions/balance is called, post the assertion to Fava as a Balance directive (just another entry type in add_entries). Drop the SQLite balance_assertions table over time; existing rows become a one-time migration.

Pros:

  • Single source of truth, single failure surface.
  • Beancount validates assertions natively at parse time — no separate watchdog needed for that check.
  • Assertion history is in the journal (and #25's git history), so it inherits the audit story automatically.

Cons:

  • Beancount Balance directives are simpler than Libra's current model: they assert a balance at a date for one currency at a time. Libra's table carries multi-currency expected balance (sats + fiat) and a status workflow (pending / passed / failed). Multi-currency becomes multiple Balance directives at the same date.
  • Loses Libra-specific metadata (created_by, tolerance, audit history) unless mapped to Beancount meta keys. Probably fine — Beancount meta is flexible.

B. Keep both, unify the failure surface

Keep the SQLite table for Libra's workflow needs, but have the daily reconciliation watchdog also call bean-check (or equivalent) and surface any Beancount Balance failures in the same report.

Pros:

  • Minimal disruption to existing code.
  • Lets Libra keep richer assertion metadata.

Cons:

  • Doesn't solve the underlying "two systems" problem — still two writers, still drift risk. Just surfaces it.

C. Document and accept

The reviewer's framing: this is a "make sure the failure modes compose" concern, not architectural. Add a check in the daily reconciliation that also reports Beancount-side Balance directive failures, document that the two systems are intentionally parallel.

Recommendation

(A) — unify upward — is the right call long-term, particularly if the audit triplet (#26) lands, because by then the journal is the canonical record and putting assertions outside it is the same drift hazard the journal-mirror cleanup already removed for transactions. But (A) has UI consequences (the "create assertion" flow now writes to Fava, the assertion list reads from Fava entries instead of SQLite) and isn't a pure refactor.

Sequence-wise: independent of #24/#25/#26. Can land at any time, but most naturally goes after #25 lands so the assertion creation goes through the same git-backed commit path as everything else.

Scope

Defer the option choice to when this issue is picked up. Whichever option is taken, the deliverable includes:

  • A reconciliation report (likely extending tasks.py:scheduled_daily_reconciliation) that surfaces failures from both systems if both are kept, or that's the single source of truth if unified.
  • Documentation of the assertion model in CLAUDE.md.

Dependencies

  • Independent, but cleanest if landed after #25 (git-backed journal).
## The dual-system problem Libra currently has **two parallel balance-assertion systems** that don't know about each other: 1. **Libra's SQLite `balance_assertions` table** (`migrations.py:166`): records assertions with a date, account, expected balance (sats + fiat), tolerance, and status. Checked by `tasks.py:74` (daily reconciliation watchdog), reported via `views_api.py:3126+` (reconciliation endpoints). 2. **Beancount's native `Balance` directive** (`beancount/core/data.py:177-203`, confirmed against refs/repos/libra/beancount): *"asserts that the declared account should have a known number of units of a particular currency at the beginning of its date … these assertions act as checkpoints."* Validated by `bean-check` at parse time. Failures show up in Fava's UI as red flags. These are two reconciliation mechanisms that can independently pass or fail. A Libra `balance_assertion` failure surfaces in the daily reconciliation report; a Beancount `Balance` directive failure surfaces in Fava and in `bean-check` output. There is no code path that knows about both, so a failure in one is invisible to the other. "All green" in the reconciliation watchdog can coexist with a red Beancount `Balance` failure that nobody on the Libra side sees. ## Options ### A. Unify upward (write Libra assertions as Beancount Balance directives) When `POST /api/v1/assertions/balance` is called, post the assertion to Fava as a `Balance` directive (just another entry type in `add_entries`). Drop the SQLite `balance_assertions` table over time; existing rows become a one-time migration. Pros: - Single source of truth, single failure surface. - Beancount validates assertions natively at parse time — no separate watchdog needed for that check. - Assertion history is in the journal (and #25's git history), so it inherits the audit story automatically. Cons: - Beancount `Balance` directives are simpler than Libra's current model: they assert a balance at a date for *one* currency at a time. Libra's table carries multi-currency expected balance (sats + fiat) and a status workflow (`pending` / `passed` / `failed`). Multi-currency becomes multiple `Balance` directives at the same date. - Loses Libra-specific metadata (`created_by`, `tolerance`, audit history) unless mapped to Beancount meta keys. Probably fine — Beancount meta is flexible. ### B. Keep both, unify the failure surface Keep the SQLite table for Libra's workflow needs, but have the daily reconciliation watchdog also call `bean-check` (or equivalent) and surface any Beancount `Balance` failures in the same report. Pros: - Minimal disruption to existing code. - Lets Libra keep richer assertion metadata. Cons: - Doesn't solve the underlying "two systems" problem — still two writers, still drift risk. Just surfaces it. ### C. Document and accept The reviewer's framing: this is a "make sure the failure modes compose" concern, not architectural. Add a check in the daily reconciliation that also reports Beancount-side `Balance` directive failures, document that the two systems are intentionally parallel. ## Recommendation (A) — unify upward — is the right call long-term, particularly if the audit triplet (#26) lands, because by then the journal is the canonical record and putting assertions outside it is the same drift hazard the journal-mirror cleanup already removed for transactions. But (A) has UI consequences (the "create assertion" flow now writes to Fava, the assertion list reads from Fava entries instead of SQLite) and isn't a pure refactor. Sequence-wise: independent of #24/#25/#26. Can land at any time, but most naturally goes after #25 lands so the assertion creation goes through the same git-backed commit path as everything else. ## Scope Defer the option choice to when this issue is picked up. Whichever option is taken, the deliverable includes: - A reconciliation report (likely extending `tasks.py:scheduled_daily_reconciliation`) that surfaces failures from **both** systems if both are kept, or that's the single source of truth if unified. - Documentation of the assertion model in CLAUDE.md. ## Dependencies - Independent, but cleanest if landed after #25 (git-backed journal).
Sign in to join this conversation.
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
aiolabs/libra#27
No description provided.