spb/air Public MIT
AIR — The Language of Accounting.
Python 100%
1<!--2Projet : AIR — Accounting Intermediate Representation3Auteur : Simon-Pierre Boucher4Contact : contact@spboucher.ai5Fichier : air-spec-v0.1.md6-->78# AIR Specification v0.1 — The Language of Accounting910AIR (Accounting Intermediate Representation) is to accounting what LLVM IR is11to compilation: a universal intermediate language. AIR describes **economic12events** — what actually happened in the world — and a deterministic compiler13(AIC) lowers them into journal entries under versioned rule sets (ALSL).1415The core contract:1617> **An LLM (or any frontend) may only ever produce AIR. It never produces a18> journal entry.** The compiler applies taxes, standards, and policies19> deterministically, traceably, and testably.2021AIR is **self-sufficient**: with the native backend, AIR is a complete22standalone accounting system (hash-chained ledger + general ledger, trial23balance, income statement, balance sheet in text/markdown/csv/json). Third24party systems (QuickBooks, Xero, Odoo, SAP...) are optional export targets.2526## 1. Document form2728An AIR document is YAML or JSON validated against29`schemas/air-0.1.schema.json`. Three isomorphic forms exist (as in LLVM):30in-memory typed objects (`core/events.py`), canonical JSON, and readable YAML.3132```yaml33air_version: "0.1"34events:35 - id: evt_01H8XGJWBWBAQ4Z136 type: Sale37 date: 2026-07-2038 parties: {seller: "company:acme", buyer: "customer:cust_123"}39 items:40 - {sku: chair-std, qty: "3", unit_price: {amount: "333.33", currency: CAD}}41 payment: {method: card.visa, immediate: true}42 tax: {jurisdiction: CA-QC, codes: [GST, QST]}43 meta:44 source: {kind: invoice_pdf, uri: "s3://...", ocr_score: "0.97"}45 llm: {model: "claude-fable-5", confidence: "0.93"}46 policy_version: "2026.08"47```4849### 1.1 Lexical rules5051- **Amounts, quantities, rates are strings** (`"333.33"`). Bare numbers would52 be parsed as binary floats and are **rejected** (ADR 0003).53- `id` is unique per document (ULIDs recommended).54- Dates are ISO 8601.55- Unknown fields are rejected (`extra="forbid"`): the schema is the contract.5657### 1.2 Event types (v0.1)5859| Type | Meaning | Posting shape (native profile) |60|---|---|---|61| `Sale` | value delivered to a buyer | DR cash/AR total · CR revenue · CR tax payables |62| `Purchase` | value received from a vendor | DR expense/asset (+ non-recoverable tax) · DR recoverable taxes · CR cash/AP |63| `Refund` | reversal of a sale | mirror of Sale |64| `PaymentReceived` | settles a receivable | DR cash · CR AR · ± realized FX |65| `PaymentSent` | settles a payable | DR AP · CR cash · ± realized FX |66| `OwnerContribution` | equity injection | DR cash · CR owner capital |67| `LoanReceived` | debt proceeds | DR cash · CR loan payable |6869Semantic notes:7071- **No account, no debit, no credit appears in AIR.** Events are72 perspective-neutral (ADR 0002): the same event compiles to the seller's or73 buyer's books depending on the compiling entity's policy set.74- `tax.jurisdiction` names *where* the supply takes place; rates live only in75 ALSL policies.76- `fx.rate` is **observed input data** (e.g. a Bank of Canada Valet daily77 rate on the transaction date), never a constant of the system.7879## 2. Provenance — accounting SSA8081Every amount in a compilation is defined exactly once as a node in an82append-only DAG (`core/provenance.py`). Derivations record their operation:8384```85prov_000000 event_subtotal subtotal:Sale 999.99 CAD <- evt_...86prov_000001 tax tax:GST@0.05~half_up 50.00 CAD <- prov_00000087prov_000002 tax tax:QST@0.09975~half_up 99.75 CAD <- prov_00000088prov_000005 journal_line post:credit:2320 99.75 CAD <- prov_00000289```9091From any posted line one can walk back to the source event, its document URI,92its OCR/LLM confidence, and the exact policy and rounding mode applied.9394## 3. The AIC pipeline9596```97ValidationPass -> ClassificationPass -> TaxPass -> FxPass -> PostingPass98```99100After **every** pass the verifier checks (a) each entry balances per currency101and (b) the accounting equation102`Assets = Liabilities + Equity + (Revenue − Expenses)`. Any violation aborts103with clang-style diagnostics (code, location, cause, `help:` fix), e.g.:104105```106error[AIR-E400]: no tax policy in set 'ca-qc' matches jurisdiction 'CA-BC'107 --> event evt_x, field tax.jurisdiction108 pass: tax109 help: add an ALSL tax policy for this jurisdiction or mark the event tax.exempt: true110```111112Determinism: same document + same policy set = byte-identical journal. No113LLM, network, or clock inside the compiler (property-tested).114115### 3.1 Diagnostic codes116117| Code | Meaning |118|---|---|119| AIR-E100/E101/E102 | empty entry / unbalanced entry / accounting equation violated |120| AIR-E200..E203 | duplicate id / mixed currencies / no amount / negative subtotal |121| AIR-W200/W201 | taxable event without tax context / refund without related_event |122| AIR-W300 | multiple classification policies matched |123| AIR-E400/E401, AIR-W400 | no tax policy for jurisdiction / unsupported base / requested code not produced |124| AIR-E500..E502 | missing fx rate / settlement without related event / related event unusable |125| AIR-E600/E601, AIR-W600 | missing account role / no posting rule / stated gross differs from computed total |126127## 4. ALSL v0.1128129A YAML subset (full DSL later). A policy set carries: `functional_currency`,130`rounding` (mode + source), `accounts` (role → code/name/type), and131`policies` (kind `tax` or `classification`). Loader strictness: bare-float132rates are rejected; **tax policies without a source citation are rejected**.133See `alsl/policies/ca-qc-2026.yaml` for the reference set (GST 5%,134QST 9.975% on the pre-GST price, half-up per ETA s. 165.2(2) — sources in135docs/research/canada-gst-qst.md, verified 2026-08-05).136137## 5. Backends138139Common interface (`backends/base.py`): `capabilities()`,140`compile(journal) -> TargetPayload`, `post(payload, idempotency_key)`,141`reverse(receipt)`. Posted entries are never deleted — reversal appends142contra entries (or uses the target's native mechanism).143144- **native** — AIR standalone: append-only, hash-chained JSONL ledger with145 idempotent posting and built-in statements (kernel/ledger.py, reporting.py).146 Managed by the **AIR home** (kernel/workspace.py): one directory holding147 meta, ledger, content-addressed document archive, and exports — the whole148 book of record is reproducible from it.149- **generic_csv** — universal flat-file journal export.150- **quickbooks** — QBO JournalEntry payloads (requestid idempotency,151 minorversion=75, DocNumber ≤ 21 chars, contra-entry reversal). QuickBooks152 is OPTIONAL: the backend is developed and tested entirely offline against153 a mock transport that simulates QBO's documented behaviors (idempotent154 replay, duplicate-DocNumber error 6140); `qbo-export` produces155 QBO-shaped JSON with no account at all. The real HTTP transport activates156 only when the user supplies OAuth credentials.157- Planned per docs/research/erp-apis.md: Xero (Idempotency-Key header), Odoo158 (account.move), beancount text export, SAP (deferred).159160## 6. Ingestion — the only place an LLM appears161162```163source text -> Extractor -> AIR schema validation -> confidence routing164 | |165 auto-approved human inbox166 \\ /167 deterministic compiler168```169170The extractor (offline mock, or Claude with schema-constrained structured171output) emits **AIR events only** — never journal entries, accounts, or172debits/credits. Every extraction is validated against the AIR schema;173schema-invalid output ALWAYS routes to the human inbox, as does anything174below the confidence threshold (default 0.85). Approval stamps the approver175and timestamp into event meta (`ingestion/approval.py`); even approved176documents then go through the deterministic compiler. The inbox lives in the177AIR home (`inbox/pending|approved|rejected/`) — nothing is deleted.178179## 7. Agent syscalls & the audit log180181AI agents never touch the ledger, the compiler internals, or the files. Their182entire surface is the kernel syscall interface (`sdk/syscalls.py`):183184| Syscall | Effect |185|---|---|186| `CreateEconomicEvent` | schema-validate + stage a draft in `<home>/drafts/` |187| `Validate` / `Compile` | run the deterministic compiler on the drafts, no posting |188| `Post` | compile + append to the books (idempotent by draft content hash); refuses closed periods (`AIR-E700`); archives the drafts |189| `Reverse` | append the exact contra of a posted entry — never edits |190| `ClosePeriod` | close `YYYY-MM`; later posts into it are refused |191| `GenerateReport` | any statement, any format |192| `Merge` | post the drafts through the optimization passes (below) |193| `Reconcile` | match a bank statement against the books' cash movements |194195Every syscall — successful or refused — appends one record to the196**hash-chained audit log** (`<home>/audit.jsonl`, kernel/audit.py): actor,197syscall, parameter digest, outcome, timestamp, `prev_hash`, `hash`. Editing,198deleting, or reordering any record breaks `air audit` verification. A refused199action is evidence too — denials are logged like successes.200201## 8. Optimization passes & bank reconciliation202203Opt-in (`compile --optimize`, or the `Merge` syscall); the invariant is still204verified after every pass, and optimized lines carry provenance nodes pointing205to every line they absorbed (`aic/passes/optimize.py`):206207- **DuplicateDetectionPass** — identical economic content under different ids208 → warning `AIR-W800`; nothing is dropped, a human decides.209- **NettingPass** — a Refund referencing a Sale in the same compilation nets210 against it: one net entry (or none when fully offset; note `AIR-N801`).211 Refunds exceeding their sale are left alone.212- **FusionPass** — same-day payments with an identical posting shape fuse into213 one batch entry, amounts summed per line (note `AIR-N800`). 50 payments →214 1 entry — which then matches the bank's single batch deposit one-to-one.215216**Reconciliation** (`kernel/reconcile.py`, formats per217docs/research/bank-statement-formats.md): camt.053 (namespace-agnostic,218sign from `CdtDbtInd`, reference from `EndToEndId`), MT940 (`:61:` lines,219comma decimals, D/C/RD/RC marks, opening±movements=closing integrity check),220and CSV, all normalized to signed-Decimal `BankTransaction` records. Matching221is exact on (signed amount, currency) within a configurable date-tolerance222window; unmatched items on both sides are reported, never dropped.223224## 9. Versioning & incremental compilation225226`air_version` is mandatory; schema migrations are explicit.227228Changed source documents recompile as **deltas** (`aic/incremental.py`,229CLI `recompile old.yaml new.yaml`). Events are compared by content230fingerprint (SHA-256 over canonical JSON); the delta contains:231232- an exact contra **reversal entry** (`rev_je_<event>`, `reverses` link) for233 every changed or removed event;234- a **replacement entry** with a deterministic revision-suffixed id235 (`je_<event>_r<fp8>`) for every changed event;236- a normal entry for every added event; nothing for unchanged events.237238Posted history is never mutated. The delta itself must satisfy the239double-entry invariant, and posting `v1 + delta` yields byte-identical240balances to compiling `v2` directly (tested in tests/test_incremental.py).241