P3 review: P2 can read the books until the owner consents
check / check (push) Canceled after 0s

A schedule-convention setting, the two new tax categories, a disposal
before the in-service date and a depreciation catch-up each need their
feature now (tax_categories_gain_exempt, disposal_before_service,
depreciation_catch_up), so no write before consent leaves books P2 refuses
or misreads; replayed names are read with P2's TIN detector (B-28). The
cutover runs end to end through the real P2 and P3 binaries in a test.

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-21 17:43:45 -06:00
1 parent 6288ead747
commit 2c5b5a7171
78 files changed
+1375 -4

No files matched your search

+34
View File
@@ -1137,6 +1137,11 @@ impl Store {
})?;
self.ledger
.check_tax_category_required(code, name, account_type, placeholder, category)?;
// A category an earlier version doesn't know needs its feature:
// asked before the account's first line, never between the two.
if let Some(f) = category.and_then(crate::format::tax_category_feature) {
self.ensure_feature(f)?;
}
let id =
self.journal_account_added(code, name, account_type, parent, placeholder, |l| {
l.add_account(code, name, account_type, parent, placeholder)
@@ -2045,6 +2050,11 @@ impl Store {
if self.depreciation_period_was_run(asset_id, period) {
self.ensure_feature(crate::format::MONTH_REPOSTING)?;
}
// More or less than the month's own (another month caught up in
// it): a run an earlier version would read as the month's own.
if amount != due.scheduled {
self.ensure_feature(crate::format::DEPRECIATION_CATCH_UP)?;
}
let entry_id = self.create_entry(period_end, &description)?;
self.add_line(entry_id, asset.expense_account, Side::Debit, money)?;
self.add_line(entry_id, asset.accum_depr_account, Side::Credit, money)?;
@@ -2517,6 +2527,13 @@ impl Store {
if depreciation.is_some() && self.depreciation_period_was_run(asset_id, &preview.period) {
self.ensure_feature(crate::format::MONTH_REPOSTING)?;
}
// Disposed before it was placed in service (acquired, not yet in
// use): an earlier version refuses that line (its floor was the
// in-service date), so it needs its feature, asked here, before
// the first line.
if date < asset.in_service_date {
self.ensure_feature(crate::format::DISPOSAL_BEFORE_SERVICE)?;
}
if let Some((description, lines)) = &depreciation {
let eid = self.create_entry(date, description)?;
for (account, side, amount) in lines {
@@ -3806,6 +3823,9 @@ impl Store {
) -> Result<(), StoreError> {
self.ledger
.check_account_tax(account, category, deductible_percent)?;
if let Some(f) = category.and_then(crate::format::tax_category_feature) {
self.ensure_feature(f)?;
}
let line = self.append_journal(
"account.tax_set",
json!({
@@ -5484,10 +5504,24 @@ fn get_str<'a>(p: &'a Value, key: &str) -> Result<&'a str, StoreError> {
/// Replay one journal line into a ledger, asserting recorded ids. Used by
/// hydration, `verify`, and `rebuild`: the journal is the script, the engine
/// is the actor.
///
/// A full TIN is found as the version before P3 found one
/// (`ledger_core::parties::replaying`): a name, reason or fact an earlier
/// version took replays, however the commands' detector has widened since
/// (rules.md B-28).
pub(crate) fn replay_line_into_ledger(
ledger: &mut Ledger,
line: &JournalLine,
base_currency: Currency,
) -> Result<(), StoreError> {
ledger_core::parties::replaying(|| replay_line(ledger, line, base_currency))
}
/// [`replay_line_into_ledger`]'s work.
fn replay_line(
ledger: &mut Ledger,
line: &JournalLine,
base_currency: Currency,
) -> Result<(), StoreError> {
let p = &line.payload;
match line.line_type.as_str() {