File size: 14,986 Bytes
593e4c1
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
585f232
 
 
 
 
593e4c1
 
585f232
 
 
593e4c1
 
 
 
 
 
 
 
 
 
 
 
 
 
689da70
 
 
593e4c1
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
585f232
 
 
 
593e4c1
 
78dbb12
 
 
593e4c1
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
eaf69a7
 
 
 
 
 
 
 
 
593e4c1
eaf69a7
 
 
593e4c1
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
"""
Publication English rulebook: the rules every humanized sentence must satisfy before it can replace the author's
sentence. One source of truth, used in two ways:

  * PROMPT    - the rules as instructions for the language-model steps (quality check, final polish, alternatives),
                so the rewrite is written to the rules in the first place;
  * violations(original, rewrite) - the automatic checks of every rule that can be measured. The clarity gate
                (clarity.py) rejects a rewrite that breaks any of them; if no version of a sentence passes, the
                author's original sentence is kept. A sentence is therefore never made worse than the author wrote it.

Rule groups
  A. Integrity (never broken)  - meaning, claims, numbers, units, citations, terms, acronyms, negation, direction of
                                 every comparison, strength of every claim (hedging), one sentence in = one out
  B. Grammar                   - complete, correct sentences (checked by the grammar / fluency models in clarity.py)
  C. Academic register         - formal, impersonal, no contractions, no "you", no first person singular, no
                                 informal words, no rhetorical questions, no exclamation marks
  D. Human academic style      - no AI-typical vocabulary or stock phrases, no em dashes, no padding, plain
                                 specific verbs, varied rhythm, no sentence-initial "And/But/So/Also"
  E. Flow                      - logical links only where needed, no new paragraph-level claims
  F. Consistency               - the author's spelling (British / American), terminology, tense and number format

Space variables
  PUBRULES   1 (default) | 0 = automatic rule checks off (the prompt rules stay)
  PUB_MAX_WORDS  default 40   a rewrite may not exceed this length unless the original already did
"""
import os
import re

import textparse as TP

ENABLED = os.environ.get("PUBRULES", "1") != "0"
MAX_WORDS = int(os.environ.get("PUB_MAX_WORDS", "40"))

# ------------------------------------------------------------------------------------------------ word lists
AI_WORDS = ["delve", "delves", "delving", "crucial", "pivotal", "comprehensive", "furthermore", "moreover",
            "notably", "additionally", "leverage", "leverages", "leveraging", "robust", "seamless", "seamlessly",
            "intricate", "landscape", "realm", "underscore", "underscores", "underscoring", "showcase", "showcases",
            "tapestry", "multifaceted", "paramount", "meticulous", "meticulously", "holistic", "foster", "fosters",
            "fostering", "testament", "ever-evolving", "groundbreaking", "cutting-edge", "game-changer",
            "revolutionize", "revolutionise", "unprecedented", "invaluable", "noteworthy", "commendable",
            "vibrant", "embark", "embarks", "unveil", "unveils", "harness", "harnesses", "harnessing",
            "navigate", "navigating", "elevate", "elevates", "streamline", "synergy", "synergistic",
            "nuanced", "myriad", "plethora", "endeavor", "endeavour", "bolster", "bolsters", "spearhead",
            "transformative", "unwavering", "intrinsically", "profound", "profoundly", "enhance", "enhances"]

AI_PHRASES = [r"it is (?:important|worth|crucial|essential) to (?:note|mention|highlight|emphasi[sz]e)",
              r"plays? an? (?:crucial|pivotal|vital|key|significant|important) role",
              r"in today'?s (?:world|era|landscape|society|digital age)", r"a testament to",
              r"in the (?:realm|landscape|ever-evolving|rapidly evolving) (?:of )?", r"shed(?:s|ding)? light on",
              r"pav(?:e|es|ed|ing) the way", r"\bnot only\b.{1,80}\bbut also\b", r"at the end of the day",
              r"in conclusion,", r"to sum up", r"it can be (?:seen|said|argued) that", r"serves? as a",
              r"stands? as a", r"a wide (?:range|array|variety) of", r"the (?:intricate|complex) interplay",
              r"in order to", r"due to the fact that", r"with regard to", r"a (?:key|vital|crucial) aspect",
              r"rich (?:history|tapestry)", r"(?:navigate|navigating) the (?:complexities|challenges)"]
