"""Explicit high-coverage rule fallback for declarative sentences.""" from __future__ import annotations import re from app.pipeline.nlp import get_nlp _SUBJECT_DEPS = frozenset({"nsubj", "nsubjpass", "csubj", "csubjpass"}) _PROTECTED = re.compile(r"ZZPROTECTED[A-Z]+\d+ZZ", re.I) _QUOTES = frozenset({'"', "“", "”", "‘", "’"}) def _lower_continuation(text: str, subject_head) -> str: value = text.strip() if not value: return value if ( subject_head.pos_ == "PROPN" or subject_head.text == "I" ): return value return value[:1].lower() + value[1:] def force_cleft_rewrite(text: str) -> str | None: """Create a subject cleft while retaining the original predicate.""" raw = (text or "").strip() if ( not raw or raw.endswith("?") or any(mark in raw for mark in _QUOTES) or _PROTECTED.search(raw) ): return None nlp = get_nlp() if nlp is None: return None try: doc = nlp(raw) except Exception: return None root = next((token for token in doc if token.dep_ == "ROOT"), None) subjects = [token for token in doc if token.dep_ in _SUBJECT_DEPS] if root is None: return None # Gerund subjects with coordinated objects are often parsed as the root. # The first finite auxiliary provides a reliable dynamic boundary. finite_aux = next( ( token for token in doc if token.pos_ == "AUX" and token.i > 0 and token.head.i > token.i ), None, ) if doc[0].tag_ == "VBG" and finite_aux is not None: subject = doc[0] start = doc[0].idx end = finite_aux.idx else: if not subjects: return None subject = next( ( token for token in subjects if token.head == root or root in tuple(token.ancestors) ), subjects[0], ) subject_tokens = sorted(subject.subtree, key=lambda token: token.i) if not subject_tokens: return None start = subject_tokens[0].idx last = subject_tokens[-1] end = last.idx + len(last.text) prefix = raw[:start].strip() subject_text = raw[start:end].strip(" ,") predicate = raw[end:].strip() predicate = predicate.lstrip(" ,") terminal = "." if predicate.endswith(("!", ".")): terminal = predicate[-1] predicate = predicate[:-1].rstrip() if not subject_text or not predicate: return None if predicate.lower().startswith(("is it ", "was it ")): return None subject_text = _lower_continuation(subject_text, subject) core = f"it is {subject_text} that {predicate}{terminal}" if prefix: prefix = prefix.rstrip(" ,") candidate = f"{prefix}, {core}" else: candidate = core[:1].upper() + core[1:] candidate = re.sub(r"\s+", " ", candidate).strip() if candidate.lower().rstrip(".!?") == raw.lower().rstrip(".!?"): return None return candidate