fix(core,store): child accounts share their parent's type; periods nest; lines checked when added

Three spec decisions from the owner (2026-10-13), each test-first:

- A child account has its parent's type (ParentTypeMismatch): a child
  subdivides its parent. Test: a_child_account_has_its_parents_type.
- Periods nest (a month in a quarter in a year) but never partially
  overlap (PeriodsOverlap; a refusal uses no id). Test:
  periods_nest_but_never_partially_overlap.
- A line's account must exist and take postings when the line is added,
  not only at post (UnknownAccount / PlaceholderAccount from add_line
  and add_line_fx); posting still re-checks. Two older tests that pinned
  the post-time refusal now check both (the post-time check is reached
  through the unchecked replay path, as old journals can).

Compatibility: replay reads history through new Ledger::replay_*
commands (add_account, add_line, add_line_fx, create_period) that skip
checks added later, so books written before these rules still open.
The contract test books_written_under_older_rules_still_open writes such
a journal directly and found, on its first run, that the SQLite
cross-check rebuild used the checked commands and would have failed
verification ("sqlite projection can't be read back"); it now replays
too. Documented in docs/spec/README.md; decisions recorded in rules.md
(B-1, B-4, B-12).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GWVo7x3UHw5kwCUaWxiejX
This commit is contained in:
swaitsandClaude Opus 5.5 committed 2026-10-13 14:16:51 -06:00
1 parent 4eaac20311
commit 4647611f96
7 files changed
+362 -12

No files matched your search

+52
View File
@@ -1000,3 +1000,55 @@ mod refusals {
}
}
}
/// Contract (2026-10-13): rules added to the engine never lock anyone out
/// of books written before them. A journal holding what the new rules
/// refuse (a child account of another type, partially overlapping periods,
/// a draft line on a missing account) must still open and verify; replay
/// goes through Ledger::replay_*.
#[test]
fn books_written_under_older_rules_still_open() {
use serde_json::json;
let dir = test_dir("older-rules");
let mut store = Store::create(&dir, Currency::USD).unwrap();
let cash = store
.add_account("1000", "Cash", AccountType::Asset, None, true)
.unwrap();
drop(store);
// What an older wanderbooks could have written.
let mut w = JournalWriter::open(&dir.join(ledger_store::JOURNAL_FILE)).unwrap();
w.append(
"account.added",
json!({"id": 2, "code": "1010", "name": "Rent", "account_type": "Expense",
"parent": cash, "placeholder": false, "currency": "USD"}),
)
.unwrap();
w.append(
"period.created",
json!({"id": 1, "name": "2026-01", "start": "2026-01-01", "end": "2026-01-31"}),
)
.unwrap();
w.append(
"period.created",
json!({"id": 2, "name": "straddle", "start": "2026-01-15", "end": "2026-02-15"}),
)
.unwrap();
w.append(
"entry.created",
json!({"id": 1, "date": "2026-03-01", "description": "draft"}),
)
.unwrap();
w.append(
"entry.line_added",
json!({"id": 1, "account": 999, "side": "debit", "amount": "10.00", "currency": "USD"}),
)
.unwrap();
drop(w);
// Opening rebuilds from the journal; it must accept all of it.
let _ = std::fs::remove_file(dir.join(ledger_store::DB_FILE));
let store = Store::open(&dir).expect("older books still open");
store.verify().unwrap();
assert_eq!(store.ledger().chart().len(), 2);
assert_eq!(store.ledger().list_periods().len(), 2);
assert_eq!(store.ledger().get_entry(1).unwrap().lines().len(), 1);
}