PPI 리팩터링 우선순위 · use-ai-session 분리 설계 · 코드 성장 원인 분석 P0설계

마지막 업데이트 2026-09-18

작성일: 2026-09-18 측정 기준: develop e34637e5, 최근 한 달 = 2026-08-18 ~ 09-18 대상: 개발자 — 무엇을 먼저 쪼개고, 왜 계속 커지는지

한 줄 판정

리팩터링 1순위는 게스트 세션 갓훅 2개(use-ai-session 5,514줄 · use-guest-page-session 3,861줄)이고, 그 전에 테스트 CI 게이트와 V1 사체 삭제를 끝내야 한다. 코드가 계속 커지는 이유는 "에이전트가 분리를 안 해서"가 아니라 이미 큰 파일을 건드릴 때 추출 대신 추가를 선택하게 만드는 지침 공백이다. 파일 크기 기준(300/500/1,000 래칫)과 리뷰 게이트 한 줄이 빠져 있다.

이 문서의 표기사실 은 코드·git·gh에서 직접 측정한 값, 추정 은 그 값에서 끌어낸 판단이다. 구분이 없는 숫자는 전부 사실이다.

이전 감사 전체 코드 감사 (2026-07-02)가 V1 제거·보안·잠재버그를 다뤘고, 이 문서는 그 뒤 두 달 반 동안의 구조 부채와 성장 메커니즘만 본다.

1. 리팩터링 우선순위 — 코드 구조

