|
| 1 | +--- |
| 2 | +title: "Merging organizations or people — when two objects turn out to be the same entity, dedupe non-destructively" |
| 3 | +lede: "Sooner or later two rows ARE the same org (or person) — different imports, different spellings, one with a domain and one without. There's no merge anywhere: nothing re-points edges, unions the lists, or absorbs the loser's slug into aliases. Non-destructive but effective dedupe probably deserves its own microfrontend." |
| 4 | +date_created: 2026-07-24 |
| 5 | +date_modified: 2026-07-24 |
| 6 | +authors: |
| 7 | + - Michael Staton |
| 8 | +augmented_with: |
| 9 | + - Claude Code on Claude Fable 5 |
| 10 | +semantic_version: 0.0.0.1 |
| 11 | +tags: |
| 12 | + - Issue |
| 13 | + - Augment-It |
| 14 | + - Organizations |
| 15 | + - Persons |
| 16 | + - Dedupe |
| 17 | + - Merge |
| 18 | + - Canonical-Layer |
| 19 | +status: Open · Stub |
| 20 | +--- |
| 21 | + |
| 22 | +# Merge organizations or people — a stub |
| 23 | + |
| 24 | +## The need |
| 25 | + |
| 26 | +Duplicates are inevitable at capture time — the candidate gates reduce them |
| 27 | +but can't eliminate them (operator picks "create" when the match was real, |
| 28 | +two imports spell one org two ways, a person exists once with an email and |
| 29 | +once with a LinkedIn URL). Today discovering a duplicate is a dead end: |
| 30 | +nothing merges. The two rows drift apart, each accumulating links, corpus, |
| 31 | +streams, affiliations, and observations the other doesn't have. |
| 32 | + |
| 33 | +## Shape of the thing (jotted, not designed) |
| 34 | + |
| 35 | +- **Non-destructive.** The loser is absorbed, not deleted: slug → the |
| 36 | + survivor's `aliases[]` (the `updateOrg` rename precedent), arrays union'd |
| 37 | + (URL-dedup'd, the additive discipline's existing keys), observations |
| 38 | + preserved with their provenance, `client_access` union'd. A tombstone or |
| 39 | + redirect pointer so anything stamped with the old slug/uuid still resolves. |
| 40 | +- **Effective.** Affiliation edges re-pointed to the survivor (dedup'd |
| 41 | + against existing edges), opportunities re-stamped (the `updateOrg` fan-out |
| 42 | + precedent) — after the merge there is ONE row to find. |
| 43 | +- **Human-driven.** Per [[human-in-drivers-seat]]: the operator picks the |
| 44 | + survivor and resolves field conflicts (which `complete_name` wins?); |
| 45 | + side-by-side compare, not an auto-merge score. |
| 46 | +- **Perhaps its own microfrontend.** A compare-and-merge surface (two cards, |
| 47 | + field-by-field pick, one confirm) doesn't fit inside the org card; a |
| 48 | + dedicated remote fits the one-surface-per-job pattern the workbench |
| 49 | + already follows. |
| 50 | + |
| 51 | +## Deliberate contrast to keep |
| 52 | + |
| 53 | +Opportunities are **never auto-merged** by design (duplicates across record |
| 54 | +sets are intentional — see the Grilling-on-DB-Resolver decisions). This merge |
| 55 | +is for the CANONICAL layer only: organizations and persons, where one |
| 56 | +real-world entity should be one row. The opportunity rule stands. |
| 57 | + |
| 58 | +## Open questions |
| 59 | + |
| 60 | +- [ ] One merge verb (`entity.merge`?) or per-table |
| 61 | + (`organization.merge` / `person.merge`)? |
| 62 | +- [ ] Duplicate DISCOVERY — does the merge surface also hunt candidates |
| 63 | + (same domain, near-name), or only act on pairs the operator brings? |
| 64 | +- [ ] What does the tombstone look like — a row with `merged_into`, or an |
| 65 | + alias entry alone? |
| 66 | +- [ ] Observations: re-point subject/object to the survivor, or leave them |
| 67 | + citing the absorbed row and resolve through the tombstone? |
0 commit comments