config, mcp, gui: specs from sources, spec-blind suites, and the bugs they found
check / check (push) Canceled after 0s

ledger-config: parse gets a sourced spec. A fiscal year starts on the
first of a month (IRS Pub. 538); tax_classification is read by whole
words (Treas. Reg. 301.7701-3, Schedule C instructions; "business corp"
no longer reads as an S corp, "corporation or S corporation" is refused);
duplicate or blank members and shared capital accounts are refused; a
negative or non-grammatical receipt threshold is refused; codes trimmed.
tests/spec_config.rs (65 spec-blind tests, 14 properties), fuzz target
fuzz_config_parse (weekly). Mutation: 55 of 55 viable caught (from 25/38).

The receipt policy moves to ledger-config, with one loader
(effective_receipt_policy) and one decimal grammar, so the server and
the viewer enforce and show the same rule (the viewer had its own: only
expense-like debits, a silent $25 default, and no config.json).

ledger-mcp: a module spec (MCP 2025-06-18 tools, JSON-RPC 2.0, the
operating contract). Amounts in are exactly -?digits[.digits]; a number
that is one account's code and another's id (vendors, classes alike) is
refused, not guessed; the threshold is validated at startup. Fixed:
add_line with a bad class journaled the line before refusing;
profit_and_loss_by_class ignored `class`; aging buckets said "0". A wire
in front of rmcp answers malformed JSON with -32700 and invalid requests
with -32600 and their id (rmcp dropped the first, omitted the id).
tests/spec_mcp.rs (26 spec-blind tests: every tool's result against its
own outputSchema, a random-call state machine checking the journal is
untouched by every refusal, amount grammar, previews, the receipt policy).

