# PAC 接入新 Host Runbook(PAC 团队侧)

> **场景**:有新宿主(如 friday)要接入 PAC,本文档列 PAC 团队侧需要做的全部操作步骤。
> **配套文档**:
> - 给 host 看的接入契约 → `docs/host-integration.md`
> - 冷数据装载 → `apps/pac-service/data/mock-demo/README.md`
> - 数据三层结构 → `docs/three-layer-model.md`

---

## 0. 当前 PAC 侧能力速查(2026-05-14)

| 能力 | 状态 | 接口 / 工具 |
|---|---|---|
| **Auth 换 JWT** | ✅ | `POST /pac/v1/auth/token` |
| **Platform 管理(create/rotate/deactivate)** | ✅ | `pnpm pac:platform <cmd>` CLI(走 SSH,不走 HTTP)|
| **Admin 只读 + 受限更新** | ✅ | `GET / PATCH /pac/v1/admin/platforms/*`(actionUrls/pullConfig/active)|
| **AssemblerEngine — yaml 驱动 host→canonical 投影** | ✅ | `data/<host>/assemblers/*.yaml` + `AssemblerEngine` |
| **Sync 装载脱敏数据(首跑全量/日级增量统一)** | ✅ | `pnpm sync -- --dir=...`(cold-import = legacy 全量 alias)|
| **Patient timeline 可查** | ✅ | `GET /pac/v1/patients/:id/timeline` + `pnpm timeline -- --pid=...` |
| **Drift 单条识别** | ✅ | `ContractDriftError` + `[drift]` 日志前缀 |
| **数据建模能力** | ✅ | 18 action / 13 subjectType / 19 fact_type / 18 personaFeatureKey / 12 outcome(含产品收集 — 见 host-data-request-checklist)|
| **Pull cron / Pull manual** | ❌ STUB | `SyncService.runPull*` 等 W5 重写 |
| **Push 接收 + HMAC 验签** | ❌ 未实现 | 路由都没建 |
| **Reconcile 对账 / Drift N 连告警 / Quarantine 表** | ❌ | 七层防线第 4-6 条欠债 |
| **Persona / Plan engine** | ❌ STUB | W3 一起做 |

---

## 1. 接入流程(PAC 团队步骤,对应 host-integration §10)

### Phase 1:平台注册(走 CLI,**不走 HTTP**)

敏感操作(create / rotate / deactivate)**只走 CLI**,SSH 到 PAC 服务器 + DB 直连。
理由:平台注册一年几次,低频高重要;web 鉴权 + PAC 内部用户系统是 over-engineering。

```bash
# SSH 到 PAC 生产/staging 服务器
cd /apps/pac-service

# 注册新 host
pnpm pac:platform create --name=friday --push --amount-unit=yuan --timezone=Asia/Shanghai

# 输出(plaintext 密钥仅 stdout 一次性显示)
✓ Created platform "friday"
  id:        a1b2c3d4-...
  appId:     pac_friday_x7k9m2lp
  appSecret: <plaintext, 仅显示一次>
  pushSecret: <plaintext, 仅显示一次>
  active:    true

⚠️  立即把 appSecret / pushSecret 存到 1Password 共享空间 "PAC-Production/friday",
   PAC 不再保留 plaintext,失落只能 rotate。
```

**CLI 命令清单**:
```bash
pnpm pac:platform list                    # 列出所有 platform
pnpm pac:platform get <id-or-name>        # 单个详情
pnpm pac:platform create --name=<slug> ...   # 注册新 platform
pnpm pac:platform rotate-secret <id-or-name> # 轮换 appSecret(旧立即失效)
pnpm pac:platform rotate-push <id-or-name>   # 轮换 pushSecret(数组前插,grace)
pnpm pac:platform deactivate <id-or-name>    # 软停(auth 立即拒绝)
```

**安全模型**:
| 控制项 | 实现 |
|---|---|
| 谁能跑 CLI | SSH 到 PAC 服务器(SSH key 限名单)|
| DB 凭证保护 | `.env` 里 `DATABASE_URL` 只生产节点可读 |
| 操作审计 | SSH 登录日志 + DB binlog;后续可加 `pac_admin_actions` 表 |
| Plaintext 密钥 | CLI 输出 stdout,运维即看即存 1Password,不入日志 |

**HTTP `/admin/platforms` 只剩读 + 受限更新**(给将来 web dashboard 用):
- `GET /admin/platforms` 列表
- `GET /admin/platforms/:id` 详情
- `PATCH /admin/platforms/:id` 改 actionUrls / pullConfig / active(不改密钥)

