"""Apply hand-verified genders to an entity map — the human pass the pipeline asks for. `entities.py` says it plainly: "Nothing here guesses. Unresolved entities block corpus emission and go to a human pass: held is cheap, wrong is poison -- a silently mis-gendered entity scrambles pronoun agreement through every renamed copy and nothing downstream would catch it." This is that pass, written down instead of typed into a JSON by hand. ⚠ RAW PRONOUN COUNTS ARE THE TRAP, AND THIS IS WHY EVERY OVERRIDE CARRIES ITS EVIDENCE. Measured on McCarthy: the corpus runs 29,144 male pronouns to 5,036 female, a base rate of **85.3% male**. So a name sitting at 31 male / 21 female nearby is not "male-dominated" — at the base rate it would be 44/8, and 21 female against an expected 8 is a strong FEMALE signal. Carla Jean Moss was read male by the honorific/window resolver for exactly that reason. The same arithmetic recovered Pilar and Brett on Hemingway. TWO REFUSALS, because an override file is a place where a typo is invisible: * a name not present in the entity map is an ERROR, not a no-op. A silent skip means a misspelled override looks like it applied and the entity stays mis-gendered. * changing a gender the map already holds requires `"correcting": true` on that entry. Filling a HELD entity is the ordinary case; overruling the detector is not, and the two should not look the same in a diff. """ from __future__ import annotations import argparse, json from pathlib import Path def main() -> int: ap = argparse.ArgumentParser() ap.add_argument("--entities", required=True) ap.add_argument("--overrides", required=True, help='JSON: {"": {"gender": "f", "why": "...", ' '"correcting": true}} — `why` is required, `correcting` only when ' 'the map already holds a different gender') ap.add_argument("--out", required=True) a = ap.parse_args() ents = json.loads(Path(a.entities).read_text()) blob = json.loads(Path(a.overrides).read_text()) ov = {k: v for k, v in blob.items() if not k.startswith("_")} # surface -> [(work, key)] where: dict[str, list[tuple[str, str]]] = {} for w, blk in ents.items(): for k, e in blk["entities"].items(): where.setdefault(e.get("surface") or k, []).append((w, k)) missing = [n for n in ov if n not in where] if missing: print(f"== REFUSING: {len(missing)} override(s) name no entity in the map: {missing}") print(" A misspelled override that silently does nothing leaves the entity") print(" mis-gendered AND looks like it was handled.") return 1 no_why = [n for n, v in ov.items() if not (v.get("why") or "").strip()] if no_why: print(f"== REFUSING: no `why` on {no_why}. An override without its evidence is a guess.") return 1 changed = filled = 0 for name, spec in sorted(ov.items()): g = spec["gender"] for w, k in where[name]: cur = ents[w]["entities"][k].get("gender") if cur == g: continue if cur and not spec.get("correcting"): print(f"== REFUSING: {name} in {w} already reads {cur!r} and the override says " f"{g!r} without \"correcting\": true. Overruling the detector is not the " f"same act as filling a held entity.") return 1 ents[w]["entities"][k]["gender"] = g ents[w]["entities"][k]["gender_source"] = "hand-verified" changed += cur is not None filled += cur is None print(f" {name:<14} {w:<26} {str(cur):>6} -> {g} " f"{'CORRECTION' if cur else 'filled held'}") Path(a.out).write_text(json.dumps(ents, ensure_ascii=False, indent=1), encoding="utf-8") print(f"\n {filled} held entity/entities filled, {changed} detector reading(s) corrected") print(f" wrote {a.out}") return 0 if __name__ == "__main__": raise SystemExit(main())