diff --git a/scripts/r49-corpus/audit_pairs_sourcenames.py b/scripts/r49-corpus/audit_pairs_sourcenames.py index b1bc598..3f16b84 100644 --- a/scripts/r49-corpus/audit_pairs_sourcenames.py +++ b/scripts/r49-corpus/audit_pairs_sourcenames.py @@ -65,6 +65,16 @@ def main() -> int: help="rename.py's renameable threshold; mirrored from leak_gate.py") ap.add_argument("--report", default=None, help="write the full JSON breakdown here") ap.add_argument("--show", type=int, default=25, help="example beats to print") + ap.add_argument("--filter-out", default=None, metavar="PATH", + help="write a copy of the pairs with every contaminated ROW removed. " + "Turns this detector into the fix for an already-built pair set: " + "the contamination is in the beat, so dropping the row removes it " + "outright, and on Hemingway that costs 0.96%% of the training data. " + "Cheaper and cleaner than regenerating 70 beats against a second " + "generator session, which would leave the set mixed-provenance. " + "Refuses to write when a RESPONSE is contaminated -- that is a " + "different fault (pairs built against an unrenamed corpus) and " + "dropping rows would hide it rather than fix it.") a = ap.parse_args() ents_all = json.loads(Path(a.entities).read_text()) @@ -157,6 +167,40 @@ def main() -> int: }, ensure_ascii=False, indent=2), encoding="utf-8") print(f"\n wrote {a.report}") + if a.filter_out: + if nr: + print("\n== REFUSING to write a filtered copy: the RESPONSE column is contaminated.") + print(" That is not the beat-generator fault and dropping rows would hide it.") + print(" Check the provenance `corpus` path -- the pairs were probably built") + print(" against an unrenamed corpus, and the fix is upstream of this file.") + return 2 + if not (pos_ok and neg_ok): + print("\n== REFUSING to write a filtered copy: the controls did not pass, so the") + print(" set of rows to drop is not trustworthy.") + return 2 + if len({r["_src"] for r in rows}) != 1: + print("\n== REFUSING to write a filtered copy from more than one --pairs file:") + print(" the output is a single file and would silently merge train and val.") + print(" Filter each input separately.") + return 2 + drop = set() + for i, r in enumerate(rows): + if removed_pat.search(r.get("beat") or ""): + drop.add(i) + out = Path(a.filter_out) + kept = 0 + with out.open("w", encoding="utf-8") as fh: + for i, r in enumerate(rows): + if i in drop: + continue + r = {k: v for k, v in r.items() if k != "_src"} + fh.write(json.dumps(r, ensure_ascii=False) + "\n") + kept += 1 + print(f"\n wrote {out}: {kept} pairs kept, {len(drop)} dropped " + f"({len(drop) / len(rows):.2%})") + print(" ⚠ Re-run this audit against the filtered file before training on it. A fix") + print(" that is not read back is a claim, not a result.") + ok = pos_ok and neg_ok and nb == 0 and nr == 0 print(f"\n GATE: {'PASS' if ok else 'FAIL'}") return 0 if ok else 2