浏览代码

Add accounting-mapping doc and ignore .DS_Store

Several discussions this session reconciled Kuatia's vocabulary with
classical double-entry accounting, and the same confusions kept
recurring: a journal is the whole book while a journal entry is one
transaction, and a single Transfer/Envelope is one entry whereas the
transfer log is the book of them. Capture the full mapping in one
reference doc so it doesn't have to be re-derived, and cross-link it from
the doc index and the glossary. Also ignore the macOS .DS_Store file.
Cesar Rodas 1 周之前
父节点
当前提交
7dee7fe3d0

+ 1 - 0
.gitignore

@@ -1 +1,2 @@
 /target
+.DS_Store

+ 4 - 3
CLAUDE.md

@@ -2,7 +2,7 @@
 
 ## 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 (double-entry bookkeeping).
+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
 
@@ -10,7 +10,7 @@ Kuatia is an append-only, auditable, multi-asset UTXO-style ledger library in Ru
 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 (5 sub-traits), InMemoryStore, conformance tests
+  kuatia-storage/   Store trait (6 sub-traits), InMemoryStore, conformance tests
   kuatia-storage-sql/  SQL backend: SQLite/PostgreSQL via sqlx
   kuatia/           Async layer: Ledger resource, saga pipeline, intent API
 doc/
@@ -19,6 +19,7 @@ doc/
   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
@@ -60,7 +61,7 @@ Deposit: two movements cancel to zero net debit on the system account — no pos
 ## Testing
 
 ```bash
-cargo test          # runs all 119 tests across all crates
+cargo test          # runs all tests across all crates
 cargo test -p kuatia-core   # pure core tests only
 cargo test -p kuatia        # integration + saga tests
 ```

+ 7 - 3
README.md

@@ -9,8 +9,10 @@ Auditable, multi-asset UTXO-style ledger in Rust.
 
 kuatia models value as **postings** — signed amounts owned by exactly one account.
 Transfers atomically consume existing postings and create new ones, enforcing
-per-asset conservation (double-entry bookkeeping). There are no mutable balance
-fields; an account's balance is always the sum of its active postings.
+per-asset conservation. This gives the same safety guarantee as double-entry
+bookkeeping (`Σ debits = Σ credits`), expressed as `sum(consumed) == sum(created)`
+per asset over signed postings. There are no mutable balance fields; an account's
+balance is always the sum of its active postings.
 
 ```
 ┌─────────────────────────────────────────────────────┐
@@ -36,7 +38,7 @@ fields; an account's balance is always the sum of its active postings.
 |-------|---------|
 | **kuatia-types** | Domain types — `AccountId`, `Posting`, `Transfer`, `Cent`, etc. |
 | **kuatia-core** | Pure, sans-IO decision logic — validation, hashing, posting selection. |
-| **kuatia-storage** | `Store` trait (5 sub-traits), `InMemoryStore`, `store_tests!` conformance macro. |
+| **kuatia-storage** | `Store` trait (6 sub-traits), `InMemoryStore`, `store_tests!` conformance macro. |
 | **kuatia-storage-sql** | SQL-backed `Store` — SQLite and PostgreSQL via sqlx. |
 | **kuatia** | Async resource layer — `Ledger`, saga commit pipeline, intent-layer API. |
 
@@ -70,6 +72,8 @@ 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
+- [Glossary](doc/glossary.md) — terms, book scoping, and worked examples
+- [Accounting Mapping](doc/accounting-mapping.md) — how classical double-entry concepts map onto kuatia
 
 ## License
 

+ 1 - 0
crates/kuatia-core/Cargo.toml

@@ -1,5 +1,6 @@
 [package]
 name = "kuatia-core"
+description = "Pure, sans-IO core logic for the Kuatia ledger: validation, hashing, posting selection."
 version = "0.1.0"
 edition = "2024"
 license.workspace = true

+ 1 - 0
crates/kuatia-storage-sql/Cargo.toml

@@ -1,5 +1,6 @@
 [package]
 name = "kuatia-storage-sql"
+description = "SQLite/PostgreSQL storage backend for the Kuatia ledger."
 version = "0.1.0"
 edition = "2024"
 license.workspace = true

+ 2 - 2
crates/kuatia-storage-sql/README.md

@@ -30,5 +30,5 @@ store.migrate().await?;
 
 ## Schema
 
-Five tables: `accounts`, `postings`, `transfers`, `transfer_accounts`, `sagas`.
-Migrations run via `store.migrate()`.
+Tables: `accounts`, `postings`, `transfers`, `transfer_accounts`, `sagas`,
+`events`, `books`. Migrations run via `store.migrate()`.

+ 1 - 0
crates/kuatia-storage/Cargo.toml

@@ -1,5 +1,6 @@
 [package]
 name = "kuatia-storage"
+description = "Storage abstraction and conformance suite for the Kuatia ledger."
 version = "0.1.0"
 edition = "2024"
 license.workspace = true

+ 5 - 3
crates/kuatia-storage/README.md

@@ -2,7 +2,7 @@
 
 Storage abstraction for the kuatia ledger.
 
-Defines the `Store` trait (composed of four sub-traits), provides an
+Defines the `Store` trait (composed of six sub-traits), provides an
 in-memory implementation for tests, and exports a `store_tests!` conformance
 macro that any backend can use to validate its implementation.
 
@@ -14,8 +14,10 @@ macro that any backend can use to validate its implementation.
 | `PostingStore` | Posting reads, reserve/release/finalize lifecycle |
 | `TransferStore` | Transfer persistence and queries |
 | `SagaStore` | Saga state for crash recovery |
+| `EventStore` | Append-only ledger event log |
+| `BookStore` | Book (transfer policy scope) persistence |
 
-`Store` is a blanket trait — any type implementing all four sub-traits is a `Store`.
+`Store` is a blanket trait — any type implementing all six sub-traits is a `Store`.
 
 ## Conformance testing
 
@@ -26,4 +28,4 @@ async fn new_store() -> InMemoryStore { InMemoryStore::new() }
 kuatia_storage::store_tests!(new_store);
 ```
 
