Measured, not asserted
Published
Every failure this series has measured ends in an answer that is wrong: a value missing, unreadable, in the wrong unit, taken over the wrong set, or read off the wrong row. A recall scope offers none of those. Every lot it names really does contain the suspect material. The lots that are wrong are the lots that are not there.
A recall answer is a walk, and a walk can stop early
A suspect resin lot goes into two moulded sub-assemblies. One of those is consumed by a finished pump that shipped 1,200 units. The other passes through a rework lot before reaching a second finished pump that shipped 300 units. Asking which finished goods contain the suspect resin is a walk up the consumption graph, and the correct answer is six lots and 1,500 units in customers' hands.
Two configuration values decide where that walk stops: how many levels it follows, and whether it follows rework lots. Both live in the options object handed to the traceability engine. Neither of them can make the walk return a lot that is not affected. All they can do is make it stop.
That is what makes this shape different from everything measured before it. A truncated recall is a correct PREFIX of the right answer. Every row in it survives inspection, every lot number resolves, every quantity reconciles against the lot record it came from. There is nothing in the output to find, because the defect is the absence of rows.
The mitigation worked twice, and then it did not
The caller in this test carries the set-shaped version of the mitigation the previous pass validated: rather than asking whether the answer looks sensible, its rule asks whether the traversal ran out of levels with consumers still unvisited, and whether any branch was skipped.
Renaming ^(traceDepth)$ alone was caught cold. The library fell back to a single level, the walk stopped with an open frontier, and the rule refused to issue: scope-rejected(completeness-check(the walk stopped with consumers unvisited)). Renaming ^(includeRework)$ alone was caught too, by the same rule counting the branch it had skipped. Two for two, which is a good result for a five-line check.
Renaming ^(consumedLots)$ -- the field on the lot record that names what each lot consumed -- was not caught. The graph had no edges the library could read, so the walk finished immediately, the frontier was empty, no branch was skipped, and the rule reported checked by completeness-check(the walk exhausted every consumer). That sentence is TRUE. The walk did exhaust every consumer it could see. There were none.
The issued recall covered one lot and zero units shipped. The suspect resin, and nothing else -- which is precisely the answer a completeness check defined over the traversal's own state is unable to object to, because the evidence it reads was produced by the same lost name.
Two truncations, two different silences
Losing the enabling flag in front of the check reproduces a pattern this series has now measured in several domains. ^(traceDepth|requireScopeCheck)$ issued a recall covering three lots and zero shipped units, and printed frontier-open=true on the way out. The one line that proves the answer is incomplete was computed, printed, and consulted by nothing, because the flag that decides whether to consult it went in the same pattern.
The rework variant is quieter and, to my mind, the more dangerous of the two. ^(includeRework|requireScopeCheck)$ issued a recall covering four lots and 1,200 of the 1,500 units -- and printed frontier-open=false. That is not a check being skipped. The walk genuinely did exhaust every consumer it was permitted to follow. A completeness check that was still running would have passed this one, because the branch was not truncated, it was filtered.
So there are two ways to stop short and only one of them looks like stopping short. A depth limit leaves a visible edge. A filter leaves nothing at all, and the 300 units behind the rework lot are simply not in the notice.
The set was right and the count was zero
One more arm is worth reporting because it is the mirror image. Renaming ^(shippedUnits)$ left the recall scope perfect -- all six lots, correctly identified, in the right order -- and reported 0 ea shipped. The completeness check passed, correctly, because completeness of the SET was never in question.
A regulator reading that notice sees a company that has traced its genealogy properly and concluded no product reached customers. Every lot number in the notice can be verified against the batch record. The one number that decides whether anybody is at risk is the one that moved.
By contrast, renaming ^(lotId)$ refused the whole traversal on the first line with unidentified-lot(refused before traversal), and renaming ^(scopeRule)$ silently swapped the caller's rule for the library's built-in one, which accepts anything -- while the output still read checked by library-builtin, so a search for evidence that a check ran still finds the word.
What to do about it
Nothing here happens under default protection, and nothing here happens to names you did not aim a pattern at. Member renaming is opt-in. Keep patterns anchored to names both written and read inside your own bundle, and keep the vocabulary of any record that crosses into an installed engine -- link fields especially -- out of scope.
Then fix the check, because the check is the interesting part. A completeness assertion defined over the traversal's own state can only see work it knows was left undone. Give it an independent expectation instead: the number of edges the genealogy SHOULD have, read from the batch record rather than from the traversal, and refuse an answer that walked fewer. A recall that reports zero shipped units for a lot the shipping system says shipped 1,500 should be an error, not a notice.
The general form is the same one this series keeps arriving at from different directions: make a missing thing an ERROR rather than an empty set, a fallback, or a shorter walk. An empty answer and a complete answer are indistinguishable to any check that reasons only about the answer.
Frequently asked questions
Does obfuscation change what a recall covers?
Not on its own. Across five presets and five areas measured this pass, protection alone left every output identical to the unprotected run -- twenty-five comparisons, no differences. What changed behaviour here was member renaming, a separate opt-in option whose pattern you write.
What was the worst result measured?
Renaming the record field that links a lot to what consumed it. The traceability engine saw a graph with no edges, issued a recall covering one lot and zero shipped units instead of six lots and 1,500 units, and the caller's completeness check reported the walk complete.
Why did the completeness check not object?
Because it is defined over the traversal's own state -- was the frontier open, was a branch skipped. A walk over a graph with no edges finishes immediately with nothing left over, so every question the check asks has an honest, reassuring answer.
Is a truncated recall detectable by spot-checking the output?
No, and that is the structural point. Every lot in a truncated recall genuinely contains the suspect material, so every row you inspect is correct. Only the rows that are absent are wrong, and you cannot inspect an absent row.
Did anything fail loudly?
Yes. Renaming the lot identifier refused the whole traversal on the first line. Renaming the depth option alone, or the rework option alone, was caught by the completeness check. The dangerous cells were the ones that also reached the flag in front of that check, or the data underneath it.
Is this specific to obfuscation?
No. A serialisation hop that drops a key, a schema migration that renames a link column, or a library upgrade produce the same truncated walk. Renaming is simply a fast way to discover which of your controls can see the difference.
What is the smallest useful change to make?
Compare the traversal against an independent expectation rather than against itself: the number of consuming lots the batch record says exist, and the shipped quantity the shipping system reports. Refuse to issue a notice that disagrees with either.
Related reading