_AI_PHRASE_RE = [re.compile(p, re.I) for p in AI_PHRASES]
_AI_WORD_RE = re.compile(r"\b(" + "|".join(re.escape(w) for w in AI_WORDS) + r")\b", re.I)

NEGATORS = r"\b(?:not|no|never|none|neither|nor|without|cannot|unable|fail(?:s|ed)? to|lack(?:s|ed|ing)?|absence)\b|n't\b"
# ("indicate" is a neutral reporting verb, like "show": swapping the two does not change the strength of a claim)
HEDGES = r"\b(?:may|might|could|can|suggests?|suggested|suggesting|impl(?:y|ies|ied|ying)|appears?|appeared|" \
         r"seems?|seemed|likely|unlikely|possibly|potentially|probably|perhaps|presumably|apparently|arguably|" \
         r"approximately|roughly|nearly|almost|partly|partially|somewhat|tends?|tended|tending|inclined|" \
         r"relatively|generally|typically|usually|often)\b"
BOOSTERS = r"\b(?:proves?|proved|proven|clearly|definitely|certainly|undoubtedly|always|completely|fully|" \
           r"guarantees?|guaranteed|conclusively|obviously|absolutely|entirely|perfectly)\b"
# a rewrite may not state a cause the author did not state
CAUSAL = r"\b(?:because|since|therefore|thus|hence|consequently|thereby|as a result|due to|owing to|so that)\b"
_WE = re.compile(r"\b(?:we|our|ours|us)\b", re.I)
# direction words: a rewrite may not turn one into its opposite
OPPOSITES = [("increas", "decreas"), ("higher", "lower"), ("more", "less"), ("more", "fewer"), ("rise", "fall"),
             ("rais", "lower"), ("improv", "degrad"), ("improv", "wors"), ("above", "below"), ("maxim", "minim"),
             ("positive", "negative"), ("significant", "insignificant"), ("significant", "non-significant"),
             ("accept", "reject"), ("before", "after"), ("includ", "exclud"), ("enabl", "disabl"),
             ("strengthen", "weaken"), ("larger", "smaller"), ("gain", "loss"), ("success", "failure"),
             ("outperform", "underperform"), ("strong", "weak"), ("faster", "slower"), ("best", "worst"),
             ("upper", "lower"), ("tamper-evident", "tampered"), ("halve", "double"), ("allow", "block"), ("allow", "prevent"), ("permit", "forbid"), ("grant", "revok"),
             ("inside", "outside"), ("honest", "dishonest"), ("trust", "distrust"), ("safe", "unsafe")]
_INFORMAL_START = re.compile(r"^\s*(?:And|But|So|Also|Plus|Anyway|Well|Basically|Actually)\b")
_EM_DASH = re.compile(r"\s[—–]\s|—")
_CONTRACTION = re.compile(r"\b\w+(?:n't|'re|'ll|'ve|'m|'d)\b", re.I)
_FIRST_SING = re.compile(r"\b(?:I|me|my|mine|myself)\b")
_YOU = re.compile(r"\b(?:you|your|yours|yourself)\b", re.I)
_UNITS = (r"(?:%|ms|µs|μs|ns|s|Hz|kHz|MHz|GHz|dB|dBm|nm|µm|μm|mm|cm|m|km|µg|μg|ng|mg|g|kg|µL|μL|mL|ml|L|mV|kV|V|µA|μA|mA|A|"
          r"mW|kW|MW|W|kWh|Wh|J|kJ|K|°C|°F|h|min|px|KB|MB|GB|TB|bps|Mbps|Gbps|Pa|kPa|MPa|GPa|bar|kN|mol|mmol|µmol|"
          r"μmol|rpm)(?:/[A-Za-zµμ]+\d?)?")
