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

+170
View File
@@ -543,6 +543,176 @@ fn apportion_is_fair() {
);
}
/// `kernel::apportion_cumulative` is `kernel::apportion` less what was
/// given: it succeeds exactly when `apportion` would on the same inputs and
/// the running total is in line range (the parts, unit and `given` are,
/// here), and then each part is apportion's less its `given`, so each
/// running total (given + part) is apportion's. With `apportion_is_exact`,
/// the parts sum to the running total less what was given, and each
/// running total is nothing or one unit above its rounded-down part (one
/// of them may also take the sub-unit fraction). A refusal leaves the
/// parts as they were.
#[kani::proof]
#[kani::unwind(5)]
fn apportion_cumulative_is_exact() {
let (n, before, rems, unit, _, whole) = any_apportionment();
let given: [i64; AP] = core::array::from_fn(|_| any_amount());
let mut reference = before;
let apportioned = kernel::apportion(whole, &mut reference[..n], &rems[..n], unit);
let mut parts = before;
let ok = kernel::apportion_cumulative(whole, &mut parts[..n], &rems[..n], unit, &given[..n]);
assert_eq!(ok.is_some(), apportioned.is_some() && whole.in_line_range());
let mut against = false;
for i in 0..n {
if ok.is_some() {
assert_eq!(parts[i] + given[i], reference[i]);
against |= parts[i] < 0;
} else {
assert_eq!(parts[i], before[i]);
}
}
kani::cover!(
ok.is_some() && n == AP && against && whole > given[0] + given[1] + given[2],
"a true-up against the net's sign"
);
}
/// Bound on the amounts the drift harnesses explore (symbolic multiply and
/// divide are costly for the solver; Lean's `allocW_drift` and
/// `cumulative_drift` carry any weights and any amounts).
const DRIFT: i64 = 1 << 12;
/// **Bounded drift** (rules.md B-13) for members with these integer
/// weights (shares w_i / W, W their sum): a cumulative members close,
/// modelled as `periods::allocate_cumulative` runs it at whole minor units
/// (unit 1), hands out exactly its net, and leaves every member's running
/// total, given + part, within one unit of their exact share of
/// everything closed, whatever earlier closes gave them (any `given`
/// summing to what they closed: legacy rounding included). Scaled by W:
/// |(given_i + part_i)·W − T·w_i| < W.
fn drift_is_under_a_unit(weights: &[i64]) {
let n = weights.len();
let mut total_weight = 0i64;
for w in weights {
total_weight += w;
}
let closed: i64 = kani::any();
let net: i64 = kani::any();
kani::assume((-DRIFT..=DRIFT).contains(&closed) && (-DRIFT..=DRIFT).contains(&net));
// What earlier closes gave: anything, as long as it adds up.
let mut given = [0i64; AP];
let mut others = 0i64;
for g in given.iter_mut().take(n - 1) {
*g = kani::any();
kani::assume((-DRIFT..=DRIFT).contains(g));
others += *g;
}
given[n - 1] = closed - others;
// allocate_cumulative: |T| split by exact share, rounded toward zero,
// the remainders ranked; a loss runs on |T| against given negated.
let total = closed + net;
let magnitude = total.abs();
let loss = total < 0;
let mut parts = [0i64; AP];
let mut rems = [0i64; AP];
let mut against = [0i64; AP];
for i in 0..n {
parts[i] = magnitude * weights[i] / total_weight;
rems[i] = magnitude * weights[i] % total_weight;
against[i] = if loss { -given[i] } else { given[i] };
}
kernel::apportion_cumulative(magnitude, &mut parts[..n], &rems[..n], 1, &against[..n])
.expect("exact shares are fewer units short than there are members");
let mut sum = 0i64;
for i in 0..n {
if loss {
parts[i] = -parts[i];
}
sum += parts[i];
let drift = (given[i] + parts[i]) * total_weight - total * weights[i];
assert!(-total_weight < drift && drift < total_weight);
}
assert_eq!(sum, net);
kani::cover!(
loss && given[0] * total_weight != closed * weights[0],
"a loss, after an earlier split off the exact share"
);
}
/// [`drift_is_under_a_unit`] for two members at half each: every odd cent
/// a tie.
#[kani::proof]
#[kani::unwind(4)]
fn cumulative_close_never_drifts_a_unit_at_halves() {
drift_is_under_a_unit(&[1, 1]);
}
/// [`drift_is_under_a_unit`] for three members at 20% / 30% / 50%:
/// remainders that differ.
#[kani::proof]
#[kani::unwind(4)]
fn cumulative_close_never_drifts_a_unit_at_tenths() {
drift_is_under_a_unit(&[2, 3, 5]);
}
/// [`drift_is_under_a_unit`] for three members at 33.33% / 33.33% /
/// 33.34%, the nearest decimal shares to a third each.
#[kani::proof]
#[kani::unwind(4)]
fn cumulative_close_never_drifts_a_unit_at_near_thirds() {
drift_is_under_a_unit(&[3333, 3333, 3334]);
}
/// A cumulative members close balances: the nominal accounts' closing
/// lines and each member's part of the running split (any earlier split
/// of what was closed before) net to zero, whatever the true-up.
#[kani::proof]
#[kani::unwind(6)]
fn cumulative_closing_entry_balances() {
const NOMINAL: usize = 3;
let nominal: [i64; NOMINAL] = core::array::from_fn(|_| {
let a = any_amount();
kani::assume((-(1 << 38)..=1 << 38).contains(&a));
a
});
let mut entry = [(Side::Debit, 0i64); NOMINAL + 2];
let mut len = 0;
for &m in &nominal {
if let Some(line) = kernel::closing_line(m) {
entry[len] = line;
len += 1;
}
}
let net = kernel::net(entry[..len].iter().copied()).expect("no overflow");
// Two members at half each; earlier closes gave them any split of
// what they closed.
let closed: i64 = kani::any();
kani::assume((-(1 << 38)..=1 << 38).contains(&closed));
let first = any_amount();
let given = [first, closed - first];
let total = closed + net;
let (magnitude, loss) = (total.abs(), total < 0);
let mut parts = [magnitude / 2, magnitude / 2];
let rems = [magnitude % 2, magnitude % 2];
let against = if loss { [-given[0], -given[1]] } else { given };
if kernel::apportion_cumulative(magnitude, &mut parts, &rems, 1, &against).is_none() {
// Only an earlier split out of line range is refused here.
assert!(!given[1].in_line_range());
return;
}
let mut true_up = false;
for p in parts {
let p = if loss { -p } else { p };
if let Some(line) = kernel::credit_line(p) {
entry[len] = line;
len += 1;
true_up |= net != 0 && (p > 0) != (net > 0);
}
}
assert_eq!(wide_net(&entry[..len]), 0);
kani::cover!(true_up, "a true-up line on a member");
}
// ---------------------------------------------------------------------------
// The checker itself
// ---------------------------------------------------------------------------