P3: odd cents split the same running way everywhere (defect 17, D-38)
check / check (push) Canceled after 0s

Members' shares of earnings are split cumulatively by largest remainder in
the closes, the members' equity statement, the K-1 worksheet and the
viewer, so a member's capital on a given day never moves by a cent when its
month is later closed. Old closes replay as they were; the first P3 close
carries any true-up. Feature members_cumulative. Kani and Lean proofs of
the bounded drift; bookspec examples.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01T3gXMVt7XJPi6ens1or9Dj
This commit is contained in:
swaitsandClaude Opus 5.5 committed 2026-10-20 21:35:00 -06:00
1 parent 2390dc6553
commit a7f3cc09a9
44 files changed
+6636 -195

No files matched your search

+74 -1
View File
@@ -142,10 +142,58 @@ sequence of commands, of any length**:
of cumulative closes is their allocation of everything the run closed
(so its rounding is one allocation's, never a sum of them), and each
close hands out exactly its net, for any allocation whose parts sum to
the whole (rules.md B-13).
the whole (rules.md B-13). Since P3 this is the run rule closes
journaled `"cumulative"` replay under (`CloseTo::MembersRun`).
- Lean `runPart_telescopes`, `closePart_total` · property
`each_member_stays_within_a_cent_of_their_share`
The P3 odd cent (defect 17; rules.md B-13; D-10, D-38) adds three laws;
their numbers are assigned when P3 lands:
- **A cumulative members close hands out exactly its net, and leaves each
member's running total one allocation of everything the count closed**,
whatever rule the count's earlier closes were made under, as long as
they handed out what they closed. The run rule is the special case where
they gave exactly the allocation. A refusal leaves the parts as they
were.
- Kani `apportion_cumulative_is_exact`,
`cumulative_closing_entry_balances` (the closing entry nets to zero
whatever the true-up)
- Lean `trueUp_total`, `trueUp_running`, `closePart_eq_trueUp`,
`Apportion.allocW_sum`, `Apportion.allocW_cumulative_total`
- properties `apportion_cumulative_agrees`,
`allocate_cumulative_agrees_with_the_integer_model`
- **Bounded drift: no member's running total is ever a minor unit from
their exact share.** With shares w_i / W (exact decimals) and every
amount a whole number of minor units (B-3), after every cumulative
close |(given_i + part_i)·W − T·w_i| < W, whatever earlier closes gave.
The precondition is the code's: `periods::allocate_cumulative` refuses a
product Decimal would round, so the remainders it ranks are exact, and
they sum to whole units (the Lean hypothesis of `rem_pos_of_rank_lt`).
- Kani `cumulative_close_never_drifts_a_unit_at_halves`, `_at_tenths`
and `_at_near_thirds` (two members at 50/50, three at 20/30/50%, three
at 33.33/33.33/33.34%; every running total, net and earlier split to
2^12 units)
- Lean `Apportion.rem_pos_of_rank_lt` (a member given a unit had a
positive remainder), `Apportion.apportion_drift` (one apportionment
is within a unit), `Apportion.allocW_drift` (the model of
`allocate_cumulative`, gain or loss), `cumulative_drift` (any
allocation within a unit stays so under the true-up),
`Apportion.allocW_cumulative_drift` (the two together): any number of
members, any amounts
- property `members_capital_never_drifts_a_cent_from_their_share`
- **A member's capital on a day never moves when its period is closed.**
The statement of members' equity gives each member, of the earnings not
yet closed, what the count's next close would give them; closing moves
those earnings into capital without changing the sum. When the count's
earlier closes gave something else (closes under an earlier rule), the
figure moves by exactly the true-up the close posts, never before.
- Lean `statement_after_close`, `statement_unmoved_by_close`
- property `each_members_capital_chains_and_holds_through_closes`;
`three_members_at_a_third_each_over_twelve_months` (every day of a
year, closed and never closed), the viewer's
`a_members_page_shows_the_same_capital_before_and_after_a_close`
## What the proofs found
The engine already had 190+ tests, property tests, mutation testing,
@@ -244,6 +292,31 @@ TINs kept out of the books (B-26), and the agent text lint. Out of scope
for proof today: the format gate and the walk are I/O, and the model in
`verify/lean` stops at the ledger.
### P3: the odd cent (2026-10-20)
The cumulative members close (rules.md B-13) adds a kernel function,
`kernel::apportion_cumulative`, which Kani proves exact and refusal-safe
for up to three parts and checks against the integer model of
`periods::allocate_cumulative` for bounded drift; Lean proves the drift
bound and the true-up laws for any number of members and any amounts. Not
proved: the regime query (`Ledger::members_regime`: which closes count),
the Decimal exactness check in `allocate_cumulative`, and the journal's
`since_seq`. They are checked by the tests in `ledger-core`
`tests/odd_cent.rs`, `ledger-store` `tests/member_closes.rs` and the
old-books fixture `x_member_closes` (docs/testing.md, "P3: odd cents").
Kani times, each harness alone on a busy laptop (Kani 0.68, 2026-10-20):
`apportion_cumulative_is_exact` 257 s, `cumulative_close_never_drifts_a_unit_at_halves`
17 s, `_at_tenths` 320 s, `_at_near_thirds` 449 s,
`cumulative_closing_entry_balances` 202 s (`apportion_is_exact`, for
scale: 121 s). Two shapes were too slow and were changed, not weakened in
what they prove: restating apportion's postconditions on the cumulative
parts directly ran past ten minutes, so `apportion_cumulative_is_exact`
proves the parts are apportion's less `given` on the same inputs (the
solver shares the two runs' formulas) and `apportion_is_exact` supplies
the rest; and the drift bound with symbolic weights ran past ten minutes,
so it is checked at three fixed sets of shares (the brief's fallback),
Lean carrying any weights.
## Running it
```sh