fix(rectification): keep one delivery and adopt with the birth date

Accept and GET now decide with the same birth snapshot as the answer, so an under-age probe cannot flip can_adopt. The exit skips a second write when this reply already delivered. The card stays without an adopt button when adoption is not allowed, and a refusal shows the Chinese sentence.
This commit is contained in:
jesse-ux
2026-10-06 16:03:57 +08:00
committed by Jesse_Chen
parent da226db1d4
commit 6e16dc5e16
34 changed files with 1100 additions and 182 deletions
+46
View File
@@ -16674,6 +16674,52 @@
- 复发自:无
- 修复版本:研究分支 `codex/rectification-nadi-seconds-research-20261005`,2026-10-05 Claude 验收后合入 staging(只有研究脚本与文档,没有运行时改动)
## BUG-1241 | 校正交付后同一轮又补出第二段回答
- 状态:resolved(分支 `codex/rectification-delivery-dup-adopt-20261006`)
- 首次发现 / 最近更新:2026-10-06 / 2026-10-06
- 来源:10-06 测试环境真机。任务书 `docs/tasks/TASK-rectification-delivery-dup-adopt-20261006.md`。
- 影响面:生时校正答完最后一题后的交付。不改引擎,不改数据库。
- 现象:同一轮出现两段助手回答。前一段是采用旁白的改写。后一段是确定性兜底,带分钟范围和「还剩几个候选」。刷新后两段都在。
- 触发条件:答题路径已经把改写后的交付句写入本轮;收尾补位找不到已问轮次编号,又用子串去比对。改写句不是兜底原文的子串,于是再写一条。
- 根因:BUG-1153 的防重只看「上一条是否包含确定性原文」。当时的测试把上一条写成已经包含这段原文,所以没覆盖「上一条是另一句改写」。60 秒内不重复记账的标记也没有拿来比对这句已经写下的交付。
- 修复:答题回复在落库前记下这句交付原文。收尾补位若发现上一条助手原文(去掉空白后)就是这句,就不再写。子串比对保留。没有任何回复承载这句时,补位仍写 1 条。不把旁白函数传进收尾,也不改旁白提示词。选择题 JSON 在已有轮次编号时带上 `x-rectification-turn-id`,作为额外保险;真库验收不依赖这个头。
- 验证:`frontend/tests/rectification-delivery-dup-adopt-20261006.test.ts` 覆盖「上一条是改写句则 0 条新写入」和「上一条不是这句则仍写 1 条」。BUG-1153 原测试仍通过。真 PostgreSQL 17 上,种好的并列卷宗走同一条收尾后,助手交付消息恰好 1 条。本机没有星历引擎,收尾里的刷新失败得很快,失败之后也没有写出第 2 条。
- 防复发:上述测试。跳过条件是「上一条等于已记下的交付句」,不能只靠加长子串。
- 相关记录:BUG-596、BUG-1149、BUG-1153。
- 复发自:BUG-1153。旧测试的上一条已经包含确定性原文,改写句走不到那条断言。
- 修复版本:待发布
## BUG-1242 | 点采用被拒,因为采用和读取没用答题时的出生日期
- 状态:resolved(分支 `codex/rectification-delivery-dup-adopt-20261006`)
- 首次发现 / 最近更新:2026-10-06 / 2026-10-06
- 来源:同上,同一轮真机。卡片上能点采用,接口返回 409 `adoption_not_allowed`。
- 影响面:生时校正的采用与案例读取。不放宽采用门,不改成年下限,不改数据库。
- 现象:答题当时判定可以按代表时间采用。点采用时同一份卷宗被判成还在区分候选,`can_adopt` 为假,返回 409。案例读取和采用走的是同一种缺出生日期的判定,和答题不一致。
- 触发条件:证据指纹已经对上,卷宗里还有一道低于成年下限的区分探针。答题路径带了出生日期,探针被拿掉,结论是可以采用。采用和读取只传指纹,缺出生日期时这道探针仍像能问,结论翻成不可采用。只补「快照仍是当前」不能对齐;出生日期能对齐。过期指纹上强行标成当前也会改结论,生产重算不会对着过期指纹这样做。
- 根因:BUG-598 只把成年下限接到答题和巡检回退。GET 与 accept 仍各自调用判定,不带出生日期。BUG-680 的成对测试没有把出生日期算进「两边必须相同」的入参。
- 修复:出生快照读出校验过的日期,答题、GET、accept 以及其余生产判定调用共用这一份入参。读不到快照就失败关闭:accept 返回 503「暂时无法采用」,GET 仍是「暂时无法读取校正记录」,不退回缺日期的相反结论。快照是否当前仍由卷宗自己算,答题路径不再单独传 true。采用门的集合没有加成员。
- 验证:对拍测试覆盖有无未成年探针、并列与不并列。修前那种只传指纹的调用仍是不可采用;共享入参后答题、采用、GET 都是可以按代表时间采用。源码合同要求生产代码里新增的判定调用旁边出现共享入参函数。真 PostgreSQL 17 上同一份并列卷宗:共享入参 `can_adopt` 为真,GET 与之相同,旧的只传指纹仍为假。
- 防复发:上述对拍与源码合同。`collect_evidence`、`discriminate_candidates`、`validate_holdout` 的公开 `can_adopt` 仍必须为假。
- 相关记录:BUG-399、BUG-417、BUG-497、BUG-598、BUG-680。
- 复发自:BUG-598。关联 BUG-680:成对测试没有覆盖出生日期这一个入参。
- 修复版本:待发布
## BUG-1243 | 不能采用时卡片仍给出采用按钮,拒绝文案是英文代码
- 状态:resolved(分支 `codex/rectification-delivery-dup-adopt-20261006`)
- 首次发现 / 最近更新:2026-10-06 / 2026-10-06
- 来源:同上。卡片在交付结果上显示采用,点下去被拒,界面带出 `adoption_not_allowed`。
- 影响面:盘型卡和旧分钟卡。不新增结果字段,不新增第二张卡。
- 现象:交付结果里只要卡片出现,按钮也出现,不看公开的 `can_adopt`。拒绝时原文把机器代码给到界面。快照不刷新,按钮还在。
- 根因:BUG-681 让交付结果都显示卡片,按钮却没有使用和采用接口相同的判据(采用集合且 `can_adopt` 为真)。客户端把 409 的代码直接显示出来。
- 修复:盘型卡和分钟卡都只在采用集合且 `can_adopt` 为真时显示按钮和「采用后按…排盘」。不能采用时卡片仍在,没有按钮,也没有那一行。已采用仍显示「已采用」。409 的 `error` 改为「这次的结果还不能采用,已刷新到最新状态」,`code` 仍是 `adoption_not_allowed`。客户端先重拉快照,再显示这句中文。
- 验证:组件测试覆盖交付但不可采用、可以采用、分钟卡三种。渲染结果里没有 `adoption_not_allowed`。409 的假请求会重拉快照,错误文案是那句中文。本轮没有在浏览器或手机上点过按钮。
- 防复发:上述组件测试和 409 测试。`frontend/DESIGN.md` 校正状态表写明不能采用时卡片仍在、没有按钮。
- 相关记录:BUG-497、BUG-681。
- 复发自:无。BUG-681 要求卡片出现,没有要求按钮和公开 `can_adopt` 一致;BUG-497 禁止把不可采用的结果放进采用集合,本轮没有放宽。
## BUG-1244 | 普通对话首轮按固定小标题汇报,读起来不像聊天
- 状态:fixed-pending-verify(分支 `codex/consult-conversational-answer-20261006`;部署和真机清单完成前不标 resolved)
@@ -0,0 +1,107 @@
# PROGRESS · 校正交付一次两段回答与点采用被拒(2026-10-06)
- 任务书:`docs/tasks/TASK-rectification-delivery-dup-adopt-20261006.md`(staging `65b61ed5`,只改文档,未部署)
- 执行分支:`codex/rectification-delivery-dup-adopt-20261006`
- 基线:任务书写的产品基线是 `e3f3a1fc`;实现从已含任务书的 `65b61ed5` 开始
- BUG:1241、1242、1243。开工时 `docs/BUG_HISTORY.md` 最大号是 BUG-1240,没有撞号
- 修复版本:待发布。本轮不推送,不声称已部署
## T0 修前复现(先于 T1–T3)
虚构出生日期 `1988-03-12`。开口窗口 `03:00–07:00`,可信范围 `04:45–05:15`,并列分钟 `04:53` 与 `05:00`。1998 年搬家探针(10 岁,低于搬家下限 18 岁)。四条带日期的证据。候选集合 `04:45-05:15:04:53,05:00`。探针的支持/冲突写的是分钟,不是候选编号。
下面两轮诊断不算复现:第一轮内存夹具把支持写成了候选编号,开口窗口又等于可信范围,采用被「范围没变窄」挡住,三处都不可采用。第一轮 PG 回执的展示许可与选择许可对不上,能力上限关掉,旁白没有改写,两条消息是同一段确定性原文。成功的那一轮才是下面的记录。
### 内存矩阵
当前证据指纹 `f0cbba1a89013d33aaa89a7ee39fc81caf3528e5a9e9407a3b41eddfea2f2c06`。
| 入参 | session_outcome | can_adopt | 探针 |
| --- | --- | --- | --- |
| 答题:出生日期 + snapshotCurrent | adopt_representative | true | 无(低于成年下限) |
| 采用:只有指纹 | discriminate_candidates | false | relocation.1998.dasha_activation |
| 只补出生日期 | adopt_representative | true | 无 |
| 只补 snapshotCurrent | discriminate_candidates | false | 探针还在 |
| 两个都补 | adopt_representative | true | 无 |
指纹已经对上时,只补出生日期就和答题一致;只补 snapshotCurrent 不行。没有未成年探针时,缺出生日期也不改变结论。
过期指纹(64 个 `b`)上,答题路径若强行 `snapshotCurrent: true`,结论也会变。生产里的重算只在重新读出的卷宗已经对上、或重算之后再从这份卷宗算出来时才标当前,不会对着过期指纹硬写成 true。所以共享入参必须带校验过的出生日期,snapshotCurrent 由卷宗自己算,答题路径不再私自传 true。
### PG17
真 PostgreSQL 17。种的是答完之后的并列卷宗,没有跑星历引擎。算法 `rectification-v5-matrix-scoring-8`,策略 `fictional-decision-policy-v1`,回执的展示/选择/确认三项与列一致且上限打开。Windows 上 `db/migrations` 的四个符号链接被检出成普通文件,和 `supabase/migrations` 重名,两目录迁移会停。这一轮把 supabase 全部加上 db 里不重名的本地身份文件放进一个临时目录,设 `MIGRATIONS_DIRECTORY`。这不是「两目录迁移在 Windows 上通过」。
同一份已对上指纹的卷宗:
| 路径 | session_outcome | can_adopt |
| --- | --- | --- |
| 采用那种只传指纹 | discriminate_candidates | false |
| 答题:出生日期 + snapshotCurrent | adopt_representative | true |
| 只补出生日期 | adopt_representative | true |
采用路由在 `can_adopt !== true` 时返回 409 `adoption_not_allowed`。GET 用的也是只传指纹,和答题不一致。
助手消息修前正好 2 条:
1. 采用旁白改写「这一轮已经把范围收窄,可以按代表时间看盘。」写入方是答题后的 `persistV9DeterministicTurn`。
2. 确定性兜底,写入方是 `finalizeSuccessfulTurnExit`(日志 `rectification_delivery_turn`,trigger 为该函数)。正文含「分钟范围 04:45–05:15」和「现在还剩 04:45–05:15 里 2 个候选」。改写句不是这段的子串,子串防重没挡住。
收尾里的引擎刷新失败得很快(本机没有引擎),失败之后仍然写下第 2 条。选择题 JSON 没有带 `x-rectification-turn-id`,所以收尾看不到已经问过的轮次,补位照写。
T2 做法:不把采用旁白函数传进收尾,也不改旁白提示词。答题回复已经记下交付原文;收尾补位在「上一条助手原文去掉空白后等于这份已记原文」时不再写。子串比对留下。没有任何回复承载时,补位仍写 1 条。
## 实现
共享入参在 `decision-inputs.ts`。出生日期读不出来就抛 `birth_snapshot_unavailable`,不退回缺日期的判定。GET 和接续仍回到原来的中文 503。accept 在这条失败上返回 503「暂时无法采用」。快照是否当前仍由卷宗比较指纹,答题路径不再单独传 true。
收尾补位先看子串,再看已记下的交付句是否就是上一条助手原文,然后才写。先记下的那句在 60 秒内不被后一句盖掉。选择题 JSON 在有轮次编号时带 `x-rectification-turn-id`。真库验收没有靠这个头:收尾函数自己比对上一条。
盘型卡和分钟卡的按钮都要「结果属于采用集合,且 `can_adopt` 为真」。`frontend/DESIGN.md` 校正状态表的候选行补了「不能采用时卡片仍在、没有按钮」。
测试缝:`interview-state` 和 `projectTurnDecision` 在调用方没传出生日期时仍只传指纹,避免改掉几十条既有单测的结论。生产的 GET、accept、答题都传校验后的日期。`dossierResponse` 缺日期会直接抛错。
既有断言改动:
| 文件 | 原值 | 新值 | 原因 |
| --- | --- | --- | --- |
| `rectification-v9-contracts.test.ts` | `step_state: stepStateFromCaseDossier(dossier)` | `step_state: stepStateFromCaseDossier(dossier, decisionInputs.birthDate)` | GET 投影与答题共用出生日期(BUG-1242) |
`rectification-readopt-card-20261002.test.ts` 的两处盘型卡渲染补了 `canAdopt: true`,断言原文没改。若干 `dossierResponse` 测试补了虚构日期 `1900-06-15`,好让成年下限不滤掉夹具里的现代探针。新夹具用 `1988-03-12`,没有复用事故卡片上的日期。
## 修后真库
仍是种好的答完并列卷宗,没有跑星历。同一份已对上指纹的卷宗:
| 路径 | session_outcome | can_adopt |
| --- | --- | --- |
| 旧的只传指纹 | discriminate_candidates | false |
| 共享出生日期 | adopt_representative | true |
| GET | adopt_representative | true |
然后走答题后的改写落库,再调收尾且不带已问轮次编号。助手交付消息恰好 1 条,就是那句改写。日志里收尾补位被调用了,没有写出第 2 条。引擎刷新仍然很快失败(本机没有引擎),失败后也没有补写。
## 本机测试
`tsc --noEmit`:0 错。`npm run lint`:0 error,126 条既有 warning,没有改它们。
新文件 `rectification-delivery-dup-adopt-20261006.test.ts` 16 项通过。相关既有文件 122 项通过,含 BUG-1153 原测试。`# cancelled` 都是 0。
全量 `npm test`(`node scripts/run-tests.mjs`,退出码 1):
| | tests | pass | fail | cancelled |
| --- | --- | --- | --- | --- |
| 基线 `65b61ed5` | 4848 | 4704 | 144 | 0 |
| 本分支 | 4864 | 4719 | 145 | 0 |
失败名比基线多 1 条、少 0 条。多出来的是「发出后 300 毫秒内出现第一行阶段句」(`rectification-latency-20260926.test.tsx`)。这次全量跑了 414 秒,这条墙钟断言没过。同一条单独再跑通过。它不读出生快照,也不写交付轮。基线全量里没有这条名字。不记成新 bug。
其余 144 条名字与基线相同。测试总数多 16,就是新文件的 16 条,都通过。
假账本原来不返回出生快照。决策入口改成缺快照就拒绝之后,第一次全量多了 60 个失败名。给没桩过的计算 RPC 补了虚构日期 `1900-06-15`(成年下限不滤掉夹具里的现代探针),采用路由的工具桩补了 `loadV9CaseCompute`,范围句那条路由测试补了同一份快照。这三处的断言原文没改,除了 `persistApplied` 的源码顺序:原值查 `loadV9CaseCompute`,新值查 `loadCaseDecisionInputs`,原因是出生日期改由共享入口读取。三栏写在对应测试里。
## 构建
`next build --webpack`。Turbopack 会拒绝工作树里指到主检出的 `node_modules` 联接,所以用 webpack。基线 `65b61ed5` 编译 2.9 分钟、构建自带的 TypeScript 100 秒,都通过,收集页面数据时停在 `/api/rectification/agent`。本分支编译 2.2 分钟、TypeScript 83 秒,都通过,收集页面数据时停在 `/api/birth-time-guide`。两边都是 Windows 不允许为 Skill 运行别名建符号链接(`EPERM`)。先碰到哪条路由取决于收集页面的工人,不是这次改动。路由表没有打出来,这台机器没能确认 `/` 仍是 Static,也没有首屏 gzip 数字。没有为了构建改符号链接,也没有在工作树里重装依赖。首页 `page.tsx` 没改;校正卡片组件有改,gzip 要等能建符号链接的环境再量。
本轮没有打开浏览器,也没有在手机上点采用。真机步骤在 `docs/testing/rectification-delivery-dup-adopt-20261006.md`。修复版本待发布,没有推送,没有部署。
@@ -0,0 +1,22 @@
# 真机清单:校正交付一次一段,采用和卡片一致(BUG-1241~1243)
修复还在分支 `codex/rectification-delivery-dup-adopt-20261006`,没有推上测试环境。等测试环境 `/api/health` 的 `deployment.gitCommit` 等于这次推上去的提交后,再用手机走下面几步。本轮没有在浏览器或手机上点过。
准备一条已经走到并列交付的校正。开口窗口要比最后留下的分钟范围宽。出生年份要让至少一道还没问的区分题落在成年下限以下。不要把真实出生资料写进反馈,只记结构。
## 怎样算通过
1. 答完最后一题后,对话里只有一段交付说明。刷新一次,仍然只有这一段,不要再出现另一段「现在还剩几个候选」。
2. 卡片还在。可以采用时才有采用按钮,以及「采用后按…排盘」。不能采用时卡片仍在,没有按钮,也没有那一行,界面上不出现 `adoption_not_allowed`。
3. 可以点的那次采用要成功。成功后卡片仍在,并显示「已采用」。再刷新一次,卡片还在,仍是已采用,不要回到还能点采用、点了却被拒绝。
4. 若这次结果还不能采用,点不到按钮。不要看到英文代码。
## 记下来
| 看什么 | 结果 |
| --- | --- |
| 交付说明是一段还是两段 | |
| 刷新后段数变不变 | |
| 有没有采用按钮 | |
| 点采用之后卡片是否显示已采用 | |
| 再刷新是否仍是已采用 | |