Measured Behaviour
Published
Every failure we have measured in this series ends in an answer that is wrong: a value missing, a party mispaid, a guard that stopped matching, a number in the wrong unit. A periodisation error is none of those. Every amount is right, every party is right, every statement foots - and the only figure a royalty department reconciles is identical to the cent.
A year of statements, and nothing wrong with any number in them
We built a royalty run the way one is really shaped: forty transactions across a year, four quarterly statement periods, revenue recognised on the day of sale, a forty-five day reporting lag because that is roughly how long digital services take to report and remit, a twenty per cent reserve held against foreign receipts and released two quarters later, and a minimum statement below which a quarter rolls into the next one.
The transactions are deliberately clustered toward the ends of their quarters. A sale in the middle of a quarter cannot move across a boundary, and a fixture made of those would measure nothing at all.
The correct run recognises 759,000.00, pays 711,960.00 across four statements, holds 47,040.00 in reserve and carries nothing. Nothing falls beyond the horizon. The rightsholder's own rule - everything recognised must be paid, held or carried, and no period may pay out more than the run recognised - passes.
Protected with default settings on five profiles - the ES5 target, the modern target, both emit-gate configurations and the string-encoding profile - the output was byte-identical. What follows required member renaming to reach names on the configuration object.
One name, and 159,960.00 dollars leaves the year
recognitionBasis decides which day a sale is recognised on. This contract recognises on the SALE day: the quarter the record was bought in is the quarter it is paid for, whatever the service takes to remit. The library recognises on RECEIPT, which is the conservative choice, what most royalty systems actually do, and the only reading a library that has not seen your contract can defend.
Renaming it adds the forty-five day lag to every transaction. Measured: the first quarter recognises 32,000.00 instead of 180,400.00 and pays nothing at all instead of 157,800.00. The year pays 552,000.00 instead of 711,960.00. Eight transactions worth 158,600.00 land past the last period end and appear on no statement in the run.
And the recognised total is 759,000.00 in both arms. To the cent. The reconciliation reports reconciles=true in both. The rightsholder's rule passes in both, because everything the run recognised really was paid, held or carried - it was simply carried rather than paid.
There is no wrong number anywhere in the broken run. Every amount is arithmetically correct. Every rightsholder is the right rightsholder. The only thing that moved is which statement a correct amount is on, and the difference between statements is a payment date. Nobody reconciles a payment date.
The half that looks inert is the half that hides it
reportingLagDays is the distance between the two bases. Renaming it on its own is completely inert: on a sale-day basis the lag is never consulted, so every figure in the run is byte-identical to the correct one. It is exactly the arm a reviewer closes as cosmetic.
Rename both names together and the money line of the run is byte-identical to the correct one as well - 759,000.00 recognised, 711,960.00 paid, 47,040.00 held, nothing beyond the horizon, all four statements to the cent. With no lag, receipt day and sale day are the same day, so the two bases coincide and the outage disappears.
So the narrow, careful pattern is the catastrophic one and the broad, sloppy one is safe. If this area had been tested only with a realistic 'rename the whole options object' regexp, the 159,960.00 would never have appeared. The dangerous cells are not at the wide end.
The other three ways a period can move
Renaming periodEndDays collapses four quarterly statements into the library's single annual one. The year pays 661,520.00 instead of 711,960.00 and the rightsholder waits twelve months for what the contract pays quarterly. Recognised total: identical.
Renaming holdbackReleasePeriods means the reserve is never released. Still held goes from 47,040.00 to 97,480.00 and the year pays 50,440.00 less, with every quarter's recognised figure untouched.
Renaming holdbackPct goes the other way: the whole reserve is released, the year pays out 759,000.00 in full, and the label has no cushion against a foreign society restating its line - which is the entire reason the reserve exists.
Renaming the territory field on the transaction records is subtler, because the reserve in this contract is scoped to foreign receipts. With no readable territory every receipt is treated as foreign: still held goes to 77,600.00 and the year pays 681,400.00. The reserve is correct, the scope of it is not, and no total moves.
What fails loudly, for contrast
Renaming saleDay - the day itself - is refused before the run computes anything: the engine cannot place a transaction on an unreadable day and says so. That contrast has now held in every area of this series. Losing the VALUE fails closed and loudly; losing the MEANING of the value - which day it counts on, which population it belongs to, which unit it is in - fails open and silently.
minimumStatementUsd was inert on every money figure here, because no quarter in this run falls short of it. That is an honest negative about the data rather than evidence about the name: on a run with a weaker quarter it is the mechanism that converts a timing shift into a payment that does not happen at all.
Why this shape is worth naming
A reconciliation compares what came in against what went out. Over the life of a contract, a periodisation error compares equal on both sides, because the sum of the periods is the same however the boundary falls. The check that would see it is a period-by-period one against the source records, and that is precisely the check nobody runs, because it has never caught anything.
The consequence is not cosmetic. A statement is a payment. Revenue pushed a quarter later is money a rightsholder does not have for three months; a reserve released a quarter later is a reserve held longer; revenue pushed past the last period on the run is on no statement at all and appears as a carry-forward, which is a line that appears on a correct statement too.
If your contracts specify a recognition basis, assert it at start-up against the value the library reports back, rather than assuming the object you passed is the object it read. That is a single line, it costs nothing, and it also catches a mistyped config file and a key dropped over a serialisation hop.
What we are not claiming
This is not a defect in the obfuscator. All five protection profiles produced byte-identical output on this file, and on the four other areas measured in the same session. The failures required member renaming pointed at names that cross into an unprotected library.
It is also not an argument against protecting a royalty system. It is an argument for scoping the rename so it cannot reach a third-party options literal, which is a configuration question rather than a trade-off - the mechanics are in Protect Members. If you operate this under an audited control framework, ISO 27001 and SOC 2 covers what an auditor will expect you to be able to show about it.
Frequently asked questions
Does obfuscation move revenue between accounting periods?
Not on its own. Protected with default settings on five profiles the run was byte-identical. The failure we measured required member renaming to reach the recognition basis on the configuration object, which then reverted to the library's own default.
How much money moved?
The year paid 552,000.00 instead of 711,960.00 - a shortfall of 159,960.00 - and eight transactions worth 158,600.00 landed past the last period end and appeared on no statement in the run.
Why didn't the reconciliation catch it?
Because the recognised total was 759,000.00 in both arms, identical to the cent. A reconciliation compares what was recognised against what was paid, held and carried, and a periodisation error moves amounts between those buckets without changing their sum.
Is there any figure that would have shown it?
The per-period figures, compared against the source transactions. In our run the first quarter recognised 32,000.00 instead of 180,400.00 and paid nothing at all. Nobody runs that comparison, because on a correct run it has never found anything.
Why was renaming two names safer than renaming one?
The reporting lag is the entire distance between the two recognition bases. Rename it as well and the bases coincide, so the run is byte-identical to the correct one. The narrow pattern is the dangerous one here, which is the opposite of the usual intuition.
What about the reserve and the period calendar?
Renaming the release interval left 97,480.00 held instead of 47,040.00 and paid 50,440.00 less. Renaming the period end days collapsed four quarterly statements into one annual one and paid 661,520.00. Recognised totals were unchanged in both.
What is the mitigation?
Scope MemberRegexp so it cannot reach a third-party options literal, and assert the recognition basis the library reports back at start-up rather than assuming the object you passed is the object it read. See Protect Members.
Related reading