Repository navigation
Ellerman's double-entry multiset ledger: machine-checked semantics and executable oracle (S01-S54) - #11
Merged
Conversation
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
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)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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
propext,Classical.choice,Quot.sound), withsorryturned into an error by the lakefile.lake env lean --run); no compiled binary sits between the checked definitions and what executes.--grossprints per-account gross debit/credit totals;--exchange COMMvalues each account in one commodity at midnight of the--nowdate 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.netValueon the oracle's market vector, proved in aMarketjoin module (S47), and the balance-rejection protocol stem documented (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
.testfiles) 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 branchesjohnw/ellerman-reviewandjohnw/ellerman-review-bof 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 buildgreen (the build is the proof check; warnings are errors).--exchangeand--grossare opt-in; the default output of the driver is byte-identical to its pre-series behavior on the whole corpus (3816 journals, tally A).