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

# Task Accuracy Baseline v1 设计稿

> **Current legacy executable baseline（2026-07-15）**：本文用于解释和回归现有 production prompt/Zod/writer vocabulary；`lead_outreach / lead_follow_up` relay、global `closeResult`、`task_progress_events`、`task_objectives / task_issues` 等不得作为 Target。新 domain/eval contract 见 [Task System Design V3](/product-design/v3/tasks-feature/task-domain-lifecycle.md)。

## 0. 先说结论

Task Accuracy Baseline v1 不是“先做 Vapi 自动外呼”，也不是“让 AI 自由发挥生成任务”。它要先证明一个更底层的能力：

> 用 production prompt + production Zod schema + synthetic scenarios，证明 AI 能稳定把 `contact / call / message / lead / task` context 转换成正确的 `taskDecisions[]`，并且所有高风险 mutation 都被 `Policy Guard` 和 DB constraint 控住。

这一步如果不准，后面接 Vapi、自动短信、AI agent execution、usage metering 都会变成“把错误更快放大”。所以 demo 前最重要的是：**让 task decision 先可信、可复现、可解释、可回归测试**。

| 维度     | Demo 前先做                                                | Demo 后再做                                               |
| ------ | ------------------------------------------------------- | ------------------------------------------------------ |
| AI 判断  | 35-60 个 synthetic scenarios，跑真实模型                       | 多轮历史、更复杂行业、模型路由                                        |
| 写库安全   | 8-12 个 DB replay 验证 writer / policy / constraints       | 并发、approval queue、agent run audit                      |
| 数据输入   | 假设 transcript 已经被上游处理成 `CallSummary` / `MessageSummary` | 原始 transcript → call classifier → contact analyzer 全链路 |
| 对外执行   | 不自动打电话、不自动发消息                                           | 接 Vapi / SMS / Email，但先走 approval                      |
| Schema | 不建议 demo 前改 DB schema                                   | 设计 `task_objectives` / `task_issues` 抽象层               |

### 0.1 今天改什么，不动什么

这次 Task Accuracy Baseline v1 先把“评审对象”和“测试对象”改对。不要把所有未来架构一次性塞进来。

今天改：

- 把 scenario 写成可 review 的业务状态，而不是只写一个简陋表格。
- 每个 scenario 必须写清楚：
  - `existingState`：已有 contact / task / attempt / lifecycle。
  - `newEvidence`：这次 call / SMS / voicemail / lead 到底发生了什么。
  - `expected`：正确的 task decision。
  - `notExpected`：明确不应该出现的 decision。
  - `rationaleZh`：为什么这样判。
- 在 `contacts-analyzer` golden eval 代码里维护同一份 scenario metadata。
- 把 `attemptCount` 放进 analyzer 的 `OPEN TASKS` context，因为 `unable_to_reach` / follow-up threshold 离不开它。
- 先输出 review Markdown，让产品先确认 expected decision，再跑真实模型。

今天不动：

- 不改 production DB schema；`tasks.attempt_count` / `task_progress_events` / `dueAt` 已够支撑 v1。
- 不加 `objective` / `desiredOutcome` column；v1 先用 scenario 的 `objectiveContext` 表达。
- 不接 Vapi。
- 不做 raw audio / speech-to-text 测试。
- 不把 message pipeline 改成主动触发 analyzer；先作为后续 issue。
- 不让 writer 自动在 attempt threshold 时 close；v1 先由 AI decision 在 threshold 上 close，deterministic writer hardening 作为后续 issue。

最重要的 policy decision / implementation gap：

```text
record_progress:
  writer 会写 task_progress_events
  writer 会 attempt_count + 1
  writer 会更新 dueAt
  writer 不会自动 close unable_to_reach

unable_to_reach:
  v1 policy 已确认 threshold = 2
  attemptCount = 0 + failed attempt -> record_progress
  attemptCount = 1 + failed attempt -> close unable_to_reach
  deterministic writer auto-close / progress_then_close 作为 #1142 后续 hardening
```

## 1. 这份设计根据什么来

Scenario 不是只靠 common sense 拍脑袋，也不是只机械覆盖 enum。优先级是：

| 优先级 | 来源                                        | 用来决定什么                                               |
| --- | ----------------------------------------- | ---------------------------------------------------- |
| 1   | production schema / prompt / Policy Guard | AI 能输出什么、哪些 mutation 必须被 reject、哪些字段必须存在             |
| 2   | 当前健身房业务 workflow                          | 什么场景应该开任务、关任务、记进展、不要动                                |
| 3   | AI eval good practice                     | 不做全排列；按 decision point 覆盖风险最高的路径                     |
| 4   | 未来 AI agent vision                        | 把 objective / approval / usage / feedback 的长期方向提前留口子 |

关键原则：

- `taskDecisions[]` 是 proposal，不是 command。
- AI 只做判断和建议；真正写库必须走 `applyTaskAction()`。
- 安全层是刹车：Zod schema、taskRef resolver、`Policy Guard`、writer guard、DB constraint 各拦一段。DNC、duplicate、store mismatch、hallucinated task ref、missing evidence 都要 fail closed。
- Scenario 不做 enum 全排列。我们按“会影响决策的字段”覆盖。
- Demo 只展示稳定通过的 case；不稳定但产品上正确的 case 标成 `knownGap`，不现场赌模型。

## 2. Source of Truth

这份设计以 live code 为准。主要读过的 source：

