Move-only (TASK-rectification-code-split-20260926 T3/T4). No behavior, receipt, billing, Skill or scoring change. - agent-run.ts: 1400 -> 135 lines; runV9AgentTurn 1046 -> 25 lines, no nested functions. It keeps the public types, the budget constants and the phase order: agent-run-prepare.ts (reliability, session, year re-ask, Skill identity, delivered guard, reserve, turn row / replay), agent-run-retry.ts (attempt loop), agent-run-attempt.ts (the old nested streamAttempt), agent-run-finish.ts (billing settle, receipts, interview, finalize), agent-run-support.ts (outcome types, retry classification, turn receipt writers), agent-run-messages.ts (buildAgentMessages / buildOpeningBrief, re-exported). - One token changed with the move: the attempt passes its own `previousErrorCode` argument to buildAgentMessages instead of reading the enclosing `lastAttemptError`; the loop passes that same value (clears the old unused-parameter warning). - Tests: whole-source contracts read tests/rectification-agent-run-surface.ts; behavioral runV9AgentTurn tests unchanged. agent-run growth caps added. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017eEAG8HD3mm8gsKXgk8uU8
163 lines
8.2 KiB
TypeScript
163 lines
8.2 KiB
TypeScript
import assert from "node:assert/strict";
|
|
import { readFileSync } from "node:fs";
|
|
import test from "node:test";
|
|
|
|
/**
|
|
* Growth caps for the three rectification files split by
|
|
* TASK-rectification-code-split-20260926, same shape as
|
|
* home-shell-growth-contract.test.ts: a coarse line guardrail (measured
|
|
* baseline + 150) plus hook-count caps that stop state from drifting back
|
|
* into the container. Line counts are `\n` counts, same as `wc -l`.
|
|
* Hook counts use `\buseX[<(]` so `useState<Type>(` is not missed.
|
|
*/
|
|
|
|
const read = (relativePath: string) => readFileSync(new URL(relativePath, import.meta.url), "utf8");
|
|
|
|
function lineCount(source: string): number {
|
|
return (source.match(/\n/g) ?? []).length;
|
|
}
|
|
|
|
function countMatches(source: string, pattern: RegExp): number {
|
|
return source.match(new RegExp(pattern.source, "g"))?.length ?? 0;
|
|
}
|
|
|
|
function sourceFrom(source: string, marker: string): string {
|
|
const start = source.indexOf(marker);
|
|
assert.notEqual(start, -1, `missing marker: ${marker}`);
|
|
return source.slice(start);
|
|
}
|
|
|
|
const REACT_HOOK_CALL = /\buse(?:State|Ref|Effect|LayoutEffect|InsertionEffect|Callback|Memo|Reducer|Context|Id|Transition|DeferredValue|SyncExternalStore|Optimistic|ActionState)\s*[<(]/;
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// T1 · rectification-agentic-chat.tsx
|
|
// Measured 2026-09-26 on codex/rectification-code-split-20260926 after the
|
|
// component split. Before (origin/staging 12cbe6f8): file 2043 lines, component
|
|
// body 1625 lines, useState 35, useRef 16, useEffect 7, useLayoutEffect 2,
|
|
// useCallback 12. After: file 737, body 630, useState 12, useRef 9,
|
|
// useEffect 5, useLayoutEffect 0, useCallback 6.
|
|
const CHAT_PATH = "../src/components/rectification-agentic-chat.tsx";
|
|
const CHAT_LINE_BASELINE = 737;
|
|
const CHAT_LINE_CAP = CHAT_LINE_BASELINE + 150;
|
|
/** D2 of the task: the component body stays at or under 900 lines. */
|
|
const CHAT_BODY_LINE_CAP = 900;
|
|
const CHAT_USE_STATE_CAP = 12;
|
|
const CHAT_USE_REF_CAP = 9;
|
|
const CHAT_EFFECT_CAP = 5;
|
|
const CHAT_USE_CALLBACK_CAP = 6;
|
|
const CHAT_COMPONENT_MARKER = "export function RectificationAgenticChat(";
|
|
|
|
/** Parameter functions split out of the chat; they take state and setters, never call hooks. */
|
|
const CHAT_PARAMETER_MODULES = [
|
|
"../src/lib/rectification-chat-messages.ts",
|
|
"../src/lib/rectification-chat-snapshot.ts",
|
|
"../src/lib/rectification-chat-turn-run.ts",
|
|
"../src/lib/rectification-chat-choice-run.ts",
|
|
"../src/lib/rectification-chat-accept-run.ts",
|
|
"../src/lib/rectification-chat-message-actions.ts",
|
|
"../src/lib/rectification-chat-question-repair.ts",
|
|
"../src/lib/rectification-chat-view.ts",
|
|
] as const;
|
|
|
|
test("rectification-agentic-chat.tsx line count stays within the coarse guardrail", () => {
|
|
const n = lineCount(read(CHAT_PATH));
|
|
assert.ok(n <= CHAT_LINE_CAP, `rectification-agentic-chat.tsx has ${n} lines; cap is ${CHAT_LINE_CAP} (${CHAT_LINE_BASELINE} baseline + 150).`);
|
|
});
|
|
|
|
test("RectificationAgenticChat body stays at or under 900 lines", () => {
|
|
const n = lineCount(sourceFrom(read(CHAT_PATH), CHAT_COMPONENT_MARKER));
|
|
assert.ok(n <= CHAT_BODY_LINE_CAP, `RectificationAgenticChat body has ${n} lines; cap is ${CHAT_BODY_LINE_CAP}.`);
|
|
});
|
|
|
|
test("RectificationAgenticChat hook counts must not grow", () => {
|
|
const body = sourceFrom(read(CHAT_PATH), CHAT_COMPONENT_MARKER);
|
|
const useState = countMatches(body, /\buseState[<(]/);
|
|
const useRef = countMatches(body, /\buseRef[<(]/);
|
|
const effects = countMatches(body, /\buse(?:Layout)?Effect[<(]/);
|
|
const useCallback = countMatches(body, /\buseCallback[<(]/);
|
|
assert.ok(useState <= CHAT_USE_STATE_CAP, `useState ${useState}; cap ${CHAT_USE_STATE_CAP}. New state belongs in a hook such as use-rectification-case-snapshot.ts.`);
|
|
assert.ok(useRef <= CHAT_USE_REF_CAP, `useRef ${useRef}; cap ${CHAT_USE_REF_CAP}. Do not rewrite state as refs to dodge the useState cap.`);
|
|
assert.ok(effects <= CHAT_EFFECT_CAP, `useEffect + useLayoutEffect ${effects}; cap ${CHAT_EFFECT_CAP}.`);
|
|
assert.ok(useCallback <= CHAT_USE_CALLBACK_CAP, `useCallback ${useCallback}; cap ${CHAT_USE_CALLBACK_CAP}. Turn bodies belong in lib/rectification-chat-*-run.ts.`);
|
|
});
|
|
|
|
test("rectification chat parameter functions call no React hooks", () => {
|
|
for (const path of CHAT_PARAMETER_MODULES) {
|
|
assert.doesNotMatch(read(path), REACT_HOOK_CALL, `${path} must stay a parameter function module (no React hooks).`);
|
|
}
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// T2 · app/api/rectification/agent/route.ts
|
|
// Measured 2026-09-26 after the route split. Before (origin/staging 12cbe6f8):
|
|
// file 1077 lines, POST 927 lines, three top-level helper functions beside
|
|
// POST. After: file 152, POST 114, no helpers beside POST (they live in
|
|
// lib/rectification-agentic/v9/agent-route-*.ts).
|
|
const ROUTE_PATH = "../src/app/api/rectification/agent/route.ts";
|
|
const ROUTE_LINE_BASELINE = 152;
|
|
const ROUTE_LINE_CAP = ROUTE_LINE_BASELINE + 150;
|
|
/** D2 of the task: POST stays at or under 250 lines. */
|
|
const ROUTE_POST_LINE_CAP = 250;
|
|
const ROUTE_POST_MARKER = "export async function POST(";
|
|
|
|
test("rectification agent route.ts line count stays within the coarse guardrail", () => {
|
|
const n = lineCount(read(ROUTE_PATH));
|
|
assert.ok(n <= ROUTE_LINE_CAP, `route.ts has ${n} lines; cap is ${ROUTE_LINE_CAP} (${ROUTE_LINE_BASELINE} baseline + 150).`);
|
|
});
|
|
|
|
test("rectification agent POST stays an assembly of branch handlers", () => {
|
|
const source = read(ROUTE_PATH);
|
|
const post = sourceFrom(source, ROUTE_POST_MARKER);
|
|
const n = lineCount(post);
|
|
assert.ok(n <= ROUTE_POST_LINE_CAP, `POST has ${n} lines; cap is ${ROUTE_POST_LINE_CAP}. New branches go in lib/rectification-agentic/v9/agent-route-*.ts.`);
|
|
const functionDeclarations = source.match(/^(?:export )?(?:async )?function\s/gm)?.length ?? 0;
|
|
assert.equal(functionDeclarations, 1, "route.ts declares only POST; helpers belong in agent-route-*.ts.");
|
|
for (const handler of [
|
|
"resolveRectificationAgentRequest",
|
|
"replyToDeclaredBirthWindow",
|
|
"handleRectificationStructuredChoice",
|
|
"handleRectificationTypedMessage",
|
|
"respondWithRectificationAgentTurn",
|
|
]) {
|
|
assert.match(post, new RegExp(`\\b${handler}\\(`), `POST must delegate to ${handler}`);
|
|
}
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// T3 · lib/rectification-agentic/v9/agent-run.ts
|
|
// Measured 2026-09-26 after the runner split. Before (origin/staging 12cbe6f8):
|
|
// file 1400 lines, runV9AgentTurn 1046 lines with four nested functions
|
|
// (streamAttempt, failedAttempt, persistCommittedPhase, finalizeTurn). After:
|
|
// file 135, runV9AgentTurn 25 lines, no nested functions; the phases live in
|
|
// agent-run-{prepare,retry,attempt,finish,support,messages}.ts.
|
|
const RUN_PATH = "../src/lib/rectification-agentic/v9/agent-run.ts";
|
|
const RUN_LINE_BASELINE = 135;
|
|
const RUN_LINE_CAP = RUN_LINE_BASELINE + 150;
|
|
/** D2 of the task: runV9AgentTurn stays at or under 300 lines. */
|
|
const RUN_FUNCTION_LINE_CAP = 300;
|
|
const RUN_FUNCTION_MARKER = "export async function runV9AgentTurn(";
|
|
|
|
function functionBody(source: string, marker: string): string {
|
|
const start = source.indexOf(marker);
|
|
assert.notEqual(start, -1, `missing marker: ${marker}`);
|
|
const end = source.indexOf("\n}\n", start);
|
|
assert.notEqual(end, -1, `unterminated function: ${marker}`);
|
|
return source.slice(start, end + 2);
|
|
}
|
|
|
|
test("agent-run.ts line count stays within the coarse guardrail", () => {
|
|
const n = lineCount(read(RUN_PATH));
|
|
assert.ok(n <= RUN_LINE_CAP, `agent-run.ts has ${n} lines; cap is ${RUN_LINE_CAP} (${RUN_LINE_BASELINE} baseline + 150).`);
|
|
});
|
|
|
|
test("runV9AgentTurn stays the order of its phases", () => {
|
|
const body = functionBody(read(RUN_PATH), RUN_FUNCTION_MARKER);
|
|
const n = lineCount(body);
|
|
assert.ok(n <= RUN_FUNCTION_LINE_CAP, `runV9AgentTurn has ${n} lines; cap is ${RUN_FUNCTION_LINE_CAP}. Phase logic belongs in agent-run-*.ts.`);
|
|
assert.doesNotMatch(body, /^\s+(?:async )?function\s/m, "runV9AgentTurn must not grow nested functions again.");
|
|
const prepare = body.indexOf("prepareV9AgentTurn(");
|
|
const attempts = body.indexOf("runV9AttemptsWithRetry(");
|
|
const finish = body.indexOf("finishV9AgentTurn(");
|
|
assert.ok(prepare >= 0 && attempts > prepare && finish > attempts, "prepare → attempts → finish");
|
|
});
|