A fix lets the batch merge job return candidates for the first time. Its commit message: “first production run is merging split folds tonight.”
Candidate sampling is randomized so that repeated rounds cover every address.
A read-only analysis pass, “triggered by the 2026-08-15 production cluster-merge run”, audits the result. A forensic script ranks every merge by risk class.
The matcher is rebuilt: identifier vetoes, reversible merges, rollback tooling and a CI precision gate (PR #2188).
The repair merges: “39 confirmed false merges from 08-15 repaired in prod with decision-edge provenance” (PR #2189).
Human verdicts become decision records, and every distinct-identifier pair from the run is dispositioned (PR #2200).
Dry runs surface four more false-merge classes. Each becomes a veto and a test fixture.
Incident write-up · August 15 to 17, 2026
39 false merges, repaired in about 13 hours
On the night of August 15 a batch merge job ran against production for the first time. It merged distinct businesses that only shared an address: grills inside one arena, stands at one venue. By 10:37 the next morning the 39 confirmed false merges were repaired in production with decision records, merges had become reversible, and a precision gate guarded the merge path in CI. Within 48 hours, four more false-merge classes found in dry runs had become vetoes.
- Impact
- 39 confirmed false merges of distinct businesses in the production entity graph
- Run to repair
- 13 h 4 min
- Found by
- A read-only audit of the run the next morning
- Root cause
- A second matcher that hadn’t been through the same checks ran against production
Written from the build record. Collection has been off since early September 2026.
Timeline
Root cause
Two matchers existed. Path A was governed: an identifier graph plus a language-model verifier, gated in CI at precision 1.0. Path B, a duplicate-cluster merge, blocked on address and merged on a name heuristic. It had not been through the fixtures, gate, verifier and rollback that Path A had.
Path B had never actually run. A row-shape bug made it return zero candidates on every invocation, silently. Fixing that bug on August 15 let it run for the first time.
Its rule merged any pair at a shared canonical address once name containment, shared tokens over the smaller name, reached 0.7. It used no negative evidence: two different active food licenses at one address were never consulted. A one-word generic name scores 1.0 against any longer name that contains it: “TACOS” fully matches “TACOS” plus any other words. Food halls and arenas file without unit numbers, so whole venues collapse onto one address. The arena grills scored 0.70 to 0.81.
Merges did not yet record pre-merge state, so making them reversible became part of the fix.
The cause, in one line: a second matching step that hadn’t been through the same checks as the first ran against production.
Detection
The next morning a read-only analysis pass audited the run. A forensic script ranked every merge by risk class, worst first: pairs that held distinct identifiers. Distinct current licenses in the same series at one address made the clearest class, because two active licenses in one series are two businesses.
Fix
Rebuild, 10:13
Identifier vetoes, one shared name-similarity module, and run-scoped reversible merges: every merge edge now records its run and the prior canonical record of both sides, and that migration was applied to production before the code merged. Rollback tooling reverts newest-first. A nightly pipeline keeps the scheduled merge in dry run. The rebuild also fixed three latent defects, including an entity-resolution tier that could not run at all: its tests mocked the database, so its SQL had never executed.
Repair, 10:37
Only the unambiguous class was repaired automatically: distinct current licenses in the same series. Cross-series pairs stayed merged as dual licensing, which avoided 52 false vetoes. Ambiguous-series and registry or FEIN conflicts went to a person, not to the script. The script ran dry by default and needed an explicit apply flag.
Adjudication, 12:19
Human verdicts became decision records (same entity, different entity, needs review), so reviewed pairs stop reappearing in forensics.
Verification
- In production. 39 confirmed false merges repaired with decision-edge provenance, and every distinct-identifier pair from the run dispositioned by 12:19.
- In CI. A merge precision fixture of 23 labeled pairs, most negatives taken from real production dry runs. One false positive fails the build.
- Locally. A real-data merge evaluation over 231 pairs scored precision 1.000 and recall 1.000, with three identifier vetoes firing at a food hall. This was a local run, not part of CI.
- Rollback refuses to guess. Merges made before reversibility existed are reported as unrevertible instead of being undone by inference.
What changed after
- Dry runs became the fixture source. By August 17 four more false-merge classes were vetoes with fixtures: license succession, where 310 of 469 shared-identifier proposals were confirmed hand-offs; hotels pulled into merge scope, 262 of 1,950 entities in the first dry run; numbered carts, 95 proposals at one venue; and outlets that share a venue name.
- Decisions are data. Every accept, veto or deferral writes a decision record, and human verdicts are durable.
- Ingest supersedes instead of dropping. An identifier conflict at ingest is recorded and superseded, never silently dropped.
- The gate is asymmetric, on purpose. A missed merge leaves a visible duplicate. A wrong merge ships a false identity claim. The merge path is tuned against the second.
The lessonKeep one governed matcher. Name containment is the wrong thing to decide identity, and a second path with write access will eventually run.