Skip to content

lot tracking, hledger 2.x, AI policy#2558

Open
simonmichael wants to merge 153 commits intomasterfrom
lots
Open

lot tracking, hledger 2.x, AI policy#2558
simonmichael wants to merge 153 commits intomasterfrom
lots

Conversation

@simonmichael
Copy link
Owner

@simonmichael simonmichael commented Mar 1, 2026

This branch is intended to be the basis for the hledger 2.0 preview 1 discussed at #2547 (comment) . I hope to release that soon, ideally with 1.52.

It mainly contains the automated lot tracking functionality I have been working on this month. This adds a major missing feature to hledger. It is working for me so far, but it's experimental and needs user testing and refinement. See also:

and for background:

This also represents the first serious use of AI tools in the project. hledger 1.x was developed with no AI assistance; hledger 2.x will carefully explore AI-assisted development. Most of the new code in this branch was generated with claude code + sonnet/opus, guided by me. I have started an AI policy doc which will hopefully grow to answer common questions.

I have reviewed each commit myself, all tests are passing and many new ones were added, and I have done a fair amount of testing with my own journals. I would love any additional human review but wow that's a lot of commits, so I don't expect too much. If you have time please take a look at code, tests, docs, and/or build and try it out. This branch will be released as a preview and anything can be changed if needed.

AI policy:

Lot tracking:

A little more on this lot tracking design:

  • It is obviously inspired by, learns from, and tries to be quite compatible with, the lot tracking in Ledger and (especially) Beancount.
  • It is a hybrid of Ledger's/Beancount's amount-based cost basis annotations, and hledger 1.x's explicit lot subaccounts; both are supported. You can use whichever notation is more convenient in a given situation.
  • It aims to be good at inferring missing information, so that often minimal or even no special notation is required. Existing journal entries might not need to change much.
  • It also allows everything to be recorded explicitly, so that nothing is hidden and lot information can be passed to a new file or exported to another system.
  • It aims to be pervasive: --lots mode makes detailed lots appear in all other reports, making it relatively easy to view and understand complex lot movements.
  • It aims to be optional: when not using --lots mode, everything should work as it always has.
  • It aims to support both global and per-account reduction methods, so that it can handle eg past and present US tax rules.
  • AI use is in the air right now, but rustledger has been influential in demonstrating its effectiveness for PTA development.

  • ;doc:README: describe the 2.x branch & plans
  • dev: MixedAmount: keep amounts with different cost basis separate
  • dev!: Hledger.Data.Types: move isAccountSubtypeOf to new module, add helpers
  • ;cabal: update cabal files
  • feat: classify cost basis postings, adding a ptype tag
  • dev!: rename PostingType/RegularPosting/ptype to PostingRealness/RealPosting/preal
  • ;doc: Cost basis / Lot syntax: update
  • ;doc: update embedded manuals
  • feat: lotful commodities; infer cost basis from transacted cost
  • imp: commodities: support tag: queries
  • imp: infer transacted cost from cost basis, when appropriate
  • fix: print: preserve empty {} cost basis annotations
  • imp:print: show cost basis before transacted cost, like beancount
  • imp: print: beancount output: handle account and commodity tags better
  • imp: print: beancount: provide a commented tolerance option; cleanups
  • imp: print: beancount output: convert market prices
  • feat: journal: accounts can be declared lotful too, with a "lots" tag
  • imp: print: beancount: use account's lots tag value as disposal method
  • imp: print: beancount: balance assignments are converted to explicit amounts
  • imp: print: beancount: convert a top-level revenue(s) account to Income
  • imp: print: beancount: convert the no-symbol commodity to "CC"
  • imp: print: beancount: increase the example tolerance
  • imp: print: beancount: also convert single-letter commodity symbols
  • ;doc: SPEC-lots: draft specification for lot-related functionality
  • feat:lots: with the --lots flag, track and show acquired lots automatically
  • imp:lots: handle disposals, in FIFO order
  • ;doc: SPEC-finalising: retroactive specification for journal finalising
  • imp:lots: handle transfers, in FIFO order (part 1)
  • imp:lots: transfers (part 2): use a better sequence in finalise pipeline
  • ;cabal: update cabal files
  • dev:lots: reorganise current and in-progress lot tests
  • dev:lots: review tests, update some expected behaviours
  • ;doc: SPEC-print: document some print behaviours as a specification
  • ;doc: SPEC-finalising: cleanup
  • dev:lots: make some tests pass
  • dev:lots: make more tests pass
  • ;doc: SPEC-finalising: update
  • dev:lots: infer cost basis for bare disposals on lotful commodities
  • dev:lots:refactor: reorganise Lots.hs by pipeline stage
  • dev:refactor: clean up journalFinalise
  • ;doc:lots: SPEC, PLAN updates
  • dev:lots: consistent field order in CostBasis definition
  • imp:lots: also infer cost basis from cost when {} is present
  • imp:lots: disposal-aware transaction balancing
  • imp:journal: accept hledger consolidated lot syntax in journal input
  • imp:print: show hledger lot syntax by default
  • imp:print: add -O ledger output format for Ledger-style lot syntax
  • imp:lots: add configurable reduction methods (FIFO, FIFO1, LIFO, LIFO1)
  • fix!:journal: always exclude gain postings from txn balancing
  • ;doc: lots: note scenarios where cost basis differs from transacted cost
  • fix:lots: show correct per-lot quantities in multi-lot disposals
  • imp:lots: infer amountless gain postings in disposal balancing
  • imp:lots: validate lots tag values; check SPECID method
  • imp:lots: show source position in lots: tag validation errors
  • fix:lots: fix disposal matching with explicit lot subaccounts
  • ;test:lots: add tests for implicit multi-lot disposals
  • imp:lots: infer gain posting in disposal when none is present
  • imp:lots: default gain account to revenue:gains when no Gain account declared
  • imp:lots: add _ptype:gain tag to gain postings
  • imp:lots: make validateUserLabels error verbose
  • imp:lots: make all Lots.hs errors verbose (position + excerpt)
  • ;doc: Output formats, print: ledger output format
  • ;doc: update cost basis / lots docs; new Lot reporting section
  • ;doc: recent dev notes
  • ;doc:lots: SPEC/PLAN updates
  • ;dev: comment update
  • dev:lots: errors cleanup
  • ;dev: fix warning
  • feat:lots: add AVERAGE and HIFO reduction methods
  • ;test:lots: fix test expectations for cost->price rename
  • imp:lots: detect bare acquisitions on lotful commodities
  • fix:lots: exclude zero-amount postings from bare acquire detection
  • fix:lots: declassify bare lotful postings when cost/price can't be inferred
  • fix:lots: detect bare lotful transfers without {} on the from posting
  • fix:lots: handle bare transfers and exclude virtual postings
  • fix:lots: detect transfers more robustly
  • fix:lots: require exact quantity matching for transfer detection
  • imp:lots: move balance assertion to generated parent posting on lot splitting
  • ;doc:lots: document balance assertion handling with lot splitting
  • imp:lots: add the generated-posting tag to generated gain postings
  • ;doc: added Reporting concepts > Detecting special postings
  • ;dev:print: refactor
  • feat:lots: parse lot subaccount names into cost basis
  • imp:lots, check: check accounts now always ignores lot subaccounts
  • ;doc: PLAN updates, add SPEC-special-postings
  • feat:lots: support symmetric equity transfers in close --clopen --lots
  • fix:lots: fix lot-split amounts having insufficient display precision
  • ;doc:PLAN-lots
  • ;doc: AI.md: start a project AI policy doc

