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

+26 -16
View File
@@ -156,6 +156,12 @@ pub fn rule_for_id(id: &RuleId) -> Option<&'static Rule> {
/// (bookspec/README.md).
pub fn rule_for_refusal(reason: &str) -> Option<&'static Rule> {
let b = match reason {
"unbalanced" | "too_few_lines" => 1,
"negative_amount" | "zero_amount" => 2,
"finer_than_minor_unit" => 3,
"parent_type_mismatch" => 4,
"already_reversed" => 10,
"period_closed" => 12,
"end_before_start" | "start_after_today" => 30,
"blank" | "unchanged" | "sibling_has_it" | "holds_a_tin" | "not_one_line"
| "code_taken" => 31,
@@ -211,84 +217,87 @@ const SPEC: Spec = &[
Area::Entry,
"Every entry is a double entry: debits equal credits",
Some(Status::Authority),
&[],
&[
"entry-an-unbalanced-entry-is-refused",
"statement-the-books-balance-and-the-equation-holds",
],
),
(
2,
Area::Entry,
"A line's amount is positive; its side gives the direction",
Some(Status::Convention),
&[],
&["entry-a-line-amount-is-positive"],
),
(
3,
Area::Ledger,
"One money through the ledger; foreign amounts at the transaction-date rate",
Some(Status::Authority),
&[],
&["ledger-a-fraction-of-a-cent-is-refused"],
),
(
4,
Area::Ledger,
"Five elements, closed list",
Some(Status::Authority),
&[],
&["ledger-a-child-account-has-its-parents-type"],
),
(
5,
Area::Ledger,
"Normal balances",
Some(Status::Convention),
&[],
&["statement-the-books-balance-and-the-equation-holds"],
),
(
6,
Area::Statement,
"The trial balance balances",
Some(Status::Authority),
&[],
&["statement-the-books-balance-and-the-equation-holds"],
),
(
7,
Area::Statement,
"The accounting equation",
Some(Status::Authority),
&[],
&["statement-the-books-balance-and-the-equation-holds"],
),
(
8,
Area::Ledger,
"Posted is forever: the book is written only forward",
Some(Status::Authority),
&[],
&["ledger-a-mistake-is-reversed-never-erased"],
),
(
9,
Area::Ledger,
"A mistake is corrected by an opposite entry, never erased",
Some(Status::Authority),
&[],
&["ledger-a-mistake-is-reversed-never-erased"],
),
(
10,
Area::Ledger,
"An entry is reversed at most once; a closing entry never",
Some(Status::Choice),
&[],
&["ledger-a-mistake-is-reversed-never-erased"],
),
(
11,
Area::Period,
"Revenue and expenses close into profit and loss, and that into capital",
Some(Status::Authority),
&[],
&["period-a-close-sweeps-the-year-into-capital"],
),
(
12,
Area::Period,
"A closed period takes no new postings; periods close in order",
Some(Status::Convention),
&[],
&["period-a-close-sweeps-the-year-into-capital"],
),
(
13,
@@ -316,6 +325,7 @@ const SPEC: Spec = &[
"cash-flow.reversal-of-a-reversal",
"cash-flow.reversed-property-contribution",
"cash-flow.two-member-llc-first-year",
"period-a-close-sweeps-the-year-into-capital",
],
),
(
@@ -392,7 +402,7 @@ const SPEC: Spec = &[
Area::Entry,
"Exact amounts in the books; rounding only on returns",
Some(Status::Authority),
&[],
&["entry-cents-add-exactly"],
),
(
22,
@@ -406,7 +416,7 @@ const SPEC: Spec = &[
Area::Ledger,
"Control roles: one account each, of the right kind, moved only when empty",
Some(Status::Convention),
&[],
&["arap-hand-control-posting-refused"],
),
(
24,
@@ -570,8 +580,8 @@ const SPEC: Spec = &[
(
44,
Area::Ledger,
"Extraction and dedup (allocated, not yet stated)",
None,
"Evidence is read from the bytes, never trusted over them; identical files are kept once",
Some(Status::Convention),
&[],
),
(
+3 -1
View File
@@ -1193,7 +1193,9 @@ impl Example {
if !matches!(line.side.as_str(), "debit" | "credit") {
return Err(what(format!("side {:?}", line.side)));
}
check_amount(&line.amount, true, &what)?;
// A refused entry may carry the amount the books refuse (zero or
// negative, B-2); an accepted one carries a positive amount.
check_amount(&line.amount, ev.refused.is_none(), &what)?;
if let Some(p) = &line.project
&& !projects.contains(p.as_str())
{
+261
View File
@@ -0,0 +1,261 @@
//! The specification text (`bookspec/spec/*.md`, P13 step 4): every rule
//! has its section, its statement, its pinned sources, its symbolic form
//! and its examples, and every citation resolves.
use std::collections::{BTreeMap, BTreeSet};
use std::path::{Path, PathBuf};
use bookspec_checker::{Rule, rules};
fn spec_dir() -> PathBuf {
Path::new(env!("CARGO_MANIFEST_DIR")).join("../spec")
}
fn sources_dir() -> PathBuf {
Path::new(env!("CARGO_MANIFEST_DIR")).join("../../docs/spec/sources")
}
/// A rule's section as the text gives it.
#[derive(Default, Debug)]
struct Section {
file: String,
heading: String,
fields: BTreeMap<String, Vec<String>>,
}
/// Every `### BS-...` section of the area files, by its identifier.
fn sections() -> BTreeMap<String, Section> {
let mut out = BTreeMap::new();
let mut files: Vec<PathBuf> = std::fs::read_dir(spec_dir())
.unwrap()
.map(|e| e.unwrap().path())
.filter(|p| p.extension().is_some_and(|x| x == "md"))
.collect();
files.sort();
for f in files {
let name = f.file_name().unwrap().to_string_lossy().into_owned();
let text = std::fs::read_to_string(&f).unwrap();
let mut current: Option<(String, Section)> = None;
let mut field = String::new();
for line in text.lines() {
if let Some(h) = line.strip_prefix("### BS-") {
if let Some((id, s)) = current.take() {
assert!(out.insert(id.clone(), s).is_none(), "{id} twice");
}
let id = format!("BS-{}", h.split(':').next().unwrap());
current = Some((
id,
Section {
file: name.clone(),
heading: h.to_string(),
..Section::default()
},
));
continue;
}
let Some((_, s)) = current.as_mut() else {
continue;
};
if let Some(rest) = line.strip_prefix("- **") {
let (key, value) = rest.split_once(":**").expect("a field: - **Name:** ...");
field = key.to_string();
let e = s.fields.entry(field.clone()).or_default();
if !value.trim().is_empty() {
e.push(value.trim().to_string());
}
} else if let Some(item) = line.strip_prefix(" - ") {
s.fields
.entry(field.clone())
.or_default()
.push(item.to_string());
}
}
if let Some((id, s)) = current.take() {
assert!(out.insert(id.clone(), s).is_none(), "{id} twice");
}
}
out
}
fn one<'a>(s: &'a Section, id: &str, field: &str) -> &'a str {
let v = s
.fields
.get(field)
.unwrap_or_else(|| panic!("{id}: no {field}"));
assert_eq!(v.len(), 1, "{id}: {field} is one line");
&v[0]
}
/// Whitespace and typographic quotes made plain, so a pin copied from a
/// wrapped or curly-quoted source still matches.
fn squash(s: &str) -> String {
let plain = s
.replace(['\u{2019}', '\u{2018}'], "'")
.replace(['\u{201c}', '\u{201d}'], "\"");
plain.split_whitespace().collect::<Vec<_>>().join(" ")
}
fn backticked(s: &str) -> Vec<&str> {
s.split('`').skip(1).step_by(2).collect()
}
/// Every one of the 45 rules has one section, in the file of its area,
/// headed with its identifier and B-number.
#[test]
fn every_rule_has_its_section() {
let sections = sections();
assert_eq!(sections.len(), 45, "one section per rule");
for rule in rules() {
let s = sections
.get(rule.id.as_str())
.unwrap_or_else(|| panic!("{}: no section", rule.id));
assert_eq!(s.file, format!("{}.md", rule.area.key()), "{}", rule.id);
assert!(
s.heading.ends_with(&format!("(B-{})", rule.b)),
"{}: heading {:?} names B-{}",
rule.id,
s.heading,
rule.b
);
}
}
/// A stated rule has a status the catalogue agrees with, a normative
/// statement, a symbolic form and applicability; a reserved one says so.
#[test]
fn every_stated_rule_is_complete() {
let sections = sections();
for rule in rules() {
let s = &sections[rule.id.as_str()];
let id = rule.id.as_str();
let status = one(s, id, "Status");
match rule.status {
Some(st) => {
assert!(status.starts_with(st.key()), "{id}: status {status:?}");
let statement = one(s, id, "Statement");
assert!(
["MUST", "SHOULD", "MAY"]
.iter()
.any(|w| statement.contains(w)),
"{id}: a statement says MUST, SHOULD or MAY"
);
for field in ["Symbolic form", "Applicability"] {
assert!(
one(s, id, field).len() > 10 && !one(s, id, field).starts_with("none"),
"{id}: {field}"
);
}
}
None => assert!(status.starts_with("reserved"), "{id}: reserved"),
}
}
}
/// A section's examples are the catalogue's, exactly; "none yet" says why.
#[test]
fn the_examples_are_the_catalogues() {
let sections = sections();
for rule in rules() {
let s = &sections[rule.id.as_str()];
let id = rule.id.as_str();
let line = one(s, id, "Examples");
let listed: BTreeSet<&str> = backticked(line)
.into_iter()
.filter(|b| *b != "none yet")
.collect();
let want: BTreeSet<&str> = rule.examples.iter().copied().collect();
if rule.examples.is_empty() {
assert!(line.starts_with("none yet"), "{id}: {line}");
if rule.status.is_some() {
assert!(
line.len() > "none yet. ".len() + 20,
"{id}: a stated rule with no example says why"
);
}
} else {
assert_eq!(listed, want, "{id}: examples");
}
}
}
/// Every citation resolves: a vendored source exists, and a verbatim
/// pin appears in it; an external reference names its paragraph.
#[test]
fn every_citation_resolves() {
let sections = sections();
let mut vendored = 0;
let mut external = 0;
let mut bad: Vec<String> = Vec::new();
for rule in rules() {
let s = &sections[rule.id.as_str()];
let id = rule.id.as_str();
let sources = s
.fields
.get("Sources")
.unwrap_or_else(|| panic!("{id}: no Sources"));
if rule.status.is_none() {
assert_eq!(sources, &vec!["none yet".to_string()], "{id}");
continue;
}
assert!(!sources.is_empty(), "{id}: sourced");
for item in sources {
if let Some(rest) = item.strip_prefix("vendored `") {
let (file, rest) = rest
.split_once("`:")
.unwrap_or_else(|| panic!("{id}: {item}"));
let path = sources_dir().join(file.strip_prefix("sources/").unwrap_or(file));
let text = std::fs::read_to_string(&path)
.unwrap_or_else(|_| panic!("{id}: {file} is not vendored"));
let hay = squash(&text);
// Every quoted pin is verbatim in the file.
let quoted: Vec<&str> = rest.split('"').skip(1).step_by(2).collect();
for q in &quoted {
if !hay.contains(&squash(q)) {
bad.push(format!("{id}: {q:?} is not in {file}"));
}
}
if quoted.is_empty() {
bad.push(format!("{id}: {item} has no verbatim pin"));
}
vendored += 1;
} else if let Some(rest) = item.strip_prefix("external:") {
assert!(
rest.trim().len() > 20,
"{id}: an external reference is exact: {item}"
);
external += 1;
} else {
panic!("{id}: a source is `vendored` or `external`: {item}");
}
}
}
assert!(bad.is_empty(), "pins not found:\n{}", bad.join("\n"));
assert!(vendored >= 30, "vendored citations: {vendored}");
assert!(external >= 15, "external citations: {external}");
}
/// The first edition's page budget: 60 pages of 55 lines.
#[test]
fn the_edition_fits_its_page_budget() {
let mut lines = 0;
for e in std::fs::read_dir(spec_dir()).unwrap() {
let p = e.unwrap().path();
if p.extension().is_some_and(|x| x == "md") {
lines += std::fs::read_to_string(p).unwrap().lines().count();
}
}
assert!(lines <= 60 * 55, "{lines} lines is over 60 pages");
}
/// A stated rule with no example is exactly one an engine cannot claim.
#[test]
fn rules_without_examples_are_not_claimable() {
let uncovered: Vec<&Rule> = rules().iter().filter(|r| !r.has_examples()).collect();
let claimable: BTreeSet<_> = bookspec_checker::covered_rules()
.into_iter()
.map(|r| r.b)
.collect();
for r in uncovered {
assert!(!claimable.contains(&r.b), "B-{}", r.b);
}
}
@@ -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"
+101
View File
@@ -0,0 +1,101 @@
# bookspec, first edition: definitions and the model
bookspec is a specification of double-entry bookkeeping that stands apart
from any program. Each rule is stated normatively, sourced to an authority
(or marked as a convention or a choice), written symbolically, and shown
by concrete examples (`bookspec/examples/*.toml`) that any engine can be
run against (`bookspec/runner`). wanderbooks is the first engine checked;
the GnuCash oracle is the second (`bookspec/gnucash`).
## Reading the rules
The words MUST, MUST NOT and MAY are RFC 2119's. A rule has
- an **identifier**, `BS-<area>-<n>`, stable and never reused; the table
from the wanderbooks catalogue's `B-1` to `B-45` is `rules-index.md`;
- a **status**: `AUTHORITY` (a source states it), `CONVENTION` (common
practice no authority states; the best reference is given), or `CHOICE`
(a person decided; the specification names the options and an engine
declares which it picks, `Claims::choices`);
- a **statement** (what MUST hold), its **sources** (each pinned exactly:
a vendored source with a verbatim quotation the build checks, or an
external reference to a paragraph, never a paraphrase of memory), its
**symbolic form**, its **examples** (ids of the TOML files) and its
**applicability** (which entities, bases and jurisdictions).
A rule with no example is stated and not claimable: an engine can only
claim a rule an example demonstrates (`covered_rules`). The first edition
says plainly which rules those are (`rules-index.md`, "—").
Sources are US GAAP (the FASB Codification: cited, never copied, and read
only in the Foundation's own view or a publisher that reproduces it) and
US federal tax (the Internal Revenue Code, Treasury Regulations, IRS
publications and form instructions, which are public domain), then older
or general authorities (Pacioli, OpenStax, ISO 4217). The large firms'
guides are never a source.
## The model
- **Currency and money.** A ledger has one **base currency**. An
**amount** is an exact decimal, a whole number of the currency's
smallest unit (ISO 4217 minor units). Arithmetic on amounts is exact;
it never uses binary floating point, and it rounds only where a rule
says so, and then by the rounding mode the rule names.
- **Account.** A code, a name and one of five **types**: asset,
liability, equity, revenue, expense. An account may have a parent of
its own type; an account may be a **header** (it groups, and takes no
posting). Assets and expenses have a **normal balance** on the debit
side; liabilities, equity and revenue on the credit side.
- **Line.** `(account, side, amount)`: `side` is debit or credit,
`amount > 0`. Its **signed value** is `+amount` for a debit and
`-amount` for a credit.
- **Entry.** A dated, described list of at least two lines. An entry is
**posted** or not; the ledger holds the posted ones. A **reversal** of
an entry is a new entry with the same accounts and amounts and the
sides flipped, linked to it.
- **Ledger.** The sequence of posted entries, written only forward. The
**balance** of an account through a day is the sum of its lines' signed
values dated on or before the day; shown on its normal side it is that
sum, negated for a credit-normal account.
- **Period and close.** A **period** is a range of days. **Closing** it
posts an entry that brings every revenue and expense account's balance
through the period's last day to zero against equity.
- **Document.** An invoice or a bill: a receivable or payable the ledger
carries as lines tagged with the document; control accounts equal the
documents behind them.
## The five laws
Every other rule serves these, and each is a theorem in `bookspec/lean`
(`Bookspec.law_*`), true of every state the ledger can reach by any
sequence of its commands:
1. **Debits equal credits.** `sum over all posted lines of signed value = 0`
(`law_debits_equal_credits`; BS-entry-1).
2. **The accounting equation.** `A = L + E + (R - X)`, with each term the
sum of the balances of that type's accounts on their normal side
(`law_accounting_equation`; BS-statement-2).
3. **The trial balance balances.** The debit balances sum to the credit
balances (`law_trial_balance`; BS-statement-1).
4. **Net income articulates to equity.** Closing a period credits equity
with exactly `R - X` through its last day
(`law_net_income_articulates`; BS-period-1).
5. **The cash flow articulates to cash.** The three sections of the
statement of cash flows sum to the change in the cash accounts'
balances (`law_cash_flow_is_change_in_cash`; BS-statement-3).
Money in the proofs is `Int`, a count of the smallest unit; every law is
a law of addition, and any finite set of exact decimals scales to
integers without loss.
## How an example is checked
An example is a TOML file: a setup, a sequence of events, and
expectations asked of the books, with every expected figure worked by
hand from the rule and its sources (`bookspec/README.md` has the shape).
Two checks stand behind each: **conformance** (the runner drives an
engine through it and compares the engine's answers with the expected
figures) and **derivation** (the checker computes the expected figures
from the rule's symbolic form, independent of any engine, and fails the
specification's own build when the example's figure disagrees; see
`bookspec/checker/src/derive.rs`).
+14
View File
@@ -0,0 +1,14 @@
# Area: 1099
Information returns for payments to contractors and landlords.
### BS-1099-1: 1099 reporting thresholds, payment methods, and decisions (B-19)
- **Status:** AUTHORITY, with the CHOICEs recorded in `docs/spec/rules.md` B-19
- **Statement:** A payee's payments made in a calendar year in the course of the payor's trade or business MUST be reportable when they reach the threshold (600 dollars through 2025; 2,000 dollars for payments made after December 31, 2025; indexed after 2026). Payments by payment card or third-party network MUST be excluded (they are reported on Form 1099-K by the settlement entity); payments through a financing account whose method is unrecorded MUST be undetermined, not counted; a payee's decision (reportable, not reportable with a reason, undetermined) applies by the payment's date; money between the company's own accounts pays no one.
- **Sources:**
- vendored `sources/irs-1099-thresholds.md`: "6041(a)"
- vendored `sources/irs-1099-payment-method.md`: "1099-K"
- **Symbolic form:** `reportable(p, y) iff sum{pay : payee = p, year = y, method not card/network} >= threshold(y)`; `threshold(y) = 600 if y <= 2025 else 2000`.
- **Examples:** `1099-a-dated-status-applies-by-payment-date`, `1099-a-members-check-through-financing-counts`, `1099-and-party-totals-for-a-members-payment`, `1099-card-payments-are-excluded`, `1099-money-between-own-accounts-pays-no-one`, `1099-money-from-a-payee-is-netted-only-as-a-refund`, `1099-the-threshold-is-the-years`, `1099-worksheet-through-as-of`
- **Applicability:** US payors; royalties (10 dollars) and attorney gross proceeds (600 dollars) are not modelled.
+64
View File
@@ -0,0 +1,64 @@
# Area: arap
Receivables and payables: documents, aging, terms, write-offs and
settlements.
### BS-arap-1: Aging buckets: Current, 1-30, 31-60, 61-90, 90+ days past due (B-18)
- **Status:** CONVENTION (bucket edges); CHOICE (a document due today is Current; aging as of a day shows only documents issued by then, less payments made by then; a payment never predates its document)
- **Statement:** An aging as of a day MUST place each open document in exactly one of the buckets Current (not yet past due, including due today), 1-30, 31-60, 61-90 and 90+ days past due, and the buckets MUST partition what the documents owe, exactly. A void document is in no bucket; a payment reversed by the day no longer counts.
- **Sources:**
- external: ASC 326-20-55-37 and 326-20-55-38, an illustrative aging of trade receivables
- **Symbolic form:** `d = as_of - due`; bucket `Current` if `d <= 0`, `1-30` if `1 <= d <= 30`, `31-60`, `61-90`, `90+` if `d >= 91`; `sum_b owed(b) = sum of open balances`.
- **Examples:** none yet. The schema has no aging query; the example for it waits on one (a document-level query is owed by this edition's successor).
- **Applicability:** accrual-basis entities with receivables or payables.
### BS-arap-2: A document's figures are its ledger lines that stand; a control account is its documents (B-38)
- **Status:** CHOICE (the owner, 2026-10-23); CONVENTION (a control account agrees with its subledger)
- **Statement:** Every ledger line on a receivables or payables control account MUST carry the document it belongs to; a document's total, paid and owed MUST be derived from its tagged lines that stand (a reversed line no longer counts); a control account's balance MUST equal the sum of its documents'. A hand line on a control account MUST be refused.
- **Sources:**
- external: ASC 310-10 (receivables); ASC 405-20-40-1 (a liability is removed only when paid or released); ASC 606-10-45-4 (a receivable is a right to consideration that is unconditional)
- **Symbolic form:** `bal(1200) = sum_{doc} owed(doc)` and `owed(doc) = sum{signed tagged lines that stand}`.
- **Examples:** `arap-hand-control-posting-refused`
- **Applicability:** engines with invoices and bills.
### BS-arap-3: Terms: when a document is due (B-39)
- **Status:** reserved
- **Statement:** Reserved: allocated in the wanderbooks plan (8.0.3) for payment terms and due dates, not yet stated in `docs/spec/rules.md`.
- **Sources:**
- none yet
- **Symbolic form:** none yet.
- **Examples:** none yet
- **Applicability:** none yet.
### BS-arap-4: Numbers and the issued copy (B-40)
- **Status:** reserved
- **Statement:** Reserved: allocated in the wanderbooks plan (8.0.3) for document numbers and the issued copy, not yet stated in `docs/spec/rules.md`.
- **Sources:**
- none yet
- **Symbolic form:** none yet.
- **Examples:** none yet
- **Applicability:** none yet.
### BS-arap-5: A debt that will not be collected is written off, and a recovery is income (B-41)
- **Status:** CHOICE (direct write-off, the owner, 2026-10-23); options: direct write-off, allowance method
- **Statement:** On the direct method, an invoice that will not be collected MUST be written off by debiting bad debt expense and crediting receivables, the receivable line tagged with the invoice; a partial write-off is the amount charged off; a write-off MUST be refused on cash-basis books and for more than is owed; a later recovery is income.
- **Sources:**
- external: 26 U.S.C. section 166(a); Treas. Reg. sections 1.166-1(e), 1.166-2(a), 1.166-3(a); 26 U.S.C. section 111 (recovery); ASC 326-20 (the allowance method, not applied)
- **Symbolic form:** `Dr 6990 amount; Cr 1200 amount` tagged with the invoice; `0 < amount <= owed`.
- **Examples:** `bad-debt-direct-write-off`
- **Applicability:** accrual-basis taxpayers.
### BS-arap-6: A settlement is exact, one party, and names its lines; the gross discount and the gross fee (B-42)
- **Status:** CONVENTION; CHOICE (the gross method for an early-payment discount, the owner A-8; the processor's fee posted gross, A-4)
- **Statement:** A settlement MUST apply to documents of one party and one kind, never the same document twice; sum of cash plus sum of discounts MUST equal what it applies to the documents, and the deposit MUST equal cash less fee, exactly; no application may exceed what its document owes on any day from the payment's date on; a discount MUST be at most the terms allow and be dated within the discount period, or carry a reason; a processor's fee posts gross (expense debited, deposit the net, receivables credited the gross).
- **Sources:**
- external: ASC 606-10-32-5 to 32-9 (the strict treatment of an expected discount, simplified here by the gross method)
- **Symbolic form:** `sum cash + sum discount = sum applied`; `deposit = sum cash - fee`; `owed_after >= 0` on every day.
- **Examples:** none yet. The schema has no payment or settlement event (`arap-settlement-exact` and `processor-fee-gross-presentation` are the names the plan allocates; they wait on that event).
- **Applicability:** engines with documents and settlements.
+45
View File
@@ -0,0 +1,45 @@
# Area: asset
Fixed assets, depreciation, the register and capitalization.
### BS-asset-1: Depreciation: straight line, salvage never depreciated, partial months by days (B-15)
- **Status:** CONVENTION (the book method); AUTHORITY for tax conventions, which an engine need not apply
- **Statement:** Depreciation MUST spread cost less salvage over the useful life in a systematic and rational manner: straight line, full months equal, the first and last partial months by their days, the last month taking what the others leave so the total is exactly cost less salvage. Salvage MUST NOT be depreciated. A run MUST post what the schedule says is due through the month and the books do not carry (a skipped month caught up); a disposal MUST post any depreciation still due through its day, then the disposal entry with its gain or loss; depreciation past a disposal MUST be refused.
- **Sources:**
- vendored `sources/fasb-asc.md`: "360-10-35-4"
- external: OpenStax, Principles of Accounting, Volume 1, section 11.3, straight-line depreciation
- **Symbolic form:** `dep_m = round((cost - salvage) * days_m / days_total)` for the partial months; `sum_m dep_m = cost - salvage`.
- **Examples:** `assets-acquired-before-in-service`, `assets-a-reversed-run-is-not-posted`, `assets-book-value-is-what-the-books-carry`, `depreciation-catch-up-month`, `depreciation-disposal-after-a-late-change-undone`, `depreciation-disposal-before-a-later-run`, `depreciation-disposal-in-final-month`, `depreciation-disposal-inside-a-posted-month`, `depreciation-disposal-mid-month`, `depreciation-final-month-by-days`, `depreciation-final-month-drift`, `depreciation-reversal-before-its-run-is-refused`, `depreciation-run-reversed-after-its-month`
- **Applicability:** entities with fixed assets; tax conventions (half-year, mid-month) are IRS Pub. 946's and are not applied.
### BS-asset-2: Expense or capitalize (book policy, mirrors tax) (B-22)
- **Status:** AUTHORITY
- **Statement:** An item costing no more than the de minimis safe harbor (500 dollars per invoice or item without an applicable financial statement; 2,500 dollars from taxable years beginning in 2016 under Notice 2015-82) MAY be expensed; a larger one SHOULD be capitalized and depreciated, and amounts paid for benefits lasting no more than twelve months MAY be expensed under the 12-month rule.
- **Sources:**
- vendored `sources/irs-capitalization.md`: "1.263(a)-1(f)"
- vendored `sources/irs-capitalization.md`: "1.263(a)-4(f)"
- **Symbolic form:** `cost <= safe_harbor implies expense MAY`; the election is made yearly on the return.
- **Examples:** none yet. This is a policy the bookkeeper applies; no engine enforces it, so no engine-neutral behaviour can be demonstrated.
- **Applicability:** US taxpayers; thresholds as of 2026.
### BS-asset-3: Asset tags: one label per asset, never reused (B-24)
- **Status:** CHOICE (the owner, 2026-10-15)
- **Statement:** An asset tag (1 to 32 ASCII letters, digits and `- _ . /`) MUST be unique ignoring case and MUST NOT be reused by another asset, even after a retag or a disposal. The fixed asset register MUST reconcile to the cost and accumulated depreciation accounts, showing unregistered cost, unposted depreciation and unattributed accumulated depreciation.
- **Sources:**
- external: CHOICE only: the owner, 2026-10-15 (docs/spec/rules.md B-24); no authority governs a label
- **Symbolic form:** `tag(a) = tag(b) implies a = b` (case folded), through the asset's whole life.
- **Examples:** none yet. The schema registers assets without tags; the register-row and register-totals queries demonstrate the reconciliation under BS-asset-1.
- **Applicability:** engines with an asset register; an engine declares its pick.
### BS-asset-4: Tax figures beside book value: the section 723 basis, the depreciable basis, the section 704(c) built-in gain (B-25)
- **Status:** AUTHORITY; CHOICE (the register carries tax basis; D-41)
- **Statement:** For property a member contributes, the partnership's basis MUST be the member's adjusted basis (carryover, section 723); book value is its fair market value; the difference at contribution is the section 704(c) built-in gain, fixed at contribution; the depreciable basis of property converted from personal use is the lesser of fair market value and adjusted basis.
- **Sources:**
- vendored `sources/irs-partnership-basis.md`: "723"
- **Symbolic form:** `basis_723 = adjusted_basis`; `built_in = fmv - basis_723` at contribution; `depreciable = min(fmv, adjusted_basis)` for personal-use conversion.
- **Examples:** `assets-723-basis-apart-from-depreciable-basis`, `assets-built-in-gain-fixed-at-contribution`, `assets-four-contributed-devices`
- **Applicability:** partnerships receiving contributed property.
+54
View File
@@ -0,0 +1,54 @@
# Area: entry
The mechanics of one entry: its lines, their sides and amounts.
### BS-entry-1: Every entry is a double entry: debits equal credits (B-1)
- **Status:** AUTHORITY
- **Statement:** An entry MUST have at least two lines, and the sum of its debit lines MUST equal the sum of its credit lines exactly, with no tolerance. An entry that does not MUST be refused whole: it posts nothing, and leaves no partial entry behind.
- **Sources:**
- vendored `sources/pacioli-1494.md`: "All entries made in the ledger have to be double entries"
- vendored `sources/irs-records.md`: "the total debits must equal the total credits"
- **Symbolic form:** for an entry with lines `(s_i, a_i)`, `a_i > 0`: `n >= 2` and `sum{a_i : s_i = debit} = sum{a_i : s_i = credit}`.
- **Examples:** `entry-an-unbalanced-entry-is-refused`, `statement-the-books-balance-and-the-equation-holds`
- **Applicability:** every entity, basis and jurisdiction.
### BS-entry-2: A line's amount is positive; its side gives the direction (B-2)
- **Status:** CONVENTION
- **Statement:** A line's amount MUST be greater than zero, and its side, never its sign, MUST give the direction money moves. A line with a negative or zero amount MUST be refused.
- **Sources:**
- vendored `sources/pacioli-1494.md`: "All the creditors must appear in the Ledger at the right hand side, and all the debtors at the left."
- **Symbolic form:** `a_i > 0`; `signed(i) = +a_i` if `s_i = debit`, `-a_i` if `s_i = credit`.
- **Examples:** `entry-a-line-amount-is-positive`
- **Applicability:** every engine. No authority forbids a negative amount outright; one way to write each line is chosen so every line reads one way.
### BS-entry-3: Exact amounts in the books; rounding only on returns (B-21)
- **Status:** AUTHORITY
- **Statement:** The books MUST keep every amount exactly, and a sum of amounts MUST be their exact sum. Rounding MUST NOT occur inside the books; where a rule needs a rounded figure (an allocation, a return line), it names the rounding mode and rounds only the total.
- **Sources:**
- vendored `sources/irs-partnerships.md`: "include cents when adding the amounts and round off only the total"
- **Symbolic form:** amounts are elements of `Z * u` (`u` the currency's smallest unit); `+` is exact on them.
- **Examples:** `entry-cents-add-exactly`
- **Applicability:** every engine. Binary floating point is not an implementation of this rule.
### BS-entry-4: What new commands take: four-digit years, amounts a line can carry (B-28)
- **Status:** CONVENTION
- **Statement:** A command MUST take only dates in 0001-01-01 through 9999-12-31 written `YYYY-MM-DD`, and amounts no larger than one quadrillion with at most ten decimal places; anything else MUST be refused, never wrapped or truncated. Replaying books already written MUST NOT apply these bounds.
- **Sources:**
- external: ISO 8601-1:2019 section 5.2.2.1, calendar dates with four-digit years in the basic profile
- **Symbolic form:** `date in [0001-01-01, 9999-12-31]`; `|amount| <= 10^15`; `scale(amount) <= 10`.
- **Examples:** none yet. The example schema writes dates as `YYYY-MM-DD` already (a malformed date fails the schema), so a refusal example needs a command whose input can be out of range; the first edition has none.
- **Applicability:** commands only; replay of existing books is exempt (posted is forever).
### BS-entry-5: Drafts and evidence (B-43)
- **Status:** reserved
- **Statement:** Reserved: allocated in the wanderbooks plan (8.0.3) for the draft states of unit P4, not yet stated in `docs/spec/rules.md`. Its identifier and area are fixed so a later edition fills it in without moving another number.
- **Sources:**
- none yet
- **Symbolic form:** none yet.
- **Examples:** none yet
- **Applicability:** none yet.
+23
View File
@@ -0,0 +1,23 @@
# Area: format
Interchange formats and the journal's format gate.
### BS-format-1: The journal format gate: newer is not corrupt (B-27)
- **Status:** CHOICE (the owner, 2026-10-16; D-39, 2026-10-20)
- **Statement:** A reader MUST distinguish a journal line it does not know because it is newer from one that is corrupt: a line's own hash is checked before it is called newer; a reader stops at the first line it cannot read and says the journal is newer; older books move to a newer format only with the owner's go-ahead.
- **Sources:**
- external: CHOICE only: the owner, 2026-10-16 (docs/spec/rules.md B-27); no authority governs a file format's evolution
- **Symbolic form:** `hash_ok(line) and type(line) unknown implies newer`; `not hash_ok(line) implies corrupt`.
- **Examples:** none yet. The example schema has no journal-file event; the gate is checked by wanderbooks' store tests.
- **Applicability:** engines that persist a journal.
### BS-format-2: The QuickBooks Desktop file (IIF): plain ASCII, every entry through as_of, closes left out, classes carried (B-34)
- **Status:** AUTHORITY
- **Statement:** An IIF file MUST be plain ASCII, tab-separated, with each line ending CR LF; lists (accounts, classes) MUST come before the transactions that use them; an account is identified by its name, a subaccount's name its path joined by colons; every entry through the as-of day MUST be in the file and closing entries left out; classes MUST be carried; text the file must write differently from the books (a name past 31 characters, a memo past 4,095) MUST be reported as a change.
- **Sources:**
- vendored `sources/intuit-iif.md`: "Accounts are identified by NAME"
- **Symbolic form:** `file = header lists transactions`, `forall line: ascii(line) and tabs(line)`.
- **Examples:** `iif-quickbooks-journal-file`
- **Applicability:** exports to QuickBooks Desktop.
+141
View File
@@ -0,0 +1,141 @@
# Area: ledger
The chart, the journal, posted-is-forever, corrections, control roles and
how the books are kept.
### BS-ledger-1: One money through the ledger; foreign amounts at the transaction-date rate (B-3)
- **Status:** AUTHORITY (the single money, the rate); CHOICE (booking in the smallest unit, the owner, 2026-10-13)
- **Statement:** A ledger MUST be kept in one base currency. A foreign amount MUST be booked at the rate in effect on the transaction date, rounded half away from zero to the base currency's smallest unit, and the original amount and rate kept. A line finer than the smallest unit MUST be refused, never rounded silently.
- **Sources:**
- vendored `sources/pacioli-1494.md`: "the money that you reckon by at the beginning should be the same all through the Ledger"
- vendored `sources/iso-4217.md`: "Minor units" (the table of currencies by minor-unit digits; two for the US dollar)
- external: ASC 830-20-30-1, initial measurement in the functional currency at the rate in effect at the transaction date
- **Symbolic form:** `base = round_half_away(foreign * rate, u)`; every booked amount `in Z * u`.
- **Examples:** `ledger-a-fraction-of-a-cent-is-refused`
- **Applicability:** every engine. The single-currency refusal is demonstrated; the foreign-currency booking has no example in the first edition (the schema has no foreign line).
### BS-ledger-2: Five elements, closed list (B-4)
- **Status:** AUTHORITY (the definitions); CONVENTION (the five-type chart); CHOICE (a child has its parent's type, the owner, 2026-10-13)
- **Statement:** Every account MUST be exactly one of asset, liability, equity, revenue, expense. An account under a parent MUST have its parent's type.
- **Sources:**
- external: FASB Concepts Statement No. 8, Chapter 4, paragraphs E16 (asset), E37 (liability), E61 (equity), E80 (revenue), E81 (expense)
- **Symbolic form:** `type(a) in {asset, liability, equity, revenue, expense}`; `parent(a) = p implies type(a) = type(p)`.
- **Examples:** `ledger-a-child-account-has-its-parents-type`
- **Applicability:** every engine; engines with a finer chart (contra accounts, memo accounts) map onto the five.
### BS-ledger-3: Normal balances (B-5)
- **Status:** CONVENTION
- **Statement:** An asset or expense account's balance MUST be shown as debits less credits, a liability, equity or revenue account's as credits less debits.
- **Sources:**
- vendored `sources/pacioli-1494.md`: "This capital must always be placed as creditor"
- external: OpenStax, Principles of Accounting, Volume 1, section 3.1
- **Symbolic form:** `bal(a) = D(a) - C(a)` if `type(a) in {asset, expense}`, else `C(a) - D(a)`.
- **Examples:** `statement-the-books-balance-and-the-equation-holds`
- **Applicability:** every engine.
### BS-ledger-4: Posted is forever: the book is written only forward (B-8)
- **Status:** AUTHORITY
- **Statement:** A posted entry MUST NOT be changed or deleted. The ledger at a later time MUST contain every earlier posted entry unchanged, and a change to a record MUST leave the audit trail intact.
- **Sources:**
- vendored `sources/pacioli-1494.md`: "You should not write backward, but always forward"
- vendored `sources/irs-records.md`: "A taxpayer establishes this reconciliation by demonstrating the relationship"
- external: Handelsgesetzbuch (HGB) section 239(3), official English translation: an entry may not be modified such that its original content can no longer be established
- **Symbolic form:** for ledger states `S_t` and `t <= t'`: `posted(S_t)` is a prefix of `posted(S_t')`.
- **Examples:** `ledger-a-mistake-is-reversed-never-erased`
- **Applicability:** every engine.
### BS-ledger-5: A mistake is corrected by an opposite entry, never erased (B-9)
- **Status:** AUTHORITY
- **Statement:** A mistaken entry MUST be corrected by a reversal: a new entry with the same accounts and amounts and the sides flipped, linked to the original. The original MUST remain as it was written, and the pair MUST net to zero on every account.
- **Sources:**
- vendored `sources/pacioli-1494.md`: "entry opposite this one in the credit for the same amount"
- **Symbolic form:** `reverse(E) = {(flip(s_i), a_i)}`; per account `signed(E) + signed(reverse(E)) = 0`.
- **Examples:** `ledger-a-mistake-is-reversed-never-erased`
- **Applicability:** every engine.
### BS-ledger-6: An entry is reversed at most once; a closing entry never (B-10)
- **Status:** CHOICE (the owner, 2026-10-13); options: reversible once, reversible repeatedly; a close never reversible
- **Statement:** An entry MUST be reversed at most once; a second reversal of it MUST be refused. A closing entry MUST NOT be reversed, because a closed period stays closed. To restore a reversed entry, post a new entry.
- **Sources:**
- vendored `sources/pacioli-1494.md`: "entry opposite this one in the credit for the same amount"
- **Symbolic form:** `|{R : reversal_of(R) = E}| <= 1`; `closing(E) implies no R with reversal_of(R) = E`.
- **Examples:** `ledger-a-mistake-is-reversed-never-erased`
- **Applicability:** an engine declares its pick (`claims.choices`).
### BS-ledger-7: Every transaction has supporting documents; records are kept (B-20)
- **Status:** AUTHORITY
- **Statement:** Every entry MUST have supporting documents, kept for the period of limitations of the return it supports (three years generally; seven for a bad debt). Evidence the books file with an entry MUST stay with it; a reversal needs none of its own.
- **Sources:**
- vendored `sources/irs-records.md`: "In addition, you must keep supporting documents"
- **Symbolic form:** `forall posted E, not reversal(E): |documents(E)| >= 1`.
- **Examples:** none yet. The example schema has no evidence event, and wanderbooks enforces this at its tool layer, not in the ledger core; a first-edition example would assert a policy of the tool surface, which is not engine-neutral.
- **Applicability:** a US taxpayer keeping records for a return.
### BS-ledger-8: Control roles: one account each, of the right kind, moved only when empty (B-23)
- **Status:** CONVENTION (a control account agrees with the records it controls); CHOICE (default codes, the owner, 2026-10-13 and 2026-10-15)
- **Statement:** An engine that posts by role (receivables, payables, revenue, and the like) MUST give each role exactly one account, postable and of the role's type, and MUST refuse to move a role to another account while the old one holds a balance. A control account's lines MUST each belong to the document that put them there; a hand line on one MUST be refused.
- **Sources:**
- external: ASC 310-10 (receivables) and ASC 405-20-40-1 (a liability is removed only when paid or released)
- **Symbolic form:** `role` is an injective partial map from roles to accounts; `bal(control(r)) = sum of open documents of r`.
- **Examples:** `arap-hand-control-posting-refused`
- **Applicability:** engines with documents (invoices, bills).
### BS-ledger-9: The books keep how they are kept (B-29)
- **Status:** CHOICE (the owner, 2026-10-16)
- **Statement:** The settings, conventions, decisions and open items that govern the books MUST be facts in the books, each with who decided it and when, so that replaying the books gives the same results without any state held elsewhere.
- **Sources:**
- external: IRS Publication 538, Accounting Periods and Methods, "Accounting Method": the method is a matter of the books
- **Symbolic form:** `state = fold(apply, empty, journal)`.
- **Examples:** none yet. The schema's setup is the example's own, not a journal; replay equivalence is checked by the wanderbooks adapter (`Store::verify`) after every example, not stated as an engine-neutral expectation.
- **Applicability:** engines that keep a journal.
### BS-ledger-10: Renaming an account changes its label, nothing else (B-31)
- **Status:** CHOICE (the owner, 2026-10-20)
- **Statement:** An account MUST be renamed without changing any entry, line, balance or statement amount; every report, past periods included, prints the new name. A blank name, an unchanged name, a sibling's name (ignoring capitals and surrounding space), a name holding a nine-digit number and a name that is not one line MUST be refused.
- **Sources:**
- vendored `sources/irs-records.md`: "A taxpayer establishes this reconciliation by demonstrating the relationship"
- vendored `sources/intuit-iif.md`: "Accounts are identified by NAME"
- **Symbolic form:** `rename(a, n)` leaves `lines`, `bal` unchanged; `name(a) = n`.
- **Examples:** `rename-changes-no-amount`
- **Applicability:** every engine.
### BS-ledger-11: Every revenue and expense account maps to a line of the return (B-32)
- **Status:** AUTHORITY (why it matters); CHOICE (warn versus refuse, the owner, 2026-10-20); options: warn, refuse
- **Statement:** A revenue or expense account (not a header) SHOULD carry a tax category linking it to a line of the return; under the refuse policy an engine MUST refuse to create one without a category or to clear one. A header needs none.
- **Sources:**
- vendored `sources/irs-records.md`: "Your books must show your gross income, as well as your deductions and credits."
- **Symbolic form:** `policy = refuse implies forall a in nominal, not header(a): category(a) defined`.
- **Examples:** `tax-category-required`
- **Applicability:** a US business filing a return.
### BS-ledger-12: Evidence: extraction, dedup, annotations, settlements (B-44)
- **Status:** CONVENTION
- **Statement:** Text and facts read off a receipt MUST be derived records the books can re-make, the file's bytes staying the authority; identical bytes MUST be one file wherever they are evidence; an annotation MUST change no amount; a payment's settlements MUST be named, never guessed.
- **Sources:**
- external: Revenue Procedure 98-25, section 5.01, machine-sensible records with an audit trail
- vendored `sources/irs-records.md`: "In addition, you must keep supporting documents"
- **Symbolic form:** `text(file) = f(bytes(file), extractor, version)`; `bytes(a) = bytes(b) implies file(a) = file(b)`.
- **Examples:** none yet. The example schema has no attachment or file event; an evidence example waits on a schema extension (the unit's step 5 names it as owed).
- **Applicability:** engines that hold receipts.
### BS-ledger-13: Audits and attestations (B-45)
- **Status:** reserved
- **Statement:** Reserved: allocated in the wanderbooks plan (8.0.3), not yet stated in `docs/spec/rules.md`.
- **Sources:**
- none yet
- **Symbolic form:** none yet.
- **Examples:** none yet
- **Applicability:** none yet.
+48
View File
@@ -0,0 +1,48 @@
# Area: partner
Members of a partnership or LLC taxed as one, and the parties the books
deal with.
### BS-partner-1: Partners share income by the partnership agreement; capital accounts carry it (B-13)
- **Status:** AUTHORITY (shares, capital accounts); CHOICE (cent rounding, 2026-10-13; counting from the first close, 2026-10-16; the statement splits unclosed earnings the same running way, 2026-10-20)
- **Statement:** A partner's distributive share of income MUST be determined by the partnership agreement. Each member's capital account MUST be one running balance of contributions, allocations and distributions; a close MUST allocate the period's net income among members by their shares, the shares summing to one. An allocation MUST sum to exactly the net, each member's share rounded to the currency's smallest unit by the largest-remainder rule and a running split carrying what earlier closes gave.
- **Sources:**
- vendored `sources/irs-partnerships.md`: "A partner's distributive share of income, gain, loss, deduction, or credit shall, except as otherwise provided in this chapter, be determined by the partnership agreement."
- vendored `sources/irs-capital-accounts.md`: "capital account"
- **Symbolic form:** `sum_k share_k = 1`; `part_k(t) = round_LR(share_k * N(t)) - sum_{s<t} part_k(s)` with `sum_k part_k(t) = N(t) - N(t-1)`.
- **Examples:** `members-close.odd-cent-across-closes`, `members-close.statement-before-and-after-a-close`, `members-close.thirds-over-twelve-months`, `members-close.true-up-of-a-per-close-split`, `p3-party-members-equity-and-k1`
- **Applicability:** entities taxed as partnerships; a count ends where the members or shares change.
### BS-partner-2: Parties: one directory, no full TIN in the books (B-26)
- **Status:** AUTHORITY (truncated TINs); CHOICE (the owner, 2026-10-15)
- **Statement:** Everyone the company deals with MUST be a party with roles. Names MUST be unique among live parties, ignoring capitals and surrounding space. A party record MUST hold at most the last four digits of a TIN; the books MUST NOT hold a nine-digit number in any party field, and a full TIN lives only in a vault outside the ledger.
- **Sources:**
- vendored `sources/irs-truncated-tin.md`: "truncated"
- vendored `sources/irs-tin-formats.md`: "taxpayer identification number"
- **Symbolic form:** `tin_last4(p) in [0-9]{4}`; no field of a party matches `[0-9]{9}`.
- **Examples:** `p3-party-tin-last4`
- **Applicability:** US persons with TINs.
### BS-partner-3: A party's part of an entry, and a party's totals (B-35)
- **Status:** CONVENTION (the sign); CHOICE (what counts as money, the owner, 2026-10-15 and 2026-10-20); AUTHORITY (what a member's capital counts; a promise is not payment)
- **Statement:** An entry's part for a party MUST be signed as a line on the party's own account would be (paid or given positive, received negative); money paid down on a liability counts once; an accrual counts once; a liability credit MUST NOT count as paying that liability's own creditor; a promise MUST NOT count as payment.
- **Sources:**
- vendored `sources/helvering-v-price-1940.md`: "Helvering v. Price"
- vendored `sources/irs-capital-accounts.md`: "capital account"
- **Symbolic form:** `part(E, p) = sum{signed lines of E on the accounts that are p's money}`; totals are sums over entries on each entry's first original's side.
- **Examples:** `p3-party-accrual-counts-once`, `p3-party-distribution-still-owed`, `p3-party-gift-card-is-a-promise`, `p3-party-members-equity-and-k1`, `p3-party-own-liability-never-pays`, `p3-party-reversed-card-payment`, `p3-party-signs-and-reversals`
- **Applicability:** every engine with parties.
### BS-partner-4: The K-1 worksheet reports tax-basis capital beside book capital (B-36)
- **Status:** AUTHORITY; CHOICE (D-41, the owner, 2026-10-20)
- **Statement:** The K-1 worksheet MUST report each member's capital on the tax basis (beginning, contributions at adjusted tax basis, share of income for tax purposes, other changes, distributions, ending) beside the book figures; a line the books cannot know MUST be shown open, never guessed.
- **Sources:**
- vendored `sources/irs-partnerships.md`: "Tax-basis method"
- vendored `sources/irs-partnership-basis.md`: "723"
- **Symbolic form:** `ending = beginning + contributions + income + other - distributions`, per member, on each basis.
- **Examples:** `partnership-k1-tax-basis-capital`, `partnership-k1-tax-basis-odd-cents`
- **Applicability:** partnerships filing Form 1065.
+34
View File
@@ -0,0 +1,34 @@
# Area: period
Periods and closes, and what an omitted date means.
### BS-period-1: Revenue and expenses close into profit and loss, and that into capital (B-11)
- **Status:** AUTHORITY (the mechanics); the account the net lands in depends on the entity
- **Statement:** Closing a period MUST post an entry that brings every revenue and expense account's balance through the period's last day to zero and credits equity with exactly revenue less expenses; the entry MUST balance. A corporation closes into retained earnings, a partnership into each partner's capital (BS-partner-1).
- **Sources:**
- vendored `sources/pacioli-1494.md`: "must then be transferred for its closing (saldo) into the capital account"
- external: OpenStax, Principles of Accounting, Volume 1, section 5.1: temporary accounts are closed at the end of each accounting period
- **Symbolic form:** for nominal `n`: `bal_n(close) = 0`; `E' = E + (R - X)` (`Bookspec.law_net_income_articulates`).
- **Examples:** `period-a-close-sweeps-the-year-into-capital`
- **Applicability:** every entity; the target account differs by entity type.
### BS-period-2: A closed period takes no new postings; periods close in order (B-12)
- **Status:** CONVENTION
- **Statement:** An entry dated within a closed period MUST be refused. Periods MUST close in order, and MUST nest (a month in a quarter in a year) and never partially overlap.
- **Sources:**
- vendored `sources/irs-periods-and-methods.md`: "A tax year is an annual accounting period for keeping records and reporting income and expenses"
- **Symbolic form:** `date(E) in closed(P) implies post(E)` is refused; `P, Q` periods: nested or disjoint.
- **Examples:** `period-a-close-sweeps-the-year-into-capital`
- **Applicability:** every engine that closes.
### BS-period-3: A report asked for without dates covers this fiscal year so far (B-30)
- **Status:** AUTHORITY (the period); CHOICE (the default, the owner, 2026-10-20)
- **Statement:** A report asked for with no dates MUST cover the fiscal year containing today, from its first day through today; an omitted start is the first day of the fiscal year holding the end; a start after today with no end MUST be refused; an end before the start MUST be refused.
- **Sources:**
- vendored `sources/irs-periods-and-methods.md`: "A tax year is an annual accounting period for keeping records and reporting income and expenses"
- **Symbolic form:** `to = today`; `from = fiscal_year_start(to)`; `from <= to`.
- **Examples:** `default-period-fiscal-year-to-date`
- **Applicability:** every engine with a fiscal-year setting.
+14
View File
@@ -0,0 +1,14 @@
# Area: prepaid
Prepaid expenses and deferred revenue.
### BS-prepaid-1: Prepaid expenses and deferred revenue are recognized over the period benefited or earned (B-16)
- **Status:** AUTHORITY (revenue); CONVENTION (prepaids)
- **Statement:** A payment received before a good or service is transferred MUST be a contract liability, recognized as revenue as the performance obligation is satisfied; a prepaid expense MUST be an asset recognized as the benefit is used. A schedule MUST recognize exactly its total over its months: by the day (first and last partial months by their days, the last taking the remainder) or in equal months from the first.
- **Sources:**
- external: ASC 606-10-45-2 (contract liability); ASC 606-10-25-23 (recognize revenue when a performance obligation is satisfied); ASC 340-10-05-4 and ASC 210-10-45-1(g)
- vendored `sources/irs-capitalization.md`: "1.263(a)-4(f)"
- **Symbolic form:** `sum_m recognized_m = total`; `recognized_m >= 0`.
- **Examples:** `schedule-daily-deferred-revenue`, `schedule-daily-small-first-and-last-month`, `schedule-equal-months-start-on-the-first`
- **Applicability:** accrual-basis entities.
+13
View File
@@ -0,0 +1,13 @@
# Area: revenue
How revenue is presented.
### BS-revenue-1: Platform payouts are recorded gross when the business is the principal (B-17)
- **Status:** AUTHORITY
- **Statement:** When the business controls the good or service before it is transferred to the customer (is the principal), a payout from a platform MUST be recorded as revenue in the gross amount of consideration, the platform's fee as an expense, and the deposit as gross less fee. An agent records only its fee or commission.
- **Sources:**
- external: ASC 606-10-55-37, 55-37A and 55-37B (principal: gross amount of consideration); ASC 606-10-55-38 (agent: the fee or commission); ASC 606-10-55-39 (indicators)
- **Symbolic form:** `revenue = gross`; `expense = fee`; `deposit = gross - fee`.
- **Examples:** none yet. The schema has no payout event; the example for a settlement through a processor (BS-arap-6) shows the same gross presentation for a customer payment.
- **Applicability:** principal versus agent is a judgment under 55-39; the CPA confirms.
+18 -18
View File
@@ -6,36 +6,36 @@ what its statement rests on and the examples that demonstrate it.
A rule whose only source is a human choice carries rules.md's **CHOICE**, bookspec's status of the same name:
the specification names the options, an engine declares its pick
(`claims.choices`, `bookspec_checker::Claims`). Rules B-39, B-40 and B-43
to B-45 are allocated in `plan/served.md` 8.0.3 and not yet stated: their
(`claims.choices`, `bookspec_checker::Claims`). Rules B-39, B-40, B-43
and B-45 are allocated in `plan/served.md` 8.0.3 and not yet stated: their
identifier and area are fixed here so a later unit fills them in
without moving anyone else's number.
| B | bookspec id | area | status | rule | examples |
|---|---|---|---|---|---|
| B-1 | `BS-entry-1` | entry | AUTHORITY | Every entry is a double entry: debits equal credits | — |
| B-2 | `BS-entry-2` | entry | CONVENTION | A line's amount is positive; its side gives the direction | — |
| B-3 | `BS-ledger-1` | ledger | AUTHORITY | One money through the ledger; foreign amounts at the transaction-date rate | — |
| B-4 | `BS-ledger-2` | ledger | AUTHORITY | Five elements, closed list | — |
| B-5 | `BS-ledger-3` | ledger | CONVENTION | Normal balances | — |
| B-6 | `BS-statement-1` | statement | AUTHORITY | The trial balance balances | — |
| B-7 | `BS-statement-2` | statement | AUTHORITY | The accounting equation | — |
| B-8 | `BS-ledger-4` | ledger | AUTHORITY | Posted is forever: the book is written only forward | — |
| B-9 | `BS-ledger-5` | ledger | AUTHORITY | A mistake is corrected by an opposite entry, never erased | — |
| B-10 | `BS-ledger-6` | ledger | CHOICE | An entry is reversed at most once; a closing entry never | — |
| B-11 | `BS-period-1` | period | AUTHORITY | Revenue and expenses close into profit and loss, and that into capital | — |
| B-12 | `BS-period-2` | period | CONVENTION | A closed period takes no new postings; periods close in order | — |
| B-1 | `BS-entry-1` | entry | AUTHORITY | Every entry is a double entry: debits equal credits | `entry-an-unbalanced-entry-is-refused`, `statement-the-books-balance-and-the-equation-holds` |
| B-2 | `BS-entry-2` | entry | CONVENTION | A line's amount is positive; its side gives the direction | `entry-a-line-amount-is-positive` |
| B-3 | `BS-ledger-1` | ledger | AUTHORITY | One money through the ledger; foreign amounts at the transaction-date rate | `ledger-a-fraction-of-a-cent-is-refused` |
| B-4 | `BS-ledger-2` | ledger | AUTHORITY | Five elements, closed list | `ledger-a-child-account-has-its-parents-type` |
| B-5 | `BS-ledger-3` | ledger | CONVENTION | Normal balances | `statement-the-books-balance-and-the-equation-holds` |
| B-6 | `BS-statement-1` | statement | AUTHORITY | The trial balance balances | `statement-the-books-balance-and-the-equation-holds` |
| B-7 | `BS-statement-2` | statement | AUTHORITY | The accounting equation | `statement-the-books-balance-and-the-equation-holds` |
| B-8 | `BS-ledger-4` | ledger | AUTHORITY | Posted is forever: the book is written only forward | `ledger-a-mistake-is-reversed-never-erased` |
| B-9 | `BS-ledger-5` | ledger | AUTHORITY | A mistake is corrected by an opposite entry, never erased | `ledger-a-mistake-is-reversed-never-erased` |
| B-10 | `BS-ledger-6` | ledger | CHOICE | An entry is reversed at most once; a closing entry never | `ledger-a-mistake-is-reversed-never-erased` |
| B-11 | `BS-period-1` | period | AUTHORITY | Revenue and expenses close into profit and loss, and that into capital | `period-a-close-sweeps-the-year-into-capital` |
| B-12 | `BS-period-2` | period | CONVENTION | A closed period takes no new postings; periods close in order | `period-a-close-sweeps-the-year-into-capital` |
| B-13 | `BS-partner-1` | partner | AUTHORITY | Partners share income by the partnership agreement; capital accounts carry it | `members-close.odd-cent-across-closes`, `members-close.statement-before-and-after-a-close`, `members-close.thirds-over-twelve-months`, `members-close.true-up-of-a-per-close-split`, `p3-party-members-equity-and-k1` |
| B-14 | `BS-statement-3` | statement | AUTHORITY | Statements: P&L for a period; cash flows by the direct method | `cash-flow.debt-to-equity`, `cash-flow.government-fees`, `cash-flow.paid-on-the-companys-behalf`, `cash-flow.part-cash-purchase`, `cash-flow.reversal-of-a-reversal`, `cash-flow.reversed-property-contribution`, `cash-flow.two-member-llc-first-year` |
| B-14 | `BS-statement-3` | statement | AUTHORITY | Statements: P&L for a period; cash flows by the direct method | `cash-flow.debt-to-equity`, `cash-flow.government-fees`, `cash-flow.paid-on-the-companys-behalf`, `cash-flow.part-cash-purchase`, `cash-flow.reversal-of-a-reversal`, `cash-flow.reversed-property-contribution`, `cash-flow.two-member-llc-first-year`, `period-a-close-sweeps-the-year-into-capital` |
| B-15 | `BS-asset-1` | asset | CONVENTION | Depreciation: straight line, salvage never depreciated, partial months by days | `assets-acquired-before-in-service`, `assets-a-reversed-run-is-not-posted`, `assets-book-value-is-what-the-books-carry`, `depreciation-catch-up-month`, `depreciation-disposal-after-a-late-change-undone`, `depreciation-disposal-before-a-later-run`, `depreciation-disposal-in-final-month`, `depreciation-disposal-inside-a-posted-month`, `depreciation-disposal-mid-month`, `depreciation-final-month-by-days`, `depreciation-final-month-drift`, `depreciation-reversal-before-its-run-is-refused`, `depreciation-run-reversed-after-its-month` |
| B-16 | `BS-prepaid-1` | prepaid | AUTHORITY | Prepaid expenses and deferred revenue are recognized over the period benefited or earned | `schedule-daily-deferred-revenue`, `schedule-daily-small-first-and-last-month`, `schedule-equal-months-start-on-the-first` |
| B-17 | `BS-revenue-1` | revenue | AUTHORITY | Platform payouts are recorded gross when the business is the principal | — |
| B-18 | `BS-arap-1` | arap | CONVENTION | Aging buckets: Current, 1-30, 31-60, 61-90, 90+ days past due | — |
| B-19 | `BS-1099-1` | 1099 | AUTHORITY | 1099 reporting thresholds, payment methods, and decisions | `1099-a-dated-status-applies-by-payment-date`, `1099-a-members-check-through-financing-counts`, `1099-and-party-totals-for-a-members-payment`, `1099-card-payments-are-excluded`, `1099-money-between-own-accounts-pays-no-one`, `1099-money-from-a-payee-is-netted-only-as-a-refund`, `1099-the-threshold-is-the-years`, `1099-worksheet-through-as-of` |
| B-20 | `BS-ledger-7` | ledger | AUTHORITY | Every transaction has supporting documents; records are kept | — |
| B-21 | `BS-entry-3` | entry | AUTHORITY | Exact amounts in the books; rounding only on returns | — |
| B-21 | `BS-entry-3` | entry | AUTHORITY | Exact amounts in the books; rounding only on returns | `entry-cents-add-exactly` |
| B-22 | `BS-asset-2` | asset | AUTHORITY | Expense or capitalize (book policy, mirrors tax) | — |
| B-23 | `BS-ledger-8` | ledger | CONVENTION | Control roles: one account each, of the right kind, moved only when empty | — |
| B-23 | `BS-ledger-8` | ledger | CONVENTION | Control roles: one account each, of the right kind, moved only when empty | `arap-hand-control-posting-refused` |
| B-24 | `BS-asset-3` | asset | CHOICE | Asset tags: one label per asset, never reused | — |
| B-25 | `BS-asset-4` | asset | AUTHORITY | Tax figures beside book value: the section 723 basis, the depreciable basis, the section 704(c) built-in gain | `assets-723-basis-apart-from-depreciable-basis`, `assets-built-in-gain-fixed-at-contribution`, `assets-four-contributed-devices` |
| B-26 | `BS-partner-2` | partner | AUTHORITY | Parties: one directory, no full TIN in the books | `p3-party-tin-last4` |
@@ -56,5 +56,5 @@ without moving anyone else's number.
| B-41 | `BS-arap-5` | arap | CHOICE | A debt that will not be collected is written off, and a recovery is income | `bad-debt-direct-write-off` |
| B-42 | `BS-arap-6` | arap | CONVENTION | A settlement is exact, one party, and names its lines; the gross discount and the gross fee | — |
| B-43 | `BS-entry-5` | entry | — | Drafts and evidence (allocated, not yet stated) | — |
| B-44 | `BS-ledger-12` | ledger | — | Evidence: extraction, dedup, annotations, settlements (stated in rules.md; its bookspec example comes with P13 step 5) | — |
| B-44 | `BS-ledger-12` | ledger | CONVENTION | Evidence is read from the bytes, never trusted over them; identical files are kept once | — |
| B-45 | `BS-ledger-13` | ledger | — | Audits and attestations (allocated, not yet stated) | — |
+56
View File
@@ -0,0 +1,56 @@
# Area: statement
Reports: the trial balance, the accounting equation, the period
statements, the package as of a day, the project.
### BS-statement-1: The trial balance balances (B-6)
- **Status:** AUTHORITY
- **Statement:** The trial balance MUST list each account's balance as a debit or a credit, and the sum of the debits MUST equal the sum of the credits, as of every day.
- **Sources:**
- vendored `sources/pacioli-1494.md`: "should be equal to the total of the debits"
- vendored `sources/irs-records.md`: "the total debits must equal the total credits"
- **Symbolic form:** `sum{max(0, D_a - C_a)} = sum{max(0, C_a - D_a)}`, equivalently `sum_a (D_a - C_a) = 0` (`Bookspec.law_trial_balance`).
- **Examples:** `statement-the-books-balance-and-the-equation-holds`
- **Applicability:** every engine.
### BS-statement-2: The accounting equation (B-7)
- **Status:** AUTHORITY
- **Statement:** At every date, assets MUST equal liabilities plus equity plus revenue less expenses, each on its normal side. Income not yet closed is part of equity.
- **Sources:**
- external: FASB Concepts Statement No. 8, Chapter 4, paragraph E62: equity is the difference between the entity's assets and its liabilities
- **Symbolic form:** `A = L + E + (R - X)` (`Bookspec.law_accounting_equation`).
- **Examples:** `statement-the-books-balance-and-the-equation-holds`
- **Applicability:** every engine.
### BS-statement-3: Statements: P&L for a period; cash flows by the direct method (B-14)
- **Status:** AUTHORITY, except where marked CONVENTION or CHOICE
- **Statement:** The profit and loss for a period MUST report revenue and expense activity within it and MUST exclude closing entries. The statement of cash flows MUST report the cash receipts and payments of the period by operating, investing and financing activity (the direct method); transfers among cash accounts MUST NOT be flows; the three sections MUST sum to the change in the cash accounts' balances; noncash investing and financing MUST be disclosed apart.
- **Sources:**
- external: ASC 230-10-45-1 (cash effects of operating, investing and financing); 230-10-45-4 and 45-5 (cash and cash equivalents; transfers among them are not cash flows); 230-10-45-14 (cash receipts and payments from operating activities); 230-10-45-25 (classes of operating cash payments); 230-10-50-3 (noncash investing and financing)
- vendored `sources/fasb-asc.md`: "230-10-45"
- **Symbolic form:** `operating + investing + financing = cash_end - cash_begin` (`Bookspec.law_cash_flow_is_change_in_cash`); an entry contributes its net movement of the cash accounts only.
- **Examples:** `cash-flow.debt-to-equity`, `cash-flow.government-fees`, `cash-flow.paid-on-the-companys-behalf`, `cash-flow.part-cash-purchase`, `cash-flow.reversal-of-a-reversal`, `cash-flow.reversed-property-contribution`, `cash-flow.two-member-llc-first-year`, `period-a-close-sweeps-the-year-into-capital`
- **Applicability:** accrual and cash basis entities reporting under US GAAP.
### BS-statement-4: A package as of a day shows the books through that day (B-33)
- **Status:** CONVENTION; CHOICE for the 1099 worksheet (the owner, 2026-10-20)
- **Statement:** A report or package made as of a day MUST reflect nothing dated after it: no figure, entry, receipt or worksheet row. Balances run through the day; a period's activity from the period's start through it.
- **Sources:**
- vendored `sources/irs-periods-and-methods.md`: "A tax year is an annual accounting period for keeping records and reporting income and expenses"
- **Symbolic form:** `report(as_of) = report(books restricted to date <= as_of)`.
- **Examples:** `as-of-package-stops-at-as-of`
- **Applicability:** every engine that produces a package.
### BS-statement-5: A project is a class with its facts; its figures are its tagged lines (B-37)
- **Status:** CHOICE (the owner, 2026-10-21); CONVENTION (job costing); AUTHORITY (costs of work on spec are expensed)
- **Statement:** A project's revenue, expenses and net MUST be exactly the revenue and expense lines tagged with it, closes left out; its to-date figures MUST be every tagged line through the date; an on-spec project's unrecovered cost MUST be its expenses less its revenue to date, never below zero.
- **Sources:**
- external: ASC 340-40-25-5 and 340-40-25-7 (costs to fulfil a contract); ASC 450-30-25-1 (gain contingencies)
- **Symbolic form:** `net(p) = sum{signed credit-normal value of tagged revenue and expense lines of p}`.
- **Examples:** `projects-invoice-and-bill-lines-count-by-project`, `projects-net-is-its-tagged-lines`, `projects-on-spec-costs-are-expenses`
- **Applicability:** engines with classes or projects.