Files
life-helper/app/lib/domain/frame/validate_frame_level.dart
joungmin 8fe6a8f378 [Developer] #204 Phase 1 MVP — Flutter app skeleton complete
- Drift 21 tables (8 catalog + 11 user + habit_dose_variants + meta_kv)
  with R1~R10 CHECK constraints and 19 indexes
- 8 hand-crafted seed JSON catalogs in app/assets/seed/
  (refs 84, protocols 34, methodologies 21, frame_patterns 30,
   reward_menu_items 30, break_protocols 8, common_frames 5, diet_patterns 5)
- 6 domain functions: recommend_variant, compute_streak,
  validate_frame_level, active_habit_quota, weekly_minimum_ratio,
  seed_importer (transactional, idempotent)
- 4 vertical-slice Riverpod screens: HabitList, HabitCreate, CheckIn, Streak
- 31 unit tests passing; flutter analyze clean
- OQ-5 streak semantics: missing entry ≠ explicit blank
  (missing = end of history; only TrackerValue.blank triggers Never-miss-twice)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-06-12 10:33:03 +09:00

151 lines
3.8 KiB
Dart

import '../models/frame_pattern.dart';
import '../models/habit.dart';
enum FrameValidationStatus { accept, warn, reject }
class FrameInput {
final FrameLevel level;
final String? originalText;
final String framedText;
const FrameInput({
required this.level,
required this.framedText,
this.originalText,
});
}
class AvoidanceHit {
final String keyword;
final int startIndex;
final int endIndex;
final FramePatternModel source;
const AvoidanceHit({
required this.keyword,
required this.startIndex,
required this.endIndex,
required this.source,
});
}
class FrameSuggestion {
final FrameLevel level;
final String text;
final FramePatternModel source;
const FrameSuggestion({
required this.level,
required this.text,
required this.source,
});
}
class FrameValidationResult {
final FrameValidationStatus status;
final List<AvoidanceHit> avoidanceHits;
final List<FrameSuggestion> suggestions;
const FrameValidationResult({
required this.status,
this.avoidanceHits = const [],
this.suggestions = const [],
});
}
/// fn-validate-frame-level: R3 (L0/L1 reject) + R7 (avoidance keywords).
FrameValidationResult validateFrameLevel(
FrameInput input, {
required Iterable<FramePatternModel> knownPatterns,
}) {
if (input.framedText.isEmpty) {
return const FrameValidationResult(
status: FrameValidationStatus.reject,
);
}
if (input.level == FrameLevel.l0 || input.level == FrameLevel.l1) {
final suggestions = _buildSuggestions(input.framedText, knownPatterns);
return FrameValidationResult(
status: FrameValidationStatus.reject,
suggestions: suggestions,
);
}
final hits = detectAvoidanceKeywords(input.framedText, knownPatterns);
if (hits.isEmpty) {
return const FrameValidationResult(status: FrameValidationStatus.accept);
}
final suggestions = _buildSuggestionsFromHits(hits);
return FrameValidationResult(
status: FrameValidationStatus.warn,
avoidanceHits: hits,
suggestions: suggestions,
);
}
List<AvoidanceHit> detectAvoidanceKeywords(
String text,
Iterable<FramePatternModel> patterns,
) {
final hits = <AvoidanceHit>[];
final seen = <String>{};
for (final p in patterns) {
var idx = text.indexOf(p.avoidanceKeyword);
while (idx >= 0) {
final key = '$idx:${p.avoidanceKeyword}';
if (!seen.contains(key)) {
seen.add(key);
hits.add(AvoidanceHit(
keyword: p.avoidanceKeyword,
startIndex: idx,
endIndex: idx + p.avoidanceKeyword.length,
source: p,
));
}
idx = text.indexOf(p.avoidanceKeyword, idx + 1);
}
}
return hits;
}
List<FrameSuggestion> _buildSuggestions(
String text,
Iterable<FramePatternModel> patterns,
) {
final relevant = patterns
.where((p) => text.contains(p.avoidanceKeyword))
.toList();
// If no keyword matches (L0 with no detectable avoidance), suggest from
// 'general' domain.
final pool = relevant.isEmpty
? patterns.where((p) => p.domain == 'general').toList()
: relevant;
return _expandSuggestions(pool);
}
List<FrameSuggestion> _buildSuggestionsFromHits(List<AvoidanceHit> hits) {
final unique = <String, FramePatternModel>{};
for (final h in hits) {
unique[h.source.id] = h.source;
}
return _expandSuggestions(unique.values.toList());
}
List<FrameSuggestion> _expandSuggestions(List<FramePatternModel> sources) {
final out = <FrameSuggestion>[];
for (final p in sources) {
out.add(FrameSuggestion(
level: FrameLevel.l2,
text: p.l2Suggestion,
source: p,
));
if (p.l3Identity != null) {
out.add(FrameSuggestion(
level: FrameLevel.l3,
text: p.l3Identity!,
source: p,
));
}
if (out.length >= 5) break;
}
return out.take(5).toList();
}