---
id: CAP-CRM-DQ-015
title: Keep related records consistent through a merge
feature: FEAT-CRM-DQ-03
product_area: crm
module: data-quality
revision: 1
status: draft
product_intent: confirmed
implementation_evidence: partially-observed
supersedes:
  - CAP-CRM-MRG-018
  - CAP-CRM-MRG-019
source_sha:
  services-crm: a8d1364f1dd44b723bb5f9acba12f983e9ec8d69
  react-console: baf6dae382d6f41dce976c7f848667ccc37e076f
intent_sources:
  - 'services-crm:docs/merging/decisions.md#D1 (decided 2026-10-11: option 1)'
  - 'services-crm:docs/merging/decisions.md#D2 (decided 2026-10-11: option 1)'
  - 'services-crm:docs/milestones/CRM-M4-duplicates-and-merge.md#CRM-Q3'
  - >-
    services-crm:docs/merging/experience/functional-requirements.md#REL-1 to REL-6, DIS-8 (package approved by
    Andrew 2026-10-13)
purpose: >-
  A merge accounts for references to its participants so related work remains attached or deliberately
  retained as history.
outcomes:
  - Declared policies prevent unintended loss of related references.
  - Original historical references remain readable through the survivor.
  - Unsupported application participation prevents execution instead of stranding its references.
concepts:
  - CON-CRM-DQ-003
actors:
  - ACT-CRM-009
  - ACT-CRM-005
surfaces:
  - console
  - api
  - job
permissions:
  - key: crm.contacts.merge
    where_checked: api
    scope: tenant
  - key: crm.organizations.merge
    where_checked: api
    scope: tenant
