瀏覽代碼

docs(v3): 新增 V3 M3 Implementation Briefs(规则包5→2+判定配置重写)

- 索引 + 4 子brief:M3A 平台热度0-1对数归一化字段(platform_heat.py+注入evidence_bundle)/ M3B 内容包重写(input_contract/hard_gates/scorecard/reason_code,Excel+JSON同步过config gate)/ M3C 删M2桥接键+砍4 future包+dispatch / M3D 规则单测重写+受控回放快照更新
- 关键约束写入:build_config_from_excel 只值覆盖不建行→新增/删除对象须JSON与Excel两边同步;evaluator无数值计算→60:40用维度满分(max60/max40)+gte分档,热度内容侧预算
- 顺序自洽:M3A先于M3B(读heat)、M3B先于M3C(删旧门槛再删桥接键,否则全拒)
- evaluator 唯一最小改:加 lt 算子(fit_confidence<0.6 必需,按拍板例外,对称lte+单测)
- 三原则守住:不改evaluator引擎(除lt)、不改thresholds/effect_status_mapping/schema/血缘/walk_policy;退役reason_code保留catalog定义;回放变化标受控、快照实跑生成
- 经事实核查岗抽验(lt算子缺口已据此修为明确前置;"briefs非已实施代码"系brief本质,格式与V3 M1/M2一致)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Sam Lee 1 月之前
父節點
當前提交
8574431f71

+ 79 - 0
tech_documents/工程落地/v3_implementation_briefs/M3/00_M3_Brief_Index.md

