Each close into members' capital rounded on its own, ties to the earlier member, so twelve monthly 50/50 closes of odd-cent income could leave one member several cents ahead of half the year (docs/testing.md #12, found with the members' equity fix). - CloseTo::Members now splits cumulatively over a run (closes back to back with the same members and shares): each close gives a member allocate(run total so far) - allocate(run total before). Periods record their close order and a cumulative close's members and net (Period::close_seq, member_close) so the next close continues the run and replay recomputes it exactly. A close to anything else, or other shares, ends the run. - Journaled "rounding": "cumulative". The per-close rule is kept as CloseTo::MembersPerClose for closes journaled "largest_remainder"; older closes still replay with the last member absorbing it. - Lean: runPart_telescopes (a member's total from a run is their allocation of the run's total) and closePart_total (each close hands out exactly its net), audited to the standard axioms. - Tests: hand-computed three one-cent months (per close A 0.03 / B 0; cumulative A 0.02 / B 0.01), a run ending, a property that each member stays within a cent of their share (fails on the per-close rule), and a store contract that each rule journals and replays as itself. Wanderland's books have no closes yet, so all of theirs use the new rule. 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
f4381f170c
commit
fb3b4ab476
11 files changed
+416
-16
No files matched your search
@@ -1099,7 +1099,9 @@ impl Store {
|
||||
CloseTo::RetainedEarnings { sweep } if sweep.is_empty() => json!("retained_earnings"),
|
||||
CloseTo::RetainedEarnings { sweep } => json!({"retained_earnings": {"sweep": sweep}}),
|
||||
CloseTo::Account { into, sweep } => json!({"account": {"into": into, "sweep": sweep}}),
|
||||
CloseTo::Members(members) | CloseTo::MembersLastAbsorbs(members) => {
|
||||
CloseTo::Members(members)
|
||||
| CloseTo::MembersPerClose(members)
|
||||
| CloseTo::MembersLastAbsorbs(members) => {
|
||||
let mut v = json!({
|
||||
"members": members
|
||||
.iter()
|
||||
@@ -1110,10 +1112,13 @@ impl Store {
|
||||
}))
|
||||
.collect::<Vec<_>>(),
|
||||
});
|
||||
// Closes journaled before 2026-10-13 carry no rounding and
|
||||
// replay with the last member absorbing it.
|
||||
if matches!(to, CloseTo::Members(_)) {
|
||||
v["rounding"] = json!("largest_remainder");
|
||||
// The rounding rule, so replay closes the same way. Closes
|
||||
// journaled before 2026-10-13 carry none (the last member
|
||||
// absorbed it).
|
||||
match to {
|
||||
CloseTo::Members(_) => v["rounding"] = json!("cumulative"),
|
||||
CloseTo::MembersPerClose(_) => v["rounding"] = json!("largest_remainder"),
|
||||
_ => {}
|
||||
}
|
||||
v
|
||||
}
|
||||
@@ -3487,10 +3492,12 @@ fn close_to_from_journal(v: Option<&Value>) -> Result<CloseTo, StoreError> {
|
||||
})
|
||||
.collect::<Result<_, StoreError>>()?;
|
||||
// The rounding rule the close was made under: absent before
|
||||
// 2026-10-13, when the last member absorbed it.
|
||||
// 2026-10-13, when the last member absorbed it; "largest_remainder"
|
||||
// per close, then "cumulative" over a run of closes.
|
||||
match v.get("rounding").and_then(|r| r.as_str()) {
|
||||
None => Ok(CloseTo::MembersLastAbsorbs(members)),
|
||||
Some("largest_remainder") => Ok(CloseTo::Members(members)),
|
||||
Some("largest_remainder") => Ok(CloseTo::MembersPerClose(members)),
|
||||
Some("cumulative") => Ok(CloseTo::Members(members)),
|
||||
Some(_) => Err(corrupt("unknown rounding")),
|
||||
}
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user