P3: party totals by what moved the money, once (defects 14, 11, 24)

Each party's part of an entry is signed as its own account would show it,
with a direction and its cash and in-kind amounts; a reversal is its
original negated. Totals on a cash or all basis: for a member, all equals
the members' equity statement and cash the K-1's cash. Money is cash,
cards and the financing accounts, and a liability credit never pays its
own creditor (creditor_of), so accruals and declared distributions count
once. tin_last4 from the TIN or W-9 facts.

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-21 12:12:26 -06:00
1 parent 9ab003d0d1
commit c2ceabbae9
52 files changed
+7535 -560

No files matched your search

+138
View File
@@ -1091,6 +1091,144 @@ fn constructive_mirrors() {
}
}
// ---------------------------------------------------------------------------
// A party's part of an entry (rules.md B-35)
// ---------------------------------------------------------------------------
/// The inputs `kernel::party_target` reads, any in range: a capital
/// movement (when every party named is a member) with the property beside
/// it, the money net, the own-account movement, and the debits (a
/// magnitude).
fn any_party_inputs() -> (Option<(i64, i64)>, i64, i64, i64) {
let capital = if kani::any() {
let property = any_amount();
kani::assume(property >= 0);
Some((any_amount(), property))
} else {
None
};
let debits = any_amount();
kani::assume(debits >= 0);
(capital, any_amount(), any_amount(), debits)
}
/// B-35 items 1-4: the direction is the sign of what counts (paid above
/// zero, received below), neither exactly when nothing counts; cash and
/// in kind each carry the whole's sign and together never exceed it; a
/// money target is all cash; a contribution counts all of itself; a
/// distribution's cash is never more than the money it paid, nor its in
/// kind more than the property it gave (the rest is still owed).
#[kani::proof]
fn party_target_direction_is_its_sign() {
let (capital, net, own, debits) = any_party_inputs();
let t = kernel::party_target(capital, net, own, debits).expect("in range");
let sign = |a: i64| a.signum();
assert!(t.cash == 0 || sign(t.cash) == sign(t.amount));
assert!(t.in_kind == 0 || sign(t.in_kind) == sign(t.amount));
assert!(t.cash.abs() + t.in_kind.abs() <= t.amount.abs());
match t.direction {
kernel::Direction::Neither => {
assert!(t.cash == 0 && t.in_kind == 0);
}
kernel::Direction::Paid => {
assert!(t.amount > 0 && t.cash + t.in_kind > 0);
}
kernel::Direction::Received => {
assert!(t.amount < 0 && t.cash + t.in_kind < 0);
}
}
let capital_case = matches!(capital, Some((e, _)) if e != 0);
if !capital_case && net != 0 {
assert!(t.amount == net && t.cash == net && t.in_kind == 0);
}
if let Some((e, property)) = capital {
if e < 0 {
// A contribution counts all of itself, money or property.
assert_eq!(t.cash + t.in_kind, e);
assert!(-t.in_kind <= property);
}
if e > 0 {
// A distribution: what it paid, then what it gave; no more.
assert!(t.cash <= net.max(0) && t.in_kind <= property);
}
}
kani::cover!(
t.direction == kernel::Direction::Paid && t.in_kind != 0 && t.cash != 0,
"a distribution partly in kind"
);
kani::cover!(
capital_case && t.direction == kernel::Direction::Neither,
"a distribution all still owed"
);
kani::cover!(
t.direction == kernel::Direction::Paid && t.cash + t.in_kind < t.amount,
"a distribution partly still owed"
);
}
/// B-35 item 3 and B-9: the target is odd. Negating the lines negates the
/// capital movement, the money net and the own-account movement, and the
/// property comes from the other side, so it keeps its size: the result
/// is the original's reverse_target, whenever it isn't the debits fallback
/// (a sign means nothing there) and a member's entry counts exactly its
/// money as cash (not an expense booked straight to capital, a debt
/// converted, or a distribution still owed). The reversal arm, not the
/// lines, gives every reversal its target, so these exceptions never reach
/// one.
#[kani::proof]
fn party_target_is_odd() {
let (capital, net, own, debits) = any_party_inputs();
if let Some((e, property)) = capital
&& e != 0
{
// The money the member's entry moved their way, signed as the
// distribution side reads it, is exactly what isn't property.
let side_net = if e > 0 { net } else { -net };
let whole = e.abs();
kani::assume(side_net.max(0).min(whole) == whole - property.min(whole));
}
let t = kernel::party_target(capital, net, own, debits).expect("in range");
let negated = capital.map(|(e, p)| (-e, p));
let n = kernel::party_target(negated, -net, -own, debits).expect("in range");
let fallback = !matches!(capital, Some((e, _)) if e != 0) && net == 0 && own == 0;
if !fallback {
assert_eq!(Some(n), kernel::reverse_target(t));
}
kani::cover!(!fallback && capital.is_some(), "a member's entry, negated");
kani::cover!(
matches!(capital, Some((e, p)) if e > 0 && p > 0 && net > 0 && net < e),
"a distribution of money and property, negated"
);
}
/// B-9: reversing a reversal gives the original back, and reverse_target
/// never panics: past the range it refuses.
#[kani::proof]
fn reverse_target_is_an_involution() {
let direction = match kani::any::<u8>() {
0 => kernel::Direction::Paid,
1 => kernel::Direction::Received,
_ => kernel::Direction::Neither,
};
let t = kernel::PartyTarget {
amount: kani::any::<i64>(),
direction,
cash: kani::any::<i64>(),
in_kind: kani::any::<i64>(),
};
match kernel::reverse_target(t) {
Some(r) => {
assert_eq!(r.direction, t.direction.flip());
assert_eq!(r.amount, -t.amount);
assert_eq!(kernel::reverse_target(r), Some(t));
}
None => {
assert!(t.amount == i64::MIN || t.cash == i64::MIN || t.in_kind == i64::MIN);
}
}
kani::cover!(t.amount == i64::MIN, "the range's edge");
}
// ---------------------------------------------------------------------------
// The checker itself
// ---------------------------------------------------------------------------