- 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
11 KiB
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。全部仍在。
另外两件影响正确性、任务书没提、我主动核过的事:
- 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,两个都不触发。 - 没有自动
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,errcode42501。 - 两段 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 文件不该有的行尾空格)。以后改了任何一边而没同步另一边,这条测试就红。
复核者可以自己再验一遍:
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 幂等性怎么保证的
两层,互相独立:
- 迁移器层:
migration.schema_migrations以文件名为主键记账,db-migrate.mjs见到已记账的文件就already applied ... continue。正常路径下这条迁移一辈子只执行一次。 - 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. 产品侧下一步
- 等门禁转绿、
Deploy Staging发上去。 - Gitea →
Migrate Staging Database→ 填那个 SHA → 运行。 - 在日志里找这两行,就是修了多少行:
notice rectification_session_title_repair repaired_titles=<n>
notice rectification_session_title_repair refreshed_activity=<n>
- 刷新页面,看历史校正会话的名字是否恢复成真实日期。
- 把两个数字回填进
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,新测试里有断言)。