rules:
  - id: BR-CAP-CRM-DQ-015-01
    proved_by: [ATS-CRM-DQ-015-01, ATS-CRM-DQ-015-15, TST-CRM-DQ-184]
    statement: >-
      Each referencing column, including references to owned children, requires one policy covering transfer,
      survivor reassignment, collision combination, domain combination, retained history, clearing, human
      decision, blocking or non-reference classification; coverage checks reject undeclared additions.
    kind: invariant
    enforced_at:
      - service
      - database
    must_prove: true
    provenance: test
    source: >-
      services-crm:tests/integration/db/merge-coverage.integration.spec.ts#declares a policy for every column
      the catalog holds
    origin: 'services-crm:docs/milestones/CRM-M4-duplicates-and-merge.md#CRM-Q3'
  - id: BR-CAP-CRM-DQ-015-02
    proved_by: [ATS-CRM-DQ-015-02, ATS-CRM-DQ-015-15]
    statement: >-
      Undeclared references block both planning and execution with their row counts, allow no override and
      cease blocking when a policy covers them.
    kind: validation
    enforced_at:
      - service
    must_prove: true
    provenance: test
    source: >-
      services-crm:modules/merges/tests/unit/services/planning/merge-decisions.spec.js#blocks the merge for a
      column the registry declares no policy for
    origin: 'services-crm:docs/milestones/CRM-M4-duplicates-and-merge.md#CRM-Q3'
  - id: BR-CAP-CRM-DQ-015-03
    proved_by: [ATS-CRM-DQ-015-03, ATS-CRM-DQ-015-09, ATS-CRM-DQ-015-15, TST-CRM-DQ-176, TST-CRM-DQ-181]
    statement: >-
      Tasks, activity links, spouse links, custom references, child organizations and integration mappings
      follow the survivor; duplicate activity links collapse and retain primary status if either link held it.
    kind: lifecycle
    enforced_at:
      - service
    must_prove: true
    provenance: test
    source: >-
      services-crm:modules/merges/tests/integration/services/merges/activity-links-merge.integration.spec.js#collapses
      a link the survivor already had into the survivor’s own, carrying the primary flag to it
    origin: 'services-crm:docs/merging/README.md#Status (architecture and plan approved by Andrew 2026-10-11)'
  - id: BR-CAP-CRM-DQ-015-04
    proved_by: [ATS-CRM-DQ-015-04, ATS-CRM-DQ-015-15, TST-CRM-DQ-176]
    statement: >-
      Audit entries, change history, import receipts and copied-from provenance retain original identities
      rather than being rewritten and remain readable through the survivor.
    kind: policy
    enforced_at:
      - service
    must_prove: true
    provenance: test
    source: 'react-console:acceptance/crm/playwright/journeys/merging/CRM-ACC-0272.spec.ts#CRM-ACC-0272'
    origin: 'services-crm:docs/merging/README.md#Status (architecture and plan approved by Andrew 2026-10-11)'
  - id: BR-CAP-CRM-DQ-015-05
    proved_by: [ATS-CRM-DQ-015-05, ATS-CRM-DQ-015-15, TST-CRM-DQ-181]
    statement: >-
      Other applications supply owned planning and execution functions in the merge transaction; a required
      missing or inaccessible participant blocks planning and a refusing participant rolls back execution.
    kind: policy
    enforced_at:
      - service
      - database
    must_prove: true
    provenance: test
    source: >-
      services-crm:modules/merges/tests/integration/services/merges/required-participants.integration.spec.js#blocks
      it as a participant that is missing, naming the functions it could not call
    origin: 'services-crm:docs/merging/decisions.md#D1 (decided 2026-10-11: option 1)'
  - id: BR-CAP-CRM-DQ-015-06
    proved_by: [ATS-CRM-DQ-015-06, ATS-CRM-DQ-015-15]
    statement: >-
      A sole contact portal login is previewed, moved to the survivor and security-audited; multiple logins or
      record-specific grants block merging pending security administration.
    kind: policy
    enforced_at:
      - service
      - database
    must_prove: true
    provenance: test
    source: >-
      services-crm:modules/merges/tests/unit/services/planning/merge-decisions.spec.js#carries the relink of
      the one login a participant holds
    origin: 'services-crm:docs/merging/decisions.md#D2 (decided 2026-10-11: option 1)'
  - id: BR-CAP-CRM-DQ-015-07
    proved_by: [ATS-CRM-DQ-015-07, ATS-CRM-DQ-015-09, ATS-CRM-DQ-015-11, ATS-CRM-DQ-015-15, TST-CRM-DQ-176, TST-CRM-DQ-180]
    statement: >-
      Outside-pair dismissals move from absorbed records with attribution and evidence; existing survivor
      decisions prevail, otherwise the latest carried decision wins, and internal dismissals end with the
      merge.
    kind: lifecycle
    enforced_at:
      - service
    must_prove: true
    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
    origin: 'services-crm:docs/merging/experience/functional-requirements.md#DIS-8'
  - id: BR-CAP-CRM-DQ-015-08
    proved_by: [ATS-CRM-DQ-015-08, ATS-CRM-DQ-015-10, ATS-CRM-DQ-015-12, ATS-CRM-DQ-015-15, TST-CRM-DQ-181]
    statement: >-
      Absorbed records lose their match keys and open or confirmed pairs, and later survivor detection
      recomputes its keys, scores and group among remaining records.
    kind: lifecycle
    enforced_at:
      - service
      - database
    must_prove: true
    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
    origin: none
  - id: BR-CAP-CRM-DQ-015-09
    proved_by: []
    statement: >-
      Related-record actions carry business labels, row counts and readable summaries, including explicit
      no-op actions and participant breakdowns where CRM computes them.
    kind: policy
    enforced_at:
      - service
    must_prove: false
    provenance: test
    source: >-
      services-crm:modules/merges/tests/integration/services/merges/plain-language.integration.spec.js#is
      marked a no-op, so a screen may leave it out
    origin: 'services-crm:docs/merging/experience/functional-requirements.md#REL-1'
  - id: BR-CAP-CRM-DQ-015-10
    proved_by: []
    statement: >-
      Carried dismissals are excluded from approval binding, so an intervening dismissal alone does not stale
      a preview.
    kind: policy
    enforced_at:
      - service
    must_prove: false
    provenance: documented
    source: 'services-crm:docs/merging/architecture.md#6.1 Policy vocabulary'
    origin: none
  - id: BR-CAP-CRM-DQ-015-11
    proved_by: []
    statement: >-
      Imports and updates do not merge look-alike records automatically; creation leaves separate records for
      detection to suggest.
    kind: policy
    enforced_at:
      - service
    must_prove: false
    provenance: documented
    source: 'services-crm:docs/merging/architecture.md#10. Data Operations boundary'
    origin: 'services-crm:docs/merging/README.md#Status (architecture and plan approved by Andrew 2026-10-11)'
scenarios:
  - id: SC-CAP-CRM-DQ-015-01
    given: An absorbed contact has tasks and shares an activity with the survivor.
    when: Execution combines the contacts.
    then: Tasks follow the survivor and one activity link retains primary status from the absorbed link.
    provenance: test
    source: 'react-console:acceptance/crm/playwright/journeys/merging/CRM-ACC-0258.spec.ts#CRM-ACC-0258'
  - id: SC-CAP-CRM-DQ-015-02
    given: An absorbed contact has a dismissal concerning someone outside the merge.
    when: Execution commits the reviewed combination.
    then: The survivor inherits that decision against the outsider.
    provenance: test
    source: 'react-console:acceptance/crm/playwright/journeys/merging/CRM-ACC-0237.spec.ts#CRM-ACC-0237'
