#!/usr/bin/env python3
"""Bank batch-287: Rule 245 body-part pronoun expansion."""

from __future__ import annotations

import datetime as dt
import json
import re
import sqlite3

from process_ledger import ensure_schema, record_event

DB = "/mnt/donto-data/donto-resources/research/translation-training/kuku-yalanji-runpod-2026-06-30/synthetic/claude-synthetic-v1-2026-07-02/synthetic.db"
LABEL = "batch-287"
NOW = dt.datetime.now(dt.timezone.utc).replace(microsecond=0).isoformat().replace("+00:00", "Z")
PROCESS_LOG = "process_logs/batch-287-body-part-pronoun-expansion-after-rule244-process.md"

TARGET_HEADS = {
    "bandin": "love handles",
    "bari": "chin",
    "bibar": "shin",
    "birri": "fingernail",
    "jiba": "liver",
    "jandal": "leg",
    "jibar": "stomach",
    "kangkul": "cheek",
    "kuwinyu": "posterior",
    "waba": "thigh",
}
PRONOUN_SUPPORT = {
    "ngali": "1du.EXCL.NOM",
    "ngaliny": "1du.INCL.NOM",
    "yubal": "2du.NOM",
    "nganjin": "1pl.EXCL.NOM",
    "yurra": "2pl.NOM",
}
SUPPORT = set(PRONOUN_SUPPORT) | {"yaluy", "yinyay", "nyajil"}
DENY_KUKU = {
    # Rejected body-part candidates, homographs, and source-prose traps.
    "junkan", "jibarr", "kangku", "kangkul kulbal", "juju", "jibabadi-y", "jibabadi",
    "jiba-badi", "bibir", "munju", "yiman", "dabulkurra", "bari-bari",
    # Non-selected supports and older Rule 234/235 frames.
    "yinya", "ngayu", "yundu", "nyulu", "ngana", "jana", "bula", "yulu", "nyajin",
    # Pronoun/demonstrative case, perception compounds, and support inflection.
    "ngalingan", "ngalinyan", "yubalan", "nganjinan", "yurrangan", "ngalininy",
    "ngalinyiny", "yubaliny", "nganjininy", "yurraniny", "ngalinka", "ngalinyanka",
    "yubalanka", "nganjinanka", "yurranka", "ngalinda", "ngalinyanda", "yubalanda",
    "nganjinanda", "yurranda", "ngalinji", "ngalinyanji", "yubalanji", "nganjinanji",
    "yurranji", "ngalinga", "ngalinyanga", "yubalanga", "nganjinanga", "yurranga",
    "yinyangka", "yinyayngka", "yinyayanka", "yinyamba", "yinyayanda", "yinyamuny",
    "yinyamundu", "yinyanji", "yinyarriny", "nyaji-nka", "nyaji-nyaku",
    "nyaji-l-nyaji", "nyajiny",
    # Recent fields remain closed.
    "mabil", "karrirrangkal", "jalngarr", "jinali", "kangku", "yalmba",
    "kambal-kambal", "jalkar", "janku", "jamba", "bakarr", "bayan", "bira",
    "jiji", "kilili", "wurun", "jakway", "yulal", "kararr", "bilangkurr",
    "kubija", "mujarrka", "barrbal", "wuyumbu", "bayil", "dalmbal", "balarri",
    "bilmbin", "jalkay", "jijirr", "birmba", "jamarbina", "nili-nili", "mabal",
    "wungku", "kuludu", "kururrungka", "karrambil", "yinjul", "kadanji",
}
DENY_ENGLISH = (
    "own", "owner", "ownership", "my ", "your ", "his ", "her ", "their ",
    "possess", "possession", "body state", "pain", "hurt", "hurts", "sore", "ill",
    "illness", "sick", "medicine", "heal", "healing", "blood", "urine", "fluid",
    "taboo", "ritual", "scar", "initiation", "animal", "beast", "dialect", "homograph",
    "walk", "walking", "move", "movement", "injury", "injured", "eat", "eating",
    "butcher", "butchery", "emotion", "glad", "guts", "insides", "organs", "jaw",
    "ankle", "toenail", "hip", "hinterland", "valley", "slap", "rude", "sitting",
    "case", "compound", "loan", "event",
)


def js(values: list[str]) -> str:
    return json.dumps(values, ensure_ascii=False)


