- PROGRESS-rectification-title-repair-migration-20260916.md:前提复核、 逐字一致的复核命令、幂等两层的说明、基线比对与环境缺口。 - BUG_HISTORY:BUG-699 / BUG-704 的「修复」补记改由迁移承担,「验证」 的欠账改成待产品在 staging 应用后从 NOTICE 回填。不新增编号。 - BLOCKED.md:BLK-002 无 Docker / 无本地 PostgreSQL,test:db 未跑、 db:migrate:check 缺 SCHEMA_DATABASE_URL,附已实跑的替代证据。 - deploy/README.md:一次性数据修补走同一个 Migrate Staging Database 按钮,行数看日志里的 notice 行。 - docs/tasks/README.md:该单改记待验收。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JUei7K13cYxLHE3Axe4A45
157 lines
11 KiB
Markdown
157 lines
11 KiB
Markdown
# PROGRESS · 把会话标题/活跃时间修补做成迁移
|
||
|
||
- 日期:2026-09-16
|
||
- 任务书:`docs/tasks/TASK-rectification-title-repair-migration-20260915.md`
|
||
- 分支:`codex/rectification-title-repair-migration-20260916`
|
||
- 实际基线:`origin/staging` @ `37e6c519`(任务书写的 `e7016551` 早已被超车,前提已逐条复核,见 §1)
|
||
- 未推 staging,按指派只提交到本分支,验收与合入由指派方做。
|
||
|
||
---
|
||
|
||
## 1. 开工前的前提复核(结论:前提全部成立)
|
||
|
||
| 任务书的前提 | 复核方式 | 结论 |
|
||
| --- | --- | --- |
|
||
| 修补脚本仍在 | `frontend/scripts/repair-rectification-session-titles.mjs` | 在,3601 字节 |
|
||
| 脚本仍是纯 SQL,Node 只连库/打印 | 通读全文 | 成立:`TITLE_MATCH_SQL` / `TITLE_APPLY_SQL` / `ACTIVITY_APPLY_SQL` 全是 SQL 字符串 |
|
||
| 数据还没被别的轮次修过 | `frontend/supabase/migrations/` 下无对应迁移 | 成立,无人做过 |
|
||
| where 条件对得上当前 schema | 逐列核对 | 成立,见下 |
|
||
| 标题格式没变 | `frontend/src/lib/agent-reply.ts` 的 `datedSessionTitle` | 成立,仍是 `M月D日 · 生时校正`,`uniquifySessionTitle` 仍可追加 ` HH:MM`,与脚本正则完全对应 |
|
||
|
||
schema 逐列核对:`chat_sessions` 的 `title` / `created_at` / `updated_at` / `user_id` 在 `20260715030000`,`session_type` 在 `20260721110000`(取值仍含 `birth_time_rectification`);`agentic_rectification_cases` 的 `session_id` / `user_id` / `last_activity_at` 与 `agentic_rectification_turns` 的 `case_id` / `created_at` 在 `20260812010000`。全部仍在。
|
||
|
||
另外两件影响正确性、任务书没提、我主动核过的事:
|
||
|
||
1. **UPDATE 不会误触发触发器。** `chat_sessions` 上只有两个触发器,`chat_sessions_default_rectification_model` 与 `chat_sessions_pin_model_config_version`,都是 `before insert or update of <指定列>`(`session_type` / `model_id` / `model_config_version`)。本迁移只写 `title` 与 `updated_at`,两个都不触发。
|
||
2. **没有自动 `updated_at` 触发器。** 所以标题那段 update 不会顺带把 `updated_at` 推成今天、把会话顶到侧栏最前——那正是 BUG-699 的现象本身,踩上就等于一边修一边复发。
|
||
|
||
---
|
||
|
||
## 2. 做了什么
|
||
|
||
### 2.1 新增迁移(任务 1)
|
||
|
||
`frontend/supabase/migrations/20260916020000_rectification_session_title_repair.sql`
|
||
|
||
- 时间戳排在当前最后一条 `20260916010000_consultation_session_capacity.sql` 之后。
|
||
- 权限守卫沿用 `20260915010000` 的写法:`current_user <> 'schema_owner'` 就 `raise exception`,errcode `42501`。
|
||
- 两段 update 放在一个 `do $repair$` 块里,各自 `get diagnostics ... = row_count` 后 `raise notice` 打出行数。
|
||
- 顺序与脚本一致:先标题,后活跃时间。
|
||
|
||
**SQL 逐字一致是机器保证的,不是我说的。** 迁移的 SQL 不是手敲的,是用 `sed` 从脚本里按行切出来拼进去的;随后
|
||
`frontend/tests/rectification-session-title-repair-migration.test.ts` 把脚本导出的三个常量与迁移正文比对(只做行尾空白归一化,因为脚本拼的是 `where ${TITLE_MATCH_SQL}`,会留一个 `.sql` 文件不该有的行尾空格)。以后改了任何一边而没同步另一边,这条测试就红。
|
||
|
||
复核者可以自己再验一遍:
|
||
|
||
```bash
|
||
cd frontend
|
||
diff <(sed -n '38,45p' supabase/migrations/20260916020000_rectification_session_title_repair.sql) \
|
||
<(sed -n '35,42p' scripts/repair-rectification-session-titles.mjs) # TITLE_APPLY
|
||
diff <(sed -n '47,55p' supabase/migrations/20260916020000_rectification_session_title_repair.sql) \
|
||
<(sed -n '21,29p' scripts/repair-rectification-session-titles.mjs) # TITLE_MATCH
|
||
diff <(sed -n '59,70p' supabase/migrations/20260916020000_rectification_session_title_repair.sql) \
|
||
<(sed -n '62,73p' scripts/repair-rectification-session-titles.mjs) # ACTIVITY_APPLY
|
||
```
|
||
|
||
三个 diff 都是空的。Asia/Shanghai 时区、正则、`case`、`coalesce(turns.last_turn_at, case_row.last_activity_at)`、`updated_at <` 单调守卫,一个字都没动。
|
||
|
||
### 2.2 幂等性怎么保证的
|
||
|
||
两层,互相独立:
|
||
|
||
1. **迁移器层**:`migration.schema_migrations` 以文件名为主键记账,`db-migrate.mjs` 见到已记账的文件就 `already applied ... continue`。正常路径下这条迁移一辈子只执行一次。
|
||
2. **SQL 自身**(真正兜底的一层,防的是有人手工重放):
|
||
- 标题:改完之后,`title` 里的月日就等于 `created_at` 的 Asia/Shanghai 月日,`is distinct from` 两个条件同时为假;`created_at is null` 的行被改成不带日期的 `生时校正`,连 `title ~ '^[0-9]{1,2}月...'` 正则都不再匹配。where 整体不再命中这些行。
|
||
- 活跃时间:改完之后 `session.updated_at` 恰好等于 `coalesce(turns.last_turn_at, case_row.last_activity_at)`,`<` 为假。`<` 同时也是单调守卫,任何情况下只前进不回拨。
|
||
|
||
所以重复应用影响 0 行,`RAISE NOTICE` 会打出 `repaired_titles=0` / `refreshed_activity=0`。
|
||
|
||
### 2.3 一处任务书没写、但不补就等于白做的改动
|
||
|
||
任务书 §5 任务 1.4 说「`RAISE NOTICE` 打出的行数会出现在 `Migrate Staging Database` 的日志里——这是产品唯一能看到数字的地方」。**照原样实现的话,那个数字不会出现。**
|
||
|
||
staging 的 migrator 服务跑的是 `npm run db:migrate` → `frontend/scripts/db-migrate.mjs`,用的是 node-postgres。node-postgres 把 NOTICE 作为 Client 的 `notice` 事件抛出,**没有监听器就直接丢掉**,既不报错也不打印。原来的 `db-migrate.mjs` 全文没有 `notice` 二字。
|
||
|
||
所以在 `runMigrations` 里加了 4 行 notice 转发,把 NOTICE 打成 `notice <message>` 走既有 `logger`。这不是顺手改别的东西,是任务 1.4 的落地前提;没有它,产品点完按钮看到的是一片空白。
|
||
|
||
边界都守住了:没动 `deploy/run-staging-migration.sh`、没动 `deploy/docker-compose.postgres.yml`、没动任何 `.gitea/workflows/**`(任务书硬红线 2)。迁移仍由**现有** migrator 原样应用。没有测试断言过 migrator 的日志行,只有 `tests/model-configuration-security.test.ts` 传了个 `logger: console` 进去,不校验输出。
|
||
|
||
### 2.4 脚本降级为只读核对(任务 2)
|
||
|
||
`frontend/scripts/repair-rectification-session-titles.mjs`:
|
||
|
||
- 文件头注明修补已由迁移承担,本文件只用于核对,并指名那条迁移。
|
||
- `--apply` 不再写库,直接报错并指向迁移与 `Migrate Staging Database` 按钮(实测退出码 1)。
|
||
- 保留两段 count 查询;**文件没删**(任务书 2.2)。
|
||
- 三个 SQL 常量改为 `export`,让它成为迁移的唯一出处、并被合同测试读取——顺带也让它们不再是 lint 眼里的死变量。
|
||
- 加了 `invokedPath === import.meta.url` 守卫。原来第 95 行的 `main().catch(...)` 是无条件执行的,`import` 这个文件就会去连库;合同测试要导入它,不加这个守卫测试会去连数据库。
|
||
|
||
---
|
||
|
||
## 3. 验证
|
||
|
||
| 项 | 命令 | 结果 |
|
||
| --- | --- | --- |
|
||
| 类型 | `./node_modules/.bin/tsc --noEmit` | **0 错** |
|
||
| lint | `npm run lint` | **0 error**(120 warning,全部在本轮未改的文件里;`scripts/db-migrate.mjs`、`scripts/repair-rectification-session-titles.mjs`、新测试三者 0 条) |
|
||
| 新合同测试 | `npx tsx --test tests/rectification-session-title-repair-migration.test.ts` | **5/5 通过** |
|
||
| 全量测试(本分支) | `npm test` | tests **3334** / pass 3288 / **fail 31** / skipped 15 |
|
||
| 全量测试(基线) | 同一工作树 detach 到 `origin/staging` @ `37e6c519` 后 `npm test` | tests **3329** / pass 3283 / **fail 31** / skipped 15 |
|
||
| 迁移文件级校验 | 假连接串调 `runMigrations({check:true})` | 报错停在 `connect ECONNREFUSED`,说明文件名正则/重复检查/目录扫描都过了 |
|
||
| `db:migrate:check` | `npm run db:migrate:check --prefix frontend` | **未通过——环境缺口**,见 §4 |
|
||
| `test:db` | `npm run test:db --prefix frontend` | **没跑——无 Docker**,见 §4 |
|
||
|
||
基线比对结论:本轮**净增 5 个测试且全绿,31 条失败一条不多一条不少**,测试总数只增不减,没有改写或弱化任何既有断言。
|
||
|
||
CLI 行为实测:
|
||
|
||
```
|
||
$ node scripts/repair-rectification-session-titles.mjs --apply
|
||
--apply has been removed; the repair now ships as migration 20260916020000_rectification_session_title_repair.sql. Apply it with Gitea -> Migrate Staging Database.
|
||
exit=1
|
||
$ node scripts/repair-rectification-session-titles.mjs
|
||
SCHEMA_DATABASE_URL is required
|
||
exit=1
|
||
```
|
||
|
||
---
|
||
|
||
## 4. 环境缺口(不得记成通过)
|
||
|
||
本机**没有 Docker**(`docker info` 失败),**也没有本地 PostgreSQL**(`which psql postgres pg_ctl initdb` 全空)。因此:
|
||
|
||
- `npm run test:db --prefix frontend` 没跑。
|
||
- `npm run db:migrate:check --prefix frontend` 在连库之前就以 `SCHEMA_DATABASE_URL is required` 退出 1,拿不到 pending 清单。这是缺库缺口,不是迁移本身有问题——同一入口的文件级校验已单独走通(见 §3)。
|
||
- **这条迁移的 SQL 从未在真实 PostgreSQL 上执行过**(它原本所在的脚本也一次都没跑过)。静态旁证是三种写法在已应用的迁移里都有先例:`update public.chat_sessions as session`(`20260808030000`、`20260901010000`)、`join lateral`(`20260814020000` 等)、`get diagnostics ... = row_count`(`20260811020000`)。旁证不等于实跑。
|
||
|
||
已登记为 `BLOCKED.md` 的 **BLK-002**。真实证据只能由产品在 staging 点一次按钮后回填。
|
||
|
||
---
|
||
|
||
## 5. 产品侧下一步
|
||
|
||
1. 等门禁转绿、`Deploy Staging` 发上去。
|
||
2. Gitea →`Migrate Staging Database`→ 填那个 SHA → 运行。
|
||
3. 在日志里找这两行,就是修了多少行:
|
||
|
||
```
|
||
notice rectification_session_title_repair repaired_titles=<n>
|
||
notice rectification_session_title_repair refreshed_activity=<n>
|
||
```
|
||
|
||
4. 刷新页面,看历史校正会话的名字是否恢复成真实日期。
|
||
5. 把两个数字回填进 `docs/BUG_HISTORY.md` 的 BUG-699 / BUG-704「验证」段,并划掉 `BLOCKED.md` 的 BLK-002。
|
||
|
||
生产停在 `7b620c7a`,没有 `use-rectification-surface.ts`,标题固定为「生时校正」且不写库,两段 where 自然匹配 0 行。**将来这条随 `main` 提升进生产时打出 `repaired_titles=0` 是预期结果,不是失败。**
|
||
|
||
---
|
||
|
||
## 6. 没做的事
|
||
|
||
- 没推 `staging`,没碰 `main`。
|
||
- 没动 `deploy/run-staging-migration.sh`、`deploy/docker-compose.postgres.yml`、任何 `.gitea/workflows/**`。
|
||
- 没改任何前端行为:`src/` 一行未动。
|
||
- 没动 `CHANGELOG.md`(任务书 3.2:用户可见行为不变)。
|
||
- 没新增 BUG 编号(任务书 3.2:这是交付通道补齐,不是新缺陷)。
|
||
- 没把迁移复制进 `frontend/db/migrations/`(BUG-127 / BUG-144,新测试里有断言)。
|