P3 review: the surface, docs and proofs
check / check (push) Canceled after 0s

Split kernel::entry_flows into entry_movements and flows_of so the
reversal-mirror proof fits CI; prove runs the fast harnesses, prove-slow
the 8 slow ones (own job in scheduled CI), prove-all both. Laws in
verification.md renumbered 1-31 with a table of all 55 harnesses.

MCP docs checked against the 115 tools (about 60 fixes) with a test that
every tool is documented; rules.md cross-references fixed; bookspec
statuses and loader tightened; viewer notes wrap and accounts sort by
code; STATUS.md says P3 is built, cutover pending.

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-22 02:13:08 -06:00
1 parent 2c5b5a7171
commit 0495726001
49 files changed
+963 -292

No files matched your search

+2 -2
View File
@@ -11,7 +11,7 @@
//! — and postings go through the normal entry machinery, so every
//! accounting invariant holds.
//!
//! Two figures of accumulated depreciation, never confused (rules.md B-24):
//! Two figures of accumulated depreciation, never confused (rules.md B-15 rule 8):
//! what the schedule says is due through a date
//! ([`accumulated_scheduled_through`]) and what the books carry, the runs
//! posted and not reversed ([`crate::Ledger::accumulated_posted`]). Book
@@ -743,7 +743,7 @@ fn schedule_months(asset: &Asset, through: NaiveDate) -> Result<Vec<MonthLine>,
/// (inclusive), exact to the minor unit ([`depreciation_schedule`]): the
/// tools' `accumulated_scheduled_through`. What the books carry is
/// [`crate::Ledger::accumulated_posted`]; the two agree once every month
/// due is posted (rules.md B-24).
/// due is posted (rules.md B-15 rule 8).
pub fn accumulated_scheduled_through(
asset: &Asset,
as_of: NaiveDate,
+17
View File
@@ -680,7 +680,17 @@ pub fn apply_cash<T: Amount>(cash: T, movements: &[T], applied: &mut [T]) -> Opt
/// operating, investing, financing, each up to its own movement; what is
/// left of a section's movement is the part no cash settled. `None` when
/// the lines don't net to zero (never for a posted entry) or on overflow.
/// It is [`entry_movements`] then [`flows_of`], so each is proved on its
/// own (docs/verification.md, law 10).
pub fn entry_flows<T: Amount>(lines: &[(Flow, Side, T)]) -> Option<EntryFlows<T>> {
let (cash, moved) = entry_movements(lines)?;
flows_of(cash, moved)
}
/// An entry's net movement on its cash lines and on each section's,
/// `(cash, [operating, investing, financing])`, debit-positive. `None`
/// on overflow.
pub fn entry_movements<T: Amount>(lines: &[(Flow, Side, T)]) -> Option<(T, [T; 3])> {
let mut cash = T::ZERO;
let mut moved = [T::ZERO; 3];
for &(flow, side, amount) in lines {
@@ -693,6 +703,13 @@ pub fn entry_flows<T: Amount>(lines: &[(Flow, Side, T)]) -> Option<EntryFlows<T>
};
*slot = slot.checked_add(delta)?;
}
Some((cash, moved))
}
/// The cash flows of an entry whose cash lines moved `cash` and whose
/// sections moved `moved` ([`entry_flows`]). `None` when they don't net
/// to zero, or on overflow.
pub fn flows_of<T: Amount>(cash: T, moved: [T; 3]) -> Option<EntryFlows<T>> {
// Balanced lines only: every part below rests on it.
let mut total = cash;
for &m in &moved {
+1 -1
View File
@@ -2534,7 +2534,7 @@ impl Ledger {
/// ```text
/// GOAL: a run the books record is the entry that posted it, no more
/// and no less, so accumulated_posted is what the books carry.
/// SOURCE: rules.md B-24; B-28 (new commands are bounded; replay checks
/// SOURCE: rules.md B-15 rules 6 and 8, B-24; B-28 (new commands are bounded; replay checks
/// only what every journaled run passed:
/// record_depreciation_run).
/// ```
+1 -1
View File
@@ -85,7 +85,7 @@ pub type PayoutPlanLine = (AccountId, Side, Decimal, Option<ClassId>);
/// collected; fees debit expense accounts), and the payout reconciles to
/// the cent.
/// A zero deposit (fees and adjustments ate the sales exactly) has no
/// deposit line: every planned line is positive (B-17, docs/spec/rules.md).
/// deposit line: every planned line is positive (B-2, docs/spec/rules.md).
pub fn payout_plan(
payout: &Payout,
chart: &ChartOfAccounts,
+67 -1
View File
@@ -1031,12 +1031,17 @@ fn entry_flows_are_bounded() {
/// A reversal's cash flows are its original's, negated, part by part:
/// what lets an entry and its reversal in one period drop out of the
/// statement together (rules.md B-14 rule 6).
/// statement together (rules.md B-14 rule 6). End to end, on entries of
/// up to two lines: at three (`NF`) one run went past half an hour at
/// four jobs on a busy machine (2026-10-21), so three lines are proved in
/// two halves, `entry_movements_of_a_reversal_mirror` and
/// `flows_of_mirrors`, and Lean `flows_reversal` proves it at any size.
#[kani::proof]
#[kani::unwind(5)]
fn entry_flows_of_a_reversal_mirror() {
let all = any_flow_lines();
let n = any_flow_len();
kani::assume(n <= 2);
let lines = &all[..n];
// Element by element (see `kani_symbolic_copy_canary`).
let mut rev = [(Flow::Cash, Side::Debit, 0i64); NF];
@@ -1046,6 +1051,67 @@ fn entry_flows_of_a_reversal_mirror() {
let a = kernel::entry_flows(lines);
let b = kernel::entry_flows(&rev[..n]);
assert_eq!(a.is_some(), b.is_some());
if let (Some(a), Some(b)) = (a, b) {
assert_eq!(b.cash, -a.cash);
for k in 0..3 {
assert_eq!(b.flows[k], -a.flows[k]);
assert_eq!(b.noncash[k], -a.noncash[k]);
}
kani::cover!(a.flows[1] != 0, "cash settling investing, reversed");
}
}
/// The two halves of `entry_flows_of_a_reversal_mirror`, each for every
/// input in its bounds: `kernel::entry_flows` is `entry_movements` then
/// `flows_of`, so a reversal mirrors when its movements are its
/// original's negated (this harness, up to `NF` lines each in any of the
/// four places) and `flows_of` is odd (the next, for any movements three
/// lines can make). Together they prove the end-to-end harness's claim,
/// whose single run past half an hour at four jobs on a busy machine
/// (2026-10-21) moved it to `just prove-slow`.
#[kani::proof]
#[kani::unwind(5)]
fn entry_movements_of_a_reversal_mirror() {
let all = any_flow_lines();
let n = any_flow_len();
let lines = &all[..n];
// Element by element (see `kani_symbolic_copy_canary`).
let mut rev = [(Flow::Cash, Side::Debit, 0i64); NF];
for i in 0..n {
rev[i] = kernel::reverse_line(lines[i]);
}
let a = kernel::entry_movements(lines);
let b = kernel::entry_movements(&rev[..n]);
assert_eq!(a.is_some(), b.is_some());
if let (Some((ca, ma)), Some((cb, mb))) = (a, b) {
assert_eq!(cb, -ca);
for k in 0..3 {
assert_eq!(mb[k], -ma[k]);
}
kani::cover!(
n == NF && ca != 0 && ma[1] != 0,
"three lines, cash and investing"
);
}
}
/// `kernel::flows_of` is odd: the negated movements give the negated
/// flows, part by part, and succeed exactly when the original's do; for
/// any cash and section movements up to `NF` lines' worth.
#[kani::proof]
#[kani::unwind(5)]
fn flows_of_mirrors() {
let wide_amount = || {
let a: i64 = kani::any();
// What up to NF in-range lines can sum to (`in_line_range`: 2^40).
kani::assume(a.unsigned_abs() <= (NF as u64) << 40);
a
};
let cash = wide_amount();
let moved: [i64; 3] = core::array::from_fn(|_| wide_amount());
let a = kernel::flows_of(cash, moved);
let b = kernel::flows_of(-cash, [-moved[0], -moved[1], -moved[2]]);
assert_eq!(a.is_some(), b.is_some());
if let (Some(a), Some(b)) = (a, b) {
assert_eq!(b.cash, -a.cash);
for k in 0..3 {