Two real local sessions are archived. In them the agent states twice that its minute came from a report already in the upstream repo rather than from the user's answers, and the follow-up "fix" wired that answer into the scoring chain; upstream now carries a Narayana tie-break derived from that single case. This repo was checked and is clean, so two red lines now keep it that way and gate any upstream sync. The same records show what the answers genuinely bought: a 30-minute window narrowed to 3. Tasks 7-10 add what makes that deliverable — the per-answer narrowing table, an explain surface, mid-case window changes with evidence retained, and the batched first round. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0155nFCgCHtoA7jhSDGmZmMu
30 KiB
任务书 · 生时校正收敛重构(2026-08-30,v2)
基线:origin/staging @ 7db2dd2d。
下面所有行号只是线索,请按选择器/函数名定位,后续提交可能让行号偏移。标注为「上游」的行号来自对 /workspace/yinduzhanxing 的一次调研,动手前请自行复核。
v2 说明:v1 的判断是「线上打分器可能不如本地方法学,需要三方盲测决定是否换引擎」。对上游仓库
/workspace/yinduzhanxing做完比对后,这个判断被推翻了。换引擎的路线已排除,本版的落地点比 v1 小一个数量级。v1 的任务 0(三方盲测)作废。
为什么要做
事实 1 · 方法学没有丢,也没有落后
/workspace/Jyotisha 与上游 /workspace/yinduzhanxing 同源(共同初始提交 a5bbba28,2026-04-20),分叉于 825a9ea5(2026-07-17)。分叉后上游 808 个提交、本仓 1,275 个提交,互相同步 0 次。
但生时校正方法学的三份文件 cmp 逐字节相同:
IDENTICAL birth-time-rectification-advanced.md
IDENTICAL birth-time-rectification-decision-tree.md
IDENTICAL birth-time-rectification-cases.md
(yinduzhanxing/references/ ↔ Jyotisha/skills/jyotish-vedic-astrology/versions/6.9.14/references/)
上游六周里在校正上新增的是治理、契约与复现文档,不是方法学本身。「本地版本更新」不成立。
事实 2 · 三套打分链路是同一算法家族
Jyotisha/scripts/rectification/scoring_service.py:11直接from scripts.active_rectification_event_engine import ...—— 线上 v5 调用的就是上游那个事件引擎。Jyotisha/scripts/minute_rectification_fact_ranker_v4.py:16import 同一个DOMAIN_CONFIG。- 两边共享同一套权重:Vimshottari MD/AD/PD =
2.0 / 1.5 / 0.75,Narayana MD/AD =2.0 / 1.0。
因此 references/rectification_sealed_holdout.v1.json 那组数字(top_1_rate 0.15、mean_absolute_minute_error 6.95、confirmation_coverage_rate 0.0)虽然测的是 v4,对线上 v5 是弱但真实的先验,不能当作无关数据。上游也没有更好的打分器可换。
事实 3 · 真正丢掉的是「停下来」的机制
上游三条链路每一条都天然终止:
| 上游链路 | 终止方式 |
|---|---|
scripts/active_rectification_questions.py |
QUESTION_TEMPLATES 是写死的 8 题分 3 轮;问完 workflow_status 转 dated_event_review_required(上游 :537) |
scripts/active_rectification_events.py |
adjudicate_candidate_rows() 一次性批量裁决(上游 :212),算完直接给 low/medium/high |
jyotish-app/rectification-engine.js |
浏览器内一次算完给置信度标签,无追问 |
且上游从不承诺分钟:can_apply 默认 False、truth_status: not_birth_time_truth 写死在代码里。
Jyotisha 把这套替换成了服务端逐轮动态生成 discriminator probe 的无界循环。
事实 4 · 无限访谈的机械解释
生产判定 frontend/src/lib/rectification-agentic/core/rectification-decision.ts:144-149:
if (!separation.sufficient) {
if (probe && !userStopped) return discriminate(...); // 只要有探针就继续问
return completeWithRange(..., "offer");
}
这条路径里没有任何轮次计数器。 同时:
confirmation_coverage_rate: 0.0—— sealed holdout 的 20 例里零例达到确认门,即门几乎不会开。- probe 的
semantic_key按domain.year.month构造,随候选集每轮重新生成,实际不会耗尽。
门永远不开 + 探针永不耗尽 = 无限访谈。 这是全部 stall 类 bug 的根因。
事实 5 · 8 轮上限算了,但没人拿它当终止条件
不要误以为它是死代码。生产链路确实在跑它:
mastra/rectification-v9-tools.ts:945 → buildCaseInferenceState
→ v9/inference-adapter.ts:248 → buildInferenceState
→ core/build-state.ts:162 → evaluateConvergence
→ core/convergence-evaluator.ts:54 maxRounds = state.max_rounds ?? DEFAULT_MAX_DISCRIMINATION_ROUNDS
→ core/types.ts:10 DEFAULT_MAX_DISCRIMINATION_ROUNDS = 8
max_rounds 没有被传入,取默认 8,算出的 result_status 也写进了 state。但 rectification-decision.ts 的判定不读它。
所以修法不是「启用死代码」(去改 convergence-evaluator.ts 改了也没用),而是把轮次预算接进 rectification-decision.ts 的判定输入。
而且终止分支已经存在:rectification-decision.ts:158 就有 completeWithRange(separation, holdout, range, "exhausted"),:336 的 kind 枚举也已支持 "exhausted"。代码路径是现成的,只是没人触发。
另有一套完整但闲置的停机策略:frontend/src/lib/birth-time-dynamic-stop-policy.ts:43-48(高置信 / 答满 10 条 safety cap / 平台期 2 轮 / 零信息增益 / 重复分区),属旧链路,V9 不走它。
事实 6 · 上游同样没有盲测证据
yinduzhanxing/references/real_case_calibration/timing_rectification_*_2026_07_23.json:
blind_replay_allowed = false
ready_case_count = 0 (minimum_case_count = 3)
reviewer_2_confirmed_count = 0
status = blocked_until_human_labels
production_tuning_allowed = false
progress.md:1611 — "zero cases ready for blind replay"
birth-time-rectification-cases.md 里的奥巴马/居里夫人案例,是拿已知出生时间回验事件并自评「吻合度 95%」——知道答案的复盘,不是校正测试。
两边都没有盲测数据。产品侧「本地能收敛」的观察,最可能是定义差异:上游问完固定题数就给候选区间并停下,那个体验就是「收敛」;按代码它给的一定是区间 + low/medium/high 标签,不是确定的分钟。
结论
产品目标定为:交付收窄后的区间,不交付唯一分钟。 唯一分钟保留为 sealed holdout 通过后才开启的路径。
要做的是把上游那套「问完就停、停下来时坦白交代」的产品契约接回来,不是重写收敛逻辑,更不是换打分引擎。
硬红线
- 任务 0 是门控,但它只是一次信息收集,不许写代码。 在拿到上游访谈手册与一份真实会话记录之前,不得改动
rectification-decision.ts的判定。 - 诚实性不可让渡。 不得为了让访谈「看起来能收敛」而放宽
confirmation-gate.ts的任何 blocker,不得降低SEALED_MINUTE_HOLDOUT门槛,不得在confirmation_allowed=false时宣称唯一分钟。这套 fail-closed 机制现在正在正确工作。 - 禁止答案回流。 任何以已知答案调出来的规则,不得进入生产打分链路,只能存在于标注
regression_only且被评测明确排除的夹具里。上游已经发生过一次(见任务 0 补充),本仓当前干净,必须保持。 - 上游同步必须过污染审查。 不得同步任何带
pl9_1993、observed_*_case_only、target_minute标记的打分逻辑,特别是上游scripts/narayana_dasha.py:601-613的 tie-break。任务 5 的同步机制必须包含这道检查。 - 超预算的出口必须是「交付区间」,不是「失败」。 走
completeWithRange(..., "exhausted"),用户拿到候选区间与明确的边界说明。任何把超预算实现成报错、静默停止或空白页的做法都是错的。 - 不得用 holdout 调参。
minute_rectification_holdout_v3的source_audit_status是invalidated_after_replay,只能当开发集看趋势,不得作为发布指标,不得写回 sealed 字段。v4_intake的production_tuning_allowed保持false。 - 不要引入第五套打分器。 不要移植
jyotish-app/rectification-engine.js的 40/35/15/10 权重。它信号更弱(无 Narayana / Arudha / Ashtakavarga / Shadbala)且从未评测。 - 状态机留在 Postgres 函数里,沿用既有模式。不得把状态机搬进应用层。
- 不得修改既有测试断言 —— 除非该断言锁的正是本轮要改的缺陷本身;那种情况必须在断言上方注明原值与原因,并在 PROGRESS 单列。
- 推 staging 前必须
./node_modules/.bin/tsc --noEmit通过。不要用npx tsc,本仓库环境下会装到空包tsc@2.0.4。 - 数据库测试必须真跑。
npm run test:db需要 Docker。没有 Docker 就不要推 —— 写进BLOCKED.md并停下。 - 不得改
.gitea/workflows/**。不得在有未提交改动的工作树上切分支。不得自行把 staging 提升到 main。
让步顺序:诚实性不回退 > 数据不损坏 > 功能与测试不回归 > 可验证的收敛改进 > 代码整洁 > 成本。
开工前置
git fetch origin --prune
git worktree add -b codex/rectification-convergence-20260830 \
../.worktrees/rectification-convergence-20260830 origin/staging
读 pre_work_error_ledger.md,跑 scripts/pre_work_check.py,读 frontend/AGENTS.md。改前在 docs/BUG_HISTORY.md 检索校正相关记录(30 条以上,停滞类那几条务必读完)。
先读这些再动手:
frontend/src/lib/rectification-agentic/core/rectification-decision.ts—— 生产判定,本轮主战场frontend/src/lib/rectification-agentic/core/convergence-evaluator.ts+core/build-state.ts:162—— 已在跑但没被消费的轮次判定frontend/src/lib/birth-time-dynamic-stop-policy.ts:43-48—— 现成但闲置的停机策略frontend/src/lib/rectification-agentic/v9/spoken-answer.ts—— 101 条清洗正则,任务 2 要删frontend/src/mastra/agentic-rectification.ts:61-68—— 系统提示,任务 2 要重写第 4 条- 上游
/workspace/yinduzhanxing/scripts/active_rectification_events.py:115-179—— 任务 3 要移植的build_candidate_result_summary() - 上游
/workspace/yinduzhanxing/jyotish-app/rectification-collaboration.js—— 四阶段状态机,参考它有多简单
任务 0(已完成 2026-08-31)· 上游访谈手册与阈值文档
三份文件已获取,归档于 references/upstream/(来源:上游维护者本地 skill 包 ~/.workbuddy/skills/jyotish-birth-time-rectification/,获取日期 2026-08-31):
references/upstream/interview_playbook.mdreferences/upstream/evidence_thresholds.mdreferences/upstream/test_birth_time_rectification_skill_contract.py
结论:上游的「收敛」= 从不试图确认分钟
1. 停止规则与本仓方向相反。
interview_playbook.md 的 Stop Rules 原文:
Stop and report the current interval when any is true:
- fewer than three dated events
- fewer than two domains
- tied first-place candidates
- user uncertainty is too high to map support/conflict reliably
即:证据不足 → 停下来,交付当前区间。
本仓 frontend/src/lib/birth-time-evidence.ts:188-189 把 minConfirmationEvents(4) / minConfirmationDomains(3) 当作确认门,达不到 → 继续问。
同一类阈值,上游当「可以开始交付的地板」,本仓当「可以下结论的天花板」。前者三轮内必达,后者按 sealed holdout 的 confirmation_coverage_rate: 0.0 是零达成。这是无限访谈的最终解释。
2. 上游的标签梯子没有「已确认」这一档。
evidence_thresholds.md:blocked → user_history_verification_required → manual_pattern_consensus → single_adapter_support → multi_adapter_consensus。顶端自注 "this is still not birth-time truth";standalone_core 模式天花板更低,只到 manual_pattern_consensus。
test_birth_time_rectification_skill_contract.py 中每一个 case(含最高档 main_repository_enhanced)都断言:
assert receipt["claim_status"] == "candidate_range_not_birth_time_truth"
3. 提问节奏是硬的三轮。
第 1 轮 3–5 道 A/B/C/D 选择题;第 2 轮只要最强 domain 的带日期事件;第 3 轮只问能区分 top 候选的 1–3 个事件。手册明写 "Never collect a long autobiography before the first route decision."
4. 终止话术是写死的。
当前最优结果是候选时间段,而不是已经确认的唯一出生分钟。临时代表时间仅用于下一轮验证与比较。
补充(2026-08-31)· 两份真实会话记录证明「本地收敛」源于答案泄漏
会话记录已归档:references/upstream/1993-session-repeated-questions.txt、references/upstream/1993-session-correction-rerun.txt。
Agent 在记录中两次自陈:
我明显受到了仓里这份 1993 案例的现成高质量材料影响…不是纯靠你当下问答独立推出来的。
14:49 这个点,主要还是被仓里的 1993 已知校准档锁住了,不是单靠这些回答「算出来」的唯一真值。
答案原本就写在上游 docs/research/qizheng_1993_native_current_report_2026_08_26.md(1993-04-17 14:49:00)。随后的「修正」是把答案接进打分链路(pl9_1993_user_case,target_minute = 14:49,提交 3bb90620)。进一步核查发现上游打分器里已有按该案例调出的规则:
scripts/narayana_dasha.py:601 "selection_levels": ["observed_pl9_1993_gemini_sagittarius_tie_break"]
scripts/narayana_dasha.py:613 evidence_status = "observed_pl9_1993_case_only"
本仓已核查未受污染(全仓 grep pl9_1993 / target_minute / 14:49 无相关命中)。红线 3、4 用于保持这一状态。
但记录同时证明了真实能力:用户的 8 条生平锚点把窗口从 14:30–15:00(30 分钟)压到 14:48–14:50(3 分钟)。收窄是真的,选定分钟不是。 这与 mean_absolute_minute_error: 6.95 及上游标签梯子顶端「still not birth-time truth」完全一致,并再次确认产品目标应为交付区间。
对已完成任务的影响
任务 1 实现的 budgetExhausted()(轮次 8 / 答题 10 / 平台期 2)方向正确但只覆盖了一半——上游是证据状态型停止且三轮即止。差额由任务 6 补齐。
任务 1(P0)· 把轮次预算接进生产判定
事实
见事实 4、5。rectification-decision.ts:144-149 的 !separation.sufficient 分支只要有 probe 且用户没喊停就无限 discriminate。轮次判定在别处算好了但没人读。
要做什么
- 给
rectification-decision.ts的判定输入增加预算三件套,参数一律取自已有常量,不要新造数字:- 轮次上限 ——
core/types.ts:10的DEFAULT_MAX_DISCRIMINATION_ROUNDS = 8 - 平台期 ——
references/rectification_policy.v1.json的maxPlateauRounds(当前只有scripts/rectification_policy.py读了,前端没用) - 安全帽 ——
birth-time-dynamic-stop-policy.ts:45的effectiveAnswerCount >= 10
- 轮次上限 ——
- 在
!separation.sufficient分支的discriminate(...)之前插入预算检查。超预算时走已存在的completeWithRange(separation, holdout, range, "exhausted")(:158)。 - 不要去改
convergence-evaluator.ts。 它算得没错,问题是没人消费。要么把result_status传进判定输入,要么在判定层独立计数——二选一,在 PROGRESS 说明选了哪个及理由。 - 轮次计数必须服务端持久化,不得靠对话历史推断。沿用既有的 receipt / turn 计数来源。
属性测试(本任务的核心交付)
在 frontend/tests/ 新增:
- 可终止:从任意状态出发,反复喂
discriminate返回的 probe 的答案,必须在 ≤ 预算轮数内到达某个completeWithRange分支。不存在无限 discriminate 循环。 - 拒答不卡死:用户对每一个 probe 都拒答时必须到达终态,不得停在
discriminate。 - 超预算即交付:超预算时返回的是
"exhausted"且带候选区间,不是错误或空结果。 - 单调性:追加证据后区间宽度只能变小或不变,永不变大。
这四条一次性锁死未来所有 stall 类 bug。
验收
- 四条属性测试全绿。
- 停滞类既有测试(
rectification-collect-stall.test.ts、rectification-range-offer-deadend.test.ts等)全绿。 - 端到端:一个用户即使一直不给有效证据,也会在预算内拿到区间并结束。
任务 2(P0)· 提问权收归服务端,模型降级为一句确认
事实
一条消息目前有四个作者:服务端决定问什么(v9/method-followup.ts,2,036 行)→ 模型写正文 → 服务端把题干接在正文之后 → UI 再渲染选择卡。
于是 src/mastra/agentic-rectification.ts:66 要求模型判断自己处在 choice / collect_spoken / 无持久化问题 三种状态中的哪一种再区别行动。模型做不到,所以有了 v9/spoken-answer.ts:33 条内部标识符正则 + 68 条中文过程话术正则,共 101 条,事后擦模型漏出的内心戏。每一条正则都是一个曾经发生过的 bug。
要做什么
- 模型永远不提问。 一轮里它的唯一职责是针对用户刚说的内容写一句确认/承接。系统提示第 4 条整条重写,删掉所有关于题干归属的条件分支。
- 问题永远由服务端渲染,永远出现在同一个问题区。
choice与collect_spoken合并成一个「问题槽」:有选项就可点,没选项就是输入框加提示。数据模型不再区分 kind,UI 不再有两条渲染路径。 - 取消「服务端把题干接在模型正文之后」的拼接。 一条消息 = 模型的一句确认(流式)+ 服务端问题区(结构化渲染),两者物理分离,各自唯一作者。
- 删掉
spoken-answer.ts的 101 条清洗正则。 如果删完发现仍需清洗,说明第 1 步没做干净,回去改第 1 步,不要把正则加回来。 - 服务端未产出问题时问题区为空 —— 这是可断言、可监控的显式状态,不是静默停滞。加服务端告警。
验收
- 用户可见文本不可能出现内部标识符或过程话术,且不是靠正则保证,是靠模型输出面收窄保证。
- 端到端连续 10 轮,问题始终在问题区,位置一致,不重复、不消失。
spoken-answer.ts的两条大正则不再存在于代码库。
任务 3(P0)· 恢复「停下来时怎么交代」的契约,交付物从第 0 轮就存在
事实
上游 active_rectification_events.py:115-179 有 build_candidate_result_summary(),产出四个面向用户的 next-step code:
collect_at_least_five_events
resolve_candidate_tie_or_narrow_window
narrow_window_before_minute_claim
do_not_apply_as_birth_time_truth
Jyotisha 把这整段删了。 于是访谈终止时没有话术锚点,只能继续问。
上游的降级阈值 Jyotisha 其实完整继承在 references/rectification_policy.v1.json(4 事件 / 3 域 / 5 分钟宽度 / 20% margin)——判据没丢,丢的是「达不到时说什么」。
要做什么
- 把上游的
build_candidate_result_summary()恢复到Jyotisha/scripts/active_rectification_events.py,让scripts/rectification/api_service.py的score_candidates()返回体带上next_step_codes与stability.label。 - 定义校正报告投影,随时可读:结论区间 + 代表分钟(明确标注为代表性、非唯一解)、置信度、逐条证据(事件 → 支持哪个候选 → 用哪个方法)、被排除的候选与理由、局限声明(复用
confirmation-gate的 blocker 文案,不要另写一套)。 - 第 0 轮就存在:证据 0 条时区间 = 用户声明的出生窗口。每答一题重算。
- 用户任何时刻可查看、可导出、可主动结束。结束即交付当前版本,不算失败。
- UI 给出区间收窄进度(
±120 → ±45 → ±18 → ±7)。这是用户为这笔钱买到的东西的可视化。 - 计费与交付对齐:产生过至少一版报告即为有效交付,
exhausted不是失败态。具体金额由TASK-billing-pricing-20260830.md决定,本任务不写任何金额。
验收
- 新 case 在零证据时即可读出一份报告。
- 每轮答题后区间单调收窄或不变。
- 用户主动结束或超预算 → 拿到报告,状态是完成不是失败。
next_step_codes出现在服务端返回体里,并被终止话术使用。
任务 4(P1)· 解决标定数据饥荒
事实
两边都没有盲测数据(事实 6)。Jyotisha 的 v4_intake blind_holdout_eligible_case_count = 0,上游 ready_case_count = 0。这是产品能不能承诺唯一分钟的唯一瓶颈,且它是数据问题不是工程问题。
上游停在这里六周了,指望它自己解决不现实。
要做什么
做一个免费的「验证我们」入口,面向已经知道自己准确出生时间(有出生证明/医院记录)的用户:
- 用户提供出生日期、地点、事件;准确时间单独收集并对打分链路屏蔽,全程不得进入候选生成或打分。
- 系统盲跑校正,出结果后再与真实时间比对,把偏差当场展示给用户。
- 每一例都是一条合格标定数据,用户同时拿到一次免费的、可验证的能力演示。
一个入口同时解决三件事:标定数据来源、最有说服力的社会证明(「我们在 N 个有出生证明的案例上盲测过,中位偏差 X 分钟」)、获客内容。
硬约束
- 真实时间必须在架构上不可能泄漏进打分链路,不是靠约定。写负向测试锁死。
- 案例默认进 intake 队列,晋级为冻结 holdout 必须走既有流程:新版本号、通过源审计、打分身份在盲测前冻结。
- 免费入口要有独立的滥用限制,不得复用咨询的公平使用配额。
- 与上游共享数据前先对齐口径,避免两边各建一套互不承认的标定集。
验收
- 真实时间泄漏的负向测试存在且通过。
- 新案例正确落入 intake 队列,不会被自动当成 holdout。
- 用户侧能看到「预测 vs 真实」的偏差对比。
任务 5(P2)· 清理与同步机制
任务 1–3 上线并稳定运行两周后才做。
- 两代数据模型并存:
birth_time_rectification_*17 张 +agentic_rectification_*16 张 = 31 张,是 50 个校正迁移的主要来源。逐张确认旧表是否仍被 RPC 写入,无引用的归档删除。 - 删掉永不执行的分支:
method-followup.ts注释写明第 5 项(外貌体质)与第 6 项(胎记疤痕)skipped; never asked。 MIN_SEPARATION_LEAD与上游口径对齐:core/candidate-separation.ts:7当前是绝对分差 8;上游active_rectification_events.py:256用的是相对 margin(< 10% 降级)。改成相对量后用minute_rectification_holdout_v3的 20 例看not_separated比例变化 —— 只看趋势,不作发布指标(红线 6)。- 建立与上游的同步机制。 六周零同步是本轮很多问题的背景。约定一个节奏(例如每月一次),至少同步
references/、scripts/active_rectification_*、方法学 md。这不是代码任务,是流程约定,写进AGENTS.md。 - 每删一张表、每并一个模块单独一个提交,便于二分回滚。
不要动
打分引擎与 Swiss Ephemeris 计算、证据账本、Postgres 里的状态机、sealed holdout 机制、rectification_policy.v1.json 的阈值。这些都是对的。
任务 6(P0)· 接过上游的终止语义
事实
见任务 0 结论。任务 1 已接入轮次型预算,但上游的终止是证据状态型的,且 exhausted 在上游是正常终态而非兜底态。
要做什么
-
增加证据型停止规则,与既有轮次预算并列(任一命中即停并交付):
- 带日期事件 < 3
- 覆盖 domain < 2
- 并列第一(已有
separation可判) - 用户不确定度过高,无法可靠映射 support/conflict
阈值取
references/upstream/evidence_thresholds.md的 Minimum Standalone Gate(3 事件 / 2 域),不要沿用本仓的 4/3——那组是确认门,语义不同,两者必须分开保留,不得互相覆盖。 -
把
exhausted从兜底态重新定位为正常终态。 改命名与全部用户可见文案:走到这里是「交付完成」,不是「很遗憾没能完成」。计费上它必须是有效交付(与TASK-billing-pricing-20260830.md对齐)。 -
引入上游的 label ladder,替换「确认唯一分钟」作为默认目标:
blocked → user_history_verification_required → manual_pattern_consensus → single_adapter_support → multi_adapter_consensus唯一分钟确认降级为 sealed holdout 通过后才开启的路径,不得作为访谈的默认终点。既有confirmation-gate.ts的 blocker 保持不变(红线 2),它现在正确地永不放行。 -
采用上游的终止话术,逐字使用任务 0 结论第 4 条那句。它是上游打磨出的合规表述,不要另写。
-
收紧提问节奏到三轮形态:第 1 轮 A/B/C/D 选择题批量出,第 2 轮只要最强 domain 的带日期事件,第 3 轮只问能区分 top 候选的 1–3 个。当前 8 轮预算作为外层熔断保留,不作为正常节奏。
验收
- 证据型停止规则有单测:3 事件以下、2 域以下、并列第一各自触发终止并交付区间。
- 4/3 确认门与 3/2 交付地板在代码中是两个独立常量,不互相覆盖,注释写明语义差别。
- 用户可见文案中不存在把正常终态描述为失败的表述。
- 默认路径下系统不再以「确认唯一分钟」为目标;相关文案与 label 均出自新梯子。
- 既有校正测试与任务 1 的四条属性测试全绿。
任务 7(P0)· 逐条回答 → 区间收窄的归因表
事实
references/upstream/1993-session-correction-rerun.txt 里,整场对话最有价值的产物是用户主动索要的那张表:
| 你的回答 | 压缩后的分钟区间 | 作用 |
|---|---|---|
| 2017 入职,岗位内容不合 | 14:48–14:50 | 很强的收窄点 |
| 2023 末起量,一改风格就稳 | 14:48–14:50 | 最接近最终分钟的一条 |
本仓没有这个概念。v9/evidence-model.ts 与 core/*.ts 中不存在 narrow / contribution / attribution 语义,只有「这条证据支持哪个候选」,没有「这条回答把区间从 X 压到了 Y」。
要做什么
- 每次证据写入后记录区间快照:写入前宽度、写入后宽度、以及该条证据的归属贡献。持久化,不靠事后重算。
- 报告(任务 3)中新增归因表:用户原话(
display_date_label口径)→ 收窄前后区间 → 一句作用说明。 - 收窄贡献必须来自服务端打分,不得由模型叙述生成。模型只做措辞。
- 无贡献的证据也要列出并标注「未产生收窄」——这本身是诚实性的一部分。
验收
- 报告含归因表,条目数等于已确认证据数。
- 每条的收窄前后宽度与该轮实际候选集一致,可复核。
- 零贡献证据被显式标注,不被静默丢弃。
任务 8(P1)· 「为什么是这个结论」的质疑通道
事实
会话记录里用户问了一句「你是信息记忆联想了吗?还是靠印度占星专业技术推理出来的?」——这一问改变了整场对话,也是全部真相的来源。
本仓没有任何「为什么」入口。v9/choice-card.ts 的 why 字段是每个选项的说明,不是「这个结论怎么来的」。
要做什么
- 结论旁提供一个显式入口,回答三件事:用了哪些证据、用了哪些方法层(dasha / D9 / D10 / D12…)、当前还有哪些 blocker 未解除。
- 内容全部来自服务端已有的
claimCards/evidenceRefs/confirmation-gateblocker,不得由模型即兴生成。 - 必须能诚实回答「这个结论有多少来自你的回答」——若某个结论主要由先验或默认值决定,要说出来。
验收
- 入口可达,内容可复核到具体证据 ID 与方法层。
- 关闭 blocker 前后,解释内容随之变化。
- 解释文本中不出现内部标识符(与任务 2 的输出面约束一致)。
任务 9(P1)· 中途改窗口与保留证据重跑
事实
1993-session-correction-rerun.txt 整篇的主题就是「默认只把 14:30–15:00 当作未知区间,重新跑一轮」。
本仓 declared_window 在开 case 时从 profile 读一次(v9/case-service.ts:42-43、rectification-agentic/session.ts:194-195),未找到中途重定义或保留证据重跑的路径。这是高频真实场景:家人过后又想起更准确的时段。
要做什么
- 支持在 case 进行中重新声明窗口,并基于已有证据重新扫描打分,不要求用户重答。
- 重跑必须留痕:旧窗口、新窗口、触发原因、重跑前后的候选与区间,进 receipt。
- 与计费对齐:同一 case 内重跑不重复扣费(沿用
rectification:case:<caseId>幂等键)。 - 若新窗口与既有证据严重冲突,如实呈现冲突,不得静默丢弃证据。
验收
- 改窗口后候选重算,已确认证据全部保留。
- 重跑留痕可查。
- 重跑不产生第二次扣费。
任务 10(P1)· 第一轮批量出题
事实
src/mastra/agentic-rectification.ts:68 第 6 条硬编码「一次一问」。
上游 references/upstream/interview_playbook.md 的节奏是第 1 轮 3–5 道 A/B/C/D 一起出,会话记录里也是「你直接回 A/B/C 就行」。一次一问 × 8 轮摩擦大、轮次消耗快,直接加剧不收敛。
要做什么
- 第 1 轮改为批量出 3–5 道 A/B/C/D 选择题,一次呈现,用户可逐条或一次性作答。
- 第 2、3 轮维持单问(只要最强 domain 的带日期事件 / 只问能区分 top 候选的 1–3 个)。
- 系统提示第 6 条随任务 2 一并重写;批量题的题干与选项由服务端产出(与任务 2 的问题槽一致),模型不参与。
- 批量轮在轮次预算里记为一轮,不是 3–5 轮。
验收
- 第 1 轮呈现 3–5 题,作答后进入单问节奏。
- 轮次计数正确。
- 与任务 1 的四条属性测试不冲突。
交付
- PROGRESS 写进
PROGRESS-rectification-convergence-20260830.md,任务 0 的会话记录结论单列。 - 每个任务一个提交。
- 推 staging 前:
./node_modules/.bin/tsc --noEmit通过、npm run lint无新增 error、npm run test:db在 Docker 下 fail=0、npm run build通过。 - 全量
./node_modules/.bin/tsx --test tests/*.test.ts的失败清单与基线逐条比对,确认无新增。