diff --git a/docs/tasks/README.md b/docs/tasks/README.md index e6b22483..0f02657f 100644 --- a/docs/tasks/README.md +++ b/docs/tasks/README.md @@ -96,6 +96,7 @@ | `TASK-rectification-followups-20260909.md` | `PROGRESS-rectification-followups-20260909.md` | 验收补漏:申报时段拦截只看钟点样式,带钟点的经历(『20:00 左右分手』『3 点到 5 点被车撞』)会被吞(BUG-631);by_time 只算引擎前 9 个候选,一小时窗 17 个候选时卡片列写『还没对照』(BUG-632) | 待验收 | `codex/rectification-followups-20260909`(BUG-631~632) | | `TASK-rectification-evidence-turn-empty-answer-20260910.md` | `PROGRESS-rectification-evidence-turn-empty-answer-20260910.md` | 证据轮模型无正文被判整轮失败:证据、评分、下一问都已落库却只剩『没有拿到下一个问题』(BUG-633);答题旁白只说『范围没变』、时间线写死『还在收窄』(BUG-634) | 待验收 | | `TASK-rectification-unwritten-evidence-claim-20260910.md` | `PROGRESS-rectification-unwritten-evidence-claim-20260910.md` | 证据轮模型只说『记下了』却没调 batch、没设下一问,财务一件静默丢失、流程停在原题且照常扣点(BUG-635);校正流不识别 Mastra schema 拒绝信封,会把被拒的 batch 报成 completed(BUG-636) | 待执行 | `codex/rectification-unwritten-evidence-claim-20260910` | +| `TASK-rectification-latent-audit-20260910.md` | `PROGRESS-rectification-latent-audit-20260910.md` | 顺带审计:账本派生同年键误杀 `known_event_quality`(BUG-637,BUG-389 复发);双轨一致性按 31 分钟逐分判冲突并降置信度、与报告两套口径(BUG-638);不可分宽度按簇代表分钟少算(BUG-639);引擎与推断层两套代表分钟、回执宫位表不是卡片那一分钟(BUG-640);另 8 条观察项 | 待执行 | `codex/rectification-latent-audit-20260910` | ### 聊天主链路与首页 diff --git a/docs/tasks/TASK-rectification-latent-audit-20260910.md b/docs/tasks/TASK-rectification-latent-audit-20260910.md new file mode 100644 index 00000000..6cff6cfd --- /dev/null +++ b/docs/tasks/TASK-rectification-latent-audit-20260910.md @@ -0,0 +1,102 @@ +# TASK · 生时校正顺带审计:同年去重误杀发挥质量题、双轨冲突按分钟误判、不可分宽度少算、两套代表分钟(2026-09-10) + +- 基线:`origin/staging` @ `a15fc3ef`(代码与 `d96b24c2` 相同;staging 已部署 `d96b24c2`) +- 分支:`codex/rectification-latent-audit-20260910`,基于 `origin/staging` +- 串行:与 `TASK-rectification-unwritten-evidence-claim-20260910.md`(BUG-635/636)**不改同一文件**,可并行;本单 BUG 编号从 **BUG-637** 起,开工时核对 `docs/BUG_HISTORY.md` 最大号(若 635/636 尚未写入,仍从 637 起并在进度记录说明)。 +- 执行方:coding agent;验收:Claude +- 来源:同一份真实 Case JSON(Skill 10.0.21,2026-09-10)里几处互相对不上的数字,逐一回到代码核实。只写结构,不写用户资料。 +- 不改 Skill 版本、不改计分 `SCORE_DELTA`、不改四选项合同、不打开确认门。 + +## 0. 结论总表 + +| 编号 | 级别 | 一句话 | 证据(Case JSON) | +| --- | --- | --- | --- | +| BUG-637 | P2 | 「已记年份的发挥质量题」永远被同年去重丢掉,BUG-389 的修复被 BUG-592 的硬排除覆盖 | `dropped_probes` 里 `education.2016.known_event_quality` reason=`same_year_asked`,IG 0.8,而 `answered_probes` 里没有任何 education 题 | +| BUG-638 | P2 | Python 双轨一致性按 31 个网格分钟逐分比较,同一簇内两个峰值就报「冲突」并降置信度;TS 报告另算一套说「只作观察」 | `dasha_agreement.status=conflict`,`vimshottari_top=04:54`、`narayana_top=04:59`,两者同在簇 `04:54–04:59`;`reasons` 含 `vimshottari_narayana_conflict`;`verification_markdown` 写「双轨…只作观察」 | +| BUG-639 | P3 | 引擎「不可分宽度」用簇代表分钟算跨度,少算两端簇的延伸;它是确认门 `adjacent_passed` 的输入 | `indistinguishable_width_minutes=29`(04:47→05:15),`credible_range=04:45–05:15`,报告写「不可分宽度:31 分钟」 | +| BUG-640 | P3 | 引擎回执与推断层各有一个代表分钟;本例两者正好跨 D24 换升,回执里「已按该分钟重算」的宫位表描述的不是卡片上的那一分钟 | 回执 `representative_time=04:54`、`house_table.time=04:54`、`natal_recast.time=04:54`、`representative_candidate_id→04:54`;`latest_result.representativeTime=04:53`、`range_delivery.representative_time=04:53`;`window_scan` D24 在 04:54 换升 | + +另有 8 条观察项(§5),是设计口径问题,不算 Bug,留给产品决定。 + +## 1. BUG-637(P2):同年去重把 `known_event_quality` 一起杀了 + +### 实证与根因 + +- BUG-389(已解决)的口径:用户已记下某年学业事件后,**针对这件已知事件**出「如愿 / 将就 / 失常」的 `event_quality` 点选卡,不要求采用门。 +- BUG-592(09-08)加了同域同年硬排除:`sameYearProbeAsked(askedKeys, domain, year)`(`probe-question-contract.ts` L320)对 `askedKeys` 里任何 `.` 前缀匹配就丢题。 +- `askedKeys` 来自 `askedDiscriminatorKeys(receipt, evidence)`(`inference-adapter.ts` L82)= 回执里已答探针键 **+** `askedEventProbeKeysFromLedgerEvidence(evidence)`(`core/candidate-contrast-packet.ts` L242)。后者把账本里每件事件变成 `domain.year` 键。 +- `known_event_quality` 的语义键形如 `education.2016.known_event_quality`,它**必然**与账本里那件 2016 学业事件同域同年——这正是它存在的前提。于是 `method-followup.ts` L881 / L957 与 `candidate-contrast-packet.ts` L480 三处都把它判成 `same_year_asked`。BUG-389 的路径自 BUG-592 起形同虚设,但 BUG-389 的测试没有覆盖「账本已有同年事件」这一输入,所以没红。 + +### 决策 + +1. 账本派生的 `domain.year` 键只用于拦 **存在性探针**(`dasha_boundary` / `dasha_activation`,即「那年有没有发生 X」——用户已经说了)。`known_event_quality`(`choice_kind=event_quality`、`source=known_event_quality`)不受账本派生键约束,仍受回执已答键约束(同一道不问两遍)。 +2. 不改 `SCORE_DELTA`、不改 BUG-592 对存在性探针的硬排除。 + +### 任务与验收 + +- 在三处判定点按 `source` / `choice_kind` 分流,或给 `sameYearProbeAsked` 加 `ledgerKeys` 与 `askedKeys` 两个参数,`known_event_quality` 只查后者。 +- 测试:账本有 2016 学业事件 + 回执含 `education.2016.known_event_quality`(IG 0.8)→ 进入候选题且可渲染为 `event_quality` 卡;同输入下 `education.2016.09.dasha_boundary` 仍被 `same_year_asked` 丢;已答过 `education.2016.known_event_quality` 后再次出现 → 丢(reason 不变)。补到 BUG-389 与 BUG-592 各自的测试文件,并在 Bug 历史里把 BUG-389 标「复发自 BUG-592」。 + +## 2. BUG-638(P2):双轨一致性在分钟粒度上判冲突,且与报告的说法不一致 + +### 实证与根因 + +- `refinement_packet.py` `dasha_agreement(built, candidate_times)` L383:对 `candidate_times` 逐分钟累加 Vimshottari / Narayana 两轨分数,取各自 argmax;**分钟不相等即 `conflict`**。`decision_policy.py` L591 传入的是 `built["candidate_times"]`,即整窗 31 个网格分钟。 +- 本例两轨峰值 04:54 与 04:59 同属一个簇(`cluster_range` 04:54–04:59),对用户而言是同一个候选,却被判「主限更偏向 04:54,分盘大运更偏向 04:59。冲突时不能按更高把握收口」;`decision_policy.py` L610 据此把 `overall_confidence` 降一级,并把 `vimshottari_narayana_conflict` 写进 `reasons` 与 `confirmation_reasons`。 +- TS 侧 `dashaAgreementAmongActive`(`refinement-packet.ts` L479,BUG-593)只在推断层 active 候选上重算,报告因此写「只作观察」(partial)。同一个问题两套答案,门禁用的是错的那套。BUG-593 记录里说引擎原值改名 `dasha_agreement_pre_inference` 供审计——代码里**不存在**这个标识符,该条记录与实现不一致。 + +### 决策 + +1. Python 侧按簇比较:两轨各自取 argmax 后映射到所在簇(`cluster_start–cluster_end`),同簇即 `agree`;或把比较对象换成簇代表分钟列表(L600 的 `candidate_decisions` 时间)。两种选一种写进进度记录。 +2. `reasons` / `confirmation_reasons` 里的 `vimshottari_narayana_conflict` 与置信度降级必须与报告使用的同一判定同源;回执保留引擎原值时字段名用 `dasha_agreement_pre_inference`,把 BUG-593 的记录补正为实际实现。 +3. 不放宽确认门:`agree` 不会让任何门从关到开(确认门仍被其它原因关闭),只是不再凭假冲突降级。 + +### 任务与验收 + +- `tests/test_rectification_decision_policy*.py` 或 `refinement_packet` 定向测试:构造两轨峰值落在同簇不同分钟 → `status=agree`;落在不同簇 → `conflict`;只有一轨有分 → `partial`。 +- TS 合同测试:回执 `dasha_agreement.status` 与 `skill_verification_report` 的双轨句一致(用 golden 回执,不手造形状)。 +- `docs/BUG_HISTORY.md` BUG-593 补正实现描述。 + +## 3. BUG-639(P3):不可分宽度按簇代表分钟算 + +- `decision_policy.py` L154 `indistinguishable_width_minutes`:`span = max(rep) - min(rep) + 1`,rep 是各簇代表分钟。本例 04:47 与 05:15 → 29;真实候选集是 04:45–05:15 → 31。BUG-593 已让**报告**改用 `credible_range`,但引擎这个值仍进 `adjacent_passed = unique_top and width <= MAX_CONFIRMATION_WIDTH_MINUTES(5)`。两簇 04:50–04:55(代表 04:53)与 04:56–05:02(代表 04:59)会被算成 7 而真实是 13;极端情形代表分钟相邻、簇各延伸数分钟时,可能把 ≤5 的门误开。 +- 决策:改为 `max(cluster_end) - min(cluster_start) + 1`;`tied_minute_count` 逻辑不动。 +- 验收:Python 定向测试覆盖上述两簇例子;回执 `indistinguishable_width_minutes` 与报告 `width_minutes` 在无推断层时相等。 + +## 4. BUG-640(P3):两套代表分钟,回执事实描述的不是卡片那一分钟 + +- 引擎回执的 `representative_time` / `representative_candidate_id` / `house_table` / `natal_recast` / `technique_audit_table`「已按该分钟重算」来自 Python 先验排序(本例 04:54,簇 04:54–04:59);推断层 `inference_state.representative_time`、`latest_result.representativeTime`、`range_delivery.representative_time`、`next_user_action.representative_time`(`turn-decision.ts` L191)来自后验排序(本例 04:53)。采用、卡片、旁白走后者;模型读到的回执事实走前者。 +- 本例两者正好隔着 D24 04:54 换升(04:53 金牛 / 04:54 双子),`natal_recast.user_meaning`「本命宫位已按 04:54 重算」与卡片首列 04:53 不是同一张盘。本次上升都是巨蟹所以肉眼看不出;若代表分钟跨 D1 或 D9 换升,报告里的「本命上升(该分钟)」就会写错。 +- 决策:有推断层时,回执投影把 `representative_time` / `house_table` / `natal_recast` 按推断代表分钟重取(`house_tables_by_time` 已按候选分钟给了表,直接选用即可),引擎原值改名 `*_pre_inference`;`technique_audit_table` 的「该分钟」措辞跟随。不改引擎。 +- 验收:golden 回执测试:推断代表分钟 ≠ 引擎代表分钟时,投影后 `house_table.time === latest_result.representativeTime`,且 `natal_recast.user_meaning` 里的分钟与卡片一致。 + +## 5. 观察项(不算 Bug,产品决定是否立单) + +1. **Skill 文案与服务器行为不一致**:Skill 10.0.21 写「财务与健康只有用户主动说才问」,服务器自 BUG-462 起在采集耗尽轮转里主动问财务(`DATED_COLLECT_ORDER` 含 finance)。模型两边都读,可能是 BUG-635 里它不肯记财务事件的一个诱因。下次升 Skill 版本时对齐。 +2. **家人带年探针被无年采集题吞掉**:`probe:family.2021` 按 BUG-539 的决策去掉年份后成了普通家人采集题;用户答「不记得了」→ 家人线标 skipped → 那道原本能分候选的 2021 年家人点选题再也不会问。可考虑「无年采集被跳过后,带年家人探针仍可作点选卡」。 +3. **`precision_stage` 两个口径**:回执 `d9_refine`(文案「可再补一件感情变化」)与 `interview.precision_stage=collect_events` 同时存在,模型读到的是前者。 +4. **「记不清」只匹配原字**:`isCollectSkipUtterance` 只认精确的「记不清」,「不记得了 / 记不得 / 忘了」走 LLM 分类器,而采集题的分类器只有 `no` 没有 skip 类。本例靠 Agent 自己调 resolve-focus 兜住了,属软肋。 +5. **步骤计数倒退**:点选题问完回到采集,顶部从「第 2 步·区分候选」退回「第 1 步·收集经历」,用户会以为流程回滚。 +6. **事件吻合率不区分候选**:9 个候选的「4 件经历里 4 件对得上」完全相同,却是卡片最醒目的一行;它描述拟合不描述区分,考虑弱化或改成相对表述。 +7. **VedAstro 分钟级校验每轮超时**:`vedastro_event_validation.failure.code=timeout`,`external_validation_not_passed` 长期在 reasons 里。已知环境缺口,不在本单。 +8. **run diagnostic 不落库**:模型为什么跳过工具,事后只能查容器日志(BUG-633 已记)。 + +## 6. 让步顺序 + +BUG-640 → BUG-639 → 可延后;BUG-637 与 BUG-638 是本单最低交付。观察项不做。 + +## 7. 开工前置命令 + +```bash +git fetch origin --prune +git worktree add -b codex/rectification-latent-audit-20260910 .worktrees/rectification-latent-audit-20260910 origin/staging +grep -o "^## BUG-[0-9]*" docs/BUG_HISTORY.md | tail -1 # 核对编号起点 +.venv/bin/python -m pytest tests -k "decision_policy or refinement_packet" -q # 记基线 +cd frontend && npm test -- tests/rectification-method-followup*.test.ts tests/rectification-delivery-report-facts.test.ts # 记基线 +``` + +## 8. 验收口径 + +- Python:定向测试 fail=0;`run_quality_gate.py --profile quick` 通过。 +- 前端:`tsc --noEmit` 0 错、`npm run lint` 0 error、相关套件 fail=0、测试总数 ≥ 基线;合同测试 fixture 来自真实引擎回执。 +- 推 staging 后 `/api/health` 的 `deployment.gitCommit` 等于最新含代码改动的提交。