Commit ·
9674032
1
Parent(s): c1f8f9f
Phase 14: Fix StageLocker over-locking + PatchSet overlap resolution
Browse files- StageLocker: Grammar now only blocked by spelling locks when >50%
of the grammar diff falls INSIDE the spelling lock. Previously,
any boundary touch (even 1 char) blocked grammar entirely.
This fixes the Collision Dataset bug (32% pass rate) where
spelling locks bled into adjacent words and blocked grammar.
- PatchSet: Relaxed overlap resolution — patches with <50% overlap
of the smaller patch are allowed to coexist. This prevents
punctuation patches from dropping spelling patches on adjacent words.
Both fixes verified with unit tests.
- src/nlp/correction_patch.py +23 -5
- src/nlp/stage_locker.py +36 -0
src/nlp/correction_patch.py
CHANGED
|
@@ -96,6 +96,11 @@ class PatchSet:
|
|
| 96 |
"""
|
| 97 |
Single owner per range. Deterministic resolution.
|
| 98 |
Uses ORIGINAL coordinates for overlap detection.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 99 |
"""
|
| 100 |
sorted_patches = sorted(
|
| 101 |
self.patches,
|
|
@@ -106,11 +111,24 @@ class PatchSet:
|
|
| 106 |
resolved = []
|
| 107 |
|
| 108 |
for patch in sorted_patches:
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 114 |
resolved.append(patch)
|
| 115 |
claimed_ranges.append((patch.start_original, patch.end_original))
|
| 116 |
else:
|
|
|
|
| 96 |
"""
|
| 97 |
Single owner per range. Deterministic resolution.
|
| 98 |
Uses ORIGINAL coordinates for overlap detection.
|
| 99 |
+
|
| 100 |
+
Phase 14: Relaxed overlap — patches that touch at a boundary or
|
| 101 |
+
have minimal overlap (<= 50% of the smaller patch) are allowed
|
| 102 |
+
to coexist. This prevents punctuation patches from killing spelling
|
| 103 |
+
patches on adjacent words (e.g., 'المشروع.' punct vs 'في' spelling).
|
| 104 |
"""
|
| 105 |
sorted_patches = sorted(
|
| 106 |
self.patches,
|
|
|
|
| 111 |
resolved = []
|
| 112 |
|
| 113 |
for patch in sorted_patches:
|
| 114 |
+
has_substantial_overlap = False
|
| 115 |
+
for cs, ce in claimed_ranges:
|
| 116 |
+
# Check if there's any overlap at all
|
| 117 |
+
if patch.start_original < ce and patch.end_original > cs:
|
| 118 |
+
# Calculate overlap amount
|
| 119 |
+
overlap_start = max(patch.start_original, cs)
|
| 120 |
+
overlap_end = min(patch.end_original, ce)
|
| 121 |
+
overlap_width = overlap_end - overlap_start
|
| 122 |
+
# Compare to the smaller patch's width
|
| 123 |
+
patch_width = max(1, patch.end_original - patch.start_original)
|
| 124 |
+
claimed_width = max(1, ce - cs)
|
| 125 |
+
smaller_width = min(patch_width, claimed_width)
|
| 126 |
+
overlap_ratio = overlap_width / smaller_width
|
| 127 |
+
if overlap_ratio > 0.5:
|
| 128 |
+
has_substantial_overlap = True
|
| 129 |
+
break
|
| 130 |
+
|
| 131 |
+
if not has_substantial_overlap:
|
| 132 |
resolved.append(patch)
|
| 133 |
claimed_ranges.append((patch.start_original, patch.end_original))
|
| 134 |
else:
|
src/nlp/stage_locker.py
CHANGED
|
@@ -81,6 +81,13 @@ class StageLocker:
|
|
| 81 |
is_locked_for(0, 5, 'grammar') on spelling lock → False (grammar > spelling)
|
| 82 |
is_locked_for(0, 5, 'punctuation') on spelling lock → True (spelling > punctuation)
|
| 83 |
is_locked_for(0, 5, 'grammar') on protection lock → True (protection > grammar)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 84 |
"""
|
| 85 |
req_priority = STAGE_PRIORITY.get(requesting_stage, 0)
|
| 86 |
for ls, le, owner in self.locked_spans:
|
|
@@ -94,6 +101,35 @@ class StageLocker:
|
|
| 94 |
f"owner={owner}({owner_priority})"
|
| 95 |
)
|
| 96 |
return True # Blocked: owner is same or higher priority
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 97 |
else:
|
| 98 |
if PIPELINE_DEBUG:
|
| 99 |
logger.debug(
|
|
|
|
| 81 |
is_locked_for(0, 5, 'grammar') on spelling lock → False (grammar > spelling)
|
| 82 |
is_locked_for(0, 5, 'punctuation') on spelling lock → True (spelling > punctuation)
|
| 83 |
is_locked_for(0, 5, 'grammar') on protection lock → True (protection > grammar)
|
| 84 |
+
|
| 85 |
+
Phase 14 (StageLocker relaxation):
|
| 86 |
+
Grammar requesting on a spelling lock uses RELAXED overlap:
|
| 87 |
+
only blocked if >50% of the grammar diff falls inside the spelling lock.
|
| 88 |
+
This allows grammar to fix words ADJACENT to spelling-fixed words
|
| 89 |
+
(the "Collision Dataset" bug — spelling locks were bleeding into
|
| 90 |
+
neighboring words and blocking valid grammar corrections).
|
| 91 |
"""
|
| 92 |
req_priority = STAGE_PRIORITY.get(requesting_stage, 0)
|
| 93 |
for ls, le, owner in self.locked_spans:
|
|
|
|
| 101 |
f"owner={owner}({owner_priority})"
|
| 102 |
)
|
| 103 |
return True # Blocked: owner is same or higher priority
|
| 104 |
+
|
| 105 |
+
# ── Phase 14: Relaxed overlap for grammar on spelling locks ──
|
| 106 |
+
# Grammar (3) outranks spelling (2) in priority, BUT we still
|
| 107 |
+
# want to protect the EXACT word spelling fixed. The relaxation:
|
| 108 |
+
# allow grammar if the overlap is < 50% of the grammar diff's
|
| 109 |
+
# width (i.e., the grammar diff is mostly OUTSIDE the lock).
|
| 110 |
+
# This catches boundary touches and small overlaps that happen
|
| 111 |
+
# when spelling locks bleed past word boundaries.
|
| 112 |
+
if requesting_stage == 'grammar' and owner == 'spelling':
|
| 113 |
+
overlap_start = max(start, ls)
|
| 114 |
+
overlap_end = min(end, le)
|
| 115 |
+
overlap_width = max(0, overlap_end - overlap_start)
|
| 116 |
+
diff_width = max(1, end - start)
|
| 117 |
+
overlap_ratio = overlap_width / diff_width
|
| 118 |
+
if overlap_ratio > 0.5:
|
| 119 |
+
# Grammar diff is mostly inside spelling lock — block
|
| 120 |
+
logger.info(
|
| 121 |
+
f"[StageLocker] Grammar blocked (overlap={overlap_ratio:.0%}): "
|
| 122 |
+
f"grammar[{start}:{end}] vs spelling[{ls}:{le}]"
|
| 123 |
+
)
|
| 124 |
+
return True
|
| 125 |
+
else:
|
| 126 |
+
# Grammar diff is mostly outside — allow
|
| 127 |
+
if PIPELINE_DEBUG or overlap_width > 0:
|
| 128 |
+
logger.info(
|
| 129 |
+
f"[StageLocker] Grammar ALLOWED (overlap={overlap_ratio:.0%}): "
|
| 130 |
+
f"grammar[{start}:{end}] vs spelling[{ls}:{le}]"
|
| 131 |
+
)
|
| 132 |
+
continue
|
| 133 |
else:
|
| 134 |
if PIPELINE_DEBUG:
|
| 135 |
logger.debug(
|