@simonmichael simonmichael added the investing Related to investments, lots, capital gains, etc. label Mar 1, 2026
@simonmichael simonmichael requested a review from Copilot March 1, 2026 07:03
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR is a large step toward hledger 2.x, centered on introducing automated lot tracking (enabled via --lots) and aligning output/interop behavior (notably Beancount and Ledger-style lot syntax), with accompanying data model updates and extensive new specs/tests.

Changes:

  • Add/extend lot tracking: classification (_ptype), reduction methods (FIFO/LIFO/HIFO/AVERAGE/SPECID variants), transfers/disposals, and --lots reporting behavior.
  • Update core types and plumbing: Posting ptypepreal, MixedAmountKey expanded to include cost basis/cost combinations, gain-posting handling in balancing, and commodity directive source positions for better errors.
  • Improve print/export: new -O ledger output format, expanded -O beancount output (tolerance option, price directives, booking method, tag/metadata handling), and updated docs/specs/tests.

Reviewed changes

Copilot reviewed 81 out of 81 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
hledger/test/print/beancount.test Updates/extends Beancount print expectations
hledger/test/ledger-compat/hledger-other.test Adjusts ledger-compat check expectations
hledger/test/json.test Updates JSON schema expectations (preal)
hledger/test/journal/lots.test Expands/renumbers lots syntax functional tests
hledger/test/journal/lots-transfer.test Adds transfer scenarios and regression coverage
hledger/test/journal/lots-transfer-entries.test Systematic transfer entry variation tests
hledger/test/journal/lots-methods.test Tests reduction/booking method variants
hledger/test/journal/lots-dispose.test Adds disposal scenarios and balancing behavior tests
hledger/test/journal/lots-dispose-entries.test Systematic disposal entry variation tests
hledger/test/journal/lots-clopen.test Tests close/open workflow interactions with lots
hledger/test/journal/lots-acquire.test Adds acquisition scenarios and lotful detection tests
hledger/test/journal/lots-acquire-entries.test Systematic acquisition entry variation tests
hledger/test/journal/infer-costbasis.test Adds cost basis inference behavior tests
hledger/test/journal/infer-cost.test Adds transacted cost inference behavior tests
hledger/test/journal/commodity-tags.test Adds commodities tag: query test
hledger/hledger.cabal Regenerated cabal metadata (hpack version bump)
hledger/Hledger/Cli/Commands/Register.hs Uses preal for virtual posting bracketing
hledger/Hledger/Cli/Commands/Print.md Documents new ledger output format
hledger/Hledger/Cli/Commands/Print.hs Adds ledger output and enhances Beancount output
hledger/Hledger/Cli/Commands/Commodities.md Documents tag: support for commodities query
hledger/Hledger/Cli/Commands/Commodities.hs Enables tag-aware commodity filtering
hledger/Hledger/Cli/Commands/Close.md Notes lots-specific balance assertion behavior
hledger/Hledger/Cli/Commands/Close.hs Skips assertions for lot subaccounts in closing txns
hledger/Hledger/Cli/Commands/Check.md Documents lot subaccount exemption for account checks
hledger/Hledger/Cli/Commands/Add.hs Uses preal when creating postings in add workflow
hledger/Hledger/Cli/Commands.hs Updates command tests to preal
hledger/Hledger/Cli/CliOptions.hs Adds --lots; refactors output-format/rawopts handling
hledger/.date.m4 Updates manual date stamp
hledger-web/hledger-web.txt Updates rendered manual footer date
hledger-web/hledger-web.cabal Regenerated cabal metadata (hpack version bump)
hledger-web/hledger-web.1 Updates manpage header date
hledger-web/Hledger/Web/Widget/AddForm.hs Uses preal when validating postings
hledger-web/Hledger/Web/Test.hs Updates rawOptsToInputOpts call signature
hledger-web/.date.m4 Updates manual date stamp
hledger-ui/hledger-ui.txt Updates rendered manual footer date
hledger-ui/hledger-ui.cabal Regenerated cabal metadata (hpack version bump)
hledger-ui/hledger-ui.1 Updates manpage header date
hledger-ui/.date.m4 Updates manual date stamp
hledger-lib/package.yaml Exposes new modules (AccountType, Lots, Write.Ledger)
hledger-lib/hledger-lib.cabal Regenerated cabal metadata + new exposed modules
hledger-lib/Hledger/Write/Ledger.hs Adds Ledger-style lot syntax transaction renderer
hledger-lib/Hledger/Write/Beancount.hs Enhances Beancount output (prices, metadata filtering, commodities/accounts)
hledger-lib/Hledger/Read/TimedotReader.hs Switches virtual posting field to preal
hledger-lib/Hledger/Read/RulesReader.hs Switches posting field to preal
hledger-lib/Hledger/Read/JournalReader.hs Adds commodity directive sourcepos; uses preal
hledger-lib/Hledger/Read/InputOptions.hs Adds lots_; removes auto_posting_tags_
hledger-lib/Hledger/Query.hs Adds tag-aware commodity matching helper
hledger-lib/Hledger/Data/Types.hs Core type changes: Commodity sourcepos, CostBasis order, LotId/ReductionMethod, MixedAmountKey expansion, preal
hledger-lib/Hledger/Data/Transaction.hs Adapts posting rendering to new AmountFormat pipeline
hledger-lib/Hledger/Data/Timeclock.hs Uses preal for virtual postings
hledger-lib/Hledger/Data/Posting.hs Updates rendering signatures; uses preal consistently
hledger-lib/Hledger/Data/Json.hs Renames JSON field output to preal
hledger-lib/Hledger/Data/JournalChecks.hs Ignores lot subaccounts in account checks; updates rendering call sites
hledger-lib/Hledger/Data/Journal.hs Adds lots helpers/validation; cost-basis/cost inference utilities
hledger-lib/Hledger/Data/Errors.hs Adds commodity tag excerpt helper for better errors
hledger-lib/Hledger/Data/Balancing.hs Excludes Gain postings from balancing in disposal txns; uses preal
hledger-lib/Hledger/Data/Amount.hs Adds Ledger lot syntax rendering; expands amount keying; preserves {}; reorders cost-basis vs cost display
hledger-lib/Hledger/Data/AccountType.hs New helpers for subtype checks (moved out of Types)
hledger-lib/Hledger/Data/AccountName.hs Updates posting type helpers to PostingRealness
hledger-lib/Hledger/Data.hs Re-exports new modules (AccountType, Lots)
hledger-lib/.date.m4 Updates manual date stamp
doc/SPEC-special-postings.md New spec doc for “special posting” detection
doc/SPEC-print.md New spec doc for print behavior/flags
doc/SPEC-lots.md New/expanded lots specification
doc/SPEC-journal.md New/expanded journal format specification
doc/SPEC-finalising.md New/expanded journal finalising pipeline specification
doc/PLAN-doc.md Planning notes for documentation work
doc/PLAN-beancount-output.md Planning notes for Beancount export
doc/NOTE-recompiling.md Dev note on faster typecheck workflow
doc/NOTE-amount-keys.md Design note on MixedAmountKey/aggregation semantics
doc/NOTE-amount-classes.md Design note on typeclass semantics for amounts
doc/NOTE-2.x.md Notes on 2.x strategy/positioning
doc/AI.md Initial project AI policy document
README.md Describes 2.x branch intent/plans

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@simonmichael
Copy link
Owner Author

