Rollup Workbench/Invented sample data/No ERP connection12/12 invariants

Mixed-depth class hierarchy · revenue by class

One question. Three answers. One is right.

When invoice lines are tagged at different depths of a class tree, some to a parent, some to a child, some to a grandchild, the question “what is this segment’s revenue?” has three plausible answers. Two are wrong, in opposite directions, and neither announces itself. Every figure below is computed on the server from 45 invoice lines across 15 classes, on every request.

Segment focus

Ask the same segment three times

Pick one of the four segments. The three cards are the three ways a report can answer, computed from the same lines. The error is not a rounding artefact; it is structural.

Exact-class grouping$1,240,000-85.3% · -$7,170,000Understates

Descendant revenue never rolls up.

Naive prefix rollup$20,820,000+147.6% · $12,410,000Double counts

Subtotals get added to the details they contain.

Closure rollup$8,410,000exact · reconciles to the ledgerCorrect

Every line counted once, at one class.

Reconciliation

The test a rollup either passes or fails

Add the four segment totals together. A correct model returns the general-ledger total exactly. Switch the reporting mode and watch the gap open. This refetches /api/rollupeach time, so the arithmetic is the server’s, not the page’s.

Closure rollup (correct)gap $0
sum of 4 segments $18,715,000GL total $18,715,000

Closure rollup (correct)

subtree(n) = direct(n) + the subtree of each child. Every line is counted exactly once, at exactly one class, and rolls up its ancestor chain once.

Class tree

Where the revenue actually sits

Interior classes carrying revenue are marked. That mark is the whole problem: a tree where only leaves are tagged has no rollup bug at all, which is why leaf-level spot checks come back clean.

Reported under: Closure rollup (correct)GET /api/rollup?mode=closure
ClassTagged directlyReportedCorrectError
Aerospace Componentstagged$640,000$5,180,000$5,180,000$0
···· Actuators$2,240,000$2,240,000$2,240,000$0
···· Fastenerstagged$380,000$2,300,000$2,300,000$0
········ Titanium$1,920,000$1,920,000$1,920,000$0
Aftermarket Servicestagged$95,000$2,315,000$2,315,000$0
···· Field Service$1,340,000$1,340,000$1,340,000$0
···· Spare Parts$880,000$880,000$880,000$0
Industrial Pumpstagged$1,240,000$8,410,000$8,410,000$0
···· Distribution$1,510,000$1,510,000$1,510,000$0
···· OEMtagged$420,000$5,660,000$5,660,000$0
········ Centrifugal$3,180,000$3,180,000$3,180,000$0
········ Positive Displacement$2,060,000$2,060,000$2,060,000$0
Process Valvestagged$210,000$2,810,000$2,810,000$0
···· Ball$1,470,000$1,470,000$1,470,000$0
···· Gate$1,130,000$1,130,000$1,130,000$0

Proof

Twelve invariants, recomputed on demand

Nothing here is compared against a stored expected value. Each check recomputes from the transaction list and asserts a property that must hold for any correct rollup over any hierarchy. This is the harness that would ride along with the real fix.

Method

What this is, and what it is not

The data is invented

Fifteen classes, 45 invoice lines, and a $18,715,000 book of revenue that I made up. There is no ERP connection here and no customer data of any kind. The dataset exists to make the bug reproducible.

The engine is the point

Correct aggregation over a mixed-depth tree is tool-independent. The closure recurrence and the invariant suite transfer to a Dataset, a Workbook, a warehouse view, or a SQL CTE without changing shape.

Currency is integral

Every amount is stored and summed as integer cents, so no rounding drift can enter a rollup and be mistaken for a hierarchy error. integer-cents asserts it.

Verify it yourself

Open the network tab, switch reporting modes, and watch /api/rollup recompute server-side. Then call /api/selftest directly.