pub struct Session { /* private fields */ }Implementations§
Source§impl Session
impl Session
Sourcepub fn create(vault_path: &Path, pp: &Passphrase) -> Result<Session, CliError>
pub fn create(vault_path: &Path, pp: &Passphrase) -> Result<Session, CliError>
Create a brand-new encrypted vault, then initialize the core event schema and the CLI config
table, and persist. (Vault::create already saved once; we re-save after the DDL.)
Sourcepub fn repair(vault_path: &Path, pp: &Passphrase) -> Result<Session, CliError>
pub fn repair(vault_path: &Path, pp: &Passphrase) -> Result<Session, CliError>
Like create, but first clears a half-created vault (orphan key, no pgp/bak) under
explicit --repair consent. Delegates to Vault::repair which refuses if a real or
recoverable vault is present (see Vault::repair safety invariant).
Sourcepub fn open(vault_path: &Path, pp: &Passphrase) -> Result<Session, CliError>
pub fn open(vault_path: &Path, pp: &Passphrase) -> Result<Session, CliError>
Open an existing vault (acquires the store single-instance lock; NFR7).
Sourcepub fn conn(&self) -> &Connection
pub fn conn(&self) -> &Connection
Borrow the live in-memory SQLite handle (core appenders use interior mutability over &Connection).
Sourcepub fn save(&mut self) -> Result<(), CliError>
pub fn save(&mut self) -> Result<(), CliError>
Persist the current DB image (encrypted, atomic; NFR2/NFR3).
Sourcepub fn snapshot(&self) -> Result<Vec<u8>, CliError>
pub fn snapshot(&self) -> Result<Vec<u8>, CliError>
Snapshot the in-memory DB image (no disk I/O) for a possible restore() after a failed save.
Sourcepub fn restore(&mut self, image: &[u8]) -> Result<(), CliError>
pub fn restore(&mut self, image: &[u8]) -> Result<(), CliError>
Restore the in-memory DB from a prior snapshot() (no disk I/O). On Err, the in-memory DB
is UNCHANGED and unsaved residue may still be live — the caller MUST latch, never swallow.
Sourcepub fn vault(&self) -> &Vault
pub fn vault(&self) -> &Vault
Borrow the vault for store-level operations (export_snapshot / backup_key).
Sourcepub fn config(&self) -> Result<CliConfig, CliError>
pub fn config(&self) -> Result<CliConfig, CliError>
The persisted projection config (TP8 treatment + lot method); default = (c)+FIFO if unset.
Sourcepub fn tax_profile(&self, year: i32) -> Result<Option<TaxProfile>, CliError>
pub fn tax_profile(&self, year: i32) -> Result<Option<TaxProfile>, CliError>
The stored per-year TaxProfile for year, or None if none has been set.
Robust to older vaults (calls tax_profile::init_table as a defensive guard).
Sourcepub fn all_tax_profiles(&self) -> Result<BTreeMap<i32, TaxProfile>, CliError>
pub fn all_tax_profiles(&self) -> Result<BTreeMap<i32, TaxProfile>, CliError>
All stored TaxProfiles, sorted by year ascending.
Sourcepub fn optimize_attested_set(&self) -> Result<BTreeSet<EventId>, CliError>
pub fn optimize_attested_set(&self) -> Result<BTreeSet<EventId>, CliError>
All attested disposal EventIds (NFR4-stable BTreeSet; feeds compliance_overlay).
Robust to older vaults (defensive init_table guard inside attested_set).
Sourcepub fn donation_details(
&self,
) -> Result<BTreeMap<EventId, DonationDetails>, CliError>
pub fn donation_details( &self, ) -> Result<BTreeMap<EventId, DonationDetails>, CliError>
All stored DonationDetails, keyed by donation EventId (NFR4-stable BTreeMap).
Robust to older vaults (defensive init_table guard inside donation_details::all).
Sourcepub fn bulk_estimated(&self) -> Result<BTreeMap<EventId, String>, CliError>
pub fn bulk_estimated(&self) -> Result<BTreeMap<EventId, String>, CliError>
All disposals flagged as estimated-FMV proceeds by the bulk-reclassify-outflow path, keyed by
the transfer_out_event (== Disposal.event); value = the date_marked provenance stamp
(NFR4-stable BTreeMap). Robust to older vaults (defensive init_table guard inside
bulk_estimated::all). build_snapshot loads the [est] marker set via THIS accessor,
NEVER conn() directly [R0-M1].
Sourcepub fn project(&self) -> Result<(LedgerState, ProjectionConfig), CliError>
pub fn project(&self) -> Result<(LedgerState, ProjectionConfig), CliError>
Load all events and run the pure deterministic projection (NFR4) over the bundled daily-close
dataset (§9.2). Returns the resolved ProjectionConfig too (so verify can display it).
Sourcepub fn load_events_and_project(
&self,
) -> Result<(Vec<LedgerEvent>, LedgerState, ProjectionConfig), CliError>
pub fn load_events_and_project( &self, ) -> Result<(Vec<LedgerEvent>, LedgerState, ProjectionConfig), CliError>
Single-load variant: loads events ONCE and returns them alongside the projection. Callers
that need both the raw event log and the projected state (e.g. verify, safe_harbor_attest)
use this to avoid the double load_all call that the project() + separate load_all()
pattern incurs.
Sourcepub fn exchange_method_election_rows(
&self,
date: TaxDate,
) -> Result<Vec<(WalletId, LotMethod, bool)>, CliError>
pub fn exchange_method_election_rows( &self, date: TaxDate, ) -> Result<Vec<(WalletId, LotMethod, bool)>, CliError>
§A.5(a): the distinct Exchange accounts in the vault, each with its currently-in-force
cost-basis method and whether that method is an explicit per-account election (true) vs
inherited from a global election / FIFO default (false), as of date. Feeds the
btctax-tui-edit method-election flow’s account list. Uses the SHARED resolver via
btctax_core::in_force_methods (the sole precedence path). Sorted by WalletId: Ord.
Sourcepub fn optimize_proposal(
&self,
year: i32,
now: OffsetDateTime,
) -> Result<OptimizeProposal, CliError>
pub fn optimize_proposal( &self, year: i32, now: OffsetDateTime, ) -> Result<OptimizeProposal, CliError>
Recompute the Mode-1 optimizer proposal for year on the HELD session. READ-ONLY: appends and
persists NOTHING (a clone-fold-discard recompute).
The TUI editor’s optimize-accept opener calls this to obtain a FRESH proposal (NFR4 — never
trusts a stale one) WITHOUT opening a second Session (a second open would deadlock on the
held VaultLock, and cmd::optimize::accept is forbidden to the editor for the same reason).
Assembles optimize_year’s inputs exactly as cmd::optimize::run/accept do — events + config
from this conn, bundled prices + tables, a FRESH tax_profile(year) read (not the cached snap),
the attested set, and proposal_made = tax_date(now, UtcOffset::UTC) — and maps OptimizeError
through the crate-internal map_opt_err (which is pub(crate) and not TUI-reachable). now
is injected by the caller for determinism.
Sourcepub fn safe_harbor_residue(
&self,
) -> Result<(Vec<AllocLot>, LotMethod), CliError>
pub fn safe_harbor_residue( &self, ) -> Result<(Vec<AllocLot>, LotMethod), CliError>
READ-ONLY: the 2025-01-01 pre-2025 Universal residue as AllocLots, plus the pre2025_method
(LotMethod) it was computed under. Appends/persists NOTHING. The single source of the pre-2025
subset, shared by cmd::reconcile::safe_harbor_allocate and the TUI allocate opener.
Reads the config ONCE: cfg.pre2025_method is the recorded method returned to the caller, and
cfg.to_projection() is the projection the residue is computed under — the two are STRUCTURALLY
the same config read, so the returned method can never diverge from the residue’s [R0-M1]. The
pre-2025 subset keeps only imports whose tax-date < 2025-01-01 plus ALL reconciliation decisions
(which shape the residue), and DROPs any prior SafeHarborAllocation so the residue stays
allocation-INDEPENDENT (matches transition::universal_snapshot).
Sourcepub fn bulk_link_transfer_plan(
&self,
filter: BulkFilter,
dest: WalletId,
) -> Result<BulkLinkPlan, CliError>
pub fn bulk_link_transfer_plan( &self, filter: BulkFilter, dest: WalletId, ) -> Result<BulkLinkPlan, CliError>
READ-ONLY: compute the bulk link-transfer plan (bulk-link-transfer D1). Selects over the
PROJECTED pending_reconciliation (which already excludes already-decided / already-linked
outs), enriches each with date / source wallet / principal / advisory USD value / carried
basis, applies the frame + from_wallet filters, and routes source == dest rows to
skipped_same_wallet. Appends and persists NOTHING; mirrors safe_harbor_residue.
The USD value is btctax_core::price::fmv_of(&prices, date, principal_sat) [R0-M1] — the
vetted checked helper (round_cents + overflow→None), NOT a hand-rolled principal × price.
The total is the HONEST FLOOR [R0-I2]: total_usd_value_floor is Σ of the PRICED rows only,
and missing_price_count records how many rows lacked a price, so the caller renders exact
$X (when 0) or ≥ $X (N unavailable).
Sourcepub fn bulk_self_transfer_in_plan(
&self,
filter: BulkStiFilter,
) -> Result<BulkStiPlan, CliError>
pub fn bulk_self_transfer_in_plan( &self, filter: BulkStiFilter, ) -> Result<BulkStiPlan, CliError>
READ-ONLY: compute the bulk classify-inbound-self-transfer plan
(bulk-classify-inbound-self-transfer D1). A close MIRROR of bulk_link_transfer_plan applied to
Cycle A’s inbound SelfTransferMine ($0 conservative basis, non-taxable). Appends/persists
NOTHING (clone-fold-discard recompute); KAT-G1-clean at the TUI call site.
Selection (structural false-classify safety) [R0-I1]: candidates are TransferIn events
still flagged UnknownBasisInbound (blocker set joined to the raw event via the index, as
self_transfer_match_plan does) MINUS any already targeted by a NON-VOIDED ClassifyInbound
(mirror open_classify_inbound_flow’s filter 3 — appending a second fires a return-blocking Hard
DecisionConflict; UnknownBasisInbound is RE-EMITTED for gift-basis-unknown states, so
“flagged” ≠ “unclassified”) MINUS wallet-less inbounds [R0-M2] (create no lot). The USD is
fmv_of [G4]; the total is the HONEST FLOOR (total_usd_fmv_floor + missing_price_count).
Sourcepub fn bulk_classify_income_plan(
&self,
filter: BulkIncomeFilter,
) -> Result<BulkIncomePlan, CliError>
pub fn bulk_classify_income_plan( &self, filter: BulkIncomeFilter, ) -> Result<BulkIncomePlan, CliError>
READ-ONLY: compute the bulk classify-inbound-income plan (bulk-classify-inbound-income, Cycle 4).
A NEAR-CLONE of bulk_self_transfer_in_plan: candidates are TransferIns still flagged
UnknownBasisInbound MINUS any already targeted by a NON-VOIDED ClassifyInbound (filter-3;
a second ClassifyInbound fires a Hard DecisionConflict) MINUS wallet-less inbounds (create no
lot; also a Hard-FmvMissing vector). Then the Cycle-4 tax-safety difference [#a]: any
candidate whose fmv_of(date, sat) is None (missing daily-close price OR overflow) is EXCLUDED
from included and counted in excluded_missing_price — a persisted Income{fmv:None} projects
to a Hard FmvMissing year-gate (NOT clearable by ManualFmv on the inbound path). included
therefore carries a RESOLVED fmv: Usd (non-Option). Appends/persists NOTHING.
Sourcepub fn bulk_reclassify_outflow_plan(
&self,
filter: BulkFilter,
) -> Result<BulkReclassifyOutflowPlan, CliError>
pub fn bulk_reclassify_outflow_plan( &self, filter: BulkFilter, ) -> Result<BulkReclassifyOutflowPlan, CliError>
READ-ONLY: compute the bulk reclassify-outflow plan (bulk-reclassify-outflow, Cycle 5). Selects
over the PROJECTED pending_reconciliation (which already excludes already-reclassified / linked
outs, and never contains wallet-less outflows), enriches each with date / source wallet /
principal / RESOLVED auto-FMV / carried basis / estimated gain, and applies the frame +
from_wallet filters. Appends/persists NOTHING (clone-fold-discard recompute); mirrors
bulk_link_transfer_plan + the bulk_classify_income_plan #a missing-price exclusion.
[#a tax-safety — the whole cycle] a candidate with no price is EXCLUDED (counted in
excluded_missing_price), NEVER reclassified: ReclassifyOutflow.principal_proceeds_or_fmv is
Usd (NOT Option), so the row could only be built by FABRICATING a 0/bogus proceeds — a SILENT
misreport (unlike bulk-income’s LOUD FmvMissing). included therefore carries a RESOLVED
fmv: Usd (non-Option), making a fabricated-proceeds Sell structurally unrepresentable here.
Estimated gain [Q3]: basis_usd = Σ pt.legs.usd_basis — computed by the fold’s SINGLE
chronological pass with ALL candidate PendingOuts pending, so Σ over multiple rows is NEVER
double-counted (an earlier-dated out drew the pool down before a later one folded). estimated_gain = round_cents(fmv − basis_usd) per row. The persisted Form-8949 numbers always run the ordinary
fold (exact); only this preview carries the FIFO-vs-method / fee-treatment residual (label it).
Sourcepub fn bulk_resolve_conflict_plan(&self) -> Result<BulkResolvePlan, CliError>
pub fn bulk_resolve_conflict_plan(&self) -> Result<BulkResolvePlan, CliError>
READ-ONLY: compute the bulk resolve-conflict plan (bulk-resolve-conflict D1). Candidate set =
live ImportConflict blockers only (engine post-filtered — an accepted/rejected conflict is no
longer flagged, so re-running never double-resolves; structural idempotence). Joins each blocker
(whose .event is the ImportConflict event id) to the event index to build a STRUCTURED row:
the conflict’s target + new_payload + new_fingerprint, plus the current_payload read from
the TARGET event (a SEPARATE event — accept adopts new_payload, reject keeps current_payload).
Appends/persists NOTHING; mirrors bulk_self_transfer_in_plan.
Sourcepub fn bulk_void_plan(&self) -> Result<BulkVoidPlan, CliError>
pub fn bulk_void_plan(&self) -> Result<BulkVoidPlan, CliError>
READ-ONLY: compute the bulk-void plan (bulk-void D1). Candidate set = the SINGLE shared
predicate btctax_core::voidable_decisions over the projected events + blockers (Decision-id ∧
not-voided ∧ is_revocable_payload ∧ #7 !effective_alloc) — the ONLY defense against sweeping
an EFFECTIVE SafeHarborAllocation into a Hard DecisionConflict. Each row precomputes its
disposal_to_clear ONCE from the same event set (a LotSelection target → ls.disposal_event)
so apply_bulk_void/persist_bulk_void never re-load the log per row. Appends/persists NOTHING;
mirrors bulk_resolve_conflict_plan. Sorted by seq for deterministic display.
Sourcepub fn self_transfer_match_plan(&self) -> Result<Vec<MatchProposal>, CliError>
pub fn self_transfer_match_plan(&self) -> Result<Vec<MatchProposal>, CliError>
READ-ONLY: propose self-transfer matches (self-transfer-passthrough C2). Appends/persists NOTHING
(a clone-fold-discard recompute, like bulk_link_transfer_plan). Pairs ONLY unreconciled legs —
candidate ins are TransferIns flagged UnknownBasisInbound [R0-M2] (enumerated from the blocker
set + joined to the raw event via the event index), candidate outs are pending_reconciliation.
A pair is proposed iff ALL criteria pass: amount within tol = max(out.fee_sat, ceil(0.005 × out.principal)) (a txid EXACT match relaxes the amount check), a ±2-day window consistent with
the direction (passthrough: in on/before out; relocate: in on/after out), and BOTH wallets present.
The suggested action is wallet topology (same-wallet ⇒ Drop, cross-tracked-wallet ⇒ Relocate).
A leg matching >1 counterpart is flagged ambiguous (surfaced, NEVER auto-picked — G-FALSE-MATCH).