Skip to content

Index every co-owner of an email/phone in dedup detection - #456

Merged
tomcardoso merged 1 commit into
mainfrom
fix/issue-441-dedup-index
Jul 20, 2026
Merged

Index every co-owner of an email/phone in dedup detection#456
tomcardoso merged 1 commit into
mainfrom
fix/issue-441-dedup-index

Conversation

@tomcardoso

Copy link
Copy Markdown
Owner

Closes #441

findDuplicatePairs kept its email and phone indexes as Map<identifier, contactId> — one contact per identifier — and only registered a contact when no entry existed yet. So when an identifier hit an existing entry but the pair was skipped (previously dismissed, or one side already paired), the current contact was never indexed under its own ID.

With A, B, C sharing an email and (A,B) dismissed, B never enters the index. (B,C) is then only found if C happens to collide with A's entry — and if (A,C) is also dismissed, C's duplicate relationship to B is missed entirely.

Change

Both indexes become Map<string, Set<string>> (identifier → all contact IDs carrying it), matching the pattern the sync engine already uses for localEmailToId / localPhoneToId. For each identifier the code scans the full set for the first eligible partner, then adds the current contact's ID unconditionally — that last part is the actual fix.

Pair-recording semantics, reason values, email-before-phone ordering, and the name-similarity pass are all unchanged. One pre-existing edge case is deliberately preserved: a contact already in pairedIds still gets indexed, matching the old else if (!existing) path.

Testing

Four unit tests added. I verified the teeth independently rather than relying on a green suite — against unmodified dedup.ts:

  • email, (A,B) + (A,C) dismissed → (B,C) — FAILS without the fix ✓
  • phone, same shape — FAILS without the fix ✓
  • the (A,B)-only-dismissed case and the two-contact baseline pass either way; they're guards against over-correcting, not regressions

npm run typecheck clean; npm test 527 passed across 30 files.

findDuplicatePairs only registered a contact's email/phone under its own
ID when the index slot was empty. If an identifier already had an entry
but the pair was skipped (dismissed, or one side already paired), the
current contact's identifier was never indexed — so a later contact
sharing that identifier could miss a valid pairing entirely (issue #441).

Change emailIndex/phoneIndex from Map<string, string> to
Map<string, Set<string>>, matching the identity-index pattern already
used in sync/engine.ts. Every contact's identifier is now added to its
set unconditionally, whether or not a pair was recorded.

Closes #441

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@tomcardoso
tomcardoso merged commit ed524b4 into main Jul 20, 2026
1 check passed
@tomcardoso
tomcardoso deleted the fix/issue-441-dedup-index branch July 20, 2026 17:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Dedup detection: email/phone index skips later contacts when a match is dismissed or already paired

1 participant