Files
Jyotisha/docs/tasks/PROGRESS-rectification-code-split-20260926.md
T
Jesse_ChenandClaude Opus 5.5 e0e2b38210
Independent Staging Quality Gate / validate (push) Failing after 6m56s
Independent Staging Quality Gate / publish (push) Skipped
docs(tasks): rectification code split acceptance
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017eEAG8HD3mm8gsKXgk8uU8
2026-09-26 20:25:19 +08:00

185 lines
20 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# PROGRESS · 生时校正代码拆分(只搬不改)+ 增长上限(2026-09-26)
## 当前结论
T1–T5 完成,D2 四个目标全部达到,没有走让步。没有新 BUG 号。没有推 `staging`,没有推 `main`。
- 工作树:`/workspace/Jyotisha/.worktrees/rectification-code-split-20260926`
- 分支:`codex/rectification-code-split-20260926`
- 开工基线:`origin/staging` = `12cbe6f8`(fewer-probes-card 与 telemetry 均已合入并部署)
- 提交(各自可单独回滚,每个都跑过全量):
| 阶段 | SHA | 内容 |
| --- | --- | --- |
| T1 聊天组件 | `3a66c39f` | 组件拆成 hook / 参数函数 / 块;切片测试改调用;增长合同(聊天部分) |
| T2 路由 | `eb773f12` | `POST` 拆成分支处理函数;切片测试改调用;增长合同(路由部分) |
| T3 agent-run | `a00c40d4` | `runV9AgentTurn` 拆成准备 / 单次尝试 / 重跑 / 收尾;增长合同(agent-run 部分) |
| T5 记录 | 本提交 | PROGRESS、CHANGELOG、任务索引一行 |
## 开工实测(`12cbe6f8`,与任务书 3ce11ee0 的数一致)
| 文件 | 行数 | 函数体 | hook |
| --- | --- | --- | --- |
| `rectification-agentic-chat.tsx` | 2043 | 组件 1625 行 | useState 35、useRef 16、useEffect 7、useLayoutEffect 2、useCallback 12 |
| `app/api/rectification/agent/route.ts` | 1077 | `POST` 927 行(旁边另有 3 个顶层辅助函数) | — |
| `v9/agent-run.ts` | 1400 | `runV9AgentTurn` 1046 行(内嵌 4 个函数) | — |
行数一律按 `\n` 计数(同 `wc -l`、同增长合同)。
## 前后对比与 D2
| 项 | 前 | 后 | D2 目标 | 结果 |
| --- | --- | --- | --- | --- |
| 聊天组件文件 | 2043 | 737 | — | — |
| 聊天组件本体 | 1625 | 630 | ≤ 900 | 达到 |
| 组件 useState | 35 | 12 | ≤ 20 | 达到 |
| 组件 useRef | 16 | 9 | — | 上限锁 9 |
| 组件 effect(useEffect + useLayoutEffect) | 9 | 5 | — | 上限锁 5 |
| 组件 useCallback | 12 | 6 | — | 上限锁 6 |
| route.ts 文件 | 1077 | 152 | — | — |
| `POST` | 927 | 114 | ≤ 250 | 达到 |
| agent-run.ts 文件 | 1400 | 135 | — | — |
| `runV9AgentTurn` | 1046 | 25 | ≤ 300 | 达到 |
## 拆到哪里
### T1 聊天组件(`frontend/src/components/rectification-agentic-chat.tsx`)
| 新文件 | 职责 | React hook |
| --- | --- | --- |
| `hooks/use-rectification-case-snapshot.ts` | 快照同步:17 个快照状态 + 缺题重试 / 修复计数、`applyCaseSnapshot`、`loadCaseSnapshot`、在途快照读取 | 有 |
| `hooks/use-rectification-board-layout.ts` | 盘面窄屏 / 抽屉 / 变化高亮 | 有 |
| `hooks/use-rectification-live-activity.ts` | 活动行计时标签与 4 秒刷新 | 有 |
| `lib/rectification-chat-turn-run.ts` | 原 `send` 函数体:POST、NDJSON 流读取、结算 / 失败 / 停止、回合后重读快照 | 无 |
| `lib/rectification-chat-choice-run.ts` | 原 `submitStructuredChoice` 函数体 | 无 |
| `lib/rectification-chat-accept-run.ts` | 原 `acceptCandidate` 函数体 | 无 |
| `lib/rectification-chat-message-actions.ts` | 复制、重新生成 | 无 |
| `lib/rectification-chat-question-repair.ts` | 缺题重读、「接着问」修复 | 无 |
| `lib/rectification-chat-messages.ts` | 持久 turn → 渲染消息、本地答题标记与撤回、回执视图 | 无 |
| `lib/rectification-chat-snapshot.ts` | 快照载荷的纯读取(当前题、题目来源、揭幕初值) | 无 |
| `lib/rectification-chat-view.ts` | 每次渲染的派生(重新生成按钮归属、点选卡、交付卡、只读范围、缺题态、题目挂载位置、时间轴) | 无 |
| `components/rectification-question-gap-notices.tsx` | 缺题块(独立题卡、准备中、已交付、快照失败、接着问、核对收尾)与只读范围行 | 无 |
做法:函数体整段原样搬移;参数函数第一行把 `deps` 解构成原来的变量名,所以函数体逐字不变(只少一层缩进)。组件里只留 `useCallback` 包一层、依赖数组**逐字保留原样**(包括原来就漏掉的 `busy` / `conversationAnchor`,行为上等于原来的闭包时机)。渲染期 `setSelectionOfferLock` 仍在组件里、用派生函数返回的 `nextOfferLock`,注释随之移到调用处。
### T2 路由(`frontend/src/app/api/rectification/agent/route.ts`)
| 新文件(`lib/rectification-agentic/v9/`) | 职责 |
| --- | --- |
| `agent-route-request.ts` | 请求闸:登录、产品开关、请求体 schema、提示词防提取、运行开关、Case↔Session 绑定、终态 |
| `agent-route-typed-message.ts` | 打字回答:报时段锁定回复、按当前 focus 的分类快路径、无 focus 时的写入分类 |
| `agent-route-structured-choice.ts` | 选择题(answer_choice / stop_and_review / skip_probe) |
| `agent-route-agent-turn.ts` | 开场 / 只读 / 打字进 agent 的 NDJSON 流、延迟回放、交付出口闸、错误码映射 |
| `agent-route-billing.ts` | 计费适配器与 Case 级请求号 |
| `agent-route-support.ts` | schema、一次性 NDJSON 回复、动作映射、上下文类型 |
`route.ts` 只建 Supabase 客户端(安装失败映射留在这里,`api-service-unavailable` 合同照旧)并按原顺序装配各分支。原来跨分支的四个可变 `let`(`expectedWrite`、`collectIntent`、`writeClassified`、`classifierDiagnostic`)收成一个 `turnState` 对象,赋值与读取时机不变。
### T3 agent-run(`frontend/src/lib/rectification-agentic/v9/agent-run.ts`)
| 新文件 | 职责 |
| --- | --- |
| `agent-run-prepare.ts` | 准备:日期可靠度、会话校验、年份重问、Skill 身份、已交付守卫、开场线索、预扣、turn 行与幂等回放 |
| `agent-run-retry.ts` | 重跑:attempt 循环 |
| `agent-run-attempt.ts` | 单次尝试:原内嵌 `streamAttempt` |
| `agent-run-finish.ts` | 收尾:结算 / 释放、终态回执、下一问、口述裁剪、finalize |
| `agent-run-support.ts` | outcome 类型、重试分类、RPC 解包、turn 级回执写入(原内嵌 `failedAttempt` / `persistCommittedPhase` / `finalizeTurn`,绑定同一 turn 后调用方式不变) |
| `agent-run-messages.ts` | `buildAgentMessages` / `buildOpeningBrief`(agent-run.ts 再导出,外部 import 不变) |
**唯一改动的 token**:单次尝试把自己的参数 `previousErrorCode` 传给 `buildAgentMessages`,原内嵌版读的是外层 `lastAttemptError`。循环调用时传的正是 `lastAttemptError`,且在该次尝试内它不会被改写,所以值相同;原来 `previousErrorCode` 未使用的 lint warning 随之消失。
## 测试对比(Node 22.14.0,本机无 Docker)
开工基线:未改动的 `origin/staging` 独立工作树跑全量 `npm test`。
| 时点 | tests | pass | fail | skipped | 失败名单 vs 基线 | 测试名 vs 基线 |
| --- | --- | --- | --- | --- | --- | --- |
| 基线 `12cbe6f8` | 4037 | 3984 | 25 | 28 | — | 4019 条 |
| T1 后(合同测试加入前) | 4037 | 3984 | 25 | 28 | 逐条一致 | 无缺失、无新增 |
| T2 后 | 4043 | 3990 | 25 | 28 | 逐条一致 | 无缺失,新增 6 条(增长合同) |
| T3 后(最终) | 4045 | 3992 | 25 | 28 | 逐条一致 | 无缺失,新增 8 条(增长合同) |
25 条失败全是开工基线就有的无 Docker / 部署类用例(数据库、staging sync、Gitea 门禁等),名单三次比对逐条一致。比对命令:
```bash
grep -E '^not ok' <log> | sed 's/^not ok [0-9]* - //; s/ # .*//' | sort # 失败名单
grep -E "^(not )?ok [0-9]+ - " <log> | sed -E 's/^(not )?ok [0-9]+ - //; s/ # .*//' | sort # 测试名
```
- `tsc --noEmit`:0 错(含 tests)。
- `npm run lint`:0 error;warning 126 → 128。多出的 3 条都是 `react-hooks/exhaustive-deps`:`setPending` 依赖里没列 hook 返回的两个 setter、卸载 effect 没列 `snapshotAbort`、活动行计时 effect 没列 `setMessages`。它们来自自定义 hook 的返回值,lint 不知道是稳定引用;依赖数组按原样保留,没有为了消 warning 改依赖(CLAUDE.md「不要为迎合工具改业务代码」,且 `rectification-surface-contract` 锁着卸载 effect 的 `}, []);`)。少掉的 1 条是 agent-run 的未使用参数(见上)。另有 3 条已有 warning 的依赖名单变长(同一原因)。
- `npm run build -- --webpack`:成功;`/`、`/chart`、`/ephemeris`、`/people` 均 `○ Static`;`rootMainFiles` gzip 130933 → 130933(0%)。构建产生的 `frontend/frontend/` 已删除,未提交。
- Python 门禁集(`gate-pytest-args.txt`,949 条):基线工作树与本分支结果完全一致:937 passed / 11 failed / 1 skipped,失败名单逐条一致。11 条全是 `test_qizheng_*`,报「vendored 七政四余 engine is unavailable」——`vendor/stem-branch/dist/cli.cjs` 不在 `origin/staging` 的树里,工作树里也没有,属环境缺口(需要构建 vendored 包,本轮禁止装依赖)。给定的 948 / 1 基线应是在构建过该产物的检出上测的。本轮改动的三组文件没有任何 Python 测试读取(已 grep `tests/*.py` 与 `scripts/*.py`)。
## 切源码测试怎么改的
两类:
1. **整文件断言**(`match` / `doesNotMatch` 在整份源码上):改读拼接源码 `tests/rectification-chat-surface.ts`、`tests/rectification-agent-route-surface.ts`、`tests/rectification-agent-run-surface.ts`(同 `home-surface.ts` 的做法,容器文件排第一)。断言本身一字不改,读取处统一写了一行「原值 / 新值 / 原因」。`doesNotMatch` 的覆盖面从一个文件变成整组文件,只会更严。共 56 个测试文件、100 处读取改为读拼接源码。
2. **切片断言**(`indexOf` / `slice` / `sourceBetween` 两个锚点之间):拆分后锚点落进不同文件或切片变空的,逐条改写,每条就地写三栏。能调用的改成调用抽出的函数或真渲染抽出的块;只能按源码查的静态合同(「生产代码不含语义正则」等)按代码去处重组切片。
逐条改写清单:
| 测试 | 原值 | 新值 | 类型 |
| --- | --- | --- | --- |
| `rectification-agentic-entry` · receipts / live tool progress | 切 `completedReceiptFromPersisted` 源码,匹配 tools / methods / 两个 filter、无 phases | 直接调用:旧形状与 `tool_activities` 形状各一例,只留公开工具(去重)与公开方法,phases 不进步骤 | 调用 |
| `rectification-agentic-entry` · aborting a live run | 切 `submitStructuredChoice` / `acceptCandidate`,匹配 `runAbort.current = abortController` 与 `signal` | 调用两个参数函数(假 fetch):请求的 signal 就是 `runAbort` 里的控制器,结束后清空;URL 各自正确 | 调用 |
| `chat-composer-queue` · rectification abort settles as stopped | 同上 | 同上 | 调用 |
| `rectification-agentic-entry` · time-selection cards | `messageLoop` 含 persisted 题卡 token;切 `showSelectionCards` 段匹配 `!busy \|\| keep…`、无 `offeredSelectionOnce` | 容器 map 段挂 `<RectificationQuestionGapNotices` 并传 `choiceNonce`;真渲染:persisted 选择题出 `is-embedded` 卡、4 个选项;派生函数:busy 收卡、采用中留卡、无「出过一次」记忆 | 渲染 + 调用 |
| `rectification-surface-contract` · question gap live row | `{questionGap === "preparing"` 在 map 之后、saved 行之前 | 容器里缺题块组件在同两个锚点之间;真渲染 preparing 态出 live 行与准备文案 | 渲染 |
| `rectification-spoken-collect` · choice options in the same message | 容器 map 段含 persisted 题卡与 `onSelect={submitChoice}` / `onStop={submitStop}` | 容器传 `submitChoice` / `submitStop` 给缺题块;缺题块里的题卡绑同样两个回调 | 源码跟随 |
| `rectification-exhaustion-exit` · collect spoken stop | 切 `RectificationReadonlyRange` 源码:有 `role="status"`、无 onStop / button | 真渲染:整段输出是一行 `role="status"` 的 `<p>`,无按钮、无「先这样」 | 渲染 |
| `rectification-adopt-cards-stay` · 更像这个 不卸卡 | 切 `keepSelectionCardsWhileBusy` 段三条 token | 派生函数四种状态:空闲出卡 / busy 收卡 / 采用中留卡 / 已采用带持久 offer 留卡 | 调用 |
| `rectification-probe-replay-loss` · P3 selection cards | 切 `showSelectionCards` 段匹配 `!busy \|\| keep` 与 `caseSnapshotLoaded` | 派生函数:快照未载入不出卡、载入出卡、busy 收卡、采用中留卡 | 调用 |
| `rectification-tiebreak-card-loss` · no tie-break entry | 切 `showSelectionCards` 段不含 `interviewQuestionBlocksAdoptOffer` | 派生函数:最新回复挂着未答采集题时交付卡照出,只把采用锁上 | 调用 |
| `rectification-timeline-20260909` · timeline reads parsed inferenceMarks | 切 `timelineView` 段:用 `candidateResult?.inferenceMarks`、不碰 receipt | 派生函数:时间轴与直接用 inferenceMarks 建轴完全相同、与只用候选分钟不同;只改 receipt 里的 inference_state,时间轴不变 | 调用 |
| `rectification-post-adopt-verify` · verified_idle closing line | 从第一处 `verified_idle` 起取 400 字,无缺题文案 / 重载标签 | 真渲染缺题块:整段就是一行 `role="status"` 收尾句 | 渲染 |
| `rectification-activity-receipt` · answer deltas keep running activity | 切 delta 分支与 frames 定义,匹配状态推导、replace 语义、「正在组织回答」 | 调用 `runRectificationChatTurn`(假 NDJSON 流):只有工具时 flush 出 thinking 空行;两段 delta(第二段 replace)后只提交一次 streaming 行,正文为 replace 后全文,活动标签「正在组织回答…」未被清掉 | 调用 |
| `application-billing-contract` · free turns bypass settlement | 切 reserve / complete / release 三段(release 以 `};\n\n try {` 收尾) | 子进程里调用 `createRectificationRunBilling`(feature-pricing 引 server-only):非 message 动作三步直接成功、不碰账务库;message 三步都先查 Case 级预留 | 调用 |
| `rectification-window-cluster-cap` / `rectification-declared-window` · declared window before the model | 源码顺序:报时段拦截早于 `const selectedModel`(及分类调用) | route.ts 先 `await replyToDeclaredBirthWindow(context)` 并 return,再解析模型、再进打字预检;直接调用:报时段落一条确定性 turn、流里给锁定回复 + run.completed,普通消息返回 null、不碰持久层 | 调用 + 装配顺序 |
| `rectification-answer-choice` · structured choice is non-model | 切 `if (isStructuredChoice)` 到 `const resolvedModel` | route.ts 在该处把选择题交给 `handleRectificationStructuredChoice` 并 return;整个处理模块含 `applyRectificationChoice(accounting`、不含 agent / 计费 | 源码跟随(范围变大) |
| `rectification-unwritten-evidence` · classifier expectedWrite reaches the runner | `expectedWrite,` 与 `let expectedWrite … = "none"` | 类型、初值 `expectedWrite: "none"`、预检赋值、传给 `runV9AgentTurn` 的 `expectedWrite: turnState.expectedWrite` 四处 | 源码跟随 |
| `rectification-turn-intent-classifier`、`rectification-collect-stall`(2 条)、`rectification-discriminator-followup-consistency`、`rectification-occupation-coverage-exit`、`rectification-spoken-collect` · typed fast path | 切 route.ts `if (action === "message")` 到 `const requestTime` | `rectificationTypedMessageFastPath()`:打字处理模块全文 + route.ts 该锚点起 + agent 轮模块到 `const requestTime` 止,同一批代码;其上的子切片顺序断言不变 | 静态合同重组 |
| `rectification-range-offer-deadend` · dual-exit constant gone | 三个源之一是 route.ts | 该源换成路由拼接源码 | 源码跟随 |
| `product-domain-registry` · product gates | route.ts 含 `isProductEnabled("rectification")` | 该闸在 `agent-route-request.ts`,另加 route.ts 必须先 `await resolveRectificationAgentRequest({` | 源码跟随 |
没有删除任何测试;没有删除任何断言而不补等价或更强的断言。被拼接源码替代的 `const route = …` 在两三个测试里改写后已无人使用,按原样删掉了这行(连同它的 `readFileSync` import),没有留空转。
改写后做过变异检查:把流 flush 的状态推导改成恒 thinking、把 `!busy || keepSelectionCardsWhileBusy` 改成 `!busy`、把 release 的免费早退删掉,对应的新断言都转红,改回后转绿。
### 仍按源码切片、没有改写的(两个锚点仍在同一个新文件里,切出的是同一段代码)
- `application-billing-contract` 第 2 条:reserve 段(在 `agent-route-billing.ts`)与错误码映射段(在 `agent-route-agent-turn.ts`)。
- `rectification-spoken-collect` agent 轮 `beforeRun` / `afterRun` / `alreadyDelivered` 段、`rectification-collect-stall` 的 `afterRun` 段(都在 `agent-route-agent-turn.ts`,单锚点切到末尾时多出的是计费模块,只会让 `doesNotMatch` 更严)。
- 若干 `className="composer-wrap"` 起的切片:结束锚点原本就不存在(切到文件尾),现在切到拼接尾,只含 `doesNotMatch`,更严。
- agent-run 的顺序切片(`rectification-delivery-ui-simplify`、`rectification-year-focus-overlay`、`rectification-probe-replay-loss` 的 settlement 段、`rectification-skipped-health-deadend`、`rectification-answer-choice` 的 `RETRYABLE_ERROR_CODES` 段):锚点都在同一个 agent-run 阶段文件里。
这些切片是用脚本逐条比对「拆分前单文件切出的文本」与「拆分后拼接源码切出的文本」筛出来的:内容相同(只差缩进)或只多出不相关文件的,留着;其余都改写了。
## 增长合同
新增 `frontend/tests/rectification-growth-contract.test.ts`(8 条),与首页 `home-shell-growth-contract.test.ts` 同型:
- 聊天组件:文件 ≤ 737 + 150;组件本体 ≤ 900;useState ≤ 12、useRef ≤ 9、effect ≤ 5、useCallback ≤ 6;8 个 `lib/rectification-chat-*.ts` 参数函数模块不得调用任何 React hook。
- 路由:文件 ≤ 152 + 150;`POST` ≤ 250;route.ts 只声明 `POST` 一个函数;`POST` 必须委托给 5 个分支处理函数。
- agent-run:文件 ≤ 135 + 150;`runV9AgentTurn` ≤ 300、不得再有内嵌函数、准备 → 重跑 → 收尾顺序不变。
## 其他
- `frontend/DESIGN.md` 一处文件指向更正:时间轴与盘面标题行共用的 `workingRectificationTime(result)` 现在由 `rectification-chat-view.ts` 的 `deriveRectificationChatView` 调用(仍是容器每次渲染一次),只改说明位置,不改 UI。
- 本轮没有浏览器真机走查;行为靠全量测试与原样搬移保证,两条组件级渲染测试(`rectification-dup-question-20260926.test.tsx`、`rectification-latency-20260926.test.tsx`)和三条路由级子进程测试照常通过。
- 没有推 `origin/staging`,没有推 `main`,没有改 workflow,没有装或升级依赖。
## 验收(Claude,2026-09-26)
在同机新建 `origin/staging` `12cbe6f8` 基线工作树,先后跑基线与本分支(Node 22.14):
| 项 | 基线 | 本分支 |
| --- | --- | --- |
| `npm test` | 4037 / 25 fail / 28 skip | 4045 / 25 / 28;失败名单逐条一致,无消失的测试名,新增 8 条增长合同 |
| tsc / lint | — | 0 错 / 0 error(warning 126→128,为保持原依赖数组不改业务代码,认可) |
| Python 门禁集 | 937 passed / 11 failed / 1 skipped | 同左,失败名单一致(均为 `test_qizheng_*`,本机缺 `vendor/stem-branch/dist/cli.cjs` 的环境缺口,门禁里会构建) |
| build | — | `/`、`/chart`、`/ephemeris`、`/people` ○ Static;rootMainFiles gzip 130933 B(±0%) |
代码抽查:聊天组件 effect 顺序唯一变化是 `currentQuestionRef` 的 layout effect 移到滚动锚 hook 之前,两者无依赖;三个抽出 hook 的 effect 依赖数组与原文一致。D2 四项目标全部达成。