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

+14 -1
View File
@@ -192,6 +192,14 @@ fn refusal(e: &LedgerError) -> String {
LedgerError::InvalidDepreciationPeriod(_) => "invalid_depreciation_period",
LedgerError::InvalidSchedule(_) => "invalid_schedule",
LedgerError::UntaggedControlLine { .. } => "untagged_control_line",
LedgerError::UnbalancedEntry { .. } => "unbalanced",
LedgerError::TooFewLines => "too_few_lines",
LedgerError::NegativeAmountLine => "negative_amount",
LedgerError::ZeroAmountLine => "zero_amount",
LedgerError::FinerThanMinorUnit { .. } => "finer_than_minor_unit",
LedgerError::ParentTypeMismatch { .. } => "parent_type_mismatch",
LedgerError::EntryAlreadyReversed(..) => "already_reversed",
LedgerError::ClosedPeriod(..) => "period_closed",
other => return format!("another refusal: {other}"),
}
.to_string()
@@ -1029,7 +1037,12 @@ impl<'a> Books<'a> {
) -> Result<(), StoreError> {
let money = |s: &str| {
let amount = exact(s, what);
assert!(amount > Decimal::ZERO, "{what}: amounts are positive");
// Only a refused entry carries an amount the books refuse
// (the schema checks it).
assert!(
amount > Decimal::ZERO || e.refused.is_some(),
"{what}: amounts are positive"
);
amount
};
let day = || when.expect("checked: dated");