def row(
    seq: int,
    english: str,
    kuku: str,
    analysis: str,
    frame: str,
    words: list[str],
    evidence: str,
) -> tuple:
    return (seq, english, kuku, analysis, frame, 3, js(words), js([]), evidence, "0.85")


EVIDENCE = {
    "bandin": "Dictionary bandin noun lines 1425-1434: gloss 'love handles', definition 'waist on the side above the hips' / 'love handles'. Rule 245 selects lexical body-part reference only; no possession, body-state, pain, or medical row. Patz 3.5 Table 3.6 sources ngali/ngaliny/yubal/nganjin/yurra; Patz 3.7 sources yaluy/yinyay; Patz 3.8.3.2 sources nyaji-l 'see'.",
    "bari": "Dictionary bari noun lines 1688-1698: gloss 'chin', definitions chin/jaw, translations chin/jaw. Rule 245 selects chin only and leaves jaw extension closed.",
    "bibar": "Dictionary bibar noun lines 2131-2140: gloss 'shin', definition shin/ankle, translations shin/ankle. Rule 245 selects shin only and leaves ankle extension closed.",
    "birri": "Dictionary birri noun lines 3190-3199: gloss 'fingernail', definition fingernail/toenail, translations fingernail/toenail. Rule 245 selects fingernail only and leaves toenail extension closed.",
    "jiba": "Dictionary jiba noun lines 8611-8623: gloss 'liver', definitions liver/insides/guts, translations liver/insides/guts/organs. Rule 245 selects liver only and leaves insides, guts, organs, emotion, butchery, eating, medical, and compound rows closed.",
    "jandal": "Dictionary jandal noun lines 7926-7934: gloss 'leg', definition lower limb/leg, translation leg. Rule 245 selects leg reference/perception only and leaves walking, movement, injury, possession, and event rows closed.",
    "jibar": "Dictionary jibar noun lines 8728-8736: gloss/definition/translation 'stomach'. Nearby jibarr locational 'hinterland' remains closed. Rule 245 selects stomach only.",
    "kangkul": "Dictionary kangkul noun lines 13276-13284: gloss/definition/translation 'cheek'. Nearby kangkul kulbal 'slap someone\\'s cheek' stays closed; kangku valley drift stays closed.",
    "kuwinyu": "Dictionary kuwinyu noun lines 16806-16818: gloss 'posterior', definition 'posterior (not a rude term)', translation posterior. Rule 245 keeps neutral posterior reference/perception only.",
    "waba": "Dictionary waba noun lines 24975-24984: gloss 'thigh', definition thigh/hip, translations thigh/hip. Rule 245 selects thigh only and leaves hip extension closed.",
}


