"""Rewrite planning — decide safety and ranked template candidates.""" from __future__ import annotations from app.engine.classify import classify_sentence, is_rewriteable_type from app.engine.models import RewritePlan, SentenceSlots, TemplateCandidate from app.engine.parse import extract_slots from app.engine.templates import can_swap_initial_subordinate, rank_templates from app.engine.voice import active_to_passive def build_plan(text: str, *, min_confidence: float = 0.55) -> RewritePlan: """Create a rewrite plan without generating text yet.""" raw = (text or "").strip() if not raw: return RewritePlan(safe=False, skip_reason="empty") kind = classify_sentence(raw) if kind == "because_clause": return RewritePlan( safe=True, template_id="because_front", candidates=[TemplateCandidate("because_front", 0.85)], confidence=0.85, slots=SentenceSlots(text=raw, sentence_type=kind, confidence=0.85), movable=["because_clause"], fixed_spans=[], ) if kind == "complex": if can_swap_initial_subordinate(raw): slots = extract_slots(raw) slots.confidence = 0.76 return RewritePlan( safe=True, template_id="complex_clause_swap", candidates=[TemplateCandidate("complex_clause_swap", 0.76)], confidence=0.76, slots=slots, movable=["subordinate_clause"], fixed_spans=list(slots.entities), ) # Other complex prose still reaches paraphrase/lexical/ensure via finalize. passive_candidate = active_to_passive(raw) if passive_candidate: slots = extract_slots(raw) slots.confidence = 0.7 return RewritePlan( safe=True, template_id="active_to_passive", candidates=[TemplateCandidate("active_to_passive", 0.7)], confidence=0.7, slots=slots, movable=["subject", "object"], fixed_spans=list(slots.entities), ) return RewritePlan( safe=False, skip_reason="complex", confidence=0.0, slots=SentenceSlots(text=raw, sentence_type=kind), ) if not is_rewriteable_type(kind): passive_candidate = active_to_passive(raw) if passive_candidate: slots = extract_slots(raw) slots.confidence = 0.7 return RewritePlan( safe=True, template_id="active_to_passive", candidates=[TemplateCandidate("active_to_passive", 0.7)], confidence=0.7, slots=slots, movable=["subject", "object"], fixed_spans=list(slots.entities), ) return RewritePlan( safe=False, skip_reason=kind, confidence=0.0, slots=SentenceSlots(text=raw, sentence_type=kind), ) slots = extract_slots(raw) if "low_confidence" in slots.reasons or slots.confidence < min_confidence: return RewritePlan( safe=False, skip_reason="low_confidence", confidence=slots.confidence, slots=slots, ) if any(r.startswith("skip:") for r in slots.reasons): return RewritePlan( safe=False, skip_reason=slots.reasons[0], confidence=slots.confidence, slots=slots, ) candidates = rank_templates(slots) structural_ids = {candidate.template_id for candidate in candidates} if not structural_ids or structural_ids <= {"subj_verb_object"}: if active_to_passive(raw): candidates.append(TemplateCandidate("active_to_passive", 0.7)) if not candidates: return RewritePlan( safe=False, skip_reason="no_template", confidence=slots.confidence, slots=slots, ) # Filter by min confidence candidates = [c for c in candidates if c.confidence >= min_confidence] if not candidates: return RewritePlan( safe=False, skip_reason="low_template_confidence", confidence=slots.confidence, slots=slots, ) fixed = list(slots.entities) if slots.negation: fixed.append(slots.negation) movable = [x for x in ("time", "manner", "place") if getattr(slots, x, "")] return RewritePlan( safe=True, slots=slots, template_id=candidates[0].template_id, candidates=candidates, fixed_spans=fixed, movable=movable, confidence=candidates[0].confidence, )