POST / rotate-secret / rotate-push / deactivate **endpoint 已移除** — 走 CLI。

### Phase 2:发凭据给 host(线下 / 安全通道)

把 `appId` + `appSecret`(+ `pushSecret` 如果启用 push)通过加密邮件 / 1Password 等发给 host backend 团队。

Host 拿到后即可:
```bash
# host 验证凭据可用
curl -X POST http://pac.example.com/pac/v1/auth/token \
  -H 'Content-Type: application/json' \
  -d '{
    "appId":"pac_friday_a1b2c3d4",
    "appSecret":"IYPRA...",
    "user":{"userId":"u_kefu_001","tenantId":"t_friday_grp1","role":"staff","clinicIds":["clinic_hz_dasha"]}
  }'
# → 拿到 host 侧用户的 JWT
```

### Phase 3:host 给裸表 + 数据字典 → PAC 写 assembler.yaml ⭐

**这是 v2 的关键步骤**(替代 v1 的"录 fieldMapping")。详细契约见 `host-integration.md` §7。

3.1 Host 给 PAC 一个**目录**:
```
friday-export-20260513.zip
├── README.md              ← 数据字典(每张表 + 每个字段 + 枚举值含义)
│
├── ── 必填 5 类(召回核心)──
├── tb_patient.csv         ← SELECT * 原样
├── tb_appointment.csv
├── tb_encounter.csv       ← + tb_dx.csv(诊断)+ tb_tx.csv(治疗)三表 1:N 嵌套
├── tb_dx.csv
├── tb_tx.csv
├── tb_charge_order.csv    ← + tb_order_item.csv(明细)1:N
├── tb_order_item.csv
├── tb_payment.csv
│
├── ── 🟠 强烈推荐 3 类(产品收集 — 业务方明确指出关键)──
├── tb_recharge.csv        ← 充值/储值/福利卡(LTV 必备,中国民营牙科核心)
├── tb_complaint.csv       ← 投诉记录(合规营销避雷)
├── tb_referral.csv        ← 转介绍记录(渠道归因)
│
└── ── 🟡 可选(有就给)── tb_refund / tb_consultation / tb_emr_document / ...
```

3.2 PAC 团队基于数据字典写 yaml:
```bash
cd apps/pac-service
mkdir -p data/friday/assemblers/
# 写 N 份 assembler.yaml(每个 canonical 资源一份)
# 模板见 data/mock-demo/assemblers/*.yaml
```

每份 assembler.yaml 描述:
- `primary`:host 的主表名
- `field_mapping`:host 字段 → PAC canonical 字段
- `join_arrays`:1:N 嵌套(如 tb_encounter ← tb_dx[]/tb_tx[])
- `enum_mapping`:host 编码(`A01`)→ PAC 枚举(`endodontic`)

3.3 写 `manifest.yaml`:
```yaml
platform_name: friday
tenant_id:     t_friday_grp1
amount_unit:   yuan
timezone:      Asia/Shanghai

tables:
  # 必填 5 类
  - { table: tb_patient,      file: tb_patient.csv }
  - { table: tb_encounter,    file: tb_encounter.csv }
  - { table: tb_dx,           file: tb_dx.csv }
  - { table: tb_tx,           file: tb_tx.csv }
  - { table: tb_appointment,  file: tb_appointment.csv }
  - { table: tb_charge_order, file: tb_charge_order.csv }
  - { table: tb_order_item,   file: tb_order_item.csv }
  - { table: tb_payment,      file: tb_payment.csv }
  # 🟠 产品收集(host 能给则导)
  - { table: tb_recharge,     file: tb_recharge.csv }
  - { table: tb_complaint,    file: tb_complaint.csv }
  - { table: tb_referral,     file: tb_referral.csv }

assemblers:
  - { file: assemblers/patient.yaml }
  - { file: assemblers/clinical_encounter.yaml }
  - { file: assemblers/appointment.yaml }
  - { file: assemblers/charge_order.yaml }
  - { file: assemblers/payment.yaml }
  # 🟠 产品收集(对应 canonical resource:recharge / complaint / referral)
  - { file: assemblers/recharge.yaml }
  - { file: assemblers/complaint.yaml }
  - { file: assemblers/referral.yaml }
```

### Phase 4:cold-import 联调

