[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>
This commit is contained in:
147
docs/design/204-flutter-bootstrap/fn-validate-frame-level.md
Normal file
147
docs/design/204-flutter-bootstrap/fn-validate-frame-level.md
Normal file
@@ -0,0 +1,147 @@
|
||||
# 함수 설계서: `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).
|
||||
Reference in New Issue
Block a user