---
id: STATE-CRM-DQ-001
title: Possible duplicate review standing
object: DOM-CRM-DQ-001
product_area: crm
module: data-quality
revision: 1
status: draft
design_intent: proposed
implementation_evidence: observed
source_sha:
  services-crm: a8d1364f1dd44b723bb5f9acba12f983e9ec8d69
states:
  - name: absent
    meaning: The workspace has no stored comparison for this pair.
  - name: open
    meaning: The pair awaits duplicate review.
  - name: dismissed
    meaning: >-
      A reviewer has rejected the match, with author, timing, note and dismissal
      evidence retained.
  - name: confirmed
    meaning: >-
      A reviewer has confirmed the identity match; the pair is excluded from
      grouping.
transitions:
  - from: absent
    to: open
    trigger: Qualifying detection or declined portal registration creates a pair.
    guard: >-
      The records share an eligible key, or registration supplies a declined
      candidate.
    side_effects:
      - persist scored evidence and its fingerprint
      - refresh grouping with the pair write
    who: Duplicate detection; Platform portal sign-up
    provenance: test
    source: >-
      services-crm:modules/duplicates/tests/integration/services/detection/duplicate-detection.integration.spec.js#found
      the two records of the batch in each other, which no lookup outside the
      batch could have done
  - from: open
    to: open
    trigger: Another detection pass finds the pair again.
    guard: Shared eligible evidence still reaches the storage floor.
    side_effects:
      - replace scoring fields and evaluation time
      - 'preserve identity, origin and initial detection'
    who: Duplicate detection
    provenance: test
    source: >-
      services-crm:modules/duplicates/tests/integration/services/persistence/duplicate-candidates.integration.spec.js#keeps
      the moment the pair was first detected, which no re-score owns
  - from: open
    to: absent
    trigger: Detection no longer finds a qualifying comparison.
    guard: Only an open pair originating from detection can be retired.
    side_effects:
      - remove the pair
      - refresh affected groups and remove undersized components
    who: Duplicate detection
    provenance: test
    source: >-
      services-crm:modules/duplicates/tests/integration/services/detection/duplicate-detection.integration.spec.js#retired
      the pair the number alone joined, its only evidence being disqualified
  - from: open
    to: dismissed
    trigger: 'A reviewer rejects a pair, group or one member’s comparisons.'
    guard: Review authority and a valid optional note are required.
    side_effects:
      - persist the author and dismissal evidence
      - refresh dismissed grouping
    who: Staff reviewer
    provenance: test
    source: >-
      services-crm:modules/duplicates/tests/e2e/duplicates.e2e.spec.js#records
      the evidence the dismissal was made against, which a later score is
      compared with
  - from: dismissed
    to: dismissed
    trigger: The same rejection is submitted again.
    guard: A dismissal is already stored.
    side_effects:
      - retain the existing decision
      - return its attribution and already-dismissed indication
    who: Staff reviewer
    provenance: test
    source: >-
      services-crm:modules/duplicates/tests/integration/services/persistence/duplicate-candidates.integration.spec.js#answers
      the second reviewer the EARLIER dismissal: its who, its name as visibility
      permits, its when and its note
  - from: dismissed
    to: open
    trigger: Restoration or newly scored strong evidence reopens review.
    guard: >-
      Restoration needs review authority and the relevant decision; automatic
      reopening needs new strong evidence.
    side_effects:
      - remove dismissal fields
      - retain the current scoring result
      - refresh open grouping
    who: Staff reviewer; duplicate detection
    provenance: test
    source: >-
      services-crm:modules/duplicates/tests/unit/services/matching/fingerprint.spec.js#leaves
      a pair dismissed when a name typo is fixed and nothing strong changes
  - from: open
    to: confirmed
    trigger: A reviewer confirms an existing open comparison.
    guard: >-
      Review authority and two different existing records of one type are
      required.
    side_effects:
      - assign full score and the human explanation
      - exclude the confirmed pair from groups
    who: Staff reviewer
    provenance: test
    source: >-
      services-crm:modules/duplicates/tests/integration/services/persistence/duplicate-candidates.integration.spec.js#takes
      the dismissal off, the person having just overruled it
  - from: dismissed
    to: confirmed
    trigger: A reviewer confirms a previously rejected comparison.
    guard: Review authority is required.
    side_effects:
      - clear the former dismissal
      - record the human confirmation
    who: Staff reviewer
    provenance: test
    source: >-
      services-crm:modules/duplicates/tests/integration/services/persistence/duplicate-candidates.integration.spec.js#takes
      the dismissal off, the person having just overruled it
  - from: absent
    to: confirmed
    trigger: A reviewer identifies an unstored duplicate pair.
    guard: Review authority is required.
    side_effects:
      - create a manual full-score confirmation without grouping
    who: Staff reviewer
    provenance: test
    source: >-
      services-crm:modules/duplicates/tests/integration/services/persistence/duplicate-candidates.integration.spec.js#is
      confirmed, manual and at full score: certainty, not a computation
  - from: confirmed
    to: dismissed
    trigger: A reviewer rejects a confirmed pair.
    guard: Confirmation does not prevent dismissal by a reviewer.
    side_effects:
      - record the rejection
      - refresh dismissed grouping
    who: Staff reviewer
    provenance: observed
    source: >-
      services-crm:modules/duplicates/services/persistence/duplicate-candidates.ts:dismissDuplicateCandidate
  - from: open
    to: absent
    trigger: Record deletion or merging removes an open pair’s participant.
    guard: Record foreign keys cascade the pair.
    side_effects:
      - remove the pair and record keys
      - carry applicable outside dismissals before merge deletion
      - refresh surviving groups
    who: Entities deletion; a merge
    provenance: test
    source: >-
      services-crm:modules/merges/tests/integration/services/merges/duplicate-groups-after-merge.integration.spec.js#leaves
      the group holding the two records that are left, counted as it now stands
  - from: dismissed
    to: absent
    trigger: Record deletion or merging removes a dismissed pair’s participant.
    guard: Merging first carries eligible outside decisions to the survivor.
    side_effects:
      - carry outside decisions or end internal ones
      - remove the absorbed pair
    who: Entities deletion; a merge
    provenance: test
    source: >-
      services-crm:modules/duplicates/tests/integration/services/persistence/duplicate-candidates.integration.spec.js#carries
      the dismissals about records outside the merge and nothing else
  - from: confirmed
    to: absent
    trigger: Deletion or successful merging removes a confirmed participant.
    guard: Record deletion cascades the comparison.
    side_effects:
      - remove the pair with its participant
    who: A merge; Entities deletion
    provenance: test
    source: >-
      services-crm:modules/merges/tests/integration/services/merges/duplicate-groups-after-merge.integration.spec.js#leaves
      the group holding the two records that are left, counted as it now stands
