raymuz commited on
Commit
70b2a71
·
verified ·
1 Parent(s): 1225dd9

Deploy NLL Universal Professional Editor production Docker build

Browse files
.dockerignore ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ .git
2
+ .github
3
+ __pycache__
4
+ *.pyc
5
+ .pytest_cache
6
+ .venv
7
+ venv
8
+ *.ipynb
9
+ *.xlsx
10
+ *.zip
.gitignore ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ __pycache__/
2
+ *.py[cod]
3
+ .venv/
4
+ venv/
5
+ .pytest_cache/
6
+ .env
7
+ *.log
DEPLOYMENT.md ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Deployment Guide
2
+
3
+ ## Hugging Face Docker Space
4
+
5
+ The repository is configured as a Docker Space on port 7860.
6
+
7
+ 1. Create a Hugging Face write token.
8
+ 2. Set `HF_TOKEN` locally.
9
+ 3. Run `scripts/deploy.sh` on Linux/macOS or `scripts/deploy.ps1` on Windows.
10
+ 4. Monitor the Space build logs.
11
+ 5. Verify `/health`, `/api/health`, the Gradio UI, and `/api/rewrite`.
12
+
13
+ ## Local verification
14
+
15
+ ```bash
16
+ docker build -t nll-editor .
17
+ docker run --rm -p 7860:7860 nll-editor
18
+ curl http://127.0.0.1:7860/health
19
+ curl http://127.0.0.1:7860/api/health
20
+ ```
21
+
22
+ ## Runtime configuration
23
+
24
+ - `NLL_ENABLE_LANGUAGETOOL=false` by default.
25
+ - Set it to `true` only if local LanguageTool comparison is required.
26
+ - STS, NLI, MLM candidate generation, and online lexical fallback remain disabled.
Dockerfile ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.11-slim
2
+
3
+ ENV PYTHONDONTWRITEBYTECODE=1 \
4
+ PYTHONUNBUFFERED=1 \
5
+ PIP_NO_CACHE_DIR=1 \
6
+ PORT=7860 \
7
+ NLL_ENABLE_LANGUAGETOOL=false \
8
+ NLL_REQUIRE_SPACY_MODEL=true
9
+
10
+ RUN apt-get update && apt-get install -y --no-install-recommends \
11
+ openjdk-17-jre-headless curl gcc g++ git && \
12
+ rm -rf /var/lib/apt/lists/*
13
+
14
+ WORKDIR /app
15
+ COPY requirements.txt .
16
+ RUN pip install --upgrade pip && pip install -r requirements.txt
17
+ RUN python -m spacy download en_core_web_sm
18
+
19
+ COPY . .
20
+ RUN useradd -m -u 1000 user && chown -R user:user /app
21
+ USER user
22
+
23
+ EXPOSE 7860
24
+ HEALTHCHECK --interval=30s --timeout=5s --start-period=60s --retries=3 \
25
+ CMD curl -fsS http://127.0.0.1:7860/health || exit 1
26
+
27
+ CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "7860", "--workers", "1", "--proxy-headers"]
Makefile ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ .PHONY: test run build
2
+
3
+ test:
4
+ pytest -q
5
+
6
+ run:
7
+ uvicorn app:app --host 0.0.0.0 --port 7860 --reload
8
+
9
+ build:
10
+ docker build -t nll-editor .
README.md CHANGED
@@ -1,14 +1,55 @@
1
  ---
2
- title: NLLUniversalProfessionalEditor
3
- emoji: 🚀
4
- colorFrom: gray
5
- colorTo: red
6
- sdk: gradio
7
- sdk_version: 6.31.0
8
- python_version: '3.13'
9
- app_file: app.py
10
  pinned: false
11
  license: apache-2.0
 
12
  ---
13
 
14
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
+ title: NLL Universal Professional Editor
3
+ emoji: 📝
4
+ colorFrom: blue
5
+ colorTo: indigo
6
+ sdk: docker
7
+ app_port: 7860
 
 
8
  pinned: false
9
  license: apache-2.0
10
+ short_description: Deterministic professional rewriting.
11
  ---
12
 
13
+ # NLL Universal Professional Editor
14
+
15
+ A deterministic, non-generative professional and academic rewriting service with:
16
+
17
+ - 30 transformation rules
18
+ - Minimal, Balanced, and Structural modes
19
+ - must-paraphrase bounded retry orchestration
20
+ - terminology, citation, quantity, polarity, modality, enumeration, grammar, and semantic safeguards
21
+ - full Gradio diagnostics
22
+ - Flask JSON API mounted under `/api`
23
+
24
+ ## Web UI
25
+
26
+ Open the Space root URL.
27
+
28
+ ## REST API
29
+
30
+ ### Health
31
+
32
+ ```bash
33
+ curl https://YOUR-SPACE.hf.space/api/health
34
+ ```
35
+
36
+ ### Rewrite
37
+
38
+ ```bash
39
+ curl -X POST https://YOUR-SPACE.hf.space/api/rewrite \
40
+ -H "Content-Type: application/json" \
41
+ -d '{
42
+ "text": "The next step is to inspect how the handler checks configuration.",
43
+ "workflow": "Edit / Paraphrase Only",
44
+ "mode": "Balanced",
45
+ "discipline": "Auto Detect",
46
+ "protected_terms": ""
47
+ }'
48
+ ```
49
+
50
+ ## Local Docker
51
+
52
+ ```bash
53
+ docker build -t nll-editor .
54
+ docker run --rm -p 7860:7860 nll-editor
55
+ ```
SECURITY.md ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ # Security Notes
2
+
3
+ - Do not commit a Hugging Face access token.
4
+ - Keep the Space private during pilot validation if documents may contain confidential material.
5
+ - The rewrite API returns diagnostics that may repeat source text; apply access controls accordingly.
6
+ - Uploaded files are processed in temporary storage and should not be treated as a long-term document repository.
7
+ - Review Hugging Face Space visibility, secrets, and hardware settings before production use.
api/README.md ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ # Flask API
2
+
3
+ The Flask app is mounted under `/api` inside the FastAPI/Gradio server.
4
+
5
+ - `GET /api/health`
6
+ - `POST /api/rewrite`
api/__init__.py ADDED
File without changes
api/flask_api.py ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from dataclasses import asdict
2
+ from flask import Flask, jsonify, request
3
+ from runtime.engine import ENGINE, RULES, MUST_PARAPHRASE_RUNTIME
4
+
5
+ flask_app = Flask(__name__)
6
+
7
+ @flask_app.get("/health")
8
+ def health():
9
+ return jsonify({
10
+ "status": "ok",
11
+ "service": "NLL Universal Professional Editor",
12
+ "rules": len(RULES),
13
+ "must_paraphrase": bool(MUST_PARAPHRASE_RUNTIME.get("active")),
14
+ "version": MUST_PARAPHRASE_RUNTIME.get("version"),
15
+ })
16
+
17
+ @flask_app.post("/rewrite")
18
+ def rewrite():
19
+ payload = request.get_json(silent=True) or {}
20
+ text = str(payload.get("text", "")).strip()
21
+ if not text:
22
+ return jsonify({"error": "The 'text' field is required."}), 400
23
+ workflow = payload.get("workflow", "Edit / Paraphrase Only")
24
+ mode = payload.get("mode", "Balanced")
25
+ discipline = payload.get("discipline", "Auto Detect")
26
+ protected_terms = payload.get("protected_terms", "")
27
+ result = ENGINE.process(text, workflow, mode, discipline, protected_terms)
28
+ return jsonify(asdict(result))
app.py ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ from fastapi import FastAPI
3
+ from fastapi.responses import JSONResponse
4
+ from starlette.middleware.wsgi import WSGIMiddleware
5
+ import gradio as gr
6
+
7
+ from api.flask_api import flask_app
8
+ import runtime.engine as runtime
9
+
10
+ # Build the exact deployment UI in the same namespace as the engine.
11
+ _ui_path = os.path.join(os.path.dirname(__file__), "runtime", "cell9.py")
12
+ with open(_ui_path, "r", encoding="utf-8") as _handle:
13
+ exec(compile(_handle.read(), _ui_path, "exec"), runtime.__dict__, runtime.__dict__)
14
+
15
+ demo = runtime.NLL_APP
16
+ app = FastAPI(title="NLL Universal Professional Editor", version="1.0.0")
17
+
18
+ @app.get("/health")
19
+ def root_health():
20
+ return JSONResponse({
21
+ "status": "ok",
22
+ "rules": len(runtime.RULES),
23
+ "must_paraphrase": runtime.MUST_PARAPHRASE_RUNTIME.get("active", False),
24
+ })
25
+
26
+ # Flask REST API is available under /api; Gradio is mounted at /.
27
+ app.mount("/api", WSGIMiddleware(flask_app))
28
+ app = gr.mount_gradio_app(app, demo, path="/")
docker-compose.yml ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ services:
2
+ nll-editor:
3
+ build: .
4
+ ports:
5
+ - "7860:7860"
6
+ environment:
7
+ NLL_ENABLE_LANGUAGETOOL: "false"
8
+ restart: unless-stopped
frontend/README.md ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ # Front end
2
+
3
+ The production front end is the professional Gradio Blocks interface defined in `runtime/cell9.py`. It is mounted at `/` by `app.py` and shares the same runtime engine as the Flask API.
packages.txt ADDED
@@ -0,0 +1 @@
 
 
1
+ openjdk-17-jre-headless
pytest.ini ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ [pytest]
2
+ testpaths = tests
3
+ addopts = -q
requirements.txt ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ gradio==5.49.1
2
+ fastapi==0.115.12
3
+ uvicorn[standard]==0.34.3
4
+ Flask==3.1.1
5
+ pandas==2.2.3
6
+ spacy==3.8.7
7
+ python-docx==1.1.2
8
+ PyMuPDF==1.25.5
9
+ language-tool-python==2.9.4
10
+ openpyxl==3.1.5
runtime/__init__.py ADDED
File without changes
runtime/cell5.py ADDED
@@ -0,0 +1,1599 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #@title 5. STANDALONE UNIVERSAL TRANSFORMATION ENGINE v4.1 — 30 deterministic rules
2
+ # Complete standalone replacement for Cell 5.
3
+ # Requires earlier notebook cells to define Candidate, CFG, Analyzer, nlp,
4
+ # optional, wn, and getInflection. This cell defines the rule registry,
5
+ # all 25 base rules, the Generator, and the v4.1 primary-clause upgrade.
6
+
7
+ from dataclasses import dataclass
8
+ from typing import Callable
9
+ from collections import Counter
10
+ import re
11
+
12
+ _REQUIRED_CELL5 = ["Candidate", "CFG", "Analyzer", "nlp"]
13
+ _missing_cell5 = [name for name in _REQUIRED_CELL5 if name not in globals()]
14
+ if _missing_cell5:
15
+ raise RuntimeError(
16
+ "Standalone Cell 5 cannot run. Missing: "
17
+ + ", ".join(_missing_cell5)
18
+ + ". Run Cells 1–4/4B first."
19
+ )
20
+
21
+ @dataclass
22
+ class Rule:
23
+ name: str
24
+ lanes: set
25
+ families: set
26
+ priority: float
27
+ fn: Callable
28
+
29
+ RULES = []
30
+
31
+ def rule(name, lanes, families, priority=.5):
32
+ """Register or replace one deterministic transformation rule."""
33
+ def deco(fn):
34
+ global RULES
35
+ RULES = [registered for registered in RULES if registered.name != name]
36
+ RULES.append(
37
+ Rule(
38
+ str(name),
39
+ set(lanes),
40
+ set(families),
41
+ float(priority),
42
+ fn,
43
+ )
44
+ )
45
+ return fn
46
+ return deco
47
+
48
+
49
+ def clean(text):
50
+ value = re.sub(
51
+ r"\s+([,.;:!?])",
52
+ r"\1",
53
+ re.sub(r"\s+", " ", str(text or "")),
54
+ ).strip()
55
+ if value:
56
+ value = value[0].upper() + value[1:]
57
+ return value if not value or value[-1] in ".!?" else value + "."
58
+
59
+
60
+ def bare(text):
61
+ return re.sub(r"[.?!]+$", "", clean(text)).strip()
62
+
63
+
64
+ def lower_initial(text):
65
+ value = str(text or "").strip()
66
+ if not value or re.match(r"^[A-Z]{2,}\b", value):
67
+ return value
68
+ return value[:1].lower() + value[1:]
69
+
70
+
71
+ def surface_key(text):
72
+ return re.sub(r"\W+", " ", str(text or "").lower()).strip()
73
+
74
+
75
+ _surface_key = surface_key
76
+
77
+
78
+ def K(text, lane, family, reason, priority=.5):
79
+ return Candidate(
80
+ clean(text),
81
+ lane,
82
+ family,
83
+ reason,
84
+ float(priority),
85
+ )
86
+
87
+
88
+ def emit(text, lane, family, reason, priority=.5):
89
+ value = clean(text)
90
+ return [] if not value else [K(value, lane, family, reason, priority)]
91
+
92
+
93
+ def _safe_subtree_text(token):
94
+ """Return a token subtree without requiring a separate helper cell."""
95
+ try:
96
+ items = list(token.subtree)
97
+ if not items:
98
+ return token.text
99
+ start = min(item.i for item in items)
100
+ end = max(item.i for item in items) + 1
101
+ return token.doc[start:end].text
102
+ except Exception:
103
+ return getattr(token, "text", "")
104
+
105
+
106
+ # =====================================================================
107
+ # BASE RULES 1–25
108
+ # =====================================================================
109
+
110
+ @rule("copular_action_frame", {"Balanced", "Structural"}, {"COPULAR_ACTION_FRAME"}, .92)
111
+ def tr_copular_action(analysis, profile, user_terms):
112
+ match = re.match(
113
+ r"(?is)^The\s+(?P<head>next|first|second|third|final|primary|main)\s+"
114
+ r"(?P<noun>step|task|action|objective|goal)\s+is\s+to\s+"
115
+ r"(?P<action>.+?)[.]?$",
116
+ analysis.text,
117
+ )
118
+ if not match:
119
+ return []
120
+ return [
121
+ K(
122
+ f"The {match['head']} {match['noun']} involves {match['action']}",
123
+ "Balanced",
124
+ "COPULAR_ACTION_FRAME",
125
+ "Converted a copular action frame into an event frame.",
126
+ .92,
127
+ )
128
+ ]
129
+
130
+
131
+ @rule("need_to_action", {"Balanced", "Structural"}, {"NEED_TO_ACTION"}, .88)
132
+ def tr_need_to(analysis, profile, user_terms):
133
+ match = re.match(
134
+ r"(?is)^(?P<subject>.+?)\s+(?P<need>need|needs|needed)\s+to\s+"
135
+ r"(?P<action>.+?)[.]?$",
136
+ analysis.text,
137
+ )
138
+ if not match:
139
+ return []
140
+ modal = "had to" if match["need"].lower() == "needed" else "must"
141
+ return [
142
+ K(
143
+ f"{match['subject']} {modal} {match['action']}",
144
+ "Balanced",
145
+ "NEED_TO_ACTION",
146
+ "Recast a need-to construction while retaining its subject and action.",
147
+ .88,
148
+ )
149
+ ]
150
+
151
+
152
+ @rule(
153
+ "finite_intent_rather_than",
154
+ {"Balanced", "Structural"},
155
+ {"FINITE_PURPOSE_INTENT", "RATHER_THAN_GERUND"},
156
+ .90,
157
+ )
158
+ def tr_finite_intent(analysis, profile, user_terms):
159
+ match = re.match(
160
+ r"(?is)^(?P<subject>.+?)\s+"
161
+ r"(?P<intent>intend|intends|intended)\s+to\s+"
162
+ r"(?P<action>.+?)\s+rather\s+than\s+"
163
+ r"(?P<alternative>\w+ing\b.+?)[.]?$",
164
+ analysis.text,
165
+ )
166
+ if not match:
167
+ return []
168
+ return [
169
+ K(
170
+ f"Rather than {match['alternative']}, {lower_initial(match['subject'])} "
171
+ f"{match['intent']} to {match['action']}",
172
+ "Balanced",
173
+ "FINITE_PURPOSE_INTENT",
174
+ "Fronted the rejected alternative while preserving the stated intent.",
175
+ .90,
176
+ )
177
+ ]
178
+
179
+
180
+ @rule(
181
+ "modal_copular_enumeration",
182
+ {"Balanced", "Structural"},
183
+ {"MODAL_COPULAR_ENUMERATION", "COORDINATED_OBJECTS_OR_ENUMERATION", "ENUMERATION"},
184
+ .91,
185
+ )
186
+ def tr_modal_enumeration(analysis, profile, user_terms):
187
+ match = re.match(
188
+ r"(?is)^(?P<subject>.+?)\s+"
189
+ r"(?P<modal>may|might|could|can)\s+be\s+in\s+"
190
+ r"(?P<items>.+?,\s*.+?\s*,?\s*(?:or|and)\s+.+?)[.]?$",
191
+ analysis.text,
192
+ )
193
+ if not match:
194
+ return []
195
+ return [
196
+ K(
197
+ f"Possible sources of {lower_initial(match['subject'])} "
198
+ f"{match['modal'].lower()} include {match['items']}",
199
+ "Balanced",
200
+ "MODAL_COPULAR_ENUMERATION",
201
+ "Recast a modal copular list while preserving uncertainty and every member.",
202
+ .91,
203
+ )
204
+ ]
205
+
206
+
207
+ @rule(
208
+ "enumeration_frame",
209
+ {"Minimal", "Balanced"},
210
+ {"COORDINATED_OBJECTS_OR_ENUMERATION", "ENUMERATION"},
211
+ .65,
212
+ )
213
+ def tr_enumeration_frame(analysis, profile, user_terms):
214
+ match = re.match(
215
+ r"(?is)^(?P<subject>.+?)\s+(?:consists?\s+of|comprises?)\s+"
216
+ r"(?P<items>.+?,.+?(?:and|or).+?)[.]?$",
217
+ analysis.text,
218
+ )
219
+ if not match:
220
+ return []
221
+ return emit(
222
+ f"{match['subject']} includes {match['items']}",
223
+ "Minimal",
224
+ "ENUMERATION",
225
+ "Varied an explicit enumeration frame without changing its members.",
226
+ .65,
227
+ )
228
+
229
+
230
+ @rule(
231
+ "using_method_relation",
232
+ {"Balanced", "Structural"},
233
+ {"USING_METHOD_RELATION", "METHOD_RELATION"},
234
+ .89,
235
+ )
236
+ def tr_using_method(analysis, profile, user_terms):
237
+ match = re.match(
238
+ r"(?is)^(?P<main>.+?)\s+using\s+(?P<method>.+?)[.]?$",
239
+ analysis.text,
240
+ )
241
+ if not match:
242
+ return []
243
+ return [
244
+ K(
245
+ f"Using {match['method']}, {lower_initial(match['main'])}",
246
+ "Balanced",
247
+ "USING_METHOD_RELATION",
248
+ "Fronted the explicitly stated method relation.",
249
+ .89,
250
+ )
251
+ ]
252
+
253
+
254
+ @rule(
255
+ "through_method_relation",
256
+ {"Balanced", "Structural"},
257
+ {"THROUGH_METHOD_RELATION", "METHOD_RELATION"},
258
+ .86,
259
+ )
260
+ def tr_through_method(analysis, profile, user_terms):
261
+ match = re.match(
262
+ r"(?is)^(?P<main>.+?)\s+through\s+(?P<method>.+?)[.]?$",
263
+ analysis.text,
264
+ )
265
+ if not match:
266
+ return []
267
+ return emit(
268
+ f"Through {match['method']}, {lower_initial(match['main'])}",
269
+ "Balanced",
270
+ "THROUGH_METHOD_RELATION",
271
+ "Fronted an explicitly stated through-method relation.",
272
+ .86,
273
+ )
274
+
275
+
276
+ @rule(
277
+ "by_gerund_method",
278
+ {"Balanced", "Structural"},
279
+ {"BY_GERUND_METHOD_RELATION", "METHOD_RELATION"},
280
+ .84,
281
+ )
282
+ def tr_by_gerund(analysis, profile, user_terms):
283
+ match = re.match(
284
+ r"(?is)^(?P<main>.+?)\s+by\s+(?P<method>\w+ing\b.+?)[.]?$",
285
+ analysis.text,
286
+ )
287
+ if not match:
288
+ return []
289
+ return emit(
290
+ f"By {match['method']}, {lower_initial(match['main'])}",
291
+ "Balanced",
292
+ "BY_GERUND_METHOD_RELATION",
293
+ "Fronted an explicitly stated by-gerund method relation.",
294
+ .84,
295
+ )
296
+
297
+
298
+ @rule(
299
+ "purpose_infinitive_relocation",
300
+ {"Balanced", "Structural"},
301
+ {"PURPOSE_INFINITIVE_FRONTING"},
302
+ .88,
303
+ )
304
+ def tr_purpose_front(analysis, profile, user_terms):
305
+ match = re.match(
306
+ r"(?is)^To\s+(?P<purpose>[^,]{2,160}),\s*(?P<main>.+?)[.]?$",
307
+ analysis.text,
308
+ )
309
+ if not match:
310
+ return []
311
+ return emit(
312
+ f"{match['main']} to {match['purpose']}",
313
+ "Balanced",
314
+ "PURPOSE_INFINITIVE_FRONTING",
315
+ "Moved the explicit purpose clause after the main proposition.",
316
+ .88,
317
+ )
318
+
319
+
320
+ @rule(
321
+ "purpose_contrast",
322
+ {"Balanced", "Structural"},
323
+ {"NEGATED_PURPOSE_CONTRAST"},
324
+ .98,
325
+ )
326
+ def tr_purpose(analysis, profile, user_terms):
327
+ match = re.match(
328
+ r"(?is)^(?P<head>.*?\b(?:purpose|aim|objective|goal)\b.*?)\s+"
329
+ r"is\s+not\s+to\s+(?P<x>.+?)\s+but\s+to\s+(?P<y>.+?)[.]?$",
330
+ analysis.text,
331
+ )
332
+ if not match:
333
+ return []
334
+ return [
335
+ K(
336
+ f"{match['head']} is to {match['y']} rather than to {match['x']}",
337
+ "Balanced",
338
+ "NEGATED_PURPOSE_CONTRAST",
339
+ "Reordered contrasted objectives while preserving both scopes.",
340
+ .98,
341
+ ),
342
+ K(
343
+ f"{match['head']} does not seek to {match['x']}. "
344
+ f"Instead, it aims to {match['y']}",
345
+ "Structural",
346
+ "NEGATED_PURPOSE_CONTRAST",
347
+ "Separated the excluded and intended objectives.",
348
+ .90,
349
+ ),
350
+ ]
351
+
352
+
353
+ @rule(
354
+ "evidential_complement",
355
+ {"Balanced", "Structural"},
356
+ {"EVIDENTIAL_COMPLEMENT"},
357
+ .94,
358
+ )
359
+ def tr_evidence(analysis, profile, user_terms):
360
+ match = re.match(
361
+ r"(?is)^(?P<src>.+?)\s+"
362
+ r"(?:provide|provides|provided|offer|offers|offered)\s+"
363
+ r"(?:clear\s+|strong\s+|empirical\s+)?evidence\s+that\s+"
364
+ r"(?P<claim>.+?)[.]?$",
365
+ analysis.text,
366
+ )
367
+ if not match:
368
+ return []
369
+ return [
370
+ K(
371
+ f"Evidence from {lower_initial(match['src'])} indicates that {match['claim']}",
372
+ "Balanced",
373
+ "EVIDENTIAL_COMPLEMENT",
374
+ "Recast the evidential frame while preserving the claim.",
375
+ .94,
376
+ ),
377
+ K(
378
+ f"According to the evidence provided by {lower_initial(match['src'])}, "
379
+ f"{match['claim']}",
380
+ "Structural",
381
+ "EVIDENTIAL_COMPLEMENT",
382
+ "Reorganized the source and claim around the same evidential relation.",
383
+ .76,
384
+ ),
385
+ ]
386
+
387
+
388
+ @rule("whether_complement", {"Balanced", "Structural"}, {"WHETHER_COMPLEMENT"}, .89)
389
+ def tr_whether(analysis, profile, user_terms):
390
+ match = re.match(
391
+ r"(?is)^(?P<subject>.+?)\s+require(?:s|d)?\s+"
392
+ r"(?:an?\s+)?(?:consideration|assessment|evaluation|analysis)\s+"
393
+ r"of\s+whether\s+(?P<clause>.+?)[.]?$",
394
+ analysis.text,
395
+ )
396
+ if not match:
397
+ return []
398
+ subject = re.sub(r"\balso\b", "", match["subject"], flags=re.I).strip()
399
+ return emit(
400
+ f"Assessing whether {match['clause']} is also required when considering "
401
+ f"{lower_initial(subject)}",
402
+ "Balanced",
403
+ "WHETHER_COMPLEMENT",
404
+ "Converted a nominalized whether-complement into an assessment clause.",
405
+ .89,
406
+ )
407
+
408
+
409
+ @rule("gerund_subject", {"Balanced", "Structural"}, {"GERUND_SUBJECT"}, .88)
410
+ def tr_gerund(analysis, profile, user_terms):
411
+ source = bare(analysis.text)
412
+ match = re.match(
413
+ r"(?is)^(?P<verb>Applying|Using|Evaluating|Comparing|Assessing|Examining|"
414
+ r"Analyzing|Implementing|Testing|Measuring)\s+(?P<object>.+?)\s+"
415
+ r"(?P<predicate>reduces?|provides?|enables?|supports?|allows?|improves?|"
416
+ r"limits?|increases?|decreases?|preserves?|reveals?|shows?)\b(?P<tail>.*)$",
417
+ source,
418
+ )
419
+ if not match:
420
+ return []
421
+ nouns = {
422
+ "applying": "application",
423
+ "using": "use",
424
+ "evaluating": "evaluation",
425
+ "comparing": "comparison",
426
+ "assessing": "assessment",
427
+ "examining": "examination",
428
+ "analyzing": "analysis",
429
+ "implementing": "implementation",
430
+ "testing": "testing",
431
+ "measuring": "measurement",
432
+ }
433
+ event = nouns[match["verb"].lower()]
434
+ return emit(
435
+ f"The {event} of {match['object']} {match['predicate']}{match['tail']}",
436
+ "Balanced",
437
+ "GERUND_SUBJECT",
438
+ "Converted a gerund event into an explicit nominal subject.",
439
+ .88,
440
+ )
441
+
442
+
443
+ @rule("whereby_mechanism", {"Balanced", "Structural"}, {"WHEREBY_CLAUSE"}, .84)
444
+ def tr_whereby(analysis, profile, user_terms):
445
+ match = re.match(
446
+ r"(?is)^(?P<main>.+?)\s+whereby\s+(?P<x>.+?)"
447
+ r"(?:\s+so\s+that\s+(?P<y>.+?))?[.]?$",
448
+ analysis.text,
449
+ )
450
+ if not match:
451
+ return []
452
+ middle = (
453
+ f"{match['main']}; under this design, {lower_initial(match['x'])}"
454
+ + (f", so that {match['y']}" if match.group("y") else "")
455
+ )
456
+ output = [
457
+ K(
458
+ middle,
459
+ "Balanced",
460
+ "WHEREBY_CLAUSE",
461
+ "Separated the main proposition from its mechanism.",
462
+ .84,
463
+ )
464
+ ]
465
+ if match.group("y"):
466
+ output.append(
467
+ K(
468
+ f"{match['main']}. Under this design, {match['x']}. "
469
+ f"This arrangement is intended so that {match['y']}",
470
+ "Structural",
471
+ "WHEREBY_CLAUSE",
472
+ "Separated the claim, mechanism, and purpose.",
473
+ .72,
474
+ )
475
+ )
476
+ return output
477
+
478
+
479
+ @rule("while_participial", {"Balanced", "Structural"}, {"WHILE_PARTICIPIAL"}, .84)
480
+ def tr_while(analysis, profile, user_terms):
481
+ match = re.match(
482
+ r"(?is)^(?P<main>.+?)\s+while\s+(?P<x>\w+ing\b.+?)[.]?$",
483
+ analysis.text,
484
+ )
485
+ if not match:
486
+ return []
487
+ return emit(
488
+ f"While {match['x']}, {lower_initial(match['main'])}",
489
+ "Balanced",
490
+ "WHILE_PARTICIPIAL",
491
+ "Reordered a simultaneous participial clause without changing its relation.",
492
+ .84,
493
+ )
494
+
495
+
496
+ @rule("fronted_adjunct", {"Balanced", "Structural"}, {"FRONTED_ADJUNCT"}, .73)
497
+ def tr_front(analysis, profile, user_terms):
498
+ if re.match(
499
+ r"(?is)^\s*(?:for purposes of|as used in|for the purposes of|"
500
+ r"in this (?:agreement|contract|plan|section|policy|specification))\b",
501
+ analysis.text,
502
+ ):
503
+ return []
504
+ match = re.match(
505
+ r"(?is)^(?P<adj>(?:For|In|Within|Under|During|Among|Across|Throughout)\s+"
506
+ r"[^,]+),\s*(?P<main>.+?)[.]?$",
507
+ analysis.text,
508
+ )
509
+ if not match:
510
+ return []
511
+ return emit(
512
+ f"{match['main']} {lower_initial(match['adj'])}",
513
+ "Balanced",
514
+ "FRONTED_ADJUNCT",
515
+ "Moved an existing domain adjunct after the main proposition.",
516
+ .73,
517
+ )
518
+
519
+
520
+ @rule("discourse_marker", {"Minimal", "Balanced"}, {"DISCOURSE_FRONTING"}, .72)
521
+ def tr_discourse(analysis, profile, user_terms):
522
+ match = re.match(
523
+ r"(?is)^(?P<mark>However|Therefore|Consequently|Thus|Moreover|Furthermore|"
524
+ r"Nevertheless|Accordingly|Additionally),\s+(?P<main>.+?)[.]?$",
525
+ analysis.text,
526
+ )
527
+ if not match:
528
+ return []
529
+ synonyms = {
530
+ "However": "Nevertheless",
531
+ "Therefore": "For this reason",
532
+ "Consequently": "As a result",
533
+ "Thus": "Accordingly",
534
+ "Moreover": "In addition",
535
+ "Furthermore": "Additionally",
536
+ "Nevertheless": "Even so",
537
+ "Accordingly": "Therefore",
538
+ "Additionally": "In addition",
539
+ }
540
+ return emit(
541
+ f"{synonyms[match['mark']]}, {match['main']}",
542
+ "Minimal",
543
+ "DISCOURSE_FRONTING",
544
+ "Varied an existing discourse marker within the same relation class.",
545
+ .62,
546
+ )
547
+
548
+
549
+ @rule(
550
+ "coordinated_passive_participles",
551
+ {"Minimal", "Balanced"},
552
+ {"COORDINATED_PASSIVE_PARTICIPLES", "COORDINATED_PREDICATES"},
553
+ .86,
554
+ )
555
+ def tr_passive_series(analysis, profile, user_terms):
556
+ source = bare(analysis.text)
557
+ match = re.search(
558
+ r"(?is)\b(?P<noun>[A-Za-z][\w-]*(?:\s+[A-Za-z][\w-]*){0,5})\s+that\s+"
559
+ r"(?:have|has|had)\s+been\s+"
560
+ r"(?P<series>(?:\w+ly\s+)?\w+(?:ed|en)\s*,\s*"
561
+ r"\w+(?:ed|en)\s*,\s*and\s*\w+(?:ed|en)(?:\s+by\s+[^.;]+)?)",
562
+ source,
563
+ )
564
+ if not match:
565
+ return []
566
+ revised = source[:match.start()] + f"{match['noun']} {match['series']}" + source[match.end():]
567
+ return emit(
568
+ revised,
569
+ "Minimal",
570
+ "COORDINATED_PASSIVE_PARTICIPLES",
571
+ "Reduced a relative passive clause while preserving its coordinated series.",
572
+ .86,
573
+ )
574
+
575
+
576
+ @rule("passive_using_nominal", {"Balanced", "Structural"}, {"PASSIVE_USING"}, .91)
577
+ def tr_passive_using(analysis, profile, user_terms):
578
+ match = re.match(
579
+ r"(?is)^(?P<subject>.+?)\s+"
580
+ r"(?:is|are|was|were|has been|have been|had been)\s+"
581
+ r"(?P<adverb>\w+ly\s+)?evaluated\s+using\s+"
582
+ r"(?P<object>.+?)[.]?$",
583
+ analysis.text,
584
+ )
585
+ if not match:
586
+ return []
587
+ adverb = (match.group("adverb") or "").strip()
588
+ return emit(
589
+ f"Evaluations of {lower_initial(match['subject'])} "
590
+ f"{adverb + ' ' if adverb else ''}use {match['object']}",
591
+ "Balanced",
592
+ "PASSIVE_USING",
593
+ "Recast a passive evaluation without inventing an actor.",
594
+ .91,
595
+ )
596
+
597
+
598
+ @rule("agentless_study_passive", {"Balanced", "Structural"}, {"PASSIVE"}, .70)
599
+ def tr_agentless(analysis, profile, user_terms):
600
+ match = re.match(
601
+ r"(?is)^(?P<topic>.+?)\s+(?:has|have)\s+(?P<also>also\s+)?been\s+"
602
+ r"(?:studied|examined|investigated)\s+"
603
+ r"(?P<context>(?:within|in|across)\s+.+?)[.]?$",
604
+ analysis.text,
605
+ )
606
+ if not match:
607
+ return []
608
+ return emit(
609
+ f"Studies {match['context']} have {match['also'] or ''}examined {match['topic']}",
610
+ "Balanced",
611
+ "PASSIVE",
612
+ "Recast an agentless research passive without inventing a specific actor.",
613
+ .70,
614
+ )
615
+
616
+
617
+ @rule("explicit_agent_active", {"Structural"}, {"PASSIVE"}, .82)
618
+ def tr_explicit_agent(analysis, profile, user_terms):
619
+ match = re.match(
620
+ r"(?is)^(?P<theme>.+?)\s+"
621
+ r"(?:was|were|is|are|has been|have been|had been)\s+"
622
+ r"(?P<verb>evaluated|examined|studied|investigated|tested|measured|reported|"
623
+ r"implemented|applied|modified|trained|collected|derived|obtained|inspected|"
624
+ r"approved|reviewed|documented)\s+by\s+(?P<agent>.+?)[.]?$",
625
+ analysis.text,
626
+ )
627
+ if not match:
628
+ return []
629
+ return emit(
630
+ f"{match['agent']} {match['verb']} {lower_initial(match['theme'])}",
631
+ "Structural",
632
+ "PASSIVE",
633
+ "Converted passive to active because the source supplied the actor.",
634
+ .82,
635
+ )
636
+
637
+
638
+ @rule(
639
+ "reduced_modifier",
640
+ {"Minimal", "Balanced"},
641
+ {"REDUCED_RELATIVE_OR_NP_MODIFIER"},
642
+ .68,
643
+ )
644
+ def tr_reduced(analysis, profile, user_terms):
645
+ source = bare(analysis.text)
646
+ output = []
647
+ for pattern, replacement in [
648
+ (r"\bresults?\s+obtained\s+from\b", "results derived from"),
649
+ (r"\bsamples?\s+obtained\s+from\b", "samples drawn from"),
650
+ (r"\bdata\s+collected\s+from\b", "data gathered from"),
651
+ (r"\bresults?\s+reported\s+independently\b", "independently reported results"),
652
+ ]:
653
+ if re.search(pattern, source, re.I):
654
+ output.append(
655
+ K(
656
+ re.sub(pattern, replacement, source, count=1, flags=re.I),
657
+ "Minimal",
658
+ "REDUCED_RELATIVE_OR_NP_MODIFIER",
659
+ "Recast a reduced participial noun modifier.",
660
+ .68,
661
+ )
662
+ )
663
+ return output
664
+
665
+
666
+ @rule(
667
+ "parenthetical_emphasis",
668
+ {"Minimal", "Balanced"},
669
+ {"PARENTHETICAL_EMPHASIS"},
670
+ .78,
671
+ )
672
+ def tr_parenthetical(analysis, profile, user_terms):
673
+ revised = re.sub(
674
+ r",\s*by\s+(?:itself|themselves)\s*,",
675
+ " alone",
676
+ analysis.text,
677
+ flags=re.I,
678
+ )
679
+ if surface_key(revised) == surface_key(analysis.text):
680
+ return []
681
+ return emit(
682
+ revised,
683
+ "Minimal",
684
+ "PARENTHETICAL_EMPHASIS",
685
+ "Integrated parenthetical emphasis into the clause.",
686
+ .78,
687
+ )
688
+
689
+
690
+ @rule("coordinated_predicate_split", {"Structural"}, {"COORDINATED_PREDICATES"}, .80)
691
+ def tr_coord_base(analysis, profile, user_terms):
692
+ try:
693
+ document = nlp()(bare(analysis.text))
694
+ if not document.has_annotation("DEP"):
695
+ return []
696
+ root = next((token for token in document if token.dep_ == "ROOT"), None)
697
+ if root is None:
698
+ return []
699
+ conjuncts = [
700
+ token for token in root.conjuncts
701
+ if token.pos_ in {"VERB", "AUX"}
702
+ ]
703
+ subjects = [
704
+ child for child in root.children
705
+ if child.dep_ in {"nsubj", "nsubjpass", "csubj"}
706
+ ]
707
+ if len(conjuncts) != 1 or not subjects:
708
+ return []
709
+ conjunct = conjuncts[0]
710
+ cut = min(token.i for token in conjunct.subtree)
711
+ left = re.sub(
712
+ r"(?:,\s*)?\b(?:and|or)\s*$",
713
+ "",
714
+ document[:cut].text.strip(),
715
+ flags=re.I,
716
+ ).strip()
717
+ subject = _safe_subtree_text(subjects[0]).strip()
718
+ right = _safe_subtree_text(conjunct).strip()
719
+ if not left or not subject or not right:
720
+ return []
721
+ return emit(
722
+ f"{left}. {subject} {right}",
723
+ "Structural",
724
+ "COORDINATED_PREDICATES",
725
+ "Split coordinated predicates and repeated only the source subject.",
726
+ .80,
727
+ )
728
+ except Exception:
729
+ return []
730
+
731
+
732
+ @rule("semicolon_split", {"Structural"}, {"LONG_MULTI_CLAUSE"}, .65)
733
+ def tr_semicolon(analysis, profile, user_terms):
734
+ parts = [item.strip() for item in bare(analysis.text).split(";") if item.strip()]
735
+ if len(parts) != 2 or not all(len(item.split()) >= 3 for item in parts):
736
+ return []
737
+ return emit(
738
+ f"{parts[0]}. {parts[1]}",
739
+ "Structural",
740
+ "LONG_MULTI_CLAUSE",
741
+ "Split two propositions already delimited by a semicolon.",
742
+ .65,
743
+ )
744
+
745
+
746
+ # =====================================================================
747
+ # GENERATOR
748
+ # =====================================================================
749
+
750
+ class Generator:
751
+ def applicable(self, analysis, lane):
752
+ detected = set(getattr(analysis, "families", []) or [])
753
+ return sorted(
754
+ [
755
+ registered
756
+ for registered in RULES
757
+ if lane in registered.lanes
758
+ and bool(registered.families & detected)
759
+ ],
760
+ key=lambda registered: -registered.priority,
761
+ )
762
+
763
+ def lane(self, analysis, profile, user_terms, lane, trace):
764
+ output = []
765
+ seen = {surface_key(analysis.text)}
766
+ for registered in self.applicable(analysis, lane):
767
+ try:
768
+ generated = registered.fn(analysis, profile, user_terms) or []
769
+ except Exception as exc:
770
+ trace["rule_errors"].append(
771
+ {
772
+ "rule": registered.name,
773
+ "lane": lane,
774
+ "error": str(exc),
775
+ }
776
+ )
777
+ continue
778
+ for candidate in generated:
779
+ if candidate is None or candidate.lane != lane:
780
+ continue
781
+ key = surface_key(candidate.text)
782
+ if key and key not in seen:
783
+ seen.add(key)
784
+ output.append(candidate)
785
+ if lane == "Minimal" and getattr(CFG, "mlm", False):
786
+ output.extend(self.lexical(analysis, profile, user_terms, trace))
787
+ return output[:16]
788
+
789
+ def generate(self, analysis, profile, user_terms, mode):
790
+ if mode not in {"Minimal", "Balanced", "Structural"}:
791
+ mode = "Balanced"
792
+ trace = {
793
+ "detected_families": list(getattr(analysis, "families", []) or []),
794
+ "applicable": {},
795
+ "generated_by_lane": {},
796
+ "rule_errors": [],
797
+ "candidate_traces": [],
798
+ }
799
+ pools = {}
800
+ for lane in ["Minimal", "Balanced", "Structural"]:
801
+ trace["applicable"][lane] = [
802
+ registered.name
803
+ for registered in self.applicable(analysis, lane)
804
+ ]
805
+ pools[lane] = self.lane(
806
+ analysis,
807
+ profile,
808
+ user_terms,
809
+ lane,
810
+ trace,
811
+ )
812
+ trace["generated_by_lane"][lane] = len(pools[lane])
813
+
814
+ order = {
815
+ "Minimal": ["Minimal"],
816
+ "Balanced": ["Balanced", "Minimal", "Structural"],
817
+ "Structural": ["Structural", "Balanced", "Minimal"],
818
+ }[mode]
819
+
820
+ output = []
821
+ seen = set()
822
+ for lane in order:
823
+ for candidate in pools[lane]:
824
+ key = surface_key(candidate.text)
825
+ if key and key not in seen:
826
+ seen.add(key)
827
+ output.append(candidate)
828
+
829
+ # Bounded depth-2 composition. This invokes registered functions
830
+ # directly and never recurses through Generator.generate.
831
+ if mode in {"Balanced", "Structural"}:
832
+ composed = []
833
+ for base in list(output[:8]):
834
+ try:
835
+ second_analysis = Analyzer().analyze(
836
+ base.text,
837
+ profile,
838
+ user_terms,
839
+ )
840
+ except Exception:
841
+ continue
842
+ next_lanes = ["Minimal"] if mode == "Balanced" else ["Balanced", "Minimal"]
843
+ for next_lane in next_lanes:
844
+ for registered in self.applicable(second_analysis, next_lane)[:4]:
845
+ try:
846
+ second_candidates = registered.fn(
847
+ second_analysis,
848
+ profile,
849
+ user_terms,
850
+ ) or []
851
+ except Exception:
852
+ continue
853
+ for second in second_candidates[:1]:
854
+ key = surface_key(second.text)
855
+ if not key or key in seen:
856
+ continue
857
+ seen.add(key)
858
+ second.lane = mode
859
+ second.family = f"{base.family}+{second.family}"
860
+ continuation = second.reason[:1].lower() + second.reason[1:]
861
+ second.reason = f"{base.reason} Then {continuation}"
862
+ second.priority = (base.priority + second.priority) / 2.0
863
+ composed.append(second)
864
+ output.extend(composed[:8])
865
+
866
+ trace["routes"] = [
867
+ name
868
+ for lane in order
869
+ for name in trace["applicable"].get(lane, [])
870
+ ]
871
+ trace["generated"] = len(output)
872
+ return output, trace
873
+
874
+ def lexical(self, analysis, profile, user_terms, trace=None):
875
+ optional_fn = globals().get("optional")
876
+ if not callable(optional_fn):
877
+ return []
878
+ pipeline = optional_fn("mlm")
879
+ if not pipeline:
880
+ return []
881
+ if "wn" not in globals() or "getInflection" not in globals():
882
+ return []
883
+ try:
884
+ document = nlp()(analysis.text)
885
+ except Exception as exc:
886
+ if trace is not None:
887
+ trace["rule_errors"].append(
888
+ {
889
+ "rule": "contextual_lexical",
890
+ "lane": "Minimal",
891
+ "error": str(exc),
892
+ }
893
+ )
894
+ return []
895
+
896
+ protected = {
897
+ str(term).lower()
898
+ for term in (
899
+ set(getattr(profile, "immutable", set()) or set())
900
+ | set(user_terms or set())
901
+ )
902
+ }
903
+ output = []
904
+ seen = {surface_key(analysis.text)}
905
+ for token in document:
906
+ if token.pos_ not in {"ADJ", "ADV", "VERB"}:
907
+ continue
908
+ if token.is_stop or not token.is_alpha:
909
+ continue
910
+ if token.lemma_.lower() in protected:
911
+ continue
912
+ try:
913
+ allowed = {
914
+ lemma.name().replace("_", " ").lower()
915
+ for synset in wn.synsets(token.lemma_)[:4]
916
+ for lemma in synset.lemmas()
917
+ }
918
+ mask = pipeline.tokenizer.mask_token
919
+ predictions = pipeline(
920
+ analysis.text[:token.idx]
921
+ + mask
922
+ + analysis.text[token.idx + len(token.text):],
923
+ top_k=12,
924
+ )
925
+ except Exception:
926
+ continue
927
+ for prediction in predictions:
928
+ word = str(prediction.get("token_str", "")).strip().lower()
929
+ if not word.isalpha() or word == token.lower_ or word not in allowed:
930
+ continue
931
+ try:
932
+ inflected = getInflection(word, tag=token.tag_)
933
+ replacement = inflected[0] if inflected else word
934
+ except Exception:
935
+ replacement = word
936
+ revised = (
937
+ analysis.text[:token.idx]
938
+ + replacement
939
+ + analysis.text[token.idx + len(token.text):]
940
+ )
941
+ candidate = K(
942
+ revised,
943
+ "Minimal",
944
+ "CONTEXTUAL_LEXICAL",
945
+ "Contextual lexical substitution supported by WordNet and the optional masked-language model.",
946
+ .45,
947
+ )
948
+ key = surface_key(candidate.text)
949
+ if key and key not in seen:
950
+ seen.add(key)
951
+ output.append(candidate)
952
+ break
953
+ if len(output) >= 3:
954
+ break
955
+ return output
956
+
957
+
958
+ # Base registry invariant: 25 unique names before the v4.1 upgrade.
959
+ _BASE_RULE_NAMES = [registered.name for registered in RULES]
960
+ _BASE_DUPLICATES = sorted(
961
+ name
962
+ for name, count in Counter(_BASE_RULE_NAMES).items()
963
+ if count > 1
964
+ )
965
+ if _BASE_DUPLICATES:
966
+ raise RuntimeError(
967
+ "Standalone Cell 5 base registry has duplicate rules: "
968
+ + ", ".join(_BASE_DUPLICATES)
969
+ )
970
+ if len(RULES) != 25:
971
+ raise RuntimeError(
972
+ "Standalone Cell 5 base registry expected 25 rules but found "
973
+ + str(len(RULES))
974
+ )
975
+
976
+ print("=" * 78)
977
+ print("CELL 5 BASE UNIVERSAL TRANSFORMATION ENGINE LOADED")
978
+ print("=" * 78)
979
+ print("Base rules:", len(RULES))
980
+ print("Base duplicate rule names:", _BASE_DUPLICATES)
981
+ print("Generator: defined")
982
+ print("=" * 78)
983
+
984
+ #@title 5. UNIVERSAL REPLACEMENT v4.1 — production primary-clause upgrade
985
+ # REPLACE the earlier Cell 5 v4 upgrade block with this block.
986
+ # Keep the original Cell 5 v3 registry/generator above it.
987
+ #
988
+ # Design goals:
989
+ # - deterministic and non-generative;
990
+ # - preserve enumerations verbatim whenever possible;
991
+ # - prefer one-sentence recasts to avoid false enumeration loss;
992
+ # - use spaCy dependency/noun-chunk evidence when available;
993
+ # - use bounded surface fallbacks when dependency parsing is unavailable;
994
+ # - never create a candidate merely to force novelty;
995
+ # - replace same-name rules without increasing the registry beyond 30.
996
+
997
+ from collections import Counter
998
+ import re
999
+
1000
+
1001
+ # =====================================================================
1002
+ # 0. SHARED PRODUCTION HELPERS
1003
+ # =====================================================================
1004
+
1005
+ _V41_PARTICIPLE_TO_BASE = {
1006
+ "spanning": "span",
1007
+ "covering": "cover",
1008
+ "including": "include",
1009
+ "containing": "contain",
1010
+ "involving": "involve",
1011
+ }
1012
+
1013
+ _V41_IRREGULAR_3SG = {
1014
+ "have": "has",
1015
+ "do": "does",
1016
+ "go": "goes",
1017
+ }
1018
+
1019
+
1020
+ def _v41_distinct(source, candidate):
1021
+ return bool(candidate and surface_key(source) != surface_key(candidate))
1022
+
1023
+
1024
+ def _v41_balanced_parentheses(text):
1025
+ pairs = {"(": ")", "[": "]", "{": "}"}
1026
+ stack = []
1027
+ for char in str(text or ""):
1028
+ if char in pairs:
1029
+ stack.append(pairs[char])
1030
+ elif char in pairs.values():
1031
+ if not stack or stack.pop() != char:
1032
+ return False
1033
+ return not stack
1034
+
1035
+
1036
+ def _v41_sentence_doc(text):
1037
+ try:
1038
+ doc = nlp()(str(text or ""))
1039
+ if len(doc):
1040
+ return doc
1041
+ except Exception:
1042
+ pass
1043
+ return None
1044
+
1045
+
1046
+ def _v41_subtree_span(token):
1047
+ items = list(token.subtree)
1048
+ if not items:
1049
+ return None
1050
+ start = min(item.i for item in items)
1051
+ end = max(item.i for item in items) + 1
1052
+ return token.doc[start:end]
1053
+
1054
+
1055
+ def _v41_noun_chunk_for_token(doc, token):
1056
+ try:
1057
+ chunks = list(doc.noun_chunks)
1058
+ except Exception:
1059
+ chunks = []
1060
+
1061
+ containing = [chunk for chunk in chunks if chunk.start <= token.i < chunk.end]
1062
+ if not containing:
1063
+ return None
1064
+
1065
+ # Prefer the narrowest chunk containing the syntactic head.
1066
+ return min(containing, key=lambda chunk: chunk.end - chunk.start)
1067
+
1068
+
1069
+ def _v41_present_verb(base, singular):
1070
+ base = str(base or "").lower()
1071
+ if not singular:
1072
+ return base
1073
+ if base in _V41_IRREGULAR_3SG:
1074
+ return _V41_IRREGULAR_3SG[base]
1075
+ if re.search(r"(?:s|x|z|ch|sh|o)$", base):
1076
+ return base + "es"
1077
+ if re.search(r"[^aeiou]y$", base):
1078
+ return base[:-1] + "ies"
1079
+ return base + "s"
1080
+
1081
+
1082
+ def _v41_token_is_singular(token):
1083
+ try:
1084
+ number = token.morph.get("Number")
1085
+ if "Sing" in number:
1086
+ return True
1087
+ if "Plur" in number:
1088
+ return False
1089
+ except Exception:
1090
+ pass
1091
+
1092
+ # Conservative surface fallback.
1093
+ return not token.text.lower().endswith("s")
1094
+
1095
+
1096
+ def _v41_list_preserved(source_fragment, candidate):
1097
+ """Require list-bearing source fragments to survive literally."""
1098
+ fragment = re.sub(r"\s+", " ", str(source_fragment or "")).strip(" .")
1099
+ candidate_norm = re.sub(r"\s+", " ", str(candidate or ""))
1100
+ return not fragment or fragment.lower() in candidate_norm.lower()
1101
+
1102
+
1103
+ def _v41_emit_if_safe(source, text, lane, family, reason, priority):
1104
+ candidate = clean(text)
1105
+ if not _v41_distinct(source, candidate):
1106
+ return []
1107
+ if not _v41_balanced_parentheses(candidate):
1108
+ return []
1109
+ return [K(candidate, lane, family, reason, priority)]
1110
+
1111
+
1112
+ # =====================================================================
1113
+ # 26. FINITE PREDICATE + WITHOUT COMPLEMENT LIST
1114
+ # =====================================================================
1115
+
1116
+ @rule(
1117
+ "without_complement_list",
1118
+ {"Balanced", "Structural"},
1119
+ {
1120
+ "FINITE_COMPLEMENT_LIST",
1121
+ "COORDINATED_OBJECTS_OR_ENUMERATION",
1122
+ "ENUMERATION",
1123
+ "LONG_MULTI_CLAUSE",
1124
+ },
1125
+ .92,
1126
+ )
1127
+ def tr_without_complement_list(analysis, profile, user_terms):
1128
+ source = bare(analysis.text)
1129
+
1130
+ match = re.match(
1131
+ r"(?is)^(?P<main>.+?)\s+without\s+"
1132
+ r"(?P<missing>.+?,\s*.+?(?:,\s*)?(?:and|or)\s+.+?)$",
1133
+ source,
1134
+ )
1135
+ if not match:
1136
+ return []
1137
+
1138
+ main = match["main"].strip()
1139
+ missing = match["missing"].strip()
1140
+ outputs = []
1141
+
1142
+ # Candidate A keeps the exact polarity marker and the complete list.
1143
+ text_a = f"Without {missing}, {lower_initial(main)}"
1144
+ if _v41_list_preserved(missing, text_a):
1145
+ outputs.extend(_v41_emit_if_safe(
1146
+ source,
1147
+ text_a,
1148
+ "Balanced",
1149
+ "FINITE_COMPLEMENT_LIST",
1150
+ "Fronted the existing without-complement while preserving the exact polarity marker and complete list.",
1151
+ .92,
1152
+ ))
1153
+
1154
+ # Candidate B preserves attachment in sentence-final position while
1155
+ # creating a distinct clause boundary. It is useful when a validator is
1156
+ # sensitive to fronted negative adjuncts.
1157
+ text_b = f"{main}; this occurs without {missing}"
1158
+ if _v41_list_preserved(missing, text_b):
1159
+ outputs.extend(_v41_emit_if_safe(
1160
+ source,
1161
+ text_b,
1162
+ "Structural",
1163
+ "FINITE_COMPLEMENT_LIST",
1164
+ "Separated the main proposition from its existing without-complement without changing negative scope.",
1165
+ .86,
1166
+ ))
1167
+
1168
+ return outputs
1169
+
1170
+
1171
+ # =====================================================================
1172
+ # 27. PROPOSAL FRAME + APPOSITIVE DEFINITION
1173
+ # =====================================================================
1174
+
1175
+ @rule(
1176
+ "proposal_appositive_definition",
1177
+ {"Balanced", "Structural"},
1178
+ {
1179
+ "PROPOSAL_FRAME",
1180
+ "APPOSITIVE_DEFINITION",
1181
+ "COORDINATED_OBJECTS_OR_ENUMERATION",
1182
+ "ENUMERATION",
1183
+ "LONG_MULTI_CLAUSE",
1184
+ },
1185
+ .98,
1186
+ )
1187
+ def tr_proposal_appositive_definition(analysis, profile, user_terms):
1188
+ source = bare(analysis.text)
1189
+
1190
+ match = re.match(
1191
+ r"(?is)^(?P<subject>We|I|The\s+(?:study|paper|article|report|team|authors?))\s+"
1192
+ r"(?P<verb>propose|proposes|introduce|introduces|present|presents)\s+"
1193
+ r"(?P<name>.+?)\s*,\s*"
1194
+ r"(?P<article>an?|the)\s+"
1195
+ r"(?P<definition>.+)$",
1196
+ source,
1197
+ )
1198
+ if not match:
1199
+ return []
1200
+
1201
+ name = match["name"].strip()
1202
+ definition = match["definition"].strip()
1203
+ article = match["article"].lower()
1204
+
1205
+ if not _v41_balanced_parentheses(name):
1206
+ return []
1207
+
1208
+ outputs = []
1209
+
1210
+ # Keep the name and full appositive enumeration in one sentence so
1211
+ # whole-candidate enumeration comparison remains stable.
1212
+ text_a = (
1213
+ f"{name} is {article} {definition} proposed by "
1214
+ f"{lower_initial(match['subject'])}"
1215
+ )
1216
+ if _v41_list_preserved(definition, text_a):
1217
+ outputs.extend(_v41_emit_if_safe(
1218
+ source,
1219
+ text_a,
1220
+ "Balanced",
1221
+ "APPOSITIVE_DEFINITION",
1222
+ "Converted the proposal-plus-appositive frame into one definitional sentence while preserving the complete definition.",
1223
+ .98,
1224
+ ))
1225
+
1226
+ # A second one-sentence option retains the source's proposal predicate.
1227
+ text_b = (
1228
+ f"{match['subject']} {match['verb']} {name} as "
1229
+ f"{article} {definition}"
1230
+ )
1231
+ if _v41_list_preserved(definition, text_b):
1232
+ outputs.extend(_v41_emit_if_safe(
1233
+ source,
1234
+ text_b,
1235
+ "Structural",
1236
+ "PROPOSAL_FRAME",
1237
+ "Recast the appositive as an explicit as-complement while preserving the name and complete definition.",
1238
+ .94,
1239
+ ))
1240
+
1241
+ return outputs
1242
+
1243
+
1244
+ # =====================================================================
1245
+ # 28. PARTICIPIAL POSTMODIFIER
1246
+ # =====================================================================
1247
+
1248
+ @rule(
1249
+ "participial_postmodifier",
1250
+ {"Balanced", "Structural"},
1251
+ {
1252
+ "PARTICIPIAL_POSTMODIFIER",
1253
+ "COORDINATED_OBJECTS_OR_ENUMERATION",
1254
+ "ENUMERATION",
1255
+ "NOMINALIZATION",
1256
+ },
1257
+ .94,
1258
+ )
1259
+ def tr_participial_postmodifier(analysis, profile, user_terms):
1260
+ source = bare(analysis.text)
1261
+ doc = _v41_sentence_doc(source)
1262
+ outputs = []
1263
+
1264
+ if doc is not None and doc.has_annotation("DEP"):
1265
+ targets = [
1266
+ token
1267
+ for token in doc
1268
+ if token.text.lower() in _V41_PARTICIPLE_TO_BASE
1269
+ and token.pos_ in {"VERB", "ADJ"}
1270
+ ]
1271
+
1272
+ for participle in targets:
1273
+ # In common parses, the participial modifier is attached to the
1274
+ # noun it modifies through acl/relcl or an equivalent relation.
1275
+ head = participle.head
1276
+ if head is participle or head.pos_ not in {"NOUN", "PROPN", "PRON"}:
1277
+ continue
1278
+
1279
+ chunk = _v41_noun_chunk_for_token(doc, head)
1280
+ if chunk is None:
1281
+ chunk_start = head.left_edge.i
1282
+ chunk_end = head.i + 1
1283
+ else:
1284
+ chunk_start = chunk.start
1285
+ chunk_end = chunk.end
1286
+
1287
+ if participle.i < chunk_end:
1288
+ continue
1289
+
1290
+ # The complement is the participle subtree excluding the
1291
+ # participle token itself. This preserves enumerated material.
1292
+ subtree = _v41_subtree_span(participle)
1293
+ if subtree is None:
1294
+ continue
1295
+
1296
+ complement_start = participle.i + 1
1297
+ complement_end = subtree.end
1298
+ if complement_start >= complement_end:
1299
+ continue
1300
+
1301
+ complement = doc[complement_start:complement_end].text.strip()
1302
+ if not complement:
1303
+ continue
1304
+
1305
+ base = _V41_PARTICIPLE_TO_BASE[participle.text.lower()]
1306
+ finite = _v41_present_verb(base, _v41_token_is_singular(head))
1307
+
1308
+ prefix = doc[:participle.i].text.rstrip()
1309
+ suffix = doc[complement_end:].text.lstrip()
1310
+ candidate = f"{prefix} that {finite} {complement}"
1311
+ if suffix:
1312
+ candidate += " " + suffix
1313
+
1314
+ if _v41_list_preserved(complement, candidate):
1315
+ outputs.extend(_v41_emit_if_safe(
1316
+ source,
1317
+ candidate,
1318
+ "Balanced",
1319
+ "PARTICIPIAL_POSTMODIFIER",
1320
+ "Converted an attested participial noun modifier into a finite relative clause while preserving its head and complement.",
1321
+ .94,
1322
+ ))
1323
+
1324
+ if outputs:
1325
+ return outputs
1326
+
1327
+ # Bounded fallback for environments without a dependency parse.
1328
+ fallback = re.match(
1329
+ r"(?is)^(?P<prefix>.+?\b(?:to|for|with|among|across)\s+)"
1330
+ r"(?P<head>(?:[A-Za-z][\w-]*\s+){0,5}[A-Za-z][\w-]*)\s+"
1331
+ r"(?P<participle>spanning|covering|including|containing|involving)\s+"
1332
+ r"(?P<items>.+?,\s*.+?(?:,\s*)?(?:and|or)\s+.+?)$",
1333
+ source,
1334
+ )
1335
+ if not fallback:
1336
+ return []
1337
+
1338
+ head = fallback["head"].strip()
1339
+ final_noun = head.split()[-1]
1340
+ singular = not final_noun.lower().endswith("s")
1341
+ base = _V41_PARTICIPLE_TO_BASE[fallback["participle"].lower()]
1342
+ finite = _v41_present_verb(base, singular)
1343
+ candidate = (
1344
+ f"{fallback['prefix']}{head} that {finite} {fallback['items']}"
1345
+ )
1346
+
1347
+ if not _v41_list_preserved(fallback["items"], candidate):
1348
+ return []
1349
+
1350
+ return _v41_emit_if_safe(
1351
+ source,
1352
+ candidate,
1353
+ "Balanced",
1354
+ "PARTICIPIAL_POSTMODIFIER",
1355
+ "Converted a bounded participial postmodifier into a finite relative clause while preserving its noun phrase and complete list.",
1356
+ .88,
1357
+ )
1358
+
1359
+
1360
+ # =====================================================================
1361
+ # 29. FINITE VERB + COMPLEX OBJECT LIST
1362
+ # =====================================================================
1363
+
1364
+ @rule(
1365
+ "finite_object_list_frame",
1366
+ {"Balanced", "Structural"},
1367
+ {
1368
+ "FINITE_OBJECT_LIST_FRAME",
1369
+ "COORDINATED_OBJECTS_OR_ENUMERATION",
1370
+ "ENUMERATION",
1371
+ "LONG_MULTI_CLAUSE",
1372
+ },
1373
+ .88,
1374
+ )
1375
+ def tr_finite_object_list_frame(analysis, profile, user_terms):
1376
+ source = bare(analysis.text)
1377
+
1378
+ match = re.match(
1379
+ r"(?is)^(?P<subject>We|I|They|The\s+[A-Za-z][\w-]*(?:\s+[A-Za-z][\w-]*){0,4})\s+"
1380
+ r"(?P<verb>pre-register|preregister|document|record|evaluate|assess|review|"
1381
+ r"analyze|compare|test|measure|include|identify|specify|report)\s+"
1382
+ r"(?P<items>.+?,\s*.+?(?:,\s*)?(?:and|or)\s+.+?)$",
1383
+ source,
1384
+ )
1385
+ if not match:
1386
+ return []
1387
+
1388
+ nominal = {
1389
+ "pre-register": "preregistration",
1390
+ "preregister": "preregistration",
1391
+ "document": "documentation",
1392
+ "record": "record",
1393
+ "evaluate": "evaluation",
1394
+ "assess": "assessment",
1395
+ "review": "review",
1396
+ "analyze": "analysis",
1397
+ "compare": "comparison",
1398
+ "test": "testing",
1399
+ "measure": "measurement",
1400
+ "include": "scope",
1401
+ "identify": "identification",
1402
+ "specify": "specification",
1403
+ "report": "report",
1404
+ }.get(match["verb"].lower())
1405
+
1406
+ if not nominal:
1407
+ return []
1408
+
1409
+ subject = match["subject"].strip()
1410
+ possessive = {"we": "Our", "i": "My", "they": "Their"}.get(subject.lower())
1411
+
1412
+ if possessive:
1413
+ candidate = f"{possessive} {nominal} covers {match['items']}"
1414
+ else:
1415
+ candidate = (
1416
+ f"The {nominal} conducted by {lower_initial(subject)} "
1417
+ f"covers {match['items']}"
1418
+ )
1419
+
1420
+ if not _v41_list_preserved(match["items"], candidate):
1421
+ return []
1422
+
1423
+ return _v41_emit_if_safe(
1424
+ source,
1425
+ candidate,
1426
+ "Balanced",
1427
+ "FINITE_OBJECT_LIST_FRAME",
1428
+ "Recast the finite predicate as an event frame while preserving every listed object verbatim.",
1429
+ .88,
1430
+ )
1431
+
1432
+
1433
+ # =====================================================================
1434
+ # 30. CONTINGENCY / DEPENDENCY RELATION
1435
+ # =====================================================================
1436
+
1437
+ @rule(
1438
+ "contingency_relation",
1439
+ {"Minimal", "Balanced", "Structural"},
1440
+ {
1441
+ "CONTINGENCY_RELATION",
1442
+ "COORDINATED_OBJECTS_OR_ENUMERATION",
1443
+ "ENUMERATION",
1444
+ "NOMINALIZATION",
1445
+ },
1446
+ .96,
1447
+ )
1448
+ def tr_contingency_relation(analysis, profile, user_terms):
1449
+ source = bare(analysis.text)
1450
+
1451
+ match = re.match(
1452
+ r"(?is)^(?P<subject>.+?)\s+"
1453
+ r"(?P<copula>is|are|was|were)\s+contingent\s+on\s+"
1454
+ r"(?P<basis>.+)$",
1455
+ source,
1456
+ )
1457
+ if not match:
1458
+ return []
1459
+
1460
+ verb = {
1461
+ "is": "depends",
1462
+ "are": "depend",
1463
+ "was": "depended",
1464
+ "were": "depended",
1465
+ }[match["copula"].lower()]
1466
+
1467
+ return _v41_emit_if_safe(
1468
+ source,
1469
+ f"{match['subject']} {verb} on {match['basis']}",
1470
+ "Minimal",
1471
+ "CONTINGENCY_RELATION",
1472
+ "Recast the contingent-on relation as an equivalent dependency relation.",
1473
+ .96,
1474
+ )
1475
+
1476
+
1477
+ # =====================================================================
1478
+ # 31. SAFER COORDINATED-PREDICATE SPLIT
1479
+ # =====================================================================
1480
+
1481
+ @rule(
1482
+ "coordinated_predicate_split",
1483
+ {"Structural"},
1484
+ {"COORDINATED_PREDICATES"},
1485
+ .86,
1486
+ )
1487
+ def tr_coord_safe(analysis, profile, user_terms):
1488
+ try:
1489
+ document = nlp()(bare(analysis.text))
1490
+ if not document.has_annotation("DEP"):
1491
+ return []
1492
+
1493
+ root = next((token for token in document if token.dep_ == "ROOT"), None)
1494
+ if root is None or root.pos_ not in {"VERB", "AUX"}:
1495
+ return []
1496
+
1497
+ subjects = [
1498
+ child for child in root.children
1499
+ if child.dep_ in {"nsubj", "nsubjpass", "csubj"}
1500
+ ]
1501
+ conjuncts = [
1502
+ token for token in root.conjuncts
1503
+ if token.pos_ in {"VERB", "AUX"}
1504
+ and token.dep_ == "conj"
1505
+ and token.i > root.i
1506
+ ]
1507
+ if len(subjects) != 1 or len(conjuncts) != 1:
1508
+ return []
1509
+
1510
+ conjunct = conjuncts[0]
1511
+ between = document[root.i + 1:conjunct.i].text
1512
+ if not re.search(r"(?:,\s*)?\b(?:and|or)\b", between, re.I):
1513
+ return []
1514
+
1515
+ subject = sub(subjects[0]).strip()
1516
+ right = sub(conjunct).strip()
1517
+ cut = min(token.i for token in conjunct.subtree)
1518
+ left = re.sub(
1519
+ r"(?:,\s*)?\b(?:and|or)\s*$",
1520
+ "",
1521
+ document[:cut].text.strip(),
1522
+ flags=re.I,
1523
+ ).strip()
1524
+
1525
+ if not left or not subject or not right:
1526
+ return []
1527
+ if len(left.split()) < 4:
1528
+ return []
1529
+ if re.match(r"(?is)^(?:propose|introduce|present)\b", right):
1530
+ return []
1531
+
1532
+ return _v41_emit_if_safe(
1533
+ bare(analysis.text),
1534
+ f"{left}. {subject} {right}",
1535
+ "Structural",
1536
+ "COORDINATED_PREDICATES",
1537
+ "Split two dependency-verified coordinated predicates and repeated only the attested subject.",
1538
+ .86,
1539
+ )
1540
+ except Exception:
1541
+ return []
1542
+
1543
+
1544
+ # =====================================================================
1545
+ # V4.1 SELF-TESTS AND REGISTRY CHECK
1546
+ # =====================================================================
1547
+
1548
+ _V41_REQUIRED_RULES = {
1549
+ "without_complement_list",
1550
+ "proposal_appositive_definition",
1551
+ "participial_postmodifier",
1552
+ "finite_object_list_frame",
1553
+ "contingency_relation",
1554
+ "coordinated_predicate_split",
1555
+ }
1556
+
1557
+ _v41_names = [registered.name for registered in RULES]
1558
+ _v41_missing = sorted(_V41_REQUIRED_RULES - set(_v41_names))
1559
+ _v41_duplicates = sorted(
1560
+ name for name, count in Counter(_v41_names).items() if count > 1
1561
+ )
1562
+
1563
+ if _v41_missing:
1564
+ raise RuntimeError("Cell 5 v4.1 missing rules: " + ", ".join(_v41_missing))
1565
+ if _v41_duplicates:
1566
+ raise RuntimeError("Cell 5 v4.1 duplicate rules: " + ", ".join(_v41_duplicates))
1567
+ if len(RULES) != 30:
1568
+ raise RuntimeError(
1569
+ "Cell 5 v4.1 expected 30 rules but found " + str(len(RULES))
1570
+ )
1571
+
1572
+ # Parser-independent invariant tests.
1573
+ if not _v41_balanced_parentheses("Framework (ALCEF-RSF)"):
1574
+ raise RuntimeError("Cell 5 v4.1 parenthesis invariant failed.")
1575
+ if _v41_balanced_parentheses("Framework (ALCEF-RSF"):
1576
+ raise RuntimeError("Cell 5 v4.1 unbalanced-parenthesis test failed.")
1577
+ if not _v41_list_preserved("A, B, and C", "The scope covers A, B, and C."):
1578
+ raise RuntimeError("Cell 5 v4.1 list-preservation invariant failed.")
1579
+ if _v41_present_verb("span", True) != "spans":
1580
+ raise RuntimeError("Cell 5 v4.1 singular conjugation test failed.")
1581
+ if _v41_present_verb("span", False) != "span":
1582
+ raise RuntimeError("Cell 5 v4.1 plural conjugation test failed.")
1583
+
1584
+ print("=" * 78)
1585
+ print("CELL 5 UNIVERSAL TRANSFORMATION ENGINE v4.1 LOADED")
1586
+ print("=" * 78)
1587
+ print("Dependency-aware noun-head extraction: active")
1588
+ print("Bounded surface fallback: active")
1589
+ print("Literal list-preservation checks: active")
1590
+ print("One-sentence proposal/appositive recasts: active")
1591
+ print("Polarity-marker preservation candidates: active")
1592
+ print("Finite relative-clause postmodifier recast: active")
1593
+ print("Safer coordinated-predicate split: active")
1594
+ print("Missing v4.1 rules:", _v41_missing)
1595
+ print("Duplicate rule names:", _v41_duplicates)
1596
+ print("Registered transformation rules:", len(RULES))
1597
+ print("Expected rule count: 30")
1598
+ print("Invariant self-tests: PASSED")
1599
+ print("=" * 78)
runtime/cell5a.py ADDED
@@ -0,0 +1,574 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #@title 5A. CANDIDATE REALIZATION RELIABILITY PATCH v3.4
2
+ # Complete replacement for Cell 5A v3.3.
3
+ # Run immediately after Cell 5 v4.1 and before Cell 6 v5.
4
+ #
5
+ # Purpose:
6
+ # - reject malformed candidates at both generation stages;
7
+ # - preserve valid relative clauses, appositives, lists, and terminology;
8
+ # - remain rerun-safe after Generator is rebuilt by Cell 5;
9
+ # - add no transformation families and change no semantic policy;
10
+ # - expose precise rejection evidence to Cell 7, Cell 7B, and Cell 9.
11
+
12
+ import re
13
+ from collections import Counter
14
+
15
+
16
+ # =====================================================================
17
+ # 0. PREREQUISITE AND VERSION CHECKS
18
+ # =====================================================================
19
+
20
+ _REQUIRED_5A = [
21
+ "RULES", "rule", "Candidate", "Generator", "clean", "K",
22
+ "lower_initial", "surface_key", "nlp",
23
+ ]
24
+
25
+ _missing_5a = [name for name in _REQUIRED_5A if name not in globals()]
26
+ if _missing_5a:
27
+ raise RuntimeError(
28
+ "Cell 5A cannot run. Missing: " + ", ".join(_missing_5a)
29
+ + ". Run the complete Cell 5 v4.1 first."
30
+ )
31
+
32
+ EXPECTED_5A_RULE_COUNT = 30
33
+ if len(RULES) != EXPECTED_5A_RULE_COUNT:
34
+ raise RuntimeError(
35
+ f"Cell 5A expected {EXPECTED_5A_RULE_COUNT} Cell 5 v4.1 rules, "
36
+ f"but found {len(RULES)}. Rerun Cell 5 v4.1 first."
37
+ )
38
+
39
+
40
+ # =====================================================================
41
+ # 1. HIGH-CONFIDENCE REALIZATION CHECKS
42
+ # =====================================================================
43
+
44
+ _SUBJECT_PRONOUNS = {"i", "we", "he", "she", "they", "who"}
45
+ _OBJECT_PRONOUNS = {"me", "us", "him", "her", "them", "whom"}
46
+ _FINITE_TAGS = {"VBD", "VBP", "VBZ", "MD"}
47
+ _DECLARATIVE_STARTERS = {
48
+ "i", "we", "you", "he", "she", "it", "they",
49
+ "this", "that", "these", "those", "the", "a", "an",
50
+ }
51
+ _RELATIVE_MARKERS = {"that", "which", "who", "whose", "where"}
52
+
53
+
54
+ def _r34_split_sentences(text):
55
+ """Return bounded sentence units without modifying source content."""
56
+ value = clean(text)
57
+ try:
58
+ document = nlp()(value)
59
+ units = [sent.text.strip() for sent in document.sents if sent.text.strip()]
60
+ if units:
61
+ return units
62
+ except Exception:
63
+ pass
64
+
65
+ return [
66
+ part.strip()
67
+ for part in re.split(r"(?<=[.!?])\s+", value)
68
+ if part.strip()
69
+ ]
70
+
71
+
72
+ def _r34_parse(text):
73
+ try:
74
+ document = nlp()(str(text or ""))
75
+ return document if len(document) else None
76
+ except Exception:
77
+ return None
78
+
79
+
80
+ def _r34_has_finite_predicate(document):
81
+ if document is None:
82
+ return False
83
+
84
+ for token in document:
85
+ if token.tag_ in _FINITE_TAGS:
86
+ return True
87
+ if token.pos_ in {"VERB", "AUX"} and any(
88
+ child.dep_ in {"aux", "auxpass"} and child.tag_ in _FINITE_TAGS
89
+ for child in token.children
90
+ ):
91
+ return True
92
+ return False
93
+
94
+
95
+ def _r34_relative_clause_is_finite(document):
96
+ """Validate finite predicates after explicit relative markers."""
97
+ if document is None:
98
+ return True
99
+
100
+ for token in document:
101
+ if token.lower_ not in _RELATIVE_MARKERS:
102
+ continue
103
+
104
+ following = document[token.i + 1:]
105
+ if not following:
106
+ return False
107
+
108
+ if not any(
109
+ item.tag_ in _FINITE_TAGS
110
+ or item.pos_ == "AUX"
111
+ for item in following
112
+ ):
113
+ return False
114
+
115
+ return True
116
+
117
+
118
+ def _r34_orphan_participial_fragment(sentence):
119
+ """
120
+ Detect noun/preposition fragments such as:
121
+ 'Protocol to small cybersecurity learners spanning ...'
122
+ while allowing a valid finite relative clause:
123
+ 'learners that span ...'
124
+ """
125
+ low = sentence.lower().strip()
126
+
127
+ if re.search(r"\b(?:that|which|who)\s+(?:span|spans|cover|covers|include|includes|contain|contains|involve|involves)\b", low):
128
+ return False
129
+
130
+ document = _r34_parse(sentence)
131
+ if document is not None and document.has_annotation("DEP"):
132
+ root = next((token for token in document if token.dep_ == "ROOT"), None)
133
+ if root is not None:
134
+ if root.pos_ in {"VERB", "AUX"} and _r34_has_finite_predicate(document):
135
+ return False
136
+
137
+ participles = [
138
+ token for token in document
139
+ if token.text.lower() in {
140
+ "spanning", "covering", "including", "containing", "involving"
141
+ }
142
+ ]
143
+ if participles and not _r34_has_finite_predicate(document):
144
+ return True
145
+
146
+ return bool(re.match(
147
+ r"(?is)^(?:[A-Z][\w-]*|The|A|An)\b.+\s+"
148
+ r"(?:spanning|covering|including|containing|involving)\s+.+$",
149
+ sentence,
150
+ )) and not bool(re.search(
151
+ r"\b(?:is|are|was|were|has|have|had|do|does|did|can|could|may|might|"
152
+ r"must|should|will|would|span|spans|cover|covers|include|includes|"
153
+ r"contain|contains|involve|involves)\b",
154
+ low,
155
+ ))
156
+
157
+
158
+ def _r34_clause_issues(text):
159
+ issues = []
160
+
161
+ for index, sentence in enumerate(_r34_split_sentences(text), start=1):
162
+ words = re.findall(r"[A-Za-z]+(?:[-'][A-Za-z]+)*", sentence)
163
+ if not words:
164
+ issues.append(f"EMPTY_SENTENCE_{index}")
165
+ continue
166
+
167
+ first = words[0].lower()
168
+ document = _r34_parse(sentence)
169
+
170
+ if first in _DECLARATIVE_STARTERS and not _r34_has_finite_predicate(document):
171
+ issues.append(f"NONFINITE_DECLARATIVE_FRAGMENT_{index}")
172
+
173
+ if document is not None:
174
+ root = next((token for token in document if token.dep_ == "ROOT"), None)
175
+ if (
176
+ first in _DECLARATIVE_STARTERS
177
+ and root is not None
178
+ and root.pos_ not in {"VERB", "AUX"}
179
+ and not _r34_has_finite_predicate(document)
180
+ ):
181
+ issues.append(f"NOMINAL_ROOT_FRAGMENT_{index}")
182
+
183
+ if not _r34_relative_clause_is_finite(document):
184
+ issues.append(f"NONFINITE_RELATIVE_CLAUSE_{index}")
185
+ else:
186
+ if re.match(
187
+ r"(?is)^(?:I|We|You|He|She|It|They|The|A|An|This|That|These|Those)\s+"
188
+ r"(?:[A-Z][\w-]*\s+){1,8}"
189
+ r"(?:Framework|System|Model|Protocol|Method|Procedure|Process|Team|Study)\b",
190
+ sentence,
191
+ ):
192
+ issues.append(f"LIKELY_NOMINAL_FRAGMENT_{index}")
193
+
194
+ if _r34_orphan_participial_fragment(sentence):
195
+ issues.append(f"ORPHAN_PARTICIPIAL_FRAGMENT_{index}")
196
+
197
+ return issues
198
+
199
+
200
+ def _r34_balanced_delimiters(text):
201
+ pairs = {"(": ")", "[": "]", "{": "}"}
202
+ stack = []
203
+ for char in str(text or ""):
204
+ if char in pairs:
205
+ stack.append(pairs[char])
206
+ elif char in pairs.values():
207
+ if not stack or stack.pop() != char:
208
+ return False
209
+ return not stack
210
+
211
+
212
+ def realization_issues(text):
213
+ """Return only high-confidence grammatical/realization defects."""
214
+ value = clean(text)
215
+ low = value.lower()
216
+ issues = []
217
+
218
+ if not value:
219
+ return ["EMPTY_OUTPUT"]
220
+
221
+ if not _r34_balanced_delimiters(value):
222
+ issues.append("UNBALANCED_DELIMITERS")
223
+
224
+ if re.search(
225
+ r"\b(?:for|to|with|by|from|of|about)\s+(?:i|we|he|she|they|who)\b",
226
+ low,
227
+ ):
228
+ issues.append("SUBJECT_PRONOUN_AFTER_PREPOSITION")
229
+
230
+ if re.match(
231
+ r"^(?:me|us|him|her|them|whom)\s+"
232
+ r"(?:am|is|are|was|were|must|should|can|could|may|might|will|would)\b",
233
+ low,
234
+ ):
235
+ issues.append("OBJECT_PRONOUN_AS_SUBJECT")
236
+
237
+ if re.match(
238
+ r"^(?:inspect|distinguish|evaluate|examine|analyze|compare|apply|use|"
239
+ r"test|measure|determine|identify|review|assess|implement|investigate)"
240
+ r"\b.+\s+is\s+the\s+(?:next|first|second|third|final|primary|main)\s+"
241
+ r"(?:step|task|action|objective|goal)\b",
242
+ low,
243
+ ):
244
+ issues.append("BARE_ACTION_AS_NOMINAL_SUBJECT")
245
+
246
+ if re.search(r"\b(?:is|are|was|were)\s+(?:is|are|was|were)\b", low):
247
+ issues.append("DUPLICATED_COPULA")
248
+
249
+ if re.search(r"\b(?:has|have|had)\s+been\s+been\b", low):
250
+ issues.append("DUPLICATED_AUXILIARY")
251
+
252
+ if re.search(r"\b(?:a|an)\s+(?:are|were|have|do)\b", low):
253
+ issues.append("DETERMINER_BEFORE_FINITE_VERB")
254
+
255
+ if re.search(
256
+ r"\b(?:of|for|with|by|from|to|in|on|under|within|across)\s+"
257
+ r"(?:The|A|An)\s+\w+",
258
+ value,
259
+ ):
260
+ issues.append("INTERNAL_CAPITALIZED_DETERMINER")
261
+
262
+ if re.search(r"\s+[,.!?;:]", value):
263
+ issues.append("SPACE_BEFORE_PUNCTUATION")
264
+
265
+ if re.search(r"[.!?]{2,}", value):
266
+ issues.append("DUPLICATED_TERMINAL_PUNCTUATION")
267
+
268
+ if len(re.findall(r"\b\w+\b", value)) < 3:
269
+ issues.append("TOO_SHORT")
270
+
271
+ issues.extend(_r34_clause_issues(value))
272
+ return list(dict.fromkeys(issues))
273
+
274
+
275
+ # =====================================================================
276
+ # 2. RELIABLE SAME-NAME RULE OVERRIDES
277
+ # =====================================================================
278
+
279
+ @rule(
280
+ "copular_action_frame",
281
+ {"Balanced", "Structural"},
282
+ {"COPULAR_ACTION_FRAME"},
283
+ .96,
284
+ )
285
+ def tr_copular_action_reliable(analysis, profile, user_terms):
286
+ match = re.match(
287
+ r"(?is)^The\s+(?P<order>next|first|second|third|final|primary|main)\s+"
288
+ r"(?P<label>step|task|action|objective|goal)\s+is\s+to\s+"
289
+ r"(?P<action>.+?)[.]?$",
290
+ analysis.text,
291
+ )
292
+ if not match:
293
+ return []
294
+
295
+ action = match["action"].strip()
296
+ first_match = re.match(r"(?is)^(?P<verb>[A-Za-z-]+)(?P<rest>.*)$", action)
297
+ if not first_match:
298
+ return []
299
+
300
+ verb = first_match["verb"]
301
+ rest = first_match["rest"]
302
+ low = verb.lower()
303
+ irregular = {
304
+ "be": "being", "have": "having", "begin": "beginning",
305
+ "run": "running", "set": "setting", "get": "getting",
306
+ }
307
+
308
+ if low in irregular:
309
+ gerund = irregular[low]
310
+ elif low.endswith("ie"):
311
+ gerund = verb[:-2] + "ying"
312
+ elif low.endswith("e") and not low.endswith(("ee", "ye", "oe")):
313
+ gerund = verb[:-1] + "ing"
314
+ else:
315
+ gerund = verb + "ing"
316
+
317
+ candidate = K(
318
+ f"The {match['order']} {match['label']} involves {gerund}{rest}",
319
+ "Balanced",
320
+ "COPULAR_ACTION_FRAME",
321
+ "Converted a copular action frame into a grammatical event frame.",
322
+ .96,
323
+ )
324
+ return [] if realization_issues(candidate.text) else [candidate]
325
+
326
+
327
+ @rule(
328
+ "need_to_action",
329
+ {"Balanced", "Structural"},
330
+ {"NEED_TO_ACTION"},
331
+ .94,
332
+ )
333
+ def tr_need_to_reliable(analysis, profile, user_terms):
334
+ match = re.match(
335
+ r"(?is)^(?P<subject>.+?)\s+(?P<need>need|needs|needed)\s+to\s+"
336
+ r"(?P<action>.+?)[.]?$",
337
+ analysis.text,
338
+ )
339
+ if not match:
340
+ return []
341
+
342
+ modal = "had to" if match["need"].lower() == "needed" else "must"
343
+ candidate = K(
344
+ f"{match['subject'].strip()} {modal} {match['action'].strip()}",
345
+ "Balanced",
346
+ "NEED_TO_ACTION",
347
+ "Recast a need-to construction as a subject-preserving necessity frame.",
348
+ .94,
349
+ )
350
+ return [] if realization_issues(candidate.text) else [candidate]
351
+
352
+
353
+ @rule(
354
+ "modal_copular_enumeration",
355
+ {"Balanced", "Structural"},
356
+ {"MODAL_COPULAR_ENUMERATION", "COORDINATED_OBJECTS_OR_ENUMERATION", "ENUMERATION"},
357
+ .95,
358
+ )
359
+ def tr_modal_copular_enumeration_reliable(analysis, profile, user_terms):
360
+ match = re.match(
361
+ r"(?is)^(?P<subject>.+?)\s+(?P<modal>may|might|could|can)\s+be\s+in\s+"
362
+ r"(?P<items>.+?,\s*.+?\s*,?\s*(?:or|and)\s+.+?)[.]?$",
363
+ analysis.text,
364
+ )
365
+ if not match:
366
+ return []
367
+
368
+ candidate = K(
369
+ f"Possible sources of {lower_initial(match['subject'])} "
370
+ f"{match['modal'].lower()} include {match['items']}",
371
+ "Balanced",
372
+ "MODAL_COPULAR_ENUMERATION",
373
+ "Recast a modal copular list while preserving uncertainty and every member.",
374
+ .95,
375
+ )
376
+ return [] if realization_issues(candidate.text) else [candidate]
377
+
378
+
379
+ @rule(
380
+ "passive_using_nominal",
381
+ {"Balanced", "Structural"},
382
+ {"PASSIVE_USING"},
383
+ .96,
384
+ )
385
+ def tr_passive_using_reliable(analysis, profile, user_terms):
386
+ match = re.match(
387
+ r"(?is)^(?:(?P<marker>However|Therefore|Consequently|Thus|Moreover|"
388
+ r"Furthermore|Nevertheless|Accordingly),\s+)?"
389
+ r"(?P<subject>.+?)\s+"
390
+ r"(?:is|are|was|were|has been|have been|had been)\s+"
391
+ r"(?P<adverb>\w+ly\s+)?evaluated\s+using\s+"
392
+ r"(?P<object>.+?)[.]?$",
393
+ analysis.text,
394
+ )
395
+ if not match:
396
+ return []
397
+
398
+ marker = (match.group("marker") or "").strip()
399
+ adverb = (match.group("adverb") or "").strip()
400
+ prefix = f"{marker}, " if marker else ""
401
+ adverb_text = f"{adverb} " if adverb else ""
402
+
403
+ candidate = K(
404
+ f"{prefix}Evaluations of {lower_initial(match['subject'])} "
405
+ f"{adverb_text}use {match['object']}",
406
+ "Balanced",
407
+ "PASSIVE_USING",
408
+ "Recast a passive evaluation as an evaluation event without inventing an actor.",
409
+ .96,
410
+ )
411
+ return [] if realization_issues(candidate.text) else [candidate]
412
+
413
+
414
+ # =====================================================================
415
+ # 3. FIRST-STAGE FILTER — RERUN SAFE
416
+ # =====================================================================
417
+
418
+ if not hasattr(Generator, "_nll_reliability_base_lane"):
419
+ Generator._nll_reliability_base_lane = Generator.lane
420
+ else:
421
+ Generator.lane = Generator._nll_reliability_base_lane
422
+
423
+
424
+ def _lane_with_realization_filter(self, analysis, profile, user_terms, lane, trace):
425
+ candidates = Generator._nll_reliability_base_lane(
426
+ self, analysis, profile, user_terms, lane, trace
427
+ )
428
+ accepted = []
429
+
430
+ for candidate in candidates:
431
+ issues = realization_issues(candidate.text)
432
+ if issues:
433
+ trace.setdefault("realization_rejections", []).append({
434
+ "stage": "lane",
435
+ "rule": getattr(candidate, "family", ""),
436
+ "lane": getattr(candidate, "lane", lane),
437
+ "text": getattr(candidate, "text", ""),
438
+ "issues": issues,
439
+ })
440
+ continue
441
+ accepted.append(candidate)
442
+
443
+ return accepted
444
+
445
+
446
+ Generator.lane = _lane_with_realization_filter
447
+
448
+
449
+ # =====================================================================
450
+ # 4. FINAL/COMPOSED FILTER — RERUN SAFE
451
+ # =====================================================================
452
+
453
+ if not hasattr(Generator, "_nll_reliability_base_generate"):
454
+ Generator._nll_reliability_base_generate = Generator.generate
455
+ else:
456
+ Generator.generate = Generator._nll_reliability_base_generate
457
+
458
+
459
+ def _generate_with_final_realization_filter(
460
+ self, analysis, profile, user_terms, mode
461
+ ):
462
+ candidates, trace = Generator._nll_reliability_base_generate(
463
+ self, analysis, profile, user_terms, mode
464
+ )
465
+
466
+ accepted = []
467
+ seen = set()
468
+
469
+ for candidate in candidates:
470
+ text = getattr(candidate, "text", "")
471
+ issues = realization_issues(text)
472
+ if issues:
473
+ trace.setdefault("realization_rejections", []).append({
474
+ "stage": "final_pool",
475
+ "rule": getattr(candidate, "family", ""),
476
+ "lane": getattr(candidate, "lane", mode),
477
+ "text": text,
478
+ "issues": issues,
479
+ })
480
+ continue
481
+
482
+ key = surface_key(text)
483
+ if not key or key in seen:
484
+ continue
485
+ seen.add(key)
486
+ accepted.append(candidate)
487
+
488
+ trace["generated_before_final_filter"] = len(candidates)
489
+ trace["generated_after_final_filter"] = len(accepted)
490
+ trace["generated"] = len(accepted)
491
+ return accepted, trace
492
+
493
+
494
+ Generator.generate = _generate_with_final_realization_filter
495
+
496
+
497
+ # =====================================================================
498
+ # 5. SELF-TESTS
499
+ # =====================================================================
500
+
501
+ _BAD_REALIZATIONS = [
502
+ "Inspect how the handler works is the next step.",
503
+ "Distinguish the possibilities is necessary for we.",
504
+ "For they, the review is necessary.",
505
+ "The system is is evaluated.",
506
+ "Evaluations of The service use latency measurements.",
507
+ "We propose. We Adaptive Learning and Competency Evaluation Framework.",
508
+ "Protocol to small cybersecurity learners spanning classification, drafting, and planning.",
509
+ "The learners that spanning classification, drafting, and planning.",
510
+ "The learners that classification, drafting, and planning.",
511
+ ]
512
+
513
+ _GOOD_REALIZATIONS = [
514
+ "The next step involves inspecting how the handler works.",
515
+ "We must distinguish the possibilities using the existing source.",
516
+ "Possible sources of the problem could include lookup, initialization, or reporting logic.",
517
+ "Evaluations of the service use latency, throughput, and failure-rate measurements.",
518
+ "We propose the Adaptive Learning and Competency Evaluation Framework.",
519
+ "The experimental testbed applies this protocol to small cybersecurity learners that span classification, generative drafting, and structured diagram planning.",
520
+ "ALCEF-RSF is a domain-independent orchestration protocol consisting of native model contracts, protected examinations, and auditable promotion proposed by us.",
521
+ ]
522
+
523
+ for bad_text in _BAD_REALIZATIONS:
524
+ if not realization_issues(bad_text):
525
+ raise RuntimeError(
526
+ "Cell 5A v3.4 failed to reject malformed text: " + bad_text
527
+ )
528
+
529
+ for good_text in _GOOD_REALIZATIONS:
530
+ detected = realization_issues(good_text)
531
+ if detected:
532
+ raise RuntimeError(
533
+ "Cell 5A v3.4 incorrectly rejected valid text: "
534
+ + good_text + " -> " + str(detected)
535
+ )
536
+
537
+ _rule_names = [registered.name for registered in RULES]
538
+ _duplicates = sorted(
539
+ name for name, count in Counter(_rule_names).items() if count > 1
540
+ )
541
+ if _duplicates:
542
+ raise RuntimeError(
543
+ "Cell 5A v3.4 duplicate rule registration: " + ", ".join(_duplicates)
544
+ )
545
+ if len(RULES) != EXPECTED_5A_RULE_COUNT:
546
+ raise RuntimeError(
547
+ f"Cell 5A v3.4 expected {EXPECTED_5A_RULE_COUNT} rules but found {len(RULES)}."
548
+ )
549
+
550
+ if getattr(Generator.lane, "__name__", "") != "_lane_with_realization_filter":
551
+ raise RuntimeError("Cell 5A v3.4 lane wrapper was not installed.")
552
+ if getattr(Generator.generate, "__name__", "") != "_generate_with_final_realization_filter":
553
+ raise RuntimeError("Cell 5A v3.4 generate wrapper was not installed.")
554
+
555
+ print("=" * 78)
556
+ print("CELL 5A CANDIDATE RELIABILITY PATCH v3.4 LOADED")
557
+ print("=" * 78)
558
+ print("Aligned Cell 5 version: v4.1")
559
+ print("Finite declarative-clause checks: active")
560
+ print("Finite relative-clause checks: active")
561
+ print("Orphan participial-fragment checks: active")
562
+ print("Delimiter-balance checks: active")
563
+ print("Pronoun-case checks: active")
564
+ print("Internal capitalization checks: active")
565
+ print("First-stage filtering: active")
566
+ print("Final/composed filtering: active")
567
+ print("Rerun-safe Generator.lane wrapping: active")
568
+ print("Rerun-safe Generator.generate wrapping: active")
569
+ print("Malformed candidate self-tests: PASSED")
570
+ print("Valid candidate self-tests: PASSED")
571
+ print("Duplicate rule registrations: none")
572
+ print("Registered transformation rules:", len(RULES))
573
+ print("Expected rule count:", EXPECTED_5A_RULE_COUNT)
574
+ print("=" * 78)
runtime/cell6.py ADDED
@@ -0,0 +1,612 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #@title 6. UNIVERSAL ROUTING v5.1 — alignment for Cell 5 v4.1 / Cell 5A v3.4
2
+ # Complete replacement for Cell 6 v5.
3
+ #
4
+ # Goals:
5
+ # - rerun-safe overlay on the true pre-Cell-6 Analyzer;
6
+ # - citation-safe sentence splitting;
7
+ # - broad surface detection plus dependency-confirmed structure detection;
8
+ # - explicit primary-family metadata and secondary-structure metadata;
9
+ # - exact alignment with the 30-rule Cell 5 v4.1 registry;
10
+ # - no transformation generation and no semantic validation in this cell.
11
+
12
+ import re
13
+
14
+ EXPECTED_CELL6_RULE_COUNT = 30
15
+
16
+
17
+ # =====================================================================
18
+ # 0. PREREQUISITES
19
+ # =====================================================================
20
+
21
+ _REQUIRED_6 = [
22
+ "Analyzer", "RULES", "sent_split", "nlp",
23
+ ]
24
+ _missing_6 = [name for name in _REQUIRED_6 if name not in globals()]
25
+ if _missing_6:
26
+ raise RuntimeError(
27
+ "Cell 6 cannot run. Missing: " + ", ".join(_missing_6)
28
+ + ". Run Cells 1–5A first."
29
+ )
30
+
31
+ if len(RULES) != EXPECTED_CELL6_RULE_COUNT:
32
+ raise RuntimeError(
33
+ f"Cell 6 v5.1 expected {EXPECTED_CELL6_RULE_COUNT} Cell 5 v4.1 rules, "
34
+ f"but found {len(RULES)}."
35
+ )
36
+
37
+
38
+ # =====================================================================
39
+ # 1. RERUN-SAFE PRESERVATION OF TRUE PRE-CELL-6 FUNCTIONS
40
+ # =====================================================================
41
+
42
+ if "_NLL_CELL6_ORIGINAL_SENT_SPLIT" not in globals():
43
+ _NLL_CELL6_ORIGINAL_SENT_SPLIT = sent_split
44
+
45
+ if not hasattr(Analyzer, "_nll_cell6_original_analyze"):
46
+ Analyzer._nll_cell6_original_analyze = Analyzer.analyze
47
+
48
+ _BASE_ANALYZE = Analyzer._nll_cell6_original_analyze
49
+
50
+
51
+ # =====================================================================
52
+ # 2. CITATION/ABBREVIATION-SAFE SENTENCE SPLITTING
53
+ # =====================================================================
54
+
55
+ _CELL6_ABBREVIATIONS = {
56
+ r"\bet\s+al\.": "et al<NLL_DOT>",
57
+ r"\be\.g\.": "e<NLL_DOT>g<NLL_DOT>",
58
+ r"\bi\.e\.": "i<NLL_DOT>e<NLL_DOT>",
59
+ r"\bFig\.": "Fig<NLL_DOT>",
60
+ r"\bFigs\.": "Figs<NLL_DOT>",
61
+ r"\bEq\.": "Eq<NLL_DOT>",
62
+ r"\bEqs\.": "Eqs<NLL_DOT>",
63
+ r"\bDr\.": "Dr<NLL_DOT>",
64
+ r"\bProf\.": "Prof<NLL_DOT>",
65
+ r"\bMr\.": "Mr<NLL_DOT>",
66
+ r"\bMs\.": "Ms<NLL_DOT>",
67
+ r"\bMrs\.": "Mrs<NLL_DOT>",
68
+ r"\bNo\.": "No<NLL_DOT>",
69
+ }
70
+
71
+
72
+ def sent_split(text):
73
+ protected = str(text or "")
74
+
75
+ for pattern, replacement in _CELL6_ABBREVIATIONS.items():
76
+ protected = re.sub(pattern, replacement, protected, flags=re.I)
77
+
78
+ parts = _NLL_CELL6_ORIGINAL_SENT_SPLIT(protected)
79
+
80
+ return [
81
+ str(part).replace("<NLL_DOT>", ".").strip()
82
+ for part in parts
83
+ if str(part).strip()
84
+ ]
85
+
86
+
87
+ # =====================================================================
88
+ # 3. ROUTING HELPERS
89
+ # =====================================================================
90
+
91
+ _PRIMARY_FAMILIES_V51 = {
92
+ "COPULAR_ACTION_FRAME",
93
+ "NEED_TO_ACTION",
94
+ "FINITE_PURPOSE_INTENT",
95
+ "RATHER_THAN_GERUND",
96
+ "MODAL_COPULAR_ENUMERATION",
97
+ "USING_METHOD_RELATION",
98
+ "THROUGH_METHOD_RELATION",
99
+ "BY_GERUND_METHOD_RELATION",
100
+ "PURPOSE_INFINITIVE_FRONTING",
101
+ "NEGATED_PURPOSE_CONTRAST",
102
+ "EVIDENTIAL_COMPLEMENT",
103
+ "WHETHER_COMPLEMENT",
104
+ "GERUND_SUBJECT",
105
+ "WHEREBY_CLAUSE",
106
+ "WHILE_PARTICIPIAL",
107
+ "FRONTED_ADJUNCT",
108
+ "DISCOURSE_FRONTING",
109
+ "COORDINATED_PASSIVE_PARTICIPLES",
110
+ "PASSIVE_USING",
111
+ "PASSIVE",
112
+ "REDUCED_RELATIVE_OR_NP_MODIFIER",
113
+ "PARENTHETICAL_EMPHASIS",
114
+ "COORDINATED_PREDICATES",
115
+ "FINITE_COMPLEMENT_LIST",
116
+ "PROPOSAL_FRAME",
117
+ "APPOSITIVE_DEFINITION",
118
+ "PARTICIPIAL_POSTMODIFIER",
119
+ "FINITE_OBJECT_LIST_FRAME",
120
+ "CONTINGENCY_RELATION",
121
+ }
122
+
123
+ _SECONDARY_FAMILIES_V51 = {
124
+ "ENUMERATION",
125
+ "COORDINATED_OBJECTS_OR_ENUMERATION",
126
+ "LONG_MULTI_CLAUSE",
127
+ "NOMINALIZATION",
128
+ "CONTRAST",
129
+ "CONTENT_COMPLEMENT",
130
+ "PURPOSE_RESULT_CLAUSE",
131
+ "PURPOSE_INFINITIVE",
132
+ "MODAL_PASSIVE",
133
+ }
134
+
135
+ _PARTICIPIAL_LEMMAS = {"span", "cover", "include", "contain", "involve"}
136
+
137
+
138
+ def _v51_dependency_context(text):
139
+ try:
140
+ doc = nlp()(text)
141
+ if not len(doc):
142
+ return None, False
143
+ return doc, bool(doc.has_annotation("DEP"))
144
+ except Exception:
145
+ return None, False
146
+
147
+
148
+ def _v51_dependency_participial_postmodifier(doc):
149
+ if doc is None or not doc.has_annotation("DEP"):
150
+ return False
151
+
152
+ for token in doc:
153
+ if token.lemma_.lower() not in _PARTICIPIAL_LEMMAS:
154
+ continue
155
+ if token.tag_ != "VBG" and not token.text.lower().endswith("ing"):
156
+ continue
157
+
158
+ # Require an attested noun/proper-noun head. This is the same
159
+ # structural evidence Cell 5 v4.1 relies on for safe realization.
160
+ if token.head is token:
161
+ continue
162
+ if token.head.pos_ not in {"NOUN", "PROPN", "PRON"}:
163
+ continue
164
+
165
+ if token.dep_ in {"acl", "relcl", "amod", "advcl", "dep"}:
166
+ return True
167
+
168
+ return False
169
+
170
+
171
+ def _v51_dependency_coord_predicates(doc):
172
+ if doc is None or not doc.has_annotation("DEP"):
173
+ return False
174
+
175
+ roots = [token for token in doc if token.dep_ == "ROOT"]
176
+ if len(roots) != 1:
177
+ return False
178
+
179
+ root = roots[0]
180
+ if root.pos_ not in {"VERB", "AUX"}:
181
+ return False
182
+
183
+ subjects = [
184
+ child for child in root.children
185
+ if child.dep_ in {"nsubj", "nsubjpass", "csubj"}
186
+ ]
187
+ if len(subjects) != 1:
188
+ return False
189
+
190
+ conjuncts = [
191
+ token for token in root.conjuncts
192
+ if token.dep_ == "conj"
193
+ and token.pos_ in {"VERB", "AUX"}
194
+ and token.i > root.i
195
+ ]
196
+ if not conjuncts:
197
+ return False
198
+
199
+ return True
200
+
201
+
202
+ def _v51_dependency_noun_enumeration(doc):
203
+ if doc is None or not doc.has_annotation("DEP"):
204
+ return False
205
+
206
+ return any(
207
+ token.dep_ == "conj"
208
+ and token.head.pos_ in {"NOUN", "PROPN", "PRON", "ADJ"}
209
+ for token in doc
210
+ )
211
+
212
+
213
+ # =====================================================================
214
+ # 4. ANALYZER OVERLAY
215
+ # =====================================================================
216
+
217
+
218
+ def _analyze_v51(self, sentence, profile, user_terms):
219
+ analysis = _BASE_ANALYZE(self, sentence, profile, user_terms)
220
+
221
+ text = str(sentence or "").strip()
222
+ low = text.lower()
223
+ families = set(getattr(analysis, "families", []) or [])
224
+ notes = getattr(analysis, "notes", None)
225
+ if notes is None:
226
+ notes = []
227
+ analysis.notes = notes
228
+
229
+ comma_count = len(re.findall(r",", text))
230
+ semicolon_count = text.count(";")
231
+ has_coord_word = bool(re.search(r"\b(?:and|or)\b", low))
232
+ has_enumeration = comma_count >= 2 and has_coord_word
233
+
234
+ doc, dependency_available = _v51_dependency_context(text)
235
+
236
+ tests = {
237
+ "CONTRAST": bool(re.search(
238
+ r"\bnot\b.+?\bbut\b|\b(?:but|although|whereas|however)\b",
239
+ low,
240
+ re.S,
241
+ )),
242
+ "CONTENT_COMPLEMENT": bool(re.search(
243
+ r"\b(?:evidence|shows?|indicates?|suggests?|demonstrates?)\s+that\b",
244
+ low,
245
+ )),
246
+ "EVIDENTIAL_COMPLEMENT": bool(re.search(
247
+ r"\b(?:provide|provides|provided|offer|offers|offered)\s+"
248
+ r"(?:clear\s+|strong\s+|empirical\s+)?evidence\s+that\b",
249
+ low,
250
+ )),
251
+ "WHETHER_COMPLEMENT": bool(re.search(
252
+ r"\brequire(?:s|d)?\s+(?:an?\s+)?"
253
+ r"(?:consideration|assessment|evaluation|analysis)\s+of\s+whether\b",
254
+ low,
255
+ )),
256
+ "NEGATED_PURPOSE_CONTRAST": bool(re.search(
257
+ r"\b(?:purpose|aim|objective|goal)\b.*?\bis\s+not\s+to\b"
258
+ r".+?\bbut\s+to\b",
259
+ low,
260
+ re.S,
261
+ )),
262
+ "GERUND_SUBJECT": bool(re.match(
263
+ r"^(?:Applying|Using|Evaluating|Comparing|Assessing|Examining|"
264
+ r"Analyzing|Implementing|Testing|Measuring)\b",
265
+ text,
266
+ re.I,
267
+ )),
268
+ "WHEREBY_CLAUSE": bool(re.search(r"\bwhereby\b", low)),
269
+ "PURPOSE_RESULT_CLAUSE": bool(re.search(r"\bso\s+that\b", low)),
270
+ "WHILE_PARTICIPIAL": bool(re.search(r"\bwhile\s+\w+ing\b", low)),
271
+ "PARENTHETICAL_EMPHASIS": bool(re.search(
272
+ r",\s*by\s+(?:itself|themselves)\s*,|"
273
+ r"\b(?:by itself|by themselves|alone)\b",
274
+ low,
275
+ )),
276
+ "FRONTED_ADJUNCT": bool(re.match(
277
+ r"^(?:for|in|within|under|during|among|across|throughout)\b[^,]{1,160},",
278
+ low,
279
+ )),
280
+ "METHOD_RELATION": bool(re.search(
281
+ r"\bthrough\s+.+|\bby\s+\w+ing\b|\busing\s+.+",
282
+ low,
283
+ )),
284
+ "PURPOSE_INFINITIVE": bool(re.search(
285
+ r"\b(?:intended|designed|used|developed|implemented)\s+to\b",
286
+ low,
287
+ )),
288
+ "MODAL_PASSIVE": bool(re.search(
289
+ r"\b(?:can|could|may|might|must|should|would|will)\s+be\s+"
290
+ r"(?:\w+ly\s+)?\w+(?:ed|en)\b",
291
+ low,
292
+ )),
293
+ "PASSIVE_USING": bool(re.search(
294
+ r"\b(?:is|are|was|were|has been|have been|had been)\s+"
295
+ r"(?:\w+ly\s+)?evaluated\s+using\b",
296
+ low,
297
+ )),
298
+ "PASSIVE": bool(re.search(
299
+ r"\b(?:is|are|was|were|has|have|had)\s+(?:\w+ly\s+)?(?:been\s+)?"
300
+ r"(?:studied|examined|investigated|evaluated|reported|obtained|"
301
+ r"derived|collected|trained|modified|implemented|applied|tested|"
302
+ r"inspected|approved|reviewed|documented)\b",
303
+ low,
304
+ )),
305
+ "REDUCED_RELATIVE_OR_NP_MODIFIER": bool(re.search(
306
+ r"\b(?:results?|evidence|data|samples?|features?|models?|constraints?|"
307
+ r"methods?|files?|representations?)\s+(?:obtained|derived|collected|"
308
+ r"trained|reported|based|intended|designed|used|modified|implemented)\b",
309
+ low,
310
+ )),
311
+ "DISCOURSE_FRONTING": bool(re.match(
312
+ r"^(?:however|therefore|consequently|thus|moreover|furthermore|"
313
+ r"nevertheless|accordingly|additionally),",
314
+ low,
315
+ )),
316
+ "LONG_MULTI_CLAUSE": (
317
+ len(text.split()) >= 32
318
+ or semicolon_count >= 1
319
+ or comma_count >= 4
320
+ ),
321
+ "COPULAR_ACTION_FRAME": bool(re.match(
322
+ r"^the\s+(?:next|first|second|third|final|primary|main)\s+"
323
+ r"(?:step|task|action|objective|goal)\s+is\s+to\b",
324
+ low,
325
+ )),
326
+ "NEED_TO_ACTION": bool(re.search(r"\bneed(?:s)?\s+to\b", low)),
327
+ "MODAL_COPULAR_ENUMERATION": bool(re.search(
328
+ r"\b(?:may|might|could|can)\s+be\s+in\s+"
329
+ r".+?,\s*.+?\s*,?\s*(?:or|and)\s+.+",
330
+ low,
331
+ re.S,
332
+ )),
333
+ "USING_METHOD_RELATION": bool(re.search(r"\busing\s+.+", low)),
334
+ "THROUGH_METHOD_RELATION": bool(re.search(r"\bthrough\s+.+", low)),
335
+ "BY_GERUND_METHOD_RELATION": bool(re.search(r"\bby\s+\w+ing\b", low)),
336
+ "FINITE_PURPOSE_INTENT": bool(re.search(
337
+ r"\b(?:intend|intends|intended)\s+to\b",
338
+ low,
339
+ )),
340
+ "RATHER_THAN_GERUND": bool(re.search(r"\brather\s+than\s+\w+ing\b", low)),
341
+ "COORDINATED_PASSIVE_PARTICIPLES": bool(re.search(
342
+ r"\b(?:(?:have|has|had)\s+been|"
343
+ r"(?:can|could|may|might|must|should|would|will)\s+be)\s+"
344
+ r"(?:\w+ly\s+)?\w+(?:ed|en)\s*,\s*"
345
+ r"\w+(?:ed|en)\s*,\s*and\s*\w+(?:ed|en)(?:\s+by\b)?",
346
+ low,
347
+ )),
348
+ "PURPOSE_INFINITIVE_FRONTING": bool(re.match(
349
+ r"^to\s+[^,]{2,160},\s+",
350
+ low,
351
+ )),
352
+ "COORDINATED_OBJECTS_OR_ENUMERATION": has_enumeration,
353
+ "ENUMERATION": has_enumeration,
354
+
355
+ # Cell 5 v4.1 primary structures.
356
+ "FINITE_COMPLEMENT_LIST": bool(re.search(
357
+ r"\bwithout\s+.+?,\s*.+?(?:,\s*)?(?:and|or)\s+.+",
358
+ low,
359
+ re.S,
360
+ )),
361
+ "PROPOSAL_FRAME": bool(re.match(
362
+ r"^(?:we|i|the\s+(?:study|paper|article|report|team|authors?))\s+"
363
+ r"(?:propose|proposes|introduce|introduces|present|presents)\b",
364
+ low,
365
+ )),
366
+ "APPOSITIVE_DEFINITION": bool(re.search(
367
+ r"\([^)]{2,120}\)\s*,\s*(?:an?|the)\s+.+",
368
+ text,
369
+ re.S,
370
+ )),
371
+ "PARTICIPIAL_POSTMODIFIER": (
372
+ _v51_dependency_participial_postmodifier(doc)
373
+ or bool(re.search(
374
+ r"\b(?:to|for|with|among|across)\s+"
375
+ r"(?:[A-Za-z][\w-]*\s+){0,5}[A-Za-z][\w-]*\s+"
376
+ r"(?:spanning|covering|including|containing|involving)\s+"
377
+ r".+?,\s*.+?(?:,\s*)?(?:and|or)\s+.+",
378
+ text,
379
+ re.I | re.S,
380
+ ))
381
+ ),
382
+ "FINITE_OBJECT_LIST_FRAME": bool(re.match(
383
+ r"^(?:we|i|they|the\s+[A-Za-z][\w-]*(?:\s+[A-Za-z][\w-]*){0,4})\s+"
384
+ r"(?:pre-register|preregister|document|record|evaluate|assess|review|"
385
+ r"analyze|compare|test|measure|include|identify|specify|report)\s+"
386
+ r".+?,\s*.+?(?:,\s*)?(?:and|or)\s+.+",
387
+ text,
388
+ re.I | re.S,
389
+ )),
390
+ "CONTINGENCY_RELATION": bool(re.search(
391
+ r"\b(?:is|are|was|were)\s+contingent\s+on\b",
392
+ low,
393
+ )),
394
+ }
395
+
396
+ families.update(name for name, matched in tests.items() if matched)
397
+
398
+ # Dependency-confirmed augmentations.
399
+ try:
400
+ if dependency_available:
401
+ if _v51_dependency_coord_predicates(doc):
402
+ families.add("COORDINATED_PREDICATES")
403
+ if _v51_dependency_noun_enumeration(doc):
404
+ families.add("COORDINATED_OBJECTS_OR_ENUMERATION")
405
+ families.add("ENUMERATION")
406
+ if _v51_dependency_participial_postmodifier(doc):
407
+ families.add("PARTICIPIAL_POSTMODIFIER")
408
+ except Exception as exc:
409
+ notes.append(f"Dependency-family overlay failed: {exc}")
410
+
411
+ safe_families = families - {"PARSE_FAILURE"}
412
+ if "PARSE_FAILURE" in families and safe_families:
413
+ families.remove("PARSE_FAILURE")
414
+ notes.append(
415
+ "Dependency parsing was unavailable or incomplete, but "
416
+ "surface-safe grammar families were detected."
417
+ )
418
+
419
+ primary = sorted(family for family in families if family in _PRIMARY_FAMILIES_V51)
420
+ secondary = sorted(family for family in families if family in _SECONDARY_FAMILIES_V51)
421
+
422
+ analysis.families = sorted(families)
423
+ try:
424
+ analysis.metadata = dict(getattr(analysis, "metadata", {}) or {})
425
+ analysis.metadata.update({
426
+ "dependency_available": dependency_available,
427
+ "routing_overlay": "NLL_CELL_6_V5_1_CELL5_V4_1_RERUN_SAFE",
428
+ "primary_families": primary,
429
+ "secondary_structures": secondary,
430
+ "primary_family_count": len(primary),
431
+ "secondary_structure_count": len(secondary),
432
+ })
433
+ except Exception:
434
+ pass
435
+
436
+ return analysis
437
+
438
+
439
+ Analyzer.analyze = _analyze_v51
440
+
441
+
442
+ # =====================================================================
443
+ # 5. ROUTE COVERAGE
444
+ # =====================================================================
445
+
446
+ CRITICAL_FAMILIES = set(_PRIMARY_FAMILIES_V51) | {
447
+ "LONG_MULTI_CLAUSE",
448
+ "COORDINATED_OBJECTS_OR_ENUMERATION",
449
+ "ENUMERATION",
450
+ }
451
+
452
+ ROUTE_COVERAGE = {
453
+ family: sorted(
454
+ registered.name
455
+ for registered in RULES
456
+ if family in registered.families
457
+ )
458
+ for family in CRITICAL_FAMILIES
459
+ }
460
+
461
+ UNCOVERED = sorted(
462
+ family for family, routes in ROUTE_COVERAGE.items() if not routes
463
+ )
464
+
465
+
466
+ # =====================================================================
467
+ # 6. RERUN-SAFETY + ALIGNMENT CHECKS
468
+ # =====================================================================
469
+
470
+ if _BASE_ANALYZE is Analyzer.analyze:
471
+ raise RuntimeError(
472
+ "Cell 6 v5.1 rerun-safety failure: Analyzer overlay references itself."
473
+ )
474
+
475
+ if _NLL_CELL6_ORIGINAL_SENT_SPLIT is sent_split:
476
+ raise RuntimeError(
477
+ "Cell 6 v5.1 rerun-safety failure: sentence splitter references itself."
478
+ )
479
+
480
+ if len(RULES) != EXPECTED_CELL6_RULE_COUNT:
481
+ raise RuntimeError(
482
+ f"Cell 6 v5.1 expected {EXPECTED_CELL6_RULE_COUNT} rules but found {len(RULES)}."
483
+ )
484
+
485
+ _EXPECTED_V51_ROUTES = {
486
+ "FINITE_COMPLEMENT_LIST": ["without_complement_list"],
487
+ "PROPOSAL_FRAME": ["proposal_appositive_definition"],
488
+ "APPOSITIVE_DEFINITION": ["proposal_appositive_definition"],
489
+ "PARTICIPIAL_POSTMODIFIER": ["participial_postmodifier"],
490
+ "FINITE_OBJECT_LIST_FRAME": ["finite_object_list_frame"],
491
+ "CONTINGENCY_RELATION": ["contingency_relation"],
492
+ }
493
+
494
+ _route_errors = {
495
+ family: {"observed": ROUTE_COVERAGE.get(family, []), "expected": expected}
496
+ for family, expected in _EXPECTED_V51_ROUTES.items()
497
+ if ROUTE_COVERAGE.get(family, []) != expected
498
+ }
499
+ if _route_errors:
500
+ raise RuntimeError("Cell 6 v5.1 route-alignment failure: " + str(_route_errors))
501
+
502
+ if UNCOVERED:
503
+ raise RuntimeError(
504
+ "Cell 6 v5.1 uncovered critical families: " + ", ".join(UNCOVERED)
505
+ )
506
+
507
+
508
+ # =====================================================================
509
+ # 7. ROUTING SELF-TEST CORPUS
510
+ # =====================================================================
511
+
512
+ _ROUTING_TESTS = [
513
+ (
514
+ "FINITE_COMPLEMENT_LIST",
515
+ "Systems train separate models without explicit rules for assessment, correction, and graduation.",
516
+ ),
517
+ (
518
+ "PROPOSAL_FRAME",
519
+ "We propose Framework X (FX), a protocol consisting of contracts, examinations, and promotion.",
520
+ ),
521
+ (
522
+ "APPOSITIVE_DEFINITION",
523
+ "We propose Framework X (FX), a protocol consisting of contracts, examinations, and promotion.",
524
+ ),
525
+ (
526
+ "PARTICIPIAL_POSTMODIFIER",
527
+ "The testbed applies the protocol to learners spanning classification, drafting, and planning.",
528
+ ),
529
+ (
530
+ "FINITE_OBJECT_LIST_FRAME",
531
+ "We pre-register questions, data, baselines, ablations, and testing.",
532
+ ),
533
+ (
534
+ "CONTINGENCY_RELATION",
535
+ "Research claims about improvement, cost, and deployment are contingent on execution and analysis.",
536
+ ),
537
+ ]
538
+
539
+ # Use the current Analyzer overlay to verify detection. Any available profile
540
+ # is sufficient because these checks address syntax/routing, not discipline.
541
+ _test_profile = next(iter(PROFILES.values())) if "PROFILES" in globals() and PROFILES else None
542
+ _routing_failures = []
543
+
544
+ if _test_profile is not None:
545
+ _test_analyzer = Analyzer()
546
+ for expected_family, sentence in _ROUTING_TESTS:
547
+ try:
548
+ observed = set(
549
+ getattr(
550
+ _test_analyzer.analyze(sentence, _test_profile, set()),
551
+ "families",
552
+ [],
553
+ )
554
+ or []
555
+ )
556
+ if expected_family not in observed:
557
+ _routing_failures.append({
558
+ "family": expected_family,
559
+ "sentence": sentence,
560
+ "observed": sorted(observed),
561
+ })
562
+ except Exception as exc:
563
+ _routing_failures.append({
564
+ "family": expected_family,
565
+ "sentence": sentence,
566
+ "error": repr(exc),
567
+ })
568
+
569
+ if _routing_failures:
570
+ raise RuntimeError(
571
+ "Cell 6 v5.1 routing self-test failure: " + str(_routing_failures)
572
+ )
573
+
574
+
575
+ print("=" * 78)
576
+ print("CELL 6 UNIVERSAL ROUTING v5.1 CHECK")
577
+ print("=" * 78)
578
+ print("Aligned Cell 5 version: v4.1")
579
+ print("Aligned Cell 5A version: v3.4")
580
+ print("Transformation rules:", len(RULES))
581
+ print("Expected transformation rules:", EXPECTED_CELL6_RULE_COUNT)
582
+ print("Uncovered critical families:", UNCOVERED)
583
+ print("Analyzer rerun-safety check: PASSED")
584
+ print("Sentence-splitter rerun-safety check: PASSED")
585
+ print("Dependency-confirmed participial routing: active")
586
+ print("Dependency-confirmed coordinated predicates: active")
587
+ print("Primary/secondary family metadata: active")
588
+ print("Routing self-tests: PASSED")
589
+
590
+ for family in [
591
+ "FINITE_COMPLEMENT_LIST",
592
+ "PROPOSAL_FRAME",
593
+ "APPOSITIVE_DEFINITION",
594
+ "PARTICIPIAL_POSTMODIFIER",
595
+ "FINITE_OBJECT_LIST_FRAME",
596
+ "CONTINGENCY_RELATION",
597
+ "COPULAR_ACTION_FRAME",
598
+ "NEED_TO_ACTION",
599
+ "MODAL_COPULAR_ENUMERATION",
600
+ "USING_METHOD_RELATION",
601
+ "THROUGH_METHOD_RELATION",
602
+ "BY_GERUND_METHOD_RELATION",
603
+ "FINITE_PURPOSE_INTENT",
604
+ "RATHER_THAN_GERUND",
605
+ "COORDINATED_PASSIVE_PARTICIPLES",
606
+ "PURPOSE_INFINITIVE_FRONTING",
607
+ "COORDINATED_OBJECTS_OR_ENUMERATION",
608
+ "ENUMERATION",
609
+ ]:
610
+ print(f"{family}: {ROUTE_COVERAGE.get(family, [])}")
611
+
612
+ print("=" * 78)
runtime/cell7.py ADDED
@@ -0,0 +1,1213 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #@title 7. VALIDATION / RANKING / ENGINE v4.3 — confidence-gated Unicode enumeration judge
2
+ # Complete replacement for Cell 7.
3
+ # Compatible with Cell 5 v4.1, Cell 5A v3.4, Cell 6 v5.1.
4
+ # Accepts arbitrary Unicode text safely; linguistic routing quality still depends
5
+ # on the loaded spaCy model and available annotations.
6
+ # Run Cell 7A and Cell 7B again after this cell.
7
+
8
+ import json
9
+ import re
10
+ import time
11
+ import unicodedata
12
+ from collections import Counter
13
+ from dataclasses import asdict
14
+ from difflib import SequenceMatcher
15
+ from pathlib import Path
16
+
17
+ EXPECTED_CELL7_RULE_COUNT = 30
18
+
19
+ _REQUIRED_CELL7 = [
20
+ "STATE", "CFG", "RULES", "Analyzer", "Generator", "Scanner",
21
+ "Candidate", "SentenceResult", "DocumentResult", "PROFILES",
22
+ "protect", "optional", "nlp", "sub", "sent_split", "surface_key",
23
+ "realization_issues",
24
+ ]
25
+ _missing_cell7 = [name for name in _REQUIRED_CELL7 if name not in globals()]
26
+ if _missing_cell7:
27
+ raise RuntimeError(
28
+ "Cell 7 cannot run. Missing: " + ", ".join(_missing_cell7)
29
+ + ". Run Cells 1–6 v5.1 in order first."
30
+ )
31
+ if len(RULES) != EXPECTED_CELL7_RULE_COUNT:
32
+ raise RuntimeError(
33
+ f"Cell 7 expected {EXPECTED_CELL7_RULE_COUNT} rules but found {len(RULES)}."
34
+ )
35
+
36
+ # Backward compatibility for older tests/helpers.
37
+ _surface_key = surface_key
38
+
39
+ NEG = {"not", "no", "never", "without", "neither", "nor"}
40
+ HEDGE = {
41
+ "may", "might", "could", "can", "possibly", "potentially",
42
+ "suggest", "suggests", "indicate", "indicates", "likely",
43
+ "approximately", "about", "must", "should", "would", "will",
44
+ }
45
+
46
+
47
+ def _ensure_state():
48
+ if not isinstance(STATE, dict):
49
+ raise RuntimeError("STATE must be a dictionary.")
50
+ STATE.setdefault("warnings", [])
51
+ STATE.setdefault("start", None)
52
+ return STATE
53
+
54
+
55
+ def sr(a, b):
56
+ return SequenceMatcher(
57
+ None,
58
+ re.sub(r"\s+", " ", str(a).lower()).strip(),
59
+ re.sub(r"\s+", " ", str(b).lower()).strip(),
60
+ ).ratio()
61
+
62
+
63
+ def token_change(a, b):
64
+ A = re.findall(r"[A-Za-z0-9]+(?:[-'][A-Za-z0-9]+)*", str(a).lower())
65
+ B = re.findall(r"[A-Za-z0-9]+(?:[-'][A-Za-z0-9]+)*", str(b).lower())
66
+ return 1 - SequenceMatcher(None, A, B).ratio()
67
+
68
+
69
+ def nli_pair(a, b):
70
+ _ensure_state()
71
+ bundle = optional("nli")
72
+ if not bundle:
73
+ return None
74
+ try:
75
+ import torch
76
+ tok, model = bundle
77
+ x = tok(a, b, return_tensors="pt", truncation=True, max_length=512)
78
+ dev = next(model.parameters()).device
79
+ x = {k: v.to(dev) for k, v in x.items()}
80
+ with torch.no_grad():
81
+ prob = torch.softmax(model(**x).logits[0], -1).cpu().numpy()
82
+ labels = {
83
+ str(model.config.id2label[i]).lower(): float(prob[i])
84
+ for i in range(len(prob))
85
+ }
86
+ return {
87
+ "entailment": max([v for k, v in labels.items() if "entail" in k] or [0]),
88
+ "contradiction": max([v for k, v in labels.items() if "contrad" in k] or [0]),
89
+ }
90
+ except Exception as exc:
91
+ STATE["warnings"].append(f"NLI scoring failed: {exc}")
92
+ return None
93
+
94
+
95
+ def stsim(a, b):
96
+ _ensure_state()
97
+ model = optional("sts")
98
+ if not model:
99
+ return None
100
+ try:
101
+ from sentence_transformers.util import cos_sim
102
+ emb = model.encode([a, b], convert_to_tensor=True, normalize_embeddings=True)
103
+ return float(cos_sim(emb[0], emb[1]).item())
104
+ except Exception as exc:
105
+ STATE["warnings"].append(f"STS scoring failed: {exc}")
106
+ return None
107
+
108
+
109
+ # =====================================================================
110
+ # RELATION / INVENTORY HELPERS
111
+ # =====================================================================
112
+
113
+ def _safe_doc(text):
114
+ try:
115
+ return nlp()(str(text or ""))
116
+ except Exception:
117
+ return None
118
+
119
+
120
+ def _neg_inventory(text, doc=None):
121
+ """Compare polarity markers by lexical class, not dependency owner."""
122
+ lo = str(text or "").lower()
123
+ return Counter(
124
+ token for token in ["not", "no", "never", "without", "neither", "nor"]
125
+ if re.search(r"(?<!\w)" + re.escape(token) + r"(?!\w)", lo)
126
+ )
127
+
128
+
129
+ def _neg_scope_debug(doc):
130
+ if doc is None or not getattr(doc, "has_annotation", lambda *_: False)("DEP"):
131
+ return []
132
+ return sorted(
133
+ (x.lower_, x.head.lemma_.lower())
134
+ for x in doc
135
+ if x.dep_ == "neg" or x.lower_ in {"never", "without", "neither", "nor"}
136
+ )
137
+
138
+
139
+ def _modals(doc, text=""):
140
+ if doc is not None:
141
+ try:
142
+ return Counter(
143
+ x.lemma_.lower()
144
+ for x in doc
145
+ if x.lemma_.lower() in HEDGE or x.tag_ == "MD"
146
+ )
147
+ except Exception:
148
+ pass
149
+ return Counter(re.findall(
150
+ r"\b(?:may|might|can|could|must|should|would|will|possibly|potentially|likely)\b",
151
+ str(text or "").lower(),
152
+ ))
153
+
154
+
155
+ def _purpose_contrast_preserved(original, candidate):
156
+ match = re.search(
157
+ r"(?is)\bnot\s+to\s+(?P<x>.+?)\s+but\s+to\s+(?P<y>.+?)[.]?$",
158
+ original,
159
+ )
160
+ if not match:
161
+ return False
162
+ x = surface_key(match["x"])
163
+ y = surface_key(match["y"])
164
+ z = surface_key(candidate)
165
+ return (
166
+ x in z and y in z
167
+ and (
168
+ "rather than" in candidate.lower()
169
+ or ("does not" in candidate.lower() and "instead" in candidate.lower())
170
+ )
171
+ )
172
+
173
+
174
+ def _unicode_normalize(text):
175
+ """Canonical Unicode normalization for comparison, never for output."""
176
+ value = unicodedata.normalize("NFKC", str(text or ""))
177
+ translations = str.maketrans({
178
+ "–": "-", "—": "-", "−": "-", "‑": "-",
179
+ "’": "'", "‘": "'", "“": '"', "”": '"',
180
+ "\u00a0": " ",
181
+ })
182
+ return re.sub(r"\s+", " ", value.translate(translations)).strip().casefold()
183
+
184
+
185
+ def _unicode_tokens(text):
186
+ """
187
+ Tokenize letters/numbers across Unicode scripts without ASCII loss.
188
+
189
+ Punctuation, including every dash/apostrophe form, is treated as a
190
+ comparison boundary. This makes these equivalent for inventory checks:
191
+ re-examination == re examination == re–examination
192
+ matched-data == matched data
193
+ The original candidate text is never modified.
194
+ """
195
+ value = _unicode_normalize(text)
196
+ output = []
197
+ current = []
198
+
199
+ for char in value:
200
+ category = unicodedata.category(char)
201
+ if category[0] in {"L", "N", "M"}:
202
+ current.append(char)
203
+ elif current:
204
+ token = "".join(current)
205
+ if token:
206
+ output.append(token)
207
+ current = []
208
+
209
+ if current:
210
+ token = "".join(current)
211
+ if token:
212
+ output.append(token)
213
+
214
+ return output
215
+
216
+
217
+ def _noun_chunk_for_token(doc, token):
218
+ try:
219
+ chunks = [chunk for chunk in doc.noun_chunks if chunk.start <= token.i < chunk.end]
220
+ except Exception:
221
+ chunks = []
222
+ return min(chunks, key=lambda chunk: chunk.end - chunk.start) if chunks else None
223
+
224
+
225
+ def _clean_member_text(text):
226
+ value = re.sub(r"\s+", " ", str(text or "")).strip(" \t\r\n,;:.()[]{}")
227
+ value = re.sub(r"^(?:and|or|nor)\s+", "", value, flags=re.I)
228
+ return value.strip()
229
+
230
+
231
+ def _dependency_enumeration_groups(doc):
232
+ """Recover coordinated members from dependency heads/noun chunks."""
233
+ if doc is None or not doc.has_annotation("DEP"):
234
+ return []
235
+
236
+ groups = []
237
+ seen = set()
238
+ for token in doc:
239
+ conjuncts = [child for child in token.children if child.dep_ == "conj"]
240
+ if not conjuncts:
241
+ continue
242
+
243
+ members = [token] + sorted(conjuncts, key=lambda item: item.i)
244
+ # Include chained conjuncts.
245
+ cursor = 0
246
+ while cursor < len(members):
247
+ for child in members[cursor].children:
248
+ if child.dep_ == "conj" and child not in members:
249
+ members.append(child)
250
+ cursor += 1
251
+ members = sorted(set(members), key=lambda item: item.i)
252
+
253
+ texts = []
254
+ for member in members:
255
+ chunk = _noun_chunk_for_token(doc, member)
256
+ if chunk is not None:
257
+ text = chunk.text
258
+ else:
259
+ # Conservative local subtree: exclude other conjunct branches.
260
+ blocked = {item.i for other in members if other is not member for item in other.subtree}
261
+ indices = [
262
+ item.i for item in member.subtree
263
+ if item.i not in blocked and item.dep_ not in {"cc", "punct"}
264
+ ]
265
+ if indices:
266
+ text = doc[min(indices):max(indices) + 1].text
267
+ else:
268
+ text = member.text
269
+ text = _clean_member_text(text)
270
+ if text:
271
+ texts.append(text)
272
+
273
+ texts = list(dict.fromkeys(texts))
274
+ key = tuple(_unicode_normalize(item) for item in texts)
275
+ if len(texts) >= 3 and key not in seen:
276
+ seen.add(key)
277
+ groups.append(texts)
278
+
279
+ return groups
280
+
281
+
282
+ def _analysis_enumeration_groups(analysis):
283
+ """Accept analyzer groups only when each item is a plausible member."""
284
+ groups = []
285
+ for group in list(getattr(analysis, "enumerations", []) or []):
286
+ cleaned = [_clean_member_text(item) for item in group]
287
+ cleaned = [item for item in cleaned if item]
288
+ # Reject boundary-corrupted pseudo-members containing multiple commas.
289
+ if len(cleaned) >= 3 and all(item.count(",") <= 1 for item in cleaned):
290
+ groups.append(cleaned)
291
+ return groups
292
+
293
+
294
+ def _top_level_segments(text):
295
+ """Split on top-level commas while respecting brackets and Unicode text."""
296
+ segments = []
297
+ current = []
298
+ stack = []
299
+ pairs = {"(": ")", "[": "]", "{": "}"}
300
+ closers = set(pairs.values())
301
+ for char in str(text or ""):
302
+ if char in pairs:
303
+ stack.append(pairs[char])
304
+ elif char in closers and stack and stack[-1] == char:
305
+ stack.pop()
306
+ if char == "," and not stack:
307
+ segments.append("".join(current).strip())
308
+ current = []
309
+ else:
310
+ current.append(char)
311
+ if current:
312
+ segments.append("".join(current).strip())
313
+ return segments
314
+
315
+
316
+ def _surface_list_groups(text):
317
+ """Conservative fallback; never cuts through hyphenated compounds."""
318
+ value = str(text or "").strip().rstrip(".?!")
319
+ segments = _top_level_segments(value)
320
+ if len(segments) < 3:
321
+ return []
322
+
323
+ # Locate the final coordinator in the final segment.
324
+ final = segments[-1]
325
+ match = re.search(r"\s+(?:and|or|nor)\s+(.+)$", final, re.I | re.S)
326
+ if not match:
327
+ return []
328
+ last_member = _clean_member_text(match.group(1))
329
+ pre_final = _clean_member_text(final[:match.start()])
330
+
331
+ # Find the most plausible list start. Prefer explicit introducers.
332
+ introducer = re.compile(
333
+ r"\b(?:using|including|involving|such as|comprising|consisting of|covers?|"
334
+ r"evaluated|assessed|measured|reviewed|identified|pre-register(?:ed)?|preregister(?:ed)?)\s+",
335
+ re.I,
336
+ )
337
+ first_index = 0
338
+ first_text = segments[0]
339
+ matches = list(introducer.finditer(first_text))
340
+ if matches:
341
+ first_member = _clean_member_text(first_text[matches[-1].end():])
342
+ else:
343
+ # Without an introducer, avoid guessing a list start from the full clause.
344
+ return []
345
+
346
+ middle = [_clean_member_text(item) for item in segments[1:-1]]
347
+ members = [first_member] + middle
348
+ if pre_final:
349
+ members.append(pre_final)
350
+ if last_member:
351
+ members.append(last_member)
352
+ members = [item for item in members if item]
353
+ return [members] if len(members) >= 3 else []
354
+
355
+
356
+ def _plausible_enumeration_member(text):
357
+ """
358
+ Reject boundary-corrupted pseudo-members without weakening true omission checks.
359
+
360
+ A valid member may contain Unicode letters, numbers, spaces, apostrophes,
361
+ and dash variants, but should not contain clause punctuation, a full
362
+ sentence lead-in, or an excessive token span.
363
+ """
364
+ value = _clean_member_text(text)
365
+ tokens = _unicode_tokens(value)
366
+
367
+ if not tokens or len(tokens) > 12:
368
+ return False
369
+ if any(mark in value for mark in [",", ";", ":", "."]):
370
+ return False
371
+ if re.match(
372
+ r"(?is)^(?:we|i|they|he|she|it|the\s+(?:study|paper|article|report|"
373
+ r"system|framework|team|model))\s+"
374
+ r"(?:propose|proposes|introduce|introduces|present|presents|"
375
+ r"evaluate|evaluates|assess|assesses|pre-register|preregister)\b",
376
+ value,
377
+ ):
378
+ return False
379
+
380
+ return True
381
+
382
+
383
+ def _enumeration_groups(analysis, source_text, source_doc):
384
+ """
385
+ Build only high-confidence list groups.
386
+
387
+ Sources are tried independently instead of accepting the first nonempty
388
+ extractor. Candidate groups are filtered for plausible member boundaries,
389
+ verified against the source, de-duplicated, and ranked by compactness.
390
+ """
391
+ candidate_groups = []
392
+ candidate_groups.extend(_dependency_enumeration_groups(source_doc))
393
+ candidate_groups.extend(_analysis_enumeration_groups(analysis))
394
+ candidate_groups.extend(_surface_list_groups(source_text))
395
+
396
+ source_tokens = _unicode_tokens(source_text)
397
+ accepted = []
398
+ seen = set()
399
+
400
+ for group in candidate_groups:
401
+ stable_group = []
402
+ for raw_item in group:
403
+ item = _clean_member_text(raw_item)
404
+ if not _plausible_enumeration_member(item):
405
+ continue
406
+
407
+ item_tokens = _unicode_tokens(item)
408
+ width = len(item_tokens)
409
+ appears_in_source = any(
410
+ source_tokens[index:index + width] == item_tokens
411
+ for index in range(len(source_tokens) - width + 1)
412
+ )
413
+ if appears_in_source:
414
+ stable_group.append(item)
415
+
416
+ # Preserve order and remove duplicate members.
417
+ unique_group = []
418
+ member_seen = set()
419
+ for item in stable_group:
420
+ key = tuple(_unicode_tokens(item))
421
+ if key and key not in member_seen:
422
+ member_seen.add(key)
423
+ unique_group.append(item)
424
+
425
+ normalized = tuple(tuple(_unicode_tokens(item)) for item in unique_group)
426
+ if len(normalized) >= 3 and normalized not in seen:
427
+ seen.add(normalized)
428
+ accepted.append(unique_group)
429
+
430
+ # Remove groups that are strict noisy supersets of a cleaner group.
431
+ compact = []
432
+ token_sets = [
433
+ {tuple(_unicode_tokens(item)) for item in group}
434
+ for group in accepted
435
+ ]
436
+ for index, group in enumerate(accepted):
437
+ current = token_sets[index]
438
+ noisier = any(
439
+ other < current
440
+ for other_index, other in enumerate(token_sets)
441
+ if other_index != index and len(other) >= 3
442
+ )
443
+ if not noisier:
444
+ compact.append(group)
445
+
446
+ return compact
447
+
448
+
449
+ def _member_present(member, candidate):
450
+ """Require normalized member tokens in order, independent of punctuation."""
451
+ member_tokens = _unicode_tokens(member)
452
+ candidate_tokens = _unicode_tokens(candidate)
453
+ if not member_tokens:
454
+ return True
455
+ width = len(member_tokens)
456
+ return any(
457
+ candidate_tokens[index:index + width] == member_tokens
458
+ for index in range(len(candidate_tokens) - width + 1)
459
+ )
460
+
461
+
462
+ def _relation_signature(text):
463
+ lo = str(text or "").lower()
464
+ classes = {
465
+ "cause": {"because", "therefore", "thus", "consequently", "hence", "as a result", "for this reason", "accordingly"},
466
+ "concession": {"although", "though", "despite", "nevertheless", "however", "even so"},
467
+ "condition": {"if", "unless", "whether"},
468
+ "simultaneous": {"while", "at the same time"},
469
+ "purpose": {"so that", "intended to", "designed to"},
470
+ "absence": {"without"},
471
+ "dependency": {"contingent on", "depend on", "depends on", "depended on"},
472
+ }
473
+ return {
474
+ key
475
+ for key, words in classes.items()
476
+ if any(re.search(r"(?<!\w)" + re.escape(word) + r"(?!\w)", lo) for word in words)
477
+ }
478
+
479
+
480
+ def _triples(doc):
481
+ triples = []
482
+ if doc is None or not doc.has_annotation("DEP"):
483
+ return triples
484
+ for verb in doc:
485
+ subjects = [x for x in verb.children if x.dep_ in {"nsubj", "nsubjpass"}]
486
+ objects = [x for x in verb.children if x.dep_ in {"obj", "dobj", "attr"}]
487
+ triples += [
488
+ (sub(subject).lower(), verb.lemma_.lower(), sub(obj).lower())
489
+ for subject in subjects
490
+ for obj in objects
491
+ ]
492
+ return triples
493
+
494
+
495
+ # =====================================================================
496
+ # VALIDATOR
497
+ # =====================================================================
498
+
499
+ class Validator:
500
+ def validate(self, original, candidate, analysis, profile, user_terms, mode):
501
+ _ensure_state()
502
+ revised = candidate.text
503
+ failures = []
504
+ reasons = []
505
+ checks = {}
506
+
507
+ def reject(code, why):
508
+ if code not in failures:
509
+ failures.append(code)
510
+ reasons.append({"code": code, "reason": why})
511
+
512
+ # -------------------------------------------------------------
513
+ # Exact protected inventories
514
+ # -------------------------------------------------------------
515
+ hard_terms = set(getattr(profile, "immutable", set()) or set()) | set(user_terms or set())
516
+ for kind, code in [
517
+ ("citation", "TERMINOLOGY_REJECTION"),
518
+ ("quantity", "QUANTITY_REJECTION"),
519
+ ("url", "TERMINOLOGY_REJECTION"),
520
+ ("equation", "TERMINOLOGY_REJECTION"),
521
+ ("code", "TERMINOLOGY_REJECTION"),
522
+ ]:
523
+ source_inventory = Counter(
524
+ item[1].lower()
525
+ for item in protect(original, hard_terms)
526
+ if item[0] == kind
527
+ )
528
+ candidate_inventory = Counter(
529
+ item[1].lower()
530
+ for item in protect(revised, hard_terms)
531
+ if item[0] == kind
532
+ )
533
+ checks[kind] = source_inventory == candidate_inventory
534
+ if source_inventory != candidate_inventory:
535
+ reject(
536
+ code,
537
+ f"{kind} inventory changed: {dict(source_inventory)} -> {dict(candidate_inventory)}",
538
+ )
539
+
540
+ missing_terms = [
541
+ term
542
+ for term in hard_terms
543
+ if re.search(r"(?<!\w)" + re.escape(term) + r"(?!\w)", original, re.I)
544
+ and not re.search(r"(?<!\w)" + re.escape(term) + r"(?!\w)", revised, re.I)
545
+ ]
546
+ if missing_terms:
547
+ reject(
548
+ "TERMINOLOGY_REJECTION",
549
+ "Missing immutable/user terms: " + ", ".join(sorted(missing_terms)),
550
+ )
551
+
552
+ # -------------------------------------------------------------
553
+ # Realization gate
554
+ # -------------------------------------------------------------
555
+ realization = list(realization_issues(revised) or [])
556
+ checks["realization_issues"] = realization
557
+ if realization:
558
+ reject(
559
+ "GRAMMAR_REJECTION",
560
+ "Candidate failed realization checks: " + " | ".join(realization),
561
+ )
562
+
563
+ source_doc = _safe_doc(original)
564
+ candidate_doc = _safe_doc(revised)
565
+
566
+ # -------------------------------------------------------------
567
+ # Polarity: compare marker inventory; retain dependency scope only
568
+ # as diagnostics. Reordering 'without' is therefore allowed.
569
+ # -------------------------------------------------------------
570
+ source_neg = _neg_inventory(original, source_doc)
571
+ candidate_neg = _neg_inventory(revised, candidate_doc)
572
+ checks["negation_inventory"] = (dict(source_neg), dict(candidate_neg))
573
+ checks["negation_scope_debug"] = (
574
+ _neg_scope_debug(source_doc),
575
+ _neg_scope_debug(candidate_doc),
576
+ )
577
+
578
+ polarity_preserved = source_neg == candidate_neg
579
+ if (
580
+ not polarity_preserved
581
+ and candidate.family.startswith("NEGATED_PURPOSE_CONTRAST")
582
+ and _purpose_contrast_preserved(original, revised)
583
+ ):
584
+ polarity_preserved = True
585
+
586
+ if not polarity_preserved:
587
+ reject(
588
+ "POLARITY_REJECTION",
589
+ f"Polarity-marker inventory changed: {dict(source_neg)} -> {dict(candidate_neg)}",
590
+ )
591
+
592
+ # -------------------------------------------------------------
593
+ # Modality / hedging
594
+ # -------------------------------------------------------------
595
+ source_modals = _modals(source_doc, original)
596
+ candidate_modals = _modals(candidate_doc, revised)
597
+ checks["modality"] = (dict(source_modals), dict(candidate_modals))
598
+ if source_modals != candidate_modals:
599
+ reject(
600
+ "MODALITY_REJECTION",
601
+ f"Modality/hedging changed: {dict(source_modals)} -> {dict(candidate_modals)}",
602
+ )
603
+
604
+ # -------------------------------------------------------------
605
+ # Enumeration preservation across the WHOLE candidate text.
606
+ # A split/reordered candidate is allowed if every source list member
607
+ # survives in token order somewhere in the complete candidate.
608
+ # -------------------------------------------------------------
609
+ enumeration_groups = _enumeration_groups(analysis, original, source_doc)
610
+ missing_by_group = []
611
+ for group_index, group in enumerate(enumeration_groups, start=1):
612
+ missing = [member for member in group if not _member_present(member, revised)]
613
+ if missing:
614
+ missing_by_group.append({"group": group_index, "missing": missing})
615
+
616
+ checks["enumeration_groups"] = enumeration_groups
617
+ checks["enumeration_missing"] = missing_by_group
618
+ if missing_by_group:
619
+ detail = " | ".join(
620
+ f"group {entry['group']}: " + ", ".join(entry["missing"])
621
+ for entry in missing_by_group
622
+ )
623
+ reject(
624
+ "ENUMERATION_REJECTION",
625
+ "Enumeration members missing: " + detail,
626
+ )
627
+
628
+ # -------------------------------------------------------------
629
+ # Subject/object orientation
630
+ # -------------------------------------------------------------
631
+ source_triples = _triples(source_doc)
632
+ candidate_triples = _triples(candidate_doc)
633
+ checks["source_triples"] = source_triples
634
+ checks["candidate_triples"] = candidate_triples
635
+ for subject, verb, obj in source_triples:
636
+ if subject != obj and any(
637
+ cs == obj and cq == subject and cv == verb
638
+ for cs, cv, cq in candidate_triples
639
+ ):
640
+ reject(
641
+ "SEMANTIC_REJECTION",
642
+ f"Subject/object orientation reversed for '{verb}'.",
643
+ )
644
+
645
+ if not source_triples and not candidate_triples:
646
+ simple = (
647
+ r"(?is)^\s*(?P<s>[A-Z][A-Za-z0-9_-]*)\s+"
648
+ r"(?P<v>\w+(?:ed|s))\s+(?P<o>[A-Z][A-Za-z0-9_-]*)"
649
+ )
650
+ source_match = re.match(simple, original)
651
+ candidate_match = re.match(simple, revised)
652
+ if (
653
+ source_match and candidate_match
654
+ and source_match["s"].lower() == candidate_match["o"].lower()
655
+ and source_match["o"].lower() == candidate_match["s"].lower()
656
+ and source_match["v"].lower() == candidate_match["v"].lower()
657
+ ):
658
+ reject(
659
+ "SEMANTIC_REJECTION",
660
+ f"Subject/object orientation reversed for '{source_match['v']}'.",
661
+ )
662
+
663
+ # -------------------------------------------------------------
664
+ # Discourse relations
665
+ # -------------------------------------------------------------
666
+ source_relations = _relation_signature(original)
667
+ candidate_relations = _relation_signature(revised)
668
+ checks["relations"] = (sorted(source_relations), sorted(candidate_relations))
669
+ if "concession" in candidate_relations - source_relations:
670
+ reject(
671
+ "SEMANTIC_REJECTION",
672
+ f"Candidate introduced concession: {sorted(source_relations)} -> {sorted(candidate_relations)}",
673
+ )
674
+ if "cause" in source_relations and not ({"cause", "purpose"} & candidate_relations):
675
+ reject(
676
+ "SEMANTIC_REJECTION",
677
+ f"Causal/result relation disappeared: {sorted(source_relations)} -> {sorted(candidate_relations)}",
678
+ )
679
+ if "absence" in source_relations and "absence" not in candidate_relations:
680
+ reject(
681
+ "POLARITY_REJECTION",
682
+ "The source's 'without' relation disappeared from the candidate.",
683
+ )
684
+ if "dependency" in source_relations and "dependency" not in candidate_relations:
685
+ reject(
686
+ "SEMANTIC_REJECTION",
687
+ "The source dependency/contingency relation disappeared.",
688
+ )
689
+
690
+ # -------------------------------------------------------------
691
+ # Optional soft semantic signals
692
+ # -------------------------------------------------------------
693
+ sts = stsim(original, revised)
694
+ forward = nli_pair(original, revised)
695
+ reverse = nli_pair(revised, original)
696
+ checks.update(sts=sts, nli_forward=forward, nli_reverse=reverse)
697
+
698
+ if sts is not None and sts < CFG.min_sts:
699
+ checks["sts_warning"] = f"Low soft STS signal: {sts:.3f}"
700
+
701
+ if forward and reverse:
702
+ if max(forward["contradiction"], reverse["contradiction"]) > .55:
703
+ reject("SEMANTIC_REJECTION", "Bidirectional NLI found contradiction.")
704
+ elif min(forward["entailment"], reverse["entailment"]) < CFG.min_nli:
705
+ checks["nli_warning"] = (
706
+ f"Weak entailment {forward['entailment']:.3f}/{reverse['entailment']:.3f}; "
707
+ "manual review advised."
708
+ )
709
+
710
+ # -------------------------------------------------------------
711
+ # Rewrite magnitude
712
+ # -------------------------------------------------------------
713
+ surface = sr(original, revised)
714
+ change = token_change(original, revised)
715
+ checks.update(surface=surface, change=change, missing_terms=missing_terms)
716
+ minimum_change = {"Minimal": .035, "Balanced": .065, "Structural": .09}[mode]
717
+
718
+ if surface_key(original) == surface_key(revised):
719
+ reject("INSUFFICIENT_REWRITE", "Candidate is identical to the source.")
720
+ elif change < minimum_change:
721
+ reject(
722
+ "INSUFFICIENT_REWRITE",
723
+ f"Token change {change:.3f} is below {mode} threshold {minimum_change:.3f}.",
724
+ )
725
+
726
+ # -------------------------------------------------------------
727
+ # Optional local LanguageTool comparison
728
+ # -------------------------------------------------------------
729
+ grammar_tool = optional("lt") if getattr(CFG, "lt", False) else None
730
+ if grammar_tool:
731
+ try:
732
+ delta = len(grammar_tool.check(revised)) - len(grammar_tool.check(original))
733
+ checks["grammar_delta"] = delta
734
+ if delta > 2:
735
+ reject(
736
+ "GRAMMAR_REJECTION",
737
+ f"Candidate introduced {delta} additional LanguageTool findings.",
738
+ )
739
+ except Exception as exc:
740
+ checks["grammar_warning"] = str(exc)
741
+
742
+ checks.update(
743
+ failures=failures,
744
+ rejection_reasons=reasons,
745
+ accepted=not failures,
746
+ )
747
+ return not failures, failures, checks
748
+
749
+
750
+ # =====================================================================
751
+ # RANKER
752
+ # =====================================================================
753
+
754
+ class Ranker:
755
+ def rank(self, original, candidates, analysis, profile, user_terms, mode):
756
+ good = []
757
+ bad = []
758
+ target = {"Minimal": .12, "Balanced": .24, "Structural": .38}[mode]
759
+
760
+ for candidate in candidates:
761
+ ok, failures, checks = Validator().validate(
762
+ original, candidate, analysis, profile, user_terms, mode
763
+ )
764
+ change = checks.get("change", token_change(original, candidate.text))
765
+ fit = max(0, 1 - abs(change - target) / max(target, .10))
766
+ sts = checks.get("sts")
767
+ semantic = .78 if sts is None else max(0, min(1, sts))
768
+ nli_scores = [
769
+ item["entailment"]
770
+ for item in [checks.get("nli_forward"), checks.get("nli_reverse")]
771
+ if item
772
+ ]
773
+ if nli_scores:
774
+ semantic = .55 * semantic + .45 * min(nli_scores)
775
+
776
+ lane_fit = (
777
+ 1.0 if candidate.lane == mode
778
+ else .82 if (mode, candidate.lane) in {
779
+ ("Balanced", "Minimal"),
780
+ ("Balanced", "Structural"),
781
+ ("Structural", "Balanced"),
782
+ }
783
+ else .65
784
+ )
785
+ grammar = max(.6, 1 - .1 * max(0, checks.get("grammar_delta", 0)))
786
+ candidate.score = (
787
+ 100 * (
788
+ .43 * semantic
789
+ + .22 * fit
790
+ + .15 * lane_fit
791
+ + .12 * grammar
792
+ + .08 * candidate.priority
793
+ )
794
+ if ok else -1e9
795
+ )
796
+ candidate.checks = checks
797
+ (good if ok else bad).append(candidate)
798
+
799
+ good.sort(key=lambda item: item.score, reverse=True)
800
+ bad.sort(key=lambda item: (
801
+ len(item.checks.get("failures", [])),
802
+ -item.checks.get("change", 0),
803
+ ))
804
+ return good, bad
805
+
806
+
807
+ # =====================================================================
808
+ # ENGINE
809
+ # =====================================================================
810
+
811
+ class Engine:
812
+ def __init__(self):
813
+ self.an = Analyzer()
814
+ self.gen = Generator()
815
+ self.sc = Scanner()
816
+ self.rank = Ranker()
817
+
818
+ def one(self, sentence, profile, user_terms, mode):
819
+ _ensure_state()
820
+ analysis = self.an.analyze(sentence, profile, user_terms)
821
+
822
+ if analysis.families == ["PARSE_FAILURE"]:
823
+ return SentenceResult(
824
+ sentence, sentence, "PARSE_FAILURE", analysis.families, None,
825
+ "Dependency parse unavailable and no surface-safe family matched.",
826
+ [], {}, {"detected": analysis.families},
827
+ )
828
+
829
+ candidates, trace = self.gen.generate(
830
+ analysis, profile, user_terms, mode
831
+ )
832
+
833
+ if not candidates:
834
+ code = (
835
+ "NO_PATTERN_COVERAGE"
836
+ if not trace.get("routes")
837
+ else "CANDIDATE_GENERATION_FAILURE"
838
+ )
839
+ return SentenceResult(
840
+ sentence, sentence, code, analysis.families, None,
841
+ "No transformation produced a distinct candidate for the detected structures.",
842
+ [], {}, trace,
843
+ )
844
+
845
+ good, bad = self.rank.rank(
846
+ sentence, candidates, analysis, profile, user_terms, mode
847
+ )
848
+
849
+ trace["candidate_traces"] = [
850
+ {
851
+ "text": item.text,
852
+ "lane": item.lane,
853
+ "family": item.family,
854
+ "reason": item.reason,
855
+ "accepted": item.checks.get("accepted"),
856
+ "failures": item.checks.get("failures", []),
857
+ "rejection_reasons": item.checks.get("rejection_reasons", []),
858
+ "change": item.checks.get("change"),
859
+ "sts": item.checks.get("sts"),
860
+ "nli_forward": item.checks.get("nli_forward"),
861
+ "nli_reverse": item.checks.get("nli_reverse"),
862
+ "score": item.score,
863
+ }
864
+ for item in good + bad
865
+ ]
866
+
867
+ if not good:
868
+ counts = Counter(
869
+ code
870
+ for item in bad
871
+ for code in item.checks.get("failures", [])
872
+ )
873
+ precedence = [
874
+ "TERMINOLOGY_REJECTION",
875
+ "POLARITY_REJECTION",
876
+ "MODALITY_REJECTION",
877
+ "QUANTITY_REJECTION",
878
+ "ENUMERATION_REJECTION",
879
+ "GRAMMAR_REJECTION",
880
+ "SEMANTIC_REJECTION",
881
+ "INSUFFICIENT_REWRITE",
882
+ ]
883
+ status = next((code for code in precedence if counts[code]), "RANKING_EMPTY")
884
+ return SentenceResult(
885
+ sentence, sentence, status, analysis.families, None,
886
+ "Candidates were generated, but none passed all hard safeguards and rewrite gates.",
887
+ [item.text for item in bad[:3]],
888
+ bad[0].checks if bad else {},
889
+ trace | {"rejections": dict(counts)},
890
+ )
891
+
892
+ selected = good[0]
893
+ trace["selected"] = {
894
+ "text": selected.text,
895
+ "lane": selected.lane,
896
+ "family": selected.family,
897
+ "score": selected.score,
898
+ }
899
+ return SentenceResult(
900
+ sentence, selected.text, "SUCCESS", analysis.families,
901
+ selected.family, selected.reason,
902
+ [item.text for item in good[1:4]],
903
+ selected.checks, trace,
904
+ )
905
+
906
+ def process(self, text, workflow, mode, discipline, terms_text, progress=None):
907
+ STATE.clear()
908
+ STATE.update(start=time.time(), warnings=[])
909
+ profile, evidence = self.route(text, discipline)
910
+ user_terms = {
911
+ item.strip()
912
+ for item in re.split(r"[,;\n]", terms_text or "")
913
+ if item.strip()
914
+ }
915
+ findings, score, band, features = self.sc.scan(text)
916
+
917
+ if workflow == "Scan Only":
918
+ return DocumentResult(
919
+ text, text, workflow, mode, profile.name, evidence,
920
+ score, band, findings, [], STATE["warnings"],
921
+ {"features": features},
922
+ )
923
+
924
+ paragraphs = [
925
+ item.strip()
926
+ for item in re.split(r"\n\s*\n", text)
927
+ if item.strip()
928
+ ]
929
+ total = sum(len(sent_split(item)) for item in paragraphs)
930
+ done = 0
931
+ results = []
932
+ output_paragraphs = []
933
+
934
+ for block_start in range(0, len(paragraphs), CFG.batch):
935
+ for paragraph in paragraphs[block_start:block_start + CFG.batch]:
936
+ output_sentences = []
937
+ for sentence in sent_split(paragraph):
938
+ targeted = (
939
+ workflow == "Edit / Paraphrase Only"
940
+ or any(finding.sentence == len(results) for finding in findings)
941
+ )
942
+ if targeted:
943
+ item = self.one(sentence, profile, user_terms, mode)
944
+ else:
945
+ item = SentenceResult(
946
+ sentence, sentence, "NOT_TARGETED", [], None,
947
+ "No scanner finding targeted this sentence.",
948
+ [], {}, {},
949
+ )
950
+ results.append(item)
951
+ output_sentences.append(item.revised)
952
+ done += 1
953
+ if progress:
954
+ progress(
955
+ done / max(total, 1),
956
+ desc=f"Sentence {done}/{total}",
957
+ )
958
+ output_paragraphs.append(" ".join(output_sentences))
959
+
960
+ return DocumentResult(
961
+ text,
962
+ "\n\n".join(output_paragraphs),
963
+ workflow,
964
+ mode,
965
+ profile.name,
966
+ evidence,
967
+ score,
968
+ band,
969
+ findings,
970
+ results,
971
+ STATE["warnings"],
972
+ {
973
+ "sentences": total,
974
+ "success": sum(item.status == "SUCCESS" for item in results),
975
+ "unchanged": sum(item.original == item.revised for item in results),
976
+ "seconds": time.time() - STATE["start"],
977
+ },
978
+ )
979
+
980
+ def route(self, text, requested):
981
+ if requested in PROFILES:
982
+ return PROFILES[requested], ["manual"]
983
+
984
+ lo = text.lower()
985
+ scores = {
986
+ name: sum(
987
+ bool(re.search(r"(?<!\w)" + re.escape(term.lower()) + r"(?!\w)", lo))
988
+ for term in profile.keywords | profile.phrases
989
+ )
990
+ for name, profile in PROFILES.items()
991
+ }
992
+ winner = max(scores, key=scores.get)
993
+ winner = winner if scores[winner] else "General Academic"
994
+ evidence = [
995
+ term for term in PROFILES[winner].keywords
996
+ if term.lower() in lo
997
+ ][:10]
998
+ return PROFILES[winner], evidence
999
+
1000
+
1001
+ ENGINE = Engine()
1002
+
1003
+
1004
+ # =====================================================================
1005
+ # REPORT WRITER
1006
+ # =====================================================================
1007
+
1008
+ from docx import Document
1009
+
1010
+
1011
+ def report(result):
1012
+ folder = Path(CFG.out) / ("NLL_" + time.strftime("%Y%m%d_%H%M%S"))
1013
+ folder.mkdir(parents=True, exist_ok=True)
1014
+
1015
+ revised_path = folder / "revised.txt"
1016
+ diagnostics_path = folder / "diagnostics.json"
1017
+ revised_path.write_text(result.revised, encoding="utf8")
1018
+ diagnostics_path.write_text(
1019
+ json.dumps(asdict(result), indent=2, default=str),
1020
+ encoding="utf8",
1021
+ )
1022
+
1023
+ document = Document()
1024
+ document.add_heading("NLL Revised Document", 0)
1025
+ document.add_paragraph(result.revised)
1026
+ document.add_heading("Review", 1)
1027
+ document.add_paragraph(
1028
+ f"Writing Pattern Score: {result.pattern_score:.0f}/100 ({result.band}). "
1029
+ "The score describes observable structural and stylistic patterns only."
1030
+ )
1031
+
1032
+ for index, item in enumerate(result.sentences, 1):
1033
+ document.add_heading(f"Sentence {index}: {item.status}", 2)
1034
+ document.add_paragraph("Original: " + item.original)
1035
+ document.add_paragraph("Revised: " + item.revised)
1036
+ document.add_paragraph("Reason: " + item.reason)
1037
+ document.add_paragraph("Detected structures: " + ", ".join(item.families))
1038
+ document.add_paragraph(
1039
+ "Diagnostics: " + json.dumps(item.diagnostics, default=str)[:4000]
1040
+ )
1041
+
1042
+ doc_path = folder / "revised_and_review.docx"
1043
+ document.save(doc_path)
1044
+ return [str(doc_path), str(revised_path), str(diagnostics_path)]
1045
+
1046
+
1047
+ # =====================================================================
1048
+ # SELF-TESTS
1049
+ # =====================================================================
1050
+
1051
+ _ensure_state()
1052
+
1053
+ # 1. Polarity movement: reordering 'without' must not be rejected simply
1054
+ # because its dependency head changed.
1055
+ _test_profile = next(iter(PROFILES.values()))
1056
+ _test_analyzer = Analyzer()
1057
+ _pol_source = (
1058
+ "Systems train separate models without explicit rules for assessment, "
1059
+ "correction, and graduation."
1060
+ )
1061
+ _pol_candidate = Candidate(
1062
+ "Without explicit rules for assessment, correction, and graduation, "
1063
+ "systems train separate models.",
1064
+ "Balanced",
1065
+ "FINITE_COMPLEMENT_LIST",
1066
+ "validator self-test",
1067
+ .9,
1068
+ )
1069
+ _pol_analysis = _test_analyzer.analyze(_pol_source, _test_profile, set())
1070
+ _pol_ok, _pol_fail, _ = Validator().validate(
1071
+ _pol_source, _pol_candidate, _pol_analysis, _test_profile, set(), "Balanced"
1072
+ )
1073
+ if "POLARITY_REJECTION" in _pol_fail:
1074
+ raise RuntimeError(
1075
+ "Cell 7 v4.3 failed: polarity-preserving 'without' movement was rejected."
1076
+ )
1077
+
1078
+ # 2. Enumeration must survive when the candidate reorganizes/splits text.
1079
+ _enum_source = "We propose Framework X, a protocol consisting of contracts, examinations, and promotion."
1080
+ _enum_candidate = Candidate(
1081
+ "Framework X is a protocol consisting of contracts, examinations, and promotion proposed by us.",
1082
+ "Balanced",
1083
+ "APPOSITIVE_DEFINITION",
1084
+ "validator self-test",
1085
+ .9,
1086
+ )
1087
+ _enum_analysis = _test_analyzer.analyze(_enum_source, _test_profile, set())
1088
+ _, _enum_fail, _ = Validator().validate(
1089
+ _enum_source, _enum_candidate, _enum_analysis, _test_profile, set(), "Balanced"
1090
+ )
1091
+ if "ENUMERATION_REJECTION" in _enum_fail:
1092
+ raise RuntimeError(
1093
+ "Cell 7 v4.3 failed: preserved enumeration was rejected."
1094
+ )
1095
+
1096
+ # 3. Malformed realization must fail at the judge even if it somehow bypasses 5A.
1097
+ _bad_candidate = Candidate(
1098
+ "Protocol to learners spanning classification, drafting, and planning.",
1099
+ "Structural",
1100
+ "PARTICIPIAL_POSTMODIFIER",
1101
+ "validator self-test",
1102
+ .9,
1103
+ )
1104
+ _bad_analysis = _test_analyzer.analyze(
1105
+ "The testbed applies the protocol to learners spanning classification, drafting, and planning.",
1106
+ _test_profile,
1107
+ set(),
1108
+ )
1109
+ _, _bad_fail, _ = Validator().validate(
1110
+ "The testbed applies the protocol to learners spanning classification, drafting, and planning.",
1111
+ _bad_candidate,
1112
+ _bad_analysis,
1113
+ _test_profile,
1114
+ set(),
1115
+ "Structural",
1116
+ )
1117
+ if "GRAMMAR_REJECTION" not in _bad_fail:
1118
+ raise RuntimeError(
1119
+ "Cell 7 v4.3 failed: malformed participial fragment was not rejected."
1120
+ )
1121
+
1122
+ # 4. Appositive before the true list must not become a pseudo-member.
1123
+ _long_enum_source = (
1124
+ "We propose Framework X (FX), a protocol consisting of native model contracts, "
1125
+ "historical contracts, protected examinations, peer study, and promotion."
1126
+ )
1127
+ _long_enum_candidate = Candidate(
1128
+ "Framework X (FX) is a protocol consisting of native model contracts, "
1129
+ "historical contracts, protected examinations, peer study, and promotion proposed by us.",
1130
+ "Balanced", "APPOSITIVE_DEFINITION", "validator self-test", .9,
1131
+ )
1132
+ _long_enum_analysis = _test_analyzer.analyze(_long_enum_source, _test_profile, set())
1133
+ _, _long_enum_fail, _ = Validator().validate(
1134
+ _long_enum_source, _long_enum_candidate, _long_enum_analysis,
1135
+ _test_profile, set(), "Balanced"
1136
+ )
1137
+ if "ENUMERATION_REJECTION" in _long_enum_fail:
1138
+ raise RuntimeError(
1139
+ "Cell 7 v4.3 failed: appositive boundary corrupted enumeration extraction."
1140
+ )
1141
+
1142
+ # 5. Hyphenated list members must remain intact during comparison.
1143
+ _hyphen_source = (
1144
+ "We pre-register baseline questions, disjoint re-examination data, matched-data "
1145
+ "baselines, ablations, and independently held-out testing."
1146
+ )
1147
+ _hyphen_candidate = Candidate(
1148
+ "Our preregistration covers baseline questions, disjoint re-examination data, "
1149
+ "matched-data baselines, ablations, and independently held-out testing.",
1150
+ "Balanced", "FINITE_OBJECT_LIST_FRAME", "validator self-test", .9,
1151
+ )
1152
+ _hyphen_analysis = _test_analyzer.analyze(_hyphen_source, _test_profile, set())
1153
+ _, _hyphen_fail, _ = Validator().validate(
1154
+ _hyphen_source, _hyphen_candidate, _hyphen_analysis,
1155
+ _test_profile, set(), "Balanced"
1156
+ )
1157
+ if "ENUMERATION_REJECTION" in _hyphen_fail:
1158
+ raise RuntimeError(
1159
+ "Cell 7 v4.3 failed: hyphenated list member was split or lost."
1160
+ )
1161
+
1162
+ # 6. Unicode punctuation variants must compare as the same content tokens.
1163
+ if _unicode_tokens("re-examination data") != _unicode_tokens("re examination data"):
1164
+ raise RuntimeError(
1165
+ "Cell 7 v4.3 failed: ASCII hyphen token normalization mismatch."
1166
+ )
1167
+ if _unicode_tokens("matched–data") != _unicode_tokens("matched-data"):
1168
+ raise RuntimeError(
1169
+ "Cell 7 v4.3 failed: Unicode dash token normalization mismatch."
1170
+ )
1171
+ if _unicode_tokens("ALCEF–RSF") != _unicode_tokens("ALCEF-RSF"):
1172
+ raise RuntimeError(
1173
+ "Cell 7 v4.3 failed: acronym dash normalization mismatch."
1174
+ )
1175
+
1176
+ # 7. Boundary-corrupted pseudo-members must be rejected, while legitimate
1177
+ # multiword and hyphenated members remain eligible.
1178
+ if _plausible_enumeration_member(
1179
+ "Framework X (FX), a protocol consisting of native contracts, historical contracts"
1180
+ ):
1181
+ raise RuntimeError(
1182
+ "Cell 7 v4.3 failed: appositive pseudo-member passed confidence gate."
1183
+ )
1184
+ if not _plausible_enumeration_member("disjoint re-examination data"):
1185
+ raise RuntimeError(
1186
+ "Cell 7 v4.3 failed: valid hyphenated member failed confidence gate."
1187
+ )
1188
+ if not _plausible_enumeration_member("independently held-out testing"):
1189
+ raise RuntimeError(
1190
+ "Cell 7 v4.3 failed: valid multiword member failed confidence gate."
1191
+ )
1192
+
1193
+ print("=" * 78)
1194
+ print("CELL 7 VALIDATION / RANKING / ENGINE v4.3 LOADED")
1195
+ print("=" * 78)
1196
+ print("Validator: OK")
1197
+ print("Ranker: OK")
1198
+ print("Engine: OK")
1199
+ print("ENGINE instance: OK")
1200
+ print("Polarity-inventory validation: active")
1201
+ print("Dependency-aware Unicode enumeration validation: active")
1202
+ print("Appositive/list-boundary protection: active")
1203
+ print("Hyphen/dash-insensitive content comparison: active")
1204
+ print("Parser-artifact group filtering: active")
1205
+ print("Enumeration-member confidence gate: active")
1206
+ print("Noisy-superset group suppression: active")
1207
+ print("Cell 5A realization gate rechecked by judge: active")
1208
+ print("Optional STS/NLI signals: soft unless contradiction is detected")
1209
+ print("Runtime warning-state safety: active")
1210
+ print("Validator self-tests: PASSED")
1211
+ print("Registered transformation rules:", len(RULES))
1212
+ print("Expected rule count:", EXPECTED_CELL7_RULE_COUNT)
1213
+ print("=" * 78)
runtime/cell7a.py ADDED
@@ -0,0 +1,163 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #@title 7A. RUNTIME-STATE COMPATIBILITY v2 — aligned with Cell 7 v4
2
+ # Complete replacement for Cell 7A.
3
+ # Run AFTER Cell 7 v4 and BEFORE Cell 7B / Cells 8–10.
4
+ #
5
+ # Cell 7 v4 already initializes STATE internally. This compatibility layer
6
+ # keeps direct Validator/Ranker/Engine.one calls safe, preserves the legacy
7
+ # _surface_key alias, prevents wrapper stacking, and recreates ENGINE once.
8
+ # It does not change routing, transformation rules, validation policy,
9
+ # ranking weights, or must-paraphrase policy.
10
+
11
+ EXPECTED_7A_RULE_COUNT = 30
12
+
13
+ _REQUIRED_7A = [
14
+ "STATE", "RULES", "Validator", "Ranker", "Engine", "ENGINE",
15
+ "surface_key", "realization_issues",
16
+ ]
17
+ _missing_7a = [name for name in _REQUIRED_7A if name not in globals()]
18
+ if _missing_7a:
19
+ raise RuntimeError(
20
+ "Cell 7A cannot run. Missing: " + ", ".join(_missing_7a)
21
+ + ". Run Cell 7 v4 first."
22
+ )
23
+
24
+ if len(RULES) != EXPECTED_7A_RULE_COUNT:
25
+ raise RuntimeError(
26
+ f"Cell 7A expected {EXPECTED_7A_RULE_COUNT} rules but found {len(RULES)}."
27
+ )
28
+
29
+
30
+ # =====================================================================
31
+ # 1. LEGACY NAME COMPATIBILITY
32
+ # =====================================================================
33
+
34
+ _surface_key = surface_key
35
+
36
+
37
+ # =====================================================================
38
+ # 2. STATE SAFETY
39
+ # =====================================================================
40
+
41
+ def _ensure_runtime_state():
42
+ if not isinstance(STATE, dict):
43
+ raise RuntimeError("STATE must be a dictionary.")
44
+ STATE.setdefault("warnings", [])
45
+ STATE.setdefault("start", None)
46
+ return STATE
47
+
48
+
49
+ _ensure_runtime_state()
50
+
51
+
52
+ # =====================================================================
53
+ # 3. WRAPPER INSTALLATION — RERUN SAFE
54
+ # =====================================================================
55
+
56
+ # Because Cell 7 has just redefined these classes, preserve its current
57
+ # methods as the canonical bases. If this cell is rerun without rerunning
58
+ # Cell 7, restore those bases before wrapping again.
59
+
60
+ if not hasattr(Validator, "_nll_state_safe_base_validate_v2"):
61
+ Validator._nll_state_safe_base_validate_v2 = Validator.validate
62
+ else:
63
+ Validator.validate = Validator._nll_state_safe_base_validate_v2
64
+
65
+ if not hasattr(Ranker, "_nll_state_safe_base_rank_v2"):
66
+ Ranker._nll_state_safe_base_rank_v2 = Ranker.rank
67
+ else:
68
+ Ranker.rank = Ranker._nll_state_safe_base_rank_v2
69
+
70
+ if not hasattr(Engine, "_nll_state_safe_base_one_v2"):
71
+ Engine._nll_state_safe_base_one_v2 = Engine.one
72
+ else:
73
+ Engine.one = Engine._nll_state_safe_base_one_v2
74
+
75
+
76
+ def _validate_with_runtime_state_v2(self, *args, **kwargs):
77
+ _ensure_runtime_state()
78
+ return Validator._nll_state_safe_base_validate_v2(self, *args, **kwargs)
79
+
80
+
81
+ def _rank_with_runtime_state_v2(self, *args, **kwargs):
82
+ _ensure_runtime_state()
83
+ return Ranker._nll_state_safe_base_rank_v2(self, *args, **kwargs)
84
+
85
+
86
+ def _one_with_runtime_state_v2(self, *args, **kwargs):
87
+ _ensure_runtime_state()
88
+ return Engine._nll_state_safe_base_one_v2(self, *args, **kwargs)
89
+
90
+
91
+ Validator.validate = _validate_with_runtime_state_v2
92
+ Ranker.rank = _rank_with_runtime_state_v2
93
+ Engine.one = _one_with_runtime_state_v2
94
+
95
+ # Recreate engine after wrapper installation.
96
+ ENGINE = Engine()
97
+
98
+
99
+ # =====================================================================
100
+ # 4. SELF-TESTS
101
+ # =====================================================================
102
+
103
+ STATE.clear()
104
+ _ensure_runtime_state()
105
+
106
+ if "warnings" not in STATE or not isinstance(STATE["warnings"], list):
107
+ raise RuntimeError("Cell 7A v2 failed: warnings state was not restored.")
108
+
109
+ if _surface_key("A test.") != surface_key("A test."):
110
+ raise RuntimeError("Cell 7A v2 failed: legacy surface-key alias mismatch.")
111
+
112
+ if getattr(Validator.validate, "__name__", "") != "_validate_with_runtime_state_v2":
113
+ raise RuntimeError("Cell 7A v2 failed: Validator wrapper not installed.")
114
+
115
+ if getattr(Ranker.rank, "__name__", "") != "_rank_with_runtime_state_v2":
116
+ raise RuntimeError("Cell 7A v2 failed: Ranker wrapper not installed.")
117
+
118
+ if getattr(Engine.one, "__name__", "") != "_one_with_runtime_state_v2":
119
+ raise RuntimeError("Cell 7A v2 failed: Engine.one wrapper not installed.")
120
+
121
+ # Direct Validator smoke test, the scenario that previously caused
122
+ # KeyError('warnings'). The exact validation outcome is irrelevant here;
123
+ # the test verifies state-safe execution.
124
+ _test_profile = next(iter(PROFILES.values())) if "PROFILES" in globals() and PROFILES else None
125
+ if _test_profile is not None:
126
+ _test_source = "Research claims are contingent on execution."
127
+ _test_analysis = Analyzer().analyze(_test_source, _test_profile, set())
128
+ _test_candidate = Candidate(
129
+ "Research claims depend on execution.",
130
+ "Minimal",
131
+ "CONTINGENCY_RELATION",
132
+ "runtime-state self-test",
133
+ .9,
134
+ )
135
+ try:
136
+ Validator().validate(
137
+ _test_source,
138
+ _test_candidate,
139
+ _test_analysis,
140
+ _test_profile,
141
+ set(),
142
+ "Minimal",
143
+ )
144
+ except KeyError as exc:
145
+ raise RuntimeError(
146
+ "Cell 7A v2 failed direct Validator state test: " + repr(exc)
147
+ )
148
+
149
+ print("=" * 78)
150
+ print("CELL 7A RUNTIME-STATE COMPATIBILITY v2 LOADED")
151
+ print("=" * 78)
152
+ print("Aligned Cell 7 version: v4")
153
+ print("STATE['warnings'] initialization: active")
154
+ print("Legacy _surface_key alias: active")
155
+ print("Validator direct-call safety: active")
156
+ print("Ranker direct-call safety: active")
157
+ print("Engine.one direct-call safety: active")
158
+ print("Rerun-safe wrapper restoration: active")
159
+ print("Direct Validator state self-test: PASSED")
160
+ print("ENGINE recreated: active")
161
+ print("Registered transformation rules:", len(RULES))
162
+ print("Expected rule count:", EXPECTED_7A_RULE_COUNT)
163
+ print("=" * 78)
runtime/cell7b.py ADDED
@@ -0,0 +1,404 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #@title 7B. DETERMINISTIC MUST-PARAPHRASE ORCHESTRATOR v2.1
2
+ # Complete replacement for Cell 7B.
3
+ # Run after Cell 7A v2 and before Cells 8, 8A, 9, and 10.
4
+ #
5
+ # This orchestration layer does not generate language itself.
6
+ # It performs a bounded search over the three existing deterministic modes,
7
+ # delegates candidate judgment to Cell 7 v4, preserves all hard safeguards,
8
+ # and keeps the source only when every safe search path is exhausted.
9
+
10
+ from collections import Counter
11
+
12
+ EXPECTED_7B_RULE_COUNT = 30
13
+
14
+ _REQUIRED_7B = [
15
+ "STATE", "RULES", "Engine", "ENGINE", "Validator", "Ranker",
16
+ "Generator", "Analyzer", "PROFILES", "Candidate",
17
+ "surface_key", "realization_issues", "_ensure_runtime_state",
18
+ ]
19
+ _missing_7b = [name for name in _REQUIRED_7B if name not in globals()]
20
+ if _missing_7b:
21
+ raise RuntimeError(
22
+ "Cell 7B cannot run. Missing: " + ", ".join(_missing_7b)
23
+ + ". Run Cell 7 v4.3 and Cell 7A v2 first."
24
+ )
25
+
26
+ if len(RULES) != EXPECTED_7B_RULE_COUNT:
27
+ raise RuntimeError(
28
+ f"Cell 7B expected {EXPECTED_7B_RULE_COUNT} rules but found {len(RULES)}."
29
+ )
30
+
31
+ # Verify that 7A v2, not an older wrapper, is installed.
32
+ if getattr(Engine.one, "__name__", "") != "_one_with_runtime_state_v2":
33
+ raise RuntimeError(
34
+ "Cell 7B requires Cell 7A v2 immediately before it. "
35
+ "Expected Engine.one wrapper '_one_with_runtime_state_v2'."
36
+ )
37
+
38
+
39
+ # =====================================================================
40
+ # 1. CONFIGURATION
41
+ # =====================================================================
42
+
43
+ MUST_PARAPHRASE_ENABLED = True
44
+
45
+ _MODE_RETRY_ORDER = {
46
+ "Minimal": ["Minimal", "Balanced", "Structural"],
47
+ "Balanced": ["Balanced", "Structural", "Minimal"],
48
+ "Structural": ["Structural", "Balanced", "Minimal"],
49
+ }
50
+
51
+ _HARD_REJECTION_CODES = {
52
+ "TERMINOLOGY_REJECTION",
53
+ "POLARITY_REJECTION",
54
+ "MODALITY_REJECTION",
55
+ "QUANTITY_REJECTION",
56
+ "ENUMERATION_REJECTION",
57
+ "GRAMMAR_REJECTION",
58
+ "SEMANTIC_REJECTION",
59
+ }
60
+
61
+
62
+ def _mp_changed(original, revised):
63
+ return surface_key(original) != surface_key(revised)
64
+
65
+
66
+ def _mp_trace(result):
67
+ trace = getattr(result, "diagnostics", None)
68
+ if trace is None:
69
+ trace = getattr(result, "trace", None)
70
+ return dict(trace or {})
71
+
72
+
73
+ def _mp_checks(result):
74
+ return dict(getattr(result, "checks", None) or {})
75
+
76
+
77
+ def _mp_result_summary(result, attempted_mode):
78
+ trace = _mp_trace(result)
79
+ checks = _mp_checks(result)
80
+ revised = getattr(result, "revised", "")
81
+ original = getattr(result, "original", "")
82
+ selected = trace.get("selected") or {}
83
+ failures = list(checks.get("failures", []) or [])
84
+ realization = list(realization_issues(revised) or [])
85
+
86
+ return {
87
+ "attempted_mode": attempted_mode,
88
+ "status": getattr(result, "status", ""),
89
+ "changed": _mp_changed(original, revised),
90
+ "selected_family": getattr(result, "family", None) or selected.get("family"),
91
+ "selected_lane": selected.get("lane"),
92
+ "selected_score": selected.get("score"),
93
+ "failures": failures,
94
+ "hard_failures": [code for code in failures if code in _HARD_REJECTION_CODES],
95
+ "realization_issues": realization,
96
+ "generated_by_lane": trace.get("generated_by_lane", {}),
97
+ "generated_before_final_filter": trace.get("generated_before_final_filter"),
98
+ "generated_after_final_filter": trace.get("generated_after_final_filter"),
99
+ "realization_rejections": trace.get("realization_rejections", []),
100
+ "terminal_rejections": trace.get("rejections", {}),
101
+ "reason": getattr(result, "reason", ""),
102
+ "revised": revised,
103
+ }
104
+
105
+
106
+ def _mp_is_safe_success(result):
107
+ if getattr(result, "status", "") != "SUCCESS":
108
+ return False
109
+
110
+ original = getattr(result, "original", "")
111
+ revised = getattr(result, "revised", "")
112
+ if not _mp_changed(original, revised):
113
+ return False
114
+
115
+ checks = _mp_checks(result)
116
+ failures = set(checks.get("failures", []) or [])
117
+ if failures & _HARD_REJECTION_CODES:
118
+ return False
119
+
120
+ if realization_issues(revised):
121
+ return False
122
+
123
+ return True
124
+
125
+
126
+ def _mp_failure_reason(attempts):
127
+ if not attempts:
128
+ return (
129
+ "No rewrite attempt was completed. The original sentence was preserved "
130
+ "because the deterministic judge had no result to assess."
131
+ )
132
+
133
+ generated_total = 0
134
+ realization_rejections = 0
135
+ rejection_codes = Counter()
136
+ statuses = Counter()
137
+
138
+ for attempt in attempts:
139
+ statuses[attempt.get("status", "UNKNOWN")] += 1
140
+
141
+ generated = attempt.get("generated_after_final_filter")
142
+ if generated is None:
143
+ generated = sum(
144
+ int(value or 0)
145
+ for value in (attempt.get("generated_by_lane", {}) or {}).values()
146
+ )
147
+ generated_total += int(generated or 0)
148
+ realization_rejections += len(attempt.get("realization_rejections", []) or [])
149
+
150
+ for code in attempt.get("failures", []) or []:
151
+ rejection_codes[code] += 1
152
+ for code, count in (attempt.get("terminal_rejections", {}) or {}).items():
153
+ rejection_codes[code] += int(count or 0)
154
+
155
+ if generated_total == 0 and realization_rejections == 0:
156
+ return (
157
+ "The original sentence was preserved because no registered transformation "
158
+ "produced a distinct candidate in Minimal, Balanced, or Structural mode."
159
+ )
160
+
161
+ if realization_rejections and generated_total == 0:
162
+ return (
163
+ "The original sentence was preserved because every generated alternative "
164
+ "failed the grammatical realization gate."
165
+ )
166
+
167
+ if rejection_codes:
168
+ detail = ", ".join(
169
+ f"{code}: {count}" for code, count in sorted(rejection_codes.items())
170
+ )
171
+ return (
172
+ "The original sentence was preserved because all distinct alternatives "
173
+ f"were rejected by the deterministic judge ({detail})."
174
+ )
175
+
176
+ status_detail = ", ".join(
177
+ f"{status}: {count}" for status, count in sorted(statuses.items())
178
+ )
179
+ return (
180
+ "The original sentence was preserved because no attempted mode produced a "
181
+ "safe validated rewrite. Attempt outcomes: " + status_detail + "."
182
+ )
183
+
184
+
185
+ # =====================================================================
186
+ # 2. RERUN-SAFE ENGINE.ONE ORCHESTRATION
187
+ # =====================================================================
188
+
189
+ # Preserve the post-7A-v2 Engine.one once. If 7B v2 is rerun without
190
+ # rerunning 7A, restore that exact base before installing this wrapper again.
191
+ if not hasattr(Engine, "_nll_must_paraphrase_base_one_v2"):
192
+ Engine._nll_must_paraphrase_base_one_v2 = Engine.one
193
+ else:
194
+ Engine.one = Engine._nll_must_paraphrase_base_one_v2
195
+
196
+
197
+ def _one_with_must_paraphrase_v2(self, sentence, profile, user_terms, mode):
198
+ _ensure_runtime_state()
199
+
200
+ requested_mode = mode if mode in _MODE_RETRY_ORDER else "Balanced"
201
+
202
+ if not MUST_PARAPHRASE_ENABLED:
203
+ return Engine._nll_must_paraphrase_base_one_v2(
204
+ self, sentence, profile, user_terms, requested_mode
205
+ )
206
+
207
+ attempts = []
208
+ attempted_results = []
209
+
210
+ for attempted_mode in _MODE_RETRY_ORDER[requested_mode]:
211
+ result = Engine._nll_must_paraphrase_base_one_v2(
212
+ self, sentence, profile, user_terms, attempted_mode
213
+ )
214
+ attempt = _mp_result_summary(result, attempted_mode)
215
+ attempts.append(attempt)
216
+ attempted_results.append(result)
217
+
218
+ if _mp_is_safe_success(result):
219
+ trace = _mp_trace(result)
220
+ trace["must_paraphrase"] = {
221
+ "enabled": True,
222
+ "requested_mode": requested_mode,
223
+ "accepted_mode": attempted_mode,
224
+ "attempt_count": len(attempts),
225
+ "attempts": attempts,
226
+ "outcome": "SAFE_CHANGED_OUTPUT",
227
+ "hard_safeguards_relaxed": False,
228
+ }
229
+
230
+ try:
231
+ result.diagnostics = trace
232
+ except Exception:
233
+ pass
234
+ try:
235
+ result.trace = trace
236
+ except Exception:
237
+ pass
238
+
239
+ original_reason = str(getattr(result, "reason", "") or "").strip()
240
+ suffix = (
241
+ f" Must-paraphrase judge accepted the {attempted_mode} attempt "
242
+ f"after {len(attempts)} bounded attempt(s)."
243
+ )
244
+ try:
245
+ result.reason = (original_reason + suffix).strip()
246
+ except Exception:
247
+ pass
248
+ return result
249
+
250
+ # Exhausted all three modes. Preserve the source and expose precisely why.
251
+ fallback = attempted_results[0]
252
+ reason = _mp_failure_reason(attempts)
253
+ trace = _mp_trace(fallback)
254
+ trace["must_paraphrase"] = {
255
+ "enabled": True,
256
+ "requested_mode": requested_mode,
257
+ "accepted_mode": None,
258
+ "attempt_count": len(attempts),
259
+ "attempts": attempts,
260
+ "outcome": "ORIGINAL_PRESERVED_AFTER_EXHAUSTIVE_SAFE_SEARCH",
261
+ "preservation_reason": reason,
262
+ "hard_safeguards_relaxed": False,
263
+ }
264
+
265
+ try:
266
+ fallback.diagnostics = trace
267
+ except Exception:
268
+ pass
269
+ try:
270
+ fallback.trace = trace
271
+ except Exception:
272
+ pass
273
+ try:
274
+ fallback.reason = reason
275
+ except Exception:
276
+ pass
277
+
278
+ return fallback
279
+
280
+
281
+ Engine.one = _one_with_must_paraphrase_v2
282
+ ENGINE = Engine()
283
+
284
+
285
+ # =====================================================================
286
+ # 3. SELF-TESTS
287
+ # =====================================================================
288
+
289
+ if getattr(Engine.one, "__name__", "") != "_one_with_must_paraphrase_v2":
290
+ raise RuntimeError("Cell 7B v2.1 failed: Engine.one wrapper not installed.")
291
+
292
+ if not hasattr(Engine, "_nll_must_paraphrase_base_one_v2"):
293
+ raise RuntimeError("Cell 7B v2.1 failed: base Engine.one was not preserved.")
294
+
295
+ for preferred, order in _MODE_RETRY_ORDER.items():
296
+ if len(order) != 3 or order[0] != preferred or len(set(order)) != 3:
297
+ raise RuntimeError(
298
+ f"Cell 7B v2 failed retry order for {preferred}: {order}"
299
+ )
300
+
301
+ # End-to-end orchestration smoke test using a known transformable sentence.
302
+ _test_profile, _ = ENGINE.route(
303
+ "The next step is to inspect how the handler checks configuration.",
304
+ "Auto Detect",
305
+ )
306
+ _test_result = ENGINE.one(
307
+ "The next step is to inspect how the handler checks configuration.",
308
+ _test_profile,
309
+ set(),
310
+ "Balanced",
311
+ )
312
+ _test_trace = _mp_trace(_test_result)
313
+ _test_mp = _test_trace.get("must_paraphrase", {}) or {}
314
+
315
+ if getattr(_test_result, "status", "") != "SUCCESS":
316
+ raise RuntimeError(
317
+ "Cell 7B v2 smoke test failed: known transformable sentence did not succeed."
318
+ )
319
+ if not _mp_changed(_test_result.original, _test_result.revised):
320
+ raise RuntimeError(
321
+ "Cell 7B v2 smoke test failed: successful result was unchanged."
322
+ )
323
+ if _test_mp.get("outcome") != "SAFE_CHANGED_OUTPUT":
324
+ raise RuntimeError(
325
+ "Cell 7B v2 smoke test failed: orchestration trace missing safe-change outcome."
326
+ )
327
+ if _test_mp.get("requested_mode") != "Balanced":
328
+ raise RuntimeError(
329
+ "Cell 7B v2.1 smoke test failed: requested mode was not preserved."
330
+ )
331
+
332
+ # Verify the live ENGINE instance is bound to the orchestration wrapper,
333
+ # not merely the Engine class.
334
+ _live_func = getattr(getattr(ENGINE, "one", None), "__func__", None)
335
+ if _live_func is not Engine.one:
336
+ raise RuntimeError(
337
+ "Cell 7B v2.1 failed: live ENGINE.one is not bound to current Engine.one."
338
+ )
339
+ if getattr(ENGINE.one, "__name__", "") != "_one_with_must_paraphrase_v2":
340
+ raise RuntimeError(
341
+ "Cell 7B v2.1 failed: live ENGINE instance is not using must-paraphrase."
342
+ )
343
+
344
+ # Document-level trace survival test. This catches the exact integration
345
+ # failure where ENGINE.one is wrapped but Engine.process returns sentence
346
+ # results without the must_paraphrase diagnostics consumed by Cell 9.
347
+ _doc_probe = ENGINE.process(
348
+ "The next step is to inspect how the handler checks configuration.",
349
+ "Edit / Paraphrase Only",
350
+ "Balanced",
351
+ "Auto Detect",
352
+ "",
353
+ )
354
+ if not getattr(_doc_probe, "sentences", None):
355
+ raise RuntimeError(
356
+ "Cell 7B v2.1 failed: document-level probe returned no sentence results."
357
+ )
358
+ _doc_sentence = _doc_probe.sentences[0]
359
+ _doc_trace = _mp_trace(_doc_sentence)
360
+ _doc_mp = _doc_trace.get("must_paraphrase", {}) or {}
361
+ if int(_doc_mp.get("attempt_count", 0) or 0) < 1:
362
+ raise RuntimeError(
363
+ "Cell 7B v2.1 failed: must-paraphrase attempt trace did not survive Engine.process."
364
+ )
365
+ if _doc_mp.get("outcome") not in {
366
+ "SAFE_CHANGED_OUTPUT",
367
+ "ORIGINAL_PRESERVED_AFTER_EXHAUSTIVE_SAFE_SEARCH",
368
+ }:
369
+ raise RuntimeError(
370
+ "Cell 7B v2.1 failed: document-level orchestration outcome is missing."
371
+ )
372
+
373
+ # Stable runtime markers for later cells. Prefer these markers over relying
374
+ # only on function-name strings.
375
+ MUST_PARAPHRASE_RUNTIME = {
376
+ "version": "2.1",
377
+ "active": True,
378
+ "engine_class_wrapper": getattr(Engine.one, "__name__", ""),
379
+ "engine_instance_wrapper": getattr(ENGINE.one, "__name__", ""),
380
+ "document_trace_verified": True,
381
+ "rule_count": len(RULES),
382
+ }
383
+
384
+ print("=" * 78)
385
+ print("CELL 7B DETERMINISTIC MUST-PARAPHRASE ORCHESTRATOR v2.1 LOADED")
386
+ print("=" * 78)
387
+ print("Aligned Cell 7 version: v4.3")
388
+ print("Aligned Cell 7A version: v2")
389
+ print("Requested-mode-first search: active")
390
+ print("Minimal/Balanced/Structural retry: active")
391
+ print("Validator + Ranker deterministic judge: active")
392
+ print("Hard safeguard relaxation: disabled")
393
+ print("Realization safeguard relaxation: disabled")
394
+ print("Unchanged-output preservation reason: active")
395
+ print("Bounded retry loop: active")
396
+ print("Sentence-level orchestration smoke test: PASSED")
397
+ print("Live ENGINE binding check: PASSED")
398
+ print("Document-level trace survival: PASSED")
399
+ print("Runtime marker: MUST_PARAPHRASE_RUNTIME active")
400
+ print("LLM generation: not used")
401
+ print("ENGINE recreated: active")
402
+ print("Registered transformation rules:", len(RULES))
403
+ print("Expected rule count:", EXPECTED_7B_RULE_COUNT)
404
+ print("=" * 78)
runtime/cell8.py ADDED
@@ -0,0 +1,262 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #@title 8. REGRESSION + ADVERSARIAL SAFETY v4 — current 30-rule stack
2
+ # Complete replacement for Cell 8.
3
+ # Compatible with Cell 5 v4.1, 5A v3.4, 6 v5.1, 7 v4, 7A v2, 7B v2.
4
+
5
+ import pandas as pd
6
+
7
+ EXPECTED_CELL8_RULE_COUNT = 30
8
+ _REQUIRED_8 = [
9
+ "ENGINE", "Engine", "Analyzer", "Validator", "Candidate", "PROFILES",
10
+ "CFG", "RULES", "surface_key", "realization_issues",
11
+ ]
12
+ _missing_8 = [name for name in _REQUIRED_8 if name not in globals()]
13
+ if _missing_8:
14
+ raise RuntimeError(
15
+ "Cell 8 cannot run. Missing: " + ", ".join(_missing_8)
16
+ + ". Run Cells 5–7B in order first."
17
+ )
18
+ if len(RULES) != EXPECTED_CELL8_RULE_COUNT:
19
+ raise RuntimeError(
20
+ f"Cell 8 expected {EXPECTED_CELL8_RULE_COUNT} rules but found {len(RULES)}."
21
+ )
22
+ if getattr(Engine.one, "__name__", "") != "_one_with_must_paraphrase_v2":
23
+ raise RuntimeError(
24
+ "Cell 8 requires Cell 7B v2. Run Cell 7B v2 before this cell."
25
+ )
26
+
27
+
28
+ def grammar_review(text):
29
+ tool = optional("lt")
30
+ if not tool:
31
+ return [{
32
+ "type": "availability",
33
+ "message": "LanguageTool disabled or unavailable; deterministic paraphrasing still runs.",
34
+ }]
35
+ try:
36
+ return [{
37
+ "type": "objective_or_style",
38
+ "message": item.message,
39
+ "context": item.context,
40
+ "replacements": item.replacements[:3],
41
+ "rule": item.ruleId,
42
+ } for item in tool.check(text)]
43
+ except Exception as exc:
44
+ return [{"type": "availability", "message": str(exc)}]
45
+
46
+
47
+ FAILED_CASES = [
48
+ "Applying XGBoost, Linear SVM, and DNN independently to both platforms reduces classifier-selection confounding and provides a common basis for robustness evaluation.",
49
+ "The purpose is not to establish a universally superior classifier but to determine how adversarial robustness and hardening vary across classifier architectures, feature spaces, attacks, and perturbation strengths.",
50
+ "These studies provide evidence that classification results obtained from unmodified malware samples do not, by themselves, describe classifier behavior when malware is deliberately modified for evasion.",
51
+ "Malware adversarial attacks also require consideration of whether changes to a feature representation can be implemented in the underlying software.",
52
+ "For Windows, Demetrio et al. (2021) examined this issue through practical manipulations of the PE file format intended to preserve malicious functionality.",
53
+ "Adversarial-example transferability has also been studied within malware environments.",
54
+ "The comparison preserves each platform's static-feature semantics and feasible perturbation constraints while applying a common adversarial-hardening methodology.",
55
+ "However, these mechanisms are commonly evaluated using different datasets, classifier families, feature representations, attack assumptions, perturbation constraints, and evaluation procedures.",
56
+ "Consequently, results reported independently for Windows and Android do not provide a controlled basis for determining how the same adversarial-hardening methodology behaves across heterogeneous static-feature environments.",
57
+ ]
58
+
59
+
60
+ def _cfg_snapshot(*names):
61
+ return {name: getattr(CFG, name, None) for name in names}
62
+
63
+
64
+ def _cfg_restore(snapshot):
65
+ for name, value in snapshot.items():
66
+ try:
67
+ setattr(CFG, name, value)
68
+ except Exception:
69
+ pass
70
+
71
+
72
+ def expanded_regression():
73
+ previous = _cfg_snapshot("sts", "nli", "mlm", "lt", "online")
74
+ CFG.sts = False
75
+ CFG.nli = False
76
+ CFG.mlm = False
77
+ CFG.lt = False
78
+ if hasattr(CFG, "online"):
79
+ CFG.online = False
80
+
81
+ rows = []
82
+ try:
83
+ for source in FAILED_CASES:
84
+ result = ENGINE.process(
85
+ source,
86
+ "Edit / Paraphrase Only",
87
+ "Balanced",
88
+ "Auto Detect",
89
+ "XGBoost, Linear SVM, DNN, Windows, Android, PE",
90
+ )
91
+ sentence = result.sentences[0]
92
+ trace = dict(getattr(sentence, "diagnostics", {}) or {})
93
+ must = trace.get("must_paraphrase", {}) or {}
94
+ realization = list(realization_issues(sentence.revised) or [])
95
+
96
+ rows.append({
97
+ "input": source,
98
+ "status": sentence.status,
99
+ "changed": surface_key(sentence.original) != surface_key(sentence.revised),
100
+ "families": " | ".join(sentence.families),
101
+ "selected_family": sentence.family,
102
+ "requested_mode": must.get("requested_mode"),
103
+ "accepted_mode": must.get("accepted_mode"),
104
+ "attempt_count": must.get("attempt_count"),
105
+ "must_paraphrase_outcome": must.get("outcome"),
106
+ "output": sentence.revised,
107
+ "realization_issues": realization,
108
+ "generated": trace.get("generated_by_lane"),
109
+ "before_final_filter": trace.get("generated_before_final_filter"),
110
+ "after_final_filter": trace.get("generated_after_final_filter"),
111
+ "rejections": trace.get("rejections", {}),
112
+ "reason": sentence.reason,
113
+ })
114
+ finally:
115
+ _cfg_restore(previous)
116
+
117
+ return pd.DataFrame(rows)
118
+
119
+
120
+ def safety_tests():
121
+ profile = PROFILES["General Academic"]
122
+ user_terms = set()
123
+ analyzer = Analyzer()
124
+ validator = Validator()
125
+
126
+ tests = [
127
+ ("Subject/object reversal", "XGBoost outperformed DNN.", "DNN outperformed XGBoost.", "SEMANTIC_REJECTION"),
128
+ ("Negation removal", "The model did not fail.", "The model failed.", "POLARITY_REJECTION"),
129
+ ("Negation addition", "The model failed.", "The model did not fail.", "POLARITY_REJECTION"),
130
+ ("Quantity change", "The study used 26 attacks.", "The study used 25 attacks.", "QUANTITY_REJECTION"),
131
+ ("Modality strengthening", "The model may improve robustness.", "The model improves robustness.", "MODALITY_REJECTION"),
132
+ ("Enumeration omission", "The system evaluated accuracy, precision, recall, and F1 score.", "The system evaluated accuracy, precision, and recall.", "ENUMERATION_REJECTION"),
133
+ ("Protected terminology removal", "The XGBoost model was evaluated.", "The model was evaluated.", "TERMINOLOGY_REJECTION", {"XGBoost"}),
134
+ ("Citation removal", "Demetrio et al. (2021) evaluated PE-file manipulations.", "The study evaluated PE-file manipulations.", "TERMINOLOGY_REJECTION"),
135
+ ]
136
+
137
+ previous = _cfg_snapshot("sts", "nli", "lt")
138
+ CFG.sts = False
139
+ CFG.nli = False
140
+ CFG.lt = False
141
+ rows = []
142
+
143
+ try:
144
+ for test in tests:
145
+ name, original, candidate_text, expected = test[:4]
146
+ protected_terms = test[4] if len(test) > 4 else set()
147
+ analysis = analyzer.analyze(original, profile, protected_terms)
148
+ candidate = Candidate(
149
+ candidate_text,
150
+ "Balanced",
151
+ "TEST",
152
+ "adversarial safety test",
153
+ .5,
154
+ )
155
+ ok, failures, checks = validator.validate(
156
+ original,
157
+ candidate,
158
+ analysis,
159
+ profile,
160
+ protected_terms,
161
+ "Balanced",
162
+ )
163
+ rows.append({
164
+ "test": name,
165
+ "expected": expected,
166
+ "accepted": ok,
167
+ "failures": failures,
168
+ "pass": expected in failures,
169
+ "rejection_reasons": checks.get("rejection_reasons", []),
170
+ })
171
+ finally:
172
+ _cfg_restore(previous)
173
+
174
+ return pd.DataFrame(rows)
175
+
176
+
177
+ # Validator-specific regression cases introduced by the v4.1/v3.4/v5.1 stack.
178
+ def relation_regression():
179
+ profile = PROFILES["General Academic"]
180
+ analyzer = Analyzer()
181
+ validator = Validator()
182
+ previous = _cfg_snapshot("sts", "nli", "lt")
183
+ CFG.sts = False
184
+ CFG.nli = False
185
+ CFG.lt = False
186
+
187
+ tests = [
188
+ (
189
+ "without fronting",
190
+ "Systems train separate models without explicit rules for assessment, correction, and graduation.",
191
+ "Without explicit rules for assessment, correction, and graduation, systems train separate models.",
192
+ "FINITE_COMPLEMENT_LIST",
193
+ "POLARITY_REJECTION",
194
+ False,
195
+ ),
196
+ (
197
+ "enumeration preservation",
198
+ "We propose Framework X, a protocol consisting of contracts, examinations, and promotion.",
199
+ "Framework X is a protocol consisting of contracts, examinations, and promotion proposed by us.",
200
+ "APPOSITIVE_DEFINITION",
201
+ "ENUMERATION_REJECTION",
202
+ False,
203
+ ),
204
+ (
205
+ "malformed participial fragment",
206
+ "The testbed applies the protocol to learners spanning classification, drafting, and planning.",
207
+ "Protocol to learners spanning classification, drafting, and planning.",
208
+ "PARTICIPIAL_POSTMODIFIER",
209
+ "GRAMMAR_REJECTION",
210
+ True,
211
+ ),
212
+ (
213
+ "valid finite relative clause",
214
+ "The testbed applies the protocol to learners spanning classification, drafting, and planning.",
215
+ "The testbed applies the protocol to learners that span classification, drafting, and planning.",
216
+ "PARTICIPIAL_POSTMODIFIER",
217
+ "GRAMMAR_REJECTION",
218
+ False,
219
+ ),
220
+ ]
221
+
222
+ rows = []
223
+ try:
224
+ for name, original, revised, family, code, should_contain in tests:
225
+ analysis = analyzer.analyze(original, profile, set())
226
+ candidate = Candidate(revised, "Balanced", family, "relation regression", .9)
227
+ ok, failures, checks = validator.validate(
228
+ original, candidate, analysis, profile, set(), "Balanced"
229
+ )
230
+ present = code in failures
231
+ rows.append({
232
+ "test": name,
233
+ "code": code,
234
+ "expected_code_present": should_contain,
235
+ "observed_code_present": present,
236
+ "pass": present == should_contain,
237
+ "accepted": ok,
238
+ "failures": failures,
239
+ "realization_issues": checks.get("realization_issues", []),
240
+ })
241
+ finally:
242
+ _cfg_restore(previous)
243
+
244
+ return pd.DataFrame(rows)
245
+
246
+
247
+ # Run small load-time invariants but do not auto-run the full regression corpus.
248
+ if len(RULES) != EXPECTED_CELL8_RULE_COUNT:
249
+ raise RuntimeError("Cell 8 rule-count invariant failed.")
250
+
251
+ print("=" * 78)
252
+ print("CELL 8 REGRESSION / ADVERSARIAL SAFETY v4 LOADED")
253
+ print("=" * 78)
254
+ print("Aligned pipeline: 5 v4.1 -> 5A v3.4 -> 6 v5.1 -> 7 v4 -> 7A v2 -> 7B v2")
255
+ print("Registered transformation rules:", len(RULES))
256
+ print("Expanded historical regression: ready")
257
+ print("8-case adversarial safety suite: ready")
258
+ print("Relation/realization regression: ready")
259
+ print("Run: expanded_regression()")
260
+ print("Run: safety_tests()")
261
+ print("Run: relation_regression()")
262
+ print("=" * 78)
runtime/cell8a.py ADDED
@@ -0,0 +1,393 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #@title 8A. COMPREHENSIVE MULTIDOMAIN REGRESSION + SAFETY v2
2
+ # Complete replacement for Cell 8A.
3
+ # Run after Cell 8 v4 and before Cell 9.
4
+ # Compatible with:
5
+ # Cell 5 v4.1 -> Cell 5A v3.4 -> Cell 6 v5.1 -> Cell 7 v4
6
+ # -> Cell 7A v2 -> Cell 7B v2 -> Cell 8 v4
7
+
8
+ import json
9
+ import re
10
+ from collections import Counter
11
+
12
+ import pandas as pd
13
+
14
+ EXPECTED_8A_RULE_COUNT = 30
15
+
16
+ _REQUIRED_8A = [
17
+ "ENGINE", "Engine", "Analyzer", "Generator", "Validator", "Ranker",
18
+ "PROFILES", "RULES", "CFG", "clean", "surface_key",
19
+ "realization_issues", "expanded_regression", "safety_tests",
20
+ "relation_regression",
21
+ ]
22
+ _missing_8a = [name for name in _REQUIRED_8A if name not in globals()]
23
+ if _missing_8a:
24
+ raise RuntimeError(
25
+ "Cell 8A cannot run. Missing: " + ", ".join(_missing_8a)
26
+ + ". Run Cells 1–8 v4 in order first."
27
+ )
28
+ if len(RULES) != EXPECTED_8A_RULE_COUNT:
29
+ raise RuntimeError(
30
+ f"Cell 8A expected {EXPECTED_8A_RULE_COUNT} rules but found {len(RULES)}."
31
+ )
32
+ if getattr(Engine.one, "__name__", "") != "_one_with_must_paraphrase_v2":
33
+ raise RuntimeError("Cell 8A requires Cell 7B v2.")
34
+
35
+
36
+ # =====================================================================
37
+ # 1. MULTIDOMAIN CORPUS
38
+ # =====================================================================
39
+
40
+ REGRESSION_CORPUS = [
41
+ # Academic
42
+ {"id":"ACA-01","domain":"Academic","mode":"Balanced","text":"The next step is to examine how representation quality affects classifier robustness.","protected":"classifier robustness","expected_any":{"COPULAR_ACTION_FRAME"}},
43
+ {"id":"ACA-02","domain":"Academic","mode":"Balanced","text":"These findings provide evidence that adversarial training improves robustness under the evaluated attack settings.","protected":"adversarial training, robustness","expected_any":{"EVIDENTIAL_COMPLEMENT"}},
44
+ {"id":"ACA-03","domain":"Academic","mode":"Balanced","text":"The study requires consideration of whether the observed effect generalizes across datasets.","protected":"datasets","expected_any":{"WHETHER_COMPLEMENT"}},
45
+ {"id":"ACA-04","domain":"Academic","mode":"Balanced","text":"Applying the same evaluation protocol across both platforms reduces methodological confounding.","protected":"evaluation protocol","expected_any":{"GERUND_SUBJECT"}},
46
+ {"id":"ACA-05","domain":"Academic","mode":"Structural","text":"The framework preserves platform-specific constraints while applying a common evaluation procedure.","protected":"platform-specific constraints","expected_any":{"WHILE_PARTICIPIAL"}},
47
+ {"id":"ACA-06","domain":"Academic","mode":"Balanced","text":"The purpose is not to identify a universally superior model but to compare robustness across feature spaces.","protected":"robustness, feature spaces","expected_any":{"NEGATED_PURPOSE_CONTRAST"}},
48
+
49
+ # Technical
50
+ {"id":"TEC-01","domain":"Technical","mode":"Balanced","text":"The next step is to inspect how the deployed handler checks for GPT configuration.","protected":"GPT configuration","expected_any":{"COPULAR_ACTION_FRAME"}},
51
+ {"id":"TEC-02","domain":"Technical","mode":"Balanced","text":"We need to distinguish those possibilities using the existing source.","protected":"existing source","expected_any":{"NEED_TO_ACTION","USING_METHOD_RELATION","METHOD_RELATION"}},
52
+ {"id":"TEC-03","domain":"Technical","mode":"Balanced","text":"The fault could be in the environment-variable lookup, the initialization routine, or the error-reporting logic.","protected":"environment-variable lookup, initialization routine, error-reporting logic","expected_any":{"MODAL_COPULAR_ENUMERATION","ENUMERATION"}},
53
+ {"id":"TEC-04","domain":"Technical","mode":"Balanced","text":"The service is evaluated using latency, throughput, and failure-rate measurements.","protected":"latency, throughput, failure-rate","expected_any":{"PASSIVE_USING","USING_METHOD_RELATION"}},
54
+ {"id":"TEC-05","domain":"Technical","mode":"Structural","text":"The deployment was tested by the engineering team.","protected":"deployment, engineering team","expected_any":{"PASSIVE"}},
55
+ {"id":"TEC-06","domain":"Technical","mode":"Balanced","text":"The process reduces configuration errors by validating required environment variables before startup.","protected":"environment variables","expected_any":{"BY_GERUND_METHOD_RELATION","METHOD_RELATION"}},
56
+
57
+ # Business
58
+ {"id":"BUS-01","domain":"Business","mode":"Balanced","text":"The next step is to review the supplier's revised implementation schedule.","protected":"implementation schedule","expected_any":{"COPULAR_ACTION_FRAME"}},
59
+ {"id":"BUS-02","domain":"Business","mode":"Balanced","text":"The delay could be in procurement, vendor mobilization, or material delivery.","protected":"procurement, vendor mobilization, material delivery","expected_any":{"MODAL_COPULAR_ENUMERATION","ENUMERATION"}},
60
+ {"id":"BUS-03","domain":"Business","mode":"Balanced","text":"We need to verify the invoice totals using the approved cost ledger.","protected":"invoice totals, approved cost ledger","expected_any":{"NEED_TO_ACTION","USING_METHOD_RELATION","METHOD_RELATION"}},
61
+ {"id":"BUS-04","domain":"Business","mode":"Balanced","text":"The team intends to submit the report this week rather than waiting until month-end.","protected":"report, month-end","expected_any":{"FINITE_PURPOSE_INTENT","RATHER_THAN_GERUND"}},
62
+ {"id":"BUS-05","domain":"Business","mode":"Structural","text":"The manager reviewed the estimate and approved the revised budget.","protected":"estimate, revised budget","expected_any":{"COORDINATED_PREDICATES"}},
63
+ {"id":"BUS-06","domain":"Business","mode":"Structural","text":"The schedule identifies mobilization, procurement, installation, testing, and closeout; each stage has a separate milestone.","protected":"mobilization, procurement, installation, testing, closeout","expected_any":{"LONG_MULTI_CLAUSE","ENUMERATION"}},
64
+
65
+ # Contract / formal requirements
66
+ {"id":"CON-01","domain":"Contract","mode":"Balanced","text":"For purposes of this plan, a Sub-RFR is an individual reimbursement request for eligible paid costs.","protected":"Sub-RFR, eligible paid costs","expected_any":{"FRONTED_ADJUNCT"},"must_remain_unchanged":True},
67
+ {"id":"CON-02","domain":"Contract","mode":"Balanced","text":"To support reimbursement processing, the Contractor shall provide invoices, inspection reports, photographs, lien releases, and other supporting documentation.","protected":"Contractor, invoices, inspection reports, photographs, lien releases","expected_any":{"PURPOSE_INFINITIVE_FRONTING","ENUMERATION"}},
68
+ {"id":"CON-03","domain":"Contract","mode":"Balanced","text":"Eligible invoices must be properly documented, reviewed, and approved by WSC.","protected":"Eligible invoices, WSC","expected_any":{"COORDINATED_PASSIVE_PARTICIPLES","COORDINATED_PREDICATES"}},
69
+ {"id":"CON-04","domain":"Contract","mode":"Balanced","text":"WSC intends to submit reimbursement requests progressively rather than waiting until project completion.","protected":"WSC, reimbursement requests, project completion","expected_any":{"FINITE_PURPOSE_INTENT","RATHER_THAN_GERUND"}},
70
+ {"id":"CON-05","domain":"Contract","mode":"Structural","text":"The work was inspected by the Owner's representative.","protected":"Owner's representative","expected_any":{"PASSIVE"}},
71
+ {"id":"CON-06","domain":"Contract","mode":"Balanced","text":"Payments are based on approved work rather than on the passage of time.","protected":"approved work, passage of time","expected_any":{"PASSIVE","CONTRAST"}},
72
+
73
+ # General / out-of-sample primary-clause structures
74
+ {"id":"GEN-01","domain":"General","mode":"Balanced","text":"General-purpose machine-learning systems often train separate models without explicit rules for competency assessment, evidence-grounded correction, and graduation.","protected":"competency assessment, evidence-grounded correction, graduation","expected_any":{"FINITE_COMPLEMENT_LIST"}},
75
+ {"id":"GEN-02","domain":"General","mode":"Balanced","text":"We propose Framework X (FX), a domain-independent orchestration protocol consisting of contracts, examinations, peer study, and auditable promotion.","protected":"Framework X, FX, contracts, examinations, peer study, auditable promotion","expected_any":{"PROPOSAL_FRAME","APPOSITIVE_DEFINITION"}},
76
+ {"id":"GEN-03","domain":"General","mode":"Balanced","text":"The experimental testbed applies this protocol to small cybersecurity learners spanning classification, generative drafting, and structured diagram planning.","protected":"small cybersecurity learners, classification, generative drafting, structured diagram planning","expected_any":{"PARTICIPIAL_POSTMODIFIER"}},
77
+ {"id":"GEN-04","domain":"General","mode":"Balanced","text":"We pre-register comparable questions, disjoint data, matched baselines, ablations, and independently held-out testing.","protected":"comparable questions, disjoint data, matched baselines, ablations, independently held-out testing","expected_any":{"FINITE_OBJECT_LIST_FRAME"}},
78
+ {"id":"GEN-05","domain":"General","mode":"Balanced","text":"Research claims about improvement, cost, generality, and deployment are contingent on execution and empirical analysis.","protected":"improvement, cost, generality, deployment, execution, empirical analysis","expected_any":{"CONTINGENCY_RELATION"}},
79
+ ]
80
+
81
+
82
+ # =====================================================================
83
+ # 2. HELPERS
84
+ # =====================================================================
85
+
86
+ def _terms(text):
87
+ return {part.strip() for part in re.split(r"[,;\n]", text or "") if part.strip()}
88
+
89
+
90
+ def _contains_term(text, term):
91
+ return re.search(re.escape(term), text, re.I) is not None
92
+
93
+
94
+ def _protected_missing(source, revised, protected_text):
95
+ return [
96
+ term for term in _terms(protected_text)
97
+ if _contains_term(source, term) and not _contains_term(revised, term)
98
+ ]
99
+
100
+
101
+ def _candidate_trace_count(trace):
102
+ return len(trace.get("candidate_traces", []) or [])
103
+
104
+
105
+ def _realization_issues_safe(text):
106
+ try:
107
+ return list(realization_issues(text) or [])
108
+ except Exception as exc:
109
+ return [f"REALIZATION_CHECK_ERROR:{exc}"]
110
+
111
+
112
+ def _process_case(case):
113
+ profile, routing = ENGINE.route(case["text"], "Auto Detect")
114
+ result = ENGINE.one(
115
+ case["text"],
116
+ profile,
117
+ _terms(case.get("protected", "")),
118
+ case.get("mode", "Balanced"),
119
+ )
120
+
121
+ trace = dict(getattr(result, "diagnostics", {}) or {})
122
+ must = trace.get("must_paraphrase", {}) or {}
123
+ families = set(getattr(result, "families", []) or [])
124
+ revised = getattr(result, "revised", case["text"])
125
+ status = getattr(result, "status", "UNKNOWN")
126
+ changed = surface_key(revised) != surface_key(case["text"])
127
+ missing = _protected_missing(case["text"], revised, case.get("protected", ""))
128
+ realization = _realization_issues_safe(revised)
129
+ expected = set(case.get("expected_any", set()))
130
+ family_hit = not expected or bool(families & expected)
131
+ must_remain = bool(case.get("must_remain_unchanged", False))
132
+
133
+ terminal_ok = status in {
134
+ "SUCCESS", "NO_PATTERN_COVERAGE", "CANDIDATE_GENERATION_FAILURE",
135
+ "TERMINOLOGY_REJECTION", "POLARITY_REJECTION", "MODALITY_REJECTION",
136
+ "QUANTITY_REJECTION", "ENUMERATION_REJECTION", "GRAMMAR_REJECTION",
137
+ "SEMANTIC_REJECTION", "INSUFFICIENT_REWRITE", "RANKING_EMPTY",
138
+ "PARSE_FAILURE",
139
+ }
140
+
141
+ # Must-paraphrase contract:
142
+ # - successful results must be changed;
143
+ # - unchanged results must document exhaustive safe search;
144
+ mp_outcome = must.get("outcome")
145
+ mp_contract = (
146
+ (status == "SUCCESS" and changed and mp_outcome == "SAFE_CHANGED_OUTPUT")
147
+ or (
148
+ status != "SUCCESS"
149
+ and not changed
150
+ and mp_outcome == "ORIGINAL_PRESERVED_AFTER_EXHAUSTIVE_SAFE_SEARCH"
151
+ )
152
+ )
153
+
154
+ pass_core = (
155
+ terminal_ok
156
+ and not missing
157
+ and not realization
158
+ and (not must_remain or not changed)
159
+ and (family_hit or status == "NO_PATTERN_COVERAGE")
160
+ and mp_contract
161
+ )
162
+
163
+ return {
164
+ "ID": case["id"],
165
+ "Domain": case["domain"],
166
+ "Mode": case.get("mode", "Balanced"),
167
+ "Status": status,
168
+ "Changed": changed,
169
+ "Detected families": " | ".join(sorted(families)),
170
+ "Expected family hit": family_hit,
171
+ "Selected family": getattr(result, "family", None),
172
+ "Requested mode": must.get("requested_mode"),
173
+ "Accepted mode": must.get("accepted_mode"),
174
+ "Attempt count": must.get("attempt_count"),
175
+ "Must-paraphrase outcome": mp_outcome,
176
+ "Must-paraphrase contract": mp_contract,
177
+ "Generated Minimal": (trace.get("generated_by_lane") or {}).get("Minimal", 0),
178
+ "Generated Balanced": (trace.get("generated_by_lane") or {}).get("Balanced", 0),
179
+ "Generated Structural": (trace.get("generated_by_lane") or {}).get("Structural", 0),
180
+ "Before final filter": trace.get("generated_before_final_filter"),
181
+ "After final filter": trace.get("generated_after_final_filter"),
182
+ "Candidate traces": _candidate_trace_count(trace),
183
+ "Missing protected": missing,
184
+ "Realization issues": realization,
185
+ "Must remain unchanged": must_remain,
186
+ "Core pass": pass_core,
187
+ "Original": case["text"],
188
+ "Revised": revised,
189
+ "Reason": getattr(result, "reason", ""),
190
+ "Rule errors": trace.get("rule_errors", []),
191
+ "Rejections": trace.get("rejections", {}),
192
+ "Routing evidence": routing,
193
+ }
194
+
195
+
196
+ # =====================================================================
197
+ # 3. SAFETY SUITE
198
+ # =====================================================================
199
+
200
+ SAFETY_CASES = [
201
+ {"id":"SAFE-01","name":"Subject/object reversal","source":"XGBoost outperformed DNN.","candidate":"DNN outperformed XGBoost.","expected":"SEMANTIC_REJECTION"},
202
+ {"id":"SAFE-02","name":"Negation removal","source":"The model did not fail.","candidate":"The model failed.","expected":"POLARITY_REJECTION"},
203
+ {"id":"SAFE-03","name":"Negation addition","source":"The model failed.","candidate":"The model did not fail.","expected":"POLARITY_REJECTION"},
204
+ {"id":"SAFE-04","name":"Quantity change","source":"The study evaluated 26 attacks.","candidate":"The study evaluated 25 attacks.","expected":"QUANTITY_REJECTION"},
205
+ {"id":"SAFE-05","name":"Modality strengthening","source":"The model may improve robustness.","candidate":"The model improves robustness.","expected":"MODALITY_REJECTION"},
206
+ {"id":"SAFE-06","name":"Enumeration omission","source":"The system evaluated accuracy, precision, recall, and F1 score.","candidate":"The system evaluated accuracy, precision, and recall.","expected":"ENUMERATION_REJECTION"},
207
+ {"id":"SAFE-07","name":"Protected terminology removal","source":"The XGBoost model was evaluated.","candidate":"The model was evaluated.","expected":"TERMINOLOGY_REJECTION","protected":{"XGBoost"}},
208
+ {"id":"SAFE-08","name":"Citation removal","source":"Demetrio et al. (2021) evaluated PE-file manipulations.","candidate":"The study evaluated PE-file manipulations.","expected":"TERMINOLOGY_REJECTION"},
209
+ ]
210
+
211
+
212
+ def _safety_case(case):
213
+ profile = PROFILES.get("General Academic", next(iter(PROFILES.values())))
214
+ protected = set(case.get("protected", set()))
215
+ analysis = Analyzer().analyze(case["source"], profile, protected)
216
+ candidate = Candidate(
217
+ case["candidate"], "Balanced", "SAFETY_TEST", "Adversarial safety test", .5
218
+ )
219
+ accepted, failures, checks = Validator().validate(
220
+ case["source"], candidate, analysis, profile, protected, "Balanced"
221
+ )
222
+ failures = list(failures or [])
223
+ return {
224
+ "ID": case["id"],
225
+ "Test": case["name"],
226
+ "Expected": case["expected"],
227
+ "Accepted": bool(accepted),
228
+ "Failures": failures,
229
+ "Pass": case["expected"] in failures,
230
+ "Checks": checks,
231
+ }
232
+
233
+
234
+ # =====================================================================
235
+ # 4. RUNNER
236
+ # =====================================================================
237
+
238
+
239
+ def run_comprehensive_regression(show_all=True):
240
+ # Keep optional model resources off so results test the deterministic stack.
241
+ previous = {
242
+ key: getattr(CFG, key, None)
243
+ for key in ["sts", "nli", "mlm", "lt", "online"]
244
+ if hasattr(CFG, key)
245
+ }
246
+ for key in previous:
247
+ setattr(CFG, key, False)
248
+
249
+ rows = []
250
+ crashes = []
251
+ try:
252
+ for case in REGRESSION_CORPUS:
253
+ try:
254
+ rows.append(_process_case(case))
255
+ except Exception as exc:
256
+ crashes.append({"ID":case["id"],"Domain":case["domain"],"Error":repr(exc)})
257
+ rows.append({
258
+ "ID":case["id"],"Domain":case["domain"],"Mode":case.get("mode","Balanced"),
259
+ "Status":"TEST_CRASH","Changed":False,"Detected families":"",
260
+ "Expected family hit":False,"Selected family":None,
261
+ "Requested mode":None,"Accepted mode":None,"Attempt count":None,
262
+ "Must-paraphrase outcome":None,"Must-paraphrase contract":False,
263
+ "Generated Minimal":0,"Generated Balanced":0,"Generated Structural":0,
264
+ "Before final filter":0,"After final filter":0,"Candidate traces":0,
265
+ "Missing protected":[],"Realization issues":[repr(exc)],
266
+ "Must remain unchanged":False,"Core pass":False,
267
+ "Original":case["text"],"Revised":case["text"],
268
+ "Reason":"Regression test crashed.","Rule errors":[],"Rejections":{},
269
+ "Routing evidence":[],
270
+ })
271
+
272
+ safety_rows = []
273
+ for case in SAFETY_CASES:
274
+ try:
275
+ safety_rows.append(_safety_case(case))
276
+ except Exception as exc:
277
+ safety_rows.append({
278
+ "ID":case["id"],"Test":case["name"],"Expected":case["expected"],
279
+ "Accepted":None,"Failures":[repr(exc)],"Pass":False,"Checks":{},
280
+ })
281
+ finally:
282
+ for key, value in previous.items():
283
+ setattr(CFG, key, value)
284
+
285
+ regression_df = pd.DataFrame(rows)
286
+ safety_df = pd.DataFrame(safety_rows)
287
+
288
+ domain_summary = regression_df.groupby("Domain", dropna=False).agg(
289
+ Sentences=("ID","count"),
290
+ Successes=("Status",lambda values:int((values=="SUCCESS").sum())),
291
+ Changed=("Changed","sum"),
292
+ Core_passes=("Core pass","sum"),
293
+ Family_hits=("Expected family hit","sum"),
294
+ MP_contracts=("Must-paraphrase contract","sum"),
295
+ Crashes=("Status",lambda values:int((values=="TEST_CRASH").sum())),
296
+ ).reset_index()
297
+
298
+ status_summary = (
299
+ regression_df.groupby(["Domain","Status"], dropna=False)
300
+ .size().reset_index(name="Count")
301
+ )
302
+
303
+ quality_failures = regression_df[
304
+ (~regression_df["Core pass"])
305
+ | regression_df["Realization issues"].map(bool)
306
+ | regression_df["Missing protected"].map(bool)
307
+ | regression_df["Rule errors"].map(bool)
308
+ ].copy()
309
+
310
+ changed_without_trace = regression_df[
311
+ regression_df["Changed"] & (regression_df["Candidate traces"] == 0)
312
+ ].copy()
313
+
314
+ summary = {
315
+ "regression_sentences": int(len(regression_df)),
316
+ "domains": int(regression_df["Domain"].nunique()),
317
+ "successes": int((regression_df["Status"] == "SUCCESS").sum()),
318
+ "changed": int(regression_df["Changed"].sum()),
319
+ "core_passes": int(regression_df["Core pass"].sum()),
320
+ "family_hits": int(regression_df["Expected family hit"].sum()),
321
+ "must_paraphrase_contracts": int(regression_df["Must-paraphrase contract"].sum()),
322
+ "crashes": int((regression_df["Status"] == "TEST_CRASH").sum()),
323
+ "quality_failures": int(len(quality_failures)),
324
+ "changed_without_trace": int(len(changed_without_trace)),
325
+ "safety_tests": int(len(safety_df)),
326
+ "safety_passes": int(safety_df["Pass"].sum()),
327
+ "registered_rules": int(len(RULES)),
328
+ }
329
+
330
+ print("=" * 88)
331
+ print("COMPREHENSIVE MULTIDOMAIN REGRESSION SUMMARY v2")
332
+ print("=" * 88)
333
+ print(json.dumps(summary, indent=2))
334
+ print("=" * 88)
335
+
336
+ try:
337
+ display(domain_summary)
338
+ display(status_summary)
339
+ display(safety_df[["ID","Test","Expected","Accepted","Failures","Pass"]])
340
+ if show_all:
341
+ display(regression_df)
342
+ if len(quality_failures):
343
+ print("QUALITY FAILURES")
344
+ display(quality_failures)
345
+ if len(changed_without_trace):
346
+ print("CHANGED OUTPUTS WITHOUT CANDIDATE TRACE")
347
+ display(changed_without_trace)
348
+ except NameError:
349
+ print(domain_summary.to_string(index=False))
350
+ print(status_summary.to_string(index=False))
351
+
352
+ return {
353
+ "summary":summary,
354
+ "regression":regression_df,
355
+ "domain_summary":domain_summary,
356
+ "status_summary":status_summary,
357
+ "safety":safety_df,
358
+ "quality_failures":quality_failures,
359
+ "changed_without_trace":changed_without_trace,
360
+ "crashes":crashes,
361
+ }
362
+
363
+
364
+ # =====================================================================
365
+ # 5. EXPORT
366
+ # =====================================================================
367
+
368
+
369
+ def export_comprehensive_regression(results, filename="nll_comprehensive_regression_v2.xlsx"):
370
+ path = f"/content/{filename}"
371
+ with pd.ExcelWriter(path, engine="openpyxl") as writer:
372
+ results["regression"].to_excel(writer, sheet_name="Regression", index=False)
373
+ results["domain_summary"].to_excel(writer, sheet_name="Domain Summary", index=False)
374
+ results["status_summary"].to_excel(writer, sheet_name="Status Summary", index=False)
375
+ results["safety"].to_excel(writer, sheet_name="Safety", index=False)
376
+ results["quality_failures"].to_excel(writer, sheet_name="Quality Failures", index=False)
377
+ print("Saved:", path)
378
+ return path
379
+
380
+
381
+ print("=" * 88)
382
+ print("CELL 8A COMPREHENSIVE MULTIDOMAIN REGRESSION SUITE v2 LOADED")
383
+ print("=" * 88)
384
+ print("Aligned pipeline: 5 v4.1 -> 5A v3.4 -> 6 v5.1 -> 7 v4 -> 7A v2 -> 7B v2 -> 8 v4")
385
+ print("Regression sentences:", len(REGRESSION_CORPUS))
386
+ print("Domains:", sorted({case['domain'] for case in REGRESSION_CORPUS}))
387
+ print("Adversarial safety tests:", len(SAFETY_CASES))
388
+ print("Registered transformation rules:", len(RULES))
389
+ print("Must-paraphrase contract checks: active")
390
+ print("Out-of-sample primary-clause cases: active")
391
+ print("Run: RESULTS_8A = run_comprehensive_regression()")
392
+ print("Optional export: export_comprehensive_regression(RESULTS_8A)")
393
+ print("=" * 88)
runtime/cell9.py ADDED
@@ -0,0 +1,739 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #@title 9. PROFESSIONAL GRADIO UI v5.1 — FULL 30-RULE DIAGNOSTICS
2
+ # Complete replacement for Cell 9.
3
+ # Compatible with Cell 5 v4.1, Cell 5A v3.4, Cell 6 v5.1,
4
+ # Cell 7 v4, Cell 7A v2, Cell 7B v2, Cell 8 v4, and Cell 8A v2.
5
+
6
+ import json
7
+ import html
8
+ import re
9
+ import tempfile
10
+ import time
11
+ import uuid
12
+ from collections import Counter
13
+ from pathlib import Path
14
+ from dataclasses import asdict, is_dataclass
15
+
16
+ import pandas as pd
17
+ import gradio as gr
18
+
19
+
20
+ # =====================================================================
21
+ # 1. PREFLIGHT
22
+ # =====================================================================
23
+
24
+ EXPECTED_RULE_COUNT = 30
25
+
26
+ _REQUIRED_CELL9 = [
27
+ "ENGINE", "Engine", "Analyzer", "Generator", "Validator", "Ranker",
28
+ "RULES", "PROFILES", "realization_issues",
29
+ ]
30
+
31
+ _missing_cell9 = [name for name in _REQUIRED_CELL9 if name not in globals()]
32
+ if _missing_cell9:
33
+ raise RuntimeError(
34
+ "Cell 9 cannot run. Missing: " + ", ".join(_missing_cell9)
35
+ + ". Run Cells 1–8A in order first."
36
+ )
37
+
38
+ if len(RULES) != EXPECTED_RULE_COUNT:
39
+ raise RuntimeError(
40
+ f"Cell 9 expected {EXPECTED_RULE_COUNT} registered transformation "
41
+ f"rules, but found {len(RULES)}. Rerun Cells 5, 5A, 6, 7, 7A, "
42
+ "and 7B in order."
43
+ )
44
+
45
+ MUST_PARAPHRASE_ACTIVE = (
46
+ getattr(Engine.one, "__name__", "") == "_one_with_must_paraphrase_v2"
47
+ )
48
+
49
+ if not MUST_PARAPHRASE_ACTIVE:
50
+ raise RuntimeError(
51
+ "Cell 9 requires the Cell 7B v2 must-paraphrase orchestrator. "
52
+ "Run Cell 7B v2 before Cell 9."
53
+ )
54
+
55
+
56
+ # =====================================================================
57
+ # 2. DETERMINISTIC SETTINGS
58
+ # =====================================================================
59
+
60
+ def _set_cfg_flag(name, value):
61
+ if "CFG" in globals():
62
+ try:
63
+ setattr(CFG, name, bool(value))
64
+ except Exception:
65
+ pass
66
+
67
+
68
+ def _apply_ui_settings(use_stanza, use_language_tool):
69
+ # No generative/model-assisted candidate production in this UI.
70
+ _set_cfg_flag("mlm", False)
71
+ _set_cfg_flag("sts", False)
72
+ _set_cfg_flag("nli", False)
73
+ _set_cfg_flag("online", False)
74
+
75
+ # Optional read-only analysis resources.
76
+ _set_cfg_flag("stanza", use_stanza)
77
+ _set_cfg_flag("lt", use_language_tool)
78
+
79
+
80
+ # =====================================================================
81
+ # 3. GENERAL HELPERS
82
+ # =====================================================================
83
+
84
+ def _json_safe(value):
85
+ if value is None or isinstance(value, (str, int, float, bool)):
86
+ return value
87
+ if is_dataclass(value):
88
+ return _json_safe(asdict(value))
89
+ if isinstance(value, dict):
90
+ return {str(key): _json_safe(item) for key, item in value.items()}
91
+ if isinstance(value, (list, tuple, set)):
92
+ return [_json_safe(item) for item in value]
93
+ if hasattr(value, "tolist"):
94
+ try:
95
+ return _json_safe(value.tolist())
96
+ except Exception:
97
+ pass
98
+ if hasattr(value, "item"):
99
+ try:
100
+ return _json_safe(value.item())
101
+ except Exception:
102
+ pass
103
+ if hasattr(value, "__dict__"):
104
+ try:
105
+ return _json_safe(vars(value))
106
+ except Exception:
107
+ pass
108
+ return str(value)
109
+
110
+
111
+ def _file_path(uploaded_file):
112
+ if uploaded_file is None:
113
+ return None
114
+ if isinstance(uploaded_file, str):
115
+ return uploaded_file
116
+ return getattr(uploaded_file, "name", None) or str(uploaded_file)
117
+
118
+
119
+ def _read_input_text(typed_text, uploaded_file):
120
+ typed_text = str(typed_text or "")
121
+ if typed_text.strip():
122
+ return typed_text.strip(), "typed_text"
123
+
124
+ path_value = _file_path(uploaded_file)
125
+ if not path_value:
126
+ raise gr.Error("Enter text or upload a TXT, DOCX, or text-based PDF.")
127
+
128
+ path = Path(path_value)
129
+ if not path.exists():
130
+ raise gr.Error("The uploaded file could not be found.")
131
+
132
+ suffix = path.suffix.lower()
133
+ if suffix == ".txt":
134
+ return path.read_text(encoding="utf-8", errors="replace").strip(), path.name
135
+
136
+ if suffix == ".docx":
137
+ from docx import Document
138
+ document = Document(str(path))
139
+ text = "\n\n".join(p.text for p in document.paragraphs).strip()
140
+ return text, path.name
141
+
142
+ if suffix == ".pdf":
143
+ try:
144
+ import fitz
145
+ except Exception as exc:
146
+ raise gr.Error("PyMuPDF is unavailable: " + str(exc))
147
+ pdf = fitz.open(str(path))
148
+ try:
149
+ text = "\n\n".join(page.get_text("text") for page in pdf).strip()
150
+ finally:
151
+ pdf.close()
152
+ if not text:
153
+ raise gr.Error(
154
+ "The PDF contains no extractable text. Scanned PDFs require OCR."
155
+ )
156
+ return text, path.name
157
+
158
+ raise gr.Error("Unsupported file type. Use TXT, DOCX, or text-based PDF.")
159
+
160
+
161
+ def _trace(item):
162
+ value = getattr(item, "diagnostics", None)
163
+ if value is None:
164
+ value = getattr(item, "trace", None)
165
+ return dict(value or {})
166
+
167
+
168
+ def _checks(item):
169
+ return dict(getattr(item, "checks", None) or {})
170
+
171
+
172
+ def _changed(item):
173
+ return str(getattr(item, "original", "")) != str(getattr(item, "revised", ""))
174
+
175
+
176
+ # =====================================================================
177
+ # 4. DIAGNOSTIC TABLES
178
+ # =====================================================================
179
+
180
+ def _sentence_rows(result):
181
+ rows = []
182
+ for number, item in enumerate(list(getattr(result, "sentences", []) or []), 1):
183
+ trace = _trace(item)
184
+ selected = trace.get("selected") or {}
185
+ generated = trace.get("generated_by_lane", {}) or {}
186
+ must = trace.get("must_paraphrase", {}) or {}
187
+ attempts = must.get("attempts", []) or []
188
+ rows.append({
189
+ "Sentence": number,
190
+ "Status": getattr(item, "status", ""),
191
+ "Changed": _changed(item),
192
+ "Requested mode": must.get("requested_mode", getattr(result, "mode", "")),
193
+ "Accepted mode": must.get("accepted_mode", ""),
194
+ "Attempts": must.get("attempt_count", len(attempts)),
195
+ "Must-paraphrase outcome": must.get("outcome", ""),
196
+ "Detected families": " | ".join(list(getattr(item, "families", []) or [])),
197
+ "Selected family": getattr(item, "family", None) or selected.get("family") or "",
198
+ "Selected lane": selected.get("lane", ""),
199
+ "Selected score": selected.get("score"),
200
+ "Minimal candidates": generated.get("Minimal", 0),
201
+ "Balanced candidates": generated.get("Balanced", 0),
202
+ "Structural candidates": generated.get("Structural", 0),
203
+ "Before final filter": trace.get("generated_before_final_filter"),
204
+ "After final filter": trace.get("generated_after_final_filter"),
205
+ "Original": getattr(item, "original", ""),
206
+ "Revised": getattr(item, "revised", ""),
207
+ "Reason": getattr(item, "reason", ""),
208
+ })
209
+ return rows
210
+
211
+
212
+ def _routing_rows(result):
213
+ rows = []
214
+ for number, item in enumerate(list(getattr(result, "sentences", []) or []), 1):
215
+ trace = _trace(item)
216
+ applicable = trace.get("applicable", {}) or {}
217
+ generated = trace.get("generated_by_lane", {}) or {}
218
+ rows.append({
219
+ "Sentence": number,
220
+ "Status": getattr(item, "status", ""),
221
+ "Detected families": " | ".join(
222
+ trace.get("detected_families", [])
223
+ or list(getattr(item, "families", []) or [])
224
+ ),
225
+ "Minimal rules": " | ".join(applicable.get("Minimal", []) or []),
226
+ "Balanced rules": " | ".join(applicable.get("Balanced", []) or []),
227
+ "Structural rules": " | ".join(applicable.get("Structural", []) or []),
228
+ "Minimal generated": generated.get("Minimal", 0),
229
+ "Balanced generated": generated.get("Balanced", 0),
230
+ "Structural generated": generated.get("Structural", 0),
231
+ "Before final filter": trace.get("generated_before_final_filter"),
232
+ "After final filter": trace.get("generated_after_final_filter"),
233
+ "Rule errors": json.dumps(_json_safe(trace.get("rule_errors", [])), ensure_ascii=False),
234
+ "Terminal rejections": json.dumps(_json_safe(trace.get("rejections", {})), ensure_ascii=False),
235
+ })
236
+ return rows
237
+
238
+
239
+ def _realization_rows(result):
240
+ rows = []
241
+ for sentence_number, item in enumerate(list(getattr(result, "sentences", []) or []), 1):
242
+ for rejection_number, rejection in enumerate(
243
+ list(_trace(item).get("realization_rejections", []) or []), 1
244
+ ):
245
+ rows.append({
246
+ "Sentence": sentence_number,
247
+ "Rejection": rejection_number,
248
+ "Stage": rejection.get("stage", ""),
249
+ "Lane": rejection.get("lane", ""),
250
+ "Family/rule": rejection.get("rule", ""),
251
+ "Issues": " | ".join(rejection.get("issues", []) or []),
252
+ "Rejected text": rejection.get("text", ""),
253
+ })
254
+ if not rows:
255
+ rows.append({
256
+ "Sentence": 0, "Rejection": 0, "Stage": "", "Lane": "",
257
+ "Family/rule": "", "Issues": "None", "Rejected text": "",
258
+ })
259
+ return rows
260
+
261
+
262
+ def _candidate_rows(result):
263
+ rows = []
264
+ for sentence_number, item in enumerate(list(getattr(result, "sentences", []) or []), 1):
265
+ candidates = list(_trace(item).get("candidate_traces", []) or [])
266
+ if not candidates:
267
+ rows.append({
268
+ "Sentence": sentence_number,
269
+ "Candidate": 0,
270
+ "Lane": "",
271
+ "Family": "",
272
+ "Accepted": False,
273
+ "Score": None,
274
+ "Change": None,
275
+ "Failures": "",
276
+ "Rejection reasons": "No candidate trace available",
277
+ "Candidate text": "",
278
+ })
279
+ continue
280
+ for candidate_number, candidate in enumerate(candidates, 1):
281
+ rows.append({
282
+ "Sentence": sentence_number,
283
+ "Candidate": candidate_number,
284
+ "Lane": candidate.get("lane", ""),
285
+ "Family": candidate.get("family", ""),
286
+ "Accepted": candidate.get("accepted", False),
287
+ "Score": candidate.get("score"),
288
+ "Change": candidate.get("change"),
289
+ "Failures": " | ".join(candidate.get("failures", []) or []),
290
+ "Rejection reasons": json.dumps(
291
+ _json_safe(candidate.get("rejection_reasons", [])),
292
+ ensure_ascii=False,
293
+ ),
294
+ "Candidate text": candidate.get("text", ""),
295
+ })
296
+ return rows
297
+
298
+
299
+ def _attempt_rows(result):
300
+ rows = []
301
+ for sentence_number, item in enumerate(list(getattr(result, "sentences", []) or []), 1):
302
+ must = _trace(item).get("must_paraphrase", {}) or {}
303
+ attempts = must.get("attempts", []) or []
304
+ if not attempts:
305
+ rows.append({
306
+ "Sentence": sentence_number, "Attempt": 0, "Mode": "",
307
+ "Status": "", "Changed": False, "Selected family": "",
308
+ "Failures": "", "Realization issues": "",
309
+ "Generated after filter": None, "Revised": "",
310
+ })
311
+ continue
312
+ for attempt_number, attempt in enumerate(attempts, 1):
313
+ rows.append({
314
+ "Sentence": sentence_number,
315
+ "Attempt": attempt_number,
316
+ "Mode": attempt.get("attempted_mode", ""),
317
+ "Status": attempt.get("status", ""),
318
+ "Changed": attempt.get("changed", False),
319
+ "Selected family": attempt.get("selected_family", ""),
320
+ "Failures": " | ".join(attempt.get("failures", []) or []),
321
+ "Realization issues": " | ".join(attempt.get("realization_issues", []) or []),
322
+ "Generated after filter": attempt.get("generated_after_final_filter"),
323
+ "Revised": attempt.get("revised", ""),
324
+ })
325
+ return rows
326
+
327
+
328
+ def _finding_rows(result):
329
+ rows = []
330
+ for finding in list(getattr(result, "findings", []) or []):
331
+ safe = _json_safe(finding)
332
+ rows.append(safe if isinstance(safe, dict) else {"Finding": safe})
333
+ return rows
334
+
335
+
336
+ # =====================================================================
337
+ # 5. SUMMARY AND FULL PAYLOAD
338
+ # =====================================================================
339
+
340
+ def _summary_markdown(result):
341
+ sentences = list(getattr(result, "sentences", []) or [])
342
+ total = len(sentences)
343
+ successes = sum(getattr(item, "status", "") == "SUCCESS" for item in sentences)
344
+ changed = sum(_changed(item) for item in sentences)
345
+ statuses = Counter(getattr(item, "status", "UNKNOWN") for item in sentences)
346
+ attempts = sum(
347
+ int((_trace(item).get("must_paraphrase", {}) or {}).get("attempt_count", 0) or 0)
348
+ for item in sentences
349
+ )
350
+ preserved = sum(
351
+ (_trace(item).get("must_paraphrase", {}) or {}).get("outcome")
352
+ == "ORIGINAL_PRESERVED_AFTER_EXHAUSTIVE_SAFE_SEARCH"
353
+ for item in sentences
354
+ )
355
+ status_text = ", ".join(f"{name}: {count}" for name, count in sorted(statuses.items()))
356
+ warnings = list(getattr(result, "warnings", []) or [])
357
+ warning_text = "<br>".join(html.escape(str(x)) for x in warnings) if warnings else "None"
358
+
359
+ return f"""
360
+ ### Processing summary
361
+
362
+ - **Discipline:** {getattr(result, 'discipline', '')}
363
+ - **Routing evidence:** {', '.join(map(str, getattr(result, 'evidence', []) or [])) or 'default profile'}
364
+ - **Writing Pattern Score:** {getattr(result, 'pattern_score', '')} — **{getattr(result, 'band', '')}**
365
+ - **Registered transformation rules:** {len(RULES)}
366
+ - **Must-paraphrase orchestrator:** {'Active' if MUST_PARAPHRASE_ACTIVE else 'Inactive'}
367
+ - **Sentences processed:** {total}
368
+ - **Successful selections:** {successes}
369
+ - **Changed sentences:** {changed}
370
+ - **Unchanged sentences:** {total - changed}
371
+ - **Total bounded attempts:** {attempts}
372
+ - **Preserved after exhaustive safe search:** {preserved}
373
+ - **Terminal statuses:** {status_text or 'None'}
374
+ - **Warnings:** {warning_text}
375
+
376
+ > The Writing Pattern Score describes observable structural and stylistic characteristics only. It does not identify authorship.
377
+ """
378
+
379
+
380
+ def _full_payload(result, source_name):
381
+ sentences = []
382
+ for number, item in enumerate(list(getattr(result, "sentences", []) or []), 1):
383
+ sentences.append({
384
+ "sentence": number,
385
+ "status": getattr(item, "status", ""),
386
+ "original": getattr(item, "original", ""),
387
+ "revised": getattr(item, "revised", ""),
388
+ "families": _json_safe(getattr(item, "families", [])),
389
+ "selected_family": getattr(item, "family", None),
390
+ "reason": getattr(item, "reason", ""),
391
+ "checks": _json_safe(_checks(item)),
392
+ "trace": _json_safe(_trace(item)),
393
+ })
394
+
395
+ return {
396
+ "source_name": source_name,
397
+ "workflow": getattr(result, "workflow", ""),
398
+ "mode": getattr(result, "mode", ""),
399
+ "discipline": getattr(result, "discipline", ""),
400
+ "routing_evidence": _json_safe(getattr(result, "evidence", [])),
401
+ "writing_pattern_score": getattr(result, "pattern_score", None),
402
+ "writing_pattern_band": getattr(result, "band", ""),
403
+ "warnings": _json_safe(getattr(result, "warnings", [])),
404
+ "stats": _json_safe(getattr(result, "stats", {})),
405
+ "registered_rule_count": len(RULES),
406
+ "must_paraphrase_active": MUST_PARAPHRASE_ACTIVE,
407
+ "must_paraphrase_wrapper": getattr(Engine.one, "__name__", ""),
408
+ "rewrite_configuration": {
409
+ "deterministic_rules": True,
410
+ "masked_language_model": False,
411
+ "sts_model": False,
412
+ "nli_model": False,
413
+ "online_lexicon": False,
414
+ "language_tool_optional": bool(getattr(globals().get("CFG", object()), "lt", False)),
415
+ },
416
+ "registered_rules": [
417
+ {
418
+ "name": registered.name,
419
+ "lanes": sorted(registered.lanes),
420
+ "families": sorted(registered.families),
421
+ "priority": registered.priority,
422
+ }
423
+ for registered in RULES
424
+ ],
425
+ "sentences": sentences,
426
+ }
427
+
428
+
429
+ # =====================================================================
430
+ # 6. REPORTS
431
+ # =====================================================================
432
+
433
+ def _write_reports(result, source_name, payload):
434
+ folder = Path(tempfile.mkdtemp(prefix="nll_review_"))
435
+ stem = Path(source_name or "nll_input").stem
436
+ token = time.strftime("%Y%m%d_%H%M%S") + "_" + uuid.uuid4().hex[:6]
437
+
438
+ txt_path = folder / f"{stem}_{token}_revised.txt"
439
+ json_path = folder / f"{stem}_{token}_diagnostics.json"
440
+ docx_path = folder / f"{stem}_{token}_review.docx"
441
+ html_path = folder / f"{stem}_{token}_review.html"
442
+
443
+ revised = str(getattr(result, "revised", ""))
444
+ txt_path.write_text(revised, encoding="utf-8")
445
+ json_path.write_text(json.dumps(payload, ensure_ascii=False, indent=2), encoding="utf-8")
446
+
447
+ from docx import Document
448
+ document = Document()
449
+ document.add_heading("NLL Universal Professional Editor — Review", level=0)
450
+ document.add_paragraph("Discipline: " + str(getattr(result, "discipline", "")))
451
+ document.add_paragraph(
452
+ "Writing Pattern Score: " + str(getattr(result, "pattern_score", ""))
453
+ + " — " + str(getattr(result, "band", ""))
454
+ )
455
+ document.add_paragraph(
456
+ "The Writing Pattern Score describes observable structural and stylistic "
457
+ "characteristics only. It does not identify authorship."
458
+ )
459
+ document.add_heading("Revised text", level=1)
460
+ for paragraph in re.split(r"\n\s*\n", revised):
461
+ document.add_paragraph(paragraph)
462
+
463
+ document.add_heading("Sentence review", level=1)
464
+ for number, item in enumerate(list(getattr(result, "sentences", []) or []), 1):
465
+ trace = _trace(item)
466
+ document.add_heading(f"Sentence {number}: {getattr(item, 'status', '')}", level=2)
467
+ document.add_paragraph("Original: " + str(getattr(item, "original", "")))
468
+ document.add_paragraph("Revised: " + str(getattr(item, "revised", "")))
469
+ document.add_paragraph("Reason: " + str(getattr(item, "reason", "")))
470
+ document.add_paragraph(
471
+ "Must-paraphrase: "
472
+ + json.dumps(_json_safe(trace.get("must_paraphrase", {})), ensure_ascii=False)
473
+ )
474
+ document.save(str(docx_path))
475
+
476
+ rows = _sentence_rows(result)
477
+ body = "".join(
478
+ "<tr>"
479
+ f"<td>{row['Sentence']}</td>"
480
+ f"<td>{html.escape(str(row['Status']))}</td>"
481
+ f"<td>{html.escape(str(row['Accepted mode']))}</td>"
482
+ f"<td>{html.escape(str(row['Detected families']))}</td>"
483
+ f"<td>{html.escape(str(row['Original']))}</td>"
484
+ f"<td>{html.escape(str(row['Revised']))}</td>"
485
+ f"<td>{html.escape(str(row['Reason']))}</td>"
486
+ "</tr>"
487
+ for row in rows
488
+ )
489
+ html_path.write_text(
490
+ f"""<!doctype html><html lang="en"><head><meta charset="utf-8">
491
+ <title>NLL Review</title><style>
492
+ body{{font-family:Arial,sans-serif;max-width:1500px;margin:30px auto;color:#172033}}
493
+ h1,h2{{color:#123b66}}table{{border-collapse:collapse;width:100%}}
494
+ th,td{{border:1px solid #cbd5e1;padding:8px;vertical-align:top;text-align:left}}
495
+ th{{background:#eaf2fb}}pre{{white-space:pre-wrap;background:#f8fafc;padding:12px}}
496
+ </style></head><body>
497
+ <h1>NLL Universal Professional Editor — Review</h1>
498
+ <p><strong>Rules:</strong> {len(RULES)} | <strong>Must paraphrase:</strong> {MUST_PARAPHRASE_ACTIVE}</p>
499
+ <h2>Revised text</h2><pre>{html.escape(revised)}</pre>
500
+ <h2>Sentence review</h2><table><thead><tr><th>#</th><th>Status</th><th>Mode</th>
501
+ <th>Families</th><th>Original</th><th>Revised</th><th>Reason</th></tr></thead>
502
+ <tbody>{body}</tbody></table>
503
+ <h2>Full diagnostics</h2><pre>{html.escape(json.dumps(payload, ensure_ascii=False, indent=2))}</pre>
504
+ </body></html>""",
505
+ encoding="utf-8",
506
+ )
507
+
508
+ return [str(txt_path), str(docx_path), str(json_path), str(html_path)]
509
+
510
+
511
+ # =====================================================================
512
+ # 7. MAIN CALLBACK
513
+ # =====================================================================
514
+
515
+ def run_ui_with_full_diagnostics(
516
+ typed_text,
517
+ uploaded_file,
518
+ workflow,
519
+ mode,
520
+ discipline,
521
+ protected_terms,
522
+ use_stanza,
523
+ use_language_tool,
524
+ progress=gr.Progress(),
525
+ ):
526
+ source_text, source_name = _read_input_text(typed_text, uploaded_file)
527
+ if not source_text.strip():
528
+ raise gr.Error("No readable text was provided.")
529
+
530
+ _apply_ui_settings(use_stanza, use_language_tool)
531
+ progress(0.02, desc="Preparing request")
532
+
533
+ try:
534
+ result = ENGINE.process(
535
+ source_text, workflow, mode, discipline, protected_terms, progress
536
+ )
537
+ except TypeError:
538
+ result = ENGINE.process(
539
+ text=source_text,
540
+ workflow=workflow,
541
+ mode=mode,
542
+ discipline=discipline,
543
+ terms_text=protected_terms,
544
+ progress=progress,
545
+ )
546
+
547
+ progress(0.86, desc="Building diagnostics")
548
+ payload = _full_payload(result, source_name)
549
+ files = _write_reports(result, source_name, payload)
550
+ progress(1.0, desc="Complete")
551
+
552
+ return (
553
+ getattr(result, "revised", source_text),
554
+ _summary_markdown(result),
555
+ pd.DataFrame(_sentence_rows(result)),
556
+ pd.DataFrame(_routing_rows(result)),
557
+ pd.DataFrame(_attempt_rows(result)),
558
+ pd.DataFrame(_realization_rows(result)),
559
+ pd.DataFrame(_candidate_rows(result)),
560
+ pd.DataFrame(_finding_rows(result)),
561
+ payload,
562
+ files,
563
+ )
564
+
565
+
566
+ # =====================================================================
567
+ # 8. PROFESSIONAL GRADIO UI
568
+ # =====================================================================
569
+
570
+ CELL9_CSS = """
571
+ .gradio-container { max-width: 1650px !important; }
572
+ #nll-title { text-align:center; margin-bottom:4px; color:#123b66; }
573
+ #nll-subtitle { text-align:center; color:#596779; margin-bottom:16px; }
574
+ .nll-panel { border:1px solid #d5dee8; border-radius:14px; padding:12px; background:#fbfdff; }
575
+ .nll-note { background:#fff8dc; border-left:4px solid #d7a100; padding:8px 12px; }
576
+ .nll-ok { background:#e8f6ee; border-left:4px solid #238636; padding:8px 12px; }
577
+ """
578
+
579
+ if "NLL_APP" in globals():
580
+ try:
581
+ NLL_APP.close()
582
+ except Exception:
583
+ pass
584
+
585
+ with gr.Blocks(css=CELL9_CSS, title="NLL Universal Professional Editor") as NLL_APP:
586
+ gr.Markdown("# NLL Universal Professional Editor", elem_id="nll-title")
587
+ gr.Markdown(
588
+ "Deterministic, non-generative rewriting with must-paraphrase orchestration, "
589
+ "meaning safeguards, and complete sentence/candidate diagnostics.",
590
+ elem_id="nll-subtitle",
591
+ )
592
+
593
+ gr.Markdown(
594
+ f"**System ready:** {len(RULES)} rules | Must-paraphrase active | "
595
+ "Cell 5A realization gate active | Cell 7 judge active",
596
+ elem_classes="nll-ok",
597
+ )
598
+
599
+ with gr.Row(equal_height=False):
600
+ with gr.Column(scale=1, elem_classes="nll-panel"):
601
+ gr.Markdown("## Input and controls")
602
+ input_text = gr.Textbox(
603
+ label="Text",
604
+ lines=20,
605
+ placeholder="Paste academic, contract, policy, technical, business, or professional text.",
606
+ )
607
+ input_file = gr.File(
608
+ label="Optional file upload",
609
+ file_types=[".txt", ".docx", ".pdf"],
610
+ type="filepath",
611
+ )
612
+ workflow_control = gr.Radio(
613
+ choices=["Scan Only", "Scan & Neutralize", "Edit / Paraphrase Only"],
614
+ value="Edit / Paraphrase Only",
615
+ label="Workflow",
616
+ )
617
+ mode_control = gr.Radio(
618
+ choices=["Minimal", "Balanced", "Structural"],
619
+ value="Balanced",
620
+ label="Preferred rewrite mode",
621
+ )
622
+ discipline_control = gr.Dropdown(
623
+ choices=["Auto Detect"] + list(PROFILES.keys()),
624
+ value="Auto Detect",
625
+ label="Discipline",
626
+ )
627
+ protected_terms_control = gr.Textbox(
628
+ label="Immutable/user-protected terms",
629
+ placeholder="Comma, semicolon, or line-separated terms, e.g., WSC, TxDOT, ALCEF-RSF",
630
+ lines=3,
631
+ )
632
+ with gr.Accordion("Optional non-generative analysis resources", open=False):
633
+ stanza_control = gr.Checkbox(
634
+ value=bool(getattr(globals().get("CFG", object()), "stanza", False)),
635
+ label="Stanza parsing support",
636
+ )
637
+ language_tool_control = gr.Checkbox(
638
+ value=bool(getattr(globals().get("CFG", object()), "lt", False)),
639
+ label="Local LanguageTool comparison",
640
+ )
641
+ analyze_button = gr.Button("Analyze / Revise", variant="primary", size="lg")
642
+ gr.Markdown(
643
+ "The selected mode is attempted first. If no safe change survives, "
644
+ "the other modes are searched. The original is retained only after "
645
+ "all safe deterministic options are exhausted.",
646
+ elem_classes="nll-note",
647
+ )
648
+
649
+ with gr.Column(scale=1, elem_classes="nll-panel"):
650
+ gr.Markdown("## Revised output and review")
651
+ revised_output = gr.Textbox(label="Revised output", lines=20)
652
+ summary_output = gr.Markdown()
653
+
654
+ with gr.Tabs():
655
+ with gr.Tab("Sentence comparison"):
656
+ sentence_table = gr.Dataframe(
657
+ label="Original, revised, mode, family, and judge outcome",
658
+ interactive=False,
659
+ wrap=True,
660
+ )
661
+ with gr.Tab("Routing diagnostics"):
662
+ routing_table = gr.Dataframe(
663
+ label="Detected families, applicable rules, and generated candidates",
664
+ interactive=False,
665
+ wrap=True,
666
+ )
667
+ with gr.Tab("Must-paraphrase attempts"):
668
+ attempt_table = gr.Dataframe(
669
+ label="Requested-mode-first judge and retry attempts",
670
+ interactive=False,
671
+ wrap=True,
672
+ )
673
+ with gr.Tab("Realization rejections"):
674
+ realization_table = gr.Dataframe(
675
+ label="Candidates rejected by Cell 5A",
676
+ interactive=False,
677
+ wrap=True,
678
+ )
679
+ with gr.Tab("Candidate diagnostics"):
680
+ candidate_table = gr.Dataframe(
681
+ label="Validated candidates, scores, and rejection reasons",
682
+ interactive=False,
683
+ wrap=True,
684
+ )
685
+ with gr.Tab("Writing-pattern findings"):
686
+ findings_table = gr.Dataframe(
687
+ label="Observable structural and stylistic findings",
688
+ interactive=False,
689
+ wrap=True,
690
+ )
691
+ with gr.Tab("Full JSON diagnostics"):
692
+ diagnostic_json = gr.JSON(label="Complete diagnostic payload")
693
+ with gr.Tab("Downloads"):
694
+ download_output = gr.File(
695
+ label="Download revised text and review reports",
696
+ file_count="multiple",
697
+ )
698
+
699
+ inputs = [
700
+ input_text, input_file, workflow_control, mode_control,
701
+ discipline_control, protected_terms_control,
702
+ stanza_control, language_tool_control,
703
+ ]
704
+ outputs = [
705
+ revised_output, summary_output, sentence_table, routing_table,
706
+ attempt_table, realization_table, candidate_table, findings_table,
707
+ diagnostic_json, download_output,
708
+ ]
709
+
710
+ analyze_button.click(
711
+ fn=run_ui_with_full_diagnostics,
712
+ inputs=inputs,
713
+ outputs=outputs,
714
+ show_progress="full",
715
+ concurrency_limit=1,
716
+ )
717
+
718
+ NLL_APP.queue(default_concurrency_limit=1, max_size=8)
719
+
720
+ print("=" * 76)
721
+ print("CELL 9 PROFESSIONAL GRADIO UI v5.1 LOADED")
722
+ print("=" * 76)
723
+ print("Aligned pipeline: 5 v4.1 -> 5A v3.4 -> 6 v5.1 -> 7 v4 -> 7A v2 -> 7B v2 -> 8 v4 -> 8A v2")
724
+ print("Registered transformation rules:", len(RULES))
725
+ print("Expected rule count:", EXPECTED_RULE_COUNT)
726
+ print("Must-paraphrase orchestrator: active")
727
+ print("Cell 5A realization diagnostics: active")
728
+ print("Sentence comparison: active")
729
+ print("Routing diagnostics: active")
730
+ print("Must-paraphrase attempt diagnostics: active")
731
+ print("Candidate diagnostics: active")
732
+ print("Writing Pattern Score terminology: active")
733
+ print("LLM/MLM candidate generation: disabled")
734
+ print("STS/NLI scoring: disabled")
735
+ print("Online lexical fallback: disabled")
736
+ print("TXT/DOCX/JSON/HTML downloads: active")
737
+ print("=" * 76)
738
+
739
+ # Launch is handled by app.py via gr.mount_gradio_app.
runtime/core.py ADDED
@@ -0,0 +1,260 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Portable runtime foundation for the NLL deterministic editor."""
2
+ from __future__ import annotations
3
+
4
+ import os
5
+ import re
6
+ import tempfile
7
+ from collections import Counter
8
+ from dataclasses import dataclass, field
9
+ from pathlib import Path
10
+ from typing import Any
11
+
12
+ import spacy
13
+
14
+
15
+ @dataclass
16
+ class Config:
17
+ batch: int = int(os.getenv("NLL_BATCH_SIZE", "32"))
18
+ out: str = os.getenv("NLL_OUTPUT_DIR", tempfile.gettempdir())
19
+ stanza: bool = False
20
+ sts: bool = False
21
+ nli: bool = False
22
+ mlm: bool = False
23
+ lt: bool = os.getenv("NLL_ENABLE_LANGUAGETOOL", "false").lower() == "true"
24
+ online: bool = False
25
+ min_sts: float = 0.72
26
+ min_nli: float = 0.45
27
+
28
+ CFG = Config()
29
+ STATE: dict[str, Any] = {"warnings": [], "start": None}
30
+ DISCLAIMER = "The Writing Pattern Score describes observable structural and stylistic characteristics only."
31
+
32
+
33
+ def _load_spacy():
34
+ try:
35
+ return spacy.load("en_core_web_sm")
36
+ except Exception as exc:
37
+ if os.getenv("NLL_REQUIRE_SPACY_MODEL", "true").lower() == "true":
38
+ raise RuntimeError("spaCy model en_core_web_sm is required for deployment.") from exc
39
+ pipe = spacy.blank("en")
40
+ if "sentencizer" not in pipe.pipe_names:
41
+ pipe.add_pipe("sentencizer")
42
+ return pipe
43
+
44
+ _NLP = _load_spacy()
45
+
46
+ def nlp():
47
+ return _NLP
48
+
49
+
50
+ def sent_split(text):
51
+ value = str(text or "").strip()
52
+ if not value:
53
+ return []
54
+ try:
55
+ doc = _NLP(value)
56
+ parts = [s.text.strip() for s in doc.sents if s.text.strip()]
57
+ if parts:
58
+ return parts
59
+ except Exception:
60
+ pass
61
+ return [x.strip() for x in re.split(r"(?<=[.!?])\s+", value) if x.strip()]
62
+
63
+
64
+ def sub(token):
65
+ try:
66
+ items = list(token.subtree)
67
+ if not items:
68
+ return token.text
69
+ start = min(x.i for x in items)
70
+ end = max(x.i for x in items) + 1
71
+ return token.doc[start:end].text
72
+ except Exception:
73
+ return getattr(token, "text", str(token))
74
+
75
+
76
+ @dataclass
77
+ class Candidate:
78
+ text: str
79
+ lane: str
80
+ family: str
81
+ reason: str
82
+ priority: float = .5
83
+ score: float = 0.0
84
+ checks: dict = field(default_factory=dict)
85
+
86
+ @dataclass
87
+ class Analysis:
88
+ text: str
89
+ families: list[str] = field(default_factory=list)
90
+ enumerations: list[list[str]] = field(default_factory=list)
91
+ notes: list[str] = field(default_factory=list)
92
+ metadata: dict = field(default_factory=dict)
93
+
94
+ @dataclass
95
+ class Profile:
96
+ name: str
97
+ keywords: set[str] = field(default_factory=set)
98
+ phrases: set[str] = field(default_factory=set)
99
+ immutable: set[str] = field(default_factory=set)
100
+
101
+ @dataclass
102
+ class Finding:
103
+ sentence: int
104
+ category: str
105
+ text: str
106
+ score: float = 0.0
107
+ message: str = ""
108
+
109
+ @dataclass
110
+ class SentenceResult:
111
+ original: str
112
+ revised: str
113
+ status: str
114
+ families: list[str]
115
+ family: str | None
116
+ reason: str
117
+ alternatives: list[str]
118
+ checks: dict
119
+ diagnostics: dict
120
+
121
+ @property
122
+ def trace(self):
123
+ return self.diagnostics
124
+ @trace.setter
125
+ def trace(self, value):
126
+ self.diagnostics = value
127
+
128
+ @dataclass
129
+ class DocumentResult:
130
+ original: str
131
+ revised: str
132
+ workflow: str
133
+ mode: str
134
+ discipline: str
135
+ evidence: list[str]
136
+ pattern_score: float
137
+ band: str
138
+ findings: list
139
+ sentences: list[SentenceResult]
140
+ warnings: list[str]
141
+ stats: dict
142
+
143
+
144
+ PROFILES = {
145
+ "General Academic": Profile(
146
+ "General Academic",
147
+ {"study", "research", "analysis", "method", "results", "framework"},
148
+ {"this study", "the results"},
149
+ set(),
150
+ ),
151
+ "Technical": Profile(
152
+ "Technical",
153
+ {"system", "service", "handler", "configuration", "deployment", "model"},
154
+ {"environment variable", "source code"},
155
+ set(),
156
+ ),
157
+ "Business": Profile(
158
+ "Business",
159
+ {"supplier", "schedule", "invoice", "budget", "manager", "procurement"},
160
+ {"implementation schedule", "cost ledger"},
161
+ set(),
162
+ ),
163
+ "Contract": Profile(
164
+ "Contract",
165
+ {"contractor", "shall", "owner", "payment", "reimbursement", "eligible"},
166
+ {"for purposes of", "approved work"},
167
+ set(),
168
+ ),
169
+ }
170
+
171
+
172
+ def _extract_enumerations(text):
173
+ groups = []
174
+ for match in re.finditer(
175
+ r"(?is)([^.;:]{1,120}?,\s*[^.;:]{1,120}?(?:,\s*[^.;:]{1,120}?)*,?\s+(?:and|or)\s+[^.;:]{1,120}?)(?=[.;:]|$)",
176
+ text,
177
+ ):
178
+ raw = match.group(1)
179
+ parts = [x.strip(" ,") for x in re.split(r",\s*|\s+(?:and|or)\s+", raw) if x.strip(" ,")]
180
+ if len(parts) >= 3:
181
+ groups.append(parts)
182
+ return groups
183
+
184
+
185
+ class Analyzer:
186
+ def analyze(self, sentence, profile, user_terms):
187
+ text = str(sentence or "").strip()
188
+ families = set()
189
+ enumerations = _extract_enumerations(text)
190
+ low = text.lower()
191
+ if enumerations:
192
+ families.update({"ENUMERATION", "COORDINATED_OBJECTS_OR_ENUMERATION"})
193
+ if re.match(r"^(?:Applying|Using|Evaluating|Comparing|Assessing|Examining|Analyzing|Implementing|Testing|Measuring)\b", text, re.I):
194
+ families.add("GERUND_SUBJECT")
195
+ if re.search(r"\b(?:is|are|was|were|has been|have been|had been)\s+\w+(?:ed|en)\b", low):
196
+ families.add("PASSIVE")
197
+ try:
198
+ doc = _NLP(text)
199
+ dep = doc.has_annotation("DEP")
200
+ except Exception:
201
+ dep = False
202
+ return Analysis(
203
+ text=text,
204
+ families=sorted(families),
205
+ enumerations=enumerations,
206
+ metadata={"dependency_available": dep, "base_analyzer": "portable"},
207
+ )
208
+
209
+
210
+ class Scanner:
211
+ def scan(self, text):
212
+ sentences = sent_split(text)
213
+ findings = []
214
+ for idx, sentence in enumerate(sentences):
215
+ words = sentence.split()
216
+ commas = sentence.count(",")
217
+ if len(words) >= 32 or commas >= 4:
218
+ findings.append(Finding(idx, "sentence complexity", sentence, .6, "Long or list-dense sentence may be complex."))
219
+ score = round(min(100.0, sum((len(s.split()) / 35.0) + (s.count(",") * .45) for s in sentences)), 2)
220
+ band = "Low" if score < 20 else "Moderate" if score < 50 else "High"
221
+ features = {"sentences": len(sentences), "long_sentences": sum(len(s.split()) >= 32 for s in sentences)}
222
+ return findings, score, band, features
223
+
224
+
225
+ _RESOURCES: dict[str, Any] = {}
226
+
227
+ def optional(name):
228
+ if name in {"sts", "nli", "mlm"}:
229
+ return None
230
+ if name == "lt":
231
+ if not CFG.lt:
232
+ return None
233
+ if "lt" not in _RESOURCES:
234
+ try:
235
+ import language_tool_python
236
+ _RESOURCES["lt"] = language_tool_python.LanguageTool("en-US")
237
+ except Exception as exc:
238
+ STATE.setdefault("warnings", []).append(f"LanguageTool unavailable: {exc}")
239
+ _RESOURCES["lt"] = None
240
+ return _RESOURCES["lt"]
241
+ return None
242
+
243
+
244
+ def protect(text, hard_terms):
245
+ value = str(text or "")
246
+ found = []
247
+ patterns = {
248
+ "url": r"https?://[^\s)\]]+",
249
+ "citation": r"\b[A-Z][A-Za-z-]+\s+et\s+al\.\s*\(\d{4}[a-z]?\)|\([A-Z][A-Za-z-]+(?:\s+et\s+al\.)?,?\s*\d{4}[a-z]?\)",
250
+ "quantity": r"(?<!\w)\d+(?:\.\d+)?%?(?!\w)",
251
+ "equation": r"\b[A-Za-z]\s*=\s*[^,.;]+",
252
+ "code": r"`[^`]+`",
253
+ }
254
+ for kind, pattern in patterns.items():
255
+ for match in re.finditer(pattern, value):
256
+ found.append((kind, match.group(0)))
257
+ for term in set(hard_terms or set()):
258
+ if term and re.search(r"(?<!\w)" + re.escape(term) + r"(?!\w)", value, re.I):
259
+ found.append(("term", term))
260
+ return found
runtime/engine.py ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Loads the validated notebook engine in one shared deployment namespace."""
2
+ from pathlib import Path
3
+ from .core import * # noqa: F401,F403
4
+
5
+ ROOT = Path(__file__).resolve().parent
6
+
7
+ def _exec(name):
8
+ path = ROOT / name
9
+ code = compile(path.read_text(encoding="utf-8"), str(path), "exec")
10
+ exec(code, globals(), globals())
11
+
12
+ for _module in ("cell5.py", "cell5a.py", "cell6.py", "cell7.py", "cell7a.py", "cell7b.py"):
13
+ _exec(_module)
14
+
15
+ ENGINE_VERSION = "4.1/3.4/5.1/4.3/2/2.1"
scripts/deploy.ps1 ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ $ErrorActionPreference = "Stop"
2
+ $Space = "raymuz/NLLUniversalProfessionalEditor"
3
+ if (-not $env:HF_TOKEN) { throw "Set HF_TOKEN to a Hugging Face write token." }
4
+ $Temp = Join-Path $env:TEMP ("nll-space-" + [guid]::NewGuid())
5
+ git clone "https://oauth2:$($env:HF_TOKEN)@huggingface.co/spaces/$Space" $Temp
6
+ Get-ChildItem -Force | Where-Object { $_.Name -notin @('.git') } | Copy-Item -Destination $Temp -Recurse -Force
7
+ Push-Location $Temp
8
+ git add .
9
+ git commit -m "Deploy NLL Universal Professional Editor"
10
+ git push origin main
11
+ Pop-Location
scripts/deploy.sh ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ SPACE="raymuz/NLLUniversalProfessionalEditor"
4
+ if ! command -v git >/dev/null; then echo "git is required"; exit 1; fi
5
+ if [ -z "${HF_TOKEN:-}" ]; then echo "Set HF_TOKEN to a write token."; exit 1; fi
6
+ REPO_URL="https://oauth2:${HF_TOKEN}@huggingface.co/spaces/${SPACE}"
7
+ TMP="$(mktemp -d)"
8
+ git clone "$REPO_URL" "$TMP/space"
9
+ find "$TMP/space" -mindepth 1 -maxdepth 1 ! -name .git -exec rm -rf {} +
10
+ cp -a . "$TMP/space/source"
11
+ cp -a "$TMP/space/source/." "$TMP/space/"
12
+ rm -rf "$TMP/space/source"
13
+ cd "$TMP/space"
14
+ git add .
15
+ git commit -m "Deploy NLL Universal Professional Editor" || true
16
+ git push origin main
tests/test_smoke.py ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from runtime.engine import ENGINE, RULES, MUST_PARAPHRASE_RUNTIME, surface_key
2
+
3
+ def test_runtime_contract():
4
+ assert len(RULES) == 30
5
+ assert MUST_PARAPHRASE_RUNTIME["active"] is True
6
+ assert MUST_PARAPHRASE_RUNTIME["document_trace_verified"] is True
7
+
8
+ def test_rewrite_smoke():
9
+ result = ENGINE.process(
10
+ "The next step is to inspect how the handler checks configuration.",
11
+ "Edit / Paraphrase Only",
12
+ "Balanced",
13
+ "Auto Detect",
14
+ "",
15
+ )
16
+ assert result.sentences
17
+ sentence = result.sentences[0]
18
+ assert sentence.status == "SUCCESS"
19
+ assert surface_key(sentence.original) != surface_key(sentence.revised)
20
+ assert sentence.diagnostics["must_paraphrase"]["attempt_count"] >= 1