docs(rectification): add provisional-adopt task brief
Independent Staging Quality Gate / validate (push) Successful in 15m33s
Independent Staging Quality Gate / publish (push) Has been cancelled

采用门对齐引擎语义:provisional 采用成为一等成功出口。
记录三权威冲突根因、经授权推翻的旧红线、以及本地 1993
分钟级收敛系答案泄漏(不构成 web 端目标)的核查结论。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0155nFCgCHtoA7jhSDGmZmMu
This commit is contained in:
Jesse_Chen
2026-09-01 08:01:25 +00:00
parent 75fc456d7e
commit 422fc65b22
@@ -0,0 +1,156 @@
# 任务书 · 采用门对齐引擎语义:provisional 采用成为一等成功出口(2026-09-01)
基线:`origin/staging` @ `75fc456d`
## 0. 决策记录(必读,本轮推翻旧红线是经产品拍板的授权变更)
上一轮任务书(`TASK-rectification-nonterminal-exit-20260901.md`)写有红线:"不得放宽 `deliveryCapability`"、"任何让事故 case 变成 `canAdopt: true` 的改动都是错的"。**该红线已由产品负责人于 2026-09-01 显式推翻**,理由见下方根因:现行 `deliveryCapability` 把"可采用"绑在了数学上不可达的条件上,导致**没有任何真实用户能走完一次成功流程**。本轮的目标:
> **当引擎按方法论判定可以出牌时,用户必须真的能采用"代表分钟 + 可信区间"provisional)。分钟级分离与 holdout 只保留给"唯一分钟确认"这道门。**
依据(三个权威本来就一致,只有 TS 层跑偏):
- 引擎 receipt`rectification-candidate-policy-v2`):`acceptance_allowed` 四门(候选存在 / ≥3 可评分事件 / ≥2 领域 / 日期质量)+ `propose_allowed`4 事件/3 领域/诊断稳定 **或** 事件吻合率 ≥80%)。它老实承认相邻分钟不可分(本 case `indistinguishable_width_minutes: 29`),所以它的交付物定义就是"代表时间 + 区间"。
- V10 Skill 文本(10.0.14`references/candidate-comparison.md`):"唯一领先和宽度≤5 **只挡确认门**,不挡出示代表性时间卡。"——**skill 文本无需改动,本轮不 bump skill 版本。**
- 上游方法论(yinduzhanxing `interview_playbook.md`):最终交付措辞就是"当前最优结果是候选时间段…临时代表时间仅用于下一轮验证与比较"。
**不放宽的东西**(新红线,见 §3):唯一分钟确认门、表达边界、证据下限、引擎 ceiling 交集。
---
## 1. 事故实证
真实 case `f83d9b42-0ac7-411b-9e43-95059b2ede43`Skill `10.0.14`,快照已存档(向任务发起人索取原始 JSON)。关键形状:
- 5 条 confirmed 证据(education×1 被引擎划为 holdout、relationship×2、career×2),3 个领域;5 道区分题已答完;候选从 04:45–05:15(30 分钟)收敛到 **05:0005:077 分钟)**,熵 2.00→0.86。流程本身工作正常且收敛良好。
- 引擎 receipt`acceptance_allowed: true``selection_allowed: true``propose_allowed: true``event_fit_rate.band: "high"`80%)、`margin_percent: 19.94`
- TS overlay(用户实际看到的):`can_adopt: false``selection_allowed: false``propose_allowed: false``choice_card: null``interview.type: "offer_provisional_range"`
- 结果:用户答完约 10 道题、花掉点数,得到一句"当前可信区间是 05:0005:07"的文字,没有候选卡、什么都存不下来,然后被继续追问"有没有记得住时间的收入变化"exhaustion 轮换:finance→occupation→health_pressure→relocation→career→relationship→通用兜底),无限循环。
## 2. 根因(已静态溯源逐条核实)
### 根因 1 · `deliveryCapability` 的两个条件数学上不可达
`frontend/src/lib/rectification-agentic/core/rectification-decision.ts``deliveryCapability`(约 :196,按符号名定位):
```ts
const locallySelectable = separation.ranked.length > 0
&& !coverageBlocks
&& stopClass?.kind !== "keep_collecting"
&& stopClass?.kind !== "exhausted"
&& separation.sufficient // ← 不可达之一
&& holdout === "passed"; // ← 不可达之二
```
- **`separation.sufficient` 不可达**:需要 top-2 领先 ≥ `MIN_SEPARATION_LEAD=8`,计分制每题 ±2。逐条核对事故 case `inference_state.probes``expected_outcomes`:**所有剩余可问的题(dasha 边界题、nakshatra 边界题)都把 05:00 与 05:07 分在同一组**,答什么都同涨同跌。唯一能分开这两个分钟的是 d24/d12 等 varga 对比题,但全部被 `yearless_ungrounded_contrast` 政策丢进 `dropped_probes`。lead 永远停在 5。
- **`holdout === "passed"` 事实上不可达**`ask_holdout_validation` 分支在 `decideRectification` 里位于 coverage 检查之后、且要求 probe 耗尽;即使问到并通过,第一条仍然封死 `canAdopt`
- 这不是单 case 现象:任何"约 5 条真实事件 + 31 分钟候选网格"的普通用户都落在同一形状上。**成功率结构性为零。**
### 根因 2 · 引擎与 TS 权威冲突时,TS 永远向"否"裁决
引擎与 skill 文本都认为本轮可出牌;TS `deliveryCapability`(来自 9f011194/4b133177 两轮收紧)单方面追加了分离+holdout 要求。overlay`decision-from-dossier.ts``overlayPublicDecision`)取交集,于是引擎的"可以"永远被 TS 的"不行"覆盖。
### 根因 3 · "本地 Agent 能收敛到 1-2 分钟"不构成 web 端目标(答案泄漏,勿对标)
有一组本地对话记录(yinduzhanxing 根目录 `1993生时校正对话-*.txt`)显示本地 Agent 调用同一 skill 能把 1993 案例收敛到 14:49。**那不是校时能力,是答案泄漏**:本地仓里存有该用户的既有结论(`qizheng_1993_native_current_report_2026_08_26.md` 记录 14:49:00、候选段表钉死 14:48-14:50、`classical_zr_known_user_case_calibration_1993` 回归包),Agent 在对话里两次自认"14:49 主要被仓里的已知校准档锁住,不是单靠回答算出来的",随后更把 `target_minute=14:49` 作为回归提示直接写进了脚本(该污染只在上游 `codex/add-birth-time-rectification-skill` 分支 commit 3bb90620**已核实未进入本产品仓与 vendored skill 快照**`grep pl9_1993|target_minute|regression_only` 为零命中)。web 端面对无答案库的陌生用户,7 分钟不可分区间 + 80% 拟合才是 5 条事件的真实信息量上限。任何人(包括测试)不得以"收敛到唯一分钟"作为本轮验收口径,也不得把该上游分支的回归提示代码引入产品。
### 附带确认(本轮不修)
- coverage 死锁**不存在**`exhaustionSpokenCollectFollowup` 的轮换在 family/education/finance 之后会问 occupation,且拒答(`occupationCollectFocusClosed`)也算覆盖。coverage 门保留。
- 引擎 receipt 的 `acceptance_allowed``diagnostic_quality.passed` 不联动,是已记录的引擎侧特性(见 BUG_HISTORY / 上轮任务书),本轮不动引擎。
- 上一轮的非终态出口闸(`finalizeSuccessfulTurnExit`)工作正常,本 case `current_question` 非空。保留。
---
## 3. 硬红线(本轮新版)
1. **唯一分钟确认门一个字不放宽。** `canConfirmExactMinute` 必须继续要求:分离充分 + holdout passed + `confirmationAllowed` + 引擎 `confirmation_allowed`(引擎恒 falsefail-closed)。改完后任何路径下 `can_confirm_exact_minute` 仍不得为 true。
2. **表达边界不放宽。** "不可分区间 / 代表性候选 / 不是已确认的唯一出生分钟"等措辞、`REPRESENTATIVE_MINUTE_DISCLAIMER``RECTIFICATION_TERMINATION_COPY` 保持;采用后写入 `accepted``active_birth_time`),**不得**写 `confirmed`
3. **证据下限不放宽。** `keep_collecting`< `MIN_STANDALONE_DATED_EVENTS=3` 或 < `MIN_STANDALONE_DATED_DOMAINS=2`)时仍不得采用。四个阈值常量(`MIN_SEPARATION_LEAD=8`、3/2、3/2)数值不动——本轮只改它们**参与哪道门**,不改它们的值。
4. **引擎 ceiling 仍是硬上限。** overlay 对引擎结果只能收紧,不能放宽:receipt 不自洽或 `acceptance_allowed=false` ⇒ 采用仍关死(`engineCapabilityCeilingFromReceipt` 的 fail-closed 语义不动)。
5. **coverage 门保留。** `coverageBlocks`(含 occupation)仍挡采用——它经 exhaustion 轮换可满足,且与 skill 文本一致。
6. **不 bump skill 版本,不改 `skills/**`。** skill 文本已与本轮目标语义一致。
7. **不修引擎(Python 侧)。** 本轮只动 TS 决策层与必要的投影/UI。
8. 推 staging 前 `cd frontend && ./node_modules/.bin/tsc --noEmit` 必须通过。**不要用 `npx tsc`**(空 worktree 会装到假包 `tsc@2.0.4`)。
9. 不改 `.gitea/workflows/**`;不在有未提交改动的工作树上切分支;不自行把 staging 提升到 main。
10. 作者无 staging 环境凭据,根因来自静态溯源 + 事故快照。你若同样无凭据,**不得声称已在真实环境验证**。
让步顺序:**用户能拿到诚实且可采用的结果 > 确认门与表达边界不放宽 > 功能与测试不回归 > 代码整洁**。
## 4. 开工前置
```bash
git fetch origin --prune
git worktree add -b codex/rectification-provisional-adopt-20260901 \
../.worktrees/rectification-provisional-adopt-20260901 origin/staging
```
`docs/research/pre_work_error_ledger.md`,跑 `scripts/pre_work_check.py`,读 `frontend/AGENTS.md`。在 `docs/BUG_HISTORY.md` 检索 **BUG-456 / BUG-459** 及上两轮记录。**行号只是线索,按符号名定位。**
---
## 任务 0(门控)· 先写不变量,先让它红
`frontend/tests/rectification-decision-authority.test.ts`(或新文件 `rectification-provisional-adopt.test.ts`)加入:
1. **事故形状回归(核心)**:按 `f83d9b42` 的真实形状构造 dossier——5 条 confirmed 证据(education/relationship×2/career×2education 为 holdout usage)、inference 5 题已答、活跃候选 05:00=24 / 05:07=19 / 04:53=8lead=5 < 8)、引擎 receipt `acceptance_allowed/selection_allowed/propose_allowed` 全 true、`confirmation_allowed=false``diagnostic_quality.passed=false`、oos_blind_prompts 剩 family/finance/health_pressure、occupation 未覆盖、family 已拒答。断言:
- a) occupation 覆盖(confirmed 或拒答关闭)后:`can_adopt === true``selection_allowed === true``credible_range``["05:00","05:07"]``representative_time === "05:00"`
- b) 任何情况下 `can_confirm_exact_minute === false`
- c) occupation 仍未覆盖时:`can_adopt === false``current_question` 语义上存在下一步(沿用上一轮不变量)。
2. **证据下限保留**:同形状但只有 2 条事件或 1 个领域 ⇒ `can_adopt === false` 且 nextAction 为收集类。
3. **引擎上限保留**:同形状但 receipt `acceptance_allowed=false`(或字段不自洽触发 fail-closed ceiling)⇒ `can_adopt === false`
4. **确认门冻结**:构造分离充分 + holdout passed + 引擎 `confirmation_allowed=false``can_confirm_exact_minute === false`;引擎四字段自洽且 confirmation true 的合成形状下才允许 true(现网引擎恒 false)。
跑一遍,确认 1a 是红的、其余按当前行为核对。**1a 全绿说明测试没写对,重写。**
## 任务 AP0)· `deliveryCapability` 重构
`core/rectification-decision.ts`,按符号名定位 `deliveryCapability`
- `locallySelectable`(供 `canAdopt` / `selectionAllowed` / `proposeAllowed`)改为:
```
ranked.length > 0
&& !coverageBlocks
&& stopClass?.kind !== "keep_collecting"
```
即**移除** `separation.sufficient`、`holdout === "passed"`、`exhausted` 三个条件。理由:分离与 holdout 移交确认门;`exhausted`tied_first / user_uncertainty_too_high)按引擎与 skill 语义只影响措辞与确认门——tie 时引擎照常给出代表候选,unsure 答案本来就不计分。
- `canConfirmExactMinute` 显式补回严格条件:
```
locallySelectable
&& separation.sufficient
&& holdout === "passed"
&& confirmationAllowed
&& engineCeiling.confirmationAllowed
```
(红线 1:净效果必须与现状等价或更严。)
- `capability` 已通过 `...capability` 展开进入 `collect/discriminate/holdoutValidation/offerRangeWithoutAdopt/completeWithRange/finish` 所有出口,无需逐出口改;确认无出口自行覆写这四个字段。
- `isNonConvergingRangeOffer``canOfferRange && !canAdopt && ...`)语义不变:capability 放行后它自然只在 coverage 未满足或证据不足时为真,exhaustion 收集兜底照旧工作。确认 `answer-choice.ts` 的 `persistNextInterviewIfIdle` / `persistExhaustionCollect` 在新语义下行为正确(occupation 覆盖前仍会追问;覆盖后进入可采用交付,不再轮询)。
## 任务 B(P0)· 交付点必须真的出牌
capability 放行只是数据层。逐条确认交付链路,缺哪补哪:
1. **投影**`decideFromDossier` → `overlayPublicDecision` → GET case 快照中 `can_adopt/selection_allowed` 为 true 时,`latest_result.candidates` 与 `interview` 投影能驱动前端候选卡(`rectification-agentic-chat.tsx` 的 `RectificationCandidateCards` / `birth-time-candidate-result`)。**用户不应需要再发一条消息才看到卡**——GET 刷新即可见。
2. **next_user_action**`decideConversationalSession` / `buildNextUserAction``method-followup.ts`)在 offer/complete 且 `canAdopt=true` 时应产出 `adopt_representative`(或等效采用引导),且该轮零追问(skill:不得同一回复既要求补证据又提供采用)。
3. **采用 RPC**`cases/[caseId]/candidates/accept` 链路上若存在 `selection_blocked` 类服务端校验,确认其读取的是新语义下的投影(采用不被旧字段挡住);采用后进入既有 `verify_adopted_time` 核前事流程,不改其逻辑。
4. **叙述**offer 轮正文按 skill §9 出验证报告(候选窗、代表分钟、相对支持、八法理由),并保留"不可分区间/代表性候选"边界句。`nonConvergingRangeNarration` 在可采用时不应再是唯一出口。
## 任务 C(P1)· 既有断言清点与治理
- 上一轮的下列不变量**按本任务书 §0 授权修改**,逐条在 PR 里写"原断言 → 新断言 → 为什么":分离不足不采用、holdout unavailable/not passed 不放行采用、`offer_provisional_range` 恒 `can_adopt=false`、(如存在)tied 不采用。
- 下列不变量**必须保持全绿**:证据不足不放行、引擎 ceiling 交集 fail-closed、确认门 fail-closed、非终态出口闸(BUG-456 组)、`read_only` 无副作用。
- 修改断言配额:仅限上述授权组;授权组之外不得改既有断言,遇到冲突停下汇报。
- `docs/BUG_HISTORY.md` 追加一条:定性为"策略死结"(不是回归),记录三权威冲突、被推翻的红线出处、授权来源与日期。
## 验收标准
1. `cd frontend && ./node_modules/.bin/tsc --noEmit` exit 0。
2. `npx tsx --test tests/rectification-*.test.ts` fail=0`skill-registry.test.ts` 16/16。测试总数允许变化(授权断言组改动 + 新增不变量),在 PR 里给出新基线数并解释增减。
3. 任务 0 的四组不变量全绿。
4. **事故形状前后对照**贴进 PR:改动前 `can_adopt=false` + finance 追问循环;改动后 occupation 覆盖 ⇒ `can_adopt=true` + 候选卡可见 + `can_confirm_exact_minute=false`。
5. 说明"新增决策分支不会绕过 capability 单点"的机制(capability 只在 `deliveryCapability` 一处计算、经展开进入全部出口,不得在出口处覆写)。
6. 未在真实环境验证的部分如实注明。