@@ -16,7 +16,7 @@ def __init__(self, reason: str):
1616
1717@dataclass (frozen = True )
1818class Budget :
19- max_seconds : int = 30
19+ max_seconds : int = 120
2020 max_draft_chars : int = 900
2121 max_risks : int = 5
2222 max_required_changes : int = 5
@@ -184,11 +184,45 @@ def _is_enforceable_required_change(item: str) -> bool:
184184
185185
186186def _contains_normalized_phrase (* , text : str , phrase : str ) -> bool :
187- normalized_text = _normalize_space (text ).lower ()
188- normalized_phrase = _normalize_space (phrase ).lower ()
187+ # Compare using token-like normalization so punctuation differences
188+ # (e.g. trailing dots/commas) do not cause false negatives.
189+ normalized_text = re .sub (r"[^a-z0-9% ]+" , " " , _normalize_space (text ).lower ())
190+ normalized_phrase = re .sub (r"[^a-z0-9% ]+" , " " , _normalize_space (phrase ).lower ())
191+ normalized_text = " " .join (normalized_text .split ())
192+ normalized_phrase = " " .join (normalized_phrase .split ())
189193 return normalized_phrase in normalized_text
190194
191195
196+ def _remove_phrase_occurrences (* , text : str , phrase : str ) -> str :
197+ cleaned = text
198+ normalized_phrase = _normalize_space (phrase ).strip ()
199+ if not normalized_phrase :
200+ return cleaned
201+
202+ variants = {normalized_phrase , normalized_phrase .rstrip (".!?" )}
203+ for variant in variants :
204+ if not variant :
205+ continue
206+ cleaned = re .sub (re .escape (variant ), "" , cleaned , flags = re .IGNORECASE )
207+
208+ cleaned = re .sub (r"\s+\." , "." , cleaned )
209+ cleaned = re .sub (r"[ \t]{2,}" , " " , cleaned )
210+ cleaned = re .sub (r"\n{3,}" , "\n \n " , cleaned )
211+ return cleaned .strip ()
212+
213+
214+ def _append_phrase_sentence (* , text : str , phrase : str ) -> str :
215+ sentence = _normalize_space (phrase ).strip ()
216+ if not sentence :
217+ return text
218+
219+ out = text .rstrip ()
220+ if out and out [- 1 ] not in ".!?" :
221+ out += "."
222+ separator = "\n \n " if "\n \n " in out else " "
223+ return (out + separator + sentence ).strip ()
224+
225+
192226
193227def validate_draft (draft : Any , * , max_chars : int ) -> str :
194228 if not isinstance (draft , str ) or not draft .strip ():
@@ -308,6 +342,28 @@ def enforce_execution_decision(self, decision: str) -> None:
308342 if decision not in self .allow_execution_decisions :
309343 raise StopRun (f"critique_decision_denied_execution:{ decision } " )
310344
345+ def apply_required_changes_fallback (self , * , text : str , required_changes : list [str ]) -> str :
346+ """
347+ Deterministic fallback for enforceable required changes:
348+ remove MUST_REMOVE/REMOVE phrases and append missing MUST_INCLUDE/ADD phrases.
349+ """
350+ candidate = (text or "" ).strip ()
351+ if not candidate :
352+ return candidate
353+
354+ phrase_rules = _extract_required_change_rules (required_changes )
355+ must_include = phrase_rules ["must_include" ]
356+ must_remove = phrase_rules ["must_remove" ]
357+
358+ for phrase in must_remove :
359+ candidate = _remove_phrase_occurrences (text = candidate , phrase = phrase )
360+
361+ for phrase in must_include :
362+ if not _contains_normalized_phrase (text = candidate , phrase = phrase ):
363+ candidate = _append_phrase_sentence (text = candidate , phrase = phrase )
364+
365+ return candidate .strip ()
366+
311367 def validate_revision (
312368 self ,
313369 * ,
0 commit comments