순위대상측정 근거 (사실)조치 (추정)
P0-1apps/web/entities/guest-session/model/use-ai-session.ts5,514줄 · useRef 110 · useEffect 32 · useCallback 56 · 단위테스트 0 · 6개월 변경 93회세션 라이프사이클 / 듣기 의도 / 응답 게이트 / 전사 / 자동 진행 / 오디오로 분할 (§4 설계)
P0-2apps/web/entities/guest-page-session/model/use-guest-page-session.ts3,861줄 · useEffect 41 · useState 31 · return 객체 143줄 · 변경 105회오케스트레이터가 갓훅화. use-step-sync·use-step-transition처럼 이미 분리된 패턴을 나머지에도 적용
P0-3apps/web/app/monitor-dashboard/[group]/[roomId]/page.tsx2,332줄 · import 50 · useEffect 20 · 인라인 핸들러 15 · 변경 106회 (전체 1위)페이지는 조립만. 로직은 widgets/monitor-dashboard/*
P1-1테스트 CI 게이트 부재테스트 파일 391개 존재. 워크플로우에 vitest/tsc 없음. apps/web/package.jsontest:* 스크립트 12개 산재P0 리팩터링의 안전망. 통합 test·check-types + PR 게이트 먼저
P1-2apps/web/lib/db-queries.ts5,488줄 · export 132 · API 라우트 86개 의존 · 한 달 +1,379도메인별 DAO 분리. lib/api/dao/voice-pipeline.dao.ts 패턴 존재
P1-3V1 죽은 코드미참조 훅 6개: use-session-manager(1,371) use-screen-share(313) use-ice-restart use-camera-stream use-network-recovery use-auto-start-on-guest-ready + 체인 끝 use-web-rtc(969)삭제만으로 약 3,000줄 감소. 위험 최저
P1-4apps/socket/src/sfu-socket/handlers/avatar-handlers.ts2,647줄에 socket.on 11개 (이벤트당 약 240줄) · connection-handlers.ts 1,943줄이벤트 핸들러 → 서비스 함수 추출. §6의 VP 통합 커밋이 붙인 7.5k가 여기
P2-1today-class-list.tsx / all-class-list.tsx2,158 + 1,177줄 · 동일 라인 289개 · 공유 핸들러 26개공통 리스트 + 날짜 범위 prop
P2-2any 사용138건 · use-monitor-session.ts 21건 집중 (소켓 이벤트 payload)소켓 이벤트 타입을 packages/shared
P2-3디렉터리 이중 구조FSD(entities/features/widgets) vs 레거시 components/sections 105개 · hooks/ 99개신규는 FSD만, 레거시는 터치할 때 이동
P3apps/web/lib/tts/typecast-voices.ts13,416줄 순수 데이터JSON 분리
실행 순서 제안 — P1-3(V1 삭제)과 P1-1(CI 게이트)은 동작 변경 없이 바로 가능하므로 사실상 첫 작업이다. 그 다음 P0-1을 §4 체크포인트대로 진행한다.

2. 리팩터링 우선순위 — 컴포넌트·레이아웃·스타일

순위대상측정 근거 (사실)조치 (추정)
P0미참조 컴포넌트 10개 삭제어디서도 import 안 됨: performance-monitor(537) manual-voucher-modal(480) host-video(367) lesson-date-time-picker(234) guest-entry-popup(196) card-end-menu(186) webrtc-status-indicator(156) monitor-settings-modal(154) issue-input-panel(142) interrupt-mode-button(134)약 2,600줄 + 대응 CSS 1,500줄 제거. 동작 변경 0
P0모니터 룸 페이지 분해§1 P0-3과 동일동일
P1모달 31개 → useOverlay 일원화modal/dialog 이름 31개 중 useOverlay 사용 7개, if (!isOpen) return null 자체 구현 12개, createPortal 직접 7개. CSS .cancelButton 15회·.confirmButton 14회·.closeButton 10회 중복 선언CLAUDE.md 규칙 위반 상태. 공통 Confirm/Form 모달 셸 + 버튼 스타일 하나로
P1alert()/confirm() 직접 호출156회 (37 / 26 파일)위 모달 통합과 같은 작업
P1테이블 34개 인라인 구현<table 직접 작성 34파일 · CSS .td 14회·.th 11회 중복shared/ui/data-table. debug 페이지 3개(redis 880·rooms 603·peers 427, useState 9~10·fetch·table 구조 동일)가 첫 대상
P1대형 폼 4개avatar-form 1,330(useState 28) · lesson-template-form 1,033 · activity-form 905(useState 25) · lesson-form 855. 공통 식별자 5개뿐통합 아니라 각 폼을 섹션으로 분할. useState 25~28은 useReducer 대상
P2features/session/ui/session-card.tsx1,237줄 · import 48 · useCallback 13 · 변경 90회상태·액션을 features/session/model
P2widgets/guest/guest-layout/ui/guest-layout.tsx662줄 (앱 레이아웃 4개 합계 135줄의 5배)디버그 패널·라우팅 상태는 stories 있는 하위 위젯으로
P2shared/ui/meet-video.tsx / friend-choice-screen.tsx1,154줄(useEffect 18, 참조 6곳) / 840줄미디어 element 관리 훅과 UI 분리
P2CSS 모듈 6,897줄chat 781 · assignment-editor 740 · avatar-form 647 · .container 15회 중복버튼·테이블·모달 셸 토큰화 후 자연 감소
P3Storybook 커버리지stories 33개 · components/sections 76개 중 12개위 통합 시 규칙대로 동반 추가

3. use-ai-session은 왜 5,514줄이 됐나

2026-01-15 생성 741줄 → 02월 853 → 04월 1,831 → 06월 2,618 → 07/13 3,222
→ 07/15 4,007 (+785, LiveKit self-hosted 도입) → 08월 4,362 → 09/01 5,206 → 09/18 5,514

8개월간 커밋 124개, 티켓 61개의 수정이 한 훅에 누적됐다. 원인은 네 가지다.

원인측정 (사실)
전송 계층 교체를 "추가"로 처리LiveKit self-hosted 커밋 +783 (d8933ee2, 리버트 후 재적용으로 두 번 기록), VP 대화 모드 +419 (b7db1a7c). Realtime 경로를 지우지 않고 옆에 붙였다
로그가 코드의 11%logger 호출 145개, 다중행 포함 589줄. "Realtime 로그 정렬 메타데이터" 커밋 2건만 +343, "세션 시작 진단 로그 보강" +221
버그 수정마다 가드 ref와 타이머 하나씩useRef 110개, setTimeout/clearTimeout 42회. PPI-1219 재응답 차단 +139, PPI-1252 후속 발화 차단 +130, PLC 오탐 방지 +142, PPI-1101 EC/NC/AGC 전환 +122
이벤트 디스패처가 한 함수handleVoiceSessionEvent 4020행부터 1,150줄, case 16개. startSession 2833행부터 1,187줄

런타임 3종(realtime·tts·livekit) 때문인가 — 약 3분의 1만

듣기·마이크 제어 비중 — 16%인데 변경 빈도 30%

구분줄수위치
전용 콜백 17개 (applyActualInputState 139, updateVadSettings 115, runLiveKitInputGateEvent 92 …)7261114~2170, 2526~2582
이벤트 핸들러 안의 듣기 분기904020~5170 내
startSession 안의 마이크 획득·relay652833~4020 내
합계약 880 / 5,514
테스트 현황 — 훅을 직접 검증하는 테스트 0개. 반면 훅이 import하는 모듈 49개 중 34개는 자체 테스트가 있다 (lib/voice-agent/* 22개 전부, tts-player, mic-relay-stream, session-events 등). 훅 밖으로 빼낸 것은 테스트되고, 훅 안에 남은 것은 테스트되지 않는 패턴이 명확하다. 저장소 전체로도 훅 147개 중 테스트 동반 22개(15%)이고, 500줄 이상 훅 16개 중 테스트 있는 7개도 대부분 훅 밖 순수 함수만 검증한다 (use-host-socket hydration 테스트 669줄이 가장 가까운 선례). 1,000줄 넘는 훅을 renderHook으로 통째 검증한 사례는 없다.

4. use-ai-session 분리 설계 승인 대기

프로젝트 brainstorming 스킬의 architectural 경로로 잡았다. 런타임 3종이 모두 살아 있어(lib/voice-agent/resolver.ts:11 VoiceRuntime = "realtime" | "tts" | "livekit") 삭제가 아닌 분리다. 승인 후 docs/superpowers/specs/ 스펙 → writing-planscode-quality-guardian 순서.

목표 · 비목표

접근 비교

내용판정
A. 런타임별 수직 분리realtime / tts / livekit 훅 3개기각 — 이벤트 처리·가드가 공통이라 중복 3배. §3에서 런타임 코드가 3분의 1임을 확인
B. 책임별 수평 분리 + 트랜스포트 커넥터 함수화서브훅 5개 + 순수 리듀서 + 커넥터 3개채택 — 현재 코드의 자연 경계와 일치
C. 상태 머신 재작성Zustand/XState 기반 재구현기각 — 테스트 0인 상태에서 위험 최대

모듈 맵 (B) — apps/web/entities/guest-session/ 기준

파일예상 줄수가져갈 코드 (현재 줄)형태
model/use-ai-session.ts~650아래 골격 + 남는 effect·래퍼오케스트레이터. 조합만, 구현 없음
lib/voice-session-event-reducer.ts~1,000handleVoiceSessionEvent 4020~5170, case 16개순수 리듀서 (event, snapshot) → effects[]. 콜백·로그·TTS 명령을 effects로 반환
lib/transport/{realtime,tts,livekit}-connector.ts + transport-handle.ts~750startSession 3463~3990 런타임 블록, stopSession dispose기존 lib/voice-agent/voice-session-transport.ts 인터페이스 구현 함수 3개. generation/abort 가드는 오케스트레이터 유지
model/use-listening-control.ts~9001114~2170, 2526~2582, ref 21개서브훅. configured 듣기 의도의 단일 소스
model/use-response-gate.ts~450cancelResponse, setResponseBlocked, fullVideo 게이트 4개, setInterruptMode/early window (1056~1460, 1969~2330)서브훅
model/use-transcript-pipeline.ts + lib/realtime-item-meta.ts~600sendTextMessage, handleExternalStt*, partial transcript (815~1056, 1462~1720), 상단 순수함수 215~407서브훅 + 순수 함수
model/use-auto-start-finish.ts~350handleSendAutoStartMessage, handleAutoFinish, resetAutoFinish, updateStartMent (2335~2526, 5181~5245)서브훅
model/use-session-audio.ts~300audioElement/output sink/shared TTS ctx/warmUpAudio/health log (2526~2650, 5224~5330, 상단 182~207)서브훅
model/use-ai-session.types.ts~150Props 49개, StartSessionParams타입

공통 정리: 콜백 동기화 effect 17개(onXRef.current = onX)는 useLatestRef 유틸 하나로. 도메인 간 공유 ref(liveKitSessionRef, peerConnectionRef 등 7개)는 오케스트레이터가 소유하는 transportHandleRef 하나로 묶어 서브훅에 주입한다. 서브훅은 트랜스포트를 소유하지 않는다.

분리 후 오케스트레이터 골격 (스케치)

export function useAiSession(props: UseAiSessionProps) {
  // 1) 콜백 props 17개 → useLatestRef 하나로 (effect 17개 제거)
  const callbacks = useLatestRef(props);
  // 2) useState 10개는 그대로 (소비자 인터페이스 불변)
  const [isActive, setIsActive] = useState(false);  /* … 9개 */
  // 3) 트랜스포트 핸들: pc / dataChannel / liveKitSession / ttsPlayer / generation / abort
  const transport = useRef(createTransportHandle());
  // 4) 서브훅: 자기 ref만 소유, 트랜스포트는 주입받아 읽기·명령만
  const audio      = useSessionAudio({ transport, audioOutputDeviceId: props.audioOutputDeviceId });
  const gate       = useResponseGate({ transport, callbacks });
  const listening  = useListeningControl({ transport, callbacks, setGuestMicStream, setIsUserTalking, externalMicStream: props.externalMicStream });
  const transcript = useTranscriptPipeline({ transport, callbacks, setCurrentTranscript });
  const auto       = useAutoStartFinish({ transport, callbacks, gate, transcript });

  // 5) 이벤트: 리듀서가 effects를 돌려주고, 여기서만 실행
  const handleVoiceSessionEvent = useCallback((event: VoiceSessionEvent) => {
    const effects = reduceVoiceSessionEvent(event, {
      gate: gate.snapshot(), listening: listening.snapshot(), transcript: transcript.snapshot(), auto: auto.snapshot(),
    });
    for (const fx of effects) {
      switch (fx.kind) {
        case "setPeerTalking":     setIsPeerTalking(fx.value); break;
        case "gate":               gate.apply(fx); break;
        case "listening":          listening.apply(fx); break;
        case "transcript":         transcript.apply(fx); break;
        case "auto":               auto.apply(fx); break;
        case "audio":              audio.apply(fx); break;
        case "callback":           callbacks.current[fx.name]?.(...fx.args); break;
        case "log":                logger[fx.level](fx.message, fx.fields); break;
      }
    }
  }, [gate, listening, transcript, auto, audio, callbacks]);

  // 6) 시작/종료: generation·abort 가드는 여기, 런타임별 연결은 커넥터 3개로
  const startSession = useCallback(async (params: StartSessionParams) => {
    const generation = transport.current.nextGeneration();
    const signal = transport.current.arm();
    const voiceSession = resolveVoiceSessionForProfile(params.profile);
    const mic = await listening.acquireMic(params.audioDeviceIdOrStream, signal);
    const audioEl = audio.prepareElement();
    if (!transport.current.isCurrent(generation, signal)) return;
    const connector = voiceSession.runtime === "livekit" ? connectLiveKit
                    : voiceSession.runtime === "tts"     ? connectTts : connectRealtime;   // 런타임 분기는 여기 한 곳
    const connected = await connector({ voiceSession, mic, audioEl, signal,
      onEvent: (e) => handleVoiceSessionEventRef.current(e), onRemoteStream: setAiAudioStream });
    if (!transport.current.isCurrent(generation, signal)) { connected.dispose(); return; }
    transport.current.attach(connected);
    listening.applyPendingAfterConnect();   // pendingVadSettings, configured 듣기 의도
    auto.sendStartMessageIfNeeded();
    setIsActive(true);
  }, [listening, audio, auto]);

  // 8) return: 키 48개 그대로, 구현만 서브훅으로 위임
  return { isSessionActive: isActive, isActive, isPeerTalking, /* … 상태 */
    handleStartSession, handleStopSession,
    ...gate.api, ...listening.api, ...transcript.api, ...auto.api, ...audio.api };
}

