# 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` 全绿