Files
Jyotisha/docs/tasks/PROGRESS-rectification-code-split-20260926.md
T

19 KiB
Raw Blame History

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;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,没有装或升级依赖。