Measured, not asserted

Does obfuscation break dangerous goods segregation?

Every failure this series has measured so far ends in a record being wrong: a value missing, a number in the wrong unit, a total over the wrong set, a row four years stale. A segregation requirement is not a property of any record. It belongs to a PAIR, and it cannot be reached from either package on its own. This test renamed the field that names a package's hazard class and watched a stowage plan with a flammable liquid six metres from an oxidiser -- twelve metres required -- come back MAY SAIL, with every unit still listed, every pair still considered, and the operator's completeness check printing a perfectly true sentence.

A requirement that belongs to neither package

The IMDG Code does not say what is true of class 3. It says what must sit between class 3 and class 5.1. Segregation is expressed as a matrix: away from, separated from, separated by one complete compartment or hold, separated longitudinally. The row and the column are two different consignments, and the requirement exists only where they meet.

That has a direct consequence in code. A segregation engine cannot iterate packages and check each one. It has to iterate PAIRS, build a key from both hazard classes, and look the requirement up. The fixture used here does exactly that, with a five-entry table -- flammable liquid against oxidiser needs twelve metres, flammable gas against flammable liquid needs three, and so on -- and a hold containing four units where exactly one pair is too close: a drum of class 3 four metres along the bay and a class 5.1 oxidiser at ten metres, six metres apart against a twelve metre requirement.

The operator in this fixture is careful in two ways that turn out to matter. The unlisted-pair default is authored fail-safe, at three metres rather than the vendor's nothing at all. And a rule of record refuses any plan that leaves a manifested unit unexamined. Both of those are good practice. Both are about to be shown to be beside the point.

The correct answer is MUST BE RESTOWED, with the reason U-11 and U-13 are 6m apart, separated-from requires 12m (class 3 vs 5.1).

Protection on its own moved nothing

Five areas, five presets, twenty-five comparisons, all identical to the unprotected runs, with the reproducibility guard running each original twice first. Nothing in this article is caused by protecting the code.

Renaming one field made a violation stop existing

With the hazard-class field renamed, every pair key became undefined|undefined. Not one of the six pairs matched the table. Every one of them inherited the operator's fail-safe three-metre default, and six metres clears three metres comfortably. The output was MAY SAIL.

Read the rest of that line before deciding how visible this is. Units examined: four, exactly as before. Pairs considered: six, exactly as before. Pairs carrying a requirement: six -- the same as the correct run, because the fail-safe default gives every unlisted pair a requirement. Reason list: empty. And the operator's rule of record printed plan-check(4 of 4 manifested unit(s) examined, 6 pair(s) considered), which is true in every particular.

There is no count anywhere on that line that differs from the correct run. The only visible difference is inside a display field, where the pair keys read undefined|undefined instead of 3|5.1 -- and that field sits well past the point where a log viewer truncates.

This is a failure shape the previous fourteen do not cover. Nothing about any package is wrong. The class is on the record, the position is on the record, the manifest is complete, the traversal is exhaustive, the conservation check passes. What moved is a relation, and a relation has no record to attach itself to.

Three different names, one outcome, and none of them is the class

The same MAY SAIL was reached from two more directions, which is what makes this a property of the shape rather than of one field.

Renaming the segregation TABLE -- the option carrying the matrix -- reverted it to the library's empty table. The pair keys still read correctly as 3|5.1 and 2.1|3; there was simply nothing to find them in, so every pair fell to the default. Renaming the DISTANCES -- the option mapping each segregation code to a number of metres -- left the codes correct and made every requirement 0m need. Both arms: six pairs, six requirements, empty reason list, MAY SAIL.

So the class field, the matrix and the distance table are three separate names, on two sides of the option/record split, and losing any one of them produces a stowage plan that reads fully checked and enforces nothing.

The fail-safe default is what makes it silent

Here is the result that inverts the usual advice. The one arm in this area that produced the CORRECT verdict is the arm that renamed the fail-safe default.

With defaultSegregation renamed, it reverted to the vendor's none. Unlisted pairs dropped out of the requirement set entirely, the count of pairs carrying a requirement fell from six to three, and the real table entry between the flammable liquid and the oxidiser still fired: MUST BE RESTOWED. Losing the careful default changed nothing about the answer.

And renaming the class field AND the default together produced MAY SAIL with with-requirement=0 and an empty requirement list -- visibly, unmistakably, nothing checked. A stowage officer glancing at that output sees a plan with zero segregation requirements in a hold full of dangerous goods and asks why.

