Files
wanderbooks/verify/lean/Wanderbooks/Parties.lean
T
swaitsandClaude Opus 5.5 c2ceabbae9 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
2026-10-21 12:12:26 -06:00

216 lines
9.1 KiB
Lean4
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import Wanderbooks.Entry
/-!
# A party's part of an entry (rules.md B-35)
Mirrors `kernel::party_target` and `kernel::reverse_target`, and proves
what makes a reversal the exact opposite of what it reverses: the target
is odd (but for the debits fallback, where a sign means nothing, and a
member's entry whose cash isn't its money: an expense booked straight to
capital, a debt converted, a distribution still owed), reversing a target
twice gives it back, and a reversal's movement over any set of accounts
(a member's equity, say) is the original's negated.
-/
namespace Wanderbooks
/-- Which way an entry moved value with its parties (`kernel::Direction`). -/
inductive Direction where
| paid
| received
| neither
deriving DecidableEq, Repr
/-- Paid and received swap; neither stays neither. -/
def Direction.flip : Direction → Direction
| .paid => .received
| .received => .paid
| .neither => .neither
@[simp] theorem Direction.flip_flip (d : Direction) : d.flip.flip = d := by
cases d <;> rfl
/-- The direction of a signed amount (`Direction::of`). -/
def Direction.of (a : Int) : Direction :=
if 0 < a then .paid else if a < 0 then .received else .neither
theorem Direction.of_neg (a : Int) : Direction.of (-a) = (Direction.of a).flip := by
unfold Direction.of
by_cases h1 : 0 < a
· rw [ite_eq_right (show ¬ (0 < -a) by omega), ite_eq_left (show -a < 0 by omega), ite_eq_left h1]; rfl
· by_cases h2 : a < 0
· rw [ite_eq_left (show 0 < -a by omega), ite_eq_right h1, ite_eq_left h2]; rfl
· rw [ite_eq_right (show ¬ (0 < -a) by omega), ite_eq_right (show ¬ (-a < 0) by omega), ite_eq_right h1,
ite_eq_right h2]; rfl
/-- `kernel::PartyTarget`. -/
structure PartyTarget where
amount : Int
direction : Direction
cash : Int
inKind : Int
deriving DecidableEq, Repr
/-- `kernel::reverse_target`: every amount negated, paid and received
swapped. -/
def reverseTarget (t : PartyTarget) : PartyTarget :=
⟨-t.amount, t.direction.flip, -t.cash, -t.inKind⟩
/-- **Reversing a reversal gives the original back** (B-9). -/
theorem reverseTarget_involutive (t : PartyTarget) :
reverseTarget (reverseTarget t) = t := by
cases t; simp [reverseTarget]
/-- A magnitude. -/
def magnitude (e : Int) : Int := if e < 0 then -e else e
theorem magnitude_neg (e : Int) : magnitude (-e) = magnitude e := by
unfold magnitude
by_cases h : e < 0
· rw [ite_eq_right (show ¬ (-e < 0) by omega), ite_eq_left h]
· by_cases h' : -e < 0
· rw [ite_eq_left h', ite_eq_right h]; omega
· rw [ite_eq_right h', ite_eq_right h]; omega
/-- The magnitude `m` with the sign of `like`. -/
def withSign (like m : Int) : Int := if like < 0 then -m else m
theorem withSign_neg (e m : Int) (he : e ≠ 0) : withSign (-e) m = -withSign e m := by
unfold withSign
by_cases h : e < 0
· rw [ite_eq_right (show ¬ (-e < 0) by omega), ite_eq_left h]; omega
· rw [ite_eq_left (show -e < 0 by omega), ite_eq_right h]
/-- What of a member's capital movement `e` counts, as (cash, in kind)
(`kernel::party_target` INVARIANTS 1-2): of a contribution (`e < 0`), the
property in it, at most `|e|`, in kind and the rest cash; of a
distribution, the money it paid (`net`, at most `e`) as cash, then the
property it gave, at most what is left, in kind. The rest of a
distribution is still owed and counts as nothing. -/
def capitalParts (e property net : Int) : Int × Int :=
if e < 0 then
(magnitude e - min (max property 0) (magnitude e), min (max property 0) (magnitude e))
else
(min (max net 0) (magnitude e),
min (max property 0) (magnitude e - min (max net 0) (magnitude e)))
/-- The capital case of `kernel::party_target` (a member's entry moving
equity `e ≠ 0`), signed as `e`; neither when nothing counts. -/
def capitalTarget (e property net : Int) : PartyTarget :=
⟨e, if (capitalParts e property net).1 + (capitalParts e property net).2 = 0 then .neither
else Direction.of e,
withSign e (capitalParts e property net).1, withSign e (capitalParts e property net).2⟩
/-- The rest of `kernel::party_target`: money moved, else the parties'
own-account movement, else the debits; neither moves nothing. -/
def moneyTarget (net own debits : Int) : PartyTarget :=
if net ≠ 0 then ⟨net, Direction.of net, net, 0⟩
else ⟨if own ≠ 0 then own else debits, .neither, 0, 0⟩
/-- `kernel::party_target`. -/
def partyTarget (capital : Option (Int × Int)) (net own debits : Int) : PartyTarget :=
match capital with
| some (e, p) => if e ≠ 0 then capitalTarget e p net else moneyTarget net own debits
| none => moneyTarget net own debits
/-- A member's entry whose cash is exactly its money: the money moved the
member's way (paid out on a distribution, received on a contribution) is
what isn't property. Not so for an expense booked straight to capital, a
debt converted, or a distribution still owed: the reversal arm, never the
lines, gives their reversals a target. -/
def cashIsMoney (e property net : Int) : Prop :=
min (max (if 0 < e then net else -net) 0) (magnitude e) =
magnitude e - min (max property 0) (magnitude e)
/-- Not vacuous: a cash contribution, a property contribution, and a
distribution of money and property each have their cash as their money. -/
example : cashIsMoney (-100) 0 (-100) ∧ cashIsMoney (-100) 100 0 ∧ cashIsMoney 100 40 60 := by
unfold cashIsMoney magnitude
decide
/-- Negating a member's entry whose cash is its money keeps what counts. -/
theorem capitalParts_neg (e p net : Int) (he : e ≠ 0) (h : cashIsMoney e p net) :
capitalParts (-e) p (-net) = capitalParts e p net := by
unfold cashIsMoney at h
unfold capitalParts
rw [magnitude_neg]
by_cases hn : e < 0
· rw [ite_eq_right (show ¬ (0 < e) by omega)] at h
rw [ite_eq_right (show ¬ (-e < 0) by omega), ite_eq_left hn, h]
congr 1
omega
· rw [ite_eq_left (show 0 < e by omega)] at h
rw [ite_eq_left (show -e < 0 by omega), ite_eq_right hn, h]
congr 1
omega
/-- **A member's capital target is odd** whenever its cash is its money. -/
theorem capitalTarget_odd (e p net : Int) (he : e ≠ 0) (h : cashIsMoney e p net) :
capitalTarget (-e) p (-net) = reverseTarget (capitalTarget e p net) := by
unfold capitalTarget reverseTarget
rw [capitalParts_neg e p net he h, withSign_neg _ _ he, withSign_neg _ _ he]
congr 1
by_cases hc : (capitalParts e p net).1 + (capitalParts e p net).2 = 0
· simp [hc, Direction.flip]
· simp [hc, Direction.of_neg]
/-- With money moved, or the parties' own accounts moved, the rest of the
target is odd too. -/
theorem moneyTarget_odd (net own debits : Int) (h : net ≠ 0 ∨ own ≠ 0) :
moneyTarget (-net) (-own) debits = reverseTarget (moneyTarget net own debits) := by
unfold moneyTarget reverseTarget
by_cases hn : net = 0
· have ho : own ≠ 0 := by omega
have : ¬ (-own = 0) := by omega
simp [hn, ho, this, Direction.flip]
· have : ¬ (-net = 0) := by omega
simp [hn, this, Direction.of_neg]
/-- **The target is odd** (rules.md B-35 item 3, B-9): negating an entry's
lines negates its capital movement, its money net and its own-account
movement, while the property beside a capital movement comes from the
other side of the lines and keeps its size; the result is the original's
`reverseTarget`, but for the debits fallback (nothing moved: a sign means
nothing there) and a member's entry whose cash isn't its money. The
engine's reversal arm, not the lines, gives every reversal its target. -/
theorem partyTarget_odd (capital : Option (Int × Int)) (net own debits : Int)
(h : (∃ e p, capital = some (e, p) ∧ e ≠ 0 ∧ cashIsMoney e p net) ∨
((∀ e p, capital = some (e, p) → e = 0) ∧ (net ≠ 0 ∨ own ≠ 0))) :
partyTarget (capital.map fun c => (-c.1, c.2)) (-net) (-own) debits =
reverseTarget (partyTarget capital net own debits) := by
cases capital with
| none =>
simp only [Option.map, partyTarget]
apply moneyTarget_odd
rcases h with ⟨_, _, hc, _⟩ | ⟨_, h⟩
· cases hc
· exact h
| some c =>
obtain ⟨e, p⟩ := c
simp only [Option.map, partyTarget]
by_cases he : e = 0
· have : ¬ (-e ≠ 0) := by omega
simp only [he, ne_eq, not_true_eq_false, ite_false, Int.neg_zero]
apply moneyTarget_odd
rcases h with ⟨e', p', hc, he', _⟩ | ⟨_, h⟩
· simp at hc; omega
· exact h
· have : -e ≠ 0 := by omega
simp only [ne_eq, he, this, not_false_eq_true, ite_true]
rcases h with ⟨e', p', hc, _, hm⟩ | ⟨hz, _⟩
· simp only [Option.some.injEq, Prod.mk.injEq] at hc
obtain ⟨rfl, rfl⟩ := hc
exact capitalTarget_odd e p net he hm
· exact absurd (hz e p rfl) he
/-- **A reversal's movement over any accounts is the original's negated**
(so the equity movement a member's target reads flips with it): summed
over a set of accounts, `movement_reversal` account by account. -/
theorem sumOver_movement_reversal (as : List Nat) (ls : List Line) :
sumOver (fun a => movement a (reversal ls)) as = -sumOver (fun a => movement a ls) as := by
have : (fun a => movement a (reversal ls)) = (fun a => -movement a ls) := by
funext a; exact movement_reversal a ls
rw [this, sumOver_neg]
end Wanderbooks