Files
Jyotisha/docs/tasks/TASK-consult-three-channels-fix-20260918.md
T
Jesse_ChenandClaude Opus 5 b4fcb9466f docs(tasks): BUG-948 改为按模式分开处置,钉住出生范围用户可用
产品 09-18 授权:范围用户必须能正常用产品。exact-timing 只记录不
拦(日期照出、按区间表述),guarantee 全模式退回重写,personal-chart
只在完全没有出生时间时拦。推翻「分钟敏感主题一律套精确时间守卫」的
旧红线(该红线曾让已校正用户问应期被挖成「[具体时间已省略]」)。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0193vBv6w5MV2cifdTUu9H5P
2026-09-18 04:36:32 +00:00

168 lines
13 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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`。