Files
Jyotisha/docs/tasks/TASK-rectification-cross-midnight-dasha-fix-20260920.md
T
Jesse_ChenandClaude Opus 5 f09f3d809a docs(tasks): 把 F2 的 engine_version 聚合缺陷查清并定序
复核结果写进 F2:engine_version 一个字段被两种含义共用——started/failed
行写前端常量(部署声称的版本),completed 行写实际产出结果的版本(命中
旧缓存即旧版本)——而聚合用与版本先后无关的字符串 max 跨行取值。

该缺陷此前一直撞对:改动前 started 写 rectification-v5,短于 completed
的 ...scoring-7,max 恰好选中正确的那行。aa46da10 把 v9EngineVersion()
缺省改成 ...scoring-8 之后,started 行在字符串序上反超,回执遂显示第 8 版
而分数实际来自第 7 版缓存。已核部署未设该环境变量,走缺省,是真实行为。

第二个缺陷:max("...-10","...-9") 实跑得到 "...-9",聚合在第 10 版静默
反向,当前第 8 版。

定序 A 先上(started/failed 不再写版本,应用层)、B 兜底(聚合改取成功
结果那一行,只改函数体)、C 拆列本单不做;第 10 版前必须解决。A 的已知
漏洞要求实测取证,不得以「应该不会」结案,并新增一条针对版本 10 反向的
回归。

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

