Deploy NLL Universal Professional Editor production Docker build
Browse files- .dockerignore +10 -0
- .gitignore +7 -0
- DEPLOYMENT.md +26 -0
- Dockerfile +27 -0
- Makefile +10 -0
- README.md +50 -9
- SECURITY.md +7 -0
- api/README.md +6 -0
- api/__init__.py +0 -0
- api/flask_api.py +28 -0
- app.py +28 -0
- docker-compose.yml +8 -0
- frontend/README.md +3 -0
- packages.txt +1 -0
- pytest.ini +3 -0
- requirements.txt +10 -0
- runtime/__init__.py +0 -0
- runtime/cell5.py +1599 -0
- runtime/cell5a.py +574 -0
- runtime/cell6.py +612 -0
- runtime/cell7.py +1213 -0
- runtime/cell7a.py +163 -0
- runtime/cell7b.py +404 -0
- runtime/cell8.py +262 -0
- runtime/cell8a.py +393 -0
- runtime/cell9.py +739 -0
- runtime/core.py +260 -0
- runtime/engine.py +15 -0
- scripts/deploy.ps1 +11 -0
- scripts/deploy.sh +16 -0
- tests/test_smoke.py +20 -0
.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:
|
| 3 |
-
emoji:
|
| 4 |
-
colorFrom:
|
| 5 |
-
colorTo:
|
| 6 |
-
sdk:
|
| 7 |
-
|
| 8 |
-
python_version: '3.13'
|
| 9 |
-
app_file: app.py
|
| 10 |
pinned: false
|
| 11 |
license: apache-2.0
|
|
|
|
| 12 |
---
|
| 13 |
|
| 14 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|