Scheduling And Rostering

Does obfuscation break shift scheduling rules?

Two independent controls enforcing the same rule is what defence in depth looks like when it is written down. This pass protected a rota that guards one post two separate ways, renamed each guard alone, and then renamed both. Each guard alone held, and moved nothing but the reason string in the log. Both together put an uncertified nurse on an intensive-care night shift.

A post protected twice, on purpose

The sample is a rostering configuration of the kind a workforce product wires in. An ICU night post requires a critical-care certification. That requirement is stated twice, deliberately. It appears declaratively in a requiredCertifications map, and again inside an authorize callback the hospital supplied when the declarative list proved too coarse for its real policy. Alongside them sit a minimum rest period of ten hours, a ceiling of six consecutive days, and a separate conflict callback carrying the fatigue rules.

This is not a contrived redundancy. It is how these systems actually accumulate: a simple list first, then a callback when the list cannot express an exception, and nobody removes the list because it still works and removing things is risky. The two controls are written by different people at different times and are never thought of as one control.

Unprotected, an uncertified nurse presented for the ICU night shift is refused with not-certified(icu-night requires critical-care). Either guard alone would produce that refusal.

Each guard alone holds, and moves only the reason

Renaming requiredCertifications alone leaves the assignment refused. The declarative list is gone, so the first layer has no opinion, and the hospital's callback catches it instead. The only thing that changed is the reason string, from not-certified(...) to not-authorized(icu-requires-critical-care-certification). Same outcome, different label.

Renaming authorize alone also leaves the assignment refused, this time with the original not-certified(...) reason, because the declarative list is intact and catches it. The configuration line reports the callback as library-builtin(no opinion) rather than caller-supplied, which is the only visible trace.

Both arms are, on their face, exactly the finding a reviewer closes as cosmetic. A log message changed. Nobody was scheduled who should not have been. The safety property held in both cases, which is precisely what the second layer was installed to do.

Both together, and the nurse is on the rota

Renaming requiredCertifications and authorize in one pattern produced SCHEDULED=true rostered(nurse-2 -> icu-night on 2026-08-18). The uncertified nurse is on the intensive-care night shift. Nothing threw, nothing warned, and the roster entry is indistinguishable from any other.

The two layers were not independent. They are two properties of the same options object, in the same file, and a single renaming pattern reaches both. Defence in depth bought nothing here, because the depth was all at the same address. This is the fourth consecutive pass in this series where a redundant pair fell to one pattern, and it is the first where the outcome is a patient-safety one rather than a financial one.

The mitigation is placement, not count. A second layer only survives a transformation the first layer did not survive if it lives somewhere the transformation cannot reach: enforced server-side, in a different service, or in a database constraint. Two callbacks on one configuration object are one layer wearing two names.

Reading a matrix after this result

The practical lesson is about triage. In this file the two arms that moved only a reported reason were the leading indicators of the worst cell in the matrix. If a review had checked each name individually, seen a label change in each case, and closed both as cosmetic, the combined pattern would have shipped.

So a half-arm that moves only a label deserves a specific follow-up question rather than a close: what else enforces this property, and is it in the same file? If the answer is yes, measure the two together before deciding either is harmless.

It also argues for testing the halves as well as the union, which sounds obvious and is the opposite of what a realistic test does. A pattern covering the whole options object would have caught this. The dangerous cells in these matrices are not reliably at the wide end.

The fatigue rules, and the two callback shapes

The file carries two caller-supplied callbacks that are phrased in opposite ways, which is ordinary and which decides how each one fails. The authorize callback returns a permission -- a field saying the assignment is allowed. The conflict callback returns a prohibition -- a field saying a conflict was found.

Renaming the permission field means the engine cannot find the allowance, treats its absence as no, and refuses assignments. That fails closed: the rota stops working and somebody notices within a shift. Renaming the prohibition field means the engine cannot find the refusal, treats its absence as no conflict, and schedules. The nurse who had already worked eleven straight days was rostered for a twelfth.

This split is not luck and it is not specific to obfuscation. A missing permission is falsy, so the answer is no. A missing prohibition is also falsy, so the answer is again no -- but here no means permitted. Phrase the flag your collaborator hands back as a permission and the same loss becomes an outage instead of a silent grant. A version bump, a dropped key in serialisation, or a hand-written mock produces the same undefined.

What protection alone did, and what to do about it

Nothing. The file was protected on five profiles -- the ES5 default, the modern target, both emit-gate configurations and the string-encoding profile -- and produced byte-identical output on all five. Every assignment was scheduled or refused exactly as it was unprotected. Renaming identifiers and encoding strings does not disturb a rostering engine.

Every result above required member renaming with a pattern naming those members. Keep such a pattern anchored to names your own code owns on both sides; a rostering configuration and the worker records handed to it are read by installed code, so they are contracts rather than internal names.

Where the rule is a safety rule, enforce it somewhere the client build cannot move. Re-check the certification requirement server-side when the roster is committed, and phrase the callback contracts as permissions. Then assert the specific case: an uncertified worker against the restricted post must be refused, and the test must assert the refusal rather than merely that a decision was returned.

Frequently asked questions

Does obfuscating a scheduling app change who gets rostered?

Not by itself. The sample was protected on five profiles including the ES5 default, the modern target and string encoding, and produced identical rosters on all five. Every result in this article required member renaming, which is off unless you enable it and supply a pattern.

The rota enforced the certification rule twice. Did that help?

Not against a pattern that reached both. Each control renamed alone still refused the uncertified nurse and changed only the reason string. Renaming both in one pattern scheduled them onto the ICU night shift.

Why did each control alone look harmless?

Because the surviving layer caught the case, so the outcome did not change and only the log message moved. That signature -- a half-arm that moves a label and nothing else -- was the leading indicator of the worst result in the file.

How should a second layer be placed so it actually survives?

Somewhere the transformation cannot reach: a different service, the server side of the request, or a database constraint. Two callbacks on the same options object in the same file are one layer with two names.

Why did one callback fail closed and the other fail open?

Because of how each answer is phrased. The authorize callback returns a permission, and a missing permission reads as no, so assignments are refused. The conflict callback returns a prohibition, and a missing prohibition also reads as no, which here means permitted.

Is that phrasing problem specific to obfuscation?

No. Any route to an undefined field produces it -- a version bump, a serialisation hop that drops undefined keys, a partial response, or a hand-written mock. Writing the flag as a permission rather than a prohibition costs nothing at design time and changes the failure direction.

What test would have caught the combined case?

Assert the specific refusal: an uncertified worker against the restricted post must be refused, by name. A test that only checks a decision object was returned, or that the roster call did not throw, passes in every arm above.

Related reading