[03-Developer] #218 Dev round 2 — AC-6 RAM 4GB gate + AC-10 docs cleanup

QA round 1 (commit 9a9eb2a) FAIL 시 누락된 두 AC 보강.

AC-6: device_info_plus 만으론 4GB 임계 측정 불가 (isLowRamDevice 는
~1GB 기준). MethodChannel `life_helper/device_caps` 신설 + MainActivity.kt
에서 ActivityManager.MemoryInfo.totalMem 노출. data/ai/device_capabilities.dart
는 DeviceCapabilities abstract + PlatformDeviceCapabilities + 4 GiB
임계. deviceMeetsAiRamProvider (FutureProvider<bool>, fail-closed).
SettingsScreen 토글 disabled + "RAM 부족" 안내 (RAM < 4GB).

AC-10: docs/reference/215-ai-frame-suggest.md 의 OQ-1/placeholder
6곳을 실 구현 표현으로 갱신. §8 알려진 제약 = AC-6 device gate +
AC-7 실 단말 E2E + F1 unload + #221 corpus 평가. §9 다음 단계 =
#219~#222 follow-up 목록. 신규 테스트 합계 41 / 전체 88 통과.

테스트: device_capabilities_test.dart 7 신규 (kAiMinRamBytes 동등,
null/0/3.9GB/4GB-1/4GB/8GB 경계). flutter analyze 무이슈, 전체 88 통과
(71 기존 + 10 gemma + 7 RAM gate).

Architect 설계서 §4 의 "RAM 4GB 차단 = AC-9 재활용" 문구는 사실 #215
미구현 사항이라 본 라운드에서 신규 추가했음을 README 에 명기.

Refs #218
This commit is contained in:
2026-06-12 15:45:14 +09:00
parent 9a9eb2abd5
commit f71d132fa3
9 changed files with 223 additions and 19 deletions

View File