검증 전략 · 체크포인트

  1. 하네스 — 리듀서 characterization 테스트(현재 동작을 그대로 기록, 케이스 16개) + useAiSession renderHook 스모크 1개. vitest jsdom 환경 존재. 이 저장소 관행("훅은 얇게, 로직은 순수 함수로 뽑아 테스트")과 일치.
  2. 순수 함수·useLatestRef 추출 (무위험)
  3. 이벤트 리듀서 추출 — 가장 먼저 테스트 효과가 나는 지점
  4. 트랜스포트 커넥터 3개 추출
  5. 서브훅 4개 순차 (listening → gate → transcript → auto)
  6. 오케스트레이터 정리
핵심 불변 조건 — 소비자가 보는 return 키 48개·props 49개 불변, 로그 문자열 불변, 런타임 3종 분기는 커넥터 선택 한 곳으로만. 유일한 구조 변경은 이벤트 핸들러가 직접 부작용을 내던 것을 리듀서가 effects 목록으로 반환하는 형태이며, 이것이 테스트 0개를 케이스 16개 단위 테스트로 바꾸는 지점이다. "여러 책임을 한 훅이 대표하는 것"은 괜찮고, "그 책임들의 상태를 한 클로저가 공유하는 것"이 문제다. 분리의 초점은 파일 나누기가 아니라 ref 소유권 나누기다.