| Area                   | Source                                                       | 说明                                                                     |
| ---------------------- | ------------------------------------------------------------ | ---------------------------------------------------------------------- |
| Task DB schema         | `callytics-common/src/db/schema/tasks.ts`                    | `status`、`typeCategory`、`closeResult`、`executorType`、dedup index       |
| Progress event schema  | `callytics-common/src/db/schema/task-progress-events.ts`     | `record_progress` 的 `progressType` / `channel` / idempotency           |
| Suggestions            | `callytics-common/src/db/schema/task-suggestions.ts`         | append-only `task_suggestions`，保留 `issueId` 未来挂 `task_issues`          |
| Playbooks              | `callytics-common/src/db/schema/task-playbooks.ts`           | `primaryAction`、`recommendedSteps`、`closeGuidance` 等执行指导               |
| Task orchestrator      | `callytics-common/src/domain/task-orchestrator.ts`           | 所有 task mutation 的统一入口：`applyTaskAction()`                             |
| Policy Guard           | `callytics-common/src/domain/policy-guard.ts`                | DNC、lifecycle allowlist、duplicate、store-match 等                        |
| AI output schema       | `contacts-analyzer/src/core/models.ts`                       | `ContactsAnalysisSchema` 和 `taskDecisions[]` Zod contract              |
| Prompt decision policy | `contacts-analyzer/src/core/prompts/task/decision-policy.ts` | create / close / update / progress / no-op 规则                          |
| Prompt input builder   | `contacts-analyzer/src/core/prompt-builder.ts`               | AI 实际看到哪些 context                                                      |
| Writer                 | `contacts-analyzer/src/infrastructure/neon-repository.ts`    | AI validated output 如何落到 contacts/tasks/timeline/suggestions/playbooks |
| Golden eval            | `contacts-analyzer/scripts/golden-eval/*`                    | 当前真实模型 + production prompt + synthetic fixtures 的 eval 框架              |

## 3. 我们到底在测试什么

### In Scope

| Action            | 目标                                                                                 |
| ----------------- | ---------------------------------------------------------------------------------- |
| `create_open`     | 新的 unresolved customer objective，且没有同类 open task                                   |
| `create_closed`   | 单次互动里 objective 出现又被解决，需要记录结果但不留 open task                                         |
| `close`           | open task 的目标成功、失败、无效、DNC、wrong number、attempt exhausted                           |
| `update`          | 同一个 open objective 有新证据，改变 next action / priority / suggestedActions               |
| `record_progress` | 员工已经尝试联系，但目标还没完成，比如 no answer / voicemail / text sent                              |
| `no-op`           | 没有 unresolved objective，或者 routine confirmation / campaign no reply                |
| Safety            | DNC、duplicate、wrong category、hallucinated task ref、missing evidence、store mismatch |

### Out of Scope for v1

- 不测原始 audio。
- 不把语音重新转 transcript。
- 不做 Vapi 自动外呼。
- 不自动发 SMS / Email / WhatsApp。
- 不实现完整 approval queue。
- 不在 demo 前做 task objective schema migration。

这些不是不重要，而是 demo 前不是最短路径。

## 4. 当前生产流程

现在的 `contacts-analyzer` 不是一个“任意 POST transcript 就生成 task”的公开接口。它是事件触发：上游 pipeline 已经把 call / message / lead 的结构化结果写进 Neon，然后 SQS 触发 analyzer 聚合 context。

```text
Call 入口：现在最接近 Task Accuracy v1 的真实主路径

RingCentral call ended webhook
  -> transcribe-queue
  -> transcribe-processor
  -> S3 transcript / structured call summary
  -> ai-analysis-queue
  -> ai-analysis-processor
  -> Neon: calls / contacts / contact_timeline
  -> contacts-analyzer SQS: { phone, storeId, source }
  -> read recent calls/messages/leads/tasks/contact
  -> production prompt + ContactsAnalysisSchema
  -> taskDecisions[]
  -> resolve taskRef T1/T2
  -> writeAnalysisWithTasks()
  -> applyTaskAction()
  -> Policy Guard + DB constraints
  -> tasks / task_progress_events / task_suggestions / task_playbooks
```

```text
Message 入口：现在主要先写事实，后续被 analyzer 读取

RingCentral SMS / voicemail webhook
  -> message-processing-queue
  -> message-processor
  -> Neon: messages / contacts / contact_timeline
  -> STOP keyword can update contact.doNotContact
  -> 当前不会主动 trigger contacts-analyzer
  -> 但下一次 contacts-analyzer 运行时会把 messages 读进 context

未来建议：
message-processor 在高价值 inbound reply / STOP / objection 场景下
  -> enqueue contacts-analyzer
```

```text
Lead 入口：lead_outreach 是 deterministic system task，不是 AI 创建

lead-tracking poller
  -> Neon: leads
  -> EventBridge LeadCreated
  -> lead-processor
  -> contacts upsert
  -> deterministic create lead_outreach task
  -> contact_timeline
  -> 后续 call/SMS/AI 再 progress / update / close

关键约束：
AI 可以 create 8 个业务 category
AI 不能 create lead_outreach
lead_outreach belongs to lead-tracking / lead-processor
```

这三条入口对应到 Task Accuracy v1 的测试含义：

| 入口      | 现在代码支持什么                                              | v1 怎么测                                                                         | Demo 后补什么                                                     |
| ------- | ----------------------------------------------------- | ------------------------------------------------------------------------------ | ------------------------------------------------------------- |
| Call    | call facts 写入后会触发 `contacts-analyzer`                 | synthetic `CallSummary` + golden eval；关键 case 做 DB replay                      | full pipeline E2E：raw transcript → call classifier → analyzer |
| Message | message / voicemail 写入 Neon；STOP 可改 DNC；analyzer 后续可读 | synthetic `MessageSummary` / voicemail transcription                           | message processor 主动 enqueue analyzer                         |
| Lead    | lead created 后 deterministic 创建 `lead_outreach`       | seed `LeadInfo` + existing `lead_outreach` task，验证 AI 后续 progress/close/update | lead event 到 analyzer 的更清晰触发策略                                |

