Fixed assets carry federal tax basis beside book value; tests sweep stale temp dirs

- 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 6adc7b34c7
commit ba7373bdb8
30 files changed
+3240 -93

No files matched your search

+44
View File
@@ -53,6 +53,45 @@ fn attachment_name(b: &Books, id: u64) -> String {
.unwrap_or_else(|| format!("attachment {id}"))
}
/// "Tax basis of FL-0001: personal-use conversion, contributor's basis
/// $1,917.21 → tax basis $1,650.00". The tax basis is ledger-core's
/// (`Asset::tax_basis`) for the event's rule and basis on the asset's
/// book cost, which never changes once registered.
fn tax_basis_text(b: &Books, asset_id: Option<u64>, p: &Value) -> String {
let asset = asset_id.and_then(|id| b.ledger.get_asset(id));
let name = match (asset, asset_id) {
(Some(a), _) => a.tag.clone().unwrap_or_else(|| a.description.clone()),
(None, Some(id)) => format!("asset {id}"),
(None, None) => "an asset".to_string(),
};
let rule = ledger_core::TaxBasisRule::parse(s(p, "rule"));
let contributor: Option<rust_decimal::Decimal> = p
.get("contributor_basis")
.and_then(|v| v.as_str())
.and_then(|v| v.parse().ok());
let Some(rule) = rule else {
return format!("Tax basis of {name} recorded");
};
let tax = asset.and_then(|a| {
let mut a = a.clone();
a.tax = Some(ledger_core::TaxBasis {
rule,
contributor_basis: contributor,
});
a.tax_basis()
});
let mut text = format!("Tax basis of {name}: {}", super::assets::rule_words(rule));
if let Some(c) = contributor {
text.push_str(&format!(", contributor's basis {}", fmt::dollars(c)));
}
match (tax, contributor.is_some()) {
(Some(t), true) => text.push_str(&format!(" → tax basis {}", fmt::dollars(t))),
(Some(t), false) => text.push_str(&format!(", tax basis {}", fmt::dollars(t))),
(None, _) => {}
}
text
}
/// A sentence for a meaningful event; `None` for the intermediate steps of
/// building an entry (created, line added), which the posted event covers.
pub fn describe(b: &Books, line: &JournalLine) -> Option<Event> {
@@ -267,6 +306,11 @@ pub fn describe(b: &Books, line: &JournalLine) -> Option<Event> {
};
ev(icon::BARCODE, text, asset_id.map(Route::Asset))
}
"asset.tax_basis_set" => ev(
icon::SCALES,
tax_basis_text(b, asset_id, p),
asset_id.map(Route::Asset),
),
"asset.described" => ev(
icon::PENCIL_SIMPLE,
format!(