From 70d595e361f2b3b65fe9b28988d08332d9921628 Mon Sep 17 00:00:00 2001 From: Jesse_Chen Date: Mon, 21 Sep 2026 17:33:03 +0800 Subject: [PATCH] =?UTF-8?q?docs(tasks):=20=E4=BC=9A=E8=AF=9D=E5=88=97?= =?UTF-8?q?=E8=A1=A8=E9=87=8D=E5=BB=BA=E9=AA=8C=E6=94=B6=E7=BB=93=E8=AE=BA?= =?UTF-8?q?=E4=B8=8E=E4=BF=AE=E5=A4=8D=E5=8D=95=EF=BC=88BUG-990=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit e71e4f92 三条实现均通过独立复跑;门禁 run 2832 红在兼容层 not(col,"is",null) 未实现,归档视图自 a1956deb 起 500。 Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_0199rbQDTsUbCVw84wc8BTFe --- docs/tasks/README.md | 3 +- .../TASK-session-list-rebuild-fix-20260921.md | 195 ++++++++++++++++++ 2 files changed, 197 insertions(+), 1 deletion(-) create mode 100644 docs/tasks/TASK-session-list-rebuild-fix-20260921.md diff --git a/docs/tasks/README.md b/docs/tasks/README.md index 0483a594..d8e36d49 100644 --- a/docs/tasks/README.md +++ b/docs/tasks/README.md @@ -152,7 +152,8 @@ | `TASK-consultation-answer-start-anchor-20260917.md` | `PROGRESS-consultation-answer-start-anchor-20260917.md` | 主会话回答落在结尾:`useConversationScrollAnchor` 是贴底跟随,流式期间视口钉在最后一个字,回答开头滚出视口;改为发送后问题钉顶、回答向下长、长出视口显示「跳到最新」、末尾动态留白;产品追加拍板:校正面同一语义(推翻 BUG-041/048 贴底),本轮开头 = 用户行或新助手行。BUG 段 930 起 | 已验收(经修复单) | `worktree/green-harbor-5be3` | | `TASK-consultation-answer-start-anchor-fix-20260917.md` | `PROGRESS-consultation-answer-start-anchor-fix-20260917.md` | 验收修复单:F1 头就是留白行时留白按整视口算(BUG-931);F2 留白只在钉住期间存在(BUG-932);前置:先修 e4e73f56 的两处 TS 错否则门禁不过 | 已验收 | `cc1a8980`(Claude 验收:tsc 0 / lint 0 error / npm test 3457 条 39 红与 11c0028d 逐条一致、新增 2 条绿 / `next build --webpack` 通过、`/` Static、首屏 gzip 591,242(较 09-16 基线 582,800 +1.45%,含会话列表单)/ Chrome 真实布局 S1–S6 全部通过,S6 新助手行距顶 16px 且增高不动,S5 不再写留白);真机六条欠 | | — | `PROGRESS-starter-greeting-20260917.md` | 首页开场语改成 claude.ai 式单行问候:`starter-greeting.ts` 的「称呼 + 追问句」五时段十五条收成三个池子(时段 / 星期 / 回访,按 `variantSelection` 确定性取一条),副标题行与 `.starter-salutation` 下线,h1 降到 `clamp(24px, 2.8vw, 30px)`;追问移到输入框占位符「想聊什么都可以」。产品直接拍板,非 Bug,不占 BUG 号 | 待验收 | `codex/starter-greeting-20260917` | -| `TASK-session-list-rebuild-20260921.md` | `PROGRESS-session-list-rebuild-20260921.md` | **P0 会话列表重建(`e4e73f56` 的返工)**:服务端 `GET /api/sessions` 用 `messages <> '[]'` 排除空咨询,但校正会话的 `messages` 永远是 `[]`(建行写死空数组、创建合同只收空数组、PATCH 忽略、`touch_chat_session_from_rectification_case` 只 bump `updated_at`)→ **全部生时校正会话从侧栏消失**,而客户端 `isListedSidebarSession` 明确放行校正会话,两层规则相反且无跨层对断(BUG-987);同一提交把副标题换成 `created_at`,排序/分组/游标仍是 `updated_at`,列表可见时间不单调(9/18→9/17→9/7→9/16)(BUG-988);BUG-928 让步下来的「复用空会话」不 bump 时间,点新建变成跳回 9/17 的旧草稿(BUG-989)。**产品 09-21 拍板**:D1 副标题改最后活动时间(推翻 BUG-929 该条,其余保留)、D2 收掉 `BLOCKED.md:120` 的让步,第一问前不落库、删复用与 `draft`。T1 不得让步,T3 可拆轮。BUG 段 987 起 | 待验收 | `codex/session-list-rebuild-20260921` | +| `TASK-session-list-rebuild-20260921.md` | `PROGRESS-session-list-rebuild-20260921.md` | **P0 会话列表重建(`e4e73f56` 的返工)**:服务端 `GET /api/sessions` 用 `messages <> '[]'` 排除空咨询,但校正会话的 `messages` 永远是 `[]`(建行写死空数组、创建合同只收空数组、PATCH 忽略、`touch_chat_session_from_rectification_case` 只 bump `updated_at`)→ **全部生时校正会话从侧栏消失**,而客户端 `isListedSidebarSession` 明确放行校正会话,两层规则相反且无跨层对断(BUG-987);同一提交把副标题换成 `created_at`,排序/分组/游标仍是 `updated_at`,列表可见时间不单调(9/18→9/17→9/7→9/16)(BUG-988);BUG-928 让步下来的「复用空会话」不 bump 时间,点新建变成跳回 9/17 的旧草稿(BUG-989)。**产品 09-21 拍板**:D1 副标题改最后活动时间(推翻 BUG-929 该条,其余保留)、D2 收掉 `BLOCKED.md:120` 的让步,第一问前不落库、删复用与 `draft`。T1 不得让步,T3 可拆轮。BUG 段 987 起 | **验收未通过(1 条 P1)** | `e71e4f92`(Claude 验收:T1/T2/T3 实现均正确;tsc 0 / lint 0 error 119 warning 与基线同 / `npm test` 本机 3640 条 34 红与 `f8d65e48` 逐条一致、净增 5 绿 / `next build --webpack` 通过、`/` Static、首屏 gzip 643,115 较基线 −0.027% / page.tsx 1826→1791。**P1:门禁 run 2832 红、staging 未部署**——新加的真实 Postgres 测试在有 Docker 的门禁机上跑出 `unsupported not filter`,兼容层 `not()` 不支持 `is` 算子,归档分支炸。修复单 `TASK-session-list-rebuild-fix-20260921.md`) | +| `TASK-session-list-rebuild-fix-20260921.md` | — | **验收修复单(挡着三条修复上线)**:本地 PG 兼容层 `LocalPostgresQueryBuilder.not()` 只认 `eq` / `cs` 两个算子,`not("archived_at","is",null)` 直接 throw → `GET /api/sessions?archived=1` 落外层 catch 回 500,**侧栏「归档记录」自 `a1956deb`(09-06,BUG-553)起一直打不开**,两周无人发现是因为只有源码正则合同看着它;本轮新加的真实 Postgres 测试第一次真跑到这条路径,把它照出来,于是门禁 run 2832 红、`e71e4f92` 没部署。**决策**:在兼容层补 `is not null / is not true / is not false`,不得反过来改调用方绕开,不得删改测试断言让门禁变绿。与 BUG-926 的 `order()` 缺口同类。BUG 段 990 起 | 待领取 | — | ### 个人报告 diff --git a/docs/tasks/TASK-session-list-rebuild-fix-20260921.md b/docs/tasks/TASK-session-list-rebuild-fix-20260921.md new file mode 100644 index 00000000..21abb938 --- /dev/null +++ b/docs/tasks/TASK-session-list-rebuild-fix-20260921.md @@ -0,0 +1,195 @@ +# TASK 会话列表重建 · 验收修复单:兼容层不支持 `not(col,"is",null)`,归档视图 500 且门禁红 + +- 日期:2026-09-21 +- 基线 commit:`e71e4f92`(`origin/staging` head;**该提交未部署**,staging 仍停在 `0c3c9d6b`) +- 分支:`codex/session-list-rebuild-fix-20260921` +- BUG 编号起点:**BUG-990**(开工核对 `docs/BUG_HISTORY.md` 实际最大号,当前最大 `BUG-989`) +- 前序:`TASK-session-list-rebuild-20260921.md` / `PROGRESS-session-list-rebuild-20260921.md` +- 关联记录:BUG-553、BUG-926、BUG-987 + +--- + +## 1. 事故实证 + +Gitea `backend-quality-gate` run **2832**(`e71e4f92`,2026-09-21 17:06→17:23):`validate` job 第 6 步 +`Validate backend, package, frontend, and database contracts` **失败**,`publish` 被跳过,因此 **staging 至今没有部署这一版**——BUG-987/988/989 三条修复一条都没到用户面前。 + +门禁机有 Docker,测试总数 **3651**,**3650 通过 / 1 失败 / 0 skip**。唯一的红是本轮新加的那条真实 Postgres 测试: + +``` +not ok 1238 - session list query keeps empty rectification rows and drops empty consultations + error: 'unsupported not filter' + stack: |- + LocalPostgresQueryBuilder.not (frontend/src/lib/db/local-postgres-client-core.ts:391:34) + applyArchiveFilter (frontend/src/lib/session-list-filter.ts:18:27) + TestContext. (frontend/tests/database-session-list-visibility.test.ts:62:7) +``` + +`database-session-list-visibility.test.ts:62` 是归档分支那一半断言: + +```ts +const archived = await excludeEmptyConsultations( + applyArchiveFilter(local.from("chat_sessions").select("id")…, true), +); +``` + +`applyArchiveFilter(query, true)` 调 `query.not("archived_at", "is", null)`。本地 Postgres 兼容层的 +`LocalPostgresQueryBuilder.not()`(`local-postgres-client-core.ts`,符号定位 `not(column: string, operator: string, value: unknown)`)只认两个算子: + +```ts +if (operator === "eq") { …neq… ; return this; } +if (operator !== "cs") throw new Error("unsupported not filter"); +``` + +`"is"` 直接 throw。 + +### 1.1 这不只是测试问题:归档视图在本地 PG 上一直是 500 + +`applyArchiveFilter` 在 `GET /api/sessions` 的 `try` 块里同步执行,抛出后落到外层 `catch`, +返回 **500 `聊天记录暂时无法读取`**。而侧栏确实有归档入口(`app-sidebar.tsx` 的 `归档记录` 分组、 +`SidebarSessionControls.onToggleArchivedView`),切过去会调 `fetchSessions(…, { archived: true })` +→ `/api/sessions?archived=1` → 500。 + +`git log -S'archived_at", "is", null'` 显示这行由 **`a1956deb`(2026-09-06,BUG-553 会话列表分页)** 引入。 +也就是说**归档视图从 09-06 起就打不开**,两周没被发现,因为当时没有任何真实数据库测试走这条路径, +而源码正则合同只看得见"有没有写这行"。本轮新测试是第一次真跑它,于是把它照出来了。 + +托管 Supabase 路径不受影响(PostgREST 原生支持 `not.is.null`),但生产与 staging 都跑自托管本地 PG。 + +--- + +## 2. 根因 + +兼容层 `not()` 实现的是 PostgREST 算子的一个子集(只有 `eq` / `cs`),调用方却按完整 PostgREST 语义写。 +这是 BUG-926「`order()` 只保留最后一键」的同一类缺陷:**兼容层缺口不会在构造期报错,只在真跑到那条 SQL 时才炸, +而全仓只有源码正则合同在看这些调用点。** + +--- + +## 3. 决策记录 + +- **D1**:在兼容层补 `not(column, "is", null | true | false)`,映射成 `is not null` / `is not true` / `is not false`。 + 这是正解——`not.is.null` 是 PostgREST 标准写法,调用方没写错。**不得**反过来改 `applyArchiveFilter` 去绕开它, + 那只会把缺口留给下一个调用方。 +- **D2**:本轮不得删改 `database-session-list-visibility.test.ts` 的任何断言来让门禁变绿。它抓到的是真 Bug。 +- **D3**:归档视图 500 作为独立 Bug 记录(BUG-990),并写明复发自 BUG-926 的同一类兼容层缺口。 + +--- + +## 4. 硬红线 + +1. 不得通过弱化或删除测试断言让门禁通过。 +2. 兼容层任何新增算子必须有**真实 Postgres** 覆盖,不得只加源码正则。 +3. 不得顺手改 `excludeEmptyConsultations` / 副标题 / 新建落库这三条已验收通过的逻辑(见 §6 验收结论)。 +4. 不得顺手升级依赖、不得顺手修不在本单里的 warning。 +5. `frontend/src/app/(app)/page.tsx` 的 `useState` / `useRef` 数不得增长(AGENTS.md §6);本单预期不碰这个文件。 + +--- + +## 5. 任务分解 + +### F1|兼容层支持 `not(col, "is", value)`(BUG-990) + +- `LocalPostgresQueryBuilder.not()` 增加 `operator === "is"` 分支,新增一种 filter kind(例如 `isNot`), + 在 `filterClause()` 里编译成 `${column} is not null` / `is not true` / `is not false`; + 其余取值仍 throw(与既有 `is` 分支的 `unsupported is filter` 保持一致的失败语义)。 +- 不要把它塞进现有 `neq` 分支:`archived_at <> null` 恒为 NULL,会静默返回 0 行——比 throw 更糟。 +- **验收标准:** + - `frontend/tests/database-session-list-visibility.test.ts` 在有 Docker 的机器上**整条通过**,归档断言返回且只返回那条已归档的校正会话。 + - 新增单元覆盖:`not(col,"is",null)` 编译出的 SQL 含 `is not null` 且不含 `<>`;`not(col,"is",undefined)` 仍 throw。 + - `local-postgres-or.test.ts` / `chat-session-authority.test.ts` 不变仍绿。 + +### F2|归档视图回归证据(BUG-990) + +- 在同一个真实 Postgres 测试里补一条:归档视图不返回未归档行、也不返回空咨询(已归档的空咨询仍不列出)。 + 现有测试已覆盖前者,把"已归档的空咨询"这一行加进 fixture 断言即可。 +- **验收标准:** 归档与正常两个视图的返回集合互斥且都符合 `cloudListIncludesSession` 的六种组合。 + +### F3|记录(BUG-990) + +- `docs/BUG_HISTORY.md` 新增 BUG-990:现象写「侧栏切到归档记录后 500」,根因写兼容层 `not` 算子子集, + **复发自 BUG-926**(同一兼容层的不同缺口),防复发写「兼容层新增算子必须有真实 Postgres 覆盖」。 +- `CHANGELOG.md` 补一句用户可感知的:归档记录可以打开了。 +- `docs/tasks/README.md` 状态板更新本单与前序单。 +- 前序单 BUG-987/988/989 的「修复版本」在本单合入并部署后回填实际 SHA。 + +--- + +## 6. 前序单(`e71e4f92`)的验收结论——除本单外全部通过,不要重做 + +我在 `.worktrees/verify-20260921` 用 `f8d65e48` 作基线独立复跑: + +| 项 | 基线 `f8d65e48` | `e71e4f92` | 结论 | +| --- | --- | --- | --- | +| `tsc --noEmit` | — | 0 错 | 通过 | +| `npm run lint` | 0 error / 119 warning | 0 error / 119 warning | 通过,warning 未增 | +| `npm test`(本机无 Docker) | 3634 条 / 3580 过 / 34 红 / 20 skip | 3640 条 / 3585 过 / 34 红 / 21 skip | 失败清单 `diff` **逐条一致**;净增 5 绿 + 1 skip(即那条 Docker 测试) | +| `next build --webpack` | 通过 | 通过 | `/` 仍 `○ Static` | +| 首屏 gzip(html + 29 个首屏 js/css,level 9) | 643,288 | 643,115 | **−173 B / −0.027%** | +| `page.tsx` 行数 | 1826 | 1791 | 缩小 | + +逐条对任务书: + +- **T1 BUG-987 通过(实现正确,被 F1 挡住)**:过滤收窄成 + `or("session_type.neq.consultation,messages.neq.[]")`,抽到 `session-list-filter.ts` 与客户端同文件; + 错误注释已删。跨层对断测试覆盖六种组合。**归档分支的规则写对了,跑不起来是兼容层的锅。** +- **T2 BUG-988 通过**:副标题改 `updatedAt`,`SESSION_LIST_COLUMNS` 去掉 `created_at`, + `session-groups.test.ts` 新增的断言正是截图那条(9/7 创建、9/16 活动)——排在活动时间该在的位置、副标题显示活动时间。 +- **T3 BUG-989 通过**:`startNewChat` 不再 POST、不再写 `?c=`;`send()` 在扣点之后、乐观写入之前 + `persistSession(currentSession, "create")`,失败撤销本地会话且发生在 `setDraft("")` 之前(草稿不丢); + `cloudCreatedIds` 防重复 POST;启动落点与 `draft` / `draftRow` / `findReusableEmptyConsultation` 全部删除。 +- **记录**:BLOCKED.md 第 120 行那条按 §4 划掉而非删除,解除说明写明了 BUG-989/987。 + +--- + +## 7. 交付前必须全跑 + +- `cd frontend && ./node_modules/.bin/tsc --noEmit` → 0 错 +- `npm run lint` → 0 error,warning 不得超过 119 +- `npm test` → 全量;失败清单与 `e71e4f92` 逐条比对 +- `npm run test:db` → **本单必跑**,这是 F1 的唯一真证据;确实没有 Docker 时按 §8 让步 +- `npm run build` → `/` 仍 Static,首屏 gzip 与 `e71e4f92` 相差 ±2% 内 + +--- + +## 8. 让步顺序 + +1. **无 Docker** → `test:db` 跑不了:F1 的实现与测试照写照提交,在 `BLOCKED.md` 记明, + 并以「门禁 run 编号 + 该测试转绿」作为交付证据——本单允许把最终验证交给门禁机, + 但**不得在 `test:db` 未绿的情况下把 BUG-990 写成 `resolved`**。 +2. 其它一律不让步:F1 是唯一挡住三条已完成修复上线的东西。 + +--- + +## 9. 开工前置命令 + +```bash +cd /workspace/Jyotisha +git status -sb | head -1 +git fetch origin --prune +git worktree add -b codex/session-list-rebuild-fix-20260921 \ + .worktrees/session-list-rebuild-fix-20260921 origin/staging +cd .worktrees/session-list-rebuild-fix-20260921/frontend +ln -s /workspace/Jyotisha/frontend/node_modules node_modules # 或 npm ci +./node_modules/.bin/tsc --noEmit +npm test 2>&1 | tail -20 # 记录基线(本机 3640 / 34 红 / 21 skip) +``` + +开工必读:`docs/BUG_HISTORY.md` 的 BUG-926(兼容层 `order()` 缺口,同类)、BUG-553、BUG-987; +`docs/tasks/PROGRESS-session-list-rebuild-20260921.md`;AGENTS.md §2、§3、§5、§7。 + +纯前端 + 兼容层,不要求 `scripts/pre_work_check.py`。 + +--- + +## 10. 真实环境欠账(本单不负责,部署后由产品负责人走) + +`e71e4f92` + 本单部署到 staging 后需要人工确认,建议写进 `docs/testing/`: + +1. 侧栏能看到 09-08 以来的生时校正会话,且时间从上到下单调递减。 +2. 截图里那条「9月16日 · 今日节奏」现在显示 9月16日(而不是 9月7日)。 +3. 连点五次「新建对话」,侧栏不出现任何空「新对话」;发一句话后出现一行,时间是今天。 +4. 第一句话发送失败时,输入框里的字还在。 +5. 切到「归档记录」不再报错(F1 的用户侧证据)。 +6. 新建后地址栏仍是上一条会话的 `?c=`,刷新会回到上一条——这是「未落库不写 URL」的已知代价, + 若产品不接受再另立单。