homestead-ledger
Homestead · Affairs — module two. The homesteader keeping their own books.
A self-contained desktop module: it ships to a household who double-clicks
it, so its store is embedded SQLite — a linked library, not a server (the
face's 2026-08-04 "no listening socket" decision holds; SQLite binds no port).
The record layer is the engine's: store.py is a thin binding —
homestead.keep.store's adapter contract on a SQLite backing, in the ledger
database. It pins the engine from PyPI — homestead-affairs>=0.1.0,<1.0 (the
distribution name; import homestead is unchanged) — and shares the
~/.homestead root with homestead-law, because a household's affairs are one
thing.
Mirror, not judge
The ledger reflects the household's money; it never authors a financial judgment and never edits a transaction. Concretely, that is the engine's record model applied to money:
- Canonical = the household's own books — imported transactions, statements — read-only by type (I-6). The app has no write path to them.
- Sidecar = the household's overlay — categorization, notes, budget envelopes, a confirmed merchant name. This is where the app writes.
- Money amounts tied to an account are L4; account numbers / SSNs are L5; due-dates and schedules are L1/L2 (see the engine's rung model). The engine enforces this at the storage boundary; a corrupt or unclassified row reads L5 on the way out.
The shared Postgres engine on the fleet side is a sync target reached
through the egress gate — never a runtime dependency of the shipped app. Sync is
an S4 egress: an L5 record never crosses, and what lands in the shared store
is only what the household chose to expose.
Status: bite 0 — the seat is bound. The store binding (
homestead.keep.storeon a SQLite ledger db), a no-egress AST guard over this package (I-17 — a money ledger must never dial out), CI (cold checkout, engine from PyPI, three OSes), and the build plan (docs/build-plan.md). The books, the "what's due" queue, CSV import, and the app land in the bites that follow.
Entering your own information
The demo (--demo) is synthetic. The household's own books go into the
household root — $HOMESTEAD_HOME, else ~/.homestead — and nothing below
needs the optional entity extra:
pip install -e .
homestead-ledger ui # entry forms, intake, queue and subscriptions, on localhost
homestead-ledger obligation add rent "Sunrise Properties LLC" 1450.00 2026-10-01 monthly
homestead-ledger obligation list # the list pane: payee, due date, cadence; the amount derives
homestead-ledger obligation show rent # the detail pane: the amount renders
homestead-ledger queue # what's due
homestead-ledger transaction add 2026-08-01 -84.23 "Whole Foods Market" --account-number 9821
homestead-ledger transaction list # the books, through the gate — the account number is never a row
python -m homestead_ledger --import statement.csv --account-number 9821 # a whole statement
python -m homestead_ledger --import card.csv --account-number 4242 --account credit_card \
--kind credit_card --liability-columns debit,credit # a card statement
python -m homestead_ledger --import statement.csv --account-number 9821 --bank chase # a slashed date column
homestead-ledger transaction list --gaps # rows whose stored date predates this fix
python -m homestead_ledger # the window, on these books (the demo only if empty)
Every imported row's date is parsed to ISO before it is ever written.
--bank <name> declares which day/month order that statement's date column
is written in (wells-fargo, chase, bank-of-america, capital-one,
usaa, discover, amex) and is tried first, so the operator's own
declaration is never overruled by a guess from somewhere else; a cell that
does not fit it still parses if it is unambiguous on its own (2026-08-01,
August 1, 2026, …). A slashed, ambiguous date (08/01/2026) with no bank
named is a per-row import error, never a guess, and a refusal names the field
rather than echoing the cell. This is what
lets the same transaction imported once as 08/01/2026 --bank chase and once
as 2026-08-01 dedup as the same row. There is no migration for a
transaction already on the books from before this fix with an unparsed,
slashed date (v1's own books are synthetic-only) — such a row keeps its old
date text, will not dedup against a re-import in the new ISO form, and
transaction list --gaps is how an operator finds it.
Marking an obligation paid
homestead-ledger obligation paid rent --account checking --fingerprint a1b2c3 # today
homestead-ledger obligation paid rent --account checking --fingerprint a1b2c3 --on 2026-08-07
homestead-ledger obligation paid rent --account checking --fingerprint a1b2c3 --replace
cadence (homestead_ledger/cadence.py) is now a closed set —
weekly/biweekly/monthly/quarterly/yearly/once — and add_obligation
refuses anything outside it by name; the server's <select> and the CLI's own
help text both list the set from cadence.CADENCES, never a copy typed a
second time. Earlier, nothing held cadence to anything, because nothing
yet rolled a due date forward by it.
obligation paid <id> --account <label> --fingerprint <fp> [--on YYYY-MM-DD] [--replace] (and the browser's Mark an obligation paid form) writes a
reference — the labeled account and the transaction's fingerprint, never an
amount — under (obligations, "paid_by", "<id>.<paid-on>"), then advances the
obligation's due_date by its cadence (cadence.roll_forward, which walks
forward from the due date, never from the day it was paid).
A month-end obligation keeps its day. add_obligation records the first
due date's day of month as the obligation's anchor ((obligations, "due_day", "<id>")) and every roll clamps against that, so rent due the
31st paid on time each month goes 31 → Feb 28 → Mar 31 → Apr 30 → May
31 instead of drifting to the 28th and staying there. The anchor is the
day of month, not "the last day of the month": an obligation due the 30th
stays on the 30th. An obligation written before the anchor existed has none;
it falls back to whatever day its due date currently holds, and
obligation add --replace is how to restore it.
A back-dated payment is recorded, and moves nothing. obligation paid --on <an older date> than a payment already on file writes its paid_by
record and leaves the schedule alone: a due date advances once per period,
not once per receipt.
A once obligation has no next date: it is marked resolved instead, and
queue//api/queue stop surfacing it (obligation show still reads it —
nothing is deleted). A second obligation paid for the same id and date is
refused (--replace to record it again) the same way a second obligation add under an occupied id is.
obligation list and the Records tab mark a paid obligation paid ✓ <date>
by reference — but only while that payment is an answer: once the next due
date arrives, or if the payment on file predates the period now open, the
row reads last paid <date> with no tick. A ledger may say "there is a
payment on file" or "this period is paid"; it must never say the second
when it means the first.
An obligation is one record per field at the pack's rungs
(homestead_ledger/obligations.py), under the household's own short id
(lowercase letters, digits and hyphens — rent, car-insurance), which is
what the queue carries; a second add under an id already on file is refused
by the store itself, not by a check that another writer could have raced, and
--replace is how you mean it; a transaction is grown onto the read-only canonical
books through books.import_transaction, the one writer — entered once,
refused on re-entry, never edited ("mirror, not judge"). The browser UI (ui)
has a Records tab with both forms and both lists, an Intake tab that
extracts amounts, dates, merchants and due dates from a pasted bill or receipt
and fills a form with one click, the Queue, and Subscriptions — the
recurring-charge pass over the real books. Merchant resolution, reconciliation
and the ledger check need pip install 'homestead-ledger[entity]' and say so
when it is missing. --account names an account kind the registry knows
(checking, savings, credit_card, loan); an unregistered name is refused
rather than quietly grown. An amount is signed from the household's own side on
every kind — money leaving is negative, money arriving is positive — so on a
credit card or a loan a charge is negative and a payment positive, and the
running balance is read back as what is owed (balance.running_balance(..., liability=True)). A card or loan statement whose columns are a Debit/Credit
split is refused unless --liability-columns CHARGE,PAYMENT says which column
means which: an issuer's own column names do not say, and guessing would
misstate a debt.
The method
Test-first, as in homestead: every claim is a check somebody can run. From a
cold checkout — the engine (homestead.keep) resolves from PyPI as
homestead-affairs, no sibling checkout needed:
pip install -e . # pulls homestead-affairs (homestead.keep) from PyPI
pytest -q
The full build plan — the reuse map, the bite order, the invariants that carry
from the engine and the ones new to money — is in
docs/build-plan.md.
Apache-2.0.
Metadata
Release files for homestead-ledger 0.4.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| homestead_ledger-0.4.0.tar.gz | 197.6 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| homestead_ledger-0.4.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 313.6 kB
Release files / homestead_ledger-0.4.0.tar.gz
| Download URL | homestead_ledger-0.4.0.tar.gz |
|---|---|
| Size | 197.6 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
3556ef4c9dc11a699ae57babad4c3952aa4a100a86b40e864419cf4d7ea3c0b2
|
|
BLAKE2b-256 checksum How to use checksums |
44d4d252162cdc157bd1abf1da7c7b13577c00e9308fd0a56fdad8ce7e5a28c7
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 11, 2026.
Transparency logRelease files / homestead_ledger-0.4.0-py3-none-any.whl
| Download URL | homestead_ledger-0.4.0-py3-none-any.whl |
|---|---|
| Size | 116.1 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
553c8326565484ee5e6ce48b46b5056c61e6e796b3d00de0856d6546cfaab7fa
|
|
BLAKE2b-256 checksum How to use checksums |
034c902cf4e61594fb3011eeeaed9cc627291af163a629b72bb19809087b44db
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 11, 2026.
Transparency log