Files
Jyotisha/docs/tasks/TASK-rectification-cross-midnight-dasha-20260920.md
T
Jesse_ChenandClaude Opus 5 03cba4780a docs(tasks): 跨午夜单产品拍板放行 + 更正决策 C 的挂载点
产品 2026-09-20 就三点拍板:A 修;B 修完重新冻结并重跑 T1/T2;C 让修复
前后的结果可区分。任务书 §3 三个待决点改为已决,状态转待领取。

更正 C 的做法。原提案是改 calculation_spec 并 bump INPUT_CONTRACT_VERSION,
但查证发现 calculation_spec_hash 的消费方全在 V4 链路,生产跑的 V9 对它零
引用,改了对真实用户历史无效。改为随修复 bump engine_version:它取自
v9EngineVersion(),全仓只写不比、无任何相等性门控。明令不得动 skill_version,
BUG-621 就是 open RPC 的 Skill 版本绑定让历史校正打不开。

§1.6 同步重写为 V9 的真实身份字段表;T4 改为 engine_version bump,验收加
一条历史会话仍能打开的定向回归。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0199rbQDTsUbCVw84wc8BTFe
2026-09-20 12:28:44 +08:00

15 KiB
Raw Blame History

TASK · 跨午夜候选的 Dasha 边界错一天(生产打分,2026-09-20)

状态:待领取。本单会改动生产打分结果,与 TASK-rectification-validation-integrity-20260920 的「不改打分」红线相反;产品已于 2026-09-20 就三点全部拍板放行,见 §3。 起因:执行 BUG-979 偏差评测时,独立审查在离线适配层发现该问题;Claude 2026-09-20 验收时在生产调用链上独立复现。