### 4.1 Transcript 怎么插入？

我们 demo 前不需要真的走 audio → transcription。可以直接假设 transcript 已经处理完，把关键内容放到 analyzer 真实会读的字段里：

| 输入类型                  | v1 怎么表达                           | 代码里的载体                                                                         |
| --------------------- | --------------------------------- | ------------------------------------------------------------------------------ |
| Call transcript       | 摘要成一句或几句事实                        | `CallSummary.executiveSummary`                                                 |
| Call classifier facts | outcome / category / customerType | `primaryCategory`、`primarySubcategory`、`primaryOutcomeResult`、`followUpNeeded` |
| SMS                   | 短信正文                              | `MessageSummary.subject`                                                       |
| Voicemail             | 语音留言转写                            | `MessageSummary.voicemailTranscription`                                        |
| Lead                  | 表单或来源信息                           | `LeadInfo`                                                                     |
| Existing task         | open task context                 | `TaskRow`，prompt 展示成 `taskRef: T1`                                             |

三层测试深度：

| Level                      | 怎么做                                                                                                      | 用途                                                          |
| -------------------------- | -------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------- |
| Level 1: Golden Eval       | 在 `golden-eval/cases.ts` 里构造 synthetic `ContactData / CallSummary / MessageSummary / LeadInfo / TaskRow` | 快速验证 prompt + Zod + real model                              |
| Level 2: DB Replay         | seed Neon test DB 的 processed rows，再跑 writer 或 analyzer harness                                          | 验证 writer / Policy Guard / DB constraints / artifact writes |
| Level 3: Full Pipeline E2E | raw transcript → call classifier → calls structured fields → contacts-analyzer                           | demo 后做，验证上游 transcript summarization                       |

Demo 前建议优先 Level 1 + Level 2。Level 3 很重要，但会把变量变多，不适合两天内证明 task decision 准确性。

### 4.2 为什么不先大改 DB schema？

短答案：demo 前不要先改 production DB schema。先把 scenario 和 eval 跑准，再让真实 scenario 反推 schema。

原因有三个：

1. Task Accuracy v1 的核心是验证 `prompt -> Zod schema -> Policy Guard -> writer` 是否稳定，不依赖新表。
2. `typeCategory` 现在已经深度绑定 prompt、Zod schema、writer、UI、open-task dedup index。两天内迁 schema 容易把 demo 风险放大。
3. 我们还没用 35-60 个 scenario 证明哪些 `objective` / `issue` 真的是高频、稳定、跨行业的抽象。

所以 v1 先在 scenario 里显式写 `objectiveContext`，但不立刻写入 production DB。这样 review 的内容是长期正确的，同时不打断现有系统。

### 4.3 `Objective / Context` 和 `AI Decision` 的区别

你问得对：如果完全没有 AI 分析，系统怎么知道 objective？

答案是：`Objective / Context` 不是“AI 已经做完判断后的结论”，而是 **AI 判断前的候选目标目录 + 当前上下文包**。AI 再基于这个上下文做 decision。

```text
Objective Catalog + Context Builder
  已知事实：
    contact lifecycle / lead status / doNotContact
    recent calls / messages / voicemail / lead
    existing open tasks / recently closed tasks
    tenant/store policy
    task playbook / suggestion history

  产出给 AI 的不是最终答案，而是候选问题：
    这里是否有 unresolved customer objective？
    如果有，是 existing task 的 objective，还是新的 objective？
    这次 evidence 应该 create / close / update / progress / no-op？

AI Decision Layer
  读取上面的 context
  判断 taskDecisions[]
  输出 proposal
  不能直接写库
```

三个例子：

| 情况                                   | Objective 从哪里来                          | AI 要判断什么                                                    |
| ------------------------------------ | --------------------------------------- | ----------------------------------------------------------- |
| 已有 open task                         | task 已经给出 objective，比如 `lead_follow_up` | 最新 call/SMS 是 `close`、`record_progress`、`update` 还是 `no-op` |
| 没有 open task，但有新 inbound evidence    | system 只知道候选 objective，比如 lead 可能要预约    | AI 判断是否要 `create_open` 或 `create_closed`                    |
| 没有 open task，也没有 actionable evidence | 只有事实，没有 objective                       | AI 应该 `no-op`，不能硬造任务                                        |

## 5. Task Decision 思考模型

这张图是 reviewer 判断 scenario 的心智模型：同一个通话可能是店员打出去，也可能是客人打进来；可能只是一次 attempt，也可能是 close outcome；也可能多轮往返后改变 next action。

```text
New evidence: call / SMS / voicemail / lead
  |
  v
Is this DNC / STOP / doNotContact?
  |
  +-- yes
  |     -> set contact.doNotContact = true
  |     -> close or block open outreach tasks
  |     -> no outbound action
  |
  +-- no
        |
        v
Does a matching open task exist?
  |
  +-- yes
  |     |
  |     v
  |   Did the latest event change the objective state?
  |     |
  |     +-- outbound attempt only
  |     |     -> record_progress: no_answer / left_voicemail / text_sent
  |     |
  |     +-- customer still deciding / asks callback
  |     |     -> record_progress or update
  |     |
  |     +-- achieved / rejected / wrong number / exhausted
  |     |     -> close: booked / converted / not_interested / wrong_number / unable_to_reach
  |     |
  |     +-- new evidence changes next action
  |     |     -> update suggestedActions / priority / dueAt
  |     |
  |     +-- noise only
  |           -> no-op
  |
  +-- no
        |
        v
      Is there a new actionable unresolved objective?
        |
        +-- yes, still open
        |     -> create_open
        |
        +-- yes, arose and settled in same call
        |     -> create_closed
        |     -> requires triggering call evidence
        |        analyzer uses triggeringCallId -> sourceCallId
        |
        +-- no
              -> no-op
```

