# 验收修复单 · 出卡精度门槛 + 引导式补经历(2026-09-16) ## 0. 基线与验收对象 - 验收对象:`cfb41daf`(已合入 `origin/staging`,任务书 `TASK-rectification-precision-gate-guided-collect-20260916.md`)。 - 代码基线:`317e9f18`(cfb41daf 的父提交)。 - staging 部署:`/api/health` 的 `deployment.gitCommit = 317e9f18`,**cfb41daf 未部署**。门禁跑的是 `CORE_PYTEST_TARGETS`,其中 `tests/test_rectification_*.py` 含本单弄红的一条(§2 F1),与未部署的现象一致。 - 分支:`codex/rectification-precision-gate-guided-collect-fix-20260916`,基于最新 `origin/staging`。 - BUG 段:**BUG-744 起**(基线最大 BUG-743)。 ## 1. 验收结论(逐条) 验收环境:本机 Linux,`node_modules` 软链,`next build --webpack`;无 Docker、无登录态、无 Chrome。 | 项 | 结论 | 证据 | | --- | --- | --- | | `tsc --noEmit` | 通过 | 0 错 | | `npm run lint` | 通过 | 0 error / 118 warning(基线同类) | | `npm test` 与基线逐条比对 | **未通过** | 基线 `317e9f18`:3372 条、31 红;`cfb41daf`:3391 条、47 红。**新增 16 红**,全部是校正套件既有断言,一个文件都没改(§2 F2)。基线的 31 红是无 Docker / `[eval]` 路径别名那一组,两边相同 | | `next build` `/` Static | 通过 | `○ /` | | 首屏 gzip | 通过 | 575,108 → 577,616(+0.44%) | | `page.tsx` 不增长 | 勉强 | 1837 → 1838(+1 行 `birthDate` prop) | | Python 定向(4 文件) | 通过 | 56 passed | | Python `tests/test_rectification_*.py` 全量 | **未通过** | `test_rectification_engine_memoization.py::test_score_candidates_matches_baseline_golden` 红:`decision_receipt` 多了 `guided_collect_windows` 键;基线同文件 14 全绿(§2 F1) | | T1 出卡门槛 | 通过(带 F3) | `deliveryMaxWidthMinutes` 只在 policy JSON 定义一处;`decideFromDossier` 与 `decideAfterInferenceChange` 有 state 分支都经 `narrowingExhaustion` 传 `precisionGateMet` / `guidedCollectExhausted`;新单测 8 条绿 | | T2 引导题源 | 通过(带 F4、F5) | `guided_collect_windows` 挂进 packet / receipt;`declined_domains` 进合同校验;不改 `MIN_BOUNDARY_DAYS` | | T3 引导问法与录入 | 通过(带 F6) | 题型顺序、录入卡、选择器年份范围、门槛未达无自由文本邀请,新单测绿;`RANGE_DELIVERY_OPEN_COLLECT_*` 已删净 | | T4 / T4b | 通过 | 跳过重问一次、拒绝不重问、七条线整领域题干、时间点负答案不关领域、七领域遍历断言 | | T5 Skill 10.0.27 | 通过 | registry + versions 目录 + hash;历史会话可开只有合同测试,浏览器级留清单第 6 条 | | T6 | 通过 | BUG-742 已修;BUG-743 `investigating` 且没编根因 | | T7 记录 | 通过 | BUG-740~743、CHANGELOG、DESIGN、VOICE、真机清单 | **总结论:未通过。** 两条门禁级红(F1、F2)必须先修;F3~F6 一并在本单做。 ## 2. 未通过项与修法 ### F1(P0,门禁红)`decision_receipt` 新键让 memoization golden 红 - 实证:`scripts/rectification/decision_policy.py` 把 `guided_collect_windows` 写进 receipt;`tests/test_rectification_engine_memoization.py::_assert_tiered_equal` 对 receipt 做键集合严格相等,左边多出 `guided_collect_windows`。该文件在 `run_quality_gate.py` 的 `CORE_PYTEST_TARGETS`(`tests/test_rectification_*.py`)里。 - 修法:这是**形状变化**,不是 BUG-733 禁止的"重建 golden 掩盖浮点漂移"。按 BUG-733 的口径,同进程差分仍是主证据;golden 只允许**追加 `guided_collect_windows` 一个键**,进度记录里贴 golden 的 diff,必须只有这一处。不得放宽 `_assert_tiered_equal` 的键集合比较。`docs/BUG_HISTORY.md` 的 BUG-733 条目补一句"2026-09-16 因 receipt 新增键重生成,diff 仅此一键"。 - 验收:`.venv/bin/python -m pytest tests/test_rectification_*.py` 全绿;golden diff 只含新键。 ### F2(P0,红线 §7.3)16 条既有前端断言红,进度记录写成"已按三栏改" 新红分布(基线全绿): | 文件 | 条数 | 性质 | | --- | ---: | --- | | `rectification-collect-direction-20260904.test.ts` | 3 | 门槛语义(原"分开即停采集") | | `rectification-adopt-flow-fix-20260903.test.ts` | 2 | 门槛语义 / 题干 | | `rectification-adopt-narration-20260904.test.ts` | 2 | 门槛语义 / 提示句 | | `rectification-targeted-collect-cards-20260913.test.ts` | 2 | 旧题干「结过婚或订过婚吗」、旧邀请 /不限领域/ | | `rectification-answer-choice.test.ts` | 1 | 选项文案 | | `rectification-choice-card.test.ts` | 1 | 题干 | | `rectification-convergence-budget.test.ts` | 1 | helper 路径 stopReason(F3) | | `rectification-decide-next-action.test.ts` | 1 | helper 路径(F3) | | `rectification-delivery-vs-collect-20260914.test.ts` | 1 | 门槛语义(GET 并列不再交付) | | `rectification-superseded-focus.test.ts` | 1 | 七条线全轮到后"最后一问"变了 | | `rectification-tied-first-fix-20260914.test.ts` | 1 | helper 路径吞掉 BUG-654 规则(F3) | - 修法:逐条二选一——属 D1~D5 预期变化的,改断言并写「原值 / 新值 / 原因」三栏;属 F3 的,先修代码再看断言。**不得删测试、不得 skip。** 进度记录列 16 条各自的处置。 - 验收:`npm test` 红数与基线逐条一致(31 条同名),总数 ≥ 3391。 ### F3(P1)`mayDeliverOnPrecision` 缺省即放行,把 BUG-654 规则一起短路 - 实证:`core/rectification-decision.ts` `mayDeliverOnPrecision`:`precisionGateMet` 与 `guidedCollectExhausted` 都缺省时返回 `true`;`decideRectification` 用它把 `stillNeedNarrowing`、`datedMethodCollectOpen` 一并绕过。于是任何不传这两个 flag 的调用(`decideConversationalSession`、`decideAfterInferenceChange` 的无 state 分支、全部既有单测)都失去了"刷新与定向线未穷尽不得交付"(BUG-654 / 656)。`tied-first-fix` 那条红就是这个:`targetedCollectExhausted: false` 也交付了。另外 `guidedCollectExhausted()` 只收 `targetedCollectExhausted`,不含 `refreshExhausted`:引导池空、刷新还没试过,也会直接出卡。 - 修法: 1. flag 缺省 → 门槛**不参与**,`stillNeedNarrowing` / `datedMethodCollectOpen` 按原逻辑生效(helper 路径行为回到 317e9f18)。 2. flag 传入 → 出卡条件 = `userStopped || (precisionGateMet && guidedCollectExhausted && refreshExhausted && targetedCollectExhausted)`;`precisionGateMet` 为假时,即使所有线问完,也只进 `collect`(用户说「没有了」除外,那时按现行规则出卡)。 3. 按 D6,`precisionGateMet` 不短路任何采集线;cfb41daf 里 `stillNeedNarrowing(input) && !mayDeliverOnPrecision(input)` 与 `datedMethodCollectOpen ... && !mayDeliverOnPrecision(input)` 两处改回原判断,只在最终交付分支加门槛。 - 验收:`rectification-tied-first-fix` / `decide-next-action` / `convergence-budget` / `collect-direction` 那几条应大部分恢复绿(原语义"线没问完不出卡"回来了);新增单测「引导池空但 refreshExhausted=false → 不交付」「门槛达标但定向线未问完 → 继续问」「所有线问完但门槛未达 → 继续引导(无题则按 D2 出卡)」。 ### F4(P1)引导窗口的领域是轮询分配的,一个窗口只问一个随机领域 - 实证:`event_probes.py` `guided_collect_windows`:Vimshottari / 那罗延本命轨道的边界与领域无关,代码用 `fallback[unlayered_index % len(fallback)]` 轮流贴一个领域;只有 `nara:d9` / `nara:d10` 两条轨道有固定领域。前端 `windowAlreadyAsked` 以 `(年, 月区间, 领域)` 为键,用户答「这段没有」只关这一组合,但引擎每个边界只产一行,同一窗口不会再以别的领域出现。结果是"2018 年 3 到 5 月有没有换工作"答没有,用户那段时间的搬家或恋爱永远问不到。 - 修法(表驱动,七条线一样):无领域轨道的窗口题不指定单一领域,题干「YYYY 年 M 到 M 月之间,有没有什么事,比如<开放领域口语 2~3 个>?」,`domain` 字段留 `null` 或 `"any"`;A 之后录入卡的类型芯片由用户选;`d9` / `d10` 轨道保持固定领域。`windowAlreadyAsked` 键去掉领域。`declined_domains` 只用于剔除例子。 - 验收:Python 单测「无领域轨道窗口 `domain` 为 any」;前端单测「any 窗口题干列 ≤3 个开放领域口语、A 后芯片无默认或默认第一开放领域」。 ### F5(P1,研究口径)离线回放没按任务书注入真值方向事件,"0/20"不能作数 - 实证:`scripts/research/guided_collect_holdout_replay.py` `synthetic_event` 一律取 `month_lo`,与真值候选的边界月无关;领域用轮询值。任务书要求"注入真值方向的带年月事件"。所以进度记录里"没有一例靠引导件达标"只说明合成事件不在真值边界上,不说明引导题源无效。 - 修法:对每个窗口,取 `starts_by_time[true_time]` 同一轨道、同一 index 的日期作为事件月(真值方向);再跑一组"反方向"(取离真值最远的候选)作对照;三档半径都跑。报「达标件数中位」「达标例数」「反方向达标例数」。仍不是合入门槛。 - 验收:`docs/research/guided_collect_holdout_2026_09_16.json` 更新并附口径说明;进度记录改数字。 ### F6(P2)录入卡提交的是选项列表拼成的句子,走模型轮落库 - 实证:`event-date-entry-card.tsx` `formatEventDateEntryMessage` 生成「2019 年 3 月,开始认真关系、分手或结婚」发给 `send("message")`,经分类器与模型再写账本。账本 summary 是三选一列表,不是用户说的事;每次录入多一轮模型调用与延迟。 - 修法:文本改为「YYYY 年 M 月(D 日),<领域标签>方面有一件事」;或直接走 `set-focus` / `batch` 的结构化写入不经模型。账本 summary 不得含「或」列表。 - 验收:单测断言提交文本;账本 summary 断言。 ### F7(P3,记录即可) - `page.tsx` 1837 → 1838:`birthDate` 可从 `conversational-birth-time-rectification.tsx` 内部取档案,把那一行挪出去。 - `tests/test_event_probes_guided_windows.py` 用手造 static contexts(沿用 `test_rectification_event_probes.py` 既有模式);`test_declined_domains_are_removed` 在空窗口时 `skipTest`。至少一条用真实引擎 golden 的 20 分钟窗口断言窗口非空、不许 skip。 ## 3. 决策记录 - D1~D5 沿用主单。 - **D6(产品负责人 2026-09-16 拍板:问完再出)**:`precisionGateMet` 只是出卡的**必要条件**,不是充分条件。交付仍要等自动题、引导窗口题、跳过线重问、未覆盖领域题全部问完(或用户说「没有了」)。也就是说 cfb41daf 里"门槛达标就短路剩余采集线"的写法要撤回:`stillNeedNarrowing` / `datedMethodCollectOpen` 不再被 `mayDeliverOnPrecision` 绕过。Skill §流程保留"所有线问完或用户说没有了后才交付"原句,只在其前加"且目前范围 ≤10 分钟、头名不并列"。 ## 4. 硬红线 主单 §4 全部沿用。另加:不得放宽 `_assert_tiered_equal`;不得删或 skip 任何既有测试;F4 不得按领域写 if。 ## 5. 开工前置 ```bash git fetch origin --prune && git status -sb git worktree add -b codex/rectification-precision-gate-guided-collect-fix-20260916 \ .worktrees/rectification-precision-gate-guided-collect-fix-20260916 origin/staging grep -o "^## BUG-[0-9]*" docs/BUG_HISTORY.md | sort -t- -k2 -n | tail -1 # 应为 BUG-743 cd frontend && npm test 2>&1 | grep -E "^# (tests|pass|fail)" # 记基线 3391 / 47 cd .. && .venv/bin/python -m pytest tests/test_rectification_*.py -q | tail -3 ``` Bug 检索:BUG-733、BUG-654、BUG-656、BUG-740~743。 ## 6. 验收口径 - F1、F2 是合入门槛:Python `tests/test_rectification_*.py` 全绿;`npm test` 红清单与基线逐条一致。 - F3~F6 各带单测;F5 只看数字与口径。 - 合入后 `/api/health` 的 `gitCommit` 必须等于修复提交;仍是 `317e9f18` 视为未部署。 - 真机清单 `docs/testing/rectification-guided-collect-20260916.md` 六条留给产品负责人。