# 02 — 对比叙述生成（表 + Agent + POST）

**What to build:** 周期对比的 LLM 半边与完整生命周期。新表 `campaign_period_comparisons`（当前活动 id 唯一 upsert、baseline_campaign_id、generating/completed/failed、result JSON、model_config_id 快照、error、triggered_by、时间戳）+ 迁移。单次 LLM 调用编排：输入两期分析 JSON + 机械 diff，复用 `resolve_analysis_model` 与 ChatClient 注入 seam；输出 schema 见 spec（trend/summary/problem_evolution/suggestion_tracking）；scenario_ids 白名单剔除；非法枚举归一（trend→stable、演化→persisting、建议→unaddressed）；解析失败落 failed。`POST /api/campaigns/{id}/comparison`：非终态 400；本期无 completed 分析 400；body 可带 `baseline_campaign_id`（缺省走自动基线，仍无 → 400 引导；手动基线须有 completed 分析）；无模型 400 引导；成功 → 后台任务 + `{"status": "generating"}`。GET 扩展合并：状态行字段 + 机械 diff + 叙述 + 基线信息。

**Blocked by:** 01（GET 与配对复用）

**Status:** done

- [x] 迁移：`campaign_period_comparisons` 表（campaign_id 唯一约束）+ 迁移测试
- [x] 仓储 upsert / get_by_campaign
- [x] 编排：FakeChatClient 断言输入含两期分析与 diff；解析失败→failed；白名单剔除虚构 scenario_ids；非法枚举归一
- [x] POST 各 400 分支 + 成功分支 + 重跑 upsert 不新增行；GET 合并返回
- [x] `pytest` + `ruff` 全绿
