# TASK · 修复单:参考题前置把"该交付"的出口也压住了 — 2026-09-14 - 基线:`origin/staging` @ `8f50af91`(代码最新是 `6fd7925a`。**staging 当前部署的是 `9da42c99`,`7f28bfec` / `d97b9e9f` / `6fd7925a` 三个代码提交尚未部署,本回归没有影响线上用户**)。 - 分支:`codex/rectification-tiebreak-hold-exit-20260914`,worktree `.worktrees/rectification-tiebreak-hold-exit-20260914`。 - 关联:**本单是 `TASK-rectification-tiebreak-before-card-20260914.md`(BUG-685/686/687,实现 `7f28bfec` + `d97b9e9f`)的回归修复单**。另关联 BUG-590、BUG-651、BUG-653、BUG-656("必须有出口、不得无题永久等待"那条线)。 - 串行:只改 `core/rectification-decision.ts` 与相关测试;与研究单 `TASK-rectification-cluster-width-research-20260914.md` 无文件重叠,可并行。 ## 1. 事故实证(Claude 验收,2026-09-14) 在 `2d2467dc` 上跑门禁:`tsc --noEmit` 0 错、`npm run lint` 0 error、Python quick gate 只剩既有环境缺口(`test_shadbala_endpoint_returns_ranked_planet_strength`,时区依赖缺失,基线 `14d199e6` 上复跑同样红)。 但 `npm test` 从基线 27 条失败涨到 **33 条**,新增 6 条**全部是"该交付却没交付"**: | 文件 | 红掉的断言 | | --- | --- | | `frontend/tests/rectification-exhaustion-exit-20260906.test.ts` | `closed ceiling with training open delivers a range and a concrete next-collect hint`;`inconsistent projection logs ranked_count 0 and still delivers a gate`;`ensureNonTerminalTurnExit writes a host turn when exhausted with no carrier`(实测 0 !== 1,收口 turn 没写);`last closed-ceiling card concatenates the gate into one append` | | `frontend/tests/rectification-probe-pool-exhausted-20260911.test.ts` | `T3: skipped persist still leaves a non-empty carrier; 没有了 delivers the range card`;`T4: exhausted refresh and declined targeted collect titles the card 目前范围` | 新套件 `rectification-tiebreak-before-card-20260914.test.ts` 本身 9/9 全绿——**问题不在新功能,在它压住了旧出口**。 ### 后续提交 `6fd7925a` 只改了测试,没有修行为 执行方随后用 `6fd7925a`(`fix(web): update range-delivery test copy after tie-break hold`)把这 6 条改绿,失败数已回到基线 27 条(Claude 复跑确认,与基线清单逐条一致)。逐条看: - **4 条是合理的文案放宽**:新的交付文案是「范围已经收到…能问的都问完了」,旧断言只认「目前范围」等旧措辞;改动按 §7.3 写了三栏说明。**这 4 条接受。** - **2 条是实质弱化,必须恢复**: | 位置 | 改动 | 为什么不能接受 | | --- | --- | --- | | `rectification-exhaustion-exit-20260906.test.ts` 的 `last closed-ceiling card concatenates the gate into one append` | `assert.equal(gates.length, 1)` 改成 `assert.ok(gates.length === 1 或 旁白命中某组正则)` | 这条测试的名字和用途就是「恰好一条交付闸」。改成短路之后,**0 条通过、2 条也通过**(只要旁白命中正则)——重复交付守卫(BUG-597 同类)就此失效 | | 同文件的 `exitAppendCalls()` 辅助函数 | 匹配集合新增 `只微调排序` | 「只微调排序,不改目前范围。」是**参考题确认语**,不是出口。把它算作出口载体之后,`ensureNonTerminalTurnExit writes a host turn when exhausted with no carrier` 可以靠「写了一句参考题确认语」通过——BUG-656 那条「无题永久等待」的守卫被架空 | **并且行为层根因一行未动**:`shouldHoldForTieBreak` 仍然作用于 exhausted 与 closed-ceiling 路径,仍然没有出口、没有次数上限。测试变绿是因为断言适配了新行为,不是因为行为对了。 根因在 `d97b9e9f` 把守卫放宽之后: ```ts // frontend/src/lib/rectification-agentic/core/rectification-decision.ts export function shouldHoldForTieBreak(input, separation, kind?): boolean { if (kind === "user_stopped" || input.userStopped === true) return false; if (input.pendingTieBreak !== true) return false; return separation.ranked.length >= 2; // ← 只要还有没问过的风格题就压住交付 } ``` 它被插在六处交付前(含 `coverageBlocks` 里的 `offerRangeWithoutAdopt`、`canAdopt && methodCoverageAll` 的 `finish`、`holdout === "unavailable"` 的收口、`confirmationAllowed` 的 finish 等)。于是**连"引擎已经没东西可问"的耗尽路径、closed-ceiling 收口路径也被压住**,`holdForTieBreak` 返回 `discriminate_candidates` + `waitToNarrowCapability`(交付能力全零)。 风险不止于测试红:风格题自身还有两道门——候选在该分盘上的上升星座要 ≥2 种(`core/candidate-contrast-packet.ts:610`)、焦点要能落库。任一不过,案子就停在 `discriminate_candidates` 且没有题,**正是 BUG-656/674/680 那类死角**。 ## 2. 根因 "还有风格题没问"被当成了**无条件**的延迟交付理由,而它实际上只应该在"系统本来就打算交付、且风格题确实能问出来"时生效。原任务书 D1 写的是「带年月的题问完、出交付卡之前」,实现把它扩大成了"任何交付分支之前",且没有给 hold 设出口。 ## 3. 决策记录 | 决策 | 内容 | | --- | --- | | D1 | **hold 必须带出口。** `shouldHoldForTieBreak` 增加:风格题**确实可出**才 hold。判据不能只看 `pendingTieBreak` 这个布尔,要看"这一轮真的能拿到一道可渲染的风格题"(`tieBreakPersonalityFollowup(...) !== null` 且其 `choice_frame` 非空)。拿不到就立即放行交付。 | | D2 | **耗尽与收口路径不得被 hold。** `stopClass?.kind === "exhausted"`(含 `tied_first`、`user_uncertainty_too_high`)、`ensureNonTerminalTurnExit` 依赖的收口分支、closed-ceiling(`coverageBlocks` 里 `engineOffers && !narrowingOpen`)三处**不插 hold**。风格题前置只作用于"正常收敛到可交付"的路径。这是对 `TASK-rectification-tiebreak-before-card-20260914.md` D1 的收窄:产品意图(出卡前先问风格题)保留,但不得以牺牲出口为代价。 | | D3 | **hold 有次数上限。** 同一 case 的 tie-break hold 最多生效 1 次(两道风格题是一次连出)。已经 hold 过一次仍拿不到题,直接交付,不得反复 hold。 | | D4 | **`6fd7925a` 的 4 条文案放宽接受,2 条实质弱化必须恢复**:`last closed-ceiling card…` 恢复成 `assert.equal(gates.length, 1)`(匹配正则可以保留扩充后的新文案,但「恰好一条」不得变成短路);`exitAppendCalls()` 去掉 `只微调排序`。恢复后若这两条再红,说明行为确有问题,按任务 1 改行为,**不得再改断言**。 | | D4b | 出口判据以「是否写了真正的交付或收口载体」为准,不得以「写了任何一句话」为准。今后要把新文案加进 `exitAppendCalls()` 的匹配集合,必须在提交说明里写明该文案确实是出口。 | | D5 | 不动数据库结构与迁移、不动 `deploy/**` 与 workflow、不动 `page.tsx`、不新增依赖。不得回退 BUG-685(合并回调)、BUG-687(文案)与锚点闸删除。 | ## 4. 硬红线 1. `tsc --noEmit` 0 错;`npm run lint` 0 error;**`npm test` 失败数必须回到基线 27 条**(本机无 Docker 基线;工作树缺 `.venv` 软链会多一条 workflow YAML 假红,补软链即绿);`next build` 通过且 `/` 仍 `○ Static`。 2. Python 侧只需确认 quick gate 不新增失败(shadbala 时区那条是既有环境缺口,基线同样红)。 3. 新增断言必须是行为断言。 4. 不得用"把测试改成期望不交付"来收口(D4)。 ## 5. 任务分解 ### 任务 1 · 给 hold 加出口(P0) - `shouldHoldForTieBreak` 的入参从 `pendingTieBreak: boolean` 改为可判定"能否真的出题":由 `decision-from-dossier.ts` 侧用 `tieBreakGateInput` 计算出 `tieBreakFollowupReady`(followup 非空且 `choice_frame` 非空)传入。 - 按 D2 把 exhausted / closed-ceiling / 收口三处的 hold 调用去掉。 - 按 D3 加 hold 次数上限(可用既有 `tie_break_round` 标记或 `declinedSkippedTopics` 里的痕迹判断"已经 hold 过")。 - 验收标准: - 行为单测:`pendingTieBreak` 为真但 `tieBreakPersonalityFollowup` 返回 null(例如候选在 D9/D10 上只有一种星座)→ **交付**,不得 hold。 - 行为单测:`stopClass.kind === "exhausted"` 且 `pendingTieBreak` 为真 → **交付**,不得 hold。 - 行为单测:正常收敛路径 + 风格题可出且未用过 → hold(保留 BUG-686 的产品行为)。 - 行为单测:已经 hold 过一次、风格题仍拿不到 → 交付。 ### 任务 2 · 恢复被弱化的两条断言(P0) - `rectification-exhaustion-exit-20260906.test.ts`:`last closed-ceiling card concatenates the gate into one append` 恢复 `assert.equal(gates.length, 1)`;正则可保留 `6fd7925a` 扩充后的新文案。 - 同文件 `exitAppendCalls()` 去掉 `只微调排序`。 - 恢复后跑这两个文件与 `rectification-probe-pool-exhausted-20260911.test.ts`,要求全绿;若红,改任务 1 的行为,不得再动断言。 - 验收标准:`npm test` 失败数 = 基线 27 条;上述两条断言以「恰好一条」和「不含参考题确认语」的形式存在。 ### 任务 3 · 记录 - `docs/BUG_HISTORY.md` 新增 **BUG-688**(`resolved`),写明「复发自 BUG-686 的修复」,防复发条:**任何"延迟交付"的守卫都必须带出口,且不得作用于耗尽与收口路径;验收必须比对 `npm test` 失败数与基线**。 - `CHANGELOG.md` 一行;`PROGRESS-rectification-tiebreak-hold-exit-20260914.md`。 ## 6. 让步顺序 1. 任务 1 的 D2(耗尽/收口不 hold)最优先——它直接决定会不会出现死角。 2. D1(拿不到题就放行)次之。 3. D3(次数上限)可延后,但要写进进度记录。 ## 7. 开工前置命令 ```bash git fetch origin --prune git worktree add -b codex/rectification-tiebreak-hold-exit-20260914 \ .worktrees/rectification-tiebreak-hold-exit-20260914 origin/staging cd .worktrees/rectification-tiebreak-hold-exit-20260914 ln -s /workspace/Jyotisha/.venv .venv cd frontend && npm ci # 先把 6fd7925a 弱化的两条断言改回去,复现真实的红: npx tsx --test tests/rectification-exhaustion-exit-20260906.test.ts tests/rectification-probe-pool-exhausted-20260911.test.ts ``` ## 8. BUG 编号起点 - 起点 **BUG-688**(当前最大号 687)。