From cb4e9559426753a9533b7a4e2baa45238d738f46 Mon Sep 17 00:00:00 2001 From: Stephen Waits Date: Mon, 26 Oct 2026 16:34:43 -0600 Subject: [PATCH] feat(bookspec): rule identifiers from B-1 to B-45 (P13) Every rule of `docs/spec/rules.md` now has one bookspec identifier, `BS--`: eleven areas (entry, ledger, statement, period, partner, asset, prepaid, arap, revenue, 1099, format), numbered in B-rule order within its area. The table is `bookspec/spec/rules-index.md`, and each stated rule carries its id under its heading in `docs/spec/rules.md`, so a reader of either can find the other. rules.md's DECISION status becomes bookspec's CHOICE: the specification names the options and an engine declares its pick (`Claims::with_choice`), which is what an example already meant by `status = "CHOICE"`. B-39 to B-45 are allocated in `plan/served.md` 8.0.3 and not yet stated: they carry an id and an area and no status, so a later unit fills them in without moving anyone else's number. The catalogue names the examples that demonstrate each rule (56 files, each under at least one rule) and the rule each refusal reason belongs to. A rule with no example cannot be claimed: a claim needs evidence. `docs/architecture.md`, `docs/testing.md` (tests and the For-P12 mutation targets) and `docs/parity.md` record the new shape; the bookspec README points at the checker, the runner and the adapter. --- bookspec/README.md | 10 +- bookspec/checker/tests/docs.rs | 28 +++++ docs/architecture.md | 32 ++++++ docs/parity.md | 10 ++ docs/spec/rules.md | 197 +++++++++++++++++++++++---------- docs/testing.md | 68 ++++++++++++ 6 files changed, 285 insertions(+), 60 deletions(-) create mode 100644 bookspec/checker/tests/docs.rs 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))