P3: rename accounts, one set of period defaults, tax categories warn or refuse (defects 15, 16, 8)
check / check (push) Canceled after 0s

books.rename_account; account names trimmed, bounded, refused when a
sibling's or TIN-shaped; books.show refuses what its target ignores. A
report asked for without dates covers this fiscal year so far, everywhere
(one rule for tools, descriptions and the viewer; the IIF and CPA package
keep their own dates). policy.require_tax_category warns or refuses; a
Form 4797 gain is shown apart on every return.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01T3gXMVt7XJPi6ens1or9Dj
This commit is contained in:
swaitsandClaude Opus 5.5 committed 2026-10-21 10:41:24 -06:00
1 parent 8255ac2103
commit 9ab003d0d1
69 files changed
+8063 -570

No files matched your search

+123 -26
View File
@@ -334,12 +334,13 @@ Tool descriptions are at most 700 characters; eight longer ones are listed
in `tool_descriptions_fit_700_characters`, to be shortened in P5.
**Annotations.** Every tool has a `title` and a `readOnlyHint`; every tool
that changes anything also has a `destructiveHint`, true for these 28:
that changes anything also has a `destructiveHint`, true for these 29:
`books.post_entry`, `books.reverse_entry`, `books.import_settings`,
`books.set_setting`, `books.retire_convention`, `books.unlink_receipt`,
`books.unlink_attachment`, `books.withdraw_attachment`,
`books.correct_asset`, `books.dispose_asset`, `books.run_depreciation`,
`books.run_schedules`, `books.run_recurring`, `books.renumber_account`,
`books.rename_account` (the label every past report prints),
`books.assign_role`, `books.close_period`, `books.reconcile`,
`books.void_statement`, `books.void_reconciliation`, `books.set_w9`,
`books.set_tin` (both overwrite a vault record), `books.purge_tin`,
@@ -411,6 +412,20 @@ every tool over every book in ledger-store's `tests/fixtures/old-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.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
(top-level accounts are siblings too; ignoring capitals and surrounding
space), one holding a run of nine digits, or one that isn't one line of
at most 200 characters, is refused (rules.md B-31); the name is kept
trimmed. A revenue or expense account (not a header) without `tax_category` is
created with a warning ("6150 Software has no tax category: the tax
summary will list it as uncategorized. Propose one from
books.tax_categories and set it with books.set_tax_category once the
human agrees."), or, under `policy.require_tax_category = refuse`,
refused (rules.md B-32). The category is checked against the account's
type and the return, as `books.set_tax_category` checks it; journaled as
`account.added` then `account.tax_set`.
- `books.draft_entry { date: "2026-10-10", description: "...", vendor?,
party?, parties?, no_party_reason? }` → `{ entry_id, status: "draft" }`.
`party` tags the counterparty (see "Tools — parties"); given with
@@ -443,6 +458,16 @@ every tool over every book in ledger-store's `tests/fixtures/old-books`.
(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.
- **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
report with no dates covers the fiscal year to date
(`company.fiscal_year_start`): `end` defaults to today (the company's
time zone), `start` to the first day of the fiscal year that holds
`end`; a `start` after the `end`, given or derived (`start` after today
with no `end`), is refused, never swapped. An `as_of` defaults to
today; a party's `year` to this calendar year. Every result names the
dates it covers.
- `books.trial_balance { as_of? }` → `as_of`, lines, totals, `balanced`
(the trial balance as of the date, default today).
- `books.balance_sheet { as_of? }` → totals (`assets`, `liabilities`,
@@ -450,13 +475,14 @@ every tool over every book in ledger-store's `tests/fixtures/old-books`.
`liability_accounts`, `equity_accounts` (`{code, name, amount}`), as of
the date (default today).
- `books.profit_and_loss { start?, end?, class? }` → totals plus
`revenue_accounts` and `expense_accounts` for the range. `end` defaults to
today, `start` to the first posted entry. Closing entries are excluded, so
a closed period still shows what it earned.
`revenue_accounts` and `expense_accounts` for the range (omitted dates:
the fiscal year to date). Closing entries are excluded, so a closed
period still shows what it earned.
- `books.cash_flow { start?, end?, cash_accounts?, financing_accounts?, investing_accounts? }` (each defaults to the books' settings, `books.cash_accounts` and so on; cash is required one way or the other)
→ the statement of cash flows, direct method (rules.md B-14, P3 defect
1): `{ method: "direct", start, end, operating_by_class, operating,
cash_from_operations, investing_by_kind, investing, cash_from_investing,
1; omitted dates: the fiscal year to date): `{ method: "direct", start,
end, operating_by_class, operating, cash_from_operations,
investing_by_kind, investing, cash_from_investing,
financing_by_kind, financing, cash_from_financing, net_change_in_cash,
cash_beginning, cash_ending, noncash_disclosures,
operating_reconciliation, reconciles, warnings }`.
@@ -521,6 +547,9 @@ every tool over every book in ledger-store's `tests/fixtures/old-books`.
with a recorded statement, that have posted activity through the end of
last month but no reconciliation (not voided) through it. The viewer's
Needs attention uses the same rule (`ledger_store::reconcilable`).
`uncategorized_accounts` lists every revenue and expense account (not a
header) with no tax category, active or not, `"CODE Name"`: the list
Needs attention shows (rules.md B-32).
## Tools — fixed assets
@@ -605,8 +634,9 @@ through the policy.
## Tools — the viewer
- `books.show { entry_id | account | report | attachment_id | asset |
party | reconciliation | search | page, start?, end? }` → `{ shown,
viewer_open, note }`. Asks the human's viewer (`ledger-gui`) to open an
party | reconciliation | search | page, start?, end?, columns?, compare? }`
→ `{ shown, viewer_open, note }`. Asks the human's viewer (`ledger-gui`)
to open an
entry, an account register or a report (for start/end dates if given),
a stored file, an asset, a party (by id or name, resolved as in "Tools
— parties"), a reconciliation (by id: its account's page, with its
@@ -623,6 +653,30 @@ through the policy.
`viewer_open` says whether one does. The note says "The viewer shows
…", or "No viewer has these books open: ask the human to open it
(opened within a couple of minutes, it shows …)".
- **Dates (rules.md B-30).** For an account or a report, the dates are
resolved by the one default (omitted: the fiscal year to date; only
`end`: its fiscal year to it) and sent to the viewer resolved, so
`shown` and the screen can't differ: "the register of 1000 Business
Checking, 2026-01-01 to 2026-10-20", "the profit and loss report,
2026-01-01 to 2026-10-20", "the trial balance report, as of
2026-10-20" (trial balance, aging, fixed asset register, a balance
sheet in one total column), "the vendor 1099 report, for 2026". A
start after the end, given or derived, is refused and nothing is
sent.
- **Options a target ignores are refused**, naming what takes them:
`start`, `end`, `columns` and `compare` for any target but an account
or a report; `columns` and `compare` for an account; `columns` but for
`profit_and_loss`, `balance_sheet` and `cash_flow`; `compare` but for
those and `tax_summary`, and never with a split; `start` for a report
as of a day (the trial balance, the agings, the fixed asset register,
a balance sheet in one total column unless compared with the prior
period) or for `vendor_1099` (its year is `end`'s); and a split into
more columns than the viewer shows (`ops::MAX_REPORT_COLUMNS`, 240:
past it the viewer shows the total).
- The viewer takes a target's dates as sent; one it can't honour (text
that isn't a date, a start after the end from an earlier writer or a
hand-edited `ops/show.jsonl`, an unknown split) is ignored whole, as
an unknown page is.
## Tools — integrity
@@ -1196,13 +1250,17 @@ posting.
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`: every entry, line by line, in date order).
`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.
- Dates: period reports take `start` (default: first entry) and `end`
(default: today, local time); `balance_sheet`, `trial_balance`,
`fixed_asset_register` and `iif` take `as_of` (default today).
- 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`
(omitted: the fiscal year to date; a start after the end is
refused); `balance_sheet`, `trial_balance`, `fixed_asset_register`
and `iif` take `as_of` (omitted: today, the company's time zone).
- IIF (`docs/spec/rules.md` B-34; plan D-36): plain ASCII text, every
line ending CR LF, inline (no `content_encoding`): every posted entry
from the first through `as_of`, period-closing entries left out
@@ -1267,16 +1325,49 @@ posting.
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`.
- `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
new name (rules.md B-31). Only the name changes (entries name accounts
by id): no entry, balance or statement amount moves, and every report,
past periods included, prints the new name. Refuses an unknown account,
a blank name, the name it has, a name another account under the same
parent has (ignoring capitals), one holding a run of nine digits, or
one that isn't one line of at most 200 characters (kept trimmed); a
`reason` is one to 2,000 characters with no full TIN. Journaled as
`account.renamed { account_id, code, name, previous, reason }`, keyed
by id (a code can change; an id can't); the first rename declares the
journal feature `account_renamed` (rules.md B-27), so an earlier
version stops at the declaration. `notes` always says that QuickBooks
matches accounts by name (tell a CPA who imported an earlier IIF), says
when sub-accounts' full names change with it, when the new name moves
the account into or out of the cash accounts (`books.cash_accounts`
unset: picked by name) or the card accounts (named "card"), and lists
conventions whose text names the old name.
- `books.set_tax_category { account, category?, deductible_percent? }`:
map a revenue or expense account to a category (null clears),
optionally overriding its deductible percent. Journaled as
optionally overriding its deductible percent. Under
`policy.require_tax_category = refuse` a revenue or expense account
that isn't a header is never left without one. Journaled as
`account.tax_set`.
- `books.export { report: "tax_summary" }`: book vs. tax amounts by
category, the difference where they disagree (meals 50%, penalties 0%),
and every P&L account still uncategorized. In the CPA package for the
fiscal year to date.
and every P&L account still uncategorized. Gains and losses on business
property (`form_4797_gain_loss`) are listed apart in `form_4797`, at
their book amounts with no tax amount and outside income and both nets,
on every return: Form 4797 figures them from the tax basis and splits
ordinary from section 1231 gain, which decides the line (never Schedule
C). In the CPA package for the fiscal year to date.
- `books.import_chart_csv` accepts an optional fifth `tax_category` column,
validated before anything is created.
validated before anything is created. A revenue or expense row without
one is listed in `warnings`, or, under `policy.require_tax_category =
refuse`, refuses the whole file (a row is never a header account: the
file can't say so).
- `policy.require_tax_category` (`books.set_setting`): `warn` (unset) or
`refuse` (rules.md B-32). The accounts invoices, bills and payroll
create (Revenue 4000, General Expenses 6999, 6100, 6110) are never
refused; their results' `warnings` name them. The first change naming
the key declares the journal feature `require_tax_category`.
## Tools — import
@@ -1287,8 +1378,10 @@ posting.
`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.
- `books.import_chart_csv { csv_text? | upload_id? }` → creates accounts
from `code,name,type,parent_code` CSV. All-or-nothing.
- `books.import_chart_csv { csv_text? | upload_id? }` → `{ created,
count, warnings? }`: creates accounts from
`code,name,type,parent_code[,tax_category]` CSV. All-or-nothing; the
name and tax category rules are `books.add_account`'s.
- Each takes the CSV as `csv_text` or as an upload (`books.begin_upload`
purpose `import`; UTF-8), never both. The CSV itself is not filed: file
it with `books.file_document` (category `import_source`) for
@@ -1434,17 +1527,18 @@ R-FUTURE-3 and rides the engine roadmap, not the MCP layer.
- `books.add_vendor { name }` → `{ id, party_id }`. Both link the record to
a party ("Tools — parties").
- `books.create_invoice { party_id, date, due_date, lines: [{ description,
amount }] }` → `{ id, total, entry_id }`. Auto-posts Dr `1200` Accounts
Receivable / Cr `4000` Revenue.
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).
- `books.create_bill { party_id, date, due_date, lines }` → `{ id, total,
entry_id }`. Auto-posts Dr `6999` General Expenses / Cr `2000` Accounts
Payable.
entry_id, warnings? }`. Auto-posts Dr `6999` General Expenses / Cr
`2000` Accounts Payable; `warnings` as for invoices.
- `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 }` → `{ rows: [{ name, current, days_30, days_60,
days_90, days_90_plus, total }] }`.
- `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.
## Tools — foreign exchange
@@ -1464,7 +1558,9 @@ entries.
- `books.run_payroll { date, cash_account, lines: [{ employee?,
employee_name?, gross_pay, federal_withholding, state_withholding, ss_employee,
medicare_employee, ss_employer, medicare_employer, other_deductions }] }`
→ `{ run_id, entry_id, total_gross, total_net }`. One balanced entry:
→ `{ run_id, entry_id, total_gross, total_net, warnings? }` (`warnings`
names an expense account it created with no tax category). One
balanced entry:
Dr 6100 Salary & Wage Expense (gross), Dr 6110 Payroll Tax Expense
(employer SS + Medicare), Cr 2110–2150 withholding payables, Cr cash
(net). Auto-creates the 61xx/21xx accounts. Respects closed periods.
@@ -1490,7 +1586,8 @@ entries.
- `books.add_line` gains optional `class` (name or id): tags the new line.
Shared costs are split explicitly across lines at entry time.
- `books.profit_and_loss` gains optional `class` filter.
- `books.profit_and_loss_by_class { start, end, class? }` → per-class revenue,
- `books.profit_and_loss_by_class { start?, end?, class? }` (omitted dates:
the fiscal year to date, as every period report) → per-class revenue,
expenses, net income, plus an unclassified row (only that class's row
when `class` is given; an unknown or ambiguous class is refused). Reversals carry their
class so divisional P&L nets correctly.
+205 -7
View File
@@ -1080,12 +1080,19 @@ lowers the format in force. Opening the books checks every complete line
before it changes anything: a cut-off last line is set aside only when the
lines before it pass, so an older version never alters newer books.
A feature (`ledger_store::format::KNOWN_FEATURES`) is a field or value on
an event type an older version knows but would misread; the writer declares
it with the format in force (at least 2) just before the first line that
needs it, so books that never use it stay readable by versions before it:
`members_cumulative` (P3, B-13: a members close counted from its count's
first close, `"rounding": "cumulative_since"`).
A line an earlier version can't read is announced by a journal feature
(`ledger_store::format::KNOWN_FEATURES`): a new event type, or a field or
value on an event type an earlier version knows but would misread. The
writer declares it once, in the `journal.format_set` just before the
first line that needs it, with the format in force (at least 2) and the
features already in force (`Store::ensure_feature`): an earlier reader
stops at the declaration, and books that never use the feature stay
readable by it. P3's: `members_cumulative` (B-13: a members close counted
from its count's first close, `"rounding": "cumulative_since"`),
`account_renamed` (before the first `account.renamed`, B-31) and
`require_tax_category` (before the first `settings.set` naming
`policy.require_tax_category`, B-32). A later piece asks the owner's
go-ahead before the first such declaration (plan D-39).
**Enforced:** `ledger_store::format` (`JournalFormat::admit`, `declared`,
`gate`, `settle`, `declaration`), `journal::ENVELOPE_KEYS`,
@@ -1281,6 +1288,196 @@ checked to change no figure or entry; `approval_rules_off_by_default`,
`tests/spec_kept.rs`; Lean `Step.record_changes_no_figure`,
`Reachable.records`; fuzz `fuzz_settings_replay`.
## B-30 A report asked for without dates covers this fiscal year so far
**Status:** AUTHORITY for the period (IRS Pub. 538,
`sources/irs-periods-and-methods.md`: "A tax year is an annual accounting
period for keeping records and reporting income and expenses"; 26 U.S.C.
§6041(a), "in any calendar year", and §6041A(a)(2), "during such calendar
year", `sources/irs-1099-thresholds.md`, for the calendar-year counts);
DECISION for using it as the default (no authority says what a tool's
omitted dates mean; the owner, 2026-10-20, `plan/served.md` P3 "Settled in
design": "a report asked for without dates covers this fiscal year so
far, everywhere, and an account's register opens on it with 'Show all
dates' one click away").
The books' annual period is the fiscal year (`company.fiscal_year_start`,
B-29). Every place a date may be omitted resolves it by one rule
(`ledger_core::calendar::PeriodDefaults`), so the agent's report tools,
`books.show`, the viewer and the tool descriptions can't say different
things:
| Kind | Used for | Both dates omitted | Only `end` given | Only `start` given |
|---|---|---|---|---|
| A period | profit and loss (and by class), cash flow, tax summary, members' equity, general ledger, general journal (csv, json, xlsx), an account's register, `books.show` of an account or a report | the first day of the fiscal year that holds today, through today | the first day of the fiscal year that holds `end`, through `end` | `start` through today |
| An instant | balance sheet, trial balance, A/R and A/P aging, fixed asset register, the IIF's `as_of` (every posted entry from the first through it, B-34), the CPA package's `as_of` | today | | |
| A calendar year | party totals (`books.parties`, `books.party`, the viewer's party pages), the viewer's 1099 worksheet | today's calendar year | | |
- A resolved start after the resolved end is refused, whether the caller
gave both dates (`InvalidDateRange`) or only a start after today, the
end an omitted end means (`StartAfterToday`, whose message says the end
was defaulted: "pass end"). It is never swapped or clamped to fit.
- A derived start before 0001-01-01 is the calendar's first day (only an
end early in year 1 under a non-January fiscal year, B-28).
- The first year: when the company was formed after its fiscal year's
first day, the default start comes before formation. The amounts are
the same, and the start is not moved to `company.formed`: start-up and
organizational costs (IRC §195, §709) can be dated before formation.
- "Today" is the company's (`company.timezone`, B-29).
- The CPA package's period and the viewer's "Year to date" preset are
the fiscal year that holds their day (`calendar::fiscal_year_start_on`),
the fact itself, never "what an omitted date means": they don't follow
this default if it ever changes.
- Not covered (no default period): search filters, the activity digest
(its `since` is required; the `books://digest` resource reads this
month so far), the `through` of `books.run_schedules` and
`books.run_recurring` (today: an operation, not a period), and the
viewer's drill-downs from a balance, which run from the books' first
entry because a balance is cumulative. The IIF is never a period (B-34):
it refuses `start` and `end`, so the general journal's default period
never shortens it, nor anything in the CPA package, which gives its
statements and ledgers its own fiscal year to date.
**Enforced:** `ledger_core::calendar` (`period_defaults`,
`PeriodDefaults::statement`, `statement_range`, `as_of`,
`calendar_year`, `fiscal_year_start_on`, `PeriodKind::describe`,
`START_DOC`, `END_DOC`, `AS_OF_DOC`, `YEAR_DOC`); `ledger_package`
`statement_range`, `as_of_date`, `pnl_statement`, `bs_statement`,
`cash_flow_statement`, `build_package`; `ledger-mcp` `BooksServer::periods`,
`statement_range`, `as_of`, every report tool, `books.show`, the
generated descriptions (`described`) and parameter docs; `ledger-gui`
`Books::periods`, `default_period`, `fiscal_ytd`, `App::show`,
`RegisterRange`. **Checked:** `ledger-core` `calendar.rs` unit tests and
`tests/properties.rs` `prop_period_defaults_resolve`; `ledger-mcp`
`server.rs` `period_tools_describe_the_one_default`, `tests/mcp_stdio.rs`
`one_default_period_everywhere_defect_16` and
`the_default_period_never_shortens_the_iif_or_the_package`,
`tests/oracle_mcp.rs`
`show_sends_the_dates_it_names_defect_16`,
`show_refuses_start_after_end_defect_15`,
`show_refuses_a_derived_inverted_range_defect_15`; `ledger-gui`
`tests/show.rs`; bookspec `examples/default-period-fiscal-year-to-date.toml`.
## B-31 Renaming an account changes its label, nothing else
**Status:** DECISION (the owner, `plan/served.md` 4.9 and P3 "Settled in
design", 2026-10-20: "a new account named like a sibling (ignoring
capitals), or holding a nine-digit number, is refused"); B-8 (posted is
forever); AUTHORITY for keeping the old name (Rev. Proc. 98-25 §5.01,
`sources/irs-records.md`: "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");
Intuit IIF (`sources/intuit-iif.md`: "Accounts are identified by NAME")
for refusing a sibling's name.
Entries name accounts by id, so a rename changes no entry, line, hash,
balance or statement amount: every report, past periods included, prints
the new name. The journal keeps the old one (`account.renamed {account_id,
code, name, previous, reason}`), so a report printed under the old name
can be tied to the new. A rename is refused when the account is unknown,
the name is blank, the name (trimmed) is the one it has, another account
under the same parent (top-level accounts are siblings too) has it
ignoring capitals and surrounding space (QuickBooks would merge the two),
it holds a run of nine digits (a full SSN, EIN or account number, B-26:
every report prints a name), or it is not one line of at most 200
characters (`account::MAX_ACCOUNT_NAME`, the bound of the company's own
name: no line break, tab or other control character, which no report, CSV
or IIF line can hold). The name is kept trimmed. The same name rules hold
for a new account (`books.add_account`, `books.import_chart_csv`), which
is journaled with its name trimmed too; accounts the engine
creates for a control role keep their fixed names and are never refused
for one. Approval rules, roles, settings and closed periods name accounts
by code or id, and a name is no posting, so none of them blocks a rename.
Existing charts are untouched: the rules are the commands', never replay's
(B-28).
A rename can still change what the books pick by name (no amount): with
`books.cash_accounts` unset, the cash accounts are the asset accounts
whose names say they hold cash, and the card accounts are the
liabilities named "card" (`ledger_store::reconcilable`); the result says
when the new name moves the account into or out of either. It also lists
conventions whose text names the old name. QuickBooks matches accounts
by name, so a CPA who imported an earlier IIF is told.
**Enforced:** `Ledger::rename_account` / `check_rename_account`,
`check_account_name` (also in `add_account`), `account::MAX_ACCOUNT_NAME`,
`check_rename_reason`,
`replay_rename_account` (only that the account exists),
`ChartOfAccounts::set_name`, `Ledger::add_control_account`;
`Store::rename_account` (journal feature `account_renamed`, declared
before the first rename, B-27); `books.rename_account` (scope
`books:post`, like renumbering). **Checked:** `ledger-core`
`tests/spec_ledger.rs` (`rename_account_*`), `tests/model.rs`
(`Cmd::Rename`); `ledger-store` `tests/store.rs`
(`rename_account_changes_only_the_name_defect_15`,
`prop_rename_changes_no_figure`, `a_journaled_rename_replays_past_the_command_rules`),
`tests/format.rs`; `ledger-mcp` `tests/mcp_stdio.rs`
(`rename_account_end_to_end_defect_15`), `tests/hostile_args.rs`;
`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);
bookspec `examples/rename-changes-no-amount.toml`.
## B-32 Every revenue and expense account maps to a line of the return
**Status:** AUTHORITY for why it matters (IRS Pub. 583,
`sources/irs-records.md`: "Your books must show your gross income, as
well as your deductions and credits"; IRS Pub. 538: "you must maintain
records that will enable you to file a correct return"; Rev. Proc. 98-25
§5.01: the relationship "between the total of the amounts in the
taxpayer's machine-sensible records by account and the taxpayer's
return"); DECISION for warn versus refuse (Pub. 583: "the law does not
require any specific kind of records"; the owner, 2026-10-20: warn by
default, header accounts exempt, the first company set to refuse at the
P3 cutover).
A tax category is the account's link to the return (`ledger_core::tax`,
`sources/irs-tax-lines.md`). An account needs one when it is a revenue or
expense account and not a header (a header takes no postings, so nothing
of its own reaches the return: no entry posts to one, a replayed one
included, and an account's header flag never changes).
- Needs attention and the digest (`uncategorized_accounts`) list every
account that needs a category and has none, with activity or not.
- `policy.require_tax_category`: `warn` (unset) or `refuse`. Under `warn`,
`books.add_account` and `books.import_chart_csv` create the account and
their results say so. Under `refuse`, they refuse it (the file whole),
naming the account and where the categories are listed; both take the
category at creation, so the refusal is always met in one call; and
`books.set_tax_category` won't clear a category an account needs.
- The accounts invoices, bills and payroll create for a control role
(Revenue 4000, General Expenses 6999, Salary & Wage Expense 6100,
Payroll Tax Expense 6110) are never refused, so an invoice is never
refused over a chart label: their results warn, and Needs attention
lists them. The policy is "no uncategorized account made through the
chart tools", not "never any".
- Turning the policy to `refuse` changes no existing account: it applies
to new commands (B-28). Needs attention keeps listing the gaps.
- Every account a tool can require has a category that fits it on every
return: the gain or loss account of a disposal (`form_4797_gain_loss`,
Form 4797), tax-exempt income (`tax_exempt_income`, Schedule K), and
each control role's. A gain or loss on business property is figured on
Form 4797 from the tax basis, and its split (ordinary gain, net section
1231 gain) decides the line, never Schedule C: the tax summary lists it
apart (`TaxSummary::form_4797`), at its book amount, with no tax amount
and in no total, on every return.
**Enforced:** `tax::needs_category`, `account_needs_category`,
`uncategorized_accounts`, `CATEGORIES`; `Ledger::check_tax_category_required`,
`check_tax_category_kept` (commands only, never `set_account_tax`, which
replay calls); `settings::TaxCategoryPolicy`, `Settings::require_tax_category`;
`Store::add_account_with_tax`, `add_account`, `set_account_tax`
(journal feature `require_tax_category`, declared before the first
`settings.set` naming the key); `ledger-mcp` `books.add_account`,
`books.import_chart_csv`, `books.digest`; `ledger-gui` Needs attention
(Info under warn, Todo under refuse). **Checked:** `ledger-core`
`tests/tax.rs`; `ledger-store` `tests/store.rs`
(`prop_tax_policy_refuses_exactly`, `a_journaled_clear_replays_under_refuse`);
`ledger-mcp` `tests/spec_tax_policy.rs`; `ledger-gui` `tests/view_model.rs`
(`needs_attention_flags_every_uncategorized_pl_account_defect_8`);
bookspec `examples/tax-category-required.toml`.
## B-33 A package as of a day shows the books through that day
**Status:** CONVENTION (a report as of a day reports the books on that
@@ -1447,4 +1644,5 @@ appendix: the file line by line and its changes, derived by hand);
`ledger-mcp` `oracle_mcp.rs`
(`iif_declares_accounts_by_role_and_balances_every_transaction`,
`iif_without_as_of_is_as_of_today_and_ties_to_the_trial_balance`),
`mcp_stdio.rs` (`mcp_import_export_over_stdio`).
`mcp_stdio.rs` (`mcp_import_export_over_stdio`,
`the_default_period_never_shortens_the_iif_or_the_package`).
+63
View File
@@ -350,6 +350,65 @@ SHA-256 of the retrieved file (`i1120s.pdf`): `eab655cff9226dbc6d80213350bac0500
> Enter only taxable portfolio interest on this line. Taxable interest is interest from all sources except interest exempt from tax and interest on tax-free covenant bonds.
---
## Gains and losses on business property (Form 4797), and tax-exempt income
Governs `form_4797_gain_loss` and `tax_exempt_income` (added in P3, defect 8, 2026-10-20: under `policy.require_tax_category = refuse` every revenue and expense account needs a category that fits it, and the gain or loss account `books.dispose_asset` takes had none). Each file below was retrieved again on 2026-10-20 and is byte for byte the one quoted above (its SHA-256 is the same); only the passages are new. Captions as above.
**Form 1065 (2025), Schedule K** (Source: <https://www.irs.gov/pub/irs-pdf/f1065.pdf>; Retrieved: 2026-10-20; SHA-256 `0f19f556e12ef53c41ba27e5930b4373103f3abcf64693c4ea686451b2a8f56f`)
> 10 Net section 1231 gain (loss) (attach Form 4797)
>
> 18a Tax-exempt interest income
>
> b Other tax-exempt income
>
> c Nondeductible expenses
**Form 1120-S (2025), Schedule K** (Source: <https://www.irs.gov/pub/irs-pdf/f1120s.pdf>; Retrieved: 2026-10-20; SHA-256 `c4b7cae7f2283a2e3225523ea001a710109fee34aad909461421025b287cd18d`)
> 9 Net section 1231 gain (loss) (attach Form 4797)
>
> 16a Tax-exempt interest income
>
> b Other tax-exempt income
>
> c Nondeductible expenses
**Form 1120 (2025), Schedule K item 9 and Schedule M-1 line 7** (Source: <https://www.irs.gov/pub/irs-pdf/f1120.pdf>; Retrieved: 2026-10-20; SHA-256 `016c7fb4042a565c23a98014b17019af7ddc89e3260dd3f926b84d6688b7be6e`)
> 9 Enter the amount of tax-exempt interest received or accrued during this tax year
>
> 7 Income recorded on books this year not included on this return (itemize): Tax-exempt interest $
**Instructions for Form 1065 (2025), Line 6 and Tax-exempt income, in part** (Source: <https://www.irs.gov/pub/irs-pdf/i1065.pdf>; Retrieved: 2026-10-20; SHA-256 `f4a35a24c7abac486908d3610754397a66b2130f0eb425dda897d78a9d91b786`)
> Line 6. Net Gain (Loss) From Form 4797
>
> Caution: Include only ordinary gains or losses from the sale, exchange, or involuntary conversion of assets used in a trade or business activity.
> Tax-exempt income. Don’t include any tax-exempt income on lines 1a through 8. A partnership that receives any tax-exempt income other than interest, or holds any property or engages in any activity that produces tax-exempt income, reports this income on Schedule K, line 18b, and in box 18 of Schedule K-1 using code B.
>
> Report tax-exempt interest income, including exempt-interest dividends received as a shareholder in a mutual fund or other RIC, on Schedule K, line 18a, and in box 18 of Schedule K-1 using code A.
**Instructions for Form 1120-S (2025), Line 4 and Tax-exempt income, in part** (Source: <https://www.irs.gov/pub/irs-pdf/i1120s.pdf>; Retrieved: 2026-10-20; SHA-256 `eab655cff9226dbc6d80213350bac050009ba90daf621c182d31c541bc62c3f2`)
> Line 4. Net Gain (Loss) From Form 4797
>
> Caution: Include only ordinary gains or losses from the sale, exchange, or involuntary conversion of assets used in a trade or business activity.
> Tax-exempt income. Don’t include any tax-exempt income on lines 1a through 5. A corporation that receives any tax-exempt income other than interest or holds any property or engages in any activity that produces tax-exempt income reports this income on Schedule K, line 16b, and in box 16 of Schedule K-1 using code B.
>
> Report tax-exempt interest income, including exempt-interest dividends received as a shareholder in a mutual fund or other regulated investment company, on Schedule K, line 16a, and in box 16 of Schedule K-1 using code A.
**Instructions for Form 1120 (2025), Line 5, in part** (Source: <https://www.irs.gov/pub/irs-pdf/i1120.pdf>; Retrieved: 2026-10-20; SHA-256 `49476a6f69a38a0fdb96fea1d66ddc936404e9b2eebb249da8e20830b0743358`)
> Report tax-exempt interest on Schedule K, item 9. Also, if required, include the same amount on Schedule M-1, line 7, or Schedule M-3 (Form 1120), Part II, line 13, if applicable.
**Instructions for Schedule C (2025), Other Schedules and Forms You May Have To File, in part** (Source: <https://www.irs.gov/pub/irs-pdf/i1040sc.pdf>; Retrieved: 2026-10-20; SHA-256 `b5536db91498a450572e37078d7d5ee8fdc400e385d4fee3a26c230a33e399b6`)
> • Form 4797 to report sales, exchanges, and involuntary conversions (not from a casualty or theft) of trade or business property.
## Mapping (wanderbooks, derived)
**This section is derived, not quoted.** It is wanderbooks' reading of the 2025 forms and instructions above, written 2026-10-13, for the categories in `crates/ledger-core/src/tax.rs`. It is not tax advice; the CPA decides the return. "—" means the category does not apply to that form (per `TaxCategory::forms`). "Not page 1" means the quoted page-1 lines have no place for it; where it goes instead (for example Schedule K) is not quoted in this file and is left to the CPA.
@@ -359,7 +418,9 @@ SHA-256 of the retrieved file (`i1120s.pdf`): `eab655cff9226dbc6d80213350bac0500
| `gross_receipts` | 1 | 1a | 1a | 1a |
| `returns_allowances` | 2 | 1b | 1b | 1b |
| `interest_income` | 6 for interest on notes and accounts receivable (instructions, line 6); other interest not determined here | Not page 1: Schedule K line 5, portfolio interest (quoted above) | Not page 1: Schedule K line 4, portfolio interest (quoted above) | 5 |
| `form_4797_gain_loss` | Not on Schedule C: Form 4797 (instructions, quoted above) | 6 (the ordinary part, from Form 4797); net section 1231 gain on Schedule K line 10 (quoted above) | 4 (the ordinary part); net section 1231 gain on Schedule K line 9 (quoted above) | 9 |
| `other_income` | 6 | 7 | 5 | 10 |
| `tax_exempt_income` | — | Not page 1: Schedule K line 18a (interest) or 18b (other) (quoted above) | Not page 1: Schedule K line 16a or 16b (quoted above) | Not page 1: Schedule K item 9 and Schedule M-1 line 7, tax-exempt interest (quoted above) |
| `cost_of_goods_sold` | 4 (from Part III, line 42) | 2 | 2 | 2 |
| `advertising` | 8 | 21 (no advertising line) | 16 | 22 |
| `bad_debts` | 27b via Part V (no line in Part II; instructions list bad debts under Part V) | 12 | 10 | 15 |
@@ -395,3 +456,5 @@ Notes on the derivation:
- Form 1065 line 13 "Rent" and Forms 1120-S line 11 / 1120 line 16 "Rents" do not split equipment from property; Schedule C line 20 does.
- Schedule C has no line for bad debts or for organizational and start-up costs; the Part V instructions quoted above put them in Other expenses (line 48 to line 27b).
- Schedule C line 30 (business use of home) and Form 1065 line 20 / 1120-S line 19 / 1120 line 25 (energy efficient commercial buildings) have no wanderbooks category.
- `form_4797_gain_loss` (2026-10-20) is the book gain or loss on business property. Form 4797 figures the tax gain from the tax basis (book and tax depreciation differ), and splits ordinary gain (recapture) from section 1231 gain; the CPA does both, and the split decides the line (the row above). So, unlike `depreciation` (on page 1 of every return, only its amount the CPA's), the tax summary can't place it: it lists it apart (`form_4797`), at its book amount, with no tax amount and outside income and both nets, on every return alike. A Form 4797 gain or loss never goes on Schedule C (the Schedule C instructions send sales of business property to Form 4797).
- `tax_exempt_income` (2026-10-20) is income at 0%: none of it is taxable, so it appears in the summary's book amounts and not in its tax amounts, the income twin of `nondeductible`. It is for the entity returns; a sole proprietor's tax-exempt interest is not business income.
+187
View File
@@ -1050,6 +1050,193 @@ the end.
(plan P3 accept). Likely equivalent, to adjudicate: `min` against `max`
in `take` where both bounds coincide, and the kind order's tie-breaks.
## P3: rename, defaults and tax categories (2026-10-20)
Defects 15, 16 and 8 (`plan/served.md` P3; rules.md B-30, B-31, B-32).
Every fixture is fictional (Fernhill Labs LLC, its members and vendors);
no mutation run (D-33): the targets are listed for P12 below.
**Named regressions**, from the brief's reproductions:
- `ledger-mcp` `tests/oracle_mcp.rs`: `show_refuses_start_after_end_defect_15`
(1.1.1-1.1.2: refused, nothing reaches the viewer),
`show_refuses_a_derived_inverted_range_defect_15` (1.1.3: a start after
today with no end, the message saying the end was defaulted),
`show_sends_the_dates_it_names_defect_16` (1.1.4: only an end covers its
fiscal year to it; "as of" and "for" wording; the viewer gets the pair
`shown` names), `show_refuses_options_its_target_ignores_defect_15`, and
`show_writes_the_viewer_command_for_each_target` (resolved dates; a
split never compared).
- `ledger-mcp` `tests/mcp_stdio.rs`: `one_default_period_everywhere_defect_16`
(a July fiscal year, a first entry the year before: P&L, by class, cash
flow, the exported tax summary, members' equity, general ledger and
journal (csv, json: the period's entries only) and the CPA package all
give 2025-07-01 through 2026-03-31 for that end; with no dates every one
gives today's fiscal year to date, the clock read before and after, R14),
`rename_account_end_to_end_defect_15` (the chart, a past period's P&L,
the IIF's full names, verify, two journal lines, refusals write nothing,
a restart keeps the name); `mcp_period_statements_over_stdio`'s old pin
(no start = the first entry) now pins the fiscal year (02-01) that holds
the end.
- `ledger-mcp` `tests/spec_tax_policy.rs`: `add_account_warns_without_tax_category_defect_8`
(1.4.1), `require_tax_category_refuses_defect_8` (refused without,
created with, the import refused whole, clearing refused, a bill's 6999
created and named in its warnings and the digest),
`a_new_account_named_like_a_sibling_is_refused`.
- `ledger-gui` `tests/view_model.rs`: `needs_attention_flags_every_uncategorized_pl_account_defect_8`
(1.4.2-1.4.3: unused and last-year-only accounts listed, headers and
categorized ones not; Info under warn, Todo under refuse), and
`the_overview_numbers_are_the_books_numbers` now pins that the viewer's
default report range, its register's default and the server's resolver
agree on random books and fiscal years.
- `ledger-gui` `tests/show.rs`: `show_never_swaps_an_inverted_target_defect_15`,
`show_report_with_only_end_uses_that_years_start_defect_16`,
`show_all_dates_is_one_click_from_a_shown_register` (review R4).
- `ledger-store` `tests/store.rs`: `rename_account_changes_only_the_name_defect_15`
(every figure, entry and earlier journal line, every projection row but
`accounts.name`; refusals append nothing; reopen, rebuild and viewer),
`a_journaled_rename_replays_past_the_command_rules` and
`a_journaled_clear_replays_under_refuse` (B-28's pairs, review R5),
`the_tax_category_policy_refuses_only_what_it_says`,
`books_written_under_p3_keep_their_names` (fixture `old-books/p3_rename`,
written once by `write_p3_rename_fixture`, `--ignored`; its figures are
also in `books_an_earlier_version_wrote_open_to_its_figures`).
- `ledger-store` `tests/format.rs`: `p3_features_are_declared_once_before_the_first_line_that_needs_them`,
`an_unknown_feature_before_a_rename_is_newer_at_the_declaration`.
**Properties:** `prop_period_defaults_resolve` (`ledger-core`, 5,000
cases: every fiscal year start replay admits, every today and every mix of
given and omitted dates, the calendar's edges weighted);
`prop_rename_changes_no_figure` and `prop_tax_policy_refuses_exactly`
(`ledger-store`, `STORE_CASES`, 64 by default); the refusal law
(`refused_commands_never_reach_the_journal`) gains renames, accounts added
with a category, and the policy turned on and off; the core model
(`tests/model.rs`) gains `Cmd::Rename`, its model keeping a name per
account.
**Unit tests:** `calendar.rs` (each `PeriodDefaults` invariant and scenario:
fiscal years 01-01, 07-01, 12-01 and a lenient 07-15; the start day and
the day before; year 1 under a July year, clamped; 9999-12-31; a one-day
period; a start one day late; no month-day or chrono date panics);
`settings.rs` (`the_tax_category_policy_is_warn_or_refuse`); `ledger-config`
`codec.rs` (`the_tax_category_policy_round_trips`); `ledger-core`
`tests/spec_ledger.rs` (`rename_account_changes_only_the_name`,
`rename_account_refusals_leave_the_books_unchanged`,
`a_closed_period_or_a_role_never_blocks_a_rename`,
`a_journaled_rename_replays_past_the_command_rules`,
`a_new_account_named_like_a_sibling_or_holding_a_tin_is_refused`,
`a_rename_reason_is_bounded_text_without_a_tin`) and `tests/tax.rs`
(`needs_category`'s and the policy's truth tables, the catalog fitting
every account a tool requires on every return, tax-exempt income at 0%);
`ledger-mcp` `server.rs` `period_tools_describe_the_one_default` (every
report tool's description ends with its kind's sentence and each date
parameter is the shared constant; "all time", "beginning of time" and
"first entry" are gone from every tool and schema; the resources say
their period) and `show_reads_each_reports_dates_as_the_viewer_does`;
`handbook.rs` (the period rule rendered from the same constant);
`hostile_args.rs` `hostile_renames_ranges_and_categories_are_answered`.
Test fixtures that gave two accounts one name under one parent
(`ledger-core` `invariants.rs` and `spec_roles.rs`, the store's refusal
law) now name each for its code: the sibling rule is the commands', and
existing charts and journals are untouched (replay takes any name). The
viewer's `spec_gui.rs` small books give their one expense account a
category, so "empty books have nothing" still holds now that an unused
uncategorized expense account is listed.
**Adversarial review (2026-10-21).** Each finding has a test that failed
before its fix (checked by breaking the code and watching it fail):
- `ledger-core` `tests/tax.rs` `a_gain_on_business_property_is_figured_on_form_4797_on_every_return`,
`ledger-import` `tests/spec_import.rs` `tax_summary_lists_form_4797_apart`,
`ledger-mcp` `tests/spec_tax_policy.rs` `a_gain_on_business_property_is_exported_apart`:
a `form_4797_gain_loss` amount was ordinary income on every return
(Schedule C included, whose instructions send it to Form 4797; on a
partnership net section 1231 gain is Schedule K line 10), its Tax column
the book gain; it is now `TaxSummary::form_4797`, book only, in no
total.
- `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`,
`hostile_args.rs` (the 100,000-character name now refused): a new
account's name was journaled untrimmed while a rename's was trimmed,
and names had no bound and could hold a line break or tab.
- `ledger-gui` `tests/view_model.rs` `all_dates_holds_an_entry_dated_after_today`:
"Show all dates" and "All time" ended today, leaving out entries dated
after it.
- `ledger-mcp` `tests/oracle_mcp.rs` `show_refuses_options_its_target_ignores_defect_15`
(a split past 240 columns): `books.show` sent a split the viewer shows
as one total.
- `ledger-mcp` `tests/mcp_stdio.rs` `rename_says_what_the_new_name_moves_defect_15`:
the notes for the name-picked cash and card sets and conventions had no
test of their own.
- `ledger-store` `tests/store.rs` `rename_account_changes_only_the_name_defect_15`
now reads the projection's renamed name itself (its comparison blanked
that column); `tests/format.rs` lost a check that recomputed the gate's
answer itself.
**Landing on main (2026-10-21),** beside odd cents, the IIF and the
cash flow statement:
- `ledger-mcp` `tests/mcp_stdio.rs` `the_default_period_never_shortens_the_iif_or_the_package`:
on a July fiscal year with entries before it, the general journal asked
without dates is the fiscal year to date, while the IIF without dates
holds every posted entry from the first through today under
`general_journal`, `journal` and any capitals, refuses `start` and
`end`, and the CPA package's IIF holds the entry before the package's
fiscal year.
- `ledger-core` `tests/bookspec.rs`: the three examples are in the
loader's one shape, and the loader gained what they need (bookspec
`README.md`): `setup.today` and omitted dates resolved by B-30,
`header` and `tax_category` on an account, a `bill` event, `[[changes]]`
(a rename, the policy) made after an event's expectations, the queries
`period`, `day`, `calendar_year`, `account_names` and
`uncategorized_accounts`, and the commands `rename_account`,
`add_account` and `set_tax_category`, tried on a copy and accepted or
refused for a named reason. Each new key has a refusal test
(`a_period_other_than_the_rules_is_refused`,
`a_refusal_for_another_reason_is_refused`,
`an_omitted_date_without_today_is_refused`,
`account_names_leaving_an_account_out_is_refused`,
`a_change_is_made_after_its_events_expectations`,
`a_command_runs_under_the_policy_in_force`,
`a_command_with_values_is_refused`).
- `ledger-import` `tests/spec_import.rs`: the IIF's tests
(`iif_names_never_clash_and_every_change_is_listed`,
`iif_distinct_accounts_never_share_a_name`,
`iif_writes_plain_ascii_and_lists_every_change`,
`iif_warnings_never_hold_a_tin`) make their siblings of one name, names
with a tab and names holding a TIN by replay (`replay_add_account`), as
books written before P3 hold them: B-31 refuses them in a new name, and
the file must still keep them apart, write them on one line and mask
them.
- `books.cash_flow`'s description is the direct method's, shortened to
fit the 700-character cap with its dates sentence
(`tool_descriptions_fit_700_characters`,
`period_tools_describe_the_one_default`).
**For P12** (D-33; none run): `ledger-core` `calendar::{fiscal_year_start_unclamped,
fiscal_year_unclamped, fiscal_year_start_on, period_defaults}`,
`PeriodDefaults::{statement, statement_range, as_of, calendar_year}`,
`PeriodKind::describe`, `Company::fiscal_year_containing`,
`ChartOfAccounts::set_name`, `Ledger::{add_account (its new name checks),
add_control_account, check_account_name, rename_account,
check_rename_account, replay_rename_account, check_rename_reason,
check_tax_category_required, check_tax_category_kept}`,
`tax::{needs_category, account_needs_category, uncategorized_accounts,
on_form_4797}` and `tax_summary`'s Form 4797 section,
`Settings::require_tax_category`; `ledger-store` `Store::{ensure_feature,
ensure_features, journal_settings (its feature), add_account,
add_account_with_tax, journal_account_added, ensure_control_account,
rename_account, set_account_tax, journal_account_tax}`, the
`account.renamed` replay and projection arms, `format::setting_feature`,
`ops::report_columns`;
`ledger-package` `statement_range`, `as_of_date`, `pnl_statement`,
`bs_statement`, `cash_flow_statement`, `year_earnings`, `build_package`'s
fiscal year; `ledger-mcp` `show` (the option refusals, the resolve, the
`shown` wording), `show_report_dates`, `rename_account` (its notes),
`add_account`, `import_chart_csv` (both loops), `profit_and_loss_by_class`'s
range, `export_journal`'s period, `described`, `uncategorized_warning`,
`created_uncategorized`, the digest's `uncategorized_accounts`,
`handbook::period_rule`. The viewer is not mutated (owner, 2026-10-18):
`view_model.rs` and `show.rs` cover it.
## Benchmarks (baseline 2026-10-13)
No budget, by decision (the owner, 2026-10-13: the books do little work
+12 -2
View File
@@ -169,9 +169,19 @@ sequence of commands, of any length**:
checked by `settings_replay_is_pure`, which snapshots every figure and
entry around each record (rules.md B-29). Settings that change how the
same entries are presented (the fiscal year, cash flow roles, members)
are outside this law by design.
are outside this law by design. A rename (`rename_account`, P3,
rules.md B-31) is a record too: the model's chart is `(code, type)`
(`Ledger.lean`), names are not modelled, so a rename is the identity
on the model and needs no new `Step` (plan I-13 is met as P1 met it
for records). The engine's side is the property
`prop_rename_changes_no_figure` (ledger-store `tests/store.rs`: every
statement amount, entry and earlier journal line unchanged, live
equals replay, the projection unchanged but `accounts.name`), and the
stateful model `tests/model.rs` keeps a name per account so engine
and model agree on every rename's refusal and nothing else moves.
- Lean `Step.record_changes_no_figure`, `Reachable.records` (no
axioms at all) · property `settings_replay_is_pure`
axioms at all) · property `settings_replay_is_pure` ·
`prop_rename_changes_no_figure` · model `Cmd::Rename`
### Closing