Measured, not asserted
Published
A critical control point is three separate things bolted together: a limit that says what is acceptable, a direction that says which side of it is acceptable, and a disposition that says what happens to product made while it was not. They are configured separately and they fail separately. This test renamed each of them in turn against a real obfuscation engine. The reading count never moved. What moved was whether the readings were compared to anything, which way round the comparison ran, and whether a deviation stopped a pallet leaving the building.
A reading taken against no limit is a reading in specification
The fixture is one shift on one line: nine monitoring readings across three critical control points and two lots. The cook step is a floor -- the product must reach at least 74C. The chiller and the pH step are ceilings. Two readings genuinely breach: the cook probe read 68.4C at 120 minutes and the chiller read 5.9C at 180 minutes, both on lot L-442. The correct run finds both, flags three intervals where nobody took a reading for longer than the plan allows, and holds L-442.
Rename the option carrying the critical-limits table and the library falls back to its own default, which is an empty table. There is nothing dishonest about that default: a monitoring library cannot invent your limits. But the consequence is that a reading with no limit to breach is not a failure, and it is not an error either. It simply is not compared.
The measured result: readings=9 against-a-limit=0 DEVIATIONS=0 unmonitored-intervals=3 lots-held=0[]. Every reading was still taken. Every reading is still in the ledger, with its CCP, its time, its lot and its value. The shift record is complete and the shift record is wrong.
The same answer arrives from the other side. Renaming the identifier field on the reading records -- the key the limits table is looked up by -- produces against-a-limit=0 DEVIATIONS=0 as well, byte for byte. So does renaming the quoted string keys of the table itself. Three routes, one silent result.
Protection on its own moved nothing
Before any renaming, all five areas of this pass were run through five presets: the ES5 default path, the modern target, the emit gate's own option set on both targets, and the string transforms. Twenty-five comparisons, every one byte-identical to the unprotected run, under a guard that runs each original twice and refuses to compare anything against a fixture that will not reproduce itself.
So nothing in this article is caused by protecting the code. Every result below required member renaming aimed at a property name that both the plan's own code and the installed monitoring library read. That is a configuration question, not a verdict on protection. The engine also has to be told which names are safe to move, and this article is about what happens when it is not.
The one figure that would have caught it
There is a number in that output line that tells the whole story, and it is not the deviation count. It is against-a-limit: nine in the correct run, zero in the failing one. A coverage figure -- how many of the readings you took were actually compared to something -- separates a clean shift from an unchecked one, and a deviation count cannot.
This is worth stating as a design point rather than an obfuscation one, because a mistyped CCP identifier in a configuration file produces exactly the same zero. Count what was checked, not just what failed, and refuse to sign a shift where those two numbers disagree with the number of readings taken.
It is also a refinement of something an earlier pass in this series concluded. That pass found that losing a configuration key announces itself in the library's own coverage count while losing a record field cannot. Here both routes announce, because this library's coverage figure is defined over the result of the lookup rather than over the size of its own table. The determinant is not option-versus-record. It is what the count is counting.
A direction is one word, and it decides which readings are wrong
The cook step is the only floor in the plan. Rename the option carrying the directions and every limit is read as a maximum, which is the commoner shape and therefore the library's default.
The deviation count goes up, from two to four, and two lots are held instead of one. That looks like a system being careful. Read the list and it is not: the three deviations on the cook step are the readings at 76.2C, 75.8C and 77.1C -- the good ones, now flagged for exceeding 74 -- and the 68.4C undercook that actually happened is not in the list at all.
So a polarity flip does not produce fewer findings. It produces different findings, with a total that reads as more diligent than the truth, and the one lot that genuinely needed holding is held for a reason that is not the reason. An investigator following up the four flagged readings would find three probes working correctly and close them.
Detected, logged, counted, and not held
The plant's own corrective-action rule is a good rule: if there are deviations, some lot must be held. Renaming the disposition option alone trips it. The shift is refused with shift-rejected(corrective-action-check(2 deviation(s) and no lot held)), and somebody has to look.
Rename the flag that enables that rule alongside it and the result is DEVIATIONS=2 unmonitored-intervals=3 lots-held=0[]. Both deviations are still found. Both are still correctly described, with the right CCP, the right time, the right lot and the right value. The undercook is in the record. And no product is held.
That is the pattern this series has hit in eight areas now: a strict control sitting behind an enabling flag, where both can be lost by one pattern. The half that would have screamed is switched off by the half that fails quietly. Any argument of the form 'if this broke we would know' is only sound when the thing that would tell you is not downstream of the thing that broke.
The interval nobody monitored
The plan says a CCP is read at least every 60 minutes. The fixture has three gaps longer than that, including two 90-minute holes on the chiller. Rename the option carrying that interval and the count goes to zero: unmonitored-intervals=0, with the deviations and the hold both still correct.
Renaming the timestamp field on the reading records reaches the same zero, from the other side, because a gap computed from unreadable times is not a number and no comparison against it is true.
Neither arm throws, neither is missing a reading, and the shift record still shows nine readings at nine stated times. What is gone is the finding that the monitoring frequency the plan promises was not met -- which is exactly the finding an auditor asks for.
What failed loudly
Renaming the plan's own corrective-action rule replaced it with the library's built-in, which accepts any disposition -- the downgrade shape, where the guard is not switched off but quietly substituted. Renaming the lot identifier left the hold list reading lots-held=1[undefined], which no warehouse system accepts.
The reading value is the interesting one. Rename it and the value guard refuses the comparison, so against-a-limit drops to zero and the deviation count with it -- the same silent zero the limits table produced, arrived at from a completely different direction. An unreadable measurement is not treated as an alarming measurement anywhere in this fixture, and that is worth checking in your own.
Set against those, the enabling-flag arm and the limits-table arm are silent. Nothing throws, nothing is undefined in the summary, and every figure on the shift record is a real figure about real readings.
What to assert
Assert the coverage, not the outcome. The number of readings compared against a limit must equal the number of readings taken, and a shift where it does not is not a clean shift -- it is an unmonitored one. That single assertion fails in the limits arm, the identifier arm, the table-key arm and the reading-value arm.
Assert one known bad reading end to end. Put an undercook in your test data and assert three things about it: that it is found, that it is classified as a deviation on the cook step, and that the lot carrying it comes back held. That fails in the direction arm and in the disposition arm, and a test that merely checks the limits table loaded passes both.
And where you can, keep the direction out of a separate key from the limit it applies to. A floor and a ceiling that differ by one word in a different object are a configuration hazard whether or not anything ever renames them.
The wider point is the one this series keeps arriving at. Protection did not do this; a naming configuration did. Exclude the property names that cross a boundary into an installed library, then prove the exclusion with a test that asserts a real breach is still caught. The same discipline is what IEC 62443 asks of industrial software generally: name the boundary, then evidence it.
Frequently asked questions
Did obfuscation on its own break HACCP monitoring?
No. Five presets -- the ES5 default, the modern target, the emit gate's option set on both targets, and the string transforms -- all produced output byte-identical to the unprotected run across all five areas measured this pass.
What actually caused the failures?
Member renaming applied to property names that cross the boundary into an installed monitoring library. The limits table is looked up by CCP identifier, and the identifier is text in a configuration and a name in code. Renaming rewrites the name and leaves the text alone.
Why does a missing critical limit produce zero deviations rather than an error?
Because a library cannot distinguish a limit you never configured from a limit whose name it can no longer read. Both are an absent entry, and an absent limit is nothing to breach. The reading is taken, logged and skipped.
Does the deviation count give any warning?
Not in the direction arm -- it goes up, from two to four, which reads as a system being careful. The four flagged readings are three good cook readings and the real chiller breach; the actual undercook is not among them.
Which single number would have caught most of this?
The coverage figure: how many readings were compared against a limit. It reads nine in the correct run and zero in the limits arm, the CCP-identifier arm, the table-key arm and the reading-value arm.
Is the corrective-action rule enough protection?
On its own it caught the disposition arm cold. Paired with the flag that enables it, it never ran: two deviations were found and logged and no lot was held. A guard is only loud while it is still reached.
What is the cheapest fix?
Exclude the property names that cross into the library from renaming, and assert one known bad reading end to end -- found, classified on the right CCP, and its lot returned as held. Requiring a measurement to be a finite number before comparing it is worth having regardless.
Related reading