edge_cases:
  - id: SC-CAP-CRM-DQ-015-03
    given: A test participant refuses its execution.
    when: The merge attempts the participant change.
    then: Execution records the refusal and both contacts remain unchanged.
    provenance: test
    source: >-
      services-crm:modules/merges/tests/integration/services/merges/toy-participant.integration.spec.js#leaves
      both contacts byte for byte as they were
constraints: []
depends_on:
  - CAP-CRM-DQ-012
related:
  - CAP-CRM-DQ-008
  - CAP-CRM-DQ-016
journeys:
  - JNY-CRM-DQ-002
open_questions:
  - OQ-CRM-DQ-003
evidence:
  - repository: services-crm
    source_sha: a8d1364f1dd44b723bb5f9acba12f983e9ec8d69
    paths:
      - docs/merging/README.md
      - docs/merging/architecture.md
      - docs/merging/decisions.md
      - docs/merging/experience/functional-requirements.md
      - docs/milestones/CRM-M4-duplicates-and-merge.md
      - modules/duplicates/tests/integration/services/persistence/duplicate-candidates.integration.spec.js
      - modules/merges/references/crm-references.ts
      - modules/merges/services/persistence/merge-participants.ts
      - modules/merges/tests/integration/services/merges/activity-links-merge.integration.spec.js
      - modules/merges/tests/integration/services/merges/duplicate-groups-after-merge.integration.spec.js
      - modules/merges/tests/integration/services/merges/plain-language.integration.spec.js
      - modules/merges/tests/integration/services/merges/required-participants.integration.spec.js
      - modules/merges/tests/integration/services/merges/toy-participant.integration.spec.js
      - modules/merges/tests/unit/services/planning/merge-decisions.spec.js
      - tests/integration/db/merge-coverage.integration.spec.ts
    note: 'Static examination connects rule claims to implementation, declared checks and the cited tests.'
  - repository: react-console
    source_sha: baf6dae382d6f41dce976c7f848667ccc37e076f
    paths:
      - acceptance/crm/playwright/journeys/merging/CRM-ACC-0237.spec.ts
      - acceptance/crm/playwright/journeys/merging/CRM-ACC-0258.spec.ts
      - acceptance/crm/playwright/journeys/merging/CRM-ACC-0272.spec.ts
    note: 'Static examination connects rule claims to implementation, declared checks and the cited tests.'
preconditions: []
postconditions: []
controls: [TC-AZ-001, TC-AZ-002, TC-AZ-003, TC-AZ-004, TC-AZ-005, TC-AZ-006, TC-DAT-001, TC-DAT-002, TC-DAT-003, TC-DAT-004, TC-IAM-001, TC-IAM-002, TC-IAM-004, TC-IAM-005, TC-IAM-006, TC-LOG-004, TC-LOG-005, TC-MON-001, TC-MON-002, TC-PI-001, TC-PI-003, TC-PI-004, TC-PI-005, TC-PRV-001, TC-PRV-002, TC-PRV-003, TC-TEN-001, TC-TEN-002, TC-TEN-003]
exercised_by: [FAS-CRM-DQ-03-02]
suites: [TST-CRM-DQ-176, TST-CRM-DQ-180, TST-CRM-DQ-181, TST-CRM-DQ-184]
---

# Keep related records consistent through a merge

## Purpose

A merge accounts for references to its participants so related work remains attached or deliberately retained as history.

## How It Works

The planner inspects related records and applies declared policies. Some references move to the survivor; others combine on collision or remain under the original identity as history. A reference without a policy blocks the merge. The preview summarizes the kinds and quantities involved. Other applications describe and execute their changes through functions owned by those applications. Their execution participates in the same transaction as CRM. Required participants that are unavailable prevent the merge, and a refusal rolls back all changes. Portal login decisions arrive through Platform's participant. Applicable outside dismissals carry to the survivor, while pairs internal to the merge disappear. The survivor receives fresh detection after the committed change. Bulk operations do not silently merge look-alike records.

## Relationships

Contact and organization planners determine owned-record combinations. Platform supplies login handling, while each other application owns its participant. Execution provides the shared commit boundary.
