fix(rectification): 有题就接着问,题问完才出卡;引导窗口不再硬贴领域

出卡时机只看题源有没有空:撤回「门槛达标就短路采集线」的写法,同时
按 D2 保住「题源全空就按现行规则出卡」——门槛只在还有题可问时挡住
出卡,precision_gate_met 改成只上报(新挂在决策与公开投影上),不再
单独决定时机。引导窗口题在无领域轨道上改问开放题,一个时间窗只问一
次;录入卡提交的是「YYYY 年 M 月,<领域>方面有一件事」,不再是题干
的三选一列表。记忆化 golden 只补一个新键并冻结墙钟。离线回放改成注
入真值方向的边界事件,另跑一组反方向对照。Skill 10.0.28。

BUG-747~752

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JUei7K13cYxLHE3Axe4A45
This commit is contained in:
Jesse_Chen
2026-09-16 12:16:55 +00:00
co-authored by Claude Fable 5.1
parent a396bbe076
commit dc732825d8
61 changed files with 6251 additions and 2053 deletions
+98
View File
@@ -11424,6 +11424,7 @@
- 根因:golden 存了全精度浮点并整体相等。这是 BUG-712 的同一形状。BUG-712 的防复发只落在 `ephemeris_events` 那一处(量化到 6 位再比),没有仓库级守卫,BUG-721 新写的 rectification golden 重蹈覆辙。
- 修复:只改测试,不动 `scripts/`。主证据改成同进程差分:同一批 static contextsA 带四层缓存、B 把 `ashtakavarga_result` / `shadbala_result` / `vimshottari_timeline` / `narayana_periods``None``compute_event_candidate_rows` 输出严格相等。golden 改成分档比较:离散字段严格相等;浮点用实测最大漂移 1.1e-3 推出的容差(绝对 2e-3 与相对 5e-4 取更宽者)。禁止调用 `write_golden()` 更新那份 JSON。
- 验证:同进程 A/B 逐字相等,且 B 的 `calc_shadbala` 次数显著高于 A(本机 6 vs 0)。反向:把某一层缓存换成全零 shadbala 对象后不再相等。golden 分档比较通过;把 golden 浮点改 1e-2 必须红;把离散字段 `time` 改掉必须红。`pytest tests/test_rectification_engine_memoization.py``pytest tests/test_rectification_*.py` 0 failed。golden JSON `git diff` 无改动。
- 2026-09-16`decision_receipt` 新增 `guided_collect_windows` 后重生成 golden,diff 仅此一键(62 行),未放宽 `_assert_tiered_equal`、未调用 `write_golden()`;同时把 `_golden_payload()``date.today()` 冻结,见 BUG-747。
- 防复发:任何 golden 比较都不得对浮点做整体 `==`;浮点必须量化或带容差,容差数值要有实测依据并写在注释里;能在同进程内做差分证明的命题,不得用跨机 golden 代替。
- 相关记录:BUG-712、BUG-721
- 复发自:BUG-712
@@ -11664,3 +11665,100 @@
- 相关记录:BUG-745
- 复发自:无
- 修复版本:待发布
## BUG-747 | 校正 receipt 新增 `guided_collect_windows` 键,记忆化 golden 门禁红
- 状态:resolved
- 首次发现:2026-09-16
- 最近更新:2026-09-16
- 影响面:`scripts/rectification/decision_policy.py``tests/test_rectification_engine_memoization.py``tests/golden/rectification_engine_memoization_v1.json`
- 用户现象:用户看不见。`run_quality_gate.py``CORE_PYTEST_TARGETS``tests/test_rectification_*.py``test_score_candidates_matches_baseline_golden` 红,门禁不放行、镜像不发布,staging 停在上一个 SHA。
- 触发条件:`build_decision_receipt` 往 receipt 里加了 `guided_collect_windows`golden 里没有这个键;`_assert_tiered_equal` 对 receipt 做键集合严格相等。
- 根因:形状变更没有同步 golden。BUG-733 的分档比较与「禁止 `write_golden()`」是对**浮点漂移**的防线,它按设计拦住了这次的键集合差异——拦截生效了,是上一轮执行方没有跑这个文件(进度记录里 Python 只跑了四个定向文件,没跑 `tests/test_rectification_*.py` 全量)。
- 修复:golden 只追加 `guided_collect_windows` 一个键(`git diff` 只有这一处、62 行),不放宽 `_assert_tiered_equal`、不调用 `write_golden()`。另外把 `_golden_payload()` 里的 `date.today()` 冻结到 `FROZEN_TODAY`:窗口年份取自墙钟,不冻结的话这份 golden 会在跨年时自己变红(BUG-733 同一族的隐患)。
- 验证:`.venv/bin/python -m pytest tests/test_rectification_*.py tests/test_event_probes_guided_windows.py -q` → 193 passedgolden 的 `git diff --stat``1 file changed, 62 insertions(+)`
- 防复发:改 `decision_receipt` 形状的轮次必须跑 `tests/test_rectification_*.py` 全量,不能只跑定向文件;golden 的任何字段都不得依赖墙钟。
- 相关记录:BUG-733、BUG-712、BUG-721、BUG-740
- 复发自:BUG-733(同一份 golden;旧防线拦住了,未被执行)
- 修复版本:待发布
## BUG-748 | 16 条既有校正断言变红却被写成「已按三栏改」
- 状态:resolved
- 首次发现:2026-09-16
- 最近更新:2026-09-16
- 影响面:`frontend/tests/rectification-*.test.ts`11 个文件)、`docs/tasks/PROGRESS-rectification-precision-gate-guided-collect-20260916.md`
- 用户现象:用户看不见。`npm test` 从基线 3372 条 / 31 红变成 3391 条 / 47 红,新增的 16 红全在校正套件,没有一个文件被改过。
- 触发条件:上一轮只跑了本单新增与相关的定向套件,全量 `npm test` 因本机无 Docker / 并行 OOM 被判定为「不能当回归清单」,于是没有与基线逐条比对。
- 根因:AGENTS §7.3「测试总数不得低于开工时实测、改断言必须写三栏」被当成只约束**主动修改**的断言;被代码变更**打红**的既有断言没有走同一条流程。无 Docker 只影响 31 条固定清单,其余部分照样可比。
- 修复:本轮把 16 条逐条处置(外加 D5/D6 连锁打红的另外 25 条,见 BUG-751),每条在测试里写「原值 / 新值 / 原因」三栏;不删测试、不 skip。
- 验证:`npm test` → 3396 条 / 31 红,红清单与基线 31 条逐条同名(无 Docker 与 `[eval]` 路径别名那一组)。
- 防复发:无 Docker 时的正确做法是「与基线逐条比对失败清单」,不是放弃全量;进度记录里不得把没跑过的套件写成已处置。
- 相关记录:BUG-751、BUG-740
- 复发自:无
- 修复版本:待发布
## BUG-749 | 引导窗口的领域是轮询贴上去的,一个窗口只问一个随机领域
- 状态:resolved
- 首次发现:2026-09-16
- 最近更新:2026-09-16
- 影响面:`scripts/rectification/event_probes.py` `guided_collect_windows``collection-question-pool.ts` `guidedWindowPrompt` / `guidedWindowPool` / `windowAlreadyAsked``event-date-entry-card.tsx`
- 用户现象:「2018 年 3 到 5 月之间,有没有入职、换工作或职责变重?」答「这段没有」之后,这个时间窗再也不会出现——用户那段时间的搬家或恋爱永远问不到。
- 触发条件:边界来自 Vimshottari 或那罗延本命轨道(与领域无关),引擎按 `fallback[unlayered_index % len(fallback)]` 轮流贴一个领域;前端 `windowAlreadyAsked` 以(年, 月区间, 领域)为键,而同一窗口只会以那一个领域出现一次。
- 根因:把「哪一段时间能切开候选」和「这段时间发生了哪一类事」混成一个字段。只有 `nara:d9` / `nara:d10` 两条轨道真的带领域。
- 修复:无领域轨道的窗口 `domain = "any"`,题干写「YYYY 年 M 到 M 月之间,有没有什么事,比如<开放领域口语 2~3 个>?」;口语从 `KIND_ORAL` 表取,剔除已拒答与已覆盖的领域,决策层不按领域写 `if``windowAlreadyAsked` 的键去掉领域,只看年 + 月区间。A 之后录入卡的芯片默认第一个仍开放的领域。`nara:d9` / `nara:d10` 保持固定领域;该领域被拒答时也退回 `any`
- 验证:`tests/test_event_probes_guided_windows.py`(真实引擎 20 分钟窗 golden,7 passed)断言无领域轨道窗口是 `any`、d9/d10 保留固定领域、拒答领域退回 `any``frontend/tests/rectification-guided-collect-20260916.test.ts` 断言开放题干的口语列表 ≤3 条、同一窗口换领域标签也不重问、芯片默认第一开放领域。
- 防复发:窗口的 `domain` 只能来自真正带领域的轨道;一个边界窗口是一个问题,已问判定不得把领域并进键里。
- 相关记录:BUG-740、BUG-741、BUG-751
- 复发自:无
- 修复版本:待发布
## BUG-750 | 录入卡提交的是选项列表拼成的句子,账本 summary 变成三选一
- 状态:resolved
- 首次发现:2026-09-16
- 最近更新:2026-09-16
- 影响面:`frontend/src/components/event-date-entry-card.tsx` `formatEventDateEntryMessage`
- 用户现象:用类型芯片 + 年/月选择器提交一件事后,账本里记的是「2019 年 3 月,开始认真关系、分手或结婚」——一句三选一列表,而不是用户选的那一类。
- 触发条件:引导窗口题答「有,我来填时间」后从录入卡提交。
- 根因:提交文本直接拼 `KIND_ORAL[domain]`,而 `KIND_ORAL` 是**题干用的例子列表**,不是事件描述。
- 修复:改成「YYYY 年 M 月(D 日),<领域标签>方面有一件事」,标签取 `RANGE_DELIVERY_DOMAIN_LABEL`;文本里不再出现「或」。
- 验证:`frontend/tests/rectification-guided-collect-20260916.test.ts` 断言提交文本与「不含『或』」。
- 防复发:任何写进账本的用户文本都不得复用题干的例子列表。
- 相关记录:BUG-740、BUG-749
- 复发自:无
- 修复版本:待发布
## BUG-751 | 精度门槛被当成充分条件,把「线没问完不出卡」一起短路
- 状态:resolved
- 首次发现:2026-09-16
- 最近更新:2026-09-16
- 影响面:`core/rectification-decision.ts` `mayDeliverOnPrecision` / `decideRectification``v9/decision-from-dossier.ts` `narrowingExhaustion`、Skill §流程
- 用户现象:门槛达标时,刷新与定向补事都还没问完就直接出交付卡;反过来,任何没有传这两个 flag 的调用(`decideConversationalSession``decideAfterInferenceChange` 的无 state 分支)因为「缺省即放行」而完全绕开了 BUG-654 / BUG-656 的「刷新与定向线未穷尽不得交付」。另外 `guidedCollectExhausted()` 不含 `refreshExhausted`:引导池空、刷新还没试过,也会出卡。
- 触发条件:`precisionGateMet``guidedCollectExhausted` 任一为真(或两者都缺省)。
- 根因:`mayDeliverOnPrecision` 写成了「或」,并且被塞进 `stillNeedNarrowing``datedMethodCollectOpen` 两条采集分支做短路,等于用门槛去撤销采集条件。
- 修复(产品 2026-09-16 决策 D2 + D6):门槛只在**还有题可问时**挡住出卡。flag 全部缺省时门槛不参与,采集分支回到 `317e9f18` 的判断;flag 传入时交付条件 = `userStopped || (guidedCollectExhausted && refreshExhausted !== false && targetedCollectExhausted !== false)`——即 `userStopped || 所有题源都问完`。题源都空了就是 D2 说的「用户确实补不出来」,卡片、采用按钮、三句正文照现行规则给,**不加「未达门槛」标注**。撤回两处 `!mayDeliverOnPrecision` 短路,门槛只加在最终交付分支。`narrowingExhaustion` 计算的 `guidedCollectExhausted` 现在含 `refreshExhausted``precisionGateMet` 保留计算,并新挂到 `RectificationDecision.precisionGateMet``publicNextAction().precision_gate_met` 上报,只是不再单独决定时机。Skill §流程改成「所有线(含引导窗口题、跳过线重问、未覆盖领域题)问完后,或用户说没有了后,才交付目前范围」,bump 到 10.0.28。
- **口径提醒**:按 D2 + D6,10 分钟门槛**不改变出卡时机**,本轮实际生效的是题源变多(引导窗口题 + 跳过线重问 + 七条线全部轮到)、线问完才出。
- 验证:`frontend/tests/rectification-precision-gate-20260916.test.ts` 新增五条——引导池空但 `refreshExhausted=false` 不交付、门槛达标但定向线未问完继续问、所有线问完但门槛未达**照现行规则出卡(含采用)**、门槛值在采集出口也照常上报、helper 路径上报 null`rectification-decision-authority.test.ts` 的公开字段深比较跟上 `precision_gate_met`。撤回短路后连锁打红的 41 条既有断言(16 条来自 BUG-748 的基线红 + 25 条本轮新红)全部按「原值 / 新值 / 原因」三栏处置,均为 D5「七条线全部轮到」与 D6「问完再出」的预期变化。
- 防复发:门槛不得出现在任何采集分支的条件里,也不得成为「题源已空仍不出卡」的理由(那会造出没有题也没有卡的死角,违反 BUG-652);缺省 flag 只能表示「门槛不参与」,不能表示「放行」。
- 相关记录:BUG-654、BUG-656、BUG-740、BUG-748、BUG-749
- 复发自:BUG-654(刷新与定向线未穷尽不得交付,被门槛短路绕开)
- 修复版本:待发布
## BUG-752 | 离线回放注入的事件与真值边界无关,「0/20」不能当结论
- 状态:resolved
- 首次发现:2026-09-16
- 最近更新:2026-09-16
- 影响面:`scripts/research/guided_collect_holdout_replay.py``docs/research/guided_collect_holdout_2026_09_16.json`
- 用户现象:用户看不见。研究结论写成「没有一例靠引导件达标」,被当作引导题源无效的证据。
- 触发条件:`synthetic_event` 一律取 `window["month_lo"]` 作为事件月,领域取轮询值,与真值候选在该窗口上的边界日期无关。
- 根因:任务书要求注入「真值方向」的带年月事件,脚本注入的是窗口左端点。落在窗口里但不在真值候选的边界上,对打分几乎没有方向性,所以量到的是合成事件的噪声,不是题源的信息量。
- 修复:按窗口的(轨道, index)取回每个候选自己的边界日期,`truth` 组注入真值候选的那一天所在月,另跑一组 `opposite`(离真值最远的候选的同一边界)做对照;三档半径都跑。汇总新增 `met_after_six_only` / `met_via_guided` / `median_events_via_guided`,把「六题后本来就达标」和「靠引导件补上」分开数。
- 验证:见 `docs/tasks/PROGRESS-rectification-precision-gate-guided-collect-fix-20260916.md` 的数字表与 `docs/research/guided_collect_holdout_2026_09_16.json`。这不是合入门槛。
- 防复发:离线回放的合成证据必须能说清「注入在谁的边界上」;只报总达标数、不分「本来就达标 / 靠注入达标」的数字不得写进结论。
- 相关记录:BUG-740、BUG-749
- 复发自:无
- 修复版本:待发布
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,187 @@
# 进度 · 出卡精度门槛 + 引导式补经历(验收修复单,2026-09-16)
- 任务书:`docs/tasks/TASK-rectification-precision-gate-guided-collect-fix-20260916.md`F1F7 + 决策 D6
- 基线:`origin/staging` @ **`a396bbe0`**(第二轮已 rebase;上一版基线 `302ff085`,代码基线 `cfb41daf`
- 分支 / 工作树:`codex/rectification-precision-gate-guided-collect-fix-20260916` / `.worktrees/rectification-precision-gate-guided-collect-fix-20260916`
- BUG**747752**。开工时最大号是 743;实现期间另一会话把侧栏统一那一单合入 staging,占掉了 744~746,所以本单整体后移到 747 起。rebase 后 `docs/BUG_HISTORY.md` 里 743 → 744 → … → 752 连续。
- 开工检索(AGENTS §5):BUG-733、654、656、740743 已逐条读完;BUG-733 的防复发条本轮补了一句(见下)。
## 0. rebase 到 `a396bbe0`
第二轮已 `git rebase origin/staging`(侧栏统一那 4 笔:`d9d34723``fb77c865``22edabbc``a396bbe0`)。
- **自动合并**`frontend/DESIGN.md``frontend/src/app/page.tsx`。page.tsx 以对方的结构为准,只保留本单那一处 `rectificationDeclaration` 合并(`declaredTime` + `birthDate` 一个对象展开)。
- **手工解冲突 3 个**,都在文档、都是「两侧各自追加」:
- `CHANGELOG.md`:两条都保留,本单那条在上(后写的在前)。
- `docs/BUG_HISTORY.md`:以 staging 版本为底,重新插入本单对 BUG-733 的那一行补注,再把本单的 BUG-747~752 整块追加到 BUG-746 之后。结果 `## BUG-` 共 739 条,末尾 743 → 744 → … → 752 连续。
- `docs/tasks/README.md`:侧栏单那一行取对方的(已验收状态),本单那一行取自己的;两行都在。
- BUG 编号 **747752 不变**744746 已被侧栏轮占用)。
## 0.1 结论先行(全部在 `a396bbe0` 上重测;基线也在 `a396bbe0` 上用一次性 worktree 重跑)
| 项 | 基线 `a396bbe0` | 本分支 |
| --- | --- | --- |
| `tsc --noEmit` | 0 错 | **0 错** |
| `npm run lint` | 0 error / 115 warning | **0 error / 115 warning** |
| `npm test` | 3399 条 / **47 红** | **3405 条 / 31 红**;31 红与基线逐条同名,**0 条新红**,基线 47 红里的 16 条全部转绿 |
| `pytest tests/test_rectification_*.py tests/test_event_probes_guided_windows.py` | — | **193 passed** |
| `next build --webpack` | `○ /` | **`○ /`**`/chart``/ephemeris` 也仍是 Static |
| 首屏 gzip | 583,765 | **582,800**(−965 B**0.17 %** |
| `frontend/src/app/page.tsx` | 1836 行 | **1835 行**(−1,未增长) |
| Skill | 10.0.27 | **10.0.28**sha256 `f0bb8295…cffe` |
## 1. F1 · 记忆化 goldenBUG-747
- `tests/golden/rectification_engine_memoization_v1.json` 只追加 `guided_collect_windows` 一个键。`git diff --stat` = **`1 file changed, 62 insertions(+)`**`git diff` 里只有一段 `+ "guided_collect_windows": [ … ]`,插在 `discriminating_event_probes` 之后(与 live receipt 的键序一致),CRLF 行尾保持不变。**没有删除或修改任何既有行。**
- 未放宽 `_assert_tiered_equal`(键集合仍是严格相等),未调用 `write_golden()`
- 顺带修了一个同族隐患:`_golden_payload()` 现在把 `scripts.rectification.event_probes``date` 换成 `_FrozenDate``today() == FROZEN_TODAY = 2026-09-16`)。窗口年份来自 `date.today()`,不冻结的话这份 golden 会在跨年时自己变红。冻结后生成的值与本机实跑一致,所以 golden 内容没有因此改变。
- `docs/BUG_HISTORY.md` 的 BUG-733 条目补了一行「2026-09-16 因 receipt 新增键重生成,diff 仅此一键」。
## 2. F2 + F3 · 断言处置清单(BUG-748 / BUG-751
先按 F3 改代码,再逐条看断言。**没有删任何测试,没有 skip 任何测试。**
### 2.1 代码改动(F3 + D6
`core/rectification-decision.ts`
```
mayDeliverOnPrecision(input):
userStopped === true → true
precisionGateMet 与 guidedCollectExhausted 都缺省 → true(门槛不参与,helper 路径回到 317e9f18
否则 guidedCollectExhausted === true
&& refreshExhausted !== false
&& targetedCollectExhausted !== false = 所有题源都问完)
```
**口径(D2 + D6,第二轮修正)**:门槛只在**还有题可问时**挡住出卡。题源全空就是 D2 说的「用户确实补不出来」,卡片、采用按钮、三句正文照现行规则给,不加「未达门槛」标注。
写全是 `userStopped || allExhausted || (precisionGateMet && allExhausted)`,化简后就是 `userStopped || allExhausted`
**按 D2 + D6,10 分钟门槛不改变出卡时机,本轮实际生效的是题源变多(引导窗口题 + 跳过线重问 + 七条线全部轮到)、线问完才出。**
`precisionGateMet` 保留计算,并新挂到 `RectificationDecision.precisionGateMet``publicNextAction().precision_gate_met`helper 路径不传 flag 时为 `null`),只上报、不决定时机。
撤回的三处短路(全部回到原判断):
1. `coverageBlocks` 分支的 `const narrowingOpen = stillNeedNarrowing(input) && !mayDeliverOnPrecision(input)`
2. `!separation.sufficient` 分支的 `if (stillNeedNarrowing(input) && !mayDeliverOnPrecision(input))`
3. `if (input.datedMethodCollectOpen === true && !input.userStopped && !mayDeliverOnPrecision(input))`
保留门槛的五个最终交付分支:`tied_first` 耗尽、`!sufficient` 的 exhausted / adopt_representative / offer、顶层 exhausted。
`v9/decision-from-dossier.ts``narrowingExhaustion``guidedCollectExhausted` 改为 `refreshDone && guidedCollectExhausted(...)`,即引导池空但刷新没试过不算问完。
新增三条单测(`rectification-precision-gate-20260916.test.ts`):
| 单测 | 断言 |
| --- | --- |
| `empty guided pool with refresh untried does not deliver` | `refreshExhausted: false` + 池空 + 门槛达标 → `ask_fact_collection``canOfferRange=false``mayDeliverOnPrecision` 直接返回 false |
| `gate met but the targeted line is still open keeps asking` | `precisionGateMet: true` + `targetedCollectExhausted: false``ask_fact_collection` |
| `every line asked but the gate unmet still delivers under the existing rules` | 池空 + 门槛未达 → `canOfferRange=true``sessionOutcomeAllowsDelivery=true`,且 `precisionGateMet` 仍报 `false`(D2:题源空了就按现行规则出卡,不加标注) |
| `the precision gate is reported on a collect exit too` | 采集出口上 `decision.precisionGateMet === false``publicNextAction().precision_gate_met === false`helper 路径(不传 flag)为 `null` |
### 2.2 十六条基线红(任务书 §2 F2 表)
| # | 文件 :: 用例 | 处置 |
| --- | --- | --- |
| 1 | `adopt-flow-fix-20260903` :: collect-phase nonterminal exit… | **改断言(三栏)**`adopt_representative/offer_provisional_range/publicCanAdopt=true``collect_evidence/ask_fact_collection/false`;随之 `repaired.persisted` false→true、repair 警告 false→true、`store.focus` null→非空。D113 分钟、14:13:13 并列)+ D6,本用例回到 BUG-648 之前的采集出口,与题名一致 |
| 2 | `adopt-flow-fix-20260903` :: skipped opening collect other still yields education… | **改断言(三栏)**`plan.next_followup = null` → 学业线 `collect:targeted:education`;下半段 `educationPlan.next_followup.domain` `horary``education`。D5 七条线全部轮到,题名要的「yields education」正是这一条 |
| 3 | `adopt-narration-20260904` :: fourteen-probe case decides offer_provisional_range… | **仅改代码即恢复绿**(F3 撤回短路后 helper 路径回到原语义),断言未动 |
| 4 | `adopt-narration-20260904` :: family collect declined vs extra distinguish declined… | **改断言(三栏)**`/搬家|换城市|还能把剩下的候选分开/` → 两侧都匹配 `/现在还剩 .+ 里 \d+ 个候选/`。D5 之后迁居已覆盖,剩下的线是财务与健康;题名要的「两侧同一个 adopt 决策、都落库」不变 |
| 5 | `answer-choice` :: answering the last discriminator persists a year-locked family collect focus… | **改断言(三栏)**`setFocus === undefined` → 写入 `collect:targeted:education`;末条「没有 focus 带 `p_asked_turn_id === TURN_ID`」改成「写入的 focus 只能是这道新问题」——`linkFocusAskedTurn` 把新问题挂到本轮刚追加的 assistant turn,而夹具让这个 id 恰好等于 `TURN_ID`,旧断言在这个夹具里已无法区分两者 |
| 6 | `choice-card` :: family coverage without a renderable discriminator still collects the next dated domain | **改断言(三栏)**`plan.next_followup = null` → 迁居线 `collect:targeted:relocation`。题名要的「still collects the next dated domain」正是这一条 |
| 7 | `collect-direction-20260904` :: holdout remaining domain uses the server collect stem… | **改断言(三栏)**`choice_frame === null``choice_kind === "existence"``USER_COLLECT_QUESTION.education``TARGETED_EXISTENCE_PROMPT.education`,另加一条「不得是反向核对口吻」的断言,保住题名的原意 |
| 8 | `collect-direction-20260904` :: four scoreable events stop dated collect… | **改断言(三栏)**`intent != collect_method_evidence``== collect_method_evidence``collection_key` 必须是 `collect:(targeted|guided):`(走定向七条线,不是旧的领域轮盘) |
| 9 | `collect-direction-20260904` :: separated candidates stop leftover dated collect once the training gate is open | **改断言(三栏)**`sessionOutcome != collect_evidence``== collect_evidence``nextAction = ask_fact_collection``userAction.id = ask_method_followup` |
| 10 | `convergence-budget` :: dossier and post-inference decisions share the half-uncertain stop rule | **改断言(三栏)**`stopReason = "probe_pool_exhausted"``null`,并显式断言两条路径的 `stopReason``nextAction` 一致(题名要的「share the rule」) |
| 11 | `decide-next-action` :: separated candidates stay in collect while dated method collect is still open | **仅改代码即恢复绿**,断言未动 |
| 12 | `delivery-vs-collect-20260914` :: POST idle persist and GET decideFromDossier agree on a first-place tie | **改断言(三栏)**`getDelivers = true``false`(D1 并列时门槛恒假);「POST 与 GET 同口径」那条不变 |
| 13 | `superseded-focus` :: duplicate_focus on the last collect ask does not emit delivery copy | **改断言(三栏)**`p_target_domain != relationship` → 改断言写入的是 `collect:(targeted|guided):` 问题(夹具本身叫 `relationshipOpenDossier`,这条线是开着的);`!= finance` 与「不得播交付文案」保留 |
| 14 | `targeted-collect-cards-20260913` :: A resolves existence then asks the year; B/C close only that line | **改断言(三栏)**:题干 `结过婚或订过婚吗?``TARGETED_EXISTENCE_PROMPT.relationship`T4b 问整个领域);`targetedCollectExhausted` 从「拒答 4 条即耗尽」改成「4 条 → false、七条 → true」 |
| 15 | `targeted-collect-cards-20260913` :: personality D9/D10 stay out of the scoring pool… | **改断言(三栏)**`narrow_hint``不限领域` → 不得含 `不限领域`,改为匹配引导句 `现在还剩 … 里 N 个候选`D3 删自由文本邀请) |
| 16 | `tied-first-fix-20260914` :: tied first place with no probe still collects while targeted questions remain | **仅改代码即恢复绿**,断言未动(这正是 F3 说的 BUG-654 规则回来了) |
### 2.3 D5/D6 连锁打红的另外 25 条(任务书未列,本轮一并处置)
撤回 `datedMethodCollectOpen` / `stillNeedNarrowing` 两处短路之后,cfb41daf 的 T4b`remainingTargetedDomains` 不再按分盘层剔除,七个 `CollectKind` 全部轮到)就重新挡住了交付——这是 D5 + D6 的**预期**结果,但 cfb41daf 用短路把它盖住了,所以这 25 条在基线上是绿的。全部按「原值 / 新值 / 原因」三栏改:
| 文件 | 条数 | 处置摘要 |
| --- | ---: | --- |
| `coverage-collect` | 4 | `offer_provisional_range` / `ready_to_adopt``ask_fact_collection` + `collect_evidence``canAdopt` 仍是引擎能力位,保持 true |
| `provisional-adopt` | 6 | 公开投影 `can_adopt` / `session_outcome` 翻到采集侧;`canRenderRectificationSelectionCards` true→false`buildNextUserAction` `adopt_representative``ask_method_followup``selectionAllowed` true→false |
| `range-offer-deadend` | 2 | `ready_to_adopt``ask_fact_collection``decision.canAdopt` true→false(零拆分探针被 drop 的断言保留) |
| `eight-method` | 1 | `rectification-offer-candidates` 由「可走」改为 `assert.rejects(/offer_not_allowed/)` |
| `collect-direction-20260904` | 1 | 同上(offer-candidates 仍被挡) |
| `exhaustion-exit-20260906` | 1 | `adopt_representative``collect_evidence``stopReason`→null、旁白由三句交付改为引导句 |
| `occupation-coverage-exit` | 1 | `canOfferRange` true→false、`collect_evidence` / `ask_fact_collection` |
| `holdout-renderable` | 1 | `offer/ready``ask_fact_collection``validated_range` 断言保留 |
| `collect-stall` | 1 | 只改夹具:补齐 education / career / relationship 三条拒答(D5 之后七条线都要轮到),断言全部保持原样(`publicCanAdopt=true``persisted=false`、不写 focus |
| `adopt-flow-fix-20260903` | 1 | `adopt_representative nonterminal exit…`:第二轮修正口径后无需改动,夹具与断言均恢复原样 |
| `answer-choice` | 1 | `last structured choice…``ready_to_adopt` / `can_adopt=true``ask_fact_collection` / false(这条的原因是 D5 的线没问完,不是门槛) |
| `decision-authority` | 1 | 公开字段深比较补 `precision_gate_met`(BUG-692 要求不得改成部分匹配,所以补字段而不是放宽比较) |
| `probe-pool-exhausted-20260911` | 1 | T3 第二轮修正后恢复原样;T4 只把末条旁白正则补上引导句(`persistNextInterviewIfIdle` 自己重算决策,写的是采集侧区间旁白) |
| `open-collect-invite-20260914` | 0 | 第二轮修正口径后恢复原样,无改动 |
| `surface-contract` | 1 | `declaredTime={rectificationDeclaredTime}``{...rectificationDeclaration}`F7 |
| `ingest-p0` / `delivery-report-facts` / `tie-break-entry-20260913` / `v9-contracts` | 4 | Skill 版本 10.0.27 → 10.0.28 |
另有 10 个文件只是 `RECTIFICATION_SKILL_VERSION` 常量断言随 bump 改数字(`collect-stall``confirmation-gate``delivery-report-facts``eight-method``exhaustion-exit``ingest-p0``occupation-coverage-exit``range-offer-deadend``replay-20260911``spoken-collect``v9-agent``v9-contracts``v9-entry-routing``window-cluster-cap``yearless-probe-downgrade``skill-registry`),每处都带一行三栏注释。
### 2.4 最终红清单
`npm test` fail=31,与基线 31 条**逐条同名**:无 Docker 的 database / deployment / staging 套件,以及 `[eval]` 路径别名那一组。
测试总数 3399 → **3405**(+6,全部是新增;**没有删除、没有 skip**):精度门槛 5 条(引导池空但刷新未试不交付、门槛达标但定向线未问完继续问、题源全空即使门槛未达也照现行规则出卡、采集出口也上报门槛、helper 路径上报 null+ F4 的 3 条 + F6 的 1 条,其中 2 条是把既有用例改写后新增的姊妹条。
## 3. F4 · 无领域轨道的窗口改成开放题(BUG-749)
- `scripts/rectification/event_probes.py`:删掉 `fallback[unlayered_index % len(fallback)]` 轮询;`vim` / `nara` 轨道的窗口 `domain = GUIDED_ANY_DOMAIN = "any"``nara:d9` / `nara:d10` 保持 `relationship` / `career`,该领域被拒答时也退回 `"any"``declined_domains` 现在只用于剔除例子与 d9/d10 的固定领域。
- 前端:`GUIDED_ANY_DOMAIN` / `GuidedWindowDomain` / `normalizeGuidedWindowDomain` / `guidedAnyExamples` / `firstOpenCollectKind``guidedWindowPrompt(window, openDomains)``any` 写「YYYY 年 M 到 M 月之间,有没有什么事,比如<口语 ≤3 个>?」,口语从 `KIND_ORAL` 表取、剔除 declined 与已覆盖,**没有按领域写 if**;全部领域都关掉时退化成「有没有什么事?」。`windowAlreadyAsked` 改成只比对(年, month_lo, month_hi),换个领域标签不重问。A 之后 `pendingGuidedYearWindow``any` 解析成第一个仍开放的领域,录入卡芯片默认落在它上面(`resolveEntryDomain` 另有防御)。
- 单测:Python 7 条(真实引擎 20 分钟窗 golden 非空、无领域轨道 = `any`、d9/d10 保留固定领域、拒答领域退回 `any`、split 均衡排序、跨年拆分);前端 3 条(开放题干口语 ≤3、同一窗口只问一次、芯片默认第一开放领域)。
## 4. F5 · 离线回放改成注入真值方向(BUG-752)
`scripts/research/guided_collect_holdout_replay.py` 新增 `window_boundary_dates()`:按 `guided_collect_windows` 自己的循环重算(轨道, index),拿回**每个候选**在该窗口上的边界日期。`truth` 组注入真值候选自己的那个月,`opposite` 组注入「剩余候选里离真值最远的那一个」的同一边界。三档半径都跑,`minute_rectification_holdout_v4.json` 20 例,**351.6 s,错误 0**,未用让步(没有只跑 ±10/±30)。
| 半径 | 方向 | 20 例达标 | 其中六题后本来就达标 | **靠引导件补上** | 达标件数中位 | 每例窗口数中位 |
| --- | --- | ---: | ---: | ---: | ---: | ---: |
| ±10 | truth | 3 | 3 | **0** | 0 | 6 |
| ±10 | opposite | 3 | 3 | **0** | 0 | 6 |
| ±30 | truth | 1 | 1 | **0** | 0 | 6 |
| ±30 | opposite | 1 | 1 | **0** | 0 | 6 |
| ±60 | truth | 0 | 0 | **0** | — | 6 |
| ±60 | opposite | 0 | 0 | **0** | — | 6 |
原始表 `docs/research/guided_collect_holdout_2026_09_16.json`(含每例的 `injected`,形如 `19:24:2026-05-08`,可核对注入确实落在不同候选的边界上)。
口径说明与观察:
- 上一轮「0/20」的写法**不成立**(注入的是窗口左端点),这一轮按真值边界注入之后**结论仍然一样**:没有一例是「六题后未达、补引导件后达标」。这次的数字可以作数。
- truth 与 opposite 两组数字完全相同,说明在这个 holdout 上,把事件放在真值的边界还是放在最远候选的边界,对 D1 门槛没有可测差别。
- 每例都有 6 个窗口(上限就是 6),所以不是「题不够」。
- 窗口年份集中在 2025–2026:排序先按 split 均衡、再按靠近今天,`lo = 出生年 + 5``hi = min(今年, 出生年+80)`。真人补经历时这几年确实最容易记得,但对名人 holdout 来说这些年份多半没有可用事件。
- **这不是合入门槛**,产品据此决定 10 分钟门槛是否要调,或者引导题源是否还要换一条路。
## 5. F6 · 录入卡提交文本(BUG-750)
`formatEventDateEntryMessage` → 「YYYY 年 M 月(D 日),<领域标签>方面有一件事」,标签取 `RANGE_DELIVERY_DOMAIN_LABEL`。单测断言两种日期形态与「不含『或』」。**没有**改成 `set-focus` / `batch` 的结构化写入(任务书给的是「或」二选一),走的仍是现有带年月证据路径。
## 6. F7 · page.tsx 与 golden fixture
- `page.tsx` **1838 → 1837**`rectificationDeclaredTime` 常量改成 `rectificationDeclaration = { declaredTime, birthDate }`JSX 里两行 prop 合成一行 `{...rectificationDeclaration}`
**偏离**:任务书写的是「把那一行挪进 `conversational-birth-time-rectification.tsx` 内部取档案」。做不到而且不划算——该组件没有档案来源:`RectificationCaseSnapshotPayload``case` 段只有 `status / accepted_time / confirmed_time / candidate_range / stage / birth_time_source`**没有 birth_date**;要在组件里拿档案得新开一次 `/api/account` 读取或改服务端合同,为了省一行代码多一次网络请求并不合算。行数目标已达成,记在这里由验收方判断。
- `tests/test_event_probes_guided_windows.py` 重写:新增 `_real_built()`,用 `build_candidate_static_context` 对公开 1990-01-01 北京测试盘的 12:0012:20(**20 分钟窗**,5 个采样分钟)跑真实引擎,断言窗口非空并逐字段校验;`test_declined_domains_are_removed` 改成 `test_declined_domains_fall_back_to_open_windows`,用真实引擎结果做,**`skipTest` 全部删除**(文件里已无 `skipTest`)。手造 static contexts 只留在排序与跨年两条纯结构断言里。
## 7. 记录
- `docs/BUG_HISTORY.md`:BUG-747752(本分支内连续;744~746 属侧栏统一单,见 §0),BUG-733 补一行。
- `CHANGELOG.md`2026-09-16 「问完所有线才出卡,时间窗不再硬贴一个领域」,写明 Skill 10.0.28。
- `frontend/DESIGN.md``guided-collect` 行改写(开放窗口题干、一窗一问、录入卡提交文本),`candidates` 行写明门槛是必要条件而非充分条件。
- `frontend/docs/VOICE.md`:新增两行(开放窗口题干、录入卡提交文本),已对照既有口径。
- `docs/tasks/README.md`:本单状态板改「已实现,待验收」+ 分支名 + 进度记录名。
- 真机清单沿用 `docs/testing/rectification-guided-collect-20260916.md`(六条),本轮没有新增浏览器级验收项。
- 未写入任何真实出生资料;示例只用公开名人 holdout 与 1990-01-01 测试盘。
## 8. 偏离与未做
1. **F7 的 `birthDate` 没有挪进组件内部**(理由见 §6),改用 props 合并把 page.tsx 降回 1837 行。
2. **(第一轮偏离,已修正)** 第一轮按修复单 F3 §2 的字面实现了「门槛未达即使线问完也只进 collect」,与主单 D2「引导题源也空了就按现行规则出卡」冲突,会造出「没有题也没有卡」的死角。产品第二轮拍板以 D2 为准:门槛只在还有题可问时挡出卡,题源全空照现行规则出卡。当前实现是修正后的版本,本记录各处数字也是修正后重测的。
3. **F2 实际处置 41 条而非 16 条**:任务书的 16 条表是在 cfb41daf 上数的,那一版用 `!mayDeliverOnPrecision` 短路盖住了 D5 带来的「七条线全部轮到」,撤回短路后另有 25 条既有断言跟着变。全部按三栏改,清单见 §2.3。
4. `run_quality_gate.py --profile quick` 的最后一级是 `npm test`,本机无 Docker,那 31 条与基线同名的失败照旧;前面各级(JSON、capabilities、fragments、BPHS、CORE_PYTEST_TARGETS)通过。
5. 浏览器级验收(登录态 / Chrome)、staging 部署核对仍是环境缺口,留给 `docs/testing/rectification-guided-collect-20260916.md`
+1 -1
View File
@@ -253,7 +253,7 @@
| `RECONCILE-20260916.md` | — | **对账清单(不是任务书)**:状态板与现实脱节(09-15~16 那 10 份早已合入却仍写「待领取/待验收」,本轮已修正),所以另外 7 份 09-10~14 的单**不能拿 README 当证据**——它们没有 `PROGRESS-*.md`,但引用的 BUG 号都是 `resolved`。要么被别的单顺带修了没留记录,要么压根没做。每行回一个「做了/没做」即可。另附三条我已查到确定没做的证据、两条状态未闭环的、以及 BLK-001 仍红(2026-09-16 在 `4f643aa0` 复跑确认) | 待产品负责人 / 执行方回填 | — |
| `TASK-rectification-precision-gate-guided-collect-20260916.md` | `PROGRESS-rectification-precision-gate-guided-collect-20260916.md` | **出卡加精度门槛 + 引导式补经历**:真机 30 分钟窗给 5 件事 / 3 领域就出卡,区间 20 分钟、5 候选并列 26/26/20%,卡下只有自由文本邀请。产品拍板 D1 出卡门槛「宽度 ≤10 分钟且前两名差 >3 个百分点且不并列」;D2 用户说没有了仍按现行规则出卡(保留永远给结果);D3 门槛未达改为系统点名逐题问 + 类型芯片 + 年/月选择器录入,删自由文本邀请;D4 跳过的线换问法再问一次(拒绝不重问,改 BUG-687 口径);D5 时间点题答「没发生」不关领域、存在性题问整个领域、七条线一张表不许按领域写死。引擎新增 `guided_collect_windows`(边界不设 45 天闸门、含小运),不改打分与 lead。Skill 10.0.27。附 T6 两条核实:B/C/D 回执文案对调、性格题后范围回弹 8 分钟。BUG-740~743 | **已验收:未通过(带修复单)** | `cfb41daf`tsc/lint/Static/gzip(+0.44%) 通过,D1D5 单测通过;但 `npm test` 新增 16 红(校正既有断言未跟上)+ memoization golden 因 receipt 新键红(门禁级,staging 仍部署 317e9f18);引导窗口领域轮询分配、回放未按真值注入、录入卡走模型轮 → `TASK-rectification-precision-gate-guided-collect-fix-20260916.md` |
| `TASK-sidebar-unify-20260916.md` | `PROGRESS-sidebar-unify-20260916.md` | **侧栏统一成一个组件 + 次级页共享外壳 + 跳转不整页刷新(纯前端)**:`/` 用 `AppSidebar`、四个次级页用另写的 `AppNavRail`,两套标记结构共用一份 CSS,会话行少 `.session-main` 一层(无内边距 / 44px 高 / 当前项标记、留 44px 空列)、页脚 56px 菜单 vs 44px 文字;首页进次级页是 `window.location.assign` 整页刷新,次级页各自在组件里挂 `SecondaryShell`、无共享 layout 无缓存,每次跳转重拉 `/api/sessions` + `/api/account`;折叠状态不持久。产品拍板 D1 只留一个 `AppSidebar`(操作回调可选 = 只读模式,D9「不带写操作」维持)、D2 路由组 `app/(secondary)/` layout 承载外壳、D3 改 `<Link>`、D4 模块级 60 s 缓存 + 首页写操作失效、D5 折叠状态存 cookie、D6 回首页慢的启动链另开一单。实测:接口 TTFB 0.630.90 s,回 `/` 启动链串行 ≥4 次往返 + 揭幕闸门 4 s。BUG-744746 | 已验收(2026-09-16Opus 子代理实现 `22edabbc`,门禁全绿、失败清单与基线逐条相同;折叠状态取 localStorage 触发让步 1;真机清单待走) | `codex/sidebar-unify-20260916` |
| `TASK-rectification-precision-gate-guided-collect-fix-20260916.md` | | **验收修复单**F1 `decision_receipt` 新增 `guided_collect_windows` 让 `test_rectification_engine_memoization` golden 红(在 CORE_PYTEST_TARGETSstaging 未部署 cfb41daf);F2 16 条既有校正断言红未按三栏改;F3 `mayDeliverOnPrecision` 缺省即放行、短路 BUG-654 且 `guidedCollectExhausted` 不含 refresh;F4 无领域轨道窗口轮询贴领域、一窗只问一领域;F5 离线回放注入 month_lo 非真值方向、0/20 不作数;F6 录入卡合成「或」列表句走模型轮;F7 page.tsx +1、手造 fixture。D6 产品已拍板:门槛只是必要条件,仍问完线再出。BUG 段 744 起 | 待领取 | 与主单同文件;开工基线 3391 / 47 |
| `TASK-rectification-precision-gate-guided-collect-fix-20260916.md` | `PROGRESS-rectification-precision-gate-guided-collect-fix-20260916.md` | **验收修复单**F1 `decision_receipt` 新增 `guided_collect_windows` 让 `test_rectification_engine_memoization` golden 红(在 CORE_PYTEST_TARGETSstaging 未部署 cfb41daf);F2 16 条既有校正断言红未按三栏改;F3 `mayDeliverOnPrecision` 缺省即放行、短路 BUG-654 且 `guidedCollectExhausted` 不含 refresh;F4 无领域轨道窗口轮询贴领域、一窗只问一领域;F5 离线回放注入 month_lo 非真值方向、0/20 不作数;F6 录入卡合成「或」列表句走模型轮;F7 page.tsx +1、手造 fixture。D6 产品已拍板:门槛只是必要条件,仍问完线再出。BUG 段 744 起 | **已实现,待验收** | `codex/rectification-precision-gate-guided-collect-fix-20260916`;进度记录 `PROGRESS-rectification-precision-gate-guided-collect-fix-20260916.md`BUG-747752Skill 10.0.28;已 rebase 到 `a396bbe0``npm test` 3405 / 31 红(基线 `a396bbe0` 实测 3399 / 47,红清单逐条同名、0 条新红),`pytest tests/test_rectification_*.py tests/test_event_probes_guided_windows.py` 193 passed;首屏 gzip 583,765 → 582,800(−0.17%),`page.tsx` 1836 → 1835 |
## 命名与归档