---
id: ARCH-CRM-01
title: 'Rhythm CRM Architecture'
product_area: crm
revision: 1
status: proposed
source:
  artifact_url: https://claude.ai/artifact/QbhursoBJ37Wr62f2yXDZq
  artifact_title: 'Rhythm CRM Architecture'
  library_group: 'CRM and people'
  library_status: 'Proposed'
  artifact_version: 5
  markdown_source: 'services-platform:docs/proposals/crm@ee02d1a554fa9543feff4cba3805c0c99c6e50fc'
  converted_at: 2026-10-23
  converted_by: 'Forge 2.0 artifact strategy session'
contents:
  - n: 1
    title: 'RHYTHM-CRM-ARCH-001: Rhythm 2.0 CRM — Definitive Architecture and Completion Plan'
    anchor: '1-rhythm-crm-arch-001-rhythm-2-0-crm-definitive-architecture-and-completion-plan'
  - n: 2
    title: '01. Product Vision, Architectural Principles and Research Method'
    anchor: '2-01-product-vision-architectural-principles-and-research-method'
  - n: 3
    title: '02. Rhythm 1.0 (Rolodex) and the Support History'
    anchor: '3-02-rhythm-1-0-rolodex-and-the-support-history'
  - n: 4
    title: '03. Rhythm 2.0 CRM Today: Assessment by Functional Area'
    anchor: '4-03-rhythm-2-0-crm-today-assessment-by-functional-area'
  - n: 5
    title: '04. Domain and Relational Data Model'
    anchor: '5-04-domain-and-relational-data-model'
  - n: 6
    title: '05. Functional Architecture: What Every CRM Capability Does'
    anchor: '6-05-functional-architecture-what-every-crm-capability-does'
  - n: 7
    title: '06. API and Service Architecture'
    anchor: '7-06-api-and-service-architecture'
  - n: 8
    title: '07. Duplicate Detection and Record Merging'
    anchor: '8-07-duplicate-detection-and-record-merging'
  - n: 9
    title: '08. Record Lifecycle and Safe Delete'
    anchor: '9-08-record-lifecycle-and-safe-delete'
  - n: 10
    title: '09. Console Experience: Search, Explorer, Queries and the 360 Pages'
    anchor: '10-09-console-experience-search-explorer-queries-and-the-360-pages'
  - n: 11
    title: '10. Data Operations: Imports, Upserts, Exports, Bulk Changes and Rollback'
    anchor: '11-10-data-operations-imports-upserts-exports-bulk-changes-and-rollback'
  - n: 12
    title: '11. Security, Audit and Operational Contracts'
    anchor: '12-11-security-audit-and-operational-contracts'
  - n: 13
    title: '12. Cross-Application Integration Contracts'
    anchor: '13-12-cross-application-integration-contracts'
  - n: 14
    title: '13. Decisions: Standing, Recorded, and Needing Andrew (Deliverable D)'
    anchor: '14-13-decisions-standing-recorded-and-needing-andrew-deliverable-d'
  - n: 15
    title: '14. Implementation Gap Matrix (Deliverable B)'
    anchor: '15-14-implementation-gap-matrix-deliverable-b'
  - n: 16
    title: '15. Milestone Plan (Completion Roadmap, Deliverable C)'
    anchor: '16-15-milestone-plan-completion-roadmap-deliverable-c'
  - n: 17
    title: '16. Testing and Acceptance Strategy'
    anchor: '17-16-testing-and-acceptance-strategy'
  - n: 18
    title: '17. Risks, Unresolved Items and Verification Limits'
    anchor: '18-17-risks-unresolved-items-and-verification-limits'
  - n: 19
    title: '18. Traceability Appendix'
    anchor: '19-18-traceability-appendix'
  - n: 20
    title: 'e01 — Rhythm 1.0 Rolodex: records and relationships (evidence)'
    anchor: '20-e01-rhythm-1-0-rolodex-records-and-relationships-evidence'
  - n: 21
    title: 'E02. Rhythm 1.0 Rolodex: activities, engagement, tasks, duplicates and merging, search, data operations'
    anchor: '21-e02-rhythm-1-0-rolodex-activities-engagement-tasks-duplicates-and-merging-search-data-operations'
  - n: 22
    title: 'E03 — Rhythm 2.0 CRM Backend Assessment (services-crm)'
    anchor: '22-e03-rhythm-2-0-crm-backend-assessment-services-crm'
  - n: 23
    title: 'e04 — CRM console assessment (react-console)'
    anchor: '23-e04-crm-console-assessment-react-console'
  - n: 24
    title: 'E05. Platform capabilities CRM depends on, and every other application''s references to CRM records'
    anchor: '24-e05-platform-capabilities-crm-depends-on-and-every-other-application-s-references-to-crm-records'
  - n: 25
    title: 'E06 — Customer pain with Rhythm 1.0 CRM (Rolodex): Jira, Zendesk, HubSpot, Linear, GitHub'
    anchor: '25-e06-customer-pain-with-rhythm-1-0-crm-rolodex-jira-zendesk-hubspot-linear-github'
  - n: 26
    title: 'Review R1: Rhythm 2.0 capabilities the CRM architecture overlooks or understates'
    anchor: '26-review-r1-rhythm-2-0-capabilities-the-crm-architecture-overlooks-or-understates'
  - n: 27
    title: 'Review R2: Rhythm 1.0 CRM functionality missing from 2.0 and from this document'
    anchor: '27-review-r2-rhythm-1-0-crm-functionality-missing-from-2-0-and-from-this-document'
  - n: 28
    title: 'Review r3: consistency, ownership and feasibility'
    anchor: '28-review-r3-consistency-ownership-and-feasibility'
  - n: 29
    title: 'Review resolutions'
    anchor: '29-review-resolutions'
decisions: []
modules: [MOD-CRM-ENT,MOD-CRM-ACT,MOD-CRM-DQ,MOD-CRM-REG]
superseded_by:
---

# Rhythm CRM Architecture

## What This Record Is

RHYTHM-CRM-ARCH-001 is the definitive architecture and completion plan for Rhythm 2.0 CRM (customer relationship management: contacts, organizations, affiliations, duplicates and merging, record lifecycle, console and data operations) and the standard against which CRM work is judged, with a gap matrix and milestones CRM-M9 to M23. Proposed 2026-10-19 for Andrew's review; eleven decisions CRM-Q1 to Q11 await him and the document itself builds nothing.

(Summary from the Rhythm 2.0 Architecture Library card; the parts below are the proposal's own text, converted verbatim.)

## Contents

1. [RHYTHM-CRM-ARCH-001: Rhythm 2.0 CRM — Definitive Architecture and Completion Plan](#1-rhythm-crm-arch-001-rhythm-2-0-crm-definitive-architecture-and-completion-plan)
2. [01. Product Vision, Architectural Principles and Research Method](#2-01-product-vision-architectural-principles-and-research-method)
3. [02. Rhythm 1.0 (Rolodex) and the Support History](#3-02-rhythm-1-0-rolodex-and-the-support-history)
4. [03. Rhythm 2.0 CRM Today: Assessment by Functional Area](#4-03-rhythm-2-0-crm-today-assessment-by-functional-area)
5. [04. Domain and Relational Data Model](#5-04-domain-and-relational-data-model)
6. [05. Functional Architecture: What Every CRM Capability Does](#6-05-functional-architecture-what-every-crm-capability-does)
7. [06. API and Service Architecture](#7-06-api-and-service-architecture)
8. [07. Duplicate Detection and Record Merging](#8-07-duplicate-detection-and-record-merging)
9. [08. Record Lifecycle and Safe Delete](#9-08-record-lifecycle-and-safe-delete)
10. [09. Console Experience: Search, Explorer, Queries and the 360 Pages](#10-09-console-experience-search-explorer-queries-and-the-360-pages)
11. [10. Data Operations: Imports, Upserts, Exports, Bulk Changes and Rollback](#11-10-data-operations-imports-upserts-exports-bulk-changes-and-rollback)
12. [11. Security, Audit and Operational Contracts](#12-11-security-audit-and-operational-contracts)
13. [12. Cross-Application Integration Contracts](#13-12-cross-application-integration-contracts)
14. [13. Decisions: Standing, Recorded, and Needing Andrew (Deliverable D)](#14-13-decisions-standing-recorded-and-needing-andrew-deliverable-d)
15. [14. Implementation Gap Matrix (Deliverable B)](#15-14-implementation-gap-matrix-deliverable-b)
16. [15. Milestone Plan (Completion Roadmap, Deliverable C)](#16-15-milestone-plan-completion-roadmap-deliverable-c)
17. [16. Testing and Acceptance Strategy](#17-16-testing-and-acceptance-strategy)
18. [17. Risks, Unresolved Items and Verification Limits](#18-17-risks-unresolved-items-and-verification-limits)
19. [18. Traceability Appendix](#19-18-traceability-appendix)
20. [e01 — Rhythm 1.0 Rolodex: records and relationships (evidence)](#20-e01-rhythm-1-0-rolodex-records-and-relationships-evidence)
21. [E02. Rhythm 1.0 Rolodex: activities, engagement, tasks, duplicates and merging, search, data operations](#21-e02-rhythm-1-0-rolodex-activities-engagement-tasks-duplicates-and-merging-search-data-operations)
22. [E03 — Rhythm 2.0 CRM Backend Assessment (services-crm)](#22-e03-rhythm-2-0-crm-backend-assessment-services-crm)
23. [e04 — CRM console assessment (react-console)](#23-e04-crm-console-assessment-react-console)
24. [E05. Platform capabilities CRM depends on, and every other application's references to CRM records](#24-e05-platform-capabilities-crm-depends-on-and-every-other-application-s-references-to-crm-records)
25. [E06 — Customer pain with Rhythm 1.0 CRM (Rolodex): Jira, Zendesk, HubSpot, Linear, GitHub](#25-e06-customer-pain-with-rhythm-1-0-crm-rolodex-jira-zendesk-hubspot-linear-github)
26. [Review R1: Rhythm 2.0 capabilities the CRM architecture overlooks or understates](#26-review-r1-rhythm-2-0-capabilities-the-crm-architecture-overlooks-or-understates)
27. [Review R2: Rhythm 1.0 CRM functionality missing from 2.0 and from this document](#27-review-r2-rhythm-1-0-crm-functionality-missing-from-2-0-and-from-this-document)
28. [Review r3: consistency, ownership and feasibility](#28-review-r3-consistency-ownership-and-feasibility)
29. [Review resolutions](#29-review-resolutions)

## 1. RHYTHM-CRM-ARCH-001: Rhythm 2.0 CRM — Definitive Architecture and Completion Plan

## RHYTHM-CRM-ARCH-001: Rhythm 2.0 CRM — Definitive Architecture and Completion Plan

Status: **proposed 2026-10-19 for Andrew's review; documentation only, nothing built by this work.** Branch `crm-architecture`
of services-platform (the precedent of the Commerce, Accounting and record-lifecycle proposals). Once accepted, this is
the standard against which all CRM implementation, enhancements, tests and guided-build acceptance are judged.

Labels used throughout: **Verified** (read in code or tests at the cited commit), **Decided** (Andrew, dated), **Ruled**
(Andrew's acceptance rulings D-01..D-53 and E-01..E-18 of 2026-10-19), **Recommended** (this document), **Open** (needs
Andrew, CRM-Q1..Q8), **Unverified**.

### How future coding agents reference this document

- Cite it as **RHYTHM-CRM-ARCH-001** with the chapter and section, for example "RHYTHM-CRM-ARCH-001 §05 2.3".
- A brief for CRM work names the milestone (**CRM-M9..M23**; CRM-M1..M8 are the retroactive milestones for built work) and the gap rows it closes (for example `MG-08`, `CF-01`) from
  [14](#15-14-implementation-gap-matrix-deliverable-b), and states which **Ruled**/**Decided** items constrain it.
- Where this document and code disagree, the code is the current state and this document is the target; the gap row says
  which. Where this document and an older design disagree, this document wins unless the older one records a later
  decision by Andrew.
- Binding sub-specifications adopted here: affiliations (`services-crm:docs/affiliations/README.md`), merging
  (`services-crm:docs/merging/`), record lifecycle (`docs/proposals/record-lifecycle/`, on branch
  `agent/crm-acc-w3-arch-lifecycle` `6873e3b6c` until it lands), record write contracts
  (`docs/standards/record-write-contracts.md`, on branch `agent/crm-acc-w3-arch-writes` until it lands), each amended only
  by later rulings named where they apply; milestones follow R2-DEV-001 (`docs/standards/milestone-development-and-acceptance.md`).

### The documents

| #   | Document                                                                           | Brief deliverable                                        |
| --- | ---------------------------------------------------------------------------------- | -------------------------------------------------------- |
| 01  | [Vision, principles and research method](#2-01-product-vision-architectural-principles-and-research-method)       | §6 items 2–3                                             |
| 02  | [Rhythm 1.0 (Rolodex) and the support history](#3-02-rhythm-1-0-rolodex-and-the-support-history) | §6 items 4–5                                             |
| 03  | [Rhythm 2.0 CRM today, by functional area](#4-03-rhythm-2-0-crm-today-assessment-by-functional-area)         | §6 item 6                                                |
| 04  | [Domain and relational data model](#5-04-domain-and-relational-data-model)                    | §6 items 8, 14                                           |
| 05  | [Functional architecture](#6-05-functional-architecture-what-every-crm-capability-does)                           | §6 items 7, 13                                           |
| 06  | [API and service architecture](#7-06-api-and-service-architecture)                 | §6 item 9                                                |
| 07  | [Duplicate detection and record merging](#8-07-duplicate-detection-and-record-merging)  | §6 item 13                                               |
| 08  | [Record lifecycle and safe delete](#9-08-record-lifecycle-and-safe-delete)                | Andrew's addition                                        |
| 09  | [Console experience: search, Explorer, queries, 360](#10-09-console-experience-search-explorer-queries-and-the-360-pages)     | §6 item 11                                               |
| 10  | [Data operations](#11-10-data-operations-imports-upserts-exports-bulk-changes-and-rollback)                                           | §2 L                                                     |
| 11  | [Security, audit and operations](#12-11-security-audit-and-operational-contracts)              | §6 item 10                                               |
| 12  | [Cross-application contracts](#13-12-cross-application-integration-contracts)                   | §6 item 12, §3                                           |
| 13  | [Decisions](#14-13-decisions-standing-recorded-and-needing-andrew-deliverable-d)                                                       | §6 item 15; **Deliverable D**                            |
| 14  | [Implementation gap matrix](#15-14-implementation-gap-matrix-deliverable-b)                                      | §6 item 16; **Deliverable B**                            |
| 15  | [Milestone plan (completion roadmap)](#16-15-milestone-plan-completion-roadmap-deliverable-c)                    | §6 item 17; **Deliverable C**                            |
| 16  | [Testing and acceptance](#17-16-testing-and-acceptance-strategy)                             | §6 item 18                                               |
| 17  | [Risks, unresolved items, verification limits](#18-17-risks-unresolved-items-and-verification-limits)     | §6 item 19                                               |
| 18  | [Traceability appendix](#19-18-traceability-appendix)                                        | §6 item 20                                               |
| —   | [evidence/](evidence/)                                                             | e01–e06 investigation reports with `path:line` citations |

### Executive summary (Deliverable E)

#### What CRM is

CRM is the system of record for the people and organizations an association deals with: their dated relationships, contact details, recorded staff work, and data quality and lifecycle. Every other application points to CRM, so it must provide a stable answer when an identity changes, merges, or goes away.

#### 1. How complete is CRM today?

**The backend is substantially built and well tested; the product is not finished.** Acceptance on **2026-10-19**, before repair waves 2–3, passed **212 of 617 journeys** and concluded that CRM was “not ready for a customer”. The Tuesday **2026-10-20** release-candidate run will measure those repairs.

The current state by area ([03](#4-03-rhythm-2-0-crm-today-assessment-by-functional-area)):

- **Solid:** contact identity and computed names; affiliations, with one primary enforced by the database; typed and linked contact information; regions; and the merge engine within CRM.
- **Usable with gaps:** organizations, which have no general edit form (Change Type works); roles and compliance; activities, whose list is read-only; tasks; duplicate detection; merge, which has no manual picker, reports totals only, and lacks participation from four applications; Explorer and queries; 360 pages; data operations; history; and security.
- **Backend only or simulated in the console:** custom-field administration, which is a browser mock in every environment; record layouts; the global search page; engagement scoring screens; and CRM processes screens.
- **Not built:** archive and safe delete, with archive in progress; business events beyond merge; portal CRM pages; relationships; directories; and engagement scoring.

#### 2. The most important missing capabilities

1. **Cross-application merge support.** Accounting, Commerce, Membership, and Submissions do not participate in merges, so merges strand their references (MG-08). The proposed protection is to block those merges (CRM-Q3).
2. **Persistent custom-field administration.** The current interface saves nothing (CF-01).
3. **Organization editing.** Organizations cannot be edited in the console (OR-02).
4. **Lifecycle controls.** Archive is in progress (LC-01). Safe delete comes later and has been deferred by Andrew (LC-03..05).
5. **Safe writes.** Lost-update and omitted-field protection are part of the acceptance repairs (WR-01, WR-02).
6. **Application contracts.** Other applications need business events and service functions (EV-01, X-04).
7. **Self-service and communication controls.** Missing capabilities include portal self-service (PO-01..05), sending that respects opt-outs and deceased status (CI-09), queryable communication preferences (SR-10b), and quick-add from shared pickers (UI-18).

#### 3. What Rhythm 1.0 did that 2.0 has not reproduced or intentionally replaced

**Not reproduced:**

- Generic relationships, member and organization directories, engagement scoring, and portal self-service.
- Organization lookup by e-mail domain, which is partly built.
- Marketing e-mail activity on the timeline and the contact e-mail history tab.
- Outbound customer integrations: EventBridge forwarding, Zapier, Higher Logic, Mailchimp, and the Outlook add-in.
- Quick-add contact from pickers, computed custom fields, staff “log in as member”, enrichment, and the Sales application.

**Intentionally replaced or dropped:** free-text organization name, now represented by affiliations; inline organization creation; name parsing on save; contact-role rule sets, which were dead in 1.0; and the per-workspace on/off setting for duplicate e-mail prevention (the rule is always on).

Full list: [14 L1 rows](#15-14-implementation-gap-matrix-deliverable-b).

#### 4. The biggest architectural flaws or risks in the current implementation

1. **Cross-application references are neither merge-safe nor delete-safe.** This is the same root cause as 1.0’s worst failures.
2. **Writes can lose data.** Until the write contract is enforced, concurrent writes can overwrite each other and omitted collections can be silently lost.
3. **The live console does not consistently reflect real functionality.** Some areas still show sample data and inert controls.
4. **Other applications read CRM’s physical tables.** That coupling prevents CRM from safely evolving its schema.
5. **SOC 2 exceptions expire on 2026-11-07.** Without resolution, services-crm CI will fail from that date.
6. **Organization hierarchy is unavailable to queries and eligibility.**

#### 5. What to change before building anything else

**Address three protections within the retroactive milestones.** Each requires Andrew’s recorded decision under R2-DEV-001 and is handled by the CRM build session:

- Block merges that would strand other applications’ references (CRM-Q3).
- Settle the SOC 2 exceptions expiring on **2026-11-07**, which would otherwise stop every services-crm merge. The decision set is in the CRM-M6 plan.
- Decide whether to close the console’s bulk-delete path until safe delete is available (CRM-Q10).

Search durability (D-29) is already landing. Once the retroactive milestones are accepted, the first new milestones, CRM-M9 and CRM-M10, remove sample data and inert controls from the live console.

#### 6. Recommended milestones, in order

Accept the built work first through retroactive milestones **CRM-M1..M8**, handled by the CRM build session. Then proceed in this order:

| Milestone | Scope                                         |
| --------- | --------------------------------------------- |
| M9        | Organization editing and Organization 360     |
| M10       | Live-console truth                            |
| M11       | Custom-field administration                   |
| M12       | Record layouts                                |
| M13       | Communication preferences                     |
| M14       | Configuration and contact channels            |
| M15       | Platform-facing contracts, backend            |
| M16       | Duplicates and merge completion               |
| M17       | Activities and tasks                          |
| M18       | Search and queries                            |
| M19       | Data operations                               |
| M20       | Cross-service safe delete, deferred by Andrew |
| M21       | Relationships and households                  |
| M23       | Record-level and field-level security         |
| M22       | Portal self-service, always last (Andrew)     |

Portal functionality comes last (Andrew, 2026-10-19): CRM-M22 is built after every other scheduled milestone, on his portal components. Other applications build their merge and deletion participants and their 360 cards in their own plans. Details, dependencies, and walkthroughs: [15](#16-15-milestone-plan-completion-roadmap-deliverable-c).

#### 7. Decisions that need Andrew

Eleven decisions are written out in full in [13 §3](#14-13-decisions-standing-recorded-and-needing-andrew-deliverable-d):

- **CRM-Q1:** task queues.
- **CRM-Q2:** relationships and households.
- **CRM-Q3:** blocking merges that would strand other applications’ references.
- **CRM-Q4:** directories.
- **CRM-Q5:** engagement scoring.
- **CRM-Q6:** e-mail types.
- **CRM-Q7:** exclusive roles.
- **CRM-Q8:** record-level staff scoping.
- **CRM-Q9:** refusing inapplicable roles.
- **CRM-Q10:** hard delete until safe delete.
- **CRM-Q11:** the 1.0 Sales application.

#### 8. Known gaps versus unverified areas

**Known gaps** are evidence-backed rows in [14](#15-14-implementation-gap-matrix-deliverable-b).

**Unverified areas** are HubSpot and Linear history, deployed behaviour after repair waves 2–3, compliance e-mail delivery, Quick Search ranking, newer Accounting tables, and same-id retry per route. Each is listed with its consequence in [17 §3](#18-17-risks-unresolved-items-and-verification-limits).

### How this document was validated

- The executive summary above was edited for readability by the Humanize pass (2026-10-19) and checked fact by fact
  against the chapters.

- Six evidence reports (e01–e06) read every source cited; major statements were rechecked against services-crm
  `origin/master` and react-console `1f345862b`.
- Independent review passes: one searched for capabilities already built in 2.0 that this document missed (27 findings),
  one for 1.0 functionality absent from 2.0 and from this document (19 gaps, 6 corrections), one for contradictions,
  ownership conflicts and feasibility (45 findings). Their reports are in [review/](review/); every high and medium finding
  was applied (resolution log in [review/resolutions.md](#29-review-resolutions)).
- Statuses were re-baselined at services-crm `bb55ae7a` after the reviews found 55 newer commits.
- Every requirement maps to a gap row or an explicit out-of-scope line ([14 Coverage check](#15-14-implementation-gap-matrix-deliverable-b)).

## 2. 01. Product Vision, Architectural Principles and Research Method

## 01. Product Vision, Architectural Principles and Research Method

Part of [RHYTHM-CRM-ARCH-001](#1-rhythm-crm-arch-001-rhythm-2-0-crm-definitive-architecture-and-completion-plan).

### 1. What CRM is

CRM is the Rhythm 2.0 application that knows **who** an association deals with. It is the system of record for people
(contacts) and organizations, for the dated relationships between them (affiliations and, later, person-to-person and
organization-to-organization relationships), for the ways to reach them (addresses, e-mail addresses, phone numbers,
communication preferences), and for the staff work recorded against them (activities and tasks). It also owns the
quality of that data: duplicate detection, record merging, data-quality remediation and the lifecycle of a record
(active, inactive, archived, deleted).

Every other application points at CRM. A membership is held by a contact or an organization; an invoice is billed to one;
a committee seat, a submission, an order line, an entitlement and a portal login all name one. CRM does not own those
records. It owns the identity they point at, and it owes them a stable answer when that identity changes, merges or goes
away.

CRM is **not**:

- the portal or its pages (the Web Experience Platform owns pages; CRM owns the data and the APIs a profile page or an
  organization roster calls);
- the query engine, the Explorer, the layout and form registry, the custom-field definition store, the data job engine,
  record history plumbing, communications or security (all Platform, consumed by CRM);
- membership status, chapters, billing, registrations or credentials (each owned by its own application, shown on CRM's
  record pages through metadata-driven cards those applications contribute).

### 2. Architectural principles

Each principle is tied to evidence: a Rhythm 1.0 failure it prevents (pattern numbers from
[02 §4](#3-02-rhythm-1-0-rolodex-and-the-support-history)), or a standing decision.

| #    | Principle                                                       | What it means in practice                                                                                                                                                                                                           | Why                                                                         |
| ---- | --------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------- |
| P-1  | **One owner per rule, every channel calls it**                  | Validation, matching, role rules, address rules and permissions live in CRM's domain services and the database. Console, portal, import, bulk update, API and other applications all go through them. No rule exists only in a form | 1.0 patterns P4, P5, P6, P9: each entry path had its own rules              |
| P-2  | **References, not copies**                                      | A contact's organization is an affiliation row, never a name string. An address supplied by an organization is a link resolved at read time, not a copy kept in sync. Role names are joined, never copied                           | 1.0 patterns P5, P6; Andrew 2026-10-13 (affiliations §0b)                   |
| P-3  | **Every reference to a CRM record is known**                    | Each application declares, in code and checked by a coverage test, every column that holds a contact or organization id, with its merge policy and (when safe delete lands) its deletion disposition                                | 1.0 pattern P1 and theme 1: ids copied into other services with no registry |
| P-4  | **All or nothing for identity changes**                         | A merge commits completely or changes nothing, inside one transaction across all participating schemas. Its preview and its execution run the same code                                                                             | 1.0 patterns P1, P2; merge decision D1 (2026-10-11)                         |
| P-5  | **Derived views have a stated freshness, or are computed live** | Duplicate candidates, Quick Search, query datasets and directories either read the source of truth or carry a measured freshness bound and a test that proves it                                                                    | 1.0 theme 2: copies that drifted                                            |
| P-6  | **No silent failure**                                           | Every partial failure (an import row, a bulk job item, a merge blocker, a compliance notice) reaches staff in the console with a reason                                                                                             | 1.0 theme 4: failures hidden in dead-letter queues                          |
| P-7  | **Lifecycle operations are distinct**                           | Deactivate, archive and hard delete are separate operations with separate permissions; archive never deletes; hard delete runs only through an impact preview once safe delete is built                                             | Acceptance rulings §2.1, D-02, D-41 (2026-10-19)                            |
| P-8  | **Writes are safe to repeat and safe under concurrency**        | Field-changing writes carry a version; a stale version answers 409; a no-op changes nothing; an omitted collection is unchanged; creates use client-assigned UUIDv7 ids so a retry never duplicates                                 | Acceptance rulings §2.2, D-05, D-09, D-43                                   |
| P-9  | **The live console shows only what works**                      | No placeholder action, sample record or mock-backed screen in a live console. Mock mode is one switch for every application                                                                                                         | Acceptance rulings §2.5, D-02, D-53; Andrew 2026-10-19 (one live switch)    |
| P-10 | **Shared systems before CRM-only components**                   | 360 cards, record actions, forms, field editors, audit views and Explorer recipes use Platform's metadata-driven systems. CRM adds definitions, not parallel frameworks                                                             | Acceptance ruling §2.5                                                      |
| P-11 | **Configuration ids are strings, record ids are UUIDv7**        | Configuration tables are keyed by immutable configuration-id strings; record tables by UUIDv7, checked in the database                                                                                                              | Andrew 2026-10-19 (ids standard, product-wide)                              |
| P-12 | **Small associations and large enterprises on one model**       | Every list has a limit, every scan is indexed, and every large operation (merge of a big organization, region recalculation, bulk job) runs as a durable background job with progress                                               | 1.0 patterns P7, P10, P11: timeouts at scale                                |

### 3. What this document is, and how it is labelled

This is the standard against which CRM implementation, enhancements, tests and guided-build acceptance are judged. Where it
and older documents disagree, this document wins unless the older document records a later decision by Andrew.

Statements carry one of these labels:

| Label           | Meaning                                                                                                                                                                                                                    |
| --------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Verified**    | Read in code or tests at the cited path and commit. A file's existence alone never earns this label                                                                                                                        |
| **Decided**     | Andrew decided it, with the date; a constraint, not a proposal                                                                                                                                                             |
| **Ruled**       | One of Andrew's acceptance rulings of 2026-10-19: D-01..D-53 (`react-console:acceptance/crm/reports/wave3-directives.md`) and E-01..E-18 (~17:20 ET, `wave4-directives-E.md`, branch `product-acceptance-crm` `82ab9ed58`) |
| **Recommended** | This document's proposal; it stands unless Andrew overturns it                                                                                                                                                             |
| **Open**        | Needs Andrew; listed in [13 §3](#14-13-decisions-standing-recorded-and-needing-andrew-deliverable-d)                                                                                                                                              |
| **Unverified**  | Could not be confirmed with the sources available                                                                                                                                                                          |

Note on the two decision namespaces: **D1..D10** are the record-merge decisions of 2026-10-11..14
(`services-crm:docs/merging/decisions.md`); **D-01..D-53** (with a hyphen) are the acceptance rulings of 2026-10-19.
New decisions in this document are numbered **CRM-Q1..** (questions for Andrew) and **CRM-ADR-01..** (records).

### 4. Research method and sources

Six evidence reports were produced on 2026-10-19 and are kept in [evidence/](evidence/). Each cites `repo/path:line`.

| Report                                                      | Covers                                                                                                                        | Sources read                                                                                                           |
| ----------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------- |
| [e01](#20-e01-rhythm-1-0-rolodex-records-and-relationships-evidence)    | Rhythm 1.0 contacts, organizations, relationships, roles, regions, directories, portal, admin console, security, provisioning | `~/rhythm/rolodex-*`, `libraries-*`, `platform-ngx-*`, `portals-ngx-site-renderer`; GitHub for repositories not cloned |
| [e02](#21-e02-rhythm-1-0-rolodex-activities-engagement-tasks-duplicates-and-merging-search-data-operations) | Rhythm 1.0 activities, engagement scoring, tasks, duplicate detection, merge, search, imports, events                         | as above, plus `platform-tasks*`, `sales-activities`, `rolodex-engagement-*`                                           |
| [e03](#22-e03-rhythm-2-0-crm-backend-assessment-services-crm)             | Rhythm 2.0 CRM backend, capability by capability with test evidence                                                           | services-crm `origin/master` `a980b553`                                                                                |
| [e04](#23-e04-crm-console-assessment-react-console)               | Rhythm 2.0 console screens for CRM: live, mock or sample; acceptance defect state                                             | react-console (snapshot and `origin/master`), `acceptance/crm/`                                                        |
| [e05](#24-e05-platform-capabilities-crm-depends-on-and-every-other-application-s-references-to-crm-records)               | Platform capabilities CRM depends on; every other application's references to CRM records                                     | services-platform, -accounting, -commerce, -committees, -membership, -submissions, -websites, macroservice-libraries   |
| [e06](#25-e06-customer-pain-with-rhythm-1-0-crm-rolodex-jira-zendesk-hubspot-linear-github)                 | Recurring customer pain with 1.0 CRM, 2020–2026                                                                               | Jira project RB, Zendesk; HubSpot, Linear and GitHub issues returned nothing (access log in e06)                       |

Also read directly for this document:

- the existing CRM sub-architectures, which this document adopts rather than rewrites:
  `services-crm:docs/affiliations/README.md` (decided 2026-10-13/14) and `services-crm:docs/merging/*` (D1..D10,
  2026-10-11..14);
- Andrew's 53 acceptance rulings and the CRM acceptance decision register
  (`react-console:acceptance/crm/reports/wave3-directives.md`, `decision-register.json`);
- the record lifecycle design ("safe delete"), `services-platform` branch `agent/crm-acc-w3-arch-lifecycle` (`6873e3b6c`),
  `docs/proposals/record-lifecycle/`, and the brief from the CRM acceptance session (2026-10-19 ~16:50 ET) stating Andrew's
  decision to defer its phases 2–3 to this document;
- the write-contract standard, branch `agent/crm-acc-w3-arch-writes`, `docs/standards/record-write-contracts.md`;
- the Commerce architecture (`docs/proposals/commerce-orders/`) as the benchmark for rigour and structure.

**Access limits and their consequence.** HubSpot tickets and Linear issues returned zero records when queried
(2026-10-19 16:39–16:40 ET); GitHub issues on the Rolodex repositories are empty and their history was squashed. The
customer-pain analysis therefore rests on Jira RB and Zendesk only. Because those two sources span 2020–2026 and are
consistent with each other and with the code, confidence in the thirteen patterns is high; confidence that no _other_
pattern exists is moderate. Jira project RX (Rhythm 2.0) was not mined in depth; 2.0 product decisions were taken from
the repositories and the acceptance decision register instead.

## 3. 02. Rhythm 1.0 (Rolodex) and the Support History

## 02. Rhythm 1.0 (Rolodex) and the Support History

Part of [RHYTHM-CRM-ARCH-001](#1-rhythm-crm-arch-001-rhythm-2-0-crm-definitive-architecture-and-completion-plan). Evidence: [e01](#20-e01-rhythm-1-0-rolodex-records-and-relationships-evidence),
[e02](#21-e02-rhythm-1-0-rolodex-activities-engagement-tasks-duplicates-and-merging-search-data-operations), [e06](#25-e06-customer-pain-with-rhythm-1-0-crm-rolodex-jira-zendesk-hubspot-linear-github). Paths below
are relative to `~/rhythm/`.

This chapter records what Rhythm 1.0's CRM let users do, the business rules hidden in its code, and the failures
customers lived with. It is the input to the "what did 1.0 do that 2.0 has not reproduced" analysis in
[14](#15-14-implementation-gap-matrix-deliverable-b) and to the regression criteria in [16](#17-16-testing-and-acceptance-strategy).

### 1. Shape of the 1.0 product

Rolodex was about twenty DynamoDB microservices (`rolodex-contacts`, `rolodex-organizations`, `rolodex-relationships`,
`rolodex-contact-roles`, `rolodex-regions`, the two directory services, `rolodex-custom-activities`, the engagement
services, `rolodex-contact-duplicates`, `rolodex-contact-steps` and others), an Angular staff console
(`rolodex-ngx-console`, `-console-controls`, `-controls`) and portal components (`rolodex-ngx-portal-controls`).
Ninety per cent of each service's behaviour came from a shared library (`libraries-dynamodb`): numbering, Joi
validation, custom-field validation, delete locks and change streams. Business rules lived partly in the services and
partly in the Angular form controls, which is why the console, portal, import and API enforced different rules.

### 2. What users could do, by area

#### 2.1 Contacts

- **Names.** Structured parts (prefix, salutation, first, middle, last, maiden, nickname, suffix, credentials). The
  display name `name`, `full_name` (with prefix) and `name_last_first` were **always computed** from the parts on save
  and overwrote anything the client sent (`rolodex-contacts/config.js:42-44`, `src/logic/calculate-full-name.js:9-93`).
  A supplied full name was parsed with `parse-full-name` only when neither first nor last name was present (RB-4385).
  Salutations that were really prefixes moved to `prefix` (RB-2367). The console form parsed a single "Name" box into
  parts unless the user had edited them.
- **Required fields.** None on the record. Validation was almost absent: e-mail addresses were not format-checked
  (`'te st@ test.com'` was kept), names had no length limits.
- **Statuses.** `active`, `duplicate`, `deceased`, `inactive`. No transition rules. Only "active versus not" mattered:
  a non-active contact could not be linked to a portal login. "Duplicate" was set by hand; merge deleted the losing
  record instead. Staff changed status through a "Change Status" dialog (with a deceased date).
- **Contact types.** Did not exist. Contacts were classified by contact roles and status only.
- **Contact information.** Three fixed e-mail slots (slot 1 primary by convention), six fixed phone slots plus typed
  "other" phones, four fixed addresses (home, work, school, seasonal) plus typed "other" addresses. A computed
  "preferred" phone and address with fallback orders; the school-phone preference fell through to the wrong value and
  "work mobile" was never resolved (`calculate-preferences.js:68-132`). US phones were formatted, others left as typed.
- **Employment.** One `organization_id` (the employer), a free-text `organization_name`, a job title and an
  `organization_history` array appended automatically when the organization changed (RB-127). Typing a new
  organization name created the organization inline (`__new:<Name>`).
- **Linked work address and phone.** Two flags copied the organization's main address and phone into the contact's
  work address and phone on every save; an organization change fanned the copy out to its contacts. Clearing the
  organization's address did not clear the copies.
- **Duplicate e-mail prevention.** A tenant setting (`disabled`, `portal`, `both`) refused a save whose primary e-mail
  belonged to another contact, whatever that contact's status. Secondary slots were never checked.
- **Portal sign-up.** The create-and-link workflow derived domains from the person's e-mails (excluding public domains
  such as gmail.com), linked the first organization with that domain, or created one, controlled by two tenant
  settings (`rolodex-contact-steps/src/lambda/steps.js:7-87`).
- **Staff actions on the record.** Edit, send message, change status, merge duplicates, delete (hard), log in to the
  portal as the member (impersonation, using the staff member's own tokens in the URL), reset the member's password,
  verify their e-mail.

#### 2.2 Organizations

- Name, acronym, website and domain (each derived from the other), alternate domains, two fixed addresses (main,
  billing) plus typed others, three fixed phones plus typed others, logo, social links, tax-exempt flag, status
  (`active`, `duplicate`, `inactive`). **No e-mail addresses.**
- **Organization types**: restrict which contact roles apply, key roles shown on the summary, per-type console and
  portal forms, and **contact-role compliance**.
- **Hierarchy**: a parent organization; ancestors recomputed on save (the depth guard never worked and moving a parent
  left children stale); descendants listed recursively; portal access inherited both up and down the tree.
- **Compliance**: per organization type, minimum and maximum holders of each contact role among the organization's
  direct contacts. The organization was flagged "not in compliance" with violations; on the transition into
  non-compliance an e-mail (Liquid template, previewable) went to holders of chosen roles. Changing the rules or an
  organization's type did nothing until an administrator ran "Reprocess Contact Role Compliance".
- **Merge** existed for organizations (source into master, field conflicts), with no duplicate detection.

#### 2.3 Relationships, roles and regions

- **Relationships**: a generic typed link between any two contacts or organizations, with start and end dates and notes.
  Relationship types fixed which kind sat on the left and right; no reciprocal label, no primary flag, no uniqueness
  or overlap rule. The console showed Current, Past and Future relationships on both record pages. Spouse was a
  separate `spouse_id` field, not a relationship.
- **Contact roles**: a tenant list with "administrative contact", "assign automatically to new contacts" and "assign
  to an organization's first contact". Roles were one array per contact, scoped to the current employer. New tenants
  got two roles (one mislabelled "Billing Contact").
- **Contact role rule sets** were configured but never evaluated anywhere (dead feature).
- **Regions**: a name only; assignment rules (postal-code ranges compared as text, states, countries, a catch-all) in
  tenant settings; reassigned on address change; bulk recalculation by an administrator.

#### 2.4 Activities, tasks and engagement

- **Activities** ([e02](#21-e02-rhythm-1-0-rolodex-activities-engagement-tasks-duplicates-and-merging-search-data-operations) §1): one contact or one organization each;
  tenant-defined types; no participants, links or attachments. The contact timeline merged staff activities with
  Campaign Monitor, Mailchimp and ActiveCampaign e-mail activity; other applications' activity stayed in their own panels.
  A missing activity type broke the whole timeline.
- **Tasks** lived in Platform (`platform-tasks`): assigned to a user or a queue (first-in, first-out, no lock on taking a
  task), an assignment e-mail, generation from product templates. Reminders and assignment roles were stored but did
  nothing. Any user could change any task; tasks were not moved on merge and were orphaned on delete.
- **Engagement scoring** (opt-in per tenant): monthly score per contact and organization from weighted metrics across
  six applications, percentile rank within the tenant, category bands, year-over-year change. Several fields were never
  filled; lookback and regeneration checked no permission.

#### 2.5 Duplicates and merge

- Contact duplicate candidates were precomputed into a separate store that ignored imported contacts (the largest source
  of duplicates), stored no reason, and was not refreshed after merges, edits or deletes. Staff could ignore pairs
  (permanently, unaudited) or add pairs by hand. A public, unauthenticated portal endpoint searched for matches.
- The merge wizard: review duplicates, resolve each conflicting field, preview, merge; a background workflow across
  services. It was **not atomic**, read only the first page of registered references (so heavily referenced records
  were merged incompletely and silently), re-keyed child records and silently dropped collisions, deleted the losing
  record with no forwarding alias, and could report success after failing.

#### 2.6 Directories, portal and security

- **Directories** (people and organizations): configured searchable fields, list or search mode, filters, radius
  search, eligibility, comments and ratings, contact-this-person messaging, cached results. Search skipped its security
  check entirely, messaging did not check whether it was enabled, and an entry could return any record of the tenant;
  a 2024 incident (ZD 20897) leaked dates of birth and home addresses until an allowlist fix (RB-7556).
- **Portal self-service**: update my profile (with name and credential locks after the first update), update my
  organization's profile, manage my organization's contacts (list with membership status, add, edit, detach) and child
  organizations, all permitted per contact role through a role-permission matrix; "administrative contact" roles got
  everything. Portal delegates existed in the schema but the code could not work.
- **Security**: permissions `rolodex:<level>:<resource>:<action>` generated per service, with resource scoping by the
  contact's organization type, chapter and section; four managed policies (read-only, user, power user, admin). Merge
  needed power user.
- **Settings**: duplicate e-mail prevention, sign-up organization matching and creation, new-contact redirect, child
  organization management in the portal, engagement, region rules, and fields added to queries.

#### 2.7 Data in and out

- Imports wrote rows first and validated afterwards, kept invalid rows, matched only by id or e-mail, and timed out on
  large files. Imported records were invisible to every consumer that filtered out imports until someone edited them.
- No bulk actions inside the CRM screens, but the System console offered tenant-wide **Mass Update** and **Mass Delete** for
  any service, contacts included, with saved mass-update templates (values, variables or expressions; add to, remove from or
  replace arrays) (`system-ngx-console/src/app/settings/settings.routes.ts:103,128`;
  `system-mass-update-templates/src/schema/object.schema.js:17-25`). Ad-hoc queries were the data-out path. Whole-tenant
  export was restricted to Rhythm administrators and an allowlist of nine tenants.
- Quick search: Algolia over declared fields; records without a name were skipped.
- Events: a contact-merge event on an EventBridge bus; record-change events could be forwarded to a tenant's own AWS
  account per application subscription (`partners-eventbridge/src/logic/push-logic.js:12-35`); Zapier triggers, Higher
  Logic and Mailchimp syncs and an Outlook add-in consumed CRM records; a declared
  `rolodex-contacts-changed` event nothing published; at least nine services consumed contact and organization change
  streams (Membership, Accreditation, Events abstracts, Regions, three partner syncs, duplicates, search).

### 3. Hidden rules and workarounds a port must not miss

| #    | Behaviour                                                                                 | Where                                                     | 2.0 treatment                                                                                                                               |
| ---- | ----------------------------------------------------------------------------------------- | --------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------- |
| H-1  | Computed names overwrite client input                                                     | `calculate-full-name.js`                                  | Kept: names are computed (Verified 2.0, [05 §2.1](#6-05-functional-architecture-what-every-crm-capability-does))                                               |
| H-2  | Non-active contacts cannot be linked to a portal login                                    | `contact-user-logic.js:113-118`                           | Platform login linking must apply the same rule; [12](#13-12-cross-application-integration-contracts)                                                    |
| H-3  | Contact-role auto-assignment skipped for imports                                          | `change-validation.js:48-56`                              | 2.0 assigns on primary change and, since D-46, on a due affiliation's start day; imports go through the same persistence (to verify, CM-04) |
| H-4  | Removing an organization clears the free-text name only if it still matches               | RB-738                                                    | Obsolete: no free-text organization in 2.0                                                                                                  |
| H-5  | A contact role filtered by organization type in the console but not the portal            | RB-5829                                                   | One rule in CRM ([05 §2.5](#6-05-functional-architecture-what-every-crm-capability-does))                                                  |
| H-6  | Email-domain matching excludes public domains                                             | `domain-logic.js:1-27`                                    | Required for sign-up matching and the by-domain lookup                                                                                      |
| H-7  | Tenant and test bypasses (`faa.org`, `tests_*`, `greekcircle`) in portal security         | `validate-rolodex-request.js:43-45`                       | Never ported: no tenant-specific code                                                                                                       |
| H-8  | Sentinel `notes` values that skip processing                                              | `flowdown.js:35`, `organization-change-logic.js:29`       | Never ported                                                                                                                                |
| H-9  | `minimum: 0` compliance rule never checked; "too few" wins over "too many"                | `contact-role-compliance.js:111-125`                      | Ruled E-04: a minimum-zero-only rule is discarded and the type still saves                                                                  |
| H-10 | Directory entry visible fields limited to what the directory displays (RB-7556 allowlist) | `build-entry-allowlist.js`                                | Required if directories are built ([13](#14-13-decisions-standing-recorded-and-needing-andrew-deliverable-d) CRM-Q4)                                                                            |
| H-11 | Org rename pushes the name to contacts with a raw write that leaves search data stale     | `rolodex-contacts/src/logic/organization-change-logic.js` | Obsolete: names joined on read                                                                                                              |
| H-12 | Merged-away ids had no forwarding record                                                  | `merge-contact.step.json`                                 | 2.0 keeps aliases and answers 410 with the survivor (D3, D-20)                                                                              |

### 4. Recurring customer failure patterns

From [e06](#25-e06-customer-pain-with-rhythm-1-0-crm-rolodex-jira-zendesk-hubspot-linear-github): thirteen patterns, ordered by damage. Each has a regression criterion
that [16](#17-16-testing-and-acceptance-strategy) turns into tests.

| #   | Pattern                                                                                      | Root cause in 1.0                                                             | 2.0 answer                                                                                                                                          | Status in 2.0                                                                                                                                                    |
| --- | -------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| P1  | Merge loses or orphans data (2021–2026; RB-7641 a paid renewal never advanced)               | Hard delete of the loser; other services kept its id; nothing re-pointed them | All-or-nothing merge across participant schemas; aliases; every reference-holding app must participate; until then such merges are blocked (CRM-Q3) | **Partial**: CRM, Platform and Committees participate; Accounting, Commerce, Membership, Submissions do not ([12 §3](#13-12-cross-application-integration-contracts)) |
| P2  | Merge tool fails with "unknown error"                                                        | Multi-service client-run steps; preview and execution ran different code      | One transaction; preview and execution share the planner; failures name the blocker                                                                 | **Verified** in CRM (merge engine)                                                                                                                               |
| P3  | Duplicate list shows phantoms and stale pairs                                                | Precomputed separate store never refreshed                                    | Candidates maintained from change events; bounded, explained scores; manual pairs; dismissals per pair                                              | **Verified** backend; console gaps ([14](#15-14-implementation-gap-matrix-deliverable-b) DQ rows)                                                                                              |
| P4  | System flows create duplicates                                                               | Every entry path had its own match-or-create                                  | One match-or-create service for every path                                                                                                          | **Partial**: synchronous checks and portal sign-up matching exist; not every path uses them                                                                      |
| P5  | Primary and role rules not enforced (828 orgs with several Owners; 4,548 unlinked org names) | Free-text organization; roles an array; compliance advisory                   | Affiliations as records; one primary in the database; roles on affiliations; no free-text organization                                              | **Verified** for primary and links; single-holder roles remain advisory (compliance)                                                                             |
| P6  | Address links and purposes differ by channel                                                 | Copies set by form hooks                                                      | Linked contact information resolved at read time in every channel                                                                                   | **Verified** backend; portal has no CRM pages yet                                                                                                                |
| P7  | Queries disagree with the record and break at scale                                          | Separately synced reporting copy; multiplied rows                             | Semantic query layer reads the source of truth                                                                                                      | **Verified** (Platform queries); hierarchy not yet queryable                                                                                                     |
| P8  | Search misses or buries exact matches                                                        | Separate index repaired by hand; merged records stayed                        | Quick Search fed by change events; merged and deleted records tombstoned                                                                            | **Partial**: DEF-CRM-0119 (records not indexed in dev) escalated                                                                                                 |
| P9  | Imports report success but partly fail                                                       | Write-then-validate; separate validation                                      | Data Job Engine: validate before apply; per-row outcome; same aggregate as the console                                                              | **Verified**; typed-collection mapping incomplete (D-40)                                                                                                         |
| P10 | Portal profile and roster fail                                                               | Separate permission model; no paging; dangling references blocked saves       | Portal APIs on CRM's services with paging and one authorization model                                                                               | **Partial**: sign-up identity matching live (Platform likely-match over two CRM SQL functions); self-service routes missing                                      |
| P11 | Region assignment wrong or too slow                                                          | Synchronous per-save rules; missing fallback crashed                          | Ordered rules with required fallback; durable recalculation                                                                                         | **Verified**                                                                                                                                                     |
| P12 | Directories leak or ignore status                                                            | Template-side filtering; no allowlist                                         | A directory projection that applies opt-out and status before any template                                                                          | **Missing** (not ported)                                                                                                                                         |
| P13 | Activities and tasks second-class                                                            | Late add-on; types not filterable                                             | Activities module with types, links, timelines, datasets                                                                                            | **Verified** backend; console list read-only                                                                                                                     |

Four themes run through them (e06): ids copied with no registry of holders; secondary copies that drifted; separate
rules per channel; failures nobody saw. Principles P-1 to P-6 in [01](#2-01-product-vision-architectural-principles-and-research-method)
answer each theme.

## 4. 03. Rhythm 2.0 CRM Today: Assessment by Functional Area

## 03. Rhythm 2.0 CRM Today: Assessment by Functional Area

Part of [RHYTHM-CRM-ARCH-001](#1-rhythm-crm-arch-001-rhythm-2-0-crm-definitive-architecture-and-completion-plan). Evidence: [e03](#22-e03-rhythm-2-0-crm-backend-assessment-services-crm) (services-crm
`origin/master` `a980b553`, 2026-10-19 13:20 ET; statuses re-baselined at `bb55ae7a` after review), [e04](#23-e04-crm-console-assessment-react-console) (react-console
`1f345862b`, which contains acceptance repair wave 3; no later commit touches a CRM screen as of `f08b10233`),
[e05](#24-e05-platform-capabilities-crm-depends-on-and-every-other-application-s-references-to-crm-records) (Platform and the other applications).

A capability counts as **working** only when the backend behaviour is exercised by an integration or end-to-end test
**and** a person can use it in a live console. A backend with no screen, or a screen over sample data, is not working.

### 1. The verdict in one paragraph

CRM's **backend is substantially built and well tested**: about 190 routes, 249 integration specs, database-enforced
invariants for the hard rules (one primary affiliation, unique primary e-mail, linked contact information, no hierarchy
loops, tenant isolation), an atomic merge engine, duplicate detection, regions, activities, tasks and data jobs. The
**product is not finished**: the live console still shows sample data or does nothing in several places (custom-field
administration, record layouts, global search, organization editing, archive), other applications' references to CRM
records are not kept consistent on merge or delete, writes are not yet protected against lost updates, and the portal has
no CRM pages. Acceptance testing on 2026-10-19 rated CRM "not ready for a customer" (212 of 617 journeys passed, 86 of
262 P0) before repair waves 2 and 3, whose effect is measured only by the release-candidate run planned for Tuesday
2026-10-20 after 07:00 ET.

### 2. By functional area

Ratings: **Solid** (works end to end, gaps are refinements) · **Usable with gaps** (core works, named gaps block some
workflows) · **Backend only** (API works, console does not offer it) · **Not built**.

| Area                                      | Rating                              | What works                                                                                                                                                                      | What does not                                                                                                                                                                                                                                               | Main evidence                                   |
| ----------------------------------------- | ----------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------- |
| Contacts: identity, names, types, numbers | Solid                               | Computed names, types with layouts, numbers, create and edit live                                                                                                               | Live editor reads sample communication types, custom-field defaults and duplicate-e-mail warning; no archive                                                                                                                                                | e03 §3; e04 add. A                              |
| Organizations                             | Usable with gaps                    | Create, types, hierarchy reads, compliance, domain derivation                                                                                                                   | **No general edit form** (Change Type works); Organization 360 live on 4 tabs; no organization history; hierarchy not queryable; no domain lookup; phones lack preferred; no e-mail                                                                         | e03 §4; e04 add. A                              |
| Affiliations                              | Solid (backend), gaps in console    | Dated typed affiliations, one primary in the database, roles on affiliations, linked contact information                                                                        | D-09 (omitting affiliations deletes them, DEF-CRM-0174), D-10/12 date classes, D-46 effective-dated roles                                                                                                                                                   | e03 §5; affiliations README                     |
| Addresses, e-mail, phones                 | Solid                               | Typed rows, preferred, linked/copied/owned, effective views, normalisation                                                                                                      | E-mail types; `disconnected` (in progress)                                                                                                                                                                                                                  | e03 §6                                          |
| Contact roles and compliance              | Usable with gaps                    | Roles, automatic assignment on primary change, compliance with Liquid notice and preview                                                                                        | Reprocess and outcome API-only in console; assignment ignores effective dates and non-primary affiliations                                                                                                                                                  | e03 §7, §4                                      |
| Regions                                   | Solid                               | Rules, preview, recalculation job, linked-address reassignment                                                                                                                  | Region ids not configuration ids; no region custom fields                                                                                                                                                                                                   | e03 §8                                          |
| Custom fields                             | **Backend only** for administration | Values stored, validated and queryable for contacts, organizations, roles, activities                                                                                           | **Console administration is a browser mock in every environment**; rich text is a demo; only API-level requiredness                                                                                                                                         | e03 §9; e04 add. B                              |
| Activities                                | Usable with gaps                    | Types, links, primary link, timelines, logging from a record, history                                                                                                           | List read-only, owner picker capped at 100, cascade on delete (D-44)                                                                                                                                                                                        | e03 §10; e04 add. C                             |
| Tasks                                     | Usable with gaps                    | Views (My Open Tasks default), drawer, complete/reopen/cancel with version                                                                                                      | Claim/release without version; no delete in console; no history; random ids; queue decision                                                                                                                                                                 | e03 §11; e04 add. C                             |
| Duplicate detection                       | Usable with gaps                    | Rules, scoring with explanations, candidates, groups, idempotent dismissals, block rules with override for contacts and organizations (service)                                 | No rule create/delete (service or console); no rescan action; Create Anyway and organization check not in the console                                                                                                                                       | e03 §12; e04 add. C; review r1                  |
| Merge                                     | Usable with gaps                    | Atomic contact and organization merge, plans, preview, aliases, 410 on every route, field-level outcome and approvals bound to child versions in the service (since `bb55ae7a`) | Console: picker unwired, Merge Details and reapproval pending; **four applications not participating**, so such merges should be blocked (CRM-Q3)                                                                                                           | e03 §12; e05 B; review r1 C                     |
| Lifecycle (archive/delete)                | Not built                           | Hard delete with CRM-internal checks                                                                                                                                            | No archive (being built by the acceptance effort); no deletion impact; deleted-record history keeps personal data                                                                                                                                           | e05 A.13; [08](#9-08-record-lifecycle-and-safe-delete) |
| Search and Explorer                       | Usable with gaps                    | Contacts, organizations, tasks, activities lists on Platform queries; saved views; select all; export; quick search                                                             | Global `/search` page is sample data; quick search not proven in dev (DEF-CRM-0119); flat filters only; AI behind a flag; membership/owner columns empty                                                                                                    | e04 add. A, D                                   |
| Saved queries and reports                 | Usable with gaps                    | Library, three start paths, grouped criteria with NOT, schedules, exports                                                                                                       | Bulk actions on results throw; no CRM reports                                                                                                                                                                                                               | e04 add. D                                      |
| 360 pages                                 | Usable with gaps                    | Contact 360 overview live; metadata-driven; activity timeline                                                                                                                   | 15 contact tabs "not available"; Organization 360 bespoke and thin; layouts designer not persisted                                                                                                                                                          | e04 add. A, B                                   |
| Data operations                           | Usable with gaps                    | Imports (contacts, organizations, configuration, activities), bulk update, bulk delete, crosswalk, rollback of contact fields                                                   | Version freeze (D-37/38) reverted; collections and custom fields not reversible; typed collections mapped only on contact insert; no one-row-per-affiliation or task import (affiliations import nested in contacts); CRM's tenant-export provider is built | e03 §13; e04 add. D                             |
| History and audit                         | Usable with gaps                    | Contact and activity field history; merge pointer in Platform audit                                                                                                             | No history for organizations, tasks, configuration                                                                                                                                                                                                          | e03 §14                                         |
| Events and integration                    | Not built (mostly)                  | `crm.contact.merged` business event; change capture inside CRM                                                                                                                  | Created/updated/deleted and organization-merged events unpublished; one service function (`tasks`); Membership handler unwired                                                                                                                              | e03 §14; e05 B                                  |
| Security                                  | Usable with gaps                    | Tenant RLS; permission catalog by tier; application access                                                                                                                      | No record-level or field-level rules; no built-in policies; locks cannot be undone (D-08)                                                                                                                                                                   | e03 §15                                         |
| Portal                                    | Not built                           | Sign-up matching functions                                                                                                                                                      | No profile, roster or organization pages; no portal routes beyond role reads                                                                                                                                                                                | e03 §17; e04 add. D                             |
| Relationships, directories, engagement    | Not built                           | —                                                                                                                                                                               | Not ported from 1.0                                                                                                                                                                                                                                         | e03 §1                                          |

### 3. Classification required by the brief

| Question                                      | Answer (with the gap-matrix rows that prove it)                                                                                                                                                                                                                                                                                                               |
| --------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Implemented and working                       | Contact identity and names, affiliations model, contact information, regions, compliance calculation, merge engine within CRM, duplicate scoring, activity logging from a record, task views, imports, tenant isolation (rows CO-01..03, AF-01..06, CI-01..04, RG-01, CM-01, MG-01..03, DQ-01..02, AC-01, AC-07, TK-01, DO-01..03, DO-12, SE-01, SR-01, X-10) |
| Implemented but incomplete                    | Role assignment, data-job rollback, Quick Search, history, task concurrency, organization 360, explorer AI, other applications' cards (CM-04, DO-05..06, SR-02, HI-01, TK-03, UI-06..08, SR-05, SR-07)                                                                                                                                                        |
| Scaffolding only                              | Business events (declared, one published), CRM extension points (EV-01, UI-05)                                                                                                                                                                                                                                                                                |
| API but no usable interface                   | Organization edit, compliance reprocess and outcome, duplicate rule create/delete, rescan, task delete (OR-02, CM-03, DQ-04, DQ-05, TK-04)                                                                                                                                                                                                                    |
| Interface but incomplete or simulated backend | Custom-field administration, record layouts list and designer, engagement scoring and CRM processes screens, global search page, contact editor reference data, saved-query bulk actions, empty list columns (CF-01, UI-02..04, UI-09, UI-10, SR-04, SR-06)                                                                                                   |
| Built but missing edge cases                  | Omitted collections and properties, stale writes, lost responses, task transitions, effective-dated roles, merge drift (WR-01..04, WR-06, AF-06, MG-06)                                                                                                                                                                                                       |
| Designed but not built                        | Archive, deletion impact, disconnected addresses, write contracts (LC-01, LC-03..05, CI-05, WR-01..02)                                                                                                                                                                                                                                                        |
| Planned but never designed sufficiently       | Relationships and households, portal self-service, record-level security, directories, engagement (RL-01..02, PO-01..04, SE-03, L1-02, L1-03)                                                                                                                                                                                                                 |
| 1.0 functionality lost                        | Generic relationships, directories, engagement scoring, portal self-service and role-permission matrix, organization domain lookup, staff impersonation, enrichment, marketing e-mail on the timeline (L1-01..04, L1-06, L1-09, L1-12, L1-13, L1-19, L1-20)                                                                                                   |
| Decisions to reconsider                       | Task queues (CRM-Q1), merge block for non-participating applications (CRM-Q3), e-mail types (CRM-Q6), single-holder roles (CRM-Q7), inapplicable roles (CRM-Q9), hard delete until safe delete (CRM-Q10); see [13](#14-13-decisions-standing-recorded-and-needing-andrew-deliverable-d)                                                                                                                           |

## 5. 04. Domain and Relational Data Model

## 04. Domain and Relational Data Model

Part of [RHYTHM-CRM-ARCH-001](#1-rhythm-crm-arch-001-rhythm-2-0-crm-definitive-architecture-and-completion-plan). Evidence: [e03](#22-e03-rhythm-2-0-crm-backend-assessment-services-crm) §2–§12. Paths are
relative to services-crm at `origin/master` `a980b553` unless prefixed.

### 1. Conventions every CRM table follows

These are **Verified** as in force today and remain the standard (e03 §2):

- Every table carries `tenant_id`; primary keys are `(tenant_id, id)`; every reference is a composite foreign key on
  `(tenant_id, <ref>_id)`. The tenant comes from the path and the execution context, never from a request body.
- Forced, fail-closed row-level security on `app.tenant_id`; every transaction runs as `rhythm_crm_app`
  (`docs/row-level-security.md`).
- snake_case tables, columns, wire properties and paths; camelCase code; `sys_` system columns
  (`sys_version`, `sys_created_at/_by_id`, `sys_last_modified_at/_by_id`, `sys_locked`, `sys_external_id`); constraints
  named `<table>_<what>_<kind>` (`docs/naming-conventions.md`).
- A text-normalization trigger on every table (trim; empty string becomes NULL).
- A record and its owned collections are written in one transaction (`lib/db/owned-collections.ts`).
- **Ids** (Decided, Andrew 2026-10-19, product-wide): configuration tables are keyed by immutable configuration-id strings
  with a `<table>_id_configuration_id_check`; record tables are keyed by UUIDv7 with a database `uuidV7Check`, assigned by
  the client where it can (retry safety, D-43). **Gap:** tasks and regions use random 10-character ids and no record table
  carries the UUIDv7 check yet ([14](#15-14-implementation-gap-matrix-deliverable-b) row X-03).

### 2. The model at a glance

```mermaid
erDiagram
    CONTACT ||--o{ CONTACT_EMAIL_ADDRESS : owns
    CONTACT ||--o{ CONTACT_ADDRESS : owns
    CONTACT ||--o{ CONTACT_PHONE_NUMBER : owns
    CONTACT ||--o{ CONTACT_COMMUNICATION_PREFERENCE : owns
    CONTACT ||--o{ CONTACT_CUSTOM_FIELD_VALUE : owns
    CONTACT ||--o{ AFFILIATION : "has (dated)"
    ORGANIZATION ||--o{ AFFILIATION : "is party to"
    AFFILIATION ||--o{ AFFILIATION_ROLE : "holds (dated)"
    CONTACT_ROLE ||--o{ AFFILIATION_ROLE : "is held as"
    AFFILIATION_TYPE ||--o{ AFFILIATION : types
    CONTACT_ADDRESS }o--o| ORGANIZATION_ADDRESS : "may link to (via affiliation)"
    CONTACT_PHONE_NUMBER }o--o| ORGANIZATION_PHONE_NUMBER : "may link to (via affiliation)"
    ORGANIZATION ||--o{ ORGANIZATION_ADDRESS : owns
    ORGANIZATION ||--o{ ORGANIZATION_PHONE_NUMBER : owns
    ORGANIZATION ||--o{ ORGANIZATION_CUSTOM_FIELD_VALUE : owns
    ORGANIZATION }o--o| ORGANIZATION : "parent of"
    ORGANIZATION_TYPE ||--o{ ORGANIZATION : types
    ORGANIZATION_TYPE ||--o{ COMPLIANCE_RULE : defines
    CONTACT_TYPE ||--o{ CONTACT : types
    CONTACT }o--o| CONTACT : "spouse (interim)"
    REGION ||--o{ CONTACT : "assigned to"
    REGION ||--o{ ORGANIZATION : "assigned to"
    ACTIVITY ||--o{ ACTIVITY_LINK : "links to"
    ACTIVITY_LINK }o--o| CONTACT : names
    ACTIVITY_LINK }o--o| ORGANIZATION : names
    ACTIVITY_TYPE ||--o{ ACTIVITY : types
    TASK }o--o| CONTACT : about
    TASK }o--o| ORGANIZATION : about
    TASK_TYPE ||--o{ TASK : types
    TASK_QUEUE |o--o{ TASK : "pools (decision CRM-Q1)"
    DUPLICATE_CANDIDATE }o--|| CONTACT : pairs
    MERGE ||--o{ MERGED_RECORD_ALIAS : leaves
```

### 3. Entities

Each row: what it is, the key business columns, the invariants the **database** enforces, and the status.

#### 3.1 Records

| Entity (table)                                         | What it is                                                                                                                                                       | Key columns                                                                                                                                                                                                                                                                                           | Database invariants                                                                                                                                                                                                             | Status                                                                              |
| ------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------- |
| Contact (`crm.contacts`)                               | A person                                                                                                                                                         | `number` (per-tenant sequence), name parts, derived `name`, `full_name`, `name_last_first`, `type_id`, `status`, `spouse_id`, `region_id`, `override_automatic_region_assignment`, gender, pronouns, birth and death dates, socials, `do_not_mail/_call/email_opt_out`, districts                     | `contacts_number_uq`; first or last name required (added `NOT VALID`); `contacts_status_check` closed list; type is a composite FK with no delete action                                                                        | Verified                                                                            |
| Organization (`crm.organizations`)                     | A company, school, chapter-like body or any non-person party                                                                                                     | `number`, `name`, `acronym`, `type_id`, `status`, `parent_organization_id`, website and domain, alternate domains, `tax_exempt`, logo, socials, `region_id`                                                                                                                                           | number unique; parent is a FK; **parent may not be a descendant** (migration `0018`); status closed list                                                                                                                        | Verified                                                                            |
| Affiliation (`crm.contact_organization_affiliations`)  | A dated relationship of a contact to an organization: employer, school, other types                                                                              | `contact_id`, `organization_id` (immutable), `affiliation_type_id`, `is_primary`, `start_date`, `end_date`, `job_title`                                                                                                                                                                               | **at most one primary per contact** (`…_one_primary_uidx`); a primary has no end date; start ≤ end; no overlapping periods for the same organization and type (exclusion constraint, migration `0067`); at most 100 per contact | Verified                                                                            |
| Affiliation role (`crm.contact_organization_roles`)    | A contact role held within an affiliation, dated                                                                                                                 | `affiliation_id`, `contact_role_id`, `start_date`, `end_date`                                                                                                                                                                                                                                         | at most 25 per affiliation; deleting a held role refused (FK)                                                                                                                                                                   | Verified                                                                            |
| Contact e-mail address                                 | Up to three ordered e-mail addresses                                                                                                                             | `position` 1..3, `email_address` (`crm.email_address` domain, lowercase)                                                                                                                                                                                                                              | **the primary (position 1) address is unique in the tenant** (`contact_email_addresses_primary_email_address_uidx`)                                                                                                             | Verified; no e-mail type ([CRM-Q6](#14-13-decisions-standing-recorded-and-needing-andrew-deliverable-d))                                |
| Contact address / phone number                         | One row per contact per configurable type                                                                                                                        | `type_id`, `is_preferred`, value columns, seasonal boundaries (addresses; recurring month/day per Ruled E-06, today full dates), `source_mode` (`owned`, `copied`, `linked`), link columns (`source_affiliation_id`, `source_organization_id`, `source_organization_address_id` / `_phone_number_id`) | one row per type; at most one preferred; a seasonal address is never preferred; linked rows have NULL values and FK-checked sources that belong to the contact's affiliation                                                    | Verified; `disconnected` mode (D-25) designed, being built by the acceptance effort |
| Organization address / phone number                    | One row per organization per configurable type                                                                                                                   | `type_id`, `is_preferred` (addresses only), values                                                                                                                                                                                                                                                    | one row per type                                                                                                                                                                                                                | Verified; **phones lack a preferred flag**; no organization e-mail                  |
| Communication preference                               | Per contact, opt in/out per communication type                                                                                                                   | `communication_type_id` (free text today), `enabled`, subscription fields                                                                                                                                                                                                                             | none on the type                                                                                                                                                                                                                | Partial: types are not a configuration table                                        |
| Custom field values                                    | Values for Platform-defined custom fields                                                                                                                        | per-record value tables built by Platform's factory                                                                                                                                                                                                                                                   | value typing per definition; string index `(tenant_id, custom_field_id)`                                                                                                                                                        | Verified for contacts, organizations, contact roles, activities                     |
| Activity (`crm.activities`)                            | A record of an interaction or note                                                                                                                               | `activity_type_id`, `subject`, sanitized HTML `body`, `status`, `direction`, `occurred_at`, `duration`, `outcome`, `owner_id` (optional)                                                                                                                                                              | type FK; body sanitised                                                                                                                                                                                                         | Verified                                                                            |
| Activity link (`crm.activity_links`)                   | Which contacts and organizations an activity is about; one is primary                                                                                            | `activity_id`, `contact_id` or `organization_id`, `is_primary`                                                                                                                                                                                                                                        | **contact and organization FKs are `ON DELETE CASCADE`** today: deleting a contact silently removes its links (D-44 changes this in CRM-M20; [08](#9-08-record-lifecycle-and-safe-delete))                                             | Verified; cascade is a known gap                                                    |
| Task (`crm.tasks`)                                     | A unit of staff work                                                                                                                                             | `task_type_id`, `subject`, `status` (open, waiting_for_someone_else, deferred — open; completed, cancelled — closed, reached only through action routes), `due_on`, `assignee_user_id`, `queue_id`, `contact_id`, `organization_id`, generic subject (registered record), action link                 | queue FK `ON DELETE RESTRICT`; contact and organization `SET NULL` on delete                                                                                                                                                    | Verified; ids not UUIDv7; no history                                                |
| Duplicate candidate / group / dismissal                | A scored, explained pair; groups of pairs; staff dismissals                                                                                                      | scores, bands, evidence                                                                                                                                                                                                                                                                               | per-pair dismissal                                                                                                                                                                                                              | Verified                                                                            |
| Merge, merge plan, alias (`crm.merged_record_aliases`) | The merge record (snapshots of every participant's original values, kept for the life of the tenant, D4); plans; the alias from a merged-away id to the survivor | —                                                                                                                                                                                                                                                                                                     | aliases unique per merged id                                                                                                                                                                                                    | Verified                                                                            |
| Record revision (`crm.record_revisions`)               | Field-level change history with actor snapshot and source                                                                                                        | —                                                                                                                                                                                                                                                                                                     | append-only                                                                                                                                                                                                                     | Verified for contacts and activities only                                           |
| Data job tables, id mappings                           | Shared Data Job Engine tables in CRM's schema; crosswalk from source keys to 2.0 ids                                                                             | —                                                                                                                                                                                                                                                                                                     | —                                                                                                                                                                                                                               | Verified                                                                            |

#### 3.2 Configuration (keyed by configuration-id strings)

| Table                                       | Governs                                                                                                                                                              | Status                                                           |
| ------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------- |
| `contact_types`                             | Contact classification; per-type console/portal layouts and forms                                                                                                    | Verified                                                         |
| `organization_types`                        | Organization classification; applicable contact roles; compliance rules; the compliance notification template (owned by the type, stored by Platform communications) | Verified                                                         |
| `affiliation_types`                         | Kind of affiliation (Employer, School, …); default contact-information links (`affiliation_type_contact_information_defaults`)                                       | Verified                                                         |
| `contact_roles`                             | Roles a contact holds within an organization; administrative flag; automatic assignment flags                                                                        | Verified                                                         |
| `address_types`, `phone_number_types`       | Purposes of addresses and phones; `applies_to` contacts / organizations / both; display order                                                                        | Verified                                                         |
| `activity_types` (+ outcomes)               | Seven system types plus tenant types; field modes                                                                                                                    | Verified                                                         |
| `task_types`, `task_queues`                 | Task kinds; work pools                                                                                                                                               | Verified; queue future is [CRM-Q1](#14-13-decisions-standing-recorded-and-needing-andrew-deliverable-d)              |
| `duplicate_rules`                           | Matching rules, action `warn`, `block` or review                                                                                                                     | Verified (no create/delete in console)                           |
| `regions` (+ assignment settings and rules) | Geographic assignment                                                                                                                                                | Verified; **ids are random record-style ids**, which breaks P-11 |
| `crm_settings`                              | Tenant CRM settings                                                                                                                                                  | Verified                                                         |
| _communication types_                       | Kinds of communication a contact can opt in or out of                                                                                                                | **Missing** as a table (free text)                               |
| _e-mail address types_                      | Purposes of e-mail addresses                                                                                                                                         | **Missing** ([CRM-Q6](#14-13-decisions-standing-recorded-and-needing-andrew-deliverable-d))                          |
| _relationship types_                        | Typed contact↔contact and organization↔organization relationships                                                                                                    | **Missing** ([CRM-Q2](#14-13-decisions-standing-recorded-and-needing-andrew-deliverable-d))                          |

#### 3.3 Lifecycle columns (being added)

Per the record-lifecycle design (phase 1, being built by the acceptance effort, [08](#9-08-record-lifecycle-and-safe-delete)):
`archived_at`, `archived_by_id`, `archive_reason` on contacts and organizations, set together (CHECK). Archive is **not** a
status value: a deceased contact can also be archived (lifecycle design R-1).

### 4. Invariants that span tables

These are enforced by the domain services inside one transaction, and tested at the integration level.

| #    | Invariant                                                                                                                                                                                                 | Enforced by                                                                                                                                                                                                                             |
| ---- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| I-1  | A contact has at most one current primary affiliation; making another affiliation primary is one aggregate save that moves the flag atomically                                                            | unique index + contact aggregate (Verified)                                                                                                                                                                                             |
| I-2  | Omitting `affiliations` (or any owned collection) from a save leaves it unchanged; removing or ending an affiliation is an explicit operation                                                             | **Ruled D-09; not built** (today a PUT that omits a collection empties it, DEF-CRM-0174)                                                                                                                                                |
| I-3  | A linked contact address or phone always names an affiliation of the same contact to the same organization, and that organization's record                                                                | composite FKs (Verified)                                                                                                                                                                                                                |
| I-4  | When a linked source record is removed, the contact keeps the last value, marked disconnected and needing verification, never shown as live                                                               | **Ruled D-25**; today becomes `copied`                                                                                                                                                                                                  |
| I-5  | Contact roles held reflect effective affiliations: a future affiliation grants no role early; an ended one stops its roles at its end date                                                                | **Ruled D-46**; automatic assignment today runs on primary change only                                                                                                                                                                  |
| I-6  | A merge leaves zero references to a merged-away id in every participating schema, and an alias for every merged-away id                                                                                   | merge engine + coverage test (Verified for CRM, Platform, Committees)                                                                                                                                                                   |
| I-7  | A record in a pending merge or deletion refuses other writes (`RecordBusy` / `RecordPendingDeletion`)                                                                                                     | merge: Verified; deletion: designed                                                                                                                                                                                                     |
| I-8  | Field-changing writes carry the current `sys_version`; missing → 400, stale → 409; a no-op changes no version, timestamp or revision                                                                      | **Ruled D-05**; standard on branch `agent/crm-acc-w3-arch-writes`; CRM enforcement held until console callers send versions. A write whose result equals the stored state answers 200 unchanged even with a stale version (standard W2) |
| I-8a | An omitted property means unchanged; explicit null means clear where permitted; full replacement is a separate operation with a complete validated payload. Applies to every record, not only collections | **Ruled E-09** (Rhythm-wide; the write-contracts standard carries it)                                                                                                                                                                   |
| I-9  | An organization any affiliation names (current, past or future) cannot be hard-deleted; archive is the path                                                                                               | composite FK without delete action (Verified, migration `0065`); lifecycle R-7 keeps it and turns the refusal into a readable blocker                                                                                                   |
| I-10 | No free-text organization name exists on a contact; every organization shown is a linked record                                                                                                           | Verified (2.0 removed it)                                                                                                                                                                                                               |

### 5. Limits

Every non-aggregate list has a limit (Platform standard). CRM's are: 100 affiliations per contact, 25 roles per
affiliation, 3 e-mail addresses, one address and one phone per type, 100,000 employees in an organization merge (D7), 2–10
records per merge (console fallback), 500 subjects per deletion request (record-lifecycle design), 10,000 ids in a
selection handed to a data job. Console caps that are **not** acceptable at enterprise scale and must be removed:
organization pickers holding at most 2,000 records in the browser, organization hierarchy and affiliation scans capped at
500, and an activity owner list capped at 100 users ([14](#15-14-implementation-gap-matrix-deliverable-b) rows UI-14, UI-15, AC-06).

## 6. 05. Functional Architecture: What Every CRM Capability Does

## 05. Functional Architecture: What Every CRM Capability Does

Part of [RHYTHM-CRM-ARCH-001](#1-rhythm-crm-arch-001-rhythm-2-0-crm-definitive-architecture-and-completion-plan). This chapter specifies target behaviour. Each section ends with a short
**State** line; the authoritative status per capability is the gap matrix in [14](#15-14-implementation-gap-matrix-deliverable-b). Duplicates and merge
are in [07](#8-07-duplicate-detection-and-record-merging); lifecycle and deletion in [08](#9-08-record-lifecycle-and-safe-delete);
search, Explorer and the 360 pages in [09](#10-09-console-experience-search-explorer-queries-and-the-360-pages).

### 1. Domains and who owns what inside CRM

| Domain           | CRM module (services-crm)                         | Owns                                                                                                                  |
| ---------------- | ------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------- |
| Records          | `entities`                                        | contacts, organizations, their contact information, affiliations and roles, custom-field values, compliance, settings |
| Configuration    | `entities` services, `configuration-types` routes | contact, organization, affiliation, address and phone types; contact roles                                            |
| Regions          | `regions`                                         | regions, assignment rules, recalculation                                                                              |
| Activities       | `activities`                                      | activities, links, types, timelines                                                                                   |
| Tasks            | `tasks`                                           | tasks, task types, task queues, the `tasks` service function                                                          |
| Duplicates       | `duplicates`                                      | detection, rules, candidates, groups, dismissals, synchronous checks, portal sign-up matching                         |
| Merges           | `merges`                                          | plans, execution, aliases, merge history                                                                              |
| Data jobs        | `data-jobs`                                       | CRM's operations in the shared Data Job Engine                                                                        |
| Tenant lifecycle | `tenant-lifecycle`                                | provisioning seeds, reset, purge, export provider                                                                     |

### 2. Contacts, organizations and their information

#### 2.1 Contacts

**Identity.** A contact is a person, identified by a UUIDv7 `id` (assigned by the client when it can, so a retried
create is recognised, D-43) and a per-tenant `number` allocated by CRM. An imported record's own number is never trusted
as the 2.0 number; its source key is kept in `sys_external_id` and the data-job crosswalk (Verified, DEF-CRM-0643).

**Names (Decided, 2026-10-09, validated here).** A contact's name is stored only as parts: prefix, first, middle, last,
suffix, credentials, nickname, maiden name, salutation. `name`, `full_name` and `name_last_first` are **computed** in the
database and a client may not send them. No name parsing happens on write (removed on purpose, 2026-10-09). A first or
last name is required. This direction is correct: 1.0 also computed the display names and overwrote client input
(H-1), and computing them avoids the class of defect where a display name disagrees with its parts. Two refinements are
**Recommended**:

- The check "first or last name required" was added `NOT VALID`; a one-off remediation report should list existing rows
  that violate it, and the constraint should then be validated.
- Import mapping may accept a single "full name" column and split it **in the import mapper only** (a preview the user
  confirms), never in the domain service. This keeps P-1 (one rule) while serving spreadsheets that hold one name column.

**Status.** `active`, `inactive`, `deceased`, `duplicate` is an operational fact a user edits (Deactivate in the
lifecycle vocabulary). Rules (Recommended, consistent with 1.0 H-2):

- Only `active` contacts are offered in new-choice pickers and Quick Search by default (`status-visibility.md` §4.1,
  Verified).
- A non-active contact cannot be newly linked to a portal login: CRM publishes discoverability (status and archive)
  through a published read (`crm.party_status`, CRM-ADR-05) and Platform's login linking refuses a non-discoverable
  contact using it (1.0 H-2).
- `duplicate` is never set by detection or merge (merging decisions, "Settled by inspection": contact status `duplicate`
  is left as is; detection does not set it; merged records are absorbed and answer 410). It remains available for tenants that flag a record manually while they work; it may be retired once
  merge adoption is complete.
- `deceased` is shown prominently in the 360 header (Ruled D-01). Sending must withhold deceased, `email_opt_out` and
  opted-out-of-type recipients; **today Platform communications suppresses only bounces and complaints**, so this is a gap
  (CI-09, CRM-M13).

**Types.** Contact types classify contacts (Member, Staff, Student...) and choose the console and portal layout and form
for that type. They are configuration (string ids), ordered, deactivatable, and referenced types cannot be deleted
(Verified).

**Other personal data.** Gender, pronouns, birth and death dates, social links, do-not-mail/call/e-mail flags and
communication preferences. Districts are stored but filled by no enrichment (Google Civic and FullContact enrichment are
not ported; [14](#15-14-implementation-gap-matrix-deliverable-b) row L1-12).

**Spouse (interim, Ruled D-19).** `spouse_id` remains until relationships exist ([§5](#5-relationships-and-households)), and
must be reciprocal: linking or unlinking either spouse updates both in one transaction.

**State.** Verified backend (e03 §3). Gaps: the live editor still reads sample reference data; no archive; delete
semantics (see [08](#9-08-record-lifecycle-and-safe-delete)).

#### 2.2 Organizations

**Identity and fields.** UUIDv7 id, number, name, acronym, type, status (`active`, `inactive`, `duplicate`), website and
domain (each derived from the other; legacy oddities recorded as debt), alternate domains, tax-exempt, logo, social links,
region, custom fields, parent organization.

**Contact channels (Ruled D-36).** Organizations have typed addresses (with a preferred flag) and typed phone numbers.
**Recommended additions:** a preferred flag on organization phone numbers (parity with addresses; the linked-information
model already assumes one), and organization e-mail addresses (a typed collection like contacts', for general inboxes
such as `info@`; 1.0 had none, so this is new and low-risk). The console must read and edit them with the same editors as
contacts (D-36).

**Hierarchy.** An organization may have a parent; a parent may never be a descendant (database rule, Verified). The
hierarchy is one tree per organization (no multiple parents). Descendant contacts can be listed (Verified, staff only).
**Required (Recommended here):** the hierarchy must be queryable. `organizations_for_queries` gains ancestor and
descendant relations (a maintained closure table or a recursive view, decided by measurement), so a saved query,
eligibility rule or export can say "this organization and everything under it". 1.0 kept ancestor arrays that went stale
when a parent moved; 2.0 must not store them without maintaining them in the same transaction as the move.

**Domain lookup (Recommended).** A tenant-scoped lookup of organizations by e-mail domain (primary and alternate
domains), excluding public e-mail domains, used by portal sign-up matching, the contact editor's suggestion and
integrations. 1.0 had it; 2.0 does not expose it ([14](#15-14-implementation-gap-matrix-deliverable-b) row OR-05). Auto-linking on a domain match is
allowed only when exactly one active organization matches; two or more is reported, never guessed (1.0 pattern P5e,f).

**State.** Verified backend. Gaps: no organization edit form in the console; Organization 360 thin; no history for
organizations; hierarchy not queryable; no domain lookup.

#### 2.3 Affiliations

_Decided 2026-10-13/14; adopted, amended by Ruled D-09, D-10, D-12, D-25 and D-46 (conflicts C-6, C-7 in [13 §5](#14-13-decisions-standing-recorded-and-needing-andrew-deliverable-d))._

The binding design is `services-crm:docs/affiliations/README.md` (§0, §0b, §0c binding). Summary:

- An affiliation is a first-class, dated record linking one contact to one organization, with a type (Employer, School,
  any tenant type), a job title, and roles held within it. A contact may hold many simultaneous and historical
  affiliations, including several with the same organization under different types.
- **Primary.** `is_primary` lives on the affiliation; at most one per contact, enforced by a unique index; a primary
  affiliation has no end date. Making another affiliation primary is one aggregate save; the previous primary stays
  current unless the user ends it. The contact stores no organization at all.
- **Organization is immutable on an affiliation.** Changing employer is ending one affiliation and adding another, which
  keeps history true and makes linked contact information safe.
- **History** is the affiliations that have ended; there is no separate history table, tab or editor. One Organization
  Affiliations section shows every affiliation by default, each labelled Upcoming, Current or Past from the service's
  `effective` value (workspace business date, Ruled D-10, D-12); the All / Current / Upcoming / Past filter and its counts
  read the same value.
- **Derived relations in queries.** "Primary organization", "employer", "school" and "organization by affiliation type"
  are generated query-layer properties over affiliations (affiliations README §7), never stored organization-id columns on
  contacts. Collection filters ("has any current affiliation with an organization of type X") are offered alongside.
- **Omitted means unchanged (Ruled D-09, E-09).** A contact save that does not include affiliations leaves them all as they
  are. Adding, editing, ending and removing an affiliation are explicit routes that execute inside the contact aggregate
  (contact row lock and version), with their own validation, audit and, for end and remove, their own permission keys
  (conflict C-6 resolved in favour of D-09 and the write-contracts standard W3). Removing (as opposed to ending) an affiliation is for mistakes only and is refused if contact information is
  linked through it, unless the same save re-points, copies or removes those rows.
- **Effects of change.** Ending an affiliation ends its roles, ends contact-information links through it (the link stays
  inspectable and resolves to nothing for current queries), and triggers compliance recalculation for the organization.
  Starting one in the future grants nothing until its start date (Ruled D-46).

**Integration with other applications.** Membership reads affiliations and roles directly for organization flow-down
memberships (`membership:modules/memberships/services/persistence/crm-people.ts`); Commerce reads them for bill-to and
pricing; Committees and Events use roles. They should read through published views or service functions rather than physical
tables (CRM-ADR-05; [12 §4](#13-12-cross-application-integration-contracts)), and must be told when an
affiliation changes through CRM's change events (Membership's handler exists but is not invoked;
[14](#15-14-implementation-gap-matrix-deliverable-b) row X-07).

**State.** Verified backend, accepted 2026-10-14. Gaps: D-09 (omitted collections), D-46 (effective-dated roles),
D-10/D-12 date classification in the console.

#### 2.4 Addresses, e-mail addresses and phone numbers

_Decided 2026-10-13/14; adopted, amended by Ruled D-25, E-06 and E-15._

- **Seasonal addresses (Ruled E-06).** Seasonal boundaries are stored as recurring month/day values, not calendar
  dates, and a season may cross December/January. The rules to specify with the implementation: a season includes its
  start day and its end day in the workspace calendar; during its season a seasonal address may be used for mail but is
  never the preferred address (unchanged rule); two seasonal addresses of one contact may not overlap.
- **Typed values.** Addresses and phones are one row per configurable type (the type is the purpose: Home, Work,
  Billing, Seasonal...), with at most one preferred per contact. Types are configuration with `applies_to` (contacts,
  organizations, both), a display order, and an atomic reorder. A seasonal address is never preferred.
- **E-mail addresses.** Up to three ordered addresses; the first is primary and unique in the tenant (duplicate e-mail
  prevention, Verified; the 1.0 tenant switch was dropped on purpose). **Open (CRM-Q6):** whether e-mail addresses get
  configurable types (Work, Personal) like phones and addresses. D-40 already requires a mapper for "work e-mail" and
  "personal e-mail" columns; that needs a type to land on.
- **Source modes.** Each contact address or phone row is `owned` (its own value), `copied` (own value, with provenance of
  where it was copied from), or `linked` (no value; it names an affiliation and that organization's record, and is
  resolved at read time in every channel). The query layer resolves links in the effective views
  (`contact_addresses_effective`, `contact_phone_numbers_effective`); organization values are never copied to make
  queries work (Andrew, 2026-10-13).
- **When a source goes away (Ruled D-25).** If the organization record a contact row is linked to is removed, the contact
  keeps the last known value with its provenance and an explicit `disconnected`, needs-verification state, offered three
  actions (verify as own, relink, remove). It is never shown as a live link. This is built in the acceptance effort's
  archive phase (record-lifecycle 05, WP-C5).
- **The tenant's default affiliation type** (used when an affiliation is created without one) is tenant-wide
  configuration; changing it requires the administrator permission (Ruled E-03).
- **Affiliation-type defaults.** Each affiliation type may map organization source types to contact destination types,
  applied when an affiliation is created or when a user chooses "Re-apply defaults"; existing rows are never overwritten.
- **Validation and normalisation.** E-mail format validated and lowercased; country and state normalised; US phone numbers
  formatted. **One validator** serves console, portal, import and API (1.0 pattern P6c). Address verification (a postal
  service) is out of scope for this architecture and can be added behind the same validator.
- **Merge.** Collections are combined per type with staff choices (`services-crm:docs/merging/conflict-resolution.md`);
  most-restrictive communication consent wins (D6).

**State.** Verified backend. Gaps: e-mail types; organization phone preferred flag; organization e-mail; `disconnected`
mode (in progress).

#### 2.5 Contact roles and compliance

- **Contact roles** are configuration: name, description (Ruled D-11), administrative flag, `assign_automatically`,
  `assign_automatically_to_first_contact`, active flag, custom fields. A role is held on an affiliation, with dates.
- **Which roles apply.** An organization type lists its applicable contact roles. Today (affiliations README §5.6) this
  filters the role picker in every channel and never refuses a save. Whether CRM should refuse an inapplicable role on
  save in every channel (1.0 RB-5829, pattern P5c) is **Open (CRM-Q9)**.
- **Automatic assignment (Ruled D-46).** Roles follow _effective_ affiliations: a role flagged automatic is granted when an
  affiliation becomes current (its start date arrives, or it is created current), not early; it ends with the affiliation.
  Since `d875cf22` a future-dated affiliation is marked due and receives its automatic roles on its start day, through the
  save or the five-minute reconciler (affiliations README §5.6a). **Recommended:** imports trigger the same assignment (1.0 skipped imports,
  H-3).
- **Compliance.** An organization type may require a minimum and maximum number of holders per role among the
  organization's current affiliations. CRM recalculates on every affiliation and role change and on type change, stores
  violations, flags the organization, and on the transition into non-compliance hands a notice (Liquid template owned by
  the type, rendered by Platform communications) to the configured recipients. A tenant-wide reprocess exists for rule
  changes. **Changing an organization's type recalculates compliance immediately** and reports the new status; a status
  calculated under the previous type is never shown as current (Ruled E-01, DEF-CRM-0111). **A minimum-only rule of zero
  is discarded, not persisted**, and the organization type still saves, including when every imported rule is
  meaningless (Ruled E-04, DEF-CRM-0324).
- **Deleting a contact role** that organization-type compliance rules or notification recipients reference is refused; the
  refusal lists the dependent configuration and offers a way to resolve it; deactivating the role remains available
  (Ruled E-02).
- **Tenant defaults are not protected records.** Default contact roles and other seeded defaults are ordinary
  configuration a tenant may change or remove, subject to referential integrity; system protection is reserved for records
  a platform invariant requires (Ruled E-13).
- **Single-holder roles (Recommended, new).** 1.0's worst role failure was 828 organizations with several Owners
  (RB-7662), because single-holder rules were advisory. Compliance stays advisory by design (it warns and notifies), and a
  role may additionally be marked **exclusive** (at most one current holder per organization), enforced by a partial
  unique index on current affiliation roles. Assigning a second holder then requires ending the first in the same save.
  This is a product change and is listed as [CRM-Q7](#14-13-decisions-standing-recorded-and-needing-andrew-deliverable-d).

**State.** Verified backend for roles, assignment and compliance; console has rules and preview, not reprocess or outcome.

#### 2.6 Regions

Regions are named configuration. Assignment settings choose which records are assigned (contacts, organizations, both)
and which contact address type decides; ordered rules (postal-code ranges, states, countries) with a required fallback
assign a region whenever the deciding address changes, including linked addresses. A per-record override stops
automation. Full recalculation runs as a resumable background job with progress (Verified; 1.0 pattern P11 answered).
**Gaps:** region ids are random record-style ids (must become configuration ids, P-11); region custom fields are not
carried (low priority).

#### 2.7 Custom fields

Definitions live in Platform metadata (one definition store for every application); values live in CRM's value tables
for contacts, organizations, contact roles and activities. Requirements:

- Types: text, long text, rich text (HTML, sanitised; Ruled D-04 keeps it), e-mail, phone, URL, yes/no, picklist,
  multi-select, number, percent, currency, date, date-time, image, file, reference, table, rating. Values are validated
  against the tenant's definition on every channel.
- Custom fields defined for a **configuration** entity (contact types, roles, organization types and the rest) are
  rendered, validated and saved by the shared metadata-driven configuration form system, with no per-entity screen code
  (Ruled E-05).
- Requiredness applies per surface (`api`, `console`, `portal`); today only `api` is enforced ([14](#15-14-implementation-gap-matrix-deliverable-b)
  row CF-03).
- Visibility and editability per role are a Platform capability (field-level permissions) that CRM consumes; not built.
- Query support through the value tables registered in datasets; users never see storage structures (semantic layer
  direction). Decimal values must be returned as numbers (DEF-CRM-0029).
- Imports map custom fields from metadata; merges choose values per field; rollback must cover custom-field changes
  ([10](#11-10-data-operations-imports-upserts-exports-bulk-changes-and-rollback)).
- **The console custom-field administration must use Platform's metadata API.** Today it is a browser-memory mock in every
  environment ([14](#15-14-implementation-gap-matrix-deliverable-b) row CF-01, critical).

### 3. Activities

Activities are a full capability, not a notes table (1.0 pattern P13).

- **What an activity is.** A record of an interaction or a note: type (seven system types — for example call, meeting,
  e-mail, note — plus tenant types with per-type field modes and outcomes), subject, sanitised rich-text body, status
  (completed or scheduled), direction, occurred-at, duration, outcome, owner (optional; Ruled D-51: created-by,
  performed-by and owner are distinct), custom fields and attachments.
- **Links.** An activity links to one or more contacts and organizations; one link is primary (Ruled D-50: the launch
  context is only the default, Make Primary is available). Shared activities appear on every linked record's timeline.
- **Timelines.** Contact and organization timelines with type and status filters, counts, paging and "last activity"
  derived on read (Verified). Other applications' activity (registrations, CE, accreditation) is **not** copied into CRM;
  it appears on the 360 through those applications' cards. E-mail engagement from marketing integrations
  (Campaign Monitor, Mailchimp, ActiveCampaign in 1.0) arrives through the Integration Platform as activities of a
  system type, written through CRM's activity service function (Recommended; see [12](#13-12-cross-application-integration-contracts)).
- **Querying (Ruled D-49).** An Activities dataset and recipe with an Explorer list (log, edit, delete from the list,
  bulk actions) and Version History in the activity sheet, using shared audit views.
- **Integration-generated activities.** Written through a CRM service function with the caller's identity as source;
  ownerless activities are editable (D-51).
- **Deletion of a linked record (Ruled D-44).** Deleting a contact never deletes an activity and never reassigns it to
  the contact's organization; the link becomes a tombstone and the activity is listed in an Orphaned Activities view
  ([08](#9-08-record-lifecycle-and-safe-delete)). Until safe delete ships (CRM-M20), the cascade on `activity_links` remains and is a
  documented gap.
- **Baseline types (Ruled D-48).** Seven default activity types and five task types are versioned in code and reconciled
  into existing workspaces idempotently on deployment.

**State.** Verified backend; console list read-only, owner picker capped at 100.

### 4. Tasks and follow-up work

- **What a task is.** A unit of staff work with a type, subject, description, due date, status (open, completed,
  cancelled), assignee (a console user), optional contact and organization, an optional subject record in any application
  (generic registered record), and an optional action link that opens the work (for example a submission review).
- **Creation.** By staff (from a record, the list or the drawer), by workflows and other applications through the
  `tasks` service function (Verified; application processes' D-09 says workflow actions create tasks through it), and by
  product templates (Accounting `product_task_templates`).
- **Views.** **My Open Tasks** is the default view (Verified). Other standard views (All, My Completed, Assigned by Me,
  Overdue, Due Soon, Unassigned, Completed) and any saved view are query-based over the `crm.tasks` dataset.
- **Concurrency (Ruled D-52).** Complete, reopen, cancel, claim and release require the current version; a stale drawer
  cannot overwrite a colleague's change through the UI or the API.
- **Task queues — Open (CRM-Q1).** The brief recorded a decision to remove task queues in favour of saved views. The code
  disagrees: queues are live in CRM, Accounting product templates route tasks to them, and the console shows one view per
  queue (e05 A.10). Only application-process workflow actions were decided not to use queues. This document recommends
  keeping a queue as a **pool** a task can be offered to (an unassigned task in a pool can be claimed by its members), and
  retiring queue-specific screens: "my team's tasks" is a saved view filtered by pool. See CRM-Q1.
- **Assignee validity (Ruled E-10).** The assignee must be a user of the workspace eligible to receive tasks, checked
  through an authoritative Platform interface (no direct read of Platform tables). A user who later becomes inactive stays
  shown as the historical assignee (DEF-CRM-0709).
- **History and notifications.** Tasks must keep record history (missing today). Assignment notifications and due-date
  reminders are delivered through Platform communications and the Attention module (a task due soon becomes an attention
  item for its assignee); 1.0's reminders never worked, so this is new behaviour, scheduled late ([15](#16-15-milestone-plan-completion-roadmap-deliverable-c)).
- **Responsibilities and attention.** Responsibilities (who looks after an area) and attention items (what needs a
  person's attention) are Platform capabilities. CRM may contribute attention providers (for example "organization out of
  compliance", "task overdue") once revision 2 of that work is released by Andrew; nothing in CRM depends on it.
- **Deletion.** Tasks can be deleted by permitted users (the console never calls delete today). On contact deletion a
  task's reference is detached with a tombstone (record-lifecycle design), not silently nulled.

**State.** Verified backend; gaps: ids, history, version on claim/release, delete in console, queue decision.

### 5. Relationships and households

1.0 had generic typed relationships between any two contacts or organizations; 2.0 has only affiliations (contact to
organization) and the interim spouse field. Ruling D-19 sets the target: "one canonical relationship exposed in both
directions, compatible with the broader CRM relationships/household model."

**Recommended model (pending CRM-Q2):**

- `relationship_types` (configuration): name, the two sides' kinds (contact or organization), a label for each direction
  ("Parent of" / "Child of"; symmetric types use one label), whether a pair may hold the type more than once at a time.
- `relationships` (records): one row per relationship, `left_id`, `right_id`, type, start and end dates, notes; exposed in
  both directions by a view; no overlapping period for the same pair and type; a self-relationship refused.
- Spouse becomes a symmetric contact↔contact relationship type; `spouse_id` is migrated and retired.
- Organization↔organization relationships cover partnerships, affiliates and sponsorships **that are not hierarchy**; the
  parent/child tree stays `parent_organization_id`.
- **Households** (shared mailing, family memberships) are a later extension: a household is an organization of a system
  type "Household" with affiliations of type "Household member", so that Membership and Commerce can bill and mail a
  household with no new party kind. This keeps one party model.
- Merge: relationships are reassigned to the survivor; duplicates collapse.

### 6. Data quality

Data quality is a set of rules at the right layer plus visible remediation; no generic rules framework is proposed.

| Concern                                                                                                                                                                       | Where the rule lives                                                        | How it shows                                                                       |
| ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------- | ---------------------------------------------------------------------------------- |
| Required fields, shapes, ranges, closed lists                                                                                                                                 | Database constraints (CHECK, NOT NULL, domains, FKs)                        | Write refused with a field-level 400                                               |
| Business rules across rows (one primary, applicable roles, linked sources, effective dates, version)                                                                          | Domain services in one transaction, backed by constraints where expressible | Write refused with a named rule                                                    |
| Normalisation (trim, case, e-mail, country/state, phone format)                                                                                                               | Database trigger and domain value objects                                   | Silent and deterministic                                                           |
| Uniqueness (primary e-mail, record number, configuration ids)                                                                                                                 | Unique indexes                                                              | 409 naming the conflicting record                                                  |
| Probable duplicates                                                                                                                                                           | Duplicates module (rules `warn`, `block`, review)                           | Warning on create; candidate queue ([07](#8-07-duplicate-detection-and-record-merging)) |
| Referential integrity inside CRM                                                                                                                                              | Composite FKs                                                               | Write refused                                                                      |
| Referential integrity across applications                                                                                                                                     | Merge participants now; deletion-impact contract later                      | Blockers in previews                                                               |
| Conflicting or stale data (disconnected addresses, compliance violations, orphaned activities, contacts with no current affiliation, records missing a required custom field) | Queryable state on the records                                              | Data Quality recipes in Explorer, each with a remediation action                   |
| Import-time validation                                                                                                                                                        | Data Job Engine validate step through the same aggregate                    | Per-row outcome before apply                                                       |
| Historical cleanup                                                                                                                                                            | Data jobs (bulk update, merge) and remediation reports                      | Job results with reasons                                                           |

**Data Quality dashboard (Recommended).** The existing Data Quality area (duplicates overview, queue, dismissed, merged)
gains read-only counters and Explorer recipes for the other conditions above, each opening a list with the relevant
remediation action (verify address, add affiliation, fix field, merge). No scoring engine; each indicator is a saved
query, so it can be added without new infrastructure.

### 7. Directories, engagement, enrichment and portal self-service

These 1.0 capabilities have no 2.0 implementation. Each needs an owner decision before design; they are listed with a
recommendation in [13](#14-13-decisions-standing-recorded-and-needing-andrew-deliverable-d):

- **Member and organization directories** (CRM-Q4). Recommended: a directory is a Web Experience Platform page backed by
  a CRM-published **directory projection** that applies opt-out, status and the configured field allowlist _before_ any
  template runs (1.0 pattern P12, H-10). CRM owns the per-contact directory opt-in/out fields and the projection.
- **Engagement scoring** (CRM-Q5). Recommended: retire the fake console screen now; design engagement as its own
  capability later, because its inputs span Membership, Events, Commerce and Communications.
- **Enrichment** (social profile, Google Civic districts). Recommended: Integration Platform connectors writing through
  CRM's service functions; no CRM-internal vendor calls. Low priority.
- **Portal self-service** (own profile, organization profile, organization roster, child organizations, a profile
  confirmation step for application processes). CRM provides portal routes over the same services. **Portal authorization
  is Platform Security's** (CRM-ADR-11): Security grants an organization-administrator capability scoped to an organization
  and its descendants and may derive it from CRM affiliation roles through a published CRM read; CRM's routes enforce what
  Security resolves (affiliations README §10). Pages belong to the Web Experience Platform. Portal sign-up identity matching
  already works (Platform likely-match over `crm.portal_contact_candidates` and `crm.create_portal_contact`). Delegates
  never worked in 1.0 (no creation flow), so they would be designed new, by decision. Milestone CRM-M22.
- **Staff "log in as member"** (1.0 impersonation). Out of CRM scope; a Platform security capability. 1.0's design put
  staff tokens in a URL and must not be repeated.

## 7. 06. API and Service Architecture

## 06. API and Service Architecture

Part of [RHYTHM-CRM-ARCH-001](#1-rhythm-crm-arch-001-rhythm-2-0-crm-definitive-architecture-and-completion-plan). Evidence: [e03](#22-e03-rhythm-2-0-crm-backend-assessment-services-crm) §2, §14–§16;
[e05](#24-e05-platform-capabilities-crm-depends-on-and-every-other-application-s-references-to-crm-records) A.12–A.13, Part B.

### 1. Shape of the service

services-crm is one macroservice on the Rhythm 2.0 macroservice standard (`@rhythmsoftware/macroservice-architecture`):
nine modules, each with `aws/{api,lambda,logic,resources}`, `db/` (one `<table>.table.ts` per table), `services/` (all SQL
for a concept in `services/persistence/<concepts>.ts`; routes never touch the database) and `tests/`. Seven API functions
(entities, configuration types, regions, tasks, activities, duplicates, merges) plus data-job and service-only functions,
behind Platform's authorizer at `crm.<api2 zone>`. The deployed stage is checked against the route list by
`npm run api:verify` (Verified).

**Recommended:** CRM adopts the `@rhythmsoftware/macroservice-architecture` validator (Accounting already does), which
enforces the ids rule and the persistence layout in CI ([14](#15-14-implementation-gap-matrix-deliverable-b) row X-03).

### 2. Public (console and integration) API

The route inventory is e03 §16. Principles every route follows (target; deviations listed in [14](#15-14-implementation-gap-matrix-deliverable-b)):

| #    | Rule                                                                                                                                                                                                                                                                                                                                                                                                                                                           | Today                                                                                                                                                                                                              |
| ---- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| A-1  | Paths `/<collection>/{tenant_id}[/{id}[/<sub>]]`, snake_case                                                                                                                                                                                                                                                                                                                                                                                                   | Verified                                                                                                                                                                                                           |
| A-2  | Create accepts a client-assigned UUIDv7 `id`, generated before the first attempt and reused on every retry. A create whose id already exists answers 409 `record_exists` with `details: { id }`; the client reads the record and treats the create as done (Ruled D-43; standard W5). Contacts, organizations, activities and tasks declare the optional idempotency contract once CRM's ledger exists                                                         | Contacts and activities accept a client id; an existing id answers 400 (must become 409 `record_exists`); no ledger                                                                                                |
| A-3  | **Update contract (Ruled E-09, D-09).** An ordinary update changes only what it names: an omitted property or collection is unchanged; an explicit `null` clears where permitted. Full replacement is a separate, explicitly named operation that requires a complete validated payload. Optimistic concurrency stays mandatory; it is not the protection against omitted properties. Collection and relationship mutations keep their own explicit operations | **Not built**: `PUT` replaces the whole record and omitted collections are emptied (`docs/entities-api.md` "Update semantics"); this changes CRM's published `PUT` semantics, see [13](#14-13-decisions-standing-recorded-and-needing-andrew-deliverable-d) CRM-ADR-04 |
| A-4  | `PATCH` is JSON Patch; the version travels as `test /sys_version`; a failed version test is 409 `version_conflict`                                                                                                                                                                                                                                                                                                                                             | Not built: version optional; stale test answers 400                                                                                                                                                                |
| A-5  | Every field-changing write and state transition carries `sys_version`: missing 400 `version_required`, stale 409 `version_conflict`; a no-op changes nothing (Ruled D-05, D-52; standard `record-write-contracts.md` W1–W2)                                                                                                                                                                                                                                    | Not built in CRM; held until the five console callers send versions                                                                                                                                                |
| A-6  | Collection mutations with their own permission and audit for relationships: add, update, end and remove an affiliation; add, end a role                                                                                                                                                                                                                                                                                                                        | To add with D-09                                                                                                                                                                                                   |
| A-7  | A merged-away id answers **410 Gone**, code `RecordMerged`, details `{survivor_id, merge_id, merged_at}`, on every route that names it, child routes included (D3, Ruled D-20)                                                                                                                                                                                                                                                                                 | Verified for record routes; child routes to confirm (D-20 lists contradictions)                                                                                                                                    |
| A-8  | An archived record answers its reads normally and refuses every write except unarchive, merge and (from CRM-M20) the deletion job, with 409 `RecordArchived` (lifecycle 01 §2)                                                                                                                                                                                                                                                                                 | Being built (record-lifecycle WP-C3)                                                                                                                                                                               |
| A-9  | A deleted record answers 404 with body code `RecordDeleted` and a pointer to the deletion record once safe delete exists                                                                                                                                                                                                                                                                                                                                       | Later milestone                                                                                                                                                                                                    |
| A-10 | Lists are cursor-paged with a stated maximum page size; every filter is indexed                                                                                                                                                                                                                                                                                                                                                                                | Verified for lists                                                                                                                                                                                                 |
| A-11 | Every success answers 200 today because the shared dispatcher cannot answer 201/202 (`docs/debt/created-status-code.md`)                                                                                                                                                                                                                                                                                                                                       | Accepted debt; fix in the library, not in CRM                                                                                                                                                                      |
| A-12 | Errors use the shared error body `{ errorMessage, errorType, message, code, details }`                                                                                                                                                                                                                                                                                                                                                                         | Verified                                                                                                                                                                                                           |

### 3. Service functions (calls from other applications)

Other applications call CRM through IAM-authorised service functions (Lambda-to-Lambda, `docs/security/service-calls.md`),
through SQL `SECURITY DEFINER` functions in the `crm` schema granted to named roles, or by reading published views. Today
CRM publishes one service function (`tasks`) and the two portal sign-up SQL functions; other applications read CRM's
physical tables directly (e05 Part B). The target set:

| Function                                                                                                                     | Caller(s)                                                                                               | Purpose                                         | Status                                                                                   |
| ---------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------- | ----------------------------------------------- | ---------------------------------------------------------------------------------------- |
| `tasks` (create, complete, cancel)                                                                                           | Platform workflow actions, Accounting product templates                                                 | Create and close tasks                          | Verified                                                                                 |
| `contacts` (read by ids, upsert, update fields, match-or-create)                                                             | Membership (officer updates; its port throws `PortNotWired`), Events, Submissions, Integration Platform | Read and write contacts through CRM's rules     | **Missing**                                                                              |
| `organizations` (read by ids, upsert, lookup by domain)                                                                      | Membership, Commerce, Integration Platform                                                              | Same for organizations                          | **Missing**                                                                              |
| `activities` (log)                                                                                                           | Integration Platform (marketing e-mail engagement), any application recording an interaction            | Write activities with a system source           | **Missing**                                                                              |
| `party_resolution` (resolve id: live, merged→survivor, archived, deleted)                                                    | Every application holding references                                                                    | One answer for "what is this id now"            | **Missing**; aliases readable via `GET /merges/{t}/aliases/{objectType}/{recordId}` only |
| `crm.portal_contact_candidates`, `crm.create_portal_contact` (SQL)                                                           | Platform portal sign-up                                                                                 | Match or create a contact at sign-up            | Verified                                                                                 |
| Merge participant functions (`<schema>.merge_plan_<object>`, `merge_apply_<object>`)                                         | Called **by** CRM in other schemas                                                                      | Rewrite references inside the merge transaction | Verified contract; adoption gaps ([12](#13-12-cross-application-integration-contracts))               |
| Deletion participant operations (`planRecordDeletion`, `holdRecordDeletion`, `applyRecordDeletion`, `releaseRecordDeletion`) | Called **by** Platform's deletion coordinator                                                           | Deletion impact                                 | Designed (record-lifecycle 02); CRM-M20                                                  |

**Match-or-create (Recommended, 1.0 pattern P4).** One function decides whether an incoming person or organization
already exists, using the duplicate rules (normalised e-mail, name, phone, postal keys) and the same `warn`/`block`
semantics as the console. Every entry path uses it: console create, portal sign-up, import upsert, event registration,
abstracts, exhibitor portal, API. Each record keeps its creation source, queryable.

### 4. Persistence, transactions and concurrency

- **One transaction per business operation.** A record and its owned collections, the change events it emits, the record
  history, and any participant function calls (merge) are written in one database transaction (`ctx.withTransaction`).
  The one documented exception is the compliance e-mail's S3 put (`compliance-email.ts:1-20`).
- **Stable ids for owned rows.** Owned collections are written in place (update changed rows by id, insert new, delete
  removed), so ids survive ordinary saves and foreign keys to them (linked contact information) are safe (affiliations
  README §0b.2 item 4).
- **Optimistic concurrency** (Ruled D-05): `sys_version` compare-and-set in one statement; the expected version is the
  client's, never re-read by the server (standard W1). System writers take an equivalent precondition inside their own
  transaction.
- **Locks.** `sys_locked` marks customer-locked records and protected system configuration; protected seeds cannot be
  edited or deleted; unlocking a customer lock is a narrowly authorised, audited action (Ruled D-08).
- **Long operations** run as durable background jobs with progress (CRM's queued worker for merges, per the merge design; Platform's job engine elsewhere): organization merges (up to 100,000
  employees, D7, D9), region recalculation, compliance reprocess, data jobs, and (later) record deletions.

### 5. Events

| Kind                                                     | Mechanism                                                                                     | CRM today                                                                    | Target                                                                                                                                                                                                                                              |
| -------------------------------------------------------- | --------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Change capture (internal)                                | Per-entity `<entity>_change_events` tables, settle trigger, dispatcher, handlers, dead-letter | Verified; consumed by CRM's own handlers and Platform's Quick Search indexer | Also invoke Membership's `membership_crm_change_handler` for affiliation, role and organization changes ([12](#13-12-cross-application-integration-contracts))                                                                                                   |
| Business events (external, outbox `crm.business_events`) | Registered with Platform extensions; subscribers through the Integration Platform             | Only `crm.contact.merged` published                                          | Publish the seven catalogued events (`crm.contact.created/.updated/.deleted`, `crm.organization.created/.updated/.deleted/.merged`) and, when archive lands, `.archived/.unarchived`; payloads carry ids and changed field names, not personal data |
| Record history                                           | `crm.record_revisions`                                                                        | Contacts and activities                                                      | Organizations, tasks, affiliations (already within contacts), configuration types and roles                                                                                                                                                         |
| Audit (Platform)                                         | `platform.audit_events`                                                                       | `record.viewed`, `record.merged`                                             | `record.archived`, `record.deleted` pointers with the lifecycle work                                                                                                                                                                                |

### 6. Observability and operations

- Structured logs with tenant, request id and actor through the shared logger; no personal data in logs.
- Every durable job exposes progress and per-item outcomes to the console (P-6).
- Dead-lettered change events and business events surface as an operator alert and a console notice for the affected
  tenant; a failed handler never silently drops work (1.0 theme 4).
- Performance budgets (Recommended, to be measured in the release-candidate environment): a contact or organization 360
  read p95 under 500 ms at the documented limits (DEF-CRM-0031: a contact at the limits currently fails with 500); a list
  page p95 under 1 s for a 1,000,000-contact tenant; a contact save p95 under 300 ms; Quick Search visibility within
  30 s of commit.

## 8. 07. Duplicate Detection and Record Merging

## 07. Duplicate Detection and Record Merging

Part of [RHYTHM-CRM-ARCH-001](#1-rhythm-crm-arch-001-rhythm-2-0-crm-definitive-architecture-and-completion-plan). The binding sub-architecture is `services-crm:docs/merging/` (approved
2026-10-11; D1..D10 decided 2026-10-11..14; phases 0–4 accepted and live in dev). This chapter states the end-to-end
behaviour, adds Andrew's acceptance rulings of 2026-10-19, and names what remains. It does not redesign the engine.

### 1. Why this is the most important CRM capability to get right

1.0's two worst recurring problems were merges that lost or orphaned data and a merge tool that failed with "unknown
error" ([02 §4](#3-02-rhythm-1-0-rolodex-and-the-support-history) P1, P2). A merge changes the
identity every other application points at. It must therefore be atomic, previewed exactly, auditable, and it must reach
every reference in every application.

### 2. Duplicate detection

**What it does (Verified, e03 §12).** Detection finds records that probably describe the same person or organization,
scores them, says why, and lets staff dismiss or confirm.

- **Candidate generation** by match keys (normalised e-mail, phone, name and postal keys), bounded so the candidate set
  stays small; **scoring** with Jaro-Winkler and nickname equivalence; **bands** (high, medium, low) and a plain-language
  **explanation** of which fields matched. No machine-learning model and no LLM (decided; deterministic model).
- **Rules** (tenant configuration): which keys and thresholds apply and the action: `warn` (show matches, allow create),
  `block` (refuse ordinary creation) or review (queue only).
- **Freshness.** Candidates are maintained from contact and organization change events, so a merged, deleted or edited
  record leaves or updates its pairs promptly; a rescan rebuilds them for a tenant on a queue. Stale pairs whose only
  evidence is no longer valid are retired, while pairs with other independent evidence are kept (Ruled D-47).
- **Groups.** Pairs form groups so staff review a person once.
- **Dismissals** are per pair, attributed and audited, and reversible ("Restore"). Dismissing an already dismissed pair
  answers idempotent success with the earlier attribution (Ruled D-34).
- **Synchronous checks** at create and edit return candidates for the form; portal sign-up uses SQL functions granted to
  Platform.
- **Calibration** uses synthetic data and 2.0 staff decisions as they accumulate; no 1.0 production data (D5).

**Rules for `block` (Ruled D-32).** A Block rule stops ordinary creation. Existing matches stay visible for review. Only
users holding a specific override permission may choose an explicit, reasoned, audited **Create Anyway**; the reason is
stored on the new record's history.

**Gaps.** Service: duplicate rules cannot be created or deleted (list and update only). Console: no rescan action, Create
Anyway not offered (the service supports it since `859d673c`), no duplicate check in the console when creating an
organization (the service applies block rules since `2bb6a828`). Bulk updates do trigger detection through change events
(a proving test is owed; `docs/data-jobs.md:770` is stale). Every entry path
must call the same match-or-create function ([06 §3](#7-06-api-and-service-architecture)).

### 3. The merge, end to end

```
Staff (or API)                CRM merge engine                         Participants (Platform, Committees, …)
──────────────                ─────────────────                         ─────────────────────────────────────
pick 2..10 records ─────────▶ create plan: survivor recommendation,
                              field and collection decisions,
                              plain-language preview  ◀──────────────── merge_plan_<object>(survivor, merged) for each
review, change choices ─────▶ re-plan; plan hash binds participants'     participant: actions, blockers, warnings
                              versions and every dependent child's
                              id and version (D-31)
confirm ────────────────────▶ validate (same planner); durable job;
                              one transaction:
                                hold records (RecordBusy)
                                apply CRM decisions and references ───▶ merge_apply_<object>(…) inside the same tx
                                write aliases, merge record (snapshots)
                                delete merged-away rows
                              commit, or roll back everything
see result ◀──────────────── merge history; 410 for old ids; audit pointer record.merged on every participant's history
```

**Rules (Verified unless marked):**

- **N-way.** Two to ten records of the same kind (contact or organization) per merge; the engine is independent of
  detection, so staff can merge any records they choose.
- **Survivor and fields.** The planner recommends a survivor; staff may change it and choose, per conflicting field and
  per collection item, which value survives (`conflict-resolution.md`). Communication consent: the most restrictive choice
  wins and is not offered (D6).
- **Affiliations.** Same organization and type with overlapping or touching periods combine (earliest start, latest end,
  open wins, roles unioned); otherwise both are kept; differing titles and notes are listed as dropped under
  `affiliation_combined`; at most one primary survives; dated history
  kept; links on combined affiliations follow the designation rules (affiliations README §0 item 7). Organization merges
  move every employee affiliation to the survivor inside the transaction, up to 100,000 employees (D7), running as a
  background job with a long-running warning in the preview (D9, D10).
- **Atomicity.** The whole merge, across CRM and every participant schema, commits or nothing does (D1). Participants are
  SQL functions in their own schemas, so ownership holds: only the owner's code writes its tables.
- **Unknown references block.** A column holding a contact or organization id with no policy fails the coverage test in CI;
  at runtime an unregistered participant pair blocks.
- **Portal logins.** One participant with a login: relinked to the survivor, shown in the plan, recorded in security
  audit. Two or more: the merge is blocked until an administrator deactivates the extra logins (D2).
- **Merged-away ids** answer **410 Gone** with the survivor on every route (D3, Ruled D-20); the console follows to the
  survivor with an explanation; aliases are kept for the life of the tenant.
- **The merge record** keeps every participant's original values for the life of the tenant, removable for one person on a
  privacy request (D4); `record.merged` pointers appear in every participant's history (D8).
- **Visibility (Ruled D-21, D-45).** Anyone permitted to read a record can read its merged-from history and merge details
  (subject to ordinary field restrictions); executing a merge needs the merge permission.

**Rulings that change the engine or console (2026-10-19):**

| Ruling     | Requirement                                                                                                                                                                                 | State                                                      |
| ---------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------- |
| D-03       | From Contact 360, merge with a second record chosen by a CRM-backed picker, not only detected pairs                                                                                         | Picker built, wired only on design routes                  |
| D-30       | Merge Details show, per field, the winning, losing and filled-from values and the absorbed originals, reusing shared audit/diff components                                                  | Service landed (`03bbc71d`); Merge Details console pending |
| D-31       | The approval fingerprint binds the **ids and versions** of every transferred child (tasks, activities, affiliations…), not counts; any change after preview requires refresh and reapproval | Service landed (`4c855bf4`); console reapproval pending    |
| D-33       | If a chosen source value changes after approval, the approval is invalidated; the new value is shown and must be reconfirmed (implemented with D-31)                                        | Service landed (`4c855bf4`)                                |
| D-20       | Consistent 410 on old child routes; survivor navigation                                                                                                                                     | Service landed (`36ceb699`); console navigation pending    |
| D-21, D-45 | Read-only merge history for permitted readers; readable merge pointer                                                                                                                       | Service landed (`ab22aac0`)                                |

### 4. Cross-application reference integrity on merge

This is the largest risk in CRM today (e05 risk 1). Of the applications that hold contact or organization ids, only
**Platform** (contacts and organizations; required for contact merges) and **Committees** (contacts only) ship merge
participant functions. **Accounting** (bill-to on fourteen document tables, subledger, Stripe customers),
**Commerce** (orders, checkouts, entitlements, promotions), **Membership** (memberships, applications, organizational
units, snapshots) and **Submissions** (applicants, participants, reviewers) ship none, so a merge today commits and
leaves their rows pointing at the merged-away id. Accounting's own plan says merges must be **blocked** until it is
registered (`accounting:docs/plan/02-accounting-architecture.md:700-704`); that has not happened.

**Target (Recommended):**

1. **Every application that holds a reference ships a participant** with a typed reference registry and a coverage test
   (the Platform and Committees pattern). Policies per reference: `reassign`, `reassign-dedupe`, `consolidate`,
   `preserve-history` (keeps the old id, resolved through aliases), `block` (a staff-facing sentence says what to do first),
   `owned`, `not-a-reference`.
2. **Financial and governance history** answers `preserve-history` (issued invoices, payments and votes keep the id they
   were issued to; readers resolve it to the survivor through aliases) or `block` for in-flight state that must be
   resolved first. Accounting decides which, in its own participant; the write-once columns are never rewritten by CRM.
3. **Block until then (CRM-Q3).** The merge design already treats unknown references as blocking. Recommended, for
   Andrew's recorded decision ([13 §3.11](#14-13-decisions-standing-recorded-and-needing-andrew-deliverable-d)):
   a merge whose records are referenced by an application with no participant for that object type is blocked with a
   sentence naming the application and the counts; merges of records nobody else references proceed; no override; the
   block lifts when the participant ships. A CRM-held list of non-participant reference columns, held to the catalog by a
   fail-closed CI test, drives it.
4. **Who builds participants.** Each application builds its own, in its own milestone plan, when Andrew's focus order lets
   it build; CRM milestones do not wait for them.
5. **Organization merges** gain participants in Platform (exists), Committees (missing: representing and reserved
   organizations), Membership, Accounting and Commerce.
6. **The relationship coverage document** in services-crm (`docs/merging/relationship-coverage.md`) is out of date; it is
   replaced by a generated cross-application matrix produced from every participant's registry in CI.

### 5. Concurrency, failure and recovery

| Situation                                   | Behaviour                                                                                                                                                                                                                                                                  |
| ------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Edit to a record during its merge           | Refused with retryable `RecordBusy` until the merge commits (D10)                                                                                                                                                                                                          |
| A dependent child changes after the preview | Approval invalidated (D-31); staff refresh and reapprove                                                                                                                                                                                                                   |
| A participant raises a blocker at apply     | Whole transaction rolls back; the merge record shows the blocker sentence; nothing changed                                                                                                                                                                                 |
| A participant function errors               | Same: rollback, failure recorded with the participant named; retry after the fix                                                                                                                                                                                           |
| Two merges overlap on a record              | The second waits or fails with `RecordBusy`; tested (`overlapping-merges` spec)                                                                                                                                                                                            |
| The job is interrupted                      | The durable job retries from the plan; the transaction either committed or did not                                                                                                                                                                                         |
| A wrong merge is discovered later           | No unmerge (by design). The merge record holds the originals; staff recreate the person from Merge Details and move references back through ordinary edits. **Recommended:** a "recreate from merge" action that prefills a new record from the snapshot (later milestone) |

### 6. Post-merge verification (test obligations)

1. Zero rows in any participating schema reference a merged-away id (registry walk).
2. Every merged-away id resolves to the survivor through aliases and answers 410.
3. The executed result equals the approved preview field by field.
4. One audit entry per merge lists every reference moved, by participant and count.
5. Quick Search no longer returns a merged-away record; duplicate candidates for it are gone.

## 9. 08. Record Lifecycle and Safe Delete

## 08. Record Lifecycle and Safe Delete

Part of [RHYTHM-CRM-ARCH-001](#1-rhythm-crm-arch-001-rhythm-2-0-crm-definitive-architecture-and-completion-plan). The binding sub-specification is the **record lifecycle design**,
`services-platform` branch `agent/crm-acc-w3-arch-lifecycle` (commit `6873e3b6c`), `docs/proposals/record-lifecycle/`
(README and chapters 01–08), written from Andrew's rulings of 2026-10-19 (§2.1, D-02, D-07, D-25, D-41, D-42, D-44). It
lands on Platform master with the acceptance session's integration job. This chapter adopts it, places it in CRM's
roadmap, and states what CRM must do. It does not redesign it.

### 1. The decision that governs timing

**Decided (Andrew, 2026-10-19 ~16:50 ET, in the CRM acceptance session):** cross-service safe delete is a feature built
in a **later milestone of this architecture**, described as "advanced functionality Rhythm 1.0 doesn't have at all". It
is **not** part of finishing the current CRM build. **Archive** (the design's phase 1) continues now in the acceptance
effort. **Until the safe-delete milestone ships, today's delete stays as it is.**

So the roadmap has: archive in progress now (inside the retroactive milestones CRM-M1..M8, gap rows LC-01 and LC-06), and
safe delete as milestone **CRM-M20** in [15](#16-15-milestone-plan-completion-roadmap-deliverable-c).

**Release consequence.** D-41 and D-44 are P0 rulings ("before a production release"). With Andrew's deferral, CRM can be
acceptance-green but is **not production-ready for any customer that hard-deletes contacts or organizations** until
CRM-M20, unless hard delete is restricted in the meantime (CRM-Q10).

### 2. Three lifecycle operations, never confused (Ruled §2.1)

| Operation       | What it does                                                                                                                                                                                  | Reversible        | Permission                                                                                   | Who sees the record afterwards                                                                                                                                                                      |
| --------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------- | -------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Deactivate**  | An ordinary edit of an operational status: contact `inactive` or `deceased`, organization `inactive`, configuration `is_active = false`                                                       | Yes, by editing   | The record's edit permission                                                                 | Everyone; excluded from new choices by the discovery rule (`status-visibility.md` §4.1)                                                                                                             |
| **Archive**     | A separate lifecycle state (`archived_at`, `archived_by_id`, `archive_reason`); the record and every historical reference to it are kept; it leaves ordinary lists, pickers and new workflows | Yes, by Unarchive | `crm.contacts.archive`, `crm.organizations.archive` (admin tier, includable in custom roles) | Readable by id, through history and existing references, and in an explicit "Archived" view; every write except unarchive, merge and (from CRM-M20) the deletion job refused (409 `RecordArchived`) |
| **Hard delete** | An explicit, authorised, impact-previewed removal run as a durable job; every application holding a reference answers the deletion-impact contract first                                      | **No**            | A delete permission plus approval of the plan's digest                                       | Nobody; a metadata-only deletion record remains for authorised auditors                                                                                                                             |

**Archive is not a status** (record-lifecycle R-1): status is an operational fact people edit; archive is a lifecycle
state with its own permission. A deceased person can also be archived. **Archive never maps to delete** (Ruled D-02).

### 3. Archive (phase 1, building now)

- Contacts and organizations gain the three archive columns (set together, CHECK), `POST …/archive` and `…/unarchive`, and
  the two permission keys. Address and phone number types gain `is_active`.
- Discovery becomes "status discoverable **and** not archived", in the same declaration that drives pickers, Quick Search
  and default criteria (record-lifecycle R-2), not a second filter.
- An archived organization keeps its identity in every historical affiliation; the organization's Contacts tab shows
  Current, Past and All (Ruled D-07). An organization any affiliation names stays non-deletable; archive is the path for a
  closed firm (R-7).
- Linked addresses and phones whose source is removed become `disconnected`, needing verification, with three actions
  (Ruled D-25; WP-C5, WP-E7).
- Console: Archive and Unarchive actions through a shared confirmation panel, gated by the new keys; the inert toolbar
  actions disappear from live views; pickers label held archived values (WP-E1 archive half, WP-E2, WP-E3).
- New references to an archived contact or organization are refused (`ObjectInvalid`, naming the field) by every service
  that creates one (lifecycle 01 §5). CRM's own activities, tasks and affiliations adopt it with archive; other applications
  adopt it with their participants (LC-06).
- Fleet jobs `crm-acc-w3b-crm-archive` and `crm-acc-w3b-ui-archive` are building this (acceptance session brief,
  2026-10-19).

### 4. Safe delete (phases 2–3, milestone CRM-M20)

**The deletion-impact contract** (record-lifecycle 02). The deleting service asks Platform to plan the deletion of
identified records at stated versions. Platform asks every participant through the tenant lifecycle participant
mechanism that already reaches every macroservice (no second registry). Each participant answers, per kind of reference it
holds, one disposition:

| Disposition | Meaning                                                                                                                        | Examples                                                                   |
| ----------- | ------------------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------- |
| `block`     | Deletion may not proceed while this holds; never overridable by the approver                                                   | an open invoice; a current committee seat; a portal login                  |
| `retain`    | The participant's record stays and keeps the id under a named retention policy; reads resolve the id to the deletion tombstone | a paid invoice; a cast vote                                                |
| `detach`    | The participant keeps its record and replaces the reference with a tombstone pointing at the deletion record                   | a task's contact; an activity link; a future meeting's participant         |
| `redact`    | Keeps the record and removes the personal content the deletion's policy names                                                  | an orphaned activity's subject and body under `erasure_request`            |
| `delete`    | Removes rows that exist only for the deleted record and have no retention requirement                                          | the contact's own e-mail addresses; duplicate candidates; search documents |

Rules: a regulated record (financial, membership, governance, submission decisions, credentials) is never `delete`
(Ruled D-41: deleting a CRM contact does not authorise financial-history deletion). No participant reassigns a reference
to another record (Ruled D-44). Each participant enforces its own answer at apply. An enabled participant that declares
no `recordReferences` is treated as holding references to everything and **blocks** by name (fail closed).

**The flow.** Preview (every participant's outcomes with counts, ids, versions and a readable reason) → approval bound to
a digest of the subjects' and every dependent record's ids and versions (Ruled §2.2) → a `record_deletion` job that
re-plans, holds the subject (writes refused `RecordPendingDeletion`), applies each participant in dependency order and
removes the owner's row last → a `platform.record_deletions` row holding who, when, why, object type and id, record
number, and per-participant dispositions with counts, and **no personal data of the deleted record** (Ruled D-42).
CRM's own revisions of the record are redacted to a tombstone in the same job (WP-C2). An authorised auditor reads the
result in a read-only deletion audit view reached from old links and data-job items (D-42). Any path that removes a
contact or organization without an approved, still-current plan is refused by CRM's persistence operation, not the
console. Bulk deletes go through the same contract in batches of at most 500 subjects.

**Activities (Ruled D-44).** An activity whose last contact link is deleted is never reassigned to the contact's
organization and never deleted for that reason. Its link becomes a tombstone, the activity is marked orphaned and appears
in an authorised Orphaned Activities view (an Explorer recipe), and its content is redacted only when the deletion's
explicit policy requires it. The `activity_links` foreign keys change from `ON DELETE CASCADE` to `NO ACTION` in phase 3.

**Work packages** (record-lifecycle 07; the discovery parts of WP-L1 and WP-P1 ship with archive): WP-L1 (deletion parts of the shared library), WP-P0 (Platform coordinator, tables, job type, audit
routes), WP-P1 (Platform's own participants), WP-C1 (CRM deletion through the contract), WP-C2 (revision redaction),
WP-C4 (orphaned activities), WP-P2..P6 (Membership, Commerce, Accounting, Committees, Submissions participants), WP-E4..E6
(console impact panel, deletion audit view, orphaned activities). Phase 3 (enforcement) switches on only when every
enabled service's manifest carries `recordReferences`.

### 5. Open questions inherited from the lifecycle design

These carry their defaults into CRM-M20 and are restated in [13 §4](#14-13-decisions-standing-recorded-and-needing-andrew-deliverable-d).
They are **not** asked now, because the milestone is deferred.

| #   | Question                                                                          | Default                                                              |
| --- | --------------------------------------------------------------------------------- | -------------------------------------------------------------------- |
| Q-1 | Separate archive permission for organizations, and its tier                       | Two keys, admin tier                                                 |
| Q-2 | Must a hard delete's approver be a second person?                                 | No; a tenant "two-person deletion" setting may come later            |
| Q-3 | How long is a deletion record kept?                                               | Life of the tenant; it holds no personal data                        |
| Q-4 | May an `erasure_request` redact activity content and retained records' free text? | Yes for CRM activities; financial and governance participants retain |
| Q-5 | Must mailings exclude a `disconnected` address until verified?                    | No automatic exclusion; the state is queryable                       |
| Q-6 | Do merges write a deletion record?                                                | No; a merge absorbs, its trail is `record.merged`                    |
| Q-7 | Do an ended affiliation's linked rows become `disconnected`?                      | No change until decided                                              |

### 6. Risks while safe delete is deferred (recorded, not re-opened)

Andrew decided today's delete stays until CRM-M20. The consequences are recorded so they are visible, not as a request
to revisit:

- A contact with an open invoice, an active membership, a current committee seat, an in-flight submission, an active
  entitlement or a portal login can be deleted; those rows keep an id that resolves to nothing (e05 Part B).
- Activities linked only to that contact lose their last link and become unreachable from any screen (DEF-CRM-0738).
- The deleted contact's revisions keep its personal data (DEF-CRM-0683; SOC 2 controls TC-DAT-003, TC-PRV-003, to be
  recorded in services-crm `docs/compliance/exceptions.md` until WP-C2 lands).

Today the console offers no Delete on a record, but **the data-job wizard bulk-deletes contacts and organizations from an
Explorer selection** (up to 500 at a time), and the API deletes single records. Whether to close that console path before
CRM-M20 is decision CRM-Q10 ([13 §3.9](#14-13-decisions-standing-recorded-and-needing-andrew-deliverable-d)); Andrew's
decision that today's delete stays stands until he answers it.

## 10. 09. Console Experience: Search, Explorer, Queries and the 360 Pages

## 09. Console Experience: Search, Explorer, Queries and the 360 Pages

Part of [RHYTHM-CRM-ARCH-001](#1-rhythm-crm-arch-001-rhythm-2-0-crm-definitive-architecture-and-completion-plan). Evidence: [e04](#23-e04-crm-console-assessment-react-console) (with addendum A–D),
[e05](#24-e05-platform-capabilities-crm-depends-on-and-every-other-application-s-references-to-crm-records) A.1–A.3, A.9.

### 1. Who owns what

| Concern                                             | Owner                                                    | CRM's part                                                                                        |
| --------------------------------------------------- | -------------------------------------------------------- | ------------------------------------------------------------------------------------------------- |
| Query engine, datasets, describe, compile, run      | Platform `modules/queries`                               | Register CRM datasets with good field descriptions; grant reads                                   |
| Saved queries, recipes, reports, exports, schedules | Platform                                                 | Ship a recipe for every CRM list and data-quality indicator                                       |
| Explorer (saved views, navigation, select all)      | Platform `modules/explorer`                              | Explorer definitions for contacts, organizations, activities, tasks                               |
| Quick Search                                        | Platform `modules/quick-search`                          | Feed it from change events; declare discoverability (status and archive)                          |
| Layouts, forms, 360 cards, record actions           | Platform `modules/layouts` + console metadata            | Contact and organization layouts; CRM cards; action definitions wired to CRM routes               |
| Cards contributed by other applications             | Each application                                         | Nothing: Membership, Accounting, Committees, Events contribute their own cards to CRM's 360 pages |
| Record history views                                | Shared console component + each service's history routes | History routes for every CRM record type                                                          |

Andrew's ruling §2.5: 360 cards, record actions, forms, field editors, audit views and Explorer recipes are built through
the shared metadata-driven systems, not CRM-only components.

### 2. Search

- **Quick Search (⌘K).** Contacts and organizations, Platform-issued secured key, results in under a second. Exact matches
  on record number, e-mail or full name rank first (1.0 pattern P8a). Created, edited, merged, archived and deleted
  records are reflected within a stated bound (target 30 s) with no manual reindex. Deletes and late updates never resurrect a record, and one
  tenant's bulk delete never stalls indexing for another (Ruled D-29). **Today:** the index did not receive new
  dev records after 17 minutes (DEF-CRM-0119, escalated), and a result opens only when the CRM-specific live switch is on.
- **The global `/search` page** is sample data in every build and is linked from the navigation. It must either become a
  full-results page over Quick Search and the contacts and organizations datasets, or be removed from the navigation
  (Recommended: build it as "see all results" for a Quick Search term; until then, remove it, P-9).
- Activities and tasks are found through their Explorer lists, not Quick Search.

### 3. Lists (Explorer)

**Behaviour required of every CRM list** (contacts, organizations, activities, tasks):

- Default filters: contacts and organizations default to discoverable records (active and not archived); tasks default to
  **My Open Tasks**; activities to the current user's recent activity or all, per saved view.
- Filters, sort, column chooser, page size, cursor paging, frozen navigation session (Previous/Next on the record),
  saved and pinned views (private and shared), select all matching (ids only, up to 10,000), export.
- **Criteria.** Explorer offers flat AND chips today; grouped AND/OR and NOT exist in the Queries builder. Explorer must
  edit the same query AST without degrading it: a negated group opened from a saved query keeps its meaning (Ruled D-24).
- **Ask (AI).** Natural-language Ask modifies the current structured query intelligently: additive requests add
  conditions, conflicting requests replace the conflicting condition ("show inactive contacts" replaces the Active
  default), with a visible change summary and undo (Ruled D-27). The backend (Platform `modules/llm` Generate Query) is
  built; the console turns it on only with `NEXT_PUBLIC_QUERY_GENERATION=live`. It must be on in live consoles once D-27
  is accepted.
- **Relationship-aware filters** (affiliation type, role, organization type, primary organization, organization hierarchy)
  are generated query properties over affiliations (affiliations README §7), plus hierarchy relations once the hierarchy is
  queryable ([05 §2.2](#6-05-functional-architecture-what-every-crm-capability-does)). In the visual builder, criteria over related
  collections ("has an affiliation where role equals Treasurer") are a reusable semantic-query capability, scheduled as a
  later query-designer milestone, not a CRM special case (Ruled E-17).
- **Columns contributed by other applications** (membership status, owner, tier, member since) come from those
  applications' dataset relations, not CRM columns. Until they exist, the columns are removed from the definitions (today
  they are always empty).
- **Actions.** Row actions (open, edit, log activity, create task, archive) and bulk actions (update fields and delete via
  data jobs; archive; add to list) are defined once in action metadata. **Today:** Assign Owner is inert; saved-query bulk
  actions throw "not available yet" in live mode; activities and tasks lists have no bulk actions; the activities list has
  no Log Activity.
- **Performance.** Every filter offered is indexed; p95 under 1 s per page for a 1,000,000-contact tenant (to be measured).

### 4. Saved queries

Three start paths, in this order on New Query: **Create with AI** (Generate Query), **Browse Package Queries** (recipes
shipped by each application, grouped by application; "package queries" here means the recipe packages each application
registers, `@rhythmsoftware/macroservice-query-recipes`), and **Build from Scratch** (criteria builder with grouping and
NOT). Parameterised queries and relative dates (Platform, built) are available to every CRM dataset. Schedules and
exports are Platform's. CRM ships recipes for: All Contacts, All Organizations, Contacts by Organization (with hierarchy),
Primary Contacts by Role, My Open Tasks, Overdue Tasks, Last Month's Calls, and each data-quality indicator
([05 §6](#6-05-functional-architecture-what-every-crm-capability-does)).

### 5. The 360 pages

**Contact 360** (metadata-driven, `lib/record-views/views/contact-360.json`):

- **Header:** name, number, type, status (with Deceased and Do Not Email/Mail/Call surfaced prominently, Ruled D-01),
  primary organization and role, preferred e-mail, phone and address, archived state, merged-from notice, duplicate
  warning, created and modified attribution (Ruled D-23).
- **CRM cards:** contact details (all typed e-mails, phones and addresses with preferred and linked/disconnected state),
  affiliations (Upcoming, Current, Past; Ruled D-10), relationships (when built), activities timeline, tasks, custom
  fields (rich text rendered safely, D-04), communication preferences, compliance-relevant roles, region, version history
  (including merged-from history, D-21).
- **Cards from other applications** (each owned by its application, contributed through layout metadata): memberships and
  chapters (Membership), invoices, payments and balance (Accounting), orders and entitlements (Commerce), registrations
  (Events), committee service (Committees), submissions (Submissions), certifications and credits (Certifications),
  communications sent (Platform communications), engagement (when designed). **Today:** fifteen of these tabs show "not
  available yet" in live mode; only Accounting contributes. A tab is shown only when its application is licensed and its
  card is live (P-9).
- **Actions:** Edit, Log Activity, Create Task, Change Type, Change Status, Archive/Unarchive (when built), Merge (with a
  record picker, D-03), Copy Link, Version History; later Delete (CRM-M20, impact panel). Send E-mail, Print Profile and
  Create Order appear only when their capability is live (Ruled D-02, D-53). Print Profile and Alerts stay hidden until
  designed; Alerts is evaluated against Attention, Responsibilities and the personalised Home before any separate page
  exists, and Print Profile against the 360 presentation architecture (Ruled E-18). Every visible action has a real, authorised
  effect.

**Organization 360** (to be moved onto the same metadata-driven view system; it is bespoke today):

- Header with type, status, parent, compliance status and violations, key contacts by role, archived state.
- Cards: details and typed contact channels (D-36), contacts (Current, Past, All, Ruled D-07), hierarchy (parent,
  children, descendants count, without the 500-record scan), activities, tasks, compliance, custom fields, version
  history (missing today), and other applications' cards (organization memberships, invoices, orders).
- Actions: **Edit Organization** (no form exists today), Change Type (recalculates compliance), Archive, Merge, Add
  Contact (creates an affiliation), Add Subsidiary, Set Parent; Email All Contacts only when communications support it.

**Loading.** Header and first card render from one record read; other cards load independently with their own empty,
error and permission states; a card's failure never blanks the page. A contact at the documented limits (100 affiliations
and so on) must render (DEF-CRM-0031 currently answers 500).

### 6. Editing

- One contact editor and one organization editor, driven by the console form registry (`/console_forms`), per contact or
  organization type. **Organizations have no editor for existing records today.**
- Reference data comes only from the live services (communication types, custom-field definitions, duplicate checks,
  organization summaries); the sample `lib/contact-editor/mock/*` data must not be reachable in a live build.
- Pickers search the server; no picker loads an entire collection into the browser (today: organizations up to 2,000).
- Affiliation editing uses explicit add/edit/end/remove operations (D-09), with workspace-date defaults (D-12).
- Session expiry never discards dirty edits: silent refresh, draft retention, version-checked restore (Ruled D-17).
- After a 409, Reload or an explicit comparison; never a silent version bump (Ruled D-35).

### 7. Configuration screens

Under Configuration › CRM: Contact Types, Organization Types (applicable roles, compliance rules, notification template
and preview, reprocess with outcome), Affiliation Types (with default contact-information links and "Re-apply
defaults"), Contact Roles (descriptions, automatic assignment, administrative flag), Address Types, Phone Number Types,
(E-mail Types if CRM-Q6), Communication Types, Activity Types, Task Types, Task Queues (subject to CRM-Q1), Regions and
assignment rules, Duplicate Rules (create, edit, disable, delete, reorder), CRM Settings. Custom fields and record layouts
are Platform configuration screens that must persist through Platform's metadata and layout APIs. Engagement Scoring
(CRM-Q5) and CRM Processes are removed from live consoles until designed (P-9).

### 8. Live and mock

There is one switch, `NEXT_PUBLIC_MOCK_MODE`, for every application (Andrew, 2026-10-19). The CRM-specific
`NEXT_PUBLIC_CRM_API` and Commerce's `NEXT_PUBLIC_COMMERCE_API` switches are retired; sample data is reachable only in mock
mode; a real record opened in mock mode shows an intelligible notice (Ruled D-06).

## 11. 10. Data Operations: Imports, Upserts, Exports, Bulk Changes and Rollback

## 10. Data Operations: Imports, Upserts, Exports, Bulk Changes and Rollback

Part of [RHYTHM-CRM-ARCH-001](#1-rhythm-crm-arch-001-rhythm-2-0-crm-definitive-architecture-and-completion-plan). Evidence: [e03](#22-e03-rhythm-2-0-crm-backend-assessment-services-crm) §13,
[e04](#23-e04-crm-console-assessment-react-console) addendum D, [e05](#24-e05-platform-capabilities-crm-depends-on-and-every-other-application-s-references-to-crm-records) A.4–A.5.

### 1. One engine, CRM supplies operations

CRM has no import framework of its own and must not grow one. The shared **Data Job Engine**
(`@rhythmsoftware/macroservice-data-jobs`, Platform `modules/data-jobs`) runs every bulk change: file imports, upserts,
bulk updates from a selection, bulk deletes, migrations from 1.0 and integration-driven batches. CRM registers
**operations** (per object type and verb) that call the same domain aggregates the console calls, so an import row is
validated by exactly the rules a console save is (P-1; 1.0 pattern P9d). Platform owns lanes (interactive and bulk),
admission and capacity, uploads, progress, cancellation, results files and the console wizard.

**Registered today (Verified):** contacts update/insert/upsert; organizations insert/upsert/update (update is only the
reference pass of an import); seven configuration types; activities insert/upsert/update/delete; contact, organization and
activity bulk delete; the source-key crosswalk (`data_id_mappings`, `records_by_source_key`); contact-update rollback.

### 2. Behaviour every CRM data job must have

| #    | Requirement                                                                                                                                                                                                             | Source                                       | Today                                                                          |
| ---- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------- | ------------------------------------------------------------------------------ |
| J-1  | Validate before apply: the validate step runs the real aggregate in a rolled-back transaction; apply runs the same code; per-row outcomes created / updated / skipped / failed, with reasons, summing to the input rows | 1.0 P9a,b                                    | Verified for the interactive lane; **file-lane (bulk) jobs cannot be dry-run** |
| J-2  | Versions captured at validation and verified at apply; a record changed in between is skipped and named, with safe revalidate and retry                                                                                 | Ruled D-37, D-38                             | **Not built** (DEF-CRM-0682 fix reverted for conflicting with D-37)            |
| J-3  | Patch imports respect source revision order and the target's version                                                                                                                                                    | Ruled D-38                                   | Not built                                                                      |
| J-4  | Idempotent: a job item retried after a crash produces the same result; client-assigned ids for creates                                                                                                                  | Ruled D-43                                   | Verified (engine idempotency)                                                  |
| J-5  | Atomic import groups resolve references inside the group by dependency order while the group still succeeds or fails as a whole; cycles and unresolved references are actionable errors, never a hang                   | Ruled E-11 (shared engine)                   | Not built                                                                      |
| J-6  | Cancellation is acknowledged at once, says committed counts are still settling, and links to the final status; never reports zero committed when the count is unknown                                                   | Ruled E-12 (shared engine)                   | Not built                                                                      |
| J-7  | Large uploads are checksum-verified inside the processing job before any change; a failed checksum fails the job clearly; verification is never skipped for size                                                        | Ruled E-16 (shared engine, Platform uploads) | Not built                                                                      |
| J-8  | Every revision written by a data job names its job, with a readable label ("Data Import", "Bulk Update") and a link to the job; batch attribution kept for troubleshooting                                              | Ruled E-08                                   | Partial: revisions carry job attribution; label and link to confirm            |
| J-9  | Match keys configurable: id, source key, e-mail or any unique custom field                                                                                                                                              | 1.0 P9c                                      | Partial: id and source key; e-mail through upsert rules                        |
| J-10 | A 100,000-row file runs as a background job without timing out, with progress                                                                                                                                           | 1.0 P9e                                      | Verified (bulk lane)                                                           |
| J-11 | Bulk work admitted by a Platform capacity table, not a static per-service share                                                                                                                                         | —                                            | Not built (`platform.data_job_capacity` missing)                               |

### 3. Mapping spreadsheet columns to CRM's model

- **Typed collections (Ruled D-40).** A normal association spreadsheet must import with usable e-mail, phone and address:
  preferred e-mail, phone and address immediately, and a mapper for several typed values per row (work and personal
  e-mail, mobile and office phone, billing and mailing address). The core schema is never constrained to one item. Today
  these columns map only on contact insert and upsert, not on update and never for organizations.
- **Untyped columns (Ruled E-15).** An untyped phone or address column resolves to the tenant's explicitly configured
  default type. The preview shows the resolved type for every mapped column; a missing default is a validation error,
  never an invented type; importing additional e-mail addresses never changes an existing contact's preferred e-mail
  unpredictably; the rule that picks preferred values is deterministic and has its own tests; e-mail column labels match
  the model, so e-mail "types" are not offered while the model keeps none (see CRM-Q6). This belongs in the shared mapping
  architecture, not a CRM-only mapper.
- **Names.** A single "full name" column may be split into parts by the mapper, shown in the preview for confirmation;
  the domain service never parses names ([05 §2.1](#6-05-functional-architecture-what-every-crm-capability-does)).
- **Organizations by name.** A row naming an organization that matches two or more organizations is reported, never
  guessed (1.0 P5f).
- **Affiliations, roles, tasks.** Contact imports already carry `affiliations[]` with nested `roles[]`, resolved through the
  source-key crosswalk (`modules/data-jobs/services/imports/contact-references.ts`). Missing: a one-row-per-affiliation
  operation against existing contacts, and task import.

### 4. Bulk updates and deletes from a selection

- Selections come from Explorer "select all matching" (up to 10,000 ids) and hand off to the wizard; permissions
  `crm.<object>.bulk` plus the operation's own.
- Bulk update writes through the aggregate and triggers the same side effects as a console edit, including duplicate
  detection (through change events on every committed write; a proving test is owed) and compliance recalculation.
- **Mass-update templates** (1.0 had them): saved bulk updates with values or expressions and add/remove operations on
  collections ("add role R") are a later engine capability (DO-16).
- Bulk delete is capped at 500 per job, validated first, typed confirmation, two permissions, and irreversible
  (`hard_delete_not_reversible`). Until safe delete ([08](#9-08-record-lifecycle-and-safe-delete)) its impact covers only CRM's
  cascade. When safe delete lands, bulk delete splits into deletion requests of at most 500 subjects through the
  deletion-impact contract, and the wizard shows every participant's outcomes (record-lifecycle WP-E4).
- Bulk archive (Recommended) once archive lands: reversible, no impact contract needed.

### 5. Rollback

**Safe rollback versus irreversible delete** is a standing architectural distinction:

- A rollback restores what the job changed **only where nothing changed since**; a record changed after the job is
  reported as a conflict, never overwritten (Ruled D-39, guardrail §2.2).
- Today rollback restores a contact's own fields; **changed collections and custom fields are `not_reversible_operation`
  and organizations have no rollback at all** (`docs/debt/rollback-of-collections.md`). Target: rollback covers owned
  collections (by stable row id) and custom-field values for contacts and organizations, with the same conflict rule.
- Deletes and merges are never rolled back by the data job engine.
- Rollback and Retry for completed jobs with blocked rows are offered in the console to holders of `data.jobs.rollback`
  (Ruled D-39).

### 6. Exports

- List exports (Explorer, saved queries) are Platform's, paged or streamed, with no fixed row cap beyond the stated
  selection limit.
- The **tenant data export**: CRM's export provider is built (`aws/crm.export-datasets.ts`, 28 datasets with readers,
  coverage and hash tests, an integration spec). Platform's orchestration (`modules/data-exports`) was stopped on
  2026-10-19 by Andrew's one-app-at-a-time order; hard-deleted references are exported as dangling until safe delete exists.

### 7. Migration from Rhythm 1.0

Tenants move from 1.0 through data jobs (standing rule: no direct legacy import). The CRM operations above, the crosswalk
and the mapping rules carry the migration. 1.0 specifics to handle in mapping: fixed e-mail/phone/address slots → typed
rows; `organization_id` + `organization_history` → affiliations (current primary plus ended ones, roles moved onto
affiliations); `organization_name` without an id → reported for review, never a guessed link (the DHI case of 4,548
contacts); relationships → relationship records once built (CRM-Q2); `link_work_address_to_organization` → linked rows;
statuses carried; `duplicate` contacts listed for review.

## 12. 11. Security, Audit and Operational Contracts

## 11. Security, Audit and Operational Contracts

Part of [RHYTHM-CRM-ARCH-001](#1-rhythm-crm-arch-001-rhythm-2-0-crm-definitive-architecture-and-completion-plan). Evidence: [e03](#22-e03-rhythm-2-0-crm-backend-assessment-services-crm) §14–§15,
[e05](#24-e05-platform-capabilities-crm-depends-on-and-every-other-application-s-references-to-crm-records) A.6–A.7. "Knox v2" in the brief is Platform's security module and the
`@rhythmsoftware/macroservice-authorization` library (its successor).

### 1. Tenancy

Every CRM table is tenant-owned with forced, fail-closed row-level security; the tenant comes from the path and the
signed execution context; a record in another tenant answers 404 (Verified, `tests/integration/db/row-level-security*.ts`).
Cross-service SQL runs through `SECURITY DEFINER` functions granted to named roles only. Customer-scoped grants are
represented once and resolved per workspace in the authorizer, never expanded into a token entry per workspace (Ruled
D-13; Platform `32a818333`, adopted per service).

### 2. Permissions

**Today (Verified):** a permission catalog `crm.<resource>.<action>` with tiers read, user and admin; application access
(`crm.application.access`) and licensing enforced; data jobs require `.bulk`; no route names a resource type, so every
check is tier-based; no field-level permissions; no built-in policies.

**Target:**

| Area                   | Rule                                                                                                                                                                                                                                        | Source                                                                         |
| ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------ |
| Record actions         | `view`, `create`, `edit`, `delete` per object; `archive` (`crm.contacts.archive`, `crm.organizations.archive`, admin tier); `merge` (power user); `bulk`; override of a Block duplicate rule; lock and unlock                               | Ruled D-02, D-08, D-32; record-lifecycle                                       |
| Relationship changes   | Removing or ending affiliations is permission-checked separately from editing a contact's own fields                                                                                                                                        | Ruled D-09                                                                     |
| Configuration          | Admin tier; changing the default affiliation type is admin-only                                                                                                                                                                             | Ruled E-03                                                                     |
| Staff access approvals | Only security administrators approve Rhythm staff access; enforced at the endpoint                                                                                                                                                          | Ruled D-14                                                                     |
| Effective permissions  | A reusable Access & Permissions view explains effective permissions, policy sources, workspace scope, and application entitlement separately                                                                                                | Ruled D-15                                                                     |
| Built-in policies      | Platform already generates read-only, user and administrator tier policies from CRM's catalog; CRM adds declared Contacts Editor and Data Steward (merge and bulk) policies                                                                 | Recommended                                                                    |
| Record-level scope     | Staff limited to records in their chapter, region or organization type, as 1.0's resource scoping allowed                                                                                                                                   | **Open** (CRM-Q8); needs Platform conditions on resource attributes            |
| Field-level            | Restrict view or edit of sensitive fields (date of birth, custom fields marked sensitive)                                                                                                                                                   | Platform capability; CRM declares sensitivity in metadata (Recommended, later) |
| Portal                 | Members read and edit their own profile; organization administrators manage their organization and its descendants. Platform Security owns the grant (derived from CRM affiliation roles through a published read); CRM's routes enforce it | 1.0 pattern P10; CRM-ADR-11; milestone CRM-M22                                 |

Every permission is enforced by the service; a button's visibility is a convenience, never the control (guardrail §2.4).

### 3. Locks (Ruled D-08, E-13, E-14)

- **Customer locks** (`sys_locked`: updates-and-deletes, or deletes only) protect a record a tenant chose to protect.
  Every lock transition, both kinds, goes through one audited lock/unlock operation that records who, when, and the
  previous and new states (E-14). Setting a full lock is restricted; unlocking is narrowly authorised (D-08).
- **Protected system records** are only those whose continued existence a platform invariant requires; they cannot be
  edited or deleted by anyone in the tenant. Ordinary tenant defaults (default roles, default types) are not protected
  (E-13).
- A lock never freezes a record's position in an ordered list; reordering stays possible because order is not the
  protected record's substance (E-14, D-08).
- Existing locked dev records are corrected through an audited repair (D-08).

### 4. Audit and history

| Trail                                   | What it records                                                                                       | Today                | Target                                                                                                                                               |
| --------------------------------------- | ----------------------------------------------------------------------------------------------------- | -------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- |
| Record history (`crm.record_revisions`) | Field-level changes with actor snapshot, source (console, API, job, system) and job attribution       | Contacts, activities | Plus organizations, tasks, configuration types and roles, regions, duplicate rules; data-job revisions labelled and linked to their job (Ruled E-08) |
| Merge record                            | Every participant's original values, decisions, references moved                                      | Verified (D4)        | Field-level outcome visible (D-30)                                                                                                                   |
| Platform audit events                   | `record.viewed`, `record.merged` pointers; security events (logins relinked, grants)                  | Verified             | `record.archived`, `record.deleted`, lock transitions                                                                                                |
| Deletion record                         | Who, when, why, object type and id, record number, per-participant dispositions; **no personal data** | Not built            | CRM-M20 (D-42)                                                                                                                                       |

**Readable history (Ruled E-07).** History stays meaningful when an associated record is archived or renamed: revisions
keep a readable label for referenced records (resolved live where the record exists, with a stored display snapshot as
fallback). What personal data survives a **hard delete** is decided only in the safe-delete milestone, and the stored
display snapshot must follow those rules. Audit records reflect the operation actually performed: a genuine edit of a
phone row keeps its identity; a replacement shows removal and addition (Ruled D-22).

**Visibility.** A reader permitted to see a record sees its history and merge details, subject to the same field
restrictions (Ruled D-21).

### 5. Privacy and SOC 2

- Personal data classification: contacts and their contact information are PII; organization data is not personal except
  where it names people.
- A hard-deleted contact's revisions keeping full personal data is a known gap until CRM-M20 (TC-DAT-003, TC-PRV-003), to
  be recorded in `services-crm:docs/compliance/exceptions.md`.
- **Date-bound risk:** from **2026-11-07** services-crm's `npm run compliance:check` fails `validate` while expired High
  exceptions remain, which would stop every merge to master (`docs/debt/soc2-remediation-windows.md`). The CRM build session
  puts the expiring exceptions to Andrew as one decision set (enable the controls, or renew with a new expiry) in the
  retroactive CRM-M6 plan, before that date.
- `master` branch protection and secret/dependency scanning are not configured for services-crm (administrator settings;
  [17](#18-17-risks-unresolved-items-and-verification-limits)).

### 6. Operations

Performance budgets, logging and dead-letter handling: [06 §6](#7-06-api-and-service-architecture).
Every long operation (organization merge, region recalculation, compliance reprocess, data jobs, deletions) is a durable
job with progress, per-item outcomes and an operator alert on failure.

## 13. 12. Cross-Application Integration Contracts

## 12. Cross-Application Integration Contracts

Part of [RHYTHM-CRM-ARCH-001](#1-rhythm-crm-arch-001-rhythm-2-0-crm-definitive-architecture-and-completion-plan). Evidence: [e05](#24-e05-platform-capabilities-crm-depends-on-and-every-other-application-s-references-to-crm-records) Part B (the full
column-by-column table).

### 1. The rule

CRM owns the identity of contacts and organizations and the relationships among them. Every other application owns its
own records and stores references to CRM identities as plain ids (no cross-schema foreign keys exist anywhere, by design).
Integrity across applications therefore comes from three contracts, not from the database:

1. **Merge participation**: every holder of a reference rewrites or preserves it inside CRM's merge transaction
   ([07 §4](#8-07-duplicate-detection-and-record-merging)).
2. **Deletion impact**: every holder answers block, retain, detach, redact or delete before a hard delete (CRM-M20,
   [08](#9-08-record-lifecycle-and-safe-delete)).
3. **Published reads**: other applications read CRM through published views and service functions, which carry a
   compatibility promise; reading CRM's physical tables stays allowed (standing decision 2026-10-08) but without it.

Synchronous, relational integrity is preferred to asynchronous synchronisation (brief §3); events are for notification,
not for keeping copies in step.

### 2. Ownership and contract per application

| Application                                | Owns                                                                                                                | References to CRM (main columns)                                                                                                 | Contract today                                                                                                              | Target contract                                                                                                                                                                            | On CRM change                             | On merge (target)                                                                           | On delete (target, CRM-M20)                                                                                                                 |
| ------------------------------------------ | ------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ----------------------------------------- | ------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------- |
| **Platform**                               | Users and portal logins, security grants, communications, audit, attention, responsibilities, Quick Search, queries | `users.contact_id`, grants, attention subjects, communication recipients, audit                                                  | Merge participant (required for contacts); Quick Search indexer invoked by CRM                                              | Same; deletion participant (logins `block`)                                                                                                                                                | Indexer re-reads the record               | Relink one login, block two (D2); grants block                                              | Logins block; audit and communications retain; search delete                                                                                |
| **Membership**                             | Memberships, applications, organizational units, rosters, snapshots                                                 | `memberships.contact_id/organization_id`, applications, unit organizations                                                       | **None** (no participant; reads CRM tables directly; CRM change handler built but not invoked; contact-update port stubbed) | Participant for contacts and organizations; invoke its change handler for affiliation, role and organization changes; read through published views; call CRM's `contacts` service function | Flow-down memberships re-derive           | Reassign ownership; consolidate duplicates per Membership's rules (1.0 kept one membership) | Current memberships block; past retain                                                                                                      |
| **Accounting**                             | Invoices, payments, credits, subledger, Stripe customers                                                            | `bill_to_kind/bill_to_id` on fourteen document tables (write-once on issued documents), subledger, `stripe_customers.contact_id` | **None**; its own plan requires "block until registered"                                                                    | Participant: issued documents preserve history (resolved through aliases), open documents block or reassign per Accounting's policy; Stripe mapping kept under survivor                    | Names resolved at read time through views | Preserve history / block                                                                    | Open receivables, pending payment intents and active schedules block; financial history retain; Stripe customer detach; never delete (D-41) |
| **Commerce**                               | Orders, checkouts, entitlements, promotions                                                                         | buyer, bill-to, recipient, holder ids                                                                                            | **None**; reads CRM addresses and affiliations directly                                                                     | Participant; published reads for addresses and affiliations                                                                                                                                | —                                         | Reassign open checkouts and active entitlements; preserve placed orders                     | Open orders, active entitlements and usable promotion codes block; open checkouts delete; placed orders retain                              |
| **Committees**                             | Committees, appointments, meetings, votes                                                                           | appointments, derived grants, participation history; representing/reserved organizations                                         | Participant for **contacts only**                                                                                           | Add organizations participant                                                                                                                                                              | Re-sync derived grants                    | Reassign appointments (block a seat held twice); preserve governance history                | Current appointments block; governance retain                                                                                               |
| **Submissions**                            | Submissions, reviews                                                                                                | applicant, participant, reviewer, actor ids                                                                                      | **None**                                                                                                                    | Participant                                                                                                                                                                                | —                                         | Reassign open submissions and assignments; preserve decisions                               | In-flight submissions and open reviews block; decisions retain                                                                              |
| **Events**                                 | Registrations, attendees (when built)                                                                               | registrant contact, organization                                                                                                 | Not on master                                                                                                               | Participant from first release                                                                                                                                                             | —                                         | Reassign                                                                                    | Future registrations block; past retain                                                                                                     |
| **Certifications, Communities, Marketing** | Their own records (not yet built in 2.0)                                                                            | Contact ids                                                                                                                      | —                                                                                                                           | Participant and published reads from first release (Recommended: a CI gate that refuses a new application with contact or organization references and no participant)                      | —                                         | —                                                                                           | —                                                                                                                                           |
| **Data Operations**                        | Job engine                                                                                                          | Job items name record ids                                                                                                        | CRM operations                                                                                                              | Same                                                                                                                                                                                       | —                                         | Items retain ids (aliases resolve)                                                          | Bulk delete through the impact contract                                                                                                     |
| **Responsibilities and Attention**         | Responsibility assignments, attention items                                                                         | Subjects may be CRM records                                                                                                      | Platform participant entries (block / reassign)                                                                             | CRM may supply attention providers (optional)                                                                                                                                              | Provider resolves `record_deleted`        | Reassign                                                                                    | Responsibility assignments block; attention items delete                                                                                    |
| **Integration Platform**                   | Connectors, event subscriptions                                                                                     | External system ids mapped to CRM ids                                                                                            | Business event `crm.contact.merged` only                                                                                    | All catalogued business events; activities written through CRM's service function                                                                                                          | Subscribers notified                      | `merged` events with survivor                                                               | `deleted` events (ids only)                                                                                                                 |
| **Portals (Web Experience Platform)**      | Pages                                                                                                               | Members' linked contact                                                                                                          | Sign-up SQL functions                                                                                                       | CRM portal routes (milestone CRM-M22)                                                                                                                                                      | —                                         | Login relink (Platform)                                                                     | Login blocks                                                                                                                                |

### 3. Merge

See [07 §4](#8-07-duplicate-detection-and-record-merging). Until
participants exist, such merges are blocked if Andrew agrees (CRM-Q3). Participants are built in each application's own
milestone plan.

### 4. Reads of CRM data by other applications

Standing decision (Andrew, 2026-10-08): every macroservice may read any table; only the owner writes. That decision
stays, but it couples other applications to CRM's physical columns (Accounting's name views, Membership flow-down,
Commerce checkout reads). **Recommended:** CRM publishes a small set of stable read views (`crm.party_names`,
`crm.current_affiliations`, `crm.party_addresses_effective`, `crm.party_status`) with a compatibility promise, and
other applications move to them; physical tables may still be read, without that promise.

### 5. Events

CRM publishes business events (ids and changed field names, no personal data) for contact and organization created,
updated, deleted, merged, archived and unarchived, through its outbox and Platform extensions. Consumers subscribe
through the Integration Platform. Change capture remains CRM-internal except for named handlers CRM invokes (Quick
Search, Membership). See [06 §5](#7-06-api-and-service-architecture).

### 6. Naming in shared contracts

"Customer" means Rhythm's paying customer in Platform and Websites, and the billed contact or organization in Accounting.
Shared contracts say **contact**, **organization** or **party** (either), never "customer", for a CRM record.

## 14. 13. Decisions: Standing, Recorded, and Needing Andrew (Deliverable D)

## 13. Decisions: Standing, Recorded, and Needing Andrew (Deliverable D)

Part of [RHYTHM-CRM-ARCH-001](#1-rhythm-crm-arch-001-rhythm-2-0-crm-definitive-architecture-and-completion-plan).

### 1. Standing decisions this architecture treats as constraints

| Source                                               | Decisions                                                                                                                                                                                                                                                                                                                                                                                                                                                                   | Where they bind                                                                                                                                                                                                          |
| ---------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| Affiliations design (Andrew, 2026-10-13/14)          | `is_primary` on the affiliation; the contact stores no organization; one Organization Affiliations section; reference-based contact information; type-driven links; affiliation-type defaults                                                                                                                                                                                                                                                                               | [05 §2.3–2.4](#6-05-functional-architecture-what-every-crm-capability-does)                                                                                                                                                             |
| Record merging (D1..D10, 2026-10-11..14)             | Participant functions in one transaction; relink one login, block two; 410 with survivor; snapshots kept for the tenant's life; no 1.0 calibration data; most restrictive consent wins; 100,000-employee ceiling; long merges accepted and warned; **unknown references block the merge** ("settled by inspection")                                                                                                                                                         | [07](#8-07-duplicate-detection-and-record-merging)                                                                                                                                                                            |
| Names (2026-10-09)                                   | Computed names; no name parsing on write                                                                                                                                                                                                                                                                                                                                                                                                                                    | [05 §2.1](#6-05-functional-architecture-what-every-crm-capability-does)                                                                                                                                                                     |
| Key roles on organization types removed (2026-10-11) | Organization types no longer carry key roles                                                                                                                                                                                                                                                                                                                                                                                                                                | [14 L1-21](#15-14-implementation-gap-matrix-deliverable-b)                                                                                                                                                                                          |
| Ids standard (2026-10-19)                            | Configuration string ids; record UUIDv7; never ask for a waiver                                                                                                                                                                                                                                                                                                                                                                                                             | [04 §1](#5-04-domain-and-relational-data-model)                                                                                                                                               |
| Console one live switch (2026-10-19)                 | All apps live or all mock; no per-app switches                                                                                                                                                                                                                                                                                                                                                                                                                              | [09 §8](#10-09-console-experience-search-explorer-queries-and-the-360-pages)                                                                                                                                                                        |
| Stabilization first (2026-10-19 ~16:05 ET)           | No new building until acceptance is green for what is built                                                                                                                                                                                                                                                                                                                                                                                                                 | [15 §2](#16-15-milestone-plan-completion-roadmap-deliverable-c)                                                                                                                                                                     |
| Focus order (2026-10-19 ~15:55 ET)                   | CRM, Accounting and the Integration Platform build and accept; every other application is merge-only for now                                                                                                                                                                                                                                                                                                                                                                | [15 §2](#16-15-milestone-plan-completion-roadmap-deliverable-c)                                                                                                                                                                     |
| Milestone standard R2-DEV-001 v1.1 (2026-10-19)      | Vertical-slice milestones; approved plan before build; one in implementation per application; hands-on acceptance                                                                                                                                                                                                                                                                                                                                                           | [15](#16-15-milestone-plan-completion-roadmap-deliverable-c)                                                                                                                                                                                           |
| Safe delete timing (2026-10-19 ~16:50 ET)            | Cross-service safe delete is a later milestone of this architecture; archive now; today's delete stays until then                                                                                                                                                                                                                                                                                                                                                           | [08 §1](#9-08-record-lifecycle-and-safe-delete)                                                                                                                                              |
| Acceptance rulings D-01..D-53 (2026-10-19)           | Lifecycle, write contracts, authorization, merge integrity, console record experience, Explorer, data operations, effective dates                                                                                                                                                                                                                                                                                                                                           | Throughout; status in the acceptance decision register                                                                                                                                                                   |
| Acceptance rulings E-01..E-18 (2026-10-19 ~17:20 ET) | Compliance on type change; referenced roles; default affiliation type admin-only; minimum-zero rules; custom fields in configuration editors; seasonal month/day; readable history; job-linked revisions; Rhythm-wide update contract; Platform-validated assignees; dependency-ordered import groups; async cancel; unprotected defaults; audited locks; untyped import columns; in-job checksum; relationship-aware query criteria later; Print Profile and Alerts hidden | [04](#5-04-domain-and-relational-data-model), [05](#6-05-functional-architecture-what-every-crm-capability-does), [06](#7-06-api-and-service-architecture), [09](#10-09-console-experience-search-explorer-queries-and-the-360-pages), [10](#11-10-data-operations-imports-upserts-exports-bulk-changes-and-rollback), [11](#12-11-security-audit-and-operational-contracts) |
| Payment model (2026-10-19 ~16:20 ET)                 | After acceptance, one payment allocates across customers; no CRM work                                                                                                                                                                                                                                                                                                                                                                                                       | none in CRM                                                                                                                                                                                                              |

### 2. Architecture decision records (made here; internal and cheap to reverse unless marked)

| #          | Decision                                                                                                                                                                                                                                                                                                                                                                                                                                                   | Alternatives considered                        | Consequence                                                                                            |
| ---------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------- | ------------------------------------------------------------------------------------------------------ |
| CRM-ADR-01 | Adopt the affiliations, merging, record-lifecycle and write-contract designs as binding sub-specifications, amended only by later rulings, which are named where they apply                                                                                                                                                                                                                                                                                | Rewrite them here                              | One source per topic                                                                                   |
| CRM-ADR-02 | Every application holding contact or organization ids ships a merge participant (and a deletion participant with safe delete), built in **that application's own milestone plan**; a CI gate refuses a new application with such references and no participant                                                                                                                                                                                             | Asynchronous repointing (rejected in D1)       | Closes 1.0 pattern P1 at the source                                                                    |
| CRM-ADR-03 | One match-or-create function used by every entry path, built on the duplicate rules; quick-add from shared pickers calls it                                                                                                                                                                                                                                                                                                                                | Per-channel matching (1.0)                     | Closes 1.0 pattern P4                                                                                  |
| CRM-ADR-04 | **Update contract (follows Ruled E-09, D-09):** CRM's ordinary update changes only what it names (omitted unchanged, explicit null clears); full replacement becomes a separately named operation. This changes the published behaviour of CRM's `PUT` (today a full replace). Clients are inventoried and migrated before enforcement                                                                                                                     | Keep `PUT` as replace and add a partial update | Clients relying on `PUT` clearing omitted fields must send explicit nulls or use the replace operation |
| CRM-ADR-05 | Other applications **should** read CRM through published, compatibility-promised views and service functions; reading physical tables stays allowed (standing decision 2026-10-08) without that promise                                                                                                                                                                                                                                                    | Forbid physical reads                          | CRM can evolve its schema without breaking readers who moved                                           |
| CRM-ADR-06 | The organization hierarchy becomes queryable through a maintained ancestor relation (closure table or recursive view, chosen by measurement)                                                                                                                                                                                                                                                                                                               | Stored ancestor arrays (1.0; went stale)       | Queries, eligibility and exports can say "and everything under it"                                     |
| CRM-ADR-07 | No CRM import framework: CRM registers operations in the shared Data Job Engine; the engine's rulings (D-37, D-38, E-11, E-12, E-15, E-16) are built by the engine's owner and verified against Accounting's and Membership's operations before CRM adopts them                                                                                                                                                                                            | A CRM importer                                 | One import behaviour across applications                                                               |
| CRM-ADR-08 | Data-quality indicators are saved queries with remediation actions, not a rules engine                                                                                                                                                                                                                                                                                                                                                                     | A generic data-quality framework               | Cheap to add                                                                                           |
| CRM-ADR-09 | Organizations gain a preferred flag on phone numbers and a typed e-mail collection                                                                                                                                                                                                                                                                                                                                                                         | Leave as 1.0                                   | Parity with contacts; enables `info@` addresses                                                        |
| CRM-ADR-10 | Marketing-integration engagement arrives as activities of a system type through CRM's activity service function                                                                                                                                                                                                                                                                                                                                            | A separate engagement store                    | One timeline                                                                                           |
| CRM-ADR-11 | Portal authorization is Platform Security's: Security grants an organization-administrator capability scoped to an organization and its descendants, and may derive it from CRM affiliation roles through a published CRM read; CRM's portal routes enforce what Security resolves (affiliations README §10)                                                                                                                                               | CRM owns a role-to-permission matrix (1.0)     | One owner for permissions                                                                              |
| CRM-ADR-12 | **Merge block (Andrew, CRM-Q3, 2026-10-19 17:48 ET, option 1):** a merge is blocked when a merged-away record is referenced by an application with no merge participant for that object type; the plan names the application and counts; merges of unreferenced records proceed; no override; the block lifts automatically when the participant ships; a CI test fails on a new referencing column without coverage. Built in the CRM-M4 plan (row MG-12) |                                                |                                                                                                        |

### 3. Decisions that need Andrew

Each decision below says what it governs and what happens today, why it matters now, the realistic options, and the
recommendation. They are asked in the delivery message in the same terms.

#### 3.1 CRM-Q1 — Task queues: keep as work pools, or retire them

**What it governs.** A task queue is a named pool of work (for example "Membership Desk"). Today a task can sit in a
queue without a person assigned, members of the queue can claim it, Accounting's product templates create tasks into a
queue, and the console shows one list view per active queue. The brief said queues had been removed in favour of saved
views; the code keeps them, and the only recorded decision (application processes' D-09) says workflow-created tasks do not
use queues.

**Why now.** It decides the task model before the activities-and-tasks milestone, and whether Accounting's templates must
change.

**Options.**

1. _Keep queues as pools; retire the queue-specific screens_ (Recommended). Unassigned work can still wait for a team; the
   per-queue views become ordinary saved views filtered by queue. Nothing breaks for Accounting.
2. _Retire queues._ One concept fewer, but unassigned team work needs another home (a role, a responsibility), Accounting's
   templates must change, and existing tasks must be reassigned.
3. _Keep everything as it is._ No work, but the console keeps a second way of listing tasks beside saved views.

**Blocks.** CRM-M17 (activities and tasks completion).

#### 3.2 CRM-Q2 — Relationships and households

**What it governs.** 1.0 let staff record typed relationships between any two people or organizations (parent of,
partner of, spouse) with dates. 2.0 has employer-style affiliations and a single spouse field, which ruling D-19 says is an
interim fix until "one canonical relationship … compatible with the broader relationships/household model" exists.

**Why now.** It sets whether spouse stays a special field and whether households (shared mailings, family memberships)
need a new party kind.

**Options.**

1. _Build typed relationships in CRM-M21, before portal self-service; spouse becomes a relationship type; households are an
   organization of a system type "Household" with member affiliations_ (Recommended). One party model; Membership and
   Commerce can bill a household with no new kind of record.
2. _Relationships only, no households._ Smaller; family memberships stay a Membership concern.
3. _Defer entirely._ Spouse stays a special field; 1.0's relationships are not migrated.

**Blocks.** CRM-M21.

#### 3.3 CRM-Q4 — Member and organization directories

**What it governs.** 1.0 published searchable people and organization directories on the portal, with chosen fields,
filters, radius search, eligibility, comments and messaging. They leaked data (search skipped its security check; a 2024
incident exposed dates of birth). Nothing exists in 2.0.

**Why now.** Customers used them; deciding scope prevents an ad-hoc rebuild with the same flaws.

**Options.**

1. _In scope after portal self-service: a Web Experience page over a CRM-published directory projection that applies
   opt-out, status and the field allowlist before any template_ (Recommended).
2. _Out of scope for 2.0 launch._ Customers lose directories until later.
3. _Port 1.0's model._ Fast to describe, repeats the leaks.

**Blocks.** Nothing scheduled; a later candidate.

#### 3.4 CRM-Q5 — Engagement scoring

**What it governs.** 1.0 scored each contact and organization monthly from weighted metrics across six applications. The
2.0 console shows an Engagement Scoring screen backed by sample data that claims "scores recalculate overnight"; no
backend exists. The fake screen is removed under the live-console rule regardless of this decision.

**Options.**

1. _Retire for launch; design engagement later as its own capability spanning Membership, Events, Commerce and
   Communications_ (Recommended).
2. _Rebuild 1.0's model in CRM._ CRM would read six applications' data.

**Blocks.** Nothing now.

#### 3.5 CRM-Q6 — E-mail address types

**What it governs.** Phones and addresses have configurable types (Work, Home…); e-mail addresses are three ordered slots
without types. Spreadsheets often carry "work e-mail" and "personal e-mail".

**Options.**

1. _Add configurable e-mail types_ (Recommended): work and personal columns (D-40) land on a type; queries can say
   "work e-mail".
2. _Keep untyped slots:_ E-15 then keeps the importer honest by offering no e-mail types.

**Blocks.** CRM-M14.

#### 3.6 CRM-Q7 — Exclusive (single-holder) contact roles

**What it governs.** Compliance rules warn when an organization has too many holders of a role but never prevent it. In
1.0 one customer had 828 organizations with several Owners, so dues invoices went to arbitrary people.

**Options.**

1. _An opt-in "exclusive" flag on a role, enforced by the database: at most one current holder per organization;
   assigning a second requires ending the first in the same save_ (Recommended).
2. _Advisory compliance only (today)._

**Blocks.** CRM-M14.

#### 3.7 CRM-Q8 — Record-level staff scoping for launch

**What it governs.** 1.0 could limit a staff member's CRM access to records of a chapter, section or organization type.
2.0 permissions are tier-only: a user sees every contact in the workspace.

**Options.**

1. _Required for launch for enterprise customers; built as a Platform capability (conditions on record attributes) that
   CRM declares_ (Recommended).
2. _Not for launch._ Chapter-based associations cannot restrict staff.

**Blocks.** CRM-M23.

#### 3.8 CRM-Q9 — Inapplicable contact roles: filter only, or refuse

**What it governs.** An organization type lists the contact roles that apply to it. Today (affiliations design §5.6) that
list only filters the role picker; a save through the API or an import with an inapplicable role is accepted. In 1.0 the
rule was enforced in the console but not the portal (RB-5829), which customers reported.

**Options.**

1. _Refuse an inapplicable role on save in every channel, naming the role and type_ (Recommended): one rule everywhere.
2. _Filter only (today):_ imports and integrations can still assign roles the type does not allow.

**Blocks.** CRM-M14.

#### 3.9 CRM-Q10 — Hard delete until safe delete exists

**What it governs.** Andrew decided that today's delete stays as it is until the safe-delete milestone. Today, a contact or
organization can be hard-deleted through the API and **through the console's bulk-delete data job from a list selection**
(up to 500 at a time), with no check of other applications. Rulings D-41 and D-44 are P0 ("before a production release"),
so until safe delete ships CRM can pass acceptance but is not production-ready for customers who hard-delete.

**Why now.** It is the one open path that strands invoices, memberships and committee seats through the console.

**Options.**

1. _Keep as decided:_ no change.
2. _Hide contact and organization bulk delete from live consoles until safe delete; keep the API for administrators_
   (Recommended): closes the console path, keeps support's tool.
3. _Restrict the delete permissions to a system role until safe delete:_ closes every path; support must use a system
   identity.

**Blocks.** The acceptance gate (LC-02).

**Decided (Andrew, 2026-10-19 17:47 ET): option 1, keep as decided.** Console bulk delete and API delete stay until safe delete (CRM-M20) ships; LC-02 is closed as "no change". CRM can pass acceptance; production readiness for customers who hard-delete still waits on safe delete (D-41, D-44).

#### 3.10 CRM-Q11 — The 1.0 Sales application

**What it governs.** 1.0 had a Sales application (leads converting to contacts and opportunities, pipelines, stages,
forecasts, sales activities). Nothing exists in 2.0 and the brief does not mention it.

**Fact needed.** Does any customer use it?

**Options.** _Out of scope for CRM 2.0; a separate application if wanted_ (Recommended); or _fold leads and opportunities
into CRM_.

**Blocks.** Nothing.

#### 3.11 CRM-Q3 — Block merges that would strand other applications' references

**What it governs.** When two contacts or organizations are merged, CRM rewrites every reference in its own schema and in
the applications that have a merge participant (contacts: Platform and Committees; organizations: Platform). Accounting,
Commerce, Membership and Submissions (and Committees for organizations) have none yet, so a merge today commits and leaves
their invoices, orders, memberships and submissions pointing at a person who no longer exists, which was 1.0's worst
recurring failure. The merge design records "unknown references block the merge" as settled by engineering practice, and
Accounting's plan requires a block, but R2-DEV-001 counts only a recorded decision of Andrew's as approval.

**Why now.** Merges are live in dev and in the console; every merge of a referenced record strands data until the other
applications build participants, which Andrew's focus order postpones.

**Options.**

1. _Block only those merges_ (Recommended): a merge is blocked when a merged-away record is referenced by an application
   with no participant for that object type; the plan names the application and the counts ("Accounting holds 3 invoices
   and 1 payment for this contact and cannot yet be updated by a merge"); merges of records nobody else references proceed;
   no override; the block lifts automatically when the application ships its participant; a CI test fails if a new
   referencing column appears without coverage.
2. _Block all merges_ until every application participates: safest, but stops the data-quality work customers need.
3. _Allow merges as today:_ references keep being stranded.

**Blocks.** Retroactive CRM-M4 (Duplicates and merge) plan; recorded as CRM-ADR-12 once answered.

**Decided (Andrew, 2026-10-19 17:48 ET): option 1, block only those merges.** Recorded as CRM-ADR-12 and in the CRM-M4 milestone record on services-crm master.

### 4. Questions deferred to a later milestone

The record-lifecycle design's Q-1..Q-7 ([08 §5](#9-08-record-lifecycle-and-safe-delete))
are asked when the safe-delete milestone is planned. Computed custom fields (CF-06) is asked with the custom-fields
milestone.

### 5. Conflicts surfaced

| #   | Conflict                                                                                                                                                                                                                                                                                                                                                     | Sources                                   | Treatment                                                                                                                                                                                                                      |
| --- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ----------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| C-1 | The brief says task queues were removed; the code keeps them and Accounting uses them                                                                                                                                                                                                                                                                        | Brief; e05 A.10                           | CRM-Q1                                                                                                                                                                                                                         |
| C-2 | Ruling E-09 changes CRM's documented `PUT` replace semantics and the write-contracts standard written earlier the same day                                                                                                                                                                                                                                   | E-09; `docs/entities-api.md`; standard W3 | Treated as superseding; CRM-ADR-04; the standard is updated by its owner                                                                                                                                                       |
| C-3 | Ruling E-13 (ordinary defaults not protected) narrows D-08's "protected seeds remain protected"                                                                                                                                                                                                                                                              | D-08; E-13                                | Read together: only records a platform invariant requires are protected                                                                                                                                                        |
| C-4 | The services-crm merge coverage document says other applications are "not in 2.0 yet"                                                                                                                                                                                                                                                                        | `docs/merging/relationship-coverage.md`   | Out of date; replaced by a generated matrix                                                                                                                                                                                    |
| C-5 | `docs/debt/contact-roles.md` says a tenant starts with no roles; two default roles are seeded                                                                                                                                                                                                                                                                | e03 §7                                    | Doc fix                                                                                                                                                                                                                        |
| C-6 | Affiliation mutations: affiliations README §4/§6 (written only through the contact aggregate; full replacement or JSON Patch), record-lifecycle 01 §3 (`remove` in the contact patch under `crm.contacts.update`) and write-contracts W3 (explicit routes with their own permission keys) disagree; D-09 requires explicit, permissioned, audited operations | Three designs; D-09                       | Follow D-09 and W3: add, update, end and remove are explicit routes that execute inside the contact aggregate (contact row lock and version); end and remove carry their own keys; the two designs are amended by their owners |
| C-7 | Ruling D-25 (removed source → `disconnected`) amends affiliations README §0b.2 item 5 (removed source → `copied`)                                                                                                                                                                                                                                            | D-25; affiliations README                 | D-25 wins; the README is amended                                                                                                                                                                                               |
| C-8 | Write-contracts W5 (existing id → 409 `record_exists`) versus a replayed answer                                                                                                                                                                                                                                                                              | Standard W5; D-43                         | W5 is followed ([06 §2](#7-06-api-and-service-architecture) A-2)                                                                                                                          |

## 15. 14. Implementation Gap Matrix (Deliverable B)

## 14. Implementation Gap Matrix (Deliverable B)

Part of [RHYTHM-CRM-ARCH-001](#1-rhythm-crm-arch-001-rhythm-2-0-crm-definitive-architecture-and-completion-plan). Every significant capability, its target, its evidence, and what remains.

**Evidence baselines:** services-crm `origin/master` `bb55ae7a` (re-baselined after review; the first read was
`a980b553`); react-console `origin/master` `057ee80fc` (no CRM screen changed since `1f345862b`, which contains repair wave
3); services-platform master `339fb04f1`; acceptance decision register on react-console branch `product-acceptance-crm`.
Review findings that changed rows are in [review/](review/).

**Status vocabulary:** **Complete** (behaviour, test and usable console), **Complete (service)** (service half on master;
console half pending), **Partial**, **Backend only**, **Simulated** (screen over sample data, or an inert control),
**Scaffolded**, **Missing**, **In retro** (being finished inside a retroactive milestone CRM-M1..M8 by the CRM build
session under a ruling), **Decision** (waits on a CRM-Q), **Unknown**.

**MS** = the CRM milestone that closes the row ([15](#16-15-milestone-plan-completion-roadmap-deliverable-c)): M1..M8 are the retroactive milestones
(built work), M9..M23 the future ones; "ext" = another application's own plan. Severity: **C** critical, **H** high, **M**
medium, **L** low. Paths are services-crm unless prefixed `C:` (react-console) or `P:` (services-platform).

### Contacts (CO)

| ID    | Capability                                                                       | 1.0                       | 2.0 evidence                                                         | Status             | Gap                                         | Sev | Work                                 | Testing             | MS    |
| ----- | -------------------------------------------------------------------------------- | ------------------------- | -------------------------------------------------------------------- | ------------------ | ------------------------------------------- | --- | ------------------------------------ | ------------------- | ----- |
| CO-01 | Computed names from parts ([05 §2.1](#6-05-functional-architecture-what-every-crm-capability-does)) | computed, overwrote input | `services/persistence/contacts.ts:361`; `contacts-names.e2e.spec.js` | Complete           | `NOT VALID` check unremediated              | L   | report, then validate the constraint | need migration test | M15   |
| CO-02 | Contact types with layouts and forms                                             | none in 1.0               | `db/contact-types.table.ts`; e2e                                     | Complete           | —                                           | —   | —                                    | have                | M1    |
| CO-03 | Numbers and external ids                                                         | number = id               | `contacts.table.ts:124`; DEF-CRM-0643                                | Complete           | —                                           | —   | —                                    | have                | M1    |
| CO-04 | Statuses and personal data                                                       | enum, no rules            | `contacts.table.ts:50,140`                                           | Partial            | districts and socials unfilled (enrichment) | L   | see L1-12                            | —                   | later |
| CO-05 | Reciprocal spouse (D-19 interim)                                                 | one-way `spouse_id`       | `74158f19`                                                           | Complete (service) | console                                     | M   | console                              | have service        | M1    |

### Organizations (OR)

| ID    | Capability                                                                                                      | 1.0                           | 2.0 evidence                                                                                                                                                                   | Status             | Gap                                              | Sev   | Work                                                | Testing              | MS  |
| ----- | --------------------------------------------------------------------------------------------------------------- | ----------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------ | ------------------------------------------------ | ----- | --------------------------------------------------- | -------------------- | --- |
| OR-01 | Records, types, hierarchy reads, no cycles ([05 §2.2](#6-05-functional-architecture-what-every-crm-capability-does))          | stale ancestors               | `organizations.table.ts`; migration `0018`; ancestry specs                                                                                                                     | Complete (service) | —                                                | —     | —                                                   | have                 | M1  |
| OR-02 | Edit an existing organization in the console                                                                    | web-form edit                 | only Change Type is live (`C:lib/organizations/update.ts:4-60`); "Edit Organization" toast-only                                                                                | Partial            | no general edit form                             | **C** | organization editor on the form registry            | need e2e create→edit | M9  |
| OR-03 | Hierarchy in queries (CRM-ADR-06)                                                                               | stored arrays                 | `organizations_for_queries` has no ancestor relation                                                                                                                           | Missing            | —                                                | H     | closure relation and dataset relations              | need depth tests     | M18 |
| OR-04 | Organization phone preferred; organization e-mail (CRM-ADR-09)                                                  | no organization e-mail        | e03 §6                                                                                                                                                                         | Missing            | —                                                | M     | columns, table, editors                             | need                 | M14 |
| OR-05 | Lookup by e-mail domain                                                                                         | yes                           | normalisation, alternate domains, free-mail list, `org_domain` match key exist (`db/organization-domain-name.ts`; `modules/duplicates/services/matching/free-mail-domains.ts`) | Partial            | no exact-domain lookup route or service function | M     | route over `domain_name` + `alternate_domain_names` | need                 | M18 |
| OR-06 | Typed organization channels in the console (D-36)                                                               | —                             | register: landed                                                                                                                                                               | Complete           | —                                                | —     | —                                                   | have                 | M1  |
| OR-07 | Portal sign-up affiliates the contact to the organization matched by domain, or creates it, per tenant switches | yes (`rolodex-contact-steps`) | sign-up matches the contact only (migration `0077`)                                                                                                                            | Missing            | —                                                | M     | sign-up flow + settings                             | need                 | M22 |

### Affiliations (AF)

| ID    | Capability                                                                                     | 1.0                              | 2.0 evidence                                                   | Status             | Gap     | Sev | Work    | Testing | MS  |
| ----- | ---------------------------------------------------------------------------------------------- | -------------------------------- | -------------------------------------------------------------- | ------------------ | ------- | --- | ------- | ------- | --- |
| AF-01 | Dated, typed, multiple affiliations ([05 §2.3](#6-05-functional-architecture-what-every-crm-capability-does)) | one organization + history array | `contact-organization-affiliations.table.ts`; migration `0065` | Complete           | —       | —   | —       | have    | M2  |
| AF-02 | One primary, database-enforced                                                                 | structural only                  | `…_one_primary_uidx`; concurrency spec                         | Complete           | —       | —   | —       | have    | M2  |
| AF-03 | Roles on affiliations, dated                                                                   | roles array                      | `contact-organization-roles.table.ts`                          | Complete           | —       | —   | —       | have    | M2  |
| AF-04 | Derived primary employer and school in queries                                                 | —                                | affiliations README §7, §17                                    | Complete           | —       | —   | —       | have    | M2  |
| AF-05 | Upcoming/Current/Past by workspace date (D-10, D-12)                                           | —                                | `650eae63`; register landed                                    | Complete           | —       | —   | —       | have    | M2  |
| AF-06 | Automatic roles follow effective affiliations (D-46)                                           | on organization change only      | `d875cf22`                                                     | Complete (service) | console | M   | —       | have    | M2  |
| AF-07 | Explicit add/update/end/remove affiliation operations with own permissions (D-09; C-6)         | —                                | today through the contact save                                 | In retro           | —       | H   | per C-6 | need    | M2  |

### Contact information (CI)

| ID    | Capability                                                                                                                           | 1.0                           | 2.0 evidence                                                                                                          | Status             | Gap                     | Sev | Work                                                           | Testing                     | MS  |
| ----- | ------------------------------------------------------------------------------------------------------------------------------------ | ----------------------------- | --------------------------------------------------------------------------------------------------------------------- | ------------------ | ----------------------- | --- | -------------------------------------------------------------- | --------------------------- | --- |
| CI-01 | Typed addresses and phones, one preferred ([05 §2.4](#6-05-functional-architecture-what-every-crm-capability-does)) | fixed slots, buggy preference | `contact-addresses.table.ts`; migration `0035`                                                                        | Complete           | —                       | —   | —                                                              | have                        | M1  |
| CI-02 | E-mails; primary unique                                                                                                              | 3 slots, no format check      | `contact-email-addresses.table.ts:48`                                                                                 | Complete           | —                       | —   | —                                                              | have                        | M1  |
| CI-03 | Linked contact information resolved at read                                                                                          | copies via flowdown           | `contact-information-sources` specs                                                                                   | Complete           | —                       | —   | —                                                              | have                        | M2  |
| CI-04 | Normalisation; one validator for every channel                                                                                       | per channel                   | migration `0048`; cleaning docs                                                                                       | Complete (service) | —                       | L   | —                                                              | have                        | M1  |
| CI-05 | Disconnected source state (D-25)                                                                                                     | —                             | archive jobs building WP-C5, WP-E7                                                                                    | In retro           | —                       | M   | —                                                              | need                        | M2  |
| CI-06 | Communication types as configuration                                                                                                 | yes                           | free-text `communication_type_id`; console uses hard-coded types                                                      | Missing            | —                       | H   | table, FK, migration, editor                                   | need                        | M13 |
| CI-07 | E-mail types (CRM-Q6)                                                                                                                | none                          | none                                                                                                                  | Decision           | —                       | M   | if yes                                                         | need                        | M14 |
| CI-08 | Seasonal boundaries as recurring month/day (E-06)                                                                                    | date window                   | full dates                                                                                                            | In retro           | —                       | M   | wave-4 item W4-3                                               | need December/January tests | M1  |
| CI-09 | Sending withholds deceased, `email_opt_out` and opted-out-of-type recipients                                                         | filtered through queries only | Platform suppresses bounces and complaints only (`P:modules/communications/services/persistence/messages.ts:401-450`) | Missing            | sending to the deceased | H   | Platform communications reads CRM party status and preferences | need service test           | M13 |

### Roles, compliance and regions (CM, RG)

| ID    | Capability                                                                                                     | 1.0                   | 2.0 evidence                                      | Status                                              | Gap | Sev | Work                            | Testing                | MS  |
| ----- | -------------------------------------------------------------------------------------------------------------- | --------------------- | ------------------------------------------------- | --------------------------------------------------- | --- | --- | ------------------------------- | ---------------------- | --- |
| CM-01 | Compliance calculation and notice ([05 §2.5](#6-05-functional-architecture-what-every-crm-capability-does))   | manual reprocess      | `organizations/compliance/*`; 5 integration specs | Complete (service; delivery by Platform not traced) | —   | —   | —                               | have                   | M2  |
| CM-02 | Recalculate on type change; discard minimum-zero rules; role delete refused with dependents (E-01, E-02, E-04) | no                    | DEF-CRM-0111, 0324                                | In retro                                            | —   | M   | wave-4 item W4-1                | need                   | M2  |
| CM-03 | Reprocess and outcome in the console                                                                           | maintenance page      | API only                                          | Backend only                                        | —   | M   | console action and outcome view | need e2e               | M14 |
| CM-04 | Automatic assignment for imports                                                                               | skipped imports       | imports call the same persistence (to verify)     | Partial                                             | —   | L   | verification test               | need                   | M7  |
| CM-05 | Exclusive (single-holder) roles (CRM-Q7)                                                                       | advisory only         | none                                              | Decision                                            | —   | H   | partial unique index            | need                   | M14 |
| CM-06 | Inapplicable roles refused on save (CRM-Q9)                                                                    | console only          | picker filter only                                | Decision                                            | —   | M   | if yes                          | need per-channel tests | M14 |
| RG-01 | Regions, rules, recalculation ([05 §2.6](#6-05-functional-architecture-what-every-crm-capability-does))                            | string postal compare | `modules/regions/**`; 6 integration specs         | Complete                                            | —   | —   | —                               | have                   | M3  |

### Custom fields and records with files (CF)

| ID    | Capability                                                                                             | 1.0                 | 2.0 evidence                                                   | Status                  | Gap                                                          | Sev   | Work                      | Testing                 | MS  |
| ----- | ------------------------------------------------------------------------------------------------------ | ------------------- | -------------------------------------------------------------- | ----------------------- | ------------------------------------------------------------ | ----- | ------------------------- | ----------------------- | --- |
| CF-01 | Custom-field administration in the console ([05 §2.7](#6-05-functional-architecture-what-every-crm-capability-does)) | yes                 | `C:lib/configuration/fields/adapter.ts:11-13,113` browser mock | **Simulated**           | nothing persists                                             | **C** | wire to Platform metadata | need e2e define→enforce | M11 |
| CF-02 | Rich-text fields (D-04)                                                                                | yes                 | demo preview in admin                                          | Partial                 | —                                                            | M     | shared sanitised editor   | need XSS tests          | M11 |
| CF-03 | Requiredness per surface                                                                               | form-level          | only `api` enforced                                            | Partial                 | —                                                            | M     | console and portal levels | need                    | M11 |
| CF-04 | Values validated and queryable                                                                         | weak                | `custom-fields-wire.ts`; e2e                                   | Complete (service)      | decimals as strings (DEF-CRM-0029)                           | L     | fix serialisation         | need                    | M11 |
| CF-05 | Custom fields in configuration editors (E-05)                                                          | —                   | —                                                              | In retro                | —                                                            | M     | wave-4 item W4-2          | need                    | M3  |
| CF-06 | Computed (read-only) custom fields                                                                     | expressions on save | deliberate debt (`docs/debt/custom-field-values.md`)           | Decision (with CRM-M11) | —                                                            | M     | Platform metadata or drop | —                       | M11 |
| CF-07 | Managed files: photos, logos, file/image fields, attachments                                           | yes                 | asset pipeline (`modules/entities/metadata/asset-objects.ts`)  | Complete (service)      | activity attachments in console; bulk lane cannot load logos | L     | console                   | have                    | M8  |
| CF-08 | Console contact-form designer persisted                                                                | web forms           | `C:lib/forms/designer/*` saves to Platform `/console_forms`    | Complete (contacts)     | organization forms designer; portal forms screen inert       | M     | —                         | have                    | M12 |

### Activities and tasks (AC, TK)

| ID    | Capability                                                                                                                | 1.0                       | 2.0 evidence                                                       | Status             | Gap                                                         | Sev | Work                                     | Testing              | MS       |
| ----- | ------------------------------------------------------------------------------------------------------------------------- | ------------------------- | ------------------------------------------------------------------ | ------------------ | ----------------------------------------------------------- | --- | ---------------------------------------- | -------------------- | -------- |
| AC-01 | Activities: types, links, primary, timelines, logging from a record ([05 §3](#6-05-functional-architecture-what-every-crm-capability-does)) | one record each           | `modules/activities/**`; 7 integration specs                       | Complete           | —                                                           | —   | —                                        | have                 | M8       |
| AC-02 | Log and act from the Activities list; bulk actions                                                                        | —                         | `C:activities-definition.tsx:52`                                   | Partial            | —                                                           | M   | action metadata                          | need e2e             | M17      |
| AC-03 | Activities dataset and version history (D-49)                                                                             | —                         | recipe `9348def1`; history kept                                    | Complete (service) | console                                                     | M   | —                                        | have                 | M8       |
| AC-04 | Marketing e-mail engagement on the timeline (CRM-ADR-10)                                                                  | three providers           | none                                                               | Missing            | —                                                           | M   | activity service function and connectors | need                 | M17      |
| AC-05 | Activities survive contact deletion (D-44)                                                                                | delete blocked            | `activity-links.table.ts:55-65` cascade                            | Missing (deferred) | —                                                           | H   | WP-C4                                    | T-4                  | M20      |
| AC-06 | Owner choice beyond 100 users                                                                                             | —                         | Platform `console_users?search=` exists; console pins `limit: 100` | Partial            | console only                                                | M   | type-ahead                               | need                 | M17      |
| AC-07 | Baseline types in older workspaces (D-48)                                                                                 | —                         | `0d5ade91`, `9da94a8e`                                             | Complete           | —                                                           | —   | —                                        | have                 | M8       |
| TK-01 | Tasks, views (My Open Tasks default), drawer ([05 §4](#6-05-functional-architecture-what-every-crm-capability-does))          | Platform tasks            | `modules/tasks/**`; 11 integration specs                           | Complete           | —                                                           | —   | —                                        | have                 | M8       |
| TK-02 | Task history                                                                                                              | —                         | none                                                               | Missing            | —                                                           | M   | history install                          | need                 | M15      |
| TK-03 | Version on every transition including claim and release (D-52)                                                            | none                      | complete/reopen/cancel in retro; claim/release send none           | Partial            | claim, release                                              | M   | service + console                        | need two-client test | M8 / M17 |
| TK-04 | Delete tasks in the console                                                                                               | —                         | `deleteTask` unused                                                | Backend only       | —                                                           | L   | action                                   | need                 | M17      |
| TK-05 | Task import                                                                                                               | —                         | no object type                                                     | Missing            | —                                                           | L   | operation                                | need                 | M17      |
| TK-06 | Task queues (CRM-Q1)                                                                                                      | first-in, first-out pools | live queues; per-queue views                                       | Decision           | —                                                           | M   | per decision                             | need                 | M17      |
| TK-07 | Assignment and due-date notifications                                                                                     | e-mail; reminders broken  | none                                                               | Missing            | —                                                           | M   | Platform communications                  | need                 | M17      |
| TK-08 | Assignee validated through Platform (E-10)                                                                                | none                      | DEF-CRM-0709                                                       | In retro           | —                                                           | M   | wave-4 item W4-6                         | need                 | M8       |
| TK-09 | Attention providers (task due soon, organization out of compliance)                                                       | —                         | —                                                                  | Later              | after Responsibilities and Attention revision 2 is released | L   | —                                        | —                    | later    |

### Duplicates and merge (DQ, MG)

| ID    | Capability                                                                                                                                                                                            | 1.0                          | 2.0 evidence                                                                 | Status             | Gap                                 | Sev   | Work                                                           | Testing                      | MS  |
| ----- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------- | ---------------------------------------------------------------------------- | ------------------ | ----------------------------------- | ----- | -------------------------------------------------------------- | ---------------------------- | --- |
| DQ-01 | Detection with explained scores, rules, groups ([07 §2](#8-07-duplicate-detection-and-record-merging))                                                                               | stale store, imports ignored | `modules/duplicates/**`; 16 integration specs                                | Complete (service) | —                                   | —     | —                                                              | have                         | M4  |
| DQ-02 | Dismissals per pair, idempotent (D-34); stale pairs retired (D-47)                                                                                                                                    | permanent, unaudited         | `be34f658`, `02a1018b`                                                       | Complete           | —                                   | —     | —                                                              | have                         | M4  |
| DQ-03 | Create Anyway under Block with override permission (D-32)                                                                                                                                             | —                            | service `859d673c`, `81775b59`; console unused                               | Complete (service) | console                             | M     | console                                                        | need                         | M4  |
| DQ-04 | Rescan from the console                                                                                                                                                                               | —                            | POST exists; console lists only                                              | Backend only       | —                                   | L     | action and history                                             | need                         | M16 |
| DQ-05 | Create and delete duplicate rules                                                                                                                                                                     | —                            | rules API is list and update only                                            | Missing            | service and console                 | M     | routes and console                                             | need                         | M14 |
| DQ-06 | Duplicate check when creating an organization                                                                                                                                                         | none since 2020              | service applies block rules (`2bb6a828`); console checks contacts only       | Complete (service) | console                             | M     | console                                                        | need                         | M16 |
| DQ-07 | One match-or-create for every entry path (CRM-ADR-03)                                                                                                                                                 | per path                     | checks and sign-up functions only                                            | Partial            | —                                   | H     | service function; adopt in imports, portal, Events             | per-path test                | M16 |
| DQ-08 | Bulk updates trigger detection                                                                                                                                                                        | —                            | change-event handler on every committed write (`duplicate-change-events.ts`) | Complete (verify)  | stale line `docs/data-jobs.md:770`  | L     | add proving test; fix doc                                      | need                         | M7  |
| MG-01 | Atomic N-way merge; preview equals execution ([07 §3](#8-07-duplicate-detection-and-record-merging))                                                                                | non-atomic                   | `modules/merges/**`; 29 integration specs                                    | Complete           | —                                   | —     | —                                                              | have                         | M4  |
| MG-02 | Aliases; 410 for merged ids on every route (D3, D-20)                                                                                                                                                 | none                         | `36ceb699`                                                                   | Complete (service) | console survivor navigation         | M     | console                                                        | have                         | M4  |
| MG-03 | Large organization merges                                                                                                                                                                             | —                            | D7, D9 benchmarks                                                            | Complete           | —                                   | —     | —                                                              | have                         | M4  |
| MG-04 | Pick any record to merge (D-03)                                                                                                                                                                       | manual add                   | picker built, wired only on design routes                                    | Partial            | wire into the queue and Contact 360 | H     | console                                                        | need                         | M4  |
| MG-05 | Field-level merge outcome (D-30)                                                                                                                                                                      | —                            | `03bbc71d`                                                                   | Complete (service) | Merge Details console               | H     | console                                                        | have                         | M4  |
| MG-06 | Approval bound to child ids and versions; value drift (D-31, D-33)                                                                                                                                    | —                            | `4c855bf4`                                                                   | Complete (service) | console reapproval                  | **C** | console                                                        | have                         | M4  |
| MG-07 | Read-only merge history; readable pointer (D-21, D-45)                                                                                                                                                | —                            | `ab22aac0`                                                                   | Complete (service) | console                             | M     | —                                                              | have                         | M4  |
| MG-08 | Merge participants in Membership, Accounting, Commerce, Submissions; Committees for organizations ([07 §4](#8-07-duplicate-detection-and-record-merging)) | pattern P1                   | Platform and Committees (contacts) only                                      | Missing            | references stranded                 | **C** | each application's plan                                        | contract kits; registry walk | ext |
| MG-09 | Required-participant list maintained                                                                                                                                                                  | —                            | `merge-participants.ts:91-94`                                                | Partial            | —                                   | H     | as participants land                                           | need                         | M15 |
| MG-10 | Generated cross-application coverage matrix and CI gate (CRM-ADR-02)                                                                                                                                  | —                            | stale `relationship-coverage.md`                                             | Missing            | —                                   | H     | —                                                              | need                         | M15 |
| MG-11 | Recreate a person from a merge snapshot                                                                                                                                                               | —                            | —                                                                            | Missing            | —                                   | L     | action                                                         | need                         | M16 |
| MG-12 | Block merges of records referenced by non-participating applications (CRM-Q3)                                                                                                                         | —                            | none                                                                         | Decision           | —                                   | **C** | non-participant reference list, planner blocker, coverage test | need                         | M4  |

### Lifecycle (LC)

| ID    | Capability                                                                                                                           | 1.0                   | 2.0 evidence                                        | Status             | Gap                                                                          | Sev   | Work                                                                                | Testing                  | MS             |
| ----- | ------------------------------------------------------------------------------------------------------------------------------------ | --------------------- | --------------------------------------------------- | ------------------ | ---------------------------------------------------------------------------- | ----- | ----------------------------------------------------------------------------------- | ------------------------ | -------------- |
| LC-01 | Archive and unarchive contacts and organizations ([08 §3](#9-08-record-lifecycle-and-safe-delete))           | none                  | fleet jobs `crm-acc-w3b-crm-archive`, `-ui-archive` | In retro           | —                                                                            | **C** | WP-C3, WP-E1..E3                                                                    | T-1                      | M1             |
| LC-02 | Hard delete of contacts and organizations from the live console until safe delete (CRM-Q10)                                          | hard delete           | no record Delete; bulk delete by selection live     | Decision           | console path strands references                                              | **C** | per CRM-Q10                                                                         | crawler check            | M7 (repair)    |
| LC-03 | Deletion-impact contract and enforcement (D-41) ([08 §4](#9-08-record-lifecycle-and-safe-delete)) | dependency objections | branch design only                                  | Missing (deferred) | **D-41 is P0: not production-ready for customers who hard-delete until M20** | **C** | WP-L1, P0, P1, C1; participants ext                                                 | T-2                      | M20            |
| LC-04 | Deletion record, revision redaction, audit view (D-42)                                                                               | full history kept     | revisions keep personal data                        | Missing (deferred) | SOC 2 TC-DAT-003                                                             | H     | WP-C2, WP-E5                                                                        | T-3                      | M20            |
| LC-05 | Orphaned activities view (D-44)                                                                                                      | —                     | cascade                                             | Missing (deferred) | P0                                                                           | H     | WP-C4, WP-E6                                                                        | T-4                      | M20            |
| LC-06 | New references to archived parties refused                                                                                           | —                     | none                                                | Missing            | other applications can bill or enrol an archived party                       | H     | CRM: activity, task, affiliation creates (with archive); others: their participants | per-service refusal test | M1 (CRM) / ext |

### Writes and concurrency (WR)

| ID    | Capability                                                                                                                         | 2.0 evidence                                   | Status   | Gap | Sev   | Work                        | Testing          | MS  |
| ----- | ---------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------- | -------- | --- | ----- | --------------------------- | ---------------- | --- |
| WR-01 | Version required; 400 missing, 409 stale (D-05) ([06 §2](#7-06-api-and-service-architecture)) | server re-reads version (DEF-CRM-0012)         | In retro | —   | **C** | standard W1; callers        | two-writer tests | M1  |
| WR-02 | Omitted unchanged; null clears; separate replace (D-09, E-09)                                                                      | PUT empties omitted collections (DEF-CRM-0174) | In retro | —   | **C** | update contract; CRM-ADR-04 | need             | M1  |
| WR-03 | Lost-response safety: client UUIDv7; existing id → 409 `record_exists` (D-43, W5)                                                  | existing id answers 400                        | In retro | —   | H     | —                           | need             | M1  |
| WR-04 | Task transitions versioned (D-52)                                                                                                  | —                                              | In retro | —   | H     | —                           | need             | M8  |
| WR-05 | No silent version bump after conflict (D-35)                                                                                       | landed                                         | Complete | —   | —     | —                           | have             | M1  |
| WR-06 | No-op writes change nothing; a stale no-op answers 200 unchanged (D-05, W2)                                                        | —                                              | In retro | —   | M     | —                           | need             | M1  |

### Data operations (DO)

| ID    | Capability                                                                                                                     | 2.0 evidence                                                                    | Status                      | Gap                                                            | Sev   | Work                                                | Testing | MS   |
| ----- | ------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------- | --------------------------- | -------------------------------------------------------------- | ----- | --------------------------------------------------- | ------- | ---- |
| DO-01 | Imports through the shared engine, validate before apply ([10 §1](#11-10-data-operations-imports-upserts-exports-bulk-changes-and-rollback)) | `modules/data-jobs/**`; 14 integration specs                                    | Complete (interactive lane) | file lane cannot dry-run                                       | M     | engine                                              | need    | M19  |
| DO-02 | Bulk update and delete from a selection                                                                                        | wizard live                                                                     | Complete                    | delete covers CRM only (LC-02/03)                              | —     | —                                                   | have    | M7   |
| DO-03 | Source-key crosswalk                                                                                                           | `records_by_source_key`                                                         | Complete                    | —                                                              | —     | —                                                   | have    | M7   |
| DO-04 | Version freeze in bulk and patch imports (D-37, D-38)                                                                          | engine library `76041c5`; CRM fix reverted then redone                          | In retro                    | —                                                              | **C** | engine owner; verify with Accounting and Membership | need    | M7   |
| DO-05 | Rollback of collections and custom fields; organizations                                                                       | `docs/debt/rollback-of-collections.md`                                          | Partial                     | —                                                              | H     | —                                                   | need    | M19  |
| DO-06 | Organization update/upsert as a full operation                                                                                 | reference pass only                                                             | Partial                     | —                                                              | M     | —                                                   | need    | M19  |
| DO-07 | Affiliation import                                                                                                             | nested affiliations and roles import with the contact (`contact-references.ts`) | Partial                     | no one-row-per-affiliation operation against existing contacts | M     | operation and mapper                                | need    | M19  |
| DO-08 | Typed collection mapping; untyped defaults (D-40, E-15)                                                                        | contact insert only                                                             | In retro                    | —                                                              | H     | engine mapping                                      | need    | M7   |
| DO-09 | Full-name column split in the mapper                                                                                           | none                                                                            | Missing                     | —                                                              | L     | —                                                   | need    | M19  |
| DO-10 | Bulk archive                                                                                                                   | —                                                                               | Missing                     | —                                                              | M     | after LC-01                                         | need    | M19  |
| DO-11 | Platform capacity table                                                                                                        | module constant                                                                 | Missing                     | —                                                              | L     | Platform                                            | need    | M19  |
| DO-12 | CRM tenant-export provider                                                                                                     | `aws/crm.export-datasets.ts` (28 datasets); coverage and integration specs      | Complete (service)          | Platform orchestration paused                                  | —     | —                                                   | have    | M7   |
| DO-13 | Engine rulings: atomic groups, async cancel, in-job checksum (E-11, E-12, E-16)                                                | —                                                                               | In retro                    | —                                                              | H     | engine owner                                        | need    | M7   |
| DO-14 | Rollback and retry in the console (D-39)                                                                                       | in progress                                                                     | In retro                    | —                                                              | H     | —                                                   | need    | M7   |
| DO-15 | Job-linked revisions (E-08)                                                                                                    | partial                                                                         | In retro                    | —                                                              | M     | —                                                   | need    | M7   |
| DO-16 | Saved mass-update templates; add/remove on collections ("add role R")                                                          | 1.0 System Mass Update                                                          | none                        | Missing                                                        | —     | M                                                   | engine  | need | M19 |

### Search, Explorer, queries and console (SR, UI)

| ID     | Capability                                                                                                        | 2.0 evidence                                                                    | Status                    | Gap                              | Sev   | Work                                     | Testing | MS        |
| ------ | ----------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------- | ------------------------- | -------------------------------- | ----- | ---------------------------------------- | ------- | --------- |
| SR-01  | Explorer lists with saved views, select all, export ([09 §3](#10-09-console-experience-search-explorer-queries-and-the-360-pages))          | Platform recipes                                                                | Complete                  | —                                | —     | —                                        | have    | M5        |
| SR-02  | Quick Search proven end to end                                                                                    | DEF-CRM-0119 escalated                                                          | Partial                   | dev index not updating           | H     | prove; freshness test                    | need    | M18       |
| SR-03  | Exact-match ranking                                                                                               | unverified                                                                      | Unknown                   | —                                | M     | —                                        | need    | M18       |
| SR-04  | Global search page                                                                                                | `C:search/page.tsx` sample data                                                 | **Simulated**             | in the navigation                | H     | build or remove                          | crawler | M10       |
| SR-05  | AI Ask edits the query with diff and undo (D-27), on in live consoles                                             | flag off by default                                                             | In retro / Partial        | flag                             | M     | —                                        | need    | M5 / M18  |
| SR-06  | Saved-query bulk actions                                                                                          | throw when live                                                                 | **Simulated**             | —                                | M     | Platform and console                     | need    | M18       |
| SR-07  | CRM recipes and data-quality indicators                                                                           | 3–4 recipes                                                                     | Partial                   | —                                | M     | recipes                                  | need    | M18       |
| SR-08  | Relationship-aware criteria in the visual builder (E-17)                                                          | —                                                                               | Missing (later by ruling) | —                                | M     | Platform query designer (`PLAT-Mn`)      | need    | ext → M18 |
| SR-09  | NOT in criteria (D-24)                                                                                            | landed                                                                          | Complete                  | —                                | —     | —                                        | have    | M5        |
| SR-10  | Search durability: deletes and late updates never resurrect; one tenant's bulk delete never stalls another (D-29) | integration landing (build session)                                             | In retro                  | —                                | **C** | Platform quick search                    | need    | M5        |
| SR-10b | Communication preferences queryable per type ("opted in to X")                                                    | stored, exported, not in `contacts.query.ts`                                    | Missing                   | mailing lists                    | H     | collection and recipe                    | need    | M13       |
| SR-11  | Role-holder columns on organization queries; "Organization Contacts by Role" recipe                               | roles are a criterion only                                                      | Missing                   | —                                | M     | —                                        | need    | M18       |
| UI-01  | One live switch ([09 §8](#10-09-console-experience-search-explorer-queries-and-the-360-pages))                                               | `NEXT_PUBLIC_CRM_API`, `_COMMERCE_API`                                          | Partial                   | DEF-CRM-0034                     | H     | retire                                   | crawler | M10       |
| UI-02  | Editor reference data from live services                                                                          | sample communication types, custom-field defaults, duplicate check              | **Simulated**             | —                                | H     | —                                        | need    | M10       |
| UI-03  | Record Layouts list                                                                                               | seeded fake rows; assignments and resolution are live (`effective_definitions`) | **Simulated** (list only) | —                                | M     | read Platform layouts                    | need    | M12       |
| UI-04  | Page-layout designer persisted and rendered                                                                       | saves to `localStorage`                                                         | **Simulated**             | —                                | H     | wire to Platform `/console_page_layouts` | need    | M12       |
| UI-05  | CRM extension points and events                                                                                   | mock-only screens                                                               | Scaffolded                | —                                | L     | after M15 events                         | —       | later     |
| UI-06  | Organization 360 on the metadata view system, with history ([09 §5](#10-09-console-experience-search-explorer-queries-and-the-360-pages))    | bespoke; four live tabs                                                         | Partial                   | —                                | H     | —                                        | need    | M9        |
| UI-07  | Contact 360 shows only live cards                                                                                 | 15 tabs "not available"                                                         | Partial                   | —                                | M     | gate by license and liveness             | crawler | M10       |
| UI-08  | Other applications contribute 360 cards, header badges and list columns                                           | Accounting only                                                                 | Partial                   | —                                | H     | those applications' plans                | need    | ext       |
| UI-09  | Engagement Scoring and CRM Processes screens                                                                      | fake in every environment                                                       | **Simulated**             | false claims                     | H     | remove (P-9)                             | crawler | M10       |
| UI-10  | Empty list columns; inert Assign Owner                                                                            | e04 D                                                                           | **Simulated**             | —                                | M     | remove until sourced                     | crawler | M10       |
| UI-11  | Alerts and Print Profile evaluated before any build (E-18)                                                        | hidden                                                                          | Complete (hidden)         | evaluations pending              | L     | evaluations                              | —       | M18       |
| UI-12  | Session expiry keeps drafts (D-17)                                                                                | landed                                                                          | Complete                  | —                                | —     | —                                        | have    | M1        |
| UI-13  | Toolbar actions real or removed (D-02)                                                                            | in progress                                                                     | In retro                  | —                                | H     | —                                        | crawler | M1        |
| UI-14  | Server-side organization picker search                                                                            | backend `?search=` exists; console loads 2,000                                  | Partial                   | console only                     | M     | —                                        | need    | M9        |
| UI-15  | Hierarchy and overview contacts without the 500-record scan                                                       | Contacts tab already paged and editable                                         | Partial                   | hierarchy and overview card      | M     | —                                        | need    | M9        |
| UI-16  | Contact 360 Communications tab over Platform message history                                                      | Platform route exists; tab is sample data                                       | **Simulated**             | —                                | M     | console                                  | need    | M18       |
| UI-17  | One-off staff e-mail from a contact or organization, logged as an activity                                        | action declared, no handler                                                     | Missing                   | —                                | M     | Platform communications                  | need    | M17       |
| UI-18  | Quick add from shared pickers through match-or-create                                                             | none (order composer creates unsaved locals)                                    | Missing                   | every application's create flows | H     | with DQ-07                               | need    | M16       |
| UI-19  | CRM home: counts and data-quality counters                                                                        | none                                                                            | Missing                   | —                                | L     | Dashboard Lab tiles or drop              | —       | M18       |

### History, events, integration and hygiene (HI, EV, X)

| ID    | Capability                                                                                                                                                               | 2.0 evidence                                   | Status             | Gap                                                                                                 | Sev   | Work                                                                                                                                                        | Testing            | MS       |
| ----- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------- | ------------------ | --------------------------------------------------------------------------------------------------- | ----- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------ | -------- |
| HI-01 | History for organizations, tasks, configuration, regions, rules ([11 §4](#12-11-security-audit-and-operational-contracts))                                       | contacts and activities only                   | Partial            | —                                                                                                   | H     | history installs (organizations in M9, the rest in M15)                                                                                                     | need               | M9 / M15 |
| HI-02 | Readable history for archived or renamed references (E-07)                                                                                                               | —                                              | In retro           | —                                                                                                   | M     | wave-4 item W4-4                                                                                                                                            | need               | M1       |
| EV-01 | Business events for contacts and organizations incl. archived/unarchived ([06 §5](#7-06-api-and-service-architecture))                                          | `crm.contact.merged` only                      | Scaffolded         | 7 of 8 unpublished; no customer delivery path until the Integration Platform's outbound events ship | H     | outbox events                                                                                                                                               | contract tests     | M15      |
| X-01  | SOC 2 exceptions before 2026-11-07 ([11 §5](#12-11-security-audit-and-operational-contracts))                                                                    | `docs/debt/soc2-remediation-windows.md`        | Open               | CI fails from 2026-11-07                                                                            | **C** | one decision set to Andrew in the retroactive CRM-M6 plan (enable controls or renew with a new expiry); most need GitHub administration or pipeline changes | `compliance:check` | M6       |
| X-02  | Branch protection; secret and dependency scanning                                                                                                                        | not configured                                 | Missing            | administrator setting                                                                               | M     | Andrew or an administrator                                                                                                                                  | —                  | M15      |
| X-03  | Ids standard: tasks UUIDv7, regions configuration ids, UUIDv7 checks, validator ([04 §1](#5-04-domain-and-relational-data-model))             | `tasks.ts:221`, `regions.ts:130`               | Partial            | —                                                                                                   | H     | migration                                                                                                                                                   | need               | M15      |
| X-04  | Service functions: contacts, organizations, activities, party resolution ([06 §3](#7-06-api-and-service-architecture)) | `tasks` only; two portal sign-up SQL functions | Partial            | Membership port stubbed                                                                             | H     | —                                                                                                                                                           | contract tests     | M15      |
| X-06  | Stale documentation fixed ([13 §5](#14-13-decisions-standing-recorded-and-needing-andrew-deliverable-d))                                                                                                | e03 §20 item 15                                | Open               | —                                                                                                   | L     | doc fixes                                                                                                                                                   | —                  | M15      |
| X-07  | Membership change handler invoked ([12 §2](#13-12-cross-application-integration-contracts))                                                  | unwired                                        | Missing            | flow-down stale                                                                                     | H     | add to CRM's stream list                                                                                                                                    | need               | M15      |
| X-09  | Published read views (CRM-ADR-05)                                                                                                                                        | physical reads                                 | Missing            | —                                                                                                   | M     | views; readers migrate in their plans                                                                                                                       | need               | M15      |
| X-10  | Eligibility condition types published by CRM (13 types)                                                                                                                  | `aws/crm.eligibility.ts`                       | Complete (service) | `crm.contact.hasPortalUser` has no fact provider; no hierarchy-aware type (OR-03)                   | M     | —                                                                                                                                                           | have               | M15      |

### Security and portal (SE, PO)

| ID    | Capability                                                                                                                                                | 2.0 evidence                                                    | Status             | Gap                                       | Sev   | Work                           | Testing | MS  |
| ----- | --------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------- | ------------------ | ----------------------------------------- | ----- | ------------------------------ | ------- | --- |
| SE-01 | Tenant RLS, tiered permission catalog, application access ([11 §1–2](#12-11-security-audit-and-operational-contracts))                                  | RLS and route-authorization specs                               | Complete           | —                                         | —     | —                              | have    | M6  |
| SE-02 | Workspace scope, staff-access approval, effective permissions (D-13, D-14, D-15)                                                                          | in progress / approved                                          | In retro           | —                                         | **C** | —                              | need    | M6  |
| SE-03 | Record-level staff scoping (CRM-Q8)                                                                                                                       | none                                                            | Decision           | —                                         | H     | Platform conditions            | need    | M23 |
| SE-04 | Field-level permissions                                                                                                                                   | none                                                            | Missing            | —                                         | M     | Platform                       | need    | M23 |
| SE-05 | Built-in CRM policies                                                                                                                                     | Platform generates read-only, user, administrator tier policies | Partial            | declared Contacts Editor and Data Steward | M     | declarations                   | need    | M23 |
| SE-06 | Audited lock/unlock; protected versus defaults (D-08, E-13, E-14)                                                                                         | `512ba8f3` and follow-ups; `record-locks.routes.ts`             | Complete (service) | console; E-14 list order                  | H     | —                              | have    | M6  |
| PO-01 | Member's own profile, with name and credential locks ([05 §7](#6-05-functional-architecture-what-every-crm-capability-does)) | Platform `cid` claim; sign-up matching live; no profile route   | Missing            | —                                         | H     | portal routes                  | need    | M22 |
| PO-02 | Member's organization profile, with name lock                                                                                                             | none                                                            | Missing            | —                                         | M     | —                              | need    | M22 |
| PO-03 | Organization roster and child organizations                                                                                                               | staff descendants route exists                                  | Missing            | —                                         | H     | portal access, paged           | need    | M22 |
| PO-04 | Organization administrator through Security grants derived from affiliation roles (CRM-ADR-11)                                                            | none                                                            | Missing            | —                                         | H     | Platform Security + CRM read   | need    | M22 |
| PO-05 | Profile-confirmation step offered to application processes                                                                                                | none                                                            | Missing            | —                                         | M     | portal route and form contract | need    | M22 |

### Relationships (RL)

| ID    | Capability                                                                                                                      | Status            | Sev | MS  |
| ----- | ------------------------------------------------------------------------------------------------------------------------------- | ----------------- | --- | --- |
| RL-01 | Typed relationships in both directions; spouse migrated ([05 §5](#6-05-functional-architecture-what-every-crm-capability-does)) | Decision (CRM-Q2) | M   | M21 |
| RL-02 | Households                                                                                                                      | Decision (CRM-Q2) | L   | M21 |

### L1

Rhythm 1.0 capabilities not yet reproduced, replaced, or intentionally dropped.

| ID    | 1.0 capability                                                                                                                                           | 2.0 treatment                                                                                                                     | Row or decision   |
| ----- | -------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------- | ----------------- |
| L1-01 | Generic relationships (Current/Past/Future)                                                                                                              | Not reproduced; planned                                                                                                           | RL-01, CRM-Q2     |
| L1-02 | Member and organization directories (comments, ratings, messaging, radius search)                                                                        | Not reproduced                                                                                                                    | CRM-Q4            |
| L1-03 | Engagement scoring                                                                                                                                       | Not reproduced; recommended retire for launch                                                                                     | CRM-Q5, UI-09     |
| L1-04 | Portal self-service (profile, organization, roster, child organizations)                                                                                 | Not reproduced; planned                                                                                                           | PO-01..PO-03      |
| L1-05 | Portal role-permission matrix                                                                                                                            | Replaced by Platform Security grants derived from affiliation roles                                                               | PO-04, CRM-ADR-11 |
| L1-06 | Organization lookup by e-mail domain                                                                                                                     | Partly built; lookup route planned                                                                                                | OR-05             |
| L1-07 | Inline organization creation from a contact form                                                                                                         | **Intentionally dropped**; create the organization, then the affiliation                                                          | —                 |
| L1-08 | Free-text organization name on a contact                                                                                                                 | **Intentionally replaced** by affiliations                                                                                        | AF-01             |
| L1-09 | Staff "log in as member", password reset, verify e-mail, last-login display                                                                              | Not reproduced; Platform security capability                                                                                      | —                 |
| L1-10 | Organization merge                                                                                                                                       | Reproduced and improved                                                                                                           | MG-01             |
| L1-11 | Compliance e-mail with preview                                                                                                                           | Reproduced                                                                                                                        | CM-01             |
| L1-12 | Enrichment: FullContact social profile; Google Civic districts and the elected-officials panel (verify the Civic API's 2025 retirement before designing) | Not reproduced; later connectors                                                                                                  | —                 |
| L1-13 | Marketing e-mail activity on the timeline                                                                                                                | Not reproduced; planned                                                                                                           | AC-04             |
| L1-14 | Contact-role rule sets                                                                                                                                   | **Intentionally dropped** (dead code in 1.0)                                                                                      | —                 |
| L1-15 | Duplicate e-mail prevention tenant switch                                                                                                                | **Intentionally dropped**; the rule is always on for the primary e-mail                                                           | CI-02             |
| L1-16 | Name parsing on save                                                                                                                                     | **Intentionally dropped** (2026-10-09); mapper-only split                                                                         | DO-09             |
| L1-17 | Task queues                                                                                                                                              | Kept in code; decision pending                                                                                                    | TK-06, CRM-Q1     |
| L1-18 | Alternate-e-mail "authorize and link" at sign-up                                                                                                         | **Replaced** by Platform likely-match with code verification over `crm.portal_contact_candidates` and `crm.create_portal_contact` | X-04              |
| L1-19 | Outbound customer integrations (EventBridge forwarding of record changes, Zapier, Higher Logic, Mailchimp lists, Outlook add-in)                         | Owned by the Integration Platform; CRM supplies events and service functions                                                      | EV-01, X-04       |
| L1-20 | Sales application (leads, opportunities, pipelines)                                                                                                      | Recommended out of scope                                                                                                          | CRM-Q11           |
| L1-21 | Key roles on organization types                                                                                                                          | **Intentionally dropped** (Andrew 2026-10-11)                                                                                     | —                 |
| L1-22 | Task groups and order references on tasks                                                                                                                | **Intentionally dropped**; generic subject reference instead                                                                      | —                 |
| L1-23 | Portal delegates and contact login profiles                                                                                                              | Non-functional or dead in 1.0; design new only by decision under PO-04                                                            | —                 |
| L1-24 | In-product CRM training exercises                                                                                                                        | Out of CRM scope; Help / Learning Center owns any successor                                                                       | —                 |
| L1-25 | New-contact redirect setting                                                                                                                             | **Intentionally dropped**; the console opens the new record                                                                       | —                 |
| L1-26 | Fields of other applications injected into contact queries by tenant setting (events, courses, committees)                                               | Replaced by cross-application relationships in the query layer                                                                    | SR-08             |

### Coverage check

Every requirement in chapters 04–12 maps to a row above or to an out-of-scope line. Rulings with no CRM gap row, repaired
inside the retroactive milestones by their owners: D-16 (Microsoft sign-in, Platform), D-18 (empty roles, Platform
security), D-26 (user time-zone preference, console), D-28 (Explorer trail, console). Out of scope for this architecture:
address verification services, a generic data-quality rules engine, a CRM-specific import framework, LLM-based duplicate
matching, and pages owned by the Web Experience Platform.

## 16. 15. Milestone Plan (Completion Roadmap, Deliverable C)

## 15. Milestone Plan (Completion Roadmap, Deliverable C)

Part of [RHYTHM-CRM-ARCH-001](#1-rhythm-crm-arch-001-rhythm-2-0-crm-definitive-architecture-and-completion-plan). Gap rows are in [14](#15-14-implementation-gap-matrix-deliverable-b); tests and the acceptance method in
[16](#17-16-testing-and-acceptance-strategy). This chapter is the architecture's **Milestone plan** in the sense of
R2-DEV-001 v1.1 (`docs/standards/milestone-development-and-acceptance.md`, approved by Andrew 2026-10-19): every
milestone below is **Proposed**; each gets a Milestone Implementation Plan for Andrew's approval before it is built, and
its record and status live in services-crm `docs/milestones/CRM-M<n>-<slug>.md` (nothing else stores milestone status).

### 1. The finish line

CRM is **complete** when every row of the gap matrix is Complete or explicitly out of scope with Andrew's agreement, and
every milestone below is **Product Accepted** by Andrew after hands-on use. Measurably:

1. The cumulative CRM acceptance run (617 journeys today, extended per milestone) passes on the acceptance workspace with
   zero failed P0 journeys and zero open P0/P1 defects, except those tracing to rulings Andrew deferred (D-41, D-42, D-44
   until CRM-M20).
2. A merge of a contact referenced in every application listed in [12 §2](#13-12-cross-application-integration-contracts)
   that is deployed to dev at the time leaves zero references to the merged-away id (registry walk).
3. The live-console crawler finds no sample data and no toast-only action on any route.

### 2. Sequencing rules

- **Retroactive milestones first.** What is already built enters through retroactive milestones **CRM-M1..CRM-M8**, owned
  by the CRM build session (R2-DEV-001 retroactive path: inventory, retroactive plan, cumulative run, live walkthrough,
  acceptance). They map to the twelve acceptance areas: M1 Contacts and organizations core; M2 Affiliations, roles and
  compliance; M3 CRM configuration; M4 Duplicates and merge; M5 Explorer, saved queries and search; M6 Access and security
  in CRM; M7 Data operations; M8 Activities and tasks. Acceptance repair waves 2–4 and archive (record-lifecycle phase 1)
  complete inside them. This plan does not duplicate that work.
- **Stabilization first (Decided 2026-10-19 ~16:05 ET).** No milestone from CRM-M9 starts until the retroactive set is
  acceptance green.
- **Protections handled inside the retroactive set:** SR-10 search durability (D-29) is a repair already landing; X-01 (SOC 2
  exceptions expiring **2026-11-07**) goes to Andrew as one decision set in the CRM-M6 plan; MG-12 (merge block, CRM-Q3) in
  the CRM-M4 plan; LC-02 per CRM-Q10. Under R2-DEV-001 each needs Andrew's recorded decision before it is built.
- **One milestone in implementation, at most two verified but unaccepted** (R2-DEV-001 rule 4). The order below is the
  build order; the next milestone's plan may be prepared while one is being built.
- **Other applications' work belongs to their plans.** Merge and deletion participants, contributed 360 cards and list
  columns are milestones of Accounting (`ACCT-Mn`), Membership (`MEMB-Mn`), Commerce (`COMM-Mn`), Submissions (`SUBM-Mn`)
  and Committees (`CMTE-Mn`), built when Andrew's focus order lets those applications build. Until then the merge block (CRM-Q3) protects data. CRM milestones never wait for them.
- **Shared standards first.** Where a ruling sets a Platform-wide convention (E-09 update contract, E-05 configuration
  forms, data-job engine rulings, D-05 versions), the shared change is built by its owner and verified against its other
  consumers before CRM adopts it.
- **Fleet.** Sonnet by default, Opus only for open-ended design or multi-module work with the reason stated. Console work
  needs Andrew's authorisation per milestone; once he approves a milestone plan, its console work is authorised.

### 3. Order and dependencies

```
CRM-M1..M8 retroactive (built work; CRM build session; records on services-crm master 0be322c0 docs/milestones/)
   │
   ├─▶ M9  Organization editing and Organization 360 ──▶ M10 Live-console truth
   │                                                      ├─▶ M11 Custom-field administration ──▶ M12 Record layouts
   │                                                      └─▶ M13 Communication preferences ──▶ M14 Configuration and channels
   ├─▶ M15 Platform-facing contracts (backend)
   │        ├─▶ M16 Duplicates and merge completion
   │        ├─▶ M17 Activities and tasks completion  (CRM-Q1)
   │        └─▶ M18 Search and queries
   ├─▶ M19 Data operations completeness
   ├─▶ M20 Cross-service safe delete (deferred by Andrew; needs participants in other applications' plans)
   ├─▶ M21 Relationships and households (CRM-Q2)
   └─▶ M23 Record-level and field-level security (CRM-Q8; Platform-led)

LAST: M22 Portal CRM self-service, after every other scheduled milestone (M9..M19, M21, M23), on Andrew's portal components
```

**Portal comes last (Andrew, 2026-10-19 about 18:10 ET).** Portal functionality is always the last milestone of the plan:
the portal is the self-service side of the product and follows what architecture and the console decide, and Andrew is
building the portal components himself. CRM-M22 is therefore scheduled after every other scheduled milestone (CRM-M20,
deferred, aside) and built on his components; no portal screens, pages or sign-in are designed or built before it. Its
number is kept so the gap matrix and traceability rows stay valid.

### 4. The milestones

Format per R2-DEV-001's milestone plan: objective, kind, dependencies, gap rows, walkthrough, acceptance. Sizes are
indicative agent-days on the fleet.

#### CRM-M9 — Organization editing and Organization 360 (user-facing; ≈ 6)

- **Objective:** staff edit every field and channel of an existing organization and see its full 360, including history.
- **Depends on:** CRM-M1, CRM-M2. **Rows:** OR-02, UI-06, HI-01 (organizations), UI-14, UI-15.
- **Walkthrough:** open an organization, edit name, type, parent, addresses, phones and custom fields; see the change in
  Version History; open Hierarchy for an organization with 600 descendants; pick a parent from a server-searched picker.
- **Acceptance:** the edit round-trips with a version check (stale edit refused); `GET /organizations/{t}/{id}/history`
  returns the revision; hierarchy and contacts lists page without a 500-record cap; CRM-ACC journeys tagged CRM-M9 pass.

#### CRM-M10 — Live-console truth (user-facing; ≈ 6)

- **Objective:** nothing in a live console shows sample data or does nothing.
- **Depends on:** CRM-M9. **Rows:** UI-01, UI-02, UI-07, UI-09, UI-10, SR-04, UI-13 (remainder), DEF-CRM-0031.
- **Walkthrough:** open every CRM navigation item and every action on a contact and an organization; use ⌘K and the full
  search page; create a contact and see real communication types and a real duplicate warning.
- **Acceptance:** the crawler passes: no module under `lib/**/mock/**` reachable in a live build, no toast fallback from
  `action-toolbar.tsx`, no "not available yet" string on a live route; `NEXT_PUBLIC_CRM_API` and `NEXT_PUBLIC_COMMERCE_API`
  removed; a contact at the documented limits renders.

#### CRM-M11 — Custom-field administration (user-facing; ≈ 6)

- **Objective:** administrators define custom fields for CRM records in the console and they are enforced everywhere.
- **Depends on:** CRM-M10. **Rows:** CF-01, CF-02, CF-03, CF-04; CF-06 decision (computed fields).
- **Walkthrough:** define a picklist and a rich-text field on contacts; make the picklist required for the console; see
  both on the form and 360; try to save without it; paste a script into the rich-text field and see it neutralised.
- **Acceptance:** the field persists through Platform metadata; `POST /contacts` enforces requiredness per surface (`api`,
  `console`, `portal`); decimals return as numbers; journeys tagged CRM-M11 pass.

#### CRM-M12 — Record layouts designed and published (user-facing; ≈ 5)

- **Objective:** an administrator designs a contact or organization page layout, publishes it, and staff see it.
- **Depends on:** CRM-M11. **Rows:** UI-03, UI-04.
- **Walkthrough:** design a layout for a contact type, publish it, open a contact of that type and see it; open another
  type and see the default.
- **Acceptance:** the designer saves to Platform `/console_page_layouts`; the published layout renders through
  `GET /effective_definitions`; the Record Layouts list reads the same data.

#### CRM-M13 — Communication preferences (user-facing; ≈ 6)

- **Objective:** a tenant defines communication types, staff and members record opt-ins, staff build "opted in to X"
  lists, and sending respects opt-outs, `email_opt_out` and deceased status.
- **Depends on:** CRM-M10. **Rows:** CI-06, SR-10b (preferences queryable), CI-09 (with Platform communications).
- **Walkthrough:** add a "Newsletter" type; opt one contact out; build a query of contacts opted in; send a test message to
  the list and see the opted-out and deceased contacts withheld with a reason.
- **Acceptance:** free-text values migrated or reported; the preference collection is queryable; Platform communications
  refuses the three recipient classes, tested at service level.

#### CRM-M14 — Configuration and contact-channel completeness (user-facing; ≈ 6)

- **Objective:** administrators manage every CRM rule in the console; organizations have the same channels as contacts.
- **Depends on:** CRM-M13. **Rows:** DQ-05, CM-03, CM-05 (CRM-Q7), CI-07 (CRM-Q6), OR-04, CM-06 (CRM-Q9).
- **Walkthrough:** create and delete a duplicate rule; reprocess compliance and read per-organization outcomes; mark a role
  exclusive and try to assign a second holder; add an organization e-mail and a preferred phone.
- **Acceptance:** each refusal is a named 409/400 at the service; outcomes visible; journeys tagged CRM-M14 pass.

#### CRM-M15 — Platform-facing contracts (backend-only; guided verification; ≈ 8)

- **Objective:** other applications and integrations can rely on CRM: service functions, published views, business events,
  history for every record type, ids that meet the standard.
- **Depends on:** CRM-M1..M8. **Rows:** X-03, X-04, X-07, X-09, EV-01, HI-01 (tasks, configuration, roles, regions, rules),
  TK-02, MG-09, MG-10, X-06, X-02 (administrator settings), CO-01 (constraint validation).
- **Why no UI walkthrough:** no person uses these in the console; the first consumer screens are other applications'.
- **Guided verification:** a script Andrew watches: create and update a contact through the `contacts` service function;
  a test subscriber receives `crm.organization.updated` with ids and changed field names and no personal data; inserting a
  non-UUIDv7 task id is refused by the database; task, configuration-type, role, region and duplicate-rule history routes
  each return a revision; Membership's change handler runs on an affiliation change; the generated cross-application
  coverage matrix lists every referencing column.
- **Migration:** tasks to UUIDv7 (inventory holders of task ids outside CRM, e.g. workflow runs); regions to configuration
  ids (rewrite `region_id` in contacts, organizations, assignment rules, and Platform saved queries and recipes in the same
  release; services-platform is in scope).

#### CRM-M16 — Duplicates and merge completion (user-facing; ≈ 6)

- **Objective:** staff find and merge any duplicates, create records without making new ones, and recover from a wrong
  merge.
- **Depends on:** CRM-M4, CRM-M15. **Rows:** DQ-04, DQ-06 (console), DQ-07, UI-18, MG-04 (wiring), MG-11.
- **Walkthrough:** quick-add a contact from an event-guest picker and be offered the existing match; run a rescan; merge
  two manually picked contacts; recreate a person from a past merge's snapshot.
- **Acceptance:** the same person pushed through the console, an import upsert and the service function ends as one contact
  (portal sign-up joins this check in CRM-M22, which comes last); rescan history visible; journeys tagged CRM-M16 pass.

#### CRM-M17 — Activities and tasks completion (user-facing; ≈ 6; after CRM-Q1)

- **Objective:** staff log and manage activities and tasks from lists, e-mail a contact, and get notified of assignments.
- **Depends on:** CRM-M8, CRM-M15 (activity service function). **Rows:** AC-02, AC-04, AC-06, TK-03 (claim/release),
  TK-04, TK-05, TK-06, TK-07, UI-17.
- **Walkthrough:** log an activity from the Activities list for two contacts and an organization; send a one-off e-mail from
  a contact and see it logged; claim a task in two browsers and see the conflict; receive the assignment e-mail.
- **Acceptance:** claim and release refuse stale versions at the service; journeys tagged CRM-M17 pass.

#### CRM-M18 — Search and queries (user-facing; ≈ 8)

- **Objective:** staff find any record quickly and query CRM by hierarchy, roles and communications.
- **Depends on:** CRM-M5, CRM-M15. **Rows:** SR-02, SR-03, OR-03, OR-05, SR-05, SR-06, SR-07, SR-11, UI-16, UI-19,
  UI-11 (evaluations); SR-08 relationship-aware criteria is a Platform query-designer milestone (`PLAT-Mn`) this one
  consumes when ready.
- **Walkthrough:** create a contact and find it in ⌘K within 30 s; query "contacts at organizations under Acme"; add role
  holders as columns on an organization query; ask "show inactive contacts" and undo; bulk-tag query results; open a
  contact's Communications tab.
- **Acceptance:** freshness test in deployed dev; exact number match ranks first; journeys tagged CRM-M18 pass.

#### CRM-M19 — Data operations completeness (user-facing; ≈ 6)

- **Objective:** every bulk change can be previewed and safely undone; organizations and affiliations import fully.
- **Depends on:** CRM-M7. **Rows:** DO-01 (file-lane dry run), DO-05, DO-06, DO-07, DO-09, DO-10, DO-11, DO-16.
- **Walkthrough:** import a spreadsheet of affiliations against existing contacts; bulk-update a custom field and roll it
  back after a colleague edits one record (conflict shown); bulk-archive a selection; apply a saved mass-update template.
- **Acceptance:** rollback refuses changed records by name; journeys tagged CRM-M19 pass.

#### CRM-M20 — Cross-service safe delete (deferred by Andrew; Platform-led; to be split into slices when planned)

- **Objective:** a contact or organization is hard-deleted only after every application has answered the impact contract.
- **Depends on:** CRM-M15; deletion participants in each application's own plan. **Rows:** LC-03, LC-04, LC-05, AC-05,
  LC-06 (other applications), record-lifecycle WP-L1 and WP-P1 (deletion parts), WP-P0, WP-C1, WP-C2, WP-C4, WP-E4..E6.
- **Walkthrough:** try to delete a contact with an open invoice and a portal login (blocked, reasons shown); clear them;
  approve; change a dependent between preview and approval (refused); complete; open the old link (deletion audit view);
  find the orphaned activity in its view.
- **Acceptance:** record-lifecycle tests T-2..T-4 (T-1 was accepted with archive); the inherited Q-1..Q-7 asked at planning.

#### CRM-M21 — Relationships and households (user-facing; after CRM-Q2; ≈ 6)

- **Rows:** RL-01, RL-02. **Walkthrough:** relate two contacts as parent and child, see the relationship on both records;
  convert a spouse link; create a household and add members. **Acceptance:** no half-updated pair; merge and query support.

#### CRM-M22 — Portal CRM self-service (user-facing; needs Platform portal identity and Security grants; ≈ 10)

- **Order:** last of all scheduled CRM milestones; depends on CRM-M9..M19, M21 and M23; built on Andrew's portal components.

- **Rows:** PO-01..PO-05, OR-07. **Walkthrough:** as a member, update the profile (name locked after first update); as an
  organization administrator, page through a 10,000-person roster, add, edit and detach a contact, manage a child
  organization; confirm details inside an application process. **Acceptance:** every rule enforced at the service; one
  authorization model (Security grants).

#### CRM-M23 — Record-level and field-level security (Platform-led; after CRM-Q8)

- **Rows:** SE-03, SE-04, SE-05. **Walkthrough:** a chapter staff member sees only their chapter's contacts in lists,
  search, 360 and exports; a sensitive field is hidden from a role. **Acceptance:** enforced in the query layer and the
  service, never only in the console.

#### Later candidates (not scheduled until Andrew decides)

Directories (CRM-Q4), engagement scoring (CRM-Q5), enrichment connectors, attention providers for CRM conditions (after
Responsibilities and Attention revision 2 is released), staff "log in as member" (Platform), the Sales application (CRM-Q11).

### 5. Acceptance for every milestone

See [16 §3](#17-16-testing-and-acceptance-strategy). A milestone is accepted only by Andrew after hands-on
use of a verified build; passing tests alone never accepts it.

## 17. 16. Testing and Acceptance Strategy

## 16. Testing and Acceptance Strategy

Part of [RHYTHM-CRM-ARCH-001](#1-rhythm-crm-arch-001-rhythm-2-0-crm-definitive-architecture-and-completion-plan).

### 1. Test levels and where each failure is proved

| Level                               | Proves                                                                                      | Where                                         | Rule                                                                                |
| ----------------------------------- | ------------------------------------------------------------------------------------------- | --------------------------------------------- | ----------------------------------------------------------------------------------- |
| Unit                                | Pure rules (name composition, scoring, region matching, mapping, preferred-value selection) | `modules/*/tests/unit`                        | Fast; no database                                                                   |
| Table integration                   | Database invariants (one primary, uniqueness, FKs, checks, RLS, normalisation)              | `tests/integration/db`                        | A direct insert that violates an invariant is refused                               |
| Service integration                 | Business rules, permissions, concurrency, transactions                                      | `tests/integration/services`                  | **A backend permission or rule is never proved only through the UI** (directive §5) |
| Contract                            | Participant functions (merge; later deletion), business events, service functions           | each participant's contract-kit suite         | Every holder of a reference runs the kit                                            |
| End-to-end (local dispatcher)       | Route behaviour, wire shapes, error codes                                                   | `tests/e2e`                                   | Each route's success and refusal codes                                              |
| Deployed dev                        | Wiring in AWS (authorizer, queues, indexer)                                                 | `tests/e2e/dev`                               | One per integration path                                                            |
| Console component                   | Presentation, form behaviour, action gating                                                 | react-console                                 | —                                                                                   |
| Product acceptance (Playwright)     | Customer journeys across services and console                                               | react-console `acceptance/crm` (617 journeys) | Run on the fleet, never on the Mac                                                  |
| Live-console crawler (new, CRM-M10) | No sample data and no toast-only action on any live route                                   | react-console                                 | Fails the build on a sample marker                                                  |

### 2. Regression criteria from the 1.0 support history

Each 1.0 failure pattern ([02 §4](#3-02-rhythm-1-0-rolodex-and-the-support-history)) becomes a
named test that must exist before the milestone that touches it is accepted.

| Pattern | Test (minimum)                                                                                                                                                                                                                                                 | Level               | Milestone         |
| ------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------- | ----------------- |
| P1      | Merge a contact referenced in every application; a registry walk finds zero references to the merged-away id; the id answers 410 with the survivor; logins moved                                                                                               | Contract + e2e      | M4 (block) / ext  |
| P2      | Executed merge equals the preview field by field; a participant blocker rolls back everything and names the blocker                                                                                                                                            | Service integration | have (M0 extends) |
| P3      | Editing, merging or deleting a record updates its candidate pairs within the stated bound; counts equal rows                                                                                                                                                   | Service integration | M16               |
| P4      | The same person pushed through console, portal sign-up, import upsert and the service function ends as one contact                                                                                                                                             | E2e                 | M16               |
| P5      | A contact never shows an unlinked organization name; an exclusive role refuses a second holder (if CRM-Q7); the applicable-role filter is identical across channels (and, if CRM-Q9 = refuse, the refusal is); ambiguous organization in an import is reported | Service + e2e       | M14 / M19         |
| P6      | An organization address edit appears on linked contacts in console, API and (later) portal                                                                                                                                                                     | E2e                 | have; M22         |
| P7      | A record edit is visible in a query immediately; one row per entity by default; hierarchy queries at depth 5                                                                                                                                                   | Query integration   | M18               |
| P8      | A created record is found in Quick Search within 30 s; exact number match ranks first; merged and deleted records never appear                                                                                                                                 | Deployed dev        | M18               |
| P9      | Validate and apply give identical per-row outcomes; outcomes sum to input rows; 100,000-row file completes                                                                                                                                                     | Engine integration  | have; M19         |
| P10     | Portal roster pages at 10,000 affiliations; a broken reference elsewhere never blocks a profile save                                                                                                                                                           | E2e                 | M22               |
| P11     | Every address change lands on a region; recalculation of a large tenant completes with progress                                                                                                                                                                | have                | —                 |
| P12     | An opted-out or inactive record never appears in a directory (if built)                                                                                                                                                                                        | E2e                 | CRM-Q4            |
| P13     | A saved activity appears on every linked timeline immediately; every activity type is filterable                                                                                                                                                               | E2e                 | have; M17         |

### 3. Guided-build acceptance

Every milestone ends with a guided UI acceptance walkthrough, using the `guided-build-acceptance` process. The walkthrough
is prepared by the session that built the milestone and contains:

1. **Environment:** the running development server URL (opened in Andrew's Chrome), the workspace
   (`acme-acceptance-main` unless stated), and the persona to sign in as.
2. **Seed data:** what the milestone's seed script creates, by name, so every step refers to real records.
3. **Steps:** exact navigation and actions, one per line.
4. **Expected result** for each step, observable on screen.
5. **Negative and edge checks:** permission refusals as a lower-privileged persona, stale-edit conflicts from two
   browsers, limits, empty states.
6. **Reporting:** how to report a defect (reply in the session naming the step) or accept ("accept CRM-Mn").

A milestone is accepted only by Andrew in that walkthrough. Passing unit, integration or Playwright tests never accepts a
milestone on its own, and the last acceptance step shows working functionality, not a summary of changed code. Milestone status
lives only in services-crm `docs/milestones/` (R2-DEV-001); the acceptance dashboard's `waves.json` lists each milestone's
journeys and dependencies and holds no status of its own.

### 4. Acceptance journeys per milestone

New journeys are added to the CRM catalogue for every milestone row whose testing column says "need", using the
`product-acceptance-testing` skill's catalogue and traceability model (capability → field → journey → Playwright scenario
→ result → defect). The milestone's `journeys` list in `waves.json` names the CRM-ACC ids that must pass.

### 5. Performance and scale tests (release-candidate environment)

- A tenant with 1,000,000 contacts and 200,000 organizations: list page p95 < 1 s; 360 read p95 < 500 ms; save p95 < 300 ms.
- Organization merge of 20,000 employees completes in at most 90 s (D9 measured 86.3 s with half linked).
- These budgets are Recommended and run when the release-candidate environment is seeded at that scale (not yet scheduled).
- Region recalculation and compliance reprocess across the tenant complete with progress and no timeout.
- Quick Search freshness under a 10,000-record bulk update.

## 18. 17. Risks, Unresolved Items and Verification Limits

## 17. Risks, Unresolved Items and Verification Limits

Part of [RHYTHM-CRM-ARCH-001](#1-rhythm-crm-arch-001-rhythm-2-0-crm-definitive-architecture-and-completion-plan).

### 1. Risks (ranked)

| #    | Risk                                                                                                                     | Likelihood                           | Impact                                                                               | Mitigation                                                                                   | Owner                                |
| ---- | ------------------------------------------------------------------------------------------------------------------------ | ------------------------------------ | ------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------- | ------------------------------------ |
| R-1  | A merge today strands references in Accounting, Commerce, Membership and Submissions (1.0 pattern P1 recurring)          | High as soon as tenants merge        | Financial and membership records pointing at a missing person                        | Merge block (CRM-Q3) in the retroactive CRM-M4 plan; participants in each application's plan | CRM build session + each application |
| R-2  | services-crm CI fails from **2026-11-07** when SOC 2 High exceptions expire, stopping every merge                        | Certain without action               | All CRM delivery stops                                                               | One decision set to Andrew in the retroactive CRM-M6 plan, before 2026-11-07                 | CRM build session                    |
| R-3  | A delete through the API or bulk delete job orphans other applications' references until CRM-M20                         | High (bulk delete is in the console) | Dangling invoices, memberships, seats, logins                                        | CRM-Q10                                                                                      | Andrew                               |
| R-4  | Rhythm-wide update contract (E-09) changes `PUT` semantics for integration clients                                       | Medium                               | An integration expecting omitted fields to clear stops clearing them                 | Caller inventory, versioned rollout, explicit replace operation                              | Platform standard owner              |
| R-5  | Live console shows sample data or inert controls to customers                                                            | High today                           | Loss of trust; wrong decisions on fake data                                          | CRM-M10 and the live crawler                                                                 | Console                              |
| R-6  | Custom-field administration persists nothing                                                                             | Certain today                        | Tenants cannot configure fields                                                      | CRM-M11                                                                                      | Console + Platform                   |
| R-7  | Quick Search not updating in dev (DEF-CRM-0119)                                                                          | Observed                             | Users cannot find new records                                                        | CRM-M18, freshness test                                                                      | Platform                             |
| R-8  | Hierarchy not queryable                                                                                                  | Certain                              | Chapters and corporate parents cannot be segmented                                   | CRM-M18                                                                                      | CRM                                  |
| R-9  | Other applications read CRM's physical tables                                                                            | Ongoing                              | A CRM column rename breaks Accounting views, Membership flow-down, Commerce checkout | Published views (CRM-ADR-05)                                                                 | CRM + readers                        |
| R-10 | Region id migration touches Platform saved queries and recipes; task id holders outside CRM to be inventoried            | Medium                               | Broken references after the migration                                                | Mapping table, same-release rewrite                                                          | CRM-M15                              |
| R-11 | Organization merges with many participants exceed the measured transaction time                                          | Low–medium                           | Longer locks                                                                         | Benchmark again as participants land, against D9                                             | CRM                                  |
| R-15 | Other applications' participants depend on Andrew's focus order; until then merges of their records are blocked (CRM-Q3) | Certain for now                      | Staff cannot merge some duplicates                                                   | Each application's plan                                                                      | Andrew's focus order                 |
| R-12 | Acceptance defect register lags code (register 357 defects; commits cite DEF-CRM-0741)                                   | Observed                             | Status over- or under-reported                                                       | Acceptance session reconciles before the release-candidate report                            | Acceptance session                   |
| R-13 | Andrew's local services-crm checkout is 102 commits behind `origin/master`                                               | Observed                             | Reading stale code locally                                                           | Read `origin/master`; pull when convenient                                                   | —                                    |
| R-14 | `master` of services-crm has no branch protection and no secret or dependency scanning                                   | Present                              | Unreviewed changes; leaked secrets undetected                                        | Administrator settings (retroactive CRM-M6 decision set)                                     | Andrew/admin                         |

### 2. Unresolved items

- Decisions needing Andrew: CRM-Q1..Q11 ([13 §3](#14-13-decisions-standing-recorded-and-needing-andrew-deliverable-d)).
- Deferred to CRM-M20: record-lifecycle Q-1..Q-7.
- Conflicts surfaced: C-1..C-8 ([13 §5](#14-13-decisions-standing-recorded-and-needing-andrew-deliverable-d)).
- Product questions still open in acceptance and not settled here: whether `DELETE` must carry a version
  (write-contracts standard, open question to Andrew); D-15 Effective Permissions and D-03 merge picker are approved but not
  started.

### 3. What could not be verified, and the consequence

| Item                                                  | Why                                                                                   | Consequence for confidence                                                                                                                                         |
| ----------------------------------------------------- | ------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| HubSpot and Linear history                            | Both returned zero records (2026-10-19 16:39–16:40 ET)                                | Support patterns rest on Jira RB and Zendesk only; consistent across six years, so high confidence in the patterns found, moderate that none were missed           |
| Deployed behaviour beyond code and tests              | No Playwright or live API runs were made for this document (runs belong to the fleet) | Statuses rely on tests in the code and the acceptance suite's last full cycle, which predates repair waves 2–3; the Tuesday release-candidate run will update them |
| Compliance e-mail delivery                            | CRM's part ends at the hand-off to Platform communications; delivery was not traced   | CM-01 is complete for CRM's part only                                                                                                                              |
| Exact-match ranking in Quick Search                   | Not tested                                                                            | SR-03 marked Unknown                                                                                                                                               |
| Newer Accounting tables (remittances, customer terms) | The local Accounting checkout was from 2026-10-18                                     | Their references are taken from the record-lifecycle inventory, not re-read                                                                                        |
| Same-id retry semantics on every create route (D-43)  | Not read route by route                                                               | WR-03 left In retro                                                                                                                                                |
| Some 1.0 repositories                                 | Read from GitHub where not cloned; history squashed by a BFG rewrite                  | 1.0 behaviour read from code at HEAD only, not its history                                                                                                         |

## 19. 18. Traceability Appendix

## 18. Traceability Appendix

Part of [RHYTHM-CRM-ARCH-001](#1-rhythm-crm-arch-001-rhythm-2-0-crm-definitive-architecture-and-completion-plan).

### 1. Brief → document

| Brief section                                                  | Where answered                                                                                                                                                                       |
| -------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| §1 A Existing artifacts (Commerce benchmark)                   | [01 §4](#2-01-product-vision-architectural-principles-and-research-method); structure mirrors `commerce-orders/`                                                                      |
| §1 B Rhythm 1.0 (Rolodex)                                      | [02](#3-02-rhythm-1-0-rolodex-and-the-support-history); [e01](#20-e01-rhythm-1-0-rolodex-records-and-relationships-evidence), [e02](#21-e02-rhythm-1-0-rolodex-activities-engagement-tasks-duplicates-and-merging-search-data-operations)                      |
| §1 C Rhythm 2.0 implementation                                 | [03](#4-03-rhythm-2-0-crm-today-assessment-by-functional-area); [e03](#22-e03-rhythm-2-0-crm-backend-assessment-services-crm), [e04](#23-e04-crm-console-assessment-react-console), [e05](#24-e05-platform-capabilities-crm-depends-on-and-every-other-application-s-references-to-crm-records)  |
| §1 D Jira and Zendesk                                          | [02 §4](#3-02-rhythm-1-0-rolodex-and-the-support-history); [e06](#25-e06-customer-pain-with-rhythm-1-0-crm-rolodex-jira-zendesk-hubspot-linear-github)                                                       |
| §1 E Existing decisions                                        | [13 §1](#14-13-decisions-standing-recorded-and-needing-andrew-deliverable-d)                                                                                                |
| §2 A Contacts                                                  | [05 §2.1](#6-05-functional-architecture-what-every-crm-capability-does)                                                                                                                                 |
| §2 B Organizations (incl. compliance e-mail, LiquidJS preview) | [05 §2.2, §2.5](#6-05-functional-architecture-what-every-crm-capability-does)                                                                                                                      |
| §2 C Affiliations                                              | [05 §2.3](#6-05-functional-architecture-what-every-crm-capability-does); [04 §4](#5-04-domain-and-relational-data-model)                                                         |
| §2 D Addresses, e-mail, phones                                 | [05 §2.4](#6-05-functional-architecture-what-every-crm-capability-does)                                                                                             |
| §2 E Activities                                                | [05 §3](#6-05-functional-architecture-what-every-crm-capability-does)                                                                                                                                  |
| §2 F Tasks                                                     | [05 §4](#6-05-functional-architecture-what-every-crm-capability-does)                                                                                                                    |
| §2 G Data quality                                              | [05 §6](#6-05-functional-architecture-what-every-crm-capability-does)                                                                                                                                |
| §2 H Duplicates and merge                                      | [07](#8-07-duplicate-detection-and-record-merging)                                                                                                                                        |
| §2 I Custom fields                                             | [05 §2.7](#6-05-functional-architecture-what-every-crm-capability-does)                                                                                                                            |
| §2 J Search, queries, Explorer                                 | [09 §2–4](#10-09-console-experience-search-explorer-queries-and-the-360-pages)                                                                                                                                         |
| §2 K 360 experiences                                           | [09 §5](#10-09-console-experience-search-explorer-queries-and-the-360-pages)                                                                                                                                    |
| §2 L Data operations                                           | [10](#11-10-data-operations-imports-upserts-exports-bulk-changes-and-rollback)                                                                                                                                                          |
| §2 M Security, audit, APIs, operations                         | [06](#7-06-api-and-service-architecture), [11](#12-11-security-audit-and-operational-contracts)                                                                                                  |
| §3 Boundaries                                                  | [12](#13-12-cross-application-integration-contracts)                                                                                                                                              |
| §4 Gap matrix                                                  | [14](#15-14-implementation-gap-matrix-deliverable-b)                                                                                                                                                               |
| §5 Completion plan and guided-build acceptance                 | [15](#16-15-milestone-plan-completion-roadmap-deliverable-c), [16 §3](#17-16-testing-and-acceptance-strategy)                                                                                      |
| §6 Artifact sections 1–20                                      | README (1), 01 (2–3), 02 (4–5), 03 (6), 05 (7), 04 (8), 06 (9), 11 (10), 09 (11), 12 (12), 07 & 05 §6 (13), 04 §4 & 07 §5 (14), 13 (15), 14 (16), 15 (17), 16 (18), 17 (19), 18 (20) |
| §7 Challenge designs                                           | [13 §2, §5](#14-13-decisions-standing-recorded-and-needing-andrew-deliverable-d)                                                                   |
| §8 Validation                                                  | [README §Validation](#1-rhythm-crm-arch-001-rhythm-2-0-crm-definitive-architecture-and-completion-plan)                                                                                                                      |
| Safe delete (Andrew's addition)                                | [08](#9-08-record-lifecycle-and-safe-delete)                                                                                                                                                |

### 2. Source index

| Kind                  | Location                                                                                                                                                               |
| --------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| CRM backend           | `github.com/rhythmsoftware/services-crm` `origin/master` `a980b553`                                                                                                    |
| Console               | `github.com/rhythmsoftware/react-console` `1f345862b` (CRM screens), `f08b10233` (latest checked)                                                                      |
| Platform              | services-platform master `339fb04f1`; branches `agent/crm-acc-w3-arch-lifecycle` `6873e3b6c`, `agent/crm-acc-w3-arch-writes`                                           |
| Acceptance            | react-console branch `product-acceptance-crm` `82ab9ed58`: `acceptance/crm/reports/wave3-directives.md`, `wave4-directives-E.md`, `decision-register.json`, `defects/` |
| CRM sub-architectures | services-crm `docs/affiliations/README.md`; `docs/merging/*`; `docs/debt/*`; `docs/migrations/*`                                                                       |
| Other applications    | services-accounting `43aba578`, -commerce `bd62910a`, -committees `bd2718a`, -membership `fcbddf2`, -submissions `75ae48b`, -websites `f038650`                        |
| Rhythm 1.0            | `~/rhythm/rolodex-*`, `libraries-*`, `platform-*`, `portals-ngx-site-renderer`; GitHub for repositories not cloned                                                     |
| Support history       | Jira project RB; Zendesk (access log in e06)                                                                                                                           |

## 20. e01 — Rhythm 1.0 Rolodex: records and relationships (evidence)

## e01 — Rhythm 1.0 Rolodex: records and relationships (evidence)

Research for the Rhythm 2.0 CRM architecture. What the Rhythm 1.0 CRM ("Rolodex") lets users do with contacts, organizations
and the things that connect them, with the business rules, edge cases and workarounds found in code. Compiled 2026-10-19.

### How to read this file

- **Citations** are `repo/path:line` relative to `~/rhythm/` (Rhythm 1.0 checkouts). Repositories marked `(gh)` were not checked
  out locally and were read from a shallow `gh repo clone rhythmsoftware/<repo>` on 2026-10-19 (default branch head).
- **Secondary sources** are cited by name and were checked against code where the claim is load-bearing:
  - `SPEC` = the formal 1.0 retrofit `~/rhythm/docs/specs/apps/rolodex/` (879 capabilities in 10 areas, `CAPABILITY_CATALOG.md`;
    131 findings `FINDINGS.md` FND-ROL-001..132; permission registry `PERMISSIONS.md`). Not runtime-verified by its authors.
  - `AFF-INV` = `services-crm/docs/affiliations/inventory-rhythm-1.md` (2026-10-13).
  - `MERGE-A` = `services-crm/docs/merging/legacy-assessment.md`.
  - `MIG/<name>` = `services-crm/docs/migrations/<name>/README.md` (2.0 port records that quote 1.0 behaviour).
- **UNVERIFIED** marks a claim taken from a secondary source and not re-read in 1.0 code during this pass.
- `~/rhythm/rolodex` and `~/rhythm/rolodex2` are **not** Rhythm 1.0: they are an early PostgreSQL macroservice POC
  ("Contacts and organizations on PostgreSQL, the first macroservice of the new platform", `rolodex/README.md:3`). They are
  excluded as evidence of 1.0 behaviour. `~/rhythm/discover-rolodex` is empty.

#### Coverage gaps in this file (see the coordinator's note)

Six parallel deep-read passes (contacts, organizations, relationships/roles/regions, directories/steps/portal/security,
Angular console UI, portal controls) ran but their reports were not delivered back to this author. This file was written from
direct reads of schemas/config, the cloned repos, the SPEC findings and the 2.0 inventories. Parts that are thinner than intended
and should be merged from those reports:

1. **Console UI screens** (`rolodex-ngx-console`, `rolodex-ngx-controls`, `rolodex-ngx-console-controls`): only behaviours cited
   by SPEC / AFF-INV / MERGE-A are included; no screen-by-screen inventory.
2. **Portal controls** (`rolodex-ngx-portal-controls` (gh)): component list and SPEC findings only.
3. **Directories** (`rolodex-contact-directories`, `rolodex-organization-directories`): the directory definition fields come from
   the shared `@rhythmsoftware/directories` schema (`rolodex-contact-directories/src/schema/object.schema.js:17`,
   `...directorySchema.getSchema()`), which was not read; content here is from SPEC.
4. **Contact steps / provisioning / eventbridge / libraries-rolodex-***: surface-level only.
5. **Contacts and organizations handlers** (`src/logic/*`, `src/lambda/*`): schemas read line by line; logic mostly via
   SPEC/MIG with spot checks.

---

### 1. Contacts (`rolodex-contacts`)

#### 1.1 Record shape — what staff can record about a person

Schema: `rolodex-contacts/src/schema/object.schema.js`.

| Group           | Fields (line)                                                                                                                                                                                                                                                        | Notes                                                                                                                                                                                                                                                               |
| --------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Identity        | `id` (:14, `^[\w                                                                                                                                                                                                                                                     | -]+$`), `number` (:15, sequential contact number)                                                                                                                                                                                                                   | Number assigned by the shared numbering (`MIG/contact-and-organization-cleaning` C-33). SPEC FND-ROL-018: numbers are sequential and guessable |
| Name parts      | `prefix` (:29), `salutation` (:32), `first_name` (:34), `middle_name` (:35), `last_name` (:38), `maiden_name` (:39), `nickname` (:40), `suffix` (:44), `credentials` (:45)                                                                                           | No part is required by the schema                                                                                                                                                                                                                                   |
| Computed names  | `name` (:17), `full_name` (:18), `name_last_first` (:19)                                                                                                                                                                                                             | Computed by `calculate-full-name.js`; format documented in `MIG/contact-names-derived-full-name` (name = first, middle initial, `"nickname"` unless equal to first, last, ` suffix`, `, credentials`; full_name = prefix + name; sort name `last, first m. "nick"`) |
| Organization    | `organization_id` (:20), `organization_name` (:21, free text allowed without id), `new_organization_info` (:22, inline org creation), `contact_role_ids[]` (:24), `contact_roles` (:25, READ ONLY names), `job_title` (:28)                                          | One current organization only (AFF-INV §1)                                                                                                                                                                                                                          |
| Flowdown links  | `link_work_address_to_organization` (:42), `link_work_phone_number_to_organization` (:43)                                                                                                                                                                            | See §4                                                                                                                                                                                                                                                              |
| Emails          | `email_address` (:46), `email_address2` (:47), `email_address3` (:48)                                                                                                                                                                                                | Three fixed slots; `Joi.string()` only — no format validation (`MIG/contact-duplicate-email-prevention` table)                                                                                                                                                      |
| Phones          | `mobile` (:49), `work_mobile` (:50), `home` (:52), `work` (:53), `fax` (:54), `school` (:55), `other_phone_numbers[] {type_id, phone_number}` (:63-66), `preferred_phone_number` (:57, calculated), `preferred_phone_number_type` (:59, `mobile                      | home                                                                                                                                                                                                                                                                | work                                                                                                                                           | work mobile                                                                                                            | alternate | school | fax`), `preferred_phone_number_type_id`(:60, required when`alternate`) |     |
| Addresses       | `home_address` (:77), `work_address` (:78), `school_address` (:79), `seasonal_address` (:80) + `seasonal_address_start/_end` (:81-82), `other_addresses[] {type_id, address}` (:92-95), `preferred_address` (:99, calculated), `preferred_address_type` (:100, `home | work                                                                                                                                                                                                                                                                | school                                                                                                                                         | alternate`— seasonal is not selectable),`preferred_address_type_id` (:101)                                             |           |
| Legislative     | `national_district` (:86), `state_upper_district` (:88), `state_lower_district` (:90)                                                                                                                                                                                | Fed by the archived `rolodex-contact-districts` (SPEC REPOSITORIES §1)                                                                                                                                                                                              |
| Region          | `region_id` (:73), `override_automatic_region_assignment` (:74)                                                                                                                                                                                                      | §7                                                                                                                                                                                                                                                                  |
| Personal        | `spouse_id` (:51, `.not(Joi.ref('id'))`), `image_url` (:104), `gender` (:105, `male                                                                                                                                                                                  | female`only),`pronouns`(:108),`date_of_birth`(:110),`date_of_death` (:111)                                                                                                                                                                                          | Spouse is a one-way pointer (SPEC FND-ROL-071)                                                                                                 |
| Social          | `facebook`, `twitter`, `instagram`, `linked_in` (:131-134), `date_social_profile_last_updated` (:112)                                                                                                                                                                |                                                                                                                                                                                                                                                                     |
| Status          | `status` (:120) `active                                                                                                                                                                                                                                              | duplicate                                                                                                                                                                                                                                                           | deceased                                                                                                                                       | inactive`, default `active`                                                                                            | §1.3      |
| History         | `organization_history[] {organization_id (req), job_title, contact_role_ids[], start_date, end_date, notes}` (:121-128)                                                                                                                                              | §5.2                                                                                                                                                                                                                                                                |
| Portal identity | `user_ids[]` (:135, linked portal logins), `merged_ids[]` (:136, never written — MERGE-A §2)                                                                                                                                                                         |                                                                                                                                                                                                                                                                     |
| Communication   | `do_not_mail` (:146), `do_not_call` (:147), `email_opt_out` (:148) (all default false), `communication_preferences[] {communication_type_id, enabled, subscription_email_address, subscription_status subscribed                                                     | digest                                                                                                                                                                                                                                                              | unsubscribed, subscription_last_updated}`(:149-155),`communication_preferences_last_confirmed` (:158)                                          | Types from `rolodex-communication-types` (gh) `src/schema/object.schema.js:14-20` (`name`, `description`, `is_active`) |
| Delegation      | `portal_delegates[] {contact_id, expiration_date, status: approved only}` (:160-166)                                                                                                                                                                                 | §10.3                                                                                                                                                                                                                                                               |
| Provenance      | `origin_service` (:143), `origin_service_record_id` (:144)                                                                                                                                                                                                           | Survivor's origin kept on merge (MERGE-A #22, RB-3552)                                                                                                                                                                                                              |
| Profile         | `date_profile_last_updated` (:16)                                                                                                                                                                                                                                    | Drives portal first-time behaviour (§10)                                                                                                                                                                                                                            |
| Custom fields   | `custom_field_values[]` (:137)                                                                                                                                                                                                                                       | §9                                                                                                                                                                                                                                                                  |

**There is no contact type in 1.0.** No `type_id`/contact-type field exists on the contact schema (read above), and 2.0's contact
types are recorded as "new Rhythm 2.0 functionality, not a port: there is no Rhythm 1.0 service" (`MIG/contact-types`).
`events-exhibitor-contact-types` exists in `~/rhythm` but belongs to Events, not Rolodex.

#### 1.2 Save-time cleaning (what the system does to what users type)

Full inventory in `MIG/contact-and-organization-cleaning` items C-1..C-39 (verified there against
`rolodex-contacts/src/object/cleaning.js` at `8bc9a5b2`). The product-visible ones:

- Name parsing: a contact sent with only `name`/`full_name` is split into parts with a third-party parser (RB-4120, RB-4385, C-16).
- Honorific salutation moved to `prefix` (RB-2367, C-3); prefix start-cased and dotted (C-4); nickname equal to first name dropped (RS-2647, C-8).
- Every `*_phone_number` formatted (RS-2635, C-7); blank `other_phone_numbers` dropped (C-11).
- Blank/null e-mails dropped (RB-1699, C-12). E-mails otherwise stored as typed (only trimmed).
- Preferred address and phone computed from the preferred type on save and every read (C-10).
- No organization ⇒ `contact_role_ids` deleted (RB-1364, C-6); role ids de-duplicated (C-1, C-9); `contact_roles` names copied (C-2).
- Inline organization creation: `organization_id` of form `__new:<name>` or `new_organization_info` creates the organization
  **before validation** (C-15; SPEC FND-ROL-054: a refused save leaves an orphan organization, resubmitting creates another).
- `organization_name` refreshed from the organization (C-18); removing the organization drops `organization_name` if it still
  equals the old name (RB-738, C-23).
- Linked work address/phone copied from the organization (C-5; §4).
- Preferences of unknown communication types dropped (RB-4275, C-13; SPEC FND-ROL-070: a transient read failure deletes consent).
- Legacy login fields (`portal_user_name`, `last_portal_login_date`, `login_count`, `last_ip`) deleted (RB-1280, C-19).
- E-mail filled from the login token (RB-7322, C-20; origin/master only).
- Country/state codes standardised on addresses (C-31; libraries-dynamodb `Country()`/`State()`).
- `deepTrim` of all strings; defaults `status=active`, `do_not_mail/do_not_call/email_opt_out=false` (C-28, C-29).

#### 1.3 Statuses

- Values: `active`, `duplicate`, `deceased`, `inactive` (`rolodex-contacts/src/schema/object.schema.js:120`). Organizations have
  `active`, `duplicate`, `inactive` — no deceased (`rolodex-organizations/src/schema/object.schema.js:62`).
- Business meaning is thin: status is an ordinary field. `duplicate` is a **label only** — it does not link to a surviving record
  and does not trigger a merge (`merged_ids` is never written, MERGE-A §2 "Old ids").
- Effects found: an inactive contact cannot be linked to a portal login (`contact-user-logic.js` `addUserToContact` is "the only place
  that checks the contact is active", SPEC FND-ROL-028 — UNVERIFIED line); inactive contacts are hidden from pickers and directories
  list only active records by default (`apply-base-query-filters.js`, SPEC FND-ROL-023).
- Staff change status through a Change Status dialog gated by `rolodex:user:contacts:change-status` (SPEC PERMISSIONS). The
  dialog's Notes box is bound to the record's own `notes`, so **every status change overwrites the record's notes and no status
  history is kept** (SPEC FND-ROL-053).
- A portal member can change their own status (no change rule protects it) (SPEC FND-ROL-010; `change-validation.js` protects only
  `user_ids` and `merged_ids`).

#### 1.4 Uniqueness and duplicates

- **Duplicate e-mail prevention** is a tenant setting `duplicate_prevention: disabled|portal|both`
  (`rolodex-settings` (gh) `src/schema/object.schema.js:18`). When on, a new primary e-mail that another contact holds is refused
  (`IllegalOperation`, HTTP 405, "The email address you specified already belongs to an another contact."); only `email_address`
  is compared, lower-cased and trimmed via the `ByEmailAddress` index; checked only when the primary e-mail changes; a query-before-save
  race exists; `portal` mode exempts console users (`MIG/contact-duplicate-email-prevention` legacy table). Settings cached
  10,000 s per Lambda (comment says 10 s) and a case-only change of one's own e-mail is refused as a duplicate of itself
  (SPEC FND-ROL-082).
- **Duplicate detection** (`rolodex-contact-duplicates` (gh)): match keys on e-mail, sound-alike names with a 1,963-name nickname
  table, postal codes; threshold 35; imports excluded; dismissals permanent and unaudited; public unauthenticated portal check
  (MERGE-A §1). Organization detection switched off since 2020 (MERGE-A §1).
- **Merge**: N sources into a survivor via `PUT /{tenantId}/{survivorId}/merge`; preview and execution use different algorithms;
  arrays of objects collapse to one element; not atomic; failures reported as success; old ids hard-deleted with no alias
  (MERGE-A §2, capabilities #15-39). Merge step function lives in `rolodex-contact-steps/build/stateMachines/merge-contact.step.json`.
- No uniqueness on names, numbers are system-assigned.

#### 1.5 Deletion

- Hard delete via the shared persistence library. Blocked while a dependent record references the contact through the
  dependency registry (`deleteAction: none`), e.g. organization/address-type/phone-type references are declared on the contact
  (`rolodex-contacts/config.js:87-113`) and other services declare theirs against contacts.
- The contact's own deletion-validation hook is a template stub, not wired: a contact linked to portal logins is deleted with the
  logins left pointing at it (SPEC FND-ROL-071). A spouse reference blocks deleting the spouse (one direction only).
- Relationships cascade-delete with either side (`rolodex-relationships/config.js:77-84`, `deleteAction: "cascade"`, service name
  resolved indirectly as `$.left_side_type`; whether the library resolves that form is unknown, SPEC FND-ROL-063).

#### 1.6 Side effects of a contact save (streams/events)

- Automatic contact roles on create or organization change (§6.2).
- Organization history append on organization change (§5.2).
- Contact role compliance recheck for old and new organization when `organization_id` or `contact_role_ids` change, or on
  create/delete (`MIG/contact-role-compliance`, triggers; `rolodex-organizations/src/logic/extract-organizations-from-contact-changes.js`).
- Region assignment when the deciding address changes (§7).
- Duplicate detection (except imports) (MERGE-A §1).
- EventBridge forwarding of contact change events to customers (`rolodex-eventbridge`; content not read — UNVERIFIED), including
  `rolodex-contacts-merged` with the security context and pre-merge record (MERGE-A #33).

---

### 2. Organizations (`rolodex-organizations`)

#### 2.1 Record shape

Schema `rolodex-organizations/src/schema/object.schema.js`:

- Identity: `id` (:13), `number` (:15, **required**, described "The contact number" — copy defect), `name` (:16, **required**), `acronym` (:17).
- Classification: `type_id` (:14), `flags[]` (:61, free strings "what type of organization this is"), `status` (:62), `tax_exempt` (:64).
- Web: `website` (:19), `domain_name` (:20), `alternate_domain_names[]` (:21).
- Addresses: `address` (:25, main), `billing_address` (:32), `other_addresses[] {type_id, address}` (:34-37).
- Phones: `main_phone_number` (:41), `fax_phone_number` (:42), `alt_phone_number` (:43), `other_phone_numbers[] {type_id, phone_number}` (:46-49).
- Hierarchy: `parent_organization_id` (:58), computed `ancestor_organization_ids[]` (:55), `number_of_ancestors` (:56), `top_level_organization_id` (:57).
- Region: `region_id` (:28), `override_automatic_region_assignment` (:29).
- Compliance: `contact_role_compliance_status` (:65, `in compliance|not in compliance`), `contact_role_compliance_violations[] {contact_role_id, contact_role_name, number_of_contacts, violation_type too few|too many, compliance_value}` (:66-72).
- Other: `description` (:24), `notes` (:59), `logo_url` (:60), social links (:74-77), `custom_field_values` (:78), `directory_comment_rating_metrics {rating_sum, rating_count}` (:81-83), `date_profile_last_updated` (:18), `sys_search_*` (:86-91).

#### 2.2 Cleaning and rules

- Website ↔ domain derivation, `www.` stripped (RS-2569, RS-2863; `MIG/contact-and-organization-cleaning` O-1). A website with a
  path keeps the path in the domain (`acme.com/about`) and never matches an e-mail domain; alternate domains never tidied; domain
  lookup rows (`rolodex_organization_domains`) case-sensitive, a second claim silently overwrites the first, and a dropped domain's
  row can be deleted out from under another organization (`organization-change-logic.js:175-201`, SPEC FND-ROL-057).
- Phone formatting (O-2), country/state cleaning (O-5), `country_name` derived (O-6), status default `active` (O-8).
- Renaming an organization rewrites every contact's `organization_name` directly (no version check, no audit, `sys_search_organization_name`
  not updated; a deleted contact stalls the whole message) (`organization-change-logic.js`, SPEC FND-ROL-056; AFF-INV §1).
- Compliance computed on create only (`src/object/cleaning.js:19-22`, SPEC FND-ROL-052); type change triggers no recheck.

#### 2.3 Hierarchy (parent/child)

- One `parent_organization_id`; ancestors computed on save (`src/logic/ancestors.js`) nearest-first; root stored as
  `top_level_organization_id` (`MIG/organization-ancestors`).
- **No cycle prevention**: the walk includes the repeated organization before stopping, so an organization can be stored as its own
  ancestor; the depth limit is written `while (parent || depth > 10)` and never applies (`ancestors.js:34-48`, SPEC FND-ROL-058).
- **Descendants are not recomputed when a branch moves** (SPEC FND-ROL-011) — stale ancestor lists then drive portal reach (§10).
- Portal "child organization management" settings (§10.2).

#### 2.4 Statuses, deletion

- Statuses `active|duplicate|inactive` (:62); Change Status dialog overwrites `notes` (SPEC FND-ROL-053). **Special case:** the exact
  note text `skip flowdown processing` switches off flowdown (`rolodex-organizations/src/lambda/flowdown.js:35-36`, SPEC FND-ROL-053).
- Deletion blocked while any contact references it (`rolodex-contacts/config.js:112-113`, `deleteAction: none`; AFF-INV §1).
  Portal members can delete their own organization or an ancestor (SPEC FND-ROL-009).
- Public read `GET /organizations/{tenantId}/public/{id}` returns the **whole** record to any portal member
  (`src/lambda/api.js:38-54`, SPEC FND-ROL-014).
- Organization merge: two-record page, no preview, irreversible confirmation; publishes no event (MERGE-A §2, §4).

---

### 3. Types and type configuration

#### 3.1 Organization types (`rolodex-organization-types` (gh))

Schema `src/schema/object.schema.js`:

- `id` (:15, `^[\w|-]+$`, generated — the client cannot supply it, `generate-identifier.js:14` throws "Not implemented" and the
  library generates), `name` (:16, required), `is_active` (:58, default true).
- **Applicable roles**: `restrict_contact_roles` (:18) + `applicable_contact_role_ids[]` (:19, min 1 when restricted). Enforced
  only in the console's role picker (`applicable-contact-role.service.ts`, AFF-INV §1/§3); the server accepts any role.
- **Key roles**: `enable_key_roles` (:21) + `key_roles[{role_id}]` (:22-27), de-duplicated on save (`src/object/cleaning.js:17-18`).
  Removed in 2.0 by Andrew 2026-10-11 (AFF-INV §3). Console trash button always removes the first row (SPEC FND-ROL-097).
- **Per-type forms**: `console_create_organization_form_id`, `console_edit_organization_form_id`, `portal_update_organization_info_form_id`
  (org forms, :29-31), `portal_create_organization_contact_form_id`, `portal_edit_organization_contact_form_id` (contact forms, :33-34).
  Form id regex `^rolodex-organizations__` / `^rolodex-contacts__`.
- **Contact role compliance** (§3.2).
- Deletion: blocked while an organization uses the type (organizations' `type_id` dependency; `MIG/organization-types`). The type's
  own dependency on compliance-rule roles is declared at `$.contact_role_compliance_rules[*].contact_role_id`
  (`config.js:57`) — a path that matches nothing because rules nest under `contact_role_compliance_options` (SPEC FND-ROL-043), so a
  role named in a rule can be deleted.
- Cached 60 s by readers (SPEC FND-ROL-088).

#### 3.2 Organization-type compliance and compliance e-mails

- Config: `enable_contact_role_compliance` (:38) + `contact_role_compliance_options` (:39-55): `contact_role_compliance_rules[{contact_role_id, minimum (blank = 0), maximum (blank = unlimited)}]` (min 1 rule),
  `notify_contacts_of_compliance_violation` (:46), `notification_options {notification_contact_role_ids[], email}` (:47-51; e-mail
  template required when notify is on — but the `when` refers to a non-sibling key, so only the browser enforces it, SPEC FND-ROL-097).
- Rules with neither minimum nor maximum truthy are **dropped on save** (`src/object/cleaning.js:30-37` `r.minimum || r.maximum`),
  so "at most 0" is lost (SPEC FND-ROL-049). The console also drops rules for inactive/non-applicable roles on every save.
- Evaluation (`rolodex-organizations/src/logic/contact-role-compliance.js`, per `MIG/contact-role-compliance`): each contact of
  the organization (any status) counts once per role it holds; fewer than a truthy minimum ⇒ `too few`; more than maximum (0
  included) ⇒ `too many`; status `in compliance`/`not in compliance`; persisted only when the violations list changed.
- **E-mail**: when status _becomes_ `not in compliance` and notify is on, one e-mail per contact holding a notification role (every
  contact when the list is absent) with an e-mail address, template context `{contact, organization}`, type "Contact Role Compliance
  Notification". The console always saves an empty list, so "absent" vs "empty" is ambiguous (SPEC FND-ROL-088).
- Triggers: org create with a type; any contact change of organization or roles; contact create/delete. Full reprocess
  `reprocess-contact-role-compliance.js`, gated by `rolodex:admin:organizations:reprocess-contact-role-compliance` (SPEC PERMISSIONS).
- Defects: counts only the first page of contacts — a large organization reads as having none (SPEC FND-ROL-044); rechecks can
  e-mail twice (FND-ROL-088); status writable by any caller (FND-ROL-052); preview e-mail sends real data to any typed address with
  no permission (`preview-contact-role-compliance-email.component.ts:46-69`, `rolodex-organizations` `api.js:155-186`, FND-ROL-025).

#### 3.3 Address types, phone number types (`rolodex-address-types`, `rolodex-phone-number-types` (gh))

- Fields: `id`, `name` (both `.insensitive()` — which does nothing for uniqueness, SPEC FND-ROL-061), `display_order`,
  `applies_to contacts|organizations|both` (default contacts), `show_in_portal`, `show_in_console`, `required_in_portal`, `is_active`
  (`rolodex-address-types/src/schema/object.schema.js:14-21`; `rolodex-phone-number-types/src/schema/object.schema.js:14-22`).
- Phone types add `flowdownable` (:18): "the user has the ability to flow down the organization phone numbers to phone numbers of this type".
- Custom types only fill `other_addresses` / `other_phone_numbers` and the `alternate` preferred slot; built-in slots are fixed
  attributes. Nothing seeded; applicability UI-only; delete blocked while referenced (`rolodex-contacts/config.js:87-107`) except
  via direct bulk delete (SPEC FND-ROL-048). Identifiers cannot be changed (SPEC FND-ROL-061).

#### 3.4 Relationship types (`rolodex-relationship-types` (gh))

- `id` (:15, allows `:`), `name` (:16), `left_side_type`, `right_side_type` (:18-19, valid `RELATIONSHIP_SERVICES` =
  contacts/organizations), `is_active` (:21). No reciprocal labels. Side kinds can be changed while in use and then every relationship
  under the type refuses to save (SPEC FND-ROL-062).

#### 3.5 Contact types — none (see §1.1).

---

### 4. Addresses, e-mails, phones and flowdown

- **Cardinality**: contact = 4 fixed addresses + seasonal window + N `other_addresses` (one per type is not enforced), 3 e-mails,
  6 fixed phones + N others; organization = main + billing address + N others, 3 fixed phones + N others (§1.1, §2.1).
- **Preferred**: one preferred address/phone per contact, by _type_ (`preferred_address_type`, `preferred_phone_number_type`),
  with the value copied into `preferred_address`/`preferred_phone_number`. Organizations have no preferred flag (main is implicit).
  The merge conflict logic ignores `preferred_address` (MERGE-A §2).
- **Validation**: almost none — e-mails and phones are `Joi.string()`; phones are reformatted; addresses get country/state codes.
  No address verification service found (UNVERIFIED negative).
- **Flowdown (organization → contact)**:
  - Contact flags `link_work_address_to_organization` / `link_work_phone_number_to_organization`
    (`rolodex-contacts/src/schema/object.schema.js:42-43`) make the contact's `work_address` = org `address` and `work_phone_number`
    = org `main_phone_number`, overwritten on every contact save (`src/logic/copy-linked-org-info.js`) and pushed when the
    organization changes (`rolodex-organizations/src/logic/flowdown/perform-flowdown.js`) (`MIG/organization-flowdown`;
    `MIG/configurable-address-phone-types` "What Rhythm 1.0 did").
  - Console: changing the organization turns the link flags on when the work address/phone is empty or already matches; clearing the
    organization clears links, work phone, work address and name (`contact-work-information-web-forms.component.ts`, AFF-INV §1).
  - Opt-out per organization via the `skip flowdown processing` note (§2.4).
  - Defects: flowdown step function has no Retry/Catch, loads only the first page of contacts; removing an org address copies nothing
    on the org side but blanks linked contact values on the contact side; links survive organization removal (commented `RB-61`
    block) (`build/template-logic.yaml` `SmPerformFlowdown`, `load-flowdownable-objects.js`, `perform-flowdown.js:27-28`, SPEC FND-ROL-051).
  - The phone type `flowdownable` flag (§3.3) suggests a planned per-type flowdown; no consumer was found in this pass (UNVERIFIED).

---

### 5. Relationships, affiliations, employment

#### 5.1 Current affiliation — one organization per contact

- A contact has exactly zero or one current organization (`organization_id`), roles there (`contact_role_ids`) and a job title on the
  contact (not per organization). **There is no primary-employer flag and no multiple current affiliations** (AFF-INV summary,
  verified against the schema §1.1). MemberSuite conversion put non-primary affiliations — some still current — into
  `organization_history` (AFF-INV §5).

#### 5.2 Organization history

- `organization_history[]` (`rolodex-contacts/src/schema/object.schema.js:121-128`): no id per entry, no organization name, no cap.
- Appended automatically only on update when the organization changed (`src/logic/populate-organization-history.js`, RB-127): old
  organization, job title, roles; `start_date` = newest entry's `end_date` (often empty), `end_date` = today. Skipped when the new
  organization equals the newest history entry — so returning to a former organization erases the stay just left (SPEC FND-ROL-055,
  asserted by a unit test). Sorted newest first on save.
- Staff can add/edit/delete past organizations in the console ("Add a Past Organization"); no portal UI (AFF-INV §2).
- Portal "Remove contact" clears `organization_id`, which writes a history entry (AFF-INV §6).

#### 5.3 Generic relationships (`rolodex-relationships`, July 2022)

- Record: `type_id` (req), `left_side_type`/`left_side_id`/`left_side_name`, `right_side_type`/`right_side_id`/`right_side_name`,
  `start_date`, `end_date` (ISO date-only) (`rolodex-relationships/src/schema/object.schema.js:17-28`). Sides are contacts or
  organizations, so contact–contact, contact–org and org–org relationships are possible, with direction fixed by the type's left/right.
- No reciprocal names, no primary flag, no roles or title. Names copied at save and never refreshed (SPEC FND-ROL-063).
- Save accepts an inactive type, duplicate relationships between the same two records, and an equal start and end date
  (SPEC FND-ROL-099).
- Used only by the optional Relationships tab on the contact/organization 360 page (current/past/future filters) and its setup
  screen; nothing in backend or portal reads it (AFF-INV §4).
- Permissions `rolodex:user:relationships:create|edit|delete`, `rolodex:admin:relationship-types:*` (console only, SPEC PERMISSIONS).

#### 5.4 Organization parent/child — see §2.3.

---

### 6. Contact roles and rule sets

#### 6.1 Roles (`rolodex-contact-roles`)

- Fields: `id` (:13), `name` (:14, required, not unique), `administrative_contact` (:15, "should be considered administrative
  contacts, and receive important information"), `assign_automatically` (:16), `assign_automatically_to_first_contact` (:17),
  `description` (:20), `is_active` (:23, **required**), `custom_field_values` (:25)
  (`rolodex-contact-roles/src/schema/object.schema.js`).
- A role is a contact's function **at its current organization** (Billing Contact, Voting Delegate). No holder limit on the role
  itself; limits live on the organization type (§3.2).
- `administrative_contact` grants every portal permission for the organization (SPEC FND-ROL-020) and is what lets a member see
  organization invoices (RB-1244, AFF-INV §6).
- Deletion blocked while a contact references the role (AFF-INV §1). Deletion validation hook commented out; roles referenced
  only in compliance rules, applicable lists or history are not protected (SPEC FND-ROL-043).

#### 6.2 Automatic assignment

- On contact create, or update/patch that changes `organization_id` (not bulk imports): add every active `assign_automatically`
  role, plus every active `assign_automatically_to_first_contact` role when nobody else is in the organization; existing roles kept
  (`rolodex-contacts/src/logic/contact-role-assignment.js`, RS-2488; `MIG/contact-role-assignment`).
- Old organization's roles are not removed by the server (the console filters by the new type) — roles carry across moves; first
  contact test races; names stored before automatic roles are added (SPEC FND-ROL-089).
- **Provisioning seeds two roles, both named "Billing Contact", both administrative and automatic**
  (`rolodex-provisioning/src/objects/contact-roles.js:21-32`; SPEC FND-ROL-020): every new member of an organization in a fresh
  tenant becomes a portal administrator of it.

#### 6.3 Role rule sets (`rolodex-contact-role-rule-sets`)

- A template service with no logic since 2019; nothing reads it except a generated client and MemberSuite ETL metadata; superseded
  by organization-type compliance (AFF-INV §3). Deployed and reachable; a forgotten rule set silently blocks role deletion; counts
  unchecked (SPEC FND-ROL-119). **Dead.**

#### 6.4 Portal role permissions

- Tenant setting `contact_role_permissions[{role_id, view_contacts, create_contacts, edit_contacts, remove_contacts,
update_organization_info, manage_child_organizations, create_child_organizations, edit_child_organizations, delete_child_organizations}]`
  (`rolodex-settings` (gh) `src/schema/object.schema.js:46-58`). `update_organization_info` is _described_ as a notification switch
  but the portal uses it to allow editing the organization profile (SPEC FND-ROL-125). Other modules add their own permissions
  (collect `view_financial_history`, `make_payments`; membership sign up/renew/update/manage privileges) (AFF-INV §6).
- Evaluated **only in the portal browser** (`contact-role-permission.service.ts`); services never check them (SPEC FND-ROL-009).
- Console page drops inactive roles' rows on save and shows a failed read as an empty grid (SPEC FND-ROL-050).

---

### 7. Regions (`rolodex-regions`)

- Region record: `id`, `name` (req), `description`, `is_active` (default true), `custom_field_values`
  (`rolodex-regions/src/schema/object.schema.js:14-19`).
- Settings (`rolodex-settings` (gh) `src/schema/object.schema.js:20-44`): `enable_automatic_region_assignment`;
  `automatic_region_assignment_options {contact_address_type home|preferred|work|school|other (default preferred),
contact_other_address_type_id (when other), automatically_assign contacts|organizations|both (default both),
rules[{type postal code|state|country|rest of the world, postal_code_ranges[{start,end}] | states[] | countries[] (1..1000), region_id}]}`.
- Assignment: on each write that changes the deciding address, unless `override_automatic_region_assignment` is set (contacts :74,
  orgs :29); stream → SNS → SQS → `src/logic/assign-region.js` / `calculate-region.js`; tenant-wide recalculation step function
  `POST /{tenantId}/recalculate`, permission `rolodex:admin:settings:recalculate-region-assignment` (`MIG/crm-regions`).
- Defects: postal codes compared as text (ZIP+4 and non-US codes misplaced; the ranges regex refuses a single code and spaces, so
  Canadian/UK codes cannot be entered); state rule ignores country; blank country counts as US; several rest-of-world rules, last
  wins (`calculate-region.js:57`, SPEC FND-ROL-060). Background writes replace the whole record from a stale copy (FND-ROL-042).
  Missing address or settings throws forever (FND-ROL-059). Feature flag `rolodex_regions` hides only the menu and switch card
  (FND-ROL-086). Recalculation is one 15-minute step, no retry (FND-ROL-087).

---

### 8. Contact and organization directories

(Thin — see Coverage gaps.)

- Staff define directories (`rolodex-contact-directories`, `rolodex-organization-directories`, schema from the shared
  `@rhythmsoftware/directories` package, `rolodex-contact-directories/src/schema/object.schema.js:16-17`): name, staff filter,
  searchable fields, entry layout template, messaging option, "public directory" checkbox (SPEC FND-ROL-022/023/092).
- Portal pages `contact-directory` and `organization-directory` (`rolodex-portal-features/src/features/contact-directory.js`,
  `organization-directory.js`).
- Without a staff filter the query is replaced with "status is active" (`src/logic/apply-base-query-filters.js`, FND-ROL-023).
- **Privacy defects**: `is_public` is read by no Rolodex code; opt-out tips promise exclusions no code applies (FND-ROL-023);
  default layout publishes e-mail and the whole address objects (FND-ROL-022); entry endpoint opens any record by id regardless of the
  directory, and org entries list every contact id (`src/logic/get-directory-entry.js`, FND-ROL-018); anonymous message route
  unbounded (FND-ROL-019); organization directory messaging always fails (`rolodex-organization-directories/src/lambda/api.js:78`,
  FND-ROL-092); default layouts name tenant-specific custom fields (`rolodex_contacts__degree__c` etc.); contact entry reads the
  membership `contact:<id>` unconditionally (FND-ROL-093).
- Organization directory supports visitor comments with ratings rolled into `directory_comment_rating_metrics`
  (`rolodex-organizations/src/schema/object.schema.js:81-83`; rescans the tenant per change, FND-ROL-094).

---

### 9. Custom fields

- Contacts, organizations, roles and regions carry `custom_field_values[]` from the platform custom-field schema
  (`rolodex-contacts/src/schema/object.schema.js:137`; `rolodex-organizations/...:78`; `rolodex-contact-roles/...:25`;
  `rolodex-regions/...:19`). Field definitions live in Platform (ids like `rolodex_contacts__degree__c`).
- Cleaning removes empty values, computes calculated fields, sorts table rows (C-26, C-32). Merge treats them as ordinary conflict
  rows (MERGE-A #21).
- Settings can add up to 50 extra contact fields to organization queries (`additional_contact_fields_to_show_in_organization_queries`,
  RB-3239, `rolodex-settings` (gh) `object.schema.js:98-103`) and event/course/committee fields (≤15 each) to contact queries (:147-178).

---

### 10. Portal-facing capabilities

#### 10.1 Pages (`rolodex-portal-features/src/features/*`, `rolodex-ngx-portal-controls` (gh) `src/lib/components/*`)

`manage-my-profile` (update-profile), `manage-my-organization` (update-organization-profile, manage-organization-contacts),
`contact-directory`, `organization-directory`.

#### 10.2 Rules and settings

- Settings (`rolodex-settings` (gh) `object.schema.js`): `restrict_contact_name_changes`, `restrict_organization_name_changes`,
  `restrict_contact_credentials_changes` (:80-82); `new_contact_redirect_route` (:61); `disable_organization_creation_during_sign_up`,
  `disable_organization_matching` (:84-85); default console/portal forms (:86-96, plus deprecated `contacts.default_*_forms` :106-131);
  child-organization management: `enable_child_organization_management_in_portal`, labels, allow create/edit/delete, forms (:62-79;
  the edit form is required when **deleting** is allowed — schema bug, SPEC FND-ROL-085).
- Access (server, shared gate `libraries-dynamodb/src/security/validate-rolodex-request.js`): a member acts for its own contact,
  contacts of the same organization, contacts that named it as unexpired delegate, and its own organization's ancestors and
  descendants. Roles are not checked server-side (AFF-INV §6; SPEC FND-ROL-009).
- **Hard-coded exemptions**: tenants matching `tests_`, `cypress_console_tests`, or **`faa.org`** skip the portal checks; the contacts
  rule also exempts contact creation in organization `greekcircle` (`rolodex-contacts/src/object/portal-access.js`
  `weAreInTestsAndNeedToAllowAccess`) (SPEC FND-ROL-002).
- A login with no contact skips the per-record check unless flag `portal_security_forbid_empty_contact_ids` is on
  (`libraries-dynamodb/src/security/authorize-portal-access.js`, FND-ROL-013).
- A member can set their own `organization_id` (gaining reach over that organization's contacts and possibly automatic admin roles),
  `status`, and `portal_delegates` (FND-ROL-010, FND-ROL-027).
- `date_profile_last_updated` is stamped on every portal save including an admin editing a colleague, so those people never see the
  new-user form (FND-ROL-096).

#### 10.3 Portal delegates

- `portal_delegates[{contact_id, expiration_date, status approved}]` on the delegating contact (`rolodex-contacts/src/schema/object.schema.js:160-166`).
  Honoured by every portal service through the shared gate. **Nothing in the product creates a delegation**; any caller who may save
  the contact can grant one; the delegate lookup (`src/lambda/list-by-portal-delegate-id.js`) returns expired ones while
  `check-manageability.js` does not (FND-ROL-027).
- `rolodex-account-delegation-processes` (gh) stores request/granted e-mail templates (`src/schema/object.schema.js:15-23`) but nothing
  reads it; `request-delegate-access.js` requires a misspelled package and `grant-delegate-access.js` has no caller (FND-ROL-118).

#### 10.4 Sign-up and login linking (`rolodex-contact-steps`)

- Step functions `create-and-link.step.json` (sign-up creates contact, optionally matches/creates organization per settings, links
  Knox login), `authorize-link-alternate-email.step.json` (claim an existing contact by a secondary e-mail) and `merge-contact.step.json`
  (`rolodex-contact-steps/build/stateMachines/`). Sign-up has no Retry/Catch on the Knox step (FND-ROL-065); alternate-e-mail link
  skips the active check and lets a member probe whether an address is on file (FND-ROL-028); first sign-in links the contact named
  by an unverified query token (FND-ROL-012, UNVERIFIED line).

---

### 11. Admin configuration (console)

(Thin — see Coverage gaps.) Setup screens cited in SPEC/PERMISSIONS: contact roles, contact role permissions, organization types
(with compliance and e-mail preview), address types, phone number types, communication types, relationship types, activity types,
regions + settings rules card + recalculate, contact/organization directories, default forms dialogs, default 360 screen, engagement
models/categories/metrics, query settings, maintenance tasks. Settings page reports failures as "Settings saved successfully"
(`settings.component.ts` `ShowError('Settings saved successfully', err)`, FND-ROL-085); five pages save the settings record whole,
each under a different permission (FND-ROL-046). The application is named "Rolodex (CRM)", "Rolodex (CRM) Setup & Administration" and
"CRM" in different places (FND-ROL-125).

---

### 12. Security (`rolodex-security`)

- Declares permission keys (`src/permissions/admin.js`, `contacts.js`, `organizations.js`, `general.js`, `queries.js`); many keys the
  console uses are not declared there (SPEC PERMISSIONS: e.g. `rolodex:user:contacts:create|edit|delete|view` cited, not declared).
- Key families: `rolodex:user:contacts:{access,view,create,edit,delete,change-status,login-as,reset-password,send-email,send-forgotten-password-email}`,
  `rolodex:user:organizations:{...,change-status,change-type}`, `rolodex:power-user:{contacts,organizations}:merge`,
  `rolodex:admin:{<type>}:{access,create,edit,delete}`, `rolodex:admin:settings:edit`, `rolodex:admin:settings:recalculate-region-assignment`,
  `rolodex:admin:organizations:reprocess-contact-role-compliance`, `rolodex:admin:contact-role-permissions:{edit,manage}`.
- **No Rolodex service checks a staff permission**; every `rolodex:` key is enforced only by console route guards/buttons
  (SPEC FND-ROL-007, "needs runtime verification" of the authorizer).

### 13. Provisioning (`rolodex-provisioning`)

- New tenant gets two contact roles (`ADMIN_CONTACT`, `BILLING_CONTACT`, both "Billing Contact", administrative, automatic —
  `src/objects/contact-roles.js:21-32`), four default forms, and the settings record, in that order (`src/logic/provision-tenant.js`).
  No types, regions or relationship types seeded. Partial failure leaves a tenant whose Create Contact page cannot open; never seeds
  `portal_new_user_profile_form_id`; replay behaviour disputed (SPEC FND-ROL-047, FND-ROL-066).

---

### 14. Special cases and workarounds (consolidated)

| #   | Behaviour                                                                                                                                        | Citation                                                                                                                    |
| --- | ------------------------------------------------------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------- |
| W1  | `faa.org`, `tests_*`, `cypress_console_tests` tenants bypass portal security; `greekcircle` org contact-creation exemption                       | `libraries-dynamodb/src/security/validate-rolodex-request.js`; `rolodex-contacts/src/object/portal-access.js` (FND-ROL-002) |
| W2  | Org note `skip flowdown processing` disables flowdown                                                                                            | `rolodex-organizations/src/lambda/flowdown.js:35-36`                                                                        |
| W3  | `__new:<name>` organization id creates an org inline before validation                                                                           | `rolodex-contacts/src/object/cleaning.js` `createNewOrganizationIfNecessary` (FND-ROL-054)                                  |
| W4  | Empty-contact portal logins allowed unless flag `portal_security_forbid_empty_contact_ids` ("temporarily ... until the Auth0 flow refactor")     | `libraries-dynamodb/src/security/authorize-portal-access.js` (FND-ROL-013)                                                  |
| W5  | Imports skip duplicate detection and automatic role assignment                                                                                   | MERGE-A §1; `MIG/contact-role-assignment`                                                                                   |
| W6  | Compliance rules with 0/blank both sides deleted                                                                                                 | `rolodex-organization-types/src/object/cleaning.js:30-37`                                                                   |
| W7  | Organization duplicate detection disabled since 2020 after a runaway loop                                                                        | MERGE-A §1                                                                                                                  |
| W8  | Settings cache 10,000 s (comment: 10 s)                                                                                                          | `rolodex-contacts/src/object/change-validation.js` `getSettings` (FND-ROL-082)                                              |
| W9  | Tenant-specific backfill scripts (e.g. `rb-4706-backfill-missing-napeo-owner-type.js`) and default directory layouts naming tenant custom fields | FND-ROL-099, FND-ROL-092                                                                                                    |
| W10 | Dead services still deployed: rule sets, login profiles, delegation processes                                                                    | FND-ROL-117/118/119                                                                                                         |

---

### 1.0 behaviours a 2.0 architecture must account for

1. A contact has at most one current organization plus a free-form history; no primary flag and no concurrent affiliations — `rolodex-contacts/src/schema/object.schema.js:20-28,121-128`.
2. Organization history is appended automatically on org change with today as end date and a usually-empty start — `rolodex-contacts/src/logic/populate-organization-history.js` (AFF-INV §2).
3. Converted MemberSuite data put still-current non-primary affiliations into history — `pso-clover-membersuite/graph/rolodex/contacts.grf` (AFF-INV §5).
4. Contact statuses are active/duplicate/deceased/inactive; organizations active/duplicate/inactive; "duplicate" links to nothing — `rolodex-contacts/.../object.schema.js:120`; `rolodex-organizations/.../object.schema.js:62`.
5. Status changes keep no history and overwrite notes — FND-ROL-053.
6. There is no contact type in 1.0 (2.0 adds one) — `MIG/contact-types`.
7. Names: seven parts plus maiden name and salutation; computed name/full name/sort name; 1.0 parses a full name into parts — `MIG/contact-names-derived-full-name`; C-16.
8. Fixed address slots (home/work/school/seasonal with date window; org main/billing) plus typed "other" lists — `rolodex-contacts/.../object.schema.js:77-101`; `rolodex-organizations/.../object.schema.js:25-37`.
9. Three fixed e-mail slots, no format validation, primary-only duplicate prevention behind a tenant setting — `rolodex-settings/src/schema/object.schema.js:18`; `MIG/contact-duplicate-email-prevention`.
10. Preferred address/phone chosen by type and copied into a computed field — `rolodex-contacts/.../object.schema.js:57-60,99-101`.
11. Address/phone types carry applies_to, display order, portal/console visibility and portal-required flags; phone types a flowdownable flag — `rolodex-phone-number-types/src/schema/object.schema.js:14-22`.
12. Flowdown: per-contact link flags copy org main address/phone to the contact's work slots on save and on org change — `rolodex-organizations/src/logic/flowdown/perform-flowdown.js`; `copy-linked-org-info.js`.
13. Flowdown is first-page-only, unretried, and can blank linked values — FND-ROL-051.
14. Organization rename rewrites denormalised `organization_name` on contacts — `organization-change-logic.js` (FND-ROL-056).
15. Inline organization creation from the contact form (`__new:`), before validation — FND-ROL-054.
16. Org hierarchy is a single parent with stored ancestors; no loop check, no descendant recompute — `rolodex-organizations/src/logic/ancestors.js:34-48` (FND-ROL-058, FND-ROL-011).
17. Website↔domain derivation and alternate domains used for org matching; case-sensitive, path-retaining — FND-ROL-057.
18. Organization types carry applicable roles, key roles, five per-type forms and compliance — `rolodex-organization-types/src/schema/object.schema.js:15-58`.
19. Applicable roles enforced only in the console — AFF-INV §3.
20. Compliance: min/max per role, counts every contact of any status, status + violations stored, e-mail on transition to non-compliant — `rolodex-organizations/src/logic/contact-role-compliance.js` (MIG/contact-role-compliance).
21. Compliance not rechecked on type or rule change; reprocess is a manual admin action — `rolodex-organizations/src/object/cleaning.js:19-22` (FND-ROL-052).
22. Compliance counts only the first page of contacts — FND-ROL-044.
23. Compliance e-mail preview sends real data to any address without permission — FND-ROL-025.
24. Contact roles: administrative flag, automatic and first-contact automatic assignment, active flag, custom fields — `rolodex-contact-roles/src/schema/object.schema.js:13-25`.
25. Automatic roles are added on create/org change and never removed; races on "first contact" — `rolodex-contacts/src/logic/contact-role-assignment.js` (FND-ROL-089).
26. Provisioning seeds two identical administrative, automatic "Billing Contact" roles — `rolodex-provisioning/src/objects/contact-roles.js:21-32`.
27. Contact role rule sets are dead code that still block role deletion — FND-ROL-119.
28. Portal permissions per role (view/create/edit/remove contacts, org info, child org CRUD) live in tenant settings and are enforced only in the browser — `rolodex-settings/src/schema/object.schema.js:46-58` (FND-ROL-009).
29. Administrative roles imply all portal permissions and organization invoice visibility — FND-ROL-020; AFF-INV §6 (RB-1244).
30. Portal reach = own contact, same organization, ancestors/descendants, unexpired delegates — `libraries-dynamodb/src/security/validate-rolodex-request.js` (AFF-INV §6).
31. Hard-coded tenant exemptions (`faa.org`, `tests_`, `greekcircle`) in portal security — FND-ROL-002.
32. Members can change their own organization, status and delegates — FND-ROL-010, FND-ROL-027.
33. Portal delegates exist in data and security but have no creation flow; delegation processes are unused — `rolodex-contacts/.../object.schema.js:160-166`; FND-ROL-118.
34. Portal name/credential/org-name change restrictions and new-contact redirect are tenant settings — `rolodex-settings/src/schema/object.schema.js:61,80-82`.
35. Sign-up can create or match an organization, controlled by two settings — `rolodex-settings/src/schema/object.schema.js:84-85`; `rolodex-contact-steps/build/stateMachines/create-and-link.step.json`.
36. Child-organization management in the portal with tenant labels and create/edit/delete toggles — `rolodex-settings/src/schema/object.schema.js:62-79`.
37. Generic relationships: typed, directional left/right between contacts/orgs, dated, no reciprocal labels, cascade delete — `rolodex-relationships/src/schema/object.schema.js:17-28`; `config.js:77-84`.
38. Relationship type side kinds can change while in use — FND-ROL-062.
39. Regions: rules by postal range/state/country/rest-of-world with per-record override and tenant recalculation — `rolodex-settings/src/schema/object.schema.js:20-44`; `MIG/crm-regions`.
40. Region postal matching is textual and US-centric — `rolodex-regions/src/logic/calculate-region.js:57` (FND-ROL-060).
41. Directories: staff-defined filtered lists with layout templates, search fields and messaging; is_public and opt-outs unenforced — FND-ROL-022/023.
42. Directory entry endpoint exposes any record by number — FND-ROL-018.
43. Organization directory comments with ratings aggregated onto the organization — `rolodex-organizations/src/schema/object.schema.js:81-83`.
44. Custom fields on contacts, organizations, roles and regions; extra contact/event/course/committee fields in queries via settings — `rolodex-settings/src/schema/object.schema.js:98-178`.
45. Communication preferences per communication type with subscribed/digest/unsubscribed and a separate subscription e-mail — `rolodex-contacts/.../object.schema.js:149-158`.
46. Do-not-mail/call and global e-mail opt-out flags — `rolodex-contacts/.../object.schema.js:146-148`.
47. Legislative district fields on contacts — `rolodex-contacts/.../object.schema.js:86-90`.
48. Spouse is a one-way self-excluding pointer — `rolodex-contacts/.../object.schema.js:51`.
49. Deletion is hard and blocked only by declared references; contacts with logins can be deleted — FND-ROL-071, FND-ROL-043.
50. Merge: N-to-1, non-atomic, divergent preview, ids not aliased, identity unioned — MERGE-A §2.
51. Duplicate detection by e-mail/sound-alike name/postal with permanent dismissals; org detection off — MERGE-A §1.
52. Staff permissions are console-only; services enforce none — FND-ROL-007.
53. Change events are forwarded to customer AWS accounts including security context — MERGE-A #33; `rolodex-eventbridge` (UNVERIFIED detail).
54. Settings record is a single whole-replaced document edited from five pages — FND-ROL-046.
55. `date_profile_last_updated` drives portal first-time flows and is stamped wrongly — FND-ROL-096.

---

### Addendum (merged by the architecture author, 2026-10-19 evening): the six helper reports

The six helper reports listed as missing above were delivered to the architecture author. Their findings are summarised
here with their citations (paths relative to `~/rhythm/`). Where they disagree with the body above, the helper reports
read the cited code more deeply and take precedence.

#### A. Contacts (`rolodex-contacts`)

- Hooks per save: clean → number → Joi → validate → custom fields → transformations (`libraries-dynamodb/src/crud/object-validation-logic.js:38-188`). Deletion validation is commented out (`rolodex-contacts/config.js:187`).
- Computed `name`, `full_name`, `name_last_first` overwrite client input (`config.js:42-44`; `src/logic/calculate-full-name.js:9-93`). Full-name parsing only when neither first nor last is present (RB-4385, `cleaning.js:252-254`); salutation→prefix (RB-2367, `cleaning.js:91-100`); unanchored prefix regex (`:102-106`).
- **No required fields** on the contact; e-mails are not format-checked (`tests/unit/object/cleaning.spec.js:212-302`).
- Status enum `active|duplicate|deceased|inactive` (`object.schema.js:120`); non-active contacts cannot be linked to a portal user (`src/logic/contact-user-logic.js:113-118`); `merged_ids` is never populated.
- Duplicate e-mail prevention by tenant setting `duplicate_prevention` (`change-validation.js:104-123`), primary slot only, any status blocks.
- Preferred phone: school case falls through to alternate; work mobile unhandled (`calculate-preferences.js:68-132`). Preferred address order work→home→school→others; seasonal never chosen.
- Linked work address/phone copied from the org on every contact save (`copy-linked-org-info.js:12-34`); org-side flowdown on MODIFY of `address`/`main_phone_number` only (`rolodex-organizations/src/lambda/flowdown.js:25-56`).
- Org rename pushes `organization_name` to contacts by a raw DynamoDB `SET`, bypassing cleaning and leaving search keys stale (`src/logic/organization-change-logic.js:16-75`).
- Inline organization creation via `__new:<Name>` (`cleaning.js:3,37-65`).
- Portal: same-org or delegate or ancestor-org access; **no visibleProperties**, so portal users can edit every field including status (`config.js:227-235`); delegate request/grant modules are broken (typo import, missing imports; `request-delegate-access.js:5-8`, `grant-delegate-access.js:6-9,32`).
- Tenant bypasses `cypress_console_tests`, `tests_*`, `faa.org`; organization `greekcircle` always allowed on portal create (`portal-access.js:13-15,54-59`; `libraries-dynamodb/src/security/validate-rolodex-request.js:43-45`).

#### B. Organizations (`rolodex-organizations`)

- Fields `object.schema.js:11-92`; **no e-mail fields**. Phone formatting top-level only; website↔domain derivation (RS-2569/RS-2863, `cleaning.js:29-50`).
- Organization type controls applicable roles, key roles, per-type forms, compliance (`rolodex-ngx-api/.../organizationType.ts:21-60`).
- Compliance (`src/logic/contact-role-compliance.js:31-177`): direct contacts only; `minimum: 0` never checked; one violation per rule ("too few" wins); e-mail only on the transition into non-compliance, to holders of chosen roles or all contacts; type cached 60 s; rules or type changes need a manual reprocess (`reprocess-contact-role-compliance.js:14-60`); preview endpoint with override address.
- Hierarchy: ancestors recomputed on save; depth guard ineffective (`ancestors.js:48`, `||` bug); no cascade when a parent moves; descendants recursive with no depth limit (`descendents.js:17-59`).
- Domain table maintained on change events, last write wins; lookup returns `undefined` not 404 (`organization-change-logic.js:12-138`; `api.js:81`).
- No automatic organization duplicate detection; manual merge via step function `rolodex-organization-merge`.
- Delete: generic library hard delete with dependency objections (`libraries-dynamodb/src/crud/object-deletion-logic.js:55-191`).
- Portal: any portal user can create an organization (RB-3198); any affiliated contact passes update/delete; access inherited up and down the hierarchy (RB-5827); the "public" read returns the whole record (TODO, `api.js:38-54`).

#### C. Relationships, roles, rule sets, regions

- Relationships (`rolodex-relationships/src/schema/object.schema.js:15-30`): type, left/right side kind and id, dates, notes; no primary, no reciprocal label (`rolodex-relationship-types` has `left_side_type`, `right_side_type`, `is_active` only); no uniqueness or overlap rule; all fields mutable; cascade-deleted with either side (`config.js:70-86`).
- Contact roles (`rolodex-contact-roles/src/schema/object.schema.js:13-25`): administrative, assign automatically, assign to first contact; assignment on create or organization change, not for bulk imports (`rolodex-contacts/src/object/change-validation.js:48-56`); roles are additive and not removed on organization change.
- Role rule sets: schema only; **no evaluator anywhere** (`rolodex-contact-role-rule-sets`).
- Regions: name only; rules in settings (`calculate-region.js:11-83`): postal ranges compared as strings, states exact, country case-insensitive with blank = US, catch-all; per-record override; SQS-driven reassignment; bulk `POST /recalculate` step function.

#### D. Directories, contact steps, portal features, security, provisioning, events

- Directory configuration (`libraries-directories/src/schema/directory.schema.js`): public flag, display mode, searchable and additional fields, custom templates, comments and ratings, entry mode, radius search, eligibility, messaging, query filter and expression, custom sort, page size, refresh period.
- **Security defects:** search's security check is commented out (`libraries-directories/src/logic/search.js:29-32`); messaging checks nothing (`send-message.js:17-40`); entry endpoint does not verify the entry belongs to the directory; unset `is_public` counts as public; RB-7556 allowlist added after ZD 20897 leaked gender, date of birth and home address.
- Contact steps = three step functions: create-and-link sign-up (domain match excluding public domains, settings `disable_organization_matching`, `disable_organization_creation_during_sign_up`), authorize alternate e-mail, contact merge (`rolodex-contact-steps/build/template-logic.yaml`).
- Security (`rolodex-security`): `rolodex:<level>:<resource>:<action>`, generated CRUD + query permissions, resource scoping of contact permissions by organization type and by membership chapter/section; managed policies read-only, user, power-user (merge), admin.
- Provisioning: two default roles (`ADMIN_CONTACT` mislabelled "Billing Contact"), none for MemberSuite tenants; locked console/portal forms; settings for form ids.
- Events: bus `rhythm-rolodex`; contact-merge event only.

#### E. Portal pages (`rolodex-ngx-portal-controls`, `platform-ngx-portal-controls`)

- Pages: Account Home, Manage My Profile, Update My Organization's Profile, Manage My Organization's Contacts, Search People, Search Organizations (`rolodex-portal-features/src/features/*`).
- Update My Profile: first-time detection, configured new-user/update forms, name and credential locks after the first update (UI-only `setTimeout` disable), `date_profile_last_updated` stamping, redirect rules (RB-3708, RB-3607).
- Manage contacts: list with membership status per contact (batched membership reads), filter and sort, add/edit through configured forms, **remove = detach from organization** (not delete); child organizations (view/create/edit/hard delete) gated by role permission AND tenant toggles.
- Permission evaluation (`contact-role-permission.service.ts:25-53`): an entry with no role grants everyone; additive OR; administrative roles get everything; enforced only in the browser.

#### F. Staff console (`rolodex-ngx-console`, `-console-controls`, `-controls`)

- Areas: Overview, Contacts, Organizations, Queries, Setup (Reports and Dashboards behind flags).
- Contact search filters and grid; create/edit via configured web forms (error if none); Contact 360 with tabs Contact Details, Organization Details (editable organization history), Emails, Activities, Relationships, Google Civic, Security (portal login info, reset password, verify e-mail), Custom Fields.
- Contact options: edit, send message, merge duplicates, delete (hard), change status (with deceased date); "Login to Portal" impersonation passes staff Auth0 tokens in the URL hash (`impersonate-portal-login-button.component.ts:66-87`).
- Merge wizard: review duplicates (confidence bands, ignore/undo), resolve field conflicts, server preview, merge, poll. Organization merge: source/master, swal confirm, step function.
- Organization 360: compliance banner, key contacts by role, Details, Organization Contacts (read-only grid with violations), Child Organizations, Activities, Relationships, Custom Fields; change type, change status, delete, merge.
- Relationships tab: Current/Past/Future filters, create/edit/delete with dates and notes.
- Setup: contact roles; role-permission matrix (portal capabilities by role, with an "All Contacts" row); organization types (restrict roles, key roles, compliance min/max table, violation e-mail with merge fields and preview, per-type forms); address and phone types (applies-to, reorder); communication types; activity types; task queues and types; relationship types; regions; directories; web forms and defaults (name/credential locks); custom fields; 360 screen designer; engagement (categories, models with weighted components and point rules, custom metrics); maintenance (recalculate regions, reprocess compliance, engagement lookback).
- No contact types; no bulk edit, bulk delete or export in CRM screens; no organization duplicate detection.

## 21. E02. Rhythm 1.0 Rolodex: activities, engagement, tasks, duplicates and merging, search, data operations

## E02. Rhythm 1.0 Rolodex: activities, engagement, tasks, duplicates and merging, search, data operations

Evidence for the Rhythm 2.0 CRM architecture. It records what the 1.0 product lets people do in five areas, with the rules
and edge cases a 2.0 design has to account for, and where in the 1.0 code each fact was seen.

How to read the citations:

- `repo/path:line` is a file under `~/rhythm/<repo>` read on 2026-10-19.
- `(GitHub)` marks a file not cloned locally and read with `gh api repos/rhythmsoftware/<repo>/contents/<path>`.
- `spec:` points at the 1.0 spec retrofit under `~/rhythm/docs/specs/apps/<app>/` (reconstructed from the code in
  September 2026). It is cited for context; where a claim matters it was re-checked in code.
- `2.0:` points at existing Rhythm 2.0 summaries under `~/rhythm2/services-crm/docs/`, reused where they agree with the code.
- **UNVERIFIED** marks a claim not confirmed in code or runtime during this pass.

Two repository names in the brief are not what they look like:

- `~/rhythm/task-master` is the Rhythm 2.0 Work Coordinator tooling ("Work Coordinator 2.0 (formerly Task Master)",
  `task-master/README.md:1`), not a 1.0 product service. It has no bearing on CRM tasks.
- `~/rhythm/libraries-rolodex-processes` is not a merge library. It is the shared schema for portal "processes": whether to
  show a contact or organization verification form first, checkout-settings override, confirmation email and eligibility
  criteria (`libraries-rolodex-processes/src/rolodex-process.schema.js:9-75`). Merging lives in `libraries-merging`,
  `rolodex-contact-steps` and `rolodex-contacts`.
- `~/rhythm/rolodex` is an undeployed 2026 PostgreSQL precursor ("Nothing is deployed", `rolodex/README.md:3-8`), not 1.0
  behaviour. It is ignored here.

---

### 1. Activities and engagement

#### 1.1 Capabilities

**Custom activities (the CRM timeline entries).** Staff record a dated activity against one contact or one organization.

- Fields: `custom_activity_type_id` (required), `date` (ISO, required), `owner_type` (free text, default `contact`),
  `contact_id` or `organization_id`, `summary`, `description` (plain text), custom field values
  (`rolodex-custom-activities/src/schema/object.schema.js:14-25`).
- Types (`rolodex-custom-activity-types`) are tenant-defined: `name`, `icon_class`, `icon_color`, `is_active`, custom fields
  (`rolodex-custom-activity-types/src/schema/object.schema.js:13-21` (GitHub)). No system types exist, so nothing in 1.0
  knows which type means "call" or "meeting".
- Lists: by contact and by organization, newest first, a page at a time (`rolodex-custom-activities/src/lambda/list-by-contact-id.js`,
  `list-by-organization-id.js`; spec: `rolodex/planning/activities-and-relationships/requirements/REQ-ROL-103-*.md`).
- Permissions checked by the Console only: `rolodex:user:custom-activities:create|edit|delete`
  (`rolodex-ngx-console-controls/projects/rolodex-ngx-console-controls/src/lib/360/oneClicks/rendering/activities-one-click/activities-one-click.component.ts:134-136`).
- No participants, no attendees, no attachments, no links to more than one record: an activity about three people is logged
  three times (2.0: `docs/migrations/activities/README.md` §1, agreed by the schema above). Note: that 2.0 summary names the
  1.0 fields `subject`, `notes`, `owner`; the real names are `summary`, `description`, `owner_type` + `contact_id`/`organization_id`.

**The activity timeline (360 page "Activities" tab).** Assembled in the browser, not on the server:

- It loads the record's custom activities and, for a contact only, System's email-marketing activity
  (`getEmailActivityByContactId`) and concatenates them (`activities-one-click.component.ts:95-128`).
- Email-marketing entries are passed through unchanged when their type is `email integration campaign monitor`,
  `... mailchimp` or `... active campaign` (`:114-116`). These come from the integration services (for example
  `partners-mailchimp/src/logic/activity-logic.js:17-60`, which pulls campaign email activity and unsubscribes into a raw table
  and resolves recipients to contacts by email address). Organizations get no email activity.
- The timeline control (`platform-ngx-console-controls/projects/controls/src/lib/components/timeline/timeline.component.ts:246-300`)
  carries a filter list of cross-application event kinds (record created/modified/deleted, record merge, profile update,
  membership join/renewal/reinstatement/termination, event registration...), but the Rolodex tab feeds it only custom
  activities and email activity. **UNVERIFIED** whether any screen feeds it the other kinds; the record-activity control's
  default data is hard-coded sample text (`record-activity-timeline/record-activity-timeline.component.ts:36-70`).
- Activities from other applications (certification CE activities, accreditation activities, sales activities, event
  registrations, memberships) do **not** land on the Rolodex timeline. Each application shows its own records in its own 360
  panel ("one-click"); the Rolodex built-in panels are activity timeline, contact details, organization info, organization
  contacts, child organizations, email history
  (`rolodex-ngx-console-controls/.../360/oneClicks/rolodex-built-in-one-clicks.ts:1-26`).

**Sales activities (a separate Sales app).** `sales-activities` records `name`, `type_id`, `date` (default now), and optional
`lead_id`, `opportunity_id`, `organization_id`, `contact_id`, `description`, plus `create_follow_up_task` and a
`follow_up_task` with a relative date, subject and description (`sales-activities/src/schema/object.schema.js:15-31`).
Types are `name`, `display_order`, `is_active` (`sales-activity-types/src/schema/object.schema.js:13-18`). No server code
reads `follow_up_task` (grep of `sales-activities/src` and `sales-ngx-console/src` finds only the schema), so the follow-up
task is stored and never created. Sales activities are not on the Rolodex timeline. Last commit 2024-12-18 ("Removing
Rockset"); **UNVERIFIED** whether any tenant uses the Sales app.

**Change notifications.** Every custom-activity change is republished to an "activity changed" SNS topic with a normalised
shape (`type: 'custom'`, date, description, owner ids, type id, security context)
(`rolodex-custom-activities/src/logic/activity-sync.js:21-91`, topic from `config.js:5`). No subscriber was found in 1.0
(spec: `rolodex/inventory/SERVICES-1.md:738-740`; local grep for the topic name finds only the publisher).

**Outlook add-in.** Reads a contact's or organization's activities with type names for display in Outlook
(`partners-outlook-addin/src/lambda/api.js:90-127`).

#### 1.2 Engagement scoring

What it does: once a month per contact and per organization, it turns what the record did across six applications into one
score out of 100, a percentile rank within the tenant, an optional engagement category, and year-over-year change, using a
formula each tenant configures. Scores are shown on the 360 pages with a component breakdown and the raw measurements
(spec: `rolodex/design/engagement-scoring/DESIGN-ROL-007-*.md` §1, §6).

Configuration:

- **Switch and chosen models** live on Rolodex settings: `enable_engagement_scoring`, `contact_engagement_scoring_model_id`,
  `organization_engagement_scoring_model_id` (`rolodex-engagement-scores/src/logic/retrieve-engagement-models.js:24-29`).
- **Engagement categories** (`rolodex-engagement-categories`): named bands such as "Highly engaged".
- **Custom metrics** (`rolodex-custom-engagement-metrics`): kind `contact fields`, `membership fields` (an expression over
  record fields) or `rockset view` (a leftover of a Rockset predecessor)
  (`rolodex-custom-engagement-metrics/src/schema/object.schema.js:17-20`).
- **Models** (`rolodex-engagement-models`): `type` contact or organization; `components[]` each with `name`, `weight` 1-100,
  `metric_type` system or custom, a system metric id or custom metric id, and `metric_rules[]` (`starting_range`,
  `ending_range`, `points` 0-100); optional category rules by score and percentile band
  (`rolodex-engagement-models/src/schema/object.schema.js:21-60`).

Rules:

- Component weights must total exactly 100 (`rolodex-engagement-models/src/object/validation.js:36`).
- A component's point ranges must be contiguous: each rule's end must equal the next rule's start, and end must exceed start
  (`validation.js:42-54`). Start is inclusive, end exclusive; a missing end means open-ended
  (`rolodex-engagement-scores/src/logic/scores/calculate-component-score.js:16-18`).
- Component score = points of the matching rule; weighted score = round(points x weight / 100); total = sum of weighted
  scores (`calculate-component-score.js:21-26`).
- Percentile rank is computed among all of the tenant's scores of that kind for the month
  (`rolodex-engagement-scores/src/logic/scores/apply-statistics.js:57`).
- The category is the first category rule whose score and percentile bounds all contain the score
  (`calculate-engagement-category.js:12-31`).
- Year-over-year fields compare with the score exactly twelve months earlier (`apply-statistics.js:96-111`).
- System metrics are SQL modules over the shared reporting database, one per source: activities count per activity type,
  certification activities and credits, child memberships, committees, course registrations, event abstracts, event
  registrations by event type, event speakers, fundraising gifts, membership, merchandise purchases
  (`rolodex-engagement-scores/src/logic/metrics/generators/`). The activity metric counts every activity dated on or before
  the scoring date, so it is a lifetime count, not a monthly one (`generators/activities.js:33-40`).
- Nightly at 01:00 UTC (`rolodex-engagement-scores/build/template-logic.yaml:166-169`): one query per metric across every
  enabled tenant writes raw files per customer, metric and month to S3, then a per-tenant run scores the month.
- An administrator can start a 60-month **lookback** and a **regeneration** that deletes every score and rebuilds from up to
  241 months of raw files (`rolodex-engagement-scores/src/lambda/api.js:44,89`;
  `src/logic/scores/regenerate-scores.js:14,38-42`).

Edge cases and defects:

- Component-level `system_metric` / `custom_metric_id` are conditionally required on a sibling `type` that the component does
  not have (the model's `type` is contact/organization), so neither is ever required
  (`rolodex-engagement-models/src/schema/object.schema.js:28-35`). A component with no metric scores 0.
- A missing metric value scores 0 points silently (`calculate-component-score.js:15-18`).
- Scores are written by a direct DynamoDB batch write with a `ConditionExpression`, bypassing validation, versioning and
  change events (`src/logic/scores/batch-write.js:14-35`). BatchWriteItem does not honour conditions on a `PutRequest`;
  **UNVERIFIED** at runtime whether the condition is ignored or the call fails.
- Component percentile ranks, first-engagement date and previous-year fields rest on properties the in-memory objects never
  carry, so they are degenerate; the code comment says so and leaves the fix to product (`apply-statistics.js:13-30`).
- A large tenant (about 4,000 contacts) had no contact scores from 2024-05 because scoring was O(N^2) and timed out; fixed
  without changing outputs (`apply-statistics.js:13-30`).
- Measurement uses one database user exempt from the tenant check, with tenant ids written into SQL text
  (`src/logic/common.js:2`; spec DESIGN-ROL-007 §12). One failing metric query stops every tenant's night (spec §11).
- Regeneration and lookback check no permission (spec: `AUTH-ROL-007`, DESIGN-ROL-007 §12). **UNVERIFIED** in code.

#### 1.3 Activities and merge, delete

- An activity declares references to its type, contact and organization with delete action `none` (block): a contact,
  organization or type with activities cannot be deleted until the activities are removed
  (`rolodex-custom-activities/config.js:41-74`). A bulk hard delete skips this (spec REQ-ROL-106).
- Because those references are registered, a contact merge repoints activities to the survivor through the registry (§3.2).
- The timeline throws in the browser when an activity's type is missing (`ct.icon_class` on an undefined type,
  `activities-one-click.component.ts:118-120`); imports and hard deletes can create such activities.

---

### 2. Tasks

#### 2.1 Capabilities

Tasks live in four Platform services, not in Rolodex: `platform-tasks`, `platform-task-types`, `platform-task-queues`,
`platform-task-assignment-roles` (2.0: `docs/migrations/task-management/README.md` §1, which agrees with the code below).

- Fields: `name` (required), `description`, `due_date` (date only), `type_id` (required), `priority` low/normal/high/urgent
  (default normal), `status` open / waiting for someone else / deferred / completed, `assignee_type` user or task queue
  (default user), `assignee_id` (required for user), `queue_id` (required for queue), `queue_priority`, `owner_id`,
  `associated_records[]` (`record_service` + `record_id`), `task_group_id`, `application` (default platform), `order_id`,
  `order_line_item_id`, `remind_assignee` + `reminder_date`, `do_not_email_assignee`, custom fields
  (`platform-tasks/src/schema/object.schema.js:14-49`).
- Relation to contacts and organizations: generic `associated_records`; on save the first `rolodex-contacts` and first
  `rolodex-organizations` entries are copied into `associated_contact_id` / `associated_organization_id` for indexed lists
  (`platform-tasks/src/object/cleaning.js:18-19`). Lists exist by assignee, owner, queue, associated contact, associated
  organization, order, task group and associated record (`platform-tasks/src/lambda/list-by-*.js`).
- Queue order: `queue_priority` defaults to the Unix time of first save (`cleaning.js:15-16`), so a queue is first in, first out.
- Queues, types and assignment roles are `id`, `name`, `is_active` configuration records.
- Taking a task from a queue is a Console PATCH to `assignee_type=user, assignee_id=me` keeping `queue_id`; no lock, so two
  people can both take it (2.0 task-management README §1). **UNVERIFIED** in code this pass.
- Views (Console): My Open Tasks, My Open Tasks Assigned to Others, All Open Tasks, My Tasks, one per active queue, saved views
  (2.0 task-management README §1).
- Reporting: query type `rolodex:tasks` over the task view joined to contact and organization
  (`rolodex-query-definitions/src/definitions/tasks.js:6-30`). Its primary table is spelled `v_platorm_tasks` (`:12`);
  **UNVERIFIED** whether the reporting view really has that name.

#### 2.2 Rules

- Any status may move to any status; there is no completion stamp and reopening leaves no trace (2.0 README §1; schema has no
  transition rule, `object.schema.js:44`; `src/object/validation.js` is empty).
- **Assignment email**: sent through SES template "New Task: {{task.name}}" (`platform-tasks/build/template-resources.yaml:293-299`)
  when a change event shows the assignee user changed, unless the assignee made the change, the task opts out, or the task is
  completed (`platform-tasks/src/logic/task-notification.js:41-100`). Imported tasks never notify (subscription filter
  `anything-but: IMPORT`, `build/template-resources.yaml:279-291`). Queue assignment sends nothing.
- **Reminders** (`remind_assignee`, `reminder_date`) are stored and never sent (no sender in `src/`; 2.0 README cites
  OD-PLT-078).
- **Order-driven task generation**: a product with `enable_task_generation` carries `tasks_to_generate[]` templates
  (`libraries-tasks/src/task-generation.schema.js:5-22`). On order fulfilment a FIFO queue triggers
  `platform-tasks` to create one task per template per line item, with Handlebars name/description over the order and ship-to
  record, due date none / specific / relative to the order date, and the ship-to contact or organization as the associated
  record (`platform-tasks/src/logic/fulfillment.js:25-160`). Failures per task are logged and swallowed (`:68-70`).
- The template schema offers assignee type `assignment role`, but generation throws "Unsupported assignee type" for it
  (`libraries-tasks/src/task-generation.schema.js:16-17`; `fulfillment.js:126-136`). Assignment roles have no consumer.
- References: a task blocks deletion of its type and queue, and is cascade-deleted with its order
  (`platform-tasks/config.js:86-102`). It declares **no** reference to contacts or organizations.
- Permissions: only the setup screens are guarded (`platform:admin:task-types:*`, `platform:admin:task-queues:*`); any tenant
  user can read and change any task (2.0 README §1). No tenant gets default types or queues (2.0 README §1).

#### 2.3 Edge cases

- Because tasks declare no contact or organization reference, deleting a contact leaves its tasks pointing at nothing, and a
  contact merge does not repoint them (the merge planner reads only registered references, §3.2). This is inferred from the
  dependency list; **UNVERIFIED** at runtime.
- Event tasks (`events-tasks`) are a different concept: a named checklist item on an event (`event_id`, `name`), with their own
  types (`events-tasks/src/schema/object.schema.js:13-15`; `events-task-types`). They are not CRM tasks.

---

### 3. Duplicate detection and merging

The 2.0 assessment `services-crm/docs/merging/legacy-assessment.md` is thorough and agrees with the code where re-checked
below. This section keeps the facts the architecture needs and the re-checks.

#### 3.1 Duplicate detection

- Detection runs on contact create, update and delete change events; **imports are excluded** by the subscription filter
  `anything-but: IMPORT`, so imported duplicates are found only by a rebuild
  (`rolodex-contact-duplicates/build/template-resources.yaml:271-274` (GitHub)).
- Match keys: lowercased emails 1-3, Double Metaphone first and last names with a nickname table, postal codes; scoring starts
  at 50 for an email hit plus 24 per email slot, threshold 35, overridable by environment
  (`rolodex-contact-duplicates/src/logic/search-logic.js:5,32,79-82` (GitHub)). Spouses sharing an email score high
  (legacy-assessment §1).
- Staff can dismiss a pair (permanent, no who/when), restore it, or mark a manual duplicate (legacy-assessment §1).
- The tenant duplicate list returns the top 100 by highest confidence, no paging
  (`rolodex-contact-duplicates/src/lambda/api.js:66-80` (GitHub)).
- A public portal duplicate search (`rolodex_contact_duplicates_portal_search`, default minimum confidence 50) returns masked
  matches without sign-in (`rolodex-contact-duplicates/src/lambda/api.js:64`, `build/template-logic.yaml:66-67` (GitHub);
  spec FINDINGS FND-ROL-017).
- The contact page banner shows when any candidate exists: the Console threshold is 0
  (`rolodex-ngx-console-controls/.../contacts/merge/duplicate-contacts-alert/duplicate-contacts-alert.component.ts:113,151`).
- Organization duplicate detection has been switched off since 2020 (legacy-assessment §1).
- **Duplicate status** is a manual record status, not a detection outcome: contacts allow `active`, `duplicate`, `deceased`,
  `inactive`; organizations `active`, `duplicate`, `inactive` (`rolodex-contacts/src/schema/object.schema.js:120`;
  `rolodex-organizations/src/schema/object.schema.js:62`). No code in the Rolodex services, merge library or Console sets it
  (local grep).

#### 3.2 Merging

- Permission `rolodex:power-user:contacts:merge` (and `...:organizations:merge`) guards the duplicates list and merge pages in
  the Console only (`rolodex-ngx-console/src/app/contacts/contacts.routes.ts:33-34`;
  `rolodex-ngx-console/src/app/organizations/organizations.routes.ts:23`; `rolodex-security/src/permissions/contacts.js:64`).
- Wizard (contacts): review duplicates, resolve conflicts one radio per field, preview, success; survivor is the record you
  started from; organizations get a two-record page with no preview (legacy-assessment §4).
- Conflicts come from `GET .../conflicts` over two or more ids (`rolodex-contacts/src/lambda/api.js:222-252`); the preview from
  `generateMergedObject` (`:254-265`) uses a different algorithm from the real merge (legacy-assessment §2).
- Execution: `PUT /{tenantId}/{survivorId}/merge` loads every record and starts a step function with the full records in its
  input; the execution name has one-second, 12-hour-clock resolution (`hh`) and can collide
  (`rolodex-contacts/src/lambda/api.js:192-220`, line 216).
- The step function: initialise, validate, plan, write payloads (each with a `Catch` to a graceful-failure step), then a `Map`
  of per-service writes with `MaxConcurrency` 10 and no `Catch`, then a `rolodex-contacts-merged` event
  (`rolodex-contact-steps/build/stateMachines/merge-contact.step.json:6-117`). Failures in the first four steps end as handled
  "success"; no compensation (legacy-assessment §2).
- **Finding references**: the planner asks the locking library for every registered reference to the merged-away record
  (`libraries-merging/src/logic/get-downstream-relationships.js:11-15`). That call is a single DynamoDB `query` with no
  pagination, so only the first page (1 MB) of references is returned
  (`libraries-locking/src/services/relationship-service.js:145-161` (GitHub)). A record with very many dependants is merged
  incompletely and silently.
- **Rewriting references**: the foreign key is rewritten by JSON path; where a child's id embeds the contact id (session
  registrations, memberships keyed `contact:<id>`, certifications `<program>:<ownerType>:<id>`), the row is cloned under a new
  id and the old one deleted; if the new id already exists the source row is dropped
  (`libraries-merging/src/logic/downstream-merge.js:179-234`).
- **Special consolidation rules** (`libraries-merging/src/logic/special/`): memberships merge into one, earliest join date wins
  (`merge-memberships.js:75-76`), chapters and past chapters concatenated (`:124-134`); certifications one per program
  (`merge-certifications.js:59-79`); donors re-keyed; accreditations one per program, with the winner possibly left pointing at
  the deleted organization (legacy-assessment §2).
- **Effects on other services**: invoices, registrations, memberships, activities and everything else that registered a
  reference are repointed by the `Map`; services that did not declare a reference (tasks, §2.3) are not.
- **Identity**: portal `user_ids` are unioned; Knox users keep pointing at the deleted contact (legacy-assessment §2).
  `merged_ids` exists on the contact schema (`rolodex-contacts/src/schema/object.schema.js:136`) and nothing writes it; old
  ids are hard-deleted with no alias.
- **Events**: contact merges publish `rolodex-contacts-merged` to the `rhythm-rolodex` bus
  (`merge-contact.step.json:92-106`), which tenants can forward to their own AWS accounts (§5.2). Organization merges publish
  nothing.
- **Audit**: the merge audit-log writer is an empty stub (legacy-assessment §2).
- No relationship-impact preview, no undo.

---

### 4. Search and query

#### 4.1 Quick search (Algolia)

- Contacts and organizations are indexed in Algolia: `indexer: 'Algolia'` with declared fields (contacts: roles, phones, image,
  job title, organization id and name, name parts, three emails, preferred address, custom fields; organizations: parent,
  phone, logo, domain and alternate domains, address, custom fields) (`rolodex-contacts/config.js:202-204`;
  `rolodex-organizations/config.js:186-187`).
- The indexer `system-algolia` consumes the platform change feed, cleans each record to its declared fields, tags it with its
  tenant and writes it to a shared per-table index (`system-algolia/src/logic/write-to-algolia.js:86-108`).
- A record without a `name` is skipped, including on delete (`write-to-algolia.js:94`; spec System FND-SYS-110).
- The Console gets a secured, tenant-filtered Algolia key valid for an hour and searches Algolia directly
  (`system-algolia/src/logic/secured-api-key-factory.js:34`; spec `system/design/search/DESIGN-SYS-005-*.md` §4).
- A full re-index runs after an import or on request; two customer-facing tables can get a per-tenant tuned index (spec
  DESIGN-SYS-005 §4).

#### 4.2 Query definitions, saved queries, scheduled delivery

- `rolodex-query-definitions` contributes nine query types to the reporting engine: contacts (with organization, parent
  organization and membership columns), organizations, organizations-and-contacts, organization contacts by role, contact
  communication preferences, contact and organization custom activities, engagement scores, tasks
  (`rolodex-query-definitions/src/definitions/*.js`; e.g. `contacts.js:6-50`).
- Queries are SQL over a replicated relational copy of the DynamoDB tables (`v_rolodex_*` views); execution is asynchronous
  (`SUBMITTED`, `CONVERTING`, `FINISHED`/`ERROR`), results go to S3 behind time-limited links; a small synchronous path exists;
  saved queries can be emailed on a schedule (spec `system/design/queries/DESIGN-SYS-*.md` §4; services `system-queries`,
  `platform-saved-searches`, `platform-custom-query-definitions`, `platform-query-delivery-schedules`).
- Query results downloaded as files are the everyday "export" for staff.

#### 4.3 Statistics

`rolodex-statistics` defines exactly two tenant statistics: number of contacts and number of organizations, each a count over
the reporting copy (`rolodex-statistics/src/logic/statistics.js:10-24`). Where they are displayed is **UNVERIFIED**.

#### 4.4 Whole-tenant export

`system-exports` zips every exportable table of a tenant (aggregate or per-table archive, optional password, optional
membership history), allowed only for Rhythm administrators or a hard-coded list of nine self-serve tenants; exporting from
restored backup tables needs a Rhythm administrator on a second allowlist
(`system-exports/src/lambda/api.js:19-83`; `src/schema/start-export-request.schema.js:3-7`; `config.js:13-16`). This export
also drives the platform-wide rebuilds (duplicates, search).

---

### 5. Imports, bulk operations, events, audit

#### 5.1 Imports and cleanup

- `system-data-imports` loads staged files straight into any table, bypassing each service's API: status `pending`, `running`,
  `rolling back`, `failed`, `completed`, `wiping`, with per-table counts and an error report
  (`system-data-imports/src/schema/object.schema.js:14-43`; readme: "one of the few services that has permissions to
  write/read every single table").
- Order of work: scan, split, **write** batches, **then validate** them with each target service's batch-validation function,
  then generate the error report and run post-migration tasks
  (`system-data-imports/build/stateMachines/load-data.step.json`, states `BatchWriteFiles` then `BatchValidateFiles`). Invalid
  rows are counted, not removed (`src/logic/validate-items.js:75-80,161-165`), so bad data stays in the table.
- Rollback replays prepared deletions per table and then erases the import record; an initial-load import can be run, rolled
  back or wiped only in a sandbox tenant (spec `system/design/imports-and-exports/DESIGN-SYS-007-*.md` §4, §6).
- Imported rows carry an `IMPORT` marker on the change feed until edited (spec DESIGN-SYS-007 §4). Every consumer that filters
  `anything-but: IMPORT` therefore ignores imported records: duplicate detection (§3.1), task notifications (§2.2), the
  EventBridge partner provisioning (`partners-eventbridge/build/template-resources.yaml:179-183`) and others.
- Field-mapping templates exist as a separate service (spec DESIGN-SYS-007 §5). No staff-facing bulk update or bulk delete of
  contacts in Rolodex was found in this pass (**UNVERIFIED**; the standard services expose `batch-operations` functions, for
  example `rolodex-custom-activities/src/lambda/batch-operations.js`).

#### 5.2 Events Rolodex publishes and who consumes them

Two channels:

1. **The platform change feed** (`system-data-replication` SNS topic `DomainObjectChangedSnsTopic`): every write to a contact
   or organization table is published with before and after images. At least these 1.0 services subscribe to Rolodex contact
   or organization changes (from `*/build/template-resources.yaml` filter policies):
   - `membership-memberships` (contacts and organizations, line 741-751)
   - `accreditation-accreditations` (organizations, 144-153); `accreditation-team-candidates` (contacts, 165-174, 235-244)
   - `events-abstracts` (contact deletion, 598-607)
   - `partners-higher-logic` (contacts, organizations, 270-280), `partners-mobilize` (contacts, 251-260),
     `partners-google-civic` (contacts, 171-180): sync to external systems
   - `rolodex-regions` (contacts, organizations, 120-133): region assignment
   - `rolodex-contacts` itself (organization updated, 386-395: copies organization data onto contacts) and
     `rolodex-organizations` (organization updated, flowdown, contact affiliation, 336-471)
   - `rolodex-contact-duplicates` (contacts, §3.1), `system-algolia` (all indexed tables, §4.1), the audit-log writer (§5.3)
   - Custom activities publish their own "activity changed" SNS topic with no subscriber (§1.1).
2. **The `rhythm-rolodex` EventBridge bus** (`rolodex-eventbridge/build/template-resources.yaml:5-8`, a bare bus). The only
   publisher found is the contact merge (`rolodex-contact-steps/build/stateMachines/merge-contact.step.json:92-106`, source
   `rolodex-contacts-merged`). Tenants can register an AWS account and choose `rolodex-contacts-merged` or
   `rolodex-contacts-changed` to forward (`integrations-eventbridge-accounts/src/schema/object.schema.js:14`);
   `partners-eventbridge` provisions a rule per tenant matching the chosen sources and the tenant id
   (`partners-eventbridge/src/logic/provisioning-logic.js:26-55`). No publisher of `rolodex-contacts-changed` was found
   (grep of every `*/build`, `*/src`, `*/config.js`), so that subscription delivers nothing (**UNVERIFIED** at runtime).
   An older SNS topic `rolodex-contacts-merged` also exists (`rolodex-contacts/config.js:83`;
   `devops-scripts/src/cloudformation/sns-subscription-contact-merged.hbs`); legacy-assessment §5 item 34 calls it a dead path.

#### 5.3 Audit and change tracking

- The audit-log writer consumes the change feed, diffs before and after, credits the last modifier and stores one file per
  record version in S3 forever; staff read a record's history and any past version (spec
  `system/design/audit-and-observability/DESIGN-SYS-*.md` §4).
- Writes that bypass the persistence library (engagement score batch writes, §1.2) carry no history; merges write no merge
  audit (§3.2); duplicate dismissals are unaudited (§3.1).
- Dead-lettered background failures go to a bucket nothing reads (same spec §4).

---

### 6. 1.0 behaviours a 2.0 architecture must account for

1. An activity belongs to exactly one contact or one organization; 1.0 has no multi-record links, participants or attachments (`rolodex-custom-activities/src/schema/object.schema.js:14-25`).
2. Activity types are tenant-defined with an icon and colour and no system meaning (`rolodex-custom-activity-types/src/schema/object.schema.js:13-21` (GitHub)).
3. A contact, organization or activity type with activities cannot be deleted (`rolodex-custom-activities/config.js:41-74`).
4. The contact timeline merges staff activities with Campaign Monitor, Mailchimp and ActiveCampaign email activity; organizations get none (`activities-one-click.component.ts:106-116`).
5. Other applications' activity (CE, accreditation, sales, registrations) is shown in their own 360 panels, not the CRM timeline (`rolodex-built-in-one-clicks.ts:1-26`).
6. A missing activity type breaks the 1.0 timeline; 2.0 must render activities whose type is gone (`activities-one-click.component.ts:118-120`).
7. Sales activities carry lead and opportunity links and a follow-up task that is never created (`sales-activities/src/schema/object.schema.js:15-31`).
8. Engagement scoring is opt-in per tenant with one contact model and one organization model (`retrieve-engagement-models.js:24-29`).
9. Model weights total 100 and point ranges are contiguous, start inclusive, end exclusive (`rolodex-engagement-models/src/object/validation.js:36-54`).
10. Scores are monthly, ranked by percentile within the tenant, banded into categories, compared year over year (`apply-statistics.js:57-116`).
11. Engagement metrics read six applications' data; the activity metric is a lifetime count per type (`generators/activities.js:33-40`).
12. Administrators can rescore 60 months back or regenerate up to 20 years, with no permission check (`rolodex-engagement-scores/src/lambda/api.js:44,89`; spec AUTH-ROL-007).
13. Tasks link to records generically and derive one contact and one organization association (`platform-tasks/src/object/cleaning.js:18-19`).
14. A task is assigned to a user or a queue; queues are FIFO by first-save time (`platform-tasks/src/schema/object.schema.js:33-40`; `cleaning.js:15-16`).
15. Assignment email goes to a newly assigned user unless self-assigned, opted out, completed or imported (`task-notification.js:41-100`).
16. Reminders and assignment roles exist in data but do nothing (`libraries-tasks/src/task-generation.schema.js:16-17`; `fulfillment.js:126-136`).
17. Product orders can generate tasks from templates with tokens and relative due dates, associated with the ship-to record (`platform-tasks/src/logic/fulfillment.js:25-160`).
18. Tasks are not repointed on merge and not protected on contact delete, because they declare no contact reference (`platform-tasks/config.js:86-102`).
19. Duplicate detection skips imports, the largest source of duplicates (`rolodex-contact-duplicates/build/template-resources.yaml:271-274` (GitHub)).
20. Duplicate scores have no stored reason; dismissals are permanent and unaudited (legacy-assessment §1; `search-logic.js:5,79-82` (GitHub)).
21. A public, unauthenticated portal duplicate search exists and leaks masked data (`rolodex-contact-duplicates/src/lambda/api.js:64` (GitHub)).
22. "Duplicate" is a manual status value on contacts and organizations, set by no automation (`rolodex-contacts/src/schema/object.schema.js:120`).
23. Merge accepts N sources into one survivor with per-field source choice (`rolodex-contacts/src/lambda/api.js:192-220`).
24. Merge reference discovery reads only the first page of registered references (`libraries-locking/src/services/relationship-service.js:145-161` (GitHub)).
25. Merge consolidates one membership, one certification per program, one accreditation per program, and re-keys composite ids, dropping collisions (`libraries-merging/src/logic/special/`; `downstream-merge.js:179-234`).
26. Merge is not atomic and can report success after failing (`merge-contact.step.json:6-117`).
27. Old ids vanish with no alias; `merged_ids` is never written; portal identities are unioned or left dangling (`rolodex-contacts/src/schema/object.schema.js:135-136`).
28. Contact merges emit an event that tenants can forward to their own AWS accounts, including the security context (`merge-contact.step.json:92-106`; `integrations-eventbridge-accounts/src/schema/object.schema.js:14`).
29. Tenants can subscribe to `rolodex-contacts-changed` but nothing publishes it (`integrations-eventbridge-accounts/src/schema/object.schema.js:14`).
30. Quick search is Algolia over declared fields with tenant-filtered keys; nameless records are skipped (`rolodex-contacts/config.js:202-204`; `system-algolia/src/logic/write-to-algolia.js:94`).
31. Reporting exposes nine Rolodex query types, including activities, engagement scores and tasks (`rolodex-query-definitions/src/definitions/`).
32. Rolodex statistics are only the contact and organization counts (`rolodex-statistics/src/logic/statistics.js:10-24`).
33. Whole-tenant export is restricted to Rhythm administrators and an allowlist (`system-exports/src/lambda/api.js:19-83`).
34. Imports write before they validate and keep invalid rows (`system-data-imports/build/stateMachines/load-data.step.json`; `validate-items.js:75-80`).
35. Imported records are invisible to every `anything-but: IMPORT` consumer until edited (`platform-tasks/build/template-resources.yaml:287-291`).
36. At least nine other services react to contact or organization changes, including Membership, Accreditation, Events and three partner syncs (`membership-memberships/build/template-resources.yaml:741-751` and §5.2).
37. Record history is a per-version diff kept forever; merges, score writes and dismissals leave no audit (spec System audit design §4; legacy-assessment §2).

## 22. E03 — Rhythm 2.0 CRM Backend Assessment (services-crm)

## E03 — Rhythm 2.0 CRM Backend Assessment (services-crm)

Evidence for the CRM architecture and gap analysis. Written 2026-10-19 from a read of the code only. Nothing was run except
`grep` and `git`.

### 0. What was read, and how to read the verdicts

- **Repository.** `rhythmsoftware/services-crm`, `origin/master` at **`a980b553`** (2026-10-19 13:20 ET, "Revert DEF-CRM-0682
  … conflicts with Andrew's ruling D-37"). It was exported with `git archive` into a scratch folder and read there.
  Andrew's local checkout `~/rhythm2/services-crm` is on `master` at `eac3df30` and is **102 commits behind** `origin/master`,
  so anyone reading files on disk is reading stale code. Three worktrees exist under `~/rhythm2/worktrees/services-crm/`
  (`crm-acc-w2-land`, `ra-p2c-crm-land`, `tde-crm-land`).
- **Paths.** Every path below is relative to the repository root, and line numbers are from `a980b553`.
- **Scale.** 9 modules (`entities`, `configuration-types`, `regions`, `tasks`, `activities`, `duplicates`, `merges`,
  `data-jobs`, `tenant-lifecycle`), 81 migrations (`0000`–`0078` with gaps, then six timestamp-named files dated
  2026-10-18), about 190 HTTP routes (§14), and roughly 1,870 commits since 2026-10-08. Test files by tier: unit 124,
  integration 249, end-to-end 52 (local dispatcher), legacy 32 (Rhythm 1.0 specs run unchanged), benchmark 4, and a 13-spec
  deployed-dev suite (`tests/e2e/dev/`).
- **Status vocabulary.**
  - **Verified complete:** the behaviour exists and an integration or end-to-end spec exercises it. I read the specs; I did not run them.
  - **Partial:** the core behaviour works, but a named part is missing or deferred.
  - **Scaffolded:** structure or declarations exist, but little behaviour.
  - **Missing:** no code exists.
  - **Unknown:** I could not determine the status from the code.
- **Acceptance context.** Product acceptance testing of CRM ran through 2026-10-19. The defect register on react-console
  `origin/master` (`acceptance/crm/reports/defects/README.md`) lists 357 defects: open 113, fixed 157, escalated 75,
  reclassified 12. Defect ids in CRM commits now reach DEF-CRM-0741, so that register lags the wave-2 and wave-3 repairs.
  Two architecture write-ups from wave 3 are relevant and still sit on unmerged services-platform branches:
  - `agent/crm-acc-w3-arch-lifecycle` (`docs/proposals/record-lifecycle/`, decisions D-02, D-07, D-25, D-41, D-42 and D-44);
  - `agent/crm-acc-w3-arch-writes` (`docs/standards/record-write-contracts.md`, decisions D-05, D-09, D-35, D-37, D-38, D-43
    and D-52).

  Both are **designed, not built**. The gap tables below say where.

### 1. services-rolodex: what it is

`~/rhythm2/services-rolodex` is **a symlink to `services-crm`** (`services-rolodex -> services-crm`, created 2026-10-15
21:06). It is not a separate or older port, and nothing separate is deployed under that name. Here is the history:

- The repository began as `services-rolodex` on 2026-10-08 (first commits `979578021` and `2401bbbd3`).
- It was renamed by Andrew's decision of 2026-10-15: "Rolodex becomes CRM, Finance becomes Accounting"
  (`docs/migrations/crm-accounting-rename/README.md`).
  - The `rolodex` schema became `crm` in migration `0075_crm_schema.sql`.
  - The stacks, functions, roles and SSM paths were renamed to `crm-*`.
  - The package became `@rhythmsoftware/services-crm`.
  - The remote of both paths is `https://github.com/rhythmsoftware/services-crm.git`.
- The `rolodex` name survives on purpose in four places:
  - migrations `0000`–`0071` and their snapshots;
  - the ported Rhythm 1.0 specs under `**/tests/legacy/**`;
  - the S3 asset folders `rolodex-contacts/`, `rolodex-organizations/` and `rolodex-contact-roles/`, which are shared with
    Rhythm 1.0 (`modules/entities/metadata/asset-objects.ts`);
  - CI's `TEST_DATABASE_URL`, which still names `services_rolodex_test`.
- Likewise, `~/rhythm2/services-finance` points at the same repository as `services-accounting`.

"Rolodex" in Rhythm 1.0 means the separate legacy microservices in `~/rhythm/rolodex-*`. CRM 2.0 ports these:

- `rolodex-contacts`, `rolodex-organizations`, `rolodex-contact-roles`, `rolodex-organization-types` and `rolodex-regions`;
- `crm-activities`, rebuilt as the `activities` module;
- the merge flows of `rolodex-contact-steps`, replaced by the record merge engine.

These are **not ported**, and nothing in CRM 2.0 corresponds to them:

- `rolodex-relationships` and `rolodex-relationship-types`: typed contact-to-contact and organization-to-organization
  relationships. `docs/affiliations/inventory-rhythm-1.md` §4 records them as unported.
- `rolodex-contact-directories` and `rolodex-organization-directories`.
- `rolodex-engagement-*` (models, scores, categories and metrics).
- `rolodex-statistics` and `rolodex-portal-features`.
- `rolodex-contact-role-rule-sets`, which was dead code.

### 2. Conventions actually in force

| Convention                         | What the code does                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  | Evidence                                                                                                                                             |
| ---------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- |
| Tenancy                            | Every table has `tenant_id`. Keys are `(tenant_id, id)`, and every reference is a composite foreign key. The tenant comes from the path (`/x/{tenantId}`) and the execution context (`tenantOf(ctx)`), never from the body.                                                                                                                                                                                                                                                                                                                                                                                                         | `AGENTS.md` ("Tenant-owned records"); e.g. `modules/entities/db/contacts.table.ts:122-138`                                                           |
| Row-level security                 | Every table has a forced, fail-closed policy on `app.tenant_id`. Each transaction switches to `rhythm_crm_app`, which cannot bypass RLS. The business-event tables have narrow dispatch and sweep policies.                                                                                                                                                                                                                                                                                                                                                                                                                         | `docs/row-level-security.md`; `db/migrations/0005_row_level_security.sql`; `tests/integration/db/row-level-security{,-coverage}.integration.spec.ts` |
| Execution context and transactions | Context comes first in every call; the tenant, clock and actor come from it. Every statement runs in `ctx.withTransaction`, and a record is saved together with its owned collections in one transaction (`lib/db/owned-collections.ts`). There is one written exception to "no external call inside a transaction": the compliance e-mail's S3 PutObject.                                                                                                                                                                                                                                                                          | `AGENTS.md`; `modules/entities/services/organizations/compliance/compliance-email.ts:1-20`                                                           |
| Time                               | Native `Date` is not used. Time comes from `ctx.now` and `@rhythmsoftware/macroservice-time`, enforced by `npm run lint:dates`.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                     | `README.md` "Working Locally"                                                                                                                        |
| Naming                             | Tables, columns, wire properties and paths are snake_case (the switch of 2026-10-16, migration `0076_snake_case_switch.sql`); code identifiers are camelCase. System columns start with `sys_`, references end in `_id`, and constraints are `<table>_<what>_<kind>`.                                                                                                                                                                                                                                                                                                                                                               | `docs/naming-conventions.md`; `tests/unit/lib/naming-standard.spec.ts`                                                                               |
| Strings                            | Values are trimmed, and an empty string becomes NULL. Every table has a `BEFORE INSERT/UPDATE` text-normalization trigger.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                          | migration `0048`; `tests/integration/db/text-normalization-coverage.integration.spec.ts`                                                             |
| E-mail addresses                   | Stored in the domain `crm.email_address` (lowercase, no whitespace), canonicalized and validated with `isEmailAddress`.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                             | `modules/entities/db/contact-email-addresses.table.ts:6-9`; `services/persistence/contacts.ts:463`                                                   |
| Record numbers                     | Allocated per tenant from `platform.number_seeds` (`insertWithNextNumber`), with collision recovery.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                | `modules/entities/db/contacts.table.ts:54-63,124,154-158`                                                                                            |
| **Ids: configuration tables**      | Configuration ids are immutable strings from `configurationIdFor`, enforced by `<table>_id_configuration_id_check` on contact types, contact roles, organization types, affiliation types, address and phone types, activity types and outcomes, duplicate rules, task types and task queues. **Regions are the exception:** they get a random 10-character id (`recordIdFor`, `modules/regions/services/persistence/regions.ts:130`), checked only by `regions_id_check` `^[A-Za-z0-9_                                                                                                                                             | -]+$` (`modules/regions/db/regions.table.ts:43`).                                                                                                    | `modules/entities/services/persistence/contact-types.ts:96`; the `*_id_configuration_id_check` constraints |
| **Ids: record tables**             | Contacts, organizations, affiliations, activities, merges and duplicate candidates get a UUIDv7 when the client names no id, but the client or an import may supply one (legacy ids are accepted). **Tasks get a random 10-character id** (`recordIdFor`, `modules/tasks/services/persistence/tasks.ts:221`). **No record table has a database `uuidV7Check`.** CRM does not depend on `@rhythmsoftware/macroservice-architecture`, so the `data.ids` rule (`STANDARD.md`, decided 2026-10-19) is not checked here, whereas Accounting carries the checks (e.g. `services-accounting/modules/billing/db/billing-notices.table.ts`). | `modules/entities/services/persistence/contacts.ts:1041,1687`; `modules/activities/services/persistence/activities.ts:257`                           |
| Update semantics                   | `PUT` replaces the whole record and requires `sys_version` (`contact-api-mapper.ts:56`). **A PUT that omits a collection empties it** (`docs/entities-api.md:275,1239`). `PATCH` is JSON Patch; a version is checked only when the client sends a `test` operation. `DELETE` is a hard delete.                                                                                                                                                                                                                                                                                                                                      | `docs/entities-api.md` "Update semantics"                                                                                                            |
| HTTP status                        | Every success answers 200; the shared dispatcher cannot answer 201 or 202.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                          | `docs/debt/created-status-code.md`                                                                                                                   |
| Persistence layout                 | Table declarations live in `modules/<m>/db/<table>.table.ts`. All SQL for a concept lives in `services/persistence/<concepts>.ts`, and routes never touch the database.                                                                                                                                                                                                                                                                                                                                                                                                                                                             | `AGENTS.md`; `tests/unit/persistence-layout.spec.ts`                                                                                                 |
| Cross-service SQL                  | `SECURITY DEFINER` functions in the owner's schema, with `EXECUTE` granted to the caller's role only.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                               | `docs/cross-service-functions.md`                                                                                                                    |

### 3. Contacts

| Capability                                                                                                                                                | Evidence                                                                                                                                                                                                                                                                                                                   | Status                         | Gap                                                                                                                                                                                                                                              |
| --------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| Structured name parts; derived `name`, `full_name`, `name_last_first`; a client may not send computed names; a first or last name is required; no parsing | `services/persistence/contacts.ts:361` (`contactNames`), `:1039`, `:1137`; `db/contacts.table.ts:75-80,146-150`; migration `0023`; `docs/migrations/contact-names-derived-full-name/README.md`; `tests/e2e/contacts-names.e2e.spec.js`, `tests/unit/services/persistence/contacts.spec.js` (all under `modules/entities/`) | Verified complete              | The first-or-last-name check was added `NOT VALID`, so existing rows were not re-checked (`contacts.table.ts:144-145`).                                                                                                                          |
| Statuses                                                                                                                                                  | `CONTACT_STATUSES = active, duplicate, deceased, inactive` (`db/contacts.table.ts:50`), with CHECK `contacts_status_check` (`:140`)                                                                                                                                                                                        | Partial                        | The list is closed and hard-coded, not tenant-configurable. Nothing derives `duplicate` (a merge deletes the losing record) and nothing reacts to `deceased`. There is no archive state; D-02 designs one (record-lifecycle branch).             |
| Contact types                                                                                                                                             | `db/contact-types.table.ts`; the type is a composite foreign key with no delete action (`contacts.table.ts:129-135`); `/contact_types` routes with reorder; `tests/e2e/contact-types.e2e.spec.js`; migration `0026`, `0033`                                                                                                | Verified complete              | —                                                                                                                                                                                                                                                |
| Numbers                                                                                                                                                   | Per-tenant sequence, `contacts_number_uq` (`contacts.table.ts:124`); an imported number is ignored and the source key kept in `sys_external_id` (DEF-CRM-0643); `tests/integration/db/contacts.table.integration.spec.js`; legacy `collision-detection.spec.js`                                                            | Verified complete              | —                                                                                                                                                                                                                                                |
| Other scalars (spouse, gender, birth and death dates, socials, do-not-contact flags, districts)                                                           | `contacts.table.ts:62-117`                                                                                                                                                                                                                                                                                                 | Partial                        | Districts and social fields are stored, but nothing fills them: Google Civic and FullContact enrichment are not ported (`docs/debt/image-files-and-enrichment.md`). `spouse_id` is the only contact-to-contact link (generic relationships, §1). |
| Photo                                                                                                                                                     | Asset pipeline (`services/assets/entity-assets.ts`); `tests/e2e/dev/contact-photo.dev.spec.ts`                                                                                                                                                                                                                             | Verified complete              | —                                                                                                                                                                                                                                                |
| Communication preferences                                                                                                                                 | `db/contact-communication-preferences.table.ts:25,34`                                                                                                                                                                                                                                                                      | Partial                        | `communication_type_id` is free text with no foreign key. Rhythm 1.0's removal of unknown types is not carried (`docs/debt/communication-type-check.md`).                                                                                        |
| Descendant listing (contacts of an organization tree)                                                                                                     | `GET /contacts/{tenantId}/descendants/{organization_id}`; recursive CTE at `services/persistence/contacts.ts:1445-1465`; `docs/migrations/list-descendant-contacts/`                                                                                                                                                       | Verified complete (staff only) | Portal access is deferred (`docs/debt/portal-descendant-contacts.md`).                                                                                                                                                                           |
| Delete                                                                                                                                                    | Hard delete, refused while the contact or an element is locked (`services/persistence/contacts.ts:1297-1322,1498`)                                                                                                                                                                                                         | Partial                        | There is no archive or undelete, and no cross-service impact check (D-41). Activity links cascade (§10). Revisions keep the deleted person's data (D-42).                                                                                        |
| Read at documented limits                                                                                                                                 | DEF-CRM-0031 (high, open): a contact at the limits (100 affiliations and so on) fails with HTTP 500 on the 360 view                                                                                                                                                                                                        | Unknown                        | Not confirmed as a backend or console fault in the register.                                                                                                                                                                                     |

### 4. Organizations

| Capability                                                                               | Evidence                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                 | Status                                         | Gap                                                                                                                                                                                                                                                                                                                                                                   |
| ---------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Organization CRUD, number, status (`active`, `duplicate`, `inactive`)                    | `modules/entities/db/organizations.table.ts:46,68,111`; `services/persistence/organizations.ts`; `tests/e2e/organizations.e2e.spec.js`                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                   | Verified complete                              | Statuses are hard-coded. There are no organization e-mail addresses at all (columns, §4 listing).                                                                                                                                                                                                                                                                     |
| Organization types                                                                       | `db/organization-types.table.ts`; configuration ids (migration `0038`); `tests/e2e/organization-types.e2e.spec.js`; legacy `rolodex-organization-types` specs                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                            | Verified complete                              | The portal list of types is not carried (`docs/debt/organization-types.md`). Key roles and forms were removed by decision (migrations `0032`, `0044`).                                                                                                                                                                                                                |
| Hierarchy: parent, no cycles, ancestors                                                  | `parent_organization_id` with a foreign key; migration `0018_organization_parent_not_descendant`; ancestors are computed on read with recursive CTEs (`services/persistence/organizations.ts:636-720`, `services/organizations/ancestry/organization-ancestry.ts`); `tests/unit/services/organizations/ancestry/organization-ancestry.spec.js`; legacy `ancestors.unit.spec.js`                                                                                                                                                                                                                                                                                                          | Verified complete (API reads)                  | **The hierarchy is not in the query layer:** `organizations_for_queries` (migration `0045`) has no ancestor or descendant columns, so saved queries and eligibility cannot express "this organization and everything under it". The organization's own type is not offered on contact lists, because picks follow one hop only (`docs/affiliations/README.md` §19.4). |
| Flowdown (copying an organization's address and phone to its contacts)                   | Retired 2026-10-14 (migration `0066`) and replaced by **linked contact information**: a contact row whose `source_mode` is `linked` names the affiliation and the organization's address or phone, and the value is resolved on read (`db/contact-addresses.table.ts:150-170`; `docs/affiliations/README.md` §0b; `tests/integration/services/persistence/contact-information-sources.integration.spec.js`; `tests/e2e/contact-information-sources.e2e.spec.js`; `tests/e2e/dev/contact-information-sources.dev.spec.ts`)                                                                                                                                                                | Verified complete                              | "As of" reads are impossible because organization address and phone history is not kept (§19.5). The `disconnected` source mode (D-25) is designed but not built.                                                                                                                                                                                                     |
| Domain name and website derivation                                                       | `db/organization-domain-name.ts`; `tests/e2e/organizations-domain-name.e2e.spec.js`                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                      | Verified complete (legacy quirks kept as debt) | `docs/debt/organization-domain-name-oddities.md`, items 2–4.                                                                                                                                                                                                                                                                                                          |
| Contact-role compliance rules (minimum and maximum per role per type) with recalculation | `services/organizations/compliance/organization-compliance.ts`, `compliance-change-events.ts` (the handler on contact `affiliations` and `organization_roles`), `compliance-reprocess.ts`; table `organization_compliance_violations`; migrations `0020`, `0031`; five integration specs under `tests/integration/services/organizations/compliance/`; `tests/e2e/organization-compliance.e2e.spec.js`; `tests/e2e/dev/contact-role-compliance.dev.spec.ts`                                                                                                                                                                                                                              | Verified complete                              | Open product questions: should a type change recalculate (DEF-CRM-0111)? Is a rule with only `minimum: 0` a rule (DEF-CRM-0324)?                                                                                                                                                                                                                                      |
| Compliance notification templates (Liquid)                                               | The template belongs to the organization type and is stored by Platform communications (`services/templates/compliance-notification-template.ts`). The notice is submitted inside the recalculation's transaction (`compliance-email.ts:1-40`). Preview: `POST /organizations/{t}/preview_contact_role_compliance_email` and `POST /email_previews/{t}`. Liquid is rendered by Platform (`liquidjs` 10.30.0 is pinned in `package.json:94` for the shared renderer). Tests: `tests/integration/services/templates/compliance-notification-template.integration.spec.js`, `tests/unit/services/organizations/compliance/compliance-email.spec.js`, `tests/e2e/email-previews.e2e.spec.js` | Partial                                        | CRM's part ends when it hands the notice to Platform (`docs/debt/organizations-logic.md`). Actual delivery depends on Platform's dispatcher and send function, which were not checked here. A type with no template is `configuration_blocked`, and there is no default template.                                                                                     |
| Locks                                                                                    | `sys_locked` on every record                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                             | Partial                                        | DEF-CRM-0118 (high, open): an organization locked `updates & deletes` can never be unlocked through the product.                                                                                                                                                                                                                                                      |
| Organization affiliations view                                                           | `GET /organizations/{t}/{id}/affiliations?effective=current`                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                             | Verified complete                              | —                                                                                                                                                                                                                                                                                                                                                                     |
| Batch, by-domain lookup, public read, field-conflict and social-profile routes           | Rhythm 1.0 `rolodex-organizations` had them (`docs/debt/organizations-logic.md`)                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                         | Missing                                        | Not carried. Lookup by domain is the one most likely to be missed, by integrations and the portal.                                                                                                                                                                                                                                                                    |

### 5. Affiliations

| Capability                                                                                                                                               | Evidence                                                                                                                                                                                                                                                                                                                                                                                                                                                  | Status            | Gap                                                                                                                                                           |
| -------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Model: dated, typed contact-to-organization affiliations, each holding roles, replacing `organization_id`, `contact_role_ids` and `organization_history` | `modules/entities/db/contact-organization-affiliations.table.ts:1-41`; `contact-organization-roles.table.ts`; migration `0065`; `docs/affiliations/README.md` (accepted 2026-10-14)                                                                                                                                                                                                                                                                       | Verified complete | —                                                                                                                                                             |
| At most one primary per contact (D1); a primary has no end date (D2); dates ordered (D3); no overlapping periods for the same organization and type (D4) | unique index `contact_organization_affiliations_one_primary_uidx` (`:85-87`); checks at `:101-106`; the exclusion constraint is in the migration (since `0067`). Aggregate tests `tests/integration/services/persistence/contact-affiliations.integration.spec.js` (scenarios 1–4, 12 and 13, concurrency at `:405-445`); table test `tests/integration/db/contact-organization-affiliations.table.integration.spec.js:155-175` (a direct insert refused) | Verified complete | —                                                                                                                                                             |
| Affiliation types                                                                                                                                        | `db/affiliation-types.table.ts`; default contact-information links (`affiliation-type-contact-info-defaults.table.ts`); `tests/e2e/affiliation-types.e2e.spec.js`                                                                                                                                                                                                                                                                                         | Verified complete | `reapply_affiliation_defaults` exists in the API, but no screen offers it (§19.6).                                                                            |
| Roles on an affiliation, dated                                                                                                                           | `contact_organization_roles` with `start_date`/`end_date` (`:50-52,74-77`); at most 25 per affiliation (`:87`)                                                                                                                                                                                                                                                                                                                                            | Verified complete | Deleting a role that organization types still list is an open question (DEF-CRM-0325).                                                                        |
| History: Organization History is the affiliations that have ended; the change history shows affiliations and roles                                       | `services/persistence/contacts.ts` (`set_primary_organization` ends or keeps the previous primary); `docs/record-history.md`; `docs/debt/contact-roles-in-history.md`                                                                                                                                                                                                                                                                                     | Verified complete | —                                                                                                                                                             |
| Effective dates and "as of"                                                                                                                              | `effectiveOn` (`:122-124`); view `contact_organization_affiliations_current`; query-layer `as_of` (`docs/entities-api.md:405`)                                                                                                                                                                                                                                                                                                                            | Verified complete | —                                                                                                                                                             |
| Limits                                                                                                                                                   | `MAX_AFFILIATIONS = 100` (`:116`)                                                                                                                                                                                                                                                                                                                                                                                                                         | Verified complete | —                                                                                                                                                             |
| **Replacing a contact removes its affiliations**                                                                                                         | DEF-CRM-0174 (high, open): a PUT without `affiliations` deletes every affiliation and role and answers 200                                                                                                                                                                                                                                                                                                                                                | Partial           | D-09 ("an omitted collection is unchanged; removing a relationship takes an explicit operation") is designed on `agent/crm-acc-w3-arch-writes` but not built. |
| Portal "acts for organization X"                                                                                                                         | `docs/affiliations/README.md` §19.7                                                                                                                                                                                                                                                                                                                                                                                                                       | Missing           | Owned by Security and the Web Experience Platform.                                                                                                            |

### 6. Addresses, e-mail addresses and phone numbers

| Capability                                                                                                                                                                                          | Evidence                                                                                                                                                                                                                                                                  | Status            | Gap                                                                                                                                                        |
| --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Contact e-mail addresses: three ordered slots, the first primary; format validated                                                                                                                  | `modules/entities/db/contact-email-addresses.table.ts:30,39,51`; `services/persistence/contacts.ts:463-464`                                                                                                                                                               | Verified complete | E-mail addresses have no configurable type (work or personal); "typed e-mails need a type column?" is open (D-40). Organizations have no e-mail addresses. |
| Duplicate-email prevention                                                                                                                                                                          | The primary address is unique in the tenant (unique index `contact_email_addresses_primary_email_address_uidx`, `:48`, migration `0009`); `docs/migrations/contact-duplicate-email-prevention/`; `tests/integration/db/contact-email-addresses.table.integration.spec.js` | Verified complete | Secondary addresses are never compared. The tenant switch from Rhythm 1.0 was dropped on purpose (`docs/debt/duplicate-email-prevention.md`).              |
| Contact addresses and phone numbers: one row per configurable type, at most one preferred, a seasonal address can never be preferred, latitude and longitude ranges, owned, copied or linked source | `db/contact-addresses.table.ts:68,84,102-126,150`; `db/contact-phone-numbers.table.ts:46-66,97`; migration `0035` (configurable types) and `0066` (sources); table and persistence specs                                                                                  | Verified complete | —                                                                                                                                                          |
| Organization addresses (with preferred) and phone numbers                                                                                                                                           | `db/organization-addresses.table.ts:56-59`; `db/organization-phone-numbers.table.ts:40`                                                                                                                                                                                   | Partial           | **Organization phone numbers have no preferred flag** (§0b.1 states it).                                                                                   |
| Configurable address and phone types (ordered, `applies_to`)                                                                                                                                        | `db/address-types.table.ts`, `phone-number-types.table.ts`, `collection-types.ts:66-68`; `/address_types` and `/phone_number_types` routes with `/order`; `tests/e2e/address-types.e2e.spec.js`, `phone-number-types.e2e.spec.js`; migrations `0035`, `0067`, `0074`      | Verified complete | —                                                                                                                                                          |
| Address cleaning (country and state normalisation) and US phone formatting                                                                                                                          | `db/address-country-and-state.ts`, `db/phone-number-format.ts`; `docs/migrations/contact-and-organization-cleaning/`                                                                                                                                                      | Verified complete | `country_name` is no longer derived (`docs/debt/name-parsing-and-address-normalization.md`).                                                               |
| Effective views for queries (`contact_addresses_effective`, `contact_phone_numbers_effective`)                                                                                                      | migration `0070`; `tests/integration/db/contact-information-effective.view.integration.spec.js`                                                                                                                                                                           | Verified complete | The `_resolved` compatibility views are still to be dropped (§19.8).                                                                                       |

### 7. Contact roles and automatic role assignment

| Capability                                                                                                                   | Evidence                                                                                                                                                                                                                                       | Status               | Gap                                                                                                                              |
| ---------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------- | -------------------------------------------------------------------------------------------------------------------------------- |
| Contact role management, with custom fields and configuration ids                                                            | `db/contact-roles.table.ts`, `contact-role-custom-field-values.table.ts`; `/contact_roles` (7 routes including `/order`); `tests/e2e/contact-roles.e2e.spec.js`; legacy `rolodex-contact-roles` specs (four unit files)                        | Verified complete    | —                                                                                                                                |
| Delete refused while a role is held                                                                                          | foreign key from `contact_organization_roles` (`contact-organization-roles.table.ts:66-72`)                                                                                                                                                    | Verified complete    | —                                                                                                                                |
| Default roles for a new tenant                                                                                               | `modules/tenant-lifecycle/services/participants/contact-role-seeds.ts` (`admin_contact`, `billing_contact`)                                                                                                                                    | Verified complete    | `docs/debt/contact-roles.md` still says "a tenant starts with none", which is stale.                                             |
| Automatic assignment (`assign_automatically`, `assign_automatically_to_first_contact`) when the primary organization changes | `services/contacts/role-assignment/contact-role-assignment.ts:1-60` (locks the organization's members); `tests/integration/services/contacts/role-assignment/contact-role-assignment.integration.spec.js`; the legacy unit test runs unchanged | Verified complete    | Assignment happens only when the primary organization changes. Additional affiliations get no automatic roles (by design, §5.6). |
| Role names copied onto a contact (`contact_roles`)                                                                           | `docs/debt/contact-roles.md`                                                                                                                                                                                                                   | Missing (deliberate) | Names are joined on read through the affiliations instead.                                                                       |

### 8. Regions

| Capability                                                                                                             | Evidence                                                                                                                                                                                                                                                                                                                                                       | Status            | Gap                                                                                              |
| ---------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------- | ------------------------------------------------------------------------------------------------ |
| Region CRUD; assignment settings and rules (postal ranges, countries, states); preview; resumable tenant recalculation | `modules/regions/**` (19 routes); migrations `0019`, `0043`, `0055`; `services/region-assignment/*`; six integration specs including `region-assignment-rules`, `-delivery` and `region-recalculation-errors`; `tests/unit/region-matching.spec.js`; `tests/e2e/regions.e2e.spec.js`; `tests/e2e/dev/regions.dev.spec.ts`; the legacy `rolodex-regions` config | Verified complete | —                                                                                                |
| Reassignment on an address change, including addresses linked to an organization                                       | handlers `region-assignment`, `region-assignment-override` and `linked-contact-region-assignment` (`docs/change-events.md:234-245`)                                                                                                                                                                                                                            | Verified complete | —                                                                                                |
| Region custom fields                                                                                                   | `docs/debt/region-custom-fields.md`                                                                                                                                                                                                                                                                                                                            | Missing           | —                                                                                                |
| Region ids                                                                                                             | `recordIdFor` (`services/persistence/regions.ts:130`)                                                                                                                                                                                                                                                                                                          | Partial           | A region is configuration but gets a random record-style id, which breaks the ids standard (§2). |

### 9. Custom fields

| Capability                                                                       | Evidence                                                                                                                                                                                                                                                                                                                           | Status                        | Gap                                                                                                                                                                                                                                                                                                                           |
| -------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Storage                                                                          | Value tables built by Platform's factory: `contact_custom_field_values`, `organization_custom_field_values`, `contact_role_custom_field_values` (in `modules/entities/db/`) and `activity_custom_field_values` (`modules/activities/db/`); migrations `0007`, `0016`, `20261018115459_custom_field_string_index_without_value.sql` | Verified complete             | There are no custom fields on regions, tasks, organization types, contact types or affiliations.                                                                                                                                                                                                                              |
| Validation against the tenant's definitions (Platform metadata) and the wire map | `lib/api/custom-fields-wire.ts`; `docs/entities-api.md:1133-1139`; `tests/e2e/contacts-custom-fields.e2e.spec.js`, `organizations-custom-fields.e2e.spec.js`; `tests/e2e/dev/custom-fields.dev.spec.ts`                                                                                                                            | Verified complete (API level) | `required` is enforced only at the `api` level; the `console` and `portal` levels are not enforced anywhere. There are no read-only or computed fields and no field-level permissions (`docs/debt/custom-field-values.md`). DEF-CRM-0029: decimals are answered as strings (`"5.00"`), and the console form draws them empty. |
| Query                                                                            | The value table is registered in the queries datasets (`aws/crm.queries.ts`, `aws/crm.query-grants.ts`); the string index                                                                                                                                                                                                          | Verified complete             | The eligibility condition `crm.custom_field` offers only text operators (`docs/debt/eligibility-custom-field-value-type.md`).                                                                                                                                                                                                 |
| Image and document fields                                                        | asset pipeline (`entityAssets.changes`)                                                                                                                                                                                                                                                                                            | Verified complete             | —                                                                                                                                                                                                                                                                                                                             |

### 10. Activities

| Capability                                                                                                                                                                                                                  | Evidence                                                                                                                                                                                                                                              | Status                                                              | Gap                                                                                                                                                                            |
| --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| Activity types (seven system types plus the tenant's own, field modes, outcomes); activities (sanitised HTML body, status, direction, owner); links to many contacts and organizations; attachments; custom fields; history | `modules/activities/**`; migration `0078`; `docs/migrations/activities/README.md` (status line: "built … awaiting review"); 12 routes plus history and timelines; seven integration specs; `tests/e2e/activities.e2e.spec.js`; the timeline benchmark | Verified complete (backend)                                         | No deployed-dev spec covers activities. There is no tenant-wide activity search, only a search within one record's timeline (decision D2). E-mail and calendar sync is absent. |
| Timelines (contact and organization) with summaries; last activity derived when read                                                                                                                                        | `/activity_timelines/{t}/(contacts                                                                                                                                                                                                                    | organizations)/{id}[/summary]`; `services/persistence/timelines.ts` | Verified complete                                                                                                                                                              | —   |
| Activity data jobs (insert, upsert, update, delete)                                                                                                                                                                         | the `crm.activities` entries in `modules/data-jobs/services/operations/data-operations.ts:49-66`; `tests/integration/services/operations/activity-imports.integration.spec.js`                                                                        | Verified complete                                                   | —                                                                                                                                                                              |
| Behaviour when a linked record is deleted                                                                                                                                                                                   | `modules/activities/db/activity-links.table.ts:6-7,55-65`: `ON DELETE CASCADE` on the contact and organization links                                                                                                                                  | Partial                                                             | Deleting a contact silently drops its links, and an activity can end up linked to nothing. D-44 (tombstones and an "orphaned activities" view) is designed but not built.      |
| Replace semantics on PUT                                                                                                                                                                                                    | DEF-CRM-0700: a PUT that omits a field clears it                                                                                                                                                                                                      | Partial                                                             | Open product question.                                                                                                                                                         |

### 11. Tasks

| Capability                                                                                                                                                                                                                                               | Evidence                                                                                                                                                                                                                                    | Status            | Gap                                                                                                                                                                                                                                                                            |
| -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| Tasks (lifecycle: open, completed, cancelled), assignee, queue, task types and queues (configuration ids), offered tasks, claim and assign, subject pointing at any registered record, workflow actions through `POST /tasks_service/{t}` (service-only) | `modules/tasks/**`; migrations `0040`, `0052`, `20261018011759_tasks_action_link`, `20261018191938_tasks_role_claim`; 11 integration specs including concurrency and role-claim; `tests/e2e/tasks.e2e.spec.js`, `tasks-service.e2e.spec.js` | Verified complete | Task ids are random 10-character strings (`tasks.ts:221`), not UUIDv7. Tasks keep **no record history**. The assignee is not checked to be a console user of the workspace (DEF-CRM-0709). Complete, reopen and cancel do not require a version (D-52 is designed, not built). |

### 12. Duplicate detection, record merges and merge jobs

| Capability                                                                                                                                                                                                                                           | Evidence                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                         | Status                         | Gap                                                                                                                                                                                                                                                                                                                                                                                                             |
| ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Detection: match keys, Jaro-Winkler and nickname scoring, bands, explanations, duplicate rules (`warn`, `block`, review), candidates and groups, dismissals, rescans on a queue, synchronous checks, portal sign-up matching                         | `modules/duplicates/**` (17 routes); migrations `0051`, `0054`, `0064`, `0077`; 12 unit and 16 integration specs; `tests/e2e/duplicates.e2e.spec.js`, `duplicate-groups.e2e.spec.js`; `tests/e2e/dev/duplicates.dev.spec.ts`; the detection handlers on the contact and organization streams (`aws/crm.change-events.ts:48-86`)                                                                                                                                                                                                                                                                                                                                                  | Verified complete              | A bulk update writes no detection request (`docs/data-jobs.md` "What is not built").                                                                                                                                                                                                                                                                                                                            |
| Merge planning (survivor recommendation, field and collection decisions, plain-language preview), validation, execution as a durable job, plan expiry, aliases for merged-away ids, settlement, tenant merge history; contacts **and** organizations | `modules/merges/**` (11 routes); migrations `0047`, `0050`, `0056`, `0058`, `0059`; 13 unit and 29 integration specs (`merge-execution`, `organization-merge-execution`, `overlapping-merges`, `required-participants`, `worked-example`, `record-merge-job`, `organization-merge-job` and others); three e2e specs; `tests/e2e/dev/merges.dev.spec.ts`                                                                                                                                                                                                                                                                                                                          | Verified complete              | There is no unmerge: a merge cannot be reversed. 201 and 202 cannot be answered. The merge plan's hash leaves out reassignments that appear after the preview (question 3 of the arch-writes report).                                                                                                                                                                                                           |
| References rewritten in CRM                                                                                                                                                                                                                          | `modules/merges/references/crm-references.ts:60-430` lists every column that holds a contact or organization id, with a policy for each: the contact's collections, affiliations and roles, conversion issues, custom-field reference values (by row), tasks (both references), activity links, `contacts.spouse_id`, managed assets, change events, region recalculations, record revisions, data-job items, receipts, id mappings and bulk jobs; the organization's addresses and phones, compliance violations, affiliations and `organizations.parent_organization_id`. It is checked against the live catalog by `tests/integration/db/merge-coverage.integration.spec.ts`. | Verified complete              | —                                                                                                                                                                                                                                                                                                                                                                                                               |
| Participants in other services (`<schema>.merge_plan_<object>` and `merge_apply_<object>`, found in the catalog)                                                                                                                                     | `docs/cross-service-functions.md`; `modules/merges/services/persistence/merge-participants.ts:91-94` (`REQUIRED_PARTICIPANT_SCHEMAS = { contacts: ['platform'], organizations: [] }`); `required-participants.integration.spec.js`. Only **services-platform** and **services-committees** ship merge functions on `origin/master`.                                                                                                                                                                                                                                                                                                                                              | Partial (a cross-service risk) | **services-accounting (24 migrations referencing `contact_id` or `organization_id`), services-commerce (7), services-submissions (3) and services-membership declare no participant.** A merge deletes the losing record while those services keep its id; only `GET /merges/{t}/aliases/{objectType}/{recordId}` can resolve it, and only when the consumer asks. Organizations require no participant at all. |
| Business event on merge                                                                                                                                                                                                                              | `aws/crm.events.ts:26` (`crm.contact.merged`); `modules/merges/services/merges/merge-events.ts:8`; `tests/e2e/business-events.e2e.spec.js`                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                       | Partial                        | `crm.organization.merged` is in the Integration Platform catalogue but not published.                                                                                                                                                                                                                                                                                                                           |

### 13. Data jobs: import, export, bulk update, rollback

| Capability                                                                                                                                                              | Evidence                                                                                                                                                                                                                             | Status            | Gap                                                                                                                                                                                                     |
| ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Operation registry: contacts (`update`, `insert`, `upsert`), organizations (`insert`, `upsert`, `update`), seven configuration types, activities; ordered by dependency | `modules/data-jobs/services/operations/data-operations.ts:49-66`; `docs/data-jobs.md:194-215`                                                                                                                                        | Verified complete | The organization `update` is only the reference-only second pass of an import.                                                                                                                          |
| Interactive and bulk lanes (SQS, Step Functions bulk workflow, admission, cancellation, submission purge, idempotency)                                                  | `modules/data-jobs/aws/api/*` (about 33 routes); migrations `0057`, `0068`, `20261018014429`, `20261018035527`; 14 integration specs under `tests/integration/services/jobs/`; four e2e specs; `tests/e2e/dev/data-jobs.dev.spec.ts` | Verified complete | The bulk capacity is a module constant (2 in dev, 4 in production), not Platform's `platform.data_job_capacity`, which does not exist (`docs/debt/platform-data-job-capacity.md`).                      |
| Imports with files (logos, photos, image custom fields)                                                                                                                 | `services/operations/record-files.ts`; `initial-import-guarantees.integration.spec.js:18-30`                                                                                                                                         | Verified complete | `docs/data-jobs.md:755ff` still says "A bulk import cannot carry a file", which is stale.                                                                                                               |
| Crosswalk and reading back by source key                                                                                                                                | `data_id_mappings`; `POST /contacts/{t}/data_jobs/records_by_source_key`; `records-by-source-key` integration and e2e specs                                                                                                          | Verified complete | —                                                                                                                                                                                                       |
| Bulk delete (contacts, organizations, activities)                                                                                                                       | `contact-delete.ts`, `organization-delete.ts`, `activity-delete.ts`; the matching integration specs                                                                                                                                  | Verified complete | A delete is not reversible (`hard_delete_not_reversible`).                                                                                                                                              |
| Rollback                                                                                                                                                                | `services/operations/contact-update-rollback.ts`; `contact-update-rollback.integration.spec.js`; `docs/data-jobs.md:666-704`                                                                                                         | Partial           | It puts back a contact's own fields only. **Changed collections and custom fields are `not_reversible_operation`**, and organizations have no rollback at all (`docs/debt/rollback-of-collections.md`). |
| Versions frozen when a job validates (D-37 and D-38)                                                                                                                    | DEF-CRM-0682 was fixed, then **reverted at `a980b553`** because it conflicted with D-37                                                                                                                                              | Missing           | Unless the submitter sends `expectedVersion`, the apply step overwrites any edit made since validation (`docs/data-jobs.md` "What is not built").                                                       |
| Tenant reset (load a tenant again)                                                                                                                                      | `modules/tenant-lifecycle/services/participants/reset-batches.ts`; `reset.integration.spec.js`                                                                                                                                       | Verified complete | —                                                                                                                                                                                                       |
| Export                                                                                                                                                                  | The tenant data export (`modules/tenant-lifecycle/services/export/dataset-exports.ts`, `export-staging.ts`, `export-coverage.spec.js`); list exports go through Platform's query layer                                               | Partial           | The tenant export's staging switch is still being landed (worktree `tde-crm-land`, branch `tde-crm-staging-on`). CRM has no export of its own.                                                          |

### 14. Quick search, record history, change events, business events, outbox

| Capability                                                                                                                                | Evidence                                                                                                                                                                                                                                                                                         | Status            | Gap                                                                                                                                                                                                                                                                                                                                                                                              |
| ----------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| Quick Search                                                                                                                              | CRM only feeds Platform's index: the remote handler `quick-search-indexer` on the contact and organization streams (`modules/entities/services/quick-search/quick-search-indexer.ts`). Tests: `tests/unit/services/quick-search-indexer.spec.js`, `tests/unit/quick-search-declaration.spec.ts`  | Partial           | **DEF-CRM-0119 (high, escalated): new records did not reach the dev index after 1,004 s.** Activities and tasks are not fed. No CRM spec proves end-to-end indexing. `docs/debt/events-and-search.md` is stale.                                                                                                                                                                                  |
| Change capture (per-entity `<entity>_change_events` tables, settle trigger, dispatcher, handlers, claim, dead-letter)                     | `docs/change-events.md`; migrations `0029`, `0034`; `aws/crm.change-events.ts`; `tests/integration/db/change-events-capture.integration.spec.js`; `tests/unit/change-event-registry.spec.ts`                                                                                                     | Verified complete | The legacy outbox was retired on 2026-10-10. Change events stay inside CRM by design.                                                                                                                                                                                                                                                                                                            |
| Record history (`crm.record_revisions`: field-level, actor label snapshot, source, job attribution) and history routes                    | `docs/record-history.md`; `db/change-event-installs.ts:89,149`; `lib/record-history/*`; migrations `0046`, `0053`; `contacts.history.integration.spec.js`, `record-revisions.queries.integration.spec.js`, `tests/e2e/contacts-history.e2e.spec.js`, `tests/e2e/dev/contact-history.dev.spec.ts` | Partial           | **Only contacts and activities keep history.** Organizations, tasks, regions and every configuration type have none (`change-event-installs.ts:16-18`; `defineRecordHistoryRoutes` is used only in `contacts.routes.ts:34` and `activities.routes.ts:38`). A deleted contact's revisions keep its personal data indefinitely (D-42, privacy). There is no "deleted records" view (DEF-CRM-0604). |
| Business events (outbox `crm.business_events` and its queue, dispatch and sweep policies, manifest registration with Platform extensions) | `docs/business-events.md`; migration `20261018051458_business_events.sql`; `aws/crm.events.ts:26`; `tests/integration/db/business-events.integration.spec.ts`; `tests/unit/business-events.spec.ts`                                                                                              | Partial           | **Only `crm.contact.merged` is published.** The catalogue (`macroservice-libraries` `packages/integration-contracts/src/generated/schemas.ts:10601-10748`) declares `crm.contact.created`, `.updated`, `.deleted`, `crm.organization.created`, `.updated`, `.deleted` and `.merged`, plus a `crm.contact.enrich` command. None of those is published or handled.                                 |
| Audit                                                                                                                                     | Revisions plus Platform's security audit                                                                                                                                                                                                                                                         | Partial           | There is no CRM read-audit and no audit of configuration changes (consequence of the row above).                                                                                                                                                                                                                                                                                                 |

### 15. Row-level security, permissions and policies

| Capability                                                                                                                                         | Evidence                                                                                                                                                                                           | Status            | Gap                                                                                                                                                                                                                                                   |
| -------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Tenant RLS on every table                                                                                                                          | §2; `tests/integration/db/row-level-security*.ts`                                                                                                                                                  | Verified complete | —                                                                                                                                                                                                                                                     |
| Permission catalog (`crm.<resource>.<action>`, tiers read/user/admin; `crm.application.access` enforced since 2026-10-13; licensing step enforced) | `aws/crm.service.ts:75-300`; `aws/api/crm.routes.ts`; `tests/unit/route-authorization.spec.ts`, `application-access.spec.ts`; `modules/entities/tests/e2e/application-access-enforced.e2e.spec.js` | Verified complete | `README.md` says ".bulk … no route requires it yet", which is stale: data jobs require `.bulk` (`data-jobs.routes.ts:99`, `bulk-data-jobs.routes.ts:148-210`).                                                                                        |
| Record-level access                                                                                                                                | No route names a `resource_type`, so every condition is tier-based (`contacts.routes.ts:14`)                                                                                                       | Missing           | Rules such as "staff of chapter X see only their chapter" or "only my region" cannot be expressed. A known bug in `accessFilter` (`docs/debt/access-filter-single-id.md`) breaks id-set grants in every service, and CRM works around it in one read. |
| Field-level permissions                                                                                                                            | —                                                                                                                                                                                                  | Missing           | This includes custom fields (§9).                                                                                                                                                                                                                     |
| Built-in policies                                                                                                                                  | `docs/built-in-policies.md`: "CRM declares none today"; `tests/unit/built-in-policies.spec.ts`                                                                                                     | Missing           | There is no "Contacts editor" or similar; tenants rely on the generated tier policies only.                                                                                                                                                           |
| Separate permission for removing relationships, and for overwriting (D-09, D-37)                                                                   | arch-writes standard                                                                                                                                                                               | Missing           | Designed only.                                                                                                                                                                                                                                        |

### 16. APIs (route inventory)

About 190 routes across seven API functions plus one service-only route. The method and path lists come from the
`method:` declarations (167 literal entries) plus the generated route families: collection types ×2, task configurations ×2,
history ×2, and data jobs per object type. Paths are snake_case.

- **entities** (`crm_entities_api`)
  - `/contacts/{t}`: GET (paged; `organization_id`, `search`, `fields`), POST.
  - `/contacts/{t}/{id}`: GET, PUT, PATCH, DELETE.
  - `/contacts/{t}/{id}/history`, `/history/actors` and `/history/{revisionId}`.
  - `/contacts/{t}/descendants/{organization_id}`.
  - `/organizations/{t}`: GET, POST.
  - `/organizations/{t}/{id}`: GET, PUT, PATCH, DELETE.
  - `/organizations/{t}/{id}/affiliations`.
  - `POST /organizations/{t}/reprocess_compliance` and `POST /organizations/{t}/preview_contact_role_compliance_email`.
  - `/crm_settings/{t}`: GET, PATCH.
  - `GET /effective_definitions/{t}`.
  - `POST /email_previews/{t}`.
- **configuration-types** (`crm_configuration_types_api`)
  - `/contact_types`, `/contact_roles`, `/organization_types`, `/affiliation_types`, `/address_types` and
    `/phone_number_types`, each with the full CRUD set and, where ordered, `PUT …/order`.
- **regions** (`crm_regions_api`)
  - `/regions/{t}[/{id}]`.
  - `/region_assignments/{t}/settings`, `/rules[/{id}]`, `/preview` and `/recalculations[/{id}]`.
- **tasks** (`crm_tasks_api`)
  - `/tasks/{t}[/{id}]`, plus `/assign` and `/claim`.
  - `/task_types/{t}[/{id}|/order]` and `/task_queues/{t}[...]`.
  - `GET /offered_tasks/{t}`.
  - `POST /tasks_service/{t}`: service-only, never on API Gateway.
- **activities** (`crm_activities_api`)
  - `/activities/{t}[/{id}]`, plus `/history`.
  - `/activity_types/{t}[/{id}|/order]`.
  - `/activity_timelines/{t}/(contacts|organizations)/{id}[/summary]`.
- **duplicates** (`crm_duplicates_api`)
  - `/duplicates/{t}/rules[/{ruleId}]`, `/summary` and `/rescans`.
  - `/duplicates/{t}/{objectType}/candidates[/{id}[/dismissal]]`, `/groups[/{id}[/dismissal]]`, `/checks`,
    `/rescans[/{id}]` and `/records/{recordId}/candidates`.
- **merges** (`crm_merges_api`)
  - `/merges/{t}/plans[/{planId}[/validations|/executions]]` and `/limits`.
  - `/merges/{t}[/{mergeId}[/details]]`.
  - `/merges/{t}/aliases/{objectType}/{recordId}`.
- **data jobs**
  - `/contacts/{t}/data_jobs[...]`, including `records_by_source_key` and `{jobId}/rollback`.
  - `/organizations/{t}/data_jobs[...]`.
  - Bulk data jobs for the seven configuration types and for activities.

The deployed stage is checked against this list by `npm run api:verify`. The authorizer manifest is generated from
`aws/api/crm.routes.ts` (`crmModuleRoutes`).

### 17. Portal-facing endpoints

| Capability                                   | Evidence                                                                                                                                                                                                                                  | Status            | Gap                                                                                                                                                                                                                                                                                                                            |
| -------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| Portal routes                                | Only the contact-role reads are `RouteAccess.Portal` (`modules/entities/aws/api/contact-roles.routes.ts:76,118`). Every other route is console-only.                                                                                      | Scaffolded        | CRM 2.0 has no portal identity. Several things are deferred: a member's own profile read and write, the descendants listing, organization types, the effective layout and form (`docs/debt/portal-layout-consumption.md`), portal delegates (`docs/debt/portal-delegates.md`), and "acts for organization" rules (Jira RX-11). |
| Portal sign-up contact matching and creation | `crm.portal_contact_candidates(...)` and `crm.create_portal_contact(...)`: `SECURITY DEFINER`, granted to `rhythm_platform_app` (migration `0077`); `modules/duplicates/tests/integration/db/portal-sign-up-contacts.integration.spec.js` | Verified complete | Platform calls them. The link from a contact to a user lives in Platform and is a required merge participant.                                                                                                                                                                                                                  |

### 18. configuration-types module

`modules/configuration-types` holds only infrastructure:

- one function, `crm_configuration_types_api`;
- the routes of the six configuration types, declared in `modules/entities/aws/api/` over the entities services.

It was split out so the entities logic stack stays within its resource budget (`docs/stack-layout.md`). Its routes are
covered by `tests/unit/configuration-types-routes.spec.ts` and by the per-type e2e specs in `modules/entities/tests/e2e/`.
**Verified complete** as a packaging move. It has no behaviour of its own.

### 19. Debt register (`docs/debt/`, 30 files)

| Item                                                                      | State on `a980b553`                                                                                                                                    |
| ------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ |
| access-filter-single-id                                                   | Open. The library bug breaks `accessFilter` id sets everywhere; CRM has a local workaround.                                                            |
| communication-type-check                                                  | Open. Communication types are not ported.                                                                                                              |
| contact-roles                                                             | Partly stale. Management, assignment and the default seeds exist; the name copy is still missing.                                                      |
| contact-roles-in-history                                                  | Resolved.                                                                                                                                              |
| created-status-code                                                       | Open. The library cannot answer 201 or 202.                                                                                                            |
| custom-field-values                                                       | Open. No computed or read-only fields, no field permissions, and console and portal requiredness is not enforced.                                      |
| data-job-finished-batches                                                 | Resolved.                                                                                                                                              |
| duplicate-email-prevention                                                | Deliberately not restored. The rule itself is enforced.                                                                                                |
| eligibility-custom-field-value-type                                       | Open. The rules package needs a `dynamic` value type.                                                                                                  |
| events-and-search                                                         | **Stale.** Quick Search feeding exists; publishing is still mostly missing (§14).                                                                      |
| github-security-scanning                                                  | Open. No secret, dependency or Dependabot scanning.                                                                                                    |
| image-files-and-enrichment                                                | Images done. Enrichment (FullContact, Google Civic) is open.                                                                                           |
| master-branch-protection                                                  | Open. No ruleset on `master`.                                                                                                                          |
| merge-and-portal-users                                                    | Merge done. Portal identity is still open.                                                                                                             |
| name-parsing-and-address-normalization                                    | `country_name` is not derived. Name parsing was removed on purpose.                                                                                    |
| organization-denormalization                                              | Names are joined on read. Inline organization creation is not supported.                                                                               |
| organization-domain-name-oddities                                         | Items 2–4 remain. Item 1 was fixed (DEF-CRM-0250).                                                                                                     |
| organization-type-template-on-delete                                      | Resolved.                                                                                                                                              |
| organization-types                                                        | Portal list open.                                                                                                                                      |
| organizations-logic                                                       | Most items are carried. Batch, by-domain, public, conflicts, social and directory metrics are not.                                                     |
| platform-data-job-capacity                                                | Open. Platform's table does not exist.                                                                                                                 |
| portal-delegates / portal-descendant-contacts / portal-layout-consumption | Deferred until portal identity exists.                                                                                                                 |
| prototype-leftovers                                                       | Open. It needs a decision to drop the `audit.*` and `platform.*` objects and the empty buckets.                                                        |
| region-custom-fields                                                      | Open.                                                                                                                                                  |
| rollback-of-collections                                                   | Open.                                                                                                                                                  |
| shared-lib-check-in-ci                                                    | Resolved by the packaged libraries.                                                                                                                    |
| soc2-remediation-windows                                                  | Open, with a deadline: **from 2026-11-07, `npm run compliance:check` fails `validate`** while expired High exceptions remain, which stops every merge. |

### 20. Top 15 backend gaps and risks (ranked)

1. **Cross-service references are not merge- or delete-safe.** A contact or organization merge rewrites CRM's own
   references (`modules/merges/references/crm-references.ts`). Outside CRM, only Platform (required for contacts,
   `merge-participants.ts:91-94`) and Committees take part. Accounting, Commerce, Submissions and Membership store contact and
   organization ids with no participant and no foreign key, so a merge or a hard delete leaves them pointing at a record
   that no longer exists. The deletion-impact contract D-41 is designed (`agent/crm-acc-w3-arch-lifecycle`
   `docs/proposals/record-lifecycle/02,06`) but not built.
2. **A full save silently deletes relationships.** A PUT that omits a collection empties it (`docs/entities-api.md:275,1239`;
   DEF-CRM-0174, high, open). PATCH needs no version, and a data job's apply step does not freeze the versions it validated
   (DEF-CRM-0682 was reverted at `a980b553`). D-05, D-09 and D-37 are designed (`docs/standards/record-write-contracts.md`
   on `agent/crm-acc-w3-arch-writes`) but none is enforced, and the console caller inventory says the callers must change
   first.
3. **Lifecycle is hard delete only.** There is no archive state (D-02, D-07). Deleting a contact cascades its activity links
   (`modules/activities/db/activity-links.table.ts:55-65`), which breaks D-44. Its revisions keep the person's personal data
   for the life of the tenant (D-42, a privacy and SOC 2 gap, `TC-DAT-003` and `TC-PRV-003`).
4. **Integration Platform events are missing.** Seven of the eight catalogued CRM business events are not published
   (`aws/crm.events.ts` defines only `crm.contact.merged`). Subscribers cannot learn about contact or organization creates,
   updates, deletes or organization merges.
5. **Organizations, tasks and configuration have no change history.** Only the contact and activity streams declare
   `history` (`db/change-event-installs.ts:89,149`). The organization 360 has no Version History, and nothing audits
   changes to compliance rules, roles or types.
6. **The ids standard is broken in places.** Tasks (records) and regions (configuration) get 10-character random ids from
   `recordIdFor` (`tasks.ts:221`, `regions.ts:130`). No record table carries a `uuidV7Check`, and client and imported ids
   are accepted unchecked. CRM has not adopted the `@rhythmsoftware/macroservice-architecture` validator, whose `data.ids`
   rule was decided on 2026-10-19.
7. **There is no portal surface.** Portal users can reach only the contact-role reads (`contact-roles.routes.ts:76,118`)
   and the two sign-up SQL functions. Self-service profile, organization rosters (descendants), delegates and the portal
   layout or form are all deferred until portal identity exists.
8. **Quick Search is not proven end to end.** CRM's feeder exists and is unit-tested, but DEF-CRM-0119 (escalated) shows
   new dev records not indexed after about 17 minutes. Activities and tasks are not fed, and activity search is limited to
   one timeline by design.
9. **Bulk-change rollback is incomplete.** Collections and custom fields are `not_reversible_operation`, and organizations
   cannot be rolled back (`docs/debt/rollback-of-collections.md`; `docs/data-jobs.md:666-704`). A bad bulk update to
   custom fields or affiliations has no undo.
10. **Authorization is tier-only.** There are no record-level conditions (`contacts.routes.ts:14`), no field-level
    permissions (custom fields included), and no built-in policies (`docs/built-in-policies.md`). The library's
    `accessFilter` id-set bug is still open (`docs/debt/access-filter-single-id.md`).
11. **Custom fields are partial.** Values are stored and validated for contacts, organizations, contact roles and activities
    only. There are no computed or read-only fields. Console and portal requiredness is not enforced. Eligibility on custom
    fields is text-only, and decimals come back as strings (DEF-CRM-0029).
12. **The hierarchy cannot be queried.** Ancestors and descendants are computed in application code
    (`organizations.ts:636-720`) and are absent from `organizations_for_queries`. Segments, eligibility rules and exports
    cannot say "this organization and its children". There is also no as-of history of organization addresses and phones
    (`docs/affiliations/README.md` §19.4-5).
13. **Rhythm 1.0 features are missing with no plan on record.** Generic relationships (`rolodex-relationships`; only
    `spouse_id` exists), directories, engagement scoring and statistics, organization by-domain lookup and batch routes,
    organization e-mail addresses, a preferred organization phone, communication types (free-text `communication_type_id`),
    and enrichment (FullContact, Google Civic).
14. **Platform dependencies are unfinished.** `platform.data_job_capacity` does not exist, so each service admits bulk work
    from a static share. The compliance e-mail ends at the hand-off to Platform communications (delivery not verified in
    CRM), and the dispatcher cannot answer 201 or 202.
15. **Delivery hygiene could stop work on a known date.** `master` has no branch protection and nothing scans for secrets
    or vulnerable dependencies (`docs/debt/master-branch-protection.md`, `github-security-scanning.md`). The SOC 2 expiry
    makes CI fail from **2026-11-07** (`docs/debt/soc2-remediation-windows.md`). Several documents have drifted from the
    code (`README.md` on `.bulk`, `docs/data-jobs.md` on file imports, `docs/debt/contact-roles.md`, `events-and-search.md`,
    the activities status line "awaiting review"). Andrew's local `services-crm` checkout is 102 commits behind
    `origin/master`.

## 23. e04 — CRM console assessment (react-console)

## e04 — CRM console assessment (react-console)

Written 2026-10-19 (about 16:50 ET) for the CRM architecture gap analysis.

### Sources and how complete this is

- **Snapshot:** react-console `origin/master` at **`1f345862b`** (2026-10-19 16:25 ET, "CRM acceptance repair wave 3
  (console)"). It was extracted with `git archive` to a scratch directory. The local `master` checkout is 2,107
  commits behind and was not used.
- **Since the snapshot:** after `git fetch`, `origin/master` is `f08b10233` (16:42 ET). The commits between the two
  that touch `components lib app` are `87b1f15b3` (merge), `7611563e0`, `d8d2f74a2`, `bd7a07086`, `422690321` and
  `1edeb01f7`. **All of them are Accounting repairs (DEF-ACT-\*). None touches a CRM screen.** The snapshot therefore
  already includes repair wave 3 (`1f345862b`). That commit touched the Contact and Organization 360s, the action
  toolbar, the Log Activity sheet, the Activities explorer, the record-search Ask, the contact editor, the
  contact-type and contact-role configuration, the data-job wizard, the merge workspace, the login screen and the
  tasks page.
- **Screen audits:** four read-only audits ran in parallel (CRM records; activities/tasks/merge; configuration;
  explorer/data-ops/portal). **I could not read any of their results.** They finished after I had to write this
  file, and their reports went to the coordinator instead of me. The coordinator is merging them. The table in §1
  is built from my own reads of the snapshot, marked **[verified]**, and from the acceptance product-quality review
  (`acceptance/crm/reports/product-quality-review.md` §2), marked **[PQR]**. The PQR was written 2026-10-17 against
  console `68dbc31f`, before repair waves 1–3, so its rows may already be fixed. Re-check each [PQR] row against the
  audit results.
- **Acceptance material** (snapshot copy, `acceptance/crm/`): `reports/release-readiness.md`,
  `reports/wave3-directives.md`, `reports/wave3-plan.md`, `reports/decision-register.json` (updated 17:44 UTC, so
  before the wave-3 console landing), `reports/defects/register.json`, `reports/repairs.json`,
  `reports/product-quality-review.md`, `inventory/`, `waves.json`.

#### Live/mock mechanism (the console has two switches)

1. `NEXT_PUBLIC_MOCK_MODE` / `NEXT_PUBLIC_MOCK_PREVIEW` (`lib/config/mock-mode.ts`) turns on MSW fixtures from
   `mocks/`. It is refused on production deployments (D-06). `pnpm dev` defaults to mock mode; `pnpm dev:live` forces
   live (`scripts/dev.mjs`).
2. `NEXT_PUBLIC_CRM_API === "live"` → `CRM_LIVE` (`lib/api/crm.ts:128`). The Contact 360 and Organization 360 pages
   render a browser-side live component when it is set. **Without it they render built-in sample data**
   (`lib/contact-360.ts`, `lib/contacts/store`), not MSW data (`app/(console)/console/[tenantId]/crm/contacts/[id]/page.tsx`).
   The flag is still read in `lib/record-context/contact.crm.ts`, `organization.crm.ts`,
   `lib/contacts/record-operations.ts`, `lib/affiliations/use-organization-affiliations.ts`,
   `lib/duplicates/crm-adapter.ts`, `lib/quick-search/destinations.ts` and
   `components/console/order/order-360-live.tsx`. Andrew's "one live switch" rule (2026-10-19) is in the acceptance
   runbook (`e566f91fa`), but the code still has the per-application switch. The half-live risk is DEF-CRM-0034.

### 1. Screen inventory

Paths are relative to `app/(console)/console/[tenantId]/`. "Live" means a real CRM or Platform API is used when the
flags are live.

| Screen                     | Route                                                                             | File                                                                                                    | Live / mock                                                                                            | Works                                                                                                                                                               | Missing / inert                                                                                                                                                                                                                                                                                                                                                                                              |
| -------------------------- | --------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| Contacts explorer          | `/crm/contacts`                                                                   | `crm/contacts/page.tsx`, `components/console/contacts/contacts-definition.tsx`                          | Live CRM (Platform query layer); MSW in mock                                                           | List, filters, Ask box (rule parser), saved views, selection and bulk bar [verified, partial]                                                                       | Columns Owner, Membership, Conference and Org Membership show "isn't available yet" (`contacts-definition.tsx:30-33`). Ask phrases the parser can't read are refused (`record-search.tsx:57`). AI Generate only with `NEXT_PUBLIC_QUERY_GENERATION` live. Assign Owner disabled [PQR]. Quick-create sheet wrote only to the console store [PQR; re-check]                                                    |
| New contact                | `/crm/contacts/new`                                                               | `crm/contacts/new/page.tsx`, `components/contact-editor/*`                                              | Live CRM                                                                                               | Create, with a duplicate hint                                                                                                                                       | No "Create anyway" for a block rule (DEF-CRM-0246, D-32). Typed address/phone lists were hard-coded (PQR Q8; re-check)                                                                                                                                                                                                                                                                                       |
| Contact 360                | `/crm/contacts/[id]`                                                              | page + `lib/record-views/views/contact-360.json`, `components/console/contact/contact-360-live.tsx`     | Live CRM when `CRM_LIVE`; otherwise sample data, with a `SampleRecordMissing` notice (D-06) [verified] | Metadata-driven cards, timeline, Change Type, version history. Wave 3 added restrictions/deceased header, phones and created/modified (D-01, D-23) — needs re-check | Archive is hidden on live views (no handler; waits for lifecycle contract B) (`action-toolbar.tsx:62-74`) [verified]. No Delete (DEF-CRM-0710). Many application tabs (Membership, Events, Donations, …) have no dataset (`contact.crm.ts`) [PQR]. Engagement score is fixture data [PQR]. Contact at the documented limits gives a 500 (DEF-CRM-0031). Merge from the record needs a picker (D-03, batch 2) |
| Contact edit               | `/crm/contacts/[id]/edit`                                                         | `components/contact-editor/contact-editor.tsx`                                                          | Live CRM                                                                                               | Full form: emails, phones, addresses, affiliations, custom fields. Rich-text fields via D-04 (`3ffae58f6`). Draft restore (D-17)                                    | Spouse picker was sample-backed (DEF-CRM-0026, open). Decimal custom-field display (DEF-CRM-0029). Merged-away save leaves a raw "Gone" toast (DEF-CRM-0737/0028; D-20 batch 2)                                                                                                                                                                                                                              |
| Contact orders             | `/crm/contacts/[id]/orders/new`, `/[orderId]`                                     | `components/console/order/*`                                                                            | Prototype with sample catalogs                                                                         | —                                                                                                                                                                   | D-53: hide on live consoles. Wave 3 changed the page (+18 lines); verify it is gated                                                                                                                                                                                                                                                                                                                         |
| Organizations explorer     | `/crm/organizations`                                                              | `components/console/organizations/organizations-definition.tsx`                                         | Live CRM                                                                                               | List, filters, views                                                                                                                                                | Membership, Tier, Owner, Plan and Region columns show "isn't available yet" (`:46-50`) [verified]                                                                                                                                                                                                                                                                                                            |
| New organization           | `/crm/organizations/new`                                                          | `crm/organizations/new/page.tsx`                                                                        | Live CRM                                                                                               | Create; parent and region pickers repaired (DEF-CRM-0100/0101)                                                                                                      | No address or phone editor (DEF-CRM-0314, D-36)                                                                                                                                                                                                                                                                                                                                                              |
| Organization 360           | `/crm/organizations/[id]`                                                         | `components/console/organization/*` (hand-built, not metadata-driven)                                   | Live CRM when `CRM_LIVE`                                                                               | Overview, Profile card (DEF-CRM-0106), Contacts tab with Current/Past (D-07 part), Hierarchy                                                                        | Inert toolbar actions are hidden on live views (D-02), which leaves no Edit Organization form. 12 tabs are "not available yet" (`organization.crm.ts:77`). Hierarchy scans at most 500 organizations (DEF-CRM-0117). People count stops at 100 (DEF-CRM-0760). Add Person can't set a role (DEF-CRM-0187). No organization change history (CAP-AUD-002 missing)                                              |
| Activities                 | `/crm/activities`, `/crm/activities/[id]`                                         | `components/console/activities/activities-explorer.tsx`, `components/activities/log-activity-sheet.tsx` | Live CRM / Platform dataset (D-49, wave 3)                                                             | Activities explorer and version history new in wave 3. Log/Edit activity                                                                                            | Attachments disabled (`log-activity-sheet.tsx:79`) [verified]. Make Primary (D-50) and optional owner (D-51) in wave 3 — needs re-check                                                                                                                                                                                                                                                                      |
| Tasks                      | `/tasks`                                                                          | `tasks/page.tsx`, `lib/api/tasks.ts`                                                                    | Live CRM                                                                                               | My Open Tasks, saved views, complete/reopen/cancel                                                                                                                  | Version on state change (D-52) needs console + service enforcement. No assignment notifications or reminders (CAP-ACT-020 missing). Queue routing controls disabled [PQR]                                                                                                                                                                                                                                    |
| Data Quality               | `/crm/dataQuality`, `/dismissed`, `/duplicates/[objectType]`, `/groups/[groupId]` | `components/duplicates/*`                                                                               | Live CRM                                                                                               | Review queue, dismiss, merge from a group                                                                                                                           | "Merging … isn't available yet" for object types without merge (`overview.tsx:50`). No rescan, plans list or low-band queue screen [PQR]. Manual "these are the same" exists only on design previews (`/design/dataQuality/*`) [PQR]                                                                                                                                                                         |
| Merge history / details    | `/crm/dataQuality/merges`, `/[mergeId]`                                           | same                                                                                                    | Live CRM                                                                                               | List, summary                                                                                                                                                       | No field-level outcome (DEF-CRM-0231, D-30). Only the first page of history is readable (DEF-CRM-0257)                                                                                                                                                                                                                                                                                                       |
| Merge workspace            | `/crm/merges/new`, `/crm/merges/plans/[planId]`                                   | `components/duplicates/merge/merge-workspace.tsx`                                                       | Live CRM                                                                                               | Preview, approve, execute                                                                                                                                           | No second-record picker (DEF-CRM-0006, D-03). Approval fingerprint lacks child ids/versions (D-31/33, service)                                                                                                                                                                                                                                                                                               |
| Types & roles admin        | `/configuration/crm/types/[collection]`                                           | `components/configuration/resources/crm.tsx`, `contact-types.tsx`, `contact-roles.tsx`                  | Live CRM                                                                                               | CRUD for contact, organization, affiliation, address and phone types, roles and regions. Descriptions (D-11) added in wave 3                                        | Address/phone types can't be deactivated (client refuses) [PQR]. Some resources stay disabled with "isn't available yet" (`crm.tsx:43`) [verified]. Locked seeds can't be unlocked (D-08)                                                                                                                                                                                                                    |
| Custom fields admin        | `/configuration/[module]/fields/...`                                              | `lib/configuration/fields/adapter.ts`                                                                   | **Mock: seed adapter** (`adapter.ts:5,29` → `seedFields()`) [verified]                                 | Browsing and drafting in memory                                                                                                                                     | Never calls Platform `/metadata`. Invented record types (`record-types.ts:349`). No real field definitions (CAP-CFG-013 partial)                                                                                                                                                                                                                                                                             |
| Record layouts             | `/configuration/[module]/recordLayouts/[recordType]/designer`                     | `components/layout-designer/*`                                                                          | **Browser only**                                                                                       | Design                                                                                                                                                              | Save Draft stores locally: "Stored in this browser." (`designer-toolbar.tsx:140`) [verified]. Not wired to Platform layout definitions (CAP-CFG-012)                                                                                                                                                                                                                                                         |
| Forms                      | `/configuration/[module]/forms/console/contacts`, `/forms/portal/contacts`        | `app/.../forms/*`                                                                                       | Console forms partly; portal inert by design [PQR]                                                     | —                                                                                                                                                                   | No organization console form. Portal forms are inert                                                                                                                                                                                                                                                                                                                                                         |
| Compliance + email preview | (organization type editor)                                                        | `components/configuration/resources/*`                                                                  | API exists                                                                                             | Rules on organization types                                                                                                                                         | Reprocess, test-send and outcome are API-only [PQR §2, Q6]. Type change doesn't say compliance is unchanged (DEF-CRM-0111)                                                                                                                                                                                                                                                                                   |
| Duplicate rules            | `/maintenance/[area]`                                                             | —                                                                                                       | Live                                                                                                   | Toggle                                                                                                                                                              | Add/Delete rule not available [PQR]                                                                                                                                                                                                                                                                                                                                                                          |
| Queries                    | `/queries`, `/new`, `/[id]`, `/schedules/*`                                       | `lib/api/queries.ts`, `saved-queries.ts`, `query-recipes.ts`                                            | Live Platform query layer                                                                              | Build from scratch, Browse Package Queries (recipes), save/share. NOT groups (D-24) and AI Ask edit-with-undo (D-27) in wave 3                                      | Describe With AI needs the generation flag. Export Results and Schedule had stub paths [PQR; re-check]. "A record screen for … isn't available yet" from the record drawer (`record-drawer.tsx:90`) [verified]. Generated SQL / Act on records inert [PQR]                                                                                                                                                   |
| Data jobs                  | `/dataManagement/dataJobs`, `/new`, `/[jobId]`                                    | `components/data-management/data-jobs/*`                                                                | Live Platform data-jobs                                                                                | Import, bulk update/delete, impact summary, rejections. Mapping changes (D-40) in wave 3                                                                            | History filters not available (`data-jobs-toolbar.tsx:33`). Rollback permission and Retry (D-39, DEF-CRM-0653/0654) in wave 3 — re-check. Deletion-impact preview (D-41) waits for the lifecycle contract                                                                                                                                                                                                    |
| Data exports               | `/dataManagement/dataExports`, `/dataExportSettings`                              | `components/data-management/data-exports/*`                                                             | Platform (tenant data export, parked)                                                                  | —                                                                                                                                                                   | Several "isn't available yet" placeholders (export-intro, detail tabs)                                                                                                                                                                                                                                                                                                                                       |
| Global search              | `/search`                                                                         | `search/page.tsx`                                                                                       | **Sample only:** filters `lib/console-data` arrays (`:8,23-32`) [verified]                             | —                                                                                                                                                                   | Never reaches CRM                                                                                                                                                                                                                                                                                                                                                                                            |
| Quick Search (⌘K)          | shell                                                                             | `lib/quick-search/destinations.ts`                                                                      | Live search index; result destination depends on `CRM_LIVE`                                            | Type-ahead                                                                                                                                                          | "This record view isn't available yet." when not live (`:35-50`). Indexing quota problems (DEF-CRM-0417/0421/0612)                                                                                                                                                                                                                                                                                           |
| Extensions on CRM records  | `/configuration/platform/extensions/*`                                            | `lib/extensions`                                                                                        | Platform                                                                                               | Extension points, events, handlers admin                                                                                                                            | Other applications' 360 tabs (Membership, Accounting, …) are not supplied (CAP-XAP-001/003/011)                                                                                                                                                                                                                                                                                                              |
| Effective permissions      | `/security/users/[userId]`                                                        | —                                                                                                       | Live Platform                                                                                          | Profile, Roles, History                                                                                                                                             | No effective-permissions view (DEF-CRM-0530, D-15, batch 2)                                                                                                                                                                                                                                                                                                                                                  |
| Portal                     | `app/(portal)/portal/[tenantId]/*`                                                | invoices, payments, payment methods, statement                                                          | Accounting                                                                                             | —                                                                                                                                                                   | **No CRM feature in the portal**: no profile, contact-information or organization pages. Portal was out of acceptance scope                                                                                                                                                                                                                                                                                  |

### 2. Acceptance summary

#### Results and verdict

The release-candidate cycle (2026-10-18/19, before waves 2–3) gave this result:

| Journeys | Passed | Failed | Blocked |
| -------- | ------ | ------ | ------- |
| All 617  | 212    | 371    | 34      |
| P0 (262) | 86     | 165    | 11      |

The verdict in `release-readiness.md` is **"Not ready for a customer."** The gate in `waves.json` is "not assessed".
A clean release-candidate run (W-RC) on `acme-acceptance-main` is planned after waves 2–3 and the batch-2 deploy.

The inventory has 194 capabilities: 120 implemented, 63 partial, 8 missing, 3 out of scope. The 8 missing are
AUD-002 (organization history), ACT-019 (Organization 360 Activity tab), ACT-020 (task notifications), XAP-002
(Commerce orders from a contact), XAP-003 (Membership on the 360), XAP-010 (Alerts/Integrations), XAP-011 (360
tabs for applications that don't exist yet) and DAT-015 (duplicate detection during imports).

#### Defect counts

The register has 357 defects. Severity: 4 critical, 82 high, 168 medium, 103 low. Status from `repairs.json`:
157 fixed, 75 escalated (waiting on rulings, now ruled), 12 reclassified, 113 open.

Open plus escalated, by area:

| Area              | Open + escalated | High   |
| ----------------- | ---------------- | ------ |
| data-operations   | 25               | 7      |
| cross-application | 24               | 3      |
| explorer          | 22               | 6      |
| queries           | 20               | 2      |
| contacts          | 17               | 4      |
| adversarial       | 17               | 2      |
| merging           | 13               | 2      |
| auditing          | 12               | 1      |
| organizations     | 11               | 3      |
| security          | 11               | 2      |
| configuration     | 10               | 2      |
| affiliations      | 6                | 1      |
| **Total**         | **188**          | **35** |

Caveat: the register predates the wave-3 console landing (`1f345862b`) and the wave-2 integration, so some of
these are fixed but not yet recorded.

Fixes by repository: react-console 89, services-crm 57, services-platform 11.

Wave status:

- **Wave 2:** integrating.
- **Wave 3:** console batch 1 landed. The rest is "in_progress" (51) or "approved" (2) in the decision register.
- **Batch 2, waiting on its inputs:** ui-merge, ui-locks, ui-access, lifecycle, crm-dataops-adoption, and D-05/D-52
  enforcement.

#### Andrew's rulings that bind CRM behaviour (wave3-directives.md, 2026-10-19)

- **D-01** Contact 360 shows restrictions, deceased/inactive, spouse, region and all typed phones via metadata cards.
- **D-02** Every visible action has a real effect; hide inert ones; Archive is never a hard delete.
- **D-03** Merge from the 360 with a second-record picker.
- **D-04** Keep the rich-text field type; build a sanitized editor and renderer.
- **D-05** (P0) Version required on field-changing writes: 400 if missing, 409 if stale; a no-op write creates no new version.
- **D-06** Keep the unified live/mock switch; explain sample-missing records; no mock mode in production.
- **D-07** Archive organizations; Current/Past/All affiliations; keep identity references.
- **D-08** (P0) Audited, narrowly authorized unlock; protect system seeds; repair locked dev records.
- **D-09** (P0) An omitted affiliations collection means unchanged; only explicit mutations change it.
- **D-10** Separate Upcoming, Current and Past affiliations using workspace dates.
- **D-11** Role and type descriptions are visible and editable.
- **D-12** The workspace timezone sets business dates.
- **D-13** (P0) Customer-scoped grants are resolved in the authorizer, not expanded into the token.
- **D-14** (P0) Power Users cannot approve staff access; enforced at the endpoint.
- **D-15** Effective Permissions view, separate from application entitlement.
- **D-16** Hide Microsoft sign-in until it works.
- **D-17** Silent refresh plus a dirty-draft restore that checks the version.
- **D-18** Allow empty placeholder roles, clearly labelled.
- **D-19** Spouse is reciprocal; target is one canonical relationship.
- **D-20** Merged-away records: API answers 410 with the survivor; the console navigates to the survivor.
- **D-21** Read-only users can see merge history.
- **D-22** A real edit keeps the row id; a replacement is remove + add.
- **D-23** Created/modified attribution on the 360.
- **D-24** NOT groups in the criteria builder, round-tripping the query structure.
- **D-25** Keep a linked address whose source was removed, with provenance and a disconnected status (catalog correction).
- **D-26** User timezone for timestamps.
- **D-27** AI Ask edits the existing query intelligently, with a diff and undo.
- **D-28** New-tab trail as built (catalog correction).
- **D-29** (P0) Search deletes use durable deletion/version checks; isolate one tenant's deletes from others.
- **D-30** Field-level merge outcome in Merge Details.
- **D-31** (P0) Merge approval fingerprint includes child ids and versions; reapprove after changes.
- **D-32** A block rule blocks; an authorized, audited "Create anyway" override.
- **D-33** (P0) A source-value change invalidates the approved merge.
- **D-34** Dismissing an already-dismissed pair is idempotent, with a helpful message.
- **D-35** (P0) Keep Editing after a conflict never advances the version.
- **D-36** Organization address and phone editors on the 360 and form.
- **D-37** (P0) Bulk update captures and verifies versions; skips and reports conflicts.
- **D-38** (P0) Patch imports respect source revision and target version.
- **D-39** Rollback and Retry wired, with permissions.
- **D-40** Map spreadsheet email/phone/address columns, with a typed-mapper architecture.
- **D-41** (P0) Cross-module deletion-impact contract and preview.
- **D-42** Read-only deletion audit view.
- **D-43** (P0) Client UUIDv7 on create; re-read after an ambiguous response.
- **D-44** (P0) Never reassign or delete orphaned activities; use retention-aware tombstones.
- **D-45** Merge pointer folded into the unified merge history.
- **D-46** Roles follow effective affiliations; reconcile at date boundaries.
- **D-47** Retire only duplicate pairs with no remaining evidence.
- **D-48** Versioned baseline configuration reconciled into existing workspaces.
- **D-49** Activities Explorer and activity version history.
- **D-50** Make Primary on activity links.
- **D-51** Activity owner is optional.
- **D-52** (P0) Task Complete/Reopen/Cancel require a version.
- **D-53** Hide the order prototype on live consoles.

#### Capabilities acceptance found broken or missing (customer-impacting)

- **Security:** query permission ignored deny rules (DEF-CRM-0550/0758/0404; repaired, W-SEC landed). Grant-slice
  lockout at 14 workspaces (DEF-CRM-0558). Power User staff keys (DEF-CRM-0539). Bulk Actions shown to an
  administrator denied the bulk key (DEF-CRM-0688).
- **Data integrity:** a bulk delete removed 4 records against 3 approved (DEF-CRM-0663). Re-delivery overrode a staff
  edit (DEF-CRM-0655). A stale shared-query save overwrote silently (DEF-CRM-0458). Merges emit no record.merged
  event (DEF-CRM-0231/0743).
- **Unrecoverable locks:** DEF-CRM-0118/0321.
- **Capacity:** a 360 at the documented limits fails (0031); people count capped at 100 (0760); only the first page
  of merge history (0257); hierarchy capped at 500 (0117).
- **Search indexing:** 0417, 0421, 0612.
- **Missing console capability:** contact/organization delete and archive; organization edit form; organization
  address/phone editors; effective permissions; field-level merge outcome; Create-anyway; manual duplicate; rescan;
  deleted-contact history (0683); CSV collection mapping (0660); rollback and retry (0653/0654); compliance
  reprocess; custom-field definitions against Platform metadata; layouts persisted to Platform.

### 3. Top 15 console gaps (ranked)

1. **No record lifecycle in the console.** There is no Delete, Archive or deletion-impact preview for contacts or
   organizations. Archive is hidden because it has no handler (`action-toolbar.tsx:62-74`); delete is API or bulk
   only. Rulings D-02, D-07, D-41, D-42, D-44; blocked on lifecycle contract B.
2. **Custom fields admin is a mock.** It is a seed adapter (`lib/configuration/fields/adapter.ts:5,29`) with
   invented record types (`record-types.ts:349`) and never calls Platform metadata. CAP-CFG-013.
3. **Record layouts are not persisted.** The designer saves to the browser only (`designer-toolbar.tsx:140`), so the
   metadata-driven 360 cannot be configured per tenant. CAP-CFG-012/010.
4. **The Organization 360 is hand-built and thin.** No edit form behind it; 12 tabs not available
   (`organization.crm.ts:77`); 500-organization hierarchy cap (DEF-CRM-0117); 100-person count (0760); no change
   history (CAP-AUD-002). D-02, D-36.
5. **Two live switches.** `CRM_LIVE` (`lib/api/crm.ts:128`) beside `MOCK_MODE` keeps sample-data fallbacks in the
   360 pages, Quick Search and duplicates. This conflicts with the one-switch rule; DEF-CRM-0034.
6. **Concurrency, retry and version enforcement.** D-05, D-35, D-43, D-52 are partly console (wave 3) and partly
   service. Enforcement is held until the console callers land.
7. **The merge experience is incomplete.** No second-record picker (D-03/DEF-CRM-0006); no field-level outcome
   (D-30/0231); no Create-anyway (D-32/0246); no rescan, manual-duplicate or plan-list screens (PQR); paging of merge
   history (0257); the merged-away editor dead end (D-20/0737).
8. **Data operations safety.** Rollback and Retry (D-39, 0653/0654); bulk version capture (D-37); patch ordering
   (D-38); the 4-for-3 delete (0663); collection mapping (D-40/0660).
9. **Effective Permissions screen is absent.** D-15/DEF-CRM-0530 (batch 2). Bulk Actions are not gated by key
   (0688).
10. **Global search is sample-only.** `/search` filters `lib/console-data` arrays (`search/page.tsx:8,23-32`).
    Quick Search depends on `CRM_LIVE` for its destinations (`destinations.ts:35-50`).
11. **Cross-application tabs and columns are placeholders.** Membership, Accounting, Events, Orders and others on
    the Contact 360; Owner, Membership, Tier and Plan columns are "not available yet"
    (`contacts-definition.tsx:30-33`, `organizations-definition.tsx:46-50`). CAP-XAP-001/002/003/011; the order
    prototype must stay hidden (D-53).
12. **Compliance administration is API-only.** No reprocess action, outcome screen or test send; a type change
    gives no hint (DEF-CRM-0111, PQR Q6).
13. **Queries surface stubs.** Record drawer "record screen isn't available yet" (`record-drawer.tsx:90`); Export
    and Schedule paths need re-checking (PQR); Generated SQL / Act on records are inert; AI depends on the
    generation flag and the AI allowance.
14. **Locks and seeds.** No unlock UI (D-08); address/phone types can't be deactivated (PQR); some CRM
    configuration resources are disabled (`resources/crm.tsx:43`); duplicate rules can't be added or deleted.
15. **No CRM in the portal.** No self-service profile, contact information or organization pages under
    `app/(portal)`. Activities lack attachments (`log-activity-sheet.tsx:79`) and task notifications
    (CAP-ACT-020).

---

### Addendum (merged by the architecture author, 2026-10-19 evening): the four screen audits

Four screen audits ran against the same snapshot (react-console `1f345862b`, which already contains repair wave 3; no
later commit touches a CRM screen as of `f08b10233`). Their findings were delivered to the architecture author after the
file above was written, and are summarised here with their citations. Paths are relative to react-console; `[t]` =
`app/(console)/console/[tenantId]`. Where a row above (marked [PQR]) disagrees, the audit below is the newer evidence.

#### A. Records (contacts, organizations, 360s, editors, search)

- Every action without an `href` or `panel` only shows a toast and is removed on a live console
  (`components/console/record/action-toolbar.tsx:44-45,56-61,73-74,93`; `components/record-views/record-view.tsx:124-141`).
- Contacts and Organizations lists always use Platform's query layer (recipes `crm.contacts.with-organizations`,
  `crm.organizations.with-types`), not the `CRM_LIVE` switch. Page sizes 10/25/50/100; selection and export cap 10,000
  (`lib/record-explorer/recipe-adapter.ts:34-35`).
- New/Edit Contact: live save is `POST`/`PATCH /contacts/{t}` with a client UUIDv7 (`lib/contacts/record-operations.ts:43-51`).
  **Sample reference data is still read when live:** communication types (`components/contact-editor/defaults.ts:15-22`,
  `components/forms/contacts/collection-components.tsx:47,202`), custom-field defaults (`defaults.ts:4-12`), the
  duplicate-e-mail warning (searches sample contacts, `collections/email-list-editor.tsx:8,40`), and the primary
  organization summary and linked work address (`contact-editor.tsx:396-397`, `logic.ts:20,38`).
- Organization picker reads up to 2,000 organizations into the browser (`lib/record-pickers/crm-adapter.ts:164`); generic
  picker sources still point at the sample directory routes (`lib/record-pickers/sources.ts:28-30,160-161`).
- Contact 360: live overview, duplicates card, merged banner, activity timeline, record details, Accounting tab. **15 tabs
  say "not available yet"** when live (membership, chapters, committees, volunteering, engagement, events, certifications,
  education, credits, invoices, donations, communications, notes, documents, relationships; `lib/record-context/contact.crm.ts:112-128`).
  Toast-only (hidden live): Send email, Print profile, **Archive contact** (`lib/record-views/views/contact-360.json:103,130,165`).
  **No Delete anywhere** (`deleteContact` in `lib/api/crm.ts` is never called).
- Version history adapters exist for contacts and activities only (`lib/record-history/adapters.ts:9-13`).
- New Order from a contact exists only in mock mode (`orders/new/page.tsx:33-43`).
- New Organization works live (`lib/organizations/create.ts:60`). **There is no organization edit route or form**;
  "Edit Organization" is toast-only (`components/console/organization/organization-sections.tsx:72-80`).
- Organization 360 is bespoke (not metadata-driven) and live on four tabs (Overview, Contacts, Hierarchy, Activity;
  `lib/record-context/organization.crm.ts:78`). Hierarchy and affiliation lists are found by scanning at most 500
  organizations (`:72,512-516`). Archive Organization, Add Contact, Assign Role, Email All Contacts, Export CSV, Add
  Subsidiary, Set Parent, Remove from Hierarchy are toast-only (`organization-sections.tsx:84-120`). No Accounting tab.
- Quick Search (⌘K): Platform issues an Algolia key, the browser queries Algolia; contacts and organizations only;
  a result opens only with `CRM_LIVE` on (`lib/quick-search/destinations.ts:49-56`).
- **The `/search` page is always sample data** (`search/page.tsx:8,22-31`) and is linked from the nav (`components/console/nav-config.ts:528`).
- Two live switches remain: `NEXT_PUBLIC_CRM_API` (`lib/api/crm.ts:120`) and `NEXT_PUBLIC_COMMERCE_API`.

#### B. Configuration screens

- **Live and solid:** contact, organization, affiliation, address, phone, activity and task types; contact roles; task
  queue basics; regions with assignment rules and recalculation; organization-type compliance rules with the CRM-rendered
  e-mail preview (`lib/email-designer/preview.ts:51-67`); console contact forms (Platform `/console_forms`). Saves are full
  replaces guarded by `sysVersion`.
- **Sample data in every environment:** the custom-fields admin (`lib/configuration/fields/adapter.ts:11-13,113`; record
  types hard-coded `record-types.ts:349`; nothing persists; the Platform metadata API in `lib/metadata/client.ts:23` is
  not used); the Record Layouts list (`components/configuration/resources/presentation.tsx:43-53`); the layout designer
  (saves to `localStorage`, `lib/layout-designer/persistence.ts:30-33`, contacts only, nothing reads a published layout);
  Engagement Scoring (`resources/crm.tsx:228-269`, claims "scores recalculate overnight"); CRM Processes
  (`resources/workflows.tsx:70`).
- The Text (HTML) custom-field type has no settings and an `execCommand` demo preview (`field-types.tsx:217-224`,
  `previews.tsx:90-95`), not the shared sanitised editor (`components/rich-text/rich-text-editor.tsx`).
- Duplicate rules: edit, turn off, reorder; **no create, no delete** (`lib/configuration/duplicate-rules.ts:67-72`).
- Task queues: Team, Assigned By, Due In and Escalate are disabled "isn't available yet" (`resources/crm.tsx:104-136`).
- Missing configuration collections: e-mail address types, communication types (`components/configuration/catalog.ts:18-63`).
- Extensions screens are mock-only and define no CRM extension points or events (`extensions/layout.tsx:12-19`).
- Portal forms: an empty-state screen (`components/forms/management/portal-forms.tsx:29-34`).

#### C. Activities, tasks, duplicates and merge

- Activities list (recipe `crm.activities.all`): views, filters, export; **no row actions, bulk actions or Log Activity**
  (`components/console/activities/activities-definition.tsx:52`). Activities can be logged only from a record timeline.
- Log/Edit Activity sheet: links with Make Primary, optional owner, client ids, verify after a lost answer. The owner list
  is the first 100 users only (`components/activities/use-activities.ts:48`).
- Tasks: nine standard views with **My Open Tasks** as default plus **one view per active queue**
  (`components/console/tasks/tasks-definition.tsx:44-65,103`); complete, reopen and cancel send `sysVersion`
  (`lib/api/tasks.ts:117,205-207`); **claim and release send no version**; `deleteTask` is never called.
- Data Quality: overview counts, queue (20 groups a page), dismissed list, recently merged, merge details, merge workspace.
  **No rescan button**; **no record picker** to start or extend a merge (`new-merge-screen.tsx:79-90`,
  `merge-workspace-screen.tsx:24-27`); **merge details show four totals only, no field-level outcome**
  (`history/merge-details.tsx:34-41`); **"Create anyway" after a blocking rule is not built** (`lib/duplicates/checks.ts:13-16`
  unused); organizations get no duplicate check on create.
- My Responsibilities, Responsibilities settings and Needs Your Attention are live Platform screens; Attention Settings is
  a disabled button.

#### D. Explorer, queries, data jobs, portal

- Explorer filters are flat AND chips; grouped AND/OR/NOT exists only in the Queries builder
  (`components/queries/criteria-group.tsx:60,314-318`). AI generation needs `NEXT_PUBLIC_QUERY_GENERATION=live`
  (`lib/api/generate-query.ts:90-92`), otherwise a rule-based fallback.
- Contacts columns Membership Status and Owner, Organizations columns Membership, Tier, Account Owner, Member Since, Region
  are always empty; "Assign Owner" bulk action is inert (`contacts-actions.tsx:27-49`, `contacts-definition.tsx:99-106`,
  `organizations-definition.tsx:172-182`).
- Saved-query bulk actions (Add to List, Delete, tags, e-mail, owner, status) throw "not available yet" live
  (`lib/queries/platform/client-live.ts:637`). Only the data-job Update/Delete handoff works.
- Mock-mode Queries use Next API routes over an in-memory store, not MSW (`lib/queries/client.ts:33-35,71`).
- Data jobs: contacts insert/upsert/update and delete by selection (≤ 500); organizations insert/upsert/update; lookup
  types import. E-mail/phone/address columns map **only on contact insert/upsert** (`lib/api/data-jobs/collection-targets.ts:84,195-196`).
  No affiliation, task or activity object type in the wizard (`object-types.ts:43-83`). Object-type filter on the job list
  disabled. File-lane jobs cannot be dry-run.
- Data exports (whole tenant) live; Reports catalogue has no CRM reports.
- **The portal has no CRM pages** (Accounting self-service only; `lib/portal/member.tsx:94`).

## 24. E05. Platform capabilities CRM depends on, and every other application's references to CRM records

## E05. Platform capabilities CRM depends on, and every other application's references to CRM records

Evidence for the CRM architecture. Read-only investigation, 2026-10-19 (ET). Nothing was changed in any repository.

**Repositories read, at the commit each local checkout held:**

| Repository                                                  | Commit                                                                         | Branch                                                                                                                          |
| ----------------------------------------------------------- | ------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------- |
| services-platform                                           | `339fb04f1`                                                                    | master (plus branch `origin/agent/crm-acc-w3-arch-lifecycle` `6873e3b6c` and `origin/sandbox-architecture`, both NOT on master) |
| services-crm                                                | `eac3df30`                                                                     | master                                                                                                                          |
| services-accounting (`services-finance` is a symlink to it) | `43aba578` (2026-10-18; the lifecycle proposal below read a newer `cd3658f16`) | master                                                                                                                          |
| services-commerce                                           | `bd62910a`                                                                     | master                                                                                                                          |
| services-committees                                         | `bd2718a`                                                                      | master                                                                                                                          |
| services-membership                                         | `fcbddf2`                                                                      | master                                                                                                                          |
| services-submissions                                        | `75ae48b`                                                                      | master                                                                                                                          |
| services-websites                                           | `f038650`                                                                      | `ws-install-content-workflows`                                                                                                  |
| macroservice-libraries                                      | `91e6562`                                                                      | master                                                                                                                          |

Labels: **Built** = on master in code; **Partial** = foundation on master, named pieces missing; **Designed** = a written design,
nothing built; **Branch** = a design that exists only on an unmerged branch. Paths are relative to the repository named at the
start of the row or section; `P:` = services-platform, `C:` = services-crm, `L:` = macroservice-libraries `packages/`.

---

### Part A. Platform capabilities CRM depends on

#### A.0 Summary table

| #   | Capability                                                          | Status                                                             | Lives in                                                                                                    | CRM today                                                                                    | What CRM must do                                                                                                                              |
| --- | ------------------------------------------------------------------- | ------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- |
| 1   | Semantic query layer (datasets, describe, compile, run)             | Built                                                              | `P:modules/queries`, `L:metadata`                                                                           | Registers `crm.contacts`, `crm.organizations` and more via `queries:register`                | Keep every new table/field declared beside its metadata; grant `rhythm_query_reader` on what other apps' views read                           |
| 2   | Saved queries, recipes, reports, exports, schedules                 | Built                                                              | `P:modules/queries`, `L:query-recipes`                                                                      | 3 recipes (`contacts-with-organizations`, `organizations-with-types`, `tasks-all`)           | Ship a recipe per list screen; saved views anchor to recipes                                                                                  |
| 3   | Explorer (saved views, navigation sessions, select-all)             | Built                                                              | `P:modules/explorer`, `P:docs/explorer.md`                                                                  | Contacts/Organizations Explorer uses it                                                      | Nothing new beyond recipes; views hold no record data                                                                                         |
| 4   | Entity-aware queries (record links) incl. polymorphic slots         | Built (phases 1-4, 6; polymorphic 2026-10-18); phase 5 UI open     | `L:metadata` 0.22.0, `P:.../record-binding.ts`                                                              | Contacts/Organizations are link targets (Contact 360 / Organization 360)                     | Declare `identity`/display fields; any new CRM record type with a screen declares itself a link target                                        |
| 5   | Relative dates                                                      | Built                                                              | `L:metadata/src/date-criteria.ts`, `L:query-recipes/src/relative-dates`, `macroservice-time/relative-dates` | Available to every CRM dataset                                                               | None                                                                                                                                          |
| 6   | Create with AI (Generate Query)                                     | Built backend; console wired behind a flag                         | `P:modules/llm` (`prompts/queries/generate-query.md`), `P:docs/llm/llm-foundation-and-generate-query.md`    | Works on CRM datasets via describe                                                           | Good field descriptions in metadata (the model sees only describe)                                                                            |
| 7   | "Query packages"                                                    | Not found under that name                                          | —                                                                                                           | —                                                                                            | The nearest things are recipe packages (`@rhythmsoftware/macroservice-query-recipes`) and the tenant-export "package"; confirm what was meant |
| 8   | Quick Search + status-aware visibility                              | Built (accepted 2026-10-12)                                        | `P:modules/quick-search`, `P:docs/quick-search.md`, `P:docs/proposals/status-visibility.md`                 | CRM change-event handlers invoke Platform's indexer; status rule declared                    | Extend the discoverability rule to "not archived" when archive lands (lifecycle R-2)                                                          |
| 9   | Data Job Engine (imports, updates, upserts, deletes, rollback)      | Built                                                              | `L:data-jobs`, `P:modules/data-jobs`, `P:docs/data-jobs/*`, `C:modules/data-jobs`                           | Imports (config, orgs, contacts, activities), contact update (+rollback), three hard deletes | Add organization update/upsert; route every delete through the deletion-impact contract when it lands                                         |
| 10  | Tenant data export                                                  | Partial (WP-2 foundation; stopped 2026-10-19)                      | `P:modules/data-exports`, `P:docs/data-exports.md`, `P:docs/proposals/tenant-data-export/`                  | No CRM export provider on master                                                             | Implement the export provider in CRM's tenant-lifecycle participant when the work resumes                                                     |
| 11  | Record change history (revisions) and audit events                  | Built (accepted 2026-10-12)                                        | `C:record_revisions`, `P:modules/audit`, `P:docs/audit-events.md`                                           | Revisions in CRM schema; `record.viewed`, `record.merged` in Platform                        | Redact a hard-deleted record's revisions (lifecycle R-6, not built)                                                                           |
| 12  | Security / authorization (Knox v2 successor)                        | Built                                                              | `P:modules/security`, `L:authorization`, `P:docs/security/*`                                                | Permission keys `crm.contacts.*`, row-level security, portal `cid` claim                     | New keys for archive (`crm.*.archive`) and deletion approval                                                                                  |
| 13  | Service calls (Lambda-to-Lambda, IAM)                               | Built mechanism                                                    | `P:docs/security/service-calls.md`, `L:http`, `L:execution`                                                 | CRM publishes ONE service function: `tasks`                                                  | Publish contact/organization service functions (Membership's port is stubbed waiting for it)                                                  |
| 14  | Custom fields (definitions)                                         | Built                                                              | `P:modules/metadata`, `P:docs/metadata.md`, `L:custom-fields`                                               | Values in CRM's `*_custom_field_values`; definitions in Platform                             | Nothing new; reference fields are merge-reassigned by CRM                                                                                     |
| 15  | Custom objects / associations                                       | Designed (nothing built; D-01..D-18 open)                          | `P:docs/proposals/custom-objects/`                                                                          | —                                                                                            | Would add `related-records` cards to Contact/Organization 360 and association delete rules                                                    |
| 16  | Page layouts and forms                                              | Built                                                              | `P:modules/layouts`, `P:docs/layouts.md`                                                                    | Purposes `crm.contacts`, `crm.organizations`; CRM owns `layout_assignments`                  | Register a purpose per new record page                                                                                                        |
| 17  | Extensions (points, handlers, bindings, business-event subscribers) | Built foundation                                                   | `P:modules/extensions`, `P:docs/extensions.md`                                                              | `extensions:register`; CRM publishes business event `crm.contact.merged` only                | Publish delete/archive business events if other apps must react                                                                               |
| 18  | Responsibilities and attention                                      | Built foundation (attention migration 2026-10-19); revision 2 held | `P:modules/responsibilities`, `P:modules/attention`                                                         | Not adopted                                                                                  | Optional: attention providers for CRM conditions; nothing blocks CRM                                                                          |
| 19  | Tasks                                                               | CRM-owned (not Platform)                                           | `C:modules/tasks`                                                                                           | Tasks, types, **queues** all live                                                            | See A.10: queues were NOT removed in code                                                                                                     |
| 20  | Uploads / assets                                                    | Built                                                              | `L:assets`, `P:docs/assets.md`                                                                              | `crm.managed_assets`; delete retires files to `tmp/deleted/` (30 days)                       | None                                                                                                                                          |
| 21  | Communications / e-mail templates (LiquidJS)                        | Built (accepted 2026-10-09/10)                                     | `P:modules/communications`, `P:docs/communications/architecture.md`, `P:lib/email`                          | First consumer (contact role compliance)                                                     | Declare purposes in the shared package; templates are owned by records                                                                        |
| 22  | Snapshots                                                           | Built                                                              | `P:modules/snapshots`, `P:docs/snapshots/`                                                                  | CRM is not a snapshot source                                                                 | None                                                                                                                                          |
| 23  | Sandboxes                                                           | Branch (proposal only)                                             | `origin/sandbox-architecture` `docs/proposals/sandboxes/`                                                   | —                                                                                            | Would need a copy manifest beside CRM's purge rules                                                                                           |
| 24  | Eligibility                                                         | Built engine in dev; Platform module present; PARKED by Andrew     | `P:modules/eligibility`, `P:docs/eligibility.md`                                                            | `C:aws/crm.eligibility.ts` registers condition types                                         | None until unparked                                                                                                                           |
| 25  | Record merge participant contract                                   | Built                                                              | `L:record-merge` 0.2.0, `P:docs/record-merge-participant.md`                                                | CRM is the merge engine                                                                      | See Part B                                                                                                                                    |
| 26  | Safe delete / archive / deletion-impact contract                    | **Branch** (proposal, documentation only)                          | `origin/agent/crm-acc-w3-arch-lifecycle` `docs/proposals/record-lifecycle/`                                 | Hard delete with CRM-internal checks only                                                    | See A.13                                                                                                                                      |

#### A.1 Semantic query layer, saved queries, recipes, reports

- **What it does.** Every application registers datasets (a registered object plus the SQL source Platform compiles against).
  People and other services query them in one criteria language; saved queries, recipes, reports, exports and schedules sit on
  top. (`P:docs/queries/README.md`; design `P:docs/queries/semantic-query-layer.md`, decisions 2026-10-08.)
- **Where.** `P:modules/queries`; shared contract `@rhythmsoftware/macroservice-metadata` and
  `@rhythmsoftware/macroservice-query-recipes`.
- **Status.** Built and live in dev.
- **CRM's use.** `C:aws/crm.queries.ts` (registration manifest, `npm run queries:register`), `C:aws/crm.recipes.ts`, three
  recipe files under `C:modules/*/queries/recipes/`. Recipes are registered at deploy by `recipes:register`, validated by Platform
  against the live registry (`P:docs/queries/saved-queries-and-recipes.md` §3.3).
- **Cross-service use of CRM through it.** Accounting pages a person's saved query of contacts/organizations through the
  `platform_queries_record_ids` Lambda (`saved-queries-and-recipes.md` §5.3), so billing populations depend on CRM datasets.
- **What CRM must do.** Every new CRM table that a screen lists needs a dataset and a recipe; a grant to `rhythm_query_reader`
  for any columns another application's `security_invoker` view joins (Accounting's views join `crm.contacts`/`organizations`
  `(tenant_id, id, name)`; `accounting:modules/transactions/db/customer-names.ts`).

#### A.2 Explorer, saved views, entity-aware links, relative dates, Create with AI

- **Explorer** (`P:docs/explorer.md`, `P:modules/explorer`). Built. Saved views (private/shared) anchored to a recipe;
  frozen navigation sessions ("25 of 250", Previous/Next); "Select All Matching" returns ids only. Holds no record data, so it
  needs no re-check on delete. A navigation session can hold ids of records deleted since; the target page shows its not-found state.
- **Entity-aware queries** (`P:docs/proposals/entity-aware-queries.md`). Phases 1-4 live in dev 2026-10-16; `linkFields`
  stored since 2026-10-17; phase 5 (builder indicator, "Link to" menu) awaits V0 approval. A contact name in any result opens
  Contact 360. **Polymorphic references** (`entity-aware-queries-polymorphic-references.md`): built 2026-10-18 (metadata 0.22.0)
  so one column can link to a contact _or_ organization (Accounting's bill-to, submissions' owner record). G.7 of the main doc:
  "Deleted between run and click: the target 360 shows its existing not-found state."
- **Relative dates.** Built in the libraries (`$relativeDate` criterion, `L:metadata/src/date-criteria.ts`,
  `L:query-recipes/src/relative-dates.spec.ts`); stored unresolved, resolved at run time.
- **Create with AI.** Platform's Generate Query operation (`P:modules/llm/prompts/queries/generate-query.md`,
  `P:docs/llm/llm-foundation-and-generate-query.md`): plain words → a query in the semantic language; the model never writes SQL
  or sees records. Console wiring is behind a flag (`react-console:docs/queries-restore/PLAN.md`, "Create With AI").
- **"Query packages."** No design or code under that name in Platform, the libraries or the console. Closest matches: recipe
  packages and the tenant-export package. **Open question for the architecture.**

#### A.3 Quick Search and status-aware visibility

- Built (`P:docs/quick-search.md`; accepted 2026-10-12 per `P:docs/proposals/status-visibility.md`). CRM's change-event streams
  include `quickSearchContactIndexerHandler` / `quickSearchOrganizationIndexerHandler` (`C:aws/crm.change-events.ts`), which
  invoke Platform's indexer; a DELETE change event tombstones the document.
- Status visibility: contacts `active|inactive|deceased|duplicate`, organizations `active|inactive|duplicate`; one declaration
  says which are discoverable; new queries start with `Status is any of Active`; Quick Search hides the rest unless "Include
  inactive records". A record already referenced somewhere is never hidden by this rule.
- **What CRM must do.** When archive lands, extend the same declaration to "discoverable and not archived" (lifecycle R-2) rather
  than add a second filter.

#### A.4 Data Job Engine and data operations

- **What it does.** One engine (`@rhythmsoftware/macroservice-data-jobs`) for imports, bulk update, upsert, hard delete and
  compensating rollback, with validate-then-apply, frozen target sets and receipts. Platform hosts uploads (multipart),
  data runs (packages in dependency order) and tenant reset (`P:docs/data-jobs/README.md`, `uploads.md`, `data-runs.md`,
  `tenant-reset.md`). Design: `P:docs/proposals/rhythm-data-job-engine-plan.md`, `P:docs/proposals/data-engine-architecture/`.
- **CRM's operations** (`C:modules/data-jobs/services/operations/data-operations.ts`): configuration imports; organization,
  contact and activity imports (insert/upsert lanes); `crm.contacts/update` with `contact-update-rollback.ts`;
  `crm.contacts/delete`, `crm.organizations/delete`, activities delete. **No organization bulk update or upsert-by-id operation.**
- **Rollback vs hard delete** (engine plan §13.2; data-engine 16.9-16.12):
  - Bulk edit/upsert on a live tenant: compensating rollback (restores a field only if nobody changed it since; created records
    deleted only if unchanged and unreferenced).
  - **Hard delete: never undone.** Protection is in front: validate-only first (the engine refuses `mode: 'apply'` without a prior
    validation), an impact digest echoed on apply, both `<object>.bulk` and `<object>.delete`, and a typed confirmation
    (`DELETE 37 contacts`). Recovery is a manual staff re-create from revisions with a **new id**; files go to `tmp/deleted/`
    for 30 days. The delete operation declares no rollback and answers `hard_delete_not_reversible`
    (`C:modules/data-jobs/services/operations/contact-delete.ts`).
  - Financial documents are never rolled back; correct through Accounting's reversing documents (16.10).
  - Whole-cluster PITR is never used for one tenant; restore-and-extract into a new cluster (16.11).
  - Merged-away ids are aliased in `crm.merged_record_aliases`; an import keyed by an old id resolves through
    `resolveMergedId` (`L:record-merge/merged-ids`) or is rejected `record_merged`, never recreated (engine plan §6.3).
- **Gap that matters for safe delete.** The validation impact counts only CRM's own cascades and FK blocks
  (`listContactDeleteImpacts`, `C:modules/entities/services/persistence/contacts.ts:1303`). It does not ask any other
  application what it holds (see A.13 and Part B).

#### A.5 Tenant data export

`P:docs/data-exports.md`: WP-2 foundation only (tables, definitions/destinations API, permission keys, coverage registry,
purge/reset participants); nothing generates a package; executions stay `queued`. Stopped by Andrew 2026-10-19. Design says the
coverage registry extends the tenant-lifecycle participant contract (`tenant-data-export/07-coverage-registry.md`). Rhythm 2.0
hard-deletes (no `deleted_at` anywhere, `05-export-package-format.md`), so an export marks relationships `dangling: n` when
"a hard-deleted contact on an old invoice" remains. **CRM has no export provider on master** (Commerce has `modules/catalog/export/*`).

#### A.6 Record change history and audit

- CRM revisions (`crm.record_revisions`, append-only, INSERT/UPDATE/DELETE revoked from the app role) — approved 2026-10-11,
  accepted 2026-10-12, live in dev (`P:docs/proposals/record-change-history.md`). A deleted record's revisions keep its last
  values for the life of the tenant (the basis of the manual re-create, and the privacy gap D-42 rules against).
- Platform's `platform.audit_events` (`P:docs/audit-events.md`): `record.viewed`, `record.merged` (one row per participating
  record, written by Platform's merge apply), `commerce.intervention`, `eligibility.change`, `attention.*`. Viewable record
  types registry includes `crm.contacts` (`P:modules/audit/registry/viewable-record-types.ts:73`).
- **What CRM must do.** If D-42 is built: redact revisions of a hard-deleted record to a tombstone and write a metadata-only
  `platform.record_deletions` row (Branch design, A.13).

#### A.7 Security and authorization (Knox v2's successor)

- Built: `P:modules/security` (authorizer, roles, policies, assignments, portal identities), library
  `@rhythmsoftware/macroservice-authorization`. The 2026-10-08 design doc's status line ("nothing implemented") is stale; the
  implementation plan (`security-implementation-plan.md`) was executed.
- Facts CRM depends on: permission keys per action (`crm.contacts.read/query/create/update/delete`, `<object>.bulk`); forced
  row-level security on every tenant table; **contact-keyed assignments** (`security_assignments.contact_id`, derived rows owned
  by their deriver, revision note 2026-10-18); the portal user's linked contact is the soft column `platform.users.contact_id`
  and is signed into the security context as claim `cid` (`P:docs/security/portal-contact-claim.md`; "Knox V2 defined a claim
  of the same name").
- **Service calls** (`P:docs/security/service-calls.md`, design `service-principals.md` accepted 2026-10-17): a macroservice
  invokes another's Lambda directly, authorized by IAM; principals `svc_<service>`. **CRM publishes only `tasks`**
  (`C:aws/crm.service-functions.ts`). Membership's contact-update port is a stub that throws until CRM publishes a contacts
  service function (`membership:modules/memberships/ports/crm-contacts.ts`).
- Cross-service SQL functions are the other integration path: CRM owns `crm.portal_contact_candidates` and
  `crm.create_portal_contact` (granted to `rhythm_platform_app`); CRM calls `platform.user_display_name` and each participant's
  merge functions (`C:docs/cross-service-functions.md`).
- **What CRM must do.** Add archive and deletion-approval keys (lifecycle Q-1 default: `crm.contacts.archive`,
  `crm.organizations.archive`, admin tier); publish service functions for contact/organization reads and writes other apps need.

#### A.8 Custom fields and custom objects

- **Custom fields** — Built. Platform's metadata module owns the registry of objects, each tenant's definitions and label
  overrides; `describe_object` merges them (`P:docs/metadata.md`). Removal is archival (`archived_at`; keys never reused; values
  kept). CRM stores values in `crm.contact_custom_field_values`, `organization_custom_field_values`,
  `contact_role_custom_field_values`, `activity_custom_field_values`; reference-typed values (`reference_value`) are soft ids that
  the merge reassigns by the field's target (`C:docs/merging/relationship-coverage.md` rows 9-11, 23, 29). Library
  `custom-fields` 0.2.9 standardised the `*_string_idx`.
- **Custom objects** — Designed, nothing built (`P:docs/proposals/custom-objects/README.md`, D-01..D-18 open). Relevant to CRM:
  a general association model with `on_delete_from/to = detach | cascade | restrict` (03 §6.3, line 370); a cascade rule may never
  delete a native record (03:782); "archive is the ordinary removal", `DELETE` a staff operation (04:135); a `related-records`
  card on Contact 360 / Organization 360 (03 §11.3).

#### A.9 Layouts, 360 cards, extensions

- **Layouts** — Built (`P:docs/layouts.md`, `P:modules/layouts/purposes.ts`). Purposes: `crm.contacts`
  (administered by `crm.contact-layouts`), `crm.organizations`, `committees.committees`. Platform stores definitions; the
  record's owner stores the assignment (`layout_assignments.referrer_id` points at a type, not a record — merge P12).
- **360 cards** — Contact 360 / Organization 360 are console screens; metadata-driven cards beyond the page layout (the
  `related-records` card) are only in the custom-objects design.
- **Extensions** — Built foundation (`P:docs/extensions.md`, migration `20261018070404_extensions_foundation`): extension
  points, handlers, bindings, invocations, event types and **event subscribers** (`platform.event_subscribers` now exists; CRM's
  dispatcher comment still says it is "not on Platform's master yet", `C:aws/crm.business-events.ts`). CRM publishes one
  business event, `crm.contact.merged` (`C:aws/crm.events.ts`); none for delete or organization merge. The extensibility
  architecture proposal itself still awaits D-01..D-13.

#### A.10 Tasks, responsibilities and attention — the task-queue claim, verified

- **Claim to verify:** "task queues removed in favour of saved views." **Not true in code.** `crm.task_queues` exists (migration
  `C:db/migrations/0040_tasks.sql`, `C:modules/tasks/db/task-queues.table.ts`); a task may be in a queue, held by a user, both
  or neither; a queue a task is in cannot be deleted (`tasks_queue_fk ON DELETE RESTRICT`). Accounting and Commerce read
  `crm.task_queues` / `crm.task_types` (Accounting `product_task_templates`). The only written source of the claim is
  `P:docs/proposals/application-processes-and-workflow-actions/14-decisions.md:176` ("the brief says 2.0 replaced task queues
  with saved views"), whose recommended D-09 is narrower: workflow actions create tasks through CRM's `tasks` service function,
  **do not use `queue_id`**, and "my review tasks" is a saved view. The console maps "Applications › CRM › Task Queues" as a live
  configuration screen (`react-console:docs/migration/13-consolidation-map.md:21`). **Decision needed in the CRM architecture:**
  keep queues, or retire them in favour of saved views over `tasks-all`.
- **Responsibilities** (`P:docs/responsibilities.md`) — Built: who is expected to look after an operational area; grants no
  permission. Scopes never name a contact/organization today; merge policy `block` if they ever do.
- **Attention** (`P:docs/attention.md`) — Built foundation 2026-10-19: applications write items at source through SQL functions;
  `subject_dataset` may be `crm.contacts`/`crm.organizations` (`registration-manifest.ts:130`); a provider resolves an item with
  outcome `record_deleted`. CRM has not adopted it. Revision 2 is held by Andrew.

#### A.11 Uploads/assets, communications, snapshots, sandboxes, eligibility

- **Assets** — Built (`P:docs/assets.md`, `@rhythmsoftware/macroservice-assets/record-assets`). A save that assigns a file
  succeeds only once the file is stored, validated and tenant-owned. CRM's delete detaches the record's files; the cleanup pass
  moves them to `tmp/deleted/`, deleted after 30 days (Andrew, 2026-10-08).
- **Communications and e-mail** — Built and accepted (`P:docs/communications/architecture.md`, `P:docs/email.md`). Templates
  render with LiquidJS (`modules/communications/services/templates/render-message.ts`); merge fields are read from the records;
  each template is owned by a record (`owner_object_id`, `owner_record_id`); purposes come from
  `@rhythmsoftware/macroservice-communications/purposes`; CRM submits through `platform_communications_request`. Platform keeps
  `communication_messages.recipient_contact_id` as delivery history.
- **Snapshots** — Built (`P:docs/snapshots/README.md`, migration `0105`); sources are Accounting and Membership; CRM is not.
- **Sandboxes** — Branch only (`origin/sandbox-architecture`, `docs/proposals/sandboxes/`, "nothing is implemented"): in-database
  copy per service under one exported snapshot, record ids preserved, a copy manifest beside each table's purge rule. CRM would
  supply that manifest.
- **Eligibility** — Engine and repairs on Platform master in dev; PARKED by Andrew 2026-10-19. CRM registers condition types
  (`C:aws/crm.eligibility.ts`, `eligibility:register`). Portal eligibility reads the `cid` claim.

#### A.12 Record merge (the only cross-application reference-rewrite contract that exists)

- Contract: `@rhythmsoftware/macroservice-record-merge` 0.2.0 (`L:record-merge`), modules `participant`, `policies`,
  `participant-test-kit`, `merged-ids`. A participant is **two SQL functions in its own schema** —
  `<schema>.merge_plan_<object>(survivor_id text, merged_ids text[]) → jsonb` and
  `<schema>.merge_apply_<object>(merge_id, survivor_id, merged_ids, decisions jsonb) → jsonb` — `SECURITY DEFINER`, pinned
  `search_path`, `EXECUTE` granted only to `rhythm_crm_app`, called **inside CRM's one merge transaction**
  (`P:docs/record-merge-participant.md`).
- **Discovery is the Postgres catalog**, not a registry table: CRM reads `pg_proc` for functions of those names it may execute
  (`C:modules/merges/services/persistence/merge-participants.ts`). A half pair is a blocker. **Required participants:**
  `contacts: ['platform']`, `organizations: []`. A missing required schema blocks (`ParticipantMissing`); **a missing
  non-required participant is simply not called.**
- Policy vocabulary: `owned`, `reassign`, `reassign-dedupe`, `consolidate`, `preserve-history`, `block`, `not-a-reference`
  (`C:docs/merging/architecture.md` §6.1). Each participant keeps a typed reference registry held to its schema by a coverage
  test (`P:lib/record-merge/platform-references.ts`, `committees:lib/record-merge/committees-references.ts`).
- Merges hard-delete the merged-away records and alias them in `crm.merged_record_aliases`; the business event
  `crm.contact.merged` is published (contact merges only).

#### A.13 "Safe delete": everything that exists, precisely

**Searched** services-platform (docs and code) for `safe delete`, `soft delete`, `hard delete`, `archive`, `deletion request`,
`deletion participant`, `record_deleted`, plus services-crm and `~/rhythm2/briefs`.

1. **There is no soft delete anywhere in Rhythm 2.0.** No `deleted_at` column in any macroservice
   (`P:docs/proposals/tenant-data-export/05-export-package-format.md:107`); CRM contact delete is a hard `DELETE` cascading to
   owned rows (`P:docs/proposals/quick-search.md:237`, `record-change-history.md:110`). "Safe delete" appears in no Platform file.
2. **CRM's delete today** (`C:modules/entities/services/persistence/contacts.ts:1868`, `organizations.ts:1201`): refuses when
   the record or an element is `sys_locked`; contact: clears other contacts' `spouse_id`, cascades owned rows (addresses,
   e-mails, phones, preferences, custom values, affiliations and roles, activity links), `tasks.contact_id` → `SET NULL`,
   recomputes duplicate groups, detaches files; organization: refused by FK while any child organization or affiliation names it.
   **It consults no other application.** Accounting invoices, memberships, committee seats, submissions, portal logins and
   Commerce orders keep the dead id (soft references; see Part B).
3. **Bulk hard delete** (Data Job Engine): validate-first, digest-bound, typed confirmation, two permission keys, no undo,
   second approver for >1,000 records on a live tenant recommended but not decided (`data-engine-architecture/17-security…:84-85,218`).
4. **Platform reactions to a CRM delete that exist today:** Quick Search tombstones on the DELETE change event; attention items
   resolve `record_deleted` (`db/migrations/20261019130938_attention.sql:841`). Membership has a CRM change handler
   (`membership:modules/memberships/services/flowdown/crm-change-events.ts`) that **CRM never invokes** (it is not in
   `C:aws/crm.change-events.ts`; Membership's README item 14 says wiring it is CRM's change).
5. **Archive exists only for configuration**: custom field definitions (`archived_at`), list options (`archived`), roles. Custom
   objects design: "archive is the ordinary removal" (Designed).
6. **Tenant-level deletion** (`tenant_deletion_requests`, lifecycle `purge`/`reset` participants) is built and is the model the
   record-level design reuses; it is not record deletion.
7. **The record-level design — Branch, not on master.** `docs/proposals/record-lifecycle/` on
   `origin/agent/crm-acc-w3-arch-lifecycle` (`6873e3b6c`), "proposed 2026-10-19, documentation only", written from Andrew's
   CRM-acceptance rulings **D-02, D-07, D-25, D-41, D-42, D-44**. The CRM acceptance wave plan records that the **cross-service
   safe delete (D-41, D-42, D-44) was deferred by Andrew "to a later milestone owned by the CRM architecture retrospective"**, while
   archive (D-02, D-07, D-25) proceeds in CRM and console (`~/rhythm2/briefs/acceptance-waves/overrides/crm.waves.json:213`).
   Its content, which the CRM architecture should adopt as the "safe delete" design:
   - **Three operations, never confused:** _Deactivate_ (edit `status`; reversible; status-visibility hides from new choices);
     _Archive_ (new columns `archived_at`, `archived_by_id`, `archive_reason`; reversible; leaves lists/pickers/new workflows,
     keeps every historical reference; never a status value, never a delete; "Never map Archive to hard deletion", D-02);
     _Hard delete_ (explicit, authorized, impact-previewed durable job; irreversible; leaves a metadata-only record).
   - **One deletion-impact contract (D-41):** the deleting service asks Platform to plan; Platform asks every participant
     through the **tenant-lifecycle participant manifests and lifecycle functions** (`P:modules/tenants/services/lifecycle/
remote-participant-manifests.ts`, `remote-participants.ts`), two new `operations` entries added the way `reset` was. Each
     participant answers per reference kind **block | retain | detach | redact | delete**, with counts, ids and versions, a
     readable reason and a retention-policy reference. Approval binds a digest of subject and dependent versions. A
     `record_deletion` job re-plans, holds the subject, applies participants in dependency order, removes the owner's row last.
     **Any `DELETE` without an approved, still-current plan is refused server-side** in the owner's persistence operation.
   - **Fail closed (R-4):** a participant that declares references but does not serve the operations is a blocker
     `participant_not_reporting`.
   - **R-7:** an organization named by any affiliation (current, past, future) is never deletable; archive is the path.
     **R-8:** financial, membership, governance and submission history answers `retain` or `block`, never `delete` ("Do not infer
     deleting a CRM contact authorizes financial-history deletion", D-41).
   - **Retention (D-42):** `platform.record_deletions` row with who/when/why, object type/id, record number, per-participant
     dispositions — no name, address, e-mail or content; CRM's revisions of the deleted record redacted to a tombstone; a
     read-only deletion audit view reachable from old links and data-job items.
   - **Activities (D-44):** the deleted record's activity links become tombstones; an activity left with no link is marked
     orphaned (never reassigned to the organization, never deleted for that reason); an Orphaned Activities view.
   - **Linked addresses (D-25):** a contact row linked to a removed organization address keeps the last value with provenance
     and status `disconnected` (needs verification).
   - Open questions with fail-closed defaults: Q-1 archive permission tier; Q-2 second approver (default no); Q-3 deletion
     record retention (life of tenant); Q-4 erasure reason; Q-5 disconnected addresses in mailings; Q-6 merges do not write a
     deletion record (they write `record.merged`); Q-7 ended affiliations' linked rows.
   - Its participant inventory (`06-participant-inventory.md`) is the most complete per-column map of cross-app references and
     recommended delete dispositions; Part B below agrees with it and adds the merge column.

---

### Part B. Every other application's references to CRM records

**Mechanisms found (all applications):**

- **No cross-schema foreign keys.** `REFERENCES "crm".` appears in no migration of any service (only test fixtures).
  Every reference is a soft `text` id, usually beside a `*_kind` discriminator (`contact`/`organization`).
- **Direct SQL reads of CRM tables**, by standing decision ("every macroservice reads any table", `rhythm_table_reader`, Andrew
  2026-10-08; `membership:modules/memberships/services/persistence/crm-people.ts:1-8`): Membership (affiliations, roles,
  parents), Commerce (addresses, affiliations), Accounting views (`LEFT JOIN crm.contacts/organizations` for names), Committees
  ports (addresses, disclosures).
- **Metadata references** (`target: 'crm.contacts'`) in each app's registered objects, which give query links but no integrity.
- **Merge participant functions** (catalog-discovered) — only Platform and Committees have them.
- **Change-event subscription** — only Platform's Quick Search indexer is invoked by CRM; Membership's handler exists, unwired.
- **Business events** — `crm.contact.merged` only; nobody outside CRM subscribes on master.
- **Service functions** — CRM publishes `tasks` only.

#### B.1 The table

"on merge" = what CRM's merge does today (participant function, or nothing). "on delete" = what happens today when CRM hard-deletes
the record (nothing cross-app enforces anything; "dangles" = the id stays and resolves to nothing).

| App         | Referencing table.column                                                                                                                                                                                                                                                                                                                                            | Mechanism                                                                                                                                                                                 | On merge (today)                                                                                     | On delete (today)                                                                               | Evidence                                                                                                                |
| ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------- |
| Platform    | `users.contact_id` (portal login)                                                                                                                                                                                                                                                                                                                                   | soft text, no index/uniqueness; `cid` claim                                                                                                                                               | **reassign** (relink, audited in `security_audit_events`); **blocks** if 2+ participants have logins | dangles: portal user linked to nothing                                                          | `P:docs/record-merge-participant.md`; `P:lib/record-merge/platform-references.ts`                                       |
| Platform    | `security_assignments.resource_id` (+`resource_type`)                                                                                                                                                                                                                                                                                                               | soft, polymorphic                                                                                                                                                                         | **block** (`resource_scoped_grants`)                                                                 | dangles (none exist today)                                                                      | same                                                                                                                    |
| Platform    | `security_assignments.contact_id` (derived grants)                                                                                                                                                                                                                                                                                                                  | soft                                                                                                                                                                                      | `owned` by deriver (Committees re-syncs via `platform.sync_derived_assignments`)                     | dangles until re-derived                                                                        | `platform-references.ts:165-178`                                                                                        |
| Platform    | `responsibility_assignments.resource_id`, `unassigned_responsibilities.resource_id`                                                                                                                                                                                                                                                                                 | soft, polymorphic                                                                                                                                                                         | **block**                                                                                            | dangles (never names CRM today)                                                                 | `platform-references.ts`                                                                                                |
| Platform    | `attention_items.subject_id`, `attention_item_audiences.contact_id/organization_id`, `attention_states.principal_id`                                                                                                                                                                                                                                                | soft                                                                                                                                                                                      | registry says **reassign**                                                                           | provider resolves `record_deleted`                                                              | `platform-references.ts:221-`; attention migration `:841`                                                               |
| Platform    | `attention_items.about_record_id`                                                                                                                                                                                                                                                                                                                                   | soft, polymorphic                                                                                                                                                                         | preserve-history                                                                                     | `record_deleted`                                                                                | same                                                                                                                    |
| Platform    | `portal_contact_challenges.contact_id`                                                                                                                                                                                                                                                                                                                              | soft                                                                                                                                                                                      | preserve-history                                                                                     | dangles (transient)                                                                             | same                                                                                                                    |
| Platform    | `audit_events.record_type+record_id`                                                                                                                                                                                                                                                                                                                                | soft, append-only                                                                                                                                                                         | preserve-history + `record.merged` rows                                                              | retained (ids only)                                                                             | `P:docs/audit-events.md`                                                                                                |
| Platform    | `communication_messages.recipient_contact_id`, `.recipient_key`; `communication_requests.source_entity_id`                                                                                                                                                                                                                                                          | soft                                                                                                                                                                                      | preserve-history                                                                                     | retained                                                                                        | coverage P3-P5                                                                                                          |
| Platform    | `quick_search_documents.record_id`                                                                                                                                                                                                                                                                                                                                  | index state                                                                                                                                                                               | not-a-reference                                                                                      | tombstoned by DELETE change event                                                               | coverage P8                                                                                                             |
| Platform    | `saved_queries.definition`, `query_recipes.query` (jsonb literals)                                                                                                                                                                                                                                                                                                  | classified jsonb                                                                                                                                                                          | not-a-reference; filter `id = <merged id>` matches nothing (known limitation)                        | same                                                                                            | coverage P11                                                                                                            |
| Platform    | `communication_templates.owner_record_id`, `layout_assignments.referrer_id`                                                                                                                                                                                                                                                                                         | type pointers                                                                                                                                                                             | not-a-reference                                                                                      | n/a                                                                                             | coverage P12                                                                                                            |
| Accounting  | `invoices`, `payments`, `credits`, `credit_applications`, `credit_memos`, `debit_memos`, `refunds`, `reversals`, `write_offs`, `settlements`, `payment_intents`, `payment_reversals`, `receivable_events`, `installment_schedules` `.bill_to_kind/.bill_to_id`                                                                                                      | soft kind+id; **write-once/frozen** on issued invoices, payments, credits, credit applications (`20261018180757_document_reassign_and_edit.sql:25-55`); names via `LEFT JOIN crm.*` views | **nothing** — no participant; invoices keep the merged-away id, customer name resolves to null       | dangles; views show null name (no row dropped)                                                  | `accounting:modules/transactions/db/*.table.ts`; `customer-names.ts`; `docs/plan/02-accounting-architecture.md:700-716` |
| Accounting  | `ar_snapshot_items.customer_kind/customer_id` (+ `customer_name` snapshot)                                                                                                                                                                                                                                                                                          | soft, dated fact                                                                                                                                                                          | nothing                                                                                              | retained with name snapshot                                                                     | `ar-snapshot-items.table.ts:74-76`                                                                                      |
| Accounting  | `subledger_entries.contact_id/organization_id`, `revenue_recognition_schedules.contact_id/organization_id`                                                                                                                                                                                                                                                          | soft                                                                                                                                                                                      | nothing                                                                                              | dangles                                                                                         | `modules/batches/db/*.table.ts`                                                                                         |
| Accounting  | `stripe_customers.contact_id` (one processor customer per contact)                                                                                                                                                                                                                                                                                                  | soft                                                                                                                                                                                      | nothing (plan: keep both mappings under survivor)                                                    | dangles; processor customer orphaned                                                            | `stripe-customers.table.ts:22`                                                                                          |
| Accounting  | `billing_notices.contact_role_ids`, `document_templates.contact_role_ids`, `product_task_templates` → `crm.task_types/task_queues`, `product_special_prices` criteria over `crm.contacts`                                                                                                                                                                           | configuration ids                                                                                                                                                                         | n/a                                                                                                  | a deleted role/queue leaves a stale id                                                          | `0073_product_features.sql:9`                                                                                           |
| Commerce    | `orders.buyer_kind/buyer_id`, `.bill_to_kind/bill_to_id`, `.acting_contact_id`; `order_lines.recipient_kind/recipient_id`                                                                                                                                                                                                                                           | soft kind+id                                                                                                                                                                              | **nothing**                                                                                          | dangles (orders never deleted)                                                                  | `commerce:modules/orders/db/orders.table.ts:138-143`, `order-lines.table.ts:82-83`                                      |
| Commerce    | `checkouts.buyer_*`, `.bill_to_*`, `.acting_contact_id`; `checkout_items.recipient_*`                                                                                                                                                                                                                                                                               | soft                                                                                                                                                                                      | nothing                                                                                              | dangles                                                                                         | `modules/checkout/db/*.table.ts`                                                                                        |
| Commerce    | `entitlements.holder_kind/holder_id` (contact\|organization)                                                                                                                                                                                                                                                                                                        | soft                                                                                                                                                                                      | nothing                                                                                              | dangles (an active entitlement held by nobody)                                                  | `modules/entitlements/db/entitlements.table.ts:36,62-63`                                                                |
| Commerce    | `promotion_codes.owner_kind/owner_id`; `promotion_redemptions.purchaser_*`, `.organization_id`                                                                                                                                                                                                                                                                      | soft                                                                                                                                                                                      | nothing                                                                                              | dangles                                                                                         | `modules/promotions/db/*.table.ts`                                                                                      |
| Commerce    | `offer_presentations.acting_contact_id`                                                                                                                                                                                                                                                                                                                             | soft                                                                                                                                                                                      | nothing                                                                                              | dangles                                                                                         | `modules/recommendations/db/offer-presentations.table.ts:41`                                                            |
| Commerce    | reads `crm.contact_addresses`, `crm.organization_addresses`, affiliations; pricing extension facts `crm.contacts`                                                                                                                                                                                                                                                   | direct SQL read                                                                                                                                                                           | n/a                                                                                                  | read misses                                                                                     | `modules/checkout/services/persistence/{party-addresses,crm-affiliations}.ts`; `modules/pricing/extensions/*.ts`        |
| Committees  | `committee_appointments.contact_id`, `.designated_by_contact_id`; `derived_assignments.contact_id`; custom-field `reference_value`                                                                                                                                                                                                                                  | soft; **participant `committees.merge_plan/apply_contacts`**                                                                                                                              | **reassign** (blocks a seat held twice; re-syncs Platform grants)                                    | dangles                                                                                         | `committees:lib/record-merge/committees-references.ts`; migrations `20261018013744`, `…025140`, `…123021`, `…132524`    |
| Committees  | `*_versions`, `meeting_participants`, `agenda_items.presenter_contact_id`, `meeting_notifications`, `calendar_feed_tokens`, `proposed_action_items.assignee_contact_id`, `decision_eligible_voters`, `votes`, `recusals`, `opening_interests`, `notices`, `conflict_disclosures`, `nominations.nominee/nominated_by_contact_id`, `opening_alternates` `.contact_id` | soft                                                                                                                                                                                      | **preserve-history** (keeps merged-away id; readers resolve via alias table)                         | dangles                                                                                         | same registry (26 entries)                                                                                              |
| Committees  | `committee_appointments.representing_organization_id`, `committee_seats.reserved_for_organization_id`                                                                                                                                                                                                                                                               | soft                                                                                                                                                                                      | preserve-history — **no organizations participant yet** ("follow-up")                                | dangles                                                                                         | registry `:74-92`                                                                                                       |
| Committees  | `committee_record_links.record_type/record_id`                                                                                                                                                                                                                                                                                                                      | soft, polymorphic                                                                                                                                                                         | not in the registry                                                                                  | dangles                                                                                         | `modules/committees/db/committee-record-links.table.ts`                                                                 |
| Membership  | `memberships.contact_id/organization_id` (owner), `applications.contact_id/organization_id`, `application_assigned_contacts.contact_id`                                                                                                                                                                                                                             | soft; flow-down reads CRM directly                                                                                                                                                        | **nothing** — `merge_plan_memberships`/`merge_apply_memberships` "were not built"                    | dangles; flow-down handler would re-derive org memberships on DELETE but CRM does not invoke it | `membership:modules/memberships/db/memberships.table.ts:49-50`; `docs/migrations/membership-core/README.md:180-182`     |
| Membership  | `organizational_unit_organizations.organization_id`                                                                                                                                                                                                                                                                                                                 | soft                                                                                                                                                                                      | nothing                                                                                              | dangles                                                                                         | `modules/structure/db/organizational-unit-organizations.table.ts:24`                                                    |
| Membership  | `roster_export_rows.contact_id/organization_id`, `membership_snapshot_members.member_id`, `record_revisions.record_id`                                                                                                                                                                                                                                              | soft                                                                                                                                                                                      | nothing                                                                                              | dangles / retained                                                                              | `modules/memberships/db/roster-export-rows.table.ts`; `modules/snapshots/db/…members.table.ts:44`                       |
| Membership  | contact updates by officers                                                                                                                                                                                                                                                                                                                                         | **service port stub** (throws `PortNotWired`) waiting for a CRM service function                                                                                                          | n/a                                                                                                  | n/a                                                                                             | `modules/memberships/ports/crm-contacts.ts`                                                                             |
| Submissions | `submissions.applicant_contact_id/applicant_organization_id`, `.subject_record_id`, `.owner_record_id`                                                                                                                                                                                                                                                              | soft + `patternCheck(RECORD_ID_PATTERN)`; metadata target `crm.contacts`                                                                                                                  | **nothing** (no participant)                                                                         | dangles                                                                                         | `submissions:modules/intake/db/submissions.table.ts:56-61,79-89`                                                        |
| Submissions | `submission_participants.contact_id/organization_id`, `submission_revisions.submitted_by_contact_id`, `submission_attachments.uploaded_by_contact_id`, `submission_events.actor_contact_id`, `review_assignments.reviewer_contact_id`, `conflict_declarations.contact_id`, `review_team_members.contact_id`                                                         | soft + pattern checks                                                                                                                                                                     | nothing                                                                                              | dangles (open review assigned to nobody)                                                        | `modules/{intake,outcomes,review,definitions}/db/*.table.ts`                                                            |
| Websites    | none to CRM records. `customer_id` columns (`website_changes`, `web_templates`, `security_audit_events`) mean **Rhythm's paying customer** (`platform.customers`), not a CRM party                                                                                                                                                                                  | —                                                                                                                                                                                         | n/a                                                                                                  | n/a                                                                                             | `websites:modules/changes/db/website-changes.table.ts:108-109`; `P:modules/customers/db/customers.table.ts`             |

Note on naming: in Platform and Websites `customer_id` is Rhythm HQ's customer (tenant owner); in Accounting "customer" is the CRM
contact/organization a document bills. The CRM architecture should avoid "customer" for CRM parties in shared contracts.

#### B.2 What happens on CRM merge and delete, in one paragraph each

**Merge.** CRM plans and applies every reference in its own schema from its typed registry, then calls each catalog-discovered
participant in schema order inside one transaction. Only `platform` (contacts and organizations) and `committees` (contacts only)
publish participants. Platform is required for contact merges; nothing is required for organizations. Accounting, Commerce,
Membership and Submissions publish none, so **a merge today silently leaves their rows pointing at the merged-away id**; readers can
resolve it through `crm.merged_record_aliases`, but none of those apps do. Accounting's own plan says the default for an
unregistered Accounting must be **block** and that `accounting` must be added to CRM's required list in the same release
(`accounting:docs/plan/02-accounting-architecture.md:700-704`); neither has happened. `C:docs/merging/relationship-coverage.md`
§3-§4 still describes these apps as "not in 2.0 yet" and financial records as "default block" — that matrix is out of date.

**Delete.** CRM's single and bulk deletes check only CRM-internal locks and foreign keys and write a DELETE change event that, outside
CRM's own handlers, only Platform's Quick Search consumes. Every row in B.1 outside CRM keeps an id that resolves to nothing. There is no cross-application
deletion contract on master; the designed one is on an unmerged branch (A.13 item 7) and its build was deferred by Andrew to the
CRM architecture milestone.

---

### Integration risks for CRM (ranked)

1. **Merges silently strand financial and membership references.** CRM's required-participant list is `contacts: ['platform']`,
   `organizations: []` (`C:modules/merges/services/persistence/merge-participants.ts`); Accounting (14 bill-to tables, write-once
   bill-to on issued documents), Commerce, Membership and Submissions have no participant, so a merge proceeds and their rows keep
   a deleted id. Accounting's design requires "block until registered" (`accounting:docs/plan/02-accounting-architecture.md:700`).
   Fix: add every reference-holding schema to the required list (fail closed) until its participant exists; Accounting's
   participant must rewrite frozen columns through a definer function or answer `preserve-history` plus a live-account link.
2. **Hard delete consults no other application.** `deleteContact`/`deleteOrganization` and the bulk delete operations
   (`C:modules/entities/services/persistence/contacts.ts:1868`, `organizations.ts:1201`, `C:modules/data-jobs/services/operations/contact-delete.ts`)
   can remove a contact with open invoices, an active membership, a current committee seat, an in-flight submission, an active
   entitlement or a portal login. The designed fix (deletion-impact contract, D-41) is on an unmerged branch and deferred. Until
   it lands, the safe interim is: archive as the user-facing removal, and hard delete refused when any non-CRM reference exists
   (a read-only check across the soft columns listed in B.1).
3. **The portal login dangles on delete.** `platform.users.contact_id` has no FK; deleting the contact leaves a portal user whose
   `cid` claim names nothing, which eligibility and portal authorization read (`P:docs/security/portal-contact-claim.md`). The
   lifecycle design answers `block` `portal_login`; nothing enforces it today.
4. **No archive exists for contacts or organizations.** Only `status` (`active/inactive/deceased/duplicate`) and hard delete.
   Andrew's ruling D-02 forbids mapping Archive to delete; archive columns, permission keys and the "discoverable and not archived"
   rule are designed but unbuilt (`record-lifecycle/01`, R-1, R-2).
5. **Deleted-record history keeps all personal data.** CRM revisions of a deleted contact keep full content for the life of the
   tenant (`record-change-history.md`); D-42 rules this out (redact + metadata-only deletion record). Privacy/SOC 2 exposure.
6. **Membership's CRM change handler is unwired.** Organization flow-down memberships do not re-derive on contact/organization
   changes or deletes because CRM's stream list (`C:aws/crm.change-events.ts`) does not invoke `membership_crm_change_handler`
   (`membership:docs/migrations/membership-core/README.md` item 14).
7. **CRM exposes no service functions for contacts/organizations.** Only `tasks` (`C:aws/crm.service-functions.ts`); Membership's
   contact-update port throws `PortNotWired`. Other apps fall back to direct SQL reads of CRM tables, which couples them to CRM's
   physical schema (column renames break Accounting views, Membership flow-down, Commerce checkout reads).
8. **Committees has no organizations participant**, so representing/reserved organizations keep merged-away ids
   (`committees-references.ts:76,92`); organization merges reach only Platform.
9. **Coverage documentation is stale.** `C:docs/merging/relationship-coverage.md` (built at CRM `d23f0be`) lists 12 Platform columns
   and treats Accounting/Membership/Commerce as future; Platform's registry now has 15 entries (attention, responsibilities,
   portal challenges), and the other apps exist. The CRM architecture should replace it with a generated cross-app matrix.
10. **Task queues: brief and code disagree.** The brief says queues were replaced by saved views; `crm.task_queues` is live, has a
    RESTRICT FK, a console configuration screen, and Accounting reads it. Decide before designing tasks.
11. **Saved filters on a specific record id break silently** after merge or delete (Platform P11, known limitation; warning only).
12. **No CRM export provider** for the tenant data export, and the export design marks hard-deleted references as `dangling`;
    CRM will owe an export provider when that work resumes.
13. **"Customer" is overloaded** (Rhythm HQ customer in Platform/Websites vs CRM party in Accounting); shared contracts should
    say contact/organization ("party") explicitly.
14. **CRM's business-events dispatcher comment is out of date** (`platform.event_subscribers` now exists on Platform master); only
    `crm.contact.merged` is published, so no app can subscribe to organization merges, deletes or archives.

## 25. E06 — Customer pain with Rhythm 1.0 CRM (Rolodex): Jira, Zendesk, HubSpot, Linear, GitHub

## E06 — Customer pain with Rhythm 1.0 CRM (Rolodex): Jira, Zendesk, HubSpot, Linear, GitHub

Gathered 2026-10-19, 16:37–16:42 ET, read-only. No ticket or issue was created, edited, commented on or transitioned.

### Purpose

This file records what customers repeatedly struggled with in the Rhythm 1.0 CRM (internally "Rolodex": contacts, organizations, affiliations, duplicates and merge, addresses, custom fields, imports, queries, search, contact roles, regions, directories, activities and tasks, and portal profile updates). The aim is to give the Rhythm 2.0 CRM architecture a list of failure patterns it must not repeat. Each pattern has an acceptance criterion that a regression test can check.

We looked for patterns that recur across tenants and years, not for counts. Ticket ids, dates and customer organization names are given as evidence. Personal names and contact details from the tickets are left out on purpose.

Sources that worked: the Jira project **RB (Rhythm R&D)**, which holds the engineering bug tickets escalated from support, and **Zendesk** (support tickets, 2020 to today). Sources that returned nothing: HubSpot tickets, Linear issues, GitHub issues, and useful GitHub or git history. The access log at the end has the details.

### Patterns

The table is ordered by how much damage each pattern did to customers: silent data loss first, then cosmetic problems.

| #   | Pattern                                                                                     | What customers were trying to do                                                                                                                                                                                                                                 | Why 1.0 struggled (cause)                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                              | Representative evidence (key, date)                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                   | Regression criterion for 2.0                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                       |
| --- | ------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| P1  | **Merge silently loses or orphans data that belongs to the merged record**                  | Merge two records for the same person or company and keep everything both records owned: memberships, chapters and sections, orders and registrations, custom fields, photos, files, gifts, audit history, logins, accreditation data, and links to integrations | Merge copied the selected fields onto the master record and then **hard-deleted** the other record. Every other microservice kept its own copy of the contact or organization id (memberships, finance, fundraising, accreditations, files, security/Auth0, Higher Logic sync). Nothing re-pointed those ids. They became dangling foreign keys that broke later saves, and the failure often showed only in the dead-letter queue.                                                                                                                                    | RB-22 (2021-07-29, org merge leaves inherited "flowdown" membership pointing at the deleted org); RB-385 (2021-11-09, custom fields overwritten at random or dropped); RB-626 / RB-627 (2022-01-26, org merge orphans invoices and loses record data); RB-916 (2022-03-31, PSA, membership history lost); RB-956 (2022-04-19); RB-1703 (2022-11-01, registrations and orders not moved); RB-1879 (2022-12-16, NAEMSP and PSA, membership sporadically not moved); ZD 5433 (2023-01-25, CVMA, memberships and logins); RB-2433 / RB-2438 / RB-2439 (2023-04, INCOSE, audit log, security tab, chapters and sections); ZD 6256 (2023-04-24, sections and chapters not moved); RB-2546 (2023-05, file inaccessible after merge); RB-2640 / ZD 6610 (2023-06, photo not moved); ZD 8209 (2023-09-25, ISTSS, membership not moved, high priority); RB-3357 (2023-12-01, gifts give "record not found"); RB-3476 (2023-12-21, org merge drops accreditation candidates, custom fields and visit data); ZD 17865 (2025-11-24, FBA, org merge removed the org from a gift); RB-6278 (2025-12-12, AHRA, Higher Logic keeps syncing the deleted id, so the member cannot log in); RB-6392 (2026-01-27, parent org cannot edit membership after a child-org merge); RB-7337 (2026-07-01, membership save fails with ForeignKeyViolation and retries in a DLQ loop); RB-7378 (2026-07-10, merge orphans the Auth0 login by design); RB-7641 (2026-09-04, NLA, paid renewal silently never advances because of a stale contact id) | (a) Merge is one atomic operation, or a saga with compensation, that re-points **every** reference to the losing id. "Every" means every table or service that has registered a reference to the contact or organization entity type, including integrations and identity. (b) After a merge, a test that walks the reference registry finds **zero** rows still pointing at the losing id. (c) The losing record stays as a tombstone with `merged_into`, so any late or external reference resolves to the survivor and is never a dangling key. (d) Logins, external-system ids and portal identities move to the survivor. (e) Every merge writes one audit entry listing every reference it moved.                                                                                                                                                            |
| P2  | **The merge tool itself fails, with errors that say nothing useful**                        | Run a cleanup of hundreds of duplicates after go-live                                                                                                                                                                                                            | Merge was a multi-step process across several services, run by the client. Any service failing partway left the merge half done and showed "unknown error". The preview step and the real execution ran different code. Permission checks differed between screens. RB-24 (2021-04) refactored contact merge "to fix the intermittent/unknown errors", yet the errors kept coming.                                                                                                                                                                                     | ZD 1183 (2021-03-12); RB-71 / RB-76 (2021-08, CLAS); RB-438 (2021-12); RB-847 (2022-03); RB-1136 (2022-06); RB-1158 / RB-1286 (2022-06/07); ZD 4131 (2022-07-21, AAPOR, high priority: "unknown errors", merge operation failure, ghost duplicates, all during a cleanup); RB-1535 / RB-1542 (2022-09, preview fails); RB-1769 (2022-11); RB-1847 (2022-12, read-only users can merge); RB-1934 / RB-2067 (2023-01/02); RB-2028 / RB-2046 / ZD 5414 (2023, AIC, power users blocked on one screen; multiple windows); ZD 11015 (2024-03); ZD 11945 (2024-05); ZD 12510 (2024-07); ZD 13576 (2024-10); RB-5893 (2025-09); RB-6120 (2025-11); ZD 17664 (2025-11); RB-6826 (2026-04, ASAM, blank merge page for imported records); RB-7311 (2026-06, Rubber, unable to merge organization)                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                               | (a) A merge either completes entirely or changes nothing, and any failure names the blocking record or rule. (b) The preview is computed by the same code that executes the merge, and the executed result equals the preview field by field. (c) Merge permission is checked on the server and is the same on every screen. (d) Bulk merge of a list of pairs runs as a background job with a result for each pair.                                                                                                                                                                                                                                                                                                                                                                                                                                               |
| P3  | **Duplicate detection shows phantoms, stale pairs and wrong counts, and cannot be steered** | Work through a duplicate queue that shrinks as it is worked                                                                                                                                                                                                      | Duplicate candidates were computed ahead of time into a separate store (`rolodex-contact-duplicates`, still being rebuilt in RB-6467, 2026-02). That store was not updated when records were merged, deleted or edited. Scores were not normalised ("109% probability"). Users could not add a pair by hand, and dismissing a pair failed.                                                                                                                                                                                                                             | RB-451 / RB-452 / RB-467 (2021-12-08, RRCNA, merged records reappear, counts wrong, list incomplete); RB-715 (2022-02-17, "ignore" throws an error); RB-1699 / ZD 4835 (2022-10, AISAP, one contact shows 315 false duplicates); ZD 5754 (2023-03, AIC, listed but no duplicates in the dropdown); ZD 6900 (2023-06, cannot remove potential duplicates, "illegal operation"); ZD 6925 / ZD 8158 (2023-06 and 2023-09, INCOSE, cannot add a duplicate pair by hand); RB-5947 (2025-09-29, SITE, more than 12 contacts listed with no duplicates); RB-6317 / RB-6352 (2025-12 to 2026-01, global duplicates clean-up epic)                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                             | (a) The candidate list is computed from current records: a merged, deleted or edited record leaves or updates its pairs within seconds. (b) Every listed pair can be opened, and the scores are bounded and explained (which fields matched). (c) Staff can create a pair by hand, and "not a duplicate" is saved per pair and never shown again. (d) The count shown equals the number of rows.                                                                                                                                                                                                                                                                                                                                                                                                                                                                   |
| P4  | **System flows create duplicates in the first place**                                       | Have each person or company exist once, whichever way they entered (import, portal sign-up, registration, abstracts, exhibitor portal, social login)                                                                                                             | Each entry path had its own "find or create" logic. Email matching was case-sensitive in one place. Social sign-in did not always carry an email. Imports created a new contact when no match key was given. No query could find "import-created duplicates" afterwards, so the only clean-up was manual.                                                                                                                                                                                                                                                              | RB-794 (2022-03, linking during group registration creates a duplicate); RB-866 (2022-03, abstracts create duplicates because email matching was case-sensitive); ZD 7842 (2023-09-08, ACUHO-I, corporate org rosters suddenly full of duplicates from a data import a year earlier; no query to find them for mass delete); RB-7306 (2026-06, exhibitor portal does not match existing contacts); RB-7375 (2026-07, duplicate portal accounts from Auth0 social sign-in with missing emails); RB-7641 (2026-09, NLA, the initial data load created two contacts and two orgs for one member)                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                         | (a) One match-or-create service is used by **every** entry path (console, portal, import, registration, abstracts, exhibitor, API), with normalised keys (case-folded email, normalised phone and name). (b) A test pushes the same person through each path and ends with exactly one contact. (c) Every record keeps its creation source (import batch, portal, API), and that source can be queried.                                                                                                                                                                                                                                                                                                                                                                                                                                                            |
| P5  | **Contact-to-organization affiliation, "primary" and role rules are not enforced**          | Know which organization a person works for, who the primary or billing contact is, and let roles drive billing, renewal notices, portal access and directories                                                                                                   | The organization name was a free-text field next to an optional `organization_id`. Imports filled in the text without a link, and organizations with the same name could not be told apart. Roles were an array on the contact, with role names copied into other records. "Single holder" roles such as Owner or Billing were advisory only: the "Contact Role Compliance" feature warns but does not prevent. Email-domain auto-linking had no list of free email domains to exclude. Rules by organization type were enforced in the console but not in the portal. | ZD 862 (2021-01, domain match during sign-up); RB-2466 (2023-04, contact roles not queryable); RB-5153 (2025-03, GEO, a role named "string" auto-assigned to all contacts); RB-5273 / ZD 17191 / ZD 17400 (2025, NASBO, mass update of roles fails); RB-5829 / ZD 16777 / ZD 20596 (2025-08 to 2026-07, MSA-CESS and CCCA, role limits by organization type ignored in the portal); RB-5691 / RB-5694 / ZD 16332 / ZD 16361 (2025-07, FBA and MSA-CESS, roles not populating on the portal profile form); RB-5989 / ZD 17379 (2025-10, CLARB, renamed roles stay stale in queries); ZD 18051 (2025-12, domain match does not link); ZD 19166 (2026-03-16, DHI, org shown as plain text, not linked: support found **4,548** contacts with an org name and no org id, and could not fix them because several orgs share a name); RB-7088 (2026-05, FAA, an ISP domain auto-populates an unrelated org); RB-7093 (2026-05, primary contact role missing on org memberships); RB-7662 (2026-09-09, SBAM, **828** orgs with more than one Owner contact, one with 57, so an arbitrary person receives the dues invoice); ZD 21184 (2026-09-17, GEO, several billing contacts appear "somehow"; the workaround was the compliance min/max rule); RB-7874 (2026-10-09, change-affiliation process removes the primary chapter)                                                                                                                                                                                              | (a) An affiliation is its own record (contact, organization, role(s), primary flag, start and end dates). A contact can never show an organization name that is not a linked organization. (b) A role marked single-holder per organization (Owner, Billing, Primary) is enforced by a database constraint in every write path. Assigning a second holder either replaces the first or is rejected. (c) Role cardinality and organization-type rules give identical results in console, portal, import, mass update and API. (d) Renaming a role takes effect everywhere at once, because roles are referenced by id and never copied by name. (e) Domain auto-linking skips free and ISP email domains and only links on an exact verified domain. (f) An import that names an organization ambiguously (two or more matches) reports the row and does not guess. |
| P6  | **Addresses: links, purposes and validation behave differently in each place**              | Use the organization's address as a contact's work address, choose which address is used for mailing and billing, and import addresses                                                                                                                           | "Link to organization address" was a copy set by form hooks, not a live reference, and it did not render in the portal. Bill-to-organization picked the contact's address. Imports rejected valid addresses with a "not allowed" error. Which address counts as "preferred" was not visible to the people building queries.                                                                                                                                                                                                                                            | RB-2317 (2023-04, custom address, relationship and phone types missing from queries); RB-2578 (2023-05); ZD 11663 / ZD 11686 / RB-4038 / RB-4044 (2024-05, TCEA, the link checkbox auto-checks and cannot be cleared); RB-4316 / RB-4389 (2024-07/08, linked org address not shown in the portal); RB-5127 / RB-5417 / RB-5422 (2025-03/04, bill-to-org uses the contact's address); RB-5136 / ZD 14747 (2025-02/03, AAPPR, import rejects valid addresses as "not allowed"); RB-7326 (2026-06) and ZD 20263 (2026-06, still on hold): the same import failure came back; ZD 17774 / ZD 18078 (2025-11/12); ZD 20523 / ZD 20927 (2026-07/08, NEWEA, cannot get the work address into a mailing query); ZD 20792 (2026-08, AFFI, "Address fields - which one is correct")                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                              | (a) A contact address can be a live reference to an organization address, and edits to the organization address show up on the contact everywhere, including the portal. (b) Address purposes (mailing, billing, directory) are explicit settings, and query fields name the purpose they resolve. (c) Import, portal, console and API share one address validator, and an address valid in one is valid in all. (d) Bill-to-organization always uses the organization's billing address.                                                                                                                                                                                                                                                                                                                                                                          |
| P7  | **Queries disagree with the record and break at scale**                                     | Build lists for mailings, integrations (Mailchimp, Higher Logic, Active Campaign) and reports that match what staff see on the record                                                                                                                            | Queries ran against a separately synced reporting copy. Sync lagged ("not updating even 1 hour later") or missed fields. The field catalogue kept fields that had been deleted or never imported. One-to-many outputs (logins, membership status, assigned memberships) multiplied rows. Large output sets timed out (504) or were capped at 10K rows.                                                                                                                                                                                                                 | RB-838 / RB-1014 / RB-1543 / RB-1557 (2022, field "does not exist", table not found); RB-1521 (2022-09); RB-1837 (2022-12, INCOSE, a 16th output field gives a 504); RB-2383 (2023-04, contacts KPI capped at 10K); RB-3774 / RB-5535 (2024-02 / 2025-05, # of Logins duplicates rows); RB-4695 / ZD 12095 (2024, AFFI, contacts not in the final import appear in queries); RB-4742 (2024-10, mass update blocked by duplicate rows); RB-4904 (2025-01, a blank field matches criteria); RB-4999 / ZD 14574 (2025-01, AAPPR, fields that do not exist appear in the field browser); RB-5644 (2025-07, custom field value on the record is blank in the query; WMA too); RB-5812 / ZD 16709 (2025-08, WMA, the reverse); RB-5996 (2025-10, duplicate contacts when membership status is added); ZD 20519 / ZD 20520 (2026-07, queries not updating); ZD 20529 (2026-07, FBA, custom CRM data missing from a membership query); ZD 21262 (2026-09, MMA, "how do I get unique results?")                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                | (a) A query either reads the system of record, or reads a projection whose lag is bounded (seconds) and shown in the UI. A test edits a record and sees the change in a query within that bound. (b) Results are one row per entity by default, and one-to-many fields are aggregated or listed, never multiplied, unless the user asks for "one row per child". (c) The field catalogue lists only fields that exist now, deleted fields disappear, and renamed configuration shows the new name. (d) A query with at least 50 output fields over the largest tenant finishes inside a stated time budget, with no fixed row cap. Export is paged or streamed.                                                                                                                                                                                                    |
| P8  | **Universal search misses or buries exact matches**                                         | Type a contact or organization number, name or email and land on the record                                                                                                                                                                                      | The search index was a separate copy updated asynchronously and repaired by manual reindexing. Fuzzy matching was tuned so loosely that exact number matches ranked low. Merged records stayed in the index.                                                                                                                                                                                                                                                                                                                                                           | RB-399 (2021-11, both merged and current records shown); RB-2658 (2023-06, CMA, merged orgs still searchable); RB-3957 (2024-04); ZD/RB-4596 (2024-10, ACG sandbox); RB-5331 (2025-04); RB-5847 (2025-08, search by number gives a blank result); RB-6404 (2026-02); RB-6555 (2026-03, ACG); RB-7123 / RB-7125 / RB-7126 / RB-7148 / RB-7156 (2026-05, ASAM, ISAC, AMPRA, SBAM, active orgs not found by number or name); RB-7325 (2026-06, ISACS); RB-7582 (2026-08, AIS, an exact org number match buried below unrelated orgs)                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                     | (a) An exact match on record number, email or full name always ranks first. (b) A created, edited, merged or deleted record shows up correctly in search within a stated bound (seconds), and no manual reindex is ever needed. (c) Merged and deleted records never appear in results.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                            |
| P9  | **Imports report success but partly or silently fail**                                      | Load and refresh contacts, organizations, custom fields, roles and links in bulk                                                                                                                                                                                 | Imports ran through "custom data tasks" with their own validation, separate from the UI. The preview did not run the real write. Rows could only be matched by id or email. Large files timed out. Some fields (picklists, positions, addresses) were dropped without any message.                                                                                                                                                                                                                                                                                     | ZD 1251 / ZD 1811 / ZD 1925 / ZD 2140 (2021, picklist values not written, code vs description); ZD 2035 (2021-07, contacts without organizations); ZD 3518 (2022-03, CREP, "successful" but no records); RB-1435 (2022-08, INCOSE, imported contacts missing from queries); ZD 6806 (2023-06, AGPA, truncation); RB-3814 (2024-02, false success); RB-3815 / RB-3813 (2024-02, mapping lost); RB-4627 (2024-10, more than 900 rows times out); RB-4301 (2024-07, contact id not recognised); ZD 14032 (2024-12, TCEA, a custom field never imports); ZD 16998 / ZD 17153 (2025-09, adding organization to contact via import); RB-7153 (2026-05, MMA, needs license number as the match key); ZD 20989 (2026-09, CVMA, "unable to do import for mass update")                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                         | (a) The dry-run preview runs the same validation and write code as the real run, and the real run's per-row results equal the preview's. (b) Every row ends as created, updated, skipped or failed, with a reason, and the total of those outcomes equals the number of input rows. (c) The match key is configurable (id, email, or any unique custom field). (d) Imports and UI share one validator. (e) A 100k-row file runs as a background job without timing out.                                                                                                                                                                                                                                                                                                                                                                                            |
| P10 | **Portal self-service of a profile and an organization's roster fails**                     | Members update their own profile; organization administrators manage their roster, including child organizations                                                                                                                                                 | Portal permissions were checked separately from console permissions. Roster pages loaded every contact in one call, with no paging. A dangling reference elsewhere (a deleted org, a merged contact) blocked the whole profile save.                                                                                                                                                                                                                                                                                                                                   | ZD 1702 (2021-05); ZD 4026 (2022-07, institutional reps cannot see their org's contacts); ZD 7364 (2023-08, TLA, ServerError saving profile); ZD 10268 (2024-01, ACUHO-I); ZD 10331 (2024-01, Access Denied linking an org); ZD 12806 (2024-08, ACN, roster page blank); ZD 14584 (2025-01); RB-5131 / RB-5222 / RB-5743 (2025-03/08, cannot add or remove contacts); RB-5646 / RB-6617 / RB-6986 (2025-07 to 2026-04, MSA and DHI, child orgs not editable when logged in to the portal); RB-5630 (2025-07, Agile Alliance, roster of 614 member contacts shows none); ZD 21085 (2026-09, IDEC, profile save blocked by ForeignKeyViolation to a non-existent org membership)                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                        | (a) Portal and console share one authorisation model: an organization administrator role, granted through an affiliation, covers that organization and its descendants. (b) Roster screens are paged and work at 10,000 affiliations. (c) Saving a profile never fails because some _other_ record holds a broken reference (see P1c).                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                             |
| P11 | **Region assignment is wrong, blank or too slow to recalculate**                            | Assign members to regions or areas from their address, with ordered rules and a fallback                                                                                                                                                                         | Regions were set by a calculated custom field or region rules evaluated per save. A missing fallback rule crashed. Recalculation ran synchronously and returned 504s. Rule order could only be changed by clicking each rule up or down.                                                                                                                                                                                                                                                                                                                               | ZD 9321 / ZD 9838 / ZD 9931 (2023-11/12, ACUHO-I, calculated region not populating); RB-4127 (2024-05, geographic regions support); ZD 13490 (2024-10-18, TCEA, out-of-state members get numeric regions, others blank); RB-4802 / RB-4858 (2024-11/12, NVFC and TCEA, recalculation errors and 504s); RB-4162 (2024-06); ZD 16713 (2025-08, MSA-CESS, no sort order for rules); RB-7290 (2026-06, TypeError with no "rest of the world" rule)                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                        | (a) Every address change re-evaluates the region rules in a fixed priority order and always lands on a region, because a default rule is required. (b) Recalculating every record runs as a background job with progress, for any tenant size. (c) Rules can be re-ordered directly, for example by drag or by number.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                             |
| P12 | **Directories leak, mislabel or ignore status**                                             | Publish member, organization and contact directories that respect opt-outs and status                                                                                                                                                                            | Directory templates were user-editable Nunjucks with no validation. Opt-out options were rendered from internal field ids. Status filtering was left to each template.                                                                                                                                                                                                                                                                                                                                                                                                 | RB-3849 (2024-03, radius search outside the US); RB-4565 / RB-4914 (2024-09 / 2025-01, opt-outs show field ids); RB-5359 (2025-04, messages not sent to all primary contacts); RB-7452 / RB-7559 (2026-07/08, default template fails to render); ZD 19067 (2026-03, roles in the directory not updated); ZD 20011 / ZD 20770 (2026-05/08, ACG, directory security vulnerability); ZD 20235 (2026-06, ACG, inactive members appear); ZD 20370 (2026-07, state filter)                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  | (a) A directory is a read-only projection that applies opt-outs and the configured status filter **before** any template runs. (b) Templates are checked when saved. (c) Labels come from configuration names, never ids. (d) A test proves an opted-out or inactive record never appears.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                         |
| P13 | **Activities and tasks are second-class**                                                   | Log interactions and staff tasks on a record and filter them                                                                                                                                                                                                     | Activities were a late add-on (Tasks support, RB-3943, arrived in 2024-04). Activity types defined in Setup were not usable as filters. The UI did not refresh after a write. Activity imports timed out.                                                                                                                                                                                                                                                                                                                                                              | ZD 2504 (2021-09, org query with activities); ZD 5301 (2023-01, activities tab not loading); RB-3894 / RB-3917 / ZD 11098 (2024-03, types and system actions not filterable); RB-4301 (2024-07); RB-4592 (2024-10, a new activity appears only after a refresh); RB-4627 (2024-10, activity import times out); ZD 17388 (2025-10, AHRA, activity import breaks custom fields)                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                         | (a) An activity or task appears on the record as soon as it is saved. (b) Every configured activity type and system action can be used as a filter and as a query field. (c) Activity import follows P9's rules.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                   |

#### Themes that run through all patterns

1. **Ids copied into other services, with no place to re-point them.** Patterns P1, P5, P10 and P12 all come from Rolodex ids being stored in other services' DynamoDB tables with no list of who holds them. The 2026 tickets (RB-7337, RB-7378, RB-7641) say so directly: "merge doesn't propagate the surviving identifier to dependent systems". The 2.0 design needs a registry of every place that refers to a contact or organization, and that registry should drive merge, delete and the integration tests.
2. **Copies that drift from the source.** Patterns P3, P7, P8 and P12 each read from a secondary copy: the duplicates store, the query store, the search index, the directory. Those copies drifted from the records with no visible lag and no automatic repair. Every projection in 2.0 needs a stated freshness bound and a test that proves it.
3. **Each way in had its own rules.** Patterns P4, P5, P6 and P9 come from console, portal, import, mass update and API each applying their own validation, matching and role rules. In 2.0, one domain service should own each rule, and every channel should call that service.
4. **Failures customers could not see.** The worst incidents (RB-7337, RB-7641, ZD 3518, RB-3814) were silent: the screen said "success", or the customer saw nothing, while the failure sat in a DLQ. In 2.0, every partial failure must reach staff in the UI with a reason.

### Notes by source

#### Jira (rhythmsoftware.atlassian.net)

- Projects visible to this account: DSC, ESCALATION, OPS, PI, RB, RPS, RX. All CRM evidence came from **RB (Rhythm R&D)**, where support escalates engineering bugs. Every RB ticket uses the same template (issue to be solved, Loom video, repro steps, customer expectation), so reading the full description of a representative set was enough to understand each pattern.
- JQL searches run (RB, plus ESCALATION, which returned no CRM hits): merge/duplicate (more than 100 hits, read 2021–2023 ascending and 2025–2026 descending); relationship/employer/primary/affiliation/rolodex; contact or organization × address/email/phone/custom field/search/portal/profile/role/directory/region/activity/task/delete; relationship/region/directory/activity/task/universal search/import before 2025-02; custom field or query × contact/organization/CRM.
- Read in full: RB-22, RB-24, RB-385, RB-451, RB-1837, RB-1879, RB-2317, RB-3476, RB-4389, RB-5136, RB-5630, RB-5644, RB-5947, RB-6317, RB-6352, RB-6467, RB-7088, RB-7337, RB-7378, RB-7380, RB-7582, RB-7641, RB-7662, RB-7874. The rest were triaged from the summary line only.
- Many RB hits for "merge" are about email _merge fields_ (template placeholders), not record merge. Those were excluded.
- "Employment" hits are almost all CLARB-specific employment-history custom objects (RB-5551…RB-7097). They are a custom-object feature, not CRM affiliation, so they were excluded from P5. They are still worth noting for the custom-objects design: verified employment history became a big contracted build.

#### Zendesk (rhythmsoftware.zendesk.com)

- Zendesk is still in active use: tickets run from 2020-09 to 2026-10-19, and several CRM tickets are open today (ZD 21001 organization contact roles, ZD 21499 INCOMPAS's own org missing from the contact-form dropdown, ZD 21365 "Not allowed to update address"). The note that "support moved to HubSpot" was not confirmed by any data source (see HubSpot below).
- Rough hit totals, given as context only: "duplicate merge contact" 183; subject "import contacts" 72; subject "query" 1,246; subject "contact role" 108; subject "address" 265; subject "custom field" 168; subject "directory" 261; "merge organization" 65; activity/region 44; profile/universal search 40.
- Read in full, with comments where shown: 4131, 7842 (comments), 19166 (comments), 21184 (comments), 14032, 20523, 21085, 6925, 16713, 13490, 21262, 14574. The rest were triaged by subject.
- Three comment threads matter most. On 7842 (ACUHO-I, 2023), support found the duplicates came from a data import, and the customer had no way to query for them before a mass delete. On 19166 (DHI, 2026), support found 4,548 contacts with an org name but no org id, and could not repair them because several organizations share the same name. On 21184 (GEO, 2026), the only fix offered for multiple billing contacts was a compliance warning rule, not enforcement.

#### HubSpot (Service Hub tickets)

- `search_crm_objects` with objectType TICKET returned `{"results":[],"total":0}` three times at about 16:39–16:40 ET: with query "merge duplicate contact", with query "contact", and with no query at all. The connected HubSpot portal either has no ticket records or this connector cannot see them. No HubSpot evidence is claimed.

#### Linear (linear-rhythm)

- `list_issues` returned `{"issues":[],"hasNextPage":false}` for query "contact", for query "CRM", and with no filter at all (limit 10), at about 16:39–16:40 ET. The connected workspace exposes no issues to this account. No Linear (2.0 CRM) evidence is claimed. The 2.0 Jira project RX was visible but was not searched for this file, which covers 1.0 customer pain.

#### GitHub (org rhythmsoftware)

- `gh` was authenticated as andrewryan1906. `rolodex-contacts` has issues enabled but **zero issues**. `gh issue list` returned nothing for rolodex-contacts, rolodex-organizations, rolodex-relationships, rolodex-ngx-console and rolodex-query-definitions.
- `rolodex-contacts` has only 9 PRs (2026, e.g. #9 "fix(RB-7201): never answer 404 once a contact has been found"). The local clones have rewritten or squashed history: rolodex-contacts has 1 commit (2026-09-29) and rolodex-organizations has 1 commit (2026-07-23). A `rolodex-contacts.bfg-report` directory suggests a BFG history rewrite. rolodex-relationships has 20 commits from 2022-07, with nothing relevant. GitHub therefore holds no meaningful record of customer pain. Jira RB is the engineering record.

### Access log (2026-10-19, Eastern Time)

| Time (ET)   | Tool                                         | Call                                                                                                                                                                           | Result                                                                                                                |
| ----------- | -------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------- |
| 16:37       | Atlassian `getAccessibleAtlassianResources`  | list sites                                                                                                                                                                     | OK: cloudId fdafd06f-…; read:jira-work available                                                                      |
| 16:37       | Zendesk `search_tickets`                     | "duplicate merge contact"                                                                                                                                                      | OK: 183 hits                                                                                                          |
| 16:37       | Atlassian `getVisibleJiraProjects`           | browse                                                                                                                                                                         | OK: 7 projects                                                                                                        |
| 16:37–16:38 | Atlassian `searchJiraIssuesUsingJql`         | 7 JQL searches                                                                                                                                                                 | OK. Results were too large to return inline, so they were saved to the session's tool-results files and read with jq. |
| 16:38       | Zendesk `search_tickets`                     | relationship/employer/primary; import; query; contact role; address; custom field; organization link; directory; activity/region; profile/universal search; merge organization | OK. The free-text "relationship employer primary" returned only 1 hit; subject searches worked.                       |
| 16:38–16:39 | Zendesk `get_ticket` / `get_ticket_comments` | 12 tickets, 3 comment threads                                                                                                                                                  | OK                                                                                                                    |
| 16:39–16:40 | HubSpot `search_crm_objects` TICKET          | 3 searches                                                                                                                                                                     | Returned 0 records each time (see HubSpot notes)                                                                      |
| 16:39–16:40 | Linear `list_issues`                         | 3 calls                                                                                                                                                                        | Returned 0 issues each time (see Linear notes)                                                                        |
| 16:39–16:40 | `gh` CLI, `git log`                          | issues and PRs on 5 rolodex repos; local history                                                                                                                               | Issues empty; PRs 9 on rolodex-contacts; history squashed                                                             |

## 26. Review R1: Rhythm 2.0 capabilities the CRM architecture overlooks or understates

## Review R1: Rhythm 2.0 capabilities the CRM architecture overlooks or understates

Reviewer: independent read-only review, 2026-10-19. Scope: capabilities that **already exist** in Rhythm 2.0 and that
RHYTHM-CRM-ARCH-001 omits, understates, or marks Missing / Partial / Backend only / Simulated / Scaffolded. Every finding
below was verified in code; nothing is inferred from documentation alone.

Commits read:

- services-crm `origin/master` **`bb55ae7a`** (the document's baseline is `a980b553`; findings 1–15 were already true at
  `a980b553` unless marked "since baseline").
- react-console `origin/master` **`057ee80fc`** (no CRM console code changed since the document's `1f345862b`).
- services-platform `master` **`339fb04f1`**; macroservice-libraries `origin/master`.

Paths without a prefix are services-crm. `C:` = react-console, `P:` = services-platform.

### A. Rows whose status is wrong or understated

| #   | Finding                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                 | Document location                                                                                                       | Evidence (path:line @ commit)                                                                                                                                                                                                                                                                                                                                                                                                                                                 | Correction needed                                                                                                                                                                                                                                                                                                     |
| --- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 1   | **CRM already has a full tenant-export provider.** Every CRM tenant resource is classified for export; 28 dataset declarations with streaming readers are attached to the tenant-lifecycle participants and answer `export`; the unit tier asserts coverage against the database tables and the object registry and pins each declaration's hash; an integration spec runs the export. Landed in `f0c43674`/`ee2ca86f`, both inside the document's own baseline.                                                                                                                                                                                                                                                                                        | 14 row **DO-12** ("none", Missing (export paused)); 10 §6 ("needs a CRM export provider … when that work resumes")      | `aws/crm.export-datasets.ts:1-13` (header) and imports `:14-40` @ bb55ae7a; `modules/tenant-lifecycle/services/export/dataset-exports.ts`; `modules/tenant-lifecycle/services/persistence/tenant-exports.ts:1-20`; `modules/tenant-lifecycle/services/participants/entities-participant.ts:37-38`; `modules/tenant-lifecycle/tests/unit/participants/export-coverage.spec.js`; `modules/tenant-lifecycle/tests/integration/services/export/tenant-export.integration.spec.js` | DO-12 → **Complete (backend)**; remaining gap is only Platform's paused orchestration (`P:modules/data-exports`) and the dangling-reference note. Rewrite 10 §6. Remove from M8 work.                                                                                                                                 |
| 2   | **Bulk updates and imports already trigger duplicate detection.** Detection is a change-event handler on the contact and organization streams; change events are written by the commit-time settle trigger on every committed write, so a bulk update through the persistence service is re-keyed and re-scored like a console edit. The handler's "skip import events" seam is inert because no event carries a job id ("every event is processed normally"). `docs/data-jobs.md:770` ("No duplicate detection request is written") is stale against `docs/data-jobs.md:289` ("an import does … the duplicate match keys") and the handler.                                                                                                            | 14 row **DQ-08** (Missing, cites `docs/data-jobs.md`); 10 §4 ("including duplicate detection requests (missing today)") | `modules/duplicates/services/detection/duplicate-change-events.ts:1-24, 144-155` @ bb55ae7a; `aws/crm.change-events.ts:48-49, 75, 86`; settle trigger on `crm.contacts` in `db/migrations/0076_snake_case_switch.sql:717`                                                                                                                                                                                                                                                     | DQ-08 → **Complete (verify)**: add one integration test (bulk update of an e-mail produces a candidate pair) and fix the stale line in `docs/data-jobs.md`. The real open item is the opposite one: a deliberate end-of-job set-based pass instead of per-event scoring for large imports.                            |
| 3   | **Affiliations and their roles already import with the contact.** Contact insert/upsert (and update) carry `affiliations[]` with nested `roles[]`; each affiliation's organization and each role are resolved through the source-key crosswalk, set-based, and re-resolved inside the item's transaction. The initial-import guarantees spec loads "affiliations" in a whole synthetic package.                                                                                                                                                                                                                                                                                                                                                         | 14 row **DO-07** ("none", Missing, H); 10 §3 ("Affiliation and role import … are new operations")                       | `modules/data-jobs/services/imports/contact-references.ts:1-8, 45-82, 101-120` @ bb55ae7a; `docs/data-jobs.md:766` (initial-import-guarantees spec loads affiliations)                                                                                                                                                                                                                                                                                                        | DO-07 → **Partial**: nested affiliation/role import on the contact exists. What is missing is a standalone affiliation-row operation (one spreadsheet row per affiliation against existing contacts) and its mapper. Lower severity to M.                                                                             |
| 4   | **The Platform layouts/forms service is built, CRM stores and resolves assignments, and the console reads and writes them.** Platform `modules/layouts` has four definition resources (console/portal page layouts, console/portal forms) and an assignment registry. CRM contact types, organization types and `crm_settings` carry the five slot ids; `GET /effective_definitions/{t}` resolves type-vs-tenant-default precedence. The console's "Forms & Layouts" section edits the five assignments for contact **and** organization types against those Platform lists, and the create screens resolve their form through `effective_definitions`. Only the **page-layout designer** (localStorage) and the **Record Layouts list** are simulated. | 14 rows **UI-03**, **UI-04** (Simulated; Work "Platform layouts"); 09 §7; 03 table row "360 pages"; 15 M3               | `P:modules/layouts/aws/api/*.routes.ts`, `P:docs/layouts.md` §1 @ 339fb04f1; `modules/entities/db/layout-slots.ts:35-39`, `modules/entities/db/crm-settings.table.ts:32-41`, `modules/entities/aws/api/effective-definitions.routes.ts:1-10, 34` @ bb55ae7a; `C:lib/configuration/layout-definitions.ts:1-40`, `C:components/configuration/layout-assignments-section.tsx:1-30`, `C:lib/forms/resolve-create-form.ts:1-12, 57-73` @ 057ee80fc                                 | Split UI-03/UI-04: assignment and resolution = **Complete**; console page-layout designer persistence to `/console_page_layouts` and rendering of the published layout on the 360 = remaining gap. The Work cell should say "wire the existing designer to Platform `/console_page_layouts`", not "Platform layouts". |
| 5   | **Organization lookup by domain is mostly built.** The organization save derives `domain_name` from `website` (and back) with legacy normalisation; organizations carry `alternate_domain_names`; duplicate detection has an `org_domain` match key and domain evidence; the public/free-mail domain exclusion list the document lists as work already exists. A draft check (`POST /duplicates/{t}/organizations/checks`) already finds organizations sharing a domain.                                                                                                                                                                                                                                                                                | 14 row **OR-05** (Missing; Work "Route + service function, public domains excluded"); L1-06; 05 §2.2                    | `modules/entities/db/organization-domain-name.ts:1-60` and `modules/entities/tests/e2e/organizations-domain-name.e2e.spec.js` @ bb55ae7a; `modules/duplicates/services/matching/free-mail-domains.ts:1-13`; `modules/duplicates/services/matching/fingerprint.ts:40`; `modules/duplicates/aws/api/duplicates.routes.ts:98, 538-540`; `modules/entities/metadata/organizations.object.ts` (exclude `alternate_domain_names`)                                                   | OR-05 → **Partial**: normalisation, exclusion list and an indexed domain key exist; remaining work is an exact-domain lookup route/service function over `domain_name` + `alternate_domain_names`. Drop "public domains excluded" from Work (done).                                                                   |
| 6   | **Server-side organization name search already exists.** `GET /organizations/{t}?search=` (and `GET /contacts/{t}?search=`) filter by name, paged. The console picker still loads up to 2,000 rows because its header comment (written before the param) says there is no search.                                                                                                                                                                                                                                                                                                                                                                                                                                                                       | 14 row **UI-14** (Partial; work implies backend) ; 09 §6 ("today: organizations up to 2,000")                           | `modules/entities/aws/api/organizations.routes.ts:136-142` (query `['limit','cursor','fields','search']`) and `modules/entities/services/persistence/organizations.ts:598-624` @ bb55ae7a (also at a980b553); `C:lib/record-pickers/crm-adapter.ts:4-6` (stale comment) @ 057ee80fc                                                                                                                                                                                           | UI-14 Work → console-only: switch the organization picker to `search=` (or the `crm.organizations` dataset, as the contact picker already does). Note ILIKE is unindexed under RLS — add a scale test.                                                                                                                |
| 7   | **Server-side user search exists for the activity owner picker.** Platform `console_users` takes `search`, `limit`, `cursor`; the console hook already sends `search` but pins it to `""` with `limit: 100`.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                            | 14 row **AC-06** (Partial; Work "server-side user search")                                                              | `P:modules/security/aws/api/console-users.routes.ts:86-88` @ 339fb04f1; `C:components/activities/use-activities.ts:48`, `C:lib/security/hooks.ts:102` @ 057ee80fc                                                                                                                                                                                                                                                                                                             | AC-06 Work → console-only: type-ahead over the existing search param.                                                                                                                                                                                                                                                 |
| 8   | **Built-in CRM policies partly exist.** Platform generates tier policies for every app from its permission catalog (`policy:crm:read-only`, `:user`, `:administrator`); CRM's catalog is tiered for exactly this. Only _declared_ job policies (Contacts Editor, Data Steward) are missing.                                                                                                                                                                                                                                                                                                                                                                                                                                                             | 14 row **SE-05** ("none", Missing); 11 §2 table; 03 "Security" row ("no built-in policies")                             | `P:docs/security/built-in-policies.md:16-23` @ 339fb04f1; `aws/crm.service.ts:7-11` @ bb55ae7a                                                                                                                                                                                                                                                                                                                                                                                | SE-05 → **Partial**: Read Only and CRM Administrator are already the generated tier policies; remaining = declared Contacts Editor and Data Steward (merge + bulk).                                                                                                                                                   |
| 9   | **The organization 360's Contacts tab is already paged and editable.** It renders the paged affiliations API (current/past/all, as-of date, type and role filters, URL-held paging) with add / edit / end affiliation dialogs. Only the hierarchy tree and the overview contacts card use the 500-row scan.                                                                                                                                                                                                                                                                                                                                                                                                                                             | 14 row **UI-15** (Partial, cites scan); 09 §5                                                                           | `C:components/console/organization/organization-tab-panel.tsx:160-164`, `C:components/console/organization/organization-affiliations-section.tsx:28-34` @ 057ee80fc; backend `modules/entities/aws/api/organizations.routes.ts:168-195`                                                                                                                                                                                                                                       | Narrow UI-15 to hierarchy and overview card; record organization-side affiliation editing as Complete.                                                                                                                                                                                                                |
| 10  | **Organization editing is not toast-only.** The live console changes an organization's type with a version-checked JSON Patch (`test /sysVersion` + `replace /typeId`), which drives compliance recalculation.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                          | 14 row **OR-02** ("No edit route or form", cites toast-only); 09 §5 actions                                             | `C:lib/organizations/update.ts:4-11, 21-60`, `C:components/console/organization/organization-screen.tsx:14` @ 057ee80fc                                                                                                                                                                                                                                                                                                                                                       | Keep severity, but say "only Change Type is live; no general edit form".                                                                                                                                                                                                                                              |
| 11  | **The manual "add a record to a duplicate group" picker is built.** The group inspector offers a record picker and calls `POST /duplicates/{t}/{type}/candidates` (`markAsDuplicates`) against the live CRM adapter; the merge workspace accepts a picker too. It is wired only on the `/design/dataQuality` preview routes, not on the console queue screen.                                                                                                                                                                                                                                                                                                                                                                                           | 14 row **MG-04** (Missing (approved))                                                                                   | `C:components/duplicates/data-quality/group-inspector.tsx:333-345, 466-473`, `C:lib/duplicates/crm-adapter.ts:593-597`, `C:components/duplicates/merge/merge-workspace.tsx:175-182`, `C:app/(console)/design/dataQuality/start/page.tsx:61` @ 057ee80fc; backend `modules/duplicates/aws/api/duplicates.routes.ts:424-426`                                                                                                                                                    | MG-04 → **Partial (built, unwired)**: pass `renderRecordPicker` in `C:components/console/duplicates/duplicate-queue-screen.tsx`; still need the single-record entry (D-03).                                                                                                                                           |
| 12  | **Portal foundations exist that the PO rows call "none".** Platform signs the portal user's linked contact as claim `cid` (`ctx.caller.contactId`) via portal contact-match; CRM already serves two portal routes (contact roles read); the staff route for contacts of an organization tree (`/contacts/{t}/descendants/{org}`) is built; the console has a working portal shell with an acting-as-member stand-in.                                                                                                                                                                                                                                                                                                                                    | 14 rows **PO-01..PO-04** (2.0 evidence "none")                                                                          | `P:docs/security/portal-contact-claim.md` @ 339fb04f1; `modules/entities/aws/api/contact-roles.routes.ts:78, 120` (`RouteAccess.Portal`), `modules/entities/aws/api/contacts.routes.ts:222-243` @ bb55ae7a; `C:app/(portal)/portal/[tenantId]/layout.tsx`, `C:lib/portal/member.tsx:1-20` @ 057ee80fc                                                                                                                                                                         | Fill the 2.0 evidence cells; PO-03 work is "portal-access the existing descendant query, paged", not new. Status stays Missing for the member-facing features.                                                                                                                                                        |

### B. Built 2.0 capabilities the document never mentions

| #   | Finding                                                                                                                                                                                                                                                                                                                                                                                                                             | Document location                                                           | Evidence (path:line @ commit)                                                                                                                                                                                                      | Correction needed                                                                                                                                                                                     |
| --- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 13  | **CRM contributes 13 eligibility condition types** to Platform's eligibility registry (contact status/type/age, organization type/status, address country/state/postal code, affiliation role/active/organization, custom field, has-portal-user), with as-of temporal support and portal-explanation sensitivity; registered on deploy (`npm run eligibility:register`). `crm.contact.hasPortalUser` is a stub (no fact provider). | Absent from 05, 06, 12, 14 (only "hierarchy cannot be used in eligibility") | `aws/crm.eligibility.ts:1-25, 143-389` @ bb55ae7a (present at a980b553); `package.json:38`                                                                                                                                         | Add a gap-matrix row (Complete, with gaps: `hasPortalUser` provider missing; no hierarchy-aware type, ties to OR-03/CRM-ADR-06) and list it in 06 §3/12 as a published contract other apps depend on. |
| 14  | **Console contact-form designer is live and persisted** (FormEngine designer saving to Platform `/console_forms`; list / new / edit screens; definitions validated to refuse stored code), and the resolved create form is rendered on Contact/Organization create.                                                                                                                                                                 | No 14 row (09 §6 mentions the registry; only e04 §B lists it)               | `C:lib/forms/designer/customization.ts:1-25`, `C:components/forms/management/contact-forms-list.tsx`, `C:app/(console)/console/[tenantId]/configuration/[module]/forms/console/contacts/{page,new/page,[id]/page}.tsx` @ 057ee80fc | Add a row (Complete for contact console forms; Missing: organization forms designer, portal forms screen is inert by design `…/forms/portal/contacts/page.tsx:11`).                                   |
| 15  | **Managed files for CRM records** (contact photo, organization logo, `image`/`file` custom fields of contacts, organizations and contact roles, activity attachments) go through the shared asset pipeline with a cleanup function; imports copy URLs/uploads through it.                                                                                                                                                           | Absent from 04, 05, 14 (logo appears only as a column)                      | `modules/entities/metadata/asset-objects.ts:1-25` @ bb55ae7a; `modules/entities/aws/logic/entities-asset-cleanup.lambda.ts`; `docs/data-jobs.md` "Records with files"; libraries `packages/assets`                                 | Add a row (Complete backend; console attachments on activities missing per e04; bulk lane cannot load logos per `docs/data-jobs.md`).                                                                 |

### C. Landed on services-crm master since the document's baseline (`a980b553..bb55ae7a`)

These rows are "In M0"/"in progress" in chapter 14 but the service half is on master now. Console halves may still be open.

| #   | Rows                                                                                                                           | Landed commit(s) @ bb55ae7a                                                                                                            | Correction needed                                                                                    |
| --- | ------------------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------- |
| 16  | CO-05 (D-19 reciprocal spouse)                                                                                                 | `74158f19`                                                                                                                             | Service half Complete                                                                                |
| 17  | AF-06, CM-04 (D-46 automatic roles follow effective affiliations)                                                              | `d875cf22`                                                                                                                             | Service half Complete; CM-04 → Complete once imports verified (imports call the same persistence)    |
| 18  | AC-07 (D-48 baseline reconciliation on deploy)                                                                                 | `0d5ade91`, `9da94a8e`, `67407bcf`                                                                                                     | Complete                                                                                             |
| 19  | AC-03 (D-49 activities Explorer recipe)                                                                                        | `9348def1` (`modules/activities/queries/recipes/activities-all.recipe.json`)                                                           | Recipe Complete; version-history part already Complete (activity stream keeps history)               |
| 20  | DQ-02 (D-34 idempotent dismissal, D-47 stale pairs retired)                                                                    | `be34f658`, `02a1018b`                                                                                                                 | Complete                                                                                             |
| 21  | DQ-03 (D-32 block rules + override permission), DQ-06 backend                                                                  | `859d673c`, `81775b59`, `2bb6a828` (organization create held to block rules, `crm.organizations.create-despite-duplicate-block`)       | Service Complete for contacts and organizations; console "Create Anyway" and org-create check remain |
| 22  | MG-02 child routes (D-20 410 on every route of an absorbed record)                                                             | `36ceb699`                                                                                                                             | Complete                                                                                             |
| 23  | MG-05 (D-30 field-level merge outcome)                                                                                         | `03bbc71d`                                                                                                                             | Complete (service)                                                                                   |
| 24  | MG-06 (D-31/D-33 approvals bound to ids+versions)                                                                              | `4c855bf4`                                                                                                                             | Complete (service)                                                                                   |
| 25  | MG-07 (D-21/D-45 merge history readable without merge permission)                                                              | `ab22aac0`                                                                                                                             | Complete (service)                                                                                   |
| 26  | SE-06 (D-08 audited lock/unlock; `/lock`, `/unlock`, `/lock_changes` on contacts, organizations, contact roles; repair script) | `512ba8f3`, `2cd101e5`, `01993cec`, `41bf92db`, `f5a94f78`, `bb55ae7a`; routes `modules/entities/aws/api/record-locks.routes.ts:56-90` | Complete (service)                                                                                   |

### D. Incidental: one row overstates what exists (outside this review's main scope, verified)

| #   | Finding                                                                                                                                                                                                                           | Document location                       | Evidence                                                                                                  | Correction needed                                                                                                           |
| --- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------- | --------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------- |
| 27  | **The backend cannot create or delete duplicate rules.** The rules API is list (`GET`) and update (`PUT`) only; three default rules are seeded per tenant. "Backend only" implies the API exists and only the console is missing. | 14 row **DQ-05** (Backend only); e04 §B | `modules/duplicates/aws/api/duplicates.routes.ts:103-104, 623-643` @ bb55ae7a (no POST/DELETE on `RULES`) | DQ-05 → **Missing** (service + console); ties to `MAX_DUPLICATE_RULES` in `modules/duplicates/db/duplicate-rules.table.ts`. |

### Checked and confirmed as stated (no correction)

OR-03 (no ancestor relation in `organizations_for_queries`), OR-04 (no organization e-mail; organization phones have no
preferred flag), CI-06 (free-text `communication_type_id`, no table anywhere), TK-02 / HI-01 (history installed only for
contacts and activities), TK-04 (`deleteTask` unused), TK-07, DQ-04 (rescan POST exists, console only lists), CM-03,
SR-04 (search page reads `lib/console-data`), CF-01 (mock adapter), UI-01 (`NEXT_PUBLIC_CRM_API`, `_COMMERCE_API`
still read), EV-01 (`crm.contact.merged` only), X-04 (`tasks` only service function), X-07 (no Membership handler on
the change-event streams), LC-01 (archive not on master).

## 27. Review R2: Rhythm 1.0 CRM functionality missing from 2.0 and from this document

## Review R2: Rhythm 1.0 CRM functionality missing from 2.0 and from this document

Independent review of [RHYTHM-CRM-ARCH-001](#1-rhythm-crm-arch-001-rhythm-2-0-crm-definitive-architecture-and-completion-plan) (README, chapters 01–18, evidence e01, e02, e06), 2026-10-19.

**Question asked.** What did Rhythm 1.0's CRM (Rolodex, its shared controls, and the Platform and partner services that
served it) let customers do that Rhythm 2.0 does not do, and that the document neither lists as a gap (chapter 14,
including the L1 rows) nor drops or puts out of scope on purpose? Also: where does the document describe 1.0 wrongly?

**Sources read.** 1.0: `~/rhythm/` (rolodex-_, rolodex-ngx-_, libraries-rolodex-_, platform-tasks, rolodex-query-definitions,
system-ngx-console, system-mass-update-templates, partners-_, sales-*), plus `gh` for repos that are not local
(`rhythmsoftware/rolodex-settings`, `rolodex-organization-duplicates`, `rolodex-training-exercises`). 2.0: services-crm
`origin/master` `bb55ae7a`, react-console `origin/master` `057ee80fc`, services-platform `origin/master`. Paths prefixed
`C:` are react-console, `P:` services-platform, `CRM:` services-crm; unprefixed paths are under `~/rhythm/`.

**What is already well covered (not repeated below).** Relationships, directories (including comments, ratings, messaging,
radius search: CRM-Q4), engagement scoring and its maintenance actions, portal self-service and the role-permission matrix,
domain lookup, impersonation and password reset, FullContact enrichment, marketing e-mail activity, region rules and
recalculation, compliance reprocess, task queues, task reminders (TK-07), tasks from product templates (Accounting
`product_task_templates`, verified present), 360 designer (UI-03/04), contact role rule sets.

### 1. Missing and not accounted for

Severity uses chapter 14's scale (C, H, M, L). "Covered?" says where the document touches the subject, if anywhere.

| #     | 1.0 capability (what it did for the customer)                                                                                                                                                                                                                                                                                                                                                                                                                     | 1.0 evidence                                                                                                                                                                                                                                                                                                                                 | 2.0 state (evidence)                                                                                                                                                                                                                                                                                                                                                   | Covered in document?                                                                                                                                                                                                         | Recommended addition                                                                                                                                                                                                                                                                                                                                                                                      |
| ----- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| R2-01 | **Contact communication-preferences query.** Staff built mailing lists from "who opted in or out of communication type X" (one row per contact per type, with `email_opt_out`, subscription status).                                                                                                                                                                                                                                                              | `rolodex-query-definitions/src/definitions/contact-communication-preferences.js:9-11,61-76`                                                                                                                                                                                                                                                  | Preferences are stored (`CRM:modules/entities/db/contact-communication-preferences.table.ts`) and exported in the tenant export (`CRM:modules/entities/export/contacts.dataset.ts:28,47`) but are **not queryable**: `CRM:modules/entities/queries/contacts.query.ts` declares no preferences collection; only `communication_preferences_last_confirmed` is a column. | No. SR-07 says "9 query types vs 3–4 recipes" generically; CI-06 covers the type table only.                                                                                                                                 | New row **SR-10** "Communication preferences queryable per type (collection on the contacts query + recipe 'Opted in to X')", Sev **H** (mailing-list building is a core use), M7, depends on CI-06.                                                                                                                                                                                                      |
| R2-02 | **Cross-application fields injected into every contact or organization query** by tenant setting: up to 15 events, courses and committees whose registration/enrolment/membership fields appeared on contact queries; up to 50 contact fields shown per role on organization queries ("Billing Contact e-mail" on an organization row); a dedicated "Organization Contacts by Role" query type. Membership chapter and section names were on every contact query. | `rolodex-settings` (gh) `src/schema/object.schema.js:98-103,147-178`; `rolodex-query-definitions/src/definitions/organization-contacts-by-role.js:3-6`; `contacts.js:45-63`                                                                                                                                                                  | Roles exist only as an `exists` criterion collection (`CRM:modules/entities/queries/contacts.query.ts:27-30`); no "contact holding role R at this organization" columns on the organization query; other applications' fields depend on their datasets and relationship-aware criteria (SR-08, later by ruling).                                                       | Partly: e01 §settings lists the settings; chapter 14 has no row.                                                                                                                                                             | New row **SR-11** "Role-holder columns on organization queries (name, e-mail, phone of the holder of role R) and a recipe replacing 'Organization Contacts by Role'", Sev M, M7. State in SR-08 that the event/course/committee settings are replaced by cross-application relationships, not by settings.                                                                                                |
| R2-03 | **E-mail history tab on the contact.** Every message any 1.0 application sent the contact, with delivery and open events and the body.                                                                                                                                                                                                                                                                                                                            | `rolodex-ngx-console/src/app/contacts/pages/view-contact/view-contact.component.html:23-26`; `rolodex-ngx-console-controls/.../contact-email-history.component.ts:38`; `contact-message-info.component.ts:28,45`                                                                                                                             | Platform already answers `GET /communications/{tenantId}/messages?recipient_contact_id=` (`P:modules/communications/aws/api/communications.routes.ts:297-330`), but the console's Communications tab is sample data (`C:lib/contact-360.ts:181,663-666`).                                                                                                              | Mentioned only as a tab that shows "not available" ([09](#10-09-console-experience-search-explorer-queries-and-the-360-pages) line 85); no gap row.                                                                                                                  | New row **UI-16** "Contact 360 Communications tab over Platform message history (list, delivery state, body)", Sev M, M7 (or M2 if the sample tab must go first under UI-07).                                                                                                                                                                                                                             |
| R2-04 | **Staff "Send an Email" to one contact** from the record.                                                                                                                                                                                                                                                                                                                                                                                                         | `rolodex-ngx-console-controls/.../contact-options/contact-options.component.html:3-6,24-28` (permission `rolodex:user:contacts:send-email`)                                                                                                                                                                                                  | The layout declares a "Send email" action (`C:lib/record-views/views/contact-360.json:103-106`) with **no handler** anywhere in `components/` or `lib/`; the permission `sendEmail` exists (`C:lib/authz/permissions.ts:48`).                                                                                                                                          | Listed among 1.0 staff actions ([02](#3-02-rhythm-1-0-rolodex-and-the-support-history) line 52); no row. UI-13 (inert toolbar actions) would remove it, not build it.                                                                   | New row **UI-17** "One-off staff e-mail from a contact or organization through Platform communications (logged as an activity)", Sev M, M6. Until built, UI-13 must remove the action.                                                                                                                                                                                                                    |
| R2-05 | **Quick Add Contact / Organization from any picker.** Every contact selector across apps (event guests, co-authors, exhibitor contacts, nominees, donors, course/exam registrations, insertion orders) could create a contact inline, with configurable quick-add fields.                                                                                                                                                                                         | `rolodex-ngx-console-controls/.../contacts/quick-add-contact/quick-add-contact.component.ts:85`; `.../quick-add-contact-form-settings/`; used e.g. in `events-ngx-controls/.../add-guests/add-guests.component.html`, `fundraising-ngx-console/.../create-gift/create-gift.component.html` (31 templates)                                    | Shared pickers (`C:components/contact-editor/fields/entity-pickers.tsx`, `C:components/accounting/shared/customer-picker.tsx`) offer no create. The order composer's "Quick Add Contact" creates a local, unsaved object (`C:components/order-composer/customer-panel.tsx:130-145`).                                                                                   | No. DQ-07 (one match-or-create service) is the back end it should call, but no row covers the shared UI.                                                                                                                     | New row **UI-18** "Quick add from the shared contact/organization picker, through the match-or-create service (duplicate check before create)", Sev H (every application's create flows depend on it), M5 with DQ-07.                                                                                                                                                                                     |
| R2-06 | **Per-application status badges on the 360 header**: finance (payments), commerce, membership status and type, accreditation, events, certifications, awards, sponsorship, documents, careers, submissions, fundraising, publications.                                                                                                                                                                                                                            | `rolodex-ngx-console-controls/.../badges/badges/badges.component.html:1-17`; rendered at `contact-360-shell.component.html:51`, `organization-360-shell.component.html:48`                                                                                                                                                                   | The header shows membership status/tier only from sample data and says the source "does not exist yet" (`C:components/console/contact/contact-header.tsx:91-100`).                                                                                                                                                                                                     | Partly: UI-08 covers cards and list columns contributed by other apps, not header badges.                                                                                                                                    | Extend **UI-08**: "…and a header badge per licensed application (status at a glance)". No new milestone.                                                                                                                                                                                                                                                                                                  |
| R2-07 | **Shared "confirm your contact / organization details" step in portal processes.** Membership, awards, certifications and submissions applications embedded a CRM step that showed a configured contact or organization form before the applicant proceeded, plus eligibility criteria and a confirmation e-mail.                                                                                                                                                 | `libraries-rolodex-processes/src/rolodex-process.schema.js:9-30,53-73`; consumers `membership-application-processes/package.json:65`, `awards-application-processes/package.json:52`, `certifications-application-processes/package.json:52`, `submissions-processes/package.json:49`                                                        | Not found in CRM, Platform or the console. Application processes are being designed in `P:docs/proposals/application-processes-and-workflow-actions/`.                                                                                                                                                                                                                 | No.                                                                                                                                                                                                                          | New row **PO-05** "Profile-confirmation step offered to application processes (a CRM portal route + form contract)", Sev M, M11, owner shared with Application Processes.                                                                                                                                                                                                                                 |
| R2-08 | **Sign-up organization link or creation by e-mail domain**, governed by two tenant settings (`disable_organization_matching`, `disable_organization_creation_during_sign_up`).                                                                                                                                                                                                                                                                                    | `rolodex-contact-steps/src/lambda/steps.js:19-22,45-54`; `rolodex-settings` (gh) `object.schema.js:84-85`                                                                                                                                                                                                                                    | 2.0 sign-up creates or matches the **contact only** (`CRM:db/migrations/0077_portal_sign_up_contacts.sql:1-12`; no organization or affiliation logic in the file).                                                                                                                                                                                                     | Partly: [02](#3-02-rhythm-1-0-rolodex-and-the-support-history) line 49 and H-6 describe it; OR-05 covers only the lookup route.                                                                                                         | Extend **OR-05** or add **OR-07** "Portal sign-up affiliates the new contact to the organization matched by e-mail domain, or creates it, per tenant switches", Sev M, M11.                                                                                                                                                                                                                               |
| R2-09 | **Alternate e-mail "authorize and link".** A person signing up with an address already on an existing contact received an e-mail to authorize linking the new login to that contact (24-hour link).                                                                                                                                                                                                                                                               | `rolodex-contact-steps/build/stateMachines/authorize-link-alternate-email.step.json`; `src/logic/linking-logic.js:17-33`                                                                                                                                                                                                                     | **Replaced**, not missing: Platform's portal likely-match offers up to three masked candidates and verifies by a six-digit code to the candidate's e-mail or phone (`P:modules/security/services/portal/contact-match.ts:1-20`), backed by `crm.portal_contact_candidates` / `crm.create_portal_contact` (`CRM:db/migrations/0077_portal_sign_up_contacts.sql`).       | **No: the document never mentions likely-match or these two CRM SQL functions**, and P10 says CRM has "no portal identity yet".                                                                                              | New L1 row **L1-18** "Alternate-e-mail authorize-and-link → replaced by Platform likely-match with code verification"; add the two functions to [06 §3](#7-06-api-and-service-architecture) and to X-04's inventory; correct P10's status to "Partial: sign-up matching live".                                                                                                                         |
| R2-10 | **Outbound integrations fed by CRM records**: record-change events pushed to a tenant's own AWS EventBridge (partner event sources, per-application event subscriptions); Zapier triggers (contact and organization created, changed, deleted) and a "create activity" action; Higher Logic contact and organization sync; Mailchimp list sync; Outlook add-in showing contact, organization and activities.                                                      | `partners-eventbridge/src/logic/push-logic.js:12-35`; `partners-zapier/zapier/triggers/rolodex-contacts/`, `zapier/actions/create-rolodex-activity/`; `partners-higher-logic/build/template-resources.yaml:278-280`; `partners-mailchimp/src/services/lists/`; `partners-outlook-addin/src/lambda/api.js:35,77,110` (last commit 2026-06-30) | Integration Platform has inbound webhooks only (`P:modules/integrations/aws/api/webhooks.routes.ts`); outbound domain events are a plan (`P:docs/proposals/application-processes-and-workflow-actions/evidence/02-rhythm-2-platform-infrastructure.md:367`). CRM publishes `crm.contact.merged` only (EV-01).                                                          | Partly: [12](#13-12-cross-application-integration-contracts) names the Integration Platform as consumer of "all catalogued business events"; AC-04 covers inbound marketing activity. No row names these customer-facing integrations. | New L1 row **L1-19** "Outbound customer integrations (EventBridge forwarding, Zapier, Higher Logic, Mailchimp lists, Outlook add-in): owned by the Integration Platform; CRM's part is EV-01 events and X-04 service functions". Add to EV-01's "Gap": "no customer delivery path until the Integration Platform's outbound events ship". Sev H for EV-01 (customers wired these into their own systems). |
| R2-11 | **Sales application** (leads with conversion to contact and opportunity, opportunities, pipelines and stages, forecast categories, sales activities with follow-up tasks, sales queries and statistics).                                                                                                                                                                                                                                                          | `sales-leads/src/schema/object.schema.js:17-35`; `sales-ngx-console/src/app/{leads,opportunities,pipelines}`; repos `sales-opportunities`, `sales-pipelines`, `sales-forecast-categories` (last commits 2025-06-10)                                                                                                                          | Nothing in services-crm, services-platform, services-commerce or the console.                                                                                                                                                                                                                                                                                          | Mentioned only in e02 §1 (sales activities, usage UNVERIFIED). Not in L1, not in the out-of-scope list.                                                                                                                      | New L1 row **L1-20** "Sales (leads, opportunities, pipelines): out of scope for CRM 2.0 unless a tenant uses it", plus a fact for Andrew to confirm: does any tenant use Sales? Sev depends on that answer.                                                                                                                                                                                               |
| R2-12 | **Calculated fields** on contacts and organizations: tenant-configured expressions run on every save (system settings → Calculated Fields per service).                                                                                                                                                                                                                                                                                                           | `system-ngx-console/src/app/settings/settings.routes.ts:94-97`; `system-ngx-console/src/app/settings/pages/calculatedFields/calculated-field-form.service.ts:23-28`                                                                                                                                                                          | Not carried, recorded as deliberate debt (`CRM:docs/debt/custom-field-values.md:16-18`); no Platform metadata support.                                                                                                                                                                                                                                                 | No. [05 §2.7](#6-05-functional-architecture-what-every-crm-capability-does) lists custom-field types without computed fields.                                                                                                                            | New row **CF-06** "Computed (read-only) custom fields: decision whether Platform metadata provides them; if not, L1 drop", Sev M, M3.                                                                                                                                                                                                                                                                     |
| R2-13 | **CRM home dashboard**: KPI tiles (number of contacts, number of organizations, potential duplicates, each opening a query) and an embedded, tenant-editable dashboard; backed by `rolodex-statistics`.                                                                                                                                                                                                                                                           | `rolodex-ngx-console/src/app/home/home.component.html:1-9`; `home/legacy-dashboard/legacy-dashboard.component.html:15,53`; `rolodex-statistics/src/logic/statistics.js:10-24`                                                                                                                                                                | No CRM landing page (routes under `C:app/(console)/console/[tenantId]/crm/` are lists, records and Data Quality); a generic Dashboard Lab exists (`C:app/(console)/console/[tenantId]/reports/dashboardLab/page.tsx`).                                                                                                                                                 | No (e02 notes the statistics, display "UNVERIFIED").                                                                                                                                                                         | New row **UI-19** "CRM home: counts and Data Quality counters as Dashboard Lab tiles, or explicit drop", Sev L, M7.                                                                                                                                                                                                                                                                                       |
| R2-14 | **Organization type "key roles"** shown on the organization summary.                                                                                                                                                                                                                                                                                                                                                                                              | [02](#3-02-rhythm-1-0-rolodex-and-the-support-history) line 61 (document's own description)                                                                                                                                                                                                                                                             | **Removed on purpose** by Andrew 2026-10-11 (`CRM:modules/entities/db/organization-types.table.ts:27-28`).                                                                                                                                                                                                                                                             | No: the removal is not in L1.                                                                                                                                                                                                | New L1 row **L1-21** "Key roles on organization types: intentionally dropped (Andrew 2026-10-11)".                                                                                                                                                                                                                                                                                                        |
| R2-15 | **Task groups (linked tasks), order and order-line references on tasks.**                                                                                                                                                                                                                                                                                                                                                                                         | `platform-tasks/src/schema/object.schema.js:21,42-43`; `src/lambda/list-by-task-group-id.js`, `list-by-order-id.js`                                                                                                                                                                                                                          | Dropped on purpose (`CRM:modules/tasks/db/tasks.table.ts:3-7`); a generic subject reference replaces the order link.                                                                                                                                                                                                                                                   | No.                                                                                                                                                                                                                          | Add to L1 (**L1-22**): "Task groups and order references: intentionally dropped; generic subject reference instead".                                                                                                                                                                                                                                                                                      |
| R2-16 | **Portal delegates and account-delegation processes**; **contact login profiles** (last portal login, login count, last IP).                                                                                                                                                                                                                                                                                                                                      | e01 §10.3; `rolodex-contacts/src/schema/object.schema.js:172-176` (login fields deprecated); console Security tab copied the user's last login and count onto the record (`rolodex-ngx-console/src/app/contacts/pages/view-contact/view-contact.component.ts:128-129`)                                                                       | None in CRM.                                                                                                                                                                                                                                                                                                                                                           | Delegates appear in [05 §7](#6-05-functional-architecture-what-every-crm-capability-does) as a 1.0 capability to rebuild; login profiles nowhere. Neither is in L1.                                                                                      | Add to L1 (**L1-23**): "Delegates: non-functional in 1.0 (no creation flow); rebuild only by decision under PO-04. Login profiles: dead; last-login display belongs to Platform security (extend L1-09)".                                                                                                                                                                                                 |
| R2-17 | **Portal profile restrictions and staff redirect setting**: `restrict_contact_name_changes`, `restrict_contact_credentials_changes`, `restrict_organization_name_changes`; `new_contact_redirect_route` (where staff land after creating a contact).                                                                                                                                                                                                              | `rolodex-settings` (gh) `object.schema.js:61,80-82`                                                                                                                                                                                                                                                                                          | None; console always opens the new record.                                                                                                                                                                                                                                                                                                                             | e01 lists them; PO-01/02 rows do not carry them as requirements.                                                                                                                                                             | Add to **PO-01/PO-02** acceptance: "name, credential and organization-name locks per tenant". L1 drop for the redirect setting.                                                                                                                                                                                                                                                                           |
| R2-18 | **In-product training exercises** ("CRM 101/201": create a contact, change status, change organization type, custom-field exercises) with completion codes.                                                                                                                                                                                                                                                                                                       | `rolodex-ngx-console/src/app/training-exercises/`; `rhythmsoftware/rolodex-training-exercises` (gh) `src/exercises/` (last push 2026-02-18)                                                                                                                                                                                                  | None.                                                                                                                                                                                                                                                                                                                                                                  | No.                                                                                                                                                                                                                          | L1 row (**L1-24**): "Training exercises: out of CRM scope; Help / Learning Center owns any successor".                                                                                                                                                                                                                                                                                                    |
| R2-19 | **Google Civic elected-officials panel** (national, state upper and lower officials for the contact's districts), not only district fields.                                                                                                                                                                                                                                                                                                                       | `rolodex-ngx-console/src/app/contacts/pages/view-contact/view-contact.component.html:38-46`; `partners-google-civic/src/logic/representative-logic.js:36-45`                                                                                                                                                                                 | District columns exist, nothing fills them, no officials panel.                                                                                                                                                                                                                                                                                                        | Partly: L1-12 says "Google Civic districts".                                                                                                                                                                                 | Reword **L1-12**: "…districts and the elected-officials panel". Note for the decision: Google announced the retirement of the Civic Information API's representatives lookup in 2025 (verify before designing), so a successor needs another data provider.                                                                                                                                               |

### 2. Places the document describes 1.0 (or the 2.0 starting point) wrongly

| #    | Document says                                                                                                                                                  | What the code shows                                                                                                                                                                                                                                                                                                                                                                                                   | Fix                                                                                                                                                                                                          |
| ---- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| M-01 | [02 §2.7](#3-02-rhythm-1-0-rolodex-and-the-support-history) line 127: "No CRM bulk edit, bulk delete or export in the console." (e02 §5.1 itself marked this UNVERIFIED.) | 1.0 had tenant-wide **Mass Update** and **Mass Delete** in the System console for any service, contacts included (`system-ngx-console/src/app/settings/settings.routes.ts:103,128`), with saved **mass-update templates** whose fields could be set by value, variable or expression and whose arrays could be replaced, added to or removed from (`system-mass-update-templates/src/schema/object.schema.js:17-25`). | Correct the sentence. Check DO-02 for parity: saved templates, expression values, add/remove on collections (for example "add role R").                                                                      |
| M-02 | [02 §2.7](#3-02-rhythm-1-0-rolodex-and-the-support-history) line 130: events were "a contact-merge event … (forwardable to tenants' own AWS accounts)".                   | Forwarding was a general mechanism: `partners-eventbridge` pushed record-change events to each tenant AWS account subscribed per application (`push-logic.js:24-35`), not only merges. Which Rolodex events were routed to it is not wired in any template found (UNVERIFIED).                                                                                                                                        | Say "record-change events could be forwarded per application subscription"; tie to R2-10.                                                                                                                    |
| M-03 | [04](#5-04-domain-and-relational-data-model) line 81: task `status` is "(open, completed, cancelled)".                                                                 | 2.0 has five: `open`, `waiting_for_someone_else`, `deferred`, `completed`, `cancelled` (`CRM:modules/tasks/db/tasks.table.ts:60-67`), carrying 1.0's "waiting for someone else" and "deferred" (`platform-tasks/src/schema/object.schema.js:44`).                                                                                                                                                                     | Correct the list.                                                                                                                                                                                            |
| M-04 | [05 §2.1](#6-05-functional-architecture-what-every-crm-capability-does) line 51: deceased "suppresses communications (Platform communications reads the status)".                          | Platform communications withholds only addresses on its suppression list (`hard_bounce`, `complaint`, `manual`) and reserved domains (`P:modules/communications/services/persistence/messages.ts:401-450`; `communication-suppressions.table.ts:17`); nothing reads CRM status, `email_opt_out` or communication preferences. (1.0 did not enforce these at send either; it filtered through queries.)                | Mark as **Missing** and add a row (CI-09) "Platform communications refuses deceased, `email_opt_out` and opted-out-of-type recipients", Sev H (sending to the deceased is a known reputational failure), M4. |
| M-05 | [02 §4](#3-02-rhythm-1-0-rolodex-and-the-support-history) P10 and README §2 item 7: "no portal identity in CRM yet".                                                      | Portal sign-up matching is live through CRM's two SQL functions and Platform likely-match (R2-09).                                                                                                                                                                                                                                                                                                                    | Restate as "Partial: sign-up identity matching live; self-service routes missing".                                                                                                                           |
| M-06 | [05 §7](#6-05-functional-architecture-what-every-crm-capability-does): delegates listed among 1.0 portal capabilities to carry.                                                            | Delegates could not be created in 1.0 (e01 §10.3: no creation flow, broken modules).                                                                                                                                                                                                                                                                                                                                  | Say so in §7 so the decision is "design new", not "port".                                                                                                                                                    |

### 3. Checked and found accounted for

Region assignment options (address type, contacts/organizations/both: `CRM:modules/regions/aws/api/regions.schemas.ts:110-115`);
compliance and region maintenance actions (CM-03, RG-01); default console and portal forms (CO-02, form registry);
child-organization portal settings and role permissions (PO-03, PO-04); organization duplicate detection (DQ-06: 1.0's
`rolodex-organization-duplicates` service existed but its console alert never set its flag,
`duplicate-organizations.component.ts`); social lookup on forms (L1-12); task assignment e-mail and reminders (TK-07);
undelete/revert (1.0 used it only for import rollback: DO-05, DO-14).

## 28. Review r3: consistency, ownership and feasibility

## Review r3: consistency, ownership and feasibility

Reviewer pass 3 of RHYTHM-CRM-ARCH-001 (README and chapters 01–18 as of 2026-10-19 ~17:10 ET). Scope: internal
contradictions, contradictions with the binding sources the document adopts, ownership conflicts, database-versus-API
mismatches, milestone feasibility against the stabilization-first rule, testability of acceptance criteria, and broken
links. No chapter was edited.

Sources checked:

- services-crm `origin/master` `bb55ae7a` (the document's baseline `a980b553` is 55 commits behind it):
  `docs/affiliations/README.md`, `docs/merging/decisions.md`, `docs/merging/architecture.md`, `modules/tasks/db/tasks.table.ts`,
  `modules/activities/db/activity-links.table.ts`, `docs/debt/soc2-remediation-windows.md`, `docs/compliance/exceptions.md`.
- services-platform `origin/agent/crm-acc-w3-arch-lifecycle` `6873e3b6c` (`docs/proposals/record-lifecycle/` README, 01–08);
  `origin/agent/crm-acc-w3-arch-writes` `55c0cefba` (`docs/standards/record-write-contracts.md`); `origin/master`
  `docs/standards/milestone-development-and-acceptance.md` (R2-DEV-001).
- react-console worktree `product-acceptance-crm` (`ac9753fb2`): `wave3-directives.md`, `wave4-directives-E.md`,
  `decision-register.json` (updated 2026-10-19 21:01 UTC).
- services-accounting `origin/master` (`product_task_templates`); merge-participant migrations in Membership, Accounting,
  Commerce, Submissions, Committees.

Format: **severity** | location | problem | suggested fix. Items are ordered High, then Medium, then Low.

### High

1. **High** | 08 §6 (last paragraph); 13 CRM-ADR-11; 14 row LC-02; 17 R-3 | **The console already exposes hard delete.** The
   document says "the console exposes no Delete action today … deletion is reachable only through the API and the bulk delete
   data job", treats CRM-ADR-11 as already true (LC-02 "Complete (as policy)") and rates R-3 "Medium (no console Delete)". But
   the bulk delete data job _is_ a console feature: Explorer "select all matching" hands contacts or organizations (≤ 500) to
   the live data-job wizard, which deletes them (e04 addendum D lines 70, 332–334; 09 §3 "bulk actions (update fields and
   delete via data jobs)"; 10 §4; 14 DO-02 "wizard live"). The protection the ADR relies on does not exist, and this is exactly
   the path that orphans invoices, memberships and seats until CRM-M9. | Replace CRM-ADR-11's decision with: "Hard delete of
   contacts and organizations stays out of the live console until CRM-M9, **including the data-job wizard's bulk delete from
   an Explorer selection**; archive (and bulk archive, DO-10) is the console's removal action. Activity and task deletes are
   unaffected." Change LC-02 to Status **Partial**, 2.0 evidence "no record Delete; bulk delete by selection live (e04 add. D)",
   Gap "bulk delete of contacts/organizations reachable in the console", Sev **C**, Work "hide contact and organization Delete
   in Explorer bulk actions and the data-job wizard in live mode", MS **M0** (a repair, not new building). Replace the 08 §6
   sentence with "Today the console offers no Delete on a record, but the data-job wizard bulk-deletes contacts and
   organizations from an Explorer selection; removing that path from live consoles until CRM-M9 is part of CRM-M0 (LC-02)."
   Change R-3 likelihood to "High (bulk delete is in the console)".

2. **High** | 06 §2 rule A-2 | **Contradicts the write-contracts standard W5 (and D-43's mechanism).** A-2 says "a retry with
   the same id and same body answers the original record". W5 says a create whose id already exists answers **409
   `record_exists`** with `details: { id }`, and the _client_ reads the record and treats the create as done; replaying the
   original answer is the idempotency ledger's job (`Idempotent-Replayed`), not the id's. | Replace A-2's rule with: "Create
   accepts a client-assigned UUIDv7 `id`, generated before the first attempt and reused on every retry. A create whose id
   already exists in the tenant answers 409 `record_exists` with `details: { id }`; the client reads the record and treats the
   create as done (Ruled D-43; standard W5). Contacts, organizations, activities and tasks declare the `optional` idempotency
   contract once CRM's ledger table exists." Replace "Today" with "Contacts and activities accept a client id; an existing id
   answers 400 'this id already exists' (must become 409 `record_exists`); no ledger." Add the same to WR-03's Gap column.

3. **High** | 05 §7 (portal self-service bullet); 11 §2 row "Portal"; 14 PO-04 and L1-05; 15 CRM-M11 | **Ownership conflict
   with the binding affiliations design.** The document makes CRM own portal authorization: "an organization-administrator
   capability is granted through a role on an affiliation … one model … all enforced by CRM". Affiliations README §10 (binding)
   says "Security owns grants … An affiliation or role confers none of those by itself; those services read affiliations to
   build their own rules", and §5.6 "CRM affiliation roles are not security roles … and grant nothing by themselves". Two
   owners (CRM and Platform Security) for one permission rule. | Replace in 05 §7, 11 §2 and PO-04: "Portal authorization is
   Platform Security's. Security grants an organization-administrator capability scoped to an organization and its
   descendants, and may derive that grant from CRM affiliation roles through a published CRM read (affiliations README §10).
   CRM's portal routes enforce the grant Security resolves; CRM does not own the role-to-permission mapping." Change L1-05's
   treatment to "Replaced by Security grants derived from affiliation roles (Platform)" and list Platform Security as a
   dependency of CRM-M11.

4. **High** | 05 §2.5 "Which roles apply"; 16 §2 P5 | **Database/API semantics contradict the binding design.** 05 says
   applicable roles are "enforced by CRM in every channel … not by a form", i.e. an inapplicable role is refused on save.
   Affiliations README §5.6 (binding, adopted "unchanged" per 05 §2.3 and CRM-ADR-01): "as today this filters the picker and
   never refuses a save." Either the document changes an accepted design without saying so, or the requirement is wrong. |
   Replace the 05 bullet with: "**Which roles apply.** An organization type lists its applicable contact roles. Today (binding
   affiliations README §5.6) this filters the role picker in every channel and never refuses a save. Whether CRM should refuse an
   inapplicable role on save, in every channel (1.0 RB-5829, pattern P5c), is **Open (CRM-Q9)**." Add CRM-Q9 to 13 §3 (Blocks:
   CRM-M3) and change 16 P5's "role rules identical across channels" to "the applicable-role filter is identical across
   channels (and, if CRM-Q9 = refuse, the refusal is)".

5. **High** | 05 §2.3 heading and "Omitted means unchanged" bullet; 06 A-6; 11 §2 "Relationship changes"; 13 §5 | **Three
   binding sources disagree on affiliation mutations and the document silently picks one.** Affiliations README §4/§6:
   affiliations are written _only_ through the contact aggregate, "no endpoint, service or job writes an affiliation on its
   own", collection semantics "full replacement, or JSON Patch". Record-lifecycle 01 §3: removing an affiliation is "`remove`
   of that element in the contact's patch … under `crm.contacts.update`" (no separate permission). Write-contracts W3: "explicit
   operations … are routes of their own … each with a permission key". D-09 requires explicit, permissioned, audited
   operations. The document's heading still says "adopted unchanged". | Rename 05 §2.3 to "Affiliations (Decided 2026-10-13/14;
   adopted, amended by Ruled D-09, D-10, D-12, D-46)" and update the four links to its anchor (13 §1, 14 AF-01, 18 rows §2 C)
   to `#23-affiliations-decided-2026-10-1314-adopted-amended-by-ruled-d-09-d-10-d-12-d-46`. Add to 13 §5: "C-6 | Affiliation
   mutations: affiliations README §4/§6 (aggregate only; full replacement or JSON Patch), record-lifecycle 01 §3 (`remove` in
   the contact patch under `crm.contacts.update`) and write-contracts W3 (explicit routes with their own permission keys)
   disagree; D-09 requires explicit, permissioned, audited operations | Follow D-09 and W3: add, update, end and remove are
   explicit routes that still execute inside the contact aggregate (contact row lock and version; affiliations README §4 rules
   1–9); end and remove carry their own keys, remove the more privileged; the affiliations README and lifecycle 01 §3 are
   amended by their owners." Add the same C-entry for D-25 versus affiliations README §0b.2 item 5 ("removed source becomes
   `copied`").

6. **High** | README §5; 15 §2 "Stabilization first", §3 graph and "Critical gaps"; 17 R-2; 14 X-01, X-08 | **The three
   "before anything else" items cannot start under the roadmap's own rules.** 15 §2 says no milestone after CRM-M0 starts until
   acceptance is green; X-01 (SOC 2, hard date **2026-11-07**) is in CRM-M1, X-08 (targeted merge block) is "first item" of
   CRM-M4, and the graph makes M4 wait for M1 ("needs M1 events and service functions") with "one milestone accepted at a time".
   R-2 then says X-01 "may need to run during M0", contradicting 15 §2. If CI fails on 2026-11-07 it also stops the CRM-M0
   repair merges themselves. Neither item is new building: X-01 can be met by setting each expiring High exception to
   `escalated` (services-crm `docs/debt/soc2-remediation-windows.md` lines 5, 14; EX-DAT-002 expires 2026-11-07, owner Andrew)
   and X-08 is a data-protection repair. | Add to 15 §2: "**Exceptions (repairs, not new building).** X-01 runs now inside
   CRM-M0 and lands before 2026-11-01: each High exception expiring 2026-11-07 is fixed or set to `escalated` with Andrew's
   agreement. X-08 runs inside CRM-M0 (see item 7). LC-02 (item 1) likewise." Set X-01, X-08 MS to **M0**. Change the graph
   label to "CRM-M4 … (X-04/X-07 need M1 business events)". Delete "may need to run during M0" from R-2 and replace its
   mitigation with "In CRM-M0 (15 §2 exception)".

7. **High** | 13 §3 CRM-Q3; 07 §4 item 3; 12 §3; README §7; 14 X-08, MG-09; 17 R-1 | **CRM-Q3 re-asks a settled question.** The
   binding merging decisions list "Unknown references | Block the merge; CI guard prevents them" under "Settled by inspection
   … not up for decision", 07 §3 itself states "Unknown references block", and Accounting's plan requires a block. The
   "targeted block" recommended in CRM-Q3 _is_ that settled rule; wave-3 directive §6.7 says not to re-open answered
   questions. Holding it as a question gates the protection on Andrew and on M4. | Replace CRM-Q3 with a record: "CRM-ADR-12 |
   A contact or organization referenced in a schema that has no registered merge participant cannot be merged: the plan
   carries a blocker naming the application and the count; merges of records nobody else references proceed (merging
   decisions, 'Unknown references: block'; Accounting plan 02 lines 700–704) | Allow and repair later (rejected in D1) | Data
   protected now without stopping all merges | I". Remove CRM-Q3 from README §7, 12 §3 and 17 §2; renumber nothing (keep Q3
   retired). If a question remains it is only: "Should an authorised user be able to override the block for a named
   application?" X-08 becomes a CRM-M0 repair.

8. **High** | 15 CRM-M1 Acceptance and Walkthrough; CRM-M2 scope (OR-02, UI-06); §3 graph | **Circular dependency between M1
   and M2.** M1's acceptance ("organization 360 shows Version History") and walkthrough ("edit an organization, open its
   Version History") need the organization editor (OR-02) and the metadata Organization 360 with history (UI-06), both in M2;
   M2's UI-06 "with version history" needs HI-01 from M1. The graph shows them as independent. | Make M1 a backend-only
   milestone (R2-DEV-001 §2.1, guided verification) and replace its Acceptance with: "`npm run compliance:check` green;
   `GET /organizations/{tenant_id}/{id}/history` returns a revision after an API `PATCH`; task, configuration-type, role,
   region and duplicate-rule history routes each return a revision after an edit; inserting a non-UUIDv7 task id is refused
   by the database; a test subscriber receives `crm.organization.updated` carrying ids and changed field names and no personal
   data." Replace its Walkthrough with the same steps run as a script Andrew watches. Add `CRM-M1 ──▶ CRM-M2` to the graph.

9. **High** | 14 (no row); 09 §2; 15 CRM-M0 Goal; 14 Coverage check | **Ruling D-29 (P0, in progress) is missing from the whole
   document**, as are D-16, D-18, D-26 and D-28. D-29 is about Quick Search of CRM records ("late update after deletion cannot
   resurrect a record; one tenant's bulk delete must not stop indexing for others") and underlies 16 P8. M0 claims "every
   non-deferred ruling D-01..D-53 is implemented and verified" and the Coverage check claims every requirement has a row. |
   Add row "SR-10 | Search durability: source-of-truth deletion and version checks, ordered idempotent indexing, one tenant's
   bulk delete isolated (D-29) | `[09 §2](#10-09-console-experience-search-explorer-queries-and-the-360-pages)` | separate index repaired by hand | register:
   in progress | In M0 | — | **C** | Platform quick-search | late update after delete never resurrects; cross-tenant isolation
   under a 500-record delete | M0". Add to 09 §2: "Deletes and late updates never resurrect a record, and one tenant's bulk
   delete never stalls indexing for another (Ruled D-29)." Add under the 14 Coverage check: "Rulings outside CRM's gap rows,
   repaired in CRM-M0 by their owners: D-16 (Microsoft sign-in, Platform), D-18 (empty roles, Platform security), D-26 (user
   time-zone preference, console), D-28 (Explorer trail, console)."

10. **High** | 15 CRM-M4 (and CRM-M9, CRM-M7 UI-08); 13 CRM-ADR-02 | **Not feasible under Andrew's current orders and repository
    ownership.** M4 builds participants in services-membership, -accounting, -commerce, -submissions and -committees (≈ 20
    agent-days). Andrew's focus order of 2026-10-19 ~15:55 ET limits building and acceptance to CRM, Accounting and the
    Integration Platform ("all else merge-only"); Submissions is stopped; Accounting's repository is owned by the Accounting
    Build Out session. The roadmap names no owner per repository and no gate for those applications, so M4 (and M9's WP-P2..P6,
    M7's UI-08) cannot start or be accepted by the CRM session. | Split M4: **CRM-M4** (CRM-owned: X-04 service functions, X-07
    Membership handler invocation from CRM's stream list, X-09 published views, MG-09 list maintenance, MG-10 generated matrix
    and CI gate) and per-application participant milestones in each application's own plan (`ACCT-Mn`, `MEMB-Mn`, `COMM-Mn`,
    `SUBM-Mn`, `CMTE-Mn`, R2-DEV-001 §2 identifiers). Add to M4: "**Owners and gating.** Each participant is built and accepted
    by its application's session, when Andrew's focus order allows that application to build; until then CRM-ADR-12's block
    (item 7) is the protection. CRM-M4 does not wait for them." Apply the same note to M9 and to UI-08.

11. **High** | 08 §1, §6; 15 §1 item 1 and CRM-M0 Goal/Acceptance | **D-41 and D-44 are P0 ("must be resolved before a
    production release", wave-3 directive §3) but the document never states the release consequence of deferring them.**
    Andrew deferred safe delete to CRM-M9; the decision register marks D-41, D-42, D-44 `deferred`. 15 §1 requires "no open P0",
    M0 is "acceptance green" with two deferred P0s, and D-41's acceptance ("blocked operations cannot proceed by bypassing
    console") is unmet while the API and bulk delete work. | Add to 08 §1: "D-41 and D-44 are P0 rulings. With Andrew's deferral
    CRM can be acceptance-green (CRM-M0) but is not production-ready for any tenant that hard-deletes contacts or organizations
    until CRM-M9, unless Andrew also restricts hard delete to a system identity in the meantime." Name the deferred rulings in
    M0's Acceptance ("no failing P0 journey except those tracing to deferred D-41, D-42, D-44"). Offer Andrew the interim
    choice explicitly (restrict `crm.contacts.delete` / `crm.organizations.delete` to a system role until CRM-M9) as a new
    CRM-Q.

### Medium

12. **Medium** | README "How this document was validated"; 03 §2 Merge, Affiliations rows; 04 I-5; 05 §2.5 "Automatic
    assignment"; 07 §2 Gaps and §3 rulings table; 14 rows AF-06, MG-02, MG-05..07, DQ-02, DQ-03, DQ-06, CO-05, AC-07, SE-06 |
    **Stale against the origin/master the README says was rechecked.** Since `a980b553`, services-crm master landed D-08 locks
    (512ba8f3), D-19 (74158f19), D-20 410 on every route (36ceb699), D-21/D-45 (ab22aac0), D-30 field outcome (03bbc71d),
    D-31/D-33 approvals bound to dependent ids and versions (4c855bf4), D-32 including organization create (859d673c,
    2bb6a828), D-34, D-46 (d875cf22), D-47, D-48, D-49, D-10/D-12 service half (650eae63). The document still says "Not built"
    for D-30, D-31, D-33, "Partly built" for D-20, "assignment runs only when the primary affiliation changes", and "no duplicate
    check when creating an organization". | Re-baseline e03, 03, 07 and 14 at `bb55ae7a` or later. Example replacements for 07
    §3's State column: D-30 "Service landed (03bbc71d); Merge Details console pending"; D-31/D-33 "Service landed (4c855bf4);
    console reapproval pending"; D-20 "Service landed (36ceb699); console survivor navigation pending". 05 §2.5: replace "Today
    assignment runs only when the primary affiliation changes (Verified)" with "Since d875cf22 a future-dated affiliation is
    marked due and receives its automatic roles on its start day, through the save or the five-minute job reconciler
    (affiliations README §5.6a)". 07 §2 Gaps: "no duplicate check _in the console_ when creating an organization (the service
    applies block rules since 2bb6a828)".

13. **Medium** | 04 §3.1 Task row; 05 §4 "What a task is" | **Database closed list misstated.** The document gives task status
    as `open, completed, cancelled`; the table allows `open | waiting_for_someone_else | deferred | completed | cancelled`
    (`modules/tasks/db/tasks.table.ts:60`, lifecycle 01 §7), the first three open. | Replace with "status (open,
    waiting_for_someone_else, deferred — open; completed, cancelled — closed, reached only through action routes)". Also
    `assignee_id` → `assignee_user_id` (`tasks.table.ts:146`).

14. **Medium** | 06 §2 rule A-8; 08 §2 Archive row | **API semantics contradict the lifecycle design.** A-8 says an archived
    record "refuses writes other than unarchive"; record-lifecycle 01 §2: an archived contact "can be merged, because a merge is
    how a duplicate is cleaned up. It can be hard-deleted through the job." | Replace A-8 with: "An archived record answers its
    reads normally and refuses every write except unarchive, merge and (from CRM-M9) the deletion job, with 409
    `RecordArchived` (record-lifecycle 01 §2, WP-C3)." Same wording in the 08 §2 "Who sees the record afterwards" cell.

15. **Medium** | 08 §3; 12 §2 (no column); 14 (no row) | **Archive's cross-application rule is missing.** Record-lifecycle 01
    §1 and §5: a service creating a _new_ reference to an archived party refuses it (`ObjectInvalid`, shared "party is
    selectable" helper). The other applications adopt it in WP-P2..P6, which the lifecycle rollout puts in phase 2, i.e.
    CRM-M9. So from archive's launch (M0) until M9, Membership, Accounting, Commerce, Committees and Submissions can create new
    memberships, invoices, orders and appointments for archived parties; CRM-internal new references (log an activity, create a
    task for an archived contact) are not specified either. | Add to 08 §3: "New references to an archived contact or
    organization are refused (`ObjectInvalid`, naming the field) by every service that creates one (record-lifecycle 01 §5).
    CRM's own activities, tasks and affiliations adopt it with archive (CRM-M0); other applications adopt it with their
    participants." Add row "LC-06 | New references to archived parties refused | 08 §3 | — | none | Missing | other apps can
    bill or enrol an archived party | H | CRM: activity/task/affiliation create checks; others: WP-P2..P6 | per-service refusal
    test | M0 (CRM) / per-app plans".

16. **Medium** | 15 CRM-M0 scope; 14 Work column of CI-08, CM-02, CF-05, TK-08, HI-02 ("wave 4 M4-n"); 14 MS column |
    **Milestone id collision.** The acceptance session's wave-4 items are "M4-1..M4-8" while the roadmap's milestone is "CRM-M4"
    and the matrix MS column says "M4". "wave 4 M4-3" (CI-08) reads as CRM-M4. | Rename the wave-4 items "W4-1..W4-8" everywhere
    and state in the 14 legend: "MS = CRM milestone (Mn means CRM-Mn); W4-n = acceptance wave-4 item inside CRM-M0".

17. **Medium** | 15 milestone scopes versus 14 MS column | **Scope lists and matrix disagree.** Rows assigned to a milestone in
    14 but absent from its 15 scope: CO-01 (M1), TK-02 (M1), X-02 (M1), CF-04 (M3), CM-04 (M0/M3), LC-02 (M2), DO-01 (M8),
    AC-05 (M9). Listed in 15 but assigned elsewhere in 14: MG-05, MG-06, MG-07 (M5 scope "rows not delivered by M0", matrix MS
    "M0"). Ambiguous "M0/M5" for MG-04 (D-03) and DQ-03 (D-32), although 15 says M0 implements every non-deferred ruling (both are
    non-deferred). | Add the missing ids to each scope (e.g. M1: "CO-01 constraint validation; TK-02 via HI-01; X-02 branch
    protection and scanning (Andrew/admin)"; M3: "CF-04 decimals as numbers"; M8: "DO-01 file-lane dry run"; M9: "AC-05 via
    LC-05"). Give every row one MS: MG-04 and DQ-03 "M0" (rulings), M5 keeps only DQ-04, DQ-06, DQ-07, DQ-08, MG-11; delete
    "rows not delivered by M0 among MG-04..MG-07" from M5.

18. **Medium** | 15 §4 (all milestones); 16 §5; 15 §1 item 1 | **Acceptance criteria missing or not testable.** 15 §4 promises
    "acceptance criteria" per milestone, but M3, M5, M6, M7, M8, M10, M11 and M12 have none and M10–M12 have no walkthrough.
    M0's "release-readiness report and Andrew's walkthrough" has no threshold. M2's "crawler green" depends on an undefined
    "sample marker". 16 §5 "within the D9 budget" is undefined (D9 accepted 53.9–86.3 s for 20,000 employees against a 60 s
    target "as measured") and the 1,000,000-contact tenant has no milestone that builds it. | Add an Acceptance line to each,
    e.g. M3 "a custom field created in the console persists through Platform metadata and is enforced by `POST /contacts` per
    surface (`api`, `console`, `portal`); a published layout renders for its contact type; duplicate rule create/delete
    round-trips; reprocess shows per-organization outcomes; CRM-ACC journeys tagged CRM-M3 pass"; M5 "a manually picked pair
    merges; a task added after preview refuses execution (`plan_stale`); Create Anyway without the override key answers 409
    `possible_duplicate`, with it is audited in `crm.duplicate_block_overrides`". M0: "the release-candidate run has zero failed
    P0 journeys other than those tracing to deferred D-41/D-42/D-44 and zero open P0/P1 defects outside them". M2: define the
    crawler's markers (the `lib/*/mock/*` module ids, the toast fallback of `action-toolbar.tsx`, the "not available yet"
    string). 16 §5: "20,000-employee organization merge completes in ≤ 90 s (D9 measured 86.3 s with half linked)"; name the
    milestone that seeds the 1,000,000-contact tenant (or mark the budgets Recommended and unscheduled).

19. **Medium** | 15 §3 dependency graph | **Missing dependencies.** M6's AC-04 needs the `activities` service function (X-04,
    M4); M7's OR-05 needs the `organizations` service function's domain lookup (X-04, M4); M2's UI-06 needs HI-01 (M1, item 8);
    M7's UI-08 needs other applications' cards (item 10). | Add `CRM-M4 ──▶ CRM-M6 (AC-04 only)`, `CRM-M4 ──▶ CRM-M7 (OR-05
service function)`, `CRM-M1 ──▶ CRM-M2`; mark UI-08 "external".

20. **Medium** | 15 §2, §3, §4; 16 §3 last paragraph | **Does not follow R2-DEV-001** (services-platform master
    `docs/standards/milestone-development-and-acceptance.md`). The standard requires milestones to be small vertical slices
    (15–30-minute walkthroughs); M1 is a horizontal bucket, and M4 (≈ 20 agent-days, six repositories), M9 (≈ 30) and M3 (≈ 12)
    are far larger. It allows **one milestone in build** and one awaiting acceptance per application; the graph fans eight
    milestones out of M0 and §2 says the next "may be prepared in parallel". It makes `docs/milestones/CRM-M<n>-<slug>.md` the
    single source of milestone status ("nothing else stores milestone status"), whereas 16 §3 adds milestone waves with
    `depends_on` and `journeys` to react-console `waves.json`. It asks for a chapter named "Milestone plan". | Add to 15 §2:
    "Milestones follow R2-DEV-001: one CRM milestone in build and one awaiting acceptance at a time; each has an approved
    Milestone Implementation Plan and a record in services-crm `docs/milestones/`." Split M1 (ids; history; events), M3
    (custom fields; layouts; configuration types) and M4 (item 10) into vertical slices. Replace 16 §3's last sentence with "The
    acceptance testing system reads each milestone's record; `waves.json` lists the milestone's journeys but holds no status."
    Title 15 "Completion roadmap (milestone plan)".

21. **Medium** | 05 §4 "Responsibilities and attention"; 14 TK-07; 15 CRM-M6 Walkthrough | **Internal contradiction and
    dependency on held work.** 05 says CRM's attention providers wait for Andrew to release Responsibilities and Attention
    revision 2 and "nothing in CRM depends on it"; TK-07 puts "due-soon attention items" in M6 and M6's walkthrough requires "a
    due-soon item in Needs Your Attention". Revision 2 is held by Andrew. | Split TK-07: "TK-07 assignment and due-date
    notifications through Platform communications (M6)"; "TK-09 attention providers (task due soon, organization out of
    compliance) — later candidate, after Andrew releases Responsibilities and Attention revision 2". Replace M6's last
    walkthrough clause with "receive the assignment notification e-mail".

22. **Medium** | 13 §3 CRM-Q2 and CRM-Q4; README §6; 15 §3, CRM-M11, "Later candidates" | **Recommendations contradict the
    milestone order.** CRM-Q2 recommends relationships "after portal self-service", but M10 (relationships) precedes M11
    (portal) in README §6 and the roadmap. CRM-Q4 "Blocks CRM-M11+", M11's scope ends "then directories per CRM-Q4" and the graph
    draws "M11 ─▶ directories", while "Later candidates (not scheduled until Andrew decides)" lists Directories. | Either swap
    M10/M11 or change CRM-Q2's recommendation to "Build in CRM-M10, before portal self-service; spouse stays interim until then".
    Delete "then directories per CRM-Q4" from M11 and the "─▶ directories (CRM-Q4)" arrow; set CRM-Q4 Blocks to "nothing
    scheduled (later candidate)".

23. **Medium** | README §7 ("Each is written out in 13 §3"); 13 §3 | **The decisions are only one-line rows.** 13 §3 calls itself
    "short form for the record" and says each question "is written out in full in the delivery message"; the document holds no
    full write-up (what the rule does today, why now, options with trade-offs, recommendation and why), yet README §7 points to
    13 §3 as where each is "written out". | Add 13 §3.1–§3.8 (and §3.9 for CRM-Q9, item 4), one section per question with: what
    it governs today, why it matters now, the realistic options and trade-offs, the recommendation and why, and what it blocks.
    Or change README §7 to say where the full text actually lives.

24. **Medium** | 12 §2 column "On delete (target, CRM-M9)"; 08 §4 disposition examples | **Dispositions differ from the
    lifecycle's participant inventory (06), which the document adopts.** Commerce: an open checkout is `delete` (abandoned), an
    order not yet closed is `block` (12 says "open checkouts block; orders retain"; 08 §4 cites "an open checkout's acting
    contact" as `detach`). Platform: responsibility assignments and resource-scoped grants `block`, attention items `delete` (12's
    Responsibilities row says "Delete items"). Accounting: `stripe_customers` `detach`. Submissions: in-flight submissions `block`.
    | Replace cells: Commerce "Open orders, active entitlements and usable promotion codes block; open checkouts delete; placed
    orders retain"; Responsibilities and Attention "Responsibility assignments block; attention items delete"; Accounting "Open
    receivables, pending payment intents and active schedules block; financial history retain; Stripe customer detach; never
    delete (D-41)"; Submissions "In-flight submissions and open reviews block; decisions retain". Change 08 §4's `detach` example
    to "a task's contact; an activity link; a future meeting's participant".

25. **Medium** | 12 §1 item 3; 05 §2.3 "Integration with other applications" | **Contradicts 12 §4 and CRM-ADR-05.** 12 §1
    says other applications read CRM "never through CRM's physical tables" and 05 says they "must read through published views
    … not physical tables"; 12 §4 and CRM-ADR-05 keep the standing 2026-10-08 decision that physical reads remain allowed
    without a compatibility promise. | 12 §1 item 3: "**Published reads**: other applications read CRM through published views
    and service functions, which carry a compatibility promise; reading CRM's physical tables stays allowed (standing decision
    2026-10-08) but without that promise." In 05 replace "must read" with "should read … (CRM-ADR-05)".

26. **Medium** | 15 CRM-M1 Schema, Risks, Repositories; 17 R-10 | **Wrong cross-repository dependency, missing real one.**
    Accounting `product_task_templates` hold `task_type_id` and `queue_id` (configuration ids;
    `accounting:modules/products/db/product-task-templates.table.ts:8,44`), not task ids, so the task-id migration does not touch
    them. The region-id change does touch Platform saved queries and query recipes (services-platform), which is not in M1's
    repository list. | Replace M1 Schema with "id migrations for tasks (record ids; holders of task ids outside CRM to be
    inventoried, e.g. workflow runs that close the task they created) and regions (configuration ids; rewrite `region_id` in
    contacts, organizations, assignment rules and Platform saved queries and recipes in the same release)". Repositories: add
    services-platform. R-10: "Region id migration touches Platform saved queries; task id migration's external holders to be
    inventoried".

27. **Medium** | 15 CRM-M0 scope; 14 DO-04, DO-13; 13 CRM-ADR-07 | **Ownership of the shared data-job engine is unassigned.**
    M0 is "owned by the CRM acceptance session" and lists the engine rulings (E-11, E-12, E-15, E-16; D-37, D-38 via
    `engine library 76041c5`), while CRM-ADR-07 says they belong to `@rhythmsoftware/macroservice-data-jobs` and Platform
    `modules/data-jobs`, which Accounting and Membership also consume (E-ruling expectation 6: verify against other
    consumers). | Add to DO-04/DO-13 Work: "engine (macroservice-libraries `data-jobs` + Platform `modules/data-jobs`), built by
    the engine's owner, verified against Accounting's and Membership's operations before CRM adopts it", and name that owner in
    15 §2 "Shared standards first".

28. **Medium** | 09 §1 "Cards contributed by other applications"; 14 UI-08; 15 CRM-M7 scope and Walkthrough | **Ownership:
    another application's work inside a CRM milestone.** 09 §1 says CRM's part in other applications' cards is "Nothing", yet
    UI-08 (Membership, Committees, Events, Commerce, Submissions cards and list columns) is CRM-M7 scope and M7's walkthrough
    needs "a Contact 360 with Membership and Committees cards". | Move UI-08 out of CRM-M7 into those applications' plans; keep in
    M7 only "CRM's 360 hides a tab until its application's card is live (UI-07) and accepts contributed cards through layout
    metadata". Change the walkthrough clause to "open a Contact 360 and see only live cards".

29. **Medium** | 13 §3 CRM-Q6 recommendation | **Misstates E-15.** It says "D-40 and E-15 need a type to map 'work e-mail'
    honestly", but E-15(5) explicitly accommodates the model without e-mail types ("no e-mail types shown if the model does not
    keep them") and D-40 only requires duplicate typed columns to "map correctly when implemented". | Replace with "Yes: work and
    personal e-mail columns (D-40) can then land on a type; without types, E-15(5) keeps the mapper honest by offering no e-mail
    types."

### Low

30. **Low** | 14 line 51 (CI-01 Target); 18 line 17 (§2 D) | **Broken anchors.** Both link
    `05-functional-architecture.md#24-addresses-e-mail-addresses-and-phone-numbers-decided-2026-101314-adopted`; GitHub's slug
    of "2.4 Addresses, e-mail addresses and phone numbers (Decided 2026-10-13/14; adopted)" is
    `#24-addresses-e-mail-addresses-and-phone-numbers-decided-2026-10-1314-adopted`. | Replace in both places. (All other
    relative file links and anchors in README, 01–18 and `evidence/` resolve. README line 132's `review/` resolved only once
    review files were written; its claim that "findings and resolutions are recorded" needs the resolutions appended.)

31. **Low** | 04 §4 I-9 | **Mislabelled invariant.** An organization any affiliation names cannot be deleted _today_, by the
    database: the affiliation's `organization_id` FK has no delete action (affiliations README §3.2; lifecycle 01 §3 citing
    migration 0065:191). The row says "record-lifecycle R-7 (Recommended there, adopted here)" in a table headed "enforced by the
    domain services". | Replace the "Enforced by" cell with "composite FK without delete action (Verified, migration 0065);
    record-lifecycle R-7 keeps it and turns the refusal into a readable blocker".

32. **Low** | 02 §3 H-3 and H-9 | **Stale or contradictory 2.0 treatment.** H-9 says "Open product question DEF-CRM-0324"; it
    was ruled by E-04. H-3 says "2.0 assigns on primary-affiliation change for every channel", while 05 §2.5 and CM-04 say
    imports should be made to trigger it (Recommended, Partial). | H-9: "Ruled E-04: a minimum-zero-only rule is discarded, the
    type still saves". H-3: "2.0 assigns on primary change and, since D-46, on a due affiliation's start day; imports to trigger
    the same (CM-04, Recommended)".

33. **Low** | 07 §3 "Affiliations" bullet | **Paraphrase loses the binding rule.** "Same organization and type are consolidated";
    affiliations README §9 combines them only with "overlapping or touching periods" (otherwise both are kept), with dropped
    titles/notes listed under `affiliation_combined`. | Replace with "same organization and type with overlapping or touching
    periods combine (earliest start, latest end, open wins, roles unioned); otherwise both are kept; differing titles and notes
    are listed as dropped under `affiliation_combined`".

34. **Low** | 05 §2.1 Status bullet | **Vague citation** "merge D-decision 'settled by inspection'". | "(merging decisions,
    'Settled by inspection': contact status `duplicate` is left as is; detection does not set it)".

35. **Low** | 06 §4 "Long operations" | **Execution vehicle differs from the merge design.** The merging decisions say "Queued
    work + worker Lambda, one transaction; CRM has no job engine"; 06 lists organization merges under "Platform job engine / Step
    Functions". | Verify in `modules/merges` and correct to whichever is true; if merges stay on CRM's queued worker, say so.

36. **Low** | 08 §4 "Work packages"; 15 CRM-M9 scope and Acceptance | **Phase split ignored.** Lifecycle 07 §3 ships WP-L1
    (discovery, "party is selectable") and WP-P1 (discovery in queries and Quick Search) in phase 1 (archive, CRM-M0), and T-1 is
    the archive test. M9 claims all of WP-L1, WP-P1 and T-1..T-4. | M9 scope: "WP-L1 and WP-P1 (deletion parts)"; Acceptance
    "T-2..T-4 (T-1 is accepted in CRM-M0 with archive)".

37. **Low** | 14 EV-01; 15 CRM-M1 scope | **Events list incomplete.** 06 §5 and 12 §5 publish `.archived` and `.unarchived`;
    EV-01 and M1 scope only created/updated/deleted/merged. | Add to EV-01 Work and M1: "`crm.contact.archived/.unarchived`,
    `crm.organization.archived/.unarchived` once LC-01 lands".

38. **Low** | 15 §1 item 2 | **Untestable finish-line clause.** "Referenced by every application" includes applications not on
    master (Events, Certifications, Communities). | "A merge of a contact referenced in every application listed in 12 §2 that
    is deployed to dev at the time of the run leaves zero references to the merged-away id (registry walk)."

39. **Low** | 01 P-8; 04 I-8; 06 A-5 | **W2 nuance missing.** A stale write that changes nothing answers 200 `unchanged`, not
    409 (standard W2, "A no-op is not refused for a stale version"). | Append to I-8: "a write whose result equals the stored state
    answers 200 unchanged even with a stale version; a missing version is still 400".

40. **Low** | 13 §3 CRM-Q5 "Blocks"; 09 §7 | **Wrong dependency.** Removing the fake Engagement Scoring screen is already required
    by D-02/D-53 and P-9; CRM Processes is unrelated to engagement yet is tied to CRM-Q5. | CRM-Q5 Blocks: "nothing now (the fake
    screen is removed in CRM-M2 under P-9)"; 09 §7: "Engagement Scoring (CRM-Q5) and CRM Processes are removed from live consoles
    until designed (P-9)."

41. **Low** | 01 §2 vs 02 §4; 04 §3.3, 08 §2 vs 17 §1 | **Identifier namespaces collide.** Principles P-1..P-12 versus 1.0
    patterns P1..P13; record-lifecycle R-1..R-8 versus risks R-1..R-14 (04 §3.3 "record-lifecycle R-1" next to 17's R-1). | Prefix
    the borrowed ids where cited: "lifecycle R-1", "1.0 pattern P5"; or rename this document's risks "CRM-R-1..".

42. **Low** | 03 §3 row "1.0 functionality lost" | **L1-05 is listed as lost but 14 marks it replaced.** | Change "L1-01..06"
    to "L1-01..04, L1-06".

43. **Low** | README "Binding sub-specifications"; 13 CRM-ADR-01 | **Two binding sources are cited as if on this repository's
    master.** `docs/proposals/record-lifecycle/` and `docs/standards/record-write-contracts.md` exist only on branches
    `agent/crm-acc-w3-arch-lifecycle` (`6873e3b6c`) and `agent/crm-acc-w3-arch-writes` (`55c0cefba`). | Cite branch and commit in
    the README as 01 §4 already does, until they land.

44. **Low** | 05 §2.3 "History" bullet | **Display default differs from the binding design.** "Upcoming, Current and Past are
    shown separately"; affiliations README §0 item 1 shows all affiliations in one section by default, and §3.2a adds an All /
    Current / Upcoming / Past filter read from the service's `effective`. | "One Organization Affiliations section shows every
    affiliation by default, each labelled Upcoming, Current or Past from the service's `effective` value (workspace business
    date, Ruled D-10, D-12); the All/Current/Upcoming/Past filter and its counts read the same value."

45. **Low** | 05 §2.1 Status bullet; 01 P-1 | **Rule owned in two places.** "A non-active contact cannot be newly linked to a
    portal login (Platform enforces at linking)" places a CRM eligibility rule in Platform, against P-1. | "CRM publishes
    discoverability (status and archive) through `crm.party_status`; Platform's login linking refuses a non-discoverable contact
    using it (1.0 H-2)."

## 29. Review resolutions

## Review resolutions

How the three review reports were applied (2026-10-19 evening). Every **high** and **medium** finding was applied; low
findings were applied except where noted.

### r1 — capabilities overlooked in 2.0 (27 findings)

All applied in [14](#15-14-implementation-gap-matrix-deliverable-b) after re-baselining at services-crm `bb55ae7a`: DO-12 Complete (service); DQ-08
Complete (verify); DO-07 Partial; UI-03/UI-04 narrowed to the list and designer; OR-05 Partial; UI-14 and AC-06 console-only;
SE-05 Partial; UI-15 narrowed; OR-02 Partial (Change Type live); MG-04 Partial (picker built, unwired); PO rows cite existing
foundations; new rows X-10 (eligibility condition types), CF-07 (managed files), CF-08 (contact form designer); eleven
rulings' service halves marked Complete (service); DQ-05 corrected to Missing. Chapters 03, 07, 10 updated to match.

### r2 — 1.0 functionality missing (19 gaps, 6 corrections)

New rows: SR-10b (communication preferences queryable), SR-11 (role-holder columns), UI-16 (Communications tab), UI-17
(staff e-mail), UI-18 (quick add), PO-05 (profile confirmation), OR-07 (sign-up organization by domain), CF-06 (computed
fields), CI-09 (sending withholds deceased and opted-out), UI-19 (CRM home), DO-16 (mass-update templates); UI-08 extended to
header badges; L1-18..L1-26 added; L1-12 reworded. Corrections M-01 (1.0 mass update existed), M-02 (event forwarding was
general), M-03 (five task statuses), M-04 (deceased is not suppressed today), M-05 (sign-up identity is partial), M-06
(delegates never worked) applied in 02, 04 and 05. The Sales application became decision CRM-Q11.

### r3 — consistency and feasibility (45 findings)

- High 1 (console bulk delete): recorded in 08 §6 and LC-02; **not** removed unilaterally, because Andrew decided today's
  delete stays; put to him as CRM-Q10.
- High 2 (create retry): 06 A-2 now follows standard W5.
- High 3 (portal authorization owner): CRM-ADR-11; 05 §7, 11 §2, PO-04, L1-05 updated.
- High 4 (inapplicable roles): 05 §2.5 states today's filter-only rule; CRM-Q9 added.
- High 5 (affiliation mutations): conflict C-6 recorded and resolved toward D-09/W3; 05 §2.3 heading and text amended; C-7 for D-25.
- High 6 (urgent items gated): protections moved into the retroactive milestones; each needs Andrew's recorded decision.
- High 7 (CRM-Q3 re-asks a settled rule): first recorded as an ADR; then, because R2-DEV-001 counts only Andrew's recorded
  decisions as approval (point raised by the CRM build session), restored as decision CRM-Q3 with the rule written out.
- High 8 (M1/M2 cycle): milestones re-cut into vertical slices per R2-DEV-001 (CRM-M9..M23); no cycle.
- High 9 (D-29 and others missing): SR-10 added; D-16, D-18, D-26, D-28 listed in the coverage check.
- High 10 (participants in other repositories): moved to each application's own plan ("ext"); CRM milestones do not wait.
- High 11 (P0 release consequence): stated in 08 §1 and LC-03.
- Medium 12–29: applied (re-baseline; task statuses; A-8 archived writes; LC-06; wave-4 items renamed W4-n; scope/matrix
  alignment; acceptance criteria per milestone; dependencies; R2-DEV-001 milestone plan; TK-07/TK-09 split; Q2/Q4 order;
  full decision write-ups in 13 §3; dispositions per lifecycle 06; published-reads wording; region-id migration scope;
  engine ownership; UI-08 moved to other applications; CRM-Q6 wording).
- Low 30–45: applied, except 41 (identifier namespaces): borrowed ids are prefixed where they appear next to this
  document's own ("lifecycle R-1", "1.0 pattern P5"); the risk list keeps R-n.