invalid:
  - from: dismissed
    to: absent
    why: >-
      Detection retains dismissed rows; participant removal can still remove
      them.
  - from: confirmed
    to: open
    why: >-
      Detection preserves confirmed standing but can replace qualifying scores
      and reasons.
  - from: open
    to: open
    why: 'Rescoring cannot replace origin, stable identity or first detection.'
concurrency: >-
  Unique ordered pairs absorb duplicate inserts. Review writes lock their rows,
  while detection reads decisions before opening its write transaction; the
  implementation does not recheck dismissal fingerprints at commit. The latest
  review decision is therefore not a proved concurrency guarantee.
invariants:
  - INV-DOM-CRM-DQ-001-03
  - INV-DOM-CRM-DQ-001-04
  - INV-DOM-CRM-DQ-001-05
  - INV-DOM-CRM-DQ-001-06
  - INV-DOM-CRM-DQ-001-07
evidence:
  - >-
    services-crm at a8d1364f:
    modules/duplicates/db/duplicate-candidates.table.ts;
    services/persistence/duplicate-candidates.ts (dismiss, restore, add manual,
    remove); services/detection/duplicate-detection.ts;
    services/candidates/dismissal-carry.ts.
---

# Possible Duplicate Review Standing

## The Lifecycle

```mermaid
stateDiagram-v2
  state "absent" as absent
  state "open" as open
  state "dismissed" as dismissed
  state "confirmed" as confirmed
  absent --> open: Qualifying detection or declined portal registration creates a pair
  open --> open: Another detection pass finds the pair again
  open --> absent: Detection no longer finds a qualifying comparison
  open --> dismissed: A reviewer rejects a pair, group or one member’s comparisons
  dismissed --> dismissed: The same rejection is submitted again
  dismissed --> open: Restoration or newly scored strong evidence reopens review
  open --> confirmed: A reviewer confirms an existing open comparison
  dismissed --> confirmed: A reviewer confirms a previously rejected comparison
  absent --> confirmed: A reviewer identifies an unstored duplicate pair
  confirmed --> dismissed: A reviewer rejects a confirmed pair
  open --> absent: Record deletion or merging removes an open pair’s participant
  dismissed --> absent: Record deletion or merging removes a dismissed pair’s participant
  confirmed --> absent: Deletion or successful merging removes a confirmed participant
```

## Transitions Explained

Detection can replace evidence on any qualifying pair while keeping its stored standing. New strong evidence can separately reopen dismissal. Human confirmation initially assigns full score and removes grouping, but does not protect the score from later detection. Merging carries outside dismissals before absorbed comparisons cascade away.

## What Cannot Happen

Automatic retirement affects only open detected pairs. Review decisions do not expire merely because evidence weakens, and a pair needs both participating records.

## Concurrency and Timing

Background delivery can trail a record save. Reviewer writes and regrouping commit together, but simultaneous review and detection need the concurrency qualification recorded in the structured model.
