Audit 2026-09-28 (evening): a late author response is never overwritten by the window check, an accept waits for a panel report, a review of a paper that studies models passes the redaction gate, a data sentence does not take back a code claim, the stats snapshot never shrinks
Browse files- code_availability.py +11 -1
- crossref_deposit.py +16 -4
- intake/apply_decision.py +8 -0
- intake/author_approval.py +61 -29
- intake/scripts/test_audit_20260928b.py +122 -0
- redaction.py +4 -3
code_availability.py
CHANGED
|
@@ -38,6 +38,9 @@ _CLAIM_PATTERNS = [
|
|
| 38 |
r"\b(?:archive|repository)\s+(?:contains|includes|provides)\b",
|
| 39 |
]
|
| 40 |
_CLAIM_RE = re.compile("|".join(f"(?:{p})" for p in _CLAIM_PATTERNS), re.I)
|
|
|
|
|
|
|
|
|
|
| 41 |
|
| 42 |
# Said right after an availability heading, these mean there is nothing to link.
|
| 43 |
_NOTHING_TO_SHARE = re.compile(
|
|
@@ -81,7 +84,14 @@ def find_claims(text: str) -> list[str]:
|
|
| 81 |
"""Availability statements in `text`, one quoted sentence each."""
|
| 82 |
out: list[str] = []
|
| 83 |
for m in _CLAIM_RE.finditer(text):
|
| 84 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 85 |
continue
|
| 86 |
quote = _sentence(text, m.start(), m.end())
|
| 87 |
if quote not in out:
|
|
|
|
| 38 |
r"\b(?:archive|repository)\s+(?:contains|includes|provides)\b",
|
| 39 |
]
|
| 40 |
_CLAIM_RE = re.compile("|".join(f"(?:{p})" for p in _CLAIM_PATTERNS), re.I)
|
| 41 |
+
# The first pattern is a heading ("Data availability"); its answer follows it.
|
| 42 |
+
_HEADING_RE = re.compile(_CLAIM_PATTERNS[0], re.I)
|
| 43 |
+
_SENTENCE_END = re.compile(r"[.!?](?:\s|$)")
|
| 44 |
|
| 45 |
# Said right after an availability heading, these mean there is nothing to link.
|
| 46 |
_NOTHING_TO_SHARE = re.compile(
|
|
|
|
| 84 |
"""Availability statements in `text`, one quoted sentence each."""
|
| 85 |
out: list[str] = []
|
| 86 |
for m in _CLAIM_RE.finditer(text):
|
| 87 |
+
after = text[m.end(): m.end() + 120]
|
| 88 |
+
if not _HEADING_RE.fullmatch(m.group(0)):
|
| 89 |
+
# "Code is available. No new data were generated." The second
|
| 90 |
+
# sentence is about data; it does not take back the code claim.
|
| 91 |
+
end = _SENTENCE_END.search(after)
|
| 92 |
+
if end:
|
| 93 |
+
after = after[: end.start()]
|
| 94 |
+
if _NOTHING_TO_SHARE.search(after):
|
| 95 |
continue
|
| 96 |
quote = _sentence(text, m.start(), m.end())
|
| 97 |
if quote not in out:
|
crossref_deposit.py
CHANGED
|
@@ -905,12 +905,24 @@ def _push_publications(sub_dir: Path, submission: dict, doi: str, *, log) -> dic
|
|
| 905 |
f"the landing page will show none")
|
| 906 |
# The public stats snapshot rode only the legacy Zenodo path and had stood
|
| 907 |
# still since May (audit 2026-09-28 pass D); refresh it with every publication.
|
|
|
|
|
|
|
|
|
|
| 908 |
try:
|
| 909 |
import stats as _stats
|
| 910 |
-
|
| 911 |
-
|
| 912 |
-
|
| 913 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 914 |
except Exception as exc: # the stats page must never block a registration
|
| 915 |
log(f" crossref: stats snapshot not refreshed ({exc})")
|
| 916 |
publications.commit_and_push(message=f"publications: {entry['title']} ({entry['slug']}) β {doi}",
|
|
|
|
| 905 |
f"the landing page will show none")
|
| 906 |
# The public stats snapshot rode only the legacy Zenodo path and had stood
|
| 907 |
# still since May (audit 2026-09-28 pass D); refresh it with every publication.
|
| 908 |
+
# The review files behind the May snapshot left reviews/ at the public flip,
|
| 909 |
+
# so a fresh count can be LOWER than the published one; a snapshot that
|
| 910 |
+
# would shrink the total is never written (audit 2026-09-28, evening).
|
| 911 |
try:
|
| 912 |
import stats as _stats
|
| 913 |
+
out = os.path.join(publications.WEBSITE_REPO, "src", "data", "stats.json")
|
| 914 |
+
fresh = _stats.compute_stats(config.REVIEWS_DIR)
|
| 915 |
+
try:
|
| 916 |
+
published = json.loads(Path(out).read_text()).get("total_reviewed") or 0
|
| 917 |
+
except Exception:
|
| 918 |
+
published = 0
|
| 919 |
+
if (fresh.get("total_reviewed") or 0) < published:
|
| 920 |
+
log(f" crossref: stats snapshot kept: a fresh count of {fresh.get(total_reviewed)} "
|
| 921 |
+
f"would replace the published {published}")
|
| 922 |
+
else:
|
| 923 |
+
stats_path = _stats.write_stats(config.REVIEWS_DIR, out)
|
| 924 |
+
if stats_path:
|
| 925 |
+
extra.append(stats_path)
|
| 926 |
except Exception as exc: # the stats page must never block a registration
|
| 927 |
log(f" crossref: stats snapshot not refreshed ({exc})")
|
| 928 |
publications.commit_and_push(message=f"publications: {entry['title']} ({entry['slug']}) β {doi}",
|
intake/apply_decision.py
CHANGED
|
@@ -168,6 +168,14 @@ def main(argv: list[str]) -> int:
|
|
| 168 |
code_link_claims = []
|
| 169 |
|
| 170 |
panel_md, rqc_md = worker._scrubbed_report_pair(sub_id, title, tier=tier)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 171 |
deposit_doi = state_pre.get("deposit_doi")
|
| 172 |
deposit_url = state_pre.get("deposit_url")
|
| 173 |
|
|
|
|
| 168 |
code_link_claims = []
|
| 169 |
|
| 170 |
panel_md, rqc_md = worker._scrubbed_report_pair(sub_id, title, tier=tier)
|
| 171 |
+
# The acceptance email attaches the panel report and the record publishes it:
|
| 172 |
+
# an accept with none (the panel never ran, e.g. right after a code-check
|
| 173 |
+
# override, or the redaction gate dropped it) waits (audit 2026-09-28).
|
| 174 |
+
if verdict == "accept" and not panel_md.strip() and not force:
|
| 175 |
+
print(f"{sub_id} has no publishable panel report (not reviewed yet, or the redaction "
|
| 176 |
+
f"gate dropped it: see the curator alert). Nothing done. Decide after the panel, "
|
| 177 |
+
f"or run with ICSAC_DECISION_FORCE=1 to accept without the report.", file=sys.stderr)
|
| 178 |
+
return 3
|
| 179 |
deposit_doi = state_pre.get("deposit_doi")
|
| 180 |
deposit_url = state_pre.get("deposit_url")
|
| 181 |
|
intake/author_approval.py
CHANGED
|
@@ -23,7 +23,10 @@ author_hold_note.
|
|
| 23 |
"""
|
| 24 |
from __future__ import annotations
|
| 25 |
|
|
|
|
| 26 |
import datetime as _dt
|
|
|
|
|
|
|
| 27 |
import hashlib
|
| 28 |
import json
|
| 29 |
import re
|
|
@@ -107,7 +110,24 @@ def _load(sub_dir: Path) -> Optional[dict]:
|
|
| 107 |
|
| 108 |
|
| 109 |
def _save(sub_dir: Path, rec: dict) -> None:
|
| 110 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 111 |
|
| 112 |
|
| 113 |
def is_test(sub_id: str) -> bool:
|
|
@@ -227,31 +247,39 @@ def record(sub_dir: Path, rec: dict, choice: str, note: str = "",
|
|
| 227 |
raise ValueError(f"unknown exclusion(s): {bad}")
|
| 228 |
if choice != "approve" and excl:
|
| 229 |
excl = [] # exclusions only mean something on an approval
|
| 230 |
-
|
| 231 |
-
|
| 232 |
-
|
| 233 |
-
|
| 234 |
-
|
| 235 |
-
|
| 236 |
-
|
| 237 |
-
|
| 238 |
-
|
| 239 |
-
|
| 240 |
-
|
| 241 |
-
|
| 242 |
-
|
| 243 |
-
|
| 244 |
-
|
| 245 |
-
|
| 246 |
-
"
|
| 247 |
-
|
| 248 |
-
|
| 249 |
-
"
|
| 250 |
-
"
|
| 251 |
-
|
| 252 |
-
|
| 253 |
-
|
| 254 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 255 |
if audit:
|
| 256 |
try:
|
| 257 |
audit({"sub_id": sub_id, "event": "author_response", "choice": choice,
|
|
@@ -324,9 +352,13 @@ def check_windows(*, now: Optional[_dt.datetime] = None) -> int:
|
|
| 324 |
f"Accepted {str(st.get('completed_at', '?'))[:10]}; the author has not responded by "
|
| 325 |
f"{rec['deadline'][:10]}. Your call: intake/register-doi.sh {sub_dir.name} --live"):
|
| 326 |
continue
|
| 327 |
-
|
| 328 |
-
|
| 329 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 330 |
pinged += 1
|
| 331 |
return pinged
|
| 332 |
|
|
|
|
| 23 |
"""
|
| 24 |
from __future__ import annotations
|
| 25 |
|
| 26 |
+
import contextlib
|
| 27 |
import datetime as _dt
|
| 28 |
+
import fcntl
|
| 29 |
+
import os
|
| 30 |
import hashlib
|
| 31 |
import json
|
| 32 |
import re
|
|
|
|
| 110 |
|
| 111 |
|
| 112 |
def _save(sub_dir: Path, rec: dict) -> None:
|
| 113 |
+
p = Path(sub_dir) / FILE
|
| 114 |
+
tmp = p.with_name(p.name + ".tmp")
|
| 115 |
+
tmp.write_text(json.dumps(rec, indent=2) + "\n")
|
| 116 |
+
os.replace(tmp, p)
|
| 117 |
+
|
| 118 |
+
|
| 119 |
+
@contextlib.contextmanager
|
| 120 |
+
def _locked(sub_dir: Path):
|
| 121 |
+
"""One writer at a time per paper: the intake server records responses while
|
| 122 |
+
the batch tick marks closed windows, and a stale read in either would overwrite
|
| 123 |
+
the other's write (audit 2026-09-28)."""
|
| 124 |
+
fd = os.open(str(Path(sub_dir) / (FILE + ".lock")), os.O_CREAT | os.O_RDWR, 0o600)
|
| 125 |
+
try:
|
| 126 |
+
fcntl.flock(fd, fcntl.LOCK_EX)
|
| 127 |
+
yield
|
| 128 |
+
finally:
|
| 129 |
+
fcntl.flock(fd, fcntl.LOCK_UN)
|
| 130 |
+
os.close(fd)
|
| 131 |
|
| 132 |
|
| 133 |
def is_test(sub_id: str) -> bool:
|
|
|
|
| 247 |
raise ValueError(f"unknown exclusion(s): {bad}")
|
| 248 |
if choice != "approve" and excl:
|
| 249 |
excl = [] # exclusions only mean something on an approval
|
| 250 |
+
with _locked(sub_dir):
|
| 251 |
+
cur = _load(sub_dir)
|
| 252 |
+
if cur is not None:
|
| 253 |
+
if cur.get("token_sha256") != rec.get("token_sha256"):
|
| 254 |
+
raise Locked("this link is no longer valid; write to help@icsacinstitute.org "
|
| 255 |
+
"with your submission ID in the subject line")
|
| 256 |
+
rec.clear()
|
| 257 |
+
rec.update(cur)
|
| 258 |
+
st = _state(sub_dir)
|
| 259 |
+
if st.get("crossref_registered_at"):
|
| 260 |
+
raise Locked("the DOI is already registered; write to help@icsacinstitute.org for a correction")
|
| 261 |
+
if rec.get("status", "pending") != "pending" or rec.get("responses"):
|
| 262 |
+
# One response per paper (policy since 2026-09-28): the first click is the
|
| 263 |
+
# instruction of record; anything after it goes through help@.
|
| 264 |
+
raise Locked("your response is already on record; for any change write to "
|
| 265 |
+
"help@icsacinstitute.org with your submission ID in the subject line")
|
| 266 |
+
entry = {"at": _iso(_now()), "choice": choice, "exclusions": excl, "note": note,
|
| 267 |
+
"quote_ok": bool(quote_ok) and choice == "approve",
|
| 268 |
+
"orcid": (orcid or "").strip()} # the iD that signed this response (2026-09-28)
|
| 269 |
+
rec.setdefault("responses", []).append(entry)
|
| 270 |
+
rec["status"] = CHOICES[choice]
|
| 271 |
+
_save(sub_dir, rec)
|
| 272 |
+
fields = {
|
| 273 |
+
"author_approval_status": rec["status"],
|
| 274 |
+
"author_exclusions": excl,
|
| 275 |
+
"promotion_opt_out": bool(set(excl) & PROMOTION_IDS),
|
| 276 |
+
"persistence_opt_out": "persistence" in excl,
|
| 277 |
+
"withdrawn_by_author": choice == "withdraw",
|
| 278 |
+
"author_quote_ok": entry["quote_ok"],
|
| 279 |
+
}
|
| 280 |
+
if choice in ("hold", "withdraw"):
|
| 281 |
+
fields["author_hold_note"] = note
|
| 282 |
+
_update_state(sub_dir, **fields)
|
| 283 |
if audit:
|
| 284 |
try:
|
| 285 |
audit({"sub_id": sub_id, "event": "author_response", "choice": choice,
|
|
|
|
| 352 |
f"Accepted {str(st.get('completed_at', '?'))[:10]}; the author has not responded by "
|
| 353 |
f"{rec['deadline'][:10]}. Your call: intake/register-doi.sh {sub_dir.name} --live"):
|
| 354 |
continue
|
| 355 |
+
with _locked(sub_dir):
|
| 356 |
+
# Re-read: the author may have answered while the ping was in flight.
|
| 357 |
+
cur = _load(sub_dir)
|
| 358 |
+
if cur and cur.get("status") == "pending" and not cur.get("window_closed_pinged"):
|
| 359 |
+
cur["window_closed_pinged"] = True
|
| 360 |
+
cur["window_closed_pinged_at"] = _iso(now)
|
| 361 |
+
_save(sub_dir, cur)
|
| 362 |
pinged += 1
|
| 363 |
return pinged
|
| 364 |
|
intake/scripts/test_audit_20260928b.py
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""Regression checks for the second audit of 2026-09-28 (the evening sweep).
|
| 3 |
+
Offline: no network, no mail, no pings; every write lands in one temporary
|
| 4 |
+
directory that is removed at the end, pass or fail. Run from the repo root:
|
| 5 |
+
.venv/bin/python intake/scripts/test_audit_20260928b.py
|
| 6 |
+
|
| 7 |
+
Covers: a late author response is never overwritten by the window check; a
|
| 8 |
+
second response still refuses; the code claim is not taken back by a data
|
| 9 |
+
sentence; the redaction gate passes a review of a paper that studies models and
|
| 10 |
+
still stops seat talk; accept waits for a panel report.
|
| 11 |
+
"""
|
| 12 |
+
from __future__ import annotations
|
| 13 |
+
|
| 14 |
+
import json
|
| 15 |
+
import shutil
|
| 16 |
+
import sys
|
| 17 |
+
import tempfile
|
| 18 |
+
from pathlib import Path
|
| 19 |
+
|
| 20 |
+
ROOT = Path(__file__).resolve().parents[2]
|
| 21 |
+
sys.path.insert(0, str(ROOT))
|
| 22 |
+
|
| 23 |
+
failures: list[str] = []
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
def check(cond: bool, name: str) -> None:
|
| 27 |
+
print((" ok " if cond else " FAIL ") + name)
|
| 28 |
+
if not cond:
|
| 29 |
+
failures.append(name)
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
tmp = Path(tempfile.mkdtemp(prefix="icsac-audit-b-"))
|
| 33 |
+
try:
|
| 34 |
+
# ββ author approval: the window check and a late response ββββββββββββββ
|
| 35 |
+
from intake import author_approval as aa
|
| 36 |
+
aa.SUBMISSIONS_ROOT = tmp
|
| 37 |
+
sub = tmp / "ICSAC-SUB-00099"
|
| 38 |
+
sub.mkdir()
|
| 39 |
+
(sub / "state.json").write_text(json.dumps({"state": "completed", "decision": "accept",
|
| 40 |
+
"completed_at": "2026-09-01T00:00:00Z"}))
|
| 41 |
+
rec = {"token_sha256": "abc", "issued_at": "2026-09-01T00:00:00Z",
|
| 42 |
+
"deadline": "2026-09-08T00:00:00Z", "status": "pending", "responses": [],
|
| 43 |
+
"window_closed_pinged": False}
|
| 44 |
+
aa._save(sub, rec)
|
| 45 |
+
sent = []
|
| 46 |
+
|
| 47 |
+
def ping_while_author_answers(msg: str) -> bool:
|
| 48 |
+
# The author's late approval lands while the curator ping is in flight.
|
| 49 |
+
sent.append(msg)
|
| 50 |
+
if len(sent) == 1: # record() pings the curator too; answer only once
|
| 51 |
+
aa.record(sub, aa._load(sub), "approve", "", [], orcid="0000-0000-0000-0001")
|
| 52 |
+
return True
|
| 53 |
+
|
| 54 |
+
real_ping = aa._ping
|
| 55 |
+
aa._ping = ping_while_author_answers
|
| 56 |
+
try:
|
| 57 |
+
aa.check_windows()
|
| 58 |
+
finally:
|
| 59 |
+
aa._ping = real_ping
|
| 60 |
+
final = aa._load(sub)
|
| 61 |
+
check(final["status"] == "approved" and len(final["responses"]) == 1,
|
| 62 |
+
"a response recorded during the window ping survives the tick")
|
| 63 |
+
check(not final.get("window_closed_pinged"), "the tick does not mark an answered paper as closed in silence")
|
| 64 |
+
try:
|
| 65 |
+
aa.record(sub, aa._load(sub), "withdraw", "x" * 40, [], orcid="0000-0000-0000-0001")
|
| 66 |
+
check(False, "a second response is refused")
|
| 67 |
+
except aa.Locked:
|
| 68 |
+
check(True, "a second response is refused")
|
| 69 |
+
stale = dict(final, token_sha256="other")
|
| 70 |
+
(sub / aa.FILE).write_text(json.dumps(dict(final, status="pending", responses=[])))
|
| 71 |
+
try:
|
| 72 |
+
aa.record(sub, stale, "approve", "", [], orcid="0000-0000-0000-0001")
|
| 73 |
+
check(False, "a response through a re-issued link's old token is refused")
|
| 74 |
+
except aa.Locked:
|
| 75 |
+
check(True, "a response through a re-issued link's old token is refused")
|
| 76 |
+
check(not list(sub.glob("*.tmp")), "no temporary file is left beside the record")
|
| 77 |
+
|
| 78 |
+
# ββ code claims βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 79 |
+
import code_availability as ca
|
| 80 |
+
check(len(ca.find_claims("The code is available on request. No new data were generated.")) == 1,
|
| 81 |
+
"a data sentence after a code claim does not take the claim back")
|
| 82 |
+
check(ca.find_claims("Data availability\nNot applicable.\n") == [],
|
| 83 |
+
"a heading answered 'Not applicable' is still no claim")
|
| 84 |
+
check(ca.find_claims("Data availability: no new data were generated in this study.") == [],
|
| 85 |
+
"a heading answered 'no new data' is still no claim")
|
| 86 |
+
check(len(ca.find_claims("Code availability: the scripts are available on request.")) >= 1,
|
| 87 |
+
"a heading followed by a real statement is a claim")
|
| 88 |
+
|
| 89 |
+
# ββ redaction: models studied vs seats named ββββββββββββββββββββββββββββ
|
| 90 |
+
import redaction
|
| 91 |
+
try:
|
| 92 |
+
redaction.assert_clean("The authors compare the GPT model with two Llama models and a Gemini model.")
|
| 93 |
+
check(True, "a review of a paper that studies models passes the gate")
|
| 94 |
+
except redaction.RedactionLeak:
|
| 95 |
+
check(False, "a review of a paper that studies models passes the gate")
|
| 96 |
+
for leak in ("The Claude-position reviewers agree.", "the Qwen slot scored it lower", "a GPT reviewer"):
|
| 97 |
+
try:
|
| 98 |
+
redaction.assert_clean(leak)
|
| 99 |
+
check(False, f"seat talk is fatal: {leak!r}")
|
| 100 |
+
except redaction.RedactionLeak:
|
| 101 |
+
check(True, f"seat talk is fatal: {leak!r}")
|
| 102 |
+
|
| 103 |
+
# ββ accept waits for a panel report βββββββββββββββββββββββββββββββββββββ
|
| 104 |
+
src = (ROOT / "intake" / "apply_decision.py").read_text()
|
| 105 |
+
i = src.find("panel_md, rqc_md = worker._scrubbed_report_pair(")
|
| 106 |
+
j = src.find('if verdict == "accept" and not panel_md.strip() and not force:')
|
| 107 |
+
k = src.find("publications_url_str: str | None = None")
|
| 108 |
+
check(0 < i < j < k, "the no-report guard sits before the first side effect of an accept")
|
| 109 |
+
|
| 110 |
+
# ββ the stats snapshot never shrinks ββββββββββββββββββββββββββββββββββββ
|
| 111 |
+
import stats
|
| 112 |
+
rdir = tmp / "reviews"; rdir.mkdir()
|
| 113 |
+
check(stats.compute_stats(str(rdir)).get("total_reviewed") == 0, "an empty reviews dir counts zero")
|
| 114 |
+
cd = (ROOT / "crossref_deposit.py").read_text()
|
| 115 |
+
g = cd.find("if (fresh.get(\"total_reviewed\") or 0) < published:")
|
| 116 |
+
w = cd.find("stats_path = _stats.write_stats(config.REVIEWS_DIR, out)")
|
| 117 |
+
check(0 < g < w, "a fresh count below the published total is never written")
|
| 118 |
+
finally:
|
| 119 |
+
shutil.rmtree(tmp, ignore_errors=True)
|
| 120 |
+
|
| 121 |
+
print(f"\n{'ALL GREEN' if not failures else f'{len(failures)} FAILURE(S)'}")
|
| 122 |
+
sys.exit(1 if failures else 0)
|
redaction.py
CHANGED
|
@@ -92,9 +92,10 @@ SOFT_WARN_TOKENS: tuple[str, ...] = (
|
|
| 92 |
# Panel-composition talk in reviewer or RQC prose: a vendor or model name tied
|
| 93 |
# to a seat ("Claude-position reviewers", "the GPT slot"). A paper's own AI-use
|
| 94 |
# disclosure never takes this shape, so it is fatal wherever it appears, prose
|
| 95 |
-
# included (audit 2026-09-28 item 4).
|
|
|
|
| 96 |
FORBIDDEN_PANEL_PATTERNS: tuple[str, ...] = (
|
| 97 |
-
r"(?i)\b(?:claude|anthropic|gpt|chatgpt|openai|gemini|gemma|qwen|deepseek|llama|mistral|nemotron|sonnet|opus)[- ](?:position|seat|slot|reviewer
|
| 98 |
)
|
| 99 |
|
| 100 |
FORBIDDEN_EXFIL_PATTERNS: tuple[str, ...] = (
|
|
@@ -756,7 +757,7 @@ def build_public_rqc_markdown(parsed: ParsedRQC) -> str:
|
|
| 756 |
enforces this before the redaction writes anything to the site.
|
| 757 |
"""
|
| 758 |
status_line = (
|
| 759 |
-
"Review Quality Control: flagged β reviewed by
|
| 760 |
if parsed.flag
|
| 761 |
else "Review Quality Control: passed."
|
| 762 |
)
|
|
|
|
| 92 |
# Panel-composition talk in reviewer or RQC prose: a vendor or model name tied
|
| 93 |
# to a seat ("Claude-position reviewers", "the GPT slot"). A paper's own AI-use
|
| 94 |
# disclosure never takes this shape, so it is fatal wherever it appears, prose
|
| 95 |
+
# included (audit 2026-09-28 item 4). "Model" is not a seat word: a review of a
|
| 96 |
+
# paper that studies "the GPT model" or "Llama models" must not trip the gate.
|
| 97 |
FORBIDDEN_PANEL_PATTERNS: tuple[str, ...] = (
|
| 98 |
+
r"(?i)\b(?:claude|anthropic|gpt|chatgpt|openai|gemini|gemma|qwen|deepseek|llama|mistral|nemotron|sonnet|opus)[- ](?:position|seat|slot|reviewer)s?\b",
|
| 99 |
)
|
| 100 |
|
| 101 |
FORBIDDEN_EXFIL_PATTERNS: tuple[str, ...] = (
|
|
|
|
| 757 |
enforces this before the redaction writes anything to the site.
|
| 758 |
"""
|
| 759 |
status_line = (
|
| 760 |
+
"Review Quality Control: flagged β reviewed by the curation team before acceptance."
|
| 761 |
if parsed.flag
|
| 762 |
else "Review Quality Control: passed."
|
| 763 |
)
|