几个容易混的点：

- 店员打出去没人接：通常是 `record_progress`，不是 `close`，也不是新建任务。
- 客人打进来并预约成功：如果有 open `lead_follow_up`，应该 `close booked`；如果没有 open task 且从产生到解决都在这通电话里，可能是 `create_closed booked`。
- “我不感兴趣”通常是 `close not_interested`；只有明确 “STOP / do not contact / remove me” 才是 DNC。
- Routine confirmation、waiver、arrival logistics 不应该变成 task。
- `dueAt` 只是 staff effort 的排期，不应该阻止记录真实 outcome。

## 6. Scenario Schema

Scenario 的目的不是让机器看懂，而是让产品 / 业务 / 工程能一起 review：“给了这些证据，expected decision 是否正确？”

### 6.1 Review 格式

不要只写 “C-01 lead follow-up create”。每个 scenario 都要写成下面这种状态机格式：

```text
Case A: 店员打给 lead，没人接

existingState:
  Contact 是 active lead
  Open task: T1 lead_outreach
  T1 attemptCount = 0

newEvidence:
  Outbound call
  callState = no_answer
  客户没有任何回复

expected:
  record_progress no_answer on T1
  task 保持 open
  后续由 dueAt 安排下一次 follow-up

notExpected:
  close unable_to_reach
  create_open lead_follow_up
  taskDecisions=[]

rationaleZh:
  单次 outbound no-answer 是一次员工尝试，不是目标完成或失败；
  已有 lead_outreach 就推进原 task，不能重复创建 lead_follow_up。
```

完整 review 清单由代码导出，避免文档和测试漂移：

```bash
cd ../callytics-infrastructure
npm run eval:golden -- --review-md ../docs/docs/ai/product/task-accuracy/baseline-v1-scenario-review.md
```

这个命令不调用模型，不需要 `OPENROUTER_API_KEY`。它只把 `GOLDEN_CASES[].review` 导出成 Markdown，给你逐个 review。

### 6.2 Executable Schema

代码里的每个 `GoldenCase` 同时包含两层：

| 层            | 用途                                                                                         |
| ------------ | ------------------------------------------------------------------------------------------ |
| `review`     | 给产品/业务/工程 review，写 `existingState / newEvidence / expected / notExpected / rationaleZh`    |
| `assertions` | 给 golden eval 执行，只断言结构化字段，比如 `action / taskId / typeCategory / closeResult / progressType` |

建议 registry 里每个 case 长这样：

```ts
interface TaskAccuracyScenario {
  id: string;
  titleZh: string;
  priority: 'P0-demo' | 'P1-baseline' | 'P2-hardening';
  reviewStatus: 'approved' | 'needs_product_decision' | 'known_gap';
  demoCandidate?: {
    id: 'P0-01' | string;
    status: 'needs_model_validation' | 'blocked_known_gap' | 'candidate';
    titleZh: string;
    rationaleZh: string;
  };
  bucket:
    | 'create_open'
    | 'create_closed'
    | 'close'
    | 'update'
    | 'record_progress'
    | 'negative_noop'
    | 'safety';
  sourceOfRule: Array<'production_prompt' | 'zod_schema' | 'policy_guard' | 'business_rule' | 'eval_practice'>;
  contact: {
    lifecycleStage: 'lead' | 'member' | 'churned' | 'unknown';
    lifecycleState: 'active' | 'paused' | 'terminal';
    doNotContact: boolean;
    leadStatus?: string | null;
    purchaseIntent?: 'high' | 'medium' | 'low' | null;
    hasOpenComplaint?: boolean | null;
  };
  existingTasks: Array<{
    taskRef: 'T1' | 'T2';
    status: 'open' | 'closed';
    typeCategory: string;
    attemptCount?: number;
    dueAtRelative?: string;
    suggestedAction?: string;
    closeResult?: string;
  }>;
  evidence: {
    source: 'call' | 'sms' | 'voicemail' | 'lead' | 'mixed';
    direction?: 'Inbound' | 'Outbound';
    hasTriggeringCallId?: boolean;
    fromPhoneRole?: 'store' | 'contact';
    toPhoneRole?: 'store' | 'contact';
    summaryZh: string;
  };
  objectiveContext?: {
    mode: 'existing_task' | 'candidate_objective' | 'no_objective';
    objectiveType?: string;
    objectiveZh?: string;
    desiredOutcomeZh?: string;
    successCriteria?: string[];
    failureModes?: string[];
    evidenceRequirement?: string[];
  };
  expected: {
    taskDecisions: Array<Record<string, unknown>>;
    contactFields?: Record<string, unknown>;
    dbReplay?: boolean;
    artifactChecks?: Array<'task_suggestions' | 'task_playbooks'>;
  };
  reviewQuestionZh: string;
  notes?: string;
}
```

字段解释：

| 字段                 | 为什么要有                                                                        |
| ------------------ | ---------------------------------------------------------------------------- |
| `priority`         | 区分 demo 必测、baseline、hardening，不把所有 case 混成一锅                                 |
| `bucket`           | 确保覆盖的是 decision point，不是 enum 全排列                                            |
| `sourceOfRule`     | 标清这个 expected 是来自 prompt、schema、Policy Guard、业务规则还是 eval practice            |
| `contact`          | lifecycle / DNC / intent 直接影响 AI 是否可以 create                                 |
| `existingTasks`    | open task 决定 close / update / progress；recently closed 只能做 read-only context |
| `evidence`         | call/SMS/voicemail/lead、方向、phone role、触发证据会改变 writer 行为                      |
| `objectiveContext` | 用中文把“这个场景的业务目标”讲清楚；v1 先用于 review/eval，不要求 production DB 已有这个字段               |
| `expected`         | 结构化 expected decision，避免只写自然语言导致测试不可执行                                       |
| `reviewQuestionZh` | 给你逐个判断 scenario 是否正确时用                                                       |
| `reviewStatus`     | 区分已认可、需要产品拍板、已知 gap，避免把模糊 case 写成硬断言                                         |
| `demoCandidate`    | P0 demo 候选 metadata；最终是否进 demo 取决于真实模型稳定性和 DB replay                         |

