Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017eEAG8HD3mm8gsKXgk8uU8
20 KiB
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 门禁等),名单三次比对逐条一致。比对命令:
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;rootMainFilesgzip 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 测试读取(已 greptests/*.py与scripts/*.py)。
切源码测试怎么改的
两类:
- 整文件断言(
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 处读取改为读拼接源码。 - 切片断言(
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-collectagent 轮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 四项目标全部达成。