产品 09-18 授权:范围用户必须能正常用产品。exact-timing 只记录不 拦(日期照出、按区间表述),guarantee 全模式退回重写,personal-chart 只在完全没有出生时间时拦。推翻「分钟敏感主题一律套精确时间守卫」的 旧红线(该红线曾让已校正用户问应期被挖成「[具体时间已省略]」)。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0193vBv6w5MV2cifdTUu9H5P
168 lines
13 KiB
Markdown
168 lines
13 KiB
Markdown
# 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`。
|