ROWS = [
    row(1, "We two, not you, see the love handles here.", "Ngali bandin yaluy nyajil.", "ngali bandin yaluy nyajil | 1du.EXCL.NOM love.handles here see.NPST", "r245_1du_excl_body_part_here_see_bandin", ["ngali", "bandin", "yaluy", "nyajil"], EVIDENCE["bandin"]),
    row(2, "We two, not you, see the chin here.", "Ngali bari yaluy nyajil.", "ngali bari yaluy nyajil | 1du.EXCL.NOM chin here see.NPST", "r245_1du_excl_body_part_here_see_bari", ["ngali", "bari", "yaluy", "nyajil"], EVIDENCE["bari"]),
    row(3, "We two, not you, see the shin here.", "Ngali bibar yaluy nyajil.", "ngali bibar yaluy nyajil | 1du.EXCL.NOM shin here see.NPST", "r245_1du_excl_body_part_here_see_bibar", ["ngali", "bibar", "yaluy", "nyajil"], EVIDENCE["bibar"]),
    row(4, "We two, not you, see the fingernail here.", "Ngali birri yaluy nyajil.", "ngali birri yaluy nyajil | 1du.EXCL.NOM fingernail here see.NPST", "r245_1du_excl_body_part_here_see_birri", ["ngali", "birri", "yaluy", "nyajil"], EVIDENCE["birri"]),
    row(5, "We two, not you, see the liver here.", "Ngali jiba yaluy nyajil.", "ngali jiba yaluy nyajil | 1du.EXCL.NOM liver here see.NPST", "r245_1du_excl_body_part_here_see_jiba", ["ngali", "jiba", "yaluy", "nyajil"], EVIDENCE["jiba"]),
    row(6, "We two, not you, see the leg here.", "Ngali jandal yaluy nyajil.", "ngali jandal yaluy nyajil | 1du.EXCL.NOM leg here see.NPST", "r245_1du_excl_body_part_here_see_jandal", ["ngali", "jandal", "yaluy", "nyajil"], EVIDENCE["jandal"]),
    row(7, "We two, not you, see the stomach here.", "Ngali jibar yaluy nyajil.", "ngali jibar yaluy nyajil | 1du.EXCL.NOM stomach here see.NPST", "r245_1du_excl_body_part_here_see_jibar", ["ngali", "jibar", "yaluy", "nyajil"], EVIDENCE["jibar"]),
    row(8, "We two, not you, see the cheek here.", "Ngali kangkul yaluy nyajil.", "ngali kangkul yaluy nyajil | 1du.EXCL.NOM cheek here see.NPST", "r245_1du_excl_body_part_here_see_kangkul", ["ngali", "kangkul", "yaluy", "nyajil"], EVIDENCE["kangkul"]),
    row(9, "We two, not you, see the posterior here.", "Ngali kuwinyu yaluy nyajil.", "ngali kuwinyu yaluy nyajil | 1du.EXCL.NOM posterior here see.NPST", "r245_1du_excl_body_part_here_see_kuwinyu", ["ngali", "kuwinyu", "yaluy", "nyajil"], EVIDENCE["kuwinyu"]),
    row(10, "We two, not you, see the thigh here.", "Ngali waba yaluy nyajil.", "ngali waba yaluy nyajil | 1du.EXCL.NOM thigh here see.NPST", "r245_1du_excl_body_part_here_see_waba", ["ngali", "waba", "yaluy", "nyajil"], EVIDENCE["waba"]),
    row(11, "You and I see the love handles there.", "Ngaliny bandin yinyay nyajil.", "ngaliny bandin yinyay nyajil | 1du.INCL.NOM love.handles there see.NPST", "r245_1du_incl_body_part_there_see_bandin", ["ngaliny", "bandin", "yinyay", "nyajil"], EVIDENCE["bandin"]),
    row(12, "You and I see the chin there.", "Ngaliny bari yinyay nyajil.", "ngaliny bari yinyay nyajil | 1du.INCL.NOM chin there see.NPST", "r245_1du_incl_body_part_there_see_bari", ["ngaliny", "bari", "yinyay", "nyajil"], EVIDENCE["bari"]),
    row(13, "You and I see the shin there.", "Ngaliny bibar yinyay nyajil.", "ngaliny bibar yinyay nyajil | 1du.INCL.NOM shin there see.NPST", "r245_1du_incl_body_part_there_see_bibar", ["ngaliny", "bibar", "yinyay", "nyajil"], EVIDENCE["bibar"]),
    row(14, "You and I see the fingernail there.", "Ngaliny birri yinyay nyajil.", "ngaliny birri yinyay nyajil | 1du.INCL.NOM fingernail there see.NPST", "r245_1du_incl_body_part_there_see_birri", ["ngaliny", "birri", "yinyay", "nyajil"], EVIDENCE["birri"]),
    row(15, "You and I see the liver there.", "Ngaliny jiba yinyay nyajil.", "ngaliny jiba yinyay nyajil | 1du.INCL.NOM liver there see.NPST", "r245_1du_incl_body_part_there_see_jiba", ["ngaliny", "jiba", "yinyay", "nyajil"], EVIDENCE["jiba"]),
    row(16, "You and I see the leg there.", "Ngaliny jandal yinyay nyajil.", "ngaliny jandal yinyay nyajil | 1du.INCL.NOM leg there see.NPST", "r245_1du_incl_body_part_there_see_jandal", ["ngaliny", "jandal", "yinyay", "nyajil"], EVIDENCE["jandal"]),
    row(17, "You and I see the stomach there.", "Ngaliny jibar yinyay nyajil.", "ngaliny jibar yinyay nyajil | 1du.INCL.NOM stomach there see.NPST", "r245_1du_incl_body_part_there_see_jibar", ["ngaliny", "jibar", "yinyay", "nyajil"], EVIDENCE["jibar"]),
    row(18, "You and I see the cheek there.", "Ngaliny kangkul yinyay nyajil.", "ngaliny kangkul yinyay nyajil | 1du.INCL.NOM cheek there see.NPST", "r245_1du_incl_body_part_there_see_kangkul", ["ngaliny", "kangkul", "yinyay", "nyajil"], EVIDENCE["kangkul"]),
    row(19, "You and I see the posterior there.", "Ngaliny kuwinyu yinyay nyajil.", "ngaliny kuwinyu yinyay nyajil | 1du.INCL.NOM posterior there see.NPST", "r245_1du_incl_body_part_there_see_kuwinyu", ["ngaliny", "kuwinyu", "yinyay", "nyajil"], EVIDENCE["kuwinyu"]),
    row(20, "You and I see the thigh there.", "Ngaliny waba yinyay nyajil.", "ngaliny waba yinyay nyajil | 1du.INCL.NOM thigh there see.NPST", "r245_1du_incl_body_part_there_see_waba", ["ngaliny", "waba", "yinyay", "nyajil"], EVIDENCE["waba"]),
    row(21, "You two see the love handles here.", "Yubal bandin yaluy nyajil.", "yubal bandin yaluy nyajil | 2du.NOM love.handles here see.NPST", "r245_2du_body_part_here_see_bandin", ["yubal", "bandin", "yaluy", "nyajil"], EVIDENCE["bandin"]),
    row(22, "You two see the chin here.", "Yubal bari yaluy nyajil.", "yubal bari yaluy nyajil | 2du.NOM chin here see.NPST", "r245_2du_body_part_here_see_bari", ["yubal", "bari", "yaluy", "nyajil"], EVIDENCE["bari"]),
    row(23, "You two see the shin here.", "Yubal bibar yaluy nyajil.", "yubal bibar yaluy nyajil | 2du.NOM shin here see.NPST", "r245_2du_body_part_here_see_bibar", ["yubal", "bibar", "yaluy", "nyajil"], EVIDENCE["bibar"]),
    row(24, "You two see the fingernail here.", "Yubal birri yaluy nyajil.", "yubal birri yaluy nyajil | 2du.NOM fingernail here see.NPST", "r245_2du_body_part_here_see_birri", ["yubal", "birri", "yaluy", "nyajil"], EVIDENCE["birri"]),
    row(25, "You two see the liver here.", "Yubal jiba yaluy nyajil.", "yubal jiba yaluy nyajil | 2du.NOM liver here see.NPST", "r245_2du_body_part_here_see_jiba", ["yubal", "jiba", "yaluy", "nyajil"], EVIDENCE["jiba"]),
    row(26, "You two see the leg here.", "Yubal jandal yaluy nyajil.", "yubal jandal yaluy nyajil | 2du.NOM leg here see.NPST", "r245_2du_body_part_here_see_jandal", ["yubal", "jandal", "yaluy", "nyajil"], EVIDENCE["jandal"]),
    row(27, "You two see the stomach here.", "Yubal jibar yaluy nyajil.", "yubal jibar yaluy nyajil | 2du.NOM stomach here see.NPST", "r245_2du_body_part_here_see_jibar", ["yubal", "jibar", "yaluy", "nyajil"], EVIDENCE["jibar"]),
    row(28, "You two see the cheek here.", "Yubal kangkul yaluy nyajil.", "yubal kangkul yaluy nyajil | 2du.NOM cheek here see.NPST", "r245_2du_body_part_here_see_kangkul", ["yubal", "kangkul", "yaluy", "nyajil"], EVIDENCE["kangkul"]),
    row(29, "You two see the posterior here.", "Yubal kuwinyu yaluy nyajil.", "yubal kuwinyu yaluy nyajil | 2du.NOM posterior here see.NPST", "r245_2du_body_part_here_see_kuwinyu", ["yubal", "kuwinyu", "yaluy", "nyajil"], EVIDENCE["kuwinyu"]),
    row(30, "You two see the thigh here.", "Yubal waba yaluy nyajil.", "yubal waba yaluy nyajil | 2du.NOM thigh here see.NPST", "r245_2du_body_part_here_see_waba", ["yubal", "waba", "yaluy", "nyajil"], EVIDENCE["waba"]),
    row(31, "We all, not you, see the love handles there.", "Nganjin bandin yinyay nyajil.", "nganjin bandin yinyay nyajil | 1pl.EXCL.NOM love.handles there see.NPST", "r245_1pl_excl_body_part_there_see_bandin", ["nganjin", "bandin", "yinyay", "nyajil"], EVIDENCE["bandin"]),
    row(32, "We all, not you, see the chin there.", "Nganjin bari yinyay nyajil.", "nganjin bari yinyay nyajil | 1pl.EXCL.NOM chin there see.NPST", "r245_1pl_excl_body_part_there_see_bari", ["nganjin", "bari", "yinyay", "nyajil"], EVIDENCE["bari"]),
    row(33, "We all, not you, see the shin there.", "Nganjin bibar yinyay nyajil.", "nganjin bibar yinyay nyajil | 1pl.EXCL.NOM shin there see.NPST", "r245_1pl_excl_body_part_there_see_bibar", ["nganjin", "bibar", "yinyay", "nyajil"], EVIDENCE["bibar"]),
    row(34, "We all, not you, see the fingernail there.", "Nganjin birri yinyay nyajil.", "nganjin birri yinyay nyajil | 1pl.EXCL.NOM fingernail there see.NPST", "r245_1pl_excl_body_part_there_see_birri", ["nganjin", "birri", "yinyay", "nyajil"], EVIDENCE["birri"]),
    row(35, "We all, not you, see the liver there.", "Nganjin jiba yinyay nyajil.", "nganjin jiba yinyay nyajil | 1pl.EXCL.NOM liver there see.NPST", "r245_1pl_excl_body_part_there_see_jiba", ["nganjin", "jiba", "yinyay", "nyajil"], EVIDENCE["jiba"]),
    row(36, "We all, not you, see the leg there.", "Nganjin jandal yinyay nyajil.", "nganjin jandal yinyay nyajil | 1pl.EXCL.NOM leg there see.NPST", "r245_1pl_excl_body_part_there_see_jandal", ["nganjin", "jandal", "yinyay", "nyajil"], EVIDENCE["jandal"]),
    row(37, "We all, not you, see the stomach there.", "Nganjin jibar yinyay nyajil.", "nganjin jibar yinyay nyajil | 1pl.EXCL.NOM stomach there see.NPST", "r245_1pl_excl_body_part_there_see_jibar", ["nganjin", "jibar", "yinyay", "nyajil"], EVIDENCE["jibar"]),
    row(38, "We all, not you, see the cheek there.", "Nganjin kangkul yinyay nyajil.", "nganjin kangkul yinyay nyajil | 1pl.EXCL.NOM cheek there see.NPST", "r245_1pl_excl_body_part_there_see_kangkul", ["nganjin", "kangkul", "yinyay", "nyajil"], EVIDENCE["kangkul"]),
    row(39, "We all, not you, see the posterior there.", "Nganjin kuwinyu yinyay nyajil.", "nganjin kuwinyu yinyay nyajil | 1pl.EXCL.NOM posterior there see.NPST", "r245_1pl_excl_body_part_there_see_kuwinyu", ["nganjin", "kuwinyu", "yinyay", "nyajil"], EVIDENCE["kuwinyu"]),
    row(40, "We all, not you, see the thigh there.", "Nganjin waba yinyay nyajil.", "nganjin waba yinyay nyajil | 1pl.EXCL.NOM thigh there see.NPST", "r245_1pl_excl_body_part_there_see_waba", ["nganjin", "waba", "yinyay", "nyajil"], EVIDENCE["waba"]),
    row(41, "You all see the love handles here.", "Yurra bandin yaluy nyajil.", "yurra bandin yaluy nyajil | 2pl.NOM love.handles here see.NPST", "r245_2pl_body_part_here_see_bandin", ["yurra", "bandin", "yaluy", "nyajil"], EVIDENCE["bandin"]),
    row(42, "You all see the chin here.", "Yurra bari yaluy nyajil.", "yurra bari yaluy nyajil | 2pl.NOM chin here see.NPST", "r245_2pl_body_part_here_see_bari", ["yurra", "bari", "yaluy", "nyajil"], EVIDENCE["bari"]),
    row(43, "You all see the shin here.", "Yurra bibar yaluy nyajil.", "yurra bibar yaluy nyajil | 2pl.NOM shin here see.NPST", "r245_2pl_body_part_here_see_bibar", ["yurra", "bibar", "yaluy", "nyajil"], EVIDENCE["bibar"]),
    row(44, "You all see the fingernail here.", "Yurra birri yaluy nyajil.", "yurra birri yaluy nyajil | 2pl.NOM fingernail here see.NPST", "r245_2pl_body_part_here_see_birri", ["yurra", "birri", "yaluy", "nyajil"], EVIDENCE["birri"]),
    row(45, "You all see the liver here.", "Yurra jiba yaluy nyajil.", "yurra jiba yaluy nyajil | 2pl.NOM liver here see.NPST", "r245_2pl_body_part_here_see_jiba", ["yurra", "jiba", "yaluy", "nyajil"], EVIDENCE["jiba"]),
    row(46, "You all see the leg here.", "Yurra jandal yaluy nyajil.", "yurra jandal yaluy nyajil | 2pl.NOM leg here see.NPST", "r245_2pl_body_part_here_see_jandal", ["yurra", "jandal", "yaluy", "nyajil"], EVIDENCE["jandal"]),
    row(47, "You all see the stomach here.", "Yurra jibar yaluy nyajil.", "yurra jibar yaluy nyajil | 2pl.NOM stomach here see.NPST", "r245_2pl_body_part_here_see_jibar", ["yurra", "jibar", "yaluy", "nyajil"], EVIDENCE["jibar"]),
    row(48, "You all see the cheek here.", "Yurra kangkul yaluy nyajil.", "yurra kangkul yaluy nyajil | 2pl.NOM cheek here see.NPST", "r245_2pl_body_part_here_see_kangkul", ["yurra", "kangkul", "yaluy", "nyajil"], EVIDENCE["kangkul"]),
    row(49, "You all see the posterior here.", "Yurra kuwinyu yaluy nyajil.", "yurra kuwinyu yaluy nyajil | 2pl.NOM posterior here see.NPST", "r245_2pl_body_part_here_see_kuwinyu", ["yurra", "kuwinyu", "yaluy", "nyajil"], EVIDENCE["kuwinyu"]),
    row(50, "You all see the thigh here.", "Yurra waba yaluy nyajil.", "yurra waba yaluy nyajil | 2pl.NOM thigh here see.NPST", "r245_2pl_body_part_here_see_waba", ["yurra", "waba", "yaluy", "nyajil"], EVIDENCE["waba"]),
]