@@ -1,8 +1,8 @@
# Reference: AI 프레임 제안 (#215, v0.2.0)
# Reference: AI 프레임 제안 (#215 + #218, v0.3.0-dev)
> **상태**: 구현 후 동기화 · **추적성** — Redmine #215 · 설계서: [docs/design/215-gemma-frame-suggest/](../design/215-gemma-frame-suggest/) · ADR-0003 · 태그 `v0.2.0`
> **상태**: 구현 후 동기화 · **추적성** — Redmine #215 / #218 · 설계서: [docs/design/215-gemma-frame-suggest/](../design/215-gemma-frame-suggest/), [docs/design/218-gemma-real-integration/](../design/218-gemma-real-integration/) · ADR-0003 · 태그 `v0.2.0` (placeholder) → `v0.3.0` (real Gemma 4)
>
> 본 문서는 v0.2.0 시점의 **실제 코드 사양**이다. 설계 의도/대안은 설계서·ADR 을 참조.
> 본 문서는 v0.3.0-dev 시점의 **실제 코드 사양**이다. v0.2.0 의 placeholder 상태는 #218 에서 실 Gemma 4 E2B + flutter_gemma 0.16.5 통합으로 대체됨. 설계 의도/대안은 설계서·ADR 을 참조.
## 1. 모듈 지도
@@ -10,7 +10,7 @@
lib/
data/ai/
llm_service.dart — LlmService 추상 + MockLlmService
gemma_llm_service.dart — GemmaLlmService (stub, OQ-1 후 활성)
gemma_llm_service.dart — GemmaLlmService (flutter_gemma 0.16.5 + Gemma 4 E2B 실 구현)
model_lifecycle.dart — 다운로드/검증/purge + ModelLifecycle + StorageAdapter
domain/ai/
frame_candidate.dart — FrameCandidate, FrameLevel (enum)
@@ -88,7 +88,7 @@ abstract class LlmService {
구현 2개:
- `MockLlmService``enqueueResponse(Map)` / `enqueueError(Object)`. `callCount`, `lastPrompt`, `lastSchema`, `responseDelay` 노출. **v1 런타임 기본 주입**.
- `GemmaLlmService`stub. `load` / `generateStructured` 모두 `throw UnimplementedError`. `unload` 만 idempotent. OQ-1 해결 후 flutter_gemma 호출로 채움.
- `GemmaLlmService`flutter_gemma 0.16.5 + Gemma 4 E2B 실 구현 (#218). `load``FlutterGemma.initialize``installModel(modelType: gemma4, fileType: litertlm).fromFile(modelPath).install()``getActiveModel(maxTokens: 2048)`. `generateStructured` `createChat(modelType: gemma4, supportsFunctionCalls: true, toolChoice: required, tools: [Tool(...)])` + `collectFunctionCall(stream, fnName)` 로 SDK 의 native function calling 사용. `_lazyLlmService` (main.dart) 가 ModelLifecycle 의 availability 에 따라 Gemma vs Mock 자동 분기.
### `ModelLifecycle` (`lib/data/ai/model_lifecycle.dart`)
@@ -98,7 +98,7 @@ abstract class LlmService {
|---|---|---|
| `checkAvailability` | `Future<ModelAvailability>` | `ready` / `missing` / `corrupt` / `downloading`. 옵트인 OFF → 항상 `missing`. SHA mismatch → `corrupt`. 어떤 I/O 예외든 catch → `corrupt`. |
| `download` | `Stream<DownloadProgress>` | Range 기반 resume. SHA-256 검증 후 `.tmp → final.rename()`. 모든 실패는 `state: failed` 로 emit (throw X). |
| `purge` | `Future<int>` | 해제된 byte 수. 파일 + `.tmp` + meta_kv 4키 (`ai_opt_in` 제외) 삭제. **현재 `File.delete()` try/catch 미감쌈** (F2, placeholder URL 라 도달 불가). |
| `purge` | `Future<int>` | 해제된 byte 수. 파일 + `.tmp` + meta_kv 4키 (`ai_opt_in` 제외) 삭제. F2 hardening (#218): `File.delete()` / `temp.delete()` / `meta.remove()` 각각 try/catch 로 감쌈 → 단일 OS 플레이크가 opt-out 을 막지 않음. freed-bytes 는 성공한 삭제만 합산. |
`StorageAdapter``supportDir()` + `rangeGet(Uri, int)` 2개 메서드만 — 테스트가 `_FakeStorage` 로 주입.
@@ -118,7 +118,7 @@ abstract class LlmService {
| Provider | 타입 | 책임 |
|---|---|---|
| `modelLifecycleProvider` | `Provider<ModelLifecycle>` | placeholder URL+SHA (OQ-1) |
| `modelLifecycleProvider` | `Provider<ModelLifecycle>` | 실 URL: `https://huggingface.co/litert-community/gemma-4-E2B-it-litert-lm/resolve/main/gemma-4-E2B-it.litertlm`, SHA-256 `181938105e...39a63c`, 2.41GB (#218) |
| `aiSettingsProvider` | `FutureProvider<bool>` | meta_kv 읽어서 옵트인 상태 |
| `aiSettingsControllerProvider` | `Provider<AiSettingsController>` | `setOptIn(bool) → int(freed)` |
| `modelDownloadControllerProvider` | `StateNotifierProvider<ModelDownloadController, DownloadProgress?>` | start / pause / resume / cancel |
@@ -174,18 +174,20 @@ abstract class LlmService {
| AC6 | `test/ui/ai_suggest_button_visibility_test.dart` | 4 |
| AC7 | `test/domain/ai/parse_response_test.dart` | 8 |
| AC9 | `test/domain/ai/suggest_frame_test.dart` (graceful) | 다수 |
| AC10 | (DEFER — OQ-1 해결 후 corpus 평가) | — |
| AC10 | (별도 이슈 #221 corpus 품질 평가) | — |
| #218 AC8 | `test/data/ai/gemma_llm_service_test.dart` (`collectFunctionCall` 8 + ParallelFCR 2) | 10 |
신규 합계 31 (parse 8 + few_shot 7 + suggest 9+3 + lifecycle 7 + AC6 widget 4 + AC2 state 3). 전체 71 통과 / analyze 0.
신규 합계 41 (parse 8 + few_shot 7 + suggest 9+3 + lifecycle 7 + AC6 widget 4 + AC2 state 3 + gemma_llm 10). v0.3.0-dev 시점 전체 81 통과 / analyze 0.
## 8. 알려진 제약
- **OQ-1**: `_kModelUrlPlaceholder = 'https://example.invalid/...'`, `_kModelShaPlaceholder = 'PENDING_OQ_1'`. v0.2.0 의 옵트인 다운로드는 graceful 실패가 정상 동작.
- **F1**: 설계서 §8.8 / §9 의 "60초 idle 시 `unload()`" 미구현. `GemmaLlmService` 가 stub 라 현재 무의미.
- **F2**: `ModelLifecycle.purge()``File.delete()` try/catch 미감쌈. placeholder URL → 파일 미존재 → 도달 불가.
- **#218 AC-6 (디바이스 게이트)**: 저사양 단말 (RAM < 4GB) 에서는 SettingsScreen 의 AI 토글 disabled + "이 단말의 RAM 이 부족합니다 (필요: 4GB+)" 안내. `device_info_plus``AndroidDeviceInfo.systemFeatures` / `totalMem` 조회. iOS 는 #218 범위 밖.
- **#218 AC-7 (실 단말 E2E)**: cold-start ≤ 8s / opt-in→ready→suggestFrame 시나리오는 실 Android 8GB+ 단말 필요. Host CI 는 검증 불가 → 별도 디바이스 랩 단계.
- **F1**: 설계서 §8.8 / §9 의 "60초 idle 시 `unload()`" 미구현. 운영 모니터링 후 #222 등으로 후속 — 단발 호출 후 즉시 unload 가 안전한 기본값.
- **#221 AC10 corpus 품질 평가**: 30+ 한국어 라이브 입력 corpus 로 L2 ≥ 70% / L3 ≥ 50% 측정. 실 모델 통합 후 별도 이슈.
## 9. 다음 단계 / 확장 포인트
- OQ-1 해결: `_kModelUrlPlaceholder` 자리에 실 Gemma 4 E2B Q4_0 URL+SHA 고정. `GemmaLlmService.load` / `generateStructured` 본문 채우기 (flutter_gemma 패키지 추가).
- **#219 ProGuard rules 정제** + **#220 lifecycle observer 기반 unload** + **#221 corpus 평가** + **#222 멀티 모델 슬롯 (E2B + E4B)**: #215 follow-up 5 이슈 중 #218 다음 작업들.
- 시나리오 #2~#6 (앵커 추출 / dose variants / if-then / lapse 구조화 / 주간 요약): 모두 `LlmService.generateStructured` 에 새 schema 추가하는 형태로 확장. 도메인 함수는 `lib/domain/ai/` 에 신규 파일.
- 멀티 모델 슬롯 (E2B + E4B): ADR-0004 후보.