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

+48 -3
View File
@@ -26,8 +26,21 @@ use serde_json::{Value, json};
/// The newest journal format this version reads and writes.
pub const SUPPORTED_JOURNAL_FORMAT: u32 = 2;
/// Optional journal features this version reads (none yet).
pub const KNOWN_FEATURES: &[&str] = &[];
/// Optional journal features this version reads. Each is a field or value
/// on an event type an older version knows, which that version would
/// misread silently; a writer declares the feature (`journal.format_set`)
/// before its first such line, so the older version stops there instead
/// (rules.md B-27). Books that never use one stay readable by versions
/// before it.
pub const KNOWN_FEATURES: &[&str] = &[MEMBERS_CUMULATIVE];
/// Feature: `period.closed` closes to members with `"rounding":
/// "cumulative_since"` and `since_seq`, the count of members closes
/// starting at the first since the members or shares last changed (P3
/// defect 17, rules.md B-13). An older version would call the rounding
/// unknown, or (before the format gate) replay the close under the run
/// rule, which can split it differently.
pub const MEMBERS_CUMULATIVE: &str = "members_cumulative";
/// The line type that declares the journal format.
pub const FORMAT_SET: &str = "journal.format_set";
@@ -188,6 +201,12 @@ impl JournalFormat {
/// Read a `journal.format_set` line, refusing a format or feature this
/// version doesn't read.
pub(crate) fn declared(line: &JournalLine) -> Result<JournalFormat, StoreError> {
declared_knowing(line, KNOWN_FEATURES)
}
/// [`declared`] by a version that knows the features `known`: what an
/// older version, which knew fewer, does with the same line.
fn declared_knowing(line: &JournalLine, known: &[&str]) -> Result<JournalFormat, StoreError> {
let p = &line.payload;
let format = p
.get("format")
@@ -216,7 +235,7 @@ pub(crate) fn declared(line: &JournalLine) -> Result<JournalFormat, StoreError>
let unknown: Vec<&str> = features
.iter()
.map(String::as_str)
.filter(|f| !KNOWN_FEATURES.contains(f))
.filter(|f| !known.contains(f))
.collect();
if !unknown.is_empty() {
return Err(newer(
@@ -485,4 +504,30 @@ mod tests {
));
}
}
/// P3 defect 17's feature (members_cumulative): this version reads it;
/// a version before it (KNOWN_FEATURES empty, as through P2) stops at
/// its declaration with NewerJournal, never replaying a close counted
/// from its regime's start under the run rule.
#[test]
fn an_older_reader_stops_at_the_members_cumulative_feature() {
assert!(KNOWN_FEATURES.contains(&MEMBERS_CUMULATIVE));
let l = line(
FORMAT_SET,
json!({"format": 2, "features": [MEMBERS_CUMULATIVE]}),
);
assert_eq!(
declared(&l).unwrap(),
JournalFormat {
format: 2,
features: vec![MEMBERS_CUMULATIVE.to_string()]
}
);
match declared_knowing(&l, &[]) {
Err(StoreError::NewerJournal { need, .. }) => {
assert_eq!(need, "journal features members_cumulative")
}
other => panic!("expected NewerJournal, got {other:?}"),
}
}
}