_NUM = re.compile(rf"(?:(?<=[\s(])[-−+±])?(?<![\w.])\d+(?:[.,]\d+)*(?:\s?{_UNITS}(?![A-Za-z]))?")
_PAST_AUX = re.compile(r"\b(?:was|were|had|did)\b", re.I)
_PRES_AUX = re.compile(r"\b(?:is|are|has|have|does|do)\b", re.I)


def _count(pat, t):
    return len(re.findall(pat, t or "", re.I))


def _stems(t):
    return [w.lower() for w in re.findall(r"[A-Za-z][A-Za-z\-]+", t or "")]


def _has(words, stem):
    return any(w.startswith(stem) for w in words)


def _acronyms(t):
    return {w for w in re.findall(r"\b[A-Z][A-Z0-9]{1,}[a-z]?\b", TP.CITE.sub(" ", t or ""))}


def _numbers(t):
    return sorted(re.sub(r"\s+", "", n).replace("−", "-") for n in _NUM.findall(TP.CITE.sub(" ", t or "")))


def violations(original, new):
    """Rules of groups A, C, D and F that `new` breaks compared with `original` (empty list = passes).
    A rule only counts when the rewrite introduces the problem: whatever the author already wrote is never held
    against the rewrite."""
    if not ENABLED or not new or not original or new.strip() == original.strip():
        return []
    o, n = original, new
    out = []
    # ---- A. integrity
    if _numbers(n) != _numbers(o):
        out.append("numbers or units changed")
    if _acronyms(o) - _acronyms(n):
        out.append("acronym dropped")
    if _acronyms(n) - _acronyms(o):
        out.append("acronym added")
    if (_count(NEGATORS, n) > 0) != (_count(NEGATORS, o) > 0) or abs(_count(NEGATORS, n) - _count(NEGATORS, o)) > 1:
        out.append("negation changed")
    ow, nw = _stems(o), _stems(n)
    for a, b in OPPOSITES:
        for x, y in ((a, b), (b, a)):
            if _has(nw, x) and not _has(ow, x) and _has(ow, y) and not _has(nw, y):
                out.append(f"direction reversed ({y}… → {x}…)")
                break
    if _count(HEDGES, o) and not _count(HEDGES, n):
        out.append("hedging removed (claim made stronger)")
    if _count(BOOSTERS, n) > _count(BOOSTERS, o):
        out.append("claim made stronger")
    if _count(CAUSAL, n) and not _count(CAUSAL, o):       # "X mediates Y, suggesting Z" is not "because X, Z"
        out.append("causal link added")
    if _WE.search(n) and not _WE.search(o):               # "we surveyed": a voice (and often a method) not the author's
        out.append("first person plural added")
    if len(TP.split_sentences(n)) != len(TP.split_sentences(o)):   # one sentence in = one sentence out
        out.append("sentence split or merged")
    import profile_settings as PS
    if PS.get("rules") == "integrity":                  # priority "ai": integrity rules only
        return out
    # ---- C. academic register
    for pat, why in ((_CONTRACTION, "contraction"), (_FIRST_SING, "first person singular"), (_YOU, "addresses the reader")):
        if pat.search(n) and not pat.search(o):
            out.append(why)
    if "!" in n and "!" not in o:
        out.append("exclamation mark")
    # ---- D. human academic style
    added = {m.group(0).lower() for m in _AI_WORD_RE.finditer(n)} - {m.group(0).lower() for m in _AI_WORD_RE.finditer(o)}
    if added:
        out.append("AI-typical word: " + ", ".join(sorted(added)))
    for rx in _AI_PHRASE_RE:
        if rx.search(n) and not rx.search(o):
            out.append("AI-typical phrase")
            break
    if _EM_DASH.search(n) and not _EM_DASH.search(o):
        out.append("em dash")
    if _INFORMAL_START.search(n) and not _INFORMAL_START.search(o):
        out.append("sentence starts with a conjunction")
    if len(n.split()) > max(MAX_WORDS, len(o.split()) + 5):
        out.append("sentence too long")
    # ---- F. consistency
    po, pr_o = len(_PAST_AUX.findall(o)), len(_PRES_AUX.findall(o))
    pn, pr_n = len(_PAST_AUX.findall(n)), len(_PRES_AUX.findall(n))
    if (po and not pr_o and pr_n and not pn) or (pr_o and not po and pn and not pr_n):
        out.append("tense changed")
    return out


