bridge — The Ledger Bridge · Area Specification
last conformance run: 4/4 scenarios pass · 6 acts mapped 2026-10-07 20:02 UTC
Spec
Status: 0.3. The hand-over to the ledger of record. 0.3 adds preliminary financials: a profit and loss and a balance sheet derived from the same journal, for the person and their accountant to look at before the close.
Saybooks owns operational truth and derives balanced journal lines from it (core_journal). The LEDGER OF RECORD — QuickBooks Online, Xero, an accountant's own system — owns the chart of accounts, manual journals, adjustments, and the close. The bridge is how the first feeds the second. We never compete with the ledger; we make it better fed than it has ever been.
1. Calibration: who this is for
The person keeping the books, and the accountant who receives them. The accountant supplies the chart: codes and names as their system spells them. The person hands over a period and gets a file their ledger imports. Neither has to re-key anything, and both can answer "what did the ledger already get?" from the record.
2. Scope
In: mapping onto their chart — our fixed derivation accounts, and (0.2) the person's own spending categories and the bank or card each statement came from; building the journal for a period in the shape the target imports (Xero manual journal CSV, QuickBooks Online journal entries CSV, plain CSV); recording every hand-over with its control totals and the exact file.
Both sides of the books reach the ledger: money in from invoicing and order to cash, and (0.2) money out from statements the agent read — spending, fees and receipts, each posting against the account it was paid from.
Out (§9): posting into the ledger over an API, pulling anything back, the chart itself, manual journals, adjustments, the close, currency conversion, cost of goods and inventory.
3. What it is
- Account mapping — our fixed derivation accounts (Accounts Receivable, Sales Revenue, Service Revenue, Sales Tax Payable, Cash, Customer Deposits, Customer Credits, Sales Returns & Allowances, Bad Debt Expense; later the p2p set: Inventory, GRNI, AP, COGS, price variance) mapped onto the customer's REAL chart. The mapping is workspace config, set once with the accountant, logged like everything else.
- Export formats — journal entries in the shapes the targets actually import (Xero manual-journal CSV; QBO journal import; plain CSV for everyone else). Exact field layouts to be pinned against live trials, not documentation.
- Hand-over tracking — what has been exported, through when. The journal itself stays a pure derivation (re-derivation is the truth); the bridge records the hand-over so month-end is a diff against last export, not a re-key and not a duplicate import. Late-arriving or voided facts after an export produce a visible correction entry in the next export, never a silent rewrite of the past.
3. What it is not
No chart of accounts of our own. No manual journals. No close. No bank feeds — the CSV import on unapplied cash is the honest boundary, and bank-feed integrations remain the moat we say out loud we have not crossed.
Acceptance test (the plan)
- Xero demo company: free, resettable, imports manual journals — the primary target. Export a month of the demo fixture's books, import, verify the trial balance ties to
core_journal's totals and AR ties too2c_ar_aging. - QBO trial account: same exercise against QBO's journal import; document plan-level quirks honestly (import availability differs by plan/region).
- The acceptance bar: an accountant who has never seen Saybooks can take one export file and land it in their ledger without asking us anything.
5. Entities and lifecycles
- map — one row per thing that needs a chart account, of three kinds: derivation (our fixed accounts), category (a word the person reviewed spending under) and source (the bank or card a statement came from). Each carries their code (Xero matches on it), their name (QuickBooks matches on it), the tax rate label their import expects, a note. Config, changed by a logged act. Changing it never rewrites a hand-over that already went out.
- export — one hand-over: format, period, entry and line counts, control totals, a hash, and the file exactly as it went out, at a capability link. Immutable.
6. The acts
Writes (2): map_account, export. Reads (4): accounts (our accounts, which the books use, how each is mapped, which formats are ready), preview (the period in the target's shape with its totals and anything unmapped; records nothing), exports (what has been handed over, through when), financials (a preliminary profit and loss for a period and a balance sheet at its end, with what they leave out). Environment: core's journal derivation, and whatever module produced the facts.
7. Invariants
- B-1 The journal is a derivation, never a posting. An export reads it and changes nothing in the books; re-derivation stays the truth.
- B-2 Money is integer minor units. Every export balances: debits equal credits, per entry and in total.
- B-3 One currency per hand-over. A period holding more than one is refused until a currency is named; sums never cross currencies.
- B-4 An export is refused while an account the period uses has no mapping the chosen format needs, naming the accounts and which field is missing.
- B-5 Every hand-over records its format, period, entry and line counts, control totals, a hash and the actor, and keeps the file exactly as it went out.
- B-6 Omitting the period start continues from the last hand-over in that format: month-end is a diff, not a re-key.
- B-7 Re-exporting a period that has already gone over is allowed and always visible: the result names the earlier hand-over and says whether the numbers have changed since.
- B-8 The chart is theirs. Codes and names come from the accountant; nothing is invented.
- B-10 Every statement row posts against the account it moved on, or is named as not posted. A reviewed purchase debits its category and credits the statement's account; a fee debits Bank Fees; money in debits that account and credits its category; a transfer debits or credits the other side it names. A transfer that names no other side posts nothing — which is how money already recorded elsewhere in these books stays out without being counted twice.
- B-11 Whatever does not post is named, every time. The preview and the hand-over both list the statement rows left out, with their value, because the ledger's bank balance will differ from the statement by exactly that much until they are dealt with.
- B-12 Preliminary statements are derived on request and never stored: a profit and loss for the period and a balance sheet at its end, one currency at a time, for a period that must be named. The balance sheet balances by construction: each statement account starts from the printed opening balance of its first statement (opening balance equity, as a bookkeeper would) and is checked against its latest printed closing balance, and the result says whether it ties. Transfers are placed by the person's own word for where the money went (another statement account, savings, an owner draw, a loan); a word the books cannot place is shown as unclassified and asked about, never guessed. The statements say on their face that they are preliminary and what they leave out: rows not reviewed, rows left out on purpose, an account that does not tie, the adjustments only the accountant makes.
- B-9 Every write is a logged act with an actor; refusals are logged too.
8. Conformance (scenarios)
01 the first hand-over: an invoice, a payment and its application; the accounts read shows what is in use and unmapped; the export is refused naming them; the accountant's codes are written; the preview is ready; the export records the hand-over and returns a link · 02 the month after: a second period continues from the last hand-over without a start date, a re-export of a period already sent is allowed and says so, and the plain CSV needs no chart at all · 03 the spending side: a card statement imported and reviewed; categories and the card account are mapped; a transfer posts nothing and an unreviewed row is reported as left out; the file carries the expense against the card · 04 preliminary financials: refused without a period and for a period that is not one; an issued invoice partly paid and a checking statement with software, a fee, a transfer to savings, an owner draw and the client's payment; the profit and loss, a balance sheet that balances and a checking account that ties to its statement; a later row left unreviewed is named and the account no longer ties, while the statements still balance.
9. Deferred — with reasons
| Item | Why deferred, not rejected |
|---|---|
| Posting over an API (QBO/Xero apps) | A file a person reviews and imports is the honest boundary for a system that is not the ledger. An API posting is a standing write permission into someone's books; it needs their app review and their consent, not ours. |
| Automatic correction entries | A fact that changes after a period went over shows as a visible re-export today. Deriving a reversing entry pair instead needs the accountant's convention, not ours. |
| Their chart, pulled | Reading the chart from the ledger would remove the typing, and it needs the same API connection as posting. |
| Cost of goods, inventory | Items carry no cost; the ledger of record owns margin. |
| Grouping entries | One entry per fact, so every posting traces to an invoice or a statement line. Summarising a month into one entry per account is a policy the accountant should choose. |
| A bank-transaction shape | Some ledgers would rather import statement rows than journal entries. Journal entries are correct double entry and one shape; the other is a format, addable when someone's import asks for it. |
| Currency conversion | Sums are per currency and never cross. A rate and a policy belong to the ledger. |
---
Change log: 0.3 (2026-10-07) — preliminary financials, from Aryeh Munk's phase 1 list ("Please generate preliminary Q1 financials"). 0.0-sketch (2026-08) — a goal on the record. 0.2 (2026-09-10) — the spending side: categories and statement accounts are mapped the same way, purchases contributes its own postings, transfers post nothing and unreviewed rows are reported rather than dropped in silence. 0.1 (2026-09-10) — built: the account map, three export shapes, hand-over tracking with control totals and the exact file. Prompted by an accounting firm asking whether it syncs with QuickBooks or Xero; the honest answer was "it derives the lines but cannot hand them over yet", so that was the thing to build. Layouts follow the published import templates and want one live trial each; the formats are a table in src/modules/bridge/views.js, so a correction is a data change.
Acts
| Act | Kind | Required |
|---|---|---|
map_account | write | account |
export | write | format to |
accounts | read | none |
preview | read | format to |
exports | read | none |
financials | read | none |
Invariants
| Id | Invariant |
|---|---|
B-1 | The journal is a derivation, never a posting: an export reads it and changes nothing. |
B-2 | Money is integer minor units; every export balances, per entry and in total. |
B-3 | One currency per hand-over; a period holding more than one is refused until a currency is named. |
B-4 | An export is refused while an account the period uses lacks the mapping the format needs, naming it. |
B-5 | Every hand-over records format, period, counts, control totals, a hash and the actor, and keeps the exact file. |
B-6 | Omitting the period start continues from the last hand-over in that format. |
B-7 | Re-exporting a period already sent is allowed and always visible, naming the earlier hand-over. |
B-8 | The chart is theirs: codes and names come from the accountant, never invented. |
B-9 | Every write is a logged act with an actor; refusals are logged too. |
B-10 | Spending posts against the account it was paid from; a transfer posts nothing, so money already recorded elsewhere is never counted twice. |
B-11 | An unreviewed row posts nothing and is reported as left out, in the preview and on the hand-over. |
B-12 | Preliminary statements are derived on request and never stored, balance by construction from the statements' printed openings, say whether each statement account ties, and name on their face what they leave out. |
Scenarios
Each scenario is a file of acts with expected outcomes. ok means the act must succeed with the listed fields; refused means the act must be refused with a sentence containing the listed text. Refusals are contract.
the first hand-over: map their chart, then send the period pass
set_company_profile, create_customer| # | Act | Expect | Why |
|---|---|---|---|
| 1 | draft_invoice | ok total=300000 | |
| 2 | issue_invoice | ok status="issued" | |
| 3 | record_payment | ok | |
| 4 | apply_payment | ok | |
| 5 | accounts | ok in_use=4 | receivable, service revenue, cash and deposits are in use; nothing is mapped yet |
| 6 | export | refused account code | B-4: Xero matches on the code and there is none |
| 7 | map_account | ok | |
| 8 | map_account | ok | |
| 9 | map_account | ok | |
| 10 | map_account | ok account="Customer Deposits" | |
| 11 | preview | ok ready=true entry_count=3 line_count=6 | three entries: the invoice, the receipt, the application |
| 12 | export | ok entry_count=3 line_count=6 | B-5: recorded with its control totals and the file kept |
| 13 | exports | ok count=1 |
the month after: continue from the last hand-over, and re-send visibly pass
set_company_profile, create_customer| # | Act | Expect | Why |
|---|---|---|---|
| 1 | draft_invoice | ok | |
| 2 | issue_invoice | ok | |
| 3 | map_account | ok | |
| 4 | map_account | ok | |
| 5 | export | ok entry_count=1 | |
| 6 | export | refused already exported through | B-6: nothing new after the last hand-over |
| 7 | export | ok entry_count=1 | B-7: allowed, and the result names the earlier hand-over |
| 8 | export | ok | the plain CSV needs no chart at all |
| 9 | exports | ok count=3 |
the spending side: a card statement reaches the ledger pass
set_company_profile| # | Act | Expect | Why |
|---|---|---|---|
| 1 | import_statement | ok rows_in=5 | the agent read the statement; it reconciles to the printed balances |
| 2 | review_batch | ok reviewed=4 still_unreviewed=1 | the Adobe row is left unreviewed on purpose; the deposit is a transfer that says where it went |
| 3 | preview | ok entry_count=4 | B-10 the transfer names no other side so it posts nothing and is listed; B-11 the unreviewed row is listed too |
| 4 | export | refused account name | B-4: the categories and the card have no chart account yet |
| 5 | map_account | ok kind="category" | |
| 6 | map_account | ok | |
| 7 | map_account | ok | |
| 8 | map_account | ok kind="source" | |
| 9 | map_account | ok kind="derivation" | |
| 10 | map_account | refused no category | B-8: only words the books actually carry |
| 11 | preview | ok ready=true entry_count=4 line_count=8 | |
| 12 | export | ok entry_count=4 | B-11: the hand-over names what it left out, with its value |
| 13 | exports | ok count=1 |
preliminary financials, said on their face pass
set_company_profile, create_customer| # | Act | Expect | Why |
|---|---|---|---|
| 1 | financials | refused need a period | B-12 a statement is about a period |
| 2 | financials | refused is not a period | |
| 3 | draft_invoice | ok | |
| 4 | issue_invoice | ok | |
| 5 | record_payment | ok | |
| 6 | apply_payment | ok | |
| 7 | import_statement | ok rows_in=5 | |
| 8 | review_batch | ok reviewed=5 | |
| 9 | financials | ok balanced=true income_total=300000 expense_total=7499 net_income=292501 total_assets=1242501 total_liabilities=0 unreviewed_in_period=0 | B-12 revenue 3,000.00 less software and the fee; checking 10,425.01 ties to its statement with the invoice payment counted where it landed; savings an asset, the owner draw against equity |
| 10 | import_statement | ok | |
| 11 | financials | ok balanced=true net_income=292501 unreviewed_in_period=1 | B-12 an unreviewed row is named, not counted; the checking account no longer ties and says so, and the statements still balance |
These pages are generated from the files in specs/ in the repository; scenarios are executed by src/conformance.js on every build. Written and maintained by Peter Varga, who also runs Portland AI Works. github.com/alviso/saybooks, AGPL-3.0.