From 3e65c83f96589cd273155f67f5e5e2a4799e40ec Mon Sep 17 00:00:00 2001 From: Stephen Waits Date: Mon, 26 Oct 2026 16:08:11 -0600 Subject: [PATCH] docs: B-26's TIN separators and P7's mutation targets (P7) rules.md B-26 now says what the scanner reads: a comma or a parenthesis between a TIN's 3-2-4 or 2-7 groups, and any run of spaces counting once, while an amount's comma-grouped thousands stay an amount. The filing scan's (a) and (b) renderings gain commas. docs/testing.md gains the P7 section and the For-P12 mutation targets (D-33), and plan/ served.md's P7 As-built block records the three departures. --- docs/spec/rules.md | 20 +++++++++++++------- docs/testing.md | 40 ++++++++++++++++++++++++++++++++++++++++ plan/served.md | 6 +++++- 3 files changed, 58 insertions(+), 8 deletions(-) diff --git a/docs/spec/rules.md b/docs/spec/rules.md index 416bcdd..250eca9 100644 --- a/docs/spec/rules.md +++ b/docs/spec/rules.md @@ -1482,8 +1482,12 @@ 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 (`has_full_tin`: up to three spaces, hyphens, dots, slashes or -underscores between neighbors, in any script's digits; past ASCII every -Unicode space and dash and the fullwidth and small forms count, and +underscores between neighbors, in any script's digits, a run of spaces +counting once however long it is; and a comma or a parenthesis between +a TIN's 3-2-4 or 2-7 groups (DECISION, P7, 2026-10-26), so +`123,45,6789` and `(123) 45-6789` read as a TIN while an amount's +comma-grouped thousands do not, `1,234,567.89` being 1-3-3-2; past ASCII +every Unicode space and dash and the fullwidth and small forms count, and invisible characters such as a zero-width space or a right-to-left mark don't count toward the three: `sources/unicode-tin-text.md`, Unicode 17.0). Files holding one (`books.get_w9`, `books.vendor_1099`'s @@ -1511,12 +1515,14 @@ and refuses any text file (by its type or name, or because it decodes as UTF-8 or UTF-16, with a byte-order mark or, NUL-interleaved, without one) holding (a) a TIN the vault holds for a party of the books, anywhere in the text, as nine digits in a row, as 3-2-4 digits -joined by hyphens, spaces or dots, or as 2-7 digits joined by a hyphen -or a space (compared on the server against the decrypted vault, never -returned or logged; a vault that can't be opened leaves (b) alone), or +joined by hyphens, spaces, dots or commas, or as 2-7 digits joined by a +hyphen, a space or a comma (compared on the server against the decrypted +vault, never returned or logged; a vault that can't be opened leaves (b) +alone), or (b) a number printed as an SSN or EIN is, standing alone: 3-2-4 digits -joined by hyphens (123-45-6789) or 2-7 joined by a hyphen (12-3456789), -with no digit, and no hyphen or dot followed by a digit, on either side, +joined by hyphens or commas (123-45-6789, 123,45,6789) or 2-7 joined by a +hyphen or a comma (12-3456789, 12,3456789), with no digit, and no hyphen +or dot followed by a digit, on either side, except the company's own EIN as its settings hold it, printed 2-7: it is the company's, no person's, the settings already hold it in full (B-29), and every company document goes into the books, its formation papers and diff --git a/docs/testing.md b/docs/testing.md index c3d878b..7011e5f 100644 --- a/docs/testing.md +++ b/docs/testing.md @@ -2807,3 +2807,43 @@ asserts from `invoice_document` today, so expect survivors there until one does: `PartiallyPaid` (no sample is part paid and not yet due) and `Draft` (only S4's preview reaches it; the renderer's own test renders it but asserts only that it succeeds). +## P7: the TIN scanner's separators (2026-10-26) + +rules.md B-26; plan 8.2 P7, with vault v2 closed by D-48 (nothing of +HPKE, keyslots or human pages is built here). The scanner +(`ledger_core::parties::has_full_tin`, `redact_full_tins`) reads the +commas, parentheses and any run of spaces that a TIN's groups can be +written with: `123,45,6789`, `(123) 45-6789`, `12 3456789`. A run of +spaces counts once however long it is; a comma or a parenthesis +separates only a 3-2-4 or 2-7 group, so an amount's comma-grouped +thousands (`1,234,567.89`, 1-3-3-2) stay an amount. The legacy run +reading is unchanged for everything it found (the property asserts it +never forgets one), and replay still reads a journaled line as P2 did +(`parties::replaying`, B-28). The access log's masking and digest use +the same function; the filing scan keeps its narrow printed shapes +(B-26: a bank export's routing numbers must go in) and gains the comma +renderings. + +Tests: `tin_shapes_with_any_separator_are_found` +(`crates/ledger-core/tests/properties.rs`: generated separators on a +3-2-4 and a 2-7 shape, found and masked to nine bullets; no false +positive on an amount or on `company.duns`'s nine-digit D-U-N-S number); +`has_full_tin_truth_table`, `has_full_tin_reads_every_script_and_separator` +and `has_full_tin_never_forgets_a_tin` (`spec_parties.rs`, updated where +they pinned the old separator set); `access_log_masks_a_comma_separated_tin` +(`crates/ledger-store/src/access.rs`: a client's texts and a digest); +`a_file_with_a_comma_separated_tin_is_refused` +(`crates/ledger-mcp/src/tin_files.rs`, every encoding, vault locked and +open, and an amount still filed). The TIN canaries in +`crates/ledger-cli/tests/spec_vault.rs` still pass. + +**For P12 (mutation targets):** `tin_gap` (the space-run arm, so four +spaces count once; `count <= MAX_TIN_GAP`; the invisible skip; the +`grouped` arm), `tin_space`, `grouped_tin_separator` (each of `,`, `(` +and `)`), `grouped_tin_runs` (each shape, the `n > 0` join, and the +nine-digit check), `full_tin_runs`'s `extend(grouped_tin_runs(..))`; +`tin_files::tin_shaped`'s comma in each rendering and +`holds_printed_tin`'s comma join; `access::masked`'s string arm. +Deleting any one of them leaves a TIN that the old scanner passed +either readable or unmasked, which the truth table and the property +catch. diff --git a/plan/served.md b/plan/served.md index 821d56a..46d9c8c 100644 --- a/plan/served.md +++ b/plan/served.md @@ -1415,7 +1415,11 @@ Each unit follows the 8.0.4 template. "A-n" is an owner answer numbered in 8.3.1 5. **Files:** owns `has_full_tin`/`redact_full_tins`; touches `access.rs`, `tin_files.rs`, the two docs. 6. **Tests and proofs:** named above. 7. **Done when:** the property passes; the two integration tests pass; `no_mcp_output_contains_full_tin`-style canary tests (the existing TIN canary in `crates/ledger-cli/tests/spec_vault.rs`) still pass; `just check` green. -8. **As built:** none yet. +8. **As built** (2026-10-26, `0176214`, `fd9f678`): vault v2 stays closed (D-48): nothing of HPKE, keyslots, a recovery key or `/h/` pages was built. Departures from Build: + - Step 1's separators are read as the unit's own guard requires: a run of spaces counts once however long it is (`tin_gap`), and a comma or a parenthesis joins only a 3-2-4 or a 2-7 group (`grouped_tin_runs`), so `1,234,567.89` (1-3-3-2) stays an amount and a nine-digit D-U-N-S number stays `company.duns`'s kind. The truth table and the ASCII-equality property in `crates/ledger-core/tests/spec_parties.rs` pinned the old separator set (`291, 12, 2846` and `12 3456789` were false); they were updated in the same commit, and the property is now `has_full_tin_never_forgets_a_tin` (everything found before is still found). + - Step 1's ACH payment-method exemption is P4b S4's `invoicing.payment_methods` (`Kind::PaymentMethods`), which is not built; the property pins the two exemptions the books have today (`company.duns`, an amount) and says the S4 kind keeps a routing number out of the rule when it lands. + - Step 2's "the filing scan uses the same function" is not how `tin_files.rs` is built: B-26 makes its text check deliberately narrow so a bank export's routing numbers and amounts can be filed. It kept its own shapes and gained the comma renderings in `tin_shaped` and `holds_printed_tin` rather than calling `has_full_tin`, which would refuse `an_ordinary_bank_export_passes`. + - `docs/spec/rules.md` B-26 and `docs/testing.md` P7's For-P12 paragraph are updated; no other doc names this unit. #### P8: audit and attestations