Merge remote-tracking branch 'origin/staging' into codex/consult-no-presupposition-backtest-20261001
Independent Staging Quality Gate / validate (push) Successful in 17m11s
Independent Staging Quality Gate / publish (push) Successful in 3m23s

# Conflicts:
#	CHANGELOG.md
#	docs/BUG_HISTORY.md
This commit is contained in:
Jesse_Chen
2026-10-02 07:17:06 +08:00
10 changed files with 216 additions and 3 deletions
+7
View File
@@ -1,5 +1,6 @@
# 印度占星 Skill 更新日志 # 印度占星 Skill 更新日志
<<<<<<< ours
## 2026-10-02 — 普通对话不再预设处境;受冲时请你确认;你纠正时先认下 ## 2026-10-02 — 普通对话不再预设处境;受冲时请你确认;你纠正时先认下
- (同日第二次调整)盘上受冲多时,回答只说「这条线有压力或距离的迹象」并给出现实里可能的几种情况,不再替你断定父母在不在身边、有几段感情、身体好不好,也不写「底子不差」「关系稳」这类反向安慰;最后请你说一句实际情况,全篇只问一次。婚姻写「感情这条线上有波折的迹象」。事业先讲一生的事业形态,三层机会不再用「面试 / 职位」。你纠正时只对上真正对得上的几条,分不出的会直说「盘本身分不出这一点」。 - (同日第二次调整)盘上受冲多时,回答只说「这条线有压力或距离的迹象」并给出现实里可能的几种情况,不再替你断定父母在不在身边、有几段感情、身体好不好,也不写「底子不差」「关系稳」这类反向安慰;最后请你说一句实际情况,全篇只问一次。婚姻写「感情这条线上有波折的迹象」。事业先讲一生的事业形态,三层机会不再用「面试 / 职位」。你纠正时只对上真正对得上的几条,分不出的会直说「盘本身分不出这一点」。
@@ -17,6 +18,12 @@
- 「身心压力」改名「健康」,默认问题改为「我的身体底子怎样,哪些方面要多留意?」,先讲体质底子;不诊断、不给治疗建议不变(BUG-1163)。 - 「身心压力」改名「健康」,默认问题改为「我的身体底子怎样,哪些方面要多留意?」,先讲体质底子;不诊断、不给治疗建议不变(BUG-1163)。
- 暂未生效:只问年运或只问时运的轮次,因字数上限(12,000 字符)没有余量,暂不加通用读法和年运清单,待产品决定。Skill 版本不变;不改数据库结构。 - 暂未生效:只问年运或只问时运的轮次,因字数上限(12,000 字符)没有余量,暂不加通用读法和年运清单,待产品决定。Skill 版本不变;不改数据库结构。
## 2026-10-02 — 采用后答完核对题,会说这件事和采用的时间对不对得上
- 采用后答核对题,回复先说结果:「这件事和 04:55 对得上」「对不上;如果别的经历也对不上,可以回到上面的卡片改选」或「分不出」;盘型口径下再说「盘型结论不变」或给出变化后的各盘上升(BUG-1170)。
- 题数规则不变(至多 2 道)。Skill 版本不变;不改数据库结构。
>>>>>>> theirs
## 2026-10-01 — 普通对话的数据卡看得到「谁照谁、谁和谁同宫、燃烧」,瑜伽按领域分 ## 2026-10-01 — 普通对话的数据卡看得到「谁照谁、谁和谁同宫、燃烧」,瑜伽按领域分
- 数据卡每颗星带上被哪些星照到、和谁同宫、是否燃烧;各领域选中的宫带上被哪些星照到(BUG-1154、BUG-1155)。 - 数据卡每颗星带上被哪些星照到、和谁同宫、是否燃烧;各领域选中的宫带上被哪些星照到(BUG-1154、BUG-1155)。
+19
View File
@@ -15605,6 +15605,7 @@
- 复发自:无 - 复发自:无
- 修复版本:缓解随 `codex/consult-card-affliction-data-20261001`;其余无 - 修复版本:缓解随 `codex/consult-card-affliction-data-20261001`;其余无
<<<<<<< ours
## BUG-1160 | 普通对话没有「先看受冲、再看旺弱」的读法,模型默认「旺 = 好」 ## BUG-1160 | 普通对话没有「先看受冲、再看旺弱」的读法,模型默认「旺 = 好」
- 状态:resolved(文本层;2026-10-02 产品定年运 / 时运字数口径后全部领域生效) - 状态:resolved(文本层;2026-10-02 产品定年运 / 时运字数口径后全部领域生效)
@@ -15743,3 +15744,21 @@
- 复发自:无 - 复发自:无
- 决策日志(2026-10-02):产品 2026-10-02 决定(名人回测第一轮对照组误报 1→15;Claude 按卡片确定性计数核对,在场与缺席的父亲受冲信号数相同,盘分不出):事业清单去求职口吻、加「一生事业形态」首行(见 BUG-1162)。第二轮回测 16 / 18 → 12 / 18,仍未过 ≤ 3。剩余 12 份多是模型按 AL / A10 / 10 宫主落宫读形态时确实读不出公众型(乔布斯 A10 落 12 宫、奥巴马 10 宫主落 6 宫、Bush / Zidane 10 宫主落 1 宫),事业形态与盘面信号的对应需要占星口径决定,不是措辞问题。状态维持 investigating。提交 `066558a6`。 - 决策日志(2026-10-02):产品 2026-10-02 决定(名人回测第一轮对照组误报 1→15;Claude 按卡片确定性计数核对,在场与缺席的父亲受冲信号数相同,盘分不出):事业清单去求职口吻、加「一生事业形态」首行(见 BUG-1162)。第二轮回测 16 / 18 → 12 / 18,仍未过 ≤ 3。剩余 12 份多是模型按 AL / A10 / 10 宫主落宫读形态时确实读不出公众型(乔布斯 A10 落 12 宫、奥巴马 10 宫主落 6 宫、Bush / Zidane 10 宫主落 1 宫),事业形态与盘面信号的对应需要占星口径决定,不是措辞问题。状态维持 investigating。提交 `066558a6`。
- 修复版本:`codex/consult-no-presupposition-backtest-20261001`(未推送,部分) - 修复版本:`codex/consult-no-presupposition-backtest-20261001`(未推送,部分)
## BUG-1170 | 采用后答完核对题只说「前事核对到这里」,不说对不对得上
- 状态:resolved(2026-10-02 Claude 直接执行;产品 10-01 选 A)
- 首次发现 / 最近更新:2026-10-01 / 2026-10-02
- 影响面:`v9/answer-choice.ts::persistApplied`(已采用且所答焦点是 reverse_verify / out_of_sample_check)、新增 `v9/post-adopt-verdict.ts`。
- 用户现象:采用后答了「2016 年 9 月那次升学」核对题,回复只有「前事核对到这里。之后新建对话即按已采用时间排盘;对不上随时改选。」用户不知道答案说明了什么,只问一道就结束。
- 触发条件:已采用,答一道核对题(跳过不算)。
- 根因:收尾文案只看「还有没有下一道」,不看刚答的这道对采用时间是支持还是冲突。只问一道是规则所致(采用后核对至多 2 道,只取采用前没问过、不在用户已报年份/领域、未拒答的探针;该案只剩 1 道),不是本条要改的。
- 决策记录:产品 10-01 选 A——收尾先说结果,题数规则不变。
- 修复:`postAdoptCheckVerdict` 用该探针按所答类别的 supports / conflicts,对照「采用分钟所在候选」(簇含该分钟,否则最近):支持→「这件事和 HH:MM 对得上。」;冲突→「这件事和 HH:MM 对不上;如果别的经历也对不上,可以回到上面的卡片改选。」;都不涉及→「这件事分不出 HH:MM 和别的时间。」;记不清→「这件事记不清,不算。」盘型口径再比对答题前后的分盘摘要:没变→「盘型结论不变。」,变了→「盘型结论有变化:<各目标盘上升与档位>」。结果句放在收尾句或下一道题之前。只用服务端事实,不写「确认 / 精确」。
- 验证:`frontend/tests/rectification-post-adopt-verdict-20261002.test.ts`(候选映射、四种结果、盘型变/不变);`frontend/tests/rectification-answer-choice.test.ts` 新增作答最后一道核对题用例(去掉接线即红);本机 PG17 真库重放虚构案例:回复为「这件事和 15:26 对得上。盘型结论不变。」+ 收尾句。
- 防复发:采用后作答的回复必须带结果句;跳过保持原收尾句(既有用例锁定)。
- 相关记录:BUG-536~538、BUG-1146、BUG-1150。
- 复发自:无
- 修复版本:`codex/rectification-post-adopt-verify-20261001`。
>>>>>>> theirs
@@ -63,3 +63,7 @@ Python 快速门:本单不改 `scripts/**`、`tests/**`,未跑。`test:db`
## 第四轮:交付正文重复(BUG-1153,我在 BUG-1149 引入) ## 第四轮:交付正文重复(BUG-1153,我在 BUG-1149 引入)
真机:交付时「D1 上升…用了 5 道选择题」连出两条。gate 收尾轮次在 BUG-1149 后首次真的落库,与答题回复里的同一段重复。修:写前比对最近一条助手消息。真机另报两点:①采用消息里题干「出现两次」——服务端与单测均确认正文已剥题干,粘贴里多出的一遍来自选项卡给读屏用的隐藏标题(sr-only legend)被鼠标选中复制,屏幕上只显示一次;②答一道核对题就结束——按现行规则:采用后核对题至多 2 道,且只取采用前没问过、不在用户已报年份/领域、未拒答的探针,该案只剩 1 道,属设计,已向产品提出改进选项。 真机:交付时「D1 上升…用了 5 道选择题」连出两条。gate 收尾轮次在 BUG-1149 后首次真的落库,与答题回复里的同一段重复。修:写前比对最近一条助手消息。真机另报两点:①采用消息里题干「出现两次」——服务端与单测均确认正文已剥题干,粘贴里多出的一遍来自选项卡给读屏用的隐藏标题(sr-only legend)被鼠标选中复制,屏幕上只显示一次;②答一道核对题就结束——按现行规则:采用后核对题至多 2 道,且只取采用前没问过、不在用户已报年份/领域、未拒答的探针,该案只剩 1 道,属设计,已向产品提出改进选项。
## 第五轮:核对题答完说结果(BUG-1170,产品选 A)
新增 `v9/post-adopt-verdict.ts`;`persistApplied` 已采用且所答为核对题时,在收尾句或下一题前加结果句。真库重放:「这件事和 15:26 对得上。盘型结论不变。」+ 收尾句。题数规则不变。
+1 -1
View File
@@ -265,7 +265,7 @@
| `TASK-rectification-chart-tier-caveats-20261001.md` | — | **报告与聊天标注判断不了的分盘**:采用后读 `active_birth_provenance`(segment-v1)→ 结果 `segment_summary` 档位,blocked/indistinct 的 D9/D10 在聊天 answer_policy 与一句说明、报告章节顶部提示 + 主题降级;旧资料逐字节不变;不改库、不改 consult/route.ts | 待领取 | BUG-1138~1139 | | `TASK-rectification-chart-tier-caveats-20261001.md` | — | **报告与聊天标注判断不了的分盘**:采用后读 `active_birth_provenance`(segment-v1)→ 结果 `segment_summary` 档位,blocked/indistinct 的 D9/D10 在聊天 answer_policy 与一句说明、报告章节顶部提示 + 主题降级;旧资料逐字节不变;不改库、不改 consult/route.ts | 待领取 | BUG-1138~1139 |
| `TASK-rectification-angle-timing-research-20261001.md` | `PROGRESS-rectification-angle-timing-research-20261001.md` | **角度类时间技法研究(离线)**:行运土木罗压本命上升 / 天顶度数、次限推运角、太阳弧角(1° ≈ 4 分钟出生时间),77 例 v5;乱序日期对照 ≥ 50 次必做、留一法、不得挤出真值;显著才叠加到六题后验比头段命中与区间宽度 | 已实现待验收(0/9 预登记检验显著,BUG-1141 closed_by_design;分支 codex/rectification-angle-timing-research-20261001,未推送) | BUG-1141 | | `TASK-rectification-angle-timing-research-20261001.md` | `PROGRESS-rectification-angle-timing-research-20261001.md` | **角度类时间技法研究(离线)**:行运土木罗压本命上升 / 天顶度数、次限推运角、太阳弧角(1° ≈ 4 分钟出生时间),77 例 v5;乱序日期对照 ≥ 50 次必做、留一法、不得挤出真值;显著才叠加到六题后验比头段命中与区间宽度 | 已实现待验收(0/9 预登记检验显著,BUG-1141 closed_by_design;分支 codex/rectification-angle-timing-research-20261001,未推送) | BUG-1141 |
| (直接执行,无任务书) | `PROGRESS-rectification-segment-order-20261001.md` | **健康区分题写入失败(BUG-1143)+ 按盘型挑题默认开启(BUG-1144)**:区分类焦点把 `health_pressure` 原样写入、违反表约束、写入被吞后提前交付(BUG-672 同族);修后 77 例真库回放平均问题数 ±10 2.8→4.6、±30 3.6→5.4,D10 头段 ±30 39→52;按段选题 ≤61 分钟默认开,±30 D9 46→51、D10 52→55,真值段 76/76 | 已验收(Claude 10-01 直接执行) | 分支 codex/rectification-segment-order-20261001 | | (直接执行,无任务书) | `PROGRESS-rectification-segment-order-20261001.md` | **健康区分题写入失败(BUG-1143)+ 按盘型挑题默认开启(BUG-1144)**:区分类焦点把 `health_pressure` 原样写入、违反表约束、写入被吞后提前交付(BUG-672 同族);修后 77 例真库回放平均问题数 ±10 2.8→4.6、±30 3.6→5.4,D10 头段 ±30 39→52;按段选题 ≤61 分钟默认开,±30 D9 46→51、D10 52→55,真值段 76/76 | 已验收(Claude 10-01 直接执行) | 分支 codex/rectification-segment-order-20261001 |
| (直接执行,无任务书) | `PROGRESS-rectification-post-adopt-card-20261001.md` | **采用后核对题无按钮(BUG-1146)+ 盘型口径回复念分钟排名(BUG-1147)**:keep 分支丢探针身份致 GET 空卡(BUG-498 复发);采用接口不写轮次、只读运行被 60 秒交付窗口挡掉,题挂到上一题回复下;改为采用接口写「已采用 HH:MM。再核对一件过去的事。」轮次并挂题。盘型口径下去掉「领先 / 落后」、交付旁白不再说「分不开」、边界句只说一次 | 已验收(Claude 10-01 直接执行);真机回报后补修 BUG-1149(请求号非 uuid 被真库拒写)、BUG-1150(核对题作答 stale_probe),本机真库重放通过;BUG-1151 盘型报告去分钟排名(产品选 A);BUG-1153 交付正文连出两条(BUG-1149 引入的回归);真机清单 `docs/testing/rectification-post-adopt-card-20261001.md` | 分支 codex/rectification-post-adopt-verify-20261001 | | (直接执行,无任务书) | `PROGRESS-rectification-post-adopt-card-20261001.md` | **采用后核对题无按钮(BUG-1146)+ 盘型口径回复念分钟排名(BUG-1147)**:keep 分支丢探针身份致 GET 空卡(BUG-498 复发);采用接口不写轮次、只读运行被 60 秒交付窗口挡掉,题挂到上一题回复下;改为采用接口写「已采用 HH:MM。再核对一件过去的事。」轮次并挂题。盘型口径下去掉「领先 / 落后」、交付旁白不再说「分不开」、边界句只说一次 | 已验收(Claude 10-01 直接执行);真机回报后补修 BUG-1149(请求号非 uuid 被真库拒写)、BUG-1150(核对题作答 stale_probe),本机真库重放通过;BUG-1151 盘型报告去分钟排名(产品选 A);BUG-1153 交付正文连出两条(BUG-1149 引入的回归);BUG-1170 核对题答完说对不对得上(产品选 A);真机清单 `docs/testing/rectification-post-adopt-card-20261001.md` | 分支 codex/rectification-post-adopt-verify-20261001 |
| `TASK-rectification-typed-event-scoring-research-20260929.md` | `PROGRESS-rectification-typed-event-research-20260929.md` | **打字经历按选择题规则计分(离线研究,不上线)**:计分通道不对称 + 已入账年份挡题;R0 学业质量题措辞 / 年精度显示成 1 月(冻结文件,需重新冻结) | 已验收关单(Claude 10-01:三类技法 9 项检验 0 显著,真值平均排位 0.49~0.59;Claude 补正对照——埋入信号排位 0.077、挪年 0.502——流程有效;BUG-1141 closed_by_design) | BUG-1088、1089 | | `TASK-rectification-typed-event-scoring-research-20260929.md` | `PROGRESS-rectification-typed-event-research-20260929.md` | **打字经历按选择题规则计分(离线研究,不上线)**:计分通道不对称 + 已入账年份挡题;R0 学业质量题措辞 / 年精度显示成 1 月(冻结文件,需重新冻结) | 已验收关单(Claude 10-01:三类技法 9 项检验 0 显著,真值平均排位 0.49~0.59;Claude 补正对照——埋入信号排位 0.077、挪年 0.502——流程有效;BUG-1141 closed_by_design) | BUG-1088、1089 |
| `TASK-report-reader-polish-20260929.md` | `PROGRESS-report-reader-polish-20260929.md` | **报告页打磨**:生成入口挪进页面主体(删标题栏按钮)、详情页到底部按钮(懒渲染一次到底)、导出按钮带文字、目录一级/二级分层、去掉「字段」与 RL/NL 缩写表头、状态列同义重复去重、报告表格淡底色、分块导出显示真实文件大小 | Claude 直接执行并自验(tsc 0、lint 0 error、全量 fail 与基线同 24 条、`/` Static、gzip +7 B、快速门 Python 1000 passed),已部署 staging `d7772011`,真机欠 | BUG-1092~1094 | | `TASK-report-reader-polish-20260929.md` | `PROGRESS-report-reader-polish-20260929.md` | **报告页打磨**:生成入口挪进页面主体(删标题栏按钮)、详情页到底部按钮(懒渲染一次到底)、导出按钮带文字、目录一级/二级分层、去掉「字段」与 RL/NL 缩写表头、状态列同义重复去重、报告表格淡底色、分块导出显示真实文件大小 | Claude 直接执行并自验(tsc 0、lint 0 error、全量 fail 与基线同 24 条、`/` Static、gzip +7 B、快速门 Python 1000 passed),已部署 staging `d7772011`,真机欠 | BUG-1092~1094 |
| `TASK-report-english-edition-20260929.md` | `PROGRESS-report-english-edition-20260929.md` | **报告中英两版**:同一次引擎计算渲染 zh/en 两遍(不用模型翻译),瑜伽库 477 条补英文、模板与前端表头英文化;中文逐字节不变、英文零汉字、两版数字序列一致;阅读页 `?lang=en` 切换,导出当前语言 + 「问 AI 建议导出英文版」提示;旧报告不补英文 | Claude 直接执行(含两个 fork 子代理)并自验(tsc 0、lint 0 error、全量 fail 与基线同 24 条、`/` Static、gzip +0.06%、Python 定向全绿),已部署 staging `d7772011`,真机欠 | — | | `TASK-report-english-edition-20260929.md` | `PROGRESS-report-english-edition-20260929.md` | **报告中英两版**:同一次引擎计算渲染 zh/en 两遍(不用模型翻译),瑜伽库 477 条补英文、模板与前端表头英文化;中文逐字节不变、英文零汉字、两版数字序列一致;阅读页 `?lang=en` 切换,导出当前语言 + 「问 AI 建议导出英文版」提示;旧报告不补英文 | Claude 直接执行(含两个 fork 子代理)并自验(tsc 0、lint 0 error、全量 fail 与基线同 24 条、`/` Static、gzip +0.06%、Python 定向全绿),已部署 staging `d7772011`,真机欠 | — |
@@ -10,7 +10,7 @@
- 出现一条新的助手消息「已采用 HH:MM。再核对一件过去的事。」,不是挂在上一题「已记录…」下面; - 出现一条新的助手消息「已采用 HH:MM。再核对一件过去的事。」,不是挂在上一题「已记录…」下面;
- 如果有核对题,题目下有**四个按钮**,可以点。 - 如果有核对题,题目下有**四个按钮**,可以点。
4. **刷新**:刷新页面,核对题还在、四个按钮还在、仍挂在那条采用消息下面。 4. **刷新**:刷新页面,核对题还在、四个按钮还在、仍挂在那条采用消息下面。
5. **答核对题**:点一个按钮,不出现「这次没提交上」,能进入下一题或看到「前事核对到这里…」。 5. **答核对题**:点一个按钮,不出现「这次没提交上」;回复先说「这件事和 HH:MM 对得上 / 对不上 / 分不出」,再说「盘型结论不变」或给出变化,然后进入下一题或看到「前事核对到这里…」。
5a. **选中时**:点「更像这个」后,上面的旁白和卡片不应消失;如果仍消失一下,请录屏发给 Claude。 5a. **选中时**:点「更像这个」后,上面的旁白和卡片不应消失;如果仍消失一下,请录屏发给 Claude。
5b. **验证报告**:交付卡点「查看验证报告」,里面没有「当前候选基本并列」「本命上升(该分钟)」和「排名 / 时间 / 相对支持」表。 5b. **验证报告**:交付卡点「查看验证报告」,里面没有「当前候选基本并列」「本命上升(该分钟)」和「排名 / 时间 / 相对支持」表。
6. **旧校正会话**:从会话列表打开一个以前的校正会话,能打开、不报错。 6. **旧校正会话**:从会话列表打开一个以前的校正会话,能打开、不报错。
+1 -1
View File
@@ -276,7 +276,7 @@ Jyotisha 的可见文案是产品的一部分。正确性红线(真实性、
| 已答6轮,再问下去也分不开04:53和05:00,没有年份的分盘题也不再问……两者相对支持度都是16。 | 再问下去也分不开 04:53 和 05:00。这两个时间按现有信息分不开。范围和代表分钟照常说。 | 旁白不得念内部计分词。卡上的数字按上一行的 5 个百分点规则显示,旁白不念百分比。 | | 已答6轮,再问下去也分不开04:53和05:00,没有年份的分盘题也不再问……两者相对支持度都是16。 | 再问下去也分不开 04:53 和 05:00。这两个时间按现有信息分不开。范围和代表分钟照常说。 | 旁白不得念内部计分词。卡上的数字按上一行的 5 个百分点规则显示,旁白不念百分比。 |
| 候选 A 预测下一次事业变动更可能在 2027 年 3 月附近。 | 每列用服务器拼好的一行时段;没有窗就写「未来一年没有明显的时段」;没算出来写「这一分钟还没对照」。 | 窗口按候选分钟各算,模型不写。不得把「没算」说成「没有窗」。 | | 候选 A 预测下一次事业变动更可能在 2027 年 3 月附近。 | 每列用服务器拼好的一行时段;没有窗就写「未来一年没有明显的时段」;没算出来写「这一分钟还没对照」。 | 窗口按候选分钟各算,模型不写。不得把「没算」说成「没有窗」。 |
| 「再补一件经历」后卡片消失、输入框上方空白 | 需要补经历时继续在输入框说下一件;交付卡不再提供「再补一件经历」按钮。 | 交付卡只负责选分钟。 | | 「再补一件经历」后卡片消失、输入框上方空白 | 需要补经历时继续在输入框说下一件;交付卡不再提供「再补一件经历」按钮。 | 交付卡只负责选分钟。 |
| 采用后会拿盘外核对来验证。 / 已采用 04:53 · 范围 … / 改选 / 之后新建对话即按此时间排盘。 | 没有核对题:已采用 04:53,新建对话即按这个时间排盘。有核对题:前事核对到这里。之后新建对话即按已采用时间排盘;对不上随时改选。 | 采用状态在该列按钮上;整张卡留着。收尾句跟卡片同一列,不单独贴左,也不回输入框上方。 | | 采用后会拿盘外核对来验证。 / 已采用 04:53 · 范围 … / 改选 / 之后新建对话即按此时间排盘。 | 没有核对题:已采用 04:53,新建对话即按这个时间排盘。有核对题:前事核对到这里。之后新建对话即按已采用时间排盘;对不上随时改选。答完核对题先说结果(BUG-1170):「这件事和 04:53 对得上。」/「这件事和 04:53 对不上;如果别的经历也对不上,可以回到上面的卡片改选。」/「这件事分不出 04:53 和别的时间。」/「这件事记不清,不算。」;盘型口径再接「盘型结论不变。」或「盘型结论有变化:…」。 | 采用状态在该列按钮上;整张卡留着。收尾句跟卡片同一列,不单独贴左,也不回输入框上方。 |
| (采用后只念代表分钟)代表分钟是 05:12。 | 这 27 分钟里,事业方向、性格底色的判断是稳定的;婚恋(D9)会随分钟变,看盘时按范围读。这不是已确认的唯一出生分钟。 | 采用后先讲区间里什么稳、什么随分钟变;两句来自服务端,不是模型编的。 | | (采用后只念代表分钟)代表分钟是 05:12。 | 这 27 分钟里,事业方向、性格底色的判断是稳定的;婚恋(D9)会随分钟变,看盘时按范围读。这不是已确认的唯一出生分钟。 | 采用后先讲区间里什么稳、什么随分钟变;两句来自服务端,不是模型编的。 |
| 当前问题已更新,请刷新后重新作答。 | 这一问刚换成新的,刷新后再答就行。 | 机器状态句改成下一步。 | | 当前问题已更新,请刷新后重新作答。 | 这一问刚换成新的,刷新后再答就行。 | 机器状态句改成下一步。 |
| 选择题处理失败,请稍后重试。 / 这道题已经过期,请回答当前问题(点选没提交上,下面又画出同一张卡) | 这次没提交上,请再点一次。 | 点选没到服务器时撤回已选,只留一张可点的卡,错误行就这一句。不说「稍后」,不把服务端状态码翻成话。 | | 选择题处理失败,请稍后重试。 / 这道题已经过期,请回答当前问题(点选没提交上,下面又画出同一张卡) | 这次没提交上,请再点一次。 | 点选没到服务器时撤回已选,只留一张可点的卡,错误行就这一句。不说「稍后」,不把服务端状态码翻成话。 |
@@ -7,6 +7,7 @@
*/ */
import { createHash } from "node:crypto"; import { createHash } from "node:crypto";
import { postAdoptCheckVerdict } from "./post-adopt-verdict.ts";
import { posteriorMap, scoreDeltas } from "../core/decision-fingerprint"; import { posteriorMap, scoreDeltas } from "../core/decision-fingerprint";
import { productDeliveryForDomain } from "./product-delivery.ts"; import { productDeliveryForDomain } from "./product-delivery.ts";
import { nextProbe } from "../core/build-state"; import { nextProbe } from "../core/build-state";
@@ -2415,6 +2416,26 @@ ${nextInterview.hostNarration}`;
hostNarration = adoptionNarration ?? completedRangeNarration ?? hostNarration; hostNarration = adoptionNarration ?? completedRangeNarration ?? hostNarration;
} }
// BUG-1170: an answered post-adopt check says what it meant for the adopted time.
const answeredIntent = input.dossier.conversationSummary.activeFocus?.intent;
if (accepted && !skipThisProbe && input.userStopped !== true
&& (answeredIntent === "reverse_verify" || answeredIntent === "out_of_sample_check")) {
const before = inferenceForPersistedAnswer(input.dossier.latestResult?.decisionReceipt ?? null);
const after = input.decisionState ?? before;
const segmentProduct = input.dossier.case.rectificationDomain !== null && input.dossier.case.rectificationDomain !== undefined;
const verdict = postAdoptCheckVerdict({
acceptedTime: input.dossier.case.acceptedTime,
probe: input.probeId
? (after?.probes.find((probe) => probe.id === input.probeId) ?? before?.probes.find((probe) => probe.id === input.probeId) ?? null)
: null,
answerClass: input.answerClass,
candidates: after?.candidates ?? [],
segmentBefore: segmentProduct ? before?.segment_summary ?? null : null,
segmentAfter: segmentProduct ? after?.segment_summary ?? null : null,
});
if (verdict) hostNarration = hostNarration ? `${verdict}\n\n${hostNarration}` : verdict;
}
const choiceStem = nextChoiceReady ? renderableChoiceStem(nextFocus) : null; const choiceStem = nextChoiceReady ? renderableChoiceStem(nextFocus) : null;
if (choiceStem) { if (choiceStem) {
const body = skipThisProbe const body = skipThisProbe
@@ -0,0 +1,81 @@
/**
* What a post-adopt check said about the adopted time (BUG-1170, product 10-01 option A).
*
* The closing line used to be only 「前事核对到这里」: the reader answered and learned nothing.
* The verdict reads the answered probe's own expected outcomes for the candidate that holds the
* adopted minute, and — in the segment product — whether any target chart's ascendant or tier
* moved with this answer. Server facts only; no model text.
*/
import type { AnswerClass, ConflictProbe, InferenceCandidate } from "../core/types.ts";
import { chartSummaryCopy, type SegmentSummary } from "../core/segment-summary.ts";
export type PostAdoptVerdictInput = Readonly<{
acceptedTime: string | null | undefined;
probe: ConflictProbe | null | undefined;
answerClass: AnswerClass | null | undefined;
candidates: readonly InferenceCandidate[];
segmentBefore?: SegmentSummary | null;
segmentAfter?: SegmentSummary | null;
}>;
function minutes(clock: string): number | null {
const match = /^(\d{2}):(\d{2})/.exec(clock);
return match ? Number(match[1]) * 60 + Number(match[2]) : null;
}
function inRange(clock: number, range: readonly [string, string]): boolean {
const start = minutes(range[0]);
const end = minutes(range[1]);
if (start === null || end === null) return false;
return start <= end ? clock >= start && clock <= end : clock >= start || clock <= end;
}
/** The candidate that stands for the adopted minute: its cluster holds it, else the nearest. */
export function candidateForAdoptedMinute(
acceptedTime: string,
candidates: readonly InferenceCandidate[],
): InferenceCandidate | null {
const clock = acceptedTime.slice(0, 5);
const at = minutes(clock);
if (at === null || candidates.length === 0) return null;
const holding = candidates.find((candidate) => candidate.time === clock || candidate.cluster_times?.includes(clock))
?? candidates.find((candidate) => inRange(at, candidate.cluster_range));
if (holding) return holding;
const distance = (candidate: InferenceCandidate) => {
const value = minutes(candidate.time);
if (value === null) return Number.POSITIVE_INFINITY;
const raw = Math.abs(value - at);
return Math.min(raw, 1440 - raw);
};
return [...candidates].sort((left, right) => distance(left) - distance(right))[0] ?? null;
}
function chartsMoved(before: SegmentSummary, after: SegmentSummary): boolean {
return after.charts.some((chart) => {
const prior = before.charts.find((row) => row.chart === chart.chart);
return !prior || prior.sign !== chart.sign || prior.tier !== chart.tier;
});
}
export function postAdoptCheckVerdict(input: PostAdoptVerdictInput): string | null {
const clock = input.acceptedTime?.trim().slice(0, 5) ?? "";
if (!clock || !input.answerClass) return null;
if (input.answerClass === "unsure") return "这件事记不清,不算。";
if (!input.probe) return null;
const candidate = candidateForAdoptedMinute(clock, input.candidates);
const outcome = input.probe.expected_outcomes.find((row) => row.answer_class === input.answerClass) ?? null;
const ids = candidate ? new Set([candidate.id, candidate.time]) : new Set<string>();
const supports = Boolean(outcome?.supports.some((id) => ids.has(id)));
const conflicts = Boolean(outcome?.conflicts.some((id) => ids.has(id)));
const fit = supports && !conflicts
? `这件事和 ${clock} 对得上。`
: conflicts && !supports
? `这件事和 ${clock} 对不上;如果别的经历也对不上,可以回到上面的卡片改选。`
: `这件事分不出 ${clock} 和别的时间。`;
const before = input.segmentBefore ?? null;
const after = input.segmentAfter ?? null;
if (!before || !after) return fit;
return chartsMoved(before, after)
? `${fit}盘型结论有变化:${chartSummaryCopy(after)}`
: `${fit}盘型结论不变。`;
}
@@ -1795,6 +1795,30 @@ function lastVerifyDossier() {
})); }));
} }
test("answering the last post-adopt verify question says what it meant for the adopted time (BUG-1170)", async () => {
const dossier = withAcceptedTime(lastVerifyDossier());
const accounting = persistChoiceAccounting(dossier, nextFocusHandler());
const applied = await applyRectificationChoice(accounting.client, {
userId: USER_ID,
caseId: CASE_ID,
sessionId: SESSION_ID,
actionId: ACTION_ID,
action: CHOICE_ACTION,
focusId: FOCUS_ID,
questionId: QUESTION_ID,
probeId: EDUCATION_2014_PROBE.id,
optionId: "A",
expectedRevision: (
dossier.latest_result as {
decision_receipt: { inference_state: { revision: number } };
}
).decision_receipt.inference_state.revision,
});
assert.match(applied.narration, /^这件事(和 05:07 (对得上|对不上|分不出)|记不清)/);
assert.ok(applied.narration.endsWith(RECTIFICATION_USER_COPY.postAdoptVerifyDone), applied.narration);
assert.doesNotMatch(applied.narration, /确认|精确|领先|落后/);
});
test("skipping the last post-adopt verify question closes with start_consultation", async () => { test("skipping the last post-adopt verify question closes with start_consultation", async () => {
const dossier = withAcceptedTime(lastVerifyDossier()); const dossier = withAcceptedTime(lastVerifyDossier());
const accounting = persistChoiceAccounting(dossier, nextFocusHandler()); const accounting = persistChoiceAccounting(dossier, nextFocusHandler());
@@ -0,0 +1,57 @@
import assert from "node:assert/strict";
import test from "node:test";
import { candidateForAdoptedMinute, postAdoptCheckVerdict } from "../src/lib/rectification-agentic/v9/post-adopt-verdict.ts";
import type { SegmentSummary } from "../src/lib/rectification-agentic/core/segment-summary.ts";
// BUG-1170 (product 10-01 option A): after a post-adopt check the reply says whether the event
// fits the adopted time, and whether the chart verdicts moved — not only 「前事核对到这里」.
const candidate = (time: string, range: [string, string], clusterTimes?: string[]) => ({
id: time, time, cluster_range: range, ...(clusterTimes ? { cluster_times: clusterTimes } : {}),
prior_score: 10, posterior_score: 10, probability: 0.5, status: "active", rank: 1, strong_conflict_count: 0,
}) as never;
const CANDIDATES = [candidate("04:53", ["04:48", "04:57"], ["04:53", "04:55"]), candidate("05:03", ["05:00", "05:07"])];
const probe = {
id: "probe:education.2016.09.known_event_quality:x", semantic_key: "education.2016.09.known_event_quality",
candidate_split_hash: "x", domain: "education", year: 2016, question: "2016", candidate_ids: ["04:53", "05:03"],
information_gain: 0.4, source: "known_event_quality",
expected_outcomes: [
{ answer_class: "yes", supports: ["04:53"], conflicts: ["05:03"] },
{ answer_class: "no", supports: ["05:03"], conflicts: ["04:53"] },
{ answer_class: "weak_yes", supports: [], conflicts: [] },
],
} as never;
const summary = (tier: string, sign: number): SegmentSummary => ({
targets: ["D1", "D9"], window_minutes: 21, scan_complete: true, no_rectification_needed: false, adoption_minute: null,
charts: [
{ chart: "D1", sign: 3, tier: "certain", alternatives: [], tied: false },
{ chart: "D9", sign, tier, alternatives: [], tied: false },
],
}) as never;
test("the adopted minute maps to the candidate whose cluster holds it, else the nearest", () => {
assert.equal(candidateForAdoptedMinute("04:55:00", CANDIDATES)?.time, "04:53");
assert.equal(candidateForAdoptedMinute("05:01", CANDIDATES)?.time, "05:03");
assert.equal(candidateForAdoptedMinute("05:30", CANDIDATES)?.time, "05:03", "outside every cluster: nearest");
});
test("verdict: fits, does not fit, cannot tell, and unsure", () => {
const base = { acceptedTime: "04:55:00", probe, candidates: CANDIDATES };
assert.equal(postAdoptCheckVerdict({ ...base, answerClass: "yes" }), "这件事和 04:55 对得上。");
assert.equal(postAdoptCheckVerdict({ ...base, answerClass: "no" }),
"这件事和 04:55 对不上;如果别的经历也对不上,可以回到上面的卡片改选。");
assert.equal(postAdoptCheckVerdict({ ...base, answerClass: "weak_yes" }), "这件事分不出 04:55 和别的时间。");
assert.equal(postAdoptCheckVerdict({ ...base, answerClass: "unsure" }), "这件事记不清,不算。");
assert.equal(postAdoptCheckVerdict({ ...base, answerClass: null }), null);
assert.equal(postAdoptCheckVerdict({ ...base, acceptedTime: null, answerClass: "yes" }), null);
assert.equal(postAdoptCheckVerdict({ ...base, probe: null, answerClass: "yes" }), null, "no probe, no claim");
});
test("segment product says whether the chart verdicts moved", () => {
const base = { acceptedTime: "04:55", probe, candidates: CANDIDATES, answerClass: "yes" as const };
assert.equal(postAdoptCheckVerdict({ ...base, segmentBefore: summary("credible", 7), segmentAfter: summary("credible", 7) }),
"这件事和 04:55 对得上。盘型结论不变。");
const moved = postAdoptCheckVerdict({ ...base, segmentBefore: summary("credible", 7), segmentAfter: summary("tentative", 6) });
assert.match(moved!, /^这件事和 04:55 对得上。盘型结论有变化:D1 上升:巨蟹(确定);D9 上升:天秤(倾向,备选也要看)。$/);
for (const text of [moved!]) assert.doesNotMatch(text, /确认|精确|相对支持|领先|落后/);
});