P3 review: asset bases and the tax-basis K-1

Schema 13 is as shipped and the asset columns are schema 14 (shipped
migrations pinned by hash); the tax-basis K-1 leaves net income open (only
the return has it) and takes a contributed prepaid plan at its book amount
as its section 723 basis; a disposal of contributed property names its
704(c) gain or loss; aliases compare by meaning; fixture amounts made up.

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:44 -06:00
1 parent 2ae93fb2d6
commit 6288ead747
24 files changed
+860 -127

No files matched your search

+78 -3
View File
@@ -18,7 +18,7 @@
use chrono::NaiveDate;
use ledger_core::{AccountType, AssetId, Currency, LedgerError, Money, NewAsset};
use ledger_store::journal::read_and_verify_journal;
use ledger_store::{JOURNAL_FILE, JournalWriter, Store, StoreError, db_file};
use ledger_store::{JOURNAL_FILE, JournalWriter, Store, StoreError, db_file, db_file_for};
use serde_json::{Value, json};
use std::path::{Path, PathBuf};
@@ -887,6 +887,49 @@ fn downgrade_db_to_v12(db: &Path) {
.unwrap();
}
/// A database at v13 exactly as it shipped on main before the assets'
/// basis columns (P3 review, 2026-10-21: those columns were first written
/// into v13 itself, so a v13 file built by the earlier commits opened as
/// current and failed to load, "no such column"): its file is another
/// schema's, left byte for byte, and this version builds its own from the
/// journal, with every run, and verifies.
#[test]
fn a_v13_file_built_before_the_basis_columns_is_left_and_rebuilt() {
use ledger_core::BasisRule::*;
let (dir, mut s, _, x, y) = fixture("v13-shipped");
s.run_depreciation(x, "2026-01").unwrap();
s.set_asset_basis(y, contribution(Carryover723, "640.00", "1200.00"))
.unwrap();
drop(s);
let v13 = dir.join(db_file_for(13));
std::fs::rename(dir.join(db_file()), &v13).unwrap();
let conn = rusqlite::Connection::open(&v13).unwrap();
conn.execute_batch(
"ALTER TABLE assets DROP COLUMN fmv_at_contribution;
ALTER TABLE assets DROP COLUMN sec704c_method;
ALTER TABLE assets DROP COLUMN basis_source_json;
CREATE TABLE asset_depreciation_v12(asset_id INTEGER NOT NULL, period TEXT NOT NULL,
entry_id INTEGER NOT NULL, amount TEXT NOT NULL, PRIMARY KEY (asset_id, period));
INSERT INTO asset_depreciation_v12 SELECT asset_id, period, entry_id, amount FROM asset_depreciation;
DROP TABLE asset_depreciation;
ALTER TABLE asset_depreciation_v12 RENAME TO asset_depreciation;
UPDATE meta SET value = '13' WHERE key = 'schema_version';",
)
.unwrap();
drop(conn);
let shipped = std::fs::read(&v13).unwrap();
let s = Store::open(&dir).expect("books with a shipped v13 file open");
assert_eq!(s.database_file(), db_file());
s.verify().unwrap();
assert_eq!(s.ledger().depreciation_runs(x).len(), 1);
assert_eq!(
basis_of(&s, y),
Some(contribution(Carryover723, "640.00", "1200.00"))
);
drop(s);
assert_eq!(std::fs::read(&v13).unwrap(), shipped, "left byte for byte");
}
/// Downgrade the database to schema v10 (none of v11's attachment links,
/// statements, reconciliations, or payroll_lines.employee_id).
fn downgrade_db_to_v10(dir: &Path) {
@@ -909,12 +952,12 @@ fn downgrade_db_to_v10(dir: &Path) {
}
/// P2's database (`wanderbooks.db`, schema v12) is left as it is, byte
/// for byte: this version builds its own v13 file from the journal, its
/// for byte: this version builds its own file from the journal, its
/// runs table keyed by entry with every run kept and the new columns
/// empty, and it verifies; a basis set after then reloads from it (each
/// schema in a file of its own: rules.md B-27).
#[test]
fn a_p2_database_is_left_and_v13_built_from_the_journal() {
fn a_p2_database_is_left_and_this_versions_built_from_the_journal() {
use ledger_core::BasisRule::*;
let (dir, mut s, _, x, y) = fixture("v12");
s.run_depreciation(x, "2026-01").unwrap();
@@ -1213,3 +1256,35 @@ fn the_viewer_and_writer_agree_on_a_bad_depreciation_posted_payload() {
}
}
}
/// B-27: books an older version wrote (format 1, declaring nothing) refuse
/// a basis while they wait for the owner's format upgrade, a purchase's
/// too, journaling nothing; the feature is never declared on the way (P3
/// review, 2026-10-21).
#[test]
fn a_basis_in_books_pending_their_upgrade_is_refused_and_journals_nothing() {
use ledger_core::BasisRule::*;
let dir = test_dir("basis-pending");
let mut s = Store::create_in_format(
&dir,
Currency::USD,
&ledger_store::format::JournalFormat::default(),
)
.unwrap();
let a = accounts(&mut s);
let x = laptop(&mut s, &a, "Laptop A");
let was = std::fs::read(dir.join(JOURNAL_FILE)).unwrap();
for b in [
ledger_core::AssetBasis::purchase(),
contribution(Carryover723, "640.00", "1200.00"),
] {
match s.set_asset_basis(x, b) {
Err(StoreError::FormatUpgradePending { need }) => {
assert!(need.contains(ledger_store::format::ASSET_BASIS), "{need:?}")
}
other => panic!("expected FormatUpgradePending, got {other:?}"),
}
}
assert_eq!(std::fs::read(dir.join(JOURNAL_FILE)).unwrap(), was);
assert_eq!(basis_of(&s, x), None);
}
+33
View File
@@ -369,3 +369,36 @@ fn new_books_get_their_schemas_own_file() {
assert_eq!(ledger_store::is_database_file(name), is, "{name}");
}
}
/// A shipped migration's text never changes (P3 review, 2026-10-21: the
/// assets' v13 columns were first added to the v13 already on main, so a
/// v13 file built before them would open without them). Each schema's
/// file is built once by these scripts; a change is a new migration. The
/// SHA-256 of each shipped one, v1 to v13 (append the next as it ships).
#[test]
fn a_shipped_migration_never_changes() {
use sha2::{Digest, Sha256};
const SHIPPED: [&str; 13] = [
"8b253dded247d8518a7454b1c894a147ac211ee32ea2671dca2c586c5184c046",
"9fc19f844ed33b8f08eee8437ee3eb038e854737d5febe79ce3ac80abeb18614",
"215af9a7a54cda382f721488fedb34f68eb36399730de1b23371d44eb16fd79d",
"7923eb31ab8a08f20b8d7df017814095861478cb796af14994dbb85dbe558c20",
"95caa91ee3327b5590ad1ddefa76bad189950091cfcd140121f978e0af258e8a",
"74181a819beb4349808b9a095fbcda0073890fa6f54bc3fa5535c5a6644e35c6",
"df008d2a7cf832173908d1e33f795b88d272eae9cd23a5ebebe7207871d7076f",
"d0cd600ef4aa7bc2301e682bace23f3ce311b5f991e5d7bff447a9520ca8e89f",
"b2e54e8cf47b7f55f42f3fd4876fce5183e229aea08059518a50cbbffdca2db2",
"8881527992c586f69644276db921e640fb6548ee1d2f7c313a46c53e1e63430e",
"cc18b31e929b5889c0d09190b3211ccefd9d73df8e1e05c93f9e460097e1553a",
"dd2a93d760350f2edd9462f4a52888c0645040582739d3a93b17a3195238d501",
"9aa8a0ea7ea15315a4e38281e3a6d4e14beb26333991ebb2824d9c8cc5a51941",
];
for (i, want) in SHIPPED.iter().enumerate() {
let got: String = Sha256::digest(MIGRATIONS[i].as_bytes())
.iter()
.map(|b| format!("{b:02x}"))
.collect();
assert_eq!(&got, want, "migration v{} changed after it shipped", i + 1);
}
assert_eq!(MIGRATIONS.len() as u32, SCHEMA_VERSION);
}