Measured, not asserted

Does obfuscation break flight duty and rest limits?

The maximum duty period for a crew is not a number in a configuration file. It is a row, selected by when the crew reports and how many sectors they fly. A row that cannot be found is not an error in any rostering engine written to be reused -- it is a fallback. This test renamed the field carrying the segment count and watched a crew be rostered 0.9 hours past its limit, measured against a limit that was real, finite, printed, and belonged to a different duty.

The limit is a row, not a number

A published duty table has a shape everybody in the industry recognises: report early and fly four sectors and you get the shortest day in the book; report late and fly one and you get the longest. The fixture used here carries four report-time bands and one to four segments, so sixteen limits between 9 and 14 hours, plus a fallback of 16 hours for anything the table does not cover, a rest floor of 10 hours, and an extension policy switched off.

Three pairings are measured. One reports mid-morning for two sectors over 12.5 hours with 11 hours of rest behind it, and is legal. One reports at 04:00 for four sectors -- the 10-hour row, the shortest in the table -- and is scheduled 10.9 hours, so it must be refused. One has a perfectly legal duty length and only 8.5 hours of rest, so it must be refused on rest alone. Each fails on at most one axis, so the axes cannot mask each other.

Protection on its own moved nothing

The base column for this pass was five areas across five presets: ES5, modern, the emit gate's own option set on both targets, and the string transforms. Twenty-five comparisons, twenty-five identical. Nothing below is caused by protecting the code.

Renaming the segment count rostered a crew past its limit

With the segments field renamed, the table lookup stopped matching. The engine did what a reusable engine does: it fell back to the operator's configured maximum of 16 hours. The 04:00 four-sector pairing was ROSTERED, printing limit-read=16h ceiling=16h scheduled=10.9h.

Every other value on that line is correct. The band is still early. The rest is still 12 hours. The crew identifier is right. And the operator's own roster rule -- which exists precisely to refuse a duty whose limit could not be established -- passed, and printed roster-check(band early, undefined segment(s), limit 16h, scheduled 10.9h, rest 12h).

Read that check's own sentence. It names the segment count as undefined, in its output, on the same line as the approval. It passed anyway, because it was written to test whether the limit is finite, and 16 is finite. A limit that is real, plausible and published is exactly what a sanity check cannot argue with.

Losing more made it safe, which is the uncomfortable part

The same rename plus the fallback limit -- two names instead of one -- produced the opposite outcome. With no configured fallback the engine substituted its own default of infinity, the operator's rule saw a limit that was not finite, and every duty in the run was refused with duty-rejected(roster-check(no limit for band early/undefined segment(s) -- unreadable)).

So the wider pattern fails closed and loudly, and the narrower one puts a crew into the aeroplane 54 minutes over. A reviewer who tests only the realistic broad pattern -- rename everything on the options object -- sees a system that fails safe and concludes the area is fine.

The lesson generalises past renaming: a fallback that is a real number is more dangerous than a fallback that is unusable, because every check you would write to catch the second one passes the first.

The report time is a key too, and the flags point the wrong way

Renaming the report hour classified every duty into an unknown band, missed the table the same way, and rostered both the day and the early pairings at the 16-hour fallback. Renaming the whole duty table did it for all three.

Two option keys moved the answer without any lookup being involved. The extension policy, switched off by the operator, reverts to the library's permissive default: the 10-hour limit becomes a 12-hour ceiling and the 10.9-hour duty is legal. The rest floor of 10 hours reverts to zero, and the crew with 8.5 hours of rest is rostered with an empty reasons list.

Neither of those is a bug in the library. A rostering engine that does not know your regulator has to default to permitting; the operator is the party that knows. That asymmetry is what decides the direction of every one of these failures.

What failed loudly

The crew identifier produced unidentified-crew(refused before rostering). The scheduled duty hours and the rest hours produced non-finite-duty(refused before rostering). The guard's own ok field turned every duty into duty-rejected. All of these are the cheap failures: a rostering system that refuses every pairing is discovered before the first departure.

The two pinned controls -- the hour label and the extension length, both set equal to the library default on purpose -- measured identical, as predicted, for the twenty-third consecutive pass this prediction has held.

If you protect a rostering or crew-planning application

Make a missed table lookup an error rather than a fallback. If the engine cannot establish which row governs a duty, it should refuse to roster it, exactly as it refuses a duty whose hours are unreadable. That single change turns the dangerous cell in this area into the loud one.

Then assert which row was applied, not whether the number it carries looks sensible. The check in this test printed a finite limit and approved; a check that printed 'row early/4' and compared it against the pairing's own segment count would have refused. Checking the value can only tell you the value is a real one, and in a published table they all are.

And keep the names you share with an installed engine out of the pattern. A member regexp scoped to the fields your own code owns is the difference between a build that is protected and a build that is protected and correct.

Frequently asked questions

Did obfuscation change any duty limit result on its own?

No. Twenty-five base comparisons across five presets were identical to the unprotected run. Every result in this article required member renaming aimed at a name the installed rostering engine also reads.

What happened when the segment count was renamed?

The duty table lookup missed, the engine fell back to the operator's configured maximum of 16 hours, and a four-sector early-report pairing scheduled at 10.9 hours was rostered against a 10-hour published limit.

Did the operator's own safety check catch it?

No. It passed and printed the segment count as undefined on the same line as the approval, because it was written to refuse a limit that is not finite, and the 16-hour fallback is finite. A plausible limit is what a sanity check cannot argue with.

Why did renaming two names produce a safer result than renaming one?

Losing the fallback as well left the engine with its own default of infinity. The operator's rule then saw a non-finite limit and refused every duty in the run. The broad pattern fails closed; the narrow one flies a crew 54 minutes over.

Which configuration keys were dangerous on their own?

The extension policy and the rest floor. Both revert to permissive library defaults -- extensions allowed, no rest requirement -- because a general-purpose engine cannot know your regulator. The operator is the party that knows, so the operator's value is the one that goes missing.

What failed loudly?

The crew identifier, the scheduled hours and the rest hours all refused before rostering, and renaming the guard's ok field rejected every duty. Those are the cheap failures: somebody notices before the first departure.

What is the cheapest fix?

Make a missed lookup an error rather than a fallback, and assert which row was applied rather than whether its value looks sensible. Both are worth having regardless of obfuscation, because a dropped key over a serialisation hop produces the same miss.

Related reading