""" Triage proposed cuts before a human sets final boundaries. Two checks, both grounded in the case's own record terms: END — does the paragraph after the cut still read like this case's article (under-cut), or does the last kept paragraph carry no case signal (possible teaser over-cut)? INNER — is there an ad or teaser wedged INSIDE the kept range (the end-only check misses these)? Prints only what needs a human's eyes, with the surrounding lines. Run propose_cuts.py first. """ import io import json import re import sys from pathlib import Path sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding="utf-8", errors="replace") BASE = Path(__file__).resolve().parent sys.path.insert(0, str(BASE)) from propose_cuts import case_terms, score # noqa: E402 STORE = json.loads((BASE / "sourced-articles.json").read_text(encoding="utf-8")) CUTS = json.loads((BASE / "staging" / "proposed-cuts.json").read_text(encoding="utf-8")) RECS = {r["id"]: r for r in json.loads((BASE.parent / "attacks-export-Gart-website.json").read_text(encoding="utf-8"))} # Ad / teaser markers. A hit only matters on a paragraph that carries NO case term, so # these substrings flagging inside real prose are dropped by the score==0 guard. AD = re.compile(r"(bitpanda|etoro|\bkraken\b|\bledger\b|binance|bitvavo|bybit|coinbase\b|" r"cashback|\bbonus\b|publicité|newsletter|whatsapp|telegram|s'inscrire|" r"inscrivez|abonn|👉|🔔|🔒|🗞|📰|disclosure|disclaimer|sponsored|affiliate|" r"loading more|read the latest|delivered free|was this writing|" r"next article|previous article|recevez|découvrez)", re.I) def clip(s, n=90): return " ".join(str(s).split())[:n] def field_of(cid): return {b["staged_file"]: b["field"] for b in STORE[str(cid)] if b.get("staged_file")} def main(): batch = {int(a) for a in sys.argv[1:]} total = flagged = 0 for key in sorted(CUTS, key=lambda k: (-int(k.split(":")[0]), k)): cid = int(key.split(":")[0]) if cid not in batch: continue total += 1 c = CUTS[key] fn = key.split(":")[1] field = field_of(cid).get(fn, "?") terms = case_terms(RECS[cid]) paras = (BASE / "staging" / f"{cid:03d}" / fn).read_text(encoding="utf-8").split("\n\n") s, e = c["start"], c["end"] if s is None or e is None: print(f"{cid} {field:6} {fn.split('_',1)[1].replace('.txt',''):20} " f"NO BODY RUN — off-case or all-furniture page <<< INSPECT") flagged += 1 continue probs = [] if score(paras[e], terms) == 0 and len(paras[e]) > 60: probs.append("LATE?") nxt = paras[e + 1] if e + 1 < len(paras) else "" if nxt and score(nxt, terms) >= 2: probs.append("EARLY?") if c["case_term_hits"] < 4: probs.append("WEAK") inner = [i for i in range(s, e + 1) if score(paras[i], terms) == 0 and AD.search(paras[i])] line = (f"{cid} {field:6} {fn.split('_',1)[1].replace('.txt',''):20} " f"{'t+' if c.get('title') is not None else ''}{s}..{e:<4} h={c['case_term_hits']}") if probs or inner: flagged += 1 print(line, " <<<", "|".join(probs) + (f" INNER@{inner}" if inner else "")) print(f" last[{e}]: {clip(paras[e])}") if "EARLY?" in probs: print(f" next[{e+1}]: {clip(nxt)}") for i in inner: print(f" inner[{i}]: {clip(paras[i])}") else: print(line) print(f"\n{total} articles | {flagged} need a human look") if __name__ == "__main__": main()