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())
{