def has_token(low: str, token: str) -> bool:
    return re.search(rf"(?<![a-z-]){re.escape(token)}(?![a-z-])", low) is not None


def has_target_morphology(low: str, head: str) -> bool:
    suffixes = "ngka|ngku|angka|nga|ngu|ba|mba|mbu|muny|darr|burr|bu|ka|nka|nku|ji|jirr|ku|mundu|amu|mu|iji|nda|ndu|anda|undu|unji|nji|kari|baka|baja|bungal|bungan|mal|man|rriny|karra|manda|bulal"
    return re.search(rf"(?<![a-z-]){re.escape(head)}-(?:{suffixes})\b", low) is not None


def english_has_denied(english_low: str, denied: str) -> bool:
    return denied in english_low if " " in denied else re.search(rf"\b{re.escape(denied.strip())}\b", english_low) is not None


def scan_static() -> list[str]:
    issues: list[str] = []
    if len(ROWS) != 50:
        issues.append(f"{LABEL}: expected 50 rows, found {len(ROWS)}")
    seen_ku: set[str] = set()
    for expected, row_data in enumerate(ROWS, start=1):
        seq, english, kuku, analysis, frame, _tier, words_used, loans_used, evidence, confidence = row_data
        label = f"{LABEL}.{seq:02d}"
        low = kuku.lower()
        english_low = english.lower()
        words = json.loads(words_used)
        if seq != expected:
            issues.append(f"{label}: sequence mismatch; expected {expected}")
        if not english[:1].isupper() or english[-1:] not in ".!?":
            issues.append(f"{label}: English punctuation/capitalization failure")
        if " | " not in analysis:
            issues.append(f"{label}: analysis missing gloss separator")
        if not frame.startswith("r245_"):
            issues.append(f"{label}: Rule 245 frame must start with r245_")
        if low in seen_ku:
            issues.append(f"{label}: duplicate Kuku surface inside batch")
        seen_ku.add(low)
        if json.loads(loans_used):
            issues.append(f"{label}: loans_used must be empty")
        denied = [bad for bad in DENY_KUKU if has_token(low, bad)]
        if denied:
            issues.append(f"{label}: denied Kuku token(s) present: {denied}")
        targets = [head for head in TARGET_HEADS if has_token(low, head)]
        if len(targets) != 1:
            issues.append(f"{label}: expected exactly one Rule 245 target noun, found {targets}")
        for head in targets:
            selected = TARGET_HEADS[head]
            dotted = selected.replace("-", ".").replace(" ", ".")
            if head not in words:
                issues.append(f"{label}: target head {head} missing from words_used")
            if selected not in english_low:
                issues.append(f"{label}: English must include selected sense {selected!r}")
            if selected not in analysis and dotted not in analysis:
                issues.append(f"{label}: analysis must gloss {head} as {selected}")
            if has_target_morphology(low, head):
                issues.append(f"{label}: target head {head} has forbidden morphology")
        for support in SUPPORT:
            if has_token(low, support) and support not in words:
                issues.append(f"{label}: support {support} missing from words_used")
        pronouns = [pronoun for pronoun in PRONOUN_SUPPORT if has_token(low, pronoun)]
        if len(pronouns) != 1:
            issues.append(f"{label}: rows require exactly one selected bare nominative pronoun, found {pronouns}")
        if 1 <= seq <= 10 and (not low.startswith("ngali ") or not low.endswith(" yaluy nyajil.") or len(low.split()) != 4):
            issues.append(f"{label}: rows 1-10 require Ngali TARGET yaluy nyajil")
        if 11 <= seq <= 20 and (not low.startswith("ngaliny ") or not low.endswith(" yinyay nyajil.") or len(low.split()) != 4):
            issues.append(f"{label}: rows 11-20 require Ngaliny TARGET yinyay nyajil")
        if 21 <= seq <= 30 and (not low.startswith("yubal ") or not low.endswith(" yaluy nyajil.") or len(low.split()) != 4):
            issues.append(f"{label}: rows 21-30 require Yubal TARGET yaluy nyajil")
        if 31 <= seq <= 40 and (not low.startswith("nganjin ") or not low.endswith(" yinyay nyajil.") or len(low.split()) != 4):
            issues.append(f"{label}: rows 31-40 require Nganjin TARGET yinyay nyajil")
        if 41 <= seq <= 50 and (not low.startswith("yurra ") or not low.endswith(" yaluy nyajil.") or len(low.split()) != 4):
            issues.append(f"{label}: rows 41-50 require Yurra TARGET yaluy nyajil")
        if any(english_has_denied(english_low, bad) for bad in DENY_ENGLISH):
            issues.append(f"{label}: denied body-part source-prose/action/use field in English")
        if not evidence or not confidence:
            issues.append(f"{label}: missing evidence/confidence")
    return issues


