Attachments, statements, reconciliations, TIN vault, counterparty policy
check / check (push) Canceled after 0s
check / check (push) Canceled after 0s
MCP: books.link_attachment, unlink_attachment, attachments; link_to on attach_receipt and file_document; record_statement, statements; books.reconcile previews and, confirmed against a recorded statement, records the reconciliation; void_reconciliation, reconciliations; import_bank_csv links its source file; digest lists unreconciled accounts. TIN vault: books.set_w9 takes the TIN and the form into the encrypted vault (last four in the books); books.vendor_1099 include_tins and save_csv_to for the CPA's copy; books.get_w9; books.purge_tin. A leak test scans every tool output, the journal, the database, and the blobs. [books] parties = off | suggest | require (default suggest): every preview names the entry's party or suggests likely ones; require refuses to post until it names one or is marked as having none (entry.no_party). Viewer: documents on every subject page; statements and reconciliation history per account; reconciliation pages; reconcile and no-counterparty attention items; party money by period (paid out, received in, net, what for, by month); Needs attention gets its own full-width section. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GWVo7x3UHw5kwCUaWxiejX
This commit is contained in:
1 parent
e265ed4339
commit
ae927c3f73
41 files changed
+8463
-465
No files matched your search
+170
-35
@@ -150,7 +150,12 @@ below.
|
||||
amounts wanderbooks may no longer know as of today (`{ name,
|
||||
known_through, catalog, source, why }`): research the current value or
|
||||
ask the human, then update it upstream (pull request or issue at
|
||||
github.com/swaits/wanderbooks).
|
||||
github.com/swaits/wanderbooks). `unreconciled_accounts` lists the cash
|
||||
and card accounts due for reconciliation (`{ code, name,
|
||||
reconciled_through?, due_through, next_step }`): every `[books]
|
||||
cash_accounts` account and every liability with a recorded statement
|
||||
that has posted activity through the end of last month but no
|
||||
reconciliation (not voided) through it.
|
||||
|
||||
## Tools — fixed assets
|
||||
|
||||
@@ -226,12 +231,15 @@ through the policy.
|
||||
## Tools — the viewer
|
||||
|
||||
- `books.show { entry_id | account | report | attachment_id | asset |
|
||||
party | search | page, start?, end? }` → `{ shown, 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 transactions search,
|
||||
or a page (`parties` among them). Exactly one target; entries, accounts,
|
||||
attachments, assets, and parties are checked to exist. Never touches the books: the request is a small JSON file in
|
||||
party | reconciliation | search | page, start?, end? }` → `{ shown,
|
||||
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
|
||||
statements and reconciliations; viewer target `{show: "reconciliation",
|
||||
id}`), a transactions search, or a page (`parties` among them). Exactly
|
||||
one target; entries, accounts, attachments, assets, parties, and
|
||||
reconciliations are checked to exist. Never touches the books: the request is a small JSON file in
|
||||
the user's runtime directory (`$XDG_RUNTIME_DIR/wanderbooks/viewer-<hash
|
||||
of the data dir>.json`), written atomically, watched by the viewer. A
|
||||
viewer started within two minutes of a request opens it; older requests
|
||||
@@ -269,8 +277,9 @@ entries tagged with the vendor. Card payments move no asset (the card
|
||||
processor's 1099-K covers them), so they drop out; reversals of tagged
|
||||
entries carry the tag and net out. Thresholds: $600 for payments through
|
||||
2025, $2,000 from 2026 (indexed for inflation after 2026); always
|
||||
overridable, always for the CPA to confirm. TINs are never stored, only
|
||||
whether a W-9 is on file.
|
||||
overridable, always for the CPA to confirm. The books keep whether a W-9
|
||||
is on file and the TIN's last four; full TINs live only in the TIN vault
|
||||
("Tools — the TIN vault").
|
||||
|
||||
- `books.list_vendors {}` → `{ vendors: [{ id, name, form_1099?,
|
||||
tin_on_file, party_id }] }`.
|
||||
@@ -282,11 +291,21 @@ whether a W-9 is on file.
|
||||
takes `vendor` too; bills and their payments are tagged automatically.
|
||||
`books.set_entry_party` tags the vendor too when the party has a vendor
|
||||
record, so tagging by party counts for 1099s the same way.
|
||||
- `books.vendor_1099 { year, threshold? }` → `{ year, threshold, vendors:
|
||||
[{ vendor_id, name, form_1099?, tin_on_file, paid, reportable }],
|
||||
missing_w9, notes }`. For a year whose threshold wanderbooks doesn't know
|
||||
(after 2026 until the indexed amount is added) it refuses with what to
|
||||
do; pass `threshold` explicitly once the figure is confirmed.
|
||||
- `books.vendor_1099 { year, threshold?, include_tins?, save_csv_to? }` →
|
||||
`{ year, threshold, vendors: [{ vendor_id, name, form_1099?, tin_on_file,
|
||||
paid, reportable, party_id?, tin_last4?, tin?, tin_type?, tin_note? }],
|
||||
missing_w9, notes, warning?, saved_csv? }`. For a year whose threshold
|
||||
wanderbooks doesn't know (after 2026 until the indexed amount is added)
|
||||
it refuses with what to do; pass `threshold` explicitly once the figure
|
||||
is confirmed. `tin_last4` comes from the party's W-9 facts. With
|
||||
`include_tins: true` (default false) each line carries `tin`, the full
|
||||
TIN formatted as the IRS writes it (123-45-6789; 12-3456789 for an
|
||||
EIN), and `tin_type`, read from the vault, or `tin_note` saying why not
|
||||
(not in the vault, the vault locked, a record whose last four or hash
|
||||
disagree with the books); `warning` then says the output holds full
|
||||
TINs, for the CPA only. This is the only tool output that ever holds a
|
||||
full TIN. `save_csv_to` also writes the worksheet as CSV (TINs only with
|
||||
`include_tins`) to a new file outside the books dir, mode 0600.
|
||||
|
||||
## Tools — parties
|
||||
|
||||
@@ -330,10 +349,14 @@ texts are at most 500 characters, no control characters.
|
||||
`party.role_set`. Removing the last 1099 role with W-9 facts on file, or
|
||||
employee with W-4 facts, is refused: clear the facts first.
|
||||
- `books.set_w9 { party, received_on, entity_type, exempt_payee_code?,
|
||||
fatca_code?, tin_last4, kept_at }` or `{ party, clear: true }` → the
|
||||
party. Journaled as `party.w9_set`. A party with a vendor record also
|
||||
gets the record's `tin_on_file` set to match (journaled as
|
||||
`vendor.1099_set`), so `books.vendor_1099`'s `missing_w9` follows.
|
||||
fatca_code?, tin_last4, kept_at, tin?, tin_type?, w9_file_path? }` or
|
||||
`{ party, clear: true }` → the party. Journaled as `party.w9_set`. A
|
||||
party with a vendor record also gets the record's `tin_on_file` set to
|
||||
match (journaled as `vendor.1099_set`), so `books.vendor_1099`'s
|
||||
`missing_w9` follows. `tin`, `tin_type`, and `w9_file_path` put the full
|
||||
TIN and the signed form in the TIN vault (below); with them `tin_last4`
|
||||
comes from the TIN (a given one must agree) and `kept_at` defaults to
|
||||
`"wanderbooks vault"`.
|
||||
- `books.set_w4 { party, received_on, state, kept_at }` or `{ party, clear:
|
||||
true }` → the party. Journaled as `party.w4_set`.
|
||||
- `books.set_entry_party { entry_id, party? }` → the entry (as
|
||||
@@ -354,7 +377,7 @@ texts are at most 500 characters, no control characters.
|
||||
The party (every mutating party tool returns it): `{ party_id, name,
|
||||
label, display_name?, roles, website?, address?, notes?, archived,
|
||||
vendor_id?, customer_id?, w9?: { received_on, entity_type,
|
||||
exempt_payee_code?, fatca_code?, tin_last4, kept_at }, w4?: { received_on,
|
||||
exempt_payee_code?, fatca_code?, tin_last4, kept_at, in_vault }, w4?: { received_on,
|
||||
state, kept_at } }`. `label` is the display name, else the name; `roles`
|
||||
include vendor and customer when it has those records.
|
||||
|
||||
@@ -383,11 +406,86 @@ include vendor and customer when it has those records.
|
||||
payments drop out); `form_1099` is the record's mark.
|
||||
- `gross_pay` / `net_pay`: payroll lines naming it, in the year.
|
||||
|
||||
## Tools — the TIN vault
|
||||
|
||||
Full TINs (SSN, EIN, ITIN) and signed W-9 forms, kept so the CPA's 1099
|
||||
worksheet is complete, never in the books (`ledger-vault`; design in
|
||||
`docs/parties-and-attachments.md` "The TIN vault"; B-26).
|
||||
|
||||
- **Where:** `vault/` in the books dir (the parent of the data dir, beside
|
||||
`wanderbooks.toml`; git-ignored), one file per record, each encrypted
|
||||
(XChaCha20-Poly1305) and bound to its party. **Key:** the file named by
|
||||
`[vault] key_file` in `wanderbooks.toml`, else
|
||||
`~/.config/wanderbooks/vault.key` (`$XDG_CONFIG_HOME`), mode 0600,
|
||||
created the first time a TIN or form is stored, never when reading. A
|
||||
key file inside the books dir is refused, and so is storing into books
|
||||
kept in git (a `.git` beside them) whose `.gitignore` doesn't list
|
||||
`vault/`. Without the key the vault is
|
||||
locked: the books open and verify; the worksheet says the TINs are
|
||||
missing.
|
||||
- **Never out:** no tool output, error, or log holds more than a TIN's
|
||||
last four, except `books.vendor_1099 { include_tins: true }`. The tool
|
||||
arguments that take a TIN never echo it, even when refused.
|
||||
- `books.set_w9 { ..., tin, tin_type, w9_file_path? }` (above): validates
|
||||
the TIN against its type (nine digits, hyphens allowed; an ITIN starts
|
||||
with 9 and has an IRS group in digits 4-5; an SSN or EIN doesn't start
|
||||
with 9), checks the facts on a copy of the ledger, then writes the vault
|
||||
and journals the facts with `vault_record`, the SHA-256 of the TIN
|
||||
record (of the form's record when only a form is given). The form (PDF,
|
||||
PNG, JPEG; 25 MB at most) goes to the vault only, never the attachment
|
||||
store. Facts given later without a TIN keep the vault record when their
|
||||
last four agree with the vault's TIN, and are refused when they don't
|
||||
(pass the new TIN, or purge). Clearing the facts (`clear: true`) is
|
||||
refused while the vault holds the party's TIN or form: purge first.
|
||||
- `books.get_w9 { party, save_to }` → `{ party_id, path, file_name,
|
||||
size_bytes, note }`. Writes the party's form from the vault to `save_to`:
|
||||
a new file (an existing one is refused), in a folder that exists,
|
||||
outside the books dir, mode 0600. Returns the path, never the contents.
|
||||
Journals nothing.
|
||||
- `books.purge_tin { party, reason, confirmed? }` → `{ status: "preview" |
|
||||
"purged", party_id, name, tin_in_vault, w9_file_in_vault, tin_last4?,
|
||||
journaled, note }`. Without `confirmed`, a preview that changes nothing.
|
||||
Confirmed, deletes the party's vault records (needs no key) and, when the
|
||||
party has W-9 facts, journals `party.w9_set` with `vault_record` cleared,
|
||||
the last four kept, and `kept_at` saying when and why it was purged.
|
||||
Refused when the reason is blank or there is nothing to purge.
|
||||
|
||||
## Tools — attachments
|
||||
|
||||
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`,
|
||||
`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
|
||||
code in `label`). A file linked to an entry is one of the entry's receipts:
|
||||
the receipt policy counts it.
|
||||
|
||||
- `books.link_attachment { attachment_id, subject }` → `{ attachment_id,
|
||||
links }`. Journaled as `attachment.linked`. Refused for an unknown or
|
||||
withdrawn attachment, a subject that doesn't exist, or one it is linked
|
||||
to already (or filed on).
|
||||
- `books.unlink_attachment { attachment_id, subject, reason }` → `{
|
||||
attachment_id, links }`. Journaled as `attachment.unlinked` with the
|
||||
reason; the file stays filed. Refused when the reason is blank, it isn't
|
||||
linked there, it is filed on that entry (`books.unlink_receipt`), it is
|
||||
a posted entry's last receipt under the receipt policy
|
||||
(`receipt_required`), or it is the statement of a reconciliation not
|
||||
voided.
|
||||
- `books.attachments { subject }` → `{ subject, attachments: [...] }`: the
|
||||
files on a subject (an entry's: filed on it and linked to it), not
|
||||
withdrawn, each as in `books.list_receipts`.
|
||||
- `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.
|
||||
|
||||
## Tools — receipts & documents
|
||||
|
||||
- `books.attach_receipt { entry_id?, file_name, mime_type, content_base64 }`
|
||||
→ `{ receipt_id, sha256, deduplicated, size_bytes }`. `entry_id` is
|
||||
optional: a receipt can be filed before its entry exists.
|
||||
- `books.attach_receipt { entry_id?, file_name, mime_type, content_base64,
|
||||
description?, link_to? }` → `{ attachment_id, sha256, deduplicated, links
|
||||
}`. `entry_id` is optional: a receipt can be filed before its entry
|
||||
exists. `link_to` as in "Tools — attachments".
|
||||
- `books.link_receipt { receipt_id, entry_id }` → link a previously filed
|
||||
(unlinked, not withdrawn) receipt to its entry. A receipt linked elsewhere
|
||||
must be unlinked first.
|
||||
@@ -403,13 +501,19 @@ include vendor and customer when it has those records.
|
||||
blob store. Nothing in the evidence trail is ever erased — receipts change
|
||||
by new events, the same way entries change by reversals.
|
||||
- `books.list_receipts { entry_id?, include_withdrawn? }` → metadata list
|
||||
(no bytes), each with `withdrawn` / `withdrawn_reason`. Withdrawn
|
||||
attachments are omitted unless `include_withdrawn` is true.
|
||||
- `books.get_receipt { receipt_id }` → `{ metadata, content_base64 }`.
|
||||
(no bytes), each with `withdrawn` / `withdrawn_reason` and `links` (every
|
||||
subject it is linked to, its filing entry first). Withdrawn attachments
|
||||
are omitted unless `include_withdrawn` is true. `entry_id` lists the
|
||||
files filed on the entry and linked to it.
|
||||
- `books.get_receipt { attachment_id }` → `{ metadata, description?,
|
||||
links, content_base64 }`.
|
||||
- `books.file_document { category: "formation" | "legal" | "tax" |
|
||||
"accounting" | "other", file_name, mime_type, content_base64,
|
||||
description? }` → `{ document_id, sha256, deduplicated }`. The Drive-folder
|
||||
replacement (today's 01 Formation / 02 Legal / 03 Tax / 04 Accounting).
|
||||
"accounting" | "statement" | "contract" | "photo" | "warranty" |
|
||||
"import_source" | "other", file_name, mime_type, content_base64,
|
||||
description?, link_to? }` → `{ attachment_id, sha256, deduplicated, links
|
||||
}`. The Drive-folder replacement (today's 01 Formation / 02 Legal / 03
|
||||
Tax / 04 Accounting). Never a W-9: it shows the full TIN (`books.set_w9
|
||||
w9_file_path`).
|
||||
|
||||
## Receipt enforcement
|
||||
|
||||
@@ -512,8 +616,12 @@ posting.
|
||||
## Tools — import
|
||||
|
||||
- `books.import_bank_csv { csv_text, bank_account_code, default_offset_code,
|
||||
rules? }` → creates one draft per transaction (QBO upload layout).
|
||||
`rules` maps description substrings to account codes.
|
||||
rules?, date_order?, source_attachment_id? }` → creates one draft per
|
||||
transaction (QBO upload layout). `rules` maps description substrings to
|
||||
account codes. `source_attachment_id` (the export as filed, category
|
||||
`import_source`) is checked before any draft and linked to the bank
|
||||
account (`source_linked`; false when it was already), never to the
|
||||
drafts: a bank export is not a receipt.
|
||||
- `books.import_chart_csv { csv_text }` → creates accounts from
|
||||
`code,name,type,parent_code` CSV. All-or-nothing.
|
||||
- `books.import_local_file { path, category: "receipt" | "document", ... }`
|
||||
@@ -590,11 +698,38 @@ R-FUTURE-3 and rides the engine roadmap, not the MCP layer.
|
||||
- `books.mark_cleared { entry_id, line_index, cleared }` → `{ ok }`.
|
||||
Only posted lines can be marked; the flag is workflow metadata, not
|
||||
accounting data (posted entries stay immutable).
|
||||
- `books.reconcile { account, statement_date, statement_balance }` →
|
||||
`{ account_code, book_balance, cleared_balance, statement_balance,
|
||||
difference, uncleared_lines: [{ entry_id, line_index, date, description,
|
||||
side, amount }] }`. `difference` is cleared − statement; zero means
|
||||
reconciled.
|
||||
- `books.record_statement { attachment_id, account, period_start,
|
||||
period_end, opening_balance, closing_balance }` → the statement `{
|
||||
attachment_id, file_name, account_code, account_name, period_start,
|
||||
period_end, opening_balance, closing_balance, reconciliation_id? }`.
|
||||
Records what a filed statement says (balances as printed, in the
|
||||
account's normal direction, cents at most) and links it to the account.
|
||||
Journaled as `statement.recorded` then `attachment.linked`. Refused for
|
||||
an unknown or withdrawn attachment, one already recording a statement,
|
||||
an account that isn't an asset or liability, or a period that runs
|
||||
backwards.
|
||||
- `books.statements { account? }` → `{ statements }`, by statement date.
|
||||
- `books.reconcile { statement_attachment? | account, statement_date,
|
||||
statement_balance, confirmed? }` → `{ status: "preview" | "reconciled",
|
||||
account_code, statement_date, statement_attachment?, book_balance,
|
||||
cleared_balance, statement_balance, difference, uncleared_lines,
|
||||
cleared_lines, problems, reconciliation? }`, each line `{ entry_id,
|
||||
line_index, date, description, side, amount }`. `difference` is cleared
|
||||
− statement; zero means reconciled. With `statement_attachment` the
|
||||
account, date, and balance are the recorded statement's (any given must
|
||||
agree). Without `confirmed: true` it is a preview that records nothing;
|
||||
`problems` lists why a confirmation would be refused. Confirmed, it
|
||||
needs a recorded statement, a difference of exactly zero, and a date
|
||||
after the account's last reconciliation not voided; it then journals
|
||||
`reconciliation.completed` (the cleared lines, the statement) and links
|
||||
the statement to it, returning `reconciliation`.
|
||||
- `books.reconciliations { account? }` → `{ reconciliations: [{
|
||||
reconciliation_id, account_code, account_name, statement_date,
|
||||
statement_balance, statement_attachment, cleared: [{ entry_id,
|
||||
line_index }], voided, voided_reason? }] }`, voided ones included.
|
||||
- `books.void_reconciliation { reconciliation_id, reason }` → the
|
||||
reconciliation, voided. Journaled as `reconciliation.voided`; the
|
||||
account can be reconciled through that date again.
|
||||
|
||||
## Tools — A/R and A/P
|
||||
|
||||
|
||||
@@ -106,6 +106,13 @@ bill, payroll run, documents, and history. Members link to their capital
|
||||
accounts. Global search finds parties. Needs-attention: a contractor
|
||||
paid without W-9 facts; a 1099 party paid over the threshold.
|
||||
|
||||
Money with a party (Stephen, 2026-10-15: "how much have I spent on Apple
|
||||
versus how much did I get back"): paid out, received in, and net, for a
|
||||
chosen period (this year, last year, any year, all time), from
|
||||
`ledger_core::parties::activity`; what it was for (the non-money lines by
|
||||
account); a monthly out-vs-in chart; the 1099 amount for the calendar
|
||||
year. The directory shows the same columns, sortable.
|
||||
|
||||
## The TIN vault
|
||||
|
||||
Decision (Stephen, 2026-10-15): wanderbooks holds full TINs, so the 1099
|
||||
@@ -138,6 +145,21 @@ worksheet it hands the CPA is complete, but never in the journal.
|
||||
contains a nine-digit run.
|
||||
- **Books without a vault** (another machine, a restored backup) still
|
||||
open and verify; the 1099 worksheet then says which TINs are missing.
|
||||
- **Tools** (built; `docs/mcp-interface.md` "Tools — the TIN vault"):
|
||||
`books.set_w9` takes `tin` + `tin_type` and `w9_file_path` (the form
|
||||
goes to the vault, never the attachment store) and journals the last
|
||||
four with `vault_record`; facts that disagree with the vault's TIN are
|
||||
refused, and clearing facts with a vault record needs a purge first.
|
||||
`books.get_w9 {party, save_to}` writes the form to a new file outside the
|
||||
books dir and returns only the path. `books.purge_tin {party, reason,
|
||||
confirmed?}` previews, then deletes the records (no key needed) and
|
||||
journals the purge as `party.w9_set` with `vault_record` cleared and
|
||||
`kept_at` saying when and why. `books.vendor_1099 {include_tins: true}`
|
||||
is the one output with full TINs (with a warning; `save_csv_to` writes
|
||||
the CSV outside the books dir). A key file inside the books dir is
|
||||
refused. `tests/spec_vault.rs` scans every tool response, the server's
|
||||
log, and every file in the books dir outside `vault/` for the TINs in
|
||||
any formatting.
|
||||
|
||||
## Attachments
|
||||
|
||||
@@ -158,6 +180,14 @@ that entry). A receipt is an attachment linked to an entry; the receipt
|
||||
policy counts links to the entry, as now. A file stored once can be
|
||||
linked to many subjects (no more refiling).
|
||||
|
||||
Tools (built; `docs/mcp-interface.md` "Tools — attachments"):
|
||||
`books.link_attachment`, `books.unlink_attachment` (refused for a posted
|
||||
entry's last receipt under the receipt policy, and for a standing
|
||||
reconciliation's statement), `books.attachments {subject}`, `link_to` on
|
||||
`books.attach_receipt` and `books.file_document` (all or nothing), and
|
||||
`links` on every listed attachment. `books.file_document` takes the kinds
|
||||
below as its category (`receipt` is `books.attach_receipt`'s).
|
||||
|
||||
### Kinds
|
||||
|
||||
`receipt`, `statement`, `contract`, `formation`, `legal`, `tax`,
|
||||
@@ -177,11 +207,23 @@ is never edited; a later one supersedes it; `reconciliation.voided
|
||||
history with its statement; Needs-attention: an account with activity not
|
||||
reconciled through last month's end.
|
||||
|
||||
Tools (built; `docs/mcp-interface.md` "Tools — bank reconciliation"):
|
||||
`books.record_statement`, `books.statements`, `books.reconcile` (preview
|
||||
by default; `statement_attachment` + `confirmed: true` records it),
|
||||
`books.reconciliations`, `books.void_reconciliation`; `books.show
|
||||
reconciliation`; `books.digest` `unreconciled_accounts` (the `[books]
|
||||
cash_accounts` and liabilities with statements, not reconciled through
|
||||
last month's end). The statement of a reconciliation not voided can't be
|
||||
unlinked from it.
|
||||
|
||||
### Import sources
|
||||
|
||||
`books.import_bank_csv` files the CSV (kind `import_source`), links it to
|
||||
every entry it created, and records the row each came from, so every
|
||||
imported line traces to its source row.
|
||||
Built: file the export (`books.file_document` category `import_source`)
|
||||
and pass `source_attachment_id` to `books.import_bank_csv`; it is linked
|
||||
to the bank account imported into. Not to the drafts it creates: a file
|
||||
linked to an entry counts as its receipt, and a bank export is not one
|
||||
(DECISION, 2026-10-15, building the MCP tools). Recording the row each
|
||||
draft came from is not built yet.
|
||||
|
||||
### Projection
|
||||
|
||||
@@ -210,6 +252,28 @@ the digest, the viewer's Needs attention, and the session-start hook, and
|
||||
enforcement where a rule is a rule (tax basis at registration). The
|
||||
standard: do it now, completely.
|
||||
|
||||
### Counterparties
|
||||
|
||||
Decided (Stephen, 2026-10-15): the books should be easy to make complete
|
||||
without forcing one style. `[books] parties = "off" | "suggest" |
|
||||
"require"`, default `"suggest"`:
|
||||
|
||||
- `off`: never mention counterparties.
|
||||
- `suggest`: a draft or preview without a party names the likely one
|
||||
(existing parties matched against the description, the receipt's text,
|
||||
the vendor), or offers to create one; `next_steps` carries the exact
|
||||
call. Nothing is refused.
|
||||
- `require`: posting is refused until the entry names a party or is
|
||||
marked as having none (transfers, depreciation, amortization, closes
|
||||
are exempt by kind).
|
||||
|
||||
Also: memorized matches (journaled rules like description contains
|
||||
"OPENROUTER" → OpenRouter, applied by bank-CSV import and new drafts,
|
||||
reviewable and removable); a backfill punch list grouping untagged
|
||||
entries by likely party for one confirmation per group; 1099 nudges (a
|
||||
party given contractor or landlord, or a payment that looks like
|
||||
services to an individual, asks for the role and the W-9).
|
||||
|
||||
## After that: updates (check and seamless upgrade)
|
||||
|
||||
Decided (Stephen, 2026-10-15), after the completeness engine. New versions
|
||||
|
||||
+20
-2
@@ -521,8 +521,26 @@ and the full TIN lives only in the encrypted vault, outside the journal
|
||||
need the employee role; payroll lines that name their employee name a
|
||||
party with that role.
|
||||
|
||||
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
|
||||
file outside the books dir): `books.set_w9` `tin` writes it there and
|
||||
journals only the last four and the vault record's hash; the signed W-9
|
||||
goes there too, never to the attachment store. No tool output, error, or
|
||||
log holds a full TIN except `books.vendor_1099` with `include_tins`
|
||||
(DECISION, Stephen, 2026-10-15: for the CPA, when asked); files holding
|
||||
one (`books.get_w9`, `save_csv_to`) are written only outside the books
|
||||
dir. `books.purge_tin` deletes a party's vault records and journals that
|
||||
it did.
|
||||
|
||||
**Enforced:** `crate::parties` (has_full_tin, check_text, W9Facts::check,
|
||||
W4Facts::check), `Ledger::add_party`, `update_party`, `set_party_role`,
|
||||
`set_party_w9`, `set_party_w4`, `set_entry_party`, `validate_payroll`.
|
||||
`set_party_w9`, `set_party_w4`, `set_entry_party`, `validate_payroll`;
|
||||
`ledger-vault` (Tin::new, the encrypted records, records_held,
|
||||
purge_records); `ledger-mcp` `books.set_w9`, `books.get_w9`,
|
||||
`books.purge_tin`, `books.vendor_1099` (open_vault refuses a key file in
|
||||
the books dir; outside_books for files with a TIN).
|
||||
**Checked:** `ledger-core` `tests/spec_parties.rs`; `ledger-store`
|
||||
`tests/parties.rs`; `ledger-mcp` `tests/spec_parties.rs`.
|
||||
`tests/parties.rs`; `ledger-vault` `tests/vault.rs`; `ledger-mcp`
|
||||
`tests/spec_parties.rs`, `tests/spec_vault.rs` (every response, the log,
|
||||
and every file in the books dir outside `vault/` scanned for the TINs in
|
||||
any formatting).
|
||||
Reference in new issue
Block a user