Files
life-helper/docs/design/204-flutter-bootstrap/fn-validate-frame-level.md
joungmin b8e563176b [Architect] #204 design spec — Flutter bootstrap + 18 tables
Phase 1 설계서 작성 완료. docs/design/204-flutter-bootstrap/ 13 개 파일:
- README.md (12 섹션 모두 채움, 함수 19 개 명세, AC 16 항)
- 01-project-structure.md (feature-first + layer-first 하이브리드)
- 02-drift-schema-catalog.md (Catalog 7 테이블 Dart 정의)
- 03-drift-schema-user.md (User 11 테이블 + R1~R10 강제 매트릭스)
- 04-migrations.md (schemaVersion v1 + 인덱스 17 개)
- 05-seed-data.md (assets/seed/*.json + first-run import)
- 06-ux-contracts.md (체크인 R8 ≤ 60 초 흐름)
- fn-recommend-variant / fn-compute-streak / fn-weekly-minimum-ratio
- fn-validate-frame-level / fn-active-habit-quota / fn-seed-importer

핵심 결정: dose_variants 는 별도 habit_dose_variant 테이블로 정규화 (FK
무결성 + recommendVariant SQL 단순성). ADR-0002 승격 권장.

Refs #204

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-06-11 16:57:31 +09:00

148 lines
5.8 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 함수 설계서: `validateFrameLevel` (#204)
> 부모 설계서: [README.md](./README.md) · 상태: Draft
> 작성: [AI] Architect · 구현: `lib/domain/frame/validate_frame_level.dart::validateFrameLevel` (TBD)
> 테스트: `test/domain/frame/validate_frame_level_test.dart` (TBD)
## 1. 시그니처
```dart
FrameValidationResult validateFrameLevel(
FrameInput input, {
required Iterable<FramePattern> knownPatterns, // catalog 에서 미리 load
});
class FrameInput {
final FrameLevel level; // L0/L1/L2/L3
final String? originalText;
final String framedText;
}
class FrameValidationResult {
final FrameValidationStatus status; // accept / warn / reject
final List<AvoidanceHit> avoidanceHits; // R7 회피 키워드 검출 결과
final List<FrameSuggestion> suggestions; // L2/L3 변환 제안
}
enum FrameValidationStatus { accept, warn, reject }
class AvoidanceHit {
final String keyword;
final int startIndex;
final int endIndex;
final FramePattern source;
}
class FrameSuggestion {
final FrameLevel level; // L2 or L3
final String text;
final FramePattern source;
}
```
## 2. 책임
사용자가 입력한 frame 의 (1) level 이 L2/L3 가드를 통과하는지 (R3) 검사하고, (2) framed_text 안의 회피 키워드를 detect 해 변환 제안을 반환한다 (R7).
## 3. 입력
| 파라미터 | 타입 | 제약/검증 | 설명 |
|----------|------|-----------|------|
| `input.level` | `FrameLevel` enum | not null | L0 이면 reject (R3) |
| `input.framedText` | `String` | not empty | 사용자 최종 문장 |
| `input.originalText` | `String?` | nullable | L0 였던 원본 (audit) |
| `knownPatterns` | `Iterable<FramePattern>` | catalog 에서 load | 회피 키워드 사전 |
## 4. 출력
- **반환**: `FrameValidationResult`.
- status:
- `reject` — L0/L1 입력. UI 는 모달로 L2/L3 변환 제안 표시 후 사용자 선택.
- `warn` — L2/L3 이지만 회피 키워드 감지 (예: "안 ..." , "끊기"). UI 는 inline warning + 변환 제안. 사용자가 '계속' 선택 가능.
- `accept` — 통과.
- avoidanceHits — UI 가 underline 표시.
- suggestions — frame_pattern.l2_suggestion / l3_identity 에서 가져온 대체 텍스트.
- **부수효과**: 없음. **순수 함수**.
## 5. 동작 / 알고리즘
```
1. if input.level in (L0, L1):
suggestions = buildSuggestions(input.framedText, knownPatterns)
return reject(suggestions, hits=[])
2. hits = detectAvoidanceKeywords(input.framedText, knownPatterns)
3. if hits.isEmpty:
return accept
4. # L2/L3 인데 회피 키워드 감지 — warn
suggestions = buildSuggestions(input.framedText, knownPatterns, hits)
return warn(suggestions, hits)
function detectAvoidanceKeywords(text, patterns) -> List<AvoidanceHit>:
hits = []
for p in patterns:
idx = text.indexOf(p.avoidance_keyword)
while idx >= 0:
hits.push(AvoidanceHit(keyword=p.avoidance_keyword, start=idx,
end=idx+keyword.length, source=p))
idx = text.indexOf(p.avoidance_keyword, idx+1)
return hits.distinctBy((h)->(h.start, h.keyword))
function buildSuggestions(text, patterns, hits=null) -> List<FrameSuggestion>:
relevant = hits != null
? hits.map(h => h.source).distinct()
: patterns.filter(p => text.contains(p.avoidance_keyword))
return relevant.expand((p) -> [
FrameSuggestion(L2, p.l2_suggestion, p),
if (p.l3_identity != null) FrameSuggestion(L3, p.l3_identity, p),
]).take(5).toList() # UI 부담 위해 최대 5 개
```
> 회피 키워드 사전은 `frame_patterns.json` 시드 (~30 항목). 'general' 도메인이 default 매칭 풀, 특정 domain (food/drink/...) 은 habit 의 protocol category 로 필터링 가능 (v2 최적화).
## 6. 에러 & 실패 모드
| 조건 | 처리 | 반환 |
|------|------|------|
| `framedText` empty | reject + suggestions=[] | reject |
| `level` enum 외 값 | 호출자 보장 — 도달 불가 | — |
| `knownPatterns` empty | accept (사전 없으면 R7 skip) | accept |
| 회피 키워드가 다른 단어의 부분 일치 (예: "안전" 안에 "안") | false positive 발생. 본 phase 는 substring 매칭. 단어 경계 처리는 v2 (한국어 형태소 분석 필요). | warn (사용자가 '계속' 선택) |
## 7. 엣지케이스
- "끊기" 가 "끊기는 게 아니라" 안에 포함 → hit. warn. 사용자가 '계속'.
- L3 ("나는 무알콜인 사람이다") + 회피 키워드 "무알콜" — '무X' 패턴이 사전에 있다면 hit. 사용자가 '계속'.
- L2 + 회피 키워드 없음 → accept.
- L0 + 회피 키워드 없음 → reject + L2 일반 제안 ('이걸 대신 해보세요').
## 8. 복잡도 / 성능
- 시간: O(N × M), N = patterns 수 (~30), M = text 길이 (≤ 100). worst ≈ 3000 char ops → < 100 µs.
- 공간: O(hits).
- 호출 빈도: habit 생성 폼에서 입력 변경 시 debounced (300 ms). 무시.
## 9. 의존성
- `FramePattern` 모델 (domain/models).
- `FrameLevel` enum.
- catalog DAO 가 startup 에 patterns 를 한 번 load 해서 Riverpod provider 로 공유.
## 10. 테스트 케이스
- [ ] accept: L2, "기상 직후 햇빛 10 분 받기" → status=accept, hits=[]
- [ ] reject (L0): L0, "햇빛 안 빼먹기" → status=reject, suggestions 포함 L2/L3
- [ ] reject (L1): L1, "햇빛 챙기기" → status=reject (L2 으로 변환 제안)
- [ ] warn: L2, "술 끊기 (조금만)" — "끊기" hit → status=warn, suggestions
- [ ] accept (L3 클린): L3, "나는 아침 햇빛 사람이다" → accept
- [ ] empty framedText → reject
- [ ] empty patterns → accept (회피 검출 skip)
- [ ] 다중 hits: "끊기 안 하기" → 2 개 hits, suggestions ≤ 5
## 11. 추적성
- 인수조건: #204 AC-8 (R3 reject), AC 보강 (R7 warn).
- 관련 메모리: `feedback_positive_framing.md` (L2/L3 강제).
- 관련 schema: `frame_pattern.schema.json`, `habit.schema.json` (frame.level).