docs(tasks): 侧栏统一进度记录、BUG-744~746、真机清单与 CHANGELOG

`PROGRESS-sidebar-unify-20260916.md` 记基线与改后的 tsc / lint / 测试总数 /
失败清单 diff / 五个路由标记 / gzip,逐条列改过的既有断言、触发的让步顺序
(T5 第 1 条)、次级页体积增加的原因,以及无 Chrome 无登录态的环境缺口。
`docs/testing/sidebar-unify-20260916.md` 是七组可照做的真机条目。
状态板本单一行改「待验收」,执行分支 `codex/sidebar-unify-20260916`。

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0193vBv6w5MV2cifdTUu9H5P
This commit is contained in:
Jesse_Chen
2026-09-16 11:23:43 +00:00
co-authored by Claude Fable 5.1
parent fb77c86585
commit 22edabbcd3
5 changed files with 255 additions and 1 deletions
+49
View File
@@ -11615,3 +11615,52 @@
- 复发自:无
- 修复版本:无
## BUG-744 | 侧栏写成两套标记,次级页会话行与页脚跟首页长得不一样
- 状态:resolved
- 首次发现:2026-09-16
- 最近更新:2026-09-16
- 影响面:`frontend/src/components/app-sidebar.tsx`、已删除的 `app-nav-rail.tsx``frontend/src/components/sidebar-session-row.tsx``frontend/src/app/globals.css`
- 用户现象:staging 真机上,`/` 的侧栏和 `/chart``/ephemeris``/reports` 的侧栏不是同一个东西——会话行的高度、内边距、当前项标记都对不上,页脚一个 56px 一个 44px。
- 触发条件:从 `/` 走到任一次级页,对比左侧同一列。
- 根因:次级页侧栏当初为了不把 `Home()` 的会话管理层上提,另写了第二个组件。它的会话行是 `<Link className="session-row nav-rail-row">` 直接套 `.session-title`**没有 `.session-main` 这一层**,因此丢掉了 `padding: var(--space-2)``min-height: 44px``.session-main[data-active="true"]::before` 的 2px 当前项标记;而 `.session-row``grid-template-columns: minmax(0, 1fr) 44px` 仍给一个从不渲染的菜单按钮留着 44px 空列。页脚另起 `.nav-rail-identity`(44px flex,带「N 点」),与 `/``.profile-trigger`56px grid、带 chevron)并列存在。产品要的是「不带写操作」,从来不是「长得不一样」;分叉是实现手段的副作用。
- 修复:`AppSidebar` 收编第二个组件——会话操作与账户菜单收进可选的 `controls`,不传就渲染只读模式:会话行仍是同一个 `SidebarSessionRow`、同一套 `.session-row > .session-main` 标记,只是 `.session-main``<Link>`、不渲染菜单按钮、并用 `data-readonly="true"` 去掉那一列 44px 空位;页脚是同一个 `.profile-trigger`,渲染成去 `/` 的链接,不带 chevron、不带菜单。`app-nav-rail.tsx``.nav-rail-*` 两段 CSS 删除。只读模式**只**少菜单按钮、chevron、账户菜单三样。
- 验证:`frontend/tests/sidebar-contract.test.ts` 新增「the same component renders read-only when `/` is not the one mounting it」,锁 `controls?` 可选、只读行带 `.session-main`、只读行不含 `session-menu-trigger``.session-row[data-readonly="true"]` 不留 44px 列、页脚是 `.profile-trigger` 链接;`grep -rn "AppNavRail\|nav-rail" frontend/src` = 0。改过的既有断言逐条写在 `docs/tasks/PROGRESS-sidebar-unify-20260916.md`
- 防复发:产品里只有一个侧栏组件。再要一个「只读侧栏」必须走 `controls` 缺省,不得新建第二个组件或第二套类名;合同测试锁住「只读模式只少三样」。
- 相关记录:BUG-711、BUG-438
- 复发自:无
- 修复版本:待发布
## BUG-745 | 首页进次级页整页刷新,次级页外壳逐页重挂,每跳一次重拉会话列表
- 状态:resolved
- 首次发现:2026-09-16
- 最近更新:2026-09-16
- 影响面:`frontend/src/components/app-sidebar.tsx``frontend/src/app/(secondary)/layout.tsx``frontend/src/hooks/use-sidebar-data.ts``frontend/src/lib/sidebar-data-cache.ts``frontend/src/hooks/use-session-management.ts``frontend/src/app/page.tsx`
- 用户现象:每进一次次级页,侧栏的会话列表都要重新读一遍;从次级页点「新建对话」回首页要等很久。
- 触发条件:`/``/chart``/chart``/ephemeris``/reports`
- 根因:三件事叠加。① 侧栏的 `leaveChat()``window.location.assign(path)`,首页出去是整页刷新,React 树与内存全部清零。② `app/` 下没有把四个次级路由包起来的 layout,三个页面各自在组件内部渲染 `SecondaryShell`,每份都挂一套 `SidebarProvider + 侧栏`,因此**即便是客户端跳转**外壳也整个重挂,数据 hook 的 effect 重新 `GET /api/sessions?limit=40` + `GET /api/account`。③ 这两个读没有任何缓存。
- 修复:① 三个页面项改 `<SidebarMenuLink href=…>` 客户端跳转,`persistLoginSessionReturn()` 保留在 `onClick` 里,`/login` 仍是硬跳转;顺带删掉从来没被调用过的死 prop `onOpenReports`(侧栏里解构成 `_onOpenReports`),它删掉后 `page.tsx``router` 再无消费者,`useConsultationRun` 那个同样解构成 `_router` 的死参数一并删。② 四个路由移进 `app/(secondary)/` 路由组,`layout.tsx` 承载 `SidebarProvider + AppSidebar(只读) + SidebarInset`,URL 与四个渲染标记均未变。③ 模块级按账户 id 键的 60 s 缓存,同步读、后台刷新;`/` 上新建 / 重命名 / 删除 / 归档 / 收藏成功后调 `invalidateSidebarCache()`401 清空。不落 localStorage。
- 验证:`frontend/tests/sidebar-data-cache.test.ts` 4 条(命中不重拉、过期重拉一次、写操作后拿到新标题、双账户不串、只存内存、401 清空);`frontend/tests/chart-page-view.test.tsx` 新增「the (secondary) layout mounts one read-only sidebar for all four routes」;`next build` 四个路由标记与基线逐个一致(`/chart` `○``/ephemeris` `○``/reports` `ƒ``/reports/[reportId]` `ƒ`),`/``○`。浏览器级证据(Network 面板没有 document 请求、跨页没有 `/api/sessions`)无 Chrome 无登录态做不了,写成 `docs/testing/sidebar-unify-20260916.md` 的真机清单。
- 防复发:次级页不得再在组件内部挂第二份 `SidebarProvider`;合同测试锁 layout 里 `<SidebarProvider>` 恰好一次、且不传 `controls`。侧栏源码里不得再出现 `window.location``/login` 的硬跳转在 `page.tsx``use-billing-panel.ts`,由 `chat-navigation-a11y-contract` 守)。
- 相关记录:BUG-744、BUG-746
- 复发自:无
- 修复版本:待发布
## BUG-746 | 侧栏折叠状态不跨页保留,每换一页又展开
- 状态:resolved
- 首次发现:2026-09-16
- 最近更新:2026-09-16
- 影响面:`frontend/src/components/ui/sidebar.tsx``frontend/src/lib/sidebar-state.ts`
- 用户现象:在 `/` 把侧栏收起来,进 `/chart` 又是展开的;反过来也一样。
- 触发条件:桌面或平板宽度下收起侧栏,然后换一页。
- 根因:`SidebarProvider``defaultOpen` 只活在内存里,`ready` 之后一律回到 `defaultSidebarOpen(viewport)` 的断点默认值,没有任何持久化。以前四个次级页各挂一份 provider,跳一次就重置一次。
- 修复:`readStoredSidebarOpen` / `writeStoredSidebarOpen` 落在 `localStorage``sidebar_state` 键;`commitOpen` 每次显式切换时写入,`ready` 的那个 effect 用 `readStoredSidebarOpen(viewport) ?? defaultSidebarOpen(viewport)` 取值。移动端抽屉不读不写。
- 验证:`frontend/tests/sidebar-state.test.ts` 新增两条——收起后重挂仍收起(含无存储 / 存了脏值 / 存储不可用三种降级)、移动端不读不写且 `null` 存储不抛。
- 防复发:不得改用需要服务端读取的 cookie:`/``/chart``/ephemeris` 都是 `○ Static`,服务端读 cookie 会让三条路由一起掉出静态渲染。
- 已知缺口(不得写成已解决):读取发生在挂载后的同一个 effect 里,也就是断点默认值原本就生效的那一帧。对一个把侧栏收起来的桌面用户,**整页加载**时首帧仍是展开、随后收起,这是让步顺序第 1 条,任务书允许。客户端跳转(`/` ↔ 次级页)不受影响,因为 provider 不重挂或重挂时 `localStorage` 已可读。
- 相关记录:BUG-745
- 复发自:无
- 修复版本:待发布
@@ -0,0 +1,118 @@
# PROGRESS · 侧栏统一成一个组件 + 次级页共享外壳 + 跳转不再整页刷新(2026-09-16
任务书:`docs/tasks/TASK-sidebar-unify-20260916.md`
分支:`codex/sidebar-unify-20260916`
工作树:`/workspace/Jyotisha/.worktrees/sidebar-unify-20260916`
开工基线:`git rev-parse origin/staging` = `062af26d`(任务书里写的 `cfb41daf` 之后又落了一条纯文档提交,本单实际从 `062af26d` 起)。交付前 `origin/staging` 又前进到 `302ff085`(纯文档),本分支已 rebase 到其上;`docs/tasks/README.md` 有一处状态板冲突,已按「本单那行取我方(待验收),另一单那行取上游」解开,未覆盖任何他人内容。
BUG 编号:开工核对 `docs/BUG_HISTORY.md` 最大号为 **BUG-743**,本单用 **BUG-744 / 745 / 746**,与任务书一致,无顺延。
## 一、基线 vs 改后
| 项 | 基线 `062af26d` | 改后 | 结论 |
| --- | --- | --- | --- |
| `./node_modules/.bin/tsc --noEmit` | 0 错 | 0 错 | ✅ |
| `npm run lint` | 0 error / **118** warning | 0 error / **115** warning | ✅ 少 3 条(删掉的 `app-nav-rail.tsx``use-nav-rail.ts` 带走的,没有顺手修任何 warning) |
| `npm test` | tests **3391** · pass 3329 · fail **47** · skipped 15 | tests **3399** · pass 3337 · fail **47** · skipped 15 | ✅ 总数 +8,失败清单 `diff` **逐条一致**(见 §二) |
| `/` 渲染标记 | `○` Static | `○` Static | ✅ |
| `/chart` | `○` | `○` | ✅ |
| `/ephemeris` | `○` | `○` | ✅ |
| `/reports` | `ƒ` | `ƒ` | ✅ |
| `/reports/[reportId]` | `ƒ` | `ƒ` | ✅ |
| `.next/static` css chunk gzip 合计 | **40,266** B | **40,223** B | ✅ 0.11% |
| `/` HTML 引用的 js chunk gzip 合计 | **603,843** B23 个) | **609,504** B25 个) | ✅ +0.94% |
| `/chart` 同口径 | 282,125 B17 个) | 319,352 B20 个) | ⚠️ +13.2%,原因见 §六 |
| `/ephemeris` 同口径 | 318,825 B19 个) | 346,838 B21 个) | ⚠️ +8.8%,同上 |
gzip 口径沿用 `PROGRESS-cend-ui-r1-20260916.md``.next/static` 下全部 css chunk 的 gzip 字节合计;js 一列是预渲染 HTML 里 `/_next/static/chunks/*.js` 去重后逐个 gzip 求和。同机、同脚本、改前改后各测一次。
## 二、失败清单 diff
```
diff baseline-fails.txt after-fails.txt → 空(IDENTICAL
```
47 条全部是无 Docker 的既有环境缺口(真实 Postgres 迁移、部署 compose、staging 同步、RLS 合同等),与基线逐条相同。中途出现过 16 条新红,全部由本单造成、全部已处理,处理方式见 §四。
## 三、任务分解逐条
| 条目 | 结论 | 说明 |
| --- | --- | --- |
| **T1** `AppSidebar` 收编 `AppNavRail` | ✅ 完成 | `controls?: AppSidebarControls` 缺省即只读;`app-nav-rail.tsx``use-nav-rail.ts``.nav-rail-*` 两段 CSS 全部删除;`grep -rn "AppNavRail\|nav-rail" frontend/src` = **0** |
| **T2** 路由组 layout 承载外壳 | ✅ 完成 | 新建 `src/app/(secondary)/layout.tsx``chart` / `ephemeris` / `reports` 三个目录 `git mv` 进去;`SecondaryShell``SecondaryHeader`(只剩 46px 顶栏),14 处调用改名;四个 URL 与四个渲染标记均未变 |
| **T3** 列表与账户缓存 + 首页写穿 | ✅ 完成(取「失效」不取「写穿」,见下) | `src/lib/sidebar-data-cache.ts` 模块级 `Map<accountId, entry>` + 60 s TTL`use-sidebar-data.ts` 同步读、后台刷新;`use-session-management.ts` 五个写操作成功后 `invalidateSidebarCache()`401 清空 |
| **T4** 首页进次级页改客户端跳转 | ✅ 完成 | `leaveChat()` 只剩 `persistLoginSessionReturn()` + 关抽屉;三个页面项由 `NAV_PAGES` 渲染成 `<SidebarMenuLink href=…>`;死 prop `onOpenReports` 删除 |
| **T5** 折叠状态持久化 | ⚠️ 完成,但触发让步顺序第 1 条 | 选 **localStorage**,键 `sidebar_state`;整页加载时首帧仍可能闪一下,见 §五 |
| **T6** 文档与清单 | ✅ 完成 | `frontend/DESIGN.md``docs/testing/sidebar-unify-20260916.md``docs/BUG_HISTORY.md` BUG-744/745/746、`CHANGELOG.md` 一条、`docs/tasks/README.md` 状态板 |
### T3 为什么是「失效」不是「写穿」
任务书 T3 原话是「调用 `invalidateSidebarCache()`(或直接写穿)」,两者都在主方案内,**没有触发让步顺序第 2 条**。选失效的理由:`Home()` 持有的是完整的 `ChatSession`(带 messages、rectification 字段、chartProfile 等),只读侧栏要的是精简的 `SidebarSession`,写穿等于在两处各维护一份表示;而失效只多一次 GET,代价确定。落点全在 `use-session-management.ts``page.tsx` 一行没为这件事变过。
## 四、改过的既有断言(原值 / 新值 / 原因)
共 9 处。没有一条是削弱:每一条要么主语不变只换取证位置,要么额外补了反向断言。
| # | 文件 · 测试 | 原值 | 新值 | 原因 |
| --- | --- | --- | --- | --- |
| 1 | `sidebar-contract` · `leaves the chat document for chart, ephemeris, and reports` → 改名 `reaches chart, ephemeris and reports with links, not a document load` | `persistLoginSessionReturn(); window.location.assign(path)` + 三个 `leaveChat("/…")` 调用点 | 三项由 `NAV_PAGES` 渲染成 `<SidebarMenuLink>``leaveChat()` 只剩 `persistLoginSessionReturn()`;新增 `doesNotMatch(/window\.location/)` | D3/T4。原写法是**有意**的整页刷新,理由是「首页用 `history.pushState` 维护 `?c=``router.push` 离不开首页」;`<Link>` 没有这个限制。`?c=` 的保存一步没少,仍在跳转前。测试条数不变(改名不算删条) |
| 2 | `sidebar-contract` · `history renders recency group labels and a silent load-more sentinel` | `sessionControls.hasMore ? <div ref={loadMoreRef}` | `hasMoreSessions ? <div ref={loadMoreRef}` | `sessionControls` 在只读模式下不存在,改从可选的 `controls` 解构;`hasMoreSessions` 是派生的稳定布尔值,effect 依赖数组也用它,否则 exhaustive-deps 会要求整个对象、让 IntersectionObserver 每次渲染重建。哨兵行为不变 |
| 3 | `chat-navigation-a11y-contract` · `in-app destinations navigate client-side…` | `import { useRouter }` + `const router = useRouter()` + `onOpenReports={() => router.push("/reports")}` | `doesNotMatch(pageSource, /useRouter/)` + `doesNotMatch(pageSource, /onOpenReports/)` + 断言侧栏用 `<SidebarMenuLink className="report-nav-button">` 且不含 `window.location` | `onOpenReports` 从 D9 起就是死 prop(侧栏解构成 `_onOpenReports` 从未调用),真正跳转走的是整页刷新。删掉后 `router``page.tsx` 再无消费者。断言主语(站内目的地不得整页刷新)不变,且从「首页持有 router」升级成「侧栏用 Link」 |
| 4 | `personal-report-entry` · `entry is global in the sidebar and absent from the active session header` | `match(sidebarSource, /onOpenReports/)` + `match(pageSource, /onOpenReports=\{\(\) => router\.push\("\/reports"\)\}/)` | `match(sidebarSource, /\{ href: "\/reports", label: "我的报告"/)` + 三条 `doesNotMatch`(侧栏无 `onOpenReports`、页面无 `onOpenReports`、页面无 `useRouter` | 同 #3。入口仍然只在侧栏一处,主语不变 |
| 5 | `personal-report-view` · `the reader renders inside the app shell, in every phase` | `import { SecondaryShell }` + `<SecondaryShell title="个人报告">` 计数 | `import { SecondaryHeader }` + `<SecondaryHeader title="个人报告" />` 计数 | D2 把 provider + 侧栏 + inset 上移到 layout`SecondaryShell` 拆剩顶栏并改名。「每个阶段都在外壳里、都恰好一次、没有裸 `<main>`」一字未改 |
| 6 | `chart-page-view` · `sidebar adds 星盘 and 星历 after 新建对话 without renaming 我的报告` | 标签写在 `<SidebarHeader>` JSX 里,用 `header.indexOf(">星盘<")` 量顺序;`leaveChat("/chart")` / `leaveChat("/ephemeris")` | 顺序改在模块常量 `NAV_PAGES` 里量,另断言这一段仍排在「新建对话」之后;三个 href/label 对逐条断言;新增 `doesNotMatch(/window\.location\.assign/)` | T1 要求三项在两种模式下逐字一致,写两遍必然再分叉,因此收进常量。主语(新建对话 → 星盘 → 星历 → 我的报告,且「我的报告」未改名)不变 |
| 7 | `chart-page-view` · `the chart page shell is visible before the natal chart arrives` | `match(markup, /新建对话/)`(在页面组件产物里断言侧栏第一项) | `match(markup, /data-sidebar="trigger"/)`;侧栏本体改由新增的 layout 合同断言 | D2 之后侧栏不再由页面组件渲染,页面产物里本就不该有它。主语(这一页不靠一次性「返回对话」链接回去)不变,覆盖没有减少——新增的 layout 测试比原断言更强 |
| 8 | `chart-page-view` / `ephemeris-page` 的 render 辅助 | `renderToStaticMarkup(<ChartPageView …/>)` / `(<EphemerisView …/>)` | 外面裹一层 `SidebarProvider``withSidebarProvider` | provider 从每页各一份上移到 layout,页面组件自己不再自带,而顶栏里的 `SidebarTrigger` 仍要读它。改的是测试挂载环境,不是断言主语 |
| 9 | `chart-view-route` / `ephemeris-page` / `stale-client-recovery` 的源码路径 | `../src/app/{chart,ephemeris,reports}/page.tsx` | `../src/app/(secondary)/{…}/page.tsx` | 路由组括号不进 URL`/chart` 等一字未改;改的只是源码位置 |
### 新增测试(+8 条,总数 3391 → 3399
- `tests/sidebar-data-cache.test.ts`(新文件,4 条):命中缓存不重拉 / 过期后重拉一次 / 写操作后拿到新标题(并逐条锁住五个写路径的调用点)/ 双账户不串 / 只存内存 / 401 清空。
- `tests/sidebar-state.test.ts` +2:收起后重挂仍收起(含无存储、脏值、存储不可用三种降级);移动端不读不写且 `null` 存储不抛。
- `tests/sidebar-contract.test.ts` +1`the same component renders read-only when / is not the one mounting it`——`controls?` 可选、只读行带 `.session-main`、只读行无 `session-menu-trigger``.session-row[data-readonly="true"]` 不留 44px 列、页脚是 `.profile-trigger` 链接。
- `tests/chart-page-view.test.tsx` +1`the (secondary) layout mounts one read-only sidebar for all four routes`——`<SidebarProvider>` 恰好一次、不传 `controls`、数据 hook 只发两个 GET 且不含任何写方法。
## 五、让步顺序
**触发了第 1 条**(T5 折叠持久化 SSR 首帧对不齐),没有触发第 2、3 条。
- 选的是 **localStorage**,不是 cookie。理由是硬约束而不是偏好:`/``/chart``/ephemeris` 三条路由都是 `○ Static`,在服务端读 cookie 会让它们一起掉出静态渲染,直接违反红线 3。cookie 若只在客户端读,则不比 localStorage 早一帧,没有任何收益。
- 因此读取只能发生在挂载后——具体是 `SidebarProvider``ready` 那个 effect,也就是断点默认值**原本就生效**的同一帧。对客户端跳转(`/` ↔ 次级页)没有任何闪动,因为 provider 要么不重挂、要么重挂时 `localStorage` 已可读。
- 剩下的缺口只有一种情形:桌面用户把侧栏收起来之后**整页加载**(F5 或直接输入网址)。首帧是展开,effect 之后收起。这一下在基线上不存在,因为基线根本不记状态。已写进 BUG-746 的「已知缺口」段与真机清单 E-4,请产品实测后判断是否值得再开一单(真正的修法是服务端读 cookie,代价是 `/` 掉出静态)。
- 同轮考虑并否决的两个绕法:`useState` 初始化里同步读 localStorage(客户端首帧与 SSR HTML 不一致,React 19 会报 hydration mismatch`suppressHydrationWarning` 又会让 React 跳过属性修补);根 layout 里塞内联脚本改 `<html>` 属性(要把 `.chat-app` 的栅格改成跟 `<html>` 走,动的面比本单大得多)。
## 六、次级页体积增加的原因
红线 4 只约束 `/` 的首屏 gzip(+0.94%,通过),次级页要求「列出」。它们涨了 8.8% / 13.2%,原因是 D1 的直接代价:次级页原先加载的是一个精简的只读组件,现在加载的是**整个** `AppSidebar` —— 里面包含 `@base-ui/react/menu``ThemePreferenceMenu``UserAvatar` 和带菜单的 `SidebarSessionRow`,即便只读模式一个都不渲染,模块仍然在同一个 chunk 里被打进去。
这是「一个组件」与「次级页更小」之间的取舍,产品在 D1 已经拍板要前者。若日后要把这部分拿回来,可行的做法是把账户菜单弹层拆成独立组件、只在 `controls` 存在时动态加载;本单没有做,因为它会在 `/` 的页脚引入一个加载态,撞红线 7(揭幕后不得出现 spinner / 骨架)。建议作为独立一单评估。
## 七、红线逐条
| 红线 | 结论 |
| --- | --- |
| 1 · `tsc` 0 错、`lint` 0 error | ✅ 0 / 0warning 118 → 115 |
| 2 · 测试总数不降;`sidebar-contract`(43) 与 `sidebar-state`(5) 不删条 | ✅ 3391 → 3399`sidebar-contract` 43 → **44**(只改写、只新增,未删);`sidebar-state` 5 → **7** |
| 3 · `/``○ Static`;其余四路由标记不变;路由组不改 URL | ✅ 五个标记逐个比对一致(见 §一);四个 URL 未变 |
| 4 · `/` 首屏 gzip ±2% | ✅ CSS 0.11%JS +0.94%;次级页已列出并说明 |
| 5 · `page.tsx` 不增长;`Home()``useState` / `useRef` 不增长 | ✅ 行数 1838 → **1837**(净删);`useState` **36 → 36**cap 36)、`useRef` **37 → 37**cap 37);`home-shell-growth-contract` 全绿。对 `page.tsx` 的改动只有两处:`AppSidebar` 的 props 形状(散 props 收进 `controls`)、删掉 `useRouter` 与死 prop `onOpenReports` |
| 6 · 次级页侧栏不发写接口;只读模式不 import `page.tsx` 的 hook | ✅ `use-sidebar-data.ts` 只有两个 GET,合同测试禁止 `method: "POST\|PATCH\|PUT\|DELETE"`layout 不传 `controls``app-sidebar.tsx` 不 import 任何 `page.tsx` 的 hook(合同测试仍禁止 `fetch(` / `/api/` |
| 7 · 揭幕后不得出现 spinner / 骨架 | ✅ 缓存未命中时仍是既有静态文案 `sidebar-empty`(「对话列表读取中」/「暂无对话…」/「登录后可以看到你的对话」);合同测试禁止 hook 里出现 `skeleton\|Spinner` |
| 8 · 不改 `useConversationScrollAnchor` / `ChatComposer` / BUG-698 的 `@supports` / 报告盘面 grid | ✅ 四者一字未动 |
| 9 · 同提交更新 `DESIGN.md` | ✅ 「Secondary page shell」整节重写成「一个组件、两种模式 + 只读少哪三样」,删掉 `.nav-rail-*` 描述;「Sidebar shell」的 **State** 一行按 T5 重写;报告打印一段的「nav rail」改「sidebar」 |
| 10 · 不升依赖、不顺手修 warning | ✅ `package.json` / `package-lock.json` 未动;warning 的减少全部来自删文件 |
## 八、环境缺口(不得写成通过)
| 项 | 缺什么 | 替代证据 |
| --- | --- | --- |
| Network 面板确认跳转无 document 请求 | 无 Chrome、无登录态 | 源码合同:侧栏禁止 `window.location`,三项是 `<SidebarMenuLink href>`;真机清单 §A |
| 跨页无 `/api/sessions` / `/api/account` | 同上 | layout 合同锁 `<SidebarProvider>` 恰好一次 + `useSidebarData()` 只在 layout 调用;缓存单测覆盖「命中不发 fetch」。真机清单 §B |
| 两页侧栏 DOM outerHTML 逐节点 diff | 同上(且任务书要求脱敏真实会话标题,本地无数据) | 同一个组件、同一份 `SidebarSessionRow`、同一份 CSS,差异面由 `sidebar-contract` 的只读合同锁死;真机清单 §D 给了可照做的 `copy(...)` 步骤 |
| 重命名后进 `/chart` 立刻可见 | 同上 | 单测锁五个写路径都调 `invalidateSidebarCache()`;真机清单 §C |
| 折叠状态跨页 / 整页刷新是否闪 | 同上 | 纯函数单测 + provider 源码断言;真机清单 §E(含第 4 条专门记录闪动) |
| `/reports/[reportId]` 打印仍隐藏侧栏 | 无 Chrome | `REPORT_SHELL_PRINT_CSS` 命中的五个选择器(`.chat-app``.chat-panel``[data-slot='sidebar-inset']``[data-slot='sidebar']``.chat-header`)在 layout 化之后一个不少、层级关系也没变——原先 `SecondaryShell` 渲染的正是这同一串,只是位置从组件内挪到了 layout;`printing a report inside the shell drops the chrome and its height lock` 合同测试仍绿。真机清单 §F |
| staging 部署核对 | 未推送(按分工只交本地分支,验收由主会话做) | 无 |
## 九、Python 侧
本轮零 Python 改动,未动 `scripts/``tests/`(Python)。核对过没有任何 Python 测试或脚本枚举 `frontend/src/app` 的路由集合(`grep -rn "src/app" --include=*.py` 只命中 `page.tsx``globals.css` 与几个 `api/*/route.ts` 的定点路径,均未移动),因此路由组不会打红 Python 合同(BUG-132 / BUG-736 那一类复发面已核对)。
+1 -1
View File
@@ -252,7 +252,7 @@
| `TASK-api-server-backdoor-close-20260916.md` | — | **取代 decomposition 单的阶段 1** · **第二版(2026-09-16 改写)**:第一版的「8 方法 / 314 行」**错了一个数量级**——闭包只跟了 `self._x()`,漏掉 `_compute_consultation_workflow` 那 6 行委托转给模块级 `execute_consultation_workflow(self, ...)` 的一跳。执行方重算实测 **115 方法 / 4,104 行(占全类 51% 方法),但碰 HTTP 上下文的仍是 0 个**。产品在 A/B/C 中**选定 B(mixin 抽取)**:方法体原样搬进 `ConsultationComputeMixin``JyotishAPIHandler(BaseHTTPRequestHandler, Mixin)` 靠 MRO 解析,离线调用方直接实例化 mixin——四个伪造点全关。**产品同时授权:§4 那条「不得搬 ≥150 行业务方法」按本意解释(本意是防逐行改写的 C 案),mixin 放行。** 必须先过 **spike 闸门**:搬完后 `test_api_server_security.py` 一字不改直接跑,绿则继续、红则退回 A(只搬 7 方法/300 行、关 2 个伪造点)。真风险是循环 import——闭包引用同文件 44 个模块级函数 + 21 个常量。`__new__` 归零只针对 scripts 侧 4→0tests 侧 29 处含不许改的 security 测试,保持不增长)。阶段 3 仍不做。不占 BUG 号 | **已执行(spike 红 → 退回 A** | spike 实测:整体 mixin 化后 `test_api_server_security.py` **1 failed / 128 passed**——`test_chart_async_job_executes_in_background` 挂在 `monkeypatch.setattr(jyotish_api_server, '_write_async_job_record', ...)`:调用方法随 mixin 搬走后从新模块 globals 解析,补丁落在旧模块上不生效(已证明把同一 fake 打到 mixin 模块即恢复原行为,非搬坏)。循环 import 不是障碍(移动集不引用 `JyotishAPIHandler`)。按 §6.3 退回 A`scripts/offline_compute_mixins.py` 收 7 方法 / 300 行 + 3 助手 + `BadRequest`,关掉 `consultation_workflow_service:27` 与 `local_accuracy_report:141` 两处伪造,scripts 侧 `__new__` **4 → 2**;类方法 225 → 218、行数 11,291 → 10,924security 测试一字未改 **129 passed**。剩余 2 处伪造都在咨询工作流链上,需另案解决 monkeypatch 落点问题 |
| `RECONCILE-20260916.md` | — | **对账清单(不是任务书)**:状态板与现实脱节(09-15~16 那 10 份早已合入却仍写「待领取/待验收」,本轮已修正),所以另外 7 份 09-10~14 的单**不能拿 README 当证据**——它们没有 `PROGRESS-*.md`,但引用的 BUG 号都是 `resolved`。要么被别的单顺带修了没留记录,要么压根没做。每行回一个「做了/没做」即可。另附三条我已查到确定没做的证据、两条状态未闭环的、以及 BLK-001 仍红(2026-09-16 在 `4f643aa0` 复跑确认) | 待产品负责人 / 执行方回填 | — |
| `TASK-rectification-precision-gate-guided-collect-20260916.md` | `PROGRESS-rectification-precision-gate-guided-collect-20260916.md` | **出卡加精度门槛 + 引导式补经历**:真机 30 分钟窗给 5 件事 / 3 领域就出卡,区间 20 分钟、5 候选并列 26/26/20%,卡下只有自由文本邀请。产品拍板 D1 出卡门槛「宽度 ≤10 分钟且前两名差 >3 个百分点且不并列」;D2 用户说没有了仍按现行规则出卡(保留永远给结果);D3 门槛未达改为系统点名逐题问 + 类型芯片 + 年/月选择器录入,删自由文本邀请;D4 跳过的线换问法再问一次(拒绝不重问,改 BUG-687 口径);D5 时间点题答「没发生」不关领域、存在性题问整个领域、七条线一张表不许按领域写死。引擎新增 `guided_collect_windows`(边界不设 45 天闸门、含小运),不改打分与 lead。Skill 10.0.27。附 T6 两条核实:B/C/D 回执文案对调、性格题后范围回弹 8 分钟。BUG-740~743 | **已验收:未通过(带修复单)** | `cfb41daf`tsc/lint/Static/gzip(+0.44%) 通过,D1D5 单测通过;但 `npm test` 新增 16 红(校正既有断言未跟上)+ memoization golden 因 receipt 新键红(门禁级,staging 仍部署 317e9f18);引导窗口领域轮询分配、回放未按真值注入、录入卡走模型轮 → `TASK-rectification-precision-gate-guided-collect-fix-20260916.md` |
| `TASK-sidebar-unify-20260916.md` | | **侧栏统一成一个组件 + 次级页共享外壳 + 跳转不整页刷新(纯前端)**:`/` 用 `AppSidebar`、四个次级页用另写的 `AppNavRail`,两套标记结构共用一份 CSS,会话行少 `.session-main` 一层(无内边距 / 44px 高 / 当前项标记、留 44px 空列)、页脚 56px 菜单 vs 44px 文字;首页进次级页是 `window.location.assign` 整页刷新,次级页各自在组件里挂 `SecondaryShell`、无共享 layout 无缓存,每次跳转重拉 `/api/sessions` + `/api/account`;折叠状态不持久。产品拍板 D1 只留一个 `AppSidebar`(操作回调可选 = 只读模式,D9「不带写操作」维持)、D2 路由组 `app/(secondary)/` layout 承载外壳、D3 改 `<Link>`、D4 模块级 60 s 缓存 + 首页写操作失效、D5 折叠状态存 cookie、D6 回首页慢的启动链另开一单。实测:接口 TTFB 0.630.90 s,回 `/` 启动链串行 ≥4 次往返 + 揭幕闸门 4 s。BUG-744746 | 待领取 | |
| `TASK-sidebar-unify-20260916.md` | `PROGRESS-sidebar-unify-20260916.md` | **侧栏统一成一个组件 + 次级页共享外壳 + 跳转不整页刷新(纯前端)**:`/` 用 `AppSidebar`、四个次级页用另写的 `AppNavRail`,两套标记结构共用一份 CSS,会话行少 `.session-main` 一层(无内边距 / 44px 高 / 当前项标记、留 44px 空列)、页脚 56px 菜单 vs 44px 文字;首页进次级页是 `window.location.assign` 整页刷新,次级页各自在组件里挂 `SecondaryShell`、无共享 layout 无缓存,每次跳转重拉 `/api/sessions` + `/api/account`;折叠状态不持久。产品拍板 D1 只留一个 `AppSidebar`(操作回调可选 = 只读模式,D9「不带写操作」维持)、D2 路由组 `app/(secondary)/` layout 承载外壳、D3 改 `<Link>`、D4 模块级 60 s 缓存 + 首页写操作失效、D5 折叠状态存 cookie、D6 回首页慢的启动链另开一单。实测:接口 TTFB 0.630.90 s,回 `/` 启动链串行 ≥4 次往返 + 揭幕闸门 4 s。BUG-744746 | 待验收 | `codex/sidebar-unify-20260916` |
| `TASK-rectification-precision-gate-guided-collect-fix-20260916.md` | — | **验收修复单**F1 `decision_receipt` 新增 `guided_collect_windows` 让 `test_rectification_engine_memoization` golden 红(在 CORE_PYTEST_TARGETSstaging 未部署 cfb41daf);F2 16 条既有校正断言红未按三栏改;F3 `mayDeliverOnPrecision` 缺省即放行、短路 BUG-654 且 `guidedCollectExhausted` 不含 refresh;F4 无领域轨道窗口轮询贴领域、一窗只问一领域;F5 离线回放注入 month_lo 非真值方向、0/20 不作数;F6 录入卡合成「或」列表句走模型轮;F7 page.tsx +1、手造 fixture。D6 产品已拍板:门槛只是必要条件,仍问完线再出。BUG 段 744 起 | 待领取 | 与主单同文件;开工基线 3391 / 47 |
## 命名与归档
+77
View File
@@ -0,0 +1,77 @@
# 真机清单 · 侧栏统一 + 次级页共享外壳 + 跳转不整页刷新(2026-09-16
对应任务书 `TASK-sidebar-unify-20260916.md`、进度记录 `PROGRESS-sidebar-unify-20260916.md``BUG-744` / `BUG-745` / `BUG-746`
下面每一条自动化做不了:都需要**浏览器 + 已登录账号**。执行环境无 Chrome、无登录态,因此这些是环境缺口,不是「已通过」。
准备:桌面 Chrome,宽度 ≥ 1024px,登录后至少有 3 段对话(其中 1 段已收藏)。开 DevTools → Network,勾上 `Preserve log`,筛选框留空。
## A · 跳转不再整页刷新(BUG-745 ①)
1. 停在 `/`Network 面板 Clear。
2. 点侧栏「星盘」。
- **期望**:地址栏变 `/chart`Network 里 **没有** Type = `document` 的请求(只有 `fetch/xhr``script`)。
- **不期望**:整页白一下、Network 顶部出现一条 `chart` 的 document 请求。
3. 同样点「星历」「我的报告」各一次,各自确认无 document 请求。
4. 点侧栏「新建对话」回 `/`,确认同样没有 document 请求。
## B · 四个页面之间不重拉会话列表(BUG-745 ②③)
1. 停在 `/chart`Network Clear,筛选框输入 `api/`
2. 依次点「星历」→「我的报告」→ 打开任意一份已完成报告。
- **期望**:整个过程里 **没有** `/api/sessions``/api/account` 请求。
- 页面自己的数据请求(`/api/ephemeris``/api/reports` 等)照常,属正常。
3. 原地等 70 秒以上,再点一次「星盘」。
- **期望**:这时才出现一次 `/api/sessions` + 一次 `/api/account`(60 秒缓存过期后的后台刷新),而且**刷新期间侧栏的会话列表一直在**,不闪空、不出骨架。
## C · 写操作后立刻可见(BUG-745 ③)
1.`/`,把任意一段对话重命名成一个好认的名字(例如「重命名验证 A」)。
2. 立刻点「星盘」。
- **期望**`/chart` 左侧栏里这一行已经是新名字。
3.`/` 新建一段对话,立刻进「我的报告」。
- **期望**:新对话在侧栏最上面。
4.`/` 归档一段对话,立刻进「星历」。
- **期望**:它已从侧栏消失。
## D · 两页侧栏逐节点一致(BUG-744)
1.`/` 打开 DevTools Console,执行:
`copy(document.querySelector('[data-sidebar="sidebar"]').outerHTML)`,粘进一个文本文件。
2.`/chart` 执行同一句,粘进第二个文件。
3. 把两份里的会话标题替换成同样的占位(脱敏后再对比),然后 diff。
- **期望**:差异只有三处——每行的菜单按钮 `.session-menu-trigger`、页脚的 `.chevron`、页脚外面那层账户菜单触发器(`/``<button>`,次级页是 `<a href="/">`);另加 `/` 的会话行是 `<button class="session-main">`、次级页是 `<a class="session-main">`
- **不期望**:行高不同、`.session-main` 这一层在某一边不存在、某一边多出一列空白、页脚高度不同(两边都该是 56px)。
4.`/chart` 执行 `document.querySelectorAll('[data-sidebar="sidebar"] button').length`
- **期望**:只剩折叠 / 展开相关的触发器——展开态是 `[data-sidebar="rail"]` 一个;收起态还会有一个 `aria-label="聊天记录"` 的展开按钮(它也只做展开,不写任何数据)。
- **不期望**:出现任何「更多操作」按钮或账户菜单触发器。
5. 目测:`/chart` 上当前选中的页面项(星盘)有和 `/` 一致的选中态;会话行 hover 时行高、圆角、底色与 `/` 相同。
## E · 折叠状态跨页(BUG-746
1.`/` 点顶栏的折叠按钮收起侧栏。
2. 点「星盘」。
- **期望**`/chart` 的侧栏仍是收起的 64px 图标栏。
3.`/chart` 展开侧栏,点「新建对话」回 `/`
- **期望**`/` 的侧栏是展开的。
4. **已知缺口,只需记录不算失败**:在收起状态下按 F5 **整页刷新**,首帧可能先展开再收起(让步顺序第 1 条)。请记下「有没有看见这一下」以及是否刺眼,作为是否值得再开一单的依据。
5. 手机宽度(DevTools 设备模拟,375px):打开抽屉 → 点任意会话 → 抽屉关闭;再进 `/chart`,抽屉应当是**关着的**(移动端不记状态)。
## F · 报告打印仍然只印报告(BUG-745 回归面)
1. 打开任意一份已完成报告 `/reports/<id>`
2. Ctrl/Cmd+P 看打印预览。
- **期望**:侧栏、46px 顶栏、右侧目录都不在纸上;报告正文完整分多页,**不是**被截成一页。
3. 点页面上的「导出报告(.md)」确认仍能下载。
## G · 未登录降级
1. 退出登录(或用无痕窗口直接访问 `/chart`)。
- **期望**:侧栏还在,列表区是「登录后可以看到你的对话」一行字,页脚是「去登录」。
- **不期望**:骨架屏、spinner、「正在加载」、或任何报错块。
2. 点「去登录」。
- **期望**:进 `/login`(这一条**是**有意的整页跳转,Network 里有 document 请求属正常)。
## 结果怎么记
每条写「通过 / 不通过 + 截图或一句现象」。不通过的连同 Network 截图(去掉 Cookie 与 Authorization 头)一起回给会话,作为修复单的事故实证。