---
id: STATE-CRM-DQ-002
title: Duplicate group
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: none
    meaning: No stored component represents these records at this standing.
  - name: open group
    meaning: Open comparisons connect a bounded review component.
  - name: dismissed group
    meaning: Dismissed comparisons connect a stored rejected component.
transitions:
  - from: none
    to: open group
    trigger: At least two records become connected by open comparisons.
    guard: The component must contain two or more records.
    side_effects:
      - 'store counts, strength and timing'
      - attach the grouped pairs
      - invoke the held attention seam
    who: Group recomputation inside the pair change
    provenance: test
    source: >-
      services-crm:modules/duplicates/tests/integration/services/detection/duplicate-detection.integration.spec.js#grouped
      every pair it stored, the groups of a batch being written in the pass’s
      own transaction (§14)
  - from: open group
    to: open group
    trigger: 'Comparison changes resize, join or rescore a component.'
    guard: At least two connected records remain.
    side_effects:
      - choose the prior identity with the largest contribution
      - recompute counts
      - flag traversal truncated by the ten-record limit
    who: Group recomputation
    provenance: test
    source: >-
      services-crm:modules/duplicates/tests/unit/services/groups/group-components.spec.js#keeps
      the id for the part holding MORE of the group’s records, the other part
      being a new group
  - from: open group
    to: dismissed group
    trigger: A reviewer dismisses all open comparisons in the selected group.
    guard: Review authority and at least one undecided pair are required.
    side_effects:
      - return the resulting dismissed component and its separate identity
    who: Staff reviewer
    provenance: test
    source: >-
      services-crm:modules/duplicates/tests/integration/services/persistence/duplicate-candidates.integration.spec.js#leaves
      no pair of the group dismissed, a group being decided about whole or not
      at all
  - from: dismissed group
    to: open group
    trigger: A reviewer undoes the selected group rejection.
    guard: >-
      Only pairs attributed to that decision reopen; an empty restoration is
      refused.
    side_effects:
      - return the resulting open component
    who: Staff reviewer
    provenance: test
    source: >-
      services-crm:modules/duplicates/tests/integration/services/persistence/duplicate-candidates.integration.spec.js#leaves
      the pair somebody dismissed on its own dismissed, that being a decision of
      its own
  - from: open group
    to: none
    trigger: Pair changes or record removal leave no component of two records.
    guard: No additional permission check occurs during regrouping.
    side_effects:
      - delete the empty or undersized group
      - invoke the held attention resolution
    who: >-
      Group recomputation in the detection, decision, deletion or merge
      transaction
    provenance: observed
    source: 'services-crm:modules/duplicates/services/groups/group-components.ts'
  - from: dismissed group
    to: none
    trigger: The dismissed component loses its final connected pair.
    guard: No additional guard applies during regrouping.
    side_effects:
      - remove the dismissed group
    who: Group recomputation
    provenance: observed
    source: 'services-crm:modules/duplicates/services/groups/group-components.ts'
invalid:
  - from: none
    to: open group
    why: Confirmed comparisons are excluded from both component types.
  - from: open group
    to: open group
    why: >-
      Traversal refuses to combine components beyond ten records and marks the
      boundary.
concurrency: >-
  Regrouping runs in the pair-changing transaction over a bounded neighborhood.
  Existing identities are assigned deterministically by contribution; new
  identities are allocated when no prior component can be retained. This is not
  a proof that independent concurrent regrouping always selects the same new
  identity.
invariants:
  - INV-DOM-CRM-DQ-001-07
  - INV-DOM-CRM-DQ-001-08
evidence:
  - >-
    services-crm at a8d1364f: modules/duplicates/db/duplicate-groups.table.ts;
    services/groups/group-components.ts;
    services/persistence/duplicate-groups.ts:recomputeDuplicateGroups;
    modules/entities/services/persistence/contacts.ts and organizations.ts
    (recompute on delete).
---

# Duplicate Group

## The Lifecycle

```mermaid
stateDiagram-v2
  state "none" as none
  state "open group" as open_group
  state "dismissed group" as dismissed_group
  none --> open_group: At least two records become connected by open comparisons
  open_group --> open_group: Comparison changes resize, join or rescore a component
  open_group --> dismissed_group: A reviewer dismisses all open comparisons in the selected group
  dismissed_group --> open_group: A reviewer undoes the selected group rejection
  open_group --> none: Pair changes or record removal leave no component of two records
  dismissed_group --> none: The dismissed component loses its final connected pair
```

## Transitions Explained

Groups are derived working components with persisted counts. Split and join rules retain one prior identity by contribution; other pieces receive new identities. Group dismissal returns a component of another standing, and undo applies only to pairs recorded by that dismissal.

## What Cannot Happen

Singletons are removed and confirmed pairs are ungrouped. A bounded component can omit links to a larger connected set.

## Concurrency and Timing

Counts and membership are written with pair changes. Attention hooks remain disabled pending metadata registration, and deterministic reuse of existing identities should not be mistaken for concurrency control over new components.
