36 KiB
Stage 9: Implementation readiness and codebase details (plan/served.md 766-2058)
Reviewers: egm-implementation-readiness (IR), egm-codebase-details (CD). 2026-10-26. Additional to Stages 5-8. Symbols, not line numbers. Sizes assume an entry-level Rust engineer new to this codebase; one engineer-day = one reviewed PR, just check green, 200-400 lines including tests and docs.
Cross-cutting
IR-CC-1 Blocker. The mechanical recipes every unit repeats are nowhere written down
R-A New journal feature. (1) crates/ledger-store/src/format.rs: pub const NAME: &str = "name"; with a doc comment saying what an older version would misread; append to KNOWN_FEATURES (Store::ensure_feature has debug_assert!(KNOWN_FEATURES.contains(..))). (2) Unit test name_is_its_own_feature beside document_standing_is_its_own_feature: declared_knowing with every earlier feature gives NewerJournal, require_feature gives FormatUpgradePending, upgrade().adds() names it. (3) Every Store command writing the marked line calls self.ensure_feature(NAME) before rehearse/journaling; store test <write>_waits_for_<name> modelled on repaying_after_a_reversal_waits_for_document_standing (tests/arap_standing.rs): refused, journal unchanged, succeeds after upgrade, replay equals live. (4) crates/ledger-cli/tests/format_upgrade.rs::books_p2_served_cut_over_whole: one entry in the refused list and the same call after consent. (5) handbook/format-upgrade.md and rules.md B-27 list the feature. Feature names are allocated in the plan; no engineer invents one.
R-B New event type. (1) format.rs EVENT_TYPES (alphabetical; tests/format.rs::every_event_the_store_replays_passes_the_gate scans store.rs for every "x.y" => arm and fails on a mismatch either way). (2) A payload reader in ledger-store/src/meta.rs in the style of project_set_of: missing key → corrupt, unknown enum value → format::newer_content. (3) A replay_line arm in store.rs calling a Ledger::replay_* method (line.seq is available). (4) An apply_line_rows arm (projection SQL); a new migration in schema.rs and SCHEMA_VERSION 15 → 16 only when a table or column changes; tests/schema_files.rs. (5) projection_rows/compare_projections list the new table; Ledger::differs_from gains the new state (the ("line_docs", ..) pattern). (6) fuzz/fuzz_targets/fuzz_store_replay.rs const TYPES (the_replay_fuzzer_writes_every_event_type fails otherwise). (7) A describe arm in ledger-gui/src/pages/journal.rs. (8) rules.md and docs/receipt-store.md event table.
R-C New MCP tool (one commit; three tests fail on a partial state). (1) types.rs params/out structs with JsonSchema and doc comments (they are the schema descriptions). (2) Handler in server.rs or tools/<group>.rs with #[tool(name, title, description, annotations(read_only_hint, destructive_hint, open_world_hint))] (annotations_complete). (3) scopes.rs::TABLE row and its array length constant (compile error otherwise). (4) docs/mcp-interface.md entry in the shape `books.x { a, b? } → { c }` (every_tool_is_documented_and_every_documented_tool_exists). (5) Description ≤ 700 chars (tool_descriptions_fit_700_characters pins a count OVER; P5 drives it to 0), tool_text_lint. (6) Over-the-wire test in crates/ledger-cli/tests/ through common::Books::new(tag) → .wire() → .call(tool, json) → answer_of. (7) docs/parity.md row.
R-D New setting key. (1) ledger-core/src/settings.rs KEYS entry Key { name, toml, kind, sensitivity, doc } (pattern company.duns); a TIN exemption is a Kind arm, not a key match (no_tin is applied by every text Kind; Kind::Duns escapes by having its own arm). (2) setting_feature arm in ledger-store/src/format.rs (pub(crate); Store::journal_settings calls ensure_features(setting_feature(..)); the "settings.set" replay arm refuses an unknown key as newer_content). (3) ledger-config toml_round_trip_zero_events, codec_props.rs; settings_units.rs for the canonical form. (4) The settings.rs INVARIANTS block.
R-E New handbook section. crates/ledger-mcp/handbook/<id>.md plus a tuple in handbook.rs::SECTIONS (include_str!); placeholders like {document_projects} via document_projects_rule(declared) give different text before and after the owner's upgrade; the tests in handbook.rs check every section renders and ids are unique.
R-F New Needs-attention item. Until the attention engine moves, an item is written twice: ledger-gui/src/attention.rs::find (and its GOAL block's INVARIANT 2 list) and the server's books.start next steps in tools/kept.rs::start_out. Parity test ledger-gui/tests/view_model.rs; screenshot through shot() (ledger-gui/tests/invoices.rs).
R-G A **For P12 (mutation targets):** paragraph in docs/testing.md naming each new function's arms.
IR-CC-2 Major. docs/testing.md has no section for S1 part B, S2 or S3 (last heading "P4b S1, part A"). P12's input is three units short. 0.5-day pre-step.
IR-CC-3 Major. The journal-safety bar for a unit that writes new lines is not a checklist. "8.0.5 Before a journal-touching unit merges": (a) ledger-store/tests/store.rs::books_written_under_older_rules_still_open gains a hand-written line of the new shape via JournalWriter; (b) a new fixture under tests/fixtures/old-books/ written by the version before, plus expected.json; (c) the books_p2_served_cut_over_whole refused entry; (d) the _is_its_own_feature test; (e) tests/format.rs arms; (f) a figures.json golden from the previous version where a figure could move (pattern tests/fixtures/p4-arap-before, arap_standing.rs::write_the_p4_arap_fixture); (g) "merge does not need the owner's go-ahead; the live books refuse the new write until he gives it".
P4b S4 documents_v2
- CD-A1/A2. How a document is journaled today:
Store::create_invoice(customer_id, date, due_date, lines):rehearse→approval_gate→arap::validate_lines,arap::validate_due_date,closed_period_containing→ensure_document_lines(callsensure_feature(DOCUMENT_LINE_PROJECTS)only if any line hasclass_id) →ensure_control_account(&RECEIVABLES)/(&REVENUE)(billsGENERAL_EXPENSES,PAYABLES) → posting fromarap::document_parts→journal_auto_lines(entry.created,entry.line_added×n,entry.posted) →append_journal("invoice.created", {id, customer_id, date, due_date, lines: [document_line_json], entry_id})→self.ledger.create_invoice(..)→apply_line_to_sqlite. The feature pattern isensure_document_lines+document_line_json(omitsNonekeys: byte-identical old shape) +parse_document_lines(absent/null → None). The posting is built twice: inStore::create_invoiceand inLedger::create_invoice_steps(ensure_control,add_tagged_line(.., DocTag{Origin}),add_document_parts);Ledger::replay_invoice/replay_billre-derive and compare (ReplayMismatch). Line accounts change:arap::document_partskey →(Option<AccountId>, Option<ClassId>),add_document_parts, both*_steps, bothStore::create_*posting vectors, bothreplay_*.check_document_line_accountgoes besideLedger::check_line_account, reusingcontrol_role_ofand thecash_accountssetting. - CD-A3.
arap::validate_due_date(date, due_date)(callscalendar::check, refusesDueBeforeDate) is whatTerms::due_datefeeds;arap::validate_linesis wherequantity × unit_price == amountgoes. - CD-A4. MCP today:
CreateDocumentParams { party_id, date, due_date (required), lines: Vec<DocumentLineParam{description, amount, class?}> },CreateDocumentOut { id, total, entry_id, warnings, notes };BooksServer::create_invoice→document_lines(&store, p.lines)→store.create_invoice(..)→created_uncategorized→document_notes.resolve_vendor(store, &str)exists (id or name, ambiguity refused); addresolve_customermirroring it. State thedue_datefallback:termsarg → partydefault_terms→invoicing.default_terms→ refuse. - CD-A5. Preview pattern:
BooksServer::post_entrywithconfirmed: Option<bool>returns the fulloutunchanged when not confirmed;preview_lines_for(store, &[(AccountId, Side, Money)])computes before/after balances; engine probe =Store::rehearse(let mut probe = self.ledger.clone()), soStore::preview_invoicerunsLedger::create_invoiceon a clone. Settleconfirmed: bool(run_schedules) vsOption<bool>(post_entry, reverse_entry, dispose_asset, reconcile). - CD-A6 Blocker. "
issued_pdfininvoice.created" cannot be done in the plan's event order:Store::attachallocates the id byMAX(id)+1and needs the entry to exist. Choose (b): noissued_pdffield; the filed copy isreceipt.attachedon the entry plusattachment.linkedtoSubject::Invoice(id)(Store::link_attachment,attachments_for(Subject)already read links); the sha is already inreceipt.attached'sblob_sha. Render (invoice_pdf(&InvoiceDoc)) beforejournal_auto_lines. ReuseStore::attach_receipt(Some(entry_id), "INV-0007.pdf", "application/pdf", &bytes, Some("Issued invoice")). - CD-A7. Settings:
KEYS,Kindarms;invoicing.payment_methodsneedsKind::PaymentMethodswhose arm validates the list and skipsno_tinforlinesofach/wiremethods only; each key an arm insetting_feature;Settings::company().profile(Vec<(toml_key, value)>ofcompany.*text keys) is howinvoice_documentfinds the address, so seller email/website are newcompany.email/company.websitekeys. - CD-A8.
party.updated {id, change: PartyField};PartyFieldis#[serde(tag="field", content="value")]withName, DisplayName, Website, Address, Notes, Archived;Ledger::check_update_partyappliesparties::check_text(TIN scan). The replay arm maps an unknownfieldtoJournalCorrupt, notnewer_content: S4 must fix that (precedentmeta::project_set_of), addPartyField::Email,PartyField::DefaultTerms, gate withensure_feature(DOCUMENTS_V2), extendUpdatePartyParams, add adescribearm. - CD-A9.
ledger_package::invoice_document(ledger, company, id, as_of, today) -> Result<InvoiceDoc, String>buildsInvoiceDoc { number ("Invoice {id}"), issue_date, due_date, terms: None, seller: InvoiceParty, bill_to, items: Vec<InvoiceItem{description, amount}>, total, settled: Vec<(String, Decimal)>, balance, stamp: Option<InvoiceStamp>, stamp_line, note };InvoiceStamp { Paid, PartiallyPaid, PastDue, Void, Draft };templates/invoice.typprints seller, INVOICE + number, bill-to, dates, Terms row, lines (description, amount), Total, settled rows, Balance due, stamp, note;paper: "us-letter". New in S4: quantity × unit price column, customer PO, payment methods + QR, memo, footer, seller email/website, the issued stamp-less copy, thetermsstring. - CD-A10. QR:
labels.rs::qr_svg(tag) -> Result<String, PdfError>andtemplates/labels.typ'simage(bytes(l.qr), format: "svg", ..). - CD-A11. S1 kept
amount_paid,owed,statusonInvoice/Billas derived caches (Ledger::refresh_doc), never journaled; the full status isarap::DocStatus { Open, Partial, Paid, WrittenOff, Void, Overpaid, Unattached }viaarap::doc_status(origin_stands, origin_tagged, total, outstanding, written_off);ControlCheck.attention: Vec<DocAttention>lists Overpaid/Unattached. The plan's machine omits both. - CD-A12. Handbook mechanism:
handbook.rs::SECTIONS,section(id, settings, currency, document_line_projects),books://handbook/{id}. S4 addsinvoices-and-bills.mdand a{documents_v2}placeholder. - CD-A13.
create_billwith a file: the upload path istools/files.rs(begin_upload,hand_over) and theupload_id-to-bytes helper used bybooks.attach_receipt; categoryreceipts, filed on the bill's entry, linkedSubject::Bill. - IR-S4-1 step order (each
just checkgreen; four PRs: 1-4, 5-6, 7-8, 9-11): 1 coreTermstypes +Terms::describe(), teststerms_derive_due_dates,discount_bounds_and_late_reason, Kaniterms_due_date_laws; 2 optional fields onInvoice/Bill/DocumentLinewith#[serde(default, skip_serializing_if)]asclass_id,PartySnapshot,check_document_line_account,document_partsby(account, class), q×u rule "compare asDecimal, no rounding; mismatch isLedgerError::LineArithmetic", testsbill_lines_post_to_their_accounts,qty_times_price_must_equal_amount,invoice_line_to_deferred_revenue_posts_to_2400; 3 numbers and duplicates (next_invoice_number(settings),(vendor, normalized vendor_ref)), testsnumbers_run_and_voids_keep_theirs,duplicate_vendor_ref_refused,first_number_locked_after_first_numbered_invoice; 4 settings (R-D) and party fields; 5 store (R-A, R-B):DOCUMENTS_V2,ensure_featureonly when any new field isSome,no_new_fields_journals_byte_for_byte,documents_v2_waits_for_the_upgrade, projection columns (schema 16),books_p2_served_cut_over_wholeentry; 6 issued PDF filed in the issuing call (issued_pdf_is_filed_with_the_invoice_or_nothing_is); 7 MCPCreateInvoiceParams,customerresolution,party_idalias until P5's doc rewrite, preview returning the number and a DRAFTHandedFile;documents.rs::an_invoice_from_issue_to_paid(S4 leg),documents_v2_refused_until_consent; 8create_billwithvendor_ref,received_on,terms,upload_id | existing_attachment_id(refused when both absent underdocuments_v2),duplicate_reason;duplicate_vendor_invoice_refused; 9 PDF delta with golden sha inledger-import/tests/spec_import.rsbesidepdf_is_deterministic_and_labels_are_data,invoice_pdf_prints_what_it_should; 10 viewer (shot("invoice-numbered"),view_model.rs); 11 handbook (R-E), rules.md B-39, B-40, parity, For-P12. S6 may start after step 1 merges. - IR-S4-2. Must stay green:
legacy_documents_replay_with_tags,legacy_reversal_now_counts,a_payment_journals_its_method_and_reference,several_payment_events_on_one_entry_tag_its_lines_in_order,the_list_shows_the_ledgers_figures;an_invoice_renders_deterministicallygolden updated once with the recomputation in the commit; no bookspec example changes. - IR-S4-3/4. Rollback: an older version derives the posting without
accountand hitsReplayMismatch, which is what the gate prevents (CC-3 a-e). No figure moves. External: D-39 go-ahead for live use only. Size 12-16 days.
P4b S5 invoice_sent
- IR-S5-1. Depends on S1 only; if merged before S4,
tois required and the reminder uses 14 days fixed; after S4 the defaults apply. - IR-S5-2. Decisions to write: legacy invoices exempt from "never sent"; predicate
status ∈ {Open, Partial} ∧ sends.is_empty() ∧ today(company.timezone) > date + 2 days;mark_senton Void refused, on Paid allowed; bills get no sends;Sentgainsby: String,reference: Option<String>,SentVia::Other(String). - IR-S5-3 order. 1 core
Sent,Invoice.sends,Ledger::mark_sent;a_send_changes_no_figure,mark_sent_on_void_refused; 2 store (R-A, R-B)INVOICE_SENT,invoice.sent, projectioninvoice_sends,differs_from;mark_sent_waits_for_invoice_sent,sends_replay_and_project;arap_props.rsgainsMarkSent; 3 MCPbooks.mark_sent(W);documents.rs::mark_sent_records_who_and_how; 4 attention (R-F); 5 handbook, docs, For-P12. Size 3-4 days.
P4b S6 settlements
- IR-S6-1 / CD-B2. Exists:
Movement::Discount,DocState.discounted,kernel::running_bounds/bounds_keptwith Kanibounds_check_matches_definitionandbounds_kept_is_the_definition_on_books_in_bounds(the plan lists the first as to-build).Store::ensure_payment_features(doc, amount, details)is the gate for payment events (addssettlements);Store::receive_payment_withusesjournal_auto_entry(two lines); multi-document needsjournal_auto_lines.PaymentParams,PaymentDetails { instrument, reference }(MAX_REFERENCE_CHARS = 200) are whatapplications[]extends.AgingParams { as_of },AgingRowOuthave nodetail/control. Export seams:hand_over_export,layout_statement,ledger_import::report::{format_amount, amount_parts},pdf::statement_pdf; CPA README assembled inledger-package/src/package.rs. - IR-S6-2. Replay for multi-document/discounted entries: legacy single-document events keep the positional rule (
several_payment_events_on_one_entry_tag_its_lines_in_order); undersettlementslines are named explicitly (Stage 8:line/discount_lineindices on the existing events); unmatched lines listed bycontrol_check;a_settlement_names_its_lines_and_replays. - IR-S6-3. Kernel:
kernel::settle(applications, discounts, fee)andkernel::discount_max(total, percent_bp); Kanisettle_is_exact,discount_max_exact; bridge properties intests/kernel_bridge.rs(omitted by the plan). - IR-S6-4. Roles (
purchase_discounts,sales_allowances,payment_processing_fees) via the on-demand creation used byreceivables/payables;templates.rs::no_template_puts_another_account_at_a_control_codestays green. Aging CSV columnsnumber, party, date, due_date, terms, total, paid, discounted, outstanding, bucket, days_overdue; tie-out lineTOTAL,…,<Σ outstanding>,GL <account> <balance>,difference 0.00.books.bills_due(R): rows{bill, vendor, vendor_ref, due_date, outstanding, discount_by?, max_discount?}, footerby_date: [{date, amount}]running sum. - IR-S6-5 order. 1 kernel + Kani + bridge; 2 core
Ledger::settle, testsone_payment_settles_two_invoices,discount_bounds_and_late_reason,fee_splits_gross_fee_net; 3 storeSETTLEMENTS(R-A), line indices (R-B),discount_reasonjournaled,settlement_waits_for_the_upgrade,arap_props.rsops, cut-over entry; 4 roles/templates; 5 MCPapplications[],fee?,paid_in_full_marking?(bool),books.bills_due, agingdetail/control; over-the-wireone_payment_settles_two_invoices,bill_on_terms_with_discount,card_paid_bill_is_not_1099_nec,equipment_bill_payment_is_investing_cash,bills_due_orders_by_due_and_discount,multi_document_payment_counts_once_for_1099; 6 exports and CPA package; 7 viewer; 8 handbook, docs, For-P12, bookspec. Depends on S4 step 1. Size 8-10 days.
P4b S7 write_offs
- IR-S7-2. Depends on S4 step 6 (attach inside a document command):
write_off_invoice {…, upload_id | existing_attachment_id}required;LedgerError::WriteOffOnCashBasiswhenbooks.accounting_basis = cash(Company.basisfromSettings::company()). - CD-B3. Exists:
Movement::WriteOff/Adopted/RoleMove,DocStatus::WrittenOff/Unattached,arap::UnattachedLine,ControlCheck.unattached,Ledger::check_assign_role(SCENARIO 6RoleAccountHasHandLines),Store::assign_rolejournalingrole.assigned,Ledger::control_role_of. - IR-S7-1/3, CD-B5. If adoption/role moves were built:
differs_frommust compareline_docsdirectly; LeanDocuments.lean(TLine,gl,docSum,unattached,Step := document | reverse | assign | release,Reachable.control_is_documents) has no bounds notion and no adopt constructor. (Stage 8 defers both tools; S7 =write_off_invoice+bad_debtsrole + B-41 + LeanStep.writeOff.) - IR-S7-4 order (cut). 1 core
Ledger::write_off;half_paid_rest_written_off_is_written_off,write_off_refused_on_cash_basis,write_off_then_recovery; 2 LeanStep.writeOff,control_is_documentsre-proved; 3 store feature + event (R-A, R-B); 4 MCPbooks.write_off_invoice(destructive);documents.rs::write_off_then_recovery; 5 attention, viewer; 6 handbook step 4, B-41, bookspecbad-debt-direct-write-off. Size 4-6 days (8-10 with adoption and role moves).
P4
- CD-C1. Drafts:
EntryStatus { Draft, Posted },JournalEntry { id, date, description, lines, status, reversal_of }withpub(crate)mutators;Ledger::posted_entries()iteratesorder; drafts only in theentriesmap. A draft state is a new field +differs_from+ projection column +entry.draft_state_setarms. - CD-C2.
Ledger::reverse_entry_steps:check_reversal→create_entry→replay_add_line(flipped) +set_line_class+tag_line→mark_reversal_of→post_entry→ copiesentry_vendors,entry_instruments,entry_parties,entry_no_party→ schedulepostedset →refresh_docs_of. (Stage 8 keeps reversals immediate, so this stays.) - CD-C3. The gate today:
ledger_config::ReceiptPolicy::receipt_required(&Ledger, &JournalEntry),describe(),rule(currency); callersBooksServer::post_entry(ReceiptRequiredError { code: "receipt_required", remedy_tool: "books.attach_receipt", split_group }),digest,dispose_asset,handbook::evidence_rule, kept.rsRulesOut.evidence; GUIBooks::receipt_rule()andmissing_receipt.Store::post_entrychecks onlyapprovals_missing. - CD-C4.
BooksServer::digest(DigestParams { since })filters by entry date;DigestEntry,totals_by_account,stale_rules,tail_repairs,unreconciled_accounts,uncategorized_accounts,control_problems,settings_problems.posted_seq: add anentry_id → seqmap filled in the"entry.posted"replay arm (precedentLedger::apply_settings(.., line.seq)), no new event. - CD-C6. Tools taking
confirmedtoday:post_entry,reverse_entry,run_schedules,dispose_asset,reconcile, and the posting runs. - IR-P4 order (after Stage 8's cut). 1 draft states,
list_drafts,discard_draft,set_draft_state;discard_unblocks_close; LeanStep.discard; 2 gate unification: policy evaluation moves fromledger-configtoledger-core(evidence::requirement(&Ledger, &JournalEntry, &Settings)),Store::post_entrycalls it, GUI deletes the string heuristic (gui_and_server_gate_agree); 3 digestsince_seq,as_of, drafts by state (digest_since_is_posting_time); 4previewdigest at the envelope, required by every confirm tool (stale_preview_refused); 5 attention (R-F), handbookdrafts-and-reversals.md, rules, For-P12. Featuredrafts_v2in S7's upgrade batch; schema bump forentries.draft_state,entries.posted_seq. Size 8-10 days.
P5
- CD-D1.
books.startv1:StartParams { focus },start_out -> StartOut { book, head: HeadOut { seq, hash }, state, rules, settings_summary, conventions, open_items, projects, truncated, handbook_sections, format_upgrade, next_steps: Vec<NextStepOut { tool, args, why }>, isolation },START_MAXfit loop. v2 extends these. - CD-D2 Blocker. Prompts:
opening.rs::capabilities()enables tools and resources only; no#[prompt_router],list_prompts,get_prompt,list_resource_templates. P5 adds them and the bridge's cold-startopening_initializemust advertise the same. - CD-D3.
tool_descriptions_fit_700_charactersassertsover == OVER; P5 drivesOVERto 0.handbook::CONNECT_MAX = 1400,CONNECT_LEAD = 512. - CD-D4.
journal_history: reuse thedescribearms ofledger-gui/src/pages/journal.rsby moving them toledger-storebesidemeta.rs; read withjournal::read_journal_and_format+Store::tail_repairs. - CD-D5. Source-only revision: refusal in
Ledger::check_convention;Source { decided_by, decided_on, citation, note }; new eventconvention.source_revised+ feature. - IR-P5-1 Blocker. The result envelope touches every handler and every wire assertion: single owner, merged before or after every other MCP-touching branch (the EM writes which in 8.0.3).
- IR-P5-2. Agent evals:
crates/ledger-cli/tests/agent_evals/*.json[{"call": "books.start", "args": {}}, {"expect_next_steps_contains": "books.set_setting"}, …], one testagent_evals_follow_only_next_steps. - IR-P5-3 order. 1 envelope; 2 guard registry enum with
remedy_tool; 3books.startv2; 4 prompts and resource templates with golden renders on thep2-servedfixture; 5journal_history, source-only revision; 6 descriptions ≤ 700, handbook complete; 7docs/mcp-interface.mdrewrite; 8 agent evals. Size 10-12 days.
P6
- CD-E1 Blocker. "Identical bytes return the existing record" contradicts
Store::attach_receiptINVARIANT 3 ("each filing MUST be its own attachment");attachalways allocates a new id even whenBlobStore::putreportsdeduplicated. P6 must state the invariant change and that legacy duplicates stay. - CD-E2. Consolidation sequence:
link_attachmentthe first record to each subject,unlink_attachment/unlink_receiptthe duplicate, thenwithdraw_attachment(dup, "duplicate of #N")(it refuses while linked:AttachmentLinked,AlreadyLinked). - CD-E3. Correction to earlier stages:
ledger-gui/src/receipts.rs::text_ofis UTF-8 text only; PDF rendering is the privatepdf_first_page_png/render_pdf(hayro 0.8, first 30 pages); the only PDF text extraction is a test helper inledger-import/tests/spec_import.rsusinghayro_interpretGlyphRun(dev-dependency);tin_files::text_ofnever reads a PDF. P6: promote the reader toledger_import::pdf_text(bytes), move the rasterizer toledger_import::page_png(bytes, page, scale), call both fromtin_files::text_ofand the viewer. No lopdf. - CD-E4/E5.
GetReceiptParams { attachment_id, view: "meta" | "bytes" }:text,page_imageare new arms (HandedFileviahand_over). Categories validated inBooksServer::file_document;Attachment.categorydoc listsreceipts, formation, legal, tax, accounting, otherwhile descriptions already usestatement,import_source; new categories (notices, plans, history; P8'sworkpaper) extend that one place. - IR-P6-1. Backfill =
wanderbooks extract --book IDover the admin socket, idempotent on(attachment_id, extractor_version). - IR-P6-2 order. 1
ledger-import::extract::{pdf_text, page_png}behind both callers;extraction_deterministic;fuzz_extract_pdf; 2 eventattachment.text_extracted+ featureattachment_text; 3get_receiptmeta+text,page_image; 4 dedup remainder (find_attachment,consolidate_duplicates,attachment.withdrawn {duplicate_of}, the "only evidence" refusal); 5 annotations and receipt numbers; 6 backfill command; 7 D-20 categories. Accept proxy: every attachment of thep2-servedfixture givesget_receipt< 8 KB. Size 8-10 days.
P8
- CD-F1. Exists:
Store::verify_and_heal() -> VerifyReport,verify(),verify_blobs(),Snapshot::verify_blobs();BooksServer::verifyalready appendscontrol_check(NaiveDate::MAX); access logledger_store::access::{AccessLog, read(book_dir) -> (Vec<Recorded>, Head)};CashFlowStatement.reconciles. Drop "vault public-key sha" (P7 superseded). - CD-F2. "Closes in order" is an engine refusal already; member deltas
kernel::apportion_cumulative; schedulesSchedule::due/next/remaining;FixedAssetRegister::unposted_depreciation(); the audit calls these. - CD-F3. Existing problem lists to convert:
settings_problems,control_problems,unreconciled_accounts,tax::uncategorized_accounts,rules::stale_rules,Backups::standing+needs_attention(),drill_due; no sharedFindingtype: P8 defines one inledger-core.Subject::Period(u64)for linking the workpaper;Store::file_document(category, ..)to file it. - IR-P8-1 Blocker. A table
check | predicate (function) | finding code | fixture (how seeded) | severity, one row per check; fixtures built in-test throughStore;audit_detects_each_seeded_defectiterates it. - IR-P8-2 order. 1
ledger_core::audit::statements(&Ledger, through) -> Vec<Finding>; 2Store::integrity_findings; 3 server composition,audit.runevent + featureaudits,workpapercategory; 4 workpaper JSON + Typst (templates/workpaper.typ, deterministic sha test); 5 attest tools with argument lists, onepolicy.attestation_barsetting,export_cpa_packagewithout_audit_reason; 6 attention, handbookcpa-handoff.md, For-P12. Size 10-12 days.
P9 / P10 / P11
- CD-G1. Dispatched commands (
ledger-cli/src/main.rs):gui, setup, server, host, mcp, serve, close, books, doctor, token, connect, passphrase, upload, download;ledger-host/src/commands.rs:serve, close, books (new_books, adopt, put_aside, restore/plan_restore/carry_out_restore, upgrade/upgrade_format, back_up_now), connect/connect_agent, token, passphrase, doctor, upload, download. Noupdate,helper,tunnel,fetch. P9's move =commands::adopt+admin::Request::Adopt/AdoptPreview. - CD-G2.
connect_agent(common, agent, book): findsclaude/codex, makes the token (make_token,tokens::keeper(..).set(..)), runs<cli> mcp remove|add wanderbooks-<book> -- <this_executable()> mcp --book <id>; writes nothing into a project folder (Codex global). The books home is new:commands::books_home(book) -> PathBuf+ files,connect_agentunchanged. - CD-G3. Viewer menu:
menu.rsMenu/Job::{Package, Worksheet(y)};owner.rstrait Owner { close_books, new_books, back_up_now, plan_restore, restore, connect(Agent, book), upgrade_format(book, passphrase) },Dialog::{NewBooks, Restore, Close, Upgrade}, implemented over the admin socket (ledger_mcp::admin::{Request, Response, call}), not "the lease socket's owner intents" (plan 857 stale). The dock = newOwner::start_agent(Agent, book) -> Child+ a panel inapp.rs. - CD-G4. Built already (P11 lists as to-build):
host.rsINVARIANT 7run/update.lock(take_update_lock),Ending::Restart,restart(), the version handshake refusal;bridge.rswith_list_changed, teststhe_agent_hears_of_changed_tools_only_after_a_restart,bridge_survives_server_restart,bridge_initializes_during_cold_start. The updater = fetch, verify, stage, swap, then the existing restart. - CD-G5. Backup/restore reuse:
ledger-store/src/backup.rs(Git,commit,push(Target),verify,drill,prove_archive),ledger-mcp/src/backup.rs(Backups::{open, standing, drill_due, drill},Upgraded,UpgradeBackup), adminRequest::UpgradeFormat,BackUpNow,Restore/RestorePreview;Store::upgrade_format() -> Option<FormatUpgrade>. Routes inlocal.rs:/b/{book}/mcp,/u/{upload},/a/{attachment},/x/{export};bearer(headers)→auth::Token,Scope { Read, Write, Post, Admin }. - IR-P9-1. P9a (no decision needed):
deploy/wanderbooks-server.service,deploy/README.md,self_host_adopt_same_head,.github/workflows/two-user.ymlrunningagent_uid_cannot_read_tenant(neither exists today), static bearer over a documented Tailscale Funnel; no seccomp/Landlock. 3-4 days. P9b after D-7. P9c not planned. External: a machine, tunnel, domain/TLS, a reviewer. - IR-P10-1. Not an engineering unit:
docs/runbooks/re-audit.mdagainst existing tools plus a CI testdefect_reproductions_all_fixedasserting the 24 named regression tests exist. 1 engineer-day; owner-run after P5, P6, P8. - IR-P11-1/2. Non-code prerequisites listed separately (Apple Developer ID and notarization credentials as CI secrets; Windows signing identity; GitHub Releases on the mirror; the release hardware token; clean VMs). Order: 1 trial (2-3 days, output a change-log entry per format); 2
wanderbooks update,rollback, staged layout<root>/updates/<version>/, tested against afile://feed (update_is_atomic,failed_update_rolls_back,unsigned_update_refused,bridge_survives_update); 3 Linux packages in CI containers (unsigned first;linux_packages_install_and_upgrade); 4 signing (minisign + OS; the two-key manifest per the owner's answer to Q-4); 5 macOS/Windows bundles; 6 dock as its own unit (alacritty_terminal, books home contents verbatim,dock_runs_the_chosen_agent,books_home_holds_no_books_data). Size 20-25 (1-5) + 8-10 (dock).
P12 / P13
- CD-H1. Nine fuzz targets (
fuzz_config_parse, fuzz_core_serde, fuzz_entry_validate, fuzz_import_csv, fuzz_journal_line, fuzz_op_sequence, fuzz_settings_replay, fuzz_store_open, fuzz_store_replay), scheduled.yml at 600 s each;fuzz_op_sequence::Ophas no document op. Mutation recipes:mutants(core; excludes proofs.rs;-j 4),mutants-store(STORE_CASES=64,-j 3),mutants-import,mutants-config,mutants-mcp(MCP_CASES=16, with ledger-cli tests,--no-default-features); CI shards core 8 / store 4 / import 2 / config 1 / mcp 16,timeout-minutes: 350. No recipe forledger-host,ledger-package,ledger-vault,ledger-cli: P12 adds them.--jobserver-tasksis not used (the recipes use-j). Pins: cargo-mutants 27.1.0, cargo-fuzz 0.13.2, kani-verifier 0.68.0, lean4 4.34.1. - IR-P12-1. Parameters: 4 h per fuzz target (or the owner's number); the maxima are
STORE_CASES,WB_ORDERS_DEPTH=6, proptest case counts named in justfile/docs/testing.md; buildfuzz_http_mcp_request,fuzz_upload_putfirst (2 days); CC-2's sections first. Size 2-3 engineer-days of code plus machine time. - CD-I1 Blocker. The in-repo seed is the starting point:
bookspec/README.md(schema:id, title, rule, status, sources, tags, appendix?;[setup]fields;[[events]]kindslines | reverses | close | bill | invoice | run_depreciation | dispose_asset | run_schedules | register_scheduleplusrefused, payee, parties, instrument, older;[[changes]];[[expect]]with ~30 queries); 56 examples; loadercrates/ledger-core/tests/bookspec.rs(#[serde(deny_unknown_fields)]structs,every_bookspec_example_holds, applies events to a realStoreandverifys);ledger-package/tests/package.rs::the_bookspec_iif_example_matches_the_engine. TheEnginetrait is today the loader overStore. - CD-I2. P4b's bookspec ids need loader extensions:
InvoiceSpec/BillSpeclackdue_date/terms; noreceive_payment/pay_bill/void_document/write_offevents; noar_aging/ap_aging/control_check/documentqueries; the refusal vocabulary needsUntaggedControlLine. - IR-P13-1 order. 1 move the loader into
bookspec/checker/(own Cargo workspace) exposingSetup,Event,Expect;ledger-core/tests/bookspec.rsbecomes the adapter by path dependency; 2pub trait Engine { fn load(&mut self, &Setup) -> Result<(), Refusal>; fn apply(&mut self, &Event) -> Result<(), Refusal>; fn query(&self, &Query) -> Result<Value, Refusal>; fn claims(&self) -> Claims; },Refusal { rule: RuleId, reason }; 3 rule idsBS-<area>-<n>mapped from B-1..B-41; 4 the spec skeleton with a done line ("every B-rule has an id, statement, source, one example; the five laws are Lean theorems inbookspec/lean/"); 5 runner (pass/fail/not claimed), GnuCash adapter later. In-tree until step 5. Size 15-20 days.
Cross-cutting reuse to cite (CD-J)
crates/ledger-cli/tests/common/mod.rs: Home (new, create, adopt, token, token_with, admin(Request)), Server::start, Wire (initialize, call, rpc, upload, begin_and_send, fetch, fetch_text), Books (new, with, placed, adopted, wire, session, settle), upload_args, answer_of; parity precedent projects_parity.rs; older-rules fixtures store.rs::books_written_under_older_rules_still_open, arap_standing.rs::legacy_documents_replay_with_tags (exists; plan lists it as to-build); BooksServer::as_of(Option<&str>) the one date source; BooksServer::hand_over(book, file_name, mime, bytes) -> HandedFile; parse_doc_ref("invoice:7"), doc_ref_text, doc_label, movement_key (an "INV-0007" form is a new parser arm).
Summary table
| Unit | Depends on | Non-code prerequisites | Size (eng-days) |
|---|---|---|---|
| Pre-steps (recipes, testing.md S1B/S2/S3, attention over PackageBooks, server.rs split) | — | — | 4-6 (+ the 700-line attention refactor) |
| S4 | S1-S3 | go-ahead for live use only | 12-16 (4 PRs) |
| S5 | S1 | same | 3-4 |
| S6 | S4 step 1 | same | 8-10 |
| S7 (cut) | S4 step 6 | same | 4-6 |
| P4 (cut) | S4-S7 MCP steps | shared go-ahead | 8-10 |
| P5 | P4; envelope single-owner | live-session check | 10-12 |
| P6 | — | — | 8-10 |
| P8 | P6 step 3, P4 | — | 10-12 |
| P9a / P9b | — / D-7 | machine, tunnel, TLS / reviewer | 3-4 / later |
| P10 | P5, P6, P8 | owner-run | 1 |
| P11a / P11b dock | — | certificates, keys, VMs | 20-25 / 8-10 |
| P12 | everything | desktop | 2-3 + runs |
| P13 | — | — | 15-20 |