Fixed assets carry federal tax basis beside book value; tests sweep stale temp dirs
check / check (push) Canceled after 0s
check / check (push) Canceled after 0s
- core: TaxBasisRule (cost IRC 1012, carryover IRC 723, personal-use conversion Treas. Reg. 1.168(i)-4(b)), TaxBasis with the contributor's adjusted basis, Asset::tax_basis and ::built_in_gain (IRC 704(c)); the register gains tax rule, contributor basis, tax basis, built-in gain, total tax basis, missing count (rules.md B-25; sources vendored in docs/spec/sources/irs-partnership-basis.md) - store: asset.tax_basis_set journaled, replayed, projected (schema v10) - mcp: books.set_asset_tax_basis; register_asset takes tax_basis_rule and contributor_basis, checked before anything registers (found by the spec-blind suite); list_assets and the register export (json, csv) carry tax basis; instructions say to record it for every asset - import: the register's XLSX and PDF gain the federal tax basis table with citations - viewer: tax basis on the Assets page, the asset page (a Tax basis card), the report, the journal, and Needs attention for assets without one - just: check and the mutation recipes sweep this user's test book dirs older than an hour (killed runs had left 23 GB in RAM-backed /tmp) - docs: the parties, attachments, TIN vault, completeness engine, and updates designs (docs/parties-and-attachments.md) - tests: spec-blind tax basis suites in core, store, MCP; register PDF/XLSX and viewer tests 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
49c49aef33
commit
6e6f9d65b2
30 files changed
+3240
-93
No files matched your search
@@ -0,0 +1,229 @@
|
||||
# Parties and attachments
|
||||
|
||||
Design for two features built together (Stephen, 2026-10-15): a directory
|
||||
of the parties the company deals with, and attachments that link to any
|
||||
subject. Status: design; built after asset tax basis.
|
||||
|
||||
## Why together
|
||||
|
||||
Both answer "what is this connected to?" A W-9 note, a contract, a bank
|
||||
statement, a photo of a laptop: each is a file about a party, an account, a
|
||||
reconciliation, an asset, a period. One subject model serves both.
|
||||
|
||||
## Parties
|
||||
|
||||
A party is anyone the company deals with. Today's vendors and customers
|
||||
become parties; employees stop being name strings on payroll lines; the
|
||||
Apple, Google, Anthropic, IRS, and Utah that live only in entry
|
||||
descriptions get records.
|
||||
|
||||
### Model (ledger-core, `parties.rs`)
|
||||
|
||||
```text
|
||||
Party {
|
||||
id: PartyId, // vendors and customers keep their ids (see Migration)
|
||||
name: String, // legal or trade name, as on documents
|
||||
display_name: Option<String>,
|
||||
roles: BTreeSet<Role>, // vendor, contractor, customer, employee, member,
|
||||
// payout_platform, tax_agency, financial_institution, landlord
|
||||
website: Option<String>,
|
||||
address: Option<String>, // business address; no personal PII beyond it
|
||||
w9: Option<W9Facts>, // contractor/landlord: the TIN itself is in the vault
|
||||
w4: Option<W4Facts>, // employee: never the SSN
|
||||
form_1099: Option<Form1099>, // carried over from Vendor
|
||||
member: Option<String>, // a member's name in wanderbooks.toml, when role member
|
||||
notes: Option<String>,
|
||||
archived: bool, // no longer dealt with; history stays
|
||||
}
|
||||
W9Facts { received: NaiveDate, entity_type: String, exempt_payee_code: Option<String>,
|
||||
fatca_code: Option<String>, tin_last4: String, kept_at: String }
|
||||
W4Facts { received: NaiveDate, state: String, kept_at: String }
|
||||
```
|
||||
|
||||
Rules (to vendor as B-26 with sources):
|
||||
|
||||
- A full TIN or SSN is never in the journal: `tin_last4` is exactly four
|
||||
digits, and any nine-digit run in any party field is refused (the engine
|
||||
checks, not the agent). Full TINs live only in the vault (below).
|
||||
Source: IRS truncated-TIN rules (Treas. Reg. §301.6109-4, to vendor) for
|
||||
what may appear; DECISION (Stephen, 2026-10-15) for the vault.
|
||||
- Roles are a set; each role's facts are only allowed with the role
|
||||
(W-9 facts need contractor or landlord; W-4 facts need employee).
|
||||
- Names are unique ignoring case and surrounding space among
|
||||
non-archived parties.
|
||||
- A 1099 needs a contractor or landlord role; 1099 totals come from the
|
||||
entries tagged with the party, as today's vendor 1099 report does.
|
||||
|
||||
### Links
|
||||
|
||||
- Entries: `entry.party_set {entry_id, party_id|null}` generalizes
|
||||
`entry.vendor_set`; an entry has one counterparty.
|
||||
- Invoices and bills already name a customer or vendor id: those are
|
||||
party ids after migration.
|
||||
- Payroll lines name an employee party id (old runs keep their name
|
||||
strings; new runs must name a party with the employee role).
|
||||
|
||||
### Journal events
|
||||
|
||||
`party.added {id, name, roles}`, `party.updated {id, field, value}` (one
|
||||
field per event: name, display_name, website, address, notes, archived),
|
||||
`party.role_added {id, role}`, `party.role_removed {id, role}`,
|
||||
`party.w9_set {id, facts|null}`, `party.w4_set {id, facts|null}`,
|
||||
`entry.party_set`. Replayed and projected like the rest; every event gets
|
||||
viewer journal text (the GUI test enforces it).
|
||||
|
||||
### Migration
|
||||
|
||||
Old journals replay unchanged. `vendor.added` and `customer.added` keep
|
||||
their meaning and create parties with the vendor or customer role; the
|
||||
party id space starts above the highest vendor and customer ids, and a
|
||||
vendor and a customer with the same name stay two parties until the
|
||||
human merges them (`books.merge_parties`, journaled, re-pointing links).
|
||||
`entry.vendor_set` replays as `entry.party_set`. `vendor.1099_set` sets
|
||||
the party's `form_1099` and the contractor role.
|
||||
|
||||
### Tools
|
||||
|
||||
`books.add_party`, `books.update_party`, `books.party_roles`,
|
||||
`books.set_w9`, `books.set_w4`, `books.parties` (list, filter by role),
|
||||
`books.party` (one: facts, roles, totals this year, documents, links),
|
||||
`books.merge_parties`, `books.set_entry_party`. `books.add_vendor` and
|
||||
`books.add_customer` stay as shortcuts.
|
||||
|
||||
### Viewer
|
||||
|
||||
A Parties page (by role, searchable) and a page per party: facts, roles,
|
||||
year-to-date paid / owed / invoiced / 1099 amount, every entry, invoice,
|
||||
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.
|
||||
|
||||
## The TIN vault
|
||||
|
||||
Decision (Stephen, 2026-10-15): wanderbooks holds full TINs, so the 1099
|
||||
worksheet it hands the CPA is complete, but never in the journal.
|
||||
|
||||
- **Where:** `vault/` in the books dir, listed in `.gitignore`. Never in
|
||||
the hash-chained journal, the SQLite projection, git, an export other
|
||||
than the 1099 worksheet, or the viewer. Nothing outside the vault holds
|
||||
more than the last four digits.
|
||||
- **What:** per party, the full TIN, its type (SSN, EIN, ITIN), and the
|
||||
W-9 file itself, each encrypted at rest (XChaCha20-Poly1305, one random
|
||||
nonce per record; the file format carries a version and the party id as
|
||||
associated data, so a record can't be moved to another party).
|
||||
- **Key:** a random 256-bit key in the OS keyring (Secret Service on Linux,
|
||||
Keychain on macOS), created on first use; never on disk in the books dir.
|
||||
A recovery copy is the human's job: `just vault-export` writes the key
|
||||
and records as one passphrase-encrypted file for a password manager.
|
||||
- **Journal:** `party.w9_set` records that a W-9 was received, its date,
|
||||
entity type, codes, and last four, plus a hash of the vault record, so
|
||||
the books prove a W-9 existed without holding it.
|
||||
- **Purge:** `books.purge_tin {party, reason}` deletes the vault record
|
||||
(after the retention period, or on request) and journals that it was
|
||||
purged; the journal's facts stay.
|
||||
- **Use:** only `books.vendor_1099` with `include_tins: true` (and the CPA
|
||||
package, when asked) decrypts them. The MCP server never returns a TIN
|
||||
in any other tool, log, or error. Tests check that no other output
|
||||
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.
|
||||
|
||||
## Attachments
|
||||
|
||||
### Subjects
|
||||
|
||||
An attachment links to any number of subjects:
|
||||
|
||||
```text
|
||||
Subject = Entry(id) | Account(id) | Reconciliation(id) | Asset(id) | Party(id)
|
||||
| Invoice(id) | Bill(id) | Period(id) | PayrollRun(id) | Schedule(id)
|
||||
| Company
|
||||
```
|
||||
|
||||
`attachment.linked {attachment_id, subject}` and `attachment.unlinked
|
||||
{attachment_id, subject, reason}` replace the single `entry_id` link (old
|
||||
`receipt.attached` with an entry and `receipt.linked` replay as links to
|
||||
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).
|
||||
|
||||
### Kinds
|
||||
|
||||
`receipt`, `statement`, `contract`, `formation`, `legal`, `tax`,
|
||||
`accounting`, `photo`, `warranty`, `import_source`, `other`. A statement
|
||||
carries `{account_id, period_start, period_end, opening_balance,
|
||||
closing_balance}`, checked against the account's currency.
|
||||
|
||||
### Reconciliations
|
||||
|
||||
`books.reconcile` with `confirmed: true` records a reconciliation:
|
||||
`reconciliation.completed {id, account_id, statement_date,
|
||||
statement_balance, cleared: [(entry_id, line)], attachment_id}`. It
|
||||
refuses unless the difference is exactly zero and a statement for that
|
||||
account and date is attached (the receipt rule's twin). A reconciliation
|
||||
is never edited; a later one supersedes it; `reconciliation.voided
|
||||
{id, reason}` exists for mistakes. Viewer: per-account reconciliation
|
||||
history with its statement; Needs-attention: an account with activity not
|
||||
reconciled through last month's end.
|
||||
|
||||
### 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.
|
||||
|
||||
### Projection
|
||||
|
||||
SQLite: `attachment_links(attachment_id, subject_kind, subject_id)`,
|
||||
`statements(attachment_id, account_id, period_start, period_end, opening,
|
||||
closing)`, `reconciliations(...)`, `reconciliation_lines(...)`; the old
|
||||
`attachments.entry_id` column is kept and filled for entry links during
|
||||
the migration so older readers still work.
|
||||
|
||||
## Testing
|
||||
|
||||
Spec blocks on every command; spec-blind suites per crate; the
|
||||
refusal-leaves-books-unchanged property; replay and migration from v10
|
||||
books; the GUI's every-event-has-text test; mutation on the new modules.
|
||||
|
||||
## After this: the completeness engine
|
||||
|
||||
Decided (Stephen, 2026-10-15), built after parties and attachments: one
|
||||
set of completeness rules in the core (asset: tag, serial, tax basis,
|
||||
acquisition link, label; entry: receipt, 1099 vendor tag; account:
|
||||
reconciled through last month; month: depreciation, amortization, close;
|
||||
party: W-9 before paying a contractor; settings: no problems; and the
|
||||
rules this design adds), `next_steps` on every tool response naming the
|
||||
exact call that completes each, `books.todo` as the one punch list behind
|
||||
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.
|
||||
|
||||
## After that: updates (check and seamless upgrade)
|
||||
|
||||
Decided (Stephen, 2026-10-15), after the completeness engine. New versions
|
||||
fix accounting, tax, and legal rules, so books running an old version may
|
||||
be wrong; staying current is part of keeping correct books.
|
||||
|
||||
- **Check:** the MCP server checks for a newer release when it starts and
|
||||
once a day while running (and the viewer and `just session-start` ask
|
||||
it). Releases carry notes and a severity: a fix to an accounting, tax, or
|
||||
legal rule (a 1099 threshold, a depreciation rule, a refusal that was
|
||||
wrong) is marked so, and says which books it may affect. `[updates]
|
||||
check = "daily" | "never"` in wanderbooks.toml; the check contacts only
|
||||
the release host and sends nothing about the books.
|
||||
- **Surface:** the update is a completeness item (`books.todo`), in
|
||||
`books.digest`, a banner in the viewer, and the session-start hook; the
|
||||
MCP instructions tell the agent to raise it with the human at once, with
|
||||
the release notes, and to treat a rules fix as urgent.
|
||||
- **Upgrade:** one step (`wanderbooks update`, or the agent offers it):
|
||||
download the release, verify its signature against a key pinned in the
|
||||
installed build, then rehearse: open the books read-only with the new
|
||||
build and run the full verify (replay, projection, migrations) before
|
||||
switching. Swap binaries atomically, keep the previous build for
|
||||
rollback, restart the viewer, and have the MCP server exit cleanly so the
|
||||
client reconnects to the new one. Never downgrade; never touch the
|
||||
journal except through the new build's ordinary migrations.
|
||||
- **After:** the agent re-runs whatever the fix affects (for example,
|
||||
`books.vendor_1099` for the year, or the register) and tells the human
|
||||
what changed in their books, if anything.
|
||||
Reference in new issue
Block a user