SPB Git

spb/air Public MIT

AIR — The Language of Accounting.

Python 100%
11.3 KB

# AIR Specification v0.1 — The Language of Accounting

AIR (Accounting Intermediate Representation) is to accounting what LLVM IR is to compilation: a universal intermediate language. AIR describes economic events — what actually happened in the world — and a deterministic compiler (AIC) lowers them into journal entries under versioned rule sets (ALSL).

The core contract:

An LLM (or any frontend) may only ever produce AIR. It never produces a journal entry. The compiler applies taxes, standards, and policies deterministically, traceably, and testably.

AIR is self-sufficient: with the native backend, AIR is a complete standalone accounting system (hash-chained ledger + general ledger, trial balance, income statement, balance sheet in text/markdown/csv/json). Third party systems (QuickBooks, Xero, Odoo, SAP...) are optional export targets.

# 1. Document form

An AIR document is YAML or JSON validated against schemas/air-0.1.schema.json. Three isomorphic forms exist (as in LLVM): in-memory typed objects (core/events.py), canonical JSON, and readable YAML.

yaml
air_version: "0.1"
events:
  - id: evt_01H8XGJWBWBAQ4Z1
    type: Sale
    date: 2026-07-20
    parties: {seller: "company:acme", buyer: "customer:cust_123"}
    items:
      - {sku: chair-std, qty: "3", unit_price: {amount: "333.33", currency: CAD}}
    payment: {method: card.visa, immediate: true}
    tax: {jurisdiction: CA-QC, codes: [GST, QST]}
    meta:
      source: {kind: invoice_pdf, uri: "s3://...", ocr_score: "0.97"}
      llm: {model: "claude-fable-5", confidence: "0.93"}
      policy_version: "2026.08"

# 1.1 Lexical rules

  • Amounts, quantities, rates are strings ("333.33"). Bare numbers would be parsed as binary floats and are rejected (ADR 0003).
  • id is unique per document (ULIDs recommended).
  • Dates are ISO 8601.
  • Unknown fields are rejected (extra="forbid"): the schema is the contract.

# 1.2 Event types (v0.1)

Type Meaning Posting shape (native profile)
Sale value delivered to a buyer DR cash/AR total · CR revenue · CR tax payables
Purchase value received from a vendor DR expense/asset (+ non-recoverable tax) · DR recoverable taxes · CR cash/AP
Refund reversal of a sale mirror of Sale
PaymentReceived settles a receivable DR cash · CR AR · ± realized FX
PaymentSent settles a payable DR AP · CR cash · ± realized FX
OwnerContribution equity injection DR cash · CR owner capital
LoanReceived debt proceeds DR cash · CR loan payable

Semantic notes:

  • No account, no debit, no credit appears in AIR. Events are perspective-neutral (ADR 0002): the same event compiles to the seller's or buyer's books depending on the compiling entity's policy set.
  • tax.jurisdiction names where the supply takes place; rates live only in ALSL policies.
  • fx.rate is observed input data (e.g. a Bank of Canada Valet daily rate on the transaction date), never a constant of the system.

# 2. Provenance — accounting SSA

Every amount in a compilation is defined exactly once as a node in an append-only DAG (core/provenance.py). Derivations record their operation:

text
prov_000000 event_subtotal  subtotal:Sale            999.99 CAD  <- evt_...
prov_000001 tax             tax:GST@0.05~half_up      50.00 CAD  <- prov_000000
prov_000002 tax             tax:QST@0.09975~half_up   99.75 CAD  <- prov_000000
prov_000005 journal_line    post:credit:2320          99.75 CAD  <- prov_000002

From any posted line one can walk back to the source event, its document URI, its OCR/LLM confidence, and the exact policy and rounding mode applied.

# 3. The AIC pipeline

text
ValidationPass -> ClassificationPass -> TaxPass -> FxPass -> PostingPass

After every pass the verifier checks (a) each entry balances per currency and (b) the accounting equation Assets = Liabilities + Equity + (Revenue − Expenses). Any violation aborts with clang-style diagnostics (code, location, cause, help: fix), e.g.:

text
error[AIR-E400]: no tax policy in set 'ca-qc' matches jurisdiction 'CA-BC'
  --> event evt_x, field tax.jurisdiction
  pass: tax
  help: add an ALSL tax policy for this jurisdiction or mark the event tax.exempt: true

Determinism: same document + same policy set = byte-identical journal. No LLM, network, or clock inside the compiler (property-tested).

# 3.1 Diagnostic codes

Code Meaning
AIR-E100/E101/E102 empty entry / unbalanced entry / accounting equation violated
AIR-E200..E203 duplicate id / mixed currencies / no amount / negative subtotal
AIR-W200/W201 taxable event without tax context / refund without related_event
AIR-W300 multiple classification policies matched
AIR-E400/E401, AIR-W400 no tax policy for jurisdiction / unsupported base / requested code not produced
AIR-E500..E502 missing fx rate / settlement without related event / related event unusable
AIR-E600/E601, AIR-W600 missing account role / no posting rule / stated gross differs from computed total

# 4. ALSL v0.1

