Download runtime/cell11.py from raymuz/NLLUniversalProfessionalEditor: direct link, hf CLI and curl.
- Browser
- Download file 45.3 kB
-
https://huggingface.co/spaces/raymuz/NLLUniversalProfessionalEditor/resolve/main/runtime/cell11.py
- Command line
-
hf download hf://spaces/raymuz/NLLUniversalProfessionalEditor/runtime/cell11.py
-
curl -L -o cell11.py https://huggingface.co/spaces/raymuz/NLLUniversalProfessionalEditor/resolve/main/runtime/cell11.py
45.3 kB
| #@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"(?<!\w)" + re.escape(token) + r"(?!\w)", lo) | |
| ) | |
| def _neg_scope_debug(doc): | |
| if doc is None or not getattr(doc, "has_annotation", lambda *_: False)("DEP"): | |
| return [] | |
| return sorted( | |
| (x.lower_, x.head.lemma_.lower()) | |
| for x in doc | |
| if x.dep_ == "neg" or x.lower_ in {"never", "without", "neither", "nor"} | |
| ) | |
| def _modals(doc, text=""): | |
| if doc is not None: | |
| try: | |
| return Counter( | |
| x.lemma_.lower() | |
| for x in doc | |
| if x.lemma_.lower() in HEDGE or x.tag_ == "MD" | |
| ) | |
| except Exception: | |
| pass | |
| return Counter(re.findall( | |
| r"\b(?:may|might|can|could|must|should|would|will|possibly|potentially|likely)\b", | |
| str(text or "").lower(), | |
| )) | |
| def _purpose_contrast_preserved(original, candidate): | |
| match = re.search( | |
| r"(?is)\bnot\s+to\s+(?P<x>.+?)\s+but\s+to\s+(?P<y>.+?)[.]?$", | |
| 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"(?<!\w)" + re.escape(word) + r"(?!\w)", lo) for word in words) | |
| } | |
| def _triples(doc): | |
| triples = [] | |
| if doc is None or not doc.has_annotation("DEP"): | |
| return triples | |
| for verb in doc: | |
| subjects = [x for x in verb.children if x.dep_ in {"nsubj", "nsubjpass"}] | |
| objects = [x for x in verb.children if x.dep_ in {"obj", "dobj", "attr"}] | |
| triples += [ | |
| (sub(subject).lower(), verb.lemma_.lower(), sub(obj).lower()) | |
| for subject in subjects | |
| for obj in objects | |
| ] | |
| return triples | |
| # ===================================================================== | |
| # VALIDATOR | |
| # ===================================================================== | |
| class Validator: | |
| def validate(self, original, candidate, analysis, profile, user_terms, mode): | |
| _ensure_state() | |
| revised = candidate.text | |
| failures = [] | |
| reasons = [] | |
| checks = {} | |
| def reject(code, why): | |
| if code not in failures: | |
| failures.append(code) | |
| reasons.append({"code": code, "reason": why}) | |
| # ------------------------------------------------------------- | |
| # Exact protected inventories | |
| # ------------------------------------------------------------- | |
| hard_terms = set(getattr(profile, "immutable", set()) or set()) | set(user_terms or set()) | |
| for kind, code in [ | |
| ("citation", "TERMINOLOGY_REJECTION"), | |
| ("quantity", "QUANTITY_REJECTION"), | |
| ("url", "TERMINOLOGY_REJECTION"), | |
| ("equation", "TERMINOLOGY_REJECTION"), | |
| ("code", "TERMINOLOGY_REJECTION"), | |
| ]: | |
| source_inventory = Counter( | |
| item[1].lower() | |
| for item in protect(original, hard_terms) | |
| if item[0] == kind | |
| ) | |
| candidate_inventory = Counter( | |
| item[1].lower() | |
| for item in protect(revised, hard_terms) | |
| if item[0] == kind | |
| ) | |
| checks[kind] = source_inventory == candidate_inventory | |
| if source_inventory != candidate_inventory: | |
| reject( | |
| code, | |
| f"{kind} inventory changed: {dict(source_inventory)} -> {dict(candidate_inventory)}", | |
| ) | |
| missing_terms = [ | |
| term | |
| for term in hard_terms | |
| if re.search(r"(?<!\w)" + re.escape(term) + r"(?!\w)", original, re.I) | |
| and not re.search(r"(?<!\w)" + re.escape(term) + r"(?!\w)", revised, re.I) | |
| ] | |
| if missing_terms: | |
| reject( | |
| "TERMINOLOGY_REJECTION", | |
| "Missing immutable/user terms: " + ", ".join(sorted(missing_terms)), | |
| ) | |
| # ------------------------------------------------------------- | |
| # Realization gate | |
| # ------------------------------------------------------------- | |
| realization = list(realization_issues(revised) or []) | |
| checks["realization_issues"] = realization | |
| if realization: | |
| reject( | |
| "GRAMMAR_REJECTION", | |
| "Candidate failed realization checks: " + " | ".join(realization), | |
| ) | |
| source_doc = _safe_doc(original) | |
| candidate_doc = _safe_doc(revised) | |
| # ------------------------------------------------------------- | |
| # Polarity: compare marker inventory; retain dependency scope only | |
| # as diagnostics. Reordering 'without' is therefore allowed. | |
| # ------------------------------------------------------------- | |
| source_neg = _neg_inventory(original, source_doc) | |
| candidate_neg = _neg_inventory(revised, candidate_doc) | |
| checks["negation_inventory"] = (dict(source_neg), dict(candidate_neg)) | |
| checks["negation_scope_debug"] = ( | |
| _neg_scope_debug(source_doc), | |
| _neg_scope_debug(candidate_doc), | |
| ) | |
| polarity_preserved = source_neg == candidate_neg | |
| if ( | |
| not polarity_preserved | |
| and candidate.family.startswith("NEGATED_PURPOSE_CONTRAST") | |
| and _purpose_contrast_preserved(original, revised) | |
| ): | |
| polarity_preserved = True | |
| if not polarity_preserved: | |
| reject( | |
| "POLARITY_REJECTION", | |
| f"Polarity-marker inventory changed: {dict(source_neg)} -> {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<s>[A-Z][A-Za-z0-9_-]*)\s+" | |
| r"(?P<v>\w+(?:ed|s))\s+(?P<o>[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"(?<!\w)" + re.escape(term.lower()) + r"(?!\w)", lo)) | |
| for term in profile.keywords | profile.phrases | |
| ) | |
| for name, profile in PROFILES.items() | |
| } | |
| winner = max(scores, key=scores.get) | |
| winner = winner if scores[winner] else "General Academic" | |
| evidence = [ | |
| term for term in PROFILES[winner].keywords | |
| if term.lower() in lo | |
| ][:10] | |
| return PROFILES[winner], evidence | |
| ENGINE = Engine() | |
| # ===================================================================== | |
| # REPORT WRITER | |
| # ===================================================================== | |
| from docx import Document | |
| def report(result): | |
| folder = Path(CFG.out) / ("NLL_" + time.strftime("%Y%m%d_%H%M%S")) | |
| folder.mkdir(parents=True, exist_ok=True) | |
| revised_path = folder / "revised.txt" | |
| diagnostics_path = folder / "diagnostics.json" | |
| revised_path.write_text(result.revised, encoding="utf8") | |
| diagnostics_path.write_text( | |
| json.dumps(asdict(result), indent=2, default=str), | |
| encoding="utf8", | |
| ) | |
| document = Document() | |
| document.add_heading("NLL Revised Document", 0) | |
| document.add_paragraph(result.revised) | |
| document.add_heading("Review", 1) | |
| document.add_paragraph( | |
| f"Writing Pattern Score: {result.pattern_score:.0f}/100 ({result.band}). " | |
| "The score describes observable structural and stylistic patterns only." | |
| ) | |
| for index, item in enumerate(result.sentences, 1): | |
| document.add_heading(f"Sentence {index}: {item.status}", 2) | |
| document.add_paragraph("Original: " + item.original) | |
| document.add_paragraph("Revised: " + item.revised) | |
| document.add_paragraph("Reason: " + item.reason) | |
| document.add_paragraph("Detected structures: " + ", ".join(item.families)) | |
| document.add_paragraph( | |
| "Diagnostics: " + json.dumps(item.diagnostics, default=str)[:4000] | |
| ) | |
| doc_path = folder / "revised_and_review.docx" | |
| document.save(doc_path) | |
| return [str(doc_path), str(revised_path), str(diagnostics_path)] | |
| # ===================================================================== | |
| # SELF-TESTS | |
| # ===================================================================== | |
| _ensure_state() | |
| # 1. Polarity movement: reordering 'without' must not be rejected simply | |
| # because its dependency head changed. | |
| _test_profile = next(iter(PROFILES.values())) | |
| _test_analyzer = Analyzer() | |
| _pol_source = ( | |
| "Systems train separate models without explicit rules for assessment, " | |
| "correction, and graduation." | |
| ) | |
| _pol_candidate = Candidate( | |
| "Without explicit rules for assessment, correction, and graduation, " | |
| "systems train separate models.", | |
| "Balanced", | |
| "FINITE_COMPLEMENT_LIST", | |
| "validator self-test", | |
| .9, | |
| ) | |
| _pol_analysis = _test_analyzer.analyze(_pol_source, _test_profile, set()) | |
| _pol_ok, _pol_fail, _ = Validator().validate( | |
| _pol_source, _pol_candidate, _pol_analysis, _test_profile, set(), "Balanced" | |
| ) | |
| if "POLARITY_REJECTION" in _pol_fail: | |
| raise RuntimeError( | |
| "Cell 7 v4.3 failed: polarity-preserving 'without' movement was rejected." | |
| ) | |
| # 2. Enumeration must survive when the candidate reorganizes/splits text. | |
| _enum_source = "We propose Framework X, a protocol consisting of contracts, examinations, and promotion." | |
| _enum_candidate = Candidate( | |
| "Framework X is a protocol consisting of contracts, examinations, and promotion proposed by us.", | |
| "Balanced", | |
| "APPOSITIVE_DEFINITION", | |
| "validator self-test", | |
| .9, | |
| ) | |
| _enum_analysis = _test_analyzer.analyze(_enum_source, _test_profile, set()) | |
| _, _enum_fail, _ = Validator().validate( | |
| _enum_source, _enum_candidate, _enum_analysis, _test_profile, set(), "Balanced" | |
| ) | |
| if "ENUMERATION_REJECTION" in _enum_fail: | |
| raise RuntimeError( | |
| "Cell 7 v4.3 failed: preserved enumeration was rejected." | |
| ) | |
| # 3. Malformed realization must fail at the judge even if it somehow bypasses 5A. | |
| _bad_candidate = Candidate( | |
| "Protocol to learners spanning classification, drafting, and planning.", | |
| "Structural", | |
| "PARTICIPIAL_POSTMODIFIER", | |
| "validator self-test", | |
| .9, | |
| ) | |
| _bad_analysis = _test_analyzer.analyze( | |
| "The testbed applies the protocol to learners spanning classification, drafting, and planning.", | |
| _test_profile, | |
| set(), | |
| ) | |
| _, _bad_fail, _ = Validator().validate( | |
| "The testbed applies the protocol to learners spanning classification, drafting, and planning.", | |
| _bad_candidate, | |
| _bad_analysis, | |
| _test_profile, | |
| set(), | |
| "Structural", | |
| ) | |
| if "GRAMMAR_REJECTION" not in _bad_fail: | |
| raise RuntimeError( | |
| "Cell 7 v4.3 failed: malformed participial fragment was not rejected." | |
| ) | |
| # 4. Appositive before the true list must not become a pseudo-member. | |
| _long_enum_source = ( | |
| "We propose Framework X (FX), a protocol consisting of native model contracts, " | |
| "historical contracts, protected examinations, peer study, and promotion." | |
| ) | |
| _long_enum_candidate = Candidate( | |
| "Framework X (FX) is a protocol consisting of native model contracts, " | |
| "historical contracts, protected examinations, peer study, and promotion proposed by us.", | |
| "Balanced", "APPOSITIVE_DEFINITION", "validator self-test", .9, | |
| ) | |
| _long_enum_analysis = _test_analyzer.analyze(_long_enum_source, _test_profile, set()) | |
| _, _long_enum_fail, _ = Validator().validate( | |
| _long_enum_source, _long_enum_candidate, _long_enum_analysis, | |
| _test_profile, set(), "Balanced" | |
| ) | |
| if "ENUMERATION_REJECTION" in _long_enum_fail: | |
| raise RuntimeError( | |
| "Cell 7 v4.3 failed: appositive boundary corrupted enumeration extraction." | |
| ) | |
| # 5. Hyphenated list members must remain intact during comparison. | |
| _hyphen_source = ( | |
| "We pre-register baseline questions, disjoint re-examination data, matched-data " | |
| "baselines, ablations, and independently held-out testing." | |
| ) | |
| _hyphen_candidate = Candidate( | |
| "Our preregistration covers baseline questions, disjoint re-examination data, " | |
| "matched-data baselines, ablations, and independently held-out testing.", | |
| "Balanced", "FINITE_OBJECT_LIST_FRAME", "validator self-test", .9, | |
| ) | |
| _hyphen_analysis = _test_analyzer.analyze(_hyphen_source, _test_profile, set()) | |
| _, _hyphen_fail, _ = Validator().validate( | |
| _hyphen_source, _hyphen_candidate, _hyphen_analysis, | |
| _test_profile, set(), "Balanced" | |
| ) | |
| if "ENUMERATION_REJECTION" in _hyphen_fail: | |
| raise RuntimeError( | |
| "Cell 7 v4.3 failed: hyphenated list member was split or lost." | |
| ) | |
| # 6. Unicode punctuation variants must compare as the same content tokens. | |
| if _unicode_tokens("re-examination data") != _unicode_tokens("re examination data"): | |
| raise RuntimeError( | |
| "Cell 7 v4.3 failed: ASCII hyphen token normalization mismatch." | |
| ) | |
| if _unicode_tokens("matched–data") != _unicode_tokens("matched-data"): | |
| raise RuntimeError( | |
| "Cell 7 v4.3 failed: Unicode dash token normalization mismatch." | |
| ) | |
| if _unicode_tokens("ALCEF–RSF") != _unicode_tokens("ALCEF-RSF"): | |
| raise RuntimeError( | |
| "Cell 7 v4.3 failed: acronym dash normalization mismatch." | |
| ) | |
| # 7. Boundary-corrupted pseudo-members must be rejected, while legitimate | |
| # multiword and hyphenated members remain eligible. | |
| if _plausible_enumeration_member( | |
| "Framework X (FX), a protocol consisting of native contracts, historical contracts" | |
| ): | |
| raise RuntimeError( | |
| "Cell 7 v4.3 failed: appositive pseudo-member passed confidence gate." | |
| ) | |
| if not _plausible_enumeration_member("disjoint re-examination data"): | |
| raise RuntimeError( | |
| "Cell 7 v4.3 failed: valid hyphenated member failed confidence gate." | |
| ) | |
| if not _plausible_enumeration_member("independently held-out testing"): | |
| raise RuntimeError( | |
| "Cell 7 v4.3 failed: valid multiword member failed confidence gate." | |
| ) | |
| print("=" * 78) | |
| print("CELL 7 VALIDATION / RANKING / ENGINE v4.3 LOADED") | |
| print("=" * 78) | |
| print("Validator: OK") | |
| print("Ranker: OK") | |
| print("Engine: OK") | |
| print("ENGINE instance: OK") | |
| print("Polarity-inventory validation: active") | |
| print("Dependency-aware Unicode enumeration validation: active") | |
| print("Appositive/list-boundary protection: active") | |
| print("Hyphen/dash-insensitive content comparison: active") | |
| print("Parser-artifact group filtering: active") | |
| print("Enumeration-member confidence gate: active") | |
| print("Noisy-superset group suppression: active") | |
| print("Cell 5A realization gate rechecked by judge: active") | |
| print("Optional STS/NLI signals: soft unless contradiction is detected") | |
| print("Runtime warning-state safety: active") | |
| print("Validator self-tests: PASSED") | |
| print("Registered transformation rules:", len(RULES)) | |
| print("Expected rule count:", EXPECTED_CELL7_RULE_COUNT) | |
| print("=" * 78) | |