From d575e89a835dbaf8a4bc890909b53c65cb54d89e Mon Sep 17 00:00:00 2001 From: jesse-ux Date: Sun, 20 Sep 2026 15:55:09 +0800 Subject: [PATCH] fix(rectification): stop stamping pending receipts and document aggregate blocker Implement BUG-984 F2 option A and reproduce mixed completed identities through real tools. Stop at the required SQL authorization boundary; F1/F3/F4 remain pending. Co-Authored-By: Claude Code --- BLOCKED.md | 9 +- CHANGELOG.md | 4 + docs/BUG_HISTORY.md | 5 +- docs/research/pre_work_error_ledger.md | 6 ++ ...ctification-cross-midnight-fix-20260920.md | 84 +++++++++++++++++++ docs/tasks/README.md | 2 +- ...ctification-cross-midnight-fix-20260920.md | 28 +++++++ frontend/src/mastra/rectification-v9-tools.ts | 14 ++-- ...tion-engine-version-cross-midnight.test.ts | 55 ++++++++++-- 9 files changed, 186 insertions(+), 21 deletions(-) create mode 100644 docs/tasks/PROGRESS-rectification-cross-midnight-fix-20260920.md create mode 100644 docs/testing/rectification-cross-midnight-fix-20260920.md diff --git a/BLOCKED.md b/BLOCKED.md index ec9e94d9..86673589 100644 --- a/BLOCKED.md +++ b/BLOCKED.md @@ -1,6 +1,13 @@ # BLOCKED -## 跨午夜前置修复合入:Gitea 写入认证失败(2026-09-20) +## BUG-984 补单:F2 混合成功身份实证触发 SQL 授权停点(2026-09-20) + +- A 已局部实现:started/failed 不写算法身份,completed 只用实际结果来源。真实工具 + 原生 golden 的同 turn compare/diagnostics 组合可产生 scoring-9、scoring-10 两条成功回执;字符串 max 返回 scoring-9,不是最近成功 scoring-10。该测试证明缺陷仍存在,不是聚合修复通过。 +- 既有 RPC 输出没有各 completed 的来源身份;历史 fingerprint 也不能还原。按补单须升 B(兼容函数体迁移),但当前执行授权禁止自行迁移,已停止进一步业务实施并报告。F1/F3/F4 pending;未改 SQL/UI/缓存、未推送。见 `docs/tasks/PROGRESS-rectification-cross-midnight-fix-20260920.md`。 + +## ~~跨午夜前置修复合入:Gitea 写入认证失败(2026-09-20)~~ + +- **已解除**:主会话成功将前置代码 `3f39bafc4a1fb7fc9d0a257d2528ea1a64295792` 推 staging 并 ls-remote 核对;本轮 fetch 亦看到其后纯文档 `f09f3d80`。下面保留原认证失败历史,不再作为 BUG-984 开工阻塞;部署另行验收。 - 用户授权将已 review 通过的 `25232ce4` 合入 staging,并执行 BUG-984。已同步 `a3577ce2`,本地整合提交 `b27d4de963f9ba7c4072233cabe06c6875eb0582`;缓存补单完整保留 staging 版,BUG-985 按第三环境复验改 resolved。 - `git push origin HEAD:staging` 被拒:`remote: Failed to authenticate user` / `fatal: Authentication failed`。随后 `git ls-remote` 确认远端仍 `a3577ce2fa0bfa6047d397e50ed2e676c58a4420`,不得声称已交付或已部署。 diff --git a/CHANGELOG.md b/CHANGELOG.md index 8c52547b..ec3c0abf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # 印度占星 Skill 更新日志 +## 2026-09-20 — 校正开始与失败回执不再冒充结果来源(局部实现,未发布) + +compare/diagnostics 开始或失败时不写算法身份,成功时只记录实际结果来源,不回退部署默认值。同轮多个成功结果仍可能被旧 SQL 字符串聚合选错,本轮因此停在迁移授权边界;缓存只读策略尚未实施,不宣称 BUG-984 已修。Skill 版本不变。 + ## 2026-09-20 — 修正跨午夜出生范围的候选打分 跨午夜窗口中的候选改用各自日期计算 Dasha 边界,避免午夜后的候选沿用前一天日期;候选分数与排序可能随之变化,打分权重和确认门不变。后端算法身份与 V9 默认回执版本同步更新;版本接口可达且未被环境覆盖的分钟评分会按既有规则失效旧缓存。时段缓存和聚合回执仍有既有追溯缺口,未宣称所有旧结果都已重新计算。历史回执不重写,Skill 版本不变。部署与受控会话验收状态见 `docs/tasks/PROGRESS-rectification-cross-midnight-20260920.md`。 diff --git a/docs/BUG_HISTORY.md b/docs/BUG_HISTORY.md index 200dd5d3..3bdd0ab2 100644 --- a/docs/BUG_HISTORY.md +++ b/docs/BUG_HISTORY.md @@ -13009,12 +13009,13 @@ - 触发条件:历史时段缓存证据指纹未变;本次运行默认版本已升,而缓存算法身份仍旧。 - 根因:`scoreAndPersistCurrentEvidence()` 的时段分支在版本探测之前按证据指纹提前返回;成功回执使用旧结果身份,但 SQL 将不同阶段的 `engine_version` 取字符串最大值,而不是成功结果来源。分钟模式另有环境覆盖/探测失败回退边界,不能宣称身份无条件一致。 - F3 调用链查证(2026-09-20):前端时段分支 → `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()`;时段支持率确实消费该候选分数。故本项升级为 BUG-981 端到端验收阻塞项。受影响的是证据未变、未触发三轮转分钟的历史跨午夜时段缓存命中;新建/无缓存/证据变化仍走已修路径。`late_night` 299 分钟跨日;`unknown` 1439 分钟虽同样走时段扫描,其首轮 `00:00–23:59` 本身同日,不能据此声称有跨日偏移,选中跨午夜时段后才触发。逐层行号见 `docs/tasks/PROGRESS-rectification-cross-midnight-gate-fix-20260920.md`。 -- 修复:未实施,当前 Dasha 单不顺带改缓存政策或 SQL;补单 `docs/tasks/TASK-rectification-cross-midnight-dasha-fix-20260920.md` 待批准后执行。 +- 修复:补单策略 b 已批准,前轮认证 blocker 已由主会话解除(前置代码 `3f39bafc` 已合 staging,部署另验)。本轮仅完成 F2 A:compare/diagnostics 的 started/failed 不再写版本,completed 不回退前端常量。真实工具同 turn 混版本 completed 已实测成立,触发 B,但 SQL 未授权,停止进一步实施;F1/F3/F4 pending,BUG 保持未解决。进度见 `docs/tasks/PROGRESS-rectification-cross-midnight-fix-20260920.md`。 - 验证:独立纯虚构 TS 探针使用替身 RPC/fetch,旧时段缓存返回 `cached:true`、算法尾号 -7、fetch 次数 0;真实工具调用路径记录 started 尾号 -8 / completed 尾号 -7。聚合 SQL 的 `max(engine_version)` 已核源码,未真跑 DB,因此不伪称数据库端到端通过。 - 防复发:后续必须分别覆盖分钟/时段缓存、实际版本接口、部分环境覆盖与接口失败,成功回执必须绑定实际结果来源;历史打开与 Skill 绑定不变,不得重标或删除旧结果来掩盖问题。 - 相关记录:BUG-427、BUG-621、BUG-981。 - 复发自:未发现同症状既有记录;既有分钟算法身份缓存门不覆盖提前返回的时段分支,回执测试未组合新版 started 与旧缓存 completed。 -- 修复版本:未实施;本轮 T4 仅完成默认及后端身份同步,整体验收未通过。 +- F2 补充实测:原生虚构输入 golden 经真实 compare/diagnostics 工具顺序执行,仅替换算法标签模拟滚动版本;同一 turn 可写 completed scoring-9 与 scoring-10,而字符串 max 为 scoring-9。既有聚合 RPC 不暴露各成功行身份,历史 fingerprint 无法还原,应用层 A 不足以闭环。定向身份 9/9(含缺陷诊断)、tsc 0;五文件回归 61 项中 57 pass / 4 Windows symlink EPERM,未冒称 DB 聚合已验。 +- 修复版本:`codex/rectification-cross-midnight-fix-20260920` 的 F2 A 局部实现,B 授权阻塞、未推送;缓存与聚合整体验收未通过。 ## BUG-985 | 同日不变性回归写死浮点分数哈希导致跨机门禁失败 diff --git a/docs/research/pre_work_error_ledger.md b/docs/research/pre_work_error_ledger.md index 618f7e45..ec19027a 100644 --- a/docs/research/pre_work_error_ledger.md +++ b/docs/research/pre_work_error_ledger.md @@ -2,6 +2,12 @@ Purpose: read this file before substantial project work. It exists to stop repeat mistakes caused by multiple Codex windows, WorkBuddy mirrors, local drafts, backup folders, and partial cloud-git visibility. +## 2026-09-20 · BUG-984 补单前置解除与聚合授权停点 + +- 前轮写认证 blocker 已由主会话解除:前置代码 `3f39bafc` 已推 staging 并核对;本执行树 fetch 到后续纯文档 `f09f3d80`。下面旧记录保留为历史,不再作为当前开工 blocker。 +- A(started/failed 不写版本)不能证明整个回执聚合已修。真实工具同 turn compare/diagnostics 可各写不同身份的 completed,scoring-9/scoring-10 的字符串 max 仍选 9。必须按补单报告 B 授权停点,不能用新 fingerprint 绕过旧历史来源缺失,也不能自动开 SQL 迁移。 +- 本轮预检 remote verified,focused 23 pass / 1 历史镜像路径断言失败。Docker 29.8.0 可用;Windows 定向注册表实际有 4 项 symlink EPERM,不写成无 Docker。 + ## 2026-09-20 · 跨午夜已验收分支合入写认证失败 - fetch 与 ls-remote 正常不代表写认证有效。获准合入后的 `git push origin HEAD:staging` 报 `Failed to authenticate user`;远端仍 `a3577ce2`,本地已合并为 `b27d4de9`。未重试借用凭据或改认证配置。 diff --git a/docs/tasks/PROGRESS-rectification-cross-midnight-fix-20260920.md b/docs/tasks/PROGRESS-rectification-cross-midnight-fix-20260920.md new file mode 100644 index 00000000..13f9c9c4 --- /dev/null +++ b/docs/tasks/PROGRESS-rectification-cross-midnight-fix-20260920.md @@ -0,0 +1,84 @@ +# BUG-984 缓存与成功结果身份补单进度(2026-09-20) + +## 结论与授权边界 + +**blocked:F2 方案 A 已完成局部实现,实测证实同一 turn 可出现不同身份的多个 completed,按补单须升级 B;未获 SQL 迁移授权,依指令停止进一步业务实施。F1 / F3 / F4 未完成,不可交付为 BUG-984 修复。** + +- 执行树:`.worktrees/rectification-cross-midnight-fix-20260920`,分支 `codex/rectification-cross-midnight-fix-20260920`。 +- 前置代码基线:`3f39bafc4a1fb7fc9d0a257d2528ea1a64295792`。主会话已成功推 staging 并核对 ls-remote,**前轮 Gitea 写认证 blocker 已解除**;保留旧失败历史,不把它当本轮阻塞。 +- 本轮 fetch 发现补充文档 `f09f3d809a538a48979b94f1c851d1dc528e49ba`,干净树快进到该提交。它仅修改任务书与索引,代码仍为上述基线。 +- 已读 AGENTS、前端三规范、BUG-621/981/984 完整记录与错误台账。最大 Bug 编号 985,本轮续写 BUG-984,不占新号。 +- 不 push、不调用线上、不读取凭据、不改 workflow/main/DNS/SQL/Skill/input contract/评分常数/确认门,不删除或重标历史缓存。 + +## F2 实测与应用层可行性 + +### A 的局部实现 + +`createRectificationV9Tools()` 的 compare / diagnostics started 与 failReceipt 不再携带部署声称版本;completed 只记录实际返回的 algorithmVersion,compare 缺失时不回退前端默认版本。 + +同一真实 golden 用例先红后绿:原实现 started 仍写 -8,修改断言后 6 pass / 1 fail;局部修复并补两条测试后 9 pass / 0 fail。 + +### 已构造出的 B 触发条件 + +`frontend/tests/rectification-engine-version-cross-midnight.test.ts` 的 `runGoldenToolSequence()` 通过实际 `createRectificationV9Tools()` 构造一个 Case/turn,依次运行 compare、diagnostics;RPC/fetch 为受控替身,响应结构和数值来自既有原生引擎虚构输入 golden,仅算法身份标签被替换以模拟滚动版本。未声称执行未来 -9/-10 算法。 + +| 行 | 实际工具 | 状态 | 写入身份 | +| --- | --- | --- | --- | +| 1 | compare | started | null | +| 2 | compare | completed | scoring-9 | +| 3 | diagnostics | started | null | +| 4 | diagnostics | completed | scoring-10 | + +两条 completed 的 turn_id 相同;最新成功来自 scoring-10,但字符串最大值是 scoring-9。测试保留的是**缺陷诊断证据**,不是“聚合已修”的验收绿灯。并未运行真实 PostgreSQL 聚合,因此不宣称数据库端到端已验证。 + +- compare 与 diagnostics 各自独立调用引擎;两次完成之间没有“同 turn 必须同算法”的锁或一致性检查。部署滚动/环境来源差异不会被 A 消除。 +- 现有 `get_agentic_rectification_turn_receipt()` SQL 按 selected successful attempt 过滤后仍执行 `max(tr.engine_version)`,活动只返回 tool/status/methods/timing/fingerprint,不含各 completed 的 engine_version。 +- `loadV9TurnReceipt()` 只能读这个聚合字段;既有历史 compare fingerprint 仅含 hash/timings,diagnostics fingerprint 仅为 hash。因此应用层拿既有 RPC 输出无法无损恢复各历史成功行身份,也不能把当前 dossier 最新结果冒充历史 turn 来源。 +- 在新 fingerprint 塞身份只能处理新数据,修不了历史 max 污染;引入第二条服务角色原始表读取会另造回执权限/attempt/重试排序投影,不是本单 A,且任务书已明确“构造成立就升 B”。因此不绕过授权另造旁路。 +- 建议下一步 B:新增向后兼容函数体迁移,保留 owner/turn/attempt 过滤,仅从实际成功行取身份;明确多工具/失败重试语义,使用 started_at 与 id 稳定排序,不依赖字符串版本序。不得原地改已应用迁移。主会话审批后再实施并真跑 test:db。 + +## 任务状态 + +| 项 | 状态 | 剩余工作 | +| --- | --- | --- | +| F1 minute / block_scan 缓存 | pending | 真实 golden 时段旧缓存红测、统一可信身份复用条件;当前完全未改 | +| F2 成功回执 | blocked | A 局部完成;多 completed 实证触发 B,等待迁移授权 | +| F3 策略 b | pending | 只读标注及所有服务端写入口拒绝尚未实现,不是只藏按钮 | +| F4 完整回归/交付 | pending | 全量、build Static/gzip、DB 与受控 staging minute/late-night 验收未完成 | + +## 已执行检查 + +| 检查 | 结果 | +| --- | --- | +| pre_work_check(Python 3.11.7) | remote verified;focused 23 pass / 1 fail,历史 `.workbuddy` 镜像路径断言;与台账已知症状一致,未造目录 | +| 工具能力 | Node 22.23.2;Docker daemon 29.8.0 可用;node:22-bookworm / postgres:17-alpine 已有 | +| 算法身份文件基线 | 7 / 7 pass | +| A 红测 | 6 pass / 1 fail,started 身份断言 | +| A + 多成功来源诊断 | 9 / 9 pass;含缺陷存在证明,不是 F2 总体验收通过 | +| tsc --noEmit | 0 error | +| 定向五文件(含 BUG-621、case-service、skill-registry、activity) | 61 tests / 57 pass / 4 fail;四项均 Windows symlink EPERM,详见下列名单;尚未另跑基线逐条对照,不标全绿 | +| lint | 0 error / 120 warnings;未顺手修改既有 warnings | +| 全量 / build / Static / gzip / 标准 DB | 因必须 B 的授权停点而暂缓,未执行;不是预设工具缺失 | + +定向四项失败来自未改动的 `skill-registry.test.ts`,单文件复跑 16 tests / 12 pass / 4 fail,失败标题精确如下: + +1. `checked-in registry verifies hashed product packages and leaves consult on the live skill` +2. `path traversal and symlink escape fail closed` +3. `symbolic links are rejected even when their target stays inside the project root` +4. `live consult skill reads a hand-updated tree without a registry hash` + +没有以修改业务代码或弱化安全断言消红。DESIGN 未修改:本轮在 F2 授权停点终止,尚未实施任何 UI 标注/形状变化;后续 F3 必须同提交更新 DESIGN。 + +## 断言调整说明 + +| 原值 | 新值 | 原因 | +| --- | --- | --- | +| 新 Case compare/diagnostics started 回执身份为 CURRENT | started 为 null,completed 保持真实 CURRENT | F2 A 的明确授权;开始阶段未产生结果,不能冒充成功来源 | + +新增两条:diagnostics 503 的 failed 身份为空;同 turn 两工具 completed -9/-10 与字符串最大值相冲突。未知身份的既有缓存降级测试仍如实锁现状,F3 未开始,不声称已修。 + +## 部署与后续独立验收 + +本 agent 无线上访问。主会话通知前置 run2819 validate 已 success、publish 尚 in_progress,最近通知的 staging health 仍为旧 SHA;这不构成本轮部署或身份验收证据。 + +独立验收重点:工具 started/failed 实参、完成态来源无默认回退、混版本诊断测试的边界、旧回执按绑定 Skill 打开不变。F1/F3 的实现与完整测试须在解除 B 授权阻塞后继续。真人清单:`docs/testing/rectification-cross-midnight-fix-20260920.md`。 diff --git a/docs/tasks/README.md b/docs/tasks/README.md index 4ff4ab7e..7c0c7513 100644 --- a/docs/tasks/README.md +++ b/docs/tasks/README.md @@ -293,7 +293,7 @@ | `TASK-rectification-validation-integrity-20260920.md` | `PROGRESS-rectification-validation-20260920.md` | **生时校正验证体系补缺(纯离线评测,不改打分不改产品)**:会议要求把「推断真实出生时间」与「用户认可的参考盘」分开证明。核对结论——**产品口径侧四条已落地**(`accepted`≠`confirmed` 两条写入路径、确认门 fail-closed 且 `holdout` 为 `not_ready` 使 `confirmation_allowed` 不可能为真、采用不写 `reported_birth_time`、无任何把采用率当准确率的指标;运行时也无按生日走捷径的分支);**缺口全在评测本身**。三条:① 封存契约 `rectification_sealed_holdout.v1.json` 的三个打分哈希互不相同(封存 `f41c298d` / 契约记录 `99730c84` / 基线实测 `b15d9ea1`),`official_eval_trial_count: 0`——当前实现**从未产出过一次有效官方盲测**,唯一跑过那次已被资料审计作废(top-1 `0.15`,发布门要 `0.60`),可见的 `0.45` 自带「不得当发布指标」标记(BUG-978);② 全部离线评测的候选窗**以真值为圆心**(`_candidate_moments()`、`request_from_case()` 的 `true_time`),生产以申报时间为圆心(`ENGINE_SEARCH_RADIUS_MINUTES = 15`)——「真值掉出窗外」这一失败模式从不可见(BUG-979);③ v3 封存但每例仅 3 事件、v4 有 7+ 事件却已被看过并用于调参,**无口径干净又贴近真实会话的封存集**;六题回放的 `0.80/0.55/0.35` 是「真值方向最优答」的上帝视角上界,±30/±60 仍低于发布门(BUG-980)。T1 申报偏差敏感性 sweep、T2 有效重跑 + 契约对齐 + **防复发新测试**、T3 只出 v5 采集协议、T4 记录。**硬红线:不得改 12 个打分文件、不得用封存集调参、不得把 `status` 改 `ready`。** 家庭信息(父母职业/兄弟姐妹)在拿到基线数字前不开工——现有七领域全是带日期事件,静态属性没有输入口。前置:owner-case-purge 三提交仍未合入 staging。BUG-978~980 | **已验收通过(2026-09-20,Claude 独立复算)**;独立盲测仍 blocked;门禁/部署待核验 | `932f2fff`。Claude 独立复核:T1 完整重跑 900 组合**逐例 8 字段 0 差异**、summary/specification 全等,另从 900 条逐例重算 45 格表与发布表逐位相同;T2 独立复跑 0.45/0.50/6.45/0/1.0/0.0 与哈希全序 0.05/0.10 全部一致;12 个打分文件字节未动、`status=not_ready`、`confirmation_coverage_rate=0`、实跑 `holdout_passed()`=False;新增/触及 Python 测试本机 38 条全绿;freshness 测试比对时排除 `frozen_at_utc`(BUG-693/694 教训已用上)。**执行方正确推翻任务书 T2.3**:「首次口径干净的官方盲测」与 BUG-428 防复发(已看过结果的案例不得再计入盲测)冲突,已核 BUG-428 原文,是任务书写错。环境缺口:本机 frontend 无 node_modules,tsc/lint/npm test/build 未能复核,采信执行方自报。遗留 → `TASK-rectification-cross-midnight-dasha-20260920.md`(BUG-981) | | `TASK-rectification-cross-midnight-dasha-20260920.md` | — | **跨午夜候选的 Dasha 边界错一天(生产打分)**:`scoring_service.py` 调 `merge_transition_proximity()` 只传**一个** `birth_date`,该函数用它算全部候选的 Vimshottari / Narayana 起始日期,候选之间只靠 `_context_time()` 的 `HH:MM` 区分、**日期被丢掉**。窗口跨午夜时午夜后候选的 dasha 边界整体错一天。Claude 验收时在生产调用链独立复现:窗口 `23:50→00:10`、21 个候选,**恰好那 11 个跨日候选分数错、10 个同日候选逐位相同**(幅度 +0.0267 / −0.0133,本例头名未变)。算术上界 = `cap/kernel_width`,day 精度 0.067 分/件,18 件可累计约 1.2 分,而随分钟变化项总量仅约 2.1 分 —— **上界是推的不是实测,真实幅度本单必须实测**。踩中路径:`late_night` 时段 `23:00–03:59`、`unknown` `00:00–23:59`、23:45 后或 00:15 前申报的 ±15 窗。连带发现:`calculation_spec()` 不含打分实现身份,修复后同一 spec hash 对应不同分数,历史 Case 静默失去可复现性(同 BUG-427 类型)。**产品 2026-09-20 已就三点拍板:A 修、B 修完重新冻结并重跑 T1/T2、C 让新旧结果可区分。** C 的做法经查证已修正:`calculation_spec_hash` 全在 **V4** 链路、**V9 零引用**,原提案 bump `INPUT_CONTRACT_VERSION` 对真实历史无效已作废;改为随修复 bump `engine_version`(`v9EngineVersion()` 缺省串用于 started/failed;成功回执及 minute 缓存身份来自后端,旧「只写不比」假设作废),**不得动 `skill_version`**(BUG-621:open RPC 要求绑定 Skill 等于当前版本,bump 会让历史校正打不开)。硬红线:只改「按候选日期取 dasha 起始」,不得动 kernel/cap/share 任一常数;确认门不变。BUG-981 | **核心修复与 BUG-985 已 review 通过,本地合并 b27d4de9;推 staging 认证失败,端到端仍受 BUG-984 阻塞** | 实现 `aa46da10`(分支 `codex/rectification-cross-midnight-20260920`,未合入 staging)。Claude 独立 review:修复后整窗打分与**逐候选独立重算 21/21 一致**;非跨午夜分数基线 vs 修复**逐位不变**(我测半径 10 六例 + 半径 60 三例,执行方证据覆盖 19 例 / 2299 候选全部 `bytes_equal`);kernel/cap/share/`PRECISION_WEIGHTS` 一个未动;`status=not_ready`、coverage 0、`holdout_passed()` False、官方试次 0;决策 C 执行正确(`ALGORITHM_VERSION` 7→8,`INPUT_CONTRACT_VERSION` 与 `skill_version` 未动,golden 里 `calculation_spec_hash` 不变可证);既有断言改动均带三栏说明且为加强。**P1 阻塞**:新增的 `test_same_day_public_aa_scores_keep_pre_fix_bytes` 写死 121 个浮点分数的 SHA-256,在 Claude 机器上 ordinal 2/3 红、**基线与修复分支同样红**(非修复所致),且经 `test_rectification_cross_midnight_gate.py` re-export 落入快速门 glob —— 同 glob 基线 214 passed/0 failed、`aa46da10` 4 failed,推 staging 会让门禁红。执行方自报未通过的 BUG-984 经独立确认成立且更重:`block_scan` 只比 evidence 指纹不读打分身份,而 `late_night`(299 min)/`unknown`(1439 min) 均 >120 走该分支、又恰是最易跨午夜的窗口 | | `TASK-rectification-cross-midnight-gate-fix-20260920.md` | `PROGRESS-rectification-cross-midnight-gate-fix-20260920.md` | **review 修复单:门禁级浮点哈希断言(`aa46da10` 的合入阻塞项)**:新增回归把 121 个分数的 SHA-256 写死成字面量,于是断言范围把跨机 libm/pyswisseph 差异也圈了进去。实测该组 121 个分数**全无浮点尾噪**(`repr(s)==repr(round(s,4))` 121/121),所以差异是第 4 位真的不同,靠改序列化消不掉。同一提交刚改过的 `test_rectification_engine_memoization.py` docstring 正好写着「跨机舍入已漂 1.1e-3,不得整体 `==` 比较」——教训被引用了又被踩。执行方自己的证据 JSON 用的却是正确做法(`scores_bytes_equal` = 同机基线 vs 当前)。**根因**:要证的是相对不变量(同机基线 vs 当前),却实现成绝对不变量(当前 vs 某台机器的历史哈希)。F1 改用生产代码已有的 legacy 回退路径做同机 A/B 对照(去掉 `candidate_at` 即修复前行为),验收要求**两台浮点环境不同的机器各跑一次**、且回退核心修复后该测试仍绿(证明它守的是同日不变性而非跨午夜回归的替身);F2 bridge 重复收集(4 failed = 2×2);F3 查证 `block_scan` 重算是否经过被修 helper —— 会则 BUG-981 在 late_night/unknown 路径等于没上线、BUG-984 升为阻塞项;F4 记录。**硬红线:不得删测试/skip/把哈希改成本机当前值消红,不得改已验收的打分代码。** 串行:本单 → `aa46da10` 合入 → BUG-984 补单。BUG-985 | **已 review 通过;BUG-985 resolved;合入推送被 Gitea 认证阻塞,远端仍 a3577ce2** | 实现 `25232ce4`(分支 `codex/rectification-cross-midnight-gate-fix-20260920`)。F1 改为同进程 A/B:在 `scoring_service.merge_transition_proximity` 调用边界剥掉 `candidate_at` 走生产既有 legacy 回退,对比 121 个分数与整份贡献矩阵的 canonical 字节;另加 `legacy_calls == [121]` 与 `static_contexts is contexts` 两道防空转保险。Claude 复核:写死哈希字面量 **0 残留**;**第三套环境(Linux + Python 3.13,与执行方 Windows 3.11.7 不同)定向 18 条全绿** → 两机验收闭环;**回退日期修复探针:同日 3 条全绿、跨午夜相关 4 条转红**,证明同日不变性与跨午夜正确性已真正分开;快速门 glob staging `1b646659` 200 passed/0 failed → `25232ce4` 216 passed/0 failed(+16,零新增失败,总数未降);相对合并点仅动 2 个测试文件 + 文档,`scripts/`、前端、golden、打分常数零改动。BUG-985 记录含我要求的防复发条,并正确认定为 BUG-733 同形复发。**建议 BUG-985 由 `blocked` 改 `resolved`**(证据即第三环境复跑)。环境缺口:完整快速门在 Claude 机器 120 秒超时被杀,6→4 那组数以执行方记录为准 | -| `TASK-rectification-cross-midnight-dasha-fix-20260920.md` | — | **BUG-984 缓存与结果身份补单(BUG-981 的端到端阻塞项)**:`scoreAndPersistCurrentEvidence()` 的 `block_scan` 分支只比 `evidenceLedgerFingerprint` 即返回 `cached:true` 与旧 `algorithmVersion`,该返回发生在 `readV9EngineScoringIdentity()` **之前**;`minute` 分支则有身份门。F3 已查证完整调用链到 `merge_transition_proximity()`,故核心修复合入后,**证据未变的历史跨午夜时段缓存命中仍返回修复前分数**——在本单闭环前不得声称跨午夜问题已修。边界已按源码收窄:`late_night`(23:00–03:59) 跨日;`unknown`(00:00–23:59) 虽 >120 分钟但**本身同日**,选中跨午夜子时段后才触发(此处修正了 Claude 先前把两者并列的说法)。产品 2026-09-20 放行且**同日拍板 F3 策略选 b**:版本接口取不到可信身份时,旧缓存**只读展示 + 显著标注「按旧算法产出」**,否决 a(重算,会把接口抖动放大成长等待,时段扫描受 `JYOTISH_HEAVY_COMPUTE_CONCURRENCY=2` 限流)与 c(照常复用,与已定原则冲突)。b 的三条边界:只读结果**服务端拒绝采用/确认**(靠删不靠藏,须有定向用例)、标注必须用户可见并对照 `VOICE.md`(涉界面同提交更新 `DESIGN.md`)、回执来源身份仍是产出它的版本。**F2 的 SQL 问题已查清并定序(A 先上 / B 兜底 / 第 10 版前必须解决)**:`engine_version` 一个字段被「部署声称的版本」(started/failed 行,取前端常量)与「实际产出结果的版本」(completed 行,命中旧缓存即旧版本)共用,聚合却用与版本先后无关的字符串 `max`。**该缺陷此前一直撞对,`aa46da10` 之后才变真错**:它把 `v9EngineVersion()` 缺省由 `rectification-v5` 改为 `…scoring-8`,started 行遂在字符串序上反超 completed 行 → 回执显示第 8 版而分数来自第 7 版缓存;已核 `deploy/`、`.gitea/` 未设 `RECTIFICATION_ENGINE_VERSION`,走缺省,**是真实行为**。第二个缺陷:实跑 `max("…-10","…-9") = "…-9"`,**该聚合在第 10 版静默反向**(现为第 8 版)。A=started/failed 不再写版本(应用层,必做);A 的漏洞(同 turn 多个 completed 行版本不同)**必须实测取证,不得以「应该不会」结案**;B=聚合改取成功结果那一行(只改函数体,向后兼容);C=拆列本单不做。**四项全部可开工。**硬红线:不重标/不删历史结果,不 bump Skill,不改 V4 input contract,不引入按 `engineVersion` 拒绝打开历史会话。串行:BUG-985 合入 → 本单。BUG-984 | **待领取**(四项全部可开工) | — | +| `TASK-rectification-cross-midnight-dasha-fix-20260920.md` | `PROGRESS-rectification-cross-midnight-fix-20260920.md` | **BUG-984 缓存与结果身份补单(BUG-981 的端到端阻塞项)**:`scoreAndPersistCurrentEvidence()` 的 `block_scan` 分支只比 `evidenceLedgerFingerprint` 即返回 `cached:true` 与旧 `algorithmVersion`,该返回发生在 `readV9EngineScoringIdentity()` **之前**;`minute` 分支则有身份门。F3 已查证完整调用链到 `merge_transition_proximity()`,故核心修复合入后,**证据未变的历史跨午夜时段缓存命中仍返回修复前分数**——在本单闭环前不得声称跨午夜问题已修。边界已按源码收窄:`late_night`(23:00–03:59) 跨日;`unknown`(00:00–23:59) 虽 >120 分钟但**本身同日**,选中跨午夜子时段后才触发(此处修正了 Claude 先前把两者并列的说法)。产品 2026-09-20 放行且**同日拍板 F3 策略选 b**:版本接口取不到可信身份时,旧缓存**只读展示 + 显著标注「按旧算法产出」**,否决 a(重算,会把接口抖动放大成长等待,时段扫描受 `JYOTISH_HEAVY_COMPUTE_CONCURRENCY=2` 限流)与 c(照常复用,与已定原则冲突)。b 的三条边界:只读结果**服务端拒绝采用/确认**(靠删不靠藏,须有定向用例)、标注必须用户可见并对照 `VOICE.md`(涉界面同提交更新 `DESIGN.md`)、回执来源身份仍是产出它的版本。**F2 的 SQL 问题已查清并定序(A 先上 / B 兜底 / 第 10 版前必须解决)**:`engine_version` 一个字段被「部署声称的版本」(started/failed 行,取前端常量)与「实际产出结果的版本」(completed 行,命中旧缓存即旧版本)共用,聚合却用与版本先后无关的字符串 `max`。**该缺陷此前一直撞对,`aa46da10` 之后才变真错**:它把 `v9EngineVersion()` 缺省由 `rectification-v5` 改为 `…scoring-8`,started 行遂在字符串序上反超 completed 行 → 回执显示第 8 版而分数来自第 7 版缓存;已核 `deploy/`、`.gitea/` 未设 `RECTIFICATION_ENGINE_VERSION`,走缺省,**是真实行为**。第二个缺陷:实跑 `max("…-10","…-9") = "…-9"`,**该聚合在第 10 版静默反向**(现为第 8 版)。A=started/failed 不再写版本(应用层,必做);A 的漏洞(同 turn 多个 completed 行版本不同)**必须实测取证,不得以「应该不会」结案**;B=聚合改取成功结果那一行(只改函数体,向后兼容);C=拆列本单不做。**四项全部可开工。**硬红线:不重标/不删历史结果,不 bump Skill,不改 V4 input contract,不引入按 `engineVersion` 拒绝打开历史会话。串行:BUG-985 合入 → 本单。BUG-984 | **blocked:F2 A 局部完成,多 completed 混版本实测成立,等待 B 的 SQL 授权;F1/F3/F4 pending** | `codex/rectification-cross-midnight-fix-20260920`;前置认证已解除,代码基线 `3f39bafc`、文档基线 `f09f3d80`;本轮未推送 | ## 命名与归档 diff --git a/docs/testing/rectification-cross-midnight-fix-20260920.md b/docs/testing/rectification-cross-midnight-fix-20260920.md new file mode 100644 index 00000000..cb4cc63d --- /dev/null +++ b/docs/testing/rectification-cross-midnight-fix-20260920.md @@ -0,0 +1,28 @@ +# BUG-984 补单验收清单(2026-09-20) + +当前 **blocked**:F2 A 局部实现,多 completed 混版本实测触发 B,SQL 未获授权;F1/F3 尚未实现。以下不是已通过清单,不能将本分支当完整修复部署。 + +## 自动化续验 + +- [ ] 获准 B 后新增兼容函数体迁移,不修改旧迁移,标准 `npm run test:db` 实跑。 +- [ ] 同一成功 attempt 的 started -8 / completed -7 只展示 -7;历史行字节不变。 +- [ ] 同 turn compare / diagnostics 多 completed -9 / -10 不按字符串序选 9;明确取最新成功的稳定排序与各工具来源语义。 +- [ ] 失败后重试、成功后失败、不同 attempt、历史未带新 metadata 都覆盖,不能用当前 dossier 身份替代历史来源。 +- [ ] minute / late-night block_scan 均用真实 golden 测同身份命中、旧身份重算;未知当前身份时旧缓存只读、不强制重算。 +- [ ] 无覆盖、完整覆盖、部分覆盖、版本接口超时/错误均区分当前身份与来源。 +- [ ] 不经界面直接调所有采用/确认/候选选择写入口,旧只读结果均由服务端拒绝。 +- [ ] `BUG-621` 历史按绑定 Skill 打开回归;不增加 engineVersion 打开相等门。 +- [ ] tsc、lint、定向、全量、build `/` Static、首屏 gzip ±2%;实际平台失败与基线逐项对照,不削弱断言。 + +## 受控 staging 真人验收(由主会话安排) + +只用明确虚构资料与本人受控账号,不读取或借用他人登录态;不得将出生资料、会话内容、标识符或 Cookie 写入记录。 + +- [ ] 先核对部署 gitCommit(允许其后纯文档差异),不是只看 HTTP 200。 +- [ ] 依次测试分钟历史结果与 late-night 时段历史结果;记录是否缓存、实际来源版本、是否重算,不记个人资料。 +- [ ] 版本探测异常时旧结果仍可查看,显著出现“按旧算法产出”;没有采用/确认/选择写入口,伪造请求也拒绝。 +- [ ] 已生成历史回执来源保留,不显示 started 的部署版本;旧 Skill 会话仍可从侧栏打开。 +- [ ] 恢复版本接口后当前同身份缓存可正常复用,身份变化按新规则重算,不删除或重标旧结果。 +- [ ] 失败重试后活动与成功来源一致;多人/多个请求压力下没有把版本接口抖动放大成强制重计算。 + +当前没有浏览器级完成证据,本 agent 未调用线上。详见 `docs/tasks/PROGRESS-rectification-cross-midnight-fix-20260920.md`。 diff --git a/frontend/src/mastra/rectification-v9-tools.ts b/frontend/src/mastra/rectification-v9-tools.ts index dd7c4bb5..1d722a67 100644 --- a/frontend/src/mastra/rectification-v9-tools.ts +++ b/frontend/src/mastra/rectification-v9-tools.ts @@ -160,7 +160,6 @@ import { readV9EngineScoringIdentity, cachedEngineScoreIsReusable, toEngineEvents, - v9EngineVersion, executedMethodsFromLedger, runV9RangeReading, runV9BlockScan, @@ -900,7 +899,6 @@ export function createRectificationV9ReadOnlyTools(ctx: RectificationV9Context) export function createRectificationV9Tools(ctx: RectificationV9Context) { const { accounting, userId, caseId, turnId, attemptId, userMessage } = ctx; - const engineVersion = v9EngineVersion(); let hasReadCase = false; let spokenPromptFailures = 0; let setFocusCompleted = 0; @@ -934,7 +932,7 @@ export function createRectificationV9Tools(ctx: RectificationV9Context) { toolName: string, publicPhase: string, error: unknown, - extra: { inputFingerprint?: string | null; engineVersion?: string | null } = {}, + extra: { inputFingerprint?: string | null } = {}, ) => { const code = safeToolErrorCode(error); const message = engineMessageForReceipt(error); @@ -947,7 +945,6 @@ export function createRectificationV9Tools(ctx: RectificationV9Context) { })); await receipt(toolName, publicPhase, "failed", { inputFingerprint: extra.inputFingerprint ?? null, - engineVersion: extra.engineVersion ?? null, safeErrorCode: code, resultFingerprint: JSON.stringify({ safe_error_code: code, @@ -1925,7 +1922,7 @@ export function createRectificationV9Tools(ctx: RectificationV9Context) { execute: async (input) => { assertCaseRef(input, caseId); const inputFingerprint = canonicalToolInputFingerprint("rectification-compare-candidates", input); - await receipt("rectification-compare-candidates", "candidates.comparing", "started", { inputFingerprint, engineVersion }); + await receipt("rectification-compare-candidates", "candidates.comparing", "started", { inputFingerprint }); try { const scored = await scoreAndPersistCurrentEvidence(input.caseId); const latest = scored.parsed.case.stage === "block_scan" @@ -1979,14 +1976,13 @@ export function createRectificationV9Tools(ctx: RectificationV9Context) { vedastro_validate_ms: scored.timings.vedastro_validate_ms, persist_ms: scored.timings.persist_ms, }), - engineVersion: scored.persisted.algorithmVersion ?? engineVersion, + engineVersion: scored.persisted.algorithmVersion ?? null, executedMethods: scored.score.executedMethods, }); return { ...projection, executed_methods: scored.score.executedMethods }; } catch (error) { await failReceipt("rectification-compare-candidates", "candidates.comparing", error, { inputFingerprint, - engineVersion, }); throw error; } @@ -2001,7 +1997,7 @@ export function createRectificationV9Tools(ctx: RectificationV9Context) { execute: async (input) => { assertCaseRef(input, caseId); const inputFingerprint = canonicalToolInputFingerprint("rectification-read-diagnostics", input); - await receipt("rectification-read-diagnostics", "diagnostics.completed", "started", { inputFingerprint, engineVersion }); + await receipt("rectification-read-diagnostics", "diagnostics.completed", "started", { inputFingerprint }); try { const dossier = await loadV9CaseDossier(accounting, userId, input.caseId); const parsed = parseDossierForTools(dossier); @@ -2034,7 +2030,7 @@ export function createRectificationV9Tools(ctx: RectificationV9Context) { }); return { ...projection, executed_methods: diagnostics.executedMethods }; } catch (error) { - await failReceipt("rectification-read-diagnostics", "diagnostics.completed", error, { inputFingerprint, engineVersion }); + await failReceipt("rectification-read-diagnostics", "diagnostics.completed", error, { inputFingerprint }); throw error; } }, diff --git a/frontend/tests/rectification-engine-version-cross-midnight.test.ts b/frontend/tests/rectification-engine-version-cross-midnight.test.ts index 43757ee7..b7ccbc72 100644 --- a/frontend/tests/rectification-engine-version-cross-midnight.test.ts +++ b/frontend/tests/rectification-engine-version-cross-midnight.test.ts @@ -109,7 +109,11 @@ test("unreachable versions retain the existing unknown-identity cache fallback, assert.equal(cachedEngineScoreIsReusable({ ...fingerprints, algorithmVersion: PREVIOUS }, fingerprints, live), true); }); -test("new Case scoring writes the new version on both started and completed receipts", async (t) => { +async function runGoldenToolSequence(t: test.TestContext, options: { + compareVersion?: string; + diagnosticsVersion?: string; + diagnosticsFailure?: boolean; +} = {}) { isolateIdentityEnv(t); const request = golden.request; const candidateRange = { start_time: request.start_time, end_time: request.end_time }; @@ -123,7 +127,13 @@ test("new Case scoring writes the new version on both started and completed rece const path = new URL(String(url)).pathname; calls.push(path); if (path.endsWith("/versions")) return Response.json(golden.versions); - if (path.endsWith("/score") || path.endsWith("/diagnostics")) return Response.json(golden.score); + if (path.endsWith("/score")) { + return Response.json({ ...golden.score, algorithm_version: options.compareVersion ?? CURRENT }); + } + if (path.endsWith("/diagnostics")) { + if (options.diagnosticsFailure) return Response.json({ error: "fixture_unavailable" }, { status: 503 }); + return Response.json({ ...golden.score, algorithm_version: options.diagnosticsVersion ?? CURRENT }); + } throw new Error(`unexpected engine request ${path}`); }); const accounting = fakeAccounting({ @@ -152,15 +162,44 @@ test("new Case scoring writes the new version on both started and completed rece }); const tools = createRectificationV9Tools({ userId: USER_ID, caseId: CASE_ID, turnId: TURN_ID, accounting: accounting.client as never }); for (const name of ["rectification-compare-candidates", "rectification-read-diagnostics"] as const) { - await (tools[name] as unknown as { execute(input: unknown): Promise }).execute({ caseId: CASE_ID }); - const receipts = accounting.calls.filter((call) => call.fn === "insert_agentic_rectification_tool_receipt" && call.args.p_tool_name === name); - assert.deepEqual(receipts.map((call) => [call.args.p_status, call.args.p_engine_version]), [ - ["started", CURRENT], ["completed", CURRENT], - ]); + const execute = () => (tools[name] as unknown as { execute(input: unknown): Promise }).execute({ caseId: CASE_ID }); + if (options.diagnosticsFailure && name === "rectification-read-diagnostics") await assert.rejects(execute); + else await execute(); } assert.ok(calls.includes("/api/rectification/v5/score")); const persisted = accounting.calls.find((call) => call.fn === "persist_agentic_rectification_candidate_v2"); - assert.equal(persisted?.args.p_algorithm_version, CURRENT); + assert.equal(persisted?.args.p_algorithm_version, options.compareVersion ?? CURRENT); + return accounting.calls.filter((call) => call.fn === "insert_agentic_rectification_tool_receipt"); +} + +test("new Case scoring writes actual identity only on completed receipts", async (t) => { + const receipts = await runGoldenToolSequence(t); + assert.deepEqual(receipts.map((call) => [call.args.p_status, call.args.p_engine_version]), [ + // 原值: started=CURRENT;新值: null;原因: BUG-984,开始不是成功结果身份。 + ["started", null], ["completed", CURRENT], ["started", null], ["completed", CURRENT], + ]); +}); + +test("failed diagnostics do not claim an engine result identity", async (t) => { + const receipts = await runGoldenToolSequence(t, { diagnosticsFailure: true }); + assert.deepEqual(receipts.map((call) => [call.args.p_status, call.args.p_engine_version]), [ + ["started", null], ["completed", CURRENT], ["started", null], ["failed", null], + ]); +}); + +test("BUG-984 blocker: real tool sequence permits mixed completed identities in one turn", async (t) => { + // Identity-only mutation models rolling backend versions; all response shape and values + // remain the real native golden. This is not a claim to have run future algorithms. + const version9 = "rectification-v5-matrix-scoring-9"; + const version10 = "rectification-v5-matrix-scoring-10"; + const receipts = await runGoldenToolSequence(t, { compareVersion: version9, diagnosticsVersion: version10 }); + const completed = receipts.filter((call) => call.args.p_status === "completed"); + assert.equal(new Set(completed.map((call) => call.args.p_turn_id)).size, 1); + assert.deepEqual(completed.map((call) => call.args.p_engine_version), [version9, version10]); + // Preserve the blocker as positive evidence, not a false passing acceptance test. + // F2 acceptance must replace this diagnostic once the approved aggregate fix lands. + assert.equal([version9, version10].sort().at(-1), version9); + assert.notEqual([version9, version10].sort().at(-1), completed.at(-1)?.args.p_engine_version); }); test("history opens with its bound Skill and reads old engine receipt values unchanged", async (t) => {