From c6d421edbade44c6d703bec10f90dd53298e1150 Mon Sep 17 00:00:00 2001 From: Jesse_Chen Date: Tue, 22 Sep 2026 13:24:23 +0800 Subject: [PATCH] =?UTF-8?q?docs(tasks):=20=E6=A0=A1=E6=AD=A3=E4=BC=9A?= =?UTF-8?q?=E8=AF=9D=E6=A0=87=E9=A2=98=E6=94=B9=E5=86=99=E7=BB=93=E6=9E=9C?= =?UTF-8?q?=E8=80=8C=E9=9D=9E=E6=97=A5=E6=9C=9F=EF=BC=88BUG-1001=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit BUG-988 后标题日期与副标题重复,且 BUG-929 删 uniquify 后同日多条 标题相同。产品拍板标题承载 accepted/confirmed 分钟或 candidate_range, 存量批量重算。红线:不得 bump updated_at,标题算式只能有一处实现。 Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_0199rbQDTsUbCVw84wc8BTFe --- docs/tasks/README.md | 1 + ...ification-session-title-result-20260922.md | 269 ++++++++++++++++++ 2 files changed, 270 insertions(+) create mode 100644 docs/tasks/TASK-rectification-session-title-result-20260922.md diff --git a/docs/tasks/README.md b/docs/tasks/README.md index a84255e3..486a9d49 100644 --- a/docs/tasks/README.md +++ b/docs/tasks/README.md @@ -33,6 +33,7 @@ | 任务书 | 进度 | 主题 | 状态 | 落点 | | --- | --- | --- | --- | --- | +| `TASK-rectification-session-title-result-20260922.md` | — | **校正会话标题改写结果(BUG-1001)**:BUG-988 把副标题改成最后活动时间后,标题里的日期成了重复;而 BUG-929 删掉 `uniquifySessionTitle` 之后,`resolveSessionTitle` 对校正入口只返回 `生时校正 · M月D日`,**同一天多条标题完全相同**(真机截图:9/17 三条同名、9/16 两条同名、9/14 一天 5 条);旧标题的钟点后缀是创建时间、副标题是最后活动时间,同一行两个对不上的时间。**产品 09-22 拍板**:D1 标题改为承载结果——有 `accepted_time`/`confirmed_time` 写 `生时校正 · HH:MM`,否则写 `candidate_range` 的 `生时校正 · HH:MM–HH:MM`(推翻 BUG-929 的日期口径,但「不得再加墙钟去重后缀」保留);D2 存量批量重算(推翻 BUG-929「旧标题不批量改」,先例 `20260916020000_rectification_session_title_repair.sql`);D3 今日节奏保留日期(我判定的例外);D4 只在结果变化时改 title,`open` 路径不动(对 BUG-699 边界的有限扩展)。**红线**:任何写标题的路径都不得 bump `updated_at`(否则 13 条历史会话集体跳顶、毁掉 BUG-988);标题算式必须只有一处实现(一个 SQL 函数,触发器与回填共用)——BUG-987/992 都栽在「两层规则各写一份」。T4 跨层对断不得让步。已 grep 出真正受影响的只有 7 个文件,Python 侧只查裸字符串、无需改。BUG 段 1001 起 | 待领取 | — | | `TASK-rectification-convergence-20260830.md` | `PROGRESS-rectification-convergence-20260830.md` | 收敛重构 v2 | 已验收 | `3a4396a4`、`86ba17ee` | | `TASK-rectification-decision-authority-20260831.md` | `PROGRESS-rectification-decision-authority-20260831.md` | 决策权威归一与停止语义 | 已合入 | `9f011194` | | `TASK-rectification-billing-20260831.md` | — | run_failed 与开场问题槽为空 | 已合入 | `47b4b06b` | diff --git a/docs/tasks/TASK-rectification-session-title-result-20260922.md b/docs/tasks/TASK-rectification-session-title-result-20260922.md new file mode 100644 index 00000000..0c665dbf --- /dev/null +++ b/docs/tasks/TASK-rectification-session-title-result-20260922.md @@ -0,0 +1,269 @@ +# 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` + - 否则 → `生时校正 · `(用 `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. 自己手动重命名过的会话,名字没有被改回去。