Measured, not asserted
Published
Percentage rent is charged on sales above a breakpoint, and there are two standard ways to arrive at that breakpoint. The natural breakpoint is the annual base rent divided by the percentage rate. The stated breakpoint is whatever the parties negotiated, and it is frequently higher because a tenant who paid for a fit-out negotiates room before the overage starts. A library that cannot read the stated figure does not fail: it derives the natural one, which is real, published and entirely defensible -- just not the one in this lease. This test measured what that costs.
A threshold that is derived rather than missing
The fixture is one trading year for a store: twelve monthly sales lines plus two carve-out lines the tenant reports separately. Countable sales are 5,412,000.00. Reported sales including the carve-outs are 6,027,500.00. The lease states a breakpoint of 4,100,000.00, sets an ordered, cumulative tier table at 6% then 4.5% then 3%, and carves out returns, gift-card issuances, employee sales and orders shipped from the store for an online sale.
Correct: breakpoint=4100000.00 via stated, OVERAGE=1312000.00, PERCENTAGE-RENT-DUE=71040.00.
Rename the option carrying the stated figure and the engine derives one instead: breakpoint=3150000.00 via natural(annual base rent divided by 6%). That number is not an artefact. It is the annual base rent of 189,000.00 over the 6% rate, and it is the figure every leasing textbook calls the natural breakpoint. The result is OVERAGE=2262000.00 and PERCENTAGE-RENT-DUE=109860.00 -- 54% more overage rent on identical sales.
The landlord's settlement rule prints sales-check(12 of 14 month(s) counted, 2 excluded by the lease) in both runs, word for word, and foots=true in both. Twelve months are counted in both. Two are excluded in both. Every reported line is accounted for in both.
A clean half is not evidence about the pair
Renaming the annual base rent on its own is completely inert: PERCENTAGE-RENT-DUE=71040.00, byte-identical to correct, because the stated breakpoint still wins and the base rent is never consulted. That is precisely the finding a reviewer closes as cosmetic.
Rename the stated breakpoint and the base rent together -- one pattern, two names -- and the engine has nothing to derive from either: breakpoint=0.00 via natural, OVERAGE=5412000.00, PERCENTAGE-RENT-DUE=204360.00. Every dollar of sales from the first is now overage. The settlement still reports twelve of fourteen months counted and still foots.
So one half is silent and costly, one half is completely clean, and the pair is nearly three times the correct bill. Reading each half individually and finding one inert tells you nothing about the union.
Switching the guard off with the breakpoint -- ^(requireSalesCheck|statedBreakpoint)$ -- was byte-identical to the breakpoint alone. The rule is defined over month coverage, and the breakpoint does not disturb month coverage.
Three routes to the exclusions, and one of them leaves a tell
The carve-outs are reachable three ways and all three arrive together. Renaming the exclusion table produced PERCENTAGE-RENT-DUE=98737.50. Renaming the four exclusion keys inside it produced the identical number. Renaming saleType, the record field each sales line is classified by, produced the identical number again -- the option side and the record side reaching the same answer, which this series has now measured in five consecutive domains.
Those three arms do leave a tell, and it is worth being honest about: the check line changes to 14 of 14 month(s) counted, 0 excluded by the lease. A reviewer who knows two lines should be excluded would see it. A reviewer who does not would read a fully counted year.
Renaming only the single narrowest key, returns, is the quiet version of the same thing: PERCENTAGE-RENT-DUE=85372.50 and a check line reading 13 of 14 month(s) counted, 1 excluded. One carve-out lost, one number in a sentence changed, and 14,332.50 of overage rent charged on sales the lease says do not count.
The tier table, and the two arms that fail closed
The tier table is ordered and cumulative, and losing it does not disable percentage rent -- it flattens it. Renaming the table produced PERCENTAGE-RENT-DUE=78720.00: the natural 6% applied flat to the whole overage instead of tapering to 4.5% and 3%. Renaming upTo, the ceiling on each tier, produced the identical number by removing every boundary. Both are over-charges of 7,680.00 with a settlement that reads perfectly.
Two arms in this area are loud. Renaming ratePct made the percentage rent non-numeric and the settlement was refused. Renaming grossSales made every sales figure unreadable and the settlement was refused with no sales were reported. Nothing was charged in either.
That is the contrast this series keeps measuring, now holding for the twenty-first shape running: lose the VALUE and the system refuses; lose the MEANING -- which threshold, which tier, which sale counts -- and the system produces a confident, fully reconciled bill that is wrong. If you are shipping this kind of logic into a tenant-facing portal, our low-code platform protection notes cover where the client-side half sits.
What to change, whether or not you obfuscate
Never let a negotiated figure fall back to a derived one. A natural breakpoint is a sensible default for a library that has not been told the lease terms and a terrible default for a settlement that was supposed to use a stated one. Make the absence of a stated breakpoint an error when the lease has one, and record which route produced the figure on the statement itself.
Assert the threshold against the contract, not the arithmetic. Every check in this fixture is defined over month coverage and it passes in every arm above. The assertion that catches this is a comparison of the breakpoint actually used against the breakpoint in the lease exhibit -- one number, held outside the settlement code.
Count the exclusions explicitly. A settlement that reports how many lines it excluded, and a rule that requires that count to match the lease's carve-out list, turns three of the arms above from silent into loud at no cost.
And keep member-renaming patterns narrow and explicit, excluding any options object that carries thresholds, tier tables or exclusion lists. Renaming identifiers and control flow is unaffected by all of this.
How this was measured
Five commercial-real-estate areas were driven through the real obfuscation engine on five presets -- the ES5 default, the modern target, the emit gate's option set on both targets, and the string transforms. All five produced output byte-identical to the unprotected run on all five presets, twenty-five for twenty-five.
The member-renaming column is sixty-nine patterns on two presets, one hundred and thirty-eight verdicts: one hundred and twenty-eight moved the output, ten did not, and all ten are the pinned control arms measured on both presets.
Before any of that, every option key and every record field was deleted in the unprotected world to confirm it changes the answer at all -- ninety arms plus a union arm, zero structurally blind on the first run.
Every figure quoted above came from executing the protected artifact and reading the fields that decide the outcome, not from a diff summary.
Frequently asked questions
Did obfuscation on its own break percentage rent?
No. All five presets produced output byte-identical to the unprotected run across all five areas measured this pass.
What actually broke it?
Member renaming reaching the stated breakpoint, the exclusion table or the keys inside it, the tier table, or the record field each sales line is classified by.
Why did losing the breakpoint not produce a missing threshold?
Because a library with no stated figure derives the natural breakpoint from the base rent and the percentage rate. That is a real, published, defensible number -- 3,150,000.00 here -- just not the one the parties negotiated.
Why did the settlement check not catch it?
It is defined over month coverage: every reported line must be counted or excluded. That is true in every arm, so it printed 12 of 14 months counted word for word in both the correct and the failing run.
Renaming the base rent alone did nothing. Is that safe?
No. It is inert only because the stated breakpoint still wins. Renaming both in one pattern left nothing to derive from, made the breakpoint zero, and turned every dollar of sales into overage -- 204,360.00 against a correct 71,040.00.
Which arms were loud?
Renaming the tier rate and renaming the gross sales figure. Both made the arithmetic non-numeric and the settlement was refused before anything was charged.
What is the safest way to use member renaming here?
Keep the pattern narrow and explicit and exclude any options object carrying thresholds, tier tables or exclusion lists. Renaming identifiers and control flow is unaffected.
Related reading