# v0.8 — 周期对比（Period Comparison）

## Problem Statement

运营者用评估活动对同一对象做周期性考察，每期末尾拿到一份智能分析（总体结论、问题诊断、改善建议）。但单期分析是孤立的：这一期的问题到底是新冒出来的、持续未愈的、还是已经消解的？上一期的改善建议落实了吗？要回答这些，人得自己打开两期报告逐条对照自由文本——而这正是平台该替人做的元层研判。

## Solution

在相邻两期活动之间提供**周期对比**：机械 diff 给出两期聚合指标的客观变化（整窗与分场景的通过率/可用性/时延 delta，ADR-0004 口径，确定性计算），LLM 在两期分析结论之上产出结构化演进叙述（趋势判断、问题演化、建议追踪）。报告抽屉内新增「周期对比」区块，与智能分析并列。正式线活动全链自动（活动完成 → 分析 → 对比），加速调试线手动按需。

## User Stories

1. 作为运营者，我希望打开本期活动报告时看到"与上一期相比"的指标变化表（整窗通过率/可用性/时延及分场景 delta），以便快速判断走势。
2. 作为运营者，我希望看到一个趋势徽章（改善/平稳/退化），以便不看数字也能感知方向。
3. 作为运营者，我希望看到问题演化列表（新增/持续/消解，含涉及场景与说明），以便知道该担心什么、可以放下什么。
4. 作为运营者，我希望看到上期建议的追踪状态（已落实/部分落实/未落实/新增），以便评估改进动作的有效性。
5. 作为运营者，我希望正式线活动的对比在分析完成后自动生成，以便零操作拿到结论。
6. 作为调试者，我希望加速线活动能手动点「生成对比」，以便验证功能而不等完整周期。
7. 作为运营者，当本期活动计划与历史不一致（无自动基线）时，我希望能手动挑选一个历史活动作基线，以便换计划后仍能对比。
8. 作为运营者，我希望对比生成失败时看到原因并可重试，以便区分模型问题与数据问题。
9. 作为运营者，我希望对比叙述与指标 diff 分开呈现（叙述是 LLM 判断、数字是确定性计算），以便知道哪部分可以较真。
10. 作为管理员，我希望对比复用「分析默认」模型（活动覆盖 ?? 全局默认），以便不引入新的配置面。
11. 作为运营者，我希望对比中引用的场景是真实存在的（白名单过滤），以便不被模型虚构误导。

## Implementation Decisions

### 配对（计划指纹 + 自动基线）

- **计划指纹** = 评测对象 + 计划条目集合（场景 id、偏移秒数、次数）+ 窗口秒数。指纹相等的正式线活动构成活动串
- **自动基线** = 同活动串中、完成时间早于本期、且已有 completed 分析的最近一期活动
- 指纹不一致 → 无自动基线；`POST /comparison` 接受可选 `baseline_campaign_id` 手动指定（跨串允许，可比性由用户负责）
- 基线活动被删或分析被重置 → 视为无基线

### 产出（混合：机械 diff + LLM 叙述）

- **机械 diff**：纯函数，输入两期 `generate_campaign_report` 结果，输出整窗 + 分场景的通过率/可用性/时延 delta（读时现算，不存储）
- **LLM 叙述**：单次调用（不做两阶段——输入只有两份已消化的分析 JSON + 机械 diff，无原始样本），输入上限轻量；输出结构化 JSON：

```json
{
  "trend": "improving | stable | regressing",
  "summary": "总体演进结论",
  "problem_evolution": [{"status": "new | persisting | resolved", "title", "detail", "scenario_ids": []}],
  "suggestion_tracking": [{"text", "status": "addressed | partial | unaddressed | new", "note"}]
}
```

- 叙述中的 `scenario_ids` 落库前过白名单（同 v0.7）；非法 `trend`/`status` 枚举归一（trend→stable，status→persisting / unaddressed）
- LLM 解析失败 → 行落 failed + error

### 存储与生命周期