0. 基线与交付

  • 基线:origin/staging = 932f2fff(2026-09-20 实测核对)。
  • worktree .worktrees/rectification-cross-midnight-20260920,分支 codex/rectification-cross-midnight-20260920。
  • 改动落点:scripts/rectification/**、tests/**,可能含 references/rectification_sealed_holdout.v1.json 与 docs/research/**(取决于 §3 决策 B)。全部在 deploy/gated-paths.txt 内,推 staging 会触发门禁并重新发布镜像。
  • 串行依赖:本单改的 scripts/rectification/scoring_service.py 与 dasha_transition_proximity.py 是 frozen_scoring.files 之外的文件,但改动会让 932f2fff 落盘的 T1/T2 数字失效(§3 决策 B)。开工前确认没有别的会话正在改同两个文件。

1. 事故实证

基线 932f2fff,符号定位。

1.1 缺陷位置

scripts/rectification/scoring_service.py 的 build_event_contribution_matrix() 在调用 merge_transition_proximity() 时,只传一个 scoring_request["birth_date"]:

merge_transition_proximity(matrix_payload, scoring_request["events"],
                           static_contexts, scoring_request["birth_date"], ...)

scripts/rectification/dasha_transition_proximity.py 的 merge_transition_proximity() 把这个值用于全部候选:vim_key = (birth_date, moon, lo, hi)、_vim_start_dates(birth_date, ...)、_narayana_start_dates(..., birth_date, ...)。候选之间只靠 _context_time() 取出的 HH:MM 区分,日期被丢掉。

因此当搜索窗跨午夜时,午夜之后的候选仍按窗口起始日计算 Vimshottari / Narayana 起始日期,全部 dasha 边界整体错一天,事件邻近度得分随之错。

对照:同模块的静态星盘层读 candidate_at,日期是对的。只有 transition-proximity 这一个 helper 吃单一 birth_date(scripts/research/reported_offset_sweep.py 的 score_window() docstring 已记录这一边界)。

1.2 实测复现(Claude 验收时独立执行,基线 932f2fff)

取公开 AA 案例,把窗口人为挪到 23:50 → 00:10(21 个候选,其中 11 个跨日),同一组 static_contexts 分别走生产路径与按候选日期分组的正确路径:

项 实测
分数不同的候选数 11 / 21
分数相同的候选 10 个,全部是同日候选
偏移幅度 00:00–00:04 为 +0.0267;00:05–00:10 为 -0.0133
本例头名是否改变 否(00:10 两侧一致)

错的恰好就是那 11 个跨日候选,同日候选一个都不差 —— 诊断精确坐实,不是浮点噪声。

1.3 上界是算术推的,不是实测

dasha_transition_proximity.py 常数:DAY_KERNEL_DAYS = 15 / DAY_MAX_POINTS = 1.0,MONTH_KERNEL_DAYS = 45 / MONTH_MAX_POINTS = 0.35,VIM_SHARE = 0.6 / NARAYANA_SHARE = 0.4。

差一天导致的单事件最大偏移 = cap / kernel_width:day 精度 1.0/15 ≈ 0.067 分,month 精度 0.35/45 ≈ 0.008 分。

真实会话每例 5–18 件事。若多数为 day 精度且同向偏移,累计上界可达 ≈1.2 分。对照 docs/research/rectification_minute_resolution_closure_2026_09_14.md 实测的随分钟变化项总量约 2.1 分,这个量级足以改变头名与并列关系。1.2 分是算术上界,不是实测值;1.2 节的实测样本只有 3 件事、以 month/year 精度为主,所以只错了 0.03。真实影响幅度本单必须实测,不得沿用任何一侧的数字。

1.4 谁会踩到

窗口跨午夜的真实路径(均已在 Bug 历史登记):

  • period_only 的 late_night 时段映射为 23:00–03:59(见 BUG-3315 所在记录)。
  • unknown 映射为 00:00–23:59。
  • 申报时间在 23:45 之后、或 00:15 之前,默认 ±15 分钟窗即跨午夜(frontend/src/lib/rectification-agentic/v9/search-window.ts 的 ENGINE_SEARCH_RADIUS_MINUTES = 15);放宽到 ±30/60/120 后覆盖面更大。

1.5 记录现状

该事实目前只写在 BUG-979 的「独立审查追加」一行与 BLOCKED.md 里,没有独立编号、没有 investigating 状态。BUG-979 自身是 resolved,其正文已声明「resolved 仅指离线缺少偏差维度的缺口……不代表生产跨日问题已修」。按 AGENTS.md §5.4,已确认事实必须有自己的记录与状态。

1.6 生产结果没有打分实现身份(连带发现,2026-09-20 修正)

先更正一条:scripts/rectification/scoring_service.py 的 calculation_spec() 与随结果落库的 calculation_spec_hash,其消费方全部在 V4 链路(frontend/supabase/migrations/20260726020000_birth_time_rectification_v4.sql、frontend/src/mastra/rectification-tools.ts)。生产跑的是 V9,V9 对 calculation_spec_hash 零引用(已全仓检索确认)。因此「bump INPUT_CONTRACT_VERSION 以区分新旧结果」对真实用户历史无效,不要做。

V9 的真实情况:

字段 来源 是否被相等性门控
skill_version Skill 包版本 是。BUG-621:open RPC 要求绑定 Skill 等于当前版本,bump 会让历史校正打不开
engine_version frontend/src/lib/rectification-agentic/v9/engine-client.ts 的 v9EngineVersion()=环境变量 RECTIFICATION_ENGINE_VERSION,缺省 "rectification-v5" 否。全仓只写不比,无任何 === / SQL 相等判断
打分实现身份(如 12 文件的 implementation_sha256) 不存在 —

所以修复打分后,V9 历史结果与新结果在回执里没有任何可区分的标记。这与 BUG-427 的防复发条(「评测指标必须与产生它的 implementation_sha256 绑定记录」)是同一类问题,只是发生在生产结果侧而非评测侧。

2. 根因

  1. merge_transition_proximity() 的接口把「出生日期」当成窗口级常量,而不是候选级属性。候选身份在 _context_time() 处被压成 HH:MM,日期信息在进入该函数时就已丢失。
  2. 全仓没有一条跨午夜窗口的打分层回归。BUG-098 记录里「跨午夜兼容」指的是逐分钟枚举,transition proximity 是后加的层,没有被那条覆盖。tests/ 下无任何 transition / proximity 命名的测试文件。
  3. 生产结果的可复现性只绑定输入规格,不绑定打分实现身份,所以打分变更不会触发任何告警。

3. 决策记录

2026-09-20 产品负责人已就三点拍板,本单可以开工。

  • A. 修不修?→ 修。 修复会改变跨午夜窗口下所有候选的分数,可能改变头名、并列关系与交付区间。产品接受这一影响:它是确定性错误,不是精度取舍。
  • B. 修完要不要重新冻结并重跑 T1/T2?→ 要。 932f2fff 的 900 组合与 v3 重跑成绩是用有缺陷的实现算的,修复后不再对应。重跑成本已实测:T2 约 10 秒,T1 全量 900 组合约 4 分钟。旧数字保留并标注作废原因,不删除历史。
  • C. 历史已交付 Case 怎么处理?→ 让新旧结果可区分,但挂在 engine_version 上。
    • 原始提案(改 calculation_spec 并 bump INPUT_CONTRACT_VERSION)已作废:§1.6 查证表明那条链路属 V4,生产 V9 零引用,改了对真实历史无效。
    • 采纳做法:随修复 bump engine_version(v9EngineVersion() 的缺省串,或部署侧 RECTIFICATION_ENGINE_VERSION),使修复前后的 V9 回执可区分。该字段全仓只写不比,bump 不影响历史会话打开。
    • 不得 bump skill_version(BUG-621:open RPC 要求绑定 Skill 等于当前版本,bump 会让历史校正打不开)。
    • 是否把 12 文件的 implementation_sha256 也写进 V9 回执,作为可选增强;若做,必须是新增字段,不得改动既有字段的含义或类型。

4. 硬红线

  1. 不得为了让修复看起来无影响而缩小窗口、改时段映射或回避跨午夜路径。
  2. 修复只允许改「按候选日期取 dasha 起始」这一件事。不得顺带调 DAY_KERNEL_DAYS / MONTH_KERNEL_DAYS / *_MAX_POINTS / VIM_SHARE / NARAYANA_SHARE 中的任何一个常数——那是打分权重,属 rectification_minute_resolution_closure_2026_09_14.md 已证伪的改法方向。
  3. 不得触碰确认门:status 保持 not_ready、confirmation_coverage_rate 保持 0.0、holdout_passed() 保持 False。
  4. 不得把修复后的重跑成绩写成「独立官方盲测」——BUG-428 的红线不因换实现而解除(official_valid_independent_blind 保持 false、官方试次保持 0)。
  5. 缓存键必须跟着改:vim_key / narayana_key 现在以 birth_date 为首元素,改成按候选日期后,必须确认缓存不会跨日期串用,否则修了接口没修行为。
  6. 不得写入真实用户资料;实证只用已登记的公开 AA 案例。
  7. 不顺手升级依赖、不修不在本单内的 warning。

5. 任务分解

T1 · 先写失败测试(BUG-981)

在改实现之前,新增 tests/test_dasha_transition_proximity_cross_midnight.py:

  • 构造跨午夜窗口(如 23:50–00:10),断言每个跨日候选的 dasha 起始日期等于该候选自己的日期推出的值,而不是窗口起始日。
  • 断言同日候选的分数不受修复影响(回归保护)。
  • 断言缓存不跨日期串用(红线 5)。

验收标准:该测试在修复前必须红、修复后转绿;进度记录里贴出修复前的失败输出。

T2 · 修实现

把候选日期传到底。参考实现是 scripts/research/reported_offset_sweep.py 的 score_window()(按候选日期分组、每组一次矩阵、合并后全窗排序),但那是离线适配层,不要照搬结构——生产侧应在 merge_transition_proximity() 内部按候选日期取 dasha 起始,避免多次重建矩阵带来的性能回退。

验收标准:

  • T1 全绿;tests/test_rectification_*.py、tests/test_active_rectification_*.py、tests/test_minute_rectification_*.py 与基线逐条比对,0 新增失败。
  • 非跨午夜窗口的分数逐位不变:取至少 3 个公开 AA 案例,修复前后分数字节相同。这是本单最重要的一条回归。
  • 性能不回退:同窗口打分耗时与基线相比不超过 +20%(基线实测:20 例 ±60 档 8.6 秒)。

T3 · 按决策 B 重新冻结与重跑

产品已答「要」,本项必做。

  • 重新计算 12 个 frozen_scoring.files 的实现哈希(修复若落在这 12 个文件内,哈希会变;若落在之外,需在报告里说明为何成绩仍变)。
  • 重跑 scripts/research/sealed_holdout_rerun.py 与 scripts/research/reported_offset_sweep.py,更新两份研究文档与 JSON,旧数字保留并标注作废原因,不删除历史。
  • 更新 references/rectification_sealed_holdout.v1.json 的 current_tree_scorer,status 仍为 not_ready。

验收标准:tests/test_sealed_holdout_contract_freshness.py 绿(它会因哈希漂移而红,这正是它存在的意义);两份研究文档的口径块含新哈希;official_valid_independent_blind 仍 false。

T4 · 按决策 C 给结果加可区分标记

  • bump engine_version:改 frontend/src/lib/rectification-agentic/v9/engine-client.ts 的 v9EngineVersion() 缺省串(或与部署侧 RECTIFICATION_ENGINE_VERSION 协同,二选一并在进度记录写明选了哪个及原因)。
  • 不得改 skill_version;不得 bump INPUT_CONTRACT_VERSION(见 §1.6,那是 V4 死链路)。
  • 可选增强:在 V9 回执新增打分实现身份字段。做则必须是新增字段,不得改既有字段含义或类型。

验收标准:

  • 修复后新建 Case 的回执 engine_version 为新值,历史 Case 回执保留旧值。
  • 历史校正会话仍能打开(BUG-621 的定向回归必须跑,结果贴进度记录)。
  • 全仓确认 engine_version 仍无相等性门控——若本单引入了任何按它比较的逻辑,视为越界。

T5 · 记录

  • docs/BUG_HISTORY.md 新增 BUG-981,关联 BUG-098、BUG-427、BUG-979;说明 BUG-098 的「跨午夜兼容」只覆盖逐分钟枚举、不覆盖后加的 transition proximity 层。
  • 修正 BUG-979:在正文补一行指向 BUG-981,不改其 resolved 状态(它的 resolved 本就只指离线缺口)。
  • 顺带修正 BUG-978 的状态字符串 resolved/partial —— AGENTS.md §5 只认 investigating / blocked / resolved 三种,按实际改成其中之一并把「partial」的含义写进正文。
  • BLOCKED.md 里那条跨午夜条目改为指向 BUG-981,不删除。
  • docs/tasks/README.md 状态板加一行;实现合入 staging 的同一次推送里改状态。
  • CHANGELOG.md:若决策 A 为修,则记一行(跨午夜出生时段的候选打分修正,用户可感知为候选排序变化)。

6. 让步顺序

  1. 最先保 T1 + T2:失败测试与修复本身。没有 T1 就没有证据证明修对了。
  2. 其次 T3(重跑,产品已拍板必做)。确实跑不完时,必须在两份研究文档顶部加一行「本文数字产出自已修复前的实现」,不得留着不说。
  3. T4 的 engine_version bump 不可砍(产品已拍板 C);可选的「回执加打分实现身份」可砍,砍了要在 §1.6 的事实进 BUG-981 正文。
  4. 不得砍掉 T2 的「非跨午夜窗口分数逐位不变」回归。 砍了它,这次修复就无法与打分权重变更区分开。

7. 开工前置命令

git fetch origin --prune
git worktree add -b codex/rectification-cross-midnight-20260920 \
  .worktrees/rectification-cross-midnight-20260920 origin/staging
cd .worktrees/rectification-cross-midnight-20260920
git log --oneline -1        # 必须是 932f2fff 或其后代
python3 scripts/pre_work_check.py --remote-timeout 8 --command-timeout 45

本单属 AGENTS.md §9 的引擎类任务,pre_work_check.py 必跑,并先读 docs/research/pre_work_error_ledger.md。

必读:docs/research/rectification_minute_resolution_closure_2026_09_14.md(尤其第 6 节三条提醒与「不得调打分权重」的结论)、docs/research/reported_offset_2026_09_20.md 的「初扫作废与独立复核」一节(离线侧是怎么修的、为什么不能照搬)。

环境备忘:本机无 .venv,系统 python3(3.13)可 import swisseph;frontend 无 node_modules,前端侧检查需在有依赖的环境做。

8. BUG 编号起点

docs/BUG_HISTORY.md 当前最大号 980。本单从 BUG-981 起。开工时重新核对最大号,若已被占用则顺延并在进度记录里说明。