feat(bookspec): the specification text and an example for the ledger's own laws (P13 step 4)

bookspec/spec holds the first edition: the model and the five laws
(00-model.md) and one file per area, each B-rule a section with its
status, a MUST/MAY statement, sources pinned either to a vendored source
with a verbatim quotation or to an exact external paragraph, its symbolic
form, its examples and its applicability. B-39, B-40, B-43 and B-45 are
reserved sections (allocated, not yet stated in rules.md); B-44 is stated
in rules.md and now carries its bookspec id and CONVENTION status.

Eight new examples show the laws that had none, each figure worked by
hand: an unbalanced entry is refused (Pacioli ch. 36, Pub. 583), a
negative or zero line is refused, a fraction of a cent is refused (ISO
4217), a child account takes its parent's type, the trial balance and the
accounting equation on one small book (cash 6550.25 = loan 2000 + capital
5000 + (sales 750.50 - rent 1200.25)), a reversal leaves the original
standing and a second reversal is refused, a close sweeps revenue and
expenses into Retained Earnings (3999) and the closed quarter takes no
late entry, and cents add exactly. The refusal vocabulary gains
unbalanced, negative_amount, zero_amount, too_few_lines,
finer_than_minor_unit, parent_type_mismatch, already_reversed and
period_closed; a refused entry may carry a non-positive amount.

The checker's spec.rs fails the build on a rule without its section, a
statement without MUST/SHOULD/MAY, a pin that is not verbatim in its
vendored source, examples that differ from the catalogue, or more than 60
pages.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HS8WZziDnyxunC23MUxrLp
This commit is contained in:
swaitsandClaude Fable 5.1 committed 2026-10-29 12:51:30 -06:00
1 parent 055decf7e2
commit 1fbc1383cb
26 files changed
+1595 -36

No files matched your search

