fix(rectification): exhaustion exit, explain layer, range reading, unknown-time scan (BUG-565–568)
Independent Staging Quality Gate / validate (push) Successful in 9m20s
Independent Staging Quality Gate / publish (push) Successful in 6m51s

Keep askable cards after exhaustion, explain each probe, read the adopted credible range in reports and chat, and compare declared periods before the minute grid when the clock is unknown.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Jesse_Chen
2026-09-07 09:10:37 +08:00
co-authored by Cursor
parent 43a26a3ccb
commit 814c924e4a
91 changed files with 5362 additions and 224 deletions
@@ -0,0 +1,45 @@
# PROGRESS · 生时校正穷尽出口修复(2026-09-06)
工作树:`.worktrees/rectification-convergence-exit-fix-20260906`
分支:`codex/rectification-convergence-exit-fix-20260906`(跟踪 `origin/staging`)
基线:`origin/staging` @ `b938c76a`;工作树文档头 `43a26a3c`
未改 `SKILL.md`(仍 10.0.14)、未改引擎、未改任何门槛数值。未重新引入 `USER_COLLECT_QUESTION.other` 兜底。未把 `.venv` 加入提交。
本单状态:**待验收**。BUG-565 / 566 / 567 已实现;BUG-560 保持 blocked 并补根因升级。
| 任务 | 状态 | BUG |
| --- | --- | --- |
| 5.1 单一写入方 | 完成 | BUG-565 |
| 5.2 穷尽态视为已交付 | 完成 | BUG-565 |
| 5.3 穷尽分支不吞题 | 完成 | BUG-566 |
| 5.4 范围小字还原 + 记录 | 完成 | BUG-567;BUG-560 根因升级;`docs/BLOCKED.md` BLK-001 |
## 实现要点
- `persistExhaustionCollect` 门槛分支不再 `persistV9DeterministicTurn`,只返回 `{ hostNarration, terminalNote: true, persisted: false }`。
- 共用 `isExhaustedGateState({ remainingCollect, methods, canAdopt, accepted })`。`inspectNonTerminalTurnExit.satisfied` 认穷尽态。
- idle 穷尽分支额外要求 `nextAction ∈ {offer_provisional_range, complete_with_range, ask_fact_collection}`。`ask_candidate_discriminator` / `ask_holdout_validation` 即使 `!followup` 也不落入门槛,落到 `persistNextInterviewAfterChoice`。
- `finalizeSuccessfulTurnExit`:`terminalNote` 且本回合尚未写门槛时写一次(幂等 `requestId`),并跳过 `ensureNonTerminalTurnExit`。agent-run opening/evidence 先写并把 `askedTurnId` 传给 finalize。`completedMessageResponse` 带上 `x-rectification-turn-id`。
- 点选:`persistApplied` 在 `EXHAUSTION_DELIVERY_ACTIONS` 时仍走 `persistNextInterviewAfterChoice`,把门槛句拼进该回合正文。`shouldContinueAfterStructuredChoice` 对 offer/complete 仍为 false(UI 不等下一问)。
- `RectificationReadonlyRange` 回到 `<p role="status">`。口述停止按钮保留。
### 既有断言改动
| 文件 | 原值 | 新值 | 原因 |
| --- | --- | --- | --- |
| `rectification-exhaustion-exit` 源扫描 | 口述停止按钮和范围小字共用 `CHOICE_STOP_LABEL` / `onStop` | 仅口述态 `.rectification-collect-stop` 使用该文案;范围行是 status 句,没有 `onStop` / `<button` | BUG-567 |
| BUG-558 三个形状用例 | 未改断言 | 仍通过;关闭天花板夹具去掉 holdout 事件,避免白名单后 idle 去问 holdout | 白名单后 A4 用例 2 才仍是门槛 |
## 测试
| 命令 | 结果 |
| --- | --- |
| 非 DB `tests/rectification-*.test.ts` + `agent-voice-copy-contract.test.ts` | **910 pass / 0 fail**(解释层叠上后 917) |
| `npx tsc --noEmit` | 0 错 |
| `npm run lint` | 0 error / 81 warning(既有 warning,未清) |
| `.venv/bin/python -m pytest tests/test_active_rectification_api.py::test_long_real_conversation_reaches_vedastro_after_local_range_is_narrow` | 失败:`winning_segment` 04:16 / 宽 1 vs 期望 05:07–05:08 / 宽 2。基线 `e2f4b55c` 同样失败。见 `docs/BLOCKED.md` BLK-001。 |
排除 `rectification-*-database.test.ts`。ERR-011:验收命令用 `.venv/bin/python -m pytest`。未改 `page.tsx`。
随本分支推 `origin/staging`。后续 explain-layer / range-reading / unknown-time 叠在同一提交。
@@ -0,0 +1,33 @@
# PROGRESS · 生时校正过程解释层(2026-09-06)
工作树:`.worktrees/rectification-convergence-exit-fix-20260906`
分支:`codex/rectification-convergence-exit-fix-20260906`(叠在 BUG-565~567 之后)
基线:`origin/staging` @ `b938c76a`;文档头 `43a26a3c`
未改 `SKILL.md`、未改 `contracts/probe-question-v1.json`、未改任何门、未改 `page.tsx`。
本单状态:**待验收**。
| 任务 | 状态 |
| --- | --- |
| B3 步骤条 | 完成 |
| B1 卡片 why_user / answer_impact | 完成 |
| B2 答后旁白 | 完成 |
| B4 记录 | 完成 |
## 实现要点
- `probe-explain.ts` 纯函数:`{period} 这段经历能把当前 n 段候选分成两组(毗湿奴多利、那罗延)`;varga_style 写「这题对照 D9/D10 的类型差异」。A/B/C 来自 `expected_outcomes` 的 supports/conflicts,口径是区间「04:31–04:39 这段」。D 固定「不计分,换一题」。
- `composeChoiceNarration` 在传入 `score_deltas` 聚合与范围前后值时拼「领先 / 落后」和「范围从 X 收到 Y / 范围没变」。unsure / 跳过仍是「这题先不计分」。
- `step-state.ts` 从 `nextAction` / 方法覆盖 / `stopReason` / 是否已采用派生四段。Case GET 投影 `step_state`;聊天在输入框上方渲染 `.rectification-step-state`。
- `case-service.ts` 再导出 `projectRectificationStepState`。Case 行视图仍是数据库列;完整快照在 `interview-state.ts` 与 GET。
## 测试
| 命令 | 结果 |
| --- | --- |
| 非 DB `tests/rectification-*.test.ts` + `agent-voice-copy-contract.test.ts` | **917 pass / 0 fail** |
| `npx tsc --noEmit` | 0 错 |
| `npm run lint` | 0 error |
`contracts/probe-question-v1.json` 未改。随本分支推 `origin/staging`。
@@ -0,0 +1,36 @@
# PROGRESS · 生时校正可信区间成为一等公民(2026-09-06)
工作树:`.worktrees/rectification-convergence-exit-fix-20260906`
分支:`codex/rectification-convergence-exit-fix-20260906`(叠在 BUG-565~567 与 explain-layer 之后)
基线:`origin/staging` @ `b938c76a`;文档头 `43a26a3c`
未改 `SKILL.md`、未改 `contracts/probe-question-v1.json`、未改采用/确认门、未改 `page.tsx`。`jyotish_api_server.py` 只加了 `/api/rectification/v5/range_reading` 薄注册。
本单状态:**待验收**。BUG-568。
| 任务 | 状态 |
| --- | --- |
| A1 采用落库 `adopted_credible_range`,报告 RPC 优先读它 | 完成 |
| A2 引擎 ≤31 逐分钟,>31 等距 31 点 | 完成 |
| A3 `verified_chart(accepted)` 聊天接同一份敏感度 | 完成 |
| A4 采用旁白稳定/随分钟变两句 + `range_reading` | 完成 |
| A5 `declared_birth_window` 粗看复用 | 完成 |
| A6 记录 | 完成 |
## 实现要点
- 迁移 `20260906020000_adopted_credible_range.sql`:加列;先 DROP 五参采用函数再 CREATE 六参 `p_credible_range jsonb`。采用写 `adopted_credible_range`,不改 `candidate_range`。`read_report_candidate_range` 仍只返回 `{start_time,end_time}`。
- `flexible_birth_time_profile` 上限从 15 提到 31,错误码改为 `candidate_count_must_be_two_to_thirty_one`。任务书按 2~31 上限写;仓库原先是 15。
- 采用旁白在 `validateAdoptNarration` 之后追加两句;引擎不可用则省略,不报错。
- `confirmed` 不附区间。`accepted` 才带 `birth_time_accuracy=provisional` + `candidate_range`。
## 测试
见 `docs/testing/rectification-range-reading-20260906.md`。
本工作树隔离验收:
- 非 DB `rectification-*.test.ts` + `agent-voice-copy-contract`(排除 database):931 pass / 0 fail
- Docker `frontend` `npm run test:db`:本单当时 **36 pass / 0 fail**(约 310s)。unknown-time 叠入 `database-rectification-block-scan.test.ts` 后,同一工作树再单独跑为 **37 pass / 0 fail**(约 214s)。
- `./node_modules/.bin/tsc --noEmit`:0
- `./node_modules/.bin/eslint --quiet src tests`:0 error
@@ -0,0 +1,39 @@
# PROGRESS · 完全不知道出生时间时先比时段(2026-09-06)
工作树:`.worktrees/rectification-convergence-exit-fix-20260906`
分支:`codex/rectification-convergence-exit-fix-20260906`(叠在 BUG-565~567、explain-layer、range-reading 之后)
基线:`origin/staging` @ `b938c76a`;文档头 `43a26a3c`
未改 `SKILL.md`(`stage` 不是 Case status,不 bump 10.0.15)、未改 `contracts/probe-question-v1.json`、未改采用/确认门、未改 `page.tsx`。`jyotish_api_server.py` 只加了 `/api/rectification/v5/block_scan` 薄注册。
本单状态:**待验收**。
| 任务 | 状态 |
| --- | --- |
| C1 引擎 `minute_step` + `block_scan` | 完成 |
| C2 Case `stage` / `block_scan` 列与 RPC;unknown 开全日窗 | 完成 |
| C3 采集门槛后出时段卡;选 A/B/C 进分钟;D 再采集;intake / 开场线索 | 完成 |
| C4 记录 | 完成 |
## 实现要点
- 引擎请求可选 `minute_step` 1–15,默认 1。`minute_step=1` 输出与改前一致。`block_scan` 以 10 分钟步长扫 24 小时,只聚合五段相对支持,不写探针、不出分钟卡。
- Case 打开:`birth_time_source=unknown` 且无填报钟点 → `candidate_range=00:00–23:59`、`stage=block_scan`。有填报钟点仍走分钟。
- 时段比较门槛用带年月经历 3 件、领域 2 个,不用训练/holdout 门(holdout 预留会把 3 件事收成 2 件训练,永远不出卡)。
- 四选卡 A/B/C 是支持最高的三段;选中后 `advance_agentic_rectification_case_from_block_scan` 把窗口改成该时段并切 `stage=minute`。D / 说不好只写 `declined_at_fingerprint`,不采用、不停止会话。
- `block_scan` 阶段 `can_adopt` / `selection_allowed` 恒 false;latest_result 不投影分钟候选。家人 `birth_time_clue` 只进开场旁白,改不了搜索窗口。
- intake「完全不清楚」改为可以直接开始校正,先从记得的经历比出大致时段;删劝退句。
## 测试
见 `docs/testing/rectification-unknown-time-20260906.md`。
本工作树隔离验收:
- 非 DB `rectification-*.test.ts` + `agent-voice-copy-contract`(排除 database):**931 pass / 0 fail**
- `.venv/bin/python -m pytest tests/test_rectification_v5_services.py -q`:**31 pass / 0 fail**
- `./node_modules/.bin/tsc --noEmit`:0
- `./node_modules/.bin/eslint --quiet src tests`:0 error
- Docker `frontend` `npm run test:db`(单独跑,约 214s):**37 pass / 0 fail**。中间曾因 `||` 拼布尔得到 `true:f:f:true` 失败 1 条;改成 `concat_ws` 后全量重跑通过。
随本分支推 `origin/staging`。
+4 -4
View File
@@ -62,10 +62,10 @@
| `TASK-session-title-guard-fix-20260906.md` | `PROGRESS-session-title-guard-fix-20260906.md` | BUG-553 修复单:`consult/route.ts` 标题守卫比较的是 RPC 前快照,`append_consultation_question` 已改写标题,守卫恒 0 行,模型标题只靠客户端 PATCH 落库 | 待验收 | `codex/session-title-guard-fix-20260906`(BUG-557) |
| `TASK-rectification-collect-vs-offer-consistency-20260905.md` | `PROGRESS-rectification-collect-vs-offer-consistency-20260905.md` | 带年份采集没问完就出采用卡 + 报告,同一轮又被搬家采集题把卡挤掉:决策层判 `adopt_representative` 而计划层仍有 dated 采集(BUG-546 只修了一半);改为剩余采集未完保持 `collect_evidence`,出牌轮才出卡写报告 | 已验收通过 `ca4e2408`(2026-09-05;staging 部署仍停在 `afd14948`,`deploy-staging` 自 `c295b853` 起连续失败,先解决 `bab07187` 的待迁移) | `codex/rectification-collect-vs-offer-consistency-20260905`(BUG-550) |
| `TASK-rectification-convergence-exit-20260906.md` | `PROGRESS-rectification-convergence-exit-20260906.md` | 采集问完落到「也可以再说一件事」无出口(非收敛出牌/非终止修复兜底 `other`)、同年月多领域拆题与同域同年重复问、`relative_support` 正比例归一把引擎证据压平成 7–9 使 lead 8 不可达;改为穷尽即交付 + 口述态停止按钮、同年月合并为「发生了什么」四选卡 + 同域同年去重 + 性格题须锚点、prior 按 (分−窗口最低分) 归一并过 20 例公开 holdout 校准门 | 已验收(部分完成:B2 合并卡未做、C 校准未过门未启用;1 P1 + 2 P2 见修复单) | `150d7ef1` / `3a9ae736` / `62afa521`(BUG-558~560,已合入 staging)|
| `TASK-rectification-convergence-exit-fix-20260906.md` | `PROGRESS-rectification-convergence-exit-fix-20260906.md` | BUG-558 修复单:门槛翻译句每回合写 2~3 条相同助手消息(`persistExhaustionCollect` 自己写 turn + 非终止修复再写一次)、穷尽分支排在问题持久化前吞掉可渲染区分卡/holdout 题、范围小字变按钮但文案仍是状态句;并把校准结论(引擎原始分分钟级区分力≈随机)写进 BUG-560 与 BLOCKED | 待领取 | `codex/rectification-convergence-exit-fix-20260906`(BUG-565~567) |
| `TASK-rectification-explain-layer-20260906.md` | `PROGRESS-rectification-explain-layer-20260906.md` | 过程解释层(对照上游 yinduzhanxing 旧工作台唯一领先的业务层):每张卡服务端生成「为什么问这题」与 A/B/C/D「答了会怎样」、答后旁白改成「哪段升降 + 范围从 X 收到 Y」、每轮步骤条「第 N 步 / 为什么 / 下一步」;不做双视图 | 待领取(串行:在 exit-fix 之后、range-reading 之前) | `codex/rectification-explain-layer-20260906` |
| `TASK-rectification-range-reading-20260906.md` | `PROGRESS-rectification-range-reading-20260906.md` | 可信区间成为一等公民:采用时落库 `adopted_credible_range`,报告 `read_report_candidate_range` 与聊天 `verified_chart(accepted)` 都改读它并接同一份 `birth_time_sensitivity`(现在报告读的是开工窗口,BUG-568);引擎 >15 分钟只取 3 样本改为 ≤31 逐分钟;采用旁白加「稳定 / 随分钟变」两句;`declared_birth_window` 复用 | 待领取(串行:在 explain-layer 之后) | `codex/rectification-range-reading-20260906`(BUG-568) |
| `TASK-rectification-unknown-time-20260906.md` | `PROGRESS-rectification-unknown-time-20260906.md` | 完全不知道出生时间的两段式路线:`stage=block_scan` 以 10 分钟步长扫 24 小时只做事件计分、出五时段四选卡(不写账本不采用),选定后进现有分钟流程;引擎加 `minute_step`;开场读 `birth_time_clue`;删 intake 劝退文案 | 待领取(串行:在 range-reading 之后) | `codex/rectification-unknown-time-20260906` |
| `TASK-rectification-convergence-exit-fix-20260906.md` | `PROGRESS-rectification-convergence-exit-fix-20260906.md` | BUG-558 修复单:门槛翻译句每回合写 2~3 条相同助手消息(`persistExhaustionCollect` 自己写 turn + 非终止修复再写一次)、穷尽分支排在问题持久化前吞掉可渲染区分卡/holdout 题、范围小字变按钮但文案仍是状态句;并把校准结论(引擎原始分分钟级区分力≈随机)写进 BUG-560 与 BLOCKED | 待验收 | `codex/rectification-convergence-exit-fix-20260906`(BUG-565~567) |
| `TASK-rectification-explain-layer-20260906.md` | `PROGRESS-rectification-explain-layer-20260906.md` | 过程解释层(对照上游 yinduzhanxing 旧工作台唯一领先的业务层):每张卡服务端生成「为什么问这题」与 A/B/C/D「答了会怎样」、答后旁白改成「哪段升降 + 范围从 X 收到 Y」、每轮步骤条「第 N 步 / 为什么 / 下一步」;不做双视图 | 待验收 | 叠在 `codex/rectification-convergence-exit-fix-20260906` |
| `TASK-rectification-range-reading-20260906.md` | `PROGRESS-rectification-range-reading-20260906.md` | 可信区间成为一等公民:采用时落库 `adopted_credible_range`,报告 `read_report_candidate_range` 与聊天 `verified_chart(accepted)` 都改读它并接同一份 `birth_time_sensitivity`(现在报告读的是开工窗口,BUG-568);引擎 >15 分钟只取 3 样本改为 ≤31 逐分钟;采用旁白加「稳定 / 随分钟变」两句;`declared_birth_window` 复用 | 待验收 | 叠在 `codex/rectification-convergence-exit-fix-20260906`(BUG-568) |
| `TASK-rectification-unknown-time-20260906.md` | `PROGRESS-rectification-unknown-time-20260906.md` | 完全不知道出生时间的两段式路线:`stage=block_scan` 以 10 分钟步长扫 24 小时只做事件计分、出五时段四选卡(不写账本不采用),选定后进现有分钟流程;引擎加 `minute_step`;开场读 `birth_time_clue`;删 intake 劝退文案 | 待验收 | 叠在 `codex/rectification-convergence-exit-fix-20260906` |
| `TASK-api-not-configured-mislabel-20260904.md` | `PROGRESS-api-not-configured-mislabel-20260904.md` | 16 处路由把数据库瞬断(部署切换窗口)兜底翻译成 503「服务尚未配置」;改为仅配置错误用该文案,其余 `service_unavailable`,收敛为共享 helper | 已验收 | `5483649b`(BUG-542);2 条子进程测试留 CI Node 22 复核 |
| `TASK-rectification-ux-20260902.md` | `PROGRESS-rectification-ux-20260903.md` | 会话面空白假死与交互摩擦 | 已验收 | `d159f08e`(09-03 在新基线重做后合入,BUG-505~509) |