From 35de346f3f7a8f38a5a0b48eedb9c0db964a8782 Mon Sep 17 00:00:00 2001 From: shaofl <2899218482@qq.com> Date: Tue, 28 Jul 2026 02:08:33 -0700 Subject: [PATCH 1/3] test(eval): bound the bounded warmup class by its structural cell ceiling (D5 C4a review LOW-1) --- qt/factor_eval_reconcile.py | 52 +++++++++++++++- tests/test_factor_eval_reconcile.py | 95 +++++++++++++++++++++++++++++ 2 files changed, 145 insertions(+), 2 deletions(-) diff --git a/qt/factor_eval_reconcile.py b/qt/factor_eval_reconcile.py index 4ca0352..0ea507d 100644 --- a/qt/factor_eval_reconcile.py +++ b/qt/factor_eval_reconcile.py @@ -23,7 +23,18 @@ * bounded factor: the differing row sits in the grid's first ``lookback_depth - 1`` trading dates — ALL THREE directions allowed (frozen-NaN -> new-finite, finite -> finite as a partial pool fills, - and a finite value on a row the frozen panel does not have); + and a finite value on a row the frozen panel does not have). The + class is CEILED at ``(lookback_depth - 1) x |frozen symbols|`` cells + (D5 C4a review LOW-1): a structural bound, not a calibrated one — + every warmup cell is a distinct (date, symbol) pair with the date in + the warmup boundary and the symbol known to the frozen panel (the + ``new_only_finite`` direction requires ``frozen_symbols`` membership), + so the class cannot exceed it by construction and an overshoot means + the boundary/counting logic itself is broken -> FAIL. AMPLITUDE stays + unbounded inside the region by the standing ruling: for C5's purpose + (geometry reconciliation) that is acceptable, because the two loading + geometries legitimately differ on exactly those cells — the value gate + for the rest of the grid is the 1e-12 tolerance outside the boundary; * valid-day-POOLED factor: the early region [2021-07-01, 2021-10-31], same three directions, and the per-month counts must be non-increasing ("按月递减至零"; a violation fails the mode) with ONE @@ -731,6 +742,11 @@ class PanelDiff: warmup_by_direction: dict[str, int] = field(default_factory=dict) warmup_monotonic: bool = True warmup_exempt_month: str | None = None + #: Structural cell ceiling of the bounded ``warmup_left_extension`` + #: class — ``(lookback_depth - 1) x |frozen symbols|`` (None for pooled + #: factors, whose class is gated by the monthly monotonicity machinery + #: instead). See ``classify_panel_differences``. + warmup_max_cells: int | None = None ok: bool = True def by_class(self, classification: str) -> list[PanelCellDiff]: @@ -771,6 +787,20 @@ def classify_panel_differences( where diffs land; a frozen panel with no finite value gets no exemption (conservative). Later months stay gated. + Bounded warmup class CELL CEILING (D5 C4a review LOW-1): the class is + capped at ``(lookback_depth - 1) x |frozen symbols|`` cells. This is a + STRUCTURAL bound, not a calibrated one: every cell the classifier can + place in the class is a distinct (date, symbol) pair whose date sits in + the warmup boundary and whose symbol is known to the frozen panel (the + ``new_only_finite`` direction explicitly requires frozen-symbol + membership), so the class cannot exceed the ceiling by construction — + an overshoot means the boundary/counting logic itself is broken and + fails the mode. AMPLITUDE inside the region stays UNBOUNDED by the + standing ruling: for C5's geometry-reconciliation purpose that is + acceptable, because the two loading geometries legitimately differ on + exactly those cells (the old runner's partial pool vs the saturated + one); everything outside the boundary is still gated at 1e-12. + ``is_cross_sectional`` (D4c ``stores_intermediate`` factors — the served value is a per-date cross-sectional combine): enables the ``threshold_flip_contamination`` class and the wider float-tail cap @@ -801,6 +831,18 @@ def classify_panel_differences( n_warm = max(int(lookback_depth) - 1, 0) warmup_dates = set(grid_dates[:n_warm]) + # Structural cell ceiling of the bounded warmup class (D5 C4a review + # LOW-1): every warmup cell the classifier can emit is a distinct + # (date, symbol) pair with the date in ``warmup_dates`` and the symbol + # in the frozen panel's symbol set (the ``new_only_finite`` direction + # requires frozen-symbol membership), so the class cannot exceed + # ``n_warm x |frozen symbols|`` by construction — an overshoot means + # the boundary/counting logic is broken and fails the mode. Pooled + # factors get NO count ceiling: their class is gated by the monthly + # monotonicity + exempt-month machinery instead. + if not is_pooled: + result.warmup_max_cells = n_warm * len(frozen["symbol"].unique()) + def _in_warmup(date: pd.Timestamp, symbol: str) -> bool: if is_pooled: return early_lo <= date <= early_hi @@ -944,6 +986,11 @@ def _warmup_cell(date, symbol, frozen_v, new_v, direction: str) -> None: not any(d.classification.startswith("unclassified") or d.classification.startswith("unregistered") for d in result.diffs) and result.warmup_monotonic + and ( + result.warmup_max_cells is None + or len(result.by_class("warmup_left_extension")) + <= result.warmup_max_cells + ) and len(result.by_class("float_reordering_tail")) <= float_tail_max and len(result.by_class("threshold_flip_tail")) <= flip_max and len(result.by_class("threshold_flip_contamination")) @@ -1120,12 +1167,13 @@ def run_panels_mode(config_path: str, factor_id: str, repo_root: Path) -> PanelD ) logger.info( "panels %s: rows frozen=%d new=%d equal=%d tol=%d warmup=%d(%s) " - "exempt_month=%s " + "warmup_ceiling=%s exempt_month=%s " "float_tail=%d threshold_flip=%d flip_contamination=%d footprint=%d " "unclassified=%d max_rel=%.3e live_calls=%d ok=%s", factor_id, result.rows_frozen, result.rows_new, result.equal, result.within_tolerance, len(result.by_class("warmup_left_extension")), result.warmup_by_direction, + result.warmup_max_cells, result.warmup_exempt_month, len(result.by_class("float_reordering_tail")), len(result.by_class("threshold_flip_tail")), diff --git a/tests/test_factor_eval_reconcile.py b/tests/test_factor_eval_reconcile.py index c70c475..714edeb 100644 --- a/tests/test_factor_eval_reconcile.py +++ b/tests/test_factor_eval_reconcile.py @@ -665,6 +665,101 @@ def test_panels_bounded_new_only_finite_row_outside_warmup_fails(): assert len(result.by_class("unregistered_new_finite_row")) == 1 +# --------------------------------------------------------------------------- # +# Bounded warmup CELL CEILING (D5 C4a review LOW-1): the class is capped at +# (lookback_depth - 1) x |frozen symbols| — a structural bound (every class +# cell is a distinct (date, symbol) pair in warmup_dates x frozen_symbols), +# so an overshoot means the boundary/counting logic itself is broken. +# --------------------------------------------------------------------------- # +def test_panels_bounded_warmup_ceiling_is_the_structural_derivation(): + # 3 symbols, lookback_depth=4 -> ceiling = 3 warmup dates x 3 symbols = 9. + frozen = _frozen_panel( + [(f"2021-07-0{d}", s, 1.0) for d in (1, 2, 3, 4, 5) for s in ("A", "B", "C")], + "f", + ) + new = _new_series( + [(f"2021-07-0{d}", s, 1.0) for d in (1, 2, 3, 4, 5) for s in ("A", "B", "C")] + ) + result = classify_panel_differences( + new, frozen, factor_id="f", is_pooled=False, lookback_depth=4 + ) + assert result.ok, result.diffs + assert result.warmup_max_cells == 3 * 3 + + +def test_panels_bounded_warmup_cells_exactly_at_the_ceiling_pass(): + # lookback_depth=3 -> 2 warmup dates; 2 symbols -> ceiling 4. All four + # warmup cells move (finite -> finite): AT the ceiling, still registered. + frozen = _frozen_panel( + [ + ("2021-07-01", "A", 0.10), ("2021-07-01", "B", 0.50), + ("2021-07-02", "A", 0.20), ("2021-07-02", "B", 0.60), + ("2021-07-03", "A", 0.30), ("2021-07-03", "B", 0.70), + ], + "f", + ) + new = _new_series( + [ + ("2021-07-01", "A", 0.11), ("2021-07-01", "B", 0.55), + ("2021-07-02", "A", 0.25), ("2021-07-02", "B", 0.65), + ("2021-07-03", "A", 0.30), ("2021-07-03", "B", 0.70), + ] + ) + result = classify_panel_differences( + new, frozen, factor_id="f", is_pooled=False, lookback_depth=3 + ) + assert result.ok, result.diffs + assert len(result.by_class("warmup_left_extension")) == 4 + assert result.warmup_max_cells == 4 + + +def test_panels_bounded_warmup_cells_beyond_the_ceiling_fail(): + # REVERSE (LOW-1): a frozen panel with DUPLICATE (date, symbol) rows + # emits one warmup cell per duplicate — 4 cells against the structural + # ceiling of 2 warmup dates x 1 symbol = 2. An overshoot of a bound the + # class cannot structurally exceed means the counting is broken -> FAIL. + # The individual cells still classify as warmup (the ceiling is a COUNT + # gate on the class, not a per-cell reclassification). + frozen = _frozen_panel( + [ + ("2021-07-01", "A", 0.10), ("2021-07-01", "A", 0.10), # duplicate + ("2021-07-02", "A", 0.20), ("2021-07-02", "A", 0.20), # duplicate + ("2021-07-03", "A", 0.30), + ], + "f", + ) + new = _new_series( + [("2021-07-01", "A", 0.15), ("2021-07-02", "A", 0.25), ("2021-07-03", "A", 0.30)] + ) + result = classify_panel_differences( + new, frozen, factor_id="f", is_pooled=False, lookback_depth=3 + ) + assert len(result.by_class("warmup_left_extension")) == 4 + assert result.warmup_max_cells == 2 + assert not result.ok + + +def test_panels_pooled_warmup_class_has_no_cell_ceiling(): + # Pooled factors: NO count ceiling — the monthly monotonicity gate is + # the mechanism (a large but non-increasing count still passes). + frozen = _frozen_panel( + [ + ("2021-07-01", "A", 1.0), ("2021-07-01", "B", 1.0), + ("2021-08-02", "A", 1.0), + ], + "f", + ) + new = _new_series( + [("2021-07-01", "A", 2.0), ("2021-07-01", "B", 2.0), ("2021-08-02", "A", 2.0)] + ) + result = classify_panel_differences( + new, frozen, factor_id="f", is_pooled=True, lookback_depth=20 + ) + assert result.ok, result.diffs + assert len(result.by_class("warmup_left_extension")) == 3 + assert result.warmup_max_cells is None + + def test_panels_pooled_warmup_class_all_directions_with_monthly_decrease(): frozen = _frozen_panel( [ From daf173b1335fed668dbe18040911d4702830236d Mon Sep 17 00:00:00 2001 From: shaofl <2899218482@qq.com> Date: Tue, 28 Jul 2026 02:08:33 -0700 Subject: [PATCH 2/3] docs(factors): D5 C5 property-test migration map + audit report skeleton --- docs/factors/d5_c5_audit_report.md | 131 ++++++++++++++ .../factors/d5_property_test_migration_map.md | 170 ++++++++++++++++++ 2 files changed, 301 insertions(+) create mode 100644 docs/factors/d5_c5_audit_report.md create mode 100644 docs/factors/d5_property_test_migration_map.md diff --git a/docs/factors/d5_c5_audit_report.md b/docs/factors/d5_c5_audit_report.md new file mode 100644 index 0000000..f373443 --- /dev/null +++ b/docs/factors/d5_c5_audit_report.md @@ -0,0 +1,131 @@ +# D5 C5 —— 四腿全量对账审计报告 + +> **状态**:**骨架**。结果区全部 TBD,由全量对账(11 因子 × panels/reports/anchors) +> 跑完后填充。填充纪律:每处差异必须落在 §二 预登记清单的具名条目内;**未编目的 +> 差异 = 验收不通过**(设计 v3.2 §五腿 3:「允许差异,但每处差异必须归因到具名原因; +> 变好的差异优先怀疑泄漏」)。 +> **执行环境**:11 因子 × {no_book, with_book} 统一 exec-only runner +> (`qt/factor_eval_runner.py`)+ 对账 harness(`qt/factor_eval_reconcile.py` 三模式)。 + +--- + +## 一、范围与方法 + +### 1.1 四腿定义(设计 v3.2 §五) + +| 腿 | 内容 | 本步载体 | +|---|---|---| +| 腿 1 性质测试搬迁 | 旧 runner 测试的每条性质在新套件有映射,覆盖数非降(R16) | `docs/factors/d5_property_test_migration_map.md`(59/59 有映射,0 静默丢失) | +| 腿 2 手算锚 | 每因子 ≥5 行从 raw parquet 徒手复算,不 import 新引擎;warmup 区差异归 `warmup_left_extension`,jump 非 warmup 行必须 reconcile(证明 service 携带截断定义) | harness `--mode anchors` vs `artifacts/refactor_baseline/hand_anchors_d2.json` | +| 腿 3 语义对账 | 新 artifact vs 冻结 exec artifact **值级**比对(IC/ICIR/分位价差/verdict;逐字节已知不成立,见 §二 #1);差异按白名单分类,聚合路径之外任何变化即失败 | harness `--mode reports`(JSON 叶子 + MD 行级配对,strict/no-book 与 bookclose 双闸) | +| 腿 4 面板腿 | service 面板(decision 视图 × exec_to_exec)vs D1 冻结面板**逐格**(1e-12;NaN 集合差异单独分类);jump 走 `pr_c_cutoff_fix` 参照物 | harness `--mode panels`(具名类:`warmup_left_extension` / `float_reordering_tail` / `threshold_flip_tail` / `threshold_flip_contamination`) | + +### 1.2 硬闸门与基线纪律 + +- **77/77 硬闸门**:三个模式入口都先跑 `verify_all()`,冻结 exec 基线字节不可达或被改 + 是**硬错误不是 skip**(没有基线就没有对账;「对账通过但没有基线」是 + compare_postmerge 空对账形态)。 +- **只读冻结基线**:对账只读 D5a 冻结的 77 份(`artifacts/refactor_baseline/exec_baseline/`, + sha256 manifest 入 git),**绝不读 live 路径**——新 runner 写同名文件,跑一次就毁掉 + 唯一基线。 +- **cache-only**:每次 service 取数后断言 `stk_mins_live_calls == 0`,非零即 ABORT。 +- **harness 加固(本准备步)**:bounded `warmup_left_extension` 类加**结构性 cell 数 + ceiling** =(w−1 个 warmup 交易日)×(冻结面板 symbol 数)——该类按定义不可能超过 + 此界,越界即 FAIL(C4a 评审 LOW-1;类内**幅度仍不设限**的既有裁定不变,理由:两种 + 加载几何在该区本来就合法地不同,区外仍由 1e-12 把门)。 + +## 二、预登记漂移清单(对账白名单) + +以下来源:编目 §七 / §七之二 / §七之三 / §七之四 / §七之五 +(`docs/factors/d5_runner_difference_catalogue.md`)+ 交接 §3 +(`tmp/design/HANDOFF_2026-07-26_refactor_execution.md`)。**不在本表内的差异一律 +unclassified → FAIL。** + +| # | 名称 | 边界 | 出处 | +|---|---|---|---| +| 1 | 契约 v1.0/v1.1 artifact 新增(`spec` 16→20 键 / `eval_config` +3 键 / 顶层 `eval_contract_version` + `corrections` / MD +4 行 provenance + 每条更正 +1 行) | 全为**新增**、零删除、零数值变化;逐字节对账已知不成立,只成立值级 + 结构性断言 | 编目 §七、§七之二;交接 §3 #1 | +| 2 | exec 侧 no_book 与 with_book 的 `eval_config` 首次合法不同(`book_view`: null vs run 模式) | 仅此一键;不得当作两格之间的回归 | 编目 §七之二 ⚠️;交接 §3 #2 | +| 3 | served-panel NaN 足迹(D4c):行集变密、按行数算分母的覆盖率变,读值不受影响 | 额外行必须全 NaN;有限值的新行只许落在 warmup 边界内 | 交接 §3 #3 | +| 4 | `warmup_left_extension`(panels/anchors 通用,三方向) | bounded:冻结网格前 w−1 个交易日(w=`lookback_depth`),cell 数 ≤ (w−1)×|symbols|(本步新增 ceiling);pooled:早区 [2021-07-01, 2021-10-31] + 按月计数非递增,豁免月 = 冻结面板**首个 finite 值所在月**(结构性锚定;全 NaN 冻结面板不豁免) | 编目 §七之三根因一、§七之四 #2、§七之五裁定 1;交接 §3 #4② | +| 5 | `float_reordering_tail` | rel ≤ 5e-12 或 abs ≤ 1e-12;cap:bars-only ≤101 cell,截面 ≤1,000 cell | 编目 §七之三根因二、§七之五裁定 3 | +| 6 | `threshold_flip_tail`(count 因子) | 幅度恰 ±1 count、rel ≤ 1e-2、≤25 cell;实测 600623.SH 2023-06-15..07-14 | 编目 §七之三根因二 | +| 7 | `threshold_flip_contamination`(仅截面因子) | 窗口 [2023-06-01, 2023-07-14];直接 symbol 600623.SH ≤2e-04,其余 ≤1e-06,总 ≤20,000 cell;逐格校验,越界即 unclassified | 编目 §七之五裁定 2 | +| 8 | `warmup_aggregate_effect`(reports 腿) | 仅聚合叶子(`sections[*]` / `verdict.reasons[*]` / `verdict.axes.*`)且**数字承载**;纯标签翻转(PASS→FAIL 无数字)仍失败 | 编目 §七之三 reports #2 | +| 9 | `registered_sanity_stem_rename` | sanity 报告名 `eval__` → `factor_eval__`,双侧以 `_exec_basis_sanity.md` 收尾 | 编目 §七之三 reports #4 | +| 10 | `registered_run_order_artifact` | `exec_price_artifact_reused` False→True(同会话 run 序);反向不登记 | 编目 §七之三 reports #5 | +| 11 | jump 更正效应(`registered_correction_effect`) | 仅当新 JSON 带 `corrections` 结构化承载(契约 v1.1);其值级验证在 panels 腿(`pr_c_cutoff_fix` 参照物)与 anchors 腿(非 warmup 行必须 reconcile),不在 reports 腿 | 编目 §七之三 reports #6、§七之二 ✅;交接 §3 #4③ | +| 12 | vpq `neutralization_coverage` add-Section | 整个 subtree 为新增;按 section **名字**匹配(`REGISTERED_EXTRA_SECTIONS`),绝不按裸下标 | 编目 §七之四 #1 | +| 13 | `book_view_effect`(with_book(decision) 格) | decision 书 vs 冻结 close 书的 Incremental 轴数值差:**全量报告、不设闸**;闸在 `_bookclose` 格(见 §四) | harness 设计(a)/(b) 分解;交接 §3 D5 C4 ⚠️ | +| 14 | `hand_anchors_d2.json` 报 `all_ok_frozen14: False` | 70 行 5 处失配**全部且仅有 jump**(手算已截断 vs 冻结 `panels_d2` 未截断)——正确信号非回归 | 交接 §3 #5 | + +## 三、每因子结果表(TBD) + +填写口径:panels = 具名类 cell 计数 + unclassified 计数(必须为 0);reports = +no_book / with_book(decision) / with_book(bookclose) 三格各自的登记类计数与 +unregistered 计数(strict 格必须为 0);anchors = ok/warmup/failed 行数(failed 必须 +为 0)。`stk_mins_live_calls` 每因子必须为 0。 + +| 因子 | panels: warmup / float_tail / flip_tail / flip_contam / footprint / unclassified / ceiling | reports: no_book | reports: with_book(decision) | reports: with_book(bookclose) | anchors: ok / warmup / failed | live_calls | +|---|---|---|---|---|---|---| +| jump_amount_corr_20 | TBD | TBD | TBD | TBD | TBD | TBD | +| minute_ideal_amp_10 | TBD | TBD | TBD | TBD | TBD | TBD | +| amp_marginal_anomaly_vol_20 | TBD | TBD | TBD | TBD | TBD | TBD | +| volume_peak_count_20 | TBD | TBD | TBD | TBD | TBD | TBD | +| intraday_amp_cut_10 | TBD | TBD | TBD | TBD | TBD | TBD | +| peak_interval_kurtosis_20 | TBD | TBD | TBD | TBD | TBD | TBD | +| valley_relative_vwap_20 | TBD | TBD | TBD | TBD | TBD | TBD | +| valley_ridge_vwap_ratio_20 | TBD | TBD | TBD | TBD | TBD | TBD | +| ridge_minute_return_20 | TBD | TBD | TBD | TBD | TBD | TBD | +| valley_price_quantile_20 | TBD | TBD | TBD | TBD | TBD | TBD | +| peak_ridge_amount_ratio_20 | TBD | TBD | TBD | TBD | TBD | TBD | + +## 四、with_book 差异分解表(TBD) + +交接 §3 的硬要求:with_book 的差异必须可分解、**整体归因不可接受**(「with_book 变了 +是因为书视图改了」会把引擎回归藏在 intended change 后面)。三分量: + +- **A = no_book 引擎效应**:no_book 格的登记类差异(干净的「有没有弄坏东西」测试, + 先对干净)。 +- **B = close 书引擎效应**:`with_book(bookclose)` 格(legacy-faithful close 书,strict) + 的登记类差异——引擎在带书条件下的效应,与 A 同口径可比。 +- **C−B = 书视图修正**:`with_book(decision)` 格的 `book_view_effect` 叶子(decision 书 + vs close 书的预期差异,全量列出不设闸)——即 §1.1 活缺陷(close(d) 书)的修正幅度。 + +| 因子 | A(no_book 各类计数) | B(bookclose 各类计数) | C−B(book_view_effect 叶子数 / 涉及轴) | 分解闭合?(B 超 A 的部分是否全部具名) | +|---|---|---|---|---| +| jump_amount_corr_20 | TBD | TBD | TBD | TBD | +| minute_ideal_amp_10 | TBD | TBD | TBD | TBD | +| amp_marginal_anomaly_vol_20 | TBD | TBD | TBD | TBD | +| volume_peak_count_20 | TBD | TBD | TBD | TBD | +| intraday_amp_cut_10 | TBD | TBD | TBD | TBD | +| peak_interval_kurtosis_20 | TBD | TBD | TBD | TBD | +| valley_relative_vwap_20 | TBD | TBD | TBD | TBD | +| valley_ridge_vwap_ratio_20 | TBD | TBD | TBD | TBD | +| ridge_minute_return_20 | TBD | TBD | TBD | TBD | +| valley_price_quantile_20 | TBD | TBD | TBD | TBD | +| peak_ridge_amount_ratio_20 | TBD | TBD | TBD | TBD | + +## 五、判定(TBD) + +| 因子 | 腿 1 性质映射 | 腿 2 anchors | 腿 3 reports | 腿 4 panels | 判定 | +|---|---|---|---|---|---| +| (11 行,每行 PASS/FAIL + 一句话归因) | 59/59(本准备步已立) | TBD | TBD | TBD | TBD | + +**整体结论**:TBD(全 11 因子四腿全过才进入 C6 删除旧 runner;任一不过则旧 runner +不删、迭代新 runner——设计 v3.2 §九 D5 行回滚条款)。 + +## 六、已知局限(登记,不阻塞) + +1. **C4a 评审 LOW-1(已处置)**:bounded `warmup_left_extension` 类原无 cell 数上限, + warmup 区内任意幅度错误都被吞(暴露面 ~1.6% 网格)。本准备步已加结构性 ceiling + (§1.2);**类内幅度仍不设限**是既有裁定——C5 用途(几何对账)下可接受,因为两种 + 加载几何在该区本来就合法地不同;区外由 1e-12 把门。 +2. **C4a 评审 LOW-2**:reports 腿 `warmup_aggregate_effect` 的「数字承载」是 digit OR + 规则——散文理由行的语义可变(改了词但含数字仍放行);verdict 标签等无数字翻转 + 仍严格失败。判定阅读时应以 panels 腿为值级闸门(reports 腿是聚合层)。 +3. **NIT 登记**:C4a NIT-1/NIT-2(装饰性,不影响判定)。 +4. **既有披露缺陷(非本次回归,见交接 §4)**:六个 JSON 字段被 `MAX_VALUE_CHARS=200` + 静默截断(census 守卫登记为 frozen set);9/11 dashboard FACTOR DEFINITION 带叠字 + (限行 + 省略标记已落地,C5 重渲染后残留形态需目视确认)。 +5. **census 守卫 cwd 陷阱**:读真实语料的 census 测试在无语料的 worktree 里静默 + skip——全量对账的 gate 行必须报「语料可达/不可达」两组数字,别只看总数。 diff --git a/docs/factors/d5_property_test_migration_map.md b/docs/factors/d5_property_test_migration_map.md new file mode 100644 index 0000000..0a07f03 --- /dev/null +++ b/docs/factors/d5_property_test_migration_map.md @@ -0,0 +1,170 @@ +# D5 C5 腿 1 —— 性质测试搬迁映射表(旧 runner 测试 → 新套件) + +> **状态**:D5 C5 准备交付物(设计 v3.2 §五腿 1「性质测试搬迁」的映射底账;C6 的 R16 +> 「覆盖数非降」审计直接消费本表)。 +> **口径**:旧套件 = 11 个旧 runner 的测试文件 `tests/test_eval_*_runner.py`(**实有 10 份、 +> 59 条测试函数**——`peak_ridge_amount_ratio`(PR-M)**没有** runner 测试文件,编目 +> §六 BUG 3 已登记,见 §五)。新套件 = 统一 runner 落地后的测试面(`test_factor_eval_runner.py` +> / `test_factor_eval_disclosures.py` / `test_factor_eval_providers.py` / +> `test_factor_eval_reconcile.py` / `test_exec_basis_eval.py` / `test_factor_service.py` / +> `test_factor_store_universe.py` / `test_minute_binding_vpq.py` / +> `test_minute_diagnostics_channel.py` / 各 `test__factor.py` / +> `test_factor_eval_contract.py` / `test_factor_eval_standard.py` / +> `test_factor_requires_spec_v1.py` 等)。 +> **基线**:`feat/factor-refactor-d5-c5-prep` @ origin/main(含 PR #105/#107)。全部 node-ID +> 在该基线上以 `pytest --collect-only -q` 核对存在(抽查记录见 §六)。 +> **枚举方式**:`pytest --collect-only -q tests/test_eval_*_runner.py` 得每文件计数,再逐条 +> `grep -n "def test"` 人工归类。计数:4+4+4+4+4+10+14+4+7+4 = **59**。 + +## 状态定义 + +| 状态 | 含义 | +|---|---| +| **已搬迁** | 测试体逐字(或近逐字)移入新文件;node-ID 改变,断言不变 | +| **已覆盖** | 旧测试主张的**性质**在新套件由一条或多条测试承担(载体可不同) | +| **载体退役** | 旧测试的**字面断言形态**不再存在,但其性质在新层有钉;理由逐条写明 | +| **NET-NEW** | 新套件超出旧套件的性质(旧套件从未测过) | + +--- + +## 一、六个同构 4 测试文件(24 条) + +适用文件(每文件 4 条,结构逐字同构,下表以 `test_eval_jump_amount_corr_runner.py` +的 node-ID 为代表,其余五个文件的同名测试映射相同): + +- `tests/test_eval_jump_amount_corr_runner.py`(PR-C,4) +- `tests/test_eval_minute_ideal_amplitude_runner.py`(PR-D,4) +- `tests/test_eval_amp_marginal_anomaly_vol_runner.py`(PR-E,4) +- `tests/test_eval_volume_peak_count_runner.py`(PR-F,4) +- `tests/test_eval_peak_interval_kurtosis_runner.py`(PR-H,4) +- `tests/test_eval_valley_relative_vwap_runner.py`(PR-I,4) + +| 旧 node-ID(jump 代表) | 性质 | 新套件映射 | 状态 | +|---|---|---|---| +| `test_minute_loader_is_cache_only_and_discloses_empty` | 分钟取数 cache-only(`live_calls=0`);未覆盖 symbol 披露为 empty、绝不暖跑;工程化 fixture 上的手算值逐格对上 | `tests/test_factor_eval_providers.py::test_minute_bars_reads_normalized_bars_with_zero_live_calls`、`::test_minute_bars_empty_and_missing_are_empty_never_live`(cache-only + 空披露);`tests/test_factor_store_universe.py::test_second_symbol_batch_is_filled_not_silently_dropped[fid]`(缺口按 (date,symbol) 判定、不静默丢名);手算值 → 各因子自己的 `tests/test__factor.py`(D2 迁移,11 因子一文件,如 `test_jump_amount_corr_factor.py` 16 条含手算锚) | 已覆盖 | +| `test_minute_loader_blocks_when_nothing_cached` | 全空缓存 → loud `ValueError("no requested symbol produced")`,绝不静默出空结果 | `tests/test_factor_service.py::test_zero_output_symbol_is_not_silently_dropped`、`::test_zero_output_symbol_veto_mutation`(零输出否决 + mutation,**更强**:逐 symbol 级别);`tests/test_factor_eval_providers.py::test_minute_bars_empty_and_missing_are_empty_never_live` | 已覆盖 | +| `test_evaluate_two_runs_produces_full_reports_and_incremental_axis` | 双跑(no_book/with_book)产出 8 个 mandatory sections + verdict;no_book purity=Skipped → Incremental 轴 NOT_ASSESSED;with_book purity=Section 含 `incremental_ic_ir` → 轴被评估;md/json/dashboard 全落盘;metrics 提取出门禁字段 | `tests/test_exec_basis_eval.py::test_exec_basis_eval_writes_its_own_reports_and_leaves_the_control_alone`、`::test_the_two_exec_artifacts_state_their_own_information_sets`、`::test_exec_basis_eval_discloses_parameters_and_coverage_in_every_report`(落盘 + 披露);`tests/test_factor_eval_standard.py::test_all_eight_sections_are_present_and_ordered`(8 节齐备);Incremental 轴语义 → `tests/test_factor_eval_contract.py` 的 purity/Incremental 判决测试(契约层 v0.7 起未动);metrics → `qt/exec_basis_eval.py::_extract_metrics` 由 `test_exec_basis_eval.py` 各测试间接钉住(metrics dict 喂 CLI 行,见 `test_exec_basis_cli_line_survives_an_absent_metric`) | 已覆盖 | +| `test_no_book_run_never_reaches_adopt` | 结构性保证:no-book + 无执行事实 → 封顶 Watch(exploratory cap + NOT_ASSESSED 轴) | `tests/test_factor_eval_contract.py::test_exploratory_run_is_capped_at_watch_instead_of_adopt`(exploratory 封顶);同文件的 Adopt 路径测试(`..._ADOPT_FACTS` 族)钉住 Tradable NOT_ASSESSED 时不可达 Adopt。**契约层在重构中一字未动**,此性质不需要 runner 级重测 | 已覆盖 | + +## 二、`tests/test_eval_ridge_minute_return_runner.py`(PR-K,10 条) + +| 旧 node-ID | 性质 | 新套件映射 | 状态 | +|---|---|---|---| +| `test_minute_loader_is_cache_only_and_discloses_empty` | 同 §一 #1 + CASE A 手算 ridge-minute return 日和(+2.5,区分求和 vs 复利) | §一 #1 各条 + `tests/test_ridge_minute_return_factor.py`(手算值测试族) | 已覆盖 | +| `test_minute_loader_reports_the_ridge_scarcity_distribution` | ridge 分布 + 有效率必须**实测**于真实 bar;披露的 floor 必须是本 run 实际施加的门(否则披露在描述没人执行的门);render 行含门值 | 诊断帧产生 → `tests/test_minute_diagnostics_channel.py::test_the_diagnostic_set_is_exactly_the_declared_three`、`::test_the_frames_carry_the_symbol_label_the_summarizers_read[factor_id]`、`::test_the_sink_is_scoped_to_the_emit_window[factor_id]`;汇总+门值 → `tests/test_factor_eval_disclosures.py::test_ridge_return_summarize_*`(3 条)+ `::test_summarizer_defaults_are_the_gates_the_binding_applies`(汇总器默认 ≡ 计算门 ≡ 模块常量,防手抄漂移) | 已覆盖 | +| `test_coverage_reports_the_return_guard_attrition` | ridge bar 被日内 lag 丢弃必须**可见**(return_guard_attrition),不得静默吸收 | `tests/test_factor_eval_disclosures.py::test_ridge_return_summarize_reports_the_return_guard_attrition` | 已搬迁 | +| `test_summarize_ridge_coverage_counterfactual_at_the_comparison_floor` | 反事实:把 ridge floor 提到比较基准(20)还剩几天 → 降门买到的天数是个数不是口号 | `tests/test_factor_eval_disclosures.py::test_ridge_return_summarize_counterfactual_at_the_comparison_floor` | 已搬迁 | +| `test_summarize_ridge_coverage_handles_no_frames` | 空输入不除零、NaN 渲染 | `tests/test_factor_eval_disclosures.py::test_ridge_return_summarize_handles_no_frames`(**改名**:编目 BUG 6 的重名冲突在此家闭合,加因子前缀) | 已搬迁 | +| `test_minute_loader_blocks_when_nothing_cached` | 同 §一 #2 | 同 §一 #2 | 已覆盖 | +| `test_evaluate_two_runs_produces_full_reports_and_incremental_axis` | 同 §一 #3 | 同 §一 #3 | 已覆盖 | +| `test_extract_metrics_surfaces_the_pr_k_comparison_quantities` | 头对头比较量(turnover / net_long_short_by_cost {1,2,4} / rank_autocorr / half_life / 截面规模 / monotonicity / gross spread)以数字形式可取 | **性质层**:这些量由评估核产出并钉在契约/标准层(`tests/test_factor_eval_standard.py::test_cost_scenarios_only_move_the_cost_line`、`::test_turnover_charges_the_first_period_and_a_full_rotation` 等;报告 JSON payload 含全量)。**载体**:旧 runner 私有 `extract_metrics` 的**键集形状**不搬迁——统一 runner 的 `_extract_metrics`(`qt/exec_basis_eval.py:242`)按编目 §四收敛为单一公共子集,比较量留在报告 JSON/section payload 里读 | 载体退役(理由 R1,见 §四) | +| `test_absent_stability_metrics_render_as_na_not_a_crash` | Skipped section → 指标 None → 汇总行渲染 `n/a` 而非在错误处理之外抛 TypeError | `tests/test_exec_basis_eval.py::test_exec_basis_cli_line_survives_an_absent_metric`(同一性质,钉在新 CLI 行 `format_exec_basis_line` 上)。注:旧测试里对 `qt.cli._fmt_metric` 的直接断言随旧 CLI 命令在 C6 一并删除,性质已由新钉承担 | 已覆盖 | +| `test_no_book_run_never_reaches_adopt` | 同 §一 #4 | 同 §一 #4 | 已覆盖 | + +## 三、`tests/test_eval_valley_ridge_vwap_ratio_runner.py`(PR-J,7 条) + +| 旧 node-ID | 性质 | 新套件映射 | 状态 | +|---|---|---|---| +| `test_minute_loader_is_cache_only_and_discloses_empty` | 同 §一 #1 + CASE A 手算 valley/ridge VWAP 比值 | §一 #1 各条 + `tests/test_valley_ridge_vwap_ratio_factor.py` 手算值族 | 已覆盖 | +| `test_minute_loader_reports_the_ridge_scarcity_distribution` | 同 PR-K 同名性质(实测分布 + 门值即所施之门 + render) | `tests/test_minute_diagnostics_channel.py` 各条 + `tests/test_factor_eval_disclosures.py::test_valley_ridge_summarize_*`(2 条)+ `::test_summarizer_defaults_are_the_gates_the_binding_applies` | 已覆盖 | +| `test_summarize_ridge_coverage_counterfactual_at_the_valley_floor` | 反事实:ridge 腿按 valley floor(20)还剩几天 | `tests/test_factor_eval_disclosures.py::test_valley_ridge_summarize_counterfactual_at_the_valley_floor` | 已搬迁 | +| `test_summarize_ridge_coverage_handles_no_frames` | 空输入安全 | `tests/test_factor_eval_disclosures.py::test_valley_ridge_summarize_handles_no_frames` | 已搬迁 | +| `test_minute_loader_blocks_when_nothing_cached` | 同 §一 #2 | 同 §一 #2 | 已覆盖 | +| `test_evaluate_two_runs_produces_full_reports_and_incremental_axis` | 同 §一 #3 | 同 §一 #3 | 已覆盖 | +| `test_no_book_run_never_reaches_adopt` | 同 §一 #4 | 同 §一 #4 | 已覆盖 | + +## 四、`tests/test_eval_valley_price_quantile_runner.py`(PR-L,14 条) + +| 旧 node-ID | 性质 | 新套件映射 | 状态 | +|---|---|---|---| +| `test_minute_loader_is_cache_only_and_discloses_empty` | 同 §一 #1 + 手算 raw 日分位(三票 0.25/0.50/0.75)+ 无效日(hi==lo)不产值 | §一 #1 各条 + `tests/test_valley_price_quantile_factor.py`(手算族:`test_hand_value_*`、`test_flat_day_with_hi_equal_lo_is_invalid`)+ `tests/test_minute_binding_vpq.py::test_service_read_through_reproduces_the_legacy_runner_cell_for_cell`(service 路径逐格复现旧 runner) | 已覆盖 | +| `test_loader_ships_the_residual_not_the_raw_quantile` | shipped 因子是反转中性化**残差**不是 raw 分位(防"算了又丢"接线 bug);带截距 OLS 残差和≈0 | `tests/test_valley_price_quantile_factor.py::test_factor_is_residualized_not_raw_qbar` + `tests/test_minute_binding_vpq.py::test_combine_preserves_the_intermediate_rows_and_is_a_per_date_reduction` | 已覆盖 | +| `test_loader_actually_consumes_the_daily_closes` | 换日频 close 面板必须改变因子值(否则上面全在 raw 分位上空过) | `tests/test_minute_binding_vpq.py::test_shifted_reversal_tracks_a_perturbed_close_bit_identically`(扰动 close → 残差逐位跟动)+ `::test_service_refuses_a_missing_daily_provider_for_vpq`(缺日频 provider loud 拒绝) | 已覆盖 | +| `test_loader_reports_the_neutralization_coverage` | 中性化覆盖率是**实测**的(raw/rev/residual 行数、截面 min/max、raw×rev Spearman) | `tests/test_factor_eval_runner.py::test_valley_price_quantile_runs_end_to_end_with_neutralization_section`(用同一 fake **独立重算**期望值再比对——比旧测试更强,非重言式)+ `tests/test_factor_eval_disclosures.py::test_neutralization_summarize_*`(2 条汇总器单测)+ `::test_neutralization_render_matches_the_former_cli_inline_line`(渲染行逐字钉) | 已覆盖 | +| `test_loader_blocks_when_nothing_cached` | 同 §一 #2 | 同 §一 #2 | 已覆盖 | +| `test_thin_cross_section_leaves_the_factor_nan` | 截面 < `min_cross_section` → 该日全 NaN,绝不退回 raw | `tests/test_valley_price_quantile_factor.py::test_residualization_below_min_cross_section_is_all_nan` + `tests/test_factor_store_universe.py::test_min_cross_section_gate_still_bites_on_the_read_path` | 已覆盖 | +| `test_summarize_neutralization_counts_missing_reversal_rows` | 汇总器对缺 reversal 行的计数 | `tests/test_factor_eval_disclosures.py::test_neutralization_summarize_counts_missing_reversal_rows` | 已搬迁 | +| `test_summarize_neutralization_handles_an_all_missing_reversal` | 全缺 reversal → 零覆盖率、Spearman NaN | `tests/test_factor_eval_disclosures.py::test_neutralization_summarize_handles_an_all_missing_reversal` | 已搬迁 | +| `test_reversal_from_a_daily_panel_is_the_t_minus_1_ratio` | 反转输入 = 日频面板 close 的 **T-1** 比值(不用当日 close) | `tests/test_valley_price_quantile_factor.py::test_reversal_20_is_minus_the_t_minus_1_twenty_day_return`、`::test_reversal_uses_t_minus_1_close_not_day_d` + `tests/test_minute_binding_vpq.py::test_shifted_panel_reversal_is_bit_identical_to_internal_t1`(平移面板第三条路逐比特等价钉) | 已覆盖 | +| `test_evaluate_two_runs_produces_full_reports_and_incremental_axis` | 同 §一 #3 | 同 §一 #3 | 已覆盖 | +| `test_extract_metrics_surfaces_the_pr_l_comparison_quantities` | 同 PR-K 同名性质 + `ic_pearson_mean` 与 rank IC 并列 + sign=+1 时 aligned spread 不误标 | 同 PR-K:性质层量在契约/标准层有钉;`_extract_metrics` 键集形状不搬迁 | 载体退役(理由 R1,见 §四) | +| `test_spec_sign_is_positive_so_aligned_spread_is_not_mis_signed` | PR-L 的预注册 sign=+1(v0.8 成本符号缺陷只咬 sign=−1) | `tests/test_factor_requires_spec_v1.py::test_shipped_factor_declarations_match_the_d0_table[...]`(18 因子声明含 `expected_ic_sign` 参数化钉死)+ `tests/test_valley_price_quantile_factor.py::test_spec_declares_the_pre_registered_sign_and_the_pinned_deviations`;v0.8 对齐价差语义由契约层 verdict 测试钉(`tests/test_factor_eval_contract.py`,该层重构零改动) | 已覆盖 | +| `test_no_book_run_never_reaches_adopt` | 同 §一 #4 | 同 §一 #4 | 已覆盖 | +| `test_min_valley_bars_default_matches_the_pr_i_floor` | PR-L 复用 PR-I 的 valley floor,两 run 覆盖率可比 | **双侧钉值**:`tests/test_valley_price_quantile_factor.py::test_definition_constants_are_the_pinned_values`(`VALLEY_QUANTILE_MIN_VALLEY_BARS == 20`)+ `tests/test_valley_relative_vwap_factor.py`(`VALLEY_VWAP_MIN_VALLEY_BARS == 20`,line 401 断言)。任一侧漂移都会红其一;跨模块相等性断言语句本身不再存在 | 已覆盖(注:相等性由"同值双钉"承载,非单条等式断言) | + +## 五、`tests/test_eval_intraday_amp_cut_runner.py`(PR-G,4 条) + +| 旧 node-ID | 性质 | 新套件映射 | 状态 | +|---|---|---|---| +| `test_amp_cut_loader_is_cache_only_and_combines_cross_section` | cache-only + per-symbol 统计量 → 截面 z-score combine(截面归零);stats 面板行数;empty 披露 | §一 #1 各条 + `tests/test_intraday_amp_cut_factor.py::test_amp_cut_cross_section_zscore_hand_value`、`::test_amp_cut_cross_section_below_min_is_all_nan`、`::test_amp_cut_full_pipeline_cross_section` + **D4c 中间量架构**:`tests/test_factor_store_universe.py::test_only_the_cross_sectional_factor_stores_an_intermediate`、`::test_the_cross_sectional_combine_runs_once_per_request`、`::test_served_panel_matches_the_direct_engine_and_adds_only_nan_rows[fid]` | 已覆盖 | +| `test_amp_cut_loader_blocks_when_nothing_cached` | 同 §一 #2 | 同 §一 #2 | 已覆盖 | +| `test_amp_cut_evaluate_two_runs_produces_full_reports_and_incremental_axis` | 同 §一 #3 | 同 §一 #3 | 已覆盖 | +| `test_amp_cut_no_book_run_never_reaches_adopt` | 同 §一 #4 | 同 §一 #4 | 已覆盖 | + +## 六、PR-M(`peak_ridge_amount_ratio`)——旧套件 **0 条**(编目 BUG 3) + +`qt/eval_peak_ridge_amount_ratio.py` 从未有 runner 测试文件(`grep -rln +eval_peak_ridge_amount_ratio tests/` 无命中)。新套件在此是**净增**: + +- `tests/test_factor_eval_disclosures.py::test_peak_summarize_counterfactual_at_the_ridge_floor` + 与 `::test_peak_summarize_handles_no_frames`(**NET-NEW**:三个兄弟因子都有的 + 反事实 + 空帧对,PR-M 补平)。 +- `tests/test_factor_eval_disclosures.py::test_disclosure_binding_covers_exactly_the_three_publishing_factors`(披露绑定恰好覆盖 PR-J/K/M 三因子,其余因子**陈述式**无披露)。 +- 因子数学由 `tests/test_peak_ridge_amount_ratio_factor.py`(43 条)承担。 + +## 七、新套件超出旧套件的 NET-NEW(选列,非穷举) + +统一 runner 落地新增、旧 59 条从未覆盖的性质: + +- **runner 配置门**(编目 C1/C2 collapse):`test_factor_eval_runner.py::test_preconditions_fail_readably`(4 参数化)、`::test_missing_oos_is_a_readable_error`、`::test_invalid_book_mode_is_rejected_before_any_work`。 +- **BUG 5 闭合**:`::test_config_book_empty_or_exact_is_accepted`、`::test_config_book_mismatch_is_a_readable_error`(runner 不读 config `factors:` → 声明不符即 loud)。 +- **exec 身份 + book 双模式**:`::test_build_eval_config_declares_the_exec_identity_and_shared_kwargs`、`::test_end_to_end_wiring_both_book_modes`(含 `_bookclose` 后缀隔离)、`tests/test_factor_eval_reconcile.py` 的 `check_new_pair_consistency`(no_book/with_book 的 eval_config 只差 `book_view`)。 +- **add-Section 桥**:`test_factor_eval_disclosures.py::test_extra_section_never_moves_the_verdict_or_mandatory_sections`、`::test_an_extra_section_may_never_shadow_a_mandatory_name`、`::test_exec_basis_augmentation_seam_preserves_report_and_verdict`。 +- **诊断通道**(`test_minute_diagnostics_channel.py` 全文件,10 条)。 +- **对账 harness**(`test_factor_eval_reconcile.py`,80 条:硬闸门、panels 具名类正反向、reports 白名单、anchors)。 +- **service/store universe 语义**(`test_factor_service.py` / `test_factor_store_universe.py`:单点填≡批量填、零输出否决、(date,symbol) 缺口、中间量 universe 无关、max 形 floor 红测试等)。 +- **vpq 绑定等价**(`test_minute_binding_vpq.py`:平移面板第三条路双侧钉)。 + +## 八、计数对照(R16 非降证据) + +| 桶 | 条数 | +|---|---| +| 旧套件总条数(10 文件) | **59** | +| 已搬迁(逐字移入新文件) | 7(PR-J×2、PR-K×3、PR-L×2 汇总器测试) | +| 已覆盖(性质在新套件有钉) | 50 | +| 载体退役(性质仍在新层有钉,字面断言形态不搬迁) | 2(PR-K/PR-L 的 `extract_metrics` 键集形状,理由 R1 见下) | +| **有映射合计** | **59 / 59(100%)** | +| 完全无映射、无理由退役 | **0** | +| 新套件 NET-NEW(超出旧套件的性质,选列见 §七) | ≥40(含 PR-M 补平 3 条) | + +**非降结论**:旧 59 条逐条有映射(7 搬迁 + 50 覆盖 + 2 载体退役但性质有钉),无一条 +静默丢失;新套件另有 NET-NEW。性质覆盖数严格上升。 + +### 退役理由 + +- **R1(PR-K `test_extract_metrics_surfaces_the_pr_k_comparison_quantities` / PR-L + `test_extract_metrics_surfaces_the_pr_l_comparison_quantities` 的键集形状)**:旧 runner + 各自私有 `extract_metrics`,键集三档(编目 §四实测"与因子无关")。统一 runner 把 + metrics 收敛为单一公共子集(`qt/exec_basis_eval.py::_extract_metrics`),比较量 + (turnover / net-by-cost 三档 / autocorr / half-life / 截面规模 / monotonicity / + ic_pearson)仍由评估核产出、钉在契约/标准层测试,并全文落在报告 JSON 的 section + payload 里——**信息无损失,损失的是"从 metrics dict 里读"这个便利载体**。该载体随 + 11 个旧 runner 在 C6 删除;若后续需要同样的 CLI 头对头摘要,应作为一个显式需求 + 重新提出,而不是靠保留旧 runner 来保住它。 + +## 九、node-ID 抽查记录 + +按完成定义抽查 10 条(`pytest --collect-only -q ` 后 grep node-ID),全命中: + +| # | node-ID | 结果 | +|---|---|---| +| 1 | `tests/test_factor_eval_providers.py::test_minute_bars_reads_normalized_bars_with_zero_live_calls` | ✅ | +| 2 | `tests/test_factor_service.py::test_zero_output_symbol_veto_mutation` | ✅ | +| 3 | `tests/test_factor_eval_contract.py::test_exploratory_run_is_capped_at_watch_instead_of_adopt` | ✅ | +| 4 | `tests/test_factor_eval_standard.py::test_all_eight_sections_are_present_and_ordered` | ✅ | +| 5 | `tests/test_factor_eval_disclosures.py::test_ridge_return_summarize_reports_the_return_guard_attrition` | ✅ | +| 6 | `tests/test_factor_eval_runner.py::test_valley_price_quantile_runs_end_to_end_with_neutralization_section` | ✅ | +| 7 | `tests/test_minute_binding_vpq.py::test_service_read_through_reproduces_the_legacy_runner_cell_for_cell` | ✅ | +| 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` | ✅ | From 39e7f9c7b44f993cbf1029f21d30425950c2dc57 Mon Sep 17 00:00:00 2001 From: shaofl <2899218482@qq.com> Date: Tue, 28 Jul 2026 02:27:42 -0700 Subject: [PATCH 3/3] docs(eval): state the index-uniqueness premise of the warmup cell ceiling (C5 prep review NIT) --- qt/factor_eval_reconcile.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/qt/factor_eval_reconcile.py b/qt/factor_eval_reconcile.py index 0ea507d..770bb9d 100644 --- a/qt/factor_eval_reconcile.py +++ b/qt/factor_eval_reconcile.py @@ -795,7 +795,11 @@ def classify_panel_differences( ``new_only_finite`` direction explicitly requires frozen-symbol membership), so the class cannot exceed the ceiling by construction — an overshoot means the boundary/counting logic itself is broken and - fails the mode. AMPLITUDE inside the region stays UNBOUNDED by the + fails the mode. The "by construction" argument assumes (date, symbol) + index uniqueness on both sides; duplicate frozen rows overcount, and + the class then exceeds the ceiling and fails CLOSED (the safe + direction — bad data is never waved through). AMPLITUDE inside the + region stays UNBOUNDED by the standing ruling: for C5's geometry-reconciliation purpose that is acceptable, because the two loading geometries legitimately differ on exactly those cells (the old runner's partial pool vs the saturated