docs(plan): 审计排期批实施计划——P2×9+P3×9+顺带×2 折 8 棒(Retry-After/状态机互斥/IC 闸可观测/死亡回写留痕/manifest 四态/缺口单去重/小因子换皮/杂项收敛),需求=审计原文逐条内嵌 [nas]

This commit is contained in:
2026-10-05 12:44:46 +08:00
parent 586d2b172f
commit feef354115
@@ -0,0 +1,129 @@
# 审计排期批(P2×9+P3×9+顺带×2)Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** 用户令「剩余认同件修掉」——infra 分诊派给我域的排期批全量:P2-1/2/5/6/9/10/11/12/13 + P3 因子簇 9/11/12/15/16/17/18/19/20(+顺带 P3-10/13 同文件)。
**Architecture:** 按文件族折 8 棒(T1 llm/T2 状态机互斥/T3 ic_gate+批接线/T4 version_registry/T5 manifest 四态/T6 缺口单去重/T7 factor_guard 换皮/T8 杂项收敛),每棒 TDD 红绿+commit。需求原文=审计档 `audit/20261003_recent2days.md`(坐标经 infra 核对 HEAD 有效;P5 落地后 routes_pipeline 行号有位移,实施者以 grep 定位为准)。
**Tech Stack:** FastAPI/SQLite/pytest(后端),无前端件。
## Global Constraints
- 测试一律 `./venv310/bin/python -m pytest`;TDD 红绿双向留痕。
- commit 标签 subject 末尾:`[vps] [no-doc]`(T8 例外:含因子档更新则不打 [no-doc])。
- 共享工作树:`git add` 只加本任务 pathspec。
- 猴补丁纪律:被测对象模块级 import。
- 审计坐标行号可能有 P5 后位移——以符号名/grep 实际定位为准,不盲信行号。
---
### Task 1: llm 族——P2-1 Retry-After 真实现 + P2-2 L5 字符串感知
**Files:** Modify `sanguo_api/llm/client.py`、`sanguo_api/llm/json_utils.py`;Test `tests/api/test_llm_client.py`、`tests/api/test_llm_json_utils.py`
**Requirements(审计原文):**
- P2-1:`_post` 只返回 `(status_code, text)`,headers 全程丢弃;测试 fixture 给 Retry-After "2" 恰与固定退避 `_BACKOFF_SECONDS[0]=2.0` 重合,断言靠巧合通过。**修**:`_post` 带回 headers;429 时解析 Retry-After 秒数作 sleep 值(非数字/缺失回落固定退避);测试改用与固定退避不重合的值(如 Retry-After "7" → sleeps==[7.0])并修正失真测试名;解析失败回落路径也要有测试。
- P2-2:L5 尾逗号清洗 `_TRAILING_COMMA` 不感知字符串,实测 `{"s": "a, }",}` 被改写为 `{"s": "a }}"` 解析成功=字段值损坏当成功。**修**:L5 清洗改字符串感知状态机(复用 `_balanced_braces` 的 in_str/esc 扫描模式,只在字符串外替换 `,}`/`,]`);钉回归测试:`{"s": "a, }",}` 解析结果 `{"s": "a, }"}` 原值保全。L5 若因此救不回某些此前被误救的输入属正确行为(宁可 502 不可静默损坏)。
**Interfaces:** `_post(self, payload) -> tuple[int, str, httpx.Headers]`(三返回,调用方两处解包同步改);`_clean_trailing_commas(text: str) -> str` 模块级新函数(json_utils,供 robust_json_parse L5 与 _strict_retry 复用如需)。
- [ ] 写失败测试(Retry-After "7" 真等 7 秒/非数字回落/字符串内尾逗号保全)→ 红
- [ ] 实现(_post 三返回+429 分支解析;字符串感知清洗)→ 绿
- [ ] 全套件 `tests/api/test_llm_client.py tests/api/test_llm_json_utils.py tests/api/test_llm_probe.py` 绿
- [ ] Commit: `fix(llm): Retry-After 真实现+L5 清洗字符串感知——_post 带回 headers 429 解析秒数(非数字回落固定退避,测试改不重合值钉真等待)+尾逗号清洗改字符串感知状态机(字符串内 ", }" 原值保全,宁 502 不静默损坏) (审计 P2-1/P2-2) [vps] [no-doc]`
### Task 2: 状态机族——P2-5 条件 UPDATE + P2-6 decompose 按卡互斥
**Files:** Modify `sanguo_portfolio/pipeline_store.py`(update_hypothesis_state)、`sanguo_api/routes_pipeline.py`(decompose 端点);Test `tests/portfolio/test_pipeline_store.py`(若无则建)、`tests/api/test_routes_pipeline_hypotheses.py`
**Requirements(审计原文):**
- P2-5:`pipeline_store.py` update_hypothesis_state 的 UPDATE 不带 `AND state=?` 守卫、无事务包裹:并发下末写者胜,可产出「building 态带 death_reason」甚至把 graveyard 迁出——D3 禁复活在竞态下不成立。**修**:下沉为单条条件 UPDATE(`WHERE id=? AND state IN (允许源态)`;rowcount==0 时二次 SELECT 区分「卡片不存在」与「状态不符」分别 raise/返 None);死写(graveyard 终态迁出)被条件挡住。测试:模拟 graveyard 卡片调 update→ 被拒(状态不变)。
- P2-6:`routes_pipeline.py` decompose 端点「registry 落盘 + 卡片转 building」跨存储两步写不原子且无锁;并发 decompose 同卡整文件覆盖丢更新。**修**:仿 `_GRADUATE_LOCK` 加 `_DECOMPOSE_LOCK = threading.Lock()`(进程内互斥,decompose 全程 with);同卡二次并发请求串行化后由 P2-5 的状态条件+existing_names 查重自然兜住。测试:锁存在性不强测行为,钉「decompose 端点在锁内完成注册+转态」的既有行为回归即可(并发真测不做——线程池+async 端点组合测不划算,注释说明)。
**Interfaces:** `update_hypothesis_state(path, hyp_id, new_state, *, allowed_from: tuple[str, ...] | None = None, **cols) -> bool | None`(None=卡片不存在;False=状态不符被拒;True=成功——调用方按需消费;既有调用点不传 allowed_from 保持现行为)。
- [ ] 失败测试(graveyard 拒迁/条件 UPDATE rowcount 路径)→ 红 → 实现 → 绿
- [ ] decompose 加锁+模块级常量+注释
- [ ] `tests/api/test_routes_pipeline_hypotheses.py tests/portfolio/` 相关绿
- [ ] Commit: `fix(pipeline): 假设状态机条件 UPDATE+decompose 按卡互斥——update_hypothesis_state 带 allowed_from 守卫(单条条件 UPDATE+rowcount 二次分因,graveyard 终态竞态不可迁出)+decompose 全程 _DECOMPOSE_LOCK(仿 _GRADUATE_LOCK,跨存储两步写丢更新窗口闭合) (审计 P2-5/P2-6) [vps] [no-doc]`
### Task 3: IC 闸族——P2-9 计数告警 + P3-10/11/12 同文件顺带
**Files:** Modify `sanguo_factor/ic_gate.py`、`sanguo_factor/monthly_batch.py`;Test `tests/factor/`(ic_gate/monthly_batch 相应文件)
**Requirements(审计原文):**
- P2-9:新因子 parquet 缺件→整体 continue;report 无 `skipped_missing_values`/`compared_pairs` 字段,monthly_batch 仅 flagged 非空才打印——系统性 eval 失败呈现为绿灯。**修**:ic_gate report 增两计数字段;monthly_batch 接线处在 `checked < len(new_names)` 或比对数为 0 时 stderr 显式告警(不 exit 非零——告警属信号语义)。测试:构造一因子缺 parquet→report 计数如实+告警文案可捕获(capsys)。
- P3-10(顺带,同文件):有效 corr 日数无下限——常数截面日滤除后少数日均值即可触发置疑。**修**:加 `_MIN_CORR_DAYS = 20` 下限(不足→该因子标注 `insufficient_days` 不进置疑清单,report 留痕);测试钉边界(19 拒 20 过)。
- P3-11:origin=decomposer 新因子判定双向漂移——分解因子每月重考永不收敛;手工升版即无声退出闸视野。**修**:判定改以注册表条目级 `origin` 字段(vr.upsert_factor 时已可带;若现落在 version params 则 monthly_batch 判定改读条目级并兼容旧数据)——先读代码核实字段实际位置再落;测试:分解因子连续两月不再重考/手工升版仍在闸视野。
- P3-12:plan_batch「纯函数」docstring 契约被 resolver 全局注册副作用打破。**修**:docstring 勘正(明示 resolver=resolve_with_yaml 时有进程内注册副作用、跨调用幂等);不改行为。
- [ ] 失败测试(缺件计数/告警/日数下限/origin 判定)→ 红 → 实现 → 绿
- [ ] `tests/factor/` 全目录绿(真注册表守门测试在列,必须全套跑)
- [ ] Commit: `fix(factor): IC 正交闸可观测性+判定收敛——report 增 skipped_missing_values/compared_pairs 计数+checked<len stderr 告警(系统性 eval 失败不再呈现绿灯)+有效 corr 日数下限 20(常数日滤除后少数日不触发置疑,insufficient_days 留痕)+origin 判定改条目级(分解因子不再每月重考,手工升版不退闸视野)+plan_batch docstring 勘正副作用 (审计 P2-9/P3-10/11/12) [vps] [no-doc]`
### Task 4: version_registry——P2-10 死亡回写留痕+绝对化预检
**Files:** Modify `sanguo_factor/version_registry.py`;Test `tests/factor/`(version_registry 相应)
**Requirements(审计原文):**
- P2-10:D5 死亡回写「卡片不存在」路径零痕迹返回 False,调用点忽略返回值;`--pipeline-db` 缺省 cwd 相对+_connect 自动 CREATE 空库——错误目录跑 CLI 时因子落 graveyard 而卡片回写静默 no-op 还顺手生成空库。**修**:①False 路径显式 stderr+registry_events.jsonl 附记一行(`death_writeback_missed` 类事件,含卡片 id+因子名);②`--pipeline-db` 解析后 os.path.isabs 预检:相对路径→stderr 警告并 os.path.abspath 绝对化(不拒跑,但落库位置明确);③不存在时不再静默建空库回写——预检 os.path.exists 不存在→stderr 警告「卡片库不存在,回写跳过」并跳过回写(不创建)。测试:三条各钉一例(capsys+events 文件断言)。
- [ ] 失败测试 → 红 → 实现 → 绿 → Commit: `fix(factor): 死亡回写零痕迹路径留痕+pipeline-db 绝对化——卡片不存在 False 路径 stderr+registry_events 附记 death_writeback_missed+相对路径 abs 化警告+库不存在跳过回写不再静默建空库 (审计 P2-10) [vps] [no-doc]`
### Task 5: manifest 四态——P2-11/12 fail-soft 同刀+状态清单测试化
**Files:** Modify `sanguo_factor/monthly_batch.py`(缺口比对段)、`sanguo_factor/data_gap_check.py`(对齐 CLI 语义);Test `tests/factor/test_monthly_batch.py`、`tests/factor/`(data_gap 相应)
**Requirements(审计原文):**
- P2-11:缺 manifest → gaps={} 完全安静(不写 data_gaps.json、不触发 gap_issue)——数据侧最需告警的场景零输出;独立 CLI 同情形 exit 1,两处契约相反。**修**:缺 manifest 落 `{"manifest_missing": true}` 标记件(data_gaps.json 照写);月度批内该标记不阻批(stderr 告警);文档化 env 绝对路径建议(docstring 一行)。
- P2-12:manifest 存在但语法坏(yaml.YAMLError)/形状坏(标量列表→AttributeError)两态无捕获批前崩溃。**修**:缺口比对整体 try/except(OSError/YAMLError/AttributeError→stderr 告警+gaps 按缺失处理照跑批);坏 YAML/非 dict 两态测试。
- 状态清单测试化:参数化测试枚举四态(缺路径/坏语法/坏形状/正常)各自契约(标记件/告警/gaps 空或实)——防「只修一态」再犯(本窗口三度只修一态的教训)。
- [ ] 四态参数化失败测试 → 红 → 实现 → 绿
- [ ] Commit: `fix(factor): manifest fail-soft 四态同刀——缺路径落 manifest_missing 标记件(最需告警场景不再零输出)+坏语法/坏形状整体 try/except 照跑批+与 CLI 语义对齐说明+四态参数化测试钉契约(防只修一态再犯) (审计 P2-11/P2-12) [vps] [no-doc]`
### Task 6: 缺口单去重——P2-13 三重加固 + P3-20 地址副本收敛
**Files:** Modify `sanguo_factor/data_gap_check.py`;Test `tests/factor/`(data_gap 相应)
**Requirements(审计原文):**
- P2-13:①open issue 首页 50 条上限——超后目标单跌出首页 find_open_issue 静默失明同指纹月月重复开单。**修**:search API 兜底(GET /repos/{owner}/{repo}/issues?state=open&type=issues 按标题关键词/指纹 sha1 前 8 位搜,复用现有 token 通道;首页找不到再 search,search 也空才确认不存在)。②LIST 网络异常 `except Exception: return None` 后照走 open=fail-open。**修**:异常与「确认不存在」区分——异常时跳过 open 本月(stderr 留痕,下月再试)。③**修**:按指纹落本地 append-only 台账(`data/gap_issue_ledger.jsonl`:指纹→issue号/日期)作第二道去重——开单前查台账,命中即跳过并 stderr。
- P3-20(顺带同文件):内网 Gitea 地址 fallback 硬编码第二份副本(与 version_registry 同值)。**修**:import version_registry 的常量(或提为共享常量)消副本;测试钉两处同步。
- [ ] 失败测试(search 兜底路径 mock/异常跳过/台账去重/地址单源)→ 红 → 实现 → 绿
- [ ] Commit: `fix(factor): 缺口单去重三重加固——find_open_issue 加 search API 兜底(首页 50 条上限外不再失明)+LIST 异常与不存在区分(异常跳过开单下月再试,不再 fail-open)+指纹本地 append-only 台账第二道去重+内网地址副本收敛单源 (审计 P2-13/P3-20) [vps] [no-doc]`
### Task 7: factor_guard——P3-9 小因子换皮漏检 + P3-13 NAME_RE 尾换行
**Files:** Modify `sanguo_factor/factor_guard.py`;Test `tests/factor/test_factor_guard.py`
**Requirements(审计原文):**
- P3-9(在 P2-9=Task 3 之后,顺序已满足):整树 <8 节点的交换律重排同时躲过 dup_exact(严格序键)与 dup_subtree(<8 节点不触发)——小因子换皮漏检。**修**:dup_violations 增一级「swap=True 整树同构」比对(`_key(tree.body, swap=True)` 与在库比对相等且严格序键不等→报 dup_subtree「交换律重排换皮」)——语义=交换律重排本就是换皮,归 dup_subtree 不动 dup_exact 的「逐字重发」语义。测试:`close+open`(已过门)vs `open+close`(新候选)→ dup_subtree 拒;不同结构小因子不误伤。
- P3-13(顺带):NAME_RE `$` 允许尾换行(`"abc\n"` 过闸)。**修**:`re.compile(r"^[a-z][a-z0-9_]{2,39}\Z")`(\Z 不吃尾换行);测试钉 `"abc\n"` 拒。
- [ ] 失败测试 → 红 → 实现 → 绿 → `tests/factor/` 全目录绿(真注册表+decompose 套件回归)
- [ ] Commit: `fix(factor): 小因子交换律换皮漏检+NAME_RE 尾换行——dup_violations 增 swap=True 整树同构级(<8 节点小因子 close+open 重排不再躲过双检,归 dup_subtree 不动逐字重发语义)+NAME_RE 改 \Z(abc\n 不过闸) (审计 P3-9/P3-13) [vps] [no-doc]`
### Task 8: 杂项收敛——P3-15/16/17/18/19
**Files:** Modify `sanguo_factor/monthly_review.py`、`sanguo_factor/exam_gate.py`、`config/factor_registry.yaml`、`sanguo_factor/composite_library.py`;Test `tests/factor/`(相应);Doc `docs/factor_research/factor-system-design.md` §7.7 台账一行
**Requirements(审计原文):**
- P3-15:`_PROMOTION_BAR=2.0` 注释宣称「单一事实来源」,pre_gate_table 实为另一处硬编码 2.0。**修**:第二处改引 `_PROMOTION_BAR` 常量;负 t 侧镜像门边带仅文档注释说明(负向晋级不存在,维持现状)。
- P3-16:exam_gate 缺周期跳过使判定可退化为单周期对比即 SUGGEST_PROMOTE。**修**:周期覆盖不足(三周期缺任一)→ 判定降级为 `INSUFFICIENT_PERIODS`(不建议晋级,报告明示缺哪期);fail-closed。
- P3-17:同日重跑同候选覆盖既有 md 报告(label 不含 run_id)。**修**:报告文件名含时间戳(`{label}_{HHMMSS}` 或日期+序号);同日重跑不再覆盖。
- P3-18:promotion_t=26.32 条目本体无 evidence_run 指针。**修**:config/factor_registry.yaml 的 composite_quant12_v2a 条目补 `evidence_run: ev_20260911_xxxx`(核实真实 run id 后填——用 NAS eval 库 latest 或 docs 已记录值;若无法核实则补 `evidence_note` 指向因子档 §7.6 记录)。
- P3-19:等权合成交集语义(任一源缺→该股当日 null,分母恒 12 不重归一)易误读。**修**:composite_library 对应函数 docstring 明示交集语义+已知限制。
- 档随码走:因子档 §7.7 追加一行台账(P3-16 exam_gate fail-closed 语义+P3-18 evidence 指针惯例)——本 commit 不打 [no-doc]。
- [ ] 各项失败测试/核实 → 红绿
- [ ] `tests/factor/` 全绿
- [ ] Commit: `fix(factor): 判定与档案五项收敛——_PROMOTION_BAR 单源化(第二处硬编码改引常量)+exam_gate 缺周期 fail-closed(单周期对比不再 SUGGEST_PROMOTE,INSUFFICIENT_PERIODS 明示)+报告文件名带时间戳(同日重跑不覆盖)+v2a 条目补 evidence 指针+等权交集语义 docstring 明示;因子档 §7.7 台账 (审计 P3-15/16/17/18/19) [vps]`
---
## 收尾(主 session)
- 全量回归 `tests/api/ tests/portfolio/ tests/factor/` 绿+前端不动无需 build。
- push 前边界核查;push 后守望 CI。
- 台账+memory;向 infra 回执(含 P3-1..8 LLM/pipeline 簇未派件提示——我域文件,下一波可接)。