Files
Jyotisha/docs/tasks/TASK-rectification-session-title-result-20260922.md
T
Jesse_ChenandClaude Opus 5 c6d421edba docs(tasks): 校正会话标题改写结果而非日期(BUG-1001)
BUG-988 后标题日期与副标题重复,且 BUG-929 删 uniquify 后同日多条
标题相同。产品拍板标题承载 accepted/confirmed 分钟或 candidate_range,
存量批量重算。红线:不得 bump updated_at,标题算式只能有一处实现。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0199rbQDTsUbCVw84wc8BTFe
2026-09-22 13:24:23 +08:00

270 lines
16 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# TASK 校正会话标题改为写结果,不再写日期(BUG-1001)
- 日期:2026-09-22
- 基线 commit:`3fed71ab`(`origin/staging` head;开工 `git fetch` 后以实际 head 为准。已部署 SHA 当时是 `1bc6a954`,门禁在 `6d06fca0` 红过,与本单无关)
- 分支:`codex/rectification-session-title-result-20260922`
- BUG 编号起点:**BUG-1001**(`docs/BUG_HISTORY.md` 当前最大号 `BUG-1000`,开工时复核)
- 关联记录:**BUG-929**(本单推翻其标题口径)、BUG-699、BUG-704、BUG-988、BUG-987、BUG-992
---
## 1. 事故实证
产品负责人 2026-09-22 的侧栏截图(真机 staging):
| 标题 | 副标题 |
| --- | --- |
| 今日节奏 · 9月18日 | 9月18日 11:18 |
| **9月17日 · 生时校正** | 9月17日 19:12 |
| 9月17日 · 生时校正 **09:35** | 9月17日 **11:15** |
| **9月17日 · 生时校正** | 9月17日 07:57 |
| **9月17日 · 生时校正** | 9月17日 00:24 |
| 9月16日 · 生时校正 | 9月16日 22:25 |
| 9月16日 · 今日节奏 | 9月16日 18:34 |
| 9月16日 · 生时校正 | 9月16日 16:20 |
| …9月15 / 9月14 共 8 条,其中 9月14 一天 5 条 | … |
三个问题:
1. **日期印了两遍。** BUG-988 把副标题改成最后活动时间之后,标题里的日期就是冗余。
2. **标题不再能区分同一天的多条。** `uniquifySessionTitle` 在 BUG-929 那轮删掉了,`resolveSessionTitle`
现在对校正入口只返回 `datedSessionTitle(at, "生时校正")`(`frontend/src/lib/agent-reply.ts`,符号定位
`function datedSessionTitle` 与 `export function resolveSessionTitle`)。截图里 9月17日 三条标题**完全相同**,
9月16日 两条也相同;9月14日 那天有 5 条。日期这个维度根本不够用。
3. **旧标题里的钟点和副标题对不上。** 带后缀那几条(`… 生时校正 09:35` / 副标题 `11:15`,
`… 02:14` / `02:19`,`… 15:37` / `15:38`)是 BUG-929 删 `uniquify` 之前留下的历史标题,
BUG-929 当时决定「旧标题不批量改」。标题里的钟点是**创建时间**,副标题现在是**最后活动时间**,
同一行摆着两个对不上的时间,读起来像 bug。
产品负责人的原话是「既然下面有实际日期了,标题是不是就不需要日期了」。
但**只做减法会让标题这一列信息量归零**——13 行全叫「生时校正」。所以本单是替换,不是删除。
---
## 2. 根因
侧栏行有两列信息:标题和副标题。BUG-988 把副标题定成「最后活动时间」之后,
标题继续承载日期,就同时犯了两个错:与副标题重复,且不承载这次会话的内容。
普通咨询会话的标题是模型对首问的总结(内容摘要),校正会话没有这条路
(`shouldGenerateSessionTitle` 对 `birth_time_rectification` 直接 `return false`),
于是退化成了日期。而校正会话真正有区分度、用户真正想在列表里找的,是**这次校正算出了什么**。
---
## 3. 决策记录
产品负责人 2026-09-22 就地拍板:
- **D1(推翻 BUG-929 的「新标题 `生时校正 · M月D日`」)**:校正会话标题改为承载**校正结果**:
- 有 `confirmed_time` 或 `accepted_time` → `生时校正 · HH:MM`
- 否则 → `生时校正 · <start>–<end>`(用 `candidate_range`,该列 `not null`,永远有值)
BUG-929 的其余决定保留:**不得再给同名标题追加墙钟 HH:MM 后缀**(那正是问题 3 的来源)。
- **D2(推翻 BUG-929 的「旧标题不批量改」)**:存量校正会话标题一次性批量重算,
日期在前的旧格式与钟点后缀一并清掉。有现成先例:`20260916020000_rectification_session_title_repair.sql`
就是干这个的,路走过。不改就会上半截新格式、下半截旧格式,比现在还乱。
- **D3(我的判断,产品未单独过问,按例外处理)**:**「今日节奏」保留日期**。它一天一条,
日期就是它的全部内容,没有别的可写;去掉就全叫「今日节奏」。副标题重复一次日期是可接受的代价。
若产品验收时不同意,另开一轮,本单不动它。
- **D4(对 BUG-699 边界的有限扩展)**:BUG-699 的防复发写的是「打开已有会话仍不得改 title / updatedAt」。
本单要在**校正结果变化时**改 title。这**不是**打开会话时改,`open` 路径一个字都不动。
执行方不得以 BUG-699 为由拒改,但也不得顺手放宽 open 路径。
---
## 4. 硬红线
1. **不得 bump `updated_at`。** 写标题的任何路径都不许碰 `chat_sessions.updated_at`
(BUG-988 / BUG-704)。存量回填迁移一旦动了它,13 条历史会话会集体跳到列表顶端、全显示今天——
那会直接毁掉刚修好的 BUG-988。回填迁移必须有「`updated_at` 逐行不变」的断言。
2. **运行时与回填必须共用同一个标题函数。** 今天已经在同一件事上吃过两次亏:
BUG-987 是服务端与客户端两套相反的过滤规则,BUG-992 是前端与 Python 两套不同步的合同。
本单要求把标题算式写成**一个 SQL 函数**,触发器和回填迁移都调它,不得各写一份。
3. **`open` 路径不得改 title**(BUG-699)。
4. 不得再引入墙钟 HH:MM 去重后缀(BUG-929 防复发)。
5. 不得动会话删除、收藏、重命名;**用户手动重命名过的标题不得被覆盖**(见 T2 的判据)。
6. 删改任何前端符号或可见文案之前,按 `frontend/AGENTS.md` 跑 `git grep -n "<符号>" -- tests/ frontend/`,
不得只扫 `frontend/`(BUG-992 的教训,已列在 §6)。
7. 改任何既有断言写「原值 / 新值 / 原因」三栏(AGENTS.md §7.3);比测试规模比**用例名列表 diff**,
不比 `# tests` 汇总数(BUG-995)。
---
## 5. 任务分解
### T1|标题算式落成一个 SQL 函数(BUG-1001)
新增迁移,建 `public.rectification_session_title(p_accepted time, p_confirmed time, p_range jsonb) returns text`,
`immutable`,`set search_path=''`:
- `p_confirmed` 非空 → `'生时校正 · ' || to_char(p_confirmed, 'HH24:MI')`
- 否则 `p_accepted` 非空 → 同上,用 `p_accepted`
- 否则 `p_range` 的 `start_time` / `end_time` 都合法(复用现成的
`public.agentic_rectification_is_clock`)→ `'生时校正 · ' || start || '–' || end`
(**破折号用 U+2013 EN DASH,与交付卡口径一致,不要用连字符**)
- 否则 → `'生时校正'`
- **验收标准:** 四个分支各一条真实 Postgres 断言;`immutable` 且不读表。
### T2|结果变化时回写标题(BUG-1001)
仿照现成的 `public.touch_chat_session_from_rectification_case()`
(`frontend/supabase/migrations/20260915010000_rectification_touch_chat_session.sql`,
同一 security 姿态、同一 `schema_owner` 守卫、同一 `drop trigger if exists` + `create trigger` 写法),
新增 `after insert or update of accepted_time, confirmed_time, candidate_range on public.agentic_rectification_cases` 的触发器:
```
update public.chat_sessions
set title = public.rectification_session_title(new.accepted_time, new.confirmed_time, new.candidate_range)
where id = new.session_id
and user_id = new.user_id
and session_type = 'birth_time_rectification'
and title <> public.rectification_session_title(...) -- 无变化不写
and (title = '生时校正' or title ~ <自动派生标题的正则>) -- 用户改过名的不覆盖
```
- **不得**出现 `updated_at = ...`(红线 1)。
- 「用户改过名的不覆盖」的判据:标题匹配自动派生形态才覆盖。自动派生形态包含
**旧格式**(`M月D日 · 生时校正[ HH:MM]`)、**BUG-929 格式**(`生时校正 · M月D日`)、
**本单格式**(`生时校正[ · …]`)三种。这个正则与 `frontend/src/lib/session-title.ts` 的
`DATED_ENTRY_TITLE` 是同一件事的两处表达——**T4 要求两边对断**。
- **验收标准:**
- 真实 Postgres:采用一个分钟 → 标题变 `生时校正 · HH:MM`,同一行 `updated_at` **逐字节不变**;
区间收窄 → 标题跟着变;用户手动改成「我的校正」后再采用 → 标题**不被覆盖**。
- `last_activity_at` 触发的既有 touch 触发器仍正常(两个触发器互不干扰,各测一条)。
### T3|新建时不带日期,前端口径跟上(BUG-1001)
- `frontend/src/lib/agent-reply.ts`:`resolveSessionTitle` 对 `entrypoint === "birth_time_rectification"`
与 `isRectificationHandoffQuestion` 分支改为返回 `"生时校正"`(不带日期,结果由 T2 的触发器补)。
`datedSessionTitle` 若只剩「今日节奏」一个调用者,保留但改名或加注释说明它现在只服务今日节奏(D3)。
- `frontend/src/lib/session-title.ts`:`DATED_ENTRY_TITLE` 要继续认得住**三种**自动派生形态
(旧格式、BUG-929 格式、本单格式),否则 `shouldGenerateSessionTitle` / `isAutoDerivedSessionTitle`
会把新标题当成用户自定义标题,进而让模型去覆盖它。
- **验收标准:**
- `agent-reply.test.ts` 断言校正入口返回 `生时校正`(原值 `生时校正 · M月D日`,写三栏说明)。
- 新增断言:`isAutoDerivedSessionTitle` 对 `生时校正`、`生时校正 · 05:07`、`生时校正 · 05:00–05:15`、
`生时校正 · 9月17日`、`9月17日 · 生时校正`、`9月17日 · 生时校正 09:35` **全部为 true**;
对 `我的校正` 为 false。
- `shouldGenerateSessionTitle` 对校正会话仍恒 false(不变)。
### T4|跨层对断(BUG-1001,防复发的主断言)
- 新增一条测试,把 T1 的 SQL 函数与前端的 `DATED_ENTRY_TITLE` 放在同一张表里对断:
同一组输入(六种自动派生形态 + 一个用户自定义标题),SQL 侧「会不会覆盖」与 TS 侧
「是不是自动派生」必须给出相同答案。
- 理由写进测试注释:BUG-987 与 BUG-992 都是「两层规则各写一份、没人对断」。
- **验收标准:** 故意把 SQL 正则改一个字符,这条测试必须转红(执行方本地自测一次,撤回,不提交)。
### T5|存量批量重算(BUG-1001)
新增向前业务迁移:
```sql
update public.chat_sessions s
set title = public.rectification_session_title(c.accepted_time, c.confirmed_time, c.candidate_range)
from public.agentic_rectification_cases c
where c.session_id = s.id
and s.session_type = 'birth_time_rectification'
and s.title <> public.rectification_session_title(c.accepted_time, c.confirmed_time, c.candidate_range)
and (s.title 匹配三种自动派生形态之一);
```
- 幂等(第二次跑匹配 0 行)。**不得** `set updated_at`。
- 没有对应 case 的校正会话(理论上不存在,`session_id` 是 unique FK)跳过,不报错。
- **验收标准:**
- 真实 Postgres:造六条不同状态的会话(已确认 / 已采用 / 只有区间 / 旧格式带钟点 / 旧格式不带钟点 / 用户改过名),
跑迁移后前五条按规则改写、第六条不动;**全部六行的 `updated_at`、`pinned`、`messages` 逐字节不变**;
重跑迁移 0 行受影响。
- 迁移里的 `update` 语句由测试**从文件切出来执行**,不得手抄(BUG-995 那轮定的做法)。
### T6|记录(BUG-1001)
- `docs/BUG_HISTORY.md` 新增 BUG-1001,**复发自 BUG-929**(标题口径两次返工)。
防复发写两条:标题算式只能有一处实现,运行时与回填共用;侧栏同一行的两列不得承载同一个事实。
- `CHANGELOG.md`:一句用户能懂的——历史校正会话的名字改成显示算出来的出生时间或当前范围。
- `frontend/DESIGN.md` 的 **Navigation item** 一节:标题口径同提交更新
(现在写的是「New dated titles are `生时校正 · M月D日` / `今日节奏 · M月D日` (category first)」)。
- `docs/tasks/README.md` 状态板加行。
---
## 6. 会被牵动的文件(我已 grep 过,照这份查)
真正断言标题格式的只有这 7 处(`git grep -ln "生时校正 · \|月.日 · 生时校正\|datedSessionTitle\|DATED_ENTRY_TITLE\|datedRectificationTitle\|RECTIFICATION_DATED_TITLE\|repairedRectificationTitle" -- tests/ frontend/`):
| 文件 | 说明 |
| --- | --- |
| `frontend/src/lib/agent-reply.ts` | `datedSessionTitle` / `resolveSessionTitle`,T3 改这里 |
| `frontend/src/lib/session-title.ts` | `DATED_ENTRY_TITLE` 正则,T3 扩到三种形态 |
| `frontend/src/lib/rectification-session-title-repair.ts` | BUG-699/704 的旧修补助手,判断是否仍需要;若被 T5 取代就删并说明 |
| `frontend/scripts/repair-rectification-session-titles.mjs` | 同上,旧脚本 |
| `frontend/supabase/migrations/20260916020000_rectification_session_title_repair.sql` | **不得修改**(历史迁移),只作为写法先例 |
| `frontend/tests/agent-reply.test.ts` | 断言要改 |
| `frontend/tests/rectification-session-title-repair.test.ts`、`-migration.test.ts`、`frontend/tests/session-list-filter.test.ts` | 断言要跟着改 |
**Python 侧确认不需要改**:`tests/test_daily_and_rectification_entrypoints.py` 只断言裸字符串
`"生时校正" in source`,任何新格式都满足;`tests/test_api_server_security.py` 的命中与标题无关。
(这一条是我替你查过的结论,但开工时仍按红线 6 自己再跑一次 grep。)
---
## 7. 交付前必须全跑
- `cd frontend && ./node_modules/.bin/tsc --noEmit` → 0 错
- `npm run lint` → 0 error,warning 不得超过 **119**
- `npm test` → 全量;用**用例名列表 diff**与基线比对,只允许新增
- `npm run test:db` → **本单必跑**,T2 / T5 的证据全在这里;无 Docker 时按 §8 让步
- `npm run build` → `/` 仍 `○ Static`,首屏 gzip ±2%
- `python3 -m pytest tests/test_daily_and_rectification_entrypoints.py -q` → 确认未被波及
---
## 8. 让步顺序
1. **无 Docker** → `test:db` 跑不了:T1/T2/T5 的实现与真实 Postgres 测试照写照提交,
`BLOCKED.md` 记明,最终以门禁 run 转绿为准;**BUG-1001 在门禁那几条 DB 测试转绿之前不得标 `resolved`**。
2. **T5 存量回填与 T1–T4 拆轮** → 允许,但顺序必须是**先 T1–T4 后 T5**:
先让新结果能写对,再回填历史。反过来回填完又被旧触发器逻辑覆盖。
3. **T4 跨层对断做不出来** → 不得让步。这是本单唯一真正的防复发措施。
4. D3(今日节奏保留日期)若产品验收时否决 → 另开一轮,本单不扩范围。
---
## 9. 开工前置命令
```bash
cd /workspace/Jyotisha
git status -sb | head -1
git fetch origin --prune
git worktree add -b codex/rectification-session-title-result-20260922 \
.worktrees/rectification-session-title-result-20260922 origin/staging
cd .worktrees/rectification-session-title-result-20260922/frontend
ln -s /workspace/Jyotisha/frontend/node_modules node_modules # 或 npm ci
# 基线用例名单(比名单,不比总数)
npm test 2>&1 | grep -E "^(not )?ok [0-9]+ - " | sed -E 's/^(not )?ok [0-9]+ - //' | sort > /tmp/names-base.txt
# 自己再跑一次范围确认(红线 6)
cd .. && git grep -n "datedSessionTitle\|DATED_ENTRY_TITLE\|生时校正 · " -- tests/ frontend/
```
开工必读:`docs/BUG_HISTORY.md` 的 **BUG-929**(本单推翻它)、BUG-699、BUG-704、BUG-988、BUG-992、BUG-995;
`frontend/AGENTS.md`;`frontend/DESIGN.md` 的 Navigation item 一节;
`frontend/supabase/migrations/20260915010000_rectification_touch_chat_session.sql`(触发器写法先例)。
注意:在 `frontend/` 里跑 `npx tsx --test` 会生成未跟踪的 `frontend/frontend/node_modules`,交付前清掉。
---
## 10. 部署后真人走查
1. 侧栏里的校正会话,名字变成 `生时校正 · 05:07`(有结论的)或 `生时校正 · 05:00–05:15`(只有范围的),
**不再出现两条同名**。
2. 那几条带 `09:35` / `02:14` 这类钟点后缀、且与下面时间对不上的旧标题,全部消失。
3. **这些会话在列表里的位置和下面的时间都没有变**——如果它们集体跳到顶端、全显示今天,
说明回填动了 `updated_at`,立刻回报(红线 1)。
4. 「今日节奏」仍然是 `今日节奏 · M月D日`(D3 的有意例外)。
5. 自己手动重命名过的会话,名字没有被改回去。