Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0193vBv6w5MV2cifdTUu9H5P
30 KiB
TASK · 生时校正:采集完没有出口、反推题形态不对、候选收不敛(2026-09-06)
- 基线:
origin/staging@e2d6f203(代码头e2f4b55c,staging 已部署同一 SHA,事故就是这版跑出来的) - 分支:
codex/rectification-convergence-exit-20260906 - 执行方:coding agent;验收:Claude
- 涉及文件:
- TS:
frontend/src/lib/rectification-agentic/v9/answer-choice.ts、method-followup.ts、decision-from-dossier.ts、probe-question-contract.ts、core/build-state.ts、core/candidate-separation.ts(只读)、frontend/src/app/api/rectification/agent/route.ts、frontend/src/components/rectification-agentic-chat.tsx、frontend/src/lib/rectification-agentic/user-copy.ts - Python:
scripts/rectification/decision_policy.py、scripts/rectification/event_probes.py、scripts/rectification/probe_question_contract.py、contracts/probe-question-v1.json - 不改
scripts/jyotish_api_server.py(增长冻结),不改frontend/src/app/page.tsx
- TS:
- BUG 编号起点:BUG-558(BUG-557 已被
TASK-session-title-guard-fix-20260906.md预留;开工时grep -o "^## BUG-5[0-9][0-9]" docs/BUG_HISTORY.md | tail -1复核) - 与其他任务书的关系:与
TASK-session-title-guard-fix-20260906.md(consult 路由)无文件交集,可并行。本单三个工作流 A → B → C 串行,同一分支分三次提交。
1. 事故实证(staging,2026-09-06,产品负责人实测)
年份一律抽象(Y、Y+4 …),不写出生资料。用户一次走完整个采集,共说了 7 件带时间的经历,答了 6 张点选卡,最后停在一句"也可以再说一件你记得大概时间的事",界面只剩"目前范围 …–…,还在收窄"的只读小字,没有候选卡、没有停止按钮、没有下一步。
| 轮 | 系统问 | 用户答 | 备注 |
|---|---|---|---|
| 1 | 开场采集(GENERIC_COLLECT_QUESTION) |
上大学 Y 年 9 月、毕业 Y+4 年 6 月 | 2 件 month |
| 2 | 感情采集(USER_COLLECT_QUESTION.relationship 原文) |
Y+8 年 5 月认识、8 月 8 日分手 | month + day |
| 3 | 点选卡「在亲密关系里更接近哪一种 A/B/C/D」 | 点选 | varga_style,有事件锚点 |
| 4 | 点选卡「平时做事更接近哪一种 A/B/C/D」 | 点选 | varga_style,无任何时间锚点 |
| 5 | 点选卡「Y+7 年 5 月前后有没有入职/换工作 A/B/C/D」 | 点选 | existence,career |
| 6 | 点选卡「Y+8 年 4 月前后有没有入职/换工作」 | 点选 | existence,career |
| 7 | 点选卡「Y−1 年 5 月前后有没有搬家/长期住外地」 | 点选 | existence,relocation |
| 8 | 家人采集(原文) | "没有" | declined |
| 9 | 钱的采集(原文) | Y+10 年 1 月欠薪、开始欠债 | month;助手随后说"想先看看当前候选也直接说一声,我来汇总" |
| 10 | 搬家采集(原文) | Y+7 年 7 月搬到外地 | month |
| 11 | 点选卡「Y+7 年前后工作上有没有入职/换工作」 | 点选 | 与第 5 轮同域同年,只是去掉了月份 |
| 12 | 身体/压力采集(原文) | Y+10 年 1–8 月难熬 | range |
| 13 | 职业采集(原文) | 三段职业,无年份 | occupation_note |
| 14 | 「也可以再说一件你记得大概时间的事。」(USER_COLLECT_QUESTION.other 原文) |
—— | 死胡同 |
只读小字显示范围仍有 27 分钟宽。每张点选卡答完只回"已记录你的选择,并更新了候选比较",看不出哪一题让范围动了。
产品负责人的三条判断,本单照单全收:
- 与 Agent 互动完之后没有下一步,用户不知道该做什么,卡在那里。
- 占星反推题与验证冲突题应当是"在几年几月发生了什么事 → 你觉得更像 A/B/C/D"这种形态;现在是一个领域一个领域地问"那时候有没有 X",同一个时间点被拆成几道题,还有没时间锚点的性格题。
- 时间根本收敛不了。
2. 根因(三条,互相独立,都已在代码里定位)
2.1 出口:非收敛出牌 / 非终止修复都退化成"再说一件事",没有交付
answer-choice.ts::persistNextInterviewIfIdle在isNonConvergingRangeOffer(decision)(canOfferRange && !canAdopt && nextAction === "offer_provisional_range")时调用persistExhaustionCollect;ensureNonTerminalTurnExit在"没有问题也没有采用载体"时同样调用它;route.ts无焦点分支也走同一条。persistExhaustionCollect→method-followup.ts::exhaustionSpokenCollectFollowup:先nextDatedCollectFollowup(七个领域都问过/拒过就为空),再职业,最后 无条件落到otherCollectFollowup,即第 14 轮那句话。它的domain="other"不在REMAINING_EVIDENCE_COLLECT_DOMAINS,所以既不算"剩余采集"也不挡出牌,但它作为collect:other:*焦点被持久化后,下一轮再答一件事又回到同一处——没有终止条件。- 出口所依赖的
canAdopt=false从哪来,服务端没有留下任何日志(persistExhaustionCollect不记录decision的输入)。可能来源有四个:引擎acceptance_allowed=false(decision_policy.py的 event_quality / domain_diversity / date_quality 门)、trainingScoreableGate因证据 status 非confirmed而关闭、candidateScoresFromDossier在 inference 存在但投影不一致时返回空数组(ranked.length===0→keep_collecting)、engineCapabilityCeilingFromReceipt因 receipt 字段自相矛盾而返回全关。本单不猜是哪一个,先补日志与形状回归,再把"不管哪一个都不能落到 other"钉死。 - 用户能看到的唯一状态是
RectificationReadonlyRange(sessionOutcome ∈ {collect_evidence, discriminate_candidates}时渲染),它不可点。口述采集态没有"先这样,先看当前范围"按钮(CHOICE_STOP_LABEL只挂在点选卡上)。turn-intent-classifier.ts定义了ask_about_result,route.ts却只处理stop_rectification,而且只在点选焦点分支里处理;助手那句"想先看看当前候选也直接说一声"是模型自由发挥,系统兑现不了。
2.2 题型:探针按"领域 × 大运边界月"一条条出,同一日期拆成多题,同域同年月份不同再问一遍
-
event_probes.py::_discriminating_event_probe_lists对每个领域独立扫描_union_boundary_dates,同一个边界月会给 career、finance、relocation 各生成一条 existence 探针。用虚构出生资料(1998-03-15,北京,04:30–05:30)把本次事故的 7 件事喂进score_candidates,引擎公开的 8 条探针是:semantic_key gain 说明 career.2022.06.dasha_boundary1.11 同一月 finance.2022.06.dasha_boundary1.06 同一月 career.2023.05.dasha_boundary1.10 同一月 finance.2023.05.dasha_boundary1.01 同一月 relocation.2018.05.dasha_boundary0.99 relationship.2019.02.dasha_boundary0.96 finance.2024.03.dasha_boundary0.92 career.2024.05.dasha_boundary0.88 8 条里 4 条是"同一个月、换个领域再问一次"。用户感受就是"同一个时间点被反复问有没有 X、有没有 Y"。
-
去重键是
(domain, year, month, source)(_partition_ranked_probes)与 TS 侧askedKeys(semantic_key / split hash 精确匹配)。_try_activation_probe用年龄带中点年份生成年级探针(career.<Y>.dasha_activation,无月份),_existence_blocked_years只看账本里的年份,不看已问过的探针年份,于是第 5 轮问了career.Y+7.05,第 11 轮又问career.Y+7(BUG-540 修的是质量探针绑定,没覆盖这条)。 -
varga_style 卡(第 4 轮"平时做事")来自
contrastPacket的 D10 类型对照,没有任何日期锚点;第 3 轮同类卡之所以可接受,只是因为模型自己把"回想你 Y+8 年的这段感情"写进了题干。 -
_agent_brief给模型的提示是"写一句自然的是/否题",产品要的"那时候发生了什么 → 更像哪一种"在探针层根本没有这个题型。
2.3 收敛:相对支持度把引擎证据压平成 7–9,之后只有点选卡在推分
同一份虚构资料的量化结果(references/ 里没有该用户任何资料,数字只说明机制):
| 指标 | 4 件事 | 7 件事 |
|---|---|---|
| 61 个分钟的原始分 min / max | 6.03 / 11.19 | 10.96 / 15.52 |
| 原始分极差 | 5.16 | 4.56 |
公开候选(12 个特征簇代表)的 relative_support |
9 9 9 9 9 8 8 8 8 8 8 7 | 9 9 9 9 9 8 8 8 8 8 8 7 |
| top-2 领先 | 0 | 0 |
诊断 margin_percent |
0.80 | 2.26 |
acceptance_allowed / propose_allowed |
true / true | true / true |
decision_policy.py::_relative_support把 100 按原始分正比例分给最多 12 个簇代表。原始分是各事件正分之和,底座 11–15,事件带来的差异只有 4.5,正比例归一后每个簇 7–9 分,任何证据组合都到不了MIN_SEPARATION_LEAD=8。core/build-state.ts::buildInferenceState把relative_support当prior_score,之后每道点选卡按SCORE_DELTA±2 推分;credible-range.ts::unionStillValidRange取"与峰值差 < 8"的簇并集。于是:7 件带年月的经历对可信区间几乎没有贡献,区间只靠点选卡一题题往下压,压 4 题才可能淘汰一个簇;点选卡又有 8 轮 / 10 答的熔断。这就是"收不敛"。- 同一份数据换成"支持度 ∝ (原始分 − 窗口最低分)",12 个簇代表变成 14/14/12/12/10/9/8/7/6/5/2/0;换成 softmax(T=0.5)是 29/26/14/13/6/4/3/1/1/1/0/0。差别来自引擎已经算出来的证据,不是新证据。
TASK-rectification-provisional-adopt-20260901.md§2 当时已经看到"lead 永远停在 5",决策是"只改门、四个常数不动",但没有追到 prior 被压平这一层。本单不动MIN_SEPARATION_LEAD=8的数值,改的是喂给它的尺度。
3. 决策记录(产品负责人 2026-09-06 授权"彻查并彻底解决",以下是 PM 代为落地的决策,可否决)
- 采集封顶必须交付。 方法覆盖完成(
blockingMethodsCovered)、七个带年月领域都问过或拒过、职业已覆盖之后,不得再问任何开放式采集题。USER_COLLECT_QUESTION.other/USER_COLLECT_QUESTION_RETRY.other与otherCollectFollowup只保留给开场(!dashaCovered);exhaustionSpokenCollectFollowup删除other兜底。这推翻TASK-rectification-nonterminal-exit-20260901.md里"每轮至少留一个问题"的实现方式:每轮至少留一个问题或一个交付载体的规则不变,但穷尽时的载体改为交付,不是"再说一件事"。 - 交付分两种。 引擎
acceptance_allowed=true→ 走既有adopt_representative出牌(候选卡 + 八法报告 + 停止原因句)。acceptance_allowed=false→ 不出牌,但必须把 receipt 的acceptance_reasons翻成用户能照做的一句话(例:"还差 1 件带月份的经历,领域不限" / "两件事的日期还没对清"),并给"先这样,先看当前范围"按钮;范围小字改为可点。不得再出现"也可以再说一件事"。 - 口述采集态也要有停止按钮,与点选卡共用
CHOICE_STOP_LABEL,走STOP_ACTION(→paused→provisional_range_user_stopped)。ask_about_result意图等同于用户按了这个按钮。助手文案不得承诺系统做不到的事(Skill 文案另开单,本单只在user-copy.ts与提示词里删掉"说一声我来汇总"类措辞的来源——若来源在模型自由发挥,进度记录写明即可)。 - 题型改为"某年某月发生了什么"。 同一 (year, month) 的多领域 existence 探针合并成一张卡(见 5.B2),选项是领域,不是"发生了 / 没发生";题干必须写出年月,若账本里有 ±2 个月内的其他领域事件,题干要提到它("Y 年 7 月你搬到外地前后,工作上……")。这是产品要的"反推 + 验证冲突"形态。四选项契约(BUG-390)不放宽:一张卡仍是 A/B/C/D 四项,只是每项映射到两条探针的答案。
- 同域同年只问一次。 已问过
career.Y.05.*,则career.Y、career.Y.03、career.Y±1(按EXISTENCE_NEARBY_YEARS)全部视为已问。Python 与 TS 两侧都要生效。 - 无锚点的性格题不出。 varga_style 卡只在同域账本里有带年月事件时才可渲染,题干提示必须带那件事的年月;排序在带年月卡之后。
- prior 换尺度。
_relative_support改为按 (原始分 − 全窗口最低分) 归一(或温度可调的 softmax,由校准决定),MIN_SEPARATION_LEAD=8、3/2 采用门、4/3 确认门、maxPlateauRounds等数值一律不动。这是对TASK-rectification-decision-authority-20260831.md§4 "数值不动"的补充而非推翻:常数不动,改的是输入尺度,并且必须过 5.C2 的校准门。合入前把校准表拿给产品负责人看一眼,他点头再合。 - 先补日志再修。
persistExhaustionCollect的每次进入都要写一条结构化日志(无 PII),本单合入后若真实环境再复现,靠日志定位canAdopt=false的来源,不靠猜。
4. 硬红线
- BUG-390 四选项契约、
contracts/probe-question-v1.json的 yes / weak_yes / no / unsure 语义不放宽;新增 choice_kind 必须同时进 Pythonprobe_question_contract.py与 TSprobe-question-contract.ts的契约测试。 tests/test_rectification_engine_convergence.py::test_scripts_and_frontend_have_no_answer_key_literals必须继续通过:不得在代码里写任何真实案例的年份/分钟字面量。- 采用门 / 确认门 / 训练门 / 熔断的数值不动(
MIN_ACCEPTANCE_*、MIN_CONFIRMATION_*、MIN_SEPARATION_LEAD、DEFAULT_MAX_DISCRIMINATION_ROUNDS、EFFECTIVE_ANSWER_SAFETY_CAP、maxPlateauRounds、minUncertaintyAnswers)。 - BUG-503(一道 D 不停)、BUG-520(区分卡答"否"不是拒答)、BUG-540/541(质量探针绑定)、BUG-544/546/547/549/550 的用例原样通过;改既有断言写"原值 / 新值 / 原因"三栏。
- 点选答案仍不写证据账本(BUG-389–393 分层)。
- 前端:
tsc --noEmit0 错、npm run lint0 error、测试总数不低于开工时origin/staging实测;page.tsx不增长。Python:run_quality_gate.py --profile quick通过,scripts/jyotish_api_server.py不增长。 - 任务书 / 进度 / Bug 历史 / 测试 fixture 只用抽象年份或公开名人(
references/real_case_calibration/)。
5. 任务分解
A · 出口(BUG-558)
A1 结构化日志。 persistExhaustionCollect 入口 console.warn(JSON.stringify({ event: "rectification_exhaustion_collect", case_id, can_adopt, ceiling: {acceptance, selection, propose}, training_gate: {count, domains, open}, stop_class, ranked_count, probe_key, budget: {rounds, answers, plateau}, next_domain, next_source }))。
- 验收:单测断言字段齐全且不含 evidence summary / 年份。
A2 穷尽即交付。 改 exhaustionSpokenCollectFollowup:删 other 兜底,返回 null 时由调用方分流——
engineCeiling.acceptanceAllowed && trainingGate.open→ 走adopt_representative交付(复用adoptHostNarration/deliveryAdoptNarration,stopReason用新增的"probe_pool_exhausted",文案:"能分开候选的问题已经问完,先按现有材料给你结果。")。decideRectification需要一条新的completeWithRange(..., "exhausted")入口:probe===null && !datedMethodCollectOpen && methodCoverageAll且capability.canAdopt时不再走offerRangeWithoutAdopt,而是finish("adopt_representative")。- 否则 →
hostNarration= 范围句 + 门槛翻译句(acceptance_reasons→ 用户文案映射表放user-copy.ts:insufficient_events、insufficient_domain_diversity、low_date_quality、no_candidates;TS 侧keep_collecting的insufficient_dated_events/insufficient_domains同表);不持久化任何焦点;ensureNonTerminalTurnExit的"满足"条件加上"本轮已写出门槛翻译句"(用 turn 上的question=null && terminal_note标记,具体载体由执行方定,进度记录写明)。 route.ts无焦点分支的isNonConvergingRangeOffer同步改为上面两种出口。- 验收:形状回归见 A4;
grep -rn "USER_COLLECT_QUESTION.other\|collectQuestionRetryByDomain.other" frontend/src只剩开场路径;listUserVisibleCopy与agent-voice-copy-contract测试更新。
A3 停止入口。 rectification-agentic-chat.tsx:currentQuestion.kind === "collect_spoken" 时在输入框上方渲染与点选卡同款的 CHOICE_STOP_LABEL 按钮(复用 submitStop 路径,STOP_ACTION);RectificationReadonlyRange 改为按钮,同一动作。route.ts:ask_about_result 在点选与口述两个焦点分支都映射到 STOP_ACTION(口述分支现在只处理 decline,要补 stop 与 ask_about_result)。frontend/DESIGN.md 记录新按钮位置;文案对照 frontend/docs/VOICE.md。
- 验收:
rectification-v9-entry-routing或新测试覆盖ask_about_result→ paused;组件测试覆盖口述态出现停止按钮。
A4 事故形状回归(核心)。 新文件 frontend/tests/rectification-exhaustion-exit-20260906.test.ts,dossier 形状:7 条 confirmed 证据(education×2 month、relationship month+day、finance month、relocation month、health_pressure range)、family declined、occupation_note、answered_probes 6 条(2 条 varga_style、4 条 existence,含同域同年一月一年的两条)、discriminating_event_probes 全部已问或 no_split_among_active、receipt acceptance_allowed/selection_allowed/propose_allowed=true、confirmation_allowed=false。断言:
decideFromDossier不是collect_evidence;persistNextInterviewIfIdle不写collect:other:*焦点;hostNarration 含停止原因句与范围句。- 同形状但 receipt
acceptance_allowed=false, acceptance_reasons=["insufficient_events"]→ hostNarration 含门槛翻译句,不写焦点,ensureNonTerminalTurnExit不抛。 - 同形状但
candidateScoresFromDossier返回空(模拟投影不一致)→ 同 2,且日志行ranked_count=0。
B · 题型(BUG-559)
B1 同域同年去重(两侧)。 Python:_discriminating_event_probe_lists 接收 asked_probe_keys(由 API 请求体新增可选字段 asked_probe_keys: string[] 透传;normalize_rectification_request 允许该字段,不改 jyotish_api_server.py 主体),_existence_blocked_years(domain, known_years ∪ asked_years);_try_activation_probe 同样受阻。TS:remainingConflictProbes / renderableEventProbe / renderableContrastProbe 的 asked 判断改为按 domain.year 前缀匹配(career.2023.05.dasha_boundary 覆盖 career.2023*),并按 EXISTENCE_NEARBY_YEARS 扩到相邻年。
- 验收:
tests/test_rectification_event_probes.py新增:已问career.Y.05.dasha_boundary后不再产出career.Y.*、career.Y±1.*;TSrectification-choice-card.test.ts新增同样断言。
B2 "某年某月发生了什么"合并卡。 新 choice_kind: "boundary_pair":
- Python:
_partition_ranked_probes之后,对同 (year, month) 且领域不同的两条 existence 探针(取 gain 最高的两条)生成一条合并探针:semantic_key = boundary.<Y>.<MM>,component_probes = [key1, key2],style_options:A<dom1 event_family 短句>(→ p1 yes, p2 no)、B<dom2 短句>(→ p1 no, p2 yes)、C两件都有(→ 都 yes)、D都没有(→ 都 no);expected_outcomes按四个组合合并 supports/conflicts;information_gain取两条的联合分组熵(_group_entropy对四组)。原两条单领域探针从 public 列表移除,进dropped_probes(reason="merged_into_boundary_pair")。选项标签长度守LABEL_MIN/MAX,用DOMAIN_CATALOG里的event_family缩到 ≤ 12 字。 - 契约:
contracts/probe-question-v1.json加boundary_pair的answer_class映射(A=pair_first、B=pair_second、C=pair_both、D=pair_none,"这题跳过" = 两条 unsure);probe_question_contract.py与probe-question-contract.ts同步,契约测试比对字节。 - TS:
applyRectificationChoice对boundary_pair写两条answered_probes(各自semantic_key、answer_classyes/no),buildInferenceState不改;askedKeys同时记boundary.<Y>.<MM>与两条分量键(配合 B1)。 - 题干:
_agent_brief新分支:"时间范围锁定 {Y 年 M 月前后};请问用户那段时间身上发生了什么变化,选项由服务端给出;若账本里 ±2 个月内有其他领域事件,题干先提那件事再问。"method-followup.ts在user_prompt_hint里把邻近账本事件(只给领域与年月,不给 summary)拼进去。 - 验收:Python 测试——两条同月探针合并成一条、四个 outcome 的 supports 互不重叠且并集覆盖两条分量、原探针进 dropped;TS 测试——一张
boundary_pair卡答 A 后answered_probes两条、posterior 变化等于分别答 p1 yes + p2 no;契约测试;agent-voice-copy-contract更新。 - 若 B2 做不完(见 §6),至少 B1 + 题干锚点(邻近账本事件写进
user_prompt_hint)要落地。
B3 varga_style 卡要锚点。 rankRenderableDiscriminators / firstRenderableYearlessFollowup:choice_kind === "varga_style" 且同域账本无 confirmed 带年月事件 → 进 dropped_probes(reason="unanchored_varga_style"),不渲染;有事件时 user_prompt_hint 带该事件年月,selection_score 乘 0.8 排在带年月卡之后。
- 验收:
rectification-varga-style-*.test.ts新增:无同域事件不出卡;有事件出卡且 hint 含年月。
C · 收敛(BUG-560)
C1 prior 换尺度。 decision_policy.py::_relative_support(scores, floor):weight = max(score − floor, 0),floor = 全分钟网格(built["candidate_times"] 全部行,不只是公开代表)的最低分;全零时保持均分。可选 softmax 版本 exp((score − max)/T),两者都实现,由 C2 选定默认并记录在 POLICY_VERSION(bump 到 rectification-candidate-policy-v4;ALGORITHM_VERSION 同步 bump,test_algorithm_and_policy_versions_change_with_proximity_semantics 模式照抄)。rectification_report 与 candidate_summary 里所有引用 relative_support 的文案不变。
- 验收:
tests/test_rectification_diagnostics_clusters.py或新测试——同一 rows 下新支持度的 top-2 领先 ≥ 旧值;均分回退用例。
C2 校准(合入门)。 新脚本 scripts/rectification_prior_calibration.py(只读 references/real_case_calibration/minute_rectification_development_v1.json 调参、minute_rectification_holdout_v3.json 20 例验证;excluded_from_tuning=true 的例子只进验证):每例以真实分钟 ± candidate_radius_minutes 为窗口跑 score_candidates,分别按旧归一、score−min、softmax(T∈{0.25,0.5,1,2}) 计算 unionStillValidRange(lead 8)宽度与是否覆盖真实分钟。输出表:方案 × {holdout 覆盖数/20, 中位宽度, 均值宽度}。
- 通过标准:所选方案 holdout 覆盖数 ≥ 旧方案覆盖数 − 1,且中位宽度 < 旧方案;否则保留旧归一,C1 只留代码不启用,进度记录写明。
- 表格进
PROGRESS-*.md§C2,产品负责人确认后才合 C。
C3 存量。 POLICY_VERSION 变化会让 candidateSnapshotSource.scoringPolicyVersion 失配 → snapshotCurrent=false → 已有 Case 下一轮自动重算(TASK-rectification-tails-20260901.md 已铺路);执行方确认这条路径仍通,并在 docs/testing/ 清单里写"旧 Case 打开后第一轮会重算一次"。
C4 显示。 isRecommendedRectificationCandidate 沿用 lead 8,不改;RectificationCandidateCards 的"相对支持度 N"不改文案。进度记录附一张 4 件事 / 7 件事虚构样例的前后支持度对比表(用 §2.3 的虚构资料即可)。
D · 记录
docs/BUG_HISTORY.md:BUG-558(出口)、BUG-559(题型与去重)、BUG-560(prior 压平);BUG-560 关联 BUG-463 与TASK-rectification-provisional-adopt-20260901.md§2。CHANGELOG.md:三条用户可感知变化;Skill 版本不 bump(本单不改 SKILL.md;文案单另开)。docs/tasks/PROGRESS-rectification-convergence-exit-20260906.md:A/B/C 各一节,含测试数字、C2 校准表、偏离。docs/testing/rectification-convergence-exit-20260906.md:真实环境清单——(1) 走完七个领域后出现候选卡或门槛翻译句,不再出现"也可以再说一件事";(2) 口述采集态有"先这样"按钮,点了出范围;(3) 同一年月不再被问两次;(4) 合并卡题干带年月并提到邻近事件;(5) 旧 Case 首轮重算一次。docs/tasks/README.md状态板加一行。
6. 让步顺序
- A1 + A2 + A4 不可拆,是本单的底线;A3 可以后置到同分支第二次提交。
- B1 + B3 必做;B2 若一天内做不完,改为"B1 去重 + 题干锚点",并在进度记录写明 B2 未做与原因,本单状态保持"部分完成",不得标完成。
- C1 + C2 必做;C2 不过门就不启用 C1,按 §5.C2 写明。
- D 不可省。
7. 开工前置命令
git fetch origin --prune
git worktree add -b codex/rectification-convergence-exit-20260906 .worktrees/rectification-convergence-exit-20260906 origin/staging
cd .worktrees/rectification-convergence-exit-20260906
ln -s /workspace/Jyotisha/frontend/node_modules frontend/node_modules
ln -s /workspace/Jyotisha/.venv .venv
# 基线数字(写进进度记录)
cd frontend && npx tsx --test tests/rectification-*.test.ts tests/agent-voice-copy-contract.test.ts 2>&1 | grep -E "^# (tests|pass|fail)"; cd ..
.venv/bin/python -m pytest tests/test_rectification_event_probes.py tests/test_rectification_engine_convergence.py tests/test_rectification_diagnostics_clusters.py tests/test_active_rectification_api.py -q
grep -o "^## BUG-5[0-9][0-9]" docs/BUG_HISTORY.md | tail -1
收尾:同一组命令 fail=0;./node_modules/.bin/tsc --noEmit;npm run lint;npm run build 后 / 仍 Static、首屏 gzip ±2%;.venv/bin/python scripts/run_quality_gate.py --profile quick。
附录 · 复现脚本(虚构资料,可直接跑)
# .venv/bin/python 下运行;出生资料为虚构值,仅复现机制
import uuid, sys, os
sys.path.insert(0, "."); sys.path.insert(0, "scripts")
from scripts.rectification.contracts import normalize_rectification_request
from scripts.rectification.api_service import score_candidates
EV = [("education","education_start","2016-09-01","2016-09-30","month"),
("education","education_completion","2020-06-01","2020-06-30","month"),
("relationship","relationship_start","2024-05-01","2024-05-31","month"),
("relationship","relationship_end","2024-08-08","2024-08-08","day"),
("finance","finance_loss","2026-01-01","2026-01-31","month"),
("relocation","relocation","2023-07-01","2023-07-31","month"),
("health_pressure","pressure_period","2026-01-01","2026-08-31","range")]
req = normalize_rectification_request({"birth_date":"1998-03-15","start_time":"04:30","end_time":"05:30",
"lat":39.9042,"lon":116.4074,"tz":8.0,"events":[{"id":str(uuid.uuid4()),"domain":d,"event_kind":k,
"date_start":s,"date_end":e,"precision":p,"summary":k} for d,k,s,e,p in EV]})
out = score_candidates(req)
print([(c["time"], c["relative_support"]) for c in out["candidate_decisions"]])
print([(p["semantic_key"], p["information_gain"]) for p in out["decision_receipt"]["discriminating_event_probes"]])
验收(Claude,2026-09-06,基线 origin/staging @ b938c76a)
实现 150d7ef1(A)/ 3a9ae736(B)/ 62afa521(C+D)已由执行方直接合入 staging。独立复跑:
| 门 | 结果 |
|---|---|
tsc --noEmit |
0 错 |
npm run lint |
0 error / 82 warning(新增测试文件带 1 条既有类型的 warning) |
非 DB tests/rectification-*.test.ts + agent-voice-copy-contract |
905 pass / 0 fail |
scripts/rectification_prior_calibration.py |
与进度记录同数:holdout proportional 17/20 · 中位宽 21;offset 12/20 · 10;softmax 0.25/0.5/1/2 = 10/11/12/16 · 6/9/14.5/21;无方案过门 |
| Python 定向(event_probes / engine_convergence / relative_support / v5_services / diagnostics_clusters / active_rectification_api) | 1 失败:test_long_real_conversation_reaches_vedastro_after_local_range_is_narrow,在基线 e2f4b55c 上同样失败(已复跑),非本轮引入 |
逐条结论:
| 项 | 结论 |
|---|---|
| A1 日志 | 通过。字段齐全,无 summary / evidence;probe_key 含探针年份(大运边界年,非出生资料),可接受 |
| A2 穷尽即交付 | 未通过(P1):门槛翻译句路径每个回合写 2 条相同助手消息(scratch 计数 2;evidence 动作回合为 3;点选路径一条独立 + 一条拼在「已记录你的选择」后)。见修复单 BUG-565 |
| A2 决策层 | 通过。keep_collecting 仍采集;probe_pool_exhausted 停止原因进 finish |
A3 口述停止 + ask_about_result |
通过(服务端 STOP 在采集焦点上不抛,answerClass=null 分支放行)。P2:范围小字改成按钮但文案仍是状态句,与新按钮重复,建议还原为状态句 |
| A4 形状回归 | 通过,三例齐 |
| B1 同域同年去重 | 通过,两侧生效,asked_probe_keys 不进指纹 |
| B2 合并卡 | 未做,按让步顺序允许,状态「部分完成」正确 |
| B3 varga 锚点 | 通过 |
| C1/C2 | 通过(默认未启用;校准数字复现一致)。校准门本身有缺陷:旧方案"覆盖 17/20"是因为它根本不收窄(中位宽 21 = 整个 21 分钟窗口),拿它当基线任何收窄方案都过不了。真正的发现是:把先验拉尖后,真实分钟落在领先集合里的只有 12/20(宽 10),接近随机(10/21≈48%)。引擎原始分在分钟级的区分力近乎没有,这是产品级结论,见修复单 §2 |
| C3 | 通过(未 bump 版本,旧 Case 不重算,清单已按此写) |
| D | 通过;BUG-560 标 blocked 正确 |
| 新问题 | P2:persistNextInterviewIfIdle 新增的「无剩余采集且不可采用 → 穷尽」分支排在问题持久化之前,引擎采用门关闭但仍有可渲染区分卡 / holdout 题时会把题吞掉 |