```bash
cd apps/pac-service

# 先 dry-run 看翻译效果(不写库)
pnpm sync -- --dir=./data/friday --dry-run

# 真写库(首跑 cursor 空 = 全量;cohort 分批 + 可并行,内存恒定)
pnpm sync -- --dir=./data/friday --no-recompute
#   榨资源:PAC_COHORT_CONCURRENCY=3 pnpm sync -- --dir=./data/friday --no-recompute
#   首跑完手动补 persona/plan:
pnpm recompute-persona -- --host=friday && pnpm recompute-plans -- --host=friday

# 验证 timeline
pnpm timeline -- --platform=friday --tenant=t_friday_grp1 --pid=<host pid>

# 跑 selector 自检(看召回算法能不能在该 host 数据上识别 10 子场景 K00-K09)
pnpm verify-scenarios     # 注意:这个验证脚本基于 demo 数据;真实 host 数据需重写期望集
```

> **统一 sync 入口**:`pnpm sync` 首跑(cursor 空)= 全量,之后 cron 增量,同一套 `importDirectory`。
> `pnpm cold-import` 保留为 legacy「忽略 cursor 强制全量」alias。

**数据装载是 PAC 三大数据通道之一**(跟 pull/push 共用 AssemblerEngine + 下游 pipeline)。
host 持续导出 + PAC 日级 `pnpm sync` 增量(cron 自动)即可作为产线长期数据通道。

### Phase 5:开 pull 周期 / push 实时(W5+ 才能做)

⚠️ **当前 STUB,这一段先等**。Pull cron / Push receiver / HMAC 验签都还没实装。

Phase 4 的 cold-import 走的就是同一套下游 pipeline(AssemblerEngine → Synthesizer → Parser → patient_facts),所以**pull/push 通道接通后,只需要把"如何拿到 raw 表数据"那段换掉**(从读文件改成 HTTP 拉 / 收 webhook),AssemblerEngine 之后的中下游零改动。**写好的 assembler.yaml 直接复用**。

---

## 2. 运维场景(密钥失落 / push 密钥轮换 / 软停)

### 2.1 appSecret 失落 → 重发

```bash
pnpm pac:platform rotate-secret friday
# → stdout 显示新 appSecret(plaintext 仅一次),旧立即失效;立即同步给 host
```

### 2.2 Push 密钥轮换(平滑过渡)

```bash
pnpm pac:platform rotate-push friday
# → stdout 显示新 pushSecret + total_keys,数组前插旧 key 仍接受 grace
#   host 切完 → 单独 DB 脚本清理旧 hash(本期不暴露专用 CLI)
```

### 2.3 软停 host(合同到期 / 安全事件)

```bash
pnpm pac:platform deactivate friday
# → active=false,后续 auth 立即 10101 拒绝。
#   不删数据,patient_transactions / patient_facts / followup_plans 全留(审计需要)。
```

---

## 3. 接入完成判定标准

完成接入意味着:

- [ ] `POST /admin/platforms` 拿到新 host 的 id / appId / appSecret
- [ ] Host 用 appSecret 换出 JWT(code=0,有 accessToken)
- [ ] `pnpm sync -- --dir=<host 导出目录> --no-recompute` 全成功(failed=0,facts > 0)
- [ ] `pnpm timeline -- --pid=<host 某 patient>` 能看到完整 actual + planned 列表
- [ ] 10 子场景(K00-K09)selector 跑出来命中数 > 0(说明数据形态真的能进召回)

如果 sync 报 `[drift] xxx row rejected: <字段路径>` —— 那是 host 实际字段 / 枚举值跟 PAC 的 assembler.yaml 错位,调整对应 yaml 重跑即可(不需要 host 改任何东西)。

---

## 4. 数据从 host 拿到后的落点速查

```
host 脱敏导出 → CSV/JSON 文件 + manifest.yaml
   ↓ scp / S3 / 邮件
data/<host>-<timestamp>/
   ↓ pnpm sync -- --dir=...
[1] platforms              (Admin 早已录入)
[2] patients               (upsert,主档)
[3] patient_transactions   (append-only,raw_payload 留 host 原文)
[4] patient_facts          (版本流,parser 衍生)
[5] sync_logs              (该批 import 的账本:status/factsEmitted/duplicates/failed)
   ↓ GET /pac/v1/patients/:id/timeline
客服可读 timeline
```

后续 W3+ 引入 Persona / Plan / PlanExecution 后,fact 层之上的画像 / 召回计划自然展开。
