Projects: classes as entities, and the sidebar fits short windows
check / check (push) Canceled after 0s

A project is a class with facts (description, status, kind, party,
dates, links, notes) in one project.set event behind the format
feature projects. Four tools (books.add_project, update_project,
projects, project); documents and decisions link to projects; B-37:
on-spec costs are expensed as incurred, repayment is revenue, and the
unrecovered cost is never below zero (Kani). Viewer: a Projects list
and a project page with its figures, by month and by account. The
sidebar measures its footer instead of reserving a fixed 172 px.

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-22 03:50:52 -06:00
1 parent 0495726001
commit 321b94562a
71 files changed
+7446 -217

No files matched your search

+5
View File
@@ -35,6 +35,11 @@ Statuses: `covered` (built and tested), `partial` (built, known gaps listed),
- Row 10 (`books.digest`) has no sheet equivalent — the sheet can't tell you
what changed. This is parity *plus*: the new system is more reviewable
than the old one, which is the point of the exercise.
- Projects (rules.md B-37) have no sheet equivalent either: a class with
its facts, created and changed by the agent (`books.add_project`,
`books.update_project`), read by the viewer's Projects pages and by
`books.projects` / `books.project` (D-28: the agent writes, the viewer
reads).
- Row 12 is deliberately excluded. Email filing is a mail problem, not a
books problem; it stays where it is.
+11
View File
@@ -62,3 +62,14 @@ definitions exactly.
reports it: cash plus the adjusted tax basis of property contributed,
plus their share of income as figured for tax, less cash and the adjusted
tax basis of property distributed (Form 1065 instructions, Item L).
- **Project** — A class with its facts (rules.md B-37): what it is, where it
stands (active, paused, finished, abandoned), what kind of work it is
(internal product, client work, on spec, other), who it is for, its
dates and links. Its figures are the revenue and expense lines tagged to
it (job costing).
- **On spec** — Work done hoping to be paid. Its costs are expenses as they
are spent; the hoped-for payment is a gain contingency, recorded neither
as a receivable nor as income until it arrives (ASC 450-30-25-1).
- **Unrecovered cost** — An on-spec project's expenses less its revenue to
date, never below zero: what it would take to be repaid in full. A
figure, never an asset.
+111 -18
View File
@@ -131,7 +131,8 @@ settings-and-conventions section says how to use them.
state ("needs_onboarding" or "active"), settings_source, rules: {
evidence, parties, approval_rules }, settings_summary: [{ key, value,
set_by, seq }], conventions: [{ id, kind, title, summary }], open_items:
[{ id, title, due }], truncated?, handbook_sections, format_upgrade?: {
[{ id, title, due }], projects: [{ id, name, status, kind? }],
truncated?, handbook_sections, format_upgrade?: {
guard, format, adds, how }, next_steps: [{ tool, args?, why }],
isolation }`. The connect text names it as the first call.
`format_upgrade` is there only for books waiting for a format upgrade
@@ -141,6 +142,8 @@ settings-and-conventions section says how to use them.
`isolation` is `same_user (advisory)` (D-32): the books are hidden from
agents in the owner's own account, not locked against them. Lists cut
to fit say so in `truncated` ("conventions: 4 more (books.conventions)").
`projects` lists the active and paused projects by name, at most 20
("projects: N more (books.projects)" past that).
`next_steps`, in order: the format upgrade's (when waiting);
`books.import_settings` (books that still read a settings file); the
onboarding step (`books.handbook {section: "contract"}`, when the chart
@@ -192,9 +195,15 @@ as it is made.
failing midway could leave a partial draft, as for any draft built line
by line).
- `books.record_decision { subject, decision_md, decided_by: [..],
decided_on, evidence_attachment?, supersedes?, citation? }` → `{ id }`;
`books.decisions { include_superseded? }`. A decision supersedes at most
one live decision; its evidence is a document still in the books.
decided_on, evidence_attachment?, supersedes?, citation?, projects? }` →
`{ id }`; `books.decisions { include_superseded?, project? }`. A decision
supersedes at most one live decision; its evidence is a document still
in the books. `projects` (ids or names, each once, resolved before
anything is written) names the projects it is about (rules.md B-37); a
decision without them journals the bytes it always did, and one with
them needs the journal feature `projects`. `books.decisions` gives each
decision's `projects: [{ id, name }]` (left out when none); `project`
filters to the decisions naming one.
- `books.raise_item { title, detail_md?, due?, decided_by, ... }` → `{ id }`,
`books.resolve_item { id, resolution, decided_by, ... }`,
`books.open_items { include_resolved? }`.
@@ -379,11 +388,12 @@ that changes anything also has a `destructiveHint`, true for these 29:
`books.set_tin` (both overwrite a vault record), `books.purge_tin`,
`books.create_invoice`, `books.create_bill`, `books.receive_payment`,
`books.pay_bill`, `books.run_payroll`, `books.remit_payroll_taxes`; and an
`idempotentHint` on the ten that a repeat with the same arguments leaves
`idempotentHint` on the eleven that a repeat with the same arguments leaves
the journal and the vault as they were: `books.verify`,
`books.run_depreciation`, `books.run_schedules`, `books.run_recurring`,
`books.set_party_roles`, `books.set_setting`, `books.import_settings`,
`books.set_vendor_1099`, `books.set_entry_instrument` and
`books.set_vendor_1099`, `books.set_entry_instrument`,
`books.update_project` and
`books.back_up_now` (a setter such as `books.update_party`,
`books.set_fx_rate` or `books.set_tin` journals a line, and `books.set_tin`
seals a new vault record, every time, so none carries it;
@@ -835,12 +845,14 @@ through the policy.
## Tools — the viewer
- `books.show { entry_id | account | report | attachment_id | asset |
party | reconciliation | search | page, start?, end?, columns?, compare? }`
party | project | 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
— parties"), a project (by id or name: viewer target `{show:
"project", class_id}`; page `projects` its list), a reconciliation (by id: its account's page, with its
statements and reconciliations; viewer target `{show:
"reconciliation", id}`), a transactions search, or a page (`parties`
among them). Exactly one target; entries, accounts, attachments,
@@ -1270,11 +1282,12 @@ texts are at most 500 characters, no control characters.
after `books.purge_tin` (P3 defect 11: it read W-9 facts only, so a
member's showed as missing).
- `books.party { party, year?, basis? }` → `{ party, year, totals,
entries, invoices, bills, payroll }`: the facts; the calendar year's
entries, invoices, bills, payroll, projects }`: the facts; the calendar year's
totals (default this year); every entry it is on (drafts included,
newest first); its invoices and bills (`{ id, date, due_date, total,
paid, balance, status, entry_id }`); and the payroll lines naming it
(`{ run_id, date, entry_id, gross_pay, net_pay }`).
(`{ run_id, date, entry_id, gross_pay, net_pay }`); and the projects for
or with it, by name (`{ id, name }`, rules.md B-37).
- `basis` (both): `"cash"` (the default) or `"all"`; any other word is
refused (`basis is "cash" or "all"`). Echoed in the output. Not the
books' accounting method (`books.accounting_basis`).
@@ -1480,7 +1493,8 @@ and the CPA package carries them.
A stored file (a receipt or a document) links to any number of subjects;
a subject is `{ kind, id? }`: `entry` (id), `account` (code or id),
`asset` (tag or id), `party` (id or name), `invoice`, `bill`, `period`,
`asset` (tag or id), `party` (id or name), `project` (id or name: its
contract, spec, pitch; journal feature `projects`), `invoice`, `bill`, `period`,
`payroll_run`, `schedule`, `reconciliation` (ids), or `company` (no id).
Every subject is checked to exist. In outputs a subject is `{ kind, id?,
label }` (`id` absent for the company; for an account, its id, with the
@@ -1504,7 +1518,9 @@ the receipt policy counts it.
- `link_to: [subject]` on `books.attach_receipt` and `books.file_document`
links the new file in the same call, each link its own
`attachment.linked`. All or nothing: every subject is checked (exists,
named once, not the entry it is filed on) before anything is filed.
named once, not the entry it is filed on, its journal feature declared:
a `project` needs `projects`) before anything is filed, so a refusal
leaves no file filed without its links.
## Tools — receipts & documents
@@ -2019,13 +2035,90 @@ entries.
- `books.list_payroll_runs` → `{ runs: [{ id, date, employee_count,
total_gross, entry_id }] }`.
## Tools — classes (divisional tracking)
## Tools — projects (classes)
- `books.add_class { name }` → `{ id }`. E.g. "Rondo", "Coach vs Coach".
- `books.list_classes` → `{ classes: [{ id, name }] }`.
- `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.
A project is a class with its facts (`docs/spec/rules.md` B-37): the
same id, the same name, so `books.add_line`'s `class`, the P&L `class`
filter and `books.profit_and_loss_by_class` take projects. A `project`
reference is an id or a name (ignoring capitals in any script and
surrounding space; in a link's subject a JSON number is always an id); a
number that is one project's id and another's name, which only old books
can hold, is refused, naming what to pass instead. Names are unique,
ignoring capitals and surrounding space, and never digits only (they
would read as an id). A change that keeps a project's own name never
checks it again, so old books holding a duplicate name can change either
project's facts; a rename onto another project's name is refused. Every project write but a name alone needs the journal feature
`projects` (B-27): books waiting for their format upgrade (D-39) can add
a project by name only, and every other project write (facts, a link to
a project, a decision naming one) is refused with `format_upgrade_pending`
until the owner upgrades them.
- `books.add_project { name, description?, status?, kind?, party?,
started_on?, ended_on?, links?, notes? }` (books:write) → the project: `{ id,
name, status, kind, description, party: { party_id, label }?,
started_on, ended_on, links, notes }`. `status`: `active` (default),
`paused`, `finished`, `abandoned`. `kind`: `internal` (the company's own
product), `client` (work for a customer), `on_spec` (work done hoping
to be paid), `other`; null when not recorded. `party`: who it is for or
with (id or name). `ended_on` only when finished or abandoned, never
before `started_on`. `links`: up to 8 web addresses, one line each, no
spaces, none twice (never opened). Text fields: one line (name, links,
at most 200 characters) or long text (description, notes, at most
2,000), never a full TIN. A name alone journals `class.added`; facts add
`project.set` (the whole record).
- `books.update_project { project, name?, description?, status?, kind?,
party?, started_on?, ended_on?, links?, notes? }` (books:write;
idempotentHint) → the
project. Only the fields given change; `""` clears description, kind,
party, started_on, ended_on or notes; `links` replaces the list (`[]`
clears it). At least one field. Every field is checked before anything
is written; then the whole record is journaled as `project.set`, and a
record with nothing new journals nothing. A rename changes the label
every report prints, nothing else (not destructive, as a party's name).
- `books.projects { status? }` (books:read) → `{ through, projects: [{ ...project,
to_date: { through, revenue, expenses, net, unrecovered_cost } }] }`: every
project (every class), by name ignoring capitals; `to_date` counts every
posted revenue and expense line tagged to it through today in the
company's time zone, closing entries left out; `unrecovered_cost` only
for an on-spec project (else null): expenses less revenue, never below
zero.
- `books.project { project, start?, end? }` (books:read; omitted dates: the fiscal
year to date, B-30, as every period report) → `{ project, period: {
start, end, revenue, expenses, net, by_account: [{ code, name, type
("revenue" | "expense"), amount }], by_month: [{ month ("YYYY-MM"),
start, end, revenue, expenses, net }] }, to_date (as above, through
end), entries: [{ entry_id, date, description, status, project_amount,
total, reversal_of?, receipts: [attachment_id] }], documents: [as
books.list_receipts], decisions: [as books.decisions] }`. The figures
are `ledger_core::project_figures`'s, the viewer's: only revenue and
expense lines count (a tagged asset purchase is listed with its entries,
its project amount zero); `by_month` holds the period's last 240 months
at most. `to_date` always counts every tagged line through `end`,
whatever the project's dates: job-to-date beside the period. The on-spec
`unrecovered_cost` is the "if repaid" figure, never a receivable (ASC
450-30-25-1). Entries are newest first, drafts included; decisions
oldest first, superseded ones too.
- `books.add_class { name }` → `{ id }`: a project with only its name;
refuses a name another project has.
- `books.list_classes` → `{ classes: [{ id, name }] }` in id order.
- `books.add_line` takes optional `class` (a project: name or id): tags the
new line. Shared costs are split explicitly across lines at entry time.
Its result gains `note` when the project is finished or abandoned
("project X is finished (ended 2026-09-30): tag a late cost or invoice
to it only if it belongs to it"): tagged anyway.
- `books.profit_and_loss` takes optional `class` (a project: name or id)
to filter to.
- `books.record_payout` lines and adjustments take optional `class` (a
project); its result gains `notes`, one per finished or abandoned
project tagged, as `books.add_line`'s `note`.
- An invoice or a bill takes no project yet: `books.create_invoice` and
`books.create_bill` post their entry at once with untagged lines, and
only a draft's lines take a class (`books.add_line`'s `class`). Link the document to the
project (`books.link_attachment` subject kind `project`); when its lines
must count in the project's figures, book it as a hand-built entry
(`books.draft_entry` + `books.add_line` class) instead, which then is
no invoice or bill in the agings (an open item: tagged document
lines).
- `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
+91 -3
View File
@@ -1374,7 +1374,16 @@ Treas. Reg. §301.6109-4(a)); no party field may hold a nine-digit number,
and the full TIN lives only in the encrypted vault, outside the journal
(DECISION). W-9 facts need a 1099 role (contractor, landlord); W-4 facts
need the employee role; payroll lines that name their employee name a
party with that role.
party with that role. A project's text (its name, description, links and
notes: B-37) holds no full TIN either (`settings::line_text`,
`long_text`). A link is a web address, so its rule reads it as one
(`settings::holds_printed_tin_in_link`, DECISION of this rule, so store
listings can be kept): the printed SSN (3-2-4) and EIN (2-7) forms and a
bare nine digits standing alone are refused, but nine digits joined to a
letter or to `/ = ? & #` are an id in a path or query (an app store
listing's `.../id284882215`) and are kept. A project's name keeps the
plain rule: a nine-digit job or PO number is written by its last four,
as everywhere else in the books.
The full TIN, when the books need it for the 1099 worksheet, lives in the
TIN vault (`vault/` in the books dir, git-ignored; encrypted under a key
@@ -1570,9 +1579,13 @@ B-16; and a `settings.set` naming `books.schedule_convention`, a key an
earlier version refuses), `month_reposting` (a schedule month or a depreciation period
posted again after a reversal un-posted it; an older version would refuse
the second `schedule.posted`, and its projection ignore the second
`asset.depreciation_posted`, B-15) and `asset_basis` (an
`asset.depreciation_posted`, B-15), `asset_basis` (an
`asset.basis_set`, a new event type: an asset's basis facts with the
value at contribution and the §704(c) method, B-25). A command asks for
value at contribution and the §704(c) method, B-25) and `projects` (a
`project.set`, a new event type: a class's project facts; an
`attachment.linked` to the subject kind `project`, which an older version
would call corrupt; a `decision.recorded` with `projects`, which an older
version would drop silently: B-37). A command asks for
the feature it needs (`Store::ensure_feature`) before its first journaled
line, and only when the line it is about to write uses it, so a write
refused for waiting on the upgrade journals nothing, never an entry
@@ -2525,3 +2538,78 @@ package). **Checked:** `ledger-core` `tests/tax_capital.rs`;
(`the_k1_worksheet_reports_tax_basis_capital`), `tests/p3_seams.rs`
(`depreciation_is_one_figure_everywhere`,
`the_cpa_package_as_of_each_date_is_the_tools_figures`).
## B-37 A project is a class with its facts; its figures are its tagged lines
**Status:** DECISION (the owner, 2026-10-21: "a Projects page that treats
a class as a project ... Projects are entities too and can have name,
description, whatever other metadata you think they should have"; the
convention `link-to-projects` in the books: "whenever applicable, we link
things to [projects]"); CONVENTION (job costing, item 3); AUTHORITY (on
spec, item 4: ASC 450-30-25-1; ASC 340-40-25-5 with B-22;
`sources/fasb-asc.md`, both quoted through Deloitte's DART roadmaps and
not yet re-read in the ASC basic view: an open item)
1. **A project is a class.** Same id and name; every class is one (a
class an earlier version wrote has the default facts: active, kind not
recorded). Its facts, the few a job cost sheet carries: a
description (its scope of work), a status (`active`, `paused`,
`finished`, `abandoned`), a kind (`internal`: the company's own
product; `client`: work for a customer; `on_spec`: work done hoping to
be paid; `other`; or not recorded), a party (who it is for or with),
the dates it started and ended (an end only when finished or
abandoned, never before the start; informational: no figure filters by
them), up to eight links (web addresses, never opened) and notes. Names
are one line, unique ignoring capitals (in any script) and surrounding
space, and never digits only (a project is named by id or name, so such
a name would read as an id); no field holds a full TIN (B-26). A
change that keeps a project's own name never checks the name again, so
old books (replay never checks names) holding a duplicate can still
change either project, and a fact-only change never renames. Journaled as `class.added`, then the whole
record as `project.set` when there is more than a name (journal feature
`projects`, B-27). Replay checks only what it can't do without (the
class and the party exist), never the text rules, so a rule tightened
later never breaks books already kept. No budget, estimate, contract
value or percent complete: each is a document linked to the project, a
revenue-recognition question the books don't face, or the CPA's.
2. **Links.** A line is tagged with a project (its class); a receipt or
document is linked to a project (attachment subject `project`); a
decision names the projects it is about. Whether to link is the
company's convention; how is this rule.
3. **Figures (job costing).** For a period: the revenue and expense lines
tagged to it, dated in the period, closing entries left out (B-11),
each on its normal side; net is revenue less expenses. To date: every
such line through the day, from the first day the books keep, whatever
the project's dates. A tagged balance-sheet line counts nothing (an
asset bought for it counts as it is depreciated or expensed, tagged
then). No cost is allocated: a shared cost counts only as split across
lines when it is entered. The projects with the untagged lines
partition the P&L. By month, the period's last 240 months at most.
4. **On spec.** Work done hoping to be paid: its costs are expenses when
incurred (B-22; ASC 340-40-25-5 makes a cost of an anticipated
contract an asset only if, among its criteria, it is expected to be
recovered, which on spec it isn't). The hoped-for payment is a gain
contingency: no receivable and no income until received (ASC
450-30-25-1). A repayment is revenue tagged to the project (a
reimbursement credited to the expense it repays, tagged, comes to the
same). Its unrecovered cost, "if repaid", is expenses less revenue to
date, never below zero: a figure, never an asset.
5. **Status.** Finished and abandoned projects still take lines (a late
cost, a final invoice); `books.add_line` and `books.record_payout` say
so when one is tagged.
6. Omitted dates mean what B-30 says (`books.project`'s period); to date
is always stated as such, beside it.
**Enforced:** `ledger_core::classes::{ProjectFacts, ProjectStatus,
ProjectKind}`, `Ledger::{check_set_project, set_project,
replay_set_project, projects_of_party}`, `reports::{project_figures,
project_to_date, project_entries}`, `kernel::unrecovered_cost`;
`Store::{add_project, set_project, subject_feature}` (feature
`projects`); `books.add_project`, `books.update_project`,
`books.projects`, `books.project`; the viewer's Projects pages.
**Checked:** `ledger-core` `tests/spec_projects.rs`, `ledger-store`
`tests/projects.rs`, `ledger-mcp` `tests/spec_projects.rs`, `ledger-gui`
`tests/projects.rs`; bookspec `projects-net-is-its-tagged-lines` and
`projects-on-spec-costs-are-expenses`; Kani `unrecovered_cost_laws`;
proptests `prop_project_nets_partition_the_pnl`,
`prop_project_set_round_trips`.
+25
View File
@@ -249,3 +249,28 @@ consideration" it expects (55-37B); an agent recognizes only its fee or
commission (55-38).
→ B-17: an app seller is the principal for its app, so its revenue is
the gross price; the platform's commission is an expense.
## 450-30-25-1 (Gain contingencies)
Added 2026-10-21 for B-37 from the paragraph's text as Deloitte's DART
roadmaps quote it (originally FAS 5 ¶17(a)); not re-read in the ASC
basic view: check it there at the next reading. A contingency that might
result in a gain "usually should not be reflected in the financial
statements because to do so might be to recognize revenue before its
realization."
→ B-37 item 4: a payment hoped for from work done on spec is recorded
neither as a receivable nor as income until it is received; the
unrecovered cost is a figure, never an asset.
## 340-40-25-5 (Costs to fulfill a contract)
Added 2026-10-21 for B-37 as the DART revenue roadmap sets it out; not
re-read in the ASC basic view: check it there at the next reading. Costs
incurred to fulfill a contract (one not in another Topic's scope) are an
asset only if all three criteria are met: they relate directly to a
contract or an anticipated contract the entity can specifically
identify; they generate or enhance resources used in satisfying
performance obligations in the future; and they "are expected to be
recovered."
→ B-37 item 4 (with B-22): on-spec work has no contract it is expected
to recover its costs from, so its costs are expensed as incurred.
+140 -1
View File
@@ -23,7 +23,7 @@ promise.
| Model-based stateful | the Lean state machine, transcribed (3) | `tests/model.rs` | `just check` | ~3s | every change |
| Differential: Decimal vs i64 | the kernel at two types (3) | `kernel_bridge.rs` | `just check` | <1s | every change |
| Differential: GnuCash | another implementation, adjudicated (3) | `tests/oracle_gnucash.rs`, `oracle/gnucash/` | `just oracle` | ~2 min | weekly |
| Formal proofs | Kani (bounded) and Lean (unbounded) | `src/proofs.rs`, `verify/lean/` | `just prove` (47 harnesses and Lean), `just prove-slow` (the 8 in the justfile's `kani_slow`, each under its own 30-minute cap) | prove ~5 min and prove-slow ~8 min at four jobs on a quiet laptop (each ~25 min under heavy load; docs/verification.md, "Running it") | weekly (CI runs both) |
| Formal proofs | Kani (bounded) and Lean (unbounded) | `src/proofs.rs`, `verify/lean/` | `just prove` (48 harnesses and Lean), `just prove-slow` (the 8 in the justfile's `kani_slow`, each under its own 30-minute cap) | prove ~5 min and prove-slow ~8 min at four jobs on a quiet laptop (each ~25 min under heavy load; docs/verification.md, "Running it") | weekly (CI runs both) |
| Fuzzing | crash, hang, broken books (4); for `fuzz_store_replay` (validly chained journals with fuzz-chosen events): books that open must verify, and the viewer must accept exactly what the writer does | `fuzz/fuzz_targets/`, corpora in `fuzz/corpus/` | `just fuzz <target> <secs>` | 10 min each | weekly |
| Miri | undefined behavior in our use of dependencies (4) | the core's unit, bridge, invariant tests (the exhaustive calendar walk aside) | `just miri` | ~4 min | on demand |
| Golden | `tests/golden/fixture_company.json` (5) | `invariants.rs` | `just check` | <1s | every change |
@@ -1755,6 +1755,145 @@ before); the bookspec loader's exact-spelling filters and `check_k1`'s
The earlier P3 sections' lists stand (one name corrected:
`Ledger::depreciation_runs`, which was `depreciation_runs_ever`).
## P3: projects (2026-10-21)
The owner's ask: "a Projects page that treats a class as a project ...
Projects are entities too", a company rule to link things to projects
(the convention `link-to-projects`), and the sidebar's links drawn under
the status box in a short window (rules.md B-37). Mutation runs wait for
P12 (D-33); none was run.
- **ledger-core:** `tests/spec_projects.rs`
(`project_facts_default_for_a_bare_class`,
`set_project_checks_every_field`,
`set_project_renames_the_class_and_moves_no_figure`,
`add_class_refuses_a_duplicate_name`,
`replay_set_project_takes_what_the_text_rules_would_refuse`,
`project_figures_are_its_tagged_lines`,
`project_to_date_ignores_the_start_and_the_projects_dates`,
`a_tagged_balance_sheet_line_counts_nothing`,
`closing_entries_are_left_out_of_project_figures`,
`a_reversal_nets_a_project_to_zero`,
`unrecovered_cost_only_for_on_spec_and_never_negative`,
`projects_of_party_lists_them_by_name`,
`check_decision_refuses_an_unknown_or_repeated_project`,
`differs_from_names_projects`,
`names_compare_one_way_for_the_duplicate_check_and_the_lookup`,
`old_books_duplicates_resolve_to_the_lowest_id_and_change_freely`,
`an_unchanged_name_is_kept_exactly_and_not_checked_again`,
`a_name_of_digits_only_is_refused`,
`a_link_is_read_as_a_web_address_for_a_full_tin`); `properties.rs`
`prop_project_nets_partition_the_pnl` (random tags on both lines of
random entries, a random period: the projects plus the untagged lines
are the P&L, and each project's months sum to its period) and
`prop_project_entries_sum_to_the_net_to_date` (project_entries' own
signs: the posted entries' project amounts are the net to date, with
refunds, reimbursements, tagged cash lines and drafts beside them);
Kani `unrecovered_cost_laws` (`src/proofs.rs`, docs/verification.md law
32, run by `just prove`: never above
max(expenses, 0) when revenue isn't negative; the earlier "never above
expenses" fails it, checked).
- **ledger-store:** `tests/projects.rs`
(`add_project_journals_class_added_then_project_set`,
`a_name_only_project_journals_class_added_alone`,
`project_writes_wait_for_the_upgrade_and_journal_nothing`,
`update_with_nothing_new_journals_nothing`,
`project_set_replays_verifies_and_reloads`,
`an_unknown_status_or_kind_is_newer_a_malformed_record_corrupt`,
`a_decision_without_projects_writes_the_bytes_it_always_did`,
`attachments_link_to_a_project_and_unlink`,
`old_books_open_to_their_figures_with_projects_known` (each fixture's
report snapshot and verify against its expected.json),
`format_2_books_without_the_feature_wait_for_it_alone` (the live
books after install: format 2, every earlier feature, not `projects`),
`old_books_with_a_duplicate_name_change_either_project`,
`a_fact_only_change_never_renames_an_untrimmed_old_name`, and the proptest
`prop_project_set_round_trips`); `tests/format.rs`
`p3_features_wait_for_the_upgrade_and_are_declared_once` gains
`projects` and a `project.set`.
- **ledger-mcp:** `tests/spec_projects.rs`
(`add_project_then_project_returns_its_facts_and_figures`,
`a_name_only_project_journals_only_class_added`,
`update_project_checks_every_field_before_writing`,
`update_project_with_nothing_new_journals_nothing`,
`project_figures_match_profit_and_loss_by_class`,
`project_to_date_counts_lines_before_the_period`,
`on_spec_project_shows_unrecovered_cost_and_others_null`,
`project_lists_entries_documents_decisions_and_party`,
`link_attachment_to_a_project_by_name_and_id`,
`file_document_with_a_bad_project_files_nothing`,
`record_decision_names_projects_and_decisions_filter_by_project`,
`party_lists_its_projects`, `start_lists_active_and_paused_projects`,
`show_project_sends_its_target`,
`project_writes_wait_for_the_format_upgrade`,
`add_line_notes_a_finished_project`,
`record_payout_notes_a_finished_project`,
`a_name_resolves_as_the_duplicate_check_compares`,
`old_books_duplicates_take_fact_only_changes`,
`a_digit_name_that_shadows_an_id_says_what_to_pass`); `hostile_args.rs`
`a_project_over_the_whole_calendar_answers_quickly` and
`books.projects` among the read-only calls; `spec_vault.rs`'s idempotent
list gains `books.update_project`; the unit tables (`annotations_complete`,
scopes' table) list 119 tools, `period_tools_describe_the_one_default`
checks the period tools' wording, and `mcp_stdio.rs`'s `tools/list`
shows 116; `every_tool_is_documented_and_every_documented_tool_exists`
holds with the four new tools (on the rebase onto the surface review it
caught `books.list_classes` written mid-line and a `books.set_line_class`
the doc named, which is no tool). `ledger-store` `tests/format.rs`
`the_replay_fuzzer_writes_every_event_type` caught `project.set`
missing from `fuzz_store_replay`'s types; it and the project fields are
in them now.
- **ledger-gui:** `tests/projects.rs`
(`projects_page_lists_every_project_alphabetically`,
`project_figures_are_ledger_cores`, `unrecovered_cost_shows_only_on_spec`,
`a_project_row_opens_the_project`, `an_entry_row_opens_the_entry`,
`a_document_opens_the_document`, `the_party_link_opens_the_party`,
`profit_by_project_bar_opens_the_project`,
`party_page_lists_its_projects`, `entry_line_project_opens_the_project`,
`journal_describes_project_set`, `show_project_target_opens_its_page`,
`a_long_link_stays_in_its_card` (200 characters, 1440 and 900 wide),
`a_long_name_keeps_the_page_in_the_window` (with spaces and without),
`a_long_party_name_keeps_to_its_card`,
`a_month_bar_opens_nothing_and_margin_keeps_one_place`,
`an_entry_without_receipts_says_nothing_of_them`; with `SHOT_DIR` set
the edge pages are saved as PNGs); `charts.rs`
`a_crowded_chart_keeps_its_years` and
`a_view_only_chart_shows_figures_and_takes_no_click`;
`old_books.rs` `old_books_classes_read_as_projects` and every project
page over the old books; `read_only.rs` renders the Projects list and
every project page over read-only books (what the brief called
`visiting_project_pages_changes_nothing`); `breadcrumbs.rs` a project's
crumbs. The sidebar: `edges.rs`
`sidebar_links_never_draw_under_the_status_box` (1440x500, scrolled to
the end: Journal ends above the status box),
`rail_links_never_draw_under_its_footer` (900x420) and
`a_normal_window_shows_every_link_unscrolled` (1440x920); both short
ones fail on the code before the fix (checked by putting it back);
`a_very_short_window_keeps_one_link` (1440x220, 900x200: a link stays
whole above the footer, which gives way off the bottom). The
sidebar links, painted table rows and bars now carry accessible names,
which the tests find them by. Shots before and after:
`~/.cache/wanderbooks/shots-p3-projects/` (`before-*`, `after-*`,
`final-*`, and the project pages light, dark, 1100 and 900 wide).
- **bookspec:** `setup.projects`, a line's `project`, and the query
`project`; examples `projects-net-is-its-tagged-lines` and
`projects-on-spec-costs-are-expenses`; refused:
`a_project_named_twice_is_refused`,
`a_line_naming_an_unknown_project_is_refused`,
`a_project_query_leaving_a_value_out_is_refused`.
**For P12 (mutation targets):** `check_set_project`'s arms (ended_on with
active/paused, `<` vs `<=` on the dates, the duplicate name's
self-exception, case folding) and `ProjectFacts::check` (the link limit,
whitespace, duplicates); `kernel::unrecovered_cost` (`max`, the sign);
`project_figures`' class filter, its 240-month window and the
closing-entry exclusion; `project_entries`' normal-side sign and the
balance-sheet zero; `project_set_of` (newer vs corrupt);
`Store::add_project`'s "facts beyond the default" test (one line vs two);
`set_project`'s unchanged short-cut; `subject_feature`; `check_link_to`'s
feature check; the decision `projects` omit-when-empty;
`late_tag_note`'s `is_over`; `remember_footer`'s threshold.
## Benchmarks (baseline 2026-10-13)
No budget, by decision (the owner, 2026-10-13: the books do little work
+17 -1
View File
@@ -450,6 +450,21 @@ one kernel function, `kernel::basis_figures`, for every rule:
(the `Decimal` instantiation agrees, rule by rule) · property
`prop_basis_figures_laws`
### A project's unrecovered cost (P3, projects)
The "if repaid" figure of work done on spec (rules.md B-37 item 4) is
one kernel function, `kernel::unrecovered_cost`:
32. **Never below zero, the shortfall exactly, and no overflow**: the
result is at least zero; it is expenses less revenue when expenses
are at least revenue, and zero otherwise; with revenue not negative
it is never above the expenses or zero, whichever is larger
(expenses can be negative: a reimbursement credited to the expense
above its cost). · Kani `unrecovered_cost_laws` (every
revenue and expense a line could carry) · ledger-core
`unrecovered_cost_only_for_on_spec_and_never_negative`. No Lean model:
the function is one subtraction and a `max`.
## What the proofs found
The engine already had 190+ tests, property tests, mutation testing,
@@ -606,7 +621,7 @@ Lean carrying any weights.
```sh
mise install # once: Kani, Lean, and the rest, pinned in mise.toml
just prove # 47 Kani harnesses and Lean
just prove # 48 Kani harnesses and Lean
just prove-slow # the other 8: the justfile's kani_slow
just prove-all # both
```
@@ -705,4 +720,5 @@ each harness's whole run, its compile of about 50 s included):
| `catch_ups_is_every_suffixs_catch_up` | 29 | prove | 35 |
| `true_up_reaches_the_target` | 30 | prove | 0 |
| `basis_figures_laws` | 31 | prove | 0 |
| `unrecovered_cost_laws` | 32 | prove | 0 |
| `kani_symbolic_copy_canary` | (the checker: "Trusting the checker") | prove | 3 |