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

16 KiB
Raw Blame History

TASK 校正会话标题改为写结果,不再写日期(BUG-1001)

  • 日期:2026-09-22
  • 基线 commit3fed71aborigin/staging head;开工 git fetch 后以实际 head 为准。已部署 SHA 当时是 1bc6a954,门禁在 6d06fca0 红过,与本单无关)
  • 分支:codex/rectification-session-title-result-20260922
  • BUG 编号起点:BUG-1001docs/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 datedSessionTitleexport 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 把副标题定成「最后活动时间」之后, 标题继续承载日期,就同时犯了两个错:与副标题重复,且不承载这次会话的内容。

普通咨询会话的标题是模型对首问的总结(内容摘要),校正会话没有这条路 (shouldGenerateSessionTitlebirth_time_rectification 直接 return false), 于是退化成了日期。而校正会话真正有区分度、用户真正想在列表里找的,是这次校正算出了什么


3. 决策记录

产品负责人 2026-09-22 就地拍板:

  • D1(推翻 BUG-929 的「新标题 生时校正 · M月D日」):校正会话标题改为承载校正结果

    • confirmed_timeaccepted_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 路径不得改 titleBUG-699)。
  4. 不得再引入墙钟 HH:MM 去重后缀(BUG-929 防复发)。
  5. 不得动会话删除、收藏、重命名;用户手动重命名过的标题不得被覆盖(见 T2 的判据)。
  6. 删改任何前端符号或可见文案之前,按 frontend/AGENTS.mdgit 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 immutableset search_path=''

  • p_confirmed 非空 → '生时校正 · ' || to_char(p_confirmed, 'HH24:MI')

  • 否则 p_accepted 非空 → 同上,用 p_accepted

  • 否则 p_rangestart_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.tsDATED_ENTRY_TITLE 是同一件事的两处表达——T4 要求两边对断
  • 验收标准:
    • 真实 Postgres:采用一个分钟 → 标题变 生时校正 · HH:MM,同一行 updated_at 逐字节不变; 区间收窄 → 标题跟着变;用户手动改成「我的校正」后再采用 → 标题不被覆盖
    • last_activity_at 触发的既有 touch 触发器仍正常(两个触发器互不干扰,各测一条)。

T3|新建时不带日期,前端口径跟上(BUG-1001)

  • frontend/src/lib/agent-reply.tsresolveSessionTitleentrypoint === "birth_time_rectification"isRectificationHandoffQuestion 分支改为返回 "生时校正"(不带日期,结果由 T2 的触发器补)。 datedSessionTitle 若只剩「今日节奏」一个调用者,保留但改名或加注释说明它现在只服务今日节奏(D3)。
  • frontend/src/lib/session-title.tsDATED_ENTRY_TITLE 要继续认得住三种自动派生形态 (旧格式、BUG-929 格式、本单格式),否则 shouldGenerateSessionTitle / isAutoDerivedSessionTitle 会把新标题当成用户自定义标题,进而让模型去覆盖它。
  • 验收标准:
    • agent-reply.test.ts 断言校正入口返回 生时校正(原值 生时校正 · M月D日,写三栏说明)。
    • 新增断言:isAutoDerivedSessionTitle生时校正生时校正 · 05:07生时校正 · 05:0005: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

新增向前业务迁移:

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_atpinnedmessages 逐字节不变 重跑迁移 0 行受影响。
    • 迁移里的 update 语句由测试从文件切出来执行,不得手抄(BUG-995 那轮定的做法)。

T6|记录(BUG-1001

  • docs/BUG_HISTORY.md 新增 BUG-1001复发自 BUG-929(标题口径两次返工)。 防复发写两条:标题算式只能有一处实现,运行时与回填共用;侧栏同一行的两列不得承载同一个事实。
  • CHANGELOG.md:一句用户能懂的——历史校正会话的名字改成显示算出来的出生时间或当前范围。
  • frontend/DESIGN.mdNavigation 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 / resolveSessionTitleT3 改这里
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.tsfrontend/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 errorwarning 不得超过 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. 无 Dockertest:db 跑不了:T1/T2/T5 的实现与真实 Postgres 测试照写照提交, BLOCKED.md 记明,最终以门禁 run 转绿为准;BUG-1001 在门禁那几条 DB 测试转绿之前不得标 resolved
  2. T5 存量回填与 T1T4 拆轮 → 允许,但顺序必须是先 T1T4 后 T5: 先让新结果能写对,再回填历史。反过来回填完又被旧触发器逻辑覆盖。
  3. T4 跨层对断做不出来 → 不得让步。这是本单唯一真正的防复发措施。
  4. D3(今日节奏保留日期)若产品验收时否决 → 另开一轮,本单不扩范围。

9. 开工前置命令

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.mdBUG-929(本单推翻它)、BUG-699、BUG-704、BUG-988、BUG-992、BUG-995 frontend/AGENTS.mdfrontend/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:0005:15(只有范围的), 不再出现两条同名
  2. 那几条带 09:35 / 02:14 这类钟点后缀、且与下面时间对不上的旧标题,全部消失。
  3. 这些会话在列表里的位置和下面的时间都没有变——如果它们集体跳到顶端、全显示今天, 说明回填动了 updated_at,立刻回报(红线 1)。
  4. 「今日节奏」仍然是 今日节奏 · M月D日D3 的有意例外)。
  5. 自己手动重命名过的会话,名字没有被改回去。