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 avoidanceHits; final List 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 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 detectAvoidanceKeywords( String text, Iterable patterns, ) { final hits = []; final seen = {}; 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 _buildSuggestions( String text, Iterable 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 _buildSuggestionsFromHits(List hits) { final unique = {}; for (final h in hits) { unique[h.source.id] = h.source; } return _expandSuggestions(unique.values.toList()); } List _expandSuggestions(List sources) { final out = []; 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(); }