docs(report): add BUG-526 candidate-range read task brief — degrade and RPC path

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016P5RoqzmUQEbeC2qjAkeGr
This commit is contained in:
Jesse_Chen
2026-09-04 10:39:27 +00:00
parent cce919fe8d
commit 846b60642f
@@ -0,0 +1,77 @@
# 任务书 · BUG-526 修复:报告链路不得直读已收权的校正表(2026-09-04)
基线:`origin/staging` @ `cce919fe`(开工时 `git fetch` 后以 `origin/staging` HEAD 为准)。
## 事故实证(沿用执行方 BUG-526 取证,已核对代码与迁移)
- BUG-524float 崩溃)已修复并部署(`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 RPCv9 校正链路全部如此,因此校正功能正常。
- 上游同步 `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`,降级则相应状态)。
### 任务 4P1)· 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 + 全套质量门实际输出