@@ -0,0 +1,75 @@
id = "entry-a-line-amount-is-positive"
title = "A negative or zero line amount is refused; the side gives the direction"
rule = "A line's amount MUST be greater than zero, and its side, not its sign, MUST give the direction money moves; an entry with a negative or zero line MUST be refused."
status = "CONVENTION"
sources = [
"Pacioli, Summa (1494), ch. 36: debtors at the left, creditors at the right (docs/spec/sources/pacioli-1494.md)",
"CONVENTION, the owner, 2026-10-13 (docs/spec/rules.md B-2): no authority forbids a negative amount outright; one reading of every line is chosen",
]
tags = ["accrual", "book", "us", "entry"]
[setup]
currency = "USD"
fiscal_year_start = "01-01"
basis = "accrual"
rounding = "half-even"
[[setup.accounts]]
code = "1000"
name = "Cash"
type = "asset"
[[setup.accounts]]
code = "3000"
name = "Capital"
type = "equity"
[[setup.accounts]]
code = "4000"
name = "Sales"
type = "revenue"
tax_category = "gross_receipts"
[[setup.accounts]]
code = "6000"
name = "Rent"
type = "expense"
tax_category = "rent_property"
# A credit of minus 100.00 would be a debit of 100.00 written the other
# way; the books keep one way to write it.
[[events]]
date = "2026-01-05"
description = "A negative credit"
refused = "negative_amount"
lines = [
{ account = "1000", side = "debit", amount = "100.00" },
{ account = "4000", side = "credit", amount = "-100.00" },
]
[[events]]
date = "2026-01-06"
description = "A zero line"
refused = "zero_amount"
lines = [
{ account = "1000", side = "debit", amount = "0.00" },
{ account = "4000", side = "credit", amount = "0.00" },
]
# The same 100.00 written with the sides doing the work is accepted.
[[events]]
date = "2026-01-07"
description = "A sale"
lines = [
{ account = "1000", side = "debit", amount = "100.00" },
{ account = "4000", side = "credit", amount = "100.00" },
]
[[expect]]
query = "trial_balance"
as_of = "2026-12-31"
[expect.values]
"1000" = "100.00"
"3000" = "0.00"
"4000" = "-100.00"
"6000" = "0.00"
@@ -0,0 +1,76 @@
id = "entry-an-unbalanced-entry-is-refused"
title = "An entry whose debits and credits differ is refused and posts nothing"
rule = "An entry MUST have debits equal to credits, exactly, and an entry that does not MUST be refused whole, leaving the books as they were."
status = "AUTHORITY"
sources = [
"Pacioli, Summa (1494), ch. 36: \"All entries made in the ledger have to be double entries\" (docs/spec/sources/pacioli-1494.md)",
"IRS Publication 583: \"the total debits must equal the total credits\" (docs/spec/sources/irs-records.md)",
]
tags = ["accrual", "book", "us", "entry", "double-entry"]
[setup]
currency = "USD"
fiscal_year_start = "01-01"
basis = "accrual"
rounding = "half-even"
[[setup.accounts]]
code = "1000"
name = "Cash"
type = "asset"
[[setup.accounts]]
code = "3000"
name = "Capital"
type = "equity"
[[setup.accounts]]
code = "4000"
name = "Sales"
type = "revenue"
tax_category = "gross_receipts"
[[setup.accounts]]
code = "6000"
name = "Rent"
type = "expense"
tax_category = "rent_property"
[[events]]
date = "2026-01-02"
description = "Opening contribution"
lines = [
{ account = "1000", side = "debit", amount = "1000.00" },
{ account = "3000", side = "credit", amount = "1000.00" },
]
# Debits 100.00, credits 90.00: ten dollars short on the credit side.
[[events]]
date = "2026-01-05"
description = "A sale that does not balance"
refused = "unbalanced"
lines = [
{ account = "1000", side = "debit", amount = "100.00" },
{ account = "4000", side = "credit", amount = "90.00" },
]
# One cent over is just as wrong: there is no epsilon.
[[events]]
date = "2026-01-06"
description = "A sale a cent over"
refused = "unbalanced"
lines = [
{ account = "1000", side = "debit", amount = "100.01" },
{ account = "4000", side = "credit", amount = "100.00" },
]
# By hand: only event 1 posted, so cash holds 1,000.00 (a debit), capital
# 1,000.00 (a credit, shown negative), and sales nothing.
[[expect]]
query = "trial_balance"
as_of = "2026-12-31"
[expect.values]
"1000" = "1000.00"
"3000" = "-1000.00"
"4000" = "0.00"
"6000" = "0.00"
@@ -0,0 +1,68 @@
id = "entry-cents-add-exactly"
title = "Amounts are kept exactly: ten cents and twenty cents make thirty"
rule = "The books MUST keep every amount exactly and add exactly, so that no sum of cents differs from its true total by any amount, however small; rounding MUST NOT occur inside the books."
status = "AUTHORITY"
sources = [
"IRS Form 1065 instructions: \"include cents when adding the amounts and round off only the total\" (docs/spec/sources/irs-partnerships.md)",
"Pacioli, Summa (1494), ch. 36: the credits equal the debits, which no float sum guarantees (docs/spec/sources/pacioli-1494.md)",
]
tags = ["accrual", "book", "us", "entry", "exact"]
[setup]
currency = "USD"
fiscal_year_start = "01-01"
basis = "accrual"
rounding = "half-even"
[[setup.accounts]]
code = "1000"
name = "Cash"
type = "asset"
[[setup.accounts]]
code = "2000"
name = "Loan"
type = "liability"
[[setup.accounts]]
code = "3000"
name = "Capital"
type = "equity"
[[setup.accounts]]
code = "4000"
name = "Sales"
type = "revenue"
tax_category = "gross_receipts"
[[setup.accounts]]
code = "6000"
name = "Rent"
type = "expense"
tax_category = "rent_property"
# In binary floating point 0.1 + 0.2 is 0.30000000000000004. Two debits of
# 0.10 and 0.20 against one credit of 0.30 balance exactly.
[[events]]
date = "2026-01-05"
description = "Three small sales"
lines = [
{ account = "1000", side = "debit", amount = "0.10" },
{ account = "1000", side = "debit", amount = "0.20" },
{ account = "4000", side = "credit", amount = "0.30" },
]
[[events]]
date = "2026-01-06"
description = "A sale of a trillion and a cent"
lines = [
{ account = "1000", side = "debit", amount = "1000000000000.01" },
{ account = "4000", side = "credit", amount = "1000000000000.01" },
]
[[expect]]
query = "balances"
as_of = "2026-12-31"
[expect.values]
"1000" = "1000000000000.31"
"4000" = "1000000000000.31"
@@ -0,0 +1,55 @@
events = []
id = "ledger-a-child-account-has-its-parents-type"
title = "An account under a parent has the parent's type"
rule = "An account MUST be one of the five elements (asset, liability, equity, revenue, expense), and an account under a parent MUST have its parent's type, so a rollup never mixes elements."
status = "AUTHORITY"
sources = [
"FASB Concepts Statement 8, Ch. 4, E16, E37, E61, E80, E81: the five elements (docs/spec/sources/fasb-asc.md)",
"CHOICE, the owner, 2026-10-13 (docs/spec/rules.md B-4): a child takes its parent's type",
]
tags = ["accrual", "book", "us", "ledger", "chart"]
[setup]
currency = "USD"
fiscal_year_start = "01-01"
basis = "accrual"
rounding = "half-even"
[[setup.accounts]]
code = "6000"
name = "Operating"
type = "expense"
header = true
[[setup.accounts]]
code = "6150"
name = "Software"
type = "expense"
parent = "6000"
tax_category = "office_expense"
[[setup.accounts]]
code = "1000"
name = "Cash"
type = "asset"
# A revenue account under an expense header would mix two elements.
[[expect]]
query = "add_account"
account = { code = "6200", name = "Refunds", type = "revenue", parent = "6000", tax_category = "gross_receipts" }
refused = "parent_type_mismatch"
# An expense under the expense header is a child of the same element.
[[expect]]
query = "add_account"
account = { code = "6200", name = "Hosting", type = "expense", parent = "6000", tax_category = "office_expense" }
accepted = true
[[expect]]
query = "trial_balance"
as_of = "2026-12-31"
[expect.values]
"6000" = "0.00"
"6150" = "0.00"
"1000" = "0.00"
@@ -0,0 +1,65 @@
id = "ledger-a-fraction-of-a-cent-is-refused"
title = "A line finer than the currency's smallest unit is refused"
rule = "A ledger kept in one currency MUST hold amounts in whole units of that currency's smallest unit, so a line finer than it MUST be refused, never rounded silently."
status = "CHOICE"
sources = [
"Pacioli, Summa (1494), ch. 36: \"the money that you reckon by at the beginning should be the same all through the Ledger\" (docs/spec/sources/pacioli-1494.md)",
"ISO 4217: the US dollar has two minor-unit digits (docs/spec/sources/iso-4217.md)",
"CHOICE, the owner, 2026-10-13 (docs/spec/rules.md B-3): books in the smallest unit, so cash can match a bank statement",
]
tags = ["accrual", "book", "us", "ledger", "currency"]
[setup]
currency = "USD"
fiscal_year_start = "01-01"
basis = "accrual"
rounding = "half-even"
[[setup.accounts]]
code = "1000"
name = "Cash"
type = "asset"
[[setup.accounts]]
code = "3000"
name = "Capital"
type = "equity"
[[setup.accounts]]
code = "4000"
name = "Sales"
type = "revenue"
tax_category = "gross_receipts"
[[setup.accounts]]
code = "6000"
name = "Rent"
type = "expense"
tax_category = "rent_property"
# 10.005 dollars is no amount anyone can pay.
[[events]]
date = "2026-01-05"
description = "Half a cent too fine"
refused = "finer_than_minor_unit"
lines = [
{ account = "1000", side = "debit", amount = "10.005" },
{ account = "4000", side = "credit", amount = "10.005" },
]
[[events]]
date = "2026-01-06"
description = "The same sale in cents"
lines = [
{ account = "1000", side = "debit", amount = "10.01" },
{ account = "4000", side = "credit", amount = "10.01" },
]
[[expect]]
query = "trial_balance"
as_of = "2026-12-31"
[expect.values]
"1000" = "10.01"
"3000" = "0.00"
"4000" = "-10.01"
"6000" = "0.00"
@@ -0,0 +1,101 @@
id = "ledger-a-mistake-is-reversed-never-erased"
title = "A reversal cancels an entry and leaves the original standing, and an entry is reversed only once"
rule = "A posted entry MUST NOT be changed or erased; a mistake MUST be corrected by an entry opposite to it, which MUST leave the original's lines as written, and an entry MUST be reversed at most once."
status = "AUTHORITY"
sources = [
"Pacioli, Summa (1494), ch. 31: \"make another entry opposite this one\"; ch. 36: \"You should not write backward, but always forward\" (docs/spec/sources/pacioli-1494.md)",
"Rev. Proc. 98-25 section 5.01: the audit trail (docs/spec/sources/irs-records.md)",
"CHOICE, the owner, 2026-10-13 (docs/spec/rules.md B-10): a correction corrects once",
]
tags = ["accrual", "book", "us", "ledger", "reversal"]
[setup]
currency = "USD"
fiscal_year_start = "01-01"
basis = "accrual"
rounding = "half-even"
[[setup.accounts]]
code = "1000"
name = "Cash"
type = "asset"
[[setup.accounts]]
code = "2000"
name = "Loan"
type = "liability"
[[setup.accounts]]
code = "3000"
name = "Capital"
type = "equity"
[[setup.accounts]]
code = "4000"
name = "Sales"
type = "revenue"
tax_category = "gross_receipts"
[[setup.accounts]]
code = "6000"
name = "Rent"
type = "expense"
tax_category = "rent_property"
[[events]]
date = "2026-01-10"
description = "Rent paid twice by mistake"
lines = [
{ account = "6000", side = "debit", amount = "800.00" },
{ account = "1000", side = "credit", amount = "800.00" },
]
[[events]]
date = "2026-01-12"
description = "Reverse entry 1"
reverses = 1
# A second reversal of the same entry would take the 800.00 out twice.
[[events]]
date = "2026-01-13"
description = "Reverse entry 1 again"
refused = "already_reversed"
reverses = 1
# The original still reads as it was written: posted is forever.
[[expect]]
query = "entry"
entry = 1
[expect.values]
"6000.debit" = "800.00"
"1000.credit" = "800.00"
# The reversal is the original with its sides flipped.
[[expect]]
query = "entry"
entry = 2
[expect.values]
"6000.credit" = "800.00"
"1000.debit" = "800.00"
# Net of the two, nothing: as if nothing had been written (ch. 31).
[[expect]]
query = "trial_balance"
as_of = "2026-12-31"
[expect.values]
"1000" = "0.00"
"2000" = "0.00"
"3000" = "0.00"
"4000" = "0.00"
"6000" = "0.00"
# Before the reversal's date the original stands alone.
[[expect]]
query = "trial_balance"
as_of = "2026-01-11"
[expect.values]
"1000" = "-800.00"
"2000" = "0.00"
"3000" = "0.00"
"4000" = "0.00"
"6000" = "800.00"
@@ -0,0 +1,108 @@
id = "period-a-close-sweeps-the-year-into-capital"
title = "A close zeroes revenue and expenses into equity, and the closed period takes no new postings"
rule = "A close MUST zero every revenue and expense account through its last day and credit equity with exactly the net income; a period once closed MUST take no new postings."
status = "AUTHORITY"
sources = [
"Pacioli, Summa (1494), ch. 34: \"you shall close this account into the capital account\" (docs/spec/sources/pacioli-1494.md)",
"OpenStax, Principles of Accounting, Vol. 1, 5.1: temporary accounts are closed at the end of each period (docs/spec/rules.md B-11)",
"CONVENTION (docs/spec/rules.md B-12): a closed period is locked, which follows from posted-is-forever",
]
tags = ["accrual", "book", "us", "period", "close"]
[setup]
currency = "USD"
fiscal_year_start = "01-01"
basis = "accrual"
rounding = "half-even"
[[setup.accounts]]
code = "1000"
name = "Cash"
type = "asset"
[[setup.accounts]]
code = "2000"
name = "Loan"
type = "liability"
[[setup.accounts]]
code = "3000"
name = "Capital"
type = "equity"
[[setup.accounts]]
code = "4000"
name = "Sales"
type = "revenue"
tax_category = "gross_receipts"
[[setup.accounts]]
code = "6000"
name = "Rent"
type = "expense"
tax_category = "rent_property"
[[events]]
date = "2026-01-02"
description = "Members put in cash"
lines = [
{ account = "1000", side = "debit", amount = "5000.00" },
{ account = "3000", side = "credit", amount = "5000.00" },
]
[[events]]
date = "2026-02-10"
description = "A sale"
lines = [
{ account = "1000", side = "debit", amount = "900.00" },
{ account = "4000", side = "credit", amount = "900.00" },
]
[[events]]
date = "2026-03-10"
description = "Rent"
lines = [
{ account = "6000", side = "debit", amount = "350.00" },
{ account = "1000", side = "credit", amount = "350.00" },
]
# Close January 1 through March 31 into capital.
[[events]]
date = "2026-03-31"
description = "Close the quarter"
close = { start = "2026-01-01", to = "retained_earnings" }
# March 31 is closed: an entry dated inside the quarter is refused.
[[events]]
date = "2026-03-15"
description = "A late sale"
refused = "period_closed"
lines = [
{ account = "1000", side = "debit", amount = "50.00" },
{ account = "4000", side = "credit", amount = "50.00" },
]
# By hand: net income = 900.00 - 350.00 = 550.00. Revenue and expense are
# zero; the 550.00 lands in Retained Earnings (3999, the account the books
# create for the close), so equity is 5000.00 + 550.00 = 5550.00; cash is
# 5000 + 900 - 350 = 5550.00, so assets equal equity.
[[expect]]
query = "balances"
as_of = "2026-12-31"
[expect.values]
"1000" = "5550.00"
"3000" = "5000.00"
"3999" = "550.00"
"4000" = "0.00"
"6000" = "0.00"
# The statement for the quarter excludes the close: the year's income is
# still reported.
[[expect]]
query = "profit_and_loss"
from = "2026-01-01"
to = "2026-03-31"
[expect.values]
"4000" = "900.00"
"6000" = "350.00"
net_income = "550.00"
@@ -0,0 +1,113 @@
id = "statement-the-books-balance-and-the-equation-holds"
title = "Normal balances, the trial balance and the accounting equation on one small book"
rule = "An account MUST show its balance on its normal side (assets and expenses debit, liabilities, equity and revenue credit); the trial balance's debits MUST equal its credits; and assets MUST equal liabilities plus equity plus revenue less expenses."
status = "AUTHORITY"
sources = [
"Pacioli, Summa (1494), ch. 36: the total of the credits equals the total of the debits (docs/spec/sources/pacioli-1494.md)",
"FASB Concepts Statement 8, Ch. 4, E62: equity \"is the difference between the entity's assets and its liabilities\" (docs/spec/sources/fasb-asc.md)",
"OpenStax, Principles of Accounting, Vol. 1, 3.1: normal balances (CONVENTION; docs/spec/rules.md B-5)",
]
tags = ["accrual", "book", "us", "statement", "equation"]
[setup]
currency = "USD"
fiscal_year_start = "01-01"
basis = "accrual"
rounding = "half-even"
[[setup.accounts]]
code = "1000"
name = "Cash"
type = "asset"
[[setup.accounts]]
code = "2000"
name = "Loan"
type = "liability"
[[setup.accounts]]
code = "3000"
name = "Capital"
type = "equity"
[[setup.accounts]]
code = "4000"
name = "Sales"
type = "revenue"
tax_category = "gross_receipts"
[[setup.accounts]]
code = "6000"
name = "Rent"
type = "expense"
tax_category = "rent_property"
[[events]]
date = "2026-01-02"
description = "Members put in cash"
lines = [
{ account = "1000", side = "debit", amount = "5000.00" },
{ account = "3000", side = "credit", amount = "5000.00" },
]
[[events]]
date = "2026-01-03"
description = "A loan"
lines = [
{ account = "1000", side = "debit", amount = "2000.00" },
{ account = "2000", side = "credit", amount = "2000.00" },
]
[[events]]
date = "2026-01-10"
description = "A sale"
lines = [
{ account = "1000", side = "debit", amount = "750.50" },
{ account = "4000", side = "credit", amount = "750.50" },
]
[[events]]
date = "2026-01-15"
description = "Rent"
lines = [
{ account = "6000", side = "debit", amount = "1200.25" },
{ account = "1000", side = "credit", amount = "1200.25" },
]
# By hand. Cash: 5000.00 + 2000.00 + 750.50 - 1200.25 = 6550.25.
# Debit side: cash 6550.25 + rent 1200.25 = 7750.50.
# Credit side: loan 2000.00 + capital 5000.00 + sales 750.50 = 7750.50.
[[expect]]
query = "trial_balance"
as_of = "2026-12-31"
[expect.values]
"1000" = "6550.25"
"2000" = "-2000.00"
"3000" = "-5000.00"
"4000" = "-750.50"
"6000" = "1200.25"
# Each on its normal side, all positive here; the equation:
# assets 6550.25 = liabilities 2000.00 + equity 5000.00
# + (revenue 750.50 - expenses 1200.25) = 6550.25.
[[expect]]
query = "balances"
as_of = "2026-12-31"
[expect.values]
"1000" = "6550.25"
"2000" = "2000.00"
"3000" = "5000.00"
"4000" = "750.50"
"6000" = "1200.25"
# The trial balance as of an earlier day balances too: on January 9 there
# is no sale and no rent.
[[expect]]
query = "trial_balance"
as_of = "2026-01-09"
[expect.values]
"1000" = "7000.00"
"2000" = "-2000.00"
"3000" = "-5000.00"
"4000" = "0.00"
"6000" = "0.00"