-This generates 22 tests covering every Store method.
+This generates a test for every Store method, run against any backend.

+ 1 - 0
crates/kuatia-types/Cargo.toml

@@ -1,5 +1,6 @@
 [package]
 name = "kuatia-types"
+description = "Domain types for the Kuatia ledger: postings, accounts, transfers, books."
 version = "0.1.0"
 edition = "2024"
 license.workspace = true

+ 3 - 2
crates/kuatia-types/README.md

@@ -14,9 +14,10 @@ This crate is the foundation — every other kuatia crate depends on it.
 | `EnvelopeId([u8; 32])` | Content-addressed transfer hash |
 | `PostingId { transfer, index }` | Posting identity within a transfer |
 | `Cent(i64)` | Smallest monetary unit, checked arithmetic |
-| `Posting` | Signed amount owned by one account |
+| `Posting` | Signed amount owned by one account (positive = held, negative = offset) |
 | `Transfer` | Atomic unit: consumes + creates postings |
-| `Account` | Versioned entity with policy and flags |
+| `Account` | Versioned entity with policy, flags, and book |
+| `Book` / `BookId` | Transfer policy scope — gates which accounts/assets may participate |
 | `PostingStatus` | `Active` → `PendingInactive` → `Inactive` |
 
 ## Traits

+ 1 - 0
crates/kuatia/Cargo.toml

@@ -1,5 +1,6 @@
 [package]
 name = "kuatia"
+description = "Append-only, auditable, multi-asset UTXO-style ledger: async resource and saga commit pipeline."
 version = "0.1.0"
 edition = "2024"
 license.workspace = true

+ 6 - 0
crates/kuatia/README.md

