# Reconcile is a verdict, not a report

> transactions.dev · by visibility.cloud — 2026-07-31
> Written for the RFID systems-integrator / VAR practice — the practice lead who has to prove value in the receiving bay at go-live.

One thesis: **reconciliation between a shipment's paperwork and its physical
receipt is only worth deploying when it returns a machine verdict — a typed
diff with an exit code — because the moment that decides an integration's
fate is a go-live in a receiving bay, not a dashboard review three weeks
later.**

## The go-live problem, stated honestly

You run an integration practice. You have sold the reader infrastructure,
commissioned the portals, tuned the antennas, and the customer's acceptance
criterion is one sentence: *prove the system sees what arrives.* On go-live
day the evidence you actually possess is two artifacts that have never met:

- the **ASN** — the 856 or DESADV the supplier sent, claiming what left the
  dock: SSCCs, GTINs, quantities, lots;
- the **event stream** — what the portals captured: ObjectEvents and
  AggregationEvents, timestamped, with EPCs the readers actually saw.

Every practice has improvised this comparison in a spreadsheet at least
once, at 6am, with the customer's ops manager watching. The improvised
version has two failure modes: it is not repeatable, and it renders an
opinion instead of a verdict. "Looks like everything came in" is not an
acceptance artifact.

## The verdict shape

`transactions.dev reconcile` takes the document and the events and returns
the difference, typed:

```
$ npx transactions.dev reconcile 856-receipt-01.edi \
    --against events-receipt-01.jsonl --tz -05:00 --json
```

```json
{
  "document": "urn:epcglobal:cbv:bt:0614141000012:SHIP20260728002",
  "po": "urn:epcglobal:cbv:bt:0724142000017:PO-8813",
  "claimed":  { "pallets": 2, "cases": 60, "lots": ["2026A", "2026B", "L57"] },
  "observed": { "pallets": 2, "cases": 61, "lots": ["2026A", "2026B", "L58"] },
  "verdicts": [
    { "class": "matched", "sscc": "https://id.gs1.org/00/006141411234567890" },
    { "class": "matched", "sscc": "https://id.gs1.org/00/006141419876543210" },
    { "class": "matched", "gtin": "00614141073467", "lot": "2026A",
      "claimed": 36, "observed": 36 },
    { "class": "missing", "gtin": "00614141073467", "lot": "2026B",
      "claimed": 12, "observed": 11, "delta": -1 },
    { "class": "lot_mismatch", "gtin": "00614141765430",
      "documentLot": "L57", "observedLot": "L58", "quantity": 12 },
    { "class": "unexpected", "gtin": "00614141073474", "observed": 2,
      "note": "no ASN line covers this GTIN" }
  ],
  "verdict": "DISCREPANT",
  "exit": 1
}
```

That is a verbatim run, not an illustration: the document is the pinned
fixture `corpus/reconcile/856-receipt-01.edi` — two pallets, three lines,
sixty cases — and the events are `corpus/reconcile/events-receipt-01.jsonl`,
a dock read of both pallets and their contents. Re-running it produces the
same bytes.

Two things there are worth a paragraph.

**The handling units get verdicts, not just a count.** The first two entries
are the pallets, compared by SSCC. A claimed pallet no event observed is
`missing` in its own right — the single most common receiving discrepancy,
and the one a totals line can hide, because two claimed against two observed
says nothing about whether they are the same two. The summary line's
`pallets=2/2` and the verdict list cannot disagree, because they are the same
comparison.

**And `lot_mismatch` is the one a quantity comparison can never see.** The
document says lot L57 and the dock read L58; the counts agree exactly, twelve
against twelve, so every arithmetic check in the world passes. What arrived is
not what was promised. The verdict says so by name.

The exit code is the contract: `0` when shipped and seen agree, `1` when
they do not, `2` when the inputs are unusable. That is the same CLI law the
whole family runs — payload on stdout, typed errors on stderr, verdicts as
exit codes — so the reconcile step drops into a go-live script, a nightly
cron, or an acceptance test without a human in the middle. Wire it to the
gate: the lane goes live when reconcile exits zero three receipts in a row.

Two verdict classes deserve a sentence each. `missing` is the claim-side
story, and it is the one in this transcript twice: the ASN said 48 cases of
lot 2026B, the portal saw 47, and now the shortage conversation starts from a
machine record instead of a recollection. `unexpected` is the integrity-side
story: something came through the door that no paperwork covers — mis-pick,
substitution, or a mislabeled case — and it is caught at the threshold, not
at the customer complaint.

## The report is a contract, not a printout

Every field name in that transcript is documented surface, not incidental
output. The closed verdict-class vocabulary — `matched`, `missing`,
`unexpected`, `lot_mismatch` — the top-level `verdict`, and the
`0`/`1`/`2` exit semantics are the report contract this site publishes,
worked lot-by-lot against a second fixture in
[the dock-door post](/blog/the-asn-said-48-the-dock-saw-47/): same shape,
different shipment, including the `lot_mismatch` class a quantity-only
comparison can never see. Contract stability is what makes the verdict
wirable: your go-live script greps `"verdict"` and branches on the exit
code, and neither moves under you between engagements. And because the
comparison is a pure function of two sets, the report re-runs: a portal
that flushes buffered reads after an outage changes the observed set, and
the same ASN then upgrades this morning's `missing` to `matched` without
a human edit — the verdict history is itself evidence of when the site
knew what.

## Why the join is what makes this legal

The comparison only works because both sides speak the same identifiers,
and that is core-spec machinery, not product glue. The ASN side is compiled
by [the join verb](/blog/the-856-is-already-an-aggregation-tree/): the HL
tree becomes the expected aggregation structure, and the document's
identity rides along as `bizTransactionList` entries typed `desadv` and
`po` — [core CBV fields, not extensions](/blog/biztransaction-is-a-core-field/).
The event side is whatever your capture infrastructure already produces,
verbatim; when the discrepancy is settled and a receiving advice goes back
upstream, that answer already has a CBV name too — `recadv`.

Nothing in the reconcile path writes to the event stream. The events stay
what the portals observed; the documents stay what the partner claimed. The
verb's entire job is the difference — which also means it is safe to run
retroactively over last quarter's receipts the day it is installed, and the
backlog of quiet shortages it surfaces on day one is, in practice, the
strongest sales artifact a practice lead can walk into a renewal with.

## What this is not

Not a TMS, not a yard system, not a claims platform, and not a promise that
paperwork disputes disappear. A carrier will still argue. What changes is
what they are arguing against: a conformant event record with a typed diff
against the shipper's own ASN — evidence a counterparty can re-verify
without joining anything, which is the family's one sentence and the reason
the executive layer at [visibility.cloud](https://visibility.cloud) can
stand on records this layer produces. Your customer's auditors read the
verdict; their agents can pull the same verdict through the MCP server with
the same access a person would use.

At go-live, run reconcile, print the transcript, and hand it over. The
acceptance meeting gets very short when the artifact is a verdict.

---
Access: https://transactions.dev/get-access/ · All posts: https://transactions.dev/blog/ · Machine face: https://transactions.dev/llms.txt
