P3: rename accounts, one set of period defaults, tax categories warn or refuse (defects 15, 16, 8)
check / check (push) Canceled after 0s
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:
69 files changed
+8063
-570
No files matched your search
+123
-26
@@ -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
@@ -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`).
|
||||
@@ -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
@@ -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
@@ -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
|
||||
|
||||
|
||||
Reference in new issue
Block a user