fix(db): check callers against the adapter that actually runs

The self-hosted client was typed as a full Supabase client, so an old scoring poll called .single() on a plain promise and failed. The new DataClient only lists methods the adapter implements.

Co-Authored-By: Grok (agent loop)
This commit is contained in:
jesse-ux
2026-10-15 09:21:14 +08:00
co-authored by Grok (agent loop)
parent 9274491681
commit b184448871
19 changed files with 366 additions and 47 deletions
+17
View File
@@ -36131,3 +36131,20 @@
- 相关记录:BUG-203(锁文件,不是这一条)、BUG-1063(人物表列级读取权)、BUG-1348。
- 复发自:无。
- 修复版本:分支 `codex/release-path-production-20261014`。编号接在 BUG-1349 之后。
## BUG-1351 | 自托管下旧版校正评分轮询 rpc 结果多读一步,必抛 TypeError
- 状态:resolved
- 首次发现:2026-10-13
- 最近更新:2026-10-15
- 影响面:旧版生时校正(legacy-guided-v1)的评分轮询,`POST /api/birth-time-journey` 且类型为 `poll_scoring`
- 用户现象:这类存量校正在查看评分进度时直接失败,页面拿不到「已领取 / 处理中 / 已完成」。
- 触发条件:自托管数据库(当前 staging 与生产都是这条路径)上,对 `claim_birth_time_scoring_job` 的返回值再调用 `.single()`。该函数返回的是表格里的一行,本地适配层把整表收成数组,没有 `.single`。
- 根因:数据访问层被标成完整的 Supabase 客户端,编译器看不出适配层没有 `.single`。原有测试用内存仓库,没有走过真实适配层。
- 修复:去掉 `.single()`。返回值如果是数组就取第一行,如果已是单行对象就直接用,再做原来的字段校验。Supabase 回滚分支不带 `.single()` 时同样返回数组,行为一致。
- 验证:修复前两条单元测试都抛出 `supabase.rpc(...).single is not a function`。修复后这两条通过。另有一条真实迁移测试:函数返回值是数组,认领结果为已领取,且不抛错。`tsc --noEmit` 0 错。
- 防复发:适配层 `implements` 自己的 `DataClient`。新增调用如果用了适配层没有的方法,编译直接失败。评分认领有数据库级用例。
- 相关记录:BUG-124、BUG-130、BUG-576、BUG-926
- 复发自:无
- 修复版本:分支 `codex/data-layer-types-20261013`。任务书预留 BUG-1343;开工时本文件最大号已是 BUG-1350,1343 没有条目。按「从当前最大号往后」编为 BUG-1351。
@@ -0,0 +1,92 @@
# PROGRESS · 数据访问层 DataClient 类型(第一阶段)
- 分支:`codex/data-layer-types-20261013`
- 基线:`origin/staging` `9274491681f6fcacbc87e3797441d2c5221de874`
- 前置:`origin/codex/node-runtime-resources-20261013` 已是 `origin/staging` 的祖先(`merge-base --is-ancestor` 退出码 0)。`local-postgres-client-core.ts` 只加了 `import type` 和两处 `implements`。
- `frontend/src/lib/rectification-agentic/session.ts` 已不在,本单没有改它。
- 编号:任务书预留 BUG-1343。开工时 `docs/BUG_HISTORY.md` 最大号是 BUG-1350,1343 没有条目。按台账规则从最大号往后,记为 **BUG-1351**。
## T1 · 编译器暴露的 2 条
`DataClient` 换上之后、缺陷尚未修时,`tsc --noEmit` 退出码 2,恰好 2 条:
```
src/app/api/chart-profiles/route.ts(59,40): error TS7006: Parameter 'row' implicitly has an 'any' type.
src/lib/birth-time-scoring-job-store.ts(103,10): error TS2339: Property 'single' does not exist on type 'Promise<Readonly<{ data: any; error: Readonly<{ message: string; code?: string | undefined; }> | null; count?: number | null | undefined; }>>'.
```
`grep`:`as unknown as SupabaseClient` 在 `frontend/src` 无结果。`@supabase/supabase-js` 只剩 `admin-client-core.ts` 的 `createClient` 回滚分支。
## T2 · 缺陷修复红转绿
修复前,`claimScoringJob` 对返回数组的假 `rpc` 调用 `.single()`:
- `claimScoringJob reads the first row when rpc returns an array`:失败,`TypeError: supabase.rpc(...).single is not a function`
- `claimScoringJob also accepts the single-object rpc shape`:同样失败
去掉 `.single()` 并兼容数组与单对象之后,这两条通过。真实迁移用例 `claim_birth_time_scoring_job returns an array and claimScoringJob accepts it` 通过:`data` 是数组,第一行 `claim_state` 为 `claimed`,store 返回 `{ kind: "claimed", algorithmVersion }`。第一次启动测试库时迁移进程只留下笼统的 `database migration failed`(脚本会把非预期异常收成这一句),容器未到 Healthy。同一条用例在容器 Healthy 后重跑通过。
`chart-profiles/route.ts` 的 `row` 标为 `unknown`。
修完后 `tsc --noEmit` 退出码 0。`npm run lint` 退出码 0,0 error,123 warning(与改动前的存量警告数量一致,本单没有新增 warning)。
## T3 · 转译差分
类型层改动用 esbuild 0.28.1、`--format=esm`、`--tsconfig=frontend/tsconfig.json`,旧文件与新文件都按这份 tsconfig 转译。除 T2 的两个文件和新建的 `data-client.ts` 外,全部 `SAME`:
- `src/lib/birth-time-journey-case-loader.ts`
- `src/lib/birth-time-journey-store.ts`
- `src/lib/birth-time-journey-turn-persistence.ts`
- `src/lib/db/local-postgres-client-core.ts`
- `src/lib/rectification-agentic/v9/case-service.ts`
- `src/lib/rectification-agentic/v9/product-domain.ts`
- `src/lib/rectification-agentic/v9/tool-service.ts`(只改了一句注释;转译后注释被去掉,产物相同)
- `src/lib/rectification-in-chat-server.ts`
- `src/lib/supabase/admin-client-core.ts`
- `src/lib/supabase/server.ts`
- `src/mastra/rectification-v9-tools.ts`
任务书把旧文件写到系统临时目录再转译。esbuild 0.28.1 会沿文件所在目录寻找 `tsconfig.json`。临时目录找不到本仓库的 `tsconfig.json`(`target` 为 ES2017),类字段的转译结果会和仓库内文件不一致,即使源码只有类型或注释差别也会报 DIFF。两边都带上同一份 `--tsconfig` 后为 SAME。这不是运行时变化。
## T4 · 把 data 收成 unknown 的错误清单
临时把 `UntypedData` 改成 `unknown` 后 `tsc --noEmit` 退出码 2,共 93 条、12 个文件。随后已改回 `any`。
| 条数 | 文件 |
| ---: | --- |
| 24 | `src/app/api/account/route.ts` |
| 17 | `src/app/api/consult/route.ts`(其中 1 条是 `DataClient` 不能赋给 `GuardedTitleClient`,`consult/route.ts` 约 1055 行) |
| 16 | `src/app/api/consult/status/route.ts` |
| 12 | `src/app/api/payment/epay/status/route.ts` |
| 7 | `src/lib/epay/availability.ts` |
| 5 | `src/lib/rectification-agentic/v9/product-domain.ts` |
| 4 | `src/app/api/sessions/route.ts` |
| 3 | `src/lib/rectification-agentic/v9/agent-route-request.ts` |
| 2 | `src/app/api/chart-profiles/[id]/route.ts` |
| 1 | `src/app/api/account/avatar/route.ts` |
| 1 | `src/app/api/chart-profiles/route.ts` |
| 1 | `src/app/api/sessions/[id]/route.ts` |
和任务书写作时的 122 条相比,`consult/route.ts` 少了 10 条,`chart-profiles/route.ts` 少了 1 条(本单已把 `row` 标成 `unknown`)。没有单独再出现一条 `session-title-guard.ts` 的错误,`GuardedTitleClient` 那条记在 `consult/route.ts` 里。这些留给第二阶段,本单没有改那些业务文件。
## test:db
`npm run test:db`(`tsx --test --test-concurrency=1 tests/database-*.test.ts`)约 19 分钟,退出码 1。汇总:85 条,通过 72,失败 13,取消 0,跳过 0。
失败的 13 条与 `tools/agent-loop/windows-known-failures.txt` 逐条一致,没有多出来的失败:
- staging backups are encrypted, atomic, private, and retain the newest three
- rejects destructive backup directory aliases and symlink components before mutation
- rejects unsafe writable backup parents before creating the target
- rejects a direct canonical sticky shared backup directory before chmod
- creates every absent backup path component privately despite a permissive caller umask
- same-second backups publish once without overwriting the completed archive
- find enumeration failures preserve existing backups and do not report completion
- refuses full disks and removes a failed-pipeline partial file
- database env validator accepts punctuated literal required secrets
- database env validator enforces an explicit staging owner uid without printing values
- database env validator accepts a private valid file without printing values
- personal report job migration is atomic, lease-bound, recoverable, and owner read-only
- personal report sections enforce owner-read RLS and service-owned durable transitions
本单新增的 3 条(两条假客户端、一条真实迁移认领)都在通过的 72 条里。
+1 -1
View File
@@ -489,5 +489,5 @@
| `TASK-python-hygiene-20261013.md` | — | 免费层排队开关报反、静默吞异常、功能吉凶兜底合一、测试专用脚本归位(BUG-1340) | 待领取(排队:pl9-render-split) | `codex/python-hygiene-20261013` |
| `TASK-dasha-unify-20261013.md` | — | /api/dasha 改走引擎同一套实现、删占位注册表、Ashtottari 出生余额(BUG-1341、1344) | 待领取(排队:python-hygiene) | `codex/dasha-unify-20261013` |
| `TASK-api-server-service-extract-20261013.md` | — | API 单体第一阶段:拆出不带 HTTP 的计算类,生产伪造 handler 归零 | 待领取(排队:dasha-unify) | `codex/api-server-service-extract-20261013` |
| `TASK-data-layer-types-20261013.md` | — | 数据访问层改用自己的 DataClient 类型第一阶段(BUG-1343) | 待领取(排队:node-runtime-resources) | `codex/data-layer-types-20261013` |
| `TASK-data-layer-types-20261013.md` | `PROGRESS-data-layer-types-20261013.md` | 数据访问层改用自己的 DataClient 类型第一阶段(BUG-1351;任务书预留的 1343 已被更大编号越过) | 待验收 | `codex/data-layer-types-20261013` |
| `TASK-contracts-helpers-dedupe-20261013.md` | — | 前后端契约与小工具去重第一阶段 | 待领取(排队:frontend-dead-code、consult-route-split) | `codex/contracts-helpers-dedupe-20261013` |