simonmichael commented Mar 1, 2026

PS this brings up a difficult topic. If you don't like the sound of this, please do take a look at https://github.com/simonmichael/hledger/blob/lots/doc/AI.md . Feedback welcome on the mail list thread, here, or in chat.

@simonmichael
Copy link
Owner Author

The lots branch has been updated with many fixes and improved diagnostics, and a check lots so you can easily add lots checking to your flycheck-hledger config.

It's now pretty much accepting all my sloppy inconsistent real-world journals, except for genuinely wrong/unfinished entries. By that I mean it's checking them carefully enough, and also tolerating enough variation in journal entries, that the lot movements are calculated as intended (no more "insufficient lots" errors caused by some far away entry; problems are generally reported at the earliest relevant location 🤞).

So: more real world testing welcome. Also, there's a user-oriented set of example journal entries showing the most common patterns: examples/lots/lot-entries.journal.

@simonmichael simonmichael changed the title lot tracking, hledger 2.x lot tracking, hledger 2.x, AI policy Mar 7, 2026
@simonmichael simonmichael force-pushed the lots branch 3 times, most recently from 7ae25fd to e857ccb Compare March 23, 2026 02:27
When --lots-warn is used, lot calculation is enabled (implies --lots)
and lot selection failures (insufficient lots, no lots available,
ambiguous selector) produce warnings to stderr instead of aborting.
The affected postings pass through unchanged, allowing partial lot
processing of journals with known gaps.

