Shared parties with exact shares, member capital statements, TINs for any party, K-1 worksheet
check / check (push) Canceled after 0s
check / check (push) Canceled after 0s
Entries may name several parties, each with an exact amount, a percent, or a share derived from the members' capital lines or [[company.members]] shares (entry.parties_set); shares always sum exactly. The 1099 worksheet counts a vendor's share. A member's party page is their partner capital statement, from the same ledger_core::k1 source as the CPA package. books.set_tin stores any party's TIN (members' SSNs for K-1s) in the vault (party.tin_set, last four in the books). The CPA package carries a K-1 member worksheet (k1-members.csv, with a members' total that ties to Schedule L) and the 1099 worksheet with full TINs, saved outside the books only. files_jointly_with notes spouses' two K-1s land on one 1040. Adversarial review fixes: reversed contributions no longer inflate K-1 contributions and distributions; a K-1 row never takes another party's TIN; TIN detection covers dot/slash/dash separators; refusals redact TINs; vendor tagging can't silently replace shared parties; reversals follow re-apportioning. 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
df3fc3f4e4
commit
4c74169d18
32 files changed
+7232
-375
No files matched your search
+133
-26
@@ -33,7 +33,10 @@ to 100%) drives the statement of members' equity, `[books]
|
||||
accounting_basis` ("accrual" or "cash", default accrual) labels report
|
||||
footers, and `[books] receipt_threshold` sets the receipt policy unless
|
||||
`config.json` sets `receipt_policy`. Members may name a
|
||||
`distribution_account`; single-owner businesses may set `[books] close_into`
|
||||
`distribution_account`, and `files_jointly_with` (spouses filing one joint
|
||||
return; the CPA package's README notes their two K-1s go on one Form 1040,
|
||||
and k1-members.csv ends with a members' total, not a K-1, that ties to
|
||||
Schedule L); single-owner businesses may set `[books] close_into`
|
||||
(the equity account a close puts net income in; default Retained Earnings)
|
||||
and `close_sweep` (draw, distribution, or dividend accounts closed into it).
|
||||
The cash flow statement's account roles are `[books] cash_accounts` (what
|
||||
@@ -112,21 +115,31 @@ below.
|
||||
- `books.chart` → `{ accounts: [{ id, code, name, type, parent, placeholder,
|
||||
balance }] }`. Balances included: one call answers "what do we have."
|
||||
- `books.draft_entry { date: "2026-10-10", description: "...", vendor?,
|
||||
party? }` → `{ entry_id, status: "draft" }`. `party` tags the
|
||||
counterparty (see "Tools — parties"); given with `vendor`, both must name
|
||||
the same vendor record, else nothing is drafted.
|
||||
party?, parties?, no_party_reason? }` → `{ entry_id, status: "draft" }`.
|
||||
`party` tags the counterparty (see "Tools — parties"); given with
|
||||
`vendor`, both must name the same vendor record, else nothing is
|
||||
drafted. `parties` names several, each with a share (as
|
||||
`books.set_entry_party`), in place of `party` and `vendor`.
|
||||
- `books.add_line { entry_id, account: "1000" | <id>, side: "debit" | "credit",
|
||||
amount: "123.45" }` → `{ ok: true }`. A refused line (unknown account or
|
||||
class, bad amount) adds nothing.
|
||||
- `books.post_entry { entry_id, confirmed }` → `{ status, entry_id, date,
|
||||
description, total, lines (with resulting balances), receipts,
|
||||
receipt_required, receipt_missing, split_detected }`. Without
|
||||
`confirmed: true`, `status` is `"preview"` and nothing changes; with it,
|
||||
the receipt policy is enforced and `status` is `"posted"`.
|
||||
receipt_required, receipt_missing, split_detected, party_policy,
|
||||
party_id?, party?, parties, parties_error?, no_party_reason?,
|
||||
party_missing, party_suggestions }`. Without `confirmed: true`, `status`
|
||||
is `"preview"` and nothing changes; with it, the receipt policy is
|
||||
enforced and `status` is `"posted"`. `parties` lists every party the
|
||||
entry names with its share resolved to the cent (`{ party_id, party,
|
||||
share: "amount" | "percent" | "derived", amount?, percent?, resolved }`);
|
||||
when the shares don't add up to what the entry moved, `parties_error`
|
||||
says why and confirming fails with code `party_shares`.
|
||||
- `books.reverse_entry { entry_id, date, description, confirmed }` → same
|
||||
preview/confirm pattern as post.
|
||||
- `books.get_entry { entry_id }` → full entry with lines, receipts, and
|
||||
its `vendor`, `party_id`, and `party` (the party's label) tags.
|
||||
its `vendor`, `party_id`, and `party` (the party's label, when it names
|
||||
exactly one) tags, and `parties` / `parties_error` as in the
|
||||
`post_entry` preview.
|
||||
- `books.search_entries { account?, date_from?, date_to?, text?, limit?, offset? }`
|
||||
→ entries with lines, newest first. `limit` caps at 100.
|
||||
- `books.trial_balance { as_of? }` → `as_of`, lines, totals, `balanced`
|
||||
@@ -290,14 +303,22 @@ is on file and the TIN's last four; full TINs live only in the TIN vault
|
||||
the tag. Metadata only: posted entries may be tagged. `books.draft_entry`
|
||||
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.
|
||||
record, so tagging by party counts for 1099s the same way. On an entry
|
||||
naming several parties the parties are `books.set_entry_party`'s:
|
||||
tagging the vendor of a party already named keeps every share as it
|
||||
is; tagging a vendor whose party isn't named, or clearing the tag while
|
||||
its party is named, is refused (drop or add the party with `parties`
|
||||
instead). An entry and its reversal always carry the same tags.
|
||||
- `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
|
||||
is confirmed. On an entry naming several parties, `paid` counts only
|
||||
the vendor's resolved share of the payment (rules.md B-19); an entry
|
||||
whose shares don't resolve counts in full and `notes` names it.
|
||||
`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
|
||||
@@ -359,11 +380,44 @@ texts are at most 500 characters, no control characters.
|
||||
`"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
|
||||
`books.get_entry`). `party` null or omitted clears the tag. A party with
|
||||
a vendor record also tags the entry's vendor; one without, or clearing,
|
||||
clears the vendor tag. Metadata only; posted entries may be tagged.
|
||||
Journaled as `entry.party_set`.
|
||||
- `books.set_entry_party { entry_id, party?, parties?, no_party_reason? }`
|
||||
→ the entry (as `books.get_entry`). `party` null or omitted clears the
|
||||
tag. A party with a vendor record also tags the entry's vendor; one
|
||||
without, or clearing, clears the vendor tag. Metadata only; posted
|
||||
entries may be tagged. Tagging an entry that was reversed, or a
|
||||
reversal, tags the other too (the whole reversal chain), so each
|
||||
party's part of the pair still nets to zero. Journaled as
|
||||
`entry.party_set`.
|
||||
`no_party_reason` (with no party) marks an entry as having no
|
||||
counterparty (`entry.no_party`).
|
||||
- **Several parties:** `parties: [{ party, amount? | percent? }]`, in
|
||||
place of `party`, names every counterparty with its share; `[]` clears.
|
||||
Journaled as `entry.parties_set { entry_id, parties: [{ party_id, share
|
||||
}] }`, `share` being `{"amount": "30.00"}`, `{"percent": "60"}`, or
|
||||
`"derived"`. Each share, of what the entry moved (its `entry_flow` net;
|
||||
for an entry moving no money, like property contributed, its total):
|
||||
- `amount`: exactly that, positive, in cents; the amounts must sum to
|
||||
what it moved.
|
||||
- `percent`: that percent; the percents must sum to exactly 100. Each
|
||||
part is rounded down to the cent and the leftover cents go to the
|
||||
first party listed.
|
||||
- neither (derived): a sole party gets all of it. Members named
|
||||
together (parties with the member role named in `[[company.members]]`)
|
||||
get what their own capital and distributions lines on the entry say,
|
||||
when it has any (these must account for all it moved); else their
|
||||
`[[company.members]]` shares, rescaled to the members named, rounded as
|
||||
percents are. A derived share for a non-member among several is
|
||||
refused.
|
||||
Every party gives the same kind of share. Refused, journaling nothing: a
|
||||
party named twice, an unknown party, mixed kinds, percents not summing
|
||||
to 100, an amount finer than a cent, two parties with vendor records (a
|
||||
1099 counts a payment toward one vendor; the one vendor party's record
|
||||
is tagged and counts the whole payment), and, once the entry has
|
||||
balanced lines, shares that don't resolve. A draft's shares are checked
|
||||
against its lines again at `post_entry`. Under `[books] parties =
|
||||
"require"`, any party (or the no-party mark) satisfies the check.
|
||||
Reversals carry every share. There is no "joint" party: a joint member
|
||||
transaction names both members and lets the shares derive.
|
||||
- `books.parties { role?, include_archived?, year? }` → `{ year, parties:
|
||||
[{ party_id, name, label, roles, vendor_id?, customer_id?, w9_on_file,
|
||||
tin_last4?, w4_on_file, archived, paid, received }] }`, by id.
|
||||
@@ -397,7 +451,8 @@ include vendor and customer when it has those records.
|
||||
positive net was paid to the party, a negative one received from it; a
|
||||
reversal counts against its original's side. A bill isn't paid until its
|
||||
payment is; an entry only moving money between money accounts nets to
|
||||
zero.
|
||||
zero. An entry naming several parties counts this party's resolved
|
||||
share only (an entry whose shares don't resolve counts for none).
|
||||
- `invoiced` / `billed`: its invoices' and bills' totals dated in the year.
|
||||
`owed` / `receivable`: its open bills' and invoices' balances now (every
|
||||
year).
|
||||
@@ -408,9 +463,12 @@ include vendor and customer when it has those records.
|
||||
|
||||
## 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).
|
||||
Full TINs (SSN, EIN, ITIN) and signed W-9 forms, kept so what the CPA
|
||||
gets is complete (every member's TIN for their K-1, every payee's for
|
||||
their 1099), never in the books (`ledger-vault`; design in
|
||||
`docs/parties-and-attachments.md` "The TIN vault"; B-26). DECISION
|
||||
(Stephen, 2026-10-15): the vault holds everyone's TIN, members included,
|
||||
and the CPA package carries them.
|
||||
|
||||
- **Where:** `vault/` in the books dir (the parent of the data dir, beside
|
||||
`wanderbooks.toml`; git-ignored), one file per record, each encrypted
|
||||
@@ -424,14 +482,31 @@ worksheet is complete, never in the books (`ledger-vault`; design in
|
||||
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.
|
||||
last four, except `books.vendor_1099 { include_tins: true }`; the CPA
|
||||
package carries them only in its zip, written outside the books dir.
|
||||
The tool arguments that take a TIN never echo it, even when refused.
|
||||
- **TIN facts:** any party (any role) may have `tin` facts:
|
||||
`{ tin_type: "ssn" | "ein" | "itin", last4, vault_record?, purged? }`,
|
||||
journaled as `party.tin_set {id, facts|null}` and shown in every party
|
||||
output as `tin: { tin_type, last4, in_vault, purged? }`. They never
|
||||
disagree with the party's W-9 facts: both on file means the same last
|
||||
four (the engine refuses otherwise).
|
||||
- `books.set_tin { party, tin, tin_type }` → the party. Validates the TIN
|
||||
as `books.set_w9` does, checks the facts on a copy of the ledger, writes
|
||||
the TIN to the vault (the same key and git-ignore checks), and journals
|
||||
`party.tin_set` with the record's hash. Any role. A party whose W-9
|
||||
facts name another last four is refused (a payee's new TIN comes with
|
||||
its W-9: `books.set_w9`); one whose W-9 facts agree gets their
|
||||
`vault_record` updated too (`party.w9_set`).
|
||||
- `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,
|
||||
record (of the form's record when only a form is given). With a TIN it
|
||||
also journals the party's TIN facts (`party.tin_set`, as
|
||||
`books.set_tin`); a TIN with a new last four clears the old TIN facts
|
||||
first (`party.tin_set` null), so the two never disagree. 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
|
||||
@@ -447,8 +522,10 @@ worksheet is complete, never in the books (`ledger-vault`; design in
|
||||
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.
|
||||
the last four kept, and `kept_at` saying when and why it was purged;
|
||||
when it has TIN facts, `party.tin_set` likewise, with `purged` saying
|
||||
when and why. Refused when the reason is blank or there is nothing to
|
||||
purge.
|
||||
|
||||
## Tools — attachments
|
||||
|
||||
@@ -794,8 +871,38 @@ entries.
|
||||
|
||||
## Tools — CPA handoff
|
||||
|
||||
- `books.export_cpa_package { as_of?, cash_accounts?, financing_accounts?, investing_accounts? }`
|
||||
→ `{ file_name, content_base64 }`. A zip with the full set, each
|
||||
- `books.export_cpa_package { as_of?, cash_accounts?, financing_accounts?,
|
||||
investing_accounts?, include_receipts?, include_tins?, save_to? }` →
|
||||
`{ file_name, content_base64?, saved_to?, tins_included, tin_gaps, note }`.
|
||||
**TINs** (`include_tins`, default true; DECISION, Stephen, 2026-10-15):
|
||||
the K-1 worksheet and the 1099 worksheet carry every member's and
|
||||
payee's full TIN from the vault, so the zip is written to `save_to` (a
|
||||
new `.zip` path, or an existing folder it goes in as
|
||||
`cpa-package-<as_of>.zip`; outside the books dir, `exports/` included;
|
||||
mode 0600) and the output is its path, never its bytes; without
|
||||
`save_to` it is refused. A locked vault or a missing TIN never fails the
|
||||
package: the row says why ("TIN not in the vault", "the TIN vault is
|
||||
locked", a record that disagrees with the books), the README lists it,
|
||||
and `tin_gaps` names each member and each reportable payee without one.
|
||||
`include_tins: false`: last fours only, returned as `content_base64`
|
||||
unless `save_to` is given. **K-1 worksheet** (`k1-members.csv`, with
|
||||
`[[company.members]]`): one row per member, in order: `member,
|
||||
party_id, share, capital_account, tin, tin_type, tin_note` (without
|
||||
TINs: `tin_last4, tin_type`), then `beginning_capital,
|
||||
contributions_cash, contributions_property, net_income, distributions,
|
||||
other, ending_capital` for the fiscal year to date: the statement of
|
||||
members' equity's figures (`ledger_core::k1`), contributions split into
|
||||
property (what a contribution debits to asset accounts other than cash,
|
||||
receivables, and prepaid accounts: a prepaid schedule's account, or one
|
||||
named "prepaid") and cash (the rest, including a service the members
|
||||
paid ahead). Property in an entry crediting several members is shared
|
||||
in proportion to their credits. An entry and its reversal both in the
|
||||
year cancel: neither is a contribution or a distribution. A member's
|
||||
party is the party with the member's name (not a display name) and the
|
||||
member role, as the viewer's capital statement finds it; with none, the
|
||||
member's TIN is a gap.
|
||||
**1099 worksheet** with TINs adds `party_id, tin_last4, tin, tin_type,
|
||||
tin_note`. A zip with the full set, each
|
||||
statement as PDF and XLSX: `profit-and-loss`, `balance-sheet`,
|
||||
`cash-flow` (reconciled to `cash_accounts`), `members-equity`
|
||||
(multi-member LLCs), `trial-balance`; plus `general-ledger.xlsx` and
|
||||
|
||||
+127
-15
@@ -62,7 +62,8 @@ Rules (to vendor as B-26 with sources):
|
||||
### Links
|
||||
|
||||
- Entries: `entry.party_set {entry_id, party_id|null}` generalizes
|
||||
`entry.vendor_set`; an entry has one counterparty.
|
||||
`entry.vendor_set` (one counterparty); `entry.parties_set` names
|
||||
several, each with a share (below).
|
||||
- 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
|
||||
@@ -74,9 +75,94 @@ Rules (to vendor as B-26 with sources):
|
||||
`PartyField` per event, `{field, value}`: name, display_name, website,
|
||||
address, notes, archived), `party.role_set {id, role, on}`,
|
||||
`party.w9_set {id, facts|null}`, `party.w4_set {id, facts|null}`,
|
||||
`entry.party_set {entry_id, party_id|null}`. Replayed and projected like
|
||||
`party.tin_set {id, facts|null}` (TIN facts: type, last four, vault
|
||||
record, when purged), `entry.party_set {entry_id, party_id|null}`,
|
||||
`entry.parties_set {entry_id, parties: [{party_id, share}]}` (a share is
|
||||
`{"amount": "30.00"}`, `{"percent": "60"}`, or `"derived"`; `[]`
|
||||
clears). Replayed and projected like
|
||||
the rest; every event gets viewer journal text (the GUI test enforces it).
|
||||
|
||||
### Shares: an entry with several parties
|
||||
|
||||
DECISION (Stephen, 2026-10-15): there is no "joint" party. A joint
|
||||
transaction is with both members and shows on both members' pages, each
|
||||
with their share; apportioning is explicit and exact when it isn't
|
||||
derived. Spec: `ledger_core::shares`.
|
||||
|
||||
- An entry names a list of `PartyShare {party, share}`; `Share` is
|
||||
`Amount(d)`, `Percent(p)`, or `Derived`. Tagging one party
|
||||
(`set_entry_party`) is one derived share: all of it.
|
||||
`Ledger::entry_party` means "the sole party"; `entry_parties` lists
|
||||
them all.
|
||||
- **What a share is of:** the entry's `entry_flow` net (what it moved
|
||||
with its counterparties), or its total debits when the net is zero (a
|
||||
member contributing property moves no money but is still theirs).
|
||||
- **Amount:** exactly that, positive, in cents; the amounts must sum to
|
||||
what the entry moved.
|
||||
- **Percent:** more than 0, at most 100, summing to exactly 100. Each part
|
||||
is rounded toward zero to the cent and the leftover cents (fewer than
|
||||
one per party) go to the first party in order.
|
||||
- **Derived:** a sole party gets all of it. Several parties must all be
|
||||
members (`[[company.members]]` matched by name to a party with the
|
||||
member role; core is handed them as `PartyMember`s). If the entry has
|
||||
lines on a named member's capital or distributions account, each
|
||||
member gets what their own lines say (and those must account for all
|
||||
it moved); otherwise each gets their income share, rescaled to the
|
||||
members named, rounded as percents are.
|
||||
- **Refused** (journaling nothing): a party named twice, an unknown
|
||||
party, mixed kinds of share, percents not summing to 100, an amount
|
||||
finer than a cent, and two parties with vendor records. A 1099 counts
|
||||
a payment toward a single vendor: the vendor tag follows the one party
|
||||
with a vendor record, and the 1099 worksheet counts the whole payment
|
||||
toward it, so name a vendor among others only when the whole payment
|
||||
was its (otherwise record its part as its own entry). Whether shares
|
||||
add up depends on the settings (cash accounts, members), so core
|
||||
checks only their form; the MCP server checks they resolve once the
|
||||
entry has balanced lines, and `post_entry` refuses (`party_shares`)
|
||||
until they do. Replay never consults settings.
|
||||
- **Money:** `parties::activity_with` counts each party's resolved share;
|
||||
an entry moving no money counts nothing; an entry whose shares don't
|
||||
resolve counts for no one (listed as unresolved). Reversals carry
|
||||
every share, so a reversal subtracts each party's part; re-tagging
|
||||
either one (parties, shares, or vendor) re-tags the other, so the pair
|
||||
always nets to zero.
|
||||
- **For the agent:** a joint member transaction (a reimbursement on Due
|
||||
to Shareholders, a contribution or distribution from joint funds)
|
||||
names both members with no shares and lets them derive; an uneven
|
||||
split gives amounts.
|
||||
|
||||
### A member's capital statement
|
||||
|
||||
A member's party page (role member, named in `[[company.members]]`,
|
||||
whose capital account is in the chart) is their capital account
|
||||
statement over the page's period (`Books::capital_statement`). Every
|
||||
figure is the CPA package's K-1 worksheet row for that member and span
|
||||
(`ledger_core::k1::k1_detail`, itself the statement of changes in
|
||||
members' equity split into cash and property), so the page and the
|
||||
K-1 never disagree:
|
||||
|
||||
- capital at the start of the period (including their share of earnings
|
||||
not yet closed into capital);
|
||||
- add contributions of cash (deposits, and what they paid for the
|
||||
company) and of property (debits to asset accounts that are neither
|
||||
cash nor receivables, at the value booked: fair market value);
|
||||
- add their share of the period's net income, closed or not;
|
||||
- less distributions;
|
||||
- any other change in their capital, when there is one;
|
||||
- capital at the end of the period (always opening plus the movements,
|
||||
exactly);
|
||||
- their share of profit, loss, and capital; and their part of what the
|
||||
company owes the members jointly, split by the members' shares: the
|
||||
balance of the `[books] financing_accounts` liabilities (the
|
||||
liabilities members advance), or, only when that is not set, of the
|
||||
liability accounts named "due to" / "owed to" (such as 2100 Due to
|
||||
Shareholders).
|
||||
|
||||
Cash and property are told apart by `[books] cash_accounts`. Without it
|
||||
the statement reads money as the engine does (every asset account but
|
||||
receivables), so every contribution reads as cash; set `cash_accounts`
|
||||
to see property apart. (The Cash page keeps its own name-based guess.)
|
||||
|
||||
### Migration
|
||||
|
||||
Old journals replay unchanged. `vendor.added` and `customer.added` keep
|
||||
@@ -87,7 +173,13 @@ same name share one party; a second vendor of a taken name gets a new
|
||||
party named "Name (2)" (names stay unique). Vendor and customer names
|
||||
obey the party text rules (no full TIN). `entry.vendor_set` also sets the
|
||||
entry's party, and `entry.party_set` to a party with a vendor record sets
|
||||
the vendor tag: one counterparty.
|
||||
the vendor tag: one counterparty. On an entry naming several parties the
|
||||
vendor tag follows the parties, never the other way: `entry.vendor_set`
|
||||
for the vendor of a party already named keeps every share; for any other
|
||||
vendor, or clearing the tag while its party is named, it is refused. A
|
||||
vendor record can't link to a party named beside another party with a
|
||||
vendor record (refused: the entry would name two vendors); linking one
|
||||
tags the entries naming the party that have no vendor tag yet.
|
||||
|
||||
### Tools
|
||||
|
||||
@@ -115,13 +207,22 @@ year. The directory shows the same columns, sortable.
|
||||
|
||||
## 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.
|
||||
Decision (Stephen, 2026-10-15): wanderbooks holds full TINs, so what it
|
||||
hands the CPA is complete, but never in the journal. The vault holds
|
||||
everyone's, members included, and the CPA package carries them: "In the
|
||||
vault, that's the WHOLE reason we built it. And it needs to be in the
|
||||
damn CPA package."
|
||||
|
||||
- **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.
|
||||
than the 1099 worksheet and the CPA package (saved outside the books
|
||||
dir), or the viewer. Nothing outside the vault holds more than the last
|
||||
four digits.
|
||||
- **Who:** any party, whatever its roles: every member (their K-1 needs
|
||||
it) and every 1099 payee. The books keep the party's TIN facts (type,
|
||||
last four, the vault record's hash; `party.tin_set`), which never
|
||||
disagree with its W-9 facts' last four. The viewer shows "TIN on file
|
||||
••••1234 (SSN)" on the party's page, never more.
|
||||
- **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
|
||||
@@ -139,24 +240,35 @@ worksheet it hands the CPA is complete, but never in the journal.
|
||||
- **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.
|
||||
- **Use:** `books.vendor_1099` with `include_tins: true` and
|
||||
`books.export_cpa_package` (its K-1 worksheet, `k1-members.csv`, and its
|
||||
1099 worksheet; `include_tins` defaults to true there) decrypt them.
|
||||
The package is written only to `save_to`, outside the books dir, and
|
||||
the tool returns its path; it is refused without one. 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.
|
||||
open and verify; the 1099 worksheet and the CPA package then say which
|
||||
TINs are missing (`tin_gaps`), and the package is still built.
|
||||
- **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.
|
||||
With a TIN it also records the party's TIN facts. `books.set_tin
|
||||
{party, tin, tin_type}` stores any party's TIN (a member's SSN, say) the
|
||||
same way and journals `party.tin_set`; a party whose W-9 facts name
|
||||
another last four is refused (a payee's new TIN comes with its W-9).
|
||||
`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
|
||||
`kept_at` saying when and why, and `party.tin_set` with `vault_record`
|
||||
cleared and `purged` saying when and why. `books.vendor_1099 {include_tins: true}`
|
||||
is the one tool output with full TINs (with a warning; `save_csv_to`
|
||||
writes the CSV outside the books dir); `books.export_cpa_package
|
||||
{save_to}` writes them in its zip, outside the books dir, and returns
|
||||
only the path. 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.
|
||||
|
||||
+66
-11
@@ -234,6 +234,14 @@ closed period).
|
||||
period at a time: a member's ending on one statement is their beginning
|
||||
on the next, to the cent, and a member's period income is the
|
||||
roll-forward's remainder (their share within a rounding cent).
|
||||
- A correction undoes what it corrects (B-9): an entry and its reversal
|
||||
both in a statement's span are neither contributions nor
|
||||
distributions (`reports::cancelled_in_span`; the K-1 worksheet reads
|
||||
the same). The K-1 worksheet's contributed property (Item L) is what
|
||||
the member owned and handed over, with its own basis (§723): never a
|
||||
prepaid service they paid for the company (cash), and shared among
|
||||
members credited in one entry in proportion to their credits
|
||||
(`k1` INVARIANTS 3, 4, 6).
|
||||
|
||||
**Enforced:** `CloseTo::Members`, `periods::allocate`,
|
||||
`kernel::apportion`, `members_equity`. **Checked:** Kani
|
||||
@@ -387,6 +395,38 @@ bill is not paid until it is (`form1099::vendor_1099_report_with`,
|
||||
checked by `payments_come_from_cash_and_financing_only`). A W-9 is on
|
||||
file by the vendor's flag or its party's W-9 facts.
|
||||
|
||||
An entry naming several parties (a payment shared between a contractor
|
||||
and a member; `ledger_core::shares`) counts only the vendor's share
|
||||
toward its 1099: what was paid to the payee (§6041(a)), not what the
|
||||
entry moved for others. The share is resolved (`shares::resolve`)
|
||||
against the entry's flow, the same target the MCP server checks shares
|
||||
against, then scaled to the 1099 payment: the share unchanged when the
|
||||
payment equals the target (the usual case, an expense paid from cash or
|
||||
by a member), else payment × share ÷ target, rounded to the nearest cent,
|
||||
half away from zero (so a part charged to a card scales out, 1099-K). A
|
||||
percent split's leftover cent goes to the party named first (shares
|
||||
INVARIANT 8). A reversal carries the shares and subtracts the same part,
|
||||
and re-apportioning either one re-apportions the other
|
||||
(`Ledger::set_entry_parties` INVARIANT 5), so the pair nets to zero. The
|
||||
vendor tag follows the parties: on an entry naming several,
|
||||
`set_entry_vendor` never replaces their shares (refused unless the
|
||||
vendor's party is named), and a vendor record never links to a party
|
||||
named beside another vendor (`add_vendor`), checked by
|
||||
`tagging_the_vendor_keeps_a_shared_entrys_parties`,
|
||||
`clearing_the_vendor_on_a_shared_entry_is_refused`,
|
||||
`re_apportioning_a_reversed_entry_re_apportions_its_reversal`,
|
||||
`a_named_party_gaining_a_vendor_record_keeps_one_vendor_per_entry`.
|
||||
Shares that don't resolve count the whole payment toward the vendor (the
|
||||
conservative answer) and the entry is listed (`Vendor1099Report::
|
||||
unresolved`; a `books.vendor_1099` note). An entry naming one party
|
||||
counts as before (`form1099::vendor_1099_report_with_members`, checked
|
||||
by `a_shared_payment_counts_the_vendors_share`,
|
||||
`a_percent_split_gives_the_leftover_cent_to_the_first`,
|
||||
`a_reversed_shared_payment_nets_to_zero`,
|
||||
`a_shared_payment_partly_on_a_card_scales_to_the_payment`,
|
||||
`unresolved_shares_count_in_full_and_are_listed`). DECISION (Stephen,
|
||||
2026-10-15: shares).
|
||||
|
||||
**Enforced:** `form1099::default_threshold`, a dated rule
|
||||
(`ledger_core::rules::THRESHOLD_1099`, known through 2026, review from
|
||||
2026-11-01). For a later year the engine refuses with `RuleNotKnown`
|
||||
@@ -525,20 +565,35 @@ 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.
|
||||
goes there too, never to the attachment store. Any party may have its
|
||||
TIN in the vault, whatever its roles: every member's (for their K-1) and
|
||||
every 1099 payee's belongs there (DECISION, Stephen, 2026-10-15: "In the
|
||||
vault, that's the WHOLE reason we built it. And it needs to be in the
|
||||
damn CPA package."). `books.set_tin` stores one for any party; the books
|
||||
journal its TIN facts (type, last four, the record's hash:
|
||||
`party.tin_set`), which never disagree with its W-9 facts' last four. 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): a refusal that quotes its input masks any TIN in it
|
||||
(`parties::redact_full_tins`, checked by `refusals_never_echo_a_tin`),
|
||||
and a party text field refuses nine digits however they are separated
|
||||
(space, hyphen, dot, slash, underscore; `has_full_tin`). Files holding one (`books.get_w9`, `save_csv_to`, and the CPA
|
||||
package, whose K-1 and 1099 worksheets carry every member's and payee's
|
||||
TIN by default) are written only outside the books dir, and the package
|
||||
comes back only as its path. A locked vault or a missing TIN never fails
|
||||
the package: it says which TINs are missing. `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`;
|
||||
`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).
|
||||
`set_party_w9`, `set_party_w4`, `set_party_tin` (TinFacts::check; TIN and
|
||||
W-9 facts agree), `set_entry_party`, `validate_payroll`; `ledger-core`
|
||||
`k1` (the K-1 worksheet); `ledger-vault` (Tin::new, the encrypted
|
||||
records, records_held, purge_records); `ledger-mcp` `books.set_w9`,
|
||||
`books.set_tin`, `books.get_w9`, `books.purge_tin`, `books.vendor_1099`,
|
||||
`books.export_cpa_package` (open_vault refuses a key file in the books
|
||||
dir; outside_books for files with a TIN; vault_tin checks the vault
|
||||
against the books).
|
||||
**Checked:** `ledger-core` `tests/spec_parties.rs`; `ledger-store`
|
||||
`tests/parties.rs`; `ledger-vault` `tests/vault.rs`; `ledger-mcp`
|
||||
`tests/spec_parties.rs`, `tests/spec_vault.rs` (every response, the log,
|
||||
|
||||
Reference in new issue
Block a user