# Rerank & Freqcap（重排 / 去重 / 频控）｜Tasks

> 对应计划：`spec_kit/Personalized Reco/modules/rerank-freqcap/plan.md`
>
> 本清单执行原则：
>
> - 本模块只做 **Dedup / Freqcap / Feed MMR 重排**，不做 Soft Scoring 与 Hard Filter。
> - V1 冷却窗口以“输入集合代表窗口内历史”为准（后续接入服务端历史再按天计算）。

---

## 0. 任务标记规则

- 用勾选框标记执行状态：
  - `[ ]` 未开始
  - `[x]` 已完成
- 每个任务都要求可独立验收（有明确产出/可运行的检查方式）。

---

## 1. 文档对齐（先把口径写死，避免实现漂移）

- [x] 1.1 校对 `modules/rerank-freqcap/spec.md` 与 `modules/rerank-freqcap/plan.md` 一致性
  - **检查点**：
    - 输入：`scene/scored_candidates/history/k/config` 字段与命名一致
    - 输出：`ranked_items` 与 `meta` 字段集合一致
    - 去重键：`sentence_key=content_id`、`author_key`、`template_key` 口径一致
    - Feed：MMR λ=0.7 与 Sim 规则一致
  - **验收**：两份文档不存在冲突描述，且“V1 冷却窗口语义”写清楚（集合代表窗口内历史）。

- [x] 1.2 在 `plan.md` 中补充/固定“标签构造策略”与“候选截断策略”（若后续要改再更新）
  - **变更点**：
    - 明确 `tags` 的 V1 定义：`stage + need_argmax + context_argmax`
    - 明确 `top_n_for_mmr` 默认值与作用（性能兜底）
  - **验收**：实现时不会出现“标签到底取哪些 key”的二义性。

---

## 2. 目录与骨架（与推荐子模块同级）

- [x] 2.1 新建目录 `server/app/features/personalized_reco/rerank_freqcap/`
  - **包含**：
    - `__init__.py`
    - `types.py`（`ScoredCandidate`、`RerankConfig`、`RerankMeta`、`RerankResult`）
    - `defaults.py`（按 scene 的默认参数：λ、cooldown_*、top_n_for_mmr）
    - `utils.py`（ID 归一化、Jaccard、tag 构造等）
    - `rerank.py`（主入口 `rerank_and_freqcap`）
  - **验收**：可通过 `app.features.personalized_reco.rerank_freqcap.*` 正常 import。

---

## 3. 类型与接口（稳定契约，便于 reco-engine 调用）

- [x] 3.1 定义 `ScoredCandidate`（最小字段集合）
  - **必须字段**：
    - `content_id: int`
    - `final_score: float`
  - **建议字段**（用于多样性/频控）：
    - `author_id: str | None`
    - `template_id: str | None`
    - `content_profile`（至少能取到 stage/need_suitability/context_suitability；缺失时需降级）
  - **验收**：能承载 MMR 相似度计算所需数据；缺失字段不会导致异常。

- [x] 3.2 定义 `RerankConfig`（可调参）
  - **字段**：
    - Feed：`mmr_lambda`（默认 0.7）、`top_n_for_mmr`（默认 200）
    - Push/Widget：`cooldown_sentence_days/cooldown_author_days/cooldown_template_days`（用于配置与可观测）
  - **验收**：能从 scene 推导默认 config（或由调用方传入覆盖）。

- [x] 3.3 定义 `RerankMeta` 与 `RerankResult`
  - **meta 必须字段**：
    - `candidate_pool_size_after_dedup`
    - `candidate_pool_size_after_freqcap`
    - `missing_history_fields`
  - **建议字段**：
    - `freqcap_filtered_counts`（sentence/author/template）
  - **验收**：字段集合固定；任何输入都能产出 meta（包括候选为空）。

---

## 4. 核心算法实现（V1 最小集合）

- [x] 4.1 实现历史 ID 归一化（避免 str/int 混用漏过滤）
  - **规则**：
    - `already_recommended_ids` / `touched_or_viewed_ids` 尽量转为 `int` 集合
    - 转换失败的值忽略并记录 debug
  - **验收**：单测覆盖 `["1", 2, "bad"]` 等混合输入，过滤结果正确且稳定。

