diff --git a/docs/tasks/README.md b/docs/tasks/README.md index 5013e7cd..c5039123 100644 --- a/docs/tasks/README.md +++ b/docs/tasks/README.md @@ -50,6 +50,8 @@ | `TASK-rectification-collect-direction-20260904.md` | `PROGRESS-rectification-collect-direction-20260904.md` | 可评分事件 2 条时盘外核对抢跑到刚拒答的家人领域,Agent 只能改写成不指向任何领域的泛问;缺第三件带年份的事却先问职业 | 已验收:5.1/5.3/5.4 通过;5.2 顺序错位(任务书责任)见修复单 | `codex/rectification-collect-direction-20260904`(BUG-527~530) | | `TASK-rectification-collect-direction-fix-20260904.md` | `PROGRESS-rectification-collect-direction-fix-20260904.md` | dated 补采集分支插在方法轮转之前,新案例第二问被家人抢占感情/事业;`7ee7f825` CSS 与合同测试脱节挡住门禁 | 已验收通过(0be51e65),待 staging 部署核对 | `codex/rectification-collect-direction-fix-20260904`(BUG-531~532) | | `TASK-rectification-post-adopt-verify-20260904.md` | `PROGRESS-rectification-post-adopt-verify-20260904.md` | 采用后核对题重复已问题;核对卡「这题跳过」等于整案停止、已采用仍念"选一个先用着"、前端"没有拿到下一个问题";采用旁白承诺的 holdout/OOS 核对与实际计划不同源(原任务书 §4 错误);家庭采集题带年份前缀自相矛盾 | 待验收 | `codex/rectification-post-adopt-verify-20260904`(BUG-536~539) | +| `TASK-rectification-quality-probe-dedupe-20260904.md` | — | 候选区分阶段同一道"上大学"题问两次:`pickProbe` 按种类取第一条质量探针无视 `semantic_key`(第二次答案绑到错的证据);引擎对毕业等学业 kind 也套"高考发挥"模板;同域同分组质量探针不去重 | 待执行 | `codex/rectification-quality-probe-dedupe-20260904`(BUG-540 起) | +| `TASK-api-not-configured-mislabel-20260904.md` | — | 16 处路由把数据库瞬断(部署切换窗口)兜底翻译成 503「服务尚未配置」;改为仅配置错误用该文案,其余 `service_unavailable`,收敛为共享 helper | 待执行 | `codex/api-not-configured-mislabel-20260904`(BUG-542 起) | | `TASK-rectification-ux-20260902.md` | `PROGRESS-rectification-ux-20260903.md` | 会话面空白假死与交互摩擦 | 已验收 | `d159f08e`(09-03 在新基线重做后合入,BUG-505~509) | ### 聊天主链路与首页 diff --git a/docs/tasks/TASK-api-not-configured-mislabel-20260904.md b/docs/tasks/TASK-api-not-configured-mislabel-20260904.md new file mode 100644 index 00000000..a3b25012 --- /dev/null +++ b/docs/tasks/TASK-api-not-configured-mislabel-20260904.md @@ -0,0 +1,72 @@ +# TASK · 部署/数据库瞬断被报成「服务尚未配置」(2026-09-04) + +- 基线:`origin/staging` @ `a75929c1` +- 分支:`codex/api-not-configured-mislabel-20260904` +- 执行方:coding agent;验收:Claude +- BUG 编号起点:**BUG-542**(若 `TASK-rectification-quality-probe-dedupe-20260904.md` 尚未开工则从 BUG-540 起;开工时 `grep -o "BUG-5[0-9][0-9]" docs/BUG_HISTORY.md | sort -u | tail -1` 复核) + +## 1. 事故实证 + +2026-09-04 约 22:00(UTC+8)用户在 staging 校正面点选答案,`POST /api/rectification/agent` 与 `GET /api/rectification/cases/?sessionId=…` 均返回 `503 {"error":"服务尚未配置"}`。同一时刻 `GET /api/health`: + +| 字段 | 当时 | 数分钟后 | +| --- | --- | --- | +| `deployment.gitCommit` | `a75929c1`(刚落地) | `a75929c1` | +| `status` | `blocked` | `ok` | +| `database.latestMigration` | `null` | `20260904010000_read_report_candidate_range.sql` | +| `database.requiredMigrationsPresent` | `false` | `true` | +| `checks.rectificationMigrations` | `degraded` / `"Error"` | `ok` | +| `checks.modelCatalog` | `blocked` / `database_model_catalog_unavailable` | `ok` | + +即:数据库在部署切换窗口内短暂不可用,随后自愈;环境变量从未缺失。 + +## 2. 根因 + +16 处路由用同一段兜底(示例 `frontend/src/app/api/rectification/cases/[caseId]/route.ts` L38–45、`frontend/src/app/api/rectification/agent/route.ts` L143–153): + +``` +try { supabase = await createServerSupabaseClient(); accounting = createAdminSupabaseClient(); } +catch { return 503 {"error":"服务尚未配置"} } +``` + +`createServerSupabaseClient()` 在自托管分支里会先读身份会话(`readIdentitySession` → 数据库查询),任何数据库连接错误都从这里抛出,被 `catch` 一律翻译成"尚未配置"。文案本来只对应 `SupabaseConfigurationError`(`frontend/src/lib/supabase/config.ts`,`code = "DATABASE_NOT_CONFIGURED"`,缺环境变量时抛出)。 + +全部 16 处:`api/rectification/{agent, cases/open, cases/entry-summary, cases/[caseId], cases/[caseId]/{close, adopt-skill, upgrade-skill, candidates/accept, turns/[turnId]/regenerate}}`、`api/consult/{route, status, cancel}`、`api/models`、`api/birth-time-journey`、`api/birth-time-guide`、`lib/onboarding-post.ts`。 + +## 3. 决策记录 + +1. 只有 `isSupabaseConfigurationError(error)` 为真时才允许返回"服务尚未配置";其它异常一律 `503 {"error":"服务暂时不可用,请稍后重试","code":"service_unavailable"}`,并 `console.error` 带路由名与 `error.name`(不打印堆栈里的连接串)。 +2. 不新增重试、不改部署顺序、不动 health 判定;部署窗口内的短暂 503 是可接受的,这里只修**说法**,让用户与验收方不再误以为环境变量丢了。 +3. 16 处收敛为一个共享 helper(放 `frontend/src/lib/api/` 或既有的 route 工具目录,位置由执行方按现有惯例选),不得复制 16 份新文案。 + +## 4. 硬红线 + +- 不得改 `createServerSupabaseClient` / `createAdminSupabaseClient` 的抛错语义与调用顺序。 +- 未登录仍是 `401`,与本任务无关的路径不得顺带改动。 +- 不得在错误响应里回显任何环境变量名之外的信息;配置错误响应保留现有 `"服务尚未配置"` 文案(既有测试引用)。 + +## 5. 任务分解 + +### 5.1 共享 helper + 16 处替换 +- 验收:`grep -rn "服务尚未配置" frontend/src` 只命中 helper 一处;`tsc --noEmit`、`npm run lint` 0 error。 + +### 5.2 回归测试 +- 新增 `frontend/tests/api-service-unavailable-20260904.test.ts`:模拟 `createServerSupabaseClient` 抛 (a) `SupabaseConfigurationError` → 503 `服务尚未配置`;(b) 普通 `Error("connection refused")` → 503 `code=service_unavailable`,文案不含"尚未配置"。至少覆盖 `rectification/agent` 与 `cases/[caseId]` 两条路由(Node 22 module mock 契约见 `docs/BUG_HISTORY.md` 里 `test(reports): document Node 22 mock contract` 相关条目)。 +- 既有引用"服务尚未配置"的测试若断言的是非配置类异常,改断言并写"原值 / 新值 / 原因"。 + +### 5.3 记录 +- `docs/BUG_HISTORY.md` 新增一条(现象、触发条件=部署切换窗口、根因=兜底翻译、修复、验证);`CHANGELOG.md` 一条;`docs/tasks/PROGRESS-api-not-configured-mislabel-20260904.md`。 + +## 6. 让步顺序 + +5.1 > 5.2 > 5.3。若某条路由的测试 harness 不支持 mock,只覆盖两条主路由并把其余写进进度记录。 + +## 7. 开工前置命令 + +```bash +git fetch origin --prune +git worktree add -b codex/api-not-configured-mislabel-20260904 .worktrees/api-not-configured-mislabel-20260904 origin/staging +cd .worktrees/api-not-configured-mislabel-20260904 +ln -s /workspace/Jyotisha/frontend/node_modules frontend/node_modules +grep -rn "服务尚未配置" frontend/src frontend/tests | wc -l +``` diff --git a/docs/tasks/TASK-rectification-quality-probe-dedupe-20260904.md b/docs/tasks/TASK-rectification-quality-probe-dedupe-20260904.md new file mode 100644 index 00000000..2e5a28f2 --- /dev/null +++ b/docs/tasks/TASK-rectification-quality-probe-dedupe-20260904.md @@ -0,0 +1,81 @@ +# TASK · 校正候选区分阶段同一道"上大学"题问两次(2026-09-04) + +- 基线:`origin/staging` @ `a75929c1`(含 `d4627a50` 采用后核对线修复;本任务书不依赖其验收结论) +- 分支:`codex/rectification-quality-probe-dedupe-20260904` +- 执行方:coding agent;验收:Claude +- 涉及文件:`scripts/rectification/event_probes.py`、`frontend/src/lib/rectification-agentic/v9/choice-card.ts`;与 `TASK-rectification-post-adopt-verify-20260904.md`(已实现 `d4627a50`)无文件冲突,可并行。 +- BUG 编号起点:**BUG-540**(开工时用 `grep -o "BUG-5[0-9][0-9]" docs/BUG_HISTORY.md | sort -u | tail -1` 复核) + +## 1. 事故实证(staging,2026-09-04,候选区分阶段) + +用户在同一案例里连续两次看到同一张选择卡:"**YYYY 年 M 月那次上大学,更接近如愿、将就调剂、发挥失常还是说不清**",四个选项完全相同。用户两次都作答,第二次答完范围继续收窄。案例 dump 中可对上的事实(不写具体值): + +| # | 事实 | 位置 | +| --- | --- | --- | +| A | 该案例学业域有两条已核实事件:**入学**(`education_start`)与**毕业**(`education_completion`),相隔四年 | 案例 `evidence[]` | +| B | 引擎对这两条事件各发了一条 `known_event_quality` 探针;两条的 `expected_outcomes` 的 yes/no 候选分钟集合**完全相同**(同一 D24 星座切分),只有 `semantic_key`(`education.<入学年>.known_event_quality` / `education.<毕业年>.known_event_quality`)、`target_evidence_id`、`candidate_split_hash` 不同 | `scripts/rectification/event_probes.py::_quality_distinguish_probes`(L901–997)、`candidate_contrast.py::candidate_split_hash`(hash 把 `year/month` 掺进去,所以同分组不同年 ≠ 同一 hash) | +| C | 第二张卡的 `question_id` / `probe_id` / 评分都指向**毕业**探针,但卡面题干与日期标签是**入学**探针的 | `choice-card.ts::pickProbe`(L233–251):`followup.choice_kind === "event_quality"` 分支取 `pool` 里**第一条**质量探针就返回,`semantic_key` 匹配写在它后面,永远轮不到;`periodFor`(L254)与 `eventQuestionPrompt`(L280)都从 `pickProbe` 拿探针,所以日期与题干一起错 | +| D | 即便 C 修好,毕业事件的题干仍会是"YYYY 年 6 月那次**上大学**……"、选项仍是"发挥明显失常 / 只是将就调剂 / 基本如愿录取 / 当时说不清楚" | `event_probes.py::_quality_user_meaning`(L887)只按 `domain == "education"` 套"上大学"模板;`QUALITY_DISTINGUISH_OPTIONS["education"]`(L92)同样不看事件 `kind` | + +## 2. 根因 + +1. **前端选探针的优先级反了**:`pickProbe` 先按"种类"再按"键"。同域只有一条质量探针时看不出来;一旦引擎给了两条,第二次 followup 的卡面永远渲染第一条。用户第二次实际是在**对着入学题、给毕业探针打分**——答案被记到了错的证据上。 +2. **引擎对学业域所有事件一律发"高考发挥"题**:质量探针的题干与选项是 BUG-390 时按"高考/录取"设计的,那时只有 `education_start` 一种学业事件会到这里;现在采集线会产生 `education_completion`(毕业)等其它 kind,模板没有跟着分。毕业没有"调剂/发挥失常"这种体验,这道题对毕业事件**没有可回答的语义**。 +3. **同分组质量探针没有去重**:两条探针的 yes/no 分钟集合一模一样,问第二道不带来任何新信息(信息增益相同、支持/冲突集合相同),却占了 `MAX_QUALITY_DISTINGUISH_PROBES = 2` 的全部名额和用户一轮回答。`candidate_split_hash` 因为掺了年份而不相等,`askedDiscriminatorKeys` 的 split-hash 去重拦不住。 + +## 3. 决策记录 + +1. **一道题只能绑定一条探针,且必须是 followup 指定的那条**:`pickProbe` 的匹配顺序改为 `probe_id` → `semantic_key` → 种类兜底。找不到 `semantic_key` 对应探针时不得退回"同种类第一条",改为返回 `null`(让上游按无探针路径走,不出卡),并在进度记录里说明这种情况是否在测试中出现过。 +2. **质量探针只对有"体验词表"的事件种类发**:学业域仅 `education_start`、`education_change`、`education_interruption` 可发("如愿 / 调剂 / 失常"对这三类都有意义);`education_completion` 与其它 kind **不发**质量探针,而不是新造一套"毕业顺不顺"的模板。理由:产品要求"多余入口宁可删除也不修";毕业是否顺利本身对 D24 切分没有独立证据价值(与入学事件是同一切分)。 +3. **同域同分组只发一条**:`_quality_distinguish_probes` 内对 `(domain, frozenset(yes_times), frozenset(no_times))` 去重,保留时间最早(或 `information_gain` 最高、并列取最早)的一条;被去掉的事件不计入 `MAX_QUALITY_DISTINGUISH_PROBES` 名额。 +4. **已经答过的错绑答案不追溯修正**。已有案例的历史轮次不回写;只保证新的轮次不再出错。若要为受影响案例做补救,另开任务书。 +5. 本任务书不推翻 BUG-389/390/391/392/393 的任何防复发措施;BUG-390 的"质量探针只对学业发出、不得把高考题干套到事业/感情"仍然成立,本次是把它进一步收紧到学业内的特定 kind。 + +## 4. 硬红线 + +- 不得改 `QUALITY_DISTINGUISH_OPTIONS` 四选项的文案与 `answer_class` 映射(BUG-390 / 四选项合同 `tests/test_probe_question_contract.py`、`frontend/tests/rectification-probe-question-contract.test.ts`)。 +- 不得新增第二套"毕业体验"模板或选项。 +- 不得改 `candidate_split_hash` 的算法(其它模块靠它做 asked-key 去重与回执投影);去重在 `_quality_distinguish_probes` 内部按分组集合做。 +- `pickProbe` 改动不得影响 `varga_style` / `existence` 探针的现有选取(`rectification-choice-card.test.ts` 既有断言原样通过;如需改断言写"原值 / 新值 / 原因")。 +- 任务书、进度记录、Bug 历史、测试 fixture 一律用抽象年份与虚构事件;不得出现真实 case_id / session_id / 出生候选分钟。 + +## 5. 任务分解 + +### 5.1 `pickProbe` 按键选探针(前端) + +- 改 `choice-card.ts::pickProbe`:顺序 `followup.probe_id`(`ChoiceCardFollowup` 需补 `probe_id?: string`,由 `method-followup.ts` 的 followup 透传,见其 L172 已有字段)→ `followup.semantic_key` → 无键时才按 `choice_kind` 兜底;有键但找不到 → `null`。 +- 验收:`frontend/tests/rectification-choice-card.test.ts` 新增用例——同域两条 `known_event_quality` 探针(不同 `semantic_key` / `target_evidence_id` / `display_date_label`,相同 expected_outcomes),followup 指向第二条时,`buildChoiceFrame` 的 `question_id`、题干日期标签、`user_meaning` 全部来自第二条;反向再断言指向第一条时不受影响。 + +### 5.2 质量探针按事件 kind 发(引擎) + +- `_quality_distinguish_probes` 只对 `kind ∈ {education_start, education_change, education_interruption}` 的学业事件生成;`education_completion` 及其它 kind 跳过。事件记录里的 kind 字段名以 `contracts.py::EVENT_KINDS` 为准,不要凭 `domain` 猜。 +- 验收:`tests/test_rectification_event_probes.py` 新增:同域一条 `education_start` + 一条 `education_completion` → 只产出一条质量探针且 `target_evidence_id` 指向入学;单独一条 `education_completion` → 零条质量探针,且该事件仍参与普通时间探针(不得因此少问它本身)。 + +### 5.3 同域同分组去重(引擎) + +- 在 5.2 之后再做:同域两条可发事件(如 `education_start` + `education_change`)若 yes/no 集合相同,只保留一条。 +- 验收:同文件新增用例,两条 `education_start` 类事件、相同分组 → 一条探针;不同分组 → 两条(`MAX_QUALITY_DISTINGUISH_PROBES` 上限不变)。 + +### 5.4 记录 + +- `docs/BUG_HISTORY.md` 新增 BUG-540(前端错绑)、BUG-541(引擎 kind 与去重);关联 BUG-390。 +- `docs/tasks/PROGRESS-rectification-quality-probe-dedupe-20260904.md`;`CHANGELOG.md` 一条。 +- `docs/testing/rectification-quality-probe-dedupe-20260904.md`:真实环境清单——新建案例录入"入学 + 毕业"两条学业事件后,候选区分阶段最多出现一次"上大学"题,且题干年份 = 入学年份。 + +## 6. 让步顺序 + +5.1 > 5.2 > 5.3 > 5.4。5.3 若与既有 `information_gain` 排序测试冲突,允许先只做 5.1 + 5.2 并把 5.3 写进 `BLOCKED.md`。 + +## 7. 开工前置命令 + +```bash +git fetch origin --prune +git worktree add -b codex/rectification-quality-probe-dedupe-20260904 .worktrees/rectification-quality-probe-dedupe-20260904 origin/staging +cd .worktrees/rectification-quality-probe-dedupe-20260904 +ln -s /workspace/Jyotisha/frontend/node_modules frontend/node_modules +grep -o "BUG-5[0-9][0-9]" docs/BUG_HISTORY.md | sort -u | tail -1 +.venv/bin/python -m pytest tests/test_rectification_event_probes.py tests/test_probe_question_contract.py -q +cd frontend && npx tsx --test tests/rectification-choice-card.test.ts tests/rectification-probe-question-contract.test.ts +``` + +收尾:`tsc --noEmit`、`npm run lint`、`npm test`(与 `origin/staging` 基线失败清单逐条比对)、`.venv/bin/python scripts/run_quality_gate.py --profile quick`。