Measured Behaviour
Published
Taking a sprinkler system out of service is not made safe by the permit. It is made safe by the things the permit is conditional on: a fire watch walking the floor, and somebody outside the building knowing it is undefended. Those are two separate clauses, written separately on purpose. They are also two properties of one object, and one rename pattern reaches both.
A permission that is only safe because something else is true
An impairment permit is a piece of paper that says a fire protection system is off between these hours in this zone. On its own it protects nothing. What protects the building is the set of compensatory measures the permit is issued against: a continuous fire watch patrolling at intervals, notification to the brigade and the insurer, hot work suspended, and a hard cap on how long the system may stay down.
We built the control as it actually looks. Four impairments over a two-day shutdown. An eight-hour cap. A watch that counts as continuous only if patrols are no more than thirty minutes apart. No impairment running through the night. Six jobs requested across five zones, each with the floor area it would be working in.
The correct run holds four of the six jobs. One permit runs twelve hours against an eight-hour cap. One runs from six in the evening to two in the morning. One zone is impaired and carries no permit at all. And one permit - the row this whole area is built around - is three hours long, in daylight, inside the cap, and fails BOTH compensatory clauses at once: the watch is logged but forty-five minutes apart, and the brigade was never told.
Two jobs proceed, 8,400 square metres are worked on, and no work happens anywhere without protection. That is the baseline.
Two clauses, each individually invisible
The permit that fails both clauses is what makes the experiment possible, because it means each clause can be switched off without changing any verdict.
Rename the option that requires a fire watch and the watch clause stops being consulted. The permit is still refused - the brigade still was not told. Proceeded stays at 2, held stays at 4, 8,400 square metres are still worked, and the counter stays at zero. The only thing that moves is a printed reason in the permit register.
Rename the option that requires notification and the same thing happens in reverse. The watch clause still refuses the permit because forty-five minutes is not continuous. Same counters, different printed reason.
Two arms, both producing a value diff with no behaviour change. In a review those are two lines in the cosmetic pile. They are also the exact signature that defence in depth is SUPPOSED to produce from inside a test suite: each layer looks redundant because the other one catches everything it would have caught. That is the property the layers exist to have.
Rename both and the permit is valid. Proceeded goes from 2 to 3, area worked goes from 8,400 to 9,300, and the counter held outside the bundle - floor area worked on while that zone genuinely had no protection and no compensating watch - goes from 0 to 900 square metres. Both member-renaming presets produced identical numbers. Over six planned shutdowns a year that is 5,400 square metres of undefended building with hot work in it.
Redundancy is not independence when one pattern reaches both
The important part is not that the union is worse than the halves. It is WHY the two layers fall together, because that reason is a design decision rather than an accident.
The fire watch and the notification are genuinely separate controls. They fail differently, they are performed by different people, and neither is derived from the other. What they share is an address: both are keys on the same options object in the same file. A regexp scoped to "the options for the permit system" reaches both, and one edit removes a defence that two independent processes were maintaining.
The mitigation is placement, not count. A second layer only survives if it lives somewhere the transformation cannot reach - enforced server-side against a record the client cannot rewrite, or in a different bundle with a different renaming scope. Two clauses side by side in one literal are one clause with two printed reasons.
The same structure shows up wherever people write belt-and-braces checks in one place. We measured it in permit-to-work authorisation and again in energy isolation verification, and it is the reason a review's "both of these are individually harmless" needs a second sentence about whether one pattern reaches both.
The record fields fail the other way, and that is worth knowing
Every compensatory measure is recorded as a flag on the permit: the watch was logged, the brigade was notified, the insurer was notified, hot work was suspended. Rename any of those and the flag reads absent, absent is falsy, and falsy means the measure was not taken.
So the good permit becomes a bad one. Proceeded went from 2 to 1, area worked from 8,400 to 4,200, and the counter stayed at zero. The shutdown grinds to a halt, somebody complains loudly, and the problem is found the same morning. That is the safe direction, and it is not luck.
These flags are PERMISSIONS - was it done, was it logged, was it told - and a missing permission is falsy, so the answer is no. The names that failed open in this file are all on the other side: options that switch a clause OFF, a cap that reverts to none, a night restriction that reverts to allowed. Write the flag your collaborator hands back as a permission rather than as a prohibition and the missing case is the refusal.
One record field did nothing at all: the interval between patrols. Renaming it makes the gap unreadable, an unreadable gap fails no comparison, so the watch clause passes - and the notification clause still refuses the permit. Counter-inert, one printed reason moved. It is the third member of the same cosmetic-looking family, and it belongs to the union that is not.
What the good-permit test cannot show you
The negative control was the same four impairments raised properly: short windows in daylight, patrols twenty minutes apart, everybody told, hot work stopped. Five of six jobs proceed, one is held because its zone has no permit, nothing unprotected.
Every arm that switches a compensatory clause OFF is inert in that control. The reason has nothing to do with the obfuscator: a clause that nothing was going to fail cannot be shown to matter by removing it. What the control does catch is the opposite family - the arms that make a good permit read as a bad one - and those are the loud ones that were never the risk.
If your test suite for an impairment system consists of raising a correct permit and checking that work is allowed, it is measuring the half of the space that fails safely. The fixture that can see the other half needs a permit that is bad for more than one reason, because that is the only fixture in which each reason can be removed independently.
What to do about it
Scope member renaming so that a single regexp does not reach two controls you are relying on independently. In practice that means reading your options objects and asking, for each pair of keys, whether you would accept losing both at once - and moving one of them if the answer is no.
Where the answer has to be no, move the second layer out of reach. A compensatory-measures check that runs on the server against a permit record the client cannot rewrite is not affected by anything in this article. Two clauses in one client-side literal are.
And keep one figure outside the bundle. Every number here came from a register, held under flat string keys, of which zones genuinely had no protection. The permit book reconciled perfectly in every arm - proceeded plus held equals jobs, area adds up - because reconciliation is a statement about what the run decided, not about what was true. The register is the only thing in the whole area that could tell the difference.
Frequently asked questions
Does obfuscation disable an impairment permit system?
Not by itself. Protecting the model on five presets without member renaming produced byte-identical output. What changed the permit decisions was member renaming reaching the option keys that decide which compensatory clauses are consulted.
What was the worst result?
Renaming the fire-watch requirement and the notification requirement together. Each was individually inert on every counter; together they validated a permit that fails both clauses, and 900 square metres of genuinely undefended building was worked on for a shift.
Why does testing each clause separately miss it?
Because the permit under test fails both clauses. Switch one off and the other still refuses, so no verdict moves and only a printed reason changes. That is exactly what working defence in depth looks like from inside a test.
Which direction do the record fields fail in?
Closed. The compensatory flags are permissions - was the watch logged, was the brigade told - and a missing permission is falsy, so a renamed flag makes a good permit invalid. Proceeded went from 2 to 1 and the shutdown stopped, which is loud and gets fixed.
Did the estates team's own check catch anything?
No. It asserts that every job was allowed or held and that the area reconciles. Both hold in every arm, because both are statements about the permits the run validated rather than about the building.
Is the fix to add a third layer?
No. A third clause on the same object falls with the other two. The fix is placement: put one layer where the transformation cannot reach it, such as a server-side check against a record the client does not own.
Does this only matter for obfuscation?
No. Any rename-shaped change reaches the same pair: property mangling in a minifier, a codemod, a config schema migration, or a library version bump that renames two options in one release.
Related reading