"""Safe post-parse normalizers for IOL-AI 2026 (v8+). Only match_letters / text_to_num. Never force list arity. """ from __future__ import annotations import re from typing import List def normalize_match_letter(ans: str) -> str: ans = ans.strip() m = re.fullmatch(r"[\(\[]?([A-Za-z])[\)\]]?[.)]?", ans) if m: return m.group(1).upper() tokens = re.findall(r"\b([A-Za-z])\b", ans) if tokens: return tokens[-1].upper() m = re.search(r"[A-Za-z]", ans) return m.group(0).upper() if m else ans def normalize_text_to_num(ans: str) -> str: a = re.sub(r"(?i)^(answer|ans|result)\s*[:=]\s*", "", ans.strip()).strip() if re.fullmatch(r"[\d\s+\-*/^=()]+", a.replace(",", "")): a = a.replace(",", "").replace(" ", "") if "=" in a and " = " not in a: a = a.replace("=", " = ") return a.strip() m = re.search(r"\d+", a) return m.group(0) if m and len(a) < 40 else a def safe_normalize_answers(answers: List[str], task_type: str) -> List[str]: """Per-line only. Does not pad/truncate. No-op for other task types.""" task_type = (task_type or "").strip().lower() out: List[str] = [] for a in answers: a = a.strip() if task_type == "match_letters": a = normalize_match_letter(a) elif task_type == "text_to_num": a = normalize_text_to_num(a) out.append(a) return out