youssefreda9 commited on
Commit
03cd504
·
1 Parent(s): f1671d1

FIX-33/34/35: Fix infinite loop — grammar corruption + spelling conflict

Browse files

FIX-33: Grammar rules fix_prepositions_advanced was corrupting
الامتحان→الامتحين. The regex blindly replaced ان→ين after prepositions,
but الامتحان is a root noun not a dual. Added blocklist of ~60 common
root-form nouns and skip all ال-prefixed words ending in ان.

FIX-34: Remove auto-re-analysis from Apply All in editor.js.
When spelling and grammar disagree (spelling strips ذهبن→ذهب,
grammar re-adds ذهبوا→ذهبن), the auto-re-analysis creates an
infinite loop of contradictory suggestions.

FIX-35: Spelling no longer strips valid conjugation suffixes.
The insertion fix was accepting ذهبن→ذهب (removing final ن) as
'extra char removed'. But ن is a feminine plural verb suffix.
Now blocks removal of suffix chars (ن,ت,ا,ي,ة,و,ه) from word
endings, preventing spelling from undoing grammar corrections.

Files changed (3) hide show
  1. src/app.py +20 -0
  2. src/js/editor.js +4 -4
  3. src/nlp/grammar/grammar_rules.py +32 -2
src/app.py CHANGED
@@ -1062,6 +1062,26 @@ def _is_small_spelling_change(orig_word, corr_word, vocab_manager=None):
1062
  _orig_oov = not vocab_manager.is_iv(orig_word)
1063
  _corr_iv = vocab_manager.is_iv(corr_word)
1064
  if _orig_oov and _corr_iv:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1065
  logger.info(
1066
  f"[SPELLING] Insertion fix accepted (OOV→IV): "
1067
  f"'{orig_word}'→'{corr_word}' (extra char removed)"
 
1062
  _orig_oov = not vocab_manager.is_iv(orig_word)
1063
  _corr_iv = vocab_manager.is_iv(corr_word)
1064
  if _orig_oov and _corr_iv:
1065
+ # FIX-35: Don't strip valid conjugation suffixes.
1066
+ # If the "extra char" is at the END and is a common
1067
+ # Arabic verb/noun suffix, this is likely a grammar
1068
+ # conjugation, not a typo. Skip it.
1069
+ _CONJUGATION_SUFFIXES = {'ن', 'ت', 'ا', 'ي', 'ة', 'و', 'ه'}
1070
+ _removed_char = None
1071
+ for _di2 in range(len(orig_word)):
1072
+ if orig_word[:_di2] + orig_word[_di2 + 1:] == corr_word:
1073
+ _removed_char = orig_word[_di2]
1074
+ _removed_pos = _di2
1075
+ break
1076
+ if (_removed_char in _CONJUGATION_SUFFIXES
1077
+ and _removed_pos == len(orig_word) - 1
1078
+ and len(corr_word) >= 3):
1079
+ logger.info(
1080
+ f"[SPELLING] Rejected suffix strip: "
1081
+ f"'{orig_word}'→'{corr_word}' "
1082
+ f"(removing suffix '{_removed_char}' likely strips conjugation)"
1083
+ )
1084
+ return 0.0
1085
  logger.info(
1086
  f"[SPELLING] Insertion fix accepted (OOV→IV): "
1087
  f"'{orig_word}'→'{corr_word}' (extra char removed)"
src/js/editor.js CHANGED
@@ -719,12 +719,12 @@ function applyAllSuggestions() {
719
  updateSuggestionsList([]);
720
  if (typeof showToast === 'function') showToast('✓ تم تطبيق ' + suggestions.length + ' تصحيح');
721
  } finally {
722
- // FIX-32: Delay guard reset until AFTER re-analysis fires,
723
- // preventing input events from triggering double analysis.
724
  setTimeout(() => { _isApplyingSuggestion = false; }, 400);
725
  }
726
- // P2/User Request: Auto re-analyze after applying all suggestions
727
- setTimeout(() => { analyzeText(); }, 300);
 
728
  }
729
 
730
  function clearEditor() {
 
719
  updateSuggestionsList([]);
720
  if (typeof showToast === 'function') showToast('✓ تم تطبيق ' + suggestions.length + ' تصحيح');
721
  } finally {
722
+ // FIX-32: Delay guard reset
 
723
  setTimeout(() => { _isApplyingSuggestion = false; }, 400);
724
  }
725
+ // FIX-34: Do NOT auto-re-analyze after Apply All.
726
+ // This caused an infinite loop when spelling/grammar disagree.
727
+ // User can click "Analyze" manually if they want to re-check.
728
  }
