diff --git a/crates/ledger-cli/tests/defects.rs b/crates/ledger-cli/tests/defects.rs new file mode 100644 index 0000000..cc3fe43 --- /dev/null +++ b/crates/ledger-cli/tests/defects.rs @@ -0,0 +1,265 @@ +//! The re-audit's engineering mirror (plan P10; `docs/runbooks/re-audit.md`): +//! every defect the 2026-10-16 audit found (1 to 20), P0 (21, 22) and P3 +//! (23, 24) has a named regression test somewhere in the workspace, and the +//! runbook names only tools and commands that exist. +//! +//! The list below is the single place that says which test stands guard for +//! which defect. A test renamed or deleted fails here, so a defect's +//! reproduction cannot be lost quietly. This proves the tests exist, are +//! real `#[test]`s and are not ignored; that they pass is `cargo test`'s +//! job (the runbook's step 4 runs them by name). +//! +//! SOURCE: plan/served.md 8.0 ("defect n") and P10 step 2. + +use std::path::{Path, PathBuf}; + +fn root() -> PathBuf { + Path::new(env!("CARGO_MANIFEST_DIR")) + .join("../..") + .canonicalize() + .unwrap() +} + +fn read(rel: &str) -> String { + std::fs::read_to_string(root().join(rel)).unwrap_or_else(|e| panic!("{rel}: {e}")) +} + +/// `(defect, test name, file)`. Defect 2 is closed by D-12 and D-47 (the +/// rule it now shows is that a reversal needs no receipt of its own); +/// defect 20 (the migration scripts) was retired at the cutover and is +/// checked separately below. +const DEFECTS: [(u32, &str, &str); 23] = [ + ( + 1, + "defect1_two_member_llc_first_year_regression", + "crates/ledger-core/tests/spec_cash_flow.rs", + ), + ( + 2, + "a_reversal_needs_no_receipt_of_its_own", + "crates/ledger-cli/tests/drafts.rs", + ), + ( + 3, + "get_receipt_default_has_text_and_no_bytes", + "crates/ledger-cli/tests/evidence.rs", + ), + ( + 4, + "same_bytes_one_record_many_links", + "crates/ledger-store/tests/attachments.rs", + ), + ( + 5, + "a_draft_waits_for_its_receipt_and_posts", + "crates/ledger-cli/tests/drafts.rs", + ), + ( + 6, + "digest_since_seq_is_posting_time", + "crates/ledger-cli/tests/drafts.rs", + ), + ( + 7, + "the_readme_lists_what_the_iif_writes_differently", + "crates/ledger-package/tests/package.rs", + ), + ( + 8, + "add_account_warns_without_tax_category_defect_8", + "crates/ledger-cli/tests/spec_tax_policy.rs", + ), + ( + 9, + "defect9_a_new_schedule_spreads_by_the_day", + "crates/ledger-core/tests/schedules.rs", + ), + ( + 10, + "annotations_replay_and_project", + "crates/ledger-store/tests/attachments.rs", + ), + ( + 11, + "defect11_the_directory_shows_a_members_tin_last4", + "crates/ledger-cli/tests/spec_vault.rs", + ), + ( + 12, + "payment_methods_round_trip_and_classify_payments", + "crates/ledger-cli/tests/spec_1099.rs", + ), + ( + 13, + "four_contributed_devices_each_take_the_lower_of_value_and_basis", + "crates/ledger-core/tests/spec_asset_basis.rs", + ), + ( + 14, + "defect14_party_totals_take_a_basis", + "crates/ledger-cli/tests/spec_party_totals.rs", + ), + ( + 15, + "rename_account_end_to_end_defect_15", + "crates/ledger-cli/tests/mcp_journeys.rs", + ), + ( + 16, + "one_default_period_everywhere_defect_16", + "crates/ledger-cli/tests/mcp_journeys.rs", + ), + ( + 17, + "the_audit_odd_cent_never_piles_up_across_closes", + "crates/ledger-core/tests/odd_cent.rs", + ), + ( + 18, + "undecided_is_not_not_reportable", + "crates/ledger-core/tests/form1099.rs", + ), + ( + 19, + "no_tool_takes_a_path", + "crates/ledger-mcp/src/server_tests.rs", + ), + ( + 21, + "as_of_package_iif_stops_at_as_of", + "crates/ledger-package/tests/package.rs", + ), + ( + 22, + "defect22_depreciation_through_mid_final_month_is_prorated_not_plugged", + "crates/ledger-core/tests/spec_assets_payroll.rs", + ), + ( + 23, + "a_vendor_marked_no_1099_is_not_undecided", + "crates/ledger-cli/tests/spec_1099.rs", + ), + ( + 24, + "defect24_an_accrual_through_another_liability_counts_once", + "crates/ledger-core/tests/spec_party_totals.rs", + ), +]; + +/// Defects 1 to 24 each have a named test that exists, is a test, and is +/// not ignored (defect 20 is the retired scripts, below). +#[test] +fn every_defect_reproduction_is_a_named_test() { + let mut covered: Vec = DEFECTS.iter().map(|d| d.0).collect(); + covered.push(20); + covered.sort_unstable(); + assert_eq!( + covered, + (1..=24).collect::>(), + "a defect is missing" + ); + + let mut missing = Vec::new(); + for (defect, name, file) in DEFECTS { + let text = read(file); + let lines: Vec<&str> = text.lines().collect(); + let needle = format!("fn {name}("); + let Some(at) = lines.iter().position(|l| { + l.trim_start().starts_with(&needle) + || l.trim_start().starts_with(&format!("pub {needle}")) + }) else { + missing.push(format!("defect {defect}: no `fn {name}` in {file}")); + continue; + }; + // The attributes and doc lines above the function, up to the + // previous blank line or item. + let above: String = lines[at.saturating_sub(8)..at].join("\n"); + let in_proptest = text.contains("proptest!"); + if !above.contains("#[test]") && !in_proptest { + missing.push(format!( + "defect {defect}: `{name}` in {file} is not a #[test]" + )); + } + if above.contains("#[ignore") { + missing.push(format!("defect {defect}: `{name}` in {file} is ignored")); + } + } + assert!(missing.is_empty(), "{}", missing.join("\n")); +} + +/// Defect 20, the migration scripts, was retired at the cutover: there is +/// no tool for them, and none may come back as a way past the journal. +/// The defect 19 mirror (`import_local_file` and every path parameter) is +/// `no_tool_takes_a_path` above. +#[test] +fn defect_20_no_migration_tool_exists() { + let scopes = read("crates/ledger-mcp/src/scopes.rs"); + for line in scopes.lines().filter(|l| l.contains("(\"books.")) { + for banned in [ + "migrate", + "import_local_file", + "import_sheet", + "verify_sheet", + ] { + assert!(!line.contains(banned), "a retired tool is back: {line}"); + } + } +} + +/// The runbook names only tools and commands that exist: each +/// `books.` is in the scopes table (the 8.0.3 list of tools), each +/// `wanderbooks ` is a command `main.rs` dispatches, and each +/// test file it names is a file. +#[test] +fn the_re_audit_runbook_names_only_what_exists() { + let runbook = read("docs/runbooks/re-audit.md"); + let scopes = read("crates/ledger-mcp/src/scopes.rs"); + let main = read("crates/ledger-cli/src/main.rs"); + + let mut tools = Vec::new(); + let mut rest = runbook.as_str(); + while let Some(i) = rest.find("books.") { + let tail = &rest[i..]; + let end = tail + .find(|c: char| !(c.is_ascii_alphanumeric() || c == '_' || c == '.')) + .unwrap_or(tail.len()); + let name = tail[..end].trim_end_matches('.'); + // Prose such as "books.start's" ends at the apostrophe; a bare + // "books." (the company's books followed by a full stop) is not a name. + if name.len() > "books.".len() && !tools.contains(&name.to_string()) { + tools.push(name.to_string()); + } + rest = &tail[end.max(1)..]; + } + assert!( + tools.len() > 20, + "the runbook should name its tools: {tools:?}" + ); + let unknown: Vec<&String> = tools + .iter() + .filter(|t| !scopes.contains(&format!("(\"{t}\""))) + .collect(); + assert!( + unknown.is_empty(), + "tools the runbook names that do not exist: {unknown:?}" + ); + + for cmd in ["gui", "connect"] { + if runbook.contains(&format!("wanderbooks {cmd}")) { + assert!(main.contains(&format!("\"{cmd}\"")), "no command {cmd}"); + } + } + assert!( + runbook.contains("wanderbooks gui"), + "step 6 names the viewer" + ); + + for step in 1..=8 { + assert!( + runbook.contains(&format!("## Step {step}.")), + "the runbook lost step {step}" + ); + } + // The runbook must not point at the data root or tell anyone to read it. + assert!(!runbook.contains("wanderbooks.v"), "names a database file"); +} diff --git a/docs/runbooks/re-audit.md b/docs/runbooks/re-audit.md new file mode 100644 index 0000000..5d963a7 --- /dev/null +++ b/docs/runbooks/re-audit.md @@ -0,0 +1,189 @@ +# The re-audit, through the MCP alone + +The owner's acceptance of the whole programme (plan/served.md, P10). An +agent audits a company's books using only the books tools, the books' own +filed documents and the viewer's Export menu. Nothing here reads the data +root, a database file or the journal file. The engineering mirror of this +runbook is `crates/ledger-cli/tests/defects.rs` (every tool named below +exists; every defect has a named regression test); the run itself is the +owner's, on the owner's books. + +Money is a decimal string everywhere. Every tool named is a tool in +`books.start`'s list; if one is missing, stop: the runbook is stale, not +the books. + +## Before you start + +- Two roles. The **auditor** runs `books.audit`, reads, and proposes fixes. + The **verifiers** (at least two agents, not the auditor, not whoever made + the entry) attest. The human confirms every correction. +- The bar is `policy.attestation_bar` (default `effort=high verifiers=2 + adversarial=true`, D-9). Read it with `books.settings`. A verifier's + `method` is its own claim, said truthfully: `{ model, reasoning_effort, + adversarial, verifier_agents }`. A reading that differs from the entry is + recorded with `disagrees: true` and never counts; never bend a reading to + fit. +- Context discipline. A receipt is read as text first + (`books.get_receipt`, the default `meta` view is under 8 KiB, `view: + "text"` gives the whole text), as one page picture (`view: "page_image"`, + `page`, `max_px`) only when the text is empty or looks wrong, and never as + `bytes`. Record each verifier's own token count for receipts; the exit + bar is under 100K tokens per verifier. +- Start with `books.start` (rules, conventions, open items, its `next_steps`) + and `books.handbook` section `attestations-and-audit`. The `audit` prompt + (`scope`: `month`, `annual`, `cpa_handoff` or `full`) is the same + procedure for a client that offers prompts. +- Audits and attestations need the books' `audits` feature (the owner's + format upgrade). Without it the tools refuse with `format_upgrade_pending`: + do the upgrade first, as its own step, with the owner. + +## The two cases + +- **T1, desktop only.** Claude Code and Codex on the owner's machine, through + `wanderbooks connect`. Separation between the agent and the data root is + advisory. The evidence that it held is the access log + (`books.access_log`, `books:admin`) and the agents' transcripts, which never + name the data root or a path into it. +- **T2, a self-host machine running** (D-30, D-31; `deploy/README.md`). The + same steps, plus claude.ai through the tunnel once P9b exists. Separation + is enforced by the machine's account split. Until P9b, T2 is T1 with the + server on its own machine. + +The steps are identical; only the evidence in step 8 differs. + +## Step 1. The first audit and its fixes + +1. `books.audit { scope: "full", through: }` without + `confirmed`: it lists the checks and changes nothing. Show the human. + Then run it with `confirmed: true` and the `preview` digest. Do the same + for `{ scope: "cpa_handoff", through: }`. +2. `books.workpaper { id }` for each run: the findings and the PDF as filed. +3. Fix every finding through journaled corrections, never an edit: + - a wrong posted entry: `books.reverse_entry { entry_id, date, + description }`, previewed, then `confirmed: true`; then the right entry + through `books.draft_entry`, `books.add_line`, `books.post_entry` + (preview, then `confirmed`); + - a reversal with no receipt of its own (an old one, `legacy_reversal_exempt`): + its evidence is its original's (D-12, D-47). Link the original's receipt + to the reversal with `books.link_attachment { attachment_id, subject }`; + `books.list_receipts { entry_id }` finds the receipt; + - a duplicate file: `books.consolidate_duplicates` (preview, then + `confirmed`); `books.find_attachment` to look one up by hash or number; + - a receipt's facts: `books.annotate_attachment`; + - an open draft: post it, or `books.discard_draft { reason }`; + - an undecided 1099 vendor: `books.set_vendor_1099`; + - a missing tax category: `books.set_tax_category`; + - a judgment the books should remember: `books.record_decision`. + Nothing is fixed without the human's confirmation. +4. Audit again at the new head. An audit is idempotent on the same books + (`already_run`), so a new run means the books changed. Repeat until the + `full` and `cpa_handoff` runs have no error findings; keep the final two + run ids. Each journaled fix can be traced with `books.journal_history + { subject }`. + +## Step 2. Every receipt attested + +For every receipt on a posted entry (`books.list_receipts`, entry by entry, +or `books.search_entries` then `books.get_entry`): + +1. Verifier A reads `books.get_receipt` (text; a page image only if needed) + and calls `books.attest_receipt { attachment_id, entry_id, amount, date, + method, note? }` with the total and the date as read off the receipt. +2. Verifier B does the same independently, without seeing A's reading. +3. `books.attestations { kind: "receipt" }` shows each claim with + `counted`. A claim below the bar, or with `disagrees: true`, does not + count; the disagreement is a finding: reopen step 1 for that entry. + +## Step 3. Every entry reviewed, every period attested + +1. `books.review_entry { entry_id, method }` for every posted entry, by a + verifier that did not make it. +2. `books.attest_period { period, method }` for each month since the + company's formation (`books.list_periods`), each against a standing + audit at the current head (`scope: "month"` or `"full"`), then once for + the fiscal year against the `cpa_handoff` run. A period attestation is + refused when the books changed since the audit: audit again, then attest. +3. `books.attestations` with no filter: every row has `counted: true` and + none has `disagrees`. + +## Step 4. The defect regressions + +The 2026-10-16 audit found 20 defects; P0 found two more (21, 22) and P3 +two (23, 24). Each has a named regression test in the workspace; +`crates/ledger-cli/tests/defects.rs` holds the list `(defect, test, file)` +and proves each name exists. Show them green, from a checkout of the build +under audit: + +``` +cargo test -p ledger-cli --test defects +``` + +then each test named in that list, by name, for example +`cargo test -p ledger-core --test spec_cash_flow +defect1_two_member_llc_first_year_regression`. Two defects have no code +fix to test: defect 2 (reversals without their own evidence) is closed by +D-12 and D-47 (its test shows the rule now) and defect 20 (the migration +scripts) was retired at the cutover (no tool exists; the test checks none +came back). Record the test run's output in the sign-off decision (step 7). + +## Step 5. The Sheet comparison + +The old Google Sheet and the books were frozen as of 2026-10-11 and +compared a final time before the cutover (P2, `verify-sheet`); that run is +filed as a document. There is no comparison tool and this +runbook builds none. + +1. `books.find_attachment { text: "verify-sheet" }` finds the filed run + (its extracted text names the tool); `books.get_receipt { attachment_id }` + reads it (text view; its `links` and `sha256` identify it). Cite it by + attachment id and sha256. If it is not found, stop: the comparison was + not filed at the cutover and the owner decides what stands in for it. +2. `books.record_decision { subject: "Sheet comparison as of 2026-10-11", + decision_md, decided_by: [], decided_on, evidence_attachment: + }` stating that the frozen Sheet and the books + agreed then, and nothing newer is claimed. + +## Step 6. The CPA package from both doors + +1. The agent's door: `books.export_cpa_package { as_of: , + include_tins: true }` (TINs are included by default, D-29). Without a + counted audit it is refused (`audit_required`); never use + `without_audit_reason` for the re-audit, the point is that it is not + needed. Save the zip with the download command in its `file`. +2. The viewer's door: `wanderbooks gui `, the Export menu, the CPA + package as of the same day. It lands in the Downloads folder. +3. Compare the two zips file by file: byte-identical except for timestamps + (the README's generated-at line and the zip entries' times). A difference + in any figure is a defect: stop and report it. + +## Step 7. Verify, back up, sign off + +1. `books.verify { blobs: true }`: `status: "ok"` (not `healed`: a healed + result means a torn tail was set aside; note it and look at + `books.journal_history`'s `tail_repairs`). +2. `books.backup`: every target not `stale`, `last_error` empty, and + `last_drill_at` set with no `drill_error`. If not, `books.back_up_now` + (an admin action) and read it again. The drill is the restore test; a + backup that was never restored is not a backup. +3. The owner's sign-off: `books.record_decision { subject: "Re-audit + sign-off", decision_md, decided_by: [], decided_on }` naming + the final `cpa_handoff` and `full` run ids, the journal head + (`books.verify`'s `head`), the defects run, and the CPA package's sha256. + +## Step 8. The exit evidence + +The re-audit passes only when all of these are true. The owner checks them; +the agent presents the proof for each. + +| Exit bar | Proof | +|---|---| +| Zero failing workpaper checks | the final `full` and `cpa_handoff` workpapers (`books.workpaper`) | +| Cash flow difference 0.00 | `books.cash_flow` for the year; no `cash_flow_unfooted` finding | +| Every receipt and posted entry has a counted attestation, none disagrees | `books.attestations` (step 3.3) | +| Every action under the right principal | `books.access_log`: the auditor's token and each verifier's token appear on their own calls; no action by an admin token that an agent token did | +| No transcript names the data root | grep each agent's transcript for the root path before sign-off (T1); T2: the tenant root is not readable by the agent's account | +| No result carried a full TIN to a client that should not see one | `books.access_log` shows `include_tins` only on `books.export_cpa_package` and `books.get_w9`/`books.vendor_1099` calls by the owner's agent; the agents' exports may carry them (D-29), nothing else | +| Receipt reading stayed under 100K tokens per verifier | each verifier's own token count for its receipt calls (the harness's count, written into the attestation `note`) | +| The sign-off is journaled | `books.decisions` shows it | + +When all are true the books are re-audited through the MCP alone. diff --git a/docs/testing.md b/docs/testing.md index db1c715..a643a37 100644 --- a/docs/testing.md +++ b/docs/testing.md @@ -3922,3 +3922,33 @@ export gate (`cpa_audit_standing`'s four conditions and its feature test, the `refuse`/`warn` split, the reason's trim); `audit_items` in Needs attention (the declared test, the attested set, the last-ended fiscal year, the three year conditions). + +## P10: the re-audit runbook's mirror + +`docs/runbooks/re-audit.md` is the owner's acceptance, run on his books +through the MCP alone. What CI can check of it is in +`crates/ledger-cli/tests/defects.rs`: + +- `every_defect_reproduction_is_a_named_test`: a table of `(defect, test + name, file)` for the 2026-10-16 audit's defects 1 to 19, P0's 21 and 22 + and P3's 23 and 24 (23 rows); each named function exists in its file, is + a `#[test]` (or inside `proptest!`) and is not `#[ignore]`d. Defect 2 is + closed by D-12 and D-47, so its row is the rule as it now stands + (`a_reversal_needs_no_receipt_of_its_own`). +- `defect_20_no_migration_tool_exists`: defect 20 (the migration scripts) + was retired at the cutover; no tool of that kind is in the scopes table. +- `the_re_audit_runbook_names_only_what_exists`: every `books.` in + the runbook is in `crates/ledger-mcp/src/scopes.rs`, the commands it + names are dispatched by `main.rs`, and its eight steps are present. + +The mirror proves the tests exist, not that they pass; the runbook's step 4 +runs them by name. No CI job was added: the test runs in the existing +`check` job's `cargo test --workspace`. The run itself (a counted +attestation on every receipt and entry, the two CPA packages equal, the +sign-off) is the owner's and has no CI equivalent. + +**For P12 (mutation targets):** the `DEFECTS` table's file/name lookup (a +`starts_with` on `fn (`; the `#[test]`/`#[ignore]` window of eight +lines above it) and the scopes-line scan in `defect_20_*`. A mutant that +widens the window or drops the `ignore` check survives only if no ignored +or untested name is in the table; the table is the oracle. diff --git a/plan/served.md b/plan/served.md index 5ad3f33..10e279a 100644 --- a/plan/served.md +++ b/plan/served.md @@ -1639,7 +1639,7 @@ Each unit follows the 8.0.4 template. "A-n" is an owner answer numbered in 8.3.1 5. **Files:** owns `docs/runbooks/re-audit.md`, `crates/ledger-cli/tests/defects.rs`. 6. **Tests and proofs:** the one test. 7. **Done when (engineering):** the runbook exists and names only tools and commands that exist; the test passes. **Exit (the owner's run):** zero failing workpaper checks; cash flow difference 0.00; every receipt and posted entry carries a counted attestation and none disagrees; the access log shows every action under the right principal and no transcript names the data root; no result carried a full TIN to a client that should not see one (D-29: the agent's exports may); agent context on receipts stayed under 100K tokens per the harness's own token count; the sign-off journaled. -8. **As built:** none yet. +8. **As built:** (2026-10-29, branch `P10`.) `docs/runbooks/re-audit.md`: eight steps (the audit and its fixes; receipts attested; entries reviewed and periods attested; the defect regressions; the Sheet comparison; the CPA package from both doors; verify, backup drill and sign-off; the exit table), T1 and T2, every tool named is in the scopes table. `crates/ledger-cli/tests/defects.rs`: `every_defect_reproduction_is_a_named_test` (23 rows, defects 1 to 19 and 21 to 24), plus two the unit did not list: `defect_20_no_migration_tool_exists` and `the_re_audit_runbook_names_only_what_exists` (Done-when "names only tools that exist" needed a check). Departures: (a) **`books.inherit_evidence` does not exist** (only the event type is gated in `format.rs`; no tool, no scope row); the runbook links the original's receipt to the old reversal with `books.link_attachment`, which journals `attachment.linked` and satisfies the evidence rule, and `legacy_reversal_exempt` is informational so the audit is clean either way. If the owner wants the D-12 annotation as such, a tool is a later unit. (b) Defect 2 (closed by D-12/D-47) maps to `a_reversal_needs_no_receipt_of_its_own`'s rule; defect 20 has no test of its own and gets the no-tool check. (c) The Sheet workpaper is found with `books.find_attachment { text }` because `workpaper` is not a `books.file_document` category (the audit files its own). (d) No CI job added: `check` runs the new test. **Owner's, not checked here:** the run itself (zero failing checks, counted attestations, equal CPA packages, backup drill, sign-off, transcripts free of the data root, token counts). #### P11a: packaging and updates (D-19, D-22, D-24, D-25, D-26)