Warnings use the same rich error format (with transaction excerpt)
as the normal error messages.
Change lot debug traces (--debug=5) to show a one-line transaction
summary "FILENAME:LINE DATE DESC" instead of the full transaction
excerpt, e.g.:
  lots: -:1 2026-01-15 buy: acquired 10 AAPL {2026-01-15, $50} on assets:stocks
Make --lots-warn also catch pairIndexedTransferPostings failures
(mismatched/unmatched transfer postings) as warnings, passing the
whole transaction through unchanged. Also use lotDbg helper for
classification traces, replacing verbose dbg5 calls that dumped
full Transaction records.
…ERAGEALL)

*ALL variants select lots per-account like the base methods, but validate
that the selected lots would also be chosen first if all accounts' lots
were considered together. AVERAGEALL additionally computes the weighted
average cost across the global pool rather than per-account.
Lot errors (insufficient lots, no lots available, ambiguous selector)
now show which reduction method is active and where it came from
(account tag, commodity tag, or default), plus a hledger command to
review prior lot operations for the affected account/commodity.

Also adds showOtherAccountLots to the "insufficient lots" error
(was already shown for "no lots available").
Replace txnErrPrefix with postingErrPrefix at selectLots call sites,
so lot errors show the posting line highlighted rather than the
transaction header line.
When a posting involves a lotful commodity or account in an asset account
but the classifier couldn't determine its role (acquire, dispose, or
transfer), it previously passed through silently. Now with --lots-warn,
a warning is emitted at the point where classification fails, helping
users find the root cause of downstream "insufficient lots" errors.

