Shipped vs seen: receiving is a set diff.
Receiving looks like a workflow problem — clipboards, dock doors, a WMS screen with an exceptions queue someone works by phone. Strip the ceremony and it is set arithmetic: the ASN asserts an expected set (what the shipper says is on the truck), the scanners record an observed set (what actually crossed your dock), and every receiving decision is a function of the difference. If both sets are conformant EPCIS, that difference is a deterministic diff — computable, typed, and boring. Boring is the goal.
Two sets, one grain discipline
The expected set comes from the document side. The 856 maps deterministically onto events: the ASN's packing tree becomes aggregation assertions, its lines become GTIN-grain quantities, its references become bizTransactionList entries. After the join, "what the ASN said" is a set of (epcClass | epc, quantity) claims hanging off a desadv reference.
The observed set comes from capture. Dock scans land as ObjectEvents — bizStep: receiving, the same desadv reference stamped on each event, quantities at whatever grain the label actually carried. Because event identity is the CBV §8.9 content hash, a flaky scanner that fires twice adds nothing: the replay dedupes to one stored event, so the observed set is stable under exactly the failure modes a dock produces.
The one design decision that matters is grain. If the product is lot-tracked, the diff must run at GTIN+lot — an LGTIN key — not at GTIN. A count that matches at GTIN grain can hide a substituted lot, and the substituted lot is precisely the case a trace-back later turns on. The rule: diff at the finest grain both sides carry, and record when the two sides disagree about what that grain is.
The run
biztx reconcile takes the ASN-derived expected set and the captured event set for one desadv reference and emits typed discrepancy records. Here is the run over the worked fixture family from the 856 post — the ASN promised 96 EA of lot 7A-4412; the dock saw 84 of it, and 12 of a lot the document never mentioned:
$ biztx reconcile --desadv urn:epc:id:gdti:0614141.11711.5501 --json
{
"ok": false,
"desadv": "urn:epc:id:gdti:0614141.11711.5501",
"expected": 1,
"observed": 2,
"discrepancies": [
{
"kind": "short",
"epcClass": "urn:epc:class:lgtin:0614141.007346.7A-4412",
"expected": 96,
"observed": 84,
"delta": -12
},
{
"kind": "substitution",
"expectedEpcClass": "urn:epc:class:lgtin:0614141.007346.7A-4412",
"observedEpcClass": "urn:epc:class:lgtin:0614141.007346.7B-0090",
"observed": 12,
"note": "gtin-grain count matches; lot-grain does not"
}
]
}
Exit code 1 — the negative domain verdict, distinct from a crash, same as every verb on this origin. The vocabulary of kind is closed: short, over, substitution, unexpected. Four words, machine-routable:
- short — expected quantity not observed. Routes to the claims process, with the ASN reference attached.
- over — observed beyond the expectation. Routes to a hold-and-verify step.
- substitution — grain mismatch inside a matching coarse count: the wrong-lot case. Routes to quality, because this is the record a recall interrogates.
- unexpected — observed with no expectation at all. Routes to a human, every time.
A discrepancy record is not prose in a comment field. It is a typed result an agent can branch on without regexing a message — the same error-surface discipline as the rest of the door's contract. And because both inputs are conformant documents validated against the pinned official schema, a discrepancy is never an artifact of two systems disagreeing about field names: the diff runs over one vocabulary, so what it reports is a fact about the freight.
The receiving advice is downstream, not separate
Here is the part that removes an integration project from your roadmap: the receiving advice — recadv in CBV's own vocabulary, the 861 in X12 terms — is a projection of this diff. What you accepted is the observed set minus the holds; what you dispute is the discrepancy list. Systems that author the receiving advice by hand, in a separate screen, from separate data, are re-deriving the diff informally and paying for every divergence in a reconciliation meeting. Compute the diff once, project the document from it, and the document and the record cannot disagree — because one is a function of the other.
The view upstairs is the same diff
Nothing in this post is developer-only. The exception dashboard an operations VP reads — shorts by supplier, substitution rate by lot, unexpected receipts by site — is this same diff, recomputed over the same events, rendered in the executive register. That surface is visibility.cloud: the identical spine read from the business side, traces and custody evidence instead of exit codes. You build the diff once; both audiences read it.
Start with the sets. The fixture family validates today — npx epcis.dev validate on the ASN-derived events and the receiving events, no account, exit code is the answer. Run the diff on your own dock: reconcile rides the capture surface with the rest of the document verbs — get a capture key and say what you will capture.
We answer in writing. We take at most five conversations a month, only when you ask for one, and only after you already have the written read.