import: grounded in Intuit's specs; atomic chart and bank imports
check / check (push) Canceled after 0s

New vendored sources (docs/spec/sources/): Intuit's bank CSV upload and
IIF formats, ECMA-376 / Excel number storage, and the IRS form lines the
tax categories map to.

IIF export (rewritten to Intuit's IIF Import Kit):
- declares every account first (!ACCNT NAME, ACCNTTYPE, ACCNUM): an
  undeclared account is created in QuickBooks "as a new bank account";
  the MCP export types them (cash accounts BANK, the control accounts AR
  and AP, registered fixed assets FIXASSET, the rest by type);
- subaccounts by colon path, parents first; colons in a name replaced;
- ASCII only (IIF is ASCII text), dashes and quotes transliterated;
- the kit's 2007+ journal layout and M/D/YYYY dates; an entry using an
  undeclared account is refused.

Bank CSV:
- the 4-column layout is money out then money in by position, which
  matches Intuit's "Credit, Debit" and banks' "Debit, Credit" both (the
  doc's "Expense, Income" labels were wrong, the parsing was not);
- a slash date's order comes from the file (a part over 12 settles it)
  or the caller's new date_order, never a guess: Intuit recommends
  dd/mm/yyyy, US banks write mm/dd/yyyy;
- fractions of a cent and double negatives ("(-5)") are refused; the
  header is recognized on the first non-blank row.

Chart CSV: a blank code or name is refused with its row (found by the
new fuzz_import_csv target), and the core now refuses a blank account
code or name too (replay keeps older books).

MCP:
- books.import_chart_csv and books.import_bank_csv rehearse the whole
  file on a copy of the ledger first: a bad row anywhere refuses the
  file (before, a duplicate within the file, a parent after its child,
  or a date in a closed period left earlier rows behind);
- bank rules: the longest matching pattern wins (they arrive in a map,
  so "first match" changed from run to run).

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-13 20:47:41 -06:00
1 parent f1bdac4bca
commit c72a762ef1
20 files changed
+4154 -182

No files matched your search

+5
View File
@@ -195,6 +195,11 @@ pub enum LedgerError {
#[error("account code must not be empty")]
EmptyAccountCode,
/// An account's name is empty or only spaces: a chart lists every
/// account by number and title.
#[error("account name must not be empty")]
EmptyAccountName,
/// The referenced parent account does not exist.
#[error("unknown parent account id {0}")]
UnknownParentAccount(u64),
+10 -1
View File
@@ -670,6 +670,15 @@ impl Ledger {
parent: Option<AccountId>,
placeholder: bool,
) -> Result<AccountId, LedgerError> {
// A chart lists each account by number and title; neither may be
// blank (2026-10-13; the core refused only an empty code, so a code
// of spaces or an unnamed account got in).
if code.trim().is_empty() {
return Err(LedgerError::EmptyAccountCode);
}
if name.trim().is_empty() {
return Err(LedgerError::EmptyAccountName);
}
if let Some(p) = parent.and_then(|p| self.chart.get(p))
&& p.account_type != account_type
{
@@ -684,7 +693,7 @@ impl Ledger {
}
/// [`Self::add_account`] without the rules added on 2026-10-13 (a
/// child's type), for replaying journals written before them. Never
/// child's type; a blank code or name), for replaying journals written before them. Never
/// for new work.
pub fn replay_add_account(
&mut self,