A merge lost a phone number on its first live run

Found by walking the scenario against production rather than by a test.
Two records, each with a phone; the survivor kept its own, and the
source's simply stopped existing. Searching for it returned nothing.

The first version's rule was "fill the survivor's blanks, never overwrite
what it has", which is right about which value WINS and said nothing
about the one that loses. One person can have two numbers, two spellings
of a name, a work address and a personal one - and a merge that quietly
deletes one is exactly the data loss this file already refuses elsewhere:
"silently turning Alice back into Visitor 3 is data loss the operator
cannot see happen."

The profile is now reconciled field by field in Go rather than in one
clever upsert, because the interesting case was never the winner. Blanks
are still filled and the survivor still keeps its own values, but every
losing value is returned in `discarded` AND appended to the survivor's
notes - the response is read once and the record is read forever.

Notes themselves are additive rather than a winner: two people writing
about one customer wrote two different true things.

mergeProfiles is pure, so the rule is asserted directly - four cases
including the ordinary one, a typed record joining a camera record with
no profile at all, which must add no noise.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KGcjxF1cNLcuwc3DAPcnfj
This commit is contained in:
2026-09-29 16:02:06 +05:30
parent 7dda6ab508
commit 7c564aca3c
3 changed files with 170 additions and 19 deletions

View File

@@ -194,6 +194,11 @@ type MergeResult struct {
// these on cards and read them aloud, so a merge has to say which one
// died rather than leaving somebody to discover it at a counter.
RetiredRef string `json:"retired_ref"`
// Discarded lists profile values the survivor already had a different
// answer for - a second phone number, a different spelling of a name.
// They are appended to the survivor's notes as well: this response is
// read once and the record is read forever.
Discarded []string `json:"discarded,omitempty"`
}
// MergeRequest names the record to keep.