- 新表 `campaign_period_comparisons`：当前活动 id 唯一 upsert、baseline_campaign_id、generating/completed/failed、result（叙述 JSON）、model_config_id 快照、error、triggered_by（auto/manual）、时间戳——镜像 `campaign_analyses` 模式
- 模型解析复用 v0.7 `resolve_analysis_model`（活动覆盖 ?? 全局分析默认）；不可解析 → 自动链静默跳过 / 手动 400 引导
- 自动链：`execute_campaign_analysis` 成功落 completed 后，活动为正式线且存在自动基线 → enqueue 对比生成；任一前提不满足静默跳过
- 重新生成（手动 POST）= 同活动 upsert 覆盖，可借此更换基线重算

### API

- `GET /api/campaigns/{id}/comparison`：无行 → 空态 `{"status": "none"}`；有行 → 机械 diff（现算）+ 叙述行合并返回；附 `baseline_campaign_id` 与自动配对结果，前端据此渲染或引导选基线。`metric_diff` 跟随生效基线：有对比行按行内基线算，否则按自动基线算
- `POST /api/campaigns/{id}/comparison`：body 可选 `{"baseline_campaign_id": "..."}`；非终态活动 400；当前活动无 completed 分析 400（先生成分析）；无模型 400 引导；成功 → 后台任务 + `{"status": "generating"}`
- 两期都有 completed 分析是生成前提（基线无分析 → 400 提示先生成基线分析）

### 前端（报告抽屉「周期对比」区块）

- 位置：「智能分析」区块之下，复用区块状态行范式（生成中 5s 轮询 / 失败重试 / 未生成按钮）
- 已生成：趋势徽章 + 总结 → 指标变化表（整窗 + 分场景 delta，按好坏着色——比率升为绿、时延降为绿，反之红）→ 问题演化（新增红/持续橙/消解绿 Tag）→ 建议追踪（已落实绿/部分橙/未落实灰/新增蓝）
- 无自动基线：基线选择器（同对象历史终态活动下拉）+ 生成按钮
- 展示基线活动名与完成时间、所用分析模型名与生成时间

## Testing Decisions

好测试只测外部行为，不测实现细节；全部落在既有最高层 seam：

1. **机械 diff 纯函数**：手搓两份报告 dict，断言 delta 结构与数值、空桶/None 处理（先例如 `test_report_render.py`）
2. **计划指纹与自动基线**：内存 SQLite，指纹相等/不等、跳过无分析活动、跳过非正式线、取最近一期（先例如 `test_campaign_analysis.py` 的仓储测试）
3. **LLM 叙述编排**：FakeChatClient（v0.7 seam）覆盖成功编排、解析失败→AnalysisError 式失败、白名单剔除虚构 scenario_ids、非法枚举归一
4. **API 集成**：镜像 `test_campaign_analysis_api.py`——GET 空态、POST 非终态 400、无分析 400、无基线 400、手动指定基线成功、重跑 upsert 不新增行
5. **自动链**：spy 对比入口，断言分析完成后正式线自动触发、加速线不触发、无基线不触发（镜像 `test_campaign_analysis_auto_trigger.py`）
6. **迁移测试**：表存在 + campaign_id 唯一约束（先例如 `test_campaign_analyses_migration.py`）

## Out of Scope

- 跨活动对比（多对象横切）——词汇表中另一个轴，本规格不涉及
- 活动列表趋势标记（↑↓）——后续增强
- 对比结果纳入 Markdown 导出——看使用情况再定
- 基线活动的选取 UI 优化（如按指纹分组展示）
- 三期以上的趋势序列（只对比相邻两期）

## Further Notes

- 设计决策源自 `/grill-with-docs` 访谈（6 问 6 决）：配对 A、产出 C 混合、呈现 A 抽屉区块、生命周期 A 新表+联动触发、输入 A 轻量、输出 A 结构化
- 「周期对比」「正式线/加速调试线」已入 `CONTEXT.md` 词汇表
- 对比不重新消化失败样本：样本已被两期分析消化，对比是元层研判