729
 
730
  function clearEditor() {
src/nlp/grammar/grammar_rules.py CHANGED
@@ -201,11 +201,41 @@ class ArabicGrammarGuard:
201
 
202
  return text
203
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
204
  def fix_prepositions_advanced(self, text):
205
  # Allow conjunctions (و، ف) before prepositions
206
  # (في المهندسون) -> (في المهندسين)
207
- # Require stem >= 4 chars to avoid matching root-level ان endings (الامتحان, الإنسان, etc.)
208
- text = re.sub(r'\b([وف]?(?:في|من|إلى|على|عن|حتى))\s+([أ-ي]{4,})(ون|ان)\b', r'\1 \2ين', text)
 
 
 
 
 
 
 
 
 
 
 
 
 
209
 
210
  # (وبالمبرمجون) -> (وبالمبرمجين)
211
  text = re.sub(r'\b([وف]?[بلكف])ال([أ-ي]{4,})(ون|ان)\b', r'\1ال\2ين', text)
 
201
 
202
  return text
203
 
204
+ # FIX-33: Words ending in ان that are root-form nouns, NOT duals/plurals.
205
+ # These must never have ان→ين replacement.
206
+ _PREP_BLOCKLIST = {
207
+ 'الامتحان', 'امتحان', 'الإنسان', 'إنسان', 'انسان', 'الانسان',
208
+ 'الميدان', 'ميدان', 'البرلمان', 'برلمان', 'السلطان', 'سلطان',
209
+ 'العنوان', 'عنوان', 'الديوان', 'ديوان', 'البستان', 'بستان',
210
+ 'البنيان', 'بنيان', 'الإيمان', 'إيمان', 'ايمان', 'الايمان',
211
+ 'الأمان', 'أمان', 'امان', 'الامان', 'العدوان', 'عدوان',
212
+ 'البيان', 'بيان', 'البرهان', 'برهان', 'الشيطان', 'شيطان',
213
+ 'الأذان', 'أذان', 'السودان', 'لبنان', 'عمان', 'الأردن',
214
+ 'الحيوان', 'حيوان', 'القرآن', 'قرآن', 'الدخان', 'دخان',
215
+ 'المكان', 'مكان', 'الزمان', 'زمان', 'الجدران', 'جدران',
216
+ 'النيران', 'نيران', 'الألوان', 'ألوان', 'البلدان', 'بلدان',
217
+ 'الأوطان', 'أوطان', 'الأبدان', 'أبدان', 'الأركان', 'أركان',
218
+ 'الفرسان', 'فرسان', 'الغزلان', 'غزلان', 'القضبان', 'قضبان',
219
+ }
220
+
221
  def fix_prepositions_advanced(self, text):
222
  # Allow conjunctions (و، ف) before prepositions
223
  # (في المهندسون) -> (في المهندسين)
224
+ # FIX-33: Use callback to skip root-form nouns ending in ان
225
+ def _prep_replace(m):
226
+ prep = m.group(1)
227
+ stem = m.group(2)
228
+ suffix = m.group(3)
229
+ full_word = stem + suffix
230
+ # Skip words in blocklist (root nouns, not duals)
231
+ if full_word in self._PREP_BLOCKLIST:
232
+ return m.group(0) # return unchanged
233
+ # Skip ال-prefixed words ending in ان — almost always root nouns
234
+ if stem.startswith('ال') and suffix == 'ان':
235
+ return m.group(0) # return unchanged
236
+ return f'{prep} {stem}ين'
237
+
238
+ text = re.sub(r'\b([وف]?(?:في|من|إلى|على|عن|حتى))\s+([أ-ي]{4,})(ون|ان)\b', _prep_replace, text)
239
 
240
  # (وبالمبرمجون) -> (وبالمبرمجين)
241
  text = re.sub(r'\b([وف]?[بلكف])ال([أ-ي]{4,})(ون|ان)\b', r'\1ال\2ين', text)