Jelajahi Sumber

Document journaling support and record the framing decision

Users asked whether the ledger supports journaling, in particular compound
entries that touch more than two accounts. The mechanics already existed (a
Transfer is a list of Movements committed atomically, and the transfer log is
append-only), but there was no affirmative doc saying so and no ADR capturing
the naming/framing choice. This makes the accounting model discoverable and
pins down what is and is not the journal.

Add doc/journaling.md (single, compound, and multi-asset entries; the transfer
log as the journal; auditability by replay) and ADR-0013, which decides to
model journaling with existing types rather than adding Journal/JournalEntry.
Update the README overview, ADR index, CHANGELOG, CLAUDE.md doc listing, and
cross-links from transfers.md and accounting-mapping.md.
Cesar Rodas 1 hari lalu
induk
melakukan
feee785d6c
8 mengubah file dengan 302 tambahan dan 8 penghapusan
  1. 9 0
      CHANGELOG.md
  2. 1 0
      CLAUDE.md
  3. 8 0
      README.md
  4. 3 1
      doc/accounting-mapping.md
  5. 121 0
      doc/adr/0013-journaling-model.md
  6. 5 6
      doc/adr/README.md
  7. 153 0
      doc/journaling.md
  8. 2 1
      doc/transfers.md

+ 9 - 0
CHANGELOG.md

@@ -5,6 +5,15 @@ All notable changes to this project are documented in this file.
 The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
 and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
 
+## [Unreleased]
+
+### Documentation
+
+- Document that the ledger supports journaling: a committed transfer is a
+  journal entry, a transfer with multiple movements is a compound journal
+  entry, and the transfer log is the accounting journal. Adds
+  `doc/journaling.md` and ADR-0013, and notes it in the README overview.
+
 ## [0.2.0] - 2026-07-01
 
 ### Added

+ 1 - 0
CLAUDE.md

@@ -19,6 +19,7 @@ doc/
   crates.md         Crate reference: modules, types, APIs
   accounts.md       Account model, policies, lifecycle
   transfers.md      Transfer/Movement API, resolve algorithm
