From 846b60642f18f35ce233d707388171cecd4916bc Mon Sep 17 00:00:00 2001 From: Jesse_Chen Date: Fri, 4 Sep 2026 10:39:27 +0000 Subject: [PATCH] =?UTF-8?q?docs(report):=20add=20BUG-526=20candidate-range?= =?UTF-8?q?=20read=20task=20brief=20=E2=80=94=20degrade=20and=20RPC=20path?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_016P5RoqzmUQEbeC2qjAkeGr --- TASK-report-candidate-range-read-20260904.md | 77 ++++++++++++++++++++ 1 file changed, 77 insertions(+) create mode 100644 TASK-report-candidate-range-read-20260904.md diff --git a/TASK-report-candidate-range-read-20260904.md b/TASK-report-candidate-range-read-20260904.md new file mode 100644 index 00000000..2ae7aa11 --- /dev/null +++ b/TASK-report-candidate-range-read-20260904.md @@ -0,0 +1,77 @@ +# 任务书 · BUG-526 修复:报告链路不得直读已收权的校正表(2026-09-04) + +基线:`origin/staging` @ `cce919fe`(开工时 `git fetch` 后以 `origin/staging` HEAD 为准)。 + +## 事故实证(沿用执行方 BUG-526 取证,已核对代码与迁移) + +- BUG-524(float 崩溃)已修复并部署(`7b1354a7`),staging 引擎 smoke 200——但产品侧 personal_full 仍失败:创建后约 16 秒 `calculation_unavailable`,无章节行,**API 访问日志里没有 `POST /api/consultation_workflow`**(引擎从未被调用)。 +- staging Postgres 日志三次 `permission denied for table agentic_rectification_cases`,与 job 三次 attempt 对齐。 +- 权限事实链(委托方已核对):`20260812010000_agentic_rectification_v9_runtime.sql:234` 曾 `grant all ... to service_role`;`20260814010000_immutable_skill_registry.sql:25` **`revoke all on table public.agentic_rectification_cases from service_role`**,此后无任何再授权——该表运行时只许走 security definer RPC,v9 校正链路全部如此,因此校正功能正常。 +- 上游同步 `a7041529` 新增了**两处** service_role 直读(BUG-526 只点名了第一处): + 1. `frontend/src/lib/personal-report-worker.ts:278`(worker 的 `generate` 包装); + 2. `frontend/src/app/api/reports/route.ts:205`(报告创建 route 的 `loadCandidateRange`,同样 `admin.from("agentic_rectification_cases")`,`if (error) throw error`)。 +- 触发条件:资料 `birth_time_status = "accepted"`(非 confirmed)。两处读取都会 `permission denied`;错误被映射成可重试 `calculation_unavailable`,报告在调用引擎之前就死了。 + +## 决策记录 + +- **解除** `TASK-report-sensitivity-crash-20260904.md` 红线 4"前端不需要改动"的限制——仅限报告链路的候选窗读取(上述两处及其共享工具),其余 worker 逻辑仍不在授权内。 +- **授权新增一个只读 security definer RPC 的数据库迁移**(方案 B),作为"不改表结构"红线的明确例外:只新增 function,不动表、列、RLS、既有 grant。 +- 沿用既有原则:候选窗是敏感度的**可选增强层**,其读取失败绝不允许打死整份报告(与引擎侧"敏感度层失败降级"同一原则,BUG-524 修复已在引擎侧落实)。 + +## 硬红线 + +1. **不得恢复 `service_role` 对 `agentic_rectification_cases` 的表级权限**。20260814 的收权是安全设计(skill 身份不可变性防御),谁也不许 re-grant。 +2. **读失败必须降级**:两处候选窗读取在任何错误(permission denied、超时、行不存在)下都返回 `null` range 并继续——报告以无窗口的敏感度输入正常生成,不得再映射成 `calculation_unavailable`。 +3. 错误语义区分(BUG-526 防复发):`calculation_unavailable` 从此只代表引擎调用失败;候选窗读失败只记一条不含内容的 warn 级日志(错误码级别)。 +4. 迁移(方案 B)照 v9 系列写法:`security definer`、`revoke all from public`、显式 `grant execute to service_role`、事务内、幂等、时间戳晚于现有迁移;**必须 `npm run test:db` 真跑**(需要 Docker;没有就走 BLOCKED 流程停下)。 +5. 其余既有红线延续(不改 `.gitea/workflows/**`、不提升 main、`./node_modules/.bin/tsc` 而非 `npx tsc`、隐私日志边界)。 + +## 开工前置 + +```bash +git fetch origin --prune +git worktree add -b codex/report-candidate-range-20260904 \ + ../.worktrees/report-candidate-range-20260904 origin/staging +``` + +先读:`frontend/src/lib/personal-report-worker.ts`(generate 包装)、`frontend/src/app/api/reports/route.ts:195-220`、`frontend/src/lib/personal-report-route-core.ts` 的 `loadCandidateRange` 消费处(创建路径的 catch 现在会把报告直接 markFailed——确认修复后该 catch 只兜真正的异常)、`frontend/supabase/migrations/20260814010000_immutable_skill_registry.sql`(RPC 的既有写法与 grant 模式)、`docs/BUG_HISTORY.md` BUG-526。 + +## 任务 + +### 任务 1(P0)· 两处读取降级 + 语义修正 + +- worker 与 create route 的候选窗读取统一为共享 helper:任何失败 → warn 日志(错误码)→ 返回 `null`,继续生成;`birth_time_rectification_cases` 那条查询(`.in("status", ["confirmed","completed"])`,其中 `completed` 不在该表状态枚举里,顺带修正为真实枚举值)同样降级。 +- 验收:单元测试模拟 permission denied / 抛错 / 空行三态,报告链路继续且敏感度输入无窗口;`calculation_unavailable` 不再由候选窗路径产生。 + +### 任务 2(P0)· 合法读取通道(方案 B) + +- 新增只读 RPC(如 `read_report_candidate_range(p_user_id uuid)`):返回该用户最新 `candidate_accepted` 行的 `candidate_range`(以及 `birth_time_rectification_cases` 的 confirmed 候选窗,若同函数覆盖更简洁可合并),security definer + 只授 `service_role`。 +- 两处读取切到 RPC;RPC 失败仍走任务 1 的降级。 +- 验收:`npm run test:db` 覆盖——`service_role` 可 execute、`authenticated`/`anon` 不可 execute、无行时返回空、RPC 不暴露除候选窗外的任何列。 + +### 任务 3(P0)· 部署后真实验证(历轮顺延的最终实测) + +health SHA 对齐后真实生成 standard personal_full(默认 5 主题):报告 ready、各章有正文;telemetry 无 `length`;≥3 处 writer 输出回溯 bundle;每章实测 `inputTokens`/墙钟对照 `docs/tasks/PROGRESS-report-skill-parity-20260901.md` 的 2 倍线裁决。**取证要快**(容器 recreate 丢日志的教训)。accepted 生时下确认敏感度层状态合法(有 RPC 窗口则 `candidate_window_only`,降级则相应状态)。 + +### 任务 4(P1)· BUG-526 收口 + +`docs/BUG_HISTORY.md` BUG-526 补修复、验证、修复版本;注明第二读者(reports/route.ts:205)与 `completed` 枚举值错误的顺带修正。 + +## 不在本轮范围 + +- worker 其余逻辑、报告生成管线、writer 合同。 +- `agentic_rectification_cases` 的表结构、RLS、既有 grant。 +- 上游同步修复单(TASK-upstream-sync-fix)的其余项。 + +## 收尾 + +- `docs/tasks/PROGRESS-report-candidate-range-20260904.md`;推送后核对 health `.deployment.gitCommit`(注意 memory 记录显示 7ee7f825 曾有合同测试挡门禁——若门禁再挡,先修挡门项或走 BLOCKED,不得绕门禁)。 +- 不提升 main。 + +## 交付物清单 + +1. 共享候选窗读取 helper:降级三态测试 + 错误语义修正 +2. 只读 RPC 迁移 + `test:db` 实跑输出(权限矩阵四断言) +3. 两处调用切换 diff;`completed` 枚举修正说明 +4. 部署后真实报告全套实测(章节正文 / telemetry / 回溯 / tokens 与 2 倍线对照) +5. BUG-526 收口 + PROGRESS + 全套质量门实际输出