# TASK · 三通道重做修复单(2026-09-18) > 基线:`origin/staging` @ `7ae3aa9a`(含实现提交 `94c1e81f`)。 > 上游任务书:`docs/tasks/TASK-consult-three-channels-20260918.md`;实现进度:`docs/tasks/PROGRESS-consult-three-channels-20260918.md`。 > 本单只修 `94c1e81f` 带进来的未通过项,不重开设计。 > BUG 编号起点:开工时 `docs/BUG_HISTORY.md` 最大号为 **BUG-944**,本单占 **BUG-945 ~ BUG-949**。 ## 0. 验收结论(2026-09-18,Claude 验收) 在 `7ae3aa9a` 的干净工作树上实测: | 项 | 结果 | | --- | --- | | `tsc --noEmit` | 0 错 ✅ | | `npm run lint` | 0 error(120 warning,与基线同量级)✅ | | `npm test` | **3484 tests / fail 43**;基线 `742ffbc6` 为 **3484 / fail 31** → **新增 12 条红,0 条转绿** ❌ | | 测试总数 | 3484 = 3484,未下降 ✅ | | Python `tests/test_birth_time_journey_contract.py`、`tests/test_api_server_growth_contract.py` | 17 passed ✅(属 BUG-939/940 那两个提交) | | staging 部署 | `/api/health` 的 `deployment.gitCommit` 仍是 `742ffbc6`;门禁第 299 行就是 `npm test --prefix frontend`,12 条红即镜像不发布 ❌ | 进度记录写的「`consultation-agentic-runtime.test.ts` 本机 Windows EPERM 无法加载,以 Linux 门禁为准」在 Linux 上已证伪:这 12 条不是环境问题,全部可稳定复现(单独跑该 5 个文件:95 tests / fail 12,与全量一致)。 ## 1. BUG-945(P0)领域上限降到 2,且超限是 schema 拒绝而不是截断 **实证**(`frontend/src/mastra/consultation-tools.ts`,`7ae3aa9a` 行号): - `:93` `CONSULTATION_DOMAIN_DURATION_MS` 由 21_000 改成 31_000。 - `:95-99` `CONSULTATION_DOMAIN_WALL_CLOCK_MS = 110_000 - 45_000 = 65_000`,`MAX_CONSULTATION_DOMAINS = floor(65_000 / 31_000) = **2**`(改前是 3)。 - `:137` 工具入参 `domains: z.array(...).min(1).max(MAX_CONSULTATION_DOMAINS)`。 直接实跑 `createConsultationTools(...).execute({question, domains:["career","finance","home"]})`: ``` calls [] {"error":true,"message":"Tool input validation failed for run-jyotish-consultation ... - domains: Array must contain at most 2 element(s)"} ``` 即:**模型提三个领域 → 一个领域都不算**,`runWorkflow` 零次调用。而同文件 `:632` 的工具描述同时对模型说「list every domain the question needs」「Do not drop a relevant domain to shorten the plan」「if the clock runs short the server executes the domains that fit and returns the rest in omitted_domains」——描述承诺截断,schema 执行拒绝,两者互相矛盾。`:368-373` 的 `executableDomainPlan`(真正的截断逻辑)因此在超限路径上永远走不到,成了死安全网。 这条与本次任务书的目标直接冲突:任务书要治的就是 `empty_answer`,而「三领域问题被 schema 挡掉」是又一条通向空回答的新路径。 **要求**: 1. `domains` 入参上限与「执行能装下几个」解耦:schema 侧放宽到领域注册表允许的长度(或保留一个明显更宽的上限,如 6),超出部分由 `executableDomainPlan` 截断并进 `omitted_domains`,不得再由 zod 拒绝整次调用。 2. 工具描述与实际行为对齐:描述里说的「装不下的进 omitted_domains」必须是真的。 3. 验收标准:新增合同测试——请求 3 个及以上领域时 `runWorkflow` 至少被调用 1 次、结果带非空 `omitted_domains`、不出现 `Tool input validation failed`。 ## 2. BUG-946(P1)31s 常数换掉后,既有预算断言没跟着改,6 条测试红 `frontend/tests/consultation-agentic-runtime.test.ts` 仍写着 21s / cap=3 的口径: ``` assert.equal(MAX_CONSULTATION_DOMAINS, 3); // 实际 2 assert.ok(MAX_CONSULTATION_DOMAINS * 21_000 <= 65_000); assert.deepEqual(executableDomainPlan([...5 个]), { domains: [...3 个], ... }); assert.equal(domainFitsRunBudget(42_000, 2), true); ``` 红的 6 条:`multi-domain plan canonicalizes...`、`the merged answer policy...`(`Cannot read properties of undefined (reading 'answer_policy')`)、`the identical natal projection is carried once...`(`reading 'map'`)、`the domain cap is what the run budget can actually pay for`(2 !== 3)、`a plan larger than the cap cannot be expressed...`(false !== true)、`a plan that runs long stops early...`(`[]` vs `['career']`)。 **要求**:BUG-945 修完后,按新口径重写这 6 条断言,每条按 §7.3 写「原值 / 新值 / 原因」三栏。**不得**用放宽断言(改成 `assert.ok(cap >= 1)` 之类)的方式让它变绿。 ## 3. BUG-947(P1)校正流的思考旁白被新门吞掉 `frontend/src/lib/rectification-agentic/v9/stream-mapping.ts:194` 把 `sanitizePublicThinkingText` 换成了 `acceptThinkStepText`: ```ts export function toPublicThinkingDelta(text: string) { const cleaned = acceptThinkStepText(text); // think-step-gate.ts if (!cleaned) return null; return { type: "thinking.delta", text: cleaned }; } ``` `frontend/src/lib/think-step-gate.ts:8-17` 的门是给 **Pass 2 的整条条目** 设计的:`length >= 8`、必须含 `。!?`、必须含 CJK。但这里喂进去的是 **provider 的 `reasoning-delta` 分片**——分片既不成句也常常短于 8 字。测试 `never publishes reasoning-delta to the browser` 正是被这条打红:`mapStreamChunkToThinking("先核对经历。")` 返回 `null`(6 字 < 8)。 后果:校正流用户在计算期看到的「思考」行会大面积消失。任务书 §2.1 的设计是「不发 chunk、只发完整条目」,实现只做了后半句(换门),没做前半句(校正流仍然按 chunk 喂),于是从「删词」变成「整条丢」。 **要求**:二选一,在进度记录里写明选了哪条与理由—— - (a) 校正流也改成条目制:累积到句末标点再过门发一条; - (b) 校正流保留分片通道,但给它一个分片级的**门**(只判 CJK / 是否含工具名等,不改写、不删词),不复用 `acceptThinkStepText`。 验收标准:`frontend/tests/rectification-step-answer.test.ts` 全绿,且新增一条断言证明连续分片最终能产出至少一条可见 thinking 行。 ## 4. BUG-948(P1)三类检测器在生产路径上没人调用 —— 按模式分开处置 ### 4a. 缺口实证 任务书 §3.5 的原话是「正则本身**保留为检测器**供 Pass 4 判定,但不得再写回文本」。实现只做了后半句: - `frontend/src/lib/timing-output-guard.ts:147/152`:`guardPreciseTimingOutput` / `guardGeneralNoBirthTimeOutput` 变成恒等函数。 - `frontend/src/lib/consultation-birth-time-mode.ts:85-95`:`createBirthTimeModeOutputGuard` 仍在调这两个恒等函数——整个出生时间模式的输出边界现在**什么都不做**。 - `frontend/src/app/api/consult/route.ts:1233`:Pass 4 只接了 `detectMethodologyBookkeeping`。`detectPreciseTimingViolations` / `detectGeneralNoBirthTimeViolations` 全仓无生产调用方。 红的 3 条测试:`evidence-blocked unverified answers still use the deterministic timing guard`、`minute-sensitive or timing themes use the precise-timing output guard`、`general mode deterministically rejects personal chart claims while preserving general knowledge`。 ### 4b. 决策记录(产品负责人 2026-09-18 授权) > 产品诉求原话:**「即使用户知道一个出生范围,也能正常地用咱们的产品,而不是不能用。」** 这条决策**推翻**旧的「分钟敏感主题一律套精确时间守卫」红线。旧红线的实证写在 `frontend/tests/rectification-range-reading-20260906.test.ts:50-69`: ```ts const text = "Rahu 大运为 2013年11月21日 至 2031年11月22日。"; // verified_chart + currentTheme 是 marriage(分钟敏感)或 timing → assert.match(marriage, /具体时间已省略/); ``` 即**已校正、已确认分钟的用户,只要问应期就被挖空**。`docs/BUG_HISTORY.md` 第 3504 行记的就是这个事故(用户因此以为看精确日期要先付费做校正)。本单不恢复该行为,也不把它换成「退回重写」——要恢复的是「照实说,按区间说」。 三类检测器按**保护对象**分开处置: | 检测器 | 适用模式 | 处置 | 理由 | | --- | --- | --- | --- | | `exact-timing`(日期 / 月份) | `verified_chart`、`unverified_birth_time`、`declared_birth_window` | **不拦、不改写**,命中只记 `pass4-observe` 运行步 | 大运与行运边界是算出来的事实。范围用户该拿到的是「2013 年 11 月中下旬」这类**区间说法**,不是被删掉的半句话 | | `guarantee`(一定会 / 保证 / 注定 / will definitely) | 全部模式 | **拦**:Pass 4 命中记 `pass4-reject` 并退回 Pass 3 重写一次;二次仍命中则该句不发,其余正文照常 | 与出生精度无关,是可信度红线;拦它不减少任何用户信息 | | `personal-chart`(「你的上升是巨蟹座」类) | **仅** `general_no_birth_time` | **拦**:同上退回重写;二次仍命中用 `GENERAL_NO_BIRTH_TIME_REFUSAL` 兜底句替代**整段**(不得就地替换半句) | 此模式下连出生日期精度都没有,任何「你的盘」断言都是编造;同时挡住 fixture 里的提示注入例句 | 关键是第三行的「仅」:出生范围用户走的是 `declared_birth_window`(`shouldRunDeclaredWindowWorkflow`),不是 `general_no_birth_time`,因此这条拦截不会落到范围用户头上。窗口模式的读盘口径已经存在,沿用即可:`ACCEPTED_RANGE_READING_INSTRUCTION`——「区间里稳定的主题按代表分钟读;会随分钟变的主题按范围读,不得写成单一分钟结论」。 ### 4c. 任务分解与验收标准 1. **接线**:`detectPreciseTimingViolations` / `detectGeneralNoBirthTimeViolations` 接进 Pass 4,按 4b 表分流到 `pass4-observe`(只记)与 `pass4-reject`(退回重写)。回执必须能区分这两者。 - 验收:回执里出现 `pass4-observe` / `pass4-reject` 两种运行步,且都带命中的 `kind`。 2. **恒等壳下线**:`guardPreciseTimingOutput` / `guardGeneralNoBirthTimeOutput` 两个 `@deprecated` 恒等函数与 `createBirthTimeModeOutputGuard` 的调用点一并删除,别留「看起来在保护、其实什么都不做」的假边界。 - 验收:全仓 grep 不到这两个函数名。 3. **范围用户合同测试(本单的产品验收点)**:新增断言——`declared_birth_window` 模式下,含具体日期的正文**原样通过**,不出现 `[具体时间已省略]`,且分钟敏感主题的回答里带区间说法。 - 验收:这条测试在 `git revert` 掉 4b 的改动后必须变红(证明它真的钉住了这个行为)。 4. **重写 3 条红测试**:按新口径改断言,每条写「原值 / 新值 / 原因」三栏,其中 `rectification-range-reading-20260906.test.ts:50-69` 的期望由「挖空」改为「日期原样保留」。不得删除测试。 5. **文案同步**:`CHANGELOG.md` 写明「出生范围用户的应期回答不再被删字,改为按区间表述」;若 `frontend/docs/VOICE.md` 有相关口径一并更新。 ## 5. BUG-949(P2)容量算术两条红 `frontend/tests/consultation-session-capacity.test.ts`: - `measured thinkingSections and receipts stay inside the physical-cap arithmetic`:`1-domain sections JSON length 1053`,低于断言下限 1200(本命计划改成四标题后 thinking plan 变小了,是预期方向,但断言没跟上)。 - `session-detail JSON at the old ~19-round cap versus the new ~50-round cap`:`19 full rounds already exceeded the old combined quota`。 **要求**:按新的 thinking plan 形状重算这两条断言的上下界,写三栏说明;顺带确认 `docs` 里凡是引用「19 轮 / 50 轮」容量口径的地方是否需要同步。 ## 6. 硬红线(照抄上游任务书,未变) 1. `tsc --noEmit` 0 错、`npm run lint` 0 error、`npm test` **不得比基线 `742ffbc6` 的 31 条失败更多**(目标是回到 31,即 12 条全绿)。 2. 测试总数不得低于 3484。 3. 改任何既有断言写「原值 / 新值 / 原因」三栏;不得用弱化断言换绿。 4. 不得再引入「先混流再过滤」;新增正则必须能回答「是门还是刀」。 5. provider reasoning 不进任何对外事件、数据库可见字段、完整原文日志(`consultation-budget.ts:55` 的 80 字截断日志维持现状即可)。 ## 7. 让步顺序 1. 先修 BUG-945(P0,用户可感知的空回答路径),再修 946。 2. BUG-947、948 可与 945 并行(文件不重叠:947 只碰 `rectification-agentic/v9/stream-mapping.ts` + `think-step-gate.ts`,948 只碰 `timing-output-guard.ts` / `consultation-birth-time-mode.ts` / `route.ts` 的 Pass 4 接线)。 3. BUG-949 最后做,它依赖前面几条最终的 thinking plan 形状。 4. 实在做不完,**945 + 946 必须一起进**,否则 staging 仍然发不出镜像。 ## 8. 开工前置 ```bash git fetch origin --prune git worktree add -b codex/consult-three-channels-fix-20260918 \ .worktrees/consult-three-channels-fix-20260918 origin/staging cd .worktrees/consult-three-channels-fix-20260918/frontend npm test 2>&1 | grep -E "^# (tests|pass|fail)" # 开工基线:tests 3484 / pass 3426 / fail 43 ``` 收工要求:`docs/tasks/PROGRESS-consult-three-channels-fix-20260918.md` + `docs/BUG_HISTORY.md`(BUG-945~949)+ `CHANGELOG.md`(若有用户可感知变化),与代码同一批推 `staging`。