diff --git a/BLOCKED.md b/BLOCKED.md index d08c8c52..322396fb 100644 --- a/BLOCKED.md +++ b/BLOCKED.md @@ -166,3 +166,18 @@ - 本单范围:只记录。不改引擎、不改门槛、不改断言来让它变绿。 - 相关:BUG-560 根因升级(分钟级原始分区分力≈随机);`docs/tasks/TASK-rectification-convergence-exit-20260906.md` 验收段。 + +## BLK-002 · 校正标题/活跃时间修补迁移无法本机实跑(2026-09-16) + +- 状态:blocked(环境缺口,非代码缺陷) +- 分支:`codex/rectification-title-repair-migration-20260916` +- 对象:`frontend/supabase/migrations/20260916020000_rectification_session_title_repair.sql`(BUG-699 / BUG-704 的一次性数据修补) +- 缺什么:本机既没有 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 清单。 +- 替代证据(都已实跑): + - 迁移的两段 SQL 与 `frontend/scripts/repair-rectification-session-titles.mjs` 逐字一致,由 `frontend/tests/rectification-session-title-repair-migration.test.ts` 比对,5/5 通过; + - 迁移器的文件级校验(文件名正则、重复文件名、目录扫描)在连库前完成——用假连接串调 `runMigrations({check:true})`,报错停在 `connect ECONNREFUSED` 而不是 `invalid migration filename`,说明新文件名与排序被接受; + - `update public.chat_sessions as session`、`join lateral`、`get diagnostics ... = row_count` 三种写法在已应用的迁移里都有先例(`20260808030000`、`20260901010000`、`20260811020000`); + - 全量 `npm test`:3334 tests / 31 fail,与同一工作树上 `origin/staging` @ `37e6c519` 的 3329 / 31 逐条一致(本轮 +5 test 全绿,失败集合未变)。 +- 真实证据待补:产品在 staging 点一次 `Migrate Staging Database`,从运行日志的 `notice ... repaired_titles=` / `notice ... refreshed_activity=` 读回行数,回填进 BUG-699 / BUG-704 的「验证」。解除后划掉本条而不是删除。 diff --git a/deploy/README.md b/deploy/README.md index d3c1a198..3fd540d2 100644 --- a/deploy/README.md +++ b/deploy/README.md @@ -214,6 +214,8 @@ curl -fsS https://staging.jyotisha.chat/api/health The normal application deployment workflow never runs database migrations. Apply migrations to the private staging PostgreSQL cluster first, verify them, and only then deploy application code that depends on them. +One-off data repairs go through the same `Migrate Staging Database` button, not through ad-hoc scripts on the host: ship the repair as an ordinary migration under `frontend/supabase/migrations/` so the migrator applies it exactly once, and have it report how many rows it touched with `RAISE NOTICE`. `scripts/db-migrate.mjs` forwards notices, so those counts appear in the migration run log as `notice ` lines — that log is where you read the result. Write such repairs to be idempotent anyway (their `where` must stop matching rows they already fixed), so a manual re-run cannot double-apply. `frontend/supabase/migrations/20260916020000_rectification_session_title_repair.sql` is the worked example. + ## Staging PostgreSQL operations This section is the server-side runbook for the disposable staging PostgreSQL volume. It does not replace the production instructions above. diff --git a/docs/BUG_HISTORY.md b/docs/BUG_HISTORY.md index bd5d6fb2..c0289552 100644 --- a/docs/BUG_HISTORY.md +++ b/docs/BUG_HISTORY.md @@ -10863,8 +10863,8 @@ - 用户现象:点开一条以前的生时校正,标题变成今天的日期,并出现在列表最上面。刷新后位置会回去,标题不会。 - 触发条件:点侧栏里已有的校正会话,或从首页校正卡打开一条已存在的可续校正。 - 根因:复发自 BUG-553。`openRectificationCase` 构造 `merged` 时 `pinned` / `archivedAt` 从 `existing` 继承,`title` 和 `updatedAt` 却无条件用墙钟重算,再 `persistSession` 把标题写回服务端。`persistSession` 的 update 不写 `updated_at`(BUG-553 的修复仍在),所以排序只是客户端暂时错位。BUG-553 的防复发当时只是一句话,测试只锁了元数据 PATCH 和改名/收藏/归档/换模型/换资料,没有覆盖校正 open。 -- 修复:已有会话的 `title` / `updatedAt` 继承 `existing`,只有新建才 `resolveSessionTitle` + `timestamp()`。错日期标题按 `created_at` 的 Asia/Shanghai 月日修回;对不上正则的手改标题不动。 -- 验证:`frontend/tests/session-open-preserves-identity.test.ts`、`rectification-session-title-repair.test.ts`。浏览器级验收见 `docs/testing/rectification-open-identity-20260915.md`。staging 标题修回行数见本单 PROGRESS。 +- 修复:已有会话的 `title` / `updatedAt` 继承 `existing`,只有新建才 `resolveSessionTitle` + `timestamp()`。错日期标题按 `created_at` 的 Asia/Shanghai 月日修回;对不上正则的手改标题不动。存量错标题的修补已改为一次性迁移 `20260916020000_rectification_session_title_repair.sql`,随 `Migrate Staging Database` 按钮应用(原先只写在 `frontend/scripts/repair-rectification-session-titles.mjs` 里,要 `SCHEMA_DATABASE_URL` 才能跑,产品没有执行通道,所以一次都没跑过);该脚本降级为只读核对工具。 +- 验证:`frontend/tests/session-open-preserves-identity.test.ts`、`rectification-session-title-repair.test.ts`、`rectification-session-title-repair-migration.test.ts`(比对迁移与核对脚本的 SQL 是否仍逐字一致)。浏览器级验收见 `docs/testing/rectification-open-identity-20260915.md`。**欠账**:staging 标题修回行数待产品点一次 `Migrate Staging Database` 后,从运行日志的 `notice ... repaired_titles=` 回填。无 Docker 环境跑不了 `npm run test:db`,迁移未实跑过。 - 防复发:构造可能作用于已有会话的 `ChatSession` 时,不得无条件写 `updatedAt: timestamp()`。`resolveSessionTitle` 的 `at` 默认墙钟,只能用于新建。 - 相关记录:BUG-553、BUG-704、BUG-705 - 复发自:BUG-553 @@ -10943,8 +10943,8 @@ - 用户现象:一条校正连着答几小时,侧栏位置仍停在创建那天。叠加上被改成今天的标题后,列表变成「名字是今天、排序是旧日期」。 - 触发条件:在生时校正里写 turn、点选、采用或停止。打开、刷新、改名、收藏不触发。 - 根因:复发自 BUG-553 的另一半。全仓只有咨询 RPC `append_consultation_question` 写 `chat_sessions.updated_at`。校正 turn 走 `append_agentic_rectification_turn` 等,只推 `agentic_rectification_cases.last_activity_at`,不碰会话表。`persistSession` 的 update 按 BUG-553 故意不写 `updated_at`。 -- 修复:`last_activity_at` 推进时触发器同步把对应校正会话的 `updated_at` 推到同一时刻,且不得回拨。历史冻结行按最后一条 turn / `last_activity_at` 回填。客户端 `persistSession` 仍不写 `updated_at`。 -- 验证:`frontend/tests/rectification-v9-migration.test.ts` 的 touch-session 迁移合同。浏览器级见 `docs/testing/rectification-open-identity-20260915.md`。staging 回填行数见本单 PROGRESS。 +- 修复:`last_activity_at` 推进时触发器同步把对应校正会话的 `updated_at` 推到同一时刻,且不得回拨。历史冻结行按最后一条 turn / `last_activity_at` 回填,该回填同样已改为一次性迁移 `20260916020000_rectification_session_title_repair.sql`(与 BUG-699 的标题修补同一条),随 `Migrate Staging Database` 应用;`updated_at <` 的单调守卫保证只前进不回拨,重复应用影响 0 行。客户端 `persistSession` 仍不写 `updated_at`。 +- 验证:`frontend/tests/rectification-v9-migration.test.ts` 的 touch-session 迁移合同、`rectification-session-title-repair-migration.test.ts` 的回填 SQL 合同。浏览器级见 `docs/testing/rectification-open-identity-20260915.md`。**欠账**:staging 回填行数待产品点一次 `Migrate Staging Database` 后,从运行日志的 `notice ... refreshed_activity=` 回填。无 Docker 环境跑不了 `npm run test:db`,迁移未实跑过。 - 防复发:校正对话活动必须推进 `chat_sessions.updated_at`。元数据 PATCH、打开、刷新不得推进。不得为了修这个让 `persistSession` 重新写 `updated_at`。 - 相关记录:BUG-553、BUG-699、BUG-705 - 复发自:BUG-553 diff --git a/docs/tasks/PROGRESS-rectification-title-repair-migration-20260916.md b/docs/tasks/PROGRESS-rectification-title-repair-migration-20260916.md new file mode 100644 index 00000000..b026a578 --- /dev/null +++ b/docs/tasks/PROGRESS-rectification-title-repair-migration-20260916.md @@ -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 ` 走既有 `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= +notice rectification_session_title_repair refreshed_activity= +``` + +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,新测试里有断言)。 diff --git a/docs/tasks/README.md b/docs/tasks/README.md index c5602acd..9b37120c 100644 --- a/docs/tasks/README.md +++ b/docs/tasks/README.md @@ -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 段 707–709 | 已验收(带修复单) | `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` |