# 评审记录(2026-09-17):选股计划排序轴换质地,提交 4d7ca55 > 评审对象:选股系统仓库 `main` 上的提交 4d7ca55(开发者 2026-09-17 上午交付,交付时未推送)。基线 6138c01。 > 评审依据:`docs/选股计划调整方案_2026-09-17.md` 第十节"评审方式"。评审者只看证据。 > 写法遵守 `.claude/output-styles/readable-chinese.md`。本文件是四个仓库逐字同步的副本之一。 --- ## 一、结论 **有条件通过。** 代码主体与规格书一致,五个测试文件本地全绿,四个开关设旧值时逐字回旧成立,分数编码与三门槛一确认三风险都没有动。三处必改,合成一个修复提交,09-21 拉代码之前做完并复审。两条建议级不阻塞。一条拍板项。 三处必改各一句话。 1. 装配路与快照路对"已启动成员"的主题限额判定不一致。同一天的计划,早上落盘的主榜前二十与接口读快照给出的前二十可能不是同一批。 2. 计划里的 `encoding` 说明句在旧轴下也写"名次按质地带优先",与旧轴的实际次序相反。 3. 交接文档一段没写,规格书工作包五第三条没有完成。 修复小包的规格写在《上线与协同阶段工作方案与规格书_2026-09-17》工作包甲。本记录只写核对结果与理由。 --- ## 二、我亲手跑出的证据 **本地单测(开发机,选股系统仓库)。** 五个文件末行全部 ALL OK:`test_plan_rank_axis.py`、`test_plan_quality_rank.py`、`test_card.py`、`test_pool_logic.py`、`test_plan_snapshot.py`。四个开关设旧值再跑 `test_plan_rank_axis.py`,仍 ALL OK。七个改动的模块 `py_compile` 通过。开发者说只能在部署机跑的五个测试文件,我本机同样跑不起来,原因与他说的一致:缺 `psycopg` 或 `fastapi`,或本机 Python 3.9 不支持联合类型注解。这是既有状况,不是本次引入。 一条要说明:`test_plan_snapshot.py` 里"拿真实快照逐行比对"那一例在本机是**跳过**的,因为本机没有快照文件。它只有在部署机上跑才真正生效。必改一正是它会查出的那件事。 **旧路径等价(开发机)。** 入池的 `tier` 与 `candidate` 两个分支从内联代码抽成了纯函数 `select_plan_rows`。我用随机合成的主榜行做了三百轮对照,每轮六种组合(两个来源乘三种档位白名单),新函数与旧内联逻辑的输出零差异。 **部署机只读读数(选股系统机器,09-16 快照)。** 仓库停在 3d3f317,与规格书基线一致。`.env` 里有 `POOL_TOP=50`、`POOL_MAX=100`、`CARD_REQUIRE_STARTED=0`,没有 `POOL_SOURCE`、`PLAN_RANK_AXIS`、`POOL_THEME_CAP`,所以拉代码后默认值直接生效,与规格书第五节说的一致。 09-16 快照主榜全量五百三十二行。质地分布:好十六、中四十八、差一百三十四、无评析三百三十四。我按规格书的词典序在这份快照上模拟了一遍质地序,前二十里好十六、中四。前二十里有六行没有传导主题,正好落在"第六只起被限额顶掉"的位置,规划代理那条发现在真实数据上成立。前二十里"储能"主题五只,正好碰到限额边界。 "已启动成员"的行(证据由已动成员视图补上的行)全量八行,旧序名次八十二、三百五十三、三百九十、四百九十一、五百、五百二十、五百二十六、五百三十。旧序前二十里没有它们;模拟质地序前二十里有一行。这就是必改一在真实数据上的样子。 --- ## 三、逐项核对 按规格书第十节的核对步骤逐条记。 1. **diff 对照附录己的文件清单。** 十五个文件改动,全在清单内。`factors.py` 与 `api.py` 没有出现在 `git show --stat` 里。清单里的"交接文档"没有改,见必改三。 2. **本地跑测试文件。** 见第二节。 3. **四个开关设旧值时的逐字回旧断言是不是真断言。** 是,但覆盖方式要说清。第一例只断言 `_rerank` 在旧轴下与 09-14 的 `_rerank_by_quality` 序列与分值逐字相同,它验的是分派。另外三个开关各有自己的旧路径断言:入池旧路径由我的三百轮对照补上,评析名单的 `score` 分支是第七例,卡内序关掉是第八例,主题限额例外关掉是第九例。四条合起来才是"四开关逐字回旧"。 4. **主题限额的例外只在质地轴下生效。** 是。装配时按 `config.PLAN_RANK_AXIS` 传入,快照路按行上的 `rank_axis` 推断。回退那天读旧快照仍按质地轴截取,这是对的,因为那份快照的序就是质地序。但两条路对"已启动成员"的判定不一致,见必改一。 5. **卡内序、入池、评析名单三处的次级键与去重。** 卡内序在判决与涨幅之间插入负的质地带,关掉时元组与旧实现逐字相同。入池 `rank` 源只取被指向的行、按新序截前五十,备用开关并进旧序强传导前五十、去重、受 `POOL_MAX` 兜底。评析名单两路并集去重后按新序名次排,`score` 分支按原始分。三处都有确定的末位键。 6. **`_row` 只加键。** 是。加了 `quality_band`、`rank_axis`、`pointed` 三键,质地轴下再加 `rank_old`。`score`、`score_adj`、`tier` 的值与含义没有变。顶层 `encoding` 的值变了,见必改二。 7. **分数编码。** `factors.py` 一字未动。组内分从原始分反解,档位用整除,与编码一致,我逐个区间验过。 8. **旧序名次。** `rank_old` 取自全量序,不是截断后的序。主榜与观察档两个序列的代码不重叠,两张名次表合并不会互相覆盖。 9. **一百二十天守卫。** 守卫是活的:评析摘要里的 `age_days` 由数据日减报告运行日算出,取值真实。守卫对质地带、质地门、卡内序三处生效。它**不对**旧轴的加减八分生效,`score_adj` 也不受守卫。这与规格书第五节表里那一行的写法不一致,但代码是对的,因为规格书交付要求第四条要求 `score_adj` 的值不变。规格书这一行我已改正,见第五节建议三。 10. **入池不变式的验证命令是否成立。** 成立。入池走 `plan.collect` 要五百行的宽池、主题限额取 `POOL_THEME_CAP`(默认零),再交给纯函数选行;规格书第六节那条命令拿快照全量行喂同一个纯函数,两边看到的是同一个序。 11. **评析目标名单拿到的是全量行。** 是。`persist` 收到的是落盘快照,`main` 是全量新序行,每行带原始分与新序名次。 12. **周报的底本。** 周报不读快照,它用 `plan.collect` 把当日重新装配一遍再取全量行。两份对照名单都从这份重算取。这是既有做法,不是本次引入,但它决定了四周复核读到的是什么,见第六节拍板项。 --- ## 四、必改 ### 必改一:装配路与快照路对"已启动成员"的主题限额判定不一致 **是什么。** 主榜截取有两条路。装配路是 `collect` 里的内部函数 `_pick`,早上出计划时用它截主榜前二十,写进 md 与快照的 `main_shown`。快照路是模块级函数 `pick_rows`,接口读快照时用它按请求参数截取,持仓管理系统要的三百只前缀从这里来。两条路本该同一套规则,`test_plan_snapshot.py` 拿真实快照逐行比对钉住这件事。 这次的例外判定在两条路上写法不同。装配路看的是传导视图有没有这只票的证据行:没有证据行就当无主题、免限额。快照路看的是行上的 `evidence.theme`:没有主题才免限额。差别出在"已启动成员"上。已启动成员在传导视图里没有证据行,但装配时 `_row` 会用已动成员视图的目标环节把 `evidence.theme` 补上(`plan.py` 约 550 行)。于是同一只已启动成员,装配路把它当无主题免限额、不计数,快照路把它算进所在主题、受限额。 **为什么以前没炸。** 旧序下这些行排在八十名以后,碰不到前二十的限额。旧代码里两条路对它们其实也不一致(装配路把它们归进"无传导"桶计数,快照路归进所在主题),只是不影响前二十。质地序把被指向的已启动成员提到了前列,09-16 快照模拟出来前二十里有一行,"储能"又正好五只碰到边界,差一只就会让两条路给出不同的第二十名。 **后果。** 同一天,md 与快照里的 `main_shown` 是一份前二十,接口给持仓管理系统的是另一份。规格书第二节"同日重跑结果一致"这条原则在两条路之间失守。部署机上跑 `test_plan_snapshot.py` 会在第一个出事的交易日变红。 **修法。** 不在两处各打补丁,改成一处实现两处调用:把"按主题限额截取"抽成模块级纯函数,装配路与快照路都调它,主题的取法与 `_row` 写 `evidence` 的取法同源(含已动成员视图的补充)。这样装配路对已启动成员的判定也变成"算进所在主题",与快照路一致。这一步同时改正了旧轴下的既有分歧,改的只是 md 与 `main_shown` 这条路对已启动成员的计数,接口路(持仓管理系统读的那条)原样不动。逐字回旧那一例单测不受影响。细节与单测在工作方案附录甲。 ### 必改二:`encoding` 句在旧轴下写错了 `collect` 返回的 `encoding` 一句无条件追加了"名次按质地带优先,传导只在带内作次级键(开关 PLAN_RANK_AXIS)"。旧轴下名次不按质地带优先,这句话与实际相反。持仓管理系统把这句原样收进它的状态页给人看。修法是只在质地轴下追加,或把句子改成条件式写法。一行改动。 ### 必改三:交接文档一段没写 规格书工作包五第三条要求交接文档一段并四仓库同步,提交里没有。修法是在最新一份交接文档 `docs/交接_2026-09-10.md` 末尾加一节带日期的补记,把台账那一行从"最新到第 057 条"改成 063,四仓库同步。交接段的骨架在工作方案附录甲。 --- ## 五、建议级(不阻塞) 1. **评析名单 `score` 分支不是逐字回旧。** 旧代码取快照序前一百,快照序是 09-14 的调整分序;新代码按原始分排前一百。差别只在第一百名附近被加减八分挪动的几只。评析名单只管覆盖,这点差别没有后果。不改,记在这里。 2. **两处小瑕疵。** `_rerank_by_axis` 的 `axis` 参数没有用到;`card.sort_key` 在函数体内导入 `config`。都不影响行为。 3. **规格书两处文字改正。** 第五节表里 `COMPANY_REVIEW_STALE_DAYS` 那一行说守卫对加减分也生效,工作包二第二条说"也不再加减分",与代码和交付要求第四条矛盾,按代码改正。附录乙说周报"从快照 `_full` 读主榜行",实际是现算后取 `_full`,按实际改正。这两处我已经改在规格书里,随本记录同步。 4. **周报里"主榜新序前二十"对改动前的日子。** 09-25 周报的窗口含 09-18 与 09-21 两个旧序日,那两天这一行的名单其实就是旧序前二十,标签会略有误导。旧序名单那一行对这两天正确地跳过了。周报一段注记即可,不改代码。 --- ## 六、拍板项:周报的底本 周报用 `plan.collect` 把每个计划日重新装配一遍,而不是读当天落盘的快照。重新装配用的是跑周报那一刻的评析报告:查询取每只票最新一次运行,不按日期截断。于是 09-22 的"新序前二十"在 09-25 算出来时,用的是 09-25 手里的质地档。一家 09-24 才有报告的公司,会被回头算进 09-22 的前列。 这在质地是次级项的时候是小事,在质地成为主轴之后不是。四周复核比的是"新序前二十与旧序前二十谁更好",两份名单都应该是当天真正出过的那份。快照落盘时的注释写着"它是复盘与对账的唯一底本",周报却没有读它。 我的建议是周报改成先读当日快照的全量行,没有快照的日子才回落到重新装配,加一个开关默认读快照。改动小,但它改变周报所有既有名单的取数方式,从 09-02 有快照那天起的读数会与旧周报的口径不同,所以要用户拍板。写在工作方案工作包丙与拍板点一。2026-09-17 用户拍板:做,随修复小包同一个提交。 --- ## 七、答开发者的两个问题 **要不要现在推送。** 复审已经做完,不必等 09-19。提交 4d7ca55 我随本记录一起推到远端,修复小包在它上面再提一个。选股系统机器只在 09-21 收盘后由人拉代码,中间远端有一个待修的提交没有风险。 **要不要现在补交接段。** 要,放进修复小包,与必改一、必改二同一个提交。 --- ## 八、附:文件清单对照 提交改了十五个文件:`plan.py`、`card.py`、`pool.py`、`review_targets.py`、`plan_review.py`、`plan_reconcile.py`、`config.py`、`.env.example`、`README.md`、`docs/复盘决定台账.md`、`docs/选股计划入池_对接说明.md`、`docs/选股说明_下游对接.md`、新 `test_plan_rank_axis.py`、`test_plan_quality_rank.py`、`test_plan_snapshot.py`。附录己清单里只差交接文档。清单外没有文件被动。 --- ## 九、修复小包复审补记(2026-09-17 晚,评审者):通过 **对象。** 提交 e7cf0a9(选股系统 `main`),工作包甲三处必改与工作包丙合成一个提交。开发者交付时未推送,复审通过后由我推送。 **证据。** 本地七个测试文件末行全部 ALL OK:`test_plan_rank_axis.py`(十二例)、`test_plan_snapshot.py`、`test_plan_quality_rank.py`、新 `test_plan_review_source.py`、`test_card.py`、`test_pool_logic.py`、`test_plan_review_heads.py`。四个开关设旧值再跑 `test_plan_rank_axis.py` 仍 ALL OK;`PLAN_REVIEW_SOURCE=live` 再跑 `test_plan_review_source.py` 仍 ALL OK;三个改动模块 `py_compile` 通过。 **逐项核对(按工作方案第十节)。** 1. 截取规则只剩一处实现。模块级 `take_by_theme` 是唯一的循环,`collect` 里的 `_pick` 与模块级 `pick_rows` 都只剩一行调用。 2. 两条路的主题取法同源。装配路的 `_theme_of` 与 `_row` 写 `evidence` 的三行逐字对得上:传导视图有证据行取它,否则已启动成员用已动成员视图补的主题,否则空。快照路取行上的 `evidence.theme`,空串与缺失都当无主题,与装配路一致。 3. 旧轴下无主题行仍归"无传导"桶计数。`uncapped_no_theme` 为假时桶名取"(无传导)",第十二例钉住六条只放五条。 4. `encoding` 句两轴都对,改成按开关分别写清。 5. 交接段已写进 `docs/交接_2026-09-10.md` 第十一节,台账行改成 063。提交信息说"四仓库同步",但开发者只在选股系统仓库提交,另外三份副本由我随本补记同步。 6. 工作包丙:无快照日回落到现算并注记"底本:现算",开关设 live 时有快照也现算,单测三条都钉住。`run` 从计划字典里只取 `_full`、`candidates`、`watch` 三个键,快照整理出的字典正好齐这三个,没有漏掉的键。 **建议级,不阻塞。** 一、`_row` 里那三行可以改成直接调 `_theme_of`,让取法只剩一份文字,眼下是两份逐字相同的文字。二、第十一例在测试里抄了一遍 `_theme_of` 的逻辑而不是调真函数,`_theme_of` 若改成模块级纯函数,测试就能调真的。都留到下次顺手改。 **下一步。** e7cf0a9 已推送,09-21 收盘后按计划拉。开发者转做工作包乙,规格在工作方案附录乙,09-19 交付。09-22 早上按工作方案第六节核"两条路同一份前二十"。