- [x] 4.2 实现 Dedup（必做）
  - **规则**：过滤 `content_id ∈ seen_ids` 的候选
  - **产出**：`meta.candidate_pool_size_after_dedup`
  - **验收**：输出不包含历史出现过的 `content_id`。

- [x] 4.3 实现 Freqcap（Push/Widget 必做；Feed 可选）
  - **V1 策略**：
    - 句子维度（必做）：同句硬过滤（使用 dedup 的 seen_ids 即可）
    - 作者/模板维度（增强项）：
      - 若提供 `recent_author_ids`：命中则硬过滤；否则在 `missing_history_fields` 记录 `author`
      - 若提供 `recent_template_ids`：命中则硬过滤；否则在 `missing_history_fields` 记录 `template`
  - **产出**：
    - `candidate_pool_size_after_freqcap`
    - `freqcap_filtered_counts`（建议）
  - **验收**：在有/无 `recent_*_ids` 输入时行为一致且可解释。

- [x] 4.4 实现 Feed：tag 构造与相似度 `Sim`
  - **tag 规则（V1 写死）**：
    - `stage:<stage>`
    - `need:<argmax_key>`（从 `need_suitability` 取最大值 key；为空则跳过）
    - `context:<argmax_key>`（从 `context_suitability` 取最大值 key；为空则跳过）
  - **Jaccard**：`|A∩B|/|A∪B|`，空集合时返回 0
  - **Sim 累加规则**：
    - 同 content_id → 1
    - template_id 相同且非空 → +0.6
    - author_id 相同且非空 → +0.3
    - +0.1 * Jaccard(tags)
    - clamp 到 `[0,1]`
  - **验收**：单测覆盖缺失字段（无 author/template/tags）时仍能算出稳定 Sim。

- [x] 4.5 实现 Feed：MMR 选序列
  - **规则**：
    - Top1：按 `final_score` 最大
    - 后续：按 `MMR(c)=λ*Rel(c)-(1-λ)*maxSim` 选择
    - `Rel=final_score`
  - **性能兜底**：先截断候选到 `top_n_for_mmr` 再做 MMR
  - **验收**：
    - Top1 恒等于最高分
    - 候选足够时，序列不出现大量同作者/同模板紧邻重复（可用阈值断言）

- [x] 4.6 实现 Push/Widget：最终 TopK
  - **规则**：dedup+freqcap 后按 `final_score` 降序取前 k 条
  - **验收**：输出长度 ≤ k，且分数单调不增（允许相等）。

- [x] 4.7 实现主入口 `rerank_and_freqcap(...) -> RerankResult`
  - **规则**：
    - 三场景共用 dedup
    - Feed：MMR；Push/Widget：TopK
    - 必须输出 meta（即使 ranked_items 为空）
  - **验收**：任何输入（含空候选）不抛异常，并输出稳定结构。

---

## 5. 单元测试（pytest，纯函数为主）

- [x] 5.1 新建测试文件 `server/tests/test_rerank_freqcap.py`
  - **用例覆盖**：
    - dedup：历史集合过滤正确（含 str/int 混用）
    - freqcap：有/无 recent_author/template 的分支与 meta 缺失标记
    - feed mmr：Top1=最高分；后续避免同作者/模板紧邻（构造数据断言）
    - push/widget：TopK 输出正确
  - **验收**：`pytest -q tests/test_rerank_freqcap.py` 通过。

---

## 6. 最终自检清单（合入前）

- [x] 6.1 文档一致性检查
  - **验收**：`spec.md` / `plan.md` / 实现接口签名三者一致（尤其 meta 字段与默认参数）。

- [x] 6.2 回归检查（不影响已实施模块）
  - **验收**：不修改 `content-repository` 与 `scoring` 的既有逻辑；仅新增 `rerank-freqcap` 模块与测试。

- [x] 6.3 全量测试通过
  - **命令**（在 `server/`）：
    - `pytest -q`
  - **验收**：所有用例通过。

- [x] 6.4 全部完成后更新大规范 `overview.md`
  - **变更点**：
    - 将 `modules/rerank-freqcap/` 标记为“已实施”
    - 增加一条变更记录（日期 + 交付物：plan/tasks/代码/测试）
  - **验收**：`spec_kit/Personalized Reco/overview.md` 中模块状态与交付记录准确。

