Fixed assets carry federal tax basis beside book value; tests sweep stale temp dirs
check / check (push) Canceled after 0s

- core: TaxBasisRule (cost IRC 1012, carryover IRC 723, personal-use conversion Treas. Reg. 1.168(i)-4(b)), TaxBasis with the contributor's adjusted basis, Asset::tax_basis and ::built_in_gain (IRC 704(c)); the register gains tax rule, contributor basis, tax basis, built-in gain, total tax basis, missing count (rules.md B-25; sources vendored in docs/spec/sources/irs-partnership-basis.md)
- store: asset.tax_basis_set journaled, replayed, projected (schema v10)
- mcp: books.set_asset_tax_basis; register_asset takes tax_basis_rule and contributor_basis, checked before anything registers (found by the spec-blind suite); list_assets and the register export (json, csv) carry tax basis; instructions say to record it for every asset
- import: the register's XLSX and PDF gain the federal tax basis table with citations
- viewer: tax basis on the Assets page, the asset page (a Tax basis card), the report, the journal, and Needs attention for assets without one
- just: check and the mutation recipes sweep this user's test book dirs older than an hour (killed runs had left 23 GB in RAM-backed /tmp)
- docs: the parties, attachments, TIN vault, completeness engine, and updates designs (docs/parties-and-attachments.md)
- tests: spec-blind tax basis suites in core, store, MCP; register PDF/XLSX and viewer tests

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-15 13:30:09 -06:00
1 parent 49c49aef33
commit 6e6f9d65b2
30 files changed
+3240 -93

No files matched your search

+8 -1
View File
@@ -5,7 +5,7 @@
//! is the current version.
/// Current schema version.
pub const SCHEMA_VERSION: u32 = 9;
pub const SCHEMA_VERSION: u32 = 10;
/// Migrations, in order. `MIGRATIONS[i]` migrates from version `i` to `i+1`.
pub const MIGRATIONS: &[&str] = &[
@@ -220,5 +220,12 @@ CREATE TABLE IF NOT EXISTS asset_tags_log(
asset_id INTEGER NOT NULL,
tag TEXT NOT NULL
);
"#,
r#"
-- v10: each asset's federal tax basis: the rule (cost, carryover,
-- personal_use_conversion) and the contributing member's adjusted basis
-- (TEXT, exact decimal, never REAL).
ALTER TABLE assets ADD COLUMN tax_rule TEXT;
ALTER TABLE assets ADD COLUMN contributor_basis TEXT;
"#,
];
+70
View File
@@ -725,6 +725,27 @@ impl Store {
Ok(())
}
/// Record an asset's federal tax basis (see
/// `Ledger::set_asset_tax_basis`). Journaled as `asset.tax_basis_set`.
pub fn set_asset_tax_basis(
&mut self,
id: AssetId,
tax: ledger_core::TaxBasis,
) -> Result<(), StoreError> {
self.ledger.check_asset_tax_basis(id, tax)?;
let line = self.append_journal(
"asset.tax_basis_set",
json!({
"asset_id": id,
"rule": tax.rule.key(),
"contributor_basis": tax.contributor_basis.map(|b| b.to_string()),
}),
)?;
self.ledger.set_asset_tax_basis(id, tax)?;
apply_line_to_sqlite(&self.conn, &line)?;
Ok(())
}
/// Change an asset's description (see `Ledger::set_asset_description`).
/// Journaled as `asset.described`, with the description it had.
pub fn set_asset_description(
@@ -2897,6 +2918,21 @@ impl Store {
let (id, serial) = row?;
ledger.set_asset_serial(id as u64, Some(&serial))?;
}
let mut xstmt = self.conn.prepare(
"SELECT id, tax_rule, contributor_basis FROM assets WHERE tax_rule IS NOT NULL ORDER BY id",
)?;
let taxes = xstmt.query_map([], |r| {
Ok((
r.get::<_, i64>(0)?,
r.get::<_, String>(1)?,
r.get::<_, Option<String>>(2)?,
))
})?;
for row in taxes {
let (id, rule, basis) = row?;
let tax = tax_basis_from_journal(&json!({"rule": rule, "contributor_basis": basis}))?;
ledger.set_asset_tax_basis(id as u64, tax)?;
}
let mut qstmt = self.conn.prepare(
"SELECT id, acquisition_entry FROM assets WHERE acquisition_entry IS NOT NULL ORDER BY id",
)?;
@@ -3452,6 +3488,10 @@ pub(crate) fn replay_line_into_ledger(
"asset.tagged" => {
ledger.set_asset_tag(get_u64(p, "asset_id")?, get_str(p, "tag")?)?;
}
"asset.tax_basis_set" => {
let tax = tax_basis_from_journal(p)?;
ledger.set_asset_tax_basis(get_u64(p, "asset_id")?, tax)?;
}
"asset.described" => {
ledger.set_asset_description(get_u64(p, "asset_id")?, get_str(p, "description")?)?;
}
@@ -3680,6 +3720,26 @@ fn apply_document_payment(
/// The close target recorded in a `period.closed` line. Absent or
/// `"retained_earnings"`: Retained Earnings with no sweeps (every journal
/// written before targets existed).
/// An `asset.tax_basis_set` payload's rule and contributor basis, strictly:
/// an unknown rule or a basis that isn't a decimal string is corrupt.
fn tax_basis_from_journal(p: &Value) -> Result<ledger_core::TaxBasis, StoreError> {
let corrupt = |what: &str| StoreError::JournalCorrupt(format!("asset.tax_basis_set {what}"));
let rule =
ledger_core::TaxBasisRule::parse(get_str(p, "rule")?).ok_or_else(|| corrupt("rule"))?;
let contributor_basis = match p.get("contributor_basis") {
None | Some(Value::Null) => None,
Some(v) => Some(
v.as_str()
.and_then(|s| s.parse::<Decimal>().ok())
.ok_or_else(|| corrupt("contributor_basis"))?,
),
};
Ok(ledger_core::TaxBasis {
rule,
contributor_basis,
})
}
fn close_to_from_journal(v: Option<&Value>) -> Result<CloseTo, StoreError> {
let corrupt = |what: &str| StoreError::JournalCorrupt(format!("close_to: {what}"));
let ids = |v: &Value| -> Result<Vec<AccountId>, StoreError> {
@@ -3929,6 +3989,16 @@ fn apply_line_rows(conn: &Connection, line: &JournalLine) -> Result<(), StoreErr
params![id, tag],
)?;
}
"asset.tax_basis_set" => {
conn.execute(
"UPDATE assets SET tax_rule = ?, contributor_basis = ? WHERE id = ?",
params![
get_str(p, "rule")?,
p.get("contributor_basis").and_then(|v| v.as_str()),
get_u64(p, "asset_id")? as i64
],
)?;
}
"asset.described" => {
conn.execute(
"UPDATE assets SET description = ? WHERE id = ?",