Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 14 additions & 5 deletions docs/factors/d1_panel_freeze_manifest.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,15 +19,24 @@
的结构性预防。具体操作:checkout `3669c90`,把本分支的 `qt/panel_freeze.py` 原样放入
(它只 import、不改任何因子数学模块),再跑下方命令。**任何在 D2 之后的树上直接重跑
本工具得到的"基线"都不是基线**,与冻结哈希不一致时以本文记录的哈希为准。
- **重跑命令**(cwd = 仓库根,缓存根就位):
- ⚠️ **重生成能力已于 D5 C6 退役(owner 2026-07-28 裁定),本基线 frozen-forever。**
重生成路径调的是 11 个旧 eval runner 的私有 `_load_*_panel`,C6 删除它们后这条路必然
失效;与其留一条跑起来就会坏的代码,不如显式退役。`python -m qt.panel_freeze`
(不带 `--verify`)现在是**可读的报错**,不是静默 no-op。**从当前树重跑本来就不合法**
(见上一条 provenance 规则),所以失去的不是复核能力。

- **现在能跑的是验证**(cwd = 仓库根):

```
/home/shaofl/Development/env_tools/envs/quant_mf/bin/python -m qt.panel_freeze
/home/shaofl/Development/env_tools/envs/quant_mf/bin/python -m qt.panel_freeze --verify
```

输出根默认 `artifacts/refactor_baseline`(`--output-root` 可改;`--resume` 语义见
模块 docstring——已存在的面板从冻结文件读回并**重新走 process + 对账**后才被接受,
绝不盲信旧文件)。
它对**两棵**冻结树(本基线 14 个面板 + `pr_c_cutoff_fix` 1 个)逐面板重算 canonical
content hash 与整条 manifest 行(rows / 日期范围 / symbol 数 / NaN 数 / mean / std /
file sha256),与**本文 §六表格**(git 里的权威)以及 `manifest.json`(gitignored 的第二
见证)双向核对,任一不符即非零退出。多出一个未登记的面板文件同样判负。
**本文的表格因此不只是记录,而是被程序读取的期望值**——改它就等于改验证结果,会出现在
`git diff` 里。

## 二、数据面(与十一因子评估循环同面)

