Scoping a Recall by Traceability Lot Code Without Over- or Under-Scoping
The exact failure this page resolves shows up the first time you scope a recall with a plain WHERE traceability_lot_code = ? query and two facilities have used the same lot-code string. The query returns rows from both, so the recall either widens to include a completely unrelated product — over-scoping — or, if a well-meaning deduplication step collapses the two into one lot, it quietly drops a genuinely affected lot — under-scoping. Both outcomes are compliance failures: one destroys good product and buries the investigation in noise, the other leaves contaminated product on the shelf. The root of both is the same misconception, that a traceability lot code is a global identifier. It is not, and the Lot-Level Recall Scoping engine keys its whole graph around that fact.
This guide walks the specific collision that produces the bug, reproduces it against realistic Key Data Element payloads, and fixes it by namespacing the lot code with the Global Location Number of the facility that assigned it. The fix is small — a composite key — but it changes the correctness of every recall query downstream, including the chain expansion in one-up, one-back reconstruction and the record request in FDA 24-hour response automation.
Root Cause: A Lot Code Is Only Unique Where It Was Assigned
Under FSMA 204, a traceability lot code is assigned by the entity that originates, transforms, or first packs the food, per 21 CFR 1.1320. The rule requires the code to identify the lot; it does not require, and cannot require, that the string be unique across every unrelated facility in the supply chain. Uniqueness is scoped to the assigning location. That is a deliberate feature of the regulation — a farm and a distributor three tiers away have no coordination mechanism to guarantee distinct strings — but it is a trap for anyone who treats the code as a primary key.
Collisions are not rare edge cases; they are the predictable result of how facilities generate codes:
- Date-based codes. A facility that stamps
LOT-20240512orRM-0512from the pack date will collide with every other facility using the same convention on the same day. Two romaine packers in different states routinely produce identical strings. - Per-facility sequential counters. A plant that assigns
000123from an internal counter that resets or that started at the same seed as a sister plant produces the same low-numbered codes at both sites. - Shared conventions across a cooperative. Growers in a co-op often adopt one code template, so the assigning-location context is the only thing that distinguishes their lots.
The regulatory consequence is precise. The traceability lot code KDE (§ 1.1320) is meaningful only when paired with the traceability lot code source or its location reference — the ship-from and receive-to GLNs recorded on the shipping and receiving CTEs (§ 1.1340 and § 1.1345). Strip that pairing away in a recall query and you have discarded the one piece of information that makes the code identify a real lot. The engineering rule that follows: never match on the lot code alone; the identity of a lot is the pair (assigning_location_gln, traceability_lot_code).
Figure — the same lot code, two facilities, two scoping outcomes:
Minimal Reproducible Example
The collision is easiest to see against a small ledger slice where two facilities have independently stamped RM-0512. Facility A (GLN 0086000000012) assigned it to a romaine lot; Facility B (GLN 0086000000029) assigned it to an unrelated spinach lot. Only Facility A’s lot is implicated by the contamination signal.
from decimal import Decimal
# Realistic ledger slice: two physically distinct lots that share a code string.
LEDGER = [
{"assigning_location_gln": "0086000000012", "traceability_lot_code": "RM-0512",
"product_description": "Romaine, chopped", "cte_type": "Shipping",
"counterparty_gln": "0086000000100", "quantity": Decimal("40"), "unit_of_measure": "case"},
{"assigning_location_gln": "0086000000012", "traceability_lot_code": "RM-0512",
"product_description": "Romaine, chopped", "cte_type": "Shipping",
"counterparty_gln": "0086000000117", "quantity": Decimal("18"), "unit_of_measure": "case"},
{"assigning_location_gln": "0086000000029", "traceability_lot_code": "RM-0512",
"product_description": "Spinach, baby", "cte_type": "Shipping",
"counterparty_gln": "0086000000208", "quantity": Decimal("55"), "unit_of_measure": "case"},
]
CONTAMINATED_LOT_CODE = "RM-0512" # the signal named only the code, not the facility
# Naive scope: match on the lot code alone.
over_scoped = [row for row in LEDGER if row["traceability_lot_code"] == CONTAMINATED_LOT_CODE]
print("rows in scope:", len(over_scoped)) # -> 3
print("products in scope:", {r["product_description"] for r in over_scoped})
# -> {'Romaine, chopped', 'Spinach, baby'} <- spinach does not belong in this recall
Three rows come back and two of them are the wrong product. The recall now spans an unrelated spinach lot that never touched the contaminated romaine, over-scoping the event. The under-scoping variant is just as easy to trigger: a team that notices the duplicate and “cleans” the data by deduplicating on the lot code alone collapses Facility A and Facility B into a single row, and depending on which survives the dedup, a genuinely affected lot silently vanishes from the recall.
# The "cleanup" that causes under-scoping: dedup on the code alone.
deduped = {row["traceability_lot_code"]: row for row in LEDGER} # last write wins
print("lots after dedup:", len(deduped)) # -> 1
print("survivor:", deduped["RM-0512"]["product_description"]) # -> 'Spinach, baby'
# Facility A's romaine — the ACTUAL affected lot — was overwritten and dropped.
One dict keyed on the code reduces two distinct lots to one, and the survivor is the wrong one. The affected romaine lot is now invisible to every downstream query. Both bugs stem from the same root: the code was treated as globally unique when it is only locally unique.
Fix: Namespace the Lot Code by Assigning-Location GLN
The fix is to make the assigning-location GLN part of the key everywhere a lot is identified. A lot is (assigning_location_gln, traceability_lot_code), never the code alone. Seeding a recall then requires both halves — which the contamination signal can supply, because the implicated product came from a known facility — and every match, dedup, and join uses the pair.
from decimal import Decimal
def lot_key(row: dict) -> tuple[str, str]:
"""The only correct identity for a lot: assigning GLN plus lot code."""
return (row["assigning_location_gln"], row["traceability_lot_code"])
def scope_by_lot(ledger: list[dict], seed_gln: str, seed_tlc: str) -> list[dict]:
"""Scope on the composite key so a shared code string cannot cross facilities."""
target = (seed_gln, seed_tlc)
return [row for row in ledger if lot_key(row) == target]
# The signal now carries the facility that assigned the code, not just the code.
scoped = scope_by_lot(LEDGER, seed_gln="0086000000012", seed_tlc="RM-0512")
print("rows in scope:", len(scoped)) # -> 2 (both Facility A shipments)
print("products in scope:", {r["product_description"] for r in scoped})
# -> {'Romaine, chopped'} spinach is a different node and is correctly excluded
# Namespaced dedup keeps distinct lots distinct.
deduped = {lot_key(row): row for row in LEDGER}
print("lots after dedup:", len(deduped)) # -> 2, nothing dropped
With the composite key, the romaine recall returns exactly Facility A’s two shipments and nothing else, and the namespaced dedup preserves both physical lots instead of collapsing them. The unrelated spinach lot is a different node in the graph — it was never reachable from the seed and is correctly excluded, which is the structural collision defense the parent Lot-Level Recall Scoping engine relies on. Where the ledger genuinely stores only a bare code, the fix belongs upstream at ingestion, where Schema Validation Rules can require the assigning GLN alongside every lot code so the pair is never separated in the first place.
Verification: SQL and Assertions
Confirm the fix at the query layer first, because most recall scoping runs as SQL against the ledger. The wrong query and the right query, side by side, make the difference concrete:
-- WRONG: matches the code across every facility, over-scoping the recall.
SELECT assigning_location_gln, traceability_lot_code, product_description
FROM ledger_events
WHERE traceability_lot_code = 'RM-0512';
-- returns Facility A romaine AND Facility B spinach
-- RIGHT: the composite key confines scope to the lot that was actually assigned.
SELECT assigning_location_gln, traceability_lot_code, product_description
FROM ledger_events
WHERE assigning_location_gln = '0086000000012'
AND traceability_lot_code = 'RM-0512';
-- returns Facility A romaine only
To find collisions before they distort a recall, audit the ledger for any lot code that more than one facility has assigned. A non-empty result is a list of codes where scoping on the code alone would over-scope:
-- Collision audit: lot codes assigned by more than one location.
SELECT traceability_lot_code, COUNT(DISTINCT assigning_location_gln) AS facilities
FROM ledger_events
GROUP BY traceability_lot_code
HAVING COUNT(DISTINCT assigning_location_gln) > 1
ORDER BY facilities DESC;
Then pin the behavior with assertions so a regression cannot reintroduce the bare-code match:
def test_composite_key_prevents_cross_facility_scope():
scoped = scope_by_lot(LEDGER, "0086000000012", "RM-0512")
# Over-scope guard: no unrelated product leaks in.
assert {r["product_description"] for r in scoped} == {"Romaine, chopped"}
assert all(r["assigning_location_gln"] == "0086000000012" for r in scoped)
assert len(scoped) == 2
# Under-scope guard: namespaced dedup drops nothing.
deduped = {lot_key(row): row for row in LEDGER}
assert len(deduped) == 2
# Collision detection: the bare code really is shared across facilities.
facilities = {row["assigning_location_gln"]
for row in LEDGER if row["traceability_lot_code"] == "RM-0512"}
assert len(facilities) == 2
print("verified: composite key scope is exact, dedup lossless")
test_composite_key_prevents_cross_facility_scope()
The three guards map to the three failures: no foreign product in scope (no over-scope), a lossless dedup (no under-scope), and a positive collision count that proves the test is exercising a real shared code rather than a trivially unique one. Wire the collision audit into a scheduled check so a new supplier feed that introduces a colliding code convention surfaces before a recall, the same continuous posture the real-time data quality checks for traceability guide applies to ingestion.
Related Edge Cases to Check Next
- Whitespace and case variants of the same code.
RM-0512,rm-0512, andRM-0512are three keys to a naive match but one lot to a human. Normalize the code — trim and case-fold — at ingestion before it becomes half of the composite key, so a formatting variant does not fragment a single lot into several nodes and under-scope it. - Transformation reassigns the code within one facility. When a facility transforms an input lot into a new product it assigns a fresh code under § 1.1350, so the same GLN can legitimately hold two different lot codes that are causally linked. Scope must follow the transformation edge, not assume one GLN maps to one lot; the one-up, one-back reconstruction guide handles that linkage.
- A GLN that itself is reused or mis-keyed. If two physical sites share a single registered GLN, the composite key collides again one level up. Validate GLN check digits and confirm each GLN maps to exactly one physical location as part of GS1 identifier validation before trusting it as a namespace.
Frequently Asked Questions
Why is a traceability lot code not globally unique?
Because FSMA 204 assigns the code at the point of origin, transformation, or initial packing under § 1.1320, and there is no cross-industry registry that hands out unique strings. Uniqueness is guaranteed only within the assigning facility. Two unrelated packers can and do produce the same string, especially with date-based or sequential conventions, so a code only identifies a real lot when paired with the location that assigned it.
What is the practical difference between over-scoping and under-scoping?
Over-scoping pulls unrelated product into the recall — a query on a shared code returns a second facility’s lot that never touched the hazard — which wastes product and buries the investigation in noise. Under-scoping drops a genuinely affected lot, usually when a deduplication step keyed on the bare code collapses two distinct lots into one and the wrong one survives. Over-scoping is expensive; under-scoping is dangerous, because contaminated product stays on the shelf.
How does the assigning-location GLN fix both failures at once?
Making the key the pair of assigning GLN and lot code turns each physical lot into a distinct node. A shared code string across two facilities becomes two keys, so a scope query returns only the intended lot and a dedup preserves both instead of merging them. One change to the key eliminates the cross-facility match that causes over-scoping and the lossy collapse that causes under-scoping.
How do I find lot-code collisions before a recall exposes them?
Run a periodic audit that groups the ledger by lot code and counts distinct assigning GLNs, flagging any code assigned by more than one facility. A non-empty result lists exactly the codes where a bare-code scope query would over-scope. Wiring that audit into continuous data-quality monitoring surfaces a colliding code convention when a new supplier feed introduces it, rather than during a live traceback.
Should the contamination signal include the facility, or just the lot code?
It should include the facility. The implicated product is always traced to a known origin — a supplier advisory, a positive sample from a specific lot, a customer complaint tied to a shipment — so the assigning GLN is available at seeding time. Seeding on the pair rather than the bare code is what lets the scope stay minimal from the very first node instead of over-scoping and being narrowed later.
Related
- Lot-Level Recall Scoping — the scoping engine whose composite node key this fix underpins.
- Simulate an FDA 24-hour traceability record request — drives the scoping engine end to end once the key is correct.
- One-Up, One-Back Reconstruction — follows transformation edges where one facility legitimately holds multiple lot codes.
- GS1 Identifier Validation — validates the GLN that serves as the lot-code namespace.
- Real-time data quality checks for traceability — runs the collision audit continuously against incoming feeds.
Up: Lot-Level Recall Scoping — this fix is the collision-control rule the scoping engine depends on.