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:
1 parent
4eaac20311
commit
4647611f96
7 files changed
+362
-12
No files matched your search
@@ -2374,6 +2374,10 @@ impl Store {
|
||||
}
|
||||
|
||||
/// Rebuild a ledger purely from the SQLite projection (used by verify).
|
||||
/// Rebuild the ledger from the SQLite projection, for the cross-check.
|
||||
/// Like journal replay, it reads history, so it uses the engine's
|
||||
/// `replay_*` commands: rules added later never refuse books written
|
||||
/// before them.
|
||||
fn load_ledger_from_sqlite(&self) -> Result<Ledger, StoreError> {
|
||||
let mut ledger = Ledger::new(self.base_currency);
|
||||
let mut stmt = self.conn.prepare(
|
||||
@@ -2400,7 +2404,7 @@ impl Store {
|
||||
"account {code} currency {currency} != base"
|
||||
)));
|
||||
}
|
||||
let got = ledger.add_account(
|
||||
let got = ledger.replay_add_account(
|
||||
&code,
|
||||
&name,
|
||||
account_type,
|
||||
@@ -2477,7 +2481,7 @@ impl Store {
|
||||
})?;
|
||||
for (idx, lr) in lrows.enumerate() {
|
||||
let (account, side, amount, currency, class_id) = lr?;
|
||||
ledger.add_line(
|
||||
ledger.replay_add_line(
|
||||
got,
|
||||
account as u64,
|
||||
parse_side(&side)?,
|
||||
@@ -2883,7 +2887,7 @@ pub(crate) fn replay_line_into_ledger(
|
||||
let want = get_u64(p, "id")?;
|
||||
let account_type: AccountType = serde_json::from_value(p["account_type"].clone())?;
|
||||
let parent = p.get("parent").and_then(|v| v.as_u64());
|
||||
let got = ledger.add_account(
|
||||
let got = ledger.replay_add_account(
|
||||
get_str(p, "code")?,
|
||||
get_str(p, "name")?,
|
||||
account_type,
|
||||
@@ -2928,7 +2932,7 @@ pub(crate) fn replay_line_into_ledger(
|
||||
let fx_rate: Decimal = get_str(p, "fx_rate")?
|
||||
.parse()
|
||||
.map_err(|_| StoreError::JournalCorrupt("bad fx rate".into()))?;
|
||||
ledger.add_line_fx(
|
||||
ledger.replay_add_line_fx(
|
||||
id,
|
||||
account,
|
||||
side,
|
||||
@@ -2940,7 +2944,7 @@ pub(crate) fn replay_line_into_ledger(
|
||||
)?;
|
||||
}
|
||||
None => {
|
||||
ledger.add_line(id, account, side, Money::new(amount, currency))?;
|
||||
ledger.replay_add_line(id, account, side, Money::new(amount, currency))?;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2957,7 +2961,7 @@ pub(crate) fn replay_line_into_ledger(
|
||||
for l in p.get("lines").and_then(|v| v.as_array()).ok_or_else(|| {
|
||||
StoreError::JournalCorrupt("entry.posted without lines".into())
|
||||
})? {
|
||||
ledger.add_line(
|
||||
ledger.replay_add_line(
|
||||
id,
|
||||
get_u64(l, "account")?,
|
||||
parse_side(get_str(l, "side")?)?,
|
||||
@@ -3132,7 +3136,7 @@ pub(crate) fn replay_line_into_ledger(
|
||||
}
|
||||
"period.created" => {
|
||||
let want = get_u64(p, "id")?;
|
||||
let got = ledger.create_period(
|
||||
let got = ledger.replay_create_period(
|
||||
get_str(p, "name")?,
|
||||
parse_date(get_str(p, "start")?)?,
|
||||
parse_date(get_str(p, "end")?)?,
|
||||
|
||||
Reference in new issue
Block a user