CLAUDE.md 5.8 KB

Kuatia — Project Context

What is this

Kuatia is an append-only, auditable, multi-asset UTXO-style ledger library in Rust. Value is tracked as signed postings — no mutable balance fields. Transfers atomically consume and create postings, enforcing per-asset conservation — the double-entry-style safety invariant (sum(consumed) == sum(created) per asset).

Crate layout

crates/
  kuatia-types/     Domain types: AccountId, Posting, Movement, Cent, AutoId, etc.
  kuatia-core/      Pure, sync, no-IO logic: validation, hashing, posting selection
  kuatia-storage/   Store trait (7 sub-traits), InMemoryStore, conformance tests
  kuatia-storage-sql/  SQL backend: SQLite/PostgreSQL via sqlx
  kuatia/           Async layer: Ledger resource, saga pipeline, intent API
doc/
  architecture.md   Architecture decisions and rationale
  crates.md         Crate reference: modules, types, APIs
  accounts.md       Account model, policies, lifecycle
  transfers.md      Transfer/Movement API, resolve algorithm
  glossary.md       Terms, book design, exchange & supermarket examples
  accounting-mapping.md  Classical double-entry ↔ Kuatia term mapping

Key concepts

  • Posting: signed amount of one asset owned by one account. Lifecycle: Active → PendingInactive → Inactive.
  • Movement: { from, to, asset, amount } — the fundamental unit of intent. All operations (pay, deposit, withdraw) are one or more movements.
  • Envelope: concrete postings to consume and create — the resolved form of movements.
  • Conservation: for each asset, sum(consumed) == sum(created).
  • Account policies: NoOverdraft, CappedOverdraft, UncappedOverdraft, SystemAccount, ExternalAccount. Only NoOverdraft forbids negative postings; the other four permit them. An overdraft is a negative posting that covers a shortfall — down to the floor for CappedOverdraft, unbounded for UncappedOverdraft.
  • Atomic commit: CommitStore::commit_transfer is the single atomic boundary — postings, transfer record, account index, and events apply in one transaction. It enforces CappedOverdraft CAS guards and reservation ownership. reserve_postings/release_postings carry a ReservationId so only the owning saga can finalize/release a reserved posting.

Architecture

  • Pure core / async layer separation: kuatia-core has zero IO, fully deterministic, testable with golden vectors. kuatia adds async Store trait and saga pipeline.
  • Saga commit pipeline: reserve → validate → finalize, with automatic retry and LIFO compensation via the legend crate.
  • Content-addressed transfers: EnvelopeId = double-SHA-256 of canonical bytes. Provides idempotency and tamper evidence.
  • Append-only accounts: versioned, never modified in place. Snapshot pinning prevents TOCTOU races.
  • Store uses Arc<dyn Store>: Ledger is non-generic, enabling concrete saga types.

Resolve algorithm

Two-pass:

  1. For each movement, create output posting on to and accumulate net debit on from.
  2. For each (account, asset) with positive net debit, select postings (greedy largest-first) and compute change. If positive postings are insufficient: CappedOverdraft/UncappedOverdraft accounts consume all positives and create a negative posting for the shortfall (floor enforced in validation); other policies fail with InsufficientFunds.

Deposit: two movements cancel to zero net debit on the system account — no posting selection needed.

Validation steps (validate_and_plan)

  1. Non-empty
  2. No duplicate consumed PostingIds
  3. Consumed postings exist
  4. Consumed postings Active or PendingInactive
  5. Referenced accounts exist, not frozen, not closed
  6. Account snapshot pinning
  7. Book policy (if a book is loaded): referenced assets/accounts/flags allowed by the book
  8. Per-asset conservation
  9. Negative postings forbidden only on NoOverdraft (allowed on overdraft/system/external)
  10. Policy enforcement (balance floor)

Testing

cargo test          # runs all tests across all crates
cargo test -p kuatia-core   # pure core tests only
cargo test -p kuatia        # integration + saga tests

Conventions

  • Clarity over cleverness
  • All arithmetic in Rust only — the storage layer is a dumb record keeper. No SQL SUM, MAX, MIN, AVG, or any computation on monetary amounts or domain values in queries. COUNT(*) for pagination row totals is allowed (it counts rows, not domain values). Balances are always computed in Rust with checked arithmetic (checked_add, checked_sub, checked_neg) — no silent overflow
  • No unwrap()/expect() in production code — all errors bubble up via Result
  • Domain types for all identifiers — never raw integers or byte arrays in public APIs
  • Use "Posting" not "Coin" for accounting clarity
  • TransferBuilder convenience methods (.pay(), .deposit(), .withdraw()) over raw .movement() construction
  • Every Store sub-trait method must have a conformance test in store_tests! macro — new trait methods require new tests
  • .deposit() returns Result<Self, OverflowError> — callers must handle the error
  • No AUTOINCREMENT / SERIAL in the database — all IDs are generated in Rust. Use snowflake-style i64 IDs with the following bit layout:

    [0][  40 bits: ms timestamp  ][ 23 bits: CRC32(data) ]
    ^sign (always 0 = positive)
    
    • Bit 63: always 0 (keeps i64 positive)
    • Bits 62–23: milliseconds since KUATIA_EPOCH_MS (2026-01-01T00:00:00Z), not the Unix epoch — 40 bits ≈ 34.8 years going forward (until ~2060)
    • Bits 22–0: lower 23 bits of CRC32 of context-specific data (e.g. serialized event)
    • When no data is provided, an internal atomic counter is used (wraps on 23-bit overflow)
    • Implementation: AutoId in kuatia-types/src/autoid.rs, includes inline CRC32 (IEEE)
    • Generated in Rust, stored as plain BIGINT — the DB never assigns IDs