Expand Down
51 changes: 51 additions & 0 deletions docs/factors/d5_property_test_migration_map.md
Original file line number Diff line number Diff line change
Expand Up @@ -168,3 +168,54 @@ eval_peak_ridge_amount_ratio tests/` 无命中)。新套件在此是**净增**
| 8 | `tests/test_factor_store_universe.py::test_min_cross_section_gate_still_bites_on_the_read_path` | ✅ |
| 9 | `tests/test_exec_basis_eval.py::test_exec_basis_cli_line_survives_an_absent_metric` | ✅ |
| 10 | `tests/test_factor_requires_spec_v1.py::test_shipped_factor_declarations_match_the_d0_table` | ✅ |

## 十、C6 准入核销(2026-07-30,改造 PR 内实跑)

R16 的门是"删旧 runner 测试文件之前,59 条逐条有映射"。本节是**在树上跑出来的**核销结果,
不是对上面表格的复述。审计脚本 `tmp/context/cc_c6_verify_only/r16_audit.py`(gitignored,
一次性);**耐久的守卫是 `tests/test_property_test_migration_map.py`(7 条)**,它把同样三个
问题变成常跑测试。

| 问题 | 方法 | 结果 |
|---|---|---|
| 旧套件真的是 10 文件 / 59 条吗 | 逐文件 `grep "^def test"` | **10 / 59** ✅(4+4+4+4+4+10+14+4+7+4,与 §零 口径一致) |
| 59 条**逐条**在本表有行吗 | 26 个 distinct 旧测试名 vs 表格第一列,再按文件展开 | **59 / 59** ✅,0 条无映射 |
| 本表引用的新套件 node-ID 今天还在吗 | 解析全文引用 → 与 `pytest --collect-only` 比对(见下「口径与出处」) | **56 / 56 全部命中** ✅ |
| 家族通配(`…_*(N 条)` 形式)的 N 属实吗 | 按前缀数实际测试数 | **3 / 3 家族数目吻合** ✅(ridge_return 3、valley_ridge 2、neutralization 2) |
| 引用是否唯一解析 | 省略文件名的裸引用按**名字**在全套件查找 | **全部唯一** ✅(无重名歧义——本仓 `tests/` 无 `__init__.py`,重名会静默丢一条) |

**结论:R16 覆盖数非降的前置条件成立,删除 PR 可以开始。**

### 口径与出处(评审 LOW-1 / NIT-1 的更正)

- **本节数字实跑于本 PR 的最后一个代码 commit 之后的工作树**,`pytest --collect-only` 收集到
**2,701** 个 node-ID。**初稿写的 2,687 在任何一个 commit 上都不成立**:那次审计跑在**它自己
那一批 7 条守卫测试落地之前**(`191b443` 上实为 2,694),也就是**审计跑在自己的守卫存在之前**。
更正连同原因一起记在这里,而不是把数字悄悄换掉。
- ⚠️ **收集总数是背景,不是主张**——它随任何新增测试变动(本 PR 期间就走过
2,694 → 2,697 → 2,701)。**承重的三个数是 59/59、56/56、0 未解析**,它们与总数无关。
引数字时**必须连同它是在哪棵树上量的一起引**(这与本仓「报极值必须连它的窗口一起报」同形)。
- **「56」的口径**:*去重后、家族通配展开后的 distinct 新套件 node-ID*——同一个 node-ID 在表中
被引多次只计一次,`::name_*(N 条)` 展开为它的 N 个成员。评审用另一套口径数到 **58**;
**两者都是 0 条未解析**,所以差的是计数约定、不是覆盖。此处把本表的口径写死,使这个数可复算。

**两条如实记录(都不改变结论)**:

- 审计脚本第一版把 `pytest --collect-only -q` 传成了 `-qq`(`pyproject.toml` 已含
`addopts="-q"`,见 handoff 陷阱 2),node-ID 列表被吞掉,于是报出一大批"引用了但没收集到"。
**是"与预期矛盾的观察"暴露的,不是被任何测试抓住的。**
- 第二版用 `(?!\*)` 排除家族通配,被**回溯**绕过:正则把名字末尾的下划线让出来,改匹配一个更短
但同样错误的名字,于是三个家族 stem 仍被当成缺失的测试。改成 `(?![A-Za-z0-9_*])` 才成立。
守卫里保留了这条注释,因为下一个人写同样的正则会踩同样的坑。

**守卫的射程(写进它自己的 docstring)**:它检查引用**能否解析**,不检查被引用的测试是否
仍然断言本表声称的那条性质。**改名会被抓住,就地掏空不会。**

**`test_no_legacy_runner_test_is_missing_from_the_map` 在旧文件被删后会变成空过**——这是设计
如此:它的职责是"只要旧文件还在,就不许存在没被映射的旧测试",而不是让旧文件活下去;一个会
因为删除而变红的守卫,对本表要授权的那个 PR 就是个陷阱。

**守卫立刻咬到了本节自己**:§十初稿在表格里用一个虚构的双冒号引用当占位符举例,守卫把它当成
真引用、判定"引用了但不存在"。占位符已改成不含双冒号的描述。**这不是误报**——本表的引用格式
就是双冒号加测试名,任何写成那个形状的东西都应该指向一个真实测试。写这一段本身又踩了一次
(复述时把占位符原样抄了回来),第二次才改对。
128 changes: 123 additions & 5 deletions docs/factors/d5_runner_difference_catalogue.md
Original file line number Diff line number Diff line change
Expand Up @@ -231,12 +231,12 @@ aligned_base = sign * gross - base_cost
(`tests/__init__.py` 收集陷阱)同一接口。**非测试模块共四个**(实测
`grep -rln "qt\.eval_" --include=*.py | grep -v ^./tests/`):

| 模块 | 用途 | C6 处置 |
| 模块 | 用途 | C6 处置(**已执行**,改造 PR,2026-07-30) |
|---|---|---|
| `qt/cli.py` | 11 个 `_cmd_run_eval_*` 子命令(§五 C13) | 随统一 runner 收敛为一个子命令 |
| **`qt/panel_freeze.py`** | **生产 D1 冻结基线的工具**——调各 runner 私有 `_load_*_panel` + `_build_book_factors`,「零公式重抄」正是靠 import 它们实现的 | **删 runner 会打断「重新生成 / 重新验证 D1 基线」的能力** |
| **`qt/panel_reconcile.py`** | D2 逐格对账工具(同上依赖) | 同上 |
| **`qt/hand_anchors_engine_values.py`** | D2 手算锚的引擎侧取值 | 同上 |
| `qt/cli.py` | 11 个 `_cmd_run_eval_*` 子命令(§五 C13) | ✅ 收敛为 `run-factor-eval` 一个子命令;11 个旧命令名保留**迁移提示**(argparse 的 `invalid choice` 只会让人以为自己打错了) |
| **`qt/panel_freeze.py`** | **生产 D1 冻结基线的工具**——调各 runner 私有 `_load_*_panel` + `_build_book_factors`,「零公式重抄」正是靠 import 它们实现的 | ✅ **改 verify-only**:重生成 loud raise,`--verify` 逐面板重算 canonical hash + 整条 manifest 行,对着 **git 里的**哈希表核 |
| **`qt/panel_reconcile.py`** | D2 逐格对账工具(同上依赖) | ✅ **改 verify-only**:重建退役,**比较仍然真跑**(它本来就不需要 runner)——两侧各自从盘上独立读回、逐格重导出 D2 结论 |
| **`qt/hand_anchors_engine_values.py`** | D2 手算锚的引擎侧取值 | ✅ **改 verify-only**:重算退役,`--verify` 用记录自身的 hand/engine 数重导出 rel_diff / ok / all_ok_daily |

⚠️ **后三个全是 D1/D2 的验收工具**,而 **D5 面板腿的比较对象正是 `panel_freeze.py` 的产物**。
按设计 v3.2 §五第 4 腿的 **provenance 规则**,基线只许从钉住的 pre-D2 SHA 重新生成——
Expand All @@ -251,6 +251,17 @@ import 的 runner loader,即便统一 runner 已上线)、**要么显式记
**必须同批更新**——否则就是本项目 #76/#78/#82 那条形态的又一次复发(文档教人跑一条已经
跑不了的命令)。

**已落实(2026-07-30 改造 PR)**:走的是第二条路——owner 2026-07-28 裁定**退役重生成能力**,
**D1/D2 基线自此不可从本仓再生**,只能依赖已冻结的 artifact + 哈希。三个工具都不删,改成
只读验证;重生成入口是 **loud raise 而不是静默 no-op**(红线 #9)。要求 ② 同批完成:
`d1_panel_freeze_manifest.md` §一 与 `pr_c_cutoff_fix_reference_panel.md` §四 的「重跑命令」
已改为退役声明 + 验证命令。

**这条不对称损失的账,最后是这样结的**:担心的是「工具已被删、想复核也无从下手」。现在复核
能力**变强了而不是变弱**——旧的重生成路径从来不能用于复核(provenance 规则禁止从当前树重跑,
从被验证的树重建基线正是本仓犯过一次的空对账),而新的 `--verify` 是一条以前根本不存在的路:
15 个面板逐一重算 canonical hash 与整条 manifest 行,对着 **git 里的**期望值核,3 秒跑完。

## 七、与 C5 对账的接口

**本表 §二/§三 = 允许出现的差异白名单;§四 = 必须逐值一致的项;§五 = 已判定归一(对账中
Expand Down Expand Up @@ -849,3 +860,110 @@ description 相应改名(`REUSED from ...` → `SHARED taxonomy in ...`);
`classify_panel_differences` / `classify_anchor_row` / `run_panels_mode` / `run_anchors_mode`
/ `load_anchor_rows` / `frozen_panel_path` / `_build_bundle` **一个字节未变** ⇒ panels 与
anchors 的结论不受影响,**只重跑 reports 腿**。

## 六之三、C6 改造期登记的三件事(lead 2026-07-30 裁定,本节只登记不修)

### (1) D2 手算锚的 engine 比较记录已被冲掉——**丢的是可再验证性,不是结论**

`artifacts/refactor_baseline/hand_anchors_d2.json` 现在只有 20 条 `daily_pending_engine`、
**0 条 `daily_engine_compared`**:2026-07-25 18:04 一次 `python -m qt.hand_anchor_rows` 重跑
(PR #103 的 jump 截断修正配套)把 engine 侧的比较结果整段覆盖掉了,而填回它的
`qt.hand_anchors_engine_values` 已在 C6 退役。

**必须分清的两件事**:

- **D2 手算锚那条腿的结论仍然在案**——PR #89 正文与
[`docs/progress/07_factor_layer_refactor.md`](../progress/07_factor_layer_refactor.md) 的 D2 条目
白纸黑字记着「**88 行分层手算锚 0 失配**(脚本运行时守卫禁 import 引擎,曾自抓 3 个口径错)」。
D2 的验收**不因此动摇**。
- **丢的是「今天再验一次」的能力**。四条 daily 锚(momentum/reversal/liquidity/overnight_mom)
的 engine 侧数字已不在盘上,退役后也无法重算。

因此 `python -m qt.hand_anchors_engine_values --verify` **在今天的树上必然非零退出**,措辞是
`NOT VERIFIED (nothing recorded)` 而**不是** `FAILED`——"没验"与"验了不过"是两回事,退出码只有
一位,措辞必须补上这个区分。

⚠️ **它不进标准 gates 清单**(lead 裁定):一条永远不转绿的红,只会训练人忽略红。

### (2) 冻结目录里有 7 个**不受任何哈希保护**的文件

上一条暴露的问题比它自己大:`hand_anchors_d2.json` 就在
`artifacts/refactor_baseline/`——那个被反复称作「绝不可覆盖」的目录——**而它确实被覆盖了**。
于是普查了整个目录(113 个文件):

| 类别 | 数量 | 保护来源 |
|---|---|---|
| `exec_baseline/*` | 77 | **git**:`docs/factors/d5_exec_baseline_manifest.json` 逐文件 sha256 |
| `panels/*.parquet` | 14 | **git**:`d1_panel_freeze_manifest.md` §六(canonical + file sha) |
| `panels_d2/*.parquet` | 14 | **git**:同上表(canonical,经 `panel_reconcile --verify`) |
| `pr_c_cutoff_fix/panels/*.parquet` | 1 | **git**:`pr_c_cutoff_fix_reference_panel.md` §五(canonical) |
| **无任何哈希覆盖** | **7** | —— |

那 7 个是:`hand_anchors_d2.json`、`manifest.json`、`manifest.md`、`manifest_d2.json`、
`reconcile_d2.md`、`pr_c_cutoff_fix/manifest.json`、`pr_c_cutoff_fix/manifest.md`。
**它们之间还有程度差别,不要一概而论**:

- `manifest.json` / `manifest_d2.json` / `pr_c manifest.json` —— **自身字节未被钉,但内容受约束**:
`panel_freeze --verify` 把每一行(canonical / file sha / rows / 日期 / symbol 数 / NaN 数 /
mean / std)与 git 文档、与盘上面板三方互核,header 的 `producing_git_sha` 也与文档核。
改内容会被抓,改字节(重排、格式化)不会。
- `manifest.md` / `reconcile_d2.md` / `pr_c manifest.md` —— 渲染产物,内容可从别处推导,风险低。
- **`hand_anchors_d2.json` —— 真正无保护**:没有任何一处记着它该长什么样,而它是唯一一个
**已被实际覆盖**的。

**要记住的那句话**:「本次改动冻结基线一个字节未写」是**关于今天这次操作**的陈述,
**不是关于该目录不可变的陈述**。目录本身没有强制不可变性——上面这 7 个文件谁都能改,
其中 1 个已经被改过。

### (3) C5 对账 harness 读这两样东西时**不核任何哈希**

**三处**(`qt/factor_eval_reconcile.py`,**按符号名记,不记行号**——行号会随任何平移失效,
本 PR 自己就是证据:初稿写的 `:1539` 取自 `main`,而本分支的 `711c0a8` 把该文件平移了 +3):

| 符号 | 读什么 | 何时是唯一读入点 |
|---|---|---|
| `run_panels_mode` | `pd.read_parquet(frozen_panel_path(...))` 冻结 D1 面板 | `--mode panels` |
| `load_anchor_rows` | `hand_anchors_d2.json` | `--mode anchors` |
| `run_anchors_mode` | 经 `frozen_panel_path()` 再读一次冻结 D1 面板,求 warmup 日期网格 | **`--mode anchors` 单跑时,这是唯一的冻结面板读入点** |

⚠️ 第三处是评审补的,初稿漏了。**清单不全 = 那一处留着永远不验**——删除 PR 的处置正是
「把 verify 接进读入点」,接两处而漏一处,等于给自己发一张只覆盖三分之二的通行证。

**两句话都必须写下,缺一句都会误导**:

- **C5 的四腿全绿,是在一棵从未经过哈希核对的基线上得出的。** 而 (2) 已经证明这个目录**能**被写,
所以这不是假想风险。
- **同时**:今天 `python -m qt.panel_freeze --verify` 对 15 个面板逐一重算 canonical hash 与整条
manifest 行、**15/15 通过**,`panel_reconcile --verify` 14/14 `max_rel=0.0`
⇒ **面板此刻完好,C5 的结论不受影响**。

**处置**:修在**下一个 PR(删除 PR)**——那个 PR 的主题正是「让 frozen-forever 名副其实」,
把 `qt.panel_freeze.verify_frozen_panels` 接进 C5 harness 的**三个**读入点正属于它。
**本 PR 不动 C5 harness。**

### (3之二) `hand_anchor_rows` 的 payload 只写 pending、不写 compared —— 已知后果,本 PR 不改行为

`qt/hand_anchor_rows.py::run_hand_anchors` 每次成功跑完都会把 `hand_anchors_d2.json` 整份重写,
payload 只含 `daily_pending_engine`,**不含 `daily_engine_compared`** ⇒ **它正是 2026-07-25 冲掉
engine 比较记录的那个动作**。而在本 PR 之前,它跑完还会 print 「run python -m
qt.hand_anchors_engine_values」—— **用户刚把记录冲掉,程序就指他去跑一条现在会 raise 的命令**。

- **那行 print 已在本 PR 改掉**(改为指向 `--verify` 并说明记录已无法重建;措辞
author-once 于 `qt.hand_anchors_d2.ENGINE_COMPARISON_POINTER`,printer 只组合不复述)。
- **「`hand_anchor_rows` 是否应当被禁止覆盖冻结目录里的记录」属于行为变更**,与
§六之三(2) 的「该目录没有强制不可变性」是同一件事 ⇒ **并进删除 PR**(与上面三个读入点的
verify 接线一起做)。**本 PR 不改它的写入行为。**

### (4) 第三个工具的退役理由:一条如实记录的不对称

C6 退役的三个工具对 11 个旧 runner 的依赖**不是一个量级**:

| 工具 | 对旧 runner 的依赖 | 删 runner 后是否必然失效 |
|---|---|---|
| `qt/panel_freeze.py` | `minute_recipes()` import **全部 11 个模块**并调其私有 `_load_*_panel` | **是** |
| `qt/panel_reconcile.py` | 走 `qt.panel_freeze` 的 recipes,同上 | **是** |
| `qt/hand_anchors_engine_values.py` | 只用了 `qt.eval_jump_amount_corr._check_preconditions` **一个前置检查** | **否**——改指统一 runner 即可继续活着 |

**裁定维持**(owner 2026-07-28 的裁定是「退役重生成能力、基线 frozen-forever」,那是**目的**,
不是从「依赖 11 个 runner」推出来的推论;依赖强弱不改变裁定)。但如实记下:
**第三条退役真的放弃了一个未必注定失效的能力**,这一点已单独提给 owner。
13 changes: 10 additions & 3 deletions docs/factors/pr_c_cutoff_fix_reference_panel.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@
| 产出 | D1 冻结 run(`main` @ `3669c90`),见 `d1_panel_freeze_manifest.md` | 见下 §四 |
| 引擎 | pre-D2 runner loader | **同一条 runner loader**(`qt.panel_freeze` + `--only`) |
| 用途 | **已发布内容的忠实记录**;D2 逐位对账的历史参照 | **D5 面板腿对该因子的有效参照** |
| 是否可被覆盖 | **否**(本次改动只新增,一个字节没动) | 可再生(命令在 §四) |
| 是否可被覆盖 | **否**(本次改动只新增,一个字节没动) | **否**(C6 起同样 frozen-forever;产生方式已退役,见 §四) |

**为什么「旧引擎 + 截断输入」是干净的 refactor-only 对照**:本次改动是**纯输入截断**——
被喂进相关系数计算的 bar 少了一批,`compute_jump_amount_corr` 里从 `amp` 到 Pearson
Expand All @@ -52,10 +52,17 @@
**不要**把两份面板放进同一次对账去「取平均」或「看哪个更接近」:它们是两个不同定义的因子值,
不是同一个量的两次测量。

## 四、新参照面板的产生方式(可复跑)
## 四、新参照面板的产生方式(**已于 D5 C6 退役**,此处存档)

⚠️ **下面这条命令不再可用**:owner 2026-07-28 裁定退役重生成能力,`--only` 连同整个冻结
路径一并退役(它依赖 11 个旧 runner 的私有 loader,C6 删除它们后必然失效)。本面板与 D1
基线一样 **frozen-forever**;验证走
`python -m qt.panel_freeze --verify`,它会把本文 §五 表格里的 `canonical_sha256` 当作
git 里的权威期望值,对盘上这个面板逐格重算核对。原命令留在这里是 provenance 的一部分——
它记录的是这份面板**当初怎么来的**,不是现在怎么再来一次。

```
cd <repo root> # 缓存根 artifacts/cache/tushare/v1 就位
cd <repo root> # 缓存根 artifacts/cache/tushare/v1 就位 [RETIRED, C6]
/home/shaofl/Development/env_tools/envs/quant_mf/bin/python -m qt.panel_freeze \
--config config/phase_c_jump_amount_corr.yaml \
--output-root artifacts/refactor_baseline/pr_c_cutoff_fix \
Expand Down
Loading