# 함수 설계서: `CorpusLogger` (#312) > **부모 설계서**: ./README.md · **상태**: Draft > **작성**: [AI] Architect · **구현**: `app/lib/ai/diagnostics/corpus_logger.dart` (신규) · **테스트**: `app/test/ai/diagnostics/corpus_logger_test.dart` (신규) ## 1. 시그니처 ```dart abstract class CorpusLogger { void onTextChunk(int turn, String text); void onFunctionCall( int turn, String accumulatedPrefix, String toolName, Map args, ); } class DebugCorpusLogger implements CorpusLogger { /// kDebugMode + --dart-define=ENABLE_CORPUS_LOG=1 일 때만 non-null. /// production 빌드에서는 항상 null. static CorpusLogger? maybeCreate(); @override void onTextChunk(int turn, String text); @override void onFunctionCall(...); } ``` ## 2. 책임 (단일 책임, 1줄) ChatSessionController 의 event 루프에서 발생한 텍스트 청크와 function call 의 raw payload 를 디버그 빌드에서 stdout 으로 dump 한다 — corpus 수집 부담을 줄이기 위한 한시적 진단 도구. ## 3. 입력 ### `onTextChunk` | 파라미터 | 타입 | 제약/검증 | 설명 | |----------|------|-----------|------| | `turn` | int | ≥0 | userTurn 내 multi-turn 루프의 turn index (0=첫 LLM 응답). | | `text` | String | non-null | 도착한 텍스트 청크 (raw, 누적 X). | ### `onFunctionCall` | 파라미터 | 타입 | 제약/검증 | 설명 | |----------|------|-----------|------| | `turn` | int | ≥0 | 동일. | | `accumulatedPrefix` | String | non-null | tool call 도착 시점까지의 누적 텍스트 (chunks 의 concat). | | `toolName` | String | non-null, non-empty | Gemma 가 호출한 도구 이름. | | `args` | `Map` | non-null | tool 인자. JSON serializable 가정. | ### `maybeCreate` - 입력 없음. ## 4. 출력 - `onTextChunk` / `onFunctionCall`: **반환 없음**. 부수효과 = stdout 한 줄 emit (디버그). I/O 실패 시 swallow. - `maybeCreate`: **반환** `CorpusLogger?` — `kDebugMode` 가 true 이고 `const bool.fromEnvironment('ENABLE_CORPUS_LOG')` 가 true 일 때 `DebugCorpusLogger()` 인스턴스, 그 외 null. ## 5. 동작 / 알고리즘 ### `DebugCorpusLogger.onTextChunk` ``` 1. _emit({ 'kind': 'text_chunk', 'turn': turn, 'text': text, }); ``` ### `DebugCorpusLogger.onFunctionCall` ``` 1. _emit({ 'kind': 'function_call', 'turn': turn, 'accumulated_prefix': accumulatedPrefix, 'tool_name': toolName, 'args': args, }); ``` ### `_emit(Map payload)` ``` 1. try: 2. final line = '[CorpusLogger] ' + jsonEncode(payload); 3. developer.log(line, name: 'CorpusLogger'); 4. catch (_): 5. // swallow — diagnostic 이 user flow 를 깨지 않게. ``` ### `DebugCorpusLogger.maybeCreate` ``` 1. if (!kDebugMode) return null; 2. const enabled = bool.fromEnvironment('ENABLE_CORPUS_LOG', defaultValue: false); 3. if (!enabled) return null; 4. return DebugCorpusLogger._(); ``` ## 6. 에러 & 실패 모드 | 조건 | 처리 | 반환/예외 | |------|------|-----------| | `jsonEncode` 가 args 의 non-serializable 키로 throw | `_emit` 의 try/catch swallow | void (silent) | | `developer.log` I/O 실패 | swallow | void | | `maybeCreate` 가 production 호출 | `kDebugMode=false` 분기에서 null | null (정상) | | ctor 직접 호출 시도 | private ctor `_()` 로 차단 | 컴파일 에러 | ## 7. 엣지케이스 - **매우 빈번한 호출**: 토큰 단위 stream 이라 `onTextChunk` 가 초당 수십 회. `developer.log` 가 stdout flush 부담 — 단, debug only + 한시적이라 수용. - **args 에 BigInt / DateTime**: `jsonEncode` 가 throw → swallow. corpus 결과 누락 시 코드 보강 (toString fallback) 가능하지만 본 설계는 swallow 만. - **multi-turn 루프**: 같은 userTurn 내에서 turn 0, 1, 2 ... 각각의 prefix 가 모두 캡처되어야 비교 가능 — caller (ChatSessionController) 가 정확한 turn idx 를 넘긴다. - **logger null 이지만 inject 됨**: callsite 가 `logger?.onTextChunk(...)` 패턴이므로 null-safe. ## 8. 복잡도 / 성능 - 시간: 각 호출 O(payload 크기). `jsonEncode` 가 prefix 길이에 선형. - 공간: emit 마다 임시 string. 영구 보관 없음 (stdout sink). - 호출 빈도: 토큰당 1회 (text_chunk), tool call 당 1회 (function_call). 한 userTurn 에 수십-수백 호출 가능 — debug only 라 수용. ## 9. 의존성 - `dart:convert` — `jsonEncode`. - `dart:developer` — `log`. - `flutter/foundation.dart` — `kDebugMode`. - 환경 변수: `ENABLE_CORPUS_LOG` (dart-define). ## 10. 테스트 케이스 - [ ] **maybeCreate**: production 시뮬 (kDebugMode false fake) → null 반환. - [ ] **maybeCreate**: debug + ENABLE_CORPUS_LOG=false → null. - [ ] **maybeCreate**: debug + ENABLE_CORPUS_LOG=true → non-null DebugCorpusLogger. - [ ] **onFunctionCall happy**: 인자 정상 → stdout 에 `[CorpusLogger]` 라벨 + JSON 한 줄 emit (capture for verification). - [ ] **onFunctionCall non-serializable args**: `{'date': DateTime.now()}` → throw 안 함 (swallow), test 가 timeout 없이 종료. - [ ] **onTextChunk** 빈 텍스트 → swallow 없이 정상 emit (filter 안 함, 무엇이 들어왔는지 그대로 기록하는 게 corpus 의 정직성). > ENABLE_CORPUS_LOG 의 dart-define 기반 테스트는 `--dart-define=ENABLE_CORPUS_LOG=true` 로 별도 `flutter test` invocation 또는 mockable wrapper 로 분리. Architect 권고 = wrapper (`bool _readEnableFlag()` 를 visibleForTesting 으로 expose) 로 테스트 간소화. ## 11. 추적성 - 인수조건: AC1 (corpus 수집 인프라). - 관련 ADR: 없음 (한시적 진단 도구). - 본 이슈 종료 후 제거 여부 검토 — follow-up 이슈로 발행 권장 ("CorpusLogger 정리 — corpus 결과 반영 후 logger 제거 또는 영구화 결정").