@@ -0,0 +1,79 @@
+# V3 M3 规则包瘦身 5→2 + 判定配置重写 Brief Index
+
+状态:本目录覆盖 V3 M3——把 M2 产出的 Gemini 字段真正接进规则:重写内容判定包(hard_gates+scorecard 全读 Gemini 字段)、删 M2→M3 桥接键、砍 4 个 future 包、平台热度归一化落地。**判定逻辑全部配置驱动**(规则包 JSON,Excel 是 source of truth),`evaluator.py` 引擎本体不改。对标 M2 颗粒度。
+
+## 目标定位
+
+M2 后 `recall_decision._build_pattern_match_result`(recall_decision.py:59-70)写入 `fit_senior_50plus/fit_confidence/relevance_score/reason/judge_status` + 桥接键 `pattern_recall/category_or_element_binding="matched"`(67-68)。当前规则包(douyin_content_discovery_rule_pack_v1)仍是 decode/分类树语言:10 hard_gates 含 `pattern_recall_required`(not_in["matched"])等、scorecard 3 维读旧画像。M3 做完:硬门槛只认"适合50+/置信度/技术失败",打分=相关性60:平台热度40,旧 reason_code 退役;桥接键删除;管线在真实/回放下产出由 Gemini 驱动的决策。
+
+## 子 brief 执行口径(A→D 线性,A 先行因 B 的 scorecard 要读 A 的字段)
+
+| 子 brief | 文件 | 核心交付 | 类型 |
+|---|---|---|---|
+| **M3A** | `M3A_Platform_Heat_Field.md` | 平台热度 0-1 字段内容侧预计算(`platform_heat.py` 对数归一化)+ 注入 evidence_bundle `content_engagement_metrics.platform_heat` | 代码 |
+| **M3B** | `M3B_Content_Pack_Rewrite.md` | 重写内容包 input_contract/hard_gates/scorecard/dimensions/decision_reason_codes(Excel+JSON 同步,过 config gate) | 配置 |
+| **M3C** | `M3C_Bridge_Removal_And_Pack_Cut.md` | 删 recall 桥接键(67-68);砍 4 future 包 + dispatch(Excel+JSON);确认 walk_policy 承接 | 代码+配置 |
+| **M3D** | `M3D_Tests_And_Controlled_Replay_Update.md` | 重写规则单测;受控更新回放快照(新规则新基线) | 测试 |
+
+## 不处理范围(红线)
+
+- 不实现边表 frontier loop(M4);不接并发(M5);不动平台接入/M2 gemini client。
+- **evaluator 引擎仅一处最小改动**:加 `lt` 算子(对称现有 `lte`,2 行 + 单测)——`fit_confidence<0.6` 必需,按 09 拍板"如需新 operator 再小改"的明确例外;其余 gate/scoring 全走现有 op(eq/gte/lte/in/not_in/is_empty)。**因 evaluator 无数值计算能力**(无乘法/对数/区间映射),60:40 用"维度满分"实现(relevance 维 max60、platform_heat 维 max40,各用 gte 分档),热度归一化在 M3A 内容侧预算成 0-1 字段。
+- 不改 thresholds 三档(70/60)、effect_status_mapping、DB schema、血缘(source_evidence 门槛保留)。
+- 不删 DB 历史旧 reason_code 数据(只换词表定义,历史行不动)。
+- 锚点/分档数值用拍板起步值,M7 真跑标定(标"可先按默认推进")。
+
+## 已拍板(2026-06-11,09 计划 M3,本里程碑内嵌)
+
+- 硬门槛仅:`fit_senior_50plus==false → REJECT`(content_not_fit_senior)、`fit_confidence<0.6 → REJECT`(content_low_confidence,低置信即拒非复看);技术失败 `judge_status=="failed" → KEEP_FOR_REVIEW`(content_judge_failed)。
+- 相关性进打分不单独拒;打分 相关性60:热度40,总分沿用 ≥70 进池 / 60–69 待复看 / <60 拒。
+- 新 reason_code:content_not_fit_senior / content_low_confidence / content_judge_failed + 沿用 content_score_pool/review/reject;旧 content_pattern_recall_required / category_or_element_binding_required / missing_content_portrait / age_50_plus_weak 退役。
+
+## 开工前必须拍板
+
+- 无。
+
+## 现有证据(共享,子 brief 各自细引)
+
+- **Excel 是 source of truth(关键)**:`scripts/build_config_from_excel.py`(读 `tech_documents/规则包映射/规则包映射配置表.xlsx`→`product_documents/规则包/douyin_rule_packs.v1.json`)**只对已存在 JSON 对象按 (rule_pack_id, child_id) 做值覆盖,不凭空建新行**。→ **新增 gate/rule/dimension/reason_code 必须 JSON 与 Excel 同步增同一对象**(JSON 手加 canonical 对象 + Excel 加行),删除同理两边删。`--check` 字节相等 + canonical + rule_pack_fk 三闸把关。
+- Excel sheet 与列(verbatim):`hard_gate_rules`(gate_id/rule_pack_id/priority/gate_label→label/field_path→when.field/operator→when.op/expected_value→when.value/severity/stop_scoring/decision_action/decision_reason_code/effect_status/enabled)、`scorecard_scoring_rules`(scoring_rule_id/dimension_id/rule_pack_id/field_path/operator/expected_value/score_value/score_formula/missing_policy/priority/enabled)、`scorecard_dimensions`(dimension_id/rule_pack_id/dimension_key→key/dimension_label→label/max_score/weight_percent/runtime_status/evidence_paths)、`threshold_actions`、`decision_reason_codes`、`rule_pack_dispatch`。
+- 规则包现状:内容包 input_contract(253-272)、10 hard_gates(274-430)、scorecard 3 活跃维+6 scoring_rules(432-572)、thresholds(574-601)、hard_gate_primary_reason_priority(611-620)、decision_reason_codes(1390-1462)、effect_status_mapping(1275-1334,不改)、dispatch 5 条仅 content enabled(21-91)。
+- evaluator:`evaluator.py` op 仅 eq/gte/lte/in/not_in/is_empty;维度内 priority 取首条命中分值、维度间直接相加;`_total_interactions`(397-399)= digg+comment+share+collect;无乘法/对数。bundle 可读 content/source_evidence/content_audience_profile/content_engagement_metrics/content_risk_check/pattern_match_result/run_context。
+- M2 桥接键:`recall_decision.py:67-68`(pattern_recall/category_or_element_binding="matched",注释已标"M3 删")。
+- heat 注入点:`content_discovery_builder._build_evidence_bundle`(115-184),`content_engagement_metrics.statistics` 在 156-157(digg_count 等原始值在此)。
+- platform_profiles(douyin/shipinhao).json runtime 段**无 heat 锚点**(M3A 加)。
+- walk_policy.json `edge_permissions`(23-29)+`budget_tiers`(34-37)已承接 path_stop/budget 语义(M3C 砍包安全)。
+- config gate:`uv run --with openpyxl python scripts/run_config_gate.py` 现 5 闸全 pass;`validate_rule_pack_config.py` 校验 dispatch FK / hard_gate decision_action 在 catalog / scoring_rule dimension_key 绑定 / threshold action。
+- 规则测试:`test_rule_judgment_hard_gates.py`(6)、`test_rule_judgment_scorecard.py`(10)。基线 `uv run pytest -q`=306。
+
+## 数据合同(汇总)
+
+- 新硬门槛读 `pattern_match_result.{fit_senior_50plus,fit_confidence,judge_status}`;新 scorecard 读 `pattern_match_result.relevance_score`(relevance 维)+ `content_engagement_metrics.platform_heat`(heat 维)。
+- `platform_heat` ∈ [0,1] = 该内容在本平台的对数归一化热度(M3A 预算)。
+- 删桥接键后 pattern_match_result 不再有 pattern_recall/category_or_element_binding。
+- Excel↔JSON 字节相等、canonical、rule_pack_fk 三闸恒过。
+
+## 验证命令(汇总)
+
+```bash
+uv run --with openpyxl python scripts/run_config_gate.py                          # 5 闸全 pass(改规则后)
+uv run pytest tests/test_platform_heat.py -q                                      # M3A
+uv run pytest tests/test_rule_judgment_hard_gates.py tests/test_rule_judgment_scorecard.py -q  # M3B/M3C 新规则
+uv run pytest tests/test_case_replay.py -q                                        # M3D 受控新基线
+uv run pytest -q                                                                  # 全绿
+rg -n "pattern_recall_required|category_or_element_binding_required|missing_content_portrait" content_agent product_documents tests   # 仅历史/退役注释
+```
+
+## 失败归因(汇总)
+
+- config gate excel_json_byte_equal 红:新增对象只改了 Excel 或只改了 JSON(必须两边同步同一对象)。
+- 管线全拒:删桥接键(M3C)早于删旧门槛(M3B),旧 gate 读到 None→拒(顺序须 B 先 C 后)。
+- 热度恒 0:M3A 的 platform_heat 没注入 bundle 或路径与 scoring_rule field_path 不一致。
+- evaluator 报错/不支持:误在 scoring_rule 里写乘法/公式(evaluator 不支持,必须 M3A 预算字段 + gte 分档)。
+
+## sub-agent 交叉验证要点
+
+- 确认新增 gate/rule/reason_code 在 Excel 与 JSON 两边同步、config gate 字节相等过。
+- 确认 60:40 用维度满分(max60/max40)+ gte 分档,未在配置里写 evaluator 不支持的数值运算。
+- 确认桥接键(67-68)删除在旧门槛删除之后;real_id45 回放按受控新基线钉新快照(非回归)。
+- 确认 walk_policy 承接被砍包语义、evaluator 引擎零改、DB schema/血缘/effect_status_mapping 不动。

+ 80 - 0
tech_documents/工程落地/v3_implementation_briefs/M3/M3A_Platform_Heat_Field.md

@@ -0,0 +1,80 @@
+# M3A 平台热度归一化字段 Implementation Brief
+
+状态:本 brief 只覆盖「内容侧预计算 `platform_heat`(0-1 对数归一化)并注入 evidence_bundle」。因 `evaluator.py` 无数值计算能力(无对数/乘法),热度必须在判定**之前**算成一个 0-1 字段,scorecard 才能用 gte 分档读它(M3B)。先于 M3B。不改 evaluator、不改 M2 client。
+
+## 目标
+
+新建 `platform_heat.py`:`heat_score(digg_count, platform) -> float`,按平台锚点 `[floor,ceil]` 做对数归一化 `clamp((log10(digg+1)-log10(floor))/(log10(ceil)-log10(floor)), 0, 1)`;在 `content_discovery_builder._build_evidence_bundle` 把结果挂到 `bundle["content_engagement_metrics"]["platform_heat"]`,供 M3B 的 platform_heat 评分维读取。跨平台唯一公共指标=点赞(实测:抖音全量 statistics、视频号仅 like_count→digg_count)。
+
+## 现有证据
+
+- evaluator 能力:`evaluator.py` op 仅 eq/gte/lte/in/not_in/is_empty,`_total_interactions`(397-399)是唯一聚合,**无对数/乘法/区间映射** → 热度必须内容侧预算成 0-1 字段。
+- 注入点:`content_discovery_builder._build_evidence_bundle`(115-184),`content_engagement_metrics` 在 155-158:`{"statistics": result["statistics"], **result["statistics"]}`(digg_count 等原始值在此);M2 已把 `content_audience_profile` 镜像挂在此函数(125/159)。
+- digg 来源:M1 归一化 item `statistics.digg_count`(抖音←digg_count、视频号←like_count);bundle 路径 `content_engagement_metrics.statistics.digg_count`。
+- 平台标识:bundle/item `platform`("douyin"/"shipinhao")。
+- platform_profiles runtime 段**无 heat 锚点**(douyin/shipinhao.json 核实)。
+- 实测量级:抖音爆款 digg 503万、视频号样本 92~1272(captures);→ 起步锚点 抖音 floor=1e4/ceil=1e6、视频号 floor=50/ceil=5e4。
+
+## 修改范围
+
+- 新建 `content_agent/business_modules/content_discovery/platform_heat.py`。
+- 改 `content_discovery_builder._build_evidence_bundle`:`content_engagement_metrics` 加 `"platform_heat": heat_score(...)`。
+- 新建 `tests/test_platform_heat.py`。
+
+## 不修改范围
+
+- 不改 evaluator、不改规则包(M3B 才加读 platform_heat 的 scoring_rule)、不改 M2 gemini client/recall。
+- 不改 statistics 既有字段、不改 content_audience_profile 镜像。
+- 不引新依赖(math 标准库)。
+- **锚点不进 platform_profiles(那是文档、当前不被代码读)**:锚点作 platform_heat.py 模块常量 `_HEAT_ANCHORS`,清晰标注"M7 真跑标定";如 M4 让 profiles 变代码可读,再迁移(churn 最小,不为 2 个数现在造 profile 加载路径)。
+
+## 涉及文件 / 函数 / 类
+
+- 新建 `content_agent/business_modules/content_discovery/platform_heat.py`
+  - `_HEAT_ANCHORS = {"douyin": (10000.0, 1000000.0), "shipinhao": (50.0, 50000.0)}`(注释:起步值,M7 标定)
+  - `_DEFAULT_ANCHOR = (100.0, 100000.0)`(未知平台兜底)
+  - `def heat_score(digg_count: Any, platform: str) -> float`:取 anchor;`d=max(int(digg_count or 0),0)`;`floor,ceil=anchor`;`raw=(log10(d+1)-log10(floor))/(log10(ceil)-log10(floor))`;`return round(min(max(raw,0.0),1.0), 4)`。
+- 改 `content_discovery_builder.py:_build_evidence_bundle`
+  - `content_engagement_metrics` 字典加 `"platform_heat": heat_score(result.get("statistics",{}).get("digg_count"), result.get("platform","douyin"))`。
+- 新建 `tests/test_platform_heat.py`
+  - `test_heat_floor_maps_to_zero`(digg=floor → ~0)
+  - `test_heat_ceil_maps_to_one`(digg≥ceil → 1.0)
+  - `test_heat_midpoint_between_zero_and_one`(几何中点 → ~0.5)
+  - `test_heat_missing_or_zero_digg_is_zero`(None/0 → 0.0)
+  - `test_heat_unknown_platform_uses_default_anchor`
+  - `test_heat_shipinhao_low_digg_not_unfairly_zero`(视频号 digg=1000 在自己锚点下得到正分,印证跨平台公平)
+
+## 数据合同
+
+- `heat_score(digg, platform) -> float ∈ [0,1]`(4 位小数);digg≤floor→0、≥ceil→1、之间对数插值;缺失/0→0;未知平台用默认锚点。
+- bundle 新增路径 `content_engagement_metrics.platform_heat`(M3B 的 scoring_rule field_path 用它)。
+- 锚点为模块常量,值=拍板起步值,M7 标定。
+
+## 实施步骤
+
+1. 写 `platform_heat.py`(锚点常量 + heat_score 对数归一化 clamp)。
+2. `_build_evidence_bundle` 注入 `content_engagement_metrics.platform_heat`(import heat_score)。
+3. 写 `tests/test_platform_heat.py` 6 例。
+4. 全量 pytest:platform_heat 测试绿;既有 306 不回归(注入新键不影响现有规则——现规则不读 platform_heat,M3B 才读)。
+
+## 验证命令
+
+```bash
+uv run pytest tests/test_platform_heat.py -q
+uv run python -c "from content_agent.business_modules.content_discovery.platform_heat import heat_score; print(heat_score(503*10**4,'douyin'), heat_score(92,'shipinhao'), heat_score(0,'douyin'))"   # ~1.0, >0, 0.0
+uv run pytest -q                                   # 306 不回归
+```
+
+## 失败归因
+
+- 抖音/视频号都偏低或都偏高:用了同一套锚点(必须按 platform 取各自 floor/ceil)。
+- log10(0) 崩:digg+1 + floor≥1 保护;floor 不能为 0。
+- 注入路径错:挂到了 statistics 子层而非 content_engagement_metrics 顶层(M3B field_path 取不到)。
+- 既有回归:注入新键意外覆盖了 statistics 展开键(只 add 不 overwrite)。
+
+## sub-agent 交叉验证要点
+
+- 确认 heat_score 对数归一化、clamp [0,1]、缺失→0、按 platform 取锚点。
+- 确认注入点是 `content_engagement_metrics.platform_heat`、与 M3B scoring_rule field_path 一致。
+- 确认锚点是清晰标注的起步常量(M7 标定)、未引新依赖、未改 evaluator/规则包。
+- 确认 306 不回归(现规则不读新键)。

+ 89 - 0
tech_documents/工程落地/v3_implementation_briefs/M3/M3B_Content_Pack_Rewrite.md

@@ -0,0 +1,89 @@
+# M3B 内容判定包重写(Excel+JSON) Implementation Brief
+
+状态:本 brief 覆盖「重写 `douyin_content_discovery_rule_pack_v1` 的 input_contract / hard_gates / scorecard / dimensions / decision_reason_codes,使其读 Gemini 字段与 M3A 的 platform_heat」。纯配置(Excel source of truth + JSON 同步),不改 evaluator。删桥接键/砍包归 M3C,测试/回放归 M3D。依赖 M3A 的 `platform_heat` 字段已注入。
+
+## 目标
+
+把内容包从 decode/分类树语言换成 Gemini 语言:硬门槛 = 适合50+/置信度/技术失败;打分 = 相关性60 + 平台热度40(维度满分 + gte 分档,evaluator 无需数值运算);reason_code 换新词表。**因 build_config_from_excel 只值覆盖不建行**,新增对象必须 Excel 行与 JSON 对象**同步新增**,删除对象两边同步删,最终过 config gate 5 闸。
+
+## 现有证据
+
+- 内容包现状:input_contract(253-272,required 含 `pattern_match_result.pattern_recall`/`category_or_element_binding`/`content_audience_profile.age_50_plus_level`);10 hard_gates(274-430);scorecard 3 活跃维(content_audience_profile max50 / interaction_performance max30 / freshness_available max20)+ 6 scoring_rules(432-572);thresholds 70/60/0(574-601);hard_gate_primary_reason_priority(611-620);decision_reason_codes(1390-1462)。
+- M2 产字段(可读):`pattern_match_result.{fit_senior_50plus(bool),fit_confidence(float),relevance_score(float),judge_status(str "ok"/"failed")}`;M3A 产 `content_engagement_metrics.platform_heat(0-1)`。
+- Excel 流程:改值走 `hard_gate_rules`/`scorecard_scoring_rules`/`scorecard_dimensions`/`decision_reason_codes` sheet → `build_config_from_excel.py --write` → gate;**新增/删除对象需 JSON 与 Excel 同步**(build 只 overlay 已存在的 (rule_pack_id, child_id))。
+- evaluator op:eq/gte/lte/in/not_in/is_empty;维度内首条命中、维度间相加。
+- config gate:canonical / excel_json_byte_equal / rule_pack_fk / excel_json_sync / query_prompts,现全 pass。
+
+## 修改范围
+
+- 改 Excel `tech_documents/规则包映射/规则包映射配置表.xlsx`:`hard_gate_rules`、`scorecard_dimensions`、`scorecard_scoring_rules`、`decision_reason_codes` sheet 的对应行(增/删/改)。
+- 改 JSON `product_documents/规则包/douyin_rule_packs.v1.json`:内容包 input_contract / hard_gates / scorecard.dimensions / scorecard.scoring_rules / decision_reason_codes / hard_gate_primary_reason_priority(与 Excel 同步)。
+- (不在本 brief:删桥接键/砍包=M3C;测试=M3D。)
+
+## 不修改范围
+
+- 不改 evaluator、不改 thresholds(70/60/0)、不改 effect_status_mapping、不改 input_contract 的 `source_evidence` 必填(血缘保留)。
+- 不改 walk_strategy / walk_policy(M3C/M4)。
+- 不动其它 4 个 future 包内容(M3C 整砍)。
+
+## 涉及文件 / 函数 / 类(具体改动清单)
+
+**input_contract**(253-272):删 required `pattern_match_result.pattern_recall`、`pattern_match_result.category_or_element_binding`、`content_audience_profile`、`content_audience_profile.age_50_plus_level`;加 `pattern_match_result.fit_senior_50plus`、`pattern_match_result.fit_confidence`、`pattern_match_result.relevance_score`、`content_engagement_metrics.platform_heat`;保留 `content.platform_content_id`/`source_evidence`/`run_context.run_id`。
+
+**hard_gates**(Excel `hard_gate_rules` + JSON 同步):
+- 删 4:`pattern_recall_required`、`category_or_element_binding_required`、`missing_content_portrait`、`age_50_plus_weak`。
+- 新增 3(JSON 加对象 + Excel 加行):
+  - `not_fit_senior`:field `pattern_match_result.fit_senior_50plus`、op `eq`、value `false`、severity `fatal`、action `REJECT_CONTENT`、reason `content_not_fit_senior`。
+  - `low_confidence`:field `pattern_match_result.fit_confidence`、op `lt`、value `0.6`、severity `fatal`、action `REJECT_CONTENT`、reason `content_low_confidence`。
+  - `judge_failed`:field `pattern_match_result.judge_status`、op `eq`、value `failed`、severity `review`、action `KEEP_CONTENT_FOR_REVIEW`、reason `content_judge_failed`。
+- 保留 5:`missing_platform_content_id`、`missing_source_evidence`、`high_risk_content`、`not_safe_for_50_plus`、`missing_platform_author_id`。
+- **前置:给 evaluator 加 `lt` 算子(已核实 evaluator op 集 = eq/gte/lte/in/not_in/is_empty,无 `lt`)。** `fit_confidence<0.6` 必须严格小于(0.6 应放行,`lte 0.6` 会误拒 0.6;浮点边界 hack 如 lte 0.59 不可取)。按 09 拍板"如需新 operator 再小改 evaluator,单独标注":在 `evaluator.py` op 分派处加 `lt`(对称现有 `lte`,`value is not None and actual < value`,约 2 行)+ `test_rule_judgment_hard_gates.py` 加一条 lt 单测(M3D)。**这是 M3 唯一且明确的引擎小改,其余 gate/scoring 全用现有 op。**
+
+**scorecard.dimensions**(Excel `scorecard_dimensions` + JSON):
+- 退役旧 3(content_audience_profile/interaction_performance/freshness_available → runtime_status deprecated、max_score 0)。
+- 新增 2:`relevance`(max_score 60、weight_percent 60、active、evidence_paths `["pattern_match_result.relevance_score"]`)、`platform_heat`(max_score 40、weight_percent 40、active、evidence_paths `["content_engagement_metrics.platform_heat"]`)。
+
+**scorecard.scoring_rules**(Excel `scorecard_scoring_rules` + JSON,删旧 6 增新):
+- relevance 维(field `pattern_match_result.relevance_score`,gte 分档):`score_relevance_high`(gte 0.8→60)、`score_relevance_mid`(gte 0.6→45)、`score_relevance_low`(gte 0.4→25)。(<0.4 无命中→0)
+- platform_heat 维(field `content_engagement_metrics.platform_heat`,gte 分档):`score_heat_high`(gte 0.8→40)、`score_heat_mid`(gte 0.6→30)、`score_heat_low`(gte 0.4→20)、`score_heat_min`(gte 0.2→10)。(<0.2→0)
+
+**decision_reason_codes**(Excel `decision_reason_codes` + JSON):新增 `content_not_fit_senior`/`content_low_confidence`/`content_judge_failed`(is_hard_gate=true,前两 reason_category hard_gate、judge_failed review_needed);旧 `content_pattern_recall_required`/`category_or_element_binding_required`/`missing_content_portrait`/`age_50_plus_weak` 标 status=deprecated(保留定义供历史数据可读,不再被引用);沿用 `content_score_pool/review/reject`/`missing_platform_content_id` 等。
+
+**hard_gate_primary_reason_priority**(611-620):去退役项,加 `content_not_fit_senior`/`content_low_confidence`,保留 missing_platform_content_id/missing_source_evidence/high_risk_content/content_score_reject。
+
+## 数据合同
+
+- hard_gates 引用字段全部存在于 input_contract / bundle(rule_pack_fk 闸校验);新 scoring_rule 的 dimension_key 绑定新 dimensions(fk 校验)。
+- relevance 维满分 60、platform_heat 维满分 40,相加 ≤100;阈值不变 → ≥70 池/60-69 复看/<60 拒。
+- Excel↔JSON 字节相等、canonical、fk 三闸恒过。
+- 退役 reason_code 仍在 catalog(status=deprecated),不被任何 active gate/threshold 引用。
+
+## 实施步骤
+
+1. 先确认 M3A 已注入 `content_engagement_metrics.platform_heat`。
+2. **JSON 与 Excel 同步**编辑(新增对象两边都加、删除两边都删、改值改 Excel):input_contract → dimensions → scoring_rules → hard_gates → decision_reason_codes → priority。
+3. `python scripts/build_config_from_excel.py --write` 重建(确认 JSON 与手加对象一致、canonical)。
+4. `uv run --with openpyxl python scripts/run_config_gate.py` 5 闸全 pass(重点 excel_json_byte_equal + rule_pack_fk)。
+5. (规则单测/回放在 M3D;此处只保证 gate 过 + 包结构合法。)
+
+## 验证命令
+
+```bash
+uv run --with openpyxl python scripts/run_config_gate.py          # 5 闸全 pass
+uv run python -c "import json;p=json.load(open('product_documents/规则包/douyin_rule_packs.v1.json'));g=[x['gate_id'] for x in p['rule_packs'][0]['hard_gates']];print('not_fit_senior' in g, 'pattern_recall_required' not in g)"   # True True
+uv run python scripts/validate_rule_pack_config.py               # fk 一致
+```
+
+## 失败归因
+
+- excel_json_byte_equal 红:新增对象只在一侧(JSON 或 Excel),或值不一致;build overlay 后与 JSON 不字节相等。
+- rule_pack_fk 红:新 gate 的 decision_action 不在 catalog、新 scoring_rule 的 dimension_key 没绑新 dimension、reason_code 未登记。
+- 评分恒 0:scoring_rule field_path 与 M3A 注入路径不符,或 relevance_score 字段名拼错。
+- op 不支持:用了 evaluator 没有的 op(应以实测 op 集为准;`lt` 若无,按拍板小改 evaluator 加 lt + 单测,单独标注)。
+
+## sub-agent 交叉验证要点
+
+- 确认新增/删除对象在 Excel 与 JSON 两边同步、config gate 字节相等过。
+- 确认 relevance(max60)+platform_heat(max40)维度满分方案、gte 分档,未用 evaluator 不支持的运算。
+- 确认旧 4 hard_gates 删除、旧 reason_code 标 deprecated 不被引用、source_evidence 门槛保留。
+- 确认 input_contract 改为读 Gemini 字段 + platform_heat、thresholds/effect_status_mapping 未动。

