From 22edabbcd306ddaa1e9162973708fa45a4172306 Mon Sep 17 00:00:00 2001 From: Jesse_Chen Date: Wed, 16 Sep 2026 11:23:07 +0000 Subject: [PATCH] =?UTF-8?q?docs(tasks):=20=E4=BE=A7=E6=A0=8F=E7=BB=9F?= =?UTF-8?q?=E4=B8=80=E8=BF=9B=E5=BA=A6=E8=AE=B0=E5=BD=95=E3=80=81BUG-744?= =?UTF-8?q?=EF=BD=9E746=E3=80=81=E7=9C=9F=E6=9C=BA=E6=B8=85=E5=8D=95?= =?UTF-8?q?=E4=B8=8E=20CHANGELOG?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `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 Claude-Session: https://claude.ai/code/session_0193vBv6w5MV2cifdTUu9H5P --- CHANGELOG.md | 10 ++ docs/BUG_HISTORY.md | 49 ++++++++ docs/tasks/PROGRESS-sidebar-unify-20260916.md | 118 ++++++++++++++++++ docs/tasks/README.md | 2 +- docs/testing/sidebar-unify-20260916.md | 77 ++++++++++++ 5 files changed, 255 insertions(+), 1 deletion(-) create mode 100644 docs/tasks/PROGRESS-sidebar-unify-20260916.md create mode 100644 docs/testing/sidebar-unify-20260916.md diff --git a/CHANGELOG.md b/CHANGELOG.md index 1f3e0a0e..391f8d47 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,15 @@ # 印度占星 Skill 更新日志 +## 2026-09-16 — 四个页面共用同一个侧栏,换页不再重读列表、不再自己展开 + +星盘、星历、我的报告和报告阅读页的侧栏,此前是另写的一份,会话行比对话页矮一截、少了当前项的那条竖标记,行尾还空着一块给一个根本不存在的菜单按钮;页脚也是另一种。现在全站只有一个侧栏:在这四个页面上它不带重命名、归档、删除和账户菜单,除此之外每一行、每一处间距都和对话页一模一样。 + +从对话点进这三个页面不再整页刷新,四个页面之间来回走也不再重挂侧栏——最近对话读过一次,一分钟内换页直接沿用,在对话页改了标题、新建、删除、归档或收藏,下一次进去看到的就是改完的。 + +侧栏收起来的状态现在记得住:在对话页收起,进星盘还是收起的。手机上的抽屉不记——它每次都该是关着的。 + +Skill 版本不变(纯界面改动)。 + ## 2026-09-16 — 生时校正出卡加精度门槛,补经历改成系统点名 精度不够时不再出交付卡。区间还宽、头名还并列时,系统按大运边界逐条问「这几个月有没有这类事」,用类型芯片和年/月选择器录入,不再邀请随便打字。点「先跳过」的线会换一种问法再问一次;答「这类事都没有过」的不再问。用户明确说「没有了」仍然立刻给目前范围。Skill 升到 10.0.27。旧的校正会话仍按当时绑定的版本打开。 diff --git a/docs/BUG_HISTORY.md b/docs/BUG_HISTORY.md index 5bc93330..83cde650 100644 --- a/docs/BUG_HISTORY.md +++ b/docs/BUG_HISTORY.md @@ -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()` 的会话管理层上提,另写了第二个组件。它的会话行是 `` 直接套 `.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` 是 ``、不渲染菜单按钮、并用 `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`。③ 这两个读没有任何缓存。 +- 修复:① 三个页面项改 `` 客户端跳转,`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 里 `` 恰好一次、且不传 `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 +- 复发自:无 +- 修复版本:待发布 diff --git a/docs/tasks/PROGRESS-sidebar-unify-20260916.md b/docs/tasks/PROGRESS-sidebar-unify-20260916.md new file mode 100644 index 00000000..eb0f090d --- /dev/null +++ b/docs/tasks/PROGRESS-sidebar-unify-20260916.md @@ -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** B(23 个) | **609,504** B(25 个) | ✅ +0.94% | +| `/chart` 同口径 | 282,125 B(17 个) | 319,352 B(20 个) | ⚠️ +13.2%,原因见 §六 | +| `/ephemeris` 同口径 | 318,825 B(19 个) | 346,838 B(21 个) | ⚠️ +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` + 60 s TTL;`use-sidebar-data.ts` 同步读、后台刷新;`use-session-management.ts` 五个写操作成功后 `invalidateSidebarCache()`;401 清空 | +| **T4** 首页进次级页改客户端跳转 | ✅ 完成 | `leaveChat()` 只剩 `persistLoginSessionReturn()` + 关抽屉;三个页面项由 `NAV_PAGES` 渲染成 ``;死 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` 渲染成 ``;`leaveChat()` 只剩 `persistLoginSessionReturn()`;新增 `doesNotMatch(/window\.location/)` | D3/T4。原写法是**有意**的整页刷新,理由是「首页用 `history.pushState` 维护 `?c=`,`router.push` 离不开首页」;`` 没有这个限制。`?c=` 的保存一步没少,仍在跳转前。测试条数不变(改名不算删条) | +| 2 | `sidebar-contract` · `history renders recency group labels and a silent load-more sentinel` | `sessionControls.hasMore ?
router.push("/reports")}` | `doesNotMatch(pageSource, /useRouter/)` + `doesNotMatch(pageSource, /onOpenReports/)` + 断言侧栏用 `` 且不含 `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 }` + `` 计数 | `import { SecondaryHeader }` + `` 计数 | D2 把 provider + 侧栏 + inset 上移到 layout,`SecondaryShell` 拆剩顶栏并改名。「每个阶段都在外壳里、都恰好一次、没有裸 `
`」一字未改 | +| 6 | `chart-page-view` · `sidebar adds 星盘 and 星历 after 新建对话 without renaming 我的报告` | 标签写在 `` 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()` / `()` | 外面裹一层 `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`——`` 恰好一次、不传 `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 里塞内联脚本改 `` 属性(要把 `.chat-app` 的栅格改成跟 `` 走,动的面比本单大得多)。 + +## 六、次级页体积增加的原因 + +红线 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 / 0;warning 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`,三项是 ``;真机清单 §A | +| 跨页无 `/api/sessions` / `/api/account` | 同上 | layout 合同锁 `` 恰好一次 + `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 那一类复发面已核对)。 diff --git a/docs/tasks/README.md b/docs/tasks/README.md index b42d17cd..ffca3298 100644 --- a/docs/tasks/README.md +++ b/docs/tasks/README.md @@ -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→0(tests 侧 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,924;security 测试一字未改 **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%) 通过,D1~D5 单测通过;但 `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 改 ``、D4 模块级 60 s 缓存 + 首页写操作失效、D5 折叠状态存 cookie、D6 回首页慢的启动链另开一单。实测:接口 TTFB 0.63~0.90 s,回 `/` 启动链串行 ≥4 次往返 + 揭幕闸门 4 s。BUG-744~746 | 待领取 | — | +| `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 改 ``、D4 模块级 60 s 缓存 + 首页写操作失效、D5 折叠状态存 cookie、D6 回首页慢的启动链另开一单。实测:接口 TTFB 0.63~0.90 s,回 `/` 启动链串行 ≥4 次往返 + 揭幕闸门 4 s。BUG-744~746 | 待验收 | `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_TARGETS,staging 未部署 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 | ## 命名与归档 diff --git a/docs/testing/sidebar-unify-20260916.md b/docs/testing/sidebar-unify-20260916.md new file mode 100644 index 00000000..10444460 --- /dev/null +++ b/docs/testing/sidebar-unify-20260916.md @@ -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`、页脚外面那层账户菜单触发器(`/` 是 `