42 lines
11 KiB
Markdown
42 lines
11 KiB
Markdown
# Stage 8: Intermediate zoom-out on the proposed mitigations (`plan/served.md` §8, 971-1889)
|
||
|
||
Reviewer: `egm-zoom-out`, 2026-10-26, having read the Stage 5-7 reports and the code they cite. Judges the proposed fixes before they are applied.
|
||
|
||
## 0. The one change that dissolves most of the design blockers: drop "reversal as a draft"
|
||
**Blocker. P4 971-993; I-4/I-5 (64-65); 4.9 513; 5.2 574; DC-A2, DC-A3, DC-B3, ST-F4, comprehension F1/F3/F5/E5.**
|
||
No owner decision asks for a reversal to be a draft: top decision 5 and D-12 say old reversals borrow the original's receipt; the live handbook and `ReceiptPolicy` say reversals need none; S1 built `void_document` as an immediate reversal and the owner upgraded to it. A reversal stays previewed-then-posted in one call, every reversal check at the moment it posts (what `check_reversal` already does), evidence = the original's by rule; evidence a reversal genuinely needs of its own (a returned-item notice for a bounce) is attached right after, as every at-once tool works today. Delete from P4: `entry.reversal_drafted`, `Step.draftReversal`, `evidence: link_original|{ids}|later`, the "reversal draft needs evidence" attention item, `reversal_needs_evidence`. Rewrite I-4: "Corrections are posted reversals; a reversal's evidence is its original's." `inherit_evidence` becomes an audit annotation only (D-12 names it: keep as a one-line event for the re-audit).
|
||
|
||
## 1. The critic's mechanisms, one by one
|
||
- **1a `settlement.posted` (DC-A4): over-engineered.** Replay already tags "the first untagged control line for `amount`" (ledger.rs:6466). Simpler: add `line` (and `discount_line`) indices to the existing `invoice.payment_received`/`bill.paid` events under `settlements`; the tool writes lines in a fixed order (payment lines in application order, then discount lines).
|
||
- **1b `Movement::Recovery` + role (DC-A11): YAGNI.** Handbook rule: same tax year, reverse the write-off then `receive_payment`; prior year, a plain entry Dr deposit / Cr "Bad debt recoveries" (revenue, tax category `other_income`), memo naming the invoice; the document stays Written off. Record as a decision.
|
||
- **1c `receive_payment.excess_to` (DC-A6): contradicts answer 5 and is unnecessary.** Replacement-before-bounce works today: park the cash Dr 1000 / Cr 2300 (no role, so allowed); reverse payment 1 dated the bounce day; `receive_payment {deposit_to: 2300, date ≥ bounce}` (`check_payment_account` accepts a role-less liability; bounds pass). Write it as a handbook procedure.
|
||
- **1d discounts on asset-line bills (DC-A7): take the second option.** Refuse a discount on a bill with an asset or prepaid line, remedy in the error (record the bill net, or reclassify Dr 6995 / Cr 1500). Record the decision.
|
||
- **1e the fee (DC-A5): Blocker on the document, Major on the scope.** The fee goes in S6. One kernel identity check with Kani (Σcash + Σdiscount == Σapplication; deposit == Σcash − fee); do not refactor `payout_plan`. Copy the payout pattern for the fee line (same expense account payouts use). `receive_payment {…, fee?: {amount, account?}}`, one posting row, behind `settlements`.
|
||
- **1f `adopt_control_line` variants (DC-A8): defer the whole tool.** P4b.14 item 4 already gives zero-code remedies (reverse and re-enter; or leave it with a decision and a CPA README note, which the identity's `unattached` term exists for). Build it only if `books.control_check` on the live books lists a line neither remedy can take. Same for `assign_role move_balance` (B-23 already says a control role with a balance stays). S7 shrinks to `write_off_invoice` + `bad_debts` role + B-41.
|
||
- **1g `owed_on` per-account identity (DC-A9): falls away with 1f.**
|
||
- **1h evidence gate (DC-A10/B2): right direction, four times too big.** Three moves: (1) move the policy evaluation from `ledger-config` into `ledger-core` as one function over `(&Ledger, &JournalEntry, &Settings)`; exemptions by origin use what `Ledger` already knows (closing entries, schedule postings, depreciation runs); (2) `Store::post_entry` calls it; (3) the GUI calls the same function over its Snapshot and deletes the string match at books.rs:708. At-once tools stay "post at once, attach evidence right after" with the existing `missing_receipt` item; the invoice carries its PDF and the bill its upload anyway (S4).
|
||
- **1i two-shape HMAC `preview_token` (DC-B1): one digest, no key, one choke point.** Every preview result (a draft's `preview_entry`, and every tool called without `confirmed`) returns `preview: sha256(canonical preview payload)` (draft: canonical draft state; at-once operation: canonical args + journal head seq); every confirm takes `preview`, recomputes, refuses `stale_preview` on mismatch. Computed in the result envelope, not per tool; no key file; required from the P4 sweep on (no "one release" grace).
|
||
- **1j jobs as lease holders (DC-C2): remove with `books.job`.** 3.9 as built lets a running request finish before a close.
|
||
- **1k audit split (DC-C3): accept the "current head" definition (a sentence), not a layered architecture.** Pure checks beside `Ledger::control_check`; `Store::verify`/`verify_blobs`/access-log chain in the store; the tool composes. One `audit.run {through, through_seq, workpaper_attachment, findings}` event.
|
||
|
||
## 2. The template: 8 fields, not 17
|
||
Keep: **Status**; **Depends on** (merge-first units and the owner go-ahead); **Feature and schema** (allocated name and projection schema number, or none); **Build** (each bullet ends `crate/path::symbol new|changed`; Core/Store/MCP/Viewer sub-bullets only where present); **Files** (owns / also touches); **Tests** (named, with file; proofs here too); **Done when** (numbered, checkable by the EM without the owner's books); **As built**. Drop: Runs in parallel with / Blocks (derivable; state once in the 8.0 table), Decisions in force (one reverse index in 10.4), Defects closed (fold into Done when), Docs (say once in 8.0), Risks (§9 only; a unit-specific risk is a sentence in Build), Open questions (a unit with one is not ready; they live in 10.3 and block Status). Keep the P4b reference design adjacent to the P4b slices; strike superseded sentences inline "→ D-n".
|
||
|
||
## 3. Do the zoom-out cuts remove anything D-19..D-46 requires?
|
||
- **3a P7 → scanner fix + decision: no conflict.** D-19's "human path (P2 `/h/`, P7 vault)" is superseded by D-28/D-29; D-31 needs only that the key travels in the backup (it does). Strike v2 residue in 3.4, 3.5, §6 (`keyring.json`, `preview.key`, `human.*` actions, `unwrap_only_in_human_handler`) "→ D-28/D-29".
|
||
- **3b P6 → extraction + dedup + annotations: keep one thing.** D-20: "P6 adds what makes it pleasant": document categories (notices, plans, history) and the viewer's Documents page grouped by category. Keep them (small) or record a deferral.
|
||
- **3c P9 split: one wording fix.** Top decision 4 says claude.ai on the web can reach the self-host machine, so P9b (claude.ai via D-7(b) external IdP) stays a planned unit after the P10 path, not "when wanted". P9c (replica viewer, invites) deferred by a decision row.
|
||
- **3d P8: partly conflicts with D-9.** "Counted full audit" must be computable, so the bar cannot be prose only. Minimal: one `policy.attestation_bar` setting (effort level, verifier count); attest tools record the self-reported `method`; "counted" evaluated against the bar in force at query time. Drop bar versioning, prospective semantics and the Lean lemma.
|
||
- **3e P11: "minisign only" conflicts with D-25/D-26 as recorded** (the two-key manifest with rotation and expiry is the owner's choice). Do not cut silently: one owner question (keep the manifest or minisign only), recorded either way. The P11a/P11b split and "the dock spawns the agent as an ordinary child" are safe; D-27 keeps the dock a unit.
|
||
- **3f Z-4, Z-9, Z-13, Z-17: no conflicts** (D-34 says bookspec is "its own public project": say it moves out when the harness is stable; the 56 examples keep running in `just check` meanwhile).
|
||
|
||
## 4. Missed at the architecture level
|
||
- **4a Major. Six P4b features where the owner decided one remaining upgrade.** `document_standing` is declared on the live books (2026-10-24). Answer 5 leaves exactly one more go-ahead, after S7. Fold S4-S7 into one feature, `documents_v2`, declared once after S7 (with 1f, `control_lines` disappears). State once: on the owner's books, S4-S6 writes carrying new fields or event types are refused with the upgrade remedy until the S7 upgrade; everything else works at once. If that wait is unacceptable, the EM asks the owner once for an upgrade after S4.
|
||
- **4b Major. Order.** With §0 and 1i, P4 is small and has no reason to precede S4-S7. Order: S4 → S6 (S5 and S7 beside), then P4 as one sweep adding `preview` to every confirm tool at the envelope, then P5.
|
||
- **4c Nit.** Settings nobody asked for in S4: `invoicing.paper`, `reminder_days`/`remind_days` as settings (constants until asked); q×u only as printed text, "refused unless q×u equals amount exactly at the given scale".
|
||
- **4d Nit.** "Filed in the same store call" vs D-46 (B6): D-46 is about the agent's context, not server-internal writes; the server stores the bytes it rendered through the store's own attach path (`Store::attach_receipt` takes bytes); the DRAFT preview PDF is a `HandedFile` like any export.
|
||
- **4e Nit.** The attention move is a 700-line refactor: `attention::find` reads a GUI-built `Books` (origin labels, `missing_receipt`, depreciation maps, W-9 status). Moving it over `PackageBooks` means generalising that builder from `Snapshot` to `&impl PackageBooks` (books.rs:689ff). Size the pre-step accordingly; still no new crate or trait.
|
||
|
||
## Recommendation (the shortest list that makes the document executable)
|
||
(1) Drop reversal-as-draft everywhere. (2) Rewrite P4 as a small sweep after P4b: draft states, `discard_draft`, the policy function moved into ledger-core and called by `Store::post_entry` and the GUI, one `preview` digest at the result envelope required by every confirm, defect 6's digest fields. (3) In P4b: one remaining feature (`documents_v2`) with one upgrade after S7 and the stated refusal window; the fee in S6 by the payout pattern with one kernel identity check; `line` indices on the existing payment events; handbook procedures for recovery, replacement-before-bounce (via 2300) and discounts on asset bills (refused with remedy); S7 cut to `write_off_invoice`; `adopt_control_line`, `move_balance`, `owed_on`, `Movement::Recovery`, `excess_to` and apportioned discounts each recorded as a deferral decision. (4) P5 without `books.job`, URL elicitation or jobs-as-leases; P6 as extraction + dedup + annotations + D-20's categories; P7 as the scanner fix + a decision closing vault v2; P8 composed from existing checks with one bar setting evaluated at query time and one `audit.run` event; P9 as a recipe + bearer unit now and a D-7(b) claude.ai unit later, remote viewer and invites recorded as not planned; P11 split, with one owner question on the D-25 manifest before any cut. (5) A pre-step unit, single owner: `server.rs` split by tool group and `attention::find` moved over `PackageBooks`. (6) The 8.0 status/dependency/shared-files table and the 8-field template; superseded text struck inline "→ D-n"; symbol names never line numbers; the owner's answers numbered A-1..A-14.
|