### 6.3 Scenario Registry / Golden Eval / DB Replay / Demo Pack 的区别

这几个名字容易混在一起。它们不是同一个东西，也不是四套重复系统。

```text
Scenario Registry
  = 题库 / 真值表
  = 我们认为业务上应该发生什么

Golden Eval
  = 让真实模型做题
  = 验证 AI 判断准不准

DB Replay
  = 把关键答案拿去真实 writer path 试写
  = 验证 Policy Guard / writer / DB constraints 是否安全

Demo Pack
  = 从题库里挑稳定通过的展示路线
  = 给 investor / team 看，不是新的测试层
```

更具体地说：

| 名称                | 输入                                                                             | 做什么                                      | 输出                          | 主要回答的问题           |
| ----------------- | ------------------------------------------------------------------------------ | ---------------------------------------- | --------------------------- | ----------------- |
| Scenario Registry | synthetic `contact / call / message / lead / task` context + expected decision | 保存可评审、可执行的 case                          | `GOLDEN_CASES[]`            | 这道题应该怎么判？         |
| Golden Eval       | Scenario Registry + production prompt + production Zod schema + real model     | 跑模型并检查 assertions                        | pass / fail / xfail         | AI 会不会判对？         |
| DB Replay         | 关键 scenario + seed DB rows + proposed action                                   | 走 writer / Policy Guard / DB constraints | persisted assertion verdict | 判对以后，写库会不会安全？     |
| Demo Pack         | Scenario Registry 里的 `demoCandidate` metadata + 稳定 eval 结果                     | 选一条演示路线                                  | P0 demo case list           | 哪些 case 可以放心现场展示？ |

可以把它们理解成：

```text
Scenario Registry 是考试题
Golden Eval 是让 AI 真做题
DB Replay 是把答案拿去真实系统里试写库
Demo Pack 是挑已经稳定做对的题现场展示
```

### 6.4 DB Replay 不是新业务逻辑

DB Replay 需要写一点 test harness，但不应该写新的 task 业务逻辑。正确边界是：

```text
DB Replay Scenario
  |
  | seed synthetic DB rows
  | contacts / calls / messages / leads / existing tasks
  v
真实 Neon test branch
  |
  | inject mock AI output
  | taskDecisions[] = expected decision
  v
现有 production code
  |
  | contacts-analyzer handler
  | -> resolveTaskRefs(T1/T2)
  | -> NeonRepository.writeAnalysisWithTasks()
  | -> common applyTaskAction()
  | -> Policy Guard / DB constraints
  v
真实数据库写入
  |
  | query persisted state
  | tasks / task_progress_events / contact_timeline / contacts / artifacts
  v
assert expected DB state
```

所以新增的是：

- scenario seed data。
- mock AI output injector。
- persisted assertion runner。

不新增的是：

- task writer。
- duplicate logic。
- DNC logic。
- `create_open / create_closed / close / update / record_progress` 业务逻辑。
- Policy Guard。

当前可执行入口：

```bash
cd ../callytics-infrastructure
npm run eval:db-replay
```

运行前置条件：

- `NEON_API_KEY` 已设置。
- `NEON_PROJECT_ID_TEST` 指向 test Neon project。
- 该命令会创建 ephemeral Neon branch，跑完后删除。

为什么只选 8-12 个 DB replay，而不是把 57 个 scenario 全部 DB replay：

- Golden Eval 已经负责全量判断覆盖。
- DB Replay 更慢、更重，需要 Neon branch、seed、writer、query assertions。
- 很多 scenario 在 writer 层走同一条路径，全部 replay 会变成重复测同一段代码。
- 第一批先覆盖代表性 mutation 和高风险 guard：`create_open / create_closed / close / record_progress / update / no-op / DNC / duplicate / forbidden create / missing evidence / hallucinated taskRef`。
- 后续如果 matrix 某个 cell 在真实模型、线上或 demo 中不稳，再给那个 cell 加 targeted DB replay。

### 6.5 它是不是 plug-in / plug-out？

方向上是，但 Baseline v1 先不把 framework 抽象过度。

长期目标是：

```text
厚底座 AI Foundation
├─ context builder
├─ LLM / structured output
├─ Policy Guard
├─ writer / Orchestrator
├─ eval harness
├─ replay harness
└─ feedback loop

薄 Skill: Fitness Task Skill
├─ objective / category definitions
├─ prompt fragments
├─ allowed actions
├─ scenario registry
└─ demo pack

未来薄 Skill: Fraud Detection Skill
├─ fraud objective definitions
├─ fraud policy rules
├─ fraud scenarios
└─ fraud demo pack
```

Task Accuracy Baseline v1 暂时还是 `contacts-analyzer` 专用，但设计上已经按这个方向切分：

- scenario 是业务 skill 层的东西。
- prompt / schema / model call 是 AI Decision 层。
- Policy Guard / writer 是底座层。
- demo pack 是展示路线，不是业务逻辑。

以后如果换到 fraud、billing、support，应该替换的是 skill 的 objective/scenario/playbook，不是重写整套 eval / policy / replay 基座。

### 6.6 什么时候会触发这些检查？

Task Accuracy Foundation 不是 production request path 里的同步检查。真实用户事件进来时，系统不会临时跑完整 Scenario Registry。

```text
真实 production event
  -> contacts-analyzer
  -> taskDecisions[]
  -> Policy Guard / writer
  -> DB mutation
```

