Skip to content

Ellerman's double-entry multiset ledger: machine-checked semantics and executable oracle (S01-S54) - #11

Merged
jwiegley merged 55 commits into
mainfrom
johnw/ellerman-review
Nov 7, 2026
Merged

jwiegley merged 55 commits into
mainfrom
johnw/ellerman-review

Conversation

@jwiegley

@jwiegley jwiegley commented Nov 7, 2026

Copy link
Copy Markdown
Member

This branch carries the implementation of William Ellerman's vector-space model of double-entry accounting ("A double-entry multiset ledger", The Mathematical Intelligencer, 2026) as machine-checked Lean semantics, together with an executable oracle derived from it, in 54 signed commits (S01 to S54; the 55th unit is a documentation record kept beside the repository).

What the series does

  • The mathematics. The paper's abelian groups of accounts and commodity amounts become a groupoid of one isomorphism per property type; each module of the theory states the paper's theorems as named Lean lemmas and closes with the three-axiom budget only (propext, Classical.choice, Quot.sound), with sorry turned into an error by the lakefile.
  • The executable oracle. A journal parser, an evaluator that reduces balances through the same isomorphisms, and a driver whose output is byte-compatible with the semantics the theorems prove. The oracle is interpreted (lake env lean --run); no compiled binary sits between the checked definitions and what executes.
  • New executable capability (opt-in, default output unchanged): --gross prints per-account gross debit/credit totals; --exchange COMM values each account in one commodity at midnight of the --now date through the recorded price chain, with 14 named refusal reasons for the shapes it cannot value as ledger does. Both are announced in README.md and AGENTS.md.
  • New specification: a computable netValue on the oracle's market vector, proved in a Market join module (S47), and the balance-rejection protocol stem documented (S54).
  • Documentation truth: README.md's balance and valuation contract corrected against the theorems; coverage limits and every accepted-but-undocumented departure marked beside its rule (S49 to S54).

How correctness was established

The C++ implementation (ledger/ledger) pins this repository as a submodule and a flake input, and the review bisimulated the two: every commit of the C++ branches was replayed against the oracle over ledger's full corpus (4,417 .test files) at every step, ending green at the tips (PASS 3827, FAIL 0, NEG:unbalanced 52, SKIP:negative 459; the local ctest 4,393 of 4,393). Two upstream regressions the review exposed are filed as ledger/ledger#3344 and ledger/ledger#3345, with fixes and witness tests on branches johnw/ellerman-review and johnw/ellerman-review-b of ledger/ledger (PR to follow).

The bisimulation procedure, its recorded departures, and the module inventory are documented in AGENTS.md and README.md; the per-unit record, witnesses and audits live in the working notes beside the review.

Verification of this branch

  • nix build green (the build is the proof check; warnings are errors).
  • The axiom budget audit: 26 files, every theorem on the three-axiom budget.
  • The environment audit: 3,462 constants in 32 modules, 0 outside budget.
  • The import/layer scan: the specification layer never imports the executable layer; the run path closes over 14 modules.
  • --exchange and --gross are opt-in; the default output of the driver is byte-identical to its pre-series behavior on the whole corpus (3816 journals, tally A).

jwiegley and others added 30 commits September 30, 2026 05:39
free_flow_balanced, the only double-entry theorem stated for the free
model, assumes that the flow of every generator is zero.  Only a journal
whose entries move nothing satisfies that hypothesis, so the theorem is
degenerate rather than false, and it does not carry equal debits and
credits down to computed flows as README.md:55 says it does.  Ellerman
states the principle per property type (On Double-Entry Bookkeeping,
2014): a transaction is recorded as a zero-term (pp. 488-489), zero
added to zero is zero (pp. 489, 496), and a vector equation holds when
each of its components does (p. 494).  On the signed side of his debit
isomorphism, the side that PacioliGroup represents, a row that sums to
zero is a zero-row (p. 492).  The hypothesis the theorem needs is
therefore that each entry sums to zero in each commodity, not that it is
zero.

Pacioli.lean now defines trialBalance, which maps each account to its
commodity and sums; commodityTotal, one column of it; and zeroRows, its
kernel as an AddSubgroup, with mem_zeroRows, trialBalance_single,
trialBalance_apply and commodityTotal_apply.  The subgroup needs
Mathlib.Algebra.Group.Subgroup.Ker, which Pacioli did not reach before.
The import adds seven modules to the closure of Ledger/Driver.lean,
which the interpreted oracle loads: 3200 modules against 3193 before,
counted from env.header.moduleNames.

flowAux is generalized from PacioliGroup to any additive commutative
group, which makes naturality (map_flowAux) statable; flowAux_mem and
flow_mem give subgroup preservation.  trialBalance_flowAux applies
naturality with values in Commodity →₀ ℚ, giving the per-commodity
totals of any term as the denotation of the entries' own residuals.
netFlow_eq_flowAux in Oracle.lean elaborates unchanged, and Oracle.lean
and Register.lean are untouched.  ofFlowHom turns any additive
functional of flows into a valuation functor of the free model.
free_hom_balanced instantiates balanced_generators_balance at it, and
free_ker_balanced gives the kernel form for a map into any additive
commutative group.  free_trial_balance is the per-commodity theorem, and
free_double_entry its vector form: if every entry is a zero-row, so is
every derived flow.  free_flow_balanced keeps its signature and becomes
the corollary at the identity map, with a docstring that says why its
hypothesis is degenerate.

The docstring of free_trial_balance names classes of generators that the
oracle emits and that can fail its hypothesis: a priced exchange, a
residual accepted as an implicit exchange or within the balancing
tolerance, a paren-virtual posting, whether written in the transaction
or appended by an automated transaction, and a timeclock entry, which is
added without a balance check.  It does not claim that the list is
complete, and at this commit it is not: given the automated transaction
"= /Food/" with the posting "Assets:Savings 0.1" and a lunch of 10.00
USD, the oracle prints USD|1|Assets:Savings, while ledger 3.4.1 reports
"Transaction does not balance".

The module docstring of Pacioli.lean and the README are left to later
commits, so README.md:55 goes on crediting free_flow_balanced until the
README account of the invariant is rewritten.

lake build exits 0, with warnings as errors.  #print axioms reports only
propext, Classical.choice and Quot.sound for 25 declarations: the 21
added or changed here, flow and accountValuation beside them, and the
two theorems in Oracle.lean that unfold flowAux.  An audit of all 1806
constants declared in Ledger.lean and the nineteen modules it imports,
which are every module under Ledger/ except Gen, finds none outside that
budget; both audits fail, as they must, on a planted native_decide
theorem.  The oracle's output on six probe journals, among them
Ellerman's vector ledger in two forms and the demo journal of the C++
suite, is byte-identical to its output before this change, and so is its
default output over the 3816 positive and the 469 negative journals
extracted from the C++ suite.  The bisimulation against the C++ tree,
over 4384 .test files, reports PASS 3816, FAIL 0, SKIP:negative 489,
SKIP:no-journal 77 and SKIP:python 2, with both detector fixtures green.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0143bpV19FtoLrrR59maPRPk
No theorem of the specification says that a balanced entry touches
two accounts; every balance result says only that sums vanish.
Ellerman observes that an equation x + ... + y = w + ... + z cannot
survive a change to one term alone, and that the property belongs to
the transaction and not to the method of recording it, so it is not
unique to double entry (On Double-Entry Bookkeeping, 2014,
pp. 486-487).  PacioliGroup represents his single-sided system, the
image of the group of T-accounts under the debit isomorphism, and
there the same fact says that a zero-row touches two or more accounts
(p. 492).  Because the single-sided system has that property too, it
cannot be what is double in double entry; what is double is the
two-sidedness of the T-accounts (p. 487).  The zero-row subgroup of
the previous commit makes the statement expressible.

Pacioli.lean now proves one_term_change, for a fixed total in any
AddCancelCommMonoid, and derives zeroRow_two_accounts from it: a
nonzero zero-row touches two distinct accounts of one commodity.  The
docstring says that PacioliGroup is the single-sided image and not
Ellerman's group of T-accounts, which is why the corollary is his
p. 492 observation and not an account of what is double, and that
the two accounts, sharing a commodity, differ in name, so they are
two accounts also in his vector sense.
zeroRows_change_two_accounts reads the same fact as a change between
two zero-rows, and single_mem_zeroRows_iff says that a lone posting
is a zero-row only when it moves nothing.  free_flow_two_accounts
carries the corollary through flow_mem to every nonzero derived flow
of the free model, under the zero-row hypothesis of
free_double_entry.  An example on unitSpec, one generator moving a
dollar from cash to food, shows that its hypotheses hold together, so
it does not repeat the degenerate hypothesis of free_flow_balanced.

implicit_exchange_not_mem_zeroRows marks the limit of the corollary:
10 EUR against -11 USD, with no stated price, is not a zero-row, yet
C++ Ledger and the oracle both accept it as an implicit exchange.  Its
docstring points to the two-commodity residual convention recorded in
the module docstring of Journal.lean.  On a journal holding that one
transaction, ledger 3.4.1 prints the two postings without error, and
a row comparison finds the build of the C++ working tree and the
oracle in agreement on EUR|10|Assets:EUR and USD|-11|Assets:USD.  An
accepted transaction therefore need not be a zero-row.  No executable
rule changes, and no comment is added to Journal.lean.

lake build exits 0, with warnings as errors.  #print axioms reports
only propext, Classical.choice and Quot.sound for the six new
theorems.  An audit of all 1812 constants declared in Ledger.lean and
the modules it imports, six more than before this change, finds none
outside that budget, and both audits fail, as they must, on a planted
native_decide theorem.  The oracle's default output over the 3816
positive and the 469 negative journals extracted from the C++ suite
is byte-identical to its output before this change.  The
bisimulation against the C++ tree, over 4384 .test files, reports
PASS 3816, FAIL 0, SKIP:negative 489, SKIP:no-journal 77 and
SKIP:python 2, with both detector fixtures green.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0143bpV19FtoLrrR59maPRPk
The README says that the invariant that debits equal credits is a
theorem about the shape of morphisms, "since every morphism has equal
measure on its two ends" (README.md:52-53), and the D12 paragraph of
Valuation.lean says that no law connects the object part of a valuation
to its measure of morphisms, "nor should one".  No theorem stood behind
either sentence.  Ellerman places the double-entry principle in the
encoding of each transaction as a zero-term (On Double-Entry
Bookkeeping, 2014, p. 489), and observes that the change of two or more
terms is a characteristic of the transaction and not of the method of
recording it (p. 487).  Read as a statement about every morphism and a
measure of objects, the README sentence holds only of a measure that
does not distinguish objects, because the zero morphism 0 : Hom A B
exists between any two objects and every valuation measures it as zero.

Valuation.lean now states that failure in each object-indexed reading.
endpoint_measure_const and endpoint_measure_degenerates take the
difference form, W.V f = φ B - φ A for every morphism f : Hom A B, and
conclude that φ is constant and that W vanishes.  The docstring says
that only the zero valuation is such a difference, since the zero
valuation is the difference of any constant measure.
conserved_measure_const and conserved_measure_zero take the conservation
form, φ A = φ B across every morphism, and conclude that φ is constant,
and zero everywhere when it is zero on the empty object.
Valuation.ofObject_ends_equal_degenerates takes the equal-ends form at a
static valuation and concludes that the valuation is zero, and
ofObject_endpoint_degenerates is the difference form at a static
valuation.  None of these six endpoint theorems assumes 𝕋Laws, and
each rests on a comparison of the two ends of a zero morphism.  A
seventh declaration, the simp lemma ofObject_singleton, reads the value
of a one-account object.  The D12 paragraph names the endpoint theorems
and says that they refute only the object-measure readings of a
morphism's two ends.  The import lines of Valuation.lean are unchanged.

Pacioli.lean adds oneFlow_gen_measure, which measures the generator of
unitSpec, a morphism from the empty object to itself, as 1 at
someAccount, and endpoint_law_refuted, which uses it to show that not
every valuation of every lawful model is a difference of endpoint
measures.  Because the two ends of that generator are the same object,
the difference form fails on the generator itself, and the zero
morphism plays no part in the counterexample.  What the theorem adds is
a lawful model with a nonzero valuation, so that
endpoint_measure_degenerates excludes a valuation that exists and is
not vacuous.  The docstring of endpoint_law_refuted says so.  The
converse of the principle, that only zero added to zero gives zero
(n. 12), is not added.  The README paragraph and the module docstring
of Pacioli.lean are left to later commits.

lake build exits 0, with warnings as errors.  #print axioms reports only
propext, Classical.choice and Quot.sound for the nine new theorems.  An
audit of all 1821 constants declared in Ledger.lean and the modules it
imports, nine more than before this change, finds none outside that
budget, and both audits fail, as they must, on a planted native_decide
theorem.  The oracle's default output over the 3816 positive and the 469
negative journals extracted from the C++ suite is byte-identical to its
output before this change.  The bisimulation against the C++ tree, over
4384 .test files, reports PASS 3816, FAIL 0, SKIP:negative 489,
SKIP:no-journal 77 and SKIP:python 2, with both detector fixtures green.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0143bpV19FtoLrrR59maPRPk
The docstring of the static Valuation in Valuation.lean described it as
the object-level companion of a valuation functor "(opening balances,
price tables)", and the docstring of Valuation.ofObject called it "the
unique commutative-monoid homomorphism from the free CMon" with no
theorem behind the word.  The parenthetical is wrong on both counts.
Ellerman writes that "the initial T-accounts in the ledger add up to the
zero-account" and that each transaction is encoded as T-accounts that
add to the zero-account (On Double-Entry Bookkeeping, 2014, p. 489),
and that Beginning Ledger + Journal = Ending Ledger (p. 496).  A
beginning ledger is therefore of the same kind as a transaction
zero-term, and recording it as one entry posted to the empty ledger,
rather than as a state, is faithful to him.  The oracle has no separate
construct for opening balances: Driver.journalTerm tensors every
generator, an opening one included, onto a term that starts from the
identity on the empty object.  An unconstrained function Account → ℝ
is, moreover, not confined to ledgers that add up to the zero-account,
since nothing requires its values to sum to zero within each commodity,
whereas the signed form of Ellerman's beginning ledger does so within
each property type (a zero-row, p. 492, and a zero-row of vectors,
p. 498).  That second argument rests on Ellerman's model and does not
carry over to C++ Ledger, which accepts opening entries that do not sum
to zero within each commodity, since a posting with a lot price or a
cost is balanced at that price or cost rather than at its own amount,
the convention that the Journal.lean module docstring records.  For a
journal that opens $500 of cash and 10 AAPL at $100 against
Equity:Opening Balances, ledger 3.4.1 bal and the oracle both report
$500, 10 AAPL and $-1500, so the dollar column sums to $-1000 and the
AAPL column to 10.  For C++ journals the first argument therefore
stands alone.  Prices, for their part, are rates between commodities,
carried by PriceGroupoid in Prices.lean and, when dated, by TimedPrices
in TimedPrices.lean, not quantities held by accounts.  No declaration
outside Valuation.lean uses the static Valuation, so only the prose was
wrong.

The Valuation docstring now says that it measures state rather than flow
and represents neither opening balances nor price tables.  It gives two
reasons for excluding opening balances, with the citations of pp. 489,
492 and 498: a beginning ledger can be recorded as one entry posted to
the empty ledger, which is how the oracle records an opening entry, and
an unconstrained function need not sum to zero within each commodity.
It adds that the second reason does not carry over to C++ Ledger, whose
accepted opening entries need not sum to zero within each commodity, so
that for C++ journals the first reason stands alone.  The ofObject
docstring says that it sums over the multiset and names ofObjectHom,
ofObjectHom_apply and ofObjectHom_unique.  Three declarations are new.
ofObjectHom v, of type Object →+ ℝ, is Multiset.sumAddMonoidHom
composed with Multiset.mapAddMonoidHom v; the simp lemma
ofObjectHom_apply says, by rfl, that it computes ofObject v; and
ofObjectHom_unique proves, through Multiset.addHom_ext, that every
additive map Object →+ ℝ agreeing with v on every singleton is
ofObjectHom v.  The uniqueness claim is therefore a theorem.  The import
lines of Valuation.lean are unchanged, and no other file is touched.

lake build exits 0, with warnings as errors.  #print axioms reports only
propext, Classical.choice and Quot.sound for ofObjectHom_unique and
ofObjectHom_apply.  An audit of all 1824 constants declared in
Ledger.lean and the modules it imports, three more than before this
change, finds none outside that budget, and the audit fails, as it
must, on a planted native_decide theorem.  The phrase "opening
balances, price tables" no longer occurs in Valuation.lean, counted
after whitespace folding.  The oracle's default output over the 3816
positive and the 469 negative journals extracted from the C++ suite is
byte-identical to its output before this change.  The bisimulation
against the C++ tree, over 4384 .test files, reports PASS 3816, FAIL 0,
SKIP:negative 489, SKIP:no-journal 77 and SKIP:python 2, with both
detector fixtures green.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0143bpV19FtoLrrR59maPRPk
The docstring of 𝕋Laws in Groupoid.lean says that neither composition
nor tensor distributes over hom-set addition and that each candidate
law is refuted by the valuation, which would count some flow twice,
and it gives left distributivity of composition as its example.  Only
one of the candidate laws had a degeneracy theorem behind it:
zero_absorption_degenerates in Pacioli.lean shows that zero
absorption forces every valuation to zero.  The free model refuted
zero absorption and tensor-additivity (comp_not_zero_absorbing,
tensor_not_additive), but no Lean statement showed that either
distributive law of composition, or tensor-additivity, collapses the
valuations, and nothing refuted left distributivity, the docstring's
own example.  The README, for its part, says that the argument
zero_absorption_degenerates records refutes the bilinearity law as
well, although that theorem concerns absorption only (item I8 of the
Ellerman analysis).

Pacioli.lean gains four theorems.  left_distrib_degenerates,
right_distrib_degenerates and tensor_additive_degenerates each take a
valuation functor on any 𝕋 together with the law in question and
conclude that the valuation of every morphism is zero: (0 + 0) ∘ f,
f ∘ (0 + 0) and (0 + 0) ⊗ g measure f or g once, while the
distributed forms measure it twice.  None of the three assumes
𝕋Laws, and neither does zero_absorption_degenerates; a check of the
elaborated types confirms that 𝕋Laws occurs in none of the four
statements.  comp_not_left_additive refutes left distributivity over
all lawful models by the free model on unitSpec, where
(id + id) ∘ f and id ∘ f + id ∘ f carry the generator's flow once and
twice, so the removed law is not derivable from 𝕋Laws.  The 𝕋Laws
docstring now cites the four degeneracy theorems, each beside the
equation it concerns, and claims a collapse only for those four laws:
absorption of 0 in the right operand of composition, additivity of
composition in either operand, and additivity of tensor in its first
operand.  Because "left" in these names refers to the operand of
composition, as it does in id_comp, whereas the multiplicative
reading calls (a + b) * c right distributivity, each theorem's
docstring states its law in full.  The docstring of the categorical
AddCommMonoid instance cites the degeneracy of both distributive laws
beside that of absorption, since a Preadditive structure would impose
all three.

Groupoid.lean also called the hom-set monoid "the CMon enrichment" in
four places and described 𝕋 as enriched over commutative monoids in
its module docstring and in the docstring of the structure 𝕋, while
the 𝕋Laws docstring says that 𝕋 is not a CMon-enriched category in
the standard sense.  The planned correction of Ledger.lean (item I11)
takes the second reading.  Those docstrings now say that each hom-set
is a commutative monoid, Mathlib's AddCommMonoid, which decision D16
makes compatible with valuations only, so that the structure is not
an enrichment in the standard sense.  The Pacioli.lean module
docstring, which lists the D16 witnesses, is left to the later
documentation unit that owns it.  No import line changes.

The locked build script, a full rebuild of Groupoid and of every
module above it with warnings as errors, exits 0 with no warning in
its log.  #print axioms reports only propext, Classical.choice and
Quot.sound for the four new theorems and for
zero_absorption_degenerates and tensor_not_additive.  An audit of all
1828 constants declared in Ledger.lean and the modules it imports,
four more than before this change, finds none outside that budget,
and the audit fails, as it must, on a planted native_decide theorem.
Every declaration name cited in Groupoid.lean resolves, apart from
the nine tokens already on the checker's allowlist before this
change, and the phrase "CMon enrichment" no longer occurs in
Groupoid.lean, counted after whitespace folding.  The oracle's default
output over the 3816 positive and the 469 negative journals extracted
from the C++ suite is byte-identical to its output before this change.
The bisimulation against the C++ tree, over 4384 .test files, reports
PASS 3816, FAIL 0, SKIP:negative 489, SKIP:no-journal 77 and
SKIP:python 2, with both detector fixtures green.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0143bpV19FtoLrrR59maPRPk
The module docstring of TimedPrices.lean says that the constant-rate
model captures exactly what -V reports do.  Yet timedConstRates fixes
one base functional W for every instant and measures commodity c at
instant t by (1 / priceAt t c) • W, so a holding's value in a fixed
numeraire cannot change as prices move (items I8 and I14 of the
Ellerman analysis).  The planned correction of that docstring, and the
corollary that item I14 names timedConstRates_no_holding_gain, both
need this fact as a checked statement.  Decision D5 of the
implementation plan keeps the general form in this module, so that
the later units cite one theorem for one fact.

TimedPrices.lean gains timedConstRates_value_const: for any time type,
any valuation functor W on any 𝕋, any nowhere-zero price function
priceAt, any instant t, any commodity b and any morphism f, the price
of b at t times the b-measurement of f in the fiber at t equals
W.V f.  The proof unfolds that measurement to
(1 / priceAt t b) * W.V f and closes by field_simp from the nonzero
hypothesis at t and b.  Its docstring states the fact and nothing
more.  The module docstring, and the docstrings of Prices.lean and of
price_soundness, are left to the later documentation unit that owns
them (decision D35); neither Prices.lean nor Theorems.lean has a
diff, and no import line changes.

The locked build script, lake build with warnings as errors, exits 0
with no warning in its log.  #print axioms reports only propext,
Classical.choice and Quot.sound for timedConstRates_value_const and
for price_soundness.  An audit of all 1829 constants declared in
Ledger.lean and the modules it imports, one more than before this
change, finds none outside that budget, and the audit fails, as it
must, on a planted native_decide theorem.  The oracle's default output
over the 3816 positive and the 469 negative journals extracted from
the C++ suite is byte-identical to its output before this change.
The bisimulation against the C++ tree, over 4384 .test files, reports
PASS 3816, FAIL 0, SKIP:negative 489, SKIP:no-journal 77 and
SKIP:python 2, with both detector fixtures green.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0143bpV19FtoLrrR59maPRPk
Item G4 of the Ellerman analysis concerns where the doubleness of
double entry lies.  Ellerman locates it in the two-sidedness of the
T-accounts and not in the number of accounts that a transaction
touches (p. 487).  Pacioli.lean already records the second half of
that argument in zeroRow_two_accounts, but nothing in the theory
relates the domain and codomain of a generator to the signs of its
flow: the abstract theory leaves endpoints unconstrained by flows, and
the proof of double_entry_invariant never reads its domain.  Decision
D4 of the implementation plan names the new module after Ellerman's
own term and places it as a specification leaf above Pacioli rather
than inside it, because Oracle imports Pacioli and the leaf's imports
would otherwise enlarge the closure that every oracle run loads: a
file importing Ledger.Driver loads 3200 modules, and 3205 with the
leaf's two Mathlib imports added.  The five added modules all come
from Mathlib.Algebra.Order.BigOperators.Group.Finset, since
Mathlib.Algebra.Order.Group.PosPart already lies in the closure of
Ledger.Pacioli; the PosPart import is kept because D4 prescribes it
and because the proofs use its lemmas directly.

Ledger/TwoSides.lean imports
Mathlib.Algebra.Order.BigOperators.Group.Finset,
Mathlib.Algebra.Order.Group.PosPart and Ledger.Pacioli, and nothing
else.  EndsCoherent A B v says that every account with a positive
entry in v lies in B and every account with a negative entry lies in
A.  It holds of the zero flow, and it is preserved by negation with
the ends exchanged, by tensor and by addition.  two_ends proves that
for a coherent zero-row, in each commodity, the positive parts over
the codomain sum to the negative parts over the domain, which is
Ellerman's equality of debits and credits (p. 496) located on the two
ends.  eq_zero_of_endsCoherent_zero proves that a coherent zero-row
with an empty domain is zero, and eq_zero_of_endsCoherent_dom_eq_zero
states the same for a domain that is empty only propositionally.  The
module docstring reads this as the case of Ellerman's [x // 0], which
is a zero-term only when x = 0, and attributes that fact to the
cross-sum equality of his Table 1 (p. 485), as the analysis report
does.  sum_commodity_eq_zero is the per-commodity lemma these proofs
share.

At the level of generators, GenSpec.SignCoherent asks that the flow
of each generator be coherent with its own ends, and
GenSpec.IsCoherent asks the same of an L2 morphism through its flow
and its endpoints, both of which are invariant under Rel.  Coherence
holds for the generators of a coherent specification and for id and
0, and it is closed under inv, tensor and +.
flow_eq_zero_of_isCoherent states that a coherent morphism out of the
empty object whose flow is a zero-row has zero flow, which is the form
of "an opening balance with no offset moves nothing" in which the
domain does work.  FreeMor.CompFree singles out the terms built
without composition, and endsCoherent_flowAux together with
isCoherent_mk proves that such a term over a coherent specification
is coherent.  Two witnesses bound the scope.
unitSpec_not_signCoherent shows that the counterexample model of
Pacioli.lean is incoherent.  comp_breaks_coherence exhibits two
coherent zero-row generators (chainSpec, chainFlows) whose composite
out of the empty object has a nonzero zero-row flow that credits an
account lying in neither end, so coherence is not closed under
composition.

The declarations keep the names that the plan gives them, and the
module docstring states that coherent there means coherent in sign,
which is unrelated to the monoidal coherence of Groupoid.lean.  The
docstring attributes the reading of endpoint multiplicities as
posting counts to the oracle's construction, Driver.specFor, and not
to the abstract theory, in which endpoints are arbitrary multisets.
That citation is prose only; the module imports nothing from the
executable layer.

No existing file changes.  The lakefile glob builds the new module.
Ledger.lean does not import it yet: the index entry and the AGENTS.md
and README entries belong to the later documentation unit that owns
those files, and the sentence that points from this module to the
executable transfers joins its docstring when those transfers land.

The locked build script, lake build with warnings as errors, exits 0,
and its log records one module built, Ledger.TwoSides, and no warning; a
separate check of the file with autoImplicit off and warnings as errors
also exits 0.  The #print axioms output for fifteen of the module's
nineteen theorems, all but EndsCoherent.zero, sum_commodity_eq_zero,
isCoherent_id and isCoherent_zero, lists only propext, Classical.choice
and Quot.sound, and the checker that reads that output reports a planted
native_decide theorem as a failure.  The environment-wide audit, which
loads the index and every module under Ledger/ except Gen, reports 1886
constants in those 21 modules and none outside the same budget; 57 of
those constants are declared in TwoSides, and the preceding commit's
message records 1829.  Its self-test with a planted native_decide
theorem fails, as it must.  The name checker reports every name that the
new docstrings cite as resolved, Driver.specFor included.  The import
scan reports no specification module importing the executable layer, and
a file importing Ledger.Driver reports Ledger.TwoSides absent from its
module closure.  Module-closure probes give 3196 modules for
Ledger.Pacioli, with Mathlib.Algebra.Order.Group.PosPart present and
Mathlib.Algebra.Order.BigOperators.Group.Finset absent, and give 3200
and 3205 modules for Ledger.Driver with the first and the second of
these imports added alone.  The oracle's default output over the 3816
positive and the 469 negative journals extracted from the C++ suite is
byte-identical to its output before this change, by cmp against the
recorded snapshots.  The bisimulation log against the C++ tree, over
4384 .test files, reports PASS 3816, FAIL 0, SKIP:negative 489,
SKIP:no-journal 77 and SKIP:python 2, with both detector fixtures green.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0143bpV19FtoLrrR59maPRPk
The preceding commit proved the two-sides theorems of item G4 for
every generator specification that is coherent in sign, but nothing
proved that the oracle's own construction is one.  Driver.specFor
gives each generator the accounts of its negative postings as its
domain and those of its positive postings as its codomain, so the
theorems of TwoSides reached the generators the oracle builds only in
prose.  Decision D4 of the implementation plan puts the executable
transfers in a module of their own above Driver, so that neither
Oracle nor Driver imports TwoSides: the preceding commit measured that
a file importing Ledger.Driver loads 3200 modules, and 3205 with the
two Mathlib imports of TwoSides added, since only one of them lies
outside that closure.

Ledger/DriverTwoSides.lean imports Ledger.Driver and Ledger.TwoSides
and holds only proofs and the two definitions they need.
specFor_coherent proves that specFor puts every account whose net flow
in a generator is positive in that generator's codomain, and every
account whose net flow is negative in its domain.  It is proved
pointwise by specFor_coherent_at from two list lemmas, foldl_add_snd
and list_sum_nonpos; the second mirrors Mathlib's List.sum_nonneg,
which has no nonpositive counterpart of its own at this pin (a
#check of List.sum_nonpos against the built tree fails with an unknown
constant), and is proved in one line as List.sum_le_card_nsmul at
zero.  postingsFlow and flowsFin restate flowsFor as a
PacioliGroup flow, flowsFin_apply and coe_flowsFin prove that the two
agree, and specFor_coherent_fin restates coherence in that form.
journalTerm_compFree proves that the journal term, a tensor of
generators, is built without composition, and journalTerm_coherent
combines it with endsCoherent_flowAux and netFlow_eq_flowAux: the flow
that netFlow computes for the whole journal credits only accounts in
the total domain and debits only accounts in the total codomain.
flowsFin_two_ends, kept by decision D8, is two_ends for an oracle
generator whose flow is a zero-row, and
flowsFor_eq_zero_of_dom_eq_zero proves that such a generator with an
empty domain moves nothing.  The proofs unfold specFor, flowsFor and
journalTerm, whose text this commit leaves unchanged.

The module docstring states that the balance theorems concern
generators whose flow is a zero-row, which the oracle does not always
emit, and names three kinds of generator that are not.  The planned
docstring called a timeclock entry single-sided.  That is false when a
bucket account is declared, because the check-out rule in Journal.lean
then emits an opposite bucket posting, as C++ does
(test/regress/1607.test; src/timelog.cc:84-91), so the docstring says
instead that a timeclock entry is added without a balance check, the
wording of free_trial_balance.  The planned closing sentence called an
empty-domain generator with a nonzero flow unbalanced; it now says
that the flow is not a zero-row, since a priced trade is balanced on
weights without being one.  The docstring also names the route of each
transfer, flowsFin_apply for the generators and netFlow_eq_flowAux for
the journal term, and says that TwoSides, if Oracle or Driver imported
it, would bring into the oracle's closure a Mathlib import that the
closure does not yet contain.  TwoSides.lean gains one sentence that
points to the new module and to Driver.specFor_coherent.

AGENTS.md said that Driver is the only module that joins the two
sides.  Read as the two layers, that sentence is false once
DriverTwoSides exists, and the interim replacement drafted in the
plan, "the only module on the oracle's run path that joins the two
sides", was itself ambiguous, because Oracle lies on the run path and
imports Pacioli (issue R22 of the plan's review).  The text now says
that, apart from the index Ledger.lean, which imports Parse, Journal
and Oracle directly, Driver is the only module that joins the two
chains of the executable layer, the journal-reading chain of Parse and
Journal and the evaluating chain that begins at Oracle, and that a
module which imports an executable module together with a
specification module only to state proofs, such as DriverTwoSides, is
imported by no module on the oracle's run path, defined there as the
import closure of Driver.  This amends a binding rule and is submitted
for the user's approval.  It is the first of four such amendments in
the series; the others are the protocol admissibility sentence, the
join-module rules of the final inventory, and the balance-rejection
stem as protocol.  If the user declines only the wording, this sentence
and the closing sentence of the final inventory are rewritten and
nothing else changes.  If the user declines modules of this kind, this
commit is dropped together with the later GrossLedger and Market
modules and the README and inventory text that cites them.  The later
documentation commit replaces the whole layer section, and it also
adds the new module to Ledger.lean, which does not import it yet.

The locked build script, lake build with warnings as errors, exits 0,
and its log records Ledger.TwoSides and Ledger.DriverTwoSides built
and no warning.  A Nix build of the oracle from a scratch clone
carrying this change exits 0 after 1100 jobs, and the built oracle
prints the two expected rows for a two-posting smoke journal.  git
diff shows no change to Driver.lean, Oracle.lean or Journal.lean, and
a textual comparison reports specFor, journalTerm and flowsFor
identical to their text at f5d891e.  The #print axioms output for all
fourteen declarations of the module, the five principal theorems
included, lists only propext, Classical.choice and Quot.sound, and the
checker that reads that output reports a planted native_decide
theorem as a failure.  The environment-wide audit reports 1916
constants in 22 modules and none outside the same budget; 30 of them
are declared in DriverTwoSides, and the preceding commit's message
records 1886 in 21.  Its self-test with a planted native_decide
theorem fails, as it must.  The name checker resolves every name that
the edited docstrings and AGENTS.md cite; the one unresolved token is
the module name DriverTwoSides, which is allowlisted.  The import scan
reports no specification module importing the executable layer, no
run-path module importing a leaf or join module, and no module under
Ledger/ importing DriverTwoSides, and a file importing Ledger.Driver
still loads 3200 modules, with Ledger.TwoSides and
Ledger.DriverTwoSides absent.  In AGENTS.md, after whitespace folding,
the new sentence occurs once and the old one not at all, and the
register checker reports AGENTS.md and every added line clean.  The
oracle's default output over the 3816 positive and the 469 negative
journals extracted from the C++ suite is byte-identical to its output
before this change, by cmp against the recorded snapshots.  The
bisimulation log against the C++ tree, over 4384 .test files, reports
PASS 3816, FAIL 0, SKIP:negative 489, SKIP:no-journal 77 and
SKIP:python 2, with both detector fixtures green.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0143bpV19FtoLrrR59maPRPk
Item I13 of the Ellerman analysis concerns the theorem named
pacioli_equation, which proves that assets equal liabilities plus
equity as a rearrangement of the trial balance.  Ellerman's article
calls that identity the balance sheet equation (pp. 488-490 and 496)
and reserves the name of Pacioli for the group in which double entry
is represented, so the old name attaches Pacioli to the wrong object.
The statement and the proof are not in question; the corrections that
the analysis asks of the docstring belong to a later unit.

Ledger/Theorems.lean renames the theorem to balance_sheet_equation
and leaves its statement, proof and docstring unchanged.  A
deprecated alias keeps the old name,

  @[deprecated balance_sheet_equation (since := "2026-09-27")]
  alias pacioli_equation := balance_sheet_equation

so that a use of the old name reports its replacement.  The lakefile
passes -DwarningAsError=true to every module of the package, so that
report is an error there.  The date is the one that the implementation
plan fixes for this change.  Decision D35 of the plan leaves the
docstring, whose heading still reads "The Pacioli equation", to the
documentation unit that owns it.

README.md named the old theorem in its paragraph on valuations.  That
reference would have sent readers to a name whose use now fails, so
its one identifier is replaced by the new name and the last lines of
the paragraph are refilled to the width of the file.  Decision D1
assigns the rewriting of that paragraph to the coordinated README
unit, and this edit anticipates none of it.

The locked build script, lake build with warnings as errors, exits 0
after 1100 jobs.  A search of the repository, excluding the .lake
build directory, finds the old name only in the alias line of
Theorems.lean, and a count of it in README.md gives 0.  A two-line
file that imports Ledger.Theorems and refers to
Ledger.pacioli_equation, checked with autoImplicit off and warnings as
errors, exits 1 with the single error that the constant has been
deprecated and that Ledger.balance_sheet_equation should be used
instead; before this change the same check of that file exited 0,
and the same check of a file that refers to
Ledger.balance_sheet_equation exits 0.  The #print axioms output for
balance_sheet_equation, double_entry_invariant,
closing_transfers_net_income and trial_balance lists only propext,
Classical.choice and Quot.sound.  The environment-wide audit reports
1917 constants in 22 modules and none outside the same budget; the
preceding commit's message records 1916, and the difference is the
alias.  The register checker reports README.md and every added line
clean.  The oracle's default output over the 3816 positive and the 469
negative journals extracted from the C++ suite is byte-identical to
its output before this change, by cmp against the recorded snapshots.
The bisimulation log against the C++ tree, over 4384 .test files,
reports PASS 3816, FAIL 0, SKIP:negative 489, SKIP:no-journal 77 and
SKIP:python 2, with both detector fixtures green.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0143bpV19FtoLrrR59maPRPk
Item G6 of the Ellerman analysis found that the specification
prototypes each restated the vocabulary they share, and the report's
section on item I17 records the lesson that free_valuation_ext, the
statement that two valuation functors on the free model which agree
on every generator agree on every morphism, belongs in Pacioli.lean.
The leaf modules that later units of the implementation plan add name
the morphism of a generator, compare valuations generator by
generator, transport a flow assignment through an additive map, and
add two flow assignments.  Without one home for these facts each leaf
would prove its own copy, which is the duplication that G6 exists to
remove.  Decision D6 of the plan fixes the names that the leaves use;
this change supplies the lemmas.

The GenSpec namespace of Ledger/Pacioli.lean gains a subsection after
free_flow_two_accounts with seven declarations.  genHom t is the
morphism of the free model that the generator t denotes, from S.dom t
to S.cod t.  flow_genHom evaluates flow on it and is a simp lemma.
ofFlowHom_gen evaluates ofFlowHom on it and is deliberately not one,
because simp already reaches the same right side through ofFlowHom_V
and flow_genHom; the design text tagged it, but the leaves cite it
only by name, and its docstring records the choice.
free_valuation_ext is proved by the induction principle of
freeGenerated: the structural cases are closed by V_id, V_zero,
V_comp, V_inv, V_tensor and V_add of both valuation functors, and the
generator case by the transport lemma V_castHom together with the
hypothesis.  It uses nothing from the valuation-functor algebra of
Derived, so Pacioli, which lies in the import closure of the oracle,
gains no import.  flow_map states that flow is natural in the
generator assignment under an additive endomorphism of PacioliGroup;
it is map_flowAux passed through the quotient, so the induction over
terms is not repeated.  flow_add_flows states that flow is additive in
the generator assignment, by induction over terms, and
ofFlowHom_add_flows carries that additivity to the valuation functors
ofFlowHom.

The locked build script, lake build with warnings as errors, exits 0
after 1100 jobs with no warning in its log.  A probe file checks that
plain simp proves the statement of ofFlowHom_gen and that
simp only [ofFlowHom_gen] still does, and the design tree's
PriceVector leaf, which cites ofFlowHom_gen and free_valuation_ext,
compiles against this tree; both checks exit 0.  #print axioms lists
only propext, Classical.choice and Quot.sound for the seven
declarations and for ofFlowHom_V.  The environment-wide audit reports
1928 constants in 22 Ledger modules and none outside the same budget;
the preceding commit's message records 1917, and a listing of the
constants of Ledger.Pacioli accounts for the eleven new ones as the
seven declarations and four auxiliary lemmas that abel generates in
the proof of flow_add_flows.  The import lines of Pacioli.lean are
unchanged, and none names Derived.  The scan of added Lean lines finds
no sorry, native_decide, axiom, unsafe or implemented_by, and the
register checker reports every added line clean.  The oracle's
default output over the 3816 positive and the 469 negative journals
extracted from the C++ suite is byte-identical to its output before
this change, by cmp against the recorded snapshots.  The bisimulation
log against the C++ tree, over 4384 .test files, reports PASS 3816,
FAIL 0, SKIP:negative 489, SKIP:no-journal 77 and SKIP:python 2, with
both detector fixtures green.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0143bpV19FtoLrrR59maPRPk
Item I4 of the Ellerman analysis found that the module docstring of
Ledger/Pacioli.lean, at the analysed revision the only place in the
repository that named Ellerman, gave no citation and called the carrier
both "Ellerman's Pacioli group" and "the free abelian group".
Account →₀ ℚ is nonzero and divisible, so it is not free abelian: it is
the free ℚ-vector space on Account, and it is the signed, single-sided
image of Ellerman's group of T-accounts under his debit isomorphism, not
that group. Earlier units of the implementation plan also added
declarations to this file (trialBalance, zeroRows, one_term_change,
zeroRow_two_accounts, free_double_entry, free_trial_balance,
free_flow_two_accounts, the degeneracy theorems for the distributive
laws and for tensor additivity, comp_not_left_additive and
endpoint_law_refuted) whose place in the module description decision D35
assigns to this documentation unit, and decision D37 requires
Pacioli.lean to be free of em-dashes.

The module docstring is rewritten in four sections. The carrier section
describes PacioliGroup as above. It notes that the debit isomorphism
carries Ellerman's P(N^n) onto Z^n (p. 494), so that the carrier is the
rational, finitely supported analogue of that Z^n, indexed by Account,
and that the names refer to the group without claiming that the carrier
is it. It gives the full citation of the 2014 article with the pages the
analysis verified: p. 486 for the two isomorphisms, p. 488 for a
program's internal mapping under either of them, p. 492 for Ellerman's
adoption of the debit isomorphism, and pp. 493-494 for the vector case.
It states that the debit-positive convention is inherited from the
journal syntax rather than endorsed by the article, citing
doc/ledger3.texi (node "Stating where money goes", line 721 of the C++
checkout: "Ledger uses only subtractions and additions") and the --dc
option, whose manual entry (lines 7501-7558) shows the positive amounts
in the debit column and the magnitudes of the negative amounts in the
credit column. It also states that Driver.flowsFor (Driver.lean:101-103)
sums the amounts of the engine's postings without changing their sign,
and that under the credit isomorphism every printed row would change
sign. The denotation section keeps the existing table. A new section on
balance in every commodity places trialBalance, zeroRows (p. 492),
free_double_entry, free_trial_balance, free_flow_balanced (the
degenerate case of free_ker_balanced), one_term_change (pp. 486-487),
zeroRow_two_accounts and free_flow_two_accounts, calling each theorem
by the name its own docstring gives it, and points to the generator
classes named in the docstring of free_trial_balance without restating
them or calling that list complete. The section on decision D16 of the
worksheet names the removed laws exactly, the two absorption laws and
the two distributive laws of composition, says that additivity of
tensor was never among the laws, and lists every proved witness.

Several sentences of the prepared drafts were narrowed because they
claimed more than is proved. The drafted witness bullet said that the
free model refutes the removed laws, but no refutation of right
distributivity or of 0 ∘ f = 0 is stated. The bullet now names the three
refutations that exist, and a paragraph after the list says that right
distributivity is still not derivable, since in the free model
right_distrib_degenerates would collapse the valuation that
oneFlow_gen_measure shows to be nonzero, and that 0 ∘ f = 0 is excluded
in the same way by the mirror image of zero_absorption_degenerates,
which the file does not state as a theorem. The drafted unitSpec
docstring said that the counterexamples need a flow that no endpoint
constrains; they use one, but a sign-coherent specification with a
nonzero generator would serve as well, so the docstring now says that
they take such a flow, and it cites unitSpec_not_signCoherent together
with Driver.specFor_coherent for the oracle's generators. The docstring
of tensor_additive_degenerates said that it puts tensor additivity
"back", but tensor additivity was never a law: the Phase 2-3 law set
recorded it as a deliberate non-law (C++ commit d55a8228f,
lean/Ledger/Groupoid.lean). It now says "Adding". The PacioliGroup
docstring is replaced by the prepared text, and the four remaining
em-dash docstrings and the contraction in the section on the commuting
squares are rewritten by the prepared edits. The pointer to the
TAccount module is left to the unit that adds that module (decision
D11).

The locked build script, lake build with warnings as errors, exits 0
after 1100 jobs, rebuilding Ledger.Pacioli, with no warning in its log.
The register checker reports Pacioli.lean clean, where it reported 13
findings before the change. The cited-name check resolves every
backticked identifier it selects from the file, 43 in all, among them
Finsupp.basisSingleOne, Driver.flowsFor, Driver.specFor_coherent and
unitSpec_not_signCoherent. After whitespace folding, "free abelian"
occurs 0 times (1 before) and "the Pacioli group" 0 times (2 before).
The environment-wide axiom audit reports 1928 constants in 22 Ledger
modules and none outside propext, Classical.choice and Quot.sound, the
count recorded for the preceding commit. The scan of added Lean lines
finds no forbidden construct, the register check of added lines is
clean, the gate self-test is green, and every C++ test cited by the tree
exists. The oracle's default output over the 3816 positive and the 469
negative journals extracted from the C++ suite is byte-identical to its
output before this change, by cmp against the recorded snapshots. The
bisimulation log against the C++ tree, over 4384 .test files, reports
PASS 3816, FAIL 0, SKIP:negative 489, SKIP:no-journal 77 and
SKIP:python 2, with both detector fixtures green.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0143bpV19FtoLrrR59maPRPk
Item I10 of the Ellerman analysis found that, at the revision it
analysed, the repository realized Ellerman's group of T-accounts only
in its signed, single-sided image: neither of his two isomorphisms was
named, and the pair construction itself was not built. The preceding
commit corrected the Pacioli.lean docstring to describe PacioliGroup
as that debit image (item I4), naming both isomorphisms in prose, and
left the pointer to the pair construction to this commit. Decision D11
of the implementation plan orders the two so that no docstring ever
names a missing module. Landing I10 one commit after I4, rather than
in the same commit as the analysis asked, is a recorded deviation that
keeps both of its aims.

The new module Ledger/TAccount.lean is generic in an index type ι. A
T-account is a pair of finitely supported unsigned rationals,
(ι →₀ ℚ≥0) × (ι →₀ ℚ≥0). CrossEq is the equality by cross-sums of
Table 1 (p. 485), swap is reversal, and reduced is the reduced
representation [x - min(x,y) // y - min(x,y)] in truncated
subtraction. net is the net debit balance [d // c] ↦ d - c of p. 486,
and net_eq_net_iff shows that cross-sum equality is exactly equality
of net balances. reduce is the Jordan decomposition v ↦ [v⁺ // v⁻];
reduced_unique and reduce_net_eq_reduced prove that the reduced form
is unique and that the formula of Table 1 computes it. PacioliP ι is
Mathlib's Algebra.GrothendieckAddGroup of ι →₀ ℚ≥0, and
toP_eq_toP_iff states Table 1 literally: two T-accounts denote one
element of that group exactly when their cross-sums agree. toP_add
and toP_swap are the rows of the same table for addition and for
inverses. debit is the Grothendieck lift of the inclusion of unsigned
into signed quantities, and debitIso makes it an isomorphism onto
ι →₀ ℚ. creditIso is debitIso followed by negation.
debitIso_symm_apply and creditIso_symm_apply give the inverses as
x ↦ [x⁺ // x⁻] and x ↦ [x⁻ // x⁺], the presentation of p. 494.
existsUnique_extension is the universal property of the group, and
accountDebitIso : PacioliP Account ≃+ PacioliGroup holds by
definition, since PacioliGroup is Account →₀ ℚ. The page citations
were checked against the text of the article: Table 1 on p. 485, the
T-account [d // c] and both isomorphisms on p. 486, the single-sided
update "Initial ledger + journal = ending ledger" on p. 492, the
vector isomorphisms on p. 494, "Beginning Ledger + Journal = Ending
Ledger" on p. 496, and note 6 on p. 499, which permits rational
numbers.

The declarations and proofs are those of the prototype that the
analysis checked, whose module docstring the plan's design stage
rewrote; no statement or proof differs from it. This commit changes
its prose in four places. The closing paragraph of the module
docstring now says that no module on the oracle's run path imports
the leaf, and it does not name the GrossLedger join module, which
does not exist yet. The sentence on the oracle no longer says that
the oracle computes in the carrier: PacioliGroup is the carrier of
the denotation, the oracle computes with plain functions into exact
rationals, and the docstring cites netFlow_eq_flowAux, which proves
the two equal account by account. The docstring of
debitIso_beginning_add_journal quoted p. 496 in an abridged form; it
now quotes the phrase exactly, gives the page, and says that the
theorem is the additivity of the debit isomorphism, which carries the
T-account update of p. 496 onto the single-sided update of p. 492.
The docstrings of swap, toP_add and toP_swap now cite the rows of
Table 1 that they state, which the prototype left uncited.

No module imports the leaf, so the import closure of Driver is
unchanged; the lakefile's glob over Ledger builds it. Its entries in
the Ledger.lean index, the README and AGENTS.md are left to the
plan's final inventory unit, as they were for TwoSides. The carrier
section of the Pacioli.lean module docstring gains one closing
paragraph that points to the new module and to accountDebitIso.

The locked build script, lake build with warnings as errors, exits 0
after 1128 jobs, building Ledger.TAccount and rebuilding
Ledger.Pacioli and the modules above it, with no warning in its log.
#print axioms lists only propext, Classical.choice and Quot.sound for
each of 39 declarations of the module, among them toP_eq_toP_iff,
debitIso_symm_apply, creditIso_symm_apply, existsUnique_extension and
accountDebitIso. The environment-wide axiom audit reports 1985
constants in 23 Ledger modules and none outside that budget. The
preceding commit's message records 1928 in 22, and a listing of the
constants of Ledger.TAccount accounts for the 57 new ones as the 48
declarations of the file, seven generated equation lemmas and two
auxiliary proofs. The cited-name check resolves the backticked
identifiers of both files, among them netFlow and netFlow_eq_flowAux,
the register checker reports both files clean, "GrossLedger" does not
occur in TAccount.lean, and the import-direction scan is clean. The
scan of added Lean lines finds no forbidden construct, the register
check of added lines is clean, the gate self-test is green, and every
C++ test cited by the tree exists. The oracle's default output over
the 3816 positive and the 469 negative journals extracted from the
C++ suite is byte-identical to its output before this change, by cmp
against the recorded snapshots. The bisimulation log against the C++
tree, over 4384 .test files, reports PASS 3816, FAIL 0, SKIP:negative
489, SKIP:no-journal 77 and SKIP:python 2, with both detector
fixtures green.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0143bpV19FtoLrrR59maPRPk
Item I12 of the Ellerman analysis found that balance_sheet_equation,
then named pacioli_equation, takes three arbitrary and possibly
overlapping sets of accounts, and that its reading of liabilities and
equity as credit-positive exists only as a convention of its
docstring. Ellerman fixes the reading by a side attached to each
account: p. 486 labels "each balance sheet (and income statement)
T-account as debit-balance or credit-balance", p. 496 partitions the
accounts of the final equation "according to the side of the initial
equation from which they were encoded", and note 11, which begins on
p. 499 and ends on p. 500, adds a credit-balance revenue account and a
debit-balance expense account. The page citations were checked against
the text of the article.

The new module Ledger/Decoding.lean imports Mathlib.Tactic.DeriveFintype
and Ledger.Derived. Side has the two constructors debit and credit, and
Side.decode is the identity on the debit side and negation on the
credit side: measurements in this project are already debit-positive,
so decoding a credit-side account is the change from the debit
isomorphism to the credit isomorphism, which is the definition of
"reported credit-positive". sum_debit_add_sum_credit states that a side
function partitions any finite set of accounts, so coverage and
disjointness hold by construction. decoded_iff_trial shows that, for
every side function, the decoded equation is equivalent to the trial
balance, whose statement does not mention the side; the side chooses
presentation, not truth. decoded_balance_sheet concludes the decoded
equation for every derived flow of a journal with balanced entries.
decoded_of_balance_sheet derives the side-based form from
balance_sheet_equation itself, not from the deprecated alias; its
statement coincides with that of decoded_balance_sheet, and only its
proof differs. Conversely, balance_sheet_of_decoded recovers
balance_sheet_equation for pairwise disjoint classes. The new content
is therefore a definition, not a stronger theorem. AccountClass names
the five classes with their normal sides (asset and expense debit, the
rest credit); classBalance, classValuation and five_class_equation
state A + X = L + E + R for every derived flow; closing_decoded and
closing_classBalance restate closing_transfers_net_income with equity
and revenue reported credit-positive. ChartOfAccounts assigns a side to
each account name, so every commodity of one account decodes alike, and
chart_decoded_balance_sheet states the decoded equation one commodity
at a time.

Side lives in this leaf and not in Ledger/Basic.lean, as the analysis
required, because Basic is imported by Parse and Gen in the executable
layer. The prose says "balance side" rather than "label", because
Ledger/Labels.lean uses "label" for transaction metadata. C++ Ledger
has no account types, so nothing here is bisimulated; no module imports
the leaf, and the oracle is unchanged.

The statements and proofs are those of the prototype that the analysis
checked, with the changes that the plan's design stage made.
decoded_of_pacioli, pacioli_of_decoded and classW are renamed
decoded_of_balance_sheet, balance_sheet_of_decoded and classValuation;
the proof of decoded_of_balance_sheet calls balance_sheet_equation, the
primary name since the rename commit; the prototype's namespace is
dropped, its replayed lakefile options give way to the lakefile itself,
and its two Mathlib imports to the single import
Mathlib.Tactic.DeriveFintype; and the module docstring is rewritten for
a leaf of the repository.

This commit departs from the design in one binder and in docstring
prose. closing_decoded and closing_classBalance now take the category
T implicitly, as closing_transfers_net_income does, since the closing
hypothesis determines it; no other declaration uses either theorem.
The module docstring cited decoded_iff_trial alone for the claim that
the decoded equation holds for every side function, which that theorem
proves only as an equivalence with the trial balance, and it now cites
decoded_balance_sheet as well. It called the side-based form and
balance_sheet_equation interderivable, although the converse holds only
for pairwise disjoint classes, and it now says so. The docstring of
decoded_of_balance_sheet notes that its statement coincides with that
of decoded_balance_sheet. The docstring of chart_decoded_balance_sheet
said that Ledger balances per commodity, which is not so for an
exchange balanced at cost; it now says that Ledger reports balances per
commodity. The docstring of Side names the initial equation, as p. 496
does, the spelling "labelled" becomes "labeled", and the docstring of
balance_sheet_of_decoded is reflowed.

The locked build script, lake build with warnings as errors, exits 0
after 1131 jobs with no warning in its log. The axiom check of the 17
#print axioms records for the new theorems reports every one within
propext, Classical.choice and Quot.sound, and the environment-wide
audit, which covers the new definitions as well, reports 2094
constants in 24 Ledger modules and none outside that budget. The
cited-name check resolves every identifier it selects from the new
file. grep finds no pacioli_equation in Ledger/Decoding.lean and no
inductive Side in Ledger/Basic.lean (exit status 1 for both). The
import scan places Decoding in the specification layer with the single
direct import Derived and reports no specification module that reaches
an executable one; grep finds no module that imports Decoding. Like
TwoSides and TAccount, the leaf is not yet named in the module index,
the README or AGENTS.md, and Ledger.lean does not import it. The scan
exits 1 only for such missing entries, the four of Decoding and those
of the earlier leaves TwoSides, TAccount and DriverTwoSides, which the
plan's documentation commits add. The scan of added Lean lines finds
no forbidden construct, the register check of added lines is clean,
the gate self-test is green, and every C++ test cited by the tree
exists.
The oracle's default output over the 3816 positive and the 469
negative journals extracted from the C++ suite is byte-identical to
its output before this change, by cmp against the recorded snapshots.
The bisimulation log against the C++ tree, over 4384 .test files,
reports PASS 3816, FAIL 0, SKIP:negative 489, SKIP:no-journal 77 and
SKIP:python 2, with both detector fixtures green.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0143bpV19FtoLrrR59maPRPk
Item I7 of the Ellerman analysis found that the specification has no
functional of the form price times quantity. Ellerman values a vector
ledger by a single price vector: on pp. 497-498 "the vector accounts of
property accounting collapse to the scalar accounts of value
accounting", and "Scalar equation = price vector times vector equation".
The valuations of this repository are additive functionals applied after
recording, but none of them is that dot product. Item G6 further
required that the vocabulary the valuation prototypes defined locally
have one owner: two prototypes gave quantityValuation different bodies,
they spelled the commodities of Ellerman's example three ways, and one
prototype's accountValue was another's valueHom. Finally, the docstring
of CommodityValuation read `CV c` as net flow denominated in commodity
c, which runs together two readings that behave differently.

The new module Ledger/PriceVector.lean imports
Mathlib.LinearAlgebra.Finsupp.LinearCombination, Ledger.Pacioli and
Ledger.TimedPrices. priceDot p is the linear combination of the price
vector p composed with trialBalance, an additive map from the carrier
PacioliGroup to the rationals, with lemmas for application, single
postings, subtraction and scaling of the price vector, and
priceDot_of_mem_zeroRows, which states that a zero-row has value zero at
every price vector. value_neutral_not_zeroRow shows that the converse
fails at every price vector; it replaces the prototype's existential,
which the zero price vector satisfied. valueHom p n is the value of the
postings of one account name, and priceDot_eq_sum_valueHom attributes
the ledger-wide value to the account names that cover the support.

On the free model, GenSpec.priceValuation and GenSpec.accountValue are
the instances of ofFlowHom at priceDot p and valueHom p n, and
GenSpec.quantityValuation, the only definition of that name in the tree,
is the family of instances at commodityTotal c. priceValuation is total
value, the net value moved summed over accounts, in the unit in which p
is quoted; the value reading of CommodityValuation, value in the
numeraire c, is its scaling by 1 / p c, and the two take the same values
when p c = 1. The theorems show that total value respects subtraction of
price vectors, that it vanishes on every derived morphism of a journal
whose entries are zero-rows, and that it is the sum of the per-account
values over any names that cover the postings of every generator.
quantityValuation_vanishes records that the quantity reading is
identically zero on such a journal, so it is not a report-grade
measurement. priceVectorValuation weights any quantity family by prices
over a finite commodity set; priceValuation_eq_priceVector identifies
total value with that weighting of the quantity reading whenever the set
covers the commodities of every generator, and
valueIn_eq_quantity_of_single shows that value in the numeraire b equals
the quantity of b moved when every generator moves only b and p b is
nonzero. quantity_priceAction_uninhabited shows that on unitSpec with
oneFlow the quantity reading admits no sound price action converting USD
to EUR; since PriceAction.constModel is sound for the value reading on
the same model, it is a narrow witness that the readings differ.
exchangeReport presents value at a total, nowhere-zero price table as
PriceAction.constModel over the price-vector functional, and its
docstring calls it an idealization, not a bisimulated claim.

EllermanWidgets evaluates the worked example of pp. 496-498 at the
prices (1, 100, 40), with the commodity strings "$", "WIDGET" and "HALF"
that decision D17 fixed; its section docstring maps them, as D17 asks,
to Ellerman's three types of property, "cash, outputs (widgets), and
inputs (half-widgets)" (p. 496), listed in that order in each vector.
assets_value states that these prices value the Assets vector
(10200, 35, 20) at 14500; scalar_equation evaluates the ending ledger
account by account to 14500 for Assets, -9200 for Liabilities and -5300
for Equity, with total 0; txn2b_mem_zeroRows shows that transaction 2b
as the article journals it is a zero-row, and
txn2bAssets_value_neutral_not_zeroRow shows that its Assets leg alone is
value-neutral at these prices without being a zero-row. Replaying
evidence/critic/ell-vector.dat of the analysis, both the oracle of this
tree and ledger 3.4.1 print the ending vectors (10200, 35, 20),
(-9200, 0, 0) and (-1000, -35, -20) for Assets, Liabilities and Equity
under these three commodity names, so the goldens and the printed rows
use the same keys.

The docstring of CommodityValuation in Ledger/Valuation.lean, which
decision D35 assigns to this change, now states both readings: the
quantity reading, which vanishes on a journal that balances per
commodity, and the value reading, value in the numeraire c at a fixed,
nowhere-zero price vector, over which PriceAction.constModel is sound.
It adds that on a journal that moves only commodity c the readings agree
at the member c, and that they differ in general. It cites
quantityValuation and valueIn_eq_quantity_of_single. No other text of
Valuation.lean changes.

The module is the prototype that the plan's design stage consolidated,
with the following changes to its prose. The docstring of
quantityValuation named scopedValuation as the report-grade measurement,
a definition that decision D3 does not ship; it now names accountValue,
per account name. The module docstring called the carrier the Pacioli
group twice, and now says "the carrier `PacioliGroup`" (item I4). It
cited Ledger/Lots.lean, which does not exist until the Lots leaf lands,
and that parenthetical is dropped (critic issue R31); the plan assigns
its restoration to the change that adds Lots.lean.

Two docstrings of the example pointed, in the same way, at artifacts
that later changes add. The section docstring cited the journals that
replay the example, which exist in neither repository at this commit,
and it now gives the D17 mapping instead. The docstring of assets_value
called the derivation of the ending vector by posting a golden of the
executable layer, which does not exist yet, and it now says only that
the theorem does not derive that vector by posting. The prototype wrote
the value-reading family as (1 / p c) scaling priceValuation flows p,
which does not elaborate: p is rational, and a valuation functor admits
only real scalars. The module docstring and the CommodityValuation
docstring now write the cast, (1 / (p c : ℝ)), as the statement of
valueIn_eq_quantity_of_single does. The module docstring no longer says
that both readings are defined here, because the value reading has no
definition of its own; it names the two declarations in which that
reading appears. The docstring of valueIn_eq_quantity_of_single no
longer says that the readings agree exactly on single-commodity
journals, which suggests they agree only there, whereas both vanish on
every journal that balances per commodity; it now states agreement at
the member b alone. The docstring of value_neutral_not_zeroRow drops its
reference to the prototype's existential, which this repository never
contained, and states why the converse fails: at least two commodities
exist, so the functional that p defines on property vectors has a
nonzero kernel, and trialBalance is onto.

The prototype also called priceValuation itself the value reading, in
its docstring, in the heading of its section, and in the docstrings of
priceValuation_sub_prices and priceValuation_eq_priceVector. Under the
module's own definition of that reading as the family scaled by 1 / p c,
the linearity docstring was false, and the bridge docstring was off by
that factor at any numeraire whose price is not 1. These now say total
value; the docstring of priceValuation defines that term as the net
value moved, summed over accounts, and states how the value reading is
obtained from it. The docstring of valueHom_sub_prices, like those of
the priceDot lemmas, now names the law it proves, difference, rather
than claiming linearity outright. The docstring of
valueIn_eq_quantity_of_single now names its hypothesis that p b is
nonzero, and that of quantity_priceAction_uninhabited says that it
witnesses the difference of the readings only together with constModel.
The module docstring said that everything in it is noncomputable, but
priceVectorValuation and EllermanWidgets.price are compiled; it now says
so of priceDot, valueHom and the free-model functors built on them.

The CommodityValuation docstring departs from its draft in the cast, in
the scope of the agreement, and in one phrase: the draft said "a fixed
price vector", but constModel requires every price to be nonzero, so the
text says "a fixed, nowhere-zero price vector". No module imports the
leaf yet; Ledger.lean does not import it, as for the earlier leaves. The
only edit inside the oracle's import closure is the docstring of
CommodityValuation, and the oracle's output is unchanged.

The locked build script, lake build with warnings as errors, exits 0
after 1219 jobs with no warning in its log. The axiom check of the 25
#print axioms records, one for every theorem of the module, reports each
within propext, Classical.choice and Quot.sound, and the
environment-wide audit, which covers the new definitions as well,
reports 2148 constants in 25 Ledger modules and none outside that
budget. The cited-name check resolves every identifier it selects from
Ledger/PriceVector.lean and Ledger/Valuation.lean. grep finds no
scopedValuation under Ledger/ (exit status 1), and a whitespace-folded
count finds "the Pacioli group", Lots.lean, "golden", "replay" and the
uncast term (1 / p c) zero times in the new module, and the uncast term
zero times in Ledger/Valuation.lean. Scratch examples proving
priceValuation flows p = ofFlowHom flows (priceDot p) and accountValue
flows p n = ofFlowHom flows (valueHom p n) by rfl typecheck with exit
status 0, as does a second scratch file in which the value-reading
family, written with the cast, elaborates as a CommodityValuation and
PriceAction.constModel over it typechecks at a nowhere-zero price
vector. A third scratch file, also with exit status 0, shows that the
value reading's member c takes the same values as priceValuation when
p c = 1 and a different value at p = 2 on oneFlow, that the conclusion
of valueIn_eq_quantity_of_single fails at p b = 0, and that constModel
is a sound price action for the value reading on unitSpec with oneFlow
over a groupoid that has a USD to EUR conversion; there Lean reports
priceDot, valueHom, priceValuation, accountValue, quantityValuation and
exchangeReport noncomputable, and priceVectorValuation and
EllermanWidgets.price not. The import scan is clean, and the closure
check of Ledger.Driver reports Ledger.PriceVector and
Mathlib.LinearAlgebra.Finsupp.LinearCombination outside the run path.
The scan of added Lean lines finds no forbidden construct, the register
check of added lines is clean, the gate self-test is green, and every
C++ test cited by the tree exists. The oracle's default output over the
3816 positive and the 469 negative journals extracted from the C++ suite
is byte-identical to its output before this change, by cmp against the
recorded snapshots. The bisimulation log against the C++ tree, over 4384
.test files, reports PASS 3816, FAIL 0, SKIP:negative 489,
SKIP:no-journal 77 and SKIP:python 2, with both detector fixtures green.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0143bpV19FtoLrrR59maPRPk
Item I14 of the Ellerman analysis found that timedConstRates, the only
time-indexed price model in the specification, fixes one functional W
for all instants and measures commodity c by (1 / priceAt t c) • W. A
holding's value in a numeraire of constant price therefore never
changes: the model varies the unit of measurement, and no holding gains
or loses value between two instants. Ellerman applies one price vector
once (pp. 497-498), so the article states no revaluation identity; the
analysis proposed one as an extension of it, anchored in his "add on a
zero-account or zero-term" (p. 496). The prototype of the item also
found that an equality of valuation functors cannot be stated
conveniently without an extensionality lemma for ValuationFunctor.

Decision D13 places that lemma, ValuationFunctor.ext_V, in
Ledger/Derived.lean, in the section on the valuation-functor algebra,
before def neg: two valuation functors with the same measurement are
equal, because the three laws are propositions. Derived.lean lies
outside the import closure of Ledger.Driver, so the oracle does not
load it.

The new module Ledger/HoldingGain.lean imports Ledger.PriceVector and
nothing else. timedConstRates_no_holding_gain records the blind spot:
with a numeraire held at price 1, the value of every morphism is the
same at every instant. As decision D5 requires, it is derived from
timedConstRates_value_const, which the correction of the TimedPrices
docstring added, by rewriting that theorem at the two instants, and is
not proved a second time. GenSpec.ofFlowHom_sub states that flow
valuations subtract as their functionals do. timedPriceVector is the
timed model whose values move: at instant t, commodity c measures the
whole flow at the price vector priceAt t, in units of c, and the action
of each fiber is PriceAction.constModel over GenSpec.priceValuation
flows (priceAt t), so soundness at each instant is inherited.
timedPriceVector_CV writes the fiber measurement as a sum over
accounts, and timedPriceVector_CV_eq identifies it with the price-vector
valuation at the numeraire-relative prices priceAt t d / priceAt t c.
holding_gain_functor states the gain between two instants as an
equality of valuation functors, the price-vector valuation at the
change in numeraire-relative prices, with no hypothesis on the price of
the numeraire. holding_gain and holding_gain_const are its forms for a
numeraire held at price 1 and at a constant price k, and
accountValue_sub is the form for one account name.
holding_gain_balanced shows that the whole-flow gain vanishes when every
generator is a zero-row; its docstring adds that the whole-flow value
is then zero at every instant. The content of the identity therefore
lies in the per-account form and in journals that balance at cost.

scalarize maps a vector flow to its scalar accounts at prices p, in
signed form and all in the numeraire c, and scalarize_apply,
trialBalance_scalarize and scalarize_sub_prices give its value per
account name, its trial balance and its dependence on the price vector.
By the last, for every flow the scalar accounts at new prices are those
at old prices plus the revaluation. revaluation_mem_zeroRows adds that
on a flow that is a zero-row the revaluation is itself a zero-row, so on
such a flow revaluation is the addition of a zero-row: the signed form,
under the debit isomorphism, of Ellerman's operation, in a use the
article does not make. revaluation_mem_zeroRows_flow is the same for
every derived morphism of a journal whose generators are zero-rows. On a
flow that balances only at cost the revaluation need not be a zero-row:
by trialBalance_scalarize its trial balance is the value of the flow at
the price change, booked in the numeraire, which with the numeraire held
at price 1 is the holding gain.

Two examples show that the identity is not vacuous in either setting.
HoldingGainExample.cost_exchange_holding_gain takes one generator that
buys 1 AAPL for 100 USD; AAPL moves from 100 to 120 with USD as the
numeraire at price 1, and the holding gain is 20. That generator is not
a zero-row, which is the shape of Ledger's @ exchanges. The second
example transfers 1 AAPL from Equity to Assets:Broker, a zero-row by
balancedFlow_mem_zeroRows, so its whole-flow gain is zero; yet under
the same price change balanced_account_holding_gain finds that the
value it books in Assets:Broker rises by 20, and
balanced_revaluation_apply finds that its revaluation books 20 USD
there. The per-account form and revaluation_mem_zeroRows are therefore
not vacuous on a journal that balances per commodity.

The module is the one the plan's design stage prepared, with changes to
its prose and one addition. It spelled "fibre" in the module docstring
and in three declaration docstrings, and now spells "fiber", as
TimedPrices.lean and Decoding.lean do. Its module docstring cited
timedConstRates_no_holding_gain for the claim that value in a numeraire
of any constant price never changes, but that theorem assumes price 1;
the docstring now cites timedConstRates_value_const for the claim and
names timedConstRates_no_holding_gain as its case at price 1. The module
docstring also said that a placeholder price for an unpriced commodity
leaves differences unchanged when the placeholder does not move.
holding_gain_functor depends on prices only through the change in
numeraire-relative prices, so when the price of the numeraire moves, the
gain depends on the value chosen for a placeholder even if that value is
the same at both instants. Nor does the claim hold when the unpriced
commodity is the numeraire itself: a placeholder k held at both instants
never moves relative to the numeraire, yet it divides every gain by k,
as holding_gain_const states. The sentence now covers only commodities
other than the numeraire, asks that the placeholder not move relative to
the numeraire, and states the numeraire case separately. Four docstrings
called the output of scalarize scalar T-accounts. Ledger/TAccount.lean
reserves that name for the unsigned pairs [d // c], and Pacioli.lean
says that the carrier keeps only each pair's signed difference, so they
now say scalar accounts, Ellerman's term on pp. 497-498, and the
docstring of scalarize says that the form is signed. For the same reason
the module docstring now calls the scalar zero-row the signed form,
under the debit isomorphism, of Ellerman's zero-account or zero-term, as
Pacioli.lean words the correspondence for zeroRows. The docstring of
revaluation_mem_zeroRows put p. 496 on the whole claim and made the
decomposition at new prices conditional on a zero-row, although
scalarize_sub_prices proves it for every flow; it now ties p. 496 to the
operation alone, says that the article does not apply it to revaluation,
and states the decomposition for every flow. Its bold title, the heading
of the revaluation section and the module docstring said without
qualification that revaluation is the addition of a scalar zero-row,
although revaluation_mem_zeroRows proves it only for a flow that
balances per commodity, and on the cost exchange of the first example
the revaluation has trial balance 20 USD. The module docstring now
states the decomposition for every flow, restricts the zero-row claim to
flows that balance per commodity, and gives the trial balance on a
cost-balanced flow; the title and the heading carry the same
restriction. scalarize_sub_prices, examplePrice_ne and
cost_exchange_holding_gain gain the docstrings they lacked. The addition
is the second example: the module docstring said that the content of the
identity lies in the per-account form, and no checked statement showed
that form nonzero on a journal that balances per commodity; it now cites
the example. TimedPrices.lean is not touched: decision D16 assigns the
pointer from its docstring to this module to a later change. No module
imports the leaf yet, and Ledger.lean does not import it, as for the
earlier leaves.

The locked build script, lake build with warnings as errors, exits 0
after 1220 jobs with no warning in its log. The axiom check reports the
one #print axioms record for ValuationFunctor.ext_V and the sixteen
records the plan lists for the theorems of the new module, each within
propext, Classical.choice and Quot.sound; a scratch copy of that list
extended by the three theorems of the second example reports nineteen
records within the same budget. The environment-wide audit, which covers
the new definitions as well, reports 2187 constants in 26 Ledger modules
and none outside that budget. The cited-name check resolves every
identifier it selects from Ledger/Derived.lean, and every one from
Ledger/HoldingGain.lean except the bound variable priceAt, which its
allowlist records as a local name. grep finds exactly one import line in
the new module, import Ledger.PriceVector. Three scratch files typecheck
with exit status 0 and no output. They prove that two price tables
differing only at a commodity other than the numeraire, whose price does
not move relative to the numeraire in either table, give the same
holding gain; that a placeholder k for the numeraire, held at both
instants, gives 1 / k times the gain that the placeholder 1 gives, and
that on the cost exchange the placeholder 2 for USD gives 10 in place of
20; that on a journal of zero-rows each side of holding_gain_balanced is
zero; that each fiber's action is constModel over priceValuation by rfl;
that value in a numeraire held at any constant nonzero price is the same
at every instant of timedConstRates; that the decomposition at new
prices holds for every flow; that with the numeraire held at price 1 the
trial balance of the revaluation, read in the numeraire, is the holding
gain; and that on the cost exchange, which is not a zero-row, the
revaluation has trial balance 20 USD and so is not a zero-row. The scan
of added Lean lines finds no forbidden construct, the register check of
added lines is clean, the gate self-test is green, and every C++ test
cited by the tree exists. The oracle's default output over the 3816
positive and the 469 negative journals extracted from the C++ suite is
byte-identical to its output before this change, by cmp against the
recorded snapshots. The bisimulation log against the C++ tree, over 4384
.test files, reports PASS 3816, FAIL 0, SKIP:negative 489,
SKIP:no-journal 77 and SKIP:python 2, with both detector fixtures green.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0143bpV19FtoLrrR59maPRPk
The Ellerman analysis found, at the revision it examined (f5d891e),
that AGENTS.md said lot matching is specified in the theory while no
specification module mentioned lots (item I8), and its item I16
proposes the refinement that gives lots a place in the specification.
The oracle is lot-blind: lot prices enter the balance check, but
parseAmountField drops the fixated flag that annotLoop returns when it
books the lot value, and the flows binding of finalizeXact records
every amount under its bare commodity. Ellerman's note 20 (text on
p. 500, marker on p. 498) says that valuing produced goods at cost
"could be accommodated in property accounting at the cost of treating
'Produced Widgets' as a different form of property from 'Sold Widgets'
(so each type of property has a single unit value)", and sets the
refinement aside. Treating Ledger's lots the same way is an extension by
analogy, not the article's claim. Item G6 further required that the
revaluation work built later use I16's structure (base, lot, value
expression, with a fixated lot valued at its lot price, as C++ values
it) and not the stub of I17, in which every lot floats.

The new module Ledger/Lots.lean imports Ledger.PriceVector and nothing
else. LotPrice records a lot price's commodity, its per-unit price and
whether it is fixated. LotRefinement has exactly three fields: base, the
commodity a lot annotates (C++ referent()), lot, the lot price if there
is one, and valueExpr, whether a valuation expression applies to the
commodity. A refinement describes the commodities of one journal, so a
hypothesis over every commodity constrains only those the journal uses.
A lot is thus a property type in its own right, an annotated commodity
string, so refined flows stay in Account →₀ ℚ and add freely, which
keeps the refinement clear of note 18's warning about database fields.
forget is the group homomorphism that replaces each commodity by its
base; priceDot_forget prices through it, and flow_forget, an instance of
GenSpec.flow_map, states that the flow denotation commutes with it.
marketVector gives precedence to fixation, then to a valuation
expression, then to the base commodity's price. Floating says that a
commodity has no fixated lot and no valuation expression, and
market_of_lotBlind, the scoping theorem, states that under -X the market
value of any morphism's refined flow is computable from its lot-blind
flow when every commodity floats. xTarget and marketX give -X its
vector-valued form, in which a fixated lot keeps its value in its lot
price's commodity, and marketX states when it models C++: where C++
finds every price it needs in the target, the idealization unfixedPrice
states, and then under three conditions, namely that prices are read in
the target, so that the target's own price is 1, that every commodity
whose base is the target floats, and that every valuation expression
evaluates to an amount in the target. LotsIn is the single-unit
hypothesis, and marketX_of_lotsIn reduces the vector report to the
scalar priceDot under it, as marketX_of_floating, through
xTarget_of_floating, does when every commodity floats. vTarget and
marketV give plain -V. basisVector, gainVector, gain_fixated_zero, gainG
and gainG_of_basis_cost model -B and -G, the latter as C++ computes it
when -X tgt precedes -G, under the conditions of marketX: market value
minus a separately booked cost that is not revalued. They also state a
sufficient condition for that to be the scalar gain functional.
marketVector_sub_of_not_floating and marketVector_sub_of_floating show
that motion of the price table, with the valuation expressions held
fixed, is invisible on a commodity that does not float and equals the
change in its base's price on one that does.
LotExamples.basis_not_factor, fixated_not_factor and plainV_not_factor
show that -B, fixated lots under -X, and plain -V need refined flows.

The module docstring says that the module specifies lot identity and
valuation and does not specify lot matching, that its theorems prove
linear algebra only, and that each correspondence to a C++ report is an
observed behavior cited beside the definition that models it, together
with the conditions under which it holds. Those citations are at C++
revision 76b9dc2b2, and each was read there: annotate.cc:169 sets the
fixated flag for {=P} and {{=T}}, textual_xacts.cc:713 and
xact.cc:816-819 carry a fixated cost into a fixated annotation,
annotate.cc:403-409 and amount.cc:1270-1275 value a fixated lot at its
lot price and, under plain -V, send a floating lot to its lot price's
commodity, annotate.cc:417-418 applies a valuation expression before the
price graph, as commodity.cc:206-207 does for a value directive of the
base commodity, post.cc:1056-1106, run for each posting at
journal.cc:585, writes a valuation expression taken from a tag, a
directive or a session option into the annotation of the posting's
commodity, commodity.cc:116-155 evaluates one in the default scope and
reprices a basket result, amount.cc:1323-1327 places the value in the
commodity of the price point, amount.cc:1279-1280 returns an amount
whose base commodity is the target in the target before a fixated lot
price or a valuation expression is used, amount.cc:927 leaves the
rational untouched when rounding, report.h:607-610 and 925-956 define -B
and -G, report.h:853-861 and expr.cc:333-349 apply the pieces of a
display expression in option order, report.h:903-909 and 1034-1040 make
-X turn on the market piece of -V, report.cc:947-951 falls back to
value(moment) when there is no exchange commodity, report.cc:957-970
values a single annotated amount whose lot price is in the -X target at
its lot price when no price is found, xact.cc:693 skips the gain fold
when the lot price and the cost are in different commodities,
xact.cc:696-729 folds the rounded gain into the posting cost, pool.cc
rounds per-unit prices in exchange, and post.cc:400-418 is get_cost. The
fixated cost syntaxes cite test/regress/1794.test. References into the
executable layer name the annotLoop branches and the finalizeXact
binding, never a line of Ledger/Journal.lean.

The module is the one the plan's design stage consolidated, with these
changes. It spelled "behaviour" and "modelled", and now spells
"behavior" and "modeled", as the rest of the tree does. It cited the
fixated-cost witness as a bare 1794, and now gives the path of the C++
test, which the citation check verifies. The docstring of basisVector
labeled it -B without saying what C++ reports; it now cites
report.h:607-610, where -B reports rounded(cost), and get_cost, which
reads the booked cost when a posting has one, and says that the two
agree where that cost equals the lot price times the quantity, the
condition gainG_of_basis_cost assumes. The same docstring gave the
scalar reading's condition as the single-unit hypothesis, a name the
module gives to LotsIn alone, and now states both parts as a sufficient
condition: every lot price in the target, and every unannotated holding
the target itself. The docstring of gainG_of_basis_cost said that -G is
the scalar gain functional only under its hypotheses, a necessity the
theorem does not prove; it now says under them. A doubled blank line is
removed.

The first review of the change found that the docstring of gainG, which
called it -G as C++ computes it, is contradicted by C++. On a journal
holding one W {10 EUR} lot booked at 10 EUR, with W priced at 15 USD and
EUR at 2 USD, both ledger 3.4.1 and a build of the C++ tree print -2 EUR
under plain -G, USD-5 under -G -X USD, and -10 EUR with USD15 under -X
USD -G. Only the last is the model's value, which a scratch file proves
to be 15 USD minus 10 EUR. The docstring now names that invocation and
states the other two with the source locations that explain them: plain
-G places the market leg as vTarget does, and under -G -X tgt the market
piece of -X applies last and revalues a booked cost held in another
commodity. On a USD lot booked at 10 USD, all three invocations print 5
USD; that is the regime of gainG_of_basis_cost, and its docstring now
says that the orders agree there. The same review found three sentences
that claimed more than their statements prove, and they are corrected.
marketX_of_lotsIn said that the scalar reading is wrong without LotsIn;
it can fail, but it also holds when every commodity floats, which
xTarget_of_floating and marketX_of_floating, added here, prove.
basisVector said "only when". marketVector_sub_of_not_floating did not
say that the valuation expressions are held fixed, although C++
evaluates them in the default scope and reprices a basket result at
market, so their values can move with prices. The LotRefinement
docstring now states the one-journal reading, which the hypothesis that
every commodity floats needs, since a refinement that read annotations
off every string would never satisfy it. The docstring of widgetLots
says that its constant base is why widgetLots_forget holds for any two
commodities and that the counterexamples use it only on lots of W. The
vTarget citation is narrowed to amount.cc:1274-1275, where the floating
branch is, and the scoping-theorem label moves to market_of_lotBlind,
which the module docstring already names in that role.

A second review found that the docstrings of xTarget and marketX, which
said that every unit other than a fixated lot is converted into the
target and called marketX -X tgt as C++ reports it, hold only under
conditions they did not state, and that gainG and gain_fixated_zero
repeated the claim. C++ returns an amount whose base commodity is the
target in the target, unconverted, before it uses a fixated lot price or
a valuation expression (amount.cc:1279-1280), and it places a valuation
expression's value in the commodity of the expression's result. Both
ledger 3.4.1 and a build of the C++ tree print 100 EUR under -X EUR for
100 EUR {=1.10 USD}, where the model gives 110 USD; 100 EUR with
USD-110.00 under -X EUR -G for the same lot, where the model's gain is
zero; 1 W under -X W for 1 W {10 USD} ((20 EUR)); and 20 EUR under both
-X USD and plain -V for that holding, where the model places the value
in USD. A scratch file of the review, which typechecks against this
tree, proves the model's 110 USD and zero gain. No statement or proof
changes. The marketX docstring now states the three conditions given
above, with their citations; gainG models -X tgt -G under them;
gain_fixated_zero says that the zero is the model's per-unit gain and
that C++ reports the lot's quantity under -X of its base commodity; and
vTarget says that a valuation expression's value lands in the commodity
of the expression's result. market_of_lotBlind and the module docstring
add that prices are read in the target, the one condition that floating
commodities do not already meet. A new scratch file proves that
condition's effect: with prices read in EUR, the model's -X EUR report
of 100 EUR {1.10 USD} is the 100 EUR that C++ prints, and with a table
that gives EUR the price 6/5 it is 120 EUR.

The same review found three smaller overstatements. The valueExpr
docstring named only the ((expr)) annotation, although a value directive
in a commodity block also overrides the price graph
(commodity.cc:206-207): with W given the value 20 USD and priced at 15
USD, both binaries print 20 USD under -X USD. C++ also writes into the
annotation of each posting's commodity, after balancing, a valuation
expression taken from a Value tag, from a value directive of the
account, the commodity or the whole journal, or from the matching
session options (post.cc:1056-1106, called at journal.cc:585). The
docstring now names these sources, and those of LotRefinement,
unfixedPrice and marketVector_sub_of_not_floating speak of a valuation
expression rather than a ((expr)) annotation. The docstring of
gainG_of_basis_cost said that the booked cost equals the basis value at
acquisition, which fails for an acquisition booked in another commodity,
since xact.cc:693 then skips the gain fold: both binaries print 5 EUR
under -B for 1 W {10 USD} @@ 5 EUR. It now names the cases in which the
hypothesis holds and that one in which it fails. The AGENTS.md clause
said "lot refinement with its market valuation", the phrase the plan
keeps out of that binding file because comparator authors could read it
as a claim about C++ -V and -X (decision D41), and it now says "lot
refinement with its valuation".

A third review found four smaller points. The witness of
LotExamples.fixated_not_factor priced every commodity at 15, so its
table did not read prices in USD, the first of the three conditions of
marketX, although the counterexample does not depend on that. The
witness now prices W at 15 and every other commodity at 1; the statement
is unchanged, the same norm_num call closes the proof, and the docstring
says that the example sits within the conditions of marketX with target
USD. No other statement or proof changes. The marketX docstring said
that the early return of amount.cc:1279-1280 comes before a fixated lot
price is read, but amount.cc:1270-1272 reads that price into the price
point first and the return skips only its use, so the docstring now says
"used". The idealization that C++ finds every price, which only
unfixedPrice stated, is now named in the marketX docstring as well, and
unfixedPrice now says that when no price is found C++ still values a
single annotated amount whose lot price is in the -X target at its lot
price times its quantity (report.cc:957-970). Both binaries show it on a
journal holding 2 V {10 USD} and 1 Z {3 EUR} with no price directive:
bal -X USD prints USD20 for the V holding alone, and 2 V with 1 Z,
unconverted, when both holdings make up the account's total, and the
register prints the V posting as USD20. The docstrings of gainG and
marketVector_sub_of_not_floating, which kept ragged line breaks from the
earlier edits, are reflowed, and basis_not_factor speaks of lots at
different lot prices rather than at different bases, a word that
collided with the field base.

Three residual critic issues of the plan are resolved here. The module
docstring of Ledger/PriceVector.lean dropped its pointer to
Ledger/Lots.lean while that file did not exist (R31), and the pointer is
restored after "ignores fixated lot prices". AGENTS.md said that
register rows, market valuation and lot matching are specified in the
theory, which the new module docstring contradicts (R18, and the
report's instruction for item I8 to correct AGENTS.md now). Its limits
paragraph now names what is specified at this commit (register rows,
valuation at fixed prices, price-vector valuation, holding gains at
dated prices, and lot refinement with its valuation), says that none of
it is bisimulated, and says that lot matching is not specified at all
and that the oracle's flows do not distinguish lots. That sentence is
the one drafted for the later coverage change; the clauses on the opt-in
output flags and on basis gain stay with later changes, because neither
exists yet. The paragraph's opening sentence is unchanged. The
acceptance criterion that LotRefinement has only the fields base, lot
and valueExpr, which the plan did not check (R32), is checked by
printing the structure with awk.

The locked build script, lake build with warnings as errors, exits 0
after 1221 jobs with no warning in its log. The axiom check reports
eighteen #print axioms records, the sixteen the plan lists for the
module and the two theorems added in review, each within propext,
Classical.choice and Quot.sound; the environment-wide audit, which
covers the new definitions as well, reports 2272 constants in 27 Ledger
modules and none outside that budget. The cited-name check resolves
every identifier it selects from Ledger/Lots.lean and
Ledger/PriceVector.lean: the names in the LotRefinement and LotExamples
namespaces by qualified name, and the rest as C++ identifiers or private
definitions of the executable layer that its allowlist records. grep for
a Journal.lean line number in the new module exits 1 with no output, and
the awk print of the structure shows the fields base, lot and valueExpr
and no other. The register check reports AGENTS.md, Ledger/Lots.lean and
Ledger/PriceVector.lean clean. Three scratch files typecheck with exit
status 0 and no output. The first proves that under -X EUR the vector
report of one fixated USD lot differs from the scalar priceDot placed in
EUR, the case the docstring of marketX_of_lotsIn warns about; that the
example refinement fails LotsIn "USD"; that the fixated lot's market
value does not move with prices; and that LotRefinement's anonymous
constructor takes the three fields. The second proves the model's value
on the W {10 EUR} lot above, the scalar reading of marketX for a
refinement whose only lot floats in EUR and which fails LotsIn "USD",
the -X value of a morphism computed from its lot-blind flow, and the 5
USD gain on the USD lot. The third is the one on prices read in EUR
described above. The import scan is clean. The scan of added Lean lines
finds no forbidden construct, the register check of added lines is
clean, the gate self-test is green, and every C++ test cited by the tree
exists. The oracle's default output over the 3816 positive and the 469
negative journals extracted from the C++ suite is byte-identical to its
output before this change, by cmp against the recorded snapshots. The
bisimulation log against the C++ tree, over 4384 .test files, reports
PASS 3816, FAIL 0, SKIP:negative 489, SKIP:no-journal 77 and SKIP:python
2, with both detector fixtures green.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0143bpV19FtoLrrR59maPRPk
The Ellerman analysis, at the revision it examined (f5d891e), found that
Ledger's @ syntax leaves implicit the part of Ellerman's Equity leg that
crosses commodities. In transaction 2b (p. 497) the article posts Assets
[(2000,0,0)//(0,20,0)] together with Equity [(0,20,0)//(2000,0,0)], so
the entry is a zero-term in every property type, while the same entry in
cost syntax leaves per-commodity totals of $2000 and -20 WIDGET on both
the oracle and C++. Its item I19 proposes to state the completion
formally, as the idempotent projection id - conversionLeg o
trialBalance, and to locate the accounting content of the completed
journal in a price hypothesis and a freshness hypothesis, since
per-commodity balance of the completed journal holds for every flows
assignment. Item I17 contributes the statements about completing legs
that hold for any market vector, conversion_value and
completed_revalues_free; its statements over lot refinement belong to a
later BasisGain leaf. The plan's decision D14 drops I19's section 5
(pricedEval and the two K3 theorems), which the prototype's own header
called redundant and one of which used a deprecated alias.

The new module Ledger/Conversion.lean imports Ledger.PriceVector and
nothing else. conversionLeg places a per-commodity residual on the
accounts of one name, with its trial balance and its price proved; the
conversion leg of a vector is its residual so placed, and
equityConversion is the equity leg, its negation, which cancels the
residual; add_equityConversion_mem_zeroRows says that adding it yields a
zero-row. completion is id - conversionLeg o trialBalance, which
subtracts the conversion leg and so adds the equity leg; completion_idem
proves it idempotent, completion_range proves its range to be zeroRows,
completion_ker proves its kernel to be the range of conversionLeg, and
completion_erases_stale exhibits a nonzero entry on the conversion
account that completion erases, which is why conversion_account_value
assumes freshness. withConversion completes every generator, and its
balance theorem, flow_withConversion and completed_trialBalance hold for
every flows assignment; their docstrings say that they are facts about
the projection and not accounting facts. completed_revalues_free states
that a journal completed by any leg assignment is worth zero at every
price vector, and conversion_value (I17) that the completing legs are
worth minus the journal there. EntryPrices carries per-entry prices with
a positivity field, CostBalanced says that each entry balances at its
own prices, conversionLeg_value_zero_at_cost and
conversionLeg_value_revaluation value the conversion leg at those prices
and at any other, and residual_exchange, which uses the positivity
field, proves that a cost-balanced entry that is not a zero-row gives up
one commodity and receives another. freshFor is the subgroup of vectors
that post nothing to the conversion name, and conversion_account_value
and its per-entry form state that under freshness the conversion account
records minus the journal's value, which per cost-balanced entry is
minus its revaluation. EllermanWidgets.txn2b_completion recovers
transaction 2b from its Assets leg, txn2b_leg_value_zero values the
Equity leg that completion adds at zero at the prices (1, 100, 40) of p.
498, and txn2b_equity_at_cost values Equity at -400 under a cost
valuation of widgets by their inputs, the practice that note 20 sets
aside. CmdEquity.opening_leg replays the first test equity block of
test/baseline/cmd-equity.test as the equity leg of the journal's flow.
The tactic push Not is used and push_neg is not.

The module is the file the plan prepared, with docstring corrections
made after checking them against the article and the C++ source at
76b9dc2b2, and with one theorem, txn2b_leg_value_zero, restated after
review; no other statement or proof changed. The module docstring said
that every transaction of the article posts against Equity, but
transaction 3, the loan payment, posts Assets against Liabilities; it
now says that every transaction except 3 posts against Equity, quotes
the article's reason (it deals "for simplicity, directly with equity
rather than using 'income statement' or property-flow accounts"), and
concludes only that 2b's equity leg is the counterpart that transactions
1 and 2a also carry. The docstring of completed_revalues_free called its
content Ellerman's point that revaluation of a property-balanced journal
is free, but the article does not discuss revaluation (grep of the
article's text for "revalu" finds nothing); it now derives the fact by
linearity from the article's zero-terms and says that the reading is the
module's. EntryPrices now says that C++ rejects a negative cost
(textual_xacts.cc:696-697) but accepts a zero one, so the positivity
field narrows what C++ admits, and CostBalanced cites xact.cc:325, where
the balance check values a costed amount at its cost, and states the
condition under which it models that check. The docstrings of costPrice
and CmdEquity.journalFlow now say that only input costs are counted and
that the flow is the oracle's balance report for the journal, with the
signs stated on the accounts.

Review found four more places where the prose said more than, or other
than, the statements. txn2b_leg_value_zero was stated about
conversionLeg "Equity" (trialBalance txn2bAssets), the negation of the
Equity leg its docstring named, and was true only because a zero value
does not depend on sign; it is now stated about equityConversion
"Equity" txn2bAssets, the leg that txn2b_completion shows completion
adds, and its proof still reduces to
txn2bAssets_value_neutral_not_zeroRow through priceDot_conversionLeg.
The module docstring now distinguishes the conversion leg, which
completion subtracts, from the equity leg, which it posts, and the
docstring of conversionLeg_value_revaluation says that the posted leg is
worth minus the revaluation, which is what conversion_account_value_gen
states a fresh conversion account records. The docstring of
completion_erases_stale opened with "Freshness is necessary", which that
theorem, an erasure witness, does not state; it and the docstring of
completion_ker now say what the theorem shows and that this is why
conversion_account_value assumes freshness. The witness paragraph for
C++ equity said that an unfiltered report books the negation of the
journal's per-commodity totals; it now says under default options, cites
filters.cc:1610-1611, where balances accumulated from virtual postings
that need not balance are left out of the total, filters.cc:1619-1624
and 1534-1535, which book one negated posting per commodity, and
filters.h:1096-1097, which creates Equity:Opening Balances, and names
the two options that change the booking: --invert negates each account's
balance before the total is formed (filters.cc:1564 and 1571-1572), and
a lot option such as --lot-prices keeps the lot annotations the report
otherwise strips (filters.cc:1568). The docstring of residual_exchange
now says that the residual whose coordinates share a sign is nonzero.

The locked build script, lake build with warnings as errors, exits 0
after 1222 jobs with no warning in its log. The axiom check reports the
32 #print axioms records the plan lists for the module, each within
propext, Classical.choice and Quot.sound; the environment-wide audit,
which also covers the new definitions, reports 2338 constants in 28
Ledger modules and none outside that budget. The cited-name check
resolves every identifier it selects from the module, with no allowlist
entry. grep for push_neg or pricedEval in the module exits 1 with no
output. On test/baseline/cmd-equity.test, equity from the C++ build and
from ledger 3.4.1 both book -4 AAA and 3.80 GBP to Equity:Opening
Balances, and with --invert both book 4 AAA and -3.80 GBP there; the
oracle, run on the journal part of that file, reports
AAA|4|Assets:Broker and GBP|-3.8|Assets:Bank, the flow
CmdEquity.journalFlow states. On transaction 2b in cost syntax
(Assets:Cash $2000 against Assets:Widgets -20 WIDGET @ $100), C++ equity
books $-2000 and 20 WIDGET to Equity:Opening Balances, which is the
Equity leg that txn2b_completion adds, and the oracle reports
$|2000|Assets:Cash and WIDGET|-20|Assets:Widgets. A scratch file
typechecks with exit status 0: it restates the range and the kernel of
completion, derives completion o completion = completion as an equation
of homomorphisms from completion_idem, and shows that without positivity
the conclusion of residual_exchange fails, since a one-commodity
residual balances at the zero price vector. A second scratch file,
written in review, typechecks with exit status 0: it shows that the
hypotheses of residual_exchange are jointly satisfiable on the Assets
leg of 2b, and that an entry of 1 USD posted to Equity completes to an
entry worth 0 on Equity at the unit price vector, not the -1 that the
conclusion of conversion_account_value would give. The import scan is
clean, and no module imports Ledger.Conversion. The scan of added Lean
lines finds no forbidden construct, the register check of added lines is
clean, the gate self-test is green, and every C++ test cited by the tree
exists. The oracle's default output over the 3816 positive and the 469
negative journals extracted from the C++ suite is byte-identical to its
output before this change, by cmp against the recorded snapshots. The
bisimulation log against the C++ tree, over 4384 .test files, reports
PASS 3816, FAIL 0, SKIP:negative 489, SKIP:no-journal 77 and SKIP:python
2, with both detector fixtures green.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0143bpV19FtoLrrR59maPRPk
The Ellerman analysis, at the revision it examined (f5d891e), proposed
in its item I17 to name market value minus basis value the basis gain
and to show that the equity leg an exchange leaves out is forced to be
worth minus that gain. Its prototype was checked against a stub of
LotRefinement whose market vector read every lot at its base
commodity's price, so every lot floated, contrary to the refinement of
item I16, now Ledger/Lots.lean, in which fixated lots and valuation
expressions keep their own value, as C++ keeps them. Item G6 therefore
asks that I17 be restated over I16's structure: the statements that
hold for any market vector carry over, the price-motion law is proved
over refined flows without hypothesis, the coarse forms that read
lot-blind flows assume that every commodity floats, and a witness
shows that this hypothesis cannot be dropped.

The new module Ledger/BasisGain.lean imports Ledger.Lots and
Ledger.Conversion and nothing else, both of the specification layer,
and uses the LotRefinement of Lots.lean and no other lot structure.
basisGain is market value minus basis value as a valuation functor on
the free model. market_eq_basisGain proves that on a journal whose
generators balance at basis the market value is the basis gain,
conversion_leg_forced that any leg assignment completing such a
journal is worth minus the basis gain, and equityConversion_entry that
the equity leg of Conversion.lean is that forced entry; all three hold
for any market vector. basisGain_change_refined states the change in
basis gain under a move of the price table, with the valuation
expressions held fixed, as the refined flows valued at the difference
of market vectors. basisGain_change and market_eq_coarse read the
lot-blind flows and carry the hypothesis that every commodity floats,
and so does entry_change, which states the change in any completing
leg under price motion and, by conversion_value, needs no balance at
basis. The examples in BasisGainExamples give a basis gain of $300
for purchases at $150 and $160 valued at $170, a forced entry of -$300
on the free model (twoBuys_forced_entry), and a basis gain of $200 at
every quote for a purchase at $150 followed by an unmatched sale at
$170, which is realized gain never booked and not unrealized gain.
bare_basis_degenerate shows that over the lot-blind flows of those two
purchases, which bareBuys_eq_forget identifies, no basis that balances
them values the dollar; hbasis_weaker shows that a generator worth zero
under basisVector need not balance in any commodity; and
basisGain_change_needs_floating refutes the unconditional coarse law
on one fixated widget lot. OptUnrealizedGains replays in the tree the
reading of opt-unrealized-gains.test described below.

The module is the file the plan prepared, with docstring corrections
made after checking them against the article, against Lots.lean and
Conversion.lean as they landed, and against the C++ source at
76b9dc2b2, and with the changes review asked for. The names the file
uses from Lots, Conversion and PriceVector resolved unchanged, and the
two fixated examples now read prices in USD, as described below.
Review found that entry_change carried the hypothesis hbasis without
using it, since its conclusion mentions neither basisVector nor
basisGain; the hypothesis is dropped and the proof goes through
conversion_value, which strengthens the theorem without changing what
it says about a journal that balances at basis. Review also asked that
two claims the docstrings relied on be proved in the tree:
bareBuys_eq_forget, that the bare purchases are the lot-blind flows of
the refined ones, and the reading of opt-unrealized-gains.test, which
had been checked only in a scratch file and is now the namespace
OptUnrealizedGains. hbasis_weaker was added as the witness of the
corrected hbasis paragraph.

The module docstring said that in Ellerman's vector journal
revaluation needs no entry, which the article does not discuss; as in
the correction made to completed_revalues_free, it now derives the fact
by linearity from the article's zero-terms and says that the reading is
the repository's. It said that a journal balancing at basis fails to
balance by the value of the omitted leg, which reverses the sign; it
now cites conversion_value, by which a journal's value is minus the
value of the leg that would complete it. It said that a journal in
which every commodity floats is the scope within which lot-blind market
valuation can be compared with C++ at all, but plainV_not_factor in
Lots.lean shows that plain -V needs refined flows even when every lot
floats; it now says only that this is the hypothesis of
market_of_lotBlind. It attributed to note 20 the condition under which
hbasis reads as a balance at basis, but note 20 asks that each type of
property have a single unit value, which basisVector gives by
construction, and says nothing about units of account. The paragraph
now says that basisVector adds amounts in different commodities as
one unit, so that a balance at basis in each commodity implies hbasis,
and that the converse holds whenever, within the generator, every lot
price is denominated in one commodity and no other commodity appears
unannotated, and can fail otherwise, as hbasis_weaker shows; a first
revision had said "only when", which review showed to be too strong,
since generators outside that condition can still balance at basis.
The paragraph relates the condition to LotsIn and to the regime the
docstring of basisVector names. It said that opt-unrealized-gains.test
fixes the contract that an unfiltered bal -V --unrealized report
totals zero, and that this is equityConversion_entry read at report
time; it now states what the test at 76b9dc2b2 expects, the command
bal -V --unrealized --unrealized-gains G on a journal that buys 10 AAPL
for $200 and then prices AAPL at $30, with Assets at $100, G at $-100
and a total of 0, and says that, reading the posting to G as the
equity leg, the zero total is market_eq_basisGain together with
equityConversion_entry, replayed as OptUnrealizedGains.total_zero. The
lot that replay uses, 10 AAPL at $20, is cited to xact.cc:380 and
454-463, which give the AAPL posting of a two-commodity transaction
its computed cost, and to the exchange at xact.cc:688-690 and
pool.cc:508-522, which annotates the amount with the per-unit cost and
the posting's date. No C++ -V output is cited.

The docstring of sell170 said without a witness that the engine books
a new automatic lot. It now cites journal.h:161-162, where the default
lot policy is none; xact.cc:528, which matches lots only under another
policy; xact.cc:688-690 and pool.cc:508-522, where the exchange
annotates the sold amount with its own per-unit cost and the posting's
date; and xact.cc:692 and 803-811, which book the annotated amount when
the posting carried no lot price. The price tables of
fixated_price_motion_not_coarse and basisGain_change_needs_floating
priced USD together with the widget; they now price USD at 1 and move
the widget from 10 to 15, the correction made to fixated_not_factor in
Lots.lean. The docstrings of market_eq_coarse, basisGain_change and
entry_change now name their hypothesis, that every commodity floats,
and the docstring of basisGain_change_refined says that the valuation
expressions are held fixed.

The locked build script, lake build with warnings as errors, exits 0
after 1223 jobs, one more than before, with no warning in its log. The
axiom check reports the 16 #print axioms records the plan lists for the
module, each within propext, Classical.choice and Quot.sound, and a
scratch copy of that list extended by the ten declarations added in
review reports 26 records within the same budget; the environment-wide
audit reports 2387 constants in 29 Ledger modules and none outside
that budget. The cited-name check resolves every identifier it selects
from the module; its one bare token, basisVector, resolves through the
qualified allowlist entry that the Lots leaf added. grep for
underlying or coarsen in the module exits 1 with no output. The import
scan lists BasisGain <- Conversion, Lots, both of the specification
layer, and no module imports it yet. On a journal that buys 10 AAPL at
$150 and then sells 10 AAPL at $170 without lot selection, bal --lots
from ledger 3.4.1 and from the C++ build both report 10 AAPL {$150}
[2024/01/01] and -10 AAPL {$170} [2024/02/01] in Assets:Brokerage,
which is the booking the sell170 docstring describes, and on the
journal of opt-unrealized-gains.test bal --lots from ledger 3.4.1
reports 10 AAPL {$20.00} [2008/10/01], the lot OptUnrealizedGains
uses. A scratch file that typechecks with exit status 0 and the three
axioms shows that sell170 balances at basis.

The scan of added Lean lines finds no forbidden construct, the register
check of added lines is clean, the gate self-test is green, and every
C++ test cited by the tree exists. The oracle's default output over
the 3816 positive and the 469 negative journals extracted from the C++
suite is byte-identical to its output before this change, by cmp
against the recorded snapshots. The bisimulation log against the C++
tree, over 4384 .test files, reports PASS 3816, FAIL 0, SKIP:negative
489, SKIP:no-journal 77 and SKIP:python 2, with both detector fixtures
green.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0143bpV19FtoLrrR59maPRPk
The Ellerman analysis, at the revision it examined (f5d891e), found in
its item I20 that the tree exhibits no inhabitant of ClosesBooks
(Ledger/Derived.lean), although Ledger/Trivial.lean says that the
one-point model inhabits every structure of the design. Note 11 of the
article (pp. 499-500) describes closing as another transaction or two at
the end of the period that make the changes to equity and reset the
revenue and expense accounts to zero. The analysis proposed to leave
ClosesBooks as it is, since its docstring is accurate, to show in a
documentary theorem how weak the notion is, to state at L1 what a
closing entry does and touches, to bridge that entry to ClosesBooks in
the free model, and to inhabit the bridge with a period whose revenue is
nonzero. The plan's decision D9 takes one equity account name and
defines the closing entry as the completion of Ledger/Conversion.lean
applied to the reversed temporary balances, so that closing and
conversion are one construction, as item G6 asks; decision D14 drops the
prototype's ClosesBooksOnly, which cannot express exclusivity at the
abstract level.

The new module Ledger/Closing.lean imports Ledger.Conversion and nothing
else, a module of the specification layer, and no module imports it yet.
ClosesBooks.of_invisible shows that any morphism the three views cannot
see closes the identity period. closingEntry temp eq x is completion eq
(-(x.filter temp)); closingEntry_mem_zeroRows proves it a zero-row,
closingEntry_support proves that it touches only temporary accounts and
accounts of the name eq, and, when no equity account is temporary,
closingEntry_resets proves that it resets every temporary account and
closingEntry_equity that each commodity's equity account receives that
commodity's column of the temporary balances.
GenSpec.closesBooks_of_closingEntry proves that a morphism whose flow is
the closing entry of the period's flow satisfies ClosesBooks for the
revenue, expense and equity views of one commodity. ClosingDemo builds a
period with a sale of 100 and wages of 30 and a closing generator whose
flow is the closing entry of that period (closing_is_closingEntry).
demo_closesBooks inhabits ClosesBooks with them, period_revenue shows
the revenue to be -100, and demo_net_income_closed shows equity at -70
after the closing; demo_cash_preserved shows that the closing leaves
cash unchanged, and demo_closing_support reads closingEntry_support on
the closing generator. ClosesBooks and Trivial.lean are not changed
here.

The module is the file the plan prepared, with corrections to its
docstrings, one to a proof and one added comment, none of which changes
a statement. Three were made after checking the module docstring against
Conversion.lean as it landed. The docstring said that the closing entry
posts the reversal's per-commodity residual to equity; in the vocabulary
of Conversion.lean the completion subtracts the conversion leg, the
residual so placed, and so adds the equity leg, its negation, so the
sentence now says that the closing entry adds the reversal's equity leg.
It listed closingEntry_resets and closingEntry_equity without their
hypothesis, which it now names, and it said that ClosingDemo inhabits
the bridge, where the demonstration applies the bridge to inhabit
ClosesBooks. Review found the rest. The module docstring called
ClosesBooks weak by design, an intent that neither the analysis nor
Ledger/Derived.lean states, and it said that the note describes the
exclusivity, which the note does not; it now calls the notion weak,
which ClosesBooks.of_invisible proves, and calls the exclusivity a
reading of the note by this module. One docstring that described the
three views of ClosingDemo was attached to the first of them alone, and
each view now has its own. The bridge's proof no longer names the
equation of its set step, which no later step used. demo_closesBooks is
proved by binding the bridge's result with have and closing with exact,
and no heartbeat option is set: a scratch file that states the same
theorem with the bridge applied as a term fails with exit status 1 and
"(deterministic) timeout at `whnf`, maximum number of heartbeats
(200000) has been reached". A comment beside the proof now records this,
so that the term form is not restored.

The locked build script, lake build with warnings as errors, exits 0
after 1224 jobs, one more than before, with no warning in its log. The
axiom check reports the 36 #print axioms records the plan lists for the
module, each within propext, Classical.choice and Quot.sound, and the
environment-wide audit reports 2473 constants in 30 Ledger modules and
none outside that budget. The cited-name check resolves every identifier
it selects from the module; its one bare token, ClosingDemo, resolves
through the qualified allowlist entry for ClosingDemo.demo_closesBooks.
grep for ClosesBooksOnly, set_option, sorry and native_decide in the
module exits 1 with no output. The import scan is clean.

The scan of added Lean lines finds no forbidden construct, the register
check of added lines is clean, the gate self-test is green, and every
C++ test cited by the tree exists. The oracle's default output over the
3816 positive and the 469 negative journals extracted from the C++ suite
is byte-identical to its output before this change, by cmp against the
recorded snapshots. The bisimulation log against the C++ tree, over 4384
.test files, reports PASS 3816, FAIL 0, SKIP:negative 489,
SKIP:no-journal 77 and SKIP:python 2, with both detector fixtures green.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0143bpV19FtoLrrR59maPRPk
The Ellerman analysis, at the revision it examined (f5d891e), found in
its item G1 that the balancing engine of Ledger/Journal.lean lets the
bucket account, declared by the bucket or A directive or by default in
an account block, absorb the residual of any transaction that has no
elided posting. C++ adds the bucket posting only when the transaction
has exactly one posting: src/xact.cc:368 tests
posts.size() == 1 && !balance.is_null() before it creates the inferred
posting, and test/regress/coverage-bucket-single-post.test exercises
that path. The oracle therefore accepted unbalanced transactions of two
or more postings that C++ rejects, and it booked to the bucket the
two-commodity residual that C++ treats as an implicit exchange and the
rounding residual of a cost that C++ tolerates.

In balancePool the flag allowBucket is renamed isReal, since it marks
the real pool, and the bucket completes that pool only when ordered, the
list of every parsed posting of the transaction, has length one. The
count includes postings of every kind and is taken before a bare = 0
assignment fans out into one posting per commodity and before automated
postings are appended, as C++ counts at finalize, before extend_xact.
The second conjunct of the C++ test is implied, because the bucket is
consulted only for a nonzero real residual. A comment beside the rule
cites the source line and the regression test, and the Balancing bullet
of the module docstring now says that the bucket absorbs a real residual
only in a transaction of exactly one parsed posting. The change is the
diff the plan prepared, with three corrections to comment and docstring
text that leave behavior unchanged. Its comment said that with two or
more postings a residual is an error, although the code below it still
accepts an implicit exchange, a rounding residual, and a residual that
vanishes when lot and fixated values are substituted in a second pass;
the comment now says that such a residual is left to those cases and to
the error. It cited the regression test by bare name, a form the
citation check does not read, and now cites it as
regress/coverage-bucket-single-post, the path form of the nearby rules.
Its docstring wording did not say that only the real pool is completed
or that the count is of parsed postings, and the bullet now says both.

The locked build script, lake build with warnings as errors, exits 0
after 1224 jobs with no warning in its log. Twelve probe journals were
compared with the C++ build of the working tree, row by row. Before the
change six were accepted by the oracle and rejected by C++, two
differed in their rows (an implicit exchange and a cost rounding
residual, both booked to the bucket), three agreed and one was rejected
by both. After it five agree, six are rejected by both, each with the
oracle reason "unbalanced transaction (residual in 1 commodities)", and
one is still accepted by the oracle and rejected by C++: a single bare
= 0 assignment on an account that holds two commodities, which C++
refuses with "Cannot convert a balance with multiple commodities to an
amount", a rule outside this change. A probe whose single posting
matches an automated transaction still completes through the bucket
and agrees with C++.

Over the 469 negative journals extracted from the C++ suite the oracle
rejects 102 before and after the change, none of them a journal that
C++ accepts, and its default output over them is byte-identical to the
output before the change by cmp, although the plan allowed it to
change. The scan of added Lean lines finds no forbidden construct, the
register check of added lines is clean, the gate self-test is green,
and every C++ test cited by the tree exists. The oracle's default
output over the 3816 positive journals is byte-identical to the
recorded snapshot. The bisimulation log against the C++ tree, over
4384 .test files, reports PASS 3816, FAIL 0, SKIP:negative 489,
SKIP:no-journal 77 and SKIP:python 2, with both detector fixtures
green.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0143bpV19FtoLrrR59maPRPk
The Ellerman analysis, at the revision it examined (f5d891e), found in
its item I2 that the balancing engine of Ledger/Journal.lean treats any
residual in exactly two commodities as an implicit exchange, while C++
infers one only under the conditions of Phase 3 of finalize. Two of
those conditions depend on facts the oracle parsed and then discarded.
C++ chooses the top posting as the last annotated posting that must
balance, else the first (src/xact.cc:386-393, at 76b9dc2b2), and it
declines the inference when the top posting carries a fixated lot
price, the flag that annotate.cc:169 sets for {=P} and {{=T}}. The plan
divides I2 into two units: this one records those two facts on every
parsed posting and changes no behavior, and the next replaces the two
length tests with one predicate that reads them.

PostSpec gains the fields annotated and fixated, each with a docstring
that cites its C++ source line. The annotation loop now carries both
flags, together with a third that records whether a balance assertion
has been read, and its recursive calls pass all three by name. A lot
price, a [date] or a (note) met before any cost or assertion belongs to
the posting's own amount: it sets annotated, and such a lot price sets
fixated when it is fixated. One met after a cost belongs to the cost
amount, the reading the loop already applies to lot prices
(regress/1217), and one met after an assertion belongs to the assertion
amount, which C++ parses with its own annotations
(textual_xacts.cc:756-757); neither sets a flag. Both flags are false on
an amount with no commodity, whose annotation C++ discards
(amount.cc:1908). parseAmountField returns a private record,
AmountField, instead of a ten-element tuple, and parsePosting copies the
two flags from it. Every other consumer of the lot pair ignores its
flag, so neither new field reaches the flows.

The leading form {P} AMT has no witness: C++ rejects it with "No
quantity specified for amount" (amount.cc:1665), because a brace cannot
begin a commodity symbol (commodity.cc:331-349), and no C++ test uses
it. The oracle has accepted it since the initial import. A comment
beside the branch now records that departure, and the branch reads its
flags as it reads a lot written after the amount; before, the flag of
its lot pair was the constant true, which no rule read. A second
comment records two older forms of the same kind: an annotation after
an amount expression, (EXPR) {lot}, which C++ reports as an unexpected
character (textual_xacts.cc:908-910), and a second lot price, date or
note, which C++ reports as more than one (annotate.cc:157, 193, 249).
Both C++ binaries reject a journal of each form, while the oracle
prints balances for it and sets the flags as for an annotation on a
plain amount. Rejecting any of these forms would change behavior and
is left to a later unit with negative witnesses.

The change departs from the diff the plan prepared in the reading of
annotations after an assertion and on amounts with no commodity, and in
its comments and docstrings; none of these departures changes behavior.
Review found that the prepared diff set both flags for a posting
written AMT = ASSERT {=P}. With the next unit's predicate applied, the
oracle then rejected a journal that ledger 3.4.1 and the C++ tree both
accept, since it chose that posting as a fixated top posting. The lot
reading itself is unchanged: a lot written after an assertion still
reaches lotVal as the posting's own, a reading that no C++ test
exercises (regress/2318 writes the posting's lot first). The docstrings
record the remaining departures, both fail-open and both affecting the
two flags: the annotation C++ adds for an apply fixed rate
(textual_xacts.cc:626-638) is not parsed and sets neither flag, and on
a balance assignment both flags are false, although C++ can carry a lot
into the amount it computes (textual_xacts.cc:843-862). The docstring
of fixated also says that, unlike LotPrice.fixated in Ledger/Lots.lean,
it is not set by the fixated costs @ =P and @@ =T, since C++ fixates
the annotation price they meet only in Phase 6 of finalize
(xact.cc:816-819), after Phase 3 has chosen the top posting. The
docstring of AmountField.costVal says that only a lot priced in the
cost's commodity adjusts the cost, as the code does.

The locked build script, lake build with warnings as errors, exits 0
after 1224 jobs with no warning in its log. A scratch copy of
Ledger/Journal.lean with an #eval appended, checked against the built
tree with exit 0, parsed 45 posting texts and printed the expected
flags for each: annotated for a lot, date or note before any cost or
assertion and for the leading form, but not for one after a cost or an
assertion, on a balance assignment, or on a number with no commodity;
fixated exactly where the posting's own lot is {=P} or {{=T}}. A
scratch driver built from this tree with the next unit's predicate
applied accepts the journal that review found, as both C++ binaries
do, and otherwise prints the same output as the review's run over its
48 probe journals. The oracle's default output over those journals is
byte-identical by cmp to that of the tree before this revision. Over
the 469 negative journals extracted from the C++ suite the oracle
rejects 102, none of them a journal that C++ accepts, and its default
output over them is byte-identical by cmp to the output recorded after
the previous unit. The oracle's default output over the 3816 positive
journals is byte-identical to the recorded snapshot. The scan of added
Lean lines finds no forbidden construct, the register check of added
lines is clean, the gate self-test is green, and every C++ test cited
by the tree exists. The bisimulation log against the C++ tree, over
4384 .test files, reports PASS 3816, FAIL 0, SKIP:negative 489,
SKIP:no-journal 77 and SKIP:python 2, with both detector fixtures
green.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0143bpV19FtoLrrR59maPRPk
The Ellerman analysis, at the revision it examined (f5d891e), found in
its item I2 that the balancing engine of Ledger/Journal.lean accepts any
residual in exactly two commodities as an implicit exchange, in the
first pass and again in the second pass that values lots, while C++
infers an exchange only under the conditions of Phase 3 of finalize
(src/xact.cc:374-473, at 76b9dc2b2). The oracle therefore accepted
transactions that C++ rejects, among them an exchange beside an explicit
cost, a same-sign residual, a fixated top posting whose lot price the
cash leg contradicts, a lot gain, two commodities left by
bracket-virtual postings, and the journal of test/regress/1029.test. The
previous unit recorded the two facts that Phase 3 reads from each
posting, annotated and fixated; this one reads them.

A new private function, impliedExchange, gives the C++ verdict wherever
C++ converts the real balance and declines elsewhere. It accumulates the
real pool in posting order as C++ accumulates its balance in Phase 1,
through a small model of value_t named CppSum: an amount absorbs amounts
of its own key and turns into a balance at the first other key, keeping
its own entry only when that entry is not exactly zero (value.cc:600-607
and 1548, balance.h:128-134), and a balance never erases an entry that
addition brings to zero (balance.cc:118-133). Keys are those of C++, the
reduced commodity with its lot price per unit and that price's fixated
flag (annotate.h:120-125), with no lot on an amount that has no
commodity (amount.cc:1908). C++ converts only when no posting of any
kind carries an explicit cost (xact.cc:395-398), when that balance has
exactly two entries (xact.cc:380), neither of them exactly zero (the
guard of issue #1789), when the top posting, the last annotated
must-balance posting or else the first (xact.cc:386-393), carries no
fixated lot price (xact.cc:401-404), and when the two entries are not
one base commodity with a lot price on the top side only
(xact.cc:434-449, regress/1029). When the top posting's key is not an
entry, C++ takes the entries in the unspecified order of an unordered
map (balance.h:99), and a verdict is given only when both orientations
agree. Where they differ, the function reports that the verdict depends
on the order, and the balancing engine keeps for that case the rule
that preceded this unit.

Where C++ converts, the function follows it to the end. The conversion
first reduces the amount of every posting (xact.cc:459), which throws
for a posting with no amount (amount.cc:1153-1155), so an elided
bracket-virtual posting or an amountless paren-virtual one makes the
transaction fail. When the other side is a number with no commodity
and the top side has one, the rate takes the commodity of the top side
(amount.cc:814-815), as do the costs (amount.cc:727-744), and Phase 6
throws for a cost in its posting's own commodity (xact.cc:679-681), so
the transaction fails as well. Otherwise every must-balance posting of
the key of the top side, bracket-virtual ones included, leaves the real
balance and its cost at the implied rate enters it (xact.cc:454-465).
Phase 6 then books a gain for each converted posting whose lot price is
in the commodity of the other side or of that side's lot price, rounded
at the display precision of that commodity plus the decimals of the
posting's quantity (xact.cc:708-711 and 756-759). A gain against a
plain basis is booked whenever it is exactly nonzero, since the test at
xact.cc:696 is exact (amount.cc:1407); a gain between two lots is
booked only when it does not show as zero (xact.cc:762). Every
remaining entry must then round to zero at its display precision
(xact.cc:898). The gains are rounded one posting at a time, as C++
rounds them, and their sum can differ in either direction from the
rounded gain of the whole.

The result, which distinguishes a passing conversion, a failing one, no
conversion, and a verdict that depends on the order, replaces the
length tests of both passes for the real pool, since Phase 3 never
converts the bracket-virtual balance. A conversion that fails is
rejected at once instead of falling to the rounding and lot-value
tests. Where C++ makes no conversion, the second pass no longer accepts
a lot-valued residual in two commodities, because C++ prices fixated
lots in Phase 4, after Phase 3, and infers no exchange from what they
leave. The two length tests survive only for a verdict that depends on
the order, so that there the oracle accepts what it accepted before
this unit. The bracket-virtual pool loses its second pass altogether,
because C++ values no lot there: Phase 1 sums it once, Phase 4's
recompute skips its postings, and Phase 8 tests it as it stands
(xact.cc:505-516 and 973-979). PostSpec gains lotUnit, the lot price
per unit of the amount's own lot, so that a posting of zero quantity is
keyed by its own lot, which the quotient of lotVal by the quantity
cannot give. A lot written only after a balance assertion annotates the
assertion amount (textual_xacts.cc:756-757), so annotLoop now reports
whether the lot it kept was the amount's own, and only such a lot sets
lotUnit; lotVal still reads it as before. finalizeXact divides lotUnit
by the factor by which a commodity equivalence reduces the amount, as
C++ divides the lot price when Phase 1 reduces the amount
(amount.cc:1153-1176, #512). Comments beside each changed site cite the
source lines, and the Balancing bullet of the module docstring now says
that a two-commodity real residual, keyed by lot price, is an implicit
exchange only where C++ infers one, that a bracket-virtual residual
never is, and that lot values never settle a bracket-virtual residual.

The docstring of impliedExchange lists the known departures with the
direction in which each fails, as the plan's decisions D19 and D22
require. The swap of issue #1130 is not modelled and can fail either
way; a C directive marks its smaller unit primary
(textual_directives.cc:185-188, commodity.cc:73-75), and the docstring
gives a journal for each direction. C++ compares the top posting's
commodity as written with the entries as reduced (xact.cc:333 and 422),
which the oracle does not model, and which can fail either way where
the map order matters. The rule kept for a verdict that depends on the
order can fail either way: the C++ builds measured take as the top side
the entry whose key entered the balance first, which the oracle does
not assume, since the order is unspecified, and the docstring gives a
journal for each direction. [date], (tag) and ((value expression))
annotations do not split keys, and an apply fixed rate is not parsed,
each of which can fail either way. The lot that C++ carries into the
amount of a balance assignment (textual_xacts.cc:843-862) is not
modelled, which fails closed for a partial transfer and sale that C++
accepts and open for a sale at a lot gain that C++ rejects. A remainder
that bracket-virtual postings of the converted key leave in the real
balance is not followed into Phase 4, which can derive a cost from a
fixated lot price and recompute the balance without those postings
(xact.cc:475-518); this fails open. A total lot price {{T}} keys its
amount by the exact quotient where C++ rounds it (amount.cc:1909-1923),
which fails closed. Six lot shapes outside the function remain
accepted, each fail-open, and the three of them whose quantities net to
zero when lots are ignored are accepted in a bracket-virtual pool as
well; the sixth, a fixated top posting against an unfixated lot of
another commodity priced in the same commodity, was found in review and
is not among the five that decision D19 names. The docstring also
records that a failing verdict is the one C++ gives without
--permissive, whose rescue of a computed balance assignment
(xact.cc:941-952) the oracle does not model anywhere. The docstring of
PostSpec.fixated no longer calls the apply fixed and balance-assignment
departures fail-open, since each now fails either way, and points to
impliedExchange.

The change departs from the diff the plan prepared in nine ways. Its
docstring hunk did not apply to the bullet as the bucket unit left it,
and the bullet was edited by hand. It counted the nonzero entries of the
real balance, but C++ also counts an entry that addition brings to zero:
for $5, 10 FOO, -10 FOO and -4 EUR in that order it sees three entries,
makes no conversion and rejects the transaction, which the prepared test
accepted; the accumulation model replaces the count. It declined every
lot gain to the second pass, which tests the exact lot value of the
whole: two postings of 1 AAPL {$1.004} against $-2, at a display
precision of two decimals, were rejected there, although C++ rounds
each gain of $0.004 away and accepts them, and the oracle before this
unit agreed; the per-posting model replaces the decline. Its Boolean
result could not reject a failed conversion that a later test accepts,
so 10 AAPL {$0} beside 50 EUR {$0}, which C++ rejects with the
remainder 20 AAPL {$0}, was accepted by the second pass. Where the top
posting's key is not an entry it required both orientations to pass
and otherwise treated the residual as no exchange, which rejected six
journals that C++ and the oracle before this unit accept, among them 5
GOOG and -5 GOOG followed by 3 EUR and $0.001; the earlier rule is kept
for that case instead. Its key divided lotVal by the quantity, so that a
top posting of zero quantity matched neither entry and a transaction
that C++ accepts was rejected; lotUnit replaces the quotient. It kept
the second pass for the bracket-virtual pool, which accepted 5 AAPL {$1}
against -$5 there, although C++ rejects it with the remainder $-5 / 5
AAPL {$1}. It did not model the conversion of bracket-virtual postings
or the failure on an amountless posting, which it listed as a fail-open
departure; both are now modelled. It accepted a cost side with no
commodity, such as -5 X against 10, which C++ rejects in Phase 6.

The locked build script, lake build with warnings as errors, exits 0
after 1224 jobs with no warning in its log. The 29 I2 probe journals
match the verdicts that probes-final-expected.txt derives for this
unit: 14 agree with the C++ build of the working tree row by row, 12
are rejected by both, and the 3 that the plan documents as fail-open,
applyfixed-bad, fix-nottop and three, are accepted by the oracle alone.
Before the change 14 agreed, 1 was rejected by both and 14 were
accepted by the oracle alone. The five D19 probe journals are each
accepted by the oracle and rejected by C++, as the docstring states.
A further 142 probe journals were written in review and for this unit;
the C++ build and ledger 3.4.1 print the same output and exit status
for each of them and for the I2 and D19 journals. Against the oracle
before this unit, 51 move from acceptance by the oracle alone to
rejection by both, 59 agree before and after, 19 remain accepted by the
oracle alone and 3 remain rejected by both. Six move from agreement to
rejection by the oracle alone, each an instance of a listed departure
that fails closed: two balance assignments that carry a lot, an apply
fixed rate that settles a three-key transaction together with a
fixated lot, a total lot price beside its rounded per-unit form, and
two lots in the units of a C directive, bought below their lot price.
Three remain rejected by the oracle alone: a dated amount against plain
units, the fail-closed journal of the order-dependent case, and a
balance assignment with no exchange that C++ accepts only through the
permissive rescue. One more balance assignment, beside an exchange,
moves from a difference of rows to rejection by the oracle alone, again
because C++ rescues it under --permissive; both outcomes fail the
bisimulation. Of the 19 still accepted by the oracle alone, 17 are the
fail-open journals of departures that the docstring of impliedExchange
lists. One is {{T}} on a zero quantity, which C++ rejects as a division
by zero (amount.cc:1911), as the docstring of PostSpec.lotUnit records.
One is an amountless paren-virtual posting in a transaction that makes
no conversion, which C++ rejects after balancing ("There cannot be null
amounts after balancing a transaction"), a check that lies outside
Phase 3.

Over the 469 negative journals extracted from the C++ suite the oracle
rejects 103, one more than before, none of them a journal that C++
accepts. Its default output over them differs from the output recorded
after the previous unit, by diff, only in the section of regress/1029,
where three balance rows become "line 19: unbalanced transaction
(residual in 1 commodities)". The oracle's default output over the 3816
positive journals is byte-identical to the recorded snapshot. The scan
of added Lean lines finds no forbidden construct, the register check of
added lines is clean, the gate self-test is green, and every C++ test
cited by the tree exists. The bisimulation log against the C++ tree,
over 4384 .test files, reports PASS 3816, FAIL 0, SKIP:negative 489,
SKIP:no-journal 77 and SKIP:python 2, with both detector fixtures green.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0143bpV19FtoLrrR59maPRPk
The Ellerman analysis, at the revision it examined (f5d891e), found in
its item G3 that the reader of Ledger/Journal.lean skips every block
opened by a `~` line, while C++ finalizes a periodic transaction as it
parses it (src/textual_xacts.cc:316, at 76b9dc2b2) and rejects the file
when the transaction does not balance (src/xact.cc:960-965 and 973-979)
or when two of its postings carry no amount (src/xact.cc:336-346). Six
tests of the C++ suite record such a rejection, and the oracle accepted
the journal of each: DDB54BB8, cov6-txact-period-unbalanced,
cov9d-period-xact-unbalanced, coverage-periodic-unbalanced,
coverage-textual-periodic-unbalanced and 5D92A5EB, the last of which
has two postings without an amount.

The pending state of the reader gains a periodic block, which a `~`
line now opens in place of the ignored block. Its indented lines are
collected as those of a dated transaction are, with two exceptions
that follow from C++ collecting the postings of a template with
parse_posts, which skips a line beginning with `;` and hands every
other line to parse_post (textual_xacts.cc:951-952). A line beginning
with `;` is dropped. A line beginning with `assert `, `check ` or
`expr ` is read as a posting to the account it names, where in a
dated transaction parsePosting skips it, since only parse_xact treats
such a line as an expression (textual_xacts.cc:1107-1125). When the
block closes, flushPending finalizes it with finalizeXact against the
state before the block and keeps none of the result; an error is
reported with the prefix "periodic transaction: ". Discarding the
result follows C++, which keeps a template in journal_t::period_xacts
(textual_xacts.cc:318) while reports walk journal_t::xacts
(iterators.cc:53-54), so a template contributes no flows. finalizeXact
gains a flag that skips de-duplication by UUID for a template, since
C++ records a UUID only in journal_t::add_xact (journal.cc:594-595),
which the periodic directive never calls (textual_xacts.cc:316-318).

A template never sets the date it inherits from item_t (xact.h:434;
item.h:161), so has_date() is false for it (item.h:262), and C++ runs
lot matching and the lot gains of Phases 5 and 6 only inside
`if (has_date())` (xact.cc:522-822). The same flag therefore keeps a
template's postings from booking a lot gain in the three places where
the oracle applies Phase 6: parseAmountField leaves the cost as
written instead of folding the rounded gain into it, impliedExchange
leaves out its gain terms, and the lot-valued second pass of
balancePool keeps the cost of a posting that has one, as Phase 1 sums
it (xact.cc:325) and Phase 4 prices only a posting without one
(xact.cc:486). Seven journals written in review, each a template with
a lot, give the same output and exit status under ledger 3.4.1 and the
C++ build. Five are accepted by C++: 10 AAPL {$10} against $-150, the
same lot at {$10.00} against $-100.01, -10 AAPL {$10} against $150,
and -10 AAPL {$10} @ $15 against $150, both as real and as
bracket-virtual postings. Two are rejected by C++: that sale beside
Income:Gains $-50, and 10 AAPL {$10} @ $15 against $-100. Applying
Phase 6 to a template rejected the first five and accepted the other
two. With the flag, the oracle agrees with C++ on all seven, and the
same five journals with a date in place of the period are rejected by
both.

Six journals written in review exercise the reading of an expression
line in a template, and each gives the same output and exit status
under ledger 3.4.1 and the C++ build. Four are accepted by C++, which
books the residual to an account named after the line: $10 against
$-5 followed by `assert amount > 0`, by `check amount > 0`, by
`expr amount`, or by a tab-indented `check amount`. Skipping the line
rejected all four, and the oracle now agrees with C++ on each. Two are
rejected by C++ and were accepted by the oracle: an elided posting
beside an `assert` line, which leaves two postings without an amount,
and `expr  1` after two spaces, a posting of 1 to the account `expr`.
The oracle now rejects both. Five further templates around the rule
agree: one posting under a bucket account, a note after the line, a
status mark before it, a residual in two commodities, and a template
that already balances. A dated transaction of $10 against $-5 with a
`check` line, which the rule leaves as it was, is rejected by both.

Known departures remain, and the comment beside the rule records each as
a journal shape. C++ learns display precision and amount styles from
template amounts, which govern the rows printed, the balancing tolerance
and the reading of later amounts, while the oracle learns nothing from a
template, so this departure fails in either direction. Decision D18 of
the plan keeps it for a later change. Until then, after a template of
$500.1, C++ prints the balances of a later (1/3 $) as $0.3, which the
oracle rounds away. After a template of $1.000, C++ rejects a later
3 X @ $0.333 against $-1.00, and after one of 10,50 EUR a later
5.25 EUR, and the oracle accepts both. After a template of EUR 10,50,
C++ reads a later EUR 1,500 against EUR -1,5 as 1.5 in each and accepts
it, and the oracle rejects it. Four further shapes are rejected by C++
and accepted by the oracle: a balance assertion or assignment in a
template, which C++ parses without a transaction (textual_xacts.cc:952),
so that the `=` is never read as one (textual_xacts.cc:743) and is
refused as an unexpected character; an automated transaction whose
posting unbalances a template, which C++ applies and verifies again
(textual_xacts.cc:317, xact.cc:1683-1684); a period expression that C++
cannot parse, or a note after it (textual_xacts.cc:303,
baseline/cmd-source.test); and an empty or whitespace-only line among
the postings of a template, at which C++ ends them
(textual_xacts.cc:945, 949-950), so that it finds the postings before
the line unbalanced or refuses the indented line after it
(textual.cc:289), a convention the oracle also departs from in a dated
transaction. A template inherits the other departures of a dated
transaction, among them those that impliedExchange records and the value
expressions that the reader cannot evaluate. A date annotation does not
split keys, so 10 AAPL [2024/01/01] against -5 AAPL is rejected here in
a template as in a dated transaction, while C++ accepts both; the
amounts (abs($-10)) and (account("Assets:Cash").total) are likewise
rejected here in a template as in a dated transaction, while C++ accepts
both.

The locked build script, lake build with warnings as errors, exits 0
after 1224 jobs with no warning in its log. Of the thirteen G3 probe
journals, compared with the C++ build of the working tree row by row,
six agree and six are rejected by both, as probes-final-expected.txt
derives for this unit; before the change five of the six now rejected by
both were accepted by the oracle alone. The thirteenth, t-prec3, is the
$500.1 shape above. Fourteen further journals written for this unit give
the same output and exit status under ledger 3.4.1 and the C++ build;
five agree with the oracle, four are rejected by both, and five are
rejected by C++ alone, each through one of the departures above. Four
more, written in review, bear on the empty line, and each gives the same
output and exit status under both C++ builds: a template split by one
and a template balanced before one with an indented note after it are
rejected by C++ and accepted by the oracle, as is a dated transaction
split by one, while a template followed by an empty line and a dated
transaction agrees.

Over the 469 negative journals extracted from the C++ suite the oracle
rejects 109, six more than before, none of them a journal that C++
accepts, and regress/1114 is the only negative with a balance error
that it still accepts. Its default output over them differs from the
output recorded after the previous unit, by diff, in exactly the six
sections of the tests named above, each of which becomes a
"periodic transaction:" error. The oracle's default output over the
3816 positive journals is byte-identical to the recorded snapshot. The
scan of added Lean lines finds no forbidden construct, the register
check of added lines is clean, the gate self-test is green, every C++
test cited by the tree exists, and git cat-file finds each of the six
cited tests and baseline/cmd-source.test at 76b9dc2b2. The
bisimulation log against the C++ tree, over 4384 .test files, reports
PASS 3816, FAIL 0, SKIP:negative 489, SKIP:no-journal 77 and
SKIP:python 2, with both detector fixtures green.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0143bpV19FtoLrrR59maPRPk
The Ellerman analysis, at the revision it examined (f5d891e), found in
its item I6 that the oracle accepts the journal of
test/regress/1114.test, a transaction of (1/9 FOO) @ 200 EUR against -22
EUR. Its residual of 2/9 EUR rounds to zero at the display precision of
EUR, which is 0 because a cost amount teaches no precision. C++ rejects
the journal: before its final zero test it widens a cost commodity of
display precision 0 to the precision of the first explicit cost in that
commodity whose precision is positive, on a posting of any kind and with
the cost as Phase 6 leaves it, and it restores the precision afterwards
(src/xact.cc:868-895 and 971-979, at 76b9dc2b2). regress/1114 was the
last negative test of the C++ suite with a balance error that the oracle
still accepted.

PostSpec gains costPrec, the precision of an explicit cost amount: the
decimals of the cost literal and, for a per-unit cost, which C++
multiplies by the posting's amount, the precision of that amount as well
(textual_xacts.cc:699-710, amount.cc:720). Phase 6 then raises it: when
the posting's own lot price is in the cost's commodity and the basis,
the lot price times the amount kept unrounded (pool.cc:502), differs
from the cost at all (xact.cc:696), C++ adds the gain to the cost
(xact.cc:729), and the sum keeps the larger precision
(amount.cc:630-632) even when the gain rounds to zero. A {P} price
counts the decimals of its own literal (annotate.cc:187); a {{T}} price
is divided by the amount, which adds the amount's precision and six
places (amount.cc:812), and its value is rounded at the amount's
precision, the commodity's and six (amount.cc:1908-1923).
parseAmountField models both. annotLoop now returns a structure,
AnnotTail, in place of a positional tuple, and reports in it whether the
cost it read is per-unit, and the decimals of the lot literal it kept
and whether that lot is a total. Parse does not track the precision of
an expression amount, so parseAmountField approximates it as decision
D21 of the plan chose: an expression that divides, or whose value the
decimals of its literals cannot represent, counts six places more than
those decimals, after the six places by which C++ extends a quotient
(amount.cc:805-812).

A new private function, zeroTestPrec, gives the precision at which the
final zero test of a real pool reads a residual, and balancePool tests
the real residual at that precision in both of its passes. A
bracket-virtual pool keeps the display precision, since C++ tests it
after restoring (xact.cc:971-979). C++ widens no cost without a
commodity (xact.cc:881) and tests a residual without one exactly
(amount.cc:1378, 1429), so balancePool now tests such a residual exactly
in either pool, apart from the regress/1125 tolerance described below.
The rounding of roundsToZero moves into roundsToZeroAt, which both tests
share. The zero test of Phase 3 inside impliedExchange is unchanged,
since C++ widens only in Phase 8.

As decision D21 requires, the docstring of zeroTestPrec records each
measured departure of the widened test as a journal shape, marked
fail-open or fail-closed, so that a later list of the shapes the oracle
accepts although C++ rejects them can point to one place. A cost written
as an expression counts only the decimals of its literals, while C++
extends its division by six places and, parsing a cost without
migration, does not cap it (amount.cc:1832-1835): 2 FOO @ ($100/9)
against -$22, and 2 FOO @@ ($200/9) against -$22, are accepted here and
rejected by C++. C++ caps a product or quotient amount at its
commodity's display precision plus six (amount.cc:748-749, 825-826): a
transaction of (1.25/3 FOO) @ 1 EUR, (-1.25/3 FOO) @ 1 EUR, 1 BAR @
0.0000001 EUR and 0 EUR is rejected here and accepted by C++. A quotient
counts the decimals of its larger operand and six, where C++ adds the
precisions of both operands and six (amount.cc:811-812), which shows
once the cap does not bind: after 1.00 FOO has taught FOO two places,
(1.5/0.5 FOO) @ 1 EUR with 1 BAR @ 0.000000006 EUR against -3 EUR is
accepted here and rejected by C++. A product counts the decimals of its
larger factor where C++ adds the precisions of both, and two places more
for a % factor (amount.cc:720, 733): (0.5 * 0.4 FOO) @ 1 EUR against -1
BAR @ 0.16 EUR is accepted here and rejected by C++, while (0.5 * 0.5
FOO) @ 4 EUR with 1 BAR @ 0.001 EUR against -1 EUR, and (1 FOO * 50%) @
1 EUR twice with 1 BAR @ 0.001 EUR against -1 EUR, are rejected here and
accepted by C++. C++ tests a residual exactly when its precision does
not exceed the widened one (amount.cc:1407): (1/3000000 FOO) @ 1 EUR
against 0 EUR is accepted here and rejected by C++. The raise of Phase 6
compares the commodity of the lot price with that of the cost as
written, before finalizeXact resolves a commodity alias, which C++
resolves as it parses (pool.cc:150-155): where E is an alias of EUR,
-0.5 FOO {1.001 E} @ 1 EUR twice with 1 BAR @@ 0.0004 EUR against 1 EUR
is accepted here and rejected by C++. The gain that Phase 6 books is
compared by the same names, so for such a lot it is not booked either, a
departure that predates this change; resolving the alias for the raise
alone would raise the precision of a cost whose gain is still missing,
so the raise keeps the parent commit's treatment of such a lot.

The raise also reads a {P} price at the decimals of its own literal,
while C++ keys an annotated commodity by the value of its price
(pool.cc:200, annotate.cc:97-100) and so reads the price at the decimals
of the first literal of that value: after 1 FOO {1.00100 EUR}, -0.5 FOO
{1.001 EUR} @ 1 EUR twice with (4/100000 EUR) against 1 EUR is accepted
here and rejected by C++, and after 1 FOO {1.001 EUR} the same
transaction written with {1.00100 EUR} is rejected here and accepted by
C++. Where a cost carries its own lot price in the commodity of the
posting's basis, Phase 6 rewrites the cost into that commodity
(xact.cc:735-774) and C++ widens that commodity, while costPrec stays
with the cost as written: 10 FOO {1 USD} @ 2 EUR {0.5 USD} and -20 EUR
{0.5 USD} with (1/10 USD) against 0 USD are accepted here and rejected
by C++. Last, the widening reads the display precision that this reader
learns, which omits what C++ learns from the amounts of an automated
transaction's postings and of a periodic template, from a define and a
typed metadata value, and from an apply fixed rate (textual_xacts.cc:271
and 952, textual_directives.cc:534-535 and 1081, metadata.cc:194,
token.cc:555). Where C++ holds a positive precision from one of these it
widens nothing, so after any of the five gives EUR two places, 1.0001
FOO @ 1 EUR against -1 BAR @ 1 EUR is rejected here and accepted by C++.

The first product shape is not among the departures the plan listed; it
was found while giving each listed departure its direction, and the
percent, quotient, alias, annotation, swap and precision-source shapes
were found in review. Measured against the C++ build and an oracle built
from the parent commit, the eight shapes accepted here alone were
accepted by the oracle before this change as well. The nine rejected
here alone were all accepted before it, eight with the same rows as C++
and one with a difference in a displayed amount that predates this
change. Six of the nine, the second annotation shape and the five
precision sources, are rejections that the widening introduces where the
precision it reads, or the lot literal it raises from, differs from what
C++ holds. They are recorded and not repaired: repairing them would mean
tracking precision from five sources that this reader does not learn
from, two of which, typed metadata values and apply fixed rates, it does
not read as amounts at all, and modeling the reuse of annotated
commodities, which is keyed by lot date and tag as well; both lie
outside this unit.

The tolerance of the regress/1125 rule can also admit a residual that
the widened test, or the exact test of the null commodity, rejects. C++
adds the exact adjustment of rounding each per-unit cost of a real
posting at its price's decimals (xact.cc:908-929), while the oracle
tolerates half a unit in the last place of every cost in that commodity
written with decimals, total costs included. So -0.5 FOO @ 1.00 EUR
twice with 1 BAR @@ 0.01 EUR against 1.00 EUR, and the same with @@ 0.50
EUR for each @ 1.00 EUR, in a commodity of display precision 2 that no
widening touches, are accepted here and rejected by C++. So are 0.5 FOO
@ 0.5 EUR twice with (49/100 EUR) against -1 EUR, whose residual of 0.01
EUR the widened precision of 2 rejects, and 1 FOO @ 0.333 against
(-1/3), whose residual has no commodity. The tolerance is also why a lot
shape with total costs, -0.5 FOO {1.001 EUR} @@ 0.5 EUR twice with 1 BAR
@@ 0.04 EUR against 1 EUR, stays accepted here although the raised
precision of its costs is now modeled. The oracle built from the parent
commit accepts all five shapes as well, and the C++ build rejects them.
In the other direction, C++ tests the residual plus its adjustment at
the precision of the zero test, while the oracle accepts a residual only
when it settles alone or lies within the tolerance: 0.5 FOO @ 1.005 EUR
with 1 BAR @@ 0.49696 EUR against -1 EUR, which C++ tests at the widened
precision of 4, and the same with @@ 0.4921 EUR against -1.00 EUR, which
it tests at the display precision of 2, are rejected here and accepted
by C++. The oracle built from the parent commit accepted the first,
which the widening now rejects, and already rejected the second. The
comment beside the tolerance records the four accepted shapes as
fail-open and the two rejected ones as fail-closed, and the docstring of
zeroTestPrec names the tolerance where it says that a residual without a
commodity is tested exactly.

The locked build script, lake build with warnings as errors, exits 0
after 1224 jobs with no warning in its log. The twenty probe journals of
this unit, compared with the C++ build row by row, give six AGREE and
fourteen BOTH-REJECT, the verdicts derived for this unit from the C++
binary alone. Six surround regress/1114: its own transaction, quotients
of a third and of two decimal literals, a product, a decimal amount at
an integer per-unit cost, and a total cost with one decimal. Ledger
3.4.1 and the C++ build reject each of them with "Transaction does not
balance", and before this change the oracle accepted all six. These six
were written for this unit, because the probe files that the plan named
for them did not exist. Twelve probe the widening rule. Among them are a
cost on a paren-virtual posting that widens the real test, and a
commodity whose format directive sets its precision to 0. A {P} lot
price of three decimals raises the precision to 4, so that a residual of
0.0004 EUR is rejected while 0.00004 EUR is accepted. A {{1 EUR}} lot on
-3 FOO raises it to 6 through the rounded per-unit price. A residual
without a commodity is rejected at 0.01. A bracket residual of 0.2 EUR
from a cost, beside a real cost that widens EUR to 1, is accepted by
both, which shows that the bracket-virtual pool is not widened. The
earlier bracket probe, whose posting teaches EUR a precision of 1,
rejects on either reading and does not show this. The oracle built from
the parent commit accepts the lot, total-lot and null-commodity probes,
which C++ rejects. The controls of regress/777 and of the regress/1125
rounding still agree. Thirty-two further journals pin the departures
that the comments record: the thirteen fail-open journals give
CPP-REJECTS, the eleven fail-closed ones ORACLE-REJECTS, and eight
controls give AGREE or BOTH-REJECT. Two of the controls show that C++
holds the learned precision of 2 in the precision-source shapes: a
residual of 0.01 EUR after the automated transaction is rejected by
both, while one after a plain amount of two decimals agrees at 0.0001.
Ledger 3.4.1 accepts and rejects the same thirty-two journals as the C++
build. Re-deriving the expected verdicts of every probe set from the C++
binary adds the five new files to this unit's first set and the
thirty-two departure journals as a set of their own, and changes nothing
else.

Over the 469 negative journals extracted from the C++ suite the oracle
rejects 110, one more than before, none of them a journal that C++
accepts, and no negative with a balance error is left accepted. Its
default output over them differs from the output recorded after the
previous unit, by diff, only in the section of regress/1114, which
becomes an unbalanced-transaction error at line 13. The oracle's default
output over the 3816 positive journals is byte-identical to the recorded
snapshot. The scan of added Lean lines finds no forbidden construct, the
register check of added lines is clean, the gate self-test is green, and
every C++ test cited by the tree exists at 76b9dc2b2. The bisimulation
log against the C++ tree, over 4384 .test files, reports PASS 3816, FAIL
0, SKIP:negative 489, SKIP:no-journal 77 and SKIP:python 2, with both
detector fixtures green.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0143bpV19FtoLrrR59maPRPk
The Ellerman analysis, at the revision it examined (f5d891e), found in
its item I6 that the oracle skips an unresolved include silently,
although the comment beside splice in Driver.lean said the engine
reports it. The driver left such a line in the spliced text, and the
engine had no case for it, so it fell to the permissive skip of an
unrecognized directive and the rest of the journal was balanced without
the missing file. C++ rejects a missing file with "File to include was
not found" (src/textual_directives.cc:458-459, at 76b9dc2b2), and an
include without an argument with "Directive 'include' requires an
argument" (textual_directives.cc:1034-1038). Reading splice for that fix
showed a second silent loss: it returned the text unspliced once eight
levels of nesting were used up, while C++ counts the files that include
the current one and refuses an include directive only above 100
(textual_directives.cc:343-347).

Three rounds of review of the draft, by probes compared with the C++
build, found that the include rule could still lose, misread or wrongly
reject a file. splice took an include only when a space followed the
word, so "include", a tab and a file name was skipped silently, while
C++ splits the word at a space or a tab (next_element,
src/utils.h:623-640). globPaths joined an absolute or "~" path to the
directory of the including file, so it could read a file that C++ never
reads; it did not normalize ".." as C++ resolve_path does
(src/utils.cc:833-852); on a case-insensitive filesystem it accepted a
name that matches only in another case, where C++ matches each component
case-sensitively (textual_directives.cc:259-305, regress/1660); and it
read a directory that the pattern matched as if it were a file. splice
also resolved and depth-checked include lines inside comment and test
blocks, which C++ never executes, and because its self test compared
path strings as written, a top file named without a directory included
itself through a glob. The engine opened a comment block on "comment"
only when the word stood alone, and closed one only at a line equal to
"end comment" or "end test" once trimmed, narrower on both sides than
C++. Both block tests, and the reading of an include line, also saw the
line before the trim that C++ read_line applies to every line
(src/textual.cc:198-226): it drops a trailing vertical tab, form feed
and Unicode whitespace, so "comment" followed by a no-break space or a
form feed opens a block in C++, and "include part.dat" followed by a
form feed reads part.dat. Once an unresolved include became an error,
the oracle rejected an include inside such a block, or inside a block
that C++ opens with an argument or a prefix, and reported such an
include argument as not found.

Journal.lean gains an include case in handleDirective. It rejects a bare
include with the C++ reason in lower case, and an include whose argument
matched no file but the including one with "file to include was not
found", naming the argument as written where C++ names the resolved
path, and cites regress/1670, coverage-error-include-not-found and
coverage-directive-no-arg. Three helpers state the rules that C++
applies to a line before any directive sees it. readLineTrim drops the
trailing characters that std::isspace accepts, then the trailing Unicode
whitespace that is_unicode_whitespace names (isUnicodeWs, moved here
from Driver.lean; textual.cc:136-163, 198-226), and no ASCII whitespace
after that. directiveWord takes the first word, split at a space or a
tab, after at most two "@" or "!" prefixes, and the argument after the
spaces and tabs that follow it (textual.cc:321-324,
textual_directives.cc:1031, utils.h:598-602, 623-640). The tests that
open and close a comment or test block move into opensCommentBlock and
closesCommentBlock, which the engine and splice share, and now follow
C++: a block opens on the directive word "comment" or "test" of the
trimmed line, whatever argument follows
(textual_directives.cc:1035-1036, 1071-1072, 1124-1125), and closes at
the first line that begins with "end comment" or "end test", leading
whitespace kept (textual_directives.cc:941-949). The engine therefore
also skips the lines of a block opened with an argument, with a prefix,
or with trailing whitespace that only read_line drops, which it read
before.

In Driver.lean, splice now reads an include line as C++ does: on the
line as readLineTrim leaves it, in column 0 only (textual.cc:284-290),
with the directive word "include", and never inside a comment or test
block. It drops a byte order mark from each file as C++ does
(textual.cc:191-195), and closes at the end of an included file a block
that the file leaves open, since C++ reads a block no further than its
own file (textual_directives.cc:941-949). An include that it does not
resolve reaches the engine as a bare "include" or as "include ARG".
globPaths resolves as include_directive does
(textual_directives.cc:365-392): a path that begins with "/" is taken as
written, one that begins with "~" takes its prefix from HOME, any other
is joined to the directory of the including file, and the result is
normalized lexically and matched from the root one component at a time,
case-sensitively, with only directories continuing the walk and only
regular files matching last (textual_directives.cc:270-305). The
password database that C++ consults for "~user", or when HOME is unset,
is out of the oracle's reach, so such a path stays unresolved and the
include is rejected as not found. The top file is named as C++
open_for_reading names it (src/context.h:144-146, 157): normalized
lexically, which keeps the leading ".." of a relative path, and joined
to the current directory without a second normalization. The oracle
skips a match equal to the including file, as include_directive does
(textual_directives.cc:395-399), so a glob in a top file named by a path
that begins with ".." includes that file once more, as in C++. splice
counts the files that include the current one and throws "maximum
include depth exceeded (100)" for an include in a file that more than
maxIncludeDepth (100) files include, so a chain of 101 nested includes
is read and one of 102 is refused, as in C++. The whole read, the top
file included, runs inside one toBaseIO match in main, so a file that
cannot be read is reported for its own section on one "!! ERROR" line,
with the second line of the error's text joined to the first, and later
files are still processed; before this change the driver stopped there
with an uncaught error. The helpers that this change adds to Driver.lean
(maxIncludeDepth, normalComps, topPathname, expandHome and readSpliced)
are declared in the namespace Ledger.Driver; globPaths, splice and main
keep their names. The output protocol is unchanged: a rejection is
reported, as before, by one "!! ERROR" line and a reason.

Four departures in the matching of a path component are recorded in the
globPaths docstring and kept as they stood before this change. C++
compiles every component with mask_t::assign_glob
(textual_directives.cc:280, src/mask.cc:379-423), which leaves "^" and
"$" as anchors, takes a backslash as escaping the next character, and
copies a bracket expression into the regular expression as written
(mask.cc:390-395). The oracle compares a component without a glob
character exactly, matches "^" and "$" in a glob literally, drops every
backslash, and translates the characters inside a bracket as it does
outside one, so that "[^a]" is not a negated class and "x[*].dat" does
not match a file named "x*.dat". C++ also resolves a pattern that begins
with a backslash against the working directory of the process
(textual_directives.cc:370), where the oracle joins it to the directory
of the including file. For a name with "^" or "$", a pattern with a
backslash, or a bracket expression with "^", "*" or "?", the two can
therefore read different files.

The locked build script, lake build with warnings as errors, exits 0
after 1224 jobs with no warning in its log, and the definitions that the
DriverTwoSides proofs unfold are unchanged against f5d891e. The probe
journals of this unit, compared with the C++ build row by row, give
AGREE for chains of 100 and 101 nested includes, BOTH-REJECT for a chain
of 102 and for two files that include each other, BOTH-REJECT for a
missing file and for an include without an argument, and AGREE for an
include that resolves; before this change the same comparison gave DIFF
for the chains of 100 and 101, whose innermost transaction was lost, and
CPP-REJECTS for the other four. On the chain of 102 the oracle prints
"!! ERROR maximum include depth exceeded (100)". A second set of twelve
journals covers the first review: a tab after the word, with an existing
and with a missing file and after "!"; an absolute path to an existing
file and to a missing one; a wrongly cased name; a path through a
missing directory and ".."; a glob that matches a directory beside a
file; a cycle whose closing include sits in a comment block; a comment
block left open by an included file; a byte order mark before an
include; and an indented include line, which C++ reads as a posting.
Against the C++ build they give eight AGREE and four BOTH-REJECT, where
the oracle before this change gave seven DIFF, four CPP-REJECTS and one
ORACLE-REJECTS. A third set of fifteen covers the second review. Eleven
of them hold a missing include inside a comment block opened with an
argument, after "@", after "!" and with a tab; a missing include after
an indented "end comment", which leaves the block open, and after "end
commentary", which closes it; an existing include after "end comment"
followed by text; a transaction inside a block opened with an argument;
and an include followed by a no-break space, by a space and a no-break
space, and by an ideographic space alone. They give eight AGREE and
three BOTH-REJECT, where the oracle before this change gave five AGREE,
three DIFF and three CPP-REJECTS. The other four pin the departures at
their direction: names with "$" and with "^" and a pattern with an
escaped "*" give CPP-REJECTS, as before this change, and an include of a
file without read permission gives ORACLE-REJECTS, by one "!! ERROR"
line and no other line in its section, where before this change the
driver stopped. A fourth set of twenty covers the third review.
Thirteen of them hold a comment or test block opened on a word followed
by a no-break space, an ideographic space or a form feed, at the top of
a journal and in an included file, with a missing include, an existing
include or a transaction inside it; a block that an included file opens
on its last line, without a line end; and an include argument followed
by a form feed and by a vertical tab. They give thirteen AGREE, where
the oracle before this change gave six AGREE and seven DIFF. The other
seven pin at their direction behavior that this change keeps: bracket
expressions holding "*" and "?" give ORACLE-REJECTS, where before this
change the include was skipped and they gave DIFF; a bracket holding "*"
beside a decoy file named "x].dat" gives CPP-REJECTS, and two negated
classes give DIFF, as before this change; and the two splicing gaps
named in the last paragraph give DIFF and CPP-REJECTS, as before this
change. From the semantics checkout, a top file whose "include *.dat"
also matches itself is read twice by C++ and by the oracle when it is
named by a relative path that begins with "..", and once by both when it
is named by its absolute path or, from its own directory, as "top.dat",
"./top.dat" or "x/../top.dat".

Over the 469 negative journals extracted from the C++ suite the oracle
rejects 123, thirteen more than before, none of them a journal that C++
accepts. Its default output over them differs from the output recorded
after the previous unit, by diff, only in the sections of thirteen
include tests, each of which C++ rejects: regress/1670, 1862,
1862-quoted-include, 6188B0EC, BF3C1F82-2, cov4-directive-no-arg,
coverage-directive-no-arg, coverage-error-include-not-found,
coverage-session-parse-error and the four coverage-utils-tilde tests.
The oracle's default output over the 3816 positive journals is
byte-identical to the recorded snapshot. The scan of added Lean lines
finds no forbidden construct, the register check of added lines is
clean, the gate self-test is green, and every C++ test cited by the tree
exists at 76b9dc2b2. The bisimulation log against the C++ tree, over
4384 .test files, reports PASS 3816, FAIL 0, SKIP:negative 489,
SKIP:no-journal 77 and SKIP:python 2, with both detector fixtures green.

Two departures remain visible rather than silent. The oracle reads every
file as UTF-8, so a journal or an included file that is not valid UTF-8
is rejected with the reader's error where C++ reads it; and C++ reads a
file that it finds but cannot open, such as one without read permission,
as empty and accepts the journal, where the oracle rejects it. Two gaps
in splicing predate this change and remain silent: an "apply account"
that an included file leaves open applies to the later transactions of
the including file, where C++ gives each included file its own apply
stack (textual_directives.cc:427-431), and an indented posting after an
include line joins the last transaction of the included file, where C++
rejects it (textual.cc:284-290).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0143bpV19FtoLrrR59maPRPk
The Ellerman analysis, at the revision it examined (f5d891e), found in
its item I5 that the Balancing bullet of the Journal.lean module
docstring misstates the conservation contract. It said that each
posting contributes its lot-price value if annotated, else its cost,
else its own amount, while balancePool weighs a posting at its cost or
its own amount in a first pass that values no lot apart from the gain
of a lot priced in its cost's commodity, and substitutes lot values
only in a second pass, taken when the first leaves a real residual it
cannot accept. It also did not say that the check is made on these
weights while the recorded flows are each posting's own amount, so that
a transaction balanced through a cost, a lot value or an implicit
exchange can leave nonzero per-commodity totals, as C++ does:
test/regress/1784.test expects totals of 28 ABCDEF and -280.00 EUR, and
test/baseline/cmd-equity.test expects equity to book the negation of
such totals to Equity:Opening Balances. Three engine changes since
that revision narrowed the bucket to a transaction of a single posting,
confined the implicit exchange to the cases of C++ Phase 3, and widened
the zero test for a cost commodity of display precision 0
(regress/1114); the bullet named the first two but neither that
widening nor the regress/1125 tolerance.

The bullet now has one sub-bullet for each part of the procedure. The
pools are real and deferred postings together, bracket-virtuals apart
(regress/563), and paren-virtuals exempt. The first pass weighs a
posting at its cost, to which outside a periodic template a lot priced
in the cost's commodity adds its rounded gain (baseline/opt-base); C++
Phase 6 does so for a posting of the real pool whose amount has a
commodity (xact.cc:692-729), and the bullet names the two departures
outside that case. Then come the elided posting and the bucket
(xact.cc:368), and the implicit exchange with its Phase 3 citation,
whose verdict decides wherever C++ converts, together with the
order-dependent rule, stated in full rather than by reference, which
applies when the two orders in which C++ may take the entries lead to
different verdicts. The zero test carries the regress/1114 widening
and the exact test of the null commodity, and the tolerance by which
the oracle, in either pool, stands in for the per-unit cost rounding
that C++ adjusts in the real pool alone (regress/1125). The second
pass has its witness
(regress/coverage-wave3-fixated-price) and the statement that the
bracket-virtual pool has none, and the own-amount flows have the two
C++ witnesses above. The bullet also states where C++ values a lot: in
Phase 4 for a fixated lot on a posting without a cost, and in Phase 6
for a lot whose posting has a cost, written or computed, in the lot
price's commodity or whose cost itself carries a lot price in that
commodity (xact.cc:475-518, 692-774), adding neither value to the
bracket-virtual balance (xact.cc:505-516, 712-713). Since the second
pass values every lot of the real pool, the bullet points to the
docstring of impliedExchange for the lot shapes known to be accepted
in consequence.

The plan's prepared diff does not apply to the tree as the earlier
units left it (git apply --check exits 1), since those units rewrote
the same bullet, and the text was written again by hand on the current
tree with four corrections. The diff cited opt-base for the second
pass, but its second transaction balances in the first, since the gain
of its lot priced in dollars enters the cost weight; opt-base is now
cited for that and the fixated-price test for the second pass. It said
that the lot value replaces the cost in the second pass, which a
periodic template does not do, since its cost keeps precedence
(xact.cc:486, 522); the bullet says so. It said that such a transaction
leaves nonzero totals, where two costs can cancel; the bullet says that
it can. It described the tolerance as per-unit cost rounding, where the
oracle accepts a residual within the sum of half a unit in the last
place of each cost written with decimals in its commodity; the bullet
states that rule and names regress/1125 as what it stands in for. The
phrases of the earlier units are kept, and the bucket is now described
as completing a transaction of a single posting, counting the parsed
postings of every kind, so that the phrase check of the plan's critic
issue R32 can read it.

Review of the first draft of this change found that the docstring of
impliedExchange, which the bullet cites, said that C++ values only
fixated lots and the lot of the implicit exchange, whereas Phase 6
also values the lot of a posting whose written cost is in the lot
price's commodity. With ledger 3.4.1 and a current C++ build,
10 AAA {$5} @ $6 against -$50 balances through that gain. The
docstring now states the rule above, and its list of the lot shapes
that C++ rejects and the second pass accepts gains a seventh, a lot
whose posting has a cost in another commodity: 10 AAA {5 EUR} @ $6
against -50 EUR, which both C++ builds reject with a remainder of $60
and -50 EUR and which the oracle accepts at the lot value. The same
review found that the tolerance of regress/1125 also applies in the
bracket-virtual pool, where C++ makes no adjustment (xact.cc:910) and
tests the balance at the display precision (xact.cc:973-979); the
regress/1125 journal with every posting bracket-virtual is rejected by
both C++ builds with a remainder of $-0.01 and accepted by the oracle,
and the comment beside the tolerance now records that fail-open
departure. The comment at the second pass no longer cites opt-base,
which never reaches it, and opt-base is named as the witness of the
gain instead.

Review of the second draft found that the gain which costVal adds
departs from Phase 6 in two cases that no comment recorded, while the
draft said that the oracle adds it as Phase 6 does. C++ adds the gain
of a bracket-virtual posting to the real balance (xact.cc:712-713) and
tests the bracket-virtual balance with the cost as written
(xact.cc:322-325, 973-979), and it keeps no lot on an amount with no
commodity (amount.cc:1908), so that it adds no gain for one. Ledger
3.4.1 and a current C++ build agree on six probe journals, and the
oracle departs from them in both directions. A bracket-virtual
10 AAA {$5} @ $6 against a bracket-virtual -$50, alone or beside a
balanced real pair, is rejected by C++ with a remainder of $-10 and
accepted by the oracle; against a bracket-virtual -$60 beside a real
$10, with or without a real posting of $0, it is accepted by C++ and
rejected by the oracle. 10 {$5} @ $6 against -$50 is rejected
by C++ with a remainder of $10 and accepted by the oracle, and against
-$60 it is accepted by C++ and rejected by the oracle. The comment
beside costVal now records both departures with these journals and
their directions, and the bullet confines its claim of agreement to a
posting of the real pool whose amount has a commodity. The engine is
unchanged; aligning costVal with C++ is executable-layer work for a
later unit with its own witness. The same review found that the
impliedExchange docstring still called the bracket-virtual first pass
lot-blind; it now says that this pass keys an amount without its lot,
which is why the first three shapes net to zero there, and that the
gain is a further departure there. It also found that the docstring
said the second pass values every lot, which a periodic template does
not do where a cost is written, that the bullet did not say that a
verdict of C++ in Phase 3 excludes the tolerance and the second pass,
that the fixated-price citation sat beside the order-dependent rule
instead of the second pass, and that one docstring line was 99
columns wide. Each is corrected.

Review of the third draft found four points of wording, each now
corrected. The comment at the bracket-virtual return of balancePool
still said that C++ values no lot in that pool, although Phase 4 sets
a cost on a bracket-virtual fixated lot (its loop at xact.cc:485-488
does not skip a virtual posting) and Phase 6 computes a bracket-virtual
gain; the comment now says that C++ adds no lot value to the
bracket-virtual balance, since the recompute of Phase 4 skips those
postings and Phase 6 adds their gain to the real balance
(xact.cc:505-516, 712-713, 973-979). The order-dependent rule read as
though the two orders always lead to different outcomes, where that is
its second condition (impliedExchange gives .unordered only then); it
now says "when", and it sits with the implicit exchange, whose verdict
it concerns, rather than with the zero test. The opt-base witness sat
above gainVal, which feeds only the capital_gain binding of automated
transactions, while balancePool weighs the gain that costVal computes;
the witness now sits beside costVal. That comment also said that each
departure fails either way, which reads as always failing, and now
says that each can.

A review of an earlier unit found that the Timeclock bullet called its
postings single-sided, which is false when a bucket account is
declared: the check-out rule then posts the opposite duration to the
bucket, as C++ create_timelog_xact does (src/timelog.cc:84-91 at
76b9dc2b2, test/regress/1607.test), and the rule cited no witness. The
bullet now says that the opposite amount goes to the bucket account
when one is declared, and a comment beside the check-out rule cites
those lines and post.h:244-245, under which a timelog posting need not
balance. A comment beside the automatic check-out of open entries at
the pinned time cites time_log_t::close (timelog.cc:173-195), which
reaches the same function.

Every change is to a docstring or a comment. The locked build script,
lake build with warnings as errors, exits 0 after 1224 jobs with no
warning in its log. The oracle run on the journals of regress/1784,
baseline/cmd-equity and regress/1607, extracted from the C++ tree at
76b9dc2b2, prints the own-amount rows 28 ABCDEF and -280 EUR, 4 AAA and
-3.8 GBP, and 4 h and -4 h, the last to Equity:Time. The edited text
cites eight C++ tests. On the journals of seven of them its output is
byte-identical by cmp to its output before the change, and on the
eighth, the negative test regress/1114, its rejection is byte-identical
by cmp to the entry for that file in the negative-corpus snapshot of
the previous unit. Whitespace-folded phrase
checks of the bullet find "transaction of a single posting", the
implicit exchange with its Phase 3 citation, regress/1114 and
regress/1125 once each, and the file no longer contains
"single-sided" or "lot-blind".

The scan of added Lean lines finds no forbidden construct, the register
check of added lines is clean, the gate self-test is green, and every
C++ test cited by the tree exists at 76b9dc2b2. The oracle's default
output over the 3816 positive journals is byte-identical to the
recorded snapshot, and over the 469 negative journals it is
byte-identical by cmp to the output recorded for the previous unit. The
bisimulation log against the C++ tree, over 4384 .test files, reports
PASS 3816, FAIL 0, SKIP:negative 489, SKIP:no-journal 77 and
SKIP:python 2, with both detector fixtures green; the unit's tail
check exits 0.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0143bpV19FtoLrrR59maPRPk
The plan's decision D18 chose that a balanced periodic template should
keep the display precisions and amount styles that its posting amounts
teach, as C++ does. That decision is reversed here: a template teaches
the oracle nothing, as it did before, and the departure from C++ is
recorded beside the rule. An implementation of D18 was built, measured
against ledger 3.4.1 and a current C++ build, and withdrawn. It had
grown to 497 added and 80 removed lines in Ledger/Journal.lean, and it
changed the oracle's output on no file of the real corpus: the tail
check of that tree found the default output over the 3816 positive
journals identical to the recorded snapshot, and the output over the
469 negative journals byte-identical by cmp to that of the previous
unit. The cause is structural. A template that teaches inherits every
departure of the oracle's precision and style model for dated amounts,
and each family of template journals measured against the two C++
binaries exposed a further one: a name that a later alias claims, a
commodity locked by a D directive or a format subdirective, the
tentative precision of a P directive, the style of the null commodity,
and the precision of an amount expression. Each needed an exclusion of
its own, and the exclusion for amount expressions made the template's
own zero test ignore their precision, so that a template of
`X ($1.5551)` against `Y $-2`, which both C++ builds reject as
unbalanced, was accepted. Under the convergence rule, by which the
previous behavior stands where new logic is not demonstrably correct,
the engine is unchanged, and this commit changes only comments and
docstrings of Ledger/Journal.lean.

The comment on periodic templates in flushPending now states what C++
learns and what the oracle discards. C++ parses a template's postings
before it balances them (src/textual_xacts.cc:312 at 76b9dc2b2), and
each posting amount migrates its display precision and style to its
commodity there as in a dated transaction (amount.cc:1830-1864); a
cost or a lot price, parsed without migration (textual_xacts.cc:691,
693-694; annotate.cc:187), teaches nothing on either side. The comment
gives the reason for discarding what a template teaches, and one
witness for each effect with its direction. Each witness was run on
ledger 3.4.1, build/ledger and the oracle of this tree, with the
template written as `~ Monthly`, the posting named and an elided `Y`:

- rows printed: after `X $500.1`, both C++ builds print a later
  `(1/3 $)` as `0.3 $` and the oracle prints no row (wrong rows);
  after `X 1.000,50 EUR`, they print a later `1234 EUR` as
  `1.234,00 EUR`, and the oracle prints the same value, EUR|1234, but
  declares no `%% dc EUR` (a missing declaration);
- balancing verdict: after `X $1.000`, a later `A 3 X @ $0.333`
  against `B $-1.00` is rejected by C++ with a remainder of $-0.001
  and accepted by the oracle (fail-open);
- reading of later amounts: after `X 10,50 EUR`, a later `5.25 EUR` is
  rejected by C++ ("Incorrect use of thousand-mark period") and
  accepted by the oracle (fail-open); after `X EUR 10,50`, a later
  `EUR 1,500` against `EUR -1,5` is accepted by C++ and rejected by
  the oracle (fail-closed);
- costs and lot prices: after `X 10 AAPL @ $1.2345` or
  `X 10 AAPL {$1.2345}`, no side prints a row for a later `(1/3 $)`.

The comment it replaces said that C++ prints the first of these
balances as `$0.3`; both C++ builds print `0.3 $`, since the literal
`3 $` gives the commodity a suffixed style. The bullet on periodic
templates in the module docstring now names the departure. The
comment on the `@` branch of annotLoop no longer says that a cost
literal teaches display precision, which contradicted both the code,
where its decimals reach only costDecs, and the measurement above. The
example of zeroTestPrec gives its template an elided `Y`, since a
template of one posting is rejected by both sides as unbalanced, and
writes its rate as `apply fixed BAZ 0.50 EUR`, since a block that
fixes FOO over the transaction makes C++ reject it as well; with these
changes each of its five examples is rejected by the oracle and
accepted by both C++ builds, as the docstring states.

Seven departures of the dated path that the measurements exposed are
recorded beside their rules, each with an inline journal and its
direction, and each re-run on both C++ builds and the oracle of this
tree:

- the D directive: C++ parses its amount with migration before it
  locks the commodity (textual_directives.cc:149-166), so that a
  higher precision learned earlier survives (amount.cc:1847-1848).
  After `A $500.12345` and then `D $1,000.00`, C++ prints A as
  `$500.12345` and a later `($1/3)` as `$0.33333`, where the oracle
  prints 500.12 and 0.33 (wrong rows), and a later `$1.001` against
  `$-1.00` is rejected by C++ with a remainder of $0.00100 and
  accepted by the oracle (fail-open);
- the P directive: C++ grants a tentative precision when the
  commodity's precision is 0 and the commodity is neither known nor
  locked (pool.cc:628-634), and the source commodity of a price
  directive becomes known (pool.cc:647), while the oracle grants it
  only to a commodity with no recorded precision. After `X $10` and
  `P 2024/01/01 EUR $1.2345`, C++ prints a later `(1/3 $)` as
  `0.3333 $` and the oracle prints no row (wrong rows), and a later
  `($3/10)` against `$0` is rejected by C++ and accepted by the oracle
  (fail-open); after `P 2024/01/01 EUR $1.2345` and
  `P 2024/01/01 FOO 1.25 EUR`, C++ prints no row for a later
  `(1/3 EUR)` and the oracle prints 0.33 (wrong rows);
- the style rule of finalizeXact, for the null commodity: C++ reads a
  bare amount with that commodity's style, which only a D directive
  changes (amount.cc:1714-1715, 1850; textual_directives.cc:157).
  After a bare `X 10,50`, a later bare `5.25` is accepted by C++ and
  rejected by the oracle as ambiguous (fail-closed);
- the same rule, for a locked commodity: C++ migrates no style to a
  commodity that a D directive or a format subdirective has locked
  (amount.cc:1836; textual_directives.cc:165, 875). After
  `D 1,000.00 EUR` and `X 10,50 EUR`, a later `5.25 EUR` is accepted
  by C++ and rejected by the oracle (fail-closed); after `D $1,000.00`
  and `X $1,5`, or `format 1,000.00 EUR` in a block `commodity EUR`
  and `X 10,5 EUR`, C++ reads a later `$1,500` or `1,500 EUR` as 1500
  and the oracle reads 1.5 (wrong rows);
- the same rule, for an amount expression: the oracle learns no style
  from it, since parseAmount reports none, while C++ parses each
  literal of a posting expression with migration
  (textual_xacts.cc:585-587, token.cc:548-555), so that the literal's
  style reaches its commodity. After `X (10,50 EUR)`, a later
  `5.25 EUR` is rejected by C++ ("Incorrect use of thousand-mark
  period") and accepted by the oracle (fail-open), and C++ reads a
  later `1,500 EUR` as 1.5 where the oracle reads 1500 (wrong rows);
- the alias subdirective: C++ retires a commodity whose name a later
  alias claims (pool.cc:150-173). After `X 10,50 E` and then
  `commodity EUR` with `alias E`, C++ reads a later `1,500 E` as
  1500 EUR and the oracle reads 1.5 (wrong rows), and a later `5.25 E`
  is accepted by C++ and rejected by the oracle (fail-closed);
- the precision rule of finalizeXact, for an amount expression: the
  oracle teaches the result's commodity the largest number of decimals
  among the literals, while C++ migrates each literal's precision to
  that literal's own commodity and a bare literal's to none
  (token.cc:540-555, amount.cc:1836). After `X ($1 * 1.555)`, C++
  prints X as `2 $` and no row for a later `(1/3 $)`, where the oracle
  prints 1.555 and 0.333 (wrong rows), and after `X (1.234 * $2)` a
  later `($1/3)` against `$-0.33` is accepted by C++ and rejected by
  the oracle (fail-closed).

The comment beside the D directive no longer says that the directive
locks the style, which lockPrecision does not do, cites
textual_directives.cc:165 rather than amount.cc for the lock, and
loses an em-dash. Every C++ file cited in these comments is unchanged
between 76b9dc2b2 and the current C++ tree (git diff --quiet exits 0
for each).

Departures that the same measurements exposed but whose rules live
outside Journal.lean are left to a later unit and are recorded only in
this message. The truncated function of Parse.lean rounds at the
precision recorded under the name written, while C++ truncates at the
display precision of the commodity, to which the literal in the call
has already migrated its decimals (report.cc:1159-1168 at 76b9dc2b2,
token.cc:540-555): after `X 10.555 E`, a later
`(truncated(1.23456 E))` against `-1.235 E` is rejected by C++ and
accepted by the oracle (fail-open), as is `(truncated(1.23456))`
against `-1.235` after a bare `X 0.001`. Driver.lean rounds a
half-way display value away from zero: after `X 10,5 EUR` and
`X 10 AAPL @ 1,555 EUR`, each against an elided `Y`, C++ prints the
balance of Y as `-26,0 EUR` and the oracle prints -26.1 (wrong rows).
Driver.lean also prints a bare `(1/3)` as 1/3, where C++ prints
0.333333 (wrong rows), and Parse.lean rejects `(2.5 EUR * $1 / 1 EUR)`
as mixing commodities and `(EUR 1/3)` as an unknown variable, both of
which C++ accepts (fail-closed).

The scratch probe sets of the plan follow the reversal. S27's set holds
the D18 design's four journals at their recorded verdicts, the template
witnesses above, the template journals gathered while D18 was attempted
(each agrees with C++, or is rejected by both, only while a template
teaches nothing), and the three unbalanced templates with amount
expressions; a second set holds the witnesses of the seven dated
departures (15 journals). The periodic-template journal of S24's set
returns to that set at ORACLE-REJECTS, as S24 stated it. The row
comparator of the scratch tools now reports a file whose C++ rows it
cannot read under the oracle's `%% dc` declarations as CMPFAIL instead
of stopping, since one such file had hidden the verdicts of every file
after it; the expected verdicts were derived again from the stated
directions and the C++ binary alone, and only the sections of S27
changed.

Every change to Ledger/Journal.lean is to a comment or a docstring.
The locked build script, lake build with warnings as errors, exits 0
after 1224 jobs with no warning in its log. The scan of added Lean
lines finds no forbidden construct, the register check of added lines
is clean, the gate self-test is green, and every C++ test cited by the
tree exists at 76b9dc2b2. The oracle's default output over the 3816
positive journals is byte-identical to the recorded snapshot, and over
the 469 negative journals, with 123 rejections, it is byte-identical by
cmp to the output recorded for the previous unit. The probe sets of
S27 match their expected verdicts (58 verdicts), and so do those of
every unit through S27 (220 verdicts). The bisimulation log against
the C++ tree, over 4384 .test files, reports PASS 3816, FAIL 0,
SKIP:negative 489, SKIP:no-journal 77 and SKIP:python 2, the tally of
the previous unit, with both detector fixtures green; the unit's tail
check exits 0.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0143bpV19FtoLrrR59maPRPk
C++ verifies a transaction once more after an automated transaction has
added a posting that must balance: auto_xact_t::extend_xact calls
xact.verify() (src/xact.cc:1683-1684 at 76b9dc2b2), which sums the real
and bracket-virtual postings into one balance and tests it at display
precision (xact.cc:1040-1110). The oracle makes no such check and so
accepts journals that C++ rejects with "While applying automated
transaction"; the Ellerman implementation plan records this as item N1
(decision D7). The rule that closes the gap needs values that
finalizeXact computes and discards: for each orientation in which C++
can take a balancing pool and accept it, the two entries that it
converts and what it leaves for the final zero test; the postings that
each automated rule generates and the posting each was generated from;
and whether a lot price was written as a total. This change returns
those values without using them, so that the rule can be reviewed on its
own, and it changes no output.

balancePool now returns a PoolResult: the completed postings as before,
one candidate per admissible orientation of Phase 3, and a flag set when
three conditions hold: the pool reaches the test of the implicit
exchange, with no elided posting, a nonzero exact residual and no
completion by the bucket; the verdict of Phase 3 there depends on the
unspecified order of its two entries; and the pool is accepted, by the
earlier rule that passes a residual in two commodities or by any other
test. An orientation is admissible when C++ can take it and accept the
pool, as the oracle models C++, and its candidate pairs the two entries
that it converts, x first, if it converts, with the entries that it
leaves for Phase 8. A pool that balances exactly, through its elided
posting or through the bucket has one candidate with no conversion and
nothing left; a pool accepted by the implicit exchange, or under an
order-dependent verdict, has the candidates that impliedExchange finds,
in the second case possibly none; and a pool accepted otherwise has one
candidate with no conversion, whose residual is the exact residual under
the zero test or the per-unit cost allowance and otherwise the residual
of the second pass. The allowance leaves the residual unadjusted, since
C++ adjusts only the balance that Phase 8 tests (xact.cc:898-929,
regress/1125). The candidates that do not come from impliedExchange rest
on residuals that merge by commodity, or value, the lots that C++ keeps
apart (annotate.h:120-125), and so they inherit the lot departures that
the docstring of impliedExchange lists: 10 A {$1} against -10 A has one
empty candidate, while C++ rejects it. Among them is the candidate of an
orientation that the top posting's key decides and in which Phase 3
makes no conversion, which carries the merged residual: under
format 1.00 A, 0.004 A {$1} against -0.001 A leaves 0.003 A, while C++
tests the two entries apart. An exactly balanced pool leaves the flag
unset even where the verdict of C++ depends on the order: 10 A {$1}
against -10 A beside bracket-virtual postings of 1 B {$1} and -1 B {$1}
is rejected by C++ with the lot first and accepted with the plain units
first.

Each candidate of impliedExchange lists the entries that Phase 8 tests,
each with its remainder, exact zeros dropped. An orientation that
converts lists x, y with any gain booked against it, and z with a gain
between two lots. An orientation that makes no conversion, x and y being
one base with a lot price on x only, leaves the two entries apart in the
real balance, and it is admissible only when each entry passes the zero
test on its own, after Phase 4 has replaced a fixated lot by its cost in
the lot price's commodity (xact.cc:475-518). The verdict in one
orientation, formerly an optional Boolean, is now a small inductive
type: no conversion, with the entries left when each passes; a
conversion that fails; or a conversion that passes, with its entries.
The converting verdict tests each entry at its display precision, the
same conjunction as before, and the failing verdict, formerly a Boolean
argument of the accepting constructor of the exchange, is a constructor
of its own.

The real pool's zero test moves to the scope of finalizeXact as
settlesWide, unchanged, and its comment records where verify() differs:
it makes no allowance for rounded per-unit costs, and it widens by the
cost of every posting of the extended transaction, the cost that a
generated posting copies from its template included (xact.cc:1076-1091,
1611-1612). The PoolResult docstring records that verify() sums the
costs that Phase 6 leaves, which differ from the balance of Phase 8 for
an exchange of two lots priced in one commodity (xact.cc:764-774): C++
accepts 10 AAA {$5} against -5 BBB {$10}, and rejects it with the
remainder $-50 and 10 AAA {$5} once an automated transaction adds
Assets:X 1 and Assets:Y -1 to the AAA posting. applyAutos also returns
one batch per rule, in rule order, of triples of the matched posting,
the generated posting, and a flag set when the template posting has no
fixed amount and the generated amount keeps the matched commodity, which
covers the bare-number multiplier. The flag is a precondition of the
cost that C++ derives from the copied annotation (xact.cc:1546-1547,
1611-1627), not that cost: C++ derives one only when the matched posting
has a cost, written, computed by Phase 3 in the orientation taken, or
derived by Phase 4 from a fixated lot price (xact.cc:475-518), so the
full condition depends on the orientation and on Phase 4. Beside a
fixated top posting, which Phase 3 never converts, only Phase 4 gives
the matched posting a cost: under = /Assets:A/ with Assets:X 0.5 and
Assets:Y $-25, the C++ build and ledger 3.4.1 both accept 10 AAA {=$5}
against $-50, and with Assets:Y $-20 both reject it with the remainder
$5.

PostSpec.lotTotal records a lot price of the posting's own written as
{{T}} on an amount with a commodity. C++ marks such an annotation
ANNOTATION_PRICE_NOT_PER_UNIT (annotate.cc:161-164), and when it derives
a cost from an annotation that bears the mark it uses the per-unit price
unscaled and negated for a negative amount (xact.cc:1611-1621). The mark
belongs to the interned commodity, not to the posting: it is not a
semantic flag (annotate.h:95), and the pool returns the annotated
commodity already interned for the same base, per-unit price, date, tag,
value expression and fixated flag (pool.cc:200, 212-243), so the first
lot of a key in parse order decides the mark for every later lot of that
key. The docstring of the field records the departure that a rule
reading it would make in each direction, with witnesses on which both
C++ binaries agree. Under = /Assets:A/ with Assets:X 1 and
Assets:Y $-50, 10 AAA {{$50}} @ $5 against $-50 is rejected with the
remainder $-45 alone and accepted after an earlier transaction of
10 AAA {$5} @ $5, where the field is set (fail-closed); 10 AAA {$5} @ $5
against $-50, accepted alone, is rejected after an earlier transaction
of 10 AAA {{$50}} @ $5, where the field is not set (fail-open).

Two existing departures that these values expose are recorded and left
as they are, since this change alters no output. The docstring of
impliedExchange gains the fail-closed counterpart of its lot shapes:
where Phase 3 makes no conversion and the lot's posting is the top
posting, C++ tests a lot and plain units of its commodity apart
(xact.cc:898), so under format 1.00 A and format $1.00 it accepts
0.004 A {$100} beside 0.004 A in either order, which the oracle rejects.
Under a bracket-virtual top posting of another commodity the existing
order-dependent departure applies instead. A comment beside the template
parser records that C++ multiplies by every template amount without a
commodity (xact.cc:1546-1547), while the parser takes one that is not a
bare number as a fixed amount: with Assets:X 1 [2024/01/01] and
Assets:Y -1 [2024/01/01], or 1 @ $1 and -1 @ $1, applied to 10 AAA
against -10 AAA, C++ generates 10 AAA and -10 AAA, and the oracle 1 and
-1.

The plan was written before the implicit exchange keyed its entries by
lot and modelled the remainders that bracket-virtual postings and Phase
6 gains leave, and before the order-dependent rule. The change follows
the tree: the exchange entries carry their lot keys; a residual lists
every entry that the verdict tests, which reduces to the plan's forms
(nothing, or the doubled y of a same-sign exchange) when no such
remainder or gain arises; each residual is paired with the conversion of
its own orientation, where the plan had one exchange beside a list of
residuals, so the plan's fields residuals and exchange are named
candidates and orderDependent; the plan's derivesCost is the
precondition described above, since the full condition depends on the
orientation and on Phase 4; and lotTotal is read from the lot literal
that annotLoop already reports. The plan cites xact.cc:1618-1633 for the
derived cost; at 76b9dc2b2 the lines are 1611-1627.

The change was first compiled on a scratch copy of the tree, where
Journal, Driver and DriverTwoSides compiled with warnings as errors. A
traced copy of the tree, not part of this change, printed the new
values. 3 X @ $0.333 against $-1.00, which the zero test accepts, leaves
the residual $-0.001 with no conversion, and the same pair as
bracket-virtual postings leaves it in that pool. A same-sign exchange of
10 X and 0.001 Y under format 1.00 Y leaves 0.002 Y. With 5 GOOG and
-5 GOOG ahead of 10 EUR and -11 USD, both orientations are admissible
and each leaves nothing. With 5 GOOG and -5 GOOG ahead of 0.6 EUR and
$0.001 under format $1.00 and format 1,000 EUR, which C++ accepts only
when EUR is converted, the flag is set and the one candidate converts
EUR and leaves $0.002. Under format 1.00 A, format $1.00 and a
bracket-virtual top posting of another commodity, where the C++ builds
measured take as x the entry that entered the balance first, the flag is
set in each of the following. 10 A {$1} against -10.004 A gives only the
converting candidate, and the C++ build and ledger 3.4.1 both reject the
file with the lot first and accept it with the plain units first.
0.004 A {$100} beside 0.004 A gives one candidate with no conversion
that holds both entries, and both accept the file with the lot first and
reject it otherwise. 0.1 A {=$0.001} beside 0.004 A gives one candidate
with no conversion that leaves $0.0001 and 0.004 A, and both accept it
with the lot first. 0.004 A {=$10} beside 0.004 A gives no candidate,
and both reject it in either order, with the remainder $0.04 when the
lot comes first. 10 A {$1} against -5 A beside an amountless
paren-virtual posting gives the flag and no candidate. The {{72.00 EUR}}
lot of the auto-lottotal probe sets lotTotal, while {{$50}} on an amount
without a commodity does not.

On the working tree the locked build script, lake build with warnings as
errors, exits 0 after 1224 jobs with no warning in its log. The oracle's
raw output on the 43 N1 probe journals, recorded before the edit, is
byte-identical by cmp after it, and against C++ the probes still give 27
AGREE and 16 CPP-REJECTS. The negative-corpus count reports 123 of 469
journals rejected, as before. The scan of added Lean lines finds no
forbidden construct, the register check of added lines is clean, the
gate self-test is green, and every C++ test cited by the tree exists at
76b9dc2b2. The oracle's default output over the 3816 positive journals
is byte-identical to the recorded snapshot, and over the 469 negative
journals it is byte-identical by cmp to the output recorded for the
previous unit. The bisimulation log against the C++ tree, over 4384
.test files, reports PASS 3816, FAIL 0, SKIP:negative
489, SKIP:no-journal 77 and SKIP:python 2, with both detector fixtures
green; the unit's tail check exits 0.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0143bpV19FtoLrrR59maPRPk
C++ verifies a transaction once more after an automated transaction
has added a posting that must balance: auto_xact_t::extend_xact runs
one rule at a time and, after a rule that generated such a posting,
calls xact.verify() (src/xact.cc:1677-1684 at 76b9dc2b2). verify()
sums the real and bracket-virtual postings into one balance, each at
its cost if it has one and else at its amount, makes no implicit
exchange and no allowance for rounded per-unit costs, and tests the
balance at display precision (xact.cc:1040-1110). The oracle made no
such check, so it accepted journals that C++ rejects with "Transaction
does not balance" after "While applying automated transaction"; the
Ellerman implementation plan records the gap as item N1 (decision D7).
Before this change, 16 of the 43 N1 probe journals were accepted by
the oracle and rejected by the C++ build. The witness is the probe
auto1: under = /Food/ with Assets:Savings 0.1, C++ rejects
Expenses:Food 10.00 USD against an elided Assets:Cash.

The new function verifyExtended makes the check, and finalizeXact
calls it after applyAutos. The balance before the extension is the
residual of a candidate of the real pool, the balance that Phase 8 also
tests, together with the residual of the bracket-virtual pool. A
generated posting weighs the cost written on its template posting,
which C++ copies to it (xact.cc:1611-1612). Otherwise, when the
matched posting has a cost, written or computed by Phase 3, and the
generated amount carries a lot price, it weighs the cost that C++
derives from that price (xact.cc:1613-1627): the price per unit times
the generated quantity, or for a lot written {{T}} the price per unit
unscaled and negated for a negative quantity, since the annotation
keeps ANNOTATION_PRICE_NOT_PER_UNIT (amount.cc:1909-1923). The lot
price is the template amount's own; else, when the generated amount
copies the matched posting's annotation, the lot price that the
matched posting writes; else, in a transaction with a date, the
per-unit cost with which Phase 6 annotates the matched amount
(xact.cc:802-811, pool.cc:437, 508-522), that is the written cost per
unit or the rate of Phase 3 when the orientation converts the posting.
Otherwise it weighs its amount. The zero test widens a cost commodity
of display precision 0 as Phase 8 does (xact.cc:879-896, 1076-1091),
reading the copied costs of the generated postings too. C++ also
extends and checks a periodic template (textual_xacts.cc:316-317),
which has no Phase 6 and so no annotation from it, and the check runs
there as well; the comment of flushPending, which listed this as an
accepted departure, now records the check instead.

The check runs in every orientation of Phase 3 that the real pool
admits, rule by rule over the weights so far. A transaction that
passes in every orientation continues; one that fails in every
orientation is rejected with "line N: unbalanced transaction after
automated transaction (residual in K commodities)", N being the
transaction's first line, which C++ names in its "While balancing
transaction" context; one whose verdict depends on the orientation is
refused with "out-of-scope automated posting weighed through an
implicit exchange whose orientation depends on commodity primacy
(#1130) or on the order of the balance entries". Both causes decide
the orientation in C++ and neither is recorded here: the swap toward
a COMMODITY_PRIMARY commodity (xact.cc:425-431), and the unspecified
order of an unordered map when the top posting's key is not an entry
(balance.h:99, xact.cc:422-423). Because primacy is not recorded, the
refusal also falls on a transaction before which no price directive,
C directive or earlier exchange has marked any commodity primary,
where C++ takes the orientation that the top posting's key decides:
under = /Brokerage/ with Expenses:Fees 0.01 and Assets:Cash $-15.00,
both C++ binaries accept Assets:Brokerage 10 AAPL {$150.00} against
Assets:Cash $-1500.00 as the first transaction of a journal, and the
oracle refuses it. A record of the commodities that C++ marks primary
would narrow the refusal. The plan's per-rule wording refuses a
transaction as soon as one rule's verdict depends on the orientation;
deciding per orientation over all rules gives the same verdict
wherever that wording gives one, and rejects where every orientation
fails at some rule.

Five parts of the change depart from the plan or follow the tree as
S28 left it. First, the orientations include the one that the swap of
issue #1130 takes: S28's candidates hold only the orientation that the
top posting's key decides, as the oracle models C++ without commodity
primacy, so impliedExchange now also returns, in the new field
PoolResult.swapped, the candidate of the opposite orientation when the
two entries are of different commodities and that orientation
converts and passes. Balancing does not read it. Without it the probe
exchfixed, 10 EUR against -11.00 USD with Assets:Savings 0.1 and
Income:X -1.10 USD, would be accepted, and its copy
exchfixed-eurprimary, which C++ rejects because a P directive makes
EUR primary, would be accepted too. Second, the plan listed template
postings with their own cost as a departure; a first prototype
without them rejected two positive journals, regress/2268 and
regress/coverage-auto-xact-post-cost, which C++ accepts only through
the copied cost, so AutoPost now keeps the cost and its precision and
applyAutos gives them to the generated posting, whose flows are
unchanged. Third, review found that the plan's rule derived a cost
only from an annotation copied from the matched posting, while C++
derives one from any lot price on the generated amount, a fixed
template amount's own included: under = /Food/ with Assets:X
1 AAA {$5} and Assets:Y $-5, both C++ binaries accept Expenses:Food
10 EUR @ $1.10 against an elided posting and reject it with Assets:Y
-1 AAA, where the first version of this rule did the opposite. AutoPost
now keeps the lot price per unit and the {{T}} mark of a template
amount with a commodity, and applyAutos gives them to the generated
posting. A new field PostSpec.lotScale records the factor by which a
commodity equivalence reduced an amount, because C++ reduces a matched
amount at the end of finalize but neither a periodic template nor a
generated posting (xact.cc:987-996), and a {{T}} lot enters the
derived cost at its price per unit of the amount as C++ holds it.
Beside C 1 Kb = 1024 b, C++ accepts a template amount 2 Kb {{$10}}
against Assets:Y $-5, deriving $5; with Assets:X 1 and Assets:Y $-5
it rejects Expenses:Food 2 Kb {{$10}} @ $5 in a transaction, deriving
$0.0048828125, and accepts the same postings in a periodic template.
Fourth, the plan cites xact.cc:1684, 1614-1633 and
amount.cc:1909-1922; at 76b9dc2b2 the lines are 1677-1684, 1611-1627
and 1909-1923. Fifth, the key function of impliedExchange moves to a
shared lotKeyOf, unchanged.

The docstring of verifyExtended lists the known departures, each with
its direction and a journal on which the C++ build, run with
--permissive as the comparator runs it, and ledger 3.4.1 agree: amounts
annotated with a lot price, a date or a note, keyed apart from plain
units by verify() and merged here, whether on a matched posting, a
template amount or a template cost (fail-open and fail-closed); Phase 4
costs of fixated lots, not modelled (fail-closed and fail-open); the
rounding of the per-unit cost of Phase 6 at pool.cc:460-468 (fail-open
and fail-closed); an amount expression in the matched commodity, which
C++ annotates only when its value carries the annotation (fail-open and
fail-closed); the costs that Phase 6 rewrites for an exchange of two
lots (fail-open); the lotTotal field standing in for the mark on the
interned commodity, whose docstring witnesses now hold under this rule
as stated there; the display precision that template amounts teach C++,
which fails open beside Assets:Savings $0.001 and closed beside
Assets:Y -0.33 XYZ, where the template gives XYZ two places and verify()
widens nothing; and the rejection by verify() of a cost in the commodity
of its own amount (xact.cc:1064-1070), not made here (fail-open), as the
same check of Phase 6 on a written posting (xact.cc:679-681) is not. The
rounding of a {{T}} per-unit price could fail either way below the sixth
place beyond the display precision; no journal is known on which it
changes the verdict. The docstring of applyAutos now records that it
matches only real postings, while C++ matches virtual and deferred ones
too (xact.cc:1438-1447): a rule that matches only a virtual or deferred
posting is accepted here although C++ rejects it when the generated
posting unbalances the transaction (fail-open), and when C++ accepts it
the generated postings are missing from the balances here. The comment
beside the rule by which a template posting without an amount mirrors
the matched posting now records that C++ rejects such a posting when its
rule matches ("Automated transaction's posting has no amount",
xact.cc:1529-1531, regress/716), so that the rejection of regress/716
and regress/coverage-xact-auto-noamount here names another cause, and
that a template whose generated postings balance, such as Assets:X with
Assets:Y -1, is still accepted here (fail-open).

The rule was first compiled and run on a scratch copy of the tree,
where the positive corpus stayed byte-identical only once template
costs were kept; the revision after the first review was measured on
the working tree. The final revision, after a second review, changes
comments and the value of lotScale on a generated posting without a
lot price, a value that no rule reads, so the oracle's output is the
same as that of the revision before it on every journal. On the final
tree the locked build script, lake build with warnings as errors,
exits 0 after 1224 jobs with no warning in its log. The oracle's
default output over the 3816 positive journals is byte-identical to
the recorded snapshot, by cmp. The scan of added Lean lines finds no
forbidden construct. Forty-one journals from review and from this
revision, run through the C++ build, ledger 3.4.1 and the oracle,
find the two C++ binaries agreeing on every one; the oracle gives
their verdict on 23, refuses five that they accept, where its verdict
depends on the order of the balance entries or on primacy, and
departs on thirteen in the directions that the docstrings record:
three for the same-commodity cost, two for the display precision,
three for virtual or deferred matches, four for lot keys and one for
a template posting without an amount. The bisimulation log against
the C++ tree, over 4384 .test files, reports PASS 3816, FAIL 0,
SKIP:negative 489, SKIP:no-journal 77 and SKIP:python 2, with both
detector fixtures green.

The remaining measurements were taken on the revision before the
final one. Against the C++ build the 43 N1 probe journals give 26
AGREE, 16 BOTH-REJECT and one ORACLE-REJECTS, exchfixed, with the
refusal reason, by the comparator's verdict lines. Fifteen of the
sixteen journals that C++ rejects carry the unbalanced reason; the
sixteenth, exchfixed-eurprimary, carries the refusal, since its
verdict here depends on the orientation that commodity primacy
decides, which the oracle does not model. The oracle prints "line 4:
unbalanced transaction after automated transaction (residual in 1
commodities)" for auto1. The negative-corpus count reports 126 of 469
journals rejected, three more than before, with no rejection that C++
does not share; the three, regress/716,
regress/coverage-xact-auto-noamount and
regress/coverage-txact-pattern-empty, are rejected by C++ for a
template posting without an amount or an empty pattern, and the
oracle's output over the negative corpus differs from the previous
unit's in those three files only, by diff. Every probe set of the
plan from S01 to S29 matches its expected verdicts (264 verdicts,
exit 0). The register check of added lines is clean, the gate
self-test is green, every C++ test cited by the tree exists at
76b9dc2b2, and the unit's tail check exits 0. A scratch copy of the
C++ harness with the planned classifier of unbalanced negatives, run
against that tree, reports PASS 3816, FAIL 0, NEG:unbalanced 35 and
SKIP:negative 454 with the NEG detector fixtures green, by its log.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0143bpV19FtoLrrR59maPRPk
C++ computes a balance assignment whose target has no commodity, such as
`Assets:A = 0`, by subtracting from the target the account's balance and
the account's earlier postings in the transaction, with lot annotations
stripped (compute_balance_diff, src/textual_xacts.cc:390-479 at
76b9dc2b2). Unless every amount left is zero as amount_t::is_zero reads
it, it converts the difference to one amount with balance_t::to_amount
(textual_xacts.cc:842-858), and when more than one commodity is left,
to_amount throws "Cannot convert a balance with multiple commodities to
an amount" (src/balance.h:473-480) and the journal is rejected. The
oracle instead zeroed the account in every commodity for a target of
zero and, for any other target, posted an amount without a commodity, so
it accepted journals that C++ rejects. The Ellerman implementation plan
records the gap as item N2 (decision D23). Before this change, six of
the fourteen D23 probe journals were accepted by the oracle and rejected
by the C++ build. The witness is the probe barezero-multi: with $10 and
20 EUR in Assets:A, C++ rejects Assets:A = 0 against an elided Assets:B.

In finalizeXact a target without a commodity now forms the same
difference: the target, less the current amount of the account in each
commodity of its balance and of its earlier postings in the transaction,
with every exact zero dropped as balance_t drops it
(src/balance.cc:162-170). The lots of one commodity count as one, since
the oracle keys balances without lots. A difference left in more than
one commodity, some amount of which is not zero, rejects the journal
with "line N: balance assignment without a commodity leaves a balance in
K commodities", N being the posting's line, the line that C++ names in
its "While parsing file" context. Otherwise the posting receives what
C++ gives it: the single amount left, in its own commodity, or a zero
without a commodity when nothing is left or what is left is zero
(textual_xacts.cc:842, 851-858 and 866). The zero test is the one C++
applies (src/amount.cc:1374-1430): at display precision for an amount
with a commodity and exactly for one without, and exactly as well for an
amount that the account holds in a commodity as a single inferred
amount, which C++ tags with the precision of the cost that it balances
(BIGINT_COST_PREC, src/xact.cc:211-226) and then reads at that precision
(amount.cc:1384-1402, issue #1692), unless an earlier posting of the
transaction in the commodity is subtracted from it, since a sum drops
the tag (amount.cc:634-642 and 677-681). The flows are counted as Driver
counts them for a lone inferred balance (flowStats). A target of zero
over a single commodity therefore still zeroes the account
(regress/1587).

A difference that spans commodities must measure each of them as C++
does. C++ decides once for the account whether a real assignment
measures the real-only balance: it does when the account has any posting
that is neither virtual nor from a timelog, in any commodity, on any
date and with or without an amount (textual_xacts.cc:398-409; finalize
adds every posting that is not deferred to its account,
src/xact.cc:1001-1005). The real-only balance then leaves out only
virtual postings, so timelog postings stay in it (textual_xacts.cc:435,
and src/account.cc:868 in file order). The oracle made that choice per
commodity and left timelog postings out of the real-only balance. Per
commodity, the virtual amounts of a real account in a commodity without
a real posting would enter the difference as a second commodity and
reject journals that C++ accepts: with $10 in Assets:A and (Assets:A) 5
EUR earlier in the transaction, C++ accepts Assets:A = 0 against an
elided Assets:B, leaving 5 EUR in Assets:A and $10 in Assets:B (probe
ro-virt-intra; virt-five, virt-intra, virt-percomm and four more have
the same cause). The oracle now makes the choice once for the account,
from datedEvents, whose Bool becomes a kind that tells a timelog posting
from a virtual one (EvKind), and the real-only balance keeps timelog
postings, so that a timelog session and $10 in Assets:A leave two
commodities and are rejected as C++ rejects them (timelog-real). An
elided real posting that a balanced transaction leaves at zero, which
the oracle's balancing drops, is recorded as an event of no amount,
since it still counts in C++ (xact.cc:829-842): after $10 and $-10
against an elided Assets:A, and (Assets:A) $10 and (Assets:A) 20 EUR,
C++ measures the real-only balance for Assets:A = 0 and posts a zero
(elided-zero-bare). The same measure serves a target with a commodity,
as compute_balance_diff serves both in C++. The comments beside the
rules and the timelog reader cite these lines, and the Assignments
bullet of the module docstring states the rule.

Five parts of the change go beyond the plan's wording, and each moves a
case toward C++. First, the rejection applies only when some amount left
is not zero, since C++ tests diff.is_zero() before it converts the
difference (textual_xacts.cc:842); without that condition a scratch
build rejected a journal that holds ($1 / 3) and (1 EUR / 3) in Assets:A
before Assets:A = 0, which both sides accept. Second, the plan said that
the assignment otherwise resolves as before; it now posts what C++
posts, which changes several shapes, among them the following. With 5
and $10 in Assets:A, Assets:A = 5 against an elided Assets:B left $10 in
Assets:A and now leaves it in Assets:B (nullcancel). Over ($1 / 3)
alone, which shows as $0, Assets:A = 0 zeroed the account and now posts
a zero, so that a later ($1 / 3) makes the account show $1
(dz-accum-single1, a single-commodity case, and dz-accum-multi1). After
Assets:A 5 EUR in the same transaction, Assets:A = 0 left 5 EUR in
Assets:A and -5 EUR in Assets:B and now leaves both accounts at zero
(intra-only). The posting of a target of zero is now kept when nothing
is left, with a zero without a commodity, as C++ keeps it
(textual_xacts.cc:866), where the oracle dropped it; an automated
transaction can therefore match it, so that a rule on ^Assets:Empty$
that posts $1 and $-1 now applies to Assets:Empty = 0 on an empty
account (auto-fixed-zero), and it counts as a real posting when a later
real assignment chooses the real-only balance, so that after Assets:A =
0 on an empty account and (Assets:A) $10, Assets:A = $0 leaves the $10
in Assets:A (zero-empty-then-ro, and zero-then-real). A single inferred
amount tagged with its cost precision is tested exactly, as before, so
that over $-0.0004 inferred from a cost of $0.0004, (Assets:A) = 0 still
zeroes the account (costprec-virt). Third, the per-account choice of the
real-only balance also changes targets with a commodity: with $10 in
Assets:A and (Assets:A) 20 EUR before, Assets:A = 0 EUR against an
elided Assets:B moved the virtual 20 EUR to Assets:B and now posts a
zero, as C++ does (cv-target), and a real posting dated later but read
earlier now makes the choice as it does in C++ (ro-future-real). Fourth,
the elided posting left at zero changes a target with a commodity as
well: in the shape of elided-zero-bare with only the dollars, Assets:A =
$5 now posts $5 as C++ does, where it posted $-5 (elided-zero). Fifth,
the reader marked the posting that completes a transaction against the
bucket as inferred whether or not a cost tags it. The exact test above
would read such a posting as tagged and reject a journal that C++ and
the previous oracle accept, in which the bucket completes a lone ($1 /
300) and a later Equity:Bucket = 0 meets it beside (1 EUR / 300)
(bucket-untagged, and bucket-tag). C++ completes the transaction through
a null posting to the bucket (src/xact.cc:368-372), which it fills and
tags as it fills an elided one (xact.cc:829-837 and 234-237, through
set_cost_prec_if_needed at 195-231), so the bucket's posting now takes
the cost rule that the reader already applied to an elided one, and a
completion that a cost tags is still tested exactly (bucket-tagged,
rejected by both). Driver reads the same mark for a lone inferred
balance, so the display of a bucket completion changes with it: after
$1.00 in another account, a lone ($1/1000) left $-0.001 shown in the
bucket, which C++ shows as zero and the oracle now does as well
(bucket-display). A bare assignment over such a completion alone now
posts a zero, as over ($1 / 3) above, where it zeroed the account
(bucket-untagged-single and bucket-tag-single, single-commodity cases).
In each of these cases C++ gives the new result.

The comments record nine departures. Six concern the precision or the
commodities of a difference without a commodity. The display precision
is the one learned from every posting of the transaction, while C++ has
learned it only from the amounts before the assignment, so a later
posting that widens it makes a difference that C++ takes as zero be
rejected here when it is left in several commodities (fail-closed,
dispzero-later) and be posted here when it is left in one (wrong rows,
dispzero-later-single). A periodic template and the postings of an
automated transaction teach C++ the precision of their amounts and the
oracle nothing, the departure recorded at flushPending and zeroTestPrec,
so that after a template with Assets:Z $0.001, or an automated
transaction that matches nothing with (Assets:Z) $0.001, C++ rejects
Assets:A = 0 over ($1 / 3) and (1 EUR / 3), which the oracle accepts
(fail-open, tmpl-prec, auto-prec and auto-prec4). C++ has not yet
reduced the amounts of the transaction through commodity equivalences
(textual_xacts.cc:583-587; they are reduced in finalize,
src/xact.cc:996), while the reader reduces each as it reads it, so that
under C 1 EUR = 100 CENT two units of the equivalence are two
commodities in C++ and one here (fail-open, equiv-intra and
equiv-intra2). The flows that decide a tag are counted on every date and
of every kind, while C++ reads only those of the balance it measures, so
that a tagged amount beside a later posting in its commodity, or beside
a virtual one under the real-only balance, is read at display precision
(fail-open, costprec-later and costprec-virtbeside), and a posting of
exactly zero, which balance_t neither adds nor subtracts
(src/balance.cc:122-123 and 162-163), counts here as well (fail-open,
intra-zero-dollar and tag-then-zero-dollar). The amount that C++ posts
keeps the tag (balance.h:477-478), which the posting that balances the
assignment takes over (xact.cc:234-237), so that C++ shows $-0.0004 in
that posting where it shows as zero here (wrong rows, costprec-single,
as before this change and also with a target of $0, costprec-ctarget).
And the price precision that tags an amount is recovered from the
postings of its own balancing pool, while C++ scans every posting of the
transaction (xact.cc:210), so that a bracketed completion beside a real
posting with a cost is read untagged (wrong rows, bracket-tag-real;
fail-open, bracket-tag-virt); this one predates the change and is stated
where the precision is recovered (pricePrec). The seventh departure, now
stated at EvKind, predates the change: C++ holds a deferred posting back
from its account until the journal is read (xact.cc:1001-1003,
src/textual.cc:413), reads a deferred assignment as a real one in
choosing the real-only balance (textual_xacts.cc:399) and subtracts an
earlier deferred posting of the transaction from that balance
(textual_xacts.cc:467-469), while the oracle reads every deferred
posting as a virtual one (wrong rows, deferred-prior,
deferred-prior-comm, deferred-assign, deferred-intra and
deferred-intra-10; fail-open, deferred-intra-eur). The eighth, stated at
the dated balance, also predates it: C++ compares the date of each
earlier posting itself (post_t::date, src/post.cc:143-174), which a
posting note [DATE] sets, while every event here carries the date of its
transaction (wrong rows, postdate-comm). With the new rejection this
departure turns one shape that was wrong rows into a rejection that C++
does not share (fail-closed, postdate), as the first does for
dispzero-later. The ninth, stated there as well, predates the change
too: an assignment reads its account as written, while C++ reads it
after a payee rule has mapped an Unknown account
(journal_t::register_account, src/journal.cc:179-188), and the oracle
keeps the balances under the mapped name (fail-open, unknown-bare and
unknown-payee).

The rule was first compiled and run on a scratch copy of the tree, where
the probes below gave the same verdicts as on the working tree, the
oracle's default output over the 3816 positive journals was
byte-identical to the recorded snapshot, and its output over the 469
negative journals was byte-identical to the previous unit's, both by
cmp. On the working tree the locked build script, lake build with
warnings as errors, exits 0 after 1224 jobs with no warning in its log.
Against the C++ build the fourteen D23 probes give six BOTH-REJECT, for
bare5, barefive, intra, lotsmixed, barezero-multi and o-barezero, and
eight AGREE, by the comparator's verdict lines; the six were CPP-REJECTS
before. A second probe set of 84 journals, added to the final probe gate
as S30 d23-review with verdicts derived from the C++ build and the
recorded departures alone, gives 46 AGREE, 12 BOTH-REJECT, CPP-REJECTS
for tmpl-prec, auto-prec, auto-prec4, equiv-intra, equiv-intra2,
costprec-later, costprec-virtbeside, intra-zero-dollar,
tag-then-zero-dollar, bracket-tag-virt, deferred-intra-eur, unknown-bare
and unknown-payee, ORACLE-REJECTS for dispzero-later and postdate, and
DIFF for dispzero-later-single, costprec-single, costprec-ctarget,
costprec-later-single, bracket-tag-real, deferred-prior,
deferred-prior-comm, deferred-assign, deferred-intra, deferred-intra-10
and postdate-comm, all as expected. Run with the oracle of the previous
unit, the same check reports 32 of these verdicts changed, besides the
six D23 probes; run with a scratch build that keeps the bucket's
unconditional mark, it reports five, ORACLE-REJECTS for bucket-untagged
and bucket-tag and DIFF for bucket-display, bucket-untagged-single and
bucket-tag-single. The C++ build and the 3.4.1 release give the same
verdicts over both sets. The negative-corpus count reports 126 of 469
journals rejected, as before, with no rejection that C++ does not share;
no test of the C++ corpus exercises the rule, and the plan's unit C11 is
to add one. The oracle's default output over the positive corpus is
byte-identical to the recorded snapshot, and over the negative corpus
byte-identical to the previous unit's, by cmp. The scan of added Lean
lines finds no forbidden construct, the register check of added lines is
clean, the gate self-test is green, every C++ test cited by the tree
exists at 76b9dc2b2, and both probe sets of this unit match their
expected verdicts. The bisimulation log against the C++ tree, over 4384
.test files, reports PASS 3816, FAIL 0, SKIP:negative 489,
SKIP:no-journal 77 and SKIP:python 2, with both detector fixtures green,
and the unit's tail check exits 0.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0143bpV19FtoLrrR59maPRPk
jwiegley and others added 24 commits October 2, 2026 06:04
C++ reads an alias directive in alias_directive
(src/textual_directives.cc:716-728 at 76b9dc2b2). The name ends at the
first `=`, and a line without one is ignored. The target is named by
top_account()->find_account, that is, under the `apply account` stack in
force when the directive is read, and find_account rejects an empty
component of it but drops a trailing colon (src/account.cc:79-131).
account_alias_directive then rejects an alias equal to the full name of
its target (textual_directives.cc:701-714). An account block is
registered by account_directive through register_account under the same
stack (textual_directives.cc:602-611), and register_account returns an
alias expansion as it stands, adding the stack only when no alias fires
(src/journal.cc:169-177). The oracle instead rejected an alias line
without `=`, or with more than one, as malformed, stored the target as
written, put the `apply account` prefixes of a posting in front of an
alias expansion as well, opened an account block under its name as
written, so that its alias, payee and default subdirectives named the
unprefixed account, and accepted an alias equal to its target. The
Ellerman implementation plan records the gap as item N3. Before this
change the nineteen N3 probe journals gave three ORACLE-REJECTS, six
CPP-REJECTS, nine DIFF and one AGREE against the C++ build, and the
oracle accepted the negative tests baseline/dir-alias-fail and
regress/coverage-wave11-account-alias-self.

The alias directive now follows alias_directive. An empty target or an
empty component of it rejects the journal with "line N: alias target 'T'
has an empty sub-account name", and an alias equal to its resolved
target with "line N: illegal alias A=T". The resolution of an account
name is split into expandAliases, the former body, and applyPrefixes,
and resolveAccount applies the prefixes only when no alias fires; the
bucket and A directives, which C++ resolves through register_account
(textual_directives.cc:168-171), follow it. A top-level account block is
opened under the name that resolveAccount gives, a nested block under
the enclosing account, which the nested dispatch passes as the parent
(textual_directives.cc:651-655), unless an alias fires
(textual_directives.cc:611), and the alias subdirective rejects an alias
equal to the block's resolved name. Under `apply account Top`,
`alias Foo=Bar` makes `Foo` denote `Top:Bar`, a later posting to `Foo`
under another `apply account` takes no second prefix, and
`alias Top:Foo=Foo` is illegal; the comments beside the rules cite these
lines and give these shapes inline.

Five parts of the change go beyond the plan's wording, and each keeps or
moves a case toward C++. First, the plan's rule that a line without `=`
is ignored would also have accepted a bare `alias`, which C++ rejects
before alias_directive runs; the oracle still rejects it, now with
"directive 'alias' requires an argument", the wording of
general_directive (textual_directives.cc:1034-1038,
regress/coverage-wave11-directive-noarg, and regress/1036). Second, a
new findAccountName reads a name as find_account does, so that
`alias Foo=Bar:` makes `Foo` denote `Bar` and `alias Foo=Foo:` is
illegal, as in C++. apply_account_directive finds its name the same way
(textual_directives.cc:464-487), so findAccountName now reads it too:
under `apply account Top:` a posting to `Baz` posts to `Top:Baz`, where
the oracle printed `Top::Baz`, and a bare `apply account`, which the
oracle accepted with an empty prefix, is rejected with "directive 'apply
account' requires an argument". Third, general_directive ends the
directive word at the first space or tab (next_element,
textual_directives.cc:1028-1029), as the include splicer already did, so
that `alias<TAB>Foo=Bar` defines an alias, which the oracle skipped as
an unknown directive. Fourth, account_directive_body ends a
subdirective's keyword in the same way and requires an argument for
every keyword but `default` (textual_directives.cc:637-641); a bare
`alias`, `payee`, `note` or `account` in a block, which the oracle
ignored, is now rejected with "account directive 'K' requires an
argument", and `default` with an argument sets the bucket, as
account_default_directive ignores the argument. Fifth, C++ names a
timelog account under the stack without expanding an alias, at the
check-in and at the check-out alike (clock_in_directive and
clock_out_directive, textual_directives.cc:110 and 139). With several
entries open it closes the one whose check-in named the same account
(src/timelog.cc:112-122), it rejects a second check-in to an account
already checked in (timelog.cc:201-207), and it expands the aliases of
the full name and applies the payee rules only when it books the hours
(create_timelog_xact, timelog.cc:76-77), by the check-in's description
or, when that is empty, the check-out's (timelog.cc:133-136). The oracle
resolved the check-in account as a posting's when it read the check-in,
so that with `alias Foo=Bar` and then `apply account Top`, `i ... Foo`
booked its hours to `Top:Bar`, and the new resolveAccount alone would
have booked them to `Bar`, where C++ books them to `Top:Foo`. It matched
a check-out against that resolved and payee-mapped name or against the
name as written, so that it accepted `o ... Bar` with entries for `Foo`
and `Baz` open under `alias Foo=Bar`, which C++ rejects, and rejected
`o ... Unknown` against an entry whose `Unknown` a payee rule had
mapped, which C++ accepts. An entry now keeps the name under the stack
and its description, a check-out is matched on the name under the stack,
the new timelogAccount expands the aliases and applies the payee rules
when an entry is checked out or closed at the end of the journal, and a
second check-in to the same account is rejected with "cannot double
check-in to the same account".

The comments record three departures in the code the change touches,
each of which predates it. C++ drops a trailing colon of any account
name and rejects an empty component, which the oracle does only for an
alias target and an `apply account` name (wrong rows for a posting to
`Foo:`; fail-open for one to `Foo::Bar` or a block `account A::B`).
Under --recursive-aliases C++ rejects a whole name that would expand a
second time (journal.cc:272-274, regress/coverage-error-circular-alias)
and an expansion that ends at the name it began with
(journal.cc:341-345, regress/coverage-journal-alias), where the
expansion stops here (fail-open, `alias A=B` and `alias B=A` with a
posting to `A` or to `A:X`). An automated transaction keeps its template
accounts as written, where C++ registers them under the stack and
through the aliases when it reads the rule and through the aliases again
when it applies it (src/textual_xacts.cc:271 and 571,
src/xact.cc:1655-1656), so that with `alias Foo=Bar` a template posting
to `Foo` generates its amount in `Bar` in C++ and in `Foo` here, and a
rule read under `apply account Top` generates its amounts in `Top:Foo`
and `Top:Cash` in C++ and in `Foo` and `Cash` here (wrong rows).

The first form of the change was compiled and run on a scratch copy of
the tree before it was applied; the revisions after review were made on
the working tree, where every figure below was taken. The locked build
script, lake build with warnings as errors, exits 0 after 1224 jobs with
no warning in its log. The oracle's default output over the 3816
positive journals is byte-identical to the recorded snapshot by cmp
(SNAP IDENTICAL). Its output over the 469 negative journals differs from
the previous unit's in twelve files. Ten are now rejected, each at the
line where its test expects the C++ error: baseline/dir-alias-fail and
regress/coverage-wave11-account-alias-self for an illegal alias;
regress/553, cov6-textdir-apply-acct-noarg,
coverage-textual-apply-account-no-arg and
coverage-wave11-apply-account-noarg for a bare `apply account`;
regress/785, coverage-account-subdir-no-arg and
coverage-wave11-account-noarg for a bare block `alias`; and
regress/coverage-timelog-double-checkin for a double check-in. The
reasons for the bare `alias` in regress/1036 and
coverage-wave11-directive-noarg take the new wording. The
negative-corpus check reports 136 of 469 journals rejected, 126 before,
and no rejection that C++ does not share, exit 0. Against the C++ build
the nineteen N3 probes give six BOTH-REJECT, for alias-emptyrhs, self,
acct-self, alias-self-full, acct-alias-apply-self and
alias-apply-account-self, and thirteen AGREE, by the comparator's
verdict lines, and the probe gate reports PROBES OK for its S31 set of
nineteen verdicts. A further 164 inline shapes from the implementation
and its review, among them nested blocks, bare and tab-separated
subdirectives, the bucket directive, timelog entries under aliases and
`apply account`, and trailing colons, give 114 AGREE and 35 BOTH-REJECT;
the fifteen others show the three recorded departures. The 28 shapes
added after review give the same verdicts against the Ledger 3.4.1
release binary. The register gate reports CLEAN, the forbidden-construct
scan reports none, and the citation check reports CITECHECK OK. The
bisimulation over 4384 C++ test files gives PASS 3816 and FAIL 0, with
489 negative, 77 journal-less and 2 Python files skipped, exit 0.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0143bpV19FtoLrrR59maPRPk
C++ prints an amount, and amount_t::is_zero prints one to test it for
zero, through stream_out_mpq (src/amount.cc:208-245 at 76b9dc2b2). That
function divides the numerator by the denominator in binary at a
precision of their two bit lengths plus 768 bits (extend_by_digits 6,
src/amount.h:121), rounding to nearest with ties to even, and
mpfr_asprintf prints the binary value at the display precision, again
with ties to even. Driver.displayAmount rounded half away from zero
instead, and the zero test of Journal.lean read a remainder of exactly
half a unit as zero. The rules agree except at an exact decimal tie,
where the binary expansion decides in C++: at precision 1, 0.35 prints
0.3 and 0.45 prints 0.5, and at precision 7 a residual of half a unit
prints 0.0000001 and is not zero. The Ellerman implementation plan
records the gap as item N4, and the message of the unit that re-scoped
periodic templates listed the half-way display rounding of Driver.lean
among the departures left to a later unit.

The new Parse.cppRoundAt reproduces stream_out_mpq. Away from an exact
tie it compares exactly, which agrees with the binary value at every
precision up to 231 places, since 10^231 < 2^768; at a tie, or beyond
231 places, it forms the binary value with binRound and rounds that with
ties to even. displayAmount and roundsToZeroAt now use it. Every other
use of roundAt, which rounds half away from zero as in_place_roundto
does (src/amount.cc:1072-1100), is unchanged, and specFor, flowsFor and
journalTerm are untouched.

Two rules ask whether an amount still tagged with the precision of its
cost rounds to zero, and C++ asks that with in_place_roundto rather than
as it prints. The widening of a lone inferred balance asked it of the
displayed value (src/amount.cc:2041-2053), and now calls roundAt itself,
which keeps that rule where it was: without the edit, `A 1 FOO @ $0.005`
against an elided `B`, with `$` shown at two places, would print `B` at
-0.005, where C++ prints no row for it. A balance assignment without a
commodity took a tagged difference as zero only when it was exactly
zero, while C++ does so only when the display precision is 0 or
in_place_roundto gives zero there, and otherwise tests the difference at
display precision as it tests any amount (src/amount.cc:1384-1402).
Review of this change found the shape, and the rule now follows the
split: after `$1.00` in one account and `1 FOO @ $0.005` against an
elided `Assets:A`, `Assets:A  = 0` posts a zero, as C++ does, where it
posted `$0.005` before. The comments beside the rules cite these lines
and give the shapes inline; the C++ regression tests that will witness
them belong to the C++ series and are cited by the follow-up after it
merges. The paragraph of Gen.lean on ties now says that the oracle
reproduces the C++ rule and that the generator still manufactures no
ties, and the zero-test bullet of the Journal docstring names the new
rounding.

This changes output values, not the protocol format. A displayed amount
at an exact decimal tie can change value, and so can the verdict of a
zero test at one. A transaction left exactly half a unit out of balance
at precision 7, 8, 10, 11 or 12, of the precisions 0 to 12 measured, is
now rejected, as C++ rejects it, unless a cost written with decimals in
that commodity brings the residual within the allowance beside costTol,
which still accepts it, a departure recorded before this change. The
README sentence on ties lands with the README paragraph on exact
arithmetic.

The first form of the change was compiled and run on a scratch copy of
the tree before it was applied, where the positive and negative
snapshots were identical to the recorded ones; the balance-assignment
rule was added after review, in the tree itself, and every result below
is from the final tree. The locked build script, lake build with
warnings as errors, exits 0 after 1224 jobs with no warning in its log.
Eight #guard checks of cppRoundAt against the Python reference of the
plan pass, sem-check exit 0, where before the change they failed on the
unknown name; a further 750 cases, exact ties at precisions 0 to 11,
random rationals, near ties and precisions from 230 to 300, give the
reference's value for every case by diff. Against the C++ build and the
Ledger 3.4.1 release binary alike, the six tie probes give AGREE for
ties.dat, tie-disp, tie-acc and tie-p6 and BOTH-REJECT for tie-acc2 and
tie-p7, by the comparator's verdict lines, where before ties.dat and
tie-disp gave DIFF and tie-p7 CPP-REJECTS; in ties.dat the old rule
printed 65 of the 120 ties differently from C++. Seventy-one further
shapes (forty ties at each precision from 0 to 8, half-unit and larger
residuals at precisions 0 to 12, three lone inferred balances, and the
re-scope message's `-26,0 EUR` shape) gave 8 AGREE, 45 BOTH-REJECT, 5
CPP-REJECTS and 13 DIFF before the change and give 21 AGREE and 50
BOTH-REJECT after it. Ten balance assignments over a tagged difference
give AGREE against both binaries: four at a tie that rounds down in
binary, at precision 2 with and without a commodity directive, with the
cost written as `$0.0050`, and at precision 6, gave DIFF before the rule
was changed, and six others, at `$0.004` and `$0.006`, at precision 7,
where the tie rounds up, at precision 0, where C++ tests exactly, and
with a second commodity at and below a tie, give AGREE as well. The
probe sets of units S20 to S31 give the same verdicts as the tree before
the change, by cmp, on every probe file present. The oracle's default
output over the 3816 positive journals is byte-identical to the recorded
snapshot by cmp (SNAP IDENTICAL), and its output over the 469 negative
journals is byte-identical to the previous unit's by cmp. The
negative-corpus check reports 136 of 469 journals rejected, as before,
and no rejection that C++ does not share, exit 0. The probe gate reports
PROBES OK for the S32 set of six verdicts. The driver pin, the
cited-name check of Gen.lean and the citation check pass, the register
gate reports CLEAN, and the forbidden-construct scan reports none. The
bisimulation over 4384 C++ test files gives PASS 3816 and FAIL 0, with
489 negative, 77 journal-less and 2 Python files skipped, exit 0.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0143bpV19FtoLrrR59maPRPk
AGENTS.md forbids disabling a lint to make a proof pass, yet
Ledger/Oracle.lean turned off four linters (unusedSimpArgs,
unnecessarySeqFocus, unusedTactic and unreachableTactic) so that the
five Demo goldens, anonymous examples closed by simp with nine private
inequality lemmas and norm_num, would build. The analysis report
behind this series (item G2) found that removing the four options
alone fails the build with unused-simp-argument errors. The goldens
are now the named theorems golden_checking, golden_salary,
golden_rent, golden_food and golden_trial_balance, each proved by
decide +kernel, and the nine lemmas and the four options are deleted.
The kernel variant is required: plain decide, tried on two goldens,
gets stuck in the elaborator's reduction of rational addition. It is
not native_decide, and it puts no compiler between the definitions
and the check.

The oracle also had no golden journal in more than one commodity. The
new namespace Ledger.Ellerman transcribes the worked vector ledger of
Ellerman, Accounting Education 23:5 (2014), pp. 496-498: the opening
balance sheet and transactions 1, 2a, 2b and 3, with postings taken
from the single-sided table of p. 498 in the commodities $, WIDGET
and HALF (decision D17). Flows and endpoints are computed as flowsFor
and specFor compute them, and the journal is the right-nested tensor
that journalTerm builds. Eleven theorems, all by decide +kernel,
state the seven nonzero ending balances (10200, 35, 20, -9200, -1000,
-35 and -20), the zero goods of Liabilities, the trial balance in
each commodity, the ending values 14500, -9200 and -5300 at the
prices (1, 100, 40) of p. 498, and the value of each entry, which
reproduces the scalar example of p. 489 (15000, 1200, 1500 and 800)
with the sale 2b worth zero. The Entry docstring quotes p. 497, "for
simplicity, directly with equity rather than using 'income statement'
or property-flow accounts", and states that the encoding makes no
claim about how a sale ought to be booked. The section header states
the scope that the report attaches to these goldens (critic issue
R11): the journal is transcribed by hand, because Parse and Journal
contain partial definitions the kernel cannot evaluate, and the
valuation goldens use a hand-written rational price function, so they
check the arithmetic of pp. 489 and 498, not Ledger/Valuation.lean
and not market valuation. The Demo paragraph of the module docstring
is rewritten to match. It notes that every golden but entry_values
evaluates netFlow: entry_values values each entry's own flows, so it
checks the transcription and the price arithmetic rather than the
evaluator. The docstring of flows notes that no entry of p. 498 posts
twice to one account and commodity, so each of its sums has at most
one term.

Two parts of the report's proposal are not taken. The goldens are not
also stated over the driver's own construction in a module above
Driver (decision D33); the hand transcription, with endpoints shaped
as specFor shapes them, stays in Oracle, which cannot import Driver.
No second planted defect on a non-$ column is added to the C++
harness (decision D32): the comparator already compares the rows of
every commodity across the corpus and fails on any perturbed row, and
the mandatory detector on test/semantic/demo.dat already shows on
every run that it can fail, so a second detector would add a run
without adding evidence. The C++ fixture that replays the same
journal arrives with a later C++ change; it is not cited here, and
the follow-up after that change adds the citation.

Verification. The build (lake build, 1224 jobs) exits 0 with no
warning in its log. A grep of Oracle.lean for set_option, the private
inequality lemmas and #print axioms finds nothing. #print axioms over
the sixteen goldens lists propext, Classical.choice and Quot.sound for
each (16 records within the budget), and the environment-wide audit
finds 2859 constants in 30 Ledger modules and none outside the
budget. As a negative control, the Assets $ golden restated at 10201
fails with "Tactic `decide` failed"; so do the Demo checking balance
at 2851, a Liabilities WIDGET balance of 1, an Equity value of -5301,
a value of 1 for entry 2b and a trial balance of 1 in each commodity,
while the negation of each of these six is proved by decide +kernel,
so the kernel decides them false rather than giving up. Outside the
repository, the oracle driver run on the same journal written as text
prints the seven rows of the goldens, and ledger 3.4.1 prints the
same balances. No executable definition changes: the oracle's default
output is byte for byte that of the previous commit on the 3816
positive journals of the snapshot and on the 469 negative journals,
and the bisimulation against the C++ suite reports PASS 3816, FAIL 0
and SKIP:negative 489, with both detector fixtures green.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0143bpV19FtoLrrR59maPRPk
Item 5 of proposal I5 in the Ellerman analysis asks for two theorems
about netFlow, the evaluator that the oracle runs: the per-commodity
trial balance, which until now was stated only for the free model, as
free_trial_balance in Ledger/Pacioli.lean, and a statement of what the
per-commodity totals of a term are when its generators do not balance.
The analysis concluded that those totals vanish when every transaction
balances in the commodity and in general equal the sum of the
per-transaction residuals, which can cancel, and it rejected an "exactly
when" wording for that reason. It checked both statements in a scratch
file against f5d891e; they are taken from there.

Both theorems are proofs about the executable layer and sit in two of
its modules; neither module's imports change. The theorem
GenSpec.netFlow_trial_balance in Ledger/Oracle.lean states, by induction
on the term, that if every generator's flows sum to zero over the
commodity-c accounts of a finite set s, so do the flows of every term
over the same accounts. The theorem GenSpec.netFlow_commodity_sum in
Ledger/Register.lean states, with no hypothesis on the generators, that
the same sum of a term's net flow is the sum over its generator
occurrences, in presentation order, of each occurrence's signed flows
over those accounts; it is proved by rewriting with occurrences_foot.
Register imports Oracle and not the reverse, so the first theorem is
proved directly rather than derived from the second.

The docstring of netFlow_trial_balance carries the class sentence of
free_trial_balance word for word. After the priced exchange, it names
the residual accepted as an implicit exchange or within the balancing
tolerance, the paren-virtual posting written in the transaction or
appended by an automated transaction, and the timeclock entry. It does
not claim that these classes are exhaustive, and it names neither
automated postings nor bucket completions as a class, which earlier
units removed. The drafted docstring of the theorem called each
generator a zero-row in c under the hypothesis; since s is any finite
set, the docstring now says so only for an s that holds every account a
generator touches.

The docstring of netFlow_commodity_sum names no class. For an s that
holds every account the term's generators touch, it reads the theorem as
a residual formula: the residual of a term in c is the signed sum of the
residuals of its occurrences. It names trialBalance_flowAux in
Ledger/Pacioli.lean as the same formula for the free model, and it
states that residuals of distinct occurrences can cancel, as those of a
purchase and a sale of the same quantity at one price do, so that a zero
sum does not show that each generator balances. A purchase and a sale of
different quantities at one price do not cancel; a review probe, rerun
with the locked check script (exit 0), finds residuals of 6 AAPL and -30
dollars for trades of 10 and 4 AAPL at 5 dollars. An example after the
theorem checks the cancellation in the kernel with decide +kernel: a
purchase of 10 AAPL for 50 dollars and a sale of the same quantity for
the same amount each leave a nonzero residual in AAPL, and the residuals
of their two occurrences in a tensor sum to zero. Its three definitions
are private to the module.

Verification. The locked build script, lake build with warnings as
errors, exits 0 after 1224 jobs with no warning in its log. #print
axioms over the two theorems lists propext, Classical.choice and
Quot.sound for each (2 records within the budget), and the
environment-wide audit finds 2865 constants in 30 Ledger modules and
none outside the budget; the example, restated as a theorem in a scratch
file, lists the same three axioms. Every name cited in the docstrings of
the two modules resolves. The class-list check finds the identical class
sentence in the docstrings of free_trial_balance and
netFlow_trial_balance, and neither names automated postings or bucket
completions. The scan of added Lean lines finds no forbidden construct,
the register check of added lines is clean, the gate self-test is green,
and every C++ test cited by the tree exists at 76b9dc2b2. No definition
the oracle runs changes: the oracle's default output over the 3816
positive journals is byte-identical to the recorded snapshot, and over
the 469 negative journals it is byte-identical by cmp to the output
recorded for the previous unit. The bisimulation log against the C++
tree at f396a2e69 reports PASS 3816, FAIL 0, SKIP:negative 489,
SKIP:no-journal 77 and SKIP:python 2, with both detector fixtures green;
the unit's tail check exits 0.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0143bpV19FtoLrrR59maPRPk
Item I9 of the Ellerman analysis found that the semantics could not
represent gross debit and credit totals, and that its register was
coarser than the C++ one: registerRows in Ledger/Register.lean gives one
row per generator occurrence, while C++ register prints one row per
posting, and no definition summed the debits and the credits of an
account separately. The analysis asked for posting-level data in
Register, a footing law with no hypothesis, a swap of the two columns
under reversal, posting rows with a running total per commodity, two
granularities kept apart, a gross trial balance that takes per-commodity
balance as its hypothesis, and a record that a valuation functor on a
lawful 𝕋 with nonnegative values is zero. This commit is the executable
half of that item. The module that reads gross totals as Ellerman's
T-accounts comes separately, and neither Driver nor the output protocol
changes here.

GenSpec gains postingFlows, termPostings, postingValues, debitTotal,
creditTotal and grossTotals, which take for each generator the list of
postings it records; postingFlows sums that list per account by the same
filter and fold as flowsFor in Ledger/Driver.lean. The theorems are
termPostings_foot and gross_foot (the amounts posted to an account, and
its debits less its credits, are its net flow, for every term),
debitTotal_nonneg and creditTotal_nonneg, grossTotals_inv (a reversal
swaps the two columns), gross_trial_balance, and txn_gross_foot for
txnDebitTotal and txnCreditTotal, which net each transaction at the
account first, as Ellerman's unreduced ledger adds the T-terms
[x⁺ // x⁻] of whole transactions (pp. 489 and 494).
ValuationFunctor.nonneg_valuation_trivial derives from V_inv that a
valuation functor on a lawful 𝕋 with nonnegative values is zero, which
is why gross totals are folds over terms rather than valuations of the
quotient. LabeledGenSpec gains runRows, labeledPostings and postingRows,
with postingRows_postings, postingRows_foot and postingRows_total, the
last stating the running total as a prefix sum.

The witnesses are C++ files at 76b9dc2b2, and the module states that
the C++ line numbers it cites are those of that revision. The dc option
of src/report.h:751-754 splits the amount of each posting into its
positive and negative parts, and in test/baseline/opt-dc.test, bal --dc
shows Expenses:Food at $40 of debits and $10 of credits, where netting
each transaction first gives $35 and $5. An example checks the three
account rows of that report and the netted pair. Gross totals are stated
per account, as bal --dc --flat prints them, since without --flat the
row of a parent account also counts the postings to its subaccounts.
The running total follows calc_posts::operator() in src/filters.cc
(lines 427, 434 and 441), and a second example reproduces, row by row
in EUR, GBP and AAA, the total column of the reg report of
test/baseline/cmd-register.test. A third example shows that gross
totals are not Rel-invariant: the KFC entry followed by its reversal is
Rel-equal to the identity, yet it debits and credits Expenses:Food $20
each. Every numeric claim of the three is checked by decide +kernel, and
the Rel equality is Rel.inv_comp. The postings that the first two encode
are those that Journal.run emits for the two journals, printed by a
scratch script.

The docstrings also record where C++ output departs from rows and totals
computed over the engine's postings, citing the C++ source of each case.
Every case below was run on the release binary ledger 3.4.1 and on a
build of the C++ tree at f396a2e69, which print the same output, and the
engine's postings for the same journals come from the scratch script.
Under --average without --collapse or --depth (src/chain.cc:334-335),
calc_posts starts every posting flagged ITEM_GENERATED from the total
that the last such posting to its account reached, or from zero
(src/filters.cc:415-425 and 444-445): after postings totalling $50, the
running total of the automated posting (Budget:Food) $-20, the value of
the expression total, which --format '%(account)|%(amount)|%(total)\n'
prints, is $30 under reg and $-20 under reg --average. Under --average,
with or without those options, the total column of the default format
shows that running total divided by the posting count of calc_posts
(src/report.h:583-586): $-5 for that posting, and $7 for the KFC row of
Expenses:Food in test/baseline/opt-dc.test, whose running total is $20.
Within a transaction the engine lists real, then bracket-virtual, then
paren-virtual postings, each pool's written postings before the
completion of its elided posting, and the automated postings after all
of them (applyAutos in Ledger/Journal.lean), so that it lists last the
paren-virtual posting of Virtual move in test/regress/3211.test and the
elided Assets:Cash of KFC - Rebate in test/baseline/opt-dc.test, both of
which C++ prints first. C++ gives an elided posting one amount in its
place and appends a posting for each further amount that completes it
(src/xact.cc:233-246), and it hides a posting whose amount displays as
zero unless --empty is given (src/filters.cc:843-848 and 902), while the
engine keeps a written zero and emits nothing for a zero completion. On
a journal that pays $10.00 into Assets:Cash and then makes three
purchases of 1 AAA @ $0.333 against it, elided, reg adds to the second
purchase a row of $-0.01 for <Adjustment>
(display_filter_posts::output_rounding, src/filters.cc:859-887), which
no posting of the engine matches. Order does not affect gross totals,
but the split of a completion by lot annotation does: after a purchase
of 10 AAPL {$50}, a transaction that posts -5 AAPL {$50} and
5 AAPL {$60} to Assets:Broker against an elided Equity:Adj gives
Equity:Adj 5 AAPL in each column of bal --dc, while the engine emits no
posting to it, and with 8 AAPL {$60} in place of 5 AAPL {$60}, reg
prints two rows for Equity:Adj, 5 AAPL and -8 AAPL, where the engine
emits one posting of -3 AAPL. On a journal that posts $20 and $-10 to
Assets:Cash and $30 to Assets:Bank, bal --dc prints $50 and $10 for
Assets, which receives no posting, while bal --dc --flat prints rows for
the two subaccounts and none for Assets. Both binaries confirm the scope
note on zero suppression: an account whose postings net to zero is
hidden by bal and shown by bal --dc.

The gross trial balance is stated in its proved direction only: when
every generator's flows sum to zero over the commodity-c accounts of a
finite set, the debit and credit totals of every term over the same
accounts are equal. No docstring says "only when". For a single term the
converse fails, since its debits less its credits are the summed
residuals of its occurrences, and residuals can cancel; an example
quantified over every term proves that identity from gross_foot and
netFlow_commodity_sum, and the docstring of gross_trial_balance points
to it and to the cancellation example after netFlow_commodity_sum. The
definition is named grossTotals rather than after the C++ option,
because dc already names the decimal-comma declaration of the oracle's
output protocol. The module docstring states that register rows are not
bisimulated, since the oracle prints no register and no C++ output is
compared with them mechanically, the cmd-register example being a
transcription by hand, and that valuation under -B, -V and -X, and the
choice of the accounts that bal --dc displays, among them an account
whose balance is zero, which bal hides, lie outside these definitions.

Verification. The locked build script, lake build with warnings as
errors, exits 0 after 1224 jobs with no warning in its log.
The command #print axioms lists propext, Classical.choice and Quot.sound
for each of the eleven new theorems (11 records within the budget), and
the environment-wide audit finds 2974 constants in 30 Ledger modules and
none outside the budget; its self-test, with a planted native_decide
theorem, fails as it should. The four new examples, restated as theorems
in a scratch file, list the same three axioms. Every name that this
commit cites in the module's docstrings resolves to the intended
constant, postingRows by its qualified name, except finalizeXact and
applyAutos, private definitions in Ledger/Journal.lean, which the name
checker lists as private. As negative controls, scratch copies of the
module stating Expenses:Food at (40, 11), the netted pair at (35, 6),
the reversed entry at (20, 0) and the last GBP total at -21 each fail
with "Tactic `decide` failed", while the negation of each of the four is
proved by decide +kernel, so the kernel decides them false rather than
giving up. The scan of added Lean lines finds no forbidden construct,
the register check of added lines is clean, the gate self-test is green,
and every C++ test cited by the tree exists at 76b9dc2b2. An import scan
shows that only Ledger.lean imports Register, and that the import
closure of Ledger/Driver.lean, which is the oracle's run path, does not
contain it. Separately, the oracle's default output over the 3816
positive journals of the snapshot is byte-identical to the recorded
snapshot, and over the 469 negative journals it is byte-identical by cmp
to the output recorded at the previous commit. The bisimulation log
against the C++ tree at f396a2e69 reports PASS 3816, FAIL 0,
SKIP:negative 489, SKIP:no-journal 77 and SKIP:python 2, with both
detector fixtures green; the unit's tail check exits 0.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0143bpV19FtoLrrR59maPRPk
Item I9 of the Ellerman analysis asked for gross debit and credit totals
at two granularities, and it deferred the part that depends on
Ellerman's T-accounts until item I10 had built them; its basis in the
article includes the unreduced ending ledger of p. 489, the total
transaction zero-term of p. 490 and the implicit T-term of p. 491. The
preceding commit is the executable half of the item:
Ledger/Register.lean computes debitTotal, creditTotal and grossTotals
posting by posting, and txnDebitTotal and txnCreditTotal with each
generator occurrence netted at the account first, all as rational
numbers. Nothing connected those numbers to the T-accounts, the equality
by cross-sums and the Pacioli group of Ledger/TAccount.lean, although
Ellerman's ledger on p. 489 is an unreduced sum of T-terms whose two
sides are such totals: an opening equation zero-account and three
transaction zero-terms add up to [16500 // 2000] for Assets, "(in
reduced form) [14500 // 0]". This commit is the proof half of item I9, a
join module; neither the oracle nor its output protocol changes.

Ledger/GrossLedger.lean imports Ledger.Register and Ledger.TAccount.
postingFlowsF restates postingFlows as a vector of PacioliGroup, and
postingFlowsF_apply proves that the two agree account by account.
grossLedger adds the T-term [x⁺ // x⁻] of every posting of a term, and
txnGrossLedger adds that of the net flow of every generator occurrence,
with its two sides exchanged when the occurrence is reversed; neither
sum is reduced. grossLedger_sides proves that the two sides of
grossLedger at an account are debitTotal and creditTotal, and
txnGrossLedger_sides that those of txnGrossLedger are txnDebitTotal and
txnCreditTotal, so that both granularities of Register are read as
T-accounts. net_grossLedger and net_txnGrossLedger prove that the net
balance of either, the image of its class under the debit isomorphism,
is the flow denotation flowAux of the term when the flows of each
generator are the sums of its postings. grossLedger_crossEq follows:
the two granularities give pairs that can differ but always have equal
cross-sums, which is Ellerman's equality of T-accounts (Table 1,
p. 485). For Expenses:Food in test/baseline/opt-dc.test, whose gross
totals the preceding commit checks at $40 and $10 and whose totals at
transaction granularity it checks at $35 and $5, the pairs are
[40 // 10] and [35 // 5]. toP_grossLedger proves that the class of
grossLedger in the Pacioli group PacioliP is the preimage under
debitIso of the denotation flow of the term's class in FreeHom. That is
the reading of p. 491, where [16500 // 2000] = [14500 // 0] "holds by
the equality of cross-sums", and it makes the class invariant under Rel
although gross totals are not.

The definitions and the statements of the five theorems and the apply
lemma are those of the implementation plan's design. The theorem
txnGrossLedger_sides, which the design did not include, is added beside
grossLedger_sides so that the examples can cite it at transaction
granularity. The design's proofs were written against an earlier draft
of Register, so three of them are adapted to the Register of the
preceding commit, whose postingFlows is a fold and whose debitTotal and
creditTotal sum positive and negative parts. The private lemmas
coe_postingFlowsF and flowAux_postingFlowsF are added for them, and
postingFlowsF_apply rewrites the fold with the library lemmas
List.foldl_map and List.sum_eq_foldl rather than with a third copy of
the fold lemma that Register.lean and DriverTwoSides.lean each state.

Three examples accompany the theorems. The first proves that Rel-equal
terms give gross ledgers of one class, from toP_grossLedger and
Quotient.sound. The second transcribes the four transactions of
test/baseline/opt-dc.test as Register.lean does for its own example,
whose definitions are private to that module, and checks that at
Expenses:Food the gross ledger is [40 // 10] and the gross ledger of
whole transactions is [35 // 5], so that the two gross ledgers are
distinct T-accounts although grossLedger_crossEq makes their cross-sums
equal. The third transcribes Ellerman's table, each row posted as
signed amounts, debit-positive: at both granularities the opening
equation zero-account and the three transactions give the ending ledger
[16500 // 2000], [800 // 10000] and [1200 // 6500] for Assets,
Liabilities and Equity (p. 489), whose reduced forms under
TAccount.reduced are [14500 // 0], [0 // 9200] and [0 // 5300], and the
three transactions alone give the total transaction zero-term
[1500 // 2000], [800 // 0] and [1200 // 1500] (p. 490). The two
granularities agree on this table because each of its rows posts to
each account at most once, so the table checks both definitions but
cannot tell them apart; the second example does. The numbers of both
examples are checked by decide +kernel after rewriting with
grossLedger_sides and txnGrossLedger_sides and, for the reduced forms,
with net_grossLedger, net_txnGrossLedger and
TAccount.reduce_net_eq_reduced. The page citations were checked against
the text of the article: Table 1 on p. 485, the ledger on p. 489, the
total transaction zero-term on p. 490, the update of the assets account
on p. 491, and x ↦ [x⁺ // x⁻] on p. 494.

The module is of the kind that AGENTS.md describes and the
implementation plan calls a join module: it imports an executable module
together with a specification module only to state proofs, and no module
on the oracle's run path, the import closure of Driver, imports it; at
this commit no module imports it at all. It holds only proofs, the
definitions that they need and three examples. The lakefile's glob over
Ledger builds it, and its entries in the Ledger.lean index, the README
and AGENTS.md are left to the plan's final inventory unit, as they were
for TAccount. The proofs sit in a module of their own rather than in
Register.lean so that importing Register does not bring in TAccount: a
file importing Ledger.Register loads 3199 modules, a file importing
Ledger.Register and Ledger.TAccount loads 3232, and none of the four
Mathlib modules that TAccount imports lies in the first closure. The
last paragraph of the TAccount.lean module docstring gains the sentence
that the plan drafted for this commit, "The join module `GrossLedger`
imports it to state gross totals as T-accounts, for proofs only", which
the commit that built TAccount withheld until the module existed.
Modules of this kind rest on the amendment to AGENTS.md that the
DriverTwoSides commit submitted for the user's approval. If the user
declines the category, this commit is reverted as a whole: no module
imports GrossLedger, and no file but these two names it.

Verification. The locked build script, lake build with warnings as
errors, exits 0 after 1225 jobs, one more than at the preceding commit,
with no warning in its log. #print axioms lists propext,
Classical.choice and Quot.sound for each of the six declarations that
the plan names, postingFlowsF_apply and the five theorems, and for
txnGrossLedger_sides (7 records within the budget). The environment-wide
audit finds 3055 constants in 31 Ledger modules and none outside the
budget, against 2974 in 30 at the preceding commit; the 81 new ones are
the ten public declarations of the module with the equation lemmas of
its three definitions, its three private lemmas, the fifteen private
definitions of its examples with the matchers of two of them, and the
two private types of the examples' rows, each with its four
constructors and the 19 auxiliary constants that Lean generates for it.
The audit's self-test, with a planted native_decide theorem, fails as
it should. The three examples, restated as theorems in a scratch copy
of the module, list the same three axioms. As negative controls,
scratch copies that each state one wrong value fail with "Tactic
`decide` failed": the Assets row of the ending ledger as
[16500 // 2001], its reduced form as [14501 // 0], or the Equity row of
the total transaction zero-term as [1200 // 1501], each at either
granularity, and the Expenses:Food pairs of opt-dc.test as [40 // 11]
or [35 // 6]. The negation of each of these eight is proved by decide
+kernel, so the kernel decides them false rather than giving up. A
copy whose last opt-dc.test transaction posts its net $15 to
Expenses:Food in one posting fails on the inequality, and in that copy
the two gross ledgers are proved to agree at Expenses:Food, so the
inequality rests on the two postings to that account. Every name that
the docstrings of the two files cite resolves to the intended constant,
except the module name GrossLedger and accountDebitIso, which the name
checker resolves through its allowlist, the second by its qualified
name. The import-direction scan is clean, nothing imports GrossLedger,
only GrossLedger imports TAccount, and the import closure of
Ledger.Driver holds 3200 modules, Ledger.GrossLedger and Ledger.TAccount
not among them. The scan of added Lean lines finds no forbidden
construct, the register check of added lines is clean, the gate
self-test is green, and every C++ test cited by the tree exists at
76b9dc2b2. The oracle's default output over the 3816 positive journals
of the snapshot is byte-identical to the recorded snapshot, and over the
469 negative journals it is byte-identical by cmp to the output recorded
at the preceding commit. The bisimulation log against the C++ tree at
f396a2e69 reports PASS 3816, FAIL 0, SKIP:negative 489, SKIP:no-journal
77 and SKIP:python 2, with both detector fixtures green; the unit's tail
check exits 0.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0143bpV19FtoLrrR59maPRPk
The automated transaction of test/regress/842.test is
`= tag("Category") == "food"` with the template `(Budget:Food)  -1`.
Both C++ binaries generate `$-50` and `$50` in Budget:Food for the two
postings of the transaction noted `; Category: food`, and nothing for
the transactions noted `dining` and `fuel`. No arm of the oracle's
predicate compiler took a term that begins with `tag(`, so the term
fell to the account-pattern fallback, a regular expression that names
no account, and the rule never fired. The file passed the
bisimulation all the same, because the two generated postings cancel
in the balance of Budget:Food, which a comparison of net balances
cannot see. C++ lexes `tag`, `meta` and `data` alike as a metadata
test and desugars `tag(K) == V` into `tag(/K/) == "V"`
(src/query.cc:262-276, 412-460, 628-643): K is a name pattern that
ignores case (src/mask.cc:365-367) and V is compared as a string, and
post_t::get_tag reads a posting's tags before its transaction's
(src/post.cc:96-110). Line numbers are those of 76b9dc2b2; the files
cited are unchanged at f396a2e69.

The reading is narrow by measurement. Once the test can hold, every
construct that sets a tag, and every reader of the generated
postings, becomes reachable from it, and C++ reads each of them in a
way of its own. Wider readings of the same test, each built on a
scratch stage, generated postings that neither the C++ build nor the
3.4.1 release generates, among them these three:

- `apply tag Cat: dining` around a transaction noted
  `; Category: food`, under `= tag("Cat") == "food"`: C++ reads the
  applied name `Cat` first, and the test fails;
- `; à Category: food` under a posting, under
  `= tag("Category") == "food"`: C++ takes the two-byte word `à` as
  the first token of the note and reads no tag;
- `expr ACategory = "dining"` under a posting noted
  `; Category: food`: the expression tags the posting in C++, whose
  test then reads `ACategory`, a name that sorts first and contains
  `Category`, and fails.

The oracle therefore reads the test in the one shape that the corpus
holds, in a journal whose lines take forms whose reading was measured
to coincide with that of C++ and that holds none of the combinations
of those forms that were measured to diverge, and it refuses every
other journal that holds such a test, fail-closed, as an out-of-scope
`tag()` predicate. The bounds name the forms that are read rather
than the constructs that are not, so that a construct that no form
admits is refused.

`tagEqForm` accepts the whole predicate only as `tag("K") == "V"`,
with one space on each side of `==`, double quotes, and K and V runs
of ASCII letters, digits, `_` and `-`. `tagEqScan` checks the journal
before a line is read. It refuses any line outside a `comment` or
`test` block that begins with `=`, other than the rule, and that calls
`tag(`, `meta(` or `data(` where no ASCII letter, digit or `_`
precedes the name, whether the rule is present or not. The test is on
the whole line as C++ reads it, past any carriage return, so that no
quoting, `expr`, negation, parenthesis, conjunction or carriage return
escapes it, while `has_tag(`, the spellings in which no parenthesis
directly follows the name, and `%("K") == "V"`, in which no name
precedes the parenthesis, keep their reading. When the rule is
present, every line that C++ reads must be blank, a comment, the rule
with template lines that are virtual postings with an integer
multiplier, or part of a transaction after the rule: a header of a
date that C++ accepts and a payee of plain words; a posting of an
account and, optionally, an amount in `$` or in an uppercase
commodity, with no cost, lot, balance assertion or assignment,
expression or note; or a note line of plain words or of
`; Name: value`, the name other than `Value`. No line may hold `uuid:`
or a carriage return before its end, and no file may be included. A
directive of any kind, an included file, a transaction before the
rule, another automated transaction, an `assert`, `check` or `expr`
line, a note on the line of a header or a posting, a typed or listed
tag and a date in a note therefore refuse the journal. For each kind
of refused line the docstring gives at least one journal shape that
C++ was measured to read otherwise, with the C++ location where one
applies; any other spelling outside the admitted forms, such as a date
written with `-` or a thousands separator, is refused because the
bounds admit only the forms whose reading was measured.

An included file is refused because the scan reads the text that
`splice` in Driver.lean makes, in which a block can continue into the
first line of an included file or past its last, while C++ reads each
file with a reader of its own and rejects an indented line that
continues no block of its file: after the template line
`(Budget:Food)  -1`, `include t.inc`, where t.inc holds
`    (Budget:Other)  1`, makes the engine read a rule with two template
lines, while both C++ binaries reject the journal. The text
does not show where a spliced file begins, so `splice` now also
returns whether it spliced a file, `readSpliced` returns that with
the text, and `balancesOf` passes it to Journal `run` as `spliced`,
which only the scan reads; for a journal without the rule the output
is unchanged. A transaction before the rule is refused because the
rule does not test it and the guard below does not check it: there,
`Assets:Cash` elided beside `Expenses:A  $5`, `Expenses:B  $-5` and
`Expenses:C  0 EUR` makes both binaries reject the journal, which the
engine reads.

`tagEqGuard` then checks each transaction. It refuses one in which a
name other than K that contains K, ignoring case, tags a posting or
the transaction, and one with an elided posting beside a commodity
whose written amounts net to zero, whether the test holds or not.
C++ completes an elided posting from a running sum of the written
amounts, in which a commodity that nets to zero survives as a zero
amount when it is the only one and as a zero entry when it nets to
zero after a second commodity has entered; it makes a posting of
each, which the rule then tests, and it rejects the transaction when
the sum is left with no entry (src/xact.cc:180-248, 829-844,
1012-1015, 1438-1447; src/value.cc:600-607; src/balance.cc:118-133;
src/balance.h:128-134). The engine completes an elided posting with
the commodities that do not net to zero alone. Beside
`Expenses:C  3 EUR`, `Expenses:A  $5` and `Expenses:B  $-5`, both
binaries complete an elided `Assets:Cash` with `$0` and `-3 EUR`, and
under `; Category: food` the `$0` posting generates a `$0` posting in
Budget:Food, which the engine, completing with `-3 EUR` alone, does
not generate; beside `Expenses:A  $5`, `Expenses:B  $-5` and
`Expenses:C  0 EUR`, both binaries reject the transaction, which the
engine reads.

Within these bounds, `tagEqHolds` reads the value of K on the posting
or, failing that, on its transaction, the last that a note line sets,
and compares it with V, and the rule generates through the existing
machinery. Apart from reading the rule and refusing as above, no
existing function changes its behavior: the readers of `%TAG`,
`!%TAG`, `has_tag` and `comment =~`, the header note, the notes of
rules, their appending, the order of rules and the completion of
elided postings are as before; `splice` splices the same text and only
returns the flag beside it; and a journal in which no line that begins
with `=`, outside a `comment` or `test` block, calls `tag(`, `meta(`
or `data(` is read exactly as before.

Verification, on the final tree. The locked build script, lake build
with warnings as errors, exits 0 after 1225 jobs with no warning in
its log. A scratch program that prints Journal.run's per-posting flows
gives, for the regress/842 journal and for test/regress/842.test
itself, Budget:Food `$-50` and `$50` in the Grocery Store transaction
and none in the other two, where HEAD gives none. cmp_rows.py gives
AGREE for tagpred.dat, the plan's probe with the tag under one
posting, with the row `$|-50|Budget:Food` that was missing before,
and AGREE for the four has_tag probes of regress/872, 2268, 1182_3
and 1894_1, whose oracle output is byte-identical to that of HEAD.

The regression check ran 52,262 journals through the C++ build, the
3.4.1 release, a scratch stage of HEAD checked file by file against
git, and the working tree, by per-posting flows with zero rows shown
and by Driver balances with includes spliced, in batches. They are
hand-written probes and witnesses and journals generated at random
around the tag test, among them 3,000 with elided postings beside
commodities that net to zero, in random orders, 600 split into a main
file and an included file at a random line, and 1,200 within the
bounds and at their edge. No journal that agrees with either binary at
HEAD disagrees in the tree other than by a refusal, by either
comparison (0 regressions in each, per-transaction flows included),
and every journal that disagrees with C++ in the tree, other than by a
refusal, gives the same output as at HEAD. By balances against the
build, 22,902 journals agree, with the same rows or a rejection on
both sides, 27,762 are refused, and the other 1,598 give the same
output as at HEAD; against the release, 22,901 agree, 27,763 are
refused and the same 1,598 remain. By flows against the build, 22,223
agree, 27,754 are refused and 2,285 give the same output as at HEAD.
Every one of the 5,583 journals in which no line that begins with `=`
calls `tag(`, `meta(` or `data(` gives the same output as at HEAD by
both comparisons. Of the journals that hold the rule and that the tree
reads, 7,247 by either comparison, every one agrees with both
binaries, generated postings included: 4,964 with the same rows and
2,283 rejected on both sides. A journal of each shape that the
docstrings name as refused is refused.

The oracle's default output over the 3816 positive journals is
byte-identical to the recorded snapshot by cmp (SNAP IDENTICAL): the
two postings that regress/842 now generates cancel. Its output over
the 469 negative journals is byte-identical to that of the previous
unit by cmp. The probe gate reports PROBES OK for the S37 set of five
verdicts. The environment-wide axiom audit finds 3113 constants in 31
Ledger modules and none outside the budget of propext,
Classical.choice and Quot.sound. The citation check passes, the
register gate reports CLEAN, and the forbidden-construct scan reports
none. The bisimulation over 4384 C++ test files gives PASS 3816 and
FAIL 0, with no out-of-scope skip and 489 negative, 77 journal-less
and 2 Python files skipped, exit 0.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0143bpV19FtoLrrR59maPRPk
C++ completes an elided posting, and the posting that it adds to the
bucket for a transaction of one posting, from the sum that Phase 1 of
finalize accumulates, keyed by annotated commodity, with one posting
for each entry of that sum (src/xact.cc:180-248, 368-372, 829-863;
src/annotate.h:120-125), an entry that nets to zero after a second key
has entered included (src/balance.cc:118-133), and extend_xact matches
each of those postings (src/xact.cc:1438-1447). The engine completes
either with one posting per commodity of a nonzero residual. An
automated transaction that matches a completion therefore books once
per key in C++ and once per commodity here, and nothing here where the
commodity nets to zero, and the check that C++ makes after the rule
sums what it generated by key. The implementation plan's probe set
for item N5 is four journals; before this commit, against both C++
binaries, they gave: d29/auto.dat, `= /^Equity/` with `(Budget)  $10`
over `Assets:Tajer  100 c (TAG)`, `Assets:Gruulmorg  200 c` and an
elided `Equity:Gold`, books `$20` in C++ and `$10` here (DIFF);
autosame.dat, the same without `(TAG)`, agrees; n5-zero.dat,
`= /Elided/` with `Budget  1` over `Assets:S  10 AAPL {$50}`,
`Assets:T  -10 AAPL` and an elided `Assets:Elided`, is rejected by C++
in the check that follows the rule and read here (CPP-REJECTS);
n5-zero-fixed.dat, with `Budget  $10` and `Other  $-10`, books `$20`
and `$-20` in C++ and nothing here (DIFF). Line numbers are those of
76b9dc2b2; the files cited are unchanged at f396a2e69.

This commit refuses such a rule, as out of scope, where its effect, or
the check that C++ makes after it, depends on postings that the
engine merges or does not make, with `line N: out-of-scope automated
transaction on a completion C++ splits by annotation` or `... on a
completion C++ posts at zero`, N being the line of the transaction.
The cases that these tests do not refuse are recorded beside the rules
that cause them, each with the direction in which it fails, and so is
the way in which a departure older than this commit hides a split from
them; they are listed below.

- annotLoop records whether the cost of a posting carries an
  annotation of its own (PostSpec.costAnnotated): C++ parses the cost
  with its annotations (src/textual_xacts.cc:691) and sums it under
  its annotated commodity (src/xact.cc:325-333).
- balancePool lists, for a pool that its elided posting or the
  bucket's posting completes, the weights that stand for what C++
  sums: each posting's cost, or else its amount, marked when
  annotated, and, apart from a cost that carries its own annotation,
  the lot gain that costVal adds to it, for the gain that C++ books
  apart and plain (src/xact.cc:735-763). A commodity in which two or
  more weights fall, one of them marked, is at risk: its completion
  postings carry PostSpec.splitRisk, and PoolResult.splitComms lists
  it with its weights, for the gross totals of a later commit; nothing
  reads that field yet. A commodity whose weights net to exactly zero
  becomes a ZeroCompletion of the elided posting or of the bucket's
  posting (src/xact.cc:368-372). The test for the bucket and the
  completions at zero that it makes are bound once.
- applyAutos now returns an error, with the transaction's line, and
  beside its postings the reason of a refusal that finalizeXact raises
  once verifyExtended has passed. Every other refusal is raised at once,
  before verifyExtended, and so takes the place of the reason of that
  check where both it and C++ reject a transaction for imbalance: what
  the engine generates from a completion that C++ splits or posts at
  zero need not be what C++ generates, and the check can find an
  imbalance there that C++ does not have. Under
  `= /^Equity/ and expr amount < -250` with `Budget  1`, C++ accepts the
  transaction of d29/auto.dat, splitting its completion into `-200 c`
  and `-100 c (TAG)`, neither of which passes the comparison, while the
  merged `-300 c` passes it here; the previous commit rejected that
  transaction for imbalance, and this one refuses it. A negative test
  of C++ in which both reject such a transaction for imbalance would
  therefore read the refusal where a comparison of reasons expects that
  of an unbalanced transaction; no test of the negative corpus does, the
  oracle's output over that corpus being unchanged. A rule without
  template postings is never refused: C++ runs it after every rule that
  has them (src/journal.cc:659-667), so that the note it adds reaches no
  rule that generates a posting, and it changes no balance. Any other
  rule is refused when one of its branches matches a completion at risk,
  its amount comparisons dropped, and the rule has a template with a
  fixed amount or an expression, or the branch compares the amount,
  calls all() or reads account("N").total; when a branch matches a
  completion at risk, the rule's templates only multiply, those of them
  that must balance do not cancel, and a written posting of the pool
  that the rule does not match has an annotated weight in that
  commodity, the refusal that waits for verifyExtended; when a branch
  matches a completion at zero of a commodity at risk; when a branch
  matches any other completion at zero, at its zero amount, and the rule
  has a template with a fixed amount or an expression, a template
  posting that is not `(virtual)`, or a note; and, beside a completion
  at zero, when a branch that calls any() or all(), which test every
  posting of the transaction in C++ (src/post.cc:653-700), matches a
  real posting or a real completion at zero once its calls and amount
  comparisons are dropped. A rule whose templates only multiply stays
  accepted on a completion at risk when its branches do none of these
  and the second of these tests does not refuse it: any() can only turn
  true as the rule generates postings, and every other condition reads
  the posting of each key as it reads the completion, the posting of a
  key carrying the details of the posting completed
  (src/xact.cc:241-243) and a commodity condition reading the base
  commodity (src/post.cc:350-370), so that a rule that matches the
  posting of the first key, which C++ tests where the elided posting is
  written, as it tests there the posting that it makes of the commodity
  unsplit, matches those of the keys after it. The argument covers only
  a rule that matches the first key: one that fails any() there can
  still pass it at a later key, a case listed below. With
  `(Budget)  0.1` in place of `$10`, d29/auto.dat books `-30 c` in C++
  and here. A rule whose templates are all `(virtual)` multipliers, with
  no note, stays accepted on any other completion at zero, as
  regress/2482 requires of its completion at zero in `BTC`. A periodic
  template is tested alike: C++ extends it
  (src/textual_xacts.cc:316-317) and gives each posting that a rule
  generates there to its account (src/xact.cc:1669-1671), and a real one
  makes a later real balance assignment to that account measure the
  real-only balance (src/textual_xacts.cc:398-409). Under `= /Cash/`
  with `Budget  1` and `Other  -1`, a template `~ Monthly` in which `$5`
  and `$-5` complete an elided `Assets:Cash` with `$0`, followed by
  `(Budget)  $10` and by `Budget  = $0` against an elided `Equity`,
  makes C++ post a zero, where `$-10` was posted here before this
  commit, which refuses it.

The plan's design differs from this in seven places, each forced by a
journal measured against both binaries.

- The plan tested an amount comparison at the merged completion, or at
  the negated weights of a commodity at zero. C++ compares the amount
  of each posting by key, which the engine does not know:
  `= /^Equity/ and expr amount > -250` with `(Budget)  1` books
  `-300 c` in C++ over the journal of d29/auto.dat, while the merged
  `-300 c` fails the comparison and the plan's rule would not fire.
  The tests therefore drop the comparisons, ZeroCompletion carries no
  amounts, and PoolResult.zeroCompletions takes the place of the
  planned splitZero list.
- The plan accepted, on a completion at risk, every rule whose
  templates only multiply and whose branch makes no amount comparison.
  C++ tests the posting of each key after the postings that the rule
  generated from the keys before it have joined the transaction and
  their accounts (src/xact.cc:1669-1671), so that a branch that calls
  all() or reads a total can stop matching partway through the keys:
  under `= /^Equity/ and expr 'all(account =~ /Assets|Equity/)'` with
  `(Budget)  1`, d29/auto.dat books `-200 c` in C++, the generated
  posting failing all() for the second key, and `-300 c` here; and
  after `(Budget)  10 c`, under `= expr account =~ /^Equity/ and
  account("Budget").total > -150 c` with `(Budget)  1`, the same
  transaction books `-200 c` in C++ and `-300 c` here. Such a branch
  is refused as well. And C++ checks the transaction again after a
  rule that adds a posting that must balance, summing each annotated
  key apart (src/xact.cc:1040-1110, 1683-1684), while verifyExtended
  weighs the merged completion: under `= /^(Assets:C|Equity)/` with
  `Budget  1`, C++ rejects `Assets:A  100 c (TAG)`, `Assets:B  -100 c`
  and `Assets:C  300 c` against an elided `Equity:Gold`, which it
  completes with `-200 c` and `-100 c (TAG)`, with the remainder
  `100 c` and `-100 c (TAG)`, and accepts them without `(TAG)`, while
  verifyExtended passes both. A rule whose templates that must balance
  do not cancel is therefore refused where a written posting that it
  does not match has an annotated weight in the commodity of the
  completion that it matches, once verifyExtended has passed, so that
  a transaction that the engine's own check rejects keeps that reason:
  with `Assets:B  -50 c`, both reject that journal for imbalance.
- The plan counted a commodity condition among the triggers. C++ reads
  the commodity of a posting with its annotation stripped
  (src/post.cc:350-370): `expr commodity == 'c'` with `(Budget)  1`
  books `-300 c` in C++ and here, so the condition is not a trigger.
- The bucket's posting, completions at zero of a commodity that is not
  at risk, and the functions any() and all() beside a completion at
  zero were not in the plan. They rest on the same mechanism. With
  `bucket Assets:Cash`, under `= /Cash/` with `(Budget)  $10`,
  `Assets:T  10 AAPL {$50} @ $5 {$1}` alone gives `Assets:Cash` the
  postings `$-450` and `$-50 {$1}` in C++, which books `$20`, against
  `$-500` and `$10` here, and `Expenses:Food  $0` alone books `$10` in
  C++ and nothing here. Under the same rule, `3 EUR`, `$5` and `$-5`
  complete an elided `Assets:Cash` with `$0` and `-3 EUR` in C++,
  which books `$20`, against `$10` here. And
  `= /Assets:S/ and expr 'all(account =~ /Assets:[ST]/)'` with
  `(Budget)  $10` books nothing in C++ over the transaction of
  n5-zero-fixed.dat and `$10` here.
- The plan accepted a rule whose templates only multiply on a
  completion at zero that does not split. C++ generates from that
  completion a posting of zero for each template posting, which the
  engine does not make, and appends to the completion a note written
  before the template postings (src/xact.cc:1497-1503). A posting of
  zero that is not virtual makes a later real balance assignment to
  its account measure the real-only balance
  (src/textual_xacts.cc:398-409): under `= /Cash/` with `Budget  1`
  and `Other  -1`, after `$5` and `$-5` complete an elided
  `Assets:Cash` with `$0`, and a later `(Budget)  $10`, C++ posts a
  zero for `Budget  = $0`, where `$-10` was posted here. Such a rule
  is refused, as is one with a template posting that is not
  `(virtual)` or with a note. One whose templates are all `(virtual)`
  multipliers stays accepted: regress/2482, in the positive corpus,
  matches its completion at zero in `BTC` with one, and refusing it
  would refuse that test.
- The plan refused a rule on a completion at zero of a commodity at
  risk whatever its templates, a rule without template postings
  included. C++ runs such a rule after every rule that has them
  (src/journal.cc:659-667), so that its note reaches no rule that
  generates a posting: under `= /Cash/` with only `; :Z:`, `$5` and
  `$-5` complete an elided `Assets:Cash` with `$0` in C++, which tags
  that posting and books nothing, and nothing is booked here. No test
  refuses such a rule.
- The gain weight was not in the plan: under `= /Elided/` with
  `(Budget)  $10`, `Assets:T  10 AAPL {$50} @ $5 {$1}` completes an
  elided posting with `$-450` and `$-50 {$1}` in C++, which books
  `$20`, against `$-500` and `$10` here, although no other weight falls
  in `$`.

Seventeen departures are recorded beside the rules, each measured
against both binaries, and a departure that verifyExtended recorded
before this commit gains a witness. Four of the seventeen fail open on
an automated transaction that meets a completion that C++ splits or
posts at zero. The lot that C++ carries into the amount of a balance
assignment (src/textual_xacts.cc:843-862) is not seen, so that a split
that it causes is not refused: with `10 AAPL {$5}` held in
`Assets:Broker`, `Assets:Broker  = 0 AAPL` beside
`Assets:Other  4 AAPL` completes an elided `Equity:Gold` with
`-4 AAPL` and `10 AAPL {$5}` in C++, which books `$20` under a fixed
`$10`, and with `6 AAPL` here, which books `$10`. The gain that C++
books apart on a cost whose annotation has a price in the commodity of
the lot price is not counted when costVal books none, so that a split
that it causes is not refused: under `= /Elided/` with
`(Budget)  $10`, `Assets:T  10 AAPL {$5} @ $5 {$20}` completes an
elided posting with `$950` and `$-50 {$20}` in C++, which books `$20`,
and with `$-50` here, which books `$10`; and the gain that C++ books
on a cost in another commodity is not counted either:
`Assets:T  10 AAPL {$50} @ 5 EUR {$1}` against an elided posting books
`$20` in C++ and `$10` here. The postings of zero that a rule of
`(virtual)` multipliers generates from a completion at zero enter the
sum by which C++ shows their account, a sum that drops the precision
of a lone inferred amount (src/amount.cc:634-642): when `Budget` holds
a lone inferred `$-0.0004`, `= /Cash/` with `(Budget)  1` over the
completion of `$5` and `$-5` makes C++ show `Budget` as zero, while it
shows `$-0.0004` here. The fifth case that these tests do not refuse
is the per-key departure of verifyExtended, which holds without a
split, and its record there gains the witness: C++ derives the cost of
a posting that a rule generates from a matched posting with a cost
from the lot price of the matched amount, which lacks an annotation
that the cost carries (src/xact.cc:1611-1627), so that under
`= /^(Assets|Equity)/` with `Budget  1` it rejects
`Assets:Tajer  10 AAPL @ $5 (note)` against an elided `Equity:Gold`,
with the remainder `$50` and `$-50 (note)`, and the same posting
beside `Assets:Gruulmorg  $20`, whose completion it splits into
`$-20` and `$-50 (note)`, and accepts both without `(note)`, while all
four are accepted here.

The other thirteen predate this commit and depart from C++ whether or
not a rule meets a completion that C++ splits or posts at zero. At
PostSpec.costAnnotated, three: C++ books no gain of the usual kind on
a cost that carries an annotation of its own, and books one apart, of
the basis less the cost times the price that annotates it, only when
that price is in the commodity of the lot price, whatever the
commodity of the cost (src/xact.cc:693, 735-774;
src/annotate.cc:343-355), while costVal adds the basis less the cost
to a cost in the commodity of the lot price and nothing to a cost in
another one. `Assets:S  10 c {$60} @ $5 (note)` completes an elided
posting with `$-50 (note)` in C++ and `$-600` here,
`Assets:T  10 AAPL {$50} @ $5 {$20}` with `$500` and `$-50 {$20}`
against `$-500`, and `Assets:T  10 AAPL {$50} @ 5 EUR {$1}` with
`$-450` and `-50 EUR {$1}` against `-50 EUR`, and the commodity swap
of baseline/feat-commodity_swap with the amount of its loss posting
removed books `$2,000.00` to `Expenses:Capital Loss` in C++ and
nothing here. At the note rule of applyAutos: C++ gives a note written
under a template posting only to the postings generated from that
template (src/xact.h:370-374, src/xact.cc:1658-1665), which no later
rule with template postings matches (src/xact.cc:1443-1447), while
finalizeAuto gathers every note line of the rule: under `= /Food/`
with `(Budget)  $10` and `; :SEEN:` under it, and `= /Food/ and %SEEN`
with `(Budget)  $10`, `Expenses:Food  $5` books `$10` in C++ and `$20`
here. In the docstring of applyAutos: the engine runs a rule without
template postings in its place in the file, while C++ runs it after
every rule that has them: under `= /^Equity/` with only `; :SPLIT:`,
followed by `= %SPLIT` with `(Budget)  1`, `Assets:Tajer  100 c`
against `Equity:Gold  -100 c` books nothing in C++ and `-100 c` here.
Beside the events of elided real postings: the bucket's posting for a
transaction of one posting whose sum is zero has no event here, so
that with `bucket Assets:Cash`, after `Expenses:Food  $0` alone and
`(Assets:Cash)  $10`, `Assets:Cash  = $0` posts a zero in C++ and
`$-10` here. At PoolResult.posts: C++ fills the elided posting where
it is written with the first key of the sum, in the order in which it
sorts the keys, and appends a posting for each further key after the
postings of the transaction (src/xact.cc:234-246, 834-835;
src/balance.cc:494-497, 509-521), while the engine's completions
follow the pool's written postings, in the reverse of the order in
which their commodities first appear; a rule tests each posting after
the postings that it generated from those before it, which any() and
all() read: under `= expr 'account =~ /^Assets/ or account
=~ /^Equity/ and any(account =~ /Budget/)'` with `(Budget)  1`, an
elided `Equity:Gold` written before `Assets:Tajer  100 c` and
`Assets:Gruulmorg  200 c` makes C++ book `300 c` and this `0 c`, and
under `= /Elided/ and expr 'all(account =~ /Assets/)'` with
`(Budget)  1`, `Assets:S  $5` and `Assets:T  3 EUR` against an elided
`Assets:Elided` make C++ book `$-5` and this `-3 EUR`. At
verifyExtended: verify() sums the cost that Phase 6 rewrites into the
basis of a lot when that cost carries a price of its own in the
commodity of the lot price (src/xact.cc:735-774), which the engine
does not weigh: under `= /Assets:Y/` with `Budget  1` and `Other  -1`,
C++ rejects `Assets:U  5 AAPL {$40} @ $5 {$1}`, `Assets:X  $-25 {$1}`
and `Assets:Y  $-175`, which it accepts without the rule, and both are
accepted here. At flushPending, two: C++ gives each posting that a
rule generates in a periodic template to its account
(src/xact.cc:1669-1671), where the posting, which belongs to no
transaction (src/textual_xacts.cc:490, src/xact.cc:93-100), takes the
current date, that of --now when it is given (src/post.cc:163-174); a
real one makes a later real balance assignment to that account
measure the real-only balance (src/textual_xacts.cc:398-409), and an
assignment dated on or after the current date sums its amount as well
(src/textual_xacts.cc:430-439), while the engine keeps only the
template's verdict: under `= /Expenses:Food/` with `Budget  1` and
`Other  -1`, after `~ Monthly` with `Expenses:Food  $100` against an
elided `Assets:Cash`, and `(Budget)  $10`, `Budget  = $0` against an
elided `Equity` posts a zero in C++ and `$-10` here, and with the
assignment dated after the current date C++ posts `$-100`. C++ reads a
posting of a template as one without a transaction, for which any()
is false and all() true (src/post.cc:653-700), while the engine tests
both over the template's postings and those generated: under
`= /^Equity/ and expr 'any(account =~ /Tajer/)'` with `Budget  1`, a
template of `Assets:Tajer  100 c` against an elided `Equity:Gold` is
accepted by C++ and rejected here (fail-closed), and under
`= /^Equity/ and expr 'all(account =~ /Nothing/)'` with `Budget  1`
it is rejected by C++ and accepted here (fail-open). At the account()
predicate of an automated transaction, two: the engine reads the
balance that the account held before the transaction, the quantities
of all its commodities summed, while C++ reads the account's total
(src/account.cc:884-900), with the postings of the transaction that
have joined the account (src/xact.cc:987-1010) and those that rules
generated before, and fails with "Cannot convert a balance with
multiple commodities to an amount" when that total is held in more
than one commodity or under more than one annotation: under
`= expr account =~ /^Equity/ and account("Budget").total > 5 c` with
`(Budget)  1`, `Budget  10 c` against an elided `Equity` books
`-10 c` to `Budget` in C++ and nothing here, and with `(Budget)  10 c`
and `(Budget)  3 EUR` before, under the bound `-150 c`, C++ rejects
`Assets:A  100 c` against an elided `Equity`, which is accepted here
(fail-open), as it does with `3 c (TAG)` in place of `3 EUR`. And
beside dateOk, a departure recorded nowhere before: the engine reads
the date of the transaction, while C++ reads the date of the posting
tested (src/item.cc:149-150, 354-355; src/post.cc:143-174), which a
note `; [DATE]` on the posting sets (src/item.cc:86-103): under
`= /^Equity/ and expr date >= [2024/02/01]` with `(Budget)  $10`,
`Assets:Tajer  100 c` and `Assets:Gruulmorg  200 c` against an elided
`Equity:Gold` that carries `; [2024/03/01]`, in a transaction dated
2024/01/01, book `$10` in C++ and nothing here.

These tests read each branch as the engine reads it. A departure that
makes the engine read a branch as false where C++ reads it as true for
the posting of some key therefore hides the split, or the completion at
zero, from them, and the docstring of applyAutos says so. The placement
of a completion does this for a call of any() or all() that the split
decides: under the any() rule above, with `(TAG)` on `Assets:Tajer`, C++
books `200 c`, the first key, `-200 c`, failing any() and the appended
`-100 c (TAG)` passing it, and `300 c` without `(TAG)`, while this books
`0 c` in both; and under
`= /^Equity|Assets:Tajer/ and expr 'all(account =~ /Assets|Equity/)'`
with `(Budget)  1`, the postings of d29/auto.dat with the elided posting
written first make C++ book `-200 c`, and `-300 c` without `(TAG)`,
while this books `100 c` in both. The split can there decide as well
whether C++ accepts the transaction: under
`= /^(Assets:C|Equity)/ and expr 'all(account =~ /Assets|Equity/)'` with
`Budget  1`, an elided `Equity:Gold` written before
`Assets:A  200 c (TAG)`, `Assets:B  100 c` and `Assets:C  -100 c` is
completed in C++ with a zero where it is written, from which the rule
generates `Budget  0`, all() then failing for `Assets:C`, and C++
accepts the transaction, which it rejects without `(TAG)`, while the
engine generates `Budget  -100 c` from `Assets:C` and rejects both for
imbalance, before this commit and after it (fail-closed). The reading of
the quantifiers in a template does it for a call of all() that is false
here: under `= /^Equity/ and expr 'all(account =~ /Nothing/)'` with
`(Budget)  $10`, a template with the postings of d29/auto.dat, followed
by `(Budget)  $5` and, dated after the current date, by `Budget  = $0`
against an elided `Equity`, makes C++ post `$-25`, and `$-15` without
`(TAG)`, where `$-5` is posted here in both. The balance that the engine
reads for account("N").total, the place of a rule without template
postings, the note written under a template posting and the date of a
posting do it as well: under the date rule above, the postings of
d29/auto.dat in a transaction dated 2024/01/01, with `; [2024/03/01]` on
the elided `Equity:Gold`, make C++ book `$20`, and `$10` without
`(TAG)`, while this books nothing in both.

The refusals over-approximate wherever the engine cannot tell what C++
keeps, and the docstrings say so. They refuse, fail-closed, a commodity
whose weights all carry one annotation; a gain counted on a cost whose
annotation has no price in the commodity of the lot price; a zero entry
that C++ drops because it netted to zero before a second key entered; a
rule whose templates only multiply on a completion at zero of a
commodity at risk, when what it generates nets to zero in each key, as
when its templates cancel or are all virtual, or when it also matches
the written postings whose weights the keys negate; an expression
template whose result is linear in the amount; on a completion at risk,
a call of all() that the generated postings satisfy, a comparison of the
amount or a total that every key passes as the completion does, a
template with a fixed amount when a call of all() or any(), or a
comparison of the amount, lets a single key match, as under
`= /^Equity/ and expr amount < -150` with `(Budget)  $10` over
d29/auto.dat, where C++ books `$10` on the key of `-200 c` alone, and a
rule whose templates that must balance do not cancel where the annotated
weights of the written postings that it does not match net to zero in
each key, as with `(TAG)` on both `Assets:A` and `Assets:B` in the
journal of the second plan difference above, which C++ accepts with
`Assets:D  50 c {$2}` and `Assets:E  -50 c {$2}` beside them or without;
on a completion at zero, a rule with template postings and a note, or
with a template posting that is not `(virtual)`, when nothing later
reads what it adds; and a quantifier beside any completion at zero. So
is a rule on a completion in a periodic template that C++ reads as the
engine does: under `= /^Equity/` with `(Budget)  $10`, a template with
the postings of d29/auto.dat is refused, while C++ shows the balances
that the engine shows without the refusal.

To close the class rather than its instances, the combinations of the
dimensions on which these tests turn were enumerated, one journal for
each, in two matrices (scratch, not committed). The first holds 28116:
the completion, through an elided real posting written first or last,
the bucket's posting, or an elided bracket-virtual or deferred
posting; its context, a dated transaction or a periodic template, each
with or without a later virtual posting and a real balance assignment,
dated after the current date, to the account to which the rule posts;
the commodity, named or null, on which C++ keeps no annotation;
nineteen shapes of written postings, of one or two plain amounts, a
note, a lot price or a lot date on one of two, one note on both, a
zero net with or without a note, beside a third commodity or not, two
amounts plain or split beside a third commodity, a cost with or
without a priced annotation over a lot, a cost with or without a note
beside a plain amount, and the bucket's posting alone, with or without
a note, at zero or not; eleven predicates, on the account of the
completion or of every real posting, the commodity, the amount, a
total, a tag, any() in two forms and all() in three; and nine
templates, virtual, real and bracket-virtual multipliers, a real
multiplier alone, a fixed virtual amount, a fixed real pair, an
expression, a note before a multiplier, and a note alone. Its
predicates match the completion alone or every posting, so it never
meets the check that follows a rule that matches only some of the
written postings, which a review of an earlier draft of this commit
found unrefused. The second matrix, of 39564 journals, enumerates that
dimension: the completion, through an elided real posting written
first, second or last, the bucket's posting, an elided bracket-virtual
posting among bracket-virtual postings, or an elided deferred posting;
a dated transaction or a periodic template; a named or null commodity;
the written postings, three in one commodity whose first two net to
zero, each plain or with a note, a second note, a lot price or a lot
date, in fourteen combinations, three whose first two do not net to
zero, in four, two that net to zero, in eight, or a cost written
plain, with a note or with a lot price, or over a lot with or without
a priced annotation, beside a plain or noted amount and a plain amount
in the cost's commodity, or the bucket's posting alone, plain, noted,
at zero or with a cost; every set of the written postings and the
completion that the rule's account pattern can match; and twelve
templates, five of them for the postings whose first two do not net to
zero and for the bracket-virtual and deferred completions: a virtual
multiplier, real and bracket-virtual multipliers that cancel and that
do not, a real multiplier of one half, a real and a bracket-virtual
multiplier that cancel, a virtual multiplier beside a real one, a
fixed virtual amount, a fixed real pair, and an expression. Each
journal of both matrices was run through both C++ binaries, through a
stage built from the previous commit's Journal.lean and through a
stage of this tree, and the two binaries agree on every one. In
neither matrix does a combination that agreed with C++ before this
commit disagree after it, and the oracle's output changes only by a
refusal. In the first, 13056 agree before and after, 5569 that agreed
are refused (fail-closed), 2048 that disagreed in their rows and 133
that the oracle rejected for another reason are refused, 780 that C++
rejects are refused, 2090 are rejected by both before and after, 548
of them now by a refusal, and 4440 disagree before and after, 3397 in
their rows, 858 accepted where C++ rejects them and 185 rejected where
C++ accepts them. In the second, 17301 agree before and after, 4130
that agreed are refused (fail-closed), 1070 that disagreed in their
rows and 665 that C++ rejects are refused, 12241 are rejected by both
before and after, 540 of them now by a refusal, and 4157 disagree
before and after, 1943 in their rows, 2152 accepted where C++ rejects
them and 62 rejected where C++ accepts them. Of those 1088 now
refused, 893 were rejected before this commit for an imbalance after
an automated transaction in the transaction or template that is now
refused, whose check the refusal precedes, and 195, all in the first
matrix, for an imbalance or a balance assignment in a later
transaction, the oracle reporting only the first error of a file. The
test that waits for verifyExtended accounts, in the second matrix, for
465 of the refusals of journals that C++ rejects and for 90 of those
that agreed, each of the latter a journal whose written postings that
the rule does not match carry one note or one lot price and net to
zero.

Each of the 4440 of the first matrix was assigned to a departure
recorded beside its rule by tests over its sibling combinations, among
them the same journal without the transaction or template that carries
the completion, the dated counterpart of a template, and the
counterpart with the elided posting written last: 2390 to the postings
that C++ gives to accounts from a template (flushPending), 538 to
bracket-virtual and 514 to deferred postings, which the engine does not
match (applyAutos), 456 to the placement of a completion
(PoolResult.posts), 236 to the quantifiers of a template
(flushPending), 209 to the totals of the account() predicate, 93 to the
cost that Phase 6 rewrites (verifyExtended), and 4 to the cost that
C++ derives for a generated posting (verifyExtended). 484 of them
depend on the split in C++, the rule booking otherwise with the
annotations removed: 278 on a bracket-virtual or deferred completion,
92 on the rewritten cost, 78 on the cases of any() and all() that the
placement departure hides, 28 on the postings that a rule generates in
a template, 5 on a total that the annotation of a generated posting
leaves under two keys, and 3 on the derived cost. The 5569 refused
while they agreed fall under the over-approximations above: 2832 in a
periodic template, 1231 for a quantifier beside a completion at zero,
532 for a template posting that is not `(virtual)`, 174 for an
expression and 174 for a note, each on a completion at zero, 208 on a
completion at zero of a commodity at risk, 150 for an expression linear
in the amount, 132 for weights that all carry one annotation, 56 for a
fixed amount that a quantifier lets a single key match, 50 for a zero
entry that C++ drops, and 16 for a comparison of the amount and 14 for
a total that every key passes as the completion does.

Each of the 4157 of the second matrix was assigned in the same way, by
the mechanism that its dimensions name, checked against a sibling
combination: 3229 to bracket-virtual postings, or to a deferred
completion that the rule matches, which the engine does not match
(applyAutos), the counterpart with a real elided posting agreeing or
being refused or rejected by both in all but those that the next three
departures cover; 499 to the check of the written postings that a rule
matches without the completion, which C++ keys by annotation
(verifyExtended), C++ accepting the counterpart without the annotations;
364 to the cost that Phase 6 rewrites (verifyExtended), which C++
rejects under templates that cancel as well, and whose counterpart
without the priced annotation it accepts; and 65 to the cost that C++
derives for a generated posting (verifyExtended), whose counterpart
without the annotation of the cost it accepts. 1365 of them depend on
the annotations, the rule booking otherwise with them removed: 442 on
bracket-virtual or deferred postings, 499 on the written postings
matched without the completion, 364 on the rewritten cost and 60 on the
derived cost. The 4130 refused while they agreed fall under the
over-approximations above: 2577 in a periodic template, 506 for an
expression linear in the amount, 555 on a completion at zero of a
commodity at risk, 210 on another completion at zero for a template
posting that is not `(virtual)` or for an expression, 192 for a fixed
amount where every weight carries one annotation or where C++ drops a
zero entry, and 90 by the test that waits for verifyExtended, as above.

Verification, on the final tree. The locked build script, lake build
with warnings as errors, exits 0 after 1225 jobs with no warning in its
log. cmp_rows.py over the four journals gives ORACLE-REJECTS with the
split reason for d29/auto.dat, AGREE for d29/autosame.dat, BOTH-REJECT
for crit/n5-zero.dat, whose oracle reason is the split reason, and
ORACLE-REJECTS with the split reason for crit/n5-zero-fixed.dat, against
the C++ build and the 3.4.1 release alike. The added lines name no
scratch path (grep exit 1). negcheck.py reports 469 negative journals,
136 rejected by the oracle, and no rejection that C++ does not share.
Every witness that the docstrings cite for the test that waits for
verifyExtended, for the comparison of the amount that lets a single key
match, for the derived cost, for the date of a posting, for the refusals
that precede the engine's check and for the zero that C++ completes in
place under all() was run through both binaries, the stage of the
previous commit and a stage of this tree, and gives the rows and
verdicts cited. The first journal of that test, and its variants with
`[Budget]  1`, with `Budget  2` and `Other  -1`, with `Budget  0.5` and
the elided posting written first, with a lot price and with a lot date,
the journal of `Expenses:Rent`, and a payday journal under
`Liabilities:Tax  0.2` and `Expenses:Tax  -0.1`, are rejected by C++ and
accepted before this commit, and are refused by this tree; each
counterpart without the annotations agrees before and after. The journal
under templates that cancel agrees before and after; the journal with
`Assets:B  -50 c` is rejected by both, here for imbalance; the journal
with `(TAG)` on `Assets:B` as well, and the same with a lot price in
place of both notes, agree before this commit and are refused by it, as
are the journal of the single key under `expr amount < -150` and the
journal with `(TAG)` on `Assets:B` and with `Assets:D  50 c {$2}` and
`Assets:E  -50 c {$2}` added, in either order. The journal under
`expr amount < -250` is accepted by C++, was rejected here for imbalance
before this commit, and is refused by it. The two journals of the
derived cost are rejected by C++ and accepted here before and after, and
their counterparts without `(note)` agree; the journal of the date of a
posting and its counterpart without `(TAG)` disagree in their rows
before and after; and the journal of the zero completed in place under
all() is accepted by C++ and rejected here for imbalance before and
after, while its counterpart without `(TAG)` is rejected by both.

The change was prototyped on a scratch stage first. Over the 3816
positive journals the oracle's default output is byte-identical to the
recorded snapshot (SNAP IDENTICAL), and over the 469 negative journals
it is byte-identical to that of the previous commit (cmp), although
the plan allowed this commit to change it. Of the 284 probe journals
that the plan's units name and that exist, only auto.dat, n5-zero.dat
and n5-zero-fixed.dat change output, each to the refusal. A second
stage, built from the previous commit's Journal.lean and checked to
reproduce its output on those 284 journals, was compared with a stage
of this commit's Journal.lean over 9000 generated journals: 4500
around split, zero-net, bucket and quantifier shapes, with costs
annotated or not, bracket-virtual and deferred elided postings,
periodic templates and a commodity equivalence, each with a twin that
has no automated transaction. Against either binary, no journal
changes output other than by a refusal, and no twin changes. 534
journals that disagreed with C++ while their twin agreed are now
refused, 128 that disagreed along with their twin are refused as well,
and 730 that agreed are refused (fail-closed). Of the journals with
rules that still disagree while their twin agrees, 187 elide a
bracket-virtual or deferred posting, which the engine does not match
(the departure recorded at applyAutos), 1 rests on the cost that Phase
6 rewrites (src/xact.cc:774) before the check that follows a rule,
which verify() sums, for a cost that carries its own lot price, the
departure that verifyExtended now records, 2 on the display of a
generated amount in a commodity of a C directive, and 2 on a note
written under a template posting (the departure recorded at the note
rule); each of these gives the same output as before this commit, and
none rests on a completion that C++ splits or posts at zero. 400 more
generated journals aimed at the bucket's posting, each with its twin,
give the same picture: no output changes other than by a refusal, 31
journals that disagreed while their twin agreed are refused, 51 that
disagreed along with their twin and 39 that agreed are refused as
well, and the only journals with rules that still disagree while their
twin agrees are 6 that C++ rejects in the check that follows a rule
over a cost that Phase 6 rewrites, the shape above.

1500 further generated journals, each with a twin, compare the same
two stages, and against either binary no output changes other than by
a refusal. 700 chain rules, read quantifiers, payee, comment and tag
conditions and notes on the elided posting, each with a twin that has
no automated transaction, and no twin changes. Of these journals, 83
with rules still disagree while their twin agrees, each as before this
commit: 60 on a payee condition, which C++ rejects, 16 on a comment
condition, 2 on `not %TAG`, 4 on the place and order of the postings
of a completion in two commodities under all(), the departure now
recorded at PoolResult.posts, each of which disagrees as well with its
annotations removed, and 1 on the cost that Phase 6 rewrites, as
above. In one of the four, under `= /Elided/ and expr 'all(account =~
/Assets/)'` with `(Budget)  -0.5`, C++ shows `$-112` in `Budget` with
the annotations and `$-125` without them, so that the split decides
what it books there, a case of the refusal of all() that the placement
departure hides. 800 are built so that C++ splits a completion by
annotation, under rules whose templates only multiply, with conditions
on the commodity, a tag, the account, any() and all(), and notes
written before the template postings, each with a twin whose
annotations are removed. The 36 that disagreed while their twin agreed
all call all() and are now refused, so that none of them disagrees any
longer, and 120 that agreed are refused, 114 of them for a call of
all() that the generated postings satisfy (fail-closed). These 6400
journals and their twins, with 1329 more, each with a twin, that a
review of an earlier draft of this commit generated, were run again
through both binaries, the stage of the previous commit and a stage of
this tree: every figure above holds for this tree, and the test that
waits for verifyExtended changes the output of 2 of the 1329, each of
which C++ rejects, and of no other journal.

Residual issue R5 of the plan asks the last unit allowed to change the
output over the negative corpus, this one, to run C04's step 0 and a
draft of C05's same-entry check. Two scratch copies of test/semantic
were run against this tree: one with C04's NEG classifier, as an
earlier unit of this series rebuilt it from the plan, and one with a
draft of the same-entry check added to it, under which the oracle and
C++ must reject the same entry, by the header line, or by the last
line of a periodic template, while a file whose C++ context names an
included file is counted apart, and a fixture whose rejected entries
differ must fail. Each gives PASS 3816, FAIL 0, NEG:unbalanced 35 and
SKIP:negative 454, exit 0. Under the draft, 28 files match by header
line, 5 periodic templates by their last line, and 2 are counted
without the check, as C05's design measured, and every NEG detector
fixture, the new one included, is green.

The environment-wide axiom audit finds 3146 constants in 31 Ledger
modules and none outside the budget of propext, Classical.choice and
Quot.sound. The forbidden-construct scan reports none, the register
gate reports CLEAN, the citation check passes, the probe gate reports
PROBES OK for the S38 set of four verdicts, and the bisimulation over
4384 C++ test files gives PASS 3816 and FAIL 0, with no out-of-scope
skip and 489 negative, 77 journal-less and 2 Python files skipped,
exit 0.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0143bpV19FtoLrrR59maPRPk
Item I21 of the Ellerman analysis compares gross debit and credit
totals with C++ through an opt-in flag (decision D26 of the
implementation plan), and the driver that will print them needs two
facts that the journal engine did not export. The first is a count of
the flows of each account and commodity in each column, for the display
rule of a lone inferred amount (BIGINT_COST_PREC, regress/1692), which
Ledger/Driver.lean applies to a net row from the counts of flowStats
and which a debit or credit column needs from its own flows. The second
is whether the engine can stand for the gross totals of C++ at all. C++
completes an elided posting, or the bucket's posting, with one posting
for each annotated key of the sum that Phase 1 of finalize accumulates
and Phase 6 extends with the lot gains that it books
(src/xact.cc:180-248, 368-372, 692-713, 735-763, and Phase 7 at
824-863; src/annotate.h:120-125), while the engine completes it with
one posting per commodity. Decision D29 refuses gross totals where that
split moves them, and it keys the refusal on splits whose weights have
both signs, whatever the annotation, after measurement showed that the
planning prototype's key, the lot price alone, missed splits by date,
tag and fixation and refused seven files whose splits share one sign.
This commit adds both facts to the engine's result. Nothing reads them
yet, and the oracle's output does not change.

bumpStat takes the quantity of the flow that it counts, and
St.signStats and Result.signStats count, for each account, commodity
and column, the flows of nonzero amount and the inferred ones among
them, a positive amount falling in the debit column; one helper,
bumpCount, keeps both these counts and those of flowStats. Under `--dc`
C++ gives each posting the pair
`(amount > 0 ? amount : 0, amount < 0 ? amount : 0)`
(src/report.h:751-754 at 76b9dc2b2; test/baseline/opt-dc.test), so a
posting of zero brings the integer 0 to both columns, and C++ adds that
integer by way of a balance, which ignores a zero (src/value.cc:572-575,
591-594, 621-622, 1524-1525; src/balance.h:131;
src/balance.cc:122-123). Such a posting therefore counts in neither
column. With the build and with the 3.4.1 release,
`A  1 FOO @ $0.0004` against an elided `B`, with a transaction of
`B  $0` and `C  $0` before it or after it, prints no row for `B` under
`bal --flat`, the sum having dropped the precision of the lone inferred
amount (src/amount.cc:634-642), and still prints `$0.0004` in the
credit column of `B` under `bal --dc --flat`. A zero whose amount
carries an annotation departs from this in the net row, a departure
older than this commit: C++ adds `B  $0 (n)` to the total of `B` by way
of a balance as well, since its annotated commodity is not the plain
one (src/value.cc:600-603), so that both binaries print `$-0.0004` for
`B`, while flowStats counts the zero and the oracle prints no row. The
docstring of bumpStat records it and names both readers of flowStats,
the net row of the driver and the balance assignment of finalizeXact.

St.lotFanout and Result.lotFanout are set when a pool that its elided
posting or the bucket's posting completes has a commodity listed in
PoolResult.splitComms, as the preceding commit made it, whose weights
include one above zero and one below, whether its residual is zero or
not, or whose completion the engine tags with the precision of a cost.
When no two weights of a commodity have opposite signs, no two postings
of its keys do either, and the debit and credit totals of the completed
account are those of the engine's single posting; when the weights have
both signs, the postings can have both. In regress/1638, C++ completes
the elided Income:Currency of the transaction `Buy food` with `40 USD`
and `-40 USD {=1.30 CAD}`, and the columns of that account hold 100 USD
and 140 USD in both binaries, where the engine's flows give 60 USD and
100 USD. A split of one sign can still change how a column shows. C++
applies its test of the tag to each posting of the split
(src/xact.cc:234-247), and the sum of two of them in one column drops
the tag (src/amount.cc:634-642). With `A  1 FOO @ $0.0001` and
`B  $5 (n)` against an elided `C`, both binaries complete `C` with
`$-0.0001` and `$-5 (n)` and show `$5` in its credit column, while the
engine's single completion, `$-5.0001`, is tagged and counts in
signStats as a lone inferred amount, which a column read from those
counts would show unrounded. This second condition goes beyond decision
D29, which keys the refusal on splits of both signs alone, and beyond
the design that the plan gives this commit. Over the positive corpus it
flags no journal that the first condition leaves unflagged: the scratch
output described below is byte-identical with the second condition and
without it. The comment beside the rule cites src/xact.cc:180-248,
Phase 7 and regress/1638, as residual issue R16 of the plan asks; the
plan's range 180-247 is given as 180-248, the lines of
add_balancing_post at 76b9dc2b2, as the preceding commit cites them. The
docstring of PostSpec.inferred, which had none, calls the field the
engine's mark of the tag and points to the comment that records where
the mark departs from the tag, and those of PostSpec.splitRisk and
PoolResult.splitComms name the rule as a reader.

The docstring of Result.lotFanout records, each with a journal measured
against both binaries, where the flag is set in excess and where it is
missed. It is set in excess where C++ keeps all the weights of the
commodity under one key, as in regress/1217, or where its keys net to
one sign, in either case only where the completion is not tagged; where
a tagged completion shares its column with another flow; where the tag
cannot change how the column shows, since C++ widens the display of a
tagged amount only at display precision 0 or where the amount rounds to
zero at that precision (src/amount.cc:2041-2054), and since a whole
amount, which C++ leaves untagged (src/xact.cc:222-226), shows alike at
any precision; and where the commodity is listed only for a weight of
zero, which C++ adds to no key (src/balance.h:131;
src/balance.cc:122-123). After `X  $1.0000` against `Y`, the tagged
split above shows `$5.0001` in the credit column of `C` in both
binaries, as the engine's single posting does; `A  10 FOO @ $5.00` and
`B  $10 (n)` complete an elided `C` with `$-50` and `$-10 (n)` in both
binaries, neither tagged, whose credit column shows `$60` as the
engine's tagged `$-60` does; and `A  1 FOO @ $0.0001` beside `B  $0 (n)`
completes an elided `C` with `$-0.0001` alone in both binaries, whose
credit column shows `$0.0001` as the engine's does. Where the keys net
to one sign and the completion is tagged, the flag is needed: with
`B2  $-1 (n)` beside the tagged split above, both binaries complete `C`
with `$-0.0001` and `$-4 (n)` and show `$4` in its credit column, where
the engine's tagged `$-4.0001` counts as a lone inferred amount. It is
missed where the engine's weights depart from those of C++, as the
docstrings of PoolResult.splitComms and PostSpec.costAnnotated record,
and where C++ gives an amount an annotation that the engine does not
keep, as the docstring of PostSpec.fixated records. Among the first is
the lot gain of a bracket-virtual posting, which C++ adds to the real
balance (src/xact.cc:712-713) and the engine weighs in the
bracket-virtual pool: with an elided `[Assets:D]` and an elided
`Assets:D`, `[Assets:A]  -10 AAPL {$1} @ $5` beside `Assets:C  $-60 (n)`
completes `[Assets:D]` with `$50` and `Assets:D` with `$-40` and
`$60 (n)` in both binaries, whose columns hold `$110` and `$40`, while
the engine's flows of `$10` and `$60` leave `$70` in the debit column
alone, the net row of `Assets:D` being `$70` on both sides. Among the
second are the lot that C++ carries into the amount of a balance
assignment, from its target or from the account's single lot, and the
annotation of an `apply fixed` rate. Under `apply fixed CAD $0.75`,
`Assets:A  10 CAD` beside `Assets:C  -1 FOO @ 4 CAD` completes an elided
`Equity` with `4 CAD` and `-10 CAD {=$0.75}` in both binaries, whose
columns then hold `4 CAD` and `10 CAD`, while the engine gives `-6 CAD`
and leaves the flag unset; and `Assets:Broker  = 10 AAPL {$5}` beside
`Assets:Other  -10 AAPL` completes an elided `Equity:Gold` with
`10 AAPL` and `-10 AAPL {$5}`, whose columns hold `10 AAPL` each, where
the engine makes no posting.

The comment beside costVal recorded the bracket-virtual gain only as a
departure in acceptance. It now records what the gain does to
completions, measured with both binaries. Where an elided posting
completes each pool, the gain passes from the real completion of C++ to
the bracket-virtual one in the engine: `[Assets:A]  -10 AAPL {$1} @ $5`
against an elided `[Assets:B]`, beside an elided `Assets:D`, gives
`[Assets:B]` `$50` and `Assets:D` `$-40` in C++, and `[Assets:B]` `$10`
alone in the oracle (wrong rows). When the two elided postings name one
account, its net row agrees and its columns do not: with
`Assets:C  $30`, C++ completes `[Assets:D]` with `$50` and `Assets:D`
with `$-70`, whose columns hold `$50` and `$70`, where the engine's
flows give `$10` and `$30`. The docstring of PoolResult.splitComms
records the gain, beside the two annotations above, as a gap in the
refusals of automated transactions: under `= /^Assets:D$/` with
`(Budget)  $10`, the journal of the bracket-virtual gain with an elided
`[Assets:B]` and `Assets:C  $-60 (n)` books `$20` in both binaries and
`$10` in the oracle, as the journal of the fixed rate does under
`= /Equity/` and a single lot carried into a balance assignment does
under `= /Equity:Gold/`. No positive journal has a bracket-virtual
posting with a cost or a lot.

Four departures of the tag of an inferred amount predate this commit.
Each gives wrong net rows today and passes unchanged into the new
counts, and each is now recorded with journals measured against both
binaries. C++ recovers the precision only from a cost in the plain
commodity of the amount that it tags (src/xact.cc:213), so that
`A  1 FOO @ $0.0004 (n)` against an elided `C`, which C++ completes with
an untagged `$-0.0004 (n)` that it prints as `$-0 (n)`, shows no row for
`C` in C++ and `$|-0.0004|C` in the oracle. C++ tags each posting of a
split, as above, so that the journal of `A  1 FOO @ $0.0001` and
`B  $5 (n)` shows `$-5` for `C` in C++ and `$|-5.0001|C` in the oracle.
C++ tags an amount only where the recovered precision exceeds the
precision of its commodity at that point (src/xact.cc:217), a test that
the engine makes as though that precision were 0, so that where that
precision later falls, through a `format` subdirective or an amount that
replaces a precision taken from a price directive
(src/textual_directives.cc:871-874; src/amount.cc:1839-1846), an amount
that C++ leaves untagged can be shown unrounded: after `X  $1.0000`
against an elided `Y`, `A  1 FOO @ $0.0001` against an elided `C`,
followed by `commodity $` with `format $1.00`, shows no row for `C` in
C++ and `$|-0.0001|C` in the oracle, and so does
`P 2024/01/01 FOO $0.0001` before the transaction of `C` with `X  $1.00`
after it; the oracle of the preceding commit prints the same rows for
both journals. The comment on COST_PREC tagging in balancePool records
these three beside the earlier one, under which the engine scans the
pool where C++ scans the transaction (src/xact.cc:210), and it records
that the engine omits as well the test of src/xact.cc:222-226, under
which C++ tags only an amount with digits after the point, an omission
that cannot change how an amount shows, since a whole amount shows alike
at any precision. C++ also keeps the tag on a posting that an automated
transaction generates from a tagged amount, through the product of a
multiplier, which amount_t::multiply leaves tagged, or through the value
of an expression such as `(amount)` (src/xact.cc:1533-1549;
src/amount.cc:705-753), where the engine generates the posting untagged:
under `= /^C$/` with `(Budget)  1`, `(Budget)  (1)` or
`(Budget)  (amount)`, `A  1 FOO @ $0.0004` against an elided `C` gives
`Budget` the row `$-0.0004` in both binaries and no row in the oracle. A
comment where applyAutos instantiates a template records it, and the
docstring of bumpStat names it beside the others. All but the tag of a
split can reach a column alone, where the account holds a flow of the
other sign and the net rows agree: `A  1 FOO @ $0.0004 (n)` against an
elided `C`, then `C  $1` against an elided `D`, give `C` the row `$1` on
both sides, while both binaries show zero in the credit column of `C`,
and the engine's tagged `$-0.0004`, alone in that column, counts as a
lone inferred amount; the scan of the pool, the omitted precision test
and the multiplier, with a later flow of the other sign in the account
that they reach, do the same. For gross totals the flag covers the tag
of a split and none of the others, as the docstring of Result.lotFanout
says. None of these departures changes a row of the positive corpus: the
bisimulation passes for every positive journal, the columns differ from
those of C++ only where the measurements below say, and of the five
positive journals with a cost that carries an annotation of its own,
only regress/1217 writes such a cost with decimals, in ETH, whose
weights net to zero there, so that the engine makes no completion in ETH
and C++ completes that key with a posting of zero. This commit changes
no output and leaves the net rows as they were.

The flag and the counts were measured with a scratch copy of
Ledger/Driver.lean, not committed, that prints the flag, compares
signStats with the flows of Result.gens and prints the columns of each
account. Of the probe journals of decision D29, recreated from the
planning record, the flag holds for the journals of regress/1638, 1998
and coverage-wave7-xact-edge and for the date, tag, fixation, price,
date-only, mixed-sign and date-lot probes, whose columns all differ from
those of C++ in the completed account. It does not hold for the journals
of the seven same-sign tests, baseline/opt-lot-dates, opt-lot-prices,
opt-lots, opt-lots_basis and opt-lots_basis_base, and
regress/coverage-commodity-mixed-sort and coverage-xact-fixated-perunit,
nor for the note and autosame probes, whose columns all equal those of
C++; auto.dat stays refused by the rule of the preceding commit. The
flag holds as well for the tagged split above, and for it with
`B2  $-1 (n)` beside; for `A  1 FOO @ $0.0004 (n)` and
`B  1 BAR @ $0.0001` against an elided `C`, which both binaries complete
with `$-0.0001` and `$-0.0004 (n)` and show neither as a row nor in a
column, where the engine's column would show `$0.0005`; for
`Assets:T  10 AAPL {$5} @ $50 {$1}` against an elided posting, which
both binaries complete with `$450` and `$-500 {$1}`, a split that the
gain of Phase 6 makes; and, in excess, for the tagged split followed by
`C  $-1`, whose credit column is `$6` in both binaries and in the
engine, for the tagged split after `X  $1.0000`, and for the split of
`$-50` and `$-10 (n)` and the completion beside `B  $0 (n)` above. It
does not hold for the journals of the bracket-virtual gain, the
annotated zero, the multiplier, the tag of a cost with a note of its
own, or the omitted precision test. Over the 3816 positive journals of
the snapshot the flag holds for four, regress/1217, 1638, 1998 and
coverage-wave7-xact-edge. A scan of the C++ register of the same
journals (`reg --lots --empty`) for calculated postings of both signs in
one commodity on one account of one transaction finds such a split in
1638, 1998 and coverage-wave7-xact-edge alone, with either binary. In
regress/1217 the flag is set in excess: the cost
`0.00160 ETH {334.51 EUR} [2017-06-13]` and the amount
`-0.00160 ETH {334.51 EUR} [2017-06-13]` fall under one key, and C++
completes the elided posting with `-334.51 EUR` and a posting of zero,
whose columns are the engine's. With the lone inferred rule of the net
rows applied to each column from signStats, after the account rewrites,
the columns of the engine equal those of C++ for 3811 of the 3812
positive journals without the flag, with either binary; for the
remaining one, regress/1883, the scratch comparison read the line that
its eval directive prints as a balance row, and its columns agree
without that line. For every journal that the oracle reads, 3816
positive and 333 of the 469 negative, the debit count of each account
and commodity equals its positive flows, the credit count its negative
flows, and its flowStats count the two together with its flows of zero.
With the scratch lines removed, the scratch driver's output is
byte-identical to the positive snapshot and to the negative snapshot
recorded at the preceding commit.

Verification. The locked build script exits 0 after 1225 jobs with no
warning in its log. driverpin reports DRIVERPIN OK, and git diff --stat
of Ledger/Driver.lean, README.md and AGENTS.md prints nothing. The diff
contains xact.cc:180 twice. The gate self-test is green, the
forbidden-construct scan reports none, the register gate reports CLEAN
and the citation check passes. The environment-wide axiom audit finds
3154 constants in 31 Ledger modules, against 3146 at the preceding
commit, and none outside the budget of propext, Classical.choice and
Quot.sound. The oracle's default output over the 3816 positive journals
is byte-identical to the recorded snapshot (SNAP IDENTICAL), and over
the 469 negative journals it is byte-identical by cmp to the output
recorded at the preceding commit. The bisimulation log against the C++
tree at f396a2e69 reports PASS 3816, FAIL 0, SKIP:out-of-scope 0,
SKIP:cpp-parse 0, SKIP:negative 489, SKIP:no-journal 77 and
SKIP:python 2 over 4384 test files, with both detector fixtures green,
and the unit's tail check exits 0.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0143bpV19FtoLrrR59maPRPk
Item I21 of the Ellerman analysis observes that the oracle's net rows
cannot see an error that cancels in every account it touches. A balance
assignment read as a plain amount is such an error when a later
assignment to the same account absorbs the difference: the journals
tele.dat and tele-wrong.dat of the analysis, which differ only in
`Assets:Cash  = $300` against `Assets:Cash  $300`, print the same net
rows in the oracle and in C++, while their debit and credit totals
differ. Decision D26 of the implementation plan answers the item with an
opt-in flag, so that the default output stays as it is; decision D30
fixes its name and line shapes, `--gross` with `>> dr ROW` and
`>> cr ROW`, and a refusal of the form `?? KIND reason`; decision D29
refuses gross totals where C++ completes an elided posting with postings
that its lot annotations keep apart; and decision D36 folds the
announcement into the commit that adds the flag. The definitions were in
place: Ledger/Register.lean computes debitTotal and creditTotal posting
by posting and proves gross_foot, and the preceding commit made the
engine count the flows of each column (Result.signStats) and flag the
completions that C++ splits (Result.lotFanout).

Ledger/Driver.lean now imports Ledger.Register, and `--gross` applies to
the next file only and resets after it, as the other flags do. grossRows
computes, for each account the journal touches, GenSpec.debitTotal and
GenSpec.creditTotal over the journal term, the postings of each
generator being those the engine emits, and merges the totals under the
report-time rewrites as the net rows are merged. Each total is displayed
by displayRow, the display rule of the net rows, now a definition of its
own that the net rows also call, so that a gross line is displayed by
the same code as a net row. The lone inferred rule reads the counts of
the total's own column, since under `--dc` C++ adds into a column only
the amounts of its sign, the other postings bringing a zero that the sum
ignores (src/report.h, src/value.cc and src/balance.cc at 76b9dc2b2,
cited in the docstring): after `A  2 X @ 0.2 Y` against an elided `B`,
and `B  1 Y` against an elided `Equity`, both C++ binaries show `1 Y`
for the balance of `B` and `0.4 Y` in its credit column under
`bal --dc --flat`, and so do the net row and the `>> cr` line here. The
C++ test planned for this shape lands only after this series merges, so
the docstring cites BIGINT_COST_PREC in src/amount.cc and
test/regress/1692.test, with the shape written inline. The gross lines
follow the net rows, sorted; where Result.lotFanout holds, the single
line `?? gross lot-annotated-completion` takes their place, citing
src/xact.cc:180-248 and Phase 7 with test/regress/1638.test, 1998.test
and coverage-wave7-xact-edge.test. Residual issue R9 of the plan found
that the source drafted for this unit spelled the reason with a space,
which no protocol text describes; the literal is the hyphenated one, and
Driver.lean holds it once and holds no spaced form. specFor, flowsFor
and journalTerm, which the DriverTwoSides proofs unfold, are unchanged.
The run path, the import closure of Driver, grows from 3200 to 3202
modules, Ledger.Labels and Ledger.Register, and still holds no
specification leaf and no join module.

The announcement lands here. The Driver.lean module docstring names the
flag and its lines, and its first paragraph is rewritten without
em-dashes. README.md names five flags and gains a paragraph on output
flags: the default output is byte for byte what it was, the gross lines
and their meaning are described with gross_foot cited, and the blind
spot is stated as residual issue R10 asks, since an error that moves
amounts between postings of one sign in one account leaves both columns
as they were: an error is hidden from the net rows exactly when it
cancels in every account it touches, and the gross totals of those
accounts still move unless it also cancels within their debits and
within their credits, which is the blind spot of the gross comparison
itself. The paragraph names both conditions that set Result.lotFanout:
the postings with which C++ completes an elided posting can differ in
sign, or the engine's single completion can carry the precision of a
cost, which C++ drops from the sum of several postings in one column.
In the second case, `A  1 FOO @ $0.0001` and `B  $5 (n)` against an
elided `C` give `C` a credit column of `$5` in both C++ binaries, where
the engine's tagged `$-5.0001`, alone in its column, would show in full,
and the oracle refuses that journal. The paragraph says that the refusal
is never a rejection, and that the test behind it refuses some files in
excess and misses others, as the docstring of Result.lotFanout records
with journals. The protocol paragraph of AGENTS.md describes the flag,
its lines, whose amounts are positive magnitudes, and the refusal, and
it does not say that the refusal marks exactly the files whose gross
totals the oracle does not reproduce, since it does not: over the
positive corpus the oracle refuses regress/1217 in excess, and after
`A  1 FOO @ $0.0004 (n)` against an elided `C`, then `C  $1` against an
elided `D`, it prints `>> cr $|0.0004|C` where both C++ binaries show
zero in the credit column of `C`, a miss that the docstring records.

AGENTS.md also gains a sentence that amends a binding rule, and it is
listed here for the user's approval, as residual issue R30 asks: "An
extension is admissible only when it is opt-in and leaves the default
output byte-identical, and it is announced here and in the README in the
commit that introduces it." If it is declined, removing that sentence is
the whole change, here and in the paragraph that the `--exchange` unit
rewrites; the flag meets the condition either way.

Residual issue R24 notes that the CI smoke test of this repository
exercised only the default output, while the C++ harness will depend on
`--gross` once its pin moves. The Oracle smoke test step of
.github/workflows/ci.yml now also fails when the default output holds a
line beginning `>> ` or `?? `, and runs the oracle with `--gross`,
requiring `>> dr $|100|Assets:Cash` and `>> cr $|100|Equity:Opening`; it
remains one step, so AGENTS.md describes it truly. The step's commands,
with `--no-link` added to the build, were run locally against
`nix build .#oracle` of this tree, which checks every proof: the build
succeeded and every grep matched, as the log of that scratch run shows,
and the run exited 0.

Measured against the C++ build at f396a2e69, the gross comparator of the
plan reports no difference on its 31 probe journals. GROSS-REJECTED goes
only to d29/auto.dat and crit/n5-zero-fixed.dat, which the preceding
commits refuse as out of scope. GROSS-REFUSED goes to the journals of
regress/1638, 1998 and coverage-wave7-xact-edge and to the date,
date-lot, date-only, fixation, mixed-sign, price and tag probes. The
other 16 agree, among them tele.dat, tele-wrong.dat, the journal of
regress/842, whose generated Budget:Food postings give `$50` in each
column on both sides, the lone inferred probe, and the journals of the
seven same-sign lot tests that the planning prototype refused. The 3.4.1
release gives the same verdicts. For every refused journal, both
binaries complete some account of one transaction with calculated
postings of both signs in one commodity, read from `reg --lots --empty`
with the calculated flag; `print --generated` shows the same postings,
while plain `print` reproduces the elided posting without its
completion.

Residual issue R5 asks this unit to run the gross comparison over the
positive corpus while an oracle fix can still land here. A scratch copy
of test/semantic with a draft of the harness change planned for C14 was
run against this tree: the oracle receives `--gross` before each file,
the extension lines are diverted from the net rows, each column is
compared with C++
`balance --flat --no-total --amount '(amount > 0 ? amount : 0)'` and its
negative counterpart with eval preambles stripped, refusals are checked
against a list, and a telescoping fixture is planted on the C++ side. It
reports PASS 3816 and FAIL 0 on the net rows, and GROSS:PASS 3812,
GROSS:FAIL 0 and four refusals, with 6250 debit rows and 4781 credit
rows compared in the 3776 files whose C++ columns are not empty, and the
fixture's net rows equal while its gross rows differ (exit 0). The
refusals are regress/1217, 1638, 1998 and coverage-wave7-xact-edge, 1217
in excess, as the preceding commit recorded. A list without its last
entry makes the run fail and name that file (exit 1). The refusal list
therefore holds four files, not the ten that decision D29 measured with
the prototype's key, and C14's gross-refusals.txt is to carry these
four.

Verification. The locked build script exits 0 with no warning in its
log. The protocol checker, the flag count (five flags), driverpin, the
cited-name check over README.md, AGENTS.md and Driver.lean, and the
citation check pass; for the cited-name check, the reviewed allowlist
gains the fourteen tokens of the include and display docstrings of
earlier commits that it had never been run over, the same fourteen being
unresolved at the preceding commit, five of them checked by qualified
name. README.md and AGENTS.md are register-clean, the Driver.lean module
docstring holds no em-dash, neither file names a placeholder or
`--exchange`, the import scan is clean and the closure check prints
false for every leaf and join module. The environment-wide axiom audit
finds 3159 constants in 31 Ledger modules, none outside the budget of
propext, Classical.choice and Quot.sound. The oracle's default output is
byte-identical to the recorded snapshot over the 3816 positive journals
(SNAP IDENTICAL) and, by cmp, to the preceding commit's over the 469
negative ones, the probe gate reports PROBES OK for the S40 set of 31
verdicts, and the bisimulation against the C++ tree reports PASS 3816,
FAIL 0, SKIP:out-of-scope 0, SKIP:cpp-parse 0, SKIP:negative 489,
SKIP:no-journal 77 and SKIP:python 2 over 4384 test files, with both
detector fixtures green, and the unit's tail check exits 0.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0143bpV19FtoLrrR59maPRPk
Item I18 of the Ellerman analysis found that the property-sweep
generator, Ledger/Gen.lean, writes no reversal pair, the [x//y] + [y//x]
= [0//0] of Ellerman (2014), p. 486, and no two-posting exchange between
commodities, the value collapse of his transaction 2b under an inferred
price (pp. 497-498), and that the driver's rules for dropping a zero
total cite no C++ witness. Unit S41 of the implementation plan adds both
shapes and the witnesses. Its residual issue R4 found that the diff
prepared for the unit announced nothing about reproducibility, left the
genXact docstring describing eight shapes drawn by a `below 8` roll
while its code rolled ten outcomes, listed neither new shape, and kept
calling every generated journal balanced although the exchange shape
balances only at the rate that C++ infers.

Gen gains four accounts, Assets:Rev0 to Assets:Rev3, that only the
reversal shape touches and that no automated rule matches. A reversal
pair is an explicit single-commodity entry of two or three postings
among them, followed on the same day by its reversal, which negates
every amount and takes the payee that Label.ofReversal gives a reversal,
so that each of those accounts nets to exactly zero. The exchange shape
writes two postings in different commodities, of opposite signs and
whole cents each, with no cost, lot or elided posting. C++ balances it
at the rate that Phase 3 of finalize infers (src/xact.cc:374-473 at
76b9dc2b2), and the oracle accepts it through impliedExchange, which an
earlier commit of this series confined to the conditions of that phase.
The shape roll widens from eight outcomes to ten, outcomes 8 and 9
selecting the new shapes. The code follows the plan's prototype, except
that the reversal is written through emitXact, which takes an optional
payee whose default leaves the text of every other shape as it was,
instead of through a second copy of its formatting, and that its payee
is the one Label.ofReversal gives rather than "Reversal of N".

The module docstring now says how an entry balances: in each commodity,
as written or once its elided posting is completed; at its stated cost,
when it has one; and, in the exchange shape, only at the rate that C++
infers. It lists both new shapes and makes the announcement that I18
asks for: the shape roll has had ten outcomes instead of eight since the
reversal-pair and exchange shapes were added, which changes the shape of
about four draws in five, so every seed yields a different corpus than
it did before, and a corpus generated earlier cannot be reproduced from
its seed by this revision. The genXact docstring describes the ten
outcomes and repeats that consequence. While the list of shapes was
rewritten, three of its claims that the generated text contradicts were
corrected, since the generator writes no tab separator and no posting
status flag and writes up to five postings in an entry rather than four;
the list now also names the spelling -$12.34 and calls the note what it
is, a line under the payee, and the em-dash and en-dash of the module
docstring are gone. Since a reversal pair is one draw of the roll and
two transactions, the count argument, the usage line and the header of
each generated file now speak of draws.

Ledger/Driver.lean gains comments beside its three zero tests and no
code. The test in balancesOf drops a total of exactly zero before the
report-time rewrites merge totals, as displayRow would drop it after
them. C++ forms the total of an account by addition alone
(post_t::add_to_value and account_t::total, src/post.cc:977-1014 and
src/account.cc:884-900 at 76b9dc2b2), and an addition to a balance
ignores an amount of exactly zero and never erases an entry
(balance_t::operator+=, src/balance.cc:118-133), so a commodity whose
amounts net to exactly zero has no entry or an entry of value zero, and
neither prints a row. The plan names an erasure in src/balance.cc as the
witness here, but the only call that erases an entry there, in
balance_t::operator-= (src/balance.cc:170), lies on no path that forms
the total of an account, so the comment cites the addition instead. The
display test of displayRow follows format_accounts::mark_accounts, which
displays an account only when its display total is nonzero
(src/output.cc:325), and print_amount_from_balance, which prints an
amount of a balance only when it is nonzero (src/balance.cc:561), both
through amount_t::is_zero (src/amount.cc:1374-1430). The zero test of
the lone inferred branch cites the exact comparison that is_zero makes
at the widened precision (src/amount.cc:1388-1408). Five probe journals
check these sentences against the C++ build at f396a2e69 and the 3.4.1
release: an account whose dollars cancel exactly, alone or beside euros
added before or after the cancelling postings, and an account whose
total is $-0.003 at a display precision of two, alone or beside euros.
Both binaries print the same rows as the oracle for all five. On both,
the value expression to_amount(total), which C++ refuses for a balance
of more than one entry (value_t::in_place_cast, src/value.cc:1565-1581),
fails for the account whose euros precede the cancelling dollars, which
therefore keeps a dollar entry of value zero, and yields 5.00 EUR for
the account whose dollars cancel before the euros arrive, since an
amount of exactly zero is dropped when a total becomes a balance
(src/balance.h:128-134). The option --empty cannot draw this
distinction, because report_t::fn_scrub gives an entry of zero to every
commodity of an account's visited postings that its balance lacks
(src/report.cc:1036-1081), and each binary prints $0.00 under it for
both accounts. For the total of $-0.003, an entry that is nonzero and
therefore present, each binary prints $-0.00 under --empty, the sign
showing the entry itself rather than an inserted zero, and no row
without it, so such a total is dropped only when the report is
displayed.

With seeds 1, 2, 3, 42, 1234 and 99999 at 336 draws, the generator
writes 365 to 372 transactions per corpus, among them 29 to 36 reversal
pairs and 26 to 43 exchanges, and every corpus differs from the one that
the preceding commit writes for its seed; classified by their text, 1638
of the 2016 draws changed shape. Against the C++ build the plan's
comparators report AGREE on the net rows and GROSS-AGREE on the debit
and credit columns for all six corpora, as they do against the 3.4.1
release, and the oracle neither rejects nor refuses any of them. No
reversal account prints a net row on either side, while each corpus
gives the oracle twelve `>> dr` and twelve `>> cr` lines for those
accounts. A scratch copy of the driver without the zero test of
balancesOf and that of the display branch of displayRow makes the net
comparator report DIFF on all six corpora, each time on exactly the
twelve zero rows of the reversal accounts and on nothing else; over the
six corpora of the preceding commit the same copy agrees on four and
differs on two, each on one lot account whose units net to zero by
chance. The generated corpora therefore reach the suppression of an
exact zero on every seed, where the earlier shapes reached it only by
coincidence, and, as the module docstring now says, never the
suppression of a nonzero total that shows as zero, since every total is
a whole number of cents or of units.

Verification. The locked build script exits 0 after 1225 jobs with no
warning in its log, and driverpin reports specFor, journalTerm and
flowsFor unchanged. The gates that R4 adds hold, a grep count of 0 for
"Eight shapes" or "below 8" in Gen.lean and a count of 1 for "different
corpus" there, and #print axioms shows that the new and changed
definitions of Gen depend on no axiom outside propext, Classical.choice
and Quot.sound. The forbidden-construct scan finds none, every added
line is register-clean, and the citation check, the protocol check and
the flag count (five flags) pass; the cited-name check passes over
Gen.lean and Driver.lean once its reviewed allowlist names
print_amount_from_balance, a C++ functor with no qualified name to write
instead. The oracle's default output is byte-identical to the recorded
snapshot over the 3816 positive journals (SNAP IDENTICAL) and, by cmp,
to the preceding commit's over the 469 negative ones; the probe gate
reports PROBES OK for the twelve verdicts of this unit, AGREE and
GROSS-AGREE for each corpus; and the bisimulation against the C++ tree
reports PASS 3816, FAIL 0, SKIP:out-of-scope 0, SKIP:cpp-parse 0,
SKIP:negative 489, SKIP:no-journal 77 and SKIP:python 2 over 4384 test
files, with both detector fixtures green. The unit's tail check exits 0.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0143bpV19FtoLrrR59maPRPk
Item I22 of the Ellerman analysis asks the oracle to value holdings at
market prices as C++ `bal -X` does, and its first stage is the table of
dated prices on which any such valuation rests. Unit S42 of the
implementation plan records the points that `P` directives give, with no
change to the oracle's output and no protocol flag (decision D34), and
requires that the scratch comparator which checks the table against C++
`pricedb` be shown able to fail, and be run on a scratch stage, before
the working tree is touched.

Ledger/Journal.lean gains PricePoint, an instant on the reader's day
count, a commodity and its price. The price directives write points to
St.priceLog, and priceTable resolves the log once the journal has been
read, keeping the point written last at each instant on each two
commodities in either direction, since C++ keys the prices of an edge of
its undirected graph by instant alone (history.cc:114-120, 351-369).
regress/coverage-iterators-dup-price repeats an equal price, which
`pricedb` lists once either way, so the replacement of a different or a
reversed price rests on the source. pricesAt keeps the points not after
an instant, as find_price and pricedb read the table
(history.cc:257-263, 418), with the Driver's `--now` setting that
instant to 00:00 of its date (global.cc:483-493).

priceDirective reads a directive as parse_price_directive does
(pool.cc:548-652). A time is read with its date by priceInstant, as
parse_datetime reads `Y/M/D H:M:S` or `M/D/Y H:M:S` (times.cc:421-442,
2530-2531; regress/1202, regress/1130), the rest of the time field
unread and a second of 60 taken as the next minute. A date without a
time is read by priceDay through the readers that C++ tries in turn
(times.cc:229-327, 2539-2544): `Y/M/D`, `Y/M` as the first of the month,
and `M/D` under a `Y` or `year` directive
(regress/coverage-wave2-textual), with `.` and `-` read as `/`. A year
outside 1400 to 9999 and a day that its month lacks are refused, as
boost refuses them. A directive without a date takes the instant of the
Driver's `--now` (pool.cc:568-572, times.h:127-128) or, without it, the
issue `undated-price`. A quoted symbol skips one character after its
quote, an alias names its commodity, an expression price is the issue
`price-expr` (regress/1101), any other price is read in the journal's
style without teaching it, with no defined name evaluated, since C++
reads one as a commodity (amount.cc:1665), and is reduced through the
equivalences; a zero price is kept (regress/1085), a price in its own
commodity records nothing (pool.cc:639-642, regress/1940;
history.cc:353), and the price commodity becomes primary (pool.cc:646,
commodity.cc:72-78).

Where a directive is not read as C++ reads it, the new issue
`unread-price` stands in place of its point, so that the table fails
closed: a date or a time outside the forms above; any date without a
time under an input date format, which C++ tries before its own readers
while it stops reading `.` and `-` as `/` there (times.cc:2499-2502),
and which St.dfmt does not record in full; a date of a month and a day
after `apply year`, which sets the year that C++ gives it while
defaultYear does not follow (textual_directives.cc:464-476, 543-559), or
in a journal with included files, at whose end C++ restores its year
(textual_directives.cc:424-446); a missing symbol or one whose quote is
not closed; a price that parseAmount does not read or that carries an
annotation, which C++ reads into the commodity of the price
(amount.cc:1636-1657, annotate.cc:147-271); and a price whose commodity
C++ reads otherwise. For the last, cppPriceComm reads the commodity of
the price as amount_t::parse and commodity_t::parse_symbol read it
(amount.cc:1550-1580, 1617-1659; commodity.cc:331-349, 359-381,
404-436): C++ ends a symbol written without quotes at characters that
parseAmount keeps in it, among them `!`, `&`, `*`, `+`, `/`, `:`, `<`,
`>`, `?`, `^`, `|` and `~`, reads a backslash in a symbol as an escape
and a reserved word of its expression language as no symbol, skips a tab
before a symbol, and reads no symbol after a number that a comma begins
or ends, so that it reads `P 2024/01/01 EUR 5 A!B` as a price of 5 A,
and `P 2024/01/01 EUR 5 and` and `P 2024/01/01 EUR 5, USD` as prices of
5 in no commodity. After an in-file `--now` line, which C++ reads with
its time and which the end of an included file or `end apply year`
undoes (report.h:1059-1074 at 76b9dc2b2), a directive without a date
records `undated-price`. C++ rejects the journal at most such
directives, where the issue changes no comparison; where it reads one,
as it reads `P 01/15 EUR $1.10` under no year directive in the year of
`--now`, or `P 2024/01/01 EUR 5 A!B`, the issue marks the point that the
table lacks. A line whose date follows the `P` directly, which C++ reads
as a price directive by its first character (textual.cc:329, 363-364),
records its point, while the line is otherwise skipped as an
unrecognized one as before; the tentative precision that the `P` arm
does not grant it is recorded beside it as a known departure.

The `P` arm records the point before the tentative precision rule, which
is unchanged; its comment now records that the rule finds no price where
text follows one, which both C++ binaries contradict for
`P 2024/01/01 EUR $1.2345 ; note` followed by `($1/3)`. The docstrings
of PoolResult.swapped and verifyExtended now say that balancing does not
read the primary commodities, where they said the oracle does not record
them. The docstring of priceDirective records three departures, each
with its witness, in which the table holds a point that C++ does not
record, or lacks one that it does, with no issue. The first is the price
that a `C` directive records at 1970-01-01
(textual_directives.cc:185-188), which `pricedb` hides, together with
the reduction of a price through a one-to-one equivalence, measured on a
probe journal. The second follows an `alias` subdirective that takes a
name which a posting, a price or another alias gave to a commodity of
its own: C++ retires that commodity and gives its vertex of the price
graph, with the prices on it, to the commodity that the alias names,
which stops reading the vertex that it held (pool.cc:150-173,
history.cc:336-349), while the table keeps each point under the
commodities that its directive named. The table therefore lacks points
that C++ reads for one of the two commodities through the vertex that
they share, which `pricedb` can list twice: regress/1678 shows it for a
price, and probe journals of the second review for a posting and for a
second alias. It also keeps the points recorded before the alias for the
commodity that the alias names, which C++ no longer reads: after
`P 2024/01/01 E $1`, `P 2024/01/01 EUR $3` and `commodity EUR` with
`alias E`, both C++ binaries list `EUR` at `$1` alone and value a later
posting of `10 EUR` at $10, where the table holds `EUR` at `$3`. The
third is a price read in a style other than the one in which C++ reads
it, a departure of the reader's style model that the style rule of
finalizeXact, the `D` arm and flushPending record for other amounts:
after `D $1,000.00` and a posting of `$1,5`, C++ reads
`P 2024/01/04 Z $1,500` as 1500 and the oracle as 1.5, and after a
periodic template with `1.000,50 EUR` it reads
`P 2024/01/04 Z 1,234 EUR` as 1.234 and the oracle as 1234. A
fail-closed reading was measured and set aside: an issue for every price
whose number reads differently in the two styles would stand in place of
18 points of 6 positive journals (opt-immediate, 781, 2031_a, 2031_b,
2481 and 3FE26304), 16 of which the cross-check below finds that C++
reads alike, the other two being prices of the built-in unit `s`, which
`pricedb` hides, while a record of every source of a style that the
reader does not follow reaches far beyond this unit. Result.priceIssues
promises that, apart from these departures, each price directive of a
journal that C++ reads either writes the point that C++ records or has
an entry there. Result.primary and St.primary now say that the list is
partial: it leaves out the commodity of a price expression, the price
commodity of a directive with `unread-price` and both units of a `C`
directive, the larger through parse_conversion, which gives it the flags
of the smaller (textual_directives.cc:191, amount.cc:1983-1985), all of
which C++ marks where it reads the directive. The second review also
corrected four docstrings: without `--now`, C++ records an undated price
at the instant at which the run began, which it fixes before it reads
any file (global.cc:76), so that no year directive moves it;
parse_datetime reads `.` and `-` as `/` under an input date format as
well (times.cc:429-432); priceDay states the year that C++ gives `M/D`
under no year directive and under `St.yearUnsure` separately; and the
docstring of run now says that St.yearUnsure reads its splice flag too.
The third review corrected three statements. The docstring and this
message had described the alias departure only as a point that the table
lacks; they now name both directions, and the docstring notes that the
comment at history.cc:347, that the vertex given up has no edges, does
not hold where a price was recorded on it. Of twelve probe journals run
through the comparator on both C++ binaries, the eleven with such an
alias differ in the directions described and the twelfth, without one,
matches. Result.primary and the first departure of priceDirective now
name both units of a `C` directive, where they named the smaller alone.
The two citations of the `--now` handler now name revision 76b9dc2b2 of
report.h, since the C++ branch under test inserts lines above it and
moves it to lines 1076-1091.

The comparator needed repair before it could be trusted. The comparator
recovered for this work lacked three edits that the planning record
shows were made after its last full write (the two-commodity test of its
pure subset, exact rational comparison and the decimal-comma retry), and
they were replayed from that record. Measurement then showed four ways
in which `pricedb` hides points that C++ holds, each now handled on the
C++ side or by a named classification: a zero price, listed only under
`--empty` (regress/1085 and 1703); a commodity marked NOMARKET by `N` or
the `nomarket` subdirective (iterators.cc:180, 192), compared through a
copy with those lines commented out after a check that the copy lists
every point of the journal alike; an equal price repeated later on its
day, listed once (iterators.cc:103-164), a rule now applied to both
sides; and the larger unit of a `C` directive and the built-in units of
time, which the printer reports as `equivalence` and `builtin-unit`. The
file list now includes the `pricesdb` alias, which adds six files,
regress/coverage-iterators-dup-price among them. The first review of
this unit then found that every C++ run of the comparator read the init
file of the account that ran it, which here gives
`--input-date-format %Y/%m/%d` and with it turns off the reading of `.`
and `-` as `/`; every C++ run now passes `--args-only`, as the C++ test
harness does, and over the list this changes no verdict. The second
review found that the comparator read the default listing, which prints
each price in the style of its commodity, as a period decimal first and
as a comma decimal only where that failed, so that `1,234 EUR` for a
decimal-comma EUR, 1.234 in C++, read as 1234. C++ now lists each point
through `--pricedb-format` with its quantity printed by `quantity()`, in
the style of the null commodity, and a last field that prints 1/2 in
that same style and so names the decimal mark; fields and records end at
control characters, so that a commodity that holds a blank, a bar or a
line feed stays in its field. Without `--now`, where `pricedb` lists no
point after the instant at which the run began (commodity.cc:105-111),
the comparison drops the oracle's later points, and a commodity keeps a
double quote that has no partner. Over the list the new reading changes
no verdict. The self-test reports green: MATCH on an unchanged table of
six points, of which plain `pricedb` lists four, DIFF for a changed and
for a dropped point, ISSUE-ONLY for an issue line, MATCH for a repeat on
its day and DIFF for a changed repeat; a decimal-comma price of three
decimals read as 1.234, with MATCH for that value and DIFF for 1234; the
style of the null commodity read under `--decimal-comma` and after
`D 1.000,00`; and, without `--now`, DIFF for a point after the clock
before the comparison drops it and MATCH after. The scratch printer
imports only Ledger.Journal and is generated from Driver.lean, whose
include splicing it copies verbatim; neither the comparator nor the
printer is committed.

The list holds 349 journals, 135 of them pure. The first stage run, over
the 343 files listed before the `pricesdb` alias, matched 128 pure
files, found three classified as `equivalence` and a DIFF on
regress/2031_a, whose only price is of the built-in unit `s`, hidden by
`pricedb`; the `builtin-unit` classification followed. The recorded
stage run over the 135 pure files reports 131 MATCH and 4 ISSUE-ONLY
(three `equivalence`, one `builtin-unit`) and exits 0, and so do the
stages of the three later versions. After the build, the full list
reports 208 MATCH, 135 DIFF and 6 ISSUE-ONLY, with the issues
builtin-unit 2, equivalence 5 and price-expr 4, the last all in
regress/1101; every pure file is MATCH or ISSUE-ONLY, the 135 files that
differ are recorded as the set D42, and the verdict lines are the same
for every reviewed version and for this one. Every difference is a point
that C++ derives from postings, which the next units of the plan record:
a cross-check that runs C++ on each journal with its transaction,
automated and periodic blocks commented out matches the table on all 343
unclassified files of the list, and on 335 of the 362 positive journals
with a `P` or `C` line, 26 being classified and regress/1678 differing
as its departure records, alike for every version. Two of the
differences also show a point here alone: regress/778, whose cost price
at 00:00 hides an equal later price of that day from `pricedb`, and
regress/coverage-session-leeway, whose exchange price replaces a `P`
price of the same instant. Of the 159 probe journals of the first round,
95 match; 20 are ISSUE-ONLY, 11 of them for `unread-price`; 41 are
rejected by C++, of which 37 record `unread-price` here and 4, each a
price in its own commodity, record nothing; and 3 differ: regress/1678's
departure, an exchange price that unit S44 records, and a journal whose
in-file `--now` moves the reference time of `pricedb`, which the printer
does not follow. Four probes of an in-file `--now` without the Driver's
`--now` report `undated-price`. The 357 probe journals of the second
round, 311 of the two reviewers and 46 written for this revision, give
the same verdicts on both C++ binaries: 200 MATCH, 94 ISSUE-ONLY, 45
rejected by C++ and 18 DIFF, where the version of the first round,
measured by the same comparator, differed on 72. The 54 that changed are
commodity forms that C++ reads otherwise, now `unread-price`; of the 18
that remain, 10 show the style departure, 3 the alias departure, 2 cost
prices and 1 an exchange price that units S43 and S44 record, and 2 an
in-file `--now`.

The version first reviewed replaced a point on every write, at a cost
quadratic in the number of prices. On stages of that version, of the
parent commit and of this one, a journal of 32000 price directives on
distinct days takes 69.41 s, 3.34 s and 5.00 s, and one of 16000 takes
19.32 s, 2.44 s and 3.26 s, with output identical throughout.

Residual issue R14 is resolved by the citations above: the check of the
plan counts ten added lines that cite pool.cc:540 to 579 or
history.cc:350 to 359. For R15 the reason `undated-price` is kept. It is
recorded without the Driver's `--now`, where no-terminus refuses first,
and after an in-file `--now` line, where it can fire under `--exchange`;
the drafted comment of its refusal for unit S48 now says so and leaves
the source of the terminus to that unit. The drafted comment of the new
reason `unread-price` stands beside it, and the drafted README paragraph
names it; the refusal list of unit S48 and the reason set of unit C15
must carry it, eleven reasons in all. For R26, `pricesAt` is the
flag-free helper on which the terminus graph of unit S48 rests; the
split of S48 into a helper commit and a flag commit is left to that
unit.

Verification. The locked build script exits 0 after 1225 jobs with no
warning in its log. The gate self-test is green, the forbidden-construct
scan reports none, the register gate reports CLEAN and the citation
check passes. The environment-wide axiom audit finds 3234 constants in
31 Ledger modules and none outside propext, Classical.choice and
Quot.sound, and driverpin reports DRIVERPIN OK. The oracle's default
output is byte-identical to the recorded snapshot over the 3816 positive
journals (SNAP IDENTICAL), and by cmp to the preceding commit's over the
469 negative ones, on the stage and in the tree. The bisimulation
against the C++ tree reports PASS 3816, FAIL 0, SKIP:out-of-scope 0,
SKIP:cpp-parse 0, SKIP:negative 489, SKIP:no-journal 77 and SKIP:python
2 over 4384 test files, with both detector fixtures green, and the
unit's tail check exits 0.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0143bpV19FtoLrrR59maPRPk
Item I22 of the Ellerman analysis values holdings at market prices as
C++ `bal -X` does, on the dated price table that unit S42 began with
the points of `P` directives. C++ also records a price for each cost
written on a posting of a transaction with a date: Phase 6 of
`finalize` hands every such posting to `commodity_pool_t::exchange`
(xact.cc:669-690), which records the absolute value of the cost
divided by the amount at 00:00 of the posting's date (pool.cc:390-397,
437, 488-493). Unit S43 of the implementation plan adds these points,
with no change to the oracle's output and no protocol flag (decision
D34), and asks that the change be prototyped on a scratch stage before
the working tree is touched.

Ledger/Journal.lean gains costPrice, which derives the price from the
cost as C++ holds it at that phase (PostSpec.rawCost: a per-unit `@ P`
times the amount, or a total `@@ T` signed as the amount, before any
lot gain) and from the amount as written, unreduced through the
commodity equivalences and named through the commodity aliases
(PostSpec.rawAmount). A divided price in a commodity is rounded half
away from zero at the amount's precision (PostSpec.amtPrec) plus the
display precision of the price's commodity when Phase 6 runs plus six
(pool.cc:445-468, issue #2975), as baseline/cmd-pricedb's 1.31666667
USD shows. The plan names the `@` literal as the price of a per-unit
cost; C++ divides that cost as well, so the literal stands unless it
has more places than the rounding keeps, and `1 AAPL @ $0.12345678`
records $0.123457 on both C++ binaries, which the code follows. Where
the amount is zero at its display precision the price is the cost
itself, rounded at the cost's precision before the raise of Phase 6
(PostSpec.rawCostPrec) or at its commodity's display precision,
whichever is larger. A price without a commodity is not rounded, and
an amount without a commodity records a price of the null commodity,
which `pricedb` does not list but through which `bal -X` values a
chain on both C++ binaries. The plan speaks of a real posting, while
Phase 6 exchanges the cost of a posting of any kind, and so does the
code: `(Assets:Broker)  1 AAPL @ $5`, and the same posting in brackets
or angle brackets, records $5 on both C++ binaries.

No price is recorded for a virtual cost (regress/999-a, regress/999-b,
regress/1050; PostSpec.costVirtual), for an exactly zero price, for a
fixated lot price of the amount's own, for an amount that an
`apply fixed` or `apply rate` directive annotates (St.fixatedComms, kept
in step with St.applyStack, and PostSpec.rateFixed), for a cost whose
own lot price is in the amount's commodity (regress/1217;
PostSpec.costLotComm), for a cost in the amount's commodity, or in a
periodic template, for which C++ skips the exchange phase
(xact.cc:522-822); the comment drafted for that rule stands beside it. A
fixated cost `@ =P` records its price, since Phase 6 fixates the lot
price only after the exchange (xact.cc:816-819).

The point is dated as C++ dates the posting (post.cc:143-174): by a date
that a note of the posting gives, else by the date of its transaction.
C++ reads the dates of a transaction in turn, each replacing the one
before: the date of its header, a date in the note of the header, one in
a note line before the first posting, and then one in the text of each
`apply tag` in force, the innermost first, so that the outermost
prevails (Phase 7 of parse_xact, textual_xacts.cc:1167-1188).
St.applyTags keeps those texts in step with St.applyStack, as C++ keeps
them, trimmed and wrapped in colons when they hold none
(textual_directives.cc:493-501, 527); pushApply and popApply push and
pop St.applyStack, St.fixatedComms and St.applyTags together, and no
other code changes the three, as the docstring that St.applyStack gains
says. Under `apply tag [2024/02/01] x:y`, both C++ binaries list the
price of `1 AAPL @ $5` in a transaction of 2024/01/03 at 2024/02/01, and
they still do when that directive is nested around
`apply tag [2024/03/01] z:w` and the header carries the note
`; [2024/01/09]`; `apply tag [2024/02/01]`, which C++ wraps in colons,
and `apply tag Note: [2024/02/01]` give no date. noteDateOf reads a date
in a note or in such a text as item_t::parse_tags does (item.cc:66-104),
and priceDay reads every one of these dates, as parse_date reads them,
so priceDateFields, priceDayValid and priceDay move unchanged above
finalizeXact, and priceDay's docstring gains one sentence. The note of a
transaction is the text that parse_xact reads after its payee, which
ends only at a `;` that a tab or two blanks precede
(textual_xacts.cc:1038-1074), and xactHeaderParts reads the date and the
note of a header so for the prices of costs, while the rest of the
reader keeps its own reading of the header: both C++ binaries list the
price of `1 AAPL @ $101` at 2024/01/05 under
`2024/01/05 Buy ; [2024/01/09]`, `2024/01/05 ; [2024/01/09]` and
`2024/01/05 *  ; [2024/01/09]`, and at 2024/01/09 under
`2024/01/05 Buy  ; [2024/01/09]`. Its docstring states that C++ passes
over a state mark after a code only where none precedes the code
(textual_xacts.cc:1019-1036), as the code does. A point dated by a note
or by an `apply tag` carries the issue `posting-date`, as the plan asks
for a posting's own date. An `apply tag` left open in an included file
stays in force here to the end of the journal, the scope departure that
the `apply` arm of handleDirective records, so that a later cost of the
including file is dated by it here and not in C++, with the same issue.
Where priceDay does not read such a date the issue stands alone, and
where it does not read the transaction's date the issue `unread-price`
stands in place of the point; none of the 226 transactions with a cost
in the price list is of that kind. priceDay reads no date of a month and
a day where St.yearUnsure is set, after `apply year` and in a journal
into which a file is spliced, and the docstring of that flag and the
comment at `apply year`, which named the date of a price directive
alone, now name the dates that the prices of costs read: under `Y 2024`
and `apply year 2024`, or beside an `include` line, both C++ binaries
list the price of `1 AAPL @ $5` in a transaction of `01/03` at
2024/01/03, where this reader holds the issue `unread-price` in place of
the point, and beside an `include` line they list at 2024/01/09 the
price of a posting whose note reads `[01/09]`, where this reader holds
the issue `posting-date` alone. recordCostPoints writes the points once
the transaction is balanced, since Phase 6 follows the implicit exchange
of Phase 3, choosing for each between the point, the issue and both in a
single match, and the commodity of each price becomes primary, as
`add_price` makes it (commodity.cc:72-78, commodity.h:237), whether the
table holds the point or an issue stands in its place. The plan leaves
that marking to unit S44, whose design reads the primary commodities of
recorded costs; it is made here, where the costs are recorded, so that
the docstring of Result.primary stays true, and that docstring now names
the commodity of a cost that Phase 3 computes for an implicit exchange
among what the list leaves out. The docstrings of PricePoint,
St.priceLog, St.primary, St.priceIssues, Result.prices and
Result.priceIssues, and the parenthetical remarks on St.primary in
PoolResult.swapped and verifyExtended, name the costs, and those of
St.priceLog, St.primary and Result.prices name recordCostPoints, which
writes the points and the marking.

A transaction dropped for a UUID seen before records the prices of its
costs all the same, since C++ finalizes a transaction before it looks
its UUID up and drops it (journal.cc:573-596). The date of these prices
can differ from that of the transaction seen before: C++ compares the
amounts of the two with the annotations that Phase 6 gives them
(journal.cc:547-555, 624-639), which carry the date of the exchange only
where the amount has no lot price and no lot date of its own
(xact.cc:684-692, 803-812; pool.cc:508-522), so that a sale of
`-1 AAPL {$4} @ $5`, or a purchase of `1 AAPL [2024/01/01] @ $5`,
repeated under one UUID on 2024/01/03 and on 2024/01/10 is accepted, and
`pricedb` lists AAPL at $5 on 2024/01/10 on both binaries. finalizeXact
therefore reads the postings of such a transaction and writes their
points, and nothing else, to the state before it. A duplicate with a
posting that parsePosting does not read is still dropped whole without
an error, and the issue `unread-price`, with no commodity, stands in
place of its points, since C++ can read such a posting: both binaries
read `Assets:Cash  $-5 = (abs($-5) * -1)` in the duplicate of the
purchase above, skipping the check of a duplicate's assertion
(textual_xacts.cc:787-809, regress/1771), and list AAPL at $5 on
2024/01/10, while parsePosting does not read `abs`. The comment beside
the rule records the two departures that remain there: the duplicate
teaches this reader nothing, while C++ learns from it as it reads it,
and C++ rejects a journal whose duplicate is not equivalent, which is
not checked here (fail-open), as `1 AAPL @ $5` repeated under one UUID
on two dates shows on both binaries.

The module header described the dropped transaction as one whose note
carries a UUID seen before, which is not what the reader does, and an
earlier draft of this commit kept that wording. The header now says that
uuidOf finds the UUID in the body of the transaction, and a docstring of
uuidOf records what it reads, the first `uuid:` of any posting line or
note line, in any case, with its value in lower case, against C++, which
reads the `UUID` tag of the transaction alone (journal.cc:593), from the
note of its header, its note lines before the first posting and an
`apply tag` in force, matching the name of the tag in any case
(metadata.cc:117) and keeping its value as written. The departure, which
predates this unit, leaves the rows wrong both ways, as six journals
show on both binaries: two transactions that post `$5` to
`Assets:uuid:x`, that carry `; UUID: abc` on a posting or on a note line
after one, or of which the one carries `; UUID: abc` and the other
`; UUID: ABC` on a note line before its postings, are both kept by C++
and reduced to one here, and two that carry the UUID in the note of the
header or under `apply tag UUID: abc` are reduced to one by C++ and both
kept here. The prices of their costs are the same in either reading. The
comment at the `apply` arm, which said that UUID tags never come from
`apply` and that `apply tag` gives labels only, now records the date and
the UUID that its text can give.

The docstring of costPrice records the departures known, each measured
on both C++ binaries: the precision of an amount expression and its zero
test, which `(1.5 * 1.5 AAPL) @@ $10` and `(1.25/3 FOO) @ 1 EUR` show;
the precision of a cost expression on an amount that is zero, which
`0 AAPL @@ ($100/3)` shows; the display precisions that this reader
learns otherwise than C++, at which the price is rounded and the zero
test of its amount reads; the style, alias and one-to-one `C` departures
that the reader gives every amount, which a periodic template that
teaches C++ alone a decimal comma, and `10 X @ 5 A` beside
`C 1 A = 1 B`, show; and an `apply fixed` left open in an included file,
which this reader, splicing included files into one text, keeps in force
to the end of the journal, while C++ ends it with the file, so that a
later `10 AAPL @ $50` records $50 in C++ and no price here, with no
issue in its place, as a comment at the guard of costPrice now states.
That exclusion fails open, and it is kept as it is and pinned by a probe
set: an issue raised for it in every journal into which a file is
spliced would also flag the costs that C++ excludes, and a precise one
would need the boundaries of the included files, which the spliced text
does not keep. The precision departures come from every source of
display precision that this reader omits, from the tentative precision
of a price directive, from the precision that a `D` directive locks,
from the literals of an amount expression, from the amount of a balance
assertion or assignment, and from a transaction dropped for a UUID seen
before. C++ locks the precision of a `D` directive only once it has
parsed the directive's amount with migration
(textual_directives.cc:149-166), so that a higher precision learned
earlier survives, while lockPrecision sets the directive's own decimals;
the comment at the `D` arm, which recorded that departure for the rows
and the zero test, now says that it reaches the prices of costs, and
costPrice gives two witnesses: after `Assets:Cash  $1.2345` and then
`D $1.00`, `3 AAPL @@ $100` records $33.3333333333 in C++ and
$33.33333333 here, and after `Assets:A  0.0001 AAPL` and then
`D 1.00 AAPL`, `0.001 AAPL @@ $10` records $10000 in C++ and $10 here,
where its amount is zero at the two places of the directive. The
docstring of zeroTestPrec named the omitted sources as a template, an
automated transaction, a `define`, a typed metadata value and an
`apply fixed` rate. It now gives the mechanism behind the `define` and
the typed value, which reaches further: C++ teaches a commodity the
decimals of each amount literal of a value expression that it parses
with migration (token.cc:542-555), which is every value expression but
those of a cost and of a balance assertion or assignment, parsed with
PARSE_NO_MIGRATE (textual_xacts.cc:691-694, 756-760), and that of a
posting amount, which this reader approximates. It names among them a
`Value:` tag, a line of a transaction that begins with `assert`, `check`
or `expr`, the `eval`, `expr`, `assert`, `check` and `value` directives,
the predicate of an automated transaction, and the expression
subdirectives of `account`, `tag` and `commodity`, each measured on both
binaries, and it gives a witness in rows: after a transaction of
`Assets:Cash  $10` with the line `check amount < $0.001`, C++ prints a
later `($1/3)` as `$0.333`, where this reader prints no row for it.
costPrice cites that docstring with a witness in prices,
`3 AAPL @@ $100` recording $33.333333333 in C++ and $33.333333 here
after the same transaction, and with the others it had: after
`~ Monthly` with `X  0.50 EUR` against an elided `Y`,
`3 AAPL @@ 100 EUR` records 33.33333333 EUR in C++ and 33.333333 EUR
here. Three precision departures were recorded nowhere, and each stands
beside its rule with a witness in rows measured on both binaries: the
amount of an assertion or an assignment teaches here no more than six
places, while C++ learns any number from a literal, so that after
`$1.00` and `Assets:Cash  $0 = $1.0000000` C++ prints a later `($1/3)`
as `$0.3333333`, which shows as 0.33 here; such an amount teaches here
when it is an expression, which C++ parses without migration
(textual_xacts.cc:756-760); and it teaches the commodity named as
written, where C++ names it through the commodity aliases
(pool.cc:121-127, 150-154). The scope of an `apply` left open in an
included file is recorded at the `apply` arm of handleDirective, where
`apply account Sub` left open makes C++ post a later `Assets:B  $5` to
`Assets:B` and this reader to `Sub:Assets:B`. The docstring of
Result.priceIssues names the precision and include departures among its
exceptions and the issue that an unread duplicate leaves, and that of
Result.primary names the commodities that the two leave out and excepts
the departures under which a price rounds to zero on one side alone:
after a periodic template with `X  $0.50`, `10 AAPL @@ $0.000001`
records $0.0000001 in C++, which marks `$` primary, and nothing here,
and after `Assets:A  $1.0000001` and then `D $1.00`,
`10 AAPL @@ $0.00000001` records $0.000000001 in C++ and nothing here.

The scaffolding needed three repairs. Three probe files that the unit's
verification names were missing: $D/i22/p1.dat,
probes/tmpl/template-price2.dat and probes/pdate/posting-date.dat. They
were rebuilt from the planning transcripts, the first from its heredoc
and the other two from the commands that wrote them, and each gives the
`pricedb` output that the plan records, on both binaries. The comparator
read each C++ price through `quantity()`, which prints at the precision
of the quotient, the cost's plus the amount's plus six, while C++ stores
a divided price rounded at the amount's precision plus the price
commodity's display precision plus six: after `$1.00`, `3 AAPL @@ $100`
stores 33.33333333 and printed 33.333333. It now divides the quantity
five times by 1, which adds thirty printed places and no value, and
compares a price printed with thirty places or more, a repeating decimal
such as the 100/3 of a cost without a commodity, within one unit of its
last place. Its self-test gains both cases, each with a DIFF, and is
green; over the price list at the parent commit the new reading gives
verdict lines identical to the S42 log. The scratch printer, regenerated
by impl/S43/printer/gen.py, writes a point of the null commodity with
its commodity as `""` and reports `?? null-commodity` for it, since
`pricedb` hides that commodity. The unit's probe sets gain five, which
pin the dates that an `apply tag` gives, the scope departure of one left
open in an included file, two precision departures in prices and the
rows of the `check` line and of the six UUID journals, each at the
verdict derived from the rules above and cross-checked against the C++
binary; run against the draft that the second review read, they report
its dates as a mismatch. Five more pin, at their direction, the scope of
an `apply fixed` left open in an included file and the three `D`
witnesses above, each DIFF, and the dates that St.yearUnsure withholds,
ISSUE-ONLY with no point for a note's date, beside the same dates read
in a journal with neither, which hold the points of C++; run against a
stage of the parent commit, the sets of the dates report a mismatch.

Measurements. On the stage, before the working tree was edited, the
price list of 349 journals reported 296 MATCH, 42 DIFF and 11
ISSUE-ONLY, the oracle's default output was byte-identical to the
recorded snapshot over the 3816 positive journals and to the parent
commit's over the 469 negative ones, and each later stage, the last
identical to the tree, gave the same results. In the tree,
$C3/logs/prices-S43.txt records the same counts against S42's 208 MATCH,
135 DIFF and 6 ISSUE-ONLY: 89 files moved from DIFF to MATCH and 4 to
ISSUE-ONLY, and regress/1688 moved from MATCH to ISSUE-ONLY. The 42
differences are a subset of the 135 of S42, and a classification of
every point that C++ lists and the table lacks finds each in a
transaction of its day in which the priced commodity carries no written
cost, an implicit exchange, some with a lot price, for units S44 and S45
to record ($C3/logs/prices-S43-classes.txt, 0 unexplained); the one
point that the table alone holds is the `P` price of
regress/coverage-session-leeway that an exchange price replaces in C++.
The issue counts are unchanged but for three: `posting-date` 3, the
three tests that unit C01 adds with a dated cost, whose points match C++
once the issue is set aside; `builtin-unit` 3, up from 2, for
regress/1688, whose costs on hours record a price of `h` that `pricedb`
hides and that C++ uses, valuing a later `2h` at $110; and
`null-commodity` 1, new, for regress/unrealized-null-commodity, whose
`-1000 @ 0.2 GBP` prices the null commodity. `price-expr` 4 and
`equivalence` 5 are unchanged. No journal of the price list holds an
`apply tag` whose text gives a date, and the list's log is identical,
line for line, to that of the draft that the second review read. p1.dat
matches C++ exactly, with AAPL at $90, $95, $101 at 12:30 and
$33.333333; both template probes match with MSFT at $300 alone; and the
posting-date probe is ISSUE-ONLY, the printer giving
`P 1704585600 AAPL $|101` and `?? posting-date AAPL`, as C++ records it
at 2024/01/07. The unit's probe sets report PROBES OK (35 verdicts). Of
the 357 probe journals of the second review of S42, sty08 and tpl3, two
cost prices, moved from DIFF to MATCH and every other verdict is
unchanged. Of 166 probe journals, the unit's own rule and edge probes
with those of its first review, the rules for duplicates and headers
move 7 to MATCH and leave every other verdict as it was: four headers in
which a single blank precedes the `;`, after the payee, a state mark or
the date, and the lot sale, the lot date and the overwritten instant of
a repeated UUID; each of the 20 that differ is a witness of a departure
above. Twelve more headers, run on both binaries, hold the point of C++
in every case, five with no issue and seven with `posting-date`, and the
duplicate whose assertion parsePosting does not read is ISSUE-ONLY for
`unread-price`, with the balances of C++. Of 500 probe journals, those
of the unit and of its first two reviews with the witnesses above, run
through a stage of the draft that the second review read and through the
tree, the output of each is identical but for the 28 that hold an
`apply tag` whose text gives a date, 21 distinct journals of which 7
appear twice. Of the 21, 17 now hold the points of C++ with
`posting-date`; the two in which such a tag is left open in an included
file date a later cost of the including file by it, the scope departure,
with the same issue; and two whose date this reader does not read, a
month and a day without a year and a month 13 that C++ rejects, carry
the issue alone. On 8994 scratch journals with a cost, run through a
stage of the parent commit and through the draft that the second review
read, 3370 moved from DIFF to MATCH, none moved to DIFF, 11 moved from
MATCH to ISSUE-ONLY for a price of `h` or of the null commodity, and 624
from DIFF to ISSUE-ONLY for `posting-date`, of which 623 match C++ with
the issue set aside, the other holding a note date without a year. Of
the 156 that differ, 97 hold an implicit exchange or a lot price without
a cost, 55 are journals of the review of unit S27 in which a template
teaches C++ alone the style of a cost, 2 show the zero test of an amount
expression, and 2 are the witnesses of the departures above. That run
predates the reading of `apply tag` texts, and none of the 8994 holds an
`apply tag` with a bracket, so its verdicts stand.

Verification. The locked build script exits 0 after 1225 jobs with no
warning in its log. The gate self-test is green, the forbidden-construct
scan reports none, the register gate reports CLEAN, the citation check
passes, the environment-wide axiom audit finds 3283 constants in 31
Ledger modules and none outside propext, Classical.choice and
Quot.sound, and driverpin reports DRIVERPIN OK. The oracle's default
output is byte-identical to the recorded snapshot over the 3816 positive
journals (SNAP IDENTICAL), and by cmp to the preceding commit's over the
469 negative ones. The bisimulation against the C++ tree reports PASS
3816, FAIL 0, SKIP:out-of-scope 0, SKIP:cpp-parse 0, SKIP:negative 489,
SKIP:no-journal 77 and SKIP:python 2 over 4384 test files, with both
detector fixtures green, and the unit's tail check exits 0.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0143bpV19FtoLrrR59maPRPk
Item I22 of the Ellerman analysis values holdings at market prices as
C++ `bal -X` does, on the dated price table that units S42 and S43 built
from the `P` directives and from the costs written on postings. C++ also
records a price where it balances a transaction of two commodities
through an implicit exchange. Phase 3 of `finalize` gives every
must-balance posting of the key of one entry of the real balance, `x`,
the cost of the rate `|y/x|` that the other entry implies
(xact.cc:454-465), and Phase 6 hands each such posting to
`commodity_pool_t::exchange`, which records the cost divided by the
amount at 00:00 of the posting's date (xact.cc:669-690,
pool.cc:437-493). The sides are those of the top posting's key unless
the swap of issue #1130 applies: where the commodity of `x` is already
COMMODITY_PRIMARY and that of `y` is not, C++ exchanges them, so that
the primary commodity prices the other (xact.cc:425-431). Unit S44 of
the implementation plan adds these points with the swap on the valuation
side, leaves the balancing verdict as it was (decision D22), changes no
output and adds no protocol flag (decision D34), and asks that the
change be prototyped on a scratch stage first.

Ledger/Journal.lean gains exchangePoints, which reads the candidates
with which balancePool accepted the real pool through the implicit
exchange (PoolResult.candidates, the top posting's side first) and
returns a point for each converted posting. recordCostPoints writes
those points as it writes the points of written costs: dated by a note
of the posting, of its transaction or of an `apply tag` in force, with
the issue `posting-date` where such a note dates it, and with the
commodity of the price made primary (commodity.cc:72-78), so that a
later exchange reads it. The price is the rate times the reduced amount,
divided by the amount as written, rounded half away from zero at the
precision of that amount plus the display precision of the price's
commodity plus six, as unit S43 rounds a divided cost (pool.cc:460-468).
In $SP/exch.dat, after a cost that makes `$` primary, `-7.00 GEN`
against `$20.00` prices `GEN` at $2.8571428571, the rate 20/7 at ten
places, and `€3.00` against `$-10.00` prices `€` at $3.3333333333, as
C++ lists them. Each converted posting records its own point, so that of
two postings of the key the later prevails at their instant, with its
own precision: after `$1.00`, `1.5 EUR` and `1.50 EUR` against `$-10`
price `EUR` at $3.3333333333, and in the other order at $3.333333333, on
both C++ binaries.

The sides follow C++. Where the commodity of `x` is primary and that of
`y` is not, exchangePoints swaps them, reading St.primary, which holds
the commodities that the price directives, the written costs and the
earlier implicit exchanges mark. In test/regress/1130.test, after price
directives in `USD`, `USD 210.0` against `EUR -200.0` prices `EUR` at
1.05 USD, and in a chain whose first exchange marks `$` primary, a
second that writes the dollars first prices `EUR` in `$`. Where the top
posting's key is not an entry of the balance, as for a bracket-virtual
top posting, C++ takes the two entries in the unspecified order of its
map and swaps them (balance.h:99, xact.cc:422-423). The swap then fixes
the sides where exactly one of the two commodities is primary, and
otherwise the new issue `exchange-order` stands in place of the points,
once for each of the two commodities. A small inductive, ExchangeSides,
carries the three outcomes.

No point is recorded where C++ records none (pool.cc:437, 483-493): for
a posting whose own lot price is fixated; for a cost whose lot price is
in the amount's commodity (issue #1217), which the swap can make the
cost of `x`; for an amount that is exactly zero; for a price that rounds
to zero, which marks no commodity primary either; for a periodic
template, which has no Phase 6 (xact.cc:522); and where the side `y` has
no commodity, whose rate C++ gives the commodity of `x` before it
rejects the cost (amount.cc:814-815, xact.cc:679-681). After
`100000000000 EUR` against `$-0.01`, whose price rounds to zero, both
C++ binaries price a later `$5` against `-4 GBP` as `$` at 0.8 GBP,
which the table holds. For two lots of one commodity whose must-balance
postings in it are all written in it, C++ records no price, whichever
keys it gives the amounts, since the price would be in the amount's own
commodity and the cost carries the annotation of the other lot, which
keeps Phase 6 from the lot price of issue #3232 (pool.cc:492;
xact.cc:735, 782). exchangePoints therefore returns neither a point nor
an issue for them, before any of the rules on issues below: beside
`Assets:C  = $0`, `10 AAPL {$5}` against `-5 AAPL {$10}` records no
price on both C++ binaries, and none here. A lot written in a unit that
a `C` directive reduces to that commodity is priced in it, as the source
of Phase 6 records it, so that a transaction with one, or with an amount
in that commodity that an assignment computes, is read as any other,
with an issue for it standing once: beside `C 1 KAAPL = 1000 AAPL`,
`1 KAAPL {$5000}` after `-500 AAPL {$10}` prices `KAAPL` at 500 AAPL
here, a price that neither `pricedb` nor a valuation shows on either C++
binary.

Where the amount shows as zero at the display precision of its commodity
without being zero (`amount_t::is_zero`), Phase 6 takes the cost itself
as the price (pool.cc:437) and rounds it at the larger of the cost's
precision and the display precision of its commodity (pool.cc:466,
amount.cc:882-891). The cost's precision is the rate's plus the
amount's, and the rate's is the sum of the precisions of the two entries
of the balance plus six, but no more than the display precision of the
commodity of `y` plus six (amount.cc:810-826). CppSum, the accumulator
of the real balance that impliedExchange reads, now carries beside its
single amount and each entry the precision that C++ keeps with it, and
the new cppSum builds it for impliedExchange and exchangePoints alike:
an amount added to the single amount of its key raises its precision
even when it is zero, a balance ignores an exactly zero amount, and an
entry that is zero where the single amount turns into a balance is
dropped with its precision (value.cc:600-607, 1548; amount.cc:630-632;
balance.cc:118-133; balance.h:128-134). test/regress/1789.test, whose
`-0.001 B` shows as zero under `format 1000.00 B`, prices `B` at 1 A on
both C++ binaries and here; under `format 1 EUR`, with `$` shown to four
places, `100 EUR` and `0.4 EUR` against `$-110` price `EUR` at
$0.43824701, at eight places, and three variants pin the three rules of
the accumulation, the third at eleven places.

The issue `unread-price` stands, once for each of the two commodities,
in place of the points of a transaction in which C++ can key an amount
otherwise than the engine, and so take another top posting
(xact.cc:386-393), other sides, other postings to convert (xact.cc:461)
or a lot that withholds the price (issue #1217, pool.cc:483-486). There
are three such cases, each measured on both C++ binaries. First, a
balance assignment computes the amount of a must-balance posting in a
commodity: C++ can carry into that amount the target's lot or the
account's single lot, even into an amount of zero
(textual_xacts.cc:843-866), and this reader keeps neither. After
`10 AAPL {6 EUR}` in `Assets:Broker`, `Assets:Cash  60 EUR` followed by
`Assets:Broker  = 0 AAPL` prices `AAPL` at 6 EUR, where the sides of
the engine would price `EUR` at 0.166667 AAPL, and
`Assets:Bank  $-66.00 (a)` and `Assets:Cash  60 EUR (b)` followed by
`Assets:Broker  = 10 AAPL {6 EUR}`, which computes a zero, price `$` at
0.90909091 EUR, where they would price `EUR` at $1.1. An amount without
a commodity keeps no annotation (amount.cc:1908), and where Phase 3
converts one, the issue stands for the null commodity alone, in place
of its point, whose precision this reader does not keep: after `10` in
`Assets:A`, `Assets:A  = 5` against `$10.00` prices the null commodity
at $2, through which `P 2024/01/04 X 3` values one X at $6. Such an
amount also leaves unknown the precision of the rate, which rounds the
price of an amount that shows as zero, and so where a `C` directive
reduces another commodity to the null commodity, the issue stands as
well for that commodity in place of the point of an amount in it that
shows as zero: beside `C 1 X = 1`, under `commodity X` with
`format 1 X`, the same assignment beside `Assets:B  0.001 X` against
`Assets:C  $10.00` sets the issue for the null commodity and for `X`, in
a journal that both C++ binaries read. Second, an `apply fixed` or
`apply rate` directive annotates the amount of a must-balance posting
with the fixated price of the directive (textual_xacts.cc:626-638),
which the engine does not key, and the posting that C++ takes as its top
posting, the last must-balance posting so annotated or annotated of its
own, carries no fixated price: after `10 CAD` under
`apply fixed CAD $0.75`, a top posting of `-20 XYZ [2024/01/01]` prices
`XYZ` at 0.5 CAD in C++. Where that posting carries a fixated price,
Phase 3 converts nothing (xact.cc:386-404) and no point is recorded:
both C++ binaries record no price for `$-75.00` against `100 CAD`, which
the engine reads as an exchange that prices `$` in `CAD`. A
paren-virtual posting so annotated changes nothing, since Phase 3 reads
only the postings that must balance (xact.cc:388, 461): beside
`(Memo)  5 CAD`, `10 EUR` against `$-11.00` prices `EUR` at $1.10 on
both binaries and here. Third, two or more must-balance postings of the
key of `x` include one whose annotation carries a date, a note or a
value expression. C++ converts only the postings whose annotated
commodity is that of `x`, these details included (xact.cc:461,
annotate.h:120-125), which the key here leaves out, so that it can leave
a posting of the key without a cost and record the price of another:
after `$1.00`, `7 EUR` against `$-20.00` beside the bracket-virtual
postings `1.000 EUR [2024/01/01]` and `-1.000 EUR [2024/01/01]` prices
`EUR` at $2.85714286, the rate at the precision of the `7 EUR`, where
the last posting of the key would price it here at eleven places. The
new flag PostSpec.annotDetails, which annotLoop sets for a date, a note
or a value expression met on the amount's own annotation, marks those
postings, so that one lot written in two postings without any of them,
`5 AAPL {$5}` twice against `$-50`, still records `AAPL` at $5 as C++
does. The first and third rules are wider than C++ where C++ keys the
amount as the engine does, as for an assignment to an account that holds
no lot or for two postings dated alike, and there the issue stands where
the table could hold the point.

A transaction dropped for a UUID seen before records its exchange points
as well, since C++ finalizes it before it drops it (journal.cc:573-596).
finalizeXact runs impliedExchange over its postings where no real
posting lacks an amount, since C++ skips the balance assignments of a
duplicate and leaves their postings without one
(textual_xacts.cc:787-809): `10 AAPL {$5}` against `$-50`, repeated
under one UUID on 2024/01/10, prices `AAPL` at $5 on 2024/01/03 and on
2024/01/10 on both C++ binaries and here.

The docstrings that described the table, its issues and the primary
commodities as those of the directives and the written costs now name
the costs of an implicit exchange as well: those of St.priceLog,
St.primary, St.priceIssues, recordCostPoints, Result.prices,
Result.primary and Result.priceIssues, and the bullet of the module
header on UUID duplicates. Result.primary no longer leaves out the
commodity of the cost of an implicit exchange; its fourth kind is now
the commodity of the lot price that Phase 6 records beside such a cost
for a posting whose lot price is in another commodity (issue #3232,
xact.cc:782-800), which unit S45 records; the departures that it sets
apart, under which a price rounds to zero on one side alone, are now
those of exchangePoints as well as those of costPrice, the arithmetic of
`%` among them (below); and its claim of completeness excludes the issue
`exchange-order` and amounts in hours or minutes, which can give an
implicit exchange other sides in C++ (below). Result.priceIssues states
the three cases of the issue `unread-price` above and its issues for the
null commodity and for a commodity that a `C` directive reduces to it,
says that elsewhere than for a price directive whose symbol is missing,
empty or not closed by its quote and for a transaction dropped for its
UUID an entry without a commodity names the null commodity, sets the
arithmetic of `%` apart from its claim, and states that the table holds
neither that lot price nor an issue in its place. The docstrings of
PostSpec.amtPrec, PostSpec.rawAmount, PostSpec.rateFixed and
PostSpec.fixated name exchangePoints among their readers, the first two
cppSum as well. The #1130 entry of the docstring of impliedExchange and
the docstrings of PoolResult.swapped and verifyExtended, which said that
St.primary was kept for valuation alone, now say that exchangePoints
reads it for the sides of these prices, while the balancing verdict
still does not.

The docstring of exchangePoints records the departures known, each
measured on both C++ binaries. The points inherit those of
impliedExchange and those of costPrice in display precision and style,
and in the precision of an amount expression, the approximation of
parseAmountField: after `$1.00`, `(1.5 * 1.5 EUR)` against `$-10.00`
prices `EUR` at $4.4444444444 in C++, at the precision 2 of the product
plus 2 plus 6, and at $4.444444444444444 here. Among those of
impliedExchange, a top posting in a unit that a commodity equivalence
reduces, such as the hours and minutes that C++ reduces to seconds
(session.cc:80-81), matches neither entry in C++, which then lets the
order of the entries decide: `$-50.00` against `1 h (note)` prices `$`
at 72 s in C++ and `h` at $50 here, a wrong point in a journal for which
the scratch printer reports the issue `builtin-unit`. St.primary leaves
out the units of a `C` directive, so that beside `C 1 Kb = 1024 b`,
`1024 b` against `$-1000` prices `$` at 1.024 b in C++ and `b` at
$0.976563 here, a wrong point in a journal for which the scratch printer
reports the issue `equivalence`. An `apply fixed` left open in an
included file stays in force here to the end of the journal, the scope
departure that S43 records for costs, so that `100 CAD` against
`$-75.00` after the `include` line prices `CAD` at $0.75 in C++ and
records nothing here, with no issue in its place (fail-open). C++
multiplies and divides by an amount in `%` as by a hundredth of its
quantity (amount.cc:727-733, 799-808), arithmetic that this reader does
not model. Where Phase 6 divides a cost by such an amount, the price
that C++ records is the quotient here times a hundred, rounded at the
same places: `10%` against `$-20.00` prices `%` at $2 here and at $200
in C++, through which a later `1%` values at $2.00 on both C++ binaries
(amount.cc:1324-1325). Where the amount shows as zero, C++ records the
cost at the two places more that a product by such an amount carries
(amount.cc:733): under `commodity %` with `format 1%`, `100%` and
`0.001%` against `$-110.00` price `%` at $0.001099989 here and at
$0.0010999890001 in C++, through which a later `10000000000000%` values
at $109998900.01 on both binaries. The docstring of costPrice now
records the same departure for a total cost, which unit S43 left
unstated: `10% @@ $20.00` records $200 in C++ and $2 here. `pricedb`
hides the prices of `%`, for which the scratch printer reports the issue
`builtin-unit`, and a price in `%` of another commodity is the same in
C++ and here.

The first review of this unit found two shapes in which the draft it
read recorded a point where C++ records another or none, with no issue
in its place, and the corrections above follow from it. That draft set
`unread-price` for an amount that a balance assignment computes only
among the postings that Phase 3 converts, so that the lot that C++
carries into the amount, which makes it the top posting, gave a wrong
point or an extra one: `= 0 AAPL` beside `60 EUR` priced `EUR` at
0.166667 AAPL where C++ prices `AAPL` at 6 EUR, and with `AAPL` primary
it priced `EUR` in `AAPL` where C++ records nothing, since after the
swap the lot of the cost is in the amount's commodity (issue #1217). The
zero that an assignment with a lot computes was measured afterwards and
decides the top posting as well. The draft also priced every posting of
the key, so that bracket-virtual postings with a date or a note set the
price at their own precision. Its rule for `apply fixed` read a
paren-virtual posting as well, raising an issue for a table equal to
that of C++, and named only the commodity of the top posting's side,
which the swap can leave unpriced; the rule now reads the postings that
must balance and names both commodities. The review also asked that the
docstring of exchangePoints name both sources of its candidates and the
departure in the precision of an amount expression, that its date
parameter be named xactNoteDate after the note date that it carries,
that cppEntryPrecs, which repeated the accumulation of CppSum, give way
to one accumulator, that the sides be an inductive rather than an
Except value, and that the docstrings of Result.primary and
Result.priceIssues be reflowed to the width of the file. The departure
of a top posting in hours was found while the claim of completeness of
Result.primary was checked against these rules.

The second review found five minor faults in the draft that it read,
and the corrections above follow from it. That draft named the two
commodities of an issue without checking that they differ, and applied
its rules on issues before the test of one commodity, so that two lots
of one commodity beside a balance assignment set `unread-price` twice
for `AAPL` where C++ records no price. The review proposed that the test
of one commodity return first in every case; a stage of that form
dropped the point of a lot written in the larger unit of a `C`
directive, which the draft recorded as C++ does, and so the test comes
first only where every posting of the commodity is written in it, and
elsewhere the issue that stands names the commodity once. The draft
recorded for an amount in `%` a price that C++ records at a hundred
times, with no departure stated. Result.priceIssues said that an entry
without a commodity names the null commodity elsewhere than for a UUID
duplicate, which three returns of priceDirective contradict. Some
conditions of exchangePoints cited their witnesses only in its
docstring: the side `y` without a commodity, the amount that is exactly
zero and the price that rounds to zero now cite theirs beside the rule.
The comment of annotLoop named a date and a note for annotDetails, which
a value expression sets as well. The review also asked for a comment
stating that the arm for a rate whose precision is not known can never
run, and for the coupling of its recomputed balance with the filter of
finalizeXact to be stated. The second is stated beside the rule; the
first does not hold, since the arm runs where a `C` directive reduces a
commodity to the null commodity, as the journal quoted above shows, and
the comment beside the arm, the docstring of exchangePoints and
Result.priceIssues now state that case.

The scaffolding needed one repair and gains the unit's probe sets. The
probe journal $D/i22/p2.dat, which the unit's verification names, was
missing. It was rebuilt from the heredoc that wrote it in the planning
transcripts (agent-acf3682f885dfa58d.jsonl, 2026-09-27T07:19:37),
truncated before its fourth transaction as the command that followed
there truncated it, and both C++ binaries list the four points that the
transcript records. probes-final.sh and probes_expected.py gain twenty
sets of S44, over journals under $C3/probes/s44, and
probes-final-expected.txt gains their 101 lines and no other: 35
journals MATCH, among them those quoted above; 21 are ISSUE-ONLY for the
issues that the oracle or its scratch printer reports for them, nine of
them for the rules of the first review and four for the second; two
departures, of an `apply fixed` left open in an included file and of the
precision of an amount expression, are pinned at DIFF; and printer lines
pin the two points of p2.dat that this unit adds, the issues, the point
of a lot written in the larger unit of a `C` directive, and the points
of the departures beside a `C` directive, of an amount expression, of a
top posting in hours and of amounts in `%`. Each verdict was
cross-checked against both C++ binaries, which agree on every journal.
The gate reports a mismatch when it is run against a stage of the parent
commit; against the draft that the first review read, where seven of the
new journals differ from C++ and two report an issue for a table equal
to that of C++; against the draft that the second review read, where the
two journals of two lots of one commodity report the issue twice; and
against the form of the test that the second review proposed, where the
point of the lot written in a larger unit is missing.

Measurements. On the stage, before the working tree was edited, the
price list of 349 journals reported 336 MATCH, 2 DIFF and 11 ISSUE-ONLY,
and the oracle's default output was byte-identical to the recorded
snapshot over the 3816 positive journals and to the parent commit's over
the 469 negative ones. The stages that followed the model of an amount
shown as zero and the corrections of the first and second reviews
gave the same list, line for line, and the same snapshots, and the
working tree, identical to the last stage, gives the same list
($C3/logs/prices-S44.txt) against S43's 296 MATCH, 42 DIFF and 11
ISSUE-ONLY. The 2 differences, baseline/opt-exchange and regress/3232,
are a subset of the 42 of S43, and each lacks only the lot price that
Phase 6 records beside a computed cost, of `-155 A {2 D}` against
`1000.00 E` and of `100 A {3 C}` against `-200 B`; the issue counts are
those of S43. regress/coverage-session-leeway now matches, its exchange
price replacing the `P` price of the same instant, as in C++. Two
planted defects on scratch stages show the comparator failing on what
this unit adds: without the swap, regress/1130, baseline/opt-exchange,
exch.dat and two swap probes differ, the first holding `USD` at
0.95238095 EUR where C++ holds `EUR` at 1.05 USD; without the rounding,
exch.dat, regress/1738, regress/786A3DD0, baseline/opt-exchange and four
rounding probes differ. Of the 26282 journals of S43's broad scratch
corpus that hold a transaction without `@` and with amounts in two or
more commodities, run through a stage of the parent commit and through
the stage that preceded the rule for an `apply fixed` directive, with
the seven journals that hold such a directive run again through the
stage of that rule, 1485 moved from DIFF to MATCH, 28 from DIFF to
ISSUE-ONLY (27 for `exchange-order` and 1 for `unread-price`), and 18
from MATCH to ISSUE-ONLY, for which C++ lists no point: 13 for
`exchange-order` in transactions dated after the reference time of the
comparison, and 5 for a price of the null commodity or of a built-in
unit, which `pricedb` hides. None moved to DIFF. Of the 9 that still
differed, 8 lacked only such a lot price, and
impl/review/S42/r2/p/sty01.dat is the style departure of a periodic
template, which differs at the parent commit as well. Run again through
the stage of the corrections of the first review, no journal changes
its verdict, and two keep ISSUE-ONLY with other issues:
impl/review/S21/r2-parity/p/p09-assignlot.dat, whose assignment now
sets the issue for both commodities, and
impl/review/S22/r2/probes/fanout-zero.dat, whose assignment sets
`unread-price` where `exchange-order` stood. A stage of the first form
of the third rule, which read any annotation, moved seven journals that
write one lot in two postings from MATCH to ISSUE-ONLY, among them
impl/review/S22/r2/probes/gain-split.dat, and the flag of dates and
notes returns them to MATCH. Run through the stages of the
corrections of the second review, the form that the review proposed for
the test of one commodity and the guarded form that the unit keeps, no
journal of the broad corpus changes its verdict or its issues. The six
corpora of the current Gen (impl/S41/regen) MATCH, their tables holding
62 to 79 points against 30 to 41 at the parent commit. The unit's probe
sets report PROBES OK (82 verdicts). `probes-final.sh --check-upto S44`
finds every set as expected but for 115 probe files of the sets of S20
to S29 that the scratchpad no longer holds, which it reports as missing
in 117 entries; this unit touches none of them.

Verification. The locked build script exits 0 after 1225 jobs with no
warning in its log. The gate self-test is green, the forbidden-construct
scan reports none, the register gate reports CLEAN, the citation check
passes, driverpin reports DRIVERPIN OK, and the environment-wide axiom
audit finds 3324 constants in 31 Ledger modules and none outside
propext, Classical.choice and Quot.sound. The oracle's default output is
byte-identical to the recorded snapshot over the 3816 positive journals
(SNAP IDENTICAL), and by cmp to the preceding commit's over the 469
negative ones. The bisimulation against the C++ tree reports PASS 3816,
FAIL 0, SKIP:out-of-scope 0, SKIP:cpp-parse 0, SKIP:negative 489,
SKIP:no-journal 77 and SKIP:python 2 over 4384 test files, with both
detector fixtures green, and the unit's tail check exits 0.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0143bpV19FtoLrrR59maPRPk
Item I22 of the Ellerman analysis values holdings at market prices as
C++ `bal -X` does, on the dated price table that units S42 to S44 built
from the `P` directives, the written costs and the costs of an implicit
exchange. After S44 the price list still differed from C++ `pricedb` on
two journals, test/baseline/opt-exchange.test and
test/regress/3232.test, and each lacked only the lot price that Phase 6
of `finalize` records beside a cost that Phase 3 computes (issue #3232,
xact.cc:780-800 at 76b9dc2b2). Where such a posting's amount carries a
lot price that is not fixated and is in another commodity than the cost,
and the cost carries no annotation, C++ hands the lot price to
`commodity_pool_t::exchange`, which records it as the price of the
amount's commodity at 00:00 of the posting's date and makes its
commodity primary (pool.cc:390-398, commodity.cc:72-78). Unit S45 of the
implementation plan adds these points, changes no output (decision D34)
and asks that the change be prototyped on a scratch stage first.

Ledger/Journal.lean gains PostSpec.lotPrice, the lot price per unit as
C++ keeps it in the annotation of the amount as written, which
finalizeXact sets for an amount with a commodity: a `{P}` reduced
through the commodity equivalences, as C++ reduces the price of an
annotation when it parses it (annotate.cc:187, amount.cc:1905-1906), and
a `{{T}}` so reduced and divided by the absolute quantity, a quotient
without a commodity taking that of the amount, its divisor, except on an
amount in `%` (amount.cc:799-808, 814-815), and rounded half away from
zero at the precision of the amount plus the display precision of the
quotient's commodity plus six (amount.cc:1908-1923), where
PostSpec.lotUnit keeps the exact quotient. After `1.00 EUR`,
`3 XYZ {{10 EUR}}` against `-200 CHF` prices `XYZ` at 3.33333333 EUR on
both C++ binaries and here, and `3 XYZ {{10}} @ 2 CHF` shows the lot as
`3 XYZ {3.333333 XYZ}` under `bal --lots` on both. The display precision
is read from St.precisions as it stood before the posting, while C++
reads it after the amount's literal has taught its precision to the
commodity of the amount as written (amount.cc:1836-1849), so that the
two can differ only for a quotient in that commodity, a lot price that
is never recorded: `3.00 XYZ {{10}} @ 2 CHF` keeps 3.3333333333 XYZ on
both binaries, at the two places that `3.00` gives `XYZ`, where the
field holds 3.33333333 XYZ, as a scratch stage that records such lot
prices prints for that amount converted against `-6 CHF` (impl/S45r2/p);
the docstring of the field and the comments beside the rule say so. The
precision is not read from the parser's copy in St.amt, which neither
the `P` arm of handleDirective nor the tentative branch of bumpPrecision
brings up to date: after `P 2024/01/01 FOO 1.2345 EUR` and then
`1.5 EUR`, both binaries price `XYZ` at 3.3333333 EUR, and a scratch
stage that read the copy priced it at 3.333333 EUR. The stale copy
predates this unit and also rounds the lot gain of costVal and gainVal
and the total of costPrec in parseAmountField, which gives wrong rows:
after the same two lines, `10 AAA {1.23456 EUR} @ 1.2 EUR` against an
elided posting gives it `-12.3 EUR` on both binaries and `-12 EUR` here,
at the parent commit as in this one (impl/S45/p/x-stale-prec.dat). The
comment beside the rule now records that departure, and the comments on
costPrec and costVal in parseAmountField and on the tentative branch of
bumpPrecision point to it; fixing it would change rows, which this unit
may not do, and is left to a later unit.

exchangePoints records the lot price of each converted posting right
after the point of its cost, through recordCostPoints, so that it is
dated as that point is, carries the issue `posting-date` where a note
dates it, and makes its commodity primary. It records none for a fixated
lot, for a lot price in the cost's commodity, where Phase 6 books a gain
instead (xact.cc:692-693), and where the cost carries an annotation
(xact.cc:735). The cost carries the annotation of the entry `y` of the
real balance (amount.cc:727-744): its lot price, as for two lots of one
commodity, or the date, note or value expression of the postings that
make `y`, which C++ keys (annotate.h:120-125) and the key here leaves
out. Where one posting makes `y`, its own details decide: with `CHF`
primary, `100 XYZ {3 EUR}` against `-200 CHF (note)`, or against
`-200 CHF [2024/01/01]`, prices `XYZ` at 2 CHF alone on both binaries.
Where two or more postings make `y` and one of them carries such
details, C++ keys them as one entry with those details, or apart, and
then makes no conversion, or as one plain entry where the postings that
carry the details net to exactly zero before another key enters the
balance; this reader does not follow which, and a new case of the issue
`unread-price` stands in place of the lot price, for the commodity that
it prices: `5 CHF (n)` and `-5 CHF (n)` before `100 XYZ {3 EUR}` against
`-200 CHF` price `XYZ` at 3 EUR in C++, and `-150 CHF (n)` and
`-50 CHF (n)` with `CHF` primary record no lot price. No point is
recorded beside a written cost, which the rule does not read:
`100 XYZ {3 EUR} @ 2 CHF` against `-200 CHF` prices `XYZ` at 2 CHF
alone. The exchange tests neither the sign nor the zero of the price, so
that `{0 EUR}` and `{-3 EUR}` are recorded as they stand, and a
converted posting whose amount is exactly zero records its lot price at
the date of its own posting although its cost records none, as both
binaries do.

Review found that C++ does not always record the lot price as written.
As it parses an amount, the commodity pool can resolve its lot to one
that Phase 6 computed for an earlier cost, of the same commodity, date
and fixated flag, whose price is in the commodity of the price written
and agrees with it once both are rounded at the lesser of their
precisions, where exactly one such lot exists and the lot written has
neither a note nor a value expression, which no computed lot has
(pool.cc:229-243, 274-331, the fuzzy matching that test/regress/524.test
covers); the annotation then holds the computed price, and Phase 6
records that. After `10 AAPL @@ $1,234.56` on 2024/01/05,
`-10 AAPL {$123.46} [2024/01/05]` against `€1,400.00` prices `AAPL` at
$123.456 on 2024/03/08 on both binaries, where the first draft of this
unit recorded $123.46 and no issue. Every lot that Phase 6 computes
carries a date (pool.cc:508-522, xact.cc:688-690), so that only a lot
written with a date can be so resolved, and this reader keeps no
computed lots: `unread-price` now stands in place of the lot price of a
converted posting whose own lot carries a date and a price with a
commodity. A new flag, PostSpec.annotDate, carries a date of the
amount's own annotation from annotLoop to that rule. The issue stands
also where no earlier lot exists, as for `100 XYZ {3 EUR} [2024/01/01]`
against `-200 CHF`, which both binaries price at 3 EUR (fail-closed),
while a lot with a note and no date, which no computed lot matches, is
recorded as before. The same resolution books the gain of such a lot at
the computed price, a departure in the rows that predates this unit and
is now recorded beside costVal: after `$1.00`, `10 AAPL @@ $1,234.56` on
2024/01/05 and a later `-10 AAPL {$123.46} [2024/01/05] @ $130.00`
against an elided posting give it `$1234.56` on both binaries and
`$1234.6` here.

Two departures of the table are recorded in the docstring of
exchangePoints, each measured on both C++ binaries. A lot price in the
commodity of the amount itself fails the assertion that the graph holds
no edge from a commodity to itself (history.cc:353), and C++ rejects
the journal: `100 XYZ {3 XYZ}` against `-200 CHF` is rejected by both
binaries, and so is `3 XYZ {{10}}` against it, whose quotient takes the
commodity of the amount; each is read here without that lot price
(fail-open), as a price directive in the commodity that it prices is
read. The first draft recorded a lot point for the second, in the null
commodity at 10/3. C++ divides a `{{T}}` lot price on an amount in `%`
by a hundredth of the quantity (amount.cc:799-808), the arithmetic of
`%` that S44 records for costs: `100% {{300 EUR}}` against `-200 CHF`
prices `%` at 300 EUR in C++ and at 3 EUR here, through which
`bal -X EUR` values a later `1%` at 3 EUR on both binaries and at 3 EUR
here, since this reader holds that `1%` as one unit. The first draft
said that the price here would value it at 0.03 EUR, which was wrong.

The docstrings that described the table, its issues and the primary
commodities now name these lot prices: those of the module header's UUID
bullet, PostSpec.lotUnit, PostSpec.annotDetails, PostSpec.amtPrec,
St.priceLog, St.primary, St.priceIssues, recordCostPoints,
exchangePoints, Result.prices, Result.primary and Result.priceIssues.
exchangePoints names them among the sources of St.primary that decide
the sides of a later exchange: after `100 XYZ {3 EUR}` against
`-200 CHF`, `10 EUR` against `-20 GBP` prices `GBP` at 0.5 EUR on both
binaries and here, `EUR` being primary through the lot price alone.
exchangePoints and Result.primary now say that an issue in place of all
the points of a transaction stands for its lot prices as well, while a
lot price beside a cost in place of whose point alone `unread-price`
stands is recorded and makes its commodity primary, since Phase 6
records it whatever the precision of the cost. Result.primary no longer
leaves out the commodity of a recorded lot price, its list of kinds left
out falls from five to four, and its claim of completeness drops the
exclusion of lot prices beside inferred costs. Result.priceIssues states
the two new cases of `unread-price`, gives that issue as well to the lot
price beside a cost in a transaction whose date priceDay does not read,
as under `--input-date-format %Y/%m/%d`, where `100 XYZ {3 EUR}` against
`-200 CHF` sets it for `XYZ` once for the cost and once for the lot
price, gives `posting-date` to these lot prices, and no longer says that
the table holds neither the lot price nor an issue. The paragraphs that
the unit edits are reflowed.

The scaffolding gains the unit's probe sets and corrects one stated
verdict. probes-final.sh and probes_expected.py gain the sets of S45
over 34 journals under $C3/probes/s45, and probes-final-expected.txt
gains their 85 lines, so that its section of S45 holds 17 sets and 70
verdicts, with no other change but the line of p2.dat below: 18 journals
MATCH; 12 are ISSUE-ONLY for the issues above, among them three whose
own lot carries a date (lot-dated, lot-dated-total and lot-dated-alone),
and for the printer's lines for a `C` directive and the null commodity;
the departure of `%` is pinned through the printer's builtin-unit line
for a total with a commodity and without one; the two rejected lot
prices in the amount's own commodity are pinned at CPP-REJECTS with
their points; and printer checks pin the points that this unit adds. The
comparator gives every journal the same verdict against both C++
binaries, and both reject the journals pinned at CPP-REJECTS. The design
text expected p2.dat to MATCH, but S43 sets the issue `posting-date` for
its `10 CHF @ $1.10  ; [2024/01/10]`, so that the comparator reports
ISSUE-ONLY; its line now says so, and its six points, among them `XYZ`
at 2 CHF and at 3 EUR on 2024/01/05, are pinned by a printer check and
equal those that both binaries list once the issue is set aside
(impl/S45/tools/issueblind.py, which reads with the comparator's own
functions). So that a spurious seventh point cannot pass that check,
probes_norm.py gains a check of the form `#N TEXT`, exactly N lines that
begin with TEXT, which probes_expected.py derives as COUNT, and the
check of p2.dat counts six points; on a planted output of three points
where two are expected it reports MISCOUNT. Run against the first draft
of this unit, the probe sets of S45 report a mismatch on the dated lots
and on `3 XYZ {{10}}`, and against the parent commit they report one as
well.

Measurements. On the stage, before the working tree was edited, the
price list of 349 journals reported 338 MATCH, 0 DIFF and 11 ISSUE-ONLY
against S44's 336, 2 and 11, the two differences of S44 now matching,
with the issue counts of S43 and S44 (builtin-unit 3, equivalence 5,
null-commodity 1, posting-date 3, price-expr 4); the oracle's default
output was byte-identical to the recorded snapshot over the 3816
positive journals and to the parent commit's over the 469 negative ones.
The working tree, identical to the final stage, gives the same list line
for line ($C3/logs/prices-S45.txt). Of the 26282 journals of S44's broad
scratch corpus, run through a stage of the parent commit and through the
stage of this unit, 8 moved from DIFF to MATCH, each lacking only such a
lot price, and no other journal changed its verdict or its issues; the
one that still differs is the style departure of a periodic template
that S44 names; a stage of the parent commit built afresh in review, and
the final stage, give the same result. The rules added in review change
no verdict and no issue of that corpus or of the price list, and of the
reviewers' probes only those whose converted posting carries a dated
lot, which become ISSUE-ONLY. Five planted defects on scratch stages
each turn a probe of this unit to DIFF: without the rounding of a total,
three; without the rule on the details of `y`, three; without the
exclusion of a fixated lot, one; without that of a lot price in the
cost's commodity, one, whose lot price of 2.001 CHF replaces the point
of 2 CHF; and with the parser's copy of the precisions, one. The six
corpora of the current Gen (impl/S41/regen) MATCH. The unit's probe sets
report PROBES OK (70 verdicts), and `probes-final.sh --check-upto S45`
reports the same 117 entries for missing probe files of S20 to S29 that
S44 recorded and no other mismatch.

Verification. The locked build script exits 0 after 1225 jobs with no
warning in its log. The gate self-test is green, the forbidden-construct
scan reports none, the register gate reports CLEAN, the citation check
passes, driverpin reports DRIVERPIN OK, and the environment-wide axiom
audit finds 3333 constants in 31 Ledger modules and none outside
propext, Classical.choice and Quot.sound. The oracle's default output is
byte-identical to the recorded snapshot over the 3816 positive journals
(SNAP IDENTICAL), and by cmp to the preceding commit's over the 469
negative ones. The bisimulation against the C++ tree reports PASS 3816,
FAIL 0, SKIP:out-of-scope 0, SKIP:cpp-parse 0, SKIP:negative 489,
SKIP:no-journal 77 and SKIP:python 2 over 4384 test files, with both
detector fixtures green, and the unit's tail check exits 0.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0143bpV19FtoLrrR59maPRPk
Item I22 of the Ellerman analysis values holdings at market prices as
C++ `bal -X` does, on the dated price table that units S42 to S45 built.
C++ values an amount through a value expression before it consults the
price graph: the expression of the annotation of the amount's commodity,
which only a fixated lot price precedes (annotate.cc:400-405, 417-418),
and otherwise the expression of the commodity itself
(commodity.cc:206-207). As it adds each posting of a transaction to the
journal, `extend_post` gives the posting's amount the first that it
finds among the value of its `Value` tag, the expression of its account,
the account expression of the journal, that of its commodity, the
commodity expression of the journal and the expression of the journal
(post.cc:1056-1105, journal.cc:584-585). The two probes of the plan,
design/critic-complete/vexpr.dat, which writes the lot
`10 AAPL {$150} ((200))`, and vexpr2.dat, which declares
`commodity AAPL` with `value 200`, make both C++ binaries show 2000 for
`Assets:Broker` under `bal -X $ --now 2026/03/01`, where the table's
AAPL price of $170 gives $1700; the oracle reads both, and its rows
agree with C++. Unit S46 of the implementation plan records these
expressions without changing any output, so that the valuation of S48
can refuse where C++ values otherwise, and asks that the change be
prototyped on a scratch stage first.

Ledger/Journal.lean gains PostSpec.lotExpr, set where the amount's own
annotation opens with a `(` that a second `(` follows directly
(annotate.cc:203-246), on an amount with a commodity and on the target
of a balance assignment, whose annotation C++ keeps in the amount that
it computes (`strip` is false, textual_xacts.cc:807, 855-858),
PostSpec.costExpr, set where such an expression annotates a cost with a
commodity, and two fields of St that `run` exports as
Result.valueExprComms and Result.valueExprAll. The first lists, each
once: the commodity of an amount or of a balance-assignment target that
carries a lot expression and of a fixed template amount of an automated
transaction that carries one, which C++ keeps on every posting generated
from the template (xact.cc:1545-1549); the commodity of a cost that
carries one, which C++ parses with its annotation (textual_xacts.cc:691)
and adds to the balance of the transaction in that annotated commodity
(xact.cc:325-333), so that an elided posting or the bucket's takes the
expression (xact.cc:368-369, 829-844); the commodity of a `commodity`
block with a `value` subdirective (textual_directives.cc:830-831,
852-853), under the name that the reader keeps and under the symbol that
C++ reads from the directive text (commodityDirectiveSymbol,
commodity.cc:438-455), which the block now carries, so that
`commodity N2` in test/regress/2321.test is recorded as `N2` and
`commodity "AAPL 2"` as `AAPL 2` alone; and every commodity of a
transaction with a date, its flows and the amounts and costs that it
writes, where a note of the transaction or of one of its postings, or
the text of an `apply tag` in force, holds `value:` in any case
(namesValueTag over noteText), a superset of the note lines from which
C++ gives the tag `Value` a value, whose name it reads in any case
(metadata.cc:47-51, 137-197). A written amount, a template amount and a
block are named through the commodity aliases and recorded again reduced
through the commodity equivalences where that differs (valueExprNames);
a target, which C++ parses reduced (textual_xacts.cc:757,
amount.cc:1905-1906), a cost and a flow appear in their reduced names
alone; and the null commodity is never recorded, since C++ keeps no
annotation on an amount of it (amount.cc:1908) and `extend_post` fails
an assertion where such a posting would take a value expression
(pool.cc:349): under `; Value: 200`, after `value 200` and under
`account Assets:A` with `value 200`, both binaries reject `Assets:A  10`
against `Equity  -10`, which the engine reads, a fail-open that predates
this unit, and under the tag they value `10 AAPL @ 5` against
`-5 EUR @ 10`, whose costs alone are in the null commodity. The second
is set by a `value` directive (textual_directives.cc:935-936,
1131-1132), a `value` subdirective of an `account` block
(textual_directives.cc:647-648, 739-740), an `eval` or `expr`
subdirective of an `account` block, which C++ evaluates at once in a
scope that passes each definition on to that of the report
(textual_directives.cc:677-681, scope.h:250-254), an automated
transaction a line of which holds `value:` (xact.cc:1496-1503,
1658-1667), a `define`, `def`, `expr` or `eval` directive, which C++
reads with `eval_directive` in the scope of the report
(textual_directives.cc:911-914, 1081-1094; journal.cc:718-719,
global.cc:99), an `expr` line of a transaction with a date
(textual_xacts.cc:1107-1118) and an `expr` or `eval` line of an
automated transaction (textual_xacts.cc:252-267, xact.cc:1505-1508),
which C++ evaluates as it evaluates those directives, and a check-in or
check-out line of a timelog whose note holds `value:`, since C++ gives
the transaction that it makes of the entry the note of one of the two
(timelog.cc:52-53, 138-139). Over the price list the flag is set for
test/baseline/feat-value-expr.test, whose command is `reg -V`, and
test/regress/2399.test, whose command is `reg -X EUR`, so that no
`bal -X` journal of the price list sets it, as the design text expected;
off the price list it is set for 54 positive journals of the corpus,
among them the four with `eval 1 + 1` in an `account` block,
test/regress/coverage-account-eval.test, cov4-account-eval.test,
cov6-textdir-account-eval.test and coverage-account-subdirectives.test,
and thirteen with an `expr` or `eval` line in a transaction or in an
automated transaction, such as test/regress/coverage-inline-expr.test
and cov6-xact-auto-eval.test. Each shape that the docstrings cite was
measured on both C++ binaries under `bal -X $` at 2026/03/01, most with
`P 2026/02/01 AAPL $170`, and the two binaries gave the same output for
every shape (impl/S46/cpp-X.txt, 84 journals, impl/S46/r2/cpp-X.txt, 44
runs, and impl/S46/r3/cpp-X.txt, 33 runs, three of which differ only in
the path of pool.cc that an assertion message names). The comments
beside the `define`, `expr`, `eval` and `value` directives, beside the
two `value` subdirectives, beside the `eval` and `expr` subdirectives of
an `account` block, beside the `expr` lines of a transaction, the `expr`
and `eval` lines of an automated transaction and the timelog lines say
what each records; `expr` and `eval` share one arm of handleDirective,
as C++ reads both with one call (textual_directives.cc:1091-1092).

Five choices beyond the design text were forced by measurements. First,
the design named the `Value` tag among the sources of the commodity list
without saying which commodities it reaches. A posting without the tag
takes that of its transaction (post.cc:88-94), and `extend_post` sets
the expression in place on an annotated commodity that every amount of
it shares (post.cc:1101-1102): after `10 AAPL @ $150` on 2026/01/05, a
second `10 AAPL @ $150` on that day under `; Value: 200` makes both
postings show 2000, and so does the tag on a duplicate of a UUID, which
C++ extends before it drops it (journal.cc:576-596). The record
therefore takes every commodity of the transaction, for a duplicate
those of the amounts and costs that it writes, and the flag is set in
place of commodities that are not all known where such a duplicate holds
a posting that parsePosting does not read, beside the issue
`unread-price` that already stands there, or where an automated
transaction is in force, whose postings C++ generates in the duplicate
and extends before it drops it (journal.cc:581-585): with
`P 2026/02/01 BBB $170` and a rule `= /Assets:A/` with
`(Budget)  10 BBB {$150}`, a duplicate under `; Value: 200` of a
transaction that the rule matches twice makes both binaries show 2000
for a later `10 BBB {$150}` and 4000 for `Budget`, against $1700 and
$3400 without the tag. Second, an automated transaction gives its notes
to the postings that it matches and generates, a virtual posting among
them, which applyAutos does not match: under `= /Broker/` with
`; Value: 200` and `(Budget)  1`, both binaries show 2000 for a virtual
`(Assets:Broker)  10 AAPL`; such a rule sets the flag. Third, the design
named `define` alone, while `def`, `expr` and `eval` are read by the
same `eval_directive`, and an `eval` or `expr` subdirective of an
`account` block, an `expr` line of a transaction and an `expr` or `eval`
line of an automated transaction evaluate their text in the same way:
`define market(a, d, t) = 7`, the same text after any of the other
three, `eval market(a, d, t) = 7` in the block of any account,
`expr market(a, d, t) = 7` as a line of a transaction, of a duplicate of
a UUID as well, and `eval market(a, d, t) = 7` under `= /Broker/` each
make `bal -X $` show 7 for every account; each sets the flag, whatever
it defines, while `define x = 5` and an `expr` or `eval` line of a rule
that matches nothing change nothing (conservative). Fourth, a lot
expression is read on the amount's own annotation, on that of the target
of a balance assignment and on that of a cost. `= 10 AAPL ((200))`
against an elided posting, as a virtual posting or between two bracketed
postings, and `= 10 AAPL {$150} ((200))` against an elided posting, show
2000 on both binaries, while against `$-1500`, where Phase 6 drops the
expression of the computed amount as it drops that of a written one,
`= 10 AAPL ((200))` shows $1700. An expression on a cost leaves the AAPL
holding at $1700 and reaches the posting that takes the cost: with
`P 2026/02/01 EUR $1.1`, `10 AAPL @ 140 EUR ((200))` against an elided
`Assets:Cash` makes both binaries show -280000 for it, where the table
gives $-1540, and as much for a bucket, for `@@ 1400 EUR ((200))`, for a
virtual cost and for the expression after a lot date, after a lot price
of the amount or before an assertion. A `$` cost shows nothing under
`bal -X $`, since C++ converts an amount of the commodity sought without
valuing it (amount.cc:1279-1280), while with `P 2026/02/01 $ 0.9 EUR`,
`bal -X EUR` shows -300000 for `Assets:Cash` against
`10 AAPL @ $150 ((200))`, against EUR-1350.0 without the expression; the
cost of a template posting of an automated transaction, which C++ adds
after the transaction balances (journal.cc:576-581), records nothing,
and under `= /Broker/`, `(Budget)  5 XYZ @ 3 EUR ((200))` leaves
`Budget` at $15. Fifth, a timelog entry takes the note of its check-in
line, or that of its check-out line where the check-in line has none
(timelog.cc:52-53, 138-139): with `P 2026/02/01 s $0.01`, an entry of
two hours checked in under `; Value: 200`, or checked out under it with
an account and a payee, shows 1440000 on both binaries, its bucket's
posting -1440000, against $72.00 without the note or under
`apply tag Value: 200`, which does not reach a timelog entry; a check-in
or check-out line whose note holds `value:` sets the flag.

The record is conservative in the places that the docstrings name: a lot
expression that Phase 6 drops, where the posting has a cost, written or
computed, and its amount no lot price (xact.cc:802-811, pool.cc:508-522:
$1700 for `10 AAPL ((200)) @ $150`, for `10 AAPL ((200))` against
`$-1500` and for `= 10 AAPL ((200))` against `$-1500`), or that a
fixated price precedes ($1500); a cost that no posting takes, as on a
virtual posting; a `value` directive after the postings that it would
reach (post.cc:1091-1092, $1700); the commodities of a transaction other
than those of the tagged posting; a note such as `; Value:` with no
value ($1700); an automated transaction that matches nothing, under
which neither an `expr` line nor a tagged duplicate values anything
($1700); and a timelog note that C++ does not read, where no payee
precedes it (textual_directives.cc:92-99, 121-128) or where the check-in
line has a note of its own ($72.00). A periodic template records
nothing, since C++ adds none of its postings to the journal, and both
binaries leave the holding at $1700 after a template with the tag or
with the lot expression. An in-file `--value-expr`,
`--account-value-expr` or `--commodity-value-expr` line sets nothing,
since C++ gives the journal these options before it reads any file
(session.cc:210-217, 240-262), and both binaries leave the holding at
$1700 after each. The commodity of a price directive whose price is an
expression is left out, since the issue `price-expr` already stands for
it (pool.cc:589-614, test/regress/1101.test).

Four limits are recorded in the docstrings rather than in the record. An
assignment in another expression that C++ evaluates as it reads the
journal defines a name as well: `check market(a, d, t) = 7`, or
`assert (market(a, d, t) = 7) | 1`, an assertion that holds, as a
directive, as a subdirective of an `account` block, which C++ evaluates
against each posting to the account (textual_directives.cc:656-676), or
of a `tag` block (textual_directives.cc:901-904, journal.cc:463-472), or
as a line of a transaction, or of an automated transaction that matches
a posting (xact.cc:1505-1520), `= expr 'market(a, d, t) = 7'` as the
predicate of an automated transaction, and `Foo:: market(a, d, t) = 7`
as a note of a transaction, of an `account` block or of a posting of a
periodic template, or as the text of an `apply tag`, each make
`bal -X $` show 7 for every account on both binaries, while such a block
of an account that no posting names, or such a line of a rule that
matches nothing, leaves the holding at $1700. The engine reads each of
these journals and records nothing, so that a valuation that relies on
the record fails open there; it rejects an amount expression that
assigns for its unknown name. The record can miss a smaller unit of a
commodity that it names, since C++ values an amount whose commodity has
no price through each of its larger units in turn (amount.cc:1301-1320),
and so through the expression of a larger unit, while valueExprNames
reduces a name only through the equivalences read before it is recorded:
after `commodity KB` with `value 200` and then `C 1 KB = 1024 B`, both
binaries show 200 for `(Assets:A)  1 KB`, a flow of 1024 B in the
engine, and the record names KB alone, beside the equivalence that
Result.equivs holds. C++ parses each recorded expression as it reads it
and rejects one that it cannot read, `((x--))`, `value x--` in a
`commodity` block and `; Value: x--` on both binaries, while the engine
reads each such journal, a fail-open that predates this unit. And the
engine does not read the text of an `expr` or `eval` directive as it
reads that of `define`, a departure that predates this unit and fails
closed, now recorded at their arm of handleDirective: after
`expr x = $5`, both binaries read `Assets:A  (x * 2)` against `Equity`
as $10, as after `define x = $5`, and the engine rejects the journal for
the unknown variable `x`.

The comments and docstrings now also record four departures that predate
this unit, in each of which C++ rejects a journal that the engine reads
(fail-open). An assertion that fails, as `assert market(a, d, t) = 7`
does as a directive, as a line of a transaction or of a rule that
matches, and in an `account` or `tag` block, since a definition yields
null (op.cc:250-268), makes C++ reject the journal
(textual_directives.cc:922-924, textual_xacts.cc:1119-1121,
xact.cc:1509-1511, journal.cc:477-479). An automated transaction has no
`note` or `format` line in C++, which reads one as a template posting to
an account of that name (textual_xacts.cc:268-275), while the engine
skips it, as the preceding commit did under a comment that called such
lines metadata: under `= /Broker/` with `(Budget)  1`, both binaries
reject `note something here` and `format %(account)`, which have no
amount (xact.cc:1529-1531), and `note  1`, whose posting to `note`
leaves the transaction unbalanced. A `value` directive or the `value`
subdirective of an account reaches a posting in the null commodity as
the tag does (pool.cc:349), and a second value expression on an amount
is rejected as more than one (annotate.cc:213-214).

The scaffolding changes in five places. The two probes of the plan were
missing from the scratchpad, among the design files removed before
2026-10-02; each is the only write of its file in the planning
transcripts, one heredoc in agent-ac28a2879beda6040.jsonl at
2026-09-27T10:21:33, whose text a later `cat` in
agent-a5fa14c68a435a880.jsonl prints again, and both binaries print 2000
for each as the transcripts record; impl/recovered-design-files.md gains
their row. The scratch printer $C3/prices_cmp.lean is generated by
impl/S46/printer/gen.py, S43's generator with two more lines of output
after the issues: `?? value-expression COMM` for each commodity of
Result.valueExprComms and a bare `?? value-expression` where
Result.valueExprAll holds. probes-final.sh and probes_expected.py gain
thirteen sets of S46 over 117 journals under $C3/probes/s46: 38 journals
ISSUE-ONLY with the commodities that the docstrings state, among them
six annotated assignment targets (assign-*, one under an equivalence and
one against `$-1500` beside its `unread-price` issues), the quoted block
name recorded once (comm-quoted), the block of a larger unit, recorded
as KB alone where the `C` directive follows it and as KB and B where it
precedes it, beside the equivalence (comm-larger-*), the costs in the
null commodity, recorded without it (null-cost-tag), and nine costs that
carry an expression (cost-*, `$` for cost-expr and EUR for the others,
cost-virtual-post among them, whose cost no posting takes); 30 with the
bare flag, among them the `eval` and `expr` subdirectives of an
`account` block (acct-*), `expr` and `eval` lines of a transaction and
of an automated transaction (gap-expr-market, gap-expr-tag and
gap-auto-expr, named for the limit that they pinned before the second
review round, xact-expr-dup, auto-eval and auto-expr-nomatch), two
duplicates under an automated transaction, beside the commodity ZZZ that
they write (dup-auto, dup-auto-disabled), and seven timelog journals,
beside the issue `builtin-unit s` (timelog-*); the duplicate with an
unread posting ISSUE-ONLY with `unread-price` and the bare flag; 30 for
the shapes that record nothing, 28 MATCH and two timelog journals
ISSUE-ONLY with `builtin-unit s` alone, among them the fourteen shapes
of the assignment limit (gap-*, two `check` lines and five assertions
that hold among them), an unannotated target, a `check` subdirective of
a `tag` block, the cost of a template posting of an automated
transaction, a cost in the null commodity and a duplicate without the
tag; and the net rows of all 117: 99 AGREE, auto-virtual DIFF at the
departure of applyAutos, expr-define ORACLE-REJECTS at the departure
recorded at the `expr` and `eval` directives, and CPP-REJECTS for the
sixteen journals that C++ rejects and the engine reads, bad-lot,
bad-comm, bad-tag, space-paren, whose `( (200))` C++ reads as a note
before it rejects the second `)`, null-flow-tag, which fails the
assertion of pool.cc:349, and eleven for the four departures above, an
assertion that fails in each of five places (assert-fail-*),
auto-note-line, auto-format-line, auto-note-amount, null-value-dir,
null-account-value and lot-two-expr. probes-final-expected.txt gains the
229 lines that probes_expected.py derives for these sets, inserted after
the two sets of S46, with no other change: regenerating the whole file
would rewrite the verdicts that S40 pinned by hand and those of files of
earlier units that are missing now. The scratchpad was cleared on
2026-10-13 and 2026-10-14, after the first state of this unit, and the
parts of the work of S42 to S46 that this unit runs were rebuilt from
the session transcripts and checked against what they record:
prices_cmp.py from two independent full prints of its 483 lines, the
generators of the printer from S42's full print and the recorded edits
of S43 and S46 (the printer of S45 equal to a full print, and the diff
from it to that of S46 equal to the recorded diff), the price list by
`prices_cmp.py --make-list` (349 journals), the 54 first probe journals
from their heredocs (both binaries' output over them equal to the
record), their 109 expected lines equal to the recorded output of
probes_expected.py, and the negative-corpus snapshot of the preceding
commit on a stage of it, byte-identical to the one that S37 recorded;
impl/recovered-design-files.md gains a row for each. The rebuilt
scaffolding gives, on a stage of the preceding commit, the price list
that S45 recorded, 338 MATCH and 11 ISSUE-ONLY, and for the first state
of this unit 329 MATCH, 20 ISSUE-ONLY and PROBES OK (108 verdicts), as
recorded.

Measurements. Each rule was compiled and probed on a scratch stage
before the working tree was built. The third review round adds no rule;
its stage, consol2/stages/S46r3, holds the Journal.lean of the working
tree, and its printer and oracle give the tree's output over the
journals and the price list below (impl/S46/r3/stage-cmp.log). On a
stage of the preceding commit, the oracle's default output under
`--now 2026/08/26` over the 547 journals that the probe sets name and
that exist, or that impl/S46/p, impl/S46/fix/p, impl/S46/r2/p and
impl/S46/r3/p hold, is byte-identical to this unit's, and over those
journals and the price list, 896 runs, the scratch printer of S45 prints
there what the printer of this unit prints here, but for 210 lines
`?? value-expression` (impl/S46/r3/allprobes). Over the 3816 positive
journals the record names a commodity or sets the flag for 71, nine of
them on the price list, and sets the flag for 56; the second review
round changed the record of thirteen of them, each by the flag of an
`expr` or `eval` line, and of no other
(impl/S46/r2/logs/census-r2-summary.json), and the third changed the
record of none (impl/S46/r3/logs/census-r3.json). The price list of 349
journals reports 329 MATCH, 0 DIFF and 20 ISSUE-ONLY against S45's 338,
0 and 11, with the same counts for every other issue and 14 issue lines
`value-expression`: nine journals move from MATCH to ISSUE-ONLY,
test/baseline/dir-commodity-value.test (`$` and `USD`, whose block
carries `alias FOO`), feat-value-expr.test (`$` and the bare flag),
opt-value-expr.test (`USD`, under `; VALUE:: 30 DM`),
test/regress/2321.test (`N2`, `N3` and `N4`), 2399.test (the bare flag,
from its automated transaction), cov6-post-value-tag.test (`FOO` and
`BAR`), cov9-annotate-value-expr.test (`AAPL`),
coverage-wave2-annotate.test (`$`) and coverage-wave8-annotate.test
(`GOOG`); the working tree gives the list line for line as the first
state of this unit did ($C3/logs/prices-S46.txt). Twenty-two planted
defects, each on a scratch stage of the final code, turn probes of this
unit (impl/S46/r3/planted); the twenty of the second review round turn
the probes that they turned there (impl/S46/r2/planted), and one of them
the two blocks of a larger unit as well: without the lot expressions of
amounts and targets, twelve (vexpr.dat, five lot probes and the six
targets); without the `value` subdirective of a commodity, five
(vexpr2.dat, comm-after, comm-quoted and comm-larger-*, which keep the
equivalence alone); without the flag, the thirty of its sets and the
duplicate; without the test of a header note, of the lines of a
transaction or of an `apply tag`, one, fourteen and one; without the
record of a duplicate, three (dup-shared, dup-auto and
dup-auto-disabled); with a lot expression read after a cost as well,
eleven (the nine costs, cost-null and cost-auto); without the target in
finalizeXact, or its flag in parseAmountField, six each (assign-*);
without the `eval` and `expr` subdirectives of an `account` block, three
(acct-*); with the symbol read from the block's name, one (comm-quoted);
with the null commodity kept, one (null-cost-tag); and, for the rules
that the second review round adds, without the cost in finalizeXact or
its flag in annotLoop, nine each (cost-*); without the flag of a
check-in line or of a check-out line, four and three (timelog-*);
without the flag of a duplicate under an automated transaction, two
(dup-auto and dup-auto-disabled); without the flag of an `expr` line of
a transaction, three (gap-expr-market, gap-expr-tag and xact-expr-dup);
and without that of an `expr` or `eval` line of an automated
transaction, three (gap-auto-expr, auto-eval and auto-expr-nomatch);
and, for the pins of the third review round, with the record reduced
through the final equivalences at the end of `run`, one
(comm-larger-after, which then names B as well), and with an `assert`
directive setting the flag, one (gap-assert-dir). The state of the
working tree before the second review round fails the probe sets of S46
on the 24 probes of the rules that round adds.

Verification. The locked build script exits 0 after 1225 jobs with no
warning in its log. The gate self-test is green, the forbidden-construct
scan reports none, the register gate reports CLEAN, the citation check
passes, driverpin reports DRIVERPIN OK, and the environment-wide axiom
audit finds 3354 constants in 31 Ledger modules and none outside
propext, Classical.choice and Quot.sound. The oracle's default output is
byte-identical to the recorded snapshot over the 3816 positive journals
(SNAP IDENTICAL), and by cmp to the preceding commit's over the 469
negative ones. The bisimulation against the C++ tree reports PASS 3816,
FAIL 0, SKIP:out-of-scope 0, SKIP:cpp-parse 0, SKIP:negative 489,
SKIP:no-journal 77 and SKIP:python 2 over 4384 test files, with both
detector fixtures green, the probe sets of S46 report PROBES OK (220
verdicts), and the unit's tail check exits 0.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0143bpV19FtoLrrR59maPRPk
Item I22 of the Ellerman analysis asks for the oracle's balances valued
in one commodity, computed by a per-account-name netValue that is proved
equal to priceDot composed with flow, so that the valued rows rest on a
theorem rather than on an informal correspondence between two formulas.
Ellerman values property at a price vector on p. 498: "Multiplying the
physical quantities by their price and adding up ... yields the value of
the property vectors". Ledger/PriceVector.lean states that valuation as
priceDot, the scalar product of a price vector with the ledger-wide
property vector, and as valueHom, the value of one account name's
property vector, but both are noncomputable additive homomorphisms on
PacioliGroup, which the interpreted oracle cannot evaluate, and the
module docstring of PriceVector records that an executable market
valuation needs a computable twin with an agreement theorem. This commit
adds the twin and the agreement. The opt-in flag that prints valued rows
is left to a later commit, so neither the oracle's output nor its
protocol changes here.

Ledger/Oracle.lean, an executable module on the oracle's run path, gains
two definitions in namespace GenSpec after netFlow_trial_balance, and no
import. netValue sums, over a list of accounts, each account's net flow
netFlow times the price of its commodity, with plain functions and exact
rationals as netFlow computes, and netValueAt is netValue over the
accounts of the list that bear one name. The definitions are those of
the implementation plan's prototype, recovered from the planning
transcripts. The prototype's docstring of netValueAt read "The valued
row the driver prints for one account name", which is false while the
driver prints no valued row, so both docstrings are rewritten. Both
state the agreement under the conditions below, on the list of accounts
and on the flows of the generators, and name the module that proves it.
That of netValue also says that an account listed twice is counted twice
and an account not listed is not counted, and that the flows flowsFor of
Driver meet the condition on the flows, by coe_flowsFin.

Ledger/Market.lean imports Ledger.Oracle and Ledger.PriceVector.
netValue_eq_priceDot proves that netValue is priceDot of the flow
denotation flow of the term's class in FreeHom, and
netValueAt_eq_valueHom proves that netValueAt is valueHom of the same
flow, both for a list of accounts that has no duplicates and holds every
account at which the term's flow is nonzero, and for flows of the
generators that are vectors of PacioliGroup read pointwise. The proofs
rewrite with priceDot_apply or valueHom_apply and flow_mk, replace
netFlow by flowAux through netFlow_eq_flowAux, and turn the finitely
supported sum into the list sum with Finsupp.sum_of_support_subset and
List.sum_toFinset; netValueAt_eq_valueHom also uses the private lemma
sum_filter_map_ite, which the prototype proved by induction and which is
proved here from Mathlib's List.sum_map_ite. The prototype wrote the
flows of the generators as (fun t a => flowsF t a); both theorems here
write them as (fun t => (flowsF t : Account → ℚ)), the form of
netFlow_eq_flowAux. The two forms are definitionally equal, so each
theorem stated in the prototype's form is proved by the corresponding
new theorem as a term. Scratch files that apply each theorem, in each
form, to the flows flowsFor arr that Driver computes find no difference
between the forms. After rw [← coe_flowsFin], exact proves the
statement for flowsFor arr in all four cases, as DriverTwoSides does
with netFlow_eq_flowAux, and rw fails in all four with "Did not find an
occurrence of the pattern", because the rewritten flows bind
Fin arr.size where the theorems bind (specFor arr).Gen; once change
restates that binder, rw succeeds in all four.

Market is a join module, the kind that AGENTS.md describes and the
implementation plan names: it imports the executable module Oracle
together with the specification leaf PriceVector only to state proofs,
it holds only the two theorems and the lemma that they use, and no
module on the oracle's run path, the import closure of Driver, imports
it; at this commit no module imports it at all. The lakefile's glob over
Ledger builds it, and its entries in the Ledger.lean index, the README
and AGENTS.md are left to the plan's final inventory unit, as they were
for GrossLedger. The agreement sits in a module of its own rather than
in Oracle.lean so that PriceVector stays off the run path, as decision
D12 of the plan requires: a file importing Ledger.Oracle loads 3197
modules, and 3314 with Ledger.PriceVector added, and a file importing
Ledger.Driver loads 3202 modules, and 3319 with Ledger.PriceVector
added, the 117 more being Ledger.PriceVector, Ledger.TimedPrices,
Ledger.Derived and 114 modules outside Ledger, among them
Mathlib.LinearAlgebra.Finsupp.LinearCombination.

Modules of this kind rest on the amendment to AGENTS.md that the
DriverTwoSides commit submitted for the user's approval. If the user
declines the category, Market is dropped and netValue and netValueAt
stay in Oracle: Ledger/Market.lean is deleted, and the sentences of the
two docstrings that state and cite the agreement are removed. No module
imports Market, and no other file of the repository names the module,
netValue, netValueAt or the two theorems.

Verification. The locked build script, lake build with warnings as
errors, exits 0 after 1226 jobs, one more than the 1225 of the preceding
commit's tree, with no warning in its log. #print axioms lists propext,
Classical.choice and Quot.sound for netValue_eq_priceDot and
netValueAt_eq_valueHom (2 records within the budget). The
environment-wide audit finds 3361 constants in 32 Ledger modules and
none outside the budget, against 3354 in 31 for the preceding commit's
tree, measured by building that tree with this change set aside; the
seven new constants are netValue and netValueAt, the two theorems, the
private lemma, and the equation lemmas of the two definitions, which
Lean generates in Market, where rw first unfolds them. The audit's
self-test, with a planted native_decide theorem, fails as it should. In
scratch files checked against the built tree, #eval of netValueAt on the
Ellerman journal of Oracle.lean at the prices of p. 498 prints 14500 for
Assets, and decide +kernel proves the rows of Assets, Liabilities and
Equity to be 14500, -9200 and -5300, equal to the hand-written value
that Ellerman.ending_value uses, and netValue over all nine accounts to
be 0; on the demo journal at price 1 it proves 2850 for Assets:Checking
and 0 in total. As negative controls, the Assets row stated as 14501
fails with "Tactic `decide` failed" and its negation is proved by decide
+kernel; the Assets row is 24700 when Assets in dollars is listed twice
and 11000 when Assets in widgets is left out, the counting that the
docstring of netValue describes; and copies of netValue_eq_priceDot
without the hypothesis of no duplicates or without the support
hypothesis fail to elaborate. Every declaration name that the docstrings
of the two files cite resolves to the intended constant; the one
unresolved token is the module name PriceVector, which the name checker
allowlists. The import-direction scan is clean, and the import closure
of Ledger.Driver holds 3202 modules, as for the preceding commit's tree,
with Ledger.Market, Ledger.PriceVector and
Mathlib.LinearAlgebra.Finsupp.LinearCombination not among them. The scan
of added Lean lines finds no forbidden construct, the register check of
added lines is clean, the gate self-test is green, and every C++ test
cited by the tree exists at 76b9dc2b2. The oracle's default output over
the 3816 positive journals of the snapshot is byte-identical to the
recorded snapshot, and over the 469 negative journals it is
byte-identical by cmp to the output recorded at the preceding commit.
The bisimulation log against the C++ tree at f396a2e69 reports PASS
3816, FAIL 0, SKIP:negative 489, SKIP:no-journal 77 and SKIP:python 2,
with both detector fixtures green; the unit's tail check exits 0.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0143bpV19FtoLrrR59maPRPk
Item I22 of the Ellerman analysis asks the oracle to value its balances
in one commodity at dated prices, as C++ `bal -X COMM` values them, so
that the price table of the preceding commits is compared with C++
through the balances that it values. Decision D30 of the implementation
plan names the flag `--exchange COMM`, its rows `>> value ROW` and its
refusal `?? exchange REASON`; decision D31 admits a commodity whose only
path of prices to the target is a single chain, valued at the product of
the prices along it; decision D36 folds the announcement into the commit
that adds the flag. The definitions were in place: the dated price table
and its records (Result.prices, Result.priceIssues,
Result.valueExprComms, Result.valueExprAll), and netValueAt in
Ledger/Oracle.lean with netValueAt_eq_valueHom in the Market join
module.

Ledger/Driver.lean reads `--exchange COMM` for the next file only, as it
reads the other flags. exchangeRows takes the points of the table that
are not after 00:00 of the Driver's `--now` date (pricesAt) and keeps,
for each two commodities, the latest of them (priceEdges), as C++ keeps
one undirected edge per pair and reads its latest price not after the
terminus (history.cc:114-120 and 257-263 at 76b9dc2b2). A commodity held
in a nonzero account total is valued through the path that a
breadth-first search finds when every hop of it is a bridge, so that the
path is the only one, at the product of the hop rates, each inverted
against the direction of its price (history.cc:503-617,
amount.cc:705-751 and 909-917). For each account after the report-time
rewrites, the line `>> value COMM|AMOUNT|ACCOUNT` carries the sum of
netValueAt over the account names that the rewrites merge into it,
displayed by displayRow as a net row is, after the net and gross lines,
sorted. A lone inferred amount in the target keeps its recovered
precision where nothing else is added to it, as C++ copies an amount of
the target unvalued and drops that mark from a sum (amount.cc:1279-1280
and 634-642): after `A  3 X @ 0.25 Y` against an elided `B`, both C++
binaries show `Y-0.75` for `B` and `Y1` for `A` under `-X Y`, and so do
the value rows. A holding valued at exactly zero adds nothing to the
amount and leaves the mark, since C++ adds no amount of exactly zero to
a balance (balance.cc:122-123), which the comment beside that test
cites. specFor, flowsFor and journalTerm are unchanged.

Where the oracle cannot value a file as C++ does, one line
`?? exchange REASON`, with the commodity concerned where there is one,
replaces the value lines; the file is not rejected, and its net rows
still print. The seventeen reasons, tested in this order, each carry
their witness in a comment beside their branch: no-terminus (no Driver
`--now`, or a journal line that sets `--now`, which moves C++'s
terminus, report.h:1065 and 1070); price-expr, unread-price and
exchange-order, the price issues of those names; value-expression;
posting-date, the price issue of that name; fixated-lot; equivalence,
for any `C` directive; commodity-alias; price-precision; null-commodity,
since C++ never values an amount without a commodity (amount.cc:1263);
percent, since C++ multiplies by an amount in `%` as by a hundredth
(amount.cc:727-740); time-unit; no-price-path; alternative-price-path;
zero-inverse-price; and rounds-to-zero. C++ leaves a price of zero at
zero where it inverts it (amount.cc:909-917): with
`P 2024/01/01 EUR $0.00` and `Assets:Checking  $100`, `bal -X EUR` shows
nothing on both C++ binaries, and test/regress/1085.test crosses the
same price in its direction.

Review of a first draft measured three shapes for which the draft
printed value rows that departed from C++, and each now has a reason.
price-precision: C++ grants the commodity of a price directive's price
the decimals of the price where its precision is 0 and it is neither
known nor locked (pool.cc:628-634), the source of the directive becoming
known (pool.cc:647), while the reader grants it only to a commodity
without a recorded precision. After `Assets:Cash  $1000`,
`3 AAPL @ $170` and `P 2024/02/01 AAPL $170.25`, `bal -X $` shows
$510.75 for the AAPL account on both C++ binaries, where the draft's
value row was $511; after `P 2024/01/01 EUR $1.2345` and
`P 2024/01/01 FOO 1.25 EUR`, `bal -X EUR` shows `EUR12` for 10 FOO,
where it was 12.5; and a cost that Phase 6 rounds at the precision
records $0.0016344302 in C++ and $0.001634 in the table. The reason is
conservative: it fires at any directive at which the reader's grant
differs from the one that C++ makes from the precision recorded before
the directive, for any commodity, but for the larger unit of a `C`
directive, whose file `equivalence` refuses first; a precision that the
reader learns otherwise than C++ before the directive is an inherited
departure, below. rounds-to-zero: C++ tests a value exactly where the
precision that the value carries is not above that of the target
(amount.cc:1374-1429), and under `--flat` shows a parent none of whose
children it shows (output.cc:312-330), so that after
`Assets:Cash  $10.00`, `P 2024/01/01 $ 150 JPY` and
`Assets:Yen  0.5 JPY` it shows `$0.00` for `Assets:Yen`, and after
`P 2024/01/01 AAPL $155.00` and two expenses of $50.00 and $35.00
`bal --flat -X AAPL` shows `AAPL1` for `Expenses`, rows that the draft
left out; the reason fires where an account's value is not zero but
shows as zero, in excess where C++ hides the account as well. time-unit:
under `-X` C++ does not convert what it shows to a larger unit
(report.cc:815-821), while displayRow does, so that for
`Assets:Time  2h`, `bal -X s` shows `7200s` where the draft showed 2h. A
second review found the minute and the hour owed the same reason as
targets: C++ shows a value in either at the precision that its built-in
conversions `1.0m` and `1.00h` give them, one place and two
(session.cc:80-81), while displayRow shows it at the precision recorded
for the unit, zero where no amount sets one, so that with
`P 2024/01/01 h 7 EUR` and `Assets:Euro  10 EUR`, `bal -X h` shows
`1.43h` on both C++ binaries where the value row was 1h, and with the
same price on `m`, `bal -X m` shows `1.4m` where it was 1m. The reason
now fires for every target that the built-in chain of time units names,
`s`, `m` and `h`, any `C` directive being refused before it. Over the
corpus with every comparable file forced to one target, the draft gave
VALUE:FAIL 3 for EUR, 9 for AAPL and 53 for `s`; this tree gives
VALUE:FAIL 0 with VALUE:PASS 3050 for `$`, 300 for EUR, 411 for AAPL and
105 for GBP, while under `s`, `m` and `h` it compares no file, every
comparable file being refused, 3555 of them `time-unit` under each.

The remaining-scope plan left this unit to settle the reason set, the
source of the terminus, two departures of the price table, the
placements of value expressions that S46 does not record, and the report
options that a journal line sets. Residual issue R15: `undated-price` is
not a reason, since the reader records that issue only without the
Driver's `--now` or after a journal line that sets `--now`, where
no-terminus refuses first, as the comment of that branch and the
docstring of Result.nowInFile say. The issue `exchange-order`, which S44
added, has a reason of its own. Of the kinds that the scratch price
printer reported, `%` and the null commodity have reasons of their own,
and a held time unit needs none, since C++ values a commodity through a
larger unit only where no chain reaches the target
(amount.cc:1301-1320), which the oracle refuses as no-price-path; the
targets `s`, `m` and `h` are time-unit, above. The terminus is the
Driver's `--now`, and a journal line that sets `--now` is refused, as
above. Of the two departures of the price table recorded at the price
directive reader, an `alias` subdirective that can retire a commodity is
refused, and a price read in a style other than C++'s is not. No reason
covers the known departures that the value rows inherit from the net
totals, from the dated price table and from the display precisions that
the reader learns, whose records the docstring of exchangeRows names,
saying as well that they are incomplete (below), nor four further shapes
that it records: a price read in another style, a journal line that sets
another report option or, since the fifth review (below), a session
option that takes effect only once C++ has read the journal, an
assignment in an expression that C++ evaluates as it reads the journal,
the limit that the docstring of Result.valueExprAll states, and such an
expression that values a commodity at the terminus, which the third
review found (below). Each can give wrong value rows (fail-open), and
each was measured on both C++ binaries, for the departure list of the
coverage unit: after `D $1,000.00` and a posting of `$1,5`,
`P 2024/01/04 Z $1,500` makes `bal -X $` show $1,500.00 for `1 Z` where
the value row is $1.5; a journal line `--exchange EUR` makes it show
EUR1530.0, and `--amount 7` shows 7 for every account; and
`check market(a, d, t) = 7`, among the shapes that the docstring of
Result.valueExprAll lists, shows 7 for every account. An inherited one,
the precision of an amount expression that exchangePoints records: after
`Assets:Cash  $1.00`, `Assets:A  (1.5 * 1.5 EUR)` against
`Assets:B  $-10.00` and `Assets:C  1000000000 EUR`, `bal -X $` shows
$4444444444.40 for `Assets:C` on both C++ binaries and the value row is
$4444444444.44. The 79 journals of S46's last review, run under
`--exchange '$'` against both C++ binaries, give 54 refusals, 11
rejections by the oracle (8 of journals that C++ rejects as well and 3
of journals that it accepts), 4 agreements and 10 differences: eight of
the assignment shape and two of report options set in a journal. The
second review measured an inherited departure that shows where the net
rows agree: a departure of the precision learned for the target reaches
every value row, and after an automated transaction whose template
amount `(Budget)  $0.49` teaches C++ alone two places for `$`,
`bal -X $` shows $2.50 for `1.25 X` priced at $2 on both C++ binaries,
where the value row is $2. The docstring of exchangeRows now says so
with that shape, and a probe pins it as a departure; refusing it would
need a new record of template amounts, `apply fixed` rates and
value-expression literals, and a new reason.

Four refusals need facts that the Driver could not see, so
Ledger/Journal.lean, a file outside this unit's planned list, gains four
fields of Result that record and change nothing else. fixatedLotComms
names the commodities to which C++ gives a fixated lot price: a lot
`{=P}` and an `apply fixed` rate, which the existing flags
PostSpec.fixated and PostSpec.rateFixed mark; a fixated cost `@ =P`
(xact.cc:816-819) and a fixated lot on a cost or on the target of a
balance assignment, which the new flag PostSpec.fixatedMark marks,
filled from the new costFixated result of annotLoop; and a template
amount of an automated transaction with any of these. For a posting that
the new flag marks, both the amount and the cost are recorded, though
C++ fixates one of them alone, so that the record is conservative and
the refusal can name a commodity that C++ values through the graph: for
`10 AAPL @ 5 EUR {=$2}` it names AAPL, which `bal -X $` values at $1700
on both C++ binaries, where C++ fixates the completion in EUR. For a
balance assignment the target alone is recorded, since the reader drops
its cost, so that where a fixated cost, a cost with a fixated lot price
or, after a target that is an amount expression, a fixated lot price
follows the target, each a line that C++ rejects (below), the
fixated-lot refusal stands only where a flow in the commodity of the
target is nonzero. The docstrings of the flag and of the field and the
comment of the branch say so, and probes pin both. nowInFile exposes
St.nowInFile, which a line `--now_` or `--now-` sets as well since the
fourth review (below). aliasPrices is set at an `alias` subdirective of
a `commodity` block after a point that names the alias or the block's
commodity (pool.cc:150-173, history.cc:336-349). pricePrecisionComms
names the commodities at whose price directive the two grants differ:
priceDirective now also returns the price as C++ reads it (PriceRead),
and recordPriceGrant, called from the `P` arm and from a `P` line that
the first field follows directly, compares the precision that the
directive leaves the commodity in the reader with the one that C++ gives
it from the precision recorded before the directive, reading a commodity
as known where C++ has marked it known, which the new St.knownComms
holds: the source of an earlier price directive, the commodity of an `N`
directive (textual_directives.cc:202-209), and, since the third review
(below), the commodity of an `apply fixed` or `apply rate` directive and
that of a `commodity` directive read while C++ checks commodities, which
the new flag St.commodityChecking follows. Its docstring names the
places at which C++ marks a commodity known that another record stands
for: a `D` directive, for which St.lockedPrec stands since the directive
also locks the commodity (textual_directives.cc:160 and 165), and the
source of a price directive whose price is an expression (pool.cc:610)
or that the reader does not read, where the issue `price-expr` or
`unread-price` refuses first, and, since the fourth review, the larger
unit of a `C` directive, to which C++ copies the flags of the smaller
unit (amount.cc:1984-1985), where `equivalence` refuses first. The
shapes behind the first three fields were measured on both C++ binaries.
With `P 2024/02/01 AAPL $170`, `bal -X $` shows $1500 for
`10 AAPL {=$150}`, for `10 AAPL @ =$150` and under
`apply fixed AAPL $150`. A journal line `--now 2024/03/01` makes it
value `1 AAPL` at $4 beside a later price of $5 on 2024/04/01. After
`P 2024/01/01 E $1`, `P 2024/01/01 EUR $3` and `commodity EUR` with
`alias E`, it values `10 EUR` at $10, where the table's point gives $30.
The oracle refuses each of these journals. A run of the review's random
journals against this tree found a departure of the net rows that no
comment recorded, and the comment on COST_PREC tagging in balancePool
now records it as its fifth: C++ recovers the precision of a per-unit
cost from the cost amount, which adds the decimals of the amount to
those of the price (xact.cc:214-216, textual_xacts.cc:701-707), and the
reader from the cost literal, so that `A  2.5 FOO @ $3` against an
elided `C` shows `$-7.5` on both C++ binaries and `$-8` here. No corpus
journal shows it. The second review found a departure of the default
output that predates this unit: displayAmount shows a total converted to
minutes at two places, where C++ shows the one place of its built-in
conversion `1.0m` (session.cc:80), so that `Assets:Time  100s` shows
`1.7m` on both C++ binaries and `m|1.67` here. No corpus journal shows
it either. The docstring of displayAmount now records it, the module
docstring of Driver.lean no longer says that the minute is shown as C++
shows it, and the default output is unchanged.

A third review found two places at which C++ marks a commodity known
that St.knownComms did not follow, so that price-precision did not fire
where the two grants differed, and the value rows departed while the net
rows agreed. C++ reads an `apply fixed` or `apply rate` directive with
the function that reads a price directive, recording no price but
marking the directive's commodity known (textual_directives.cc:530-541,
pool.cc:645-647): after `apply fixed EUR $1.10` and `end apply fixed`,
`P 2024/01/01 FOO 1.25 EUR` grants EUR no decimal place, and
`bal -X EUR` shows `EUR12` for 10 FOO on both C++ binaries, where the
value row was 12.5. And a journal line that sets `--strict`,
`--pedantic`, `--strict-commodity` or `--pedantic-commodity` makes C++
check commodities, which no later option undoes, so that it registers
the commodity of each later `commodity` directive as a declared one and
marks it known (session.h:280-310, textual_directives.cc:814,
journal.cc:397-410): after `--strict`, `commodity $` and
`commodity AAPL`, `P 2024/02/01 AAPL $170.25` grants `$` none, and
`bal -X $` shows $511 for 3 AAPL on both C++ binaries, where the value
row was $510.75, as C++ shows it without the `--strict` line. The
`apply` arm of handleDirective now records the commodity of such a
directive in St.knownComms, through the new helper recordKnownComm; a
journal line that sets one of the four options sets
St.commodityChecking, which nothing clears; and while it is set, each
`commodity` directive records its commodity, found through the commodity
aliases. Both journals are now refused price-precision, and so are six
variants of the second, under `--pedantic` with declared accounts, under
`--strict-commodity`, under `--pedantic-commodity`, under `--strict`
followed by `--permissive`, with a price in EUR, and with a declaration
that names its commodity through an alias, and two of the first, an
`apply rate $ 2 GBP` before a price in `$` and an
`apply fixed AAPL 1 XYZ` before a price in AAPL. Three controls, the
second journal without the strictness line, one whose `commodity $`
precedes that line and one whose `apply fixed` marks a commodity that no
price names, are valued as both C++ binaries value them. The checks
themselves are not modelled: under `--pedantic`, C++ rejects a posting
to an account that no directive declares, which the reader accepts, as
before. No corpus journal has such a directive together with a price
directive. The docstrings of St.knownComms and PriceRead now say that
C++ marks the source of a price directive known whether or not it
records the price, that of St.knownComms lists the places at which C++
marks a commodity known as it reads a journal, in pool.cc, journal.cc
and textual_directives.cc (amount.cc joined them in the fourth review),
that of recordPriceGrant names the `commodity` directive read while C++
checks commodities, and the comment of the price-precision branch says
that the comparison starts from the precision recorded before the
directive. A precision that the reader learns otherwise before the
directive stays an inherited departure, which the docstring of
recordPriceGrant now records with the shape that the review measured:
`apply fixed XYZ $1.123` gives `$` three tentative places in C++ alone
(pool.cc:628-634), so that after `P 2024/01/03 EUR $1.1111`, `bal -X $`
shows $3.333 for 3 EUR on both C++ binaries, where the value row is
$3.3333; a probe pins it. The review also found a fourth fail-open
shape, which the docstring of exchangeRows, the README and AGENTS.md now
record beside the other three: an expression that C++ evaluates as it
reads the journal and that values a commodity at the terminus leaves a
price in the cache that C++ keeps for that commodity
(commodity.cc:171-224), which the report reads, though a later price of
another commodity on its chain changes the value, since a new price
clears the cache of the commodity that it prices alone
(commodity.cc:85). After `P 2024/01/01 AAPL 100 EUR`,
`P 2024/01/01 EUR $1`, 3 AAPL with the transaction line
`check market(1 AAPL, [2026/08/26], "$") > 0` and then
`P 2024/01/03 EUR $2`, `bal -X $ --now 2026/08/26` shows $300 on both
C++ binaries, where the value row is $600; a probe pins it. Two smaller
findings of the same round are taken up as well: the sentence of that
docstring on the way displayRow shows a total in a time unit now says
that the second alone is shown in a larger unit, and exchangeRows builds
no price graph where there is no terminus, in place of one at instant 0
that the branch no-terminus discarded.

A fourth review found that the reader followed C++'s commodity checking
only for the hyphenated spellings of the four options. C++ finds an
in-file option by its name with each `-` read as `_`, trying the name
with a `_` appended before the name itself (find_option, option.cc:71-94
at 76b9dc2b2), and is_eq matches a declared name that ends in `_`, the
mark of an option that takes an argument, with or without that `_`
(option.h:450-461). So a line `--strict_commodity` or
`--pedantic_commodity` in place of `--strict` in the third review's
journal makes both C++ binaries show $511, where the tree of the third
fix round printed the value row $510.75 and no `??` line. The in-file
option branch of handleDirective now reads the name as C++ looks it up,
St.commodityChecking follows every spelling of the four options, and
both journals are refused price-precision. The same lookup reads
`--now_` and `--now-` as `--now`: with the prices of $4 and $5 of the
in-file `--now` journal above and a line `--now_ 2024/03/01`, both
binaries show $4, where that tree printed $5, and they show $4 for
`--now- 2024/03/01` as well; such a line now sets St.nowInFile, and
no-terminus refuses it. The branches that apply `--decimal-comma`,
`--recursive-aliases`, the format of `--input-date-format` and the date
of `--now` still read the spelling given alone, a departure that
predates this unit and whose fix would change the default output, unlike
the fix of a record that the value rows alone read, such as those of the
strictness lines and of `--now` above and that of an input date format
below. The comment of the branch records it as fail-open, each of the
four branches giving wrong rows on both C++ binaries: under
`--recursive_aliases`, `alias A=B` and `alias B=C`, a posting of $5 to
`A` shows under `C` there and under `B` here; under `--decimal_comma`,
`1.000 EUR` is a thousand euros there and one here; under
`--input_date_format`, the dates of transactions depart, as the fifth
review found (below); and after `--now_ 2024/03/01`, a check-in at 22:00
on 2024/02/29 that no check-out closes shows `2.00h` there and is closed
here at the Driver's `--now`. The opening comment of the branch, which
said that the C++ reader accepts any option line, now says that C++
rejects a line whose name it does not know
(textual_directives.cc:251-252), as both binaries reject
`--no-such-option` with exit status 1, a journal that the reader
accepts, recorded as fail-open. No journal of the C++ corpus at
f396a2e69 has an option line in another spelling, and the default output
is unchanged. Three smaller findings of the same review are taken up:
the docstring of St.knownComms names the larger unit of a `C` directive,
as above; README.md and AGENTS.md now say that the inherited departures
that no reason covers do not include the precision that a price
directive grants, which price-precision refuses, as the docstring of
exchangeRows says; and a comment beside the Driver's loop over its
arguments says that a flag that takes a value takes the next argument
that the tests before its own pending test do not claim, so that
`--exchange --gross FILE` applies `--gross` and takes FILE as the
target, reading no file, as `--now` and `--input-date-format` take their
values.

A fifth review, of the fixes of the fourth, found the same gap in a
third record. St.inputDateFormat, the guard of priceDay behind
unread-price, followed the spelling `--input-date-format` alone, while
C++ reads `--input_date_format`, `--input-date-format-` and
`--input_date_format_` as the same option (OPT(input_date_format_),
session.cc:461, its handler at session.h:270-275). Under those spellings
the reader dated prices with its own readers, and the value rows
departed from both C++ binaries with no `??` line where the net rows
agree: after `Y 2024` and `--input_date_format %d/%m`,
`P 01/03 AAPL $101` and `P 02/01 AAPL $102` value 2 AAPL at $202 on both
binaries, which date the first price March 1, where the value row was
$204; and with `--input_date_format %Y/%d/%m`, `P 2024/01/01 AAPL $3`
and `P 2024/03/01 AAPL $5`, `bal -X $ --now 2024/02/01` shows $5 on
both, where the value row was $3. The in-file option branch now sets
St.inputDateFormat for every spelling that C++ reads as the option and
leaves the format itself as it was, so that the seven journals of the
review that showed the gap are refused unread-price with no value line,
and the default output is unchanged, since priceDay alone reads the
record. The format itself departs in the default rows under those
spellings, which the comment of the older branches now records with two
shapes measured on both binaries: after `--input_date_format %d/%m/%Y`
and an automated transaction on `date >= [2024/02/01]` with
`(Budget)  $1`, a transaction dated `01/03/2024` gives `Budget` $2 there
and nothing here, where its date is read as January 3; and a balance
assignment `= $15` dated `02/03/2024`, after $10 dated `05/01/2024`,
posts $5 there and $15 here. The fourth review's fixes found no such row
because their automated transaction followed the transaction, and C++
applies a rule to no transaction read before it; the records that said
otherwise are corrected.

The review asked as well that the class be closed rather than found one
record at a time, so every field of St that the value path reads was
enumerated, with every session option of session.h and `--now`, each
measured on both binaries with and without `~/.ledgerrc`. The records
that a journal line sets for the value rows, St.inputDateFormat,
St.nowInFile and St.commodityChecking, now follow every spelling that
C++ reads; the branches that apply `--decimal-comma`,
`--recursive-aliases`, the format of `--input-date-format` and the date
of `--now` follow the spelling given alone, and honouring the others
would change the default output. Six of the options that the reader
ignores make C++ book or accept otherwise than the reader when a journal
line sets them, since C++ applies them as it reads the journal, and the
opening comment of the in-file option branch, which said that the reader
applies the parse-semantic lines, now names the options that it applies
and records, marks every other line fail-open, and gives these six with
their measurements: `--no-aliases`, after which a posting to an alias
shows under the alias there and under its account here (the review's
second finding); `--check-in-file-order`, after which a balance
assignment reads the postings that precede it in the file, so that
`= $15` dated 2024/01/01, after a posting of $10 dated 2024/01/05
earlier in the file, posts $5 there and $15 here; `--day-break`, which
splits a timelog entry at midnight, so that an automated transaction on
the date of the second day books 2.00h of a four-hour entry there and
nothing here; `--check-payees`, under which C++ rejects an undeclared
payee where `--pedantic` is in force; `--file`, which clears the list of
files that C++ is walking, so that both binaries exit with status 0, 1
or 139 from one run to the next; and, as the sixth review found (below),
`--permissive`, the one departure of the six that fails closed. Eight
more change nothing, since C++ applies them before it reads the journal,
as measured for `--account-value-expr`, `--commodity-value-expr`,
`--value-expr`, `--time-round`, `--lot-matching`, `--price-db`,
`--master-account` and `--hashes`. And `--time-colon` and the options of
`--download` take effect only once C++ has read the journal, as the
report options do but two (below), `--download` changing the value rows
alone: with `--getquote` naming a script that quotes AAPL at $7,
`bal -X $` shows $70 for 10 AAPL priced at $2 on both binaries, where
the value row is $20. The docstring of exchangeRows and README.md now
name such session options in the first of the four further shapes,
beside the report options. Four journals of the C++ corpus at f396a2e69
carry a line of an ignored option (`--day-break`, `--check-payees`,
`--permissive` and `--explicit`), in shapes that show no departure, and
the bisimulation passes all four. Every departure that this unit's
records hand to the coverage unit, beyond those that the remaining-scope
plan assigns it, is now recorded beside its rule, but two rejections by
the oracle, which print no value line, and a trailing decimal mark of a
suffix amount that C++ rejects for an earlier separator, as in
`1,5. EUR`, which the reader accepts, a departure whose rule lies in
Ledger/Parse.lean, outside this unit's files (below). The one that was
not, an `end apply rate` after `apply rate`, which C++ rejects, as it
rejects any `end apply KIND` that does not name the innermost `apply`
and an `end` that no `apply` encloses (textual_directives.cc:573-598;
test/regress/coverage-wave11-end-apply-mismatch.test and
coverage-wave11-end-apply-empty.test), is now recorded beside the `end`
arm, which accepts all three (fail-open). No code changes but the new
branch; the rest of the round is comments, docstrings and README.md.

A sixth review, of the fixes of the fifth, found the class open for one
more record, three comments that did not hold and a departure that no
comment recorded. A journal line `--decimal_comma`, which C++ reads as
`--decimal-comma` and the reader does not apply, left the reader's
prices departing where the net rows agree: after the line,
`P 2024/01/02 AAPL $1.500` values 10 AAPL at $15000 on both binaries,
where the value row was $15, and the reason that the fifth round gave
for leaving that shape fail-open, that a refusal would need a new
reason, was wrong, since unread-price stands for a price that the reader
does not read as C++ reads it. A new field of St, decimalCommaUnapplied,
which such a line sets and which priceDirective and recordCostPoints
read, puts the issue unread-price in place of every point that a price
directive, a cost or a lot price beside one gives after the line
(session.h:233, amount.cc:1714-1715), so that such journals are refused;
the seventh review extended the record to every value row of such a
journal and confined it to a reader that does not read amounts with a
decimal comma already (below). The default output is unchanged, since
the Driver reads the issues in exchangeRows alone. C++ reads no other
spelling of the option: both binaries reject `--decimal-comma-` as an
illegal option, since is_eq matches a name declared without a final
underscore exactly (option.h:450-461). The opening comment of the
in-file option branch said that `--permissive` changes nothing that the
reader models, but C++ reads the checking style that it sets as it
balances a transaction, and accepts one that a computed balance
assignment leaves unbalanced in one commodity by adjusting the assigned
posting (xact.cc:932-955, test/regress/2005.test): after a line
`--permissive`, `Assets:Cash  $10` against `Equity` and then
`Assets:Cash  = $15` with `Expenses  $3`, both binaries show $7 under
`Assets:Cash` and reject the journal without the line, while the reader
rejects it either way, a fail-closed departure that the `--permissive`
which the harness gives C++ on the command line hides. The comment said
as well that the report options take effect only once C++ has read the
journal, but `--ignore-diacritics` sets at once the folding of accents
with which C++ matches a pattern (report.h:988, mask.cc:363-479), so
that after such a line `= /Cafe/` with `(Budget)  $1` books $1 to
`Budget` for a posting of $5 to `Expenses:Café` on both binaries, which
the reader does not; an enumeration of the report options found no other
but `--now` whose handler acts while C++ reads. The comment of the `end`
arm said that a stray `end test` or `end comment` closes nothing, but
C++ takes the kind of an `end` line from the text after its first word
(textual_directives.cc:573-598), so that `end test`, `end foo` or any
other single word closes the innermost `apply`: after
`apply account Foo` and `end test`, a posting of $5 to `Assets:Cash`
shows there on both binaries and under `Foo:Assets:Cash` here, and with
10 AAPL priced at $2 in its place the value row is
`$|20|Foo:Assets:Broker`, where both show $20 under `Assets:Broker`. And
C++ rejects an option line after the prefix `!` or `@`
(textual.cc:321-372), which the reader reads as an option line: both
binaries reject `!--input_date_format %Y/%d/%m`,
`@--input_date_format %Y/%d/%m` and `!--permissive`, which the reader
accepts. Each of these four is now recorded beside its rule, marked
fail-open or, for `--permissive`, fail-closed, and fixing any of them
would change the default output. Two smaller corrections: the comment of
the branch for `--input_date_format` now says that C++ reads a date with
a time without the format (times.cc:421-442), so that prices dated
`2024/01/01 00:00:00` and `2024/03/01 00:00:00` value 1 AAPL at $3 on
both binaries and in the value rows at `--now 2024/02/01`; and, found
while fixing, once C++ has read a line `--decimal-comma` in either
spelling it shows every amount with a decimal comma (amount.cc:308-310,
324-326), while the Driver declares one only for a style that the reader
learns, so that `$10.50` read before the line shows as `$10,50` there
with no `%% dc $` line here, a departure of the default output that
predates this unit and is now recorded beside that branch.

A seventh review, of the fixes of the sixth, found the record of
`--decimal_comma` incomplete. The reader learns display precisions from
the amounts that it reads after the line, as it reads them, and every
value row shows the target at the precision learned for it, so that the
value rows still departed with no `??` line where the net rows agree:
with `P 2024/01/01 EUR 3 AAPL` and 2 AAPL before the line, and
`1.500 EUR` against `-1.500 EUR` after it, both binaries show `1 EUR`
for the AAPL account under `-X EUR`, having read 1500 at the precision
0, where the value row was 0.667 EUR. The line now records, beside the
field, the issue unread-price without a commodity, which exchangeRows
alone reads, so that every value row of such a journal is refused, those
valued through a price read before the line among them, in excess where
neither an amount read after the line nor what C++ shows departs; the
four journals of the sixth review are now refused without a commodity,
since that issue comes first. The record is not set where the reader
reads every amount with a decimal comma already, under the Driver's
`--decimal-comma` or after a line in the hyphenated spelling, where the
line changes nothing in C++ either: under that flag, after the line,
`Assets:A  10 AAPL @ $1,5` against `Assets:Cash` gives the value rows
$15 and $-15 that both binaries show, where the tree of the sixth review
refused them. The default output is unchanged. The comment on
`--ignore-diacritics` named too narrow a scope: C++ folds a pattern
where it compiles it and a text where it matches it (mask.cc:362-371,
mask.h:125-146), and the quick matcher of an automated transaction keeps
its result for each account name (xact.cc:1466-1491), so that a match
made after the line can differ wherever the automated transaction was
read, save for an account that it tested before the line. Both binaries
book $1 to `Budget` for a posting to `Expenses:Café` read after the line
where `= /Cafe/` precedes the line, as where it follows it, and book
nothing where `= /Café/` precedes it, while the reader books nothing in
the first case and $1 in the second; and where a posting to that account
precedes the line, they book nothing for a later posting to it and $1
for one to `Expenses:Café:Dinner`. The comment now says so, with these
measurements, and marks the shape fail-open as before.

An eighth review, of the fixes of the seventh, approved the change with
one minor finding: three docstrings, of recordCostPoints, priceDirective
and Result.priceIssues, still said that the issue unread-price stands in
place of the points read after any journal line `--decimal_comma`, while
since the seventh review the line records nothing under the Driver's
`--decimal-comma` or after a line in the hyphenated spelling. Each now
states that condition. So do the sentence of the docstring of
Result.priceIssues on the issue without a commodity, which now names the
first such line, the one that records the issue, and the comment of the
unread-price branch of exchangeRows, since both named a line that the
reader does not apply, a clause that holds of every such line; and the
comment beside the guard in priceDirective no longer says that the
reader does not read the amount with a decimal comma, which is not so
after a later line in the hyphenated spelling, where the guard refuses
in excess. Comments alone change, and the default output and the value
rows are as before.

An audit of the tree of the eighth review found that README.md and the
docstring of exchangeRows, to which AGENTS.md points, still said that
the known departures that the value rows inherit are recorded beside
their rules, while some known departures of the net totals are recorded
beside no rule, among them shapes that C++ rejects and the reader
accepts, which the remaining-scope plan leaves to the coverage unit.
After `Assets:Broker  5 AAPL @ $150` on 2026/01/04, a balance assignment
`Assets:Broker  = 15 AAPL {$150}` on 2026/01/05 against an elided
`Assets:Other` gives `Assets:Broker` 20 AAPL on both C++ binaries and 15
AAPL here, so that `bal -X $` shows $3000 for it there and the value row
is $2250, with no `??` line; and `Assets:Broker  10 AAPL (lot (a))`
against `Assets:Cash  $-1500`, which both binaries reject, gives value
rows here. README.md now says that the comments beside the rules record
such departures but not all of them, so that a value line can depart
from C++ where no comment says so; the docstring of exchangeRows names
the records of each kind and says that they are incomplete; and
AGENTS.md says that the docstring says so. The fix changes nothing else
in the repository, and the default output and the value rows are as
before. The audit found as well that nine of the seventeen reasons had
no probe that pins them; each now has one (below).

A review of the audit's fix found that the sentence above on the
departures that this unit's records hand to the coverage unit did not
hold: the records of that fix handed it one more, the cost after a
balance-assignment target, which no comment recorded. C++ reads the
target of a balance assignment alone, a literal with its annotations or
an amount expression without any, and rejects whatever follows it but a
note (textual_xacts.cc:756-760 and 906-910), while the assignment branch
of parseAmountField reads a cost or a balance assertion there, and after
an amount expression its annotations as well, and drops them: with
`P 2026/02/01 AAPL $170`, both C++ binaries reject
`Assets:Broker  = 10 AAPL @ $150` against an elided `Assets:Cash`
("Unexpected char '@'"), and the oracle prints its net rows and the
value rows $1700 and $-1700 with no `??` line, as it does with
`@@ $1500`, `(@) $150`, `{$150} @ $150` or `= 10 AAPL` in place of the
cost, and for `= (10 AAPL) {$150}`, which both binaries reject as well.
The comment of the branch now records these shapes, marked fail-open,
and with that record the sentence holds. The same comment said that the
flags of the target stay false but for lotExpr, while this unit sets
PostSpec.fixatedMark there as well, for a fixated lot price on the
target, which C++ keeps in the amount that it computes, and for a
fixated cost after the target, as `@ =$150`, a cost with a fixated lot
price or a fixated lot price after an amount expression, lines that C++
rejects; the comment now says so, and the docstrings of
PostSpec.fixatedMark and Result.fixatedLotComms name those shapes, whose
value rows the Driver refuses as the next paragraph states. The other
review of the fix approved; it found that the probe of price-expr ran
under `--exchange '$'`, where C++ shows `1 C` unvalued for want of a
path to `$`, so that what C++ does there says nothing of the departure
that the refusal guards, and the probe now runs under `--exchange USD`,
the commodity of the journal's prices, where both binaries value C at
USD1.10 from the evaluated expression and the oracle still refuses
price-expr. Comments alone change in the repository, and the default
output and the value rows are as before.

Two reviews examined that pass. One found that two of its claims about a
fixated cost after a balance-assignment target did not hold: that the
record holds the commodities of both the target and the cost, and that
the Driver refuses the value rows of every such journal. The assignment
branch keeps no cost, so that finalizeXact records the commodity of the
target alone, and the Driver refuses the value rows only where a flow in
that commodity is nonzero, as in the journals that the pass measured,
whose target is 10 AAPL. With `P 2026/02/01 EUR $2` as well and a
transaction `Assets:Euro  100 EUR` against `Equity` before it, both C++
binaries reject `Assets:Broker  = 0 AAPL @ =150 EUR` against an elided
`Assets:Cash` ("Unexpected char '@'"), and the oracle prints the value
rows $200 and $-200 with no `??` line, as it does with
`= 0 AAPL @ 150 EUR {=$2}`, `= (0 AAPL) {=$50}` and `= (0 AAPL) ((200))`
in its place, each rejected by both binaries. The comment of the branch
and the docstrings of PostSpec.fixatedMark and Result.fixatedLotComms
now say what the record holds and where the fixated-lot branch refuses
the value rows; recording the commodity of the dropped cost as well
would refuse the value rows of one more shape that C++ rejects while its
net rows still print, and would bring the reader no closer to C++. The
other review approved; it found that the comment did not name two more
tails that C++ rejects after a target and the reader accepts, and the
first review found the second of them as well. The first is a second lot
price, date, note or value expression on a literal target, which C++
reports as more than one (annotate.cc:157, 193, 213-214, 249): both
binaries reject `= 10 AAPL {$150} {$160}`,
`= 10 AAPL [2026/01/01] [2026/01/02]`, `= 10 AAPL (a) (b)` and
`= 10 AAPL {$150} {=$160}` ("Commodity specifies more than one price",
"date" or "tag"), where the oracle prints the net rows and the value
rows $1700 and $-1700; of two lot prices or value expressions, a first
fixated lot price is refused fixated-lot and a value expression
value-expression, as one alone is. The second is a lone `.`: annotLoop
ends a tail that is a lone `.` as it ends an empty one, which reads the
decimal mark that ends a prefix amount, a cost or a target, as in
`$10.`, `AAPL 10.`, `10 AAPL @ $150.` and `= $10.`, which C++ keeps in
the quantity (amount.cc:1575-1579) and both binaries show as the oracle
does, but it accepts as well every other tail that trims to a lone `.`,
among them a `.` that white space sets apart, as in `10 AAPL .`,
`10 AAPL {$150} .`, `10 AAPL @ $150 .`, `10 AAPL = 10 AAPL .`,
`10 AAPL .  ; a note`, `= 10 AAPL .` and `= $10 .`, which both binaries
reject ("Unexpected char '.'"); for the first the oracle prints
`AAPL|10|Assets:Broker`, `AAPL|-10|Assets:Cash` and the value rows $1700
and $-1700 with no `??` line. The comment of the branch now names both
tails, and the lone `.`, which the reader has accepted after every
amount since its import, is recorded beside its test in annotLoop,
marked fail-open; the next paragraph names the other tails that the test
accepts. Comments alone change in the repository, and the default output
and the value rows are as before.

Two reviews examined that fix and approved it, with four minor findings,
all of them about comments. Both found that the record beside the test
of a lone `.` named too few of the tails that the test accepts. It
accepts every tail that trims to a lone `.`, among them one written
directly after a closing `}`, `]` or `)`, as the reviews found, or after
the closing `"` of a quoted commodity, as this round found: both
binaries reject `10 AAPL {$150}.`, `10 AAPL [2026/01/01].`,
`10 AAPL (note).`, `(10 AAPL).`, `10 AAPL @ $150 {$2}.`,
`10 AAPL @ ($150).`, `= 10 AAPL {$150}.`, `= (10 AAPL).` and
`10 "AAPL".` ("Unexpected char '.'"), and for the first the oracle
prints the net rows and the value rows $1700 and $-1700 with no `??`
line. And, as the first review found, where the quantity holds a
separator before the decimal mark that the test serves, C++ checks the
punctuation of the quantity with the mark in it (amount.cc:1724-1827),
while the number read here lacks it: both binaries reject `$1.5.`
("Too many periods in amount", amount.cc:1730) and `$1,23.`
("Incorrect use of thousand-mark comma", amount.cc:1797), and under the
decimal comma `$1,5.` ("Too many commas in amount", amount.cc:1759),
while the oracle prints for `$1,23.` the line `%% dc $`, the rows $1.23
and $-1.23 and the value rows $1.23 and $-1.23; `$1,234.`, `$1,000.` and
`$1'234.` agree. The comment beside the test now records both, marked
fail-open, and records as well the side on which the test fails closed,
which the first review found and the second noted: where a cost, a lot
price, date or note, or a balance assertion follows the mark, the reader
rejects the journal as trailing junk, while both binaries accept
`$10. @ 5 EUR`, showing $10 and EUR-50, and $10 and $-100 under `-X $`,
and the eight other such shapes that this round measured. The first
review's journals show that the trailing mark of a suffix amount departs
in the same way, as in `1,5. EUR` and `1.234. EUR`, which both binaries
reject and the oracle reads as 1.5 EUR, with `%% dc EUR`, and as 1.234
EUR; but that mark never reaches the test: numTokenOf leaves it out of
the number, and the suffix branch of parseAmount in Ledger/Parse.lean
drops it from the tail. That file is outside this unit, so the departure
is left to the coverage unit unrecorded, the exception that the sentence
above on the departures handed to it now names, and a probe pins it.
The second review found as well that the docstring of
Result.fixatedLotComms named a fixated lot price after the target among
the lines that C++ rejects, which takes in
`= 10 AAPL {=$50}`, a line that C++ accepts, and that it and two other
records said that the Driver prints the value rows where no flow in the
commodity of the target is nonzero, while only the fixated-lot branch
does not refuse them there and another reason still can. The docstring
now names the three shapes as the docstring of PostSpec.fixatedMark
names them, and the three records say what that branch does. Comments
alone change in the repository, and the default output and the value
rows are as before.

The announcement lands here. README.md names six flags and gains a
paragraph on `--exchange` that cites netValueAt_eq_valueHom with its
conditions, a list of accounts without duplicates that holds every
account with a nonzero flow, as the driver's list does, and names every
reason; the protocol paragraph of AGENTS.md describes the flag, its
lines and its reasons and keeps the admissibility sentence; the
Driver.lean module docstring names the flag and its lines. The citation
of netValueAt_eq_valueHom, in the README and in the docstring of
exchangeRows, depends on the S08 amendment on join modules, which the
user is asked to approve; if the category is declined, both citations go
and nothing else changes. Residual issue R24 is taken up as S40 took it
up for `--gross`: the Oracle smoke test step of
.github/workflows/ci.yml, also outside the planned list, runs the oracle
with `--now 2026/08/26 --exchange '$'` as well and requires
`>> value $|100|Assets:Cash` and `>> value $|-100|Equity:Opening`; it
remains one step.

The plan's probes print the expected lines:
`>> value $|2551.85|Assets:Broker`, `>> value $|-2305|Assets:Cash` and
`>> value $|5|Expenses:Food` for x1.dat; `?? exchange fixated-lot AAPL`
for p1.dat; `?? exchange value-expression AAPL` and no value line for
each of the two value-expression probes; for the chain and rounding
probes the rows of C++ `bal --flat --no-total -X '$'` at the same
`--now` ($330.00 and $-330.00, $3000000000 and $-3000000000);
`?? exchange alternative-price-path XYZ` for the cycle probe; and
`?? exchange no-terminus` without `--now`. The first review's journals
for the three new reasons are probes as well, together with one where
both sides grant the same precision, whose value rows equal C++'s. The
second review's are too: the targets `h` and `m` refused time-unit;
three journals in which a lone inferred amount in `$` keeps its mark
alone or beside a holding valued at zero, and loses it beside one valued
above zero, as on both C++ binaries; the journal of the example above;
the conservative fixated-lot line; and, pinned at their direction, the
template precision and the minute. The third review's are too: the ten
journals refused price-precision above, the three controls, and, pinned
at their direction with the value rows that the oracle prints, the
`apply fixed` precision and the stale cache. The fourth review's are
too: the two strictness journals refused price-precision and the
`--now_` and `--now-` journals refused no-terminus, with no value line,
and, pinned at their direction, the departures of `--recursive_aliases`
and of `--now_` with a dangling check-in against C++ beside their
hyphenated controls, which agree, and the rows that the oracle prints
for `--decimal_comma` and for its control, since the row comparator does
not read the decimal comma of C++'s display. The fifth review's are too:
the seven journals refused unread-price with no value line; the two
shapes of the dates of transactions under `--input_date_format`, against
C++ beside their hyphenated controls, which agree; the session options
that the reader ignores, against C++ at their directions, with controls,
and those that change nothing; the value rows of `--download`, pinned
with the value rows that the oracle prints, and the price read after
`--decimal_comma`, refused since the sixth review; and the three `end`
lines that C++ rejects, beside one that it accepts. The sixth review's
are too: the price, the cost, the price with a time and the price
without a date read after `--decimal_comma`, and the price read before
it, refused unread-price with no value line, the last since the seventh
review; the `end` lines that C++ reads as closing the innermost `apply`,
against C++, with the value row that one of them gives, and the two that
C++ rejects; the journals of `--permissive`, which the oracle rejects
with and without the line under the `--permissive` that the row
comparator gives C++; `--ignore-diacritics` beside its control; the
option lines after `!` and `@` and in the spellings `--decimal-comma-`
and `--permissive_`, which C++ rejects; and the prices with a time under
`--input_date_format`, valued. The seventh review's are too: its journal
of the precision learned after `--decimal_comma`, refused unread-price,
beside the two controls, whose value rows equal C++'s; a hyphenated line
after the underscore one, refused in excess; the underscore line under
the Driver's `--decimal-comma`, and after a hyphenated line, whose value
rows equal C++'s; and the three shapes of `--ignore-diacritics` above,
against C++, beside a control. And the audit's: for each of the nine
reasons that no probe pinned, price-expr, exchange-order, posting-date,
equivalence, commodity-alias, null-commodity, percent, no-price-path and
zero-inverse-price, one journal refused with that reason and no value
line. And this pass's: the cost, the total cost, the virtual cost, the
cost after a lot and the balance assertion after a balance-assignment
target, and a lot price after a target that is an amount expression,
which C++ rejects, beside the target alone, which it accepts; the value
rows that the first gives, printed with no `??` line; and the fixated
cost after the target, the fixated lot price on it and one after an
amount expression, refused fixated-lot, and a value expression after an
amount expression, refused value-expression, each with no value line.
And the reviews' of this pass: the second lot price, date and note of a
literal target, and the lone `.` after an amount, a lot, a cost and an
assertion, before a note and after two targets, which C++ rejects,
beside the decimal mark that ends two prefix amounts, a cost and a
target, which both sides read alike; the value rows that the lone `.`
gives; of two annotations of a target, a first fixated lot price refused
fixated-lot and two value expressions refused value-expression, each
with no value line; and the value rows of the four zero targets, printed
with no `??` line. And those of the reviews of that fix: the `.` after a
closing `}`, `]`, `)` or `"` in nine shapes, and the decimal mark after
a period or a comma that C++ would read as a decimal comma, which C++
rejects, beside three groups that both sides read alike; the value rows
of `10 AAPL {$150}.` and of `$1,23.`; the mark followed by a cost, a
total cost, a lot price, date or note or an assertion, which the oracle
rejects and C++ accepts, beside a `;` note after it; and the trailing
mark of a suffix amount after such a separator, which C++ rejects,
beside two controls.

Residual issue R5 asks this unit to run the value comparison over the
corpus while an oracle fix can still land here. A scratch copy of
test/semantic with a draft of the harness change planned for C15 was run
against this tree and the C++ build at f396a2e69: the targets come from
each file's own `bal` lines, the oracle receives `--exchange T`, its
value rows are compared with C++ `balance --flat --no-total -X T`,
refusals are checked against a list, and a detector perturbs a price on
the C++ side alone. It reports PASS 3816 and FAIL 0 on the net rows,
both net detector fixtures and the value detector green, and VALUE:PASS
60 and VALUE:FAIL 0, with 146 value rows compared in 59 files, 11 files
set aside for a `-X` form other than one commodity, and 37 refusals:
alternative-price-path 1, equivalence 3, fixated-lot 8, no-price-path
10, posting-date 3, price-precision 10, time-unit 1 and value-expression
1 (exit 0), the same tallies and the same 37 refusals as at the trees of
the fixes of the second to the eighth reviews and of the audit. The
time-unit refusal is test/regress/1018.test, whose target is `h`, which
the tree of the first fix round refused as no-price-path; with the list
measured on that tree, the run at the tree of the second fix round
failed on that file alone (exit 1). A refusal list without its last line
makes the run fail and name that file (exit 1). Of the ten files refused
price-precision, the draft compared and passed eight and refused two as
alternative-price-path; two of the ten show the departure in their own
target, with values that happen to be whole, and the other eight are
refused in excess. These 37 are the list that C15's value-refusals.txt
is to carry; the run reads `~/.ledgerrc`, as check_all.py runs C++, and
no `~/.pricedb` exists.

The resumed verification found a defect in the scratch harness, not in
the oracle: its net-detector failure branch belonged to the test of
`--value`, rather than to the detector's result. It is now attached to
the result in both the ordinary and the forced-target drafts, and a
missing refusal list is an error in the ordinary draft, not an unchecked
run. On the real oracle and C++ build, a net defect made inert let the
old draft exit 0, while the corrected draft exits 1 and names the failed
detector. Making the value defect inert likewise exits 1; the control
and the run without `--value` exit 0, and a missing refusal list exits
2. These results are in `harness-mutations.log` under
`impl/S48/resume-final-u1AP1h/logs`, whose script exits 0. The corrected
ordinary draft repeats the counts above (`r5.log`, exit 0), with the
same value-results list as the preceding round; the planted incomplete
list still fails (`planted-ratchet.log`, exit 1).

Verification, rerun at this tree after the corrections that the reviews
of the third pass asked for. The locked build script, lake build with
warnings as errors, exits 0 after 1226 jobs with no warning in its log,
and `nix build .#oracle` of this tree builds, after which the three runs
of the Oracle smoke test step pass and the script that runs them exits
0. The protocol checker reports OK for six flags and the five line
prefixes, the flag count reports six, driverpin, the citation check and
the cited-name check over README.md, AGENTS.md and Driver.lean pass, the
last after nine names that the new comments cite join its reviewed
allowlist (six C++ identifiers and three private definitions of
Journal.lean), and README.md and AGENTS.md are register-clean. The
seventeen reasons that Driver.lean can print equal those named in the
`?? exchange` paragraphs of README.md and AGENTS.md and the reason set
of the harness draft. The environment-wide axiom audit finds 3462
constants in 32 Ledger modules and none outside propext,
Classical.choice and Quot.sound, against 3361 for the preceding commit.
The import scan is clean, and the closure of Ledger.Driver holds neither
Ledger.Market, Ledger.PriceVector nor
Mathlib.LinearAlgebra.Finsupp.LinearCombination. The scan of added Lean
lines finds no forbidden construct, the register check of added lines is
clean, and the gate self-test is green. The oracle's default output over
the 3816 positive journals of the snapshot is byte-identical to the
recorded snapshot, and over the 469 negative journals byte-identical by
cmp to the output recorded at the preceding commit. The probe gate
reports PROBES OK for the one hundred and six S48 sets (280 verdicts)
and for every set of the units before S48 (846 verdicts), and the price
comparator over its 349 journals prints the same lines as at S46, 329
MATCH, 20 ISSUE-ONLY and no DIFF. Every journal that this message and
the comments of the round measure was run again under the oracle and
both C++ binaries, and each gives the result stated. The bisimulation
log against the C++ tree at f396a2e69 reports PASS 3816, FAIL 0,
SKIP:negative 489, SKIP:no-journal 77 and SKIP:python 2, with both
detector fixtures green; the unit's tail check exits 0.

The verification completed after the halt is recorded under
`impl/S48/resume-final-u1AP1h/logs`. The complete claims script exits 0
(claims.log); claims-audit.log records each nested return code and the
46 journal summaries of the last round, all agreeing with its recorded
measurements, and the separate decimal-comma case. The earlier claims
differ only in source line numbers, the added comment marker and the
recorded nondeterministic exits of `--file`. The build, static checks,
forced-target comparisons, price comparison, axiom audit, import-closure
check, Nix smoke test, probe gate and tail check each exit 0, as
verification.log records. The tail log records the same semantics tree
as at the halt, 1ebf20674bb0c5ac5e40867b3f287612bacd13c4. The five
repository files were preserved, not reworked, in this resumption.

Co-Authored-By: GLM (Z.ai) via pi <noreply@z.ai>
Pi-Session: 01a163ec-4fbc-72ee-a3ac-ca278238f2d3 (agent-deck ledger-semantics on hera)
The I8 review found that the pricing docstrings called an abstract
conversion law the correctness of C++ -V and -X reports. The law takes
its rates and valuations as parameters and specifies neither dated
observation selection nor price-path lookup. Describe that boundary
rather than a correspondence with the reports.

The constant-rate model accepts a nowhere-zero price function and uses
its ratios, not arbitrary independent rates. Fixed-price valuation is
compositional, as constModel and the valuation-functor algebra show.
The timed model keeps its base functional fixed, so a numeraire whose
price is fixed cannot show holding gains. Cite
timedConstRates_value_const and timedConstRates_no_holding_gain for
that limit, and point to timedPriceVector in Ledger/HoldingGain.lean
for the model whose price-vector functional varies with time. These
references follow the declarations committed in S06, S14 and S15.

Remove the unproved minimality claim from the law-basis heading.
Resolve R7 here: ValuationFunctor propagates balance from generators
on a generated lawful model; the generator hypothesis is not a functor
law. Narrow the historical extraction paragraph to the exclusion of
distinct commodity measurements, rather than claiming that every
journal with two commodities made the old structure uninhabited.
R18 already landed in S16, commit 5afc3be: AGENTS.md disclaims lot
matching. Its text is unchanged. Clean the rewritten docstrings in
the repository register, without changing imports, definitions or
proofs.

Verification is recorded under impl/S49/logs. The locked sem-build.sh
exits 0 after 1226 jobs (build.log). The cited-name check reports
LEAFNAMES OK on the four files (leafnames.log). All sixteen axiom
records lie within propext, Classical.choice and Quot.sound
(axioms.log and axioms-raw.log, exit 0). Text outside block comments
is byte-identical to HEAD in all four files (doc-only.log). The folded
text checks find none of the three stale phrases or R7's old sentence
(absence.log and absence-r7.log: count 0, grep exit 1). The rewritten
docstrings and AGENTS.md pass the register check (style-docstrings.log,
exit 0).

The unit tail exits 0 (tail.log): gate self-tests pass, the forbidden
scan finds nothing, the added-line register check is clean, and the
citation check passes. The default output is byte-identical over 3816
positive journals and, by cmp against S48, over 469 negative journals;
no baseline is replaced. The bisimulation reports PASS 3816, FAIL 0,
489 negative skips, 77 without a journal and 2 with Python, with both
ratchet bins empty and both detector fixtures green, over 4384 files
(consol3/logs/bisim-S49.out, exit 0). Tally A is thirteen passes above
the original plan because C01 added nineteen positive tests rather
than six; the remaining-scope plan records that adjustment.

Co-Authored-By: GLM (Z.ai) via pi <noreply@z.ai>
Pi-Session: 01a163ec-4fbc-72ee-a3ac-ca278238f2d3 (agent-deck ledger-semantics on hera)
The I13 review found that the prose gave the empty domain work that
its proof never uses and described debit-positive equity as growing
by a negative amount. Distinguish the scalar double_entry_invariant
from the coherent, per-commodity result flow_eq_zero_of_isCoherent,
and cite comp_breaks_coherence for the limit under composition.

Name the balance-sheet equation consistently, including finsetSum.
Separate its algebraic hypothesis from the disjoint, covering account
classes needed for the accounting reading. Include temporary revenue
and expense accounts in equity and require the whole journal to the
report date. Explain the credit-isomorphism negations and the closing
sign: revenue plus expense is negative in a profitable period, so
credit-positive equity increases. The ClosesBooks conditions and the
functor laws force the transfer.

Restrict Trivial's heading and inhabitation claim to the exhibited
core structures, resolving R8. Name ClosingDemo.demo_closesBooks as a
later inhabitant with nonzero revenue. Narrow the prepared draft's
consistency sentence to satisfiability of the listed structures; it
does not establish every theorem's hypotheses. R7 was already resolved
in S49 (c829208); keep that ValuationFunctor docstring and check that
its old phrase is absent. No import, definition or proof changes.

Verification is recorded under impl/S50/logs. The locked sem-build.sh
exits 0 after 1226 jobs (build.log). The cited-name check passes on all
four files (leafnames.log), and all 23 axiom records are within the
three-axiom budget (axioms.log and axioms-raw.log, exit 0). Text outside
block comments is byte-identical to HEAD in the four files, and the
six rewritten docstrings pass the register check (doc-only.log and
style-docstrings.log, exit 0). The folded-text check finds none of the
six stale phrases, including R7 and R8 (absence.log: count 0, grep
exit 1).

The unit tail exits 0 (tail.log): the gate self-tests, forbidden scan,
added-line register check and citation check pass. Default output is
byte-identical over 3816 positive journals and, against S49, over 469
negative journals; no baseline is replaced. The bisimulation reports
PASS 3816, FAIL 0, 489 negative skips, 77 without a journal and 2 with
Python over 4384 files, with both ratchet bins empty and both detector
fixtures green (consol3/logs/bisim-S50.out, exit 0). Tally A is thirteen
passes above the original plan because C01 added nineteen positive
tests rather than six, as the remaining-scope plan records.

Co-Authored-By: GLM (Z.ai) via pi <noreply@z.ai>
Pi-Session: 01a163ec-4fbc-72ee-a3ac-ca278238f2d3 (agent-deck ledger-semantics on hera)
The README made balance a property of morphism endpoints and ruled
out compositional pricing. The checked theory instead propagates a
balance hypothesis on entries and admits fixed-price valuations.
Replace those claims together, as D1 requires, after the declarations
that support their replacements have landed.

The invariant paragraphs address I3 through balanced_generators_balance,
free_double_entry and flowsFin_two_ends. I8 names the separate
degeneracy witnesses, priceValuation and the two holding-gain results.
The tower paragraph distinguishes the signed carrier from the group
of T-accounts (I4); the Ellerman section relates the constructions,
decoding and hand-transcribed goldens (I11, I10, I7, I12, G2). It
states the balance-sheet reading's coverage and date conditions (I13)
and distinguishes default signed rows from the opt-in gross columns.

The implementation contract distinguishes weights from own quantities
(I5), restricts the bucket to one posting (G1), names implicit exchanges
(I2), and scopes non-permissive balancing to the assignment witness
(I6). It records S32's decimal-tie rule at src/amount.cc:208-245.
The five residual classes agree with both balance docstrings. The
prepared prose is qualified for net endpoint parts, relative prices,
lot-price passes, and display precision; it makes no claim of exact
agreement outside the witnessed conventions.

The equity paragraph describes the offset calculation over posting
subtotals, not account net balances, following
src/filters.cc:1673-1688,1960-1961. The C++ build and 3.4.1 release
both omit the $5 paren-virtual subtotal from a mixed account's offset
and include nonvirtual timeclock postings
(S51/fix-r1/equity-probes.log). Neither guarantees an accepted opening
entry: both accept virtual-cost.dat and emit its offset, but reject
the emitted journal with "Transaction does not balance", exit 1,
under default and isolated configurations
(S51/fix-fess1/equity-roundtrip.log). The README claims only the offset
formula, not that it makes the emitted entry balance.

I1's README prose lands here rather than with I1 because it needs the
later endpoint theorems, the sequencing deviation required by D1.
Counts, coverage and provenance remain for S54. No executable rule,
theorem, import, pin or output protocol changes.

The full build exits 0 with 1226 jobs and no warnings
(S51/fix-fess1/build.log). The cited C++ tests 2005, 1784 and cmd-equity
pass all eleven command checks, each run separately through
cpp-regress.sh, exit 0 (S51/fix-fess1/cpp-witnesses.log).
S51/fix-fess1/checks.log records CLEAN, LEAFNAMES OK, CLASSLISTS OK, the
paragraph helper's green self-test, resolved paths, the four required
phrases, both holding-gain citations and six flags.

tail_unit S51 A exits 0 (S51/fix-fess1/tail.log): the added-line gates
and their self-test pass, citations resolve, all 3816 positive
snapshots match the baseline, and all 469 negative snapshots match
S50. The latter records the oracle's rejection of 2005.
S51/fix-fess1/bisim-S51.out records PASS 3816, FAIL 0, empty
out-of-scope and cpp-parse bins, and both detector fixtures green over
4384 files. Tally A is thirteen above the plan because C01 added
nineteen positive tests rather than six.

Co-Authored-By: GLM (Z.ai) via pi <noreply@z.ai>
Pi-Session: 01a163ec-4fbc-72ee-a3ac-ca278238f2d3 (agent-deck ledger-semantics on hera)
The introduction called the groupoid enriched over commutative
monoids and placed Free and Pacioli in the executable layer. The
checked construction instead gives each hom-set a commutative
monoid without the usual compatibility with composition; Free and
Pacioli provide the specification's representation tower.

Replace only the mathematical-object and two-layer sections for I11.
The repository's D16 is witnessed by zero_absorption_degenerates,
left_distrib_degenerates and right_distrib_degenerates in Pacioli.
The text names those laws by content rather than ruling out every
relation between aggregation and composition. R12 was already
resolved by S05: Groupoid no longer calls the structure a CMon
enrichment. Its absence check still gives 0 (S52/checks.log).

Adapt the prepared draft to the definitions that landed. Describe
only automated-transaction rules as the separate derivation system;
Derivation does not model periodic transactions, and its definitions
do not give assertions their own rules. State Gen's role separately:
it supplies journals, never expected results. The module index and
imports remain byte-identical for S53, as S52/checks.log verifies.

The full build exits 0 with 1226 jobs and no warnings
(S52/build.log). S52/checks.log records LEAFNAMES OK, CLEAN for the
replaced section, the three absence counts and changed-import count
at 0, the exact edit-boundary check, and a clean import scan.
S52/axioms.log records AXCHECK OK for five supporting theorems,
within the three-axiom budget. No definition, proof, executable rule,
pin or output protocol changes.

tail_unit S52 A exits 0 (S52/tail.log): the added-line gates and their
self-test pass, citations resolve, all 3816 positive snapshots match
the baseline, and all 469 negative snapshots match S51. The
bisimulation records PASS 3816, FAIL 0, empty out-of-scope and
cpp-parse bins, and both detector fixtures green over 4384 files.
Tally A is thirteen above the original plan because C01 added
nineteen positive tests rather than six.

Co-Authored-By: GLM (Z.ai) via pi <noreply@z.ai>
Pi-Session: 01a163ec-4fbc-72ee-a3ac-ca278238f2d3 (agent-deck ledger-semantics on hera)
Nine specification leaves and three proof-only join modules were
absent from the index and the layer tables. List all 32 modules in
AGENTS.md, Ledger.lean and the README, and import the twelve additions
from the index. Gen remains separate because it defines its own main.
The direct-import tables include Driver's import of Register.

This coordinated inventory narrows D15: the AGENTS tables and the
Ledger.lean index are updated once here rather than with each leaf.
S08's interim sentence preserved the distinction between the two
executable chains until this final account. Retain that distinction
and state that the run path is Driver's import closure.

List two AGENTS.md rule changes for the user's approval: join modules
hold only definitions and theorems used for proofs, are imported only
by Ledger.lean, and stay off the run path; an import change updates the
direct-import list in the same commit. The R30 approval checkpoint
records the exact S53 edits if either amendment is declined. Approval
has not been recorded as given.

Adapt the prepared index to the landed graph: specification leaves
also import one another, not only through join modules. Describe the
journal engine as witnessed conventions with recorded departures,
not as an unqualified claim of C++ fidelity. Keep the narrowed Trivial
entry and distinguish Oracle's netFlow and netValue from Pacioli's
flow; Register names gross debit and credit totals and posting rows.
No definition, proof, executable rule, pin or output protocol changes.

The full build exits 0 with 1226 jobs and no warnings (S53/build.log).
S53/inventory.log records layers.py OK, the exact import-table check,
all 32 names in each document, 31 index imports, CLEAN and LEAFNAMES
OK. S53/text-checks.log records the required phrase counts, the three
reviewed homes of "the Pacioli group", and unchanged text outside the
inventory sections. The axiom audit covers 3462 constants in 32
modules with none outside the three-axiom budget; Lean reports every
leaf and join absent from Driver's closure (S53/axioms-closure.log).

tail_unit S53 A exits 0 (S53/tail.log): the added-line gates and their
self-test pass, citations resolve, all 3816 positive snapshots match
the baseline, and all 469 negative snapshots match S52. The
bisimulation records PASS 3816, FAIL 0, empty out-of-scope and
cpp-parse bins, and both detector fixtures green over 4384 files.
Tally A is thirteen above the original plan because C01 added
nineteen positive tests rather than six.

Co-Authored-By: GLM (Z.ai) via pi <noreply@z.ai>
Pi-Session: 01a163ec-4fbc-72ee-a3ac-ca278238f2d3 (agent-deck ledger-semantics on hera)
The README called the balance semantics complete without naming the
C++ revision or the known departures outside its corpus. State the
4364-file corpus and comparator revision, 76b9dc2b2: 3796 comparisons,
489 negative files, 77 without a journal and two with Python. The
S54-base bisimulation log reports these bins, FAIL 0 and both detector
fixtures green, exit 0, using the C++ build at f396a2e69. D24 is kept
in substance: the merged-suite counts and the description of the new
C++ comparisons wait for the post-PR-B follow-up. The opt-in outputs
compute gross totals and values for a comparator; this change claims
no standing comparison of them. Lot matching remains unspecified.

Document the existing reason stem for transaction imbalance in README
and AGENTS: line N: unbalanced transaction, with the periodic prefix
for a template and N the header line in the include-spliced text.
This is a binding rule change submitted for the user's approval, not
approval already given. Item 4 of approval-checkpoint-S08.txt records
what to remove if it is declined and the C04 classifier to review.
The protocol probes give the stem for ordinary, periodic and included
entries and exclude an include failure, as protocol.log records.
Other errors retain their own wording. No output changes.

The coordinator's R3 decision replaces the proposed duplicate index
of departures with comments beside their rules and a repository grep.
The README points to the fail-open and fails open markers, including
impliedExchange, zeroTestPrec and the automated-transaction check,
without claiming completeness for the language. This expands S54's
file list to comments and docstrings in Journal, Parse and Driver,
as the remaining-scope plan requires. The marker search counts 84
lines before this change, 83 in Journal and one in Driver, and 95
after it, 93 in Journal, one in Parse and one in Driver. These count
lines, not distinct shapes; some describe wrong rows or missed
refusals rather than accepted invalid journals.

Record the missing departures: nested lot notes, a total lot price on
zero units, annotated costs against plain amounts, an elided posting
beside two zero annotated keys, an annotated assignment target over a
held balance, and truncated rounding where C++ truncates. Mark the
existing acceptance departures in the field and candidate docstrings
as well. The comments cite annotate.cc:203-257, amount.cc:1911,
xact.cc:325-333 and 1014-1015, textual_xacts.cc:807-831 and 908-910,
and report.cc:1159-1168. Record the mixed-commodity and alphabetic
prefix expression rejections, converted-unit display precision and
bare rational display without calling those fail-open. The stale
precision copy, fuzzy lot gain, fractional-cost zero tests and
bracket-virtual gain already had comments; their witnesses still
show the recorded departures. Periodic templates still teach nothing.

The truncated comment limits literal precision teaching to the ordinary
posting-amount witness. Cost and balance assertion or assignment
expressions use PARSE_NO_MIGRATE (textual_xacts.cc:693-694, 759-760).
Four precision probes confirm the distinction on both C++ binaries:
a later quotient displays five places after the posting amount, but
three after a cost, assertion or assignment. The output is recorded
in fix-r1/trunc-precision.log; the evaluator is unchanged.

The 34 balance witnesses give their expected direction on both the
C++ build and Ledger 3.4.1, as probes.log and probe-results.json
record, and the oracle output before and after is byte-identical by
cmp. In particular, the old half-way display witness now agrees:
S32 removed that departure, and Y shows -26 EUR on all three runs.
The missing q8, t1 and equiv0 witnesses were reconstructed from the
recorded shapes, not recovered verbatim. Four value-expression
placement probes give 200 before the postings or after the holding,
but $1700 after its counter-posting, on both binaries; the
value-expr-position.log records this, and Result.valueExprAll now
states the condition. These local C++ runs read the existing init
file, as the plan's comparison commands do.

The private zeroTestPrec citation required one local entry in the
scratch name allowlist, after checking its declaration. No comparator,
expectation or acceptance condition was relaxed. The scratch
comment-only check was corrected to recognize character literals;
it now confirms identical non-comment code in all three Lean files.
The documented counts, stem occurrences and removed overclaims pass
the prose assertions; protocol, six-flag, name and register checks
pass, as docs.log and docs-assertions.log record. The driver pin also
passes. R18 was already resolved by S16 and is retained.

The locked full build exits 0 after 1226 jobs with no warning in
build.log. The S54 tail exits 0: the gate self-test is green, forbidden
constructs are absent, style and citations pass, and the positive
snapshot is identical over 3816 journals. The 469-journal negative
snapshot is byte-identical to S53's. The S54 bisimulation log reports
PASS 3816, FAIL 0, SKIP:negative 489, SKIP:no-journal 77 and SKIP:python
2 over 4384 files, with the other bins empty and both detectors green.
This retains tally A, including C01's thirteen additional positives.
R5's NEG and same-entry pre-measure is not repeated: S38 recorded
35 and FAIL 0, later boundaries preserve the negative output, and
this unit changes no executable definition. The remaining-scope plan
permits that reliance; this is not a fresh rejection-side measurement.

Co-Authored-By: GLM (Z.ai) via pi <noreply@z.ai>
Pi-Session: 01a163ec-4fbc-72ee-a3ac-ca278238f2d3 (agent-deck ledger-semantics on hera)
Main advanced eleven commits past f5d891e while the series ran, three
of which touched the files the series had rewritten. The merge keeps
the series engine wholesale and ports main behaviour into it, with
each rule keeping its witness:

- Ledger/Driver.lean gains main globComponent
  (d5c7eec, regress/include-glob-escapes, include-glob-literals,
  coverage-mask-glob-escape), and globPaths matches a component
  through it instead of the series isGlob/globRx pair, whose
  departures from assign_glob the merge removes; the docstrings
  record the two departures that remain.
- commodityDirectiveSymbol delegates to Parse.headerSymbol
  (0a743b6, regress/3313), so a bare symbol ends at a semicolon and a
  backslash quotes the next character.
- Alias lines in a commodity block strip the comment through
  Parse.stripCommodityComment (0a743b6, regress/3313_2), and a format
  sample takes a bare symbol through Parse.quoteBareSymbol (1115499,
  regress/3312).
- Account names, at the header, at @account and nested in a block,
  end before a hard-separated comment through Parse.headerAccount
  (8342e12, regress/3320, 3320_3, 3320_4).
- A value expression written as `commodity == "N"` unquotes the
  quoted symbol (e633730, regress/3241).
- PostSpec threads swapCostVal and gainVal through finalizeXact
  canonicalisation, which main 6c6942e added; the series own
  swap-gain booking already covers the regress/3314 family, and all
  of main witnesses pass on the merged oracle.

The merged oracle passes all twenty-nine of main new witness files
(3313, 3313_2-3313_5, 3314 with its variants, 3320, 3320_2-3320_4,
include-glob-diacritics, include-glob-escapes, include-glob-literals,
cov9-include-glob) against their expected output, builds green under
the no-sorry warnings-as-errors lakefile, and Ledger.Journal.run
depends on propext, Classical.choice and Quot.sound only.

Co-Authored-By: GLM (Z.ai) via pi <noreply@z.ai>
Pi-Session: 01a163ec-4fbc-72ee-a3ac-ca278238f2d3 (agent-deck ledger-semantics on hera)
@jwiegley
jwiegley merged commit 65af4c4 into main Nov 7, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant