Ver Fonte

docs: second-round cross-validation fixes for M3-M6 briefs

四岗只读交叉验证(结构/decision-complete/一致性/代码事实)后修复:
- 事实错误:M4C/M4D 规则包 ID、M5B 幽灵方法名、M4A 配置名、M4D 路径、binding 行号
- 合同拍死:M4C binding 增补载体、M5B 限流 code 空集白名单、M6B decode event_id
  与最小补跑/.env 短等待档载体、M6C 聚合算法唯一来源
- 必失败 gate 修正:M6D threshold 误伤、M5D 密钥扫描自匹配
- 模糊词收口与 M4/M5/M6 索引重复模板段清理;06 计划五类 decode 事件对齐

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sam Lee há 3 dias atrás
pai
commit
e6bc217cf1
21 ficheiros alterados com 90 adições e 81 exclusões
  1. 4 4
      tech_documents/工程落地/06_V2阶段开发计划.md
  2. 11 0
      tech_documents/工程落地/v2_implementation_briefs/M3-M6_Brief_Repair_Summary.md
  3. 1 1
      tech_documents/工程落地/v2_implementation_briefs/M3/M3B_Policy_Bundle_By_Entity.md
  4. 1 0
      tech_documents/工程落地/v2_implementation_briefs/M3/M3C_Excel_Driven_Portrait_Rules.md
  5. 2 1
      tech_documents/工程落地/v2_implementation_briefs/M3/M3D_Scorecard_Missing_Dimensions.md
  6. 3 2
      tech_documents/工程落地/v2_implementation_briefs/M3/M3E_Tests_And_Gates.md
  7. 4 16
      tech_documents/工程落地/v2_implementation_briefs/M4/00_M4_Brief_Index.md
  8. 1 1
      tech_documents/工程落地/v2_implementation_briefs/M4/M4A_Query_Next_Page_Gate.md
  9. 3 1
      tech_documents/工程落地/v2_implementation_briefs/M4/M4B_Decision_Filtered_Author_Tag.md
  10. 15 9
      tech_documents/工程落地/v2_implementation_briefs/M4/M4C_Edge_Binding_And_Rule_Pack_Record.md
  11. 4 3
      tech_documents/工程落地/v2_implementation_briefs/M4/M4D_Source_Path_Records.md
  12. 1 1
      tech_documents/工程落地/v2_implementation_briefs/M4/M4E_Tests_And_Replay.md
  13. 3 9
      tech_documents/工程落地/v2_implementation_briefs/M5/00_M5_Brief_Index.md
  14. 5 7
      tech_documents/工程落地/v2_implementation_briefs/M5/M5B_Rate_Limit_And_Platform_Errors.md
  15. 3 3
      tech_documents/工程落地/v2_implementation_briefs/M5/M5C_Category_Match_V2.md
  16. 3 1
      tech_documents/工程落地/v2_implementation_briefs/M5/M5D_Tests_And_Smoke.md
  17. 2 6
      tech_documents/工程落地/v2_implementation_briefs/M6/00_M6_Brief_Index.md
  18. 3 3
      tech_documents/工程落地/v2_implementation_briefs/M6/M6A_Run_Events_Duration.md
  19. 10 3
      tech_documents/工程落地/v2_implementation_briefs/M6/M6B_Decode_Event_Sink.md
  20. 5 5
      tech_documents/工程落地/v2_implementation_briefs/M6/M6C_Timeline_Summary.md
  21. 6 5
      tech_documents/工程落地/v2_implementation_briefs/M6/M6D_Tests_And_Gates.md

+ 4 - 4
tech_documents/工程落地/06_V2阶段开发计划.md

@@ -411,8 +411,8 @@ V2 只增强后端事件和 API,不做前端组件。
 ### 技术实现思路
 
 - 每个阶段记录 stage、started_at、ended_at、duration_ms、attempt 等运行信息。
-- decode 增加 submitted、polling、succeeded、timeout 中间事件。
-- 单条 decode 超时后落 pending,不阻塞整条 run;后台最小补跑继续尝试回扣
+- decode 增加 submitted、polling、succeeded、failed、timeout 中间事件。
+- 单条 decode 超时后落 pending,不阻塞整条 run;run 末尾做一轮最小补跑继续尝试回扣(同步单轮,不起后台线程)
 - timeline 聚合总耗时、各阶段耗时和失败计数,供后续 web 看板展示。
 - V2-M6 先记录运行事实和失败计数,不预设卡点阈值。
 - “卡住”的业务判断留到后续基于真实运行数据再定义。
@@ -435,7 +435,7 @@ V2 只增强后端事件和 API,不做前端组件。
 
 - 增强 `content_agent_run_events`,不新增 DB 表 / runtime 文件。
 - 细粒度事件进 `raw_payload`,包含 stage、started_at、ended_at、duration_ms、attempt、错误类型和等待状态。
-- decode 通过 event_sink 发 submitted / polling / succeeded / timeout 中间事件。
+- decode 通过 event_sink 发 submitted / polling / succeeded / failed / timeout 中间事件。
 - decode 做最小补跑 / 不阻塞。
 - `.env.example` 加短等待档但默认仍 1200。
 - timeline 聚合 `total_duration_ms`、各阶段耗时、query 失败次数、平台限流次数、decode 中间态和错误计数。
@@ -461,7 +461,7 @@ V2 只增强后端事件和 API,不做前端组件。
 | 离线真实 case 回放 | 确认真实 id=45 暴露的问题被修复 | 画像缺失分流、被淘汰内容不外扩、walk action 归属包、timeline 耗时 |
 | 多案例语料库 | 覆盖全拒绝、入池、待复看三种结局 | 各结局 snapshot 可读,变更可解释 |
 | config x case 矩阵 | 确认改配置能改变产物但不破链路 | 默认包、放宽画像门槛、自定义 query profile、future 包归属 |
-| live smoke | 探测上游接口漂移 | 抖音风控、decode 卡点、分类树 v2 结构、DB 不残留 running |
+| live smoke | 探测上游接口漂移 | 抖音风控、decode 耗时 / 等待、分类树 v2 结构、DB 不残留 running |
 | 配置与 schema 闸 | 防止文档 / 配置 / DB 合同漂移 | config gate、schema registry、DB validator、命名检查 |
 
 详细命令见附录 D。

+ 11 - 0
tech_documents/工程落地/v2_implementation_briefs/M3-M6_Brief_Repair_Summary.md

@@ -61,6 +61,17 @@
 - 高危模糊词检查:已清除会导致误实现的模糊建议、二选一表述、旧测试名和错误 runtime 路径。
 - 配置与 schema 校验结果见本轮执行记录。
 
+## 第二轮交叉验证修复(提交 7927208 后复核)
+
+提交后再次由四个全新只读岗(结构完整性 / decision-complete / 跨文档一致性 / 代码事实)交叉验证,发现并修复以下问题:
+
+- 事实错误:M4C / M4D 数据合同示例中的 `douyin_path_observe_rule_pack_v1` 改为真实存在的 `douyin_path_stop_rule_pack_v1`;M5B 涉及函数中不存在的 `search_keyword/fetch_next_page/search_tag` 改为真实统一入口 `search()`;M4A 的 `rule_blocked -> stop_search_query` 改为真实配置名 `tr_rule_blocked_stop` + `stop_rule_blocked`;M4D 的 runtime_files 路径补全为 `content_agent/integrations/runtime_files.py`;M3B / M4 index 的 binding 行号尾界改为 284-345。
+- 合同缺口拍死:M4C 新增施工步骤 1,在 `walk_rule_pack_binding` 增补 `path_stop`(Path / douyin_path_stop_rule_pack_v1)与 `decision_to_asset`(Content / douyin_content_discovery_rule_pack_v1)两条 binding,承接 06 已拍板的 edge 归属修正;M5B business code 白名单拍死为 `RATE_LIMIT_BUSINESS_CODES` 空集常量(先靠 429 + message token,发现真实 code 再补入);M6B 固定 decode 事件 `event_id=evt_decode_{platform_content_id}_{event_type}_{attempt}`,并补"最小补跑 + .env.example 短等待档"两个 06 已拍板项的施工载体;M6C 拍死 `total_duration_ms` 回退顺序、`query_failure_count` 唯一来源(walk_actions query 链 failed)与 `decode_status_counts` 不混合回退规则。
+- 必失败 gate 修正:M6D 高危词扫描去掉合法术语 `threshold`,拆为自动失败 + 人工复核两段;M5D 密钥扫描改为人工复核口径并列明既有合法命中。
+- 模糊词收口:M4B skipped reason 写成场景→取值固定映射;M4D reason_code 改固定枚举;M4 index 回滚顺序"建议"改"固定为"、交付顺序拍死;M5 index "如要保护 blogger"改为固定独立 bucket;M3D / M6D "只断言关键字段"改为字段清单;M3E "如测试覆盖不足"改为按断言清单补齐。
+- 结构清理:M4 / M5 / M6 index 删除第二遍模板尾部的重复小节,独有信息(rule_next 口径、事件 id 枚举、bucket 口径)并入正文对应小节;M4E 断言区标题归位为"数据合同 / 断言清单";M6A/M6C/M6D 对 `test_run_timeline_observability.py`、`test_decode_events.py` 统一标注"新建"。
+- 其他:M3C 补 effect_status 经 `effect_status_mapping` 推导的机制说明;M3D 补顶层 `score_missing` 既有字段的命名区分;M3E 认领 Excel `strategy_id` 漂移复核项并放行 `evaluator.py:136` 既有命中;M5C 标清两个 category_match.py 的文件归属与唯一施工点;06 总计划 decode 事件统一为五类、"decode 卡点"措辞改"decode 耗时 / 等待"。
+
 ## 后续实现提醒
 
 - M3 不是零回归;画像缺失从淘汰变待复看是已拍板的受控变化,必须更新 replay 断言。

+ 1 - 1
tech_documents/工程落地/v2_implementation_briefs/M3/M3B_Policy_Bundle_By_Entity.md

@@ -10,7 +10,7 @@
 
 - `policy_json.py:27-42` 只返回一个 `rule_pack`、`rule_pack_id`、`dispatch`。
 - `rule_judgment.run()` 当前只接收单个 `policy_bundle` 并读取 `policy_bundle["rule_pack"]`。
-- `walk_rule_pack_binding` 在 `douyin_walk_strategy.v1.json:284-338` 已声明 edge 到 rule pack 的归属,但 policy bundle 当前没有 entity 映射可用。
+- `walk_rule_pack_binding` 在 `douyin_walk_strategy.v1.json:284-345` 已声明 edge 到 rule pack 的归属,但 policy bundle 当前没有 entity 映射可用。
 
 ## 修改范围
 

+ 1 - 0
tech_documents/工程落地/v2_implementation_briefs/M3/M3C_Excel_Driven_Portrait_Rules.md

@@ -61,6 +61,7 @@ M3C 唯一配置合同如下:
 - 画像完全缺失:进入待复看,不继续评分。
 - 画像明确为 weak:仍按现行 hard gate 淘汰。
 - `missing` 不再由 `age_50_plus_weak` 二次命中。
+- JSON `hard_gates` 条目当前没有 `effect_status` 字段,M3C 也不为其新增:`pending` 的落地机制是 `decision_action=KEEP_CONTENT_FOR_REVIEW` 经 `effect_status_mapping` 推导(evaluator `_effect_status_for_decision()`),施工时在 `effect_status_mapping` 中确认 / 补充该映射。
 
 ## 施工步骤
 

+ 2 - 1
tech_documents/工程落地/v2_implementation_briefs/M3/M3D_Scorecard_Missing_Dimensions.md

@@ -51,6 +51,7 @@ def _scorecard_all_dimensions_missing(scorecard): ...
 - `scorecard.dimensions[]` 增加 `score_missing` 属于 runtime JSON / raw payload 扩展,不新增 DB 列。
 - `matched_scoring_rules` 仍保留原数组。
 - `decision_replay_data.missing_dimensions` 写入所有 `score_missing=true` 的 dimension id,便于复盘。
+- 注意:`_scorecard_total()` 顶层已有布尔 `score_missing`(evaluator.py:284,含义=整体无可用评分),保持原义不动;本模块新增的是 dimension row 级 `score_missing` 与 `decision_replay_data.missing_dimensions`,实现时不得覆盖顶层字段。
 
 ## Unit Test
 
@@ -73,7 +74,7 @@ uv run pytest tests/test_case_replay.py tests/test_config_case_matrix.py -q
 
 ## 风险
 
-- 风险:新增 `score_missing` 让 snapshot diff 变大。只断言关键字段;如果 replay 因 M3 受控变化产生 snapshot diff,更新对应 M3 断言并在测试说明中标注“scorecard missing dimensions 受控变化”。
+- 风险:新增 `score_missing` 让 snapshot diff 变大。snapshot 只断言 `decision_action`、`decision_reason_codes`、dimension row 的 `score_missing`、`missing_dimensions` 四类字段;如果 replay 因 M3 受控变化产生 snapshot diff,更新对应 M3 断言并在测试说明中标注“scorecard missing dimensions 受控变化”。
 
 ## 回滚
 

+ 3 - 2
tech_documents/工程落地/v2_implementation_briefs/M3/M3E_Tests_And_Gates.md

@@ -15,7 +15,7 @@
 ## 修改范围
 
 - 补强现有 M3 相关测试文件。
-- 如测试覆盖不足,在上述现有文件中新增用例;不新增无关测试模块。
+- 断言清单中尚未被现有用例覆盖的条目,在上述现有文件中新增用例补齐;不新增无关测试模块。
 
 ## 不修改范围
 
@@ -101,7 +101,7 @@ git diff --name-only | rg "schema_registry|content_agent_schema.sql|runtime_file
 rg -n "missing.*weak|weak.*missing|age_50_plus_level" content_agent/business_modules/rule_judgment
 ```
 
-第二条命令只用于人工复核:允许测试名和配置读取引用,不允许业务硬编码分支。
+第二条命令只用于人工复核:允许测试名和配置读取引用,不允许业务硬编码分支。已知既有命中:`evaluator.py:136` 对 `age_50_plus_level` 的字段缺省归一属机制层取值,不算业务硬编码分支,复核时放行。
 
 ## 失败归因
 
@@ -121,3 +121,4 @@ rg -n "missing.*weak|weak.*missing|age_50_plus_level" content_agent/business_mod
 - 确认测试文件名都存在,新增测试只落在 M3 相关测试文件。
 - 确认 `git diff --name-only` 不包含 `content_agent/api.py`、`content_agent/schemas.py`、`tests/test_api.py`、`content_agent/dashboard_service.py`、`web/`。
 - 确认 `validate_schema_registry.py` 仍为 pass。
+- 确认 Excel `rule_package_meta.strategy_id` 与 JSON `strategy_id` 的漂移已由 `validate_config_excel_sync.py` 覆盖;若校验未覆盖该字段,在本轮施工中一并修正并补断言。

+ 4 - 16
tech_documents/工程落地/v2_implementation_briefs/M4/00_M4_Brief_Index.md

@@ -25,7 +25,7 @@
 - `walk_engine.py:211-279` `_execute_author_edges()` 从 discovered items 去重作者后直接拉作品,没有按 RuleDecision 过滤。
 - `_tag_queries()` 已看 `search_query_effect_status in {"success","pending"}`,但还没有用 `decision_action` 做 ADD / KEEP / REJECT 分层。
 - `douyin_walk_strategy.v1.json:240-248` 已声明 `query_next_page` cursor trigger,`272-281` 已声明 `rule_blocked` 停止。
-- `douyin_walk_strategy.v1.json:284-338` 已声明 edge 到 rule pack 的 binding
+- `douyin_walk_strategy.v1.json:284-345` 已声明 edge 到 rule pack 的 binding(当前 6 条边,`path_stop`、`decision_to_asset` 由 M4C 增补)
 - 真实 DB run `v1_run_faaf9a1d0ad6`:`query_next_page=2` success、`author_to_works=2` failed、二者 `rule_pack_id=NULL`;`path_stop=4` 错挂 Content 包。
 
 ## 已拍板 / 默认推进
@@ -39,6 +39,7 @@
 ## 数据合同
 
 - walk action 继续写 `walk_actions.jsonl` / `content_agent_walk_actions`。
+- `rule_next` 决策口径固定来自 `rule_decisions`(内存)与 `walk_rule_pack_binding`(绑定源)。
 - 不新增 DB 列;`walk_actions.rule_pack_id` 写 edge binding 的归属包。
 - `raw_payload.rule_pack_binding` 写 binding 明细。
 - `raw_payload.rule_pack_execution` 写实际执行事实。
@@ -58,7 +59,7 @@
 - 不新增无限深层游走。
 - 不做多包叠加。
 - future 包未执行时必须明确 `executed=false`。
-- M4 触发过滤可先交付;binding 写回依赖 M3 dispatch 参数化但不得伪装已执行。
+- 交付顺序固定:M4A / M4B(触发过滤)先交付;M4C / M4D(binding 写回)在 M3A dispatch 参数化合入后交付,期间不得伪装已执行。
 
 ## 不修改范围
 
@@ -82,19 +83,6 @@ uv run pytest -q
 - 只覆盖 M4A-M4E 子 brief 的接线与可观测记录。
 - 不改 M2 query 生成、M5/M6 平台链路。
 
-## 不修改范围
-
-- 不新增 DB schema / runtime 文件。
-- 不改 `content_agent/api.py`、`content_agent/schemas.py`、`tests/test_api.py`、`content_agent/dashboard_service.py`、`web/` 的现有用户改动。
-- 不做多包叠加。
-
-## 数据合同
-
-- `rule_next` 决策口径固定来自 `rule_decisions`(内存)与 `walk_rule_pack_binding`(绑定源)。
-- `walk_actions.rule_pack_id` 表示边归属。
-- `raw_payload.rule_pack_execution` 表示实际执行。
-- `query_next_page` 仅允许 `search_query_effect_status=="success"`。
-
 ## Unit Test
 
 - `tests/test_walk_engine_pagination.py`
@@ -118,7 +106,7 @@ uv run pytest -q
 
 ## 回滚
 
-- 回滚顺序建议 M4A→M4B→M4C→M4D→M4E,保留现有 M4 前基础代码行为。
+- 回滚顺序固定为 M4E→M4D→M4C→M4B→M4A(按依赖反向),保留现有 M4 前基础代码行为。
 
 ## 复核清单
 

+ 1 - 1
tech_documents/工程落地/v2_implementation_briefs/M4/M4A_Query_Next_Page_Gate.md

@@ -9,7 +9,7 @@
 ## 当前事实
 
 - `content_agent/business_modules/walk_engine.py` 的 `_pagination_queries()` 当前只检查 `item.has_more`、`next_cursor` 和 query 去重。
-- `product_documents/抖音游走策略/douyin_walk_strategy.v1.json` 已写 `rule_blocked -> stop_search_query`,但当前游走执行没有完整消费该闸。
+- `product_documents/抖音游走策略/douyin_walk_strategy.v1.json` 已写 rule_blocked 停止闸:trigger `tr_rule_blocked_stop`(`content_effect_status=="rule_blocked"`,edge=`path_stop`)+ stop policy `stop_rule_blocked`(`stop_action="stop_path"`),但当前游走执行没有完整消费该闸。
 - DB run `v1_run_faaf9a1d0ad6` 中 4 条 `search_clue` 全是 `rule_blocked`,仍有 `q_001_page_002`、`q_002_page_002` 两条翻页 query。
 - `record_run()` 在 `execute_walk()` 之后才统一落 runtime / DB,游走阶段不能依赖尚未落库的 `search_clues`。
 

+ 3 - 1
tech_documents/工程落地/v2_implementation_briefs/M4/M4B_Decision_Filtered_Author_Tag.md

@@ -93,7 +93,9 @@ source path 只在已有映射能表达时写;M4B 不新增 runtime 文件。
    - `tag_query`:ADD true,KEEP false,REJECT false。
 4. `_execute_author_edges(...)` 用 helper 过滤作者候选。
 5. `_tag_queries(...)` 用 helper 过滤 tag 候选。
-6. 对被过滤的作者/tag 写 skipped walk action,原因固定为 `blocked_by_rule_decision` 或 `review_tag_expansion_disabled`。
+6. 对被过滤的作者/tag 写 skipped walk action,reason 按场景固定映射,无第三种取值:
+   - `REJECT_CONTENT` / `rule_blocked` 导致 author / tag 被过滤 → `blocked_by_rule_decision`。
+   - `KEEP_CONTENT_FOR_REVIEW` 导致 tag 不扩展 → `review_tag_expansion_disabled`。
 7. KEEP 作者扩展使用低预算,不突破现有作者数 / 作品数上限。
 
 ## Unit Test

+ 15 - 9
tech_documents/工程落地/v2_implementation_briefs/M4/M4C_Edge_Binding_And_Rule_Pack_Record.md

@@ -15,12 +15,14 @@
 
 - `product_documents/抖音游走策略/douyin_walk_strategy.v1.json` 有 edge -> rule pack binding。
 - binding 来源是 `WalkStrategyStore.load_walk_strategy()["walk_rule_pack_binding"]`,不是 policy bundle 主体。
+- `walk_rule_pack_binding` 当前只有 6 条边(video_to_author、author_to_works、video_to_hashtag、hashtag_to_query、query_next_page、budget_downgrade),没有 `path_stop`、`decision_to_asset` 条目。
 - `walk_engine.py` 当前不读取 binding。
 - `_walk_action()` 当前没有 `rule_pack_id` 参数。
 - DB run `v1_run_faaf9a1d0ad6` 中 `query_next_page` / `author_to_works` 的 `rule_pack_id` 是 NULL,`path_stop` 错挂 Content 包,不能说明 Path 包真的执行过。
 
 ## 修改范围
 
+- `product_documents/抖音游走策略/douyin_walk_strategy.v1.json`(仅 `walk_rule_pack_binding` 增补 `path_stop`、`decision_to_asset` 两条 binding,不改其他 section)
 - `content_agent/business_modules/walk_engine.py`
 - `content_agent/integrations/walk_strategy_json.py`
 - `tests/test_walk_actions_runtime.py`
@@ -54,7 +56,7 @@
     "rule_pack_binding": {
       "edge_id": "author_to_works",
       "target_entity": "Budget",
-      "dispatch_policy": "future"
+      "dispatch_policy": "advisory"
     },
     "rule_pack_execution": {
       "executed": false,
@@ -69,7 +71,7 @@
 
 ```json
 {
-  "rule_pack_id": "douyin_path_observe_rule_pack_v1",
+  "rule_pack_id": "douyin_path_stop_rule_pack_v1",
   "raw_payload": {
     "rule_pack_execution": {
       "executed": true,
@@ -84,16 +86,20 @@
 
 ## 施工步骤
 
-1. 在游走入口加载 walk strategy 后,构建 `_binding_by_edge_id(walk_strategy)`。
-2. `_binding_by_edge_id(...)` 只读取 `walk_strategy["walk_rule_pack_binding"]`。
-3. `_resolve_edge_binding(edge_id, walk_strategy)` 返回 binding 明细;未知 edge 返回空 binding 和 reason `edge_binding_missing`。
-4. `_walk_action(...)` 接收 binding 和 execution record。
-5. `_walk_action(...)` 顶层写 `rule_pack_id=binding.rule_pack_id`。
-6. future 包未启用时写:
+1. 在 `douyin_walk_strategy.v1.json` 的 `walk_rule_pack_binding` 增补两条 binding(仅此两条;`budget_downgrade` 已有条目不动):
+   - `path_stop` → `target_entity="Path"`、`rule_pack_id="douyin_path_stop_rule_pack_v1"`、`rule_pack_version="1.0.0"`、`required=false`、`dispatch_policy="advisory"`。
+   - `decision_to_asset` → `target_entity="Content"`、`rule_pack_id="douyin_content_discovery_rule_pack_v1"`、`rule_pack_version="1.0.0"`、`required=false`、`dispatch_policy="advisory"`。
+   增补后跑 `uv run python scripts/validate_walk_strategy_config.py` 确认配置合法。
+2. 在游走入口加载 walk strategy 后,构建 `_binding_by_edge_id(walk_strategy)`。
+3. `_binding_by_edge_id(...)` 只读取 `walk_strategy["walk_rule_pack_binding"]`。
+4. `_resolve_edge_binding(edge_id, walk_strategy)` 返回 binding 明细;未知 edge 返回空 binding 和 reason `edge_binding_missing`。
+5. `_walk_action(...)` 接收 binding 和 execution record。
+6. `_walk_action(...)` 顶层写 `rule_pack_id=binding.rule_pack_id`。
+7. future 包未启用时写:
    - `executed=false`
    - `executed_rule_pack_id=null`
    - `reason="future_pack_not_enabled"`
-7. 复用 Content decision 作为游走 gate 时写:
+8. 复用 Content decision 作为游走 gate 时写:
    - `executed=true`
    - `executed_rule_pack_id="douyin_content_discovery_rule_pack_v1"`
    - `reason="content_decision_reused_for_walk_gate"`

+ 4 - 3
tech_documents/工程落地/v2_implementation_briefs/M4/M4D_Source_Path_Records.md

@@ -17,7 +17,8 @@
 
 ## 当前事实
 
-- `content_agent/runtime_files.py` 已包含 `source_path_records.jsonl` 和 `walk_actions.jsonl`。
+- `content_agent/integrations/runtime_files.py` 的 `RUNTIME_FILENAMES` 已包含 `source_path_records.jsonl` 和 `walk_actions.jsonl`。
+- `path_stop`、`decision_to_asset` 的 binding 条目由 M4C 施工步骤 1 在 `walk_rule_pack_binding` 中增补;M4D 只消费,不再改配置。
 - DB run `v1_run_faaf9a1d0ad6` 中 `source_path_records=8`、`walk_actions=8`。
 - M4 前真实 `path_stop` 使用 Content 包,不能说明 Path 包真的执行过。
 - `query_next_page` / `author_to_works` 缺少 rule pack binding 和 execution 说明。
@@ -55,7 +56,7 @@
   "edge_id": "path_stop",
   "from_node_id": "rule_decision",
   "to_node_id": "stop",
-  "rule_pack_id": "douyin_path_observe_rule_pack_v1",
+  "rule_pack_id": "douyin_path_stop_rule_pack_v1",
   "raw_payload": {
     "rule_pack_binding": {
       "edge_id": "path_stop",
@@ -82,7 +83,7 @@
    - `walk_strategy.run()` 生成的 path stop / decision asset 路径
 2. 统一通过 `_walk_action()` 写入 rule pack owner 和 execution record。
 3. 在 source path record 写入处复用同一份 binding / execution 对象,避免双写不一致。
-4. skipped 动作也要写明 `reason_code`,例如 `blocked_by_rule_decision`、`future_pack_not_enabled`、`edge_binding_missing`
+4. skipped 动作也要写明 `reason_code`,固定枚举为 `blocked_by_rule_decision`、`review_tag_expansion_disabled`、`future_pack_not_enabled`、`edge_binding_missing`,不另造新值
 5. source path record 不作为游走决策输入,只作为解释输出。
 
 ## Unit Test

+ 1 - 1
tech_documents/工程落地/v2_implementation_briefs/M4/M4E_Tests_And_Replay.md

@@ -32,7 +32,7 @@
 - 不改 runtime 文件名常量。
 - 不触碰 `content_agent/api.py`、`content_agent/schemas.py`、`tests/test_api.py`、`content_agent/dashboard_service.py`、`web/` 的既有用户改动。
 
-## 数据合同
+## 数据合同 / 断言清单
 
 M4 测试必须覆盖:
 

+ 3 - 9
tech_documents/工程落地/v2_implementation_briefs/M5/00_M5_Brief_Index.md

@@ -9,7 +9,7 @@
 - `fetch_author_works()` 必须打 `/crawler/dou_yin/blogger`。
 - `account_id = author.platform_author_id`。
 - 12 秒限流 bucket 只覆盖搜索链:`keyword_search`、`query_next_page`、`tag_query`。
-- blogger 作者作品不进入同一搜索 bucket;如要保护 blogger,使用独立 bucket,默认不阻塞搜索链。
+- blogger 作者作品不进入搜索 bucket,固定使用独立 `douyin_blogger` bucket(见 M5B 施工步骤),默认不阻塞搜索链。
 - 限流错误区分为 `PLATFORM_RATE_LIMITED`。
 - 分类树 match-paths v2 parser 支持 `items[].matches[].path` 和 `items[].matched_paths[]`。
 
@@ -74,6 +74,8 @@
 }
 ```
 
+限流 bucket 固定:搜索链共用 `douyin_search`,作者作品独立 `douyin_blogger`(默认不阻塞搜索链)。category match v2 输出支持 `items[].matches[].path` 与 `items[].matched_paths[]`。
+
 ## 红线验收
 
 - 不把 live Crawapi 放入常态 CI。
@@ -104,14 +106,6 @@ uv run python scripts/validate_schema_registry.py
 - 不把 live smoke 并入默认 CI。
 - 不触碰 `content_agent/api.py`、`content_agent/schemas.py`、`tests/test_api.py`、`content_agent/dashboard_service.py`、`web/` 的现有用户改动。
 
-## 数据合同
-
-- 作者作品接口固定 `/crawler/dou_yin/blogger` 与 `account_id/platform_author_id` 映射。
-- 搜索链限流 bucket:`douyin_search`。
-- 作者作品限流 bucket:`douyin_blogger`(默认不阻塞搜索链)。
-- 限流错误码:`PLATFORM_RATE_LIMITED`。
-- category match v2 输出支持 `items[].matches[].path` 与 `items[].matched_paths[]`。
-
 ## Unit Test
 
 - `tests/test_douyin_client.py`

+ 5 - 7
tech_documents/工程落地/v2_implementation_briefs/M5/M5B_Rate_Limit_And_Platform_Errors.md

@@ -9,7 +9,7 @@
 ## 当前事实
 
 - `douyin.py:_post_json()` 当前 HTTP/network/bad_json/business_error 都转 `RuntimeError`。
-- `platform_access._query_failure()` 对普通异常统一输出 `PLATFORM_REQUEST_FAILED`。
+- `platform_access._query_failure()` 对普通异常统一输出 `PLATFORM_REQUEST_FAILED`;其 `ContentAgentError` error code 透传分支已存在(platform_access.py:80-88),施工步骤 10 是验证保持,不是新写
 - `content_agent/errors.py` 当前没有 `PLATFORM_RATE_LIMITED`。
 - DB run `v1_run_faaf9a1d0ad6` 中作者作品失败为 `RuntimeError`,错误分类无法支持后续失败计数。
 
@@ -34,9 +34,7 @@
 - `ErrorCode`
 - `ContentAgentError`
 - `CrawapiDouyinClient._post_json(...)`
-- `CrawapiDouyinClient.search_keyword(...)`
-- `CrawapiDouyinClient.fetch_next_page(...)`
-- `CrawapiDouyinClient.search_tag(...)`
+- `CrawapiDouyinClient.search(...)`:keyword 搜索、翻页、tag query 的统一入口。client 没有独立的翻页 / tag 方法,三类操作都经 `platform_access.run()` → `client.search()` 进入。
 - `CrawapiDouyinClient.fetch_author_works(...)`
 - `platform_access._query_failure(...)`
 
@@ -82,12 +80,12 @@ blogger 作者作品使用独立 bucket:
        def wait(self, bucket: str) -> None
    ```
 3. `CrawapiDouyinClient.__init__()` 接收 `rate_limiter=None`。
-4. keyword search、query next page、tag query 调 `_post_json(..., rate_limit_bucket="douyin_search")`。
+4. keyword search、query next page、tag query 统一经 `search()` 调 `_post_json(..., rate_limit_bucket="douyin_search")`,不新增独立方法
 5. blogger 作者作品调 `_post_json(..., rate_limit_bucket="douyin_blogger")`,不阻塞 `douyin_search`。
 6. `_post_json()` 在发请求前按 bucket 调 `rate_limiter.wait(bucket)`。
 7. `_post_json()` 固定识别以下限流:
    - HTTP 429。
-   - business code 命中明确限流白名单
+   - business code 命中 `RATE_LIMIT_BUSINESS_CODES` 白名单(`douyin.py` 模块级常量,初始值固定为空集——当前没有任何已证实的限流 business code,识别先依靠 HTTP 429 与 message token;live smoke / 真实运行发现新 code 后再补入常量并加用例)
    - message 命中固定 token:`限流`、`请求频繁`、`rate limit`、`too many requests`。
 8. `_post_json()` 对限流抛 `ContentAgentError(ErrorCode.PLATFORM_RATE_LIMITED, ...)`。
 9. `_post_json()` 对普通 500、业务失败、强制登录、bad_json 继续走非限流错误。
@@ -101,7 +99,7 @@ blogger 作者作品使用独立 bucket:
 - `test_search_chain_uses_shared_search_bucket`
 - `test_blogger_uses_separate_bucket_from_search_chain`
 - `test_http_429_maps_to_platform_rate_limited`
-- `test_business_rate_limit_code_maps_to_platform_rate_limited`
+- `test_business_rate_limit_code_maps_to_platform_rate_limited`(测试内 monkeypatch `RATE_LIMIT_BUSINESS_CODES` 注入样例 code 验证机制,不依赖真实 code)
 - `test_rate_limit_message_token_maps_to_platform_rate_limited`
 - `test_force_login_without_rate_limit_code_is_not_rate_limited`
 - `test_bad_json_is_not_rate_limited`

+ 3 - 3
tech_documents/工程落地/v2_implementation_briefs/M5/M5C_Category_Match_V2.md

@@ -29,9 +29,9 @@
 
 ## 涉及文件 / 函数
 
-- `CategoryMatchClient.match_paths(...)`
-- `_extract_path_matches(...)`
-- category match 结果归一 helper
+- `CategoryMatchClient.match_paths(...)`(`content_agent/integrations/category_match.py`,现状默认已打 v2 path,本模块只读不改)
+- `_extract_path_matches(...)`(`content_agent/business_modules/content_discovery/pattern_recall/category_match.py`,唯一施工点)
+- category match 结果归一 helper(与 `_extract_path_matches` 同文件)
 
 ## 数据合同
 

+ 3 - 1
tech_documents/工程落地/v2_implementation_briefs/M5/M5D_Tests_And_Smoke.md

@@ -108,9 +108,11 @@ uv run python scripts/smoke_douyin_blogger.py --author-id '<known_author_id>'
 
 ```bash
 git diff --name-only | rg "content_agent_schema.sql|schema_registry|runtime_files.py|web/" && exit 1 || true
-rg -n "COOKIE|TOKEN|PASSWORD|CONTENTFIND_API_CRAWAPI_KEY=.*[^<]" tests tech_documents/工程落地/v2_implementation_briefs/M5 && exit 1 || true
+rg -n "COOKIE|TOKEN|PASSWORD|CONTENTFIND_API_CRAWAPI_KEY=.*[^<]" tests scripts
 ```
 
+第二条命令只用于人工复核,不接 `exit 1`:`tests/test_p6_drift_guards.py`、`tests/test_p8_drift_guards.py`、`tests/test_database_runtime.py` 中既有防泄漏断言的字面量属合法命中,放行;`scripts/smoke_douyin_blogger.py` 与本轮新增测试必须零真实密钥 / cookie 值。本文档自身包含这些大写词,不纳入扫描目录。
+
 ## 失败归因
 
 - pytest 真连外部:fake HTTP 没接入。

+ 2 - 6
tech_documents/工程落地/v2_implementation_briefs/M6/00_M6_Brief_Index.md

@@ -49,6 +49,8 @@ M6 先记录运行事实和失败计数,不预设卡点阈值:
 - 不新增 DB 表或列。
 - 细粒度事实写 `run_events.raw_payload`。
 - timeline API 返回新增 `summary`,保留 `items`、`total`、`data_origin`。
+- run events:固定 `stage_started` / `stage_completed` / `stage_failed`,事件 id `evt_stage_{stage}_{attempt}_{phase}`。
+- decode events:固定 `decode_submitted` / `decode_polling` / `decode_succeeded` / `decode_failed` / `decode_timeout`,事件 id `evt_decode_{platform_content_id}_{event_type}_{attempt}`。
 
 固定 summary:
 
@@ -94,12 +96,6 @@ uv run pytest -q
 - 不输出自动 `stalled=true`、`is_blocked=true` 解释字段。
 - 不替代 RuleDecision 业务决策。
 
-## 数据合同
-
-- run events:固定 `stage_started` / `stage_completed` / `stage_failed`,事件 id `evt_stage_{stage}_{attempt}_{phase}`。
-- decode events:固定 `decode_submitted` / `decode_polling` / `decode_succeeded` / `decode_failed` / `decode_timeout`。
-- timeline 响应新增 `summary`,保留 `items`、`total`、`data_origin`。
-
 ## Unit Test
 
 - `tests/test_run_timeline_observability.py`

+ 3 - 3
tech_documents/工程落地/v2_implementation_briefs/M6/M6A_Run_Events_Duration.md

@@ -4,7 +4,7 @@
 
 ## 目标
 
-每个主要阶段记录开始、结束、耗时、attempt 和状态,供后续 timeline 聚合。
+每个 graph 节点阶段记录开始、结束、耗时、attempt 和状态,供后续 timeline 聚合。stage 名固定取 `graph.py` 注册的节点名,不另造别名。
 
 ## 当前事实
 
@@ -18,7 +18,7 @@
 - `content_agent/graph.py`
 - `content_agent/run_service.py`
 - `content_agent/business_modules/run_record/recorder.py`
-- `tests/test_run_timeline_observability.py`
+- `tests/test_run_timeline_observability.py`(新建,见 M6D)
 
 ## 不修改范围
 
@@ -94,7 +94,7 @@ phase 只能是:
 
 ## 施工步骤
 
-1. 在 graph/run_service 外层包装每个阶段,不在各业务函数里散写计时。
+1. 包装层固定在 `graph.py` 节点注册处统一实现(单一入口包装每个节点);`run_service.py` 不重复计时,不在各业务函数里散写计时。
 2. 阶段开始前写 `stage_started`。
 3. 阶段成功后写 `stage_completed`。
 4. 阶段失败时写 `stage_failed`,并保留原异常向上抛或原失败处理路径。

+ 10 - 3
tech_documents/工程落地/v2_implementation_briefs/M6/M6B_Decode_Event_Sink.md

@@ -1,6 +1,6 @@
 # M6B Decode Event Sink Implementation Brief
 
-状态:本 brief 覆盖 decode submitted / polling / succeeded / failed / timeout 中间事件。M6B 是施工单位,不再需要额外拍板。
+状态:本 brief 覆盖 decode submitted / polling / succeeded / failed / timeout 中间事件、最小补跑与 `.env.example` 短等待档。M6B 是施工单位,不再需要额外拍板。
 
 ## 目标
 
@@ -19,8 +19,9 @@
 - `content_agent/business_modules/content_discovery/pattern_recall/recall_decision.py`
 - `content_agent/business_modules/walk_engine.py`
 - `content_agent/graph.py`
-- `tests/test_decode_events.py`
+- `tests/test_decode_events.py`(新建)
 - `tests/test_pattern_recall_decode.py`
+- `.env.example`(只加 `CONTENTFIND_PATTERN_RECALL_MAX_WAIT_SECONDS` 短等待档注释示例,默认值 1200 不变)
 
 ## 不修改范围
 
@@ -77,6 +78,8 @@ event sink 接收 dict:
 - `elapsed_ms`
 - `failure_reason`
 
+事件写入 `run_events.jsonl` / `content_agent_run_events` 时,`event_id` 固定为 `evt_decode_{platform_content_id}_{event_type}_{attempt}`(submitted 首发 attempt=1,polling 按轮次递增,succeeded / failed / timeout 取终止时的 attempt 值),与 M6A 的 `evt_stage_*` 命名空间不冲突。
+
 ## 施工步骤
 
 1. `decode_content(..., event_sink=None)` 增加可选参数,默认 None 保持旧调用兼容。
@@ -88,7 +91,9 @@ event sink 接收 dict:
 7. 每次调用 event_sink 都用 try/except 包住;sink 异常必须吞掉,不改变 decode 业务结果。
 8. sink 异常只允许写脱敏 debug / raw 信息,不向最终业务结果注入失败。
 9. `recall_decision.run(..., event_sink=None)` 透传给 `decode_content()`。
-10. graph / walk 调用 pattern recall 时提供 sink,把事件 append 到 `run_events.jsonl`。
+10. graph / walk 调用 pattern recall 时提供 sink,把事件按上述 `event_id` 规则 append 到 `run_events.jsonl`。
+11. 最小补跑(06 已拍板项):`recall_decision.run()` 主循环结束后,对 decode 状态仍为 pending / running 的内容同步执行一轮补跑——每条只再调一次 `get_decode_result()`,不重新 submit、不新增等待循环、不起后台线程 / 进程;成功则更新该条 evidence 并发 `decode_succeeded`,仍未完成则保持 pending 并发 `decode_timeout`。补跑不改变 validation 与业务结果语义。
+12. `.env.example` 增加 `CONTENTFIND_PATTERN_RECALL_MAX_WAIT_SECONDS` 短等待档注释示例(如 300),默认值仍为 1200,代码默认行为不变。
 
 ## Unit Test
 
@@ -99,6 +104,7 @@ event sink 接收 dict:
 - `test_decode_timeout_still_returns_pending`
 - `test_decode_client_error_records_failed_event`
 - `test_decode_event_sink_exception_is_swallowed`
+- `test_minimal_rerun_pass_updates_pending_decode_once`
 
 在 `tests/test_pattern_recall_decode.py` 补强:
 
@@ -131,3 +137,4 @@ uv run pytest tests/test_case_replay.py -q
 - [ ] event sink 异常被吞掉。
 - [ ] timeout 仍返回 pending/running,不定义卡住。
 - [ ] 旧调用不传 event_sink 时行为不变。
+- [ ] pending decode 在 run 末尾有且只有一轮最小补跑,不起后台线程。

+ 5 - 5
tech_documents/工程落地/v2_implementation_briefs/M6/M6C_Timeline_Summary.md

@@ -17,8 +17,8 @@ timeline 返回运行事实摘要,不自动判断“卡住”。
 
 - `content_agent/dashboard_service.py`
 - `content_agent/schemas.py`
-- `tests/test_run_timeline_observability.py`
-- `tests/test_decode_events.py`
+- `tests/test_run_timeline_observability.py`(新建,见 M6D)
+- `tests/test_decode_events.py`(新建,见 M6B/M6D)
 - `tests/test_api.py`
 
 ## 不修改范围
@@ -81,11 +81,11 @@ decision.get("search_query_effect_status") or decision.get("content_effect_statu
 
 1. `DashboardService.timeline()` 保留现有 `items`、`total`、`data_origin`。
 2. 新增 `_timeline_summary(events, walk_actions, source_paths, recalls)`。
-3. `total_duration_ms` 优先用 run started/completed 或 stage events 聚合
+3. `total_duration_ms` 固定算法:优先取 run started / completed 事件差值;任一缺失时回退为 `stage_duration_ms` 求和;两者都缺时为 `null`,不估算
 4. `stage_duration_ms` 读取 `run_events.raw_payload.stage` 和 `duration_ms`。
-5. `query_failure_count` 统计 query failure event 或 walk/platform failure 中 query 失败
+5. `query_failure_count` 唯一来源:`walk_actions` 中 query 链动作(`query_next_page`、`tag_query`)且 `status=="failed"` 的条数;不读取 run_events 失败事件,不求并集、不双计。首轮 keyword 搜索失败属于 stage 失败,计入 `error_counts` 而非本字段
 6. `platform_rate_limited_count` 统计 `error_code=="PLATFORM_RATE_LIMITED"`。
-7. `decode_status_counts` 统计 M6B decode events,也兼容 pattern recall evidence 的最终 decode status
+7. `decode_status_counts` 只统计 M6B decode events;仅当该 run 没有任何 decode event(M6 前旧数据)时,整体回退为统计 recalls 的最终 decode status;两种来源不混合、不双计
 8. `error_counts` 按 `error_code` 聚合。
 9. `walk_status_counts` 按 walk action status 聚合。
 10. `TimelineResponse` schema 新增 `summary`,保留旧字段。

+ 6 - 5
tech_documents/工程落地/v2_implementation_briefs/M6/M6D_Tests_And_Gates.md

@@ -8,7 +8,7 @@
 
 ## 当前事实
 
-- `tests/test_run_timeline_observability.py` 当前是 M6 需要新增或补强的测试文件。
+- `tests/test_run_timeline_observability.py` 当前不存在,是 M6 需要新建的测试文件。
 - `tests/test_decode_events.py` 当前是 M6 需要新增的测试文件。
 - `tests/test_api.py` 当前有未提交脏改;M6 实施前必须先读 diff,再在不覆盖用户改动的前提下补 timeline schema/API 断言。
 - schema registry 当前维持 21 表 / 13 runtime 文件。
@@ -42,7 +42,7 @@
 
 ## 施工步骤
 
-1. 新建或补强 `tests/test_run_timeline_observability.py`。
+1. 新建 `tests/test_run_timeline_observability.py`。
 2. 新建 `tests/test_decode_events.py`。
 3. 读取当前 `tests/test_api.py` diff,确认用户改动后再补 timeline response 断言。
 4. 跑 M6A-M6C 单元测试。
@@ -71,15 +71,16 @@ uv run pytest -q
 
 ```bash
 git diff --name-only | rg "content_agent_schema.sql|schema_registry|runtime_files.py|web/" && exit 1 || true
-rg -n "stalled=true|is_blocked=true|threshold|阈值判断" content_agent tests && exit 1 || true
+rg -n "stalled=true|is_blocked=true" content_agent tests && exit 1 || true
+rg -n "阈值判断|卡住判断" content_agent
 ```
 
-文档中允许出现“禁止输出 stalled/is_blocked”的说明;实现代码不得出现自动卡住判断。
+前两条命令自动失败;第三条命令只用于人工复核 M6 新增代码未引入自动卡住阈值,不接 `exit 1`——scorecard 的 `threshold` 是既有合法术语(`evaluator.py`、`tests/test_rule_judgment_scorecard.py` 等),不纳入自动失败。文档中允许出现“禁止输出 stalled/is_blocked”的说明;实现代码不得出现自动卡住判断。
 
 ## Case Replay
 
 - `real_id45` 业务结果不因 M6 改变。
-- 可以新增观测字段 snapshot,但必须只断言关键字段,不对时间戳做 byte-equal。
+- 可以新增观测字段 snapshot,但只断言 `event_type`、`stage`、`attempt` 与计数类字段,不对 `started_at` / `ended_at` / `duration_ms` 做精确值断言,不对时间戳做 byte-equal。
 - replay diff 只允许出现 run event / timeline summary 的观测字段变化。
 
 ## 失败归因