docs(tasks): 会话列表重建验收结论与修复单(BUG-990)
e71e4f92三条实现均通过独立复跑;门禁 run 2832 红在兼容层 not(col,"is",null) 未实现,归档视图自a1956deb起 500。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0199rbQDTsUbCVw84wc8BTFe
This commit is contained in:
co-authored by
Claude Opus 5
parent
e71e4f9200
commit
70d595e361
@@ -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 起 | 待领取 | — |
|
||||
|
||||
### 个人报告
|
||||
|
||||
|
||||
@@ -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.<anonymous> (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」的已知代价,
|
||||
若产品不接受再另立单。
|
||||
Reference in New Issue
Block a user