diff --git a/bookspec/README.md b/bookspec/README.md index d60edea..435439e 100644 --- a/bookspec/README.md +++ b/bookspec/README.md @@ -13,6 +13,14 @@ Every example is checked against wanderbooks on every `cargo test` with fails the build, so the examples can't rot. An expected value is worked by hand from the cited source, never copied from an engine's output. +The schema, the parser (`Example::from_toml`, every structure with +`deny_unknown_fields`) and the `Engine` trait an accounting engine +implements live in `bookspec/checker`; `bookspec/runner` runs any engine +through the examples and reports per rule: pass, fail or not claimed. The +wanderbooks adapter is `crates/ledger-core/tests/bookspec.rs`. The rule +identifiers are `BS--`, tabled in `bookspec/spec/rules-index.md` +(P13 steps 1 to 3). + ## The shape of an example One TOML file per example in `examples/`, named for its `id`. @@ -99,7 +107,7 @@ declares `largest-remainder`, the rule a members close splits by. authorities alone (a standard, a regulation, a statute, a court, the mechanics of double entry); `CONVENTION`, common practice where no authority governs (rules.md's CONVENTION); `CHOICE`, an owner's or the -house's decision (a plan D-number, or a DECISION rules.md records) that +house's decision (a plan D-number, or a CHOICE rules.md records) that some expected value depends on, even when authorities carry the rest. An example whose figures follow a decision cites it in `sources`. diff --git a/bookspec/checker/tests/docs.rs b/bookspec/checker/tests/docs.rs new file mode 100644 index 0000000..d27d8de --- /dev/null +++ b/bookspec/checker/tests/docs.rs @@ -0,0 +1,28 @@ +//! The documentation bookspec's identifiers live in: `docs/spec/rules.md` +//! carries each stated rule's id and speaks bookspec's `CHOICE`. + +use std::path::Path; + +use bookspec_checker::rules; + +/// `docs/spec/rules.md` carries every stated rule's id under its heading, +/// and no longer calls a choice a decision (P13 step 3). +#[test] +fn rules_md_carries_every_id() { + let text = std::fs::read_to_string( + Path::new(env!("CARGO_MANIFEST_DIR")).join("../../docs/spec/rules.md"), + ) + .expect("docs/spec/rules.md"); + for rule in rules().iter().filter(|r| r.status.is_some()) { + assert!( + text.contains(&format!("**bookspec:** `{}`", rule.id)), + "B-{} carries {}", + rule.b, + rule.id + ); + } + assert!( + !text.contains("DECISION"), + "rules.md speaks bookspec's CHOICE" + ); +} diff --git a/docs/architecture.md b/docs/architecture.md index ed9baeb..73da267 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -134,3 +134,35 @@ tokio mutex never poisons. So the server fails closed: a panic in a request (its task, or while it holds the guard) sets `stopped`, and every later request, on every session, is refused until a restart, which re-verifies the journal. + +## bookspec (`bookspec/`, P13) + +bookspec is the formal accounting specification (D-34): every rule stated +normatively, sourced, with a symbolic form and concrete examples, and a +conformance harness any engine can be run against. It lives in its own +Cargo workspace inside this repository until the harness is stable enough +to move to its own repository (P13 step 6, D-34, Z-17): + +- `bookspec/checker` is engine-independent: the example schema and its + parser (`Example::from_toml`, every structure `deny_unknown_fields`, the + cross-field shape checked by `Example::validate`), the rule catalogue + (`rules()`, `RuleId` `BS--`, the refusal-to-rule table) and the + `Engine` trait (`load`, `apply`, `query`, `claims`, and `change` for a + `[[changes]]` entry, which posts no entry and so is neither a setup nor + an event). +- `bookspec/runner` drives any `Engine` through the examples and reports + per rule: pass, fail or not claimed. It compares the engine's answers + with the examples' exact expected values; it never asks the engine + whether its own answer is right. +- `bookspec/spec/rules-index.md` maps B-1 to B-45 to the bookspec ids; + `docs/spec/rules.md` carries each stated rule's id under its heading. +- `crates/ledger-core/tests/bookspec.rs` is the wanderbooks adapter: it + implements `Engine` over `Store` (a dev-dependency only, so no engine + crate depends on the specification) and remains the loader that checks + every figure. `ledger-package` answers the formats appendix's IIF + queries through the same adapter, as the package test does. + +Neither bookspec crate depends on `ledger-core`; the dependency points +inward from the adapter. `just check` runs the bookspec workspace's +format, clippy and tests beside the wanderbooks workspace's, so an example +cannot rot (P13 step 1). diff --git a/docs/parity.md b/docs/parity.md index 2d42a33..ab4f4a5 100644 --- a/docs/parity.md +++ b/docs/parity.md @@ -71,3 +71,13 @@ do everything the GSheet+GDrive system does today? Tracked separately in `docs/capability-parity.md`, with its own cutover rule (parallel run + the owner's approval). The two catalogs converge: when both are green, the sheet retires. + +### P13 bookspec (2026-10-26) + +P13 is a specification, not a feature, so no row of the semantic catalog +moves and the floor stands. It adds the checker crate (`bookspec/checker`: +the example schema, the rule identifiers `BS--` and the `Engine` +trait), the runner (`bookspec/runner`: pass, fail or not claimed per rule), +and the rule index from B-1 to B-45 (`bookspec/spec/rules-index.md`). Every +existing bookspec example still runs on every `cargo test`, through both +the loader's own checks and the `Engine` trait. diff --git a/docs/spec/rules.md b/docs/spec/rules.md index 250eca9..66be869 100644 --- a/docs/spec/rules.md +++ b/docs/spec/rules.md @@ -3,19 +3,24 @@ Every rule the engine enforces, the authority for it, and what checks it. Status is **AUTHORITY** (a source states the rule), **CONVENTION** (common practice no authority states; the best reference is given), or -**DECISION** (a human chose, and when). Vendored sources are in +**CHOICE** (a human chose, and when; bookspec's CHOICE: the specification +names the options, an engine declares its pick). Vendored sources are in `sources/`; FASB and OpenStax are quoted briefly under their copyright and license (see `README.md`). Quotes from FASB's Codification were read in Deloitte's DART roadmaps, which reproduce it ("FASB Accounting Standards Codification: Copyright © 2005 - 2026 by Financial Accounting Foundation"). -Spec blocks in the code cite these as `SOURCE: rules.md B-n`. +Spec blocks in the code cite these as `SOURCE: rules.md B-n`. Every rule +also has a bookspec identifier, `**bookspec:** BS--`: the table +from B-1 to B-45 is `bookspec/spec/rules-index.md` (P13, D-34). --- ## B-1 Every entry is a double entry: debits equal credits +**bookspec:** `BS-entry-1` + **Status:** AUTHORITY - Pacioli ch. 36 (`sources/pacioli-1494.md`): "All entries made in the @@ -28,7 +33,7 @@ Spec blocks in the code cite these as `SOURCE: rules.md B-n`. **Enforced:** `kernel::check_lines` via `JournalEntry::validate` (two or more lines, debits = credits exactly). A line's account must exist and -take postings when the line is added, not only at post: DECISION +take postings when the line is added, not only at post: CHOICE (the owner, 2026-10-13), `Ledger::add_line` (`a_line_on_an_unknown_or_placeholder_account_is_refused_when_added`). **Checked:** Kani `check_lines_is_exact`, `accepted_entry_nets_to_zero`; Lean @@ -36,6 +41,8 @@ take postings when the line is added, not only at post: DECISION ## B-2 A line's amount is positive; its side gives the direction +**bookspec:** `BS-entry-2` + **Status:** CONVENTION Pacioli keeps amounts in a debit column and a credit column (ch. 36: "All @@ -50,6 +57,8 @@ that every line reads one way (2026-10-13, found by the formal pass). ## B-3 One money through the ledger; foreign amounts at the transaction-date rate +**bookspec:** `BS-ledger-1` + **Status:** AUTHORITY - Pacioli ch. 36: "the money that you reckon by at the beginning should be @@ -62,7 +71,7 @@ that every line reads one way (2026-10-13, found by the formal pass). A foreign line is booked in the base currency's smallest unit (ISO 4217 minor units, `sources/iso-4217.md`), half away from zero; a ledger in one money holds no fractions of its smallest unit, and its cash can then -match a bank statement: DECISION (the owner, 2026-10-13), after the GnuCash +match a bank statement: CHOICE (the owner, 2026-10-13), after the GnuCash cross-check found wanderbooks kept $108.34567 where GnuCash booked $108.35. Lines booked before then replay as booked. @@ -84,6 +93,8 @@ GnuCash oracle. ## B-4 Five elements, closed list +**bookspec:** `BS-ledger-2` + **Status:** AUTHORITY (definitions); the five-type chart is CONVENTION FASB Concepts Statement No. 8, Ch. 4 (2021), which replaced Concepts @@ -97,11 +108,13 @@ or carrying out other activities." E81 expenses "are outflows or other using up of assets … or incurrences of its liabilities …". **Enforced:** `AccountType` (five variants). A child account has its -parent's type: DECISION (the owner, 2026-10-13), `Ledger::add_account` +parent's type: CHOICE (the owner, 2026-10-13), `Ledger::add_account` (`ParentTypeMismatch`). **Checked:** `a_child_account_has_its_parents_type`. ## B-5 Normal balances +**bookspec:** `BS-ledger-3` + **Status:** CONVENTION No standard defines normal balances. OpenStax, *Principles of Accounting, @@ -117,6 +130,8 @@ with a credit, their 'normal' balance is a credit." Pacioli ch. 12: capital ## B-6 The trial balance balances +**bookspec:** `BS-statement-1` + **Status:** AUTHORITY Pacioli ch. 36: "the total of the credits — I do not say creditors — @@ -129,6 +144,8 @@ Lean `trial_balance_iff`, `Reachable.trial_balance`, ## B-7 The accounting equation +**bookspec:** `BS-statement-2` + **Status:** AUTHORITY CON 8 Ch. 4 ¶E62 (cited): equity "is the difference between the entity's @@ -141,6 +158,8 @@ of equity (B-11). ## B-8 Posted is forever: the book is written only forward +**bookspec:** `BS-ledger-4` + **Status:** AUTHORITY - Pacioli ch. 36: "You should not write backward, but always forward — @@ -158,6 +177,8 @@ hash-chained. **Checked:** Lean `Step.extends`, `Steps.extends`; property ## B-9 A mistake is corrected by an opposite entry, never erased +**bookspec:** `BS-ledger-5` + **Status:** AUTHORITY Pacioli ch. 31 (the *storno*): "you shall make another entry opposite this @@ -172,7 +193,9 @@ Lean `accepted_reversal_iff`, `movement_with_reversal`. ## B-10 An entry is reversed at most once; a closing entry never -**Status:** DECISION (the owner, 2026-10-13) +**bookspec:** `BS-ledger-6` + +**Status:** CHOICE (the owner, 2026-10-13) Follows from B-9 (a correction corrects once) and B-11 (a closed period stays closed). **Enforced:** `Ledger::check_reversal`. **Checked:** @@ -180,6 +203,8 @@ stays closed). **Enforced:** `Ledger::check_reversal`. **Checked:** ## B-11 Revenue and expenses close into profit and loss, and that into capital +**bookspec:** `BS-period-1` + **Status:** AUTHORITY (mechanics); the account the net lands in depends on the entity @@ -197,13 +222,15 @@ the entity ## B-12 A closed period takes no new postings; periods close in order +**bookspec:** `BS-period-2` + **Status:** CONVENTION Periods themselves: IRS Pub. 538 (`sources/irs-periods-and-methods.md`) defines the tax year, calendar and fiscal. Locking a closed period and closing in order are practice (and follow from B-8: a closed period's book is not rewritten). Periods nest (a month in a quarter in a year) -and never partially overlap: DECISION (the owner, 2026-10-13), +and never partially overlap: CHOICE (the owner, 2026-10-13), `Ledger::create_period` (`PeriodsOverlap`; `periods_nest_but_never_partially_overlap`). **Enforced:** `closed_period_containing`, `CloseOutOfOrder`, `Store::rehearse`. **Checked:** Lean `Steps.frozen`; @@ -212,10 +239,12 @@ closed period). ## B-13 Partners share income by the partnership agreement; capital accounts carry it +**bookspec:** `BS-partner-1` + **Status:** AUTHORITY (shares; capital accounts; a count ends where the -members or shares change: §706(d)(1), Treas. Reg. §1.706-4(a)); DECISION -(cent rounding, the owner 2026-10-13); DECISION (counted from the company's -first close, D-10, 2026-10-16); DECISION (the statement splits unclosed +members or shares change: §706(d)(1), Treas. Reg. §1.706-4(a)); CHOICE +(cent rounding, the owner 2026-10-13); CHOICE (counted from the company's +first close, D-10, 2026-10-16); CHOICE (the statement splits unclosed earnings the same running way, D-38, 2026-10-20) - 26 U.S.C. §704(a) (`sources/irs-partnerships.md`): "A partner's @@ -238,7 +267,7 @@ earnings the same running way, D-38, 2026-10-20) worksheet carries the book capital analysis, and beside it the tax-basis capital as far as the books know it (B-36). - **No authority** says how to round a share to the cent. Largest - remainder, ties to the member listed first: DECISION (the owner, + remainder, ties to the member listed first: CHOICE (the owner, 2026-10-13). Closes made before then replay with the last member absorbing it. - **Members closes split cumulatively, counted from the company's first @@ -323,7 +352,7 @@ earnings the same running way, D-38, 2026-10-20) service contract credited straight to capital is property (a contract handed over), while a prepaid service they paid a vendor for on the company's behalf, booked through Due to Shareholders (Dr prepaid / Cr - 2100, then Dr 2100 / Cr capital), is cash (DECISION, the owner, + 2100, then Dr 2100 / Cr capital), is cash (CHOICE, the owner, 2026-10-15); and it is shared among members credited in one entry in proportion to their credits (`k1` INVARIANTS 3, 4, 6). @@ -364,7 +393,9 @@ earnings the same running way, D-38, 2026-10-20) ## B-14 Statements: P&L for a period; cash flows by the direct method -**Status:** AUTHORITY, except where marked CONVENTION or DECISION +**bookspec:** `BS-statement-3` + +**Status:** AUTHORITY, except where marked CONVENTION or CHOICE The P&L covers a period; a closing entry is not income or spending (B-11), so period statements exclude it. @@ -575,7 +606,7 @@ in public sources that reproduce the Codification, `sources/fasb-asc.md`): close has no cash line. 11. **What a member or a lender pays, or receives, for the company is cash the company borrowed (or a member contributed) and spent** - (DECISION, D-42, the owner, 2026-10-20, revising D-35's first point; + (CHOICE, D-42, the owner, 2026-10-20, revising D-35's first point; bookspec's CHOICE: the standard leaves it open). *Source.* The Codification does not address a payment a third party makes on the entity's behalf (PwC Viewpoint FSP 6.9.3: "ASC 230 does @@ -648,7 +679,7 @@ in public sources that reproduce the Codification, `sources/fasb-asc.md`): contribution), and none of the guides read presents an expense someone paid on the entity's behalf as noncash. The books' own K-1 worksheet already counts it as a cash contribution (`k1.rs` - INVARIANT 3, DECISION 2026-10-15), so the two agree. + INVARIANT 3, CHOICE 2026-10-15), so the two agree. - **Card purchases** follow the card's classification (D-35, unchanged): on a card listed as financing, the issuer is a lender that paid the merchant, so the purchase is borrowed and spent and the @@ -761,6 +792,8 @@ the tools' `ledger-cli` `tests/p3_seams.rs`). ## B-15 Depreciation: straight line, salvage never depreciated, partial months by days +**bookspec:** `BS-asset-1` + **Status:** CONVENTION (book method); AUTHORITY for tax conventions, which wanderbooks does not apply @@ -770,7 +803,7 @@ life of the asset," with "(Cost – salvage value)/Useful life"; partial periods: "A common method is to allocate depreciation expense based on the number of months the asset is owned in a year." wanderbooks prorates partial months by days, including a schedule through a mid-month date -(DECISION, the owner 2026-10-13). ASC 360-10-35-4 (read 2026-10-14, +(CHOICE, the owner 2026-10-13). ASC 360-10-35-4 (read 2026-10-14, `sources/fasb-asc.md`) prescribes no method: cost less salvage is spread over the useful life "in a systematic and rational manner," as equitably as possible to the periods served; straight line by days is one such @@ -948,6 +981,8 @@ dates, `accumulated_posted` and its edges, ## B-16 Prepaid expenses and deferred revenue are recognized over the period benefited or earned +**bookspec:** `BS-prepaid-1` + **Status:** AUTHORITY (revenue); CONVENTION (prepaids) ASC 606-10-45-2 (cited): payment before transfer → "the entity shall @@ -965,13 +1000,13 @@ spreads by the day from its start (the first day covered), as depreciation does (B-15 rules 1-4: full months equal, the first and last partial months by their days, the last taking what the others leave, rounded to the minor unit): a 12-month policy from March 15 posts -thirteen month ends, a small March and a small final March (DECISION, +thirteen month ends, a small March and a small final March (CHOICE, the owner, D-11 confirmed in D-37, 2026-10-20). Schedules registered before P3 keep equal months, the last absorbing rounding, forever: a posted month's replay re-derives its plan. `books.schedule_convention` sets the convention new schedules take (unset: daily). An equal-months schedule starts on the 1st of a month, for prepaid expenses and deferred revenue -alike (DECISION, the owner, D-37, 2026-10-20): revenue is never recognized +alike (CHOICE, the owner, D-37, 2026-10-20): revenue is never recognized before it is earned (ASC 606-10-25-23), and one rule for both is simpler and never early; schedules an earlier version took mid-month replay as they were. A reversed month is un-posted, so it is due again; a month in @@ -1008,6 +1043,8 @@ B-16 cites them (P3 depreciation review R7). ## B-17 Platform payouts are recorded gross when the business is the principal +**bookspec:** `BS-revenue-1` + **Status:** AUTHORITY ASC 606-10-55-37 (cited): "An entity is a principal if it controls the @@ -1024,17 +1061,19 @@ CPA confirms. ## B-18 Aging buckets: Current, 1–30, 31–60, 61–90, 90+ days past due -**Status:** CONVENTION (bucket edges); DECISION (due today is Current) +**bookspec:** `BS-arap-1` + +**Status:** CONVENTION (bucket edges); CHOICE (due today is Current) ASC 326-20-55-37/38 (cited) illustrates "one way" to age trade receivables with buckets "current … 1–30 days past due … 31–60 … 61–90 … more than 90 days past due"; it is illustrative, not a rule. The FFIEC Call Report instructions (bank regulators, not binding here): "The past due status … should be determined in accordance with its contractual -repayment terms." A document due today is not yet past due: DECISION +repayment terms." A document due today is not yet past due: CHOICE (the owner, 2026-10-13). Aging as of a date shows only documents issued by then, less only payments made by then; and a payment never predates its -invoice or bill (a real prepayment is a deposit): DECISIONS (the owner, +invoice or bill (a real prepayment is a deposit): CHOICES (the owner, 2026-10-13). What a document owes as of a date is read from its ledger lines that stand then (B-38): a void invoice is in no bucket, a payment reversed by then no longer counts, one reversed later still does; the @@ -1050,8 +1089,10 @@ buckets partition what the documents owe, exactly. ## B-19 1099 reporting thresholds, payment methods, and decisions +**bookspec:** `BS-1099-1` + **Status:** AUTHORITY (`sources/irs-1099-thresholds.md`, -`sources/irs-1099-payment-method.md`), with the DECISIONs named below. +`sources/irs-1099-payment-method.md`), with the CHOICEs named below. $600 through 2025; $2,000 for payments made after Dec. 31, 2025 (26 U.S.C. §6041(a), §6041A(a)(2), as amended by Pub. L. 119-21 §70433; §70433(f): @@ -1107,7 +1148,7 @@ card's account (a business_card method's), or other. First match wins: |---|---| | the entry's method pays through this line's account, card or network | card (excluded) | | the entry's method pays through this line's account, bank or a member's own cash or check | counts | -| a cash account (no method, or one naming another account) | counts: presumed check, ACH or transfer (DECISION D-40, owner, 2026-10-20); tag a debit-card payment with the card | +| a cash account (no method, or one naming another account) | counts: presumed check, ACH or transfer (CHOICE D-40, owner, 2026-10-20); tag a debit-card payment with the card | | a financing account, or a member's or platform method's account (no method, or one naming another account) | undetermined until the method is recorded | | a company card's account (a business_card method's) | card (excluded) | | anything else (an expense, payables, prepaid) | no payment line | @@ -1148,7 +1189,7 @@ employee's wages go on Form W-2 (§1.6041-2(a)(1)). Paid by card is never a vendor's reason: it is a property of each payment. Every new decision names who made it and when (`settings::Source`). -**Dated decisions** (DECISION D-40, owner, 2026-10-20: "legally this has +**Dated decisions** (CHOICE D-40, owner, 2026-10-20: "legally this has to be 2 dated statuses"). Each decision takes effect on a date (a W-9's, or a tax election's: Form 8832's effective date, §301.7701-3(c)(1)(iii)), or from the beginning; each payment takes the decision in force on its @@ -1203,7 +1244,7 @@ named beside another vendor (`add_vendor`), checked by `a_named_party_gaining_a_vendor_record_keeps_one_vendor_per_entry`. Shares that don't resolve count the whole payment toward the vendor (the conservative answer) and the entry is listed (`Vendor1099Report:: -unresolved`; a `books.vendor_1099` note). DECISION (the owner, +unresolved`; a `books.vendor_1099` note). CHOICE (the owner, 2026-10-15: shares). **The quieter note.** Untagged posted entries are named only when they @@ -1229,7 +1270,7 @@ contradict. (`ledger_core::rules::THRESHOLD_1099`, known through 2026, review from 2026-11-01). For a later year the engine refuses with `RuleNotKnown` rather than guess; the MCP digest and the viewer's "Needs attention" list -flag the rule once it is due (DECISION, the owner, 2026-10-13). The +flag the rule once it is due (CHOICE, the owner, 2026-10-13). The bucket table is `kernel::payment_bucket` (Kani `payment_bucket_is_the_table`, `payment_split_is_exact`, `net_buckets_keeps_the_total`). **Checked:** `an_unknown_year_threshold_is_never_guessed`, @@ -1266,6 +1307,8 @@ MCP tests through a real books server (ledger-cli); bookspec ## B-20 Every transaction has supporting documents; records are kept +**bookspec:** `BS-ledger-7` + **Status:** AUTHORITY (`sources/irs-records.md`) IRS Pub. 583: "In addition, you must keep supporting documents"; they @@ -1277,6 +1320,8 @@ generally; 7 for bad debts). ## B-21 Exact amounts in the books; rounding only on returns +**bookspec:** `BS-entry-3` + **Status:** AUTHORITY (for where rounding happens) Form 1065 instructions (`sources/irs-partnerships.md`): "If two or more @@ -1289,6 +1334,8 @@ cents (and FX precision) exactly (R-2). ## B-22 Expense or capitalize (book policy, mirrors tax) +**bookspec:** `BS-asset-2` + **Status:** AUTHORITY (`sources/irs-capitalization.md`) Treas. Reg. §1.263(a)-1(f)(1)(ii)(D) sets $500 per invoice or item for a @@ -1301,8 +1348,10 @@ taxable years beginning on or after January 1, 2016. The 12-month rule: ## B-23 Control roles: one account each, of the right kind, moved only when empty +**bookspec:** `BS-ledger-8` + **Status:** CONVENTION (a control account agrees with the records it -controls); DECISIONS (the owner, 2026-10-13 and 2026-10-15) +controls); CHOICES (the owner, 2026-10-13 and 2026-10-15) Invoices, bills, payroll, and closes post to accounts the engine picks by role: receivables, revenue, payables, general expense, retained earnings, @@ -1310,9 +1359,9 @@ salary and payroll tax expense, and five payroll withholdings. A role uses its default code (1200, 4000, 2000, 6999, 3999, 6100, 6110, 2110–2150) until the human assigns it one of the chart's own accounts; the assignment is journaled, so replay never depends on a setting outside the journal -(DECISION, 2026-10-15). A role's account is postable and of the role's +(CHOICE, 2026-10-15). A role's account is postable and of the role's type (B-4), and the engine refuses one that isn't rather than renumber -the chart (DECISION, 2026-10-13). No account fills two roles. A +the chart (CHOICE, 2026-10-13). No account fills two roles. A receivables, payables, withholding, or retained earnings account must keep agreeing with what it controls (open invoices, open bills, taxes withheld, closed earnings): such a role moves only when its account holds no @@ -1340,7 +1389,9 @@ frees its code); the control guard and `Ledger::control_check` (B-38). ## B-24 Asset tags: one label per asset, never reused -**Status:** DECISION (the owner, 2026-10-15) +**bookspec:** `BS-asset-3` + +**Status:** CHOICE (the owner, 2026-10-15) Each fixed asset may carry a tag, the label stuck on the thing itself (generated `PREFIX-0001` or the human's own: 1–32 ASCII letters, digits, @@ -1363,8 +1414,10 @@ the books' figure, never the schedule's (P3, defect 13). ## B-25 Tax figures beside book value: the §723 basis, the depreciable basis, the §704(c) built-in gain -**Status:** AUTHORITY (`sources/irs-partnership-basis.md`); DECISION -(the owner, 2026-10-15: the register carries tax basis); DECISION D-41 +**bookspec:** `BS-asset-4` + +**Status:** AUTHORITY (`sources/irs-partnership-basis.md`); CHOICE +(the owner, 2026-10-15: the register carries tax basis); CHOICE D-41 (the owner, 2026-10-20: the depreciable basis of property a member contributed from personal use is the lesser of its fair market value and the member's adjusted basis, the CPA to confirm at year end; the @@ -1440,8 +1493,10 @@ never assumed). **Checked:** `ledger-core` `tests/spec_asset_basis.rs`, ## B-26 Parties: one directory, no full TIN in the books +**bookspec:** `BS-partner-2` + **Status:** AUTHORITY (truncated TINs: `sources/irs-truncated-tin.md`); -DECISIONS (the owner, 2026-10-15) +CHOICES (the owner, 2026-10-15) Everyone the company deals with is a party, with roles (vendor, contractor, customer, employee, member, payout platform, tax agency, @@ -1451,12 +1506,12 @@ not archived, ignoring case and surrounding space. A W-9 is recorded as its facts and the TIN's last four digits (the part a truncated TIN shows, Treas. Reg. §301.6109-4(a)); no party field may hold a nine-digit number, and the full TIN lives only in the encrypted vault, outside the journal -(DECISION). W-9 facts need a 1099 role (contractor, landlord); W-4 facts +(CHOICE). W-9 facts need a 1099 role (contractor, landlord); W-4 facts need the employee role; payroll lines that name their employee name a party with that role. A project's text (its name, description, links and notes: B-37) holds no full TIN either (`settings::line_text`, `long_text`). A link is a web address, so its rule reads it as one -(`settings::holds_printed_tin_in_link`, DECISION of this rule, so store +(`settings::holds_printed_tin_in_link`, CHOICE of this rule, so store listings can be kept): the printed SSN (3-2-4) and EIN (2-7) forms and a bare nine digits standing alone are refused, but nine digits joined to a letter or to `/ = ? & #` are an id in a path or query (an app store @@ -1471,13 +1526,13 @@ under the book's own key, `keys/legacy.key` beside it): `books.set_w9` journals only the last four and the vault record's hash; the signed W-9 goes there too, never to the attachment store. Any party may have its TIN in the vault, whatever its roles: every member's (for their K-1) and -every 1099 payee's belongs there (DECISION, the owner, 2026-10-15: "In the +every 1099 payee's belongs there (CHOICE, the owner, 2026-10-15: "In the vault, that's the WHOLE reason we built it. And it needs to be in the damn CPA package."). `books.set_tin` stores one for any party; the books journal its TIN facts (type, last four, the record's hash: `party.tin_set`), which never disagree with its W-9 facts' last four. No tool output, error, or log holds a full TIN except `books.vendor_1099` -with `include_tins` (DECISION, the owner, 2026-10-15: for the CPA, when +with `include_tins` (CHOICE, the owner, 2026-10-15: for the CPA, when asked): a refusal that quotes its input masks any TIN in it (`parties::redact_full_tins`, checked by `refusals_never_echo_a_tin`), and a party text field refuses nine digits however they are separated @@ -1498,10 +1553,10 @@ over as an export the asking token fetches with `wanderbooks download` (kept 15 minutes), never inline (D-44; since D-46 no tool takes or gives any file's bytes inline: files come in only by upload and go out only by download). (Until slice 6 they were -written only outside the books dir: DECISION, P0 review, 2026-10-16, so +written only outside the books dir: CHOICE, P0 review, 2026-10-16, so none could be filed back in as a receipt and read out again.) No file holding a full TIN is filed into the books -(DECISION, P0 review round 2, 2026-10-16): the server keeps, beside the +(CHOICE, P0 review round 2, 2026-10-16): the server keeps, beside the vault, the SHA-256 of every file it read or wrote with full TINs (the W-9 `books.set_w9` reads, recorded once the call has passed every check, just before the vault keeps it, so a refused call marks no file; each @@ -1526,9 +1581,9 @@ or dot followed by a digit, on either side, except the company's own EIN as its settings hold it, printed 2-7: it is the company's, no person's, the settings already hold it in full (B-29), and every company document goes into the books, its formation papers and -plans printing it (DECISION, the owner, 2026-10-17); (a) still refuses it +plans printing it (CHOICE, the owner, 2026-10-17); (a) still refuses it should the vault hold that number for a party. Full-width digits and -Unicode hyphens and spaces count as their ASCII kin (`tin_files.rs`). The text check is narrow on purpose (DECISION, +Unicode hyphens and spaces count as their ASCII kin (`tin_files.rs`). The text check is narrow on purpose (CHOICE, the owner, 2026-10-16): bank exports are filed for reconciliation, and their routing numbers, long references and dates next to amounts are not TINs; the broad `has_full_tin` reading stays on what the server says @@ -1536,7 +1591,7 @@ not TINs; the broad `has_full_tin` reading stays on what the server says answer would tell an agent whether a text it chose holds a vault TIN; the tools show each TIN's last four, so an agent filing texts full of nine-digit numbers ending in those four could find the TIN in about 17 -filings. So (a) answers for at most two candidates a filing (DECISION, +filings. So (a) answers for at most two candidates a filing (CHOICE, P0 review, 2026-10-16): before the vault is opened, a text holding more than two distinct TIN-shaped numbers (in any rendering (a) reads; one number written several ways counts once) that end in the last four of a @@ -1593,10 +1648,12 @@ unit tests in `tin_files.rs`. ## B-27 The journal format gate: newer is not corrupt -**Status:** DECISION (the owner, 2026-10-16; `plan/served.md` 5.1 and P0); -DECISION (P0 review, 2026-10-16: a line's own hash is checked before the +**bookspec:** `BS-format-1` + +**Status:** CHOICE (the owner, 2026-10-16; `plan/served.md` 5.1 and P0); +CHOICE (P0 review, 2026-10-16: a line's own hash is checked before the line is called newer; the envelope and its hash are frozen across -formats); DECISION (the owner, 2026-10-20, D-39, and `plan/served.md` +formats); CHOICE (the owner, 2026-10-20, D-39, and `plan/served.md` 3.9: older books move to a newer format only with the owner's go-ahead, asked at every open until given) @@ -1789,11 +1846,13 @@ schema); `ledger-mcp` `local.rs` (`format_upgrade_waits_for_consent`, ## B-28 What new commands take: four-digit years, amounts a line can carry +**bookspec:** `BS-entry-4` + **Status:** CONVENTION (ISO 8601's basic calendar dates have four-digit years, as do the spreadsheet and PDF formats the books export to); -DECISION (P0 review round 3, 2026-10-16, finding S1: the server stops +CHOICE (P0 review round 3, 2026-10-16, finding S1: the server stops for every session when a request panics, so no input may reach one); -DECISION (P0 review round 4, 2026-10-17: posted is forever, so the +CHOICE (P0 review round 4, 2026-10-17: posted is forever, so the bounds are the commands', never replay's) **New commands.** Every date a command takes or derives is in @@ -1904,7 +1963,9 @@ pinned; every tool over every old book) and `server.rs` ## B-29 The books keep how they are kept -**SOURCE:** DECISION (plan/served.md I-3, owner decisions of 2026-10-16: +**bookspec:** `BS-ledger-9` + +**SOURCE:** CHOICE (plan/served.md I-3, owner decisions of 2026-10-16: "the journal holds all books state"; light governance: decisions are recorded with who made them and when, not as paperwork); IRS Pub. 538 and the rules of B-1 to B-28 for what each setting means. @@ -1978,12 +2039,14 @@ checked to change no figure or entry; `approval_rules_off_by_default`, ## B-30 A report asked for without dates covers this fiscal year so far +**bookspec:** `BS-period-3` + **Status:** AUTHORITY for the period (IRS Pub. 538, `sources/irs-periods-and-methods.md`: "A tax year is an annual accounting period for keeping records and reporting income and expenses"; 26 U.S.C. §6041(a), "in any calendar year", and §6041A(a)(2), "during such calendar year", `sources/irs-1099-thresholds.md`, for the calendar-year counts); -DECISION for using it as the default (no authority says what a tool's +CHOICE for using it as the default (no authority says what a tool's omitted dates mean; the owner, 2026-10-20, `plan/served.md` P3 "Settled in design": "a report asked for without dates covers this fiscal year so far, everywhere, and an account's register opens on it with 'Show all @@ -2048,7 +2111,9 @@ generated descriptions (`described`) and parameter docs; `ledger-gui` ## B-31 Renaming an account changes its label, nothing else -**Status:** DECISION (the owner, `plan/served.md` 4.9 and P3 "Settled in +**bookspec:** `BS-ledger-10` + +**Status:** CHOICE (the owner, `plan/served.md` 4.9 and P3 "Settled in design", 2026-10-20: "a new account named like a sibling (ignoring capitals), or holding a nine-digit number, is refused"); B-8 (posted is forever); AUTHORITY for keeping the old name (Rev. Proc. 98-25 §5.01, @@ -2111,13 +2176,15 @@ bookspec `examples/rename-changes-no-amount.toml`. ## B-32 Every revenue and expense account maps to a line of the return +**bookspec:** `BS-ledger-11` + **Status:** AUTHORITY for why it matters (IRS Pub. 583, `sources/irs-records.md`: "Your books must show your gross income, as well as your deductions and credits"; IRS Pub. 538: "you must maintain records that will enable you to file a correct return"; Rev. Proc. 98-25 §5.01: the relationship "between the total of the amounts in the taxpayer's machine-sensible records by account and the taxpayer's -return"); DECISION for warn versus refuse (Pub. 583: "the law does not +return"); CHOICE for warn versus refuse (Pub. 583: "the law does not require any specific kind of records"; the owner, 2026-10-20: warn by default, header accounts exempt, the first company set to refuse at the P3 cutover). @@ -2170,11 +2237,13 @@ bookspec `examples/tax-category-required.toml`. ## B-33 A package as of a day shows the books through that day +**bookspec:** `BS-statement-4` + **Status:** CONVENTION (a report as of a day reports the books on that day: balances through it, a period's activity from the period's start through it; nearest authorities the accounting period, IRS Pub. 538, `sources/irs-periods-and-methods.md`, and the information returns' -calendar year, `sources/irs-1099-thresholds.md`); DECISION (owner, +calendar year, `sources/irs-1099-thresholds.md`); CHOICE (owner, 2026-10-20, plan D-36: the 1099 worksheet stops at as_of too) Nothing in the CPA package made as of a day (`books.export_cpa_package`, @@ -2226,6 +2295,8 @@ and `bookspec/examples/1099-worksheet-through-as-of.toml` (`ledger-core` ## B-34 The QuickBooks Desktop file (IIF): plain ASCII, every entry through as_of, closes left out, classes carried +**bookspec:** `BS-format-2` + **Status:** AUTHORITY (Intuit, `sources/intuit-iif.md`: IIF files are ASCII text, tab-separated, with no escape; an account is known by its NAME, a subaccount's NAME its path joined by colons; lists come before @@ -2243,7 +2314,7 @@ QuickBooks Desktop, as an account's); CONVENTION (a QuickBooks integrator and an IIF importer, same file: a longer name is cut on a chart import but not in a journal's lines; a double quote is a text delimiter; Conductor's QuickBooks Desktop API: a class's name -case-insensitive, so no two alike ignoring case); DECISION (owner, 2026-10-20, plan D-36: plain ASCII, what isn't +case-insensitive, so no two alike ignoring case); CHOICE (owner, 2026-10-20, plan D-36: plain ASCII, what isn't ASCII transliterated and listed; CR LF; DOCNUM the entry's id; closing entries left out; every entry since the company began until the CPA says otherwise); Unicode Standard Annex #14 (the mandatory line breaks) @@ -2386,8 +2457,10 @@ appendix: the file line by line and its changes, derived by hand); ## B-35 A party's part of an entry, and a party's totals +**bookspec:** `BS-partner-3` + **Status:** CONVENTION (the sign: B-2's, a line on the party's own -account); DECISION (the owner, 2026-10-15: `entry_flow`, money paid down +account); CHOICE (the owner, 2026-10-15: `entry_flow`, money paid down counted once; the owner, 2026-10-20, P3 design settled with each review's recommendation: a member's entry counts what moved their capital, money is cash, cards, the members' financing and loans, and a liability credit @@ -2615,10 +2688,12 @@ distributions declared or of depreciated property, the properties), Kani ## B-36 The K-1 worksheet reports tax-basis capital beside book capital +**bookspec:** `BS-partner-4` + **Status:** AUTHORITY (`sources/irs-partnerships.md`: Instructions for Form 1065 (2025), Item L, "Tax-basis method"; required from tax year 2020: Instructions for Form 1065 (2020), Item L, "you must use the tax -basis method this year"); DECISION D-41 (the owner, 2026-10-20: "the +basis method this year"); CHOICE D-41 (the owner, 2026-10-20: "the K-1 worksheet reports tax-basis capital ... the contributed property at its §723 carryover basis, beside the book figures") @@ -2670,7 +2745,7 @@ book capital reconciles to tax capital: account of a prepaid-expense schedule: a service contract the member handed over, B-13) with no fixed asset for it enters at its book amount, with a note: a contract's carryover basis is what the books - recorded for it (DECISION, P3 review 2026-10-21; the CPA to confirm). + recorded for it (CHOICE, P3 review 2026-10-21; the CPA to confirm). - What the books can't know is never guessed. The share of net income "as figured for tax purposes" is the return's, always: the books are kept on their own basis, and every book-tax difference (Schedule M-1) @@ -2711,11 +2786,13 @@ package); the odd cents, `ledger_core::tax_capital::RunningSplit` on ## B-37 A project is a class with its facts; its figures are its tagged lines -**Status:** DECISION (the owner, 2026-10-21: "a Projects page that treats +**bookspec:** `BS-statement-5` + +**Status:** CHOICE (the owner, 2026-10-21: "a Projects page that treats a class as a project ... Projects are entities too and can have name, description, whatever other metadata you think they should have"; the convention `link-to-projects` in the books: "whenever applicable, we link -things to [projects]"); DECISION (the owner, 2026-10-22, plan D-43: an invoice's or bill's +things to [projects]"); CHOICE (the owner, 2026-10-22, plan D-43: an invoice's or bill's lines take a project, each line one or none, item 8); CONVENTION (job costing, items 3 and 8; the snapshot, item 7: a presentation choice from management accounting, not GAAP); AUTHORITY (on @@ -2877,7 +2954,9 @@ proptests `prop_project_nets_partition_the_pnl`, ## B-38 A document's figures are its ledger lines that stand; a control account is its documents -**Status:** DECISION (the owner, 2026-10-23, plan D-45: "we kinda need +**bookspec:** `BS-arap-2` + +**Status:** CHOICE (the owner, 2026-10-23, plan D-45: "we kinda need stronger invoice support. Like make it a first class citizen"; hand entries to receivables and payables refused, the owner's answer 5); CONVENTION (a control account agrees with its subledger, as B-23; the diff --git a/docs/testing.md b/docs/testing.md index 7011e5f..e18efef 100644 --- a/docs/testing.md +++ b/docs/testing.md @@ -2847,3 +2847,71 @@ nine-digit check), `full_tin_runs`'s `extend(grouped_tin_runs(..))`; Deleting any one of them leaves a TIN that the old scanner passed either readable or unmasked, which the truth table and the property catch. +## P13: bookspec, the checker crate and the runner (2026-10-26) + +bookspec's example schema moved out of the loader into +`bookspec/checker`, its own Cargo workspace inside the repository (P13 +step 1, D-34). `crates/ledger-core/tests/bookspec.rs` is now the +wanderbooks adapter: it imports `Example`, `Setup`, `Event`, `Expect`, +`Query`, `Refusal` and the rest from the checker by a path dev-dependency, +and it is unchanged in what it proves. The loader parses every example +with `Example::from_toml` (the parser plus the schema's own cross-field +checks), so a malformed example fails before an engine sees it. + +Step 2 adds the `Engine` trait (`bookspec/checker/src/engine.rs`: `load`, +`apply`, `query`, `claims`, and `change` for a `[[changes]]` entry), the +runner (`bookspec/runner`), and the adapter's implementation over `Store`. +The runner runs all 56 examples, one fresh engine each, compares every +answer with the example's exact values and every refusal with the reason +the example names, and reports per rule: pass, fail or not claimed. The +adapter claims the rules the catalogue gives an example; a rule with no +example is not claimed, never silently passed. + +Step 3 adds the rule identifiers: `BS--` for the eleven areas, +`bookspec/spec/rules-index.md` as the table from B-1 to B-45, and each +stated rule's id under its heading in `docs/spec/rules.md`. `DECISION` +there is bookspec's `CHOICE`: the specification names the options, an +engine declares its pick (`Claims::with_choice`). B-39 to B-45 are +allocated (`plan/served.md` 8.0.3) and not yet stated; they carry an id and +an area and no status. + +- **`bookspec/checker` `tests/schema.rs`:** `every_seed_example_parses_and_validates` + (all 56, the file named for its id, ids unique), `an_unknown_key_is_refused`, + `the_status_is_authority_convention_or_choice`, `a_line_amount_is_exact_and_positive`, + `an_event_is_exactly_one_kind`, `reverses_counts_events_from_one`, + `the_members_shares_sum_to_one`, `a_share_is_a_fraction_above_zero`, + `every_refusal_reason_names_a_rule`, `an_unknown_query_is_refused`, + `a_period_may_be_refused`, `a_command_shows_no_values`, + `a_figure_query_takes_exact_decimals`, `a_query_does_not_carry_its_expected_values`, + `a_format_query_is_of_the_formats_appendix`, `the_rule_index_lists_every_stated_rule`. +- **`bookspec/checker` `tests/rules.rs`:** `every_b_rule_has_one_id`, + `the_index_matches_the_catalogue`, `every_example_is_listed_under_a_rule`, + `a_claimed_rule_has_an_example`, `an_id_has_one_spelling`, + `a_choice_rule_declares_its_option`. +- **`bookspec/checker` `tests/docs.rs`:** `rules_md_carries_every_id`. +- **`bookspec/runner` `tests/runner.rs`:** `a_passing_engine_passes`, + `a_wrong_answer_fails`, `an_engine_that_claims_nothing_is_not_claimed`, + `a_claim_without_an_example_fails`, `a_refused_query_fails`, + `an_example_listed_under_no_rule_fails`, `a_refused_change_fails`. The + fake engine proves the runner judges answers, not the engine's belief. +- **`ledger-core` `tests/bookspec.rs`:** `the_runner_reports_every_claimed_rule_green` + (all 56 examples through the trait, every claimed rule pass) and + `every_bookspec_example_holds` (unchanged: the loader's own checks). +- **`ledger-package` `tests/package.rs`:** `the_bookspec_iif_example_matches_the_engine` + unchanged; the adapter answers the formats appendix through + `ledger_package::export_iif`, the same code the package test checks. + +**For P12 (mutation targets):** `rules::build`'s area ordinals (a rule in +the wrong area); `RuleId::parse`'s prefix, area, digit, leading-zero and +zero checks; `rule_for_refusal`'s mapping arms; `schema::is_exact_decimal` +(each character class and the single point), `is_date`, `is_month`, +`is_month_day` and `check_amount`'s positivity; `Example::validate`'s +closed-list arms, the one-kind rule, `reverses < n`, the shares summing to +1, `after` bounds, the command's accepted-or-refused exclusivity and the +`only` table; `Query::is_complete`'s membership; `Refusal::of_reason`'s +fallback; the runner's `compare` (key equality, missing and extra keys, +list and row equality), `Outcome`'s mapping and `run`'s "claimed but no +example" arm; the adapter's `event_of`, its `depreciation_schedule` month +span (acquisition through as-of, 0.00 where the schedule reaches no month) +and every `answer_*` figure against the loader's same check (the two must +agree on all 56 examples).>>>>>>> 382e9406 (feat(bookspec): rule identifiers from B-1 to B-45 (P13))