Skip to content

0013 — Fulfilment is summed on read, never stored

Status

Accepted.

Context

Delivery.fulfils points at an Agreement. Someone eventually has to ask the obvious question — how much of this agreement has actually arrived? — and the obvious implementation is a delivered_kg column on the agreement that ingest increments.

That column would be wrong within a week. Deliveries are superseded when a weight is corrected and retracted when they were logged against the wrong farmer, so any counter needs a decrement path, and a decrement path in an append-only store is a second, mutable copy of the truth that nothing can audit. Offline sync makes it worse: the increment order is whatever order the phones happened to reconnect in.

Decision

The kernel computes fulfilment from the delivery rows on every read. It is attached to the agreement's RecordView as fulfilment and stored nowhere.

Aggregation happens in SQL — count, sum, and two filtered counts grouped by fulfils — so a busy agreement does not pull every delivery across the wire, and a page of agreements costs one query rather than one per agreement.

Superseded and retracted deliveries are excluded, using the same predicates the rest of the read path uses. A corrected delivery counts once, at the corrected weight. A retracted one does not count at all.

Three things the shape insists on saying

unconvertible and incomplete. If a delivery's quantity never reached kilograms, it contributes nothing to the sum. Reporting delivered_kg alone would then quietly understate, and a lender reading "62% fulfilled" would have no way to know the real figure is higher by an unknown amount. incomplete says the total is a floor.

committed_kg may be null. If the agreement's own quantity_committed was never normalized, the shortfall is unknowable, not zero. outstanding_kg is null rather than misleading.

outstanding_kg is not clamped. Over-delivery reads as a negative number. Clamping at zero would hide the single most interesting thing an agreement can tell you.

confirmed is counted separately from deliveries because arrival and agreement are different facts, and the gap between them is exactly what a lender is buying.

Consequences

This is display-time aggregation over records the caller can already see, which is the same boundary decision 0009's consent guard draws — derive() runs after guard(), so nothing is summed for a caller who was not entitled to the rows.

It is deliberately not a balance and not an obligation. It sums physical quantities against a stated commitment. Money stays out (invariant 6), and B5's obligation summing is a separate question answered separately.

If this becomes slow, the answer is a rollup keyed on the agreement that can be rebuilt from the log at any time, not a counter that cannot.