Zero-amount postings (e.g. from balance assertions) are skipped.
Two transfers on the same date moving portions of the same lot to the
same destination account would overwrite instead of summing quantities,
due to M.union being left-biased. The addLotState helper (previous
commit) fixes this by using M.unionWith to sum quantities.
Show -f options and --verbose-tags.
When a transaction contains both a fee (negative lotful posting paired
with expenses) and a transfer (negative lotful posting paired with
another asset account), the otherAssetReceives heuristic was too broad
and classified both as transfer-from. This caused the fee posting to
pair with the transfer-to, delivering the wrong amount.

Now checks for a non-asset counterpart at the exact same
commodity+quantity (hasFeeCounterpart); when found, the posting is
classified as dispose instead of transfer-from.
When an equity transfer-to posting (opening balance) creates a lot on the
same date as a regular acquire, findDatesNeedingLabels can't predict the
collision (it only counts acquire-tagged postings, not transfer-to postings
processed as acquires). Rather than erroring with "duplicate lot id",
processAcquirePosting now auto-generates a disambiguating label when a
collision is detected and the posting has no user-provided label.
`hledger check lots` runs the lot-tracking pipeline
(posting classification, lot calculation, disposal balancing).
It also enables --verbose-tags, so that any error messages will show
the ptype classification tags.
Income statement accounts (Revenue, Expense, Gain) are flow accounts
that should not track lots. A posting like `expenses:fees 0.1 ETSY {$80} @ $90`
(stock fee on disposal) was getting classified as acquire and receiving
a lot subaccount, which is incorrect.

Now shouldClassifyWithCostBasis skips acquire/dispose classification for
income statement account types, while still allowing transfers (to equity etc.).
check lots: strict mode, stops at first lot error (unchanged behavior).
check lotswarn: lotswarn mode, lot selection failures are warnings not
fatal errors. Only declaration errors and disposal balance failures are
fatal. Uses a two-pass approach: quiet first pass detects hard errors,
then diagnostic pass shows scoped warnings.

Also adds journalCalculateLotsQuiet (suppresses trace/warn output) and
parseErrorDate (extracts date from error messages for scoping).
When --lots-warn is active and a lotful acquire posting is declassified
because no cost basis or price could be inferred, emit a warning.
This catches cases like prices accidentally left inside comments
(e.g. `2220 T  ; @ $0.01` where the @ is after the semicolon).
Previously, a bare acquire posting (no {cost basis} or @ price) that
couldn't infer a cost was silently declassified — its _ptype tag was
stripped and the posting passed through untracked. This created hidden
balance discrepancies where the raw account balance included the
amount but the lot state didn't, causing "insufficient lots" errors on
later transactions.

Now processAcquirePosting returns an error for this case, and
foldMPostings handles it like dispose errors: hard error with --lots,
warning with --lots-warn. The error surfaces at the actual problem
transaction instead of a later one.

Also fix shouldClassifyBareTransferTo to exclude postings with @ price,
preventing misclassification of acquisitions as transfers.

Also tweak some error messages.
Refactor lot calculation to collect warnings in the return type instead
of using a trace-style warnFn callback. journalCalculateLotsImpl now
returns Either String (Journal, [String]) where the [String] contains
any warning messages. This removes the RankNTypes/forall parameter and
eliminates unsafePerformIO from the warning path.

check lotswarn now uses journalCalculateLotsImpl directly to get the
warnings list, prints them via warnIO, and exits with code 1 if any
were found. Previously it always exited 0 when there were no hard
errors, even if warnings were printed.
…otswarn

All lot selection/classification failures are now hard errors. Remove the
--lots-warn flag, lots_warn_ InputOpts field, journalCalculateLotsImpl,
journalCalculateLotsQuiet, parseErrorDate, and the Lotswarn check. Simplify
journalCalculateLots signature (Bool -> Journal -> Either String Journal).
Unclassified lotful postings in asset accounts are now hard errors with --lots.
Update tests accordingly.
Zero-amount postings of lotful commodities (e.g. balance assertions like
`0 AAPL = 100 AAPL` or balance assignments inferring zero) don't represent
lot movements, so they shouldn't trigger the "was not classified" error.
Use index-based posting lookup (makePostingErrorExcerptByIndex) instead
of equality-based search, which failed when postings were modified by
the balancer after parsing. The error now points to the specific posting
line rather than the transaction start.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

investing Related to investments, lots, capital gains, etc.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants