---
id: DOM-CRM-DQ-003
title: Merge Plan
realises: CON-CRM-DQ-003
product_area: crm
module: data-quality
revision: 1
status: draft
design_intent: proposed
implementation_evidence: observed
source_sha:
  services-crm: a8d1364f1dd44b723bb5f9acba12f983e9ec8d69
  react-console: baf6dae382d6f41dce976c7f848667ccc37e076f
owned_by: SVC-CRM-DQ-merges
lifecycle: STATE-CRM-DQ-004
custom_fields: none
identity: >-
  The service gives each plan a time-ordered identity that supports a resumable
  workspace link. Stable question identities associate decisions with the
  conflicts they answer.
attributes:
  - name: record type
    kind: enum
    required: true
    computed: false
    notes: >-
      The plan is either for contacts or organizations; creation fixes this
      type.
  - name: participants
    kind: collection
    required: true
    computed: false
    notes: The selection contains between two and ten distinct records of that type.
  - name: survivor
    kind: reference
    required: true
    computed: false
    notes: >-
      The retained record must be a participant; an omitted choice uses the
      recommendation.
  - name: survivor recommendation
    kind: computed
    required: true
    computed: true
    notes: >-
      Contact ranking prefers a sole portal login, then a sole integration
      identity, then reference count and age. Organization ranking omits the
      login signal.
  - name: answers
    kind: collection
    required: false
    computed: false
    notes: >-
      Decisions attach a chosen value or source to a question with author and
      time. Updates accumulate decisions unless explicitly cleared.
  - name: computed plan
    kind: computed
    required: false
    computed: true
    notes: >-
      The stored preview includes proposed values and collections, conflicts,
      reference and login actions, blockers, warnings, omissions, participant
      summaries and estimated size.
  - name: material hash
    kind: computed
    required: false
    computed: true
    notes: >-
      The digest covers material changes, dependent identities and versions, and
      selected source records; wording, estimates and carried dismissals are
      excluded.
  - name: status
    kind: enum
    required: true
    computed: true
    notes: >-
      The planner reports needs decisions, ready or blocked; execution adds
      executing and executed, and the sweep adds expired.
  - name: expiry
    kind: datetime
    required: true
    computed: true
    notes: >-
      Implementation sets expiry one week after creation without extending it on
      edits. The approved last-change lifetime remains unmet.
  - name: version and attribution
    kind: computed
    required: true
    computed: true
    notes: >-
      Optimistic versions guard changes, while creator and modification
      attribution describe the working row.
  - name: merge attempts
    kind: computed
    required: false
    computed: true
    notes: 'Reads include up to ten recent attempts for this plan, including failures.'
relationships:
  - to: DOM-CRM-ENT-001
    cardinality: One contact plan selects two to ten contacts.
    ownership: >-
      Entities retains contact ownership; the plan stores selected identities
      and a computed preview.
    cross_service: false
  - to: DOM-CRM-ENT-002
    cardinality: One organization plan selects two to ten organizations.
    ownership: >-
      Entities retains organization ownership; the plan holds references and
      copied preview values.
    cross_service: false
  - to: DOM-CRM-ENT-003
    cardinality: >-
      A plan can describe moves or combinations for many participant
      affiliations.
    ownership: >-
      Entities owns affiliations; planning describes proposed treatment without
      applying it.
    cross_service: false
  - to: DOM-CRM-DQ-004
    cardinality: >-
      One plan can have many attempts, with at most one active or successful
      attempt.
    ownership: >-
      Merge history retains the plan identity without a foreign key and can
      outlive the plan.
    cross_service: false
  - to: DOM-CRM-DQ-001
    cardinality: Selections may originate from review groups without a stored association.
    ownership: The duplicates module remains the owner of review evidence.
    cross_service: false
invariants:
  - id: INV-DOM-CRM-DQ-003-01
    statement: >-
      A plan selects two to ten distinct participants and retains one of them as
      survivor.
    enforced_at:
      - service
      - database
    provenance: observed
    source: >-
      services-crm:modules/merges/services/persistence/record-merge-plans.ts:createRecordMergePlan;
      modules/merges/db/record-merge-plans.table.ts:record_merge_plans_record_ids_check
  - id: INV-DOM-CRM-DQ-003-02
    statement: >-
      A stored computed preview and its hash must either both exist or both be
      absent.
    enforced_at:
      - database
    provenance: observed
    source: >-
      services-crm:modules/merges/db/record-merge-plans.table.ts:record_merge_plans_plan_hash_check
  - id: INV-DOM-CRM-DQ-003-03
    statement: >-
      Plan changes reject an outdated version before applying the requested
      decisions.
    enforced_at:
      - api
      - service
    provenance: test
    source: >-
      services-crm:modules/merges/tests/integration/services/merges/merge-plan-decisions.integration.spec.js#leaves
      both decisions on the plan, at the version it was refused against
  - id: INV-DOM-CRM-DQ-003-04
    statement: >-
      Decisions accumulate by question until cleared, and a request cannot
      answer and clear the same question.
    enforced_at:
      - api
      - service
    provenance: test
    source: >-
      services-crm:modules/merges/tests/e2e/merges.e2e.spec.js#keeps the first
      answer when the second request answers only the other conflict
  - id: INV-DOM-CRM-DQ-003-05
    statement: >-
      Field choices are restricted to participating values or an empty choice
      for optional fields.
    enforced_at:
      - service
    provenance: test
    source: >-
      services-crm:modules/merges/tests/unit/services/planning/contact-fields.spec.js#offers
      clearing the field as well as each value held
  - id: INV-DOM-CRM-DQ-003-06
    statement: >-
      Plans cannot be edited after entering executing, executed or expired
      standing.
    enforced_at:
      - service
    provenance: observed
    source: >-
      services-crm:modules/merges/services/merges/merge-plans.ts:assertPlanIsOpen
  - id: INV-DOM-CRM-DQ-003-07
    statement: >-
      Open-plan reads, changes and validation recompute current data and store
      changed results; settled plans retain their stored preview.
    enforced_at:
      - service
    provenance: test
    source: >-
      services-crm:modules/merges/tests/e2e/merges.e2e.spec.js#answers the
      validation with the plan as it is now, ready to execute
  - id: INV-DOM-CRM-DQ-003-08
    statement: >-
      Execution requires cleared blockers and required questions, with explicit
      acceptance of defaulted decisions.
    enforced_at:
      - api
      - service
    provenance: test
    source: >-
      services-crm:modules/merges/tests/e2e/merges.e2e.spec.js#names the
      conflict that is still open, so a console can take the reader to it
  - id: INV-DOM-CRM-DQ-003-09
    statement: >-
      Execution refuses a current material preview that differs from the stored
      reviewed preview.
    enforced_at:
      - service
    provenance: test
    source: >-
      services-crm:modules/merges/tests/integration/services/merges/approval-binding.integration.spec.js#refuses
      an execution that submits no hash too, because the stored plan is the one
      last shown
  - id: INV-DOM-CRM-DQ-003-10
    statement: >-
      Presentation changes, estimates, survivor recommendations and carried
      dismissals are excluded from material hashing.
    enforced_at:
      - service
    provenance: test
    source: >-
      services-crm:modules/merges/tests/integration/services/merges/lookup-labels.integration.spec.js#answers
      the same material hash, a name being no part of what staff are approving
  - id: INV-DOM-CRM-DQ-003-11
    statement: A reference lacking a merge policy creates a blocker with no override.
    enforced_at:
      - service
    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
  - id: INV-DOM-CRM-DQ-003-12
    statement: >-
      The expiry sweep closes overdue working plans at an instant fixed one week
      after creation, including plans edited since then.
    enforced_at:
      - service
    provenance: observed
    source: >-
      services-crm:modules/merges/services/persistence/record-merge-plans.ts:defaultExpiry,
      updateRecordMergePlan, expireRecordMergePlans
  - id: INV-DOM-CRM-DQ-003-13
    statement: >-
      Plan routes require both read and merge authority for the relevant record
      type.
    enforced_at:
      - api
    provenance: test
    source: >-
      services-crm:modules/merges/tests/e2e/merges.e2e.spec.js#refuses the plan
      to a caller who may merge contacts but not organizations
  - id: INV-DOM-CRM-DQ-003-14
    statement: Workspace policy limits access to a plan’s own workspace.
    enforced_at:
      - database
    provenance: observed
    source: >-
      services-crm:db/row-level-security.ts (record_merge_plans forced workspace
      policy)
physical:
  - record_merge_plans
evidence:
  - >-
    services-crm at a8d1364f: modules/merges/db/record-merge-plans.table.ts;
    services/merges/merge-plans.ts, merge-plan-objects.ts, merge-refusals.ts,
    merge-plan-lists.ts; services/planning/*.ts;
    services/persistence/record-merge-plans.ts; docs/merging/architecture.md §4,
    conflict-resolution.md, api.md §4.
---

# Merge Plan

## What It Models

A merge plan is a shared working preview of a proposed combination. It records the selected participants, survivor and answers while the planner derives the values and reference changes that execution would apply.

## Identity and Attributes

The plan identity supports returning to the same workspace. Decisions carry their authors, and optimistic versions protect edits. Open-plan reads can persist a refreshed preview and approval hash, so reading is an observable write when underlying data changes.

## Relationships

Participants and affiliations remain owned by Entities. Plans describe their proposed treatment and can generate multiple attempts after failure; merge history remains even if a plan is later removed.

## Invariants

Database checks constrain participant count, survivor membership and preview/hash pairing. The service additionally enforces distinct selections, version checks, valid answers and execution readiness.

## How It Is Stored and Shared

The merges module owns the working row in CRM. Any qualified workspace administrator can access it regardless of creator, and it is excluded from workspace exports. No custom fields are declared.

## Open Design Questions

Product still needs to resolve creator-only versus shared editing and whether another administrator’s read renews approval. Implementation must reconcile creation-based expiry and missing cleanup with the approved lifetime and retention rules.