138 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# TASK · 跨午夜修复验收补单:缓存与结果身份(2026-09-20)
> 状态:**待领取,四项全部可开工**(2026-09-20 产品放行,F3 策略已于同日拍板选 b,见 §3.2)。
> 本单是 BUG-981 的**端到端验收阻塞项**(F3 调用链已查证,见 §0):核心修复合入后,新建 / 无缓存 / 证据变化的 Case 走已修路径,但证据未变的历史跨午夜时段缓存命中仍返回修复前分数。在本单闭环前,对外只能说「新算的会对」,**不得声称跨午夜问题已修**。
## 0. 基线与串行依赖
- **2026-09-20 F3 已查证:BUG-984 是 BUG-981 端到端验收阻塞项。** 真实调用链为 `scoreAndPersistCurrentEvidence()` → `runV9BlockScan()` → POST `/api/rectification/v5/block_scan` → `_compute_rectification_v5_block_scan()` → `api_service.block_scan()` → `score_candidates()` → `build_event_contribution_matrix()` → `merge_transition_proximity()`。后端时段支持率消费这条链的候选分数,不是另一套打分。证据行号及边界见 `PROGRESS-rectification-cross-midnight-gate-fix-20260920.md` 的 F3。
- 影响限定为证据未变的历史跨午夜时段缓存命中,且尚未触发三轮转分钟;新 Case、无缓存或证据变化会进入已修 helper。因此不能说整个时段路径都没有修复,也不能在旧缓存缺口未闭环前宣称 BUG-981 端到端完成。`late_night` 的 `23:00–03:59` 跨日;`unknown` 的 `00:00–23:59` 虽超过 120 分钟但本身同日,后续选择跨午夜时段才进入跨日触发范围。
- 串行顺序维持:门禁修复单(BUG-985)→ 原核心修复合入 staging → 本缓存补单。上述查证不构成缓存策略或 SQL 实施授权。
- 原任务基线 `origin/staging = 03cba478`,前轮代码 `932f2fff`;待原任务最终提交后以其 SHA 为本单执行基线,不以未经确认的部署状态推导基线。
- 依赖 `TASK-rectification-cross-midnight-dasha-20260920.md` 完成后串行执行。将涉及 `frontend/src/lib/rectification-agentic/v9/score-persist.ts`、回执查询及测试;不得与原任务 `engine-client.ts` 版本更新并行改同一文件。
- 建议 worktree `.worktrees/rectification-cross-midnight-fix-20260920`,分支 `codex/rectification-cross-midnight-fix-20260920`。
## 1. 事故实证(按符号定位)
1. `scoreAndPersistCurrentEvidence()` 的 `block_scan` 分支先比较 `evidenceLedgerFingerprint`,命中即返回 `cached:true` 与缓存原 `algorithmVersion`;该返回发生在 `readV9EngineScoringIdentity()` 之前。跨午夜 late-night 时段可进入此分支,后端真实重算也确实会走本轮修复的 helper。故即使版本接口正常、后端已升级,旧时段分数仍可能被复用。
2. `rectification-v9-tools.ts` 的 compare started 回执使用默认 engineVersion,成功回执使用实际 `persisted.algorithmVersion`。现有 `20260902020000_rectification_tool_activity_timing.sql` 的回执聚合用 `max(tr.engine_version)`,不是成功结果的身份。新版开始标记与旧缓存成功标记并存时,聚合可显示新版,不能证明新版重新算过。
3. minute 分支已有算法身份相等缓存门,但 `readV9EngineScoringIdentity()` 可优先环境覆盖;仅有部分配置或版本接口失败时,不能保证实际旧缓存失效。不得把这些既有降级条件写成无条件安全。
## 2. 根因
- 时段与分钟评分缓存的身份条件不一致。
- 聚合将运行阶段版本与计算结果来源版本混为同一可取字符串最大值的字段。
- 任务书最初“全仓只写不比”的假设不成立;同步 bump 后端算法身份是必要但非充分条件。
## 3. 决策记录
产品原授权 C 要求新旧结果可区分,并禁止改 Skill 版本、V4 input contract 和新增历史打开相等门。原核心修复轮已同步既有后端算法身份和前端默认值,没有改缓存政策或数据库。
**2026-09-20 产品放行本单**(Claude review `25232ce4` 通过、F3 调用链查证完成之后)。已定与未定分列如下。
### 3.1 已定(可直接执行)
**所有评分缓存复用必须有实际计算身份;成功回执展示成功结果来源,不用开始阶段覆盖;无法取得可信当前身份时,不把旧缓存标为当前已验证结果。**
由此可直接开工的是 F1(先红测时段旧缓存)、F2(成功回执身份)、F4(回归、记录、部署)。这三项不依赖 §3.2 的未决点。
### 3.2 已定:版本接口取不到可信身份时,旧缓存**只读展示并标注**(2026-09-20 产品拍板选 b)
`readV9EngineScoringIdentity()` 取不到可信当前身份时:
- **不拒绝、不强制重算**(否决 a)。理由:时段扫描是重计算且受 `JYOTISH_HEAVY_COMPUTE_CONCURRENCY`(默认 2)限流,把一次外部接口抖动放大成用户侧长等待,代价与本单要防的风险不对等。
- **不照常复用**(否决 c,与 §3.1 直接冲突)。
- **采用 b**:旧结果可以**只读展示**,但必须显著标注为「按旧算法产出」,且**不得被当作当前已验证结果**。
b 的三条边界,违反任一条即本单不通过:
1. **只读结果不得进入采用 / 确认路径。** 它不能被 `accepted`,更不能被 `confirmed`;交付卡、候选选择等任何写入入口都不得接受它。
2. **标注必须是用户可见的,不是只写进回执。** 文案先对照 `frontend/docs/VOICE.md`;若涉及界面呈现,同一提交内更新 `frontend/DESIGN.md`(`AGENTS.md` §4)。
3. **不得因为「只读」就放宽身份记录**:回执里该结果的来源身份仍必须是产出它的那个版本,不得被开始阶段的当前版本覆盖(这与 F2 是同一条原则)。
**SQL 迁移**:执行顺序已定为 **A 先上、B 兜底、第 10 版前必须解决**,展开见 F2。A 是应用层改动,不需要迁移;是否升到 B(改聚合函数体)由 F2 的实测结论决定,**不得跳过 A 直接开迁移**。若判定需要 B,按 §4 必须向后兼容当前已部署版本并真跑 `npm run test:db`(需 Docker,无 Docker 即写成环境缺口,不得宣称通过)。方案 C(拆列)本单不做。
## 4. 硬红线
- 不重标旧结果为新算法,不删除历史回执/缓存来假装完成迁移。
- 不 bump Skill,不改 V4 input contract,不引入按 engineVersion 拒绝打开历史会话。
- 不调打分常数,不改确认门,不把已曝光重跑当独立盲测。
- 如新增 SQL 迁移必须向后兼容当前已部署版本,真跑标准 DB 测试;不得原地改已应用迁移。
- 不顺带修聚类跨度/凌晨日期锚点;它们分别为 BUG-982/983,需独立明确范围。
## 5. 任务分解与验收
### F1 · 先红测时段旧缓存
使用真实引擎脱敏 golden 构造旧算法时段缓存,同证据指纹、当前版本接口返回新身份。旧实现必须复现无网络重算仍返回旧结果;新实现不得把旧缓存作为当前分数复用。minute、block_scan 两条都覆盖,当前身份相同的正确缓存仍可命中。
### F2 · 成功回执身份
#### 事实(Claude 2026-09-20 在 `25232ce4` 上复核)
同一个 `engine_version` 字段被两种含义共用,而聚合用的是与版本先后无关的字符串 `max`:
| 写入点 | 值的来源 | 真实含义 |
| --- | --- | --- |
| `rectification-v9-tools.ts` 的 `started` 回执(compare / diagnostics)与 `failReceipt` | `v9EngineVersion()`,即前端常量 | 「当前部署声称是哪个版本」 |
| `completed` 回执 | `scored.persisted.algorithmVersion`(compare)、`diagnostics.algorithmVersion` | 「实际产出这个结果的版本」——命中旧缓存时就是旧版本 |
`frontend/supabase/migrations/20260902020000_rectification_tool_activity_timing.sql` 用 `max(tr.engine_version)` 跨该 turn 的全部回执行取值,不区分上表两种含义。
**这个缺陷此前一直存在,但恰好撞对;`aa46da10` 之后才变成真错误:**
| 时期 | started 行 | completed 行(命中旧缓存) | `max` 选中 | 正确性 |
| --- | --- | --- | --- | --- |
| `aa46da10` 之前 | `rectification-v5` | `rectification-v5-matrix-scoring-7` | `…-7` | 碰巧正确 |
| `aa46da10` 之后 | `rectification-v5-matrix-scoring-8` | `rectification-v5-matrix-scoring-7` | **`…-8`** | **错**:回执显示第 8 版,分数实际来自第 7 版缓存 |
`aa46da10` 把 `v9EngineVersion()` 的缺省从 `rectification-v5` 改成 `rectification-v5-matrix-scoring-8`(`engine-client.ts`),于是 started 行在字符串比较里反超了 completed 行。已核 `deploy/` 与 `.gitea/` **未设** `RECTIFICATION_ENGINE_VERSION`,走的就是这个缺省——**是真实行为,不是假设**。
**第二个缺陷**:字符串 `max` ≠ 版本新旧。实跑验证 `max("…scoring-10", "…scoring-9") = "…scoring-9"`。**该聚合在第 10 版会静默反向**,当前是第 8 版。
#### 执行顺序:A 先上、B 兜底、第 10 版前必须解决
**A(应用层,本单必做)**:`started` 与 `failed` 回执不再写 `engine_version`,只有 `completed` 写。消除最主要的错误来源,不需要迁移。
**A 的已知漏洞,必须实测而不是假设**:同一 turn 内若出现**多个 completed 行且版本不同**(例如 compare 命中旧缓存记 `…-7`、diagnostics 重新算记 `…-8`),`max` 仍会挑错。F2 必须给出这种组合**是否会真实出现**的证据:能构造出来就升 B,构造不出来就在进度记录里写明判定依据与边界,不得以「应该不会」结案。
**B(改聚合函数体,兜底)**:把 `max(engine_version)` 改成取该 turn **成功结果那一行**的身份(例如最近一条 `status='completed'` 且 `engine_version is not null` 的行),不再跨含义取最大值。只改函数体、不改表结构,向后兼容。
**C(拆成两列:部署版本 / 结果出处)本单不做**,但「第 10 版反向」给 B 定了时限:迟早要改,不应拖过第 10 版。若 F2 判定必须升 B,顺带把版本比较不再依赖字符串序这件事一并解决。
#### 验收标准
- 覆盖 started 新版本 / completed 旧缓存版本、diagnostics、失败重试与历史回执。
- 用户看到的成功结果身份必须来自**该成功结果**,不能由字符串 `max` 决定。
- 必须有一条针对「第 10 版反向」的回归:构造 `…-9` 与 `…-10` 两行,断言聚合结果不是按字符串序挑的。
- 历史回执保持原值,旧会话仍可打开(`BUG-621` 回归必跑)。
- 若升 B:新增兼容迁移、不原地改已应用迁移、真跑 `npm run test:db`。
### F3 · 失败与覆盖边界(策略已定为 b,见 §3.2)
分别测试无环境覆盖、完整覆盖、部分覆盖、版本接口超时 / 错误四种状态。每种状态不得混淆当前身份与来源身份。
验收标准:
- 四种状态各有定向用例,断言身份取不到时走**只读展示**而非重算或静默复用。
- **必须有一条用例断言:只读结果调用采用路径会被拒绝。** 不是「界面上没有按钮」,是服务端拒绝——参照本仓既有做法,入口靠删不靠藏。
- 标注文案在用户可见层出现,且与 `frontend/docs/VOICE.md` 对照过;涉及界面则同一提交更新 `frontend/DESIGN.md`。
- 只读结果的回执来源身份仍是产出它的版本,不被开始阶段版本覆盖。
### F4 · 回归、记录与部署
前端 tsc/lint/相关与全量测试、build Static/gzip;数据库按需;BUG-621 历史打开回归必跑。更新 BUG-981 及缓存身份独立记录、BLOCKED、进度与测试清单。staging 受控会话分别验证 minute 与 late-night block_scan,不用匿名健康检查替代结果身份验收。
## 6. 让步顺序
先保证不伪造结果身份,其次保证缓存只复用兼容结果,再考虑缓存命中率;界面附加信息可延期,成功回执来源正确性不可砍。
## 7. 开工前置命令
先 `git status -sb`,再 fetch 并确认原任务最终 SHA;使用独立 worktree。读取 `AGENTS.md`、错误台账、前端三份规范、BUG-621/981 及本轮缓存身份记录;运行预检。未批准前不得执行迁移或推送生产。
## 8. BUG 编号
原日期修复为 BUG-981,附带既有范围/锚点问题为 BUG-982/983。本单缓存与聚合身份问题拟用 BUG-984;开工重核最大号与并行占用,按最终 Bug History 为准。