Measured, not asserted

Does obfuscation break co-tenancy and rent abatement?

A co-tenancy clause is a condition with a duration attached, and the two are configured separately. The condition decides whether relief is owed at all: are the named anchors trading, and is the centre occupied above the agreed floor? The duration decides how much. This test renamed each half of the condition on its own against a real obfuscation engine and got output byte-identical to the correct run -- the exact reading a reviewer closes as cosmetic. Then it renamed both in one pattern and the tenant's entire entitlement disappeared, with every check still passing.

Two tests joined by OR, and each one alone is enough

The fixture is a 365-day measurement window over a seven-store centre. The lease names three anchors, requires two of them to be trading, requires 70% occupancy, allows a 90-day cure period, abates half the base rent, and caps relief at 270 days. Two anchors go dark -- one on day 61, one on day 118 -- so from day 118 only one named anchor is trading and occupancy is below 70%.

Correct: FAILING-DAYS=248 from day 118, 90 of them consumed by the cure, RELIEF-DAYS=158 and RENT-ABATED=40906.85.

Rename the minimum-anchors option on its own and the result is byte-identical to correct on every field: 248 failing days, 158 relief days, 40,906.85 abated. Rename the minimum-occupancy option on its own and the result is byte-identical again. Both arms look completely clean, because the two tests are joined by OR and either one alone still fails the day.

Rename both in one pattern and the condition can no longer return false. FAILING-DAYS=0. first-failure=none. RELIEF-DAYS=0. RENT-ABATED=0.00. The abatement check prints abatement-check(365 of 365 day(s) evaluated, 0 relief day(s) within 0 failing day(s)) and passes.

Redundancy is not independence when one pattern reaches both

The two tests are genuine defence in depth. They are separate controls, they enforce different properties, they were negotiated separately and they are checked separately. What they are not is independent, because they are two keys on the same object in the same file, and one renaming pattern reaches both.

The library's defaults are what makes the union total rather than partial. A minimum of zero anchors does not mean "no anchors required"; it means the anchor test cannot return false. A zero occupancy floor means the occupancy test cannot return false. A determination made under those defaults is not an error and does not look like one -- it is a clean, well-formed finding that the centre is compliant, produced against a centre that is 52% occupied with two anchors dark.

Switching the guard off alongside them changed nothing: ^(requireAbatementCheck|minAnchorsOpen|minOccupancyPct)$ produced the same zeros. The rule was contributing nothing to remove, because it is defined over days evaluated and an internal consistency bound, and both hold perfectly when nothing fails.

The mitigation here is placement, not count. A second layer only survives if it lives where the transformation cannot reach it -- recompute the occupancy test from the rent roll on the server, or from a source the tenant holds, rather than from a second key beside the first.

A record field reaches the same zero

Renaming closedFromDay -- the field on each store record that says when it went dark -- produced the identical result: zero failing days, zero relief, nothing abated. A store that never closes is a store that never fails the test.

That is the option side and the record side arriving at the same answer by different routes, which this series has now measured in five consecutive domains. It matters for scoping a renaming pattern: excluding the configuration object is not sufficient if the records the configuration is evaluated against are still in scope.

The opposite direction is reachable four ways and all four agree. Renaming the anchor name list, renaming the store name field the list is matched against, renaming openFromDay, and renaming area each produced 365 failing days from day one, relief capped at the 270-day maximum, and RENT-ABATED=69904.11. That is a 28,997.26 over-credit, and it is the direction the landlord notices on the next rent invoice.

Note what the anchor list is: a list of tenant names, as text. Renaming the record field the names are matched against moves one side of that comparison and not the other, so the guard keeps running and matches nothing. A rule written as data does not move when the property name it refers to moves.

The quiet under-credit, and two honest inert arms

Renaming the abatement percentage is the subtlest cell in the area. The determination is entirely correct -- 248 failing days, 158 relief days, first failure on day 118 -- and RENT-ABATED=0.00. The system correctly identifies that relief is owed and then credits nothing. Every diagnostic reads right; only the money is missing.

Two arms were inert and both are worth reporting rather than hiding. The maximum-relief cap did not move anything, because 158 relief days is under the 270-day limit -- that is a fact about the fixture's data, not about the name. The alternate-rent percentage did not move the abated figure either, because in this determination the abatement branch is what applies.

An inert arm is not evidence of safety. It is evidence that this data did not exercise that name, which is a different statement and a much weaker one. If you are assessing how much of a tenant-facing entitlement engine belongs on the client at all, our widget and SDK protection notes cover the boundary.

What to change, whether or not you obfuscate

Phrase a threshold so that losing it fails closed. A minimum of zero anchors and a floor of zero percent are both defaults that make a test unable to fail. Requiring the threshold to be present and finite before evaluating the condition -- and refusing the determination otherwise -- is one line and turns the worst cell above into a loud one.

Do not put two layers of defence on one object. If the occupancy test is meant to survive the loss of the anchor test, it has to live somewhere the same edit cannot reach: a different service, a different deployment, a figure recomputed by the party being protected.

Assert that a control RAN, not that it exists. The configuration screen in every failing arm above still reports the abatement rule as caller-supplied and installed, and that report is accurate -- the rule is present, bound and correct, and there was simply nothing for it to object to.

And keep member-renaming patterns narrow and explicit, excluding both the options object carrying the condition and the records it is evaluated against. 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. Nothing threw, nothing was vacuous, ES5 and modern agreed on every arm.

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 one purpose-built union arm, zero structurally blind on the first run. The union arm existed because both halves of the condition were predicted inert alone, and that prediction was registered before the run rather than after it.

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 co-tenancy determination?

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 both halves of the co-tenancy condition in one pattern -- the minimum-anchors threshold and the minimum-occupancy threshold -- or reaching the record field that says when a store went dark.

Each half alone was clean. Why is that not reassuring?

Because the two tests are joined by OR, so either one alone still fails the day. A clean reading on each half individually says nothing about the pair, and the pair took 248 failing days to zero and erased 40,906.85 of abatement.

Why do the library defaults make it total rather than partial?

A minimum of zero anchors means the anchor test cannot return false; a zero occupancy floor means the occupancy test cannot return false. Both defaults make a test structurally unable to fail rather than merely permissive.

Which direction does the landlord notice?

The over-credit. Renaming the anchor list or the store name field produced 365 failing days from day one and 69,904.11 abated against a correct 40,906.85 -- an over-credit that appears on the next rent invoice.

Is defence in depth useless here?

No, but count is not the point -- placement is. Two controls on one object in one file fall to one pattern. A second layer only survives if it lives where the same edit cannot reach it.

What is the safest way to use member renaming here?

Keep the pattern narrow and explicit and exclude both the options object carrying the condition and the records it is evaluated against. Renaming identifiers and control flow is unaffected.

Related reading