+  journaling.md     Journaling: transfers as (compound) journal entries
   glossary.md       Terms, book design, exchange & supermarket examples
   accounting-mapping.md  Classical double-entry ↔ Kuatia term mapping
 ```

+ 8 - 0
README.md

@@ -14,6 +14,13 @@ bookkeeping (`Σ debits = Σ credits`), expressed as `sum(consumed) == sum(creat
 per asset over signed postings. There are no mutable balance fields; an account's
 balance is always the sum of its active postings.
 
+It supports **journaling**: a committed transfer is a journal entry, a transfer
+with multiple movements is a compound journal entry (one balanced event touching
+many accounts, and, since each asset balances independently, many assets), and
+the append-only transfer log is the accounting journal. Replaying that log
+reconstructs every balance, so the ledger is auditable by construction. See
+[doc/journaling.md](doc/journaling.md).
+
 ```
 ┌─────────────────────────────────────────────────────┐
 │                   kuatia (async)                    │
@@ -72,6 +79,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
 - [Crate Reference](doc/crates.md) — modules, types, and APIs per crate
 - [Accounts](doc/accounts.md) — account model, policies, and lifecycle
 - [Transfers](doc/transfers.md) — Movement struct, resolve algorithm, and TransferBuilder API
+- [Journaling](doc/journaling.md) — transfers as journal entries, compound and multi-asset entries, the transfer log as the journal
 - [Glossary](doc/glossary.md) — terms, book scoping, and worked examples
 - [Accounting Mapping](doc/accounting-mapping.md) — how classical double-entry concepts map onto kuatia
 

+ 3 - 1
doc/accounting-mapping.md

@@ -10,7 +10,9 @@ mechanical model is different:
 | `Σ debits = Σ credits` | `sum(consumed) == sum(created)` per asset |
 
 This page maps classical accounting vocabulary onto Kuatia's types and clears
-up the terms that are easy to conflate.
+up the terms that are easy to conflate. For a focused, affirmative walkthrough
+of journaling (single, compound, and multi-asset entries, and the transfer log
+as the journal) see [journaling.md](journaling.md).
 
 The most important correction: in classical accounting a journal is the
 append-only book of original entry, while a journal entry is one committed

+ 121 - 0
doc/adr/0013-journaling-model.md

@@ -0,0 +1,121 @@
+# Journaling model: a transfer is a journal entry, the transfer log is the journal
+
+* Status: accepted
+* Authors: Cesar Rodas
+* Date: 2026-07-08
+* Targeted modules: kuatia-types, kuatia-core, kuatia (transfer log), doc
+* Associated tickets/PRs: n/a
+
+## Context and Problem Statement
+
+Kuatia is presented as a double-entry-style ledger, but it does not have a
+"journal" or "journal entry" type, and it has a `Book` type whose name invites
+confusion with an accounting book or journal. Users ask whether the ledger
+supports journaling, in particular compound entries that touch more than two
+accounts. The mechanics exist already (a `Transfer` is a list of `Movement`s
+committed atomically, and `TransferStore` is an append-only log), so the open
+question is naming and framing: what, in Kuatia's model, is a journal entry,
+what is the journal, and what is `Book`?
+
+## Decision Drivers
+
+* Accountants and integrators reason in journal / journal-entry / compound-entry
+  terms; the model should map onto those without a new type.
+* The mapping must be unambiguous about what is and is not the journal, because
+  `Book`, the transfer log, and the event log are all easy to conflate.
+* Per-asset conservation is the safety invariant and must be the stated
+  equivalent of `Σ debits = Σ credits`.
+* Avoid introducing redundant types that duplicate `Transfer` / `Envelope` /
+  `TransferStore`.
+
+## Considered Options
+
+#### Option 1: Model journaling with the existing types, and document the mapping
+
+Declare that a committed `Transfer` (resolved into an `Envelope`) is a journal
+entry, that a `Transfer` with multiple `Movement`s is a compound journal entry,
+and that the transfer log (`TransferStore` of `EnvelopeRecord`s) is the
+accounting journal. Enforce balance through the existing per-asset conservation
+check. Capture the terminology in `accounting-mapping.md` and `journaling.md`.
+
+**Pros:**
+
+* No new types; the intent API (ADR-0005) and transfer log already provide the
+  grain (one entry) and the collection (the journal).
+* Compound and multi-asset entries fall out for free: a transfer is already a
+  list of movements, and conservation is already per asset.
+* Auditability is already structural: replaying the log reconstructs balances,
+  and content-addressed ids give tamper evidence.
+
+**Cons:**
+
+* The word "posting" is a noun in Kuatia (a value fragment) but a verb in
+  classical accounting (the act of recording), a collision that must be called
+  out.
+* `Book` keeps a name that suggests "accounting book"; the docs must repeatedly
+  disclaim it.
+
+#### Option 2: Add explicit `Journal` and `JournalEntry` types
+
+Introduce dedicated types that wrap the transfer log and a committed transfer.
+
+**Pros:**
+
+* The vocabulary is present in the type system, not only in prose.
+
+**Cons:**
+
+* Pure duplication: `JournalEntry` would be a `Transfer`/`Envelope`, `Journal`
+  would be `TransferStore`. Two names for one concept invites drift.
+* A dedicated debit/credit line type would fight the UTXO model, where the
+  primitive is a signed posting, not a one-sided debit or credit line.
+
+#### Option 3: Rename `Book` to something journal-adjacent
+
+Rename `Book` to reduce the accounting-book confusion.
+
+**Pros:**
+
+* Removes one source of naming conflation.
+
+**Cons:**
+
+* `Book` is a transfer policy scope, not a journal; a journal-adjacent name
+  would make the confusion worse, not better. The fix is documentation, not a
+  rename.
+
+## Decision Outcome
+
+Chosen option: Option 1. Model journaling with the existing types and document
+the mapping. A committed `Transfer` resolved into an `Envelope` is a journal
+entry; a `Transfer` with multiple `Movement`s is a compound journal entry; the
+transfer log is the accounting journal. Per-asset conservation
+(`sum(consumed) == sum(created)`, enforced in `validate_and_plan`) is the
+equivalent of `Σ debits = Σ credits`. `Book` is a transfer policy scope, not the
+journal, not a journal entry, and not a balance partition. The event log
+(ADR-0010) is for lifecycle notifications and is not the journal.
+
+### Positive Consequences
+
+* Journaling, including compound and multi-asset entries, is supported with no
+  new types and no schema change.
+* The journal is auditable by replay and tamper-evident by construction.
+* The naming pitfalls (`Book` vs. journal, posting-noun vs. posting-verb,
+  transfer log vs. event log) are documented in one place.
+
+### Negative Consequences
+
+* The vocabulary lives in documentation rather than in the type system, so it
+  relies on `journaling.md` and `accounting-mapping.md` staying accurate.
+* `Book` retains a name that needs a standing disclaimer.
+
+## Links
+
+* Documented by [journaling.md](../journaling.md) and
+  [accounting-mapping.md](../accounting-mapping.md)
+* Builds on [ADR-0001](0001-modified-utxo-signed-postings.md) (value as signed
+  postings, conservation is structural)
+* Builds on [ADR-0005](0005-intent-api-movements-vs-envelopes.md) (a `Transfer`
+  is intent resolved into an `Envelope`)
+* Relates to [ADR-0010](0010-event-stream-vs-transfer-log.md) (the event stream
+  is not the journal)

+ 5 - 6
doc/adr/README.md

@@ -21,6 +21,8 @@ to reverse a decision. Instead, a new ADR supersedes it.
 | [0009](0009-monetary-representation-integer-minor-units.md) | Monetary amounts as integer minor units | accepted | `Cent` is an `i64` newtype of minor units with only checked arithmetic; scale lives in the presentation-only `Amount`, not on the stored value or asset. Makes 0001's conservation exact. |
 | [0010](0010-event-stream-vs-transfer-log.md) | Derived event stream vs. transfer log | accepted | A secondary append-only `EventStore` feed (outbox-style) for transfer + account-lifecycle events; transfer log stays authoritative. `append_event` is idempotent on a content key, a scoped exception to 0003. |
 | [0011](0011-swappable-money-backing.md) | Swappable integer backing for money, default i64 | accepted | `Cent` moves to a `kuatia-money` crate over a `CentBacking` trait; the i64↔i128 width is a cargo feature, hidden from the API, stored as text. Refines 0009. |
+| [0012](0012-subaccounts.md) | Subaccount dimension on account identity | accepted | Account identity gains a subaccount dimension (used for per-destination inflight holding subaccounts). |
+| [0013](0013-journaling-model.md) | Journaling model: transfer as journal entry | accepted | A committed `Transfer`/`Envelope` is a (compound) journal entry; the transfer log is the accounting journal; `Book` is a policy scope, not the journal. Frames 0001/0005/0010 in accounting terms. |
 
 ## Recommended future ADRs
 
@@ -43,18 +45,15 @@ captured as an ADR, roughly in priority order:
    load and apply.
 5. **All arithmetic in Rust, never in SQL**: no `SUM`/`MAX`/etc. on
    monetary values; the storage layer stays a dumb record keeper.
-6. **`Book` is a transfer-policy scope, not the accounting journal**: the
-   naming/modeling decision and why it is easy to conflate
-   (`accounting-mapping.md`).
-7. **Posting proliferation & consolidation**: greedy largest-first
+6. **Posting proliferation & consolidation**: greedy largest-first
    selection (ADR-0001/0005) fragments balances into ever-smaller change
    postings; whether and how to consolidate, and what to do with dust, is
    undecided.
-8. **Retention / pruning of `Inactive` postings and the append-only
+7. **Retention / pruning of `Inactive` postings and the append-only
    logs**: both the transfer log and the derived event stream (ADR-0010)
    grow without bound; archival/retention is deferred and currently a
    conscious omission.
-9. **Read/projection consistency model**: a balance is a non-transactional
+8. **Read/projection consistency model**: a balance is a non-transactional
    sum over `Active` postings, so a read concurrent with a commit is
    eventually consistent; the read-side guarantee is implied but never
    stated.

+ 153 - 0
doc/journaling.md

@@ -0,0 +1,153 @@
+# Journaling
+
+Kuatia supports journaling. A committed transfer is a journal entry, and the
+transfer log is the accounting journal: the append-only, ordered book of every
+committed entry. This page states what that means in practice, shows single,
+compound, and multi-asset entries, and explains why the log is auditable by
+replay.
+
+For the vocabulary mapping (journal vs. journal entry vs. `Book`, and the terms
+that are easy to conflate) see [accounting-mapping.md](accounting-mapping.md).
+For the resolve algorithm and the builder API see [transfers.md](transfers.md).
+For the decision record see
+[ADR-0013](adr/0013-journaling-model.md).
+
+## What "journaling" means here
+
+In classical bookkeeping:
+
+- A **journal** is the book of original entry: a chronological, append-only
+  record of every accounting event.
+- A **journal entry** is one balanced event. Its debits equal its credits.
+- A **compound journal entry** is one event that touches more than two
+  accounts.
+
+Kuatia provides the same guarantees over a UTXO-style, posting-based model:
+
+| Classical accounting | Kuatia |
+|---|---|
+| Journal (book of original entry) | Transfer log (`TransferStore` of `EnvelopeRecord`s) |
+| Journal entry (one balanced event) | Committed `Transfer` resolved into an `Envelope` |
+| Compound journal entry | `Transfer` with multiple `Movement`s |
+| `Σ debits = Σ credits` | Per-asset conservation `sum(consumed) == sum(created)` |
+
+The mechanism differs (signed postings instead of mutable balances), the
+accounting grain is identical: one committed transfer is one journal entry.
+
+## A journal entry: one balanced event
+
+A `Transfer` is a `Vec<Movement>` committed atomically. Each `Movement` is
+`{ from, to, asset, amount }`. On commit, `resolve()` turns the movements into
+an `Envelope` of concrete postings to consume and create, and validation
+enforces per-asset conservation before anything is written. The whole entry
+commits or none of it does.
+
+A minimal two-account entry:
+
+```rust
+let transfer = TransferBuilder::new()
+    .pay(alice, bob, usd, Cent::from(50))
+    .build();
+```
+
+This is one journal entry: 50 USD leaves Alice, 50 USD arrives at Bob, and the
+resolved envelope balances (`sum(consumed) == sum(created)` for USD).
+
+## Compound entries: more than two accounts
+
+A compound journal entry is native, not a special case. Because a `Transfer`
+holds a list of movements, chaining builder calls accumulates legs into one
+atomic entry.
+
+Classical compound entry (a cash sale with tax):
+
+```
+2026-06-26  Cash sale of goods
+  Dr  Cash ................. 115
+      Cr  Revenue ..........     100
+      Cr  Sales tax payable .      15
+```
+
+The equivalent business effects as one Kuatia transfer:
+
+```rust
+let transfer = TransferBuilder::new()
+    .book(sales_book)
+    .pay(customer, revenue, usd, Cent::from(100))
+    .pay(customer, tax_payable, usd, Cent::from(15))
+    .build();
+// One Transfer -> one Envelope -> one EnvelopeRecord in the transfer log.
+```
+
+Both are a single balanced event. In the classical entry `Σ Dr (115) = Σ Cr
+(115)`. In Kuatia the resolved envelope satisfies per-asset conservation for
+USD. The resolve step aggregates net debit per `(account, asset)` before
+selecting postings, so several legs debiting the same account share one
+selection pass. See
+[transfers.md](transfers.md#resolve-algorithm) for the aggregation detail, and
+the note in [accounting-mapping.md](accounting-mapping.md#a-journal-entry-is-multi-account)
+on why this shows business effects rather than a literal debit/credit
+translation.
+
+## Multi-asset entries
+
+Conservation is enforced per asset, and each asset is an independent
+conservation boundary. A single entry can therefore move more than one asset as
+long as every asset balances on its own. This is how a currency exchange or FX
+trade is recorded as one journal entry: the USD legs balance against USD, and
+the EUR legs balance against EUR, within the same envelope.
+
+A hand-built multi-asset envelope is committed through the same path with
+`ledger.commit_envelope(envelope)`. Validation returns
+`ConservationViolation { asset, consumed_sum, created_sum }` if any single asset
+fails to balance.
+
+## The transfer log is the journal
+
+One entry and the journal differ only in grain:
+
+- A `Transfer` / `Envelope` is one record: one journal entry.
+- The transfer log is the collection of all records: the accounting journal.
+  `TransferStore` persists each committed envelope as `EnvelopeRecord {
+  envelope, receipt, created_at }` in append-only order.
+
+Committing an entry returns a `Receipt { transfer_id }` naming the committed
+envelope. The `EnvelopeId` is content-addressed (the double-SHA-256 of the
+canonical envelope bytes), which gives idempotency (re-committing the same
+envelope returns the cached receipt) and tamper evidence (any edit changes the
+id).
+
+Kuatia keeps a second append-only sequence, the event log (`EventStore` of
+`LedgerEvent`), for lifecycle notifications and projections. It is not the
+journal. It records that something happened; the transfer log records exactly
+which postings moved. See
+[ADR-0010](adr/0010-event-stream-vs-transfer-log.md).
+
+## Auditability by replay
+
+There are no stored balances to drift. An account's balance is the sum of its
+`Active` postings, computed on demand. Re-applying every `EnvelopeRecord` in
+order reconstructs all balances exactly. Because each entry is balanced per
+asset and the log is append-only and content-addressed, the journal is both
+verifiable (recompute any balance) and tamper-evident (any change to a past
+entry breaks its id).
+
+## Not a journal
+
+Two Kuatia concepts are easy to mistake for the journal and are not:
+
+- `Book` is a transfer policy scope. It gates which accounts and assets may
+  participate in a transfer. It is not the journal, not a journal entry, and not
+  a balance partition. See
+  [accounting-mapping.md](accounting-mapping.md#where-book-fits-and-doesnt).
+- The event log records lifecycle notifications for subscribers. The transfer
+  log is the source of truth for balances.
+
+## See also
+
+- [accounting-mapping.md](accounting-mapping.md): full classical double-entry
+  to Kuatia vocabulary mapping.
+- [transfers.md](transfers.md): `Movement`, the resolve algorithm, and the
+  `TransferBuilder` API.
+- [ADR-0013](adr/0013-journaling-model.md): the decision to model a transfer
+  as a (compound) journal entry and the transfer log as the journal.

+ 2 - 1
doc/transfers.md

@@ -176,7 +176,8 @@ let transfer = TransferBuilder::new()
 ```
 
 A single transfer can contain multiple movements of different types. All
-movements execute atomically.
+movements execute atomically. A transfer with multiple movements is a compound
+journal entry; see [journaling.md](journaling.md).
 
 ## Commit Paths