Compare commits

..

4 Commits

Author SHA1 Message Date
Jesse_Chen 6c7a8b0262 docs(tasks): other-collect fallback dead-end fix brief (BUG-586)
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0193vBv6w5MV2cifdTUu9H5P
2026-09-07 16:56:47 +00:00
Jesse_Chen 3973b6c3dc docs(tasks): duplicate narration fix brief (BUG-584/585)
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0193vBv6w5MV2cifdTUu9H5P
2026-09-07 15:31:59 +00:00
Jesse_Chen a00b069dcc fix(rectification): unblock range-card tsc and restore post-adopt windows (BUG-583)
Independent Staging Quality Gate / validate (push) Successful in 13m23s
Independent Staging Quality Gate / publish (push) Successful in 30m54s
Null-check the candidate result before reading resultId so next build can ship. Adopt reconnects prospective windows on the first verify turn, and dismissing the range card now shows the collect hint.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-07 20:22:24 +08:00
Jesse_Chen f3e7270237 docs(tasks): accept ab57d03f with tsc blocker; range delivery fix brief (BUG-583)
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0193vBv6w5MV2cifdTUu9H5P
2026-09-07 12:04:39 +00:00
18 changed files with 438 additions and 17 deletions
+4
View File
@@ -1,5 +1,9 @@
# 印度占星 Skill 更新日志
## 2026-09-07 — 区间卡能编译,采用后才说预测窗口,再补经历有下一句提示
生时校正交付卡不再把编译挡在门外。点采用后,第一轮核对旁白末尾会接上实际的下次事业变动窗口(有才写)。点「再补一件经历」后卡片收起,输入框上方改成收集经历的提示。Skill 版本仍是 10.0.15。
## 2026-09-07 — 生时校正结尾改成一张区间卡,不再让你在分钟里挑
问完经历后,交付轮只出一张卡:这段范围、排盘用的代表分钟、这段里稳定和会随分钟变的主题。如果几段之间还有没问过的关系盘或事业盘差别,会并排两三列对照,文案只来自类型表。动作只有「按这个范围用」和「再补一件经历」。不再显示相对支持度,也不再并排三行「候选 A 预测下次事业变动」。Skill 版本 10.0.15。
+16
View File
@@ -9030,5 +9030,21 @@
- 复发自:BUG-558
- 修复版本:待发布(`codex/rectification-stop-rescore-fix-20260907`
## BUG-583 | 区间交付卡判空顺序使 tsc 失败,staging 无法部署
- 状态:resolved
- 首次发现:2026-09-07
- 最近更新:2026-09-07
- 影响面:`rectification-agentic-chat.tsx` 区间卡渲染、采用后首轮旁白、`RectificationRangeDelivery`「再补一件经历」
- 用户现象:staging 门禁与 `next build` 因 TypeScript 失败,BUG-581~582 的修复无法部署。采用后看不到承诺过的预测窗口。点「再补一件经历」后卡片消失、输入框上方没有下一句。
- 触发条件:`ab57d03f` 合入后跑 `tsc --noEmit`;或交付轮点「再补一件经历」;或采用后进入核对。
- 根因:渲染条件先读 `candidateResult.resultId` 再写 `candidateResult &&`,收窄发生在属性读取之后。`prospectiveWindowsNarration` 从交付旁白撤掉后没有接到采用后首轮。`onContinue` 只改本地 dismiss,不给采集提示。步骤条类名 `rectification-step-state` 已由 BUG-575 禁止,不能靠加回步骤条过关。
- 修复:判空提到 `dismissedRangeResultId` 之前。`persistNextInterviewIfIdle` 在已采用时把 `prospective_probes` 接到首轮旁白末尾;`deferredCareerWindow` 仍留在交付旁白。收起卡片后用 `RangeDeliveryContinueHint` 显示「第 1 步·收集经历」加 `continueCollectFallback`,不用被禁的步骤条类名。
- 验证:`frontend/tests/rectification-range-delivery-20260907.test.ts``tsc --noEmit` 原文见 `PROGRESS-rectification-range-delivery-fix-20260907.md`
- 防复发:区间卡渲染源码必须先出现 `candidateResult &&` 再读 `resultId`。采用后首轮 idle persist 必须调用 `appendPostAdoptProspectiveWindows`。聊天源码仍不得出现 `rectification-step-state`
- 相关记录:BUG-575、BUG-581、BUG-582
- 复发自:无(区间卡 `ab57d03f` 引入;任务书 4.2 验收未过)
- 修复版本:待发布(`codex/rectification-range-delivery-fix-20260907`
@@ -0,0 +1,50 @@
# PROGRESS · 区间交付卡修复单(2026-09-07
工作树:`.worktrees/rectification-range-delivery-fix-20260907`
分支:`codex/rectification-range-delivery-fix-20260907`
任务书:`docs/tasks/TASK-rectification-range-delivery-fix-20260907.md`
基线:`origin/staging` @ `f3e72702`(任务书写 `ab57d03f`;其上有验收文档提交)
本单状态:**待验收**。未 commit、未 push。
未改:采用 RPC 校验、确认门、`MIN_SEPARATION_LEAD``_relative_support``page.tsx`、Skill 版本(仍 10.0.15)、聊天源码不出现 `rectification-step-state`BUG-575)。
## 做了什么
- **3.1** `rectification-agentic-chat.tsx` 渲染条件改为 `showSelectionCards && candidateResult && dismissedRangeResultId !== candidateResult.resultId && …`
- **3.2** 选「接上」:`persistNextInterviewIfIdle``acceptedTime` 时把 `prospectiveWindowsNarration(receipt.prospective_probes)` 接到首轮旁白末尾。交付旁白仍留 `deferredCareerWindow`。跳过最后一道核对题的 `postAdoptVerifyDone` 不走 idle 包装,仍是原句。
- **3.3** 「再补一件经历」收起卡片后,输入框上方 `RangeDeliveryContinueHint` 显示 `STEP_STATE_COPY.collect.headline` + `continueCollectFallback`。不用被禁的 `.rectification-step-state`
- **3.4** BUG-583、CHANGELOG、本文件、场景第 5 条、DESIGN / VOICE。
## 三栏(被触碰断言)
| 用例 | 原值 | 新值 | 理由 |
| --- | --- | --- | --- |
| 区间卡渲染条件 | 先读 `candidateResult.resultId``candidateResult &&` | 先 `candidateResult &&` | TS18047 |
| 采用后首轮旁白 | 无 `prospectiveWindowsNarration` 调用点 | idle persist 已采用时追加 | 决策 2 接上 |
| 「再补一件经历」 | 只 `setDismissedRangeResultId` | 另显示收集经历提示 | 决策 3;不恢复步骤条类名 |
## 测试
开工前 `frontend` `./node_modules/.bin/tsc --noEmit; echo "tsc exit $?"` 原文:
```
src/components/rectification-agentic-chat.tsx(1655,67): error TS18047: 'candidateResult' is possibly 'null'.
tsc exit 2
```
修复后同一命令原文(stdout 为空,只有 echo):
```
tsc exit 0
```
- 任务书指定 TS 套件(`rectification-*.test.ts` + `consultation-*.test.ts` + `agent-voice-copy-contract.test.ts` + `skill-registry.test.ts`,排除 database):**1230 passed / 0 failed**`ab57d03f` 同切片 1227;本单 +3
- 本单改动文件 eslint**0 error**chat 上 2 条既有 hooks warning
- Docker `database-rectification-*.test.ts`:本机未跑,记 blocked
- 浏览器点选「再补一件经历」:无登录态 / Chrome 驱动,记环境缺口,见 `docs/testing/rectification-scenarios-20260907.md` §5
## 偏离
1. 步骤条文案用 `RangeDeliveryContinueHint``.rectification-continue-hint`),没有恢复 `.rectification-step-state`。BUG-575 防复发与 `rectification-step-state-20260906.test.ts` 禁止该类名。
2. 预测窗口挂在 `persistNextInterviewIfIdle`(采用 RPC 之后的首轮),不挂 `applyRectificationChoice` 的跳过核对收尾,以免改掉 `postAdoptVerifyDone` 全等断言。
+5 -2
View File
@@ -69,8 +69,11 @@
| `TASK-rectification-capability-fix-20260907.md` | `PROGRESS-rectification-capability-fix-20260907.md` | 能力补齐修复单:答后旁白把 `range_start/range_end`(搜索窗口)当范围比较,每题都说「范围没变」;`block_scan` 五段支持度按段内原始分求和,长时段先天占优(24/24/36/30/30 个候选);`TRACK_LABEL` 音译与产品 Vimshottari/Narayana 口径不一;BLK-001 写到 `docs/BLOCKED.md` 应回根目录 | 已验收通过(P2block_scan 15 s 壁钟断言在门禁里可能间歇红) | `517df002`BUG-569570);staging 未部署,需先 Migrate Staging Database |
| `TASK-rectification-declared-uncertainty-20260907.md` | `PROGRESS-rectification-declared-uncertainty-20260907.md` | 出生时间「有多确定」只在 intake 问一次:三档(医院记录 / 家人大概 ±15·30·60·120 / 时段或未知),校正窗口读档案(现在有钟点一律 ±15,声明值被忽略,BUG-571);吻合率 <60% 且代表分钟贴窗口边缘时出服务端一键放宽卡,放宽后重算并写回档案(BUG-572);窗口 >120 分钟先切三子段迭代到 ≤120 再进分钟(BUG-573);日级事件问一次可靠度;四个脚本化手测场景进 docs/testing | 已验收通过(2 P3 建议:可靠度正则去掉「记得」、档案写回改走 account-profile-patch | `8e31680b`BUG-571573);迁移 `20260907020000` 待应用,部署前先 Migrate Staging Database |
| `TASK-rectification-stale-compare-fix-20260907.md` | `PROGRESS-rectification-stale-compare-fix-20260907.md` | **P0**BUG-559 把过长 `candidate_split_hash` 塞进引擎比较键,答完性格/边界卡后比较静默失败;快照过期压过用户停止;holdout 用过期领域被改写成「再说一件事」;采集轮正文双写 | 已验收通过(P2 正文双写根因未拆、P3 重算节流 Map 只增不减) | `4e0db55f`(落地编号 BUG-577580);staging 未部署 |
| `TASK-rectification-stop-rescore-fix-20260907.md` | `PROGRESS-rectification-stop-rescore-fix-20260907.md` | **P0**:BUG-579 的停止路径用「裸重算」(`rescoreMinuteAfterWindowChange` 不重建 `inference_state`、不重放已答探针、不写 transition),停止后范围变宽、卡片变成引擎裸支持度、采用 RPC 报 `candidate_state_inconsistent`;无框区分题经 `spokenCollectFallbackFollowup` 变成 domain=other 的采集题,再次冒出开场句 `GENERIC_COLLECT_QUESTION`;holdout 采集排在带年月采集之前 | 验收 | `codex/rectification-stop-rescore-fix-20260907`BUG-581582 |
| `TASK-rectification-range-delivery-card-20260907.md` | `PROGRESS-rectification-range-delivery-card-20260907.md` | 结尾重设计:四张分钟卡 + 相对支持度换成一张区间交付卡(范围、代表分钟、稳定/敏感主题、边界句)+ 只列未问 D9/D10/月宿分歧的 2~3 列对比面板(文案只取类型表),动作只有「按这个范围用 / 再补一件经历」;删交付旁白里三行预测窗口;Skill §9 口径改、bump 10.0.15 | 待验收 | `codex/rectification-range-delivery-card-20260907` |
| `TASK-rectification-stop-rescore-fix-20260907.md` | `PROGRESS-rectification-stop-rescore-fix-20260907.md` | **P0**:BUG-579 的停止路径用「裸重算」(`rescoreMinuteAfterWindowChange` 不重建 `inference_state`、不重放已答探针、不写 transition),停止后范围变宽、卡片变成引擎裸支持度、采用 RPC 报 `candidate_state_inconsistent`;无框区分题经 `spokenCollectFallbackFollowup` 变成 domain=other 的采集题,再次冒出开场句 `GENERIC_COLLECT_QUESTION`;holdout 采集排在带年月采集之前 | 验收通过(逻辑);但同提交含区间卡 tsc 错,整体不可部署 | `ab57d03f`BUG-581582 |
| `TASK-rectification-range-delivery-card-20260907.md` | `PROGRESS-rectification-range-delivery-card-20260907.md` | 结尾重设计:四张分钟卡 + 相对支持度换成一张区间交付卡(范围、代表分钟、稳定/敏感主题、边界句)+ 只列未问 D9/D10/月宿分歧的 2~3 列对比面板(文案只取类型表),动作只有「按这个范围用 / 再补一件经历」;删交付旁白里三行预测窗口;Skill §9 口径改、bump 10.0.15 | **未通过**:tsc 1 错(判空顺序);P2 预测窗口句兑现不了;P3 再补经历无提示 | `ab57d03f`;修复单 `TASK-rectification-range-delivery-fix-20260907.md` |
| `TASK-rectification-range-delivery-fix-20260907.md` | `PROGRESS-rectification-range-delivery-fix-20260907.md` | 区间卡修复单:`candidateResult` 判空提到最前(tsc 阻断);预测窗口接到采用后首轮或删句;「再补一件经历」隐藏卡后给采集提示;进度记录须贴 tsc 原文 | 待验收 | `codex/rectification-range-delivery-fix-20260907`BUG-583 |
| `TASK-rectification-duplicate-narration-20260907.md` | `PROGRESS-rectification-duplicate-narration-20260907.md` | 每轮采集旁白说两遍 + 题干在正文与问题块各一次:`step-answer` 的 set-focus 例外(BUG-533)放行了 set-focus 前后两个 step 的正文并拼接;`detachCollectSpokenAssistantText` 只剥逐字后缀,模型写进正文的问句没有代码守卫(BUG-488 防线只在提示词) | 待执行 | `codex/rectification-duplicate-narration-20260907`BUG-584585 |
| `TASK-rectification-other-collect-fallback-20260908.md` | `PROGRESS-rectification-other-collect-fallback-20260908.md` | 七领域问完落到「也可以再说一件」、职业题从未出现、不自动出卡:`USER_COLLECT_QUESTION.other` 仍有三条活路(职业焦点 `target_domain` 压成 other 后被 active-focus 承接 followup 重建;set-focus 两次无效兜底查表;无领域采集归 other),`collect:other:*` 焦点挂着让 `persistNextInterviewIfIdle` 的出卡路径永远不跑 | 待执行(触发链 investigating,需事故 Case receipt | `codex/rectification-other-collect-fallback-20260908`BUG-586 |
| `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-505509 |
@@ -0,0 +1,76 @@
# TASK · 生时校正修复单:同一轮旁白说两遍、题干在正文与问题块各出现一次(2026-09-07)
- 基线:`origin/staging` @ `f3e72702`(代码头 `ab57d03f`,含 BUG-583 未修的 tsc 错;staging 已部署 `84a29754`,事故在多轮实测中稳定复现)
- 分支:`codex/rectification-duplicate-narration-20260907`,基于 `origin/staging`。与 `codex/rectification-range-delivery-fix-20260907`BUG-583,只改 `rectification-agentic-chat.tsx`)无文件重叠,可并行;谁后合谁 rebase。
- 执行方:coding agent;验收:Claude
- 涉及文件:`frontend/src/lib/rectification-agentic/v9/step-answer.ts``v9/agent-run.ts``v9/collect-prompt.ts``v9/turn-question.ts``frontend/tests/rectification-step-answer.test.ts``rectification-collect-prompt.test.ts`
- BUG 编号起点:**BUG-584**BUG-583 已由区间卡修复单占用;开工时 `grep -o "^## BUG-5[0-9][0-9]" docs/BUG_HISTORY.md | tail -1` 复核)
- 优先级:**P1**(每一轮采集都复现;用户读到两段近似旁白 + 两遍题干,以为系统出错)
## 1. 事故实证(staging,多次实测;只写结构)
一条助手消息自上而下是:
1. 旁白第一段:承接用户刚说的事,说范围/对照情况,**末尾把下一题问了一遍**(自然语言,含年份与领域,以「?」结尾)。
2. 旁白第二段:换个说法把第一段又说一遍(有时更短,有时又带一遍题)。
3. 「本轮对照了 …」方法句(客户端 `vargaSentenceFromMethods` 渲染)。
4. 问题块:服务端 `ConversationFocus.spokenPrompt` 的题干,与第 1 段末尾那句同义或逐字相同。
## 2. 根因(两条,叠加)
### 2.1 BUG-584P1):set-focus 前后两个 step 的正文都被放行并拼接
`step-answer.ts` 的规则:文本先于 public 工具调用的算「规划稿」,工具调用时 `retractLive` 撤回。但 `KEEP_LIVE_SPOKEN_TOOLS = {rectification-set-focus}` 是例外(`a793edb1`,BUG-533:为了不把开场问候撤掉)——set-focus 之前已直播的中文保留,set-focus `tool-result``resetBuffers` 不撤回;随后模型在**下一个 step** 看到 set-focus 结果再写一段正文,`step-finish(reason=stop)``shouldPublishStepText` 正常发布。`agent-run.ts``publishSpokenStep``spokenRaw += spoken`,两段正文无分隔、无去重直接拼成 `answerText`,落库为 `assistant_message`
固定这个行为的正是测试 `rectification-step-answer.test.ts::keeps a live opening greeting when the next public tool is set-focus`:它断言 set-focus 之后的第二段也 `live` 发布。开场轮两段短句无伤;证据轮模型在 set-focus 前先总结一遍、拿到结果后又总结一遍,就是用户看到的"说了两遍"。receipt 里每轮两个 `intent.classified` 是同一件事的旁证。
附带损失:set-focus 前尚未触发直播(没遇到句末标点)的文本、以及已直播部分之后 `publishedUpTo` 以外的尾巴,在 `tool-result``resetBuffers` 里被静默丢掉——这是偶发"旁白半句"的来路。
### 2.2 BUG-585(P1):题干去重只认逐字后缀,模型写进正文的问句剪不掉
服务端只在 `turn-question.ts::attachQuestionsToTurns``detachCollectSpokenAssistantText(turn.text, question.prompt)` 剥题干,而它只剥两种形态:正文整体 === 题干,或正文以 `\n\n${题干}` 结尾。模型自己在正文里问出来的句子(同义改写、或逐字但嵌在段落中间)一概保留;问题块再从 focus 渲染一遍 → 同一题两遍。`composeCollectSpokenAssistantText` 里那套「按题干前 12 字剪句」只用于拒答确认句(`answer-choice.ts` L1059),agent 路径根本没走。
不让模型提问的约束只有提示词:`SKILL.md` L59「正文……不得自行提出、复述、改写或预告问题」、set-focus 描述「你只写 spokenPrompt」、opening brief「不要提问」。BUG-488 的防复发写的是「不得同时让模型提问又拼接同一句服务端题干」,靠"模型仍不自己提问"兜住;`d9404976` 把题干改成消息内问题块后,这条防线没有任何代码守卫,于是复发。
另外 `agent-run.ts` L284 `const collectSpokenEmitted = false` 是常量,只被原样透传进结果,没有任何读者——它就是当年打算做"题干已出、正文别再出"守卫的残骸。
## 3. 决策记录
1. **有服务端问题的轮次,正文不得含问句;含则服务端剪掉。** 这是把 SKILL.md L59 从提示词升级为代码守卫:`answer.composed` 之前,若本轮 set-focus 成功(focus.askedTurnId = 本轮),对 `answerText``stripQuestionSentences(body, stem)`:按 `。!?\n` 切句,删掉 (a) 与题干相同、(b) 以题干前 12 字开头、(c) 以「?」/「?」结尾 的句子。剪空则用一句固定承接(`RECTIFICATION_USER_COPY` 新增 `collectHandoff`,对照 `VOICE.md`,不得说"请回答下面的问题"之类机器话)。剪后文本以 `answer.delta replace:true` 再发一次,保证直播与落库一致。选择题轮(`choice_frame`)同样适用——选项由卡片承载,正文不问。
2. **set-focus 之后不再接受新一段正文。** `step-answer.ts`set-focus `tool-result` 后置 `state.stemAttached = true`(随 turn 存活,不随 `resetBuffers` 清);此后 step 的 `text-delta` 只有在本轮此前**没有**发布过任何正文时才放行(保住 BUG-533 的开场问候路径:问候在 set-focus 前,或模型只在 set-focus 后说一句),否则 `discard`。两段都有时保留**前一段**——它是模型对刚才证据的直接回应;后一段只是对工具结果的复述。
3. **set-focus 前的未直播文本不得静默丢弃。** `tool-call(set-focus)` 时把 `state.text` 未发布部分作为 `publish` 效果发出(它是给用户的话,不是规划稿),再 `resetBuffers`。若该文本没有 CJK(英文规划稿),维持原 discard。
4. 删除 `collectSpokenEmitted` 常量与其类型字段。
5. 提示词不动、Skill 不 bump(不是口径变化,是守卫补齐)。不动采用门、确认门、`MIN_SEPARATION_LEAD``_relative_support``minute_step=1` 指纹;不动 `frontend/src/app/page.tsx`
## 4. 任务分解
### 4.1 BUG-584:单段正文
- `step-answer.ts` 按决策 2、3 改;`StepAnswerState``stemAttached``publishedAny`(后者由 `agent-run.ts` 在首次 `publishSpokenStep` 成功后写回,或由 state 自记)。
- 改既有测试 `keeps a live opening greeting when the next public tool is set-focus`:原值——set-focus 后第二段 `live` 发布;新值——第二段 `discard`;原因——决策 2。新增:(a) set-focus 前无正文、后有一段 → 后段 `live/publish`BUG-533 路径仍通);(b) set-focus 前有未闭合半句「范围已经收到 05:00 到 05:10」(无句末标点)→ `tool-call(set-focus)` 时以 `publish` 发出,不丢;(c) 英文规划稿 + set-focus → 仍 discard。
- `agent-run` 级:`rectification-v9-agent-run*.test.ts`(现有 mock stream 夹具)构造「文本 → set-focus → 文本 → finish」序列,断言 `answerText` 只含第一段,`answerDeltas` 无第二段,且没有 `replace:true` 的清空事件。
### 4.2 BUG-585:正文剪问句
- `collect-prompt.ts` 新增 `stripQuestionSentences(body, stem)`(复用 `composeCollectSpokenAssistantText` 的切句与前缀规则 + 问号规则),`composeCollectSpokenAssistantText` 内部改调它,行为对拒答句路径不变(既有 `rectification-collect-prompt.test.ts` 全部保持)。
- `agent-run.ts`:流结束、`answer.composed` 之前,读本轮 focus`loadV9CaseDossier` 已在 `discriminatorInvariant` 里做过,复用那次读取,不加第二次 RPC);`askedTurnId === turnId` 且 schema 里有 spokenPrompt → 剪;剪空 → `collectHandoff`;变化时 `emitVisibleSpoken(stripped)`(它自会发 `replace:true`)。
- `turn-question.ts::attachQuestionsToTurns` 同样改调 `stripQuestionSentences`,让历史消息回看也干净(老数据不迁移)。
- 验收:单元——(a) 正文「…范围收在 05:00–05:10。你大概哪一年搬过家?有年份就行。」+ 题干「你大概是哪一年搬的家?」→ 剪成「…范围收在 05:00–05:10。」;(b) 正文中段逐字含题干 → 剪掉那句,前后句保留;(c) 正文只有问句 → `collectHandoff`;(d) 无本轮 focus → 原样。合同——`agent-voice-copy-contract` 增加「有 focus 的已结算助手正文不得以『?』结尾的句子出现」(对 `docs/testing` 之外的夹具跑)。
### 4.3 记录
- `docs/BUG_HISTORY.md` BUG-584(关联 BUG-533:其修复放行了第二段)、BUG-585(复发自 BUG-488:防线只在提示词;关联 BUG-461、BUG-471、`d9404976`);`CHANGELOG.md` 一条;`PROGRESS-rectification-duplicate-narration-20260907.md``docs/testing/rectification-scenarios-20260907.md` 加一条「每轮助手气泡只有一段旁白,题干只在问题块出现一次」。
## 5. 让步顺序
4.2 是用户直接看到的"两遍题",先做;4.1 次之;决策 3(半句不丢)可后置为 P3 但要在进度记录里写明;4.3 不可省。
## 6. 开工前置命令
```bash
git fetch origin --prune
git worktree add -b codex/rectification-duplicate-narration-20260907 .worktrees/rectification-duplicate-narration-20260907 origin/staging
cd .worktrees/rectification-duplicate-narration-20260907
ln -s /workspace/Jyotisha/frontend/node_modules frontend/node_modules
cd frontend && ./node_modules/.bin/tsc --noEmit; npm run lint 2>&1 | tail -1
ls tests/rectification-*.test.ts tests/agent-voice-copy-contract.test.ts | grep -v database | xargs npx tsx --test 2>&1 | grep -E "^# (tests|pass|fail)"
```
开工时 tsc 若仍报 `rectification-agentic-chat.tsx` L1655BUG-583),先确认 `codex/rectification-range-delivery-fix-20260907` 是否已合入;未合入则本单不修它,进度记录里写清基线含该错。
@@ -0,0 +1,83 @@
# TASK · 生时校正修复单:采集问完落到「也可以再说一件你记得大概时间的事」,职业题从未出现、没有自动出卡(2026-09-08)
- 基线:`origin/staging` @ `3973b6c3`(代码头 `a00b069d`staging 已部署 `a00b069d`;事故转录来自 2026-09-07 晚间实测,跑在 `84a29754` 还是 `a00b069d` 上待用户确认,下文两版都对过)
- 分支:`codex/rectification-other-collect-fallback-20260908`,基于 `origin/staging`
- 执行方:coding agent;验收:Claude
- 涉及文件:`frontend/src/lib/rectification-agentic/v9/server-focus.ts``persistableFocusDomain``stableFollowupQuestionId`)、`v9/method-followup.ts`active-focus 承接 followup、`collectQuestionDomain``otherCollectFollowup` 死代码)、`frontend/src/mastra/rectification-v9-tools.ts`set-focus 的 `invalid_spoken_prompt` 兜底 L10851100)、`frontend/src/lib/rectification-agentic/user-copy.ts``USER_COLLECT_QUESTION.other` / `_RETRY.other`
- BUG 编号起点:**BUG-586**584/585 已由重复旁白修复单占用;开工时 `grep -o "^## BUG-5[0-9][0-9]" docs/BUG_HISTORY.md | tail -1` 复核)
- 优先级:**P1**(采集走完后无出口,第四次实测复现;BUG-558 同现象、不同来路)
## 1. 事故实证(只写结构)
七个带年月领域全部问过或拒过(学业、感情、事业、家人拒答、财务、迁居、健康),中间答了 5 张点选卡,范围从 30 分钟收到 8 分钟。最后一轮:
1. 助手气泡两段旁白(BUG-584);
2. 「本轮对照了 …」方法句;
3. 问题块:**「也可以再说一件你记得大概时间的事。」**——这是 `USER_COLLECT_QUESTION.other` 逐字;
4. 没有区间交付卡、没有职业题「你平时主要做什么工作?」、没有停止按钮以外的出口。receipt 显示该轮 6 步(此前每轮 3 步)。
按当前设计,这一步应当是:职业口述题 → (若有未问领域则 holdout)→ `adopt_representative` 自动出区间交付卡。职业题一次都没出现,而「other」这句取代了它。
## 2. 已由代码确认的事实
### 2.1 这句固定文案在 `a00b069d` 仍有三条活路
`spokenFollowupForUser``domain="other"` 的采集 followup:只有「`source=method_coverage``method_id=dasha_events` 且非 retry 且账本无带年月事件」才给开场句 GENERIC;其余一律查表落到 `USER_COLLECT_QUESTION.other`。BUG-558 只堵了 `exhaustionSpokenCollectFollowup` 这一条(`otherCollectFollowup` 现已无人调用,是死代码),但 `domain="other"` 还能从下面三处产生:
| # | 来路 | 位置 |
| --- | --- | --- |
| A | **职业焦点落库时领域被压成 other**DB 约束 `target_domain in (education, career, relationship, relocation, finance, health, family, other)` 没有 `occupation``persistableFocusDomain("occupation")` 返回 `"other"`(测试 `rectification-server-focus.test.ts` L716/L775 锁定)。随后 `buildMethodFollowupPlan` 有 active focus 时用 `domain: focus.targetDomain` 重建「承接 followup」(`method-followup.ts` L20162025`source="active_focus"`),职业焦点被重建成 `domain=other, intent=collect``spokenFollowupForUser` 给出「也可以再说一件」。`stableFollowupQuestionId` 对它算出 `collect:other:collect_method_evidence`,与原职业焦点 `collect:occupation:collect_method_evidence` 不同,因此不会被判 `already_open`,会 supersede 掉职业题 | `server-focus.ts` L201206、L5253`method-followup.ts` L2016 |
| B | **set-focus 工具的兜底**:模型 `spokenPrompt` 两次 `invalid_spoken_prompt` 后,`USER_COLLECT_QUESTION[nextFollowup.domain]` 直接落库;`nextFollowup.domain="other"` 时就是这句 | `rectification-v9-tools.ts` L10851100 |
| C | `spokenCollectFallbackFollowup` / `collectQuestionDomain(null | "unknown" | "active_focus")` 把无领域的采集 followup 归为 other | `method-followup.ts` L1343、L1371 |
### 2.2 尚未确认的触发链(`investigating`
最符合转录的链:该轮模型先用 set-focus 写出职业题(焦点 `target_domain=other`),同一轮再次调用 set-focus(6 步);第二次的 `next_followup` 是承接 followup`domain=other`),模型的 `spokenPrompt``domain_mismatch`(模型填 `targetDomain="career"``persistableFocusDomain` 后 ≠ `"other"`)或词表问题两次被拒,走 2.1-B 兜底,把职业题覆盖成「也可以再说一件」。**这一段需要事故 Case 的 receipt 证实**(预期能看到两条 `rectification-set-focus / intent.classified / failed / invalid_spoken_prompt` 和一条 completed,以及 `collect:occupation:*` 焦点被 `collect:other:*` supersede)。执行方拿不到 staging 数据时,把这一节保留为 `investigating`,不得编造。
无论触发链是哪条,2.1 的三条活路都是确定性缺陷,修掉后这句话在代码里不再存在。
### 2.3 为什么没有自动出卡
出卡的服务端路径是 `persistNextInterviewIfIdle → persistExhaustionCollect``exhaustionSpokenCollectFollowup` 为空且 `acceptanceAllowed && trainingGate.open && canAdopt` 时写 `adopt_representative` 旁白 + `terminalNote`,客户端据此渲染区间交付卡。它只在**没有 active focus** 时运行(L1158 提前返回)。一个 `collect:other:*` 焦点挂着,出卡永远轮不到;用户答了这句「再说一件」之后,若职业仍未覆盖,还会再来一遍。
## 3. 决策记录
1. **删掉 `USER_COLLECT_QUESTION.other` 与 `USER_COLLECT_QUESTION_RETRY.other`**(产品口径:多余入口宁可删除也不修)。`spokenFollowupForUser``domain=other` 只剩开场 GENERIC 一条路;非开场的 other 返回 `null`,上游按「无可问」处理。`otherCollectFollowup` 一并删除。
2. **职业焦点不再丢领域。** 不改表(本轮不动迁移):`target_domain` 仍写 `other`,但 `questionId` 已是 `collect:occupation:collect_method_evidence`;承接 followup 的 `domain` 改为「先从 `questionId` 解析 `collect:<domain>:` 段,再退回 `focus.targetDomain`」。新增 `parseCollectFocusQuestionId``persistableFocusDomain("occupation") === "other"` 的两条测试保留(DB 约束未变)。
3. **set-focus 兜底只允许表内真实领域。** L10851100`fallbackDomain` 必须是 `USER_COLLECT_QUESTION` 的键且 ≠ `other`(删表后自然满足),否则返回 `invalid_spoken_prompt` 不落地;且兜底题干必须与当前 `next_followup` 同领域。
4. **同一轮第二次 set-focus 必须承接、不得覆盖。** 若 active focus 的 `askedTurnId === turnId`(本轮刚写的),set-focus 返回该焦点投影并标 `idempotent: true`,不重建、不 supersede。
5. **有 `collect:other:*` 非开场焦点的存量 Case**`persistNextInterviewIfIdle` 开头把这种焦点判为 stale 并 `skipped`(复用 `isStalePreAdoptFocus` 旁边加一条 `isOrphanOtherCollectFocus``questionId``collect:other:` 开头且账本已有带年月事件),让出卡路径能跑起来。
6. 不动采用门、确认门、`MIN_SEPARATION_LEAD``_relative_support``minute_step=1` 指纹;不动 `page.tsx`Skill 不 bump。
## 4. 任务分解
### 4.1 BUG-586:三条活路 + 出卡
- 决策 15 全部落地。
- 验收(形状用例,`rectification-collect-stall.test.ts` / `rectification-occupation-coverage-exit.test.ts` 扩):
- (a) 7 个带年月领域已覆盖或拒答、职业未覆盖、active focus 为 `collect:occupation:*``target_domain=other`)→ `buildMethodFollowupPlan` 的承接 followup `domain === "occupation"``spokenFollowupForUser` 给「你平时主要做什么工作?」,`stableFollowupQuestionId` 等于原焦点 id`already_open`,不 supersede)。
- (b) 同 (a),模型 `spokenPrompt` 两次无效 → set-focus 返回 `invalid_spoken_prompt`**不**落库任何 `collect:other:*`
- (c) 同一轮 set-focus 两次(第二次 `next_followup` 为承接 followup)→ 第二次返回 `idempotent: true`,焦点 id 不变。
- (d) 职业已答、无剩余采集、`acceptanceAllowed && trainingGate.open && canAdopt``persistNextInterviewIfIdle` 返回 `terminalNote: true` 的采用旁白,`agent-run` 写 exhaustion gate turn,客户端 `canOfferCards` 为真(区间交付卡)。
- (e) 存量 `collect:other:*` 焦点 + 账本有带年月事件 → 进入 `persistNextInterviewIfIdle` 时被 `skipped`,随后走 (d)。
- (f) `agent-voice-copy-contract`:源码与 `user-copy.ts` 不再含「也可以再说一件」;`rectification-server-focus.test.ts` L517/L727、`rectification-adopt-flow-*.test.ts` 里以 `collect:other:collect_method_evidence` 为期望值的断言逐条写「原值 / 新值 / 原因」。
### 4.2 记录
- `docs/BUG_HISTORY.md` BUG-586:状态先写 `investigating`2.2 未证实)或 `resolved`(若执行方能从 staging receipt 证实触发链);复发自 BUG-558(同现象),关联 BUG-580、BUG-582`CHANGELOG.md``PROGRESS-rectification-other-collect-fallback-20260908.md``docs/testing/rectification-scenarios-20260907.md` 加「七领域问完后下一问必须是职业题或直接出区间卡,不得出现『再说一件』」。
## 5. 让步顺序
决策 1、2、3 必做且同一提交;决策 4 若 set-focus 工具改动牵扯太广可后置为 P2 但要写进度记录;决策 5 必做(否则存量 Case 卡死);4.2 不可省。
## 6. 开工前置命令
```bash
git fetch origin --prune
git worktree add -b codex/rectification-other-collect-fallback-20260908 .worktrees/rectification-other-collect-fallback-20260908 origin/staging
cd .worktrees/rectification-other-collect-fallback-20260908
ln -s /workspace/Jyotisha/frontend/node_modules frontend/node_modules
cd frontend && ./node_modules/.bin/tsc --noEmit; npm run lint 2>&1 | tail -1
ls tests/rectification-*.test.ts tests/agent-voice-copy-contract.test.ts | grep -v database | xargs npx tsx --test 2>&1 | grep -E "^# (tests|pass|fail)"
grep -rn "也可以再说一件" src tests # 收工时必须为空
```
`codex/rectification-duplicate-narration-20260907`BUG-584/585,改 `step-answer.ts` / `agent-run.ts` / `collect-prompt.ts`)无文件重叠;谁后合谁 rebase。
@@ -77,3 +77,21 @@ ln -s /workspace/Jyotisha/frontend/node_modules frontend/node_modules
cd frontend && ls tests/rectification-*.test.ts tests/consultation-*.test.ts tests/agent-voice-copy-contract.test.ts | grep -v database | xargs npx tsx --test 2>&1 | grep -E "^# (tests|pass|fail)"
grep -n "^version" ../skills/jyotish-birth-time-rectification/SKILL.md
```
## 验收(Claude2026-09-07`origin/staging` @ `ab57d03f`
**未通过(P1 阻断)**`frontend/src/components/rectification-agentic-chat.tsx` L1655 把 `dismissedRangeResultId !== candidateResult.resultId` 放在 `candidateResult &&` 判空之前,`tsc --noEmit``TS18047: 'candidateResult' is possibly 'null'`。硬红线 tsc 0 错不满足,`next build` 会失败,staging 门禁过不去。修法一行:把判空提到最前面。执行方进度记录写 tsc 0,不符。
其余各项(在修掉这一行之后可视为通过):
| 项 | 结论 |
| --- | --- |
| 4.1 投影 `range_delivery` | 通过。`divergence-panel.ts`:活跃簇 2~3、只列未问的 D9/D10/月宿层,星座先从探针选项取、再从换升表推,文案只取 `D9_TYPE_TABLE` / `D10_TYPE_TABLE``userChoice` 与 nakshatra `traits`;代表 id 用公开 UUID |
| 4.2 组件 | 通过。四张分钟卡、相对支持度、当前推荐徽章全部删除;两个动作;列单选,"都不像 / 说不好"清空 |
| 4.3 旁白与 Skill | 通过。三行预测窗口删除;Skill §9 改口径、10.0.15 快照与 live 一致、五份 references 与 10.0.14 逐字相同(没有新写方法学);注册表新条目 `sourceCommit: null` 与历史条目一致 |
| 4.4 记录 | 通过;手测场景第 5 条已加 |
| P2 | 旁白改成"下一次事业变动的预测窗口留在采用后的核对阶段",但采用后没有任何地方再展示预测窗口(`prospectiveWindowsNarration` 已无调用点)——这是一句兑现不了的承诺,与之前"说一声我来汇总"同类。要么在采用后核对首轮真的接上,要么把这句删掉 |
| P3 | "再补一件经历"只在本地隐藏卡片,不给用户任何提示该说什么;建议隐藏后由步骤条/旁白给一句采集提示 |
| P3 | lint warning 从 84 涨到 103 |
修复单:`TASK-rectification-range-delivery-fix-20260907.md`
@@ -0,0 +1,44 @@
# TASK · 区间交付卡修复单:tsc 判空顺序、兑现不了的预测窗口句、再补经历无提示(2026-09-07)
- 基线:`origin/staging` @ `ab57d03f`
- 分支:`codex/rectification-range-delivery-fix-20260907`
- 执行方:coding agent;验收:Claude
- 优先级:**P1 阻断**tsc 不过,staging 门禁与 `next build` 都会失败,BUG-581~582 的 P0 修复被一起卡住)
- BUG 编号起点:**BUG-583**(开工时 `grep -o "^## BUG-5[0-9][0-9]" docs/BUG_HISTORY.md | tail -1` 复核)
## 1. 事故实证
| # | 事实 | 位置 |
| --- | --- | --- |
| 1 | `tsc --noEmit``TS18047: 'candidateResult' is possibly 'null'` | `frontend/src/components/rectification-agentic-chat.tsx` L1655`showSelectionCards && dismissedRangeResultId !== candidateResult.resultId && message.renderKey === selectionCardMessageKey && candidateResult && (…)` |
| 2 | 交付旁白句"下一次事业变动的预测窗口留在采用后的核对阶段",但 `prospectiveWindowsNarration` 已无任何调用点,采用后从不展示 | `user-copy.ts` `deferredCareerWindow``grep prospectiveWindowsNarration` 只剩定义 |
| 3 | 「再补一件经历」只 `setDismissedRangeResultId`,卡片消失后没有任何提示 | `rectification-agentic-chat.tsx` `onContinue` |
| 4 | 执行方进度记录写"tsc 0",与 `ab57d03f` 实测不符 | `PROGRESS-rectification-range-delivery-card-20260907.md` |
## 2. 决策记录
1. 判空提到最前:`showSelectionCards && candidateResult && dismissedRangeResultId !== candidateResult.resultId && …`
2. 预测窗口二选一,本单选 **接上**:采用后 `postAdoptVerify` 第一轮旁白末尾追加 `prospectiveWindowsNarration(receipt.prospective_probes)`(有则加,无则不加);`deferredCareerWindow` 句保留。若执行方评估接不上,则删句并写明。
3. 「再补一件经历」隐藏卡片后,步骤条切到"第 1 步·收集经历"文案,并在输入框上方给一句 `continueCollectFallback`(已有文案:"请继续说下一件你记得比较清楚、大概带年份的经历。")。
4. 进度记录必须贴 `tsc --noEmit` 的真实输出行;没有复跑不得写 0。
## 3. 任务分解
- 3.1 判空修复 + 源扫描断言(`rectification-range-delivery-20260907.test.ts`:正则锁定 `candidateResult &&` 出现在 `dismissedRangeResultId` 之前)。
- 3.2 预测窗口接到采用后首轮(`postAdoptVerify` 路径),测试:有 `prospective_probes` 的 receipt → 采用后首轮旁白含"预测下一次事业变动";无则不含。
- 3.3 再补经历提示:组件测试——点击后卡片隐藏且出现 `continueCollectFallback`
- 3.4 记录:BUG-583tsc 判空,复发自本单 4.2);CHANGELOG 一句;PROGRESS 贴 tsc 原文。
## 4. 让步顺序
3.1 必须当天合入(否则 P0 修复无法部署);3.2、3.3 其次;3.4 不可省。
## 5. 开工前置命令
```bash
git fetch origin --prune
git worktree add -b codex/rectification-range-delivery-fix-20260907 .worktrees/rectification-range-delivery-fix-20260907 origin/staging
cd .worktrees/rectification-range-delivery-fix-20260907
ln -s /workspace/Jyotisha/frontend/node_modules frontend/node_modules
cd frontend && ./node_modules/.bin/tsc --noEmit; echo "tsc exit $?"
```
@@ -70,3 +70,22 @@ ln -s /workspace/Jyotisha/frontend/node_modules frontend/node_modules
ln -s /workspace/Jyotisha/.venv .venv
cd frontend && ls tests/rectification-*.test.ts tests/consultation-*.test.ts tests/agent-voice-copy-contract.test.ts | grep -v database | xargs npx tsx --test 2>&1 | grep -E "^# (tests|pass|fail)"
```
## 验收(Claude2026-09-07`origin/staging` @ `ab57d03f`;与区间交付卡同一提交)
| 门 | 结果 |
| --- | --- |
| tsc | **1 错**(在同提交的区间交付卡改动里:`rectification-agentic-chat.tsx` L1655 先访问 `candidateResult.resultId` 再判空,`'candidateResult' is possibly 'null'`)。执行方进度记录写 tsc 0,与实测不符——应是最后一次改动后没有复跑 |
| lint | 0 error / 103 warning(比上一轮多 19 条,分散在测试文件与 `rectification-v9-tools.ts`,未清) |
| 前端 rectification + consultation + birth-time + skill + voice(非 DB | 1605 / 0 |
| Python v5_services / input_contract / growth contract | 全绿 |
| 引擎 `minute_step=1` 字节级不变 | 全等 |
| 项 | 结论 |
| --- | --- |
| 4.1 BUG-581 | 通过。`scoreAndPersistCurrentEvidence` 抽到 `v9/score-persist.ts`,停止/闲置 `keepAnswers=true``previous` 重放;时段/放宽 `keepAnswers=false` 仍写出空答案的 `inference_state`;VedAstro 校验与对比包逻辑随之搬迁 |
| 4.2 BUG-582 | 通过。无框区分题一律 `skipped`;工具层与 `spokenFollowupForUser` 的 GENERIC 兜底删除;`dropped_probes(frameless_distinguish)` 记录 |
| 4.3 顺序 | 通过。holdout 排在带年月采集与职业之后 |
| 4.4 记录 | 通过 |
结论:本单逻辑通过,但同一提交因区间交付卡的 tsc 错误**整体不可部署**,见 `TASK-rectification-range-delivery-card-20260907.md` 验收段。
@@ -88,5 +88,5 @@
- 若有分歧面板,每列只有 1~3 行,且都是没问过的 D9 / D10 / 月宿层
- 不见「相对支持度」「当前推荐」「采用此时间」
- 点「按这个范围用」成功,设置页出生时间状态变为已采用
- 点「再补一件经历」卡片收起,服务端范围不变
- 点「再补一件经历」卡片收起,输入框上方出现「第 1 步·收集经历」和「请继续说下一件…」,服务端范围不变
+1 -1
View File
@@ -252,7 +252,7 @@ The birth-time rectification session is the consultation transcript plus a house
- **Life-event evidence:** after deterministic questionnaire completion, render three structured event rows by default and allow up to six. Each row uses a domain select, a precision select, and a matching year/month/day control; free-form descriptions are not part of scoring.
- **Candidate result:** keep the reported range, candidate interval, and active-time status visually separate. Delivery shows one range card (window, representative minute, stable/sensitive themes, boundary sentence) plus an optional unasked D9/D10/nakshatra panel. Support numbers stay on the house board. Low confidence keeps evidence editing open; medium offers save or add evidence; high uses a separate confirmation action and never labels the representative minute as the true birth time.
- **Evidence accessibility:** every row keeps visible labels, validation errors use live regions, add/remove controls retain 44px targets, and scoring/confirmation loading states disable duplicate submission without hiding the existing evidence.
- **One-question guide:** the guided journey renders only the persisted `nextAction` and one server-selected question. A deterministic question is visible immediately; Agent wording may replace it without changing the question identity, domain, precision request, progress, or permissions. The composer explicitly permits an approximate year and keeps skip and pause as secondary 44px actions. Spoken collect (`collect_spoken`) places the same `CHOICE_STOP_LABEL` (“先这样,先看当前范围”) as a 44px secondary button above `ChatComposer`. The readonly range line is a status sentence, not a stop control. Discriminator cards fold “为什么问这题” under the stem. Hovering or selecting an option does not reveal an `answer_impact` time line. The composer has no `rectification-step-state` status sentence.
- **One-question guide:** the guided journey renders only the persisted `nextAction` and one server-selected question. A deterministic question is visible immediately; Agent wording may replace it without changing the question identity, domain, precision request, progress, or permissions. The composer explicitly permits an approximate year and keeps skip and pause as secondary 44px actions. Spoken collect (`collect_spoken`) places the same `CHOICE_STOP_LABEL` (“先这样,先看当前范围”) as a 44px secondary button above `ChatComposer`. The readonly range line is a status sentence, not a stop control. Discriminator cards fold “为什么问这题” under the stem. Hovering or selecting an option does not reveal an `answer_impact` time line. The composer has no `rectification-step-state` status sentence. After “再补一件经历” dismisses the range card, `rectification-composer-meta` shows the collect-step headline plus `continueCollectFallback`.
- **Draft review:** natural-language answers become one inline review card. The evidence domain is read-only and uses its Chinese label; precision controls which exact year, month, or day input is available. Incomplete drafts keep edit and skip paths visible, while confirmation is disabled until the structured date is valid. Status and errors use polite or assertive live regions without clearing the persisted journey.
- **Scoring and retry:** `score_pending` is a quiet progress surface with cancellable bounded polling and no manual compare control. `retry_scoring` preserves the confirmed evidence and exposes one explicit retry action. Refresh and device changes resume from the persisted action rather than inferring progress from copy.
- **Guided candidate states:** low confidence presents the saved candidate range and either another evidence question or a safe finish; medium confidence can save the range but never apply a representative minute; high confidence names both “候选时间” and “当前排盘使用时间” before explicit confirmation; ready states that the current chart time changed while the original report remains preserved. No state calls a candidate the true birth minute.
+2 -1
View File
@@ -33,7 +33,8 @@ Jyotisha 的可见文案是产品的一部分。正确性红线(真实性、
| 剩下的题分不开当前候选。更站得住的范围是 05:15,代表分钟 05:15。 | 再问下去也分不出更准的时间了。眼下更站得住的是 05:15。 | 单分钟不要把范围和代表分钟念两遍;「当前候选」是内部词。 |
| 可以从下面的时间里选一个采用。 / 下面的时间可以先用着。 | 我按你说的经历认真分析过了,下面是这次的结果。 | 这是认真分析后的结果,不是随便先用着;采用仍不是确认。区间交付卡必须就在这句下面。 |
| 相对支持度 16 · 采用此时间 | 按这个范围用 / 再补一件经历 | 交付对象是区间,不是四张分钟卡上的支持度数字。 |
| 候选 A 预测下一次事业变动更可能在 2027 年 3 月附近。 | 下一次事业变动的预测窗口留在采用后的核对阶段。 | 交付轮不并排三行预测。 |
| 候选 A 预测下一次事业变动更可能在 2027 年 3 月附近。 | 下一次事业变动的预测窗口留在采用后的核对阶段。 | 交付轮不并排三行预测。采用后首轮核对旁白末尾才接上实际窗口句。 |
| 「再补一件经历」后卡片消失、输入框上方空白 | 第 1 步·收集经历。请继续说下一件你记得比较清楚、大概带年份的经历。 | 收起交付卡后要告诉人下一步做什么。 |
| 采用后会拿盘外核对来验证。 | 前事核对到这里。之后新建对话即按已采用时间排盘;对不上随时改选。 | 已采用且没有下一道核对题时用 `postAdoptVerifyDone`,不再念 `adoptCue`。 |
| (采用后只念代表分钟)代表分钟是 05:12。 | 这 27 分钟里,事业方向、性格底色的判断是稳定的;婚恋(D9)会随分钟变,看盘时按范围读。这不是已确认的唯一出生分钟。 | 采用后先讲区间里什么稳、什么随分钟变;两句来自服务端,不是模型编的。 |
| 当前问题已更新,请刷新后重新作答。 | 这一问刚换成新的,刷新后再答就行。 | 机器状态句改成下一步。 |
+10
View File
@@ -3091,6 +3091,16 @@ input:not([type="radio"]):not([type="checkbox"]):not([class^="ant-"]):not([class
.rectification-composer-meta .rectification-collect-stop {
margin: 0;
}
.rectification-continue-hint {
margin: 0;
color: var(--color-ink-secondary);
font-size: var(--type-caption);
line-height: 1.5;
}
.rectification-continue-hint strong {
color: var(--color-ink);
font-weight: 600;
}
.rectification-candidates-heading { display: grid; gap: var(--space-1); }
.rectification-candidates-heading strong { font-size: var(--type-title-sm); font-family: var(--font-display); font-weight: 600; letter-spacing: -.2px; }
.rectification-candidates-heading span,
@@ -39,7 +39,7 @@ import {
workingRectificationHouseTable,
type RectificationCandidateResult,
} from "@/lib/rectification-candidate-result";
import { RectificationRangeDelivery } from "@/components/rectification-range-delivery";
import { RangeDeliveryContinueHint, RectificationRangeDelivery } from "@/components/rectification-range-delivery";
import {
diffRectificationBoard,
RECTIFICATION_BOARD_SPLIT_MIN_PX,
@@ -1506,6 +1506,15 @@ export function RectificationAgenticChat(props: RectificationAgenticChatProps) {
void submitStructuredChoice(STOP_ACTION, "stop");
}
const rangeCardDismissed = Boolean(
candidateResult
&& dismissedRangeResultId === candidateResult.resultId
&& !candidateResult.selectedTime
&& savedStatus !== "accepted"
&& savedStatus !== "confirmed"
&& !readonly
);
const spokenCollectOpen = currentQuestion?.kind === "collect_spoken" && !readonly && !busy;
const boardPeek = compactBoard && !boardOpen ? (
<RectificationBoardPeek
result={candidateResult}
@@ -1652,7 +1661,7 @@ export function RectificationAgenticChat(props: RectificationAgenticChatProps) {
onRegenerate={() => void regenerateMessage(message)}
/>
)}
{showSelectionCards && dismissedRangeResultId !== candidateResult.resultId && message.renderKey === selectionCardMessageKey && candidateResult && (
{showSelectionCards && candidateResult && dismissedRangeResultId !== candidateResult.resultId && message.renderKey === selectionCardMessageKey && (
<RectificationRangeDelivery
result={candidateResult}
acceptingCandidateId={acceptingCandidateId}
@@ -1724,8 +1733,10 @@ export function RectificationAgenticChat(props: RectificationAgenticChatProps) {
<span className="rectification-adopt-status__hint"></span>
</div>
)}
{currentQuestion?.kind === "collect_spoken" && !readonly && !busy && (
{(rangeCardDismissed || spokenCollectOpen) && (
<div className="rectification-composer-meta">
<RangeDeliveryContinueHint visible={rangeCardDismissed && !busy} />
{spokenCollectOpen && (
<button
type="button"
className="rectification-collect-stop"
@@ -1733,6 +1744,7 @@ export function RectificationAgenticChat(props: RectificationAgenticChatProps) {
>
{CHOICE_STOP_LABEL}
</button>
)}
</div>
)}
<ChatComposer
@@ -9,6 +9,7 @@ import {
rangeDeliverySensitiveCopy,
rangeDeliveryStableCopy,
} from "@/lib/rectification-agentic/user-copy";
import { STEP_STATE_COPY } from "@/lib/rectification-agentic/v9/step-state";
import type { RangeDeliveryProjection } from "@/lib/rectification-agentic/v9/divergence-panel";
function adoptCandidateId(
@@ -25,6 +26,17 @@ function adoptCandidateId(
?? null;
}
export function RangeDeliveryContinueHint({ visible }: Readonly<{ visible: boolean }>) {
if (!visible) return null;
return (
<p className="rectification-continue-hint" role="status">
<strong>{STEP_STATE_COPY.collect.headline}</strong>
{" "}
{RECTIFICATION_USER_COPY.continueCollectFallback}
</p>
);
}
export function RectificationRangeDelivery({
result,
acceptingCandidateId,
@@ -92,7 +92,7 @@ import {
import { followupCaseArgs, isBlockChoiceSchema, isWidenWindowSchema } from "./block-scan.ts";
import { mutateCaseForBlockChoice, mutateCaseForWidenWindow, rescoreStaleMinuteSnapshotIfNeeded } from "./block-scan-answer.ts";
import type { SessionOutcomeKind } from "./confirmation-gate";
import { refinementFromDecisionReceipt } from "./refinement-packet";
import { prospectiveWindowsNarration, refinementFromDecisionReceipt } from "./refinement-packet";
import { projectCurrentQuestion } from "./turn-decision";
const EXHAUSTION_DELIVERY_ACTIONS = new Set([
@@ -143,6 +143,18 @@ function withDeferredCareerWindow(base: string): string {
return `${base} ${extra}`.replace(/\s+/g, " ").trim();
}
export function appendPostAdoptProspectiveWindows(
narration: string,
receipt: Readonly<Record<string, unknown>> | null | undefined,
): string {
const extra = prospectiveWindowsNarration(
refinementFromDecisionReceipt(receipt).prospective_probes,
);
if (!extra) return narration;
if (narration.includes(extra)) return narration;
return `${narration} ${extra}`.replace(/\s+/g, " ").trim();
}
async function withRangeReadingNarration(
base: string,
input: {
@@ -1105,21 +1117,26 @@ export async function persistNextInterviewIfIdle(input: {
caseId: input.caseId,
askedTurnId: input.askedTurnId ?? null,
});
let dossier = rescored.dossier;
const finishIdle = <T extends { hostNarration: string | null }>(result: T): T => {
let hostNarration = result.hostNarration;
if (dossier.case.acceptedTime && hostNarration) {
hostNarration = appendPostAdoptProspectiveWindows(
hostNarration,
dossier.latestResult?.decisionReceipt,
);
}
if (
input.userStopped === true
&& rescored.rescoreAttempted
&& !rescored.snapshotCurrent
&& result.hostNarration
&& hostNarration
) {
return {
...result,
hostNarration: withLastSuccessfulCompareNotice(result.hostNarration),
};
hostNarration = withLastSuccessfulCompareNotice(hostNarration);
}
return result;
if (hostNarration === result.hostNarration) return result;
return { ...result, hostNarration };
};
let dossier = rescored.dossier;
const staleFocus = dossier.conversationSummary.activeFocus;
const staleFocusId = staleFocus?.id;
if (
@@ -169,6 +169,7 @@ test("delivery narration defers career windows and the range card replaces minut
assert.doesNotMatch(choice, /withProspectiveWindows/);
assert.doesNotMatch(choice, /候选 A 预测/);
assert.match(choice, /deferredCareerWindow/);
assert.match(choice, /appendPostAdoptProspectiveWindows/);
assert.match(chat, /<RectificationRangeDelivery/);
assert.doesNotMatch(chat, /相对支持度/);
assert.doesNotMatch(card, /相对支持度|概率|置信度/);
@@ -1,6 +1,8 @@
import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
import test from "node:test";
import { createElement } from "react";
import { renderToStaticMarkup } from "react-dom/server";
import { INFERENCE_ALGORITHM_VERSION, type InferenceCandidate, type InferenceState } from "../src/lib/rectification-agentic/core/types.ts";
import {
@@ -10,8 +12,11 @@ import {
d9DivergenceLabel,
} from "../src/lib/rectification-agentic/v9/divergence-panel.ts";
import { D10_TYPE_TABLE, D9_TYPE_TABLE } from "../src/lib/rectification-agentic/v9/varga-type-tables.ts";
import { REPRESENTATIVE_MINUTE_DISCLAIMER } from "../src/lib/rectification-agentic/user-copy.ts";
import { RECTIFICATION_USER_COPY, REPRESENTATIVE_MINUTE_DISCLAIMER } from "../src/lib/rectification-agentic/user-copy.ts";
import type { NakshatraBoundary } from "../src/lib/rectification-agentic/v9/refinement-packet.ts";
import { appendPostAdoptProspectiveWindows } from "../src/lib/rectification-agentic/v9/answer-choice.ts";
import { RangeDeliveryContinueHint } from "../src/components/rectification-range-delivery.tsx";
import { STEP_STATE_COPY } from "../src/lib/rectification-agentic/v9/step-state.ts";
const ID_A = "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaa1";
const ID_B = "bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbb2";
@@ -288,3 +293,53 @@ test("the range-delivery component does not render relative support", () => {
assert.match(source, /rangeDeliveryAdopt/);
assert.match(source, /rangeDeliveryContinue/);
});
test("chat null-checks candidateResult before reading dismissedRangeResultId", () => {
const chat = readFileSync(new URL("../src/components/rectification-agentic-chat.tsx", import.meta.url), "utf8");
const start = chat.indexOf("showSelectionCards && candidateResult");
assert.ok(start >= 0);
const slice = chat.slice(start, start + 280);
assert.match(slice, /candidateResult && dismissedRangeResultId !== candidateResult\.resultId/);
assert.ok(slice.indexOf("candidateResult &&") < slice.indexOf("dismissedRangeResultId"));
});
const CAREER_WINDOW_PROBE = {
candidate_label: "A",
domain: "career",
window_label: "2027 年 3 月附近",
user_meaning: "候选 A 预测下一次事业变动更可能在 2027 年 3 月附近。这是预测窗口,不是承诺;下次发生时回来补一条,可进一步分辨。",
used_for_scoring: false,
};
test("post-adopt first-round narration appends prospective windows when present", () => {
const base = "2013 年前后,钱上有没有明显变化?";
const withWindows = appendPostAdoptProspectiveWindows(base, {
prospective_probes: [CAREER_WINDOW_PROBE],
});
assert.match(withWindows, /预测下一次事业变动/);
assert.match(withWindows, /2013 年前后/);
const without = appendPostAdoptProspectiveWindows(base, { prospective_probes: [] });
assert.equal(without, base);
assert.doesNotMatch(without, /预测下一次事业变动/);
const idle = readFileSync(
new URL("../src/lib/rectification-agentic/v9/answer-choice.ts", import.meta.url),
"utf8",
);
const start = idle.indexOf("export async function persistNextInterviewIfIdle");
const slice = idle.slice(start, start + 1800);
assert.match(slice, /appendPostAdoptProspectiveWindows/);
assert.match(slice, /dossier\.case\.acceptedTime/);
});
test("dismissing the range card hides it and shows the collect-continue hint", () => {
const hidden = renderToStaticMarkup(createElement(RangeDeliveryContinueHint, { visible: false }));
assert.equal(hidden, "");
const shown = renderToStaticMarkup(createElement(RangeDeliveryContinueHint, { visible: true }));
assert.ok(shown.includes(STEP_STATE_COPY.collect.headline));
assert.ok(shown.includes(RECTIFICATION_USER_COPY.continueCollectFallback));
assert.doesNotMatch(shown, /rectification-step-state/);
const chat = readFileSync(new URL("../src/components/rectification-agentic-chat.tsx", import.meta.url), "utf8");
assert.match(chat, /RangeDeliveryContinueHint/);
assert.match(chat, /setDismissedRangeResultId\(candidateResult\.resultId\)/);
assert.match(chat, /rangeCardDismissed/);
});