def main() -> int:
    issues = scan_static()
    if issues:
        print("pre-bank scan failed:")
        for issue in issues:
            print(f"  - {issue}")
        return 1

    db = sqlite3.connect(DB)
    db.execute("PRAGMA foreign_keys = ON")
    db.execute("BEGIN")
    ensure_schema(db)
    existing = db.execute("SELECT id FROM batches WHERE label=?", (LABEL,)).fetchone()
    if existing:
        batch_id = existing[0]
        accepted = db.execute(
            "SELECT COUNT(*) FROM sentences WHERE batch_id=? AND status IN ('accepted','revised')",
            (batch_id,),
        ).fetchone()[0]
        if accepted:
            raise SystemExit(f"{LABEL} already has accepted/revised rows; refusing to overwrite")
        db.execute("DELETE FROM reviews WHERE sentence_id IN (SELECT id FROM sentences WHERE batch_id=?)", (batch_id,))
        db.execute("DELETE FROM lessons WHERE batch_id=?", (batch_id,))
        db.execute("DELETE FROM sentences WHERE batch_id=?", (batch_id,))
    else:
        db.execute(
            "INSERT INTO batches(label, cheatsheet_version, guide_version, notes, created_at) VALUES(?,?,?,?,?)",
            (LABEL, "Rule 245 draft", "operator-guide-2026-07-09", "draft under review", NOW),
        )
        batch_id = db.execute("SELECT id FROM batches WHERE label=?", (LABEL,)).fetchone()[0]

    collisions: list[str] = []
    for seq, _english, kuku, *_rest in ROWS:
        found = db.execute(
            """
            SELECT b.label, s.seq, s.status
            FROM sentences s JOIN batches b ON b.id=s.batch_id
            WHERE lower(s.kuku)=lower(?) AND s.status IN ('accepted','revised','under_review')
            """,
            (kuku,),
        ).fetchall()
        if found:
            collisions.append(f"{seq}: {kuku} -> {found}")
    if collisions:
        raise SystemExit("collision scan failed:\n" + "\n".join(collisions))

    db.executemany(
        """
        INSERT INTO sentences(
          batch_id, seq, english, kuku, analysis, frame, tier, words_used,
          loans_used, evidence, status, confidence, created_at
        )
        VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?)
        """,
        [
            (
                batch_id,
                seq,
                english,
                kuku,
                analysis,
                frame,
                tier,
                words_used,
                loans_used,
                evidence,
                "under_review",
                confidence,
                NOW,
            )
            for seq, english, kuku, analysis, frame, tier, words_used, loans_used, evidence, confidence in ROWS
        ],
    )
    record_event(
        db,
        batch_label=LABEL,
        phase="source",
        event_type="source-readback",
        actor="claude-main",
        detail="Rule 245 source pass retained the ten Rule 234 body-part target nouns; checked dictionary entries and line evidence for bandin, bari, bibar, birri, jiba, jandal, jibar, kangkul, kuwinyu, and waba; checked Patz 3.5 Table 3.6 for ngali, ngaliny, yubal, nganjin, and yurra; Patz 3.7 for yaluy/yinyay; and Patz 3.8.3.2 for nyaji-l.",
        artifact_path=PROCESS_LOG,
        created_at=NOW,
    )
    record_event(
        db,
        batch_label=LABEL,
        phase="planning",
        event_type="construction-scope",
        actor="claude-main",
        detail="Selected Rule 245 body-part pronoun expansion after Rule 244: same ten Rule 234 target nouns in five zero-collision nominative-pronoun perception bands. No new body-part lexemes, possession, body-state, pain, illness, medical, event, homograph, target case, pronoun case, or perception-compound semantics opened.",
        artifact_path=PROCESS_LOG,
        created_at=NOW,
    )
    record_event(
        db,
        batch_label=LABEL,
        phase="preflight",
        event_type="coverage-audit",
        actor="claude-main",
        detail="Token-aware pre-batch coverage after Rule 244: bandin 10, bari 14, bibar 20, birri 18, jiba 51, jandal 34, jibar 52, kangkul 14, kuwinyu 10, waba 14; pronouns ngali 101, ngaliny 35, yubal 49, nganjin 51, yurra 94.",
        command="python3 token-aware coverage audit over accepted/revised rows",
        artifact_path=PROCESS_LOG,
        created_at=NOW,
    )
    record_event(
        db,
        batch_label=LABEL,
        phase="preflight",
        event_type="duplicate-preflight",
        actor="claude-main",
        detail="Exact duplicate preflight over all 50 planned Rule 245 body-part pronoun-expansion perception surfaces returned zero accepted, revised, or under-review collisions.",
        command="python3 batch_scripts/bank_287.py internal collision scan",
        output_excerpt="0",
        artifact_path=PROCESS_LOG,
        created_at=NOW,
    )
    record_event(
        db,
        batch_label=LABEL,
        phase="preflight",
        event_type="static-guard-pass",
        actor="claude-main",
        detail="Static guards passed: 50 rows, exactly one Rule 245 target noun per row, exactly one selected bare nominative pronoun per row, no loans, no target suffixes, no excluded possession/body-state/pain/illness/medicine/body-fluid/taboo/event/homograph/source-prose drift, no pronoun case, no perception compound, and five planned frame bands only.",
        command="python3 batch_scripts/bank_287.py",
        artifact_path="batch_scripts/bank_287.py",
        created_at=NOW,
    )
    record_event(
        db,
        batch_label=LABEL,
        phase="bank",
        event_type="bank-success",
        actor="claude-main",
        detail="Banked 50 hand-authored Rule 245 body-part pronoun-expansion perception rows as under_review.",
        command="python3 batch_scripts/bank_287.py",
        artifact_path="batch_scripts/bank_287.py",
        created_at=NOW,
    )
    db.commit()
    print(f"banked {LABEL}: 50 under_review rows")
    return 0


if __name__ == "__main__":
    raise SystemExit(main())
