ALL-1875 PR-3: GraphQL schema — duplicateCandidates query + dismissDuplicate mutation
queued## ALL-1875: Contact merge UI + auto duplicate detection — PR 3 of 5
### Goal
Expose DuplicateCandidate data to the frontend via GraphQL (query + dismiss mutation). Also add merge history fields to the CanonicalContact type.
### Context
- Linear issue: ALL-1875
- Branch: `wilbo/all-1875-graphql-duplicate-candidates`
- Worktree: `mono-all-1875-graphql-duplicate-candidates`
- Depends on: PR-1 merged (DuplicateCandidate repo exists)
### What to build in `domains/identity/src/graphql/`
**New GraphQL types (add to the SDL schema):**
```graphql
type DuplicateContactCandidate {
id: ID!
workspaceId: String!
contactA: CanonicalContact!
contactB: CanonicalContact!
confidenceScore: Float!
matchSignals: [String!]!
detectedAt: String!
dismissedAt: String
dismissedBy: String
}
type DuplicateContactCandidatesConnection {
nodes: [DuplicateContactCandidate!]!
totalCount: Int!
}
```
**New queries (add to `Query` type):**
```graphql
duplicateContactCandidates(
workspaceId: ID!
first: Int
after: String
orderBy: DuplicateCandidateOrderBy # confidence_desc | detected_at_desc
minConfidence: Float
): DuplicateContactCandidatesConnection!
@requireCapability(name: "contact:read")
```
**New mutations (add to `Mutation` type):**
```graphql
dismissContactDuplicate(id: ID!): DuplicateContactCandidate!
@requireCapability(name: "contact:merge_write")
```
**Also extend `CanonicalContact` type** with:
```graphql
mergeHistory: [CanonicalContactMergeEvent!]!
```
where `CanonicalContactMergeEvent` surfaces the CONTACTS_MERGED source events (winnerId, loserId, actorId, mergeEventId, createdAt) for the unmerge history section.
### Resolver files to create
- `resolvers/canonicalContact/queries/duplicateContactCandidates.ts`
- `resolvers/canonicalContact/mutations/dismissContactDuplicate.ts`
- `resolvers/canonicalContact/fields/mergeHistory.ts`
### Resolver registration
Wire into `domains/identity/src/graphql/resolvers/query/index.ts` and `mutation/index.ts`.
### Key existing files
- `domains/identity/src/graphql/resolvers/canonicalContact/mutations/mergeCanonicalContacts.ts` — mutation pattern
- `domains/identity/src/graphql/resolvers/canonicalContact/queries/canonicalContact.ts` — query pattern
- `domains/identity/src/graphql/resolvers/canonicalContact/fields/` — field resolver pattern
- `domains/identity/src/repositories/duplicateCandidate/index.ts` — use this repo (from PR-1)
### Acceptance test
- `duplicateContactCandidates(workspaceId: "...")` returns candidates for a workspace that has them
- `dismissContactDuplicate(id: "...")` sets dismissedAt and gates the item from future queries
- `canonicalContact { mergeHistory { ... } }` returns CONTACTS_MERGED events for a merged contact
- Permission check: caller without `contact:merge_write` capability gets a 403 on `dismissContactDuplicate`
### Depends on
- PR-1 merged (fleet-task dd69e15f-601c-468a-a128-9a9bd795e237)
### Out of scope
- Frontend UI (PR-4, PR-5)
Event Timeline
created