Measured, not asserted

Does obfuscation break allergen labeling controls?

A declaration is a partition of what is in the pack into what reaches the label and what does not. Every allergen is described correctly on the side it lands on. The two sides always add back up to the ingredient list, because moving one across does not create or destroy an ingredient. And the label is generated from the same classification that decided it -- so a label that omits milk is perfectly consistent with the assessment that omitted it, and the record of the decision carries no trace of the error.

Four declared allergens and one precautionary statement

The fixture is one product on one line: eight ingredients, four regulated allergens present -- gluten from the flour, milk from butter and from milk powder, soya from the lecithin in the chocolate, egg from whole egg -- and a production line whose previous run was a peanut product with an unvalidated changeover. The correct label reads CONTAINS EGG, GLUTEN, MILK, SOYA; MAY CONTAIN PEANUT.

Rename the option carrying the list of allergens that a label has to name and nothing is regulated any more. The plant's own label rule catches it immediately: it refuses a declaration where allergens sit in the recipe and none are declared. That is a good rule and it works.

Rename the flag that enables that rule alongside it and the same pack ships. The measured result: ingredients=8 assessed=8 DECLARED=0 precautionary=0 and a statement reading CONTAINS <nothing>.

Eight ingredients. All eight assessed. A complete, internally consistent declaration of nothing, on a product containing four of the fourteen allergens a label is legally required to name.

Protection on its own moved nothing

The five presets ran first, as in every area this pass: the ES5 default path, the modern target, the emit gate's option set on both targets, and the string transforms. Twenty-five comparisons across five areas, all byte-identical to the unprotected run, under a guard that runs each original twice before comparing anything to it.

So this is not a protection result. It is a naming-configuration result, and the specific names involved are ones that cross the boundary into an installed labelling library.

The same empty label by a second route

Renaming the plant's label rule instead of the flag that enables it produces the identical outcome. The library substitutes its own built-in rule, which accepts any label, and the declaration goes through reading CONTAINS <nothing> once more.

The difference between the two routes matters when you are reading a log. In the flag route the rule is present, correct, bound and never called. In the substitution route something called a label check does run, and passes, and reports itself. Asking the system whether a label check is configured returns a true answer to the wrong question in both.

The honest negative sits beside them. Renaming the allergen arrays on the ingredient records is loud, because this bundle both writes those arrays and reads them in its own label rule. The library sees nothing and declares nothing; the plant's rule still sees four allergens in the recipe and refuses. A guard written by the party that writes the data it inspects is invisible to a rename by construction -- which is exactly why it stayed loud here, and exactly why it should not be relied on as evidence.

Four routes to a lost may-contain statement, all of them silent

The precautionary statement is the quieter half of this label, and it came off four separate ways, none of which the plant's rule refuses -- because that rule only fires when nothing at all is declared.

Renaming the cross-contact matrix, the precautionary threshold, the line identifier on the product record, or the quoted line keys inside the matrix each produced DECLARED=4 precautionary=0 and the statement CONTAINS EGG, GLUTEN, MILK, SOYA. Four correct allergens, correctly named, on a pack made on a line that last ran peanut without a validated changeover.

Every one of those arms leaves a label that reads like a normal label. There is no blank, no undefined, no missing field. The pack simply stops warning about the thing the plant knows about and has documented in a matrix that is still sitting in the configuration.

One arm is genuinely inert and it is worth naming for honesty: the validated-clean-lines option. It is inert because the product in this fixture runs on the unvalidated line, so the validated list is never consulted. That is a property of the fixture, and a different product would move it.

The grimmest cell

Renaming the ingredient allergen arrays together with the flag that enables the plant's rule produces a label reading CONTAINS <nothing>; MAY CONTAIN PEANUT.

The pack now warns about the one allergen it does not contain and stays silent about the four it does. The cross-contact path still works, because it reads a matrix keyed by line rather than the ingredient records. The recipe path does not.

It is not a plausible thing for a person to write and it is a perfectly plausible thing for a build to produce, and that is the whole point of measuring rather than reasoning.

Why the record of the decision is no help

A partition failure has a property that the failure shapes earlier in this series do not: the audit artefact is generated from the same classification that went wrong. The label is produced from the declared list. The declared list is what moved. So the label is a faithful record of the decision that was taken, and contains no trace of the decision that should have been.

The counts do not help either, because they are conserved. Eight ingredients in, eight assessed, and the two sides of the partition still add back up to the ingredient list -- moving an allergen from the declared side to the not-present side does not create or destroy an ingredient.

And the detection asymmetry runs the wrong way, as it does for every partition. Declaring too much is caught by the first person who reads a label with an implausible allergen on it. Declaring too little is caught by a consumer.

What failed loudly

Renaming the allergen arrays alone, as described above, refused the declaration. So did renaming the regulated list on its own. Both are the plant's rule doing its job, and both are the reason that rule is worth having.

Renaming the ingredient identifiers, the ingredient names, the product identifier or the supplier field moved only display text: the declaration itself was correct in all four. Renaming the fields the recipe system writes -- which arrive as text and are only read here -- moved only the specification header line.

Set against those, the pairs above are silent. The label prints. The counts print. The rule, where it still runs, prints a true sentence.

What to assert

Assert one known allergen end to end. Take a product you know contains milk and assert that the generated label text contains the word MILK. Not that the assessment ran, not that eight ingredients were processed -- that the string on the pack names the allergen. That single assertion fails in both empty-label arms and costs one line.

Assert the precautionary statement the same way, on a product you know runs on a line with an unvalidated changeover. That fails in all four of the silent arms.

Treat an empty declaration as an event requiring a person rather than a clean result. A product with no allergens is possible; a product whose recipe lists allergens and whose label lists none is not, and that comparison is the plant's rule, which is why it is worth keeping reachable.

For the renaming itself: exclude the property names that cross into the labelling library, and keep the label-text assertion as the test that would fail if somebody removed the exclusion. This series has now measured the same lesson in twenty areas -- the engine renames what it is told to rename, and deciding what crosses a boundary is the part that has to be got right, exactly as it is for the protected-health-data paths covered in our HIPAA notes.

Frequently asked questions

Did obfuscation on its own break allergen labelling?

No. All five presets produced output byte-identical to the unprotected run across the five areas measured this pass. Every failure below required member renaming reaching names the installed labelling library reads.

What produced a label reading CONTAINS nothing?

Renaming the regulated-allergen list together with the flag that enables the plant's own label rule. The list alone is caught cold by that rule; the pair ships a pack containing milk, egg, gluten and soya under an empty declaration.

Is there a second route to the same empty label?

Yes. Renaming the plant's label rule itself makes the library substitute its own built-in, which accepts any label. Something called a label check still runs and still passes.

How many ways did the may-contain statement disappear?

Four: the cross-contact matrix, the precautionary threshold, the line identifier on the product record, and the quoted line keys inside the matrix. All four left the four declared allergens correct, and none was refused by the plant's rule.

Why did renaming the ingredient allergen arrays fail loudly?

Because this bundle both writes those arrays and reads them in its own label rule, so both sides moved together while the unprotected library's read did not. The library declared nothing and the plant's rule still saw four allergens and refused.

Do the counts give any warning?

No. Eight ingredients in and eight assessed in every arm, because moving an allergen from one side of the partition to the other does not create or destroy an ingredient. The label is generated from the same classification, so it agrees with itself.

What is the cheapest test that catches this?

Assert that the generated label text for a product you know contains milk includes the word MILK, and that a product made on an unvalidated line carries its precautionary statement. Both fail in the arms above; a test that checks the assessment ran passes them.

Related reading