+ 83 - 0
tech_documents/工程落地/v3_implementation_briefs/M3/M3C_Bridge_Removal_And_Pack_Cut.md

@@ -0,0 +1,83 @@
+# M3C 删桥接键 + 砍 4 future 包 Implementation Brief
+
+状态:本 brief 覆盖「删 M2→M3 桥接键(recall_decision.py:67-68);把 author_expand/tag_expansion/path_stop/budget_observe 4 个 future 包及其 dispatch 从规则包(Excel+JSON)整砍」。**必须在 M3B 之后**(M3B 已删读桥接键的旧门槛,此时删桥接键才安全)。游走控制语义已由 walk_policy 承接(只确认引用,不实现 loop——M4)。
+
+## 目标
+
+清掉过渡物与死配置:① recall 不再写 `pattern_recall/category_or_element_binding="matched"` 桥接键(M3B 后无门槛读它);② 规则包只留 1 个真用的内容包,4 个 dispatch_enabled=false 的 future 包(author/tag/path/budget)整体移除——它们的游走控制语义在 V3 已搬到 `walk_policy.json`。瘦身后规则包 = 1 内容判定包(+ walk_policy 控游走),即"5→2"。
+
+## 现有证据
+
+- 桥接键:`recall_decision.py:67-68`:
+  ```python
+  "pattern_recall": "matched",                 # 67
+  "category_or_element_binding": "matched",    # 68
+  ```
+  注释已标"M3 删旧门槛后移除"。M3B 删 `pattern_recall_required`/`category_or_element_binding_required` 门槛后,这两键无人读。
+- 4 future 包:`rule_packs[]` 含 author_expand/tag_expansion/path_stop/budget_observe(dispatch_enabled=false);`rule_pack_dispatch`(21-91)5 条仅 content enabled,其余 4 条 future。
+- walk_policy 承接(已存在):`walk_policy.json` `edge_permissions`(23-29,"取代...原 path_stop/budget 包语义的新家")、`budget_tiers`(34-37)。→ 砍包不丢语义。
+- Excel:`rule_pack_dispatch` sheet(dispatch_id/.../dispatch_enabled);各包的 hard_gate/scoring 行分布在对应 sheet,按 rule_pack_id 区分。
+- config gate `dispatch_conflict` 校验:同分组多条 enabled 才报错;删 future 包不触发(它们 enabled=false)。
+
+## 修改范围
+
+- 改 `recall_decision.py`:删 67-68 两行桥接键(+ 相应注释)。
+- 改 JSON `douyin_rule_packs.v1.json`:`rule_packs[]` 删 4 个 future 包对象;`rule_pack_dispatch` 删 4 条 future dispatch(保留 dispatch_content)。
+- 改 Excel `规则包映射配置表.xlsx`:`hard_gate_rules`/`scorecard_*`/`threshold_actions`/`decision_reason_codes`/`rule_pack_dispatch` 中属于这 4 个 rule_pack_id 的行删除(与 JSON 同步)。
+- (测试/回放在 M3D。)
+
+## 不修改范围
+
+- 不改 walk_policy.json(只确认它承接,本里程碑不动它;M4 才实现 loop 消费)。
+- 不改 content 内容包(M3B 已重写)、不改 effect_status_mapping/decision_action_catalog 中仍被 content 包引用的条目。
+- 不改 evaluator、不动 source_evidence 血缘、不删退役 reason_code 的 catalog 定义(M3B 已标 deprecated)。
+- 不删 walk_strategy.v1.json(产品侧游走策略,M4 范畴)。
+
+## 涉及文件 / 函数 / 类
+
+- `content_agent/business_modules/content_discovery/pattern_recall/recall_decision.py`
+  - `_build_pattern_match_result`:删 `"pattern_recall": "matched"`(67)、`"category_or_element_binding": "matched"`(68)及上方注释。返回 dict 仅留 fit_senior_50plus/fit_confidence/relevance_score/reason/judge_status/pattern_recall_evidence_id。
+- `product_documents/规则包/douyin_rule_packs.v1.json`
+  - `rule_packs`:移除 author_expand/tag_expansion/path_stop/budget_observe 4 个包对象。
+  - `rule_pack_dispatch`:移除 4 条 future dispatch(留 dispatch_content)。
+- `tech_documents/规则包映射/规则包映射配置表.xlsx`
+  - 各 sheet 删 rule_pack_id ∈ {author_expand,tag_expansion,path_stop,budget_observe} 的行;`rule_pack_dispatch` sheet 删对应 4 行。
+- 顺序:JSON 与 Excel 同步删 → `build_config_from_excel.py --write` → config gate。
+
+## 数据合同
+
+- recall 产出的 pattern_match_result 不再含 pattern_recall/category_or_element_binding。
+- 规则包 `rule_packs` 仅剩 douyin_content_discovery_rule_pack_v1;`rule_pack_dispatch` 仅剩 dispatch_content(enabled)。
+- config gate(canonical/byte_equal/fk/sync) 全过;`dispatch_conflict` 不报(无多 enabled)。
+- 游走控制语义不丢(walk_policy 承接,M4 消费)。
+
+## 实施步骤
+
+1. **确认 M3B 已完成**(旧 pattern/category 门槛已删)——否则删桥接键会让管线全拒。
+2. 删 recall_decision.py:67-68 桥接键。
+3. JSON 与 Excel 同步删 4 future 包 + 4 future dispatch 行。
+4. `python scripts/build_config_from_excel.py --write` → `uv run --with openpyxl python scripts/run_config_gate.py` 全 pass。
+5. `rg` 确认无桥接键/无 future 包残留引用(下游、测试)。
+
+## 验证命令
+
+```bash
+uv run --with openpyxl python scripts/run_config_gate.py
+rg -n '"pattern_recall": "matched"|"category_or_element_binding": "matched"' content_agent           # 无
+uv run python -c "import json;p=json.load(open('product_documents/规则包/douyin_rule_packs.v1.json'));print([x['rule_pack_id'] for x in p['rule_packs']], [d['dispatch_id'] for d in p['rule_pack_dispatch']])"   # 仅 content 包 + dispatch_content
+uv run pytest -q                                          # 配合 M3D 全绿
+```
+
+## 失败归因
+
+- 管线全拒:M3C 早于 M3B(旧门槛还在却没了桥接键 → pattern_recall=None not_in["matched"]→拒)。顺序必须 B→C。
+- byte_equal 红:JSON 删了包但 Excel 没删对应行(或反之)。
+- fk 红:删了被 content 包引用的 catalog 项(只应删 future 包专属项)。
+- 残留引用:测试/代码仍引用被砍包 id(M3D 一并清)。
+
+## sub-agent 交叉验证要点
+
+- 确认桥接键删除在 M3B 之后、删后无门槛读它、管线不全拒。
+- 确认 4 future 包 + dispatch 在 JSON 与 Excel 两边同步删、gate 全过、仅剩 content 包。
+- 确认 walk_policy 承接字段在(不动它)、未删 source_evidence 血缘、未动 walk_strategy。
+- 确认 pattern_match_result 不再含桥接键、无残留引用。

+ 83 - 0
tech_documents/工程落地/v3_implementation_briefs/M3/M3D_Tests_And_Controlled_Replay_Update.md

@@ -0,0 +1,83 @@
+# M3D 规则单测重写 + 受控回放更新 Implementation Brief
+
+状态:本 brief 覆盖「重写规则判定单测(新门槛/新打分维)+ 受控更新回放快照(新规则下 real_id45/sph_caihong 的新基线)」。M3 收尾,全量 pytest 绿。回放结果**变化是受控的**(09 受控变化表 M3 允许结果变化),不是回归。
+
+## 目标
+
+让测试反映 M3 的新判定:硬门槛(不适合50+拒/低置信拒/技术失败复看)、打分(relevance60+heat40 维度满分)、新 reason_code、旧 reason_code 不再出现;并把回放基线(FakeGemini 三结局 × 双渠道)重钉到新规则下的产物。
+
+## 现有证据
+
+- 规则测试现状:`test_rule_judgment_hard_gates.py`(6 例,构造 bundle 改字段断言 decision_action/reason_code/triggered_blocking_rules)、`test_rule_judgment_scorecard.py`(10 例,断言 score=各维相加、dimensions 明细);用 `decide(run_id, policy_run_id, idx, bundle, policy_bundle)` + `_state(tmp_path)` 取 policy_bundle/evidence_bundles。
+- 回放:`test_case_replay.py`(real_id45 现 4×KEEP、syn_pool、syn_review);`test_replay_gemini_seam.py`(M0/M2 改写过,FakeGemini 注入);M2 后 FakeGemini fake_gemini_pool=fit_true/conf0.9/rel0.85、review=rel0.45、fail=status failed(`tests/gemini_helpers.py`)。
+- 受控变化:09 计划 M3 受控变化表"允许结果变化"。real_id45 旧 4×KEEP 因 `missing_content_portrait`(M3 退役)→ 新规则下按 fit_senior/relevance/heat 重新落档。
+- M3A 产 platform_heat、M3B 新规则、M3C 删桥接键/砍包 —— 本 brief 在三者之后跑。
+
+## 修改范围
+
+- 重写 `tests/test_rule_judgment_hard_gates.py`、`tests/test_rule_judgment_scorecard.py`。
+- 受控更新 `tests/test_case_replay.py` 断言 + `tests/fixtures/snapshots/real_id45/decision_summary.json`(及 sph_caihong 若有快照);更新 `tests/test_replay_gemini_seam.py` 断言到新规则产物。
+- 如 M3B 因缺 op 小改了 evaluator,补 evaluator 对应单测。
+
+## 不修改范围
+
+- 不改规则包(M3B)/recall(M3C)/heat(M3A) —— 本 brief 只测。
+- 不改 thresholds、不改 effect_status_mapping、不改 DB schema。
+- 不动与 decode 无关的既有测试(M2 已删 decode 测试)。
+
+## 涉及文件 / 函数 / 类(测试用例清单)
+
+- `tests/test_rule_judgment_hard_gates.py`(重写):
+  - `test_not_fit_senior_rejected`(fit_senior_50plus=false → REJECT/content_not_fit_senior/rule_blocked)
+  - `test_low_confidence_rejected`(fit_confidence=0.4 → REJECT/content_low_confidence)
+  - `test_judge_failed_kept_for_review`(judge_status="failed" → KEEP_FOR_REVIEW/content_judge_failed/pending)
+  - `test_missing_platform_content_id_still_rejected`(保留门槛仍生效)
+  - `test_high_relevance_high_fit_passes_gates_into_scorecard`(fit_true/conf0.9 → 不被门槛拦,进打分)
+  - `test_old_pattern_reason_codes_absent`(任何决策不出现 content_pattern_recall_required/missing_content_portrait)
+- `tests/test_rule_judgment_scorecard.py`(重写):
+  - `test_score_relevance_and_heat_sum`(relevance_score 0.85→60 + platform_heat 0.9→40 = 100 → 进池/content_score_pool)
+  - `test_relevance_high_heat_low`(rel 0.85→60 + heat 0.3→0 = 60 → 待复看)
+  - `test_mid_relevance_mid_heat_review`(rel 0.6→45 + heat 0.6→30 = 75 → 进池)边界校验
+  - `test_low_relevance_rejected_by_score`(rel 0.3→0 + heat 0.3→0 = 0 → <60 拒/content_score_reject)
+  - `test_scorecard_only_two_active_dimensions`(dimensions 只含 relevance/platform_heat,旧维不在)
+  - `test_platform_heat_dimension_reads_engagement_metrics`(field_path 命中 M3A 注入值)
+- `tests/test_case_replay.py`(受控更新):real_id45 在默认 FakeGemini(pool)下新决策(进池/按新分);更新 `_SUMMARY` 断言 + decision_summary 快照;注释标"M3 受控变化:画像门槛退役、改 Gemini+heat 评分"。
+- `tests/test_replay_gemini_seam.py`(更新):三结局(pool→进池、review→按分待复看、fail→content_judge_failed 待复看)断言。
+- `tests/fixtures/snapshots/real_id45/decision_summary.json`:重钉新基线(pooled/review/rejected 计数按新规则)。
+
+## 数据合同
+
+- 单测断言新 reason_code 词表;旧词表不出现。
+- 打分断言遵循 relevance(60/45/25/0)+heat(40/30/20/10/0)分档 + 阈值 70/60。
+- 回放快照 = 新规则 + FakeGemini 默认结果的真实产物(由实现后实跑生成,非手编)。
+- 全量 pytest 绿。
+
+## 实施步骤
+
+1. 在 M3A+M3B+M3C 落地后,重写两个规则单测(按新门槛/新维度路径)。
+2. 实跑 `replay_case` 取 real_id45/sph_caihong 新产物 → 据实更新断言与快照(不手编数字)。
+3. 更新 test_replay_gemini_seam 三结局断言。
+4. `uv run pytest -q` 全绿;`rg` 确认旧 reason_code 仅历史注释。
+
+## 验证命令
+
+```bash
+uv run pytest tests/test_rule_judgment_hard_gates.py tests/test_rule_judgment_scorecard.py -q
+uv run pytest tests/test_case_replay.py tests/test_replay_gemini_seam.py -q
+uv run pytest -q
+uv run --with openpyxl python scripts/run_config_gate.py    # 与 M3B/M3C 配合仍 pass
+```
+
+## 失败归因
+
+- 快照对不上:手编了数字而非实跑产物(应用 replay 实跑结果重钉)。
+- 打分断言错:分档边界(gte 0.8/0.6/0.4)与 M3B 配置不一致。
+- 旧 reason_code 仍现:M3B 漏删某 gate 或 priority 仍引用退役项。
+- 回放"回归"误判:M3 结果变化是受控的,断言/快照应一起更新并注明,而非视为 bug。
+
+## sub-agent 交叉验证要点
+
+- 确认单测覆盖三新门槛 + 打分各档 + 旧 reason_code 缺席。
+- 确认回放快照由实跑生成、变化已注明"M3 受控变化"。
+- 确认全量 pytest 绿、config gate 仍过。
+- 确认未改规则包/recall/heat 实现(本 brief 只测)。