5. 최근 한 달, 왜 코드가 계속 늘었나

결론 — 총량 증가의 주범은 기능 볼륨이고, 지침 부재가 만든 문제는 "핫파일에 덧붙이기" 한 패턴에 국한된다. 분리 지침은 실제로 없다. 다만 그것이 전체 증가의 원인은 아니다.

[한 달 순증 +5.2만 줄, 런타임 ts/tsx · 2026-08-18~09-18 · 커밋 103개]
  ├─ 신규 파일 +3.7만 (261개)          ← 기능 추가. 에이전트는 새 모듈을 잘 만듦
  ├─ 300줄 미만 파일 +1.1만              ← 정상 성장
  ├─ 1000~3000줄 파일 −0.4만             ← 레거시 제거(−5,608)·MFCC 제거(−3,586)가 상쇄
  └─ 3000줄+ 파일 +0.3만                ← 문제 구간. 파일 4개에 집중
        use-ai-session +954 / use-guest-page-session +715 / db-queries +1,379 / livekit-client-session +2,159

[핫파일 덧붙이기 메커니즘]
  이슈 티켓(PPI-1219 재응답 차단, 1252 후속발화 차단, 1102 듣기 복원 …)
  → brainstorming "bounded" 경로 = "기존 흐름을 수정" 으로 분류
  → 가드 ref + effect를 기존 훅 안에 추가 (+20~140줄/커밋, use-ai-session 11커밋 중 9개가 순증)
  → 검토 게이트에 파일 크기·책임 분리 기준 없음 → 통과
  → 5,514줄

