Files
Jyotisha/frontend/tests/rectification-growth-contract.test.ts
T
Jesse_ChenandClaude Opus 5.5 3a66c39f48 refactor(rectification): split the chat component into hooks, parameter functions and blocks
Move-only (TASK-rectification-code-split-20260926 T1/T4). No behavior, API,
copy, DB, Skill or scoring change.

- rectification-agentic-chat.tsx: 2043 -> 737 lines; component body
  1625 -> 630; useState 35 -> 12, useRef 16 -> 9, effects 9 -> 5,
  useCallback 12 -> 6.
- Snapshot sync -> hooks/use-rectification-case-snapshot.ts; board layout
  and live label clock -> two small hooks.
- send / submitStructuredChoice / acceptCandidate bodies ->
  lib/rectification-chat-{turn,choice,accept}-run.ts parameter functions
  (bodies verbatim, deps destructured to the same names); copy/regenerate,
  question repair, pure transcript and snapshot helpers and the per-render
  view derivation -> lib/rectification-chat-*.ts (no React hooks).
- Question-gap blocks and the read-only range line ->
  components/rectification-question-gap-notices.tsx.
- Dependency arrays kept exactly as before (lint warnings +3, listed in
  PROGRESS); no dep added to appease the linter.
- Tests: whole-source contracts read tests/rectification-chat-surface.ts
  (container + split files, like home-surface.ts); slices of moved code now
  call the extracted functions or render the extracted block, each with a
  原值/新值/原因 comment. New tests/rectification-growth-contract.test.ts.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017eEAG8HD3mm8gsKXgk8uU8
2026-09-26 19:23:52 +08:00

89 lines
4.4 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).`);
}
});