diff --git a/docs/tasks/README.md b/docs/tasks/README.md index 1a3c1248..02ce32af 100644 --- a/docs/tasks/README.md +++ b/docs/tasks/README.md @@ -243,6 +243,7 @@ | `TASK-consultation-session-capacity-20260915.md` | — | **对话上限单(一份迁移,可并行;不碰 route.ts)**:`append_consultation_question` 的 200,000 字符额度里,`thinkingText`(≤4,000) + `thinkingSections`(实测 1,521/2,243/2,977) 占一半以上,而 `techniqueTruth`/`workflowReceipt`/`agentExecutionReceipt` 照样入库却不计入——同一条上限身兼二职且两职都没做好,约 **19 轮** 就「已写满」(200 条那档永远碰不到)。**产品定案:思考文本不计入**,额度只数用户读得到的正文(约 19 → 约 50 轮),另设一条按 `length(elem::text)` 把全部字段算全的物理上限(算式取 1,000,000,写进迁移注释)护住数据库行;两档都返回同一个 `session_full`。保留 advisory lock / 幂等 / 满员拒绝(BUG-464 防复发)。BUG 段 732 | 待领取 | — | | `TASK-freeze-metric-change-20260915.md` | — | **规则单(后面两单的前置,无 BUG 号)**:两条增长冻结余量都用完(`page.tsx` 1,951/1,951 余 **0**;`jyotish_api_server.py` 11,334/11,363 余 **29**),冻结从「逼新代码往外走」退化成「拦路」。实证:`page.tsx` 行数砍 59% 但 `Home()` 的 `useState` 从 56 涨到 **66**(拆的是代码不是状态);api server **225 个类方法只有 12 处真碰 HTTP 上下文**,4 处 `__new__` 伪造空壳就是这么来的。**产品拍板换口径**:主门改成「`Home()` 的 useState/useRef 不得增长」与「类方法数 + `__new__` 计数不得增长」,行数降级为粗护栏;**同时推翻 §6「参数式 hook 内部保持 0 个 React hook」**(那正是状态搬不走的原因)。改 `AGENTS.md` §6 + 两个合同测试,不碰业务代码 | 待领取 | — | | `TASK-home-state-lowering-20260915.md` | — | **page.tsx 状态下沉第一簇(串行在 freeze-metric-change + C2 + R3 之后)**:66 个 state 里 `rectification*` 占 **15** 个,而它们服务的 `` 本来就是 `dynamic()` 懒加载子树、挂着 24 个 props;`useRectificationSurface` 要解构约 56 个参数。把这簇搬进子树,`Home()` 的 useState 从 66 降到 ≤ 53。**零行为变化**;第一步必须先把 15 个逐个分类(只服务子树 / 外壳也要读)。产品否决了 Context Provider 与外部 store 两条路。不占 BUG 号 | 待领取 | — | +| `TASK-rectification-engine-memoization-fix-20260915.md` | — | **验收修复单(只改测试,一行实现不许动)**:BUG-721 的实现**等价性成立**(我在改前 `6b3248bf` / 改后 `e4788dfc` 同机跑同一 payload,`candidate_scores` 逐字相同),9 条计数断言全过;但等价 golden 在本机复现不出来——4 处浮点尾数差(score 1.0e-4 ×2、`margin_percent` 1.1e-3 ×2)。**复发自 BUG-712**(「不得对全精度浮点做整体 `==`」,那一单只落在 ephemeris 一处)。而 `tests/test_rectification_*.py` 在 `CORE_PYTEST_TARGETS` 里,**staging 门禁靠机器舍入碰巧一致才是绿的**。修法:主证据换成**同进程差分**(把 static context 的四个缓存键置 `None` 即可回退旧路径,A/B 严格相等),golden 降为离散字段严格相等 + 浮点带容差(容差按实测 1.1e-3 推);**禁止重建 golden 来「修」**。另含六份 golden 的仓库级排查。BUG-733 | 待领取 | — | ## 命名与归档 diff --git a/docs/tasks/TASK-rectification-engine-memoization-fix-20260915.md b/docs/tasks/TASK-rectification-engine-memoization-fix-20260915.md new file mode 100644 index 00000000..bb75ac9f --- /dev/null +++ b/docs/tasks/TASK-rectification-engine-memoization-fix-20260915.md @@ -0,0 +1,162 @@ +# TASK · 验收修复单:等价 golden 跨机不稳,门禁靠运气绿 + +- 日期:2026-09-15 +- 基线 commit:`origin/staging` @ `e4788dfc`(实现落在 `53a37ce9`) +- 执行分支:`codex/rectification-engine-memoization-fix-20260915` +- 来源:Claude 对 `TASK-rectification-engine-memoization-20260915`(BUG-721)的验收 +- 落点:`tests/test_rectification_engine_memoization.py`、必要时 `tests/golden/`、`docs/BUG_HISTORY.md` +- 规模:只改测试。**一行实现代码都不许动。** + +--- + +## 1. 验收结论摘要 + +BUG-721 的**实现是对的,等价性成立**,我独立复核过。门禁实测(基线 `11893c7f` → 头 `e4788dfc`,含 R1/R2/R3 三个提交): + +| 项 | 结果 | +| --- | --- | +| `tsc --noEmit` | 0 错 | +| `npm run lint` | **0 error** / 119 warning(全部既有) | +| 全量前端套件 | 基线 3291(fail 31)→ 头 3304(fail 31),**失败清单逐条一致** | +| `next build` | exit 0;`/` 仍 `○ Static` | +| 首屏 JS gzip-9 | 基线 130,872 B → 头 130,872 B,**0.00%**(两侧同一种量法) | +| `tests/test_rectification_*.py` | **1 failed, 181 passed** | + +唯一那条失败就是本单要修的:`test_score_candidates_matches_baseline_golden`。 + +**它不是实现改坏了。** 我在改前(`6b3248bf`,与 golden 自称的 `a8d29d1b` 代码相同)和改后(`e4788dfc`)两个工作树上跑同一份 payload: + +| | 12:00 | 12:01 | 12:02 | +| --- | ---: | ---: | ---: | +| 本机 **改前** | 8.6273 | 8.6226 | 8.1701 | +| 本机 **改后** | 8.6273 | 8.6226 | 8.1701 | + +**改前改后逐字相同。** 本单的其余 9 条断言(shadbala / ashtakavarga / vimshottari / narayana 各等于候选分钟数;过境盘等于去重后事件日期数;探针默认 1 次、refresh 时 2 次)全部通过。 + +## 2. 事故实证 + +失败来自 golden 文件与**任何一台机器**的实际输出对不上: + +| 字段 | 本机(改前=改后) | `tests/golden/rectification_engine_memoization_v1.json` | 差 | +| --- | ---: | ---: | ---: | +| `candidate_scores[0].score` | 8.6273 | 8.6274 | 1.0e-4 | +| `candidate_scores[1].score` | 8.6226 | 8.6227 | 1.0e-4 | +| `decision_receipt.margin_percent` | 5.2995 | 5.3006 | 1.1e-3 | +| `decision_receipt.gates.diagnostic_quality.margin_percent` | 5.2995 | 5.3006 | 1.1e-3 | + +一共 4 处,全是浮点尾数。`candidate_scores[*].score` 本身已经被引擎 `round(..., 4)` 过,差值正好是那一位的 1 ulp——也就是求和顺序/libm 舍入的跨机差异,不是算法差异。 + +**后果是实打实的**:`tests/test_rectification_*.py` 这个 glob 在 `scripts/run_quality_gate.py` 的 `CORE_PYTEST_TARGETS` 里(注释原文:「Auto staging gate is `--profile quick`…so a stale window_scan assertion in this glob stayed red on origin/staging until listed here」)。也就是说 **staging 自动门禁会跑这条断言**。今天它在 CI 上绿,只是因为那台机器的舍入和执行方的机器一致;换一台机器、换一个基础镜像、换一次 libm 版本就会红。**这条门禁现在是靠运气绿的。** + +## 3. 根因 + +golden 存了全精度浮点并做整体 `==`。这正是 **BUG-712** 的形状,那条记录的防复发一字不差地写着: + +> 不得对全精度浮点做整体 `==`。测试不得写 golden。 + +BUG-712 修的是 `ephemeris_events` 的 golden(`longitude` / `speed_longitude` 量化到 6 位再比,非浮点字段保持严格相等)。那一单的范围写死在星历端点上,新写的这份 rectification golden 是同样的写法,没被扫到。 + +更深一层:**golden 本来就不是证明「记忆化没改结果」的合适工具。** golden 证明的是「今天的输出等于某台机器某一天的输出」,中间夹了一个与被测命题无关的变量(机器)。被测命题其实可以在**同一个进程里**证明——见 §5.1。 + +## 4. 决策记录 + +产品 2026-09-15 授权本单: + +1. **不回滚 BUG-721 的实现。** 等价性已由改前/改后同机对比独立证实,实现留在 staging 上。 +2. **本单只改测试。** 一行 `scripts/` 下的实现代码都不许动——不得为了让断言通过去改业务代码(这是产品的既定偏好)。 +3. **不得用「重新生成 golden」来修。** 在本机重跑一次 `write_golden()` 能让测试变绿,但那只是把不稳定性换个方向藏起来,下一台机器照样红。**这条是硬红线,见 §5.3。** +4. **防复发升级为仓库级。** BUG-712 的那句话从单点措施提升成对所有 golden 生效的规则,并配一条能自动发现违例的守卫。 + +## 5. 任务分解 + +### 5.1 把主证据换成同进程差分(首要,也是本单真正的价值) + +实现已经把四层不变量放进 static context 的具名键(`ashtakavarga_result` / `shadbala_result` / `vimshottari_timeline` / `narayana_periods`),而每个消费点都有 `if result is None:` 的回退分支。**这意味着记忆化可以在同一个进程里关掉**:把 context 里那四个键置 `None`,消费点就会退回逐次计算的老路径。 + +新增一条断言:同一份请求、同一个进程, +- A:正常的 static contexts(带四层缓存) +- B:把那四个键全部置 `None` 的同一批 contexts(回退到逐次计算) + +断言 `compute_event_candidate_rows` 在 A / B 下的输出**严格逐字相等**。 + +这条证明不经过任何跨机变量,是比 golden 强得多的等价证据。 + +- 验收:新断言通过;人为把某一层的缓存值替换成错误对象后必须红(贴反向验证,证明这条断言不是恒为真)。 +- 验收:A / B 两条路径确实走了不同分支(用调用计数确认 B 的 `calc_shadbala` 次数显著高于 A),否则等于什么都没测。 + +### 5.2 golden 断言改成分档比较 + +保留 golden(它仍有价值:能发现跨版本的大幅漂移),但改比较方式: + +| 字段类别 | 比较方式 | +| --- | --- | +| 离散字段(`time`、`supporting_event_ids`、`conflicting_event_ids`、各 gate 布尔、`overall_confidence`、`representative_time`、排序) | **严格相等** | +| 浮点字段 | 容差比较 | + +容差必须由实测漂移推出来,不得拍脑袋:本次实测最大漂移 **1.1e-3**(`margin_percent`)。取一个有余量但仍能发现真回归的值(建议绝对 2e-3 与相对 5e-4 取更宽者),并把「这个数怎么来的」写进测试注释。 + +- 验收:把 golden 里任意一个浮点改动 1e-2,断言必须红(贴反向验证)。 +- 验收:把 golden 里任意一个离散字段改掉,断言必须红。 +- 验收:`.venv/bin/python -m pytest tests/test_rectification_*.py` 在本机 **0 failed**。 + +### 5.3 不得重建 golden + +`write_golden()` 这个辅助函数留着没问题,但**本单不得调用它更新那份 golden 文件**。若执行方认为必须重建,要在进度记录里写明理由并说明为什么不是在掩盖跨机不稳——默认答案是「不重建」。 + +- 验收:`git diff` 里 `tests/golden/rectification_engine_memoization_v1.json` **无改动**(若有改动,按上一条给出理由)。 + +### 5.4 仓库级防复发守卫 + +`tests/golden/` 下现在共 6 份:`consultation_contract_keypaths_v1.json`、`ephemeris_events_raman_20260915_90d.json`、`golden_cases.json`、`qizheng_stem_branch_19900409.json`、`rectification_engine_memoization_v1.json`、`upstream_sync2/`。 + +- 逐份检查有没有「全精度浮点 + 整体 `==`」的比较方式,结果列进进度记录(每份写明:有/无、在哪个测试里比的、怎么比的)。 +- 发现同类问题的,本单**只记录不修**(各自另开单),除非改动小到一眼可见。 + +- 验收:六份的检查结论在进度记录里,一份不漏。 + +### 5.5 Bug 历史 + +同一变更内写进 `docs/BUG_HISTORY.md`,预占 **BUG-733**(721–732 已被两轮审计的七单占用,避让)。必须写明: + +- **复发自 BUG-712**,并说明为什么没拦住:BUG-712 的防复发只落在 `ephemeris_events` 那一处,没有仓库级守卫,新写的 golden 重蹈覆辙。 +- 状态可写 `resolved`,但**证据必须是 §5.1 的同进程差分 + §5.2 的两次反向验证**,不能只写「现在绿了」。 +- 防复发升级为:**任何 golden 比较都不得对浮点做整体 `==`;浮点必须量化或带容差,容差数值要有实测依据并写在注释里;能在同进程内做差分证明的命题,不得用跨机 golden 代替。** + +## 6. 让步顺序 + +1. 5.1 **不得砍**——它才是这一单的意义;没有它,5.2 只是把红灯调成绿灯。 +2. 5.2 必须做。 +3. 5.4 可以只做「检查并记录」,修留到后续单。 +4. 5.3、5.5 不得砍。 + +## 7. 开工前置命令 + +```bash +git fetch origin --prune +git worktree add -b codex/rectification-engine-memoization-fix-20260915 \ + .worktrees/rectification-engine-memoization-fix-20260915 origin/staging +cd .worktrees/rectification-engine-memoization-fix-20260915 +git status -sb | head -1 +# 先复现:这一条应当是红的 +.venv/bin/python -m pytest tests/test_rectification_engine_memoization.py -q +``` + +开工前必读:`docs/BUG_HISTORY.md` 的 **BUG-712**(同形态前例与它的修法)与 **BUG-721**(本单要保护的那次改动)。 + +验收命令: + +```bash +.venv/bin/python -m pytest tests/test_rectification_engine_memoization.py +.venv/bin/python -m pytest tests/test_rectification_*.py # 门禁 glob,须 0 failed +.venv/bin/python scripts/run_quality_gate.py --profile quick +``` + +## 8. BUG 编号起点 + +基线 `e4788dfc` 上 `docs/BUG_HISTORY.md` 最大号为 **BUG-725**。721–732 已被两轮审计七单预占(726–732 尚未落库),本单避让,预占 **BUG-733**。开工时核对实际最大号。 + +## 9. 不在本单范围 + +- BUG-721 的实现(已验收等价,不动) +- 其余五份 golden 的修复(5.4 只检查并记录) +- `CORE_PYTEST_TARGETS` 的构成(`tests/test_rectification_*.py` 这条 glob 保留,本单是让它变得可靠,不是把它摘掉)