> For AI agents: the complete documentation index is available at /llms.txt, the full documentation bundle is available at /llms-full.txt.

# Contacts Schema

> **Status boundary**：本文的 Contacts inventory 是 2026-06 snapshot；实施前必须重新核查 live schema。原 §3/§4 的 “Final / Delta” 是历史 proposal，尤其 `task_progress_events`、global `closeResult` 和旧 Task Decision contract 不再是 Target。
>
> **Snapshot source**: `callytics-common/src/db/schema/contacts.ts` (verified 2026-06-01)
> **Task V3 selected Target**: [Task V3 Product / Domain Contract](/product-design/v3/tasks-feature/task-domain-lifecycle.md)
> **Current rollout evidence boundary**: [Task V3 Rollout 状态快照](/product-design/v3/tasks-feature/task-v3-rollout-status.md)
> **Historical Task V2+ audit**: [工程审计与设计建议](/product-design/v2/tasks-feature/task-v2-plus-engineering-audit.md)
> **表名**: `contacts` ｜ **复合主键**: `(phone, storeId)` (State C 终态,callytics-infrastructure#599)
> 阅读顺序:**2026-06 snapshot** (§2) → **historical proposed target** (§3) → **historical delta** (§4)。
>
> **Name trust scoring**:`firstNameTrustScore` 决定多写入方 UPSERT 时谁赢,详见 [name-trust.md](/product-design/v2/contacts-feature/name-trust.md)。

***

## 1. 表说明

Contacts 是中心化客户档案表,聚合来自 `calls` / `messages` / Lead pipeline / AI 分析的数据。一个号码在不同门店下是独立客户记录(跨品牌隔离由 `storeId` UUID 自动保证)。

- **State C 终态**:复合主键 `(phone, storeId)`,`franchiseId` / `accountId` 降级为审计字段(superseded by `storeId` for isolation)
- **所有枚举字段都是 `text` 列 + 注释里写约束**,**无 DB CHECK**,**无 const 数组** — 跟 `tasks` 不同(tasks 有 DB CHECK + UI 常量强制)。contacts 枚举值靠写入方(AI prompt + Lambda 代码)自觉遵守注释,数据库不拦非法值
- **写入者分类**:
  - **code** = 数据管道自动写入(通话管道 / Lead 管道 / 温度衰减规则)
  - **ai** = AI 分析写入(Per-Call / Daily Batch / On-Demand)
  - **ai+code** = AI 分析 + 规则映射共同决定
  - **ai+staff** = AI 可写入,员工可通过 UI 覆盖

***

## 2. Current Schema(2026-06-01 live)

### 2.1 `contacts` 表全字段(30 字段)

按代码存储顺序排列(PK + 隔离键在前,身份 → 生命周期 → 运营 → 活动 → 行动 → Lead 状态 → 障碍 → 分析 → 画像 → Contact Analysis 追踪 → 风险 → 时间戳)。复杂枚举(`firstNameTrustScore` / `leadStatus`)在 §2.2 展开。**~~删除线~~ + ⚠️ 标记**的字段在 Final 退役,详见 §4.1。

| #  | Field (TS)              | SQL 列                       | Type        | Nullable     | Enum / Values                                                                  | Writer   | 用途                                                                                                                                                                                                          |
| -- | ----------------------- | --------------------------- | ----------- | ------------ | ------------------------------------------------------------------------------ | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 1  | `phone`                 | `phone`                     | text        | NOT NULL, PK | —                                                                              | code     | E.164 格式电话号码,隔离键之一                                                                                                                                                                                          |
| 2  | `franchiseId`           | `franchise_id`              | text        | NOT NULL     | —                                                                              | code     | 所属品牌(审计字段,**已降级**,不再作为隔离键)                                                                                                                                                                                  |
| 3  | `accountId`             | `account_id`                | text        | NOT NULL     | —                                                                              | code     | RC providerAccountId(审计字段,**已降级**)                                                                                                                                                                          |
| 4  | `storeId`               | `store_id`                  | text        | NOT NULL, PK | —                                                                              | code     | DynamoDB StoresV2 store UUID,**终态隔离键**。由 ai-analysis-processor / message-processor / contacts-analyzer 经 `resolvePhoneIdentity()` 从 PhoneStoreAssignments 解析后写入                                             |
| 5  | `firstName`             | `first_name`                | text        | NULL         | —                                                                              | code     | 名                                                                                                                                                                                                           |
| 6  | `lastName`              | `last_name`                 | text        | NULL         | —                                                                              | code     | 姓                                                                                                                                                                                                           |
| 7  | `firstNameTrustScore`   | `first_name_trust_score`    | smallint    | DEFAULT 0    | (6 值)— 见 [§2.2.1](#_2-2-1-firstnametrustscore-—-6-levels)                      | code     | 名字来源的信任分。UPSERT 时高分覆盖低分。详见 [name-trust.md](/product-design/v2/contacts-feature/name-trust.md)                                                                                                               |
| 8  | `firstNameUpdatedAt`    | `first_name_updated_at`     | timestamptz | NULL         | —                                                                              | code     | `first_name` 最后被任何来源更新的时间(审计)                                                                                                                                                                               |
| 9  | `lifecycleStage`        | `lifecycle_stage`           | text        | NULL         | `lead` \| `member` \| `churned` \| `unknown`                                   | ai+code  | 客户生命周期阶段(4 值)                                                                                                                                                                                               |
| 10 | `lifecycleState`        | `lifecycle_state`           | text        | NULL         | `active` \| `paused` \| `terminal`                                             | ai       | 生命周期状态(3 值)。**不可派生**:churned stage 对应 active / terminal 两值,active 需要 AI 从 transcripts 判断 "customer-initiated re-engagement" 语义信号(prompt-builder.ts:154-157, 181-195)。`lead_declined` close result 触发条件依赖该字段 |
| 11 | `notes`                 | `notes`                     | text        | NULL         | —                                                                              | ai+staff | 备注                                                                                                                                                                                                          |
| 12 | `doNotContact`          | `do_not_contact`            | boolean     | NULL         | true / false                                                                   | ai+staff | DNC 标记,全渠道停止触达。**Sticky** — AI 不可反转,代码保证                                                                                                                                                                    |
| 13 | `hasCardOnFile`         | `has_card_on_file`          | boolean     | NULL         | true / false                                                                   | code     | 客户是否有信用卡存档(来自 POS/CRM 同步)                                                                                                                                                                                   |
| 14 | `lastActivityAt`        | `last_activity_at`          | timestamptz | NULL         | —                                                                              | code     | 最后活动时间(任何互动都更新)。Forward-only:只能向前推进,不可倒退                                                                                                                                                                    |
| 15 | ~~`actionNeeded`~~      | `action_needed`             | boolean     | NULL         | true / false                                                                   | ai       | 跨通话综合判断是否需要跟进。⚠️ **Final 退役**:= `EXISTS(open task for this contact)`,由代码从 task 派生。详见 §4.1                                                                                                                   |
| 16 | `actionNeededReason`    | `action_needed_reason`      | text        | NULL         | —                                                                              | ai       | 为什么需要行动                                                                                                                                                                                                     |
| 17 | `suggestedActions`      | `suggested_actions`         | jsonb       | NULL         | `Array<{ action, reason, priority: 'high'\|'medium'\|'low', priorityReason }>` | ai       | AI 建议的下一步操作列表。⚠️ 见 [§6 Open Question](#6-open-questions-待产品拍板)                                                                                                                                              |
| 18 | `leadStatus`            | `lead_status`               | text        | NULL         | (12 值)— 见 [§2.2.2](#_2-2-2-leadstatus-—-12-values)                             | ai       | Lead 漏斗状态。**Lead 状态存在 contacts 表,不在 leads 表**。前端漏斗与展示口径见 [漏斗与状态](/product-design/v2/lead-tracker-feature/lead-funnel-status.md)                                                                             |
| 19 | `leadStatusReason`      | `lead_status_reason`        | text        | NULL         | —                                                                              | ai       | 为什么是这个状态                                                                                                                                                                                                    |
| 20 | `leadObjections`        | `lead_objections`           | jsonb       | NULL         | `string[]`                                                                     | ai       | 犹豫中的顾虑标签                                                                                                                                                                                                    |
| 21 | `leadRejectionReasons`  | `lead_rejection_reasons`    | jsonb       | NULL         | `string[]`                                                                     | ai       | 明确拒绝的原因标签                                                                                                                                                                                                   |
| 22 | `purchaseIntent`        | `purchase_intent`           | text        | NULL         | `high` \| `medium` \| `low`                                                    | ai       | 购买意向等级(3 值)                                                                                                                                                                                                 |
| 23 | `purchaseIntentReason`  | `purchase_intent_reason`    | text        | NULL         | —                                                                              | ai       | 为什么是这个意向等级                                                                                                                                                                                                  |
| 24 | `goals`                 | `goals`                     | jsonb       | NULL         | `Array<{ goal: string; reason: string }>`                                      | ai       | 客户目标列表                                                                                                                                                                                                      |
| 25 | `customerSummary`       | `customer_summary`          | text        | NULL         | —                                                                              | ai       | 综合所有通话和 SMS 的整体画像摘要                                                                                                                                                                                         |
| 26 | `lastContactAnalysisAt` | `last_contact_analysis_at`  | timestamptz | NULL         | —                                                                              | code     | Contact Analysis 最后一次分析完成的时间。用于 Contact Analysis cooldown gate + Circuit Breaker(per-contact 防风暴层)                                                                                                          |
| 27 | `doNotContactUpdatedBy` | `do_not_contact_updated_by` | text        | NULL         | `staff` \| `ai` \| `system` (3 值)                                              | ai+code  | DNC 标记来源,用于 TCPA 合规审计追踪                                                                                                                                                                                     |
| 28 | `hasOpenComplaint`      | `has_open_complaint`        | boolean     | NULL         | true / false                                                                   | ai+code  | 当前是否有未解决的投诉                                                                                                                                                                                                 |
| 29 | `createdAt`             | `created_at`                | timestamptz | NOT NULL     | DEFAULT NOW()                                                                  | code     | 首次创建时间                                                                                                                                                                                                      |
| 30 | `updatedAt`             | `updated_at`                | timestamptz | NOT NULL     | DEFAULT NOW(), $onUpdate                                                       | code     | 最后更新时间(Drizzle `db.update()` 时自动刷新)                                                                                                                                                                         |

***

### 2.2 枚举清单(Current)

#### 2.2.1 `firstNameTrustScore` — 6 levels

UPSERT 时按 score 比较,高分覆盖低分;同分时新的非空值赢。详见 [name-trust.md](/product-design/v2/contacts-feature/name-trust.md)。

| Score | Source               | 含义                   |
| ----- | -------------------- | -------------------- |
| `100` | `staff`              | 员工手动输入(最权威)          |
| `80`  | `lead`               | Lead 表来源(报名表 / 表单填写) |
| `60`  | `rc_api`             | RingCentral 联系人 API  |
| `40`  | `ai_transcript`      | AI 通话转录推断            |
| `20`  | `webhook`            | 第三方 webhook          |
| `0`   | `legacy` / `unknown` | 历史数据 / 来源未知          |

#### 2.2.2 `leadStatus` — 12 values

Lead 漏斗状态(`ai` 写入)。前端只展示其中 9 个 — `showed` / `trialed` / `converted` 依赖线下数据,V1 不展示。完整映射见 [lead-funnel-status.md](/product-design/v2/lead-tracker-feature/lead-funnel-status.md)。

| Value            | 含义             |
| ---------------- | -------------- |
| `new`            | 新 lead,尚未联系    |
| `attempted`      | 已尝试联系,未接通      |
| `connected`      | 已接通并对话         |
| `booked`         | 已预约到店 / 体验课    |
| `showed`         | 已到店(依赖线下数据)    |
| `trialed`        | 已体验(依赖线下数据)    |
| `converted`      | 已转化为会员(依赖线下数据) |
| `unreachable`    | 多次尝试无法触达       |
| `lost_contact`   | 失联(原本接通过但断联)   |
| `neglected`      | 长时间未跟进         |
| `bad_timing`     | 时机不对(暂缓)       |
| `not_interested` | 明确不感兴趣         |

***

### 2.3 Indexes(Current)

| Index                         | Columns                                    | 用途                                                     |
| ----------------------------- | ------------------------------------------ | ------------------------------------------------------ |
| Primary Key (复合)              | `(phone, storeId)`                         | State C 终态主键,同一号码在不同门店是独立记录                            |
| `idx_contacts_franchise`      | `(franchiseId)`                            | 审计                                                     |
| `idx_contacts_site`           | `(accountId)`                              | 审计                                                     |
| `idx_contacts_franchise_site` | `(franchiseId, accountId)`                 | 审计                                                     |
| `idx_contacts_lead_status`    | `(franchiseId, accountId, leadStatus)`     | Lead 漏斗查询                                              |
| `idx_contacts_last_activity`  | `(franchiseId, accountId, lastActivityAt)` | 活跃度查询                                                  |
| `idx_contacts_action_needed`  | `(franchiseId, accountId, actionNeeded)`   | 行动建议查询。⚠️ **Final 移除**(随 `actionNeeded` 字段一并退役,见 §4.1) |
| `idx_contacts_store_id`       | `(storeId)`                                | partial query pattern(PK 已含,显式 index 备用)               |

***

## 3. Historical proposed schema（原 Final）

### 3.1 `contacts` 表全字段(29 字段,退役 1 个派生字段)

设计原则:**Code as Guardrail, AI as Judgment** —— AI 只输出不可派生的语义判断,可派生的值由代码派生(详见 §3.2)。从 Current 移除 1 个派生冗余字段:`actionNeeded`(#15)。`lifecycleState` 保留(churned re-engage 等需要 AI 语义判断,见 §3.2)。

| #  | Field (TS)              | SQL 列                       | Type        | Nullable     | Enum / Values                                         | Writer   | 用途                                                                                    |
| -- | ----------------------- | --------------------------- | ----------- | ------------ | ----------------------------------------------------- | -------- | ------------------------------------------------------------------------------------- |
| 1  | `phone`                 | `phone`                     | text        | NOT NULL, PK | —                                                     | code     | E.164 格式电话号码,隔离键之一                                                                    |
| 2  | `franchiseId`           | `franchise_id`              | text        | NOT NULL     | —                                                     | code     | 所属品牌(审计字段)                                                                            |
| 3  | `accountId`             | `account_id`                | text        | NOT NULL     | —                                                     | code     | RC providerAccountId(审计字段)                                                            |
| 4  | `storeId`               | `store_id`                  | text        | NOT NULL, PK | —                                                     | code     | DynamoDB StoresV2 store UUID,终态隔离键                                                    |
| 5  | `firstName`             | `first_name`                | text        | NULL         | —                                                     | code     | 名                                                                                     |
| 6  | `lastName`              | `last_name`                 | text        | NULL         | —                                                     | code     | 姓                                                                                     |
| 7  | `firstNameTrustScore`   | `first_name_trust_score`    | smallint    | DEFAULT 0    | (6 值)— 见 §2.2.1                                       | code     | 名字来源的信任分                                                                              |
| 8  | `firstNameUpdatedAt`    | `first_name_updated_at`     | timestamptz | NULL         | —                                                     | code     | first\_name 最后更新时间                                                                    |
| 9  | `lifecycleStage`        | `lifecycle_stage`           | text        | NULL         | `lead` \| `member` \| `churned` \| `unknown`          | ai       | 客户生命周期阶段(4 值)                                                                         |
| 10 | `lifecycleState`        | `lifecycle_state`           | text        | NULL         | `active` \| `paused` \| `terminal`                    | ai       | 生命周期状态(3 值)。churned re-engage 等需 AI 语义判断,不可纯派生。`lead_declined` close result 触发条件依赖该字段 |
| 11 | `notes`                 | `notes`                     | text        | NULL         | —                                                     | ai+staff | 备注                                                                                    |
| 12 | `doNotContact`          | `do_not_contact`            | boolean     | NULL         | true / false                                          | ai+staff | DNC 标记,Sticky                                                                         |
| 13 | `hasCardOnFile`         | `has_card_on_file`          | boolean     | NULL         | true / false                                          | code     | 信用卡存档                                                                                 |
| 14 | `lastActivityAt`        | `last_activity_at`          | timestamptz | NULL         | —                                                     | code     | 最后活动时间,Forward-only                                                                   |
| 15 | `actionNeededReason`    | `action_needed_reason`      | text        | NULL         | —                                                     | ai       | 为什么需要行动(保留 reason 文本,boolean 退役)                                                      |
| 16 | `suggestedActions`      | `suggested_actions`         | jsonb       | NULL         | `Array<{ action, reason, priority, priorityReason }>` | ai       | AI 建议的下一步操作列表                                                                         |
| 17 | `leadStatus`            | `lead_status`               | text        | NULL         | (12 值)— 见 §2.2.2                                      | ai       | Lead 漏斗状态                                                                             |
| 18 | `leadStatusReason`      | `lead_status_reason`        | text        | NULL         | —                                                     | ai       | 为什么是这个状态                                                                              |
| 19 | `leadObjections`        | `lead_objections`           | jsonb       | NULL         | `string[]`                                            | ai       | 犹豫中的顾虑标签                                                                              |
| 20 | `leadRejectionReasons`  | `lead_rejection_reasons`    | jsonb       | NULL         | `string[]`                                            | ai       | 明确拒绝的原因标签                                                                             |
| 21 | `purchaseIntent`        | `purchase_intent`           | text        | NULL         | `high` \| `medium` \| `low`                           | ai       | 购买意向等级                                                                                |
| 22 | `purchaseIntentReason`  | `purchase_intent_reason`    | text        | NULL         | —                                                     | ai       | 为什么是这个意向等级                                                                            |
| 23 | `goals`                 | `goals`                     | jsonb       | NULL         | `Array<{ goal, reason }>`                             | ai       | 客户目标列表                                                                                |
| 24 | `customerSummary`       | `customer_summary`          | text        | NULL         | —                                                     | ai       | 综合画像摘要                                                                                |
| 25 | `lastContactAnalysisAt` | `last_contact_analysis_at`  | timestamptz | NULL         | —                                                     | code     | Contact Analysis 最后一次分析完成时间                                                           |
| 26 | `doNotContactUpdatedBy` | `do_not_contact_updated_by` | text        | NULL         | `staff` \| `ai` \| `system`                           | ai+code  | DNC 标记来源(TCPA 审计)                                                                     |
| 27 | `hasOpenComplaint`      | `has_open_complaint`        | boolean     | NULL         | true / false                                          | ai+code  | 是否有未解决投诉                                                                              |
| 28 | `createdAt`             | `created_at`                | timestamptz | NOT NULL     | DEFAULT NOW()                                         | code     | 首次创建时间                                                                                |
| 29 | `updatedAt`             | `updated_at`                | timestamptz | NOT NULL     | DEFAULT NOW(), $onUpdate                              | code     | 最后更新时间                                                                                |

### 3.2 退役字段的真值源(派生方式)

| 退役字段           | 真值源(从哪派生)                                                                            | 派生时机                                                         |
| -------------- | ------------------------------------------------------------------------------------ | ------------------------------------------------------------ |
| `actionNeeded` | `EXISTS(SELECT 1 FROM tasks WHERE contact_phone=? AND store_id=? AND status='open')` | 现查(KPI / badge 改成 JOIN tasks)或 task 写入时维护 denormalized count |

> **为什么 `lifecycleState` 不能退役**(2026-06-02 engineer feedback):prompt-builder.ts:154-157 真值表显示 churned stage 对应 `active` / `terminal` 两值,active 需要 AI 从 transcripts 判断 "customer-initiated re-engagement" 语义信号 —— 代码无法纯派生。**`f(lifecycleStage, leadStatus)` 公式 incomplete**。此外,`lead_declined` close result 设计依赖该字段作为触发信号(见 `docs/product-design/v1/prompt-improvement/2026-05-prompt-open-issues.md:125`)—— 它有 downstream consumer,不是单纯的派生 cache。
>
> **为什么 `actionNeeded` 退役**:历史上 task 系统不完整(closeResult 混乱、无清晰 open/closed 语义),没法可靠用"有无 open task"代表"要不要跟进",所以 AI 单独输出 boolean 当缓存。task 4 层 model + Code as Guardrail 原则下,变成冗余。
>
> **跨表关联**：历史设计里的 `tasks.actionNeeded` 是同类冗余 signal 的“另一半”；V3 Target 用简单 lifecycle 与 derived projection 分开表达，见 [Task V3 Product / Domain Contract §5.1](/product-design/v3/tasks-feature/task-domain-lifecycle.md#51-数据库只保存简单的-lifecycle)。历史演进原则见 [unified-pipeline-final.md](/product-design/v2/unified-pipeline/unified-pipeline-final.md)「两个 actionNeeded 都应退役」。
>
> **仍真需要 AI 输出的字段(不退役,对照)**:`lifecycleStage`(churned/unknown 带 leadStatus 表达不了的独立信息)、`leadStatus`、`purchaseIntent`、`hasOpenComplaint`(有状态机的跨通话聚合)、`customerSummary`、`goals` 等 —— 这些是不可派生的语义判断。

***

## 4. Historical delta（2026-06 snapshot → 旧 proposed Final）

### 4.1 Removed(字段退役 + 索引一并移除)

| Removed                              | Where      | 由什么派生(真值源)                                                                                   | Phase         | Reason                                                                                                                                                          |
| ------------------------------------ | ---------- | -------------------------------------------------------------------------------------------- | ------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `contacts.actionNeeded`(Current #15) | contacts 表 | `EXISTS(SELECT 1 FROM tasks WHERE contact_phone=? AND store_id=? AND status='open')`(见 §3.2) | **分 2 phase** | studio-api 真在读这列(`routes/v3/contacts.ts` contacts list + `routes/v3/leads.ts:209` KPI summary `COUNT(*) FILTER (WHERE c.action_needed = true)`)。退役不是零成本,需先迁移读路径 |
| `idx_contacts_action_needed`         | indexes    | —                                                                                            | Phase 2       | 随 `actionNeeded` 字段一并移除(不再需要这个 index)                                                                                                                           |

> **`lifecycleState` 不退役**(2026-06-02 engineer feedback,撤销之前的退役决定):churned re-engage 等 case 不能纯派生,需要 AI 语义判断;`lead_declined` close result 设计依赖该字段作为触发信号。详见 §3.2。

#### `contacts.actionNeeded` 退役 Phase 拆解

| Phase                          | 动作                                                                                                                                                                                                                                                                                                                                                   |
| ------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Phase 1**                    | ① 改 `routes/v3/contacts.ts` + `routes/v3/leads.ts` 把 `c.action_needed` 替换成 `EXISTS(SELECT 1 FROM tasks t WHERE t.contact_phone=c.phone AND t.store_id=c.store_id AND t.status='open')` 子查询;② Leads KPI 改成"数有 open task 的 contact" — `COUNT(DISTINCT c.phone) FILTER (WHERE EXISTS ...)`;③ contact\_analyzer 暂时仍写该字段(保险,observe-only);④ **不动 schema** |
| **Phase 1 observation period** | 1-2 周 staging 上 parallel run 新旧 query 对比 KPI / contacts list 数据,verify EXISTS 口径无漂移                                                                                                                                                                                                                                                                  |
| **Phase 2**                    | observe 通过 → DROP COLUMN `action_needed` + 停 contact\_analyzer 写入 + 删 `idx_contacts_action_needed` index                                                                                                                                                                                                                                             |

> **EXISTS perf 担心**:tasks 表 `idx_tasks_contact_status` 现已 cover `(contactPhone, franchiseId, accountId, status)`,Phase 1 改名后改为 `(contactPhone, storeId, status)`(随 `tasks.store_id NOT NULL` migration 同步)。EXISTS 子查询能走 index,dashboard 高频查也 OK。无需 denormalized counter。

### 4.2 Modified

无字段名变或语义改的 modify 项(枚举值无变化,沿用 §2.2)。

### 4.3 Added

无新增字段或新表(Final 是从 Current 删 1 字段 `actionNeeded`,不加新东西)。

### 4.4 Unchanged(明确保留,reviewer 不用 verify)

`phone` / `franchiseId` / `accountId` / `storeId` / `firstName` / `lastName` / `firstNameTrustScore` / `firstNameUpdatedAt` / `lifecycleStage` / `lifecycleState` / `notes` / `doNotContact` / `hasCardOnFile` / `lastActivityAt` / `actionNeededReason` / `suggestedActions` / `leadStatus` / `leadStatusReason` / `leadObjections` / `leadRejectionReasons` / `purchaseIntent` / `purchaseIntentReason` / `goals` / `customerSummary` / `lastContactAnalysisAt` / `doNotContactUpdatedBy` / `hasOpenComplaint` / `createdAt` / `updatedAt`(共 29 字段)。

枚举(`firstNameTrustScore` 6 值 + `leadStatus` 12 值)、其余 7 个 indexes 均不变(沿用 §2.2 / §2.3)。

> **`actionNeededReason` / `suggestedActions` 仍保留** — 它们是承载内容的字段(reason 文本被前端实际显示、suggestedActions 是 AI 给出的具体建议),不是冗余 boolean。但**它们最终归属 contacts 还是并入 task 实体待定**,见 §6 Open Questions。

***

## 5. Prompt Output ↔ Schema 字段对照(给 reviewer 用)

> Reviewer 用本表逐一比对 prompt 输出 JSON 字段是否能写入 contacts 表。
> Prompt 全文存在 [`../unified-pipeline/prompts/`](../unified-pipeline/prompts/)。

### 5.1 Current(Phase 1)— Contact Analyzer 一个大 prompt

| Prompt                                            | 输出关键字段                                                                                                                                                                                                                                                                                                                                                         | 写哪张表                                                                             | Phase 1 状态                                                                                                   |
| ------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------ |
| 01 Triage / 02 Classify / 03 Verify / 04 Coaching | call routing / 33 calls 字段 / coaching                                                                                                                                                                                                                                                                                                                          | 写 calls 表(见 [calls-schema.md](/product-design/v2/calls-feature/calls-schema.md)) | n/a for contacts                                                                                             |
| **06 Contact Analyzer**                           | `lifecycleStage` / `lifecycleState` / `leadStatus` + `leadStatusReason` / `leadObjections` / `leadRejectionReasons` / `purchaseIntent` + `purchaseIntentReason` / `goals` / `customerSummary` / `actionNeeded` + `actionNeededReason` / `suggestedActions` / `hasOpenComplaint` / `doNotContact` + `doNotContactUpdatedBy` + Task Decision (`taskDecisions[]`) | contacts 表 + tasks 表 + contact\_timeline                                         | 一次 AI 调用同时输出 18 个 contact 分析字段 + task decisions。Phase 1 不拆 prompt,只改下游(走 Contact Writer / Task Orchestrator) |
| 07 Task Playbook                                  | read-only,不写库                                                                                                                                                                                                                                                                                                                                                  | n/a                                                                              | n/a                                                                                                          |

### 5.2 Historical Target — 拆成 Contact Profile + Task Decision 两个 prompt

unified-pipeline-final.md §1 把 Contact Analyzer 拆成 Stage A + Stage B,**两个 stage 在同一次 Lambda 调用里跑**,共享内存:

| Stage | Prompt                           | 输出关键字段                                                                                                                                                                                                                                                                          | 写哪张表                                                                                                  | Schema 影响                                                                                                   |
| ----- | -------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------- |
| A     | **05 Contact Profile**(Final 新拆) | `lifecycleStage` / `lifecycleState` / `leadStatus` / `leadStatusReason` / `customerSummary` / `goals` / `purchaseIntent` / `purchaseIntentReason` / `leadObjections` / `leadRejectionReasons` / `hasOpenComplaint` / `doNotContact` / `actionNeededReason` / `suggestedActions` | contacts 表(本文档)                                                                                       | **不改 schema** — 输出字段对应 §3.1 Final 29 字段。**`actionNeeded` 已退役**,prompt 不再输出该字段(由 EXISTS open task 派生,见 §3.2) |
| B     | **06 Task Decision**(Final 新拆)   | `taskDecisions[]` (action ∈ `create_open` / `create_closed` / `close` / `update` / `record_progress` / `reopen` / `no_op`,plus taskId / typeCategory / progressType / closeResult / evidence / confidence)                                                                      | tasks + task\_progress\_events(见 [tasks-schema.md](/product-design/v2/tasks-feature/tasks-schema.md)) | n/a for contacts                                                                                            |

**关键判断**:Contact Profile prompt 拆分是 **prompt architecture refactor**(让 contact 画像和 task decision 分别迭代),**不是 schema migration**。reviewer 验:① prompt JSON 字段落在 §3.1 Final 29 字段内 + 枚举值符合 §2/§3 注释;② 退役字段(`actionNeeded`)不在 prompt 输出中。

### 5.3 Reviewer checklist

| 检查项                                                                        | 怎么验                               |
| -------------------------------------------------------------------------- | --------------------------------- |
| Prompt 输出字段都在 §2 表里?                                                       | grep prompt 文件的 JSON schema,逐字段对照 |
| `lifecycleStage` 输出值 ∈ `{lead, member, churned, unknown}`?                 | prompt 例子 + Lambda 校验代码           |
| `lifecycleState` 输出值 ∈ `{active, paused, terminal}`?                       | 同上                                |
| `leadStatus` 输出值 ∈ §2.2.2 的 12 值?                                          | 同上                                |
| `purchaseIntent` 输出值 ∈ `{high, medium, low}`?                              | 同上                                |
| `goals[]` 每项有 `{goal, reason}` 两字段?                                        | JSON shape                        |
| `suggestedActions[]` 每项有 `{action, reason, priority, priorityReason}` 四字段? | JSON shape                        |
| `doNotContact = true` 后 AI 是否能改回 false?                                    | **不能** — Contact Writer 保证 sticky |

***

## 6. Open Questions(待产品拍板)

来源 unified-pipeline-final.md §4 Open Questions。本表不计入 Final scope,产品决定后再 update §2。

| 问题                                                                         | 背景                                                                                                                         | 影响 schema?                                                         |
| -------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------ |
| ~~`contacts.actionNeeded` 何时 deprecate?~~ **已决**                           | 由 `EXISTS(open task)` 派生。Phase 1 改读路径,Phase 2 DROP COLUMN。详见 §3.2 + §4.1                                                   | Final 移除;`actionNeededReason` / `suggestedActions` 是否并入 task 实体仍待定 |
| ~~`contacts.lifecycleState` 退役?~~ **撤销退役决定**(2026-06-02 engineer feedback) | churned re-engage 等 case 需 AI 语义判断,不可纯派生;`lead_declined` close result 设计依赖该字段。保留,与 `leadStatus` / `purchaseIntent` 同等地位    | 保留字段,无 schema 改动                                                   |
| Contact Writer Phase 1 不统一全部 18 个 AI 字段                                    | Phase 1 只收口 identity / DNC / lastActivityAt;AI profile fields(lifecycle / leadStatus / goals / customerSummary 等)留 Phase 2 | 不改 schema,影响 writer 路径                                             |
| Phase 1 `doNotContactUpdatedBy` 是否包含 `ai_agent`?                           | 当前 3 值 `staff` / `ai` / `system`;tool calling 上线后需要区分 `ai_agent`(autonomous tool)vs `ai`(passive prompt output)            | 可能新增 1 值                                                           |

***

## 7. Appendix A — Re-verification commands

> 2026-06-01 verified。文档 stale(>30 天)时重跑下方命令并比对。

```bash
# 1. contacts 表所有 column + 注释里的枚举
grep -E "^\s*[a-zA-Z]+:|text\(|integer\(|timestamp\(|boolean\(|jsonb\(|smallint\(" \
  ../../../../../callytics-common/src/db/schema/contacts.ts

# 2. 复合主键 + index
grep -E "primaryKey|index\(" \
  ../../../../../callytics-common/src/db/schema/contacts.ts

# 3. Contact Profile prompt 是否独立(Final 拆分状态)
ls ../../../../../callytics-infrastructure/lambda/contacts-analyzer/src/core/
# 当前预期: prompt-builder.ts (1 个未拆) — verified 2026-06-01
# Final 预期: contact-profile-prompt.ts + task-decision-prompt.ts (2 个) — 拆分后更新

# 4. Final design 是否动 contacts schema
grep -i "contacts\." \
  ../../unified-pipeline/unified-pipeline-final.md
# 期望: 只 reference,不修改 schema(verified 2026-06-01 — 仅 prompt 拆分)

# 5. Codex task pipeline final design 是否动 contacts schema
grep -i "contacts\." \
  ../../tasks-feature/design/task-pipeline-deliverable-codex.md
# 期望: 只 reference,不修改 contacts schema(verified 2026-06-01)
```

***

## 8. Cross-References

- Name trust scoring(`firstNameTrustScore` 详细规则): [name-trust.md](/product-design/v2/contacts-feature/name-trust.md)
- Contact 详情页 UI 展示: [contacts-display.md](/product-design/v2/contacts-feature/contacts-display.md)
- Tasks 表(下游 Task Decision prompt 写入): [../tasks-feature/tasks-schema.md](/product-design/v2/tasks-feature/tasks-schema.md)
- Calls 表(上游 per-call AI 写入): [../calls-feature/calls-schema.md](/product-design/v2/calls-feature/calls-schema.md)
- Coaching 字段(物理在 calls 表内): [../coaching-feature/coaching-schema.md](/product-design/v2/coaching-feature/coaching-schema.md)
- Lead funnel 状态映射: [../lead-tracker-feature/lead-funnel-status.md](/product-design/v2/lead-tracker-feature/lead-funnel-status.md)
- Store-level 隔离设计: `docs/system-design/store-level-isolation.md`
- Unified pipeline final design: [../unified-pipeline/unified-pipeline-final.md](/product-design/v2/unified-pipeline/unified-pipeline-final.md)
- Task Pipeline Deliverable (Codex): [../tasks-feature/design/task-pipeline-deliverable-codex.md](/product-design/v2/tasks-feature/design/task-pipeline-deliverable-codex.md)
- All 7 prompts: [../unified-pipeline/prompts/](../unified-pipeline/prompts/)
- 2026-06 snapshot schema: `callytics-common/src/db/schema/contacts.ts`
- Task V3 selected Target: [Task V3 Product / Domain Contract](/product-design/v3/tasks-feature/task-domain-lifecycle.md)
- Task V3 rollout boundary: [Task V3 Rollout 状态快照](/product-design/v3/tasks-feature/task-v3-rollout-status.md)
