Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f8e607c29a | ||
|
|
3b17c1b242 |
@@ -92,8 +92,10 @@
|
||||
|
||||
## 6. 代码增长冻结
|
||||
|
||||
- `scripts/jyotish_api_server.py` must not grow. New endpoints and features go in dedicated modules under `scripts/` and are thinly registered from the main file. Do not add new handler bodies, workflows, or feature branches to this file. The cap is enforced by `tests/test_api_server_growth_contract.py`(冻结时行数 + 300 行 bugfix 余量)。
|
||||
- `frontend/src/app/page.tsx` 已从 4,766 行拆到 2,000 行以下,**不得再增长**:新逻辑进 `frontend/src/hooks/`、`frontend/src/lib/` 或组件;参数式 hook 内部保持 0 个 React hook 的既定模式。
|
||||
行数是耦合的影子:盯行数时,`Home()` 的状态数和 `JyotishAPIHandler` 的类方法数仍在涨。主门换成耦合指标,行数只作粗护栏。
|
||||
|
||||
- `scripts/jyotish_api_server.py` must not grow. New endpoints and features go in dedicated modules under `scripts/` and are thinly registered from the main file. Do not add new handler bodies, workflows, or feature branches to this file. The live gate is `tests/test_api_server_growth_contract.py`:**类方法数不得增长、`JyotishAPIHandler.__new__` 伪造点不得增长**;行数是粗护栏(开工实测基线 + 300)。
|
||||
- `frontend/src/app/page.tsx`:`Home()` 的 `useState` / `useRef` 数不得增长。新逻辑进 `frontend/src/hooks/`、`frontend/src/lib/` 或组件;抽出去的 hook 与子组件应当持有自己的状态,`page.tsx` 只做装配。行数是粗护栏(开工实测基线 + 150),由 `frontend/tests/home-shell-growth-contract.test.ts` 执行。
|
||||
- 不得再手写第二个聊天输入框(一律 `ChatComposer`,自有草稿走 `value`)、第二套滚动跟随(一律 `useConversationScrollAnchor` + `JumpToLatestButton`)、第二套加载动画(揭幕后不得出现 spinner / 骨架 / "正在加载",流式生成中除外)。
|
||||
|
||||
## 7. 前端红线
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
# PROGRESS · 增长冻结改盯耦合(2026-09-16)
|
||||
|
||||
工作树:`.worktrees/freeze-metric-change-20260915`
|
||||
分支:`codex/freeze-metric-change-20260915`
|
||||
任务书基线写 `6b3248bf`;开工时 `origin/staging` = **`51a65d92`**(含 BUG-732)。
|
||||
本机 Windows。无 Docker。本单不占 BUG 号,不改 `CHANGELOG.md` / `BUG_HISTORY.md` / `docs/tasks/README.md`。
|
||||
`page.tsx` 与 `scripts/jyotish_api_server.py` **一行未动**(反向验证后已 `git checkout` 还原)。
|
||||
|
||||
## 开工实测基线(2026-09-16,`51a65d92`)
|
||||
|
||||
禁止抄任务书 1951 / 66 / 41 / 11334 / 225。下列数字全部本机现测。
|
||||
|
||||
| 组 | 指标 | 数法 | 实测 | 新门 |
|
||||
| --- | --- | --- | ---: | --- |
|
||||
| 1 | `frontend/src/app/page.tsx` 行数 | `Path.read_bytes().count(b"\n")`,与 `(source.match(/\n/g) ?? []).length` 相同 | **1951** | 粗护栏 ≤ 1951 + 150 = 2101 |
|
||||
| 2a | `Home()` `useState` | `\buseState[<(]`(含 `useState<Type>(`) | **66** | 主门,不得增长 |
|
||||
| 2b | `Home()` `useRef` | `\buseRef[<(]` | **41** | 主门,不得增长 |
|
||||
| 3 | `scripts/jyotish_api_server.py` 行数 | `Path.read_bytes().count(b"\n")` | **11291** | 粗护栏 ≤ 11291 + 300 = 11591 |
|
||||
| 4a | 四空格 `def`(任务书同一种数法) | `^ (?:async )?def \w+` 扫全文件 | **225** | 主门,不得增长 |
|
||||
| 4b | `JyotishAPIHandler.__new__` | `scripts/` + `tests/` 的 `*.py`,排除合同测试自身 | **33** | 主门,不得增长 |
|
||||
|
||||
说明:
|
||||
|
||||
- 任务书表格写 api 行数 11,334;本机 `51a65d92` 实测 **11291**(未抄)。
|
||||
- 任务书表格写 `__new__` = 4:那是 **scripts/ 生产伪造点**(`consultation_workflow_service.py` ×2、`capture_report_blocked_repairs_golden.py` ×1、`local_accuracy_report.py` ×1)。任务书要求扫 `scripts/` 与 `tests/`,实测合计 **33**(scripts 4 + tests 29)。主门按扫描范围合计冻结,只许降不许升。失败信息会列出命中文件。
|
||||
- 全文件四空格 `def` = 225,其中 `JyotishAPIHandler` 类体 222、类前嵌套 3。冻结用任务书指定的全文件数法。
|
||||
- 只按 `useState(` 会计到 26,漏掉泛型调用;合同用 `\buseState[<(]`。
|
||||
|
||||
`frontend/AGENTS.md` 只有 Next.js 版本提示,没有 §6 重复表述,本单未改。
|
||||
|
||||
## 做了什么
|
||||
|
||||
1. `tests/test_api_server_growth_contract.py`:主门改成类方法数 + `__new__` 计数;行数 rebaseline 为 11291 + 300。`must not grow` / `thinly registered` / `CORE_PYTEST_TARGETS` 三条原样仍绿。
|
||||
2. 新建 `frontend/tests/home-shell-growth-contract.test.ts`:`Home()` `useState` / `useRef` 不得增长,行数粗护栏 1951 + 150。
|
||||
3. `frontend/tests/chart-view-route.test.ts`:删除行数断言,保留 `doesNotMatch(pageSource, /chart-page|ChartPageView|\/api\/chart-view/)`,注明搬到 home-shell 合同。
|
||||
4. `AGENTS.md` §6:主门改耦合指标;删掉「参数式 hook 内部保持 0 个 React hook」;第三条三个「不得再手写第二套」原样保留;仍含 `must not grow` 与 `thinly registered`。
|
||||
|
||||
## 既有断言改动
|
||||
|
||||
| 文件 | 原值 | 新值 | 原因 |
|
||||
| --- | --- | --- | --- |
|
||||
| `chart-view-route.test.ts` `page.tsx does not grow to host the chart page` | `(pageSource.match(/\n/g) ?? []).length <= 1951` 且 `doesNotMatch(...)` | 测试改名为 `page.tsx does not host the chart page`,只留 `doesNotMatch` | 行数冻结与星盘页无关,搬到专用合同 |
|
||||
| `test_api_server_growth_contract.py` 行数 baseline | 11063(2026-09-02)+ 300 | **11291**(2026-09-16 实测)+ 300 | 任务书要求开工实测 rebaseline,不得抄旧数 |
|
||||
|
||||
## 反向验证
|
||||
|
||||
### 1. `page.tsx` 人为加一个 `useState`
|
||||
|
||||
在 `Home()` 里插入 `const [_growthContractProbe, _setGrowthContractProbe] = useState(false);`,然后:
|
||||
|
||||
```
|
||||
npx tsx --test tests/home-shell-growth-contract.test.ts
|
||||
not ok 1 - Home() useState count must not grow
|
||||
error: 'Home() has 67 useState calls; cap is 66. ...'
|
||||
# tests 3 / pass 2 / fail 1
|
||||
```
|
||||
|
||||
`useRef` 与行数粗护栏仍绿。随后 `git checkout -- frontend/src/app/page.tsx`,文件回到 `51a65d92`。
|
||||
|
||||
### 2. `jyotish_api_server.py` 人为加一个类方法
|
||||
|
||||
在 `JyotishAPIHandler` 里插入 `def _growth_contract_probe(self): return None`,然后:
|
||||
|
||||
```
|
||||
pytest tests/test_api_server_growth_contract.py
|
||||
FAILED test_jyotish_api_handler_method_count_must_not_grow
|
||||
AssertionError: ... has 226 four-space def methods; cap is 225.
|
||||
1 failed, 4 passed
|
||||
```
|
||||
|
||||
行数粗护栏、`__new__`、AGENTS 措辞、`CORE_PYTEST_TARGETS` 仍绿——尺子动的是耦合门。随后 `git checkout -- scripts/jyotish_api_server.py`。
|
||||
|
||||
## 测试
|
||||
|
||||
本 worktree 的 `.venv` 是已跟踪的指针文件(内容 `/workspace/Jyotisha/.venv`),Windows 上不能当目录用,未覆盖。合同测试用本机 `C:\Users\74082\anaconda3\python.exe`(pytest 9.1.1)。
|
||||
|
||||
| 命令 | 结果 |
|
||||
| --- | --- |
|
||||
| `python -m pytest tests/test_api_server_growth_contract.py -v` | **5 passed**(含原 `must not grow` / `thinly registered` / `CORE_PYTEST_TARGETS` 三条) |
|
||||
| `npx tsx --test tests/home-shell-growth-contract.test.ts tests/chart-view-route.test.ts` | **16 pass / 0 fail**(home-shell 3 + chart-view 13;chart-view 无行数断言) |
|
||||
| `./node_modules/.bin/tsc --noEmit` | 0 错 |
|
||||
| `npm run lint` | **0 error** / 119 warning(既有,未动) |
|
||||
| `npx tsx --test tests/*.test.ts` | 3196 tests / 3112 pass / **69 fail** / 15 skip。本单 3 条 + `page.tsx does not host the chart page` 均 `ok`。失败为无 Docker、Windows `SKILL.md` symlink `EPERM`、bash 部署脚本,与近期同机清单同类 |
|
||||
| `run_quality_gate.py --profile quick --skip-frontend-runtime` | **blocked**:`interpretation_source_inventory_gate.py` → `ModuleNotFoundError: No module named 'mcp'`。停在 pytest 之前,不是本单合同红。未写成通过 |
|
||||
|
||||
同 SHA 近期进度 `PROGRESS-consultation-session-capacity-20260915.md` 记 `tests/*.test.ts` 为 3089 / 3001 / **73** fail。本机这次 3196 / 3112 / **69** fail(PowerShell glob 与计数差一截,fail 未增加)。本单新增 3 条全绿,不把 69 写成通过。
|
||||
|
||||
未跑 `next build`:无 UI 改动;质量门在 Python 预检就停了。
|
||||
|
||||
## 未做(属范围外)
|
||||
|
||||
- 真的搬 `Home()` 状态或拆 `JyotishAPIHandler`(后续两单)
|
||||
- `__new__` 收到 0
|
||||
- 推 `origin HEAD:staging`
|
||||
@@ -0,0 +1,127 @@
|
||||
# PROGRESS · 把校正面的 15 个状态从首页搬下去(2026-09-16)
|
||||
|
||||
工作树:`.worktrees/home-state-lowering-20260915`
|
||||
分支:`codex/home-state-lowering-20260915`
|
||||
任务书基线写 `6b3248bf`;开工时 `origin/staging` = **`3b17c1b2`**(含 freeze-metric / C2 / R3)。
|
||||
本机 Windows。无 Docker。本单不占 BUG 号,不改 `CHANGELOG.md` / `BUG_HISTORY.md` / `docs/tasks/README.md`。未 `git push origin HEAD:staging`。未改数据库、API 路由、DESIGN.md(无可见样式变化)。
|
||||
|
||||
## 开工核对
|
||||
|
||||
`git log --oneline origin/staging` 前 20 行含:
|
||||
|
||||
| 依赖 | SHA | 证据 |
|
||||
| --- | --- | --- |
|
||||
| freeze-metric-change | `3b17c1b2` | `test(freeze): 增长冻结改盯耦合,行数降为粗护栏`(HEAD) |
|
||||
| C2 consultation-context-memory | `149e1ec4` | `fix(consult): drop traces, budget checkpoints, silent summary inherit` |
|
||||
| R3 settled-render-split | `58ccafb6` | `fix(rectification): stop re-parsing settled messages on every stream frame` |
|
||||
|
||||
AGENTS.md §6 已是新口径:抽出去的 hook 与子组件应当持有自己的状态。
|
||||
|
||||
## 开工实测(不要抄任务书 66/41)
|
||||
|
||||
数法与合同测试相同:`Home()` 切片上 `\buseState[<(]` / `\buseRef[<(]`。
|
||||
|
||||
| 指标 | 实测 |
|
||||
| ---: | ---: |
|
||||
| `Home()` `useState` | **66** |
|
||||
| `Home()` `useRef` | **41** |
|
||||
| `const [rectification…` | **15** |
|
||||
| `page.tsx` 行数 | **1951** |
|
||||
| `useRectificationSurface` 参数(type 字段) | **39**(任务书约 56,按 type 实测) |
|
||||
| `ConversationalBirthTimeRectification` props(type 字段) | **20**(任务书 24,按 type 实测;JSX 含 key 为 21) |
|
||||
|
||||
全量 `npm test`:**3218** tests / **3134** pass / **69** fail / **15** skipped。失败名单见下,全是环境缺口。
|
||||
|
||||
## 5.1 分类表(15 个,不得砍)
|
||||
|
||||
| 状态 | 类别 | 谁在读 | 处置 |
|
||||
| --- | --- | --- | --- |
|
||||
| `rectificationSessionId` | 外壳也要读 | `Home`:`rectificationSurfaceOpen`、`?c=` 自动打开、bootstrap `rectificationSettled`、组件 `key`;`useSessionManagement`:历史校正是否延后切换 | 留在外壳对象 `sessionId` |
|
||||
| `rectificationCaseId` | 外壳也要读 | `Home`:是否挂上校正面(`rectificationSurfaceOpen && rectificationCaseId`)、`key` | 留在外壳对象 `caseId` |
|
||||
| `rectificationOpeningSessionId` | 外壳也要读 | `AppSidebar` `openingSessionId`(侧栏「正在打开」) | 留在外壳对象 `openingSessionId` |
|
||||
| `rectificationLoading` | 外壳也要读 | 首页卡文案「正在打开」、自动打开守卫、`StarterHome` `data-opening` | 留在外壳对象 `loading` |
|
||||
| `rectificationMutationPending` | 外壳也要读 | `productEntrypointsDisabled`、自动打开守卫、首页卡 disabled | 留在外壳对象 `mutationPending` |
|
||||
| `rectificationEntrySummary` | 外壳也要读 | 首页卡 action/文案、`StarterHome` 卡片说明 | 留在外壳对象 `entrySummary` |
|
||||
| `rectificationEntrySummarySettled` | 外壳也要读 | `bootstrapPrepareReady` 揭幕门 | 留在外壳对象 `entrySummarySettled` |
|
||||
| `rectificationError` | 外壳也要读 | 自动打开守卫、bootstrap settled、首页卡报错、`rectificationErrorMessage` | 留在外壳对象 `error` |
|
||||
| `rectificationErrorSessionId` | 外壳也要读 | 侧栏行内错误;首页卡在 session 失败时藏同一条 | 留在外壳对象 `errorSessionId` |
|
||||
| `rectificationShouldStartOpening` | 只服务子树 | 校正面是否自动开场;`onOpeningConsumed` 把它关掉 | 搬进 `useRectificationSurface` |
|
||||
| `rectificationTurns` | 只服务子树 | 挂载前 hydrate 的 `initialTurns` | 搬进 hook |
|
||||
| `rectificationSnapshot` | 只服务子树 | 挂载前 hydrate 的 `initialSnapshot` | 搬进 hook |
|
||||
| `rectificationPendingQuestion` | 只服务子树 | 校正面 `pendingConsultationQuestion` | 搬进 hook |
|
||||
| `rectificationReadonly` | 只服务子树 | 校正面只读 | 搬进 hook |
|
||||
| `rectificationHeaderSlot` | 只服务子树 | 值只给校正面 portal;**setter** 仍是外壳 header 的 `ref` | 状态在 hook;`Home` 只用 `setRectificationHeaderSlot` |
|
||||
|
||||
外壳留下 **1 个** `useState`(`rectification` 对象,9 个字段)。没有多留第二个散装校正 state。`rectificationSessionOpenerRef` 仍在 `Home`:`useSessionManagement` 必须在 surface hook 之前调用,用这个 ref 把 opener 递过去。
|
||||
|
||||
## 收尾实测
|
||||
|
||||
| 指标 | 改前 | 改后 |
|
||||
| ---: | ---: | ---: |
|
||||
| `Home()` `useState` | 66 | **52**(≤53;搬走 14) |
|
||||
| `Home()` `useRef` | 41 | **39**(未上升;`resumeRectificationSession` / `rectificationOpenInFlight` 进 hook) |
|
||||
| `page.tsx` 行数 | 1951 | **1931** |
|
||||
| hook 参数(type 字段) | 39 | **24** |
|
||||
| `ConversationalBirthTimeRectification` props(type 字段) | 20 | **7**(`declaredTime` / `models` / `selectedModelId` / `onSelectModel` / `onOpenBilling` / `onSaved` / `panel`) |
|
||||
|
||||
`useRectificationSurface` 现在自己 `useState` / `useEffect` / `useRef`。子树字段打成 `rectificationPanel` 交给包装组件;包装仍 `return <RectificationAgenticChat {...props} />`。
|
||||
|
||||
## 既有断言改动
|
||||
|
||||
| 文件 | 原值 | 新值 | 原因 |
|
||||
| --- | --- | --- | --- |
|
||||
| `home-shell-growth-contract.test.ts` 三门 | useState **66** / useRef **41** / 行数基线 **1951** | **52** / **39** / **1931** | 本单下沉后的新基线 |
|
||||
| `consultation-entrypoint.test.ts` 三处 pending / 一处 onPendingChange / 一处挂载守卫 | JSX `pendingConsultationQuestion={rectificationPendingQuestion}`;`onPendingChange={setRectificationMutationPending}` | hook panel 字段 `pendingConsultationQuestion: rectificationPendingQuestion`;`onPendingChange: setRectificationMutationPending`;守卫内 `panel={rectificationPanel}` | 子树 props 收到 panel |
|
||||
| `rectification-agentic-entry.test.ts` shouldStartOpening / onOpeningConsumed / initialTurns / onMessagesChange / headerSlot | 同上,JSX `={...}` | hook panel 冒号字段;page 仍 `ref={setRectificationHeaderSlot}` | 同上 |
|
||||
| `rectification-surface-contract.test.ts` initialSnapshot | JSX `initialSnapshot={rectificationSnapshot}` | `initialSnapshot: rectificationSnapshot` | 同上 |
|
||||
| `rectification-history-open-20260909.test.ts` | `const [rectificationErrorSessionId, setRectificationErrorSessionId]` | `errorSessionId: rectificationErrorSessionId` + `setRectificationErrorSessionId` | 外壳 9 字段合成一个对象 |
|
||||
|
||||
未弱化其它既有断言。`rectification-*.test.ts` 除 Windows symlink 整文件红的 `rectification-v9-agent.test.ts`(基线已红)外全绿。
|
||||
|
||||
## 反向验证
|
||||
|
||||
在 `Home()` 里插入 `const [_growthContractProbe, _setGrowthContractProbe] = useState(false);` 后:
|
||||
|
||||
```
|
||||
not ok 1 - Home() useState count must not grow
|
||||
error: 'Home() has 53 useState calls; cap is 52. ...'
|
||||
# tests 3 / pass 2 / fail 1
|
||||
```
|
||||
|
||||
`useRef` 与行数粗护栏仍绿。随后删掉探针,文件回到 52/39/1931。
|
||||
|
||||
## 测试
|
||||
|
||||
| 命令 | 结果 |
|
||||
| --- | --- |
|
||||
| `node node_modules/typescript/bin/tsc --noEmit` | **0 错** |
|
||||
| `npm run lint` | **0 error** / **120** warning(freeze-metric 记 119;本单多 1 条:入口摘要 effect 的两个外壳 setter 不是 `useState` setter,`exhaustive-deps` 提醒。不能把它们放进 deps,否则每帧重拉摘要。未顺手修既有 warning) |
|
||||
| 定向 `npx tsx --test tests/home-shell-growth-contract.test.ts tests/consultation-entrypoint.test.ts tests/home-bootstrap-reveal.test.ts tests/rectification-agentic-entry.test.ts tests/rectification-history-open-20260909.test.ts tests/rectification-surface-contract.test.ts` | **98 pass / 0 fail** |
|
||||
| `npx tsx --test tests/rectification-*.test.ts`(加上述合同) | 除 `rectification-v9-agent.test.ts` 整文件 `SkillPackageRegistryError`(EPERM symlink,基线已有)外 fail=0 |
|
||||
| `npm test` | **3218** / **3134** pass / **69** fail / **15** skipped。失败名单与开工基线 **69 条逐条相同** |
|
||||
| `npm run build` / `next build --webpack` | 见下 |
|
||||
|
||||
## 全量失败清单(与开工基线逐条相同,环境缺口)
|
||||
|
||||
`admin code functions reject immutable codes…`;`operation-level admin email reauthentication…`;`rectification agent maps setup failures…`;`public code never imports the internal dynamic choice contract`;`tests\\birth-time-guide-agent.test.ts`;`candidate scores stay out of the specified client ownership boundary`;`tests\\consultation-agentic-runtime.test.ts`;`tests\\consultation-birth-time-mode.test.ts`;methodology 8 条(`the route's own strict checklist…` 到 `the delivered method is quoted…`);`tests\\consultation-workflow-request.test.ts`;database/backup/env 一批(`admin customer reset…` 到 `database roles have no cluster privileges`);`every color token used as a Tailwind utility…`;compose/env/YAML/sync/deploy 一批;`tests\\rectification-v9-agent.test.ts`;`tests\\skill-binding.test.ts`;`checked-in registry verifies hashed product packages…`;symlink 3 条;`live consult skill reads a hand-updated tree…`;`changed staging workflows are syntactically valid YAML`;`live staging sync…` 两条;`first immutable deployment…`;`deployment scripts pass shell syntax validation`;`is-docs-only-range.sh…`。
|
||||
|
||||
无 Docker、无 Git Bash、Windows 不能为 skill runtime 建 symlink。不是本单回归。
|
||||
|
||||
## `next build`
|
||||
|
||||
默认 Turbopack 在 worktree `node_modules` junction 上会 panic(与 R3 相同)。`next build --webpack`:Compile + TypeScript 预期能过;Collecting page data 在 Windows `EPERM` 无法为 `SKILL.md` 建 runtime symlink 时失败。本机看不到 `○ /` 路由表。源码:`page.tsx` 无 `force-dynamic`;未改 `layout.tsx` / `globals.css`。首屏 gzip 上次实测 584,413 B,本机未能从构建产物复核 ±2%。
|
||||
|
||||
## 真人走查
|
||||
|
||||
`docs/testing/home-state-lowering-20260915.md`:进入 / 退出 / 刷新 / 首页卡片打开 / 历史校正 / 开场自动触发 / 只读 / 报错 / 换模型 / 采用后回首页。本仓无登录态、无 Chrome,未测。
|
||||
|
||||
## 发现的交互缺陷(本单不修)
|
||||
|
||||
无新增。入口摘要 effect 多了一条既有风格的 `exhaustive-deps` warning,见上。
|
||||
|
||||
## 未做(范围外)
|
||||
|
||||
- `session*` / `profile*` / `synastry*` / `account*` 四簇
|
||||
- Context Provider 或外部 store
|
||||
- 推 `origin HEAD:staging`
|
||||
- 改 `docs/tasks/README.md` 状态列
|
||||
@@ -0,0 +1,88 @@
|
||||
# 真机清单 · 校正面状态下沉后行为是否与改前一致(2026-09-15)
|
||||
|
||||
对应 `TASK-home-state-lowering-20260915`。自动化锁的是状态住处与源码合同,**锁不住**真实进入/退出/刷新/开场/只读/报错。本仓没有浏览器与登录态,下面这些必须用已登录的 staging 做。
|
||||
|
||||
地址:`https://staging.jyotisha.chat`。截图不要带真实姓名、出生资料或会话内容。本单**零文案、零交互改动**;任何和改前不一样的地方都记为失败。
|
||||
|
||||
先看 `GET /api/health` 的 `deployment.gitCommit` 是否等于本单合入 staging 后的 SHA。不一致 = 先别测。
|
||||
|
||||
## 1. 从首页卡片进入
|
||||
|
||||
1. 停在首页(资料已齐、有「生时校正」卡)。
|
||||
2. 点卡片。
|
||||
|
||||
- ✅ 卡面在打开期间显示「正在打开」,光标 `progress`;没有转圈、没有骨架。
|
||||
- ✅ 主栏一次揭开校正面:已有对话的话不是空白再闪出来。
|
||||
- ✅ 左侧当前会话高亮对上;地址栏带 `?c=`。
|
||||
- ❌ 先闪普通聊天再切校正;或空白题目区等加载。
|
||||
|
||||
## 2. 退出校正面
|
||||
|
||||
1. 在校正面点左侧一条普通咨询会话,或点「新对话」。
|
||||
|
||||
- ✅ 校正面关掉,普通聊天/首页回来;输入框是原来那一套,没有第二套。
|
||||
- ✅ 校正会话仍在左侧历史里,标题还在。
|
||||
|
||||
## 3. 刷新
|
||||
|
||||
1. 打开一场校正后按浏览器刷新。
|
||||
|
||||
- ✅ 刷新后仍是这场校正(`?c=` 还在),不是掉回首页、也不是空白。
|
||||
- ✅ 已结算的回合还在,不是重跑开场。
|
||||
|
||||
## 4. 首页卡片打开(新建 / 继续)
|
||||
|
||||
1. 没有未完成校正时点卡片:应新建。
|
||||
2. 有未完成校正时点卡片:文案仍是「新建一段独立校正…」,行为与改前一致(左侧历史可继续未完成的)。
|
||||
|
||||
- ❌ 点卡片却打开了别人的场,或首页卡把侧栏错误画到卡下。
|
||||
|
||||
## 5. 历史校正
|
||||
|
||||
1. 停在首页,不要先点卡片。
|
||||
2. 点左侧一条历史生时校正。
|
||||
|
||||
- ✅ 主栏打开该场;点了有反应。
|
||||
- ✅ 若打开失败:错误出现在被点的那一行下面,首页起始卡不重复同一条。
|
||||
|
||||
## 6. 开场自动触发
|
||||
|
||||
1. 打开一场**还没有回合**、服务端标记要开场的新校正。
|
||||
|
||||
- ✅ 进入后自动开始第一轮,不必再点「开始」。
|
||||
- ✅ 已有回合的历史场不会再自动开一场。
|
||||
|
||||
## 7. 只读
|
||||
|
||||
1. 打开一场已经结束 / 只读的校正。
|
||||
|
||||
- ✅ 输入框不可用;不能再采题。
|
||||
- ✅ 已有区间卡/对照仍在原位。
|
||||
|
||||
## 8. 报错
|
||||
|
||||
1. 资料不齐时点卡片:应回到资料补全,提示「请先完成出生资料,再开始生时校正。」
|
||||
2. 打开失败(网络或服务):首页卡下或侧栏行下有可见错误,可以再点。
|
||||
|
||||
- ❌ 点了没反应、也没有错误。
|
||||
|
||||
## 9. 换模型
|
||||
|
||||
1. 校正面底部的模型选择与改前同一套控件。
|
||||
2. 换一个模型后再发一条(或等当前轮结束再换)。
|
||||
|
||||
- ✅ 仍是这场校正,没有重挂、没有丢历史。
|
||||
|
||||
## 10. 采用后回首页
|
||||
|
||||
1. 若账号里有已采用结束的场:从左侧打开,再切回首页(新对话或一条普通咨询)。
|
||||
|
||||
- ✅ 回得去;首页校正卡还在;没有第二套输入框、没有转圈。
|
||||
|
||||
## 375px
|
||||
|
||||
视口 375px 再走 1、2、5。窄屏下进入/退出仍是一次揭开,没有第二套输入框。
|
||||
|
||||
## 结论
|
||||
|
||||
> 待填。测完写日期 + 设备 + 浏览器 + 逐条正常/有问题。测不了就写「未测」,不要写成通过。
|
||||
+31
-51
@@ -21,13 +21,11 @@ import {
|
||||
entrySummaryFromResponse,
|
||||
rectificationEntryLabels,
|
||||
resolveRectificationEntryAction,
|
||||
type RectificationEntrySummary,
|
||||
} from "@/lib/rectification-entry";
|
||||
import { ConversationalBirthTimeRectification, type PersistedRectificationTurn } from "@/components/conversational-birth-time-rectification";
|
||||
import { ConversationalBirthTimeRectification } from "@/components/conversational-birth-time-rectification";
|
||||
import {
|
||||
declaredBirthTime,
|
||||
RECTIFICATION_OPENING_LABEL,
|
||||
type RectificationCaseSnapshotPayload,
|
||||
} from "@/lib/rectification-surface-state";
|
||||
import {
|
||||
toggleChatMessageFeedback,
|
||||
@@ -72,7 +70,11 @@ import { useBirthTimeGuidedJourney } from "@/hooks/use-birth-time-guided-journey
|
||||
import { useConsultationRun } from "@/hooks/use-consultation-run";
|
||||
import { useConversationScrollAnchor } from "@/hooks/use-conversation-scroll-anchor";
|
||||
import { useProfileOnboarding } from "@/hooks/use-profile-onboarding";
|
||||
import { useRectificationSurface } from "@/hooks/use-rectification-surface";
|
||||
import {
|
||||
createRectificationShellSetters,
|
||||
EMPTY_RECTIFICATION_SHELL,
|
||||
useRectificationSurface,
|
||||
} from "@/hooks/use-rectification-surface";
|
||||
import { useSessionManagement } from "@/hooks/use-session-management";
|
||||
import { sortSessions } from "@/lib/session-groups";
|
||||
import { showChatNotice as setComposerNotice } from "@/lib/chat-notice";
|
||||
@@ -282,23 +284,26 @@ export default function Home() {
|
||||
const [birthTimeConsultationConsent, setBirthTimeConsultationConsent] = useState<BirthTimeConsultationConsentState>(
|
||||
createBirthTimeConsultationConsentState,
|
||||
);
|
||||
const [rectificationSessionId, setRectificationSessionId] = useState<string | null>(null);
|
||||
const [rectificationCaseId, setRectificationCaseId] = useState<string | null>(null);
|
||||
const [rectificationHeaderSlot, setRectificationHeaderSlot] = useState<HTMLElement | null>(null);
|
||||
const [rectificationPendingQuestion, setRectificationPendingQuestion] = useState<string | null>(null);
|
||||
const [rectificationLoading, setRectificationLoading] = useState(false);
|
||||
const [rectificationMutationPending, setRectificationMutationPending] = useState(false);
|
||||
const [rectificationError, setRectificationError] = useState("");
|
||||
const [rectificationErrorSessionId, setRectificationErrorSessionId] = useState<string | null>(null);
|
||||
const [rectificationReadonly, setRectificationReadonly] = useState(false);
|
||||
const [rectificationShouldStartOpening, setRectificationShouldStartOpening] = useState(false);
|
||||
const [rectificationTurns, setRectificationTurns] = useState<PersistedRectificationTurn[]>([]);
|
||||
const [rectificationSnapshot, setRectificationSnapshot] = useState<RectificationCaseSnapshotPayload | null>(null);
|
||||
const [rectificationOpeningSessionId, setRectificationOpeningSessionId] = useState<string | null>(null);
|
||||
const [rectificationEntrySummary, setRectificationEntrySummary] = useState<RectificationEntrySummary | null>(null);
|
||||
const [rectification, setRectification] = useState(EMPTY_RECTIFICATION_SHELL);
|
||||
const {
|
||||
sessionId: rectificationSessionId,
|
||||
caseId: rectificationCaseId,
|
||||
openingSessionId: rectificationOpeningSessionId,
|
||||
loading: rectificationLoading,
|
||||
mutationPending: rectificationMutationPending,
|
||||
entrySummary: rectificationEntrySummary,
|
||||
entrySummarySettled: rectificationEntrySummarySettled,
|
||||
error: rectificationError,
|
||||
errorSessionId: rectificationErrorSessionId,
|
||||
} = rectification;
|
||||
const {
|
||||
setRectificationError,
|
||||
setRectificationErrorSessionId,
|
||||
setRectificationEntrySummary,
|
||||
setRectificationEntrySummarySettled,
|
||||
} = createRectificationShellSetters(setRectification);
|
||||
const [hydrated, setHydrated] = useState(false);
|
||||
const [bootstrapPhase, setBootstrapPhase] = useState<BootstrapPhase>("account");
|
||||
const [rectificationEntrySummarySettled, setRectificationEntrySummarySettled] = useState(false);
|
||||
const prepareStartedAt = useRef<number | null>(null);
|
||||
const [guidedJourneyPreview, setGuidedJourneyPreview] = useState(false);
|
||||
const [profileSaving, setProfileSaving] = useState(false);
|
||||
@@ -347,8 +352,6 @@ export default function Home() {
|
||||
onFollowUp() {},
|
||||
});
|
||||
const accountRefreshGuard = useRef(createLatestAccountRequestGuard());
|
||||
const resumeRectificationSession = useRef<(session: ChatSession) => void>(() => undefined);
|
||||
const rectificationOpenInFlight = useRef(false);
|
||||
const sessionDetailInFlight = useRef(new Set<string>());
|
||||
const sessionsRef = useRef(sessions);
|
||||
const rectificationSessionOpenerRef = useRef(async (_exactSessionId: string) => {});
|
||||
@@ -465,21 +468,15 @@ export default function Home() {
|
||||
});
|
||||
|
||||
const {
|
||||
refreshRectificationCase,
|
||||
setRectificationHeaderSlot,
|
||||
rectificationPanel,
|
||||
resumeRectificationSession,
|
||||
openRectificationFromHomepage,
|
||||
startNewRectification,
|
||||
handleRectificationProfileIncomplete,
|
||||
handleRectificationMessagesChange,
|
||||
} = useRectificationSurface({
|
||||
account, activeChartId, activeSessionIdRef, chartLibrary, creatingSession, modelCatalog,
|
||||
persistSession, profile, rectificationLoading, rectificationMutationPending,
|
||||
rectificationOpenInFlight, rectificationSessionId, resumeRectificationSession,
|
||||
persistSession, profile, rectification, setRectification,
|
||||
sessionSelectionSource, sessions, setActiveSessionId, setDraft, setDraftEntrypoint,
|
||||
setDraftTheme, setOnboardingStep, setProfileNotice, setRectificationCaseId,
|
||||
setRectificationEntrySummary, setRectificationError, setRectificationErrorSessionId, setRectificationLoading,
|
||||
setRectificationPendingQuestion, setRectificationReadonly, setRectificationSessionId,
|
||||
setRectificationShouldStartOpening, setRectificationSnapshot, setRectificationOpeningSessionId,
|
||||
setRectificationTurns, setSessions, uiPreview,
|
||||
setDraftTheme, setOnboardingStep, setProfileNotice, setSessions, uiPreview,
|
||||
updateSession, openAccountDialog, refreshAccount, rectificationSessionOpenerRef,
|
||||
});
|
||||
|
||||
@@ -540,6 +537,7 @@ export default function Home() {
|
||||
rectificationLoading,
|
||||
rectificationMutationPending,
|
||||
rectificationSessionId,
|
||||
resumeRectificationSession,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -1855,31 +1853,13 @@ export default function Home() {
|
||||
{rectificationSurfaceOpen && rectificationCaseId && (
|
||||
<ConversationalBirthTimeRectification
|
||||
key={`${rectificationSessionId}-${rectificationCaseId}`}
|
||||
caseId={rectificationCaseId}
|
||||
sessionId={rectificationSessionId ?? ""}
|
||||
readonly={rectificationReadonly}
|
||||
shouldStartOpening={rectificationShouldStartOpening}
|
||||
initialTurns={rectificationTurns}
|
||||
initialSnapshot={rectificationSnapshot}
|
||||
declaredTime={rectificationDeclaredTime}
|
||||
models={modelCatalog?.models ?? []}
|
||||
selectedModelId={activeSession?.modelId ?? ""}
|
||||
onSelectModel={(modelId) => void selectSessionModel(modelId)}
|
||||
onMessagesChange={handleRectificationMessagesChange}
|
||||
onOpeningConsumed={() => setRectificationShouldStartOpening(false)}
|
||||
onCompleted={() => {
|
||||
void refreshAccount();
|
||||
if (rectificationCaseId && rectificationSessionId) {
|
||||
void refreshRectificationCase(rectificationCaseId, rectificationSessionId);
|
||||
}
|
||||
}}
|
||||
onPendingChange={setRectificationMutationPending}
|
||||
onProfileIncomplete={handleRectificationProfileIncomplete}
|
||||
onOpenBilling={(options) => openAccountDialog("billing", { source: options?.source ?? "rectification" })}
|
||||
onSaved={() => void refreshAccount()}
|
||||
pendingConsultationQuestion={rectificationPendingQuestion}
|
||||
onRestart={() => void startNewRectification()}
|
||||
headerSlot={rectificationHeaderSlot}
|
||||
panel={rectificationPanel}
|
||||
/>
|
||||
)}
|
||||
|
||||
|
||||
@@ -31,7 +31,7 @@ export type PersistedRectificationTurn = Readonly<{
|
||||
}> | null;
|
||||
}>;
|
||||
|
||||
export type ConversationalBirthTimeRectificationProps = Readonly<{
|
||||
export type RectificationChatPanel = Readonly<{
|
||||
caseId: string;
|
||||
sessionId: string;
|
||||
readonly: boolean;
|
||||
@@ -39,23 +39,44 @@ export type ConversationalBirthTimeRectificationProps = Readonly<{
|
||||
initialTurns: readonly PersistedRectificationTurn[];
|
||||
/** The Case snapshot read together with the turns before the surface mounted; null when hydration failed. */
|
||||
initialSnapshot: RectificationCaseSnapshotPayload | null;
|
||||
pendingConsultationQuestion?: string | null;
|
||||
headerSlot: HTMLElement | null;
|
||||
onMessagesChange?: (messages: ChatMessage[]) => void;
|
||||
onCompleted?: () => void;
|
||||
onPendingChange?: (pending: boolean) => void;
|
||||
onProfileIncomplete?: () => void;
|
||||
onOpeningConsumed?: () => void;
|
||||
onRestart?: () => void;
|
||||
}>;
|
||||
|
||||
export type ConversationalBirthTimeRectificationProps = Readonly<{
|
||||
/** The declared birth minute from the profile, shown on the board before any candidate exists. */
|
||||
declaredTime: string | null;
|
||||
models: readonly PublicLanguageModel[];
|
||||
selectedModelId: string;
|
||||
onSelectModel: (modelId: string) => void;
|
||||
onMessagesChange?: (messages: ChatMessage[]) => void;
|
||||
onCompleted?: () => void;
|
||||
onPendingChange?: (pending: boolean) => void;
|
||||
onProfileIncomplete?: () => void;
|
||||
onOpenBilling?: (options?: { source?: string }) => void;
|
||||
onSaved?: (time: string, status: "accepted" | "confirmed") => void;
|
||||
onOpeningConsumed?: () => void;
|
||||
pendingConsultationQuestion?: string | null;
|
||||
onRestart?: () => void;
|
||||
headerSlot: HTMLElement | null;
|
||||
panel: RectificationChatPanel;
|
||||
}>;
|
||||
|
||||
export function ConversationalBirthTimeRectification(props: ConversationalBirthTimeRectificationProps) {
|
||||
export function ConversationalBirthTimeRectification({
|
||||
declaredTime,
|
||||
models,
|
||||
selectedModelId,
|
||||
onSelectModel,
|
||||
onOpenBilling,
|
||||
onSaved,
|
||||
panel,
|
||||
}: ConversationalBirthTimeRectificationProps) {
|
||||
const props = {
|
||||
declaredTime,
|
||||
models,
|
||||
selectedModelId,
|
||||
onSelectModel,
|
||||
onOpenBilling,
|
||||
onSaved,
|
||||
...panel,
|
||||
};
|
||||
return <RectificationAgenticChat {...props} />;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import type { Dispatch, MutableRefObject, SetStateAction } from "react";
|
||||
import { useEffect, useRef, useState, type Dispatch, type MutableRefObject, type SetStateAction } from "react";
|
||||
|
||||
import { resolveSessionTitle } from "@/lib/agent-reply";
|
||||
import { rectificationOpenIdentity } from "@/lib/rectification-session-open";
|
||||
@@ -27,7 +27,10 @@ import {
|
||||
openResponseFromPayload,
|
||||
type RectificationEntrySummary,
|
||||
} from "@/lib/rectification-entry";
|
||||
import type { PersistedRectificationTurn } from "@/components/conversational-birth-time-rectification";
|
||||
import type {
|
||||
PersistedRectificationTurn,
|
||||
RectificationChatPanel,
|
||||
} from "@/components/conversational-birth-time-rectification";
|
||||
import type { PublicLanguageModelCatalog } from "@/lib/public-models";
|
||||
import {
|
||||
hydrateRectificationCase,
|
||||
@@ -38,6 +41,60 @@ import {
|
||||
type RectificationCaseSnapshotPayload,
|
||||
} from "@/lib/rectification-surface-state";
|
||||
|
||||
export type RectificationShellState = {
|
||||
sessionId: string | null;
|
||||
caseId: string | null;
|
||||
openingSessionId: string | null;
|
||||
loading: boolean;
|
||||
mutationPending: boolean;
|
||||
entrySummary: RectificationEntrySummary | null;
|
||||
entrySummarySettled: boolean;
|
||||
error: string;
|
||||
errorSessionId: string | null;
|
||||
};
|
||||
|
||||
export const EMPTY_RECTIFICATION_SHELL: RectificationShellState = {
|
||||
sessionId: null,
|
||||
caseId: null,
|
||||
openingSessionId: null,
|
||||
loading: false,
|
||||
mutationPending: false,
|
||||
entrySummary: null,
|
||||
entrySummarySettled: false,
|
||||
error: "",
|
||||
errorSessionId: null,
|
||||
};
|
||||
|
||||
function fieldSetter<K extends keyof RectificationShellState>(
|
||||
setRectification: Dispatch<SetStateAction<RectificationShellState>>,
|
||||
key: K,
|
||||
): Dispatch<SetStateAction<RectificationShellState[K]>> {
|
||||
return (value) => {
|
||||
setRectification((current) => ({
|
||||
...current,
|
||||
[key]: typeof value === "function"
|
||||
? (value as (prev: RectificationShellState[K]) => RectificationShellState[K])(current[key])
|
||||
: value,
|
||||
}));
|
||||
};
|
||||
}
|
||||
|
||||
export function createRectificationShellSetters(
|
||||
setRectification: Dispatch<SetStateAction<RectificationShellState>>,
|
||||
) {
|
||||
return {
|
||||
setRectificationSessionId: fieldSetter(setRectification, "sessionId"),
|
||||
setRectificationCaseId: fieldSetter(setRectification, "caseId"),
|
||||
setRectificationOpeningSessionId: fieldSetter(setRectification, "openingSessionId"),
|
||||
setRectificationLoading: fieldSetter(setRectification, "loading"),
|
||||
setRectificationMutationPending: fieldSetter(setRectification, "mutationPending"),
|
||||
setRectificationEntrySummary: fieldSetter(setRectification, "entrySummary"),
|
||||
setRectificationEntrySummarySettled: fieldSetter(setRectification, "entrySummarySettled"),
|
||||
setRectificationError: fieldSetter(setRectification, "error"),
|
||||
setRectificationErrorSessionId: fieldSetter(setRectification, "errorSessionId"),
|
||||
};
|
||||
}
|
||||
|
||||
export type RectificationSurfaceParams = {
|
||||
account: Account | null;
|
||||
activeChartId: string;
|
||||
@@ -47,11 +104,8 @@ export type RectificationSurfaceParams = {
|
||||
modelCatalog: PublicLanguageModelCatalog | null;
|
||||
persistSession: (session: ChatSession, mode?: "create" | "update") => Promise<void>;
|
||||
profile: Profile;
|
||||
rectificationLoading: boolean;
|
||||
rectificationMutationPending: boolean;
|
||||
rectificationOpenInFlight: MutableRefObject<boolean>;
|
||||
rectificationSessionId: string | null;
|
||||
resumeRectificationSession: MutableRefObject<(session: ChatSession) => void>;
|
||||
rectification: RectificationShellState;
|
||||
setRectification: Dispatch<SetStateAction<RectificationShellState>>;
|
||||
sessionSelectionSource: MutableRefObject<"user" | "history">;
|
||||
sessions: ChatSession[];
|
||||
setActiveSessionId: Dispatch<SetStateAction<string>>;
|
||||
@@ -60,18 +114,6 @@ export type RectificationSurfaceParams = {
|
||||
setDraftTheme: (theme: Theme | null) => void;
|
||||
setOnboardingStep: Dispatch<SetStateAction<OnboardingStep>>;
|
||||
setProfileNotice: Dispatch<SetStateAction<string>>;
|
||||
setRectificationCaseId: Dispatch<SetStateAction<string | null>>;
|
||||
setRectificationEntrySummary: Dispatch<SetStateAction<RectificationEntrySummary | null>>;
|
||||
setRectificationError: Dispatch<SetStateAction<string>>;
|
||||
setRectificationErrorSessionId: Dispatch<SetStateAction<string | null>>;
|
||||
setRectificationLoading: Dispatch<SetStateAction<boolean>>;
|
||||
setRectificationPendingQuestion: Dispatch<SetStateAction<string | null>>;
|
||||
setRectificationReadonly: Dispatch<SetStateAction<boolean>>;
|
||||
setRectificationSessionId: Dispatch<SetStateAction<string | null>>;
|
||||
setRectificationShouldStartOpening: Dispatch<SetStateAction<boolean>>;
|
||||
setRectificationSnapshot: Dispatch<SetStateAction<RectificationCaseSnapshotPayload | null>>;
|
||||
setRectificationOpeningSessionId: Dispatch<SetStateAction<string | null>>;
|
||||
setRectificationTurns: Dispatch<SetStateAction<PersistedRectificationTurn[]>>;
|
||||
setSessions: Dispatch<SetStateAction<ChatSession[]>>;
|
||||
uiPreview: MutableRefObject<boolean>;
|
||||
updateSession: (sessionId: string, change: (session: ChatSession) => ChatSession) => void;
|
||||
@@ -90,11 +132,8 @@ export function useRectificationSurface(params: RectificationSurfaceParams) {
|
||||
modelCatalog,
|
||||
persistSession,
|
||||
profile,
|
||||
rectificationLoading,
|
||||
rectificationMutationPending,
|
||||
rectificationOpenInFlight,
|
||||
rectificationSessionId,
|
||||
resumeRectificationSession,
|
||||
rectification,
|
||||
setRectification,
|
||||
sessionSelectionSource,
|
||||
sessions,
|
||||
setActiveSessionId,
|
||||
@@ -103,18 +142,6 @@ export function useRectificationSurface(params: RectificationSurfaceParams) {
|
||||
setDraftTheme,
|
||||
setOnboardingStep,
|
||||
setProfileNotice,
|
||||
setRectificationCaseId,
|
||||
setRectificationEntrySummary,
|
||||
setRectificationError,
|
||||
setRectificationErrorSessionId,
|
||||
setRectificationLoading,
|
||||
setRectificationPendingQuestion,
|
||||
setRectificationReadonly,
|
||||
setRectificationSessionId,
|
||||
setRectificationShouldStartOpening,
|
||||
setRectificationSnapshot,
|
||||
setRectificationOpeningSessionId,
|
||||
setRectificationTurns,
|
||||
setSessions,
|
||||
uiPreview,
|
||||
updateSession,
|
||||
@@ -123,6 +150,29 @@ export function useRectificationSurface(params: RectificationSurfaceParams) {
|
||||
rectificationSessionOpenerRef,
|
||||
} = params;
|
||||
|
||||
const rectificationSessionId = rectification.sessionId;
|
||||
const rectificationCaseId = rectification.caseId;
|
||||
const rectificationLoading = rectification.loading;
|
||||
const rectificationMutationPending = rectification.mutationPending;
|
||||
|
||||
const setRectificationSessionId = fieldSetter(setRectification, "sessionId");
|
||||
const setRectificationCaseId = fieldSetter(setRectification, "caseId");
|
||||
const setRectificationOpeningSessionId = fieldSetter(setRectification, "openingSessionId");
|
||||
const setRectificationLoading = fieldSetter(setRectification, "loading");
|
||||
const setRectificationMutationPending = fieldSetter(setRectification, "mutationPending");
|
||||
const setRectificationEntrySummary = fieldSetter(setRectification, "entrySummary");
|
||||
const setRectificationError = fieldSetter(setRectification, "error");
|
||||
const setRectificationErrorSessionId = fieldSetter(setRectification, "errorSessionId");
|
||||
|
||||
const [rectificationHeaderSlot, setRectificationHeaderSlot] = useState<HTMLElement | null>(null);
|
||||
const [rectificationPendingQuestion, setRectificationPendingQuestion] = useState<string | null>(null);
|
||||
const [rectificationReadonly, setRectificationReadonly] = useState(false);
|
||||
const [rectificationShouldStartOpening, setRectificationShouldStartOpening] = useState(false);
|
||||
const [rectificationTurns, setRectificationTurns] = useState<PersistedRectificationTurn[]>([]);
|
||||
const [rectificationSnapshot, setRectificationSnapshot] = useState<RectificationCaseSnapshotPayload | null>(null);
|
||||
const rectificationOpenInFlight = useRef(false);
|
||||
const resumeRectificationSession = useRef<(session: ChatSession) => void>(() => undefined);
|
||||
|
||||
async function refreshRectificationEntrySummary() {
|
||||
if (!account) return;
|
||||
try {
|
||||
@@ -289,12 +339,6 @@ export function useRectificationSurface(params: RectificationSurfaceParams) {
|
||||
await openRectificationCase("new", null, null);
|
||||
}
|
||||
|
||||
resumeRectificationSession.current = (session) => {
|
||||
sessionSelectionSource.current = "history";
|
||||
void openRectificationSession(session.id);
|
||||
};
|
||||
rectificationSessionOpenerRef.current = openRectificationSession;
|
||||
|
||||
function handleRectificationProfileIncomplete() {
|
||||
setRectificationError("profile_incomplete");
|
||||
setRectificationSessionId(null);
|
||||
@@ -320,7 +364,41 @@ export function useRectificationSurface(params: RectificationSurfaceParams) {
|
||||
}));
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
resumeRectificationSession.current = (session) => {
|
||||
sessionSelectionSource.current = "history";
|
||||
void openRectificationSession(session.id);
|
||||
};
|
||||
rectificationSessionOpenerRef.current = openRectificationSession;
|
||||
});
|
||||
|
||||
const rectificationPanel: RectificationChatPanel = {
|
||||
caseId: rectificationCaseId ?? "",
|
||||
sessionId: rectificationSessionId ?? "",
|
||||
readonly: rectificationReadonly,
|
||||
shouldStartOpening: rectificationShouldStartOpening,
|
||||
initialTurns: rectificationTurns,
|
||||
initialSnapshot: rectificationSnapshot,
|
||||
pendingConsultationQuestion: rectificationPendingQuestion,
|
||||
headerSlot: rectificationHeaderSlot,
|
||||
onMessagesChange: handleRectificationMessagesChange,
|
||||
onCompleted: () => {
|
||||
void refreshAccount();
|
||||
if (rectificationCaseId && rectificationSessionId) {
|
||||
void refreshRectificationCase(rectificationCaseId, rectificationSessionId);
|
||||
}
|
||||
},
|
||||
onPendingChange: setRectificationMutationPending,
|
||||
onProfileIncomplete: handleRectificationProfileIncomplete,
|
||||
onOpeningConsumed: () => setRectificationShouldStartOpening(false),
|
||||
onRestart: () => void startNewRectification(),
|
||||
};
|
||||
|
||||
return {
|
||||
rectificationHeaderSlot,
|
||||
setRectificationHeaderSlot,
|
||||
rectificationPanel,
|
||||
resumeRectificationSession,
|
||||
refreshRectificationEntrySummary,
|
||||
refreshRectificationCase,
|
||||
openRectificationCase,
|
||||
|
||||
@@ -296,8 +296,9 @@ test("the golden chart-view envelope stays inside the page contract", () => {
|
||||
assert.match(COORDINATE_BOUNDARY.qizheng, /角宿/);
|
||||
});
|
||||
|
||||
test("page.tsx does not grow to host the chart page", () => {
|
||||
assert.ok((pageSource.match(/\n/g) ?? []).length <= 1951);
|
||||
test("page.tsx does not host the chart page", () => {
|
||||
// Line-count freeze moved to tests/home-shell-growth-contract.test.ts
|
||||
// (Home() useState / useRef caps + coarse line-count guardrail).
|
||||
assert.doesNotMatch(pageSource, /chart-page|ChartPageView|\/api\/chart-view/);
|
||||
});
|
||||
|
||||
|
||||
@@ -173,7 +173,10 @@ test("homepage birth-time card opens the V9 Agentic surface via the server case
|
||||
assert.match(source, /openRectificationFromHomepage/);
|
||||
assert.match(source, /<ConversationalBirthTimeRectification/);
|
||||
assert.match(component, /<RectificationAgenticChat \{\.\.\.props\} \/>/);
|
||||
assert.match(source, /pendingConsultationQuestion=\{rectificationPendingQuestion\}/);
|
||||
// 原值:pendingConsultationQuestion={rectificationPendingQuestion} 写在 page.tsx JSX
|
||||
// 新值:pendingConsultationQuestion: rectificationPendingQuestion 写在 hook 的 panel
|
||||
// 原因:校正面子树状态收到 panel,Home 不再逐个传
|
||||
assert.match(source, /pendingConsultationQuestion: rectificationPendingQuestion/);
|
||||
assert.doesNotMatch(source, /chooseSuggestedQuestion\([\s\S]{0,180}"birth_time_rectification"/);
|
||||
assert.doesNotMatch(source, /draftBirthTimeRectificationQuestion/);
|
||||
});
|
||||
@@ -203,7 +206,10 @@ test("homepage opens through the server Case API and merges the returned session
|
||||
assert.match(handler, /rectificationOpenInFlight\.current = true;[\s\S]*?finally \{[\s\S]*?rectificationOpenInFlight\.current = false;/);
|
||||
assert.doesNotMatch(handler, /onNarrativeDelta/);
|
||||
assert.match(source, /const rectificationSurfaceOpen = activeRectificationSession\s*&& activeSession\.id === rectificationSessionId/);
|
||||
assert.match(source, /rectificationSurfaceOpen && rectificationCaseId && \([\s\S]*?<ConversationalBirthTimeRectification[\s\S]*?pendingConsultationQuestion=\{rectificationPendingQuestion\}/);
|
||||
// 原值:同一守卫内 JSX 写 pendingConsultationQuestion={rectificationPendingQuestion}
|
||||
// 新值:同一守卫内 JSX 写 panel={rectificationPanel};pending 字段在 hook panel
|
||||
// 原因:校正面子树状态收到 panel
|
||||
assert.match(source, /rectificationSurfaceOpen && rectificationCaseId && \([\s\S]*?<ConversationalBirthTimeRectification[\s\S]*?panel=\{rectificationPanel\}/);
|
||||
});
|
||||
|
||||
test("the page never creates the session shell locally; the server owns session creation", () => {
|
||||
@@ -278,7 +284,10 @@ test("rectify-first handoffs stay as Agent context", () => {
|
||||
const source = homeSurface;
|
||||
const chat = readFileSync(new URL("../src/components/rectification-agentic-chat.tsx", import.meta.url), "utf8");
|
||||
|
||||
assert.match(source, /pendingConsultationQuestion=\{rectificationPendingQuestion\}/);
|
||||
// 原值:pendingConsultationQuestion={rectificationPendingQuestion} 写在 page.tsx JSX
|
||||
// 新值:pendingConsultationQuestion: rectificationPendingQuestion 写在 hook 的 panel
|
||||
// 原因:校正面子树状态收到 panel,Home 不再逐个传
|
||||
assert.match(source, /pendingConsultationQuestion: rectificationPendingQuestion/);
|
||||
assert.match(chat, /pendingConsultationQuestion\?\.trim\(\)/);
|
||||
// 旧 横幅承诺按钮会带回原问题 → 新 按钮已删,改成结束后新建对话再问 → 保留 pendingConsultationQuestion 传递链
|
||||
assert.match(chat, /结束后新建对话,按采用的时间再问/);
|
||||
@@ -299,7 +308,10 @@ test("ordinary consultation uses current birth data without a rectification noti
|
||||
test("rectification mutations report pending state while session-level return controls stay absent", () => {
|
||||
const source = homeSurface;
|
||||
|
||||
assert.match(source, /onPendingChange=\{setRectificationMutationPending\}/);
|
||||
// 原值:onPendingChange={setRectificationMutationPending} 写在 page.tsx JSX
|
||||
// 新值:onPendingChange: setRectificationMutationPending 写在 hook 的 panel
|
||||
// 原因:mutationPending 仍由外壳对象持有,但 setter 经 panel 交给子树
|
||||
assert.match(source, /onPendingChange: setRectificationMutationPending/);
|
||||
assert.match(source, /disabled=\{productEntrypointsDisabled \|\| rectificationLoading \|\| rectificationMutationPending\}/);
|
||||
assert.doesNotMatch(source, /重试恢复/);
|
||||
assert.doesNotMatch(source, /返回并恢复原问题|返回首页/);
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { readFileSync } from "node:fs";
|
||||
import test from "node:test";
|
||||
|
||||
const pageSource = readFileSync(new URL("../src/app/page.tsx", import.meta.url), "utf8");
|
||||
|
||||
// Measured 2026-09-16 after home-state-lowering on origin/staging @ 3b17c1b2.
|
||||
// Line count: (pageSource.match(/\n/g) ?? []).length, same as `wc -l` = 1931.
|
||||
// Hook counts use \buseState[<(] / \buseRef[<(] so `useState<Type>(` is not missed.
|
||||
// Former caps were useState 66 / useRef 41 / lines 1951 on freeze-metric @ 51a65d92.
|
||||
// This round moved 14 rectification useStates out of Home (15 → 1 shell object)
|
||||
// and 2 rectification refs into useRectificationSurface.
|
||||
const PAGE_LINE_COUNT_BASELINE = 1931;
|
||||
const PAGE_LINE_COUNT_CAP = PAGE_LINE_COUNT_BASELINE + 150;
|
||||
const HOME_USE_STATE_CAP = 52;
|
||||
const HOME_USE_REF_CAP = 39;
|
||||
|
||||
const USE_STATE_RE = /\buseState[<(]/g;
|
||||
const USE_REF_RE = /\buseRef[<(]/g;
|
||||
|
||||
function homeSource(source: string): string {
|
||||
const marker = "export default function Home(";
|
||||
const start = source.indexOf(marker);
|
||||
assert.notEqual(start, -1, "page.tsx must export default function Home(");
|
||||
return source.slice(start);
|
||||
}
|
||||
|
||||
function countMatches(source: string, pattern: RegExp): number {
|
||||
return source.match(new RegExp(pattern.source, "g"))?.length ?? 0;
|
||||
}
|
||||
|
||||
test("Home() useState count must not grow", () => {
|
||||
const n = countMatches(homeSource(pageSource), USE_STATE_RE);
|
||||
assert.ok(
|
||||
n <= HOME_USE_STATE_CAP,
|
||||
`Home() has ${n} useState calls; cap is ${HOME_USE_STATE_CAP}. Extracted hooks and child components should own their own state.`,
|
||||
);
|
||||
});
|
||||
|
||||
test("Home() useRef count must not grow", () => {
|
||||
const n = countMatches(homeSource(pageSource), USE_REF_RE);
|
||||
assert.ok(
|
||||
n <= HOME_USE_REF_CAP,
|
||||
`Home() has ${n} useRef calls; cap is ${HOME_USE_REF_CAP}. Do not rewrite state as refs to dodge the useState freeze.`,
|
||||
);
|
||||
});
|
||||
|
||||
test("page.tsx line count stays within the coarse guardrail", () => {
|
||||
const n = (pageSource.match(/\n/g) ?? []).length;
|
||||
assert.ok(
|
||||
n <= PAGE_LINE_COUNT_CAP,
|
||||
`page.tsx has ${n} lines; cap is ${PAGE_LINE_COUNT_CAP} (${PAGE_LINE_COUNT_BASELINE} baseline + 150).`,
|
||||
);
|
||||
});
|
||||
@@ -124,9 +124,15 @@ test("opening is server-owned: shouldStartOpening drives the first turn, never c
|
||||
// 新:增加 skip_probe(核对卡「这题跳过」)
|
||||
// 原因:决策 4,跳过只关本题
|
||||
assert.match(route, /action: z\.enum\(\["opening", "message", "read_only", "answer_choice", "stop_and_review", "skip_probe"\]\)/);
|
||||
assert.match(page, /shouldStartOpening=\{rectificationShouldStartOpening\}/);
|
||||
// 原值:shouldStartOpening={rectificationShouldStartOpening} 写在 page.tsx JSX
|
||||
// 新值:shouldStartOpening: rectificationShouldStartOpening 写在 hook 的 panel
|
||||
// 原因:开场开关只服务子树,随 panel 下沉
|
||||
assert.match(page, /shouldStartOpening: rectificationShouldStartOpening/);
|
||||
assert.match(page, /setRectificationShouldStartOpening\(opened\.shouldStartOpening\)/);
|
||||
assert.match(page, /onOpeningConsumed=\{\(\) => setRectificationShouldStartOpening\(false\)\}/);
|
||||
// 原值:onOpeningConsumed={() => setRectificationShouldStartOpening(false)} 写在 page.tsx JSX
|
||||
// 新值:onOpeningConsumed: () => setRectificationShouldStartOpening(false) 写在 hook 的 panel
|
||||
// 原因:同上
|
||||
assert.match(page, /onOpeningConsumed: \(\) => setRectificationShouldStartOpening\(false\)/);
|
||||
});
|
||||
|
||||
test("incomplete profiles stay in the shared onboarding flow before any open request", () => {
|
||||
@@ -221,9 +227,12 @@ test("persisted turns survive remounts; duplicate openings are suppressed by the
|
||||
// Was: the "ready"/"loading" key suffix (see above). Persisted turns now survive
|
||||
// because nothing remounts: the key is the session/Case binding only (BUG-505).
|
||||
assert.match(page, /key=\{`\$\{rectificationSessionId\}-\$\{rectificationCaseId\}`\}/);
|
||||
assert.match(page, /initialTurns=\{rectificationTurns\}/);
|
||||
assert.match(page, /onMessagesChange=\{handleRectificationMessagesChange\}/);
|
||||
assert.match(page, /onOpeningConsumed=\{\(\) => setRectificationShouldStartOpening\(false\)\}/);
|
||||
// 原值:initialTurns={rectificationTurns} / onMessagesChange={handleRectificationMessagesChange} / onOpeningConsumed={...} 写在 page.tsx JSX
|
||||
// 新值:同名字段写在 hook 的 panel
|
||||
// 原因:子树自有状态与回调收到 panel,Home 只传外壳还要读的绑定
|
||||
assert.match(page, /initialTurns: rectificationTurns/);
|
||||
assert.match(page, /onMessagesChange: handleRectificationMessagesChange/);
|
||||
assert.match(page, /onOpeningConsumed: \(\) => setRectificationShouldStartOpening\(false\)/);
|
||||
});
|
||||
|
||||
test("the agent route verifies the exact Case/Session binding before any turn", () => {
|
||||
@@ -579,7 +588,10 @@ test("compact board overlays chat as a bottom sheet above the composer", () => {
|
||||
assert.match(page, /className="chat-header-rectification"/);
|
||||
assert.match(page, /data-rectification-header-slot=""/);
|
||||
assert.match(page, /ref=\{setRectificationHeaderSlot\}/);
|
||||
assert.match(page, /headerSlot=\{rectificationHeaderSlot\}/);
|
||||
// 原值:headerSlot={rectificationHeaderSlot} 写在 page.tsx JSX
|
||||
// 新值:headerSlot: rectificationHeaderSlot 写在 hook 的 panel;page 仍 ref={setRectificationHeaderSlot}
|
||||
// 原因:header 节点在外壳,值只服务子树
|
||||
assert.match(page, /headerSlot: rectificationHeaderSlot/);
|
||||
assert.match(styles, /\.chat-header-rectification \.rectification-board-peek \{[\s\S]*width: auto/);
|
||||
assert.doesNotMatch(chat, /rectification-workspace__board-trigger/);
|
||||
assert.doesNotMatch(chat.slice(chat.indexOf("className=\"composer-wrap\""), chat.indexOf("<form className=\"composer\"")), /RectificationBoardPeek/);
|
||||
|
||||
@@ -46,7 +46,11 @@ test("a history-list open failure shows under the clicked row, not on the starte
|
||||
// 原值:rectificationError 只由 starter-home 画;selectSession 不切会话,表现为点了没反应
|
||||
// 新值:session 失败绑到被点的那一行;首页起始卡不重复同一条
|
||||
// 决策 3:错误就地显示
|
||||
assert.match(pageSource, /const \[rectificationErrorSessionId, setRectificationErrorSessionId\]/);
|
||||
// 原值:const [rectificationErrorSessionId, setRectificationErrorSessionId] = useState(...)
|
||||
// 新值:外壳 9 个校正字段合成一个 rectification 对象;errorSessionId 仍叫 rectificationErrorSessionId,setter 仍叫 setRectificationErrorSessionId
|
||||
// 原因:外壳也要读的校正状态合并成一个对象,散装 useState 下降
|
||||
assert.match(pageSource, /errorSessionId: rectificationErrorSessionId,/);
|
||||
assert.match(pageSource, /setRectificationErrorSessionId,/);
|
||||
assert.match(
|
||||
pageSource,
|
||||
/rectificationError=\{rectificationErrorSessionId \? "" : rectificationError\}/,
|
||||
|
||||
@@ -35,7 +35,10 @@ test("the rectification surface is revealed once: Case hydration precedes the sw
|
||||
// Panel: one key per binding, snapshot and turns as initial state, later turns as a prop update.
|
||||
assert.match(page, /key=\{`\$\{rectificationSessionId\}-\$\{rectificationCaseId\}`\}/);
|
||||
assert.doesNotMatch(page, /"ready" : "loading"/);
|
||||
assert.match(page, /initialSnapshot=\{rectificationSnapshot\}/);
|
||||
// 原值:initialSnapshot={rectificationSnapshot} 写在 page.tsx JSX
|
||||
// 新值:initialSnapshot: rectificationSnapshot 写在 hook 的 panel
|
||||
// 原因:snapshot 只服务子树,随 panel 下沉
|
||||
assert.match(page, /initialSnapshot: rectificationSnapshot/);
|
||||
assert.match(page, /declaredTime=\{rectificationDeclaredTime\}/);
|
||||
assert.match(wrapper, /initialSnapshot: RectificationCaseSnapshotPayload \| null;/);
|
||||
assert.match(chat, /useState\(initialSnapshot !== null\)/);
|
||||
|
||||
@@ -1,30 +1,81 @@
|
||||
"""Freeze scripts/jyotish_api_server.py growth.
|
||||
"""Freeze coupling in scripts/jyotish_api_server.py.
|
||||
|
||||
New endpoints and features must live in new modules and be thinly registered
|
||||
from the main file. This cap is the live line count at freeze (11063 on
|
||||
2026-09-02, via `wc -l`) plus 300 lines of bugfix slack.
|
||||
from the main file. Line count is only a coarse guardrail; the live gates are
|
||||
JyotishAPIHandler method count and JyotishAPIHandler.__new__ forgery sites.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from collections import Counter
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
API_SERVER = ROOT / "scripts" / "jyotish_api_server.py"
|
||||
AGENTS = ROOT / "AGENTS.md"
|
||||
|
||||
# Live `wc -l scripts/jyotish_api_server.py` at freeze. New features must not
|
||||
# consume this budget; open a module instead.
|
||||
JYOTISH_API_SERVER_LINE_COUNT_BASELINE = 11063
|
||||
# Live `wc -l scripts/jyotish_api_server.py` equivalent:
|
||||
# Path.read_bytes().count(b"\n"), measured 2026-09-16 on origin/staging @ 51a65d92.
|
||||
# New features must not consume this budget; open a module instead.
|
||||
JYOTISH_API_SERVER_LINE_COUNT_BASELINE = 11291
|
||||
JYOTISH_API_SERVER_LINE_COUNT_CAP = JYOTISH_API_SERVER_LINE_COUNT_BASELINE + 300
|
||||
|
||||
# Whole-file indent match `^ (?:async )?def \w+`, same count as
|
||||
# TASK-freeze-metric-change-20260915 §1. Measured 2026-09-16 @ 51a65d92.
|
||||
JYOTISH_API_HANDLER_METHOD_COUNT_BASELINE = 225
|
||||
|
||||
# `JyotishAPIHandler.__new__` in scripts/ and tests/ `*.py`, excluding this file.
|
||||
# Measured 2026-09-16 @ 51a65d92: scripts/ production forgeries = 4, tests/ = 29.
|
||||
JYOTISH_API_HANDLER_NEW_COUNT_BASELINE = 33
|
||||
|
||||
HANDLER_METHOD_RE = re.compile(r"^ (?:async )?def \w+", re.MULTILINE)
|
||||
NEW_MARKER = "JyotishAPIHandler.__new__"
|
||||
|
||||
|
||||
def _handler_method_count(source: str) -> int:
|
||||
return len(HANDLER_METHOD_RE.findall(source))
|
||||
|
||||
|
||||
def _new_hits() -> Counter[str]:
|
||||
hits: Counter[str] = Counter()
|
||||
skip = Path(__file__).resolve()
|
||||
for folder in (ROOT / "scripts", ROOT / "tests"):
|
||||
for path in sorted(folder.rglob("*.py")):
|
||||
if path.resolve() == skip:
|
||||
continue
|
||||
count = path.read_text(encoding="utf-8").count(NEW_MARKER)
|
||||
if count:
|
||||
hits[path.relative_to(ROOT).as_posix()] = count
|
||||
return hits
|
||||
|
||||
|
||||
def test_jyotish_api_handler_method_count_must_not_grow() -> None:
|
||||
source = API_SERVER.read_text(encoding="utf-8")
|
||||
count = _handler_method_count(source)
|
||||
assert count <= JYOTISH_API_HANDLER_METHOD_COUNT_BASELINE, (
|
||||
f"{API_SERVER.as_posix()} has {count} four-space def methods; "
|
||||
f"cap is {JYOTISH_API_HANDLER_METHOD_COUNT_BASELINE}. Move behaviour "
|
||||
"into a dedicated module; thinly registered from this file."
|
||||
)
|
||||
|
||||
|
||||
def test_jyotish_api_handler_new_count_must_not_grow() -> None:
|
||||
hits = _new_hits()
|
||||
total = sum(hits.values())
|
||||
listed = ", ".join(f"{path}:{count}" for path, count in sorted(hits.items()))
|
||||
assert total <= JYOTISH_API_HANDLER_NEW_COUNT_BASELINE, (
|
||||
f"{NEW_MARKER} appears {total} times under scripts/ and tests/; "
|
||||
f"cap is {JYOTISH_API_HANDLER_NEW_COUNT_BASELINE}. Hits: {listed}"
|
||||
)
|
||||
|
||||
|
||||
def test_jyotish_api_server_must_not_grow_beyond_bugfix_slack() -> None:
|
||||
line_count = API_SERVER.read_bytes().count(b"\n")
|
||||
assert line_count <= JYOTISH_API_SERVER_LINE_COUNT_CAP, (
|
||||
f"{API_SERVER.as_posix()} has {line_count} lines; cap is "
|
||||
f"{JYOTISH_API_SERVER_LINE_COUNT_CAP} ({JYOTISH_API_SERVER_LINE_COUNT_BASELINE} "
|
||||
"baseline + 300 bugfix slack). New endpoints and features must be new "
|
||||
"baseline + 300 coarse guardrail). New endpoints and features must be new "
|
||||
"modules, thinly registered from this file."
|
||||
)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user