Split kernel::entry_flows into entry_movements and flows_of so the reversal-mirror proof fits CI; prove runs the fast harnesses, prove-slow the 8 slow ones (own job in scheduled CI), prove-all both. Laws in verification.md renumbered 1-31 with a table of all 55 harnesses. MCP docs checked against the 115 tools (about 60 fixes) with a test that every tool is documented; rules.md cross-references fixed; bookspec statuses and loader tightened; viewer notes wrap and accounts sort by code; STATUS.md says P3 is built, cutover pending. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01T3gXMVt7XJPi6ens1or9Dj
This commit is contained in:
49 files changed
+963
-292
No files matched your search
@@ -15,6 +15,14 @@ jobs:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: jdx/mise-action@v2
|
||||
- run: just prove
|
||||
# The Kani harnesses too slow for prove's 30 minutes on four cores
|
||||
# (justfile kani_slow; docs/verification.md), on a runner of their own.
|
||||
prove-slow:
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: jdx/mise-action@v2
|
||||
- run: just prove-slow
|
||||
oracle:
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
|
||||
@@ -100,7 +100,7 @@ just # lists the recipes: check, build, prove, demo, ...
|
||||
```
|
||||
crates/ledger-core/ # engine: money, accounts, journal, ledger, reports
|
||||
crates/ledger-store/ # journal + SQLite projection + blob store
|
||||
crates/ledger-mcp/ # MCP server: 109 tools, resources, handbook, receipt policy
|
||||
crates/ledger-mcp/ # MCP server: 115 tools, resources, handbook, receipt policy
|
||||
crates/ledger-import/ # QBO CSV import, IIF/XLSX export
|
||||
crates/ledger-config/ # wanderbooks.toml company settings
|
||||
crates/ledger-cli/ # `wanderbooks`: the one executable (every mode)
|
||||
|
||||
+22
-9
@@ -94,9 +94,20 @@ down, the units left over to the largest remainders, ties to the part
|
||||
listed first; rules.md B-13). An example that closes to the members
|
||||
declares `largest-remainder`, the rule a members close splits by.
|
||||
|
||||
Required everywhere: the top-level keys, `[setup]` with its four keys and
|
||||
its accounts, the events (`events = []`, before `[setup]`, when nothing is
|
||||
posted), and at least one `[[expect]]` with values: a figure query's are
|
||||
`status` says what the expected values rest on: `AUTHORITY`, the cited
|
||||
authorities alone (a standard, a regulation, a statute, a court, the
|
||||
mechanics of double entry); `CONVENTION`, common practice where no
|
||||
authority governs (rules.md's CONVENTION); `CHOICE`, an owner's or the
|
||||
house's decision (a plan D-number, or a DECISION rules.md records) that
|
||||
some expected value depends on, even when authorities carry the rest. An
|
||||
example whose figures follow a decision cites it in `sources`.
|
||||
|
||||
Required everywhere: the top-level keys (`title`, `rule`, `tags` and
|
||||
`sources` non-empty), `[setup]` with its four keys and its accounts, the
|
||||
events (`events = []`, before `[setup]`, when nothing is posted), and at
|
||||
least one `[[expect]]`. A value from a closed list (a type, a kind, a
|
||||
role, a form, a basis) is spelled exactly as written here. A figure
|
||||
query's values are
|
||||
exact decimal strings (`"2340.00"`, `"-12.50"`; never a float), a format
|
||||
query's are text (below). Amounts on lines are positive;
|
||||
the side gives the direction.
|
||||
@@ -183,11 +194,12 @@ the side gives the direction.
|
||||
code, the company owes the party itself (a card's issuer, a lender,
|
||||
each member for what the members advanced: B-35 item 6). `tin_last4`:
|
||||
its TIN facts' last four (an SSN's); `w9_tin_last4`: its W-9's (a 1099
|
||||
role's); `tin_purged = true`: the vault's copy purged, the facts kept.
|
||||
role's); `tin_purged = true`: the vault's copy purged, the facts kept (needs
|
||||
`tin_last4`).
|
||||
Never a full TIN. Each name once.
|
||||
- `[[setup.members]]` `{ name, capital_account, share?,
|
||||
distributions_account? }`: the members of a multi-member LLC. `share`
|
||||
is the member's share of income, an exact decimal fraction (`"0.5"`,
|
||||
is the member's share of income, an exact decimal fraction above zero (`"0.5"`,
|
||||
`"0.3333"`); every member has one or none does, and the shares sum to
|
||||
1. A close to the members, `members_equity`, `k1`, `k1_tax_basis` and
|
||||
`k1_reconciliation` need them. `distributions_account`: the member's
|
||||
@@ -211,7 +223,8 @@ the side gives the direction.
|
||||
- `[[setup.instruments]]` `{ label, kind, account, last4?, owner? }`: a
|
||||
payment method (B-19): `kind` `bank`, `business_card`, `member_card`,
|
||||
`member_cash_or_check` or `platform`; `account` the code it pays
|
||||
through; `owner` a party's name (informational). A business card's
|
||||
through; `owner` a party's name in `setup.parties` (informational); each label
|
||||
once. A business card's
|
||||
liability is a card account (B-35 item 5): while none is recorded, a
|
||||
liability whose name says "card", not "gift card", is.
|
||||
- `payee` on an event: the payee (by name, one of `setup.payees`) the
|
||||
@@ -232,7 +245,7 @@ the side gives the direction.
|
||||
`"members"` (split by share, in the order listed) or
|
||||
`"retained_earnings"`. A close to the members splits cumulatively, each
|
||||
close giving each member their split of everything the members' closes
|
||||
have closed less what earlier closes gave them; `rounding = "per-close"`
|
||||
have closed less what earlier closes gave them; `rounding = "per-close"` (members only)
|
||||
splits that close's net on its own instead (books from an engine that
|
||||
rounded each close by itself). A close must have something to close: it
|
||||
posts one entry like any other event.
|
||||
@@ -292,8 +305,8 @@ answers is refused, never skipped.
|
||||
| query | dates | values | checked by |
|
||||
|---|---|---|---|
|
||||
| `cash_flow` | `from`, `to` | figures (below) | `ledger-core` `tests/bookspec.rs` |
|
||||
| `trial_balance` | `as_of` | each account's balance by code, debits less credits (a credit balance negative), every account listed | `ledger-core` `tests/bookspec.rs` |
|
||||
| `profit_and_loss` | `from`, `to` | each revenue and expense account's activity by code, on its normal side, closing entries left out, every one listed; `net_income`, revenue less expenses | `ledger-core` `tests/bookspec.rs` |
|
||||
| `trial_balance` | `as_of` | each account's balance by code, debits less credits (a credit balance negative), every account of `setup.accounts` listed (not those the books create) | `ledger-core` `tests/bookspec.rs` |
|
||||
| `profit_and_loss` | `from`, `to` | each revenue and expense account's activity by code, on its normal side, closing entries left out, every one of `setup.accounts` listed; `net_income`, revenue less expenses | `ledger-core` `tests/bookspec.rs` |
|
||||
| `entry` | `entry = N`, no dates | the lines of event N's entry, `"<code>.debit"` and `"<code>.credit"` each the sum of that account's lines on that side, every line of the entry named | `ledger-core` `tests/bookspec.rs` |
|
||||
| `balances` | `as_of` | each account named by code, its balance on its normal side (a member's capital after a loss is negative); the example names the accounts it shows | `ledger-core` `tests/bookspec.rs` |
|
||||
| `members_equity` | `from`, `to` | the statement of changes in members' equity (rules.md B-13): `"<member>.beginning"`, `.contributions`, `.net_income`, `.distributions`, `.other`, `.ending`; the example names the figures it shows | `ledger-core` `tests/bookspec.rs` |
|
||||
|
||||
@@ -13,11 +13,12 @@ member's payment whose method isn't recorded is undetermined, never
|
||||
guessed; the payee is reportable only if what certainly counts reaches the
|
||||
threshold.
|
||||
"""
|
||||
status = "AUTHORITY"
|
||||
status = "CHOICE"
|
||||
sources = [
|
||||
"Treas. Reg. §1.6041-1(a)(1)(i), (e)(1) (docs/spec/sources/irs-1099-payment-method.md)",
|
||||
"Treas. Reg. §1.6041-1(a)(1)(iv)",
|
||||
"26 U.S.C. §6041(a), as amended by Pub. L. 119-21 §70433 (docs/spec/sources/irs-1099-thresholds.md)",
|
||||
"DECISION (owner, plan/served.md P3 defect 12; D-40, 2026-10-20): a payment through financing with no payment method recorded is undetermined (wanderbooks rules.md B-19)",
|
||||
]
|
||||
tags = ["1099", "financing", "member-paid", "instruments"]
|
||||
|
||||
|
||||
@@ -3,13 +3,14 @@
|
||||
id = "assets-723-basis-apart-from-depreciable-basis"
|
||||
title = "Property contributed from personal use: the §723 basis apart from the depreciable basis"
|
||||
rule = "The partnership's basis in property a partner contributes is the partner's adjusted basis (IRC §723). Its depreciable basis, for property converted from personal use, is the lesser of fair market value and that adjusted basis at conversion (CHOICE recorded by the owner as D-41, the CPA to confirm). The §704(c) built-in gain or loss is fair market value less the §723 basis at contribution; a built-in loss counts only for the contributing partner (IRC §704(c)(1)(C))."
|
||||
status = "AUTHORITY"
|
||||
status = "CHOICE"
|
||||
sources = [
|
||||
"26 U.S.C. §723",
|
||||
"Treas. Reg. §1.168(i)-4(b)(1): 'the lesser of its fair market value or its adjusted depreciable basis'",
|
||||
"IRS Pub. 946 (2025), Property changed from personal use",
|
||||
"Treas. Reg. §1.704-3(a)(3)(ii)",
|
||||
"26 U.S.C. §704(c)(1)(C)",
|
||||
"DECISION D-41 (owner, 2026-10-20): the depreciable basis of contributed personal-use property is the lesser of its fair market value and the member's adjusted basis, the CPA to confirm (wanderbooks plan/served.md; rules.md B-25)",
|
||||
]
|
||||
tags = ["fixed-assets", "tax-basis", "partnerships", "704c"]
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ rule = "The accumulated depreciation the books carry for an asset counts each ru
|
||||
status = "AUTHORITY"
|
||||
sources = [
|
||||
"wanderbooks docs/spec/rules.md B-8, B-9 (a correction is an opposite entry; reversing the reversal reinstates the original)",
|
||||
"wanderbooks docs/spec/rules.md B-24",
|
||||
"wanderbooks docs/spec/rules.md B-15 rules 6 and 8",
|
||||
]
|
||||
tags = ["fixed-assets", "depreciation", "reversal"]
|
||||
|
||||
|
||||
@@ -3,11 +3,12 @@
|
||||
id = "assets-acquired-before-in-service"
|
||||
title = "An asset is on the register from acquisition and depreciates from its in-service date"
|
||||
rule = "Depreciation starts on the day the asset is placed in service; the asset is on the register (and the books carry its cost) from the day the company acquired it, the date of the entry that put it on the books."
|
||||
status = "AUTHORITY"
|
||||
status = "CHOICE"
|
||||
sources = [
|
||||
"ASC 360-10-35-4 (cost allocated over the periods the asset serves)",
|
||||
"IRS Pub. 946 (2025), Placed in Service: 'ready and available for a specific use' (for tax depreciation, the CPA's)",
|
||||
"wanderbooks docs/spec/rules.md B-15, B-24",
|
||||
"DECISION (owner, 2026-10-20; wanderbooks plan/served.md P3, \"Settled in design\"): an asset's acquired date comes from the entry it is linked to (rules.md B-15 rule 7)",
|
||||
]
|
||||
tags = ["fixed-assets", "dates"]
|
||||
|
||||
|
||||
@@ -3,12 +3,13 @@
|
||||
id = "assets-four-contributed-devices"
|
||||
title = "Each contributed device takes the lower of its value and its basis; gains and losses are per property"
|
||||
rule = "Each item of contributed property is its own: depreciable basis is the lesser of its fair market value and the contributor's adjusted basis; its built-in gain or loss is its own, and gains and losses of different properties are never netted."
|
||||
status = "AUTHORITY"
|
||||
status = "CHOICE"
|
||||
sources = [
|
||||
"Treas. Reg. §1.168(i)-4(b)(1)",
|
||||
"Treas. Reg. §1.704-3(a)(2): section 704(c) applies 'on a property-by-property basis'",
|
||||
"Treas. Reg. §1.704-3(a)(3)(ii)",
|
||||
"26 U.S.C. §704(c)(1)(C)",
|
||||
"DECISION D-41 (owner, 2026-10-20): the depreciable basis of contributed personal-use property is the lesser of its fair market value and the member's adjusted basis, the CPA to confirm (wanderbooks plan/served.md; rules.md B-25)",
|
||||
]
|
||||
tags = ["fixed-assets", "tax-basis", "partnerships", "704c"]
|
||||
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
id = "depreciation-reversal-before-its-run-is-refused"
|
||||
title = "A depreciation run can't be reversed on a day before its own"
|
||||
rule = "A correction comes after what it corrects: a depreciation run, or a reversal of one, can't be reversed on a day before its own. A run of March reversed on Jan 31 would leave January and February with negative depreciation."
|
||||
status = "AUTHORITY"
|
||||
sources = ["Pacioli 1494, ch. 31"]
|
||||
status = "CONVENTION"
|
||||
sources = [
|
||||
"Pacioli 1494, ch. 31",
|
||||
"wanderbooks docs/spec/rules.md B-15 rule 6, B-9: a correction comes after what it corrects (CONVENTION)",
|
||||
]
|
||||
tags = ["depreciation", "reversal", "refusal", "p3-defect-22"]
|
||||
|
||||
[setup]
|
||||
|
||||
@@ -32,7 +32,7 @@ id = "iif-quickbooks-journal-file"
|
||||
appendix = "formats"
|
||||
title = "A QuickBooks Desktop journal file as of a day"
|
||||
rule = "An IIF journal file as of a day D MUST declare every account first (NAME, ACCNTTYPE, ACCNUM), then hold one GENERAL JOURNAL transaction per posted entry dated on or before D, period closes excepted, in the order posted: a TRNS line and SPL lines, debits positive and credits negative, dated M/D/YYYY, each numbered (DOCNUM) with the entry's id; then ENDTRNS. It MUST be ASCII text, every line ending CR LF, no field holding a tab, a line break or a double quote; an account name level MUST be at most 31 characters and no two NAMEs alike, an account number at most 7 characters and no two alike (else left out), a memo at most 4,095 characters; each parent MUST be declared before its subaccounts, and a subaccount whose QuickBooks type differs from its parent's MUST be written at the top level. Each text written differently from the books MUST be reported."
|
||||
status = "AUTHORITY"
|
||||
status = "CHOICE"
|
||||
sources = [
|
||||
"Intuit, IIF Overview: import kit, sample files, and headers: IIF files are ASCII text, tab-separated; an account not already in the company file is created as a new bank account (docs/spec/sources/intuit-iif.md)",
|
||||
"Intuit, IIF Import Kit, IIF Header Help !TRNS, !SPL, !ACCNT: TRNS and SPL lines then ENDTRNS; \"Debit amounts are always positive, credit amounts are always negative\"; DOCNUM is the transaction's number; a subaccount's NAME its parents' names first, joined by colons (docs/spec/sources/intuit-iif.md)",
|
||||
@@ -40,6 +40,7 @@ sources = [
|
||||
"Intuit, Character limitations for fields in QuickBooks (US, updated 8/3/2026): Account Name 31, Account Numbers 7, Memo 4,095 (docs/spec/sources/intuit-iif.md)",
|
||||
"Intuit, Add, edit, or delete accounts in QuickBooks Desktop (updated 8/4/2026): \"The parent and subaccount must be the same account type.\" (docs/spec/sources/intuit-iif.md)",
|
||||
"Reckon, IIF import changes a double quote to a single one by design; J. Clark (2003): QuickBooks uses double quotes as text delimiters (docs/spec/sources/intuit-iif.md; not Intuit)",
|
||||
"DECISION D-36 (owner, 2026-10-20): plain ASCII, what isn't ASCII transliterated and listed; CR LF; DOCNUM the entry's id; closes left out (wanderbooks plan/served.md; rules.md B-34)",
|
||||
]
|
||||
tags = ["iif", "quickbooks", "as-of", "format", "transliteration"]
|
||||
|
||||
|
||||
@@ -2,7 +2,11 @@ id = "members-close.odd-cent-across-closes"
|
||||
title = "An odd cent split cumulatively across closes, a close to retained earnings between"
|
||||
rule = "Each close to the members MUST give each member their largest-remainder split of everything closed to the same members and shares so far, to the minor unit, less what earlier closes gave them; a close to retained earnings allocates nothing to the members and MUST NOT restart the count."
|
||||
status = "CHOICE"
|
||||
sources = ["26 U.S.C. §704(a)", "Treas. Reg. §1.704-1(b)(2)(iv)(b)"]
|
||||
sources = [
|
||||
"26 U.S.C. §704(a)",
|
||||
"Treas. Reg. §1.704-1(b)(2)(iv)(b)",
|
||||
"DECISION (owner, 2026-10-13: largest remainder; D-10, 2026-10-16: counted from the first close) (wanderbooks rules.md B-13)",
|
||||
]
|
||||
tags = ["partnership", "close", "rounding", "book", "us"]
|
||||
|
||||
# The regression this example comes from (wanderbooks P3 defect 17, audit
|
||||
|
||||
@@ -2,7 +2,10 @@ id = "members-close.statement-before-and-after-a-close"
|
||||
title = "A member's capital on a day is the same before and after its month closes"
|
||||
rule = "The statement of members' equity MUST split earnings not yet closed the way the next close to the members will (each member's split of everything closed plus those earnings, less their split of what was closed), so a member's capital as of a day MUST NOT change when a period ending on or before it is closed."
|
||||
status = "CHOICE"
|
||||
sources = ["Treas. Reg. §1.704-1(b)(2)(iv)(b)"]
|
||||
sources = [
|
||||
"Treas. Reg. §1.704-1(b)(2)(iv)(b)",
|
||||
"DECISION D-38 (owner, 2026-10-20): the statement splits unclosed earnings the same running way as the closes (wanderbooks rules.md B-13)",
|
||||
]
|
||||
tags = ["partnership", "close", "rounding", "statement", "book", "us"]
|
||||
|
||||
# Two members at 50/50; January's cent is closed to them, February's is
|
||||
|
||||
@@ -2,7 +2,11 @@ id = "members-close.thirds-over-twelve-months"
|
||||
title = "Three members at a third each, twelve monthly closes"
|
||||
rule = "Each monthly close to the members MUST give each member their largest-remainder split of everything closed so far, to the minor unit, less what earlier closes gave them, so after every close each member's total is less than one minor unit from their exact share."
|
||||
status = "CHOICE"
|
||||
sources = ["26 U.S.C. §704(a)", "Treas. Reg. §1.704-1(b)(2)(iv)(b)"]
|
||||
sources = [
|
||||
"26 U.S.C. §704(a)",
|
||||
"Treas. Reg. §1.704-1(b)(2)(iv)(b)",
|
||||
"DECISION (owner, 2026-10-13: largest remainder; D-10, 2026-10-16: counted from the first close) (wanderbooks rules.md B-13)",
|
||||
]
|
||||
tags = ["partnership", "close", "rounding", "book", "us"]
|
||||
|
||||
# A third each can't be written as decimal shares summing to one, so the
|
||||
|
||||
@@ -2,7 +2,10 @@ id = "members-close.true-up-of-a-per-close-split"
|
||||
title = "Closes rounded one at a time, trued up by the first cumulative close"
|
||||
rule = "When earlier closes split each close's net on its own, the first cumulative close that posts an entry MUST give each member their split of everything closed to the members, less what the earlier closes gave them, so it trues up the rounding they left; until it posts, the statement of members' equity MUST show the capital those closes posted, not the pending true-up."
|
||||
status = "CHOICE"
|
||||
sources = ["Treas. Reg. §1.704-1(b)(2)(iv)(b)"]
|
||||
sources = [
|
||||
"Treas. Reg. §1.704-1(b)(2)(iv)(b)",
|
||||
"DECISION D-10 (owner, 2026-10-16): members closes counted from the first close, so a later close carries any earlier rounding as a true-up (wanderbooks rules.md B-13)",
|
||||
]
|
||||
tags = ["partnership", "close", "rounding", "statement", "book", "us"]
|
||||
|
||||
# Books migrated from an engine that rounded each close on its own: three
|
||||
|
||||
@@ -20,11 +20,12 @@ promise: it counts nothing until it is paid, then once, as paid. The
|
||||
statement of members' equity counts a distribution when it is declared,
|
||||
so party totals catch up with it when it is paid.
|
||||
"""
|
||||
status = "AUTHORITY"
|
||||
status = "CHOICE"
|
||||
sources = [
|
||||
"Helvering v. Price, 309 U.S. 409, 412-413 (1940): a promise to pay is not payment (wanderbooks docs/spec/sources/helvering-v-price-1940.md)",
|
||||
"Treas. Reg. §1.704-1(b)(2)(iv)(b)(4)-(5): a partner's capital account falls by the money and the property distributed (docs/spec/sources/irs-capital-accounts.md)",
|
||||
"DECISION (owner, 2026-10-20): a member's entry counts what moved their capital; money is cash, cards, the members' financing and loans (rules.md B-35)",
|
||||
"DECISION (owner, 2026-10-20): property distributed counts at the books' carrying amount, not its fair market value (wanderbooks rules.md B-35 item 3)",
|
||||
]
|
||||
tags = ["parties", "members", "distributions", "promises", "in-kind", "money"]
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ owes its customers in service (deferred revenue), a promise. Selling one
|
||||
is money received from the buyer; redeeming it moves no money and counts
|
||||
nothing.
|
||||
"""
|
||||
status = "CONVENTION"
|
||||
status = "CHOICE"
|
||||
sources = [
|
||||
"DECISION (owner, 2026-10-20): money is cash, cards, the members' financing and loans; every other liability is a promise, counted when settled (wanderbooks rules.md B-35 item 5)",
|
||||
"Helvering v. Price, 309 U.S. 409, 412-413 (1940): a promise to pay is not payment (docs/spec/sources/helvering-v-price-1940.md)",
|
||||
|
||||
@@ -15,7 +15,7 @@ seller's own note, a distribution credited to what the members are owed.
|
||||
It counts when it is paid. Property a member contributes subject to a
|
||||
loan counts at its value net of the loan, in kind.
|
||||
"""
|
||||
status = "AUTHORITY"
|
||||
status = "CHOICE"
|
||||
sources = [
|
||||
"Helvering v. Price, 309 U.S. 409, 412-413 (1940): a promise to pay, even a secured one, is not payment (wanderbooks docs/spec/sources/helvering-v-price-1940.md)",
|
||||
"Treas. Reg. §1.704-1(b)(2)(iv)(b)(2): property contributed, net of liabilities the partnership takes it subject to (docs/spec/sources/irs-capital-accounts.md)",
|
||||
|
||||
@@ -14,9 +14,10 @@ their own funds (Dr card / Cr due to members), tagged with the card's
|
||||
issuer, is paid to the issuer; its reversal takes that back, so the pair
|
||||
nets to zero; reversing the reversal pays it again.
|
||||
"""
|
||||
status = "AUTHORITY"
|
||||
status = "CHOICE"
|
||||
sources = [
|
||||
"Pacioli, Summa (1494), Geijsbeek tr.: a mistake is corrected by the opposite entry (wanderbooks docs/spec/sources/pacioli-1494.md, rules.md B-9)",
|
||||
"DECISION D-42 (owner, 2026-10-20): what a member pays for the company through the members' financing is money: cash borrowed and spent (wanderbooks plan/served.md; rules.md B-14 rule 11, B-35 items 5 and 8)",
|
||||
]
|
||||
tags = ["parties", "reversals", "liabilities"]
|
||||
|
||||
|
||||
@@ -19,12 +19,13 @@ negated. A bill or invoice moves nothing with the party yet (neither).
|
||||
Party totals on basis cash count the cash parts, on basis all the cash
|
||||
and in-kind parts, each on its root original's side.
|
||||
"""
|
||||
status = "CONVENTION"
|
||||
status = "CHOICE"
|
||||
sources = [
|
||||
"Pacioli, Summa (1494), Geijsbeek tr., ch. 36: debit is what owes, credit what is owed (wanderbooks docs/spec/sources/pacioli-1494.md)",
|
||||
"Treas. Reg. §1.704-1(b)(2)(iv)(b)(1)-(2), (4)-(5): a partner's capital account rises by money and property contributed, falls by money and property distributed (docs/spec/sources/irs-capital-accounts.md)",
|
||||
"Helvering v. Price, 309 U.S. 409, 412-413 (1940): a promise to pay is not payment (docs/spec/sources/helvering-v-price-1940.md)",
|
||||
"DECISION (owner, 2026-10-20): a member's entry counts what moved their capital (wanderbooks rules.md B-35)",
|
||||
"DECISION D-42 (owner, 2026-10-20): what a member pays for the company through the members' financing is money: cash borrowed and spent (wanderbooks plan/served.md; rules.md B-14 rule 11, B-35 items 5 and 8)",
|
||||
]
|
||||
tags = ["parties", "signs", "reversals", "members", "in-kind", "basis"]
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ the K-1), else its W-9 facts'; the two never disagree. A TIN purged from
|
||||
the vault keeps its last four in the books. A party with neither has
|
||||
none.
|
||||
"""
|
||||
status = "AUTHORITY"
|
||||
status = "CHOICE"
|
||||
sources = [
|
||||
"Treas. Reg. §301.6109-4(a): a truncated TIN shows the last four digits (wanderbooks docs/spec/sources/irs-truncated-tin.md)",
|
||||
"DECISION (owner, 2026-10-15): a party's TIN facts and W-9 facts never disagree (wanderbooks ledger-core parties INVARIANT 2a)",
|
||||
|
||||
@@ -8,6 +8,7 @@ sources = [
|
||||
"Instructions for Form 1065 (2020), Item L: 'you must use the tax basis method this year'",
|
||||
"26 U.S.C. §723",
|
||||
"Treas. Reg. §1.704-3(a)(3)(ii)",
|
||||
"DECISION D-41 (owner, 2026-10-20): the K-1 worksheet reports tax-basis capital beside the book figures (wanderbooks plan/served.md; rules.md B-36)",
|
||||
]
|
||||
tags = ["partnerships", "k1", "tax-basis", "capital"]
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
id = "rename-changes-no-amount"
|
||||
title = "Renaming an account changes its label, never an amount"
|
||||
rule = "Entries name accounts by identity, not by name, so renaming an account MUST change no posted entry, line, balance or statement amount, and every report, past periods included, MUST print the new name. A new name MUST be refused when it is blank, the name the account has, one another account under the same parent has (ignoring capitals and surrounding space), one that holds a run of nine digits, or not one line of at most 200 characters."
|
||||
status = "AUTHORITY"
|
||||
status = "CHOICE"
|
||||
sources = [
|
||||
"Pacioli, Summa de arithmetica (1494), ch. 36: \"You should not write backward, but always forward\" (posted is forever: AUTHORITY; docs/spec/sources/pacioli-1494.md)",
|
||||
"Rev. Proc. 98-25 §5.01: \"the relationship (i.e., audit trail): (a) between the total of the amounts in the taxpayer's machine-sensible records by account and the account totals in the taxpayer's books\" (an amount never moves with a name: AUTHORITY; docs/spec/sources/irs-records.md)",
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
//! — and postings go through the normal entry machinery, so every
|
||||
//! accounting invariant holds.
|
||||
//!
|
||||
//! Two figures of accumulated depreciation, never confused (rules.md B-24):
|
||||
//! Two figures of accumulated depreciation, never confused (rules.md B-15 rule 8):
|
||||
//! what the schedule says is due through a date
|
||||
//! ([`accumulated_scheduled_through`]) and what the books carry, the runs
|
||||
//! posted and not reversed ([`crate::Ledger::accumulated_posted`]). Book
|
||||
@@ -743,7 +743,7 @@ fn schedule_months(asset: &Asset, through: NaiveDate) -> Result<Vec<MonthLine>,
|
||||
/// (inclusive), exact to the minor unit ([`depreciation_schedule`]): the
|
||||
/// tools' `accumulated_scheduled_through`. What the books carry is
|
||||
/// [`crate::Ledger::accumulated_posted`]; the two agree once every month
|
||||
/// due is posted (rules.md B-24).
|
||||
/// due is posted (rules.md B-15 rule 8).
|
||||
pub fn accumulated_scheduled_through(
|
||||
asset: &Asset,
|
||||
as_of: NaiveDate,
|
||||
|
||||
@@ -680,7 +680,17 @@ pub fn apply_cash<T: Amount>(cash: T, movements: &[T], applied: &mut [T]) -> Opt
|
||||
/// operating, investing, financing, each up to its own movement; what is
|
||||
/// left of a section's movement is the part no cash settled. `None` when
|
||||
/// the lines don't net to zero (never for a posted entry) or on overflow.
|
||||
/// It is [`entry_movements`] then [`flows_of`], so each is proved on its
|
||||
/// own (docs/verification.md, law 10).
|
||||
pub fn entry_flows<T: Amount>(lines: &[(Flow, Side, T)]) -> Option<EntryFlows<T>> {
|
||||
let (cash, moved) = entry_movements(lines)?;
|
||||
flows_of(cash, moved)
|
||||
}
|
||||
|
||||
/// An entry's net movement on its cash lines and on each section's,
|
||||
/// `(cash, [operating, investing, financing])`, debit-positive. `None`
|
||||
/// on overflow.
|
||||
pub fn entry_movements<T: Amount>(lines: &[(Flow, Side, T)]) -> Option<(T, [T; 3])> {
|
||||
let mut cash = T::ZERO;
|
||||
let mut moved = [T::ZERO; 3];
|
||||
for &(flow, side, amount) in lines {
|
||||
@@ -693,6 +703,13 @@ pub fn entry_flows<T: Amount>(lines: &[(Flow, Side, T)]) -> Option<EntryFlows<T>
|
||||
};
|
||||
*slot = slot.checked_add(delta)?;
|
||||
}
|
||||
Some((cash, moved))
|
||||
}
|
||||
|
||||
/// The cash flows of an entry whose cash lines moved `cash` and whose
|
||||
/// sections moved `moved` ([`entry_flows`]). `None` when they don't net
|
||||
/// to zero, or on overflow.
|
||||
pub fn flows_of<T: Amount>(cash: T, moved: [T; 3]) -> Option<EntryFlows<T>> {
|
||||
// Balanced lines only: every part below rests on it.
|
||||
let mut total = cash;
|
||||
for &m in &moved {
|
||||
|
||||
@@ -2534,7 +2534,7 @@ impl Ledger {
|
||||
/// ```text
|
||||
/// GOAL: a run the books record is the entry that posted it, no more
|
||||
/// and no less, so accumulated_posted is what the books carry.
|
||||
/// SOURCE: rules.md B-24; B-28 (new commands are bounded; replay checks
|
||||
/// SOURCE: rules.md B-15 rules 6 and 8, B-24; B-28 (new commands are bounded; replay checks
|
||||
/// only what every journaled run passed:
|
||||
/// record_depreciation_run).
|
||||
/// ```
|
||||
|
||||
@@ -85,7 +85,7 @@ pub type PayoutPlanLine = (AccountId, Side, Decimal, Option<ClassId>);
|
||||
/// collected; fees debit expense accounts), and the payout reconciles to
|
||||
/// the cent.
|
||||
/// A zero deposit (fees and adjustments ate the sales exactly) has no
|
||||
/// deposit line: every planned line is positive (B-17, docs/spec/rules.md).
|
||||
/// deposit line: every planned line is positive (B-2, docs/spec/rules.md).
|
||||
pub fn payout_plan(
|
||||
payout: &Payout,
|
||||
chart: &ChartOfAccounts,
|
||||
|
||||
@@ -1031,12 +1031,17 @@ fn entry_flows_are_bounded() {
|
||||
|
||||
/// A reversal's cash flows are its original's, negated, part by part:
|
||||
/// what lets an entry and its reversal in one period drop out of the
|
||||
/// statement together (rules.md B-14 rule 6).
|
||||
/// statement together (rules.md B-14 rule 6). End to end, on entries of
|
||||
/// up to two lines: at three (`NF`) one run went past half an hour at
|
||||
/// four jobs on a busy machine (2026-10-21), so three lines are proved in
|
||||
/// two halves, `entry_movements_of_a_reversal_mirror` and
|
||||
/// `flows_of_mirrors`, and Lean `flows_reversal` proves it at any size.
|
||||
#[kani::proof]
|
||||
#[kani::unwind(5)]
|
||||
fn entry_flows_of_a_reversal_mirror() {
|
||||
let all = any_flow_lines();
|
||||
let n = any_flow_len();
|
||||
kani::assume(n <= 2);
|
||||
let lines = &all[..n];
|
||||
// Element by element (see `kani_symbolic_copy_canary`).
|
||||
let mut rev = [(Flow::Cash, Side::Debit, 0i64); NF];
|
||||
@@ -1046,6 +1051,67 @@ fn entry_flows_of_a_reversal_mirror() {
|
||||
let a = kernel::entry_flows(lines);
|
||||
let b = kernel::entry_flows(&rev[..n]);
|
||||
assert_eq!(a.is_some(), b.is_some());
|
||||
if let (Some(a), Some(b)) = (a, b) {
|
||||
assert_eq!(b.cash, -a.cash);
|
||||
for k in 0..3 {
|
||||
assert_eq!(b.flows[k], -a.flows[k]);
|
||||
assert_eq!(b.noncash[k], -a.noncash[k]);
|
||||
}
|
||||
kani::cover!(a.flows[1] != 0, "cash settling investing, reversed");
|
||||
}
|
||||
}
|
||||
|
||||
/// The two halves of `entry_flows_of_a_reversal_mirror`, each for every
|
||||
/// input in its bounds: `kernel::entry_flows` is `entry_movements` then
|
||||
/// `flows_of`, so a reversal mirrors when its movements are its
|
||||
/// original's negated (this harness, up to `NF` lines each in any of the
|
||||
/// four places) and `flows_of` is odd (the next, for any movements three
|
||||
/// lines can make). Together they prove the end-to-end harness's claim,
|
||||
/// whose single run past half an hour at four jobs on a busy machine
|
||||
/// (2026-10-21) moved it to `just prove-slow`.
|
||||
#[kani::proof]
|
||||
#[kani::unwind(5)]
|
||||
fn entry_movements_of_a_reversal_mirror() {
|
||||
let all = any_flow_lines();
|
||||
let n = any_flow_len();
|
||||
let lines = &all[..n];
|
||||
// Element by element (see `kani_symbolic_copy_canary`).
|
||||
let mut rev = [(Flow::Cash, Side::Debit, 0i64); NF];
|
||||
for i in 0..n {
|
||||
rev[i] = kernel::reverse_line(lines[i]);
|
||||
}
|
||||
let a = kernel::entry_movements(lines);
|
||||
let b = kernel::entry_movements(&rev[..n]);
|
||||
assert_eq!(a.is_some(), b.is_some());
|
||||
if let (Some((ca, ma)), Some((cb, mb))) = (a, b) {
|
||||
assert_eq!(cb, -ca);
|
||||
for k in 0..3 {
|
||||
assert_eq!(mb[k], -ma[k]);
|
||||
}
|
||||
kani::cover!(
|
||||
n == NF && ca != 0 && ma[1] != 0,
|
||||
"three lines, cash and investing"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// `kernel::flows_of` is odd: the negated movements give the negated
|
||||
/// flows, part by part, and succeed exactly when the original's do; for
|
||||
/// any cash and section movements up to `NF` lines' worth.
|
||||
#[kani::proof]
|
||||
#[kani::unwind(5)]
|
||||
fn flows_of_mirrors() {
|
||||
let wide_amount = || {
|
||||
let a: i64 = kani::any();
|
||||
// What up to NF in-range lines can sum to (`in_line_range`: 2^40).
|
||||
kani::assume(a.unsigned_abs() <= (NF as u64) << 40);
|
||||
a
|
||||
};
|
||||
let cash = wide_amount();
|
||||
let moved: [i64; 3] = core::array::from_fn(|_| wide_amount());
|
||||
let a = kernel::flows_of(cash, moved);
|
||||
let b = kernel::flows_of(-cash, [-moved[0], -moved[1], -moved[2]]);
|
||||
assert_eq!(a.is_some(), b.is_some());
|
||||
if let (Some(a), Some(b)) = (a, b) {
|
||||
assert_eq!(b.cash, -a.cash);
|
||||
for k in 0..3 {
|
||||
|
||||
@@ -743,9 +743,14 @@ fn bookspec_source(id: &str) -> Source {
|
||||
fn decision(ex: &str, what: &str, kind: Option<&str>, reason: Option<&str>) -> Decision1099 {
|
||||
match what {
|
||||
"not_reportable" => {
|
||||
let kind = kind.and_then(ExemptReason::parse).unwrap_or_else(|| {
|
||||
panic!("{ex}: not_reportable needs a reason_kind, got {kind:?}")
|
||||
});
|
||||
// Exactly as the README spells it: the engine's parse forgives
|
||||
// case and spaces, an example doesn't.
|
||||
let kind = kind
|
||||
.and_then(ExemptReason::parse)
|
||||
.filter(|k| Some(k.key()) == kind)
|
||||
.unwrap_or_else(|| {
|
||||
panic!("{ex}: not_reportable needs a reason_kind, got {kind:?}")
|
||||
});
|
||||
let reason = reason.unwrap_or_else(|| panic!("{ex}: not_reportable needs a reason"));
|
||||
Decision1099::not_reportable(kind, reason).unwrap_or_else(|e| panic!("{ex}: {e}"))
|
||||
}
|
||||
@@ -1058,7 +1063,7 @@ impl<'a> Books<'a> {
|
||||
for p in &s.payees {
|
||||
let what = format!("{}: payee {}", ex.id, p.name);
|
||||
let form = Form1099::parse(&p.form)
|
||||
.filter(|_| p.form.starts_with("1099-"))
|
||||
.filter(|f| f.label() == p.form)
|
||||
.unwrap_or_else(|| panic!("{what}: form {:?}", p.form));
|
||||
// A payee in the directory with the vendor role is that
|
||||
// party's vendor record.
|
||||
@@ -1099,6 +1104,7 @@ impl<'a> Books<'a> {
|
||||
for i in &s.instruments {
|
||||
let what = format!("{} instrument {}", ex.id, i.label);
|
||||
let kind = InstrumentKind::parse(&i.kind)
|
||||
.filter(|k| k.key() == i.kind)
|
||||
.unwrap_or_else(|| panic!("{what}: kind {:?}", i.kind));
|
||||
let account = id(store.ledger(), &i.account);
|
||||
let owner = i.owner.as_ref().map(|o| {
|
||||
@@ -1959,7 +1965,11 @@ fn check_party_totals(b: &Books<'_>, x: &Expect) {
|
||||
let basis = x
|
||||
.basis
|
||||
.as_deref()
|
||||
.map(|k| Basis::parse(k).unwrap_or_else(|| panic!("{}: basis {k:?}", ex.id)))
|
||||
.map(|k| {
|
||||
Basis::parse(k)
|
||||
.filter(|b| b.key() == k)
|
||||
.unwrap_or_else(|| panic!("{}: basis {k:?}", ex.id))
|
||||
})
|
||||
.unwrap_or_else(|| panic!("{}: party_totals names its basis", ex.id));
|
||||
let party = expect_party(b, x);
|
||||
let a = ledger_core::parties::activity_with(
|
||||
@@ -2035,6 +2045,11 @@ fn check_k1(ex: &Example, l: &Ledger, members: &[MemberCapital], x: &Expect) {
|
||||
"{}: k1 needs the members and their shares",
|
||||
ex.id
|
||||
);
|
||||
assert!(
|
||||
!ex.setup.cash_accounts.is_empty(),
|
||||
"{}: k1 needs cash_accounts (what is money rather than property)",
|
||||
ex.id
|
||||
);
|
||||
let cash = codes(l, &ex.setup.cash_accounts, &ex.id);
|
||||
let rows = ledger_core::k1::k1_worksheet(l, from, to, members, &cash)
|
||||
.unwrap_or_else(|e| panic!("{}: {e}", ex.id));
|
||||
@@ -3427,6 +3442,37 @@ fn a_payment_method_not_in_the_setup_is_refused() {
|
||||
});
|
||||
}
|
||||
|
||||
/// A value from a closed list is spelled exactly as bookspec/README.md
|
||||
/// spells it: the engine's own parse forgives case and spaces, the
|
||||
/// loader doesn't (a spec has one spelling).
|
||||
#[test]
|
||||
#[should_panic(expected = "form \"1099-nec\"")]
|
||||
fn a_form_in_another_case_is_refused() {
|
||||
run_edited("1099-a-dated-status-applies-by-payment-date.toml", |t| {
|
||||
t.replacen("form = \"1099-NEC\"", "form = \"1099-nec\"", 1)
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic(expected = "kind \"Business_Card\"")]
|
||||
fn a_payment_method_kind_in_another_case_is_refused() {
|
||||
run_edited("1099-card-payments-are-excluded.toml", |t| {
|
||||
t.replacen("kind = \"business_card\"", "kind = \"Business_Card\"", 1)
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic(expected = "not_reportable needs a reason_kind")]
|
||||
fn a_reason_kind_in_another_case_is_refused() {
|
||||
run_edited("1099-a-dated-status-applies-by-payment-date.toml", |t| {
|
||||
t.replacen(
|
||||
"reason_kind = \"corporation\"",
|
||||
"reason_kind = \"Corporation\"",
|
||||
1,
|
||||
)
|
||||
});
|
||||
}
|
||||
|
||||
/// An event the example says the engine refuses must be refused: one it
|
||||
/// takes fails the example (March's run reversed on its own day is taken).
|
||||
#[test]
|
||||
|
||||
@@ -787,7 +787,7 @@ fn cash_flow_reconciles_to_change_in_cash() {
|
||||
|
||||
// Operating, by the direct method: collected 1,500 (class (a)); paid
|
||||
// 2,500 for equipment (not a registered asset: operating, rules.md
|
||||
// B-14's limit R4) and 800 rent (class (d)) = -1,800. Reconciled:
|
||||
// B-14 rule 12's limits) and 800 rent (class (d)) = -1,800. Reconciled:
|
||||
// 3,200 net income - 2,500 increase in AR (4,000 billed - 1,500
|
||||
// collected) - 2,500 increase in equipment = -1,800.
|
||||
// Financing: +10,000 owner contribution. Sum = 8,200. Reconciles.
|
||||
|
||||
@@ -596,7 +596,7 @@ mod p3 {
|
||||
proptest! {
|
||||
#![proptest_config(ProptestConfig::with_cases(500))]
|
||||
|
||||
/// B-15 INVARIANT 5 (defect 22): accumulated depreciation never falls,
|
||||
/// depreciation_schedule INVARIANT 5 (rules.md B-15; defect 22): accumulated depreciation never falls,
|
||||
/// never exceeds cost − salvage, is 0 before the in-service date and
|
||||
/// cost − salvage from the life's last day on; each schedule's lines
|
||||
/// sum to its accumulated and none is negative. Sampled through: every
|
||||
@@ -719,7 +719,7 @@ proptest! {
|
||||
}
|
||||
}
|
||||
|
||||
/// B-15 INVARIANT 6 (P3 depreciation brief 2.3, R1, R8): through every
|
||||
/// B-15 rule 4 (depreciation_schedule INVARIANT 6; P3 depreciation brief 2.3, R1, R8): through every
|
||||
/// month end of the life, the schedule is the pre-P3 running total
|
||||
/// (USD, a two-place currency), except where the exact cumulative is a
|
||||
/// half-cent tie, which the old 28-digit Decimal quotients could round
|
||||
|
||||
@@ -647,7 +647,7 @@ fn defect24_an_accrual_through_another_liability_counts_once() {
|
||||
assert_eq!(b.totals(c, Basis::All), (Decimal::ZERO, d("1200.00")));
|
||||
}
|
||||
|
||||
/// Defect 24 and B-35 item 1: a credit to what the company owes a party
|
||||
/// Defect 24 and B-35 item 6: a credit to what the company owes a party
|
||||
/// itself never counts as paying it. A fee on the issuer's own card is a
|
||||
/// bill from the issuer (counted when the card is paid); a distribution
|
||||
/// still owed to a member counts when it is paid; equipment bought on the
|
||||
|
||||
@@ -78,7 +78,7 @@ pub fn show_chart(ui: &mut Ui, b: &Books, shell: &mut Shell<'_>) {
|
||||
(AccountType::Revenue, "Income"),
|
||||
(AccountType::Expense, "Expenses"),
|
||||
] {
|
||||
let accounts: Vec<_> = b
|
||||
let mut accounts: Vec<_> = b
|
||||
.ledger
|
||||
.chart()
|
||||
.all()
|
||||
@@ -86,6 +86,8 @@ pub fn show_chart(ui: &mut Ui, b: &Books, shell: &mut Shell<'_>) {
|
||||
a.account_type == t && (shell.state.accounts.show_empty || used.contains(&a.id))
|
||||
})
|
||||
.collect();
|
||||
// In the chart's order: by code, as every report lists them.
|
||||
accounts.sort_by(|x, y| x.code.cmp(&y.code));
|
||||
if accounts.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -1881,6 +1881,26 @@ fn render(ui: &mut Ui, t: &Table, pdf: &PdfInfo, shell: &mut Shell<'_>) {
|
||||
Stroke::new(1.0, p.border),
|
||||
);
|
||||
for (ri, r) in t.rows.iter().enumerate() {
|
||||
// A note wraps over the table's width, never cut short:
|
||||
// it says how to read the figures above it.
|
||||
if r.kind == Kind::Note {
|
||||
let mut job = egui::text::LayoutJob::single_section(
|
||||
r.label.clone(),
|
||||
egui::TextFormat::simple(theme::regular(12.0), p.muted),
|
||||
);
|
||||
job.wrap.max_width = (width - 8.0).max(10.0);
|
||||
let galley = ui.painter().layout_job(job);
|
||||
let (rr, _) = ui.allocate_exact_size(
|
||||
vec2(width, galley.size().y + 14.0),
|
||||
Sense::hover(),
|
||||
);
|
||||
ui.painter().galley(
|
||||
egui::pos2(rr.left() + 4.0, rr.top() + 7.0),
|
||||
galley,
|
||||
p.muted,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
let h = match r.kind {
|
||||
Kind::Section => 36.0,
|
||||
Kind::Grand => 38.0,
|
||||
|
||||
@@ -221,7 +221,7 @@ pub struct YearEarnings {
|
||||
///
|
||||
/// ```text
|
||||
/// GOAL: the balance sheet as printed, footing and balancing.
|
||||
/// SOURCE: rules.md B-4/B-5 (assets = liabilities + equity); footing.
|
||||
/// SOURCE: rules.md B-7 (the accounting equation); footing.
|
||||
/// INVARIANTS:
|
||||
/// 1. Asset lines sum to "Total assets", liability lines to "Total
|
||||
/// liabilities", equity lines (accounts and the earnings lines) to the
|
||||
@@ -425,7 +425,7 @@ pub fn cash_flow(cf: &CashFlowStatement) -> Statement {
|
||||
///
|
||||
/// ```text
|
||||
/// GOAL: the trial balance as printed, footing.
|
||||
/// SOURCE: rules.md B-2 (debits equal credits); footing.
|
||||
/// SOURCE: rules.md B-6 (the trial balance balances; B-1); footing.
|
||||
/// INVARIANTS:
|
||||
/// 1. Each column's lines sum to its total; the totals are equal for
|
||||
/// balanced books.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# Prepaid expenses, deferred revenue and recurring entries
|
||||
|
||||
- Prepaid expenses and deferred revenue: record the original payment (with its receipt), then books.create_schedule; books.run_schedules posts the monthly amortization (preview first; exempt from the receipt policy like depreciation).
|
||||
- A new schedule spreads by the day from its start (the [books] schedule_convention setting, default daily): a policy starting mid-month posts a small first and last month, so 12 months from March 15 is 13 month ends. Equal months (monthly_even) start on the 1st, so revenue is never earned early. Schedules from before keep their equal months.
|
||||
- A new schedule spreads by the day from its start (the schedule_convention setting, default daily): a policy starting mid-month posts a small first and last month, so 12 months from March 15 is 13 month ends. Equal months (monthly_even) start on the 1st, so revenue is never earned early. Schedules from before keep their equal months.
|
||||
- Recurring entries (books.create_recurring, books.run_recurring) only ever create drafts: attach each actual receipt and post through the normal preview.
|
||||
@@ -3019,7 +3019,7 @@ impl BooksServer {
|
||||
#[tool(
|
||||
name = "books.add_account",
|
||||
title = "Add an account",
|
||||
description = "Add an account to the chart. Codes must be unique; check books.chart first. Give a revenue or expense account its tax_category (books.tax_categories): without one the result warns, or, under policy.require_tax_category = refuse, the account is refused. A name another account under the same parent has (ignoring capitals), one holding nine digits in a row, or more than one line of 200 characters, is refused.",
|
||||
description = "Add an account to the chart. Codes must be unique; check books.chart first. Give a revenue or expense account its tax_category (books.tax_categories): without one the result warns, or, under policy.require_tax_category = refuse, the account is refused. A name another account under the same parent has (ignoring capitals), one holding nine digits in a row, or one that isn't a single line of at most 200 characters, is refused.",
|
||||
annotations(
|
||||
read_only_hint = false,
|
||||
destructive_hint = false,
|
||||
@@ -3793,7 +3793,7 @@ impl BooksServer {
|
||||
#[tool(
|
||||
name = "books.withdraw_attachment",
|
||||
title = "Withdraw a receipt or document",
|
||||
description = "Withdraw an unlinked receipt or a filed document, with a reason recorded in the journal. It drops out of books.list_receipts and can never be linked again. Nothing is deleted: the bytes stay in the blob store and the journal keeps the filing and the withdrawal. Unlink a linked receipt first.",
|
||||
description = "Withdraw an unlinked receipt or a filed document, with a reason recorded in the journal. It drops out of books.list_receipts and can never be linked again. Nothing is deleted: the bytes stay in the blob store and the journal keeps the filing and the withdrawal. Unlink it first (books.unlink_receipt from its entry, books.unlink_attachment from anything else).",
|
||||
annotations(
|
||||
read_only_hint = false,
|
||||
destructive_hint = true,
|
||||
@@ -4752,7 +4752,7 @@ impl BooksServer {
|
||||
#[tool(
|
||||
name = "books.create_schedule",
|
||||
title = "Create an amortization schedule",
|
||||
description = "Spread a prepaid expense (an asset used up as time passes, e.g. a year of insurance paid up front) or deferred revenue (a liability earned as time passes, e.g. an annual subscription collected up front) over the time it covers, to the cent. convention: daily (default, by the day from start: a mid-month start posts a small first and last month, one month end more than its months) or monthly_even (equal months; starts on the 1st). Record the original payment first (Dr prepaid / Cr cash, with its receipt; or Dr cash / Cr deferred revenue). Returns the plan; post it with books.run_schedules.",
|
||||
description = "Spread a prepaid expense (an asset used up as time passes, e.g. a year of insurance paid up front) or deferred revenue (a liability earned as time passes, e.g. an annual subscription collected up front) over the time it covers, to the cent. convention: daily (by the day from start: a mid-month start posts a small first and last month, one month end more than its months) or monthly_even (equal months; starts on the 1st); omitted, the schedule_convention setting, else daily. Record the original payment first (Dr prepaid / Cr cash, with its receipt; or Dr cash / Cr deferred revenue). Returns the plan; post it with books.run_schedules.",
|
||||
annotations(
|
||||
read_only_hint = false,
|
||||
destructive_hint = false,
|
||||
@@ -5185,7 +5185,7 @@ impl BooksServer {
|
||||
#[tool(
|
||||
name = "books.set_entry_vendor",
|
||||
title = "Tag an entry's vendor",
|
||||
description = "Tag an entry (draft or posted) with the vendor it paid, for 1099 tracking, or omit vendor to clear the tag. Reporting metadata only: amounts never change. On an entry naming several parties (books.set_entry_party parties) the parties stay as they are: the vendor of a party already named is tagged with every share kept; any other vendor, or clearing the tag while its party is named, is refused (change the parties instead). An entry and its reversal are tagged together. books.pay_bill tags its payments automatically; books.draft_entry takes a vendor too.",
|
||||
description = "Tag an entry (draft or posted) with the vendor it paid, for 1099 tracking, or omit vendor to clear the tag. Reporting metadata only: amounts never change. On an entry naming one party or none, the vendor's party becomes its party; clearing also clears that party. On an entry naming several parties (books.set_entry_party parties) the parties stay as they are: the vendor of a party already named is tagged with every share kept; any other vendor, or clearing the tag while its party is named, is refused (change the parties instead). An entry and its reversal are tagged together. books.pay_bill tags its payments automatically; books.draft_entry takes a vendor too.",
|
||||
annotations(
|
||||
read_only_hint = false,
|
||||
destructive_hint = false,
|
||||
@@ -5450,7 +5450,7 @@ impl BooksServer {
|
||||
#[tool(
|
||||
name = "books.rename_account",
|
||||
title = "Rename an account",
|
||||
description = "Give an account a new name. Only the name changes: its id, code, type, tax category and every entry posted to it stay, so no entry, balance or statement amount changes; every report, past periods included, prints the new name, and the journal keeps the old one. Refuses a blank name, the name it has, a name another account under the same parent has (ignoring capitals), one holding nine digits in a row, or more than one line of 200 characters. QuickBooks matches accounts by name: if the CPA imported an earlier IIF, tell them. Never rename the human's accounts on your own: only when they ask.",
|
||||
description = "Give an account a new name. Only the name changes: its id, code, type, tax category and every entry posted to it stay, so no entry, balance or statement amount changes; every report, past periods included, prints the new name, and the journal keeps the old one. Refuses a blank name, the name it has, a name another account under the same parent has (ignoring capitals), one holding nine digits in a row, or one that isn't a single line of at most 200 characters. QuickBooks matches accounts by name: if the CPA imported an earlier IIF, tell them. Never rename the human's accounts on your own: only when they ask.",
|
||||
annotations(
|
||||
read_only_hint = false,
|
||||
destructive_hint = true,
|
||||
@@ -6078,7 +6078,7 @@ impl BooksServer {
|
||||
#[tool(
|
||||
name = "books.export",
|
||||
title = "Export a report",
|
||||
description = "Export a report. Reports: profit_and_loss, balance_sheet, cash_flow (direct method, with its reconciliation and noncash schedules; needs cash_accounts), trial_balance (as_of), members_equity (multi-member LLC; members from the settings), general_ledger (by account: running balances, splits), general_journal (each entry posted in the period; alias journal), fixed_asset_register (as_of: every asset with tag, serial, cost, depreciation, book value, reconciled to the books). Formats: pdf and xlsx (base64) for the five statements and fixed_asset_register; xlsx, csv, json for every report; iif for general_journal or journal (QuickBooks Desktop: plain ASCII text, CR LF; every posted entry from the first through as_of (start and end are refused), period closes left out, DOCNUM the entry id; names, numbers and memos fit QuickBooks' limits; warnings says what is written differently from the books, memos grouped, to tell the human). Dates: start/end for period reports (omitted: the fiscal year to date), as_of for balance_sheet, trial_balance, fixed_asset_register and iif (omitted: today).",
|
||||
description = "Export a report. Reports: profit_and_loss, balance_sheet, cash_flow (direct method, with its reconciliation and noncash schedules; needs cash_accounts), trial_balance (as_of), members_equity (multi-member LLC; members from the settings), tax_summary, general_ledger (by account: running balances, splits), general_journal (each entry posted in the period; alias journal), fixed_asset_register (as_of: every asset with tag, serial, cost, depreciation, book value, reconciled to the books). Formats: pdf and xlsx (base64) for the statements, tax_summary and fixed_asset_register; xlsx, csv, json for every report; iif for general_journal or journal (QuickBooks Desktop: plain ASCII text, CR LF; every posted entry from the first through as_of (start and end are refused), period closes left out, DOCNUM the entry id; names, numbers and memos fit QuickBooks' limits; warnings says what is written differently from the books, memos grouped, to tell the human). Dates: start/end for period reports (omitted: the fiscal year to date), as_of for balance_sheet, trial_balance, fixed_asset_register and iif (omitted: today).",
|
||||
annotations(read_only_hint = true, open_world_hint = false)
|
||||
)]
|
||||
async fn export(&self, params: Parameters<ExportParams>) -> Result<Json<ExportOut>, String> {
|
||||
@@ -11290,6 +11290,68 @@ mod tests {
|
||||
assert_eq!(over, OVER, "tool descriptions over 700 characters");
|
||||
}
|
||||
|
||||
/// The tools docs/mcp-interface.md documents: a line (or a bullet)
|
||||
/// that starts with `` `books.NAME` `` or `` `books.NAME { `` , and
|
||||
/// any `` `books.NAME { `` signature within a line.
|
||||
fn documented_tools(doc: &str) -> std::collections::BTreeSet<String> {
|
||||
let name_at = |s: &str| -> Option<(String, char)> {
|
||||
let rest = s.strip_prefix("`books.")?;
|
||||
let name: String = rest
|
||||
.chars()
|
||||
.take_while(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || *c == '_')
|
||||
.collect();
|
||||
let next = rest[name.len()..].chars().next()?;
|
||||
(!name.is_empty()).then(|| (format!("books.{name}"), next))
|
||||
};
|
||||
let mut out = std::collections::BTreeSet::new();
|
||||
for line in doc.lines() {
|
||||
let t = line.trim_start();
|
||||
let t = t.strip_prefix("- ").unwrap_or(t);
|
||||
if let Some((name, next)) = name_at(t)
|
||||
&& (next == '`' || next == ' ')
|
||||
{
|
||||
out.insert(name);
|
||||
}
|
||||
for (i, _) in line.match_indices("`books.") {
|
||||
if let Some((name, ' ')) = name_at(&line[i..])
|
||||
&& line[i + 1 + name.len()..].starts_with(" {")
|
||||
{
|
||||
out.insert(name);
|
||||
}
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// Every tool the server has is documented in docs/mcp-interface.md,
|
||||
/// and every tool the doc documents is one the server has.
|
||||
#[test]
|
||||
fn every_tool_is_documented_and_every_documented_tool_exists() {
|
||||
let doc = include_str!("../../../docs/mcp-interface.md");
|
||||
let documented = documented_tools(doc);
|
||||
let tools: std::collections::BTreeSet<String> = BooksServer::tool_router()
|
||||
.list_all()
|
||||
.iter()
|
||||
.map(|t| t.name.to_string())
|
||||
.collect();
|
||||
let undocumented: Vec<&String> = tools.difference(&documented).collect();
|
||||
let unknown: Vec<&String> = documented.difference(&tools).collect();
|
||||
assert!(
|
||||
undocumented.is_empty(),
|
||||
"tools docs/mcp-interface.md doesn't document: {undocumented:?}"
|
||||
);
|
||||
assert!(
|
||||
unknown.is_empty(),
|
||||
"docs/mcp-interface.md documents tools there aren't: {unknown:?}"
|
||||
);
|
||||
// The reader catches what it should, and only that.
|
||||
let sample = "- `books.chart` → x\n `books.decisions { a? }`; \
|
||||
`books.open_items { b? }`.\nsee `books.cash_accounts`, \
|
||||
`books.show`.";
|
||||
let got: Vec<String> = documented_tools(sample).into_iter().collect();
|
||||
assert_eq!(got, ["books.chart", "books.decisions", "books.open_items"]);
|
||||
}
|
||||
|
||||
/// rules.md B-30 (defect 16): every report tool says what its omitted
|
||||
/// dates mean in the one sentence the resolver's kind gives
|
||||
/// (PeriodKind::describe), and each date parameter in the schema's
|
||||
|
||||
@@ -127,7 +127,7 @@ pub struct CreateScheduleParams {
|
||||
/// "daily" (by the day from start, like depreciation: a mid-month
|
||||
/// start posts a small first and last month, one more month end than
|
||||
/// its months) or "monthly_even" (equal months, starting on the 1st).
|
||||
/// Default: the books' `[books] schedule_convention` setting, else
|
||||
/// Default: the books' `schedule_convention` setting, else
|
||||
/// daily.
|
||||
#[serde(default)]
|
||||
pub convention: Option<String>,
|
||||
@@ -588,7 +588,8 @@ pub struct Vendor1099LineOut {
|
||||
pub reportable: bool,
|
||||
/// The vendor's party.
|
||||
pub party_id: Option<u64>,
|
||||
/// The TIN's last four digits, from the party's W-9 facts.
|
||||
/// The TIN's last four digits: from the party's TIN facts
|
||||
/// (books.set_tin), else its W-9 facts.
|
||||
pub tin_last4: Option<String>,
|
||||
/// The full TIN, formatted (123-45-6789, 12-3456789): only with
|
||||
/// include_tins, and only when the vault holds it.
|
||||
@@ -775,8 +776,8 @@ pub struct ChartTemplateOut {
|
||||
/// Chart CSV (code,name,type,parent_code,tax_category) for
|
||||
/// `books.import_chart_csv`, after review.
|
||||
pub csv: String,
|
||||
/// The `wanderbooks.toml` settings that go with it: tax form, owners,
|
||||
/// where period closes go.
|
||||
/// The settings that go with it, as settings-file TOML for
|
||||
/// `books.import_settings`: tax form, owners, where period closes go.
|
||||
pub settings_toml: String,
|
||||
/// What to decide or know.
|
||||
pub notes: Vec<String>,
|
||||
@@ -800,8 +801,10 @@ pub struct RenumberAccountOut {
|
||||
pub previous_code: String,
|
||||
/// The code it has now.
|
||||
pub code: String,
|
||||
/// Settings in wanderbooks.toml that still name the old code: change
|
||||
/// each to the new one (the server reloads the file).
|
||||
/// Settings that still name the old code (as books.set_setting keys,
|
||||
/// or the settings file's labels for books whose settings are in a
|
||||
/// file), and conventions whose template or approval rule names it:
|
||||
/// change each (books.set_setting, books.set_convention).
|
||||
pub config_mentions: Vec<String>,
|
||||
}
|
||||
|
||||
@@ -1820,7 +1823,9 @@ pub struct ReceiptOut {
|
||||
pub sha256: String,
|
||||
/// Linked entry id, if any.
|
||||
pub entry_id: Option<u64>,
|
||||
/// Category: receipts, formation, legal, tax, accounting, other.
|
||||
/// Category: receipts (books.attach_receipt), or a document's:
|
||||
/// formation, legal, tax, accounting, statement, contract, photo,
|
||||
/// warranty, import_source, other.
|
||||
pub category: String,
|
||||
/// Original file name.
|
||||
pub file_name: String,
|
||||
@@ -2125,12 +2130,13 @@ pub struct StaleRule {
|
||||
#[derive(Debug, Deserialize, JsonSchema)]
|
||||
pub struct ExportParams {
|
||||
/// "csv", "json", "xlsx", "pdf", or "iif". XLSX and PDF are
|
||||
/// base64-encoded; PDF covers profit_and_loss, balance_sheet, and
|
||||
/// cash_flow; IIF (plain ASCII text) is only valid with report
|
||||
/// base64-encoded; PDF covers every report but general_ledger and
|
||||
/// general_journal; IIF (plain ASCII text) is only valid with report
|
||||
/// "general_journal" (alias "journal").
|
||||
pub format: String,
|
||||
/// "journal", "trial_balance", "profit_and_loss", "balance_sheet", or
|
||||
/// "cash_flow".
|
||||
/// "general_journal" (alias "journal"), "general_ledger",
|
||||
/// "trial_balance", "profit_and_loss", "balance_sheet", "cash_flow",
|
||||
/// "members_equity", "tax_summary", or "fixed_asset_register".
|
||||
pub report: String,
|
||||
/// Period start for the period reports (see [`export_start_doc`]).
|
||||
#[schemars(description = export_start_doc())]
|
||||
@@ -3406,7 +3412,8 @@ pub struct AddPartyParams {
|
||||
/// Parameters for `books.update_party`. Give at least one field to change.
|
||||
/// Every field given is checked before any is written: a bad one changes
|
||||
/// nothing. Each is journaled as its own `party.updated`, in this order:
|
||||
/// name, display_name, website, address, notes, archived.
|
||||
/// name, display_name, website, address, notes, archived; then one
|
||||
/// `party.creditor_set` per `creditor_of` account added or dropped.
|
||||
#[derive(Debug, Clone, Deserialize, JsonSchema)]
|
||||
pub struct UpdatePartyParams {
|
||||
/// The party: id or name.
|
||||
|
||||
@@ -1303,7 +1303,7 @@ fn acquired_date_comes_from_the_acquisition_entry() {
|
||||
);
|
||||
}
|
||||
|
||||
// ------------------------------- posted and scheduled (rules.md B-24)
|
||||
// ------------------------------- posted and scheduled (rules.md B-15 rule 8)
|
||||
|
||||
/// Named regression 1 (the brief's reproduction 1, its figures made up): a
|
||||
/// $1,440.00 laptop, 36 months, in service 2026-05-15, nothing run. As of
|
||||
|
||||
@@ -1175,7 +1175,7 @@ fn reversing_a_linked_acquisition_still_opens_and_verifies() {
|
||||
Store::open(&dir).unwrap().verify().unwrap();
|
||||
}
|
||||
|
||||
// ------------------------------------------------- depreciation runs (rules.md B-24)
|
||||
// ------------------------------------------------- depreciation runs (rules.md B-15 rules 6 and 8)
|
||||
|
||||
/// A run is the books' record of what they carry for an asset
|
||||
/// (Ledger::accumulated_posted): recorded live, replayed from the journal,
|
||||
|
||||
@@ -3417,7 +3417,7 @@ mod p3_properties {
|
||||
})]
|
||||
|
||||
/// rules.md B-31 (the engine-side counterpart to the Lean note,
|
||||
/// docs/verification.md 13a): on books with postings, any sequence
|
||||
/// docs/verification.md law 15): on books with postings, any sequence
|
||||
/// of renames changes no figure (the ledger's statements, without
|
||||
/// names), no entry and no earlier journal line; live equals
|
||||
/// replay; the projection equals a fresh one. The sets picked by
|
||||
|
||||
+185
-82
@@ -39,9 +39,11 @@ the books), `.formation_state`, `.formed`, `.state_entity_number`, `.ein`
|
||||
(`books.accounting_basis`, `.cash_accounts`, `.financing_accounts` (loans,
|
||||
and money the owners advanced: ASC 230-10-45-14(b)), `.investing_accounts`
|
||||
(ASC 230-10-20), `.close_into` (single owner only), `.close_sweep`,
|
||||
`.asset_tag_prefix`); and policy (`policy.evidence.threshold` (receipts
|
||||
`.asset_tag_prefix`, `.schedule_convention` (daily, the default, or
|
||||
monthly_even)); backups (`backup.git_remote`, `backup.folder`); and policy (`policy.evidence.threshold` (receipts
|
||||
strictly above it; "0.00": every entry), `.required_accounts`,
|
||||
`.exempt_accounts`, `policy.parties` (off, suggest, require),
|
||||
`.exempt_accounts`, `policy.parties` (off, suggest, require), `policy.require_tax_category`
|
||||
(warn, the default, or refuse),
|
||||
`policy.approval_rules` (off, the default, or enforce)). A value is checked
|
||||
by its key and kept in one spelling (a time zone as the database spells it,
|
||||
an amount without leading zeros, a return as `schedule_c`, `1065`, `1120s`
|
||||
@@ -129,7 +131,7 @@ settings-and-conventions section says how to use them.
|
||||
state ("needs_onboarding" or "active"), settings_source, rules: {
|
||||
evidence, parties, approval_rules }, settings_summary: [{ key, value,
|
||||
set_by, seq }], conventions: [{ id, kind, title, summary }], open_items:
|
||||
[{ id, title, due }], truncated, handbook_sections, format_upgrade?: {
|
||||
[{ id, title, due }], truncated?, handbook_sections, format_upgrade?: {
|
||||
guard, format, adds, how }, next_steps: [{ tool, args?, why }],
|
||||
isolation }`. The connect text names it as the first call.
|
||||
`format_upgrade` is there only for books waiting for a format upgrade
|
||||
@@ -139,8 +141,16 @@ settings-and-conventions section says how to use them.
|
||||
`isolation` is `same_user (advisory)` (D-32): the books are hidden from
|
||||
agents in the owner's own account, not locked against them. Lists cut
|
||||
to fit say so in `truncated` ("conventions: 4 more (books.conventions)").
|
||||
`focus` adds steps: `onboarding` the onboarding step, `close` the
|
||||
company's close steps, `audit` `books.verify`. Not built yet (P5):
|
||||
`next_steps`, in order: the format upgrade's (when waiting);
|
||||
`books.import_settings` (books that still read a settings file); the
|
||||
onboarding step (`books.handbook {section: "contract"}`, when the chart
|
||||
is empty or `focus` is `onboarding`); `books.open_items` (an item due
|
||||
within 30 days or overdue); `books.backup` (none set up, or one stale,
|
||||
failing, paused or failing its drill); the focus's step (`close`:
|
||||
`books.conventions {kind: "close_step"}`; `audit`: `books.verify`); then
|
||||
always `books.handbook {section: "contract"}` and `books.digest`.
|
||||
`focus` is `daily` (the default), `onboarding`, `close` or `audit`;
|
||||
anything else is refused. Not built yet (P5):
|
||||
`book.id`, `book.url`, `head.last_handoff`, the `agent` input, and
|
||||
loading it up front under tool search. Also `books://start`.
|
||||
**The format upgrade** (plan 3.9, D-39, rules.md B-27). Books an older
|
||||
@@ -304,9 +314,8 @@ where there is one; under "off" nothing); corrections by reversal; the receipt r
|
||||
never rounded, the accounts that always need a receipt and those exempt,
|
||||
and that reversals need none); the exemptions (depreciation and
|
||||
amortization, whose schedules are the evidence, and closes, derived from
|
||||
the books); that `books.create_invoice`, `books.create_bill`,
|
||||
`books.receive_payment`, `books.pay_bill`, `books.run_payroll` and
|
||||
`books.remit_payroll_taxes` post at once without a receipt check; TINs
|
||||
the books); that the invoice, bill, payment and payroll tools post at
|
||||
once, unchecked (in those words, naming no tool); TINs
|
||||
only into the vault; show the numbers, and after posting check
|
||||
`books.digest` and offer it to the human; and where settings live: in the
|
||||
books, changed only through the tools, or, for books that still read a
|
||||
@@ -336,8 +345,8 @@ The long guidance is the **handbook**, compiled in from
|
||||
contract, entries, drafts-and-reversals, evidence-and-receipts,
|
||||
attachments, files-in-out, reconciliation, fixed-assets,
|
||||
prepaid-and-recurring, revenue-and-payouts, parties-and-tins, 1099,
|
||||
cpa-handoff, settings-and-conventions, viewer, rules-that-change,
|
||||
control-roles, tracing-figures.
|
||||
cpa-handoff, settings-and-conventions, viewer, backups, format-upgrade,
|
||||
rules-that-change, control-roles, tracing-figures.
|
||||
- The same as resources: `books://handbook` (the index, JSON) and
|
||||
`books://handbook/{section}` (Markdown, `text/markdown`), one listed per
|
||||
section.
|
||||
@@ -370,10 +379,12 @@ that changes anything also has a `destructiveHint`, true for these 29:
|
||||
`books.set_tin` (both overwrite a vault record), `books.purge_tin`,
|
||||
`books.create_invoice`, `books.create_bill`, `books.receive_payment`,
|
||||
`books.pay_bill`, `books.run_payroll`, `books.remit_payroll_taxes`; and an
|
||||
`idempotentHint` on the seven that a repeat with the same arguments leaves
|
||||
`idempotentHint` on the ten that a repeat with the same arguments leaves
|
||||
the journal and the vault as they were: `books.verify`,
|
||||
`books.run_depreciation`, `books.run_schedules`, `books.run_recurring`,
|
||||
`books.set_party_roles`, `books.set_setting` and `books.import_settings` (a setter such as `books.update_party`,
|
||||
`books.set_party_roles`, `books.set_setting`, `books.import_settings`,
|
||||
`books.set_vendor_1099`, `books.set_entry_instrument` and
|
||||
`books.back_up_now` (a setter such as `books.update_party`,
|
||||
`books.set_fx_rate` or `books.set_tin` journals a line, and `books.set_tin`
|
||||
seals a new vault record, every time, so none carries it;
|
||||
`idempotent_tools_change_nothing_when_repeated` calls each candidate twice
|
||||
@@ -434,8 +445,9 @@ every tool over every book in ledger-store's `tests/fixtures/old-books`.
|
||||
## Tools — books
|
||||
|
||||
- `books.handbook { section? }`: the guide (see "Server instructions").
|
||||
- `books.chart` → `{ accounts: [{ id, code, name, type, parent, placeholder,
|
||||
balance }] }`. Balances included: one call answers "what do we have."
|
||||
- `books.chart` → `{ accounts: [{ id, code, name, account_type, parent,
|
||||
placeholder, balance, tax_category, deductible_percent }], base_currency
|
||||
}`. Balances included: one call answers "what do we have."
|
||||
- `books.add_account { code, name, account_type, parent?, placeholder?,
|
||||
tax_category?, deductible_percent? }` → `{ account_id, warnings? }`.
|
||||
Codes are unique; a name another account under the same parent has
|
||||
@@ -459,11 +471,13 @@ every tool over every book in ledger-store's `tests/fixtures/old-books`.
|
||||
- `books.add_line { entry_id, account: "1000" | <id>, side: "debit" | "credit",
|
||||
amount: "123.45" }` → `{ ok: true }`. A refused line (unknown account or
|
||||
class, bad amount) adds nothing.
|
||||
- `books.post_entry { entry_id, confirmed }` → `{ status, entry_id, date,
|
||||
- `books.post_entry { entry_id, confirmed?, expected_total? }` → `{ status, entry_id, date,
|
||||
description, total, lines (with resulting balances), receipts,
|
||||
receipt_required, receipt_missing, split_detected, party_policy,
|
||||
party_id?, party?, parties, parties_error?, no_party_reason?,
|
||||
party_missing, party_suggestions }`. Without `confirmed: true`, `status`
|
||||
party_missing, party_suggestions, approvals_missing? }`. `expected_total`,
|
||||
when given, must be the entry's total debits, else nothing posts.
|
||||
Without `confirmed: true`, `status`
|
||||
is `"preview"` and nothing changes; with it, the receipt policy is
|
||||
enforced and `status` is `"posted"`. `parties` lists every party the
|
||||
entry names with its share resolved to the cent (`{ party_id, party,
|
||||
@@ -478,7 +492,9 @@ every tool over every book in ledger-store's `tests/fixtures/old-books`.
|
||||
and a reversal whose reversal would post a depreciation period or a
|
||||
schedule month a second time (naming the entry that posts it now), or
|
||||
re-instate a run a later run caught up (carrying the books past the
|
||||
schedule).
|
||||
schedule); and a reversal of a depreciation run or a schedule month's
|
||||
posting (or of a reversal in its chain) dated before that entry's own
|
||||
date (a correction comes after what it corrects, B-9).
|
||||
- `books.get_entry { entry_id }` → full entry with lines, receipts, and
|
||||
its `vendor`, `party_id`, and `party` (the party's label, when it names
|
||||
exactly one) tags, `parties` / `parties_error` as in the `post_entry`
|
||||
@@ -487,8 +503,11 @@ every tool over every book in ledger-store's `tests/fixtures/old-books`.
|
||||
why, for a draft an earlier version journaled whose lines exact
|
||||
arithmetic can't sum; a draft line on an account the chart doesn't have
|
||||
(older books hold such drafts) shows an empty code and "(no account N)".
|
||||
- `books.search_entries { account?, date_from?, date_to?, text?, limit?, offset? }`
|
||||
→ entries with lines, newest first. `limit` caps at 100.
|
||||
- `books.search_entries { account_id?, date_from?, date_to?, text?, limit?,
|
||||
offset? }` → `{ entries: [{ entry_id, date, description, total,
|
||||
reversal_of, receipt_count }] }`, posted entries, newest first (no
|
||||
lines: `books.get_entry` has them). `limit` defaults to 50 and caps at
|
||||
500.
|
||||
- **Omitted dates (rules.md B-30).** One rule everywhere
|
||||
(`ledger_core::calendar::PeriodDefaults`), stated in every report
|
||||
tool's description and date parameters in the same words: a period
|
||||
@@ -563,11 +582,13 @@ every tool over every book in ledger-store's `tests/fixtures/old-books`.
|
||||
- Changed in P3 (breaking): `net_income`, `depreciation` and
|
||||
`working_capital` are gone (net income is in `operating_reconciliation`).
|
||||
- `books.digest { since: "2026-10-01" }`
|
||||
→ `{ entries_posted: [...], totals_by_account: [...], stale_rules: [...] }`.
|
||||
→ `{ since, entries: [...], totals_by_account: [...], stale_rules: [...],
|
||||
journal_repairs: [...], settings_problems: [...], unreconciled_accounts:
|
||||
[...], uncategorized_accounts: [...] }`.
|
||||
The human review surface: "what did the AI do with my books lately." The
|
||||
digest is the answer to "trust, but verify." `stale_rules` lists legal
|
||||
amounts wanderbooks may no longer know as of today (`{ name,
|
||||
known_through, catalog, source, why }`): research the current value or
|
||||
known_through, source, why }`): research the current value or
|
||||
ask the human, then update it upstream (pull request or issue at
|
||||
github.com/swaits/wanderbooks). `unreconciled_accounts` lists the cash
|
||||
and card accounts due for reconciliation (`{ code, name,
|
||||
@@ -606,23 +627,29 @@ life's last day is 9999-12-31 at the latest.
|
||||
day it is ready and available for its use) starts depreciation (P3,
|
||||
defect 13; B-15); `purchase_date` is its alias for one release (both
|
||||
given and meaning different days: refused; the alias noted in `notes`). The
|
||||
`acquisition_entry` (posted, debiting the cost account, dated on or
|
||||
before the in-service date) gives `acquired_date`, which puts the asset
|
||||
`acquisition_entry` (posted, debiting the cost account, neither a
|
||||
reversal nor reversed, dated on or before the in-service date) gives
|
||||
`acquired_date`, which puts the asset
|
||||
on the register; with none, the acquired date is the in-service date.
|
||||
The basis facts are `books.set_asset_tax_basis`'s (`tax_basis_rule` and
|
||||
`contributor_basis`, with the old rule names, are aliases for one
|
||||
release); a bad one registers nothing.
|
||||
- `books.label_asset { asset, tag?, serial?, clear_serial? }` → `{ asset_id,
|
||||
tag, serial }`: set or change the tag (unique ignoring case, never reused
|
||||
by another asset even after a retag or disposal) or the manufacturer's
|
||||
serial. `asset` is the id or current tag. Journaled as `asset.tagged`,
|
||||
release); a bad one registers nothing, and so do basis facts given
|
||||
without `basis_rule`.
|
||||
- `books.label_asset { asset, tag?, serial?, clear_serial?, description?,
|
||||
acquisition_entry? }` → `{ asset_id, tag, serial, acquisition_entry,
|
||||
description }`: set or change the tag (unique ignoring case, never reused
|
||||
by another asset even after a retag or disposal), the manufacturer's
|
||||
serial, the description, or the linked acquisition entry (posted,
|
||||
debiting the cost account, not reversed). Refused with nothing to
|
||||
change, or with both `serial` and `clear_serial`. `asset` is the id or current tag. Journaled as `asset.tagged`,
|
||||
`asset.serial_set`.
|
||||
- `books.asset_labels { assets?, skip? }` → `{ file_name, content_base64,
|
||||
tags }`: an Avery 5160/8160 label sheet (PDF): company, tag, QR code of
|
||||
the tag, description, serial. Default: every active tagged asset; `skip`
|
||||
reuses a partly used sheet.
|
||||
- `books.asset_schedule { asset_id | tag, through? }`
|
||||
→ `{ status, schedule: [{ period, amount, accumulated, book_value }] }`;
|
||||
→ `{ asset_id, description, cost, salvage_value, useful_life_months,
|
||||
status, schedule: [{ period, amount, accumulated, book_value }] }`;
|
||||
a disposed asset's ends on its disposal day. A schedule an earlier
|
||||
version's asset can't compute is a tool error saying why, never a
|
||||
panic.
|
||||
@@ -678,8 +705,9 @@ life's last day is 9999-12-31 at the latest.
|
||||
in_service_date?, reason }` → `{ asset_id, tag, cost, salvage_value,
|
||||
useful_life_months, in_service_date, acquired_date, basis, notes? }`.
|
||||
Refused while the books carry depreciation for it (a run posted and not
|
||||
reversed), and for an in-service date before its acquisition entry's
|
||||
date; `in_service` is the alias of `in_service_date` for one release.
|
||||
reversed), for a disposed asset, for a blank `reason`, and for an
|
||||
in-service date before its acquisition entry's date; `in_service` is the
|
||||
alias of `in_service_date` for one release.
|
||||
A recorded value at contribution, and so the built-in gain, never
|
||||
moves; a basis recorded before P3 has none and reads it as the book
|
||||
cost, so a cost correction moves its built-in gain, and `notes` says so.
|
||||
@@ -701,7 +729,8 @@ life's last day is 9999-12-31 at the latest.
|
||||
- `books.dispose_asset { asset_id | tag, date, proceeds, proceeds_account,
|
||||
gain_loss_account, buyer?, confirmed?, receipt_attachment_ids? }` (the
|
||||
date never before the acquired date) →
|
||||
without confirmation: `{ status: "preview", accumulated, book_value,
|
||||
without confirmation: `{ status: "preview", asset_id, entry_id (0 in a
|
||||
preview), accumulated, book_value,
|
||||
proceeds, gain_loss, final_depreciation, excess_depreciation,
|
||||
catch_up_depreciation, lines, receipt_required }` and posts nothing;
|
||||
with `confirmed: true`: auto-posts the final depreciation (through the
|
||||
@@ -774,8 +803,10 @@ through the policy.
|
||||
the next month end to post (null once every month has). Refused: a
|
||||
convention but those two (naming them); a `monthly_even` schedule not
|
||||
starting on the 1st (naming the 1st, and `daily`); one whose months
|
||||
aren't whole minor units (yen); a daily one whose last day is after
|
||||
9999-12-31. `plan_error` is null, except for a schedule an earlier
|
||||
aren't whole minor units (yen); `months` outside 1 to 600; one whose
|
||||
last day is after 9999-12-31; and, in books an older version wrote, a
|
||||
daily one (it needs the journal feature `schedule_convention`:
|
||||
`format_upgrade_pending` until the owner's format upgrade). `plan_error` is null, except for a schedule an earlier
|
||||
version accepted whose months run past the last date the books can
|
||||
compute: then it says why, `remaining` and `next` are null and `months`
|
||||
empty, and `books.run_schedules` refuses until it is dealt with.
|
||||
@@ -796,7 +827,10 @@ through the policy.
|
||||
month-end start stays on month ends (Jan 31, Feb 28, Mar 31).
|
||||
- `books.list_recurring {}` → `{ recurring: [...] }`.
|
||||
- `books.run_recurring { through? }` → `{ drafts: [{ recurring_id, date,
|
||||
entry_id }] }`: one draft per occurrence on or before `through`, once.
|
||||
entry_id }], skipped_closed_period: [{ recurring_id, date }] }`: one
|
||||
draft per occurrence on or before `through`, once; an occurrence dated
|
||||
in a closed period is not generated and is listed in
|
||||
`skipped_closed_period` for the human to decide.
|
||||
|
||||
## Tools — the viewer
|
||||
|
||||
@@ -850,7 +884,8 @@ through the policy.
|
||||
- `books.verify { blobs? }` → `{ status: "ok" | "healed", events, head,
|
||||
healed?, blobs_verified?, database, other_databases?, checked }`
|
||||
(`database`: the projection file verified, one for each schema,
|
||||
`wanderbooks.v{N}.db`; `other_databases`: other schemas' files beside it,
|
||||
`wanderbooks.v{N}.db`, or the older `wanderbooks.db` when it holds this
|
||||
version's schema; `other_databases`: other schemas' files beside it,
|
||||
left for the versions that use them). Verifies the journal's hash chain,
|
||||
replays it against the live ledger, and compares every table of the
|
||||
SQLite database row for row against a fresh projection of the journal.
|
||||
@@ -859,6 +894,22 @@ through the policy.
|
||||
every stored file. The server runs the same check, and heals the same
|
||||
way, every time it starts.
|
||||
|
||||
## Tools — access
|
||||
|
||||
- `books.access_log { limit?, before?, token?, action?, outcome? }`
|
||||
(books:admin; read-only) → `{ records, total, more, head: { seq, hash }
|
||||
}`: who reached these books through the server (plan I-9), newest
|
||||
first, each record its token, client, tool or resource, a digest of
|
||||
its arguments and whether it ran (`ok`), failed (`error`) or was
|
||||
refused. `limit` is 1 to 500 (default 50); `before` pages back (records
|
||||
before that seq); `token` narrows to a token's name, `action` to `tool`,
|
||||
`resource` or `connect` (refused at the door), `outcome` to `ok`,
|
||||
`error` or `refused` (anything else is refused). `total` counts the
|
||||
whole log; `more` says older records match too. The log is read whole
|
||||
and its chain checked first: a damaged log is reported, never shown in
|
||||
part. Books served over stdio keep no access log: the tool is not
|
||||
offered there (and is refused if called).
|
||||
|
||||
## Tools — backups
|
||||
|
||||
Books served from the data root back themselves up (plan 3.7, D-30,
|
||||
@@ -1005,9 +1056,13 @@ first entry's).
|
||||
incorporates is a new vendor. A dated decision with nothing decided
|
||||
before it is refused; one the same as the decision before its date
|
||||
takes out any decision recorded for that date ("removed"); recorded
|
||||
again exactly so, nothing is journaled ("unchanged"). Without
|
||||
`form_1099`, `tin_on_file` alone marks the W-9, keeping the decision and
|
||||
who made it; `tin_on_file` omitted keeps the flag. `warnings` names each
|
||||
again exactly so (who decided and when included), nothing is journaled
|
||||
but a changed W-9 flag ("unchanged"). Without `form_1099`, `tin_on_file`
|
||||
alone marks the W-9, keeping the decision and who made it (`changed` is
|
||||
then "unchanged", with no warnings); `reason_kind`, `reason` or
|
||||
`effective` without `form_1099` are refused, and so is a call with
|
||||
neither `form_1099` nor `tin_on_file`; `tin_on_file` omitted keeps the
|
||||
flag. `warnings` names each
|
||||
year (and its entries) whose payments the change reaches in a closed
|
||||
period or after that year's 1099s were due (January 31 of the next
|
||||
year): tell the human, a filed 1099 may need correcting. Journaled as
|
||||
@@ -1035,7 +1090,10 @@ first entry's).
|
||||
`entry.instrument_set`.
|
||||
- `books.set_entry_vendor { entry_id, vendor? }` → the entry (as
|
||||
`books.get_entry`, which shows `vendor`, and `instrument` and
|
||||
`instrument_id`). Omitting `vendor` clears the tag. Metadata only:
|
||||
`instrument_id`). Omitting `vendor` clears the tag. On an entry naming
|
||||
no party or one, the vendor's party becomes its party (clearing a
|
||||
no-party mark), and clearing the tag clears the party too when it was
|
||||
the vendor's. Metadata only:
|
||||
posted entries may be tagged. `books.draft_entry` takes `vendor` and
|
||||
`instrument` too; bills and their payments are tagged automatically.
|
||||
`books.set_entry_party` tags the vendor too when the party has a vendor
|
||||
@@ -1155,9 +1213,9 @@ texts are at most 500 characters, no control characters.
|
||||
kept), so `books.vendor_1099`'s `missing_w9` follows.
|
||||
`tin` and `tin_type` put the full TIN in the TIN vault, and the signed
|
||||
form (`w9_upload_id`, or `w9_content_base64` with `w9_sha256`, as in
|
||||
"Tools — receipts & documents") goes there too (below); with them
|
||||
`tin_last4` comes from the TIN (a given one must agree) and `kept_at`
|
||||
defaults to `"wanderbooks vault"`.
|
||||
"Tools — receipts & documents") goes there too (below); with a TIN,
|
||||
`tin_last4` comes from it (a given one must agree); with a TIN or the
|
||||
form, `kept_at` defaults to `"wanderbooks vault"`.
|
||||
- `books.set_w4 { party, received_on, state, kept_at }` or `{ party, clear:
|
||||
true }` → the party. Journaled as `party.w4_set`.
|
||||
- `books.set_entry_party { entry_id, party?, parties?, no_party_reason? }`
|
||||
@@ -1174,7 +1232,11 @@ texts are at most 500 characters, no control characters.
|
||||
place of `party`, names every counterparty with its share; `[]` clears.
|
||||
Journaled as `entry.parties_set { entry_id, parties: [{ party_id, share
|
||||
}] }`, `share` being `{"amount": "30.00"}`, `{"percent": "60"}`, or
|
||||
`"derived"`. Each share, of what the entry moved (its `entry_flow` net;
|
||||
`"derived"`. Once the entry is posted (at `post_entry`, or tagging a
|
||||
posted entry), a derived split among several parties is journaled again
|
||||
as the exact amounts it resolves to (a second `entry.parties_set`), so a
|
||||
later change to `[[company.members]]` never re-splits posted history; it
|
||||
stays derived when the shares don't resolve or a part is zero. Each share, of what the entry moved (its `entry_flow` net;
|
||||
for an entry moving no money, like property contributed, its total):
|
||||
- `amount`: exactly that, positive, in cents; the amounts must sum to
|
||||
what it moved.
|
||||
@@ -1188,8 +1250,9 @@ texts are at most 500 characters, no control characters.
|
||||
`[[company.members]]` shares, rescaled to the members named, rounded as
|
||||
percents are. A derived share for a non-member among several is
|
||||
refused.
|
||||
Every party gives the same kind of share. Refused, journaling nothing: a
|
||||
party named twice, an unknown party, mixed kinds, percents not summing
|
||||
Every party gives the same kind of share. Refused, journaling nothing:
|
||||
`party` with `parties`, or either with `no_party_reason`; a share giving
|
||||
both `amount` and `percent`; a party named twice, an unknown party, mixed kinds, percents not summing
|
||||
to 100, an amount finer than a cent, two parties with vendor records (a
|
||||
1099 counts a payment toward one vendor; the one vendor party's record
|
||||
is tagged and counts the whole payment), and, once the entry has
|
||||
@@ -1272,7 +1335,7 @@ rules.md B-35):
|
||||
cash_accounts` (every asset account but receivables when none are set),
|
||||
the cards (the business-card payment methods' liability accounts,
|
||||
`books.instruments`; while the books record none, the liability
|
||||
accounts whose names say "card", not "gift card"; never a member's
|
||||
accounts whose names say "card" and not "gift"; never a member's
|
||||
card or a platform's account: the one rule the reconciliation reminders
|
||||
and the viewer read), and the `[books]
|
||||
financing_accounts` (what the members advanced, loans). Any other
|
||||
@@ -1495,10 +1558,11 @@ and no control or invisible (Unicode format) characters; a MIME type
|
||||
the upload's; inline they are required. The upload is taken only after
|
||||
the entry and every `link_to` subject check out. `link_to` as in "Tools
|
||||
— attachments".
|
||||
- `books.link_receipt { receipt_id, entry_id }` → link a previously filed
|
||||
- `books.link_receipt { attachment_id, entry_id }` → `{ ok }`. Links a previously filed
|
||||
(unlinked, not withdrawn) receipt to its entry. A receipt linked elsewhere
|
||||
must be unlinked first; one already linked to that entry
|
||||
(`books.link_attachment`) is refused, as filing it there twice.
|
||||
Journaled as `receipt.linked`.
|
||||
- `books.unlink_receipt { attachment_id, reason }` → `{ ok, entry_id,
|
||||
remaining_receipts }`. Detaches a receipt from its entry; it stays filed,
|
||||
unlinked. Journaled as `receipt.unlinked` with the reason. Subject to the
|
||||
@@ -1506,7 +1570,9 @@ and no control or invisible (Unicode format) characters; a MIME type
|
||||
(`receipt_required` error, remedy `books.attach_receipt`), so replacing a
|
||||
receipt is attach-new, then unlink-old.
|
||||
- `books.withdraw_attachment { attachment_id, reason }` → `{ ok }`. Retires
|
||||
an unlinked receipt or a filed document: hidden from listings, never
|
||||
a receipt or document that is neither filed on an entry nor linked to
|
||||
any subject (take those off first: `books.unlink_receipt`,
|
||||
`books.unlink_attachment`); the reason must not be blank. Hidden from listings, never
|
||||
linkable again. Journaled as `attachment.withdrawn`; the bytes stay in the
|
||||
blob store. Nothing in the evidence trail is ever erased — receipts change
|
||||
by new events, the same way entries change by reversals.
|
||||
@@ -1516,7 +1582,8 @@ and no control or invisible (Unicode format) characters; a MIME type
|
||||
are omitted unless `include_withdrawn` is true. `entry_id` lists the
|
||||
files filed on the entry and linked to it.
|
||||
- `books.get_receipt { attachment_id, view?: "meta" | "bytes" }` → `{
|
||||
metadata, description?, links, content_base64?, download? }`. `meta`
|
||||
attachment_id, sha256, entry_id?, category, file_name, mime_type,
|
||||
size_bytes, description?, links, content_base64?, download? }`. `meta`
|
||||
(the default) returns no bytes. `bytes` adds `content_base64`
|
||||
(hash-verified) for a file of at most 256 KiB; a larger one gives
|
||||
`download`, the command `wanderbooks download --book B <id> --to <a
|
||||
@@ -1540,11 +1607,14 @@ engine stays policy-free (it knows nothing of receipts) — enforcement lives
|
||||
in the MCP layer, which reads the `attachments` table before confirming a
|
||||
posting.
|
||||
|
||||
- **Policy config** (server config file, not per-call): `require_receipts_above`
|
||||
(default `"25.00"` in base currency), plus `receipt_required_accounts`
|
||||
(account codes that always require receipts, e.g. meals and travel) and
|
||||
`receipt_exempt_accounts` (e.g. automated bank fees). Human-edited and
|
||||
versioned with the deployment.
|
||||
- **Policy** (the books' settings, "Company settings"; not per-call):
|
||||
`policy.evidence.threshold` (receipts strictly above it; unset
|
||||
`"25.00"`; `"0.00"`: every entry), `policy.evidence.required_accounts`
|
||||
(account codes whose entries always need one, e.g. meals and travel)
|
||||
and `policy.evidence.exempt_accounts` (e.g. automated bank fees; an
|
||||
entry is exempt only when every line is in one). Changed with
|
||||
`books.set_setting`; books from before P1 read `config.json`'s
|
||||
`receipt_policy`.
|
||||
- **Preview.** Every `post_entry` preview includes the receipt checklist
|
||||
(`receipts: [...]`, `receipt_required: bool`, `receipt_missing: bool`,
|
||||
`split_detected: [...]`), so the human sees it before confirming. A
|
||||
@@ -1560,6 +1630,12 @@ posting.
|
||||
— not to shrink the amount or split the entry to dodge the threshold. The
|
||||
tool description says this explicitly; gaming the policy is a correctness
|
||||
violation, and the append-only audit log would show it anyway.
|
||||
(`asset_id` instead of `entry_id` for a disposal; `split_group: [ids]`
|
||||
when the split check triggered.)
|
||||
- **Not checked yet:** `books.create_invoice`, `books.create_bill`,
|
||||
`books.receive_payment`, `books.pay_bill`, `books.run_payroll` and
|
||||
`books.remit_payroll_taxes` post at once with no preview and no receipt
|
||||
check: attach their evidence right after, to the entry they return.
|
||||
- **Anti-splitting.** Posting an entry triggers a split check: posted
|
||||
entries on the same date with the same normalized description and debit
|
||||
accounts are grouped, and if the group total exceeds the threshold while
|
||||
@@ -1585,17 +1661,21 @@ posting.
|
||||
and pdf are the printed statement, operating by class and investing
|
||||
and financing by kind with each kind's transactions under it, with its
|
||||
two schedules), `trial_balance`, `members_equity` (statement of
|
||||
changes in members' equity; members from `wanderbooks.toml`; earnings
|
||||
changes in members' equity; members from the settings (`company.members`); earnings
|
||||
not yet closed are split the way the next members close will split
|
||||
them, so a member's capital on a day never changes when its month is
|
||||
closed: rules.md B-13, D-38),
|
||||
`general_ledger` (by account: beginning balance, postings with running
|
||||
balance and split, ending balance), `general_journal` (alias
|
||||
`journal`: each entry posted in the period, line by line; `iif`:
|
||||
every posted entry from the first through `as_of`, never a period).
|
||||
- Formats: `pdf` and `xlsx` (base64) for the five statements; `xlsx`,
|
||||
`csv`, `json` for every report; `iif` for `general_journal` (alias
|
||||
`journal`), for QuickBooks Desktop.
|
||||
every posted entry from the first through `as_of`, never a period),
|
||||
`tax_summary` (see "Tools — tax categories"), `fixed_asset_register`
|
||||
(as of a day; see "Tools — fixed assets").
|
||||
- Formats: `pdf` and `xlsx` (base64) for `profit_and_loss`,
|
||||
`balance_sheet`, `cash_flow`, `trial_balance`, `members_equity`,
|
||||
`tax_summary` and `fixed_asset_register`; `xlsx`, `csv`, `json` for
|
||||
every report (`general_ledger` and `general_journal` have no pdf);
|
||||
`iif` for `general_journal` (alias `journal`), for QuickBooks Desktop.
|
||||
- Dates (rules.md B-30): the period reports (`profit_and_loss`,
|
||||
`cash_flow`, `members_equity`, `tax_summary`, `general_ledger`, and
|
||||
`general_journal` as csv, json and xlsx) take `start` and `end`
|
||||
@@ -1612,7 +1692,8 @@ posting.
|
||||
character past ASCII is transliterated ("René" "Rene", "北京" "Bei
|
||||
Jing", "—" "--"), a double quote is a single one (QuickBooks reads it
|
||||
as a text delimiter), a tab or line break is a space, a control
|
||||
character '?'. Within QuickBooks Desktop's limits (Intuit): an account
|
||||
character, or a character with no transliteration, '?'; a colon in an
|
||||
account's name or number '-' (a colon separates levels). Within QuickBooks Desktop's limits (Intuit): an account
|
||||
name level past 31 characters is cut there; accounts whose names would
|
||||
then match get " (code)" (QuickBooks would merge them); a subaccount
|
||||
whose QuickBooks type differs from its parent's is written at the top
|
||||
@@ -1648,8 +1729,9 @@ posting.
|
||||
capital and draws; per-partner capital and distributions plus guaranteed
|
||||
payments; officer compensation and shareholder distributions; dividends
|
||||
and income tax expense), common income and expense accounts with tax
|
||||
categories, and the `wanderbooks.toml` settings (tax form, members, close
|
||||
target). A proposal: review with the human, then `books.import_chart_csv`.
|
||||
categories, and `settings_toml`, the settings that go with it (tax form,
|
||||
members, where closes go) as settings-file TOML for
|
||||
`books.import_settings`. A proposal: review with the human, then `books.import_chart_csv`.
|
||||
|
||||
## Tools — tax categories
|
||||
|
||||
@@ -1662,10 +1744,14 @@ posting.
|
||||
- `books.renumber_account { account, code }` → `{ id, previous_code,
|
||||
code, config_mentions }`: give an account a new code. Only the code
|
||||
changes (entries name accounts by id), so balances and reports don't
|
||||
move. Refuses a code in use, or an unassigned control role's default
|
||||
code for an account of the wrong type. `config_mentions` lists the
|
||||
wanderbooks.toml settings still naming the old code (edit them; the
|
||||
server reloads the file). Journaled as `account.renumbered`.
|
||||
move. Refuses an empty code, a code in use, an unassigned control
|
||||
role's default code for an account of the wrong type or a header
|
||||
(placeholder), and any account an approval rule names (revise or retire
|
||||
the rule first). `config_mentions` lists each setting still naming the
|
||||
old code (as `books.set_setting` keys, or settings-file labels for books
|
||||
whose settings are in a file) and each convention whose template or
|
||||
approval rule names it: change each with `books.set_setting` /
|
||||
`books.set_convention`. Journaled as `account.renumbered`.
|
||||
- `books.rename_account { account, name, reason? }` → `{ id, code,
|
||||
previous_name, name, notes }` (scope `books:post`, destructive: a
|
||||
rename changes the label every past report prints): give an account a
|
||||
@@ -1718,8 +1804,13 @@ posting.
|
||||
|
||||
- `books.import_bank_csv { csv_text? | upload_id?, bank_account_code,
|
||||
default_offset_code, rules?, date_order?, source_attachment_id? }` →
|
||||
creates one draft per transaction (QBO upload layout). `rules` maps
|
||||
description substrings to account codes. `source_attachment_id` (the export as filed, category
|
||||
`{ imported: [{ entry_id, date, description, amount, offset_account }],
|
||||
count, source_attachment_id, source_linked }`: one draft per
|
||||
transaction, all or none, from the QuickBooks bank-upload layout
|
||||
(`Date,Description,Amount`, payments negative, or
|
||||
`Date,Description,<money out>,<money in>`). `rules` maps description
|
||||
substrings (ignoring case; the longest match wins) to account codes,
|
||||
else `default_offset_code`. `source_attachment_id` (the export as filed, category
|
||||
`import_source`) is checked before any draft and linked to the bank
|
||||
account (`source_linked`; false when it was already), never to the
|
||||
drafts: a bank export is not a receipt.
|
||||
@@ -1748,7 +1839,7 @@ Hand-rolled on `ServerHandler` (rmcp 3.x has no resource macros):
|
||||
- `books://receipts/{id}` — receipt metadata (as `books.get_receipt`
|
||||
view `meta`; no bytes)
|
||||
|
||||
Resources exist so read-only clients (dashboards, the future read-only GUI,
|
||||
Resources exist so read-only clients (dashboards, the viewer,
|
||||
auditors) never need tool credentials. Tested end-to-end over stdio.
|
||||
|
||||
## The agent operating contract
|
||||
@@ -1775,9 +1866,8 @@ in the server's tool descriptions, so the model reads it at use time):
|
||||
|
||||
## Out of scope for this phase
|
||||
|
||||
Bank feeds, payment processing, payroll, tax filing, multi-user authN/Z, the
|
||||
GUI (it consumes the event log in a later phase). Cash flow reporting is
|
||||
R-FUTURE-3 and rides the engine roadmap, not the MCP layer.
|
||||
Bank feeds, payment processing, payroll tax tables and filing (payroll is
|
||||
accounting only: the caller supplies every figure), tax filing.
|
||||
|
||||
## Tools — periods
|
||||
|
||||
@@ -1789,9 +1879,11 @@ R-FUTURE-3 and rides the engine roadmap, not the MCP layer.
|
||||
ledger), posting nothing; with `confirmed: true`: `{ status: "closed",
|
||||
period_id, closing_entry, lines, members }`, posting it and marking the
|
||||
period closed. The entry zeroes revenue and expense balances as of the
|
||||
period end; the net goes to `3999` Retained Earnings, or, with
|
||||
`[[company.members]]` in the settings, to each member's capital account
|
||||
by share, cumulatively (rules.md B-13; D-10): each close gives a member
|
||||
period end; the net goes to `3999` Retained Earnings (created if
|
||||
missing), or to `books.close_into` (a single owner's capital), zeroing
|
||||
the `books.close_sweep` accounts (draws, dividends) into it too; or,
|
||||
with `company.members` in the settings, to each member's capital
|
||||
account by share (each member's distribution account zeroed into it), cumulatively (rules.md B-13; D-10): each close gives a member
|
||||
their share of everything closed to these members and shares so far, to
|
||||
the currency's minor unit (odd units by largest remainder, ties to the
|
||||
member listed first when the count began), less what earlier closes gave
|
||||
@@ -1875,16 +1967,19 @@ R-FUTURE-3 and rides the engine roadmap, not the MCP layer.
|
||||
amount }] }` → `{ id, total, entry_id, warnings? }`. Auto-posts Dr
|
||||
`1200` Accounts Receivable / Cr `4000` Revenue. `warnings` names a
|
||||
revenue account it created with no tax category (rules.md B-32).
|
||||
`party_id` is the customer's `id` from `books.add_customer`, not its
|
||||
`party_id`.
|
||||
- `books.create_bill { party_id, date, due_date, lines }` → `{ id, total,
|
||||
entry_id, warnings? }`. Auto-posts Dr `6999` General Expenses / Cr
|
||||
`2000` Accounts Payable; `warnings` as for invoices.
|
||||
`2000` Accounts Payable; `warnings` as for invoices. `party_id` is the
|
||||
vendor's `id` from `books.add_vendor`, not its `party_id`.
|
||||
- `books.receive_payment { document_id, date, amount, cash_account }` →
|
||||
`{ entry_id, remaining, status }`. Posts Dr Cash / Cr A/R.
|
||||
- `books.pay_bill { document_id, date, amount, cash_account }` →
|
||||
`{ entry_id, remaining, status }`. Posts Dr A/P / Cr Cash.
|
||||
- `books.ar_aging { as_of? }` → `{ as_of, rows: [{ name, current, days_30,
|
||||
days_60, days_90, days_90_plus, total }] }` (`as_of` omitted: today).
|
||||
- `books.ap_aging { as_of }` → same, per vendor.
|
||||
- `books.ap_aging { as_of? }` → same, per vendor (`as_of` omitted: today).
|
||||
|
||||
## Tools — foreign exchange
|
||||
|
||||
@@ -1956,7 +2051,7 @@ entries.
|
||||
file keeps the books' text), the README lists the same lines (and
|
||||
names the entries with a line on accounts receivable or payable, to
|
||||
which QuickBooks Desktop may ask the CPA to add a customer or vendor),
|
||||
and `note` ends "N account name(s), number(s), place(s) or memo(s)
|
||||
and, when the file writes anything differently, `note` ends "N account name(s), number(s), place(s) or memo(s)
|
||||
are written differently in general-journal.iif to fit QuickBooks
|
||||
Desktop (warnings says how; so does the README): tell the human.", N
|
||||
counting each name, number, place and memo.
|
||||
@@ -1978,7 +2073,11 @@ entries.
|
||||
last fours only.", or "The package, with full TINs, for the CPA.",
|
||||
followed, with gaps, by "N TIN(s) are missing (tin_gaps): collect them
|
||||
with books.set_tin or books.set_w9 and export again." **K-1 worksheet** (`k1-members.csv`, with
|
||||
`[[company.members]]`): one row per member, in order: `member,
|
||||
`[[company.members]]`): one row per member, in order (with more than one
|
||||
member, a last row totals them, "Total, all members (not a K-1)", to tie
|
||||
to Schedule L; a worksheet that can't be made is replaced by
|
||||
`<name>-NOT-INCLUDED.txt` saying why, the K-1, tax-basis and 1099
|
||||
worksheets alike): `member,
|
||||
party_id, share, capital_account, tin, tin_type, tin_note` (without
|
||||
TINs: `tin_last4, tin_type`), then `beginning_capital,
|
||||
contributions_cash, contributions_property, net_income, distributions,
|
||||
@@ -2023,6 +2122,10 @@ entries.
|
||||
by class, investing and financing by kind with each member named, and
|
||||
its two schedules; reconciled to the package's cash accounts),
|
||||
`members-equity`
|
||||
(multi-member LLCs), `trial-balance`; plus `general-ledger.xlsx` and
|
||||
`general-journal.xlsx`. Period reports cover the fiscal year to date.
|
||||
(multi-member LLCs), `tax-summary`, `trial-balance`;
|
||||
`fixed-asset-register` (PDF and XLSX) when an asset was acquired by
|
||||
`as_of`; plus `general-ledger.xlsx` and `general-journal.xlsx`; and
|
||||
`README.txt` first (the year, the basis, the journal's verified event
|
||||
count and what each file is). Period reports cover the fiscal year
|
||||
holding `as_of`, through it.
|
||||
What the CPA needs; they never log into the system.
|
||||
+7
-2
@@ -15,7 +15,7 @@ layers differ in what they cover and in whether we may copy them:
|
||||
| **Pacioli (1494), Geijsbeek translation (1914)** | The mechanics of double entry: debits equal credits, the trial balance, one money, writing only forward, correcting by opposite entry, closing into capital. No modern standard restates these; they predate every regulator. | Public domain | Vendored verbatim: `sources/pacioli-1494.md` |
|
||||
| **IRS: Internal Revenue Code, Treasury Regulations, publications, form instructions** | Records and supporting documents, retention, accounting periods and methods, information returns (1099), capitalization (de minimis, 12-month rule), partners' capital, tax basis of purchased and contributed property (`sources/irs-partnership-basis.md`), payroll taxes, rounding on returns, return line items for tax categories (`sources/irs-tax-lines.md`) | Public domain (17 U.S.C. §105; IRS.gov's content policy) | Vendored verbatim: `sources/irs-*.md`, `sources/us-public-domain-status.md` |
|
||||
| **File formats: Intuit (QuickBooks Online bank CSV, QuickBooks Desktop IIF), ECMA-376 Office Open XML, Microsoft Excel limits, Avery label sheets** | What `ledger-import` reads and writes: bank/card CSV layouts and signs, IIF journal entries, how XLSX stores numbers and Excel's 15-digit precision, asset-label sheet geometry (Avery 5160) | © Intuit Inc.; © Ecma International; © Microsoft; © Avery Products Corporation | Cited with short quotes, URLs, and hashes: `sources/intuit-bank-csv.md`, `sources/intuit-iif.md`, `sources/xlsx-numbers.md`, `sources/avery-5160.md` |
|
||||
| **FASB Accounting Standards Codification (ASC)** | Authoritative US GAAP: cash flows (ASC 230), revenue and principal vs agent (ASC 606), foreign currency (ASC 830), credit losses and aging (ASC 326), property and depreciation (ASC 360) | © Financial Accounting Foundation | Cited by paragraph with short quotes only (in `rules.md`) |
|
||||
| **FASB Accounting Standards Codification (ASC)** | Authoritative US GAAP: cash flows (ASC 230), revenue and principal vs agent (ASC 606), foreign currency (ASC 830), credit losses and aging (ASC 326), property and depreciation (ASC 360) | © Financial Accounting Foundation | Cited by paragraph with short quotes only (in `rules.md`; the paragraphs read, with where and when, in `sources/fasb-asc.md` and below) |
|
||||
| **FASB Concepts Statement No. 8, Chapter 4 (2021)** | Definitions of the five elements (it replaced Concepts Statement 6) | © Financial Accounting Foundation; non-authoritative | Cited with short quotes only |
|
||||
| **Textbooks (OpenStax *Principles of Accounting, Vol. 1*)** | Conventions no standard states: normal balances, closing-entry mechanics, straight-line depreciation arithmetic | CC BY-NC-SA 4.0 (incompatible with this repo's license) | Cited with short quotes and attribution only |
|
||||
| **Convention or decision** | Where no authority speaks: aging bucket edges, cent rounding among members, closed-period locks | — | Stated as CONVENTION or DECISION, with who decided and when |
|
||||
@@ -72,4 +72,9 @@ Deloitte's public DART Roadmap): ASC 230-10-10-1, 45-1, 45-4, 45-5,
|
||||
each checked again against the same sources in the P3 review (numbers,
|
||||
pages and wording hold). Open: 105-10-05-1 to 05-3, read only in their
|
||||
2009 text (ASU 2009-01, and as Wiley GAAP 2020 quotes them); their
|
||||
current wording is unconfirmed.
|
||||
current wording is unconfirmed. Also open (P3 surface review, 2026-10-21): ASC 230-10-45-15(f) (debt
|
||||
issue costs, cited by B-14 rule 11) is in neither list above; ASC
|
||||
606-10-25-31, 25-32 and 55-20 (B-16's over-time method) are not yet read;
|
||||
B-15 rule 6 cites ASC 250 with no paragraph; B-17's 606-10-55-39 is in
|
||||
neither list. Each is to be read in the ASC basic view and listed in
|
||||
`sources/fasb-asc.md` before it is relied on further.
|
||||
+4
-3
@@ -78,8 +78,9 @@ schedules round to it (B-15, B-16); assets an earlier version took finer
|
||||
replay as journaled. **Checked:**
|
||||
`an_asset_cost_finer_than_a_cent_is_refused`,
|
||||
`a_foreign_line_is_booked_in_whole_base_units`,
|
||||
`no_entry_posts_a_fraction_of_the_minor_unit`; the GnuCash oracle.
|
||||
**Checked:** `foreign_currency_line_is_rejected`, `fx_overflow_is_an_error`.
|
||||
`no_entry_posts_a_fraction_of_the_minor_unit`,
|
||||
`foreign_currency_line_is_rejected`, `fx_overflow_is_an_error`; the
|
||||
GnuCash oracle.
|
||||
|
||||
## B-4 Five elements, closed list
|
||||
|
||||
@@ -1997,7 +1998,7 @@ books declare before their first rename, B-27); `books.rename_account` (scope
|
||||
`ledger-core` `tests/spec_ledger.rs`
|
||||
(`a_new_name_is_one_trimmed_line_of_bounded_length`), `ledger-store`
|
||||
`tests/store.rs` (`a_new_accounts_name_is_journaled_trimmed`); Lean
|
||||
`Step.record` (a name is outside the model: `docs/verification.md` 13a);
|
||||
`Step.record` (a name is outside the model: `docs/verification.md` law 15);
|
||||
bookspec `examples/rename-changes-no-amount.toml`.
|
||||
|
||||
## B-32 Every revenue and expense account maps to a line of the return
|
||||
|
||||
@@ -4,7 +4,7 @@ Governs what a member's party totals count (rules.md B-35): a member's capital a
|
||||
## Treas. Reg. §1.704-1(b)(2)(iv)(b): basic rules of capital accounts
|
||||
Source: <https://www.ecfr.gov/api/renderer/v1/content/enhanced/current/title-26?part=1§ion=1.704-1>
|
||||
Retrieved: 2026-10-20
|
||||
SHA-256 of the retrieved file (`ecfr7041.html`): `0eafa6f27d7df3a3cc93ebad7cbdc0c37a49b055c53a43d5e5e8efa29f80beeb`
|
||||
SHA-256 of the retrieved file (`ecfr17041.html`): `0eafa6f27d7df3a3cc93ebad7cbdc0c37a49b055c53a43d5e5e8efa29f80beeb`
|
||||
|
||||
> (b) Basic rules. Except as otherwise provided in this paragraph (b)(2)(iv), the partners' capital accounts will be considered to be determined and maintained in accordance with the rules of this paragraph (b)(2)(iv) if, and only if, each partner's capital account is increased by
|
||||
|
||||
@@ -30,6 +30,9 @@ Source: as above (the same retrieved file).
|
||||
## Partner's Instructions for Schedule K-1 (Form 1065), Item L
|
||||
Source: <https://www.irs.gov/instructions/i1065sk1>
|
||||
Retrieved: 2026-10-20
|
||||
SHA-256 of the retrieved file (`i1065sk1.html`): `87d46c0e6c07eb0b642e9479f72aed7c35ed005cab0b1c644af5572a9df22d7c`
|
||||
SHA-256 of the retrieved file (`i1065sk1.html`): `87d46c0e6c07eb0b642e9479f72aed7c35ed005cab0b1c644af5572a9df22d7c`
|
||||
(This page's HTML differs from fetch to fetch, so each retrieval has its
|
||||
own hash: `irs-partnerships.md` quotes another retrieval of the same day. A re-fetch on
|
||||
2026-10-21 hashed differently again; the quoted text was checked present.)
|
||||
|
||||
> The partnership must report your beginning capital account and ending capital account for the year using the tax-basis method, including the amount of capital you contributed to the partnership during the year, your share of the partnership’s current-year net income or loss as computed for tax purposes, any withdrawals and distributions made to you by the partnership, and any other increases or decreases to your capital account determined in a manner generally consistent with figuring the partner’s adjusted tax basis in its partnership interest (without regard to partnership liabilities), taking into account the rules and principles of sections 705, 722, 733, and 742.
|
||||
@@ -1,5 +1,5 @@
|
||||
# Partnerships: shares of income and capital accounts
|
||||
Governs `CloseTo::Members`, the members' equity statement, and the K-1 worksheet's capital accounts on the tax basis (`ledger_core::tax_capital`, rules.md B-36). Partners' shares come from the partnership agreement (IRC §704(a)); a partner's capital account is one running balance of what they contributed, their allocations of income and loss, and what was distributed to them (Treas. Reg. §1.704-1(b)(2)(iv)(b)); when a partner's interest changes during the year, the distributive shares take the varying interests into account, period by period (IRC §706(d)(1), Treas. Reg. §1.706-4(a)); Form 1065 reports each partner's capital account on the tax basis (Schedule K-1 item L) and the changes in them (Schedule M-2). No authority prescribes how to round a share to the cent: that rule is the owner's decision (largest remainder, 2026-10-13), counted from the company's first close (D-10, 2026-10-16).
|
||||
Governs `CloseTo::Members`, the members' equity statement, and the K-1 worksheet's capital accounts on the tax basis (rules.md B-13; `ledger_core::tax_capital`, rules.md B-36). Partners' shares come from the partnership agreement (IRC §704(a)); a partner's capital account is one running balance of what they contributed, their allocations of income and loss, and what was distributed to them (Treas. Reg. §1.704-1(b)(2)(iv)(b)); when a partner's interest changes during the year, the distributive shares take the varying interests into account, period by period (IRC §706(d)(1), Treas. Reg. §1.706-4(a)); Form 1065 reports each partner's capital account on the tax basis (Schedule K-1 item L) and the changes in them (Schedule M-2). No authority prescribes how to round a share to the cent: that rule is the owner's decision (largest remainder, 2026-10-13), counted from the company's first close (D-10, 2026-10-16).
|
||||
|
||||
## 26 U.S.C. §704(a)-(b)
|
||||
Source: <https://www.law.cornell.edu/uscode/text/26/704>
|
||||
@@ -59,7 +59,10 @@ SHA-256 of the retrieved file (`ecfr17064.html`): `e7b07463190db6a610dee37660efe
|
||||
## Partner's Instructions for Schedule K-1 (Form 1065) (2025)
|
||||
Source: <https://www.irs.gov/instructions/i1065sk1>
|
||||
Retrieved: 2026-10-20
|
||||
SHA-256 of the retrieved file (`i1065sk1.html`): `d7d6298286601fd95ab8e445c10fd62499ea1e37f8e5c3531f8e1783862d8d03`
|
||||
SHA-256 of the retrieved file (`i1065sk1.html`): `d7d6298286601fd95ab8e445c10fd62499ea1e37f8e5c3531f8e1783862d8d03`
|
||||
(This page's HTML differs from fetch to fetch, so each retrieval has its
|
||||
own hash: `irs-capital-accounts.md` quotes another retrieval of the same day. A re-fetch on
|
||||
2026-10-21 hashed differently again; the quoted text was checked present.)
|
||||
|
||||
**Item L, its first paragraph** (the partner's capital account, on the tax basis; the K-1 worksheet carries the book capital account and, beside it, the tax-basis one as far as the books know it, rules.md B-36)
|
||||
|
||||
|
||||
+70
-5
@@ -23,7 +23,7 @@ promise.
|
||||
| Model-based stateful | the Lean state machine, transcribed (3) | `tests/model.rs` | `just check` | ~3s | every change |
|
||||
| Differential: Decimal vs i64 | the kernel at two types (3) | `kernel_bridge.rs` | `just check` | <1s | every change |
|
||||
| Differential: GnuCash | another implementation, adjudicated (3) | `tests/oracle_gnucash.rs`, `oracle/gnucash/` | `just oracle` | ~2 min | weekly |
|
||||
| Formal proofs | Kani (bounded) and Lean (unbounded) | `src/proofs.rs`, `verify/lean/` | `just prove` | ~15-20 min (bounded by `entry_flows_of_a_reversal_mirror`, ~14 min; all 29 harnesses ~43 min of CPU, run in parallel) | weekly |
|
||||
| Formal proofs | Kani (bounded) and Lean (unbounded) | `src/proofs.rs`, `verify/lean/` | `just prove` (47 harnesses and Lean), `just prove-slow` (the 8 in the justfile's `kani_slow`, each under its own 30-minute cap) | prove ~5 min and prove-slow ~8 min at four jobs on a quiet laptop (each ~25 min under heavy load; docs/verification.md, "Running it") | weekly (CI runs both) |
|
||||
| Fuzzing | crash, hang, broken books (4); for `fuzz_store_replay` (validly chained journals with fuzz-chosen events): books that open must verify, and the viewer must accept exactly what the writer does | `fuzz/fuzz_targets/`, corpora in `fuzz/corpus/` | `just fuzz <target> <secs>` | 10 min each | weekly |
|
||||
| Miri | undefined behavior in our use of dependencies (4) | the core's unit, bridge, invariant tests (the exhaustive calendar walk aside) | `just miri` | ~4 min | on demand |
|
||||
| Golden | `tests/golden/fixture_company.json` (5) | `invariants.rs` | `just check` | <1s | every change |
|
||||
@@ -984,8 +984,10 @@ the end.
|
||||
`constructive_agrees`.
|
||||
- **Proofs:** Kani `entry_flows_sum_to_cash`,
|
||||
`entry_flows_pure_are_movements`, `entry_flows_are_bounded`,
|
||||
`entry_flows_of_a_reversal_mirror`, `no_cash_no_flow` (entries of up to
|
||||
three lines), `take_is_odd_and_bounded`, `apply_cash_mirrors`,
|
||||
`no_cash_no_flow` (entries of up to three lines), the reversal's mirror
|
||||
(`entry_movements_of_a_reversal_mirror` and `flows_of_mirrors` at three
|
||||
lines, `entry_flows_of_a_reversal_mirror` end to end at two, since the
|
||||
P3 surface review), `take_is_odd_and_bounded`, `apply_cash_mirrors`,
|
||||
`apply_cash_spends_exactly`, `exchanged_is_bounded`,
|
||||
`constructive_nets_to_zero`, `constructive_mirrors`; Lean
|
||||
`cashflow_sections_sum_to_cash_change`, `entry_flows_sum`,
|
||||
@@ -1015,7 +1017,8 @@ the end.
|
||||
`on_behalf`, the kernel's received sign, `max` for `min`, the uses'
|
||||
order reversed): 16 fail a test; one is equivalent today (pairing that
|
||||
ignores `on_behalf`: a mirrored reversal's real and constructive parts
|
||||
are each proved to mirror, Kani `entry_flows_of_a_reversal_mirror` and
|
||||
are each proved to mirror, Kani `entry_movements_of_a_reversal_mirror`,
|
||||
`flows_of_mirrors`, `entry_flows_of_a_reversal_mirror` and
|
||||
`constructive_mirrors`, so the check is defensive, for P4's drafts).
|
||||
The first round's 18 breaks, re-run on the reworked code (four
|
||||
adapted where the code moved), and one more for the taxes class: 18
|
||||
@@ -1436,7 +1439,7 @@ bound, the minor-unit plan check), `first_day`, `last_day`, `next`, `due`'s
|
||||
zero months, `ScheduleConvention::parse`/`key`; `settings.rs` the new key
|
||||
and `schedule_convention()`'s default; `store.rs` `run_depreciation`
|
||||
(nothing due, the feature on a re-run), `live_runs`,
|
||||
`depreciation_runs_ever`, `dispose_preview` (true-up split, `in_month`),
|
||||
`Ledger::depreciation_runs` (was `depreciation_runs_ever`), `dispose_preview` (true-up split, `in_month`),
|
||||
`check_nothing_after_disposal`, `disposal_lines`, `check_reverse_entry`
|
||||
(the re-instatement room),
|
||||
`register_schedule` (the feature only for daily), `ensure_feature`,
|
||||
@@ -1690,6 +1693,68 @@ rules.md says why not. Mutation runs wait for P12 (D-33).
|
||||
tax-basis K-1's book depreciation left it out (B-36); `books.list_assets`
|
||||
as of a day said "disposed" for a later disposal (B-15 rule 8).
|
||||
|
||||
## P3: the surface review (2026-10-21)
|
||||
|
||||
The agent's surface, the docs and the whole proof run, on `main` with all
|
||||
of P3 landed (first `2f999c6`, then rebased onto `2c5b5a7` with the
|
||||
seams, asset basis and data safety reviews, and checked again). No
|
||||
mutation run (D-33). What was run, in this
|
||||
order, each alone (under `taskset -c 4-15`, four build jobs, on a laptop
|
||||
other work kept at a load average near 30): `just check` (clean, before
|
||||
and after the fixes); `just prove` as it stood (it ran past its
|
||||
30-minute cap at four jobs, 52 of 53 harnesses verified: see
|
||||
docs/verification.md, "Running it"); `just prove` and `just prove-slow`
|
||||
after the split, until both passed; the viewer's screenshots on the demo
|
||||
books; the privacy sweep below.
|
||||
|
||||
| What | Where | Oracle |
|
||||
|---|---|---|
|
||||
| Every tool the server has is documented in `docs/mcp-interface.md`, and every tool the doc documents exists (115; `books.access_log` had no entry) | `ledger-mcp` `server.rs` `every_tool_is_documented_and_every_documented_tool_exists` (with a check of its own reader) | the doc, read tool by tool against the code |
|
||||
| The reversal's cash flow mirror at three lines, in two halves: a reversal's movements are its original's negated, and `kernel::flows_of` is odd; end to end at two lines | Kani `entry_movements_of_a_reversal_mirror`, `flows_of_mirrors`, `entry_flows_of_a_reversal_mirror` (`kernel::entry_flows` split into `entry_movements` and `flows_of`, behavior unchanged: every cash flow test, bridge and property passes) | docs/verification.md law 10; Lean `flows_reversal` |
|
||||
| A closed-list value in a bookspec example (a 1099 form, a payment method's kind, a reason kind, a party totals basis) is spelled exactly as the README spells it; a K-1 query needs `cash_accounts` | `ledger-core` `tests/bookspec.rs` `a_form_in_another_case_is_refused`, `a_payment_method_kind_in_another_case_is_refused`, `a_reason_kind_in_another_case_is_refused`; `check_k1` | bookspec/README.md |
|
||||
| Every Kani harness in `src/proofs.rs` runs in exactly one of `just prove` and `just prove-slow`, each run's count is checked, and a slow-list name that isn't a harness fails | `justfile` `_kani` | docs/verification.md, "Running it" |
|
||||
|
||||
**Docs corrected** (each against the code): `docs/mcp-interface.md`, about
|
||||
60 fixes (parameter names: `search_entries`' `account_id`, `link_receipt`'s
|
||||
`attachment_id`; outputs: `books.chart`, `books.digest`, `get_receipt`,
|
||||
`run_recurring`, `label_asset`, `asset_schedule`, `dispose_asset`,
|
||||
`import_bank_csv`; refusals: `reverse_entry`'s date rule,
|
||||
`create_schedule`, `renumber_account`, `register_asset`, `correct_asset`,
|
||||
`set_vendor_1099`, `set_entry_party`, `withdraw_attachment`; the
|
||||
idempotent list (ten, not seven), the handbook's section list,
|
||||
`books.start`'s next steps, the receipt policy's keys, the export reports
|
||||
and formats, the CPA package's files, the close's targets, stale "out of
|
||||
scope" text); tool and schema text in `server.rs` and `types.rs` where it
|
||||
said the same wrong things; rules.md cross-references (eight wrong B-rule
|
||||
numbers in code, tests and one example); bookspec's README (what the
|
||||
loader takes, the statuses defined) and thirteen examples' statuses (an
|
||||
example whose figures follow an owner decision is CHOICE and cites it);
|
||||
docs/verification.md (laws numbered 1 to 31, no gaps; every harness listed
|
||||
with its law, recipe and time); `README.md`'s tool count.
|
||||
|
||||
**Privacy sweep** (after the rebase onto `2c5b5a7`). `scrubber.originals_left`
|
||||
over every tracked file (626, binaries decoded too): none in any text
|
||||
file or fixture (the P2 served-books fixture's journal, database and
|
||||
vault included); the six Inter font files match only a descending digit
|
||||
run in their TrueType bytecode, a placeholder number the scrub's map also
|
||||
holds, not a TIN from the books. The first company's people, places and bank, by name
|
||||
(case-insensitive, LICENSE files aside): none. The eighteen figures from
|
||||
its books, with and without thousands separators: none.
|
||||
|
||||
**The viewer** (the demo books, `cargo test -p ledger-gui --test screens`,
|
||||
every page; PNGs kept outside the repo): the figures tie across pages.
|
||||
Fixed: a report's notes were drawn on one line and cut (the fixed asset
|
||||
register's explanation of its tax columns ended mid-word); they wrap over
|
||||
the table's width now. The Accounts page listed each type's accounts in
|
||||
the order they were added; it lists them by code, as the reports do.
|
||||
|
||||
**For P12 (mutation targets, none run):** `kernel::entry_movements` and
|
||||
`kernel::flows_of` (the split of `entry_flows`; the same mutants as
|
||||
before); the bookspec loader's exact-spelling filters and `check_k1`'s
|
||||
`cash_accounts` check; `documented_tools` (its own sample checks it).
|
||||
The earlier P3 sections' lists stand (one name corrected:
|
||||
`Ledger::depreciation_runs`, which was `depreciation_runs_ever`).
|
||||
|
||||
## Benchmarks (baseline 2026-10-13)
|
||||
|
||||
No budget, by decision (the owner, 2026-10-13: the books do little work
|
||||
|
||||
+183
-82
@@ -103,12 +103,19 @@ Each law, and what proves it. Kani harnesses are in
|
||||
same identities at runtime and refuses (`CashFlowDoesNotFoot`) rather
|
||||
than show a statement that doesn't foot.
|
||||
- Kani `entry_flows_sum_to_cash`, `entry_flows_pure_are_movements`,
|
||||
`entry_flows_are_bounded`, `entry_flows_of_a_reversal_mirror`,
|
||||
`no_cash_no_flow` (end to end on entries of up to three lines, each
|
||||
in any of the four places: four lines ran past half an hour per
|
||||
harness; at three, the reversal's mirror is the slowest harness in
|
||||
the suite, about 14 minutes on the development laptop, the others
|
||||
seconds to 6 minutes), `take_is_odd_and_bounded`, `apply_cash_mirrors` (the
|
||||
`entry_flows_are_bounded`, `no_cash_no_flow` (end to end on entries
|
||||
of up to three lines, each in any of the four places: four lines
|
||||
ran past half an hour per harness); the reversal's mirror at three
|
||||
lines in two halves, `entry_movements_of_a_reversal_mirror` (a
|
||||
reversal's cash and section movements are its original's negated)
|
||||
and `flows_of_mirrors` (`kernel::flows_of` is odd, for any
|
||||
movements three lines can make), `kernel::entry_flows` being the
|
||||
one then the other, and end to end on up to two lines,
|
||||
`entry_flows_of_a_reversal_mirror` (at three lines end to end it
|
||||
took about 14 minutes alone, 2026-10-20, and past 30 minutes at
|
||||
four jobs on a busy machine, 2026-10-21; split then, nothing
|
||||
weakened: the halves cover every three-line entry the end-to-end
|
||||
harness did), `take_is_odd_and_bounded`, `apply_cash_mirrors` (the
|
||||
parts, for any amounts), `apply_cash_spends_exactly` (how a
|
||||
section's cash splits among its kinds, operating's classes
|
||||
included), `exchanged_is_bounded` (the
|
||||
@@ -162,7 +169,7 @@ sequence of commands, of any length**:
|
||||
commands adds, removes, or changes an entry dated in it, and it stays
|
||||
closed.
|
||||
- Lean `Step.frozen`, `Steps.frozen`
|
||||
14a. **How the books are kept changes no entry**: in the model, a record
|
||||
15. **How the books are kept changes no entry**: in the model, a record
|
||||
(a setting, convention, decision, open item or approval; a vendor's
|
||||
1099 decision or W-9 flag, a payment method, an entry's payment-method
|
||||
tag) leaves the
|
||||
@@ -193,7 +200,7 @@ sequence of commands, of any length**:
|
||||
|
||||
### Closing
|
||||
|
||||
15. **A closing line zeroes its account**, and **a closing entry balances
|
||||
16. **A closing line zeroes its account**, and **a closing entry balances
|
||||
and zeroes every revenue and expense account** through the period's
|
||||
last day, whether the net goes to one account or is split among
|
||||
members.
|
||||
@@ -201,14 +208,22 @@ sequence of commands, of any length**:
|
||||
`closing_entry_balances`
|
||||
- Lean `closingEntry_net`, `closingEntry_zeroes`,
|
||||
`closingEntry_accepted`, `close_zeroes`
|
||||
16. **A close credits equity with exactly the net income**: revenue minus
|
||||
17. **A close credits equity with exactly the net income**: revenue minus
|
||||
expenses through the period's last day.
|
||||
- Lean `net_closings_is_net_income`, `close_credits_net_income`
|
||||
17. **An allocation's parts sum to the whole, to the unit**, however the
|
||||
other shares were rounded, and only the last share changes.
|
||||
- Kani `plug_last_sums_exactly` · Lean `plugLast_total`,
|
||||
`plugLast_keeps`
|
||||
18. **A run of member closes telescopes**: each member's total from a run
|
||||
18. **An allocation's parts sum to the whole, to the unit**, however the
|
||||
other shares were rounded, and only the last share changes; and the
|
||||
largest-remainder apportionment (`kernel::apportion`, rules.md B-13)
|
||||
succeeds exactly when the rounded-down parts are short of the whole by
|
||||
less than a unit each, then sums to the whole, gives each part nothing
|
||||
or one unit (one part may also take the sub-unit fraction), and never
|
||||
gives a unit to a part while one with a strictly larger remainder goes
|
||||
without (no member favored by position).
|
||||
- Kani `plug_last_sums_exactly`, `apportion_is_exact`,
|
||||
`apportion_is_fair` · Lean `plugLast_total`, `plugLast_keeps`,
|
||||
`Apportion.ranks_perm`, `Apportion.apportion_sum`,
|
||||
`Apportion.apportion_sum_of_shortfall` (any number of parts)
|
||||
19. **A run of member closes telescopes**: each member's total from a run
|
||||
of cumulative closes is their allocation of everything the run closed
|
||||
(so its rounding is one allocation's, never a sum of them), and each
|
||||
close hands out exactly its net, for any allocation whose parts sum to
|
||||
@@ -217,56 +232,56 @@ sequence of commands, of any length**:
|
||||
- Lean `runPart_telescopes`, `closePart_total` · property
|
||||
`each_member_stays_within_a_cent_of_their_share`
|
||||
|
||||
The P3 odd cent (defect 17; rules.md B-13; D-10, D-38) adds three laws;
|
||||
their numbers are assigned when P3 lands:
|
||||
The P3 odd cent (defect 17; rules.md B-13; D-10, D-38) adds three laws:
|
||||
|
||||
- **A cumulative members close hands out exactly its net, and leaves each
|
||||
member's running total one allocation of everything the count closed**,
|
||||
whatever rule the count's earlier closes were made under, as long as
|
||||
they handed out what they closed. The run rule is the special case where
|
||||
they gave exactly the allocation. A refusal leaves the parts as they
|
||||
were.
|
||||
- Kani `apportion_cumulative_is_exact`,
|
||||
`cumulative_closing_entry_balances` (the closing entry nets to zero
|
||||
whatever the true-up)
|
||||
- Lean `trueUp_total`, `trueUp_running`, `closePart_eq_trueUp`,
|
||||
`Apportion.allocW_sum`, `Apportion.allocW_cumulative_total`
|
||||
- properties `apportion_cumulative_agrees`,
|
||||
`allocate_cumulative_agrees_with_the_integer_model`
|
||||
- **Bounded drift: no member's running total is ever a minor unit from
|
||||
their exact share.** With shares w_i / W (exact decimals) and every
|
||||
amount a whole number of minor units (B-3), after every cumulative
|
||||
close |(given_i + part_i)·W − T·w_i| < W, whatever earlier closes gave.
|
||||
The precondition is the code's: `periods::allocate_cumulative` refuses a
|
||||
product Decimal would round, so the remainders it ranks are exact, and
|
||||
they sum to whole units (the Lean hypothesis of `rem_pos_of_rank_lt`).
|
||||
- Kani `cumulative_close_never_drifts_a_unit_at_halves`, `_at_tenths`
|
||||
and `_at_near_thirds` (two members at 50/50, three at 20/30/50%, three
|
||||
at 33.33/33.33/33.34%; every running total, net and earlier split to
|
||||
2^12 units)
|
||||
- Lean `Apportion.rem_pos_of_rank_lt` (a member given a unit had a
|
||||
positive remainder), `Apportion.apportion_drift` (one apportionment
|
||||
is within a unit), `Apportion.allocW_drift` (the model of
|
||||
`allocate_cumulative`, gain or loss), `cumulative_drift` (any
|
||||
allocation within a unit stays so under the true-up),
|
||||
`Apportion.allocW_cumulative_drift` (the two together): any number of
|
||||
members, any amounts
|
||||
- property `members_capital_never_drifts_a_cent_from_their_share`
|
||||
- **A member's capital on a day never moves when its period is closed.**
|
||||
The statement of members' equity gives each member, of the earnings not
|
||||
yet closed, what the count's next close would give them; closing moves
|
||||
those earnings into capital without changing the sum. When the count's
|
||||
earlier closes gave something else (closes under an earlier rule), the
|
||||
figure moves by exactly the true-up the close posts, never before.
|
||||
- Lean `statement_after_close`, `statement_unmoved_by_close`
|
||||
- property `each_members_capital_chains_and_holds_through_closes`;
|
||||
`three_members_at_a_third_each_over_twelve_months` (every day of a
|
||||
year, closed and never closed), the viewer's
|
||||
`a_members_page_shows_the_same_capital_before_and_after_a_close`
|
||||
20. **A cumulative members close hands out exactly its net, and leaves each
|
||||
member's running total one allocation of everything the count closed**,
|
||||
whatever rule the count's earlier closes were made under, as long as
|
||||
they handed out what they closed. The run rule is the special case where
|
||||
they gave exactly the allocation. A refusal leaves the parts as they
|
||||
were.
|
||||
- Kani `apportion_cumulative_is_exact`,
|
||||
`cumulative_closing_entry_balances` (the closing entry nets to zero
|
||||
whatever the true-up)
|
||||
- Lean `trueUp_total`, `trueUp_running`, `closePart_eq_trueUp`,
|
||||
`Apportion.allocW_sum`, `Apportion.allocW_cumulative_total`
|
||||
- properties `apportion_cumulative_agrees`,
|
||||
`allocate_cumulative_agrees_with_the_integer_model`
|
||||
21. **Bounded drift: no member's running total is ever a minor unit from
|
||||
their exact share.** With shares w_i / W (exact decimals) and every
|
||||
amount a whole number of minor units (B-3), after every cumulative
|
||||
close |(given_i + part_i)·W − T·w_i| < W, whatever earlier closes gave.
|
||||
The precondition is the code's: `periods::allocate_cumulative` refuses a
|
||||
product Decimal would round, so the remainders it ranks are exact, and
|
||||
they sum to whole units (the Lean hypothesis of `rem_pos_of_rank_lt`).
|
||||
- Kani `cumulative_close_never_drifts_a_unit_at_halves`,
|
||||
`cumulative_close_never_drifts_a_unit_at_tenths` and
|
||||
`cumulative_close_never_drifts_a_unit_at_near_thirds` (two members at 50/50, three at 20/30/50%, three
|
||||
at 33.33/33.33/33.34%; every running total, net and earlier split to
|
||||
2^12 units)
|
||||
- Lean `Apportion.rem_pos_of_rank_lt` (a member given a unit had a
|
||||
positive remainder), `Apportion.apportion_drift` (one apportionment
|
||||
is within a unit), `Apportion.allocW_drift` (the model of
|
||||
`allocate_cumulative`, gain or loss), `cumulative_drift` (any
|
||||
allocation within a unit stays so under the true-up),
|
||||
`Apportion.allocW_cumulative_drift` (the two together): any number of
|
||||
members, any amounts
|
||||
- property `members_capital_never_drifts_a_cent_from_their_share`
|
||||
22. **A member's capital on a day never moves when its period is closed.**
|
||||
The statement of members' equity gives each member, of the earnings not
|
||||
yet closed, what the count's next close would give them; closing moves
|
||||
those earnings into capital without changing the sum. When the count's
|
||||
earlier closes gave something else (closes under an earlier rule), the
|
||||
figure moves by exactly the true-up the close posts, never before.
|
||||
- Lean `statement_after_close`, `statement_unmoved_by_close`
|
||||
- property `each_members_capital_chains_and_holds_through_closes`;
|
||||
`three_members_at_a_third_each_over_twelve_months` (every day of a
|
||||
year, closed and never closed), the viewer's
|
||||
`a_members_page_shows_the_same_capital_before_and_after_a_close`
|
||||
|
||||
### Parties
|
||||
|
||||
19. **A party's part of an entry is signed as its own account would be,
|
||||
23. **A party's part of an entry is signed as its own account would be,
|
||||
with a direction that is its sign**: paid exactly when what counts is
|
||||
positive, received when negative, neither exactly when nothing counts;
|
||||
the money and property in it each carry the whole's sign and never
|
||||
@@ -275,7 +290,7 @@ their numbers are assigned when P3 lands:
|
||||
property than it gave, the rest still owed (rules.md B-35).
|
||||
- Kani `party_target_direction_is_its_sign` · bridge
|
||||
`party_target_agrees`
|
||||
20. **A reversal is the exact opposite of what it reverses**: the target
|
||||
24. **A reversal is the exact opposite of what it reverses**: the target
|
||||
is odd (negating the lines' capital movement, money and own-account
|
||||
movement gives the original's reverse, but for the debits fallback and
|
||||
a member's entry whose cash isn't exactly its money: an expense booked
|
||||
@@ -292,7 +307,7 @@ their numbers are assigned when P3 lands:
|
||||
|
||||
### 1099 payments
|
||||
|
||||
21. **A payment to a vendor is split, never lost or counted twice**:
|
||||
25. **A payment to a vendor is split, never lost or counted twice**:
|
||||
each payment line goes in the bucket rules.md B-19's table names (its
|
||||
16 cases: with no payment method naming its account, a financing line
|
||||
never counts and a card account's line is always a card's); the
|
||||
@@ -317,7 +332,7 @@ function, `kernel::prorate_by_days` (each month's share through
|
||||
unit, `MONTH_L` = lcm(28, 29, 30, 31) = 377,580 parts of a month, so a day
|
||||
of any month is a whole number of them.
|
||||
|
||||
22. **The parts of a span sum to its total exactly, none is negative, and
|
||||
26. **The parts of a span sum to its total exactly, none is negative, and
|
||||
the last is the total less the others** (plan/served.md P3 defect 9:
|
||||
"parts sum to the total; the last part absorbs the remainder"). Every
|
||||
split whose parts but the last fit in the span is computed; only parts
|
||||
@@ -329,7 +344,7 @@ of any month is a whole number of them.
|
||||
(a total in whole units is charged, so split, in whole units), from
|
||||
`Prorate.prorate_total`, `Prorate.prorate_nonneg`,
|
||||
`Prorate.parts_telescope` · bridge `prorate_agrees_at_i64_and_i128`
|
||||
23. **Each part but the last is its days times the monthly rate within one
|
||||
27. **Each part but the last is its days times the monthly rate within one
|
||||
minor unit** (P3 defect 9: "each part equals days × rate within 0.01",
|
||||
stated for any currency's minor unit). Each cumulative charge is its
|
||||
exact share rounded half up to a multiple of the unit (a tie goes up;
|
||||
@@ -344,7 +359,7 @@ of any month is a whole number of them.
|
||||
`the_last_months_charge_in_tenths_is_within_half_a_cent` · Lean
|
||||
`Prorate.roundUnit_near`, `Prorate.cum_near`, `Prorate.part_near`,
|
||||
`Prorate.part_within_a_unit` · bridge `prorated_through_agrees_at_i32_and_i128`
|
||||
24. **The charge through a day never falls, never exceeds the total, is
|
||||
28. **The charge through a day never falls, never exceeds the total, is
|
||||
continuous where one month ends and the next begins, and is the total
|
||||
on the last day**; malformed input (a negative total, no months, a
|
||||
unit that isn't positive, a part no month has, a day past the part, a
|
||||
@@ -361,7 +376,7 @@ of any month is a whole number of them.
|
||||
above the total, the total on the last day),
|
||||
`prorated_through_refuses_malformed_input` · property
|
||||
`prop_depreciation_is_monotone_and_bounded` (the engine)
|
||||
25. **A catch-up run never carries the books past the schedule** at a
|
||||
29. **A catch-up run never carries the books past the schedule** at a
|
||||
month end already posted, and posts everything due through its month
|
||||
when no run comes after it (plan/served.md P3, settled in design: a
|
||||
month skipped is caught up in the month the run is made); every
|
||||
@@ -377,7 +392,7 @@ of any month is a whole number of them.
|
||||
· property `prop_run_amounts_are_each_months_run_amount`
|
||||
· store `a_run_reversed_after_its_month_is_caught_up_in_the_reversals_month`,
|
||||
`reinstating_a_run_a_later_run_caught_up_is_refused`
|
||||
26. **A disposal's true-up reaches the schedule** through its day from what
|
||||
30. **A disposal's true-up reaches the schedule** through its day from what
|
||||
the books carry, up or down, never both (`true_up` is `columns` of the
|
||||
gap).
|
||||
- Kani `true_up_reaches_the_target` · bridge `catch_up_and_true_up_agree`
|
||||
@@ -424,7 +439,7 @@ unit tests check the rest.
|
||||
The register's tax figures beside book value (rules.md B-25) come from
|
||||
one kernel function, `kernel::basis_figures`, for every rule:
|
||||
|
||||
27. **Each figure follows from the rule and the facts, and none
|
||||
31. **Each figure follows from the rule and the facts, and none
|
||||
overflows**: a purchase depreciates from its cost and has no §723 basis
|
||||
or built-in gain; a contribution's §723 basis is the member's adjusted
|
||||
basis and its built-in gain the value less it (a loss exactly when the
|
||||
@@ -575,7 +590,8 @@ the Decimal exactness check in `allocate_cumulative`, and the journal's
|
||||
old-books fixture `x_member_closes` (docs/testing.md, "P3: odd cents").
|
||||
Kani times, each harness alone on a busy laptop (Kani 0.68, 2026-10-20):
|
||||
`apportion_cumulative_is_exact` 257 s, `cumulative_close_never_drifts_a_unit_at_halves`
|
||||
17 s, `_at_tenths` 320 s, `_at_near_thirds` 449 s,
|
||||
17 s, `cumulative_close_never_drifts_a_unit_at_tenths` 320 s,
|
||||
`cumulative_close_never_drifts_a_unit_at_near_thirds` 449 s,
|
||||
`cumulative_closing_entry_balances` 202 s (`apportion_is_exact`, for
|
||||
scale: 121 s). Two shapes were too slow and were changed, not weakened in
|
||||
what they prove: restating apportion's postconditions on the cumulative
|
||||
@@ -590,18 +606,103 @@ Lean carrying any weights.
|
||||
|
||||
```sh
|
||||
mise install # once: Kani, Lean, and the rest, pinned in mise.toml
|
||||
just prove
|
||||
just prove # 47 Kani harnesses and Lean
|
||||
just prove-slow # the other 8: the justfile's kani_slow
|
||||
just prove-all # both
|
||||
```
|
||||
|
||||
`just prove` runs every Kani harness, in parallel (`-j $(nproc)`, under
|
||||
a 30-minute timeout), builds the Lean proofs (seconds),
|
||||
and fails if any headline theorem depends on anything beyond Lean's
|
||||
standard axioms (`propext`, `Quot.sound`, `Classical.choice`). In
|
||||
particular, `sorryAx` fails it. Its time is the slowest harness's:
|
||||
`entry_flows_of_a_reversal_mirror`, about 14 minutes on the development
|
||||
laptop (8 cores); the 20 harnesses before P3 take about 17 minutes of
|
||||
CPU in all (`accounting_equation` the longest, about 4), the cash flow's
|
||||
other ten about 17 (`constructive_mirrors` about 2.5 minutes,
|
||||
`constructive_nets_to_zero` about 1.5), so the whole run is about 15 to
|
||||
20 minutes. Kani
|
||||
downloads its pinned nightly and CBMC into `~/.kani` on first use.
|
||||
`just prove` runs every Kani harness but the justfile's `kani_slow`, in
|
||||
parallel (`-j $(nproc)`), the whole run under a 30-minute timeout; builds
|
||||
the Lean proofs (seconds); and fails if any headline theorem depends on
|
||||
anything beyond Lean's standard axioms (`propext`, `Quot.sound`,
|
||||
`Classical.choice`). In particular, `sorryAx` fails it. `just prove-slow`
|
||||
runs the slow harnesses, `$(nproc)` at a time, each under its own
|
||||
30-minute timeout. Both fail if a harness they ask for isn't verified (the
|
||||
count is checked), if any cover is unsatisfiable (a vacuous proof), or if
|
||||
`kani_slow` names a harness `src/proofs.rs` doesn't have; a new harness
|
||||
runs in `prove` until it is listed as slow. CI's weekly run does both, on
|
||||
two runners (`.github/workflows/scheduled.yml`). Kani downloads its
|
||||
pinned nightly and CBMC into `~/.kani` on first use.
|
||||
|
||||
**Why the split (2026-10-21, the P3 surface review).** With all of P3 the
|
||||
suite is 55 harnesses. One `just prove` at four jobs (a CI runner's
|
||||
cores), on the development laptop under heavy load from other work (a
|
||||
load average near 30 on 16 cores, the run held to 12), could not finish
|
||||
inside its 30 minutes: 52 of the 53 harnesses of the time were verified,
|
||||
all of them proved, and the last, `entry_flows_of_a_reversal_mirror` at
|
||||
three lines, was still running; alone, under the same load, it passed 30
|
||||
minutes too. A run is only as safe as its slowest harness under the
|
||||
machine it gets, so: that harness's claim at three lines is now proved in
|
||||
two halves (`entry_movements_of_a_reversal_mirror`, `flows_of_mirrors`;
|
||||
law 10) that take a minute or two each, and the end-to-end harness runs
|
||||
at two lines; the eight harnesses that take the longest run in `just
|
||||
prove-slow`, each capped on its own. Nothing was dropped.
|
||||
|
||||
Measured, four jobs: under that load, `just prove` 1,450 s (48 harnesses
|
||||
then) and `just prove-slow` 1,365 s (seven then; `entry_flows_sum_to_cash`
|
||||
1,228 s); on the same laptop quiet (2026-10-22, the final configuration),
|
||||
`just prove` 308 s (47 harnesses, about 19 minutes of CPU, and Lean's 83
|
||||
headline theorems) and `just prove-slow` 467 s (8 harnesses, the slowest
|
||||
`entry_flows_sum_to_cash` at 301 s).
|
||||
|
||||
Every harness, the law it proves, the recipe that runs it, and its time
|
||||
in that quiet run (`prove`: each harness's verification time; `prove-slow`:
|
||||
each harness's whole run, its compile of about 50 s included):
|
||||
|
||||
| Harness | Law | Recipe | Seconds |
|
||||
|---|---|---|---:|
|
||||
| `flip_negates` | 5 | prove | 0 |
|
||||
| `sums_are_exact` | 4 | prove-slow | 82 |
|
||||
| `check_lines_is_exact` | 1 | prove | 7 |
|
||||
| `out_of_range_is_rejected` | 1 | prove | 0 |
|
||||
| `accepted_entry_nets_to_zero` | 2 | prove | 15 |
|
||||
| `movements_are_per_account` | 3 | prove | 12 |
|
||||
| `movements_sum_to_the_net` | 3 | prove | 58 |
|
||||
| `reversal_mirrors` | 5 | prove | 25 |
|
||||
| `reversal_cancels` | 5 | prove | 69 |
|
||||
| `normal_balance_is_signed` | 8 | prove | 0 |
|
||||
| `accounting_equation` | 8 | prove-slow | 176 |
|
||||
| `normal_side_increases` | 6 | prove | 0 |
|
||||
| `trial_balance_columns` | 7 | prove | 62 |
|
||||
| `closing_line_zeroes` | 16 | prove | 0 |
|
||||
| `credit_line_credits` | 16 | prove | 0 |
|
||||
| `closing_entry_balances` | 16 | prove | 11 |
|
||||
| `payment_bucket_is_the_table` | 25 | prove | 0 |
|
||||
| `payment_split_is_exact` | 25 | prove | 18 |
|
||||
| `net_buckets_keeps_the_total` | 25 | prove | 5 |
|
||||
| `plug_last_sums_exactly` | 18 | prove | 4 |
|
||||
| `apportion_is_exact` | 18 | prove | 56 |
|
||||
| `apportion_is_fair` | 18 | prove | 18 |
|
||||
| `apportion_cumulative_is_exact` | 20 | prove | 44 |
|
||||
| `cumulative_close_never_drifts_a_unit_at_halves` | 21 | prove | 11 |
|
||||
| `cumulative_close_never_drifts_a_unit_at_tenths` | 21 | prove | 149 |
|
||||
| `cumulative_close_never_drifts_a_unit_at_near_thirds` | 21 | prove-slow | 216 |
|
||||
| `cumulative_closing_entry_balances` | 20 | prove-slow | 150 |
|
||||
| `take_is_odd_and_bounded` | 10 | prove | 1 |
|
||||
| `apply_cash_mirrors` | 10 | prove | 12 |
|
||||
| `entry_flows_sum_to_cash` | 10 | prove-slow | 301 |
|
||||
| `entry_flows_pure_are_movements` | 10 | prove-slow | 151 |
|
||||
| `entry_flows_are_bounded` | 10 | prove | 20 |
|
||||
| `entry_flows_of_a_reversal_mirror` | 10 | prove | 36 |
|
||||
| `entry_movements_of_a_reversal_mirror` | 10 | prove | 30 |
|
||||
| `flows_of_mirrors` | 10 | prove | 56 |
|
||||
| `no_cash_no_flow` | 10 | prove | 75 |
|
||||
| `apply_cash_spends_exactly` | 10 | prove | 36 |
|
||||
| `exchanged_is_bounded` | 10 | prove | 0 |
|
||||
| `constructive_nets_to_zero` | 10 | prove | 26 |
|
||||
| `constructive_mirrors` | 10 | prove | 52 |
|
||||
| `party_target_direction_is_its_sign` | 23 | prove | 2 |
|
||||
| `party_target_is_odd` | 24 | prove | 5 |
|
||||
| `reverse_target_is_an_involution` | 24 | prove | 0 |
|
||||
| `prorate_parts_sum_to_total` | 26 | prove-slow | 222 |
|
||||
| `prorate_last_part_absorbs_the_remainder` | 26 | prove | 33 |
|
||||
| `a_months_charge_is_within_half_a_cent` | 27 | prove | 48 |
|
||||
| `a_months_charge_in_tenths_is_within_half_a_cent` | 27 | prove | 42 |
|
||||
| `the_last_months_charge_is_within_half_a_cent` | 27 | prove-slow | 148 |
|
||||
| `the_last_months_charge_in_tenths_is_within_half_a_cent` | 27 | prove | 36 |
|
||||
| `prorated_through_refuses_malformed_input` | 28 | prove | 7 |
|
||||
| `catch_up_never_overtakes_the_schedule` | 29 | prove | 8 |
|
||||
| `catch_ups_is_every_suffixs_catch_up` | 29 | prove | 35 |
|
||||
| `true_up_reaches_the_target` | 30 | prove | 0 |
|
||||
| `basis_figures_laws` | 31 | prove | 0 |
|
||||
| `kani_symbolic_copy_canary` | (the checker: "Trusting the checker") | prove | 3 |
|
||||
@@ -43,21 +43,17 @@ server-only:
|
||||
sweep-test-tmp:
|
||||
@find "${TMPDIR:-/tmp}" /tmp -maxdepth 1 -user "$(id -u)" -type d -mmin +60 \( -name 'wb-*' -o -name 'wanderbooks-*' -o -name 'ledger-*' \) -exec rm -rf {} + 2>/dev/null || true
|
||||
|
||||
# Formal proofs: Kani on the accounting kernel, Lean for books of any size (docs/verification.md)
|
||||
prove:
|
||||
# The Kani harnesses `prove` leaves to `prove-slow`: each takes four to
|
||||
# twenty minutes alone on a busy machine, and with them the suite (about
|
||||
# 100 minutes of CPU, 2026-10-21) can't finish inside the 30-minute cap at
|
||||
# four jobs, a CI runner's. Every other harness in src/proofs.rs runs in `prove`, a new one
|
||||
# included; a name here that isn't a harness fails both (docs/verification.md).
|
||||
kani_slow := "accounting_equation cumulative_close_never_drifts_a_unit_at_near_thirds cumulative_closing_entry_balances entry_flows_pure_are_movements entry_flows_sum_to_cash prorate_parts_sum_to_total sums_are_exact the_last_months_charge_is_within_half_a_cent"
|
||||
|
||||
# Formal proofs: Kani on the accounting kernel (all but kani_slow), Lean for books of any size (docs/verification.md)
|
||||
prove: (_kani "fast")
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
# Capped: a harness that blows up should fail, not eat the machine.
|
||||
log=$(mktemp)
|
||||
(ulimit -v 12000000; cd crates/ledger-core &&
|
||||
timeout 1800 cargo kani -j "$(nproc)" --output-format terse | tee "$log")
|
||||
# A cover that can't be satisfied means a proof is vacuous.
|
||||
if grep -E "\*\* [0-9]+ of [0-9]+ cover properties satisfied" "$log" |
|
||||
awk '{ if ($2 != $4) bad = 1 } END { exit !bad }'; then
|
||||
echo "Kani: an unsatisfiable cover; some proof is vacuous" >&2
|
||||
exit 1
|
||||
fi
|
||||
rm -f "$log"
|
||||
cd verify/lean
|
||||
lake build
|
||||
audit=$(lake env lean Audit.lean)
|
||||
@@ -67,6 +63,66 @@ prove:
|
||||
fi
|
||||
echo "Lean: $(wc -l <<<"$audit") headline theorems, standard axioms only"
|
||||
|
||||
# The slow Kani harnesses (kani_slow), each under its own 30-minute cap, $(nproc) at a time (CI runs it beside prove)
|
||||
prove-slow: (_kani "slow")
|
||||
|
||||
# Every proof: prove, then prove-slow
|
||||
prove-all: prove prove-slow
|
||||
|
||||
# Kani on one set of the harnesses: "fast" (all but kani_slow, the whole
|
||||
# run under the cap) or "slow" (each harness under the cap)
|
||||
_kani set:
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
# Every harness: the fn after each #[kani::proof] in src/proofs.rs.
|
||||
all=$(awk '/#\[kani::proof\]/ { p = 1; next } p && match($0, /^fn [a-z_0-9]+/) { print substr($0, 4, RLENGTH - 3); p = 0 }' crates/ledger-core/src/proofs.rs | sort)
|
||||
slow=$(tr ' ' '\n' <<<"{{kani_slow}}" | sort)
|
||||
if missing=$(comm -13 <(echo "$all") <(echo "$slow")) && [ -n "$missing" ]; then
|
||||
echo "kani_slow names no harness: $missing" >&2
|
||||
exit 1
|
||||
fi
|
||||
if [ "{{set}}" = slow ]; then names=$slow; else names=$(comm -23 <(echo "$all") <(echo "$slow")); fi
|
||||
logs=$(mktemp -d)
|
||||
# What each run must show: every harness asked for verified, and no
|
||||
# cover unsatisfiable (a vacuous proof).
|
||||
checked() { # log, count
|
||||
if ! grep -qE "^Complete - $2 successfully verified harnesses, 0 failures, $2 total" "$1"; then
|
||||
echo "Kani: expected $2 harness(es) verified, see $1" >&2
|
||||
return 1
|
||||
fi
|
||||
if grep -E "\*\* [0-9]+ of [0-9]+ cover properties satisfied" "$1" |
|
||||
awk '{ if ($2 != $4) bad = 1 } END { exit !bad }'; then
|
||||
echo "Kani: an unsatisfiable cover; some proof is vacuous ($1)" >&2
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
cd crates/ledger-core
|
||||
# Capped (timeout 1800, 12 GB each): a harness that blows up should
|
||||
# fail, not eat the machine.
|
||||
ulimit -v 12000000
|
||||
if [ "{{set}}" = fast ]; then
|
||||
args=()
|
||||
for h in $names; do args+=(--harness "proofs::$h"); done
|
||||
timeout 1800 cargo kani -j "$(nproc)" --output-format terse --exact "${args[@]}" | tee "$logs/fast.log"
|
||||
checked "$logs/fast.log" "$(wc -l <<<"$names")"
|
||||
else
|
||||
cargo kani --only-codegen --output-format terse
|
||||
failed=0
|
||||
for h in $names; do
|
||||
while [ "$(jobs -rp | wc -l)" -ge "$(nproc)" ]; do wait -n || failed=1; done
|
||||
( start=$(date +%s)
|
||||
timeout 1800 cargo kani --output-format terse --exact --harness "proofs::$h" > "$logs/$h.log" 2>&1 &&
|
||||
checked "$logs/$h.log" 1 &&
|
||||
echo "Kani: $h verified in $(( $(date +%s) - start )) s" ||
|
||||
{ echo "Kani: $h failed or ran past 1800 s ($logs/$h.log)" >&2; exit 1; } ) &
|
||||
done
|
||||
while [ "$(jobs -rp | wc -l)" -gt 0 ]; do wait -n || failed=1; done
|
||||
wait || failed=1
|
||||
[ "$failed" = 0 ] || exit 1
|
||||
fi
|
||||
rm -rf "$logs"
|
||||
echo "Kani ({{set}}): $(wc -l <<<"$names") harnesses verified"
|
||||
|
||||
# Random books through wanderbooks and GnuCash 5.10, compared exactly (Docker)
|
||||
oracle:
|
||||
docker build -q -t wanderbooks-gnucash-oracle oracle/gnucash
|
||||
|
||||
+100
-33
@@ -1,6 +1,6 @@
|
||||
# STATUS — 2026-10-20: "Served" P0, P1 and P2 live; P3 next
|
||||
# STATUS — 2026-10-21: "Served" P0, P1 and P2 live; P3 built and landed, its cutover pending
|
||||
|
||||
## Now: Served (plan/served.md), P0, P1 and P2 live
|
||||
## Now: Served (plan/served.md), P0, P1 and P2 live; P3 on main
|
||||
|
||||
The 2026-10-16 audit of the first company's books (62 verified findings,
|
||||
20 engine defects) and the design discussion that followed produced
|
||||
@@ -88,38 +88,105 @@ folder's `legacy/`. Agents connect with `wanderbooks connect`, the viewer is
|
||||
books data. The before-check found defect 21 again (the IIF ignores
|
||||
`as_of`).
|
||||
|
||||
**Open from P3's depreciation and schedules work (defects 22 and 9,
|
||||
review 2026-10-20), for the P3 lead:** the viewer still recomputes a
|
||||
long-life asset's whole schedule every frame (plan defect 22's note; R12
|
||||
of its brief: slow, never wrong); a schedule month a run leaves for a
|
||||
human (dated in a closed period, or reversed after it ended) stays
|
||||
unposted with nothing to mark it booked by hand, so its schedule never
|
||||
shows complete, and a schedule or an asset registered by mistake has no
|
||||
way to stop (a reversed month comes due again); both want a design
|
||||
(a schedule's postings are its plan's, re-derived on replay, so it can't
|
||||
catch a month up as depreciation does). From review round 2: a reversal
|
||||
dated before the entry it reverses is refused for depreciation runs and
|
||||
schedule postings only (B-9: a correction comes after what it corrects);
|
||||
every other entry still takes one, a general rule for the P3 lead to
|
||||
settle; the bookspec examples cite ASC 606-10-25-23, which `sources/`
|
||||
holds only through DART (vendor it with 25-31, 25-32 and 55-20); the
|
||||
every-order search runs three commands deep in `just check`; four and
|
||||
five ran clean by hand (`WB_ORDERS_DEPTH`, one and five minutes), six is
|
||||
P12's. Seen in the same review, not P3's: `ledger-store`
|
||||
`ops::tests::the_server_makes_the_folder_and_the_viewer_only_reads_it`
|
||||
failed once in a whole-workspace run (`!viewer_open` after both holders
|
||||
dropped) and passed alone and in six runs of the crate's tests; likely a
|
||||
child a concurrent test forks (backup's git) holding the viewer lock's
|
||||
descriptor until it execs.
|
||||
**P3 (engine correctness): built and landed on `main`, the cutover
|
||||
pending.** Every defect group of plan P3 is on `main` (`2c5b5a7`), each
|
||||
built from its brief, reviewed until a round came back clean, and with
|
||||
its regression tests written as bookspec examples (51 in
|
||||
`bookspec/examples/`, every one checked by `cargo test`): the odd cent
|
||||
(defect 17, D-10, D-38), the IIF in plain ASCII and the CPA package as of
|
||||
one day (defects 7, 21, D-36), the cash flow statement by ASC 230's
|
||||
direct method (defect 1, D-35, D-42), rename, one set of period defaults
|
||||
and tax categories that warn or refuse (defects 15, 16, 8), party totals
|
||||
by what moved the money (defects 14, 11, 24), dated 1099 decisions,
|
||||
payment methods and the card exception (defects 23, 12, 18, D-40),
|
||||
depreciation straight-line by days through the final month and daily
|
||||
schedules (defects 22, 9, D-37), the format upgrade only with the owner's
|
||||
go-ahead (D-39), and assets' posted and scheduled depreciation, their
|
||||
bases and the K-1 on the tax basis (defect 13, D-41); then three review
|
||||
commits: the seams between the pieces (one fictional year checked
|
||||
across engine, viewer, tools and package; three defects found and
|
||||
fixed), asset basis (schema 14, shipped migrations pinned by hash; the
|
||||
tax K-1's net income always open), and data safety (three new journal
|
||||
features, replay with P2's TIN detector, the P2 served-books fixture). Mutation waits for
|
||||
P12 (D-33): each piece's targets are listed in `docs/testing.md`.
|
||||
|
||||
**Next:** P3 (engine correctness; designed: one brief per defect group,
|
||||
each attacked by a reviewer, kept outside this public repo since they cite
|
||||
the first company's figures; the owner's questions come one at a time as
|
||||
each is built), then P4-P13 as planned. P11: one app for Windows, Mac and
|
||||
Linux (D-24). P12: max testing again (D-33). P13: bookspec (D-34), a
|
||||
formal, sourced accounting specification first, then its own formal
|
||||
validation and a conformance harness for any engine; P3's regression
|
||||
tests are its first examples.
|
||||
**The P3 surface review (2026-10-21)** checked the agent's surface, the
|
||||
docs and the whole proof run on `main` with all of P3: `just check`
|
||||
clean; the tools' docs (`docs/mcp-interface.md`) corrected tool by tool
|
||||
against the code (about 60 fixes: wrong parameter names, missing outputs
|
||||
and refusals, the idempotent list, the handbook's section list,
|
||||
`books.start`'s next steps, the CPA package's files) and a new test that
|
||||
every tool is documented and every documented tool exists
|
||||
(`every_tool_is_documented_and_every_documented_tool_exists`; 115 tools,
|
||||
`books.access_log` was undocumented); B-rule cross-references fixed in
|
||||
code and tests (eight wrong); bookspec's statuses made consistent (an
|
||||
example whose figures follow an owner decision is CHOICE and cites it:
|
||||
thirteen statuses changed, five more examples given the decision they
|
||||
rest on), its README now saying exactly what the loader takes,
|
||||
and the loader refusing a closed-list value spelled any other way. **`just
|
||||
prove` could not finish in its 30-minute cap at four jobs** (a CI
|
||||
runner's cores) on a heavily loaded laptop: 52 of 53 harnesses in, the
|
||||
reversal mirror at three lines still running. It is split, nothing
|
||||
dropped: that harness is proved in two halves (`kernel::entry_flows` is
|
||||
now `entry_movements` then `flows_of`) plus end to end at two lines, and
|
||||
`just prove` runs 47 harnesses and Lean (308 s at four jobs on a quiet
|
||||
machine; 83 headline theorems, standard axioms only) while `just
|
||||
prove-slow` runs the eight slowest, each under its own cap (467 s), and
|
||||
CI runs both (`docs/verification.md`, "Running it"). The laws are numbered
|
||||
1 to 31 with no gaps, every harness listed with its runtime. The viewer's
|
||||
P3 pages were rendered on the demo books and looked at
|
||||
(`~/.cache/wanderbooks/shots-p3-surface/`): the figures tie across
|
||||
pages (the register, the asset, the balance sheet, the tax summary, the
|
||||
1099 worksheet and the party agree to the cent); two faults fixed: a
|
||||
report's notes were cut to one line (the fixed asset register's lost its
|
||||
explanation of the tax columns; they wrap now), and the Accounts page
|
||||
listed accounts in the order they were added, not by code.
|
||||
|
||||
**Before P3 is live (the lead):** the cutover on the first company's
|
||||
books: reinstall from `main`, the format upgrade with the owner's go-ahead
|
||||
(D-39), `books.verify`, the P3 change log adjudicated against the P2
|
||||
goldens (cash flow, the odd cent's true-up, depreciation by days), the
|
||||
books fully categorized and then `policy.require_tax_category = refuse`
|
||||
(P3, "Settled in design"), and the D-40/D-41 records. The installed
|
||||
server still lacks P3's tools (`books.add_instrument` and the rest) until
|
||||
then.
|
||||
|
||||
**Still open after P3 (each for the phase named, or the lead):**
|
||||
- The viewer recomputes a long-life asset's whole schedule every frame
|
||||
(`ledger-gui` `pages/assets.rs` `schedule`, called from the asset
|
||||
page's draw; defect 22's note: slow, never wrong).
|
||||
- A schedule month a run leaves for a human (dated in a closed period, or
|
||||
reversed after it ended) has nothing to mark it booked by hand, so its
|
||||
schedule never shows complete; a schedule or an asset registered by
|
||||
mistake has no way to stop. Both want a design.
|
||||
- A reversal dated before what it reverses is refused for depreciation
|
||||
runs and schedule postings only (B-9); every other entry still takes
|
||||
one: a general rule to settle (P4's reversal as a draft is the natural
|
||||
place).
|
||||
- Sources not yet read or vendored (`docs/spec/README.md`, "Open"): ASC
|
||||
606-10-25-23 (DART only), 25-31, 25-32, 55-20 (B-16); 230-10-45-15(f)
|
||||
(B-14 rule 11); the ASC 250 paragraph B-15 rule 6 means; 606-10-55-39
|
||||
(B-17); 105-10-05-1 to 05-3's current wording. Public domain but not
|
||||
vendored: IRC §701 and §6225 (B-14 rule 5), Treas. Reg.
|
||||
§1.704-1(b)(2)(iv)(d)(1) and (g)(3) (B-25, cited only). Several cited
|
||||
pages in `sources/intuit-iif.md` carry no SHA-256, and
|
||||
`irs-entity-classification.md` and `file-names.md` no Source, Retrieved
|
||||
or SHA-256 lines; Unicode UAX #14 (B-34) has no source file; Rev. Proc.
|
||||
98-25's excerpt (`irs-records.md`) carries no "§5.01" label.
|
||||
- Eight tool descriptions are over 700 characters (listed in
|
||||
`tool_descriptions_fit_700_characters`): shortened in P5.
|
||||
- The every-order asset search runs three commands deep in `just check`
|
||||
(four and five clean by hand, `WB_ORDERS_DEPTH`); six is P12's. The
|
||||
`ledger-store` test
|
||||
`ops::tests::the_server_makes_the_folder_and_the_viewer_only_reads_it`
|
||||
failed once in a whole-workspace run (likely a forked git child holding
|
||||
the viewer lock's descriptor); it passed in this review's runs.
|
||||
|
||||
**Next:** the P3 cutover, then P4-P13 as planned. P11: one app for
|
||||
Windows, Mac and Linux (D-24). P12: max testing again (D-33). P13:
|
||||
bookspec (D-34), a formal, sourced accounting specification first, then
|
||||
its own formal validation and a conformance harness for any engine; P3's
|
||||
regression tests are its first examples.
|
||||
|
||||
---
|
||||
|
||||
|
||||
Reference in new issue
Block a user