File size: 5,789 Bytes
cbc33fe
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
"""
Programmatic quality gates. Free, deterministic, zero judge variance.

Contract: a story that FAILS any hard gate gets total reward 0, regardless of
what the judge said. Gates run before the judge so we can skip paying for
obviously-broken generations.

This module exists because of a specific autopsy finding. The prior run's
completeness check was:

    enders = ".!?\\"'"
    text[-1] in enders

which uses straight quotes only. The policy emits curly U+201D constantly, so
properly-ended stories were scored 0.3 instead of 1.0. Worse, 100% of prior-run
eval samples were truncated mid-word by a max_new_tokens wall, and the judge
rubric capped quality at 4 for truncation -- collapsing within-group quality
variance to ~0, which in GRPO means zero advantage. The quality signal was
silently dead for the entire run. Everything here is built to make that
failure loud instead of silent.
"""

from __future__ import annotations

import math
import re
import unicodedata
from dataclasses import dataclass, asdict, field

# Sentence terminators, including the curly/CJK variants the prior run missed.
TERMINALS = set('.!?…' + '"”’\'' + '。!?' + ')]}*_')
# Characters allowed to trail a real terminator (markdown emphasis, quotes).
_TRAILING_DECOR = set('*_`"”’\'» )]}')

MIN_WORDS = 150
MAX_WORDS = 600
MAX_NGRAM_REPEAT_FRAC = 0.18   # frac of 4-grams that are repeats
MIN_CHAR_ENTROPY = 3.2         # bits/char; English prose sits ~4.0-4.4
MAX_NONASCII_FRAC = 0.08       # em-dashes/curly quotes are fine; CJK dumps are not
MAX_LINE_REPEAT_FRAC = 0.25


@dataclass
class GateResult:
    passed: bool
    completeness: float                    # 1.0 ended cleanly, 0.0 truncated
    n_words: int
    reasons: list[str] = field(default_factory=list)
    # diagnostics, logged not rewarded
    repeat_4gram_frac: float = 0.0
    char_entropy: float = 0.0
    nonascii_frac: float = 0.0
    hit_token_cap: bool = False

    def as_dict(self) -> dict:
        return asdict(self)


def _words(text: str) -> list[str]:
    return text.split()


def _norm_tokens(text: str) -> list[str]:
    return re.sub(r"[^\w\s]", " ", text.lower()).split()


def ends_cleanly(text: str) -> bool:
    """True if the story ends on a sentence terminator.

    Strips trailing markdown/quote decoration first, so `...over.*` and
    `...done."` and `...gone.”` all count. Also rejects the specific
    mid-word cutoff signature: last token is a bare word with no terminator.
    """
    t = text.rstrip()
    if not t:
        return False
    while t and t[-1] in _TRAILING_DECOR:
        t = t[:-1].rstrip()
        if t and t[-1] in '.!?…。!?':
            return True
    return bool(t) and t[-1] in '.!?…。!?'


def char_entropy(text: str) -> float:
    """Shannon entropy over characters, bits/char."""
    if not text:
        return 0.0
    counts: dict[str, int] = {}
    for ch in text:
        counts[ch] = counts.get(ch, 0) + 1
    n = len(text)
    return -sum((c / n) * math.log2(c / n) for c in counts.values())


def repeat_ngram_frac(text: str, n: int = 4) -> float:
    """Fraction of n-grams that are non-first occurrences. Catches loops."""
    toks = _norm_tokens(text)
    if len(toks) < n + 1:
        return 0.0
    grams = [tuple(toks[i:i + n]) for i in range(len(toks) - n + 1)]
    return 1.0 - (len(set(grams)) / len(grams))


def repeat_line_frac(text: str) -> float:
    """Fraction of non-trivial lines that are duplicates. Catches list loops."""
    lines = [l.strip() for l in text.splitlines() if len(l.strip()) > 12]
    if len(lines) < 4:
        return 0.0
    return 1.0 - (len(set(lines)) / len(lines))


def nonascii_frac(text: str) -> float:
    """Fraction of chars outside Latin/punctuation. Typographic marks exempt."""
    if not text:
        return 0.0
    bad = 0
    for ch in text:
        if ord(ch) < 128:
            continue
        if unicodedata.category(ch) in ("Pd", "Pi", "Pf", "Po", "Zs", "Sm"):
            continue  # em dash, curly quotes, ellipsis, nbsp
        bad += 1
    return bad / len(text)


def check(
    text: str,
    *,
    finish_reason: str | None = None,
    min_words: int = MIN_WORDS,
    max_words: int = MAX_WORDS,
) -> GateResult:
    """Run all gates. `finish_reason` from vLLM/OpenAI ('length' == hit cap).

    Passing `finish_reason` is strongly preferred: it detects the token-cap
    truncation that destroyed the prior run directly, rather than inferring it
    from punctuation.
    """
    text = (text or "").strip()
    reasons: list[str] = []

    n_words = len(_words(text))
    hit_cap = finish_reason == "length"
    complete = ends_cleanly(text) and not hit_cap

    rep4 = repeat_ngram_frac(text, 4)
    repline = repeat_line_frac(text)
    ent = char_entropy(text)
    na = nonascii_frac(text)

    if not text:
        reasons.append("empty")
    if hit_cap:
        reasons.append("hit_token_cap")
    if not ends_cleanly(text):
        reasons.append("no_terminal_punctuation")
    if n_words < min_words:
        reasons.append(f"too_short({n_words}<{min_words})")
    if n_words > max_words:
        reasons.append(f"too_long({n_words}>{max_words})")
    if rep4 > MAX_NGRAM_REPEAT_FRAC:
        reasons.append(f"ngram_loop({rep4:.2f})")
    if repline > MAX_LINE_REPEAT_FRAC:
        reasons.append(f"line_loop({repline:.2f})")
    if text and ent < MIN_CHAR_ENTROPY:
        reasons.append(f"low_entropy({ent:.2f})")
    if na > MAX_NONASCII_FRAC:
        reasons.append(f"nonascii({na:.2f})")

    return GateResult(
        passed=not reasons,
        completeness=1.0 if complete else 0.0,
        n_words=n_words,
        reasons=reasons,
        repeat_4gram_frac=rep4,
        char_entropy=ent,
        nonascii_frac=na,
        hit_token_cap=hit_cap,
    )