Index every co-owner of an email/phone in dedup detection - #456
Merged
Conversation
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #441
findDuplicatePairskept its email and phone indexes asMap<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 forlocalEmailToId/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,
reasonvalues, email-before-phone ordering, and the name-similarity pass are all unchanged. One pre-existing edge case is deliberately preserved: a contact already inpairedIdsstill gets indexed, matching the oldelse if (!existing)path.Testing
Four unit tests added. I verified the teeth independently rather than relying on a green suite — against unmodified
dedup.ts:npm run typecheckclean;npm test527 passed across 30 files.