@@ -68,3 +68,9 @@ legend! {
     }
 }
 ```
+
+## See also
+
+- [doc/accounting-mapping.md](../../doc/accounting-mapping.md) — how classical
+  double-entry concepts (journal, journal entry, ledger) map onto kuatia's
+  transfer log, transfers, and postings.

+ 172 - 0
doc/accounting-mapping.md

@@ -0,0 +1,172 @@
+# Accounting Mapping — Classical Double-Entry ↔ Kuatia
+
+Kuatia provides double-entry-style **safety** using a UTXO-style model. Value is
+held as **signed postings**, and every committed transfer must satisfy per-asset
+conservation. The accounting goal is the same as classical bookkeeping; the
+mechanical model is different:
+
+| Classical double-entry | Kuatia |
+|---|---|
+| `Σ 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.
+
+The single most important correction: in classical accounting a **journal** is
+the append-only book of original entry, while a **journal entry** is *one
+committed accounting event*. In Kuatia, the closest equivalent to the classical
+journal is the **transfer log**; the closest equivalent to a journal entry is a
+committed **`Transfer`/`Envelope`**. Kuatia's **`Book`** is neither — it is a
+transfer *policy scope*, not the accounting journal.
+
+## Core mapping
+
+| Classical accounting | Kuatia | Notes |
+|---|---|---|
+| **Journal** (book of original entry) | **Transfer log** — `TransferStore` of `EnvelopeRecord`s | Append-only, ordered source of truth for committed transfers. |
+| **Journal entry** (one balanced event) | Committed **`Transfer`** (intent) → **`Envelope`** (resolved) | One atomic accounting event. |
+| **Compound journal entry** | `Transfer` with multiple `Movement`s | One event touching many accounts/assets. |
+| **Entry line / leg** | **`Posting`** effect (often derived from a `Movement`) | A concrete account-level value fragment. A `Movement` is two-sided intent `{from, to, asset, amount}` that resolves into consumed/created postings — not a 1:1 debit/credit line. |
+| **Σ debits = Σ credits** | **Per-asset conservation** `sum(consumed) == sum(created)` | Enforced in `validate_and_plan`; `ConservationViolation` otherwise. |
+| **Ledger** (accounts + running balances) | **Accounts + active postings** | Balances are projections over `Active` postings — never stored. |
+| **Posting a transaction** (the verb) | **resolve + commit** (`Transfer` → `Envelope` → apply) | Confusing collision: in Kuatia a *posting* is a noun (a value fragment), not the act of recording. |
+| **Accounting book** | *no direct equivalent unless modeled separately* | Kuatia `Book` is **not** this. |
+| **Transfer policy scope** | **`Book`** | Gates which accounts/assets may participate — see [below](#where-book-fits-and-doesnt). |
+
+> A journal entry is *one committed accounting event*. In many accounting texts
+> this event is also called a **transaction** — a word that is overloaded in a
+> ledger library (database transaction, business transaction, atomic transfer),
+> so this doc prefers "committed accounting event."
+
+## A journal entry is multi-account
+
+Double-entry entries are inherently multi-account — that is the entire point. A
+minimal entry has two legs; a **compound entry** has more. So a Kuatia transfer
+touching many accounts is not a mismatch — it *is* a (compound) journal entry.
+
+Classical compound journal entry:
+
+```
+2026-06-26  Cash sale of goods
+  Dr  Cash ................. 115
+      Cr  Revenue ..........     100
+      Cr  Sales tax payable .      15
+```
+
+The equivalent business effects as a Kuatia transfer — multiple movements,
+committed atomically:
+
+```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.
+```
+
+> **Note:** this is *not* a literal debit/credit translation. It shows the
+> business effects as movements. In a production POS model, cash, revenue, and
+> tax might be separate effects routed through system/offset accounts. (A literal
+> multi-hop `customer → cash → revenue` chain inside one transfer would require
+> spending a posting created earlier in the *same* envelope, which the resolver
+> does not do — it selects from already-committed postings.)
+
+Both are a single balanced event. In the classical entry, `Σ Dr (115) = Σ Cr
+(115)`. In Kuatia, the resolved `Envelope` satisfies per-asset conservation:
+`sum(consumed) == sum(created)` for USD.
+
+## One entry vs the journal: `Transfer`/`Envelope` vs the transfer log
+
+These differ in **grain** — one record vs. the collection of all records.
+
+- **`Transfer` / `Envelope` = one record** (one journal entry).
+  - **`Transfer`** is the *intent*: `{ movements: Vec<Movement>, book, user_data,
+    metadata }`. Callers express *what* should happen, not *which* postings.
+  - **`Envelope`** is the *resolved* form produced by `resolve()`:
+    `{ consumes: Vec<PostingId>, creates: Vec<NewPosting>, account_snapshots,
+    book, … }`. It names the concrete postings to spend and create.
+  - Committing one (`commit` / `commit_atomic`) returns a **`Receipt {
+    transfer_id }`** identifying the committed envelope — the `EnvelopeId`, which
+    is content-addressed (the double-SHA-256 of the canonical envelope bytes).
+- **Transfer log = the accounting journal.** The append-only, ordered sequence of
+  every committed envelope, persisted by **`TransferStore`** as **`EnvelopeRecord
+  { envelope, receipt, created_at }`**. Each transfer is one entry in it.
+
+> **Transfer/Envelope : transfer log :: one journal entry : the journal.**
+
+"The system is trivially auditable by replaying the transfer log" means: re-apply
+every `EnvelopeRecord` in order and you reconstruct all balances — there is no
+stored balance that can drift.
+
+## Two append-only logs — don't conflate "log"
+
+Kuatia keeps **two** distinct append-only sequences. Only the first is the
+accounting journal.
+
+| Log | Type | Records | Role |
+|---|---|---|---|
+| **Transfer log** | `TransferStore` → `EnvelopeRecord` | Full posting-level detail of each committed transfer | The accounting **journal** — the source of truth for balances. |
+| **Event log** | `EventStore` → `LedgerEvent` | High-level lifecycle notifications | Projections / subscribers; *not* the journal. |
+
+`LedgerEvent { seq, timestamp, kind }` carries a monotonic `seq` and a `kind` of
+`TransferCommitted | AccountCreated | AccountFrozen | AccountUnfrozen |
+AccountClosed`. It tells you *that* something happened; the transfer log tells
+you *exactly which postings* moved.
+
+## The UTXO wrinkle
+
+Classical ledgers post an entry by mutating each account's running balance.
+Kuatia is UTXO-style and posting-based, so the mechanism differs while the event
+grain is identical. Because postings are **signed**, debit/credit is not the
+native primitive; resolution works in terms of consuming and creating postings:
+
+- In a simple movement, the **source side** is resolved by consuming `Active`
+  postings from the source account, creating a **change** posting if the selected
+  postings exceed the amount.
+- The **destination side** is represented by newly created postings on the
+  destination account.
+- An account's **balance** is the sum of its `Active` postings — computed on
+  demand, never stored.
+
+So an entry line maps to a `Posting` effect, usually derived from a `Movement`
+(two-sided intent) that resolves into one or more postings (a created posting,
+consumed postings, and possibly change). The balancing rule is unchanged: per
+asset, `sum(consumed) == sum(created)`.
+
+## Where `Book` fits (and doesn't)
+
+`Book` is the one Kuatia concept with **no classical counterpart**. It is a
+**transfer policy scope** — it gates *which accounts and assets may participate*
+in a transfer (`BookPolicy { allowed_assets, allowed_flags, allowed_accounts }`).
+
+It is explicitly **not**:
+- the **journal** (that is the transfer log),
+- a **journal entry** (that is a `Transfer`/`Envelope`),
+- a **balance partition** — balances are global; a Book only gates participation.
+
+> **Despite the name, a Kuatia `Book` must not be confused with an accounting
+> book.** The accounting journal is the transfer log; `Book` is purely a policy
+> scope.
+
+See the [glossary](glossary.md#book) for the Book model and worked examples.
+
+## Quick reference
+
+| Classical accounting | Kuatia | Notes |
+|---|---|---|
+| Journal | Transfer log / `TransferStore<EnvelopeRecord>` | Append-only source of truth for committed transfers. |
+| Journal entry | Committed `Transfer` / `Envelope` | One atomic accounting event. |
+| Entry line / leg | `Posting` effect | A concrete account-level value fragment; movements are intent that resolve into postings. |
+| Compound journal entry | `Transfer` with multiple movements | One event touching many accounts/assets. |
+| Σ debits = Σ credits | Per-asset conservation | `sum(consumed) == sum(created)` per asset. |
+| Ledger | Accounts + active postings | Balances are projections over active postings. |
+| Posting a transaction (verb) | Resolve + commit | Avoid confusion: Kuatia `Posting` is a noun. |
+| Accounting book | No direct equivalent unless modeled separately | Kuatia `Book` is not this. |
+| Transfer policy scope | `Book` | Gates allowed accounts/assets. |
+| Proof a txn was recorded | `Receipt { transfer_id }` | Content-addressed `EnvelopeId`. |
+| Lifecycle notifications | Event log (`LedgerEvent`) | Separate from the transfer log. |
+
+**In one line:** Kuatia's transfer log is the accounting journal; each committed
+envelope is a journal entry; Kuatia's `Book` is a policy scope, not a journal or
+a balance partition.

+ 3 - 1
doc/glossary.md

@@ -1,5 +1,7 @@
 # Glossary & Usage Guide
 
+> Coming from classical accounting? See [accounting-mapping.md](accounting-mapping.md) for how journals, entries, and ledgers map onto Kuatia's transfers, postings, and books.
+
 ## Terms
 
 ### Posting
@@ -49,7 +51,7 @@ An empty policy (no restrictions) allows any account and any asset.
 
 ### Conservation
 
-For every transfer, for each asset: `sum(consumed) == sum(created)`. This is the double-entry bookkeeping invariant, enforced at the type level. No value is created or destroyed — it only moves.
+For every transfer, for each asset: `sum(consumed) == sum(created)`. This is the double-entry-style safety invariant (the UTXO-model equivalent of `Σ debits = Σ credits`), enforced at the type level. No value is created or destroyed — it only moves.
 
 ### AutoId