[S03] Merge History
Summary
The platform screen that shows what merges have been performed, what each one decided, and what it moved. It is the record of the one act in person identity that no machine is allowed to take on resemblance, so its job is to make each merge answerable after the fact: who decided, on what evidence, and whether they overrode the system’s own assessment.
It is also where a merge can still be questioned while that is possible, and where the absorbed person’s retained details are purged — on schedule, or on request.
Actor & Context
Actor: platform administrator — the same grant that S02 requires.
Frequency: occasional. Two distinct visits: shortly after merging, to re-read a decision while the absorbed record still holds its detail; and later, to answer a question about a specific person.
Precondition: the session carries the super-admin grant.
Entry point: sidebar PLATFORM ▸ Merge history. Also from a resolved pair in the duplicate-people queue, which links to the merge it produced.
Scope and disclosure
Platform scope, for the same two reasons as S02: a merge reaches every organisation, and reading one means reading personal detail across the organisation boundary. The ALL TENANTS marker is mandatory beside the page title.
One addition specific to this screen. While a merge retains the absorbed person’s data, this screen can display details that no longer belong to any active person — including medical, emergency-contact and guardian fields. That is the point of retaining them, and it is also why the retention window is bounded and why the purge is reachable from here.
What this screen is for
Three jobs, in the order they justify it. The design follows from them.
- Accountability
-
Every merge carries the assessment as it stood when it ran, and whether the operator went ahead against a negative one. That pairing is the ADR-0012 record: a machine may act on identity, a human may act on resemblance, and this is where the difference is visible.
- A review window
-
A merge whose retained data has not yet been purged is still questionable — the absorbed record’s values can be read and compared against what the survivor kept. Once purged, only the provenance remains: which side won each field, never what the losing value was.
- Erasure
-
A data-subject request under the Protection of Personal Information Act names a person, not a merge. This screen is where retained data is purged ahead of its due date.
Layout
Both views render inside the portal shell. This screen owns the main content area.
List — /platform/merge-history
| Region | Content |
|---|---|
Page header |
Title, the |
Filters |
A date range, a person, and a retention state. A free-text box searches both people’s names. The trigger filter is deliberately absent — see Why there is no trigger filter. |
Table |
One row per merge: when, the record that was kept, the record that was absorbed, how the merge was triggered, who performed it, and the retention state with its purge-due date. Newest first by default; the date column sorts. The operator column reads as absent rather than blank where no operator was captured — see Design Decisions. The absorbed record is usually nameless: see The absorbed record usually has no name. |
Row expansion |
Opens the detail for that merge in place, rather than navigating away. A working session reads several merges in sequence, and losing the list position each time would make that tedious. |
The two people are labelled kept and absorbed. The stored columns are named sourcePerson and targetPerson, which describe the merge’s inputs and not its outcome — sourcePerson is the record that was absorbed and cleared. Carrying that vocabulary onto the screen would invite exactly the wrong reading.
The absorbed record usually has no name
A merge clears the absorbed record’s personal data, and that includes the display name. So for nearly every merge already on record there is no name to show: at the time of writing, 2,456 of 2,459 merges in production have a nameless absorbed record.
The screen therefore does not promise a name it does not have. The absorbed cell shows the record’s identifier, and says that the details were cleared by the merge, with the merge date as the moment they went. It reads as a statement about what the merge did rather than as a rendering fault, which is what an empty cell beside a populated one looks like.
This is the same rule as the operator column and the purged field values: where something is genuinely absent, say so and say why, rather than leaving a gap the reader has to interpret.
Retention changes it going forward — a merge still inside its window has the name, because it has everything. It cannot change it backwards: nothing recovers a name cleared before retention existed. So the list shows both kinds side by side and must not look broken doing it.
One consequence for search: the operator can search on both names, but for an unretained merge only the surviving name can match. The screen says so where the search is, rather than letting a search for someone absorbed years ago look like a search that found nothing.
Merge detail
| Region | Content |
|---|---|
The decision |
The assessment recorded at merge time: whether the pair assessed as the same person, which details agreed and which disagreed. See An overridden assessment for the case that matters. |
What each field became |
One row per field: which record contributed the value the survivor now holds. While the merge retains its data, a second column shows what the absorbed record held, so the decision can be re-read rather than merely trusted. Once purged, that column states that the value is gone and why. |
What moved |
One row per table, with how many records moved — participations, memberships, orders, numbers, tags, results and the rest. The screen reads whichever tables the merge recorded rather than a fixed list: the tracked set has grown as tables were added, and production holds four different sets. The count is all that was recorded; which rows moved was not, so the screen does not imply it can say. A table recorded as moving nothing is a different answer from a merge that stored no counts at all, and the two read differently. |
Retention |
Whether the absorbed record’s details are still held, the date they are due to be purged, and the action to purge them now. |
An overridden assessment
The log records eligible and confirmed separately, and the difference is the whole point:
-
eligiblefalse,confirmedtrue — the pair did not assess as the same person and someone merged them anyway. This is the row that must be findable and legible. The detail names the disagreeing evidence where the merge recorded it — see Most merges recorded no assessment, and the one override recorded no evidence, because the one override on record did not. -
eligibletrue — no confirmation was ever asked for, so the flag says nothing about the operator.
Therefore the screen shows an override marker only in the first case. Rendering "confirmed" wherever the flag is true would attribute a deliberation to operators who were never asked for one, which is worse than showing nothing: it would make the marker meaningless on the rows where it matters.
Most merges recorded no assessment, and the one override recorded no evidence
The assessment was added to the log after most merges had already run. Of 2,459 merges in production, 2,430 carry none at all. The detail therefore says so, and says what the silence does not mean: nothing was recorded either way, which is not the same as a pair having been judged different.
Of the 29 that do carry one, exactly one is a genuine override — assessed as not the same person
and merged anyway. It stored {"eligible": false, "confirmed": true} and nothing else: neither the
agreeing nor the disagreeing details. So for the single row this screen most exists to explain, the
evidence behind the decision cannot be re-read.
The screen still marks it, because the marking is what makes it findable, and states that the evidence was not recorded. It must not report that no details disagreed — that would be a finding, and a false one.
The writer emits both lists today, so a future override will carry its evidence. Nothing recovers what earlier ones did not store. As with the cleared name, the screen has to render both kinds without looking broken doing either.
Why there is no trigger filter
Every merge in production carries the same trigger. A filter whose every option but one returns nothing teaches an operator that the control is broken.
The trigger stays visible as a badge on every row, so the first merge that is not operator-performed announces itself without a filter. The filter arrives with the automated sweep, which is the only thing that can ever write a second value.
Main Flow
-
Operator opens
PLATFORM ▸ Merge history. The most recent merges are listed. -
Operator narrows by date, person, or name.
-
Operator expands a merge.
-
The detail shows the assessment, what each field became, and what moved.
-
Operator either leaves it, or purges the retained data.
Alternative Flows
-
AF-1 — arriving from a resolved pair: the duplicate-people queue links a merged pair to the merge it produced, which opens expanded.
-
AF-2 — looking for one person: the person filter matches a merge where that person was on either side. Someone asking "what happened to this person" does not know which side they were on, and a record may have been kept once and absorbed later.
-
AF-3 — an erasure request: the operator purges every retained merge for the named person, in one confirmed action, rather than finding each merge by hand.
-
AF-4 — the retained data is already gone: a merge purged by the sweep, or performed before retention existed, shows provenance without absorbed values and says which of the two it is. The purge action is absent, not disabled — there is nothing to purge.
-
AF-5 — purge attempted twice: reported as already purged and treated as a success, not an error. A retry after a lost response must not read as a failure.
Acceptance Criteria
-
Use-case page authored.
-
The list shows merges newest first, with the kept and absorbed records distinguished by label, not by column order.
-
Where the absorbed record’s name was cleared by the merge, the list says so and shows the identifier, rather than rendering an empty cell.
-
The search states that only the surviving name is searchable for merges whose retained data is gone.
-
The list filters by date range, by person matching either side, and by retention state, and searches both people’s names.
-
Paging and sorting work through the established list contract, with the total available to the client.
-
A merge that was performed against a negative assessment is visibly marked in the list and explained in the detail.
-
Where an override recorded no disagreeing details, the detail says so rather than reporting that none disagreed.
-
A merge with no assessment on record says so, and says that this means nothing in either direction.
-
A table recorded as moving no records reads differently from a merge that stored no counts at all.
-
A failed load is distinguished from an empty result, and never reads as "no merges".
-
A merge that assessed as eligible carries no confirmation marker.
-
The detail states, per field, which record contributed the value the survivor holds.
-
While a merge retains its data, the detail also shows what the absorbed record held.
-
Once purged, the detail says the absorbed values are gone and whether the sweep or an operator removed them.
-
The detail reports how many records moved per table, and does not imply it knows which.
-
The retention state and purge-due date are visible on every row.
-
Every row names the operator who performed the merge, and says so plainly where none was captured rather than leaving the cell blank.
-
A purge confirmation names the person and states that the data cannot be recovered. It does not ask for a reason.
-
An operator can purge one merge’s retained data ahead of its due date, after a confirmation that names the person and states that it cannot be undone.
-
An operator can purge every retained merge for one person in a single confirmed action.
-
Purging something already purged reports success.
-
No control on this screen offers to reverse a merge.
-
The
ALL TENANTSmarker is present. -
The screen is unreachable, and its calls refused, without the platform grant.
API Surface
The list endpoint today supports neither filtering nor sorting; it returns every merge newest-first and overrides the caller’s sort. The filters above depend on the criteria surface being added.
| Call | Purpose |
|---|---|
|
The list, filtered by criteria. Returns a plain array with paging in the response headers, per the project’s list contract. Each entry already carries the field provenance, the table counts and the assessment, so expanding a row needs no second call. |
|
The total for the current filters. |
|
One merge. Used when arriving by link rather than from the list. |
|
Purges one merge’s retained data ahead of its due date. |
|
Purges every retained merge for a named person. The erasure-request path. |
(session) |
Whether the caller is a platform administrator, which gates the route. |
Filters the criteria class must carry: merge date as a range, person matching either side, both directional person filters, trigger, operator, retention state, and a free-text search across both names. The person-matching-either-side filter is a disjunction, so it needs a written specification rather than a generated one.
Out of Scope
-
Reversing a merge. Not offered, and not merely unbuilt: the table counts record how many rows moved, never which, so nothing identifies what to send back. A reversal is done from database backups.
-
Showing which individual records moved.
-
A tenant-scoped view of merges touching one organisation’s people.
-
Entry from a person’s own record, which belongs with that screen.
-
Anything that changes a merge. This screen reads, and purges.
Design Decisions
-
Kept and absorbed, never source and target. The stored names describe the inputs; on screen they would read as the outcome, and the wrong way round.
-
A cleared name is stated, not left blank. The merge clears the absorbed record’s name, so for almost every merge on record there is none. An empty cell beside a populated one reads as a fault; naming the absence and its cause reads as what happened.
-
The override marker is the screen’s reason to exist. It appears only where a negative assessment was overridden, because that is the only case where the confirmation flag means anything.
-
The trigger badge stays, the trigger filter waits. A filter with one populated option reads as a fault. The badge makes a second value visible the day one appears.
-
Retention is a column, not a footnote. It decides whether a merge can still be examined, and it is the one state on this screen with more than one value from the start.
-
The detail expands in place. Reading several merges in sequence is the actual working pattern.
-
Counts are reported as counts. The screen never implies a row-level record it does not have.
-
An absence is named, and so is its cause. The log grew a column at a time, so a merge from 2024 records far less than one performed today. Every gap the screen renders — the cleared name, the missing assessment, the unrecorded operator, the override with no evidence — is stated along with why, because a blank cell beside a populated one reads as a rendering fault rather than as the truth about what was kept.
-
Purge is confirmable and destructive; unmerge is absent. Offering an action that cannot work would be worse than the absence of one.
-
The operator column stays, although it barely distinguishes anything today. Two values exist across every merge on record —
APIfor all but two, andclaude/manual-unwindfor those — because one person has performed all of them by two routes. More operators arrive in 2027, and a column that is uninformative now becomes load-bearing then — whereas a column added later would leave the screen unable to answer the accountability question for everything merged before it. -
A purge needs no stated reason. The confirmation names the person and says the data cannot be recovered, which is enough for an operator acting on their own erasure request. A mandatory reason field would be ceremony rather than control while one person holds the grant. Worth revisiting alongside the 2027 operators, not before.
Design Anchors
-
S02 — Duplicate People — where merges are decided; same actor, same grant, same platform framing
-
ADR-0012 — why the override marker matters
-
Merge Person — the service this screen reports on
Notes
Retention is specified but not yet built; the screen’s retention column, the purge actions and the absorbed-value column all depend on it. Until it lands, every merge is in the purged state, which the design already has to render — so the screen can be built before retention exists and gains its second state when retention arrives.
Both questions raised in the design session are settled, and recorded under Design Decisions: the operator column stays, and a purge needs no stated reason.