Projects: classes as entities, and the sidebar fits short windows
check / check (push) Canceled after 0s
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:
71 files changed
+7446
-217
No files matched your search
@@ -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.
|
||||
|
||||
|
||||
@@ -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
@@ -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
@@ -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`.
|
||||
@@ -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
@@ -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
@@ -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 |
|
||||
Reference in new issue
Block a user