[Architect] #312 design spec — tool call prefix corpus & 조건부 push

설계서 3 + 절차서 1.
- README.md: 기능 설계서 (15 케이스 corpus, 임계 5/15, 경로 A/B)
- fn-corpus_logger.md: optional debug logger (kDebugMode + dart-define 가드)
- fn-userTurn_partial_push.md: chat_providers.dart 의 break 분기 수정안 (경로 A/B)
- corpus-procedure.md: 빌드/캡처/15 프롬프트/임계 판정 절차

R1-R5 모두 해소 (Architect 채택안).
ADR-0006 슬롯 = 경로 B 채택 시 작성 (Developer 단계).

Refs #312
This commit is contained in:
2026-06-15 14:17:47 +09:00
parent 41457ab96e
commit 94a9cd474b
4 changed files with 546 additions and 0 deletions

View File

@@ -0,0 +1,134 @@
# 함수 설계서: `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<String, dynamic> 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<String, dynamic>` | 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<String, dynamic> 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 제거 또는 영구화 결정").