A YAML subset (full DSL later). A policy set carries: functional_currency, rounding (mode + source), accounts (role → code/name/type), and policies (kind tax or classification). Loader strictness: bare-float rates are rejected; tax policies without a source citation are rejected. See alsl/policies/ca-qc-2026.yaml for the reference set (GST 5%, QST 9.975% on the pre-GST price, half-up per ETA s. 165.2(2) — sources in docs/research/canada-gst-qst.md, verified 2026-08-05).

# 5. Backends

Common interface (backends/base.py): capabilities(), compile(journal) -> TargetPayload, post(payload, idempotency_key), reverse(receipt). Posted entries are never deleted — reversal appends contra entries (or uses the target's native mechanism).

  • native — AIR standalone: append-only, hash-chained JSONL ledger with idempotent posting and built-in statements (kernel/ledger.py, reporting.py). Managed by the AIR home (kernel/workspace.py): one directory holding meta, ledger, content-addressed document archive, and exports — the whole book of record is reproducible from it.
  • generic_csv — universal flat-file journal export.
  • quickbooks — QBO JournalEntry payloads (requestid idempotency, minorversion=75, DocNumber ≤ 21 chars, contra-entry reversal). QuickBooks is OPTIONAL: the backend is developed and tested entirely offline against a mock transport that simulates QBO's documented behaviors (idempotent replay, duplicate-DocNumber error 6140); qbo-export produces QBO-shaped JSON with no account at all. The real HTTP transport activates only when the user supplies OAuth credentials.
  • Planned per docs/research/erp-apis.md: Xero (Idempotency-Key header), Odoo (account.move), beancount text export, SAP (deferred).

# 6. Ingestion — the only place an LLM appears

text
source text -> Extractor -> AIR schema validation -> confidence routing
                                                        |            |
                                                  auto-approved   human inbox
                                                        \\            /
                                                    deterministic compiler

The extractor (offline mock, or Claude with schema-constrained structured output) emits AIR events only — never journal entries, accounts, or debits/credits. Every extraction is validated against the AIR schema; schema-invalid output ALWAYS routes to the human inbox, as does anything below the confidence threshold (default 0.85). Approval stamps the approver and timestamp into event meta (ingestion/approval.py); even approved documents then go through the deterministic compiler. The inbox lives in the AIR home (inbox/pending|approved|rejected/) — nothing is deleted.

# 7. Agent syscalls & the audit log

AI agents never touch the ledger, the compiler internals, or the files. Their entire surface is the kernel syscall interface (sdk/syscalls.py):

Syscall Effect
CreateEconomicEvent schema-validate + stage a draft in <home>/drafts/
Validate / Compile run the deterministic compiler on the drafts, no posting
Post compile + append to the books (idempotent by draft content hash); refuses closed periods (AIR-E700); archives the drafts
Reverse append the exact contra of a posted entry — never edits
ClosePeriod close YYYY-MM; later posts into it are refused
GenerateReport any statement, any format
Merge post the drafts through the optimization passes (below)
Reconcile match a bank statement against the books' cash movements

Every syscall — successful or refused — appends one record to the hash-chained audit log (<home>/audit.jsonl, kernel/audit.py): actor, syscall, parameter digest, outcome, timestamp, prev_hash, hash. Editing, deleting, or reordering any record breaks air audit verification. A refused action is evidence too — denials are logged like successes.

# 8. Optimization passes & bank reconciliation

Opt-in (compile --optimize, or the Merge syscall); the invariant is still verified after every pass, and optimized lines carry provenance nodes pointing to every line they absorbed (aic/passes/optimize.py):

  • DuplicateDetectionPass — identical economic content under different ids → warning AIR-W800; nothing is dropped, a human decides.
  • NettingPass — a Refund referencing a Sale in the same compilation nets against it: one net entry (or none when fully offset; note AIR-N801). Refunds exceeding their sale are left alone.
  • FusionPass — same-day payments with an identical posting shape fuse into one batch entry, amounts summed per line (note AIR-N800). 50 payments → 1 entry — which then matches the bank's single batch deposit one-to-one.

Reconciliation (kernel/reconcile.py, formats per docs/research/bank-statement-formats.md): camt.053 (namespace-agnostic, sign from CdtDbtInd, reference from EndToEndId), MT940 (:61: lines, comma decimals, D/C/RD/RC marks, opening±movements=closing integrity check), and CSV, all normalized to signed-Decimal BankTransaction records. Matching is exact on (signed amount, currency) within a configurable date-tolerance window; unmatched items on both sides are reported, never dropped.

# 9. Versioning & incremental compilation

air_version is mandatory; schema migrations are explicit.

Changed source documents recompile as deltas (aic/incremental.py, CLI recompile old.yaml new.yaml). Events are compared by content fingerprint (SHA-256 over canonical JSON); the delta contains:

  • an exact contra reversal entry (rev_je_<event>, reverses link) for every changed or removed event;
  • a replacement entry with a deterministic revision-suffixed id (je_<event>_r<fp8>) for every changed event;
  • a normal entry for every added event; nothing for unchanged events.

Posted history is never mutated. The delta itself must satisfy the double-entry invariant, and posting v1 + delta yields byte-identical balances to compiling v2 directly (tested in tests/test_incremental.py).