Package lots is the cost of goods consumed as a pure function of how much of a lot has been consumed.
A realm stores receipts of a stock-keeping unit — each a quantity and what was paid for it — and asks this package what the goods it just shipped cost. Nothing here reads the chain, holds coins, or knows what a ledger is, so a realm can test a year of its own inventory costing without a node.
Example
1p, _ := NewPool(FIFO, 64)
2p.Receive(100, 2500) // 100 units for 2500 in total
3p.Receive(50, 1400) // a second, dearer receipt
4cogs, _ := p.Consume(120) // what the shipment cost: DERIVED
5onHand, _ := p.CostOnHand() // what is left is worth
The amount is never a parameter
This is the thesis the rest of the package serves. A caller says how *many* units left the shelf; it never says what they cost. The cost is computed from the lots the units came out of, so a realm cannot post a cost of goods sold that disagrees with the inventory it holds — not because it is careful, but because there is no argument through which the two could be made to disagree.
[p/.../depreciation] makes the same move for a capital asset, where the charge is a function of periods elapsed. Here it is sharpened: a depreciation schedule is fixed at acquisition and a caller supplies only time, while an inventory pool is mutated by every receipt, and the function still holds.
Why the cost is a difference of a pure function, and not a running average
The obvious implementation keeps a remaining quantity and a remaining cost and, for a shipment of n units, charges
Example
1remainingCost * n / remainingQuantity
It is wrong in a way that is specific and quiet. The division truncates, and what it truncates is retained in the lot rather than charged, so the cost charged for the first k units out of a lot **depends on how those k units were sliced into shipments**. Measured, with the whole lot's life evaluated:
Example
1lot(Qty=7, Cost=10) consumed as [1,1,1]
2 naive -> [1, 1, 1] total 3
3 telescoping -> [1, 1, 2] total 4
Nothing here is an accumulator. Lot.CostAt is a pure function of the cumulative units consumed,
Example
1CostAt(k) == Cost*k / Qty
and Lot.Take is its difference at two points. So charges **telescope** —
Example
1Take(a→b) + Take(b→c) == Take(a→c)
— by construction rather than by careful remainder arithmetic, and there is no stored remaining cost that could drift from the lot at all. A realm that loses track of what it has charged can recompute it from the quantities.
It **mis-times** cost; it does not lose it. Over a lot's whole life the naive form charges exactly Cost, because the shipment that exhausts the lot has `n == remainingQuantity` and so charges the whole remainder. Saying "it loses money" would be a stronger and false claim, and a reader who checks the totals would correctly conclude the warning was overstated and ignore the real defect. The real defect is that a *period's* cost of goods sold, and the inventory value carried at any moment before exhaustion, both depend on shipment slicing — which is a misstatement of both the income statement and the balance sheet, for a quantity nobody chose.
Measured by differential sweep against exact arithmetic, over quantities 1 to 12 and costs 1 to 50, evaluating **every ordered composition** of the quantity (so every way of slicing a lot's whole life into shipments):
Example
1compositions evaluated 204750
2naive path-dependent 108320 over 260 of 600 (Qty,Cost) pairs
3telescoping path-dependent 0
4naive conservation failures 0
5telescoping conservation failures 0
6worst naive mis-timing 3 units of cost, at Qty=12 Cost=6
"Path-dependent" has a definition, because the count is worthless without one: a composition is path-dependent if the cumulative cost charged after some prefix of it differs from the cumulative cost the same number of units would have carried had they shipped one at a time. The two zeroes on the conservation lines are the evidence for the paragraph above — the naive form reaches the right total every time and gets there wrongly — and the worst case is half of a small lot's cost, not a rounding crumb.
The weighted-average method re-anchors at every receipt, and must
Under WAVG a receipt merges into the single live lot: the merged lot's quantity is what remained plus what arrived, its cost is what remained *was worth* plus what arrived cost, and its consumed count resets to zero. The reset is not bookkeeping convenience and is not a defect — it is forced. CostAt is a function of the lot's own Qty and Cost, and a receipt changes both, so a consumed count measured against the old pair means nothing against the new one. Telescoping therefore holds **within an anchor epoch**, and across the re-anchor what holds instead is exact cost neutrality: the merged lot's cost is the outgoing lot's remaining cost plus the receipt's, with nothing dropped and nothing invented. Pool.CrossFootCost is what proves it, and it is asserted over receipt/consume interleavings in the suite.
The asymmetry this creates is worth knowing before choosing a method: WAVG can **refuse** a receipt that FIFO and LIFO accept, because the merged lot must satisfy the overflow precondition `Cost <= maxInt64/Qty` that two separate lots would each satisfy on their own. See Pool.Receive.
An error return does not revert a Gno transaction
Only a panic reverts. So every mutating method here stages its whole effect — a copy of the lots it will touch, and the total it will charge — and commits only once the last step has succeeded. A method that returned an error after mutating half the pool would leave a realm holding inventory whose cost had been partly charged, with the realm's own error handling unable to undo it, and the realm would have no way to detect it short of a cross-foot. This package does not panic on caller error; it returns errors and keeps the pool untouched when it does.
What this package does not claim
- **No novelty for FIFO, LIFO or weighted average.** They are textbook and older than software. What was not found in the searched sources is an on-chain integer implementation with a path-independent cost function; see DISCOVERY.md.
- **Nothing about money.** A lot cost is a number in a record. This package never sees a coin, never holds value, and a realm using it must not infer custody from it.
- **No specific identification.** The four methods a textbook lists are three here. Specific identification — the shipper naming which lot a unit came from — would make the consumed amount depend on a caller-supplied choice, which is the one thing the thesis above rules out. A realm that needs it should hold one pool per identified lot.
- **No units of measure, no currency, no SKU identity.** A quantity is an integer and a cost is an integer. Deciding that they are kilograms and thousandths of a ugnot, and that this pool is the one for SKU "A-1", is the realm's job.
- **No negative inventory, no write-offs, no revaluation.** Consuming more than is on hand is ErrInsufficient, never a negative lot. Shrinkage and lower-of-cost-or-market are not here; a realm that needs them needs a fourth and fifth account and should say so in its own chart.
[p/.../depreciation]: /p/g1ut6uspuh73e02yauxpmyt8g3wwddaq8utagvm3/depreciation