Put those three arms side by side and the conclusion is uncomfortable. The fail-safe default did not protect anything. What it did was convert a visible absence of checking into a visible PRESENCE of checking -- six requirements, all satisfied -- that was enforcing the wrong requirement on every pair. The safest-looking configuration produced the quietest failure, for the fourth pass in this series running.

The completeness check cannot see it, by construction

The operator's rule is defined over units: it refuses a plan that examines fewer units than the manifest declares. Renaming the class field does not remove a unit. It leaves four units, four examined, four manifested.

Adding the flag that enables that rule to the same pattern changed the output by exactly one word -- unchecked instead of the plan-check sentence -- and the verdict stayed MAY SAIL. The check was contributing nothing to catch. That is not a criticism of the check; it is the structural point. Completeness over records, conservation over totals, allowlists over fields: every mitigation this series has recommended is defined over ITEMS. A pairwise constraint has no item to hang an assertion on, so a per-item assertion is blind to it however rigorous it is.

The mitigation that does work is different in kind: assert the RELATION. Take the two units the plan says are the closest incompatible pair, and check that specific distance against that specific requirement -- not that every unit was examined. And make an unresolvable pair key an error rather than a default, so a class the table cannot read stops the plan instead of joining the permissive majority.

What failed loudly, and the one that averaged the danger away

The numbers failed closed, as usual and one field away from the class. Renaming the bay position produced non-finite-separation(refused before approval) on both holds -- nothing sails, everyone knows immediately. Renaming the return-shape fields of the operator's rule produced plan-rejected(undefined), also closed.

The hold identifier is the interesting middle case and it reproduces an earlier finding in a physical setting. Renamed, it collapsed both holds into one bucket: seven units, twenty-one pairs, and a plan-check line reading 7 of 4 manifested unit(s) examined -- more than manifested, so the check passes. The verdict came back MUST BE RESTOWED for both holds, which is the right word for the wrong reason: units that are nowhere near each other, in different compartments, were compared as though they shared a bay. Restow orders on a ship that does not need them cost money and erode trust in the tool that issued them.

The unit identifier, the UN number and the class label moved only what was printed. Correct verdicts, unreadable paperwork.

What this means if you protect stowage or hazmat software

First, recognise which of your names carry relations rather than values. A hazard class, a compatibility group, a segregation code, a tenant identifier, a counterparty identifier: these are names whose loss does not corrupt any record and does not trip any per-record assertion. They are worth excluding from a rename pattern for that reason alone, and the wider habit of not trusting a browser-side decision is set out in zero trust and client-side JavaScript.

Second, do not let a lookup miss become a default. In this fixture the engine treats an unfound pair as a pair with nothing to observe, which is a reasonable library behaviour and a catastrophic application one. Wrap the lookup: an unresolvable pair should refuse the plan.

Third, write the assertion at the level the requirement lives at. The plan-check in this fixture counts units and passes; a check that names the closest incompatible pair and its required separation fails in every open arm above.

The protected build produced the correct restow order on all five presets. The exposure is not protection; it is a rename pattern reaching names an installed engine reads, and it is avoidable with a member regexp that stops at the boundary.

Frequently asked questions

Did obfuscation change any stowage decision on its own?

No. Five areas across five presets produced twenty-five identical outputs, with each original run twice before anything was compared to it. Every failure here required member renaming reaching names the installed segregation engine also reads.

What made this different from the other failures in this series?

The record stayed correct. Every package still carried its class, its UN number and its position; the manifest was complete and the traversal exhaustive. What was lost was the requirement BETWEEN two packages, and a requirement that belongs to a pair has no record for a per-record check to inspect.

How visible was the wrong answer?

Barely. Units examined, pairs considered and pairs carrying a requirement were all identical to the correct run, the reason list was empty, and the operator's completeness check printed a true sentence. The only tell was the pair keys reading undefined inside a long display field.

Did the fail-safe default help?

It did the opposite. Renaming the fail-safe default alone produced the correct verdict. Keeping it is what turned a visibly unchecked plan into a plan showing six satisfied requirements while enforcing the wrong one on every pair.

Why did the completeness check pass?

Because it is defined over units and no unit went missing. Four units, four examined, four manifested. Adding its enabling flag to the same rename pattern changed one word of output and not the verdict, which shows the check was contributing nothing to catch.

Which names failed loudly?

The bay position, which made the separation non-finite and refused both holds outright, and the return-shape fields of the operator's own rule. The hold identifier collapsed two compartments into one and produced restow orders for pairs that are not near each other.

What check would have caught it?

One written at the level the requirement lives at: assert the actual distance between the closest incompatible pair against the requirement for that pair. And make an unresolvable pair key refuse the plan rather than fall back to a permissive default.

Related reading