生产路径每次都会走 runtime guard，但不会每次跑 Golden Eval / DB Replay。

```text
开发 / CI / demo 前
  -> Scenario Registry
  -> Golden Eval
  -> selected DB Replay
  -> Demo Pack / Known Gaps
```

建议触发方式：

| 时机                                              | 应该跑什么                                    |
| ----------------------------------------------- | ---------------------------------------- |
| 改 prompt / output schema / task prompt registry | Golden Eval                              |
| 改 `applyTaskAction()` / writer / Policy Guard   | DB Replay + related unit tests           |
| 改 scenario expected 或新增 scenario                | review Markdown generation + Golden Eval |
| demo 前                                          | 多轮 Golden Eval + 关键 DB Replay，只展示稳定 case |
| nightly / weekly                                | Golden Eval 全量或重点 case subset            |

## 7. Decision Axes

不要做全排列。我们按“会改变决策”的轴覆盖。

### 7.1 Contact Axes

| Field              | Values                                                                      | 为什么影响 decision                                             | Review question                            |
| ------------------ | --------------------------------------------------------------------------- | ---------------------------------------------------------- | ------------------------------------------ |
| `lifecycleStage`   | `lead / member / churned / unknown`                                         | 决定可以创建哪些 `typeCategory`                                    | 这个人到底是 lead、member、churned，还是不属于 workflow？ |
| `lifecycleState`   | `active / paused / terminal`                                                | `terminal` 默认不能再主动创建任务                                     | 这个 lifecycle 还能被 staff 推进吗？                |
| `doNotContact`     | `true / false`                                                              | hard stop；writer 会 cascade close open tasks                | 是否有明确 STOP / do not contact？               |
| `leadStatus`       | `new / attempted / connected / booked / showed / trialed / converted / ...` | 区分 cold lead、engaged lead、booked、converted、not\_interested | 是首次 outreach，还是已经有互动后的 follow-up？          |
| `purchaseIntent`   | `high / medium / low`                                                       | 影响 priority，不直接等于 create                                   | 是否有足够证据说明购买意图？                             |
| `hasOpenComplaint` | `true / false`                                                              | unresolved complaint 可能触发 `retention`                      | 投诉是否还没解决？                                  |

### 7.2 Task Axes

| Field                 | Values                 | 为什么影响 decision                                  |
| --------------------- | ---------------------- | ----------------------------------------------- |
| `typeCategory`        | 9 个 DB category        | existing open tasks 可以是 9 个，包括 `lead_outreach`  |
| AI-creatable category | 8 个，不含 `lead_outreach` | AI 不能创建 `lead_outreach`，那是 lead-tracking owned  |
| `status`              | `open / closed`        | 只有 open task 会有 `taskRef`；closed task 是 history |
| `attemptCount`        | `0 / 1 / threshold`    | 决定 `unable_to_reach` 是否允许                       |
| `dueAt`               | overdue / future       | 不能因为未来才 due 就忽略真实 outcome                       |

### 7.3 Evidence Axes

| Field               | Values                                | 为什么影响 decision                                                                                                  |
| ------------------- | ------------------------------------- | --------------------------------------------------------------------------------------------------------------- |
| Input source        | call / SMS / voicemail / lead / mixed | 不同输入触发不同 evidence surface                                                                                       |
| Direction           | Inbound / Outbound                    | outbound attempt 多数是 progress；inbound response 更可能 close/update/create                                          |
| Phone role          | store → contact / contact → store     | 校验方向语义和 store attribution                                                                                       |
| Triggering evidence | present / absent                      | `create_closed` 在 contacts-analyzer writer 里必须有 `triggeringCallId`，落库 payload 使用 `sourceCallId`；缺失时 writer skip |
| Channel             | phone / sms / voicemail / email       | `task_progress_events.channel` 当前支持 4 个 channel；v1 主要测 phone/SMS/voicemail，email 属于后续扩展                         |
| Multi-turn          | latest event + history                | 多轮后可能从 update 变 close，或从 no-op 变 create                                                                         |

## 8. Proposed Scenario Set

第一批目标是 35-60 个，不是把下面所有组合硬乘。P0 demo pack 是从 executable scenario metadata 里挑出来的稳定 case，不额外算一套，也不维护一份脱离代码的 wish list。

### 8.1 P0 Demo Pack

| ID    | 中文场景                                         | Expected decision                              | 为什么适合 demo                         |
| ----- | -------------------------------------------- | ---------------------------------------------- | ---------------------------------- |
| P0-01 | 新 lead 主动来电问课程和价格，还没预约                       | `create_open lead_follow_up`                   | 展示 AI 能从 intent 开 actionable task  |
| P0-02 | open lead follow-up，客户预约 intro class         | `close booked`                                 | 展示成功 outcome 会关任务                  |
| P0-03 | open lead follow-up，客户明确不感兴趣                 | `close not_interested`                         | 展示不会让 staff 继续追 dead task          |
| P0-04 | 客户 SMS 回复 STOP，且有多个 open tasks               | `doNotContact=true` + cascade close            | 展示安全边界                             |
| P0-05 | 员工打 outbound call 没人接                        | `record_progress no_answer`                    | 展示 attempt 不等于 close               |
| P0-06 | 员工 outbound 留 voicemail                      | `record_progress left_voicemail`               | 展示 progress events / attempt count |
| P0-07 | member 来电要 cancel，当场被挽留成功                    | `create_closed cancellation_risk cancel_saved` | 展示 one-touch resolved outcome      |
| P0-08 | member 有未解决投诉                                | `create_open retention`                        | 展示 retention risk                  |
| P0-09 | wrong number 证据明确                            | `close wrong_number`                           | 展示 invalid contact path            |
| P0-10 | routine confirmation，没有 unresolved objective | `taskDecisions=[]`                             | 展示不会乱建任务                           |