ledger-gui: money, accounting, and dollars never round (they rounded to
cents, banker's style); export file names avoid Windows device names,
trailing dots and spaces, empty names, and 255-byte overflow; specs on
attention, history, fuzzy search, and axis ticks.

ledger-core: the lifetime profit_and_loss is renamed unclosed_earnings
(docs/testing.md #3).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GWVo7x3UHw5kwCUaWxiejX
This commit is contained in:
swaitsandClaude Opus 5.5 committed 2026-10-14 10:13:05 -06:00
1 parent 5b8910a1cd
commit 3f20df9700
42 files changed
+4636 -363

No files matched your search

+2 -2
View File
@@ -16,7 +16,7 @@ private, and constructors are `pub(crate)` where it matters. This is what
makes the ledger auditable and what will make the future oracle harness
trustworthy — the harness only needs to drive the command surface.
Reports (`trial_balance`, `balance_sheet`, `profit_and_loss`, `snapshot`)
Reports (`trial_balance`, `balance_sheet`, `unclosed_earnings`, `snapshot`)
are free functions: pure, total, and deterministic. Same ledger in, same
report out, every time, on every platform. That determinism is the entire
basis of `docs/oracle.md`.
@@ -31,7 +31,7 @@ crates/ledger-core/
account.rs # AccountType, Side, Account, ChartOfAccounts
journal.rs # JournalLine, JournalEntry, validate()
ledger.rs # Ledger: the command surface, append-only log
reports.rs # trial_balance, profit_and_loss, balance_sheet, snapshot
reports.rs # trial_balance, unclosed_earnings, balance_sheet, snapshot
tests/
invariants.rs # accounting-law tests + randomized property test
golden/ # hand-verified expected report snapshots
+10 -10
View File
@@ -85,14 +85,13 @@ below.
- `books.draft_entry { date: "2026-10-10", description: "..." }`
→ `{ entry_id, status: "draft" }`.
- `books.add_line { entry_id, account: "1000" | <id>, side: "debit" | "credit",
amount: "123.45" }`
→ `{ lines: [...], totals: { debits, credits, balanced } }`. Running totals
after every line: the agent watches the entry balance in real time.
- `books.post_entry { entry_id, confirmed }` → without confirmation:
`{ preview: { lines, resulting_balances, receipts: { attached: [...],
required_but_missing: bool } } }`; with confirmation: validates (including
the receipt policy), posts, returns `{ entry, balances_changed: [{ code,
before, after }] }`.
amount: "123.45" }` → `{ ok: true }`. A refused line (unknown account or
class, bad amount) adds nothing.
- `books.post_entry { entry_id, confirmed }` → `{ status, entry_id, date,
description, total, lines (with resulting balances), receipts,
receipt_required, receipt_missing, split_detected }`. Without
`confirmed: true`, `status` is `"preview"` and nothing changes; with it,
the receipt policy is enforced and `status` is `"posted"`.
- `books.reverse_entry { entry_id, date, description, confirmed }` → same
preview/confirm pattern as post.
- `books.get_entry { entry_id }` → full entry with lines.
@@ -492,8 +491,9 @@ 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 }` → per-class revenue,
expenses, net income, plus an unclassified row. Reversals carry their
- `books.profit_and_loss_by_class { start, end, class? }` → 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.
## Tools — CPA handoff
+34
View File
@@ -0,0 +1,34 @@
# File names the viewer writes
Governs `ledger_gui::export::file_name`. Fetched 2026-10-14.
## Microsoft, "Naming Files, Paths, and Namespaces" (learn.microsoft.com/en-us/windows/win32/fileio/naming-a-file)
> Use any character in the current code page for a name, including
> Unicode characters and characters in the extended character set
> (128–255), except for the following: The following reserved characters:
> < (less than) > (greater than) : (colon) " (double quote) / (forward
> slash) \ (backslash) | (vertical bar or pipe) ? (question mark) *
> (asterisk) Integer value zero, sometimes referred to as the ASCII NUL
> character. Characters whose integer representations are in the range
> from 1 through 31, except for alternate data streams where these
> characters are allowed.
> Do not use the following reserved names for the name of a file: CON,
> PRN, AUX, NUL, COM1, COM2, COM3, COM4, COM5, COM6, COM7, COM8, COM9,
> COM¹, COM², COM³, LPT1, LPT2, LPT3, LPT4, LPT5, LPT6, LPT7, LPT8, LPT9,
> LPT¹, LPT², and LPT³. Also avoid these names followed immediately by an
> extension; for example, NUL.txt and NUL.tar.gz are both equivalent to
> NUL.
> Do not end a file or directory name with a space or a period. Although
> the underlying file system may support such names, the Windows shell and
> user interface does not. However, it is acceptable to specify a period
> as the first character of a name.
## POSIX (IEEE Std 1003.1-2017, `<limits.h>`)
`{NAME_MAX}`: "Maximum number of bytes in a filename (not including the
terminating null of a filename string)"; Linux's common file systems set
it to 255, and NTFS limits a name to 255 UTF-16 units. wanderbooks keeps a
name within 255 bytes of UTF-8, which fits both.
@@ -0,0 +1,52 @@
# Entity classification: which return a business files
Governs `ledger_config`'s reading of `[company] tax_classification`.
Fetched 2026-10-14.
## Treas. Reg. §301.7701-3 (eCFR, via law.cornell.edu/cfr/text/26/301.7701-3)
**(a) In general**
> A business entity that is not classified as a corporation under
> § 301.7701-2(b) (1), (3), (4), (5), (6), (7), or (8) (an eligible
> entity) can elect its classification for federal tax purposes as
> provided in this section. An eligible entity with at least two members
> can elect to be classified as either an association (and thus a
> corporation under § 301.7701-2(b)(2)) or a partnership, and an eligible
> entity with a single owner can elect to be classified as an association
> or to be disregarded as an entity separate from its owner.
**(b)(1) Domestic eligible entities**
> Except as provided in paragraph (b)(3) of this section, unless the
> entity elects otherwise, a domestic eligible entity is— (i) A
> partnership if it has two or more members; or (ii) Disregarded as an
> entity separate from its owner if it has a single owner.
## Instructions for Schedule C (Form 1040) (2025), "Single-member limited liability company (LLC)"
> Generally, a single-member domestic LLC is not treated as a separate
> entity for federal income tax purposes. If you are the sole member of a
> domestic LLC, file Schedule C (or Schedule E or F, if applicable) unless
> you have elected to treat the domestic LLC as a corporation.
## The returns (titles, from irs-tax-lines.md)
- Form 1065, U.S. Return of Partnership Income: a partnership.
- Form 1120-S, U.S. Income Tax Return for an S Corporation: an S
corporation.
- Form 1120, U.S. Corporation Income Tax Return: a corporation (an
association included) that is not an S corporation.
## Derived (wanderbooks)
| classification says | return |
|---|---|
| partnership | Form 1065 |
| S corporation (S corp, S-corp) | Form 1120-S |
| C corporation; corporation, corp, or association alone | Form 1120 |
| disregarded entity, sole proprietor(ship), single-member | Schedule C |
A phrase naming two returns ("corporation or S corporation") and anything
else names no return wanderbooks can be sure of, so it is
refused rather than guessed.
+36
View File
@@ -0,0 +1,36 @@
# JSON-RPC 2.0 Specification (jsonrpc.org/specification)
Governs `ledger-mcp`'s wire protocol (MCP is JSON-RPC 2.0). Fetched
2026-10-14.
**5 Response object**
> When a rpc call is made, the Server MUST reply with a Response, except
> for in the case of Notifications. [...]
>
> result: This member is REQUIRED on success. This member MUST NOT exist
> if there was an error invoking the method. [...]
>
> error: This member is REQUIRED on error. This member MUST NOT exist if
> there was no error triggered during invocation. [...]
>
> id: This member is REQUIRED. It MUST be the same as the value of the id
> member in the Request Object. If there was an error in detecting the id
> in the Request object (e.g. Parse error/Invalid Request), it MUST be
> Null.
>
> Either the result member or error member MUST be included, but both
> members MUST NOT be included.
**5.1 Error object**
> code: A Number that indicates the error type that occurred. This MUST be
> an integer. [...]
>
> | code | message | meaning |
> |---|---|---|
> | -32700 | Parse error | Invalid JSON was received by the server. |
> | -32600 | Invalid Request | The JSON sent is not a valid Request object. |
> | -32601 | Method not found | The method does not exist / is not available. |
> | -32602 | Invalid params | Invalid method parameter(s). |
> | -32603 | Internal error | Internal JSON-RPC error. |
+47
View File
@@ -0,0 +1,47 @@
# Model Context Protocol: tools (specification 2025-06-18)
Governs `ledger-mcp`'s tool results and errors. Source:
https://github.com/modelcontextprotocol/modelcontextprotocol,
docs/specification/2025-06-18/server/tools.mdx (MIT). Fetched 2026-10-14.
**Structured Content**
> **Structured** content is returned as a JSON object in the
> `structuredContent` field of a result.
>
> For backwards compatibility, a tool that returns structured content
> SHOULD also return the serialized JSON in a TextContent block.
**Output Schema**
> Tools may also provide an output schema for validation of structured
> results. If an output schema is provided:
>
> - Servers **MUST** provide structured results that conform to this schema.
> - Clients **SHOULD** validate structured results against this schema.
**Error Handling**
> Tools use two error reporting mechanisms:
>
> 1. **Protocol Errors**: Standard JSON-RPC errors for issues like:
> - Unknown tools
> - Invalid arguments
> - Server errors
>
> 2. **Tool Execution Errors**: Reported in tool results with `isError: true`:
> - API failures
> - Invalid input data
> - Business logic errors
**Security Considerations**
> 1. Servers **MUST**:
> - Validate all tool inputs
> - Implement proper access controls
> - Rate limit tool invocations
> - Sanitize tool outputs
(A local stdio server for one bookkeeper: access control is the file
system's, and rate limiting has no one to limit; wanderbooks takes
"validate all tool inputs" and "sanitize tool outputs" as binding.)
+1 -1
View File
@@ -83,7 +83,7 @@ accounting question), CHANGE (a source says otherwise; work pending).
|---|---|---|---|
| 1 | A P&L whose start is after its end is refused | DESIGN | No standard governs a reversed range; refusing beats silently swapping a typo |
| 2 | A balance sheet omits zero-balance accounts | CONFIRMED | Reg. S-X 210.4-03(a): "No caption should be shown ... as to which the items and conditions are not present." A comparative statement must still show a zero beside a non-zero prior column |
| 3 | The lifetime `profit_and_loss` counts closing entries | CONFIRMED | Used as earnings not yet closed on the balance sheet; closed income is already in capital (OpenStax Principles of Financial Accounting 5.1). Its name misleads: rename to unclosed earnings (pending) |
| 3 | The lifetime `profit_and_loss` counts closing entries | CONFIRMED | Used as earnings not yet closed on the balance sheet; closed income is already in capital (OpenStax Principles of Financial Accounting 5.1). Renamed `unclosed_earnings` (2026-10-14) |
| 4 | Closing a period with nothing to close still locks it | CONVENTION | A close is a lock (B-12); a dormant month must still lock and count for close-in-order |
| 5 | Draws and distributions are closed into capital at period end | CONFIRMED | OpenStax 5.1: dividends "are closed to the permanent account"; drawing accounts close to owner's capital. Monthly closes mean the year's K-1 Item L withdrawals sum across months |
| 6 | Reconciliation balances | CONFIRMED | Signed by the account's normal side (a card reads as owed), as statements show it; `reconciling_a_card_reads_its_normal_side_...` |