# ------------------------------------------------------------------------------------------------ prompt rules
PROMPT = f"""PUBLICATION ENGLISH RULES (every sentence you return must satisfy all of them):
A. Integrity - never broken:
 A1. Same meaning, same claims, same scope. Add nothing (no new facts, examples, causes, names, numbers, results).
     Drop nothing (every clause, condition, qualifier and list item stays).
 A2. Every number, unit, statistic (β, p, r, R², %, n, CI), variable, symbol, equation reference, technical term,
     acronym, model or dataset name, author name, year and citation ([12], (Kumar et al., 2021)) stays exactly as
     written and stays attached to the same claim. Never move, merge, renumber or duplicate a citation.
 A3. Never reverse a direction or a polarity: higher/lower, increase/decrease, before/after, allow/block, with/
     without, not/no stay as they are. "tamper-evident" never becomes "can be tampered with".
 A4. Keep the strength of every claim: keep may/might/suggests/approximately; never add proves/clearly/always/all.
 A5. One sentence in, one sentence out: never split, merge, drop or reorder sentences.
 A6. Never replace a technical term with a synonym ("accuracy" is not "precision", "sweeps" is not "reaches",
     "escalates" is not "increases"); when unsure, keep the author's word. The study's own constructs (every term
     defined with an abbreviation, e.g. "travel satisfaction (TS)", "intimacy (IN)") keep their exact names: never
     "pleasure", "contentment", "happiness", "closeness", "allegiance", "devotion" for them.
 A7. Never add a reason, cause, explanation or attribution the original does not state ("according to some
     experts", "since they have ...", "a systematic analysis found", "researchers contend").
 A8. Keep every list item and every name or place ("Digha, Darjeeling, Jhargram and Purulia" keeps all four).
 A9. Keep statistics exactly: "p = .120" stays "p = .120"; never turn "=" into "<" or ">", never add an operator
     that is missing in the original, never drop one ("p < .001" never becomes "p .001").
 A10. Never end a sentence or a term half-way ("... and to enhance their.", "well-,").
B. Grammar - every sentence complete and correct: subject-verb agreement, articles, verb forms, tense, prepositions,
   punctuation (keep every comma of the original that is still needed: in lists, after introductory phrases such as
   "After COVID-19," or "However,", and after a bracketed abbreviation in a list; never remove commas to change the
   style), no missing, doubled or run-together words, no
   fragments, no run-on sentences, a capital only where English needs one ("Greshake et al. generalised", not
   "et al. Generalised").
C. Academic register: formal and impersonal; no contractions, no "you", no first person singular, no informal or
   conversational words, no rhetorical questions, no exclamation marks; "we" only where the author used it.
D. Human academic style (how careful researchers actually write):
 D1. Plain, specific verbs (shows, reduces, uses, compares) instead of inflated ones.
 D2. Never use: {", ".join(AI_WORDS[:40])}; never use stock phrases such as "it is important to note",
     "plays a crucial role", "in today's world", "a testament to", "sheds light on", "paves the way",
     "not only ... but also", "a wide range of", "in order to", "due to the fact that", "serves as".
 D3. No em dashes, no lists of three adjectives, no padding, no summary sentences that repeat the previous one.
 D4. Vary rhythm naturally (a shorter direct sentence after a long one is good); never above {MAX_WORDS} words
     unless the original sentence already was; never start with And, But, So or Also.
E. Flow: each sentence follows logically from the previous one; add a linking word (however, therefore, in
   contrast, for example) only where the logic needs it; never add a new claim to make text flow.
F. Consistency: keep the author's spelling (British or American), terminology, tense and number format throughout;
   the same concept always has the same name."""