tool-accounting — double-entry ledger and audit-ready financial statements
tool-accounting is a double-entry accounting engine built to hold a group of related
entities' books as a plain-text, owner-held record rather than as rows in a hosted
database. It journals every transaction, folds those journals into a computed ledger and
trial balance, and renders audit-ready financial statements and narrative disclosure —
without requiring a server, a subscription, or a proprietary file format to read any of it
back later.
The problem it answers is durability and provability at the same time. It aims to leave behind a set of books any computer can still read in twenty years, that nothing can silently overwrite. A reviewing accountant should be able to walk from a statement figure back to the entry that produced it — without taking anyone's word for the trail in between.
Design commitment: double-entry, computed and never stored
Every transaction posts as two effects of equal and opposite size on two different accounts — a payment reduces cash and increases an expense by the identical amount, in one entry. Because every account balance is built from many such paired effects that always cancel, the sum of every balance across the ledger is provably zero at every instant. That fact, checked mechanically at the moment an entry is posted, catches what a single running total cannot: an unbalanced entry, a reference to an account that does not exist, an amount posted to only one side.
Why it matters: an error in a double-entry ledger cannot hide the way it can in a simple running total — the books either balance, or the engine refuses the entry that broke them. That is what lets an owner trust a set of books nobody has audited yet.
tool-accounting never stores the ledger itself. The running balance of every account is
recomputed in full from the underlying journal entries every time a report runs, and the
result is never written back. A second, incrementally updated copy is a second thing that
can drift from the journal it was supposedly derived from; a ledger with no existence
apart from the entries it was just computed from cannot disagree with them.
Money values are integers, never floating-point — an amount is parsed once from text into an exact count of minor units and stays exact through every fold. A value entered with more than two decimal places is refused outright rather than rounded, because float noise introduced upstream is not a real amount.
Edge cases: every report run takes an explicit run_date from the caller — never the
system clock — so the same record, rendered the same way twice, produces byte-identical
output regardless of when or where it runs. A reporting period is always explicitly
disjoint (one quarter only) or cumulative (year-to-date); the engine never infers which
from context, and a cumulative label is rejected outright at the journal-entry level.
The data model
Every entity's chart of accounts is a single flat file, not a table an operator can silently extend by typing a new code into a transaction. An entry referencing an account the chart does not contain fails to load — an invariant failure, not a new account created by side effect. The chart is nine named columns, and a file's header row must match them exactly — a renamed or reordered column is a refused file, not a warning:
entity_code,account_code,ledger_account,statement,periods,sign,posting_tag,sourced,notes
The sign column is where the chart admits what it does not yet know: a confirmed +1
or -1, or an explicit TBD or blank. An unconfirmed sign excludes that account from
every computed statement, and the exclusion is reported by name — never guessed at.
A small set of other master files carry the same discipline. Each is a single source of truth for one category of fact, referenced by code rather than re-typed at the point of use. Among them: an entity registry (jurisdiction, functional currency, reporting framework, which periods are formally delivered), a counterparty registry, a period registry, opening balances, and an exchange-rate table. A consolidation-membership table rounds out the set, kept separate from both the chart and the entity registry.
Why it matters: a reviewer reading the chart of accounts sees the same open questions the engine sees. An account whose sign convention is still undecided is excluded from every computed statement and reported by name — never guessed at.
Every posted transaction is one row of a fixed seventeen-column schema — entity, account, fiscal year, disjoint quarter, and transaction date; counterparty (tagged intercompany or external at entry time, never inferred later) and description; reference fields; an optional pre-tax subtotal and tax amount; and currency, the functional-currency amount, and an invoice reference:
entity_code,account_code,fiscal_year,period,txn_date,counterparty_type,counterparty_id,
description,ref_no,ref_source,subtotal_cad,gst_number,gst_amount,currency_code,
amount_foreign,amount_cad,invoice_number
There is exactly one such schema, and the same rule applies as to the chart: the engine refuses to load a file whose header has drifted from it.
How it works, in flat-file mode: journal files live as CSV under a plain git
repository with zero remotes — a directory tool-accounting-core reads in full once per
report run, never once per account. The filename itself carries the entity, account,
fiscal year, and quarter, duplicating what is already inside the file on purpose, so a
lint pass can cross-check a file's declared identity against its own rows.
Consolidation and multi-entity structure
tool-accounting is built to hold more than one entity at once: a primary reporting
entity, a general partner or equivalent controlling entity excluded from its own
consolidation, and one or more wholly-owned subsidiaries consolidated into the group. Each
tier can carry a different reporting obligation, registered per entity rather than assumed
platform-wide. Combining related entities' books is not simple addition. An intercompany
transaction must be identified as such at the moment it is entered. An elimination
requires both sides of a transaction to tie to exactly the same figure before it is
removed. And every eliminating entry is itself an ordinary, reviewable journal entry —
never a spreadsheet adjustment invisible to anyone who did not build it.
Why it matters: a group's consolidated balance sheet should not simply add up what one entity owes another and call it group debt — that double-counts an obligation which, from outside the group, is no obligation at all. The engine refuses to eliminate a mismatched pair rather than paper over the difference.
Ownership percentage is a general field on every consolidation-membership row from the outset, even where every member today is wholly owned, so the equity logic needs no structural rewrite if a partially owned entity is ever added. Today that field is a recorded fact awaiting its mathematics: the engine refuses outright to consolidate a member recorded below full ownership rather than scaling its lines proportionately, and it likewise refuses a membership change that falls mid-year rather than prorating it.
Audit-readiness posture
The design target is a record a reviewing accountant could reasonably accept as reliable without independently re-performing it. Four properties do that work: output reproducible from the same inputs; a population of entries that is tamper-evident once posted; an opening balance independently re-derived rather than only asserted; and a mechanical path from any statement figure back to the entries that produced it. This is a design posture, not a compliance certification of any kind — a design that makes an audit more efficient to perform is not the same claim as a design that has passed one.
Why it matters: an owner who has never engaged an auditor still gets a record held to the same discipline an audit would demand of it. The standard does not wait for someone to check the homework.
The engine refuses to render anything on an invariant failure — an unbalanced entry, a reference to an account that was never declared, an unresolved opening-balance discrepancy — rather than continuing past it with a warning. Two governed exceptions exist, both visible by construction. A formally logged reconciling item is permitted, and is itself required to close within two reporting periods or the run fails outright. And where the chart still marks an account's sign unconfirmed, the affected statements render with those accounts excluded — and the document itself discloses the resulting residual and names every excluded account, rather than forcing an artificial tie or guessing a sign.
Where the record lives
tool-accounting-core runs against a plain directory of CSV files today — a real, running
mode, and a permanent one rather than a stage the platform intends to retire. A second
storage mode is planned but not yet built: appending the same records through
service-fs — the WORM ledger backbone's hash-chained, tamper-checked append log inside a Totebox archive.
That mode would let a reviewing accountant verify not just that an entry's contents are
unaltered, but that the log it sits in has only ever been appended to since a prior
checkpoint they held. Both modes are intended to share every layer above the storage trait
itself, so which one an owner uses would change nothing about the engine's logic — only
where the bytes live.
Why it matters: an owner is never required to adopt a hosted platform to use the ledger. The engine can be handed to an accountant as a folder on a laptop, and every figure in a statement can be reproduced from it on that accountant's own machine.
The command-line toolchain
The engine ships as two crates. tool-accounting-core is a pure library — the money,
period, and journal-line types, the CSV parsers, and the chart, ledger, trial-balance, and
consolidation logic — with zero external dependencies and zero entity-specific data:
every chart, journal, and registry is read from a data directory the caller supplies. A
pilot binary crate drives that library as a command-line toolchain of report binaries,
each rendering HTML and PDF through tool-typeset, the platform's shared zero-dependency
renderer, into outputs/<fiscal_year>/ beside the data — redirectable with the
ACCOUNTING_OUTPUT_DIR environment variable, so an experiment never writes into a shared
data folder.
statements [--year YYYY] [--period Q1|Q2|Q3|YE] # consolidated statement package; YE is the default
gp_statements [--year YYYY] [--period Q1|Q2|Q3|YE] # the controlling entity's standalone package
titleco_statements # per-subsidiary packages, from one shared template
ledger_report # the full general ledger, rendered
mda [--year YYYY] [--register-root PATH] # the narrative management-discussion document
events_timeline --year YYYY | --from DATE --to DATE # business-event timeline; optional --entity CODE
The crate's default binary takes no flags at all: it prints an entity-by-entity ledger walk — every posted line with its running balance — and the trial balance folded from it, the fastest way to see what the journals currently prove. Three behaviors of the report binaries carry the engine's character. A quarter the entity registry does not mark as formally delivered still renders, but the run labels the result as audit support rather than a delivered deliverable — the registry, not the caller, decides that label. The narrative management-discussion document renders only for the primary reporting entity; asked to produce one for a controlling or nominee entity, the engine refuses, because that obligation attaches to the reporting entity alone. And the per-subsidiary statement packages render from one shared template, with a test asserting the rendered packages stay identical apart from the entity itself — a lint that runs over the same code path the binary runs, not a second implementation that could agree with itself while disagreeing with the deliverable.
Why it matters: every deliverable is one command with at most three flags, run against a folder of files — producing a complete, consolidated statement package requires no server, no login, and no vendor in the room. The toolchain is CLI-only: no terminal or console surface exists yet.
Build status
tool-accounting-core — the shared money, period, and journal-line types, the CSV parser,
and the chart, ledger, trial-balance, and consolidation logic — is built and has been
verified against real historical annual data rather than synthetic fixtures, which
surfaced and fixed real data-entry defects in the process. tool-typeset, the
zero-dependency PDF and HTML renderer this engine shares with the platform's sibling
construction tool, is built and independently verified by extracting text back out of a
rendered PDF and checking it against the source structure. Together they have already run
one full fiscal year's complete pipeline — journals into a computed ledger, a trial
balance folded from it, rendered statements, and rendered narrative. That run was entered
and rendered end to end for a primary reporting entity and its general partner, and a
second year is now in progress. Both crates carry passing unit-test suites, and the
rendered statement packages were structured line for line against independently prepared
professional drafts of the same record — an answer key, not data the reports merely
reformat.
Why it matters: an owner evaluating this platform is not being asked to take the
design on faith. The components that touch real dollar figures have already been checked
against a real year of real transactions, not designed on paper alone — which puts
tool-accounting further along than any comparable tool elsewhere in the platform's
ledger-and-statement family.
Three items this article once listed as unbuilt are now real. The consolidation fold is wired: the consolidated statement package renders the reporting entity together with its registered wholly-owned members, every consolidated line traceable back to the per-entity lines it folded from. Journal data now exists for those subsidiary entities, and each renders its own standalone year-end package from the shared template described above. And interim (quarterly) rendering is built: the statement binaries accept a quarter as readily as a year-end, with the entity registry deciding whether the result is a formally delivered period or audit support.
Still not built, and reported as such rather than approximated: non-wholly-owned
consolidation — a member recorded below full ownership is refused, not scaled; mid-year
consolidation entry or exit — refused, not prorated; opening balances — empty for every
entity, treated as an open item rather than an assumed figure, with dual verification
against a prior year's own closing balance a locked design not yet exercised in practice;
and the archive storage mode described above. The bookkeeping review terminal planned to
confirm entries into the ledger is scaffolded and active as a plugin surface, but it is
not yet wired to live ledger data — its current view renders placeholder figures. A
cross-archive aggregation component intended for a firm servicing many owners' books at
once is referred to here under the working name app-orchestration-accounting. This is
a proposed name and scope only — not a name ratified anywhere else in the platform —
and nothing under that name exists yet.
Licensing
tool-accounting is licensed under AGPL-3.0-or-later. AGPL-3.0-or-later is a copyleft
license: the source code is available to everyone, and any modified version — including
one operated as a network service — must be released under the same license if it is
distributed or made available over a network. A separate PointSav-Commercial license is
available as a paid alternative for anyone who needs to distribute a modified version, or
offer it as a network service, without that copyleft obligation.
Why it matters: a lender or an owner's own engineer can read and audit the full source before deciding whether to trust it — the code is not a black box behind a paywall.
See also
- tool-construction — construction cost, schedule, and quality ledger — the sibling development-and-construction ledger tool, built on the same double-entry design and sharing this engine's renderer
- tool-payroll — jurisdiction-aware payroll and statutory remittance — the sibling payroll engine, whose first real report is built and designed to post computed pay into this ledger as ordinary entries
- service-fs — the WORM ledger backbone — the append-log storage substrate the planned archive storage mode is designed against
- Totebox archive — the owner-held archive an entity's records are intended to live inside
- service-input — reference-archive migration and calibration — parses and content-addresses a source document before it becomes a proposed journal entry