### 8.2 Create Open Cases

覆盖 8 个 AI-creatable category。

| ID   | 中文场景                                              | Expected                           |
| ---- | ------------------------------------------------- | ---------------------------------- |
| C-01 | engaged lead 问价格 / 课程，没有预约                        | `create_open lead_follow_up`       |
| C-02 | lead 已完成 intro / trial，但没有购买 membership           | `create_open booked_not_converted` |
| C-03 | member 表达 cancel / freeze / downgrade 风险          | `create_open cancellation_risk`    |
| C-04 | member 投诉 billing / service，问题未解决                 | `create_open retention`            |
| C-05 | churned former member 主动问重新加入                     | `create_open win_back`             |
| C-06 | member 对 upgrade / add-on / personal training 感兴趣 | `create_open upgrade`              |
| C-07 | freeze expiring 或 payment recovery 需要人工处理         | `create_open renewal`              |
| C-08 | member 主动要带 friend / family / referral credit     | `create_open referral`             |

### 8.3 Negative / No-op Cases

| ID   | 中文场景                                             | Expected                                                     |
| ---- | ------------------------------------------------ | ------------------------------------------------------------ |
| N-01 | `doNotContact=true`，但又出现 sales signal            | no create；DNC only                                           |
| N-02 | `lifecycleState=terminal`，没有新的 qualifying signal | no create                                                    |
| N-03 | `unknown` lifecycle，且没有可靠 customer context       | no create                                                    |
| N-04 | cold lead 只有 lead record，没有 inbound engagement   | no `lead_follow_up`；`lead_outreach` belongs to lead-tracking |
| N-05 | campaign / broadcast SMS，没有客户回复                  | no create                                                    |
| N-06 | routine appointment confirmation                 | no create                                                    |
| N-07 | 已预约 lead，只剩 waiver / arrival / intake logistics  | no create                                                    |
| N-08 | 问题在电话里完全解决，且没有后续 objective                       | no open task                                                 |
| N-09 | vendor / corporate pitch，不属于 customer workflow   | no fitness task                                              |
| N-10 | recently closed same objective，没有新证据             | no reopen / no duplicate create                              |

### 8.4 Close Outcome Cases

Close 不和所有 category 做硬乘，而是按业务结果测。

| ID    | Open task            | 中文证据                               | Expected                             |
| ----- | -------------------- | ---------------------------------- | ------------------------------------ |
| CL-01 | `lead_follow_up`     | lead 预约 intro                      | `close booked`                       |
| CL-02 | `lead_follow_up`     | lead 成为 paying member              | `close converted`                    |
| CL-03 | `lead_follow_up`     | 客户明确拒绝，不感兴趣                        | `close not_interested`               |
| CL-04 | any outreach task    | STOP / do not contact              | DNC cascade 或 `close do_not_contact` |
| CL-05 | any lead/member task | wrong / unrelated number           | `close wrong_number`                 |
| CL-06 | `cancellation_risk`  | 客户同意留下 / freeze / downgrade        | `close cancel_saved`                 |
| CL-07 | `cancellation_risk`  | cancellation approved 或 form sent  | `close cancelled`                    |
| CL-08 | `retention`          | complaint resolved                 | `close issue_resolved`               |
| CL-09 | `upgrade`            | member 买了 upgrade / add-on         | `close upgraded`                     |
| CL-10 | `renewal`            | card fixed / renewal restored      | `close renewed`                      |
| CL-11 | `win_back`           | former member re-enrolls           | `close win_back`                     |
| CL-12 | `referral`           | referral contact captured / booked | `close referral_obtained`            |
| CL-13 | `lead_follow_up`     | attempt threshold reached，仍无法联系    | `close unable_to_reach`              |
| CL-14 | any                  | outcome 真实但分类不覆盖                   | `close other` with reason            |

### 8.5 Progress Cases

| ID   | Open task           | 中文证据                                  | Expected                                             |
| ---- | ------------------- | ------------------------------------- | ---------------------------------------------------- |
| P-01 | `lead_outreach`     | 员工 outbound call，no answer            | `record_progress no_answer channel=phone`            |
| P-02 | `lead_follow_up`    | 员工 outbound call，left voicemail       | `record_progress left_voicemail channel=voicemail`   |
| P-03 | `lead_follow_up`    | 员工发 outbound SMS，还没有回复                | `record_progress text_sent channel=sms`              |
| P-04 | `cancellation_risk` | 客户接通但还在考虑                             | `record_progress customer_considering channel=phone` |
| P-05 | `renewal`           | 客户要求晚点回拨                              | `record_progress callback_requested`                 |
| P-06 | `retention`         | 约定 manager follow-up                  | `record_progress follow_up_scheduled`                |
| P-07 | any open task       | 单次 no-answer                          | not `close unable_to_reach`                          |
| P-08 | any open task       | missing evidence / no triggering call | proposed decision may be skipped by writer           |

### 8.6 Update Cases

| ID   | Open task              | 中文新证据                               | Expected                            |
| ---- | ---------------------- | ----------------------------------- | ----------------------------------- |
| U-01 | `lead_follow_up`       | 新出现 pricing objection               | `update suggestedActions`           |
| U-02 | `cancellation_risk`    | manager callback promised           | `update suggestedActions`           |
| U-03 | `retention`            | billing dispute details clarified   | `update suggestedActions`           |
| U-04 | `renewal`              | payment link sent but not confirmed | `update suggestedActions`，not close |
| U-05 | `booked_not_converted` | 客户说要和 spouse 商量                     | `update suggestedActions`           |
| U-06 | any open task          | 重复 generic no-answer                | `record_progress`，not update        |

### 8.7 Safety Cases

