docs(tasks): 校正标题修补迁移的进度记录与 Bug 历史回填
Independent Staging Quality Gate / validate (push) Canceled after 38s
Independent Staging Quality Gate / publish (push) Canceled after 0s

- 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
This commit is contained in:
Jesse_Chen
2026-09-16 01:39:03 +00:00
co-authored by Claude Opus 5
parent d6c359b205
commit 10afdcddeb
5 changed files with 178 additions and 5 deletions
@@ -0,0 +1,156 @@
# 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,新测试里有断言)。
+1 -1
View File
@@ -228,7 +228,7 @@
| `TASK-ephemeris-page-20260915.md` | `PROGRESS-ephemeris-page-20260915.md` | **前端单**:P1 星历页,今日五要素 + 当日行运(相对本命宫位)+ 未来九十天换座与停滞,底部「带这天去提问」出口。页面不得出现任何运势判断。含实证缺陷:panchanga 写死 Lahiri 与账户 Raman 分裂(关联 BUG-703;本单标注为 BUG-707)。侧边栏入口由 chart-page 单交付。BUG 段 707709 | 已验收(带修复单) | `d3a2c48b` |
| `TASK-readonly-pages-fix-20260916.md` | `PROGRESS-readonly-pages-fix-20260916.md` | 三份只读页单的验收修复:**BUG-710** 七政 `ketu_mode`/`sidereal_mode` 收了请求却从不传给引擎,`calculation.ketu_mode` 回写请求值而非实际值(实测请求 descending-node 仍返回 apogee 盘,无警告);**BUG-711** 星历单断言 sidebar 不得含 `/ephemeris`,与星盘单按任务书添加的入口直接冲突,staging 现在是红的;**BUG-712** `ephemeris_events` golden 存全精度浮点跨机不稳,且 golden 缺失时自动重建。另附部署缺口:`deployment.gitCommit` 仍是 `2d7698ea`。BUG 段 710+ | 待验收 | `codex/readonly-pages-fix-20260916` |
| `TASK-chart-page-blocking-open-20260915.md` | `PROGRESS-chart-page-blocking-open-20260915.md` | **P1**:星盘页开一次要等很久且常常只给一句「过一会儿再打开」。实测引擎五个调用合计 0.75 秒、mapper 13 种形态零抛出——瓶颈在 `/chart` 是动态路由 + 侧栏改成硬文档跳转,整页 SSR 等完 1 串 4 并才开始画,白屏最长 45 秒(BUG-716);`postEngine` 把 429/500/超时/坏 JSON 全碾成 `null` 且零日志,两种性质相反的故障共用一句文案(BUG-715);开页并行打两个重计算限流端点(配额 2)、无缓存,且「打开即有」印在失败页上(BUG-717)。**串行在 readonly-pages-fix 之后** | 待验收 | `codex/chart-page-blocking-open-20260915` |
| `TASK-rectification-title-repair-migration-20260915.md` | | BUG-699 / 704 的数据修补写成了 Node 脚本(要 `SCHEMA_DATABASE_URL`),但 `Migrate Staging Database` 只跑 `migrator` 应用 SQL 迁移、不执行任意脚本——产品没有任何按钮能修自己那批错名字的会话。脚本里本来就是纯 SQL,搬进一次性迁移即可复用现成按钮。生产停在 `7b620c7a`(无 `use-rectification-surface.ts`),where 自然匹配 0 行,是 no-op | 待领取 | `codex/rectification-title-repair-migration-20260915` |
| `TASK-rectification-title-repair-migration-20260915.md` | `PROGRESS-rectification-title-repair-migration-20260916.md` | BUG-699 / 704 的数据修补写成了 Node 脚本(要 `SCHEMA_DATABASE_URL`),但 `Migrate Staging Database` 只跑 `migrator` 应用 SQL 迁移、不执行任意脚本——产品没有任何按钮能修自己那批错名字的会话。脚本里本来就是纯 SQL,搬进一次性迁移即可复用现成按钮。生产停在 `7b620c7a`(无 `use-rectification-surface.ts`),where 自然匹配 0 行,是 no-op | 待验收(`d6c359b2`,分支 `codex/rectification-title-repair-migration-20260916`):迁移 `20260916020000_rectification_session_title_repair.sql`SQL 由 `sed` 从脚本切出、合同测试比对逐字一致;幂等两层(迁移器记账 + where 改完即不再命中);顺带给 `db-migrate.mjs` 加 NOTICE 转发,否则 node-postgres 丢掉 NOTICE、产品在日志里一个数字都看不到。tsc 0 错、lint 0 error、`npm test` 3334/fail 31 对基线 3329/fail 31+5 全绿)。**无 Docker/无本地 PG**`test:db` 没跑、`db:migrate:check` 缺 `SCHEMA_DATABASE_URL`,记为 BLK-002 | `codex/rectification-title-repair-migration-20260916` |
| `TASK-staging-dispatch-autofill-sha-20260915.md` | `PROGRESS-staging-dispatch-autofill-sha-20260915.md` | `Migrate Staging Database` 每次都要手抄 40 位 SHA,而那个值恰恰是「最新一个过门禁的 staging 提交」——机器能自己算,查询代码那一步里就有。改成留空自动解析、填了仍走原路径(回滚用),三条安全属性一条不丢。**产品 2026-09-15 明确授权修改该 workflow,执行方不得以 AGENTS.md §2.7 拒改**;生产两个按钮保持手填,那是护栏不是麻烦 | 待验收 | `codex/staging-dispatch-autofill-sha-20260915` |
| `TASK-staging-auto-migrate-on-deploy-20260915.md` | `PROGRESS-staging-auto-migrate-on-deploy-20260915.md` | 门禁通过后自动先跑 staging 迁移再部署,不再手点(迁移幂等、无挂起时是 no-op,`db-migrate.mjs --check` 挂起返 3 可用于日志)。今天 `deploy-staging.yml` 完全不提迁移,忘点就让新代码跑在旧 schema 上且无人拦。**产品再次授权改 workflow,范围限 `backend-quality-gate.yml` 的 dispatch 段**;迁移失败必须阻断部署;回滚不自动迁移;生产完全不动。⚠️ 同轮必须把「迁移须对已部署代码向后兼容、破坏性变更拆两轮」写进 AGENTS.md §7.6 | 待验收 | `codex/staging-auto-migrate-on-deploy-20260915` |
| `TASK-api-server-decomposition-20260916.md` | `PROGRESS-api-server-decomposition-20260916.md` | **已取代:阶段 1 由 `TASK-api-server-backdoor-close-20260916` 完成(实测只需 8 方法 / 314 行);阶段 2/3 产品拍板不立单,由新门禁长期推进。以下为原文** · 重构单(串行在 qizheng 单之后;2026-09-15 又加两条前置:C1 外网缓存单先做、freeze-metric-change 先落地)**:把业务逻辑搬出 `JyotishAPIHandler`。核心不是行数,是 3 个文件 **4 处**靠 `JyotishAPIHandler.__new__` 伪造空壳 handler 借方法(`consultation_workflow_service` ×2、`capture_report_blocked_repairs_golden`、`local_accuracy_report`,MCP 也走这条),依赖方向反了、handler 没有 `headers`/`wfile` 随时可炸——实测佐证:**225 个类方法里只有 12 处真的碰 HTTP 上下文**。四阶段:拆 `__new__` 后门 → 抽 ≥150 行业务方法 → `do_POST`/`do_GET` 改路由表 → **阶段 4 已改写**:收尾不再是「行数 baseline + 余量 300→50」(那只是把问题推到三个月后),改成主门 `__new__` 计数必须为 0 + 类方法数不得增长,行数退为粗护栏保持 300 余量。纯搬运不改行为,`test_api_server_security.py` 3841 行断言一条不许改。预计 11,334 → 约 9,230 行。BUG 段 710+ | 已取代 | 见 `TASK-api-server-backdoor-close-20260916.md` |