근거

— 지침이 없어서 "분리를 안 하는" 게 아니라, 지침이 없어서 "이미 큰 파일을 건드릴 때 추출 대신 추가를 선택"하는 것이다. brainstorming의 bounded 경로("기존 흐름 수정")가 이를 정당화하고, 리뷰 게이트가 크기를 보지 않아 걸러지지 않는다.

6. VP 통합 커밋(b7db1a7c, #1041) 해부 — 부당한 순증인가

한 달 순증의 절반 이상을 차지하는 커밋이다. "VP 모드"가 3만 줄이 아니고, 154개 커밋을 하나로 squash한 통합 PR이 16.6만 줄이다.

b7db1a7c (#1041) 633 files, +166,076 / −8,046   PR 생성→머지 57초, PR #1006(PPI-1176) 통합
  ├─ apps/livekit-agent/vendor/*  +86.8k  ← LiveKit Agents 1.7.1 파이썬 런타임 vendoring (작성 코드 아님)
  ├─ 테스트                        +37.2k  ← socket 19.9k, agent 9.3k, web 8k
  ├─ 런타임 ts/tsx                 +31.3k  ← 기능 4개 묶음 (신규 438파일 +13.3만 / 기존 192파일 +2.5만 전체 기준)
  └─ 설정·문서·기타                 +2.7k

런타임 3.1만의 구성 (PR 본문 4개 섹션 ↔ 디렉터리 순증 대조)
  SFU 인증·재연결 + 음성 제어 전달(Redis)  약 13k   avatar-handlers +2,430 · voice-control-delivery-store +1,720(신규) · sfu-socket +9.4k · redis +3.6k
  LiveKit VP 대화 처리(클라이언트)          약  9k   livekit-client-session +1,802 · lib/voice-agent 신규 다수
  모니터·게스트 UI 상태 복구                약  6k   use-guest-page-session +2,500 · features/monitor +2,505 · host-socket +1,430
  관측성·API·공유 타입                      약  3k   lib/api +1,720 · shared/lib +1,403

"안정화" 13k의 실체 — 소켓 런타임이 2배가 됐다 (13,573 → 27,812)

socket 런타임 +14,239
  ├─ 기존 핸들러에 추가  +7.5k   avatar-handlers +2,430 · connection-handlers +1,386 · monitoring-handlers +851
  │                              pending-disconnect-tracker +826 · session-relay +636 · room-store +585
  └─ 신규 모듈          +6.7k   voice-control-delivery-store 1,720 · livekit-input-diagnostics 575
                                 voice-control-transition-store 467 · monitor-recipient-auth 355 · monitor-control-auth 266
                                 credential-refresh/lifecycle 318 · admission·principal·claim 스토어 6개
판정 추정"부당한 순증"은 아니고 "검증되지 않은 순증"이다. 볼륨은 기능량(vendoring 8.7만 + 테스트 3.7만 + 기능 4개)으로 설명되고, 코드 성격도 로그·가드 패치가 아니라 함수 선언과 상태 전이 로직이다. 그래도 두 가지가 남는다. ① 배치: 소켓 런타임 +14.2k 중 7.5k가 기존 핸들러 파일에 들어갔다. 로직이 정당해도 위치는 부적절하며, §5의 지침 부재와 같은 종류다. ② 검증 불가: 154커밋을 squash해 57초에 머지했기 때문에 전달 보장 서브시스템(Redis 스토어 3개, fence·lease)이 문제 규모에 맞는 설계였는지 당시 리뷰 기록이 없고, 지금 커밋 단위로 되짚을 수도 없다. "부당하다"고 말할 근거가 없는 것과 같은 이유로 "적정하다"고 말할 근거도 없다. 동기는 있다 — 소켓 재연결 시 claim 소실, 듣기 상태 복원 실패, 모니터 새로고침 stale push 같은 운영 이슈가 이 시기에 반복됐고, 이 코드는 그 계열을 generation fence로 한 번에 막으려는 설계다.

7. 코드 분할 기준 제안 — 300 / 500 / 1,000 래칫

런타임 ts/tsx 1,184개, 총 23.5만 줄 · 중앙값 82 · p75 197 · p90 433 · p95 693
  >300줄  188개(16%)  → 전체 코드의 64%
  >500줄  100개( 8%)  → 49%
  >1000줄  30개(2.5%) → 29%

훅(use-*) 147개 · 중앙값 130 · p75 245 · p90 535 · 300 초과 28개(19%) · eslint max-lines/complexity 룰 없음
기준적용이유
300줄신규 파일 lint 경고 (max-lines)업계 기본값(ESLint 기본 300, max-lines-per-function 50, complexity 20, SonarQube 인지복잡도 15)이고 이 저장소 p85와 일치. 기존 파일엔 미적용 — 188개가 걸려 소음
500줄래칫: 이 위의 파일에 순증하는 PR은 추출 체크포인트를 포함해야 통과파일 8%가 코드 절반을 차지하는 꼬리의 시작점. 여기서 막으면 성장이 멈춘다
1,000줄분할 백로그 등록, 수정 시 bounded 경로 금지30개 파일. 지난달 순증이 실제로 몰린 구간
에이전트 환경에서도 분할이 필요한 이유 추정 — 5,000줄을 읽는 비용은 에이전트에겐 거의 0이다. 문제는 읽은 뒤 수정의 정합성을 확인할 단위가 없다는 것이다. 테스트 0개, ref 110개가 얽혀 있으면 어떤 변경도 "이 부분만 맞다"고 로컬에서 말할 수 없고, 에이전트는 기존 구조를 건드리지 않는 국소 최소 수정(ref+타이머 추가, 진단 로그 추가)으로 최적화한다. 이 파일의 7~9월 성장 곡선(+1,507/두 달)이 그 증거다. 목표는 줄 수 줄이기가 아니라 검증 가능한 이음새 만들기이고, 그래서 리듀서 추출이 파일 나누기보다 앞선다.

8. 확정 / 추정 경계

항목상태
파일·훅 줄수, ref/effect/callback 개수, 커밋별 순증, 성장 곡선, 지침 문서 매칭 0건, lint 룰 부재사실 develop e34637e5 기준 직접 측정
미참조 컴포넌트·훅 목록사실 import 경로·컴포넌트명 grep 기준. 동적 import·문자열 참조는 삭제 전 재확인 필요
에이전트 작업 비중 86%추정 PR 본문 템플릿 프록시. 사람이 같은 템플릿을 쓴 경우 과대
모듈 맵 예상 줄수, 리듀서/서브훅 인터페이스추정 writing-plans 단계에서 확정
VP 통합 커밋 설계 적정성판단 불가 — 리뷰 기록 없음. Redis 스토어 3개 상태 전이 문서화 후 대조 필요
"에이전트가 갓파일을 키운다"는 인과추정 시기 상관(7~9월 가속)과 커밋 성격(ref/로그 추가)에서 추론. 사람 커밋과의 대조군 없음

관련 문서