| ID   | 风险                                               | Expected                                                                                               |
| ---- | ------------------------------------------------ | ------------------------------------------------------------------------------------------------------ |
| S-01 | AI 尝试 create `lead_outreach`                     | AI output Zod schema 不允许 `create_open lead_outreach`；如果绕过 schema，Policy Guard 也应 reject                |
| S-02 | AI 引用 `T9`，但 prompt 只展示 `T1`                     | `resolveTaskRefs` drops / rejects                                                                      |
| S-03 | AI 输出 raw UUID 作为 `taskId`                       | schema repair / reject                                                                                 |
| S-04 | 同 contact/store/category 重复 open task            | Policy Guard / DB unique prevents duplicate                                                            |
| S-05 | `create_closed` missing triggering call evidence | contacts-analyzer writer skips；shared Orchestrator 也会 reject missing `sourceCallId` 做 defense-in-depth |
| S-06 | `record_progress` missing evidence               | writer skips or Orchestrator rejects                                                                   |
| S-07 | existing task store mismatch                     | Policy Guard rejects                                                                                   |
| S-08 | `closeResult=other` without note / reason        | Policy Guard rejects                                                                                   |

## 9. TaskPlaybook / Suggestions 要不要测

要加，但 v1 不要过度扩大范围。

当前代码里 `writeAnalysisWithTasks()` 对 accepted `create_open`、带 `suggestedActions` 的 `create_closed/update` 会写：

- `task_suggestions`：append-only，每条建议一行，带 `promptVersion` / `runId`。
- `task_playbooks`：append-only/cache，`content` 包含 `primaryAction`、`recommendedSteps`、`optionsYouCanOffer`、`avoid`、`closeGuidance`。

v1 建议测两层：

| 层级                        | 是否放 v1         | 验证什么                                                                          |
| ------------------------- | -------------- | ----------------------------------------------------------------------------- |
| Artifact persistence      | 是，DB replay 必测 | accepted mutation 后 `task_suggestions.active` 和 `task_playbooks.current` 是否存在 |
| Basic content shape       | 是，轻量测          | `primaryAction`、`recommendedSteps`、`closeGuidance` 非空                         |
| Semantic playbook quality | 第二批            | playbook 是否真的是好话术、好策略、符合品牌 SOP                                                |

原因：demo 前我们要证明“AI decision + safe write + artifact created”闭环存在；playbook 质量当然重要，但它是另一个 eval 维度，不应该阻塞 Task Accuracy Baseline v1。

## 10. First Batch vs Second Batch

### 第一批：Task Accuracy Baseline v1

目标是证明 task decision 本身准。

- 35-60 个 scenario。
- Golden eval 跑 production prompt + production Zod schema + real model。
- 8-12 个 DB replay 验证 writer / policy / constraints / artifact writes。
- Demo pack 只选稳定通过 case。

### 第二批：Hardening + Voice-Agent Readiness

目标是让系统准备好接自动执行。

- 多轮 back-and-forth：几天内来回通话 / SMS。
- 同 phone 跨 store。
- recently closed 后又出现新证据。
- concurrent duplicate create。
- stale proposal：AI 开始分析后 staff 已经改了 task。
- playbook semantic quality eval。
- approval queue。
- Vapi agent execution log。
- operating hours / blackout / DNC before dialing。

## 11. Definition of Done

Task Accuracy Baseline v1 完成时应该有：

- 35-60 个 synthetic scenarios，每个有结构化 expected decision。
- 覆盖 `create_open / create_closed / close / update / record_progress / no-op / safety`。
- 使用 production prompt + production Zod schema + real model 的 golden eval。
- 8-12 个 DB replay case，验证 writer / Policy Guard / DB constraints；当前入口是 `npm run eval:db-replay`。
- `task_suggestions` / `task_playbooks` 的基础 artifact write 被验证。
- `knownGap` 明确记录，不把不稳定 case 放进 demo。
- GitHub parent issue 统筹，child issues 分开执行。
- Scenario registry 能继续复用：以后加 industry、加 Vapi、加 prompt update，都从这里扩展。

## 12. 当前缺口

| 缺口                                                    | 影响                            | Demo 前要不要做                                                                |
| ----------------------------------------------------- | ----------------------------- | ------------------------------------------------------------------------- |
| Scenario registry 不完整                                 | 没法系统 review expected decision | 要                                                                         |
| Golden eval case 太少                                   | 覆盖不了 6 action + safety        | 要                                                                         |
| DB replay harness 初版已加，当前本地缺 Neon env 未完成实跑           | 不知道当前 branch 在真实 Neon 上是否全绿   | 要，在设置 `NEON_API_KEY` / `NEON_PROJECT_ID_TEST` 后跑 `npm run eval:db-replay` |
| `voicemail -> record_progress` 曾是 known gap           | attempt count 可能不进展           | 要修或不放 demo                                                                |
| `firm rejection -> close not_interested` 曾是 known gap | staff 会继续追 dead task          | 要修或不放 demo                                                                |
| Playbook 质量 eval 没有                                   | 执行建议可能质量不稳                    | v1 只测基础 artifact，质量第二批                                                    |
| `tasks` 没有 objective layer                            | 长期跨行业不够抽象                     | 不建议 demo 前改                                                               |

## 13. 下一步评审方式

建议接下来这样推进：

1. 先把上面的 scenario registry 写成机器可读文件。
2. 你逐个 review P0 + P1：expected decision 对不对。
3. 不确定的 scenario 标 `needs_product_decision`，不要直接写进 hard assertion。
4. 通过 review 的 case 才进 golden eval。
5. 高风险 case 再进 DB replay。
6. Demo pack 从“连续稳定通过”的 case 里挑，不从 wish list 里挑。

这会让 task eval 变成团队长期资产，而不是一次性的 demo 脚本。
