#@title 7. VALIDATION / RANKING / ENGINE v4.3 — confidence-gated Unicode enumeration judge # Complete replacement for Cell 7. # Compatible with Cell 5 v4.1, Cell 5A v3.4, Cell 6 v5.1. # Accepts arbitrary Unicode text safely; linguistic routing quality still depends # on the loaded spaCy model and available annotations. # Run Cell 7A and Cell 7B again after this cell. import json import re import time import unicodedata from collections import Counter from dataclasses import asdict from difflib import SequenceMatcher from pathlib import Path EXPECTED_CELL7_RULE_COUNT = 30 _REQUIRED_CELL7 = [ "STATE", "CFG", "RULES", "Analyzer", "Generator", "Scanner", "Candidate", "SentenceResult", "DocumentResult", "PROFILES", "protect", "optional", "nlp", "sub", "sent_split", "surface_key", "realization_issues", ] _missing_cell7 = [name for name in _REQUIRED_CELL7 if name not in globals()] if _missing_cell7: raise RuntimeError( "Cell 7 cannot run. Missing: " + ", ".join(_missing_cell7) + ". Run Cells 1–6 v5.1 in order first." ) if len(RULES) != EXPECTED_CELL7_RULE_COUNT: raise RuntimeError( f"Cell 7 expected {EXPECTED_CELL7_RULE_COUNT} rules but found {len(RULES)}." ) # Backward compatibility for older tests/helpers. _surface_key = surface_key NEG = {"not", "no", "never", "without", "neither", "nor"} HEDGE = { "may", "might", "could", "can", "possibly", "potentially", "suggest", "suggests", "indicate", "indicates", "likely", "approximately", "about", "must", "should", "would", "will", } def _ensure_state(): if not isinstance(STATE, dict): raise RuntimeError("STATE must be a dictionary.") STATE.setdefault("warnings", []) STATE.setdefault("start", None) return STATE def sr(a, b): return SequenceMatcher( None, re.sub(r"\s+", " ", str(a).lower()).strip(), re.sub(r"\s+", " ", str(b).lower()).strip(), ).ratio() def token_change(a, b): A = re.findall(r"[A-Za-z0-9]+(?:[-'][A-Za-z0-9]+)*", str(a).lower()) B = re.findall(r"[A-Za-z0-9]+(?:[-'][A-Za-z0-9]+)*", str(b).lower()) return 1 - SequenceMatcher(None, A, B).ratio() def nli_pair(a, b): _ensure_state() bundle = optional("nli") if not bundle: return None try: import torch tok, model = bundle x = tok(a, b, return_tensors="pt", truncation=True, max_length=512) dev = next(model.parameters()).device x = {k: v.to(dev) for k, v in x.items()} with torch.no_grad(): prob = torch.softmax(model(**x).logits[0], -1).cpu().numpy() labels = { str(model.config.id2label[i]).lower(): float(prob[i]) for i in range(len(prob)) } return { "entailment": max([v for k, v in labels.items() if "entail" in k] or [0]), "contradiction": max([v for k, v in labels.items() if "contrad" in k] or [0]), } except Exception as exc: STATE["warnings"].append(f"NLI scoring failed: {exc}") return None def stsim(a, b): _ensure_state() model = optional("sts") if not model: return None try: from sentence_transformers.util import cos_sim emb = model.encode([a, b], convert_to_tensor=True, normalize_embeddings=True) return float(cos_sim(emb[0], emb[1]).item()) except Exception as exc: STATE["warnings"].append(f"STS scoring failed: {exc}") return None # ===================================================================== # RELATION / INVENTORY HELPERS # ===================================================================== def _safe_doc(text): try: return nlp()(str(text or "")) except Exception: return None def _neg_inventory(text, doc=None): """Compare polarity markers by lexical class, not dependency owner.""" lo = str(text or "").lower() return Counter( token for token in ["not", "no", "never", "without", "neither", "nor"] if re.search(r"(?.+?)\s+but\s+to\s+(?P.+?)[.]?$", original, ) if not match: return False x = surface_key(match["x"]) y = surface_key(match["y"]) z = surface_key(candidate) return ( x in z and y in z and ( "rather than" in candidate.lower() or ("does not" in candidate.lower() and "instead" in candidate.lower()) ) ) def _unicode_normalize(text): """Canonical Unicode normalization for comparison, never for output.""" value = unicodedata.normalize("NFKC", str(text or "")) translations = str.maketrans({ "–": "-", "—": "-", "−": "-", "‑": "-", "’": "'", "‘": "'", "“": '"', "”": '"', "\u00a0": " ", }) return re.sub(r"\s+", " ", value.translate(translations)).strip().casefold() def _unicode_tokens(text): """ Tokenize letters/numbers across Unicode scripts without ASCII loss. Punctuation, including every dash/apostrophe form, is treated as a comparison boundary. This makes these equivalent for inventory checks: re-examination == re examination == re–examination matched-data == matched data The original candidate text is never modified. """ value = _unicode_normalize(text) output = [] current = [] for char in value: category = unicodedata.category(char) if category[0] in {"L", "N", "M"}: current.append(char) elif current: token = "".join(current) if token: output.append(token) current = [] if current: token = "".join(current) if token: output.append(token) return output def _noun_chunk_for_token(doc, token): try: chunks = [chunk for chunk in doc.noun_chunks if chunk.start <= token.i < chunk.end] except Exception: chunks = [] return min(chunks, key=lambda chunk: chunk.end - chunk.start) if chunks else None def _clean_member_text(text): value = re.sub(r"\s+", " ", str(text or "")).strip(" \t\r\n,;:.()[]{}") value = re.sub(r"^(?:and|or|nor)\s+", "", value, flags=re.I) return value.strip() def _dependency_enumeration_groups(doc): """Recover coordinated members from dependency heads/noun chunks.""" if doc is None or not doc.has_annotation("DEP"): return [] groups = [] seen = set() for token in doc: conjuncts = [child for child in token.children if child.dep_ == "conj"] if not conjuncts: continue members = [token] + sorted(conjuncts, key=lambda item: item.i) # Include chained conjuncts. cursor = 0 while cursor < len(members): for child in members[cursor].children: if child.dep_ == "conj" and child not in members: members.append(child) cursor += 1 members = sorted(set(members), key=lambda item: item.i) texts = [] for member in members: chunk = _noun_chunk_for_token(doc, member) if chunk is not None: text = chunk.text else: # Conservative local subtree: exclude other conjunct branches. blocked = {item.i for other in members if other is not member for item in other.subtree} indices = [ item.i for item in member.subtree if item.i not in blocked and item.dep_ not in {"cc", "punct"} ] if indices: text = doc[min(indices):max(indices) + 1].text else: text = member.text text = _clean_member_text(text) if text: texts.append(text) texts = list(dict.fromkeys(texts)) key = tuple(_unicode_normalize(item) for item in texts) if len(texts) >= 3 and key not in seen: seen.add(key) groups.append(texts) return groups def _analysis_enumeration_groups(analysis): """Accept analyzer groups only when each item is a plausible member.""" groups = [] for group in list(getattr(analysis, "enumerations", []) or []): cleaned = [_clean_member_text(item) for item in group] cleaned = [item for item in cleaned if item] # Reject boundary-corrupted pseudo-members containing multiple commas. if len(cleaned) >= 3 and all(item.count(",") <= 1 for item in cleaned): groups.append(cleaned) return groups def _top_level_segments(text): """Split on top-level commas while respecting brackets and Unicode text.""" segments = [] current = [] stack = [] pairs = {"(": ")", "[": "]", "{": "}"} closers = set(pairs.values()) for char in str(text or ""): if char in pairs: stack.append(pairs[char]) elif char in closers and stack and stack[-1] == char: stack.pop() if char == "," and not stack: segments.append("".join(current).strip()) current = [] else: current.append(char) if current: segments.append("".join(current).strip()) return segments def _surface_list_groups(text): """Conservative fallback; never cuts through hyphenated compounds.""" value = str(text or "").strip().rstrip(".?!") segments = _top_level_segments(value) if len(segments) < 3: return [] # Locate the final coordinator in the final segment. final = segments[-1] match = re.search(r"\s+(?:and|or|nor)\s+(.+)$", final, re.I | re.S) if not match: return [] last_member = _clean_member_text(match.group(1)) pre_final = _clean_member_text(final[:match.start()]) # Find the most plausible list start. Prefer explicit introducers. introducer = re.compile( r"\b(?:using|including|involving|such as|comprising|consisting of|covers?|" r"evaluated|assessed|measured|reviewed|identified|pre-register(?:ed)?|preregister(?:ed)?)\s+", re.I, ) first_index = 0 first_text = segments[0] matches = list(introducer.finditer(first_text)) if matches: first_member = _clean_member_text(first_text[matches[-1].end():]) else: # Without an introducer, avoid guessing a list start from the full clause. return [] middle = [_clean_member_text(item) for item in segments[1:-1]] members = [first_member] + middle if pre_final: members.append(pre_final) if last_member: members.append(last_member) members = [item for item in members if item] return [members] if len(members) >= 3 else [] def _plausible_enumeration_member(text): """ Reject boundary-corrupted pseudo-members without weakening true omission checks. A valid member may contain Unicode letters, numbers, spaces, apostrophes, and dash variants, but should not contain clause punctuation, a full sentence lead-in, or an excessive token span. """ value = _clean_member_text(text) tokens = _unicode_tokens(value) if not tokens or len(tokens) > 12: return False if any(mark in value for mark in [",", ";", ":", "."]): return False if re.match( r"(?is)^(?:we|i|they|he|she|it|the\s+(?:study|paper|article|report|" r"system|framework|team|model))\s+" r"(?:propose|proposes|introduce|introduces|present|presents|" r"evaluate|evaluates|assess|assesses|pre-register|preregister)\b", value, ): return False return True def _enumeration_groups(analysis, source_text, source_doc): """ Build only high-confidence list groups. Sources are tried independently instead of accepting the first nonempty extractor. Candidate groups are filtered for plausible member boundaries, verified against the source, de-duplicated, and ranked by compactness. """ candidate_groups = [] candidate_groups.extend(_dependency_enumeration_groups(source_doc)) candidate_groups.extend(_analysis_enumeration_groups(analysis)) candidate_groups.extend(_surface_list_groups(source_text)) source_tokens = _unicode_tokens(source_text) accepted = [] seen = set() for group in candidate_groups: stable_group = [] for raw_item in group: item = _clean_member_text(raw_item) if not _plausible_enumeration_member(item): continue item_tokens = _unicode_tokens(item) width = len(item_tokens) appears_in_source = any( source_tokens[index:index + width] == item_tokens for index in range(len(source_tokens) - width + 1) ) if appears_in_source: stable_group.append(item) # Preserve order and remove duplicate members. unique_group = [] member_seen = set() for item in stable_group: key = tuple(_unicode_tokens(item)) if key and key not in member_seen: member_seen.add(key) unique_group.append(item) normalized = tuple(tuple(_unicode_tokens(item)) for item in unique_group) if len(normalized) >= 3 and normalized not in seen: seen.add(normalized) accepted.append(unique_group) # Remove groups that are strict noisy supersets of a cleaner group. compact = [] token_sets = [ {tuple(_unicode_tokens(item)) for item in group} for group in accepted ] for index, group in enumerate(accepted): current = token_sets[index] noisier = any( other < current for other_index, other in enumerate(token_sets) if other_index != index and len(other) >= 3 ) if not noisier: compact.append(group) return compact def _member_present(member, candidate): """Require normalized member tokens in order, independent of punctuation.""" member_tokens = _unicode_tokens(member) candidate_tokens = _unicode_tokens(candidate) if not member_tokens: return True width = len(member_tokens) return any( candidate_tokens[index:index + width] == member_tokens for index in range(len(candidate_tokens) - width + 1) ) def _relation_signature(text): lo = str(text or "").lower() classes = { "cause": {"because", "therefore", "thus", "consequently", "hence", "as a result", "for this reason", "accordingly"}, "concession": {"although", "though", "despite", "nevertheless", "however", "even so"}, "condition": {"if", "unless", "whether"}, "simultaneous": {"while", "at the same time"}, "purpose": {"so that", "intended to", "designed to"}, "absence": {"without"}, "dependency": {"contingent on", "depend on", "depends on", "depended on"}, } return { key for key, words in classes.items() if any(re.search(r"(? {dict(candidate_inventory)}", ) missing_terms = [ term for term in hard_terms if re.search(r"(? {dict(candidate_neg)}", ) # ------------------------------------------------------------- # Modality / hedging # ------------------------------------------------------------- source_modals = _modals(source_doc, original) candidate_modals = _modals(candidate_doc, revised) checks["modality"] = (dict(source_modals), dict(candidate_modals)) if source_modals != candidate_modals: reject( "MODALITY_REJECTION", f"Modality/hedging changed: {dict(source_modals)} -> {dict(candidate_modals)}", ) # ------------------------------------------------------------- # Enumeration preservation across the WHOLE candidate text. # A split/reordered candidate is allowed if every source list member # survives in token order somewhere in the complete candidate. # ------------------------------------------------------------- enumeration_groups = _enumeration_groups(analysis, original, source_doc) missing_by_group = [] for group_index, group in enumerate(enumeration_groups, start=1): missing = [member for member in group if not _member_present(member, revised)] if missing: missing_by_group.append({"group": group_index, "missing": missing}) checks["enumeration_groups"] = enumeration_groups checks["enumeration_missing"] = missing_by_group if missing_by_group: detail = " | ".join( f"group {entry['group']}: " + ", ".join(entry["missing"]) for entry in missing_by_group ) reject( "ENUMERATION_REJECTION", "Enumeration members missing: " + detail, ) # ------------------------------------------------------------- # Subject/object orientation # ------------------------------------------------------------- source_triples = _triples(source_doc) candidate_triples = _triples(candidate_doc) checks["source_triples"] = source_triples checks["candidate_triples"] = candidate_triples for subject, verb, obj in source_triples: if subject != obj and any( cs == obj and cq == subject and cv == verb for cs, cv, cq in candidate_triples ): reject( "SEMANTIC_REJECTION", f"Subject/object orientation reversed for '{verb}'.", ) if not source_triples and not candidate_triples: simple = ( r"(?is)^\s*(?P[A-Z][A-Za-z0-9_-]*)\s+" r"(?P\w+(?:ed|s))\s+(?P[A-Z][A-Za-z0-9_-]*)" ) source_match = re.match(simple, original) candidate_match = re.match(simple, revised) if ( source_match and candidate_match and source_match["s"].lower() == candidate_match["o"].lower() and source_match["o"].lower() == candidate_match["s"].lower() and source_match["v"].lower() == candidate_match["v"].lower() ): reject( "SEMANTIC_REJECTION", f"Subject/object orientation reversed for '{source_match['v']}'.", ) # ------------------------------------------------------------- # Discourse relations # ------------------------------------------------------------- source_relations = _relation_signature(original) candidate_relations = _relation_signature(revised) checks["relations"] = (sorted(source_relations), sorted(candidate_relations)) if "concession" in candidate_relations - source_relations: reject( "SEMANTIC_REJECTION", f"Candidate introduced concession: {sorted(source_relations)} -> {sorted(candidate_relations)}", ) if "cause" in source_relations and not ({"cause", "purpose"} & candidate_relations): reject( "SEMANTIC_REJECTION", f"Causal/result relation disappeared: {sorted(source_relations)} -> {sorted(candidate_relations)}", ) if "absence" in source_relations and "absence" not in candidate_relations: reject( "POLARITY_REJECTION", "The source's 'without' relation disappeared from the candidate.", ) if "dependency" in source_relations and "dependency" not in candidate_relations: reject( "SEMANTIC_REJECTION", "The source dependency/contingency relation disappeared.", ) # ------------------------------------------------------------- # Optional soft semantic signals # ------------------------------------------------------------- sts = stsim(original, revised) forward = nli_pair(original, revised) reverse = nli_pair(revised, original) checks.update(sts=sts, nli_forward=forward, nli_reverse=reverse) if sts is not None and sts < CFG.min_sts: checks["sts_warning"] = f"Low soft STS signal: {sts:.3f}" if forward and reverse: if max(forward["contradiction"], reverse["contradiction"]) > .55: reject("SEMANTIC_REJECTION", "Bidirectional NLI found contradiction.") elif min(forward["entailment"], reverse["entailment"]) < CFG.min_nli: checks["nli_warning"] = ( f"Weak entailment {forward['entailment']:.3f}/{reverse['entailment']:.3f}; " "manual review advised." ) # ------------------------------------------------------------- # Rewrite magnitude # ------------------------------------------------------------- surface = sr(original, revised) change = token_change(original, revised) checks.update(surface=surface, change=change, missing_terms=missing_terms) minimum_change = {"Minimal": .035, "Balanced": .065, "Structural": .09}[mode] if surface_key(original) == surface_key(revised): reject("INSUFFICIENT_REWRITE", "Candidate is identical to the source.") elif change < minimum_change: reject( "INSUFFICIENT_REWRITE", f"Token change {change:.3f} is below {mode} threshold {minimum_change:.3f}.", ) # ------------------------------------------------------------- # Optional local LanguageTool comparison # ------------------------------------------------------------- grammar_tool = optional("lt") if getattr(CFG, "lt", False) else None if grammar_tool: try: delta = len(grammar_tool.check(revised)) - len(grammar_tool.check(original)) checks["grammar_delta"] = delta if delta > 2: reject( "GRAMMAR_REJECTION", f"Candidate introduced {delta} additional LanguageTool findings.", ) except Exception as exc: checks["grammar_warning"] = str(exc) checks.update( failures=failures, rejection_reasons=reasons, accepted=not failures, ) return not failures, failures, checks # ===================================================================== # RANKER # ===================================================================== class Ranker: def rank(self, original, candidates, analysis, profile, user_terms, mode): good = [] bad = [] target = {"Minimal": .12, "Balanced": .24, "Structural": .38}[mode] for candidate in candidates: ok, failures, checks = Validator().validate( original, candidate, analysis, profile, user_terms, mode ) change = checks.get("change", token_change(original, candidate.text)) fit = max(0, 1 - abs(change - target) / max(target, .10)) sts = checks.get("sts") semantic = .78 if sts is None else max(0, min(1, sts)) nli_scores = [ item["entailment"] for item in [checks.get("nli_forward"), checks.get("nli_reverse")] if item ] if nli_scores: semantic = .55 * semantic + .45 * min(nli_scores) lane_fit = ( 1.0 if candidate.lane == mode else .82 if (mode, candidate.lane) in { ("Balanced", "Minimal"), ("Balanced", "Structural"), ("Structural", "Balanced"), } else .65 ) grammar = max(.6, 1 - .1 * max(0, checks.get("grammar_delta", 0))) candidate.score = ( 100 * ( .43 * semantic + .22 * fit + .15 * lane_fit + .12 * grammar + .08 * candidate.priority ) if ok else -1e9 ) candidate.checks = checks (good if ok else bad).append(candidate) good.sort(key=lambda item: item.score, reverse=True) bad.sort(key=lambda item: ( len(item.checks.get("failures", [])), -item.checks.get("change", 0), )) return good, bad # ===================================================================== # ENGINE # ===================================================================== class Engine: def __init__(self): self.an = Analyzer() self.gen = Generator() self.sc = Scanner() self.rank = Ranker() def one(self, sentence, profile, user_terms, mode): _ensure_state() analysis = self.an.analyze(sentence, profile, user_terms) if analysis.families == ["PARSE_FAILURE"]: return SentenceResult( sentence, sentence, "PARSE_FAILURE", analysis.families, None, "Dependency parse unavailable and no surface-safe family matched.", [], {}, {"detected": analysis.families}, ) candidates, trace = self.gen.generate( analysis, profile, user_terms, mode ) if not candidates: code = ( "NO_PATTERN_COVERAGE" if not trace.get("routes") else "CANDIDATE_GENERATION_FAILURE" ) return SentenceResult( sentence, sentence, code, analysis.families, None, "No transformation produced a distinct candidate for the detected structures.", [], {}, trace, ) good, bad = self.rank.rank( sentence, candidates, analysis, profile, user_terms, mode ) trace["candidate_traces"] = [ { "text": item.text, "lane": item.lane, "family": item.family, "reason": item.reason, "accepted": item.checks.get("accepted"), "failures": item.checks.get("failures", []), "rejection_reasons": item.checks.get("rejection_reasons", []), "change": item.checks.get("change"), "sts": item.checks.get("sts"), "nli_forward": item.checks.get("nli_forward"), "nli_reverse": item.checks.get("nli_reverse"), "score": item.score, } for item in good + bad ] if not good: counts = Counter( code for item in bad for code in item.checks.get("failures", []) ) precedence = [ "TERMINOLOGY_REJECTION", "POLARITY_REJECTION", "MODALITY_REJECTION", "QUANTITY_REJECTION", "ENUMERATION_REJECTION", "GRAMMAR_REJECTION", "SEMANTIC_REJECTION", "INSUFFICIENT_REWRITE", ] status = next((code for code in precedence if counts[code]), "RANKING_EMPTY") return SentenceResult( sentence, sentence, status, analysis.families, None, "Candidates were generated, but none passed all hard safeguards and rewrite gates.", [item.text for item in bad[:3]], bad[0].checks if bad else {}, trace | {"rejections": dict(counts)}, ) selected = good[0] trace["selected"] = { "text": selected.text, "lane": selected.lane, "family": selected.family, "score": selected.score, } return SentenceResult( sentence, selected.text, "SUCCESS", analysis.families, selected.family, selected.reason, [item.text for item in good[1:4]], selected.checks, trace, ) def process(self, text, workflow, mode, discipline, terms_text, progress=None): STATE.clear() STATE.update(start=time.time(), warnings=[]) profile, evidence = self.route(text, discipline) user_terms = { item.strip() for item in re.split(r"[,;\n]", terms_text or "") if item.strip() } findings, score, band, features = self.sc.scan(text) if workflow == "Scan Only": return DocumentResult( text, text, workflow, mode, profile.name, evidence, score, band, findings, [], STATE["warnings"], {"features": features}, ) paragraphs = [ item.strip() for item in re.split(r"\n\s*\n", text) if item.strip() ] total = sum(len(sent_split(item)) for item in paragraphs) done = 0 results = [] output_paragraphs = [] for block_start in range(0, len(paragraphs), CFG.batch): for paragraph in paragraphs[block_start:block_start + CFG.batch]: output_sentences = [] for sentence in sent_split(paragraph): targeted = ( workflow == "Edit / Paraphrase Only" or any(finding.sentence == len(results) for finding in findings) ) if targeted: item = self.one(sentence, profile, user_terms, mode) else: item = SentenceResult( sentence, sentence, "NOT_TARGETED", [], None, "No scanner finding targeted this sentence.", [], {}, {}, ) results.append(item) output_sentences.append(item.revised) done += 1 if progress: progress( done / max(total, 1), desc=f"Sentence {done}/{total}", ) output_paragraphs.append(" ".join(output_sentences)) return DocumentResult( text, "\n\n".join(output_paragraphs), workflow, mode, profile.name, evidence, score, band, findings, results, STATE["warnings"], { "sentences": total, "success": sum(item.status == "SUCCESS" for item in results), "unchanged": sum(item.original == item.revised for item in results), "seconds": time.time() - STATE["start"], }, ) def route(self, text, requested): if requested in PROFILES: return PROFILES[requested], ["manual"] lo = text.lower() scores = { name: sum( bool(re.search(r"(?