From b221b8843a03d2fdd23efda0bd1366a92632c1fb Mon Sep 17 00:00:00 2001 From: shaofl <2899218482@qq.com> Date: Fri, 31 Jul 2026 13:34:37 -0700 Subject: [PATCH] docs: record D6a and the guard-scope fix D6a switched phase0/phase2 to the factor service and proved it a no-op cell by cell: 16,388 cells at max_abs_diff 0.0, every headline metric bit-identical at full precision, and the phase0 anchor untouched, which R10 requires until D6d. Everything that would have made a later number move ambiguous - the isinstance dispatch, the missing cache argument - was registered rather than smuggled in. The finding that matters more than the switch is that pytest had been writing fabricated values into the shared factor store. The demo config uses real tickers and real dates, the store key carries no data-source dimension, and D6b's runners are CSI300 over exactly those years; the two configs derive the same key, verified independently by three parties. The fix is not a stronger key. Demo prices are a function of data.start, which the key cannot see, so those values are not a function of their own key - they are not storable at all, and the code now refuses to store them rather than trying to name them. Two guards were rewritten to derive rather than enumerate. The caller census replaced a hand-typed list after a mutation showed the real failure shape: a third unregistered caller plus a signature change plus dutifully updating everything visible left the suite entirely green. The retired-invocation guard was walking the filesystem, so it read git archive exports of pre-retirement revisions under tmp/ and turned main red while every worktree stayed green - and every gate for C6, D5 and D6a had been run inside a worktree, which structurally lacks the directories that break it. Three rules came out of the round. Claiming a property is covered requires naming which test covers it; "that path is covered" is the shape of the mistake, and it fooled its author before it fooled the lead through his report. A red test means either the code broke or the test went blind, and only looking at the colour cannot tell them apart - the lead's instruction to preserve one such red was wrong, because that red was a guard failing to see a call while reporting that the call no longer existed. And node-ID diffs count tests, not properties, so they cannot see a test that grew an assertion. --- AGENTS.md | 8 ++++---- CLAUDE.md | 8 ++++---- docs/progress/07_factor_layer_refactor.md | 13 +++++++++++++ docs/progress/pr_ledger.md | 3 +++ 4 files changed, 24 insertions(+), 8 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index a80c47d..572f32c 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -66,15 +66,15 @@ data → universe → factors(特征) → alpha(合成/预测) → portfolio(+ri | 项 | 值 | |---|---| -| `main` | 下表全部 gate 实测于 `4e73ac1`(PR #116,D5 C6 收口) | -| `pytest -p no:warnings` | **2669 passed + 1 skipped**(⚠️ 不要再传 `-q`:`pyproject.toml` 已含 `addopts="-q"`,叠成 `-qq` 会吞掉摘要行) | +| `main` | 下表全部 gate 实测于 `8aa11c1`(PR #119) | +| `pytest -p no:warnings` | **2705 passed + 1 skipped**(⚠️ 不要再传 `-q`:`pyproject.toml` 已含 `addopts="-q"`,叠成 `-qq` 会吞掉摘要行) | | `ruff check .` | clean | | phase0 锚(`run-phase0 --config config/example.yaml`) | **ic 0.9600 / annual 0.8408**(自 P0 起从未变过,任何改动都不许动它) | | `qt.exec_baseline_freeze --verify` | **77/77 @ `45c14aa`** | | `validate-config` | 32/32 | | 唯一 OPEN 的 PR | #38(历史遗留,保持 OPEN) | -**当前主线 = 因子层深度重构(设计 v3.2,D0–D7)。D5 已全部收口**:C5 四腿对账 33 格全绿(#113),**C6 完成(#115 改造 + #116 删除)——11 个旧 runner 归零,决策③「杀第二条因子取数路径」收尾**,三个重生成工具退役为 verify-only、D1 基线 frozen-forever、校验已接进 C5 harness 的三个读入点。**下一步 = Step 5 / D6a–d 存量收口**(`aggregate → factors.minute` 的 shim 日落、`asof_daily_features` 的 `mmp_ew` hook 需要新家),然后 D7 收官全量重评估(**exec-only**)。 +**当前主线 = 因子层深度重构(设计 v3.2,D0–D7)。D5 已全部收口**:C5 四腿对账 33 格全绿(#113),**C6 完成(#115 改造 + #116 删除)——11 个旧 runner 归零,决策③「杀第二条因子取数路径」收尾**,三个重生成工具退役为 verify-only、D1 基线 frozen-forever、校验已接进 C5 harness 的三个读入点。**D6a 已收口(#118)**:phase0/phase2 切 FactorService,真实 phase2 逐格 `max_abs_diff=0.0`、锚未动(R10:新锚 D6d 才转正);顺带堵死一个**已经在发生**的红线 #6 违反(demo 值写进共享 store)。**下一步 = D6b**(oos/subset/robustness 切服务,**必须先抓 legacy 再切**——归档数字已不复现,见 D6a-5),然后 D6c / D6d / D7 收官全量重评估(**exec-only**)。 > **接手必读(唯一入口)**:[`tmp/design/HANDOFF_2026-07-30_claude_code.md`](tmp/design/HANDOFF_2026-07-30_claude_code.md) > ——含当前截面、F1–F5 裁定速查表、六步路线、派发/评审/限额的具体操作法、陷阱清单。 @@ -146,6 +146,6 @@ data → universe → factors(特征) → alpha(合成/预测) → portfolio(+ri ## 路线图 -1. **因子层重构收尾(当前主线)**:~~F1 (#112)~~ → ~~C5 audit (#113)~~ → ~~C5 进度文档 (#114)~~ → ~~C6 改造 (#115) + C6 删除 (#116)~~ → **D6a–d 存量收口(下一步)** → D7 收官全量重评估(**exec-only**)。 +1. **因子层重构收尾(当前主线)**:~~F1 (#112)~~ → ~~C5 audit (#113)~~ → ~~C5 进度文档 (#114)~~ → ~~C6 (#115+#116)~~ → ~~D6a (#118)~~ → **D6b(下一步)** → D6c → D6d → D7 收官全量重评估(**exec-only**)。 2. **因子研究**:十一因子无一到 Adopt ⇒ 下一步优先做**换手/成本敏感的组合层验证**与更长 holdout,而不是继续堆新因子;补 PR #79 的 review;修订 HTML compendium。 3. **可选、须单独显式 goal**:I5g(强制 partial-fill / volume-cap);数据层 D6(`PanelStore` append/partition,仅当因子研究需要可复用派生面板时才启动);schema 守卫接 live `data-update`。 diff --git a/CLAUDE.md b/CLAUDE.md index a80c47d..572f32c 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -66,15 +66,15 @@ data → universe → factors(特征) → alpha(合成/预测) → portfolio(+ri | 项 | 值 | |---|---| -| `main` | 下表全部 gate 实测于 `4e73ac1`(PR #116,D5 C6 收口) | -| `pytest -p no:warnings` | **2669 passed + 1 skipped**(⚠️ 不要再传 `-q`:`pyproject.toml` 已含 `addopts="-q"`,叠成 `-qq` 会吞掉摘要行) | +| `main` | 下表全部 gate 实测于 `8aa11c1`(PR #119) | +| `pytest -p no:warnings` | **2705 passed + 1 skipped**(⚠️ 不要再传 `-q`:`pyproject.toml` 已含 `addopts="-q"`,叠成 `-qq` 会吞掉摘要行) | | `ruff check .` | clean | | phase0 锚(`run-phase0 --config config/example.yaml`) | **ic 0.9600 / annual 0.8408**(自 P0 起从未变过,任何改动都不许动它) | | `qt.exec_baseline_freeze --verify` | **77/77 @ `45c14aa`** | | `validate-config` | 32/32 | | 唯一 OPEN 的 PR | #38(历史遗留,保持 OPEN) | -**当前主线 = 因子层深度重构(设计 v3.2,D0–D7)。D5 已全部收口**:C5 四腿对账 33 格全绿(#113),**C6 完成(#115 改造 + #116 删除)——11 个旧 runner 归零,决策③「杀第二条因子取数路径」收尾**,三个重生成工具退役为 verify-only、D1 基线 frozen-forever、校验已接进 C5 harness 的三个读入点。**下一步 = Step 5 / D6a–d 存量收口**(`aggregate → factors.minute` 的 shim 日落、`asof_daily_features` 的 `mmp_ew` hook 需要新家),然后 D7 收官全量重评估(**exec-only**)。 +**当前主线 = 因子层深度重构(设计 v3.2,D0–D7)。D5 已全部收口**:C5 四腿对账 33 格全绿(#113),**C6 完成(#115 改造 + #116 删除)——11 个旧 runner 归零,决策③「杀第二条因子取数路径」收尾**,三个重生成工具退役为 verify-only、D1 基线 frozen-forever、校验已接进 C5 harness 的三个读入点。**D6a 已收口(#118)**:phase0/phase2 切 FactorService,真实 phase2 逐格 `max_abs_diff=0.0`、锚未动(R10:新锚 D6d 才转正);顺带堵死一个**已经在发生**的红线 #6 违反(demo 值写进共享 store)。**下一步 = D6b**(oos/subset/robustness 切服务,**必须先抓 legacy 再切**——归档数字已不复现,见 D6a-5),然后 D6c / D6d / D7 收官全量重评估(**exec-only**)。 > **接手必读(唯一入口)**:[`tmp/design/HANDOFF_2026-07-30_claude_code.md`](tmp/design/HANDOFF_2026-07-30_claude_code.md) > ——含当前截面、F1–F5 裁定速查表、六步路线、派发/评审/限额的具体操作法、陷阱清单。 @@ -146,6 +146,6 @@ data → universe → factors(特征) → alpha(合成/预测) → portfolio(+ri ## 路线图 -1. **因子层重构收尾(当前主线)**:~~F1 (#112)~~ → ~~C5 audit (#113)~~ → ~~C5 进度文档 (#114)~~ → ~~C6 改造 (#115) + C6 删除 (#116)~~ → **D6a–d 存量收口(下一步)** → D7 收官全量重评估(**exec-only**)。 +1. **因子层重构收尾(当前主线)**:~~F1 (#112)~~ → ~~C5 audit (#113)~~ → ~~C5 进度文档 (#114)~~ → ~~C6 (#115+#116)~~ → ~~D6a (#118)~~ → **D6b(下一步)** → D6c → D6d → D7 收官全量重评估(**exec-only**)。 2. **因子研究**:十一因子无一到 Adopt ⇒ 下一步优先做**换手/成本敏感的组合层验证**与更长 holdout,而不是继续堆新因子;补 PR #79 的 review;修订 HTML compendium。 3. **可选、须单独显式 goal**:I5g(强制 partial-fill / volume-cap);数据层 D6(`PanelStore` append/partition,仅当因子研究需要可复用派生面板时才启动);schema 守卫接 live `data-update`。 diff --git a/docs/progress/07_factor_layer_refactor.md b/docs/progress/07_factor_layer_refactor.md index f6a2894..e898886 100644 --- a/docs/progress/07_factor_layer_refactor.md +++ b/docs/progress/07_factor_layer_refactor.md @@ -59,3 +59,16 @@ - **方法论新增(本轮换来的)**:① **一个失败的 mutation 逼出了更好的设计**——首版针对接线的 mutation 没牙(只钉"路由"没钉"接线"),正是它促成单一网关重构;② **"把自己的守卫说弱"比"说强"更难被自己发现**——说强会被 mutation 打红,**说弱不会;没有任何 mutation 会因为文档太谦虚而失败** ⇒ 只能靠**拿措辞去对实测行为**,且必须由**别人**做;③ **控制组必须先 ACCEPTED**,否则后面每个 REFUSED 都是空的(与"mutation 必须先断言落地"是同一道理的两面)。 - ⚠️ **同一个协议洞复发三次**(评审两次发现**树在它测量期间被改动**:一次因 lead 收到主报告就派返工而评审仍有追加项、一次因实现方声明停手后自审出真缺陷**直接改了**)。**两次都是"与预期矛盾的观察"抓住的,不是测试。** 三方各补一条才闭合:**lead** 等全部条目交付且明确收工才派返工;**评审**每份报告显式声明是否收工;**实现方**声明停手后非经指派不得恢复工作——**哪怕自己发现真缺陷,也先报告再动手**。**两次污染都被逐条厘清射程而非笼统作废,无一轮重来**,靠两条定界规矩:**刻度是「哪次测量读了哪份文本」不是 commit 时间戳**(工作树**先于** commit 改变,实证一次探针跑在 commit 前 90 秒却已读到新文本);**「没动可执行代码」要用剥掉 docstring 后的 `ast.dump` 精确比来量**。 - gates(lead 独立重跑):pytest **2669 passed + 1 skipped** / ruff clean / phase0 锚 **0.9600·0.8408** / **32/32** configs / `exec_baseline --verify` **77/77 @ 45c14aa** / 三个重生成入口 **rc=1**(loud)/ 两个 `--verify` OK / 旧 CLI 名 **rc=1** + 迁移提示、新名 rc=0 / `git add -A --dry-run` 无可暂存项 / secret **0** / 冻结基线**自 2026-07-25 起 0 写入**。实现方 mutation **31/31 有牙**,评审两轮各自独立写 mutation 复现关键项。 +- ✅ **D6a(PR #118)+ 守卫射程修复(PR #119)—— phase0/phase2 切 FactorService**:D6 四子步的第一步(R17 拆分)。按 **R10 锚时间线**,旧锚 `ic 0.9600 / annual 0.8408` **本步不许动**,实测未动(新锚 D6d 才转正)。 + - **这一步的全部价值是「一次可证明为 no-op 的路径切换」** ⇒ 凡会让"某个数动了"变成多选题的改动**一律登记而不夹带**:`isinstance(FinancialFactor/ValueFactor)` 分派(D6a-2,红线 #5;`registry.requirements()` 零生产调用者)、三个 runner 漏传 `cache`(D6a-3)。**真实 phase2 逐格对账**(`phase2_real_baseline_cached.yaml`,cache-only):**16,388 cells / `max_abs_diff=0.0` / NaN mask 失配 0 / 索引集合差 0**,headline **全精度逐位相同**(21 个叶只 `elapsed_seconds` 不同)。⚠️ **顺序承重**:legacy 捕获跑在**未修改的 main** 上、切换后在冻结树重跑——切完就没有 legacy 可比(D5a 抓到过的形态)。⚠️ **诚实标注没覆盖到什么**:SSE50 面板恰好稠密(68×241=16,388)⇒ **这次真实跑一行都没走 reindex 路径**,覆盖它的是单测。**「类为空是测出来的」与「类不存在」是两回事。** + - **B1(CRITICAL,勘察期发现、已经在发生)**:`StoreKey` 无数据源维,而 `config/example.yaml` 的 demo universe 用**真实 A 股代码 + 真实日期** ⇒ **每跑一次 `pytest`(`test_provisional_anchor_d2` 调 `run_phase0`),DemoFeed 编造的 `momentum_20` 就写进共享 store**;而 **D6b 的 oos/subset 正是 CSI300 2024–2026 + momentum_20**,重叠 (date,symbol) 上会**命中 store、零重算、零报错地拿到编造值**(红线 #6)。**修法不是加固 key,是拒绝存储**:demo 价格由 `_close_path` 的 `arange(n)` 从 `data.start` 起算 ⇒ 同一 (date,symbol) 的值**随 `data.start` 而变**(实测 210.0/187.0/166.0)而 key 看不见它 ⇒ **一个身份无法被 key 捕获的值,按定义就不可存储**。`qt/factor_source.py`:合成源 ⇒ **ephemeral store**;持久 store 配合成源 ⇒ **构造期 raise**;**未分类的新 source 也 raise**。策略与切换**同一 commit**,无"先切后隔离"窗口。**三方独立证据**:①实现方与评审**各自算出同一 key** `momentum_20/7816ca515a5b9a1a__ca4d351df0bf40f6__close.parquet`;②评审从 PIT `index_weight` 实测 **2024 CSI300 与 demo 五票交集 = {000001.SZ, 000002.SZ}**(今天没撞上只因 SSE50/CSI500 交集为空);③**最硬**——真实 phase2 已把该 key 写到盘上(11:33、68 只全 .SH、零 demo 票),随后跑 demo 派生**同一 key** 而该文件 **mtime 纹丝未动**(lead 合并后又复现一次)。**(b) 给 key 加 provenance 维**登记 D6a-1(它多买到的是**真实源之间**的 vintage 漂移,属 **D3 既有性质**)。 + - **B2** store 根从 `output.root_dir` 派生、不新增旋钮(此前 16 个测试重定向 `output.*` 而 store 根不跟着走,**本身就是测试隔离缺陷**)。**B3** 服务返回整格 ⇒ 多出全 NaN footprint 行会动已发布诊断 `coverage` 的分母 ⇒ 按**红线 #8**(重构改实现不改定义)reindex 复原旧口径 + **丢掉的行数记进日志**(复原可以、隐形不行);面板有而服务没返回的 cell ⇒ **`ValueError`**(红线 #9)。 + - **LOW-2(评审)**:**D6a 是第一次把日频因子放进共享 store,而 key 没有 universe 维**;今天安全**只因为**每个日频因子都是 per-symbol,而**这条事实此前无测试无登记**(**D5b 那个 CRITICAL 的形态**;minute 面由 D4c 修掉,**日频面没继承**)。新增**行为式**守卫(5 票 vs 3 票子集,共有票逐位相同)。**为什么不做源码扫描是测出来的**:截面去均值写成 `x.unstack().sub(...).stack()` **不含 `.rank(` 也不含 `groupby(level="date")`**,扫描 token 数 **0** 而 census **变红**。红了是 **STOP-AND-REPORT**(修法是 D4c 的),**不是加豁免名单**。登记 D6a-0。 + - **LOW-3(评审)**:守卫的 `UNMIGRATED_RUNNERS` 是**手敲清单**。**MS4 是决定性证据**:第三个未登记调用者 + 改签名 + 两个已登记 runner 与可见测试**全部尽职更新** ⇒ **21 passed 全绿、完全静默**。改成**全仓派生**(`git ls-files` ∪ untracked-not-ignored;认直接/属性/别名调用;`getattr` 响亮拒绝;**运行时拼名的盲区写进 docstring**)+ **三层反空过** + ⚠️ **「D6b 会让它变红并被删掉」写进 docstring**、并明写**不许在函数还有调用者时清空期望集来"修"它**。⚠️ **一处故意的诚实**:期望集含 `tests/test_factor_source.py`(它确实调用旧入口)——**派生就是派生,不能"除了不方便的那个"**;评审实测:删掉旧入口唯一的执行覆盖 → census **变红**,**若当初豁免掉,这个警报就是静默的**。 + - **S1(实现方自审,且比它上轮报的更糟)**:一条名为 `..._preserves_every_value_...` 的测试实测的是确定性。它逐行核后发现:稀疏测试只断言**索引**、断言**值**的那条跑在**稠密**面板(reindex 是恒等)⇒ **"索引对、行错"的 reduction 全仓无人看得见**。⇒ 改名 + 把缺失的值断言补到它该在的地方。**M16 三向**(按位置 reindex:稠密值测试绿 / 稀疏索引与计数绿 / **新增稀疏值断言红**;**旧套件 21 passed 完全失明**)把"新覆盖 vs 重复"**从主张变成测量**。**NIT-2** 评审提出并标注未验证,实现方**先验证再修**(长度不变、每 date 平移一天 ⇒ 该测试 PASS、另三条红 ⇒ 坐实)。 + - **一处实现方拒绝 lead 指令并给了测量,lead 与评审都判它对**:lead 要求「清单内文件改 `ast.Attribute` 必须仍然红」。旧守卫只认 `ast.Name` ⇒ Attribute 形态让它**找到 0 个调用、栽在自己的 `assert calls` 上**,报出 **"no longer calls _compute_factor_panel"——而这句是假的**。⇒ **保住旧的红,就是保住一个「对合法重构报错、且报错理由是假话」的假阳性。** 新守卫认识该形态所以**正确地绿**;MS2 真正代理的性质由 **M17 更强地钉住**(Attribute + 改签名 → 红 ⇒ **Attribute 调用是被 bind 的**)。评审复核后判实现方对,**并认领源头是自己上一轮把这个红写成"一条好性质"**。 + - **D6a-5(会影响后续所有基线)**:**归档 phase2 数字在今天的 main 上已不复现**(annual **−10.19% → −9.41%**、sharpe/turnover 亦动,**而因子 IC 逐位不变**)⇒ 动的不是行情/因子,指向 D6a-3 那三个**实时抓**的协变量端点。**拿档案数字当今天的基线是错的。** 并写明对账的未言明前提:**其有效性依赖协变量在两次捕获之间未被上游修订**(双向危险:既能凭空造差异,也能掩盖真差异)。 + - **PR #119(合并后发现,非 D6a 缺陷)**:**`main` 上 `pytest` 是红的,而每个 worktree 里都是绿的**。`test_no_new_file_starts_instructing_people_to_run_a_retired_tool` 用 `rglob` 走文件系统 ⇒ 读进 `tmp/context/**` 里评审用 `git archive` 导出的**退役之前**的仓库副本(14 行,**其中每处命名退役工具都是合法的**)。⚠️ **值得记的是那个不对称**:**worktree 结构性地缺少两个会打破它的目录**(没有 `tmp/`;嵌套 `.claude/worktrees/` 只在主 checkout 下),而 **C6/D5/D6a 的每一次 gate 都跑在 worktree 里** ⇒ **守卫看不见会打破它的东西——从我们一贯运行它的地方看不见。** 修法照 D6a caller census 立下的先例**从 git 派生**(tracked ∪ untracked-not-ignored;**untracked 刻意包含**——新写的文件正是它存在的理由且通常未 staged;gitignored 刻意不可见,**限制写进 docstring**)。牙**验过**:新增未跟踪文件 → 红 / 移除 → 绿 / 新增 gitignored → 绿。 + - **方法论新增三条**:① **判断「某性质是否已被覆盖」必须指出是哪一条测试**——只说"路径被覆盖了"恰好是犯错的形状(那句含混的话**先骗了写它的人、再通过他的报告骗了 lead**);配套答法是**三向 mutation**。② **一条测试变红有两种可能:代码坏了,或测试瞎了**——只看红绿分不出,所以派生式守卫替掉手敲清单时要问的不是"旧的红还在吗",而是**"那条性质现在由谁直接钉住"**。③ **别把 node-ID 差分当覆盖率证明**——稀疏测试是"同一 node ID、断言变多",差分数的是测试个数不是性质。另:**派生式守卫比手敲清单更容易空过**(来源为空 ⇒ 一个断言都不执行、直接 PASS)⇒ 必须配非空断言,且**注定将来会变红的测试要自带"何时该红、那时该做什么"**。 + - **过程(第四次协议复发,lead 的新变体)**:把 S1/S2 **追加**给评审后**没有更新"什么算收工"的判据** ⇒ 评审为**原条目**报的"收工"被读成"全部完成",**返工与新一轮评审被并行派发**(评审原话:"同一个失效模式换了条路径")。规则再补两条:**追加条目必须同时说清新的收工判据**;**派返工前先确认评审没在同一棵树上开工,两者必须串行**。评审自加一条:**收工声明写死覆盖了哪些条目**。⚠️ **评审自报一次 `cd` 错误**(后台命令不继承前一条 `cd` ⇒ 它的"最终全量"跑在主 checkout、一度误报"树在我脚下变了")——**代价为零,而且正是那次误跑暴露了 `main` 变红**。评审在 E5 上换了个答法值得记:它无法审计"数字是否来自重跑那一轮",于是**自己重写全部八条从干净树重跑,让那个前提变得无关紧要**——**当一个前提只能靠对方的话支撑时,最强的动作是让结论不再依赖它。** + - gates(lead 独立重跑):pytest **2705 passed / 1 skipped**(node-ID 2691 → 2706,dropped 3 / added 18;**三条 dropped 经评审双向核过**:更强方向 + "没丢"方向)/ ruff clean / **锚 0.9600·0.8408 未动** / 32/32 configs / `--verify` 77/77 @ 45c14aa / secret 0 / 冻结基线自 2026-07-25 起 **0 写入** / **跑完 demo 后真实 store 的 `momentum_20` mtime 仍停在 11:33 ⇒ demo 零写入**。 diff --git a/docs/progress/pr_ledger.md b/docs/progress/pr_ledger.md index b87a335..ab7e85d 100644 --- a/docs/progress/pr_ledger.md +++ b/docs/progress/pr_ledger.md @@ -24,3 +24,6 @@ | #115 | D5 C6 改造:三个重生成工具退役为 verify-only + CLI 收敛 + R16 核销 | | #116 | D5 C6 删除:删 11 个旧 runner + 10 测试,校验接进 C5 harness 三个读入点 | | #117 | 进度文档(D5 C6) | +| #118 | D6a:phase0/phase2 切 FactorService(含 B1 demo-store 隔离、日频 universe 无关守卫、caller 派生普查) | +| #119 | 修 retired-invocation 守卫的射程:文件清单从 git 派生而非目录遍历(`main` 曾红而 worktree 绿) | +| #120 | 进度文档(D6a) |